From 6a2a91339f8536f18f5891df7cd3eec9e0085726 Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Thu, 1 May 2025 19:20:26 -0600 Subject: [PATCH 1/7] feat: delegation chain enforcer and test --- src/DelegationManager.sol | 7 + src/enforcers/DelegationChainEnforcer.sol | 289 +++++++++++++++++++ test/enforcers/DelegationChainEnforcer.t.sol | 238 +++++++++++++++ 3 files changed, 534 insertions(+) create mode 100644 src/enforcers/DelegationChainEnforcer.sol create mode 100644 test/enforcers/DelegationChainEnforcer.t.sol diff --git a/src/DelegationManager.sol b/src/DelegationManager.sol index 4d6a9811..7d89074f 100644 --- a/src/DelegationManager.sol +++ b/src/DelegationManager.sol @@ -14,6 +14,7 @@ import { IDeleGatorCore } from "./interfaces/IDeleGatorCore.sol"; import { Delegation, Caveat, ModeCode } from "./utils/Types.sol"; import { EncoderLib } from "./libraries/EncoderLib.sol"; import { ERC1271Lib } from "./libraries/ERC1271Lib.sol"; +import "forge-std/Test.sol"; /** * @title DelegationManager @@ -154,6 +155,9 @@ contract DelegationManager is IDelegationManager, Ownable2Step, Pausable, EIP712 // Validate caller if (delegations_[0].delegate != msg.sender && delegations_[0].delegate != ANY_DELEGATE) { + console2.log("Invalid delegate1"); + console2.log("delegations_[0].delegate:", delegations_[0].delegate); + console2.log("msg.sender:", msg.sender); revert InvalidDelegate(); } @@ -195,6 +199,9 @@ contract DelegationManager is IDelegationManager, Ownable2Step, Pausable, EIP712 // Validate delegate address nextDelegate_ = delegations_[delegationsIndex_ + 1].delegate; if (nextDelegate_ != ANY_DELEGATE && delegations_[delegationsIndex_].delegator != nextDelegate_) { + console2.log("Invalid delegate2"); + console2.log("nextDelegate_:", nextDelegate_); + console2.log("delegations_[delegationsIndex_].delegator:", delegations_[delegationsIndex_].delegator); revert InvalidDelegate(); } } else if (delegations_[delegationsIndex_].authority != ROOT_AUTHORITY) { diff --git a/src/enforcers/DelegationChainEnforcer.sol b/src/enforcers/DelegationChainEnforcer.sol new file mode 100644 index 00000000..f9114b1a --- /dev/null +++ b/src/enforcers/DelegationChainEnforcer.sol @@ -0,0 +1,289 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { Ownable2Step, Ownable } from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { IDelegationManager } from "../interfaces/IDelegationManager.sol"; +import { CaveatEnforcer } from "./CaveatEnforcer.sol"; +import { ModeCode, Delegation } from "../utils/Types.sol"; +import "forge-std/Test.sol"; + +/** + * @title DelegationChainEnforcer + * @dev This contract enforces the allowed methods a delegate may call. + * @dev This enforcer operates only in single execution call type and with default execution mode. + */ +contract DelegationChainEnforcer is CaveatEnforcer, Ownable { + using ExecutionLib for bytes; + + mapping(address delegationManager => mapping(bytes32 referralChainHash => address[] recipients)) public referrals; + mapping(address delegationManager => mapping(bytes32 referralChainHash => bool redeemed)) public redeemed; + + /// @dev The Delegation Manager contract to redeem the delegation + IDelegationManager public immutable delegationManager; + + /// @dev The enforcer used to compare args and terms + address public immutable argsEqualityCheckEnforcer; + + // TODO: make this constant if possible + uint256[] public prizeAmounts; + + ////////////////////////////// Events ////////////////////////////// + + event ReferralChainPosted(address indexed sender, bytes32 indexed referralChainHash); + event PrizesSet(address indexed sender, uint256[] prizeLevels); + ////////////////////////////// Constructor ////////////////////////////// + + // TODO: make the levels an array or struct limited to length 5 + constructor( + address _owner, + IDelegationManager _delegationManager, + address _argsEqualityCheckEnforcer, + uint256[] memory _prizeLevels + ) + Ownable(_owner) + { + require(_delegationManager != IDelegationManager(address(0)), "DelegationChainEnforcer:invalid-delegationManager"); + require(_argsEqualityCheckEnforcer != address(0), "DelegationChainEnforcer:invalid-argsEqualityCheckEnforcer"); + + delegationManager = _delegationManager; + argsEqualityCheckEnforcer = _argsEqualityCheckEnforcer; + _setPrizes(_prizeLevels); + } + + ////////////////////////////// External Methods ////////////////////////////// + + function setPrizes(uint256[] memory _prizeLevels) external onlyOwner { + _setPrizes(_prizeLevels); + } + + function post(address[] calldata _delegators) external onlyOwner { + uint256 delegatorsLength_ = _delegators.length; + require(delegatorsLength_ > 1, "DelegationChainEnforcer:invalid-delegators-length"); + + // TODO: make sure someonen can't add himself twice in the same delegation chain + // TODO: make sure the intermediary chain is involved otherwise fail, pleople would add themselves to the chain at the end + // multiple times to get paid multiple times, or an address they control? + + bytes32 referralChainHash_ = keccak256(abi.encode(_delegators)); + console2.log("1referralChainHash_:"); + console2.logBytes32(referralChainHash_); + console2.log("msg.sender:", msg.sender); + + // TODO: msg.sender here might be dangerous, the owner could do something unwanted, centralization risk + // what can someone with a delegation do? + address[] storage referrals_ = referrals[address(delegationManager)][referralChainHash_]; + require(referrals_.length == 0, "DelegationChainEnforcer:referral-chain-already-posted"); + + // Push up to the last 5 delegators in reverse order + uint256 startIndex_ = delegatorsLength_ < 5 ? 0 : delegatorsLength_ - 5; + for (uint256 i = delegatorsLength_; i > startIndex_; i--) { + console2.log("it is pushing", _delegators[i - 1]); + referrals_.push(_delegators[i - 1]); + } + + console2.log("Here referrals_.length:", referrals_.length); + + emit ReferralChainPosted(msg.sender, referralChainHash_); + } + + ////////////////////////////// Public Methods ////////////////////////////// + + // The beforeHook validates that the delegation being redeemed correctly matches the delegation chain order (checking the + // delegator address, and the delegate address if present in the terms). + + // Upon successful validation of the entire delegation chain, the transaction executes the attestation call on the + // DelegationChainEnforcer contract. This call formally posts the entire delegation chain addresses on-chain, creating a + // permanent record of the referral event. + + function beforeHook( + bytes calldata _terms, + bytes calldata, + ModeCode _mode, + bytes calldata _executionCallData, + bytes32, + address _delegator, + address + ) + public + pure + override + onlySingleCallTypeMode(_mode) + onlyDefaultExecutionMode(_mode) + { + _validatePosition(_executionCallData, _terms, _delegator); + } + + function _validatePosition(bytes calldata _executionCallData, bytes calldata _terms, address _delegator) internal pure { + (,, bytes calldata callData_) = _executionCallData.decodeSingle(); + + // TODO: this is better in separate enforcers, so it doesn't repeat on each level + // (address target_, uint256 value_, bytes calldata callData_) = _executionCallData.decodeSingle(); + // require(bytes4(callData_[0:4]) == this.post.selector, "DelegationChainEnforcer:invalid-method"); + // require(value_ == 0, "DelegationChainEnforcer:invalid-value"); + // require(target_ == address(this), "DelegationChainEnforcer:invalid-target"); + + (address[] memory delegators_) = abi.decode(callData_[4:], (address[])); + + // Restriction for gas costs. + require(delegators_.length <= 20, "DelegationChainEnforcer:invalid-delegators-length"); + + uint256 expectedPosition_ = getTermsInfo(_terms); + + require(delegators_.length > expectedPosition_, "DelegationChainEnforcer:invalid-expected-position"); + + require(delegators_[expectedPosition_] == _delegator, "DelegationChainEnforcer:invalid-delegator-or-position"); + } + + // This only runs the entire function if the delegation chain has not been redeemed yet. + // The redeemer fills the args on the root delegation, and it will only run for that delegation. + function afterHook( + bytes calldata, + bytes calldata _args, + ModeCode, + bytes calldata _executionCallData, + bytes32 _delegationHash, + address _delegator, + address _redeemer + ) + public + override + { + // console log the args + console2.log("args:"); + console2.logBytes(_args); + + console2.log("Checkpoint after hook"); + console2.log("_delegator:", _delegator); + console2.log("_redeemer:", _redeemer); + + //TODO: Could i use the post params hash instead and a new mapping? + bytes32 referralChainHash_ = keccak256(abi.encode(_executionCallData)); + if (redeemed[msg.sender][referralChainHash_]) return; + + (address[] memory referrals_, uint256 referralLength_) = _getReferrals(_executionCallData); + // require(referralLength_ > 2, "DelegationChainEnforcer:invalid-referrals-length"); + + // TODO: make the token constant if possible + (Delegation[][] memory allowanceDelegations_, uint256 allowanceLength_, address token_) = + _validateAndDecodeArgs(_args, _delegationHash, _redeemer); + + bytes[] memory permissionContexts_ = new bytes[](allowanceLength_); + bytes[] memory executionCallDatas_ = new bytes[](allowanceLength_); + ModeCode[] memory encodedModes_ = new ModeCode[](allowanceLength_); + + console2.log("allowanceDelegations_.length:", allowanceDelegations_.length); + console2.log("referralLength_:", referralLength_); + + require(referralLength_ == allowanceDelegations_.length, "DelegationChainEnforcer:invalid-delegations-length"); + + for (uint256 i = 0; i < allowanceLength_; ++i) { + permissionContexts_[i] = abi.encode(allowanceDelegations_[i]); + executionCallDatas_[i] = + ExecutionLib.encodeSingle(token_, 0, abi.encodeCall(IERC20.transfer, (referrals_[i], prizeAmounts[i]))); + encodedModes_[i] = ModeLib.encodeSimpleSingle(); + } + + uint256[] memory balanceBefore_ = _getBalances(referrals_, token_); + + // Attempt to redeem the delegation and make the payment + delegationManager.redeemDelegations(permissionContexts_, encodedModes_, executionCallDatas_); + + //TODO: If this fails, for memory, then try to pass the redeemDelegations() as callback below. + _validateTransfer(referrals_, token_, balanceBefore_); + + redeemed[msg.sender][referralChainHash_] = true; + } + + /** + * @notice Decodes the terms used in this CaveatEnforcer. + * @param _terms encoded data that is used during the execution hooks. + * @return expectedPosition_ The position of the expected delegator in the delegation chain. + */ + function getTermsInfo(bytes calldata _terms) public pure returns (uint256 expectedPosition_) { + require(_terms.length == 32, "DelegationChainEnforcer:invalid-terms-length"); + expectedPosition_ = uint256(bytes32(_terms[:32])); + } + + ////////////////////////////// Internal Methods ////////////////////////////// + + function _setPrizes(uint256[] memory _prizeLevels) internal { + uint256 prizeLevelsLength_ = _prizeLevels.length; + require(prizeLevelsLength_ == 5, "DelegationChainEnforcer:invalid-prize-levels-length"); + + // Clear existing prize amounts + delete prizeAmounts; + + for (uint256 i = 0; i < prizeLevelsLength_; ++i) { + require(_prizeLevels[i] > 0, "DelegationChainEnforcer:invalid-prize-level"); + prizeAmounts.push(_prizeLevels[i]); + } + + emit PrizesSet(msg.sender, _prizeLevels); + } + + function _getBalances(address[] memory _recipients, address _token) internal view returns (uint256[] memory balances_) { + uint256 recipientsLength_ = _recipients.length; + balances_ = new uint256[](recipientsLength_); + for (uint256 i = 0; i < recipientsLength_; ++i) { + balances_[i] = IERC20(_token).balanceOf(_recipients[i]); + } + } + + function _validateTransfer(address[] memory _recipients, address _token, uint256[] memory balanceBefore_) internal view { + // TODO: prizeAmounts read twice from storage + uint256[] memory balances_ = _getBalances(_recipients, _token); + for (uint256 i = 0; i < _recipients.length; ++i) { + require(balances_[i] >= balanceBefore_[i] + prizeAmounts[i], "DelegationChainEnforcer:payment-not-received"); + } + } + + function _getReferrals(bytes calldata _executionCallData) + internal + view + returns (address[] memory referrals_, uint256 referralLength_) + { + (,, bytes calldata callData_) = _executionCallData.decodeSingle(); + + (address[] memory delegators_) = abi.decode(callData_[4:], (address[])); + console2.log("delegators_.length:", delegators_.length); + + bytes32 referralChainHash_ = keccak256(abi.encode(delegators_)); + console2.log("2referralChainHash_:"); + console2.logBytes32(referralChainHash_); + + referrals_ = referrals[msg.sender][referralChainHash_]; + referralLength_ = referrals_.length; + console2.log("msg.sender:", msg.sender); + console2.log("referralLength_:", referralLength_); + } + + function _validateAndDecodeArgs( + bytes calldata _args, + bytes32 _delegationHash, + address _redeemer + ) + internal + view + returns (Delegation[][] memory allowanceDelegations_, uint256 allowanceLength_, address token_) + { + // TODO: make the token constant if possible + // TODO: we could assume a single direct delegation, instead of an array of delegations + (allowanceDelegations_, token_) = abi.decode(_args, (Delegation[][], address)); + allowanceLength_ = allowanceDelegations_.length; + require(allowanceLength_ > 0, "DelegationChainEnforcer:invalid-allowance-delegations-length"); + + for (uint256 i = 0; i < allowanceLength_; ++i) { + require( + allowanceDelegations_[i][0].caveats.length > 0 + && allowanceDelegations_[i][0].caveats[0].enforcer == argsEqualityCheckEnforcer, + "DelegationChainEnforcer:missing-argsEqualityCheckEnforcer" + ); + // The Args Enforcer with this data (hash & redeemer) must be the first Enforcer in the payment delegations caveats + allowanceDelegations_[i][0].caveats[0].args = abi.encodePacked(_delegationHash, _redeemer); + } + } +} diff --git a/test/enforcers/DelegationChainEnforcer.t.sol b/test/enforcers/DelegationChainEnforcer.t.sol new file mode 100644 index 00000000..a1239a31 --- /dev/null +++ b/test/enforcers/DelegationChainEnforcer.t.sol @@ -0,0 +1,238 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; + +import { BaseTest } from "../utils/BaseTest.t.sol"; +import { Delegation, Caveat, Execution } from "../../src/utils/Types.sol"; +import { Implementation, SignatureType, TestUser } from "../utils/Types.t.sol"; +import { EncoderLib } from "../../src/libraries/EncoderLib.sol"; +import { DelegationManager } from "../../src/DelegationManager.sol"; +import { IDelegationManager } from "../../src/interfaces/IDelegationManager.sol"; +import { MultiSigDeleGator } from "../../src/MultiSigDeleGator.sol"; +import { DelegationChainEnforcer } from "../../src/enforcers/DelegationChainEnforcer.sol"; +import { ArgsEqualityCheckEnforcer } from "../../src/enforcers/ArgsEqualityCheckEnforcer.sol"; +import { AllowedTargetsEnforcer } from "../../src/enforcers/AllowedTargetsEnforcer.sol"; +import { ValueLteEnforcer } from "../../src/enforcers/ValueLteEnforcer.sol"; +import { AllowedMethodsEnforcer } from "../../src/enforcers/AllowedMethodsEnforcer.sol"; +import { ERC20TransferAmountEnforcer } from "../../src/enforcers/ERC20TransferAmountEnforcer.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { BasicERC20 } from "../utils/BasicERC20.t.sol"; +import "forge-std/Test.sol"; + +contract DelegationChainEnforcerTest is BaseTest { + ////////////////////////////// Setup ////////////////////////////// + + TestUser public chainIntegrity; + TestUser public treasury; + // Intermediary Chain Account + TestUser public ICA; + + MultiSigDeleGator public aliceDeleGator; + MultiSigDeleGator public bobDeleGator; + DelegationChainEnforcer public delegationChainEnforcer; + ArgsEqualityCheckEnforcer public argsEqualityCheckEnforcer; + AllowedTargetsEnforcer public allowedTargetsEnforcer; + ValueLteEnforcer public valueLteEnforcer; + AllowedMethodsEnforcer public allowedMethodsEnforcer; + ERC20TransferAmountEnforcer public erc20TransferAmountEnforcer; + address[] public delegators; + + // Prize levels for the delegation chain rewards + uint256[] public prizeLevels; + BasicERC20 public token; + bytes32 public firstReferralDelegationHash; + + constructor() { + IMPLEMENTATION = Implementation.MultiSig; + SIGNATURE_TYPE = SignatureType.EOA; + } + + function setUp() public virtual override { + super.setUp(); + + chainIntegrity = createUser("ChainIntegrity"); + treasury = createUser("Treasury"); + ICA = createUser("Intermediary Chain Account"); + aliceDeleGator = MultiSigDeleGator(payable(users.alice.deleGator)); + bobDeleGator = MultiSigDeleGator(payable(users.bob.deleGator)); + + // Set up prize levels + prizeLevels.push(10 ether); + prizeLevels.push(10 ether); + prizeLevels.push(2.5 ether); + prizeLevels.push(1.5 ether); + prizeLevels.push(1 ether); + + argsEqualityCheckEnforcer = new ArgsEqualityCheckEnforcer(); + allowedTargetsEnforcer = new AllowedTargetsEnforcer(); + valueLteEnforcer = new ValueLteEnforcer(); + allowedMethodsEnforcer = new AllowedMethodsEnforcer(); + delegationChainEnforcer = new DelegationChainEnforcer( + address(chainIntegrity.deleGator), + IDelegationManager(address(delegationManager)), + address(argsEqualityCheckEnforcer), + prizeLevels + ); + + token = new BasicERC20(address(this), "USDC", "USDC", 18); + token.mint(address(treasury.deleGator), 100 ether); + } + + // Should create a delegation chain from chainIntegrity -> alice -> ICA -> bob -> ICA + function test_chainIntegrityCanDelegateToAliceToICAToBoB() public { + // Create delegation from chainIntegrity to alice + Delegation memory chainToAlice_ = Delegation({ + delegate: address(users.alice.deleGator), + delegator: address(chainIntegrity.deleGator), + authority: ROOT_AUTHORITY, + caveats: _getChainIntegrityCaveats(), + salt: 0, + signature: hex"" + }); + chainToAlice_ = signDelegation(chainIntegrity, chainToAlice_); + + // Create delegation from alice to ICA + Delegation memory aliceToICA_ = Delegation({ + delegate: address(ICA.deleGator), + delegator: address(users.alice.deleGator), + authority: EncoderLib._getDelegationHash(chainToAlice_), + caveats: _getPositionCaveats(0), + salt: 0, + signature: hex"" + }); + aliceToICA_ = signDelegation(users.alice, aliceToICA_); + firstReferralDelegationHash = EncoderLib._getDelegationHash(aliceToICA_); + + // Create delegation from ICA to bob + Delegation memory ICAToBob_ = Delegation({ + delegate: address(users.bob.deleGator), + delegator: address(ICA.deleGator), + authority: firstReferralDelegationHash, + caveats: new Caveat[](0), + salt: 0, + signature: hex"" + }); + ICAToBob_ = signDelegation(ICA, ICAToBob_); + + // Create delegation from bob back to ICA + Delegation memory bobToICA_ = Delegation({ + delegate: address(ICA.deleGator), + delegator: address(users.bob.deleGator), + authority: EncoderLib._getDelegationHash(ICAToBob_), + caveats: _getPositionCaveats(1), + salt: 0, + signature: hex"" + }); + bobToICA_ = signDelegation(users.bob, bobToICA_); + + delegators.push(address(users.alice.deleGator)); + delegators.push(address(users.bob.deleGator)); + + // Adding the redemption args only to the alice delegation + aliceToICA_.caveats[0].args = _getRedemptionArgs(); + + // Build delegation chain + Delegation[] memory delegations_ = new Delegation[](4); + delegations_[0] = bobToICA_; + delegations_[1] = ICAToBob_; + delegations_[2] = aliceToICA_; + delegations_[3] = chainToAlice_; + + uint256[] memory balancesBefore_ = _getBalances(delegators); + + // console2.log("delegations_[1].caveat.args"); + // console2.logBytes(delegations_[1].caveats[0].args); + + // Execute the delegation chain through ICA + invokeDelegation_UserOp(ICA, delegations_, _getExecution()); + + _validatePayments(balancesBefore_); + } + + // The args contain a delegation chain for each of the delegators/prize levels, and the token to redeem the payments. + function _getRedemptionArgs() internal view returns (bytes memory encoded_) { + // Create delegation from treasury with caveats + // Args enforcer at index 0 + // Includes the first referral delegation hash and the ICA address, because the first delegation with the delegation chain + // enforcer is the one that redeems the payments after that the others skip it. + // The args enforcer needs the redeemer, the delegation hash alone is not enough. + + Delegation[][] memory delegations_ = new Delegation[][](delegators.length); + for (uint256 i = 0; i < delegators.length; i++) { + delegations_[i] = new Delegation[](1); + + Caveat[] memory caveats_ = new Caveat[](2); + + caveats_[0] = Caveat({ + args: hex"", + enforcer: address(argsEqualityCheckEnforcer), + terms: abi.encodePacked(firstReferralDelegationHash, address(address(ICA.deleGator))) + }); + + caveats_[1] = Caveat({ + args: hex"", + enforcer: address(erc20TransferAmountEnforcer), + terms: abi.encode(token, prizeLevels[i]) // Use prize level for this delegator + }); + delegations_[i][0] = Delegation({ + delegate: address(delegationChainEnforcer), + delegator: address(treasury.deleGator), + authority: ROOT_AUTHORITY, + caveats: caveats_, + salt: 0, + signature: hex"" + }); + + delegations_[i][0] = signDelegation(treasury, delegations_[i][0]); + } + encoded_ = abi.encode(delegations_, token); + } + + function _getBalances(address[] memory _recipients) internal view returns (uint256[] memory balances_) { + uint256 recipientsLength_ = _recipients.length; + balances_ = new uint256[](recipientsLength_); + for (uint256 i = 0; i < recipientsLength_; ++i) { + balances_[i] = IERC20(token).balanceOf(_recipients[i]); + } + } + + function _validatePayments(uint256[] memory balanceBefore_) internal { + uint256[] memory balances_ = _getBalances(delegators); + for (uint256 i = 0; i < delegators.length; i++) { + assertEq(balances_[i], balanceBefore_[i] + prizeLevels[i], "The balance after is insufficient"); + } + } + + function _getExecution() internal view returns (Execution memory execution_) { + execution_ = Execution({ + target: address(delegationChainEnforcer), + value: 0, + callData: abi.encodeCall(DelegationChainEnforcer.post, (delegators)) + }); + } + + function _getPositionCaveats(uint256 _position) internal view returns (Caveat[] memory caveats_) { + caveats_ = new Caveat[](1); + caveats_[0] = + Caveat({ args: hex"", enforcer: address(delegationChainEnforcer), terms: abi.encodePacked(uint256(_position)) }); + } + + function _getChainIntegrityCaveats() internal view returns (Caveat[] memory caveats_) { + caveats_ = new Caveat[](3); + // Check target is the enforcer + caveats_[0] = Caveat({ + args: hex"", + enforcer: address(allowedTargetsEnforcer), + terms: abi.encodePacked(address(delegationChainEnforcer)) + }); + // Check value is 0 + caveats_[1] = Caveat({ args: hex"", enforcer: address(valueLteEnforcer), terms: abi.encodePacked(uint256(0)) }); + // Check method is post() + caveats_[2] = Caveat({ + args: hex"", + enforcer: address(allowedMethodsEnforcer), + terms: abi.encodePacked(DelegationChainEnforcer.post.selector) + }); + } +} From 96f4a6a3e0899db11a28d537d9d65feb9ccbbb84 Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Wed, 7 May 2025 08:39:12 -0600 Subject: [PATCH 2/7] fix: broken test --- src/DelegationManager.sol | 1 + src/enforcers/DelegationChainEnforcer.sol | 58 ++++++++------------ test/enforcers/DelegationChainEnforcer.t.sol | 12 ++-- 3 files changed, 29 insertions(+), 42 deletions(-) diff --git a/src/DelegationManager.sol b/src/DelegationManager.sol index 7d89074f..f17883f6 100644 --- a/src/DelegationManager.sol +++ b/src/DelegationManager.sol @@ -219,6 +219,7 @@ contract DelegationManager is IDelegationManager, Ownable2Step, Pausable, EIP712 Caveat[] memory caveats_ = batchDelegations_[batchIndex_][delegationsIndex_].caveats; for (uint256 caveatsIndex_; caveatsIndex_ < caveats_.length; ++caveatsIndex_) { ICaveatEnforcer enforcer_ = ICaveatEnforcer(caveats_[caveatsIndex_].enforcer); + console2.log("Calling beforeAllHook", address(enforcer_)); enforcer_.beforeAllHook( caveats_[caveatsIndex_].terms, caveats_[caveatsIndex_].args, diff --git a/src/enforcers/DelegationChainEnforcer.sol b/src/enforcers/DelegationChainEnforcer.sol index f9114b1a..8886a3be 100644 --- a/src/enforcers/DelegationChainEnforcer.sol +++ b/src/enforcers/DelegationChainEnforcer.sol @@ -15,6 +15,8 @@ import "forge-std/Test.sol"; * @title DelegationChainEnforcer * @dev This contract enforces the allowed methods a delegate may call. * @dev This enforcer operates only in single execution call type and with default execution mode. + * @dev Combine with other enforcers to validate the target, method, value, etc, in the root delegation, this avoids + * a redundant validation in this enforcer on each level. */ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { using ExecutionLib for bytes; @@ -69,24 +71,17 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { // multiple times to get paid multiple times, or an address they control? bytes32 referralChainHash_ = keccak256(abi.encode(_delegators)); - console2.log("1referralChainHash_:"); - console2.logBytes32(referralChainHash_); - console2.log("msg.sender:", msg.sender); - // TODO: msg.sender here might be dangerous, the owner could do something unwanted, centralization risk - // what can someone with a delegation do? + // TODO: centralization risk what can someone with a delegation do, we need to trust address[] storage referrals_ = referrals[address(delegationManager)][referralChainHash_]; require(referrals_.length == 0, "DelegationChainEnforcer:referral-chain-already-posted"); // Push up to the last 5 delegators in reverse order uint256 startIndex_ = delegatorsLength_ < 5 ? 0 : delegatorsLength_ - 5; for (uint256 i = delegatorsLength_; i > startIndex_; i--) { - console2.log("it is pushing", _delegators[i - 1]); referrals_.push(_delegators[i - 1]); } - console2.log("Here referrals_.length:", referrals_.length); - emit ReferralChainPosted(msg.sender, referralChainHash_); } @@ -98,7 +93,6 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { // Upon successful validation of the entire delegation chain, the transaction executes the attestation call on the // DelegationChainEnforcer contract. This call formally posts the entire delegation chain addresses on-chain, creating a // permanent record of the referral event. - function beforeHook( bytes calldata _terms, bytes calldata, @@ -120,12 +114,7 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { function _validatePosition(bytes calldata _executionCallData, bytes calldata _terms, address _delegator) internal pure { (,, bytes calldata callData_) = _executionCallData.decodeSingle(); - // TODO: this is better in separate enforcers, so it doesn't repeat on each level - // (address target_, uint256 value_, bytes calldata callData_) = _executionCallData.decodeSingle(); - // require(bytes4(callData_[0:4]) == this.post.selector, "DelegationChainEnforcer:invalid-method"); - // require(value_ == 0, "DelegationChainEnforcer:invalid-value"); - // require(target_ == address(this), "DelegationChainEnforcer:invalid-target"); - + // Target, value, method, must be validated by other enforcers, on root the delegation. (address[] memory delegators_) = abi.decode(callData_[4:], (address[])); // Restriction for gas costs. @@ -152,19 +141,18 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { public override { - // console log the args - console2.log("args:"); - console2.logBytes(_args); - console2.log("Checkpoint after hook"); console2.log("_delegator:", _delegator); console2.log("_redeemer:", _redeemer); - //TODO: Could i use the post params hash instead and a new mapping? - bytes32 referralChainHash_ = keccak256(abi.encode(_executionCallData)); + bytes32 referralChainHash_ = _getReferralChainHash(_executionCallData); + // Stops afterHook execution if the referral hash has already been redeemed + // TODO: add a test to check this mapping if (redeemed[msg.sender][referralChainHash_]) return; - (address[] memory referrals_, uint256 referralLength_) = _getReferrals(_executionCallData); + address[] memory referrals_ = referrals[msg.sender][referralChainHash_]; + + // (address[] memory referrals_, uint256 referralLength_) = _getReferralsAndValidateRedemption(referralChainHash_); // require(referralLength_ > 2, "DelegationChainEnforcer:invalid-referrals-length"); // TODO: make the token constant if possible @@ -176,21 +164,24 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { ModeCode[] memory encodedModes_ = new ModeCode[](allowanceLength_); console2.log("allowanceDelegations_.length:", allowanceDelegations_.length); - console2.log("referralLength_:", referralLength_); + // console2.log("referralLength_:", referralLength_); - require(referralLength_ == allowanceDelegations_.length, "DelegationChainEnforcer:invalid-delegations-length"); + require(referrals_.length == allowanceDelegations_.length, "DelegationChainEnforcer:invalid-delegations-length"); for (uint256 i = 0; i < allowanceLength_; ++i) { permissionContexts_[i] = abi.encode(allowanceDelegations_[i]); executionCallDatas_[i] = ExecutionLib.encodeSingle(token_, 0, abi.encodeCall(IERC20.transfer, (referrals_[i], prizeAmounts[i]))); encodedModes_[i] = ModeLib.encodeSimpleSingle(); + console2.log("prizeAmounts[i]:", prizeAmounts[i]); } uint256[] memory balanceBefore_ = _getBalances(referrals_, token_); + console2.log("ABOUT TO REDEEM INSIDE AFTERHOOK"); // Attempt to redeem the delegation and make the payment delegationManager.redeemDelegations(permissionContexts_, encodedModes_, executionCallDatas_); + console2.log("AFTER REDEMPTION INSIDE AFTERHOOK"); //TODO: If this fails, for memory, then try to pass the redeemDelegations() as callback below. _validateTransfer(referrals_, token_, balanceBefore_); @@ -241,26 +232,23 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { } } - function _getReferrals(bytes calldata _executionCallData) + function _getReferrals(bytes32 _referralChainHash) internal view returns (address[] memory referrals_, uint256 referralLength_) { - (,, bytes calldata callData_) = _executionCallData.decodeSingle(); - - (address[] memory delegators_) = abi.decode(callData_[4:], (address[])); - console2.log("delegators_.length:", delegators_.length); - - bytes32 referralChainHash_ = keccak256(abi.encode(delegators_)); - console2.log("2referralChainHash_:"); - console2.logBytes32(referralChainHash_); - - referrals_ = referrals[msg.sender][referralChainHash_]; + referrals_ = referrals[msg.sender][_referralChainHash]; referralLength_ = referrals_.length; console2.log("msg.sender:", msg.sender); console2.log("referralLength_:", referralLength_); } + function _getReferralChainHash(bytes calldata _executionCallData) internal view returns (bytes32 referralChainHash_) { + (,, bytes calldata callData_) = _executionCallData.decodeSingle(); + (address[] memory delegators_) = abi.decode(callData_[4:], (address[])); + referralChainHash_ = keccak256(abi.encode(delegators_)); + } + function _validateAndDecodeArgs( bytes calldata _args, bytes32 _delegationHash, diff --git a/test/enforcers/DelegationChainEnforcer.t.sol b/test/enforcers/DelegationChainEnforcer.t.sol index a1239a31..bd659f24 100644 --- a/test/enforcers/DelegationChainEnforcer.t.sol +++ b/test/enforcers/DelegationChainEnforcer.t.sol @@ -18,7 +18,6 @@ import { AllowedMethodsEnforcer } from "../../src/enforcers/AllowedMethodsEnforc import { ERC20TransferAmountEnforcer } from "../../src/enforcers/ERC20TransferAmountEnforcer.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { BasicERC20 } from "../utils/BasicERC20.t.sol"; -import "forge-std/Test.sol"; contract DelegationChainEnforcerTest is BaseTest { ////////////////////////////// Setup ////////////////////////////// @@ -68,6 +67,7 @@ contract DelegationChainEnforcerTest is BaseTest { allowedTargetsEnforcer = new AllowedTargetsEnforcer(); valueLteEnforcer = new ValueLteEnforcer(); allowedMethodsEnforcer = new AllowedMethodsEnforcer(); + erc20TransferAmountEnforcer = new ERC20TransferAmountEnforcer(); delegationChainEnforcer = new DelegationChainEnforcer( address(chainIntegrity.deleGator), IDelegationManager(address(delegationManager)), @@ -141,9 +141,6 @@ contract DelegationChainEnforcerTest is BaseTest { uint256[] memory balancesBefore_ = _getBalances(delegators); - // console2.log("delegations_[1].caveat.args"); - // console2.logBytes(delegations_[1].caveats[0].args); - // Execute the delegation chain through ICA invokeDelegation_UserOp(ICA, delegations_, _getExecution()); @@ -173,14 +170,15 @@ contract DelegationChainEnforcerTest is BaseTest { caveats_[1] = Caveat({ args: hex"", enforcer: address(erc20TransferAmountEnforcer), - terms: abi.encode(token, prizeLevels[i]) // Use prize level for this delegator - }); + terms: abi.encodePacked(address(token), prizeLevels[i]) + }); + delegations_[i][0] = Delegation({ delegate: address(delegationChainEnforcer), delegator: address(treasury.deleGator), authority: ROOT_AUTHORITY, caveats: caveats_, - salt: 0, + salt: i, signature: hex"" }); From c369b7cc1f9bef1267e69660999d3d4c176c2b8c Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Thu, 8 May 2025 18:16:32 -0600 Subject: [PATCH 3/7] changed visibility of function to pure --- src/enforcers/DelegationChainEnforcer.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/enforcers/DelegationChainEnforcer.sol b/src/enforcers/DelegationChainEnforcer.sol index 8886a3be..f29730e1 100644 --- a/src/enforcers/DelegationChainEnforcer.sol +++ b/src/enforcers/DelegationChainEnforcer.sol @@ -243,7 +243,7 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { console2.log("referralLength_:", referralLength_); } - function _getReferralChainHash(bytes calldata _executionCallData) internal view returns (bytes32 referralChainHash_) { + function _getReferralChainHash(bytes calldata _executionCallData) internal pure returns (bytes32 referralChainHash_) { (,, bytes calldata callData_) = _executionCallData.decodeSingle(); (address[] memory delegators_) = abi.decode(callData_[4:], (address[])); referralChainHash_ = keccak256(abi.encode(delegators_)); @@ -259,7 +259,7 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { returns (Delegation[][] memory allowanceDelegations_, uint256 allowanceLength_, address token_) { // TODO: make the token constant if possible - // TODO: we could assume a single direct delegation, instead of an array of delegations + // TODO: we could assume a single direct delegation with the total amount, instead of an array of delegations (allowanceDelegations_, token_) = abi.decode(_args, (Delegation[][], address)); allowanceLength_ = allowanceDelegations_.length; require(allowanceLength_ > 0, "DelegationChainEnforcer:invalid-allowance-delegations-length"); From cea1f966401acfcc36ff8d8da2c4212af85bb80d Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Fri, 9 May 2025 17:53:23 -0600 Subject: [PATCH 4/7] test: added redeemer enforcer --- test/enforcers/DelegationChainEnforcer.t.sol | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/enforcers/DelegationChainEnforcer.t.sol b/test/enforcers/DelegationChainEnforcer.t.sol index bd659f24..82b782ac 100644 --- a/test/enforcers/DelegationChainEnforcer.t.sol +++ b/test/enforcers/DelegationChainEnforcer.t.sol @@ -16,6 +16,7 @@ import { AllowedTargetsEnforcer } from "../../src/enforcers/AllowedTargetsEnforc import { ValueLteEnforcer } from "../../src/enforcers/ValueLteEnforcer.sol"; import { AllowedMethodsEnforcer } from "../../src/enforcers/AllowedMethodsEnforcer.sol"; import { ERC20TransferAmountEnforcer } from "../../src/enforcers/ERC20TransferAmountEnforcer.sol"; +import { RedeemerEnforcer } from "../../src/enforcers/RedeemerEnforcer.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { BasicERC20 } from "../utils/BasicERC20.t.sol"; @@ -35,6 +36,7 @@ contract DelegationChainEnforcerTest is BaseTest { ValueLteEnforcer public valueLteEnforcer; AllowedMethodsEnforcer public allowedMethodsEnforcer; ERC20TransferAmountEnforcer public erc20TransferAmountEnforcer; + RedeemerEnforcer public redeemerEnforcer; address[] public delegators; // Prize levels for the delegation chain rewards @@ -68,6 +70,7 @@ contract DelegationChainEnforcerTest is BaseTest { valueLteEnforcer = new ValueLteEnforcer(); allowedMethodsEnforcer = new AllowedMethodsEnforcer(); erc20TransferAmountEnforcer = new ERC20TransferAmountEnforcer(); + redeemerEnforcer = new RedeemerEnforcer(); delegationChainEnforcer = new DelegationChainEnforcer( address(chainIntegrity.deleGator), IDelegationManager(address(delegationManager)), @@ -217,7 +220,7 @@ contract DelegationChainEnforcerTest is BaseTest { } function _getChainIntegrityCaveats() internal view returns (Caveat[] memory caveats_) { - caveats_ = new Caveat[](3); + caveats_ = new Caveat[](4); // Check target is the enforcer caveats_[0] = Caveat({ args: hex"", @@ -232,5 +235,7 @@ contract DelegationChainEnforcerTest is BaseTest { enforcer: address(allowedMethodsEnforcer), terms: abi.encodePacked(DelegationChainEnforcer.post.selector) }); + // Check redeemer is ICA + caveats_[3] = Caveat({ args: hex"", enforcer: address(redeemerEnforcer), terms: abi.encodePacked(address(ICA.deleGator)) }); } } From 86afbf3598f46c136ef732b8109d08692105289f Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Wed, 14 May 2025 09:11:09 -0600 Subject: [PATCH 5/7] docs: improved natspec --- src/enforcers/DelegationChainEnforcer.sol | 134 +++++++++++++++++++--- 1 file changed, 115 insertions(+), 19 deletions(-) diff --git a/src/enforcers/DelegationChainEnforcer.sol b/src/enforcers/DelegationChainEnforcer.sol index f29730e1..44ba8dcf 100644 --- a/src/enforcers/DelegationChainEnforcer.sol +++ b/src/enforcers/DelegationChainEnforcer.sol @@ -13,33 +13,64 @@ import "forge-std/Test.sol"; /** * @title DelegationChainEnforcer - * @dev This contract enforces the allowed methods a delegate may call. - * @dev This enforcer operates only in single execution call type and with default execution mode. - * @dev Combine with other enforcers to validate the target, method, value, etc, in the root delegation, this avoids - * a redundant validation in this enforcer on each level. + * @notice Enforces referral-chain payments using ERC20 allowance delegations. After redemption, + * the contract receives ERC20 allowance delegations, redeems them, and distributes prizes. + * To set up a chain, call `post` with an array of up to 20 referral addresses. The last 5 + * in the array will be paid according to configured prize levels. A hash prevents replays. + * Combine with a RedeemerEnforcer so that the Intermediary Chain Account (ICA) can + * validate any off-chain requirements (e.g., KYC, step-completions) before permitting + * redemption. The ICA acts at each level as an intermediary, enabling delegations to + * unknown addresses with proper enforcers and terms to prevent errors. + * + * @dev This enforcer works in a single execution call with default execution mode. It relies + * on other enforcers to validate root delegation parameters, e.g the target, method, value, etc, + * avoiding redundant checks on each chain level. Prize amounts are fixed to exactly 5 levels and + * must be set via the owner. Cannot post the same chain twice or redeem more than once per chain hash. */ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { using ExecutionLib for bytes; + /// @dev Maps delegation manager addresses to referral array hashes to recipient addresses mapping(address delegationManager => mapping(bytes32 referralChainHash => address[] recipients)) public referrals; + + /// @dev Maps delegation manager addresses to referral array hashes to redemption status mapping(address delegationManager => mapping(bytes32 referralChainHash => bool redeemed)) public redeemed; /// @dev The Delegation Manager contract to redeem the delegation IDelegationManager public immutable delegationManager; - /// @dev The enforcer used to compare args and terms + /// @dev Enforcer to compare args and terms in allowance caveats address public immutable argsEqualityCheckEnforcer; // TODO: make this constant if possible + /// @dev Array of prize amounts for each position in the referral array uint256[] public prizeAmounts; ////////////////////////////// Events ////////////////////////////// - event ReferralChainPosted(address indexed sender, bytes32 indexed referralChainHash); + /** + * @dev Emitted when a new referral array is posted + * @param sender The address that posted the referral array + * @param referralChainHash The hash of the referral array + */ + event ReferralArrayPosted(address indexed sender, bytes32 indexed referralChainHash); + + /** + * @dev Emitted when prize amounts are set + * @param sender The address that set the prizes + * @param prizeLevels The array of prize amounts + */ event PrizesSet(address indexed sender, uint256[] prizeLevels); + ////////////////////////////// Constructor ////////////////////////////// - // TODO: make the levels an array or struct limited to length 5 + /** + * @dev Initializes the contract with the owner, delegation manager, args equality check enforcer, and prize levels + * @param _owner The address that will be the owner of the contract + * @param _delegationManager The address of the delegation manager contract + * @param _argsEqualityCheckEnforcer The address of the args equality check enforcer + * @param _prizeLevels Array of prize amounts for each position in the referral array + */ constructor( address _owner, IDelegationManager _delegationManager, @@ -53,15 +84,27 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { delegationManager = _delegationManager; argsEqualityCheckEnforcer = _argsEqualityCheckEnforcer; + // TODO: make the levels an array or struct limited to length 5 _setPrizes(_prizeLevels); } ////////////////////////////// External Methods ////////////////////////////// + /** + * @notice Update prize amounts for the last 5 referrers + * @dev Owner-only. Accepts exactly 5 non-zero values. + * @param _prizeLevels Array of 5 prize amounts, for positions 0 through 4 + */ function setPrizes(uint256[] memory _prizeLevels) external onlyOwner { _setPrizes(_prizeLevels); } + /** + * @notice Register a referral array for later prize redemption + * @dev Owner-only. Accepts 2 to 20 addresses. Stores only the last 5 (in reverse order). + * Computes a unique hash to prevent replay or duplicate registration. + * @param _delegators Full referral array addresses (length 2–20), from root to leaf + */ function post(address[] calldata _delegators) external onlyOwner { uint256 delegatorsLength_ = _delegators.length; require(delegatorsLength_ > 1, "DelegationChainEnforcer:invalid-delegators-length"); @@ -82,17 +125,18 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { referrals_.push(_delegators[i - 1]); } - emit ReferralChainPosted(msg.sender, referralChainHash_); + emit ReferralArrayPosted(msg.sender, referralChainHash_); } ////////////////////////////// Public Methods ////////////////////////////// - // The beforeHook validates that the delegation being redeemed correctly matches the delegation chain order (checking the - // delegator address, and the delegate address if present in the terms). - - // Upon successful validation of the entire delegation chain, the transaction executes the attestation call on the - // DelegationChainEnforcer contract. This call formally posts the entire delegation chain addresses on-chain, creating a - // permanent record of the referral event. + /** + * @dev Validates that the delegation being redeemed correctly matches the delegation chain order + * @param _terms 32 bytes encoded with the delegator's expected position in the referral array + * @param _mode The mode of execution (single call type and default execution mode) + * @param _executionCallData Calldata containing encoded referral array + * @param _delegator The address of the delegator + */ function beforeHook( bytes calldata _terms, bytes calldata, @@ -111,6 +155,12 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { _validatePosition(_executionCallData, _terms, _delegator); } + /** + * @dev Validates the position of a delegator in the referral array + * @param _executionCallData Calldata containing encoded referral array + * @param _terms 32 bytes encoded with the delegator's expected position in the referral array + * @param _delegator The address of the delegator + */ function _validatePosition(bytes calldata _executionCallData, bytes calldata _terms, address _delegator) internal pure { (,, bytes calldata callData_) = _executionCallData.decodeSingle(); @@ -127,8 +177,18 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { require(delegators_[expectedPosition_] == _delegator, "DelegationChainEnforcer:invalid-delegator-or-position"); } - // This only runs the entire function if the delegation chain has not been redeemed yet. - // The redeemer fills the args on the root delegation, and it will only run for that delegation. + /** + * @dev Executes after the delegation is redeemed, distributing prizes to participants + * @dev This hook executes the payment to the recipients only once for the same referral array hash, after the + * first run it is skipped. + * @dev Decodes allowance delegations, checks caveat enforcer, builds transfer calls, redeems via + * delegationManager, verifies balances, then marks redeemed. + * @param _args ABI-encoded (Delegation[][] allowanceDelegations, address token) + * @param _executionCallData Calldata containing encoded referral array + * @param _delegationHash The hash of the delegation + * @param _delegator The address of the delegator + * @param _redeemer The address of the redeemer + */ function afterHook( bytes calldata, bytes calldata _args, @@ -190,9 +250,9 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { } /** - * @notice Decodes the terms used in this CaveatEnforcer. - * @param _terms encoded data that is used during the execution hooks. - * @return expectedPosition_ The position of the expected delegator in the delegation chain. + * @dev Decodes the terms used in this CaveatEnforcer + * @param _terms 32 bytes encoded with the delegator's expected position in the referral array + * @return expectedPosition_ The position of the expected delegator in the delegation chain */ function getTermsInfo(bytes calldata _terms) public pure returns (uint256 expectedPosition_) { require(_terms.length == 32, "DelegationChainEnforcer:invalid-terms-length"); @@ -201,6 +261,10 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { ////////////////////////////// Internal Methods ////////////////////////////// + /** + * @dev Sets the prize amounts for each position in the referral array + * @param _prizeLevels Array of prize amounts for each position in the referral array + */ function _setPrizes(uint256[] memory _prizeLevels) internal { uint256 prizeLevelsLength_ = _prizeLevels.length; require(prizeLevelsLength_ == 5, "DelegationChainEnforcer:invalid-prize-levels-length"); @@ -216,6 +280,12 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { emit PrizesSet(msg.sender, _prizeLevels); } + /** + * @dev Gets the balances of recipients for a specific token + * @param _recipients Array of recipient addresses + * @param _token The token address + * @return balances_ Array of balances for each recipient + */ function _getBalances(address[] memory _recipients, address _token) internal view returns (uint256[] memory balances_) { uint256 recipientsLength_ = _recipients.length; balances_ = new uint256[](recipientsLength_); @@ -224,6 +294,12 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { } } + /** + * @dev Validates that transfers were successful by checking recipient balances + * @param _recipients Array of recipient addresses + * @param _token The token address + * @param balanceBefore_ Array of balances before the transfer + */ function _validateTransfer(address[] memory _recipients, address _token, uint256[] memory balanceBefore_) internal view { // TODO: prizeAmounts read twice from storage uint256[] memory balances_ = _getBalances(_recipients, _token); @@ -232,6 +308,12 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { } } + /** + * @dev Gets the referrals and length for a specific referral array hash + * @param _referralChainHash The hash of the referral array + * @return referrals_ Array of referral addresses + * @return referralLength_ The length of the referral array + */ function _getReferrals(bytes32 _referralChainHash) internal view @@ -243,12 +325,26 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { console2.log("referralLength_:", referralLength_); } + /** + * @dev Gets the referral array hash from execution call data + * @param _executionCallData Calldata containing encoded referral array + * @return referralChainHash_ The hash of the referral array + */ function _getReferralChainHash(bytes calldata _executionCallData) internal pure returns (bytes32 referralChainHash_) { (,, bytes calldata callData_) = _executionCallData.decodeSingle(); (address[] memory delegators_) = abi.decode(callData_[4:], (address[])); referralChainHash_ = keccak256(abi.encode(delegators_)); } + /** + * @dev Validates and decodes the arguments for the delegation + * @param _args Encoded allowance delegations and token address + * @param _delegationHash The hash of the delegation + * @param _redeemer The address of the redeemer + * @return allowanceDelegations_ Array of allowance delegations + * @return allowanceLength_ The length of the allowance delegations + * @return token_ The token address + */ function _validateAndDecodeArgs( bytes calldata _args, bytes32 _delegationHash, From 3ebab8f5f05f223ac765995058372a7948938275 Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Fri, 16 May 2025 08:34:46 -0600 Subject: [PATCH 6/7] test: cover 100% delegation chain enforcer --- src/enforcers/DelegationChainEnforcer.sol | 202 +++--- test/enforcers/DelegationChainEnforcer.t.sol | 689 ++++++++++++++++++- 2 files changed, 789 insertions(+), 102 deletions(-) diff --git a/src/enforcers/DelegationChainEnforcer.sol b/src/enforcers/DelegationChainEnforcer.sol index 44ba8dcf..2612dd09 100644 --- a/src/enforcers/DelegationChainEnforcer.sol +++ b/src/enforcers/DelegationChainEnforcer.sol @@ -15,8 +15,9 @@ import "forge-std/Test.sol"; * @title DelegationChainEnforcer * @notice Enforces referral-chain payments using ERC20 allowance delegations. After redemption, * the contract receives ERC20 allowance delegations, redeems them, and distributes prizes. - * To set up a chain, call `post` with an array of up to 20 referral addresses. The last 5 - * in the array will be paid according to configured prize levels. A hash prevents replays. + * To set up a chain, call `post` with an array of up to MAX_REFERRAL_DEPTH referral addresses. + * The last addresses according to the maxPrizePayments will be paid their configured prize levels. + * A hash of the addresses prevents replays. * Combine with a RedeemerEnforcer so that the Intermediary Chain Account (ICA) can * validate any off-chain requirements (e.g., KYC, step-completions) before permitting * redemption. The ICA acts at each level as an intermediary, enabling delegations to @@ -24,17 +25,17 @@ import "forge-std/Test.sol"; * * @dev This enforcer works in a single execution call with default execution mode. It relies * on other enforcers to validate root delegation parameters, e.g the target, method, value, etc, - * avoiding redundant checks on each chain level. Prize amounts are fixed to exactly 5 levels and + * avoiding redundant checks on each chain level. Prize amounts are fixed to exactly maxPrizePayments levels and * must be set via the owner. Cannot post the same chain twice or redeem more than once per chain hash. */ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { using ExecutionLib for bytes; - /// @dev Maps delegation manager addresses to referral array hashes to recipient addresses - mapping(address delegationManager => mapping(bytes32 referralChainHash => address[] recipients)) public referrals; + /// @dev Maximum number of referrers in the referral chain + uint256 public constant MAX_REFERRAL_DEPTH = 20; - /// @dev Maps delegation manager addresses to referral array hashes to redemption status - mapping(address delegationManager => mapping(bytes32 referralChainHash => bool redeemed)) public redeemed; + /// @dev Maximum number of prizes that will be paid in the referral chain + uint256 public immutable maxPrizePayments; /// @dev The Delegation Manager contract to redeem the delegation IDelegationManager public immutable delegationManager; @@ -42,7 +43,15 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { /// @dev Enforcer to compare args and terms in allowance caveats address public immutable argsEqualityCheckEnforcer; - // TODO: make this constant if possible + /// @dev The token address to be used for the prize payments + address public immutable prizeToken; + + /// @dev Maps delegation manager addresses to referral array hashes to recipient addresses + mapping(address delegationManager => mapping(bytes32 referralChainHash => address[] recipients)) public referrals; + + /// @dev Maps delegation manager addresses to referral array hashes to redemption status + mapping(address delegationManager => mapping(bytes32 referralChainHash => bool redeemed)) public redeemed; + /// @dev Array of prize amounts for each position in the referral array uint256[] public prizeAmounts; @@ -69,45 +78,54 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { * @param _owner The address that will be the owner of the contract * @param _delegationManager The address of the delegation manager contract * @param _argsEqualityCheckEnforcer The address of the args equality check enforcer - * @param _prizeLevels Array of prize amounts for each position in the referral array + * @param _prizeToken The address of the token to be used for the prize payments + * @param _prizeAmounts Array of prize amounts for each position in the referral array, from the first to the last position + * The maxPrizePayments is set to the length of this prizeAmounts array and it cannot be changed. */ constructor( address _owner, IDelegationManager _delegationManager, address _argsEqualityCheckEnforcer, - uint256[] memory _prizeLevels + address _prizeToken, + uint256[] memory _prizeAmounts ) Ownable(_owner) { require(_delegationManager != IDelegationManager(address(0)), "DelegationChainEnforcer:invalid-delegationManager"); require(_argsEqualityCheckEnforcer != address(0), "DelegationChainEnforcer:invalid-argsEqualityCheckEnforcer"); + require(_prizeToken != address(0), "DelegationChainEnforcer:invalid-prizeToken"); delegationManager = _delegationManager; argsEqualityCheckEnforcer = _argsEqualityCheckEnforcer; - // TODO: make the levels an array or struct limited to length 5 - _setPrizes(_prizeLevels); + prizeToken = _prizeToken; + maxPrizePayments = _prizeAmounts.length; + require(maxPrizePayments > 1, "DelegationChainEnforcer:invalid-max-prize-payments"); + _setPrizes(_prizeAmounts); } ////////////////////////////// External Methods ////////////////////////////// /** - * @notice Update prize amounts for the last 5 referrers - * @dev Owner-only. Accepts exactly 5 non-zero values. - * @param _prizeLevels Array of 5 prize amounts, for positions 0 through 4 + * @notice Update prize amounts for the last maxPrizePayments referrers + * @dev Owner-only. Accepts exactly maxPrizePayments non-zero values. + * @param _prizeAmounts Array of maxPrizePayments prize amounts, from the first to the last position */ - function setPrizes(uint256[] memory _prizeLevels) external onlyOwner { - _setPrizes(_prizeLevels); + function setPrizes(uint256[] memory _prizeAmounts) external onlyOwner { + _setPrizes(_prizeAmounts); } /** * @notice Register a referral array for later prize redemption - * @dev Owner-only. Accepts 2 to 20 addresses. Stores only the last 5 (in reverse order). - * Computes a unique hash to prevent replay or duplicate registration. - * @param _delegators Full referral array addresses (length 2–20), from root to leaf + * @dev Owner-only. Stores only the last maxPrizePayments amount of addresses that will be paid their + * configured prize levels in the afterHook. + * @dev Computes a unique hash to prevent replay or duplicate registration. + * @param _delegators Full referral array addresses (length 2–MAX_REFERRAL_DEPTH), from root to leaf */ function post(address[] calldata _delegators) external onlyOwner { uint256 delegatorsLength_ = _delegators.length; - require(delegatorsLength_ > 1, "DelegationChainEnforcer:invalid-delegators-length"); + require( + delegatorsLength_ > 1 && delegatorsLength_ <= MAX_REFERRAL_DEPTH, "DelegationChainEnforcer:invalid-delegators-length" + ); // TODO: make sure someonen can't add himself twice in the same delegation chain // TODO: make sure the intermediary chain is involved otherwise fail, pleople would add themselves to the chain at the end @@ -115,19 +133,33 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { bytes32 referralChainHash_ = keccak256(abi.encode(_delegators)); - // TODO: centralization risk what can someone with a delegation do, we need to trust address[] storage referrals_ = referrals[address(delegationManager)][referralChainHash_]; require(referrals_.length == 0, "DelegationChainEnforcer:referral-chain-already-posted"); - // Push up to the last 5 delegators in reverse order - uint256 startIndex_ = delegatorsLength_ < 5 ? 0 : delegatorsLength_ - 5; - for (uint256 i = delegatorsLength_; i > startIndex_; i--) { - referrals_.push(_delegators[i - 1]); + // // Push up to the last maxPrizePayments delegators in reverse order + // uint256 startIndex_ = delegatorsLength_ < maxPrizePayments ? 0 : delegatorsLength_ - maxPrizePayments; + // for (uint256 i = delegatorsLength_; i > startIndex_; i--) { + // referrals_.push(_delegators[i - 1]); + // } + + // Push up to the last delegators according to the amount of prizes. + uint256 startIndex_ = delegatorsLength_ > maxPrizePayments ? delegatorsLength_ - maxPrizePayments : 0; + for (uint256 i = startIndex_; i < delegatorsLength_; ++i) { + referrals_.push(_delegators[i]); } emit ReferralArrayPosted(msg.sender, referralChainHash_); } + /** + * @notice Get the referrals for a given referral chain hash + * @param _referralChainHash The hash of the referral chain + * @return referrals_ The array of referrals + */ + function getReferrals(bytes32 _referralChainHash) external view returns (address[] memory referrals_) { + return referrals[address(delegationManager)][_referralChainHash]; + } + ////////////////////////////// Public Methods ////////////////////////////// /** @@ -155,35 +187,13 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { _validatePosition(_executionCallData, _terms, _delegator); } - /** - * @dev Validates the position of a delegator in the referral array - * @param _executionCallData Calldata containing encoded referral array - * @param _terms 32 bytes encoded with the delegator's expected position in the referral array - * @param _delegator The address of the delegator - */ - function _validatePosition(bytes calldata _executionCallData, bytes calldata _terms, address _delegator) internal pure { - (,, bytes calldata callData_) = _executionCallData.decodeSingle(); - - // Target, value, method, must be validated by other enforcers, on root the delegation. - (address[] memory delegators_) = abi.decode(callData_[4:], (address[])); - - // Restriction for gas costs. - require(delegators_.length <= 20, "DelegationChainEnforcer:invalid-delegators-length"); - - uint256 expectedPosition_ = getTermsInfo(_terms); - - require(delegators_.length > expectedPosition_, "DelegationChainEnforcer:invalid-expected-position"); - - require(delegators_[expectedPosition_] == _delegator, "DelegationChainEnforcer:invalid-delegator-or-position"); - } - /** * @dev Executes after the delegation is redeemed, distributing prizes to participants * @dev This hook executes the payment to the recipients only once for the same referral array hash, after the * first run it is skipped. * @dev Decodes allowance delegations, checks caveat enforcer, builds transfer calls, redeems via * delegationManager, verifies balances, then marks redeemed. - * @param _args ABI-encoded (Delegation[][] allowanceDelegations, address token) + * @param _args ABI-encoded (Delegation[][] allowanceDelegations) * @param _executionCallData Calldata containing encoded referral array * @param _delegationHash The hash of the delegation * @param _delegator The address of the delegator @@ -206,28 +216,23 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { console2.log("_redeemer:", _redeemer); bytes32 referralChainHash_ = _getReferralChainHash(_executionCallData); + // Stops afterHook execution if the referral hash has already been redeemed // TODO: add a test to check this mapping if (redeemed[msg.sender][referralChainHash_]) return; address[] memory referrals_ = referrals[msg.sender][referralChainHash_]; - // (address[] memory referrals_, uint256 referralLength_) = _getReferralsAndValidateRedemption(referralChainHash_); - // require(referralLength_ > 2, "DelegationChainEnforcer:invalid-referrals-length"); - - // TODO: make the token constant if possible - (Delegation[][] memory allowanceDelegations_, uint256 allowanceLength_, address token_) = + (Delegation[][] memory allowanceDelegations_, uint256 allowanceLength_) = _validateAndDecodeArgs(_args, _delegationHash, _redeemer); bytes[] memory permissionContexts_ = new bytes[](allowanceLength_); bytes[] memory executionCallDatas_ = new bytes[](allowanceLength_); ModeCode[] memory encodedModes_ = new ModeCode[](allowanceLength_); - console2.log("allowanceDelegations_.length:", allowanceDelegations_.length); - // console2.log("referralLength_:", referralLength_); - require(referrals_.length == allowanceDelegations_.length, "DelegationChainEnforcer:invalid-delegations-length"); + address token_ = prizeToken; for (uint256 i = 0; i < allowanceLength_; ++i) { permissionContexts_[i] = abi.encode(allowanceDelegations_[i]); executionCallDatas_[i] = @@ -263,21 +268,21 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { /** * @dev Sets the prize amounts for each position in the referral array - * @param _prizeLevels Array of prize amounts for each position in the referral array + * @param _prizeAmounts Array of maxPrizePayments prize amounts, sorted from the first to the last position */ - function _setPrizes(uint256[] memory _prizeLevels) internal { - uint256 prizeLevelsLength_ = _prizeLevels.length; - require(prizeLevelsLength_ == 5, "DelegationChainEnforcer:invalid-prize-levels-length"); + function _setPrizes(uint256[] memory _prizeAmounts) internal { + uint256 prizeAmountsLength_ = _prizeAmounts.length; + require(prizeAmountsLength_ == maxPrizePayments, "DelegationChainEnforcer:invalid-prize-amounts-length"); // Clear existing prize amounts delete prizeAmounts; - for (uint256 i = 0; i < prizeLevelsLength_; ++i) { - require(_prizeLevels[i] > 0, "DelegationChainEnforcer:invalid-prize-level"); - prizeAmounts.push(_prizeLevels[i]); + for (uint256 i = 0; i < prizeAmountsLength_; ++i) { + require(_prizeAmounts[i] > 0, "DelegationChainEnforcer:invalid-prize-amount"); + prizeAmounts.push(_prizeAmounts[i]); } - emit PrizesSet(msg.sender, _prizeLevels); + emit PrizesSet(msg.sender, _prizeAmounts); } /** @@ -308,34 +313,6 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { } } - /** - * @dev Gets the referrals and length for a specific referral array hash - * @param _referralChainHash The hash of the referral array - * @return referrals_ Array of referral addresses - * @return referralLength_ The length of the referral array - */ - function _getReferrals(bytes32 _referralChainHash) - internal - view - returns (address[] memory referrals_, uint256 referralLength_) - { - referrals_ = referrals[msg.sender][_referralChainHash]; - referralLength_ = referrals_.length; - console2.log("msg.sender:", msg.sender); - console2.log("referralLength_:", referralLength_); - } - - /** - * @dev Gets the referral array hash from execution call data - * @param _executionCallData Calldata containing encoded referral array - * @return referralChainHash_ The hash of the referral array - */ - function _getReferralChainHash(bytes calldata _executionCallData) internal pure returns (bytes32 referralChainHash_) { - (,, bytes calldata callData_) = _executionCallData.decodeSingle(); - (address[] memory delegators_) = abi.decode(callData_[4:], (address[])); - referralChainHash_ = keccak256(abi.encode(delegators_)); - } - /** * @dev Validates and decodes the arguments for the delegation * @param _args Encoded allowance delegations and token address @@ -343,7 +320,6 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { * @param _redeemer The address of the redeemer * @return allowanceDelegations_ Array of allowance delegations * @return allowanceLength_ The length of the allowance delegations - * @return token_ The token address */ function _validateAndDecodeArgs( bytes calldata _args, @@ -352,11 +328,10 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { ) internal view - returns (Delegation[][] memory allowanceDelegations_, uint256 allowanceLength_, address token_) + returns (Delegation[][] memory allowanceDelegations_, uint256 allowanceLength_) { - // TODO: make the token constant if possible // TODO: we could assume a single direct delegation with the total amount, instead of an array of delegations - (allowanceDelegations_, token_) = abi.decode(_args, (Delegation[][], address)); + (allowanceDelegations_) = abi.decode(_args, (Delegation[][])); allowanceLength_ = allowanceDelegations_.length; require(allowanceLength_ > 0, "DelegationChainEnforcer:invalid-allowance-delegations-length"); @@ -370,4 +345,37 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { allowanceDelegations_[i][0].caveats[0].args = abi.encodePacked(_delegationHash, _redeemer); } } + + /** + * @dev Gets the referral array hash from execution call data + * @param _executionCallData Calldata containing encoded referral array + * @return referralChainHash_ The hash of the referral array + */ + function _getReferralChainHash(bytes calldata _executionCallData) internal pure returns (bytes32 referralChainHash_) { + (,, bytes calldata callData_) = _executionCallData.decodeSingle(); + (address[] memory delegators_) = abi.decode(callData_[4:], (address[])); + referralChainHash_ = keccak256(abi.encode(delegators_)); + } + + /** + * @dev Validates the position of a delegator in the referral array + * @param _executionCallData Calldata containing encoded referral array + * @param _terms 32 bytes encoded with the delegator's expected position in the referral array + * @param _delegator The address of the delegator + */ + function _validatePosition(bytes calldata _executionCallData, bytes calldata _terms, address _delegator) internal pure { + (,, bytes calldata callData_) = _executionCallData.decodeSingle(); + + // Target, value, method, must be validated by other enforcers, on root the delegation. + (address[] memory delegators_) = abi.decode(callData_[4:], (address[])); + + // Restriction for gas costs. + require(delegators_.length <= MAX_REFERRAL_DEPTH, "DelegationChainEnforcer:invalid-delegators-length"); + + uint256 expectedPosition_ = getTermsInfo(_terms); + + require(delegators_.length > expectedPosition_, "DelegationChainEnforcer:invalid-expected-position"); + + require(delegators_[expectedPosition_] == _delegator, "DelegationChainEnforcer:invalid-delegator-or-position"); + } } diff --git a/test/enforcers/DelegationChainEnforcer.t.sol b/test/enforcers/DelegationChainEnforcer.t.sol index 82b782ac..ec14687a 100644 --- a/test/enforcers/DelegationChainEnforcer.t.sol +++ b/test/enforcers/DelegationChainEnforcer.t.sol @@ -2,9 +2,11 @@ pragma solidity 0.8.23; import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { BaseTest } from "../utils/BaseTest.t.sol"; -import { Delegation, Caveat, Execution } from "../../src/utils/Types.sol"; +import { Delegation, Caveat, Execution, ModeCode } from "../../src/utils/Types.sol"; import { Implementation, SignatureType, TestUser } from "../utils/Types.t.sol"; import { EncoderLib } from "../../src/libraries/EncoderLib.sol"; import { DelegationManager } from "../../src/DelegationManager.sol"; @@ -17,8 +19,11 @@ import { ValueLteEnforcer } from "../../src/enforcers/ValueLteEnforcer.sol"; import { AllowedMethodsEnforcer } from "../../src/enforcers/AllowedMethodsEnforcer.sol"; import { ERC20TransferAmountEnforcer } from "../../src/enforcers/ERC20TransferAmountEnforcer.sol"; import { RedeemerEnforcer } from "../../src/enforcers/RedeemerEnforcer.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { BasicERC20 } from "../utils/BasicERC20.t.sol"; +import "forge-std/Test.sol"; + +// Import the event +import { DelegationChainEnforcer as DelegationChainEnforcerEvents } from "../../src/enforcers/DelegationChainEnforcer.sol"; contract DelegationChainEnforcerTest is BaseTest { ////////////////////////////// Setup ////////////////////////////// @@ -71,15 +76,161 @@ contract DelegationChainEnforcerTest is BaseTest { allowedMethodsEnforcer = new AllowedMethodsEnforcer(); erc20TransferAmountEnforcer = new ERC20TransferAmountEnforcer(); redeemerEnforcer = new RedeemerEnforcer(); + + token = new BasicERC20(address(this), "USDC", "USDC", 18); + token.mint(address(treasury.deleGator), 100 ether); + delegationChainEnforcer = new DelegationChainEnforcer( address(chainIntegrity.deleGator), IDelegationManager(address(delegationManager)), address(argsEqualityCheckEnforcer), + address(token), prizeLevels ); + } - token = new BasicERC20(address(this), "USDC", "USDC", 18); - token.mint(address(treasury.deleGator), 100 ether); + ////////////////////////////// Constructor Tests ////////////////////////////// + + /// @notice Tests that constructor reverts when delegation manager address is zero + function test_delegationManagerZero() public { + vm.expectRevert("DelegationChainEnforcer:invalid-delegationManager"); + new DelegationChainEnforcer( + address(chainIntegrity.deleGator), + IDelegationManager(address(0)), + address(argsEqualityCheckEnforcer), + address(token), + prizeLevels + ); + } + + /// @notice Tests that constructor reverts when args equality check enforcer address is zero + function test_argsEqualityCheckEnforcerZero() public { + vm.expectRevert("DelegationChainEnforcer:invalid-argsEqualityCheckEnforcer"); + new DelegationChainEnforcer( + address(chainIntegrity.deleGator), + IDelegationManager(address(delegationManager)), + address(0), + address(token), + prizeLevels + ); + } + + /// @notice Tests that constructor reverts when prize token address is zero + function test_prizeTokenZero() public { + vm.expectRevert("DelegationChainEnforcer:invalid-prizeToken"); + new DelegationChainEnforcer( + address(chainIntegrity.deleGator), + IDelegationManager(address(delegationManager)), + address(argsEqualityCheckEnforcer), + address(0), + prizeLevels + ); + } + + /// @notice Tests that constructor reverts when prize amounts array has only one element + function test_prizeAmountsLengthOne() public { + uint256[] memory singlePrize = new uint256[](1); + singlePrize[0] = 10 ether; + + vm.expectRevert("DelegationChainEnforcer:invalid-max-prize-payments"); + new DelegationChainEnforcer( + address(chainIntegrity.deleGator), + IDelegationManager(address(delegationManager)), + address(argsEqualityCheckEnforcer), + address(token), + singlePrize + ); + } + + /// @notice Tests that constructor reverts when any prize amount in the array is zero + function test_prizeAmountZero() public { + uint256[] memory invalidPrizes = new uint256[](2); + invalidPrizes[0] = 10 ether; + invalidPrizes[1] = 0; + + vm.expectRevert("DelegationChainEnforcer:invalid-prize-amount"); + new DelegationChainEnforcer( + address(chainIntegrity.deleGator), + IDelegationManager(address(delegationManager)), + address(argsEqualityCheckEnforcer), + address(token), + invalidPrizes + ); + } + + ////////////////////////////// setPrizes Tests ////////////////////////////// + + /// @notice Tests that owner can successfully set new prize amounts + function test_setPrizes() public { + uint256[] memory newPrizes = new uint256[](5); + newPrizes[0] = 20 ether; + newPrizes[1] = 15 ether; + newPrizes[2] = 10 ether; + newPrizes[3] = 5 ether; + newPrizes[4] = 2.5 ether; + + vm.prank(address(chainIntegrity.deleGator)); + delegationChainEnforcer.setPrizes(newPrizes); + + // Verify each prize amount was set correctly + for (uint256 i = 0; i < newPrizes.length; i++) { + assertEq(delegationChainEnforcer.prizeAmounts(i), newPrizes[i]); + } + } + + /// @notice Tests that non-owner cannot set prize amounts + function test_setPrizesOnlyOwner() public { + uint256[] memory newPrizes = new uint256[](5); + newPrizes[0] = 20 ether; + newPrizes[1] = 15 ether; + newPrizes[2] = 10 ether; + newPrizes[3] = 5 ether; + newPrizes[4] = 2.5 ether; + + vm.prank(address(users.bob.deleGator)); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(users.bob.deleGator))); + delegationChainEnforcer.setPrizes(newPrizes); + } + + /// @notice Tests that setting prize amounts with wrong length reverts + function test_setPrizesWrongLength() public { + uint256[] memory wrongLengthPrizes = new uint256[](3); // Should be 5 + wrongLengthPrizes[0] = 20 ether; + wrongLengthPrizes[1] = 15 ether; + wrongLengthPrizes[2] = 10 ether; + + vm.prank(address(chainIntegrity.deleGator)); + vm.expectRevert("DelegationChainEnforcer:invalid-prize-amounts-length"); + delegationChainEnforcer.setPrizes(wrongLengthPrizes); + } + + /// @notice Tests that setting prize amounts with zero value reverts + function test_setPrizesZeroAmount() public { + uint256[] memory zeroPrizes = new uint256[](5); + zeroPrizes[0] = 20 ether; + zeroPrizes[1] = 15 ether; + zeroPrizes[2] = 10 ether; + zeroPrizes[3] = 5 ether; + zeroPrizes[4] = 0; // Zero amount + + vm.prank(address(chainIntegrity.deleGator)); + vm.expectRevert("DelegationChainEnforcer:invalid-prize-amount"); + delegationChainEnforcer.setPrizes(zeroPrizes); + } + + /// @notice Tests that setting prize amounts emits PrizesSet event + function test_setPrizesEmitsEvent() public { + uint256[] memory newPrizes = new uint256[](5); + newPrizes[0] = 20 ether; + newPrizes[1] = 15 ether; + newPrizes[2] = 10 ether; + newPrizes[3] = 5 ether; + newPrizes[4] = 2.5 ether; + + vm.prank(address(chainIntegrity.deleGator)); + vm.expectEmit(true, false, false, true); + emit DelegationChainEnforcerEvents.PrizesSet(address(chainIntegrity.deleGator), newPrizes); + delegationChainEnforcer.setPrizes(newPrizes); } // Should create a delegation chain from chainIntegrity -> alice -> ICA -> bob -> ICA @@ -150,6 +301,528 @@ contract DelegationChainEnforcerTest is BaseTest { _validatePayments(balancesBefore_); } + ////////////////////////////// Post Function Tests ////////////////////////////// + + /// @notice Tests that post function works with minimum valid delegators length + function test_postMinimumDelegators() public { + address[] memory delegators_ = new address[](2); + delegators_[0] = address(users.alice.deleGator); + delegators_[1] = address(users.bob.deleGator); + + vm.prank(address(chainIntegrity.deleGator)); + vm.expectEmit(true, true, false, true); + emit DelegationChainEnforcerEvents.ReferralArrayPosted( + address(chainIntegrity.deleGator), keccak256(abi.encode(delegators_)) + ); + delegationChainEnforcer.post(delegators_); + + // Verify the referrals were stored correctly + address[] memory storedReferrals = delegationChainEnforcer.getReferrals(keccak256(abi.encode(delegators_))); + assertEq(storedReferrals.length, 2, "referrals length should be 2"); + assertEq(storedReferrals[0], delegators_[0], "referral[0] should be alice"); + assertEq(storedReferrals[1], delegators_[1], "referral[1] should be bob"); + } + + /// @notice Tests that post function works with maximum valid delegators length + function test_postMaximumDelegators() public { + address[] memory delegators_ = new address[](20); // MAX_REFERRAL_DEPTH + for (uint256 i = 0; i < 20; i++) { + delegators_[i] = address(uint160(i + 1)); // Use different addresses + } + + vm.prank(address(chainIntegrity.deleGator)); + vm.expectEmit(true, true, false, true); + emit DelegationChainEnforcerEvents.ReferralArrayPosted( + address(chainIntegrity.deleGator), keccak256(abi.encode(delegators_)) + ); + delegationChainEnforcer.post(delegators_); + + // Verify the referrals were stored correctly (only last maxPrizePayments) + address[] memory storedReferrals = delegationChainEnforcer.getReferrals(keccak256(abi.encode(delegators_))); + assertEq(storedReferrals.length, 5, "referrals length should be 5"); // maxPrizePayments + uint256 count = 0; + for (uint256 i = delegators_.length - 5; i < delegators_.length; ++i) { + assertEq(storedReferrals[count], delegators_[i], "referral order mismatch"); // Last 5 + count++; + } + } + + /// @notice Tests that post function reverts when called by non-owner + function test_postOnlyOwner() public { + address[] memory delegators_ = new address[](2); + delegators_[0] = address(users.alice.deleGator); + delegators_[1] = address(users.bob.deleGator); + + vm.prank(address(users.bob.deleGator)); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(users.bob.deleGator))); + delegationChainEnforcer.post(delegators_); + } + + /// @notice Tests that post function reverts when delegators length is 1 + function test_postDelegatorsLengthOne() public { + address[] memory delegators_ = new address[](1); + delegators_[0] = address(users.alice.deleGator); + + vm.prank(address(chainIntegrity.deleGator)); + vm.expectRevert("DelegationChainEnforcer:invalid-delegators-length"); + delegationChainEnforcer.post(delegators_); + } + + /// @notice Tests that post function reverts when delegators length exceeds MAX_REFERRAL_DEPTH + function test_postDelegatorsLengthExceedsMax() public { + uint256 maxReferralDepth = delegationChainEnforcer.MAX_REFERRAL_DEPTH(); + address[] memory delegators_ = new address[](maxReferralDepth + 1); // MAX_REFERRAL_DEPTH + 1 + for (uint256 i = 0; i < maxReferralDepth + 1; i++) { + delegators_[i] = address(uint160(i + 1)); + } + + vm.prank(address(chainIntegrity.deleGator)); + vm.expectRevert("DelegationChainEnforcer:invalid-delegators-length"); + delegationChainEnforcer.post(delegators_); + } + + /// @notice Tests that post function reverts when trying to post the same chain twice + function test_postDuplicateChain() public { + address[] memory delegators_ = new address[](2); + delegators_[0] = address(users.alice.deleGator); + delegators_[1] = address(users.bob.deleGator); + + vm.prank(address(chainIntegrity.deleGator)); + delegationChainEnforcer.post(delegators_); + + vm.prank(address(chainIntegrity.deleGator)); + vm.expectRevert("DelegationChainEnforcer:referral-chain-already-posted"); + delegationChainEnforcer.post(delegators_); + } + + /// @notice Tests that post function correctly stores delegators in correct order + function test_postCorrectOrder() public { + address[] memory delegators_ = new address[](3); + delegators_[0] = address(users.alice.deleGator); + delegators_[1] = address(users.bob.deleGator); + delegators_[2] = address(users.carol.deleGator); + + vm.prank(address(chainIntegrity.deleGator)); + delegationChainEnforcer.post(delegators_); + + // Verify the referrals were stored in correct order + address[] memory storedReferrals = delegationChainEnforcer.getReferrals(keccak256(abi.encode(delegators_))); + assertEq(storedReferrals.length, 3, "referrals length should be 3"); + assertEq(storedReferrals[0], delegators_[0], "referral[0] should be alice"); + assertEq(storedReferrals[1], delegators_[1], "referral[1] should be bob"); + assertEq(storedReferrals[2], delegators_[2], "referral[2] should be carol"); + } + + /// @notice Tests that post function correctly handles delegators array with length less than maxPrizePayments + function test_postLessThanMaxPrizePayments() public { + address[] memory delegators_ = new address[](3); // Less than maxPrizePayments (5) + delegators_[0] = address(users.alice.deleGator); + delegators_[1] = address(users.bob.deleGator); + delegators_[2] = address(users.carol.deleGator); + + vm.prank(address(chainIntegrity.deleGator)); + delegationChainEnforcer.post(delegators_); + + // Verify all delegators were stored since length < maxPrizePayments + address[] memory storedReferrals = delegationChainEnforcer.getReferrals(keccak256(abi.encode(delegators_))); + assertEq(storedReferrals.length, 3, "referrals length should be 3"); + assertEq(storedReferrals[0], delegators_[0], "referral[0] should be alice"); + assertEq(storedReferrals[1], delegators_[1], "referral[1] should be bob"); + assertEq(storedReferrals[2], delegators_[2], "referral[2] should be carol"); + } + + /// @notice Tests that afterHook reverts when delegations length doesn't match referrals length + function test_afterHookInvalidDelegationsLength() public { + // First post a valid referral chain + address[] memory delegators_ = new address[](2); + delegators_[0] = address(users.alice.deleGator); + delegators_[1] = address(users.bob.deleGator); + + vm.prank(address(chainIntegrity.deleGator)); + delegationChainEnforcer.post(delegators_); + + // Create execution data for the post function + Execution memory execution_ = Execution({ + target: address(delegationChainEnforcer), + value: 0, + callData: abi.encodeCall(DelegationChainEnforcer.post, (delegators_)) + }); + + // Create a single delegation (should be 2 to match referrals length) + Delegation[][] memory delegations_ = new Delegation[][](1); + delegations_[0] = new Delegation[](1); + + Caveat[] memory caveats_ = new Caveat[](2); + caveats_[0] = Caveat({ + args: hex"", + enforcer: address(argsEqualityCheckEnforcer), + terms: abi.encodePacked(firstReferralDelegationHash, address(ICA.deleGator)) + }); + caveats_[1] = Caveat({ + args: hex"", + enforcer: address(erc20TransferAmountEnforcer), + terms: abi.encodePacked(address(token), prizeLevels[0]) + }); + + delegations_[0][0] = Delegation({ + delegate: address(delegationChainEnforcer), + delegator: address(treasury.deleGator), + authority: ROOT_AUTHORITY, + caveats: caveats_, + salt: 0, + signature: hex"" + }); + + // Try to execute afterHook with mismatched lengths + vm.prank(address(delegationManager)); + vm.expectRevert("DelegationChainEnforcer:invalid-delegations-length"); + delegationChainEnforcer.afterHook( + hex"", + abi.encode(delegations_), + singleDefaultMode, + ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData), + firstReferralDelegationHash, + address(users.alice.deleGator), + address(ICA.deleGator) + ); + } + + /// @notice Tests that afterHook reverts when args enforcer is missing in the first delegation + function test_afterHookInvalidArgsEnforcerInFirstDelegation() public { + // First post a valid referral chain + address[] memory delegators_ = new address[](2); + delegators_[0] = address(users.alice.deleGator); + delegators_[1] = address(users.bob.deleGator); + + vm.prank(address(chainIntegrity.deleGator)); + delegationChainEnforcer.post(delegators_); + + // Create execution data for the post function + Execution memory execution_ = Execution({ + target: address(delegationChainEnforcer), + value: 0, + callData: abi.encodeCall(DelegationChainEnforcer.post, (delegators_)) + }); + + Delegation[][] memory delegations_ = new Delegation[][](2); + delegations_[0] = new Delegation[](1); + delegations_[1] = new Delegation[](1); + + Caveat[] memory caveats_ = new Caveat[](2); + // The args enforcer is missing in the first caveat + caveats_[0] = Caveat({ + args: hex"", + enforcer: address(erc20TransferAmountEnforcer), + terms: abi.encodePacked(address(token), prizeLevels[0]) + }); + // The args enforcer required to be in the first caveat + caveats_[1] = Caveat({ + args: hex"", + enforcer: address(argsEqualityCheckEnforcer), + terms: abi.encodePacked(firstReferralDelegationHash, address(ICA.deleGator)) + }); + + delegations_[0][0] = Delegation({ + delegate: address(delegationChainEnforcer), + delegator: address(treasury.deleGator), + authority: ROOT_AUTHORITY, + caveats: caveats_, + salt: 0, + signature: hex"" + }); + + // Try to execute afterHook with missing args enforcer + vm.prank(address(delegationManager)); + vm.expectRevert("DelegationChainEnforcer:missing-argsEqualityCheckEnforcer"); + delegationChainEnforcer.afterHook( + hex"", + abi.encode(delegations_), + singleDefaultMode, + ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData), + firstReferralDelegationHash, + address(users.alice.deleGator), + address(ICA.deleGator) + ); + } + + /// @notice Tests that afterHook reverts when empty caveats + function test_afterHookInvalidEmptyCaveats() public { + // First post a valid referral chain + address[] memory delegators_ = new address[](2); + delegators_[0] = address(users.alice.deleGator); + delegators_[1] = address(users.bob.deleGator); + + vm.prank(address(chainIntegrity.deleGator)); + delegationChainEnforcer.post(delegators_); + + // Create execution data for the post function + Execution memory execution_ = Execution({ + target: address(delegationChainEnforcer), + value: 0, + callData: abi.encodeCall(DelegationChainEnforcer.post, (delegators_)) + }); + + Delegation[][] memory delegations_ = new Delegation[][](2); + delegations_[0] = new Delegation[](1); + delegations_[1] = new Delegation[](1); + + // Delegation with empty caveats means that the args enforcer is missing, it reverts. + delegations_[0][0] = Delegation({ + delegate: address(delegationChainEnforcer), + delegator: address(treasury.deleGator), + authority: ROOT_AUTHORITY, + caveats: new Caveat[](0), + salt: 0, + signature: hex"" + }); + + // Try to execute afterHook with missing args enforcer + vm.prank(address(delegationManager)); + vm.expectRevert("DelegationChainEnforcer:missing-argsEqualityCheckEnforcer"); + delegationChainEnforcer.afterHook( + hex"", + abi.encode(delegations_), + singleDefaultMode, + ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData), + firstReferralDelegationHash, + address(users.alice.deleGator), + address(ICA.deleGator) + ); + } + + /// @notice Tests that afterHook reverts when payment transfer fails + function test_afterHookPaymentNotReceived() public { + DelegationManagerEmptyMock delegationManagerEmptyMock_ = new DelegationManagerEmptyMock(); + + delegationChainEnforcer = new DelegationChainEnforcer( + address(chainIntegrity.deleGator), + IDelegationManager(address(delegationManagerEmptyMock_)), + address(argsEqualityCheckEnforcer), + address(token), + prizeLevels + ); + + // First post a valid referral chain + address[] memory delegators_ = new address[](2); + delegators_[0] = address(users.alice.deleGator); + delegators_[1] = address(users.bob.deleGator); + + vm.prank(address(chainIntegrity.deleGator)); + delegationChainEnforcer.post(delegators_); + bytes32 referralChainHash_ = keccak256(abi.encode(delegators_)); + + // Create execution data for the post function + Execution memory execution_ = Execution({ + target: address(delegationChainEnforcer), + value: 0, + callData: abi.encodeCall(DelegationChainEnforcer.post, (delegators_)) + }); + + // Create delegations with valid structure but insufficient token balance + Delegation[][] memory delegations_ = new Delegation[][](2); + delegations_[0] = new Delegation[](1); + delegations_[1] = new Delegation[](1); + + Caveat[] memory caveats_ = new Caveat[](2); + caveats_[0] = Caveat({ + args: hex"", + enforcer: address(argsEqualityCheckEnforcer), + terms: abi.encodePacked(firstReferralDelegationHash, address(ICA.deleGator)) + }); + caveats_[1] = Caveat({ + args: hex"", + enforcer: address(erc20TransferAmountEnforcer), + terms: abi.encodePacked(address(token), prizeLevels[0]) + }); + + delegations_[0][0] = Delegation({ + delegate: address(delegationChainEnforcer), + delegator: address(treasury.deleGator), + authority: ROOT_AUTHORITY, + caveats: caveats_, + salt: 0, + signature: hex"" + }); + delegations_[1][0] = Delegation({ + delegate: address(delegationChainEnforcer), + delegator: address(treasury.deleGator), + authority: ROOT_AUTHORITY, + caveats: caveats_, + salt: 0, + signature: hex"" + }); + + delegations_[0][0] = signDelegation(treasury, delegations_[0][0]); + delegations_[1][0] = signDelegation(treasury, delegations_[1][0]); + // Try to execute afterHook, but it will revert because the delegation manager + // redemption function didn't execute the erc20 token transfer + vm.prank(address(delegationManagerEmptyMock_)); + vm.expectRevert("DelegationChainEnforcer:payment-not-received"); + delegationChainEnforcer.afterHook( + hex"", + abi.encode(delegations_), + singleDefaultMode, + ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData), + firstReferralDelegationHash, + address(users.alice.deleGator), + address(ICA.deleGator) + ); + } + + /// @notice Tests that afterHook reverts when allowance delegations array is empty + function test_afterHookInvalidAllowanceDelegationsLength() public { + // First post a valid referral chain + address[] memory delegators_ = new address[](2); + delegators_[0] = address(users.alice.deleGator); + delegators_[1] = address(users.bob.deleGator); + + vm.prank(address(chainIntegrity.deleGator)); + delegationChainEnforcer.post(delegators_); + + // Create execution data for the post function + Execution memory execution_ = Execution({ + target: address(delegationChainEnforcer), + value: 0, + callData: abi.encodeCall(DelegationChainEnforcer.post, (delegators_)) + }); + + // Create empty delegations array + Delegation[][] memory delegations_ = new Delegation[][](0); + + // Try to execute afterHook with empty delegations array + vm.prank(address(delegationManager)); + vm.expectRevert("DelegationChainEnforcer:invalid-allowance-delegations-length"); + delegationChainEnforcer.afterHook( + hex"", + abi.encode(delegations_), + singleDefaultMode, + ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData), + firstReferralDelegationHash, + address(users.alice.deleGator), + address(ICA.deleGator) + ); + } + + ////////////////////////////// getTermsInfo Tests ////////////////////////////// + + /// @notice Tests that getTermsInfo correctly decodes valid terms + function test_getTermsInfoValid() public { + uint256 expectedPosition = 5; + bytes memory terms = abi.encodePacked(expectedPosition); + + uint256 result = delegationChainEnforcer.getTermsInfo(terms); + assertEq(result, expectedPosition, "getTermsInfo should return correct position"); + } + + /// @notice Tests that getTermsInfo reverts when terms length is not 32 bytes + function test_getTermsInfoInvalidLength() public { + bytes memory terms = abi.encodePacked(uint256(5), uint256(6)); // 64 bytes + + vm.expectRevert("DelegationChainEnforcer:invalid-terms-length"); + delegationChainEnforcer.getTermsInfo(terms); + } + + /// @notice Tests that getTermsInfo handles zero position correctly + function test_getTermsInfoZeroPosition() public { + uint256 expectedPosition = 0; + bytes memory terms = abi.encodePacked(expectedPosition); + + uint256 result = delegationChainEnforcer.getTermsInfo(terms); + assertEq(result, expectedPosition, "getTermsInfo should handle zero position"); + } + + /// @notice Tests that getTermsInfo handles maximum position correctly + function test_getTermsInfoMaxPosition() public { + uint256 expectedPosition = type(uint256).max; + bytes memory terms = abi.encodePacked(expectedPosition); + + uint256 result = delegationChainEnforcer.getTermsInfo(terms); + assertEq(result, expectedPosition, "getTermsInfo should handle max position"); + } + + /// @notice Tests that beforeHook reverts when delegators length exceeds MAX_REFERRAL_DEPTH + function test_beforeHookInvalidDelegatorsLength() public { + // Create delegators array exceeding MAX_REFERRAL_DEPTH (20) + address[] memory delegators_ = new address[](21); + for (uint256 i = 0; i < 21; i++) { + delegators_[i] = address(uint160(i + 1)); + } + + Execution memory execution_ = Execution({ + target: address(delegationChainEnforcer), + value: 0, + callData: abi.encodeCall(DelegationChainEnforcer.post, (delegators_)) + }); + + vm.prank(address(delegationManager)); + vm.expectRevert("DelegationChainEnforcer:invalid-delegators-length"); + delegationChainEnforcer.beforeHook( + abi.encodePacked(uint256(0)), + hex"", + singleDefaultMode, + ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData), + keccak256(""), + address(users.alice.deleGator), + address(0) + ); + } + + /// @notice Tests that beforeHook reverts when expected position is greater than delegators length + function test_beforeHookInvalidExpectedPosition() public { + // Create delegators array with length 2 + address[] memory delegators_ = new address[](2); + delegators_[0] = address(users.alice.deleGator); + delegators_[1] = address(users.bob.deleGator); + + Execution memory execution_ = Execution({ + target: address(delegationChainEnforcer), + value: 0, + callData: abi.encodeCall(DelegationChainEnforcer.post, (delegators_)) + }); + + // Try with position 2 (should be 0 or 1) + vm.prank(address(delegationManager)); + vm.expectRevert("DelegationChainEnforcer:invalid-expected-position"); + delegationChainEnforcer.beforeHook( + abi.encodePacked(uint256(2)), + hex"", + singleDefaultMode, + ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData), + keccak256(""), + address(users.alice.deleGator), + address(0) + ); + } + + /// @notice Tests that beforeHook reverts when delegator doesn't match position + function test_beforeHookInvalidDelegatorOrPosition() public { + // Create delegators array with length 2 + address[] memory delegators_ = new address[](2); + delegators_[0] = address(users.alice.deleGator); + delegators_[1] = address(users.bob.deleGator); + + Execution memory execution_ = Execution({ + target: address(delegationChainEnforcer), + value: 0, + callData: abi.encodeCall(DelegationChainEnforcer.post, (delegators_)) + }); + + // Try with position 0 but wrong delegator + vm.prank(address(delegationManager)); + vm.expectRevert("DelegationChainEnforcer:invalid-delegator-or-position"); + delegationChainEnforcer.beforeHook( + abi.encodePacked(uint256(0)), + hex"", + singleDefaultMode, + ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData), + keccak256(""), + address(users.carol.deleGator), // Wrong delegator for position 0 + address(0) + ); + } + + ////////////////////////////// Internal Utils ////////////////////////////// + // The args contain a delegation chain for each of the delegators/prize levels, and the token to redeem the payments. function _getRedemptionArgs() internal view returns (bytes memory encoded_) { // Create delegation from treasury with caveats @@ -187,7 +860,7 @@ contract DelegationChainEnforcerTest is BaseTest { delegations_[i][0] = signDelegation(treasury, delegations_[i][0]); } - encoded_ = abi.encode(delegations_, token); + encoded_ = abi.encode(delegations_); } function _getBalances(address[] memory _recipients) internal view returns (uint256[] memory balances_) { @@ -239,3 +912,9 @@ contract DelegationChainEnforcerTest is BaseTest { caveats_[3] = Caveat({ args: hex"", enforcer: address(redeemerEnforcer), terms: abi.encodePacked(address(ICA.deleGator)) }); } } + +contract DelegationManagerEmptyMock { + function redeemDelegations(bytes[] calldata, ModeCode[] calldata, bytes[] calldata) external { + // Left empty on purpose + } +} From 6fd15d83db6ad47a4c9eea478f21263a44665a01 Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Mon, 19 May 2025 10:46:32 -0600 Subject: [PATCH 7/7] chore: improve gas cost, added new tests --- src/DelegationManager.sol | 8 - src/enforcers/DelegationChainEnforcer.sol | 126 +++--- test/enforcers/DelegationChainEnforcer.t.sol | 407 +++++++++++++++++-- test/utils/BaseTest.t.sol | 1 + test/utils/Types.t.sol | 1 + 5 files changed, 449 insertions(+), 94 deletions(-) diff --git a/src/DelegationManager.sol b/src/DelegationManager.sol index f17883f6..4d6a9811 100644 --- a/src/DelegationManager.sol +++ b/src/DelegationManager.sol @@ -14,7 +14,6 @@ import { IDeleGatorCore } from "./interfaces/IDeleGatorCore.sol"; import { Delegation, Caveat, ModeCode } from "./utils/Types.sol"; import { EncoderLib } from "./libraries/EncoderLib.sol"; import { ERC1271Lib } from "./libraries/ERC1271Lib.sol"; -import "forge-std/Test.sol"; /** * @title DelegationManager @@ -155,9 +154,6 @@ contract DelegationManager is IDelegationManager, Ownable2Step, Pausable, EIP712 // Validate caller if (delegations_[0].delegate != msg.sender && delegations_[0].delegate != ANY_DELEGATE) { - console2.log("Invalid delegate1"); - console2.log("delegations_[0].delegate:", delegations_[0].delegate); - console2.log("msg.sender:", msg.sender); revert InvalidDelegate(); } @@ -199,9 +195,6 @@ contract DelegationManager is IDelegationManager, Ownable2Step, Pausable, EIP712 // Validate delegate address nextDelegate_ = delegations_[delegationsIndex_ + 1].delegate; if (nextDelegate_ != ANY_DELEGATE && delegations_[delegationsIndex_].delegator != nextDelegate_) { - console2.log("Invalid delegate2"); - console2.log("nextDelegate_:", nextDelegate_); - console2.log("delegations_[delegationsIndex_].delegator:", delegations_[delegationsIndex_].delegator); revert InvalidDelegate(); } } else if (delegations_[delegationsIndex_].authority != ROOT_AUTHORITY) { @@ -219,7 +212,6 @@ contract DelegationManager is IDelegationManager, Ownable2Step, Pausable, EIP712 Caveat[] memory caveats_ = batchDelegations_[batchIndex_][delegationsIndex_].caveats; for (uint256 caveatsIndex_; caveatsIndex_ < caveats_.length; ++caveatsIndex_) { ICaveatEnforcer enforcer_ = ICaveatEnforcer(caveats_[caveatsIndex_].enforcer); - console2.log("Calling beforeAllHook", address(enforcer_)); enforcer_.beforeAllHook( caveats_[caveatsIndex_].terms, caveats_[caveatsIndex_].args, diff --git a/src/enforcers/DelegationChainEnforcer.sol b/src/enforcers/DelegationChainEnforcer.sol index 2612dd09..fb567d23 100644 --- a/src/enforcers/DelegationChainEnforcer.sol +++ b/src/enforcers/DelegationChainEnforcer.sol @@ -9,7 +9,6 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IDelegationManager } from "../interfaces/IDelegationManager.sol"; import { CaveatEnforcer } from "./CaveatEnforcer.sol"; import { ModeCode, Delegation } from "../utils/Types.sol"; -import "forge-std/Test.sol"; /** * @title DelegationChainEnforcer @@ -28,7 +27,7 @@ import "forge-std/Test.sol"; * avoiding redundant checks on each chain level. Prize amounts are fixed to exactly maxPrizePayments levels and * must be set via the owner. Cannot post the same chain twice or redeem more than once per chain hash. */ -contract DelegationChainEnforcer is CaveatEnforcer, Ownable { +contract DelegationChainEnforcer is CaveatEnforcer, Ownable2Step { using ExecutionLib for bytes; /// @dev Maximum number of referrers in the referral chain @@ -47,13 +46,13 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { address public immutable prizeToken; /// @dev Maps delegation manager addresses to referral array hashes to recipient addresses - mapping(address delegationManager => mapping(bytes32 referralChainHash => address[] recipients)) public referrals; + mapping(address delegationManager => mapping(bytes32 referralChainHash => address[] recipients)) private referrals; /// @dev Maps delegation manager addresses to referral array hashes to redemption status - mapping(address delegationManager => mapping(bytes32 referralChainHash => bool redeemed)) public redeemed; + mapping(address delegationManager => mapping(bytes32 referralChainHash => bool redeemed)) private redeemed; /// @dev Array of prize amounts for each position in the referral array - uint256[] public prizeAmounts; + uint256[] private prizeAmounts; ////////////////////////////// Events ////////////////////////////// @@ -71,6 +70,20 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { */ event PrizesSet(address indexed sender, uint256[] prizeLevels); + /** + * @dev Emitted when a payment is completed + * @param sender The address of the delegation manager on which the payment was completed + * @param referralChainHash The hash of the referral array + */ + event PaymentCompleted(address indexed sender, bytes32 indexed referralChainHash); + + /** + * @dev Emitted when a payment has been already made for a referral chain + * @param sender The address of the delegation manager on which the payment was made + * @param referralChainHash The hash of the referral array + */ + event ReferralChainAlreadyPaid(address indexed sender, bytes32 indexed referralChainHash); + ////////////////////////////// Constructor ////////////////////////////// /** @@ -127,21 +140,11 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { delegatorsLength_ > 1 && delegatorsLength_ <= MAX_REFERRAL_DEPTH, "DelegationChainEnforcer:invalid-delegators-length" ); - // TODO: make sure someonen can't add himself twice in the same delegation chain - // TODO: make sure the intermediary chain is involved otherwise fail, pleople would add themselves to the chain at the end - // multiple times to get paid multiple times, or an address they control? - bytes32 referralChainHash_ = keccak256(abi.encode(_delegators)); address[] storage referrals_ = referrals[address(delegationManager)][referralChainHash_]; require(referrals_.length == 0, "DelegationChainEnforcer:referral-chain-already-posted"); - // // Push up to the last maxPrizePayments delegators in reverse order - // uint256 startIndex_ = delegatorsLength_ < maxPrizePayments ? 0 : delegatorsLength_ - maxPrizePayments; - // for (uint256 i = delegatorsLength_; i > startIndex_; i--) { - // referrals_.push(_delegators[i - 1]); - // } - // Push up to the last delegators according to the amount of prizes. uint256 startIndex_ = delegatorsLength_ > maxPrizePayments ? delegatorsLength_ - maxPrizePayments : 0; for (uint256 i = startIndex_; i < delegatorsLength_; ++i) { @@ -153,11 +156,27 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { /** * @notice Get the referrals for a given referral chain hash + * @param _delegationManager The address of the delegation manager * @param _referralChainHash The hash of the referral chain * @return referrals_ The array of referrals */ - function getReferrals(bytes32 _referralChainHash) external view returns (address[] memory referrals_) { - return referrals[address(delegationManager)][_referralChainHash]; + function getReferrals( + address _delegationManager, + bytes32 _referralChainHash + ) + external + view + returns (address[] memory referrals_) + { + return referrals[_delegationManager][_referralChainHash]; + } + + /** + * @notice Get all the prize amounts for the referrers + * @return prizeAmounts_ The array of prize amounts + */ + function getPrizeAmounts() external view returns (uint256[] memory) { + return prizeAmounts; } ////////////////////////////// Public Methods ////////////////////////////// @@ -196,7 +215,6 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { * @param _args ABI-encoded (Delegation[][] allowanceDelegations) * @param _executionCallData Calldata containing encoded referral array * @param _delegationHash The hash of the delegation - * @param _delegator The address of the delegator * @param _redeemer The address of the redeemer */ function afterHook( @@ -205,53 +223,51 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { ModeCode, bytes calldata _executionCallData, bytes32 _delegationHash, - address _delegator, + address, address _redeemer ) public override { - console2.log("Checkpoint after hook"); - console2.log("_delegator:", _delegator); - console2.log("_redeemer:", _redeemer); - bytes32 referralChainHash_ = _getReferralChainHash(_executionCallData); - // Stops afterHook execution if the referral hash has already been redeemed - // TODO: add a test to check this mapping - if (redeemed[msg.sender][referralChainHash_]) return; + // Stops afterHook execution if the referral hash has already been paid + if (redeemed[msg.sender][referralChainHash_]) { + emit ReferralChainAlreadyPaid(msg.sender, referralChainHash_); + return; + } + redeemed[msg.sender][referralChainHash_] = true; address[] memory referrals_ = referrals[msg.sender][referralChainHash_]; (Delegation[][] memory allowanceDelegations_, uint256 allowanceLength_) = _validateAndDecodeArgs(_args, _delegationHash, _redeemer); + require(referrals_.length == allowanceLength_, "DelegationChainEnforcer:invalid-delegations-length"); + bytes[] memory permissionContexts_ = new bytes[](allowanceLength_); bytes[] memory executionCallDatas_ = new bytes[](allowanceLength_); ModeCode[] memory encodedModes_ = new ModeCode[](allowanceLength_); - - require(referrals_.length == allowanceDelegations_.length, "DelegationChainEnforcer:invalid-delegations-length"); + uint256[] memory balancesBefore_ = new uint256[](allowanceLength_); address token_ = prizeToken; - for (uint256 i = 0; i < allowanceLength_; ++i) { + for (uint256 i = 0; i < allowanceLength_;) { + balancesBefore_[i] = IERC20(token_).balanceOf(referrals_[i]); permissionContexts_[i] = abi.encode(allowanceDelegations_[i]); executionCallDatas_[i] = ExecutionLib.encodeSingle(token_, 0, abi.encodeCall(IERC20.transfer, (referrals_[i], prizeAmounts[i]))); encodedModes_[i] = ModeLib.encodeSimpleSingle(); - console2.log("prizeAmounts[i]:", prizeAmounts[i]); + unchecked { + ++i; + } } - uint256[] memory balanceBefore_ = _getBalances(referrals_, token_); - - console2.log("ABOUT TO REDEEM INSIDE AFTERHOOK"); // Attempt to redeem the delegation and make the payment delegationManager.redeemDelegations(permissionContexts_, encodedModes_, executionCallDatas_); - console2.log("AFTER REDEMPTION INSIDE AFTERHOOK"); - //TODO: If this fails, for memory, then try to pass the redeemDelegations() as callback below. - _validateTransfer(referrals_, token_, balanceBefore_); + _validateTransfer(referrals_, IERC20(token_), balancesBefore_); - redeemed[msg.sender][referralChainHash_] = true; + emit PaymentCompleted(msg.sender, referralChainHash_); } /** @@ -285,31 +301,18 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { emit PrizesSet(msg.sender, _prizeAmounts); } - /** - * @dev Gets the balances of recipients for a specific token - * @param _recipients Array of recipient addresses - * @param _token The token address - * @return balances_ Array of balances for each recipient - */ - function _getBalances(address[] memory _recipients, address _token) internal view returns (uint256[] memory balances_) { - uint256 recipientsLength_ = _recipients.length; - balances_ = new uint256[](recipientsLength_); - for (uint256 i = 0; i < recipientsLength_; ++i) { - balances_[i] = IERC20(_token).balanceOf(_recipients[i]); - } - } - /** * @dev Validates that transfers were successful by checking recipient balances * @param _recipients Array of recipient addresses * @param _token The token address * @param balanceBefore_ Array of balances before the transfer */ - function _validateTransfer(address[] memory _recipients, address _token, uint256[] memory balanceBefore_) internal view { - // TODO: prizeAmounts read twice from storage - uint256[] memory balances_ = _getBalances(_recipients, _token); - for (uint256 i = 0; i < _recipients.length; ++i) { - require(balances_[i] >= balanceBefore_[i] + prizeAmounts[i], "DelegationChainEnforcer:payment-not-received"); + function _validateTransfer(address[] memory _recipients, IERC20 _token, uint256[] memory balanceBefore_) internal view { + uint256[] memory prizes_ = prizeAmounts; + uint256 recipientsLength_ = _recipients.length; + for (uint256 i = 0; i < recipientsLength_; ++i) { + uint256 newBalance_ = _token.balanceOf(_recipients[i]); + require(newBalance_ >= balanceBefore_[i] + prizes_[i], "DelegationChainEnforcer:payment-not-received"); } } @@ -330,11 +333,10 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { view returns (Delegation[][] memory allowanceDelegations_, uint256 allowanceLength_) { - // TODO: we could assume a single direct delegation with the total amount, instead of an array of delegations (allowanceDelegations_) = abi.decode(_args, (Delegation[][])); allowanceLength_ = allowanceDelegations_.length; require(allowanceLength_ > 0, "DelegationChainEnforcer:invalid-allowance-delegations-length"); - + bytes memory packedArgs = abi.encodePacked(_delegationHash, _redeemer); for (uint256 i = 0; i < allowanceLength_; ++i) { require( allowanceDelegations_[i][0].caveats.length > 0 @@ -342,7 +344,7 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { "DelegationChainEnforcer:missing-argsEqualityCheckEnforcer" ); // The Args Enforcer with this data (hash & redeemer) must be the first Enforcer in the payment delegations caveats - allowanceDelegations_[i][0].caveats[0].args = abi.encodePacked(_delegationHash, _redeemer); + allowanceDelegations_[i][0].caveats[0].args = packedArgs; } } @@ -353,12 +355,12 @@ contract DelegationChainEnforcer is CaveatEnforcer, Ownable { */ function _getReferralChainHash(bytes calldata _executionCallData) internal pure returns (bytes32 referralChainHash_) { (,, bytes calldata callData_) = _executionCallData.decodeSingle(); - (address[] memory delegators_) = abi.decode(callData_[4:], (address[])); - referralChainHash_ = keccak256(abi.encode(delegators_)); + // Passing the addresses of the post() function. It is already the exact ABI encoding of the address[] + referralChainHash_ = keccak256(callData_[4:]); } /** - * @dev Validates the position of a delegator in the referral array + * @dev Validates the position of a delegator in the referral array during the beforeHook * @param _executionCallData Calldata containing encoded referral array * @param _terms 32 bytes encoded with the delegator's expected position in the referral array * @param _delegator The address of the delegator diff --git a/test/enforcers/DelegationChainEnforcer.t.sol b/test/enforcers/DelegationChainEnforcer.t.sol index ec14687a..10e226c4 100644 --- a/test/enforcers/DelegationChainEnforcer.t.sol +++ b/test/enforcers/DelegationChainEnforcer.t.sol @@ -22,9 +22,6 @@ import { RedeemerEnforcer } from "../../src/enforcers/RedeemerEnforcer.sol"; import { BasicERC20 } from "../utils/BasicERC20.t.sol"; import "forge-std/Test.sol"; -// Import the event -import { DelegationChainEnforcer as DelegationChainEnforcerEvents } from "../../src/enforcers/DelegationChainEnforcer.sol"; - contract DelegationChainEnforcerTest is BaseTest { ////////////////////////////// Setup ////////////////////////////// @@ -49,6 +46,8 @@ contract DelegationChainEnforcerTest is BaseTest { BasicERC20 public token; bytes32 public firstReferralDelegationHash; + uint256 public maxPrizePayments; + constructor() { IMPLEMENTATION = Implementation.MultiSig; SIGNATURE_TYPE = SignatureType.EOA; @@ -87,6 +86,7 @@ contract DelegationChainEnforcerTest is BaseTest { address(token), prizeLevels ); + maxPrizePayments = delegationChainEnforcer.maxPrizePayments(); } ////////////////////////////// Constructor Tests ////////////////////////////// @@ -161,7 +161,7 @@ contract DelegationChainEnforcerTest is BaseTest { ////////////////////////////// setPrizes Tests ////////////////////////////// /// @notice Tests that owner can successfully set new prize amounts - function test_setPrizes() public { + function test_setPrizes1() public { uint256[] memory newPrizes = new uint256[](5); newPrizes[0] = 20 ether; newPrizes[1] = 15 ether; @@ -173,8 +173,9 @@ contract DelegationChainEnforcerTest is BaseTest { delegationChainEnforcer.setPrizes(newPrizes); // Verify each prize amount was set correctly + uint256[] memory prizeAmounts = delegationChainEnforcer.getPrizeAmounts(); for (uint256 i = 0; i < newPrizes.length; i++) { - assertEq(delegationChainEnforcer.prizeAmounts(i), newPrizes[i]); + assertEq(prizeAmounts[i], newPrizes[i]); } } @@ -229,7 +230,7 @@ contract DelegationChainEnforcerTest is BaseTest { vm.prank(address(chainIntegrity.deleGator)); vm.expectEmit(true, false, false, true); - emit DelegationChainEnforcerEvents.PrizesSet(address(chainIntegrity.deleGator), newPrizes); + emit DelegationChainEnforcer.PrizesSet(address(chainIntegrity.deleGator), newPrizes); delegationChainEnforcer.setPrizes(newPrizes); } @@ -311,13 +312,12 @@ contract DelegationChainEnforcerTest is BaseTest { vm.prank(address(chainIntegrity.deleGator)); vm.expectEmit(true, true, false, true); - emit DelegationChainEnforcerEvents.ReferralArrayPosted( - address(chainIntegrity.deleGator), keccak256(abi.encode(delegators_)) - ); + emit DelegationChainEnforcer.ReferralArrayPosted(address(chainIntegrity.deleGator), keccak256(abi.encode(delegators_))); delegationChainEnforcer.post(delegators_); // Verify the referrals were stored correctly - address[] memory storedReferrals = delegationChainEnforcer.getReferrals(keccak256(abi.encode(delegators_))); + address[] memory storedReferrals = + delegationChainEnforcer.getReferrals(address(delegationManager), keccak256(abi.encode(delegators_))); assertEq(storedReferrals.length, 2, "referrals length should be 2"); assertEq(storedReferrals[0], delegators_[0], "referral[0] should be alice"); assertEq(storedReferrals[1], delegators_[1], "referral[1] should be bob"); @@ -332,13 +332,12 @@ contract DelegationChainEnforcerTest is BaseTest { vm.prank(address(chainIntegrity.deleGator)); vm.expectEmit(true, true, false, true); - emit DelegationChainEnforcerEvents.ReferralArrayPosted( - address(chainIntegrity.deleGator), keccak256(abi.encode(delegators_)) - ); + emit DelegationChainEnforcer.ReferralArrayPosted(address(chainIntegrity.deleGator), keccak256(abi.encode(delegators_))); delegationChainEnforcer.post(delegators_); // Verify the referrals were stored correctly (only last maxPrizePayments) - address[] memory storedReferrals = delegationChainEnforcer.getReferrals(keccak256(abi.encode(delegators_))); + address[] memory storedReferrals = + delegationChainEnforcer.getReferrals(address(delegationManager), keccak256(abi.encode(delegators_))); assertEq(storedReferrals.length, 5, "referrals length should be 5"); // maxPrizePayments uint256 count = 0; for (uint256 i = delegators_.length - 5; i < delegators_.length; ++i) { @@ -406,7 +405,8 @@ contract DelegationChainEnforcerTest is BaseTest { delegationChainEnforcer.post(delegators_); // Verify the referrals were stored in correct order - address[] memory storedReferrals = delegationChainEnforcer.getReferrals(keccak256(abi.encode(delegators_))); + address[] memory storedReferrals = + delegationChainEnforcer.getReferrals(address(delegationManager), keccak256(abi.encode(delegators_))); assertEq(storedReferrals.length, 3, "referrals length should be 3"); assertEq(storedReferrals[0], delegators_[0], "referral[0] should be alice"); assertEq(storedReferrals[1], delegators_[1], "referral[1] should be bob"); @@ -424,7 +424,8 @@ contract DelegationChainEnforcerTest is BaseTest { delegationChainEnforcer.post(delegators_); // Verify all delegators were stored since length < maxPrizePayments - address[] memory storedReferrals = delegationChainEnforcer.getReferrals(keccak256(abi.encode(delegators_))); + address[] memory storedReferrals = + delegationChainEnforcer.getReferrals(address(delegationManager), keccak256(abi.encode(delegators_))); assertEq(storedReferrals.length, 3, "referrals length should be 3"); assertEq(storedReferrals[0], delegators_[0], "referral[0] should be alice"); assertEq(storedReferrals[1], delegators_[1], "referral[1] should be bob"); @@ -609,7 +610,6 @@ contract DelegationChainEnforcerTest is BaseTest { vm.prank(address(chainIntegrity.deleGator)); delegationChainEnforcer.post(delegators_); - bytes32 referralChainHash_ = keccak256(abi.encode(delegators_)); // Create execution data for the post function Execution memory execution_ = Execution({ @@ -703,6 +703,92 @@ contract DelegationChainEnforcerTest is BaseTest { ); } + /// @notice Tests the afterHook events are emitted + function test_referralChainAfterHookEvents() public { + // First post a valid referral chain + address[] memory delegators_ = new address[](2); + delegators_[0] = address(users.alice.deleGator); + delegators_[1] = address(users.bob.deleGator); + + vm.prank(address(chainIntegrity.deleGator)); + delegationChainEnforcer.post(delegators_); + + // Create execution data for the post function + Execution memory execution_ = Execution({ + target: address(delegationChainEnforcer), + value: 0, + callData: abi.encodeCall(DelegationChainEnforcer.post, (delegators_)) + }); + + // Create delegations with valid structure + Delegation[][] memory delegations_ = new Delegation[][](2); + delegations_[0] = new Delegation[](1); + delegations_[1] = new Delegation[](1); + + Caveat[] memory caveats_ = new Caveat[](2); + caveats_[0] = Caveat({ + args: hex"", + enforcer: address(argsEqualityCheckEnforcer), + terms: abi.encodePacked(firstReferralDelegationHash, address(ICA.deleGator)) + }); + caveats_[1] = Caveat({ + args: hex"", + enforcer: address(erc20TransferAmountEnforcer), + terms: abi.encodePacked(address(token), prizeLevels[0]) + }); + + delegations_[0][0] = Delegation({ + delegate: address(delegationChainEnforcer), + delegator: address(treasury.deleGator), + authority: ROOT_AUTHORITY, + caveats: caveats_, + salt: 0, + signature: hex"" + }); + delegations_[1][0] = Delegation({ + delegate: address(delegationChainEnforcer), + delegator: address(treasury.deleGator), + authority: ROOT_AUTHORITY, + caveats: caveats_, + salt: 1, + signature: hex"" + }); + + delegations_[0][0] = signDelegation(treasury, delegations_[0][0]); + delegations_[1][0] = signDelegation(treasury, delegations_[1][0]); + + bytes32 referralChainHash_ = keccak256(abi.encode(delegators_)); + + // First execution to mark the chain as paid + // Emits the event PaymentCompleted + vm.prank(address(delegationManager)); + vm.expectEmit(true, true, true, true); + emit DelegationChainEnforcer.PaymentCompleted(address(delegationManager), referralChainHash_); + delegationChainEnforcer.afterHook( + hex"", + abi.encode(delegations_), + singleDefaultMode, + ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData), + firstReferralDelegationHash, + address(users.alice.deleGator), + address(ICA.deleGator) + ); + + // Second execution should emit ReferralChainAlreadyPaid event + vm.prank(address(delegationManager)); + vm.expectEmit(true, true, true, true); + emit DelegationChainEnforcer.ReferralChainAlreadyPaid(address(delegationManager), referralChainHash_); + delegationChainEnforcer.afterHook( + hex"", + abi.encode(delegations_), + singleDefaultMode, + ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData), + firstReferralDelegationHash, + address(users.alice.deleGator), + address(ICA.deleGator) + ); + } + ////////////////////////////// getTermsInfo Tests ////////////////////////////// /// @notice Tests that getTermsInfo correctly decodes valid terms @@ -821,7 +907,267 @@ contract DelegationChainEnforcerTest is BaseTest { ); } + ////////////////////////////// Helper Functions ////////////////////////////// + + function _createDelegation( + address _delegate, + TestUser memory _delegatorTestUser, + bytes32 _authority, + Caveat[] memory _caveats + ) + internal + view + returns (Delegation memory) + { + Delegation memory delegation_ = Delegation({ + delegate: _delegate, + delegator: address(_delegatorTestUser.deleGator), + authority: _authority, + caveats: _caveats, + salt: 0, + signature: hex"" + }); + return signDelegation(_delegatorTestUser, delegation_); + } + + function _createPositionCaveat(uint256 _position) internal view returns (Caveat[] memory) { + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = + Caveat({ args: hex"", enforcer: address(delegationChainEnforcer), terms: abi.encodePacked(uint256(_position)) }); + return caveats_; + } + + function _createDelegationChain(TestUser[] memory _testUsers) internal returns (Delegation[] memory) { + Delegation[] memory delegations_ = new Delegation[](_testUsers.length * 2 + 1); + bytes32 lastDelegationHash; + + // The delegations are stored from leaf to root. + // First delegation from chainIntegrity to ICA + delegations_[delegations_.length - 1] = + _createDelegation(address(ICA.deleGator), chainIntegrity, ROOT_AUTHORITY, _getChainIntegrityCaveats()); + lastDelegationHash = EncoderLib._getDelegationHash(delegations_[delegations_.length - 1]); + + // Create delegations in pairs (from ICA to user, then user back to ICA) + uint256 userCount = 0; + bool firstReferralSet = false; + for (uint256 i = delegations_.length - 2; i > 0; i -= 2) { + // From ICA to user + delegations_[i] = _createDelegation(address(_testUsers[userCount].deleGator), ICA, lastDelegationHash, new Caveat[](0)); + lastDelegationHash = EncoderLib._getDelegationHash(delegations_[i]); + + // From user to ICA + delegations_[i - 1] = _createDelegation( + address(ICA.deleGator), _testUsers[userCount], lastDelegationHash, _createPositionCaveat(userCount) + ); + lastDelegationHash = EncoderLib._getDelegationHash(delegations_[i - 1]); + + // If this is the first referral delegation, set the first referral delegation hash + // This is used in the args enforcer later + if (!firstReferralSet) { + firstReferralDelegationHash = lastDelegationHash; + firstReferralSet = true; + } + userCount++; + if (i == 1) break; + } + + return delegations_; + } + + // This test creates a delegation chain with 2 referrals with is less the amount of prize levels. + function test_twoReferralDelegationChain() public { + TestUser[] memory testUsers_ = new TestUser[](2); + testUsers_[0] = users.alice; + testUsers_[1] = users.bob; + + address[] memory delegators_ = _getAddressFromUsers(testUsers_); + + // Create delegation chain + (Delegation[] memory delegations_) = _createDelegationChain(testUsers_); + + // Store delegators for payment validation + delegators = delegators_; + + // Add redemption args to the first delegation that uses the DelegationChainEnforcer + // This is Alice to ICA + delegations_[delegations_.length - 3].caveats[0].args = _getRedemptionArgs(); + + uint256[] memory balancesBefore_ = _getBalances(delegators); + + // Execute the delegation chain through ICA + invokeDelegation_UserOp(ICA, delegations_, _getExecution()); + + _validatePayments(balancesBefore_); + } + + // This test creates a delegation chain with 5 referrals with is exactly the amount of prize levels. + function test_fiveReferralDelegationChain() public { + TestUser[] memory testUsers_ = new TestUser[](5); + testUsers_[0] = users.alice; + testUsers_[1] = users.bob; + testUsers_[2] = users.carol; + testUsers_[3] = users.dave; + testUsers_[4] = users.eve; + + address[] memory delegators_ = _getAddressFromUsers(testUsers_); + + // Create delegation chain + (Delegation[] memory delegations_) = _createDelegationChain(testUsers_); + + // Store delegators for payment validation + delegators = delegators_; + + // Add redemption args to the first delegation that uses the DelegationChainEnforcer + // This is Alice to ICA + delegations_[delegations_.length - 3].caveats[0].args = _getRedemptionArgs(); + + uint256[] memory balancesBefore_ = _getBalances(delegators); + + // Execute the delegation chain through ICA + invokeDelegation_UserOp(ICA, delegations_, _getExecution()); + + _validatePayments(balancesBefore_); + } + + // This test creates a delegation chain with 7 referrals with is more than the amount of prize levels. + // Meaning that only the first 5 payments will be paid out. + function test_sevenReferralDelegationChain() public { + TestUser[] memory testUsers_ = new TestUser[](7); + testUsers_[0] = users.alice; + testUsers_[1] = users.bob; + testUsers_[2] = users.carol; + testUsers_[3] = users.dave; + testUsers_[4] = users.eve; + testUsers_[5] = users.frank; + testUsers_[6] = users.grace; + + address[] memory delegators_ = _getAddressFromUsers(testUsers_); + + // Create delegation chain + (Delegation[] memory delegations_) = _createDelegationChain(testUsers_); + + // Store delegators for payment validation + delegators = delegators_; + + // Add redemption args to the first delegation that uses the DelegationChainEnforcer + // This is Alice to ICA + // _getRedemptionArgs(); + delegations_[delegations_.length - 3].caveats[0].args = _getRedemptionArgs(); + + uint256[] memory balancesBefore_ = _getBalances(delegators); + + // Execute the delegation chain through ICA + invokeDelegation_UserOp(ICA, delegations_, _getExecution()); + + _validatePayments(balancesBefore_); + } + + // This test creates a delegation chain with 3 referrals with exactly 3 prize levels + function test_threePrizeLevelsDelegationChain() public { + // Clear prizeLevels + delete prizeLevels; + // Set up new prize levels with 3 amounts + prizeLevels.push(15 ether); + prizeLevels.push(10 ether); + prizeLevels.push(5 ether); + + // Create new enforcer with 3 prize levels + delegationChainEnforcer = new DelegationChainEnforcer( + address(chainIntegrity.deleGator), + IDelegationManager(address(delegationManager)), + address(argsEqualityCheckEnforcer), + address(token), + prizeLevels + ); + maxPrizePayments = delegationChainEnforcer.maxPrizePayments(); + + // Create test users array with 3 users + TestUser[] memory testUsers_ = new TestUser[](3); + testUsers_[0] = users.alice; + testUsers_[1] = users.bob; + testUsers_[2] = users.carol; + + address[] memory delegators_ = _getAddressFromUsers(testUsers_); + + // Create delegation chain + (Delegation[] memory delegations_) = _createDelegationChain(testUsers_); + + // Store delegators for payment validation + delegators = delegators_; + + // Add redemption args to the first delegation that uses the DelegationChainEnforcer + delegations_[delegations_.length - 3].caveats[0].args = _getRedemptionArgs(); + + uint256[] memory balancesBefore_ = _getBalances(delegators); + + // Execute the delegation chain through ICA + invokeDelegation_UserOp(ICA, delegations_, _getExecution()); + + _validatePayments(balancesBefore_); + } + + // This test creates a delegation chain with 7 referrals with 7 prize levels + function test_sevenPrizeLevelsDelegationChain() public { + // Set up new prize levels with 7 amounts + + // Clear prizeLevels + delete prizeLevels; + // Set up new prize levels with 3 amounts + prizeLevels.push(20 ether); + prizeLevels.push(15 ether); + prizeLevels.push(10 ether); + prizeLevels.push(8 ether); + prizeLevels.push(6 ether); + prizeLevels.push(4 ether); + prizeLevels.push(2 ether); + + // Create new enforcer with 7 prize levels + delegationChainEnforcer = new DelegationChainEnforcer( + address(chainIntegrity.deleGator), + IDelegationManager(address(delegationManager)), + address(argsEqualityCheckEnforcer), + address(token), + prizeLevels + ); + maxPrizePayments = delegationChainEnforcer.maxPrizePayments(); + + // Create test users array with 7 users + TestUser[] memory testUsers_ = new TestUser[](7); + testUsers_[0] = users.alice; + testUsers_[1] = users.bob; + testUsers_[2] = users.carol; + testUsers_[3] = users.dave; + testUsers_[4] = users.eve; + testUsers_[5] = users.frank; + testUsers_[6] = users.grace; + + address[] memory delegators_ = _getAddressFromUsers(testUsers_); + + // Create delegation chain + (Delegation[] memory delegations_) = _createDelegationChain(testUsers_); + + // Store delegators for payment validation + delegators = delegators_; + + // Add redemption args to the first delegation that uses the DelegationChainEnforcer + delegations_[delegations_.length - 3].caveats[0].args = _getRedemptionArgs(); + + uint256[] memory balancesBefore_ = _getBalances(delegators); + + // Execute the delegation chain through ICA + invokeDelegation_UserOp(ICA, delegations_, _getExecution()); + + _validatePayments(balancesBefore_); + } + ////////////////////////////// Internal Utils ////////////////////////////// + function _getAddressFromUsers(TestUser[] memory _testUsers) internal pure returns (address[] memory addresses_) { + uint256 length_ = _testUsers.length; + addresses_ = new address[](length_); + for (uint256 i = 0; i < length_; i++) { + addresses_[i] = address(_testUsers[i].deleGator); + } + } // The args contain a delegation chain for each of the delegators/prize levels, and the token to redeem the payments. function _getRedemptionArgs() internal view returns (bytes memory encoded_) { @@ -831,8 +1177,15 @@ contract DelegationChainEnforcerTest is BaseTest { // enforcer is the one that redeems the payments after that the others skip it. // The args enforcer needs the redeemer, the delegation hash alone is not enough. - Delegation[][] memory delegations_ = new Delegation[][](delegators.length); - for (uint256 i = 0; i < delegators.length; i++) { + // For delegators longer than maxPrizePayments, we still create an array of length maxPrizePayments since that's the max + // prize level + require(prizeLevels.length == maxPrizePayments, "prizeLevels length must be equal to maxPrizePayments"); + + uint256 iterations_ = delegators.length > maxPrizePayments ? maxPrizePayments : delegators.length; + Delegation[][] memory delegations_ = new Delegation[][](iterations_); + + // Calculate starting index to get last 5 delegators if more than 5 + for (uint256 i = 0; i < iterations_; i++) { delegations_[i] = new Delegation[](1); Caveat[] memory caveats_ = new Caveat[](2); @@ -864,17 +1217,23 @@ contract DelegationChainEnforcerTest is BaseTest { } function _getBalances(address[] memory _recipients) internal view returns (uint256[] memory balances_) { - uint256 recipientsLength_ = _recipients.length; - balances_ = new uint256[](recipientsLength_); - for (uint256 i = 0; i < recipientsLength_; ++i) { + uint256 maxPrizeLevel = _recipients.length > maxPrizePayments ? maxPrizePayments : _recipients.length; + uint256 startIndex = _recipients.length > maxPrizePayments ? _recipients.length - maxPrizePayments : 0; + + balances_ = new uint256[](maxPrizeLevel); + for (uint256 i = startIndex; i < maxPrizeLevel; ++i) { balances_[i] = IERC20(token).balanceOf(_recipients[i]); } } function _validatePayments(uint256[] memory balanceBefore_) internal { uint256[] memory balances_ = _getBalances(delegators); - for (uint256 i = 0; i < delegators.length; i++) { - assertEq(balances_[i], balanceBefore_[i] + prizeLevels[i], "The balance after is insufficient"); + uint256 maxPrizeLevel = delegators.length > maxPrizePayments ? maxPrizePayments : delegators.length; + uint256 startIndex = delegators.length > maxPrizePayments ? delegators.length - maxPrizePayments : 0; + uint256 prizeLevelCount = 0; + for (uint256 i = startIndex; i < maxPrizeLevel; i++) { + assertEq(balances_[i], balanceBefore_[i] + prizeLevels[prizeLevelCount], "The balance after is insufficient"); + prizeLevelCount++; } } diff --git a/test/utils/BaseTest.t.sol b/test/utils/BaseTest.t.sol index 268d5301..2fa62acd 100644 --- a/test/utils/BaseTest.t.sol +++ b/test/utils/BaseTest.t.sol @@ -517,5 +517,6 @@ abstract contract BaseTest is Test { users_.dave = createUser("Dave"); users_.eve = createUser("Eve"); users_.frank = createUser("Frank"); + users_.grace = createUser("Grace"); } } diff --git a/test/utils/Types.t.sol b/test/utils/Types.t.sol index a93d5c9e..91e3288e 100644 --- a/test/utils/Types.t.sol +++ b/test/utils/Types.t.sol @@ -20,6 +20,7 @@ struct TestUsers { TestUser dave; TestUser eve; TestUser frank; + TestUser grace; } /**