Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions script/DeployCaveatEnforcers.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { ERC20TransferAmountEnforcer } from "../src/enforcers/ERC20TransferAmoun
import { ERC721BalanceGteEnforcer } from "../src/enforcers/ERC721BalanceGteEnforcer.sol";
import { ERC721TransferEnforcer } from "../src/enforcers/ERC721TransferEnforcer.sol";
import { ERC1155BalanceGteEnforcer } from "../src/enforcers/ERC1155BalanceGteEnforcer.sol";
import { ExactCalldataEnforcer } from "../src/enforcers/ExactCalldataEnforcer.sol";
import { IdEnforcer } from "../src/enforcers/IdEnforcer.sol";
import { LimitedCallsEnforcer } from "../src/enforcers/LimitedCallsEnforcer.sol";
import { NativeBalanceGteEnforcer } from "../src/enforcers/NativeBalanceGteEnforcer.sol";
Expand All @@ -26,6 +27,7 @@ import { NativeTokenStreamingEnforcer } from "../src/enforcers/NativeTokenStream
import { NonceEnforcer } from "../src/enforcers/NonceEnforcer.sol";
import { OwnershipTransferEnforcer } from "../src/enforcers/OwnershipTransferEnforcer.sol";
import { RedeemerEnforcer } from "../src/enforcers/RedeemerEnforcer.sol";
import { SpecificActionERC20TransferBatchEnforcer } from "../src/enforcers/SpecificActionERC20TransferBatchEnforcer.sol";
import { ERC20StreamingEnforcer } from "../src/enforcers/ERC20StreamingEnforcer.sol";
import { TimestampEnforcer } from "../src/enforcers/TimestampEnforcer.sol";
import { ValueLteEnforcer } from "../src/enforcers/ValueLteEnforcer.sol";
Expand Down Expand Up @@ -81,6 +83,9 @@ contract DeployCaveatEnforcers is Script {
deployedAddress = address(new ERC20TransferAmountEnforcer{ salt: salt }());
console2.log("ERC20TransferAmountEnforcer: %s", deployedAddress);

deployedAddress = address(new ERC20StreamingEnforcer{ salt: salt }());
console2.log("ERC20StreamingEnforcer: %s", deployedAddress);

deployedAddress = address(new ERC721BalanceGteEnforcer{ salt: salt }());
console2.log("ERC721BalanceGteEnforcer: %s", deployedAddress);

Expand All @@ -90,6 +95,9 @@ contract DeployCaveatEnforcers is Script {
deployedAddress = address(new ERC1155BalanceGteEnforcer{ salt: salt }());
console2.log("ERC1155BalanceGteEnforcer: %s", deployedAddress);

deployedAddress = address(new ExactCalldataEnforcer{ salt: salt }());
console2.log("ExactCalldataEnforcer: %s", deployedAddress);

deployedAddress = address(new IdEnforcer{ salt: salt }());
console2.log("IdEnforcer: %s", deployedAddress);

Expand Down Expand Up @@ -121,8 +129,8 @@ contract DeployCaveatEnforcers is Script {
deployedAddress = address(new RedeemerEnforcer{ salt: salt }());
console2.log("RedeemerEnforcer: %s", deployedAddress);

deployedAddress = address(new ERC20StreamingEnforcer{ salt: salt }());
console2.log("ERC20StreamingEnforcer: %s", deployedAddress);
deployedAddress = address(new SpecificActionERC20TransferBatchEnforcer{ salt: salt }());
console2.log("SpecificActionERC20TransferBatchEnforcer: %s", deployedAddress);

deployedAddress = address(new TimestampEnforcer{ salt: salt }());
console2.log("TimestampEnforcer: %s", deployedAddress);
Expand Down
54 changes: 54 additions & 0 deletions src/enforcers/ExactCalldataEnforcer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: MIT AND Apache-2.0
pragma solidity 0.8.23;

import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol";

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

/**
* @title ExactCalldataEnforcer
* @notice Ensures that the provided execution calldata matches exactly the expected calldata.
* @dev This caveat enforcer operates only in single execution mode.
*/
contract ExactCalldataEnforcer is CaveatEnforcer {
using ExecutionLib for bytes;

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

/**
* @notice Validates that the execution calldata matches the expected calldata.
* @param _terms The encoded expected calldata.
* @param _mode The execution mode, which must be single.
* @param _executionCallData The calldata provided for execution.
*/
function beforeHook(
bytes calldata _terms,
bytes calldata,
ModeCode _mode,
bytes calldata _executionCallData,
bytes32,
address,
address
)
public
pure
override
onlySingleExecutionMode(_mode)
{
(,, bytes calldata callData_) = _executionCallData.decodeSingle();

bytes memory termsCallData_ = getTermsInfo(_terms);

require(keccak256(termsCallData_) == keccak256(callData_), "ExactCalldataEnforcer:invalid-calldata");
}

/**
* @notice Extracts the expected calldata from the provided terms.
* @param _terms The encoded expected calldata.
* @return callData_ The expected calldata for comparison.
*/
function getTermsInfo(bytes calldata _terms) public pure returns (bytes memory callData_) {
callData_ = _terms;
}
}
296 changes: 296 additions & 0 deletions test/enforcers/ExactCalldataEnforcer.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
// SPDX-License-Identifier: MIT AND Apache-2.0
pragma solidity 0.8.23;

import "forge-std/Test.sol";
import { ModeLib } from "@erc7579/lib/ModeLib.sol";
import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol";

import { Execution, Caveat, Delegation, ModeCode } from "../../src/utils/Types.sol";
import { CaveatEnforcerBaseTest } from "./CaveatEnforcerBaseTest.t.sol";
import { ExactCalldataEnforcer } from "../../src/enforcers/ExactCalldataEnforcer.sol";
import { BasicERC20, IERC20 } from "../utils/BasicERC20.t.sol";
import { ICaveatEnforcer } from "../../src/interfaces/ICaveatEnforcer.sol";

contract ExactCalldataEnforcerTest is CaveatEnforcerBaseTest {
using ModeLib for ModeCode;

////////////////////////////// State //////////////////////////////
ExactCalldataEnforcer public exactCalldataEnforcer;
BasicERC20 public basicCF20;
ModeCode public mode = ModeLib.encodeSimpleSingle();

////////////////////////////// Setup //////////////////////////////
function setUp() public override {
super.setUp();
exactCalldataEnforcer = new ExactCalldataEnforcer();
vm.label(address(exactCalldataEnforcer), "Exact Calldata Enforcer");
basicCF20 = new BasicERC20(address(users.alice.deleGator), "TestToken1", "TestToken1", 100 ether);
}

////////////////////////////// Unit Tests //////////////////////////////

/// @notice Test that the enforcer passes when the expected calldata exactly matches the executed calldata.
function test_exactCalldataMatches() public {
// Create an execution (for example, a mint on the ERC20 token)
Execution memory execution_ = Execution({
target: address(basicCF20),
value: 0,
callData: abi.encodeWithSelector(BasicERC20.mint.selector, address(users.alice.deleGator), uint256(100))
});
bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData);

// Use the exact callData as the expected terms
bytes memory terms_ = execution_.callData;

vm.prank(address(delegationManager));
exactCalldataEnforcer.beforeHook(terms_, hex"", mode, executionCallData_, keccak256(""), address(0), address(0));
}

/// @notice Test that the enforcer reverts when the executed calldata does not exactly match the expected calldata.
function test_exactCalldataFailsWhenMismatch() public {
Execution memory execution_ = Execution({
target: address(basicCF20),
value: 0,
callData: abi.encodeWithSelector(BasicERC20.mint.selector, address(users.alice.deleGator), uint256(100))
});
bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData);

// Terms to simulate a mismatch
bytes memory terms_ = abi.encodeWithSelector(IERC20.transfer.selector, address(0), uint256(100));

vm.prank(address(delegationManager));
vm.expectRevert("ExactCalldataEnforcer:invalid-calldata");
exactCalldataEnforcer.beforeHook(terms_, hex"", mode, executionCallData_, keccak256(""), address(0), address(0));
}

/// @notice Test that the enforcer works correctly with a dynamic array parameter.
function test_equalDynamicArrayParam() public {
uint256[] memory param = new uint256[](2);
param[0] = 1;
param[1] = 2;
Execution memory execution_ = Execution({
target: address(0), // Dummy target for testing
value: 0,
callData: abi.encodeWithSelector(DummyContract.arrayFn.selector, param)
});
bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData);

bytes memory terms_ = execution_.callData;

vm.prank(address(delegationManager));
exactCalldataEnforcer.beforeHook(terms_, hex"", mode, executionCallData_, keccak256(""), address(0), address(0));
}

/// @notice Test that the enforcer works correctly with a dynamic string parameter.
function test_equalDynamicStringParam() public {
string memory param_ = "Test string";
Execution memory execution_ =
Execution({ target: address(0), value: 0, callData: abi.encodeWithSelector(DummyContract.stringFn.selector, param_) });
bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData);

bytes memory terms_ = execution_.callData;

vm.prank(address(delegationManager));
exactCalldataEnforcer.beforeHook(terms_, hex"", mode, executionCallData_, keccak256(""), address(0), address(0));
}

