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 1ee7974d..296389ee 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" @@ -68,6 +69,7 @@ ADDRESSES=( "0x8aFdf96eDBbe7e1eD3f5Cd89C7E084841e12A09e" "0x3790e6B7233f779b09DA74C72b6e94813925b9aF" "0x63c322732695cAFbbD488Fc6937A0A7B66fC001A" + "0x0000000000000000000000000000000000000000" "0x982FD5C86BBF425d7d1451f974192d4525113DfD" "0x99F2e9bF15ce5eC84685604836F71aB835DBBdED" "0x1e141e455d08721Dd5BCDA1BaA6Ea5633Afd5017" diff --git a/src/enforcers/ERC1155TransferEnforcer.sol b/src/enforcers/ERC1155TransferEnforcer.sol new file mode 100644 index 00000000..3ff69cb1 --- /dev/null +++ b/src/enforcers/ERC1155TransferEnforcer.sol @@ -0,0 +1,212 @@ +// 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 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 ////////////////////////////// + + /// @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 + * @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 _delegationHash, + address, + address + ) + public + virtual + override + onlySingleCallTypeMode(_mode) + onlyDefaultExecutionMode(_mode) + { + _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 _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 _permittedIds, uint256[] memory _permittedAmounts) + { + 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 { + revert("ERC1155TransferEnforcer:invalid-terms-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, 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 != 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_ != IERC1155.safeBatchTransferFrom.selector) { + revert("ERC1155TransferEnforcer:unauthorized-selector-batch"); + } + if (!isBatch_ && selector_ != IERC1155.safeTransferFrom.selector) { + revert("ERC1155TransferEnforcer:unauthorized-selector-single"); + } + + if (isBatch_) { + _validateBatchTransfer(_delegationHash, callData_, permittedTokenIds_, permittedAmounts_); + } else { + _validateSingleTransfer(_delegationHash, callData_, permittedTokenIds_[0], permittedAmounts_[0]); + } + } + + /** + * @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)); + + if (from_ == address(0) || to_ == address(0)) { + revert("ERC1155TransferEnforcer:invalid-address"); + } + if (_permittedTokenId != id_) { + revert("ERC1155TransferEnforcer:unauthorized-token-id"); + } + _increaseSpentMap(_delegationHash, id_, amount_, _permittedAmount); + } + + /** + * @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 _permittedAmounts Array of permitted amounts for each token ID + */ + function _validateBatchTransfer( + bytes32 _delegationHash, + bytes calldata _callData, + uint256[] memory _permittedTokenIds, + uint256[] memory _permittedAmounts + ) + internal + { + (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 (ids_[i] == _permittedTokenIds[j]) { + _increaseSpentMap(_delegationHash, ids_[i], amounts_[i], _permittedAmounts[j]); + isPermitted_ = true; + break; + } + } + if (!isPermitted_) { + revert("ERC1155TransferEnforcer:unauthorized-token-id"); + } + } + } + + /** + * @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 new file mode 100644 index 00000000..52369ada --- /dev/null +++ b/test/enforcers/ERC1155TransferEnforcer.t.sol @@ -0,0 +1,984 @@ +// 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)); + } +}