Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Payments architecture #56

Closed
wants to merge 29 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
48b6852
add draft `IPaymentCoordinator` interface
ChaoticWalrus Jun 3, 2023
12be3ed
add the `IDelegationDetails` interface
ChaoticWalrus Jun 3, 2023
8436128
add new functions and events to `IServiceManager` interface
ChaoticWalrus Jun 3, 2023
e9ed4c9
add the `weightOfStaker` function to the `IVoteWeigher` interface
ChaoticWalrus Jun 3, 2023
538dc80
add `serviceManager` function to the `IPaymentManager` interface
ChaoticWalrus Jun 3, 2023
5051fbd
remove `token` input to `postMerkleRoot` function
ChaoticWalrus Jun 5, 2023
b304dd6
init commit
Jun 6, 2023
c7833b1
implemented basic payment coordinator
Jun 6, 2023
3ececc4
implemented basic payment coordinator
Jun 6, 2023
11a0fec
added initializer, owner
Jun 6, 2023
404b0ae
added initializer, owner
Jun 6, 2023
2f61199
test setup
Jun 7, 2023
50f546d
test setup
Jun 7, 2023
f27ccd1
commit beforecheckout
Jun 7, 2023
244067b
got test working
Jun 7, 2023
844dbed
added several more tests
Jun 7, 2023
969d5f5
fixed broken testS
Jun 7, 2023
cee81d2
addressed concerns
Jun 21, 2023
8696db7
addressed last couple points
Jun 21, 2023
ed94f3e
addressed final changes
Jun 21, 2023
ea8a8e3
addressed final changes
Jun 21, 2023
e04d6ea
removed logs
Jun 21, 2023
7278609
fixed breaking test
Jun 21, 2023
08505bf
removed Test import
Jun 22, 2023
132e964
Update certora-prover.yml
Sidu28 Jun 26, 2023
2de4848
Merge pull request #58 from Layr-Labs/payment-coord
Sidu28 Jun 26, 2023
e5c41ab
added comments
Jun 26, 2023
b977a59
Merge pull request #79 from Layr-Labs/paymentcoord1
Sidu28 Jun 26, 2023
bac3394
Merge branch 'master' of https://github.com/Layr-Labs/eigenlayer-cont…
ChaoticWalrus Sep 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 193 additions & 0 deletions src/contracts/core/PaymentCoordinator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// SPDX-License-Identifier: BUSL-1.1
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";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";


/**
* @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
IPaymentCoordinator,
Initializable,
OwnableUpgradeable
{
using SafeERC20 for IERC20;

/// @notice address approved to post new Merkle roots
address public rootPublisher;

/// @notice delay in blocks before a Merkle root can be activated
uint256 public constant MERKLE_ROOT_ACTIVATION_DELAY_BLOCKS = 7200;

/// @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[] internal _merkleRootPosts;

/// @notice Mapping token => recipient => cumulative amount *claimed*
mapping(IERC20 => mapping(address => uint256)) public cumulativeTokenAmountClaimedByRecipient;

/// @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;

// 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);

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");
_;
}

constructor() {
_disableInitializers();
}

function initialize(address _initialOwner, address _rootPublisher, uint256 _eigenlayerShareBips)
external
initializer
{
_transferOwnership(_initialOwner);
_setRootPublisher(_rootPublisher);
_setEigenLayerShareBIPS(_eigenlayerShareBips);
}


/**
* @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{
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);

accumulatedEigenLayerTokenEarnings[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({
root: newRoot,
height: height,
confirmedAtBlockNumber: block.number + MERKLE_ROOT_ACTIVATION_DELAY_BLOCKS,
calculatedUpToBlockNumber: calculatedUpToBlockNumber
});
_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 <= _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
function withdrawEigenLayerShare(IERC20 token, address recipient) external onlyOwner {
uint256 amount = accumulatedEigenLayerTokenEarnings[token];
accumulatedEigenLayerTokenEarnings[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 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`.
*/
function proveAndClaimEarnings(
bytes memory proof,
uint256 rootIndex,
MerkleLeaf memory leaf,
uint256 leafIndex
) 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 root = _merkleRootPosts[rootIndex].root;
require(root != bytes32(0), "PaymentCoordinator.proveAndClaimEarnings: Merkle root is null");

bytes32 leafHash = computeLeafHash(leaf);
require(Merkle.verifyInclusionKeccak(proof, root, leafHash, leafIndex), "PaymentCoordinator.proveAndClaimEarnings: Invalid proof");

for(uint256 i = 0; i < leaf.amounts.length; 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);
}

function setRootPublisher(address _rootPublisher) external onlyOwner {
_setRootPublisher(_rootPublisher);
}

function setEigenLayerShareBIPS(uint256 _eigenlayerShareBips) external onlyOwner {
_setEigenLayerShareBIPS(_eigenlayerShareBips);
}


/// @notice Getter function for the length of the `merkleRoots` array
function merkleRootPostsLength() external view returns (uint256) {
return _merkleRootPosts.length;
}

/// @notice Getter function for a merkleRoot at a given index
function merkleRootPosts(uint256 index) external view returns (MerkleRootPost memory) {
return _merkleRootPosts[index];
}

function _setRootPublisher(address _rootPublisher) internal {
address currentRootPublisher = rootPublisher;
rootPublisher = _rootPublisher;
emit RootPublisherChanged(currentRootPublisher, rootPublisher);
}

function _setEigenLayerShareBIPS(uint256 _eigenlayerShareBips) internal {
require(_eigenlayerShareBips <= MAX_EIGENLAYER_SHARE_BIPS, "PaymentCoordinator: EigenLayer share cannot be greater than 100%");
eigenLayerShareBIPs = _eigenlayerShareBips;

emit EigenLayerShareBIPSChanged(eigenLayerShareBIPs);
}

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))));
}
}
37 changes: 37 additions & 0 deletions src/contracts/interfaces/IDelegationDetails.sol
Original file line number Diff line number Diff line change
@@ -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;
}
84 changes: 84 additions & 0 deletions src/contracts/interfaces/IPaymentCoordinator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// 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 in particular the subsequency splitting of 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;
}

/// @notice Getter function for the length of the `merkleRootPosts` array
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 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,
* 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(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 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 proven for the Merkle tree
*/
function proveAndClaimEarnings(
bytes memory proof,
uint256 rootIndex,
MerkleLeaf memory leaf,
uint256 leafIndex
) external;

}
7 changes: 7 additions & 0 deletions src/contracts/interfaces/IPaymentManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity >=0.5.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./IServiceManager.sol";

/**
* @title Interface for a `PaymentManager` contract.
Expand Down Expand Up @@ -87,6 +88,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
Expand Down
16 changes: 14 additions & 2 deletions src/contracts/interfaces/IServiceManager.sol
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.5.0;

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.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
*/
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);

Expand All @@ -29,4 +35,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);
}
7 changes: 7 additions & 0 deletions src/contracts/interfaces/IVoteWeigher.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,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);

Expand Down
Loading
Loading