/// @notice Test that the enforcer passes when both expected and execution calldata are empty (ETH transfer).
function test_emptyCalldataMatches() public {
// Create an ETH transfer execution with empty calldata.
Execution memory execution_ = Execution({ target: address(0x1234), value: 1 ether, callData: "" });
bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData);
// Expected terms: empty calldata.
bytes memory terms_ = "";

vm.prank(address(delegationManager));
exactCalldataEnforcer.beforeHook(terms_, hex"", mode, executionCallData_, keccak256(""), address(0), address(0));
}

/// @notice Test that the enforcer reverts when expected calldata is empty but execution calldata is non-empty.
function test_emptyCalldataFailsWhenMismatch() public {
// Create an ETH transfer execution with non-empty calldata.
Execution memory execution_ = Execution({ target: address(0x1234), value: 1 ether, callData: hex"abcd" });
bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData);
// Expected terms: empty calldata.
bytes memory terms_ = "";

vm.prank(address(delegationManager));
vm.expectRevert("ExactCalldataEnforcer:invalid-calldata");
exactCalldataEnforcer.beforeHook(terms_, hex"", mode, executionCallData_, keccak256(""), address(0), address(0));
}

/// @notice Test that the enforcer reverts when batch-encoded execution calldata is provided.
function test_batchEncodedExecutionReverts() public {
// Batch encode the two executions.
Execution[] memory executions_ = new Execution[](2);
bytes memory batchEncodedCallData_ = ExecutionLib.encodeBatch(executions_);

// Irrelevant because the batch decoding will fail)
bytes memory terms_ = hex"";

vm.prank(address(delegationManager));
// Expect a revert because the enforcer calls decodeSingle() on batch encoded calldata.
vm.expectRevert();
exactCalldataEnforcer.beforeHook(terms_, hex"", mode, batchEncodedCallData_, keccak256(""), address(0), address(0));
}

