From 653845a72d353a617d44d96e3dcac0a2fa02d075 Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Mon, 9 Jun 2025 21:11:47 -0600 Subject: [PATCH 1/5] feat: adds ERC1155TransferEnforcer --- src/enforcers/ERC1155TransferEnforcer.sol | 159 ++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 src/enforcers/ERC1155TransferEnforcer.sol diff --git a/src/enforcers/ERC1155TransferEnforcer.sol b/src/enforcers/ERC1155TransferEnforcer.sol new file mode 100644 index 00000000..ac1a7622 --- /dev/null +++ b/src/enforcers/ERC1155TransferEnforcer.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import { CaveatEnforcer } from "./CaveatEnforcer.sol"; +import { ModeCode } from "../utils/Types.sol"; +import { IERC1155 } from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; + +/** + * @title ERC1155TransferEnforcer + * @notice This enforcer restricts the execution to the transfer of specific ERC1155 tokens. + * @dev This enforcer operates only in single execution call type and with default execution mode. + * Supports both single and batch transfers. The terms include a boolean flag indicating the transfer type. + */ +contract ERC1155TransferEnforcer is CaveatEnforcer { + bytes4 private constant SAFE_BATCH_TRANSFER_FROM_SELECTOR = + bytes4(keccak256("safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)")); + + /** + * @notice Enforces that the contract and tokenIds are permitted for transfer + * @param _terms abi encoded (bool isBatch, address contract, uint256[] tokenIds) + * @param _mode The execution mode. (Must be Single callType, Default execType) + * @param _executionCallData the call data of the transferFrom call + */ + function beforeHook( + bytes calldata _terms, + bytes calldata, + ModeCode _mode, + bytes calldata _executionCallData, + bytes32, + address, + address + ) + public + virtual + override + onlySingleCallTypeMode(_mode) + onlyDefaultExecutionMode(_mode) + { + _validateTransfer(_terms, _executionCallData); + } + + /** + * @notice Decodes the terms to get the transfer type, permitted contract and token IDs + * @param _terms The encoded terms containing transfer type, contract address and token IDs + * @return _isBatch The transfer type flag + * @return _permittedContract The address of the permitted ERC1155 contract + * @return _ids Array of token IDs + * @return _values Array of token amounts + */ + function getTermsInfo(bytes calldata _terms) + public + pure + returns (bool _isBatch, address _permittedContract, uint256[] memory _ids, uint256[] memory _values) + { + if (_isBatch) { + (_isBatch, _permittedContract, _ids, _values) = abi.decode(_terms, (bool, address, uint256[], uint256[])); + } else { + uint256 id_; + uint256 value_; + _ids = new uint256[](1); + _values = new uint256[](1); + (_isBatch, _permittedContract, id_, value_) = abi.decode(_terms, (bool, address, uint256, uint256)); + _ids[0] = id_; + _values[0] = value_; + } + + if (_ids.length != _values.length) revert("ERC1155TransferEnforcer:invalid-ids-values-length"); + } + + /** + * @notice Validates that the transfer execution matches the permitted terms + * @dev Checks that the contract, token IDs and amounts match what is permitted in the terms + * @param _terms The encoded terms containing transfer type, contract address, token IDs and amounts + * @param _executionCallData The encoded execution data containing the transfer details + */ + function _validateTransfer(bytes calldata _terms, bytes calldata _executionCallData) internal pure { + (bool isBatch_, address permittedContract_, uint256[] memory permittedTokenIds_, uint256[] memory permittedValues_) = + getTermsInfo(_terms); + (address target_, uint256 value_, bytes calldata callData_) = ExecutionLib.decodeSingle(_executionCallData); + + if (value_ != 0) revert("ERC1155TransferEnforcer:invalid-value"); + + if (callData_.length < 4) revert("ERC1155TransferEnforcer:invalid-calldata-length"); + + if (target_ != permittedContract_) { + revert("ERC1155TransferEnforcer:unauthorized-contract-target"); + } + + bytes4 selector_ = bytes4(callData_[0:4]); + if (isBatch_ && selector_ != SAFE_BATCH_TRANSFER_FROM_SELECTOR) { + revert("ERC1155TransferEnforcer:unauthorized-selector-batch"); + } else if (!isBatch_ && selector_ != IERC1155.safeTransferFrom.selector) { + revert("ERC1155TransferEnforcer:unauthorized-selector-single"); + } + + if (isBatch_) { + // Batch transfer + (address from_, address to_, uint256[] memory ids_, uint256[] memory amounts_,) = + abi.decode(callData_[4:], (address, address, uint256[], uint256[], bytes)); + + if (from_ == address(0) || to_ == address(0)) { + revert("ERC1155TransferEnforcer:invalid-address"); + } + + _validateBatchTransfer(ids_, amounts_, permittedTokenIds_, permittedValues_); + } else { + // Single transfer + (address from_, address to_, uint256 id_, uint256 amount_,) = + abi.decode(callData_[4:], (address, address, uint256, uint256, bytes)); + + if (from_ == address(0) || to_ == address(0)) { + revert("ERC1155TransferEnforcer:invalid-address"); + } + if (permittedTokenIds_[0] != id_) { + revert("ERC1155TransferEnforcer:unauthorized-token-id"); + } + if (permittedValues_[0] != amount_) { + revert("ERC1155TransferEnforcer:unauthorized-amount"); + } + } + } + + /** + * @notice Validates that all token IDs and amounts in a batch transfer are permitted + * @dev Checks each token ID in the transfer against the permitted token IDs and their corresponding amounts + * @param _ids Array of token IDs being transferred + * @param _values Array of amounts being transferred for each token ID + * @param _permittedTokenIds Array of permitted token IDs + * @param _permittedValues Array of permitted amounts for each token ID + */ + function _validateBatchTransfer( + uint256[] memory _ids, + uint256[] memory _values, + uint256[] memory _permittedTokenIds, + uint256[] memory _permittedValues + ) + internal + pure + { + uint256 idsLength_ = _ids.length; + uint256 permittedTokenIdsLength_ = _permittedTokenIds.length; + + // Check if all token IDs in the batch are permitted + for (uint256 i = 0; i < idsLength_; i++) { + bool isPermitted_ = false; + for (uint256 j = 0; j < permittedTokenIdsLength_; j++) { + if (_permittedTokenIds[j] == _ids[i]) { + if (_permittedValues[j] != _values[i]) revert("ERC1155TransferEnforcer:unauthorized-amount"); + isPermitted_ = true; + break; + } + } + if (!isPermitted_) { + revert("ERC1155TransferEnforcer:unauthorized-token-id"); + } + } + } +} From 4182bc51ed5f3bbffab13e0ae2577a9053405c43 Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Mon, 9 Jun 2025 21:38:53 -0600 Subject: [PATCH 2/5] fix: small issue decoding terms --- src/enforcers/ERC1155TransferEnforcer.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/enforcers/ERC1155TransferEnforcer.sol b/src/enforcers/ERC1155TransferEnforcer.sol index ac1a7622..5bcfc859 100644 --- a/src/enforcers/ERC1155TransferEnforcer.sol +++ b/src/enforcers/ERC1155TransferEnforcer.sol @@ -53,14 +53,15 @@ contract ERC1155TransferEnforcer is CaveatEnforcer { pure returns (bool _isBatch, address _permittedContract, uint256[] memory _ids, uint256[] memory _values) { + _isBatch = abi.decode(_terms[:32], (bool)); if (_isBatch) { - (_isBatch, _permittedContract, _ids, _values) = abi.decode(_terms, (bool, address, uint256[], uint256[])); + (, _permittedContract, _ids, _values) = abi.decode(_terms, (bool, address, uint256[], uint256[])); } else { uint256 id_; uint256 value_; _ids = new uint256[](1); _values = new uint256[](1); - (_isBatch, _permittedContract, id_, value_) = abi.decode(_terms, (bool, address, uint256, uint256)); + (, _permittedContract, id_, value_) = abi.decode(_terms, (bool, address, uint256, uint256)); _ids[0] = id_; _values[0] = value_; } From 58a3d712d59c3ed94909589256cb2b4ca5f1f36f Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Tue, 10 Jun 2025 13:08:05 -0600 Subject: [PATCH 3/5] chore: spent maps, length checks, selector --- src/enforcers/ERC1155TransferEnforcer.sol | 165 ++++--- test/enforcers/ERC1155TransferEnforcer.t.sol | 428 +++++++++++++++++++ 2 files changed, 532 insertions(+), 61 deletions(-) create mode 100644 test/enforcers/ERC1155TransferEnforcer.t.sol diff --git a/src/enforcers/ERC1155TransferEnforcer.sol b/src/enforcers/ERC1155TransferEnforcer.sol index 5bcfc859..42c6beaa 100644 --- a/src/enforcers/ERC1155TransferEnforcer.sol +++ b/src/enforcers/ERC1155TransferEnforcer.sol @@ -11,23 +11,38 @@ import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; * @notice This enforcer restricts the execution to the transfer of specific ERC1155 tokens. * @dev This enforcer operates only in single execution call type and with default execution mode. * Supports both single and batch transfers. The terms include a boolean flag indicating the transfer type. + * @dev The enforcer tracks spent amounts per token ID to enforce transfer limits. */ contract ERC1155TransferEnforcer is CaveatEnforcer { - bytes4 private constant SAFE_BATCH_TRANSFER_FROM_SELECTOR = - bytes4(keccak256("safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)")); + ////////////////////////////// State ////////////////////////////// + + /// @notice Maps delegation manager address => delegation hash => token ID => spent amount + mapping(address delegationManager => mapping(bytes32 delegationHash => mapping(uint256 tokenId => uint256 amount))) public + spentMap; + + ////////////////////////////// Events ////////////////////////////// + /// @notice Emitted when the spent amount for a token ID is increased + /// @param sender The address of the delegation manager + /// @param delegationHash The hash of the delegation + /// @param tokenId The ID of the token being transferred + /// @param limit The maximum amount allowed for this token ID + /// @param spent The new total amount spent for this token ID + event IncreasedSpentMap(address indexed sender, bytes32 indexed delegationHash, uint256 tokenId, uint256 limit, uint256 spent); /** * @notice Enforces that the contract and tokenIds are permitted for transfer - * @param _terms abi encoded (bool isBatch, address contract, uint256[] tokenIds) + * @dev Validates that the transfer execution matches the permitted terms and updates spent amounts + * @param _terms encoded terms containing transfer type, contract address, token IDs and amounts * @param _mode The execution mode. (Must be Single callType, Default execType) * @param _executionCallData the call data of the transferFrom call + * @param _delegationHash the hash of the delegation being operated on */ function beforeHook( bytes calldata _terms, bytes calldata, ModeCode _mode, bytes calldata _executionCallData, - bytes32, + bytes32 _delegationHash, address, address ) @@ -37,117 +52,145 @@ contract ERC1155TransferEnforcer is CaveatEnforcer { onlySingleCallTypeMode(_mode) onlyDefaultExecutionMode(_mode) { - _validateTransfer(_terms, _executionCallData); + _validateTransfer(_terms, _delegationHash, _executionCallData); } /** * @notice Decodes the terms to get the transfer type, permitted contract and token IDs + * @dev Validates the terms length and structure * @param _terms The encoded terms containing transfer type, contract address and token IDs * @return _isBatch The transfer type flag * @return _permittedContract The address of the permitted ERC1155 contract - * @return _ids Array of token IDs - * @return _values Array of token amounts + * @return _permittedIds Array of token IDs + * @return _permittedAmounts Array of token amounts */ function getTermsInfo(bytes calldata _terms) public pure - returns (bool _isBatch, address _permittedContract, uint256[] memory _ids, uint256[] memory _values) + returns (bool _isBatch, address _permittedContract, uint256[] memory _permittedIds, uint256[] memory _permittedAmounts) { - _isBatch = abi.decode(_terms[:32], (bool)); - if (_isBatch) { - (, _permittedContract, _ids, _values) = abi.decode(_terms, (bool, address, uint256[], uint256[])); + if (_terms.length == 96) { + _permittedIds = new uint256[](1); + _permittedAmounts = new uint256[](1); + (_permittedContract, _permittedIds[0], _permittedAmounts[0]) = abi.decode(_terms, (address, uint256, uint256)); + } else if (_terms.length >= 224) { + _isBatch = true; + (_permittedContract, _permittedIds, _permittedAmounts) = abi.decode(_terms, (address, uint256[], uint256[])); } else { - uint256 id_; - uint256 value_; - _ids = new uint256[](1); - _values = new uint256[](1); - (, _permittedContract, id_, value_) = abi.decode(_terms, (bool, address, uint256, uint256)); - _ids[0] = id_; - _values[0] = value_; + revert("ERC1155TransferEnforcer:invalid-terms-length"); } - if (_ids.length != _values.length) revert("ERC1155TransferEnforcer:invalid-ids-values-length"); + if (_permittedContract == address(0)) revert("ERC1155TransferEnforcer:invalid-contract-address"); + if (_permittedIds.length != _permittedAmounts.length) revert("ERC1155TransferEnforcer:invalid-ids-values-length"); } /** * @notice Validates that the transfer execution matches the permitted terms * @dev Checks that the contract, token IDs and amounts match what is permitted in the terms * @param _terms The encoded terms containing transfer type, contract address, token IDs and amounts + * @param _delegationHash The hash of the delegation being operated on * @param _executionCallData The encoded execution data containing the transfer details */ - function _validateTransfer(bytes calldata _terms, bytes calldata _executionCallData) internal pure { - (bool isBatch_, address permittedContract_, uint256[] memory permittedTokenIds_, uint256[] memory permittedValues_) = + function _validateTransfer(bytes calldata _terms, bytes32 _delegationHash, bytes calldata _executionCallData) internal { + (bool isBatch_, address permittedContract_, uint256[] memory permittedTokenIds_, uint256[] memory permittedAmounts_) = getTermsInfo(_terms); (address target_, uint256 value_, bytes calldata callData_) = ExecutionLib.decodeSingle(_executionCallData); if (value_ != 0) revert("ERC1155TransferEnforcer:invalid-value"); - - if (callData_.length < 4) revert("ERC1155TransferEnforcer:invalid-calldata-length"); - - if (target_ != permittedContract_) { - revert("ERC1155TransferEnforcer:unauthorized-contract-target"); - } + if (callData_.length != 196 && callData_.length < 324) revert("ERC1155TransferEnforcer:invalid-calldata-length"); + if (target_ != permittedContract_) revert("ERC1155TransferEnforcer:unauthorized-contract-target"); bytes4 selector_ = bytes4(callData_[0:4]); - if (isBatch_ && selector_ != SAFE_BATCH_TRANSFER_FROM_SELECTOR) { + if (isBatch_ && selector_ != IERC1155.safeBatchTransferFrom.selector) { revert("ERC1155TransferEnforcer:unauthorized-selector-batch"); - } else if (!isBatch_ && selector_ != IERC1155.safeTransferFrom.selector) { + } + if (!isBatch_ && selector_ != IERC1155.safeTransferFrom.selector) { revert("ERC1155TransferEnforcer:unauthorized-selector-single"); } if (isBatch_) { - // Batch transfer - (address from_, address to_, uint256[] memory ids_, uint256[] memory amounts_,) = - abi.decode(callData_[4:], (address, address, uint256[], uint256[], bytes)); + _validateBatchTransfer(_delegationHash, callData_, permittedTokenIds_, permittedAmounts_); + } else { + _validateSingleTransfer(_delegationHash, callData_, permittedTokenIds_[0], permittedAmounts_[0]); + } + } - if (from_ == address(0) || to_ == address(0)) { - revert("ERC1155TransferEnforcer:invalid-address"); - } + /** + * @notice Validates a single ERC1155 token transfer against permitted parameters + * @dev Checks that the transfer addresses are valid and matches token ID and amount against permitted values + * @param _delegationHash The hash of the delegation being operated on + * @param _callData The encoded transfer function call data + * @param _permittedTokenId The token ID that is permitted to be transferred + * @param _permittedAmount The amount that is permitted to be transferred + */ + function _validateSingleTransfer( + bytes32 _delegationHash, + bytes calldata _callData, + uint256 _permittedTokenId, + uint256 _permittedAmount + ) + internal + { + (address from_, address to_, uint256 id_, uint256 amount_,) = + abi.decode(_callData[4:], (address, address, uint256, uint256, bytes)); - _validateBatchTransfer(ids_, amounts_, permittedTokenIds_, permittedValues_); - } else { - // Single transfer - (address from_, address to_, uint256 id_, uint256 amount_,) = - abi.decode(callData_[4:], (address, address, uint256, uint256, bytes)); + if (from_ == address(0) || to_ == address(0)) { + revert("ERC1155TransferEnforcer:invalid-address"); + } + if (_permittedTokenId != id_) { + revert("ERC1155TransferEnforcer:unauthorized-token-id"); + } + _increaseSpentMap(_delegationHash, id_, amount_, _permittedAmount); + } - if (from_ == address(0) || to_ == address(0)) { - revert("ERC1155TransferEnforcer:invalid-address"); - } - if (permittedTokenIds_[0] != id_) { - revert("ERC1155TransferEnforcer:unauthorized-token-id"); - } - if (permittedValues_[0] != amount_) { - revert("ERC1155TransferEnforcer:unauthorized-amount"); - } + /** + * @notice Updates and validates the spent amount for a token ID + * @dev Increments the spent amount and checks against permitted limit + * @param _delegationHash The hash of the delegation being operated on + * @param _id The token ID being tracked + * @param _amount The amount to increase the spent tracker by + * @param _permittedAmount The maximum amount allowed for this token ID + */ + function _increaseSpentMap(bytes32 _delegationHash, uint256 _id, uint256 _amount, uint256 _permittedAmount) internal { + uint256 spent_ = spentMap[msg.sender][_delegationHash][_id] += _amount; + if (spent_ > _permittedAmount) { + revert("ERC1155TransferEnforcer:unauthorized-amount"); } + emit IncreasedSpentMap(msg.sender, _delegationHash, _id, _permittedAmount, spent_); } /** - * @notice Validates that all token IDs and amounts in a batch transfer are permitted - * @dev Checks each token ID in the transfer against the permitted token IDs and their corresponding amounts - * @param _ids Array of token IDs being transferred - * @param _values Array of amounts being transferred for each token ID + * @notice Validates a batch ERC1155 token transfer against permitted parameters + * @dev Checks that all token IDs in the batch are permitted and their amounts don't exceed limits + * @param _delegationHash The hash of the delegation being operated on + * @param _callData The encoded batch transfer function call data * @param _permittedTokenIds Array of permitted token IDs - * @param _permittedValues Array of permitted amounts for each token ID + * @param _permittedAmounts Array of permitted amounts for each token ID */ function _validateBatchTransfer( - uint256[] memory _ids, - uint256[] memory _values, + bytes32 _delegationHash, + bytes calldata _callData, uint256[] memory _permittedTokenIds, - uint256[] memory _permittedValues + uint256[] memory _permittedAmounts ) internal - pure { - uint256 idsLength_ = _ids.length; + (address from_, address to_, uint256[] memory ids_, uint256[] memory amounts_,) = + abi.decode(_callData[4:], (address, address, uint256[], uint256[], bytes)); + + if (from_ == address(0) || to_ == address(0)) { + revert("ERC1155TransferEnforcer:invalid-address"); + } + + uint256 idsLength_ = ids_.length; uint256 permittedTokenIdsLength_ = _permittedTokenIds.length; // Check if all token IDs in the batch are permitted for (uint256 i = 0; i < idsLength_; i++) { bool isPermitted_ = false; for (uint256 j = 0; j < permittedTokenIdsLength_; j++) { - if (_permittedTokenIds[j] == _ids[i]) { - if (_permittedValues[j] != _values[i]) revert("ERC1155TransferEnforcer:unauthorized-amount"); + if (ids_[i] == _permittedTokenIds[j]) { + _increaseSpentMap(_delegationHash, ids_[i], amounts_[i], _permittedAmounts[j]); isPermitted_ = true; break; } diff --git a/test/enforcers/ERC1155TransferEnforcer.t.sol b/test/enforcers/ERC1155TransferEnforcer.t.sol new file mode 100644 index 00000000..269293a0 --- /dev/null +++ b/test/enforcers/ERC1155TransferEnforcer.t.sol @@ -0,0 +1,428 @@ +// // SPDX-License-Identifier: MIT AND Apache-2.0 +// pragma solidity 0.8.23; + +// import { Test } from "forge-std/Test.sol"; +// import { ERC1155TransferEnforcer } from "../../src/enforcers/ERC1155TransferEnforcer.sol"; +// import { IERC1155 } from "@openzeppelin/contracts/token/ERC1155/IERC1155.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 { ICaveatEnforcer } from "../../src/interfaces/ICaveatEnforcer.sol"; +// import { BasicERC1155 } from "../utils/BasicERC1155.t.sol"; + +// contract ERC1155TransferEnforcerTest is CaveatEnforcerBaseTest { +// ////////////////////// State ////////////////////// + +// ERC1155TransferEnforcer public erc1155TransferEnforcer; +// BasicERC1155 public mockERC1155; +// ModeCode public mode = ModeLib.encodeSimpleSingle(); + +// ////////////////////// Set up ////////////////////// + +// function setUp() public override { +// super.setUp(); +// erc1155TransferEnforcer = new ERC1155TransferEnforcer(); +// mockERC1155 = new BasicERC1155(address(this), "Basic ERC1155", "B1155", ""); +// vm.label(address(erc1155TransferEnforcer), "ERC1155 Transfer Enforcer"); +// vm.label(address(mockERC1155), "Basic ERC1155"); +// } + +// ////////////////////// Helper Functions ////////////////////// + +// function _encodeTerms( +// bool isBatch, +// address contract_, +// uint256[] memory tokenIds, +// uint256[] memory values +// ) +// internal +// pure +// returns (bytes memory) +// { +// return abi.encode(isBatch, contract_, tokenIds, values); +// } + +// ////////////////////// Valid cases ////////////////////// + +// // Tests single token transfer with a permitted token ID and amount +// function test_singleTransferWithSinglePermittedToken() public { +// uint256[] memory permittedTokenIds = new uint256[](1); +// uint256[] memory permittedValues = new uint256[](1); +// permittedTokenIds[0] = 1; +// permittedValues[0] = 1; + +// bytes memory terms = _encodeTerms(false, address(mockERC1155), permittedTokenIds, permittedValues); + +// Execution memory execution = Execution({ +// target: address(mockERC1155), +// value: 0, +// callData: abi.encodeWithSelector( +// IERC1155.safeTransferFrom.selector, address(users.alice.deleGator), address(users.bob.deleGator), 1, 1, "" +// ) +// }); +// bytes memory executionCallData = ExecutionLib.encodeSingle(execution.target, execution.value, execution.callData); + +// vm.prank(address(delegationManager)); +// erc1155TransferEnforcer.beforeHook(terms, "", mode, executionCallData, keccak256(""), address(0), address(0)); +// } + +// // Tests batch transfer with multiple permitted token IDs and amounts +// function test_batchTransferWithMultiplePermittedTokens() public { +// uint256[] memory permittedTokenIds = new uint256[](2); +// uint256[] memory permittedValues = new uint256[](2); +// permittedTokenIds[0] = 1; +// permittedTokenIds[1] = 2; +// permittedValues[0] = 1; +// permittedValues[1] = 1; + +// bytes memory terms = _encodeTerms(address(mockERC1155), permittedTokenIds, permittedValues, true); + +// uint256[] memory ids = new uint256[](2); +// uint256[] memory amounts = new uint256[](2); +// ids[0] = 1; +// ids[1] = 2; +// amounts[0] = 1; +// amounts[1] = 1; + +// Execution memory execution = Execution({ +// target: address(mockERC1155), +// value: 0, +// callData: abi.encodeWithSelector( +// IERC1155.safeBatchTransferFrom.selector, address(users.alice.deleGator), address(users.bob.deleGator), ids, +// amounts, "" +// ) +// }); +// bytes memory executionCallData = ExecutionLib.encodeSingle(execution.target, execution.value, execution.callData); + +// vm.prank(address(delegationManager)); +// erc1155TransferEnforcer.beforeHook(terms, "", mode, executionCallData, keccak256(""), address(0), address(0)); +// } + +// ////////////////////// Invalid cases ////////////////////// + +// // Tests rejection of invalid terms data length +// function test_invalidTermsLength() public { +// vm.expectRevert("ERC1155TransferEnforcer:invalid-terms-length"); +// erc1155TransferEnforcer.getTermsInfo(bytes("1")); +// } + +// // Tests rejection of too short calldata +// function test_invalidCalldataLength() public { +// uint256[] memory permittedTokenIds = new uint256[](1); +// uint256[] memory permittedValues = new uint256[](1); +// permittedTokenIds[0] = 1; +// permittedValues[0] = 1; + +// bytes memory terms = _encodeTerms(address(mockERC1155), permittedTokenIds, permittedValues, false); + +// Execution memory execution = Execution({ target: address(mockERC1155), value: 0, callData: abi.encodePacked(bytes4(0)) +// }); +// bytes memory executionCallData = ExecutionLib.encodeSingle(execution.target, execution.value, execution.callData); + +// vm.prank(address(delegationManager)); +// vm.expectRevert("ERC1155TransferEnforcer:invalid-calldata-length"); +// erc1155TransferEnforcer.beforeHook(terms, "", mode, executionCallData, keccak256(""), address(0), address(0)); +// } + +// // Tests rejection of unauthorized contract target +// function test_unauthorizedContractTarget() public { +// uint256[] memory permittedTokenIds = new uint256[](1); +// uint256[] memory permittedValues = new uint256[](1); +// permittedTokenIds[0] = 1; +// permittedValues[0] = 1; + +// bytes memory terms = _encodeTerms(address(mockERC1155), permittedTokenIds, permittedValues, false); + +// Execution memory execution = Execution({ +// target: address(0x1234), // Different contract address +// value: 0, +// callData: abi.encodeWithSelector( +// IERC1155.safeTransferFrom.selector, address(users.alice.deleGator), address(users.bob.deleGator), 1, 1, "" +// ) +// }); +// bytes memory executionCallData = ExecutionLib.encodeSingle(execution.target, execution.value, execution.callData); + +// vm.prank(address(delegationManager)); +// vm.expectRevert("ERC1155TransferEnforcer:unauthorized-contract-target"); +// erc1155TransferEnforcer.beforeHook(terms, "", mode, executionCallData, keccak256(""), address(0), address(0)); +// } + +// // Tests rejection of unauthorized function selector +// function test_unauthorizedSelector() public { +// uint256[] memory permittedTokenIds = new uint256[](1); +// uint256[] memory permittedValues = new uint256[](1); +// permittedTokenIds[0] = 1; +// permittedValues[0] = 1; + +// bytes memory terms = _encodeTerms(address(mockERC1155), permittedTokenIds, permittedValues, false); + +// Execution memory execution = Execution({ +// target: address(mockERC1155), +// value: 0, +// callData: abi.encodeWithSelector(bytes4(0x12345678)) // Invalid selector +// }); +// bytes memory executionCallData = ExecutionLib.encodeSingle(execution.target, execution.value, execution.callData); + +// vm.prank(address(delegationManager)); +// vm.expectRevert("ERC1155TransferEnforcer:unauthorized-selector-single"); +// erc1155TransferEnforcer.beforeHook(terms, "", mode, executionCallData, keccak256(""), address(0), address(0)); +// } + +// // Tests rejection of unauthorized token ID in single transfer +// function test_unauthorizedTokenId() public { +// uint256[] memory permittedTokenIds = new uint256[](1); +// uint256[] memory permittedValues = new uint256[](1); +// permittedTokenIds[0] = 1; +// permittedValues[0] = 1; + +// bytes memory terms = _encodeTerms(address(mockERC1155), permittedTokenIds, permittedValues, false); + +// Execution memory execution = Execution({ +// target: address(mockERC1155), +// value: 0, +// callData: abi.encodeWithSelector( +// IERC1155.safeTransferFrom.selector, +// address(users.alice.deleGator), +// address(users.bob.deleGator), +// 2, // Different token ID +// 1, +// "" +// ) +// }); +// bytes memory executionCallData = ExecutionLib.encodeSingle(execution.target, execution.value, execution.callData); + +// vm.prank(address(delegationManager)); +// vm.expectRevert("ERC1155TransferEnforcer:unauthorized-token-id"); +// erc1155TransferEnforcer.beforeHook(terms, "", mode, executionCallData, keccak256(""), address(0), address(0)); +// } + +// // Tests rejection of unauthorized amount in single transfer +// function test_unauthorizedAmount() public { +// uint256[] memory permittedTokenIds = new uint256[](1); +// uint256[] memory permittedValues = new uint256[](1); +// permittedTokenIds[0] = 1; +// permittedValues[0] = 1; + +// bytes memory terms = _encodeTerms(address(mockERC1155), permittedTokenIds, permittedValues, false); + +// Execution memory execution = Execution({ +// target: address(mockERC1155), +// value: 0, +// callData: abi.encodeWithSelector( +// IERC1155.safeTransferFrom.selector, +// address(users.alice.deleGator), +// address(users.bob.deleGator), +// 1, +// 2, // Different amount +// "" +// ) +// }); +// bytes memory executionCallData = ExecutionLib.encodeSingle(execution.target, execution.value, execution.callData); + +// vm.prank(address(delegationManager)); +// vm.expectRevert("ERC1155TransferEnforcer:unauthorized-amount"); +// erc1155TransferEnforcer.beforeHook(terms, "", mode, executionCallData, keccak256(""), address(0), address(0)); +// } + +// // Tests rejection of zero address in transfer +// function test_invalidAddress() public { +// uint256[] memory permittedTokenIds = new uint256[](1); +// uint256[] memory permittedValues = new uint256[](1); +// permittedTokenIds[0] = 1; +// permittedValues[0] = 1; + +// bytes memory terms = _encodeTerms(address(mockERC1155), permittedTokenIds, permittedValues, false); + +// Execution memory execution = Execution({ +// target: address(mockERC1155), +// value: 0, +// callData: abi.encodeWithSelector( +// IERC1155.safeTransferFrom.selector, +// address(0), // Invalid from address +// address(users.bob.deleGator), +// 1, +// 1, +// "" +// ) +// }); +// bytes memory executionCallData = ExecutionLib.encodeSingle(execution.target, execution.value, execution.callData); + +// vm.prank(address(delegationManager)); +// vm.expectRevert("ERC1155TransferEnforcer:invalid-address"); +// erc1155TransferEnforcer.beforeHook(terms, "", mode, executionCallData, keccak256(""), address(0), address(0)); +// } + +// // Tests rejection of unauthorized token ID in batch transfer +// function test_batchTransferWithUnauthorizedTokenId() public { +// uint256[] memory permittedTokenIds = new uint256[](1); +// uint256[] memory permittedValues = new uint256[](1); +// permittedTokenIds[0] = 1; +// permittedValues[0] = 1; + +// bytes memory terms = _encodeTerms(address(mockERC1155), permittedTokenIds, permittedValues, true); + +// uint256[] memory ids = new uint256[](2); +// uint256[] memory amounts = new uint256[](2); +// ids[0] = 1; +// ids[1] = 2; // Unauthorized token ID +// amounts[0] = 1; +// amounts[1] = 1; + +// Execution memory execution = Execution({ +// target: address(mockERC1155), +// value: 0, +// callData: abi.encodeWithSelector( +// IERC1155.safeBatchTransferFrom.selector, address(users.alice.deleGator), address(users.bob.deleGator), ids, +// amounts, "" +// ) +// }); +// bytes memory executionCallData = ExecutionLib.encodeSingle(execution.target, execution.value, execution.callData); + +// vm.prank(address(delegationManager)); +// vm.expectRevert("ERC1155TransferEnforcer:unauthorized-token-id"); +// erc1155TransferEnforcer.beforeHook(terms, "", mode, executionCallData, keccak256(""), address(0), address(0)); +// } + +// // Tests rejection of unauthorized amount in batch transfer +// function test_batchTransferWithUnauthorizedAmount() public { +// uint256[] memory permittedTokenIds = new uint256[](2); +// uint256[] memory permittedValues = new uint256[](2); +// permittedTokenIds[0] = 1; +// permittedTokenIds[1] = 2; +// permittedValues[0] = 1; +// permittedValues[1] = 1; + +// bytes memory terms = _encodeTerms(address(mockERC1155), permittedTokenIds, permittedValues, true); + +// uint256[] memory ids = new uint256[](2); +// uint256[] memory amounts = new uint256[](2); +// ids[0] = 1; +// ids[1] = 2; +// amounts[0] = 1; +// amounts[1] = 2; // Unauthorized amount + +// Execution memory execution = Execution({ +// target: address(mockERC1155), +// value: 0, +// callData: abi.encodeWithSelector( +// IERC1155.safeBatchTransferFrom.selector, address(users.alice.deleGator), address(users.bob.deleGator), ids, +// amounts, "" +// ) +// }); +// bytes memory executionCallData = ExecutionLib.encodeSingle(execution.target, execution.value, execution.callData); + +// vm.prank(address(delegationManager)); +// vm.expectRevert("ERC1155TransferEnforcer:unauthorized-amount"); +// erc1155TransferEnforcer.beforeHook(terms, "", mode, executionCallData, keccak256(""), address(0), address(0)); +// } + +// ////////////////////// Integration ////////////////////// + +// // Tests complete single token transfer flow with delegation +// function test_singleTransferIntegration() public { +// uint256[] memory permittedTokenIds = new uint256[](1); +// uint256[] memory permittedValues = new uint256[](1); +// permittedTokenIds[0] = 1; +// permittedValues[0] = 1; + +// bytes memory terms = _encodeTerms(address(mockERC1155), permittedTokenIds, permittedValues, false); + +// // Set up initial balances +// mockERC1155.mint(address(users.alice.deleGator), 1, 1, ""); + +// Execution memory execution = Execution({ +// target: address(mockERC1155), +// value: 0, +// callData: abi.encodeWithSelector( +// IERC1155.safeTransferFrom.selector, address(users.alice.deleGator), address(users.bob.deleGator), 1, 1, "" +// ) +// }); + +// Caveat[] memory caveats = new Caveat[](1); +// caveats[0] = Caveat({ args: "", enforcer: address(erc1155TransferEnforcer), terms: terms }); + +// Delegation memory delegation = Delegation({ +// delegate: address(users.bob.deleGator), +// delegator: address(users.alice.deleGator), +// authority: ROOT_AUTHORITY, +// caveats: caveats, +// salt: 0, +// signature: "" +// }); + +// delegation = signDelegation(users.alice, delegation); + +// Delegation[] memory delegations = new Delegation[](1); +// delegations[0] = delegation; + +// // Execute the transfer +// invokeDelegation_UserOp(users.bob, delegations, execution); + +// // Verify the transfer +// assertEq(mockERC1155.balanceOf(address(users.alice.deleGator), 1), 0); +// assertEq(mockERC1155.balanceOf(address(users.bob.deleGator), 1), 1); +// } + +// // Tests complete batch token transfer flow with delegation +// function test_batchTransferIntegration() public { +// uint256[] memory permittedTokenIds = new uint256[](2); +// uint256[] memory permittedValues = new uint256[](2); +// permittedTokenIds[0] = 1; +// permittedTokenIds[1] = 2; +// permittedValues[0] = 1; +// permittedValues[1] = 1; + +// bytes memory terms = _encodeTerms(address(mockERC1155), permittedTokenIds, permittedValues, true); + +// // Set up initial balances +// mockERC1155.mint(address(users.alice.deleGator), 1, 1, ""); +// mockERC1155.mint(address(users.alice.deleGator), 2, 1, ""); + +// uint256[] memory ids = new uint256[](2); +// uint256[] memory amounts = new uint256[](2); +// ids[0] = 1; +// ids[1] = 2; +// amounts[0] = 1; +// amounts[1] = 1; + +// Execution memory execution = Execution({ +// target: address(mockERC1155), +// value: 0, +// callData: abi.encodeWithSelector( +// IERC1155.safeBatchTransferFrom.selector, address(users.alice.deleGator), address(users.bob.deleGator), ids, +// amounts, "" +// ) +// }); + +// Caveat[] memory caveats = new Caveat[](1); +// caveats[0] = Caveat({ args: "", enforcer: address(erc1155TransferEnforcer), terms: terms }); + +// Delegation memory delegation = Delegation({ +// delegate: address(users.bob.deleGator), +// delegator: address(users.alice.deleGator), +// authority: ROOT_AUTHORITY, +// caveats: caveats, +// salt: 0, +// signature: "" +// }); + +// delegation = signDelegation(users.alice, delegation); + +// Delegation[] memory delegations = new Delegation[](1); +// delegations[0] = delegation; + +// // Execute the transfer +// invokeDelegation_UserOp(users.bob, delegations, execution); + +// // Verify the transfers +// assertEq(mockERC1155.balanceOf(address(users.alice.deleGator), 1), 0); +// assertEq(mockERC1155.balanceOf(address(users.alice.deleGator), 2), 0); +// assertEq(mockERC1155.balanceOf(address(users.bob.deleGator), 1), 1); +// assertEq(mockERC1155.balanceOf(address(users.bob.deleGator), 2), 1); +// } + +// function _getEnforcer() internal view override returns (ICaveatEnforcer) { +// return ICaveatEnforcer(address(erc1155TransferEnforcer)); +// } +// } From 62316f448650d89a7e5b0ebded3715e8f884013b Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Fri, 13 Jun 2025 14:31:38 -0600 Subject: [PATCH 4/5] feat: implement ERC1155 transfer enforcer --- script/DeployCaveatEnforcers.s.sol | 4 + .../verification/verify-enforcer-contracts.sh | 2 + src/enforcers/ERC1155TransferEnforcer.sol | 32 +- test/enforcers/ERC1155TransferEnforcer.t.sol | 1412 ++++++++++++----- 4 files changed, 1006 insertions(+), 444 deletions(-) diff --git a/script/DeployCaveatEnforcers.s.sol b/script/DeployCaveatEnforcers.s.sol index 8d1487d1..14d37bd1 100644 --- a/script/DeployCaveatEnforcers.s.sol +++ b/script/DeployCaveatEnforcers.s.sol @@ -19,6 +19,7 @@ import { ERC20PeriodTransferEnforcer } from "../src/enforcers/ERC20PeriodTransfe import { ERC721BalanceChangeEnforcer } from "../src/enforcers/ERC721BalanceChangeEnforcer.sol"; import { ERC721TransferEnforcer } from "../src/enforcers/ERC721TransferEnforcer.sol"; import { ERC1155BalanceChangeEnforcer } from "../src/enforcers/ERC1155BalanceChangeEnforcer.sol"; +import { ERC1155TransferEnforcer } from "../src/enforcers/ERC1155TransferEnforcer.sol"; import { ExactCalldataBatchEnforcer } from "../src/enforcers/ExactCalldataBatchEnforcer.sol"; import { ExactCalldataEnforcer } from "../src/enforcers/ExactCalldataEnforcer.sol"; import { ExactExecutionBatchEnforcer } from "../src/enforcers/ExactExecutionBatchEnforcer.sol"; @@ -105,6 +106,9 @@ contract DeployCaveatEnforcers is Script { deployedAddress = address(new ERC1155BalanceChangeEnforcer{ salt: salt }()); console2.log("ERC1155BalanceChangeEnforcer: %s", deployedAddress); + deployedAddress = address(new ERC1155TransferEnforcer{ salt: salt }()); + console2.log("ERC1155TransferEnforcer: %s", deployedAddress); + deployedAddress = address(new ExactCalldataBatchEnforcer{ salt: salt }()); console2.log("ExactCalldataBatchEnforcer: %s", deployedAddress); diff --git a/script/verification/verify-enforcer-contracts.sh b/script/verification/verify-enforcer-contracts.sh index 035628a9..399256a1 100755 --- a/script/verification/verify-enforcer-contracts.sh +++ b/script/verification/verify-enforcer-contracts.sh @@ -35,6 +35,7 @@ ENFORCERS=( "ERC721BalanceChangeEnforcer" "ERC721TransferEnforcer" "ERC1155BalanceChangeEnforcer" + "ERC1155TransferEnforcer" "ExactCalldataBatchEnforcer" "ExactCalldataEnforcer" "ExactExecutionBatchEnforcer" @@ -69,6 +70,7 @@ ADDRESSES=( "0x8aFdf96eDBbe7e1eD3f5Cd89C7E084841e12A09e" "0x3790e6B7233f779b09DA74C72b6e94813925b9aF" "0x63c322732695cAFbbD488Fc6937A0A7B66fC001A" + "0x0000000000000000000000000000000000000000" "0x982FD5C86BBF425d7d1451f974192d4525113DfD" "0x99F2e9bF15ce5eC84685604836F71aB835DBBdED" "0x1e141e455d08721Dd5BCDA1BaA6Ea5633Afd5017" diff --git a/src/enforcers/ERC1155TransferEnforcer.sol b/src/enforcers/ERC1155TransferEnforcer.sol index 42c6beaa..06b0b086 100644 --- a/src/enforcers/ERC1155TransferEnforcer.sol +++ b/src/enforcers/ERC1155TransferEnforcer.sol @@ -143,22 +143,6 @@ contract ERC1155TransferEnforcer is CaveatEnforcer { _increaseSpentMap(_delegationHash, id_, amount_, _permittedAmount); } - /** - * @notice Updates and validates the spent amount for a token ID - * @dev Increments the spent amount and checks against permitted limit - * @param _delegationHash The hash of the delegation being operated on - * @param _id The token ID being tracked - * @param _amount The amount to increase the spent tracker by - * @param _permittedAmount The maximum amount allowed for this token ID - */ - function _increaseSpentMap(bytes32 _delegationHash, uint256 _id, uint256 _amount, uint256 _permittedAmount) internal { - uint256 spent_ = spentMap[msg.sender][_delegationHash][_id] += _amount; - if (spent_ > _permittedAmount) { - revert("ERC1155TransferEnforcer:unauthorized-amount"); - } - emit IncreasedSpentMap(msg.sender, _delegationHash, _id, _permittedAmount, spent_); - } - /** * @notice Validates a batch ERC1155 token transfer against permitted parameters * @dev Checks that all token IDs in the batch are permitted and their amounts don't exceed limits @@ -200,4 +184,20 @@ contract ERC1155TransferEnforcer is CaveatEnforcer { } } } + + /** + * @notice Updates and validates the spent amount for a token ID + * @dev Increments the spent amount and checks against permitted limit + * @param _delegationHash The hash of the delegation being operated on + * @param _id The token ID being tracked + * @param _amount The amount to increase the spent tracker by + * @param _permittedAmount The maximum amount allowed for this token ID + */ + function _increaseSpentMap(bytes32 _delegationHash, uint256 _id, uint256 _amount, uint256 _permittedAmount) private { + uint256 spent_ = spentMap[msg.sender][_delegationHash][_id] += _amount; + if (spent_ > _permittedAmount) { + revert("ERC1155TransferEnforcer:unauthorized-amount"); + } + emit IncreasedSpentMap(msg.sender, _delegationHash, _id, _permittedAmount, spent_); + } } diff --git a/test/enforcers/ERC1155TransferEnforcer.t.sol b/test/enforcers/ERC1155TransferEnforcer.t.sol index 269293a0..52369ada 100644 --- a/test/enforcers/ERC1155TransferEnforcer.t.sol +++ b/test/enforcers/ERC1155TransferEnforcer.t.sol @@ -1,428 +1,984 @@ -// // SPDX-License-Identifier: MIT AND Apache-2.0 -// pragma solidity 0.8.23; - -// import { Test } from "forge-std/Test.sol"; -// import { ERC1155TransferEnforcer } from "../../src/enforcers/ERC1155TransferEnforcer.sol"; -// import { IERC1155 } from "@openzeppelin/contracts/token/ERC1155/IERC1155.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 { ICaveatEnforcer } from "../../src/interfaces/ICaveatEnforcer.sol"; -// import { BasicERC1155 } from "../utils/BasicERC1155.t.sol"; - -// contract ERC1155TransferEnforcerTest is CaveatEnforcerBaseTest { -// ////////////////////// State ////////////////////// - -// ERC1155TransferEnforcer public erc1155TransferEnforcer; -// BasicERC1155 public mockERC1155; -// ModeCode public mode = ModeLib.encodeSimpleSingle(); - -// ////////////////////// Set up ////////////////////// - -// function setUp() public override { -// super.setUp(); -// erc1155TransferEnforcer = new ERC1155TransferEnforcer(); -// mockERC1155 = new BasicERC1155(address(this), "Basic ERC1155", "B1155", ""); -// vm.label(address(erc1155TransferEnforcer), "ERC1155 Transfer Enforcer"); -// vm.label(address(mockERC1155), "Basic ERC1155"); -// } - -// ////////////////////// Helper Functions ////////////////////// - -// function _encodeTerms( -// bool isBatch, -// address contract_, -// uint256[] memory tokenIds, -// uint256[] memory values -// ) -// internal -// pure -// returns (bytes memory) -// { -// return abi.encode(isBatch, contract_, tokenIds, values); -// } - -// ////////////////////// Valid cases ////////////////////// - -// // Tests single token transfer with a permitted token ID and amount -// function test_singleTransferWithSinglePermittedToken() public { -// uint256[] memory permittedTokenIds = new uint256[](1); -// uint256[] memory permittedValues = new uint256[](1); -// permittedTokenIds[0] = 1; -// permittedValues[0] = 1; - -// bytes memory terms = _encodeTerms(false, address(mockERC1155), permittedTokenIds, permittedValues); - -// Execution memory execution = Execution({ -// target: address(mockERC1155), -// value: 0, -// callData: abi.encodeWithSelector( -// IERC1155.safeTransferFrom.selector, address(users.alice.deleGator), address(users.bob.deleGator), 1, 1, "" -// ) -// }); -// bytes memory executionCallData = ExecutionLib.encodeSingle(execution.target, execution.value, execution.callData); - -// vm.prank(address(delegationManager)); -// erc1155TransferEnforcer.beforeHook(terms, "", mode, executionCallData, keccak256(""), address(0), address(0)); -// } - -// // Tests batch transfer with multiple permitted token IDs and amounts -// function test_batchTransferWithMultiplePermittedTokens() public { -// uint256[] memory permittedTokenIds = new uint256[](2); -// uint256[] memory permittedValues = new uint256[](2); -// permittedTokenIds[0] = 1; -// permittedTokenIds[1] = 2; -// permittedValues[0] = 1; -// permittedValues[1] = 1; - -// bytes memory terms = _encodeTerms(address(mockERC1155), permittedTokenIds, permittedValues, true); - -// uint256[] memory ids = new uint256[](2); -// uint256[] memory amounts = new uint256[](2); -// ids[0] = 1; -// ids[1] = 2; -// amounts[0] = 1; -// amounts[1] = 1; - -// Execution memory execution = Execution({ -// target: address(mockERC1155), -// value: 0, -// callData: abi.encodeWithSelector( -// IERC1155.safeBatchTransferFrom.selector, address(users.alice.deleGator), address(users.bob.deleGator), ids, -// amounts, "" -// ) -// }); -// bytes memory executionCallData = ExecutionLib.encodeSingle(execution.target, execution.value, execution.callData); - -// vm.prank(address(delegationManager)); -// erc1155TransferEnforcer.beforeHook(terms, "", mode, executionCallData, keccak256(""), address(0), address(0)); -// } - -// ////////////////////// Invalid cases ////////////////////// - -// // Tests rejection of invalid terms data length -// function test_invalidTermsLength() public { -// vm.expectRevert("ERC1155TransferEnforcer:invalid-terms-length"); -// erc1155TransferEnforcer.getTermsInfo(bytes("1")); -// } - -// // Tests rejection of too short calldata -// function test_invalidCalldataLength() public { -// uint256[] memory permittedTokenIds = new uint256[](1); -// uint256[] memory permittedValues = new uint256[](1); -// permittedTokenIds[0] = 1; -// permittedValues[0] = 1; - -// bytes memory terms = _encodeTerms(address(mockERC1155), permittedTokenIds, permittedValues, false); - -// Execution memory execution = Execution({ target: address(mockERC1155), value: 0, callData: abi.encodePacked(bytes4(0)) -// }); -// bytes memory executionCallData = ExecutionLib.encodeSingle(execution.target, execution.value, execution.callData); - -// vm.prank(address(delegationManager)); -// vm.expectRevert("ERC1155TransferEnforcer:invalid-calldata-length"); -// erc1155TransferEnforcer.beforeHook(terms, "", mode, executionCallData, keccak256(""), address(0), address(0)); -// } - -// // Tests rejection of unauthorized contract target -// function test_unauthorizedContractTarget() public { -// uint256[] memory permittedTokenIds = new uint256[](1); -// uint256[] memory permittedValues = new uint256[](1); -// permittedTokenIds[0] = 1; -// permittedValues[0] = 1; - -// bytes memory terms = _encodeTerms(address(mockERC1155), permittedTokenIds, permittedValues, false); - -// Execution memory execution = Execution({ -// target: address(0x1234), // Different contract address -// value: 0, -// callData: abi.encodeWithSelector( -// IERC1155.safeTransferFrom.selector, address(users.alice.deleGator), address(users.bob.deleGator), 1, 1, "" -// ) -// }); -// bytes memory executionCallData = ExecutionLib.encodeSingle(execution.target, execution.value, execution.callData); - -// vm.prank(address(delegationManager)); -// vm.expectRevert("ERC1155TransferEnforcer:unauthorized-contract-target"); -// erc1155TransferEnforcer.beforeHook(terms, "", mode, executionCallData, keccak256(""), address(0), address(0)); -// } - -// // Tests rejection of unauthorized function selector -// function test_unauthorizedSelector() public { -// uint256[] memory permittedTokenIds = new uint256[](1); -// uint256[] memory permittedValues = new uint256[](1); -// permittedTokenIds[0] = 1; -// permittedValues[0] = 1; - -// bytes memory terms = _encodeTerms(address(mockERC1155), permittedTokenIds, permittedValues, false); - -// Execution memory execution = Execution({ -// target: address(mockERC1155), -// value: 0, -// callData: abi.encodeWithSelector(bytes4(0x12345678)) // Invalid selector -// }); -// bytes memory executionCallData = ExecutionLib.encodeSingle(execution.target, execution.value, execution.callData); - -// vm.prank(address(delegationManager)); -// vm.expectRevert("ERC1155TransferEnforcer:unauthorized-selector-single"); -// erc1155TransferEnforcer.beforeHook(terms, "", mode, executionCallData, keccak256(""), address(0), address(0)); -// } - -// // Tests rejection of unauthorized token ID in single transfer -// function test_unauthorizedTokenId() public { -// uint256[] memory permittedTokenIds = new uint256[](1); -// uint256[] memory permittedValues = new uint256[](1); -// permittedTokenIds[0] = 1; -// permittedValues[0] = 1; - -// bytes memory terms = _encodeTerms(address(mockERC1155), permittedTokenIds, permittedValues, false); - -// Execution memory execution = Execution({ -// target: address(mockERC1155), -// value: 0, -// callData: abi.encodeWithSelector( -// IERC1155.safeTransferFrom.selector, -// address(users.alice.deleGator), -// address(users.bob.deleGator), -// 2, // Different token ID -// 1, -// "" -// ) -// }); -// bytes memory executionCallData = ExecutionLib.encodeSingle(execution.target, execution.value, execution.callData); - -// vm.prank(address(delegationManager)); -// vm.expectRevert("ERC1155TransferEnforcer:unauthorized-token-id"); -// erc1155TransferEnforcer.beforeHook(terms, "", mode, executionCallData, keccak256(""), address(0), address(0)); -// } - -// // Tests rejection of unauthorized amount in single transfer -// function test_unauthorizedAmount() public { -// uint256[] memory permittedTokenIds = new uint256[](1); -// uint256[] memory permittedValues = new uint256[](1); -// permittedTokenIds[0] = 1; -// permittedValues[0] = 1; - -// bytes memory terms = _encodeTerms(address(mockERC1155), permittedTokenIds, permittedValues, false); - -// Execution memory execution = Execution({ -// target: address(mockERC1155), -// value: 0, -// callData: abi.encodeWithSelector( -// IERC1155.safeTransferFrom.selector, -// address(users.alice.deleGator), -// address(users.bob.deleGator), -// 1, -// 2, // Different amount -// "" -// ) -// }); -// bytes memory executionCallData = ExecutionLib.encodeSingle(execution.target, execution.value, execution.callData); - -// vm.prank(address(delegationManager)); -// vm.expectRevert("ERC1155TransferEnforcer:unauthorized-amount"); -// erc1155TransferEnforcer.beforeHook(terms, "", mode, executionCallData, keccak256(""), address(0), address(0)); -// } - -// // Tests rejection of zero address in transfer -// function test_invalidAddress() public { -// uint256[] memory permittedTokenIds = new uint256[](1); -// uint256[] memory permittedValues = new uint256[](1); -// permittedTokenIds[0] = 1; -// permittedValues[0] = 1; - -// bytes memory terms = _encodeTerms(address(mockERC1155), permittedTokenIds, permittedValues, false); - -// Execution memory execution = Execution({ -// target: address(mockERC1155), -// value: 0, -// callData: abi.encodeWithSelector( -// IERC1155.safeTransferFrom.selector, -// address(0), // Invalid from address -// address(users.bob.deleGator), -// 1, -// 1, -// "" -// ) -// }); -// bytes memory executionCallData = ExecutionLib.encodeSingle(execution.target, execution.value, execution.callData); - -// vm.prank(address(delegationManager)); -// vm.expectRevert("ERC1155TransferEnforcer:invalid-address"); -// erc1155TransferEnforcer.beforeHook(terms, "", mode, executionCallData, keccak256(""), address(0), address(0)); -// } - -// // Tests rejection of unauthorized token ID in batch transfer -// function test_batchTransferWithUnauthorizedTokenId() public { -// uint256[] memory permittedTokenIds = new uint256[](1); -// uint256[] memory permittedValues = new uint256[](1); -// permittedTokenIds[0] = 1; -// permittedValues[0] = 1; - -// bytes memory terms = _encodeTerms(address(mockERC1155), permittedTokenIds, permittedValues, true); - -// uint256[] memory ids = new uint256[](2); -// uint256[] memory amounts = new uint256[](2); -// ids[0] = 1; -// ids[1] = 2; // Unauthorized token ID -// amounts[0] = 1; -// amounts[1] = 1; - -// Execution memory execution = Execution({ -// target: address(mockERC1155), -// value: 0, -// callData: abi.encodeWithSelector( -// IERC1155.safeBatchTransferFrom.selector, address(users.alice.deleGator), address(users.bob.deleGator), ids, -// amounts, "" -// ) -// }); -// bytes memory executionCallData = ExecutionLib.encodeSingle(execution.target, execution.value, execution.callData); - -// vm.prank(address(delegationManager)); -// vm.expectRevert("ERC1155TransferEnforcer:unauthorized-token-id"); -// erc1155TransferEnforcer.beforeHook(terms, "", mode, executionCallData, keccak256(""), address(0), address(0)); -// } - -// // Tests rejection of unauthorized amount in batch transfer -// function test_batchTransferWithUnauthorizedAmount() public { -// uint256[] memory permittedTokenIds = new uint256[](2); -// uint256[] memory permittedValues = new uint256[](2); -// permittedTokenIds[0] = 1; -// permittedTokenIds[1] = 2; -// permittedValues[0] = 1; -// permittedValues[1] = 1; - -// bytes memory terms = _encodeTerms(address(mockERC1155), permittedTokenIds, permittedValues, true); - -// uint256[] memory ids = new uint256[](2); -// uint256[] memory amounts = new uint256[](2); -// ids[0] = 1; -// ids[1] = 2; -// amounts[0] = 1; -// amounts[1] = 2; // Unauthorized amount - -// Execution memory execution = Execution({ -// target: address(mockERC1155), -// value: 0, -// callData: abi.encodeWithSelector( -// IERC1155.safeBatchTransferFrom.selector, address(users.alice.deleGator), address(users.bob.deleGator), ids, -// amounts, "" -// ) -// }); -// bytes memory executionCallData = ExecutionLib.encodeSingle(execution.target, execution.value, execution.callData); - -// vm.prank(address(delegationManager)); -// vm.expectRevert("ERC1155TransferEnforcer:unauthorized-amount"); -// erc1155TransferEnforcer.beforeHook(terms, "", mode, executionCallData, keccak256(""), address(0), address(0)); -// } - -// ////////////////////// Integration ////////////////////// - -// // Tests complete single token transfer flow with delegation -// function test_singleTransferIntegration() public { -// uint256[] memory permittedTokenIds = new uint256[](1); -// uint256[] memory permittedValues = new uint256[](1); -// permittedTokenIds[0] = 1; -// permittedValues[0] = 1; - -// bytes memory terms = _encodeTerms(address(mockERC1155), permittedTokenIds, permittedValues, false); - -// // Set up initial balances -// mockERC1155.mint(address(users.alice.deleGator), 1, 1, ""); - -// Execution memory execution = Execution({ -// target: address(mockERC1155), -// value: 0, -// callData: abi.encodeWithSelector( -// IERC1155.safeTransferFrom.selector, address(users.alice.deleGator), address(users.bob.deleGator), 1, 1, "" -// ) -// }); - -// Caveat[] memory caveats = new Caveat[](1); -// caveats[0] = Caveat({ args: "", enforcer: address(erc1155TransferEnforcer), terms: terms }); - -// Delegation memory delegation = Delegation({ -// delegate: address(users.bob.deleGator), -// delegator: address(users.alice.deleGator), -// authority: ROOT_AUTHORITY, -// caveats: caveats, -// salt: 0, -// signature: "" -// }); - -// delegation = signDelegation(users.alice, delegation); - -// Delegation[] memory delegations = new Delegation[](1); -// delegations[0] = delegation; - -// // Execute the transfer -// invokeDelegation_UserOp(users.bob, delegations, execution); - -// // Verify the transfer -// assertEq(mockERC1155.balanceOf(address(users.alice.deleGator), 1), 0); -// assertEq(mockERC1155.balanceOf(address(users.bob.deleGator), 1), 1); -// } - -// // Tests complete batch token transfer flow with delegation -// function test_batchTransferIntegration() public { -// uint256[] memory permittedTokenIds = new uint256[](2); -// uint256[] memory permittedValues = new uint256[](2); -// permittedTokenIds[0] = 1; -// permittedTokenIds[1] = 2; -// permittedValues[0] = 1; -// permittedValues[1] = 1; - -// bytes memory terms = _encodeTerms(address(mockERC1155), permittedTokenIds, permittedValues, true); - -// // Set up initial balances -// mockERC1155.mint(address(users.alice.deleGator), 1, 1, ""); -// mockERC1155.mint(address(users.alice.deleGator), 2, 1, ""); - -// uint256[] memory ids = new uint256[](2); -// uint256[] memory amounts = new uint256[](2); -// ids[0] = 1; -// ids[1] = 2; -// amounts[0] = 1; -// amounts[1] = 1; - -// Execution memory execution = Execution({ -// target: address(mockERC1155), -// value: 0, -// callData: abi.encodeWithSelector( -// IERC1155.safeBatchTransferFrom.selector, address(users.alice.deleGator), address(users.bob.deleGator), ids, -// amounts, "" -// ) -// }); - -// Caveat[] memory caveats = new Caveat[](1); -// caveats[0] = Caveat({ args: "", enforcer: address(erc1155TransferEnforcer), terms: terms }); - -// Delegation memory delegation = Delegation({ -// delegate: address(users.bob.deleGator), -// delegator: address(users.alice.deleGator), -// authority: ROOT_AUTHORITY, -// caveats: caveats, -// salt: 0, -// signature: "" -// }); - -// delegation = signDelegation(users.alice, delegation); - -// Delegation[] memory delegations = new Delegation[](1); -// delegations[0] = delegation; - -// // Execute the transfer -// invokeDelegation_UserOp(users.bob, delegations, execution); - -// // Verify the transfers -// assertEq(mockERC1155.balanceOf(address(users.alice.deleGator), 1), 0); -// assertEq(mockERC1155.balanceOf(address(users.alice.deleGator), 2), 0); -// assertEq(mockERC1155.balanceOf(address(users.bob.deleGator), 1), 1); -// assertEq(mockERC1155.balanceOf(address(users.bob.deleGator), 2), 1); -// } - -// function _getEnforcer() internal view override returns (ICaveatEnforcer) { -// return ICaveatEnforcer(address(erc1155TransferEnforcer)); -// } -// } +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import "forge-std/Test.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; + +import { Execution, Caveat, Delegation, ModeCode } from "../../src/utils/Types.sol"; +import { CaveatEnforcerBaseTest } from "./CaveatEnforcerBaseTest.t.sol"; +import { BasicERC1155 } from "../utils/BasicERC1155.t.sol"; +import { EncoderLib } from "../../src/libraries/EncoderLib.sol"; +import { IDelegationManager } from "../../src/interfaces/IDelegationManager.sol"; +import { ICaveatEnforcer } from "../../src/interfaces/ICaveatEnforcer.sol"; +import { Implementation, SignatureType } from "../utils/Types.t.sol"; +import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import { IERC1155 } from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import { ERC1155TransferEnforcer } from "../../src/enforcers/ERC1155TransferEnforcer.sol"; + +/** + * @title ERC1155TransferEnforcerTest + * @notice Comprehensive tests for ERC1155TransferEnforcer following ERC20TransferAmountEnforcer structure + */ +contract ERC1155TransferEnforcerTest is CaveatEnforcerBaseTest { + using MessageHashUtils for bytes32; + using ModeLib for ModeCode; + + ////////////////////// State ////////////////////// + + ERC1155TransferEnforcer public erc1155TransferEnforcer; + BasicERC1155 public basicERC1155; + BasicERC1155 public invalidERC1155; + + // Test parameters + uint256 constant TOKEN_ID_1 = 1; + uint256 constant TOKEN_ID_2 = 2; + uint256 constant TOKEN_ID_3 = 3; + uint256 constant TRANSFER_LIMIT_1 = 100; + uint256 constant TRANSFER_LIMIT_2 = 200; + + constructor() { + IMPLEMENTATION = Implementation.Hybrid; + SIGNATURE_TYPE = SignatureType.RawP256; + } + + ////////////////////// Set up ////////////////////// + + function setUp() public override { + super.setUp(); + erc1155TransferEnforcer = new ERC1155TransferEnforcer(); + vm.label(address(erc1155TransferEnforcer), "ERC1155TransferEnforcer"); + + basicERC1155 = new BasicERC1155(address(users.alice.deleGator), "TestERC1155", "T1155", "https://test.com/"); + invalidERC1155 = new BasicERC1155(address(users.alice.deleGator), "InvalidERC1155", "I1155", "https://invalid.com/"); + + // Mint initial tokens for testing + vm.startPrank(address(users.alice.deleGator)); + basicERC1155.mint(address(users.alice.deleGator), TOKEN_ID_1, 1000, ""); + basicERC1155.mint(address(users.alice.deleGator), TOKEN_ID_2, 1000, ""); + basicERC1155.mint(address(users.alice.deleGator), TOKEN_ID_3, 1000, ""); + vm.stopPrank(); + + // Fund wallets with ETH for gas + vm.deal(address(users.alice.deleGator), 10 ether); + vm.deal(address(users.bob.deleGator), 10 ether); + + // Labels + vm.label(address(basicERC1155), "BasicERC1155"); + vm.label(address(invalidERC1155), "InvalidERC1155"); + } + + ////////////////////// Helper Functions ////////////////////// + + function _encodeSingleTerms(address _contract, uint256 _tokenId, uint256 _amount) internal pure returns (bytes memory) { + return abi.encode(_contract, _tokenId, _amount); + } + + function _encodeBatchTerms( + address _contract, + uint256[] memory _tokenIds, + uint256[] memory _amounts + ) + internal + pure + returns (bytes memory) + { + return abi.encode(_contract, _tokenIds, _amounts); + } + + ////////////////////// Valid cases ////////////////////// + + // should SUCCEED to INVOKE single transfer BELOW enforcer allowance + function test_singleTransferSucceedsIfCalledBelowAllowance() public { + uint256 transferAmount_ = 50; + + Execution memory execution_ = Execution({ + target: address(basicERC1155), + value: 0, + callData: abi.encodeWithSelector( + IERC1155.safeTransferFrom.selector, + address(users.alice.deleGator), + address(users.bob.deleGator), + TOKEN_ID_1, + transferAmount_, + "" + ) + }); + + bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData); + bytes memory inputTerms_ = _encodeSingleTerms(address(basicERC1155), TOKEN_ID_1, TRANSFER_LIMIT_1); + + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = Caveat({ args: hex"", enforcer: address(erc1155TransferEnforcer), terms: inputTerms_ }); + + Delegation memory delegation_ = Delegation({ + delegate: address(users.bob.deleGator), + delegator: address(users.alice.deleGator), + authority: ROOT_AUTHORITY, + caveats: caveats_, + salt: 0, + signature: hex"" + }); + + bytes32 delegationHash_ = EncoderLib._getDelegationHash(delegation_); + assertEq(erc1155TransferEnforcer.spentMap(address(delegationManager), delegationHash_, TOKEN_ID_1), 0); + + vm.prank(address(delegationManager)); + vm.expectEmit(true, true, true, true, address(erc1155TransferEnforcer)); + emit ERC1155TransferEnforcer.IncreasedSpentMap( + address(delegationManager), delegationHash_, TOKEN_ID_1, TRANSFER_LIMIT_1, transferAmount_ + ); + + erc1155TransferEnforcer.beforeHook( + inputTerms_, hex"", singleDefaultMode, executionCallData_, delegationHash_, address(0), address(0) + ); + + assertEq(erc1155TransferEnforcer.spentMap(address(delegationManager), delegationHash_, TOKEN_ID_1), transferAmount_); + } + + // should SUCCEED to INVOKE batch transfer BELOW enforcer allowances + function test_batchTransferSucceedsIfCalledBelowAllowances() public { + uint256[] memory tokenIds_ = new uint256[](2); + uint256[] memory transferAmounts_ = new uint256[](2); + uint256[] memory limits_ = new uint256[](2); + + tokenIds_[0] = TOKEN_ID_1; + tokenIds_[1] = TOKEN_ID_2; + transferAmounts_[0] = 30; + transferAmounts_[1] = 50; + limits_[0] = TRANSFER_LIMIT_1; + limits_[1] = TRANSFER_LIMIT_2; + + Execution memory execution_ = Execution({ + target: address(basicERC1155), + value: 0, + callData: abi.encodeWithSelector( + IERC1155.safeBatchTransferFrom.selector, + address(users.alice.deleGator), + address(users.bob.deleGator), + tokenIds_, + transferAmounts_, + "" + ) + }); + + bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData); + bytes memory inputTerms_ = _encodeBatchTerms(address(basicERC1155), tokenIds_, limits_); + + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = Caveat({ args: hex"", enforcer: address(erc1155TransferEnforcer), terms: inputTerms_ }); + + Delegation memory delegation_ = Delegation({ + delegate: address(users.bob.deleGator), + delegator: address(users.alice.deleGator), + authority: ROOT_AUTHORITY, + caveats: caveats_, + salt: 0, + signature: hex"" + }); + + bytes32 delegationHash_ = EncoderLib._getDelegationHash(delegation_); + assertEq(erc1155TransferEnforcer.spentMap(address(delegationManager), delegationHash_, TOKEN_ID_1), 0); + assertEq(erc1155TransferEnforcer.spentMap(address(delegationManager), delegationHash_, TOKEN_ID_2), 0); + + vm.prank(address(delegationManager)); + erc1155TransferEnforcer.beforeHook( + inputTerms_, hex"", singleDefaultMode, executionCallData_, delegationHash_, address(0), address(0) + ); + + assertEq(erc1155TransferEnforcer.spentMap(address(delegationManager), delegationHash_, TOKEN_ID_1), transferAmounts_[0]); + assertEq(erc1155TransferEnforcer.spentMap(address(delegationManager), delegationHash_, TOKEN_ID_2), transferAmounts_[1]); + } + + // should SUCCEED twice but FAIL on third single transfer when limit is reached + function test_singleTransferMultipleCallsReachesLimit() public { + uint256 transferAmount_ = 50; + uint256 spendingLimit_ = 100; + bytes32 delegationHash_ = keccak256("testDelegation"); + + bytes memory inputTerms_ = _encodeSingleTerms(address(basicERC1155), TOKEN_ID_1, spendingLimit_); + + // Create single execution to reuse + Execution memory execution_ = Execution({ + target: address(basicERC1155), + value: 0, + callData: abi.encodeWithSelector( + IERC1155.safeTransferFrom.selector, + address(users.alice.deleGator), + address(users.bob.deleGator), + TOKEN_ID_1, + transferAmount_, + "" + ) + }); + bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData); + + assertEq(erc1155TransferEnforcer.spentMap(address(delegationManager), delegationHash_, TOKEN_ID_1), 0); + + // First transfer - should succeed + vm.prank(address(delegationManager)); + erc1155TransferEnforcer.beforeHook( + inputTerms_, hex"", singleDefaultMode, executionCallData_, delegationHash_, address(0), address(0) + ); + assertEq(erc1155TransferEnforcer.spentMap(address(delegationManager), delegationHash_, TOKEN_ID_1), transferAmount_); + + // Second transfer - should succeed + vm.prank(address(delegationManager)); + erc1155TransferEnforcer.beforeHook( + inputTerms_, hex"", singleDefaultMode, executionCallData_, delegationHash_, address(0), address(0) + ); + assertEq(erc1155TransferEnforcer.spentMap(address(delegationManager), delegationHash_, TOKEN_ID_1), spendingLimit_); + + // Third transfer - should fail (limit reached) + vm.prank(address(delegationManager)); + vm.expectRevert("ERC1155TransferEnforcer:unauthorized-amount"); + erc1155TransferEnforcer.beforeHook( + inputTerms_, hex"", singleDefaultMode, executionCallData_, delegationHash_, address(0), address(0) + ); + + // Spent amount should remain at limit + assertEq(erc1155TransferEnforcer.spentMap(address(delegationManager), delegationHash_, TOKEN_ID_1), spendingLimit_); + } + + // should SUCCEED twice but FAIL on third batch transfer when limit is reached + function test_batchTransferMultipleCallsReachesLimit() public { + uint256 transferAmount_ = 50; + uint256 spendingLimit_ = 100; + bytes32 delegationHash_ = keccak256("testDelegationBatch"); + + uint256[] memory tokenIds_ = new uint256[](1); + uint256[] memory transferAmounts_ = new uint256[](1); + uint256[] memory limits_ = new uint256[](1); + + tokenIds_[0] = TOKEN_ID_1; + transferAmounts_[0] = transferAmount_; + limits_[0] = spendingLimit_; + + bytes memory inputTerms_ = _encodeBatchTerms(address(basicERC1155), tokenIds_, limits_); + + // Create single execution to reuse + Execution memory execution_ = Execution({ + target: address(basicERC1155), + value: 0, + callData: abi.encodeWithSelector( + IERC1155.safeBatchTransferFrom.selector, + address(users.alice.deleGator), + address(users.bob.deleGator), + tokenIds_, + transferAmounts_, + "" + ) + }); + bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData); + + assertEq(erc1155TransferEnforcer.spentMap(address(delegationManager), delegationHash_, TOKEN_ID_1), 0); + + // First batch transfer - should succeed + vm.prank(address(delegationManager)); + erc1155TransferEnforcer.beforeHook( + inputTerms_, hex"", singleDefaultMode, executionCallData_, delegationHash_, address(0), address(0) + ); + assertEq(erc1155TransferEnforcer.spentMap(address(delegationManager), delegationHash_, TOKEN_ID_1), transferAmount_); + + // Second batch transfer - should succeed + vm.prank(address(delegationManager)); + erc1155TransferEnforcer.beforeHook( + inputTerms_, hex"", singleDefaultMode, executionCallData_, delegationHash_, address(0), address(0) + ); + assertEq(erc1155TransferEnforcer.spentMap(address(delegationManager), delegationHash_, TOKEN_ID_1), spendingLimit_); + + // Third batch transfer - should fail (limit reached) + vm.prank(address(delegationManager)); + vm.expectRevert("ERC1155TransferEnforcer:unauthorized-amount"); + erc1155TransferEnforcer.beforeHook( + inputTerms_, hex"", singleDefaultMode, executionCallData_, delegationHash_, address(0), address(0) + ); + + // Spent amount should remain at limit + assertEq(erc1155TransferEnforcer.spentMap(address(delegationManager), delegationHash_, TOKEN_ID_1), spendingLimit_); + } + + ////////////////////// Invalid cases ////////////////////// + + // should FAIL to INVOKE single transfer ABOVE enforcer allowance + function test_singleTransferFailsIfCalledAboveAllowance() public { + uint256 transferAmount_ = TRANSFER_LIMIT_1 + 1; + + Execution memory execution_ = Execution({ + target: address(basicERC1155), + value: 0, + callData: abi.encodeWithSelector( + IERC1155.safeTransferFrom.selector, + address(users.alice.deleGator), + address(users.bob.deleGator), + TOKEN_ID_1, + transferAmount_, + "" + ) + }); + + bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData); + bytes memory inputTerms_ = _encodeSingleTerms(address(basicERC1155), TOKEN_ID_1, TRANSFER_LIMIT_1); + + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = Caveat({ args: hex"", enforcer: address(erc1155TransferEnforcer), terms: inputTerms_ }); + + Delegation memory delegation_ = Delegation({ + delegate: address(users.bob.deleGator), + delegator: address(users.alice.deleGator), + authority: ROOT_AUTHORITY, + caveats: caveats_, + salt: 0, + signature: hex"" + }); + + bytes32 delegationHash_ = EncoderLib._getDelegationHash(delegation_); + assertEq(erc1155TransferEnforcer.spentMap(address(delegationManager), delegationHash_, TOKEN_ID_1), 0); + + vm.prank(address(delegationManager)); + vm.expectRevert("ERC1155TransferEnforcer:unauthorized-amount"); + erc1155TransferEnforcer.beforeHook( + inputTerms_, hex"", singleDefaultMode, executionCallData_, delegationHash_, address(0), address(0) + ); + + assertEq(erc1155TransferEnforcer.spentMap(address(delegationManager), delegationHash_, TOKEN_ID_1), 0); + } + + // should FAIL to INVOKE batch transfer with one amount ABOVE enforcer allowance + function test_batchTransferFailsIfOneAmountAboveAllowance() public { + uint256[] memory tokenIds_ = new uint256[](2); + uint256[] memory transferAmounts_ = new uint256[](2); + uint256[] memory limits_ = new uint256[](2); + + tokenIds_[0] = TOKEN_ID_1; + tokenIds_[1] = TOKEN_ID_2; + transferAmounts_[0] = 30; + transferAmounts_[1] = TRANSFER_LIMIT_2 + 1; // Above limit + limits_[0] = TRANSFER_LIMIT_1; + limits_[1] = TRANSFER_LIMIT_2; + + Execution memory execution_ = Execution({ + target: address(basicERC1155), + value: 0, + callData: abi.encodeWithSelector( + IERC1155.safeBatchTransferFrom.selector, + address(users.alice.deleGator), + address(users.bob.deleGator), + tokenIds_, + transferAmounts_, + "" + ) + }); + + bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData); + bytes memory inputTerms_ = _encodeBatchTerms(address(basicERC1155), tokenIds_, limits_); + + bytes32 delegationHash_ = keccak256("test"); + + vm.prank(address(delegationManager)); + vm.expectRevert("ERC1155TransferEnforcer:unauthorized-amount"); + erc1155TransferEnforcer.beforeHook( + inputTerms_, hex"", singleDefaultMode, executionCallData_, delegationHash_, address(0), address(0) + ); + } + + // should FAIL to INVOKE invalid ERC1155-contract + function test_methodFailsIfInvokesInvalidContract() public { + uint256 transferAmount_ = 50; + + Execution memory execution_ = Execution({ + target: address(basicERC1155), + value: 0, + callData: abi.encodeWithSelector( + IERC1155.safeTransferFrom.selector, + address(users.alice.deleGator), + address(users.bob.deleGator), + TOKEN_ID_1, + transferAmount_, + "" + ) + }); + + bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData); + bytes memory inputTerms_ = _encodeSingleTerms(address(invalidERC1155), TOKEN_ID_1, TRANSFER_LIMIT_1); + + bytes32 delegationHash_ = keccak256("test"); + + vm.prank(address(delegationManager)); + vm.expectRevert("ERC1155TransferEnforcer:unauthorized-contract-target"); + erc1155TransferEnforcer.beforeHook( + inputTerms_, hex"", singleDefaultMode, executionCallData_, delegationHash_, address(0), address(0) + ); + } + + // should FAIL to INVOKE invalid execution data length + function test_notAllow_invalidExecutionLength() public { + Execution memory execution_ = Execution({ + target: address(basicERC1155), + value: 0, + callData: abi.encodeWithSelector( + IERC1155.safeBatchTransferFrom.selector, + address(users.alice.deleGator), + address(users.bob.deleGator), + new uint256[](0), // Empty array + new uint256[](0), // Empty array + "" + ) + }); + + bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData); + bytes memory inputTerms_ = _encodeSingleTerms(address(basicERC1155), TOKEN_ID_1, TRANSFER_LIMIT_1); + + bytes32 delegationHash_ = keccak256("test"); + + vm.prank(address(delegationManager)); + vm.expectRevert("ERC1155TransferEnforcer:invalid-calldata-length"); + erc1155TransferEnforcer.beforeHook( + inputTerms_, hex"", singleDefaultMode, executionCallData_, delegationHash_, address(0), address(0) + ); + } + + // should FAIL to INVOKE invalid method selector for single transfer + function test_methodFailsIfInvokesInvalidSingleSelector() public { + Execution memory execution_ = Execution({ + target: address(basicERC1155), + value: 0, + callData: abi.encodeWithSelector( + IERC1155.safeBatchTransferFrom.selector, // Wrong selector for single transfer + address(users.alice.deleGator), + address(users.bob.deleGator), + new uint256[](1), + new uint256[](1), + "" + ) + }); + + bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData); + bytes memory inputTerms_ = _encodeSingleTerms(address(basicERC1155), TOKEN_ID_1, TRANSFER_LIMIT_1); + + bytes32 delegationHash_ = keccak256("test"); + + vm.prank(address(delegationManager)); + vm.expectRevert("ERC1155TransferEnforcer:unauthorized-selector-single"); + erc1155TransferEnforcer.beforeHook( + inputTerms_, hex"", singleDefaultMode, executionCallData_, delegationHash_, address(0), address(0) + ); + } + + // should FAIL to INVOKE invalid method selector for batch transfer + function test_methodFailsIfInvokesInvalidBatchSelector() public { + uint256[] memory tokenIds_ = new uint256[](1); + uint256[] memory limits_ = new uint256[](1); + tokenIds_[0] = TOKEN_ID_1; + limits_[0] = TRANSFER_LIMIT_1; + + Execution memory execution_ = Execution({ + target: address(basicERC1155), + value: 0, + callData: abi.encodeWithSelector( + IERC1155.safeTransferFrom.selector, // Wrong selector for batch transfer + address(users.alice.deleGator), + address(users.bob.deleGator), + TOKEN_ID_1, + 50, + "" + ) + }); + + bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData); + bytes memory inputTerms_ = _encodeBatchTerms(address(basicERC1155), tokenIds_, limits_); + + bytes32 delegationHash_ = keccak256("test"); + + vm.prank(address(delegationManager)); + vm.expectRevert("ERC1155TransferEnforcer:unauthorized-selector-batch"); + erc1155TransferEnforcer.beforeHook( + inputTerms_, hex"", singleDefaultMode, executionCallData_, delegationHash_, address(0), address(0) + ); + } + + // should FAIL to INVOKE invalid terms length + function test_methodFailsIfInvokesInvalidTermsLength() public { + bytes memory inputTerms_ = abi.encode(address(basicERC1155)); // Too short + + vm.expectRevert("ERC1155TransferEnforcer:invalid-terms-length"); + erc1155TransferEnforcer.getTermsInfo(inputTerms_); + + inputTerms_ = abi.encode(address(basicERC1155), TOKEN_ID_1); + + // Empty arrays + uint256[] memory tokenIds_ = new uint256[](0); + uint256[] memory transferAmounts_ = new uint256[](0); + inputTerms_ = abi.encode(address(basicERC1155), tokenIds_, transferAmounts_); + vm.expectRevert("ERC1155TransferEnforcer:invalid-terms-length"); + erc1155TransferEnforcer.getTermsInfo(inputTerms_); + } + + // should FAIL to get terms info when passing zero address + function test_getTermsInfoFailsForZeroAddress() public { + bytes memory inputTerms_ = _encodeSingleTerms(address(0), TOKEN_ID_1, TRANSFER_LIMIT_1); + + vm.expectRevert("ERC1155TransferEnforcer:invalid-contract-address"); + erc1155TransferEnforcer.getTermsInfo(inputTerms_); + } + + // should FAIL to get terms info when arrays have different lengths + function test_getTermsInfoFailsForMismatchedArrayLengths() public { + uint256[] memory tokenIds_ = new uint256[](2); + uint256[] memory limits_ = new uint256[](1); // Different length + tokenIds_[0] = TOKEN_ID_1; + tokenIds_[1] = TOKEN_ID_2; + limits_[0] = TRANSFER_LIMIT_1; + + bytes memory inputTerms_ = _encodeBatchTerms(address(basicERC1155), tokenIds_, limits_); + + vm.expectRevert("ERC1155TransferEnforcer:invalid-ids-values-length"); + erc1155TransferEnforcer.getTermsInfo(inputTerms_); + } + + // should FAIL with unauthorized token ID in single transfer + function test_unauthorizedTokenIdInSingleTransfer() public { + uint256 transferAmount_ = 50; + + Execution memory execution_ = Execution({ + target: address(basicERC1155), + value: 0, + callData: abi.encodeWithSelector( + IERC1155.safeTransferFrom.selector, + address(users.alice.deleGator), + address(users.bob.deleGator), + TOKEN_ID_2, // Different token ID + transferAmount_, + "" + ) + }); + + bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData); + bytes memory inputTerms_ = _encodeSingleTerms(address(basicERC1155), TOKEN_ID_1, TRANSFER_LIMIT_1); + + bytes32 delegationHash_ = keccak256("test"); + + vm.prank(address(delegationManager)); + vm.expectRevert("ERC1155TransferEnforcer:unauthorized-token-id"); + erc1155TransferEnforcer.beforeHook( + inputTerms_, hex"", singleDefaultMode, executionCallData_, delegationHash_, address(0), address(0) + ); + } + + // should FAIL with unauthorized token ID in batch transfer + function test_unauthorizedTokenIdInBatchTransfer() public { + uint256[] memory permittedTokenIds_ = new uint256[](1); + uint256[] memory limits_ = new uint256[](1); + permittedTokenIds_[0] = TOKEN_ID_1; + limits_[0] = TRANSFER_LIMIT_1; + + uint256[] memory transferTokenIds_ = new uint256[](2); + uint256[] memory transferAmounts_ = new uint256[](2); + transferTokenIds_[0] = TOKEN_ID_1; + transferTokenIds_[1] = TOKEN_ID_3; // Unauthorized token ID + transferAmounts_[0] = 30; + transferAmounts_[1] = 40; + + Execution memory execution_ = Execution({ + target: address(basicERC1155), + value: 0, + callData: abi.encodeWithSelector( + IERC1155.safeBatchTransferFrom.selector, + address(users.alice.deleGator), + address(users.bob.deleGator), + transferTokenIds_, + transferAmounts_, + "" + ) + }); + + bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData); + bytes memory inputTerms_ = _encodeBatchTerms(address(basicERC1155), permittedTokenIds_, limits_); + + bytes32 delegationHash_ = keccak256("test"); + + vm.prank(address(delegationManager)); + vm.expectRevert("ERC1155TransferEnforcer:unauthorized-token-id"); + erc1155TransferEnforcer.beforeHook( + inputTerms_, hex"", singleDefaultMode, executionCallData_, delegationHash_, address(0), address(0) + ); + } + + // should FAIL with zero address in transfer + function test_invalidFromAddress() public { + uint256 transferAmount_ = 50; + + Execution memory execution_ = Execution({ + target: address(basicERC1155), + value: 0, + callData: abi.encodeWithSelector( + IERC1155.safeTransferFrom.selector, + address(0), // Invalid from address + address(users.bob.deleGator), + TOKEN_ID_1, + transferAmount_, + "" + ) + }); + + bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData); + bytes memory inputTerms_ = _encodeSingleTerms(address(basicERC1155), TOKEN_ID_1, TRANSFER_LIMIT_1); + + bytes32 delegationHash_ = keccak256("test"); + + vm.prank(address(delegationManager)); + vm.expectRevert("ERC1155TransferEnforcer:invalid-address"); + erc1155TransferEnforcer.beforeHook( + inputTerms_, hex"", singleDefaultMode, executionCallData_, delegationHash_, address(0), address(0) + ); + } + + // should FAIL with zero address in transfer + function test_invalidToAddress() public { + uint256 transferAmount_ = 50; + + Execution memory execution_ = Execution({ + target: address(basicERC1155), + value: 0, + callData: abi.encodeWithSelector( + IERC1155.safeTransferFrom.selector, + address(users.alice.deleGator), + address(0), // Invalid to address + TOKEN_ID_1, + transferAmount_, + "" + ) + }); + + bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData); + bytes memory inputTerms_ = _encodeSingleTerms(address(basicERC1155), TOKEN_ID_1, TRANSFER_LIMIT_1); + + bytes32 delegationHash_ = keccak256("test"); + + vm.prank(address(delegationManager)); + vm.expectRevert("ERC1155TransferEnforcer:invalid-address"); + erc1155TransferEnforcer.beforeHook( + inputTerms_, hex"", singleDefaultMode, executionCallData_, delegationHash_, address(0), address(0) + ); + } + + // should FAIL with non-zero value + function test_invalidNonZeroValue() public { + uint256 transferAmount_ = 50; + + Execution memory execution_ = Execution({ + target: address(basicERC1155), + value: 1 ether, // Non-zero value + callData: abi.encodeWithSelector( + IERC1155.safeTransferFrom.selector, + address(users.alice.deleGator), + address(users.bob.deleGator), + TOKEN_ID_1, + transferAmount_, + "" + ) + }); + + bytes memory executionCallData_ = ExecutionLib.encodeSingle(execution_.target, execution_.value, execution_.callData); + bytes memory inputTerms_ = _encodeSingleTerms(address(basicERC1155), TOKEN_ID_1, TRANSFER_LIMIT_1); + + bytes32 delegationHash_ = keccak256("test"); + + vm.prank(address(delegationManager)); + vm.expectRevert("ERC1155TransferEnforcer:invalid-value"); + erc1155TransferEnforcer.beforeHook( + inputTerms_, hex"", singleDefaultMode, executionCallData_, delegationHash_, address(0), address(0) + ); + } + + // should NOT transfer when max allowance is reached + function test_transferFailsAboveAllowance() public { + uint256 spendingLimit_ = 100; + uint256 firstTransfer_ = 60; + + assertEq(basicERC1155.balanceOf(address(users.alice.deleGator), TOKEN_ID_1), 1000); + assertEq(basicERC1155.balanceOf(address(users.bob.deleGator), TOKEN_ID_1), 0); + + Execution memory execution1_ = Execution({ + target: address(basicERC1155), + value: 0, + callData: abi.encodeWithSelector( + IERC1155.safeTransferFrom.selector, + address(users.alice.deleGator), + address(users.bob.deleGator), + TOKEN_ID_1, + firstTransfer_, + "" + ) + }); + + bytes memory inputTerms_ = _encodeSingleTerms(address(basicERC1155), TOKEN_ID_1, spendingLimit_); + + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = Caveat({ args: hex"", enforcer: address(erc1155TransferEnforcer), terms: inputTerms_ }); + + 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_); + bytes32 delegationHash_ = EncoderLib._getDelegationHash(delegation_); + assertEq(erc1155TransferEnforcer.spentMap(address(delegationManager), delegationHash_, TOKEN_ID_1), 0); + + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + + // First transfer - should succeed + invokeDelegation_UserOp(users.bob, delegations_, execution1_); + + assertEq(basicERC1155.balanceOf(address(users.alice.deleGator), TOKEN_ID_1), 940); + assertEq(basicERC1155.balanceOf(address(users.bob.deleGator), TOKEN_ID_1), 60); + assertEq(erc1155TransferEnforcer.spentMap(address(delegationManager), delegationHash_, TOKEN_ID_1), firstTransfer_); + + // Second transfer - should succeed (40 more to reach limit) + Execution memory execution2_ = Execution({ + target: address(basicERC1155), + value: 0, + callData: abi.encodeWithSelector( + IERC1155.safeTransferFrom.selector, address(users.alice.deleGator), address(users.bob.deleGator), TOKEN_ID_1, 40, "" + ) + }); + + invokeDelegation_UserOp(users.bob, delegations_, execution2_); + assertEq(basicERC1155.balanceOf(address(users.alice.deleGator), TOKEN_ID_1), 900); + assertEq(basicERC1155.balanceOf(address(users.bob.deleGator), TOKEN_ID_1), 100); + assertEq(erc1155TransferEnforcer.spentMap(address(delegationManager), delegationHash_, TOKEN_ID_1), spendingLimit_); + + // Third transfer - should fail (attempt transfer above allowance: balances should remain unchanged) + Execution memory execution3_ = Execution({ + target: address(basicERC1155), + value: 0, + callData: abi.encodeWithSelector( + IERC1155.safeTransferFrom.selector, address(users.alice.deleGator), address(users.bob.deleGator), TOKEN_ID_1, 1, "" + ) + }); + + invokeDelegation_UserOp(users.bob, delegations_, execution3_); + assertEq(basicERC1155.balanceOf(address(users.alice.deleGator), TOKEN_ID_1), 900); + assertEq(basicERC1155.balanceOf(address(users.bob.deleGator), TOKEN_ID_1), 100); + assertEq(erc1155TransferEnforcer.spentMap(address(delegationManager), delegationHash_, TOKEN_ID_1), spendingLimit_); + } + + // should fail with invalid call type mode (batch instead of single mode) + function test_revertWithInvalidCallTypeMode() public { + bytes memory executionCallData_ = ExecutionLib.encodeBatch(new Execution[](2)); + vm.expectRevert("CaveatEnforcer:invalid-call-type"); + erc1155TransferEnforcer.beforeHook(hex"", hex"", batchDefaultMode, executionCallData_, bytes32(0), address(0), address(0)); + } + + // should fail with invalid call type mode (try instead of default) + function test_revertWithInvalidExecutionMode() public { + vm.prank(address(delegationManager)); + vm.expectRevert("CaveatEnforcer:invalid-execution-type"); + erc1155TransferEnforcer.beforeHook(hex"", hex"", singleTryMode, hex"", bytes32(0), address(0), address(0)); + } + + ////////////////////// Integration ////////////////////// + + // should allow single token transfer integration + function test_singleTransferIntegration() public { + uint256 transferAmount_ = 50; + + Execution memory execution_ = Execution({ + target: address(basicERC1155), + value: 0, + callData: abi.encodeWithSelector( + IERC1155.safeTransferFrom.selector, + address(users.alice.deleGator), + address(users.bob.deleGator), + TOKEN_ID_1, + transferAmount_, + "" + ) + }); + + bytes memory inputTerms_ = _encodeSingleTerms(address(basicERC1155), TOKEN_ID_1, TRANSFER_LIMIT_1); + + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = Caveat({ args: hex"", enforcer: address(erc1155TransferEnforcer), terms: inputTerms_ }); + + 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_); + bytes32 delegationHash_ = EncoderLib._getDelegationHash(delegation_); + + // Execute Bob's UserOp + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + + // Enforcer allows the delegation + invokeDelegation_UserOp(users.bob, delegations_, execution_); + + // Verify the transfer + assertEq(basicERC1155.balanceOf(address(users.alice.deleGator), TOKEN_ID_1), 950); + assertEq(basicERC1155.balanceOf(address(users.bob.deleGator), TOKEN_ID_1), 50); + assertEq(erc1155TransferEnforcer.spentMap(address(delegationManager), delegationHash_, TOKEN_ID_1), transferAmount_); + + // Enforcer allows to reuse the delegation + invokeDelegation_UserOp(users.bob, delegations_, execution_); + + // Verify second transfer + assertEq(basicERC1155.balanceOf(address(users.alice.deleGator), TOKEN_ID_1), 900); + assertEq(basicERC1155.balanceOf(address(users.bob.deleGator), TOKEN_ID_1), 100); + assertEq(erc1155TransferEnforcer.spentMap(address(delegationManager), delegationHash_, TOKEN_ID_1), transferAmount_ * 2); + } + + // should allow batch token transfer integration + function test_batchTransferIntegration() public { + uint256[] memory tokenIds_ = new uint256[](2); + uint256[] memory transferAmounts_ = new uint256[](2); + uint256[] memory limits_ = new uint256[](2); + + tokenIds_[0] = TOKEN_ID_1; + tokenIds_[1] = TOKEN_ID_2; + transferAmounts_[0] = 30; + transferAmounts_[1] = 50; + limits_[0] = TRANSFER_LIMIT_1; + limits_[1] = TRANSFER_LIMIT_2; + + Execution memory execution_ = Execution({ + target: address(basicERC1155), + value: 0, + callData: abi.encodeWithSelector( + IERC1155.safeBatchTransferFrom.selector, + address(users.alice.deleGator), + address(users.bob.deleGator), + tokenIds_, + transferAmounts_, + "" + ) + }); + + bytes memory inputTerms_ = _encodeBatchTerms(address(basicERC1155), tokenIds_, limits_); + + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = Caveat({ args: hex"", enforcer: address(erc1155TransferEnforcer), terms: inputTerms_ }); + + 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_); + bytes32 delegationHash_ = EncoderLib._getDelegationHash(delegation_); + + // Execute Bob's UserOp + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + + // Enforcer allows the delegation + invokeDelegation_UserOp(users.bob, delegations_, execution_); + + // Verify the transfers + assertEq(basicERC1155.balanceOf(address(users.alice.deleGator), TOKEN_ID_1), 970); + assertEq(basicERC1155.balanceOf(address(users.alice.deleGator), TOKEN_ID_2), 950); + assertEq(basicERC1155.balanceOf(address(users.bob.deleGator), TOKEN_ID_1), 30); + assertEq(basicERC1155.balanceOf(address(users.bob.deleGator), TOKEN_ID_2), 50); + assertEq(erc1155TransferEnforcer.spentMap(address(delegationManager), delegationHash_, TOKEN_ID_1), transferAmounts_[0]); + assertEq(erc1155TransferEnforcer.spentMap(address(delegationManager), delegationHash_, TOKEN_ID_2), transferAmounts_[1]); + } + + // should NOT allow unauthorized amounts in batch transfer integration + function test_batchTransferFailsAboveAllowanceIntegration() public { + uint256[] memory tokenIds_ = new uint256[](2); + uint256[] memory transferAmounts_ = new uint256[](2); + uint256[] memory limits_ = new uint256[](2); + + tokenIds_[0] = TOKEN_ID_1; + tokenIds_[1] = TOKEN_ID_2; + transferAmounts_[0] = TRANSFER_LIMIT_1 + 1; // Exceeds limit + transferAmounts_[1] = 50; + limits_[0] = TRANSFER_LIMIT_1; + limits_[1] = TRANSFER_LIMIT_2; + + Execution memory execution_ = Execution({ + target: address(basicERC1155), + value: 0, + callData: abi.encodeWithSelector( + IERC1155.safeBatchTransferFrom.selector, + address(users.alice.deleGator), + address(users.bob.deleGator), + tokenIds_, + transferAmounts_, + "" + ) + }); + + bytes memory inputTerms_ = _encodeBatchTerms(address(basicERC1155), tokenIds_, limits_); + + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = Caveat({ args: hex"", enforcer: address(erc1155TransferEnforcer), terms: inputTerms_ }); + + 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_); + + // Execute Bob's UserOp + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + + // Should fail - balances remain unchanged + invokeDelegation_UserOp(users.bob, delegations_, execution_); + + // Verify no transfers occurred + assertEq(basicERC1155.balanceOf(address(users.alice.deleGator), TOKEN_ID_1), 1000); + assertEq(basicERC1155.balanceOf(address(users.alice.deleGator), TOKEN_ID_2), 1000); + assertEq(basicERC1155.balanceOf(address(users.bob.deleGator), TOKEN_ID_1), 0); + assertEq(basicERC1155.balanceOf(address(users.bob.deleGator), TOKEN_ID_2), 0); + } + + ////////////////////// Helper functions ////////////////////// + + function createPermissionContexts(Delegation memory del) internal pure returns (bytes[] memory) { + Delegation[] memory delegations = new Delegation[](1); + delegations[0] = del; + bytes[] memory permissionContexts = new bytes[](1); + permissionContexts[0] = abi.encode(delegations); + return permissionContexts; + } + + function createExecutionCallDatas(Execution memory execution) internal pure returns (bytes[] memory) { + bytes[] memory executionCallDatas = new bytes[](1); + executionCallDatas[0] = ExecutionLib.encodeSingle(execution.target, execution.value, execution.callData); + return executionCallDatas; + } + + function createModes(ModeCode _mode) internal pure returns (ModeCode[] memory) { + ModeCode[] memory modes = new ModeCode[](1); + modes[0] = _mode; + return modes; + } + + // Override helper from BaseTest. + function _getEnforcer() internal view override returns (ICaveatEnforcer) { + return ICaveatEnforcer(address(erc1155TransferEnforcer)); + } +} From e0b1da2de06f39d57a014b04ceeb892a6fb00f48 Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Tue, 17 Jun 2025 13:21:42 -0600 Subject: [PATCH 5/5] docs: improved natspec --- src/enforcers/ERC1155TransferEnforcer.sol | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/enforcers/ERC1155TransferEnforcer.sol b/src/enforcers/ERC1155TransferEnforcer.sol index 06b0b086..3ff69cb1 100644 --- a/src/enforcers/ERC1155TransferEnforcer.sol +++ b/src/enforcers/ERC1155TransferEnforcer.sol @@ -8,10 +8,19 @@ import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; /** * @title ERC1155TransferEnforcer - * @notice This enforcer restricts the execution to the transfer of specific ERC1155 tokens. - * @dev This enforcer operates only in single execution call type and with default execution mode. - * Supports both single and batch transfers. The terms include a boolean flag indicating the transfer type. - * @dev The enforcer tracks spent amounts per token ID to enforce transfer limits. + * @notice Enforces transfer restrictions for ERC1155 tokens within delegation contexts + * @dev This enforcer: + * - Operates exclusively in single execution call type with default execution mode + * - Supports both single and batch transfer operations (safeTransferFrom and safeBatchTransferFrom) + * - Automatically selects transfer function based on terms length + * - Maintains per-token ID transfer limits through spent amount tracking + * - Implements cumulative spending limits per delegation hash + * - Validates that only permitted contracts and token IDs can be transferred + * + * Terms Encoding Format: + * - Single transfer: abi.encode(address contract, uint256 tokenId, uint256 maxAmount) [96 bytes] + * - Batch transfer: abi.encode(address contract, uint256[] tokenIds, uint256[] maxAmounts) [≥224 bytes] + * @notice For nonfungible ERC1155 tokens, the transfer amount must be 1. */ contract ERC1155TransferEnforcer is CaveatEnforcer { ////////////////////////////// State //////////////////////////////