diff --git a/contracts/.gitignore b/contracts/.gitignore index 8161a6cd..dac978e7 100644 --- a/contracts/.gitignore +++ b/contracts/.gitignore @@ -10,3 +10,9 @@ docs/ # Dotenv file .env + + +# DS_store files +.DS_Store +lib/.DS_Store + diff --git a/contracts/mocks/MockStrategy.sol b/contracts/mocks/MockStrategy.sol new file mode 100644 index 00000000..eee4462b --- /dev/null +++ b/contracts/mocks/MockStrategy.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@eigenlayer/contracts/interfaces/IStrategy.sol"; + +contract MockStrategy is IStrategy { + IERC20 public override underlyingToken; + uint256 public override totalShares; + mapping(address => uint256) public userShares; + uint256 public constant EXCHANGE_RATE = 1e18; // 1:1 exchange rate for simplicity + + constructor(IERC20 _underlyingToken) { + underlyingToken = _underlyingToken; + emit StrategyTokenSet(_underlyingToken, 18); // Assuming 18 decimals for simplicity + } + + function deposit(IERC20 token, uint256 amount) external override returns (uint256) { + require(token == underlyingToken, "Invalid token"); + uint256 newShares = amount; + totalShares += newShares; + userShares[msg.sender] += newShares; + emit ExchangeRateEmitted(EXCHANGE_RATE); + return newShares; + } + + function withdraw(address recipient, IERC20 token, uint256 amountShares) external override { + require(token == underlyingToken, "Invalid token"); + require(userShares[msg.sender] >= amountShares, "Insufficient shares"); + userShares[msg.sender] -= amountShares; + totalShares -= amountShares; + underlyingToken.transfer(recipient, amountShares); + } + + function sharesToUnderlying(uint256 amountShares) external pure override returns (uint256) { + return amountShares; + } + + function underlyingToShares(uint256 amountUnderlying) external pure override returns (uint256) { + return amountUnderlying; + } + + function userUnderlying(address user) external view override returns (uint256) { + return userShares[user]; + } + + function shares(address user) external view override returns (uint256) { + return userShares[user]; + } + + function sharesToUnderlyingView(uint256 amountShares) external pure override returns (uint256) { + return amountShares; + } + + function underlyingToSharesView(uint256 amountUnderlying) external pure override returns (uint256) { + return amountUnderlying; + } + + function userUnderlyingView(address user) external view override returns (uint256) { + return userShares[user]; + } + + function explanation() external pure override returns (string memory) { + return "Mock Strategy for testing purposes"; + } +} \ No newline at end of file diff --git a/contracts/payments.json b/contracts/payments.json new file mode 100644 index 00000000..77873afa --- /dev/null +++ b/contracts/payments.json @@ -0,0 +1,11 @@ +{ + "leaves": [ + "0x29036a1d92861ffd464a1e285030fad3978a36f953ae33c160e606d2ac746c42", + "0x29036a1d92861ffd464a1e285030fad3978a36f953ae33c160e606d2ac746c42", + "0x29036a1d92861ffd464a1e285030fad3978a36f953ae33c160e606d2ac746c42", + "0x29036a1d92861ffd464a1e285030fad3978a36f953ae33c160e606d2ac746c42" + ], + "tokenLeaves": [ + "0xf5d87050cb923194fe63c7ed2c45cbc913fa6ecf322f3631149c36d9460b3ad6" + ] +} \ No newline at end of file diff --git a/contracts/script/HelloWorldDeployer.s.sol b/contracts/script/HelloWorldDeployer.s.sol index 394adcf8..9d189e7b 100644 --- a/contracts/script/HelloWorldDeployer.s.sol +++ b/contracts/script/HelloWorldDeployer.s.sol @@ -6,6 +6,13 @@ import {console2} from "forge-std/Test.sol"; import {HelloWorldDeploymentLib} from "./utils/HelloWorldDeploymentLib.sol"; import {CoreDeploymentLib} from "./utils/CoreDeploymentLib.sol"; import {UpgradeableProxyLib} from "./utils/UpgradeableProxyLib.sol"; +import {StrategyBase} from "@eigenlayer/contracts/strategies/StrategyBase.sol"; +import {ERC20Mock} from "../test/ERC20Mock.sol"; +import {TransparentUpgradeableProxy} from + "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {StrategyFactory} from "@eigenlayer/contracts/strategies/StrategyFactory.sol"; +import {StrategyManager} from "@eigenlayer/contracts/core/StrategyManager.sol"; + import { Quorum, @@ -19,18 +26,23 @@ contract HelloWorldDeployer is Script { address private deployer; address proxyAdmin; + StrategyBase helloWorldStrategy; + StrategyBase helloWorldStrategyImpl; CoreDeploymentLib.DeploymentData coreDeployment; HelloWorldDeploymentLib.DeploymentData helloWorldDeployment; Quorum internal quorum; - + ERC20Mock token; function setUp() public virtual { deployer = vm.rememberKey(vm.envUint("PRIVATE_KEY")); vm.label(deployer, "Deployer"); coreDeployment = CoreDeploymentLib.readDeploymentJson("deployments/core/", block.chainid); + + token = new ERC20Mock(); + IStrategy helloWorldStrategy = StrategyFactory(coreDeployment.strategyFactory).deployNewStrategy(token); quorum.strategies.push( - StrategyParams({strategy: IStrategy(address(420)), multiplier: 10_000}) + StrategyParams({strategy: helloWorldStrategy, multiplier: 10_000}) ); } @@ -41,6 +53,8 @@ contract HelloWorldDeployer is Script { helloWorldDeployment = HelloWorldDeploymentLib.deployContracts(proxyAdmin, coreDeployment, quorum); + helloWorldDeployment.strategy = address(helloWorldStrategy); + helloWorldDeployment.token = address(token); vm.stopBroadcast(); verifyDeployment(); @@ -55,6 +69,7 @@ contract HelloWorldDeployer is Script { helloWorldDeployment.helloWorldServiceManager != address(0), "HelloWorldServiceManager address cannot be zero" ); + require(helloWorldDeployment.strategy != address(0), "Strategy address cannot be zero"); require(proxyAdmin != address(0), "ProxyAdmin address cannot be zero"); require( coreDeployment.delegationManager != address(0), diff --git a/contracts/script/SetupPayments.s.sol b/contracts/script/SetupPayments.s.sol new file mode 100644 index 00000000..c91bb50a --- /dev/null +++ b/contracts/script/SetupPayments.s.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {Script} from "forge-std/Script.sol"; +import {HelloWorldDeploymentLib} from "./utils/HelloWorldDeploymentLib.sol"; +import {CoreDeploymentLib} from "./utils/CoreDeploymentLib.sol"; +import {SetupPaymentsLib} from "./utils/SetupPaymentsLib.sol"; +import {IRewardsCoordinator} from "@eigenlayer/contracts/interfaces/IRewardsCoordinator.sol"; + +contract SetupPayments is Script { + struct PaymentInfo { + address[] earners; + bytes32[] earnerTokenRoots, + address recipient; + uint256 numPayments; + uint256 amountPerPayment; + uint32 duration; + uint32 startTimestamp; + uint32 endTimestamp; + uint256 indexToProve; + } + + address private deployer; + CoreDeploymentLib.DeploymentData coreDeployment; + HelloWorldDeploymentLib.DeploymentData helloWorldDeployment; + + uint256 constant NUM_TOKEN_EARNINGS = 1; + uint256 constant DURATION = 1 weeks; + + function setUp() public { + deployer = vm.rememberKey(vm.envUint("PRIVATE_KEY")); + vm.label(deployer, "Deployer"); + + coreDeployment = CoreDeploymentLib.readDeploymentJson("deployments/core/", block.chainid); + helloWorldDeployment = HelloWorldDeploymentLib.readDeploymentJson("deployments/hello-world/", block.chainid); + } + + function run() external { + vm.startBroadcast(deployer); + IRewardsCoordinator(coreDeployment.rewardsCoordinator).setRewardsUpdater(deployer); + PaymentInfo memory info = abi.decode(vm.parseJson(vm.readFile(filePath)), (PaymentInfo)); + + createAVSRewardsSubmissions(info.numPayments, info.amountPerPayment, info.duration, info.startTimestamp); + submitPaymentRoot(info.earners, info.endTimestamp, info.numPayments, info.amountPerPayment); + + IRewardsCoordinator.EarnerTreeMerkleLeaf memory earnerLeaf = IRewardsCoordinator.EarnerTreeMerkleLeaf({ + earner: info.earners[info.indexToProve], + tokenRoot: info.earnerTokenRoots[info.indexToProve], + }); + processClaim(SetupPaymentsLib.getFilePath(), info.indexToProve, info.recipient, earnerLeaf); + + + + + + + + + vm.stopBroadcast(); + } + + + function createAVSRewardsSubmissions(uint256 numPayments, uint256 amountPerPayment, uint32 duration, uint32 startTimestamp) public { + SetupPaymentsLib.createAVSRewardsSubmissions( + IRewardsCoordinator(coreDeployment.rewardsCoordinator), + helloWorldDeployment.strategy, + numPayments, + amountPerPayment, + duration, + startTimestamp + ); + } + + function processClaim(string memory filePath, uint256 indexToProve, address recipient, IRewardsCoordinator.EarnerTreeMerkleLeaf calldata earnerLeaf) public { + SetupPaymentsLib.processClaim( + IRewardsCoordinator(coreDeployment.rewardsCoordinator), + filePath, + indexToProve, + recipient, + earnerLeaf, + NUM_TOKEN_EARNINGS, + helloWorldDeployment.strategy + ); + } + + function submitPaymentRoot(address[] calldata earners, uint32 endTimestamp, uint32 numPayments, uint32 amountPerPayment) public { + bytes32[] memory tokenLeaves = SetupPaymentsLib.createTokenLeaves( + IRewardsCoordinator(coreDeployment.rewardsCoordinator), + NUM_TOKEN_EARNINGS, + amountPerPayment, + helloWorldDeployment.strategy + ); + IRewardsCoordinator.EarnerTreeMerkleLeaf[] memory earnerLeaves = SetupPaymentsLib.createEarnerLeaves(earners, tokenLeaves); + + SetupPaymentsLib.submitRoot( + IRewardsCoordinator(coreDeployment.rewardsCoordinator), + tokenLeaves, + earnerLeaves, + helloWorldDeployment.strategy, + endTimestamp, + numPayments, + NUM_TOKEN_EARNINGS + ); + } +} \ No newline at end of file diff --git a/contracts/script/utils/CoreDeploymentLib.sol b/contracts/script/utils/CoreDeploymentLib.sol index 31ad403d..a2d33db6 100644 --- a/contracts/script/utils/CoreDeploymentLib.sol +++ b/contracts/script/utils/CoreDeploymentLib.sol @@ -144,11 +144,11 @@ library CoreDeploymentLib { ); /// TODO: Get actual values - uint32 CALCULATION_INTERVAL_SECONDS = 10 days; - uint32 MAX_REWARDS_DURATION = 10; + uint32 CALCULATION_INTERVAL_SECONDS = 1 days; + uint32 MAX_REWARDS_DURATION = 1 days; uint32 MAX_RETROACTIVE_LENGTH = 1; uint32 MAX_FUTURE_LENGTH = 1; - uint32 GENESIS_REWARDS_TIMESTAMP = 100 days; + uint32 GENESIS_REWARDS_TIMESTAMP = 10 days; address rewardsCoordinatorImpl = address( new RewardsCoordinator( IDelegationManager(result.delegationManager), diff --git a/contracts/script/utils/HelloWorldDeploymentLib.sol b/contracts/script/utils/HelloWorldDeploymentLib.sol index 98988fa9..658f6822 100644 --- a/contracts/script/utils/HelloWorldDeploymentLib.sol +++ b/contracts/script/utils/HelloWorldDeploymentLib.sol @@ -26,6 +26,8 @@ library HelloWorldDeploymentLib { struct DeploymentData { address helloWorldServiceManager; address stakeRegistry; + address strategy; + address token; } function deployContracts( @@ -43,7 +45,7 @@ library HelloWorldDeploymentLib { address(new ECDSAStakeRegistry(IDelegationManager(core.delegationManager))); address helloWorldServiceManagerImpl = address( new HelloWorldServiceManager( - core.avsDirectory, result.stakeRegistry, core.delegationManager + core.avsDirectory, result.stakeRegistry, core.rewardsCoordinator, core.delegationManager ) ); // Upgrade contracts @@ -136,7 +138,11 @@ library HelloWorldDeploymentLib { data.stakeRegistry.toHexString(), '","stakeRegistryImpl":"', data.stakeRegistry.getImplementation().toHexString(), - '"}' + '","strategy":"', + data.strategy.toHexString(), + '","token":"', + data.token.toHexString(), + '"}' ); } } diff --git a/contracts/script/utils/SetupPaymentsLib.sol b/contracts/script/utils/SetupPaymentsLib.sol new file mode 100644 index 00000000..fd48093c --- /dev/null +++ b/contracts/script/utils/SetupPaymentsLib.sol @@ -0,0 +1,271 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {IRewardsCoordinator} from "@eigenlayer/contracts/interfaces/IRewardsCoordinator.sol"; +import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategyManager.sol"; +import {Vm} from "forge-std/Vm.sol"; + + +library SetupPaymentsLib { + + Vm internal constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + string internal constant filePath = "test/mockData/payments/payments.json"; + + struct PaymentLeaves { + bytes32[] leaves; + bytes32[] tokenLeaves; + } + + function createAVSRewardsSubmissions( + IRewardsCoordinator rewardsCoordinator, + address strategy, + uint256 numPayments, + uint256 amountPerPayment, + uint32 duration, + uint32 startTimestamp + ) internal { + IRewardsCoordinator.RewardsSubmission[] memory rewardsSubmissions = new IRewardsCoordinator.RewardsSubmission[](numPayments); + for (uint256 i = 0; i < numPayments; i++) { + IRewardsCoordinator.StrategyAndMultiplier[] memory strategiesAndMultipliers = new IRewardsCoordinator.StrategyAndMultiplier[](1); + strategiesAndMultipliers[0] = IRewardsCoordinator.StrategyAndMultiplier({ + strategy: IStrategy(strategy), + multiplier: 10000 + }); + + IRewardsCoordinator.RewardsSubmission memory rewardsSubmission = IRewardsCoordinator.RewardsSubmission({ + strategiesAndMultipliers: strategiesAndMultipliers, + token: IStrategy(strategy).underlyingToken(), + amount: amountPerPayment, + startTimestamp: startTimestamp , + duration: duration + }); + + rewardsSubmissions[i] = rewardsSubmission; + } + + rewardsCoordinator.createAVSRewardsSubmission(rewardsSubmissions); + } + + function processClaim( + IRewardsCoordinator rewardsCoordinator, + string memory filePath, + uint256 indexToProve, + address recipient, + IRewardsCoordinator.EarnerTreeMerkleLeaf memory earnerLeaf, + uint256 NUM_TOKEN_EARNINGS, + address strategy + ) internal { + PaymentLeaves memory paymentLeaves = parseLeavesFromJson(filePath); + + bytes memory proof = generateMerkleProof(paymentLeaves.leaves, indexToProve); + bytes memory tokenProof = generateMerkleProof(paymentLeaves.tokenLeaves, 0); + + uint32[] memory tokenIndices = new uint32[](NUM_TOKEN_EARNINGS); + bytes[] memory tokenProofs = new bytes[](NUM_TOKEN_EARNINGS); + tokenProofs[0] = tokenProof; + + IRewardsCoordinator.TokenTreeMerkleLeaf[] memory tokenLeaves = new IRewardsCoordinator.TokenTreeMerkleLeaf[](NUM_TOKEN_EARNINGS); + tokenLeaves[0] = defaultTokenLeaf(100, strategy); + + IRewardsCoordinator.RewardsMerkleClaim memory claim = IRewardsCoordinator.RewardsMerkleClaim({ + rootIndex: 0, + earnerIndex: uint32(indexToProve), + earnerTreeProof: proof, + earnerLeaf: earnerLeaf, + tokenIndices: tokenIndices, + tokenTreeProofs: tokenProofs, + tokenLeaves: tokenLeaves + }); + + rewardsCoordinator.processClaim(claim, recipient); + } + + function submitRoot( + IRewardsCoordinator rewardsCoordinator, + bytes32[] memory tokenLeaves, + IRewardsCoordinator.EarnerTreeMerkleLeaf[] memory earnerLeaves, + address strategy, + uint32 rewardsCalculationEndTimestamp, + uint256 NUM_PAYMENTS, + uint256 NUM_TOKEN_EARNINGS + ) internal { + bytes32 paymentRoot = createPaymentRoot(rewardsCoordinator, tokenLeaves, earnerLeaves, NUM_PAYMENTS, NUM_TOKEN_EARNINGS, strategy); + rewardsCoordinator.submitRoot(paymentRoot, rewardsCalculationEndTimestamp); + } + + function createPaymentRoot( + IRewardsCoordinator rewardsCoordinator, + bytes32[] memory tokenLeaves, + IRewardsCoordinator.EarnerTreeMerkleLeaf[] memory earnerLeaves, + uint256 NUM_PAYMENTS, + uint256 NUM_TOKEN_EARNINGS, + address strategy + ) internal returns (bytes32) { + require(earnerLeaves.length == NUM_PAYMENTS, "Number of earners must match number of payments"); + bytes32[] memory leaves = new bytes32[](NUM_PAYMENTS); + + require(tokenLeaves.length == NUM_TOKEN_EARNINGS, "Number of token leaves must match number of token earnings"); + for (uint256 i = 0; i < NUM_PAYMENTS; i++) { + leaves[i] = rewardsCoordinator.calculateEarnerLeafHash(earnerLeaves[i]); + } + + writeLeavesToJson(leaves, tokenLeaves); + return (merkleizeKeccak(leaves)); + } + + function createEarnerLeaves( + address[] calldata earners, + bytes32[] memory tokenLeaves + ) public returns (IRewardsCoordinator.EarnerTreeMerkleLeaf[] memory) { + IRewardsCoordinator.EarnerTreeMerkleLeaf[] memory leaves = new IRewardsCoordinator.EarnerTreeMerkleLeaf[](earners.length); + for (uint256 i = 0; i < earners.length; i++) { + leaves[i] = IRewardsCoordinator.EarnerTreeMerkleLeaf({ + earner: earners[i], + earnerTokenRoot: createTokenRoot(tokenLeaves) + }); + } + return leaves; + } + + function createTokenRoot(bytes32[] memory tokenLeaves) public pure returns (bytes32) { + return merkleizeKeccak(tokenLeaves); + } + + function createTokenLeaves( + IRewardsCoordinator rewardsCoordinator, + uint256 NUM_TOKEN_EARNINGS, + uint256 TOKEN_EARNINGS, + address strategy + ) internal returns (bytes32[] memory) { + bytes32[] memory leaves = new bytes32[](NUM_TOKEN_EARNINGS); + for (uint256 i = 0; i < NUM_TOKEN_EARNINGS; i++) { + IRewardsCoordinator.TokenTreeMerkleLeaf memory leaf = defaultTokenLeaf(TOKEN_EARNINGS, strategy); + leaves[i] = rewardsCoordinator.calculateTokenLeafHash(leaf); + } + return leaves; + } + + function defaultTokenLeaf( + uint256 TOKEN_EARNINGS, + address strategy + ) internal view returns (IRewardsCoordinator.TokenTreeMerkleLeaf memory) { + IRewardsCoordinator.TokenTreeMerkleLeaf memory leaf = IRewardsCoordinator.TokenTreeMerkleLeaf({ + token: IStrategy(strategy).underlyingToken(), + cumulativeEarnings: TOKEN_EARNINGS + }); + return leaf; + } + + function writeLeavesToJson( + bytes32[] memory leaves, + bytes32[] memory tokenLeaves + ) internal { + string memory parent_object = "parent_object"; + vm.serializeBytes32(parent_object, "leaves", leaves); + string memory finalJson = vm.serializeBytes32(parent_object, "tokenLeaves", tokenLeaves); + vm.writeJson(finalJson, filePath); + } + + function parseLeavesFromJson(string memory filePath) internal returns (PaymentLeaves memory) { + string memory json = vm.readFile(filePath); + bytes memory data = vm.parseJson(json); + return abi.decode(data, (PaymentLeaves)); + } + + function generateMerkleProof(bytes32[] memory leaves, uint256 index) internal pure returns (bytes memory) { + require(leaves.length > 0, "Leaves array cannot be empty"); + require(index < leaves.length, "Index out of bounds"); + + leaves = padLeaves(leaves); + + uint256 n = leaves.length; + uint256 depth = 0; + while ((1 << depth) < n) { + depth++; + } + + bytes32[] memory proof = new bytes32[](depth); + uint256 proofIndex = 0; + + for (uint256 i = 0; i < depth; i++) { + uint256 levelSize = (n + 1) / 2; + uint256 siblingIndex = (index % 2 == 0) ? index + 1 : index - 1; + + if (siblingIndex < n) { + proof[proofIndex] = leaves[siblingIndex]; + proofIndex++; + } + + for (uint256 j = 0; j < levelSize; j++) { + if (2 * j + 1 < n) { + leaves[j] = keccak256(abi.encodePacked(leaves[2 * j], leaves[2 * j + 1])); + } else { + leaves[j] = leaves[2 * j]; + } + } + + n = levelSize; + index /= 2; + } + + return abi.encodePacked(proof); + } + + /** + * @notice this function returns the merkle root of a tree created from a set of leaves using keccak256 as its hash function + * @param leaves the leaves of the merkle tree + * @return The computed Merkle root of the tree. + * @dev This pads to the next power of 2. very inefficient! just for POC + */ + function merkleizeKeccak(bytes32[] memory leaves) internal pure returns (bytes32) { + // uint256 paddedLength = 2; + // while(paddedLength < leaves.length) { + // paddedLength <<= 1; + // } + + // bytes32[] memory paddedLeaves = new bytes32[](paddedLength); + // for (uint256 i = 0; i < leaves.length; i++) { + // paddedLeaves[i] = leaves[i]; + // } + leaves = padLeaves(leaves); + + //there are half as many nodes in the layer above the leaves + uint256 numNodesInLayer = leaves.length / 2; + //create a layer to store the internal nodes + bytes32[] memory layer = new bytes32[](numNodesInLayer); + //fill the layer with the pairwise hashes of the leaves + for (uint256 i = 0; i < numNodesInLayer; i++) { + layer[i] = keccak256(abi.encodePacked(leaves[2 * i], leaves[2 * i + 1])); + } + //the next layer above has half as many nodes + numNodesInLayer /= 2; + //while we haven't computed the root + while (numNodesInLayer != 0) { + //overwrite the first numNodesInLayer nodes in layer with the pairwise hashes of their children + for (uint256 i = 0; i < numNodesInLayer; i++) { + layer[i] = keccak256(abi.encodePacked(layer[2 * i], layer[2 * i + 1])); + } + //the next layer above has half as many nodes + numNodesInLayer /= 2; + } + //the first node in the layer is the root + return layer[0]; + } + + function padLeaves(bytes32[] memory leaves) internal pure returns (bytes32[] memory) { + uint256 paddedLength = 2; + while(paddedLength < leaves.length) { + paddedLength <<= 1; + } + + bytes32[] memory paddedLeaves = new bytes32[](paddedLength); + for (uint256 i = 0; i < leaves.length; i++) { + paddedLeaves[i] = leaves[i]; + } + return paddedLeaves; + } + + function getFilePath() public pure returns (string memory) { + return filePath; + } +} \ No newline at end of file diff --git a/contracts/src/HelloWorldServiceManager.sol b/contracts/src/HelloWorldServiceManager.sol index 145e0a5e..fdebbbdc 100644 --- a/contracts/src/HelloWorldServiceManager.sol +++ b/contracts/src/HelloWorldServiceManager.sol @@ -10,6 +10,8 @@ import {ECDSAUpgradeable} from import {IERC1271Upgradeable} from "@openzeppelin-upgrades/contracts/interfaces/IERC1271Upgradeable.sol"; import {IHelloWorldServiceManager} from "./IHelloWorldServiceManager.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; +import "@eigenlayer/contracts/interfaces/IRewardsCoordinator.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; /** * @title Primary entrypoint for procuring services from HelloWorld. @@ -40,12 +42,14 @@ contract HelloWorldServiceManager is ECDSAServiceManagerBase, IHelloWorldService constructor( address _avsDirectory, address _stakeRegistry, + address _rewardsCoordinator, address _delegationManager + ) ECDSAServiceManagerBase( _avsDirectory, _stakeRegistry, - address(0), // hello-world doesn't need to deal with payments + _rewardsCoordinator, _delegationManager ) {} diff --git a/contracts/test/SetupPaymentsLib.t.sol b/contracts/test/SetupPaymentsLib.t.sol new file mode 100644 index 00000000..9295359a --- /dev/null +++ b/contracts/test/SetupPaymentsLib.t.sol @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "../script/utils/SetupPaymentsLib.sol"; +import "../script/utils/CoreDeploymentLib.sol"; +import "../script/utils/HelloWorldDeploymentLib.sol"; +import "@eigenlayer/contracts/interfaces/IRewardsCoordinator.sol"; +import "@eigenlayer/contracts/interfaces/IStrategy.sol"; +import "@eigenlayer/contracts/libraries/Merkle.sol"; +import "../script/DeployEigenLayerCore.s.sol"; +import "../script/HelloWorldDeployer.s.sol"; +import {StrategyFactory} from "@eigenlayer/contracts/strategies/StrategyFactory.sol"; +import {HelloWorldTaskManagerSetup} from "test/HelloWorldServiceManager.t.sol"; +import { + Quorum, + StrategyParams, + IStrategy +} from "@eigenlayer-middleware/src/interfaces/IECDSAStakeRegistryEventsAndErrors.sol"; + +contract TestConstants { + uint256 constant NUM_PAYMENTS = 8; + uint256 constant NUM_TOKEN_EARNINGS = 1; + uint256 constant TOKEN_EARNINGS = 100; + + address RECIPIENT = address(1); + address EARNER = address(2); + uint256 INDEX_TO_PROVE = 0; + uint256 NUM_EARNERS = 4; +} + +contract SetupPaymentsLibTest is Test, TestConstants, HelloWorldTaskManagerSetup { + using SetupPaymentsLib for *; + Vm cheats = Vm(VM_ADDRESS); + + + IRewardsCoordinator public rewardsCoordinator; + IStrategy public strategy; + address proxyAdmin; + + + function setUp() public override virtual { + proxyAdmin = UpgradeableProxyLib.deployProxyAdmin(); + coreConfigData = + CoreDeploymentLib.readDeploymentConfigValues("test/mockData/config/core/", 1337); // TODO: Fix this to correct path + coreDeployment = CoreDeploymentLib.deployContracts(proxyAdmin, coreConfigData); + + mockToken = new ERC20Mock(); + + strategy = addStrategy(address(mockToken)); // Similar function to HW_SM test using strategy factory + quorum.strategies.push(StrategyParams({strategy: strategy, multiplier: 10_000})); + + helloWorldDeployment = + HelloWorldDeploymentLib.deployContracts(proxyAdmin, coreDeployment, quorum); + labelContracts(coreDeployment, helloWorldDeployment); + + rewardsCoordinator = IRewardsCoordinator(coreDeployment.rewardsCoordinator); + mockToken.mint(address(this), 100000); + mockToken.mint(address(rewardsCoordinator), 100000); + } + + + function testSubmitRoot() public { + address[] memory earners = new address[](NUM_EARNERS); + for (uint256 i = 0; i < earners.length; i++) { + earners[i] = address(1); + } + uint32 endTimestamp = rewardsCoordinator.currRewardsCalculationEndTimestamp() + 1 weeks; + cheats.warp(endTimestamp + 1); + + + bytes32[] memory tokenLeaves = SetupPaymentsLib.createTokenLeaves(rewardsCoordinator, NUM_TOKEN_EARNINGS, TOKEN_EARNINGS, address(strategy)); + IRewardsCoordinator.EarnerTreeMerkleLeaf[] memory earnerLeaves =SetupPaymentsLib.createEarnerLeaves(earners, tokenLeaves); + + cheats.startPrank(address(0), address(0)); + SetupPaymentsLib.submitRoot(rewardsCoordinator, tokenLeaves, earnerLeaves, address(strategy), endTimestamp, NUM_EARNERS, 1); + cheats.stopPrank(); + } + + function testWriteLeavesToJson() public { + bytes32[] memory leaves = new bytes32[](2); + leaves[0] = bytes32(uint256(1)); + leaves[1] = bytes32(uint256(2)); + + bytes32[] memory tokenLeaves = new bytes32[](2); + tokenLeaves[0] = bytes32(uint256(3)); + tokenLeaves[1] = bytes32(uint256(4)); + + SetupPaymentsLib.writeLeavesToJson(leaves, tokenLeaves); + + assertTrue(vm.exists("payments.json"), "JSON file should be created"); + } + + function testParseLeavesFromJson() public { + string memory filePath = "test_parse_payments.json"; + string memory jsonContent = '{"leaves":["0x1234"], "tokenLeaves":["0x5678"]}'; + vm.writeFile(filePath, jsonContent); + + SetupPaymentsLib.PaymentLeaves memory paymentLeaves = SetupPaymentsLib.parseLeavesFromJson(filePath); + + assertEq(paymentLeaves.leaves.length, 1, "Incorrect number of leaves"); + assertEq(paymentLeaves.tokenLeaves.length, 1, "Incorrect number of token leaves"); + } + + function testGenerateMerkleProof() public { + SetupPaymentsLib.PaymentLeaves memory paymentLeaves = SetupPaymentsLib.parseLeavesFromJson("test/mockData/scratch/payments.json"); + + bytes32[] memory leaves = paymentLeaves.leaves; + uint256 indexToProve = 0; + + bytes32[] memory proof = new bytes32[](2); + proof[0] = leaves[1]; + proof[1] = keccak256(abi.encodePacked(leaves[2], leaves[3])); + + bytes memory proofBytesConstructed = abi.encodePacked(proof); + bytes memory proofBytesCalculated = SetupPaymentsLib.generateMerkleProof(leaves, indexToProve); + + require(keccak256(proofBytesConstructed) == keccak256(proofBytesCalculated), "Proofs do not match"); + + bytes32 root = SetupPaymentsLib.merkleizeKeccak(leaves); + + emit log_named_bytes("proof", proofBytesCalculated); + emit log_named_bytes32("root", root); + emit log_named_bytes32("leaf", leaves[indexToProve]); + + require(Merkle.verifyInclusionKeccak( + proofBytesCalculated, + root, + leaves[indexToProve], + indexToProve + )); + } + + function testProcessClaim() public { + emit log_named_address("token address", address(mockToken)); + string memory filePath = "test/mockData/scratch/payments.json"; + + address[] memory earners = new address[](NUM_EARNERS); + for (uint256 i = 0; i < earners.length; i++) { + earners[i] = address(1); + } + uint32 endTimestamp = rewardsCoordinator.currRewardsCalculationEndTimestamp() + 1 weeks; + cheats.warp(endTimestamp + 1); + + bytes32[] memory tokenLeaves = SetupPaymentsLib.createTokenLeaves(rewardsCoordinator, NUM_TOKEN_EARNINGS, TOKEN_EARNINGS, address(strategy)); + IRewardsCoordinator.EarnerTreeMerkleLeaf[] memory earnerLeaves =SetupPaymentsLib.createEarnerLeaves(earners, tokenLeaves); + + cheats.startPrank(address(0)); + SetupPaymentsLib.submitRoot(rewardsCoordinator, tokenLeaves, earnerLeaves, address(strategy), endTimestamp, NUM_EARNERS, 1); + cheats.stopPrank(); + + + cheats.warp(block.timestamp + 2 weeks); + + cheats.startPrank(earnerLeaves[INDEX_TO_PROVE].earner, earnerLeaves[INDEX_TO_PROVE].earner); + SetupPaymentsLib.processClaim( + rewardsCoordinator, + filePath, + INDEX_TO_PROVE, + RECIPIENT, + earnerLeaves[INDEX_TO_PROVE], + NUM_TOKEN_EARNINGS, + address(strategy) + ); + + cheats.stopPrank(); + } + + function testCreateAVSRewardsSubmissions() public { + uint256 numPayments = 5; + uint256 amountPerPayment = 100; + uint32 duration = rewardsCoordinator.MAX_REWARDS_DURATION(); + uint32 startTimestamp = 10 days; + cheats.warp(startTimestamp + 1); + mockToken.approve(address(rewardsCoordinator), amountPerPayment * numPayments); + + SetupPaymentsLib.createAVSRewardsSubmissions( + rewardsCoordinator, + address(strategy), + numPayments, + amountPerPayment, + duration, + startTimestamp + ); + } +} diff --git a/contracts/test/mockData/scratch/payments.json b/contracts/test/mockData/scratch/payments.json new file mode 100644 index 00000000..77873afa --- /dev/null +++ b/contracts/test/mockData/scratch/payments.json @@ -0,0 +1,11 @@ +{ + "leaves": [ + "0x29036a1d92861ffd464a1e285030fad3978a36f953ae33c160e606d2ac746c42", + "0x29036a1d92861ffd464a1e285030fad3978a36f953ae33c160e606d2ac746c42", + "0x29036a1d92861ffd464a1e285030fad3978a36f953ae33c160e606d2ac746c42", + "0x29036a1d92861ffd464a1e285030fad3978a36f953ae33c160e606d2ac746c42" + ], + "tokenLeaves": [ + "0xf5d87050cb923194fe63c7ed2c45cbc913fa6ecf322f3631149c36d9460b3ad6" + ] +} \ No newline at end of file diff --git a/contracts/test_parse_payments.json b/contracts/test_parse_payments.json new file mode 100644 index 00000000..91fdb0e3 --- /dev/null +++ b/contracts/test_parse_payments.json @@ -0,0 +1 @@ +{"leaves":["0x1234"], "tokenLeaves":["0x5678"]} \ No newline at end of file