////////////////////////////// Integration Tests //////////////////////////////

/// @notice Integration test: the enforcer allows a token transfer delegation when calldata matches exactly.
function test_integration_AllowsTokenTransferWhenCalldataMatches() public {
// Ensure Bob starts with a zero balance.
assertEq(basicCF20.balanceOf(address(users.bob.deleGator)), uint256(0));

// Create an execution for a token transfer of 1 unit.
Execution memory execution_ = Execution({
target: address(basicCF20),
value: 0,
callData: abi.encodeWithSelector(IERC20.transfer.selector, address(users.bob.deleGator), uint256(1 ether))
});

// Use the actual callData as the expected terms.
bytes memory terms_ = execution_.callData;

Caveat[] memory caveats_ = new Caveat[](1);
caveats_[0] = Caveat({ args: hex"", enforcer: address(exactCalldataEnforcer), terms: terms_ });
Delegation memory delegation_ = Delegation({
delegate: address(users.bob.deleGator),
delegator: address(users.alice.deleGator),
authority: ROOT_AUTHORITY,
caveats: caveats_,
salt: 0,
signature: hex""
});
delegation_ = signDelegation(users.alice, delegation_);

Delegation[] memory delegations_ = new Delegation[](1);
delegations_[0] = delegation_;

// Execute Bob's UserOp twice to demonstrate reusability.
invokeDelegation_UserOp(users.bob, delegations_, execution_);
assertEq(basicCF20.balanceOf(address(users.bob.deleGator)), uint256(1 ether));

invokeDelegation_UserOp(users.bob, delegations_, execution_);
assertEq(basicCF20.balanceOf(address(users.bob.deleGator)), uint256(2 ether));
}

