From 30d3350e26eba4b555abda695becf21b8bfe6a19 Mon Sep 17 00:00:00 2001 From: Dylan DesRosier Date: Thu, 31 Oct 2024 14:36:15 +0000 Subject: [PATCH] feat: Add new enforcers and tests --- src/enforcers/NoCalldataEnforcer.sol | 52 ++++++++ src/enforcers/NoValueEnforcer.sol | 52 ++++++++ test/enforcers/AllowedCalldataEnforcer.t.sol | 2 +- test/enforcers/NoCalldataEnforcer.t.sol | 118 +++++++++++++++++++ test/enforcers/NoValueEnforcer.t.sol | 110 +++++++++++++++++ 5 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 src/enforcers/NoCalldataEnforcer.sol create mode 100644 src/enforcers/NoValueEnforcer.sol create mode 100644 test/enforcers/NoCalldataEnforcer.t.sol create mode 100644 test/enforcers/NoValueEnforcer.t.sol diff --git a/src/enforcers/NoCalldataEnforcer.sol b/src/enforcers/NoCalldataEnforcer.sol new file mode 100644 index 0000000..05f0b44 --- /dev/null +++ b/src/enforcers/NoCalldataEnforcer.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; + +import { CaveatEnforcer } from "./CaveatEnforcer.sol"; +import { Execution, ModeCode } from "../utils/Types.sol"; +import { CALLTYPE_SINGLE, CALLTYPE_BATCH } from "../utils/Constants.sol"; + +/** + * @title NoCalldataEnforcer + * @dev This contract enforces that the execution has no calldata. + * @dev This caveat enforcer only works when the execution is in single mode. + */ +contract NoCalldataEnforcer is CaveatEnforcer { + using ExecutionLib for bytes; + + ////////////////////////////// Public Methods ////////////////////////////// + + /** + * @notice Allows the delegator to restrict the calldata that is executed + * @dev This function enforces that the execution has no calldata. + * @param _mode The execution mode for the execution. + * @param _executionCallData The execution the delegate is trying try to execute. + */ + function beforeHook( + bytes calldata, + bytes calldata, + ModeCode _mode, + bytes calldata _executionCallData, + bytes32, + address, + address + ) + public + pure + override + { + if (ModeLib.getCallType(_mode) == CALLTYPE_SINGLE) { + (,, bytes calldata callData_) = _executionCallData.decodeSingle(); + require(callData_.length == 0, "NoCalldataEnforcer:calldata-not-allowed"); + } else if (ModeLib.getCallType(_mode) == CALLTYPE_BATCH) { + (Execution[] calldata executions_) = _executionCallData.decodeBatch(); + for (uint256 i = 0; i < executions_.length; i++) { + require(executions_[i].callData.length == 0, "NoCalldataEnforcer:calldata-not-allowed"); + } + } else { + revert("NoCalldataEnforcer:invalid-calltype"); + } + } +} diff --git a/src/enforcers/NoValueEnforcer.sol b/src/enforcers/NoValueEnforcer.sol new file mode 100644 index 0000000..b78fdce --- /dev/null +++ b/src/enforcers/NoValueEnforcer.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; + +import { CaveatEnforcer } from "./CaveatEnforcer.sol"; +import { Execution, ModeCode } from "../utils/Types.sol"; +import { CALLTYPE_SINGLE, CALLTYPE_BATCH } from "../utils/Constants.sol"; + +/** + * @title NoValueEnforcer + * @dev This contract enforces that the execution has no value. + * @dev This caveat enforcer only works when the execution is in single mode. + */ +contract NoValueEnforcer is CaveatEnforcer { + using ExecutionLib for bytes; + + ////////////////////////////// Public Methods ////////////////////////////// + + /** + * @notice Allows the delegator to restrict the value that is executed + * @dev This function enforces that the execution has no value. + * @param _mode The execution mode for the execution. + * @param _executionCallData The execution the delegate is trying try to execute. + */ + function beforeHook( + bytes calldata, + bytes calldata, + ModeCode _mode, + bytes calldata _executionCallData, + bytes32, + address, + address + ) + public + pure + override + { + if (ModeLib.getCallType(_mode) == CALLTYPE_SINGLE) { + (, uint256 value_,) = _executionCallData.decodeSingle(); + require(value_ == 0, "NoValueEnforcer:value-not-allowed"); + } else if (ModeLib.getCallType(_mode) == CALLTYPE_BATCH) { + (Execution[] calldata executions_) = _executionCallData.decodeBatch(); + for (uint256 i = 0; i < executions_.length; i++) { + require(executions_[i].value == 0, "NoValueEnforcer:value-not-allowed"); + } + } else { + revert("NoValueEnforcer:invalid-calltype"); + } + } +} diff --git a/test/enforcers/AllowedCalldataEnforcer.t.sol b/test/enforcers/AllowedCalldataEnforcer.t.sol index b845486..e96a29c 100644 --- a/test/enforcers/AllowedCalldataEnforcer.t.sol +++ b/test/enforcers/AllowedCalldataEnforcer.t.sol @@ -36,7 +36,7 @@ contract AllowedCalldataEnforcerTest is CaveatEnforcerBaseTest { function setUp() public override { super.setUp(); allowedCalldataEnforcer = new AllowedCalldataEnforcer(); - vm.label(address(allowedCalldataEnforcer), "Equal Parameters Enforcer"); + vm.label(address(allowedCalldataEnforcer), "Allowed Calldata Enforcer"); basicCF20 = new BasicERC20(address(users.alice.deleGator), "TestToken1", "TestToken1", 100 ether); basicCF721 = new BasicCF721(address(users.alice.deleGator), "TestNFT", "TestNFT", ""); } diff --git a/test/enforcers/NoCalldataEnforcer.t.sol b/test/enforcers/NoCalldataEnforcer.t.sol new file mode 100644 index 0000000..6492f5d --- /dev/null +++ b/test/enforcers/NoCalldataEnforcer.t.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import "forge-std/Test.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { BytesLib } from "@bytes-utils/BytesLib.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; + +import { Counter } from "../utils/Counter.t.sol"; +import { Execution, Caveat, Delegation, ModeCode } from "../../src/utils/Types.sol"; +import { CaveatEnforcerBaseTest } from "./CaveatEnforcerBaseTest.t.sol"; +import { NoCalldataEnforcer } from "../../src/enforcers/NoCalldataEnforcer.sol"; +import { IDelegationManager } from "../../src/interfaces/IDelegationManager.sol"; +import { EncoderLib } from "../../src/libraries/EncoderLib.sol"; +import { BasicERC20, IERC20 } from "../utils/BasicERC20.t.sol"; +import { BasicCF721 } from "../utils/BasicCF721.t.sol"; +import { ICaveatEnforcer } from "../../src/interfaces/ICaveatEnforcer.sol"; + +contract DummyContract { + function stringFn(uint256[] calldata _str) public { } + function arrayFn(string calldata _str) public { } +} + +contract NoCalldataEnforcerTest is CaveatEnforcerBaseTest { + using ModeLib for ModeCode; + + ////////////////////////////// State ////////////////////////////// + NoCalldataEnforcer public noCalldataEnforcer; + DummyContract public c; + + ModeCode public singleMode = ModeLib.encodeSimpleSingle(); + ModeCode public batchMode = ModeLib.encodeSimpleBatch(); + + ////////////////////// Set up ////////////////////// + + function setUp() public override { + super.setUp(); + noCalldataEnforcer = new NoCalldataEnforcer(); + vm.label(address(noCalldataEnforcer), "No Calldata Enforcer"); + c = new DummyContract(); + } + + ////////////////////// Valid cases ////////////////////// + + // should allow an execution in single mode with no calldata + function test_singleMethodNoCalldataIsAllowed() public { + // Create the execution that would be executed + Execution memory execution_ = Execution({ target: address(c), value: 0, callData: hex"" }); + bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData); + + vm.prank(address(delegationManager)); + noCalldataEnforcer.beforeHook(hex"", hex"", singleMode, executionCallData_, keccak256(""), address(0), address(0)); + } + + // should allow an execution in batch mode with no calldata + function test_batchMethodNoCalldataIsAllowed() public { + // Create the execution that would be executed + Execution memory execution_ = Execution({ target: address(c), value: 0, callData: hex"" }); + Execution[] memory executions_ = new Execution[](2); + executions_[0] = execution_; + executions_[1] = execution_; + bytes memory executionCallData_ = ExecutionLib.encodeBatch(executions_); + + vm.prank(address(delegationManager)); + noCalldataEnforcer.beforeHook(hex"", hex"", batchMode, executionCallData_, keccak256(""), address(0), address(0)); + } + + ////////////////////// Invalid cases ////////////////////// + + // should not allow an execution in single mode with calldata + function test_singleMethodCalldataIsNotAllowed() public { + // Create the execution that would be executed + Execution memory execution_ = Execution({ + target: address(c), + value: 0, + callData: abi.encodeWithSelector(DummyContract.stringFn.selector, uint256(1)) + }); + bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData); + + vm.prank(address(delegationManager)); + vm.expectRevert("NoCalldataEnforcer:calldata-not-allowed"); + noCalldataEnforcer.beforeHook(hex"", hex"", singleMode, executionCallData_, keccak256(""), address(0), address(0)); + } + + // should not allow an execution in batch mode with calldata + function test_batchMethodCalldataIsNotAllowed() public { + // Create the execution that would be executed + Execution memory execution_ = Execution({ + target: address(c), + value: 0, + callData: abi.encodeWithSelector(DummyContract.stringFn.selector, uint256(1)) + }); + Execution[] memory executions_ = new Execution[](2); + executions_[0] = execution_; + executions_[1] = execution_; + bytes memory executionCallData_ = ExecutionLib.encodeBatch(executions_); + + vm.prank(address(delegationManager)); + vm.expectRevert("NoCalldataEnforcer:calldata-not-allowed"); + noCalldataEnforcer.beforeHook(hex"", hex"", batchMode, executionCallData_, keccak256(""), address(0), address(0)); + + // Make a subset of the executions have no calldata + execution_ = Execution({ target: address(c), value: 0, callData: hex"" }); + executions_[0] = execution_; + executionCallData_ = ExecutionLib.encodeBatch(executions_); + + vm.prank(address(delegationManager)); + vm.expectRevert("NoCalldataEnforcer:calldata-not-allowed"); + noCalldataEnforcer.beforeHook(hex"", hex"", batchMode, executionCallData_, keccak256(""), address(0), address(0)); + } + + ////////////////////// Integration ////////////////////// + + function _getEnforcer() internal view override returns (ICaveatEnforcer) { + return ICaveatEnforcer(address(noCalldataEnforcer)); + } +} diff --git a/test/enforcers/NoValueEnforcer.t.sol b/test/enforcers/NoValueEnforcer.t.sol new file mode 100644 index 0000000..9b06d0b --- /dev/null +++ b/test/enforcers/NoValueEnforcer.t.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import "forge-std/Test.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { BytesLib } from "@bytes-utils/BytesLib.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; + +import { Counter } from "../utils/Counter.t.sol"; +import { Execution, Caveat, Delegation, ModeCode } from "../../src/utils/Types.sol"; +import { CaveatEnforcerBaseTest } from "./CaveatEnforcerBaseTest.t.sol"; +import { NoValueEnforcer } from "../../src/enforcers/NoValueEnforcer.sol"; +import { IDelegationManager } from "../../src/interfaces/IDelegationManager.sol"; +import { EncoderLib } from "../../src/libraries/EncoderLib.sol"; +import { BasicERC20, IERC20 } from "../utils/BasicERC20.t.sol"; +import { BasicCF721 } from "../utils/BasicCF721.t.sol"; +import { ICaveatEnforcer } from "../../src/interfaces/ICaveatEnforcer.sol"; + +contract DummyContract { + function stringFn(uint256[] calldata _str) public { } + function arrayFn(string calldata _str) public { } +} + +contract NoValueEnforcerTest is CaveatEnforcerBaseTest { + using ModeLib for ModeCode; + + ////////////////////////////// State ////////////////////////////// + NoValueEnforcer public noValueEnforcer; + DummyContract public c; + + ModeCode public singleMode = ModeLib.encodeSimpleSingle(); + ModeCode public batchMode = ModeLib.encodeSimpleBatch(); + + ////////////////////// Set up ////////////////////// + + function setUp() public override { + super.setUp(); + noValueEnforcer = new NoValueEnforcer(); + vm.label(address(noValueEnforcer), "No Value Enforcer"); + c = new DummyContract(); + } + + ////////////////////// Valid cases ////////////////////// + + // should allow an execution in single mode with no calldata + function test_singleMethodNoCalldataIsAllowed() public { + // Create the execution that would be executed + Execution memory execution_ = Execution({ target: address(c), value: 0, callData: hex"" }); + bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData); + + vm.prank(address(delegationManager)); + noValueEnforcer.beforeHook(hex"", hex"", singleMode, executionCallData_, keccak256(""), address(0), address(0)); + } + + // should allow an execution in batch mode with no calldata + function test_batchMethodNoCalldataIsAllowed() public { + // Create the execution that would be executed + Execution memory execution_ = Execution({ target: address(c), value: 0, callData: hex"" }); + Execution[] memory executions_ = new Execution[](2); + executions_[0] = execution_; + executions_[1] = execution_; + bytes memory executionCallData_ = ExecutionLib.encodeBatch(executions_); + + vm.prank(address(delegationManager)); + noValueEnforcer.beforeHook(hex"", hex"", batchMode, executionCallData_, keccak256(""), address(0), address(0)); + } + + ////////////////////// Invalid cases ////////////////////// + + // should not allow an execution in single mode with calldata + function test_singleMethodCalldataIsNotAllowed() public { + // Create the execution that would be executed + Execution memory execution_ = Execution({ target: address(c), value: 1, callData: hex"" }); + bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData); + + vm.prank(address(delegationManager)); + vm.expectRevert("NoValueEnforcer:value-not-allowed"); + noValueEnforcer.beforeHook(hex"", hex"", singleMode, executionCallData_, keccak256(""), address(0), address(0)); + } + + // should not allow an execution in batch mode with calldata + function test_batchMethodCalldataIsNotAllowed() public { + // Create the execution that would be executed + Execution memory execution_ = Execution({ target: address(c), value: 1, callData: hex"" }); + Execution[] memory executions_ = new Execution[](2); + executions_[0] = execution_; + executions_[1] = execution_; + bytes memory executionCallData_ = ExecutionLib.encodeBatch(executions_); + + vm.prank(address(delegationManager)); + vm.expectRevert("NoValueEnforcer:value-not-allowed"); + noValueEnforcer.beforeHook(hex"", hex"", batchMode, executionCallData_, keccak256(""), address(0), address(0)); + + // Make a subset of the executions have no calldata + execution_ = Execution({ target: address(c), value: 0, callData: hex"" }); + executions_[0] = execution_; + executionCallData_ = ExecutionLib.encodeBatch(executions_); + + vm.prank(address(delegationManager)); + vm.expectRevert("NoValueEnforcer:value-not-allowed"); + noValueEnforcer.beforeHook(hex"", hex"", batchMode, executionCallData_, keccak256(""), address(0), address(0)); + } + + ////////////////////// Integration ////////////////////// + + function _getEnforcer() internal view override returns (ICaveatEnforcer) { + return ICaveatEnforcer(address(noValueEnforcer)); + } +}