From 48b6852db527a486243cac2be50dca3cd3142a77 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Sat, 3 Jun 2023 14:24:59 -0700 Subject: [PATCH 01/26] add draft `IPaymentCoordinator` interface --- .../interfaces/IPaymentCoordinator.sol | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 src/contracts/interfaces/IPaymentCoordinator.sol diff --git a/src/contracts/interfaces/IPaymentCoordinator.sol b/src/contracts/interfaces/IPaymentCoordinator.sol new file mode 100644 index 000000000..478ea66a8 --- /dev/null +++ b/src/contracts/interfaces/IPaymentCoordinator.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.12; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @title Contract used to coordinate payments from AVSs to operators and splitting earnings from operators to stakers + * @author Layr Labs, Inc. + */ +interface IPaymentCoordinator { + /** + * @notice Struct used by AVSs when informing EigenLayer of a payment made to an operator, but that could be earned at least + * in part with funds from stakers who have delegated to the operator + */ + struct Payment { + IERC20 token; + address operator; + uint256[] amounts; + uint256[] quorums; + uint256 startBlockNumber; + uint256 endBlockNumber; + } + + // @notice Struct used when posting new Merkle roots + struct MerkleRootPost { + // the actual root of the tree + bytes32 root; + // the height of the tree + uint256 height; + // the block number after which the Merkle root can be proved against (to have a delay) + uint256 confirmedAtBlockNumber; + // the block number up to which the payment is calculated. Should be an already-finalized block that is sufficiently in the past. + uint256 calculatedUpToBlockNumber; + } + + // @notice Struct used for leaves of posted Merkle trees + struct MerkleLeaf { + address recipient; + IERC20[] tokens; + // cumulative all-time earnings in each token + uint256[] amounts; + } + + // TODO: better define this event? + event PaymentReceived(address indexed receivedFrom, Payment payment); + + // @notice Emitted when a new Merkle root is posted + event NewMerkleRootPosted(bytes32 root, uint256 height, uint256 confirmedAtBlockNumber, uint256 calculatedUpToBlockNumber); + + /// @notice Array of roots of posted Merkle trees, as well as associated data like tree height + // MerkleRootPost[] public merkleRootPosts; + function merkleRootPosts(uint256 index) external view returns (MerkleRootPost memory); + + /// @notice Getter function for the length of the `merkleRootPosts` array + function merkleRootPostsLength() external view returns (uint256); + + /// @notice Mapping token => recipient => cumulative amount *claimed* + // mapping(IERC20 => mapping(address => uint256)) public cumulativeTokenAmountClaimedByRecipient; + function cumulativeTokenAmountClaimedByRecipient(IERC20 token, address recipient) external view returns (uint256); + + // @notice Constant that defines the share EigenLayer takes of all payments, in basis points + function EIGENLAYER_SHARE_BIPS() external view returns (uint256); + + /** + * @notice Makes a payment of sum(amounts) paid in `token`, for `operator`'s contributions to an AVS, + * between `startBlockNumber` (inclusive) and `endBlockNumber` (inclusive) + * @dev Transfers the total payment from the `msg.sender` to this contract, so the caller must have previously approved + * this contract to transfer at least sum(`amounts`) of `token` + * @notice Emits a `PaymentReceived` event + */ + function makePayment(Payment calldata payment) external; + + // @notice Permissioned function which allows posting a new Merkle root + function postMerkleRoot(IERC20 token, bytes32 newRoot, uint256 height, uint256 calculatedUpToBlockNumber) external; + + // @notice Permissioned function which allows withdrawal of EigenLayer's share of `token` from all received payments + function withdrawEigenlayerShare(IERC20 token, address recipient) external; + + /** + * @notice Called by a staker or operator to prove the inclusion of their earnings in a posted Merkle root and claim them. + * @param token ERC20 token to claim + * @param amount The `amount` contained in the leaf of the Merkle tree to be proved against the specified Merkle root + * @param proof Merkle proof showing that a leaf containing `(msg.sender, amount)` was included in the `rootIndex`-th + * Merkle root posted for the `token` + * @param nodeIndex Specifies the node inside the Merkle tree corresponding to the specified root, `merkleRoots[rootIndex].root`. + * @param rootIndex Specifies the Merkle root to look up, using `merkleRootsByToken[token][rootIndex]` + */ + function proveAndClaimEarnings( + IERC20 token, + uint256 amount, + bytes memory proof, + uint256 nodeIndex, + uint256 rootIndex + ) external; +} \ No newline at end of file From 12be3ed3c225616966ddd978f43458c9736e913a Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Sat, 3 Jun 2023 16:03:51 -0700 Subject: [PATCH 02/26] add the `IDelegationDetails` interface --- .../interfaces/IDelegationDetails.sol | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/contracts/interfaces/IDelegationDetails.sol diff --git a/src/contracts/interfaces/IDelegationDetails.sol b/src/contracts/interfaces/IDelegationDetails.sol new file mode 100644 index 000000000..4622ad0bc --- /dev/null +++ b/src/contracts/interfaces/IDelegationDetails.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.12; + +import "./IServiceManager.sol"; + +/** + * @title Draft interface for a contract that helps structure the delegation relationship between operators and stakers. May be merged with another interface. + * @author Layr Labs, Inc. + */ +interface IDelegationDetails { + // @notice Struct used for storing a single operator's fee for a single service + struct OperatorFee { + // operator fee in basis points + uint16 feeBips; + // boolean flag used for tracking whether or not the operator has ever called `setOperatorFeeForService` for the specific service in question + bool feeSet; + } + + // @notice Event emitted when an `operator` modifies their fee for a service specified by `serviceManager`, from `previousFeeBips` to `newFeeBips` + event OperatorFeeModifiedForService(address indexed operator, IServiceManager indexed serviceManager, uint16 previousFeeBips, uint16 newFeeBips); + + // @notice Mapping operator => ServiceManager => operator fee in basis points + whether or not they have set their fee for the service + // mapping(address => IServiceManager => OperatorFee) public operatorFeeBipsByService; + function operatorFeeBipsByService(address operator, IServiceManager serviceManager) external view returns (OperatorFee memory); + + // uint256 public constant MAX_BIPS = 10000; + function MAX_BIPS() external view returns (uint256); + + // uint256 public constant MAX_OPERATOR_FEE_BIPS = 1500; + function MAX_OPERATOR_FEE_BIPS() external view returns (uint256); + + // @notice Called by an operator to set their fee bips for the specified service. Can only be called once by each operator, for each `serviceManager` + function setOperatorFeeForService(uint16 feeBips, IServiceManager serviceManager) external; + + // @notice Called by an operator to reduce their fee bips for the specified service. + function decreaseOperatorFeeForService(uint16 newFeeBips, IServiceManager serviceManager) external; +} \ No newline at end of file From 8436128a02a3f78a657729dd80f8f37859b3be5e Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Sat, 3 Jun 2023 16:09:26 -0700 Subject: [PATCH 03/26] add new functions and events to `IServiceManager` interface --- src/contracts/interfaces/IServiceManager.sol | 16 ++++++++++++++-- .../middleware/example/HashThreshold.sol | 10 ++++++++++ src/test/mocks/ServiceManagerMock.sol | 11 ++++++++--- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/contracts/interfaces/IServiceManager.sol b/src/contracts/interfaces/IServiceManager.sol index 843313c92..5bfd90619 100644 --- a/src/contracts/interfaces/IServiceManager.sol +++ b/src/contracts/interfaces/IServiceManager.sol @@ -1,14 +1,20 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.12; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "./IDelegationManager.sol"; +import "./IVoteWeigher.sol"; +import "./IPaymentManager.sol"; /** * @title Interface for a `ServiceManager`-type contract. * @author Layr Labs, Inc. */ interface IServiceManager { + // @notice Event that must be emitted when the service's VoteWeigher contract changes + event VoteWeigherChanged(IVoteWeigher previousVoteWeigher, IVoteWeigher newVoteWeigher); + + // @notice Event that must be emitted when the service's PaymentManager contract changes + event PaymentManagerChanged(IPaymentManager previousPaymentManager, IPaymentManager newPaymentManager); + /// @notice Returns the current 'taskNumber' for the middleware function taskNumber() external view returns (uint32); @@ -28,4 +34,10 @@ interface IServiceManager { function latestServeUntilBlock() external view returns (uint32); function owner() external view returns (address); + + // @notice The service's VoteWeigher contract, which could be this contract itself + function voteWeigher() external view returns (IVoteWeigher); + + // @notice The service's PaymentManager contract, which could be this contract itself + function paymentManager() external view returns (IPaymentManager); } \ No newline at end of file diff --git a/src/contracts/middleware/example/HashThreshold.sol b/src/contracts/middleware/example/HashThreshold.sol index 222081cc7..82080eb6d 100644 --- a/src/contracts/middleware/example/HashThreshold.sol +++ b/src/contracts/middleware/example/HashThreshold.sol @@ -136,4 +136,14 @@ contract HashThreshold is Ownable, IServiceManager { function recordStakeUpdate(address operator, uint32 updateBlock, uint32 serveUntilBlock, uint256 prevElement) external onlyRegistry { slasher.recordStakeUpdate(operator, updateBlock, serveUntilBlock, prevElement); } + + /// @inheritdoc IServiceManager + function voteWeigher() external view returns (IVoteWeigher) { + return IVoteWeigher(address(this)); + } + + /// @inheritdoc IServiceManager + function paymentManager() external view returns (IPaymentManager) { + return IPaymentManager(address(this)); + } } diff --git a/src/test/mocks/ServiceManagerMock.sol b/src/test/mocks/ServiceManagerMock.sol index 759077ae5..5b83ff18c 100644 --- a/src/test/mocks/ServiceManagerMock.sol +++ b/src/test/mocks/ServiceManagerMock.sol @@ -38,9 +38,14 @@ contract ServiceManagerMock is IServiceManager, DSTest { return IERC20(address(0)); } - /// @notice The Delegation contract of EigenLayer. - function delegationManager() external pure returns (IDelegationManager) { - return IDelegationManager(address(0)); + // @notice The service's VoteWeigher contract, which could be this contract itself + function voteWeigher() external pure returns (IVoteWeigher) { + return IVoteWeigher(address(0)); + } + + // @notice The service's PaymentManager contract, which could be this contract itself + function paymentManager() external pure returns (IPaymentManager) { + return IPaymentManager(address(0)); } /// @notice Returns the `latestServeUntilBlock` until which operators must serve. From e9ed4c9791a935a6e76cc65ec47b90d458400b91 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Sat, 3 Jun 2023 16:15:35 -0700 Subject: [PATCH 04/26] add the `weightOfStaker` function to the `IVoteWeigher` interface --- src/contracts/interfaces/IVoteWeigher.sol | 7 ++++ src/contracts/middleware/VoteWeigherBase.sol | 39 ++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/contracts/interfaces/IVoteWeigher.sol b/src/contracts/interfaces/IVoteWeigher.sol index 9446de2bf..b1a104805 100644 --- a/src/contracts/interfaces/IVoteWeigher.sol +++ b/src/contracts/interfaces/IVoteWeigher.sol @@ -13,6 +13,13 @@ interface IVoteWeigher { */ function weightOfOperator(address operator, uint256 quorumNumber) external returns (uint96); + /** + * @notice This function computes the total weight of the @param staker in the quorum @param quorumNumber. + * @dev This function should - in general - not take quorum eligibility/requirements into account + * @dev returns zero in the case that `quorumNumber` is greater than or equal to `NUMBER_OF_QUORUMS` + */ + function weightOfStaker(address staker, uint256 quorumNumber) external returns (uint96); + /// @notice Number of quorums that are being used by the middleware. function NUMBER_OF_QUORUMS() external view returns (uint256); diff --git a/src/contracts/middleware/VoteWeigherBase.sol b/src/contracts/middleware/VoteWeigherBase.sol index f7934603d..57ab8adbe 100644 --- a/src/contracts/middleware/VoteWeigherBase.sol +++ b/src/contracts/middleware/VoteWeigherBase.sol @@ -90,6 +90,45 @@ abstract contract VoteWeigherBase is VoteWeigherBaseStorage { return weight; } + /** + * @notice This function computes the total weight of the @param staker in the quorum @param quorumNumber. + * @dev This function should - in general - not take quorum eligibility/requirements into account + * @dev returns zero in the case that `quorumNumber` is greater than or equal to `NUMBER_OF_QUORUMS` + */ + function weightOfStaker(address staker, uint256 quorumNumber) external returns (uint96) { + uint96 weight; + + if (quorumNumber < NUMBER_OF_QUORUMS) { + uint256 stratsLength = strategiesConsideredAndMultipliersLength(quorumNumber); + + StrategyAndWeightingMultiplier memory strategyAndMultiplier; + + for (uint256 i = 0; i < stratsLength;) { + // accessing i^th StrategyAndWeightingMultiplier struct for the quorumNumber + strategyAndMultiplier = strategiesConsideredAndMultipliers[quorumNumber][i]; + + // shares of the staker in the strategy + uint256 sharesAmount = strategyManager.stakerStrategyShares(staker, strategyAndMultiplier.strategy); + + // add the weight from the shares for this strategy to the total weight + if (sharesAmount > 0) { + weight += uint96( + ( + (strategyAndMultiplier.strategy).sharesToUnderlying(sharesAmount) + * strategyAndMultiplier.multiplier + ) / WEIGHTING_DIVISOR + ); + } + + unchecked { + ++i; + } + } + } + + return weight; + } + /// @notice Adds new strategies and the associated multipliers to the @param quorumNumber. function addStrategiesConsideredAndMultipliers( uint256 quorumNumber, From 538dc80950679e2bf25554a1c9e8f8fcf6fd12e9 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Sat, 3 Jun 2023 16:17:42 -0700 Subject: [PATCH 05/26] add `serviceManager` function to the `IPaymentManager` interface --- src/contracts/interfaces/IPaymentManager.sol | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/contracts/interfaces/IPaymentManager.sol b/src/contracts/interfaces/IPaymentManager.sol index 6a1e1efc3..5e04ce8e1 100644 --- a/src/contracts/interfaces/IPaymentManager.sol +++ b/src/contracts/interfaces/IPaymentManager.sol @@ -2,6 +2,7 @@ pragma solidity =0.8.12; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "./IServiceManager.sol"; /** * @title Interface for a `PaymentManager` contract. @@ -86,6 +87,12 @@ interface IPaymentManager { uint256 signedStakeSecondQuorum; } + /** + * @notice The service's ServiceManager contract, which could be this contract itself + * @dev This address should never change! + */ + function serviceManager() external view returns (IServiceManager); + /** * @notice deposit one-time fees by the `msg.sender` with this contract to pay for future tasks of this middleware * @param depositFor could be the `msg.sender` themselves, or a different address for whom `msg.sender` is depositing these future fees From 5051fbd349327fdb8764015596176550623783ef Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Mon, 5 Jun 2023 10:20:01 -0700 Subject: [PATCH 06/26] remove `token` input to `postMerkleRoot` function this input was from a previous design and should no longer be needed given the new merkle leaf/tree structure --- src/contracts/interfaces/IPaymentCoordinator.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contracts/interfaces/IPaymentCoordinator.sol b/src/contracts/interfaces/IPaymentCoordinator.sol index 478ea66a8..e1ef0df25 100644 --- a/src/contracts/interfaces/IPaymentCoordinator.sol +++ b/src/contracts/interfaces/IPaymentCoordinator.sol @@ -4,7 +4,7 @@ pragma solidity =0.8.12; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /** - * @title Contract used to coordinate payments from AVSs to operators and splitting earnings from operators to stakers + * @title Contract used to coordinate payments from AVSs to operators and in particular the subsequency splitting of earnings from operators to stakers * @author Layr Labs, Inc. */ interface IPaymentCoordinator { @@ -71,7 +71,7 @@ interface IPaymentCoordinator { function makePayment(Payment calldata payment) external; // @notice Permissioned function which allows posting a new Merkle root - function postMerkleRoot(IERC20 token, bytes32 newRoot, uint256 height, uint256 calculatedUpToBlockNumber) external; + function postMerkleRoot(bytes32 newRoot, uint256 height, uint256 calculatedUpToBlockNumber) external; // @notice Permissioned function which allows withdrawal of EigenLayer's share of `token` from all received payments function withdrawEigenlayerShare(IERC20 token, address recipient) external; From b304dd6a06821bfbc723f507b2a728df9490775e Mon Sep 17 00:00:00 2001 From: Siddhartha Jagannath Date: Tue, 6 Jun 2023 14:21:39 +0530 Subject: [PATCH 07/26] init commit --- .../interfaces/IPaymentCoordinator.sol | 17 ---- .../operators/PaymentCoordinator.sol | 99 +++++++++++++++++++ 2 files changed, 99 insertions(+), 17 deletions(-) create mode 100644 src/contracts/operators/PaymentCoordinator.sol diff --git a/src/contracts/interfaces/IPaymentCoordinator.sol b/src/contracts/interfaces/IPaymentCoordinator.sol index e1ef0df25..1cd058309 100644 --- a/src/contracts/interfaces/IPaymentCoordinator.sol +++ b/src/contracts/interfaces/IPaymentCoordinator.sol @@ -41,26 +41,9 @@ interface IPaymentCoordinator { uint256[] amounts; } - // TODO: better define this event? - event PaymentReceived(address indexed receivedFrom, Payment payment); - - // @notice Emitted when a new Merkle root is posted - event NewMerkleRootPosted(bytes32 root, uint256 height, uint256 confirmedAtBlockNumber, uint256 calculatedUpToBlockNumber); - - /// @notice Array of roots of posted Merkle trees, as well as associated data like tree height - // MerkleRootPost[] public merkleRootPosts; - function merkleRootPosts(uint256 index) external view returns (MerkleRootPost memory); - /// @notice Getter function for the length of the `merkleRootPosts` array function merkleRootPostsLength() external view returns (uint256); - /// @notice Mapping token => recipient => cumulative amount *claimed* - // mapping(IERC20 => mapping(address => uint256)) public cumulativeTokenAmountClaimedByRecipient; - function cumulativeTokenAmountClaimedByRecipient(IERC20 token, address recipient) external view returns (uint256); - - // @notice Constant that defines the share EigenLayer takes of all payments, in basis points - function EIGENLAYER_SHARE_BIPS() external view returns (uint256); - /** * @notice Makes a payment of sum(amounts) paid in `token`, for `operator`'s contributions to an AVS, * between `startBlockNumber` (inclusive) and `endBlockNumber` (inclusive) diff --git a/src/contracts/operators/PaymentCoordinator.sol b/src/contracts/operators/PaymentCoordinator.sol new file mode 100644 index 000000000..e3ff4c18f --- /dev/null +++ b/src/contracts/operators/PaymentCoordinator.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.12; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "../interfaces/IPaymentCoordinator.sol"; + +/** + * @title Contract used to coordinate payments from AVSs to operators and in particular the subsequency splitting of earnings from operators to stakers + * @author Layr Labs, Inc. + */ +contract PaymentCoordinator is IPaymentCoordinator{ + + /// @notice address approved to post new Merkle roots + address public admin; + + uint256 public merkleRootActivationDelay; + + /// @notice maximum BIPS + uint256 public constant MAX_BIPS = 10000; + + /// @notice Array of roots of posted Merkle trees, as well as associated data like tree height + MerkleRootPost[] public merkleRootPosts; + + /// @notice Mapping token => recipient => cumulative amount *claimed* + mapping(IERC20 => mapping(address => uint256)) public cumulativeTokenAmountClaimedByRecipient; + + /// @notice Constant that defines the share EigenLayer takes of all payments, in basis points + uint256 public EIGENLAYER_SHARE_BIPS; + + // TODO: better define this event? + event PaymentReceived(address indexed receivedFrom, Payment payment); + + // @notice Emitted when a new Merkle root is posted + event NewMerkleRootPosted(MerkleRootPost merkleRootPost); + + modifier onlyAdmin(address sender) { + require(sender == admin, "PaymentCoordinator: Only admin"); + _; + } + + constructor(uint256 _eigenlayerShareBips, address _admin, uint256 _merkleRootActivationDelay) { + require(_eigenlayerShareBips <= MAX_BIPS, "PaymentCoordinator: EigenLayer share cannot be greater than 100%"); + EIGENLAYER_SHARE_BIPS = _eigenlayerShareBips; + admin = _admin; + merkleRootActivationDelay = _merkleRootActivationDelay; + } + + + /** + * @notice Makes a payment of sum(amounts) paid in `token`, for `operator`'s contributions to an AVS, + * between `startBlockNumber` (inclusive) and `endBlockNumber` (inclusive) + * @dev Transfers the total payment from the `msg.sender` to this contract, so the caller must have previously approved + * this contract to transfer at least sum(`amounts`) of `token` + * @notice Emits a `PaymentReceived` event + */ + function makePayment(Payment calldata payment) external{ + uint256 sumAmounts; + for (uint256 i = 0; i < payment.amounts.length; i++) { + sumAmounts += payment.amounts[i]; + } + payment.token.safeTransferFrom(msg.sender, address(this), sumAmounts); + emit PaymentReceived(msg.sender, payment); + } + + // @notice Permissioned function which allows posting a new Merkle root + function postMerkleRoot(bytes32 newRoot, uint256 height, uint256 calculatedUpToBlockNumber) external onlyAdmin(msg.sender){ + MerkleRootPost memory newMerkleRoot = MerkleRootPost(newRoot, height, block.number + merkleRootActivationDelay, calculatedUpToBlockNumber); + merkleRootPosts.push(newMerkleRoot); + emit NewMerkleRootPosted(newMerkleRoot); + } + + // @notice Permissioned function which allows withdrawal of EigenLayer's share of `token` from all received payments + function withdrawEigenlayerShare(IERC20 token, address recipient) external{ + + } + + /** + * @notice Called by a staker or operator to prove the inclusion of their earnings in a posted Merkle root and claim them. + * @param token ERC20 token to claim + * @param amount The `amount` contained in the leaf of the Merkle tree to be proved against the specified Merkle root + * @param proof Merkle proof showing that a leaf containing `(msg.sender, amount)` was included in the `rootIndex`-th + * Merkle root posted for the `token` + * @param nodeIndex Specifies the node inside the Merkle tree corresponding to the specified root, `merkleRoots[rootIndex].root`. + * @param rootIndex Specifies the Merkle root to look up, using `merkleRootsByToken[token][rootIndex]` + */ + function proveAndClaimEarnings( + IERC20 token, + uint256 amount, + bytes memory proof, + uint256 nodeIndex, + uint256 rootIndex + ) external; + + + /// @notice Getter function for the length of the `merkleRootPosts` array + function merkleRootPostsLength() external view returns (uint256){ + return merkleRootPosts.length; + } +} \ No newline at end of file From c7833b171c6ba6f8995cdad557f6235682509fc4 Mon Sep 17 00:00:00 2001 From: Siddhartha Jagannath Date: Tue, 6 Jun 2023 15:00:28 +0530 Subject: [PATCH 08/26] implemented basic payment coordinator --- .../interfaces/IPaymentCoordinator.sol | 2 + .../operators/PaymentCoordinator.sol | 40 ++++++++++++++----- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/contracts/interfaces/IPaymentCoordinator.sol b/src/contracts/interfaces/IPaymentCoordinator.sol index 1cd058309..c2e2245eb 100644 --- a/src/contracts/interfaces/IPaymentCoordinator.sol +++ b/src/contracts/interfaces/IPaymentCoordinator.sol @@ -39,6 +39,8 @@ interface IPaymentCoordinator { IERC20[] tokens; // cumulative all-time earnings in each token uint256[] amounts; + //index in the merkle tree + uint256 index; } /// @notice Getter function for the length of the `merkleRootPosts` array diff --git a/src/contracts/operators/PaymentCoordinator.sol b/src/contracts/operators/PaymentCoordinator.sol index e3ff4c18f..6cefe65bf 100644 --- a/src/contracts/operators/PaymentCoordinator.sol +++ b/src/contracts/operators/PaymentCoordinator.sol @@ -3,6 +3,7 @@ pragma solidity =0.8.12; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "../interfaces/IPaymentCoordinator.sol"; +import "../libraries/Merkle.sol"; /** * @title Contract used to coordinate payments from AVSs to operators and in particular the subsequency splitting of earnings from operators to stakers @@ -24,6 +25,9 @@ contract PaymentCoordinator is IPaymentCoordinator{ /// @notice Mapping token => recipient => cumulative amount *claimed* mapping(IERC20 => mapping(address => uint256)) public cumulativeTokenAmountClaimedByRecipient; + /// @notice Mapping token => cumulative amount earned by EigenLayer + mapping(IERC20 => uint256) public cumulativeEigenLayerTokeEarnings; + /// @notice Constant that defines the share EigenLayer takes of all payments, in basis points uint256 public EIGENLAYER_SHARE_BIPS; @@ -33,6 +37,8 @@ contract PaymentCoordinator is IPaymentCoordinator{ // @notice Emitted when a new Merkle root is posted event NewMerkleRootPosted(MerkleRootPost merkleRootPost); + event PaymentClaimed(MerkleLeaf merkleLeaf); + modifier onlyAdmin(address sender) { require(sender == admin, "PaymentCoordinator: Only admin"); _; @@ -59,6 +65,10 @@ contract PaymentCoordinator is IPaymentCoordinator{ sumAmounts += payment.amounts[i]; } payment.token.safeTransferFrom(msg.sender, address(this), sumAmounts); + + uint256 eigenLayerShare = sumAmounts * EIGENLAYER_SHARE_BIPS / MAX_BIPS; + cumulativeEigenLayerTokeEarnings[payment.token] += eigenLayerShare; + emit PaymentReceived(msg.sender, payment); } @@ -70,26 +80,34 @@ contract PaymentCoordinator is IPaymentCoordinator{ } // @notice Permissioned function which allows withdrawal of EigenLayer's share of `token` from all received payments - function withdrawEigenlayerShare(IERC20 token, address recipient) external{ - + function withdrawEigenlayerShare(IERC20 token, address recipient) external onlyAdmin(msg.sender){ + uint256 amount = cumulativeEigenLayerTokeEarnings[token]; + cumulativeEigenLayerTokeEarnings[token] = 0; + token.safeTransfer(recipient, amount); } /** * @notice Called by a staker or operator to prove the inclusion of their earnings in a posted Merkle root and claim them. - * @param token ERC20 token to claim - * @param amount The `amount` contained in the leaf of the Merkle tree to be proved against the specified Merkle root * @param proof Merkle proof showing that a leaf containing `(msg.sender, amount)` was included in the `rootIndex`-th * Merkle root posted for the `token` - * @param nodeIndex Specifies the node inside the Merkle tree corresponding to the specified root, `merkleRoots[rootIndex].root`. - * @param rootIndex Specifies the Merkle root to look up, using `merkleRootsByToken[token][rootIndex]` + * @param rootIndex Specifies the node inside the Merkle tree corresponding to the specified root, `merkleRoots[rootIndex].root`. */ function proveAndClaimEarnings( - IERC20 token, - uint256 amount, bytes memory proof, - uint256 nodeIndex, - uint256 rootIndex - ) external; + uint256 rootIndex, + MerkleLeaf memory leaf + ) external{ + require(leaf.amounts.length == leaf.tokens.length, "PaymentCoordinator.proveAndClaimEarnings: leaf amounts and tokens must be same length"); + bytes32 leaf = keccak256(abi.encodePacked(leaf.recipient, keccak256(abi.encodePacked(leaf.tokens)), keccak256(abi.encodePacked(leaf.amounts)), leaf.index)); + bytes32 root = merkleRootPosts[rootIndex].root; + require(Merkle.verifyInclusionKeccak(proof, root, leaf, leaf.index), "PaymentCoordinator.proveAndClaimEarnings: Invalid proof"); + + for(uint i = 0; i < leaf.amounts.length; i++) { + leaf.tokens[i].safeTransfer(leaf.recipient, leaf.amounts[i]); + cumulativeTokenAmountClaimedByRecipient[leaf.tokens[i]][leaf.recipient] += leaf.amounts[i]; + } + emit PaymentClaimed(leaf); + } /// @notice Getter function for the length of the `merkleRootPosts` array From 3ececc40e4d8e08c142a7080c1b093e152184939 Mon Sep 17 00:00:00 2001 From: Siddhartha Jagannath Date: Tue, 6 Jun 2023 15:04:42 +0530 Subject: [PATCH 09/26] implemented basic payment coordinator --- src/contracts/operators/PaymentCoordinator.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/contracts/operators/PaymentCoordinator.sol b/src/contracts/operators/PaymentCoordinator.sol index 6cefe65bf..016d8dc27 100644 --- a/src/contracts/operators/PaymentCoordinator.sol +++ b/src/contracts/operators/PaymentCoordinator.sol @@ -82,8 +82,9 @@ contract PaymentCoordinator is IPaymentCoordinator{ // @notice Permissioned function which allows withdrawal of EigenLayer's share of `token` from all received payments function withdrawEigenlayerShare(IERC20 token, address recipient) external onlyAdmin(msg.sender){ uint256 amount = cumulativeEigenLayerTokeEarnings[token]; - cumulativeEigenLayerTokeEarnings[token] = 0; token.safeTransfer(recipient, amount); + + cumulativeEigenLayerTokeEarnings[token] = 0; } /** From 11a0fec2c88a1940fbaa49f9540299750b0113d1 Mon Sep 17 00:00:00 2001 From: Siddhartha Jagannath Date: Tue, 6 Jun 2023 22:04:53 +0530 Subject: [PATCH 10/26] added initializer, owner --- .../operators/PaymentCoordinator.sol | 57 +++++++++++++++---- 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/src/contracts/operators/PaymentCoordinator.sol b/src/contracts/operators/PaymentCoordinator.sol index 016d8dc27..5282806c6 100644 --- a/src/contracts/operators/PaymentCoordinator.sol +++ b/src/contracts/operators/PaymentCoordinator.sol @@ -4,15 +4,22 @@ pragma solidity =0.8.12; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "../interfaces/IPaymentCoordinator.sol"; import "../libraries/Merkle.sol"; +import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; + /** * @title Contract used to coordinate payments from AVSs to operators and in particular the subsequency splitting of earnings from operators to stakers * @author Layr Labs, Inc. */ -contract PaymentCoordinator is IPaymentCoordinator{ +contract PaymentCoordinator is + IPaymentCoordinator, + Initializable, + OwnableUpgradeable +{ /// @notice address approved to post new Merkle roots - address public admin; + address public rootPublisher; uint256 public merkleRootActivationDelay; @@ -39,16 +46,19 @@ contract PaymentCoordinator is IPaymentCoordinator{ event PaymentClaimed(MerkleLeaf merkleLeaf); - modifier onlyAdmin(address sender) { - require(sender == admin, "PaymentCoordinator: Only admin"); + modifier onlyRootPublisher { + require(msg.sender == rootPublisher, "PaymentCoordinator: Only rootPublisher"); _; } - constructor(uint256 _eigenlayerShareBips, address _admin, uint256 _merkleRootActivationDelay) { - require(_eigenlayerShareBips <= MAX_BIPS, "PaymentCoordinator: EigenLayer share cannot be greater than 100%"); - EIGENLAYER_SHARE_BIPS = _eigenlayerShareBips; - admin = _admin; - merkleRootActivationDelay = _merkleRootActivationDelay; + function initialize(address _initialOwner, uint256 _eigenlayerShareBips, address _rootPublisher, uint256 _merkleRootActivationDelay) + external + initializer + { + _transferOwnership(_initialOwner); + _setRootPublisher(_rootPublisher); + _setMerkleRootActivationDelay(_merkleRootActivationDelay); + _setEigenLayerShareBIPS(_eigenlayerShareBips); } @@ -73,14 +83,14 @@ contract PaymentCoordinator is IPaymentCoordinator{ } // @notice Permissioned function which allows posting a new Merkle root - function postMerkleRoot(bytes32 newRoot, uint256 height, uint256 calculatedUpToBlockNumber) external onlyAdmin(msg.sender){ + function postMerkleRoot(bytes32 newRoot, uint256 height, uint256 calculatedUpToBlockNumber) external onlyRootPublisher{ MerkleRootPost memory newMerkleRoot = MerkleRootPost(newRoot, height, block.number + merkleRootActivationDelay, calculatedUpToBlockNumber); merkleRootPosts.push(newMerkleRoot); emit NewMerkleRootPosted(newMerkleRoot); } // @notice Permissioned function which allows withdrawal of EigenLayer's share of `token` from all received payments - function withdrawEigenlayerShare(IERC20 token, address recipient) external onlyAdmin(msg.sender){ + function withdrawEigenlayerShare(IERC20 token, address recipient) external onlyRootPublisher{ uint256 amount = cumulativeEigenLayerTokeEarnings[token]; token.safeTransfer(recipient, amount); @@ -110,9 +120,34 @@ contract PaymentCoordinator is IPaymentCoordinator{ emit PaymentClaimed(leaf); } + function setRootPublisher(address _rootPublisher) external onlyOwner{ + _setRootPublisher(_rootPublisher); + } + + function setMerkleRootActivationDelay(uint256 _merkleRootActivationDelay) external onlyOwner{ + _setMerkleRootActivationDelay(_merkleRootActivationDelay); + } + + function setEigenLayerShareBIPS(uint256 _eigenlayerShareBips) external onlyOwner{ + _setEigenLayerShareBIPS(_eigenlayerShareBips); + } + /// @notice Getter function for the length of the `merkleRootPosts` array function merkleRootPostsLength() external view returns (uint256){ return merkleRootPosts.length; } + + function _setRootPublisher(address _rootPublisher) internal { + rootPublisher = _rootPublisher; + } + + function _setMerkleRootActivationDelay(uint256 _merkleRootActivationDelay) internal { + merkleRootActivationDelay = _merkleRootActivationDelay; + } + + function _setEigenLayerShareBIPS(uint256 _eigenlayerShareBips) internal { + require(_eigenlayerShareBips <= MAX_BIPS, "PaymentCoordinator: EigenLayer share cannot be greater than 100%"); + EIGENLAYER_SHARE_BIPS = _eigenlayerShareBips; + } } \ No newline at end of file From 404b0ae3bd5230047887ae29040b1945bd054e7c Mon Sep 17 00:00:00 2001 From: Siddhartha Jagannath Date: Tue, 6 Jun 2023 22:11:01 +0530 Subject: [PATCH 11/26] added initializer, owner --- src/contracts/operators/PaymentCoordinator.sol | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/contracts/operators/PaymentCoordinator.sol b/src/contracts/operators/PaymentCoordinator.sol index 5282806c6..0fac041bb 100644 --- a/src/contracts/operators/PaymentCoordinator.sol +++ b/src/contracts/operators/PaymentCoordinator.sol @@ -36,7 +36,7 @@ contract PaymentCoordinator is mapping(IERC20 => uint256) public cumulativeEigenLayerTokeEarnings; /// @notice Constant that defines the share EigenLayer takes of all payments, in basis points - uint256 public EIGENLAYER_SHARE_BIPS; + uint256 public eigenLayerShareBIPs; // TODO: better define this event? event PaymentReceived(address indexed receivedFrom, Payment payment); @@ -76,7 +76,7 @@ contract PaymentCoordinator is } payment.token.safeTransferFrom(msg.sender, address(this), sumAmounts); - uint256 eigenLayerShare = sumAmounts * EIGENLAYER_SHARE_BIPS / MAX_BIPS; + uint256 eigenLayerShare = sumAmounts * eigenLayerShareBIPs / MAX_BIPS; cumulativeEigenLayerTokeEarnings[payment.token] += eigenLayerShare; emit PaymentReceived(msg.sender, payment); @@ -109,8 +109,10 @@ contract PaymentCoordinator is MerkleLeaf memory leaf ) external{ require(leaf.amounts.length == leaf.tokens.length, "PaymentCoordinator.proveAndClaimEarnings: leaf amounts and tokens must be same length"); + require(merkleRootPosts[rootIndex].confirmedAtBlockNumber > block.number, "PaymentCoordinator.proveAndClaimEarnings: Merkle root not yet confirmed"); bytes32 leaf = keccak256(abi.encodePacked(leaf.recipient, keccak256(abi.encodePacked(leaf.tokens)), keccak256(abi.encodePacked(leaf.amounts)), leaf.index)); bytes32 root = merkleRootPosts[rootIndex].root; + require(root != bytes32(0), "PaymentCoordinator.proveAndClaimEarnings: Merkle root is null"); require(Merkle.verifyInclusionKeccak(proof, root, leaf, leaf.index), "PaymentCoordinator.proveAndClaimEarnings: Invalid proof"); for(uint i = 0; i < leaf.amounts.length; i++) { @@ -120,6 +122,11 @@ contract PaymentCoordinator is emit PaymentClaimed(leaf); } + function nullifyMerkleRoot(uint256 rootIndex) external onlyRootPublisher{ + require(block.number <= merkleRootPosts[rootIndex].confirmedAtBlockNumber, "PaymentCoordinator.nullifyMerkleRoot: Merkle root already confirmed"); + merkleRootPosts[rootIndex].root = bytes32(0); + } + function setRootPublisher(address _rootPublisher) external onlyOwner{ _setRootPublisher(_rootPublisher); } @@ -148,6 +155,6 @@ contract PaymentCoordinator is function _setEigenLayerShareBIPS(uint256 _eigenlayerShareBips) internal { require(_eigenlayerShareBips <= MAX_BIPS, "PaymentCoordinator: EigenLayer share cannot be greater than 100%"); - EIGENLAYER_SHARE_BIPS = _eigenlayerShareBips; + eigenLayerShareBIPs = _eigenlayerShareBips; } } \ No newline at end of file From 2f611992f72fe64be6c84e47ccdc52709ad6be19 Mon Sep 17 00:00:00 2001 From: Siddhartha Jagannath Date: Wed, 7 Jun 2023 10:03:02 +0530 Subject: [PATCH 12/26] test setup --- .../PaymentCoordinator.sol | 9 ++-- .../interfaces/IPaymentCoordinator.sol | 11 ++--- src/test/unit/PaymentCoordinatorUnit.t.sol | 41 +++++++++++++++++++ 3 files changed, 51 insertions(+), 10 deletions(-) rename src/contracts/{operators => core}/PaymentCoordinator.sol (92%) create mode 100644 src/test/unit/PaymentCoordinatorUnit.t.sol diff --git a/src/contracts/operators/PaymentCoordinator.sol b/src/contracts/core/PaymentCoordinator.sol similarity index 92% rename from src/contracts/operators/PaymentCoordinator.sol rename to src/contracts/core/PaymentCoordinator.sol index 0fac041bb..80c4e857c 100644 --- a/src/contracts/operators/PaymentCoordinator.sol +++ b/src/contracts/core/PaymentCoordinator.sol @@ -6,6 +6,8 @@ import "../interfaces/IPaymentCoordinator.sol"; import "../libraries/Merkle.sol"; import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + /** @@ -17,6 +19,7 @@ contract PaymentCoordinator is Initializable, OwnableUpgradeable { + using SafeERC20 for IERC20; /// @notice address approved to post new Merkle roots address public rootPublisher; @@ -51,7 +54,7 @@ contract PaymentCoordinator is _; } - function initialize(address _initialOwner, uint256 _eigenlayerShareBips, address _rootPublisher, uint256 _merkleRootActivationDelay) + function initialize(address _initialOwner, address _rootPublisher, uint256 _eigenlayerShareBips, uint256 _merkleRootActivationDelay) external initializer { @@ -110,10 +113,10 @@ contract PaymentCoordinator is ) external{ require(leaf.amounts.length == leaf.tokens.length, "PaymentCoordinator.proveAndClaimEarnings: leaf amounts and tokens must be same length"); require(merkleRootPosts[rootIndex].confirmedAtBlockNumber > block.number, "PaymentCoordinator.proveAndClaimEarnings: Merkle root not yet confirmed"); - bytes32 leaf = keccak256(abi.encodePacked(leaf.recipient, keccak256(abi.encodePacked(leaf.tokens)), keccak256(abi.encodePacked(leaf.amounts)), leaf.index)); + bytes32 leafHash = keccak256(abi.encodePacked(leaf.recipient, keccak256(abi.encodePacked(leaf.tokens)), keccak256(abi.encodePacked(leaf.amounts)), leaf.index)); bytes32 root = merkleRootPosts[rootIndex].root; require(root != bytes32(0), "PaymentCoordinator.proveAndClaimEarnings: Merkle root is null"); - require(Merkle.verifyInclusionKeccak(proof, root, leaf, leaf.index), "PaymentCoordinator.proveAndClaimEarnings: Invalid proof"); + require(Merkle.verifyInclusionKeccak(proof, root, leafHash, leaf.index), "PaymentCoordinator.proveAndClaimEarnings: Invalid proof"); for(uint i = 0; i < leaf.amounts.length; i++) { leaf.tokens[i].safeTransfer(leaf.recipient, leaf.amounts[i]); diff --git a/src/contracts/interfaces/IPaymentCoordinator.sol b/src/contracts/interfaces/IPaymentCoordinator.sol index c2e2245eb..98f7bc6ea 100644 --- a/src/contracts/interfaces/IPaymentCoordinator.sol +++ b/src/contracts/interfaces/IPaymentCoordinator.sol @@ -3,6 +3,7 @@ pragma solidity =0.8.12; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + /** * @title Contract used to coordinate payments from AVSs to operators and in particular the subsequency splitting of earnings from operators to stakers * @author Layr Labs, Inc. @@ -63,18 +64,14 @@ interface IPaymentCoordinator { /** * @notice Called by a staker or operator to prove the inclusion of their earnings in a posted Merkle root and claim them. - * @param token ERC20 token to claim - * @param amount The `amount` contained in the leaf of the Merkle tree to be proved against the specified Merkle root * @param proof Merkle proof showing that a leaf containing `(msg.sender, amount)` was included in the `rootIndex`-th * Merkle root posted for the `token` - * @param nodeIndex Specifies the node inside the Merkle tree corresponding to the specified root, `merkleRoots[rootIndex].root`. * @param rootIndex Specifies the Merkle root to look up, using `merkleRootsByToken[token][rootIndex]` + * @param leaf The leaf to be inserted into the Merkle tree */ function proveAndClaimEarnings( - IERC20 token, - uint256 amount, bytes memory proof, - uint256 nodeIndex, - uint256 rootIndex + uint256 rootIndex, + MerkleLeaf memory leaf ) external; } \ No newline at end of file diff --git a/src/test/unit/PaymentCoordinatorUnit.t.sol b/src/test/unit/PaymentCoordinatorUnit.t.sol new file mode 100644 index 000000000..bf2410740 --- /dev/null +++ b/src/test/unit/PaymentCoordinatorUnit.t.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.12; + +import "../../contracts/core/PaymentCoordinator.sol"; +import "../../contracts/interfaces/IPaymentCoordinator.sol"; +import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; + + +import "forge-std/Test.sol"; + + +contract PaymentCoordinatorTest is Test { + PaymentCoordinator public paymentCoordinator; + PaymentCoordinator public paymentCoordinatorImplementation; + ProxyAdmin public proxyAdmin; + + function setUp() public { + proxyAdmin = new ProxyAdmin(); + paymentCoordinatorImplementation = new PaymentCoordinator(); + paymentCoordinator = PaymentCoordinator( + address(new TransparentUpgradeableProxy( + address(paymentCoordinatorImplementation), + address(proxyAdmin), + abi.encodeWithSelector( + paymentCoordinatorImplementation.initialize.selector, + address(this), + address(this), + 1000, + 7200 + ) + ))); + } + + function testInitialize() public { + require(paymentCoordinator.rootPublisher() == address(this), "rootPublisher should be set"); + require(paymentCoordinator.merkleRootActivationDelay() == 7200, "merkleRootActivationDelay should be set"); + require(paymentCoordinator.eigenLayerShareBIPs() == 1000, "eigenLayerShareBIPs should be set"); + require(paymentCoordinator.owner() == address(this), "owner should be set"); + } + +} \ No newline at end of file From 50f546d4ee09941958d20a88d7bb35cb0ea6562e Mon Sep 17 00:00:00 2001 From: Siddhartha Jagannath Date: Wed, 7 Jun 2023 12:46:05 +0530 Subject: [PATCH 13/26] test setup --- src/contracts/core/PaymentCoordinator.sol | 3 +-- src/test/unit/PaymentCoordinatorUnit.t.sol | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/contracts/core/PaymentCoordinator.sol b/src/contracts/core/PaymentCoordinator.sol index 80c4e857c..1b08f1f95 100644 --- a/src/contracts/core/PaymentCoordinator.sol +++ b/src/contracts/core/PaymentCoordinator.sol @@ -79,8 +79,7 @@ contract PaymentCoordinator is } payment.token.safeTransferFrom(msg.sender, address(this), sumAmounts); - uint256 eigenLayerShare = sumAmounts * eigenLayerShareBIPs / MAX_BIPS; - cumulativeEigenLayerTokeEarnings[payment.token] += eigenLayerShare; + cumulativeEigenLayerTokeEarnings[payment.token] += sumAmounts * eigenLayerShareBIPs / MAX_BIPS; emit PaymentReceived(msg.sender, payment); } diff --git a/src/test/unit/PaymentCoordinatorUnit.t.sol b/src/test/unit/PaymentCoordinatorUnit.t.sol index bf2410740..3519399d4 100644 --- a/src/test/unit/PaymentCoordinatorUnit.t.sol +++ b/src/test/unit/PaymentCoordinatorUnit.t.sol @@ -5,6 +5,9 @@ import "../../contracts/core/PaymentCoordinator.sol"; import "../../contracts/interfaces/IPaymentCoordinator.sol"; import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import "../mocks/ERC20Mock.sol"; + + import "forge-std/Test.sol"; @@ -29,6 +32,8 @@ contract PaymentCoordinatorTest is Test { 7200 ) ))); + + dummyToken = new ERC20Mock() } function testInitialize() public { @@ -38,4 +43,21 @@ contract PaymentCoordinatorTest is Test { require(paymentCoordinator.owner() == address(this), "owner should be set"); } + function testMakePayment() external { + PaymentCoordinator.Payment memory payment; + payment.token = dummyToken; + uint256[] memory amounts = new uint256[](2); + amounts[0] = 100; + amounts[1] = 200; + payment.amounts = amounts; + + + paymentCoordinator.makePayment(payment); + + require(paymentCoordinator.cumulativeEigenLayerTokeEarnings(dummyToken) == 30, "cumulativeEigenLayerTokeEarnings should be set"); + require(dummyToken.balance(address(paymentCoordinator)) == 300, "incorrect token balance"); + } + + + } \ No newline at end of file From f27ccd1759059bbec2c20f100541359ef083cf17 Mon Sep 17 00:00:00 2001 From: Siddhartha Jagannath Date: Wed, 7 Jun 2023 14:10:25 +0530 Subject: [PATCH 14/26] commit beforecheckout --- src/test/mocks/ERC20Mock.sol | 2 +- src/test/unit/PaymentCoordinatorUnit.t.sol | 25 ++++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/test/mocks/ERC20Mock.sol b/src/test/mocks/ERC20Mock.sol index 0a0bb6cfa..c18e5bb4c 100644 --- a/src/test/mocks/ERC20Mock.sol +++ b/src/test/mocks/ERC20Mock.sol @@ -41,7 +41,7 @@ contract ERC20Mock is Context, IERC20 { uint256 private _totalSupply; - uint256 initSupply = type(uint88).max; + uint256 initSupply = type(uint256).max; constructor() { _mint(msg.sender, initSupply); diff --git a/src/test/unit/PaymentCoordinatorUnit.t.sol b/src/test/unit/PaymentCoordinatorUnit.t.sol index 3519399d4..4b58c5cc9 100644 --- a/src/test/unit/PaymentCoordinatorUnit.t.sol +++ b/src/test/unit/PaymentCoordinatorUnit.t.sol @@ -13,9 +13,11 @@ import "forge-std/Test.sol"; contract PaymentCoordinatorTest is Test { + Vm cheats = Vm(HEVM_ADDRESS); PaymentCoordinator public paymentCoordinator; PaymentCoordinator public paymentCoordinatorImplementation; ProxyAdmin public proxyAdmin; + ERC20Mock public dummyToken; function setUp() public { proxyAdmin = new ProxyAdmin(); @@ -33,7 +35,7 @@ contract PaymentCoordinatorTest is Test { ) ))); - dummyToken = new ERC20Mock() + dummyToken = new ERC20Mock(); } function testInitialize() public { @@ -43,19 +45,30 @@ contract PaymentCoordinatorTest is Test { require(paymentCoordinator.owner() == address(this), "owner should be set"); } - function testMakePayment() external { + function testMakePayment(uint256[] memory amounts) external { + cheats.assume(_sum(amounts) < dummyToken.balanceOf(address(this))); PaymentCoordinator.Payment memory payment; payment.token = dummyToken; - uint256[] memory amounts = new uint256[](2); - amounts[0] = 100; - amounts[1] = 200; + // uint256[] memory amounts = new uint256[](2); + // amounts[0] = 100; + // amounts[1] = 200; payment.amounts = amounts; paymentCoordinator.makePayment(payment); require(paymentCoordinator.cumulativeEigenLayerTokeEarnings(dummyToken) == 30, "cumulativeEigenLayerTokeEarnings should be set"); - require(dummyToken.balance(address(paymentCoordinator)) == 300, "incorrect token balance"); + require(dummyToken.balanceOf(address(paymentCoordinator)) == 300, "incorrect token balance"); + } + + function _sum(uint[] memory numbers) internal pure returns (uint) { + uint total = 0; + + for (uint i = 0; i < numbers.length; i++) { + total += numbers[i]; + } + + return total; } From 244067b13002598848b2c791b1b3f3578381ca9d Mon Sep 17 00:00:00 2001 From: Siddhartha Jagannath Date: Wed, 7 Jun 2023 17:29:34 +0530 Subject: [PATCH 15/26] got test working --- src/contracts/core/PaymentCoordinator.sol | 6 ++++-- src/test/mocks/ERC20Mock.sol | 2 +- src/test/mocks/ServiceManagerMock.sol | 5 ++--- src/test/unit/PaymentCoordinatorUnit.t.sol | 18 +++++++++--------- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/contracts/core/PaymentCoordinator.sol b/src/contracts/core/PaymentCoordinator.sol index 1b08f1f95..95a150b12 100644 --- a/src/contracts/core/PaymentCoordinator.sol +++ b/src/contracts/core/PaymentCoordinator.sol @@ -8,6 +8,8 @@ import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "forge-std/Test.sol"; + /** @@ -17,7 +19,8 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; contract PaymentCoordinator is IPaymentCoordinator, Initializable, - OwnableUpgradeable + OwnableUpgradeable, + Test { using SafeERC20 for IERC20; @@ -80,7 +83,6 @@ contract PaymentCoordinator is payment.token.safeTransferFrom(msg.sender, address(this), sumAmounts); cumulativeEigenLayerTokeEarnings[payment.token] += sumAmounts * eigenLayerShareBIPs / MAX_BIPS; - emit PaymentReceived(msg.sender, payment); } diff --git a/src/test/mocks/ERC20Mock.sol b/src/test/mocks/ERC20Mock.sol index c18e5bb4c..0a0bb6cfa 100644 --- a/src/test/mocks/ERC20Mock.sol +++ b/src/test/mocks/ERC20Mock.sol @@ -41,7 +41,7 @@ contract ERC20Mock is Context, IERC20 { uint256 private _totalSupply; - uint256 initSupply = type(uint256).max; + uint256 initSupply = type(uint88).max; constructor() { _mint(msg.sender, initSupply); diff --git a/src/test/mocks/ServiceManagerMock.sol b/src/test/mocks/ServiceManagerMock.sol index 5b83ff18c..8393a7db2 100644 --- a/src/test/mocks/ServiceManagerMock.sol +++ b/src/test/mocks/ServiceManagerMock.sol @@ -4,10 +4,9 @@ pragma solidity =0.8.12; import "forge-std/Test.sol"; import "../../contracts/interfaces/IServiceManager.sol"; import "../../contracts/interfaces/ISlasher.sol"; +import "@openzeppelin/contracts/interfaces/IERC20.sol"; -import "forge-std/Test.sol"; - -contract ServiceManagerMock is IServiceManager, DSTest { +contract ServiceManagerMock is IServiceManager, Test { ISlasher public slasher; constructor(ISlasher _slasher) { diff --git a/src/test/unit/PaymentCoordinatorUnit.t.sol b/src/test/unit/PaymentCoordinatorUnit.t.sol index 4b58c5cc9..338234a13 100644 --- a/src/test/unit/PaymentCoordinatorUnit.t.sol +++ b/src/test/unit/PaymentCoordinatorUnit.t.sol @@ -18,6 +18,8 @@ contract PaymentCoordinatorTest is Test { PaymentCoordinator public paymentCoordinatorImplementation; ProxyAdmin public proxyAdmin; ERC20Mock public dummyToken; + uint256 MAX_BIPS = 10000; + uint256 eigenLayerShareBIPs = 1000; function setUp() public { proxyAdmin = new ProxyAdmin(); @@ -30,7 +32,7 @@ contract PaymentCoordinatorTest is Test { paymentCoordinatorImplementation.initialize.selector, address(this), address(this), - 1000, + eigenLayerShareBIPs, 7200 ) ))); @@ -46,25 +48,23 @@ contract PaymentCoordinatorTest is Test { } function testMakePayment(uint256[] memory amounts) external { - cheats.assume(_sum(amounts) < dummyToken.balanceOf(address(this))); + cheats.assume(amounts.length > 0); + uint256 sum = _sum(amounts); PaymentCoordinator.Payment memory payment; payment.token = dummyToken; - // uint256[] memory amounts = new uint256[](2); - // amounts[0] = 100; - // amounts[1] = 200; payment.amounts = amounts; paymentCoordinator.makePayment(payment); - - require(paymentCoordinator.cumulativeEigenLayerTokeEarnings(dummyToken) == 30, "cumulativeEigenLayerTokeEarnings should be set"); - require(dummyToken.balanceOf(address(paymentCoordinator)) == 300, "incorrect token balance"); + require(paymentCoordinator.cumulativeEigenLayerTokeEarnings(dummyToken) == sum * eigenLayerShareBIPs / MAX_BIPS, "cumulativeEigenLayerTokeEarnings should be set"); + require(dummyToken.balanceOf(address(paymentCoordinator)) == sum, "incorrect token balance"); } - function _sum(uint[] memory numbers) internal pure returns (uint) { + function _sum(uint[] memory numbers) internal returns (uint) { uint total = 0; for (uint i = 0; i < numbers.length; i++) { + cheats.assume(numbers[i] < type(uint32).max && numbers[i] > 0); total += numbers[i]; } From 844dbedcc59f4276ff684cb6643e2d48d5d46e9a Mon Sep 17 00:00:00 2001 From: Siddhartha Jagannath Date: Wed, 7 Jun 2023 21:26:35 +0530 Subject: [PATCH 16/26] added several more tests --- src/contracts/core/PaymentCoordinator.sol | 28 +++--- .../interfaces/IPaymentCoordinator.sol | 2 +- src/test/unit/PaymentCoordinatorUnit.t.sol | 89 +++++++++++++++++-- 3 files changed, 98 insertions(+), 21 deletions(-) diff --git a/src/contracts/core/PaymentCoordinator.sol b/src/contracts/core/PaymentCoordinator.sol index 95a150b12..a7d1869b8 100644 --- a/src/contracts/core/PaymentCoordinator.sol +++ b/src/contracts/core/PaymentCoordinator.sol @@ -33,7 +33,7 @@ contract PaymentCoordinator is uint256 public constant MAX_BIPS = 10000; /// @notice Array of roots of posted Merkle trees, as well as associated data like tree height - MerkleRootPost[] public merkleRootPosts; + MerkleRootPost[] public merkleRoots; /// @notice Mapping token => recipient => cumulative amount *claimed* mapping(IERC20 => mapping(address => uint256)) public cumulativeTokenAmountClaimedByRecipient; @@ -89,15 +89,20 @@ contract PaymentCoordinator is // @notice Permissioned function which allows posting a new Merkle root function postMerkleRoot(bytes32 newRoot, uint256 height, uint256 calculatedUpToBlockNumber) external onlyRootPublisher{ MerkleRootPost memory newMerkleRoot = MerkleRootPost(newRoot, height, block.number + merkleRootActivationDelay, calculatedUpToBlockNumber); - merkleRootPosts.push(newMerkleRoot); + merkleRoots.push(newMerkleRoot); emit NewMerkleRootPosted(newMerkleRoot); } + /// @notice Permissioned function which allows rootPublisher to nullify a Merkle root + function nullifyMerkleRoot(uint256 rootIndex) external onlyRootPublisher{ + require(block.number <= merkleRoots[rootIndex].confirmedAtBlockNumber, "PaymentCoordinator.nullifyMerkleRoot: Merkle root already confirmed"); + merkleRoots[rootIndex].root = bytes32(0); + } + // @notice Permissioned function which allows withdrawal of EigenLayer's share of `token` from all received payments function withdrawEigenlayerShare(IERC20 token, address recipient) external onlyRootPublisher{ uint256 amount = cumulativeEigenLayerTokeEarnings[token]; token.safeTransfer(recipient, amount); - cumulativeEigenLayerTokeEarnings[token] = 0; } @@ -113,10 +118,10 @@ contract PaymentCoordinator is MerkleLeaf memory leaf ) external{ require(leaf.amounts.length == leaf.tokens.length, "PaymentCoordinator.proveAndClaimEarnings: leaf amounts and tokens must be same length"); - require(merkleRootPosts[rootIndex].confirmedAtBlockNumber > block.number, "PaymentCoordinator.proveAndClaimEarnings: Merkle root not yet confirmed"); - bytes32 leafHash = keccak256(abi.encodePacked(leaf.recipient, keccak256(abi.encodePacked(leaf.tokens)), keccak256(abi.encodePacked(leaf.amounts)), leaf.index)); - bytes32 root = merkleRootPosts[rootIndex].root; + require(merkleRoots[rootIndex].confirmedAtBlockNumber < block.number, "PaymentCoordinator.proveAndClaimEarnings: Merkle root not yet confirmed"); + bytes32 root = merkleRoots[rootIndex].root; require(root != bytes32(0), "PaymentCoordinator.proveAndClaimEarnings: Merkle root is null"); + bytes32 leafHash = keccak256(abi.encodePacked(leaf.recipient, keccak256(abi.encodePacked(leaf.tokens)), keccak256(abi.encodePacked(leaf.amounts)), leaf.index)); require(Merkle.verifyInclusionKeccak(proof, root, leafHash, leaf.index), "PaymentCoordinator.proveAndClaimEarnings: Invalid proof"); for(uint i = 0; i < leaf.amounts.length; i++) { @@ -126,11 +131,6 @@ contract PaymentCoordinator is emit PaymentClaimed(leaf); } - function nullifyMerkleRoot(uint256 rootIndex) external onlyRootPublisher{ - require(block.number <= merkleRootPosts[rootIndex].confirmedAtBlockNumber, "PaymentCoordinator.nullifyMerkleRoot: Merkle root already confirmed"); - merkleRootPosts[rootIndex].root = bytes32(0); - } - function setRootPublisher(address _rootPublisher) external onlyOwner{ _setRootPublisher(_rootPublisher); } @@ -144,9 +144,9 @@ contract PaymentCoordinator is } - /// @notice Getter function for the length of the `merkleRootPosts` array - function merkleRootPostsLength() external view returns (uint256){ - return merkleRootPosts.length; + /// @notice Getter function for the length of the `merkleRoots` array + function merkleRootsLength() external view returns (uint256){ + return merkleRoots.length; } function _setRootPublisher(address _rootPublisher) internal { diff --git a/src/contracts/interfaces/IPaymentCoordinator.sol b/src/contracts/interfaces/IPaymentCoordinator.sol index 98f7bc6ea..4cec755a4 100644 --- a/src/contracts/interfaces/IPaymentCoordinator.sol +++ b/src/contracts/interfaces/IPaymentCoordinator.sol @@ -45,7 +45,7 @@ interface IPaymentCoordinator { } /// @notice Getter function for the length of the `merkleRootPosts` array - function merkleRootPostsLength() external view returns (uint256); + function merkleRootsLength() external view returns (uint256); /** * @notice Makes a payment of sum(amounts) paid in `token`, for `operator`'s contributions to an AVS, diff --git a/src/test/unit/PaymentCoordinatorUnit.t.sol b/src/test/unit/PaymentCoordinatorUnit.t.sol index 338234a13..74f2e610b 100644 --- a/src/test/unit/PaymentCoordinatorUnit.t.sol +++ b/src/test/unit/PaymentCoordinatorUnit.t.sol @@ -20,6 +20,11 @@ contract PaymentCoordinatorTest is Test { ERC20Mock public dummyToken; uint256 MAX_BIPS = 10000; uint256 eigenLayerShareBIPs = 1000; + uint256 rootPostDelay = 7200; + + event PaymentReceived(address indexed receivedFrom, PaymentCoordinator.Payment payment); + event NewMerkleRootPosted(PaymentCoordinator.MerkleRootPost merkleRootPost); + function setUp() public { proxyAdmin = new ProxyAdmin(); @@ -33,7 +38,7 @@ contract PaymentCoordinatorTest is Test { address(this), address(this), eigenLayerShareBIPs, - 7200 + rootPostDelay ) ))); @@ -47,17 +52,90 @@ contract PaymentCoordinatorTest is Test { require(paymentCoordinator.owner() == address(this), "owner should be set"); } - function testMakePayment(uint256[] memory amounts) external { + function testMakePayment(uint256[] memory amounts) public { cheats.assume(amounts.length > 0); uint256 sum = _sum(amounts); PaymentCoordinator.Payment memory payment; payment.token = dummyToken; payment.amounts = amounts; - + uint256 balanceBefore = dummyToken.balanceOf(address(paymentCoordinator)); + uint256 cumumlativeEarningsBefore = paymentCoordinator.cumulativeEigenLayerTokeEarnings(dummyToken); + cheats.expectEmit(address(paymentCoordinator)); + emit PaymentReceived(address(this), payment); paymentCoordinator.makePayment(payment); - require(paymentCoordinator.cumulativeEigenLayerTokeEarnings(dummyToken) == sum * eigenLayerShareBIPs / MAX_BIPS, "cumulativeEigenLayerTokeEarnings should be set"); - require(dummyToken.balanceOf(address(paymentCoordinator)) == sum, "incorrect token balance"); + require(paymentCoordinator.cumulativeEigenLayerTokeEarnings(dummyToken) - cumumlativeEarningsBefore == sum * eigenLayerShareBIPs / MAX_BIPS, "cumulativeEigenLayerTokeEarnings should be set"); + require(dummyToken.balanceOf(address(paymentCoordinator)) - balanceBefore == sum, "incorrect token balance"); + } + + function testPostMerkleRootFromUnauthorizedAddress(address unauthorizedAddress) external { + cheats.assume(unauthorizedAddress != paymentCoordinator.rootPublisher()); + cheats.startPrank(unauthorizedAddress); + cheats.expectRevert(bytes("PaymentCoordinator: Only rootPublisher")); + paymentCoordinator.postMerkleRoot(bytes32(0), 0, 0); + cheats.stopPrank(); + } + + function testPostMerkleRoot(bytes32 root, uint256 height, uint256 calculatedUpToBlockNumber) public { + PaymentCoordinator.MerkleRootPost memory merkleRootPost; + merkleRootPost.root = root; + merkleRootPost.height = height; + merkleRootPost.calculatedUpToBlockNumber = calculatedUpToBlockNumber; + + uint256 numMerkleRootsBefore = paymentCoordinator.merkleRootsLength(); + + cheats.startPrank(paymentCoordinator.rootPublisher()); + paymentCoordinator.postMerkleRoot(root, height, calculatedUpToBlockNumber); + cheats.stopPrank(); + + (bytes32 rootPost, uint256 heightPost, uint256 confirmedAtBlockNumber, uint256 calculatedUpToBlockNumberPost) = paymentCoordinator.merkleRoots(numMerkleRootsBefore); + require(rootPost == root, "root should be set"); + require(heightPost == height, "height should be set"); + require(confirmedAtBlockNumber == block.number + rootPostDelay, "confirmedAtBlockNumber should be set"); + require(calculatedUpToBlockNumberPost == calculatedUpToBlockNumber, "calculatedUpToBlockNumber should be set"); + } + + function testWithdrawEigenLayerShare(uint256[] memory amounts, address recipient) external { + cheats.assume(recipient != address(0)); + testMakePayment(amounts); + uint256 amountToClaim = paymentCoordinator.cumulativeEigenLayerTokeEarnings(dummyToken); + cheats.startPrank(paymentCoordinator.rootPublisher()); + paymentCoordinator.withdrawEigenlayerShare(dummyToken, recipient); + cheats.stopPrank(); + require(paymentCoordinator.cumulativeEigenLayerTokeEarnings(dummyToken) == 0, "cumulativeEigenLayerTokeEarnings not updated correctly"); + require(dummyToken.balanceOf(recipient) == amountToClaim, "incorrect token balance"); + } + + function testNullifyMerkleRoot(bytes32 root, uint256 height, uint256 calculatedUpToBlockNumber) external { + testPostMerkleRoot(root, height, calculatedUpToBlockNumber); + paymentCoordinator.nullifyMerkleRoot(paymentCoordinator.merkleRootsLength() - 1); + (bytes32 rootPost, , , ) = paymentCoordinator.merkleRoots(paymentCoordinator.merkleRootsLength() - 1); + require(rootPost == bytes32(0), "root should be set"); + + PaymentCoordinator.MerkleLeaf memory leaf; + leaf.amounts = new uint256[](1); + leaf.tokens = new IERC20[](1); + cheats.expectRevert(bytes("PaymentCoordinator.proveAndClaimEarnings: Merkle root is null")); + paymentCoordinator.proveAndClaimEarnings(new bytes(0), 0, leaf); + } + + function testMismatchLengthLeafEntries() external { + PaymentCoordinator.MerkleLeaf memory leaf; + leaf.amounts = new uint256[](1); + leaf.tokens = new IERC20[](2); + cheats.expectRevert(bytes("PaymentCoordinator.proveAndClaimEarnings: leaf amounts and tokens must be same length")); + paymentCoordinator.proveAndClaimEarnings(new bytes(0), 0, leaf); + } + + function testClaimEarningsForRootTooEarly(bytes32 root, uint256 height, uint256 calculatedUpToBlockNumber) external { + testPostMerkleRoot(root, height, calculatedUpToBlockNumber); + (bytes32 rootPost, , , ) = paymentCoordinator.merkleRoots(paymentCoordinator.merkleRootsLength() - 1); + + PaymentCoordinator.MerkleLeaf memory leaf; + leaf.amounts = new uint256[](1); + leaf.tokens = new IERC20[](1); + cheats.expectRevert(bytes("PaymentCoordinator.proveAndClaimEarnings: Merkle root not yet confirmed")); + paymentCoordinator.proveAndClaimEarnings(new bytes(0), 0, leaf); } function _sum(uint[] memory numbers) internal returns (uint) { @@ -67,7 +145,6 @@ contract PaymentCoordinatorTest is Test { cheats.assume(numbers[i] < type(uint32).max && numbers[i] > 0); total += numbers[i]; } - return total; } From 969d5f58dcc75543f6cd47cc974b83c907cd4203 Mon Sep 17 00:00:00 2001 From: Siddhartha Jagannath Date: Wed, 7 Jun 2023 21:45:36 +0530 Subject: [PATCH 17/26] fixed broken testS --- src/test/unit/PaymentCoordinatorUnit.t.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/test/unit/PaymentCoordinatorUnit.t.sol b/src/test/unit/PaymentCoordinatorUnit.t.sol index 74f2e610b..6c67adb23 100644 --- a/src/test/unit/PaymentCoordinatorUnit.t.sol +++ b/src/test/unit/PaymentCoordinatorUnit.t.sol @@ -104,6 +104,8 @@ contract PaymentCoordinatorTest is Test { cheats.stopPrank(); require(paymentCoordinator.cumulativeEigenLayerTokeEarnings(dummyToken) == 0, "cumulativeEigenLayerTokeEarnings not updated correctly"); require(dummyToken.balanceOf(recipient) == amountToClaim, "incorrect token balance"); + + emit log_named_uint("balance", dummyToken.balanceOf(recipient)); } function testNullifyMerkleRoot(bytes32 root, uint256 height, uint256 calculatedUpToBlockNumber) external { @@ -115,6 +117,8 @@ contract PaymentCoordinatorTest is Test { PaymentCoordinator.MerkleLeaf memory leaf; leaf.amounts = new uint256[](1); leaf.tokens = new IERC20[](1); + + cheats.roll(block.number + rootPostDelay + 1); cheats.expectRevert(bytes("PaymentCoordinator.proveAndClaimEarnings: Merkle root is null")); paymentCoordinator.proveAndClaimEarnings(new bytes(0), 0, leaf); } @@ -128,6 +132,7 @@ contract PaymentCoordinatorTest is Test { } function testClaimEarningsForRootTooEarly(bytes32 root, uint256 height, uint256 calculatedUpToBlockNumber) external { + cheats.assume(root != bytes32(0)); testPostMerkleRoot(root, height, calculatedUpToBlockNumber); (bytes32 rootPost, , , ) = paymentCoordinator.merkleRoots(paymentCoordinator.merkleRootsLength() - 1); From cee81d22d66ea87d637c4547d2b9669801542c7d Mon Sep 17 00:00:00 2001 From: Siddhartha Jagannath Date: Wed, 21 Jun 2023 14:25:40 -0700 Subject: [PATCH 18/26] addressed concerns --- src/contracts/core/PaymentCoordinator.sol | 100 ++++++++++++------ .../interfaces/IPaymentCoordinator.sol | 8 ++ src/test/EigenLayerDeployer.t.sol | 1 - src/test/unit/PaymentCoordinatorUnit.t.sol | 71 ++++++++----- 4 files changed, 122 insertions(+), 58 deletions(-) diff --git a/src/contracts/core/PaymentCoordinator.sol b/src/contracts/core/PaymentCoordinator.sol index a7d1869b8..6bb325e64 100644 --- a/src/contracts/core/PaymentCoordinator.sol +++ b/src/contracts/core/PaymentCoordinator.sol @@ -13,7 +13,7 @@ import "forge-std/Test.sol"; /** - * @title Contract used to coordinate payments from AVSs to operators and in particular the subsequency splitting of earnings from operators to stakers + * @title Contract used to coordinate payments from AVSs to operators and in particular the subsequent splitting of earnings from operators to stakers * @author Layr Labs, Inc. */ contract PaymentCoordinator is @@ -27,21 +27,25 @@ contract PaymentCoordinator is /// @notice address approved to post new Merkle roots address public rootPublisher; - uint256 public merkleRootActivationDelay; + /// @notice delay in blocks before a Merkle root can be activated + uint256 public merkleRootActivationDelayBlocks; /// @notice maximum BIPS uint256 public constant MAX_BIPS = 10000; + /// @notice maximum BIPS for eigenlayer + uint256 public constant MAX_EIGENLAYER_SHARE_BIPS = 1500; + /// @notice Array of roots of posted Merkle trees, as well as associated data like tree height - MerkleRootPost[] public merkleRoots; + MerkleRootPost[] internal _merkleRoots; /// @notice Mapping token => recipient => cumulative amount *claimed* mapping(IERC20 => mapping(address => uint256)) public cumulativeTokenAmountClaimedByRecipient; /// @notice Mapping token => cumulative amount earned by EigenLayer - mapping(IERC20 => uint256) public cumulativeEigenLayerTokeEarnings; + mapping(IERC20 => uint256) public cumulativeEigenLayerTokenEarnings; - /// @notice Constant that defines the share EigenLayer takes of all payments, in basis points + /// @notice variable that defines the share EigenLayer takes of all payments, in basis points uint256 public eigenLayerShareBIPs; // TODO: better define this event? @@ -52,18 +56,32 @@ contract PaymentCoordinator is event PaymentClaimed(MerkleLeaf merkleLeaf); + /// @notice Emitted when the rootPublisher is changed + event RootPublisherChanged(address indexed oldRootPublisher, address indexed newRootPublisher); + + /// @notice Emitted when the merkle root activiation delay is changed + event MerkleRootActivationDelayBlocksChanged(uint256 newMerkleRootActivationDelayBlocks); + + + /// @notice Emitted when the EigenLayer's percentage share is changed + event EigenLayerShareBIPSChanged(uint256 newEigenLayerShareBIPS); + modifier onlyRootPublisher { require(msg.sender == rootPublisher, "PaymentCoordinator: Only rootPublisher"); _; } - function initialize(address _initialOwner, address _rootPublisher, uint256 _eigenlayerShareBips, uint256 _merkleRootActivationDelay) + constructor() { + _disableInitializers(); + } + + function initialize(address _initialOwner, address _rootPublisher, uint256 _eigenlayerShareBips, uint256 _merkleRootActivationDelayBlocks) external initializer { _transferOwnership(_initialOwner); _setRootPublisher(_rootPublisher); - _setMerkleRootActivationDelay(_merkleRootActivationDelay); + _setMerkleRootActivationDelayBlocks(_merkleRootActivationDelayBlocks); _setEigenLayerShareBIPS(_eigenlayerShareBips); } @@ -82,33 +100,33 @@ contract PaymentCoordinator is } payment.token.safeTransferFrom(msg.sender, address(this), sumAmounts); - cumulativeEigenLayerTokeEarnings[payment.token] += sumAmounts * eigenLayerShareBIPs / MAX_BIPS; + cumulativeEigenLayerTokenEarnings[payment.token] += sumAmounts * eigenLayerShareBIPs / MAX_BIPS; emit PaymentReceived(msg.sender, payment); } // @notice Permissioned function which allows posting a new Merkle root - function postMerkleRoot(bytes32 newRoot, uint256 height, uint256 calculatedUpToBlockNumber) external onlyRootPublisher{ - MerkleRootPost memory newMerkleRoot = MerkleRootPost(newRoot, height, block.number + merkleRootActivationDelay, calculatedUpToBlockNumber); - merkleRoots.push(newMerkleRoot); + function postMerkleRoot(bytes32 newRoot, uint256 height, uint256 calculatedUpToBlockNumber) external onlyRootPublisher { + MerkleRootPost memory newMerkleRoot = MerkleRootPost(newRoot, height, block.number + merkleRootActivationDelayBlocks, calculatedUpToBlockNumber); + _merkleRoots.push(newMerkleRoot); emit NewMerkleRootPosted(newMerkleRoot); } /// @notice Permissioned function which allows rootPublisher to nullify a Merkle root - function nullifyMerkleRoot(uint256 rootIndex) external onlyRootPublisher{ - require(block.number <= merkleRoots[rootIndex].confirmedAtBlockNumber, "PaymentCoordinator.nullifyMerkleRoot: Merkle root already confirmed"); - merkleRoots[rootIndex].root = bytes32(0); + function nullifyMerkleRoot(uint256 rootIndex) external onlyRootPublisher { + require(block.number <= _merkleRoots[rootIndex].confirmedAtBlockNumber, "PaymentCoordinator.nullifyMerkleRoot: Merkle root already confirmed"); + delete _merkleRoots[rootIndex]; } // @notice Permissioned function which allows withdrawal of EigenLayer's share of `token` from all received payments - function withdrawEigenlayerShare(IERC20 token, address recipient) external onlyRootPublisher{ - uint256 amount = cumulativeEigenLayerTokeEarnings[token]; - token.safeTransfer(recipient, amount); - cumulativeEigenLayerTokeEarnings[token] = 0; + function withdrawEigenlayerShare(IERC20 token, address recipient) external onlyOwner { + uint256 amount = cumulativeEigenLayerTokenEarnings[token]; + cumulativeEigenLayerTokenEarnings[token] = 0; + token.safeTransfer(recipient, amount); } /** * @notice Called by a staker or operator to prove the inclusion of their earnings in a posted Merkle root and claim them. - * @param proof Merkle proof showing that a leaf containing `(msg.sender, amount)` was included in the `rootIndex`-th + * @param proof Merkle proof showing that a leaf was included in the `rootIndex`-th * Merkle root posted for the `token` * @param rootIndex Specifies the node inside the Merkle tree corresponding to the specified root, `merkleRoots[rootIndex].root`. */ @@ -116,49 +134,65 @@ contract PaymentCoordinator is bytes memory proof, uint256 rootIndex, MerkleLeaf memory leaf - ) external{ + ) external { require(leaf.amounts.length == leaf.tokens.length, "PaymentCoordinator.proveAndClaimEarnings: leaf amounts and tokens must be same length"); - require(merkleRoots[rootIndex].confirmedAtBlockNumber < block.number, "PaymentCoordinator.proveAndClaimEarnings: Merkle root not yet confirmed"); - bytes32 root = merkleRoots[rootIndex].root; + require(_merkleRoots[rootIndex].confirmedAtBlockNumber < block.number, "PaymentCoordinator.proveAndClaimEarnings: Merkle root not yet confirmed"); + + bytes32 root = _merkleRoots[rootIndex].root; require(root != bytes32(0), "PaymentCoordinator.proveAndClaimEarnings: Merkle root is null"); - bytes32 leafHash = keccak256(abi.encodePacked(leaf.recipient, keccak256(abi.encodePacked(leaf.tokens)), keccak256(abi.encodePacked(leaf.amounts)), leaf.index)); + + bytes32 leafHash = _computeLeafHash(leaf); require(Merkle.verifyInclusionKeccak(proof, root, leafHash, leaf.index), "PaymentCoordinator.proveAndClaimEarnings: Invalid proof"); - for(uint i = 0; i < leaf.amounts.length; i++) { + for(uint256 i = 0; i < leaf.amounts.length; i++) { leaf.tokens[i].safeTransfer(leaf.recipient, leaf.amounts[i]); cumulativeTokenAmountClaimedByRecipient[leaf.tokens[i]][leaf.recipient] += leaf.amounts[i]; } emit PaymentClaimed(leaf); } - function setRootPublisher(address _rootPublisher) external onlyOwner{ + function setRootPublisher(address _rootPublisher) external onlyOwner { _setRootPublisher(_rootPublisher); } - function setMerkleRootActivationDelay(uint256 _merkleRootActivationDelay) external onlyOwner{ - _setMerkleRootActivationDelay(_merkleRootActivationDelay); + function setMerkleRootActivationDelayBlocks(uint256 _merkleRootActivationDelayBlocks) external onlyOwner { + _setMerkleRootActivationDelayBlocks(_merkleRootActivationDelayBlocks); } - function setEigenLayerShareBIPS(uint256 _eigenlayerShareBips) external onlyOwner{ + function setEigenLayerShareBIPS(uint256 _eigenlayerShareBips) external onlyOwner { _setEigenLayerShareBIPS(_eigenlayerShareBips); } /// @notice Getter function for the length of the `merkleRoots` array - function merkleRootsLength() external view returns (uint256){ - return merkleRoots.length; + function merkleRootsLength() external view returns (uint256) { + return _merkleRoots.length; + } + + /// @notice Getter function for a merkleRoot at a given index + function merkleRoots(uint256 index) external view returns (MerkleRootPost memory) { + return _merkleRoots[index]; } function _setRootPublisher(address _rootPublisher) internal { + address currentRootPublisher = rootPublisher; rootPublisher = _rootPublisher; + emit RootPublisherChanged(currentRootPublisher, rootPublisher); } - function _setMerkleRootActivationDelay(uint256 _merkleRootActivationDelay) internal { - merkleRootActivationDelay = _merkleRootActivationDelay; + function _setMerkleRootActivationDelayBlocks(uint256 _merkleRootActivationDelayBlocks) internal { + merkleRootActivationDelayBlocks = _merkleRootActivationDelayBlocks; + emit MerkleRootActivationDelayBlocksChanged(merkleRootActivationDelayBlocks); } function _setEigenLayerShareBIPS(uint256 _eigenlayerShareBips) internal { - require(_eigenlayerShareBips <= MAX_BIPS, "PaymentCoordinator: EigenLayer share cannot be greater than 100%"); + require(_eigenlayerShareBips <= MAX_EIGENLAYER_SHARE_BIPS, "PaymentCoordinator: EigenLayer share cannot be greater than 100%"); eigenLayerShareBIPs = _eigenlayerShareBips; + + emit EigenLayerShareBIPSChanged(eigenLayerShareBIPs); + } + + function _computeLeafHash(MerkleLeaf memory leaf) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(leaf.recipient, keccak256(abi.encodePacked(leaf.tokens)), keccak256(abi.encodePacked(leaf.amounts)), leaf.index)); } } \ No newline at end of file diff --git a/src/contracts/interfaces/IPaymentCoordinator.sol b/src/contracts/interfaces/IPaymentCoordinator.sol index 4cec755a4..a401b8038 100644 --- a/src/contracts/interfaces/IPaymentCoordinator.sol +++ b/src/contracts/interfaces/IPaymentCoordinator.sol @@ -47,6 +47,13 @@ interface IPaymentCoordinator { /// @notice Getter function for the length of the `merkleRootPosts` array function merkleRootsLength() external view returns (uint256); + + /// @notice getter cumulativeTokenAmountClaimedByRecipient (mapping(IERC20 => mapping(address => uint256)) + function cumulativeTokenAmountClaimedByRecipient(IERC20 token, address recipient) external view returns (uint256); + + /// @notice getter for merkleRootPosts + function merkleRoots(uint256 index) external view returns (MerkleRootPost memory); + /** * @notice Makes a payment of sum(amounts) paid in `token`, for `operator`'s contributions to an AVS, * between `startBlockNumber` (inclusive) and `endBlockNumber` (inclusive) @@ -74,4 +81,5 @@ interface IPaymentCoordinator { uint256 rootIndex, MerkleLeaf memory leaf ) external; + } \ No newline at end of file diff --git a/src/test/EigenLayerDeployer.t.sol b/src/test/EigenLayerDeployer.t.sol index 6059f2642..3d1d1bc7a 100644 --- a/src/test/EigenLayerDeployer.t.sol +++ b/src/test/EigenLayerDeployer.t.sol @@ -113,7 +113,6 @@ contract EigenLayerDeployer is Operators { // addresses excluded from fuzzing due to abnormal behavior. TODO: @Sidu28 define this better and give it a clearer name mapping (address => bool) fuzzedAddressMapping; - //ensures that a passed in address is not set to true in the fuzzedAddressMapping modifier fuzzedAddress(address addr) virtual { cheats.assume(fuzzedAddressMapping[addr] == false); diff --git a/src/test/unit/PaymentCoordinatorUnit.t.sol b/src/test/unit/PaymentCoordinatorUnit.t.sol index 6c67adb23..4780ab5da 100644 --- a/src/test/unit/PaymentCoordinatorUnit.t.sol +++ b/src/test/unit/PaymentCoordinatorUnit.t.sol @@ -22,9 +22,19 @@ contract PaymentCoordinatorTest is Test { uint256 eigenLayerShareBIPs = 1000; uint256 rootPostDelay = 7200; + address rootPublisher = address(25); + address initialOwner = address(26); + + mapping (address => bool) fuzzedAddressMapping; + event PaymentReceived(address indexed receivedFrom, PaymentCoordinator.Payment payment); event NewMerkleRootPosted(PaymentCoordinator.MerkleRootPost merkleRootPost); + modifier fuzzedAddress(address addr) virtual { + cheats.assume(fuzzedAddressMapping[addr] == false); + _; + } + function setUp() public { proxyAdmin = new ProxyAdmin(); @@ -35,21 +45,24 @@ contract PaymentCoordinatorTest is Test { address(proxyAdmin), abi.encodeWithSelector( paymentCoordinatorImplementation.initialize.selector, - address(this), - address(this), + initialOwner, + rootPublisher, eigenLayerShareBIPs, rootPostDelay ) ))); dummyToken = new ERC20Mock(); + + fuzzedAddressMapping[address(0)] = true; + fuzzedAddressMapping[address(proxyAdmin)] = true; } - function testInitialize() public { - require(paymentCoordinator.rootPublisher() == address(this), "rootPublisher should be set"); - require(paymentCoordinator.merkleRootActivationDelay() == 7200, "merkleRootActivationDelay should be set"); + function testInitialize() public view { + require(paymentCoordinator.rootPublisher() == rootPublisher, "rootPublisher should be set"); + require(paymentCoordinator.merkleRootActivationDelayBlocks() == 7200, "merkleRootActivationDelay should be set"); require(paymentCoordinator.eigenLayerShareBIPs() == 1000, "eigenLayerShareBIPs should be set"); - require(paymentCoordinator.owner() == address(this), "owner should be set"); + require(paymentCoordinator.owner() == initialOwner, "owner should be set"); } function testMakePayment(uint256[] memory amounts) public { @@ -60,15 +73,15 @@ contract PaymentCoordinatorTest is Test { payment.amounts = amounts; uint256 balanceBefore = dummyToken.balanceOf(address(paymentCoordinator)); - uint256 cumumlativeEarningsBefore = paymentCoordinator.cumulativeEigenLayerTokeEarnings(dummyToken); + uint256 cumumlativeEarningsBefore = paymentCoordinator.cumulativeEigenLayerTokenEarnings(dummyToken); cheats.expectEmit(address(paymentCoordinator)); emit PaymentReceived(address(this), payment); paymentCoordinator.makePayment(payment); - require(paymentCoordinator.cumulativeEigenLayerTokeEarnings(dummyToken) - cumumlativeEarningsBefore == sum * eigenLayerShareBIPs / MAX_BIPS, "cumulativeEigenLayerTokeEarnings should be set"); + require(paymentCoordinator.cumulativeEigenLayerTokenEarnings(dummyToken) - cumumlativeEarningsBefore == sum * eigenLayerShareBIPs / MAX_BIPS, "cumulativeEigenLayerTokeEarnings should be set"); require(dummyToken.balanceOf(address(paymentCoordinator)) - balanceBefore == sum, "incorrect token balance"); } - function testPostMerkleRootFromUnauthorizedAddress(address unauthorizedAddress) external { + function testPostMerkleRootFromUnauthorizedAddress(address unauthorizedAddress) external fuzzedAddress(unauthorizedAddress) { cheats.assume(unauthorizedAddress != paymentCoordinator.rootPublisher()); cheats.startPrank(unauthorizedAddress); cheats.expectRevert(bytes("PaymentCoordinator: Only rootPublisher")); @@ -88,31 +101,33 @@ contract PaymentCoordinatorTest is Test { paymentCoordinator.postMerkleRoot(root, height, calculatedUpToBlockNumber); cheats.stopPrank(); - (bytes32 rootPost, uint256 heightPost, uint256 confirmedAtBlockNumber, uint256 calculatedUpToBlockNumberPost) = paymentCoordinator.merkleRoots(numMerkleRootsBefore); - require(rootPost == root, "root should be set"); - require(heightPost == height, "height should be set"); - require(confirmedAtBlockNumber == block.number + rootPostDelay, "confirmedAtBlockNumber should be set"); - require(calculatedUpToBlockNumberPost == calculatedUpToBlockNumber, "calculatedUpToBlockNumber should be set"); + IPaymentCoordinator.MerkleRootPost memory post = paymentCoordinator.merkleRoots(numMerkleRootsBefore); + require(post.root == root, "root should be set"); + require(post.height == height, "height should be set"); + require(post.confirmedAtBlockNumber == block.number + rootPostDelay, "confirmedAtBlockNumber should be set"); + require(post.calculatedUpToBlockNumber == calculatedUpToBlockNumber, "calculatedUpToBlockNumber should be set"); } function testWithdrawEigenLayerShare(uint256[] memory amounts, address recipient) external { cheats.assume(recipient != address(0)); + cheats.assume(recipient != address(paymentCoordinator)); + uint256 balanceBefore = dummyToken.balanceOf(recipient); testMakePayment(amounts); - uint256 amountToClaim = paymentCoordinator.cumulativeEigenLayerTokeEarnings(dummyToken); - cheats.startPrank(paymentCoordinator.rootPublisher()); + uint256 amountToClaim = paymentCoordinator.cumulativeEigenLayerTokenEarnings(dummyToken); + cheats.startPrank(paymentCoordinator.owner()); paymentCoordinator.withdrawEigenlayerShare(dummyToken, recipient); cheats.stopPrank(); - require(paymentCoordinator.cumulativeEigenLayerTokeEarnings(dummyToken) == 0, "cumulativeEigenLayerTokeEarnings not updated correctly"); - require(dummyToken.balanceOf(recipient) == amountToClaim, "incorrect token balance"); - - emit log_named_uint("balance", dummyToken.balanceOf(recipient)); + require(paymentCoordinator.cumulativeEigenLayerTokenEarnings(dummyToken) == 0, "cumulativeEigenLayerTokeEarnings not updated correctly"); + require(dummyToken.balanceOf(recipient) - balanceBefore == amountToClaim, "incorrect token balance"); } function testNullifyMerkleRoot(bytes32 root, uint256 height, uint256 calculatedUpToBlockNumber) external { testPostMerkleRoot(root, height, calculatedUpToBlockNumber); + cheats.startPrank(paymentCoordinator.rootPublisher()); paymentCoordinator.nullifyMerkleRoot(paymentCoordinator.merkleRootsLength() - 1); - (bytes32 rootPost, , , ) = paymentCoordinator.merkleRoots(paymentCoordinator.merkleRootsLength() - 1); - require(rootPost == bytes32(0), "root should be set"); + cheats.stopPrank(); + IPaymentCoordinator.MerkleRootPost memory post = paymentCoordinator.merkleRoots(paymentCoordinator.merkleRootsLength() - 1); + require(post.root == bytes32(0), "root should be set"); PaymentCoordinator.MerkleLeaf memory leaf; leaf.amounts = new uint256[](1); @@ -123,6 +138,14 @@ contract PaymentCoordinatorTest is Test { paymentCoordinator.proveAndClaimEarnings(new bytes(0), 0, leaf); } + function testCallNullifyMerkleRootFromUnauthorizedAddress(address unauthorizedAddress) external fuzzedAddress(unauthorizedAddress) { + cheats.assume(unauthorizedAddress != paymentCoordinator.rootPublisher()); + cheats.startPrank(unauthorizedAddress); + cheats.expectRevert(bytes("PaymentCoordinator: Only rootPublisher")); + paymentCoordinator.nullifyMerkleRoot(0); + cheats.stopPrank(); + } + function testMismatchLengthLeafEntries() external { PaymentCoordinator.MerkleLeaf memory leaf; leaf.amounts = new uint256[](1); @@ -134,7 +157,7 @@ contract PaymentCoordinatorTest is Test { function testClaimEarningsForRootTooEarly(bytes32 root, uint256 height, uint256 calculatedUpToBlockNumber) external { cheats.assume(root != bytes32(0)); testPostMerkleRoot(root, height, calculatedUpToBlockNumber); - (bytes32 rootPost, , , ) = paymentCoordinator.merkleRoots(paymentCoordinator.merkleRootsLength() - 1); + paymentCoordinator.merkleRoots(paymentCoordinator.merkleRootsLength() - 1); PaymentCoordinator.MerkleLeaf memory leaf; leaf.amounts = new uint256[](1); @@ -143,7 +166,7 @@ contract PaymentCoordinatorTest is Test { paymentCoordinator.proveAndClaimEarnings(new bytes(0), 0, leaf); } - function _sum(uint[] memory numbers) internal returns (uint) { + function _sum(uint[] memory numbers) internal view returns (uint) { uint total = 0; for (uint i = 0; i < numbers.length; i++) { From 8696db7b7ddee0ed7595ad4b0048942249019397 Mon Sep 17 00:00:00 2001 From: Siddhartha Jagannath Date: Wed, 21 Jun 2023 15:13:30 -0700 Subject: [PATCH 19/26] addressed last couple points --- src/contracts/core/PaymentCoordinator.sol | 15 +++++++++------ src/contracts/interfaces/IPaymentCoordinator.sol | 5 ++--- src/test/unit/PaymentCoordinatorUnit.t.sol | 12 ++++++------ 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/contracts/core/PaymentCoordinator.sol b/src/contracts/core/PaymentCoordinator.sol index 6bb325e64..a95104476 100644 --- a/src/contracts/core/PaymentCoordinator.sol +++ b/src/contracts/core/PaymentCoordinator.sol @@ -112,7 +112,7 @@ contract PaymentCoordinator is } /// @notice Permissioned function which allows rootPublisher to nullify a Merkle root - function nullifyMerkleRoot(uint256 rootIndex) external onlyRootPublisher { + function nullifyMerkleRoot(uint256 rootIndex) external onlyOwner { require(block.number <= _merkleRoots[rootIndex].confirmedAtBlockNumber, "PaymentCoordinator.nullifyMerkleRoot: Merkle root already confirmed"); delete _merkleRoots[rootIndex]; } @@ -133,7 +133,8 @@ contract PaymentCoordinator is function proveAndClaimEarnings( bytes memory proof, uint256 rootIndex, - MerkleLeaf memory leaf + MerkleLeaf memory leaf, + uint256 leafIndex ) external { require(leaf.amounts.length == leaf.tokens.length, "PaymentCoordinator.proveAndClaimEarnings: leaf amounts and tokens must be same length"); require(_merkleRoots[rootIndex].confirmedAtBlockNumber < block.number, "PaymentCoordinator.proveAndClaimEarnings: Merkle root not yet confirmed"); @@ -142,11 +143,13 @@ contract PaymentCoordinator is require(root != bytes32(0), "PaymentCoordinator.proveAndClaimEarnings: Merkle root is null"); bytes32 leafHash = _computeLeafHash(leaf); - require(Merkle.verifyInclusionKeccak(proof, root, leafHash, leaf.index), "PaymentCoordinator.proveAndClaimEarnings: Invalid proof"); + require(Merkle.verifyInclusionKeccak(proof, root, leafHash, leafIndex), "PaymentCoordinator.proveAndClaimEarnings: Invalid proof"); for(uint256 i = 0; i < leaf.amounts.length; i++) { - leaf.tokens[i].safeTransfer(leaf.recipient, leaf.amounts[i]); - cumulativeTokenAmountClaimedByRecipient[leaf.tokens[i]][leaf.recipient] += leaf.amounts[i]; + uint256 amount = leaf.amounts[i] - cumulativeTokenAmountClaimedByRecipient[leaf.tokens[i]][leaf.recipient]; + cumulativeTokenAmountClaimedByRecipient[leaf.tokens[i]][leaf.recipient] = leaf.amounts[i]; + leaf.tokens[i].safeTransfer(leaf.recipient, amount); + } emit PaymentClaimed(leaf); } @@ -193,6 +196,6 @@ contract PaymentCoordinator is } function _computeLeafHash(MerkleLeaf memory leaf) internal pure returns (bytes32) { - return keccak256(abi.encodePacked(leaf.recipient, keccak256(abi.encodePacked(leaf.tokens)), keccak256(abi.encodePacked(leaf.amounts)), leaf.index)); + return keccak256(abi.encodePacked(leaf.recipient, keccak256(abi.encodePacked(leaf.tokens)), keccak256(abi.encodePacked(leaf.amounts)))); } } \ No newline at end of file diff --git a/src/contracts/interfaces/IPaymentCoordinator.sol b/src/contracts/interfaces/IPaymentCoordinator.sol index a401b8038..7f7379438 100644 --- a/src/contracts/interfaces/IPaymentCoordinator.sol +++ b/src/contracts/interfaces/IPaymentCoordinator.sol @@ -40,8 +40,6 @@ interface IPaymentCoordinator { IERC20[] tokens; // cumulative all-time earnings in each token uint256[] amounts; - //index in the merkle tree - uint256 index; } /// @notice Getter function for the length of the `merkleRootPosts` array @@ -79,7 +77,8 @@ interface IPaymentCoordinator { function proveAndClaimEarnings( bytes memory proof, uint256 rootIndex, - MerkleLeaf memory leaf + MerkleLeaf memory leaf, + uint256 leafIndex ) external; } \ No newline at end of file diff --git a/src/test/unit/PaymentCoordinatorUnit.t.sol b/src/test/unit/PaymentCoordinatorUnit.t.sol index 4780ab5da..ed4191bbe 100644 --- a/src/test/unit/PaymentCoordinatorUnit.t.sol +++ b/src/test/unit/PaymentCoordinatorUnit.t.sol @@ -123,7 +123,7 @@ contract PaymentCoordinatorTest is Test { function testNullifyMerkleRoot(bytes32 root, uint256 height, uint256 calculatedUpToBlockNumber) external { testPostMerkleRoot(root, height, calculatedUpToBlockNumber); - cheats.startPrank(paymentCoordinator.rootPublisher()); + cheats.startPrank(paymentCoordinator.owner()); paymentCoordinator.nullifyMerkleRoot(paymentCoordinator.merkleRootsLength() - 1); cheats.stopPrank(); IPaymentCoordinator.MerkleRootPost memory post = paymentCoordinator.merkleRoots(paymentCoordinator.merkleRootsLength() - 1); @@ -135,13 +135,13 @@ contract PaymentCoordinatorTest is Test { cheats.roll(block.number + rootPostDelay + 1); cheats.expectRevert(bytes("PaymentCoordinator.proveAndClaimEarnings: Merkle root is null")); - paymentCoordinator.proveAndClaimEarnings(new bytes(0), 0, leaf); + paymentCoordinator.proveAndClaimEarnings(new bytes(0), 0, leaf, 0); } function testCallNullifyMerkleRootFromUnauthorizedAddress(address unauthorizedAddress) external fuzzedAddress(unauthorizedAddress) { - cheats.assume(unauthorizedAddress != paymentCoordinator.rootPublisher()); + cheats.assume(unauthorizedAddress != paymentCoordinator.owner()); cheats.startPrank(unauthorizedAddress); - cheats.expectRevert(bytes("PaymentCoordinator: Only rootPublisher")); + cheats.expectRevert(bytes("Ownable: caller is not the owner")); paymentCoordinator.nullifyMerkleRoot(0); cheats.stopPrank(); } @@ -151,7 +151,7 @@ contract PaymentCoordinatorTest is Test { leaf.amounts = new uint256[](1); leaf.tokens = new IERC20[](2); cheats.expectRevert(bytes("PaymentCoordinator.proveAndClaimEarnings: leaf amounts and tokens must be same length")); - paymentCoordinator.proveAndClaimEarnings(new bytes(0), 0, leaf); + paymentCoordinator.proveAndClaimEarnings(new bytes(0), 0, leaf, 0); } function testClaimEarningsForRootTooEarly(bytes32 root, uint256 height, uint256 calculatedUpToBlockNumber) external { @@ -163,7 +163,7 @@ contract PaymentCoordinatorTest is Test { leaf.amounts = new uint256[](1); leaf.tokens = new IERC20[](1); cheats.expectRevert(bytes("PaymentCoordinator.proveAndClaimEarnings: Merkle root not yet confirmed")); - paymentCoordinator.proveAndClaimEarnings(new bytes(0), 0, leaf); + paymentCoordinator.proveAndClaimEarnings(new bytes(0), 0, leaf, 0); } function _sum(uint[] memory numbers) internal view returns (uint) { From ed94f3e3d0d542496e832428c4029cdb54bd729e Mon Sep 17 00:00:00 2001 From: Siddhartha Jagannath Date: Wed, 21 Jun 2023 16:28:02 -0700 Subject: [PATCH 20/26] addressed final changes --- src/contracts/core/PaymentCoordinator.sol | 20 +++++++++---------- .../interfaces/IPaymentCoordinator.sol | 4 ++-- src/test/unit/PaymentCoordinatorUnit.t.sol | 18 ++++++++--------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/contracts/core/PaymentCoordinator.sol b/src/contracts/core/PaymentCoordinator.sol index a95104476..09e340445 100644 --- a/src/contracts/core/PaymentCoordinator.sol +++ b/src/contracts/core/PaymentCoordinator.sol @@ -37,7 +37,7 @@ contract PaymentCoordinator is uint256 public constant MAX_EIGENLAYER_SHARE_BIPS = 1500; /// @notice Array of roots of posted Merkle trees, as well as associated data like tree height - MerkleRootPost[] internal _merkleRoots; + MerkleRootPost[] internal _merkleRootPosts; /// @notice Mapping token => recipient => cumulative amount *claimed* mapping(IERC20 => mapping(address => uint256)) public cumulativeTokenAmountClaimedByRecipient; @@ -107,14 +107,14 @@ contract PaymentCoordinator is // @notice Permissioned function which allows posting a new Merkle root function postMerkleRoot(bytes32 newRoot, uint256 height, uint256 calculatedUpToBlockNumber) external onlyRootPublisher { MerkleRootPost memory newMerkleRoot = MerkleRootPost(newRoot, height, block.number + merkleRootActivationDelayBlocks, calculatedUpToBlockNumber); - _merkleRoots.push(newMerkleRoot); + _merkleRootPosts.push(newMerkleRoot); emit NewMerkleRootPosted(newMerkleRoot); } /// @notice Permissioned function which allows rootPublisher to nullify a Merkle root function nullifyMerkleRoot(uint256 rootIndex) external onlyOwner { - require(block.number <= _merkleRoots[rootIndex].confirmedAtBlockNumber, "PaymentCoordinator.nullifyMerkleRoot: Merkle root already confirmed"); - delete _merkleRoots[rootIndex]; + require(block.number <= _merkleRootPosts[rootIndex].confirmedAtBlockNumber, "PaymentCoordinator.nullifyMerkleRoot: Merkle root already confirmed"); + delete _merkleRootPosts[rootIndex]; } // @notice Permissioned function which allows withdrawal of EigenLayer's share of `token` from all received payments @@ -137,9 +137,9 @@ contract PaymentCoordinator is uint256 leafIndex ) external { require(leaf.amounts.length == leaf.tokens.length, "PaymentCoordinator.proveAndClaimEarnings: leaf amounts and tokens must be same length"); - require(_merkleRoots[rootIndex].confirmedAtBlockNumber < block.number, "PaymentCoordinator.proveAndClaimEarnings: Merkle root not yet confirmed"); + require(_merkleRootPosts[rootIndex].confirmedAtBlockNumber < block.number, "PaymentCoordinator.proveAndClaimEarnings: Merkle root not yet confirmed"); - bytes32 root = _merkleRoots[rootIndex].root; + bytes32 root = _merkleRootPosts[rootIndex].root; require(root != bytes32(0), "PaymentCoordinator.proveAndClaimEarnings: Merkle root is null"); bytes32 leafHash = _computeLeafHash(leaf); @@ -168,13 +168,13 @@ contract PaymentCoordinator is /// @notice Getter function for the length of the `merkleRoots` array - function merkleRootsLength() external view returns (uint256) { - return _merkleRoots.length; + function merkleRootPostsLength() external view returns (uint256) { + return _merkleRootPosts.length; } /// @notice Getter function for a merkleRoot at a given index - function merkleRoots(uint256 index) external view returns (MerkleRootPost memory) { - return _merkleRoots[index]; + function merkleRootPosts(uint256 index) external view returns (MerkleRootPost memory) { + return _merkleRootPosts[index]; } function _setRootPublisher(address _rootPublisher) internal { diff --git a/src/contracts/interfaces/IPaymentCoordinator.sol b/src/contracts/interfaces/IPaymentCoordinator.sol index 7f7379438..bc9d80316 100644 --- a/src/contracts/interfaces/IPaymentCoordinator.sol +++ b/src/contracts/interfaces/IPaymentCoordinator.sol @@ -43,14 +43,14 @@ interface IPaymentCoordinator { } /// @notice Getter function for the length of the `merkleRootPosts` array - function merkleRootsLength() external view returns (uint256); + function merkleRootPostsLength() external view returns (uint256); /// @notice getter cumulativeTokenAmountClaimedByRecipient (mapping(IERC20 => mapping(address => uint256)) function cumulativeTokenAmountClaimedByRecipient(IERC20 token, address recipient) external view returns (uint256); /// @notice getter for merkleRootPosts - function merkleRoots(uint256 index) external view returns (MerkleRootPost memory); + function merkleRootPosts(uint256 index) external view returns (MerkleRootPost memory); /** * @notice Makes a payment of sum(amounts) paid in `token`, for `operator`'s contributions to an AVS, diff --git a/src/test/unit/PaymentCoordinatorUnit.t.sol b/src/test/unit/PaymentCoordinatorUnit.t.sol index ed4191bbe..904130a96 100644 --- a/src/test/unit/PaymentCoordinatorUnit.t.sol +++ b/src/test/unit/PaymentCoordinatorUnit.t.sol @@ -74,7 +74,7 @@ contract PaymentCoordinatorTest is Test { uint256 balanceBefore = dummyToken.balanceOf(address(paymentCoordinator)); uint256 cumumlativeEarningsBefore = paymentCoordinator.cumulativeEigenLayerTokenEarnings(dummyToken); - cheats.expectEmit(address(paymentCoordinator)); + cheats.expectEmit(true, true, true, true, address(paymentCoordinator)); emit PaymentReceived(address(this), payment); paymentCoordinator.makePayment(payment); require(paymentCoordinator.cumulativeEigenLayerTokenEarnings(dummyToken) - cumumlativeEarningsBefore == sum * eigenLayerShareBIPs / MAX_BIPS, "cumulativeEigenLayerTokeEarnings should be set"); @@ -95,13 +95,13 @@ contract PaymentCoordinatorTest is Test { merkleRootPost.height = height; merkleRootPost.calculatedUpToBlockNumber = calculatedUpToBlockNumber; - uint256 numMerkleRootsBefore = paymentCoordinator.merkleRootsLength(); + uint256 numMerkleRootsBefore = paymentCoordinator.merkleRootPostsLength(); cheats.startPrank(paymentCoordinator.rootPublisher()); paymentCoordinator.postMerkleRoot(root, height, calculatedUpToBlockNumber); cheats.stopPrank(); - IPaymentCoordinator.MerkleRootPost memory post = paymentCoordinator.merkleRoots(numMerkleRootsBefore); + IPaymentCoordinator.MerkleRootPost memory post = paymentCoordinator.merkleRootPosts(numMerkleRootsBefore); require(post.root == root, "root should be set"); require(post.height == height, "height should be set"); require(post.confirmedAtBlockNumber == block.number + rootPostDelay, "confirmedAtBlockNumber should be set"); @@ -124,9 +124,9 @@ contract PaymentCoordinatorTest is Test { function testNullifyMerkleRoot(bytes32 root, uint256 height, uint256 calculatedUpToBlockNumber) external { testPostMerkleRoot(root, height, calculatedUpToBlockNumber); cheats.startPrank(paymentCoordinator.owner()); - paymentCoordinator.nullifyMerkleRoot(paymentCoordinator.merkleRootsLength() - 1); + paymentCoordinator.nullifyMerkleRoot(paymentCoordinator.merkleRootPostsLength() - 1); cheats.stopPrank(); - IPaymentCoordinator.MerkleRootPost memory post = paymentCoordinator.merkleRoots(paymentCoordinator.merkleRootsLength() - 1); + IPaymentCoordinator.MerkleRootPost memory post = paymentCoordinator.merkleRootPosts(paymentCoordinator.merkleRootPostsLength() - 1); require(post.root == bytes32(0), "root should be set"); PaymentCoordinator.MerkleLeaf memory leaf; @@ -157,7 +157,7 @@ contract PaymentCoordinatorTest is Test { function testClaimEarningsForRootTooEarly(bytes32 root, uint256 height, uint256 calculatedUpToBlockNumber) external { cheats.assume(root != bytes32(0)); testPostMerkleRoot(root, height, calculatedUpToBlockNumber); - paymentCoordinator.merkleRoots(paymentCoordinator.merkleRootsLength() - 1); + paymentCoordinator.merkleRootPosts(paymentCoordinator.merkleRootPostsLength() - 1); PaymentCoordinator.MerkleLeaf memory leaf; leaf.amounts = new uint256[](1); @@ -166,11 +166,11 @@ contract PaymentCoordinatorTest is Test { paymentCoordinator.proveAndClaimEarnings(new bytes(0), 0, leaf, 0); } - function _sum(uint[] memory numbers) internal view returns (uint) { - uint total = 0; + function _sum(uint256[] memory numbers) internal view returns (uint256) { + uint256 total = 0; for (uint i = 0; i < numbers.length; i++) { - cheats.assume(numbers[i] < type(uint32).max && numbers[i] > 0); + cheats.assume(numbers[i] < type(uint32).max); total += numbers[i]; } return total; From ea8a8e3a46e18f8a77393dfd52fcb3162b8d5f69 Mon Sep 17 00:00:00 2001 From: Siddhartha Jagannath Date: Wed, 21 Jun 2023 16:35:16 -0700 Subject: [PATCH 21/26] addressed final changes --- src/test/unit/PaymentCoordinatorUnit.t.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/unit/PaymentCoordinatorUnit.t.sol b/src/test/unit/PaymentCoordinatorUnit.t.sol index 904130a96..041e41c62 100644 --- a/src/test/unit/PaymentCoordinatorUnit.t.sol +++ b/src/test/unit/PaymentCoordinatorUnit.t.sol @@ -118,6 +118,9 @@ contract PaymentCoordinatorTest is Test { paymentCoordinator.withdrawEigenlayerShare(dummyToken, recipient); cheats.stopPrank(); require(paymentCoordinator.cumulativeEigenLayerTokenEarnings(dummyToken) == 0, "cumulativeEigenLayerTokeEarnings not updated correctly"); + emit log_named_uint("amountToClaim", amountToClaim); + emit log_named_uint("balanceBefore", balanceBefore); + emit log_named_uint("balanceAfter", dummyToken.balanceOf(recipient)); require(dummyToken.balanceOf(recipient) - balanceBefore == amountToClaim, "incorrect token balance"); } From e04d6ea7bd13f586ec28174aac396549532c5821 Mon Sep 17 00:00:00 2001 From: Siddhartha Jagannath Date: Wed, 21 Jun 2023 16:38:35 -0700 Subject: [PATCH 22/26] removed logs --- src/test/unit/PaymentCoordinatorUnit.t.sol | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/test/unit/PaymentCoordinatorUnit.t.sol b/src/test/unit/PaymentCoordinatorUnit.t.sol index 041e41c62..904130a96 100644 --- a/src/test/unit/PaymentCoordinatorUnit.t.sol +++ b/src/test/unit/PaymentCoordinatorUnit.t.sol @@ -118,9 +118,6 @@ contract PaymentCoordinatorTest is Test { paymentCoordinator.withdrawEigenlayerShare(dummyToken, recipient); cheats.stopPrank(); require(paymentCoordinator.cumulativeEigenLayerTokenEarnings(dummyToken) == 0, "cumulativeEigenLayerTokeEarnings not updated correctly"); - emit log_named_uint("amountToClaim", amountToClaim); - emit log_named_uint("balanceBefore", balanceBefore); - emit log_named_uint("balanceAfter", dummyToken.balanceOf(recipient)); require(dummyToken.balanceOf(recipient) - balanceBefore == amountToClaim, "incorrect token balance"); } From 7278609687c73cbea4af982265fc83aee253c89e Mon Sep 17 00:00:00 2001 From: Siddhartha Jagannath Date: Wed, 21 Jun 2023 16:50:57 -0700 Subject: [PATCH 23/26] fixed breaking test --- src/contracts/core/PaymentCoordinator.sol | 15 ++++++++------- src/test/unit/PaymentCoordinatorUnit.t.sol | 21 +++++++++++++++++---- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/contracts/core/PaymentCoordinator.sol b/src/contracts/core/PaymentCoordinator.sol index 09e340445..e79a0202c 100644 --- a/src/contracts/core/PaymentCoordinator.sol +++ b/src/contracts/core/PaymentCoordinator.sol @@ -42,8 +42,8 @@ contract PaymentCoordinator is /// @notice Mapping token => recipient => cumulative amount *claimed* mapping(IERC20 => mapping(address => uint256)) public cumulativeTokenAmountClaimedByRecipient; - /// @notice Mapping token => cumulative amount earned by EigenLayer - mapping(IERC20 => uint256) public cumulativeEigenLayerTokenEarnings; + /// @notice Mapping token => accumulated amount earned by EigenLayer + mapping(IERC20 => uint256) public accumulatedEigenLayerTokenEarnings; /// @notice variable that defines the share EigenLayer takes of all payments, in basis points uint256 public eigenLayerShareBIPs; @@ -94,13 +94,14 @@ contract PaymentCoordinator is * @notice Emits a `PaymentReceived` event */ function makePayment(Payment calldata payment) external{ + require(payment.amounts.length == payment.quorums.length, "PaymentCoordinator.makePayment: payment amounts and quorums must have the same length"); uint256 sumAmounts; for (uint256 i = 0; i < payment.amounts.length; i++) { sumAmounts += payment.amounts[i]; } payment.token.safeTransferFrom(msg.sender, address(this), sumAmounts); - cumulativeEigenLayerTokenEarnings[payment.token] += sumAmounts * eigenLayerShareBIPs / MAX_BIPS; + accumulatedEigenLayerTokenEarnings[payment.token] += sumAmounts * eigenLayerShareBIPs / MAX_BIPS; emit PaymentReceived(msg.sender, payment); } @@ -119,8 +120,8 @@ contract PaymentCoordinator is // @notice Permissioned function which allows withdrawal of EigenLayer's share of `token` from all received payments function withdrawEigenlayerShare(IERC20 token, address recipient) external onlyOwner { - uint256 amount = cumulativeEigenLayerTokenEarnings[token]; - cumulativeEigenLayerTokenEarnings[token] = 0; + uint256 amount = accumulatedEigenLayerTokenEarnings[token]; + accumulatedEigenLayerTokenEarnings[token] = 0; token.safeTransfer(recipient, amount); } @@ -142,7 +143,7 @@ contract PaymentCoordinator is bytes32 root = _merkleRootPosts[rootIndex].root; require(root != bytes32(0), "PaymentCoordinator.proveAndClaimEarnings: Merkle root is null"); - bytes32 leafHash = _computeLeafHash(leaf); + bytes32 leafHash = computeLeafHash(leaf); require(Merkle.verifyInclusionKeccak(proof, root, leafHash, leafIndex), "PaymentCoordinator.proveAndClaimEarnings: Invalid proof"); for(uint256 i = 0; i < leaf.amounts.length; i++) { @@ -195,7 +196,7 @@ contract PaymentCoordinator is emit EigenLayerShareBIPSChanged(eigenLayerShareBIPs); } - function _computeLeafHash(MerkleLeaf memory leaf) internal pure returns (bytes32) { + function computeLeafHash(MerkleLeaf memory leaf) public pure returns (bytes32) { return keccak256(abi.encodePacked(leaf.recipient, keccak256(abi.encodePacked(leaf.tokens)), keccak256(abi.encodePacked(leaf.amounts)))); } } \ No newline at end of file diff --git a/src/test/unit/PaymentCoordinatorUnit.t.sol b/src/test/unit/PaymentCoordinatorUnit.t.sol index 904130a96..cd553d5c0 100644 --- a/src/test/unit/PaymentCoordinatorUnit.t.sol +++ b/src/test/unit/PaymentCoordinatorUnit.t.sol @@ -71,13 +71,15 @@ contract PaymentCoordinatorTest is Test { PaymentCoordinator.Payment memory payment; payment.token = dummyToken; payment.amounts = amounts; + //this is purely to ensure that the quorum and amount length check passes. We never use this value on chain + payment.quorums = amounts; uint256 balanceBefore = dummyToken.balanceOf(address(paymentCoordinator)); - uint256 cumumlativeEarningsBefore = paymentCoordinator.cumulativeEigenLayerTokenEarnings(dummyToken); + uint256 cumumlativeEarningsBefore = paymentCoordinator.accumulatedEigenLayerTokenEarnings(dummyToken); cheats.expectEmit(true, true, true, true, address(paymentCoordinator)); emit PaymentReceived(address(this), payment); paymentCoordinator.makePayment(payment); - require(paymentCoordinator.cumulativeEigenLayerTokenEarnings(dummyToken) - cumumlativeEarningsBefore == sum * eigenLayerShareBIPs / MAX_BIPS, "cumulativeEigenLayerTokeEarnings should be set"); + require(paymentCoordinator.accumulatedEigenLayerTokenEarnings(dummyToken) - cumumlativeEarningsBefore == sum * eigenLayerShareBIPs / MAX_BIPS, "cumulativeEigenLayerTokeEarnings should be set"); require(dummyToken.balanceOf(address(paymentCoordinator)) - balanceBefore == sum, "incorrect token balance"); } @@ -113,14 +115,25 @@ contract PaymentCoordinatorTest is Test { cheats.assume(recipient != address(paymentCoordinator)); uint256 balanceBefore = dummyToken.balanceOf(recipient); testMakePayment(amounts); - uint256 amountToClaim = paymentCoordinator.cumulativeEigenLayerTokenEarnings(dummyToken); + uint256 amountToClaim = paymentCoordinator.accumulatedEigenLayerTokenEarnings(dummyToken); cheats.startPrank(paymentCoordinator.owner()); paymentCoordinator.withdrawEigenlayerShare(dummyToken, recipient); cheats.stopPrank(); - require(paymentCoordinator.cumulativeEigenLayerTokenEarnings(dummyToken) == 0, "cumulativeEigenLayerTokeEarnings not updated correctly"); + require(paymentCoordinator.accumulatedEigenLayerTokenEarnings(dummyToken) == 0, "cumulativeEigenLayerTokeEarnings not updated correctly"); require(dummyToken.balanceOf(recipient) - balanceBefore == amountToClaim, "incorrect token balance"); } + function testMismatchedAmountAndQuorumArrayLength(uint256[] memory amounts, uint256[] memory quorums) external { + cheats.assume(amounts.length != quorums.length); + PaymentCoordinator.Payment memory payment; + payment.token = dummyToken; + payment.amounts = amounts; + payment.quorums = quorums; + cheats.expectRevert(bytes("PaymentCoordinator.makePayment: payment amounts and quorums must have the same length")); + paymentCoordinator.makePayment(payment); + + } + function testNullifyMerkleRoot(bytes32 root, uint256 height, uint256 calculatedUpToBlockNumber) external { testPostMerkleRoot(root, height, calculatedUpToBlockNumber); cheats.startPrank(paymentCoordinator.owner()); From 08505bfeece83a25fa15165447e7f0be40dcf786 Mon Sep 17 00:00:00 2001 From: Siddhartha Jagannath Date: Thu, 22 Jun 2023 09:05:50 -0700 Subject: [PATCH 24/26] removed Test import --- src/contracts/core/PaymentCoordinator.sol | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/contracts/core/PaymentCoordinator.sol b/src/contracts/core/PaymentCoordinator.sol index e79a0202c..4539c4880 100644 --- a/src/contracts/core/PaymentCoordinator.sol +++ b/src/contracts/core/PaymentCoordinator.sol @@ -8,9 +8,6 @@ import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "forge-std/Test.sol"; - - /** * @title Contract used to coordinate payments from AVSs to operators and in particular the subsequent splitting of earnings from operators to stakers @@ -19,8 +16,7 @@ import "forge-std/Test.sol"; contract PaymentCoordinator is IPaymentCoordinator, Initializable, - OwnableUpgradeable, - Test + OwnableUpgradeable { using SafeERC20 for IERC20; From 132e964e2f887009b3d4391e46db0fddb44a2e64 Mon Sep 17 00:00:00 2001 From: kachapah <60323455+Sidu28@users.noreply.github.com> Date: Mon, 26 Jun 2023 11:18:18 -0700 Subject: [PATCH 25/26] Update certora-prover.yml --- .github/workflows/certora-prover.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/certora-prover.yml b/.github/workflows/certora-prover.yml index 46164fa0e..154d00950 100644 --- a/.github/workflows/certora-prover.yml +++ b/.github/workflows/certora-prover.yml @@ -43,7 +43,7 @@ jobs: java-version: '11' java-package: 'jre' - name: Install certora - run: pip install certora-cli==3.6.4 + run: pip install certora-cli==3.6.8.post3 - name: Install solc run: | wget https://github.com/ethereum/solidity/releases/download/v0.8.12/solc-static-linux From e5c41ab2a6518618b0bcb64666079c44b19e2c67 Mon Sep 17 00:00:00 2001 From: Siddhartha Jagannath Date: Mon, 26 Jun 2023 13:06:43 -0700 Subject: [PATCH 26/26] added comments --- src/contracts/core/PaymentCoordinator.sol | 23 ++++++++----------- .../interfaces/IPaymentCoordinator.sol | 4 ++-- src/test/unit/PaymentCoordinatorUnit.t.sol | 11 ++++----- 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/src/contracts/core/PaymentCoordinator.sol b/src/contracts/core/PaymentCoordinator.sol index 4539c4880..4cecaafea 100644 --- a/src/contracts/core/PaymentCoordinator.sol +++ b/src/contracts/core/PaymentCoordinator.sol @@ -24,7 +24,7 @@ contract PaymentCoordinator is address public rootPublisher; /// @notice delay in blocks before a Merkle root can be activated - uint256 public merkleRootActivationDelayBlocks; + uint256 public constant MERKLE_ROOT_ACTIVATION_DELAY_BLOCKS = 7200; /// @notice maximum BIPS uint256 public constant MAX_BIPS = 10000; @@ -71,13 +71,12 @@ contract PaymentCoordinator is _disableInitializers(); } - function initialize(address _initialOwner, address _rootPublisher, uint256 _eigenlayerShareBips, uint256 _merkleRootActivationDelayBlocks) + function initialize(address _initialOwner, address _rootPublisher, uint256 _eigenlayerShareBips) external initializer { _transferOwnership(_initialOwner); _setRootPublisher(_rootPublisher); - _setMerkleRootActivationDelayBlocks(_merkleRootActivationDelayBlocks); _setEigenLayerShareBIPS(_eigenlayerShareBips); } @@ -103,7 +102,12 @@ contract PaymentCoordinator is // @notice Permissioned function which allows posting a new Merkle root function postMerkleRoot(bytes32 newRoot, uint256 height, uint256 calculatedUpToBlockNumber) external onlyRootPublisher { - MerkleRootPost memory newMerkleRoot = MerkleRootPost(newRoot, height, block.number + merkleRootActivationDelayBlocks, calculatedUpToBlockNumber); + MerkleRootPost memory newMerkleRoot = MerkleRootPost({ + root: newRoot, + height: height, + confirmedAtBlockNumber: block.number + MERKLE_ROOT_ACTIVATION_DELAY_BLOCKS, + calculatedUpToBlockNumber: calculatedUpToBlockNumber + }); _merkleRootPosts.push(newMerkleRoot); emit NewMerkleRootPosted(newMerkleRoot); } @@ -115,7 +119,7 @@ contract PaymentCoordinator is } // @notice Permissioned function which allows withdrawal of EigenLayer's share of `token` from all received payments - function withdrawEigenlayerShare(IERC20 token, address recipient) external onlyOwner { + function withdrawEigenLayerShare(IERC20 token, address recipient) external onlyOwner { uint256 amount = accumulatedEigenLayerTokenEarnings[token]; accumulatedEigenLayerTokenEarnings[token] = 0; token.safeTransfer(recipient, amount); @@ -155,10 +159,6 @@ contract PaymentCoordinator is _setRootPublisher(_rootPublisher); } - function setMerkleRootActivationDelayBlocks(uint256 _merkleRootActivationDelayBlocks) external onlyOwner { - _setMerkleRootActivationDelayBlocks(_merkleRootActivationDelayBlocks); - } - function setEigenLayerShareBIPS(uint256 _eigenlayerShareBips) external onlyOwner { _setEigenLayerShareBIPS(_eigenlayerShareBips); } @@ -180,11 +180,6 @@ contract PaymentCoordinator is emit RootPublisherChanged(currentRootPublisher, rootPublisher); } - function _setMerkleRootActivationDelayBlocks(uint256 _merkleRootActivationDelayBlocks) internal { - merkleRootActivationDelayBlocks = _merkleRootActivationDelayBlocks; - emit MerkleRootActivationDelayBlocksChanged(merkleRootActivationDelayBlocks); - } - function _setEigenLayerShareBIPS(uint256 _eigenlayerShareBips) internal { require(_eigenlayerShareBips <= MAX_EIGENLAYER_SHARE_BIPS, "PaymentCoordinator: EigenLayer share cannot be greater than 100%"); eigenLayerShareBIPs = _eigenlayerShareBips; diff --git a/src/contracts/interfaces/IPaymentCoordinator.sol b/src/contracts/interfaces/IPaymentCoordinator.sol index bc9d80316..401fc9aec 100644 --- a/src/contracts/interfaces/IPaymentCoordinator.sol +++ b/src/contracts/interfaces/IPaymentCoordinator.sol @@ -65,14 +65,14 @@ interface IPaymentCoordinator { function postMerkleRoot(bytes32 newRoot, uint256 height, uint256 calculatedUpToBlockNumber) external; // @notice Permissioned function which allows withdrawal of EigenLayer's share of `token` from all received payments - function withdrawEigenlayerShare(IERC20 token, address recipient) external; + function withdrawEigenLayerShare(IERC20 token, address recipient) external; /** * @notice Called by a staker or operator to prove the inclusion of their earnings in a posted Merkle root and claim them. * @param proof Merkle proof showing that a leaf containing `(msg.sender, amount)` was included in the `rootIndex`-th * Merkle root posted for the `token` * @param rootIndex Specifies the Merkle root to look up, using `merkleRootsByToken[token][rootIndex]` - * @param leaf The leaf to be inserted into the Merkle tree + * @param leaf The leaf to be proven for the Merkle tree */ function proveAndClaimEarnings( bytes memory proof, diff --git a/src/test/unit/PaymentCoordinatorUnit.t.sol b/src/test/unit/PaymentCoordinatorUnit.t.sol index cd553d5c0..74a759985 100644 --- a/src/test/unit/PaymentCoordinatorUnit.t.sol +++ b/src/test/unit/PaymentCoordinatorUnit.t.sol @@ -20,7 +20,6 @@ contract PaymentCoordinatorTest is Test { ERC20Mock public dummyToken; uint256 MAX_BIPS = 10000; uint256 eigenLayerShareBIPs = 1000; - uint256 rootPostDelay = 7200; address rootPublisher = address(25); address initialOwner = address(26); @@ -47,8 +46,7 @@ contract PaymentCoordinatorTest is Test { paymentCoordinatorImplementation.initialize.selector, initialOwner, rootPublisher, - eigenLayerShareBIPs, - rootPostDelay + eigenLayerShareBIPs ) ))); @@ -60,7 +58,6 @@ contract PaymentCoordinatorTest is Test { function testInitialize() public view { require(paymentCoordinator.rootPublisher() == rootPublisher, "rootPublisher should be set"); - require(paymentCoordinator.merkleRootActivationDelayBlocks() == 7200, "merkleRootActivationDelay should be set"); require(paymentCoordinator.eigenLayerShareBIPs() == 1000, "eigenLayerShareBIPs should be set"); require(paymentCoordinator.owner() == initialOwner, "owner should be set"); } @@ -106,7 +103,7 @@ contract PaymentCoordinatorTest is Test { IPaymentCoordinator.MerkleRootPost memory post = paymentCoordinator.merkleRootPosts(numMerkleRootsBefore); require(post.root == root, "root should be set"); require(post.height == height, "height should be set"); - require(post.confirmedAtBlockNumber == block.number + rootPostDelay, "confirmedAtBlockNumber should be set"); + require(post.confirmedAtBlockNumber == block.number + paymentCoordinator.MERKLE_ROOT_ACTIVATION_DELAY_BLOCKS(), "confirmedAtBlockNumber should be set"); require(post.calculatedUpToBlockNumber == calculatedUpToBlockNumber, "calculatedUpToBlockNumber should be set"); } @@ -117,7 +114,7 @@ contract PaymentCoordinatorTest is Test { testMakePayment(amounts); uint256 amountToClaim = paymentCoordinator.accumulatedEigenLayerTokenEarnings(dummyToken); cheats.startPrank(paymentCoordinator.owner()); - paymentCoordinator.withdrawEigenlayerShare(dummyToken, recipient); + paymentCoordinator.withdrawEigenLayerShare(dummyToken, recipient); cheats.stopPrank(); require(paymentCoordinator.accumulatedEigenLayerTokenEarnings(dummyToken) == 0, "cumulativeEigenLayerTokeEarnings not updated correctly"); require(dummyToken.balanceOf(recipient) - balanceBefore == amountToClaim, "incorrect token balance"); @@ -146,7 +143,7 @@ contract PaymentCoordinatorTest is Test { leaf.amounts = new uint256[](1); leaf.tokens = new IERC20[](1); - cheats.roll(block.number + rootPostDelay + 1); + cheats.roll(block.number + paymentCoordinator.MERKLE_ROOT_ACTIVATION_DELAY_BLOCKS() + 1); cheats.expectRevert(bytes("PaymentCoordinator.proveAndClaimEarnings: Merkle root is null")); paymentCoordinator.proveAndClaimEarnings(new bytes(0), 0, leaf, 0); }