/// @notice Integration test: the enforcer blocks delegation execution when calldata does not match.
function test_integration_BlocksTokenTransferWhenCalldataDiffers() public {
assertEq(basicCF20.balanceOf(address(users.bob.deleGator)), uint256(0));

// Create an execution for a token transfer of 2 units.
Execution memory execution_ = Execution({
target: address(basicCF20),
value: 0,
callData: abi.encodeWithSelector(IERC20.transfer.selector, address(users.bob.deleGator), uint256(2 ether))
});

// Use expected terms that differ (e.g. a valid callData for a transfer of 1 unit).
bytes memory validCallData_ =
abi.encodeWithSelector(IERC20.transfer.selector, address(users.bob.deleGator), uint256(1 ether));
bytes memory terms_ = validCallData_;

Caveat[] memory caveats_ = new Caveat[](1);
caveats_[0] = Caveat({ args: hex"", enforcer: address(exactCalldataEnforcer), terms: terms_ });
Delegation memory delegation_ = Delegation({
delegate: address(users.bob.deleGator),
delegator: address(users.alice.deleGator),
authority: ROOT_AUTHORITY,
caveats: caveats_,
salt: 0,
signature: hex""
});
delegation_ = signDelegation(users.alice, delegation_);

Delegation[] memory delegations_ = new Delegation[](1);
delegations_[0] = delegation_;

invokeDelegation_UserOp(users.bob, delegations_, execution_);

// Verify that Bob's balance remains unchanged.
assertEq(basicCF20.balanceOf(address(users.bob.deleGator)), uint256(0));
}

/// @notice Integration test: ExactCalldataEnforcer allows ETH transfer when both expected and execution calldata are empty.
function test_integration_AllowsETHTransferWhenEmptyCalldataMatches() public {
// Record Carol's initial ETH balance.
uint256 initialBalance = address(users.carol.deleGator).balance;

// Create an execution for an ETH transfer with empty calldata and a non-zero value.
Execution memory execution_ = Execution({
target: address(users.carol.deleGator),
value: 10 ether,
callData: "" // Empty calldata for ETH transfer
});
// Expected terms: empty calldata.
bytes memory terms_ = "";

Caveat[] memory caveats_ = new Caveat[](1);
caveats_[0] = Caveat({ args: hex"", enforcer: address(exactCalldataEnforcer), terms: terms_ });
Delegation memory delegation_ = Delegation({
delegate: address(users.bob.deleGator),
delegator: address(users.alice.deleGator),
authority: ROOT_AUTHORITY,
caveats: caveats_,
salt: 0,
signature: hex""
});
delegation_ = signDelegation(users.alice, delegation_);

Delegation[] memory delegations_ = new Delegation[](1);
delegations_[0] = delegation_;

// Execute the delegation; Bob submits the UserOp.
invokeDelegation_UserOp(users.bob, delegations_, execution_);

// Verify that Carol's ETH balance increased by 10 ether.
assertEq(address(users.carol.deleGator).balance, initialBalance + 10 ether);
}

/// @notice Integration test: ExactCalldataEnforcer blocks ETH transfer when expected calldata is empty but execution calldata
/// is non-empty.
function test_integration_BlocksETHTransferWhenEmptyCalldataDiffers() public {
uint256 initialBalance_ = address(users.carol.deleGator).balance;

// Create an execution for an ETH transfer with non-empty calldata.
Execution memory execution_ = Execution({
target: address(users.carol.deleGator),
value: 1 ether,
callData: hex"abcd" // Non-empty calldata
});
// Expected terms: empty calldata.
bytes memory terms_ = "";

Caveat[] memory caveats_ = new Caveat[](1);
caveats_[0] = Caveat({ args: hex"", enforcer: address(exactCalldataEnforcer), terms: terms_ });
Delegation memory delegation_ = Delegation({
delegate: address(users.bob.deleGator),
delegator: address(users.alice.deleGator),
authority: ROOT_AUTHORITY,
caveats: caveats_,
salt: 0,
signature: hex""
});
delegation_ = signDelegation(users.alice, delegation_);

Delegation[] memory delegations_ = new Delegation[](1);
delegations_[0] = delegation_;

// Expect the execution to revert due to calldata mismatch.
invokeDelegation_UserOp(users.bob, delegations_, execution_);

// Verify that Carol's ETH balance remains unchanged.
assertEq(address(users.carol.deleGator).balance, initialBalance_);
}

////////////////////////////// Internal Overrides //////////////////////////////
function _getEnforcer() internal view override returns (ICaveatEnforcer) {
return ICaveatEnforcer(address(exactCalldataEnforcer));
}
}

/// @dev A dummy contract used for testing dynamic calldata parameters.
contract DummyContract {
function arrayFn(uint256[] calldata _str) public { }
function stringFn(string calldata _str) public { }
}