Repository: xmtp/smart-contracts
Pull request #56: feat: PayerReportManager
Author: login: deluca-mike
Labels: name: enhancement
description: New feature or request
Created: 2025-05-06T08:32:12Z | Merged: 2025-05-14T07:09:27Z

- added ERC712 and ERC5267 abstractions
- added `getLeafCount` to the `SequentialMerkleProofs` library to extract the leaf count from a proof, as it is needed to update the progress of settling a payer report
- added `PayerReportManager`
- `PayerRegistry.settleUsage` updated to return fees settled in the subset, as it will be needed for distribution progress tracking later

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced the Payer Report Manager contract, enabling secure submission and settlement of payer reports with multi-signature verification and Merkle proof support.
  - Added interfaces and contracts supporting EIP-712 and EIP-5267 domain separation for typed structured data signing.
  - Provided new interfaces for node and payer registries, supporting modular and upgradeable design.

- **Refactor**
  - Updated settlement functions to use a single array of payer-fee structs instead of separate arrays, simplifying usage and reducing input errors.

- **Tests**
  - Added comprehensive unit tests for the Payer Report Manager, including signature verification, settlement, migration, and upgradeability.
  - Introduced new test harnesses and mock contracts to facilitate robust testing of core features.

- **Chores**
  - Improved the Slither analysis script to ensure clean SARIF report generation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/package.json b/package.json
index 0e25c7b8..aab9440c 100644
--- a/package.json
+++ b/package.json
@@ -20,7 +20,7 @@
         "lint-staged": "lint-staged",
         "prettier": "prettier --write 'script/**/*.sol' 'src/**/*.sol' 'test/**/*.sol'",
         "prettier-check": "prettier --check 'script/**/*.sol' 'src/**/*.sol' 'test/**/*.sol'",
-        "slither": "forge build --build-info --skip '*/test/**' --skip '*/script/**' --force && slither --sarif output.sarif --compile-force-framework foundry --ignore-compile --config-file slither.config.json --fail-high .",
+        "slither": "rm -rf output.sarif && forge build --build-info --skip '*/test/**' --skip '*/script/**' --force && slither --sarif output.sarif --compile-force-framework foundry --ignore-compile --config-file slither.config.json --fail-high .",
         "solhint": "solhint -f stylish 'src/**/*.sol'",
         "solhint-fix": "solhint --fix 'src/**/*.sol'",
         "test": "forge test"
diff --git a/src/abstract/ERC5267.sol b/src/abstract/ERC5267.sol
new file mode 100644
index 00000000..aba6801c
--- /dev/null
+++ b/src/abstract/ERC5267.sol
@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { IERC5267 } from "./interfaces/IERC5267.sol";
+
+import { ERC712 } from "./ERC712.sol";
+
+/**
+ * @title An abstract implementation of EIP-5267 for domain retrieval and typed structured data hashing and signing.
+ */
+abstract contract ERC5267 is IERC5267, ERC712 {
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructs the contract.
+     */
+    constructor() ERC712() {}
+
+    /* ============ Initialization ============ */
+
+    function _initializeERC5267() internal {
+        _initializeERC712();
+    }
+
+    /* ============ View/Pure Functions ============ */
+
+    /// @inheritdoc IERC5267
+    function eip712Domain()
+        external
+        view
+        returns (
+            bytes1 fields_,
+            string memory name_,
+            string memory version_,
+            uint256 chainId_,
+            address verifyingContract_,
+            bytes32 salt_,
+            uint256[] memory extensions_
+        )
+    {
+        return (
+            hex"0f", // 01111 (salt is not used in the domain separator)
+            _name(),
+            _version(),
+            block.chainid,
+            address(this),
+            bytes32(0),
+            new uint256[](0)
+        );
+    }
+}
diff --git a/src/abstract/ERC712.sol b/src/abstract/ERC712.sol
new file mode 100644
index 00000000..6776482b
--- /dev/null
+++ b/src/abstract/ERC712.sol
@@ -0,0 +1,100 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { IERC712 } from "./interfaces/IERC712.sol";
+
+/**
+ * @title An abstract implementation of EIP-712 for typed structured data hashing and signing.
+ */
+abstract contract ERC712 is IERC712 {
+    /* ============ Variables ============ */
+
+    /// @dev keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
+    bytes32 internal constant _EIP712_DOMAIN_HASH = 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f;
+
+    /// @dev Initial Chain ID set at deployment.
+    uint256 internal immutable _initialChainId;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.ERC712
+     * @notice The UUPS storage for the ERC712 contract.
+     * @param  initialDomainSeparator Initial EIP-712 domain separator set at intialization.
+     */
+    struct ERC712Storage {
+        bytes32 initialDomainSeparator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.ERC712")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _ERC712_STORAGE_LOCATION =
+        0xc7effa11ad597798220888e5d1ba4eeddcc8c2635d01dae8b9f958ac905c1100;
+
+    function _getERC712Storage() internal pure returns (ERC712Storage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _ERC712_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructs the EIP-712 domain separator.
+     */
+    constructor() {
+        _initialChainId = block.chainid;
+    }
+
+    /* ============ Initialization ============ */
+
+    function _initializeERC712() internal {
+        _getERC712Storage().initialDomainSeparator = _getDomainSeparator();
+    }
+
+    /* ============ View/Pure Functions ============ */
+
+    /// @inheritdoc IERC712
+    // slither-disable-next-line naming-convention
+    function DOMAIN_SEPARATOR() public view returns (bytes32 domainSeparator_) {
+        return block.chainid == _initialChainId ? _getERC712Storage().initialDomainSeparator : _getDomainSeparator();
+    }
+
+    /* ============ Internal View/Pure Functions ============ */
+
+    /**
+     * @dev    Computes the EIP-712 domain separator.
+     * @return domainSeparator_ The EIP-712 domain separator.
+     */
+    function _getDomainSeparator() internal view returns (bytes32 domainSeparator_) {
+        return
+            keccak256(
+                abi.encode(
+                    _EIP712_DOMAIN_HASH,
+                    keccak256(bytes(_name())),
+                    keccak256(bytes(_version())),
+                    block.chainid,
+                    address(this)
+                )
+            );
+    }
+
+    /**
+     * @dev    Returns the digest to be signed, via EIP-712, given an internal digest (i.e. hash struct).
+     * @param  internalDigest_ The internal digest.
+     * @return digest_         The digest to be signed.
+     */
+    function _getDigest(bytes32 internalDigest_) internal view returns (bytes32 digest_) {
+        return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR(), internalDigest_));
+    }
+
+    /**
+     * @dev The name of the contract.
+     */
+    function _name() internal view virtual returns (string memory name_);
+
+    /**
+     * @dev The version of the contract.
+     */
+    function _version() internal view virtual returns (string memory version_);
+}
diff --git a/src/abstract/interfaces/IERC5267.sol b/src/abstract/interfaces/IERC5267.sol
new file mode 100644
index 00000000..4d9cd168
--- /dev/null
+++ b/src/abstract/interfaces/IERC5267.sol
@@ -0,0 +1,46 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { IERC712 } from "./IERC712.sol";
+
+/**
+ * @title EIP-5267 extended from EIP-712.
+ * @dev   The interface as defined by EIP-5267: https://eips.ethereum.org/EIPS/eip-5267
+ */
+interface IERC5267 is IERC712 {
+    /* ============ Events ============ */
+
+    /// @notice MAY be emitted to signal that the domain could have changed.
+    event EIP712DomainChanged();
+
+    /* ============ View/Pure Functions ============ */
+
+    /**
+     * @notice Returns the fields and values that describe the domain separator used by this contract for EIP-712.
+     * @return fields_            A bit map where bit i is set to 1 if and only if domain field i is present
+     *                            (0 ≤ i ≤ 4). Bits are read from least significant to most significant, and fields are
+     *                            indexed in the order that is specified by EIP-712, identical to the order in which
+     *                            they are listed (i.e. name, version, chainId, verifyingContract, and salt).
+     * @return name_              The user readable name of signing domain.
+     * @return version_           The current major version of the signing domain.
+     * @return chainId_           The EIP-155 chain id.
+     * @return verifyingContract_ The address of the contract that will verify the signature.
+     * @return salt_              A disambiguating salt for the protocol.
+     * @return extensions_        A list of EIP numbers, each of which MUST refer to an EIP that extends EIP-712 with
+     *                              new domain fields, along with a method to obtain the value for those fields, and
+     *                              potentially conditions for inclusion. The value of fields does not affect their
+     *                              inclusion.
+     */
+    function eip712Domain()
+        external
+        view
+        returns (
+            bytes1 fields_,
+            string memory name_,
+            string memory version_,
+            uint256 chainId_,
+            address verifyingContract_,
+            bytes32 salt_,
+            uint256[] memory extensions_
+        );
+}
diff --git a/src/abstract/interfaces/IERC712.sol b/src/abstract/interfaces/IERC712.sol
new file mode 100644
index 00000000..a767035f
--- /dev/null
+++ b/src/abstract/interfaces/IERC712.sol
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+/**
+ * @title Typed structured data hashing and signing via EIP-712.
+ * @dev   The interface as defined by EIP-712: https://eips.ethereum.org/EIPS/eip-712
+ */
+interface IERC712 {
+    /* ============ View/Pure Functions ============ */
+
+    /// @notice Returns the EIP712 domain separator used in the encoding of a signed digest.
+    // slither-disable-next-line naming-convention
+    function DOMAIN_SEPARATOR() external view returns (bytes32 domainSeparator_);
+}
diff --git a/src/settlement-chain/PayerRegistry.sol b/src/settlement-chain/PayerRegistry.sol
index 47f5108f..45ab7f4d 100644
--- a/src/settlement-chain/PayerRegistry.sol
+++ b/src/settlement-chain/PayerRegistry.sol
@@ -183,19 +183,20 @@ contract PayerRegistry is IPayerRegistry, Migratable, Initializable {
     }
 
     /// @inheritdoc IPayerRegistry
-    function settleUsage(address[] calldata payers_, uint96[] calldata fees_) external onlySettler whenNotPaused {
-        require(payers_.length == fees_.length, ArrayLengthMismatch());
-
+    function settleUsage(
+        PayerFee[] calldata payerFees_
+    ) external onlySettler whenNotPaused returns (uint96 feesSettled_) {
         PayerRegistryStorage storage $ = _getPayerRegistryStorage();
         int104 totalDeposits_ = $.totalDeposits;
         uint96 totalDebt_ = $.totalDebt;
 
-        for (uint256 index_; index_ < payers_.length; ++index_) {
-            address payer_ = payers_[index_];
-            uint96 fee_ = fees_[index_];
+        for (uint256 index_; index_ < payerFees_.length; ++index_) {
+            address payer_ = payerFees_[index_].payer;
+            uint96 fee_ = payerFees_[index_].fee;
 
             emit UsageSettled(payer_, fee_);
 
+            feesSettled_ += fee_;
             totalDeposits_ -= _toInt104(fee_);
             totalDebt_ += _decreaseBalance(payer_, fee_);
         }
diff --git a/src/settlement-chain/PayerReportManager.sol b/src/settlement-chain/PayerReportManager.sol
new file mode 100644
index 00000000..3f2b4481
--- /dev/null
+++ b/src/settlement-chain/PayerReportManager.sol
@@ -0,0 +1,369 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { ECDSA } from "../../lib/solady/src/utils/ECDSA.sol";
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../libraries/SequentialMerkleProofs.sol";
+
+import { IMigratable } from "../abstract/interfaces/IMigratable.sol";
+import { IParameterRegistryLike, INodeRegistryLike, IPayerRegistryLike } from "./interfaces/External.sol";
+import { IPayerReportManager } from "./interfaces/IPayerReportManager.sol";
+
+import { ERC5267 } from "../abstract/ERC5267.sol";
+import { Migratable } from "../abstract/Migratable.sol";
+
+// TODO: If a node signer can sign for more than one node, their signature for a payer report will be identical, and
+//       therefore replayable across their nodes. This may not be ideal, so it might be necessary to include the node id
+//       of the signing node in the digest.
+
+/**
+ * @title  Implementation of the Payer Report Manager.
+ * @notice This contract handles functionality for submitting and settling payer reports.
+ */
+contract PayerReportManager is IPayerReportManager, Initializable, Migratable, ERC5267 {
+    /* ============ Constants/Immutables ============ */
+
+    // solhint-disable-next-line max-line-length
+    /// @dev keccak256("PayerReport(uint256 originatorNodeId,uint256 startSequenceId,uint256 endSequenceId,bytes32 payersMerkleRoot,uint32[] nodeIds)")
+    bytes32 public constant PAYER_REPORT_TYPEHASH = 0x738138a39f1e6e7e63a210bee5419f59c5508096a6120bdf31559f9b87dc395e;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable parameterRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable nodeRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable payerRegistry;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.PayerReportManager
+     * @notice The UUPS storage for the payer report manager.
+     * @param  payerReportsByOriginator The mapping of arrays of payer reports by originator node ID.
+     */
+    struct PayerReportManagerStorage {
+        mapping(uint32 originatorId => PayerReport[] payerReports) payerReportsByOriginator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.PayerReportManager")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _PAYER_REPORT_MANAGER_STORAGE_LOCATION =
+        0x26b057ee8e4d60685198828fdf1c618ab8e36b0ab85f54a47b18319f6f718e00;
+
+    function _getPayerReportManagerStorage() internal pure returns (PayerReportManagerStorage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _PAYER_REPORT_MANAGER_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructor for the implementation contract, such that the implementation cannot be initialized.
+     * @param  parameterRegistry_ The address of the parameter registry.
+     * @param  nodeRegistry_      The address of the node registry.
+     * @param  payerRegistry_     The address of the payer registry.
+     * @dev    The parameter registry, node registry, and payer registry must not be the zero address.
+     * @dev    The parameter registry, node registry, and payer registry are immutable so that they are inlined in the
+     *         contract code, and have minimal gas cost.
+     */
+    constructor(address parameterRegistry_, address nodeRegistry_, address payerRegistry_) ERC5267() {
+        require(_isNotZero(parameterRegistry = parameterRegistry_), ZeroParameterRegistry());
+        require(_isNotZero(nodeRegistry = nodeRegistry_), ZeroNodeRegistry());
+        require(_isNotZero(payerRegistry = payerRegistry_), ZeroPayerRegistry());
+
+        _disableInitializers();
+    }
+
+    /* ============ Initialization ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function initialize() public initializer {
+        _initializeERC5267();
+    }
+
+    /* ============ Interactive Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function submit(
+        uint32 originatorNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        uint32[] calldata nodeIds_,
+        PayerReportSignature[] calldata signatures_
+    ) external returns (uint256 payerReportIndex_) {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        payerReportIndex_ = payerReports_.length;
+
+        uint32 lastSequenceId_ = payerReportIndex_ > 0 ? payerReports_[payerReportIndex_ - 1].endSequenceId : 0;
+
+        // Enforces that the start sequence ID is the last end sequence ID.
+        if (startSequenceId_ != lastSequenceId_) {
+            revert InvalidStartSequenceId(startSequenceId_, lastSequenceId_);
+        }
+
+        // Enforces that the end sequence ID is greater than or equal to the start sequence ID.
+        if (endSequenceId_ < startSequenceId_) revert InvalidSequenceIds();
+
+        // Verifies the signatures and gets the array of valid signing node IDs.
+        uint32[] memory validSigningNodeIds_ = _verifySignatures({
+            originatorNodeId_: originatorNodeId_,
+            startSequenceId_: startSequenceId_,
+            endSequenceId_: endSequenceId_,
+            payersMerkleRoot_: payersMerkleRoot_,
+            nodeIds_: nodeIds_,
+            signatures_: signatures_
+        });
+
+        payerReports_.push(
+            PayerReport({
+                startSequenceId: startSequenceId_,
+                endSequenceId: endSequenceId_,
+                feesSettled: 0,
+                offset: 0,
+                isSettled: payersMerkleRoot_ == SequentialMerkleProofs.EMPTY_TREE_ROOT,
+                payersMerkleRoot: payersMerkleRoot_,
+                nodeIds: nodeIds_
+            })
+        );
+
+        emit PayerReportSubmitted({
+            originatorNodeId: originatorNodeId_,
+            payerReportIndex: payerReportIndex_,
+            startSequenceId: startSequenceId_,
+            endSequenceId: endSequenceId_,
+            payersMerkleRoot: payersMerkleRoot_,
+            nodeIds: nodeIds_,
+            signingNodeIds: validSigningNodeIds_
+        });
+
+        if (payersMerkleRoot_ == SequentialMerkleProofs.EMPTY_TREE_ROOT) {
+            emit PayerReportSubsetSettled(originatorNodeId_, payerReportIndex_, 0, 0, 0);
+        }
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function settle(
+        uint32 originatorNodeId_,
+        uint256 payerReportIndex_,
+        bytes[] calldata payerFees_,
+        bytes32[] calldata proofElements_
+    ) external {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        if (payerReportIndex_ >= payerReports_.length) revert PayerReportIndexOutOfBounds();
+
+        PayerReport storage payerReport_ = payerReports_[payerReportIndex_];
+
+        if (payerReport_.isSettled) revert PayerReportEntirelySettled();
+
+        // Verify the payer fees provided are the next sequential payer fees from the last settled offset.
+        SequentialMerkleProofs.verify(payerReport_.payersMerkleRoot, payerReport_.offset, payerFees_, proofElements_);
+
+        payerReport_.offset += uint32(payerFees_.length);
+
+        uint32 leafCount_ = SequentialMerkleProofs.getLeafCount(proofElements_);
+
+        payerReport_.isSettled = leafCount_ == payerReport_.offset;
+
+        // Low level call which handles passing the `payerFees_` arrays as a bytes array that will be automatically
+        // decoded as the required structs by the payer registry's `settleUsage` function.
+        // slither-disable-next-line low-level-calls
+        (bool success_, bytes memory returnData_) = payerRegistry.call(
+            abi.encodeWithSelector(IPayerRegistryLike.settleUsage.selector, payerFees_)
+        );
+
+        if (!success_) revert SettleUsageFailed(returnData_);
+
+        uint96 feesSettled_ = abi.decode(returnData_, (uint96));
+
+        // slither-disable-next-line reentrancy-events
+        emit PayerReportSubsetSettled(
+            originatorNodeId_,
+            payerReportIndex_,
+            uint32(payerFees_.length),
+            leafCount_ - payerReport_.offset,
+            feesSettled_
+        );
+
+        payerReport_.feesSettled += feesSettled_;
+    }
+
+    /// @inheritdoc IMigratable
+    function migrate() external {
+        _migrate(_toAddress(_getRegistryParameter(migratorParameterKey())));
+    }
+
+    /* ============ View/Pure Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function migratorParameterKey() public pure returns (bytes memory key_) {
+        return "xmtp.payerReportManager.migrator";
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReportDigest(
+        uint32 originatorNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        uint32[] calldata nodeIds_
+    ) external view returns (bytes32 digest_) {
+        return _getPayerReportDigest(originatorNodeId_, startSequenceId_, endSequenceId_, payersMerkleRoot_, nodeIds_);
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReports(uint32 originatorNodeId_) external view returns (PayerReport[] memory payerReports_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_];
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReport(
+        uint32 originatorNodeId_,
+        uint256 payerReportIndex_
+    ) external view returns (PayerReport memory payerReport_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_][payerReportIndex_];
+    }
+
+    /* ============ Internal View/Pure Functions ============ */
+
+    function _name() internal pure override returns (string memory name_) {
+        return "PayerReportManager";
+    }
+
+    function _version() internal pure override returns (string memory version_) {
+        return "1";
+    }
+
+    /**
+     * @dev    Returns the EIP-712 digest for a payer report.
+     * @param  originatorNodeId_ The id of the node originator.
+     * @param  startSequenceId_  The start sequence ID.
+     * @param  endSequenceId_    The end sequence ID.
+     * @param  payersMerkleRoot_ The payers merkle root.
+     * @param  nodeIds_          The active node IDs during the reporting period.
+     * @return digest_           The EIP-712 digest.
+     */
+    function _getPayerReportDigest(
+        uint256 originatorNodeId_,
+        uint256 startSequenceId_,
+        uint256 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        uint32[] calldata nodeIds_
+    ) internal view returns (bytes32 digest_) {
+        return
+            _getDigest(
+                keccak256(
+                    abi.encode(
+                        PAYER_REPORT_TYPEHASH,
+                        originatorNodeId_,
+                        startSequenceId_,
+                        endSequenceId_,
+                        payersMerkleRoot_,
+                        keccak256(abi.encodePacked(nodeIds_))
+                    )
+                )
+            );
+    }
+
+    function _getRegistryParameter(bytes memory key_) internal view returns (bytes32 value_) {
+        return IParameterRegistryLike(parameterRegistry).get(key_);
+    }
+
+    function _isNotZero(address input_) internal pure returns (bool isNotZero_) {
+        return input_ != address(0);
+    }
+
+    function _toAddress(bytes32 value_) internal pure returns (address address_) {
+        // slither-disable-next-line assembly
+        assembly {
+            address_ := value_
+        }
+    }
+
+    /**
+     * @dev Verifies that enough of the signatures are valid and provided by canonical node operators and returns the
+     *      array of valid signing node IDs.
+     */
+    function _verifySignatures(
+        uint32 originatorNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        uint32[] calldata nodeIds_,
+        PayerReportSignature[] calldata signatures_
+    ) internal view returns (uint32[] memory validSigningNodeIds_) {
+        bytes32 digest_ = _getPayerReportDigest(
+            originatorNodeId_,
+            startSequenceId_,
+            endSequenceId_,
+            payersMerkleRoot_,
+            nodeIds_
+        );
+
+        // Need to determine how many of the signatures are valid and provided by canonical node operators.
+        uint8 validSignatureCount_ = 0;
+
+        // This array will help in constructing the `validSigningNodeIds_` array, who's length may be less than
+        // the length of the `signatures_` array if some of the signatures are invalid.
+        bool[] memory isValid_ = new bool[](signatures_.length);
+
+        for (uint256 index_; index_ < signatures_.length; ++index_) {
+            uint32 nodeId_ = signatures_[index_].nodeId;
+
+            // Enforces that the signing node IDs are ordered and unique.
+            if (index_ != 0 && nodeId_ <= signatures_[index_ - 1].nodeId) revert UnorderedNodeIds();
+
+            // If the signature is invalid, ignore it.
+            if (!(isValid_[index_] = _verifySignature(digest_, nodeId_, signatures_[index_].signature))) continue;
+
+            ++validSignatureCount_;
+        }
+
+        uint8 requiredSignatureCount_ = (INodeRegistryLike(nodeRegistry).canonicalNodesCount() / 2) + 1;
+
+        // Enforces that the number of valid signatures is greater than one more than half of the canonical node count.
+        if (validSignatureCount_ < requiredSignatureCount_) {
+            revert InsufficientSignatures(validSignatureCount_, requiredSignatureCount_);
+        }
+
+        validSigningNodeIds_ = new uint32[](validSignatureCount_);
+
+        uint256 writeIndex_ = 0;
+
+        for (uint256 index_; index_ < isValid_.length; ++index_) {
+            if (!isValid_[index_]) continue; // Skip invalid signatures.
+
+            unchecked {
+                validSigningNodeIds_[writeIndex_++] = signatures_[index_].nodeId;
+            }
+        }
+    }
+
+    /// @dev Returns true if the signature is from the signer of a canonical node.
+    function _verifySignature(
+        bytes32 digest_,
+        uint32 nodeId_,
+        bytes calldata signature_
+    ) internal view returns (bool isValid_) {
+        // TODO: A combined fetch of `getIsCanonicalNode` and `getSigner` would be optimal.
+
+        // If the node is not canonical, the signature is invalid.
+        if (!INodeRegistryLike(nodeRegistry).getIsCanonicalNode(nodeId_)) return false;
+
+        // Try to recover the signer from the signature. We don't want to revert if the signature is invalid.
+        address signer_ = ECDSA.tryRecoverCalldata(digest_, signature_);
+
+        // The signature is valid if the recovered signer is not `address(0)` and is the signer of the node.
+        return (signer_ != address(0)) && (signer_ == INodeRegistryLike(nodeRegistry).getSigner(nodeId_));
+    }
+}
diff --git a/src/settlement-chain/interfaces/External.sol b/src/settlement-chain/interfaces/External.sol
index a2eda705..3717c90c 100644
--- a/src/settlement-chain/interfaces/External.sol
+++ b/src/settlement-chain/interfaces/External.sol
@@ -89,3 +89,28 @@ interface IParameterRegistryLike {
 
     function get(bytes calldata key_) external view returns (bytes32 value_);
 }
+
+/**
+ * @title  Subset interface for a NodeRegistry.
+ * @notice This is the minimal interface needed by contracts within this subdirectory.
+ */
+interface INodeRegistryLike {
+    function canonicalNodesCount() external view returns (uint8 canonicalNodesCount_);
+
+    function getIsCanonicalNode(uint32 nodeId_) external view returns (bool isCanonicalNode_);
+
+    function getSigner(uint32 nodeId_) external view returns (address signer_);
+}
+
+/**
+ * @title  Subset interface for a PayerRegistry.
+ * @notice This is the minimal interface needed by contracts within this subdirectory.
+ */
+interface IPayerRegistryLike {
+    struct PayerFee {
+        address payer;
+        uint96 fee;
+    }
+
+    function settleUsage(PayerFee[] calldata payerFees_) external returns (uint96 feesSettled_);
+}
diff --git a/src/settlement-chain/interfaces/IPayerRegistry.sol b/src/settlement-chain/interfaces/IPayerRegistry.sol
index 6d517d35..581300bd 100644
--- a/src/settlement-chain/interfaces/IPayerRegistry.sol
+++ b/src/settlement-chain/interfaces/IPayerRegistry.sol
@@ -26,6 +26,16 @@ interface IPayerRegistry is IMigratable {
         // 24 bits remaining in first slot
     }
 
+    /**
+     * @notice Represents a payer and their fee.
+     * @param  payer The address a payer.
+     * @param  fee   The fee to settle for the payer.
+     */
+    struct PayerFee {
+        address payer;
+        uint96 fee;
+    }
+
     /* ============ Events ============ */
 
     /**
@@ -156,9 +166,6 @@ interface IPayerRegistry is IMigratable {
      */
     error WithdrawalNotReady(uint32 timestamp, uint32 withdrawableTimestamp);
 
-    /// @notice Thrown when the lengths or array arguments do not match.
-    error ArrayLengthMismatch();
-
     /// @notice Thrown when trying to finalize a withdrawal while in debt.
     error PayerInDebt();
 
@@ -212,10 +219,10 @@ interface IPayerRegistry is IMigratable {
 
     /**
      * @notice Settles the usage fees for a list of payers.
-     * @param  payers_ The addresses of the payers.
-     * @param  fees_   The fees to settle for each payer.
+     * @param  payerFees_   An array of structs containing the payer and the fee to settle.
+     * @return feesSettled_ The total amount of fees settled.
      */
-    function settleUsage(address[] calldata payers_, uint96[] calldata fees_) external;
+    function settleUsage(PayerFee[] calldata payerFees_) external returns (uint96 feesSettled_);
 
     /**
      * @notice Sends the excess tokens in the contract to the fee distributor.
diff --git a/src/settlement-chain/interfaces/IPayerReportManager.sol b/src/settlement-chain/interfaces/IPayerReportManager.sol
new file mode 100644
index 00000000..19eaaf00
--- /dev/null
+++ b/src/settlement-chain/interfaces/IPayerReportManager.sol
@@ -0,0 +1,204 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { IERC5267 } from "../../abstract/interfaces/IERC5267.sol";
+import { IMigratable } from "../../abstract/interfaces/IMigratable.sol";
+
+/**
+ * @title  The interface for the Payer Report Manager.
+ * @notice This interface exposes functionality for submitting and settling payer reports.
+ */
+interface IPayerReportManager is IMigratable, IERC5267 {
+    /* ============ Structs ============ */
+
+    /**
+     * @notice Represents a payer report.
+     * @param  startSequenceId  The start sequence ID.
+     * @param  endSequenceId    The end sequence ID.
+     * @param  feesSettled      The total fees already settled for this report.
+     * @param  offset           The next index in the merkle tree that has yet to be processed/settled.
+     * @param  isSettled        Whether the payer report is completely processed/settled.
+     * @param  payersMerkleRoot The payers merkle root.
+     * @param  nodeIds          The active node IDs during the reporting period.
+     */
+    struct PayerReport {
+        uint32 startSequenceId;
+        uint32 endSequenceId;
+        uint96 feesSettled;
+        uint32 offset;
+        bool isSettled;
+        bytes32 payersMerkleRoot;
+        uint32[] nodeIds;
+    }
+
+    /**
+     * @notice Represents a payer report signature.
+     * @param  nodeId    The node ID.
+     * @param  signature The signature by the node operator.
+     */
+    struct PayerReportSignature {
+        uint32 nodeId;
+        bytes signature;
+    }
+
+    /* ============ Events ============ */
+
+    /**
+     * @notice Emitted when a payer report is submitted.
+     * @param  originatorNodeId The originator node ID.
+     * @param  payerReportIndex The index of the newly stored report.
+     * @param  startSequenceId  The start sequence ID.
+     * @param  endSequenceId    The end sequence ID.
+     * @param  payersMerkleRoot The payers merkle root.
+     * @param  nodeIds          The active node IDs during the reporting period.
+     * @param  signingNodeIds   The node IDs of the signers of the payer report.
+     */
+    event PayerReportSubmitted(
+        uint32 indexed originatorNodeId,
+        uint256 indexed payerReportIndex,
+        uint32 startSequenceId,
+        uint32 indexed endSequenceId,
+        bytes32 payersMerkleRoot,
+        uint32[] nodeIds,
+        uint32[] signingNodeIds
+    );
+
+    /**
+     * @notice Emitted when a subset of a payer report is settled.
+     * @param  originatorNodeId The originator node ID.
+     * @param  payerReportIndex The payer report index.
+     * @param  count            The number of payer fees settled in this subset.
+     * @param  remaining        The number of payer fees remaining to be settled.
+     * @param  feesSettled      The amount of fees settled in this subset.
+     */
+    event PayerReportSubsetSettled(
+        uint32 indexed originatorNodeId,
+        uint256 indexed payerReportIndex,
+        uint32 count,
+        uint32 remaining,
+        uint96 feesSettled
+    );
+
+    /* ============ Custom Errors ============ */
+
+    /// @notice Error thrown when the parameter registry address is being set to zero (i.e. address(0)).
+    error ZeroParameterRegistry();
+
+    /// @notice Error thrown when the node registry address is being set to zero (i.e. address(0)).
+    error ZeroNodeRegistry();
+
+    /// @notice Error thrown when the payer registry address is being set to zero (i.e. address(0)).
+    error ZeroPayerRegistry();
+
+    /// @notice Error thrown when the start sequence ID is not the last end sequence ID.
+    error InvalidStartSequenceId(uint32 startSequenceId, uint32 lastSequenceId);
+
+    /// @notice Error thrown when the start and end sequence IDs are invalid.
+    error InvalidSequenceIds();
+
+    /// @notice Error thrown when the signing node IDs are not ordered and unique.
+    error UnorderedNodeIds();
+
+    /// @notice Error thrown when the number of valid signatures is insufficient.
+    error InsufficientSignatures(uint8 validSignatureCount, uint8 requiredSignatureCount);
+
+    /// @notice Error thrown when the payer report index is out of bounds.
+    error PayerReportIndexOutOfBounds();
+
+    /// @notice Error thrown when the payer report has already been entirely settled.
+    error PayerReportEntirelySettled();
+
+    /// @notice Error thrown when failing to settle usage via the payer registry.
+    error SettleUsageFailed(bytes returnData_);
+
+    /* ============ Initialization ============ */
+
+    /**
+     * @notice Initializes the contract.
+     */
+    function initialize() external;
+
+    /* ============ Interactive Functions ============ */
+
+    /**
+     * @notice Submits a payer report.
+     * @param  originatorNodeId_ The originator node ID.
+     * @param  startSequenceId_  The start sequence ID.
+     * @param  endSequenceId_    The end sequence ID.
+     * @param  payersMerkleRoot_ The payers merkle root.
+     * @param  nodeIds_          The active node IDs during the reporting period.
+     * @param  signatures_       The signature objects for the payer report.
+     * @return payerReportIndex_ The index of the payer report in the originator's payer report array.
+     */
+    function submit(
+        uint32 originatorNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        uint32[] calldata nodeIds_,
+        PayerReportSignature[] calldata signatures_
+    ) external returns (uint256 payerReportIndex_);
+
+    /**
+     * @notice Settles a subset of a payer report.
+     * @param  originatorNodeId_ The originator node ID.
+     * @param  payerReportIndex_ The payer report index.
+     * @param  payerFees_        The sequential payer fees to settle.
+     * @param  proofElements_    The sequential merkle proof elements for the payer fees to settle.
+     */
+    function settle(
+        uint32 originatorNodeId_,
+        uint256 payerReportIndex_,
+        bytes[] calldata payerFees_,
+        bytes32[] calldata proofElements_
+    ) external;
+
+    /* ============ View/Pure Functions ============ */
+
+    /// @notice The parameter registry key used to fetch the migrator.
+    function migratorParameterKey() external pure returns (bytes memory key_);
+
+    /// @notice The address of the parameter registry.
+    function parameterRegistry() external view returns (address parameterRegistry_);
+
+    /// @notice The address of the node registry.
+    function nodeRegistry() external view returns (address nodeRegistry_);
+
+    /// @notice The address of the payer registry.
+    function payerRegistry() external view returns (address payerRegistry_);
+
+    /**
+     * @notice Returns the payer reports for an originator node.
+     * @param  originatorNodeId_ The originator node ID.
+     * @return payerReports_     The array of payer reports.
+     */
+    function getPayerReports(uint32 originatorNodeId_) external view returns (PayerReport[] memory payerReports_);
+
+    /**
+     * @notice Returns a payer report.
+     * @param  originatorNodeId_ The originator node ID.
+     * @param  payerReportIndex_ The payer report index.
+     * @return payerReport_      The payer report.
+     */
+    function getPayerReport(
+        uint32 originatorNodeId_,
+        uint256 payerReportIndex_
+    ) external view returns (PayerReport memory payerReport_);
+
+    /**
+     * @notice Returns the EIP-712 digest for a payer report.
+     * @param  originatorNodeId_ The originator node ID.
+     * @param  startSequenceId_  The start sequence ID.
+     * @param  endSequenceId_    The end sequence ID.
+     * @param  payersMerkleRoot_ The payers merkle root.
+     * @param  nodeIds_          The active node IDs during the reporting period.
+     * @return digest_           The EIP-712 digest.
+     */
+    function getPayerReportDigest(
+        uint32 originatorNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        uint32[] calldata nodeIds_
+    ) external view returns (bytes32 digest_);
+}
diff --git a/test/unit/PayerRegistry.t.sol b/test/unit/PayerRegistry.t.sol
index 177c93a5..e34226df 100644
--- a/test/unit/PayerRegistry.t.sol
+++ b/test/unit/PayerRegistry.t.sol
@@ -531,7 +531,7 @@ contract PayerRegistryTests is Test, Utils {
         vm.expectRevert(IPayerRegistry.NotSettler.selector);
 
         vm.prank(_unauthorized);
-        _registry.settleUsage(new address[](0), new uint96[](0));
+        _registry.settleUsage(new IPayerRegistry.PayerFee[](0));
     }
 
     function test_settleUsage_paused() external {
@@ -540,21 +540,14 @@ contract PayerRegistryTests is Test, Utils {
         vm.expectRevert(IPayerRegistry.Paused.selector);
 
         vm.prank(_settler);
-        _registry.settleUsage(new address[](0), new uint96[](0));
-    }
-
-    function test_settleUsage_arrayLengthMismatch() external {
-        vm.expectRevert(IPayerRegistry.ArrayLengthMismatch.selector);
-
-        vm.prank(_settler);
-        _registry.settleUsage(new address[](0), new uint96[](1));
+        _registry.settleUsage(new IPayerRegistry.PayerFee[](0));
     }
 
     function test_settleUsage() external {
-        address[] memory payers = new address[](3);
-        payers[0] = _alice;
-        payers[1] = _bob;
-        payers[2] = _charlie;
+        IPayerRegistry.PayerFee[] memory payerFees_ = new IPayerRegistry.PayerFee[](3);
+        payerFees_[0] = IPayerRegistry.PayerFee({ payer: _alice, fee: 10e6 });
+        payerFees_[1] = IPayerRegistry.PayerFee({ payer: _bob, fee: 20e6 });
+        payerFees_[2] = IPayerRegistry.PayerFee({ payer: _charlie, fee: 30e6 });
 
         _registry.__setBalance(_alice, 30e6);
         _registry.__setBalance(_bob, 10e6);
@@ -563,11 +556,6 @@ contract PayerRegistryTests is Test, Utils {
         _registry.__setTotalDeposits(30e6 + 10e6 - 10e6); // Sum of Alice, Bob, and Charlie's balances.
         _registry.__setTotalDebt(10e6); // Charlie's debt.
 
-        uint96[] memory fees = new uint96[](3);
-        fees[0] = 10e6;
-        fees[1] = 20e6;
-        fees[2] = 30e6;
-
         // TODO: `_expectAndMockCall`.
         vm.mockCall(
             _token,
@@ -585,7 +573,9 @@ contract PayerRegistryTests is Test, Utils {
         emit IPayerRegistry.UsageSettled(_charlie, 30e6);
 
         vm.prank(_settler);
-        _registry.settleUsage(payers, fees);
+        uint96 feesSettled_ = _registry.settleUsage(payerFees_);
+
+        assertEq(feesSettled_, 10e6 + 20e6 + 30e6);
 
         assertEq(_registry.getBalance(_alice), 30e6 - 10e6);
         assertEq(_registry.getBalance(_bob), 10e6 - 20e6);
diff --git a/test/unit/PayerReportManager.t.sol b/test/unit/PayerReportManager.t.sol
new file mode 100644
index 00000000..8bf989df
--- /dev/null
+++ b/test/unit/PayerReportManager.t.sol
@@ -0,0 +1,1005 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { Test } from "../../lib/forge-std/src/Test.sol";
+
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../../src/libraries/SequentialMerkleProofs.sol";
+
+import { IERC1967 } from "../../src/abstract/interfaces/IERC1967.sol";
+import { IMigratable } from "../../src/abstract/interfaces/IMigratable.sol";
+import { IPayerReportManager } from "../../src/settlement-chain/interfaces/IPayerReportManager.sol";
+
+import { Proxy } from "../../src/any-chain/Proxy.sol";
+
+import { PayerReportManagerHarness } from "../utils/Harnesses.sol";
+
+import {
+    MockParameterRegistry,
+    MockNodeRegistry,
+    MockPayerRegistry,
+    MockMigrator,
+    MockFailingMigrator
+} from "../utils/Mocks.sol";
+
+import { Utils } from "../utils/Utils.sol";
+
+contract PayerReportManagerTests is Test, Utils {
+    bytes32 internal constant _EIP712_DOMAIN_HASH =
+        keccak256(
+            abi.encodePacked("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
+        );
+
+    bytes internal constant _MIGRATOR_KEY = "xmtp.payerReportManager.migrator";
+
+    PayerReportManagerHarness internal _manager;
+
+    address internal _implementation;
+    address internal _parameterRegistry;
+    address internal _nodeRegistry;
+    address internal _payerRegistry;
+
+    address internal _signer1;
+    uint256 internal _signer1Pk;
+    address internal _signer2;
+    uint256 internal _signer2Pk;
+    address internal _signer3;
+    uint256 internal _signer3Pk;
+    address internal _signer4;
+    uint256 internal _signer4Pk;
+
+    function setUp() external {
+        (_signer1, _signer1Pk) = makeAddrAndKey("signer1");
+        (_signer2, _signer2Pk) = makeAddrAndKey("signer2");
+        (_signer3, _signer3Pk) = makeAddrAndKey("signer3");
+        (_signer4, _signer4Pk) = makeAddrAndKey("signer4");
+
+        _parameterRegistry = address(new MockParameterRegistry());
+        _nodeRegistry = address(new MockNodeRegistry());
+        _payerRegistry = address(new MockPayerRegistry());
+
+        _implementation = address(new PayerReportManagerHarness(_parameterRegistry, _nodeRegistry, _payerRegistry));
+
+        _manager = PayerReportManagerHarness(address(new Proxy(_implementation)));
+
+        _manager.initialize();
+    }
+
+    /* ============ constructor ============ */
+
+    function test_constructor_zeroParameterRegistry() external {
+        vm.expectRevert(IPayerReportManager.ZeroParameterRegistry.selector);
+
+        new PayerReportManagerHarness(address(0), address(0), address(0));
+    }
+
+    function test_constructor_zeroNodeRegistry() external {
+        vm.expectRevert(IPayerReportManager.ZeroNodeRegistry.selector);
+
+        new PayerReportManagerHarness(_parameterRegistry, address(0), address(0));
+    }
+
+    function test_constructor_zeroPayerRegistry() external {
+        vm.expectRevert(IPayerReportManager.ZeroPayerRegistry.selector);
+
+        new PayerReportManagerHarness(_parameterRegistry, _nodeRegistry, address(0));
+    }
+
+    /* ============ initial state ============ */
+
+    function test_initialState() external view {
+        assertEq(_getImplementationFromSlot(address(_manager)), _implementation);
+        assertEq(_manager.implementation(), _implementation);
+        assertEq(_manager.parameterRegistry(), _parameterRegistry);
+        assertEq(_manager.nodeRegistry(), _nodeRegistry);
+        assertEq(_manager.payerRegistry(), _payerRegistry);
+        assertEq(_manager.migratorParameterKey(), _MIGRATOR_KEY);
+    }
+
+    /* ============ initializer ============ */
+
+    function test_initialize_reinitialization() external {
+        vm.expectRevert(Initializable.InvalidInitialization.selector);
+        _manager.initialize();
+    }
+
+    /* ============ submit ============ */
+
+    function test_submit_invalidStartSequenceId() external {
+        _manager.__pushPayerReport({
+            originatorNodeId_: 0,
+            startSequenceId_: 0,
+            endSequenceId_: 10,
+            feesSettled_: 0,
+            offset_: 0,
+            isSettled_: false,
+            payersMerkleRoot_: bytes32(0),
+            nodeIds_: new uint32[](0)
+        });
+
+        vm.expectRevert(abi.encodeWithSelector(IPayerReportManager.InvalidStartSequenceId.selector, 11, 10));
+        _manager.submit({
+            originatorNodeId_: 0,
+            startSequenceId_: 11,
+            endSequenceId_: 11,
+            payersMerkleRoot_: bytes32(0),
+            nodeIds_: new uint32[](0),
+            signatures_: new IPayerReportManager.PayerReportSignature[](0)
+        });
+    }
+
+    function test_submit_invalidSequenceIds() external {
+        _manager.__pushPayerReport({
+            originatorNodeId_: 0,
+            startSequenceId_: 0,
+            endSequenceId_: 10,
+            feesSettled_: 0,
+            offset_: 0,
+            isSettled_: false,
+            payersMerkleRoot_: bytes32(0),
+            nodeIds_: new uint32[](0)
+        });
+
+        vm.expectRevert(IPayerReportManager.InvalidSequenceIds.selector);
+        _manager.submit({
+            originatorNodeId_: 0,
+            startSequenceId_: 10,
+            endSequenceId_: 9,
+            payersMerkleRoot_: bytes32(0),
+            nodeIds_: new uint32[](0),
+            signatures_: new IPayerReportManager.PayerReportSignature[](0)
+        });
+    }
+
+    function test_submit_unorderedNodeIds() external {
+        IPayerReportManager.PayerReportSignature[] memory signatures_ = new IPayerReportManager.PayerReportSignature[](
+            2
+        );
+
+        signatures_[0] = IPayerReportManager.PayerReportSignature({ nodeId: 1, signature: "" });
+        signatures_[1] = IPayerReportManager.PayerReportSignature({ nodeId: 0, signature: "" });
+
+        vm.expectRevert(IPayerReportManager.UnorderedNodeIds.selector);
+        _manager.submit({
+            originatorNodeId_: 0,
+            startSequenceId_: 0,
+            endSequenceId_: 1,
+            payersMerkleRoot_: bytes32(0),
+            nodeIds_: new uint32[](0),
+            signatures_: signatures_
+        });
+    }
+
+    function test_submit_insufficientSignatures() external {
+        IPayerReportManager.PayerReportSignature[] memory signatures_ = new IPayerReportManager.PayerReportSignature[](
+            1
+        );
+
+        bytes memory signature_ = _getPayerReportSignature(0, 0, 0, bytes32(0), new uint32[](0), _signer1Pk);
+
+        signatures_[0] = IPayerReportManager.PayerReportSignature({ nodeId: 1, signature: signature_ });
+
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("getIsCanonicalNode(uint32)", 1), abi.encode(true));
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("getSigner(uint32)", 1), abi.encode(_signer1));
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("canonicalNodesCount()"), abi.encode(3));
+
+        vm.expectRevert(abi.encodeWithSelector(IPayerReportManager.InsufficientSignatures.selector, 1, 2));
+        _manager.submit({
+            originatorNodeId_: 0,
+            startSequenceId_: 0,
+            endSequenceId_: 0,
+            payersMerkleRoot_: bytes32(0),
+            nodeIds_: new uint32[](0),
+            signatures_: signatures_
+        });
+    }
+
+    function test_submit_zeroPayersMerkleRoot() external {
+        _manager.__pushPayerReport({
+            originatorNodeId_: 0,
+            startSequenceId_: 0,
+            endSequenceId_: 1,
+            feesSettled_: 0,
+            offset_: 0,
+            isSettled_: false,
+            payersMerkleRoot_: bytes32(0),
+            nodeIds_: new uint32[](0)
+        });
+
+        IPayerReportManager.PayerReportSignature[] memory signatures_ = new IPayerReportManager.PayerReportSignature[](
+            3
+        );
+
+        signatures_[0] = IPayerReportManager.PayerReportSignature({
+            nodeId: 1,
+            signature: _getPayerReportSignature(0, 1, 2, bytes32(0), new uint32[](0), _signer1Pk)
+        });
+
+        signatures_[1] = IPayerReportManager.PayerReportSignature({
+            nodeId: 2,
+            signature: _getPayerReportSignature(0, 1, 2, bytes32(0), new uint32[](0), _signer2Pk)
+        });
+
+        signatures_[2] = IPayerReportManager.PayerReportSignature({
+            nodeId: 3,
+            signature: _getPayerReportSignature(0, 1, 2, bytes32(0), new uint32[](0), _signer3Pk)
+        });
+
+        uint32[] memory validSigningNodeIds_ = new uint32[](2);
+
+        validSigningNodeIds_[0] = 1;
+        validSigningNodeIds_[1] = 2;
+
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("getIsCanonicalNode(uint32)", 1), abi.encode(true));
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("getSigner(uint32)", 1), abi.encode(_signer1));
+
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("getIsCanonicalNode(uint32)", 2), abi.encode(true));
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("getSigner(uint32)", 2), abi.encode(_signer2));
+
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("getIsCanonicalNode(uint32)", 3), abi.encode(false));
+
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("canonicalNodesCount()"), abi.encode(3));
+
+        vm.expectEmit(address(_manager));
+        emit IPayerReportManager.PayerReportSubmitted({
+            originatorNodeId: 0,
+            payerReportIndex: 1,
+            startSequenceId: 1,
+            endSequenceId: 2,
+            payersMerkleRoot: bytes32(0),
+            nodeIds: new uint32[](0),
+            signingNodeIds: validSigningNodeIds_
+        });
+
+        vm.expectEmit(address(_manager));
+        emit IPayerReportManager.PayerReportSubsetSettled(0, 1, 0, 0, 0);
+
+        uint256 payerReportIndex_ = _manager.submit({
+            originatorNodeId_: 0,
+            startSequenceId_: 1,
+            endSequenceId_: 2,
+            payersMerkleRoot_: bytes32(0),
+            nodeIds_: new uint32[](0),
+            signatures_: signatures_
+        });
+
+        assertEq(payerReportIndex_, 1);
+
+        IPayerReportManager.PayerReport memory payerReport_ = _manager.getPayerReport(0, 1);
+
+        assertEq(payerReport_.startSequenceId, 1);
+        assertEq(payerReport_.endSequenceId, 2);
+        assertEq(payerReport_.feesSettled, 0);
+        assertEq(payerReport_.offset, 0);
+        assertTrue(payerReport_.isSettled);
+        assertEq(payerReport_.payersMerkleRoot, bytes32(0));
+        assertEq(payerReport_.nodeIds.length, 0);
+    }
+
+    function test_submit() external {
+        _manager.__pushPayerReport({
+            originatorNodeId_: 0,
+            startSequenceId_: 0,
+            endSequenceId_: 1,
+            feesSettled_: 0,
+            offset_: 0,
+            isSettled_: false,
+            payersMerkleRoot_: bytes32(0),
+            nodeIds_: new uint32[](0)
+        });
+
+        IPayerReportManager.PayerReportSignature[] memory signatures_ = new IPayerReportManager.PayerReportSignature[](
+            3
+        );
+
+        signatures_[0] = IPayerReportManager.PayerReportSignature({
+            nodeId: 1,
+            signature: _getPayerReportSignature(0, 1, 2, bytes32(uint256(1)), new uint32[](0), _signer1Pk)
+        });
+
+        signatures_[1] = IPayerReportManager.PayerReportSignature({
+            nodeId: 2,
+            signature: _getPayerReportSignature(0, 1, 2, bytes32(uint256(1)), new uint32[](0), _signer2Pk)
+        });
+
+        signatures_[2] = IPayerReportManager.PayerReportSignature({
+            nodeId: 3,
+            signature: _getPayerReportSignature(0, 1, 2, bytes32(uint256(1)), new uint32[](0), _signer3Pk)
+        });
+
+        uint32[] memory validSigningNodeIds_ = new uint32[](2);
+
+        validSigningNodeIds_[0] = 1;
+        validSigningNodeIds_[1] = 2;
+
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("getIsCanonicalNode(uint32)", 1), abi.encode(true));
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("getSigner(uint32)", 1), abi.encode(_signer1));
+
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("getIsCanonicalNode(uint32)", 2), abi.encode(true));
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("getSigner(uint32)", 2), abi.encode(_signer2));
+
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("getIsCanonicalNode(uint32)", 3), abi.encode(false));
+
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("canonicalNodesCount()"), abi.encode(3));
+
+        vm.expectEmit(address(_manager));
+        emit IPayerReportManager.PayerReportSubmitted({
+            originatorNodeId: 0,
+            payerReportIndex: 1,
+            startSequenceId: 1,
+            endSequenceId: 2,
+            payersMerkleRoot: bytes32(uint256(1)),
+            nodeIds: new uint32[](0),
+            signingNodeIds: validSigningNodeIds_
+        });
+
+        uint256 payerReportIndex_ = _manager.submit({
+            originatorNodeId_: 0,
+            startSequenceId_: 1,
+            endSequenceId_: 2,
+            payersMerkleRoot_: bytes32(uint256(1)),
+            nodeIds_: new uint32[](0),
+            signatures_: signatures_
+        });
+
+        assertEq(payerReportIndex_, 1);
+
+        IPayerReportManager.PayerReport memory payerReport_ = _manager.getPayerReport(0, 1);
+
+        assertEq(payerReport_.startSequenceId, 1);
+        assertEq(payerReport_.endSequenceId, 2);
+        assertEq(payerReport_.feesSettled, 0);
+        assertEq(payerReport_.offset, 0);
+        assertFalse(payerReport_.isSettled);
+        assertEq(payerReport_.payersMerkleRoot, bytes32(uint256(1)));
+        assertEq(payerReport_.nodeIds.length, 0);
+    }
+
+    /* ============ settle ============ */
+
+    function test_settle_payerReportIndexOutOfBounds() external {
+        vm.expectRevert(IPayerReportManager.PayerReportIndexOutOfBounds.selector);
+        _manager.settle(0, 0, new bytes[](0), new bytes32[](0));
+    }
+
+    function test_settle_payerReportEntirelySettled() external {
+        _manager.__pushPayerReport({
+            originatorNodeId_: 0,
+            startSequenceId_: 0,
+            endSequenceId_: 0,
+            feesSettled_: 0,
+            offset_: 0,
+            isSettled_: true,
+            payersMerkleRoot_: bytes32(0),
+            nodeIds_: new uint32[](0)
+        });
+
+        vm.expectRevert(IPayerReportManager.PayerReportEntirelySettled.selector);
+        _manager.settle(0, 0, new bytes[](0), new bytes32[](0));
+    }
+
+    function test_settle_invalidProof() external {
+        // This is the root for the payer fees: (1, 100), (2, 200), (3, 300), (4, 400), (5, 500), (6, 600).
+        bytes32 payersMerkleRoot_ = 0xf3051bbf3818e5393ac18edd8ba285e62f35fe01748a8acc900eca813a6b364e;
+
+        bytes[] memory payerFees_ = new bytes[](3);
+        payerFees_[0] = abi.encode(address(1), uint96(100));
+        payerFees_[1] = abi.encode(address(2), uint96(200));
+        payerFees_[2] = abi.encode(address(3), uint96(300));
+
+        bytes32[] memory proofElements_ = new bytes32[](3);
+
+        // Incorrect leaf count as first proof element.
+        proofElements_[0] = bytes32(uint256(0x0000000000000000000000000000000000000000000000000000000000000005));
+        proofElements_[1] = bytes32(0xbf3990232dba4985267c2b0c64d09165457aa9bc02292fff8416821335f719ad);
+        proofElements_[2] = bytes32(0x179a5ab6250bc7b598e955bb7a59ed2b26171140d0c9ecbdb5e2ee15a529dfc5);
+
+        _manager.__pushPayerReport({
+            originatorNodeId_: 0,
+            startSequenceId_: 0,
+            endSequenceId_: 0,
+            feesSettled_: 0,
+            offset_: 0,
+            isSettled_: false,
+            payersMerkleRoot_: payersMerkleRoot_,
+            nodeIds_: new uint32[](0)
+        });
+
+        vm.expectRevert(SequentialMerkleProofs.InvalidProof.selector);
+        _manager.settle(0, 0, payerFees_, proofElements_);
+    }
+
+    function test_settle_settleUsageFailed() external {
+        // This is the root for the payer fees: (1, 100), (2, 200), (3, 300), (4, 400), (5, 500), (6, 600).
+        bytes32 payersMerkleRoot_ = 0xf3051bbf3818e5393ac18edd8ba285e62f35fe01748a8acc900eca813a6b364e;
+
+        bytes[] memory payerFees_ = new bytes[](3);
+        payerFees_[0] = abi.encode(address(1), uint96(100));
+        payerFees_[1] = abi.encode(address(2), uint96(200));
+        payerFees_[2] = abi.encode(address(3), uint96(300));
+
+        bytes32[] memory proofElements_ = new bytes32[](3);
+        proofElements_[0] = bytes32(uint256(0x0000000000000000000000000000000000000000000000000000000000000006));
+        proofElements_[1] = bytes32(0xbf3990232dba4985267c2b0c64d09165457aa9bc02292fff8416821335f719ad);
+        proofElements_[2] = bytes32(0x179a5ab6250bc7b598e955bb7a59ed2b26171140d0c9ecbdb5e2ee15a529dfc5);
+
+        _manager.__pushPayerReport({
+            originatorNodeId_: 0,
+            startSequenceId_: 0,
+            endSequenceId_: 0,
+            feesSettled_: 0,
+            offset_: 0,
+            isSettled_: false,
+            payersMerkleRoot_: payersMerkleRoot_,
+            nodeIds_: new uint32[](0)
+        });
+
+        // TODO: `expectCallAndRevert`.
+        vm.mockCallRevert(
+            _payerRegistry,
+            abi.encodeWithSignature("settleUsage((address,uint96)[])", payerFees_),
+            "Test Failure"
+        );
+
+        vm.expectRevert(abi.encodeWithSelector(IPayerReportManager.SettleUsageFailed.selector, "Test Failure"));
+        _manager.settle(0, 0, payerFees_, proofElements_);
+    }
+
+    function test_settle_firstHalf() external {
+        // This is the root for the payer fees: (1, 100), (2, 200), (3, 300), (4, 400), (5, 500), (6, 600).
+        bytes32 payersMerkleRoot_ = 0xf3051bbf3818e5393ac18edd8ba285e62f35fe01748a8acc900eca813a6b364e;
+
+        bytes[] memory payerFees_ = new bytes[](3);
+        payerFees_[0] = abi.encode(address(1), uint96(100));
+        payerFees_[1] = abi.encode(address(2), uint96(200));
+        payerFees_[2] = abi.encode(address(3), uint96(300));
+
+        bytes32[] memory proofElements_ = new bytes32[](3);
+        proofElements_[0] = bytes32(uint256(0x0000000000000000000000000000000000000000000000000000000000000006));
+        proofElements_[1] = bytes32(0xbf3990232dba4985267c2b0c64d09165457aa9bc02292fff8416821335f719ad);
+        proofElements_[2] = bytes32(0x179a5ab6250bc7b598e955bb7a59ed2b26171140d0c9ecbdb5e2ee15a529dfc5);
+
+        _manager.__pushPayerReport({
+            originatorNodeId_: 0,
+            startSequenceId_: 0,
+            endSequenceId_: 0,
+            feesSettled_: 0,
+            offset_: 0,
+            isSettled_: false,
+            payersMerkleRoot_: payersMerkleRoot_,
+            nodeIds_: new uint32[](0)
+        });
+
+        vm.mockCall(
+            _payerRegistry,
+            abi.encodeWithSignature("settleUsage((address,uint96)[])", payerFees_),
+            abi.encode(uint96(600))
+        );
+
+        vm.expectEmit(address(_manager));
+        emit IPayerReportManager.PayerReportSubsetSettled(0, 0, 3, 3, 600);
+
+        _manager.settle(0, 0, payerFees_, proofElements_);
+
+        assertEq(_manager.getPayerReport(0, 0).feesSettled, 600);
+        assertEq(_manager.getPayerReport(0, 0).offset, 3);
+    }
+
+    function test_settle_secondHalf() external {
+        // This is the root for the payer fees: (1, 100), (2, 200), (3, 300), (4, 400), (5, 500), (6, 600).
+        bytes32 payersMerkleRoot_ = 0xf3051bbf3818e5393ac18edd8ba285e62f35fe01748a8acc900eca813a6b364e;
+
+        bytes[] memory payerFees_ = new bytes[](3);
+        payerFees_[0] = abi.encode(address(4), uint96(400));
+        payerFees_[1] = abi.encode(address(5), uint96(500));
+        payerFees_[2] = abi.encode(address(6), uint96(600));
+
+        bytes32[] memory proofElements_ = new bytes32[](3);
+        proofElements_[0] = bytes32(uint256(0x0000000000000000000000000000000000000000000000000000000000000006));
+        proofElements_[1] = bytes32(0xe1fd91200025ec7e94c96c4e035820e7f510e94f479b07caf2791ed8991d80ca);
+        proofElements_[2] = bytes32(0x81c8c090425d4a46672a976619a2dd2fce8b9b0fea5e01d1fb32e78bff1681d0);
+
+        _manager.__pushPayerReport({
+            originatorNodeId_: 0,
+            startSequenceId_: 0,
+            endSequenceId_: 0,
+            feesSettled_: 600,
+            offset_: 3,
+            isSettled_: false,
+            payersMerkleRoot_: payersMerkleRoot_,
+            nodeIds_: new uint32[](0)
+        });
+
+        vm.mockCall(
+            _payerRegistry,
+            abi.encodeWithSignature("settleUsage((address,uint96)[])", payerFees_),
+            abi.encode(uint96(1_500))
+        );
+
+        vm.expectEmit(address(_manager));
+        emit IPayerReportManager.PayerReportSubsetSettled(0, 0, 3, 0, 1_500);
+
+        _manager.settle(0, 0, payerFees_, proofElements_);
+
+        assertEq(_manager.getPayerReport(0, 0).feesSettled, 600 + 1_500);
+        assertEq(_manager.getPayerReport(0, 0).offset, 6);
+        assertTrue(_manager.getPayerReport(0, 0).isSettled);
+    }
+
+    function test_settle_oneShot() external {
+        // This is the root for the payer fees: (1, 100), (2, 200), (3, 300), (4, 400), (5, 500), (6, 600).
+        bytes32 payersMerkleRoot_ = 0xf3051bbf3818e5393ac18edd8ba285e62f35fe01748a8acc900eca813a6b364e;
+
+        bytes[] memory payerFees_ = new bytes[](6);
+        payerFees_[0] = abi.encode(address(1), uint96(100));
+        payerFees_[1] = abi.encode(address(2), uint96(200));
+        payerFees_[2] = abi.encode(address(3), uint96(300));
+        payerFees_[3] = abi.encode(address(4), uint96(400));
+        payerFees_[4] = abi.encode(address(5), uint96(500));
+        payerFees_[5] = abi.encode(address(6), uint96(600));
+
+        bytes32[] memory proofElements_ = new bytes32[](1);
+        proofElements_[0] = bytes32(uint256(0x0000000000000000000000000000000000000000000000000000000000000006));
+
+        _manager.__pushPayerReport({
+            originatorNodeId_: 0,
+            startSequenceId_: 0,
+            endSequenceId_: 0,
+            feesSettled_: 0,
+            offset_: 0,
+            isSettled_: false,
+            payersMerkleRoot_: payersMerkleRoot_,
+            nodeIds_: new uint32[](0)
+        });
+
+        vm.mockCall(
+            _payerRegistry,
+            abi.encodeWithSignature("settleUsage((address,uint96)[])", payerFees_),
+            abi.encode(uint96(2_100))
+        );
+
+        vm.expectEmit(address(_manager));
+        emit IPayerReportManager.PayerReportSubsetSettled(0, 0, 6, 0, 2_100);
+
+        _manager.settle(0, 0, payerFees_, proofElements_);
+
+        assertEq(_manager.getPayerReport(0, 0).feesSettled, 2_100);
+        assertEq(_manager.getPayerReport(0, 0).offset, 6);
+        assertTrue(_manager.getPayerReport(0, 0).isSettled);
+    }
+
+    /* ============ migrate ============ */
+
+    function test_migrate_zeroMigrator() external {
+        vm.expectRevert(IMigratable.ZeroMigrator.selector);
+        _manager.migrate();
+    }
+
+    function test_migrate_migrationFailed() external {
+        address migrator_ = address(new MockFailingMigrator());
+
+        _mockParameterRegistryCall(_MIGRATOR_KEY, migrator_);
+
+        vm.expectRevert(
+            abi.encodeWithSelector(
+                IMigratable.MigrationFailed.selector,
+                migrator_,
+                abi.encodeWithSelector(MockFailingMigrator.Failed.selector)
+            )
+        );
+
+        _manager.migrate();
+    }
+
+    function test_migrate_emptyCode() external {
+        _mockParameterRegistryCall(_MIGRATOR_KEY, address(1));
+
+        vm.expectRevert(abi.encodeWithSelector(IMigratable.EmptyCode.selector, address(1)));
+
+        _manager.migrate();
+    }
+
+    function test_migrate() external {
+        _manager.__pushPayerReport({
+            originatorNodeId_: 1,
+            startSequenceId_: 2,
+            endSequenceId_: 3,
+            feesSettled_: 4,
+            offset_: 5,
+            isSettled_: true,
+            payersMerkleRoot_: bytes32(uint256(6)),
+            nodeIds_: new uint32[](7)
+        });
+
+        address newImplementation_ = address(
+            new PayerReportManagerHarness(_parameterRegistry, _nodeRegistry, _payerRegistry)
+        );
+
+        address migrator_ = address(new MockMigrator(newImplementation_));
+
+        // TODO: `_expectAndMockParameterRegistryCall`.
+        _mockParameterRegistryCall(_MIGRATOR_KEY, migrator_);
+
+        vm.expectEmit(address(_manager));
+        emit IMigratable.Migrated(migrator_);
+
+        vm.expectEmit(address(_manager));
+        emit IERC1967.Upgraded(newImplementation_);
+
+        _manager.migrate();
+
+        assertEq(_getImplementationFromSlot(address(_manager)), newImplementation_);
+        assertEq(_manager.parameterRegistry(), _parameterRegistry);
+
+        IPayerReportManager.PayerReport[] memory payerReports_ = _manager.getPayerReports(1);
+
+        assertEq(payerReports_.length, 1);
+
+        assertEq(payerReports_[0].startSequenceId, 2);
+        assertEq(payerReports_[0].endSequenceId, 3);
+        assertEq(payerReports_[0].feesSettled, 4);
+        assertEq(payerReports_[0].offset, 5);
+        assertTrue(payerReports_[0].isSettled);
+        assertEq(payerReports_[0].payersMerkleRoot, bytes32(uint256(6)));
+        assertEq(payerReports_[0].nodeIds.length, 7);
+    }
+
+    /* ============ _verifySignatures ============ */
+
+    function test_internal_verifySignatures_unorderedNodeIds() external {
+        IPayerReportManager.PayerReportSignature[] memory signatures_ = new IPayerReportManager.PayerReportSignature[](
+            2
+        );
+
+        signatures_[0] = IPayerReportManager.PayerReportSignature({ nodeId: 1, signature: "" });
+        signatures_[1] = IPayerReportManager.PayerReportSignature({ nodeId: 0, signature: "" });
+
+        vm.expectRevert(IPayerReportManager.UnorderedNodeIds.selector);
+        _manager.__verifySignatures({
+            originatorNodeId_: 0,
+            startSequenceId_: 0,
+            endSequenceId_: 0,
+            payersMerkleRoot_: bytes32(0),
+            nodeIds_: new uint32[](0),
+            signatures_: signatures_
+        });
+    }
+
+    function test_internal_verifySignatures_insufficientSignatures() external {
+        vm.expectRevert(abi.encodeWithSelector(IPayerReportManager.InsufficientSignatures.selector, 0, 1));
+        _manager.__verifySignatures({
+            originatorNodeId_: 0,
+            startSequenceId_: 0,
+            endSequenceId_: 0,
+            payersMerkleRoot_: bytes32(0),
+            nodeIds_: new uint32[](0),
+            signatures_: new IPayerReportManager.PayerReportSignature[](0)
+        });
+    }
+
+    function test_internal_verifySignatures_lastInvalid() external {
+        IPayerReportManager.PayerReportSignature[] memory signatures_ = new IPayerReportManager.PayerReportSignature[](
+            3
+        );
+
+        signatures_[0] = IPayerReportManager.PayerReportSignature({
+            nodeId: 1,
+            signature: _getPayerReportSignature(0, 0, 0, bytes32(0), new uint32[](0), _signer1Pk)
+        });
+
+        signatures_[1] = IPayerReportManager.PayerReportSignature({
+            nodeId: 2,
+            signature: _getPayerReportSignature(0, 0, 0, bytes32(0), new uint32[](0), _signer2Pk)
+        });
+
+        signatures_[2] = IPayerReportManager.PayerReportSignature({
+            nodeId: 3,
+            signature: _getPayerReportSignature(0, 0, 0, bytes32(0), new uint32[](0), _signer3Pk)
+        });
+
+        uint32[] memory expectedValidSigningNodeIds_ = new uint32[](2);
+
+        expectedValidSigningNodeIds_[0] = 1;
+        expectedValidSigningNodeIds_[1] = 2;
+
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("getIsCanonicalNode(uint32)", 1), abi.encode(true));
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("getSigner(uint32)", 1), abi.encode(_signer1));
+
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("getIsCanonicalNode(uint32)", 2), abi.encode(true));
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("getSigner(uint32)", 2), abi.encode(_signer2));
+
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("getIsCanonicalNode(uint32)", 3), abi.encode(false));
+
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("canonicalNodesCount()"), abi.encode(3));
+
+        uint32[] memory validSigningNodeIds_ = _manager.__verifySignatures({
+            originatorNodeId_: 0,
+            startSequenceId_: 0,
+            endSequenceId_: 0,
+            payersMerkleRoot_: bytes32(0),
+            nodeIds_: new uint32[](0),
+            signatures_: signatures_
+        });
+
+        assertEq(validSigningNodeIds_.length, 2);
+
+        assertEq(validSigningNodeIds_[0], 1);
+        assertEq(validSigningNodeIds_[1], 2);
+    }
+
+    function test_internal_verifySignatures_middleInvalid() external {
+        IPayerReportManager.PayerReportSignature[] memory signatures_ = new IPayerReportManager.PayerReportSignature[](
+            3
+        );
+
+        signatures_[0] = IPayerReportManager.PayerReportSignature({
+            nodeId: 1,
+            signature: _getPayerReportSignature(0, 0, 0, bytes32(0), new uint32[](0), _signer1Pk)
+        });
+
+        signatures_[1] = IPayerReportManager.PayerReportSignature({
+            nodeId: 2,
+            signature: _getPayerReportSignature(0, 0, 0, bytes32(0), new uint32[](0), _signer2Pk)
+        });
+
+        signatures_[2] = IPayerReportManager.PayerReportSignature({
+            nodeId: 3,
+            signature: _getPayerReportSignature(0, 0, 0, bytes32(0), new uint32[](0), _signer3Pk)
+        });
+
+        uint32[] memory expectedValidSigningNodeIds_ = new uint32[](2);
+
+        expectedValidSigningNodeIds_[0] = 1;
+        expectedValidSigningNodeIds_[1] = 3;
+
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("getIsCanonicalNode(uint32)", 1), abi.encode(true));
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("getSigner(uint32)", 1), abi.encode(_signer1));
+
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("getIsCanonicalNode(uint32)", 2), abi.encode(false));
+
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("getIsCanonicalNode(uint32)", 3), abi.encode(true));
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("getSigner(uint32)", 3), abi.encode(_signer3));
+
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("canonicalNodesCount()"), abi.encode(3));
+
+        uint32[] memory validSigningNodeIds_ = _manager.__verifySignatures({
+            originatorNodeId_: 0,
+            startSequenceId_: 0,
+            endSequenceId_: 0,
+            payersMerkleRoot_: bytes32(0),
+            nodeIds_: new uint32[](0),
+            signatures_: signatures_
+        });
+
+        assertEq(validSigningNodeIds_.length, 2);
+
+        assertEq(validSigningNodeIds_[0], 1);
+        assertEq(validSigningNodeIds_[1], 3);
+    }
+
+    /* ============ _verifySignature ============ */
+
+    function test_internal_verifySignature_notCanonicalNode() external {
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("getIsCanonicalNode(uint32)", 1), abi.encode(false));
+        assertFalse(_manager.__verifySignature(bytes32(0), 1, ""));
+    }
+
+    function test_internal_verifySignature_invalidSignature() external {
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("getIsCanonicalNode(uint32)", 1), abi.encode(true));
+        assertFalse(_manager.__verifySignature(bytes32(0), 1, ""));
+    }
+
+    function test_internal_verifySignature_notNodeOwner() external {
+        bytes memory signature_ = _getPayerReportSignature({
+            originatorNodeId_: 0,
+            startSequenceId_: 0,
+            endSequenceId_: 0,
+            payersMerkleRoot_: bytes32(0),
+            nodeIds_: new uint32[](0),
+            privateKey_: _signer1Pk
+        });
+
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("getIsCanonicalNode(uint32)", 1), abi.encode(true));
+
+        assertFalse(_manager.__verifySignature(bytes32(0), 1, signature_));
+    }
+
+    function test_internal_verifySignature() external {
+        bytes memory signature_ = _getSignature(bytes32(0), _signer1Pk);
+
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("getIsCanonicalNode(uint32)", 1), abi.encode(true));
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("getSigner(uint32)", 1), abi.encode(_signer1));
+
+        assertTrue(_manager.__verifySignature(bytes32(0), 1, signature_));
+    }
+
+    /* ============ getPayerReportDigest ============ */
+
+    function test_getPayerReportDigest() external view {
+        assertEq(
+            _manager.getPayerReportDigest({
+                originatorNodeId_: 1,
+                startSequenceId_: 2,
+                endSequenceId_: 3,
+                payersMerkleRoot_: bytes32(uint256(4)),
+                nodeIds_: new uint32[](5)
+            }),
+            0x65e9298004b22b280cef382f32439d34255f8eb6245503d0d402a6186ffe9fdc
+        );
+
+        assertEq(
+            _manager.getPayerReportDigest({
+                originatorNodeId_: 10,
+                startSequenceId_: 20,
+                endSequenceId_: 30,
+                payersMerkleRoot_: bytes32(uint256(40)),
+                nodeIds_: new uint32[](50)
+            }),
+            0xa768e78486ac9abadd510980ff8b1ba18e61d40358b934c09d10474ad5b65903
+        );
+    }
+
+    /* ============ getPayerReports ============ */
+
+    function test_getPayerReports() external {
+        assertEq(_manager.getPayerReports(1).length, 0);
+
+        uint32[] memory nodeIds_ = new uint32[](7);
+
+        for (uint32 index_; index_ < nodeIds_.length; ++index_) {
+            nodeIds_[index_] = 10 + index_;
+        }
+
+        _manager.__pushPayerReport({
+            originatorNodeId_: 1,
+            startSequenceId_: 2,
+            endSequenceId_: 3,
+            feesSettled_: 4,
+            offset_: 5,
+            isSettled_: true,
+            payersMerkleRoot_: bytes32(uint256(6)),
+            nodeIds_: nodeIds_
+        });
+
+        IPayerReportManager.PayerReport[] memory payerReports_ = _manager.getPayerReports(1);
+
+        assertEq(payerReports_.length, 1);
+
+        assertEq(payerReports_[0].startSequenceId, 2);
+        assertEq(payerReports_[0].endSequenceId, 3);
+        assertEq(payerReports_[0].feesSettled, 4);
+        assertEq(payerReports_[0].offset, 5);
+        assertTrue(payerReports_[0].isSettled);
+        assertEq(payerReports_[0].payersMerkleRoot, bytes32(uint256(6)));
+        assertEq(payerReports_[0].nodeIds.length, 7);
+
+        for (uint32 index_; index_ < nodeIds_.length; ++index_) {
+            assertEq(payerReports_[0].nodeIds[index_], 10 + index_);
+        }
+    }
+
+    /* ============ getPayerReport ============ */
+
+    function test_getPayerReport() external {
+        uint32[] memory nodeIds_ = new uint32[](7);
+
+        for (uint32 index_; index_ < nodeIds_.length; ++index_) {
+            nodeIds_[index_] = 10 + index_;
+        }
+
+        _manager.__pushPayerReport({
+            originatorNodeId_: 1,
+            startSequenceId_: 2,
+            endSequenceId_: 3,
+            feesSettled_: 4,
+            offset_: 5,
+            isSettled_: true,
+            payersMerkleRoot_: bytes32(uint256(6)),
+            nodeIds_: nodeIds_
+        });
+
+        IPayerReportManager.PayerReport memory payerReport_ = _manager.getPayerReport(1, 0);
+
+        assertEq(payerReport_.startSequenceId, 2);
+        assertEq(payerReport_.endSequenceId, 3);
+        assertEq(payerReport_.feesSettled, 4);
+        assertEq(payerReport_.offset, 5);
+        assertTrue(payerReport_.isSettled);
+        assertEq(payerReport_.payersMerkleRoot, bytes32(uint256(6)));
+        assertEq(payerReport_.nodeIds.length, 7);
+
+        for (uint32 index_; index_ < nodeIds_.length; ++index_) {
+            assertEq(payerReport_.nodeIds[index_], 10 + index_);
+        }
+    }
+
+    /* ============ DOMAIN_SEPARATOR ============ */
+
+    function test_DOMAIN_SEPARATOR() external view {
+        assertEq(
+            _manager.DOMAIN_SEPARATOR(),
+            keccak256(
+                abi.encode(
+                    _EIP712_DOMAIN_HASH,
+                    keccak256(bytes("PayerReportManager")),
+                    keccak256(bytes("1")),
+                    block.chainid,
+                    address(_manager)
+                )
+            )
+        );
+    }
+
+    /* ============ eip712Domain ============ */
+
+    function test_eip712Domain() external view {
+        (
+            bytes1 fields_,
+            string memory name_,
+            string memory version_,
+            uint256 chainId_,
+            address verifyingContract_,
+            bytes32 salt_,
+            uint256[] memory extensions_
+        ) = _manager.eip712Domain();
+
+        assertEq(fields_, hex"0f");
+        assertEq(name_, "PayerReportManager");
+        assertEq(version_, "1");
+        assertEq(chainId_, block.chainid);
+        assertEq(verifyingContract_, address(_manager));
+        assertEq(salt_, bytes32(0));
+        assertEq(extensions_.length, 0);
+    }
+
+    /* ============ helper functions ============ */
+
+    function _mockParameterRegistryCall(bytes memory key_, address value_) internal {
+        _mockParameterRegistryCall(key_, bytes32(uint256(uint160(value_))));
+    }
+
+    function _mockParameterRegistryCall(bytes memory key_, bool value_) internal {
+        _mockParameterRegistryCall(key_, value_ ? bytes32(uint256(1)) : bytes32(uint256(0)));
+    }
+
+    function _mockParameterRegistryCall(bytes memory key_, uint256 value_) internal {
+        _mockParameterRegistryCall(key_, bytes32(value_));
+    }
+
+    function _mockParameterRegistryCall(bytes memory key_, bytes32 value_) internal {
+        vm.mockCall(_parameterRegistry, abi.encodeWithSignature("get(bytes)", key_), abi.encode(value_));
+    }
+
+    function _getImplementationFromSlot(address proxy_) internal view returns (address implementation_) {
+        // Retrieve the implementation address directly from the proxy storage.
+        return address(uint160(uint256(vm.load(proxy_, EIP1967_IMPLEMENTATION_SLOT))));
+    }
+
+    function _getPayerReportSignature(
+        uint32 originatorNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        uint32[] memory nodeIds_,
+        uint256 privateKey_
+    ) internal view returns (bytes memory signature_) {
+        return
+            _getSignature(
+                _manager.getPayerReportDigest(
+                    originatorNodeId_,
+                    startSequenceId_,
+                    endSequenceId_,
+                    payersMerkleRoot_,
+                    nodeIds_
+                ),
+                privateKey_
+            );
+    }
+
+    function _getSignature(bytes32 digest_, uint256 privateKey_) internal pure returns (bytes memory signature_) {
+        (uint8 v_, bytes32 r_, bytes32 s_) = vm.sign(privateKey_, digest_);
+
+        return abi.encodePacked(r_, s_, v_);
+    }
+}
diff --git a/test/utils/Harnesses.sol b/test/utils/Harnesses.sol
index 658d2951..8e596a9b 100644
--- a/test/utils/Harnesses.sol
+++ b/test/utils/Harnesses.sol
@@ -17,6 +17,7 @@ import { PayloadBroadcaster } from "../../src/abstract/PayloadBroadcaster.sol";
 import { RateRegistry } from "../../src/settlement-chain/RateRegistry.sol";
 import { SettlementChainGateway } from "../../src/settlement-chain/SettlementChainGateway.sol";
 import { SettlementChainParameterRegistry } from "../../src/settlement-chain/SettlementChainParameterRegistry.sol";
+import { PayerReportManager } from "../../src/settlement-chain/PayerReportManager.sol";
 
 contract PayloadBroadcasterHarness is PayloadBroadcaster {
     constructor(address parameterRegistry_) PayloadBroadcaster(parameterRegistry_) {}
@@ -406,3 +407,61 @@ contract SequentialMerkleProofsHarness {
         return SequentialMerkleProofs._getReversedLeafNodesFromLeaves(leaves_);
     }
 }
+
+contract PayerReportManagerHarness is PayerReportManager {
+    constructor(
+        address parameterRegistry_,
+        address nodeRegistry_,
+        address payerRegistry_
+    ) PayerReportManager(parameterRegistry_, nodeRegistry_, payerRegistry_) {}
+
+    function __pushPayerReport(
+        uint32 originatorNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        uint96 feesSettled_,
+        uint32 offset_,
+        bool isSettled_,
+        bytes32 payersMerkleRoot_,
+        uint32[] calldata nodeIds_
+    ) external {
+        _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_].push(
+            PayerReport({
+                startSequenceId: startSequenceId_,
+                endSequenceId: endSequenceId_,
+                feesSettled: feesSettled_,
+                offset: offset_,
+                isSettled: isSettled_,
+                payersMerkleRoot: payersMerkleRoot_,
+                nodeIds: nodeIds_
+            })
+        );
+    }
+
+    function __verifySignatures(
+        uint32 originatorNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        uint32[] calldata nodeIds_,
+        PayerReportSignature[] calldata signatures_
+    ) external view returns (uint32[] memory validSigningNodeIds_) {
+        return
+            _verifySignatures(
+                originatorNodeId_,
+                startSequenceId_,
+                endSequenceId_,
+                payersMerkleRoot_,
+                nodeIds_,
+                signatures_
+            );
+    }
+
+    function __verifySignature(
+        bytes32 digest_,
+        uint32 nodeId_,
+        bytes calldata signature_
+    ) external view returns (bool isValid_) {
+        return _verifySignature(digest_, nodeId_, signature_);
+    }
+}
diff --git a/test/utils/Mocks.sol b/test/utils/Mocks.sol
index 8b3a5bcc..62a038eb 100644
--- a/test/utils/Mocks.sol
+++ b/test/utils/Mocks.sol
@@ -76,3 +76,22 @@ contract MockERC20Inbox {
         bytes calldata data_
     ) external returns (uint256 messageNumber_) {}
 }
+
+contract MockNodeRegistry {
+    uint8 public canonicalNodesCount;
+
+    function getIsCanonicalNode(uint32 nodeId_) external view returns (bool isCanonicalNode_) {
+        return true;
+    }
+
+    function getSigner(uint32 nodeId_) external view returns (address signer_) {}
+}
+
+contract MockPayerRegistry {
+    struct PayerFee {
+        address payer;
+        uint96 fee;
+    }
+
+    function settleUsage(PayerFee[] calldata payerFees_) external returns (uint96 feesSettled_) {}
+}


Reviews:
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-06T08:34:38Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 5**

<details>
<summary>🧹 Nitpick comments (13)</summary><blockquote>

<details>
<summary>test/utils/Mocks.sol (1)</summary><blockquote>

`90-97`: **Add return value to MockPayerRegistry.settleUsage**

The mock implementation of settleUsage doesn't return any value, even though the function signature indicates it should return a uint96. For more realistic testing, consider returning a value that represents the sum of fees.

```diff
-function settleUsage(PayerFee[] calldata payerFees_) external returns (uint96 feesSettled_) {}
+function settleUsage(PayerFee[] calldata payerFees_) external returns (uint96 feesSettled_) {
+    uint96 total = 0;
+    for (uint256 i = 0; i < payerFees_.length; i++) {
+        total += payerFees_[i].fee;
+    }
+    return total;
+}
```

</blockquote></details>
<details>
<summary>test/unit/PayerRegistry.t.sol (3)</summary><blockquote>

`530-536`: **Empty `PayerFee[]` is fine → but make explicit with `type(uint256).max` or a test helper.**

The tests for “not‐settler” and “paused” scenarios now construct a new, empty `PayerFee[]` directly in the call expression.  
That works, yet is a little brittle – it relies on Solidity’s array-allocation syntax in every test case.  

If a helper such as `_emptyPayerFees()` (or a shared constant) were introduced you could reuse it across unit tests, improving readability and removing two lines of cognitive overhead.

---

`547-565`: **Nit: drop the redundancies around the mocked `balanceOf` call.**

You already compute `30e6 + 10e6` on L 556 when you preset `_totalDeposits`.  
The same arithmetic re-appears on L 563 for the mocked balance.  
Storing the expected token balance in a local `uint96 balance_ = 40e6;` (then using it both for `_mockCall` and as an explanatory comment) keeps the intent crystal-clear and avoids an easy desync in future edits.

---

`575-579`: **Assertion on `feesSettled_` duplicates the inline sum.**

`assertEq(feesSettled_, 10e6 + 20e6 + 30e6);` works but obscures intent.  
Consider extracting to `uint96 expectedFees_ = 60e6;` before the call and reusing it for both `mockCall` (once you parameterise it) and the assertion, reinforcing domain meaning while cutting magic numbers.

</blockquote></details>
<details>
<summary>src/abstract/ERC721.sol (2)</summary><blockquote>

`12-17`: **Verify the hard-coded domain-type hash.**

`_EIP712_DOMAIN_HASH` is declared as  
`0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f`.

The canonical Keccak-256 of  
`"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"`  
is `0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f` – looks correct, but a comment confirming the pre-image avoids future doubt.

---

`51-54`: **Missing call to `_initializeERC712()` in inheritors can break caching.**

`_initializeERC712()` caches the initial domain separator, but nothing enforces that child contracts invoke it during their own initialisation. If an inheriting contract forgets the call, `initialDomainSeparator` remains zero and `DOMAIN_SEPARATOR()` will always recompute (cheaper now, but inconsistently gas-costly across chains).

Please document this requirement in NatSpec or make `_initializeERC712()` `internal virtual` and override it in an abstract “InitializableERC712” that throws unless called.

</blockquote></details>
<details>
<summary>src/settlement-chain/PayerReportManager.sol (1)</summary><blockquote>

`16-19`: **Open TODO: digest does not bind signer’s node ID**

The comment highlights a replay issue where a multi-node operator can re-use one signature across all their nodes.  
Because the EIP-712 digest omits `signatures_[i].nodeId`, an attacker could sign once and submit that signature under many node-IDs they control, inflating the quorum.

Consider adding `nodeId` into the typed-data hash to make each signature unique per node.

</blockquote></details>
<details>
<summary>test/utils/Harnesses.sol (2)</summary><blockquote>

`420-449`: **Harness copies `calldata` array directly into storage**

When building the `PayerReport` struct the code passes `nodeOperators_` (a `calldata` array) directly. The compiler currently performs an implicit copy to memory and then to storage, but this creates an extra allocation and future Solidity versions might tighten the rules.

For clarity and future-proofing copy explicitly:

```diff
-                nodeOperators: nodeOperators_
+                nodeOperators: nodeOperators_.toMemory() // explicit copy helper
```

(or declare `address[] memory ops = nodeOperators_;` beforehand).

Not critical for test-only code, but worth keeping consistent.

---

`380-383`: **Minor: duplicate helper already exists upstream**

`getLeafCount` was surfaced here, but a similar helper exists in the production library. If the harness only forwards the call consider inlining it in tests instead of extending the harness to keep its surface minimal.

</blockquote></details>
<details>
<summary>src/settlement-chain/interfaces/IPayerReportManager.sol (4)</summary><blockquote>

`14-23`: **Doc-comment drift – `feesSettled` field is missing from the NatSpec**

`PayerReport` now contains a `feesSettled` field (line 28), but the NatSpec block still documents `offset` directly after `endSequenceId`.  
Keeping NatSpec in sync with the struct prevents misunderstandings and autogenerated docs being out of date.

```diff
  * @param  endSequenceId    The end sequence ID.
+ * @param  feesSettled      The total fees already settled for this report.
  * @param  offset           The next index in the merkle tree that has yet to be processed/settled.
```

---

`47-56`: **Event documentation is out of sync with the actual event signature**

`PayerReportSubmitted` includes `payerReportIndex` in the emitted data but the NatSpec list omits it and the parameter order differs (docs list `attesterNodeId` before the index).

Recommend updating the comment so tooling (e.g., Slither, NatSpec generators) and developers see the real ABI.

```diff
- * @param  attesterNodeId   The attester node ID.
- * @param  startSequenceId  The start sequence ID.
+ * @param  payerReportIndex The index of the newly stored report.
+ * @param  attesterNodeId   The attester node ID.
+ * @param  startSequenceId  The start sequence ID.
```

---

`64-65`: **Consider gas/decoding impact of emitting dynamic arrays in events**

Both `address[] nodeOperators` and `uint32[] signingNodeIds` are emitted un-indexed.  
While perfectly legal, large arrays can make logs costly and harder to parse.  A more gas-efficient pattern is to emit a hash (e.g., keccak256) of the array for integrity and expose the raw list via a getter.

This may not be required now, but worth weighing against expected array sizes and on-chain log costs.

---

`162-166`: **Return a fixed-size type for `migratorParameterKey`**

Returning `bytes memory` from a pure view that always returns the same constant forces unnecessary dynamic-memory allocation.  
A `bytes32` (or `uint256`) constant is cheaper and simpler in both gas and ABI.

```diff
-function migratorParameterKey() external pure returns (bytes memory key_);
+function migratorParameterKey() external pure returns (bytes32 key_);
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between ad68a179f75de40da89b67e2f3fc4a2de58820ab and 88f44279fcdfd187fac68404d33d99ccd0af946d.

</details>

<details>
<summary>📒 Files selected for processing (15)</summary>

* `src/abstract/ERC5267.sol` (1 hunks)
* `src/abstract/ERC721.sol` (1 hunks)
* `src/abstract/interfaces/IERC5267.sol` (1 hunks)
* `src/abstract/interfaces/IERC712.sol` (1 hunks)
* `src/libraries/SequentialMerkleProofs.sol` (2 hunks)
* `src/settlement-chain/PayerRegistry.sol` (1 hunks)
* `src/settlement-chain/PayerReportManager.sol` (1 hunks)
* `src/settlement-chain/interfaces/External.sol` (1 hunks)
* `src/settlement-chain/interfaces/IPayerRegistry.sol` (2 hunks)
* `src/settlement-chain/interfaces/IPayerReportManager.sol` (1 hunks)
* `test/unit/PayerRegistry.t.sol` (3 hunks)
* `test/unit/PayerReportManager.t.sol` (1 hunks)
* `test/unit/SequentialMerkleProofs.t.sol` (1 hunks)
* `test/utils/Harnesses.sol` (3 hunks)
* `test/utils/Mocks.sol` (1 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (7)</summary>

* GitHub Check: Tests
* GitHub Check: Slither
* GitHub Check: Contacts Sizes
* GitHub Check: Lint check
* GitHub Check: Gas Report
* GitHub Check: Code Coverage
* GitHub Check: Formatting check

</details>

<details>
<summary>🔇 Additional comments (17)</summary><blockquote>

<details>
<summary>src/abstract/interfaces/IERC712.sol (1)</summary>

`1-13`: **Clean and well-structured ERC712 interface.**

The interface properly defines the EIP-712 domain separator function with appropriate documentation and follows best practices for Solidity interface design.

</details>
<details>
<summary>test/unit/SequentialMerkleProofs.t.sol (1)</summary>

`814-838`: **Comprehensive test coverage for the new getLeafCount function.**

The tests appropriately cover:
- Empty input validation with the `NoProofElements` error
- Boundary condition checking with the `InvalidLeafCount` error
- Positive cases with different valid inputs (1 and 1111)

This ensures the function behaves correctly in all scenarios.

</details>
<details>
<summary>src/libraries/SequentialMerkleProofs.sol (2)</summary>

`34-35`: **Well-defined custom error for leaf count validation.**

This error adds clear context when the leaf count exceeds the maximum uint32 value.

---

`73-85`: **Clean implementation of getLeafCount with proper validation.**

The function provides a useful utility to extract the leaf count from proof elements with appropriate checks:
- Validates the array is not empty
- Ensures the leaf count doesn't exceed uint32.max
- Returns a properly typed uint32 result

The implementation is concise and follows the library's error handling patterns.

</details>
<details>
<summary>src/settlement-chain/PayerRegistry.sol (2)</summary>

`186-189`: **Improved interface with struct-based input and useful return value.**

The function signature has been enhanced to:
1. Use the more type-safe `PayerFee[]` struct array instead of separate arrays
2. Return the total `feesSettled_` which is valuable for tracking settlement progress

This change aligns well with the PR objective of supporting distribution progress tracking in future developments.

---

`193-200`: **Well-adapted loop logic for the new struct-based approach.**

The loop implementation correctly:
- Extracts payer address and fee from the struct elements
- Accumulates the total fees in the return variable
- Maintains the same core state update logic

This ensures consistent behavior while providing additional functionality.

</details>
<details>
<summary>src/abstract/ERC5267.sol (3)</summary>

`11-17`: **LGTM: Contract inheritance and constructor defined properly**

The contract correctly inherits from both IERC5267 and ERC712, and the constructor properly calls the parent constructor.

---

`21-23`: **LGTM: Initialization function properly calls parent**

The internal initialization function correctly calls the parent initialization function, maintaining the proper initialization chain.

---

`27-50`: **LGTM: Correct implementation of EIP-5267 eip712Domain function**

The implementation of `eip712Domain()` correctly returns the domain separator components as specified in EIP-5267. The bit field `0x0f` (binary 01111) indicates that name, version, chainId, and verifyingContract fields are present, while salt is not used (though zeroed out as per the standard).

</details>
<details>
<summary>src/settlement-chain/interfaces/IPayerRegistry.sol (2)</summary>

`29-37`: **LGTM: Well-defined PayerFee struct**

The PayerFee struct is well-defined with clear documentation for each field. This encapsulation improves code readability and maintainability by grouping related data.

---

`222-225`: **LGTM: Improved settleUsage function signature**

The updated function signature is an improvement over using two separate arrays:
1. It prevents array length mismatch errors
2. It groups related data together
3. It adds a return value for the total fees settled, which will be helpful for tracking

This change aligns with best practices for Solidity function design.

</details>
<details>
<summary>test/utils/Mocks.sol (1)</summary>

`80-88`: **LGTM: Appropriate mock implementation for NodeRegistry**

The MockNodeRegistry provides the necessary functionality for testing, implementing the required view functions that would be called by contracts that depend on INodeRegistryLike.

</details>
<details>
<summary>src/settlement-chain/interfaces/External.sol (2)</summary>

`93-103`: **LGTM: Well-defined NodeRegistry interface subset**

The INodeRegistryLike interface provides a minimal set of required functions for interacting with a NodeRegistry, following the established pattern in this file. It has clear documentation and appropriate function signatures.

---

`105-116`: **LGTM: Consistent PayerRegistry interface subset**

The IPayerRegistryLike interface maintains consistency with the updated IPayerRegistry interface, using the same PayerFee struct and settleUsage function signature. This ensures consistent use of the new structure throughout the codebase.

</details>
<details>
<summary>src/abstract/interfaces/IERC5267.sol (1)</summary>

`18-45`: **Interface aligns with EIP-5267 – but confirm `bytes1` is large enough.**

EIP-5267 specifies a 256-bit bitmap (`bytes32`) for `fields`; later drafts propose a single byte.  
The current canonical spec (as of April 2024) still shows `bytes1`. If draft status changes, a one-byte bitmap could become too small once additional domain fields are standardised.  
Keeping `bytes32 fields_` today would be forward-compatible at zero extra deployment cost.


Would you like me to open a small follow-up to bump the type or add an explanatory NatSpec note?

</details>
<details>
<summary>src/settlement-chain/PayerReportManager.sol (1)</summary>

`188-194`: **Check for potential under/overflow when emitting `PayerReportSubsetSettled`**

`leafCount_ - payerReport_.offset` is cast to `uint32` inside the event. While `leafCount_` originates from an internal `uint256`, the cast will truncate silently if `leafCount_ >= 2^32`, and the subtraction will under-flow if a malicious proof managed to advance `offset` beyond `leafCount_` (even though the library should prevent it).

Consider:

1. Keeping both values as `uint256` in the event, or  
2. Adding an explicit `require(leafCount_ <= type(uint32).max, "Leaf count overflow")`.

Also apply the same guard to `uint32(payerFees_.length)`.

</details>
<details>
<summary>src/settlement-chain/interfaces/IPayerReportManager.sol (1)</summary>

`2-2`: <details>
<summary>❌ Incorrect review comment</summary>

**Confirm compiler availability – `0.8.28` is not yet a stable release**

The pragma points to `0.8.28`, a version that is not available in the current official Solidity releases (latest stable is `0.8.23/0.8.24`).  
Unless you are intentionally pinning to a nightly build, this will break CI/forge compilation for every other contributor.

```diff
-pragma solidity 0.8.28;
+pragma solidity ^0.8.24; // or the latest officially-released version your tool-chain supports
```

---

🏁 Script executed:

```shell
#!/usr/bin/env bash
# Check if Solidity v0.8.28 is an official release on GitHub
curl -s https://api.github.com/repos/ethereum/solidity/releases \
  | grep -E '"tag_name":\s*"v0\.8\.28"' && echo "found" || echo "not found"
```

Length of output: 149

---



</details>

**No change needed: Solidity 0.8.28 is an official release**  
Verification against the Solidity GitHub releases confirms that tag `v0.8.28` exists, so the pinned pragma is valid and will compile under supported tool-chains.  


> Likely an incorrect or invalid review comment.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-06T08:35:51Z
user: login: deluca-mike
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T08:39:06Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-06T08:39:25Z
user: login: deluca-mike
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T08:41:53Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-06T08:42:26Z
user: login: deluca-mike
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T08:42:51Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-06T08:43:15Z
user: login: deluca-mike
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T08:56:47Z
user: login: deluca-mike
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T08:56:55Z
user: login: deluca-mike
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T08:57:04Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>♻️ Duplicate comments (2)</summary><blockquote>

<details>
<summary>src/abstract/interfaces/IERC712.sol (1)</summary><blockquote>

`11-13`: **Naming of `DOMAIN_SEPARATOR` is intentional and EIP-712-compliant**

The uppercase constant-style name is mandated by the EIP-712 spec and is widely accepted in the ecosystem (e.g. OpenZeppelin).  
The accompanying `// slither-disable-next-line naming-convention` pragma is therefore appropriate, and the lint warning can be safely ignored.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: Lint check</summary>

[warning] 13-13: 
Function name must be in mixedCase

</details>

</details>

</blockquote></details>
<details>
<summary>src/settlement-chain/PayerReportManager.sol (1)</summary><blockquote>

`178-186`: _🛠️ Refactor suggestion_

_⚠️ Potential issue_

**Re-entrancy window & opaque low-level call to `payerRegistry`**

Effects are performed (offset, `isSettled`) and *then* an external low-level call (`.call`) is executed.  
If `payerRegistry` is malicious or compromised it can re-enter `settle` (or other external-state-changing functions) before the first call finishes, potentially:

* Calling `settle` again with a different `payerFees_`, causing double-settlement;
* Exhausting gas and reverting the first context, leaving state inconsistent.

Recommendations
```diff
+import {ReentrancyGuardUpgradeable as ReentrancyGuard} from
+    "../../lib/oz-upgradeable/contracts/security/ReentrancyGuardUpgradeable.sol";

-contract PayerReportManager is IPayerReportManager, Initializable, Migratable, ERC5267 {
+contract PayerReportManager is
+    IPayerReportManager,
+    Initializable,
+    Migratable,
+    ERC5267,
+    ReentrancyGuard
+{
```
and mark both `submit` and `settle` as `nonReentrant`.

Additionally prefer the typed interface call:

```diff
-        (bool success_, bytes memory returnData_) = payerRegistry.call(
-            abi.encodeWithSelector(IPayerRegistryLike.settleUsage.selector, payerFees_)
-        );
+        (bool success_, bytes memory returnData_) =
+            IPayerRegistryLike(payerRegistry).settleUsage(payerFees_);
```

Typed calls are safer (bubble up custom errors) and easier to audit.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>src/abstract/ERC712.sol (2)</summary><blockquote>

`25-33`: **Struct declared after state variables – minor style nit**

Some linters (and the GitHub check you saw) expect `struct`/`enum` declarations to precede state variables.  
This does not affect byte-code but keeping declarations first improves readability and avoids recurrent lint noise.

```diff
-    /* ============ UUPS Storage ============ */
-
-    /**
-     * @custom:storage-location erc7201:xmtp.storage.ERC712
-     * @notice The UUPS storage for the ERC712 contract.
-     * @param  initialDomainSeparator Initial EIP-712 domain separator set at intialization.
-     */
-    struct ERC712Storage {
+    /* ============ Storage ============ */
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.ERC712
+     * @notice UUPS storage for the ERC712 contract.
+     * @param  initialDomainSeparator Initial EIP-712 domain separator set at initialization.
+     */
+    struct ERC712Storage {
         bytes32 initialDomainSeparator;
     }
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: Lint check</summary>

[warning] 25-25: 
Function order is incorrect, struct definition can not go after state variable declaration (line 16)

---

[warning] 25-25: 
Function order is incorrect, struct definition can not go after state variable declaration (line 16)

</details>

</details>

---

`92-94`: **Tiny docstring typo**

`"he name"` → `"The name"`

```diff
-     * @dev he name of the contract.
+     * @dev The name of the contract.
```

</blockquote></details>
<details>
<summary>src/abstract/ERC5267.sol (1)</summary><blockquote>

`21-24`: **Expose `_initializeERC5267()` as `virtual` for upgradeable flexibility?**

If downstream upgradeable contracts need to extend initialization (e.g. set name/version storage before the domain separator is cached), marking the initializer `virtual` would let them `override` and compose initialisation order safely.

```diff
-    function _initializeERC5267() internal {
+    function _initializeERC5267() internal virtual {
         _initializeERC712();
     }
```
Not strictly necessary today, but helps avoid “double initialiser” patterns later.

</blockquote></details>
<details>
<summary>src/settlement-chain/PayerReportManager.sol (2)</summary><blockquote>

`334-344`: **Gas hotspot: double external calls per signature**

`_verifySignatures` calls `_verifySignature` for each element; `_verifySignature` performs TWO external `view`
calls (`getIsCanonicalNode`, `ownerOf`).  In worst-case scenarios this can surpass the 50k gas stipend for
transactions with many signatures.

Possible optimisation:

```diff
-            if (!(isValid_[index_] = _verifySignature(digest_, nodeId_, signatures_[index_].signature))) continue;
+            // Cache canonical flag to avoid calling ownerOf on non-canonical nodes.
+            if (!INodeRegistryLike(nodeRegistry).getIsCanonicalNode(nodeId_)) continue;
+
+            address signer_ = ECDSA.tryRecoverCalldata(digest_, signatures_[index_].signature);
+            if (signer_ == address(0)) continue;
+
+            if (signer_ != INodeRegistryLike(nodeRegistry).ownerOf(nodeId_)) continue;
+
+            isValid_[index_] = true;
```

Caching one interface reference at the top of the function also saves 400 gas per call.

Not critical for correctness but worthwhile for large‐scale settlements.

---

`16-18`: **Open TODO: signature replay across multi-node operators**

The comment highlights a real replay risk: an operator that controls several nodes can recycle the same
signature because the nodeId is not part of the EIP-712 struct.

Before main-net deployment, either:

1. Accept the risk and document it, or  
2. Extend `PAYER_REPORT_TYPEHASH` to include `signerNodeId` (breaking change) and bump `version()`.

Would you like an updated type-hash snippet?

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 88f44279fcdfd187fac68404d33d99ccd0af946d and 1950d5120058b5b20b133c3cbb5dc70dfa0dd955.

</details>

<details>
<summary>📒 Files selected for processing (6)</summary>

* `src/abstract/ERC5267.sol` (1 hunks)
* `src/abstract/ERC712.sol` (1 hunks)
* `src/abstract/interfaces/IERC712.sol` (1 hunks)
* `src/libraries/SequentialMerkleProofs.sol` (3 hunks)
* `src/settlement-chain/PayerReportManager.sol` (1 hunks)
* `test/unit/PayerReportManager.t.sol` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* src/libraries/SequentialMerkleProofs.sol
* test/unit/PayerReportManager.t.sol

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>src/settlement-chain/PayerReportManager.sol (1)</summary>

```
Learnt from: deluca-mike
PR: xmtp/smart-contracts#56
File: src/settlement-chain/PayerReportManager.sol:325-333
Timestamp: 2025-05-06T08:42:22.880Z
Learning: The XMTP protocol limits canonical nodes to a maximum that can fit in a `uint8` (255 nodes), which is why the `validSignatureCount_` variable in `PayerReportManager` is typed as `uint8`.
```

</details>

</details><details>
<summary>🪛 GitHub Check: Lint check</summary>

<details>
<summary>src/abstract/ERC712.sol</summary>

[warning] 25-25: 
Function order is incorrect, struct definition can not go after state variable declaration (line 16)

---

[warning] 25-25: 
Function order is incorrect, struct definition can not go after state variable declaration (line 16)

---

[warning] 59-59: 
Function name must be in mixedCase                                                                  

</details>
<details>
<summary>src/abstract/ERC5267.sol</summary>

[warning] 28-28: 
Function order is incorrect, external const function can not go after internal function (line 21)

---

[warning] 28-28: 
Function order is incorrect, external view function can not go after internal  function (line 21)

</details>
<details>
<summary>src/abstract/interfaces/IERC712.sol</summary>

[warning] 13-13: 
Function name must be in mixedCase

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (6)</summary>

* GitHub Check: Formatting check
* GitHub Check: Slither
* GitHub Check: Code Coverage
* GitHub Check: Tests
* GitHub Check: Gas Report
* GitHub Check: Contacts Sizes

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>src/abstract/ERC5267.sol (1)</summary>

`41-49`: **Confirm the `fields_` bit-mask is accurate**

Using `hex"0f"` sets bits 0-3 (name, version, chainId, verifyingContract) and clears bits 4-5 (salt, extensions).  
Because you return a zero salt and an empty extensions array, this mask appears correct.  
No action required—just calling this out for future readers.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-06T08:59:48Z
user: login: deluca-mike
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T17:12:37Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-06T17:13:11Z
user: login: deluca-mike
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T17:13:24Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-06T17:13:57Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (2)</summary><blockquote>

<details>
<summary>src/settlement-chain/PayerReportManager.sol (2)</summary><blockquote>

`367-380`: _🛠️ Refactor suggestion_

**External calls in loop may lead to performance issues**

The `_verifySignature` function makes two external calls to the node registry (`getIsCanonicalNode` and `ownerOf`) for each signature being verified. When called in a loop, this pattern can be expensive and potentially hit block gas limits with a large number of signatures.

Consider refactoring to batch these calls or cache results when possible:

```diff
 function _verifySignatures(
     // ... existing parameters
 ) internal view returns (uint32[] memory validSigningNodeIds_) {
     // ... existing code
+    
+    // Pre-fetch canonical status for all nodes to reduce external calls
+    mapping(uint32 => bool) memory isCanonical = new mapping(uint32 => bool)(signatures_.length);
+    mapping(uint32 => address) memory nodeOwners = new mapping(uint32 => address)(signatures_.length);
+    
+    for (uint256 index_; index_ < signatures_.length; ++index_) {
+        uint32 nodeId_ = signatures_[index_].nodeId;
+        isCanonical[nodeId_] = INodeRegistryLike(nodeRegistry).getIsCanonicalNode(nodeId_);
+        if (isCanonical[nodeId_]) {
+            nodeOwners[nodeId_] = INodeRegistryLike(nodeRegistry).ownerOf(nodeId_);
+        }
+    }
     
     // ... then use the cached values in the signature verification loop
```

Note: The above suggestion uses a conceptual approach - Solidity doesn't directly support mapping creation like this, so you would need to implement a different caching strategy that fits your use case.

In the worst case, if all signatures in the loop are valid, this function would make 2 * N external calls, where N is the number of signatures. Consider optimizing this for better gas efficiency and to avoid potential gas limit issues.

---

`190-196`: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Ensure reentrancy protection is in place**

The event is emitted after an external call, which could potentially be vulnerable to reentrancy attacks if the called contract implements callbacks or manipulates state that this contract depends on.

The `// slither-disable-next-line reentrancy-events` comment suggests this was flagged by a static analyzer but deemed acceptable.

To verify this is secure, ensure that:
1. The `payerRegistry` cannot re-enter this contract
2. The order of operations follows the checks-effects-interactions pattern

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if there are any callbacks or hooks in the payer registry
# that could trigger reentrant calls

# Look for potential callback functions in the payer registry
echo "Searching for potential callbacks in payer registry..."
rg -A 3 -B 3 "function.*callback|hook|after|before" --glob "src/settlement-chain/PayerRegistry.sol"

# Check if there are any delegatecalls that might bypass protections
echo "Checking for delegatecall usage..."
rg "delegatecall" --glob "src/settlement-chain/PayerRegistry.sol"
```

Length of output: 361

---

To get a clearer picture of the call sequence and any reentrancy guards, let’s pull in the surrounding context and check for modifiers:

---

🏁 Script executed:

```shell
#!/bin/bash
# Show lines 150–260 of the contract to inspect external calls around the emit
sed -n '150,260p' src/settlement-chain/PayerReportManager.sol

# Check for use of nonReentrant modifier
rg -n 'nonReentrant' --glob 'src/settlement-chain/PayerReportManager.sol'

# Check if the contract inherits from OpenZeppelin’s ReentrancyGuard
rg -n 'ReentrancyGuard' --glob 'src/settlement-chain/PayerReportManager.sol'

# Locate the contract declaration to see its full inheritance list
rg -A3 -B3 'contract PayerReportManager' --glob 'src/settlement-chain/PayerReportManager.sol'
```

Length of output: 5018

---



</details>

**Add explicit reentrancy protection and enforce checks-effects-interactions**

This function currently:  
1. Updates `offset` and `isSettled`  
2. Calls out to `payerRegistry`  
3. Emits the event  
4. Finally updates `feesSettled`  

Without a `nonReentrant` guard or moving *all* state changes before the external call, a malicious `payerRegistry` could reenter and run `settle` again before `feesSettled` is updated.

Please address by:  
- Inheriting OpenZeppelin’s `ReentrancyGuard` in `PayerReportManager.sol`.  
- Adding the `nonReentrant` modifier to `settle()`.  
- Moving the line  
  ```diff
  -    // slither-disable-next-line reentrancy-events
       emit PayerReportSubsetSettled(...);
  
       payerReport_.feesSettled += feesSettled_;
  ```
  to *before* the low-level call (or at least before any external interaction), so that **all** state updates occur prior to the `.call(...)`.  
- Reviewing `PayerRegistry.sol`’s `settleUsage` implementation to confirm it never calls back into this contract.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>src/settlement-chain/PayerReportManager.sol (2)</summary><blockquote>

`180-182`: **Consider using a safer interface call pattern**

Low-level calls bypass type checking and can be error-prone. While your implementation includes proper error handling, consider using a safer interface-based approach when possible.

The current implementation uses a low-level call to `payerRegistry` with manual ABI encoding, which requires more careful error handling.

```diff
-        // slither-disable-next-line low-level-calls
-        (bool success_, bytes memory returnData_) = payerRegistry.call(
-            abi.encodeWithSelector(IPayerRegistryLike.settleUsage.selector, payerFees_)
-        );
-
-        if (!success_) revert SettleUsageFailed(returnData_);
-
-        uint96 feesSettled_ = abi.decode(returnData_, (uint96));
+        uint96 feesSettled_ = IPayerRegistryLike(payerRegistry).settleUsage(payerFees_);
```

This would make the code more type-safe and easier to read, at the potential cost of slightly higher gas usage due to interface method dispatch.

---

`128-138`: **Ensure zero-value validations for payer report fields**

When creating a new payer report, consider validating that critical fields like `payersMerkleRoot_` and `nodeOperators_` are non-empty. For example, a zero merkle root or an empty node operators array might indicate invalid input data.

```diff
+        // Validate critical report fields
+        if (payersMerkleRoot_ == bytes32(0)) revert InvalidPayersMerkleRoot();
+        if (nodeOperators_.length == 0) revert EmptyNodeOperators();
+
         payerReports_.push(
             PayerReport({
                 attesterNodeId: attesterNodeId_,
```

Adding these checks would help prevent storing invalid or meaningless payer reports.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 1950d5120058b5b20b133c3cbb5dc70dfa0dd955 and 2a208be718e592589107a96839db6eac61b2836a.

</details>

<details>
<summary>📒 Files selected for processing (15)</summary>

* `src/abstract/ERC5267.sol` (1 hunks)
* `src/abstract/ERC712.sol` (1 hunks)
* `src/abstract/interfaces/IERC5267.sol` (1 hunks)
* `src/abstract/interfaces/IERC712.sol` (1 hunks)
* `src/libraries/SequentialMerkleProofs.sol` (3 hunks)
* `src/settlement-chain/PayerRegistry.sol` (1 hunks)
* `src/settlement-chain/PayerReportManager.sol` (1 hunks)
* `src/settlement-chain/interfaces/External.sol` (1 hunks)
* `src/settlement-chain/interfaces/IPayerRegistry.sol` (2 hunks)
* `src/settlement-chain/interfaces/IPayerReportManager.sol` (1 hunks)
* `test/unit/PayerRegistry.t.sol` (3 hunks)
* `test/unit/PayerReportManager.t.sol` (1 hunks)
* `test/unit/SequentialMerkleProofs.t.sol` (1 hunks)
* `test/utils/Harnesses.sol` (3 hunks)
* `test/utils/Mocks.sol` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* src/abstract/interfaces/IERC712.sol

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (13)</summary>

* src/abstract/ERC5267.sol
* test/unit/PayerRegistry.t.sol
* test/utils/Mocks.sol
* src/settlement-chain/interfaces/IPayerRegistry.sol
* src/settlement-chain/PayerRegistry.sol
* src/libraries/SequentialMerkleProofs.sol
* src/settlement-chain/interfaces/External.sol
* test/unit/SequentialMerkleProofs.t.sol
* src/abstract/interfaces/IERC5267.sol
* src/abstract/ERC712.sol
* src/settlement-chain/interfaces/IPayerReportManager.sol
* test/utils/Harnesses.sol
* test/unit/PayerReportManager.t.sol

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>src/settlement-chain/PayerReportManager.sol (2)</summary>

```
Learnt from: deluca-mike
PR: xmtp/smart-contracts#56
File: src/settlement-chain/PayerReportManager.sol:325-333
Timestamp: 2025-05-06T08:42:22.880Z
Learning: The XMTP protocol limits canonical nodes to a maximum that can fit in a `uint8` (255 nodes), which is why the `validSignatureCount_` variable in `PayerReportManager` is typed as `uint8`.
```

```
Learnt from: deluca-mike
PR: xmtp/smart-contracts#56
File: src/settlement-chain/PayerReportManager.sol:346-347
Timestamp: 2025-05-06T17:13:52.649Z
Learning: The `canonicalNodesCount()` function in `INodeRegistryLike` returns a `uint8`, not a `uint256`, so no explicit casting is needed when assigning its value to a `uint8` variable.
```

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>src/settlement-chain/PayerReportManager.sol (5)</summary>

`327-333`: **`validSignatureCount_` type is appropriately sized**

Using `uint8` for `validSignatureCount_` is appropriate since the XMTP protocol limits canonical nodes to 255 (max value of `uint8`). The built-in overflow protection in Solidity 0.8.x will revert if this limit is somehow exceeded.

This aligns with the protocol's design constraints as noted in the retrieved learnings.

---

`346-347`: **Canonical nodes count access is type-safe**

The code correctly uses the return type from `canonicalNodesCount()` which is `uint8`, so no explicit casting is needed. This is verified in the project's interface definitions and confirmed in the retrieved learnings.

---

`153-199`: **The `settle` function handles Merkle proof verification correctly**

The function properly:
1. Verifies the payer report exists and hasn't been fully settled
2. Uses `SequentialMerkleProofs` to verify the provided fees are the next sequential subset
3. Updates the offset and settlement status
4. Processes the fees through the payer registry
5. Updates the report's settled fee total

The code correctly uses the new `getLeafCount` function from the `SequentialMerkleProofs` library to determine if the report is fully settled.

---

`355-363`: **Good use of filtering with efficient array construction**

The code correctly uses a separate write index to efficiently build the `validSigningNodeIds_` array, skipping invalid signatures. This prevents potential out-of-bounds writes and is more gas-efficient than a two-pass approach.

The `unchecked` block is appropriate here since `writeIndex_` can never exceed `validSignatureCount_` (which is the array's length).

---

`47-53`: **Use of ERC-7201 namespaced storage pattern is a best practice**

The contract correctly implements the ERC-7201 namespaced storage pattern for upgradeable contracts, which isolates storage variables to prevent collisions during upgrades. This is an important security measure for upgradeable contracts.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-06T17:17:47Z
user: login: neekolas
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-06T18:51:28Z
user: login: neekolas
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-06T19:30:45Z
user: login: neekolas
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-06T19:35:44Z
user: login: neekolas
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T00:41:16Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (2)</summary><blockquote>

<details>
<summary>src/abstract/interfaces/IERC712.sol (1)</summary><blockquote>

`13-13`: **Function name `DOMAIN_SEPARATOR` doesn't follow Solidity naming conventions.**

While Solidity naming convention requires function names to be in mixedCase, this function intentionally uses uppercase to follow the EIP-712 standard convention. This is appropriate for compliance with the standard, but we should disable the Solidity linter warning.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: Lint check</summary>

[warning] 13-13: 
Function name must be in mixedCase

</details>

</details>

</blockquote></details>
<details>
<summary>test/unit/PayerReportManager.t.sol (1)</summary><blockquote>

`180-264`: **Potential issue: Test might be hiding a bug in zero-length node operators array.**

The `test_submit_zeroPayersMerkleRoot` test is passing with an empty `nodeOperators_` array, but there's a past review comment about a potential array bounds issue. The test should explicitly assert the length of the array after submission to catch any potential overflow issues.



Based on a previous review comment, the test might be passing despite a hidden bug. Please add an explicit check of `nodeOperators.length` as previously suggested:

```diff
    assertEq(payerReport_.payersMerkleRoot, bytes32(0));
    assertEq(payerReport_.nodeOperators.length, 0);
+   // Verify no out-of-bounds issues
+   IPayerReportManager.PayerReport memory pr = _manager.getPayerReport(0, 1);
+   assertEq(pr.nodeOperators.length, 0);
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>src/abstract/ERC5267.sol (1)</summary><blockquote>

`1-51`: **Clean and standards-compliant implementation of EIP-5267.**

The implementation properly extends ERC712 and follows the EIP-5267 standard for domain retrieval. The `eip712Domain()` method correctly returns all required domain fields with appropriate bit flags (0x0f = 01111) indicating which domain components are used.

Note: According to static analysis, there's a function order issue with external functions appearing after internal ones (line 28). Consider reordering functions to follow Solidity style conventions.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: Lint check</summary>

[warning] 28-28: 
Function order is incorrect, external const function can not go after internal function (line 21)

---

[warning] 28-28: 
Function order is incorrect, external view function can not go after internal  function (line 21)

</details>

</details>

</blockquote></details>
<details>
<summary>src/settlement-chain/PayerReportManager.sol (1)</summary><blockquote>

`371-384`: **Consider Node vs Owner private key signing approach.**

As noted in a previous comment, the current implementation validates signatures from the owner of the node NFT, not the node's private key. This may present a security concern as it requires node operators to use their valuable wallet's private key for operations.

Consider modifying the validation approach to allow using the node's private key for signatures, which would let operators keep their valuable NFT in a separate wallet from the operational key.

</blockquote></details>
<details>
<summary>test/unit/PayerReportManager.t.sol (3)</summary><blockquote>

`109-133`: **Tests invalid sequence IDs but should validate expected sequence ID values.**

While the tests verify invalid sequence IDs are rejected, consider adding a test case to confirm what valid sequence ID parameters look like, to document the intended behavior.

---

`405-440`: **Mock call verification is incomplete.**

There's a TODO comment for `expectCallAndRevert` which should be addressed. The test might not be fully verifying the interaction between contracts.


Consider implementing the suggested improvement to more accurately test the interface between contracts:

```diff
-        // TODO: `expectCallAndRevert`.
+        // Verify both the call and the revert behavior
         vm.mockCallRevert(
             _payerRegistry,
             abi.encodeWithSignature("settleUsage((address,uint96)[])", payerFees_),
             "Test Failure"
         );
```

---

`570-644`: **Migration tests with good error handling.**

The tests cover migration failures (zero migrator, migration errors, empty code) and successful migration with state preservation verification. The TODO comment for `_expectAndMockParameterRegistryCall` should be addressed.


Consider implementing the suggested improvement:

```diff
-        // TODO: `_expectAndMockParameterRegistryCall`.
+        // Expect the parameter registry call and mock the response
         _mockParameterRegistryCall(_MIGRATOR_KEY, migrator_);
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 2a208be718e592589107a96839db6eac61b2836a and de5a013b2686cfebbdb4cb3d583f8ddcbb406b2e.

</details>

<details>
<summary>📒 Files selected for processing (15)</summary>

* `src/abstract/ERC5267.sol` (1 hunks)
* `src/abstract/ERC712.sol` (1 hunks)
* `src/abstract/interfaces/IERC5267.sol` (1 hunks)
* `src/abstract/interfaces/IERC712.sol` (1 hunks)
* `src/libraries/SequentialMerkleProofs.sol` (5 hunks)
* `src/settlement-chain/PayerRegistry.sol` (1 hunks)
* `src/settlement-chain/PayerReportManager.sol` (1 hunks)
* `src/settlement-chain/interfaces/External.sol` (1 hunks)
* `src/settlement-chain/interfaces/IPayerRegistry.sol` (2 hunks)
* `src/settlement-chain/interfaces/IPayerReportManager.sol` (1 hunks)
* `test/unit/PayerRegistry.t.sol` (3 hunks)
* `test/unit/PayerReportManager.t.sol` (1 hunks)
* `test/unit/SequentialMerkleProofs.t.sol` (1 hunks)
* `test/utils/Harnesses.sol` (3 hunks)
* `test/utils/Mocks.sol` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (10)</summary>

* src/settlement-chain/interfaces/IPayerRegistry.sol
* src/settlement-chain/PayerRegistry.sol
* src/settlement-chain/interfaces/External.sol
* test/unit/PayerRegistry.t.sol
* src/abstract/interfaces/IERC5267.sol
* test/utils/Mocks.sol
* src/libraries/SequentialMerkleProofs.sol
* test/unit/SequentialMerkleProofs.t.sol
* test/utils/Harnesses.sol
* src/settlement-chain/interfaces/IPayerReportManager.sol

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (2)</summary>

<details>
<summary>src/settlement-chain/PayerReportManager.sol (1)</summary>

```
Learnt from: deluca-mike
PR: xmtp/smart-contracts#56
File: src/settlement-chain/PayerReportManager.sol:346-347
Timestamp: 2025-05-06T17:13:52.649Z
Learning: The `canonicalNodesCount()` function in `INodeRegistryLike` returns a `uint8`, not a `uint256`, so no explicit casting is needed when assigning its value to a `uint8` variable.
```

</details>
<details>
<summary>src/abstract/ERC712.sol (1)</summary>

```
Learnt from: deluca-mike
PR: xmtp/smart-contracts#56
File: src/abstract/ERC712.sol:59-62
Timestamp: 2025-05-06T17:13:09.750Z
Learning: For ERC712/EIP-712 implementations, while EIP-5267 recommends notifying clients of domain changes, technical constraints may prevent automatic event emission when chain ID changes occur.
```

</details>

</details><details>
<summary>🪛 GitHub Check: Lint check</summary>

<details>
<summary>src/abstract/ERC5267.sol</summary>

[warning] 28-28: 
Function order is incorrect, external const function can not go after internal function (line 21)

---

[warning] 28-28: 
Function order is incorrect, external view function can not go after internal  function (line 21)

</details>
<details>
<summary>src/abstract/ERC712.sol</summary>

[warning] 25-25: 
Function order is incorrect, struct definition can not go after state variable declaration (line 16)

---

[warning] 25-25: 
Function order is incorrect, struct definition can not go after state variable declaration (line 16)

---

[warning] 59-59: 
Function name must be in mixedCase                                                                  

</details>
<details>
<summary>src/abstract/interfaces/IERC712.sol</summary>

[warning] 13-13: 
Function name must be in mixedCase

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: Slither
* GitHub Check: Gas Report
* GitHub Check: Code Coverage
* GitHub Check: Formatting check

</details>

<details>
<summary>🔇 Additional comments (20)</summary><blockquote>

<details>
<summary>src/abstract/interfaces/IERC712.sol (1)</summary>

`1-14`: **Well-designed interface following EIP-712 standard.**

The interface correctly defines the core EIP-712 domain separator functionality. This serves as a foundational interface for typed structured data hashing and signing that will be extended by EIP-5267 interfaces and implemented by contracts needing signature verification.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: Lint check</summary>

[warning] 13-13: 
Function name must be in mixedCase

</details>

</details>

</details>
<details>
<summary>src/abstract/ERC5267.sol (1)</summary>

`42-49`: **Well-structured domain field mapping.**

The implementation of the domain fields is thorough, correctly including:
1. Fields flag (01111, salt is not used) 
2. Domain name from parent contract
3. Version from parent contract
4. Current chain ID
5. Contract address as the verifying contract
6. Empty salt (bytes32(0))
7. Empty extensions array

This provides proper domain separation for EIP-712 signatures.

</details>
<details>
<summary>src/abstract/ERC712.sol (4)</summary>

`13-16`: **Verified domain hash constant.**

The EIP-712 domain hash constant has been correctly computed for the standard domain structure without the salt field. This matches the keccak256 hash of "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" as specified in the comment.

---

`25-38`: **Secure UUPS storage pattern implementation.**

The contract follows best practices for storage isolation using ERC-7201 namespaced storage slots. This prevents storage collision issues when upgrading contracts. The storage location constant is correctly computed and the assembly block properly accesses it.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: Lint check</summary>

[warning] 25-25: 
Function order is incorrect, struct definition can not go after state variable declaration (line 16)

---

[warning] 25-25: 
Function order is incorrect, struct definition can not go after state variable declaration (line 16)

</details>

</details>

---

`59-61`: **Efficient domain separator caching with chain ID-aware refreshing.**

The implementation correctly caches the domain separator at initialization and recomputes it only when the chain ID changes, optimizing gas costs. This handles cross-chain deployments and hard forks appropriately.

Note that while EIP-5267 recommends emitting an event when the domain changes, view functions cannot emit events in Solidity, and there's no hook mechanism to detect chain ID changes directly.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: Lint check</summary>

[warning] 59-59: 
Function name must be in mixedCase                                                                  

</details>

</details>

---

`87-89`: **Standard-compliant EIP-712 digest computation.**

The function correctly calculates the final digest by:
1. Prefixing with "\x19\x01"
2. Including the domain separator
3. Including the structured data hash

This follows the EIP-712 standard format for typed data signing.

</details>
<details>
<summary>src/settlement-chain/PayerReportManager.sol (4)</summary>

`134-134`: **Good defensive programming for empty Merkle trees.**

The implementation correctly marks a report as settled if it contains an empty tree root, saving gas by avoiding unnecessary settlement operations for empty reports.

---

`180-193`: **Safer external call handling.**

The implementation properly:
1. Updates state before making external calls (pattern to prevent reentrancy)
2. Uses a low-level call with proper error handling
3. Reverts with the return data if the call fails
4. Decodes the returned fees value for subsequent use

This pattern ensures robustness when interacting with external contracts.

---

`331-367`: **Well-implemented signature validation with filtering.**

The signature validation logic correctly:
1. Enforces ordered and unique node IDs
2. Tracks valid signatures
3. Requires a majority quorum (>50%) of canonical nodes
4. Efficiently constructs an array of valid signing node IDs

The writeIndex_ variable prevents array out-of-bounds issues when constructing validSigningNodeIds_.

---

`314-317`: <details>
<summary>❌ Incorrect review comment</summary>

**Consider removing unused attesterNodeId parameter.**

According to a previous comment, there's uncertainty about who the "attester" is and whether this field serves a purpose. Since all signatures are from people attesting to the report, you might want to remove this field if its existence can't be justified.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check how attesterNodeId is used in the codebase
echo "Checking usage of attesterNodeId"
rg -A 2 "attesterNodeId" --type solidity
```

Length of output: 148

---

🏁 Script executed:

```shell
#!/bin/bash
# Display the function and its surrounding context
echo "=== Context from lines 280–340 in PayerReportManager.sol ==="
sed -n '280,340p' src/settlement-chain/PayerReportManager.sol

# Search for any references to attesterNodeId across all files
echo "=== Searching for attesterNodeId usage in repository ==="
rg 'attesterNodeId' .
```

Length of output: 7211

---



</details>

**Ignore removal of `attesterNodeId`**  
The `attesterNodeId` parameter is used throughout the contract and its interface—to compute the EIP-712 digest (`PAYER_REPORT_TYPEHASH`), in `_getPayerReportDigest`/`_verifySignatures`, and in legacy tests and harnesses. Removing it would break the typehash, interface, and existing unit tests. It should remain in place unless you also update the digest calculation, interface definitions, and all dependent tests.  


> Likely an incorrect or invalid review comment.

</details>
<details>
<summary>test/unit/PayerReportManager.t.sol (10)</summary>

`1-67`: **Test setup looks comprehensive and well-structured.**

The test setup properly initializes mock dependencies, creates test node operators with their private keys, and prepares the PayerReportManager contract with proper proxying. The structure follows best practices for Forge testing.

---

`71-87`: **Good validation of constructor error conditions.**

The tests properly verify that the constructor reverts with appropriate error selectors when zero addresses are provided for the critical dependencies.

---

`90-98`: **Thorough validation of initial contract state.**

The test confirms all expected initial state values including implementation address, registry addresses, and the migrator parameter key.

---

`266-347`: **Standard test submission with valid data.**

The test properly verifies the submission of a payer report with a non-zero Merkle root, validates the returned index, and confirms the stored data matches expectations.

---

`351-403`: **Good edge case testing for settle function.**

The tests verify appropriate error handling for out-of-bounds indices, already settled reports, and invalid proofs. The test for invalid proofs correctly uses an incorrect leaf count to trigger failure.

---

`442-566`: **Comprehensive testing of settlement scenarios.**

The test suite thoroughly covers partial settlement (first half and second half) and full one-shot settlement, verifying fee accumulation, offset updates, and settlement status changes.

---

`648-781`: **Good coverage of signature verification scenarios.**

The tests verify proper handling of unordered node IDs, insufficient signatures, and various invalid signature patterns (signatures at the end or in the middle of the array).

---

`784-818`: **Individual signature verification tests are thorough.**

Tests cover non-canonical nodes, invalid signatures, and verify signatures from the correct node owner.

---

`821-845`: **Good verification of digest generation.**

The test validates the computation of payer report digests with different parameters.

---

`965-1016`: **Helper functions are well-designed and reusable.**

The test includes helper functions for mocking parameter registry calls, retrieving implementation addresses, and generating signatures, which improves test code readability and maintainability.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-07T15:27:04Z
user: login: neekolas
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T18:55:27Z
user: login: deluca-mike
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T16:49:01Z
user: login: deluca-mike
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T16:49:38Z
user: login: deluca-mike
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T16:51:25Z
user: login: deluca-mike
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T16:59:43Z
user: login: coderabbitai[bot]
body: > [!CAUTION]
> Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments.  If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

**Actionable comments posted: 4**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>src/settlement-chain/NodeRegistry.sol (1)</summary><blockquote>

`181-189`: _⚠️ Potential issue_

**Prevent accidental contract lock-out when rotating admin**  
`updateAdmin()` accepts `address(0)` and would brick every `onlyAdmin` call. Add a non-zero check.

```diff
         address newAdmin_ = _toAddress(_getRegistryParameter(adminParameterKey()));
-        require(newAdmin_ != $.admin, NoChange());
+        require(newAdmin_ != address(0), InvalidAdmin());
+        require(newAdmin_ != $.admin, NoChange());
```

</blockquote></details>

</blockquote></details>

<details>
<summary>♻️ Duplicate comments (3)</summary><blockquote>

<details>
<summary>src/settlement-chain/PayerReportManager.sol (2)</summary><blockquote>

`16-18`: _⚠️ Potential issue_

**Signature replay vulnerability across multiple nodes**

Because `nodeId` is *not* part of the signed digest, an operator who controls two nodes with the **same signing address** can reuse **one** signature for **both** nodes, satisfying the quorum with fewer private keys.

Include the signing `nodeId` in `PAYER_REPORT_TYPEHASH` and `_getPayerReportDigest`, or alternatively verify that each recovered signer is unique across the signature set.

```diff
- /// @dev keccak256("PayerReport(uint256 originatorNodeId,uint256 startSequenceId,uint256 endSequenceId,bytes32 payersMerkleRoot,uint32[] nodeIds)")
+ /// @dev keccak256("PayerReport(uint256 originatorNodeId,uint256 startSequenceId,uint256 endSequenceId,bytes32 payersMerkleRoot,uint32[] nodeIds,uint32 signingNodeId)")
```

and extend `abi.encode(...)` accordingly.

Failing to address this materially weakens the BFT assumption.

---

`181-187`: _🛠️ Refactor suggestion_

**Low-level call hides interface mismatches and wastes gas**

Using `.call` defeats compile-time checks and requires an extra `abi.encode`/`decode`.

```diff
- (bool success_, bytes memory returnData_) = payerRegistry.call(
-    abi.encodeWithSelector(IPayerRegistryLike.settleUsage.selector, payerFees_)
- );
+ uint96 feesSettled_ = IPayerRegistryLike(payerRegistry).settleUsage(payerFees_);
```

This is safer (reverts automatically on failure), clearer, and ~2.8k gas cheaper.

</blockquote></details>
<details>
<summary>test/unit/PayerReportManager.t.sol (1)</summary><blockquote>

`751-755`: **Second occurrence of the same unused helper**

Same issue as above – delete or use in an assertion.

```diff
-        uint32[] memory expectedValidSigningNodeIds_ = new uint32[](2);
-
-        expectedValidSigningNodeIds_[0] = 1;
-        expectedValidSigningNodeIds_[1] = 3;
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (18)</summary><blockquote>

<details>
<summary>package.json (1)</summary><blockquote>

`23-23`: **Use safer flags for file removal in the slither script**  
Since `output.sarif` is always a file, the `-r` (recursive) flag is unnecessary and could inadvertently remove a directory if one exists with the same name. Switching to `rm -f` makes the intent clear and avoids potential risk.  


```diff
- "slither": "rm -rf output.sarif && forge build --build-info --skip '*/test/**' --skip '*/script/**' --force && slither --sarif output.sarif --compile-force-framework foundry --ignore-compile --config-file slither.config.json --fail-high .",
+ "slither": "rm -f output.sarif && forge build --build-info --skip '*/test/**' --skip '*/script/**' --force && slither --sarif output.sarif --compile-force-framework foundry --ignore-compile --config-file slither.config.json --fail-high .",
```

</blockquote></details>
<details>
<summary>test/unit/NodeRegistry.t.sol (1)</summary><blockquote>

`126-144`: **Hard-coded signer address couples the test to a specific hashing algorithm**

The expected `signer` (`0xaae97Cb3…`) is derived from the sample public key inside `addNode`.  
If the production contract ever changes the derivation method (e.g., prefixes, compression, different hash), every downstream test will break even though the behaviour is still correct.

Prefer deriving the expected address inside the test:

```solidity
bytes memory pk = hex"1f1f…1f";
address expectedSigner = address(uint160(uint256(keccak256(pk))));
assertEq(signer_, expectedSigner);
```

This keeps the test self-contained and tolerant to legitimate implementation tweaks.

</blockquote></details>
<details>
<summary>test/utils/Harnesses.sol (3)</summary><blockquote>

`113-118`: **Silent truncation when forcing `maxCanonicalNodes` into `uint8`**

`__setMaxCanonicalNodes(uint256)` down-casts to `uint8` without bounds checking:

```solidity
_getNodeRegistryStorage().maxCanonicalNodes = uint8(maxCanonicalNodes_);
```

Passing a value > 255 will wrap, possibly hiding mistakes in callers/fuzz tests.

```diff
- _getNodeRegistryStorage().maxCanonicalNodes = uint8(maxCanonicalNodes_);
+ if (maxCanonicalNodes_ > type(uint8).max) revert("maxCanonicalNodes overflow");
+ _getNodeRegistryStorage().maxCanonicalNodes = uint8(maxCanonicalNodes_);
```

---

`138-146`: **Unnecessary calldata-to-memory copy in `__setNode`**

`Node` expects `bytes memory` / `string memory`; passing `bytes calldata` / `string calldata` forces a runtime copy.  
As this helper is test-only, the impact is negligible, but you can save gas and stack depth by changing the parameters to `memory`:

```diff
- bytes calldata signingPublicKey_,
- string calldata httpAddress_
+ bytes memory signingPublicKey_,
+ string memory httpAddress_
```

---

`411-467`: **Exposure of internal arrays allows uncontrolled growth**

`__pushPayerReport` lets tests append arbitrary reports without bounds checks.  When fuzzing, this can blow past `uint32` limits (`offset`, `nodeIds.length`) causing panics that are unrelated to production code.

If the harness is intended for fuzzing, add basic guards (e.g., `require(nodeIds_.length < 256, "harness: too many nodes");`) so the invariant “<= uint8 canonical nodes” still holds.

</blockquote></details>
<details>
<summary>src/settlement-chain/PayerReportManager.sol (3)</summary><blockquote>

`170-178`: **`offset` can silently wrap for huge batches**

`payerReport_.offset += uint32(payerFees_.length);`

If an attacker submits ≥ 2^32 fees in one call (possible only through a crafted calldata, but still), the cast truncates and the `isSettled` comparison becomes incorrect.

While realistically improbable, adding a sanity check prevents undefined behaviour:

```solidity
uint256 newOffset = uint256(payerReport_.offset) + payerFees_.length;
if (newOffset > type(uint32).max) revert OffsetOverflow();
payerReport_.offset = uint32(newOffset);
```

---

`266-273`: **`abi.encodePacked(nodeIds_)` may collide**

`encodePacked` concatenates elements without length delimiters.  
With `uint32[]` the risk is low but not zero (e.g., `[1, 256]` vs `[0,1,0]` after trimming leading zeros).

Safer alternative:

```diff
- keccak256(abi.encodePacked(nodeIds_))
+ keccak256(abi.encode(nodeIds_))
```

`abi.encode` preserves the array structure and eliminates collision vectors at negligible extra cost after hashing.

---

`358-368`: **Double external call per signature is gas-heavy**

`_verifySignature` calls `getIsCanonicalNode` and `getSigner` separately for every signature.

Consider extending `INodeRegistryLike` with a single view that returns both flags (or caching results in memory) to halve external calls and save ~2400 gas per signature.

```solidity
function getNodeMeta(uint32 nodeId_) external view returns (bool isCanonical_, address signer_);
```

Then:

```solidity
(bool isCanonical_, address signer_) = INodeRegistryLike(nodeRegistry).getNodeMeta(nodeId_);
if (!isCanonical_ || signer_ == address(0)) return false;
return signer_ == ECDSA.tryRecoverCalldata(digest_, signature_);
```

</blockquote></details>
<details>
<summary>test/unit/PayerReportManager.t.sol (3)</summary><blockquote>

`701-705`: **Unused local `expectedValidSigningNodeIds_` – drop to avoid `unused-variable` warnings**

The helper array is populated but never referenced, which triggers a compiler warning (and will become an error when `solc --via-ir` is enabled).  
Remove the variable or use it in an assertion.

```diff
-        uint32[] memory expectedValidSigningNodeIds_ = new uint32[](2);
-
-        expectedValidSigningNodeIds_[0] = 1;
-        expectedValidSigningNodeIds_[1] = 2;
```

---

`621-623`: **TODO helper stub blocks full isolation of parameter-registry mocking**

The comment hints at a missing helper (`_expectAndMockParameterRegistryCall`).  
Providing such a helper will remove repeated boiler-plate and make the test intention clearer.

If you prefer a quick fix, at minimum delete the `TODO:` line to avoid confusing future maintainers.

---

`819-841`: **Hard-coding EIP-712 digests is brittle**

`getPayerReportDigest` depends on:
• `block.chainid`  
• `address(_manager)` (proxy address)  

Any change in deployment order or test chain-id will break the constants `0x65e…` and `0xa76…`.

Suggested pattern:

```solidity
bytes32 digest1 = _manager.getPayerReportDigest(…);
assertEq(digest1, _manager.getPayerReportDigest(…)); // recompute, not constant
```

If the goal is to regression-test the hashing algorithm, derive the expected digest locally with the same code instead of embedding magic constants.

</blockquote></details>
<details>
<summary>src/settlement-chain/interfaces/INodeRegistry.sol (2)</summary><blockquote>

`140-145`: **Overlapping custom errors – clarify semantic difference**

`MaxNodesReached` (new) and `MaxCanonicalNodesReached` (existing) are very similar and easily confused by integrators.

Consider renaming to underline the scope, e.g.  
`MaxTotalNodesReached` vs `MaxCanonicalNodesReached`, or merge if they are never thrown together.

---

`261-266`: **Missing NatSpec for `getSigner`**

`getSigner` is core to signature validation, yet lacks documentation while neighbouring getters are documented.  
Add a short `@notice` / `@return` description so DApp integrators understand its purpose without reading the implementation.

</blockquote></details>
<details>
<summary>src/settlement-chain/interfaces/IPayerReportManager.sol (2)</summary><blockquote>

`100-104`: **`UnorderedNodeIds` wording – include “duplicates” in description**

The current text only mentions ordering; duplicates are implicitly covered but not obvious.

```solidity
/// @notice Error thrown when the signing node IDs are not strictly sorted or contain duplicates.
error UnorderedNodeIds();
```
Minor clarity boost for integrators reading the ABI.

---

`146-154`: **`payerFees_` and `proofElements_` lack NatSpec format details**

The caller must supply exactly the “next contiguous chunk” of the Merkle tree, but this is undocumented.  
Add:

```solidity
/// @param payerFees_  ABI-encoded (address payer, uint96 fee) tuples in the exact order they appear in the tree.
/// @param proofElements_ Sequential Merkle proof elements for `payerFees_`, first word = leafCount.
```

Clearer docs prevent misuse and reduce support requests.

</blockquote></details>
<details>
<summary>src/settlement-chain/NodeRegistry.sol (3)</summary><blockquote>

`172-176`: **Allow admin to rescue a misbehaving node’s HTTP address**  
Right now only the node owner can update `httpAddress`. If the owner is unresponsive or the key is lost, network health suffers.  Consider adding an `onlyAdmin` back-door with an event to keep the network reachable.

---

`229-233`: **`getAllNodes()` can run out of gas – expose pagination**  
For ~40 M nodes this loop will exceed block gas limits and revert.  Provide a paginated getter or rely on off-chain indexing.

```diff
-function getAllNodes() external view returns (NodeWithId[] memory allNodes_) {
+function getAllNodes(uint32 start, uint32 size)
+    external
+    view
+    returns (NodeWithId[] memory allNodes_)
```

---

`253-256`: **Expose signer look-up for *all* callers, not only off-chain**  
The `getSigner` helper is handy.  Consider marking it `pure`/`view` but also add `indexed` to the `NodeAdded` event for cheaper off-chain retrieval.

</blockquote></details>

</blockquote></details>

<details>
<summary>🛑 Comments failed to post (4)</summary><blockquote>

<details>
<summary>test/unit/PayerReportManager.t.sol (1)</summary><blockquote>

438-444: _🛠️ Refactor suggestion_

**TODO left in test – failing-path not fully asserted**

`// TODO: expectCallAndRevert` indicates the negative path is only partially covered.  
Use `vm.expectCallRevert()` (available in `forge-std` ≥ v1.9) or the pattern below so the test fails if the mocked revert is not bubbled up:

```solidity
vm.expectCall(
    _payerRegistry,
    abi.encodeWithSignature("settleUsage((address,uint96)[])", payerFees_)
);
vm.expectRevert(
    abi.encodeWithSelector(IPayerReportManager.SettleUsageFailed.selector, "Test Failure")
);
```
Ensuring the call expectation fires prevents false-positives when the implementation changes.

</blockquote></details>
<details>
<summary>src/settlement-chain/NodeRegistry.sol (3)</summary><blockquote>

12-13: _🛠️ Refactor suggestion_

**Replace TODO with parameter-registry look-ups before shipping**  
`maxCanonicalNodes` is still hard-coded and the comment at L12 reminds us to migrate other constants.  If this makes it into production every future change will require an upgrade rather than a registry edit.  Please fetch all mutable parameters (e.g. `maxCanonicalNodes`, maybe `NODE_INCREMENT`) from `IParameterRegistryLike` in `initialize`.

---

91-96: _⚠️ Potential issue_

**Contract is unusable until `updateAdmin()` is called – consider initialising `admin`**  
`onlyAdmin` guards all write functions, yet `initialize()` never sets `_getNodeRegistryStorage().admin`.  
Until someone externally calls `updateAdmin()` (which itself relies on a correctly populated parameter registry key) the contract is bricked.

```diff
--- a/src/settlement-chain/NodeRegistry.sol
+++ b/src/settlement-chain/NodeRegistry.sol
@@ function initialize() public initializer {
   __ERC721_init("XMTP Nodes", "nXMTP");
-  // TODO: This should probably come from the parameter registry.
-  _getNodeRegistryStorage().maxCanonicalNodes = 20;
+  NodeRegistryStorage storage $ = _getNodeRegistryStorage();
+  $.maxCanonicalNodes = 20;                 // temporary default
+  $.admin           = msg.sender;          // avoid lock-out; can still be updated later
 }
```
Failing to set a non-zero admin can permanently lock upgrades/additions if the registry entry is ever mis-configured.  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        __ERC721_init("XMTP Nodes", "nXMTP");
-       // TODO: This should probably come from the parameter registry.
-       _getNodeRegistryStorage().maxCanonicalNodes = 20;
+       NodeRegistryStorage storage $ = _getNodeRegistryStorage();
+       $.maxCanonicalNodes = 20;                 // temporary default
+       $.admin           = msg.sender;          // avoid lock-out; can still be updated later
    }
`````

</details>

<!-- suggestion_end -->

---

101-125: _🛠️ Refactor suggestion_

**Strengthen input validation & signer derivation in `addNode`**  

1. `signingPublicKey_` length is only checked for non-zero.  Enforce the expected 64/65 bytes to avoid malformed keys and hash collisions.  
2. Deriving the signer with `address(uint160(keccak256(pubKey)))` is non-standard; the canonical derivation is `address(uint160(uint256(keccak256(pubKey[1:]))))` where `pubKey` is uncompressed and without the `0x04` prefix.  A mismatch will cause `ecrecover` validations in `PayerReportManager` to fail.

```diff
+        // Expect uncompressed SEC-1 public key (0x04 + 64 bytes)
+        require(
+            signingPublicKey_.length == 65 && signingPublicKey_[0] == 0x04,
+            InvalidSigningPublicKey()
+        );

-        signer_ = _toAddress(keccak256(signingPublicKey_));
+        unchecked {
+            signer_ = _toAddress(keccak256(signingPublicKey_[1:])); // skip prefix for EIP-191 compat
+        }
```

Finally, consider reverting if `signer_` is already registered to prevent duplicate logical identities.  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        address owner_,
        bytes calldata signingPublicKey_,
        string calldata httpAddress_
    ) external onlyAdmin returns (uint32 nodeId_, address signer_) {
        require(owner_ != address(0), InvalidOwner());
        require(signingPublicKey_.length > 0, InvalidSigningPublicKey());
        // Expect uncompressed SEC-1 public key (0x04 + 64 bytes)
        require(
            signingPublicKey_.length == 65 && signingPublicKey_[0] == 0x04,
            InvalidSigningPublicKey()
        );
        require(bytes(httpAddress_).length > 0, InvalidHttpAddress());

        NodeRegistryStorage storage $ = _getNodeRegistryStorage();

        require((uint256($.nodeCount) + 1) * NODE_INCREMENT <= type(uint32).max, MaxNodesReached());

        unchecked {
            nodeId_ = ++$.nodeCount * NODE_INCREMENT; // The first node starts with `nodeId_ = NODE_INCREMENT`.
        }

        unchecked {
            // Drop the 0x04 prefix so ecrecover (and other EC-based checks) use the canonical 64-byte key
            signer_ = _toAddress(keccak256(signingPublicKey_[1:])); 
        }

        // Nodes start off as non-canonical.
        $.nodes[nodeId_] = Node(signer_, false, signingPublicKey_, httpAddress_);

        _mint(owner_, nodeId_);

        emit NodeAdded(nodeId_, owner_, signer_, signingPublicKey_, httpAddress_);
    }
`````

</details>

<!-- suggestion_end -->

</blockquote></details>

</blockquote></details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-09T17:25:34Z
user: login: fbac
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-13T10:53:40Z

Review comments:
diff_hunk: @@ -0,0 +1,99 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { IERC712 } from "./interfaces/IERC712.sol";
+
+/**
+ * @title An abstract implementation of EIP-712 for typed structured data hashing and signing.
+ */
+abstract contract ERC712 is IERC712 {
+    /* ============ Variables ============ */
+
+    /// @dev keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
+    bytes32 internal constant _EIP712_DOMAIN_HASH = 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f;
+
+    /// @dev Initial Chain ID set at deployment.
+    uint256 internal immutable _initialChainId;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.ERC712
+     * @notice The UUPS storage for the ERC712 contract.
+     * @param  initialDomainSeparator Initial EIP-712 domain separator set at intialization.
+     */
+    struct ERC712Storage {
+        bytes32 initialDomainSeparator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.ERC712")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _ERC712_STORAGE_LOCATION =
+        0xc7effa11ad597798220888e5d1ba4eeddcc8c2635d01dae8b9f958ac905c1100;
+
+    function _getERC712Storage() internal pure returns (ERC712Storage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _ERC712_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructs the EIP-712 domain separator.
+     */
+    constructor() {
+        _initialChainId = block.chainid;
+    }
+
+    /* ============ Initialization ============ */
+
+    function _initializeERC712() internal {
+        _getERC712Storage().initialDomainSeparator = _getDomainSeparator();
+    }
+
+    /* ============ View/Pure Functions ============ */
+
+    /// @inheritdoc IERC712
+    function DOMAIN_SEPARATOR() public view returns (bytes32 domainSeparator_) {
+        return block.chainid == _initialChainId ? _getERC712Storage().initialDomainSeparator : _getDomainSeparator();
+    }
path: src/abstract/ERC721.sol
user: login: github-advanced-security[bot]
body: ## Conformance to Solidity naming conventions

Function ERC712.DOMAIN_SEPARATOR() (src/abstract/ERC721.sol#58-60) is not in mixedCase


[Show more details](https://github.com/xmtp/smart-contracts/security/code-scanning/10)
created_at: 2025-05-06T08:34:38Z
author_association: NONE
diff_hunk: @@ -0,0 +1,13 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+/**
+ * @title Typed structured data hashing and signing via EIP-712.
+ * @dev   The interface as defined by EIP-712: https://eips.ethereum.org/EIPS/eip-712
+ */
+interface IERC712 {
+    /* ============ View/Pure Functions ============ */
+
+    /// @notice Returns the EIP712 domain separator used in the encoding of a signed digest.
+    function DOMAIN_SEPARATOR() external view returns (bytes32 domainSeparator_);
path: src/abstract/interfaces/IERC712.sol
user: login: github-advanced-security[bot]
body: ## Conformance to Solidity naming conventions

Function IERC712.DOMAIN_SEPARATOR() (src/abstract/interfaces/IERC712.sol#12) is not in mixedCase


[Show more details](https://github.com/xmtp/smart-contracts/security/code-scanning/11)
created_at: 2025-05-06T08:34:38Z
author_association: NONE
diff_hunk: @@ -0,0 +1,375 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { ECDSA } from "../../lib/solady/src/utils/ECDSA.sol";
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../libraries/SequentialMerkleProofs.sol";
+
+import { IMigratable } from "../abstract/interfaces/IMigratable.sol";
+import { IParameterRegistryLike, INodeRegistryLike, IPayerRegistryLike } from "./interfaces/External.sol";
+import { IPayerReportManager } from "./interfaces/IPayerReportManager.sol";
+
+import { ERC5267 } from "../abstract/ERC5267.sol";
+import { Migratable } from "../abstract/Migratable.sol";
+
+// TODO: If a node operator owns more than one node, their signature for a payer report will be identical, and therefore
+//       replayable across their nodes. This may not be ideal, so it might be necessary to include the node id of the
+//       signer in the digest.
+
+/**
+ * @title  Implementation of the Payer Report Manager.
+ * @notice This contract handles functionality for submitting and settling payer reports.
+ */
+contract PayerReportManager is IPayerReportManager, Initializable, Migratable, ERC5267 {
+    /* ============ Constants/Immutables ============ */
+
+    // solhint-disable-next-line max-line-length
+    /// @dev keccak256("PayerReport(uint256 attesterNodeId,uint256 originatorNodeId,uint256 startSequenceId,uint256 endSequenceId,bytes32 payersMerkleRoot,address[] nodeOperators)")
+    bytes32 public constant PAYER_REPORT_TYPEHASH = 0x73ce31d3128c9ba6af04b3270a898f0abafdf9bed84e47ec7ebd2f3dd8e9c8a5;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable parameterRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable nodeRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable payerRegistry;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.PayerReportManager
+     * @notice The UUPS storage for the payer report manager.
+     * @param  payerReportsByOriginator The mapping of arrays of payer reports by originator node ID.
+     */
+    struct PayerReportManagerStorage {
+        mapping(uint32 originatorId => PayerReport[] payerReports) payerReportsByOriginator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.PayerReportManager")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _PAYER_REPORT_MANAGER_STORAGE_LOCATION =
+        0x26b057ee8e4d60685198828fdf1c618ab8e36b0ab85f54a47b18319f6f718e00;
+
+    function _getPayerReportManagerStorage() internal pure returns (PayerReportManagerStorage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _PAYER_REPORT_MANAGER_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructor for the implementation contract, such that the implementation cannot be initialized.
+     * @param  parameterRegistry_ The address of the parameter registry.
+     * @param  nodeRegistry_      The address of the node registry.
+     * @param  payerRegistry_     The address of the payer registry.
+     * @dev    The parameter registry, node registry, and payer registry must not be the zero address.
+     * @dev    The parameter registry, node registry, and payer registry are immutable so that they are inlined in the
+     *         contract code, and have minimal gas cost.
+     */
+    constructor(address parameterRegistry_, address nodeRegistry_, address payerRegistry_) ERC5267() {
+        require(_isNotZero(parameterRegistry = parameterRegistry_), ZeroParameterRegistry());
+        require(_isNotZero(nodeRegistry = nodeRegistry_), ZeroNodeRegistry());
+        require(_isNotZero(payerRegistry = payerRegistry_), ZeroPayerRegistry());
+
+        _disableInitializers();
+    }
+
+    /* ============ Initialization ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function initialize() public initializer {
+        _initializeERC5267();
+    }
+
+    /* ============ Interactive Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function submit(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) external returns (uint256 payerReportIndex_) {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        payerReportIndex_ = payerReports_.length;
+
+        uint32 lastSequenceId_ = payerReportIndex_ > 0 ? payerReports_[payerReportIndex_ - 1].endSequenceId : 0;
+
+        // Enforces that the start sequence ID is the next expected sequence ID.
+        if (startSequenceId_ != lastSequenceId_ + 1) {
+            revert InvalidStartSequenceId(startSequenceId_, lastSequenceId_ + 1);
+        }
+
+        // Enforces that the end sequence ID is greater than or equal to the start sequence ID.
+        if (endSequenceId_ < startSequenceId_) revert InvalidSequenceIds();
+
+        // Verifies the signatures and gets the array of valid signing node IDs.
+        uint32[] memory validSigningNodeIds_ = _verifySignatures({
+            originatorNodeId_: originatorNodeId_,
+            attesterNodeId_: attesterNodeId_,
+            startSequenceId_: startSequenceId_,
+            endSequenceId_: endSequenceId_,
+            payersMerkleRoot_: payersMerkleRoot_,
+            nodeOperators_: nodeOperators_,
+            signatures_: signatures_
+        });
+
+        payerReports_.push(
+            PayerReport({
+                attesterNodeId: attesterNodeId_,
+                startSequenceId: startSequenceId_,
+                endSequenceId: endSequenceId_,
+                feesSettled: 0,
+                offset: 0,
+                isSettled: false,
+                payersMerkleRoot: payersMerkleRoot_,
+                nodeOperators: nodeOperators_
+            })
+        );
+
+        emit PayerReportSubmitted({
+            originatorNodeId: originatorNodeId_,
+            payerReportIndex: payerReportIndex_,
+            attesterNodeId: attesterNodeId_,
+            startSequenceId: startSequenceId_,
+            endSequenceId: endSequenceId_,
+            payersMerkleRoot: payersMerkleRoot_,
+            nodeOperators: nodeOperators_,
+            signingNodeIds: validSigningNodeIds_
+        });
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function settle(
+        uint32 originatorNodeId_,
+        uint32 payerReportIndex_,
+        bytes[] calldata payerFees_,
+        bytes32[] calldata proofElements_
+    ) external {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        if (payerReportIndex_ >= payerReports_.length) revert PayerReportIndexOutOfBounds();
+
+        PayerReport storage payerReport_ = payerReports_[payerReportIndex_];
+
+        if (payerReport_.isSettled) revert PayerReportEntirelySettled();
+
+        // Verify the payer fees provided are the next sequential payer fees from the last settled offset.
+        SequentialMerkleProofs.verify(payerReport_.payersMerkleRoot, payerReport_.offset, payerFees_, proofElements_);
+
+        payerReport_.offset += uint32(payerFees_.length);
+
+        uint32 leafCount_ = SequentialMerkleProofs.getLeafCount(proofElements_);
+
+        payerReport_.isSettled = leafCount_ == payerReport_.offset;
+
+        // Low level call which handles passing the `payerFees_` arrays as a bytes array that will be automatically
+        // decoded as the required structs by the payer registry's `settleUsage` function.
+        (bool success_, bytes memory returnData_) = payerRegistry.call(
+            abi.encodeWithSelector(IPayerRegistryLike.settleUsage.selector, payerFees_)
+        );
+
+        if (!success_) revert SettleUsageFailed(returnData_);
+
+        uint96 feesSettled_ = abi.decode(returnData_, (uint96));
+
+        emit PayerReportSubsetSettled(
+            originatorNodeId_,
+            payerReportIndex_,
+            uint32(payerFees_.length),
+            leafCount_ - payerReport_.offset,
+            feesSettled_
+        );
+
+        payerReport_.feesSettled += feesSettled_;
+    }
path: src/settlement-chain/PayerReportManager.sol
user: login: github-advanced-security[bot]
body: ## Reentrancy vulnerabilities

Reentrancy in PayerReportManager.settle(uint32,uint32,bytes[],bytes32[]) (src/settlement-chain/PayerReportManager.sol#153-197):
	External calls:
	- (success_,returnData_) = payerRegistry.call(abi.encodeWithSelector(IPayerRegistryLike.settleUsage.selector,payerFees_)) (src/settlement-chain/PayerReportManager.sol#180-182)
	Event emitted after the call(s):
	- PayerReportSubsetSettled(originatorNodeId_,payerReportIndex_,uint32(payerFees_.length),leafCount_ - payerReport_.offset,feesSettled_) (src/settlement-chain/PayerReportManager.sol#188-194)


[Show more details](https://github.com/xmtp/smart-contracts/security/code-scanning/8)
created_at: 2025-05-06T08:34:38Z
author_association: NONE
diff_hunk: @@ -0,0 +1,375 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { ECDSA } from "../../lib/solady/src/utils/ECDSA.sol";
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../libraries/SequentialMerkleProofs.sol";
+
+import { IMigratable } from "../abstract/interfaces/IMigratable.sol";
+import { IParameterRegistryLike, INodeRegistryLike, IPayerRegistryLike } from "./interfaces/External.sol";
+import { IPayerReportManager } from "./interfaces/IPayerReportManager.sol";
+
+import { ERC5267 } from "../abstract/ERC5267.sol";
+import { Migratable } from "../abstract/Migratable.sol";
+
+// TODO: If a node operator owns more than one node, their signature for a payer report will be identical, and therefore
+//       replayable across their nodes. This may not be ideal, so it might be necessary to include the node id of the
+//       signer in the digest.
+
+/**
+ * @title  Implementation of the Payer Report Manager.
+ * @notice This contract handles functionality for submitting and settling payer reports.
+ */
+contract PayerReportManager is IPayerReportManager, Initializable, Migratable, ERC5267 {
+    /* ============ Constants/Immutables ============ */
+
+    // solhint-disable-next-line max-line-length
+    /// @dev keccak256("PayerReport(uint256 attesterNodeId,uint256 originatorNodeId,uint256 startSequenceId,uint256 endSequenceId,bytes32 payersMerkleRoot,address[] nodeOperators)")
+    bytes32 public constant PAYER_REPORT_TYPEHASH = 0x73ce31d3128c9ba6af04b3270a898f0abafdf9bed84e47ec7ebd2f3dd8e9c8a5;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable parameterRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable nodeRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable payerRegistry;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.PayerReportManager
+     * @notice The UUPS storage for the payer report manager.
+     * @param  payerReportsByOriginator The mapping of arrays of payer reports by originator node ID.
+     */
+    struct PayerReportManagerStorage {
+        mapping(uint32 originatorId => PayerReport[] payerReports) payerReportsByOriginator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.PayerReportManager")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _PAYER_REPORT_MANAGER_STORAGE_LOCATION =
+        0x26b057ee8e4d60685198828fdf1c618ab8e36b0ab85f54a47b18319f6f718e00;
+
+    function _getPayerReportManagerStorage() internal pure returns (PayerReportManagerStorage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _PAYER_REPORT_MANAGER_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructor for the implementation contract, such that the implementation cannot be initialized.
+     * @param  parameterRegistry_ The address of the parameter registry.
+     * @param  nodeRegistry_      The address of the node registry.
+     * @param  payerRegistry_     The address of the payer registry.
+     * @dev    The parameter registry, node registry, and payer registry must not be the zero address.
+     * @dev    The parameter registry, node registry, and payer registry are immutable so that they are inlined in the
+     *         contract code, and have minimal gas cost.
+     */
+    constructor(address parameterRegistry_, address nodeRegistry_, address payerRegistry_) ERC5267() {
+        require(_isNotZero(parameterRegistry = parameterRegistry_), ZeroParameterRegistry());
+        require(_isNotZero(nodeRegistry = nodeRegistry_), ZeroNodeRegistry());
+        require(_isNotZero(payerRegistry = payerRegistry_), ZeroPayerRegistry());
+
+        _disableInitializers();
+    }
+
+    /* ============ Initialization ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function initialize() public initializer {
+        _initializeERC5267();
+    }
+
+    /* ============ Interactive Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function submit(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) external returns (uint256 payerReportIndex_) {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        payerReportIndex_ = payerReports_.length;
+
+        uint32 lastSequenceId_ = payerReportIndex_ > 0 ? payerReports_[payerReportIndex_ - 1].endSequenceId : 0;
+
+        // Enforces that the start sequence ID is the next expected sequence ID.
+        if (startSequenceId_ != lastSequenceId_ + 1) {
+            revert InvalidStartSequenceId(startSequenceId_, lastSequenceId_ + 1);
+        }
+
+        // Enforces that the end sequence ID is greater than or equal to the start sequence ID.
+        if (endSequenceId_ < startSequenceId_) revert InvalidSequenceIds();
+
+        // Verifies the signatures and gets the array of valid signing node IDs.
+        uint32[] memory validSigningNodeIds_ = _verifySignatures({
+            originatorNodeId_: originatorNodeId_,
+            attesterNodeId_: attesterNodeId_,
+            startSequenceId_: startSequenceId_,
+            endSequenceId_: endSequenceId_,
+            payersMerkleRoot_: payersMerkleRoot_,
+            nodeOperators_: nodeOperators_,
+            signatures_: signatures_
+        });
+
+        payerReports_.push(
+            PayerReport({
+                attesterNodeId: attesterNodeId_,
+                startSequenceId: startSequenceId_,
+                endSequenceId: endSequenceId_,
+                feesSettled: 0,
+                offset: 0,
+                isSettled: false,
+                payersMerkleRoot: payersMerkleRoot_,
+                nodeOperators: nodeOperators_
+            })
+        );
+
+        emit PayerReportSubmitted({
+            originatorNodeId: originatorNodeId_,
+            payerReportIndex: payerReportIndex_,
+            attesterNodeId: attesterNodeId_,
+            startSequenceId: startSequenceId_,
+            endSequenceId: endSequenceId_,
+            payersMerkleRoot: payersMerkleRoot_,
+            nodeOperators: nodeOperators_,
+            signingNodeIds: validSigningNodeIds_
+        });
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function settle(
+        uint32 originatorNodeId_,
+        uint32 payerReportIndex_,
+        bytes[] calldata payerFees_,
+        bytes32[] calldata proofElements_
+    ) external {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        if (payerReportIndex_ >= payerReports_.length) revert PayerReportIndexOutOfBounds();
+
+        PayerReport storage payerReport_ = payerReports_[payerReportIndex_];
+
+        if (payerReport_.isSettled) revert PayerReportEntirelySettled();
+
+        // Verify the payer fees provided are the next sequential payer fees from the last settled offset.
+        SequentialMerkleProofs.verify(payerReport_.payersMerkleRoot, payerReport_.offset, payerFees_, proofElements_);
+
+        payerReport_.offset += uint32(payerFees_.length);
+
+        uint32 leafCount_ = SequentialMerkleProofs.getLeafCount(proofElements_);
+
+        payerReport_.isSettled = leafCount_ == payerReport_.offset;
+
+        // Low level call which handles passing the `payerFees_` arrays as a bytes array that will be automatically
+        // decoded as the required structs by the payer registry's `settleUsage` function.
+        (bool success_, bytes memory returnData_) = payerRegistry.call(
+            abi.encodeWithSelector(IPayerRegistryLike.settleUsage.selector, payerFees_)
+        );
+
+        if (!success_) revert SettleUsageFailed(returnData_);
+
+        uint96 feesSettled_ = abi.decode(returnData_, (uint96));
+
+        emit PayerReportSubsetSettled(
+            originatorNodeId_,
+            payerReportIndex_,
+            uint32(payerFees_.length),
+            leafCount_ - payerReport_.offset,
+            feesSettled_
+        );
+
+        payerReport_.feesSettled += feesSettled_;
+    }
path: src/settlement-chain/PayerReportManager.sol
user: login: github-advanced-security[bot]
body: ## Low-level calls

Low level call in PayerReportManager.settle(uint32,uint32,bytes[],bytes32[]) (src/settlement-chain/PayerReportManager.sol#153-197):
	- (success_,returnData_) = payerRegistry.call(abi.encodeWithSelector(IPayerRegistryLike.settleUsage.selector,payerFees_)) (src/settlement-chain/PayerReportManager.sol#180-182)


[Show more details](https://github.com/xmtp/smart-contracts/security/code-scanning/9)
created_at: 2025-05-06T08:34:38Z
author_association: NONE
diff_hunk: @@ -0,0 +1,375 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { ECDSA } from "../../lib/solady/src/utils/ECDSA.sol";
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../libraries/SequentialMerkleProofs.sol";
+
+import { IMigratable } from "../abstract/interfaces/IMigratable.sol";
+import { IParameterRegistryLike, INodeRegistryLike, IPayerRegistryLike } from "./interfaces/External.sol";
+import { IPayerReportManager } from "./interfaces/IPayerReportManager.sol";
+
+import { ERC5267 } from "../abstract/ERC5267.sol";
+import { Migratable } from "../abstract/Migratable.sol";
+
+// TODO: If a node operator owns more than one node, their signature for a payer report will be identical, and therefore
+//       replayable across their nodes. This may not be ideal, so it might be necessary to include the node id of the
+//       signer in the digest.
+
+/**
+ * @title  Implementation of the Payer Report Manager.
+ * @notice This contract handles functionality for submitting and settling payer reports.
+ */
+contract PayerReportManager is IPayerReportManager, Initializable, Migratable, ERC5267 {
+    /* ============ Constants/Immutables ============ */
+
+    // solhint-disable-next-line max-line-length
+    /// @dev keccak256("PayerReport(uint256 attesterNodeId,uint256 originatorNodeId,uint256 startSequenceId,uint256 endSequenceId,bytes32 payersMerkleRoot,address[] nodeOperators)")
+    bytes32 public constant PAYER_REPORT_TYPEHASH = 0x73ce31d3128c9ba6af04b3270a898f0abafdf9bed84e47ec7ebd2f3dd8e9c8a5;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable parameterRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable nodeRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable payerRegistry;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.PayerReportManager
+     * @notice The UUPS storage for the payer report manager.
+     * @param  payerReportsByOriginator The mapping of arrays of payer reports by originator node ID.
+     */
+    struct PayerReportManagerStorage {
+        mapping(uint32 originatorId => PayerReport[] payerReports) payerReportsByOriginator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.PayerReportManager")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _PAYER_REPORT_MANAGER_STORAGE_LOCATION =
+        0x26b057ee8e4d60685198828fdf1c618ab8e36b0ab85f54a47b18319f6f718e00;
+
+    function _getPayerReportManagerStorage() internal pure returns (PayerReportManagerStorage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _PAYER_REPORT_MANAGER_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructor for the implementation contract, such that the implementation cannot be initialized.
+     * @param  parameterRegistry_ The address of the parameter registry.
+     * @param  nodeRegistry_      The address of the node registry.
+     * @param  payerRegistry_     The address of the payer registry.
+     * @dev    The parameter registry, node registry, and payer registry must not be the zero address.
+     * @dev    The parameter registry, node registry, and payer registry are immutable so that they are inlined in the
+     *         contract code, and have minimal gas cost.
+     */
+    constructor(address parameterRegistry_, address nodeRegistry_, address payerRegistry_) ERC5267() {
+        require(_isNotZero(parameterRegistry = parameterRegistry_), ZeroParameterRegistry());
+        require(_isNotZero(nodeRegistry = nodeRegistry_), ZeroNodeRegistry());
+        require(_isNotZero(payerRegistry = payerRegistry_), ZeroPayerRegistry());
+
+        _disableInitializers();
+    }
+
+    /* ============ Initialization ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function initialize() public initializer {
+        _initializeERC5267();
+    }
+
+    /* ============ Interactive Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function submit(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) external returns (uint256 payerReportIndex_) {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        payerReportIndex_ = payerReports_.length;
+
+        uint32 lastSequenceId_ = payerReportIndex_ > 0 ? payerReports_[payerReportIndex_ - 1].endSequenceId : 0;
+
+        // Enforces that the start sequence ID is the next expected sequence ID.
+        if (startSequenceId_ != lastSequenceId_ + 1) {
+            revert InvalidStartSequenceId(startSequenceId_, lastSequenceId_ + 1);
+        }
+
+        // Enforces that the end sequence ID is greater than or equal to the start sequence ID.
+        if (endSequenceId_ < startSequenceId_) revert InvalidSequenceIds();
+
+        // Verifies the signatures and gets the array of valid signing node IDs.
+        uint32[] memory validSigningNodeIds_ = _verifySignatures({
+            originatorNodeId_: originatorNodeId_,
+            attesterNodeId_: attesterNodeId_,
+            startSequenceId_: startSequenceId_,
+            endSequenceId_: endSequenceId_,
+            payersMerkleRoot_: payersMerkleRoot_,
+            nodeOperators_: nodeOperators_,
+            signatures_: signatures_
+        });
+
+        payerReports_.push(
+            PayerReport({
+                attesterNodeId: attesterNodeId_,
+                startSequenceId: startSequenceId_,
+                endSequenceId: endSequenceId_,
+                feesSettled: 0,
+                offset: 0,
+                isSettled: false,
+                payersMerkleRoot: payersMerkleRoot_,
+                nodeOperators: nodeOperators_
+            })
+        );
+
+        emit PayerReportSubmitted({
+            originatorNodeId: originatorNodeId_,
+            payerReportIndex: payerReportIndex_,
+            attesterNodeId: attesterNodeId_,
+            startSequenceId: startSequenceId_,
+            endSequenceId: endSequenceId_,
+            payersMerkleRoot: payersMerkleRoot_,
+            nodeOperators: nodeOperators_,
+            signingNodeIds: validSigningNodeIds_
+        });
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function settle(
+        uint32 originatorNodeId_,
+        uint32 payerReportIndex_,
+        bytes[] calldata payerFees_,
+        bytes32[] calldata proofElements_
+    ) external {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        if (payerReportIndex_ >= payerReports_.length) revert PayerReportIndexOutOfBounds();
+
+        PayerReport storage payerReport_ = payerReports_[payerReportIndex_];
+
+        if (payerReport_.isSettled) revert PayerReportEntirelySettled();
+
+        // Verify the payer fees provided are the next sequential payer fees from the last settled offset.
+        SequentialMerkleProofs.verify(payerReport_.payersMerkleRoot, payerReport_.offset, payerFees_, proofElements_);
+
+        payerReport_.offset += uint32(payerFees_.length);
+
+        uint32 leafCount_ = SequentialMerkleProofs.getLeafCount(proofElements_);
+
+        payerReport_.isSettled = leafCount_ == payerReport_.offset;
+
+        // Low level call which handles passing the `payerFees_` arrays as a bytes array that will be automatically
+        // decoded as the required structs by the payer registry's `settleUsage` function.
+        (bool success_, bytes memory returnData_) = payerRegistry.call(
+            abi.encodeWithSelector(IPayerRegistryLike.settleUsage.selector, payerFees_)
+        );
+
+        if (!success_) revert SettleUsageFailed(returnData_);
+
+        uint96 feesSettled_ = abi.decode(returnData_, (uint96));
+
+        emit PayerReportSubsetSettled(
+            originatorNodeId_,
+            payerReportIndex_,
+            uint32(payerFees_.length),
+            leafCount_ - payerReport_.offset,
+            feesSettled_
+        );
+
+        payerReport_.feesSettled += feesSettled_;
+    }
+
+    /// @inheritdoc IMigratable
+    function migrate() external {
+        _migrate(_toAddress(_getRegistryParameter(migratorParameterKey())));
+    }
+
+    /* ============ View/Pure Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function migratorParameterKey() public pure returns (bytes memory key_) {
+        return "xmtp.payerReportManager.migrator";
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReportDigest(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) external view returns (bytes32 digest_) {
+        return
+            _getPayerReportDigest(
+                originatorNodeId_,
+                attesterNodeId_,
+                startSequenceId_,
+                endSequenceId_,
+                payersMerkleRoot_,
+                nodeOperators_
+            );
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReports(uint32 originatorNodeId_) external view returns (PayerReport[] memory payerReports_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_];
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReport(
+        uint32 originatorNodeId_,
+        uint256 payerReportIndex_
+    ) external view returns (PayerReport memory payerReport_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_][payerReportIndex_];
+    }
+
+    /* ============ Internal View/Pure Functions ============ */
+
+    function _name() internal pure override returns (string memory name_) {
+        return "PayerReportManager";
+    }
+
+    function _version() internal pure override returns (string memory version_) {
+        return "1";
+    }
+
+    /**
+     * @dev    Returns the EIP-712 digest for a payer report.
+     * @param  originatorNodeId_ The id of the node originator.
+     * @param  attesterNodeId_   The id of the node attester.
+     * @param  startSequenceId_  The start sequence ID.
+     * @param  endSequenceId_    The end sequence ID.
+     * @param  payersMerkleRoot_ The payers merkle root.
+     * @param  nodeOperators_    The active node operators during the reporting period.
+     * @return digest_           The EIP-712 digest.
+     */
+    function _getPayerReportDigest(
+        uint256 originatorNodeId_,
+        uint256 attesterNodeId_,
+        uint256 startSequenceId_,
+        uint256 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) internal view returns (bytes32 digest_) {
+        return
+            _getDigest(
+                keccak256(
+                    abi.encode(
+                        PAYER_REPORT_TYPEHASH,
+                        originatorNodeId_,
+                        attesterNodeId_,
+                        startSequenceId_,
+                        endSequenceId_,
+                        payersMerkleRoot_,
+                        keccak256(abi.encodePacked(nodeOperators_))
+                    )
+                )
+            );
+    }
+
+    function _getRegistryParameter(bytes memory key_) internal view returns (bytes32 value_) {
+        return IParameterRegistryLike(parameterRegistry).get(key_);
+    }
+
+    function _isNotZero(address input_) internal pure returns (bool isNotZero_) {
+        return input_ != address(0);
+    }
+
+    function _toAddress(bytes32 value_) internal pure returns (address address_) {
+        // slither-disable-next-line assembly
+        assembly {
+            address_ := value_
+        }
+    }
+
+    /**
+     * @dev Verifies that enough of the signatures are valid and provided by canonical node operators and returns the
+     *      array of valid signing node IDs.
+     */
+    function _verifySignatures(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) internal view returns (uint32[] memory validSigningNodeIds_) {
+        bytes32 digest_ = _getPayerReportDigest(
+            originatorNodeId_,
+            attesterNodeId_,
+            startSequenceId_,
+            endSequenceId_,
+            payersMerkleRoot_,
+            nodeOperators_
+        );
+
+        // Need to determine how many of the signatures are valid and provided by canonical node operators.
+        uint8 validSignatureCount_;
path: src/settlement-chain/PayerReportManager.sol
user: login: github-advanced-security[bot]
body: ## Uninitialized local variables

PayerReportManager._verifySignatures(uint32,uint32,uint32,uint32,bytes32,address[],IPayerReportManager.PayerReportSignature[]).validSignatureCount_ (src/settlement-chain/PayerReportManager.sol#326) is a local variable never initialized


[Show more details](https://github.com/xmtp/smart-contracts/security/code-scanning/5)
created_at: 2025-05-06T08:34:38Z
author_association: NONE
diff_hunk: @@ -0,0 +1,375 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { ECDSA } from "../../lib/solady/src/utils/ECDSA.sol";
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../libraries/SequentialMerkleProofs.sol";
+
+import { IMigratable } from "../abstract/interfaces/IMigratable.sol";
+import { IParameterRegistryLike, INodeRegistryLike, IPayerRegistryLike } from "./interfaces/External.sol";
+import { IPayerReportManager } from "./interfaces/IPayerReportManager.sol";
+
+import { ERC5267 } from "../abstract/ERC5267.sol";
+import { Migratable } from "../abstract/Migratable.sol";
+
+// TODO: If a node operator owns more than one node, their signature for a payer report will be identical, and therefore
+//       replayable across their nodes. This may not be ideal, so it might be necessary to include the node id of the
+//       signer in the digest.
+
+/**
+ * @title  Implementation of the Payer Report Manager.
+ * @notice This contract handles functionality for submitting and settling payer reports.
+ */
+contract PayerReportManager is IPayerReportManager, Initializable, Migratable, ERC5267 {
+    /* ============ Constants/Immutables ============ */
+
+    // solhint-disable-next-line max-line-length
+    /// @dev keccak256("PayerReport(uint256 attesterNodeId,uint256 originatorNodeId,uint256 startSequenceId,uint256 endSequenceId,bytes32 payersMerkleRoot,address[] nodeOperators)")
+    bytes32 public constant PAYER_REPORT_TYPEHASH = 0x73ce31d3128c9ba6af04b3270a898f0abafdf9bed84e47ec7ebd2f3dd8e9c8a5;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable parameterRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable nodeRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable payerRegistry;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.PayerReportManager
+     * @notice The UUPS storage for the payer report manager.
+     * @param  payerReportsByOriginator The mapping of arrays of payer reports by originator node ID.
+     */
+    struct PayerReportManagerStorage {
+        mapping(uint32 originatorId => PayerReport[] payerReports) payerReportsByOriginator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.PayerReportManager")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _PAYER_REPORT_MANAGER_STORAGE_LOCATION =
+        0x26b057ee8e4d60685198828fdf1c618ab8e36b0ab85f54a47b18319f6f718e00;
+
+    function _getPayerReportManagerStorage() internal pure returns (PayerReportManagerStorage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _PAYER_REPORT_MANAGER_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructor for the implementation contract, such that the implementation cannot be initialized.
+     * @param  parameterRegistry_ The address of the parameter registry.
+     * @param  nodeRegistry_      The address of the node registry.
+     * @param  payerRegistry_     The address of the payer registry.
+     * @dev    The parameter registry, node registry, and payer registry must not be the zero address.
+     * @dev    The parameter registry, node registry, and payer registry are immutable so that they are inlined in the
+     *         contract code, and have minimal gas cost.
+     */
+    constructor(address parameterRegistry_, address nodeRegistry_, address payerRegistry_) ERC5267() {
+        require(_isNotZero(parameterRegistry = parameterRegistry_), ZeroParameterRegistry());
+        require(_isNotZero(nodeRegistry = nodeRegistry_), ZeroNodeRegistry());
+        require(_isNotZero(payerRegistry = payerRegistry_), ZeroPayerRegistry());
+
+        _disableInitializers();
+    }
+
+    /* ============ Initialization ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function initialize() public initializer {
+        _initializeERC5267();
+    }
+
+    /* ============ Interactive Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function submit(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) external returns (uint256 payerReportIndex_) {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        payerReportIndex_ = payerReports_.length;
+
+        uint32 lastSequenceId_ = payerReportIndex_ > 0 ? payerReports_[payerReportIndex_ - 1].endSequenceId : 0;
+
+        // Enforces that the start sequence ID is the next expected sequence ID.
+        if (startSequenceId_ != lastSequenceId_ + 1) {
+            revert InvalidStartSequenceId(startSequenceId_, lastSequenceId_ + 1);
+        }
+
+        // Enforces that the end sequence ID is greater than or equal to the start sequence ID.
+        if (endSequenceId_ < startSequenceId_) revert InvalidSequenceIds();
+
+        // Verifies the signatures and gets the array of valid signing node IDs.
+        uint32[] memory validSigningNodeIds_ = _verifySignatures({
+            originatorNodeId_: originatorNodeId_,
+            attesterNodeId_: attesterNodeId_,
+            startSequenceId_: startSequenceId_,
+            endSequenceId_: endSequenceId_,
+            payersMerkleRoot_: payersMerkleRoot_,
+            nodeOperators_: nodeOperators_,
+            signatures_: signatures_
+        });
+
+        payerReports_.push(
+            PayerReport({
+                attesterNodeId: attesterNodeId_,
+                startSequenceId: startSequenceId_,
+                endSequenceId: endSequenceId_,
+                feesSettled: 0,
+                offset: 0,
+                isSettled: false,
+                payersMerkleRoot: payersMerkleRoot_,
+                nodeOperators: nodeOperators_
+            })
+        );
+
+        emit PayerReportSubmitted({
+            originatorNodeId: originatorNodeId_,
+            payerReportIndex: payerReportIndex_,
+            attesterNodeId: attesterNodeId_,
+            startSequenceId: startSequenceId_,
+            endSequenceId: endSequenceId_,
+            payersMerkleRoot: payersMerkleRoot_,
+            nodeOperators: nodeOperators_,
+            signingNodeIds: validSigningNodeIds_
+        });
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function settle(
+        uint32 originatorNodeId_,
+        uint32 payerReportIndex_,
+        bytes[] calldata payerFees_,
+        bytes32[] calldata proofElements_
+    ) external {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        if (payerReportIndex_ >= payerReports_.length) revert PayerReportIndexOutOfBounds();
+
+        PayerReport storage payerReport_ = payerReports_[payerReportIndex_];
+
+        if (payerReport_.isSettled) revert PayerReportEntirelySettled();
+
+        // Verify the payer fees provided are the next sequential payer fees from the last settled offset.
+        SequentialMerkleProofs.verify(payerReport_.payersMerkleRoot, payerReport_.offset, payerFees_, proofElements_);
+
+        payerReport_.offset += uint32(payerFees_.length);
+
+        uint32 leafCount_ = SequentialMerkleProofs.getLeafCount(proofElements_);
+
+        payerReport_.isSettled = leafCount_ == payerReport_.offset;
+
+        // Low level call which handles passing the `payerFees_` arrays as a bytes array that will be automatically
+        // decoded as the required structs by the payer registry's `settleUsage` function.
+        (bool success_, bytes memory returnData_) = payerRegistry.call(
+            abi.encodeWithSelector(IPayerRegistryLike.settleUsage.selector, payerFees_)
+        );
+
+        if (!success_) revert SettleUsageFailed(returnData_);
+
+        uint96 feesSettled_ = abi.decode(returnData_, (uint96));
+
+        emit PayerReportSubsetSettled(
+            originatorNodeId_,
+            payerReportIndex_,
+            uint32(payerFees_.length),
+            leafCount_ - payerReport_.offset,
+            feesSettled_
+        );
+
+        payerReport_.feesSettled += feesSettled_;
+    }
+
+    /// @inheritdoc IMigratable
+    function migrate() external {
+        _migrate(_toAddress(_getRegistryParameter(migratorParameterKey())));
+    }
+
+    /* ============ View/Pure Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function migratorParameterKey() public pure returns (bytes memory key_) {
+        return "xmtp.payerReportManager.migrator";
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReportDigest(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) external view returns (bytes32 digest_) {
+        return
+            _getPayerReportDigest(
+                originatorNodeId_,
+                attesterNodeId_,
+                startSequenceId_,
+                endSequenceId_,
+                payersMerkleRoot_,
+                nodeOperators_
+            );
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReports(uint32 originatorNodeId_) external view returns (PayerReport[] memory payerReports_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_];
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReport(
+        uint32 originatorNodeId_,
+        uint256 payerReportIndex_
+    ) external view returns (PayerReport memory payerReport_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_][payerReportIndex_];
+    }
+
+    /* ============ Internal View/Pure Functions ============ */
+
+    function _name() internal pure override returns (string memory name_) {
+        return "PayerReportManager";
+    }
+
+    function _version() internal pure override returns (string memory version_) {
+        return "1";
+    }
+
+    /**
+     * @dev    Returns the EIP-712 digest for a payer report.
+     * @param  originatorNodeId_ The id of the node originator.
+     * @param  attesterNodeId_   The id of the node attester.
+     * @param  startSequenceId_  The start sequence ID.
+     * @param  endSequenceId_    The end sequence ID.
+     * @param  payersMerkleRoot_ The payers merkle root.
+     * @param  nodeOperators_    The active node operators during the reporting period.
+     * @return digest_           The EIP-712 digest.
+     */
+    function _getPayerReportDigest(
+        uint256 originatorNodeId_,
+        uint256 attesterNodeId_,
+        uint256 startSequenceId_,
+        uint256 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) internal view returns (bytes32 digest_) {
+        return
+            _getDigest(
+                keccak256(
+                    abi.encode(
+                        PAYER_REPORT_TYPEHASH,
+                        originatorNodeId_,
+                        attesterNodeId_,
+                        startSequenceId_,
+                        endSequenceId_,
+                        payersMerkleRoot_,
+                        keccak256(abi.encodePacked(nodeOperators_))
+                    )
+                )
+            );
+    }
+
+    function _getRegistryParameter(bytes memory key_) internal view returns (bytes32 value_) {
+        return IParameterRegistryLike(parameterRegistry).get(key_);
+    }
+
+    function _isNotZero(address input_) internal pure returns (bool isNotZero_) {
+        return input_ != address(0);
+    }
+
+    function _toAddress(bytes32 value_) internal pure returns (address address_) {
+        // slither-disable-next-line assembly
+        assembly {
+            address_ := value_
+        }
+    }
+
+    /**
+     * @dev Verifies that enough of the signatures are valid and provided by canonical node operators and returns the
+     *      array of valid signing node IDs.
+     */
+    function _verifySignatures(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) internal view returns (uint32[] memory validSigningNodeIds_) {
+        bytes32 digest_ = _getPayerReportDigest(
+            originatorNodeId_,
+            attesterNodeId_,
+            startSequenceId_,
+            endSequenceId_,
+            payersMerkleRoot_,
+            nodeOperators_
+        );
+
+        // Need to determine how many of the signatures are valid and provided by canonical node operators.
+        uint8 validSignatureCount_;
+
+        // This array will help in constructing the `validSigningNodeIds_` array, who's length may be less than
+        // the length of the `signatures_` array if some of the signatures are invalid.
+        bool[] memory isValid_ = new bool[](signatures_.length);
+
+        for (uint256 index_; index_ < signatures_.length; ++index_) {
+            uint32 nodeId_ = signatures_[index_].nodeId;
+
+            // Enforces that the signing node IDs are ordered and unique.
+            if (index_ != 0 && nodeId_ <= signatures_[index_ - 1].nodeId) revert UnorderedNodeIds();
+
+            // If the signature is invalid, ignore it.
+            if (!(isValid_[index_] = _verifySignature(digest_, nodeId_, signatures_[index_].signature))) continue;
+
+            ++validSignatureCount_;
+        }
+
+        uint8 requiredSignatureCount_ = (INodeRegistryLike(nodeRegistry).canonicalNodesCount() / 2) + 1;
+
+        // Enforces that the number of valid signatures is greater than one more than half of the canonical node count.
+        if (validSignatureCount_ < requiredSignatureCount_) {
+            revert InsufficientSignatures(validSignatureCount_, requiredSignatureCount_);
+        }
+
+        validSigningNodeIds_ = new uint32[](validSignatureCount_);
+
+        for (uint256 index_; index_ < isValid_.length; ++index_) {
+            if (!isValid_[index_]) continue; // Skip invalid signatures.
+
+            validSigningNodeIds_[index_] = signatures_[index_].nodeId;
+        }
+    }
+
+    /// @dev Returns true if the signature is from the owner of a canonical node.
+    function _verifySignature(
+        bytes32 digest_,
+        uint32 nodeId_,
+        bytes calldata signature_
+    ) internal view returns (bool isValid_) {
+        // If the node is not canonical, the signature is invalid.
+        if (!INodeRegistryLike(nodeRegistry).getIsCanonicalNode(nodeId_)) return false;
+
+        // Try to recover the signer from the signature. We don't want to revert if the signature is invalid.
+        address signer_ = ECDSA.tryRecoverCalldata(digest_, signature_);
+
+        // The signature is valid if the signer is not `address(0)` and the signer is the owner of the node.
+        return (signer_ != address(0)) && (signer_ == INodeRegistryLike(nodeRegistry).ownerOf(nodeId_));
+    }
path: src/settlement-chain/PayerReportManager.sol
user: login: github-advanced-security[bot]
body: ## Calls inside a loop

PayerReportManager._verifySignature(bytes32,uint32,bytes) (src/settlement-chain/PayerReportManager.sol#361-374) has external calls inside a loop: ! INodeRegistryLike(nodeRegistry).getIsCanonicalNode(nodeId_) (src/settlement-chain/PayerReportManager.sol#367)
	Calls stack containing the loop:
		PayerReportManager.submit(uint32,uint32,uint32,uint32,bytes32,address[],IPayerReportManager.PayerReportSignature[])
		PayerReportManager._verifySignatures(uint32,uint32,uint32,uint32,bytes32,address[],IPayerReportManager.PayerReportSignature[])


[Show more details](https://github.com/xmtp/smart-contracts/security/code-scanning/6)
created_at: 2025-05-06T08:34:38Z
author_association: NONE
diff_hunk: @@ -0,0 +1,375 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { ECDSA } from "../../lib/solady/src/utils/ECDSA.sol";
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../libraries/SequentialMerkleProofs.sol";
+
+import { IMigratable } from "../abstract/interfaces/IMigratable.sol";
+import { IParameterRegistryLike, INodeRegistryLike, IPayerRegistryLike } from "./interfaces/External.sol";
+import { IPayerReportManager } from "./interfaces/IPayerReportManager.sol";
+
+import { ERC5267 } from "../abstract/ERC5267.sol";
+import { Migratable } from "../abstract/Migratable.sol";
+
+// TODO: If a node operator owns more than one node, their signature for a payer report will be identical, and therefore
+//       replayable across their nodes. This may not be ideal, so it might be necessary to include the node id of the
+//       signer in the digest.
+
+/**
+ * @title  Implementation of the Payer Report Manager.
+ * @notice This contract handles functionality for submitting and settling payer reports.
+ */
+contract PayerReportManager is IPayerReportManager, Initializable, Migratable, ERC5267 {
+    /* ============ Constants/Immutables ============ */
+
+    // solhint-disable-next-line max-line-length
+    /// @dev keccak256("PayerReport(uint256 attesterNodeId,uint256 originatorNodeId,uint256 startSequenceId,uint256 endSequenceId,bytes32 payersMerkleRoot,address[] nodeOperators)")
+    bytes32 public constant PAYER_REPORT_TYPEHASH = 0x73ce31d3128c9ba6af04b3270a898f0abafdf9bed84e47ec7ebd2f3dd8e9c8a5;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable parameterRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable nodeRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable payerRegistry;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.PayerReportManager
+     * @notice The UUPS storage for the payer report manager.
+     * @param  payerReportsByOriginator The mapping of arrays of payer reports by originator node ID.
+     */
+    struct PayerReportManagerStorage {
+        mapping(uint32 originatorId => PayerReport[] payerReports) payerReportsByOriginator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.PayerReportManager")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _PAYER_REPORT_MANAGER_STORAGE_LOCATION =
+        0x26b057ee8e4d60685198828fdf1c618ab8e36b0ab85f54a47b18319f6f718e00;
+
+    function _getPayerReportManagerStorage() internal pure returns (PayerReportManagerStorage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _PAYER_REPORT_MANAGER_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructor for the implementation contract, such that the implementation cannot be initialized.
+     * @param  parameterRegistry_ The address of the parameter registry.
+     * @param  nodeRegistry_      The address of the node registry.
+     * @param  payerRegistry_     The address of the payer registry.
+     * @dev    The parameter registry, node registry, and payer registry must not be the zero address.
+     * @dev    The parameter registry, node registry, and payer registry are immutable so that they are inlined in the
+     *         contract code, and have minimal gas cost.
+     */
+    constructor(address parameterRegistry_, address nodeRegistry_, address payerRegistry_) ERC5267() {
+        require(_isNotZero(parameterRegistry = parameterRegistry_), ZeroParameterRegistry());
+        require(_isNotZero(nodeRegistry = nodeRegistry_), ZeroNodeRegistry());
+        require(_isNotZero(payerRegistry = payerRegistry_), ZeroPayerRegistry());
+
+        _disableInitializers();
+    }
+
+    /* ============ Initialization ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function initialize() public initializer {
+        _initializeERC5267();
+    }
+
+    /* ============ Interactive Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function submit(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) external returns (uint256 payerReportIndex_) {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        payerReportIndex_ = payerReports_.length;
+
+        uint32 lastSequenceId_ = payerReportIndex_ > 0 ? payerReports_[payerReportIndex_ - 1].endSequenceId : 0;
+
+        // Enforces that the start sequence ID is the next expected sequence ID.
+        if (startSequenceId_ != lastSequenceId_ + 1) {
+            revert InvalidStartSequenceId(startSequenceId_, lastSequenceId_ + 1);
+        }
+
+        // Enforces that the end sequence ID is greater than or equal to the start sequence ID.
+        if (endSequenceId_ < startSequenceId_) revert InvalidSequenceIds();
+
+        // Verifies the signatures and gets the array of valid signing node IDs.
+        uint32[] memory validSigningNodeIds_ = _verifySignatures({
+            originatorNodeId_: originatorNodeId_,
+            attesterNodeId_: attesterNodeId_,
+            startSequenceId_: startSequenceId_,
+            endSequenceId_: endSequenceId_,
+            payersMerkleRoot_: payersMerkleRoot_,
+            nodeOperators_: nodeOperators_,
+            signatures_: signatures_
+        });
+
+        payerReports_.push(
+            PayerReport({
+                attesterNodeId: attesterNodeId_,
+                startSequenceId: startSequenceId_,
+                endSequenceId: endSequenceId_,
+                feesSettled: 0,
+                offset: 0,
+                isSettled: false,
+                payersMerkleRoot: payersMerkleRoot_,
+                nodeOperators: nodeOperators_
+            })
+        );
+
+        emit PayerReportSubmitted({
+            originatorNodeId: originatorNodeId_,
+            payerReportIndex: payerReportIndex_,
+            attesterNodeId: attesterNodeId_,
+            startSequenceId: startSequenceId_,
+            endSequenceId: endSequenceId_,
+            payersMerkleRoot: payersMerkleRoot_,
+            nodeOperators: nodeOperators_,
+            signingNodeIds: validSigningNodeIds_
+        });
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function settle(
+        uint32 originatorNodeId_,
+        uint32 payerReportIndex_,
+        bytes[] calldata payerFees_,
+        bytes32[] calldata proofElements_
+    ) external {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        if (payerReportIndex_ >= payerReports_.length) revert PayerReportIndexOutOfBounds();
+
+        PayerReport storage payerReport_ = payerReports_[payerReportIndex_];
+
+        if (payerReport_.isSettled) revert PayerReportEntirelySettled();
+
+        // Verify the payer fees provided are the next sequential payer fees from the last settled offset.
+        SequentialMerkleProofs.verify(payerReport_.payersMerkleRoot, payerReport_.offset, payerFees_, proofElements_);
+
+        payerReport_.offset += uint32(payerFees_.length);
+
+        uint32 leafCount_ = SequentialMerkleProofs.getLeafCount(proofElements_);
+
+        payerReport_.isSettled = leafCount_ == payerReport_.offset;
+
+        // Low level call which handles passing the `payerFees_` arrays as a bytes array that will be automatically
+        // decoded as the required structs by the payer registry's `settleUsage` function.
+        (bool success_, bytes memory returnData_) = payerRegistry.call(
+            abi.encodeWithSelector(IPayerRegistryLike.settleUsage.selector, payerFees_)
+        );
+
+        if (!success_) revert SettleUsageFailed(returnData_);
+
+        uint96 feesSettled_ = abi.decode(returnData_, (uint96));
+
+        emit PayerReportSubsetSettled(
+            originatorNodeId_,
+            payerReportIndex_,
+            uint32(payerFees_.length),
+            leafCount_ - payerReport_.offset,
+            feesSettled_
+        );
+
+        payerReport_.feesSettled += feesSettled_;
+    }
+
+    /// @inheritdoc IMigratable
+    function migrate() external {
+        _migrate(_toAddress(_getRegistryParameter(migratorParameterKey())));
+    }
+
+    /* ============ View/Pure Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function migratorParameterKey() public pure returns (bytes memory key_) {
+        return "xmtp.payerReportManager.migrator";
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReportDigest(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) external view returns (bytes32 digest_) {
+        return
+            _getPayerReportDigest(
+                originatorNodeId_,
+                attesterNodeId_,
+                startSequenceId_,
+                endSequenceId_,
+                payersMerkleRoot_,
+                nodeOperators_
+            );
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReports(uint32 originatorNodeId_) external view returns (PayerReport[] memory payerReports_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_];
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReport(
+        uint32 originatorNodeId_,
+        uint256 payerReportIndex_
+    ) external view returns (PayerReport memory payerReport_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_][payerReportIndex_];
+    }
+
+    /* ============ Internal View/Pure Functions ============ */
+
+    function _name() internal pure override returns (string memory name_) {
+        return "PayerReportManager";
+    }
+
+    function _version() internal pure override returns (string memory version_) {
+        return "1";
+    }
+
+    /**
+     * @dev    Returns the EIP-712 digest for a payer report.
+     * @param  originatorNodeId_ The id of the node originator.
+     * @param  attesterNodeId_   The id of the node attester.
+     * @param  startSequenceId_  The start sequence ID.
+     * @param  endSequenceId_    The end sequence ID.
+     * @param  payersMerkleRoot_ The payers merkle root.
+     * @param  nodeOperators_    The active node operators during the reporting period.
+     * @return digest_           The EIP-712 digest.
+     */
+    function _getPayerReportDigest(
+        uint256 originatorNodeId_,
+        uint256 attesterNodeId_,
+        uint256 startSequenceId_,
+        uint256 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) internal view returns (bytes32 digest_) {
+        return
+            _getDigest(
+                keccak256(
+                    abi.encode(
+                        PAYER_REPORT_TYPEHASH,
+                        originatorNodeId_,
+                        attesterNodeId_,
+                        startSequenceId_,
+                        endSequenceId_,
+                        payersMerkleRoot_,
+                        keccak256(abi.encodePacked(nodeOperators_))
+                    )
+                )
+            );
+    }
+
+    function _getRegistryParameter(bytes memory key_) internal view returns (bytes32 value_) {
+        return IParameterRegistryLike(parameterRegistry).get(key_);
+    }
+
+    function _isNotZero(address input_) internal pure returns (bool isNotZero_) {
+        return input_ != address(0);
+    }
+
+    function _toAddress(bytes32 value_) internal pure returns (address address_) {
+        // slither-disable-next-line assembly
+        assembly {
+            address_ := value_
+        }
+    }
+
+    /**
+     * @dev Verifies that enough of the signatures are valid and provided by canonical node operators and returns the
+     *      array of valid signing node IDs.
+     */
+    function _verifySignatures(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) internal view returns (uint32[] memory validSigningNodeIds_) {
+        bytes32 digest_ = _getPayerReportDigest(
+            originatorNodeId_,
+            attesterNodeId_,
+            startSequenceId_,
+            endSequenceId_,
+            payersMerkleRoot_,
+            nodeOperators_
+        );
+
+        // Need to determine how many of the signatures are valid and provided by canonical node operators.
+        uint8 validSignatureCount_;
+
+        // This array will help in constructing the `validSigningNodeIds_` array, who's length may be less than
+        // the length of the `signatures_` array if some of the signatures are invalid.
+        bool[] memory isValid_ = new bool[](signatures_.length);
+
+        for (uint256 index_; index_ < signatures_.length; ++index_) {
+            uint32 nodeId_ = signatures_[index_].nodeId;
+
+            // Enforces that the signing node IDs are ordered and unique.
+            if (index_ != 0 && nodeId_ <= signatures_[index_ - 1].nodeId) revert UnorderedNodeIds();
+
+            // If the signature is invalid, ignore it.
+            if (!(isValid_[index_] = _verifySignature(digest_, nodeId_, signatures_[index_].signature))) continue;
+
+            ++validSignatureCount_;
+        }
+
+        uint8 requiredSignatureCount_ = (INodeRegistryLike(nodeRegistry).canonicalNodesCount() / 2) + 1;
+
+        // Enforces that the number of valid signatures is greater than one more than half of the canonical node count.
+        if (validSignatureCount_ < requiredSignatureCount_) {
+            revert InsufficientSignatures(validSignatureCount_, requiredSignatureCount_);
+        }
+
+        validSigningNodeIds_ = new uint32[](validSignatureCount_);
+
+        for (uint256 index_; index_ < isValid_.length; ++index_) {
+            if (!isValid_[index_]) continue; // Skip invalid signatures.
+
+            validSigningNodeIds_[index_] = signatures_[index_].nodeId;
+        }
+    }
+
+    /// @dev Returns true if the signature is from the owner of a canonical node.
+    function _verifySignature(
+        bytes32 digest_,
+        uint32 nodeId_,
+        bytes calldata signature_
+    ) internal view returns (bool isValid_) {
+        // If the node is not canonical, the signature is invalid.
+        if (!INodeRegistryLike(nodeRegistry).getIsCanonicalNode(nodeId_)) return false;
+
+        // Try to recover the signer from the signature. We don't want to revert if the signature is invalid.
+        address signer_ = ECDSA.tryRecoverCalldata(digest_, signature_);
+
+        // The signature is valid if the signer is not `address(0)` and the signer is the owner of the node.
+        return (signer_ != address(0)) && (signer_ == INodeRegistryLike(nodeRegistry).ownerOf(nodeId_));
+    }
path: src/settlement-chain/PayerReportManager.sol
user: login: github-advanced-security[bot]
body: ## Calls inside a loop

PayerReportManager._verifySignature(bytes32,uint32,bytes) (src/settlement-chain/PayerReportManager.sol#361-374) has external calls inside a loop: (signer_ != address(0)) && (signer_ == INodeRegistryLike(nodeRegistry).ownerOf(nodeId_)) (src/settlement-chain/PayerReportManager.sol#373)
	Calls stack containing the loop:
		PayerReportManager.submit(uint32,uint32,uint32,uint32,bytes32,address[],IPayerReportManager.PayerReportSignature[])
		PayerReportManager._verifySignatures(uint32,uint32,uint32,uint32,bytes32,address[],IPayerReportManager.PayerReportSignature[])


[Show more details](https://github.com/xmtp/smart-contracts/security/code-scanning/7)
created_at: 2025-05-06T08:34:38Z
author_association: NONE
diff_hunk: @@ -0,0 +1,51 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { IERC5267 } from "./interfaces/IERC5267.sol";
+
+import { ERC712 } from "./ERC721.sol";
path: src/abstract/ERC5267.sol
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix import path typo**

There appears to be a typo in the import path. You're importing `ERC712` from a file named `ERC721.sol`. This seems incorrect as ERC721 typically refers to the NFT standard, not the EIP-712 typed data signing standard.

```diff
-import { ERC712 } from "./ERC721.sol";
+import { ERC712 } from "./ERC712.sol";
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
import { ERC712 } from "./ERC712.sol";
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->

✅ Addressed in commit 1950d51

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-06T08:35:50Z
author_association: NONE
diff_hunk: @@ -0,0 +1,99 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { IERC712 } from "./interfaces/IERC712.sol";
+
+/**
+ * @title An abstract implementation of EIP-712 for typed structured data hashing and signing.
+ */
+abstract contract ERC712 is IERC712 {
path: src/abstract/ERC721.sol
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Misleading filename (`ERC721.sol`) – contract is `ERC712`.**

The file is named as if it implements the ERC-721 NFT spec, yet it actually contains an EIP-712 helper.  
This can trip up IDE file-tree grepping, confuse newcomers, and break import-path conventions (`import "./abstract/ERC721.sol"` reads oddly).

A quick rename preserves clarity with zero byte-code impact:

```diff
- src/abstract/ERC721.sol
+ src/abstract/ERC712.sol
```

Remember to adjust all imports (`./abstract/ERC721.sol` → `./abstract/ERC712.sol`).




<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
- src/abstract/ERC721.sol
+ src/abstract/ERC712.sol
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-06T08:35:50Z
author_association: NONE
diff_hunk: @@ -0,0 +1,375 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { ECDSA } from "../../lib/solady/src/utils/ECDSA.sol";
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../libraries/SequentialMerkleProofs.sol";
+
+import { IMigratable } from "../abstract/interfaces/IMigratable.sol";
+import { IParameterRegistryLike, INodeRegistryLike, IPayerRegistryLike } from "./interfaces/External.sol";
+import { IPayerReportManager } from "./interfaces/IPayerReportManager.sol";
+
+import { ERC5267 } from "../abstract/ERC5267.sol";
+import { Migratable } from "../abstract/Migratable.sol";
+
+// TODO: If a node operator owns more than one node, their signature for a payer report will be identical, and therefore
+//       replayable across their nodes. This may not be ideal, so it might be necessary to include the node id of the
+//       signer in the digest.
+
+/**
+ * @title  Implementation of the Payer Report Manager.
+ * @notice This contract handles functionality for submitting and settling payer reports.
+ */
+contract PayerReportManager is IPayerReportManager, Initializable, Migratable, ERC5267 {
+    /* ============ Constants/Immutables ============ */
+
+    // solhint-disable-next-line max-line-length
+    /// @dev keccak256("PayerReport(uint256 attesterNodeId,uint256 originatorNodeId,uint256 startSequenceId,uint256 endSequenceId,bytes32 payersMerkleRoot,address[] nodeOperators)")
+    bytes32 public constant PAYER_REPORT_TYPEHASH = 0x73ce31d3128c9ba6af04b3270a898f0abafdf9bed84e47ec7ebd2f3dd8e9c8a5;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable parameterRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable nodeRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable payerRegistry;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.PayerReportManager
+     * @notice The UUPS storage for the payer report manager.
+     * @param  payerReportsByOriginator The mapping of arrays of payer reports by originator node ID.
+     */
+    struct PayerReportManagerStorage {
+        mapping(uint32 originatorId => PayerReport[] payerReports) payerReportsByOriginator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.PayerReportManager")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _PAYER_REPORT_MANAGER_STORAGE_LOCATION =
+        0x26b057ee8e4d60685198828fdf1c618ab8e36b0ab85f54a47b18319f6f718e00;
+
+    function _getPayerReportManagerStorage() internal pure returns (PayerReportManagerStorage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _PAYER_REPORT_MANAGER_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructor for the implementation contract, such that the implementation cannot be initialized.
+     * @param  parameterRegistry_ The address of the parameter registry.
+     * @param  nodeRegistry_      The address of the node registry.
+     * @param  payerRegistry_     The address of the payer registry.
+     * @dev    The parameter registry, node registry, and payer registry must not be the zero address.
+     * @dev    The parameter registry, node registry, and payer registry are immutable so that they are inlined in the
+     *         contract code, and have minimal gas cost.
+     */
+    constructor(address parameterRegistry_, address nodeRegistry_, address payerRegistry_) ERC5267() {
+        require(_isNotZero(parameterRegistry = parameterRegistry_), ZeroParameterRegistry());
+        require(_isNotZero(nodeRegistry = nodeRegistry_), ZeroNodeRegistry());
+        require(_isNotZero(payerRegistry = payerRegistry_), ZeroPayerRegistry());
+
+        _disableInitializers();
+    }
+
+    /* ============ Initialization ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function initialize() public initializer {
+        _initializeERC5267();
+    }
+
+    /* ============ Interactive Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function submit(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) external returns (uint256 payerReportIndex_) {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        payerReportIndex_ = payerReports_.length;
+
+        uint32 lastSequenceId_ = payerReportIndex_ > 0 ? payerReports_[payerReportIndex_ - 1].endSequenceId : 0;
+
+        // Enforces that the start sequence ID is the next expected sequence ID.
+        if (startSequenceId_ != lastSequenceId_ + 1) {
+            revert InvalidStartSequenceId(startSequenceId_, lastSequenceId_ + 1);
+        }
+
+        // Enforces that the end sequence ID is greater than or equal to the start sequence ID.
+        if (endSequenceId_ < startSequenceId_) revert InvalidSequenceIds();
+
+        // Verifies the signatures and gets the array of valid signing node IDs.
+        uint32[] memory validSigningNodeIds_ = _verifySignatures({
+            originatorNodeId_: originatorNodeId_,
+            attesterNodeId_: attesterNodeId_,
+            startSequenceId_: startSequenceId_,
+            endSequenceId_: endSequenceId_,
+            payersMerkleRoot_: payersMerkleRoot_,
+            nodeOperators_: nodeOperators_,
+            signatures_: signatures_
+        });
+
+        payerReports_.push(
+            PayerReport({
+                attesterNodeId: attesterNodeId_,
+                startSequenceId: startSequenceId_,
+                endSequenceId: endSequenceId_,
+                feesSettled: 0,
+                offset: 0,
+                isSettled: false,
+                payersMerkleRoot: payersMerkleRoot_,
+                nodeOperators: nodeOperators_
+            })
+        );
+
+        emit PayerReportSubmitted({
+            originatorNodeId: originatorNodeId_,
+            payerReportIndex: payerReportIndex_,
+            attesterNodeId: attesterNodeId_,
+            startSequenceId: startSequenceId_,
+            endSequenceId: endSequenceId_,
+            payersMerkleRoot: payersMerkleRoot_,
+            nodeOperators: nodeOperators_,
+            signingNodeIds: validSigningNodeIds_
+        });
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function settle(
+        uint32 originatorNodeId_,
+        uint32 payerReportIndex_,
+        bytes[] calldata payerFees_,
+        bytes32[] calldata proofElements_
+    ) external {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        if (payerReportIndex_ >= payerReports_.length) revert PayerReportIndexOutOfBounds();
+
+        PayerReport storage payerReport_ = payerReports_[payerReportIndex_];
+
+        if (payerReport_.isSettled) revert PayerReportEntirelySettled();
+
+        // Verify the payer fees provided are the next sequential payer fees from the last settled offset.
+        SequentialMerkleProofs.verify(payerReport_.payersMerkleRoot, payerReport_.offset, payerFees_, proofElements_);
+
+        payerReport_.offset += uint32(payerFees_.length);
+
+        uint32 leafCount_ = SequentialMerkleProofs.getLeafCount(proofElements_);
+
+        payerReport_.isSettled = leafCount_ == payerReport_.offset;
+
+        // Low level call which handles passing the `payerFees_` arrays as a bytes array that will be automatically
+        // decoded as the required structs by the payer registry's `settleUsage` function.
+        (bool success_, bytes memory returnData_) = payerRegistry.call(
+            abi.encodeWithSelector(IPayerRegistryLike.settleUsage.selector, payerFees_)
+        );
+
+        if (!success_) revert SettleUsageFailed(returnData_);
+
+        uint96 feesSettled_ = abi.decode(returnData_, (uint96));
+
+        emit PayerReportSubsetSettled(
+            originatorNodeId_,
+            payerReportIndex_,
+            uint32(payerFees_.length),
+            leafCount_ - payerReport_.offset,
+            feesSettled_
+        );
+
+        payerReport_.feesSettled += feesSettled_;
+    }
+
+    /// @inheritdoc IMigratable
+    function migrate() external {
+        _migrate(_toAddress(_getRegistryParameter(migratorParameterKey())));
+    }
+
+    /* ============ View/Pure Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function migratorParameterKey() public pure returns (bytes memory key_) {
+        return "xmtp.payerReportManager.migrator";
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReportDigest(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) external view returns (bytes32 digest_) {
+        return
+            _getPayerReportDigest(
+                originatorNodeId_,
+                attesterNodeId_,
+                startSequenceId_,
+                endSequenceId_,
+                payersMerkleRoot_,
+                nodeOperators_
+            );
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReports(uint32 originatorNodeId_) external view returns (PayerReport[] memory payerReports_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_];
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReport(
+        uint32 originatorNodeId_,
+        uint256 payerReportIndex_
+    ) external view returns (PayerReport memory payerReport_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_][payerReportIndex_];
+    }
+
+    /* ============ Internal View/Pure Functions ============ */
+
+    function _name() internal pure override returns (string memory name_) {
+        return "PayerReportManager";
+    }
+
+    function _version() internal pure override returns (string memory version_) {
+        return "1";
+    }
+
+    /**
+     * @dev    Returns the EIP-712 digest for a payer report.
+     * @param  originatorNodeId_ The id of the node originator.
+     * @param  attesterNodeId_   The id of the node attester.
+     * @param  startSequenceId_  The start sequence ID.
+     * @param  endSequenceId_    The end sequence ID.
+     * @param  payersMerkleRoot_ The payers merkle root.
+     * @param  nodeOperators_    The active node operators during the reporting period.
+     * @return digest_           The EIP-712 digest.
+     */
+    function _getPayerReportDigest(
+        uint256 originatorNodeId_,
+        uint256 attesterNodeId_,
+        uint256 startSequenceId_,
+        uint256 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) internal view returns (bytes32 digest_) {
+        return
+            _getDigest(
+                keccak256(
+                    abi.encode(
+                        PAYER_REPORT_TYPEHASH,
+                        originatorNodeId_,
+                        attesterNodeId_,
+                        startSequenceId_,
+                        endSequenceId_,
+                        payersMerkleRoot_,
+                        keccak256(abi.encodePacked(nodeOperators_))
+                    )
+                )
+            );
+    }
+
+    function _getRegistryParameter(bytes memory key_) internal view returns (bytes32 value_) {
+        return IParameterRegistryLike(parameterRegistry).get(key_);
+    }
+
+    function _isNotZero(address input_) internal pure returns (bool isNotZero_) {
+        return input_ != address(0);
+    }
+
+    function _toAddress(bytes32 value_) internal pure returns (address address_) {
+        // slither-disable-next-line assembly
+        assembly {
+            address_ := value_
+        }
+    }
+
+    /**
+     * @dev Verifies that enough of the signatures are valid and provided by canonical node operators and returns the
+     *      array of valid signing node IDs.
+     */
+    function _verifySignatures(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) internal view returns (uint32[] memory validSigningNodeIds_) {
+        bytes32 digest_ = _getPayerReportDigest(
+            originatorNodeId_,
+            attesterNodeId_,
+            startSequenceId_,
+            endSequenceId_,
+            payersMerkleRoot_,
+            nodeOperators_
+        );
+
+        // Need to determine how many of the signatures are valid and provided by canonical node operators.
+        uint8 validSignatureCount_;
+
+        // This array will help in constructing the `validSigningNodeIds_` array, who's length may be less than
+        // the length of the `signatures_` array if some of the signatures are invalid.
+        bool[] memory isValid_ = new bool[](signatures_.length);
+
+        for (uint256 index_; index_ < signatures_.length; ++index_) {
+            uint32 nodeId_ = signatures_[index_].nodeId;
path: src/settlement-chain/PayerReportManager.sol
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Avoid silent overflows by widening `validSignatureCount_`**

`validSignatureCount_` is declared `uint8`, yet `signatures_.length` can be arbitrarily large (especially in fuzz tests).  
If more than 255 valid signatures are provided the counter will silently wrap to `0`, triggering a false `InsufficientSignatures` revert.

```diff
-        uint8 validSignatureCount_;
+        uint256 validSignatureCount_;
```

Similarly change `uint8 requiredSignatureCount_` to `uint256` (or cast once you have the value) to keep the arithmetic homogeneous.  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        // Need to determine how many of the signatures are valid and provided by canonical node operators.
-        uint8 validSignatureCount_;
+        uint256 validSignatureCount_;

        // This array will help in constructing the `validSigningNodeIds_` array, who's length may be less than
        // the length of the `signatures_` array if some of the signatures are invalid.
        bool[] memory isValid_ = new bool[](signatures_.length);

        for (uint256 index_; index_ < signatures_.length; ++index_) {
            uint32 nodeId_ = signatures_[index_].nodeId;
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-06T08:35:50Z
author_association: NONE
diff_hunk: @@ -0,0 +1,375 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { ECDSA } from "../../lib/solady/src/utils/ECDSA.sol";
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../libraries/SequentialMerkleProofs.sol";
+
+import { IMigratable } from "../abstract/interfaces/IMigratable.sol";
+import { IParameterRegistryLike, INodeRegistryLike, IPayerRegistryLike } from "./interfaces/External.sol";
+import { IPayerReportManager } from "./interfaces/IPayerReportManager.sol";
+
+import { ERC5267 } from "../abstract/ERC5267.sol";
+import { Migratable } from "../abstract/Migratable.sol";
+
+// TODO: If a node operator owns more than one node, their signature for a payer report will be identical, and therefore
+//       replayable across their nodes. This may not be ideal, so it might be necessary to include the node id of the
+//       signer in the digest.
+
+/**
+ * @title  Implementation of the Payer Report Manager.
+ * @notice This contract handles functionality for submitting and settling payer reports.
+ */
+contract PayerReportManager is IPayerReportManager, Initializable, Migratable, ERC5267 {
+    /* ============ Constants/Immutables ============ */
+
+    // solhint-disable-next-line max-line-length
+    /// @dev keccak256("PayerReport(uint256 attesterNodeId,uint256 originatorNodeId,uint256 startSequenceId,uint256 endSequenceId,bytes32 payersMerkleRoot,address[] nodeOperators)")
+    bytes32 public constant PAYER_REPORT_TYPEHASH = 0x73ce31d3128c9ba6af04b3270a898f0abafdf9bed84e47ec7ebd2f3dd8e9c8a5;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable parameterRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable nodeRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable payerRegistry;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.PayerReportManager
+     * @notice The UUPS storage for the payer report manager.
+     * @param  payerReportsByOriginator The mapping of arrays of payer reports by originator node ID.
+     */
+    struct PayerReportManagerStorage {
+        mapping(uint32 originatorId => PayerReport[] payerReports) payerReportsByOriginator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.PayerReportManager")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _PAYER_REPORT_MANAGER_STORAGE_LOCATION =
+        0x26b057ee8e4d60685198828fdf1c618ab8e36b0ab85f54a47b18319f6f718e00;
+
+    function _getPayerReportManagerStorage() internal pure returns (PayerReportManagerStorage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _PAYER_REPORT_MANAGER_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructor for the implementation contract, such that the implementation cannot be initialized.
+     * @param  parameterRegistry_ The address of the parameter registry.
+     * @param  nodeRegistry_      The address of the node registry.
+     * @param  payerRegistry_     The address of the payer registry.
+     * @dev    The parameter registry, node registry, and payer registry must not be the zero address.
+     * @dev    The parameter registry, node registry, and payer registry are immutable so that they are inlined in the
+     *         contract code, and have minimal gas cost.
+     */
+    constructor(address parameterRegistry_, address nodeRegistry_, address payerRegistry_) ERC5267() {
+        require(_isNotZero(parameterRegistry = parameterRegistry_), ZeroParameterRegistry());
+        require(_isNotZero(nodeRegistry = nodeRegistry_), ZeroNodeRegistry());
+        require(_isNotZero(payerRegistry = payerRegistry_), ZeroPayerRegistry());
+
+        _disableInitializers();
+    }
+
+    /* ============ Initialization ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function initialize() public initializer {
+        _initializeERC5267();
+    }
+
+    /* ============ Interactive Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function submit(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) external returns (uint256 payerReportIndex_) {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        payerReportIndex_ = payerReports_.length;
+
+        uint32 lastSequenceId_ = payerReportIndex_ > 0 ? payerReports_[payerReportIndex_ - 1].endSequenceId : 0;
+
+        // Enforces that the start sequence ID is the next expected sequence ID.
+        if (startSequenceId_ != lastSequenceId_ + 1) {
+            revert InvalidStartSequenceId(startSequenceId_, lastSequenceId_ + 1);
+        }
+
+        // Enforces that the end sequence ID is greater than or equal to the start sequence ID.
+        if (endSequenceId_ < startSequenceId_) revert InvalidSequenceIds();
+
+        // Verifies the signatures and gets the array of valid signing node IDs.
+        uint32[] memory validSigningNodeIds_ = _verifySignatures({
+            originatorNodeId_: originatorNodeId_,
+            attesterNodeId_: attesterNodeId_,
+            startSequenceId_: startSequenceId_,
+            endSequenceId_: endSequenceId_,
+            payersMerkleRoot_: payersMerkleRoot_,
+            nodeOperators_: nodeOperators_,
+            signatures_: signatures_
+        });
+
+        payerReports_.push(
+            PayerReport({
+                attesterNodeId: attesterNodeId_,
+                startSequenceId: startSequenceId_,
+                endSequenceId: endSequenceId_,
+                feesSettled: 0,
+                offset: 0,
+                isSettled: false,
+                payersMerkleRoot: payersMerkleRoot_,
+                nodeOperators: nodeOperators_
+            })
+        );
+
+        emit PayerReportSubmitted({
+            originatorNodeId: originatorNodeId_,
+            payerReportIndex: payerReportIndex_,
+            attesterNodeId: attesterNodeId_,
+            startSequenceId: startSequenceId_,
+            endSequenceId: endSequenceId_,
+            payersMerkleRoot: payersMerkleRoot_,
+            nodeOperators: nodeOperators_,
+            signingNodeIds: validSigningNodeIds_
+        });
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function settle(
+        uint32 originatorNodeId_,
+        uint32 payerReportIndex_,
+        bytes[] calldata payerFees_,
+        bytes32[] calldata proofElements_
+    ) external {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        if (payerReportIndex_ >= payerReports_.length) revert PayerReportIndexOutOfBounds();
+
+        PayerReport storage payerReport_ = payerReports_[payerReportIndex_];
+
+        if (payerReport_.isSettled) revert PayerReportEntirelySettled();
+
+        // Verify the payer fees provided are the next sequential payer fees from the last settled offset.
+        SequentialMerkleProofs.verify(payerReport_.payersMerkleRoot, payerReport_.offset, payerFees_, proofElements_);
+
+        payerReport_.offset += uint32(payerFees_.length);
+
+        uint32 leafCount_ = SequentialMerkleProofs.getLeafCount(proofElements_);
+
+        payerReport_.isSettled = leafCount_ == payerReport_.offset;
+
+        // Low level call which handles passing the `payerFees_` arrays as a bytes array that will be automatically
+        // decoded as the required structs by the payer registry's `settleUsage` function.
+        (bool success_, bytes memory returnData_) = payerRegistry.call(
+            abi.encodeWithSelector(IPayerRegistryLike.settleUsage.selector, payerFees_)
+        );
+
+        if (!success_) revert SettleUsageFailed(returnData_);
+
+        uint96 feesSettled_ = abi.decode(returnData_, (uint96));
+
+        emit PayerReportSubsetSettled(
+            originatorNodeId_,
+            payerReportIndex_,
+            uint32(payerFees_.length),
+            leafCount_ - payerReport_.offset,
+            feesSettled_
+        );
+
+        payerReport_.feesSettled += feesSettled_;
+    }
+
+    /// @inheritdoc IMigratable
+    function migrate() external {
+        _migrate(_toAddress(_getRegistryParameter(migratorParameterKey())));
+    }
+
+    /* ============ View/Pure Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function migratorParameterKey() public pure returns (bytes memory key_) {
+        return "xmtp.payerReportManager.migrator";
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReportDigest(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) external view returns (bytes32 digest_) {
+        return
+            _getPayerReportDigest(
+                originatorNodeId_,
+                attesterNodeId_,
+                startSequenceId_,
+                endSequenceId_,
+                payersMerkleRoot_,
+                nodeOperators_
+            );
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReports(uint32 originatorNodeId_) external view returns (PayerReport[] memory payerReports_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_];
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReport(
+        uint32 originatorNodeId_,
+        uint256 payerReportIndex_
+    ) external view returns (PayerReport memory payerReport_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_][payerReportIndex_];
+    }
+
+    /* ============ Internal View/Pure Functions ============ */
+
+    function _name() internal pure override returns (string memory name_) {
+        return "PayerReportManager";
+    }
+
+    function _version() internal pure override returns (string memory version_) {
+        return "1";
+    }
+
+    /**
+     * @dev    Returns the EIP-712 digest for a payer report.
+     * @param  originatorNodeId_ The id of the node originator.
+     * @param  attesterNodeId_   The id of the node attester.
+     * @param  startSequenceId_  The start sequence ID.
+     * @param  endSequenceId_    The end sequence ID.
+     * @param  payersMerkleRoot_ The payers merkle root.
+     * @param  nodeOperators_    The active node operators during the reporting period.
+     * @return digest_           The EIP-712 digest.
+     */
+    function _getPayerReportDigest(
+        uint256 originatorNodeId_,
+        uint256 attesterNodeId_,
+        uint256 startSequenceId_,
+        uint256 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) internal view returns (bytes32 digest_) {
+        return
+            _getDigest(
+                keccak256(
+                    abi.encode(
+                        PAYER_REPORT_TYPEHASH,
+                        originatorNodeId_,
+                        attesterNodeId_,
+                        startSequenceId_,
+                        endSequenceId_,
+                        payersMerkleRoot_,
+                        keccak256(abi.encodePacked(nodeOperators_))
+                    )
+                )
+            );
+    }
+
+    function _getRegistryParameter(bytes memory key_) internal view returns (bytes32 value_) {
+        return IParameterRegistryLike(parameterRegistry).get(key_);
+    }
+
+    function _isNotZero(address input_) internal pure returns (bool isNotZero_) {
+        return input_ != address(0);
+    }
+
+    function _toAddress(bytes32 value_) internal pure returns (address address_) {
+        // slither-disable-next-line assembly
+        assembly {
+            address_ := value_
+        }
+    }
+
+    /**
+     * @dev Verifies that enough of the signatures are valid and provided by canonical node operators and returns the
+     *      array of valid signing node IDs.
+     */
+    function _verifySignatures(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) internal view returns (uint32[] memory validSigningNodeIds_) {
+        bytes32 digest_ = _getPayerReportDigest(
+            originatorNodeId_,
+            attesterNodeId_,
+            startSequenceId_,
+            endSequenceId_,
+            payersMerkleRoot_,
+            nodeOperators_
+        );
+
+        // Need to determine how many of the signatures are valid and provided by canonical node operators.
+        uint8 validSignatureCount_;
+
+        // This array will help in constructing the `validSigningNodeIds_` array, who's length may be less than
+        // the length of the `signatures_` array if some of the signatures are invalid.
+        bool[] memory isValid_ = new bool[](signatures_.length);
+
+        for (uint256 index_; index_ < signatures_.length; ++index_) {
+            uint32 nodeId_ = signatures_[index_].nodeId;
+
+            // Enforces that the signing node IDs are ordered and unique.
+            if (index_ != 0 && nodeId_ <= signatures_[index_ - 1].nodeId) revert UnorderedNodeIds();
+
+            // If the signature is invalid, ignore it.
+            if (!(isValid_[index_] = _verifySignature(digest_, nodeId_, signatures_[index_].signature))) continue;
+
+            ++validSignatureCount_;
+        }
+
+        uint8 requiredSignatureCount_ = (INodeRegistryLike(nodeRegistry).canonicalNodesCount() / 2) + 1;
+
+        // Enforces that the number of valid signatures is greater than one more than half of the canonical node count.
+        if (validSignatureCount_ < requiredSignatureCount_) {
+            revert InsufficientSignatures(validSignatureCount_, requiredSignatureCount_);
+        }
+
+        validSigningNodeIds_ = new uint32[](validSignatureCount_);
+
+        for (uint256 index_; index_ < isValid_.length; ++index_) {
+            if (!isValid_[index_]) continue; // Skip invalid signatures.
+
+            validSigningNodeIds_[index_] = signatures_[index_].nodeId;
+        }
path: src/settlement-chain/PayerReportManager.sol
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix out-of-bounds write in `validSigningNodeIds_` construction**

`validSigningNodeIds_` is pre-sized to `validSignatureCount_`, but the second loop still indexes with `index_`.  
If some signatures are invalid the code will attempt to write past the end of the array (`index_ >= validSignatureCount_`) and immediately revert with an array-out-of-bounds panic.

```diff
-        for (uint256 index_; index_ < isValid_.length; ++index_) {
-            if (!isValid_[index_]) continue; // Skip invalid signatures.
-
-            validSigningNodeIds_[index_] = signatures_[index_].nodeId;
-        }
+        uint256 ptr_;
+        for (uint256 index_; index_ < isValid_.length; ++index_) {
+            if (!isValid_[index_]) continue;      // Skip invalid signatures.
+
+            validSigningNodeIds_[ptr_] = signatures_[index_].nodeId;
+            unchecked {
+                ++ptr_;                           // Safe: ptr < validSignatureCount_
+            }
+        }
```

Please also add a sanity `assert(ptr_ == validSigningNodeIds_.length);` in tests to ensure the array is fully populated.  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        validSigningNodeIds_ = new uint32[](validSignatureCount_);

        uint256 ptr_;
        for (uint256 index_; index_ < isValid_.length; ++index_) {
            if (!isValid_[index_]) continue;      // Skip invalid signatures.

            validSigningNodeIds_[ptr_] = signatures_[index_].nodeId;
            unchecked {
                ++ptr_;                           // Safe: ptr < validSignatureCount_
            }
        }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->

✅ Addressed in commit 1950d51

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-06T08:35:50Z
author_association: NONE
diff_hunk: @@ -0,0 +1,873 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { Test } from "../../lib/forge-std/src/Test.sol";
+
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../../src/libraries/SequentialMerkleProofs.sol";
+
+import { IERC1967 } from "../../src/abstract/interfaces/IERC1967.sol";
+import { IMigratable } from "../../src/abstract/interfaces/IMigratable.sol";
+import { IPayerReportManager } from "../../src/settlement-chain/interfaces/IPayerReportManager.sol";
+
+import { Proxy } from "../../src/any-chain/Proxy.sol";
+
+import { PayerReportManagerHarness } from "../utils/Harnesses.sol";
+
+import {
+    MockParameterRegistry,
+    MockNodeRegistry,
+    MockPayerRegistry,
+    MockMigrator,
+    MockFailingMigrator
+} from "../utils/Mocks.sol";
+
+import { Utils } from "../utils/Utils.sol";
+
+contract PayerReportManagerTests is Test, Utils {
+    bytes32 internal constant _EIP712_DOMAIN_HASH = 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f;
+
+    bytes internal constant _MIGRATOR_KEY = "xmtp.payerReportManager.migrator";
+
+    PayerReportManagerHarness internal _manager;
+
+    address internal _implementation;
+    address internal _parameterRegistry;
+    address internal _nodeRegistry;
+    address internal _payerRegistry;
+
+    address internal _nodeOperator1;
+    uint256 internal _nodeOperator1Pk;
+    address internal _nodeOperator2;
+    uint256 internal _nodeOperator2Pk;
+    address internal _nodeOperator3;
+    uint256 internal _nodeOperator3Pk;
+    address internal _nodeOperator4;
+    uint256 internal _nodeOperator4Pk;
+
+    function setUp() external {
+        (_nodeOperator1, _nodeOperator1Pk) = makeAddrAndKey("nodeOperator1");
+        (_nodeOperator2, _nodeOperator2Pk) = makeAddrAndKey("nodeOperator2");
+        (_nodeOperator3, _nodeOperator3Pk) = makeAddrAndKey("nodeOperator3");
+        (_nodeOperator4, _nodeOperator4Pk) = makeAddrAndKey("nodeOperator4");
+
+        _parameterRegistry = address(new MockParameterRegistry());
+        _nodeRegistry = address(new MockNodeRegistry());
+        _payerRegistry = address(new MockPayerRegistry());
+
+        _implementation = address(new PayerReportManagerHarness(_parameterRegistry, _nodeRegistry, _payerRegistry));
+
+        _manager = PayerReportManagerHarness(address(new Proxy(_implementation)));
+
+        _manager.initialize();
+    }
+
+    /* ============ constructor ============ */
+
+    function test_constructor_zeroParameterRegistry() external {
+        vm.expectRevert(IPayerReportManager.ZeroParameterRegistry.selector);
+
+        new PayerReportManagerHarness(address(0), address(0), address(0));
+    }
+
+    function test_constructor_zeroNodeRegistry() external {
+        vm.expectRevert(IPayerReportManager.ZeroNodeRegistry.selector);
+
+        new PayerReportManagerHarness(_parameterRegistry, address(0), address(0));
+    }
+
+    function test_constructor_zeroPayerRegistry() external {
+        vm.expectRevert(IPayerReportManager.ZeroPayerRegistry.selector);
+
+        new PayerReportManagerHarness(_parameterRegistry, _nodeRegistry, address(0));
+    }
+
+    /* ============ initial state ============ */
+
+    function test_initialState() external view {
+        assertEq(_getImplementationFromSlot(address(_manager)), _implementation);
+        assertEq(_manager.implementation(), _implementation);
+        assertEq(_manager.parameterRegistry(), _parameterRegistry);
+        assertEq(_manager.nodeRegistry(), _nodeRegistry);
+        assertEq(_manager.payerRegistry(), _payerRegistry);
+        assertEq(_manager.migratorParameterKey(), _MIGRATOR_KEY);
+    }
+
+    /* ============ initializer ============ */
+
+    function test_initialize_reinitialization() external {
+        vm.expectRevert(Initializable.InvalidInitialization.selector);
+        _manager.initialize();
+    }
+
+    /* ============ submit ============ */
+
+    function test_submit_invalidStartSequenceId() external {
+        vm.expectRevert(abi.encodeWithSelector(IPayerReportManager.InvalidStartSequenceId.selector, 0, 1));
+        _manager.submit({
+            originatorNodeId_: 0,
+            attesterNodeId_: 0,
+            startSequenceId_: 0,
+            endSequenceId_: 0,
+            payersMerkleRoot_: bytes32(uint256(0)),
+            nodeOperators_: new address[](0),
+            signatures_: new IPayerReportManager.PayerReportSignature[](0)
+        });
+    }
+
+    function test_submit_invalidSequenceIds() external {
+        vm.expectRevert(IPayerReportManager.InvalidSequenceIds.selector);
+        _manager.submit({
+            originatorNodeId_: 0,
+            attesterNodeId_: 0,
+            startSequenceId_: 1,
+            endSequenceId_: 0,
+            payersMerkleRoot_: bytes32(uint256(0)),
+            nodeOperators_: new address[](0),
+            signatures_: new IPayerReportManager.PayerReportSignature[](0)
+        });
+    }
+
+    function test_submit_unorderedNodeIds() external {
+        IPayerReportManager.PayerReportSignature[] memory signatures_ = new IPayerReportManager.PayerReportSignature[](
+            2
+        );
+
+        signatures_[0] = IPayerReportManager.PayerReportSignature({ nodeId: 1, signature: "" });
+        signatures_[1] = IPayerReportManager.PayerReportSignature({ nodeId: 0, signature: "" });
+
+        vm.expectRevert(IPayerReportManager.UnorderedNodeIds.selector);
+        _manager.submit({
+            originatorNodeId_: 0,
+            attesterNodeId_: 0,
+            startSequenceId_: 1,
+            endSequenceId_: 1,
+            payersMerkleRoot_: bytes32(uint256(0)),
+            nodeOperators_: new address[](0),
+            signatures_: signatures_
+        });
+    }
+
+    function test_submit_insufficientSignatures() external {
+        IPayerReportManager.PayerReportSignature[] memory signatures_ = new IPayerReportManager.PayerReportSignature[](
+            1
+        );
+
+        bytes memory signature_ = _getPayerReportSignature(
+            0,
+            0,
+            1,
+            1,
+            bytes32(uint256(0)),
+            new address[](0),
+            _nodeOperator1Pk
+        );
+
+        signatures_[0] = IPayerReportManager.PayerReportSignature({ nodeId: 1, signature: signature_ });
+
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("getIsCanonicalNode(uint256)", 1), abi.encode(true));
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("ownerOf(uint256)", 1), abi.encode(_nodeOperator1));
+        vm.mockCall(_nodeRegistry, abi.encodeWithSignature("canonicalNodesCount()"), abi.encode(3));
+
+        vm.expectRevert(abi.encodeWithSelector(IPayerReportManager.InsufficientSignatures.selector, 1, 2));
+        _manager.submit({
+            originatorNodeId_: 0,
+            attesterNodeId_: 0,
+            startSequenceId_: 1,
+            endSequenceId_: 1,
+            payersMerkleRoot_: bytes32(uint256(0)),
+            nodeOperators_: new address[](0),
+            signatures_: signatures_
+        });
+    }
+
+    function test_submit() external {
+        _manager.__pushPayerReport({
+            originatorNodeId_: 0,
+            attesterNodeId_: 0,
+            startSequenceId_: 1,
+            endSequenceId_: 1,
+            feesSettled_: 0,
+            offset_: 0,
+            isSettled_: false,
+            payersMerkleRoot_: bytes32(uint256(0)),
+            nodeOperators_: new address[](0)
+        });
+
+        IPayerReportManager.PayerReportSignature[] memory signatures_ = new IPayerReportManager.PayerReportSignature[](
+            3
+        );
+
+        signatures_[0] = IPayerReportManager.PayerReportSignature({
+            nodeId: 1,
+            signature: _getPayerReportSignature(0, 0, 2, 2, bytes32(uint256(0)), new address[](0), _nodeOperator1Pk)
+        });
+
+        signatures_[1] = IPayerReportManager.PayerReportSignature({
+            nodeId: 2,
+            signature: _getPayerReportSignature(0, 0, 2, 2, bytes32(uint256(0)), new address[](0), _nodeOperator2Pk)
+        });
+
+        signatures_[2] = IPayerReportManager.PayerReportSignature({
+            nodeId: 3,
+            signature: _getPayerReportSignature(0, 0, 2, 2, bytes32(uint256(0)), new address[](0), _nodeOperator3Pk)
+        });
+
path: test/unit/PayerReportManager.t.sol
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Test passes despite hidden bug – please extend assertions**

The happy-path `test_submit` currently succeeds even though the implementation writes past array bounds (see critical issue above). Add a call to `signingNodeIds.length` in the emitted event or an external call to `getPayerReport` to catch the overflow.

```solidity
// after submit() add:
IPayerReportManager.PayerReport memory pr = _manager.getPayerReport(0, 1);
assertEq(pr.nodeOperators.length, 0);
```

This will start failing once the overflow manifests, turning the unit test into a regression guard.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-06T08:35:50Z
author_association: NONE
diff_hunk: @@ -0,0 +1,51 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { IERC5267 } from "./interfaces/IERC5267.sol";
+
+import { ERC712 } from "./ERC721.sol";
path: src/abstract/ERC5267.sol
user: login: deluca-mike
body: Good catch!
created_at: 2025-05-06T08:39:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,51 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { IERC5267 } from "./interfaces/IERC5267.sol";
+
+import { ERC712 } from "./ERC721.sol";
path: src/abstract/ERC5267.sol
user: login: coderabbitai[bot]
body: @deluca-mike You're welcome! Happy to help. Let me know if you need anything else while reviewing this PR.

---

<!-- This is an auto-generated comment: tweet message by coderabbit.ai -->

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=%40coderabbitai%20spotted%20a%20critical%20import%20typo%3A%20importing%20ERC712%20from%20ERC721.sol%20is%20incorrect!%20Fixing%20this%20prevents%20subtle%20bugs%20in%20EIP-712%20signing.%20Try%20%40coderabbitai%20for%20sharp%2C%20precise%20code%20reviews!%20%F0%9F%90%87%F0%9F%94%8D&url=https%3A%2F%2Fgithub.com%2Fxmtp%2Fsmart-contracts%2Fpull%2F56%23discussion_r2074994378)?

Thank you for using CodeRabbit!

<!-- end of auto-generated comment: tweet message by coderabbit.ai -->




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@deluca-mike You're welcome! Happy to help. Let me know if you need anything else while reviewing this PR.

---

&lt;!-- This is an auto-generated comment: tweet message by coderabbit.ai --&gt;

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=%40coderabbitai%20spotted%20a%20critical%20import%20typo%3A%20importing%20ERC712%20from%20ERC721.sol%20is%20incorrect!%20Fixing%20this%20prevents%20subtle%20bugs%20in%20EIP-712%20signing.%20Try%20%40coderabbitai%20for%20sharp%2C%20precise%20code%20reviews!%20%F0%9F%90%87%F0%9F%94%8D&url=https%3A%2F%2Fgithub.com%2Fxmtp%2Fsmart-contracts%2Fpull%2F56%23discussion_r2074994378)?

Thank you for using CodeRabbit!

&lt;!-- end of auto-generated comment: tweet message by coderabbit.ai --&gt;

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-06T08:39:25Z
author_association: NONE
diff_hunk: @@ -0,0 +1,375 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { ECDSA } from "../../lib/solady/src/utils/ECDSA.sol";
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../libraries/SequentialMerkleProofs.sol";
+
+import { IMigratable } from "../abstract/interfaces/IMigratable.sol";
+import { IParameterRegistryLike, INodeRegistryLike, IPayerRegistryLike } from "./interfaces/External.sol";
+import { IPayerReportManager } from "./interfaces/IPayerReportManager.sol";
+
+import { ERC5267 } from "../abstract/ERC5267.sol";
+import { Migratable } from "../abstract/Migratable.sol";
+
+// TODO: If a node operator owns more than one node, their signature for a payer report will be identical, and therefore
+//       replayable across their nodes. This may not be ideal, so it might be necessary to include the node id of the
+//       signer in the digest.
+
+/**
+ * @title  Implementation of the Payer Report Manager.
+ * @notice This contract handles functionality for submitting and settling payer reports.
+ */
+contract PayerReportManager is IPayerReportManager, Initializable, Migratable, ERC5267 {
+    /* ============ Constants/Immutables ============ */
+
+    // solhint-disable-next-line max-line-length
+    /// @dev keccak256("PayerReport(uint256 attesterNodeId,uint256 originatorNodeId,uint256 startSequenceId,uint256 endSequenceId,bytes32 payersMerkleRoot,address[] nodeOperators)")
+    bytes32 public constant PAYER_REPORT_TYPEHASH = 0x73ce31d3128c9ba6af04b3270a898f0abafdf9bed84e47ec7ebd2f3dd8e9c8a5;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable parameterRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable nodeRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable payerRegistry;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.PayerReportManager
+     * @notice The UUPS storage for the payer report manager.
+     * @param  payerReportsByOriginator The mapping of arrays of payer reports by originator node ID.
+     */
+    struct PayerReportManagerStorage {
+        mapping(uint32 originatorId => PayerReport[] payerReports) payerReportsByOriginator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.PayerReportManager")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _PAYER_REPORT_MANAGER_STORAGE_LOCATION =
+        0x26b057ee8e4d60685198828fdf1c618ab8e36b0ab85f54a47b18319f6f718e00;
+
+    function _getPayerReportManagerStorage() internal pure returns (PayerReportManagerStorage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _PAYER_REPORT_MANAGER_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructor for the implementation contract, such that the implementation cannot be initialized.
+     * @param  parameterRegistry_ The address of the parameter registry.
+     * @param  nodeRegistry_      The address of the node registry.
+     * @param  payerRegistry_     The address of the payer registry.
+     * @dev    The parameter registry, node registry, and payer registry must not be the zero address.
+     * @dev    The parameter registry, node registry, and payer registry are immutable so that they are inlined in the
+     *         contract code, and have minimal gas cost.
+     */
+    constructor(address parameterRegistry_, address nodeRegistry_, address payerRegistry_) ERC5267() {
+        require(_isNotZero(parameterRegistry = parameterRegistry_), ZeroParameterRegistry());
+        require(_isNotZero(nodeRegistry = nodeRegistry_), ZeroNodeRegistry());
+        require(_isNotZero(payerRegistry = payerRegistry_), ZeroPayerRegistry());
+
+        _disableInitializers();
+    }
+
+    /* ============ Initialization ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function initialize() public initializer {
+        _initializeERC5267();
+    }
+
+    /* ============ Interactive Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function submit(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) external returns (uint256 payerReportIndex_) {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        payerReportIndex_ = payerReports_.length;
+
+        uint32 lastSequenceId_ = payerReportIndex_ > 0 ? payerReports_[payerReportIndex_ - 1].endSequenceId : 0;
+
+        // Enforces that the start sequence ID is the next expected sequence ID.
+        if (startSequenceId_ != lastSequenceId_ + 1) {
+            revert InvalidStartSequenceId(startSequenceId_, lastSequenceId_ + 1);
+        }
+
+        // Enforces that the end sequence ID is greater than or equal to the start sequence ID.
+        if (endSequenceId_ < startSequenceId_) revert InvalidSequenceIds();
+
+        // Verifies the signatures and gets the array of valid signing node IDs.
+        uint32[] memory validSigningNodeIds_ = _verifySignatures({
+            originatorNodeId_: originatorNodeId_,
+            attesterNodeId_: attesterNodeId_,
+            startSequenceId_: startSequenceId_,
+            endSequenceId_: endSequenceId_,
+            payersMerkleRoot_: payersMerkleRoot_,
+            nodeOperators_: nodeOperators_,
+            signatures_: signatures_
+        });
+
+        payerReports_.push(
+            PayerReport({
+                attesterNodeId: attesterNodeId_,
+                startSequenceId: startSequenceId_,
+                endSequenceId: endSequenceId_,
+                feesSettled: 0,
+                offset: 0,
+                isSettled: false,
+                payersMerkleRoot: payersMerkleRoot_,
+                nodeOperators: nodeOperators_
+            })
+        );
+
+        emit PayerReportSubmitted({
+            originatorNodeId: originatorNodeId_,
+            payerReportIndex: payerReportIndex_,
+            attesterNodeId: attesterNodeId_,
+            startSequenceId: startSequenceId_,
+            endSequenceId: endSequenceId_,
+            payersMerkleRoot: payersMerkleRoot_,
+            nodeOperators: nodeOperators_,
+            signingNodeIds: validSigningNodeIds_
+        });
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function settle(
+        uint32 originatorNodeId_,
+        uint32 payerReportIndex_,
+        bytes[] calldata payerFees_,
+        bytes32[] calldata proofElements_
+    ) external {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        if (payerReportIndex_ >= payerReports_.length) revert PayerReportIndexOutOfBounds();
+
+        PayerReport storage payerReport_ = payerReports_[payerReportIndex_];
+
+        if (payerReport_.isSettled) revert PayerReportEntirelySettled();
+
+        // Verify the payer fees provided are the next sequential payer fees from the last settled offset.
+        SequentialMerkleProofs.verify(payerReport_.payersMerkleRoot, payerReport_.offset, payerFees_, proofElements_);
+
+        payerReport_.offset += uint32(payerFees_.length);
+
+        uint32 leafCount_ = SequentialMerkleProofs.getLeafCount(proofElements_);
+
+        payerReport_.isSettled = leafCount_ == payerReport_.offset;
+
+        // Low level call which handles passing the `payerFees_` arrays as a bytes array that will be automatically
+        // decoded as the required structs by the payer registry's `settleUsage` function.
+        (bool success_, bytes memory returnData_) = payerRegistry.call(
+            abi.encodeWithSelector(IPayerRegistryLike.settleUsage.selector, payerFees_)
+        );
+
+        if (!success_) revert SettleUsageFailed(returnData_);
+
+        uint96 feesSettled_ = abi.decode(returnData_, (uint96));
+
+        emit PayerReportSubsetSettled(
+            originatorNodeId_,
+            payerReportIndex_,
+            uint32(payerFees_.length),
+            leafCount_ - payerReport_.offset,
+            feesSettled_
+        );
+
+        payerReport_.feesSettled += feesSettled_;
+    }
+
+    /// @inheritdoc IMigratable
+    function migrate() external {
+        _migrate(_toAddress(_getRegistryParameter(migratorParameterKey())));
+    }
+
+    /* ============ View/Pure Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function migratorParameterKey() public pure returns (bytes memory key_) {
+        return "xmtp.payerReportManager.migrator";
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReportDigest(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) external view returns (bytes32 digest_) {
+        return
+            _getPayerReportDigest(
+                originatorNodeId_,
+                attesterNodeId_,
+                startSequenceId_,
+                endSequenceId_,
+                payersMerkleRoot_,
+                nodeOperators_
+            );
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReports(uint32 originatorNodeId_) external view returns (PayerReport[] memory payerReports_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_];
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReport(
+        uint32 originatorNodeId_,
+        uint256 payerReportIndex_
+    ) external view returns (PayerReport memory payerReport_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_][payerReportIndex_];
+    }
+
+    /* ============ Internal View/Pure Functions ============ */
+
+    function _name() internal pure override returns (string memory name_) {
+        return "PayerReportManager";
+    }
+
+    function _version() internal pure override returns (string memory version_) {
+        return "1";
+    }
+
+    /**
+     * @dev    Returns the EIP-712 digest for a payer report.
+     * @param  originatorNodeId_ The id of the node originator.
+     * @param  attesterNodeId_   The id of the node attester.
+     * @param  startSequenceId_  The start sequence ID.
+     * @param  endSequenceId_    The end sequence ID.
+     * @param  payersMerkleRoot_ The payers merkle root.
+     * @param  nodeOperators_    The active node operators during the reporting period.
+     * @return digest_           The EIP-712 digest.
+     */
+    function _getPayerReportDigest(
+        uint256 originatorNodeId_,
+        uint256 attesterNodeId_,
+        uint256 startSequenceId_,
+        uint256 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) internal view returns (bytes32 digest_) {
+        return
+            _getDigest(
+                keccak256(
+                    abi.encode(
+                        PAYER_REPORT_TYPEHASH,
+                        originatorNodeId_,
+                        attesterNodeId_,
+                        startSequenceId_,
+                        endSequenceId_,
+                        payersMerkleRoot_,
+                        keccak256(abi.encodePacked(nodeOperators_))
+                    )
+                )
+            );
+    }
+
+    function _getRegistryParameter(bytes memory key_) internal view returns (bytes32 value_) {
+        return IParameterRegistryLike(parameterRegistry).get(key_);
+    }
+
+    function _isNotZero(address input_) internal pure returns (bool isNotZero_) {
+        return input_ != address(0);
+    }
+
+    function _toAddress(bytes32 value_) internal pure returns (address address_) {
+        // slither-disable-next-line assembly
+        assembly {
+            address_ := value_
+        }
+    }
+
+    /**
+     * @dev Verifies that enough of the signatures are valid and provided by canonical node operators and returns the
+     *      array of valid signing node IDs.
+     */
+    function _verifySignatures(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) internal view returns (uint32[] memory validSigningNodeIds_) {
+        bytes32 digest_ = _getPayerReportDigest(
+            originatorNodeId_,
+            attesterNodeId_,
+            startSequenceId_,
+            endSequenceId_,
+            payersMerkleRoot_,
+            nodeOperators_
+        );
+
+        // Need to determine how many of the signatures are valid and provided by canonical node operators.
+        uint8 validSignatureCount_;
+
+        // This array will help in constructing the `validSigningNodeIds_` array, who's length may be less than
+        // the length of the `signatures_` array if some of the signatures are invalid.
+        bool[] memory isValid_ = new bool[](signatures_.length);
+
+        for (uint256 index_; index_ < signatures_.length; ++index_) {
+            uint32 nodeId_ = signatures_[index_].nodeId;
path: src/settlement-chain/PayerReportManager.sol
user: login: deluca-mike
body: While this is a good suggestion, since the protocol currently limits canonical nodes to a max that can fit in a `uint8`, I will leave it up to the `++validSignatureCount_` line to overflow with an error in the event that there are somehow more valid signatures than canonical nodes (which should never be possible).
created_at: 2025-05-06T08:41:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,375 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { ECDSA } from "../../lib/solady/src/utils/ECDSA.sol";
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../libraries/SequentialMerkleProofs.sol";
+
+import { IMigratable } from "../abstract/interfaces/IMigratable.sol";
+import { IParameterRegistryLike, INodeRegistryLike, IPayerRegistryLike } from "./interfaces/External.sol";
+import { IPayerReportManager } from "./interfaces/IPayerReportManager.sol";
+
+import { ERC5267 } from "../abstract/ERC5267.sol";
+import { Migratable } from "../abstract/Migratable.sol";
+
+// TODO: If a node operator owns more than one node, their signature for a payer report will be identical, and therefore
+//       replayable across their nodes. This may not be ideal, so it might be necessary to include the node id of the
+//       signer in the digest.
+
+/**
+ * @title  Implementation of the Payer Report Manager.
+ * @notice This contract handles functionality for submitting and settling payer reports.
+ */
+contract PayerReportManager is IPayerReportManager, Initializable, Migratable, ERC5267 {
+    /* ============ Constants/Immutables ============ */
+
+    // solhint-disable-next-line max-line-length
+    /// @dev keccak256("PayerReport(uint256 attesterNodeId,uint256 originatorNodeId,uint256 startSequenceId,uint256 endSequenceId,bytes32 payersMerkleRoot,address[] nodeOperators)")
+    bytes32 public constant PAYER_REPORT_TYPEHASH = 0x73ce31d3128c9ba6af04b3270a898f0abafdf9bed84e47ec7ebd2f3dd8e9c8a5;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable parameterRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable nodeRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable payerRegistry;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.PayerReportManager
+     * @notice The UUPS storage for the payer report manager.
+     * @param  payerReportsByOriginator The mapping of arrays of payer reports by originator node ID.
+     */
+    struct PayerReportManagerStorage {
+        mapping(uint32 originatorId => PayerReport[] payerReports) payerReportsByOriginator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.PayerReportManager")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _PAYER_REPORT_MANAGER_STORAGE_LOCATION =
+        0x26b057ee8e4d60685198828fdf1c618ab8e36b0ab85f54a47b18319f6f718e00;
+
+    function _getPayerReportManagerStorage() internal pure returns (PayerReportManagerStorage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _PAYER_REPORT_MANAGER_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructor for the implementation contract, such that the implementation cannot be initialized.
+     * @param  parameterRegistry_ The address of the parameter registry.
+     * @param  nodeRegistry_      The address of the node registry.
+     * @param  payerRegistry_     The address of the payer registry.
+     * @dev    The parameter registry, node registry, and payer registry must not be the zero address.
+     * @dev    The parameter registry, node registry, and payer registry are immutable so that they are inlined in the
+     *         contract code, and have minimal gas cost.
+     */
+    constructor(address parameterRegistry_, address nodeRegistry_, address payerRegistry_) ERC5267() {
+        require(_isNotZero(parameterRegistry = parameterRegistry_), ZeroParameterRegistry());
+        require(_isNotZero(nodeRegistry = nodeRegistry_), ZeroNodeRegistry());
+        require(_isNotZero(payerRegistry = payerRegistry_), ZeroPayerRegistry());
+
+        _disableInitializers();
+    }
+
+    /* ============ Initialization ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function initialize() public initializer {
+        _initializeERC5267();
+    }
+
+    /* ============ Interactive Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function submit(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) external returns (uint256 payerReportIndex_) {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        payerReportIndex_ = payerReports_.length;
+
+        uint32 lastSequenceId_ = payerReportIndex_ > 0 ? payerReports_[payerReportIndex_ - 1].endSequenceId : 0;
+
+        // Enforces that the start sequence ID is the next expected sequence ID.
+        if (startSequenceId_ != lastSequenceId_ + 1) {
+            revert InvalidStartSequenceId(startSequenceId_, lastSequenceId_ + 1);
+        }
+
+        // Enforces that the end sequence ID is greater than or equal to the start sequence ID.
+        if (endSequenceId_ < startSequenceId_) revert InvalidSequenceIds();
+
+        // Verifies the signatures and gets the array of valid signing node IDs.
+        uint32[] memory validSigningNodeIds_ = _verifySignatures({
+            originatorNodeId_: originatorNodeId_,
+            attesterNodeId_: attesterNodeId_,
+            startSequenceId_: startSequenceId_,
+            endSequenceId_: endSequenceId_,
+            payersMerkleRoot_: payersMerkleRoot_,
+            nodeOperators_: nodeOperators_,
+            signatures_: signatures_
+        });
+
+        payerReports_.push(
+            PayerReport({
+                attesterNodeId: attesterNodeId_,
+                startSequenceId: startSequenceId_,
+                endSequenceId: endSequenceId_,
+                feesSettled: 0,
+                offset: 0,
+                isSettled: false,
+                payersMerkleRoot: payersMerkleRoot_,
+                nodeOperators: nodeOperators_
+            })
+        );
+
+        emit PayerReportSubmitted({
+            originatorNodeId: originatorNodeId_,
+            payerReportIndex: payerReportIndex_,
+            attesterNodeId: attesterNodeId_,
+            startSequenceId: startSequenceId_,
+            endSequenceId: endSequenceId_,
+            payersMerkleRoot: payersMerkleRoot_,
+            nodeOperators: nodeOperators_,
+            signingNodeIds: validSigningNodeIds_
+        });
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function settle(
+        uint32 originatorNodeId_,
+        uint32 payerReportIndex_,
+        bytes[] calldata payerFees_,
+        bytes32[] calldata proofElements_
+    ) external {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        if (payerReportIndex_ >= payerReports_.length) revert PayerReportIndexOutOfBounds();
+
+        PayerReport storage payerReport_ = payerReports_[payerReportIndex_];
+
+        if (payerReport_.isSettled) revert PayerReportEntirelySettled();
+
+        // Verify the payer fees provided are the next sequential payer fees from the last settled offset.
+        SequentialMerkleProofs.verify(payerReport_.payersMerkleRoot, payerReport_.offset, payerFees_, proofElements_);
+
+        payerReport_.offset += uint32(payerFees_.length);
+
+        uint32 leafCount_ = SequentialMerkleProofs.getLeafCount(proofElements_);
+
+        payerReport_.isSettled = leafCount_ == payerReport_.offset;
+
+        // Low level call which handles passing the `payerFees_` arrays as a bytes array that will be automatically
+        // decoded as the required structs by the payer registry's `settleUsage` function.
+        (bool success_, bytes memory returnData_) = payerRegistry.call(
+            abi.encodeWithSelector(IPayerRegistryLike.settleUsage.selector, payerFees_)
+        );
+
+        if (!success_) revert SettleUsageFailed(returnData_);
+
+        uint96 feesSettled_ = abi.decode(returnData_, (uint96));
+
+        emit PayerReportSubsetSettled(
+            originatorNodeId_,
+            payerReportIndex_,
+            uint32(payerFees_.length),
+            leafCount_ - payerReport_.offset,
+            feesSettled_
+        );
+
+        payerReport_.feesSettled += feesSettled_;
+    }
+
+    /// @inheritdoc IMigratable
+    function migrate() external {
+        _migrate(_toAddress(_getRegistryParameter(migratorParameterKey())));
+    }
+
+    /* ============ View/Pure Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function migratorParameterKey() public pure returns (bytes memory key_) {
+        return "xmtp.payerReportManager.migrator";
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReportDigest(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) external view returns (bytes32 digest_) {
+        return
+            _getPayerReportDigest(
+                originatorNodeId_,
+                attesterNodeId_,
+                startSequenceId_,
+                endSequenceId_,
+                payersMerkleRoot_,
+                nodeOperators_
+            );
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReports(uint32 originatorNodeId_) external view returns (PayerReport[] memory payerReports_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_];
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReport(
+        uint32 originatorNodeId_,
+        uint256 payerReportIndex_
+    ) external view returns (PayerReport memory payerReport_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_][payerReportIndex_];
+    }
+
+    /* ============ Internal View/Pure Functions ============ */
+
+    function _name() internal pure override returns (string memory name_) {
+        return "PayerReportManager";
+    }
+
+    function _version() internal pure override returns (string memory version_) {
+        return "1";
+    }
+
+    /**
+     * @dev    Returns the EIP-712 digest for a payer report.
+     * @param  originatorNodeId_ The id of the node originator.
+     * @param  attesterNodeId_   The id of the node attester.
+     * @param  startSequenceId_  The start sequence ID.
+     * @param  endSequenceId_    The end sequence ID.
+     * @param  payersMerkleRoot_ The payers merkle root.
+     * @param  nodeOperators_    The active node operators during the reporting period.
+     * @return digest_           The EIP-712 digest.
+     */
+    function _getPayerReportDigest(
+        uint256 originatorNodeId_,
+        uint256 attesterNodeId_,
+        uint256 startSequenceId_,
+        uint256 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) internal view returns (bytes32 digest_) {
+        return
+            _getDigest(
+                keccak256(
+                    abi.encode(
+                        PAYER_REPORT_TYPEHASH,
+                        originatorNodeId_,
+                        attesterNodeId_,
+                        startSequenceId_,
+                        endSequenceId_,
+                        payersMerkleRoot_,
+                        keccak256(abi.encodePacked(nodeOperators_))
+                    )
+                )
+            );
+    }
+
+    function _getRegistryParameter(bytes memory key_) internal view returns (bytes32 value_) {
+        return IParameterRegistryLike(parameterRegistry).get(key_);
+    }
+
+    function _isNotZero(address input_) internal pure returns (bool isNotZero_) {
+        return input_ != address(0);
+    }
+
+    function _toAddress(bytes32 value_) internal pure returns (address address_) {
+        // slither-disable-next-line assembly
+        assembly {
+            address_ := value_
+        }
+    }
+
+    /**
+     * @dev Verifies that enough of the signatures are valid and provided by canonical node operators and returns the
+     *      array of valid signing node IDs.
+     */
+    function _verifySignatures(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) internal view returns (uint32[] memory validSigningNodeIds_) {
+        bytes32 digest_ = _getPayerReportDigest(
+            originatorNodeId_,
+            attesterNodeId_,
+            startSequenceId_,
+            endSequenceId_,
+            payersMerkleRoot_,
+            nodeOperators_
+        );
+
+        // Need to determine how many of the signatures are valid and provided by canonical node operators.
+        uint8 validSignatureCount_;
+
+        // This array will help in constructing the `validSigningNodeIds_` array, who's length may be less than
+        // the length of the `signatures_` array if some of the signatures are invalid.
+        bool[] memory isValid_ = new bool[](signatures_.length);
+
+        for (uint256 index_; index_ < signatures_.length; ++index_) {
+            uint32 nodeId_ = signatures_[index_].nodeId;
path: src/settlement-chain/PayerReportManager.sol
user: login: coderabbitai[bot]
body: @deluca-mike That's a perfectly reasonable approach. Using the `uint8` type is appropriate since it aligns with the protocol's canonical node constraints, and Solidity 0.8.x's built-in overflow checking will indeed cause a revert if something unexpected happens. Thanks for explaining your rationale!

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: deluca-mike
PR: xmtp/smart-contracts#56
File: src/settlement-chain/PayerReportManager.sol:325-333
Timestamp: 2025-05-06T08:42:22.880Z
Learning: The XMTP protocol limits canonical nodes to a maximum that can fit in a `uint8` (255 nodes), which is why the `validSignatureCount_` variable in `PayerReportManager` is typed as `uint8`.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@deluca-mike That's a perfectly reasonable approach. Using the `uint8` type is appropriate since it aligns with the protocol's canonical node constraints, and Solidity 0.8.x's built-in overflow checking will indeed cause a revert if something unexpected happens. Thanks for explaining your rationale!

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: deluca-mike
PR: xmtp/smart-contracts#56
File: src/settlement-chain/PayerReportManager.sol:325-333
Timestamp: 2025-05-06T08:42:22.880Z
Learning: The XMTP protocol limits canonical nodes to a maximum that can fit in a `uint8` (255 nodes), which is why the `validSignatureCount_` variable in `PayerReportManager` is typed as `uint8`.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-06T08:42:25Z
author_association: NONE
diff_hunk: @@ -0,0 +1,375 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { ECDSA } from "../../lib/solady/src/utils/ECDSA.sol";
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../libraries/SequentialMerkleProofs.sol";
+
+import { IMigratable } from "../abstract/interfaces/IMigratable.sol";
+import { IParameterRegistryLike, INodeRegistryLike, IPayerRegistryLike } from "./interfaces/External.sol";
+import { IPayerReportManager } from "./interfaces/IPayerReportManager.sol";
+
+import { ERC5267 } from "../abstract/ERC5267.sol";
+import { Migratable } from "../abstract/Migratable.sol";
+
+// TODO: If a node operator owns more than one node, their signature for a payer report will be identical, and therefore
+//       replayable across their nodes. This may not be ideal, so it might be necessary to include the node id of the
+//       signer in the digest.
+
+/**
+ * @title  Implementation of the Payer Report Manager.
+ * @notice This contract handles functionality for submitting and settling payer reports.
+ */
+contract PayerReportManager is IPayerReportManager, Initializable, Migratable, ERC5267 {
+    /* ============ Constants/Immutables ============ */
+
+    // solhint-disable-next-line max-line-length
+    /// @dev keccak256("PayerReport(uint256 attesterNodeId,uint256 originatorNodeId,uint256 startSequenceId,uint256 endSequenceId,bytes32 payersMerkleRoot,address[] nodeOperators)")
+    bytes32 public constant PAYER_REPORT_TYPEHASH = 0x73ce31d3128c9ba6af04b3270a898f0abafdf9bed84e47ec7ebd2f3dd8e9c8a5;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable parameterRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable nodeRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable payerRegistry;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.PayerReportManager
+     * @notice The UUPS storage for the payer report manager.
+     * @param  payerReportsByOriginator The mapping of arrays of payer reports by originator node ID.
+     */
+    struct PayerReportManagerStorage {
+        mapping(uint32 originatorId => PayerReport[] payerReports) payerReportsByOriginator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.PayerReportManager")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _PAYER_REPORT_MANAGER_STORAGE_LOCATION =
+        0x26b057ee8e4d60685198828fdf1c618ab8e36b0ab85f54a47b18319f6f718e00;
+
+    function _getPayerReportManagerStorage() internal pure returns (PayerReportManagerStorage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _PAYER_REPORT_MANAGER_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructor for the implementation contract, such that the implementation cannot be initialized.
+     * @param  parameterRegistry_ The address of the parameter registry.
+     * @param  nodeRegistry_      The address of the node registry.
+     * @param  payerRegistry_     The address of the payer registry.
+     * @dev    The parameter registry, node registry, and payer registry must not be the zero address.
+     * @dev    The parameter registry, node registry, and payer registry are immutable so that they are inlined in the
+     *         contract code, and have minimal gas cost.
+     */
+    constructor(address parameterRegistry_, address nodeRegistry_, address payerRegistry_) ERC5267() {
+        require(_isNotZero(parameterRegistry = parameterRegistry_), ZeroParameterRegistry());
+        require(_isNotZero(nodeRegistry = nodeRegistry_), ZeroNodeRegistry());
+        require(_isNotZero(payerRegistry = payerRegistry_), ZeroPayerRegistry());
+
+        _disableInitializers();
+    }
+
+    /* ============ Initialization ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function initialize() public initializer {
+        _initializeERC5267();
+    }
+
+    /* ============ Interactive Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function submit(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) external returns (uint256 payerReportIndex_) {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        payerReportIndex_ = payerReports_.length;
+
+        uint32 lastSequenceId_ = payerReportIndex_ > 0 ? payerReports_[payerReportIndex_ - 1].endSequenceId : 0;
+
+        // Enforces that the start sequence ID is the next expected sequence ID.
+        if (startSequenceId_ != lastSequenceId_ + 1) {
+            revert InvalidStartSequenceId(startSequenceId_, lastSequenceId_ + 1);
+        }
+
+        // Enforces that the end sequence ID is greater than or equal to the start sequence ID.
+        if (endSequenceId_ < startSequenceId_) revert InvalidSequenceIds();
+
+        // Verifies the signatures and gets the array of valid signing node IDs.
+        uint32[] memory validSigningNodeIds_ = _verifySignatures({
+            originatorNodeId_: originatorNodeId_,
+            attesterNodeId_: attesterNodeId_,
+            startSequenceId_: startSequenceId_,
+            endSequenceId_: endSequenceId_,
+            payersMerkleRoot_: payersMerkleRoot_,
+            nodeOperators_: nodeOperators_,
+            signatures_: signatures_
+        });
+
+        payerReports_.push(
+            PayerReport({
+                attesterNodeId: attesterNodeId_,
+                startSequenceId: startSequenceId_,
+                endSequenceId: endSequenceId_,
+                feesSettled: 0,
+                offset: 0,
+                isSettled: false,
+                payersMerkleRoot: payersMerkleRoot_,
+                nodeOperators: nodeOperators_
+            })
+        );
+
+        emit PayerReportSubmitted({
+            originatorNodeId: originatorNodeId_,
+            payerReportIndex: payerReportIndex_,
+            attesterNodeId: attesterNodeId_,
+            startSequenceId: startSequenceId_,
+            endSequenceId: endSequenceId_,
+            payersMerkleRoot: payersMerkleRoot_,
+            nodeOperators: nodeOperators_,
+            signingNodeIds: validSigningNodeIds_
+        });
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function settle(
+        uint32 originatorNodeId_,
+        uint32 payerReportIndex_,
+        bytes[] calldata payerFees_,
+        bytes32[] calldata proofElements_
+    ) external {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        if (payerReportIndex_ >= payerReports_.length) revert PayerReportIndexOutOfBounds();
+
+        PayerReport storage payerReport_ = payerReports_[payerReportIndex_];
+
+        if (payerReport_.isSettled) revert PayerReportEntirelySettled();
+
+        // Verify the payer fees provided are the next sequential payer fees from the last settled offset.
+        SequentialMerkleProofs.verify(payerReport_.payersMerkleRoot, payerReport_.offset, payerFees_, proofElements_);
+
+        payerReport_.offset += uint32(payerFees_.length);
+
+        uint32 leafCount_ = SequentialMerkleProofs.getLeafCount(proofElements_);
+
+        payerReport_.isSettled = leafCount_ == payerReport_.offset;
+
+        // Low level call which handles passing the `payerFees_` arrays as a bytes array that will be automatically
+        // decoded as the required structs by the payer registry's `settleUsage` function.
+        (bool success_, bytes memory returnData_) = payerRegistry.call(
+            abi.encodeWithSelector(IPayerRegistryLike.settleUsage.selector, payerFees_)
+        );
+
+        if (!success_) revert SettleUsageFailed(returnData_);
+
+        uint96 feesSettled_ = abi.decode(returnData_, (uint96));
+
+        emit PayerReportSubsetSettled(
+            originatorNodeId_,
+            payerReportIndex_,
+            uint32(payerFees_.length),
+            leafCount_ - payerReport_.offset,
+            feesSettled_
+        );
+
+        payerReport_.feesSettled += feesSettled_;
+    }
+
+    /// @inheritdoc IMigratable
+    function migrate() external {
+        _migrate(_toAddress(_getRegistryParameter(migratorParameterKey())));
+    }
+
+    /* ============ View/Pure Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function migratorParameterKey() public pure returns (bytes memory key_) {
+        return "xmtp.payerReportManager.migrator";
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReportDigest(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) external view returns (bytes32 digest_) {
+        return
+            _getPayerReportDigest(
+                originatorNodeId_,
+                attesterNodeId_,
+                startSequenceId_,
+                endSequenceId_,
+                payersMerkleRoot_,
+                nodeOperators_
+            );
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReports(uint32 originatorNodeId_) external view returns (PayerReport[] memory payerReports_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_];
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReport(
+        uint32 originatorNodeId_,
+        uint256 payerReportIndex_
+    ) external view returns (PayerReport memory payerReport_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_][payerReportIndex_];
+    }
+
+    /* ============ Internal View/Pure Functions ============ */
+
+    function _name() internal pure override returns (string memory name_) {
+        return "PayerReportManager";
+    }
+
+    function _version() internal pure override returns (string memory version_) {
+        return "1";
+    }
+
+    /**
+     * @dev    Returns the EIP-712 digest for a payer report.
+     * @param  originatorNodeId_ The id of the node originator.
+     * @param  attesterNodeId_   The id of the node attester.
+     * @param  startSequenceId_  The start sequence ID.
+     * @param  endSequenceId_    The end sequence ID.
+     * @param  payersMerkleRoot_ The payers merkle root.
+     * @param  nodeOperators_    The active node operators during the reporting period.
+     * @return digest_           The EIP-712 digest.
+     */
+    function _getPayerReportDigest(
+        uint256 originatorNodeId_,
+        uint256 attesterNodeId_,
+        uint256 startSequenceId_,
+        uint256 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) internal view returns (bytes32 digest_) {
+        return
+            _getDigest(
+                keccak256(
+                    abi.encode(
+                        PAYER_REPORT_TYPEHASH,
+                        originatorNodeId_,
+                        attesterNodeId_,
+                        startSequenceId_,
+                        endSequenceId_,
+                        payersMerkleRoot_,
+                        keccak256(abi.encodePacked(nodeOperators_))
+                    )
+                )
+            );
+    }
+
+    function _getRegistryParameter(bytes memory key_) internal view returns (bytes32 value_) {
+        return IParameterRegistryLike(parameterRegistry).get(key_);
+    }
+
+    function _isNotZero(address input_) internal pure returns (bool isNotZero_) {
+        return input_ != address(0);
+    }
+
+    function _toAddress(bytes32 value_) internal pure returns (address address_) {
+        // slither-disable-next-line assembly
+        assembly {
+            address_ := value_
+        }
+    }
+
+    /**
+     * @dev Verifies that enough of the signatures are valid and provided by canonical node operators and returns the
+     *      array of valid signing node IDs.
+     */
+    function _verifySignatures(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) internal view returns (uint32[] memory validSigningNodeIds_) {
+        bytes32 digest_ = _getPayerReportDigest(
+            originatorNodeId_,
+            attesterNodeId_,
+            startSequenceId_,
+            endSequenceId_,
+            payersMerkleRoot_,
+            nodeOperators_
+        );
+
+        // Need to determine how many of the signatures are valid and provided by canonical node operators.
+        uint8 validSignatureCount_;
+
+        // This array will help in constructing the `validSigningNodeIds_` array, who's length may be less than
+        // the length of the `signatures_` array if some of the signatures are invalid.
+        bool[] memory isValid_ = new bool[](signatures_.length);
+
+        for (uint256 index_; index_ < signatures_.length; ++index_) {
+            uint32 nodeId_ = signatures_[index_].nodeId;
+
+            // Enforces that the signing node IDs are ordered and unique.
+            if (index_ != 0 && nodeId_ <= signatures_[index_ - 1].nodeId) revert UnorderedNodeIds();
+
+            // If the signature is invalid, ignore it.
+            if (!(isValid_[index_] = _verifySignature(digest_, nodeId_, signatures_[index_].signature))) continue;
+
+            ++validSignatureCount_;
+        }
+
+        uint8 requiredSignatureCount_ = (INodeRegistryLike(nodeRegistry).canonicalNodesCount() / 2) + 1;
+
+        // Enforces that the number of valid signatures is greater than one more than half of the canonical node count.
+        if (validSignatureCount_ < requiredSignatureCount_) {
+            revert InsufficientSignatures(validSignatureCount_, requiredSignatureCount_);
+        }
+
+        validSigningNodeIds_ = new uint32[](validSignatureCount_);
+
+        for (uint256 index_; index_ < isValid_.length; ++index_) {
+            if (!isValid_[index_]) continue; // Skip invalid signatures.
+
+            validSigningNodeIds_[index_] = signatures_[index_].nodeId;
+        }
path: src/settlement-chain/PayerReportManager.sol
user: login: deluca-mike
body: Great catch!
created_at: 2025-05-06T08:42:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,375 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { ECDSA } from "../../lib/solady/src/utils/ECDSA.sol";
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../libraries/SequentialMerkleProofs.sol";
+
+import { IMigratable } from "../abstract/interfaces/IMigratable.sol";
+import { IParameterRegistryLike, INodeRegistryLike, IPayerRegistryLike } from "./interfaces/External.sol";
+import { IPayerReportManager } from "./interfaces/IPayerReportManager.sol";
+
+import { ERC5267 } from "../abstract/ERC5267.sol";
+import { Migratable } from "../abstract/Migratable.sol";
+
+// TODO: If a node operator owns more than one node, their signature for a payer report will be identical, and therefore
+//       replayable across their nodes. This may not be ideal, so it might be necessary to include the node id of the
+//       signer in the digest.
+
+/**
+ * @title  Implementation of the Payer Report Manager.
+ * @notice This contract handles functionality for submitting and settling payer reports.
+ */
+contract PayerReportManager is IPayerReportManager, Initializable, Migratable, ERC5267 {
+    /* ============ Constants/Immutables ============ */
+
+    // solhint-disable-next-line max-line-length
+    /// @dev keccak256("PayerReport(uint256 attesterNodeId,uint256 originatorNodeId,uint256 startSequenceId,uint256 endSequenceId,bytes32 payersMerkleRoot,address[] nodeOperators)")
+    bytes32 public constant PAYER_REPORT_TYPEHASH = 0x73ce31d3128c9ba6af04b3270a898f0abafdf9bed84e47ec7ebd2f3dd8e9c8a5;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable parameterRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable nodeRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable payerRegistry;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.PayerReportManager
+     * @notice The UUPS storage for the payer report manager.
+     * @param  payerReportsByOriginator The mapping of arrays of payer reports by originator node ID.
+     */
+    struct PayerReportManagerStorage {
+        mapping(uint32 originatorId => PayerReport[] payerReports) payerReportsByOriginator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.PayerReportManager")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _PAYER_REPORT_MANAGER_STORAGE_LOCATION =
+        0x26b057ee8e4d60685198828fdf1c618ab8e36b0ab85f54a47b18319f6f718e00;
+
+    function _getPayerReportManagerStorage() internal pure returns (PayerReportManagerStorage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _PAYER_REPORT_MANAGER_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructor for the implementation contract, such that the implementation cannot be initialized.
+     * @param  parameterRegistry_ The address of the parameter registry.
+     * @param  nodeRegistry_      The address of the node registry.
+     * @param  payerRegistry_     The address of the payer registry.
+     * @dev    The parameter registry, node registry, and payer registry must not be the zero address.
+     * @dev    The parameter registry, node registry, and payer registry are immutable so that they are inlined in the
+     *         contract code, and have minimal gas cost.
+     */
+    constructor(address parameterRegistry_, address nodeRegistry_, address payerRegistry_) ERC5267() {
+        require(_isNotZero(parameterRegistry = parameterRegistry_), ZeroParameterRegistry());
+        require(_isNotZero(nodeRegistry = nodeRegistry_), ZeroNodeRegistry());
+        require(_isNotZero(payerRegistry = payerRegistry_), ZeroPayerRegistry());
+
+        _disableInitializers();
+    }
+
+    /* ============ Initialization ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function initialize() public initializer {
+        _initializeERC5267();
+    }
+
+    /* ============ Interactive Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function submit(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) external returns (uint256 payerReportIndex_) {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        payerReportIndex_ = payerReports_.length;
+
+        uint32 lastSequenceId_ = payerReportIndex_ > 0 ? payerReports_[payerReportIndex_ - 1].endSequenceId : 0;
+
+        // Enforces that the start sequence ID is the next expected sequence ID.
+        if (startSequenceId_ != lastSequenceId_ + 1) {
+            revert InvalidStartSequenceId(startSequenceId_, lastSequenceId_ + 1);
+        }
+
+        // Enforces that the end sequence ID is greater than or equal to the start sequence ID.
+        if (endSequenceId_ < startSequenceId_) revert InvalidSequenceIds();
+
+        // Verifies the signatures and gets the array of valid signing node IDs.
+        uint32[] memory validSigningNodeIds_ = _verifySignatures({
+            originatorNodeId_: originatorNodeId_,
+            attesterNodeId_: attesterNodeId_,
+            startSequenceId_: startSequenceId_,
+            endSequenceId_: endSequenceId_,
+            payersMerkleRoot_: payersMerkleRoot_,
+            nodeOperators_: nodeOperators_,
+            signatures_: signatures_
+        });
+
+        payerReports_.push(
+            PayerReport({
+                attesterNodeId: attesterNodeId_,
+                startSequenceId: startSequenceId_,
+                endSequenceId: endSequenceId_,
+                feesSettled: 0,
+                offset: 0,
+                isSettled: false,
+                payersMerkleRoot: payersMerkleRoot_,
+                nodeOperators: nodeOperators_
+            })
+        );
+
+        emit PayerReportSubmitted({
+            originatorNodeId: originatorNodeId_,
+            payerReportIndex: payerReportIndex_,
+            attesterNodeId: attesterNodeId_,
+            startSequenceId: startSequenceId_,
+            endSequenceId: endSequenceId_,
+            payersMerkleRoot: payersMerkleRoot_,
+            nodeOperators: nodeOperators_,
+            signingNodeIds: validSigningNodeIds_
+        });
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function settle(
+        uint32 originatorNodeId_,
+        uint32 payerReportIndex_,
+        bytes[] calldata payerFees_,
+        bytes32[] calldata proofElements_
+    ) external {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        if (payerReportIndex_ >= payerReports_.length) revert PayerReportIndexOutOfBounds();
+
+        PayerReport storage payerReport_ = payerReports_[payerReportIndex_];
+
+        if (payerReport_.isSettled) revert PayerReportEntirelySettled();
+
+        // Verify the payer fees provided are the next sequential payer fees from the last settled offset.
+        SequentialMerkleProofs.verify(payerReport_.payersMerkleRoot, payerReport_.offset, payerFees_, proofElements_);
+
+        payerReport_.offset += uint32(payerFees_.length);
+
+        uint32 leafCount_ = SequentialMerkleProofs.getLeafCount(proofElements_);
+
+        payerReport_.isSettled = leafCount_ == payerReport_.offset;
+
+        // Low level call which handles passing the `payerFees_` arrays as a bytes array that will be automatically
+        // decoded as the required structs by the payer registry's `settleUsage` function.
+        (bool success_, bytes memory returnData_) = payerRegistry.call(
+            abi.encodeWithSelector(IPayerRegistryLike.settleUsage.selector, payerFees_)
+        );
+
+        if (!success_) revert SettleUsageFailed(returnData_);
+
+        uint96 feesSettled_ = abi.decode(returnData_, (uint96));
+
+        emit PayerReportSubsetSettled(
+            originatorNodeId_,
+            payerReportIndex_,
+            uint32(payerFees_.length),
+            leafCount_ - payerReport_.offset,
+            feesSettled_
+        );
+
+        payerReport_.feesSettled += feesSettled_;
+    }
+
+    /// @inheritdoc IMigratable
+    function migrate() external {
+        _migrate(_toAddress(_getRegistryParameter(migratorParameterKey())));
+    }
+
+    /* ============ View/Pure Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function migratorParameterKey() public pure returns (bytes memory key_) {
+        return "xmtp.payerReportManager.migrator";
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReportDigest(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) external view returns (bytes32 digest_) {
+        return
+            _getPayerReportDigest(
+                originatorNodeId_,
+                attesterNodeId_,
+                startSequenceId_,
+                endSequenceId_,
+                payersMerkleRoot_,
+                nodeOperators_
+            );
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReports(uint32 originatorNodeId_) external view returns (PayerReport[] memory payerReports_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_];
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReport(
+        uint32 originatorNodeId_,
+        uint256 payerReportIndex_
+    ) external view returns (PayerReport memory payerReport_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_][payerReportIndex_];
+    }
+
+    /* ============ Internal View/Pure Functions ============ */
+
+    function _name() internal pure override returns (string memory name_) {
+        return "PayerReportManager";
+    }
+
+    function _version() internal pure override returns (string memory version_) {
+        return "1";
+    }
+
+    /**
+     * @dev    Returns the EIP-712 digest for a payer report.
+     * @param  originatorNodeId_ The id of the node originator.
+     * @param  attesterNodeId_   The id of the node attester.
+     * @param  startSequenceId_  The start sequence ID.
+     * @param  endSequenceId_    The end sequence ID.
+     * @param  payersMerkleRoot_ The payers merkle root.
+     * @param  nodeOperators_    The active node operators during the reporting period.
+     * @return digest_           The EIP-712 digest.
+     */
+    function _getPayerReportDigest(
+        uint256 originatorNodeId_,
+        uint256 attesterNodeId_,
+        uint256 startSequenceId_,
+        uint256 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) internal view returns (bytes32 digest_) {
+        return
+            _getDigest(
+                keccak256(
+                    abi.encode(
+                        PAYER_REPORT_TYPEHASH,
+                        originatorNodeId_,
+                        attesterNodeId_,
+                        startSequenceId_,
+                        endSequenceId_,
+                        payersMerkleRoot_,
+                        keccak256(abi.encodePacked(nodeOperators_))
+                    )
+                )
+            );
+    }
+
+    function _getRegistryParameter(bytes memory key_) internal view returns (bytes32 value_) {
+        return IParameterRegistryLike(parameterRegistry).get(key_);
+    }
+
+    function _isNotZero(address input_) internal pure returns (bool isNotZero_) {
+        return input_ != address(0);
+    }
+
+    function _toAddress(bytes32 value_) internal pure returns (address address_) {
+        // slither-disable-next-line assembly
+        assembly {
+            address_ := value_
+        }
+    }
+
+    /**
+     * @dev Verifies that enough of the signatures are valid and provided by canonical node operators and returns the
+     *      array of valid signing node IDs.
+     */
+    function _verifySignatures(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) internal view returns (uint32[] memory validSigningNodeIds_) {
+        bytes32 digest_ = _getPayerReportDigest(
+            originatorNodeId_,
+            attesterNodeId_,
+            startSequenceId_,
+            endSequenceId_,
+            payersMerkleRoot_,
+            nodeOperators_
+        );
+
+        // Need to determine how many of the signatures are valid and provided by canonical node operators.
+        uint8 validSignatureCount_;
+
+        // This array will help in constructing the `validSigningNodeIds_` array, who's length may be less than
+        // the length of the `signatures_` array if some of the signatures are invalid.
+        bool[] memory isValid_ = new bool[](signatures_.length);
+
+        for (uint256 index_; index_ < signatures_.length; ++index_) {
+            uint32 nodeId_ = signatures_[index_].nodeId;
+
+            // Enforces that the signing node IDs are ordered and unique.
+            if (index_ != 0 && nodeId_ <= signatures_[index_ - 1].nodeId) revert UnorderedNodeIds();
+
+            // If the signature is invalid, ignore it.
+            if (!(isValid_[index_] = _verifySignature(digest_, nodeId_, signatures_[index_].signature))) continue;
+
+            ++validSignatureCount_;
+        }
+
+        uint8 requiredSignatureCount_ = (INodeRegistryLike(nodeRegistry).canonicalNodesCount() / 2) + 1;
+
+        // Enforces that the number of valid signatures is greater than one more than half of the canonical node count.
+        if (validSignatureCount_ < requiredSignatureCount_) {
+            revert InsufficientSignatures(validSignatureCount_, requiredSignatureCount_);
+        }
+
+        validSigningNodeIds_ = new uint32[](validSignatureCount_);
+
+        for (uint256 index_; index_ < isValid_.length; ++index_) {
+            if (!isValid_[index_]) continue; // Skip invalid signatures.
+
+            validSigningNodeIds_[index_] = signatures_[index_].nodeId;
+        }
path: src/settlement-chain/PayerReportManager.sol
user: login: coderabbitai[bot]
body: @deluca-mike You're welcome! Happy to help identify this potential bug. Let me know if you need any further assistance with this code.

---

<!-- This is an auto-generated comment: tweet message by coderabbit.ai -->

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Avoid%20out-of-bounds%20errors%20by%20using%20a%20separate%20pointer%20when%20filtering%20arrays!%20%F0%9F%90%87%20%40coderabbitai%E2%80%99s%20fix%20ensures%20safe%20indexing%20and%20prevents%20panics.%20Try%20it%20for%20safer%2C%20cleaner%20code!%20%F0%9F%9A%80&url=https%3A%2F%2Fgithub.com%2Fxmtp%2Fsmart-contracts%2Fpull%2F56%23discussion_r2074994394)?

Thank you for using CodeRabbit!

<!-- end of auto-generated comment: tweet message by coderabbit.ai -->




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@deluca-mike You're welcome! Happy to help identify this potential bug. Let me know if you need any further assistance with this code.

---

&lt;!-- This is an auto-generated comment: tweet message by coderabbit.ai --&gt;

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Avoid%20out-of-bounds%20errors%20by%20using%20a%20separate%20pointer%20when%20filtering%20arrays!%20%F0%9F%90%87%20%40coderabbitai%E2%80%99s%20fix%20ensures%20safe%20indexing%20and%20prevents%20panics.%20Try%20it%20for%20safer%2C%20cleaner%20code!%20%F0%9F%9A%80&url=https%3A%2F%2Fgithub.com%2Fxmtp%2Fsmart-contracts%2Fpull%2F56%23discussion_r2074994394)?

Thank you for using CodeRabbit!

&lt;!-- end of auto-generated comment: tweet message by coderabbit.ai --&gt;

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-06T08:43:15Z
author_association: NONE
diff_hunk: @@ -0,0 +1,375 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { ECDSA } from "../../lib/solady/src/utils/ECDSA.sol";
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../libraries/SequentialMerkleProofs.sol";
+
+import { IMigratable } from "../abstract/interfaces/IMigratable.sol";
+import { IParameterRegistryLike, INodeRegistryLike, IPayerRegistryLike } from "./interfaces/External.sol";
+import { IPayerReportManager } from "./interfaces/IPayerReportManager.sol";
+
+import { ERC5267 } from "../abstract/ERC5267.sol";
+import { Migratable } from "../abstract/Migratable.sol";
+
+// TODO: If a node operator owns more than one node, their signature for a payer report will be identical, and therefore
+//       replayable across their nodes. This may not be ideal, so it might be necessary to include the node id of the
+//       signer in the digest.
+
+/**
+ * @title  Implementation of the Payer Report Manager.
+ * @notice This contract handles functionality for submitting and settling payer reports.
+ */
+contract PayerReportManager is IPayerReportManager, Initializable, Migratable, ERC5267 {
+    /* ============ Constants/Immutables ============ */
+
+    // solhint-disable-next-line max-line-length
+    /// @dev keccak256("PayerReport(uint256 attesterNodeId,uint256 originatorNodeId,uint256 startSequenceId,uint256 endSequenceId,bytes32 payersMerkleRoot,address[] nodeOperators)")
+    bytes32 public constant PAYER_REPORT_TYPEHASH = 0x73ce31d3128c9ba6af04b3270a898f0abafdf9bed84e47ec7ebd2f3dd8e9c8a5;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable parameterRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable nodeRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable payerRegistry;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.PayerReportManager
+     * @notice The UUPS storage for the payer report manager.
+     * @param  payerReportsByOriginator The mapping of arrays of payer reports by originator node ID.
+     */
+    struct PayerReportManagerStorage {
+        mapping(uint32 originatorId => PayerReport[] payerReports) payerReportsByOriginator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.PayerReportManager")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _PAYER_REPORT_MANAGER_STORAGE_LOCATION =
+        0x26b057ee8e4d60685198828fdf1c618ab8e36b0ab85f54a47b18319f6f718e00;
+
+    function _getPayerReportManagerStorage() internal pure returns (PayerReportManagerStorage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _PAYER_REPORT_MANAGER_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructor for the implementation contract, such that the implementation cannot be initialized.
+     * @param  parameterRegistry_ The address of the parameter registry.
+     * @param  nodeRegistry_      The address of the node registry.
+     * @param  payerRegistry_     The address of the payer registry.
+     * @dev    The parameter registry, node registry, and payer registry must not be the zero address.
+     * @dev    The parameter registry, node registry, and payer registry are immutable so that they are inlined in the
+     *         contract code, and have minimal gas cost.
+     */
+    constructor(address parameterRegistry_, address nodeRegistry_, address payerRegistry_) ERC5267() {
+        require(_isNotZero(parameterRegistry = parameterRegistry_), ZeroParameterRegistry());
+        require(_isNotZero(nodeRegistry = nodeRegistry_), ZeroNodeRegistry());
+        require(_isNotZero(payerRegistry = payerRegistry_), ZeroPayerRegistry());
+
+        _disableInitializers();
+    }
+
+    /* ============ Initialization ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function initialize() public initializer {
+        _initializeERC5267();
+    }
+
+    /* ============ Interactive Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function submit(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) external returns (uint256 payerReportIndex_) {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        payerReportIndex_ = payerReports_.length;
+
+        uint32 lastSequenceId_ = payerReportIndex_ > 0 ? payerReports_[payerReportIndex_ - 1].endSequenceId : 0;
+
+        // Enforces that the start sequence ID is the next expected sequence ID.
+        if (startSequenceId_ != lastSequenceId_ + 1) {
+            revert InvalidStartSequenceId(startSequenceId_, lastSequenceId_ + 1);
+        }
+
+        // Enforces that the end sequence ID is greater than or equal to the start sequence ID.
+        if (endSequenceId_ < startSequenceId_) revert InvalidSequenceIds();
+
+        // Verifies the signatures and gets the array of valid signing node IDs.
+        uint32[] memory validSigningNodeIds_ = _verifySignatures({
+            originatorNodeId_: originatorNodeId_,
+            attesterNodeId_: attesterNodeId_,
+            startSequenceId_: startSequenceId_,
+            endSequenceId_: endSequenceId_,
+            payersMerkleRoot_: payersMerkleRoot_,
+            nodeOperators_: nodeOperators_,
+            signatures_: signatures_
+        });
+
+        payerReports_.push(
+            PayerReport({
+                attesterNodeId: attesterNodeId_,
+                startSequenceId: startSequenceId_,
+                endSequenceId: endSequenceId_,
+                feesSettled: 0,
+                offset: 0,
+                isSettled: false,
+                payersMerkleRoot: payersMerkleRoot_,
+                nodeOperators: nodeOperators_
+            })
+        );
+
+        emit PayerReportSubmitted({
+            originatorNodeId: originatorNodeId_,
+            payerReportIndex: payerReportIndex_,
+            attesterNodeId: attesterNodeId_,
+            startSequenceId: startSequenceId_,
+            endSequenceId: endSequenceId_,
+            payersMerkleRoot: payersMerkleRoot_,
+            nodeOperators: nodeOperators_,
+            signingNodeIds: validSigningNodeIds_
+        });
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function settle(
+        uint32 originatorNodeId_,
+        uint32 payerReportIndex_,
+        bytes[] calldata payerFees_,
+        bytes32[] calldata proofElements_
+    ) external {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        if (payerReportIndex_ >= payerReports_.length) revert PayerReportIndexOutOfBounds();
+
+        PayerReport storage payerReport_ = payerReports_[payerReportIndex_];
+
+        if (payerReport_.isSettled) revert PayerReportEntirelySettled();
+
+        // Verify the payer fees provided are the next sequential payer fees from the last settled offset.
+        SequentialMerkleProofs.verify(payerReport_.payersMerkleRoot, payerReport_.offset, payerFees_, proofElements_);
+
+        payerReport_.offset += uint32(payerFees_.length);
+
+        uint32 leafCount_ = SequentialMerkleProofs.getLeafCount(proofElements_);
+
+        payerReport_.isSettled = leafCount_ == payerReport_.offset;
+
+        // Low level call which handles passing the `payerFees_` arrays as a bytes array that will be automatically
+        // decoded as the required structs by the payer registry's `settleUsage` function.
+        (bool success_, bytes memory returnData_) = payerRegistry.call(
+            abi.encodeWithSelector(IPayerRegistryLike.settleUsage.selector, payerFees_)
+        );
+
+        if (!success_) revert SettleUsageFailed(returnData_);
+
+        uint96 feesSettled_ = abi.decode(returnData_, (uint96));
+
+        emit PayerReportSubsetSettled(
+            originatorNodeId_,
+            payerReportIndex_,
+            uint32(payerFees_.length),
+            leafCount_ - payerReport_.offset,
+            feesSettled_
+        );
+
+        payerReport_.feesSettled += feesSettled_;
+    }
path: src/settlement-chain/PayerReportManager.sol
user: login: deluca-mike
body: Fixed
created_at: 2025-05-06T08:56:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,375 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { ECDSA } from "../../lib/solady/src/utils/ECDSA.sol";
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../libraries/SequentialMerkleProofs.sol";
+
+import { IMigratable } from "../abstract/interfaces/IMigratable.sol";
+import { IParameterRegistryLike, INodeRegistryLike, IPayerRegistryLike } from "./interfaces/External.sol";
+import { IPayerReportManager } from "./interfaces/IPayerReportManager.sol";
+
+import { ERC5267 } from "../abstract/ERC5267.sol";
+import { Migratable } from "../abstract/Migratable.sol";
+
+// TODO: If a node operator owns more than one node, their signature for a payer report will be identical, and therefore
+//       replayable across their nodes. This may not be ideal, so it might be necessary to include the node id of the
+//       signer in the digest.
+
+/**
+ * @title  Implementation of the Payer Report Manager.
+ * @notice This contract handles functionality for submitting and settling payer reports.
+ */
+contract PayerReportManager is IPayerReportManager, Initializable, Migratable, ERC5267 {
+    /* ============ Constants/Immutables ============ */
+
+    // solhint-disable-next-line max-line-length
+    /// @dev keccak256("PayerReport(uint256 attesterNodeId,uint256 originatorNodeId,uint256 startSequenceId,uint256 endSequenceId,bytes32 payersMerkleRoot,address[] nodeOperators)")
+    bytes32 public constant PAYER_REPORT_TYPEHASH = 0x73ce31d3128c9ba6af04b3270a898f0abafdf9bed84e47ec7ebd2f3dd8e9c8a5;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable parameterRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable nodeRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable payerRegistry;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.PayerReportManager
+     * @notice The UUPS storage for the payer report manager.
+     * @param  payerReportsByOriginator The mapping of arrays of payer reports by originator node ID.
+     */
+    struct PayerReportManagerStorage {
+        mapping(uint32 originatorId => PayerReport[] payerReports) payerReportsByOriginator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.PayerReportManager")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _PAYER_REPORT_MANAGER_STORAGE_LOCATION =
+        0x26b057ee8e4d60685198828fdf1c618ab8e36b0ab85f54a47b18319f6f718e00;
+
+    function _getPayerReportManagerStorage() internal pure returns (PayerReportManagerStorage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _PAYER_REPORT_MANAGER_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructor for the implementation contract, such that the implementation cannot be initialized.
+     * @param  parameterRegistry_ The address of the parameter registry.
+     * @param  nodeRegistry_      The address of the node registry.
+     * @param  payerRegistry_     The address of the payer registry.
+     * @dev    The parameter registry, node registry, and payer registry must not be the zero address.
+     * @dev    The parameter registry, node registry, and payer registry are immutable so that they are inlined in the
+     *         contract code, and have minimal gas cost.
+     */
+    constructor(address parameterRegistry_, address nodeRegistry_, address payerRegistry_) ERC5267() {
+        require(_isNotZero(parameterRegistry = parameterRegistry_), ZeroParameterRegistry());
+        require(_isNotZero(nodeRegistry = nodeRegistry_), ZeroNodeRegistry());
+        require(_isNotZero(payerRegistry = payerRegistry_), ZeroPayerRegistry());
+
+        _disableInitializers();
+    }
+
+    /* ============ Initialization ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function initialize() public initializer {
+        _initializeERC5267();
+    }
+
+    /* ============ Interactive Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function submit(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) external returns (uint256 payerReportIndex_) {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        payerReportIndex_ = payerReports_.length;
+
+        uint32 lastSequenceId_ = payerReportIndex_ > 0 ? payerReports_[payerReportIndex_ - 1].endSequenceId : 0;
+
+        // Enforces that the start sequence ID is the next expected sequence ID.
+        if (startSequenceId_ != lastSequenceId_ + 1) {
+            revert InvalidStartSequenceId(startSequenceId_, lastSequenceId_ + 1);
+        }
+
+        // Enforces that the end sequence ID is greater than or equal to the start sequence ID.
+        if (endSequenceId_ < startSequenceId_) revert InvalidSequenceIds();
+
+        // Verifies the signatures and gets the array of valid signing node IDs.
+        uint32[] memory validSigningNodeIds_ = _verifySignatures({
+            originatorNodeId_: originatorNodeId_,
+            attesterNodeId_: attesterNodeId_,
+            startSequenceId_: startSequenceId_,
+            endSequenceId_: endSequenceId_,
+            payersMerkleRoot_: payersMerkleRoot_,
+            nodeOperators_: nodeOperators_,
+            signatures_: signatures_
+        });
+
+        payerReports_.push(
+            PayerReport({
+                attesterNodeId: attesterNodeId_,
+                startSequenceId: startSequenceId_,
+                endSequenceId: endSequenceId_,
+                feesSettled: 0,
+                offset: 0,
+                isSettled: false,
+                payersMerkleRoot: payersMerkleRoot_,
+                nodeOperators: nodeOperators_
+            })
+        );
+
+        emit PayerReportSubmitted({
+            originatorNodeId: originatorNodeId_,
+            payerReportIndex: payerReportIndex_,
+            attesterNodeId: attesterNodeId_,
+            startSequenceId: startSequenceId_,
+            endSequenceId: endSequenceId_,
+            payersMerkleRoot: payersMerkleRoot_,
+            nodeOperators: nodeOperators_,
+            signingNodeIds: validSigningNodeIds_
+        });
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function settle(
+        uint32 originatorNodeId_,
+        uint32 payerReportIndex_,
+        bytes[] calldata payerFees_,
+        bytes32[] calldata proofElements_
+    ) external {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        if (payerReportIndex_ >= payerReports_.length) revert PayerReportIndexOutOfBounds();
+
+        PayerReport storage payerReport_ = payerReports_[payerReportIndex_];
+
+        if (payerReport_.isSettled) revert PayerReportEntirelySettled();
+
+        // Verify the payer fees provided are the next sequential payer fees from the last settled offset.
+        SequentialMerkleProofs.verify(payerReport_.payersMerkleRoot, payerReport_.offset, payerFees_, proofElements_);
+
+        payerReport_.offset += uint32(payerFees_.length);
+
+        uint32 leafCount_ = SequentialMerkleProofs.getLeafCount(proofElements_);
+
+        payerReport_.isSettled = leafCount_ == payerReport_.offset;
+
+        // Low level call which handles passing the `payerFees_` arrays as a bytes array that will be automatically
+        // decoded as the required structs by the payer registry's `settleUsage` function.
+        (bool success_, bytes memory returnData_) = payerRegistry.call(
+            abi.encodeWithSelector(IPayerRegistryLike.settleUsage.selector, payerFees_)
+        );
+
+        if (!success_) revert SettleUsageFailed(returnData_);
+
+        uint96 feesSettled_ = abi.decode(returnData_, (uint96));
+
+        emit PayerReportSubsetSettled(
+            originatorNodeId_,
+            payerReportIndex_,
+            uint32(payerFees_.length),
+            leafCount_ - payerReport_.offset,
+            feesSettled_
+        );
+
+        payerReport_.feesSettled += feesSettled_;
+    }
path: src/settlement-chain/PayerReportManager.sol
user: login: deluca-mike
body: Fixed
created_at: 2025-05-06T08:56:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,375 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { ECDSA } from "../../lib/solady/src/utils/ECDSA.sol";
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../libraries/SequentialMerkleProofs.sol";
+
+import { IMigratable } from "../abstract/interfaces/IMigratable.sol";
+import { IParameterRegistryLike, INodeRegistryLike, IPayerRegistryLike } from "./interfaces/External.sol";
+import { IPayerReportManager } from "./interfaces/IPayerReportManager.sol";
+
+import { ERC5267 } from "../abstract/ERC5267.sol";
+import { Migratable } from "../abstract/Migratable.sol";
+
+// TODO: If a node operator owns more than one node, their signature for a payer report will be identical, and therefore
+//       replayable across their nodes. This may not be ideal, so it might be necessary to include the node id of the
+//       signer in the digest.
+
+/**
+ * @title  Implementation of the Payer Report Manager.
+ * @notice This contract handles functionality for submitting and settling payer reports.
+ */
+contract PayerReportManager is IPayerReportManager, Initializable, Migratable, ERC5267 {
+    /* ============ Constants/Immutables ============ */
+
+    // solhint-disable-next-line max-line-length
+    /// @dev keccak256("PayerReport(uint256 attesterNodeId,uint256 originatorNodeId,uint256 startSequenceId,uint256 endSequenceId,bytes32 payersMerkleRoot,address[] nodeOperators)")
+    bytes32 public constant PAYER_REPORT_TYPEHASH = 0x73ce31d3128c9ba6af04b3270a898f0abafdf9bed84e47ec7ebd2f3dd8e9c8a5;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable parameterRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable nodeRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable payerRegistry;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.PayerReportManager
+     * @notice The UUPS storage for the payer report manager.
+     * @param  payerReportsByOriginator The mapping of arrays of payer reports by originator node ID.
+     */
+    struct PayerReportManagerStorage {
+        mapping(uint32 originatorId => PayerReport[] payerReports) payerReportsByOriginator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.PayerReportManager")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _PAYER_REPORT_MANAGER_STORAGE_LOCATION =
+        0x26b057ee8e4d60685198828fdf1c618ab8e36b0ab85f54a47b18319f6f718e00;
+
+    function _getPayerReportManagerStorage() internal pure returns (PayerReportManagerStorage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _PAYER_REPORT_MANAGER_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructor for the implementation contract, such that the implementation cannot be initialized.
+     * @param  parameterRegistry_ The address of the parameter registry.
+     * @param  nodeRegistry_      The address of the node registry.
+     * @param  payerRegistry_     The address of the payer registry.
+     * @dev    The parameter registry, node registry, and payer registry must not be the zero address.
+     * @dev    The parameter registry, node registry, and payer registry are immutable so that they are inlined in the
+     *         contract code, and have minimal gas cost.
+     */
+    constructor(address parameterRegistry_, address nodeRegistry_, address payerRegistry_) ERC5267() {
+        require(_isNotZero(parameterRegistry = parameterRegistry_), ZeroParameterRegistry());
+        require(_isNotZero(nodeRegistry = nodeRegistry_), ZeroNodeRegistry());
+        require(_isNotZero(payerRegistry = payerRegistry_), ZeroPayerRegistry());
+
+        _disableInitializers();
+    }
+
+    /* ============ Initialization ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function initialize() public initializer {
+        _initializeERC5267();
+    }
+
+    /* ============ Interactive Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function submit(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) external returns (uint256 payerReportIndex_) {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        payerReportIndex_ = payerReports_.length;
+
+        uint32 lastSequenceId_ = payerReportIndex_ > 0 ? payerReports_[payerReportIndex_ - 1].endSequenceId : 0;
+
+        // Enforces that the start sequence ID is the next expected sequence ID.
+        if (startSequenceId_ != lastSequenceId_ + 1) {
+            revert InvalidStartSequenceId(startSequenceId_, lastSequenceId_ + 1);
+        }
+
+        // Enforces that the end sequence ID is greater than or equal to the start sequence ID.
+        if (endSequenceId_ < startSequenceId_) revert InvalidSequenceIds();
+
+        // Verifies the signatures and gets the array of valid signing node IDs.
+        uint32[] memory validSigningNodeIds_ = _verifySignatures({
+            originatorNodeId_: originatorNodeId_,
+            attesterNodeId_: attesterNodeId_,
+            startSequenceId_: startSequenceId_,
+            endSequenceId_: endSequenceId_,
+            payersMerkleRoot_: payersMerkleRoot_,
+            nodeOperators_: nodeOperators_,
+            signatures_: signatures_
+        });
+
+        payerReports_.push(
+            PayerReport({
+                attesterNodeId: attesterNodeId_,
+                startSequenceId: startSequenceId_,
+                endSequenceId: endSequenceId_,
+                feesSettled: 0,
+                offset: 0,
+                isSettled: false,
+                payersMerkleRoot: payersMerkleRoot_,
+                nodeOperators: nodeOperators_
+            })
+        );
+
+        emit PayerReportSubmitted({
+            originatorNodeId: originatorNodeId_,
+            payerReportIndex: payerReportIndex_,
+            attesterNodeId: attesterNodeId_,
+            startSequenceId: startSequenceId_,
+            endSequenceId: endSequenceId_,
+            payersMerkleRoot: payersMerkleRoot_,
+            nodeOperators: nodeOperators_,
+            signingNodeIds: validSigningNodeIds_
+        });
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function settle(
+        uint32 originatorNodeId_,
+        uint32 payerReportIndex_,
+        bytes[] calldata payerFees_,
+        bytes32[] calldata proofElements_
+    ) external {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        if (payerReportIndex_ >= payerReports_.length) revert PayerReportIndexOutOfBounds();
+
+        PayerReport storage payerReport_ = payerReports_[payerReportIndex_];
+
+        if (payerReport_.isSettled) revert PayerReportEntirelySettled();
+
+        // Verify the payer fees provided are the next sequential payer fees from the last settled offset.
+        SequentialMerkleProofs.verify(payerReport_.payersMerkleRoot, payerReport_.offset, payerFees_, proofElements_);
+
+        payerReport_.offset += uint32(payerFees_.length);
+
+        uint32 leafCount_ = SequentialMerkleProofs.getLeafCount(proofElements_);
+
+        payerReport_.isSettled = leafCount_ == payerReport_.offset;
+
+        // Low level call which handles passing the `payerFees_` arrays as a bytes array that will be automatically
+        // decoded as the required structs by the payer registry's `settleUsage` function.
+        (bool success_, bytes memory returnData_) = payerRegistry.call(
+            abi.encodeWithSelector(IPayerRegistryLike.settleUsage.selector, payerFees_)
+        );
+
+        if (!success_) revert SettleUsageFailed(returnData_);
+
+        uint96 feesSettled_ = abi.decode(returnData_, (uint96));
+
+        emit PayerReportSubsetSettled(
+            originatorNodeId_,
+            payerReportIndex_,
+            uint32(payerFees_.length),
+            leafCount_ - payerReport_.offset,
+            feesSettled_
+        );
+
+        payerReport_.feesSettled += feesSettled_;
+    }
+
+    /// @inheritdoc IMigratable
+    function migrate() external {
+        _migrate(_toAddress(_getRegistryParameter(migratorParameterKey())));
+    }
+
+    /* ============ View/Pure Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function migratorParameterKey() public pure returns (bytes memory key_) {
+        return "xmtp.payerReportManager.migrator";
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReportDigest(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) external view returns (bytes32 digest_) {
+        return
+            _getPayerReportDigest(
+                originatorNodeId_,
+                attesterNodeId_,
+                startSequenceId_,
+                endSequenceId_,
+                payersMerkleRoot_,
+                nodeOperators_
+            );
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReports(uint32 originatorNodeId_) external view returns (PayerReport[] memory payerReports_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_];
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReport(
+        uint32 originatorNodeId_,
+        uint256 payerReportIndex_
+    ) external view returns (PayerReport memory payerReport_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_][payerReportIndex_];
+    }
+
+    /* ============ Internal View/Pure Functions ============ */
+
+    function _name() internal pure override returns (string memory name_) {
+        return "PayerReportManager";
+    }
+
+    function _version() internal pure override returns (string memory version_) {
+        return "1";
+    }
+
+    /**
+     * @dev    Returns the EIP-712 digest for a payer report.
+     * @param  originatorNodeId_ The id of the node originator.
+     * @param  attesterNodeId_   The id of the node attester.
+     * @param  startSequenceId_  The start sequence ID.
+     * @param  endSequenceId_    The end sequence ID.
+     * @param  payersMerkleRoot_ The payers merkle root.
+     * @param  nodeOperators_    The active node operators during the reporting period.
+     * @return digest_           The EIP-712 digest.
+     */
+    function _getPayerReportDigest(
+        uint256 originatorNodeId_,
+        uint256 attesterNodeId_,
+        uint256 startSequenceId_,
+        uint256 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) internal view returns (bytes32 digest_) {
+        return
+            _getDigest(
+                keccak256(
+                    abi.encode(
+                        PAYER_REPORT_TYPEHASH,
+                        originatorNodeId_,
+                        attesterNodeId_,
+                        startSequenceId_,
+                        endSequenceId_,
+                        payersMerkleRoot_,
+                        keccak256(abi.encodePacked(nodeOperators_))
+                    )
+                )
+            );
+    }
+
+    function _getRegistryParameter(bytes memory key_) internal view returns (bytes32 value_) {
+        return IParameterRegistryLike(parameterRegistry).get(key_);
+    }
+
+    function _isNotZero(address input_) internal pure returns (bool isNotZero_) {
+        return input_ != address(0);
+    }
+
+    function _toAddress(bytes32 value_) internal pure returns (address address_) {
+        // slither-disable-next-line assembly
+        assembly {
+            address_ := value_
+        }
+    }
+
+    /**
+     * @dev Verifies that enough of the signatures are valid and provided by canonical node operators and returns the
+     *      array of valid signing node IDs.
+     */
+    function _verifySignatures(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) internal view returns (uint32[] memory validSigningNodeIds_) {
+        bytes32 digest_ = _getPayerReportDigest(
+            originatorNodeId_,
+            attesterNodeId_,
+            startSequenceId_,
+            endSequenceId_,
+            payersMerkleRoot_,
+            nodeOperators_
+        );
+
+        // Need to determine how many of the signatures are valid and provided by canonical node operators.
+        uint8 validSignatureCount_;
path: src/settlement-chain/PayerReportManager.sol
user: login: deluca-mike
body: Fixed
created_at: 2025-05-06T08:57:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,100 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { IERC712 } from "./interfaces/IERC712.sol";
+
+/**
+ * @title An abstract implementation of EIP-712 for typed structured data hashing and signing.
+ */
+abstract contract ERC712 is IERC712 {
+    /* ============ Variables ============ */
+
+    /// @dev keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
+    bytes32 internal constant _EIP712_DOMAIN_HASH = 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f;
+
+    /// @dev Initial Chain ID set at deployment.
+    uint256 internal immutable _initialChainId;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.ERC712
+     * @notice The UUPS storage for the ERC712 contract.
+     * @param  initialDomainSeparator Initial EIP-712 domain separator set at intialization.
+     */
+    struct ERC712Storage {
+        bytes32 initialDomainSeparator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.ERC712")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _ERC712_STORAGE_LOCATION =
+        0xc7effa11ad597798220888e5d1ba4eeddcc8c2635d01dae8b9f958ac905c1100;
+
+    function _getERC712Storage() internal pure returns (ERC712Storage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _ERC712_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructs the EIP-712 domain separator.
+     */
+    constructor() {
+        _initialChainId = block.chainid;
+    }
+
+    /* ============ Initialization ============ */
+
+    function _initializeERC712() internal {
+        _getERC712Storage().initialDomainSeparator = _getDomainSeparator();
+    }
+
+    /* ============ View/Pure Functions ============ */
+
+    /// @inheritdoc IERC712
+    // slither-disable-next-line naming-convention
+    function DOMAIN_SEPARATOR() public view returns (bytes32 domainSeparator_) {
+        return block.chainid == _initialChainId ? _getERC712Storage().initialDomainSeparator : _getDomainSeparator();
+    }
+
path: src/abstract/ERC712.sol
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Missing `EIP712DomainChanged` emission when the chain-ID flips**

`DOMAIN_SEPARATOR()` recomputes the value if a fork changes `block.chainid`, yet no `EIP712DomainChanged` event (required by EIP-5267) is emitted.  Consumers relying on that signal may never notice the domain update.

One minimal fix is to let the EIP-5267 wrapper take care of the emission:

```diff
// In ERC5267.sol
+    // slither-disable-next-line naming-convention
+    function DOMAIN_SEPARATOR()
+        public
+        view
+        override(IERC712, ERC712)
+        returns (bytes32 domainSeparator_)
+    {
+        // Call parent to (re)compute
+        domainSeparator_ = super.DOMAIN_SEPARATOR();
+
+        // Emit only when the cached value is no longer valid
+        if (block.chainid != _initialChainId) {
+            emit EIP712DomainChanged();
+        }
+    }
```
This maintains backward-compatibility while meeting the standard’s requirements.




> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: Lint check</summary>

[warning] 59-59: 
Function name must be in mixedCase                                                                  

</details>

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-06T08:59:47Z
author_association: NONE
diff_hunk: @@ -0,0 +1,381 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { ECDSA } from "../../lib/solady/src/utils/ECDSA.sol";
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../libraries/SequentialMerkleProofs.sol";
+
+import { IMigratable } from "../abstract/interfaces/IMigratable.sol";
+import { IParameterRegistryLike, INodeRegistryLike, IPayerRegistryLike } from "./interfaces/External.sol";
+import { IPayerReportManager } from "./interfaces/IPayerReportManager.sol";
+
+import { ERC5267 } from "../abstract/ERC5267.sol";
+import { Migratable } from "../abstract/Migratable.sol";
+
+// TODO: If a node operator owns more than one node, their signature for a payer report will be identical, and therefore
+//       replayable across their nodes. This may not be ideal, so it might be necessary to include the node id of the
+//       signer in the digest.
+
+/**
+ * @title  Implementation of the Payer Report Manager.
+ * @notice This contract handles functionality for submitting and settling payer reports.
+ */
+contract PayerReportManager is IPayerReportManager, Initializable, Migratable, ERC5267 {
+    /* ============ Constants/Immutables ============ */
+
+    // solhint-disable-next-line max-line-length
+    /// @dev keccak256("PayerReport(uint256 attesterNodeId,uint256 originatorNodeId,uint256 startSequenceId,uint256 endSequenceId,bytes32 payersMerkleRoot,address[] nodeOperators)")
+    bytes32 public constant PAYER_REPORT_TYPEHASH = 0x73ce31d3128c9ba6af04b3270a898f0abafdf9bed84e47ec7ebd2f3dd8e9c8a5;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable parameterRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable nodeRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable payerRegistry;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.PayerReportManager
+     * @notice The UUPS storage for the payer report manager.
+     * @param  payerReportsByOriginator The mapping of arrays of payer reports by originator node ID.
+     */
+    struct PayerReportManagerStorage {
+        mapping(uint32 originatorId => PayerReport[] payerReports) payerReportsByOriginator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.PayerReportManager")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _PAYER_REPORT_MANAGER_STORAGE_LOCATION =
+        0x26b057ee8e4d60685198828fdf1c618ab8e36b0ab85f54a47b18319f6f718e00;
+
+    function _getPayerReportManagerStorage() internal pure returns (PayerReportManagerStorage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _PAYER_REPORT_MANAGER_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructor for the implementation contract, such that the implementation cannot be initialized.
+     * @param  parameterRegistry_ The address of the parameter registry.
+     * @param  nodeRegistry_      The address of the node registry.
+     * @param  payerRegistry_     The address of the payer registry.
+     * @dev    The parameter registry, node registry, and payer registry must not be the zero address.
+     * @dev    The parameter registry, node registry, and payer registry are immutable so that they are inlined in the
+     *         contract code, and have minimal gas cost.
+     */
+    constructor(address parameterRegistry_, address nodeRegistry_, address payerRegistry_) ERC5267() {
+        require(_isNotZero(parameterRegistry = parameterRegistry_), ZeroParameterRegistry());
+        require(_isNotZero(nodeRegistry = nodeRegistry_), ZeroNodeRegistry());
+        require(_isNotZero(payerRegistry = payerRegistry_), ZeroPayerRegistry());
+
+        _disableInitializers();
+    }
+
+    /* ============ Initialization ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function initialize() public initializer {
+        _initializeERC5267();
+    }
+
+    /* ============ Interactive Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function submit(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) external returns (uint256 payerReportIndex_) {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        payerReportIndex_ = payerReports_.length;
+
+        uint32 lastSequenceId_ = payerReportIndex_ > 0 ? payerReports_[payerReportIndex_ - 1].endSequenceId : 0;
+
+        // Enforces that the start sequence ID is the next expected sequence ID.
+        if (startSequenceId_ != lastSequenceId_ + 1) {
+            revert InvalidStartSequenceId(startSequenceId_, lastSequenceId_ + 1);
+        }
+
+        // Enforces that the end sequence ID is greater than or equal to the start sequence ID.
+        if (endSequenceId_ < startSequenceId_) revert InvalidSequenceIds();
+
+        // Verifies the signatures and gets the array of valid signing node IDs.
+        uint32[] memory validSigningNodeIds_ = _verifySignatures({
+            originatorNodeId_: originatorNodeId_,
+            attesterNodeId_: attesterNodeId_,
+            startSequenceId_: startSequenceId_,
+            endSequenceId_: endSequenceId_,
+            payersMerkleRoot_: payersMerkleRoot_,
+            nodeOperators_: nodeOperators_,
+            signatures_: signatures_
+        });
+
+        payerReports_.push(
+            PayerReport({
+                attesterNodeId: attesterNodeId_,
+                startSequenceId: startSequenceId_,
+                endSequenceId: endSequenceId_,
+                feesSettled: 0,
+                offset: 0,
+                isSettled: false,
+                payersMerkleRoot: payersMerkleRoot_,
+                nodeOperators: nodeOperators_
+            })
+        );
+
+        emit PayerReportSubmitted({
+            originatorNodeId: originatorNodeId_,
+            payerReportIndex: payerReportIndex_,
+            attesterNodeId: attesterNodeId_,
+            startSequenceId: startSequenceId_,
+            endSequenceId: endSequenceId_,
+            payersMerkleRoot: payersMerkleRoot_,
+            nodeOperators: nodeOperators_,
+            signingNodeIds: validSigningNodeIds_
+        });
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function settle(
+        uint32 originatorNodeId_,
+        uint32 payerReportIndex_,
+        bytes[] calldata payerFees_,
+        bytes32[] calldata proofElements_
+    ) external {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        if (payerReportIndex_ >= payerReports_.length) revert PayerReportIndexOutOfBounds();
+
+        PayerReport storage payerReport_ = payerReports_[payerReportIndex_];
+
+        if (payerReport_.isSettled) revert PayerReportEntirelySettled();
+
+        // Verify the payer fees provided are the next sequential payer fees from the last settled offset.
+        SequentialMerkleProofs.verify(payerReport_.payersMerkleRoot, payerReport_.offset, payerFees_, proofElements_);
+
+        payerReport_.offset += uint32(payerFees_.length);
+
+        uint32 leafCount_ = SequentialMerkleProofs.getLeafCount(proofElements_);
+
+        payerReport_.isSettled = leafCount_ == payerReport_.offset;
+
+        // Low level call which handles passing the `payerFees_` arrays as a bytes array that will be automatically
+        // decoded as the required structs by the payer registry's `settleUsage` function.
+        // slither-disable-next-line low-level-calls
+        (bool success_, bytes memory returnData_) = payerRegistry.call(
+            abi.encodeWithSelector(IPayerRegistryLike.settleUsage.selector, payerFees_)
+        );
+
+        if (!success_) revert SettleUsageFailed(returnData_);
+
+        uint96 feesSettled_ = abi.decode(returnData_, (uint96));
+
+        // slither-disable-next-line reentrancy-events
+        emit PayerReportSubsetSettled(
+            originatorNodeId_,
+            payerReportIndex_,
+            uint32(payerFees_.length),
+            leafCount_ - payerReport_.offset,
+            feesSettled_
+        );
+
+        payerReport_.feesSettled += feesSettled_;
+    }
+
+    /// @inheritdoc IMigratable
+    function migrate() external {
+        _migrate(_toAddress(_getRegistryParameter(migratorParameterKey())));
+    }
+
+    /* ============ View/Pure Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function migratorParameterKey() public pure returns (bytes memory key_) {
+        return "xmtp.payerReportManager.migrator";
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReportDigest(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) external view returns (bytes32 digest_) {
+        return
+            _getPayerReportDigest(
+                originatorNodeId_,
+                attesterNodeId_,
+                startSequenceId_,
+                endSequenceId_,
+                payersMerkleRoot_,
+                nodeOperators_
+            );
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReports(uint32 originatorNodeId_) external view returns (PayerReport[] memory payerReports_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_];
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReport(
+        uint32 originatorNodeId_,
+        uint256 payerReportIndex_
+    ) external view returns (PayerReport memory payerReport_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_][payerReportIndex_];
+    }
+
+    /* ============ Internal View/Pure Functions ============ */
+
+    function _name() internal pure override returns (string memory name_) {
+        return "PayerReportManager";
+    }
+
+    function _version() internal pure override returns (string memory version_) {
+        return "1";
+    }
+
+    /**
+     * @dev    Returns the EIP-712 digest for a payer report.
+     * @param  originatorNodeId_ The id of the node originator.
+     * @param  attesterNodeId_   The id of the node attester.
+     * @param  startSequenceId_  The start sequence ID.
+     * @param  endSequenceId_    The end sequence ID.
+     * @param  payersMerkleRoot_ The payers merkle root.
+     * @param  nodeOperators_    The active node operators during the reporting period.
+     * @return digest_           The EIP-712 digest.
+     */
+    function _getPayerReportDigest(
+        uint256 originatorNodeId_,
+        uint256 attesterNodeId_,
+        uint256 startSequenceId_,
+        uint256 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) internal view returns (bytes32 digest_) {
+        return
+            _getDigest(
+                keccak256(
+                    abi.encode(
+                        PAYER_REPORT_TYPEHASH,
+                        originatorNodeId_,
+                        attesterNodeId_,
+                        startSequenceId_,
+                        endSequenceId_,
+                        payersMerkleRoot_,
+                        keccak256(abi.encodePacked(nodeOperators_))
+                    )
+                )
+            );
+    }
+
+    function _getRegistryParameter(bytes memory key_) internal view returns (bytes32 value_) {
+        return IParameterRegistryLike(parameterRegistry).get(key_);
+    }
+
+    function _isNotZero(address input_) internal pure returns (bool isNotZero_) {
+        return input_ != address(0);
+    }
+
+    function _toAddress(bytes32 value_) internal pure returns (address address_) {
+        // slither-disable-next-line assembly
+        assembly {
+            address_ := value_
+        }
+    }
+
+    /**
+     * @dev Verifies that enough of the signatures are valid and provided by canonical node operators and returns the
+     *      array of valid signing node IDs.
+     */
+    function _verifySignatures(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) internal view returns (uint32[] memory validSigningNodeIds_) {
+        bytes32 digest_ = _getPayerReportDigest(
+            originatorNodeId_,
+            attesterNodeId_,
+            startSequenceId_,
+            endSequenceId_,
+            payersMerkleRoot_,
+            nodeOperators_
+        );
+
+        // Need to determine how many of the signatures are valid and provided by canonical node operators.
+        uint8 validSignatureCount_ = 0;
+
+        // This array will help in constructing the `validSigningNodeIds_` array, who's length may be less than
+        // the length of the `signatures_` array if some of the signatures are invalid.
+        bool[] memory isValid_ = new bool[](signatures_.length);
+
+        for (uint256 index_; index_ < signatures_.length; ++index_) {
+            uint32 nodeId_ = signatures_[index_].nodeId;
+
+            // Enforces that the signing node IDs are ordered and unique.
+            if (index_ != 0 && nodeId_ <= signatures_[index_ - 1].nodeId) revert UnorderedNodeIds();
+
+            // If the signature is invalid, ignore it.
+            if (!(isValid_[index_] = _verifySignature(digest_, nodeId_, signatures_[index_].signature))) continue;
+
+            ++validSignatureCount_;
+        }
+
+        uint8 requiredSignatureCount_ = (INodeRegistryLike(nodeRegistry).canonicalNodesCount() / 2) + 1;
+
path: src/settlement-chain/PayerReportManager.sol
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Compilation fails: implicit down-cast from `uint256` to `uint8`**

Solidity ≥0.8 forbids implicit truncating conversions.  
`canonicalNodesCount()` returns a `uint256`, so the assignment to a `uint8` variable does not compile.

```diff
-        uint8 requiredSignatureCount_ = (INodeRegistryLike(nodeRegistry).canonicalNodesCount() / 2) + 1;
+        uint8 requiredSignatureCount_ =
+            uint8((INodeRegistryLike(nodeRegistry).canonicalNodesCount() / 2) + 1);
```

(Alternatively keep it `uint256` everywhere and down-cast only at the comparison site.)

Without this fix the whole contract cannot be deployed or tested.  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        // Previously:
-       uint8 requiredSignatureCount_ = (INodeRegistryLike(nodeRegistry).canonicalNodesCount() / 2) + 1;
+        uint8 requiredSignatureCount_ =
+            uint8((INodeRegistryLike(nodeRegistry).canonicalNodesCount() / 2) + 1);
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-06T08:59:47Z
author_association: NONE
diff_hunk: @@ -0,0 +1,100 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { IERC712 } from "./interfaces/IERC712.sol";
+
+/**
+ * @title An abstract implementation of EIP-712 for typed structured data hashing and signing.
+ */
+abstract contract ERC712 is IERC712 {
+    /* ============ Variables ============ */
+
+    /// @dev keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
+    bytes32 internal constant _EIP712_DOMAIN_HASH = 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f;
+
+    /// @dev Initial Chain ID set at deployment.
+    uint256 internal immutable _initialChainId;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.ERC712
+     * @notice The UUPS storage for the ERC712 contract.
+     * @param  initialDomainSeparator Initial EIP-712 domain separator set at intialization.
+     */
+    struct ERC712Storage {
+        bytes32 initialDomainSeparator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.ERC712")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _ERC712_STORAGE_LOCATION =
+        0xc7effa11ad597798220888e5d1ba4eeddcc8c2635d01dae8b9f958ac905c1100;
+
+    function _getERC712Storage() internal pure returns (ERC712Storage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _ERC712_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructs the EIP-712 domain separator.
+     */
+    constructor() {
+        _initialChainId = block.chainid;
+    }
+
+    /* ============ Initialization ============ */
+
+    function _initializeERC712() internal {
+        _getERC712Storage().initialDomainSeparator = _getDomainSeparator();
+    }
+
+    /* ============ View/Pure Functions ============ */
+
+    /// @inheritdoc IERC712
+    // slither-disable-next-line naming-convention
+    function DOMAIN_SEPARATOR() public view returns (bytes32 domainSeparator_) {
+        return block.chainid == _initialChainId ? _getERC712Storage().initialDomainSeparator : _getDomainSeparator();
+    }
+
path: src/abstract/ERC712.sol
user: login: deluca-mike
body: Events should only be emitted once the underlying actions that caused it have occurred. For example, an upgrade of the contract, or change of version, etc. Not only can we not emit event in view functions, but the changing of the underlying chain id is not something that we can hook into to throw the event off of.
created_at: 2025-05-06T17:12:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,100 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { IERC712 } from "./interfaces/IERC712.sol";
+
+/**
+ * @title An abstract implementation of EIP-712 for typed structured data hashing and signing.
+ */
+abstract contract ERC712 is IERC712 {
+    /* ============ Variables ============ */
+
+    /// @dev keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
+    bytes32 internal constant _EIP712_DOMAIN_HASH = 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f;
+
+    /// @dev Initial Chain ID set at deployment.
+    uint256 internal immutable _initialChainId;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.ERC712
+     * @notice The UUPS storage for the ERC712 contract.
+     * @param  initialDomainSeparator Initial EIP-712 domain separator set at intialization.
+     */
+    struct ERC712Storage {
+        bytes32 initialDomainSeparator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.ERC712")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _ERC712_STORAGE_LOCATION =
+        0xc7effa11ad597798220888e5d1ba4eeddcc8c2635d01dae8b9f958ac905c1100;
+
+    function _getERC712Storage() internal pure returns (ERC712Storage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _ERC712_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructs the EIP-712 domain separator.
+     */
+    constructor() {
+        _initialChainId = block.chainid;
+    }
+
+    /* ============ Initialization ============ */
+
+    function _initializeERC712() internal {
+        _getERC712Storage().initialDomainSeparator = _getDomainSeparator();
+    }
+
+    /* ============ View/Pure Functions ============ */
+
+    /// @inheritdoc IERC712
+    // slither-disable-next-line naming-convention
+    function DOMAIN_SEPARATOR() public view returns (bytes32 domainSeparator_) {
+        return block.chainid == _initialChainId ? _getERC712Storage().initialDomainSeparator : _getDomainSeparator();
+    }
+
path: src/abstract/ERC712.sol
user: login: coderabbitai[bot]
body: @deluca-mike You're absolutely right about both technical points. View functions cannot emit events in Solidity, and there's no mechanism to hook into external chain ID changes directly. The EIP-5267 standard does recommend notifying clients when the domain changes, but the implementation would need to respect Solidity's constraints. Your current approach is technically sound given these limitations.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: deluca-mike
PR: xmtp/smart-contracts#56
File: src/abstract/ERC712.sol:59-62
Timestamp: 2025-05-06T17:13:09.750Z
Learning: In Solidity, view functions cannot emit events as they're read-only and don't modify state.
```

---

```
Learnt from: deluca-mike
PR: xmtp/smart-contracts#56
File: src/abstract/ERC712.sol:59-62
Timestamp: 2025-05-06T17:13:09.750Z
Learning: Chain ID changes (which occur during blockchain forks) cannot be directly detected by smart contracts in real-time - there's no hook mechanism to emit events when this happens.
```

---

```
Learnt from: deluca-mike
PR: xmtp/smart-contracts#56
File: src/abstract/ERC712.sol:59-62
Timestamp: 2025-05-06T17:13:09.750Z
Learning: For ERC712/EIP-712 implementations, while EIP-5267 recommends notifying clients of domain changes, technical constraints may prevent automatic event emission when chain ID changes occur.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@deluca-mike You're absolutely right about both technical points. View functions cannot emit events in Solidity, and there's no mechanism to hook into external chain ID changes directly. The EIP-5267 standard does recommend notifying clients when the domain changes, but the implementation would need to respect Solidity's constraints. Your current approach is technically sound given these limitations.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: deluca-mike
PR: xmtp/smart-contracts#56
File: src/abstract/ERC712.sol:59-62
Timestamp: 2025-05-06T17:13:09.750Z
Learning: In Solidity, view functions cannot emit events as they're read-only and don't modify state.
```

---

```
Learnt from: deluca-mike
PR: xmtp/smart-contracts#56
File: src/abstract/ERC712.sol:59-62
Timestamp: 2025-05-06T17:13:09.750Z
Learning: Chain ID changes (which occur during blockchain forks) cannot be directly detected by smart contracts in real-time - there's no hook mechanism to emit events when this happens.
```

---

```
Learnt from: deluca-mike
PR: xmtp/smart-contracts#56
File: src/abstract/ERC712.sol:59-62
Timestamp: 2025-05-06T17:13:09.750Z
Learning: For ERC712/EIP-712 implementations, while EIP-5267 recommends notifying clients of domain changes, technical constraints may prevent automatic event emission when chain ID changes occur.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-06T17:13:11Z
author_association: NONE
diff_hunk: @@ -0,0 +1,381 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { ECDSA } from "../../lib/solady/src/utils/ECDSA.sol";
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../libraries/SequentialMerkleProofs.sol";
+
+import { IMigratable } from "../abstract/interfaces/IMigratable.sol";
+import { IParameterRegistryLike, INodeRegistryLike, IPayerRegistryLike } from "./interfaces/External.sol";
+import { IPayerReportManager } from "./interfaces/IPayerReportManager.sol";
+
+import { ERC5267 } from "../abstract/ERC5267.sol";
+import { Migratable } from "../abstract/Migratable.sol";
+
+// TODO: If a node operator owns more than one node, their signature for a payer report will be identical, and therefore
+//       replayable across their nodes. This may not be ideal, so it might be necessary to include the node id of the
+//       signer in the digest.
+
+/**
+ * @title  Implementation of the Payer Report Manager.
+ * @notice This contract handles functionality for submitting and settling payer reports.
+ */
+contract PayerReportManager is IPayerReportManager, Initializable, Migratable, ERC5267 {
+    /* ============ Constants/Immutables ============ */
+
+    // solhint-disable-next-line max-line-length
+    /// @dev keccak256("PayerReport(uint256 attesterNodeId,uint256 originatorNodeId,uint256 startSequenceId,uint256 endSequenceId,bytes32 payersMerkleRoot,address[] nodeOperators)")
+    bytes32 public constant PAYER_REPORT_TYPEHASH = 0x73ce31d3128c9ba6af04b3270a898f0abafdf9bed84e47ec7ebd2f3dd8e9c8a5;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable parameterRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable nodeRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable payerRegistry;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.PayerReportManager
+     * @notice The UUPS storage for the payer report manager.
+     * @param  payerReportsByOriginator The mapping of arrays of payer reports by originator node ID.
+     */
+    struct PayerReportManagerStorage {
+        mapping(uint32 originatorId => PayerReport[] payerReports) payerReportsByOriginator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.PayerReportManager")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _PAYER_REPORT_MANAGER_STORAGE_LOCATION =
+        0x26b057ee8e4d60685198828fdf1c618ab8e36b0ab85f54a47b18319f6f718e00;
+
+    function _getPayerReportManagerStorage() internal pure returns (PayerReportManagerStorage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _PAYER_REPORT_MANAGER_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructor for the implementation contract, such that the implementation cannot be initialized.
+     * @param  parameterRegistry_ The address of the parameter registry.
+     * @param  nodeRegistry_      The address of the node registry.
+     * @param  payerRegistry_     The address of the payer registry.
+     * @dev    The parameter registry, node registry, and payer registry must not be the zero address.
+     * @dev    The parameter registry, node registry, and payer registry are immutable so that they are inlined in the
+     *         contract code, and have minimal gas cost.
+     */
+    constructor(address parameterRegistry_, address nodeRegistry_, address payerRegistry_) ERC5267() {
+        require(_isNotZero(parameterRegistry = parameterRegistry_), ZeroParameterRegistry());
+        require(_isNotZero(nodeRegistry = nodeRegistry_), ZeroNodeRegistry());
+        require(_isNotZero(payerRegistry = payerRegistry_), ZeroPayerRegistry());
+
+        _disableInitializers();
+    }
+
+    /* ============ Initialization ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function initialize() public initializer {
+        _initializeERC5267();
+    }
+
+    /* ============ Interactive Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function submit(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) external returns (uint256 payerReportIndex_) {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        payerReportIndex_ = payerReports_.length;
+
+        uint32 lastSequenceId_ = payerReportIndex_ > 0 ? payerReports_[payerReportIndex_ - 1].endSequenceId : 0;
+
+        // Enforces that the start sequence ID is the next expected sequence ID.
+        if (startSequenceId_ != lastSequenceId_ + 1) {
+            revert InvalidStartSequenceId(startSequenceId_, lastSequenceId_ + 1);
+        }
+
+        // Enforces that the end sequence ID is greater than or equal to the start sequence ID.
+        if (endSequenceId_ < startSequenceId_) revert InvalidSequenceIds();
+
+        // Verifies the signatures and gets the array of valid signing node IDs.
+        uint32[] memory validSigningNodeIds_ = _verifySignatures({
+            originatorNodeId_: originatorNodeId_,
+            attesterNodeId_: attesterNodeId_,
+            startSequenceId_: startSequenceId_,
+            endSequenceId_: endSequenceId_,
+            payersMerkleRoot_: payersMerkleRoot_,
+            nodeOperators_: nodeOperators_,
+            signatures_: signatures_
+        });
+
+        payerReports_.push(
+            PayerReport({
+                attesterNodeId: attesterNodeId_,
+                startSequenceId: startSequenceId_,
+                endSequenceId: endSequenceId_,
+                feesSettled: 0,
+                offset: 0,
+                isSettled: false,
+                payersMerkleRoot: payersMerkleRoot_,
+                nodeOperators: nodeOperators_
+            })
+        );
+
+        emit PayerReportSubmitted({
+            originatorNodeId: originatorNodeId_,
+            payerReportIndex: payerReportIndex_,
+            attesterNodeId: attesterNodeId_,
+            startSequenceId: startSequenceId_,
+            endSequenceId: endSequenceId_,
+            payersMerkleRoot: payersMerkleRoot_,
+            nodeOperators: nodeOperators_,
+            signingNodeIds: validSigningNodeIds_
+        });
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function settle(
+        uint32 originatorNodeId_,
+        uint32 payerReportIndex_,
+        bytes[] calldata payerFees_,
+        bytes32[] calldata proofElements_
+    ) external {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        if (payerReportIndex_ >= payerReports_.length) revert PayerReportIndexOutOfBounds();
+
+        PayerReport storage payerReport_ = payerReports_[payerReportIndex_];
+
+        if (payerReport_.isSettled) revert PayerReportEntirelySettled();
+
+        // Verify the payer fees provided are the next sequential payer fees from the last settled offset.
+        SequentialMerkleProofs.verify(payerReport_.payersMerkleRoot, payerReport_.offset, payerFees_, proofElements_);
+
+        payerReport_.offset += uint32(payerFees_.length);
+
+        uint32 leafCount_ = SequentialMerkleProofs.getLeafCount(proofElements_);
+
+        payerReport_.isSettled = leafCount_ == payerReport_.offset;
+
+        // Low level call which handles passing the `payerFees_` arrays as a bytes array that will be automatically
+        // decoded as the required structs by the payer registry's `settleUsage` function.
+        // slither-disable-next-line low-level-calls
+        (bool success_, bytes memory returnData_) = payerRegistry.call(
+            abi.encodeWithSelector(IPayerRegistryLike.settleUsage.selector, payerFees_)
+        );
+
+        if (!success_) revert SettleUsageFailed(returnData_);
+
+        uint96 feesSettled_ = abi.decode(returnData_, (uint96));
+
+        // slither-disable-next-line reentrancy-events
+        emit PayerReportSubsetSettled(
+            originatorNodeId_,
+            payerReportIndex_,
+            uint32(payerFees_.length),
+            leafCount_ - payerReport_.offset,
+            feesSettled_
+        );
+
+        payerReport_.feesSettled += feesSettled_;
+    }
+
+    /// @inheritdoc IMigratable
+    function migrate() external {
+        _migrate(_toAddress(_getRegistryParameter(migratorParameterKey())));
+    }
+
+    /* ============ View/Pure Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function migratorParameterKey() public pure returns (bytes memory key_) {
+        return "xmtp.payerReportManager.migrator";
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReportDigest(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) external view returns (bytes32 digest_) {
+        return
+            _getPayerReportDigest(
+                originatorNodeId_,
+                attesterNodeId_,
+                startSequenceId_,
+                endSequenceId_,
+                payersMerkleRoot_,
+                nodeOperators_
+            );
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReports(uint32 originatorNodeId_) external view returns (PayerReport[] memory payerReports_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_];
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReport(
+        uint32 originatorNodeId_,
+        uint256 payerReportIndex_
+    ) external view returns (PayerReport memory payerReport_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_][payerReportIndex_];
+    }
+
+    /* ============ Internal View/Pure Functions ============ */
+
+    function _name() internal pure override returns (string memory name_) {
+        return "PayerReportManager";
+    }
+
+    function _version() internal pure override returns (string memory version_) {
+        return "1";
+    }
+
+    /**
+     * @dev    Returns the EIP-712 digest for a payer report.
+     * @param  originatorNodeId_ The id of the node originator.
+     * @param  attesterNodeId_   The id of the node attester.
+     * @param  startSequenceId_  The start sequence ID.
+     * @param  endSequenceId_    The end sequence ID.
+     * @param  payersMerkleRoot_ The payers merkle root.
+     * @param  nodeOperators_    The active node operators during the reporting period.
+     * @return digest_           The EIP-712 digest.
+     */
+    function _getPayerReportDigest(
+        uint256 originatorNodeId_,
+        uint256 attesterNodeId_,
+        uint256 startSequenceId_,
+        uint256 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) internal view returns (bytes32 digest_) {
+        return
+            _getDigest(
+                keccak256(
+                    abi.encode(
+                        PAYER_REPORT_TYPEHASH,
+                        originatorNodeId_,
+                        attesterNodeId_,
+                        startSequenceId_,
+                        endSequenceId_,
+                        payersMerkleRoot_,
+                        keccak256(abi.encodePacked(nodeOperators_))
+                    )
+                )
+            );
+    }
+
+    function _getRegistryParameter(bytes memory key_) internal view returns (bytes32 value_) {
+        return IParameterRegistryLike(parameterRegistry).get(key_);
+    }
+
+    function _isNotZero(address input_) internal pure returns (bool isNotZero_) {
+        return input_ != address(0);
+    }
+
+    function _toAddress(bytes32 value_) internal pure returns (address address_) {
+        // slither-disable-next-line assembly
+        assembly {
+            address_ := value_
+        }
+    }
+
+    /**
+     * @dev Verifies that enough of the signatures are valid and provided by canonical node operators and returns the
+     *      array of valid signing node IDs.
+     */
+    function _verifySignatures(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) internal view returns (uint32[] memory validSigningNodeIds_) {
+        bytes32 digest_ = _getPayerReportDigest(
+            originatorNodeId_,
+            attesterNodeId_,
+            startSequenceId_,
+            endSequenceId_,
+            payersMerkleRoot_,
+            nodeOperators_
+        );
+
+        // Need to determine how many of the signatures are valid and provided by canonical node operators.
+        uint8 validSignatureCount_ = 0;
+
+        // This array will help in constructing the `validSigningNodeIds_` array, who's length may be less than
+        // the length of the `signatures_` array if some of the signatures are invalid.
+        bool[] memory isValid_ = new bool[](signatures_.length);
+
+        for (uint256 index_; index_ < signatures_.length; ++index_) {
+            uint32 nodeId_ = signatures_[index_].nodeId;
+
+            // Enforces that the signing node IDs are ordered and unique.
+            if (index_ != 0 && nodeId_ <= signatures_[index_ - 1].nodeId) revert UnorderedNodeIds();
+
+            // If the signature is invalid, ignore it.
+            if (!(isValid_[index_] = _verifySignature(digest_, nodeId_, signatures_[index_].signature))) continue;
+
+            ++validSignatureCount_;
+        }
+
+        uint8 requiredSignatureCount_ = (INodeRegistryLike(nodeRegistry).canonicalNodesCount() / 2) + 1;
+
path: src/settlement-chain/PayerReportManager.sol
user: login: deluca-mike
body: `canonicalNodesCount()` returns a `uint8`.
created_at: 2025-05-06T17:13:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,381 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { ECDSA } from "../../lib/solady/src/utils/ECDSA.sol";
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../libraries/SequentialMerkleProofs.sol";
+
+import { IMigratable } from "../abstract/interfaces/IMigratable.sol";
+import { IParameterRegistryLike, INodeRegistryLike, IPayerRegistryLike } from "./interfaces/External.sol";
+import { IPayerReportManager } from "./interfaces/IPayerReportManager.sol";
+
+import { ERC5267 } from "../abstract/ERC5267.sol";
+import { Migratable } from "../abstract/Migratable.sol";
+
+// TODO: If a node operator owns more than one node, their signature for a payer report will be identical, and therefore
+//       replayable across their nodes. This may not be ideal, so it might be necessary to include the node id of the
+//       signer in the digest.
+
+/**
+ * @title  Implementation of the Payer Report Manager.
+ * @notice This contract handles functionality for submitting and settling payer reports.
+ */
+contract PayerReportManager is IPayerReportManager, Initializable, Migratable, ERC5267 {
+    /* ============ Constants/Immutables ============ */
+
+    // solhint-disable-next-line max-line-length
+    /// @dev keccak256("PayerReport(uint256 attesterNodeId,uint256 originatorNodeId,uint256 startSequenceId,uint256 endSequenceId,bytes32 payersMerkleRoot,address[] nodeOperators)")
+    bytes32 public constant PAYER_REPORT_TYPEHASH = 0x73ce31d3128c9ba6af04b3270a898f0abafdf9bed84e47ec7ebd2f3dd8e9c8a5;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable parameterRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable nodeRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable payerRegistry;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.PayerReportManager
+     * @notice The UUPS storage for the payer report manager.
+     * @param  payerReportsByOriginator The mapping of arrays of payer reports by originator node ID.
+     */
+    struct PayerReportManagerStorage {
+        mapping(uint32 originatorId => PayerReport[] payerReports) payerReportsByOriginator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.PayerReportManager")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _PAYER_REPORT_MANAGER_STORAGE_LOCATION =
+        0x26b057ee8e4d60685198828fdf1c618ab8e36b0ab85f54a47b18319f6f718e00;
+
+    function _getPayerReportManagerStorage() internal pure returns (PayerReportManagerStorage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _PAYER_REPORT_MANAGER_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructor for the implementation contract, such that the implementation cannot be initialized.
+     * @param  parameterRegistry_ The address of the parameter registry.
+     * @param  nodeRegistry_      The address of the node registry.
+     * @param  payerRegistry_     The address of the payer registry.
+     * @dev    The parameter registry, node registry, and payer registry must not be the zero address.
+     * @dev    The parameter registry, node registry, and payer registry are immutable so that they are inlined in the
+     *         contract code, and have minimal gas cost.
+     */
+    constructor(address parameterRegistry_, address nodeRegistry_, address payerRegistry_) ERC5267() {
+        require(_isNotZero(parameterRegistry = parameterRegistry_), ZeroParameterRegistry());
+        require(_isNotZero(nodeRegistry = nodeRegistry_), ZeroNodeRegistry());
+        require(_isNotZero(payerRegistry = payerRegistry_), ZeroPayerRegistry());
+
+        _disableInitializers();
+    }
+
+    /* ============ Initialization ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function initialize() public initializer {
+        _initializeERC5267();
+    }
+
+    /* ============ Interactive Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function submit(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) external returns (uint256 payerReportIndex_) {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        payerReportIndex_ = payerReports_.length;
+
+        uint32 lastSequenceId_ = payerReportIndex_ > 0 ? payerReports_[payerReportIndex_ - 1].endSequenceId : 0;
+
+        // Enforces that the start sequence ID is the next expected sequence ID.
+        if (startSequenceId_ != lastSequenceId_ + 1) {
+            revert InvalidStartSequenceId(startSequenceId_, lastSequenceId_ + 1);
+        }
+
+        // Enforces that the end sequence ID is greater than or equal to the start sequence ID.
+        if (endSequenceId_ < startSequenceId_) revert InvalidSequenceIds();
+
+        // Verifies the signatures and gets the array of valid signing node IDs.
+        uint32[] memory validSigningNodeIds_ = _verifySignatures({
+            originatorNodeId_: originatorNodeId_,
+            attesterNodeId_: attesterNodeId_,
+            startSequenceId_: startSequenceId_,
+            endSequenceId_: endSequenceId_,
+            payersMerkleRoot_: payersMerkleRoot_,
+            nodeOperators_: nodeOperators_,
+            signatures_: signatures_
+        });
+
+        payerReports_.push(
+            PayerReport({
+                attesterNodeId: attesterNodeId_,
+                startSequenceId: startSequenceId_,
+                endSequenceId: endSequenceId_,
+                feesSettled: 0,
+                offset: 0,
+                isSettled: false,
+                payersMerkleRoot: payersMerkleRoot_,
+                nodeOperators: nodeOperators_
+            })
+        );
+
+        emit PayerReportSubmitted({
+            originatorNodeId: originatorNodeId_,
+            payerReportIndex: payerReportIndex_,
+            attesterNodeId: attesterNodeId_,
+            startSequenceId: startSequenceId_,
+            endSequenceId: endSequenceId_,
+            payersMerkleRoot: payersMerkleRoot_,
+            nodeOperators: nodeOperators_,
+            signingNodeIds: validSigningNodeIds_
+        });
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function settle(
+        uint32 originatorNodeId_,
+        uint32 payerReportIndex_,
+        bytes[] calldata payerFees_,
+        bytes32[] calldata proofElements_
+    ) external {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        if (payerReportIndex_ >= payerReports_.length) revert PayerReportIndexOutOfBounds();
+
+        PayerReport storage payerReport_ = payerReports_[payerReportIndex_];
+
+        if (payerReport_.isSettled) revert PayerReportEntirelySettled();
+
+        // Verify the payer fees provided are the next sequential payer fees from the last settled offset.
+        SequentialMerkleProofs.verify(payerReport_.payersMerkleRoot, payerReport_.offset, payerFees_, proofElements_);
+
+        payerReport_.offset += uint32(payerFees_.length);
+
+        uint32 leafCount_ = SequentialMerkleProofs.getLeafCount(proofElements_);
+
+        payerReport_.isSettled = leafCount_ == payerReport_.offset;
+
+        // Low level call which handles passing the `payerFees_` arrays as a bytes array that will be automatically
+        // decoded as the required structs by the payer registry's `settleUsage` function.
+        // slither-disable-next-line low-level-calls
+        (bool success_, bytes memory returnData_) = payerRegistry.call(
+            abi.encodeWithSelector(IPayerRegistryLike.settleUsage.selector, payerFees_)
+        );
+
+        if (!success_) revert SettleUsageFailed(returnData_);
+
+        uint96 feesSettled_ = abi.decode(returnData_, (uint96));
+
+        // slither-disable-next-line reentrancy-events
+        emit PayerReportSubsetSettled(
+            originatorNodeId_,
+            payerReportIndex_,
+            uint32(payerFees_.length),
+            leafCount_ - payerReport_.offset,
+            feesSettled_
+        );
+
+        payerReport_.feesSettled += feesSettled_;
+    }
+
+    /// @inheritdoc IMigratable
+    function migrate() external {
+        _migrate(_toAddress(_getRegistryParameter(migratorParameterKey())));
+    }
+
+    /* ============ View/Pure Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function migratorParameterKey() public pure returns (bytes memory key_) {
+        return "xmtp.payerReportManager.migrator";
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReportDigest(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) external view returns (bytes32 digest_) {
+        return
+            _getPayerReportDigest(
+                originatorNodeId_,
+                attesterNodeId_,
+                startSequenceId_,
+                endSequenceId_,
+                payersMerkleRoot_,
+                nodeOperators_
+            );
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReports(uint32 originatorNodeId_) external view returns (PayerReport[] memory payerReports_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_];
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReport(
+        uint32 originatorNodeId_,
+        uint256 payerReportIndex_
+    ) external view returns (PayerReport memory payerReport_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_][payerReportIndex_];
+    }
+
+    /* ============ Internal View/Pure Functions ============ */
+
+    function _name() internal pure override returns (string memory name_) {
+        return "PayerReportManager";
+    }
+
+    function _version() internal pure override returns (string memory version_) {
+        return "1";
+    }
+
+    /**
+     * @dev    Returns the EIP-712 digest for a payer report.
+     * @param  originatorNodeId_ The id of the node originator.
+     * @param  attesterNodeId_   The id of the node attester.
+     * @param  startSequenceId_  The start sequence ID.
+     * @param  endSequenceId_    The end sequence ID.
+     * @param  payersMerkleRoot_ The payers merkle root.
+     * @param  nodeOperators_    The active node operators during the reporting period.
+     * @return digest_           The EIP-712 digest.
+     */
+    function _getPayerReportDigest(
+        uint256 originatorNodeId_,
+        uint256 attesterNodeId_,
+        uint256 startSequenceId_,
+        uint256 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) internal view returns (bytes32 digest_) {
+        return
+            _getDigest(
+                keccak256(
+                    abi.encode(
+                        PAYER_REPORT_TYPEHASH,
+                        originatorNodeId_,
+                        attesterNodeId_,
+                        startSequenceId_,
+                        endSequenceId_,
+                        payersMerkleRoot_,
+                        keccak256(abi.encodePacked(nodeOperators_))
+                    )
+                )
+            );
+    }
+
+    function _getRegistryParameter(bytes memory key_) internal view returns (bytes32 value_) {
+        return IParameterRegistryLike(parameterRegistry).get(key_);
+    }
+
+    function _isNotZero(address input_) internal pure returns (bool isNotZero_) {
+        return input_ != address(0);
+    }
+
+    function _toAddress(bytes32 value_) internal pure returns (address address_) {
+        // slither-disable-next-line assembly
+        assembly {
+            address_ := value_
+        }
+    }
+
+    /**
+     * @dev Verifies that enough of the signatures are valid and provided by canonical node operators and returns the
+     *      array of valid signing node IDs.
+     */
+    function _verifySignatures(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) internal view returns (uint32[] memory validSigningNodeIds_) {
+        bytes32 digest_ = _getPayerReportDigest(
+            originatorNodeId_,
+            attesterNodeId_,
+            startSequenceId_,
+            endSequenceId_,
+            payersMerkleRoot_,
+            nodeOperators_
+        );
+
+        // Need to determine how many of the signatures are valid and provided by canonical node operators.
+        uint8 validSignatureCount_ = 0;
+
+        // This array will help in constructing the `validSigningNodeIds_` array, who's length may be less than
+        // the length of the `signatures_` array if some of the signatures are invalid.
+        bool[] memory isValid_ = new bool[](signatures_.length);
+
+        for (uint256 index_; index_ < signatures_.length; ++index_) {
+            uint32 nodeId_ = signatures_[index_].nodeId;
+
+            // Enforces that the signing node IDs are ordered and unique.
+            if (index_ != 0 && nodeId_ <= signatures_[index_ - 1].nodeId) revert UnorderedNodeIds();
+
+            // If the signature is invalid, ignore it.
+            if (!(isValid_[index_] = _verifySignature(digest_, nodeId_, signatures_[index_].signature))) continue;
+
+            ++validSignatureCount_;
+        }
+
+        uint8 requiredSignatureCount_ = (INodeRegistryLike(nodeRegistry).canonicalNodesCount() / 2) + 1;
+
path: src/settlement-chain/PayerReportManager.sol
user: login: coderabbitai[bot]
body: @deluca-mike You're absolutely right. Looking at the `INodeRegistryLike` interface in `src/settlement-chain/interfaces/External.sol`, the `canonicalNodesCount()` function indeed returns a `uint8`:

```solidity
function canonicalNodesCount() external view returns (uint8 canonicalNodesCount_);
```

Since the function already returns a `uint8`, no explicit casting is needed in your code. The bot's suggestion was incorrect in this case. Thank you for the clarification!





---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: deluca-mike
PR: xmtp/smart-contracts#56
File: src/settlement-chain/PayerReportManager.sol:346-347
Timestamp: 2025-05-06T17:13:52.649Z
Learning: The `canonicalNodesCount()` function in `INodeRegistryLike` returns a `uint8`, not a `uint256`, so no explicit casting is needed when assigning its value to a `uint8` variable.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

---

<!-- This is an auto-generated comment: tweet message by coderabbit.ai -->

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Avoid%20Solidity%20compile%20errors%20by%20explicitly%20casting%20uint256%20to%20uint8%20when%20down-casting!%20%40coderabbitai%20spots%20critical%20type%20issues%20that%20block%20deployment.%20Try%20it%20for%20flawless%20builds!%20%E2%9A%99%EF%B8%8F%F0%9F%90%87&url=https%3A%2F%2Fgithub.com%2Fxmtp%2Fsmart-contracts%2Fpull%2F56%23discussion_r2075036137)?

Thank you for using CodeRabbit!

<!-- end of auto-generated comment: tweet message by coderabbit.ai -->




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@deluca-mike You're absolutely right. Looking at the `INodeRegistryLike` interface in `src/settlement-chain/interfaces/External.sol`, the `canonicalNodesCount()` function indeed returns a `uint8`:

```solidity
function canonicalNodesCount() external view returns (uint8 canonicalNodesCount_);
```

Since the function already returns a `uint8`, no explicit casting is needed in your code. The bot's suggestion was incorrect in this case. Thank you for the clarification!





---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: deluca-mike
PR: xmtp/smart-contracts#56
File: src/settlement-chain/PayerReportManager.sol:346-347
Timestamp: 2025-05-06T17:13:52.649Z
Learning: The `canonicalNodesCount()` function in `INodeRegistryLike` returns a `uint8`, not a `uint256`, so no explicit casting is needed when assigning its value to a `uint8` variable.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

---

&lt;!-- This is an auto-generated comment: tweet message by coderabbit.ai --&gt;

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Avoid%20Solidity%20compile%20errors%20by%20explicitly%20casting%20uint256%20to%20uint8%20when%20down-casting!%20%40coderabbitai%20spots%20critical%20type%20issues%20that%20block%20deployment.%20Try%20it%20for%20flawless%20builds!%20%E2%9A%99%EF%B8%8F%F0%9F%90%87&url=https%3A%2F%2Fgithub.com%2Fxmtp%2Fsmart-contracts%2Fpull%2F56%23discussion_r2075036137)?

Thank you for using CodeRabbit!

&lt;!-- end of auto-generated comment: tweet message by coderabbit.ai --&gt;

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-06T17:13:56Z
author_association: NONE
diff_hunk: @@ -0,0 +1,381 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { ECDSA } from "../../lib/solady/src/utils/ECDSA.sol";
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../libraries/SequentialMerkleProofs.sol";
+
+import { IMigratable } from "../abstract/interfaces/IMigratable.sol";
+import { IParameterRegistryLike, INodeRegistryLike, IPayerRegistryLike } from "./interfaces/External.sol";
+import { IPayerReportManager } from "./interfaces/IPayerReportManager.sol";
+
+import { ERC5267 } from "../abstract/ERC5267.sol";
+import { Migratable } from "../abstract/Migratable.sol";
+
+// TODO: If a node operator owns more than one node, their signature for a payer report will be identical, and therefore
+//       replayable across their nodes. This may not be ideal, so it might be necessary to include the node id of the
+//       signer in the digest.
+
+/**
+ * @title  Implementation of the Payer Report Manager.
+ * @notice This contract handles functionality for submitting and settling payer reports.
+ */
+contract PayerReportManager is IPayerReportManager, Initializable, Migratable, ERC5267 {
+    /* ============ Constants/Immutables ============ */
+
+    // solhint-disable-next-line max-line-length
+    /// @dev keccak256("PayerReport(uint256 attesterNodeId,uint256 originatorNodeId,uint256 startSequenceId,uint256 endSequenceId,bytes32 payersMerkleRoot,address[] nodeOperators)")
+    bytes32 public constant PAYER_REPORT_TYPEHASH = 0x73ce31d3128c9ba6af04b3270a898f0abafdf9bed84e47ec7ebd2f3dd8e9c8a5;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable parameterRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable nodeRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable payerRegistry;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.PayerReportManager
+     * @notice The UUPS storage for the payer report manager.
+     * @param  payerReportsByOriginator The mapping of arrays of payer reports by originator node ID.
+     */
+    struct PayerReportManagerStorage {
+        mapping(uint32 originatorId => PayerReport[] payerReports) payerReportsByOriginator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.PayerReportManager")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _PAYER_REPORT_MANAGER_STORAGE_LOCATION =
+        0x26b057ee8e4d60685198828fdf1c618ab8e36b0ab85f54a47b18319f6f718e00;
+
+    function _getPayerReportManagerStorage() internal pure returns (PayerReportManagerStorage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _PAYER_REPORT_MANAGER_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructor for the implementation contract, such that the implementation cannot be initialized.
+     * @param  parameterRegistry_ The address of the parameter registry.
+     * @param  nodeRegistry_      The address of the node registry.
+     * @param  payerRegistry_     The address of the payer registry.
+     * @dev    The parameter registry, node registry, and payer registry must not be the zero address.
+     * @dev    The parameter registry, node registry, and payer registry are immutable so that they are inlined in the
+     *         contract code, and have minimal gas cost.
+     */
+    constructor(address parameterRegistry_, address nodeRegistry_, address payerRegistry_) ERC5267() {
+        require(_isNotZero(parameterRegistry = parameterRegistry_), ZeroParameterRegistry());
+        require(_isNotZero(nodeRegistry = nodeRegistry_), ZeroNodeRegistry());
+        require(_isNotZero(payerRegistry = payerRegistry_), ZeroPayerRegistry());
+
+        _disableInitializers();
+    }
+
+    /* ============ Initialization ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function initialize() public initializer {
+        _initializeERC5267();
+    }
+
+    /* ============ Interactive Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function submit(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) external returns (uint256 payerReportIndex_) {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        payerReportIndex_ = payerReports_.length;
+
+        uint32 lastSequenceId_ = payerReportIndex_ > 0 ? payerReports_[payerReportIndex_ - 1].endSequenceId : 0;
+
+        // Enforces that the start sequence ID is the next expected sequence ID.
+        if (startSequenceId_ != lastSequenceId_ + 1) {
path: src/settlement-chain/PayerReportManager.sol
user: login: neekolas
body: The way we are handling this on the nodes is that the `startSequenceID` is _equal_ to the end sequence ID of the last report. That way we can handle the rare, but possible, case where an originator is missing a sequence ID one above the end of the last report.
created_at: 2025-05-06T18:51:27Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,381 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { ECDSA } from "../../lib/solady/src/utils/ECDSA.sol";
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../libraries/SequentialMerkleProofs.sol";
+
+import { IMigratable } from "../abstract/interfaces/IMigratable.sol";
+import { IParameterRegistryLike, INodeRegistryLike, IPayerRegistryLike } from "./interfaces/External.sol";
+import { IPayerReportManager } from "./interfaces/IPayerReportManager.sol";
+
+import { ERC5267 } from "../abstract/ERC5267.sol";
+import { Migratable } from "../abstract/Migratable.sol";
+
+// TODO: If a node operator owns more than one node, their signature for a payer report will be identical, and therefore
+//       replayable across their nodes. This may not be ideal, so it might be necessary to include the node id of the
+//       signer in the digest.
+
+/**
+ * @title  Implementation of the Payer Report Manager.
+ * @notice This contract handles functionality for submitting and settling payer reports.
+ */
+contract PayerReportManager is IPayerReportManager, Initializable, Migratable, ERC5267 {
+    /* ============ Constants/Immutables ============ */
+
+    // solhint-disable-next-line max-line-length
+    /// @dev keccak256("PayerReport(uint256 attesterNodeId,uint256 originatorNodeId,uint256 startSequenceId,uint256 endSequenceId,bytes32 payersMerkleRoot,address[] nodeOperators)")
+    bytes32 public constant PAYER_REPORT_TYPEHASH = 0x73ce31d3128c9ba6af04b3270a898f0abafdf9bed84e47ec7ebd2f3dd8e9c8a5;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable parameterRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable nodeRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable payerRegistry;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.PayerReportManager
+     * @notice The UUPS storage for the payer report manager.
+     * @param  payerReportsByOriginator The mapping of arrays of payer reports by originator node ID.
+     */
+    struct PayerReportManagerStorage {
+        mapping(uint32 originatorId => PayerReport[] payerReports) payerReportsByOriginator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.PayerReportManager")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _PAYER_REPORT_MANAGER_STORAGE_LOCATION =
+        0x26b057ee8e4d60685198828fdf1c618ab8e36b0ab85f54a47b18319f6f718e00;
+
+    function _getPayerReportManagerStorage() internal pure returns (PayerReportManagerStorage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _PAYER_REPORT_MANAGER_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructor for the implementation contract, such that the implementation cannot be initialized.
+     * @param  parameterRegistry_ The address of the parameter registry.
+     * @param  nodeRegistry_      The address of the node registry.
+     * @param  payerRegistry_     The address of the payer registry.
+     * @dev    The parameter registry, node registry, and payer registry must not be the zero address.
+     * @dev    The parameter registry, node registry, and payer registry are immutable so that they are inlined in the
+     *         contract code, and have minimal gas cost.
+     */
+    constructor(address parameterRegistry_, address nodeRegistry_, address payerRegistry_) ERC5267() {
+        require(_isNotZero(parameterRegistry = parameterRegistry_), ZeroParameterRegistry());
+        require(_isNotZero(nodeRegistry = nodeRegistry_), ZeroNodeRegistry());
+        require(_isNotZero(payerRegistry = payerRegistry_), ZeroPayerRegistry());
+
+        _disableInitializers();
+    }
+
+    /* ============ Initialization ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function initialize() public initializer {
+        _initializeERC5267();
+    }
+
+    /* ============ Interactive Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function submit(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) external returns (uint256 payerReportIndex_) {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        payerReportIndex_ = payerReports_.length;
+
+        uint32 lastSequenceId_ = payerReportIndex_ > 0 ? payerReports_[payerReportIndex_ - 1].endSequenceId : 0;
+
+        // Enforces that the start sequence ID is the next expected sequence ID.
+        if (startSequenceId_ != lastSequenceId_ + 1) {
+            revert InvalidStartSequenceId(startSequenceId_, lastSequenceId_ + 1);
+        }
+
+        // Enforces that the end sequence ID is greater than or equal to the start sequence ID.
+        if (endSequenceId_ < startSequenceId_) revert InvalidSequenceIds();
+
+        // Verifies the signatures and gets the array of valid signing node IDs.
+        uint32[] memory validSigningNodeIds_ = _verifySignatures({
+            originatorNodeId_: originatorNodeId_,
+            attesterNodeId_: attesterNodeId_,
+            startSequenceId_: startSequenceId_,
+            endSequenceId_: endSequenceId_,
+            payersMerkleRoot_: payersMerkleRoot_,
+            nodeOperators_: nodeOperators_,
+            signatures_: signatures_
+        });
+
+        payerReports_.push(
+            PayerReport({
+                attesterNodeId: attesterNodeId_,
+                startSequenceId: startSequenceId_,
+                endSequenceId: endSequenceId_,
+                feesSettled: 0,
+                offset: 0,
+                isSettled: false,
+                payersMerkleRoot: payersMerkleRoot_,
+                nodeOperators: nodeOperators_
+            })
+        );
+
+        emit PayerReportSubmitted({
+            originatorNodeId: originatorNodeId_,
+            payerReportIndex: payerReportIndex_,
+            attesterNodeId: attesterNodeId_,
+            startSequenceId: startSequenceId_,
+            endSequenceId: endSequenceId_,
+            payersMerkleRoot: payersMerkleRoot_,
+            nodeOperators: nodeOperators_,
+            signingNodeIds: validSigningNodeIds_
+        });
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function settle(
+        uint32 originatorNodeId_,
+        uint32 payerReportIndex_,
+        bytes[] calldata payerFees_,
+        bytes32[] calldata proofElements_
+    ) external {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        if (payerReportIndex_ >= payerReports_.length) revert PayerReportIndexOutOfBounds();
+
+        PayerReport storage payerReport_ = payerReports_[payerReportIndex_];
+
+        if (payerReport_.isSettled) revert PayerReportEntirelySettled();
+
+        // Verify the payer fees provided are the next sequential payer fees from the last settled offset.
+        SequentialMerkleProofs.verify(payerReport_.payersMerkleRoot, payerReport_.offset, payerFees_, proofElements_);
+
+        payerReport_.offset += uint32(payerFees_.length);
+
+        uint32 leafCount_ = SequentialMerkleProofs.getLeafCount(proofElements_);
+
+        payerReport_.isSettled = leafCount_ == payerReport_.offset;
+
+        // Low level call which handles passing the `payerFees_` arrays as a bytes array that will be automatically
+        // decoded as the required structs by the payer registry's `settleUsage` function.
+        // slither-disable-next-line low-level-calls
+        (bool success_, bytes memory returnData_) = payerRegistry.call(
+            abi.encodeWithSelector(IPayerRegistryLike.settleUsage.selector, payerFees_)
+        );
+
+        if (!success_) revert SettleUsageFailed(returnData_);
+
+        uint96 feesSettled_ = abi.decode(returnData_, (uint96));
+
+        // slither-disable-next-line reentrancy-events
+        emit PayerReportSubsetSettled(
+            originatorNodeId_,
+            payerReportIndex_,
+            uint32(payerFees_.length),
+            leafCount_ - payerReport_.offset,
+            feesSettled_
+        );
+
+        payerReport_.feesSettled += feesSettled_;
+    }
+
+    /// @inheritdoc IMigratable
+    function migrate() external {
+        _migrate(_toAddress(_getRegistryParameter(migratorParameterKey())));
+    }
+
+    /* ============ View/Pure Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function migratorParameterKey() public pure returns (bytes memory key_) {
+        return "xmtp.payerReportManager.migrator";
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReportDigest(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) external view returns (bytes32 digest_) {
+        return
+            _getPayerReportDigest(
+                originatorNodeId_,
+                attesterNodeId_,
+                startSequenceId_,
+                endSequenceId_,
+                payersMerkleRoot_,
+                nodeOperators_
+            );
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReports(uint32 originatorNodeId_) external view returns (PayerReport[] memory payerReports_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_];
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReport(
+        uint32 originatorNodeId_,
+        uint256 payerReportIndex_
+    ) external view returns (PayerReport memory payerReport_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_][payerReportIndex_];
+    }
+
+    /* ============ Internal View/Pure Functions ============ */
+
+    function _name() internal pure override returns (string memory name_) {
+        return "PayerReportManager";
+    }
+
+    function _version() internal pure override returns (string memory version_) {
+        return "1";
+    }
+
+    /**
+     * @dev    Returns the EIP-712 digest for a payer report.
+     * @param  originatorNodeId_ The id of the node originator.
+     * @param  attesterNodeId_   The id of the node attester.
+     * @param  startSequenceId_  The start sequence ID.
+     * @param  endSequenceId_    The end sequence ID.
+     * @param  payersMerkleRoot_ The payers merkle root.
+     * @param  nodeOperators_    The active node operators during the reporting period.
+     * @return digest_           The EIP-712 digest.
+     */
+    function _getPayerReportDigest(
+        uint256 originatorNodeId_,
+        uint256 attesterNodeId_,
+        uint256 startSequenceId_,
+        uint256 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) internal view returns (bytes32 digest_) {
+        return
+            _getDigest(
+                keccak256(
+                    abi.encode(
+                        PAYER_REPORT_TYPEHASH,
+                        originatorNodeId_,
+                        attesterNodeId_,
+                        startSequenceId_,
+                        endSequenceId_,
+                        payersMerkleRoot_,
+                        keccak256(abi.encodePacked(nodeOperators_))
+                    )
+                )
+            );
+    }
+
+    function _getRegistryParameter(bytes memory key_) internal view returns (bytes32 value_) {
+        return IParameterRegistryLike(parameterRegistry).get(key_);
+    }
+
+    function _isNotZero(address input_) internal pure returns (bool isNotZero_) {
+        return input_ != address(0);
+    }
+
+    function _toAddress(bytes32 value_) internal pure returns (address address_) {
+        // slither-disable-next-line assembly
+        assembly {
+            address_ := value_
+        }
+    }
+
+    /**
+     * @dev Verifies that enough of the signatures are valid and provided by canonical node operators and returns the
+     *      array of valid signing node IDs.
+     */
+    function _verifySignatures(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
path: src/settlement-chain/PayerReportManager.sol
user: login: neekolas
body: I am not sure who the attester is here. All the signatures are from people attesting to the report. I'm not sure if we care who created it or who submitted it, so long as it is valid. 
created_at: 2025-05-06T19:30:44Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,381 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { ECDSA } from "../../lib/solady/src/utils/ECDSA.sol";
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../libraries/SequentialMerkleProofs.sol";
+
+import { IMigratable } from "../abstract/interfaces/IMigratable.sol";
+import { IParameterRegistryLike, INodeRegistryLike, IPayerRegistryLike } from "./interfaces/External.sol";
+import { IPayerReportManager } from "./interfaces/IPayerReportManager.sol";
+
+import { ERC5267 } from "../abstract/ERC5267.sol";
+import { Migratable } from "../abstract/Migratable.sol";
+
+// TODO: If a node operator owns more than one node, their signature for a payer report will be identical, and therefore
+//       replayable across their nodes. This may not be ideal, so it might be necessary to include the node id of the
+//       signer in the digest.
+
+/**
+ * @title  Implementation of the Payer Report Manager.
+ * @notice This contract handles functionality for submitting and settling payer reports.
+ */
+contract PayerReportManager is IPayerReportManager, Initializable, Migratable, ERC5267 {
+    /* ============ Constants/Immutables ============ */
+
+    // solhint-disable-next-line max-line-length
+    /// @dev keccak256("PayerReport(uint256 attesterNodeId,uint256 originatorNodeId,uint256 startSequenceId,uint256 endSequenceId,bytes32 payersMerkleRoot,address[] nodeOperators)")
+    bytes32 public constant PAYER_REPORT_TYPEHASH = 0x73ce31d3128c9ba6af04b3270a898f0abafdf9bed84e47ec7ebd2f3dd8e9c8a5;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable parameterRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable nodeRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable payerRegistry;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.PayerReportManager
+     * @notice The UUPS storage for the payer report manager.
+     * @param  payerReportsByOriginator The mapping of arrays of payer reports by originator node ID.
+     */
+    struct PayerReportManagerStorage {
+        mapping(uint32 originatorId => PayerReport[] payerReports) payerReportsByOriginator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.PayerReportManager")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _PAYER_REPORT_MANAGER_STORAGE_LOCATION =
+        0x26b057ee8e4d60685198828fdf1c618ab8e36b0ab85f54a47b18319f6f718e00;
+
+    function _getPayerReportManagerStorage() internal pure returns (PayerReportManagerStorage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _PAYER_REPORT_MANAGER_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructor for the implementation contract, such that the implementation cannot be initialized.
+     * @param  parameterRegistry_ The address of the parameter registry.
+     * @param  nodeRegistry_      The address of the node registry.
+     * @param  payerRegistry_     The address of the payer registry.
+     * @dev    The parameter registry, node registry, and payer registry must not be the zero address.
+     * @dev    The parameter registry, node registry, and payer registry are immutable so that they are inlined in the
+     *         contract code, and have minimal gas cost.
+     */
+    constructor(address parameterRegistry_, address nodeRegistry_, address payerRegistry_) ERC5267() {
+        require(_isNotZero(parameterRegistry = parameterRegistry_), ZeroParameterRegistry());
+        require(_isNotZero(nodeRegistry = nodeRegistry_), ZeroNodeRegistry());
+        require(_isNotZero(payerRegistry = payerRegistry_), ZeroPayerRegistry());
+
+        _disableInitializers();
+    }
+
+    /* ============ Initialization ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function initialize() public initializer {
+        _initializeERC5267();
+    }
+
+    /* ============ Interactive Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function submit(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) external returns (uint256 payerReportIndex_) {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        payerReportIndex_ = payerReports_.length;
+
+        uint32 lastSequenceId_ = payerReportIndex_ > 0 ? payerReports_[payerReportIndex_ - 1].endSequenceId : 0;
+
+        // Enforces that the start sequence ID is the next expected sequence ID.
+        if (startSequenceId_ != lastSequenceId_ + 1) {
+            revert InvalidStartSequenceId(startSequenceId_, lastSequenceId_ + 1);
+        }
+
+        // Enforces that the end sequence ID is greater than or equal to the start sequence ID.
+        if (endSequenceId_ < startSequenceId_) revert InvalidSequenceIds();
+
+        // Verifies the signatures and gets the array of valid signing node IDs.
+        uint32[] memory validSigningNodeIds_ = _verifySignatures({
+            originatorNodeId_: originatorNodeId_,
+            attesterNodeId_: attesterNodeId_,
+            startSequenceId_: startSequenceId_,
+            endSequenceId_: endSequenceId_,
+            payersMerkleRoot_: payersMerkleRoot_,
+            nodeOperators_: nodeOperators_,
+            signatures_: signatures_
+        });
+
+        payerReports_.push(
+            PayerReport({
+                attesterNodeId: attesterNodeId_,
+                startSequenceId: startSequenceId_,
+                endSequenceId: endSequenceId_,
+                feesSettled: 0,
+                offset: 0,
+                isSettled: false,
+                payersMerkleRoot: payersMerkleRoot_,
+                nodeOperators: nodeOperators_
+            })
+        );
+
+        emit PayerReportSubmitted({
+            originatorNodeId: originatorNodeId_,
+            payerReportIndex: payerReportIndex_,
+            attesterNodeId: attesterNodeId_,
+            startSequenceId: startSequenceId_,
+            endSequenceId: endSequenceId_,
+            payersMerkleRoot: payersMerkleRoot_,
+            nodeOperators: nodeOperators_,
+            signingNodeIds: validSigningNodeIds_
+        });
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function settle(
+        uint32 originatorNodeId_,
+        uint32 payerReportIndex_,
+        bytes[] calldata payerFees_,
+        bytes32[] calldata proofElements_
+    ) external {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        if (payerReportIndex_ >= payerReports_.length) revert PayerReportIndexOutOfBounds();
+
+        PayerReport storage payerReport_ = payerReports_[payerReportIndex_];
+
+        if (payerReport_.isSettled) revert PayerReportEntirelySettled();
+
+        // Verify the payer fees provided are the next sequential payer fees from the last settled offset.
+        SequentialMerkleProofs.verify(payerReport_.payersMerkleRoot, payerReport_.offset, payerFees_, proofElements_);
+
+        payerReport_.offset += uint32(payerFees_.length);
+
+        uint32 leafCount_ = SequentialMerkleProofs.getLeafCount(proofElements_);
+
+        payerReport_.isSettled = leafCount_ == payerReport_.offset;
+
+        // Low level call which handles passing the `payerFees_` arrays as a bytes array that will be automatically
+        // decoded as the required structs by the payer registry's `settleUsage` function.
+        // slither-disable-next-line low-level-calls
+        (bool success_, bytes memory returnData_) = payerRegistry.call(
+            abi.encodeWithSelector(IPayerRegistryLike.settleUsage.selector, payerFees_)
+        );
+
+        if (!success_) revert SettleUsageFailed(returnData_);
+
+        uint96 feesSettled_ = abi.decode(returnData_, (uint96));
+
+        // slither-disable-next-line reentrancy-events
+        emit PayerReportSubsetSettled(
+            originatorNodeId_,
+            payerReportIndex_,
+            uint32(payerFees_.length),
+            leafCount_ - payerReport_.offset,
+            feesSettled_
+        );
+
+        payerReport_.feesSettled += feesSettled_;
+    }
+
+    /// @inheritdoc IMigratable
+    function migrate() external {
+        _migrate(_toAddress(_getRegistryParameter(migratorParameterKey())));
+    }
+
+    /* ============ View/Pure Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function migratorParameterKey() public pure returns (bytes memory key_) {
+        return "xmtp.payerReportManager.migrator";
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReportDigest(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) external view returns (bytes32 digest_) {
+        return
+            _getPayerReportDigest(
+                originatorNodeId_,
+                attesterNodeId_,
+                startSequenceId_,
+                endSequenceId_,
+                payersMerkleRoot_,
+                nodeOperators_
+            );
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReports(uint32 originatorNodeId_) external view returns (PayerReport[] memory payerReports_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_];
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReport(
+        uint32 originatorNodeId_,
+        uint256 payerReportIndex_
+    ) external view returns (PayerReport memory payerReport_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_][payerReportIndex_];
+    }
+
+    /* ============ Internal View/Pure Functions ============ */
+
+    function _name() internal pure override returns (string memory name_) {
+        return "PayerReportManager";
+    }
+
+    function _version() internal pure override returns (string memory version_) {
+        return "1";
+    }
+
+    /**
+     * @dev    Returns the EIP-712 digest for a payer report.
+     * @param  originatorNodeId_ The id of the node originator.
+     * @param  attesterNodeId_   The id of the node attester.
+     * @param  startSequenceId_  The start sequence ID.
+     * @param  endSequenceId_    The end sequence ID.
+     * @param  payersMerkleRoot_ The payers merkle root.
+     * @param  nodeOperators_    The active node operators during the reporting period.
+     * @return digest_           The EIP-712 digest.
+     */
+    function _getPayerReportDigest(
+        uint256 originatorNodeId_,
+        uint256 attesterNodeId_,
+        uint256 startSequenceId_,
+        uint256 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) internal view returns (bytes32 digest_) {
+        return
+            _getDigest(
+                keccak256(
+                    abi.encode(
+                        PAYER_REPORT_TYPEHASH,
+                        originatorNodeId_,
+                        attesterNodeId_,
+                        startSequenceId_,
+                        endSequenceId_,
+                        payersMerkleRoot_,
+                        keccak256(abi.encodePacked(nodeOperators_))
+                    )
+                )
+            );
+    }
+
+    function _getRegistryParameter(bytes memory key_) internal view returns (bytes32 value_) {
+        return IParameterRegistryLike(parameterRegistry).get(key_);
+    }
+
+    function _isNotZero(address input_) internal pure returns (bool isNotZero_) {
+        return input_ != address(0);
+    }
+
+    function _toAddress(bytes32 value_) internal pure returns (address address_) {
+        // slither-disable-next-line assembly
+        assembly {
+            address_ := value_
+        }
+    }
+
+    /**
+     * @dev Verifies that enough of the signatures are valid and provided by canonical node operators and returns the
+     *      array of valid signing node IDs.
+     */
+    function _verifySignatures(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
path: src/settlement-chain/PayerReportManager.sol
user: login: neekolas
body: I think we should ditch the field if we can't justify its existence.
created_at: 2025-05-06T19:35:43Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,381 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { ECDSA } from "../../lib/solady/src/utils/ECDSA.sol";
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../libraries/SequentialMerkleProofs.sol";
+
+import { IMigratable } from "../abstract/interfaces/IMigratable.sol";
+import { IParameterRegistryLike, INodeRegistryLike, IPayerRegistryLike } from "./interfaces/External.sol";
+import { IPayerReportManager } from "./interfaces/IPayerReportManager.sol";
+
+import { ERC5267 } from "../abstract/ERC5267.sol";
+import { Migratable } from "../abstract/Migratable.sol";
+
+// TODO: If a node operator owns more than one node, their signature for a payer report will be identical, and therefore
+//       replayable across their nodes. This may not be ideal, so it might be necessary to include the node id of the
+//       signer in the digest.
+
+/**
+ * @title  Implementation of the Payer Report Manager.
+ * @notice This contract handles functionality for submitting and settling payer reports.
+ */
+contract PayerReportManager is IPayerReportManager, Initializable, Migratable, ERC5267 {
+    /* ============ Constants/Immutables ============ */
+
+    // solhint-disable-next-line max-line-length
+    /// @dev keccak256("PayerReport(uint256 attesterNodeId,uint256 originatorNodeId,uint256 startSequenceId,uint256 endSequenceId,bytes32 payersMerkleRoot,address[] nodeOperators)")
+    bytes32 public constant PAYER_REPORT_TYPEHASH = 0x73ce31d3128c9ba6af04b3270a898f0abafdf9bed84e47ec7ebd2f3dd8e9c8a5;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable parameterRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable nodeRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable payerRegistry;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.PayerReportManager
+     * @notice The UUPS storage for the payer report manager.
+     * @param  payerReportsByOriginator The mapping of arrays of payer reports by originator node ID.
+     */
+    struct PayerReportManagerStorage {
+        mapping(uint32 originatorId => PayerReport[] payerReports) payerReportsByOriginator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.PayerReportManager")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _PAYER_REPORT_MANAGER_STORAGE_LOCATION =
+        0x26b057ee8e4d60685198828fdf1c618ab8e36b0ab85f54a47b18319f6f718e00;
+
+    function _getPayerReportManagerStorage() internal pure returns (PayerReportManagerStorage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _PAYER_REPORT_MANAGER_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructor for the implementation contract, such that the implementation cannot be initialized.
+     * @param  parameterRegistry_ The address of the parameter registry.
+     * @param  nodeRegistry_      The address of the node registry.
+     * @param  payerRegistry_     The address of the payer registry.
+     * @dev    The parameter registry, node registry, and payer registry must not be the zero address.
+     * @dev    The parameter registry, node registry, and payer registry are immutable so that they are inlined in the
+     *         contract code, and have minimal gas cost.
+     */
+    constructor(address parameterRegistry_, address nodeRegistry_, address payerRegistry_) ERC5267() {
+        require(_isNotZero(parameterRegistry = parameterRegistry_), ZeroParameterRegistry());
+        require(_isNotZero(nodeRegistry = nodeRegistry_), ZeroNodeRegistry());
+        require(_isNotZero(payerRegistry = payerRegistry_), ZeroPayerRegistry());
+
+        _disableInitializers();
+    }
+
+    /* ============ Initialization ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function initialize() public initializer {
+        _initializeERC5267();
+    }
+
+    /* ============ Interactive Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function submit(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) external returns (uint256 payerReportIndex_) {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        payerReportIndex_ = payerReports_.length;
+
+        uint32 lastSequenceId_ = payerReportIndex_ > 0 ? payerReports_[payerReportIndex_ - 1].endSequenceId : 0;
+
+        // Enforces that the start sequence ID is the next expected sequence ID.
+        if (startSequenceId_ != lastSequenceId_ + 1) {
+            revert InvalidStartSequenceId(startSequenceId_, lastSequenceId_ + 1);
+        }
+
+        // Enforces that the end sequence ID is greater than or equal to the start sequence ID.
+        if (endSequenceId_ < startSequenceId_) revert InvalidSequenceIds();
+
+        // Verifies the signatures and gets the array of valid signing node IDs.
+        uint32[] memory validSigningNodeIds_ = _verifySignatures({
+            originatorNodeId_: originatorNodeId_,
+            attesterNodeId_: attesterNodeId_,
+            startSequenceId_: startSequenceId_,
+            endSequenceId_: endSequenceId_,
+            payersMerkleRoot_: payersMerkleRoot_,
+            nodeOperators_: nodeOperators_,
+            signatures_: signatures_
+        });
+
+        payerReports_.push(
+            PayerReport({
+                attesterNodeId: attesterNodeId_,
+                startSequenceId: startSequenceId_,
+                endSequenceId: endSequenceId_,
+                feesSettled: 0,
+                offset: 0,
+                isSettled: false,
+                payersMerkleRoot: payersMerkleRoot_,
+                nodeOperators: nodeOperators_
+            })
+        );
+
+        emit PayerReportSubmitted({
+            originatorNodeId: originatorNodeId_,
+            payerReportIndex: payerReportIndex_,
+            attesterNodeId: attesterNodeId_,
+            startSequenceId: startSequenceId_,
+            endSequenceId: endSequenceId_,
+            payersMerkleRoot: payersMerkleRoot_,
+            nodeOperators: nodeOperators_,
+            signingNodeIds: validSigningNodeIds_
+        });
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function settle(
+        uint32 originatorNodeId_,
+        uint32 payerReportIndex_,
+        bytes[] calldata payerFees_,
+        bytes32[] calldata proofElements_
+    ) external {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        if (payerReportIndex_ >= payerReports_.length) revert PayerReportIndexOutOfBounds();
+
+        PayerReport storage payerReport_ = payerReports_[payerReportIndex_];
+
+        if (payerReport_.isSettled) revert PayerReportEntirelySettled();
+
+        // Verify the payer fees provided are the next sequential payer fees from the last settled offset.
+        SequentialMerkleProofs.verify(payerReport_.payersMerkleRoot, payerReport_.offset, payerFees_, proofElements_);
+
+        payerReport_.offset += uint32(payerFees_.length);
+
+        uint32 leafCount_ = SequentialMerkleProofs.getLeafCount(proofElements_);
+
+        payerReport_.isSettled = leafCount_ == payerReport_.offset;
+
+        // Low level call which handles passing the `payerFees_` arrays as a bytes array that will be automatically
+        // decoded as the required structs by the payer registry's `settleUsage` function.
+        // slither-disable-next-line low-level-calls
+        (bool success_, bytes memory returnData_) = payerRegistry.call(
+            abi.encodeWithSelector(IPayerRegistryLike.settleUsage.selector, payerFees_)
+        );
+
+        if (!success_) revert SettleUsageFailed(returnData_);
+
+        uint96 feesSettled_ = abi.decode(returnData_, (uint96));
+
+        // slither-disable-next-line reentrancy-events
+        emit PayerReportSubsetSettled(
+            originatorNodeId_,
+            payerReportIndex_,
+            uint32(payerFees_.length),
+            leafCount_ - payerReport_.offset,
+            feesSettled_
+        );
+
+        payerReport_.feesSettled += feesSettled_;
+    }
+
+    /// @inheritdoc IMigratable
+    function migrate() external {
+        _migrate(_toAddress(_getRegistryParameter(migratorParameterKey())));
+    }
+
+    /* ============ View/Pure Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function migratorParameterKey() public pure returns (bytes memory key_) {
+        return "xmtp.payerReportManager.migrator";
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReportDigest(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) external view returns (bytes32 digest_) {
+        return
+            _getPayerReportDigest(
+                originatorNodeId_,
+                attesterNodeId_,
+                startSequenceId_,
+                endSequenceId_,
+                payersMerkleRoot_,
+                nodeOperators_
+            );
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReports(uint32 originatorNodeId_) external view returns (PayerReport[] memory payerReports_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_];
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReport(
+        uint32 originatorNodeId_,
+        uint256 payerReportIndex_
+    ) external view returns (PayerReport memory payerReport_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_][payerReportIndex_];
+    }
+
+    /* ============ Internal View/Pure Functions ============ */
+
+    function _name() internal pure override returns (string memory name_) {
+        return "PayerReportManager";
+    }
+
+    function _version() internal pure override returns (string memory version_) {
+        return "1";
+    }
+
+    /**
+     * @dev    Returns the EIP-712 digest for a payer report.
+     * @param  originatorNodeId_ The id of the node originator.
+     * @param  attesterNodeId_   The id of the node attester.
+     * @param  startSequenceId_  The start sequence ID.
+     * @param  endSequenceId_    The end sequence ID.
+     * @param  payersMerkleRoot_ The payers merkle root.
+     * @param  nodeOperators_    The active node operators during the reporting period.
+     * @return digest_           The EIP-712 digest.
+     */
+    function _getPayerReportDigest(
+        uint256 originatorNodeId_,
+        uint256 attesterNodeId_,
+        uint256 startSequenceId_,
+        uint256 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) internal view returns (bytes32 digest_) {
+        return
+            _getDigest(
+                keccak256(
+                    abi.encode(
+                        PAYER_REPORT_TYPEHASH,
+                        originatorNodeId_,
+                        attesterNodeId_,
+                        startSequenceId_,
+                        endSequenceId_,
+                        payersMerkleRoot_,
+                        keccak256(abi.encodePacked(nodeOperators_))
+                    )
+                )
+            );
+    }
+
+    function _getRegistryParameter(bytes memory key_) internal view returns (bytes32 value_) {
+        return IParameterRegistryLike(parameterRegistry).get(key_);
+    }
+
+    function _isNotZero(address input_) internal pure returns (bool isNotZero_) {
+        return input_ != address(0);
+    }
+
+    function _toAddress(bytes32 value_) internal pure returns (address address_) {
+        // slither-disable-next-line assembly
+        assembly {
+            address_ := value_
+        }
+    }
+
+    /**
+     * @dev Verifies that enough of the signatures are valid and provided by canonical node operators and returns the
+     *      array of valid signing node IDs.
+     */
+    function _verifySignatures(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) internal view returns (uint32[] memory validSigningNodeIds_) {
+        bytes32 digest_ = _getPayerReportDigest(
+            originatorNodeId_,
+            attesterNodeId_,
+            startSequenceId_,
+            endSequenceId_,
+            payersMerkleRoot_,
+            nodeOperators_
+        );
+
+        // Need to determine how many of the signatures are valid and provided by canonical node operators.
+        uint8 validSignatureCount_ = 0;
+
+        // This array will help in constructing the `validSigningNodeIds_` array, who's length may be less than
+        // the length of the `signatures_` array if some of the signatures are invalid.
+        bool[] memory isValid_ = new bool[](signatures_.length);
+
+        for (uint256 index_; index_ < signatures_.length; ++index_) {
+            uint32 nodeId_ = signatures_[index_].nodeId;
+
+            // Enforces that the signing node IDs are ordered and unique.
+            if (index_ != 0 && nodeId_ <= signatures_[index_ - 1].nodeId) revert UnorderedNodeIds();
+
+            // If the signature is invalid, ignore it.
+            if (!(isValid_[index_] = _verifySignature(digest_, nodeId_, signatures_[index_].signature))) continue;
+
+            ++validSignatureCount_;
+        }
+
+        uint8 requiredSignatureCount_ = (INodeRegistryLike(nodeRegistry).canonicalNodesCount() / 2) + 1;
+
+        // Enforces that the number of valid signatures is greater than one more than half of the canonical node count.
+        if (validSignatureCount_ < requiredSignatureCount_) {
+            revert InsufficientSignatures(validSignatureCount_, requiredSignatureCount_);
+        }
+
+        validSigningNodeIds_ = new uint32[](validSignatureCount_);
+
+        uint256 writeIndex_ = 0;
+
+        for (uint256 index_; index_ < isValid_.length; ++index_) {
+            if (!isValid_[index_]) continue; // Skip invalid signatures.
+
+            unchecked {
+                validSigningNodeIds_[writeIndex_++] = signatures_[index_].nodeId;
+            }
+        }
+    }
+
+    /// @dev Returns true if the signature is from the owner of a canonical node.
+    function _verifySignature(
+        bytes32 digest_,
+        uint32 nodeId_,
+        bytes calldata signature_
+    ) internal view returns (bool isValid_) {
+        // If the node is not canonical, the signature is invalid.
+        if (!INodeRegistryLike(nodeRegistry).getIsCanonicalNode(nodeId_)) return false;
+
+        // Try to recover the signer from the signature. We don't want to revert if the signature is invalid.
+        address signer_ = ECDSA.tryRecoverCalldata(digest_, signature_);
+
+        // The signature is valid if the signer is not `address(0)` and the signer is the owner of the node.
+        return (signer_ != address(0)) && (signer_ == INodeRegistryLike(nodeRegistry).ownerOf(nodeId_));
path: src/settlement-chain/PayerReportManager.sol
user: login: neekolas
body: Something to think about here:

Everything we sign right now from the nodes (`OriginatorEnvelope`s) is signed with the node's `privateKey`, not necessarily with the private key that is the owner of the node NFT.

That allows node operators to hold their valuable NFT in one wallet, and put their private key on the node. If that private key is compromised, the attacker can send messages but can't steal money. 

This would change that. If we signed with and validated against the Node private key, we could avoid this problem and stop the private key from ever being exposed to the node.
created_at: 2025-05-07T00:41:16Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,385 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { ECDSA } from "../../lib/solady/src/utils/ECDSA.sol";
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../libraries/SequentialMerkleProofs.sol";
+
+import { IMigratable } from "../abstract/interfaces/IMigratable.sol";
+import { IParameterRegistryLike, INodeRegistryLike, IPayerRegistryLike } from "./interfaces/External.sol";
+import { IPayerReportManager } from "./interfaces/IPayerReportManager.sol";
+
+import { ERC5267 } from "../abstract/ERC5267.sol";
+import { Migratable } from "../abstract/Migratable.sol";
+
+// TODO: If a node operator owns more than one node, their signature for a payer report will be identical, and therefore
+//       replayable across their nodes. This may not be ideal, so it might be necessary to include the node id of the
+//       signer in the digest.
path: src/settlement-chain/PayerReportManager.sol
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Address the signature replay vulnerability noted in TODO comment.**

The TODO comment identifies a significant security concern: if a node operator owns multiple nodes, their signature can be reused across those nodes, potentially allowing manipulation of the consensus process. 

Consider updating the EIP-712 type structure to include the signing node ID in the digest to provide proper signature domain separation.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if any node operator owns multiple nodes
# This would verify if the vulnerability is exploitable in practice

NODE_REGISTRY=$(grep -r "nodeRegistry =" --include="*.sol" | head -1 | sed -E 's/.*nodeRegistry = ([^;]*);.*/\1/')
echo "Checking node ownership patterns in $NODE_REGISTRY"

# Find all functions that check node ownership to analyze potential callers
rg -A 2 "ownerOf\(.*\)" --type solidity
```

Length of output: 384

---

🏁 Script executed:

```shell
#!/bin/bash
# Locate NodeRegistry contract and its inheritance to check ERC721 usage
rg -g '*.sol' -n 'contract.*NodeRegistry'
rg -g '*.sol' -n 'NodeRegistry.*ERC721'

# Inspect ownerOf usage to confirm multiple ownership model
rg -g '*.sol' -n 'ownerOf\('

# Check for any balanceOf-based restrictions on node ownership
rg -g '*.sol' -n 'balanceOf\(' -A 2

# Find minting or registration functions for nodes to see if they enforce one node per operator
rg -g '*.sol' -n 'function.*\(mint\|register\).*\(node\)' -A 3
```

Length of output: 3777

---



</details>

**Include node ID in EIP-712 signature to prevent replay**  

Verification confirms that `NodeRegistry` in `src/settlement-chain/NodeRegistry.sol` inherits `ERC721Upgradeable`, allowing a single operator address to own multiple node IDs. As noted in the TODO, this enables a signature from one node to be replayed for another.  

Please update the EIP-712 “ReportRequest” type and digest logic in `src/settlement-chain/PayerReportManager.sol` to incorporate `nodeId`, for example:  
- Extend the `REPORT_REQUEST_TYPEHASH` string to include `uint256 nodeId`.  
- Modify the struct and `_buildDigest` (or equivalent) to ABI-encode `nodeId` alongside the existing fields.  
- Adjust any helper constants or test mocks to include the new field.  
- Remove the original TODO once the change is implemented.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-07T15:27:03Z
author_association: NONE
diff_hunk: @@ -0,0 +1,385 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { ECDSA } from "../../lib/solady/src/utils/ECDSA.sol";
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../libraries/SequentialMerkleProofs.sol";
+
+import { IMigratable } from "../abstract/interfaces/IMigratable.sol";
+import { IParameterRegistryLike, INodeRegistryLike, IPayerRegistryLike } from "./interfaces/External.sol";
+import { IPayerReportManager } from "./interfaces/IPayerReportManager.sol";
+
+import { ERC5267 } from "../abstract/ERC5267.sol";
+import { Migratable } from "../abstract/Migratable.sol";
+
+// TODO: If a node operator owns more than one node, their signature for a payer report will be identical, and therefore
+//       replayable across their nodes. This may not be ideal, so it might be necessary to include the node id of the
+//       signer in the digest.
+
+/**
+ * @title  Implementation of the Payer Report Manager.
+ * @notice This contract handles functionality for submitting and settling payer reports.
+ */
+contract PayerReportManager is IPayerReportManager, Initializable, Migratable, ERC5267 {
+    /* ============ Constants/Immutables ============ */
+
+    // solhint-disable-next-line max-line-length
+    /// @dev keccak256("PayerReport(uint256 attesterNodeId,uint256 originatorNodeId,uint256 startSequenceId,uint256 endSequenceId,bytes32 payersMerkleRoot,address[] nodeOperators)")
+    bytes32 public constant PAYER_REPORT_TYPEHASH = 0x73ce31d3128c9ba6af04b3270a898f0abafdf9bed84e47ec7ebd2f3dd8e9c8a5;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable parameterRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable nodeRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable payerRegistry;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.PayerReportManager
+     * @notice The UUPS storage for the payer report manager.
+     * @param  payerReportsByOriginator The mapping of arrays of payer reports by originator node ID.
+     */
+    struct PayerReportManagerStorage {
+        mapping(uint32 originatorId => PayerReport[] payerReports) payerReportsByOriginator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.PayerReportManager")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _PAYER_REPORT_MANAGER_STORAGE_LOCATION =
+        0x26b057ee8e4d60685198828fdf1c618ab8e36b0ab85f54a47b18319f6f718e00;
+
+    function _getPayerReportManagerStorage() internal pure returns (PayerReportManagerStorage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _PAYER_REPORT_MANAGER_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructor for the implementation contract, such that the implementation cannot be initialized.
+     * @param  parameterRegistry_ The address of the parameter registry.
+     * @param  nodeRegistry_      The address of the node registry.
+     * @param  payerRegistry_     The address of the payer registry.
+     * @dev    The parameter registry, node registry, and payer registry must not be the zero address.
+     * @dev    The parameter registry, node registry, and payer registry are immutable so that they are inlined in the
+     *         contract code, and have minimal gas cost.
+     */
+    constructor(address parameterRegistry_, address nodeRegistry_, address payerRegistry_) ERC5267() {
+        require(_isNotZero(parameterRegistry = parameterRegistry_), ZeroParameterRegistry());
+        require(_isNotZero(nodeRegistry = nodeRegistry_), ZeroNodeRegistry());
+        require(_isNotZero(payerRegistry = payerRegistry_), ZeroPayerRegistry());
+
+        _disableInitializers();
+    }
+
+    /* ============ Initialization ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function initialize() public initializer {
+        _initializeERC5267();
+    }
+
+    /* ============ Interactive Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function submit(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
path: src/settlement-chain/PayerReportManager.sol
user: login: neekolas
body: What do we think about defining this as node operator IDs rather than addresses?

The worry here is that the owner address of a node operator NFT can change, but the ID can't.
created_at: 2025-05-07T18:55:27Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,381 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { ECDSA } from "../../lib/solady/src/utils/ECDSA.sol";
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../libraries/SequentialMerkleProofs.sol";
+
+import { IMigratable } from "../abstract/interfaces/IMigratable.sol";
+import { IParameterRegistryLike, INodeRegistryLike, IPayerRegistryLike } from "./interfaces/External.sol";
+import { IPayerReportManager } from "./interfaces/IPayerReportManager.sol";
+
+import { ERC5267 } from "../abstract/ERC5267.sol";
+import { Migratable } from "../abstract/Migratable.sol";
+
+// TODO: If a node operator owns more than one node, their signature for a payer report will be identical, and therefore
+//       replayable across their nodes. This may not be ideal, so it might be necessary to include the node id of the
+//       signer in the digest.
+
+/**
+ * @title  Implementation of the Payer Report Manager.
+ * @notice This contract handles functionality for submitting and settling payer reports.
+ */
+contract PayerReportManager is IPayerReportManager, Initializable, Migratable, ERC5267 {
+    /* ============ Constants/Immutables ============ */
+
+    // solhint-disable-next-line max-line-length
+    /// @dev keccak256("PayerReport(uint256 attesterNodeId,uint256 originatorNodeId,uint256 startSequenceId,uint256 endSequenceId,bytes32 payersMerkleRoot,address[] nodeOperators)")
+    bytes32 public constant PAYER_REPORT_TYPEHASH = 0x73ce31d3128c9ba6af04b3270a898f0abafdf9bed84e47ec7ebd2f3dd8e9c8a5;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable parameterRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable nodeRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable payerRegistry;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.PayerReportManager
+     * @notice The UUPS storage for the payer report manager.
+     * @param  payerReportsByOriginator The mapping of arrays of payer reports by originator node ID.
+     */
+    struct PayerReportManagerStorage {
+        mapping(uint32 originatorId => PayerReport[] payerReports) payerReportsByOriginator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.PayerReportManager")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _PAYER_REPORT_MANAGER_STORAGE_LOCATION =
+        0x26b057ee8e4d60685198828fdf1c618ab8e36b0ab85f54a47b18319f6f718e00;
+
+    function _getPayerReportManagerStorage() internal pure returns (PayerReportManagerStorage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _PAYER_REPORT_MANAGER_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructor for the implementation contract, such that the implementation cannot be initialized.
+     * @param  parameterRegistry_ The address of the parameter registry.
+     * @param  nodeRegistry_      The address of the node registry.
+     * @param  payerRegistry_     The address of the payer registry.
+     * @dev    The parameter registry, node registry, and payer registry must not be the zero address.
+     * @dev    The parameter registry, node registry, and payer registry are immutable so that they are inlined in the
+     *         contract code, and have minimal gas cost.
+     */
+    constructor(address parameterRegistry_, address nodeRegistry_, address payerRegistry_) ERC5267() {
+        require(_isNotZero(parameterRegistry = parameterRegistry_), ZeroParameterRegistry());
+        require(_isNotZero(nodeRegistry = nodeRegistry_), ZeroNodeRegistry());
+        require(_isNotZero(payerRegistry = payerRegistry_), ZeroPayerRegistry());
+
+        _disableInitializers();
+    }
+
+    /* ============ Initialization ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function initialize() public initializer {
+        _initializeERC5267();
+    }
+
+    /* ============ Interactive Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function submit(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) external returns (uint256 payerReportIndex_) {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        payerReportIndex_ = payerReports_.length;
+
+        uint32 lastSequenceId_ = payerReportIndex_ > 0 ? payerReports_[payerReportIndex_ - 1].endSequenceId : 0;
+
+        // Enforces that the start sequence ID is the next expected sequence ID.
+        if (startSequenceId_ != lastSequenceId_ + 1) {
path: src/settlement-chain/PayerReportManager.sol
user: login: deluca-mike
body: So that would imply that a Payer Report is inclusive of `startSequenceID` and exclusive of its `endSequenceID`. Is that correct?
created_at: 2025-05-08T16:49:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,381 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { ECDSA } from "../../lib/solady/src/utils/ECDSA.sol";
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../libraries/SequentialMerkleProofs.sol";
+
+import { IMigratable } from "../abstract/interfaces/IMigratable.sol";
+import { IParameterRegistryLike, INodeRegistryLike, IPayerRegistryLike } from "./interfaces/External.sol";
+import { IPayerReportManager } from "./interfaces/IPayerReportManager.sol";
+
+import { ERC5267 } from "../abstract/ERC5267.sol";
+import { Migratable } from "../abstract/Migratable.sol";
+
+// TODO: If a node operator owns more than one node, their signature for a payer report will be identical, and therefore
+//       replayable across their nodes. This may not be ideal, so it might be necessary to include the node id of the
+//       signer in the digest.
+
+/**
+ * @title  Implementation of the Payer Report Manager.
+ * @notice This contract handles functionality for submitting and settling payer reports.
+ */
+contract PayerReportManager is IPayerReportManager, Initializable, Migratable, ERC5267 {
+    /* ============ Constants/Immutables ============ */
+
+    // solhint-disable-next-line max-line-length
+    /// @dev keccak256("PayerReport(uint256 attesterNodeId,uint256 originatorNodeId,uint256 startSequenceId,uint256 endSequenceId,bytes32 payersMerkleRoot,address[] nodeOperators)")
+    bytes32 public constant PAYER_REPORT_TYPEHASH = 0x73ce31d3128c9ba6af04b3270a898f0abafdf9bed84e47ec7ebd2f3dd8e9c8a5;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable parameterRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable nodeRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable payerRegistry;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.PayerReportManager
+     * @notice The UUPS storage for the payer report manager.
+     * @param  payerReportsByOriginator The mapping of arrays of payer reports by originator node ID.
+     */
+    struct PayerReportManagerStorage {
+        mapping(uint32 originatorId => PayerReport[] payerReports) payerReportsByOriginator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.PayerReportManager")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _PAYER_REPORT_MANAGER_STORAGE_LOCATION =
+        0x26b057ee8e4d60685198828fdf1c618ab8e36b0ab85f54a47b18319f6f718e00;
+
+    function _getPayerReportManagerStorage() internal pure returns (PayerReportManagerStorage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _PAYER_REPORT_MANAGER_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructor for the implementation contract, such that the implementation cannot be initialized.
+     * @param  parameterRegistry_ The address of the parameter registry.
+     * @param  nodeRegistry_      The address of the node registry.
+     * @param  payerRegistry_     The address of the payer registry.
+     * @dev    The parameter registry, node registry, and payer registry must not be the zero address.
+     * @dev    The parameter registry, node registry, and payer registry are immutable so that they are inlined in the
+     *         contract code, and have minimal gas cost.
+     */
+    constructor(address parameterRegistry_, address nodeRegistry_, address payerRegistry_) ERC5267() {
+        require(_isNotZero(parameterRegistry = parameterRegistry_), ZeroParameterRegistry());
+        require(_isNotZero(nodeRegistry = nodeRegistry_), ZeroNodeRegistry());
+        require(_isNotZero(payerRegistry = payerRegistry_), ZeroPayerRegistry());
+
+        _disableInitializers();
+    }
+
+    /* ============ Initialization ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function initialize() public initializer {
+        _initializeERC5267();
+    }
+
+    /* ============ Interactive Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function submit(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) external returns (uint256 payerReportIndex_) {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        payerReportIndex_ = payerReports_.length;
+
+        uint32 lastSequenceId_ = payerReportIndex_ > 0 ? payerReports_[payerReportIndex_ - 1].endSequenceId : 0;
+
+        // Enforces that the start sequence ID is the next expected sequence ID.
+        if (startSequenceId_ != lastSequenceId_ + 1) {
+            revert InvalidStartSequenceId(startSequenceId_, lastSequenceId_ + 1);
+        }
+
+        // Enforces that the end sequence ID is greater than or equal to the start sequence ID.
+        if (endSequenceId_ < startSequenceId_) revert InvalidSequenceIds();
+
+        // Verifies the signatures and gets the array of valid signing node IDs.
+        uint32[] memory validSigningNodeIds_ = _verifySignatures({
+            originatorNodeId_: originatorNodeId_,
+            attesterNodeId_: attesterNodeId_,
+            startSequenceId_: startSequenceId_,
+            endSequenceId_: endSequenceId_,
+            payersMerkleRoot_: payersMerkleRoot_,
+            nodeOperators_: nodeOperators_,
+            signatures_: signatures_
+        });
+
+        payerReports_.push(
+            PayerReport({
+                attesterNodeId: attesterNodeId_,
+                startSequenceId: startSequenceId_,
+                endSequenceId: endSequenceId_,
+                feesSettled: 0,
+                offset: 0,
+                isSettled: false,
+                payersMerkleRoot: payersMerkleRoot_,
+                nodeOperators: nodeOperators_
+            })
+        );
+
+        emit PayerReportSubmitted({
+            originatorNodeId: originatorNodeId_,
+            payerReportIndex: payerReportIndex_,
+            attesterNodeId: attesterNodeId_,
+            startSequenceId: startSequenceId_,
+            endSequenceId: endSequenceId_,
+            payersMerkleRoot: payersMerkleRoot_,
+            nodeOperators: nodeOperators_,
+            signingNodeIds: validSigningNodeIds_
+        });
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function settle(
+        uint32 originatorNodeId_,
+        uint32 payerReportIndex_,
+        bytes[] calldata payerFees_,
+        bytes32[] calldata proofElements_
+    ) external {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        if (payerReportIndex_ >= payerReports_.length) revert PayerReportIndexOutOfBounds();
+
+        PayerReport storage payerReport_ = payerReports_[payerReportIndex_];
+
+        if (payerReport_.isSettled) revert PayerReportEntirelySettled();
+
+        // Verify the payer fees provided are the next sequential payer fees from the last settled offset.
+        SequentialMerkleProofs.verify(payerReport_.payersMerkleRoot, payerReport_.offset, payerFees_, proofElements_);
+
+        payerReport_.offset += uint32(payerFees_.length);
+
+        uint32 leafCount_ = SequentialMerkleProofs.getLeafCount(proofElements_);
+
+        payerReport_.isSettled = leafCount_ == payerReport_.offset;
+
+        // Low level call which handles passing the `payerFees_` arrays as a bytes array that will be automatically
+        // decoded as the required structs by the payer registry's `settleUsage` function.
+        // slither-disable-next-line low-level-calls
+        (bool success_, bytes memory returnData_) = payerRegistry.call(
+            abi.encodeWithSelector(IPayerRegistryLike.settleUsage.selector, payerFees_)
+        );
+
+        if (!success_) revert SettleUsageFailed(returnData_);
+
+        uint96 feesSettled_ = abi.decode(returnData_, (uint96));
+
+        // slither-disable-next-line reentrancy-events
+        emit PayerReportSubsetSettled(
+            originatorNodeId_,
+            payerReportIndex_,
+            uint32(payerFees_.length),
+            leafCount_ - payerReport_.offset,
+            feesSettled_
+        );
+
+        payerReport_.feesSettled += feesSettled_;
+    }
+
+    /// @inheritdoc IMigratable
+    function migrate() external {
+        _migrate(_toAddress(_getRegistryParameter(migratorParameterKey())));
+    }
+
+    /* ============ View/Pure Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function migratorParameterKey() public pure returns (bytes memory key_) {
+        return "xmtp.payerReportManager.migrator";
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReportDigest(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) external view returns (bytes32 digest_) {
+        return
+            _getPayerReportDigest(
+                originatorNodeId_,
+                attesterNodeId_,
+                startSequenceId_,
+                endSequenceId_,
+                payersMerkleRoot_,
+                nodeOperators_
+            );
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReports(uint32 originatorNodeId_) external view returns (PayerReport[] memory payerReports_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_];
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReport(
+        uint32 originatorNodeId_,
+        uint256 payerReportIndex_
+    ) external view returns (PayerReport memory payerReport_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_][payerReportIndex_];
+    }
+
+    /* ============ Internal View/Pure Functions ============ */
+
+    function _name() internal pure override returns (string memory name_) {
+        return "PayerReportManager";
+    }
+
+    function _version() internal pure override returns (string memory version_) {
+        return "1";
+    }
+
+    /**
+     * @dev    Returns the EIP-712 digest for a payer report.
+     * @param  originatorNodeId_ The id of the node originator.
+     * @param  attesterNodeId_   The id of the node attester.
+     * @param  startSequenceId_  The start sequence ID.
+     * @param  endSequenceId_    The end sequence ID.
+     * @param  payersMerkleRoot_ The payers merkle root.
+     * @param  nodeOperators_    The active node operators during the reporting period.
+     * @return digest_           The EIP-712 digest.
+     */
+    function _getPayerReportDigest(
+        uint256 originatorNodeId_,
+        uint256 attesterNodeId_,
+        uint256 startSequenceId_,
+        uint256 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) internal view returns (bytes32 digest_) {
+        return
+            _getDigest(
+                keccak256(
+                    abi.encode(
+                        PAYER_REPORT_TYPEHASH,
+                        originatorNodeId_,
+                        attesterNodeId_,
+                        startSequenceId_,
+                        endSequenceId_,
+                        payersMerkleRoot_,
+                        keccak256(abi.encodePacked(nodeOperators_))
+                    )
+                )
+            );
+    }
+
+    function _getRegistryParameter(bytes memory key_) internal view returns (bytes32 value_) {
+        return IParameterRegistryLike(parameterRegistry).get(key_);
+    }
+
+    function _isNotZero(address input_) internal pure returns (bool isNotZero_) {
+        return input_ != address(0);
+    }
+
+    function _toAddress(bytes32 value_) internal pure returns (address address_) {
+        // slither-disable-next-line assembly
+        assembly {
+            address_ := value_
+        }
+    }
+
+    /**
+     * @dev Verifies that enough of the signatures are valid and provided by canonical node operators and returns the
+     *      array of valid signing node IDs.
+     */
+    function _verifySignatures(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
path: src/settlement-chain/PayerReportManager.sol
user: login: deluca-mike
body: Ok, will ditch it.
created_at: 2025-05-08T16:49:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,381 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { ECDSA } from "../../lib/solady/src/utils/ECDSA.sol";
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../libraries/SequentialMerkleProofs.sol";
+
+import { IMigratable } from "../abstract/interfaces/IMigratable.sol";
+import { IParameterRegistryLike, INodeRegistryLike, IPayerRegistryLike } from "./interfaces/External.sol";
+import { IPayerReportManager } from "./interfaces/IPayerReportManager.sol";
+
+import { ERC5267 } from "../abstract/ERC5267.sol";
+import { Migratable } from "../abstract/Migratable.sol";
+
+// TODO: If a node operator owns more than one node, their signature for a payer report will be identical, and therefore
+//       replayable across their nodes. This may not be ideal, so it might be necessary to include the node id of the
+//       signer in the digest.
+
+/**
+ * @title  Implementation of the Payer Report Manager.
+ * @notice This contract handles functionality for submitting and settling payer reports.
+ */
+contract PayerReportManager is IPayerReportManager, Initializable, Migratable, ERC5267 {
+    /* ============ Constants/Immutables ============ */
+
+    // solhint-disable-next-line max-line-length
+    /// @dev keccak256("PayerReport(uint256 attesterNodeId,uint256 originatorNodeId,uint256 startSequenceId,uint256 endSequenceId,bytes32 payersMerkleRoot,address[] nodeOperators)")
+    bytes32 public constant PAYER_REPORT_TYPEHASH = 0x73ce31d3128c9ba6af04b3270a898f0abafdf9bed84e47ec7ebd2f3dd8e9c8a5;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable parameterRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable nodeRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable payerRegistry;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.PayerReportManager
+     * @notice The UUPS storage for the payer report manager.
+     * @param  payerReportsByOriginator The mapping of arrays of payer reports by originator node ID.
+     */
+    struct PayerReportManagerStorage {
+        mapping(uint32 originatorId => PayerReport[] payerReports) payerReportsByOriginator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.PayerReportManager")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _PAYER_REPORT_MANAGER_STORAGE_LOCATION =
+        0x26b057ee8e4d60685198828fdf1c618ab8e36b0ab85f54a47b18319f6f718e00;
+
+    function _getPayerReportManagerStorage() internal pure returns (PayerReportManagerStorage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _PAYER_REPORT_MANAGER_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructor for the implementation contract, such that the implementation cannot be initialized.
+     * @param  parameterRegistry_ The address of the parameter registry.
+     * @param  nodeRegistry_      The address of the node registry.
+     * @param  payerRegistry_     The address of the payer registry.
+     * @dev    The parameter registry, node registry, and payer registry must not be the zero address.
+     * @dev    The parameter registry, node registry, and payer registry are immutable so that they are inlined in the
+     *         contract code, and have minimal gas cost.
+     */
+    constructor(address parameterRegistry_, address nodeRegistry_, address payerRegistry_) ERC5267() {
+        require(_isNotZero(parameterRegistry = parameterRegistry_), ZeroParameterRegistry());
+        require(_isNotZero(nodeRegistry = nodeRegistry_), ZeroNodeRegistry());
+        require(_isNotZero(payerRegistry = payerRegistry_), ZeroPayerRegistry());
+
+        _disableInitializers();
+    }
+
+    /* ============ Initialization ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function initialize() public initializer {
+        _initializeERC5267();
+    }
+
+    /* ============ Interactive Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function submit(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) external returns (uint256 payerReportIndex_) {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        payerReportIndex_ = payerReports_.length;
+
+        uint32 lastSequenceId_ = payerReportIndex_ > 0 ? payerReports_[payerReportIndex_ - 1].endSequenceId : 0;
+
+        // Enforces that the start sequence ID is the next expected sequence ID.
+        if (startSequenceId_ != lastSequenceId_ + 1) {
+            revert InvalidStartSequenceId(startSequenceId_, lastSequenceId_ + 1);
+        }
+
+        // Enforces that the end sequence ID is greater than or equal to the start sequence ID.
+        if (endSequenceId_ < startSequenceId_) revert InvalidSequenceIds();
+
+        // Verifies the signatures and gets the array of valid signing node IDs.
+        uint32[] memory validSigningNodeIds_ = _verifySignatures({
+            originatorNodeId_: originatorNodeId_,
+            attesterNodeId_: attesterNodeId_,
+            startSequenceId_: startSequenceId_,
+            endSequenceId_: endSequenceId_,
+            payersMerkleRoot_: payersMerkleRoot_,
+            nodeOperators_: nodeOperators_,
+            signatures_: signatures_
+        });
+
+        payerReports_.push(
+            PayerReport({
+                attesterNodeId: attesterNodeId_,
+                startSequenceId: startSequenceId_,
+                endSequenceId: endSequenceId_,
+                feesSettled: 0,
+                offset: 0,
+                isSettled: false,
+                payersMerkleRoot: payersMerkleRoot_,
+                nodeOperators: nodeOperators_
+            })
+        );
+
+        emit PayerReportSubmitted({
+            originatorNodeId: originatorNodeId_,
+            payerReportIndex: payerReportIndex_,
+            attesterNodeId: attesterNodeId_,
+            startSequenceId: startSequenceId_,
+            endSequenceId: endSequenceId_,
+            payersMerkleRoot: payersMerkleRoot_,
+            nodeOperators: nodeOperators_,
+            signingNodeIds: validSigningNodeIds_
+        });
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function settle(
+        uint32 originatorNodeId_,
+        uint32 payerReportIndex_,
+        bytes[] calldata payerFees_,
+        bytes32[] calldata proofElements_
+    ) external {
+        PayerReport[] storage payerReports_ = _getPayerReportManagerStorage().payerReportsByOriginator[
+            originatorNodeId_
+        ];
+
+        if (payerReportIndex_ >= payerReports_.length) revert PayerReportIndexOutOfBounds();
+
+        PayerReport storage payerReport_ = payerReports_[payerReportIndex_];
+
+        if (payerReport_.isSettled) revert PayerReportEntirelySettled();
+
+        // Verify the payer fees provided are the next sequential payer fees from the last settled offset.
+        SequentialMerkleProofs.verify(payerReport_.payersMerkleRoot, payerReport_.offset, payerFees_, proofElements_);
+
+        payerReport_.offset += uint32(payerFees_.length);
+
+        uint32 leafCount_ = SequentialMerkleProofs.getLeafCount(proofElements_);
+
+        payerReport_.isSettled = leafCount_ == payerReport_.offset;
+
+        // Low level call which handles passing the `payerFees_` arrays as a bytes array that will be automatically
+        // decoded as the required structs by the payer registry's `settleUsage` function.
+        // slither-disable-next-line low-level-calls
+        (bool success_, bytes memory returnData_) = payerRegistry.call(
+            abi.encodeWithSelector(IPayerRegistryLike.settleUsage.selector, payerFees_)
+        );
+
+        if (!success_) revert SettleUsageFailed(returnData_);
+
+        uint96 feesSettled_ = abi.decode(returnData_, (uint96));
+
+        // slither-disable-next-line reentrancy-events
+        emit PayerReportSubsetSettled(
+            originatorNodeId_,
+            payerReportIndex_,
+            uint32(payerFees_.length),
+            leafCount_ - payerReport_.offset,
+            feesSettled_
+        );
+
+        payerReport_.feesSettled += feesSettled_;
+    }
+
+    /// @inheritdoc IMigratable
+    function migrate() external {
+        _migrate(_toAddress(_getRegistryParameter(migratorParameterKey())));
+    }
+
+    /* ============ View/Pure Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function migratorParameterKey() public pure returns (bytes memory key_) {
+        return "xmtp.payerReportManager.migrator";
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReportDigest(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) external view returns (bytes32 digest_) {
+        return
+            _getPayerReportDigest(
+                originatorNodeId_,
+                attesterNodeId_,
+                startSequenceId_,
+                endSequenceId_,
+                payersMerkleRoot_,
+                nodeOperators_
+            );
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReports(uint32 originatorNodeId_) external view returns (PayerReport[] memory payerReports_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_];
+    }
+
+    /// @inheritdoc IPayerReportManager
+    function getPayerReport(
+        uint32 originatorNodeId_,
+        uint256 payerReportIndex_
+    ) external view returns (PayerReport memory payerReport_) {
+        return _getPayerReportManagerStorage().payerReportsByOriginator[originatorNodeId_][payerReportIndex_];
+    }
+
+    /* ============ Internal View/Pure Functions ============ */
+
+    function _name() internal pure override returns (string memory name_) {
+        return "PayerReportManager";
+    }
+
+    function _version() internal pure override returns (string memory version_) {
+        return "1";
+    }
+
+    /**
+     * @dev    Returns the EIP-712 digest for a payer report.
+     * @param  originatorNodeId_ The id of the node originator.
+     * @param  attesterNodeId_   The id of the node attester.
+     * @param  startSequenceId_  The start sequence ID.
+     * @param  endSequenceId_    The end sequence ID.
+     * @param  payersMerkleRoot_ The payers merkle root.
+     * @param  nodeOperators_    The active node operators during the reporting period.
+     * @return digest_           The EIP-712 digest.
+     */
+    function _getPayerReportDigest(
+        uint256 originatorNodeId_,
+        uint256 attesterNodeId_,
+        uint256 startSequenceId_,
+        uint256 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_
+    ) internal view returns (bytes32 digest_) {
+        return
+            _getDigest(
+                keccak256(
+                    abi.encode(
+                        PAYER_REPORT_TYPEHASH,
+                        originatorNodeId_,
+                        attesterNodeId_,
+                        startSequenceId_,
+                        endSequenceId_,
+                        payersMerkleRoot_,
+                        keccak256(abi.encodePacked(nodeOperators_))
+                    )
+                )
+            );
+    }
+
+    function _getRegistryParameter(bytes memory key_) internal view returns (bytes32 value_) {
+        return IParameterRegistryLike(parameterRegistry).get(key_);
+    }
+
+    function _isNotZero(address input_) internal pure returns (bool isNotZero_) {
+        return input_ != address(0);
+    }
+
+    function _toAddress(bytes32 value_) internal pure returns (address address_) {
+        // slither-disable-next-line assembly
+        assembly {
+            address_ := value_
+        }
+    }
+
+    /**
+     * @dev Verifies that enough of the signatures are valid and provided by canonical node operators and returns the
+     *      array of valid signing node IDs.
+     */
+    function _verifySignatures(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
+        PayerReportSignature[] calldata signatures_
+    ) internal view returns (uint32[] memory validSigningNodeIds_) {
+        bytes32 digest_ = _getPayerReportDigest(
+            originatorNodeId_,
+            attesterNodeId_,
+            startSequenceId_,
+            endSequenceId_,
+            payersMerkleRoot_,
+            nodeOperators_
+        );
+
+        // Need to determine how many of the signatures are valid and provided by canonical node operators.
+        uint8 validSignatureCount_ = 0;
+
+        // This array will help in constructing the `validSigningNodeIds_` array, who's length may be less than
+        // the length of the `signatures_` array if some of the signatures are invalid.
+        bool[] memory isValid_ = new bool[](signatures_.length);
+
+        for (uint256 index_; index_ < signatures_.length; ++index_) {
+            uint32 nodeId_ = signatures_[index_].nodeId;
+
+            // Enforces that the signing node IDs are ordered and unique.
+            if (index_ != 0 && nodeId_ <= signatures_[index_ - 1].nodeId) revert UnorderedNodeIds();
+
+            // If the signature is invalid, ignore it.
+            if (!(isValid_[index_] = _verifySignature(digest_, nodeId_, signatures_[index_].signature))) continue;
+
+            ++validSignatureCount_;
+        }
+
+        uint8 requiredSignatureCount_ = (INodeRegistryLike(nodeRegistry).canonicalNodesCount() / 2) + 1;
+
+        // Enforces that the number of valid signatures is greater than one more than half of the canonical node count.
+        if (validSignatureCount_ < requiredSignatureCount_) {
+            revert InsufficientSignatures(validSignatureCount_, requiredSignatureCount_);
+        }
+
+        validSigningNodeIds_ = new uint32[](validSignatureCount_);
+
+        uint256 writeIndex_ = 0;
+
+        for (uint256 index_; index_ < isValid_.length; ++index_) {
+            if (!isValid_[index_]) continue; // Skip invalid signatures.
+
+            unchecked {
+                validSigningNodeIds_[writeIndex_++] = signatures_[index_].nodeId;
+            }
+        }
+    }
+
+    /// @dev Returns true if the signature is from the owner of a canonical node.
+    function _verifySignature(
+        bytes32 digest_,
+        uint32 nodeId_,
+        bytes calldata signature_
+    ) internal view returns (bool isValid_) {
+        // If the node is not canonical, the signature is invalid.
+        if (!INodeRegistryLike(nodeRegistry).getIsCanonicalNode(nodeId_)) return false;
+
+        // Try to recover the signer from the signature. We don't want to revert if the signature is invalid.
+        address signer_ = ECDSA.tryRecoverCalldata(digest_, signature_);
+
+        // The signature is valid if the signer is not `address(0)` and the signer is the owner of the node.
+        return (signer_ != address(0)) && (signer_ == INodeRegistryLike(nodeRegistry).ownerOf(nodeId_));
path: src/settlement-chain/PayerReportManager.sol
user: login: deluca-mike
body: This would require the Node Registry to store the public key (or address) of each node's "signer".
created_at: 2025-05-08T16:51:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,385 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.28;
+
+import { ECDSA } from "../../lib/solady/src/utils/ECDSA.sol";
+import { Initializable } from "../../lib/oz-upgradeable/contracts/proxy/utils/Initializable.sol";
+
+import { SequentialMerkleProofs } from "../libraries/SequentialMerkleProofs.sol";
+
+import { IMigratable } from "../abstract/interfaces/IMigratable.sol";
+import { IParameterRegistryLike, INodeRegistryLike, IPayerRegistryLike } from "./interfaces/External.sol";
+import { IPayerReportManager } from "./interfaces/IPayerReportManager.sol";
+
+import { ERC5267 } from "../abstract/ERC5267.sol";
+import { Migratable } from "../abstract/Migratable.sol";
+
+// TODO: If a node operator owns more than one node, their signature for a payer report will be identical, and therefore
+//       replayable across their nodes. This may not be ideal, so it might be necessary to include the node id of the
+//       signer in the digest.
+
+/**
+ * @title  Implementation of the Payer Report Manager.
+ * @notice This contract handles functionality for submitting and settling payer reports.
+ */
+contract PayerReportManager is IPayerReportManager, Initializable, Migratable, ERC5267 {
+    /* ============ Constants/Immutables ============ */
+
+    // solhint-disable-next-line max-line-length
+    /// @dev keccak256("PayerReport(uint256 attesterNodeId,uint256 originatorNodeId,uint256 startSequenceId,uint256 endSequenceId,bytes32 payersMerkleRoot,address[] nodeOperators)")
+    bytes32 public constant PAYER_REPORT_TYPEHASH = 0x73ce31d3128c9ba6af04b3270a898f0abafdf9bed84e47ec7ebd2f3dd8e9c8a5;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable parameterRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable nodeRegistry;
+
+    /// @inheritdoc IPayerReportManager
+    address public immutable payerRegistry;
+
+    /* ============ UUPS Storage ============ */
+
+    /**
+     * @custom:storage-location erc7201:xmtp.storage.PayerReportManager
+     * @notice The UUPS storage for the payer report manager.
+     * @param  payerReportsByOriginator The mapping of arrays of payer reports by originator node ID.
+     */
+    struct PayerReportManagerStorage {
+        mapping(uint32 originatorId => PayerReport[] payerReports) payerReportsByOriginator;
+    }
+
+    // keccak256(abi.encode(uint256(keccak256("xmtp.storage.PayerReportManager")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 internal constant _PAYER_REPORT_MANAGER_STORAGE_LOCATION =
+        0x26b057ee8e4d60685198828fdf1c618ab8e36b0ab85f54a47b18319f6f718e00;
+
+    function _getPayerReportManagerStorage() internal pure returns (PayerReportManagerStorage storage $) {
+        // slither-disable-next-line assembly
+        assembly {
+            $.slot := _PAYER_REPORT_MANAGER_STORAGE_LOCATION
+        }
+    }
+
+    /* ============ Constructor ============ */
+
+    /**
+     * @notice Constructor for the implementation contract, such that the implementation cannot be initialized.
+     * @param  parameterRegistry_ The address of the parameter registry.
+     * @param  nodeRegistry_      The address of the node registry.
+     * @param  payerRegistry_     The address of the payer registry.
+     * @dev    The parameter registry, node registry, and payer registry must not be the zero address.
+     * @dev    The parameter registry, node registry, and payer registry are immutable so that they are inlined in the
+     *         contract code, and have minimal gas cost.
+     */
+    constructor(address parameterRegistry_, address nodeRegistry_, address payerRegistry_) ERC5267() {
+        require(_isNotZero(parameterRegistry = parameterRegistry_), ZeroParameterRegistry());
+        require(_isNotZero(nodeRegistry = nodeRegistry_), ZeroNodeRegistry());
+        require(_isNotZero(payerRegistry = payerRegistry_), ZeroPayerRegistry());
+
+        _disableInitializers();
+    }
+
+    /* ============ Initialization ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function initialize() public initializer {
+        _initializeERC5267();
+    }
+
+    /* ============ Interactive Functions ============ */
+
+    /// @inheritdoc IPayerReportManager
+    function submit(
+        uint32 originatorNodeId_,
+        uint32 attesterNodeId_,
+        uint32 startSequenceId_,
+        uint32 endSequenceId_,
+        bytes32 payersMerkleRoot_,
+        address[] calldata nodeOperators_,
path: src/settlement-chain/PayerReportManager.sol
user: login: deluca-mike
body: It is a question of who should be allowed to claim from the distribution contract at some point in the future: the owner of the node at claim time, or the owner of the node at the time the report was singed by the nodes?

I would argue to use node IDs and allow the owner of the node at claim time to claim distribution, as it:
- is less work for the nodes when it comes time to build or verify the Payer Reports
- ties revenue to the node, which is better than burdening any other system with the complexity of when and who owned the nodes when the reports were generated or revenue claimed

I do have a worry that the node manager can transfer any node to another address, which then includes all retroaactive unclaimed distribution. If the node admin did not have that power (which I think is best), then a node owner can chose if they claim before transferring/selling their node, or if they are going to transfer/sell their node with the included unclaimed revenue.
created_at: 2025-05-08T16:59:43Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-06T08:32:20Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This update introduces a comprehensive EIP-712 and EIP-5267 domain separator implementation, including new abstract contracts and interfaces. It adds a new `PayerReportManager` contract for managing and settling payer reports with cryptographic proofs and multi-signature validation. Several interfaces, mocks, and test harnesses are added or updated, and related tests are expanded to cover the new structures and flows.

## Changes

| File(s)                                                                                  | Change Summary |
|------------------------------------------------------------------------------------------|---------------|
| `src/abstract/ERC712.sol`, `src/abstract/ERC5267.sol`, `src/abstract/interfaces/IERC712.sol`, `src/abstract/interfaces/IERC5267.sol` | Added new abstract contracts and interfaces for EIP-712 and EIP-5267 domain separation and domain data retrieval. |
| `src/settlement-chain/PayerReportManager.sol`                                            | Added new contract for submitting and settling payer reports with signature and Merkle proof validation. |
| `src/settlement-chain/interfaces/IPayerReportManager.sol`                                | Added new interface defining the API, events, errors, and structs for `PayerReportManager`. |
| `src/settlement-chain/interfaces/External.sol`                                           | Added minimal subset interfaces for node and payer registries required by contracts in the subdirectory. |
| `src/settlement-chain/interfaces/IPayerRegistry.sol`                                     | Updated `settleUsage` to accept a single `PayerFee[]` struct array, added `PayerFee` struct, and removed a custom error. |
| `src/settlement-chain/PayerRegistry.sol`                                                 | Refactored `settleUsage` to use `PayerFee[]` input, updated logic accordingly, and changed return type to total fees settled. |
| `test/unit/PayerRegistry.t.sol`                                                          | Updated tests to use `PayerFee[]` input for `settleUsage`, removed array length mismatch test. |
| `test/unit/PayerReportManager.t.sol`                                                     | Added comprehensive tests for `PayerReportManager`, covering initialization, submission, settlement, migration, and EIP-712 domain logic. |
| `test/utils/Harnesses.sol`                                                               | Added `PayerReportManagerHarness` exposing internal state and signature verification for testing. |
| `test/utils/Mocks.sol`                                                                   | Added `MockNodeRegistry` and `MockPayerRegistry` contracts for test support. |
| `package.json`                                                                           | Updated `slither` script to remove old SARIF output before running analysis. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant PayerReportManager
    participant NodeRegistry
    participant PayerRegistry

    User->>PayerReportManager: submit(originatorNodeId, startSeqId, endSeqId, merkleRoot, nodeIds, signatures)
    PayerReportManager->>NodeRegistry: getIsCanonicalNode(nodeId)
    PayerReportManager->>NodeRegistry: getSigner(nodeId)
    PayerReportManager-->>User: emit PayerReportSubmitted

    User->>PayerReportManager: settle(originatorNodeId, reportIndex, payerFees, proofs)
    PayerReportManager->>PayerRegistry: settleUsage(payerFees)
    PayerRegistry-->>PayerReportManager: feesSettled
    PayerReportManager-->>User: emit PayerReportSubsetSettled
```

## Poem

> 🐇  
> In fields of code where rabbits dwell,  
> New contracts bloom, their stories swell.  
> Domains are signed, reports are kept,  
> With Merkle roots and fees adept.  
> Signatures checked, proofs in tow—  
> A ledger neat, where carrots grow!  
>

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> - CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> 
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 8d2e1dca7be3ac60926c1bec070e252f02ee0609 and 4178f74bdb165c70c962a3e9745479ee801451d2.

</details>

<details>
<summary>📒 Files selected for processing (14)</summary>

* `package.json` (1 hunks)
* `src/abstract/ERC5267.sol` (1 hunks)
* `src/abstract/ERC712.sol` (1 hunks)
* `src/abstract/interfaces/IERC5267.sol` (1 hunks)
* `src/abstract/interfaces/IERC712.sol` (1 hunks)
* `src/settlement-chain/PayerRegistry.sol` (1 hunks)
* `src/settlement-chain/PayerReportManager.sol` (1 hunks)
* `src/settlement-chain/interfaces/External.sol` (1 hunks)
* `src/settlement-chain/interfaces/IPayerRegistry.sol` (2 hunks)
* `src/settlement-chain/interfaces/IPayerReportManager.sol` (1 hunks)
* `test/unit/PayerRegistry.t.sol` (3 hunks)
* `test/unit/PayerReportManager.t.sol` (1 hunks)
* `test/utils/Harnesses.sol` (2 hunks)
* `test/utils/Mocks.sol` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* test/utils/Mocks.sol

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (9)</summary>

* package.json
* test/unit/PayerRegistry.t.sol
* src/settlement-chain/PayerRegistry.sol
* src/abstract/interfaces/IERC5267.sol
* src/settlement-chain/interfaces/IPayerRegistry.sol
* src/settlement-chain/interfaces/External.sol
* test/utils/Harnesses.sol
* src/settlement-chain/PayerReportManager.sol
* src/settlement-chain/interfaces/IPayerReportManager.sol

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 GitHub Check: Lint check</summary>

<details>
<summary>src/abstract/ERC5267.sol</summary>

[warning] 28-28: 
Function order is incorrect, external const function can not go after internal function (line 21)

---

[warning] 28-28: 
Function order is incorrect, external view function can not go after internal  function (line 21)

</details>
<details>
<summary>src/abstract/ERC712.sol</summary>

[warning] 25-25: 
Function order is incorrect, struct definition can not go after state variable declaration (line 16)

---

[warning] 25-25: 
Function order is incorrect, struct definition can not go after state variable declaration (line 16)

---

[warning] 59-59: 
Function name must be in mixedCase                                                                  

</details>
<details>
<summary>src/abstract/interfaces/IERC712.sol</summary>

[warning] 13-13: 
Function name must be in mixedCase

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: Tests
* GitHub Check: Gas Report
* GitHub Check: Code Coverage

</details>

<details>
<summary>🔇 Additional comments (10)</summary><blockquote>

<details>
<summary>src/abstract/ERC712.sol (4)</summary>

`25-27`: **Well-implemented storage pattern for upgrade safety**

The ERC-7201 namespaced storage pattern is correctly implemented for upgrade safety. This approach prevents storage collisions during upgrades by using a unique storage location derived from the namespace.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: Lint check</summary>

[warning] 25-25: 
Function order is incorrect, struct definition can not go after state variable declaration (line 16)

---

[warning] 25-25: 
Function order is incorrect, struct definition can not go after state variable declaration (line 16)

</details>

</details>

---

`69-80`: **Correct EIP-712 domain separator computation**

The domain separator computation follows the EIP-712 standard correctly, incorporating all required fields: domain type hash, name hash, version hash, chain ID, and contract address.

---

`87-89`: **Properly implemented EIP-712 digest computation**

The digest computation correctly implements the EIP-712 standard by prepending the domain separator to the internal digest and hashing the result, facilitating secure typed data signing.

---

`94-99`: **Well-designed extension points for derived contracts**

The abstract virtual functions `_name()` and `_version()` provide clear extension points for derived contracts to specify their domain parameters, enhancing modularity and reusability.

</details>
<details>
<summary>src/abstract/ERC5267.sol (1)</summary>

`28-50`: **Complete and correct EIP-5267 implementation**

The `eip712Domain()` function correctly implements the EIP-5267 standard by returning all required domain parameters:
- The `fields_` value (hex"0f" = 01111) properly indicates which fields are used
- All domain components are returned in the expected format
- The comment clarifies that salt is not used in the domain separator

This implementation provides all necessary information for wallets and dapps to work with the domain.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: Lint check</summary>

[warning] 28-28: 
Function order is incorrect, external const function can not go after internal function (line 21)

---

[warning] 28-28: 
Function order is incorrect, external view function can not go after internal  function (line 21)

</details>

</details>

</details>
<details>
<summary>test/unit/PayerReportManager.t.sol (5)</summary>

`71-87`: **Thorough constructor validation testing**

The constructor tests properly verify that the contract rejects zero addresses for all critical dependencies, ensuring the contract can't be deployed in an invalid state.

---

`109-357`: **Comprehensive submit function test coverage**

These tests thoroughly verify the submit function's behavior, including:
- Input validation for sequence IDs
- Validation of node ID ordering
- Verification of signature sufficiency against canonical nodes
- Event emission with proper parameters
- State updates and report indexing
- Special case handling for zero Merkle roots

This test suite provides excellent coverage of both happy paths and error cases.

---

`361-570`: **Robust settle function test coverage**

The settle function tests comprehensively cover:
- Boundary checks and error conditions
- Merkle proof validation
- External call handling
- Partial and complete settlement scenarios
- Fee accumulation and state updates
- Event emissions

The tests use well-constructed test vectors with valid Merkle proofs to thoroughly test the functionality.

---

`649-815`: **Thorough signature verification testing**

The signature verification tests cover all important aspects:
- Node ID ordering
- Signature sufficiency requirements
- Valid and invalid signature processing
- Canonical node status checks
- Mixed valid/invalid signature scenarios

This ensures the critical cryptographic verification logic is robust and correctly implemented.

---

`918-954`: **Correct EIP-712 domain implementation testing**

These tests verify that both the domain separator computation and domain data exposure via EIP-5267 work correctly, confirming compatibility with wallets and other tools that rely on these standards.

</details>

</blockquote></details>

</details>
<!-- internal state start -->


<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEYDEZyAAUASpETZWaCrKNxU3bABsvkCiQBHbGlcABpIcVwvOkgAIgAzEmouKzRZShsSbnwKXABZTDRSClj0DHoHAWZ1Gno5SGxESkglL2wGNDBqgGsSdGRbSAxHAWaAVgA2cPgMXApFdul7EikqX3IAd0hE6mx/ZEx6bG5aaiWCCNg+xGZnXDAmWaoxZH9sxHUc2Q0YK8ZYTCkZAzBhtJSXPpoWi0dTwfBYfDxSAAURsAGEAOwARgATGV6Ki0WMcRMMegBIg5mgxHCMIhwhsEAxYDx5hJ4ODKYdnDCAF4xGY0CjxalLHIoZjcaJsWbUWmvEheM70C64K5NFFqygkRz2XDcii0RA/OB9AAGAGVAsFZvA0F48pRutErPNEYgzZAvPABFQXJAATISGRIGQARgGDENuoWWghiQtvFsBHxPDwmbSLgADJJeJo/DJ3BmhlMlkkAAeVJeEK9uYUhe282Y6Egjoozr6vHwiJNCGQSZTtPt6nkqHIkcQiGc8ni4uOp3EGCINa7RH2yERy1wURmy7j3DSzTeOU0kAAgvGto8q7h+HxmAtokM0Gx6GbUukKJlsrkChgipQnqBpAowhlCSgqvgkC3P+pA8IefDHrkyDRmqMw1ogsiUiQzA/Ge0KwvC9peLI4RapA74IZkRBIHM3xNNu0QAKpTqQnoDjS8IBmgQYhvOyoRFB/i4HsWDkYkSwMVEApiX8lQMeEcYPjC8TwB0qZYIKZAQYJ9jHD+t5Vt0u4tLRFA+ngtKsvga7SMCWBJiJ/gtCsir4NwMq4Mavydt4vj+EEISMJgIF9P47IJjJLn6vAvjUCubIcjETE2FmXCwNu3CIBwAD0OU0Wq2ACBoTDMDl5bMLg3A5TcdwPPCN6IDlng+DlkwaEY+jGOAUDafwSJoHghCkOQVC1AorDsFwvD8MIojiFIMjyEwShUKo6haDoXUmFA7jAgcWCDQQxBkMo42lZ5XBUFsDhOP69Qrco62aNouhgIY3WmAYiAUAwOUqJSzy4DlhLEqSGiIPgXgcAYsRwwYFjngAkido0Cbdtz+puzKAtIRgXps5KA9St4WlDHIjgoTwkxRoMkhinqoIK8y0IstDTJK0rsCZ5FmkjdOkozsyUCKkb4qGlbaTzfxmoS2I4p615Az8SO3l27JKAdEpSjh7Bylxw5EOQ9CoSyyJI1YYBg2Ss58LQLDaFgwnmSs9ri7gsjcDEgPtE5MQLnGgYIEu4sfEbu69n0Ss0yCYJLHGjw+2I4pqvFHQ+MgvNy7iivwknBB8IcZQoMLFD/r4MywsOvL6w5yacVgZoAPqV+I1ckALDOXPFMwSPgvTIM3rd2t6/LZwrjYsCuzjsFTN6R5AvTyBWQrl5AEWJvXGkUSQ8DcPLAAiDszJ6wmiZnfzm5b8stMfWBNAeY3io/L4kEK9Il6C2AwiHcaqeWMQzSqUVEaJuno5A0BLjCdS0tOz7DIGLTciAvYMHgKpBgt9bjoWAV4I0kAAAUpt7D2lvMmRodAACUZE/jRzEAAcmQP+Ng4tVgfC4uyOMMwrjmXGhxDSH9yIMD2P4WYIEvD4AYN0HG6FpFYCRgfahUcGpAwYfwDYh1oTrn6DWVYaDZAmVoWEFs/J5jexIYpcoxccLcA9hLGgdJ5ToAoFQb4vxUAMWONZDW8c9QGj5P7O+ywX4Fz1BQX2ew+i2xRBbMAN8PZe3oAHew8Bw5Lg6kYBGlgzxeCFLXTOUFyJKFBM4PJ/UJYGRiM/Iq3oMHc3EHjAwUAAByCYiY3jnkDWm6JraM05rrW0Ic+ad09EXFeUtBnjxGdCKKZofp/QBjeEG3T6YQyhmaDqUArDVLUnYyga8N7bC3lZM0u9964iPlgjA+CKFTJ0hcWWyzBZ+DfqJEyV9Ym4kwY7eCVA2Dv20cg0QaD4AxHqEMx5DMOpw1iJ1L6cz/oUkWVpYUoomr8whas6GsN4aIzPCjEaZ1vaOExvIbG4YgT40vJAMm3oYS2ORaLc06KiT00ZvZOYCxIzs12eUWBEtaImXBZiHOJchSMt7EzUujKFDOOkNkco+SazvOtr48oPISxqTLJLBV0Tr6fPqOrCmv9VWnENPAfkJs0i6QrO8PohTAkPxKSEnBeDyH0HQu8uJntvZzHCf4RJ1A4xhwwBHHyoqRaihcqpcgG5yChikLMdMV9D53zROSugxZICMk1dBK1oxQzVG3DEC4wa3ap0Mn8e2lyglOvFEwbw9AARSH+LjWgKtbz2khi5Yp+wWwfCXE+MZZc3YHL4cc05KbLnXM9OW55Tk6TRW0NEd1GBba3G3ioAsFa+hVu+Y6p+fAXXICEbK2YJEGhNDqPIQRyiSYLzPmXf2gbP5xwOCBdQtxuBQLUnKEO2bmRfOwSC3BBxnK8GkLPfBTCSDhFYbScIsikbct0fEfRS58zUzEOEKcOSqE1hJtgEd9pggbiRFqDUR6LH0BCs4FxZSxkOK4lfIYIxKAX3im8eBojwKETXruoDIDkAMZ/suT1uIJXsojWLQ1ms+36jVWai10E36wEUNsOtt7qwXBtfgDUWp4B8DE3ifj98si1r4I8VSRA9i13FpgZe2q2F0nSeYLJOSzqONVJW0QSoxqOM3Dp3IlS+CeAEDU0Mtp6mIE6pAFpWwGWRvAnQLgZoEti2ZT0kuFF4ULKBjlNL0gcoZZWZDLwZoDCQF0CibVRoUvMvluVyrUBkQJo7dM2gKXXKiOTec1N6baDToq1VgAYkcg27WuBDcq2aGbpWKYeym4cwcXEJ29anRQxblXKtDvLptrbG89uVYfQu/Bh2tuVYgdILE2xgOgNCGd87gMTJsAfP6aDTd7vna+1tp7IcXtfHXmx2kH2HtbewIKHEkwW0zCQyD7733wJaJQ2hogGGbxw/h+dy7iAADMeIcO4Ax5jyr4PZiQ4mAAbQALrKdew5+xTnEBNzOxQgA3Itmb5XoWwrAEYHLiK8sFbRePTFMNue4vxadMaRK7qkqRDjJcDSCatJpfN0cUrI1Coa9xXimknicroOEJQ0aTIJ007eAr6nDMxK9Qk0Jfqn36h18HPcljg2htNOGlFYtY7fx8f2ogg7JbDt8KOsbjcD4AHk8h4qaU3C0yIrBnhsGeaAkebDTu7reY7b6zTY7x56CQxGwpZC44uZc5EjOAdM8Eucl6ssIMUCZYN/sUkhG8ntL30rDYLqIZXm3nyuQKYqCg4FMCuJRPiT6sJYgIkBqd6Wv2xoMmZPPO5vzeddKFJ806/zSJAvjSqWFnZdSQXRcabF1pluksdYovVkV6FZm/QRcTMQ+WNcTiKyL0rno+9/DHVxIvhEhRAAVgFHjHkjHHgnkninmnhnhQrsiHuvCClsDngQvnsZnfFaLXhQE3DctijCo0nCs/lJFzLMA8ACDMDlB+BkCQDRIDPRFDGLjilkpLmjONBjNOGUgrhSgYJ7rMm/NJCxABOxOHlmjxNBE3sApBOgAwJGDYn2ruE+LRlapuJRJ+MNsGJ6PnPZNhFCGUrgBsFBPumcE4i4v2M/AhAdPQBJN5KrEMPgKgS8mXBfHavgPqL4LYVuNJNRrnqTrgAAJwTCF7F4LygEoC5I0AbirA1hmgHgaHBhM4jLOJpDhArxAx8rxHNCI52TiwSSTzNhJAAb5w/DDbihFFlhPCkQREFrqC57CGkBWiMQZrxrsCKRyGODeBnCuHbDBiipQRAKJFNE+FgJG6iD+A8RLBmgEAeEHxZC6Z1FgIgTXr/7BhUafwTFNCDwzH2hzECCE7gIrE7okD7GfzCIxDxBNjdqbGZEISqICD2iYCRgLw2o1LqC1hLhqihhBDDi2LMiiDdChSGHBhiRGFmFpAoQSH+APhSAVAgh2pyRKGQgpHq6eC3iKjwDVD/hRFZ7QRIDroAbvAfBqDegewLz8TjQ7FeCgbXAFygrUiAmCgFI0Lm6qKUg5AAToDboRZtor64rr6lJebHE9qlIBblgVL0BH7han4NJQCjbLb3wpLYnAEUnJaVZDZAJiGkEkANEkD4I5GTjU7BQ+BJJZEUBM7hD+FBFGnpy4LPq2F4GIFrzwgkTDHRB8CMhkBNLuGpBuqeiQBDaABJhOqdNuEdqbqfgjQRQJoSQDacRKaQhDGUzggTtm7C6bIG6c0J6RgN6bgL6fXmgfglaRML0dIJmbQHgVzvDEQbzt9CQYIWQfcLItQVRPMb+IUMUKLgQavniqjIShUMSlwWSrjOfp3oTKrnSstObumFGd+CeH+ABBQJmjBABD0ZUNUJOFZEXNqZ5GUmac8gZChDGOhEGg2f0reLIvYFhDQLhJAPYapiBhKMwHgCoE+P4IkCIhONasHmvP4PQXMGflbj8q/O/OEBgIoNIOsWaR/EXOQm+hBD+uNExExFYBaHqByXBIgOIoZFBJcjFFgPuUhF5FAhWKCqSuZDRNieKOBeCPIgvIYigMgMcGuFCEkGFpCJYtUCxfqOxdMCGm3KPCZEQoSFbPTHpNwAZOLDCFOMfiHP4GAMPNXLZlxORJiTrJ5LZoYukvweueoKIQqRFrbF+U0IFLaG7FyLkMsKZelgfNYevMOPQH1CZTaDZR/ChoBUAb2pcVPB0OBSGunA4eCO5GdDkEeV8UpGgEIDkJTEEDkI4Oseyb2oRW2beEkr7iJlZS5X0PIh/G2B2H4N2LeJuNBWBRBXebZQlWebuVyCJHYeiYWkJq1kxfKnpFUEgE5uLJia+HaDQOejhHUX2lVbPF1hbmRn8HlW+YVYxQWjYt8G4DLNqfpQ3NZBOFsRseeeZUNbMEtIDuZKhs3taNzG7BNXAt2PEKRvBJ+KWR/BSXyoiOdW/KHFtbeDVY0OsbaXyqmb4Mlf+f6BcNGu3BepyTuewO2rUcRSNQqPENEDSCHI/AJd4Y2XZeFJQMRSts4qFXpABhIVumAIiGAAIAWDqjMNApBegF4BMbQPINqfQERR/OKCKLFDEF9cadSekhaEqbsM5O5ePk7NaAZmKIaNqNysmPAIFIFdlbZSwg5d0ckkbFzfHEQI7JSMFH5T+usBBYgHQmoqNExQHiiGiAfBaBeP4EwKsK4p7gxenEIkqDiVXjCECMRVEslYedanSMAZ5X0FpGuCOOkvhHSkOL4JZKSTOOHsgDJpCHIbkSWnSfQC7SeB/KVGiTum3pSDBZYs7CCs2lxQesBX8mxj8HkCkhvvfPpCeNNWpY2TEBwi2GaDnWcEtdvLOraVrCzfXSEgaRuPsY7BcVceRC/PnYhHQWZHNQYGiI0AQIURjXwOGLQN6CHEzH0p5BceKL3A5SXGiR/DMA4PEOgiCqIp7enRUM9dsEuhEh3iyZhhbhgNwgNd5c2MxVQEoK+RxfQO3S/SBJMR0iTHZZXeeT0XzLOSlQucUELGKqKOkvyZEZ5sycKb5qKfvuKSeMFjwNsrUpFmfjFmjp0jfilkAwZCA4BFlk/n9CDeQc2fg/OR2ZQJiuVpsmg0+S+exfZeZC/QcBNhRAPW/LQb9bIJmmaDRSQNRCPfw2acIwwULBRJQ+2bBIBBflssfhgonPJm1hBHg2eAAJqohNw2CJ7p7QBNzQAaNWDIgAASZ4Fopjkj6htBBD1DS58jDD4RuDFEilo8epNyxD0j+Q9jdD1gTjYhLjsyRUhaRZgoeOd4KSMw1AOQ3pSgsOlp4T+O+ouQVo1lJACTDQSTPJaTWVmTGBl1bGJ1NghVH2WTsweOcZJpz6gjSGFp1grZBkHN8tfsVTdpC+nNS+jphZ/h5Onjj+3jhDDj9DijS2y1QT2pYTFTeI0VlFMTFAcTGTFZiTZOUOYjKVSM5QFYZT2ObTCZCR0gOzsgURlTNOtpppbo8QyISNlZXjjTVDsjwz/jozzjHDddxdZwmeAz9zMji5fjCj4WYeBlQT7dOQqQvy3DFAAA0iQLINcnOufOgcc0sP9i4P043IM74446M0C+M285mN4wfKnbgFM7gBE7M9EwXIs5k/4RExZbgLkwgks2U7S3iNpIyxGMy+EAU9BcU6Uys2SziHszUxBXU9084Sdhgei1Iz8z448/8ww7ixuvi2/N44gKS+SxRZS7E6KxWQgYWd40aai7INKzY1+MA1iyM4CygWM8q2oxRAS7KxqzM1q1RQs7qyyxDms7K5s0oOWOK/OsgJGbK7Tl8Ka5i/K92TWXzvWc0Z5BQY7O/uA5/siD+faF2eLqwX2dLgObLtwemqOZWmggNNCJnGCYTFiZieZUVAxF3qirpCQzVM9Qm1QULiDGm14LQ3Vos+I3RFmPAL0CMpYoA62bw/24OwvHzD28PQweOyQGA1JnAzPBfP4H0CzUq/KClr5fCOrYs4gPmIWJnsdqbuU7gAABz1iiLYyYA7sBWCMfwZhvxIz7s3v+X2iLPOuePHvGqE1QxJB67QK/rLhoItiCPlXTXbtvteDrGPsMtKmUCfunwSt8ot6FyaK5FRJxhgd0Vhojufi9suBzsLve7HHRr+6+piAysHO/4xicMIS3JaJFyDHzuntBE3aCbrFBpIlOluzhECHNERlRkxnCv6hfvIfGrFkHll58pUnXWI10CRwahC5OI7oSKODsAxASHkSVu3C+DyRvwKhBAC1Xrf3VimzoTkSVAwim0FyuLen8qUgmTKcM1h1ZraiSEqQgqSlD0wkKd8lubQOb5CndrwP8L0ZINBbeeoOjMynn5QCbPJuQhvOQEQUEeyBEdZq0egHZRDZQCakGWQe7ua0HuzDwvrs2s9OCgXuFfpx7sleE5s6NZVb5fLWZjPtoivtFdKDOsS2w4pkdvIGtKFm/sVwvtq21cQV4Gs5Nd5fhGZjNOjQ9e1N6s8eh4VcStBud1y2jRTd+MJeLvoDJdANjsDssemwwxNbUoUe3hmsxmQAADeh3tAWiZprOrHJZEkb3AAvjN5APKctS19vOGaxHqUJ8GCJ/uImYkY6SzZV7MGx7YeWbt1GxALWfCmQ02ZQRgEm4u2isdyPRmywcjNm+jIOVjPLgW5SoTPnFR5QDGb/hITflvjLBbKOyI3W2LNphGGgFlF0ZApD5+Kolt0XPphZjkPsPKhlfkYz8kgOmu5FhbQtWebqY3VZJ7eIcgMpMCsWlBNSPIR2odCiWUrd1ofbtWNvTQPoZuIYcYWZtLuCVhEBdBXkYkese8eBU4YG7XcWUh+Bk0AMhXn8LJ14TTfRRPVPJQPMHwGaGeCiTmJ8bAEXbVLgMyAzwqL5zYX3X8AVgvPbEIhpdvFEvx0ISD2nw0CcAJBcO+TDdyYTFw0KBEN6u7H8DxtvNb38MdvZW0CQC5j2QKWF8F0UqF3vuUsg1F6FtKRg7KVdzPqo2qbMtdw0wcw909y9whG95J595AD91lpbnhzw+z4/uj8282W26z/hzO3RLQxfv90D50yqRX7UClmGcryD/qeh4aWc/Gc+tBZ6/D5Tl/2qZO4HS/XVePaGm6QBgygPNXq/wAjBsDmEPQptGWh6gC9kbsOHoEQ+5DEzyerCAehD37n8D+EjYhsfzjbsAW22PM/vjwYLX8oA49dklPSj7PIM+nWaeueDj5kAiAaoJPgSVgDXI8BeuRLnfmoF0RrGpA6SPG1P4f5CshA81rw2v7c5o2dZUhifyx448SOePWVkM0J4woJcJPDgmTzlwtpFchbMcCrnJiTkOeMGYQVoN8ZjEyOPRBimeCsBIwgKK5SisuHjrIRMuaEQ6PJ13KXlMI2EW8vYWExlsoIQuOrEXW4ov0h2b4YrILAXiW5mYBucIbmnQg6E8GsrTNP+kqIdBeettJYEoBijUlyQW6FsF4OvpfwMq9LcWE5UOqctyqAidwrx0SLyduU91Wts7XmCrVdw2GE+q9VypOhJq7hPclYU440hm02HWym9yY7eMFuCtHIaWADBQw8EWHMqvIk6rEUmAsqZBPCBqH39/A2lbPtIJC7LsIgYJSGlkIv5NMQmNQFovgn6pFojgLVXSpuS4ibgBeR4FKnxWqHPY34UIZ9NuXg58BphiABAnMNlYWga2b8csp6EeGFpxo2ZPtDCKKoDQkBB5cuu4hwFjEShBiImsRSLgh8cBFCdmq5DWCMBw+jAzGjPCjQzAde3EcoE+DXq0oN6eADcHwBqpk0HA2NZACYl14f81qUSP8mZDPx8Ui8rI5ykyyaGWlwKQtf1BLRlElwd6e9WeIfXCC418ahNZMHgiIrfoJwikCmkkCprtDMRyEdYozTaC9oMhDZEyI0AAjHCvapwnTGtRCipZ+KI8c1B41V4T4NMV9LcMcHoo5AnRuSSYZElc60jgmbVEljcg1TY0o6NicOiUkHqKpdKHaKlJUPJrwhlwRCQ+uLDQLkQ9RJNCsDB0maxis0yw7Um+n05oiKhCEM0beANSJQMq+5LwkXB2ES99hJkE6tZACxI10kAANRtbZcvESUWQqtV0hLxTOqjdcEsHwRxFkxkLNLvw0EbLj0w6zXhmaDwzacPmISevs0CXjrFM6rsXwMVXrF01QwMYbIlrBULkVv0GsQjD4HkD1Bix5YPCARA0jERqif8MQrB0JbEsj2G3JVAPmMzEsgKXwoegZHCCixYo6gIDv1HiAUDtupuSxDzVrjpJI8qwYiIoisF0iY0LYJOv4CuAOJm0zg1wVEjTHl5YMlAPRH0KeqMQTIlQsKiyAYAuAbENkKgNwCZC9jzq4sZ8jkngBgB1eEojkLXGwxl1cgz2R8M4GeS8NV+dkeOJxV3FWR8hKgWCVFl75QMPMQXWBmcJLoXUD8KDCfifin5xdL88WU4UE1kFzlfmoDEgbGwkHkCpBiXTQTcIeaLlaB5ks3qlR3z+prhtjE8ELA+Dgh9+5rOxpG2aStIaeQ/GeLfjNY2S4OLTCJEFLHGhSEpQzPxnFlaLbU9J8/eYXcOeFxDaeYUk8NCIpCwicBKUkKdZItYRSvJQiBgaGGnrIAYpCo7eqlJqnhS/mF3ZrgAC1KA+AcFiBSIGiDcuFEfqfMGnabixpZoCaYNLZ4SMZpmzESbQAtApMGWDQyMEhh670sOWW0kWtkyVCUg9pSzfApdz5gYAVpJ0upr9zNBMQ5Rq0OgNSyNC3TNmKotSPvUSnKl9gpLC9ldMOEkB6uArC9gFHBz+oFhfsermdOa7eNfWFYSPHgEjzxAAAQkTRekzTvGyIW0P4FdKVSZpmZXUsNiXR0B8E2OBFmXAPiBpKyF+LKSONakyRgpTKCNt1JmnhE3GXosrh21ulhkCpPXClm62el/9BWviVJptK5ansIm7LMWfk2Ra448QvLYYUI35YSyhWgA9pusPiZ3Yl+AU1JgDMQGH0YeA3DAeTiQEJS4Z/raGbNy1Jnk+ZrreZoLIFYmz1mBkc2UcyiKICzSSZN2dIFOas0LmZ1a5v/UNlgCysrMsQqCwoBDTB6MLOFqgKQKeBnIw3WWaG39BLxqZ508IvuLkEj1OZIcwbp70Rbv9nuuRLOWl3TnNdwiq4y/i4FzloC1uQ3ICUXK0RVzeG5cq2QZQ3E5y45+ydbl7ybklyFpdENuSATEKOs3JyEW2VEwFketu5I6XuYXMNY05jWpslKsmW5mjzVWTrVlpEzmZUsPWjs71uPNwCuzZ59cguS4QITeMU58gZ2SeGHnQCuIY8nWbgCJaO1J5u8nVprKFl0t1p105ZirJybSz/5PLKwny3cLfzVZfskVprOTKrd855MyVrLIiYO0Qg1MxQajyMBRFgYotYGCIJcAaBNApWZgroKzYEoc2ekPNsORMHzU7UQUBwOoEiQpwleAnUvraysjfML+vDaAO3jL6qkZCbwJUJzz+DkIDCYJEwvzxRJBszQndanOxEsKfg7KZoYsrIqAq2EECRCLjnLwd7G8ap8gsHixx0I/AOuGcBtk3BFE0DgeIhBwlsAPCThi4t443vou0LXddClvSUkiHEXIlzCC8LBRYT4DJgjoqmcyEpltLZFLEB4N1L4kgQbB3OWvaQuOJyAiZz0FwEReREJgzA0SVuddAvHAzsgCwmcIKJhyN7RAE+eJZPgBg2BQkcI+AWEtoneJjgoI4iRXIXAko1IP6f+PoOkowCZKp86STZjWGrTTFUFlitiBEBCA4SPEX6Spcen6y6RWJSQfnuEvch88OK2itQk4u8nMSMRndRSfQA7FyouxIcWwuEAEXUhMi4UOEI0BrT29bxF9WhSrQ96s1B4Iys7rRzQigY6MRcHiE0G8GzpCxErHXh4Tk401vij4tcrqE3Ak1JAHIR8ddTwiThUa/mLAA8SVCctmhQKpQO8DqLrFZOSgU4tCW+T1xZlAgPACp2sWf168KlP4F0syVrpqAGErCT4Bwk8Elg1feaC2HfIkxxQ7fc0C8pnRBRLc99C4VBBfg+BFQDvRVJosDxeLVCHilxQyFo77K9hvKEOKqUVRYK2RXkBDMRCPr9Afl/CH4PZxZUoRYlrFZnn0B1GUASIPMIKOIhogYJyi4peaJUjwClQlgowWQPsMO489y85qqlBktJW9LXMa+QLgukH6+SEGo/SLneGi6T9IgmDGmVBGNXfkJSMa4yeg0iDyBWppSGJc5FuDggLOfYUyLvTe7pkxlKtR6Brw87a9eS6Cr6Fgpyg4KWyR87QYQqYLdk9BZC0npQop4jl5qZgrYBqoYrxTapi5bhWnV6TqV2Ax6FgOBmIkfBm0OC8tU7SYXmhmZdk2qJZQYqNA+UZRCgHBCwUmRLir8Iwu2DBoMRmqkhSRC5C9hbMIwgFZ2ouKFClUlAUFc8dXLFFZi/0tHB8Neojl2V9w8wcsNegKRBRH6rFNSSHVKIFhORII+SQiq2W8BJAphJeDSUYATFeE4oYSTRPQToSjAGGfOCnHby7U9EuJTFeIhRw+CWQ/I+DUKLrQ8J1aN67SAgkAoo1vBHS8pM6scrNSVYHo8yvqEgS+KqYqkCgM2H2WUdEqnJSFUvT1gboM61c+QDsoODxAG+5G/ALIF3JFx2ZNcDSEkL43uM+AGqsgA4F7T+BtNtmVANJWIiOE/OfBBagVJ9GaQCl2EGwqvUunr1ahUoxoTlXFEeaxZ4HKgCYNlGJLhaionKllnVGdV3au9D6WqIBkKhbakgNwqrVvZux72YNX9d0EYRlVzFdEJ5bpA+D8TTCNXNLWVX+KSI7KqHOdC7AlEmhiNZtZoJRJPaZiNFkAGjT2LdBGJkcJkEarUXeF0gEM4vdlZmNfF0TgRSU7mjLW3hEJ+J4gHWIqOCoHpvIF4HkatSTB6c7h/W5dcJoMzMAZ1sqCTQXCk1IhMxSSUZK1j61OZHR2WM8o5uXX2RqhfQTUYhO1E6oRtvKA0VyRvKJiCtZ5cmpTWpo4CGxW9dzayJ7Fdh3QFos+s5EQCRh/w5kXTBWJDD9131ck/wCIGrD2jMKz1H4OCwRoY97tJG1DDKhs6llxxnRRLWmAQm1s1V6xXrf1X61+Kbs5pA3sfUeCNp7Qi0MYVdVsJ4R+A5AISapheon0h18IETXtsOQ+B/Bs8E8rL2lWs0qNJO9lVyPL4Lhdl2U+qu1VpDpJohJdQnQ1r4A0aI5tG55KsC1V4luKVkS0cASIRsbs8SQSGANpmq2Jjdla4ofNHgyhx2ga27wBbr11+9KA7IEOCrtGSIi7R3AFiuCEhoLxjdW3VAIkBT5XBM+U8FHRCwb65b/QmWvpaXDXjYa9qiFKyEJpWmmEQt5kEOJuDBHiSYtqCBBEYMi3tihtYgGNBdTzHpUTIjhXWggC/TtjOud7MqoMLq1p0FAMRWHWQGcBwgtlLIjkNtwVr2QBlHIOek9r4B9QeVM+v2A70G2rpdt0seKGWpWlr7z6ZK+tMLF5IGB/avGMtMRuRw1hxNuAZvQYT+CnawJSdF8oX3FA0QE0mqj+MeIlHc7vhrteoPzPmYS03evKB8W7EzEJ7mQHuS+CBOrzXLgDRcEzC0CBHOQsFK9MXgdrv25EigytdEk6rEDV1i8y+AwKY0VBexD0rnGCaSVMKZbGJT674XJJbqK6i8COq5Ukinx2Uu1vq/fXmMnx0K4d4+3THzuDqUxwilNHon/QL5blBRpkGzueiFX90gN1NI7ZhWwoMrlATKmsKLv9FqgcgBYIgLAHPQG79t4YhUr8U/WR9xQs9eekQDSIXaGdjObDADKJ0F6qd4Oy5l31EkaRwg7dT3bBQj1P02K6kmcAssP2r6R1XUuyVpX84hrtJYa3SdmrC5ilU1UpEyfGun5ZSdD7SIJgxXXWUBx1Xkaxg2qbX5GKABC2hnPEdjPZvAc2p8OmrqEDcNVI4qJMo1n7ihi9PhkuFXFHhiTWqhafoc0V8O7jrByKXPS4bQldGUFFa2da/s916if99odYvbUCS+LwgUoK5WMbdhXAvAFBthZviiR0Hf4buFw9we10o961IQRteIGpI5RTGzgZvdIB0E9k2C/ZChSSnzZ9q7NKdXek9zfSExh1ZR+44+knBCxb6vqoVZEfcmgMJM3EEE0JiQZrUtjenATa/VcO80vQNkHZPwcc5w09gtqHajxhDhqhV2cC7LilibhNxPAiAWAN41/zLDrO80c9Dz1vVrDipCU5xbPz9XInc0ElEyEvDIoUQgDe8zWWaDe7Nwm4yOCGefXpM5oNgnEyQznrdjNwpT8Wz0H8lUwyFr9s2veE+DzFFxO+vBpUiZDBGzDh2lJ1UxNrO7LD5TPPRU3nJVM4aMyAM9UypjUwXBr9UqvUy4dwMW8Ww0x1nWFoPhBi6Q13blWNT6AAh4T8EBDZcHmDYBDDNYUGcZ1kkj1aN5wXSQ8Q1BaVYsUEdwtwmnEcj9jC6WkXEq86aSAu8R9Vd5hFLJHEGqRkLAw1i4xZ/aMQQE7YMebAnm9YJmiRCauJQnbJRDR/CUZuNNQuzCk40D/kV3iQoY4iaMCHBZrkmZpbR32DkH7l2LS5Cmspltxbkj1tzshzuQwXvnhFKT1J2k1vOybCnP54s7ebtKAUQLAF6TGloKAR7YDmi/87eZ0Lfg7Nuwo3JHtyyQXyzQFiskpuAoFa+zzm0CpZrAq+rryDKFpp09KZ+mfm7ZIpm89kzvNPmPz2TKWVhe9lyykBiAMBYTnAuQLILTuZbvU3mF6y1ZSSA2afPgUYDfZy8/6akiIDPS15Yc+C5KcQsAzSZgF0yI7QfPLd8LUCjplaeDl1zGLjckboxQHEOU0F1ZDBQYBHOxQmoeQCRFlueOdqpc3aj41Qt4Ke4VIJbPBDbypSZbCz6YDS5Iimns8mO1l7oHgr4ZkRdJFYAVMSbA03HYQTxiiA5dsvECUhrML8vuAYZKKquzi0wqwbtDMMzQJWrwHVwJFFSpD064VXAo3Z5wUsbXMbqlvisQVZT2NLwJUsd5oFpiYSedjB3m4gj8rLISzb5NBSkrgI4FbWI2XQm+XNLTlz0MbnpHVjF+JvFjteGqPGp9ywvSxH/D6IvgCR6xema3QG58c+VuJPXlkEJGG86M6yqHgYpcX5igJcYMK//xCLd9uWga//GIUJomjUA1iMkj5A1Ct9HEEdPUEVGasbUwuUSMDhnsU3hLUdaZ5TqbHKH2Yvca8O1Tsiw2onjBQIUKJ6rdyqGl2+kis3Ef0l+qkjI/QyePybOmSWz7WacW1ZsupcFNNHNUD1KtY7IVdUVj+rtfPYpaoOCVwsL91TZ5z0rjcLKx13G7vsIKS3GeQgqDayWkATNnK4s2Hm02pL9Nh1rCJBFs2v5+rRuVt1Q7UyL8rZvZebixuOXB5LgPG7AAJvPNwsNPPq7nFlCVxBkW3M0kVLJuvn52Y0gW0gTmuwDSA8AunuD1os/91rsCjASbcQBI98CdatHs/lywkwlkwqHENpdIW6WDBPa0Gw0jHKtJvblHCcpTGHWTJHrnkHolXiHympXNhm71BUGu5z4UDTuIOChLhJsWwaXV/CWblDOYAbuTcHrDiCbjgFY8TccxpY197SdiTfwXoHITQDdATZQcB/X0BWPVop8oSGA0on9GJVzgJwno9DjkQHwuSN6ijbuRPKaRWATDJ8CTZistx9NaaR2EhnWR3lSErolyIBw4JQ37A2FICuBqUBCS0AiQdClQDgiWrDNfwZCqhXghFoy4aRbngUIEiy6tb48NaRhRY6zoHyplsewjWQPiKcg12lc8nE5EGcawbjCe+VTwiCCpL2m5oHxzXs9GvR48TPBetgf6b4DYDvgBkKPsPRiIgqJuJmAuSOxsC5madDksVbDjfxNdyAvHkTzJ5U86eQCV70ETUgk93RkBw6jt7AGQOgic4qIkvKbDeBo9p0ePfhAkBS1WoCgNGCaDTB7dL+nEvamrQEOWgsgJhOrRIjXaeTQt5uBQ6wKCOC4meNR1I/wdmPxQ9QXO83Z7twHkD/dru/aGzHTmW7ogDoB3ahxBwxQEZzG9BhYRA4uI+CCOiZ3ZC5BYVI45uNBkzxMceL5pWkNOm3E0JRHF5LHuVRxWX0cjgoxB/meaCGOGHBlYx2/FfkhBOHiLcSNE18ABndI+aFvCZ1KhqAQ0Dj6xzgTsyQAP9IYHk3U72NV5G8ImbSmCSKetJInIkXjq51ievx4n5puDPCHifOR6ZqdlyOZFqWGJUxo+YnRo++RBOi48z++ABSXB3LMbCCO0ysscHBjMT9qoCr3b3RmOtyliOp2o9sxEIz7JAC+1fck1wR2xmTzYSysZFz0I4sR7JKGurPQ3I1yNtNajYyNmS5bbSTpLHfRDa5krAfXCVrgfyNx+cr+YGN/jWSy2MbDRox0w6gKsPYCHDhAkS/nkXz+LJzHEKa0mQEudIozrYOM+ie/im4cThAiy+QJRP0Bjc37MuGNYMvkXOcJlzJDps2s2Xkzkp4k6czwseX0r/yAK6OdCvqlaLaxoy49tGADwkiB0UIEd3EKXj+gmXPpd7XULvj2WUktwm0KsS9419Ojnq9IAaADX8IXhY/wZHgZWTBElgDBBVAAh7dvnABlujRIQxx98QdiEzVCi2wwoyYVp4Hz6AkrYo1GN3Na7CX2gsISAXHRcvyUkQJldrhQmWsC01gk3uCZviGCwpXiPSbyv4MbRsBIxhs/APALSqZqGrHCKj8pG8SiBZrFQ3DA6A5ncvLgzQIbvAGG72qRunwXPEzQiT6B1uG3DY6atwd7rSAWQP10ldEBU01LmgSSIVbksuWvBkwJBrSfDfDW1mkbEXQ/I2Zi5o2DA3pcgFCiUvvRPovUSxJ8KGgmv5bk0WYFdDQA3RDByxBQKtBUBqAXoW0J9ztAmiFoW4oCC5ZFArK1CuoBgCD2ezPbxAAALGh5xAYgAi8QBgLQHiC0AsQZ7DEKLAmBns0PAABjQ+0AccOOWgAEQCJyFaAFHy+wETQ8TB6A20HqJACxABExgFH2gGMFxAUeKPYwM9gIDGACAcQFHgQFiFo8MAccDAVQGMFoAMAMQAnkUAJ/o9jAxgb0QwBB5xBoBpP4nkgNiDPYkAxgARSHGewCJYgKPGINAEETPY44AitAAQBMCKITAsQUn5zxMDjBceIALkMYGgAo9yepPZHiYAwESCqA3PaHpT3R7E8454gZ7aEEp4ECUeJgUnvoAF6gApecQJALEKp7QAYhRgOOakBMAo9WfIv3n0QPZ4o8kBIcOIeIBR/y8kAKPFXgInp6Q/ce0PWIDEKh4xBoeBAbnrEBMDGBqeKPDAIIoZ5xwkAAig3sYGh+w/Bgz2oXtD0J9oB4hEPEHi6OoGg9M5YPMSisn1H0BAA=== -->

<!-- internal state end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=xmtp/smart-contracts&utm_content=56):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: macroscopeapp[bot]
created_at: 2025-05-06T08:33:44Z
body: ### Implement PayerReportManager contract to handle cryptographically verified payer report submission and settlement using EIP-712 and EIP-5267 standards
* Introduces new `PayerReportManager` contract in [PayerReportManager.sol](https://github.com/xmtp/smart-contracts/pull/56/files#diff-6a7edafdcbfd59cce9a879b0794c842ebd8f7eb961fa871b85f25513232a82f9) that manages submission and settlement of payer reports with cryptographic verification using EIP-712 signatures
* Adds EIP-712 and EIP-5267 implementation through new abstract contracts [ERC712.sol](https://github.com/xmtp/smart-contracts/pull/56/files#diff-a0b4194d4166418695d38c94a7a1ecc9b20080fc740f03f280689a743bf45646) and [ERC5267.sol](https://github.com/xmtp/smart-contracts/pull/56/files#diff-02078446ea2d66519447ed36c640b79f6eb4617a042fdc5f8d277034800f05b5)
* Refactors `PayerRegistry.settleUsage` function to accept structured `PayerFee[]` array and return total fees settled
* Introduces interfaces and test infrastructure including mocks, harnesses and comprehensive unit tests

#### 📍Where to Start
Start with the `PayerReportManager` contract implementation in [PayerReportManager.sol](https://github.com/xmtp/smart-contracts/pull/56/files#diff-6a7edafdcbfd59cce9a879b0794c842ebd8f7eb961fa871b85f25513232a82f9), which contains the core logic for report submission and settlement.

----

_[Macroscope](https://app.macroscope.com) summarized 4178f74._
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-06T08:34:47Z
body: ### [LCOV](https://github.com/marketplace/actions/report-lcov) of commit [<code>4178f74</code>](56/commits/4178f74bdb165c70c962a3e9745479ee801451d2) during [Solidity #192](../actions/runs/14976186741)
<pre>Summary coverage rate:
  lines......: 100.0% (782 of 782 lines)
  functions..: 100.0% (226 of 226 functions)
  branches...: 100.0% (166 of 166 branches)

Files changed coverage rate:
                                                           |Lines       |Functions  |Branches    
  Filename                                                 |Rate     Num|Rate    Num|Rate     Num
  ===============================================================================================
  src/abstract/ERC5267.sol                                 |50.0%      4| 0.0%     2|    -      0
  src/abstract/ERC712.sol                                  |46.2%     13| 0.0%     6|    -      0
  src/settlement-chain/PayerRegistry.sol                   |27.6%    192|83.0%    53|    -      0
  src/settlement-chain/PayerReportManager.sol              |22.8%     79|77.8%    18|    -      0</pre>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-06T08:37:23Z
body: # Changes to gas cost

> Generated at commit: [82f01177aaa160bbd2eea72ee9b65eec0793da60](/xmtp/smart-contracts/commit/82f01177aaa160bbd2eea72ee9b65eec0793da60), compared to commit: [c84d3b05a83f3c045ffd896f1f390d0b1de5da0e](/xmtp/smart-contracts/commit/c84d3b05a83f3c045ffd896f1f390d0b1de5da0e)

### 🧾 Summary (20% most significant diffs)

| Contract | Method | Avg (+/-) | % |
|:-|:-|-:|-:|
| **PayerRegistryHarness** | _pausedParameterKey_<br />_settleUsage_<br />_withdrawLockPeriodParameterKey_ | +43 ❌<br />+1,858 ❌<br />+45 ❌ | **+8.67%**<br />**+16.36%**<br />**+8.44%** |
---

<details>
<summary><strong>Full diff report</strong> 👇</summary>
<br />

| Contract | Deployment Cost (+/-) | Method | Min (+/-) | % | Avg (+/-) | % | Median (+/-) | % | Max (+/-) | % | # Calls (+/-) |
|:-|-:|:-|-:|-:|-:|-:|-:|-:|-:|-:|-:|
| **PayerRegistryHarness** | 2,687,402&nbsp;(-15,451) | ___setPendingWithdrawal_<br />___setTotalDebt_<br />___setTotalDeposits_<br />___setWithdrawableTimestamp_<br />_cancelWithdrawal_<br />_deposit(address,uint96)_<br />_deposit(uint96)_<br />_getBalance_<br />_getBalances_<br />_initialize_<br />_migrate_<br />_pausedParameterKey_<br />_requestWithdrawal_<br />_settleUsage_<br />_settler_<br />_updateSettler_<br />_withdrawLockPeriodParameterKey_ | 2,742&nbsp;(0)<br />2,555&nbsp;(0)<br />2,531&nbsp;(0)<br />5,518&nbsp;(-22)<br />2,387&nbsp;(0)<br />2,644&nbsp;(0)<br />2,557&nbsp;(0)<br />602&nbsp;(0)<br />3,461&nbsp;(-53)<br />2,659&nbsp;(0)<br />4,004&nbsp;(+44)<br />539&nbsp;(+43)<br />2,535&nbsp;(-22)<br />2,651&nbsp;(-326)<br />407&nbsp;(-22)<br />3,460&nbsp;(-22)<br />578&nbsp;(+45) | **0.00%**<br />**0.00%**<br />**0.00%**<br />**-0.40%**<br />**0.00%**<br />**0.00%**<br />**0.00%**<br />**0.00%**<br />**-1.51%**<br />**0.00%**<br />**+1.11%**<br />**+8.67%**<br />**-0.86%**<br />**-10.95%**<br />**-5.13%**<br />**-0.63%**<br />**+8.44%** | 6,082&nbsp;(+21)<br />5,312&nbsp;(-3)<br />5,312&nbsp;(+3)<br />5,518&nbsp;(-22)<br />11,935&nbsp;(-70)<br />12,959&nbsp;(-221)<br />12,870&nbsp;(-221)<br />969&nbsp;(-11)<br />3,461&nbsp;(-53)<br />101,885&nbsp;(-25)<br />7,876&nbsp;(+44)<br />539&nbsp;(+43)<br />10,929&nbsp;(+38)<br />13,217&nbsp;(+1,858)<br />1,073&nbsp;(-22)<br />6,691&nbsp;(-22)<br />578&nbsp;(+45) | **+0.35%**<br />**-0.06%**<br />**+0.06%**<br />**-0.40%**<br />**-0.58%**<br />**-1.68%**<br />**-1.69%**<br />**-1.12%**<br />**-1.51%**<br />**-0.02%**<br />**+0.56%**<br />**+8.67%**<br />**+0.35%**<br />**+16.36%**<br />**-2.01%**<br />**-0.33%**<br />**+8.44%** | 5,542&nbsp;(0)<br />5,355&nbsp;(0)<br />5,331&nbsp;(0)<br />5,518&nbsp;(-22)<br />13,108&nbsp;(0)<br />19,197&nbsp;(0)<br />19,107&nbsp;(0)<br />602&nbsp;(0)<br />3,461&nbsp;(-53)<br />103,486&nbsp;(0)<br />7,894&nbsp;(+44)<br />539&nbsp;(+43)<br />11,024&nbsp;(-22)<br />4,801&nbsp;(-342)<br />407&nbsp;(-22)<br />6,922&nbsp;(-22)<br />578&nbsp;(+45) | **0.00%**<br />**0.00%**<br />**0.00%**<br />**-0.40%**<br />**0.00%**<br />**0.00%**<br />**0.00%**<br />**0.00%**<br />**-1.51%**<br />**0.00%**<br />**+0.56%**<br />**+8.67%**<br />**-0.20%**<br />**-6.65%**<br />**-5.13%**<br />**-0.32%**<br />**+8.44%** | 22,642&nbsp;(0)<br />5,355&nbsp;(0)<br />5,331&nbsp;(0)<br />5,518&nbsp;(-22)<br />13,173&nbsp;(0)<br />36,297&nbsp;(0)<br />36,207&nbsp;(0)<br />2,602&nbsp;(0)<br />3,461&nbsp;(-53)<br />103,486&nbsp;(0)<br />11,712&nbsp;(+44)<br />539&nbsp;(+43)<br />28,124&nbsp;(-22)<br />32,199&nbsp;(+25)<br />2,407&nbsp;(-22)<br />9,693&nbsp;(-22)<br />578&nbsp;(+45) | **0.00%**<br />**0.00%**<br />**0.00%**<br />**-0.40%**<br />**0.00%**<br />**0.00%**<br />**0.00%**<br />**0.00%**<br />**-1.51%**<br />**0.00%**<br />**+0.38%**<br />**+8.67%**<br />**-0.08%**<br />**+0.08%**<br />**-0.91%**<br />**-0.23%**<br />**+8.44%** | 264&nbsp;(0)<br />599&nbsp;(-38)<br />1,038&nbsp;(0)<br />4&nbsp;(0)<br />259&nbsp;(0)<br />261&nbsp;(0)<br />261&nbsp;(0)<br />761&nbsp;(0)<br />1&nbsp;(0)<br />63&nbsp;(-1)<br />4&nbsp;(0)<br />1&nbsp;(0)<br />261&nbsp;(0)<br />3&nbsp;(-1)<br />3&nbsp;(0)<br />3&nbsp;(0)<br />1&nbsp;(0) |
| **PayerRegistry** | 0&nbsp;(0) | _settler_ | 407&nbsp;(+23) | **+5.99%** | 407&nbsp;(+23) | **+5.99%** | 407&nbsp;(+23) | **+5.99%** | 407&nbsp;(+23) | **+5.99%** | 1&nbsp;(0) |
| **GroupMessageBroadcasterHarness** | 969,035&nbsp;(0) | ___setMaxPayloadSize_<br />_addMessage_ | 2,479&nbsp;(0)<br />2,679&nbsp;(0) | **0.00%**<br />**0.00%** | 5,079&nbsp;(+200)<br />4,541,518&nbsp;(-45) | **+4.10%**<br />**-0.00%** | 5,279&nbsp;(0)<br />7,242&nbsp;(+117) | **0.00%**<br />**+1.64%** | 5,279&nbsp;(0)<br />67,911,098&nbsp;(0) | **0.00%**<br />**0.00%** | 14&nbsp;(0)<br />15&nbsp;(0) |
| **Proxy** | 105,099&nbsp;(0) | _fallback_ | 471&nbsp;(0) | **0.00%** | 135,365&nbsp;(+1,818) | **+1.36%** | 31,408&nbsp;(0) | **0.00%** | 169,210,019&nbsp;(0) | **0.00%** | 10,095&nbsp;(+93) |
| **IdentityUpdateBroadcasterHarness** | 971,646&nbsp;(0) | _addIdentityUpdate_ | 2,678&nbsp;(0) | **0.00%** | 9,063,120&nbsp;(-4,829) | **-0.05%** | 7,252&nbsp;(0) | **0.00%** | 67,928,197&nbsp;(0) | **0.00%** | 15&nbsp;(0) |
| **Factory** | 785,317&nbsp;(0) | _deployImplementation_ | 21,842&nbsp;(0) | **0.00%** | 216,044,490&nbsp;(-868) | **-0.00%** | 850,947&nbsp;(0) | **0.00%** | 1,937,981,623&nbsp;(0) | **0.00%** | 18&nbsp;(0) |
| **MockFailingMigrator** | 74,725&nbsp;(-12) |  |  |  |  |  |  |  |  |  |  |
</details>
<!-- Sticky Pull Request Comment -->
author_association: NONE