Skip to content

Commit

Permalink
feat: add batch specific enforcer
Browse files Browse the repository at this point in the history
  • Loading branch information
McOso committed Feb 21, 2025
1 parent 7905da0 commit 15b9d4b
Show file tree
Hide file tree
Showing 3 changed files with 378 additions and 0 deletions.
132 changes: 132 additions & 0 deletions src/enforcers/SpecificActionERC20TransferBatchEnforcer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// SPDX-License-Identifier: MIT AND Apache-2.0
pragma solidity 0.8.23;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol";
import { ModeLib } from "@erc7579/lib/ModeLib.sol";

import { CaveatEnforcer } from "./CaveatEnforcer.sol";
import { ModeCode, Execution } from "../utils/Types.sol";

/**
* @title SpecificActionERC20TransferBatchEnforcer
* @dev This enforcer validates a batch of exactly 2 transactions where:
* 1. First transaction must match specific target, method and calldata
* 2. Second transaction must be an ERC20 transfer with specific parameters
* @dev The delegation can only be executed once
*/
contract SpecificActionERC20TransferBatchEnforcer is CaveatEnforcer {
using ExecutionLib for bytes;
using ModeLib for ModeCode;

////////////////////////////// State //////////////////////////////

// Tracks if a delegation has been executed
mapping(address delegationManager => mapping(bytes32 delegationHash => bool used)) public usedDelegations;

////////////////////////////// Events //////////////////////////////

event DelegationExecuted(address indexed delegationManager, bytes32 indexed delegationHash);

////////////////////////////// Structs //////////////////////////////

struct TermsData {
address tokenAddress;
address recipient;
uint256 amount;
address firstTarget;
bytes firstCalldata;
}

////////////////////////////// Public Methods //////////////////////////////

/**
* @notice Enforces the batch execution rules
* @param _terms The encoded terms containing:
* - ERC20 token address (20 bytes)
* - Transfer recipient address (20 bytes)
* - Transfer amount (32 bytes)
* - First transaction target address (20 bytes)
* - First transaction calldata (remaining bytes)
* @param _mode The execution mode
* @param _executionCallData The batch execution calldata
* @param _delegationHash The delegation hash
*/
function beforeHook(
bytes calldata _terms,
bytes calldata,
ModeCode _mode,
bytes calldata _executionCallData,
bytes32 _delegationHash,
address,
address
)
public
override
onlyBatchExecutionMode(_mode)
{
// Check delegation hasn't been used
if (usedDelegations[msg.sender][_delegationHash]) {
revert("SpecificActionERC20TransferBatchEnforcer:delegation-already-used");
}

// Mark delegation as used
usedDelegations[msg.sender][_delegationHash] = true;

// Decode the batch executions
Execution[] calldata executions_ = _executionCallData.decodeBatch();

// Validate batch size
if (executions_.length != 2) {
revert("SpecificActionERC20TransferBatchEnforcer:invalid-batch-size");
}

// Decode terms into struct
TermsData memory terms_ = getTermsInfo(_terms);

// Validate first transaction
if (
executions_[0].target != terms_.firstTarget || executions_[0].value != 0
|| keccak256(executions_[0].callData) != keccak256(terms_.firstCalldata)
) {
revert("SpecificActionERC20TransferBatchEnforcer:invalid-first-transaction");
}

// Validate second transaction
if (
executions_[1].target != terms_.tokenAddress || executions_[1].value != 0 || executions_[1].callData.length != 68
|| bytes4(executions_[1].callData[0:4]) != IERC20.transfer.selector
|| address(uint160(uint256(bytes32(executions_[1].callData[4:36])))) != terms_.recipient
|| uint256(bytes32(executions_[1].callData[36:68])) != terms_.amount
) {
revert("SpecificActionERC20TransferBatchEnforcer:invalid-second-transaction");
}

emit DelegationExecuted(msg.sender, _delegationHash);
}

/**
* @notice Decodes the terms used in this CaveatEnforcer
* @param _terms The encoded terms
* @return termsData_ The decoded terms data
*/
function getTermsInfo(bytes calldata _terms) public pure returns (TermsData memory termsData_) {
// Require minimum length: 20 + 20 + 32 + 20 = 92 bytes
require(_terms.length >= 92, "SpecificActionERC20TransferBatchEnforcer:invalid-terms-length");

// First 20 bytes is token address
termsData_.tokenAddress = address(bytes20(_terms[0:20]));

// Next 20 bytes is recipient address
termsData_.recipient = address(bytes20(_terms[20:40]));

// Next 32 bytes is amount
termsData_.amount = uint256(bytes32(_terms[40:72]));

// Next 20 bytes is first target
termsData_.firstTarget = address(bytes20(_terms[72:92]));

// Remaining bytes is firstCalldata
termsData_.firstCalldata = _terms[92:];
}
}
75 changes: 75 additions & 0 deletions test/DeleGatorTestSuite.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import { DeleGatorCore } from "../src/DeleGatorCore.sol";
import { EncoderLib } from "../src/libraries/EncoderLib.sol";
import { AllowedMethodsEnforcer } from "../src/enforcers/AllowedMethodsEnforcer.sol";
import { AllowedTargetsEnforcer } from "../src/enforcers/AllowedTargetsEnforcer.sol";
import { SpecificActionERC20TransferBatchEnforcer } from "../src/enforcers/SpecificActionERC20TransferBatchEnforcer.sol";
import { BasicERC20, IERC20 } from "./utils/BasicERC20.t.sol";

abstract contract DeleGatorTestSuite is BaseTest {
using ModeLib for ModeCode;
Expand All @@ -49,6 +51,7 @@ abstract contract DeleGatorTestSuite is BaseTest {
Counter aliceDeleGatorCounter;
Counter bobDeleGatorCounter;
ModeCode[] oneSingularMode;
ModeCode[] oneBatchMode;

function setUp() public virtual override {
super.setUp();
Expand All @@ -57,18 +60,28 @@ abstract contract DeleGatorTestSuite is BaseTest {
vm.label(address(allowedMethodsEnforcer), "Allowed Methods Enforcer");
allowedTargetsEnforcer = new AllowedTargetsEnforcer();
vm.label(address(allowedTargetsEnforcer), "Allowed Targets Enforcer");
specificActionEnforcer = new SpecificActionERC20TransferBatchEnforcer();
vm.label(address(specificActionEnforcer), "Specific Action ERC20 Transfer Batch Enforcer");

aliceDeleGatorCounter = new Counter(address(users.alice.deleGator));
bobDeleGatorCounter = new Counter(address(users.bob.deleGator));
token = new BasicERC20(address(users.alice.deleGator), "Test", "TST", 100 ether);

oneSingularMode = new ModeCode[](1);
oneSingularMode[0] = ModeLib.encodeSimpleSingle();

// Set up batch mode
oneBatchMode = new ModeCode[](1);
oneBatchMode[0] = ModeLib.encode(CALLTYPE_BATCH, EXECTYPE_DEFAULT, MODE_DEFAULT, ModePayload.wrap(0x00));
}

////////////////////////////// State //////////////////////////////

AllowedMethodsEnforcer public allowedMethodsEnforcer;
AllowedTargetsEnforcer public allowedTargetsEnforcer;
SpecificActionERC20TransferBatchEnforcer public specificActionEnforcer;
BasicERC20 public token;
uint256 public constant TRANSFER_AMOUNT = 10 ether;
bytes32 private DELEGATIONS_STORAGE_LOCATION = StorageUtilsLib.getStorageLocation("DeleGator.Delegations");
bytes32 private DELEGATOR_CORE_STORAGE_LOCATION = StorageUtilsLib.getStorageLocation("DeleGator.Core");
bytes32 private INITIALIZABLE_STORAGE_LOCATION = StorageUtilsLib.getStorageLocation("openzeppelin.storage.Initializable");
Expand Down Expand Up @@ -2242,6 +2255,68 @@ abstract contract DeleGatorTestSuite is BaseTest {
);
entryPoint.handleOps(userOps_, bundler);
}

// should allow a specific action ERC20 transfer batch through delegation
function test_allow_specificActionERC20TransferBatch() public {
// Create batch of executions
Execution[] memory executions = new Execution[](2);

// First execution: increment counter
bytes memory incrementCalldata = abi.encodeWithSelector(Counter.increment.selector);
executions[0] = Execution({ target: address(aliceDeleGatorCounter), value: 0, callData: incrementCalldata });

// Second execution: transfer tokens
executions[1] = Execution({
target: address(token),
value: 0,
callData: abi.encodeWithSelector(IERC20.transfer.selector, users.bob.addr, TRANSFER_AMOUNT)
});

// Create matching terms
bytes memory terms = abi.encodePacked(
address(token), // tokenAddress
users.bob.addr, // recipient
TRANSFER_AMOUNT, // amount
address(aliceDeleGatorCounter), // firstTarget
incrementCalldata // firstCalldata
);

// Create delegation from Alice to Bob with the SpecificActionERC20TransferBatchEnforcer caveat
Caveat[] memory caveats = new Caveat[](1);
caveats[0] = Caveat({ enforcer: address(specificActionEnforcer), terms: terms, args: hex"" });

Delegation memory delegation = Delegation({
delegate: users.bob.addr,
delegator: address(users.alice.deleGator),
authority: ROOT_AUTHORITY,
caveats: caveats,
salt: 0,
signature: hex""
});

delegation = signDelegation(users.alice, delegation);

// Record initial states
uint256 initialCount = aliceDeleGatorCounter.count();
uint256 initialBalance = token.balanceOf(users.bob.addr);

// Prepare delegation redemption parameters
bytes[] memory permissionContexts = new bytes[](1);
Delegation[] memory delegations = new Delegation[](1);
delegations[0] = delegation;
permissionContexts[0] = abi.encode(delegations);

bytes[] memory executionCallDatas = new bytes[](1);
executionCallDatas[0] = ExecutionLib.encodeBatch(executions);

// Bob redeems the delegation to execute the batch
vm.prank(users.bob.addr);
delegationManager.redeemDelegations(permissionContexts, oneBatchMode, executionCallDatas);

// Verify states changed correctly
assertEq(aliceDeleGatorCounter.count(), initialCount + 1);
assertEq(token.balanceOf(users.bob.addr), initialBalance + TRANSFER_AMOUNT);
}
}

abstract contract UUPSDeleGatorTest is DeleGatorTestSuite {
Expand Down
Loading

0 comments on commit 15b9d4b

Please sign in to comment.