From 3f3a34e62bfc079bf0547c6d88111d59f8c9bdfc Mon Sep 17 00:00:00 2001 From: mgretzke Date: Wed, 19 Nov 2025 13:15:01 +0100 Subject: [PATCH 1/9] tests --- test/ERC7683Allocator.t.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index 8416f1d..b28491b 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -75,6 +75,7 @@ contract ERC7683Allocator_open is MockAllocator { vm.expectRevert(); erc7683Allocator.open(onChainCrossChainOrder_); } + function test_revert_InvalidOrderDataType() public { // Order data type is invalid bytes32 falseOrderDataType = keccak256('false'); @@ -232,11 +233,10 @@ contract ERC7683Allocator_openFor is MockAllocator { IOriginSettler.GaslessCrossChainOrder memory gasless = _getGaslessCrossChainOrder(); gasless.originChainId = wrongChainId; vm.prank(user); - vm.expectRevert( - abi.encodeWithSelector(ERC7683AL.InvalidOriginChainId.selector, wrongChainId, block.chainid) - ); + vm.expectRevert(abi.encodeWithSelector(ERC7683AL.InvalidOriginChainId.selector, wrongChainId, block.chainid)); erc7683Allocator.openFor(gasless, '', ''); } + function test_revert_InvalidOrderDataType() public { // Order data type is invalid bytes32 falseOrderDataType = keccak256('false'); From 137254f82c6e93c6c82ed917e44ad52077bfef77 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Mon, 1 Dec 2025 12:17:16 +0100 Subject: [PATCH 2/9] Allocation router contract --- snapshots/AllocationRouterTest.json | 6 + src/allocators/lib/AllocatorLib.sol | 64 +++ src/interfaces/IAllocationRouter.sol | 51 +++ src/routers/AllocationRouter.sol | 150 +++++++ test/AllocationRouter.t.sol | 613 +++++++++++++++++++++++++++ 5 files changed, 884 insertions(+) create mode 100644 snapshots/AllocationRouterTest.json create mode 100644 src/interfaces/IAllocationRouter.sol create mode 100644 src/routers/AllocationRouter.sol create mode 100644 test/AllocationRouter.t.sol diff --git a/snapshots/AllocationRouterTest.json b/snapshots/AllocationRouterTest.json new file mode 100644 index 0000000..b75fea9 --- /dev/null +++ b/snapshots/AllocationRouterTest.json @@ -0,0 +1,6 @@ +{ + "depositRegisterAndAllocate_explicit_singleERC20": "224087", + "depositRegisterAndAllocate_simple_multipleERC20": "293495", + "depositRegisterAndAllocate_simple_nativeToken": "192416", + "depositRegisterAndAllocate_simple_singleERC20": "224644" +} \ No newline at end of file diff --git a/src/allocators/lib/AllocatorLib.sol b/src/allocators/lib/AllocatorLib.sol index aade9ea..34b5762 100644 --- a/src/allocators/lib/AllocatorLib.sol +++ b/src/allocators/lib/AllocatorLib.sol @@ -21,14 +21,22 @@ library AllocatorLib { /// @dev bytes4(keccak256('exttload(bytes32)')) uint256 private constant EXTTLOAD_SELECTOR = 0xf135baaa; + /// @notice Function selector for the extsload function that gets a value in transient storage + /// @dev bytes4(keccak256('extsload(bytes32)')) + uint256 private constant EXTSLOAD_SELECTOR = 0x1e2eaeaf; + /// @notice Transient storage slot for the reentrancy guard within the compact uint256 private constant REENTRANCY_GUARD_SLOT = 0x929eee149b4bd21268; + /// @notice Storage slot seed on the compact for mapping allocator IDs to allocator addresses. + uint256 private constant ALLOCATOR_BY_ALLOCATOR_ID_SLOT_SEED = 0x000044036fc77deaed2300000000000000000000000; + error InvalidBalanceChange(uint256 newBalance, uint256 oldBalance); error InvalidPreparation(); error InvalidAllocatorId(uint96 providedId, uint96 allocatorId); error InvalidRegistration(address recipient, bytes32 claimHash, bytes32 typehash); error CompactReentrancyGuardActive(); + error InvalidAllocator(); function prepareAllocation( uint256 nonce, @@ -236,6 +244,29 @@ library AllocatorLib { } } + function getRegisteredAllocator(uint96 allocatorId) internal view returns (address allocator) { + assembly ("memory-safe") { + mstore(0x00, EXTSLOAD_SELECTOR) + mstore(0x20, or(ALLOCATOR_BY_ALLOCATOR_ID_SLOT_SEED, allocatorId)) + + if iszero( + mul( + mload(0x20), + and( + gt(returndatasize(), 0x1f), // At least 32 bytes returned. + staticcall(gas(), THE_COMPACT, 0x1c, 0x24, 0x20, 0x20) + ) + ) + ) { + // revert InvalidAllocator() + mstore(0x00, 0x59dad761) + revert(0x1c, 0x04) + } + + allocator := mload(0x20) + } + } + function getCommitmentsHash(Lock[] calldata commitments, bytes32 typehash) internal pure @@ -352,6 +383,39 @@ library AllocatorLib { return Lock({lockTag: bytes12(bytes32(id)), token: splitToken(id), amount: amount}); } + /// @dev copied from the-compact/src/lib/IdLib.sol + function toAllocatorId(address allocator) internal pure returns (uint96 allocatorId) { + uint8 compactFlag; + assembly ("memory-safe") { + // Extract the uppermost 72 bits of the address. + let x := shr(184, shl(96, allocator)) + + // Propagate the highest set bit. + x := or(x, shr(1, x)) + x := or(x, shr(2, x)) + x := or(x, shr(4, x)) + x := or(x, shr(8, x)) + x := or(x, shr(16, x)) + x := or(x, shr(32, x)) + x := or(x, shr(64, x)) + + // Count set bits to derive most significant bit in the last byte. + let y := sub(x, and(shr(1, x), 0x5555555555555555)) + y := add(and(y, 0x3333333333333333), and(shr(2, y), 0x3333333333333333)) + y := and(add(y, shr(4, y)), 0x0f0f0f0f0f0f0f0f) + y := add(y, shr(8, y)) + y := add(y, shr(16, y)) + y := add(y, shr(32, y)) + + // Look up final value in the sequence. + compactFlag := and(shr(and(sub(72, and(y, 127)), not(3)), 0xfedcba9876543210000), 15) + } + + assembly ("memory-safe") { + allocatorId := or(shl(88, compactFlag), shr(168, shl(168, allocator))) + } + } + function toSeconds(bytes12 lockTag) internal pure returns (uint256 duration) { assembly ("memory-safe") { let resetPeriod := shr(253, shl(1, lockTag)) diff --git a/src/interfaces/IAllocationRouter.sol b/src/interfaces/IAllocationRouter.sol new file mode 100644 index 0000000..92a6684 --- /dev/null +++ b/src/interfaces/IAllocationRouter.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {ISignatureTransfer} from 'permit2/src/interfaces/ISignatureTransfer.sol'; +import {DepositDetails} from 'the-compact/src/types/DepositDetails.sol'; + +interface IAllocationRouter { + /// @notice Deposits, registers and allocates tokens to a given address in a single transaction. + /// @dev Calculates claim hash, type hash and allocator address internally. + /// @param sponsor The address of the sponsor. + /// @param arbiter The address of the arbiter. + /// @param permitted The array of token permissions. + /// @param details Includes the nonce, deadline and lock tag. + /// @param witnessHash The hash of the witness. + /// @param witness The witness string. + /// @param signature The signature of the permit2 transfer. + function depositRegisterAndAllocate( + address sponsor, + address arbiter, + ISignatureTransfer.TokenPermissions[] calldata permitted, + DepositDetails calldata details, + bytes32 witnessHash, + string calldata witness, + bytes calldata signature + ) external payable; + + /// @notice Deposits, registers and allocates tokens to a given address in a single transaction. + /// @dev Uses provided claim hash, type hash and allocator address. + /// @param sponsor The address of the sponsor. + /// @param arbiter The address of the arbiter. + /// @param permitted The array of token permissions. + /// @param details Includes the nonce, deadline and lock tag. + /// @param witnessHash The hash of the witness. + /// @param witness The witness string. + /// @param signature The signature of the permit2 transfer. + /// @param claimHash The hash of the claim registered in the compact. Claim nonce must match the expected allocator nonce. + /// @param allocator The address of the allocator contract. + /// @param typeHash The type hash of the batch compact. + function depositRegisterAndAllocate( + address sponsor, + address arbiter, + ISignatureTransfer.TokenPermissions[] calldata permitted, + DepositDetails calldata details, + bytes32 witnessHash, + string calldata witness, + bytes calldata signature, + bytes32 claimHash, + address allocator, + bytes32 typeHash + ) external payable; +} diff --git a/src/routers/AllocationRouter.sol b/src/routers/AllocationRouter.sol new file mode 100644 index 0000000..e6e653f --- /dev/null +++ b/src/routers/AllocationRouter.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import {AllocatorLib as AL} from '../allocators/lib/AllocatorLib.sol'; +import {ISignatureTransfer} from 'permit2/src/interfaces/ISignatureTransfer.sol'; +import {IOnChainAllocation} from 'the-compact/src/interfaces/IOnChainAllocation.sol'; +import {ITheCompact} from 'the-compact/src/interfaces/ITheCompact.sol'; +import {CompactCategory} from 'the-compact/src/types/CompactCategory.sol'; +import {DepositDetails} from 'the-compact/src/types/DepositDetails.sol'; +import { + BATCH_COMPACT_TYPEHASH, + BATCH_COMPACT_TYPESTRING_FRAGMENT_FIVE, + BATCH_COMPACT_TYPESTRING_FRAGMENT_FOUR, + BATCH_COMPACT_TYPESTRING_FRAGMENT_ONE, + BATCH_COMPACT_TYPESTRING_FRAGMENT_SIX, + BATCH_COMPACT_TYPESTRING_FRAGMENT_THREE, + BATCH_COMPACT_TYPESTRING_FRAGMENT_TWO, + LOCK_TYPEHASH +} from 'the-compact/src/types/EIP712Types.sol'; + +/// @title AllocationRouter +/// @notice Router for depositing, registering and allocating tokens to a given address in a single transaction. +contract AllocationRouter { + // Storage slot seed for mapping allocator IDs to allocator addresses + uint256 private constant _ALLOCATOR_BY_ALLOCATOR_ID_SLOT_SEED = 0x000044036fc77deaed2300000000000000000000000; + + function depositRegisterAndAllocate( + address sponsor, + address arbiter, + ISignatureTransfer.TokenPermissions[] calldata permitted, + DepositDetails calldata details, + bytes32 witnessHash, + string calldata witness, + bytes calldata signature + ) external payable { + // Get the allocator address directly from the compact and revert if allocator is address(0) + address allocator = AL.getRegisteredAllocator(AL.splitAllocatorId(details.lockTag)); + + // Prepare the ids and amounts + uint256[2][] memory idsAndAmounts = new uint256[2][](permitted.length); + bytes32[] memory commitmentHashes = new bytes32[](permitted.length); + for (uint256 i = 0; i < permitted.length; i++) { + uint256 id = AL.toId(details.lockTag, permitted[i].token); + idsAndAmounts[i][0] = id; + idsAndAmounts[i][1] = permitted[i].amount; + + // Lock struct encoding: (bytes12 lockTag, address token, uint256 amount) + commitmentHashes[i] = + keccak256(abi.encode(LOCK_TYPEHASH, details.lockTag, permitted[i].token, permitted[i].amount)); + } + + bytes32 typeHash = _computeTypehash(witness); + + // Prepare allocation + uint256 nonce = IOnChainAllocation(allocator).prepareAllocation( + sponsor, idsAndAmounts, arbiter, details.deadline, typeHash, witnessHash, '' + ); + // The signature MUST validate a claim hash with the allocators nonce. The nonce can be different from details.nonce + + bytes32 claimHash = _computeClaimHash(typeHash, arbiter, sponsor, nonce, commitmentHashes, witnessHash, witness); + + // Deposit and register the tokens using permit2 + ITheCompact(AL.THE_COMPACT).batchDepositAndRegisterViaPermit2{value: msg.value}( + sponsor, permitted, details, claimHash, CompactCategory.BatchCompact, witness, signature + ); + + // Execute allocation + IOnChainAllocation(allocator).executeAllocation( + sponsor, idsAndAmounts, arbiter, details.deadline, typeHash, witnessHash, '' + ); + } + + function depositRegisterAndAllocate( + address sponsor, + address arbiter, + ISignatureTransfer.TokenPermissions[] calldata permitted, + DepositDetails calldata details, + bytes32 witnessHash, + string calldata witness, + bytes calldata signature, + bytes32 claimHash, + address allocator, + bytes32 typeHash + ) external payable { + // Prepare the ids and amounts + uint256[2][] memory idsAndAmounts = new uint256[2][](permitted.length); + for (uint256 i = 0; i < permitted.length; i++) { + idsAndAmounts[i][0] = AL.toId(details.lockTag, permitted[i].token); + idsAndAmounts[i][1] = permitted[i].amount; + } + + // Prepare allocation + IOnChainAllocation(allocator).prepareAllocation( + sponsor, idsAndAmounts, arbiter, details.deadline, typeHash, witnessHash, '' + ); + // The signature MUST validate a claim hash with the allocators nonce. The nonce can be different from details.nonce + + // Deposit and register the tokens using permit2 + ITheCompact(AL.THE_COMPACT).batchDepositAndRegisterViaPermit2{value: msg.value}( + sponsor, permitted, details, claimHash, CompactCategory.BatchCompact, witness, signature + ); + + // Execute allocation + IOnChainAllocation(allocator).executeAllocation( + sponsor, idsAndAmounts, arbiter, details.deadline, typeHash, witnessHash, '' + ); + } + + function _computeTypehash(string calldata witness) internal pure returns (bytes32 typeHash) { + assembly ("memory-safe") { + typeHash := BATCH_COMPACT_TYPEHASH + if witness.length { + let m := mload(0x40) + mstore(m, BATCH_COMPACT_TYPESTRING_FRAGMENT_ONE) + mstore(add(m, 0x20), BATCH_COMPACT_TYPESTRING_FRAGMENT_TWO) + mstore(add(m, 0x40), BATCH_COMPACT_TYPESTRING_FRAGMENT_THREE) + mstore(add(m, 0x60), BATCH_COMPACT_TYPESTRING_FRAGMENT_FOUR) + mstore(add(m, 0x88), BATCH_COMPACT_TYPESTRING_FRAGMENT_SIX) + mstore(add(m, 0x80), BATCH_COMPACT_TYPESTRING_FRAGMENT_FIVE) + let witnessStart := add(m, 0xa8) + calldatacopy(witnessStart, witness.offset, witness.length) + mstore8(add(witnessStart, witness.length), 0x29) // Closing parenthesis + typeHash := keccak256(m, add(0xa9, witness.length)) + } + } + } + + function _computeClaimHash( + bytes32 typeHash, + address arbiter, + address sponsor, + uint256 nonce, + bytes32[] memory commitmentHashes, + bytes32 witnessHash, + string calldata witness + ) internal pure returns (bytes32 claimHash) { + assembly ("memory-safe") { + let m := mload(0x40) + mstore(m, typeHash) + mstore(add(m, 0x20), arbiter) + mstore(add(m, 0x40), sponsor) + mstore(add(m, 0x60), nonce) + calldatacopy(add(m, 0x80), 0x84, 0x20) // details.deadline + mstore(add(m, 0xa0), keccak256(add(commitmentHashes, 0x20), mul(mload(commitmentHashes), 0x20))) // abi.encodePacked(commitmentHashes) + mstore(add(m, 0xc0), witnessHash) + claimHash := keccak256(m, sub(0xe0, mul(iszero(witness.length), 0x20))) // Exclude witnessHash for no-witness cases + } + } +} diff --git a/test/AllocationRouter.t.sol b/test/AllocationRouter.t.sol new file mode 100644 index 0000000..906cdfa --- /dev/null +++ b/test/AllocationRouter.t.sol @@ -0,0 +1,613 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {Test} from 'forge-std/Test.sol'; + +import {TheCompact} from '@uniswap/the-compact/TheCompact.sol'; +import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; +import {ISignatureTransfer} from 'permit2/src/interfaces/ISignatureTransfer.sol'; + +import {BatchClaim} from '@uniswap/the-compact/types/BatchClaims.sol'; +import {BatchClaimComponent, Component} from '@uniswap/the-compact/types/Components.sol'; +import {DepositDetails} from '@uniswap/the-compact/types/DepositDetails.sol'; +import { + BATCH_COMPACT_TYPEHASH, + BATCH_COMPACT_TYPESTRING_FRAGMENT_FIVE, + BATCH_COMPACT_TYPESTRING_FRAGMENT_FOUR, + BATCH_COMPACT_TYPESTRING_FRAGMENT_ONE, + BATCH_COMPACT_TYPESTRING_FRAGMENT_SIX, + BATCH_COMPACT_TYPESTRING_FRAGMENT_THREE, + BATCH_COMPACT_TYPESTRING_FRAGMENT_TWO, + Lock +} from '@uniswap/the-compact/types/EIP712Types.sol'; +import {ResetPeriod} from '@uniswap/the-compact/types/ResetPeriod.sol'; +import {Scope} from '@uniswap/the-compact/types/Scope.sol'; + +import {HybridAllocator} from 'src/allocators/HybridAllocator.sol'; + +import {AllocatorLib} from 'src/allocators/lib/AllocatorLib.sol'; +import {IHybridAllocator} from 'src/interfaces/IHybridAllocator.sol'; +import {AllocationRouter} from 'src/routers/AllocationRouter.sol'; +import {ERC20Mock} from 'src/test/ERC20Mock.sol'; + +import {DeployTheCompact} from 'test/util/DeployTheCompact.sol'; +import {TestHelper} from 'test/util/TestHelper.sol'; + +interface EIP712 { + function DOMAIN_SEPARATOR() external view returns (bytes32); +} + +contract AllocationRouterTest is Test, TestHelper { + TheCompact compact; + HybridAllocator allocator; + AllocationRouter router; + ERC20Mock usdc; + ERC20Mock dai; + + address arbiter; + address signer; + uint256 signerPrivateKey; + address sponsor; + uint256 sponsorPrivateKey; + + uint256 defaultAmount; + uint256 defaultExpiration; + + // Permit2 constants + address constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3; + bytes32 constant PERMIT2_DOMAIN_SEPARATOR_TYPEHASH = + keccak256('EIP712Domain(string name,uint256 chainId,address verifyingContract)'); + bytes32 constant TOKEN_PERMISSIONS_TYPEHASH = keccak256('TokenPermissions(address token,uint256 amount)'); + + // BatchActivation typehash without witness (from the-compact/src/types/EIP712Types.sol) + // keccak256(bytes("BatchActivation(address activator,uint256[] ids,BatchCompact compact)BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments)Lock(bytes12 lockTag,address token,uint256 amount)")) + bytes32 constant BATCH_COMPACT_BATCH_ACTIVATION_TYPEHASH = + 0xa794ed1a28cdf297ac45a3eee4643e35d29b295a389368da5f6baa420872c9b7; + + // PermitBatchWitnessTransferFrom typehash (no witness) + string constant PERMIT_BATCH_WITNESS_TYPESTRING = + 'PermitBatchWitnessTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline,BatchActivation witness)BatchActivation(address activator,uint256[] ids,BatchCompact compact)BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments)Lock(bytes12 lockTag,address token,uint256 amount)TokenPermissions(address token,uint256 amount)'; + bytes32 constant PERMIT_BATCH_WITNESS_TYPEHASH = keccak256(bytes(PERMIT_BATCH_WITNESS_TYPESTRING)); + + function setUp() public { + // Deploy TheCompact at hardcoded address + compact = DeployTheCompact(new DeployTheCompact()).deployTheCompact(); + assertEq(address(compact), AllocatorLib.THE_COMPACT); + + // Deploy Permit2 at the expected address + _deployPermit2(); + + // Setup accounts + arbiter = makeAddr('arbiter'); + (signer, signerPrivateKey) = makeAddrAndKey('signer'); + (sponsor, sponsorPrivateKey) = makeAddrAndKey('sponsor'); + + // Deploy allocator with signer + allocator = new HybridAllocator(signer); + + // Deploy router + router = new AllocationRouter(); + + // Deploy mock tokens + usdc = new ERC20Mock('USDC', 'USDC'); + dai = new ERC20Mock('DAI', 'DAI'); + + // Setup default values + defaultAmount = 1 ether; + defaultExpiration = block.timestamp + 1 days; + + // Fund sponsor + deal(sponsor, 10 ether); + usdc.mint(sponsor, 10 ether); + dai.mint(sponsor, 10 ether); + + // Approve Permit2 for tokens + vm.startPrank(sponsor); + usdc.approve(PERMIT2, type(uint256).max); + dai.approve(PERMIT2, type(uint256).max); + vm.stopPrank(); + } + + function _deployPermit2() internal { + // Deploy Permit2 using the same pattern as the-compact tests + address permit2Deployer = address(0x4e59b44847b379578588920cA78FbF26c0B4956C); + address permit2DeployerDeployer = address(0x3fAB184622Dc19b6109349B94811493BF2a45362); + bytes memory permit2DeployerCreationCode = + hex'604580600e600039806000f350fe7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3'; + + vm.deal(permit2DeployerDeployer, 1e18); + vm.prank(permit2DeployerDeployer); + address deployedPermit2Deployer; + assembly ("memory-safe") { + deployedPermit2Deployer := + create(0, add(permit2DeployerCreationCode, 0x20), mload(permit2DeployerCreationCode)) + } + + bytes memory permit2CreationCalldata = + hex'0000000000000000000000000000000000000000d3af2663da51c1021500000060c0346100bb574660a052602081017f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a86681527f9ac997416e8ff9d2ff6bebeb7149f65cdae5e32e2b90440b566bb3044041d36a60408301524660608301523060808301526080825260a082019180831060018060401b038411176100a557826040525190206080526123c090816100c1823960805181611b47015260a05181611b210152f35b634e487b7160e01b600052604160045260246000fd5b600080fdfe6040608081526004908136101561001557600080fd5b600090813560e01c80630d58b1db1461126c578063137c29fe146110755780632a2d80d114610db75780632b67b57014610bde57806330f28b7a14610ade5780633644e51514610a9d57806336c7851614610a285780633ff9dcb1146109a85780634fe02b441461093f57806365d9723c146107ac57806387517c451461067a578063927da105146105c3578063cc53287f146104a3578063edd9444b1461033a5763fe8ec1a7146100c657600080fd5b346103365760c07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff833581811161033257610114903690860161164b565b60243582811161032e5761012b903690870161161a565b6101336114e6565b9160843585811161032a5761014b9036908a016115c1565b98909560a43590811161032657610164913691016115c1565b969095815190610173826113ff565b606b82527f5065726d697442617463685769746e6573735472616e7366657246726f6d285460208301527f6f6b656e5065726d697373696f6e735b5d207065726d69747465642c61646472838301527f657373207370656e6465722c75696e74323536206e6f6e63652c75696e74323560608301527f3620646561646c696e652c000000000000000000000000000000000000000000608083015282519a8b9181610222602085018096611f93565b918237018a8152039961025b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09b8c8101835282611437565b5190209085515161026b81611ebb565b908a5b8181106102f95750506102f6999a6102ed9183516102a081610294602082018095611f66565b03848101835282611437565b519020602089810151858b015195519182019687526040820192909252336060820152608081019190915260a081019390935260643560c08401528260e081015b03908101835282611437565b51902093611cf7565b80f35b8061031161030b610321938c5161175e565b51612054565b61031b828661175e565b52611f0a565b61026e565b8880fd5b8780fd5b8480fd5b8380fd5b5080fd5b5091346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff9080358281116103325761038b903690830161164b565b60243583811161032e576103a2903690840161161a565b9390926103ad6114e6565b9160643590811161049f576103c4913691016115c1565b949093835151976103d489611ebb565b98885b81811061047d5750506102f697988151610425816103f9602082018095611f66565b037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101835282611437565b5190206020860151828701519083519260208401947ffcf35f5ac6a2c28868dc44c302166470266239195f02b0ee408334829333b7668652840152336060840152608083015260a082015260a081526102ed8161141b565b808b61031b8261049461030b61049a968d5161175e565b9261175e565b6103d7565b8680fd5b5082346105bf57602090817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103325780359067ffffffffffffffff821161032e576104f49136910161161a565b929091845b848110610504578580f35b8061051a610515600193888861196c565b61197c565b61052f84610529848a8a61196c565b0161197c565b3389528385528589209173ffffffffffffffffffffffffffffffffffffffff80911692838b528652868a20911690818a5285528589207fffffffffffffffffffffffff000000000000000000000000000000000000000081541690558551918252848201527f89b1add15eff56b3dfe299ad94e01f2b52fbcb80ae1a3baea6ae8c04cb2b98a4853392a2016104f9565b8280fd5b50346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610676816105ff6114a0565b936106086114c3565b6106106114e6565b73ffffffffffffffffffffffffffffffffffffffff968716835260016020908152848420928816845291825283832090871683528152919020549251938316845260a083901c65ffffffffffff169084015260d09190911c604083015281906060820190565b0390f35b50346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336576106b26114a0565b906106bb6114c3565b916106c46114e6565b65ffffffffffff926064358481169081810361032a5779ffffffffffff0000000000000000000000000000000000000000947fda9fa7c1b00402c17d0161b249b1ab8bbec047c5a52207b9c112deffd817036b94338a5260016020527fffffffffffff0000000000000000000000000000000000000000000000000000858b209873ffffffffffffffffffffffffffffffffffffffff809416998a8d5260205283878d209b169a8b8d52602052868c209486156000146107a457504216925b8454921697889360a01b16911617179055815193845260208401523392a480f35b905092610783565b5082346105bf5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576107e56114a0565b906107ee6114c3565b9265ffffffffffff604435818116939084810361032a57338852602091600183528489209673ffffffffffffffffffffffffffffffffffffffff80911697888b528452858a20981697888a5283528489205460d01c93848711156109175761ffff9085840316116108f05750907f55eb90d810e1700b35a8e7e25395ff7f2b2259abd7415ca2284dfb1c246418f393929133895260018252838920878a528252838920888a5282528389209079ffffffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffff000000000000000000000000000000000000000000000000000083549260d01b16911617905582519485528401523392a480f35b84517f24d35a26000000000000000000000000000000000000000000000000000000008152fd5b5084517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b503461033657807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336578060209273ffffffffffffffffffffffffffffffffffffffff61098f6114a0565b1681528084528181206024358252845220549051908152f35b5082346105bf57817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf577f3704902f963766a4e561bbaab6e6cdc1b1dd12f6e9e99648da8843b3f46b918d90359160243533855284602052818520848652602052818520818154179055815193845260208401523392a280f35b8234610a9a5760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610a9a57610a606114a0565b610a686114c3565b610a706114e6565b6064359173ffffffffffffffffffffffffffffffffffffffff8316830361032e576102f6936117a1565b80fd5b503461033657817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657602090610ad7611b1e565b9051908152f35b508290346105bf576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf57610b1a3661152a565b90807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c36011261033257610b4c611478565b9160e43567ffffffffffffffff8111610bda576102f694610b6f913691016115c1565b939092610b7c8351612054565b6020840151828501519083519260208401947f939c21a48a8dbe3a9a2404a1d46691e4d39f6583d6ec6b35714604c986d801068652840152336060840152608083015260a082015260a08152610bd18161141b565b51902091611c25565b8580fd5b509134610336576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610c186114a0565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc360160c08112610332576080855191610c51836113e3565b1261033257845190610c6282611398565b73ffffffffffffffffffffffffffffffffffffffff91602435838116810361049f578152604435838116810361049f57602082015265ffffffffffff606435818116810361032a5788830152608435908116810361049f576060820152815260a435938285168503610bda576020820194855260c4359087830182815260e43567ffffffffffffffff811161032657610cfe90369084016115c1565b929093804211610d88575050918591610d786102f6999a610d7e95610d238851611fbe565b90898c511690519083519260208401947ff3841cd1ff0085026a6327b620b67997ce40f282c88a8e905a7a5626e310f3d086528401526060830152608082015260808152610d70816113ff565b519020611bd9565b916120c7565b519251169161199d565b602492508a51917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b5091346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc93818536011261033257610df36114a0565b9260249081359267ffffffffffffffff9788851161032a578590853603011261049f578051978589018981108282111761104a578252848301358181116103265785019036602383011215610326578382013591610e50836115ef565b90610e5d85519283611437565b838252602093878584019160071b83010191368311611046578801905b828210610fe9575050508a526044610e93868801611509565b96838c01978852013594838b0191868352604435908111610fe557610ebb90369087016115c1565b959096804211610fba575050508998995151610ed681611ebb565b908b5b818110610f9757505092889492610d7892610f6497958351610f02816103f98682018095611f66565b5190209073ffffffffffffffffffffffffffffffffffffffff9a8b8b51169151928551948501957faf1b0d30d2cab0380e68f0689007e3254993c596f2fdd0aaa7f4d04f794408638752850152830152608082015260808152610d70816113ff565b51169082515192845b848110610f78578580f35b80610f918585610f8b600195875161175e565b5161199d565b01610f6d565b80610311610fac8e9f9e93610fb2945161175e565b51611fbe565b9b9a9b610ed9565b8551917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b8a80fd5b6080823603126110465785608091885161100281611398565b61100b85611509565b8152611018838601611509565b838201526110278a8601611607565b8a8201528d611037818701611607565b90820152815201910190610e7a565b8c80fd5b84896041867f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b5082346105bf576101407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576110b03661152a565b91807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c360112610332576110e2611478565b67ffffffffffffffff93906101043585811161049f5761110590369086016115c1565b90936101243596871161032a57611125610bd1966102f6983691016115c1565b969095825190611134826113ff565b606482527f5065726d69745769746e6573735472616e7366657246726f6d28546f6b656e5060208301527f65726d697373696f6e73207065726d69747465642c6164647265737320737065848301527f6e6465722c75696e74323536206e6f6e63652c75696e7432353620646561646c60608301527f696e652c0000000000000000000000000000000000000000000000000000000060808301528351948591816111e3602085018096611f93565b918237018b8152039361121c7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe095868101835282611437565b5190209261122a8651612054565b6020878101518589015195519182019687526040820192909252336060820152608081019190915260a081019390935260e43560c08401528260e081016102e1565b5082346105bf576020807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033257813567ffffffffffffffff92838211610bda5736602383011215610bda5781013592831161032e576024906007368386831b8401011161049f57865b8581106112e5578780f35b80821b83019060807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc83360301126103265761139288876001946060835161132c81611398565b611368608461133c8d8601611509565b9485845261134c60448201611509565b809785015261135d60648201611509565b809885015201611509565b918291015273ffffffffffffffffffffffffffffffffffffffff80808093169516931691166117a1565b016112da565b6080810190811067ffffffffffffffff8211176113b457604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6060810190811067ffffffffffffffff8211176113b457604052565b60a0810190811067ffffffffffffffff8211176113b457604052565b60c0810190811067ffffffffffffffff8211176113b457604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff8211176113b457604052565b60c4359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b600080fd5b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6024359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6044359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc01906080821261149b576040805190611563826113e3565b8082941261149b57805181810181811067ffffffffffffffff8211176113b457825260043573ffffffffffffffffffffffffffffffffffffffff8116810361149b578152602435602082015282526044356020830152606435910152565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020838186019501011161149b57565b67ffffffffffffffff81116113b45760051b60200190565b359065ffffffffffff8216820361149b57565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020808501948460061b01011161149b57565b91909160608184031261149b576040805191611666836113e3565b8294813567ffffffffffffffff9081811161149b57830182601f8201121561149b578035611693816115ef565b926116a087519485611437565b818452602094858086019360061b8501019381851161149b579086899897969594939201925b8484106116e3575050505050855280820135908501520135910152565b90919293949596978483031261149b578851908982019082821085831117611730578a928992845261171487611509565b81528287013583820152815201930191908897969594936116c6565b602460007f4e487b710000000000000000000000000000000000000000000000000000000081526041600452fd5b80518210156117725760209160051b010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b92919273ffffffffffffffffffffffffffffffffffffffff604060008284168152600160205282828220961695868252602052818120338252602052209485549565ffffffffffff8760a01c16804211611884575082871696838803611812575b5050611810955016926118b5565b565b878484161160001461184f57602488604051907ff96fb0710000000000000000000000000000000000000000000000000000000082526004820152fd5b7fffffffffffffffffffffffff000000000000000000000000000000000000000084846118109a031691161790553880611802565b602490604051907fd81b2f2e0000000000000000000000000000000000000000000000000000000082526004820152fd5b9060006064926020958295604051947f23b872dd0000000000000000000000000000000000000000000000000000000086526004860152602485015260448401525af13d15601f3d116001600051141617161561190e57565b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601460248201527f5452414e534645525f46524f4d5f4641494c45440000000000000000000000006044820152fd5b91908110156117725760061b0190565b3573ffffffffffffffffffffffffffffffffffffffff8116810361149b5790565b9065ffffffffffff908160608401511673ffffffffffffffffffffffffffffffffffffffff908185511694826020820151169280866040809401511695169560009187835260016020528383208984526020528383209916988983526020528282209184835460d01c03611af5579185611ace94927fc6a377bfc4eb120024a8ac08eef205be16b817020812c73223e81d1bdb9708ec98979694508715600014611ad35779ffffffffffff00000000000000000000000000000000000000009042165b60a01b167fffffffffffff00000000000000000000000000000000000000000000000000006001860160d01b1617179055519384938491604091949373ffffffffffffffffffffffffffffffffffffffff606085019616845265ffffffffffff809216602085015216910152565b0390a4565b5079ffffffffffff000000000000000000000000000000000000000087611a60565b600484517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b467f000000000000000000000000000000000000000000000000000000000000000003611b69577f000000000000000000000000000000000000000000000000000000000000000090565b60405160208101907f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a86682527f9ac997416e8ff9d2ff6bebeb7149f65cdae5e32e2b90440b566bb3044041d36a604082015246606082015230608082015260808152611bd3816113ff565b51902090565b611be1611b1e565b906040519060208201927f190100000000000000000000000000000000000000000000000000000000000084526022830152604282015260428152611bd381611398565b9192909360a435936040840151804211611cc65750602084510151808611611c955750918591610d78611c6594611c60602088015186611e47565b611bd9565b73ffffffffffffffffffffffffffffffffffffffff809151511692608435918216820361149b57611810936118b5565b602490604051907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b602490604051907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b959093958051519560409283830151804211611e175750848803611dee57611d2e918691610d7860209b611c608d88015186611e47565b60005b868110611d42575050505050505050565b611d4d81835161175e565b5188611d5a83878a61196c565b01359089810151808311611dbe575091818888886001968596611d84575b50505050505001611d31565b611db395611dad9273ffffffffffffffffffffffffffffffffffffffff6105159351169561196c565b916118b5565b803888888883611d78565b6024908651907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b600484517fff633a38000000000000000000000000000000000000000000000000000000008152fd5b6024908551907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b9073ffffffffffffffffffffffffffffffffffffffff600160ff83161b9216600052600060205260406000209060081c6000526020526040600020818154188091551615611e9157565b60046040517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b90611ec5826115ef565b611ed26040519182611437565b8281527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0611f0082946115ef565b0190602036910137565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114611f375760010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b805160208092019160005b828110611f7f575050505090565b835185529381019392810192600101611f71565b9081519160005b838110611fab575050016000815290565b8060208092840101518185015201611f9a565b60405160208101917f65626cad6cb96493bf6f5ebea28756c966f023ab9e8a83a7101849d5573b3678835273ffffffffffffffffffffffffffffffffffffffff8082511660408401526020820151166060830152606065ffffffffffff9182604082015116608085015201511660a082015260a0815260c0810181811067ffffffffffffffff8211176113b45760405251902090565b6040516020808201927f618358ac3db8dc274f0cd8829da7e234bd48cd73c4a740aede1adec9846d06a1845273ffffffffffffffffffffffffffffffffffffffff81511660408401520151606082015260608152611bd381611398565b919082604091031261149b576020823592013590565b6000843b61222e5750604182036121ac576120e4828201826120b1565b939092604010156117725760209360009360ff6040608095013560f81c5b60405194855216868401526040830152606082015282805260015afa156121a05773ffffffffffffffffffffffffffffffffffffffff806000511691821561217657160361214c57565b60046040517f815e1d64000000000000000000000000000000000000000000000000000000008152fd5b60046040517f8baa579f000000000000000000000000000000000000000000000000000000008152fd5b6040513d6000823e3d90fd5b60408203612204576121c0918101906120b1565b91601b7f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff84169360ff1c019060ff8211611f375760209360009360ff608094612102565b60046040517f4be6321b000000000000000000000000000000000000000000000000000000008152fd5b929391601f928173ffffffffffffffffffffffffffffffffffffffff60646020957fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0604051988997889687947f1626ba7e000000000000000000000000000000000000000000000000000000009e8f8752600487015260406024870152816044870152868601378b85828601015201168101030192165afa9081156123a857829161232a575b507fffffffff000000000000000000000000000000000000000000000000000000009150160361230057565b60046040517fb0669cbc000000000000000000000000000000000000000000000000000000008152fd5b90506020813d82116123a0575b8161234460209383611437565b810103126103365751907fffffffff0000000000000000000000000000000000000000000000000000000082168203610a9a57507fffffffff0000000000000000000000000000000000000000000000000000000090386122d4565b3d9150612337565b6040513d84823e3d90fdfea164736f6c6343000811000a'; + + (bool ok,) = permit2Deployer.call(permit2CreationCalldata); + require(ok && PERMIT2.code.length != 0, 'permit2 deployment failed'); + } + + /* ====================================================================== */ + /* Helper Functions */ + /* ====================================================================== */ + + function _getLockTag() internal view returns (bytes12) { + return _toLockTag(address(allocator), Scope.Multichain, ResetPeriod.TenMinutes); + } + + /// @dev Computes the typeHash the same way as AllocationRouter._depositRegisterAndAllocate + function _computeRouterTypeHash(string memory witness) internal pure returns (bytes32) { + return keccak256( + abi.encodePacked( + BATCH_COMPACT_TYPESTRING_FRAGMENT_ONE, + BATCH_COMPACT_TYPESTRING_FRAGMENT_TWO, + BATCH_COMPACT_TYPESTRING_FRAGMENT_THREE, + BATCH_COMPACT_TYPESTRING_FRAGMENT_FOUR, + BATCH_COMPACT_TYPESTRING_FRAGMENT_FIVE, + BATCH_COMPACT_TYPESTRING_FRAGMENT_SIX, + witness, + ')' + ) + ); + } + + function _getPermit2DomainSeparator() internal view returns (bytes32) { + return keccak256( + abi.encode(PERMIT2_DOMAIN_SEPARATOR_TYPEHASH, keccak256(bytes('Permit2')), block.chainid, PERMIT2) + ); + } + + function _createTokenPermissions(address token, uint256 amount) + internal + pure + returns (ISignatureTransfer.TokenPermissions[] memory) + { + ISignatureTransfer.TokenPermissions[] memory permitted = new ISignatureTransfer.TokenPermissions[](1); + permitted[0] = ISignatureTransfer.TokenPermissions({token: token, amount: amount}); + return permitted; + } + + function _createTokenPermissions2(address token1, uint256 amount1, address token2, uint256 amount2) + internal + pure + returns (ISignatureTransfer.TokenPermissions[] memory) + { + ISignatureTransfer.TokenPermissions[] memory permitted = new ISignatureTransfer.TokenPermissions[](2); + permitted[0] = ISignatureTransfer.TokenPermissions({token: token1, amount: amount1}); + permitted[1] = ISignatureTransfer.TokenPermissions({token: token2, amount: amount2}); + return permitted; + } + + function _createDepositDetails(uint256 nonce, uint256 deadline, bytes12 lockTag) + internal + pure + returns (DepositDetails memory) + { + return DepositDetails({nonce: nonce, deadline: deadline, lockTag: lockTag}); + } + + function _hashTokenPermissions(ISignatureTransfer.TokenPermissions[] memory permitted) + internal + pure + returns (bytes32) + { + bytes32[] memory hashes = new bytes32[](permitted.length); + for (uint256 i = 0; i < permitted.length; i++) { + hashes[i] = keccak256(abi.encode(TOKEN_PERMISSIONS_TYPEHASH, permitted[i].token, permitted[i].amount)); + } + return keccak256(abi.encodePacked(hashes)); + } + + function _createIds(bytes12 lockTag, address[] memory tokens) internal pure returns (uint256[] memory) { + uint256[] memory ids = new uint256[](tokens.length); + for (uint256 i = 0; i < tokens.length; i++) { + ids[i] = AllocatorLib.toId(lockTag, tokens[i]); + } + return ids; + } + + /// @dev Computes the Lock commitment hash with proper struct encoding + /// Lock struct: (bytes12 lockTag, address token, uint256 amount) + function _computeCommitmentHash(uint256 id, uint256 amount) internal pure returns (bytes32) { + bytes32 lockTypehash = keccak256('Lock(bytes12 lockTag,address token,uint256 amount)'); + bytes12 lockTag = bytes12(bytes32(id)); + address token = address(uint160(id)); + return keccak256(abi.encode(lockTypehash, lockTag, token, amount)); + } + + function _createPermit2Signature( + ISignatureTransfer.TokenPermissions[] memory permitted, + DepositDetails memory details, + bytes32 claimHash, + uint256 signerPk + ) internal view returns (bytes memory) { + bytes12 lockTag = details.lockTag; + + // Check if first token is native + bool hasNative = permitted.length > 0 && permitted[0].token == address(0); + + // Create ids array - includes ALL tokens (including native) + uint256[] memory ids = new uint256[](permitted.length); + for (uint256 i = 0; i < permitted.length; i++) { + ids[i] = AllocatorLib.toId(lockTag, permitted[i].token); + } + bytes32 idsHash = keccak256(abi.encodePacked(ids)); + + // Create activation hash using the exact same typehash TheCompact uses + // The activator is the router (msg.sender to TheCompact) + bytes32 activationHash = + keccak256(abi.encode(BATCH_COMPACT_BATCH_ACTIVATION_TYPEHASH, address(router), idsHash, claimHash)); + + // Create permit batch hash - tokenPermissionsHash only includes ERC20 tokens (not native) + ISignatureTransfer.TokenPermissions[] memory erc20Permitted; + if (hasNative) { + erc20Permitted = new ISignatureTransfer.TokenPermissions[](permitted.length - 1); + for (uint256 i = 0; i < erc20Permitted.length; i++) { + erc20Permitted[i] = permitted[i + 1]; + } + } else { + erc20Permitted = permitted; + } + bytes32 tokenPermissionsHash = _hashTokenPermissions(erc20Permitted); + bytes32 permitBatchHash = keccak256( + abi.encode( + PERMIT_BATCH_WITNESS_TYPEHASH, + tokenPermissionsHash, + address(compact), + details.nonce, + details.deadline, + activationHash + ) + ); + + // Create digest + bytes32 domainSeparator = _getPermit2DomainSeparator(); + bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), domainSeparator, permitBatchHash)); + + // Sign + (bytes32 r, bytes32 vs) = vm.signCompact(signerPk, digest); + return abi.encodePacked(r, vs); + } + + /* ====================================================================== */ + /* Tests for depositRegisterAndAllocate */ + /* ====================================================================== */ + + function test_depositRegisterAndAllocate_simple_singleERC20() public { + bytes12 lockTag = _getLockTag(); + uint256 nonce = 1; + + // Prepare token permissions + ISignatureTransfer.TokenPermissions[] memory permitted = _createTokenPermissions(address(usdc), defaultAmount); + + // Prepare deposit details + DepositDetails memory details = _createDepositDetails(nonce, defaultExpiration, lockTag); + + // Compute ids that the router will compute + uint256[] memory ids = new uint256[](1); + ids[0] = AllocatorLib.toId(lockTag, address(usdc)); + + // The router computes the claimHash internally using the nonce returned from prepareAllocation + // We need to predict what that nonce will be (allocator.nonces() + 1 = 0 + 1 = 1) + uint256 expectedNonce = 1; + + // Compute commitment hashes using proper Lock struct encoding + bytes32[] memory commitmentHashes = new bytes32[](1); + commitmentHashes[0] = _computeCommitmentHash(ids[0], defaultAmount); + + // For empty witness, router uses BATCH_COMPACT_TYPEHASH (no Mandate) + bytes32 typeHash = BATCH_COMPACT_TYPEHASH; + + // Compute claimHash WITHOUT witnessHash (empty witness case) + bytes32 claimHash = keccak256( + abi.encode( + typeHash, + arbiter, + sponsor, + expectedNonce, + details.deadline, + keccak256(abi.encodePacked(commitmentHashes)) + ) + ); + + // Create Permit2 signature + bytes memory signature = _createPermit2Signature(permitted, details, claimHash, sponsorPrivateKey); + + // Execute + router.depositRegisterAndAllocate(sponsor, arbiter, permitted, details, bytes32(0), '', signature); + vm.snapshotGasLastCall('depositRegisterAndAllocate_simple_singleERC20'); + + // Verify allocator nonce incremented + assertEq(allocator.nonces(), 1); + + // Verify claim is authorized + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = ids[0]; + idsAndAmounts[0][1] = defaultAmount; + assertTrue( + allocator.isClaimAuthorized(claimHash, arbiter, sponsor, expectedNonce, details.deadline, idsAndAmounts, '') + ); + + // Verify tokens are in compact + assertEq(usdc.balanceOf(address(compact)), defaultAmount); + assertEq(compact.balanceOf(sponsor, ids[0]), defaultAmount); + } + + function test_depositRegisterAndAllocate_explicit_singleERC20() public { + bytes12 lockTag = _getLockTag(); + uint256 nonce = 1; + + // Prepare token permissions + ISignatureTransfer.TokenPermissions[] memory permitted = _createTokenPermissions(address(usdc), defaultAmount); + + // Prepare deposit details + DepositDetails memory details = _createDepositDetails(nonce, defaultExpiration, lockTag); + + // Compute ids + uint256[] memory ids = new uint256[](1); + ids[0] = AllocatorLib.toId(lockTag, address(usdc)); + + // Compute commitment hashes using proper Lock struct encoding + bytes32[] memory commitmentHashes = new bytes32[](1); + commitmentHashes[0] = _computeCommitmentHash(ids[0], defaultAmount); + + // For empty witness, use BATCH_COMPACT_TYPEHASH (no Mandate) + bytes32 typeHash = BATCH_COMPACT_TYPEHASH; + + // The explicit function uses the allocator's nonce after prepareAllocation increments it + // prepareAllocation returns nonces + 1 = 0 + 1 = 1 + uint256 expectedNonce = 1; + + // Compute claimHash WITHOUT witnessHash (since witness is empty, bytes32(0)) + // AllocatorLib.getClaimHash omits the witness when it's bytes32(0) + bytes32 claimHash = keccak256( + abi.encode( + typeHash, + arbiter, + sponsor, + expectedNonce, + details.deadline, + keccak256(abi.encodePacked(commitmentHashes)) + ) + ); + + // Create Permit2 signature + bytes memory signature = _createPermit2Signature(permitted, details, claimHash, sponsorPrivateKey); + + // Execute + router.depositRegisterAndAllocate( + sponsor, arbiter, permitted, details, bytes32(0), '', signature, claimHash, address(allocator), typeHash + ); + vm.snapshotGasLastCall('depositRegisterAndAllocate_explicit_singleERC20'); + + // Verify allocator nonce incremented + assertEq(allocator.nonces(), 1); + + // Verify claim is authorized + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = ids[0]; + idsAndAmounts[0][1] = defaultAmount; + assertTrue( + allocator.isClaimAuthorized(claimHash, arbiter, sponsor, expectedNonce, details.deadline, idsAndAmounts, '') + ); + + // Verify tokens are in compact + assertEq(usdc.balanceOf(address(compact)), defaultAmount); + assertEq(compact.balanceOf(sponsor, ids[0]), defaultAmount); + } + + function test_depositRegisterAndAllocate_simple_multipleERC20() public { + bytes12 lockTag = _getLockTag(); + uint256 nonce = 1; + uint256 amount1 = defaultAmount; + uint256 amount2 = defaultAmount / 2; + + // Prepare token permissions (sorted by address) + ISignatureTransfer.TokenPermissions[] memory permitted; + uint256[] memory ids = new uint256[](2); + bytes32[] memory commitmentHashes = new bytes32[](2); + + if (uint160(address(usdc)) < uint160(address(dai))) { + permitted = _createTokenPermissions2(address(usdc), amount1, address(dai), amount2); + ids[0] = AllocatorLib.toId(lockTag, address(usdc)); + ids[1] = AllocatorLib.toId(lockTag, address(dai)); + commitmentHashes[0] = _computeCommitmentHash(ids[0], amount1); + commitmentHashes[1] = _computeCommitmentHash(ids[1], amount2); + } else { + permitted = _createTokenPermissions2(address(dai), amount2, address(usdc), amount1); + ids[0] = AllocatorLib.toId(lockTag, address(dai)); + ids[1] = AllocatorLib.toId(lockTag, address(usdc)); + commitmentHashes[0] = _computeCommitmentHash(ids[0], amount2); + commitmentHashes[1] = _computeCommitmentHash(ids[1], amount1); + } + + // Prepare deposit details + DepositDetails memory details = _createDepositDetails(nonce, defaultExpiration, lockTag); + + // Expected nonce from prepareAllocation + uint256 expectedNonce = 1; + + // For empty witness, router uses BATCH_COMPACT_TYPEHASH (no Mandate) + bytes32 typeHash = BATCH_COMPACT_TYPEHASH; + + // Compute claimHash WITHOUT witnessHash (empty witness case) + bytes32 claimHash = keccak256( + abi.encode( + typeHash, + arbiter, + sponsor, + expectedNonce, + details.deadline, + keccak256(abi.encodePacked(commitmentHashes)) + ) + ); + + // Create Permit2 signature + bytes memory signature = _createPermit2Signature(permitted, details, claimHash, sponsorPrivateKey); + + // Execute + router.depositRegisterAndAllocate(sponsor, arbiter, permitted, details, bytes32(0), '', signature); + vm.snapshotGasLastCall('depositRegisterAndAllocate_simple_multipleERC20'); + + // Verify allocator nonce incremented + assertEq(allocator.nonces(), 1); + + // Verify tokens are in compact + assertEq(usdc.balanceOf(address(compact)), amount1); + assertEq(dai.balanceOf(address(compact)), amount2); + } + + function test_depositRegisterAndAllocate_simple_nativeToken() public { + bytes12 lockTag = _getLockTag(); + uint256 nonce = 1; + + // Prepare token permissions with native token (address(0)) + // Native token must be first in the array for batchDepositAndRegisterViaPermit2 + ISignatureTransfer.TokenPermissions[] memory permitted = _createTokenPermissions(address(0), defaultAmount); + + // Prepare deposit details + DepositDetails memory details = _createDepositDetails(nonce, defaultExpiration, lockTag); + + // Compute ids + uint256[] memory ids = new uint256[](1); + ids[0] = AllocatorLib.toId(lockTag, address(0)); + + // Expected nonce from prepareAllocation + uint256 expectedNonce = 1; + + // Compute commitment hashes using proper Lock struct encoding + bytes32[] memory commitmentHashes = new bytes32[](1); + commitmentHashes[0] = _computeCommitmentHash(ids[0], defaultAmount); + + // For empty witness, use BATCH_COMPACT_TYPEHASH (no Mandate) + bytes32 typeHash = BATCH_COMPACT_TYPEHASH; + + // Compute claimHash WITHOUT witnessHash (empty witness case) + bytes32 claimHash = keccak256( + abi.encode( + typeHash, + arbiter, + sponsor, + expectedNonce, + details.deadline, + keccak256(abi.encodePacked(commitmentHashes)) + ) + ); + + // Create Permit2 signature - for native tokens, the idsHash in the activation + // still includes the native token id, so we pass the full permitted array + // (TheCompact strips the native token from the Permit2 call internally) + bytes memory signature = _createPermit2Signature(permitted, details, claimHash, sponsorPrivateKey); + + // Execute with value - need to use the explicit function which is payable + router.depositRegisterAndAllocate{value: defaultAmount}( + sponsor, arbiter, permitted, details, bytes32(0), '', signature, claimHash, address(allocator), typeHash + ); + vm.snapshotGasLastCall('depositRegisterAndAllocate_simple_nativeToken'); + + // Verify allocator nonce incremented + assertEq(allocator.nonces(), 1); + + // Verify native tokens are in compact + assertEq(address(compact).balance, defaultAmount); + assertEq(compact.balanceOf(sponsor, ids[0]), defaultAmount); + } + + /* ====================================================================== */ + /* Revert Tests */ + /* ====================================================================== */ + + function test_depositRegisterAndAllocate_revert_InvalidAllocator() public { + bytes12 invalidLockTag = bytes12(0); // Invalid lockTag with allocatorId = 0 + uint256 nonce = 1; + + // Prepare token permissions + ISignatureTransfer.TokenPermissions[] memory permitted = _createTokenPermissions(address(usdc), defaultAmount); + + // Prepare deposit details with invalid lockTag + DepositDetails memory details = _createDepositDetails(nonce, defaultExpiration, invalidLockTag); + + // Attempt to call should revert with InvalidAllocator + vm.expectRevert(AllocatorLib.InvalidAllocator.selector); + router.depositRegisterAndAllocate(sponsor, arbiter, permitted, details, bytes32(0), '', ''); + } + + /* ====================================================================== */ + /* Claim Flow Tests */ + /* ====================================================================== */ + + function test_depositRegisterAndAllocate_fullClaimFlow() public { + bytes12 lockTag = _getLockTag(); + uint256 nonce = 1; + + // Prepare token permissions + ISignatureTransfer.TokenPermissions[] memory permitted = _createTokenPermissions(address(usdc), defaultAmount); + + // Prepare deposit details + DepositDetails memory details = _createDepositDetails(nonce, defaultExpiration, lockTag); + + // Compute ids + uint256[] memory ids = new uint256[](1); + ids[0] = AllocatorLib.toId(lockTag, address(usdc)); + + // Expected nonce from prepareAllocation + uint256 expectedNonce = 1; + + // Compute commitment hashes using proper Lock struct encoding + bytes32[] memory commitmentHashes = new bytes32[](1); + commitmentHashes[0] = _computeCommitmentHash(ids[0], defaultAmount); + + // For empty witness, router uses BATCH_COMPACT_TYPEHASH (no Mandate) + bytes32 typeHash = BATCH_COMPACT_TYPEHASH; + + // Compute claimHash WITHOUT witnessHash (empty witness case) + bytes32 claimHash = keccak256( + abi.encode( + typeHash, + arbiter, + sponsor, + expectedNonce, + details.deadline, + keccak256(abi.encodePacked(commitmentHashes)) + ) + ); + + // Create Permit2 signature + bytes memory signature = _createPermit2Signature(permitted, details, claimHash, sponsorPrivateKey); + + // Execute deposit + router.depositRegisterAndAllocate(sponsor, arbiter, permitted, details, bytes32(0), '', signature); + + // Now execute the claim + address recipient = makeAddr('recipient'); + Component[] memory portions = new Component[](1); + portions[0] = + Component({claimant: uint256(bytes32(abi.encodePacked(bytes12(0), recipient))), amount: defaultAmount}); + + BatchClaimComponent[] memory claims = new BatchClaimComponent[](1); + claims[0] = BatchClaimComponent({id: ids[0], allocatedAmount: defaultAmount, portions: portions}); + + BatchClaim memory claim = BatchClaim({ + allocatorData: '', + sponsorSignature: '', + sponsor: sponsor, + nonce: expectedNonce, + expires: details.deadline, + witness: bytes32(0), + witnessTypestring: '', + claims: claims + }); + + // Execute claim + vm.prank(arbiter); + bytes32 returnedClaimHash = compact.batchClaim(claim); + assertEq(returnedClaimHash, claimHash); + + // Verify tokens transferred + assertEq(usdc.balanceOf(recipient), defaultAmount); + assertEq(usdc.balanceOf(address(compact)), 0); + } +} From c66784559b1e61d88cf621d1e6a097d8b35f9043 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Thu, 4 Dec 2025 14:19:35 +0100 Subject: [PATCH 3/9] added permit2Allocation in HybridAllocator and OnChainAllocator --- .gas-snapshot | 59 +++ foundry.lock | 26 ++ src/allocators/HybridAllocator.sol | 52 ++- src/allocators/HybridERC7683.sol | 13 +- src/allocators/OnChainAllocator.sol | 65 ++- src/allocators/lib/AllocatorLib.sol | 177 ++++++- test/ERC7683Allocator.t.sol | 9 +- test/HybridAllocator.t.sol | 685 +++++++++++++++++++++++++++- test/HybridERC7683.t.sol | 21 +- test/OnChainAllocator.t.sol | 601 +++++++++++++++++++++++- test/util/ERC7683TestHelper.sol | 16 +- 11 files changed, 1645 insertions(+), 79 deletions(-) create mode 100644 .gas-snapshot create mode 100644 foundry.lock diff --git a/.gas-snapshot b/.gas-snapshot new file mode 100644 index 0000000..b829854 --- /dev/null +++ b/.gas-snapshot @@ -0,0 +1,59 @@ +HybridAllocatorTest:test_addSigner_revert_CallerNotSigner(address) (runs: 1001, μ: 20911, ~: 20911) +HybridAllocatorTest:test_addSigner_revert_signerIsZero() (gas: 20241) +HybridAllocatorTest:test_addSigner_success(address) (runs: 1001, μ: 45667, ~: 45667) +HybridAllocatorTest:test_allocateAndRegister_checkClaimHashNoWitness() (gas: 154202) +HybridAllocatorTest:test_allocateAndRegister_checkClaimHashWitness() (gas: 156090) +HybridAllocatorTest:test_allocateAndRegister_checkNonceIncrements_erc20Token() (gas: 284180) +HybridAllocatorTest:test_allocateAndRegister_checkNonceIncrements_nativeToken() (gas: 225211) +HybridAllocatorTest:test_allocateAndRegister_revert_InvalidAllocatorIdERC20() (gas: 26257) +HybridAllocatorTest:test_allocateAndRegister_revert_InvalidAllocatorIdNative() (gas: 30701) +HybridAllocatorTest:test_allocateAndRegister_revert_InvalidIds() (gas: 18197) +HybridAllocatorTest:test_allocateAndRegister_revert_InvalidValue() (gas: 29761) +HybridAllocatorTest:test_allocateAndRegister_revert_invalidTokenOrder() (gas: 94072) +HybridAllocatorTest:test_allocateAndRegister_revert_tokensNotProvided() (gas: 89461) +HybridAllocatorTest:test_allocateAndRegister_revert_zeroNativeTokensAmount() (gas: 48144) +HybridAllocatorTest:test_allocateAndRegister_revert_zeroTokensAmount() (gas: 68951) +HybridAllocatorTest:test_allocateAndRegister_slot() (gas: 143295) +HybridAllocatorTest:test_allocateAndRegister_success_erc20Token() (gas: 205628) +HybridAllocatorTest:test_allocateAndRegister_success_erc20TokenWithEmptyAmountInput() (gas: 206220) +HybridAllocatorTest:test_allocateAndRegister_success_multipleTokens() (gas: 251098) +HybridAllocatorTest:test_allocateAndRegister_success_nativeToken() (gas: 149359) +HybridAllocatorTest:test_allocateAndRegister_success_nativeTokenWithEmptyAmountInput() (gas: 149596) +HybridAllocatorTest:test_attest_revert_Unsupported() (gas: 25278) +HybridAllocatorTest:test_attest_revert_transferFailed() (gas: 95132) +HybridAllocatorTest:test_authorizeClaim_registrationDeleted() (gas: 177084) +HybridAllocatorTest:test_authorizeClaim_revert_invalidCaller(address) (runs: 1001, μ: 184831, ~: 184831) +HybridAllocatorTest:test_authorizeClaim_success_offChain(uint88) (runs: 1001, μ: 243653, ~: 243656) +HybridAllocatorTest:test_authorizeClaim_success_onChain() (gas: 303687) +HybridAllocatorTest:test_checkAllocatorId() (gas: 6933) +HybridAllocatorTest:test_checkNonce() (gas: 8274) +HybridAllocatorTest:test_checkSignerCount() (gas: 8247) +HybridAllocatorTest:test_checkSigners(address) (runs: 1001, μ: 15788, ~: 15788) +HybridAllocatorTest:test_constructor_revert_signerIsAddressZero() (gas: 38960) +HybridAllocatorTest:test_executeAllocation_revert_InvalidBalanceChange_noDeposit() (gas: 139052) +HybridAllocatorTest:test_executeAllocation_revert_InvalidPreparation() (gas: 157553) +HybridAllocatorTest:test_executeAllocation_revert_InvalidPreparation_replaySameTx() (gas: 195147) +HybridAllocatorTest:test_executeAllocation_revert_InvalidRegistration() (gas: 145394) +HybridAllocatorTest:test_executeAllocation_success_viaCaller_singleERC20() (gas: 196565) +HybridAllocatorTest:test_isClaimAuthorized_invalidSignature() (gas: 50044) +HybridAllocatorTest:test_isClaimAuthorized_signerZeroAddress() (gas: 18471) +HybridAllocatorTest:test_isClaimAuthorized_unauthorized() (gas: 159921) +HybridAllocatorTest:test_isClaimAuthorized_withSigner_bytes64() (gas: 51647) +HybridAllocatorTest:test_isClaimAuthorized_withSigner_bytes65() (gas: 51953) +HybridAllocatorTest:test_permit2Allocation_emitsAllocatedEvent() (gas: 198190) +HybridAllocatorTest:test_permit2Allocation_fullClaimFlow() (gas: 210989) +HybridAllocatorTest:test_permit2Allocation_multipleERC20() (gas: 267781) +HybridAllocatorTest:test_permit2Allocation_revert_invalidNonceCommand() (gas: 35013) +HybridAllocatorTest:test_permit2Allocation_revert_invalidNonceSponsor() (gas: 24712) +HybridAllocatorTest:test_permit2Allocation_singleERC20() (gas: 198726) +HybridAllocatorTest:test_permit2Allocation_singleERC20_withWitness() (gas: 199395) +HybridAllocatorTest:test_prepareAllocation_returnsNonce_andDoesNotIncrement() (gas: 29989) +HybridAllocatorTest:test_removeSigner_revert_CallerNotSigner(address) (runs: 1001, μ: 20806, ~: 20806) +HybridAllocatorTest:test_removeSigner_revert_InvalidSigner(address) (runs: 1001, μ: 50324, ~: 50324) +HybridAllocatorTest:test_removeSigner_revert_LastSigner() (gas: 21940) +HybridAllocatorTest:test_removeSigner_success(address) (runs: 1001, μ: 43265, ~: 43265) +HybridAllocatorTest:test_removeSigner_success_deleteSelf(address) (runs: 1001, μ: 34246, ~: 34234) +HybridAllocatorTest:test_replaceSigner_revert_CallerNotSigner(address) (runs: 1001, μ: 19776, ~: 19776) +HybridAllocatorTest:test_replaceSigner_revert_signerIsZero() (gas: 20153) +HybridAllocatorTest:test_replaceSigner_success(address) (runs: 1001, μ: 39510, ~: 39510) +HybridAllocatorTest:test_revert_authorizeClaim_InvalidSignature(uint88) (runs: 1001, μ: 185859, ~: 185859) \ No newline at end of file diff --git a/foundry.lock b/foundry.lock new file mode 100644 index 0000000..c1caec0 --- /dev/null +++ b/foundry.lock @@ -0,0 +1,26 @@ +{ + "lib/forge-chronicles": { + "rev": "d1fb566915f23a01f08747264c56f8925f15751a" + }, + "lib/forge-gas-snapshot": { + "rev": "cf34ad1ed0a1f323e77557b9bce420f3385f7400" + }, + "lib/forge-std": { + "rev": "c7be2a3481f9e51230880bb0949072c7e3a4da82" + }, + "lib/openzeppelin-contracts": { + "rev": "99eda2225c0246c265c902475c47ec0c6321f119" + }, + "lib/solady": { + "rev": "834bbc4fd366ca8bce8c532a0e3b34eca6be709c" + }, + "lib/the-compact": { + "branch": { + "name": "utility-lib", + "rev": "2506f439573138c64421a6a6d822318d3e9bca52" + } + }, + "lib/tribunal": { + "rev": "21b6e8096d8d940789eeb63c5d281e490fd63025" + } +} \ No newline at end of file diff --git a/src/allocators/HybridAllocator.sol b/src/allocators/HybridAllocator.sol index b678e6c..1c27fdc 100644 --- a/src/allocators/HybridAllocator.sol +++ b/src/allocators/HybridAllocator.sol @@ -14,6 +14,8 @@ import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; import {Extsload} from '@uniswap/the-compact/lib/Extsload.sol'; import {IdLib} from '@uniswap/the-compact/lib/IdLib.sol'; +import {DepositDetails} from '@uniswap/the-compact/types/DepositDetails.sol'; +import {ISignatureTransfer} from 'permit2/src/interfaces/ISignatureTransfer.sol'; import {IHybridAllocator} from 'src/interfaces/IHybridAllocator.sol'; /// @title HybridAllocator @@ -34,8 +36,10 @@ contract HybridAllocator is IHybridAllocator { mapping(bytes32 claimHash => bool allocated) internal claims; - /// @dev The off chain allocator must use a uint256 nonce where the first 160 bits are the sponsors address to ensure no nonce collisions - uint96 public nonces; + /// @dev The off chain allocator must use a uint256 nonce where the first byte is the off chain nonce command (0xfc). + /// The next 20 bytes are the sponsors address, followed by the freely chosen nonce within the next 11 bytes. + /// This will prevent nonce collisions. + uint88 public nonces; /// @notice The total number of authorized signers for off-chain allocations uint256 public signerCount; /// @notice Mapping tracking which addresses are authorized signers for off-chain allocations @@ -160,9 +164,10 @@ contract HybridAllocator is IHybridAllocator { recipient = AL.getRecipient(recipient); idsAndAmounts = _actualIdsAndAmounts(idsAndAmounts); + uint256 nonce = AL.getNonceWithCommand(AL.ON_CHAIN_NONCE, ++nonces); (bytes32 claimHash, uint256[] memory registeredAmounts) = ITheCompact(AL.THE_COMPACT).batchDepositAndRegisterFor{ value: msg.value - }(recipient, idsAndAmounts, arbiter, ++nonces, expires, typehash, witness); + }(recipient, idsAndAmounts, arbiter, nonce, expires, typehash, witness); Lock[] memory commitments = new Lock[](idsAndAmounts.length); for (uint256 i = 0; i < idsAndAmounts.length; i++) { @@ -176,9 +181,25 @@ contract HybridAllocator is IHybridAllocator { // Allocate the claim claims[claimHash] = true; - emit Allocated(recipient, commitments, nonces, expires, claimHash); + emit Allocated(recipient, commitments, nonce, expires, claimHash); - return (claimHash, registeredAmounts, nonces); + return (claimHash, registeredAmounts, nonce); + } + + function permit2Allocation( + address depositor, + ISignatureTransfer.TokenPermissions[] calldata permitted, + DepositDetails calldata details, + bytes32 claimHash, + string calldata witness, + bytes calldata signature + ) external returns (Lock[] memory commitments) { + commitments = AL.permit2Allocation(depositor, permitted, details, claimHash, witness, signature); + + // Allocate the claim + claims[claimHash] = true; + + emit Allocated(depositor, commitments, details.nonce, details.deadline, claimHash); } /// @inheritdoc IOnChainAllocation @@ -191,8 +212,10 @@ contract HybridAllocator is IHybridAllocator { bytes32 witness, bytes calldata /* orderData */ ) external returns (uint256 nonce) { - nonce = nonces + 1; - AL.prepareAllocation(nonce, recipient, idsAndAmounts, arbiter, expires, typehash, witness, ALLOCATOR_ID); + uint88 nonce88 = nonces + 1; + + nonce = + AL.prepareAllocation(nonce88, recipient, idsAndAmounts, arbiter, expires, typehash, witness, ALLOCATOR_ID); } /// @inheritdoc IOnChainAllocation @@ -205,10 +228,10 @@ contract HybridAllocator is IHybridAllocator { bytes32 witness, bytes calldata /* orderData */ ) external { - uint256 nonce = ++nonces; + uint88 nonce88 = ++nonces; - (bytes32 claimHash, Lock[] memory commitments) = - AL.executeAllocation(nonce, recipient, idsAndAmounts, arbiter, expires, typehash, witness); + (bytes32 claimHash, Lock[] memory commitments, uint256 nonce) = + AL.executeAllocation(nonce88, recipient, idsAndAmounts, arbiter, expires, typehash, witness); // Allocate the claim claims[claimHash] = true; @@ -220,8 +243,8 @@ contract HybridAllocator is IHybridAllocator { function authorizeClaim( bytes32 claimHash, address, /*arbiter*/ - address, /*sponsor*/ - uint256, /*nonce*/ + address sponsor, + uint256 nonce, uint256, /*expires*/ uint256[2][] calldata, /*idsAndAmounts*/ bytes calldata allocatorData_ @@ -235,10 +258,15 @@ contract HybridAllocator is IHybridAllocator { if (claims[claimHash]) { delete claims[claimHash]; + // If the claim hash is matching, the nonce must be either an on chain nonce, or a permit2 scoped nonce + // Authorize the claim return IAllocator.authorizeClaim.selector; } + // Verify the nonce is scoped to an off chain allocation and to the sponsor + AL.verifyNonce(nonce, AL.OFF_CHAIN_NONCE, sponsor); + // Check the allocator data for a valid signature by an authorized signer bytes32 digest = _deriveDigest(claimHash, _COMPACT_DOMAIN_SEPARATOR); if (block.chainid != _INITIAL_CHAIN_ID) { diff --git a/src/allocators/HybridERC7683.sol b/src/allocators/HybridERC7683.sol index a7673d6..aec55a1 100644 --- a/src/allocators/HybridERC7683.sol +++ b/src/allocators/HybridERC7683.sol @@ -105,7 +105,7 @@ contract HybridERC7683 is HybridAllocator, IERC7683Allocator { } // We ignore the order.nonce and use the one assigned by the hybrid allocator - resolvedOrder.orderId = bytes32(uint256(nonces) + 1); + resolvedOrder.orderId = bytes32(AL.getNonceWithCommand(AL.ON_CHAIN_NONCE, uint248(nonces) + 1)); return resolvedOrder; } @@ -115,7 +115,14 @@ contract HybridERC7683 is HybridAllocator, IERC7683Allocator { (IERC7683Allocator.Order calldata orderData, uint32 expires,, bytes32[] memory fillHashes) = ERC7683AL.openPreparation(order); - return ERC7683AL.resolveOrder(msg.sender, nonces + 1, expires, fillHashes, orderData, LibBytes.emptyCalldata()); + return ERC7683AL.resolveOrder( + msg.sender, + AL.getNonceWithCommand(AL.ON_CHAIN_NONCE, uint248(nonces) + 1), + expires, + fillHashes, + orderData, + LibBytes.emptyCalldata() + ); } /// @inheritdoc IERC7683Allocator @@ -133,7 +140,7 @@ contract HybridERC7683 is HybridAllocator, IERC7683Allocator { revert OnlyDepositsAllowed(); } - return nonces + 1; + return AL.getNonceWithCommand(AL.ON_CHAIN_NONCE, uint248(nonces) + 1); } /// @inheritdoc IERC7683Allocator diff --git a/src/allocators/OnChainAllocator.sol b/src/allocators/OnChainAllocator.sol index 45d5d62..e84b65e 100644 --- a/src/allocators/OnChainAllocator.sol +++ b/src/allocators/OnChainAllocator.sol @@ -14,8 +14,10 @@ import {IOnChainAllocation} from '@uniswap/the-compact/interfaces/IOnChainAlloca import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; import {Extsload} from '@uniswap/the-compact/lib/Extsload.sol'; import {IdLib} from '@uniswap/the-compact/lib/IdLib.sol'; +import {DepositDetails} from '@uniswap/the-compact/types/DepositDetails.sol'; import {Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; import {Utility} from '@uniswap/the-compact/utility/Utility.sol'; +import {ISignatureTransfer} from 'permit2/src/interfaces/ISignatureTransfer.sol'; /// @title OnChainAllocator /// @notice Allocates tokens deposited into the compact. @@ -33,8 +35,8 @@ contract OnChainAllocator is IOnChainAllocator, Utility { mapping(bytes32 tokenHash => Allocation[] allocations) internal _allocations; /// @notice Mapping of user addresses to their current nonce for replay protection. - /// @dev The actual nonce will be a combination of the next free nonce and the user address. - mapping(address user => uint96 nonce) public nonces; + /// @dev The actual nonce will be a combination of the on chain nonce command, the users address and the free nonce. + mapping(address user => uint88 nonce) public nonces; modifier onlyCompact() { if (msg.sender != AL.THE_COMPACT) { @@ -241,6 +243,38 @@ contract OnChainAllocator is IOnChainAllocator, Utility { return commitments; } + function permit2Allocation( + address depositor, + ISignatureTransfer.TokenPermissions[] calldata permitted, + DepositDetails calldata details, + bytes32 claimHash, + string calldata witness, + bytes calldata signature + ) external returns (Lock[] memory commitments) { + if (details.deadline > type(uint32).max) { + revert InvalidExpiration(details.deadline, type(uint32).max); + } + + commitments = AL.permit2Allocation(depositor, permitted, details, claimHash, witness, signature); + + // Allocate the claim + for (uint256 i = 0; i < commitments.length; i++) { + // Check the amount fits in the supported range + if (commitments[i].amount > type(uint224).max) { + revert InvalidAmount(commitments[i].amount); + } + + _storeAllocation( + commitments[i].lockTag, + commitments[i].token, + uint224(commitments[i].amount), + depositor, + uint32(details.deadline), + claimHash + ); + } + } + /// @inheritdoc IOnChainAllocation function prepareAllocation( address recipient, @@ -255,9 +289,10 @@ contract OnChainAllocator is IOnChainAllocator, Utility { revert InvalidExpiration(expires, type(uint32).max); } uint32 expiration = uint32(expires); - nonce = _getNonce(msg.sender, recipient); - - AL.prepareAllocation(nonce, recipient, idsAndAmounts, arbiter, expiration, typehash, witness, ALLOCATOR_ID); + nonce = _getNonce(msg.sender, recipient); // Includes command. AL will handle the command, so we remove it by casting to uint248. + AL.prepareAllocation( + uint248(nonce), recipient, idsAndAmounts, arbiter, expiration, typehash, witness, ALLOCATOR_ID + ); return nonce; } @@ -276,7 +311,7 @@ contract OnChainAllocator is IOnChainAllocator, Utility { revert InvalidExpiration(expires, type(uint32).max); } uint32 expiration = uint32(expires); - uint256 nonce = _getAndUpdateNonce(msg.sender, recipient); + uint256 nonce = _getAndUpdateNonce(msg.sender, recipient); // Includes command. AL will handle the command, so we remove it by casting to uint248. (bytes32 claimHash, Lock[] memory commitments) = _executeAllocation(nonce, recipient, idsAndAmounts, arbiter, expiration, typehash, witness); @@ -294,7 +329,7 @@ contract OnChainAllocator is IOnChainAllocator, Utility { bytes32 witness ) private returns (bytes32, Lock[] memory) { (bytes32 claimHash, Lock[] memory commitments) = - AL.executeAllocation(nonce, recipient, idsAndAmounts, arbiter, expires, typehash, witness); + AL.executeAllocation(uint248(nonce), recipient, idsAndAmounts, arbiter, expires, typehash, witness); // Allocate the claim for (uint256 i = 0; i < commitments.length; i++) { @@ -579,25 +614,31 @@ contract OnChainAllocator is IOnChainAllocator, Utility { } function _getAndUpdateNonce(address calling, address sponsor) internal returns (uint256 nonce) { + // Create an on chain nonce by combining the command, the calling address and the next free nonce. + // Updates the free nonce pointer. + bytes1 onChainNonceCommand = AL.ON_CHAIN_NONCE; assembly ("memory-safe") { sponsor := mul(sponsor, iszero(calling)) mstore(0x00, sponsor) mstore(0x20, nonces.slot) let nonceSlot := keccak256(0x00, 0x40) - let nonce96 := sload(nonceSlot) - nonce := or(shl(96, sponsor), add(nonce96, 1)) - sstore(nonceSlot, add(nonce96, 1)) + let nonce88 := sload(nonceSlot) + nonce := or(onChainNonceCommand, or(shl(88, sponsor), add(nonce88, 1))) + sstore(nonceSlot, add(nonce88, 1)) } } function _getNonce(address calling, address sponsor) internal view returns (uint256 nonce) { + // Create an on chain nonce by combining the command, the calling address and the next free nonce. + // Does NOT update the free nonce pointer. + bytes1 onChainNonceCommand = AL.ON_CHAIN_NONCE; assembly ("memory-safe") { sponsor := mul(sponsor, iszero(calling)) mstore(0x00, sponsor) mstore(0x20, nonces.slot) let nonceSlot := keccak256(0x00, 0x40) - let nonce96 := sload(nonceSlot) - nonce := or(shl(96, sponsor), add(nonce96, 1)) + let nonce88 := sload(nonceSlot) + nonce := or(onChainNonceCommand, or(shl(88, sponsor), add(nonce88, 1))) } } diff --git a/src/allocators/lib/AllocatorLib.sol b/src/allocators/lib/AllocatorLib.sol index 34b5762..468a573 100644 --- a/src/allocators/lib/AllocatorLib.sol +++ b/src/allocators/lib/AllocatorLib.sol @@ -5,6 +5,10 @@ import {ERC6909} from '@solady/tokens/ERC6909.sol'; import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; import {LOCK_TYPEHASH, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; +import {ISignatureTransfer} from 'permit2/src/interfaces/ISignatureTransfer.sol'; + +import {CompactCategory} from 'the-compact/src/types/CompactCategory.sol'; +import {DepositDetails} from 'the-compact/src/types/DepositDetails.sol'; /// @title AllocatorLib /// @notice Library providing core functionality for atomic token allocation verification using transient storage @@ -31,15 +35,152 @@ library AllocatorLib { /// @notice Storage slot seed on the compact for mapping allocator IDs to allocator addresses. uint256 private constant ALLOCATOR_BY_ALLOCATOR_ID_SLOT_SEED = 0x000044036fc77deaed2300000000000000000000000; + /// @notice The command indicating an on chain nonce + bytes1 internal constant ON_CHAIN_NONCE = 0x01; + + /// @notice The command indicating an off chain nonce + bytes1 internal constant OFF_CHAIN_NONCE = 0x02; + + /// @notice The command indicating a permit2 nonce + bytes1 internal constant PERMIT2_NONCE = 0x03; + + bytes1 internal constant NONCE_COMMAND_MASK = 0xff; + error InvalidBalanceChange(uint256 newBalance, uint256 oldBalance); error InvalidPreparation(); error InvalidAllocatorId(uint96 providedId, uint96 allocatorId); error InvalidRegistration(address recipient, bytes32 claimHash, bytes32 typehash); error CompactReentrancyGuardActive(); error InvalidAllocator(); + error UnauthorizedNonce(bytes1 command, address sponsor); + error InvalidCompactCall(address theCompact); + + function permit2Allocation( + address depositor, + ISignatureTransfer.TokenPermissions[] calldata permitted, + DepositDetails calldata details, + bytes32 claimHash, // This claim hash is connected to the allocation. This does not guarantee, that the allocated tokens are connected to the claim hash. + string calldata witness, + bytes calldata signature + ) internal returns (Lock[] memory commitments) { + // Verifying the nonce is scoped to a permit2 allocation and to the sponsor + verifyNonce(details.nonce, PERMIT2_NONCE, depositor); + // We can now trust permit2 to burn the nonce and prevent replay attacks + + commitments = new Lock[](permitted.length); + bytes12 lockTag = details.lockTag; + + // Prepare allocation + assembly ("memory-safe") { + let m := mload(0x40) // Store the memory pointer. Will be dirtied and restored at the end of the function. + + // Memory layout for Lock[]: + // - commitments + 0x00: length (permittedLength) + // - commitments + 0x20: absolute pointer to the Lock struct [0] + // - commitments + 0x40: absolute pointer to the Lock struct [1] + // - commitments + 0x60: lockTag[0] + // - commitments + 0x80: token[0] + // - commitments + 0xa0: amount[0] + // - commitments + 0xc0: lockTag[1] + // - ... + let permittedLength := permitted.length + let commitmentsContent := add(commitments, 0x20) + + mstore(0x14, depositor) // Store the `owner` as the first argument for the balanceOf call. + mstore(0x00, 0x00fdd58e000000000000000000000000) // function selector of `balanceOf(address,uint256)`. + + for { let i := 0 } lt(i, permittedLength) { i := add(i, 1) } { + let token := calldataload(add(permitted.offset, mul(i, 0x40))) + + // Store the lockTag and token in the Lock struct + let commitmentMemLoc := mload(add(commitmentsContent, mul(i, 0x20))) // load the absolute pointer to the Lock struct + mstore(commitmentMemLoc, lockTag) // lockTag + mstore(add(commitmentMemLoc, 0x20), token) // token + + // Store the id as the second argument for the balanceOf call. + mstore(0x34, or(lockTag, token)) + + // Retrieve and store the current balance of the depositor into the amount slot (temporarily) + let commitmentAmountMemLoc := add(commitmentMemLoc, 0x40) + + if iszero(staticcall(gas(), THE_COMPACT, 0x10, 0x44, commitmentAmountMemLoc, 0x20)) { + mstore(0x00, 0x6d728277) // InvalidCompactCall(address theCompact) + mstore(0x20, THE_COMPACT) + revert(0x1c, 0x24) + } + } + + // Deposit and register the tokens using permit2 + mstore(add(m, 0x20), depositor) + mstore(add(m, 0x0c), 0x45ebe218000000000000000000000000) // function selector of `batchDepositAndRegisterViaPermit2()`. + mstore(add(m, 0x40), 0x120) // Store the offset for the permitted + calldatacopy(add(m, 0x60), details, 0x60) // Store the details from calldata to memory + mstore(add(m, 0xc0), claimHash) + mstore(add(m, 0xe0), 0x01) // uint8(CompactCategory.BatchCompact) + let witnessOffset := add(0x140, mul(permittedLength, 0x40)) + mstore(add(m, 0x100), witnessOffset) + let signatureOffset := add(add(witnessOffset, 0x20), and(add(witness.length, 31), not(31))) // round up to the nearest multiple of 32 + mstore(add(m, 0x120), signatureOffset) + // store permitted length & contents to memory + mstore(add(m, 0x140), permittedLength) + calldatacopy(add(m, 0x160), permitted.offset, mul(permittedLength, 0x40)) + // store witness contents to memory + let witnessMemLoc := add(m, add(0x20, witnessOffset)) // Add 0x20 to skip the function selector + mstore(witnessMemLoc, witness.length) + calldatacopy(add(witnessMemLoc, 0x20), witness.offset, witness.length) + // store signature contents to memory + let signatureMemLoc := add(m, add(0x20, signatureOffset)) + mstore(signatureMemLoc, signature.length) + calldatacopy(add(signatureMemLoc, 0x20), signature.offset, signature.length) + let fullCallDataSize := add(add(signatureOffset, 0x24), and(add(signature.length, 31), not(31))) + + // Call the batchDepositAndRegisterViaPermit2 function and revert if it fails + if iszero(call(gas(), THE_COMPACT, callvalue(), add(m, 0x1c), fullCallDataSize, 0, 0)) { + mstore(0x00, 0x6d728277) // InvalidCompactCall(address theCompact) + mstore(0x20, THE_COMPACT) + revert(0x1c, 0x24) + } + + // Confirm the allocation - calculate balance differences + for { let i := 0 } lt(i, permittedLength) { i := add(i, 1) } { + let commitmentMemLoc := mload(add(commitmentsContent, mul(i, 0x20))) // load the absolute pointer to the Lock struct + // Reconstruct id from stored lockTag and token + let token := mload(add(commitmentMemLoc, 0x20)) + + let commitmentAmountMemLoc := add(commitmentMemLoc, 0x40) + let oldBalance := mload(commitmentAmountMemLoc) + + // Store the id as the second argument for the balanceOf call. + mstore(0x34, or(lockTag, token)) + + // Retrieve the new balance of the depositor and store it in the amount slot of the commitment + if iszero(staticcall(gas(), THE_COMPACT, 0x10, 0x44, commitmentAmountMemLoc, 0x20)) { + mstore(0x00, 0x6d728277) // InvalidCompactCall(address theCompact) + mstore(0x20, THE_COMPACT) + revert(0x1c, 0x24) + } + + let currentBalance := mload(commitmentAmountMemLoc) + if iszero(gt(currentBalance, oldBalance)) { + mstore(0x00, 0x9f2aec67) // InvalidBalanceChange() + mstore(0x20, currentBalance) + mstore(0x40, oldBalance) + revert(0x1c, 0x44) + } + let diffBalance := sub(currentBalance, oldBalance) + + // Update the amount in the Lock struct with the balance difference + mstore(commitmentAmountMemLoc, diffBalance) + } + + mstore(0x40, m) // Restore the memory pointer + } + + return commitments; + } function prepareAllocation( - uint256 nonce, + uint248 noncePreCommand, address recipient, uint256[2][] calldata idsAndAmounts, address arbiter, @@ -47,10 +188,12 @@ library AllocatorLib { bytes32 typehash, bytes32 witness, uint96 allocatorId - ) internal { + ) internal returns (uint256 nonce) { // Before preparing the allocation, check if the compact's reentrancy guard is active checkCompactReentrancyGuardAndRevert(); + nonce = getNonceWithCommand(ON_CHAIN_NONCE, noncePreCommand); + assembly ("memory-safe") { // identifier = keccak256(abi.encode(PREPARE_ALLOCATION_SELECTOR, recipient, ids, arbiter, expires, typehash, witness)); let memoryPointer := mload(0x40) @@ -108,19 +251,21 @@ library AllocatorLib { } function executeAllocation( - uint256 nonce, + uint248 noncePreCommand, address recipient, uint256[2][] calldata idsAndAmounts, address arbiter, uint256 expires, bytes32 typehash, bytes32 witness - ) internal view returns (bytes32 claimHash, Lock[] memory) { + ) internal view returns (bytes32 claimHash, Lock[] memory, uint256 nonce) { bytes32[] memory commitmentHashes = new bytes32[](idsAndAmounts.length); Lock[] memory commitments = new Lock[](idsAndAmounts.length); bytes32 commitmentsHash; uint256 storedNonce; + nonce = getNonceWithCommand(ON_CHAIN_NONCE, noncePreCommand); + // Before executing the allocation, check if the compact's reentrancy guard is active checkCompactReentrancyGuardAndRevert(); @@ -160,7 +305,7 @@ library AllocatorLib { mstore(0x00, PREPARE_ALLOCATION_SELECTOR) mstore(0x20, recipient) mstore(0x40, id) - // Store the current balance in transient storage + // Read the old balance from transient storage let oldBalance := tload(keccak256(0x00, 0x60)) if iszero(gt(currentBalance, oldBalance)) { mstore(0x00, 0x9f2aec67) // InvalidBalanceChange() @@ -215,7 +360,7 @@ library AllocatorLib { if (!ITheCompact(THE_COMPACT).isRegistered(recipient, claimHash, typehash)) { revert InvalidRegistration(recipient, claimHash, typehash); } - return (claimHash, commitments); + return (claimHash, commitments, storedNonce); } function checkCompactReentrancyGuardAndRevert() internal view { @@ -349,6 +494,26 @@ library AllocatorLib { return recipient; } + function getNonceWithCommand(bytes1 command, uint248 noncePreCommand) internal pure returns (uint256 nonce) { + assembly ("memory-safe") { + nonce := or(command, noncePreCommand) + } + return nonce; + } + + function verifyNonce(uint256 nonce, bytes1 expectedCommand, address expectedSponsor) internal pure { + assembly ("memory-safe") { + let command := and(nonce, NONCE_COMMAND_MASK) + let sponsor := shr(96, shl(8, nonce)) + if iszero(and(eq(command, expectedCommand), eq(sponsor, expectedSponsor))) { + mstore(0x00, 0xb8a0afb2) // UnauthorizedNonce() + mstore(0x20, command) + mstore(0x40, sponsor) + revert(0x1c, 0x44) + } + } + } + function splitId(uint256 id) internal pure returns (uint96 allocatorId_, address token_) { return (splitAllocatorId(id), splitToken(id)); } diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index b28491b..d98033f 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -55,7 +55,8 @@ contract MockAllocator is GaslessCrossChainOrderData, OnChainCrossChainOrderData assertEq(address(compactContract_), address(0x00000000000000171ede64904551eeDF3C6C9788)); erc7683Allocator = new ERC7683Allocator(); - _setUp(address(erc7683Allocator), compactContract_, _composeNonceUint(user, 1)); + // Most pre-registration tests use on-chain flows, so default to ON_CHAIN_NONCE + _setUp(address(erc7683Allocator), compactContract_, _composeNonceUint(ON_CHAIN_NONCE, user, 1)); super.setUp(); } } @@ -1267,7 +1268,8 @@ contract ERC7683Allocator_openForDeposit is MockAllocator { assertEq(ERC6909(address(compactContract)).balanceOf(user, usdcId), defaultAmount); - compact_.nonce = _composeNonceUint(address(0), 1); + // On-chain allocation uses ON_CHAIN_NONCE with address(0) in the nonce + compact_.nonce = _composeNonceUint(ON_CHAIN_NONCE, address(0), 1); compact_.commitments[0].amount = defaultAmount; (bytes32 mandateHash,) = _hashMandate(mandate_); @@ -1284,7 +1286,8 @@ contract ERC7683Allocator_openForDeposit is MockAllocator { usdc.mint(address(erc7683Allocator), amount); BatchCompact memory compact_ = _getCompact(); - compact_.nonce = _composeNonceUint(address(0), 1); + // On-chain allocation uses ON_CHAIN_NONCE with address(0) in the nonce + compact_.nonce = _composeNonceUint(ON_CHAIN_NONCE, address(0), 1); Mandate memory mandate_ = _getMandate(); IOriginSettler.GaslessCrossChainOrder memory order_ = _getGaslessCrossChainOrder(compact_, mandate_, true); diff --git a/test/HybridAllocator.t.sol b/test/HybridAllocator.t.sol index 8eb2b56..80e80ee 100644 --- a/test/HybridAllocator.t.sol +++ b/test/HybridAllocator.t.sol @@ -5,16 +5,30 @@ import {TestHelper} from './util/TestHelper.sol'; import {ERC20} from '@solady/tokens/ERC20.sol'; import {TheCompact} from '@uniswap/the-compact/TheCompact.sol'; import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; +import {ISignatureTransfer} from 'permit2/src/interfaces/ISignatureTransfer.sol'; import {BatchClaim} from '@uniswap/the-compact/types/BatchClaims.sol'; import {BatchClaimComponent, Component} from '@uniswap/the-compact/types/Components.sol'; -import {BATCH_COMPACT_TYPEHASH, BatchCompact, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; + +import {DepositDetails} from '@uniswap/the-compact/types/DepositDetails.sol'; +import { + BATCH_COMPACT_TYPEHASH, + BATCH_COMPACT_TYPESTRING_FRAGMENT_FIVE, + BATCH_COMPACT_TYPESTRING_FRAGMENT_FOUR, + BATCH_COMPACT_TYPESTRING_FRAGMENT_ONE, + BATCH_COMPACT_TYPESTRING_FRAGMENT_SIX, + BATCH_COMPACT_TYPESTRING_FRAGMENT_THREE, + BATCH_COMPACT_TYPESTRING_FRAGMENT_TWO, + BatchCompact, + Lock +} from '@uniswap/the-compact/types/EIP712Types.sol'; import {ResetPeriod} from '@uniswap/the-compact/types/ResetPeriod.sol'; import {Scope} from '@uniswap/the-compact/types/Scope.sol'; import {Test} from 'forge-std/Test.sol'; import {HybridAllocator} from 'src/allocators/HybridAllocator.sol'; +import {IOnChainAllocation} from '@uniswap/the-compact/interfaces/IOnChainAllocation.sol'; import {AllocatorLib} from 'src/allocators/lib/AllocatorLib.sol'; import {BATCH_COMPACT_WITNESS_TYPEHASH} from 'src/allocators/lib/TypeHashes.sol'; import {IHybridAllocator} from 'src/interfaces/IHybridAllocator.sol'; @@ -35,6 +49,7 @@ contract HybridAllocatorTest is Test, TestHelper { address signer; uint256 signerPrivateKey; ERC20Mock usdc; + ERC20Mock dai; address user; uint256 userPrivateKey; uint256 defaultAmount; @@ -44,20 +59,72 @@ contract HybridAllocatorTest is Test, TestHelper { BatchCompact batchCompact; + // Nonce command constants + bytes1 constant ON_CHAIN_NONCE = 0x01; + bytes1 constant OFF_CHAIN_NONCE = 0x02; + + // Helper to compose nonces with the command byte + // For on-chain allocations: address is address(0) + // For off-chain allocations: address is the sponsor + function _composeNonceUint(bytes1 command, address a, uint256 nonce) internal pure returns (uint256) { + return (uint256(uint8(command)) << 248) | (uint256(uint160(a)) << 88) | nonce; + } + + // Permit2 constants + address constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3; + bytes32 constant PERMIT2_DOMAIN_SEPARATOR_TYPEHASH = + keccak256('EIP712Domain(string name,uint256 chainId,address verifyingContract)'); + bytes32 constant TOKEN_PERMISSIONS_TYPEHASH = keccak256('TokenPermissions(address token,uint256 amount)'); + + // BatchActivation typehash for NO witness (from the-compact/src/types/EIP712Types.sol) + // keccak256(bytes("BatchActivation(address activator,uint256[] ids,BatchCompact compact)BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments)Lock(bytes12 lockTag,address token,uint256 amount)")) + bytes32 constant BATCH_COMPACT_BATCH_ACTIVATION_TYPEHASH = + 0xa794ed1a28cdf297ac45a3eee4643e35d29b295a389368da5f6baa420872c9b7; + + // BatchActivation typehash WITH witness "Mandate(uint256 witness)" + // The full typestring includes the Mandate struct definition + string constant BATCH_COMPACT_BATCH_ACTIVATION_WITH_WITNESS_TYPESTRING = + 'BatchActivation(address activator,uint256[] ids,BatchCompact compact)BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments,Mandate mandate)Lock(bytes12 lockTag,address token,uint256 amount)Mandate(uint256 witness)'; + bytes32 constant BATCH_COMPACT_BATCH_ACTIVATION_WITH_WITNESS_TYPEHASH = + keccak256(bytes(BATCH_COMPACT_BATCH_ACTIVATION_WITH_WITNESS_TYPESTRING)); + + // PermitBatchWitnessTransferFrom typehash (no mandate/witness) + string constant PERMIT_BATCH_WITNESS_TYPESTRING = + 'PermitBatchWitnessTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline,BatchActivation witness)BatchActivation(address activator,uint256[] ids,BatchCompact compact)BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments)Lock(bytes12 lockTag,address token,uint256 amount)TokenPermissions(address token,uint256 amount)'; + bytes32 constant PERMIT_BATCH_WITNESS_TYPEHASH = keccak256(bytes(PERMIT_BATCH_WITNESS_TYPESTRING)); + + // PermitBatchWitnessTransferFrom typehash WITH mandate/witness + // Used when the permit2Allocation is called with a non-empty witness typestring + string constant PERMIT_BATCH_WITNESS_WITH_MANDATE_TYPESTRING = + 'PermitBatchWitnessTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline,BatchActivation witness)BatchActivation(address activator,uint256[] ids,BatchCompact compact)BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments,Mandate mandate)Lock(bytes12 lockTag,address token,uint256 amount)Mandate(uint256 witness)TokenPermissions(address token,uint256 amount)'; + bytes32 constant PERMIT_BATCH_WITNESS_WITH_MANDATE_TYPEHASH = + keccak256(bytes(PERMIT_BATCH_WITNESS_WITH_MANDATE_TYPESTRING)); + function setUp() public { compact = DeployTheCompact(new DeployTheCompact()).deployTheCompact(); assertEq(address(compact), address(0x00000000000000171ede64904551eeDF3C6C9788)); + // Deploy Permit2 at the expected address + _deployPermit2(); + arbiter = makeAddr('arbiter'); (signer, signerPrivateKey) = makeAddrAndKey('signer'); allocator = new HybridAllocator(signer); usdc = new ERC20Mock('USDC', 'USDC'); + dai = new ERC20Mock('DAI', 'DAI'); (user, userPrivateKey) = makeAddrAndKey('user'); - deal(user, 1 ether); - usdc.mint(user, 1 ether); + deal(user, 10 ether); + usdc.mint(user, 10 ether); + dai.mint(user, 10 ether); defaultAmount = 1 ether; defaultExpiration = vm.getBlockTimestamp() + 1 days; + // Approve Permit2 for tokens + vm.startPrank(user); + usdc.approve(PERMIT2, type(uint256).max); + dai.approve(PERMIT2, type(uint256).max); + vm.stopPrank(); + allocationCaller = new OnChainAllocationCaller(address(allocator), address(compact)); batchCompact.arbiter = arbiter; @@ -66,6 +133,28 @@ contract HybridAllocatorTest is Test, TestHelper { batchCompact.expires = defaultExpiration; } + function _deployPermit2() internal { + // Deploy Permit2 using the same pattern as the-compact tests + address permit2Deployer = address(0x4e59b44847b379578588920cA78FbF26c0B4956C); + address permit2DeployerDeployer = address(0x3fAB184622Dc19b6109349B94811493BF2a45362); + bytes memory permit2DeployerCreationCode = + hex'604580600e600039806000f350fe7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3'; + + vm.deal(permit2DeployerDeployer, 1e18); + vm.prank(permit2DeployerDeployer); + address deployedPermit2Deployer; + assembly ("memory-safe") { + deployedPermit2Deployer := + create(0, add(permit2DeployerCreationCode, 0x20), mload(permit2DeployerCreationCode)) + } + + bytes memory permit2CreationCalldata = + hex'0000000000000000000000000000000000000000d3af2663da51c1021500000060c0346100bb574660a052602081017f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a86681527f9ac997416e8ff9d2ff6bebeb7149f65cdae5e32e2b90440b566bb3044041d36a60408301524660608301523060808301526080825260a082019180831060018060401b038411176100a557826040525190206080526123c090816100c1823960805181611b47015260a05181611b210152f35b634e487b7160e01b600052604160045260246000fd5b600080fdfe6040608081526004908136101561001557600080fd5b600090813560e01c80630d58b1db1461126c578063137c29fe146110755780632a2d80d114610db75780632b67b57014610bde57806330f28b7a14610ade5780633644e51514610a9d57806336c7851614610a285780633ff9dcb1146109a85780634fe02b441461093f57806365d9723c146107ac57806387517c451461067a578063927da105146105c3578063cc53287f146104a3578063edd9444b1461033a5763fe8ec1a7146100c657600080fd5b346103365760c07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff833581811161033257610114903690860161164b565b60243582811161032e5761012b903690870161161a565b6101336114e6565b9160843585811161032a5761014b9036908a016115c1565b98909560a43590811161032657610164913691016115c1565b969095815190610173826113ff565b606b82527f5065726d697442617463685769746e6573735472616e7366657246726f6d285460208301527f6f6b656e5065726d697373696f6e735b5d207065726d69747465642c61646472838301527f657373207370656e6465722c75696e74323536206e6f6e63652c75696e74323560608301527f3620646561646c696e652c000000000000000000000000000000000000000000608083015282519a8b9181610222602085018096611f93565b918237018a8152039961025b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09b8c8101835282611437565b5190209085515161026b81611ebb565b908a5b8181106102f95750506102f6999a6102ed9183516102a081610294602082018095611f66565b03848101835282611437565b519020602089810151858b015195519182019687526040820192909252336060820152608081019190915260a081019390935260643560c08401528260e081015b03908101835282611437565b51902093611cf7565b80f35b8061031161030b610321938c5161175e565b51612054565b61031b828661175e565b52611f0a565b61026e565b8880fd5b8780fd5b8480fd5b8380fd5b5080fd5b5091346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff9080358281116103325761038b903690830161164b565b60243583811161032e576103a2903690840161161a565b9390926103ad6114e6565b9160643590811161049f576103c4913691016115c1565b949093835151976103d489611ebb565b98885b81811061047d5750506102f697988151610425816103f9602082018095611f66565b037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101835282611437565b5190206020860151828701519083519260208401947ffcf35f5ac6a2c28868dc44c302166470266239195f02b0ee408334829333b7668652840152336060840152608083015260a082015260a081526102ed8161141b565b808b61031b8261049461030b61049a968d5161175e565b9261175e565b6103d7565b8680fd5b5082346105bf57602090817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103325780359067ffffffffffffffff821161032e576104f49136910161161a565b929091845b848110610504578580f35b8061051a610515600193888861196c565b61197c565b61052f84610529848a8a61196c565b0161197c565b3389528385528589209173ffffffffffffffffffffffffffffffffffffffff80911692838b528652868a20911690818a5285528589207fffffffffffffffffffffffff000000000000000000000000000000000000000081541690558551918252848201527f89b1add15eff56b3dfe299ad94e01f2b52fbcb80ae1a3baea6ae8c04cb2b98a4853392a2016104f9565b8280fd5b50346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610676816105ff6114a0565b936106086114c3565b6106106114e6565b73ffffffffffffffffffffffffffffffffffffffff968716835260016020908152848420928816845291825283832090871683528152919020549251938316845260a083901c65ffffffffffff169084015260d09190911c604083015281906060820190565b0390f35b50346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336576106b26114a0565b906106bb6114c3565b916106c46114e6565b65ffffffffffff926064358481169081810361032a5779ffffffffffff0000000000000000000000000000000000000000947fda9fa7c1b00402c17d0161b249b1ab8bbec047c5a52207b9c112deffd817036b94338a5260016020527fffffffffffff0000000000000000000000000000000000000000000000000000858b209873ffffffffffffffffffffffffffffffffffffffff809416998a8d5260205283878d209b169a8b8d52602052868c209486156000146107a457504216925b8454921697889360a01b16911617179055815193845260208401523392a480f35b905092610783565b5082346105bf5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576107e56114a0565b906107ee6114c3565b9265ffffffffffff604435818116939084810361032a57338852602091600183528489209673ffffffffffffffffffffffffffffffffffffffff80911697888b528452858a20981697888a5283528489205460d01c93848711156109175761ffff9085840316116108f05750907f55eb90d810e1700b35a8e7e25395ff7f2b2259abd7415ca2284dfb1c246418f393929133895260018252838920878a528252838920888a5282528389209079ffffffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffff000000000000000000000000000000000000000000000000000083549260d01b16911617905582519485528401523392a480f35b84517f24d35a26000000000000000000000000000000000000000000000000000000008152fd5b5084517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b503461033657807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336578060209273ffffffffffffffffffffffffffffffffffffffff61098f6114a0565b1681528084528181206024358252845220549051908152f35b5082346105bf57817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf577f3704902f963766a4e561bbaab6e6cdc1b1dd12f6e9e99648da8843b3f46b918d90359160243533855284602052818520848652602052818520818154179055815193845260208401523392a280f35b8234610a9a5760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610a9a57610a606114a0565b610a686114c3565b610a706114e6565b6064359173ffffffffffffffffffffffffffffffffffffffff8316830361032e576102f6936117a1565b80fd5b503461033657817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657602090610ad7611b1e565b9051908152f35b508290346105bf576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf57610b1a3661152a565b90807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c36011261033257610b4c611478565b9160e43567ffffffffffffffff8111610bda576102f694610b6f913691016115c1565b939092610b7c8351612054565b6020840151828501519083519260208401947f939c21a48a8dbe3a9a2404a1d46691e4d39f6583d6ec6b35714604c986d801068652840152336060840152608083015260a082015260a08152610bd18161141b565b51902091611c25565b8580fd5b509134610336576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610c186114a0565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc360160c08112610332576080855191610c51836113e3565b1261033257845190610c6282611398565b73ffffffffffffffffffffffffffffffffffffffff91602435838116810361049f578152604435838116810361049f57602082015265ffffffffffff606435818116810361032a5788830152608435908116810361049f576060820152815260a435938285168503610bda576020820194855260c4359087830182815260e43567ffffffffffffffff811161032657610cfe90369084016115c1565b929093804211610d88575050918591610d786102f6999a610d7e95610d238851611fbe565b90898c511690519083519260208401947ff3841cd1ff0085026a6327b620b67997ce40f282c88a8e905a7a5626e310f3d086528401526060830152608082015260808152610d70816113ff565b519020611bd9565b916120c7565b519251169161199d565b602492508a51917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b5091346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc93818536011261033257610df36114a0565b9260249081359267ffffffffffffffff9788851161032a578590853603011261049f578051978589018981108282111761104a578252848301358181116103265785019036602383011215610326578382013591610e50836115ef565b90610e5d85519283611437565b838252602093878584019160071b83010191368311611046578801905b828210610fe9575050508a526044610e93868801611509565b96838c01978852013594838b0191868352604435908111610fe557610ebb90369087016115c1565b959096804211610fba575050508998995151610ed681611ebb565b908b5b818110610f9757505092889492610d7892610f6497958351610f02816103f98682018095611f66565b5190209073ffffffffffffffffffffffffffffffffffffffff9a8b8b51169151928551948501957faf1b0d30d2cab0380e68f0689007e3254993c596f2fdd0aaa7f4d04f794408638752850152830152608082015260808152610d70816113ff565b51169082515192845b848110610f78578580f35b80610f918585610f8b600195875161175e565b5161199d565b01610f6d565b80610311610fac8e9f9e93610fb2945161175e565b51611fbe565b9b9a9b610ed9565b8551917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b8a80fd5b6080823603126110465785608091885161100281611398565b61100b85611509565b8152611018838601611509565b838201526110278a8601611607565b8a8201528d611037818701611607565b90820152815201910190610e7a565b8c80fd5b84896041867f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b5082346105bf576101407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576110b03661152a565b91807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c360112610332576110e2611478565b67ffffffffffffffff93906101043585811161049f5761110590369086016115c1565b90936101243596871161032a57611125610bd1966102f6983691016115c1565b969095825190611134826113ff565b606482527f5065726d69745769746e6573735472616e7366657246726f6d28546f6b656e5060208301527f65726d697373696f6e73207065726d69747465642c6164647265737320737065848301527f6e6465722c75696e74323536206e6f6e63652c75696e7432353620646561646c60608301527f696e652c0000000000000000000000000000000000000000000000000000000060808301528351948591816111e3602085018096611f93565b918237018b8152039361121c7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe095868101835282611437565b5190209261122a8651612054565b6020878101518589015195519182019687526040820192909252336060820152608081019190915260a081019390935260e43560c08401528260e081016102e1565b5082346105bf576020807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033257813567ffffffffffffffff92838211610bda5736602383011215610bda5781013592831161032e576024906007368386831b8401011161049f57865b8581106112e5578780f35b80821b83019060807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc83360301126103265761139288876001946060835161132c81611398565b611368608461133c8d8601611509565b9485845261134c60448201611509565b809785015261135d60648201611509565b809885015201611509565b918291015273ffffffffffffffffffffffffffffffffffffffff80808093169516931691166117a1565b016112da565b6080810190811067ffffffffffffffff8211176113b457604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6060810190811067ffffffffffffffff8211176113b457604052565b60a0810190811067ffffffffffffffff8211176113b457604052565b60c0810190811067ffffffffffffffff8211176113b457604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff8211176113b457604052565b60c4359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b600080fd5b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6024359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6044359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc01906080821261149b576040805190611563826113e3565b8082941261149b57805181810181811067ffffffffffffffff8211176113b457825260043573ffffffffffffffffffffffffffffffffffffffff8116810361149b578152602435602082015282526044356020830152606435910152565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020838186019501011161149b57565b67ffffffffffffffff81116113b45760051b60200190565b359065ffffffffffff8216820361149b57565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020808501948460061b01011161149b57565b91909160608184031261149b576040805191611666836113e3565b8294813567ffffffffffffffff9081811161149b57830182601f8201121561149b578035611693816115ef565b926116a087519485611437565b818452602094858086019360061b8501019381851161149b579086899897969594939201925b8484106116e3575050505050855280820135908501520135910152565b90919293949596978483031261149b578851908982019082821085831117611730578a928992845261171487611509565b81528287013583820152815201930191908897969594936116c6565b602460007f4e487b710000000000000000000000000000000000000000000000000000000081526041600452fd5b80518210156117725760209160051b010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b92919273ffffffffffffffffffffffffffffffffffffffff604060008284168152600160205282828220961695868252602052818120338252602052209485549565ffffffffffff8760a01c16804211611884575082871696838803611812575b5050611810955016926118b5565b565b878484161160001461184f57602488604051907ff96fb0710000000000000000000000000000000000000000000000000000000082526004820152fd5b7fffffffffffffffffffffffff000000000000000000000000000000000000000084846118109a031691161790553880611802565b602490604051907fd81b2f2e0000000000000000000000000000000000000000000000000000000082526004820152fd5b9060006064926020958295604051947f23b872dd0000000000000000000000000000000000000000000000000000000086526004860152602485015260448401525af13d15601f3d116001600051141617161561190e57565b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601460248201527f5452414e534645525f46524f4d5f4641494c45440000000000000000000000006044820152fd5b91908110156117725760061b0190565b3573ffffffffffffffffffffffffffffffffffffffff8116810361149b5790565b9065ffffffffffff908160608401511673ffffffffffffffffffffffffffffffffffffffff908185511694826020820151169280866040809401511695169560009187835260016020528383208984526020528383209916988983526020528282209184835460d01c03611af5579185611ace94927fc6a377bfc4eb120024a8ac08eef205be16b817020812c73223e81d1bdb9708ec98979694508715600014611ad35779ffffffffffff00000000000000000000000000000000000000009042165b60a01b167fffffffffffff00000000000000000000000000000000000000000000000000006001860160d01b1617179055519384938491604091949373ffffffffffffffffffffffffffffffffffffffff606085019616845265ffffffffffff809216602085015216910152565b0390a4565b5079ffffffffffff000000000000000000000000000000000000000087611a60565b600484517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b467f000000000000000000000000000000000000000000000000000000000000000003611b69577f000000000000000000000000000000000000000000000000000000000000000090565b60405160208101907f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a86682527f9ac997416e8ff9d2ff6bebeb7149f65cdae5e32e2b90440b566bb3044041d36a604082015246606082015230608082015260808152611bd3816113ff565b51902090565b611be1611b1e565b906040519060208201927f190100000000000000000000000000000000000000000000000000000000000084526022830152604282015260428152611bd381611398565b9192909360a435936040840151804211611cc65750602084510151808611611c955750918591610d78611c6594611c60602088015186611e47565b611bd9565b73ffffffffffffffffffffffffffffffffffffffff809151511692608435918216820361149b57611810936118b5565b602490604051907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b602490604051907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b959093958051519560409283830151804211611e175750848803611dee57611d2e918691610d7860209b611c608d88015186611e47565b60005b868110611d42575050505050505050565b611d4d81835161175e565b5188611d5a83878a61196c565b01359089810151808311611dbe575091818888886001968596611d84575b50505050505001611d31565b611db395611dad9273ffffffffffffffffffffffffffffffffffffffff6105159351169561196c565b916118b5565b803888888883611d78565b6024908651907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b600484517fff633a38000000000000000000000000000000000000000000000000000000008152fd5b6024908551907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b9073ffffffffffffffffffffffffffffffffffffffff600160ff83161b9216600052600060205260406000209060081c6000526020526040600020818154188091551615611e9157565b60046040517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b90611ec5826115ef565b611ed26040519182611437565b8281527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0611f0082946115ef565b0190602036910137565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114611f375760010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b805160208092019160005b828110611f7f575050505090565b835185529381019392810192600101611f71565b9081519160005b838110611fab575050016000815290565b8060208092840101518185015201611f9a565b60405160208101917f65626cad6cb96493bf6f5ebea28756c966f023ab9e8a83a7101849d5573b3678835273ffffffffffffffffffffffffffffffffffffffff8082511660408401526020820151166060830152606065ffffffffffff9182604082015116608085015201511660a082015260a0815260c0810181811067ffffffffffffffff8211176113b45760405251902090565b6040516020808201927f618358ac3db8dc274f0cd8829da7e234bd48cd73c4a740aede1adec9846d06a1845273ffffffffffffffffffffffffffffffffffffffff81511660408401520151606082015260608152611bd381611398565b919082604091031261149b576020823592013590565b6000843b61222e5750604182036121ac576120e4828201826120b1565b939092604010156117725760209360009360ff6040608095013560f81c5b60405194855216868401526040830152606082015282805260015afa156121a05773ffffffffffffffffffffffffffffffffffffffff806000511691821561217657160361214c57565b60046040517f815e1d64000000000000000000000000000000000000000000000000000000008152fd5b60046040517f8baa579f000000000000000000000000000000000000000000000000000000008152fd5b6040513d6000823e3d90fd5b60408203612204576121c0918101906120b1565b91601b7f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff84169360ff1c019060ff8211611f375760209360009360ff608094612102565b60046040517f4be6321b000000000000000000000000000000000000000000000000000000008152fd5b929391601f928173ffffffffffffffffffffffffffffffffffffffff60646020957fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0604051988997889687947f1626ba7e000000000000000000000000000000000000000000000000000000009e8f8752600487015260406024870152816044870152868601378b85828601015201168101030192165afa9081156123a857829161232a575b507fffffffff000000000000000000000000000000000000000000000000000000009150160361230057565b60046040517fb0669cbc000000000000000000000000000000000000000000000000000000008152fd5b90506020813d82116123a0575b8161234460209383611437565b810103126103365751907fffffffff0000000000000000000000000000000000000000000000000000000082168203610a9a57507fffffffff0000000000000000000000000000000000000000000000000000000090386122d4565b3d9150612337565b6040513d84823e3d90fdfea164736f6c6343000811000a'; + + (bool ok,) = permit2Deployer.call(permit2CreationCalldata); + require(ok && PERMIT2.code.length != 0, 'permit2 deployment failed'); + } + function _idsAndAmounts(address token, uint256 amount) internal view returns (uint256[2][] memory arr) { arr = new uint256[2][](1); arr[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), token); @@ -84,6 +173,197 @@ contract HybridAllocatorTest is Test, TestHelper { arr[1][1] = amountB; } + /* ====================================================================== */ + /* Permit2 Helper Functions */ + /* ====================================================================== */ + + function _getLockTag() internal view returns (bytes12) { + return _toLockTag(address(allocator), Scope.Multichain, ResetPeriod.TenMinutes); + } + + function _getPermit2DomainSeparator() internal view returns (bytes32) { + return keccak256( + abi.encode(PERMIT2_DOMAIN_SEPARATOR_TYPEHASH, keccak256(bytes('Permit2')), block.chainid, PERMIT2) + ); + } + + function _createTokenPermissions(address token, uint256 amount) + internal + pure + returns (ISignatureTransfer.TokenPermissions[] memory) + { + ISignatureTransfer.TokenPermissions[] memory permitted = new ISignatureTransfer.TokenPermissions[](1); + permitted[0] = ISignatureTransfer.TokenPermissions({token: token, amount: amount}); + return permitted; + } + + function _createTokenPermissions2(address token1, uint256 amount1, address token2, uint256 amount2) + internal + pure + returns (ISignatureTransfer.TokenPermissions[] memory) + { + ISignatureTransfer.TokenPermissions[] memory permitted = new ISignatureTransfer.TokenPermissions[](2); + permitted[0] = ISignatureTransfer.TokenPermissions({token: token1, amount: amount1}); + permitted[1] = ISignatureTransfer.TokenPermissions({token: token2, amount: amount2}); + return permitted; + } + + function _createDepositDetails(uint256 nonce, uint256 deadline, bytes12 lockTag) + internal + pure + returns (DepositDetails memory) + { + return DepositDetails({nonce: nonce, deadline: deadline, lockTag: lockTag}); + } + + function _hashTokenPermissions(ISignatureTransfer.TokenPermissions[] memory permitted) + internal + pure + returns (bytes32) + { + bytes32[] memory hashes = new bytes32[](permitted.length); + for (uint256 i = 0; i < permitted.length; i++) { + hashes[i] = keccak256(abi.encode(TOKEN_PERMISSIONS_TYPEHASH, permitted[i].token, permitted[i].amount)); + } + return keccak256(abi.encodePacked(hashes)); + } + + function _createIds(bytes12 lockTag, address[] memory tokens) internal pure returns (uint256[] memory) { + uint256[] memory ids = new uint256[](tokens.length); + for (uint256 i = 0; i < tokens.length; i++) { + ids[i] = AllocatorLib.toId(lockTag, tokens[i]); + } + return ids; + } + + /// @dev Computes the Lock commitment hash with proper struct encoding + function _computeCommitmentHash(uint256 id, uint256 amount) internal pure returns (bytes32) { + bytes32 lockTypehash = keccak256('Lock(bytes12 lockTag,address token,uint256 amount)'); + bytes12 lockTag = bytes12(bytes32(id)); + address token = address(uint160(id)); + return keccak256(abi.encode(lockTypehash, lockTag, token, amount)); + } + + /// @dev Creates a permit2 nonce with the correct command byte and sponsor address + function _createPermit2Nonce(address sponsor, uint88 freeNonce) internal pure returns (uint256) { + // First byte: PERMIT2_NONCE (0x03) + // Next 20 bytes: sponsor address + // Last 11 bytes: free nonce + return uint256(0x03) << 248 | uint256(uint160(sponsor)) << 88 | uint256(freeNonce); + } + + function _createPermit2Signature( + ISignatureTransfer.TokenPermissions[] memory permitted, + DepositDetails memory details, + bytes32 claimHash, + uint256 signerPk + ) internal view returns (bytes memory) { + bytes12 lockTag = details.lockTag; + + // Check if first token is native + bool hasNative = permitted.length > 0 && permitted[0].token == address(0); + + // Create ids array - includes ALL tokens (including native) + uint256[] memory ids = new uint256[](permitted.length); + for (uint256 i = 0; i < permitted.length; i++) { + ids[i] = AllocatorLib.toId(lockTag, permitted[i].token); + } + bytes32 idsHash = keccak256(abi.encodePacked(ids)); + + // Create activation hash using the exact same typehash TheCompact uses + // The activator is the allocator (msg.sender to TheCompact) + bytes32 activationHash = + keccak256(abi.encode(BATCH_COMPACT_BATCH_ACTIVATION_TYPEHASH, address(allocator), idsHash, claimHash)); + + // Create permit batch hash - tokenPermissionsHash only includes ERC20 tokens (not native) + ISignatureTransfer.TokenPermissions[] memory erc20Permitted; + if (hasNative) { + erc20Permitted = new ISignatureTransfer.TokenPermissions[](permitted.length - 1); + for (uint256 i = 0; i < erc20Permitted.length; i++) { + erc20Permitted[i] = permitted[i + 1]; + } + } else { + erc20Permitted = permitted; + } + bytes32 tokenPermissionsHash = _hashTokenPermissions(erc20Permitted); + bytes32 permitBatchHash = keccak256( + abi.encode( + PERMIT_BATCH_WITNESS_TYPEHASH, + tokenPermissionsHash, + address(compact), + details.nonce, + details.deadline, + activationHash + ) + ); + + // Create digest + bytes32 domainSeparator = _getPermit2DomainSeparator(); + bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), domainSeparator, permitBatchHash)); + + // Sign + (bytes32 r, bytes32 vs) = vm.signCompact(signerPk, digest); + return abi.encodePacked(r, vs); + } + + /// @dev Creates a Permit2 signature for allocations WITH a witness/mandate + function _createPermit2SignatureWithWitness( + ISignatureTransfer.TokenPermissions[] memory permitted, + DepositDetails memory details, + bytes32 claimHash, + uint256 signerPk + ) internal view returns (bytes memory) { + bytes12 lockTag = details.lockTag; + + // Check if first token is native + bool hasNative = permitted.length > 0 && permitted[0].token == address(0); + + // Create ids array - includes ALL tokens (including native) + uint256[] memory ids = new uint256[](permitted.length); + for (uint256 i = 0; i < permitted.length; i++) { + ids[i] = AllocatorLib.toId(lockTag, permitted[i].token); + } + bytes32 idsHash = keccak256(abi.encodePacked(ids)); + + // Create activation hash using the WITH_WITNESS typehash since we have a witness + // The activator is the allocator (msg.sender to TheCompact) + bytes32 activationHash = keccak256( + abi.encode(BATCH_COMPACT_BATCH_ACTIVATION_WITH_WITNESS_TYPEHASH, address(allocator), idsHash, claimHash) + ); + + // Create permit batch hash - tokenPermissionsHash only includes ERC20 tokens (not native) + ISignatureTransfer.TokenPermissions[] memory erc20Permitted; + if (hasNative) { + erc20Permitted = new ISignatureTransfer.TokenPermissions[](permitted.length - 1); + for (uint256 i = 0; i < erc20Permitted.length; i++) { + erc20Permitted[i] = permitted[i + 1]; + } + } else { + erc20Permitted = permitted; + } + bytes32 tokenPermissionsHash = _hashTokenPermissions(erc20Permitted); + + // Use the WITH MANDATE typehash since we have a witness + bytes32 permitBatchHash = keccak256( + abi.encode( + PERMIT_BATCH_WITNESS_WITH_MANDATE_TYPEHASH, + tokenPermissionsHash, + address(compact), + details.nonce, + details.deadline, + activationHash + ) + ); + + // Create digest + bytes32 domainSeparator = _getPermit2DomainSeparator(); + bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), domainSeparator, permitBatchHash)); + + // Sign + (bytes32 r, bytes32 vs) = vm.signCompact(signerPk, digest); + return abi.encodePacked(r, vs); + } + function test_constructor_revert_signerIsAddressZero() public { vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.InvalidSigner.selector)); new HybridAllocator(address(0)); @@ -125,12 +405,14 @@ contract HybridAllocatorTest is Test, TestHelper { function test_prepareAllocation_returnsNonce_andDoesNotIncrement() public { uint256[2][] memory idsAndAmounts = _idsAndAmounts(address(usdc), defaultAmount); - uint96 beforeNonces = allocator.nonces(); + uint88 beforeNonces = allocator.nonces(); // call prepare directly uint256 returnedNonce = allocator.prepareAllocation( user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0), '' ); - assertEq(returnedNonce, uint256(beforeNonces) + 1); + // HybridAllocator passes just the counter to AL.prepareAllocation, so nonce is: command | counter + // (address(0) is used because HybridAllocator doesn't embed recipient in nonce pre-command) + assertEq(returnedNonce, _composeNonceUint(ON_CHAIN_NONCE, address(0), uint256(beforeNonces) + 1)); // storage not incremented yet assertEq(allocator.nonces(), beforeNonces); } @@ -154,12 +436,13 @@ contract HybridAllocatorTest is Test, TestHelper { assertEq(allocator.nonces(), 1); // derive claim hash and ensure isClaimAuthorized is true + // HybridAllocator uses command | counter for nonce (no recipient embedded) Lock[] memory commitments = _idsAndAmountsToCommitments(idsAndAmounts); bytes32 claimHash = _toBatchCompactHash( BatchCompact({ arbiter: arbiter, sponsor: user, - nonce: allocator.nonces(), + nonce: _composeNonceUint(ON_CHAIN_NONCE, address(0), allocator.nonces()), expires: defaultExpiration, commitments: commitments }) @@ -193,7 +476,8 @@ contract HybridAllocatorTest is Test, TestHelper { // Compute expected claim hash for the deposit-only path Lock[] memory commitments = _idsAndAmountsToCommitments(idsAndAmounts); - uint256 expectedNonce = uint256(allocator.nonces()) + 1; // prepare will use this + // HybridAllocator uses command | counter for nonce (no recipient embedded) + uint256 expectedNonce = _composeNonceUint(ON_CHAIN_NONCE, address(0), uint256(allocator.nonces()) + 1); bytes32 expectedClaimHash = _toBatchCompactHash( BatchCompact({ arbiter: arbiter, @@ -354,7 +638,7 @@ contract HybridAllocatorTest is Test, TestHelper { assertEq(registeredAmounts.length, 1); assertEq(address(compact).balance, defaultAmount); assertEq(compact.balanceOf(address(user), idsAndAmounts[0][0]), defaultAmount); - assertEq(nonce, 1); + assertEq(nonce, _composeNonceUint(ON_CHAIN_NONCE, address(0), 1)); } function test_allocateAndRegister_success_erc20Token() public { @@ -376,7 +660,7 @@ contract HybridAllocatorTest is Test, TestHelper { assertEq(registeredAmounts[0], defaultAmount); assertEq(usdc.balanceOf(address(compact)), defaultAmount); assertEq(compact.balanceOf(address(user), idsAndAmounts[0][0]), defaultAmount); - assertEq(nonce, 1); + assertEq(nonce, _composeNonceUint(ON_CHAIN_NONCE, address(0), 1)); } function test_allocateAndRegister_success_nativeTokenWithEmptyAmountInput() public { @@ -394,7 +678,7 @@ contract HybridAllocatorTest is Test, TestHelper { assertEq(registeredAmounts[0], defaultAmount); assertEq(address(compact).balance, defaultAmount); assertEq(compact.balanceOf(address(user), idsAndAmounts[0][0]), defaultAmount); - assertEq(nonce, 1); + assertEq(nonce, _composeNonceUint(ON_CHAIN_NONCE, address(0), 1)); } function test_allocateAndRegister_success_erc20TokenWithEmptyAmountInput() public { @@ -417,7 +701,7 @@ contract HybridAllocatorTest is Test, TestHelper { assertEq(registeredAmounts.length, 1); assertEq(usdc.balanceOf(address(compact)), defaultAmount); assertEq(compact.balanceOf(address(user), idsAndAmounts[0][0]), defaultAmount); - assertEq(nonce, 1); + assertEq(nonce, _composeNonceUint(ON_CHAIN_NONCE, address(0), 1)); } function test_allocateAndRegister_success_multipleTokens() public { @@ -447,7 +731,7 @@ contract HybridAllocatorTest is Test, TestHelper { assertEq(address(compact).balance, defaultAmount); assertEq(compact.balanceOf(address(user), idsAndAmounts[0][0]), defaultAmount); assertEq(compact.balanceOf(address(user), idsAndAmounts[1][0]), defaultAmount); - assertEq(nonce, 1); + assertEq(nonce, _composeNonceUint(ON_CHAIN_NONCE, address(0), 1)); } function test_allocateAndRegister_checkNonceIncrements_nativeToken() public { @@ -692,7 +976,7 @@ contract HybridAllocatorTest is Test, TestHelper { uint256 id = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); address target = makeAddr('target'); - assertEq(usdc.balanceOf(user), defaultAmount); + assertEq(usdc.balanceOf(user), 10 ether); // setUp mints 10 ether vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.Unsupported.selector)); allocator.attest(signer, user, target, id, defaultAmount); @@ -702,7 +986,7 @@ contract HybridAllocatorTest is Test, TestHelper { uint256 id = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); address target = makeAddr('target'); - assertEq(usdc.balanceOf(user), defaultAmount); + assertEq(usdc.balanceOf(user), 10 ether); // setUp mints 10 ether vm.startPrank(user); usdc.approve(address(compact), defaultAmount); compact.depositERC20(address(usdc), bytes12(bytes32(id)), defaultAmount, user); @@ -736,7 +1020,10 @@ contract HybridAllocatorTest is Test, TestHelper { allocator.authorizeClaim(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), ''); } - function test_revert_authorizeClaim_InvalidSignature(uint128 nonce) public { + function test_revert_authorizeClaim_InvalidSignature(uint88 freeNonce) public { + // Compose the full nonce with OFF_CHAIN_NONCE command + sponsor address + uint256 nonce = _composeNonceUint(OFF_CHAIN_NONCE, user, freeNonce); + uint256[2][] memory idsAndAmounts = new uint256[2][](2); idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); idsAndAmounts[0][1] = defaultAmount; @@ -940,11 +1227,16 @@ contract HybridAllocatorTest is Test, TestHelper { assertEq(returnedClaimHash, claimHash); } + // User started with 10 ether ETH and 10 ether USDC, provided defaultAmount of each assertEq(usdc.balanceOf(address(compact)), 0, 'compact usdc balance should be 0'); - assertEq(usdc.balanceOf(address(user)), 0, 'user usdc balance should be 0'); + assertEq( + usdc.balanceOf(address(user)), + 10 ether - defaultAmount, + 'user usdc balance should be 10 ether - defaultAmount' + ); assertEq(usdc.balanceOf(address(target)), defaultAmount, 'target usdc balance should be defaultAmount'); assertEq(address(compact).balance, 0, 'compact balance should be 0'); - assertEq(address(user).balance, 0, 'user balance should be 0'); + assertEq(address(user).balance, 10 ether - defaultAmount, 'user balance should be 10 ether - defaultAmount'); assertEq(address(target).balance, defaultAmount, 'target balance should be defaultAmount'); assertEq(compact.balanceOf(address(user), idsAndAmounts[0][0]), 0, 'user eth compact balance of 0 should be 0'); assertEq( @@ -956,7 +1248,10 @@ contract HybridAllocatorTest is Test, TestHelper { ); } - function test_authorizeClaim_success_offChain(uint128 nonce) public { + function test_authorizeClaim_success_offChain(uint88 freeNonce) public { + // Compose the full nonce with OFF_CHAIN_NONCE command + sponsor address + uint256 nonce = _composeNonceUint(OFF_CHAIN_NONCE, user, freeNonce); + uint256[2][] memory idsAndAmounts = new uint256[2][](2); idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); idsAndAmounts[0][1] = defaultAmount; @@ -1027,10 +1322,16 @@ contract HybridAllocatorTest is Test, TestHelper { assertEq(returnedClaimHash, claimHash); assertEq(usdc.balanceOf(address(compact)), 0, 'compact usdc balance should be 0'); - assertEq(usdc.balanceOf(address(user)), 0, 'user usdc balance should be 0'); + // User started with 10 ether USDC (setUp), deposited defaultAmount, so has 10 ether - defaultAmount remaining + assertEq( + usdc.balanceOf(address(user)), + 10 ether - defaultAmount, + 'user usdc balance should be 10 ether - defaultAmount' + ); assertEq(usdc.balanceOf(address(target)), defaultAmount, 'target usdc balance should be defaultAmount'); assertEq(address(compact).balance, 0, 'compact balance should be 0'); - assertEq(address(user).balance, 0, 'user balance should be 0'); + // User started with 10 ether ETH (setUp), deposited defaultAmount, so has 10 ether - defaultAmount remaining + assertEq(address(user).balance, 10 ether - defaultAmount, 'user balance should be 10 ether - defaultAmount'); assertEq(address(target).balance, defaultAmount, 'target balance should be defaultAmount'); assertEq(compact.balanceOf(address(user), idsAndAmounts[0][0]), 0, 'user eth compact balance of 0 should be 0'); assertEq( @@ -1275,4 +1576,348 @@ contract HybridAllocatorTest is Test, TestHelper { HybridAllocator newAllocator = HybridAllocator(deployed); assertEq(newAllocator.ALLOCATOR_ID(), _toAllocatorId(deployed)); } + + /* ====================================================================== */ + /* Tests for permit2Allocation */ + /* ====================================================================== */ + + function test_permit2Allocation_singleERC20() public { + bytes12 lockTag = _getLockTag(); + uint88 freeNonce = 1; + uint256 nonce = _createPermit2Nonce(user, freeNonce); + + // Prepare token permissions + ISignatureTransfer.TokenPermissions[] memory permitted = _createTokenPermissions(address(usdc), defaultAmount); + + // Prepare deposit details + DepositDetails memory details = _createDepositDetails(nonce, defaultExpiration, lockTag); + + // Compute ids + uint256[] memory ids = new uint256[](1); + ids[0] = AllocatorLib.toId(lockTag, address(usdc)); + + // Compute commitment hashes + bytes32[] memory commitmentHashes = new bytes32[](1); + commitmentHashes[0] = _computeCommitmentHash(ids[0], defaultAmount); + + // Compute claimHash (no witness) + bytes32 claimHash = keccak256( + abi.encode( + BATCH_COMPACT_TYPEHASH, + arbiter, + user, + nonce, + defaultExpiration, + keccak256(abi.encodePacked(commitmentHashes)) + ) + ); + + // Create Permit2 signature + bytes memory signature = _createPermit2Signature(permitted, details, claimHash, userPrivateKey); + + // Execute + Lock[] memory commitments = allocator.permit2Allocation(user, permitted, details, claimHash, '', signature); + vm.snapshotGasLastCall('hybrid_permit2Allocation_singleERC20'); + + // Verify commitments + assertEq(commitments.length, 1); + assertEq(commitments[0].lockTag, lockTag); + assertEq(commitments[0].token, address(usdc)); + assertEq(commitments[0].amount, defaultAmount); + + // Verify claim is authorized + assertTrue(allocator.isClaimAuthorized(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); + + // Verify tokens are in compact + assertEq(usdc.balanceOf(address(compact)), defaultAmount); + assertEq(compact.balanceOf(user, ids[0]), defaultAmount); + } + + function test_permit2Allocation_singleERC20_withWitness() public { + bytes12 lockTag = _getLockTag(); + uint88 freeNonce = 1; + uint256 nonce = _createPermit2Nonce(user, freeNonce); + + // Prepare token permissions + ISignatureTransfer.TokenPermissions[] memory permitted = _createTokenPermissions(address(usdc), defaultAmount); + + // Prepare deposit details + DepositDetails memory details = _createDepositDetails(nonce, defaultExpiration, lockTag); + + // Compute ids + uint256[] memory ids = new uint256[](1); + ids[0] = AllocatorLib.toId(lockTag, address(usdc)); + + // Compute commitment hashes + bytes32[] memory commitmentHashes = new bytes32[](1); + commitmentHashes[0] = _computeCommitmentHash(ids[0], defaultAmount); + + // Create a witness value - using a simple Mandate(uint256 witness) struct + // The witness is the hash of the witness struct: keccak256(abi.encode(WITNESS_TYPEHASH, witnessValue)) + uint256 witnessValue = 12345; + bytes32 witness = keccak256(abi.encode(WITNESS_TYPEHASH, witnessValue)); + + // Compute claimHash WITH witness using BATCH_COMPACT_TYPEHASH_WITH_WITNESS + bytes32 claimHash = keccak256( + abi.encode( + BATCH_COMPACT_TYPEHASH_WITH_WITNESS, + arbiter, + user, + nonce, + defaultExpiration, + keccak256(abi.encodePacked(commitmentHashes)), + witness + ) + ); + + // Create Permit2 signature using the WITH_WITNESS helper + // This uses PERMIT_BATCH_WITNESS_WITH_MANDATE_TYPEHASH since we have a witness + bytes memory signature = _createPermit2SignatureWithWitness(permitted, details, claimHash, userPrivateKey); + + // Execute with witness typestring + // Note: The witness parameter should be just the inner content (e.g., "uint256 witness"), + // not the full struct definition, as TheCompact wraps it in "Mandate(...)" + Lock[] memory commitments = + allocator.permit2Allocation(user, permitted, details, claimHash, WITNESS_STRING, signature); + vm.snapshotGasLastCall('hybrid_permit2Allocation_singleERC20_withWitness'); + + // Verify commitments + assertEq(commitments.length, 1); + assertEq(commitments[0].lockTag, lockTag); + assertEq(commitments[0].token, address(usdc)); + assertEq(commitments[0].amount, defaultAmount); + + // Verify claim is authorized + assertTrue(allocator.isClaimAuthorized(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); + + // Verify tokens are in compact + assertEq(usdc.balanceOf(address(compact)), defaultAmount); + assertEq(compact.balanceOf(user, ids[0]), defaultAmount); + } + + function test_permit2Allocation_multipleERC20() public { + bytes12 lockTag = _getLockTag(); + uint88 freeNonce = 1; + uint256 nonce = _createPermit2Nonce(user, freeNonce); + uint256 amount1 = defaultAmount; + uint256 amount2 = defaultAmount / 2; + + // Prepare token permissions (sorted by address for Permit2) + ISignatureTransfer.TokenPermissions[] memory permitted; + uint256[] memory ids = new uint256[](2); + bytes32[] memory commitmentHashes = new bytes32[](2); + + if (uint160(address(usdc)) < uint160(address(dai))) { + permitted = _createTokenPermissions2(address(usdc), amount1, address(dai), amount2); + ids[0] = AllocatorLib.toId(lockTag, address(usdc)); + ids[1] = AllocatorLib.toId(lockTag, address(dai)); + commitmentHashes[0] = _computeCommitmentHash(ids[0], amount1); + commitmentHashes[1] = _computeCommitmentHash(ids[1], amount2); + } else { + permitted = _createTokenPermissions2(address(dai), amount2, address(usdc), amount1); + ids[0] = AllocatorLib.toId(lockTag, address(dai)); + ids[1] = AllocatorLib.toId(lockTag, address(usdc)); + commitmentHashes[0] = _computeCommitmentHash(ids[0], amount2); + commitmentHashes[1] = _computeCommitmentHash(ids[1], amount1); + } + + // Prepare deposit details + DepositDetails memory details = _createDepositDetails(nonce, defaultExpiration, lockTag); + + // Compute claimHash (no witness) + bytes32 claimHash = keccak256( + abi.encode( + BATCH_COMPACT_TYPEHASH, + arbiter, + user, + nonce, + defaultExpiration, + keccak256(abi.encodePacked(commitmentHashes)) + ) + ); + + // Create Permit2 signature + bytes memory signature = _createPermit2Signature(permitted, details, claimHash, userPrivateKey); + + // Execute + Lock[] memory commitments = allocator.permit2Allocation(user, permitted, details, claimHash, '', signature); + vm.snapshotGasLastCall('hybrid_permit2Allocation_multipleERC20'); + + // Verify commitments + assertEq(commitments.length, 2); + + // Verify claim is authorized + assertTrue(allocator.isClaimAuthorized(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); + + // Verify tokens are in compact + assertEq(usdc.balanceOf(address(compact)), amount1); + assertEq(dai.balanceOf(address(compact)), amount2); + } + + function test_permit2Allocation_revert_invalidNonceCommand() public { + bytes12 lockTag = _getLockTag(); + // Use ON_CHAIN_NONCE command (0x01) instead of PERMIT2_NONCE (0x03) + uint256 invalidNonce = uint256(0x01) << 248 | uint256(uint160(user)) << 88 | uint256(1); + + // Prepare token permissions + ISignatureTransfer.TokenPermissions[] memory permitted = _createTokenPermissions(address(usdc), defaultAmount); + + // Prepare deposit details with invalid nonce + DepositDetails memory details = _createDepositDetails(invalidNonce, defaultExpiration, lockTag); + + // Compute ids and commitment hashes (these are just for signature computation) + uint256[] memory ids = new uint256[](1); + ids[0] = AllocatorLib.toId(lockTag, address(usdc)); + bytes32[] memory commitmentHashes = new bytes32[](1); + commitmentHashes[0] = _computeCommitmentHash(ids[0], defaultAmount); + + bytes32 claimHash = keccak256( + abi.encode( + BATCH_COMPACT_TYPEHASH, + arbiter, + user, + invalidNonce, + defaultExpiration, + keccak256(abi.encodePacked(commitmentHashes)) + ) + ); + + bytes memory signature = _createPermit2Signature(permitted, details, claimHash, userPrivateKey); + + // Should revert with UnauthorizedNonce because command is not PERMIT2_NONCE + vm.expectRevert(abi.encodeWithSelector(AllocatorLib.UnauthorizedNonce.selector, bytes1(0x01), user)); + allocator.permit2Allocation(user, permitted, details, claimHash, '', signature); + } + + function test_permit2Allocation_revert_invalidNonceSponsor() public { + bytes12 lockTag = _getLockTag(); + address wrongSponsor = makeAddr('wrongSponsor'); + // Use correct command but wrong sponsor address in nonce + uint256 invalidNonce = _createPermit2Nonce(wrongSponsor, 1); + + // Prepare token permissions + ISignatureTransfer.TokenPermissions[] memory permitted = _createTokenPermissions(address(usdc), defaultAmount); + + // Prepare deposit details with nonce that has wrong sponsor + DepositDetails memory details = _createDepositDetails(invalidNonce, defaultExpiration, lockTag); + + bytes32 claimHash = bytes32(uint256(1)); // dummy claim hash + + // Signature won't matter because nonce verification happens first + bytes memory signature = new bytes(64); + + // Should revert because sponsor in nonce doesn't match depositor + vm.expectRevert(abi.encodeWithSelector(AllocatorLib.UnauthorizedNonce.selector, bytes1(0x03), wrongSponsor)); + allocator.permit2Allocation(user, permitted, details, claimHash, '', signature); + } + + function test_permit2Allocation_emitsAllocatedEvent() public { + bytes12 lockTag = _getLockTag(); + uint88 freeNonce = 1; + uint256 nonce = _createPermit2Nonce(user, freeNonce); + + // Prepare token permissions + ISignatureTransfer.TokenPermissions[] memory permitted = _createTokenPermissions(address(usdc), defaultAmount); + + // Prepare deposit details + DepositDetails memory details = _createDepositDetails(nonce, defaultExpiration, lockTag); + + // Compute ids and claimHash + uint256[] memory ids = new uint256[](1); + ids[0] = AllocatorLib.toId(lockTag, address(usdc)); + bytes32[] memory commitmentHashes = new bytes32[](1); + commitmentHashes[0] = _computeCommitmentHash(ids[0], defaultAmount); + + bytes32 claimHash = keccak256( + abi.encode( + BATCH_COMPACT_TYPEHASH, + arbiter, + user, + nonce, + defaultExpiration, + keccak256(abi.encodePacked(commitmentHashes)) + ) + ); + + bytes memory signature = _createPermit2Signature(permitted, details, claimHash, userPrivateKey); + + // Expect Allocated event + Lock[] memory expectedCommitments = new Lock[](1); + expectedCommitments[0] = Lock({lockTag: lockTag, token: address(usdc), amount: defaultAmount}); + + vm.expectEmit(true, true, true, true); + emit IOnChainAllocation.Allocated(user, expectedCommitments, nonce, defaultExpiration, claimHash); + + allocator.permit2Allocation(user, permitted, details, claimHash, '', signature); + } + + function test_permit2Allocation_fullClaimFlow() public { + bytes12 lockTag = _getLockTag(); + uint88 freeNonce = 1; + uint256 nonce = _createPermit2Nonce(user, freeNonce); + + // Prepare token permissions + ISignatureTransfer.TokenPermissions[] memory permitted = _createTokenPermissions(address(usdc), defaultAmount); + + // Prepare deposit details + DepositDetails memory details = _createDepositDetails(nonce, defaultExpiration, lockTag); + + // Compute ids + uint256 id = AllocatorLib.toId(lockTag, address(usdc)); + bytes32[] memory commitmentHashes = new bytes32[](1); + commitmentHashes[0] = _computeCommitmentHash(id, defaultAmount); + + // Compute claimHash (no witness) + bytes32 claimHash = keccak256( + abi.encode( + BATCH_COMPACT_TYPEHASH, + arbiter, + user, + nonce, + defaultExpiration, + keccak256(abi.encodePacked(commitmentHashes)) + ) + ); + + // Create Permit2 signature + bytes memory signature = _createPermit2Signature(permitted, details, claimHash, userPrivateKey); + + // Execute permit2Allocation + allocator.permit2Allocation(user, permitted, details, claimHash, '', signature); + + // Verify claim is authorized + assertTrue(allocator.isClaimAuthorized(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); + + // Now execute the claim + address recipient = makeAddr('recipient'); + Component[] memory portions = new Component[](1); + portions[0] = + Component({claimant: uint256(bytes32(abi.encodePacked(bytes12(0), recipient))), amount: defaultAmount}); + + BatchClaimComponent[] memory claimComponents = new BatchClaimComponent[](1); + claimComponents[0] = BatchClaimComponent({id: id, allocatedAmount: defaultAmount, portions: portions}); + + BatchClaim memory claim = BatchClaim({ + allocatorData: '', + sponsorSignature: '', + sponsor: user, + nonce: nonce, + expires: details.deadline, + witness: bytes32(0), + witnessTypestring: '', + claims: claimComponents + }); + + // Execute claim + vm.prank(arbiter); + bytes32 returnedClaimHash = compact.batchClaim(claim); + assertEq(returnedClaimHash, claimHash); + + // Verify tokens transferred + assertEq(usdc.balanceOf(recipient), defaultAmount); + assertEq(usdc.balanceOf(address(compact)), 0); + + // Verify claim is no longer authorized (deleted after execution) + assertFalse(allocator.isClaimAuthorized(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); + } } diff --git a/test/HybridERC7683.t.sol b/test/HybridERC7683.t.sol index e1ce98b..be5323a 100644 --- a/test/HybridERC7683.t.sol +++ b/test/HybridERC7683.t.sol @@ -59,7 +59,8 @@ contract MockAllocator is GaslessCrossChainOrderData, OnChainCrossChainOrderData assertEq(address(compactContract_), address(0x00000000000000171ede64904551eeDF3C6C9788)); hybridERC7683Allocator = new HybridERC7683(signer); - _setUp(address(hybridERC7683Allocator), compactContract_, 1 /* defaultNonce */ ); + // HybridAllocator uses simplified nonce: command | counter (no address embedded) + _setUp(address(hybridERC7683Allocator), compactContract_, _composeNonceUint(ON_CHAIN_NONCE, address(0), 1)); super.setUp(); } } @@ -408,6 +409,8 @@ contract HybridERC7683_authorizeClaim is MockAllocator { usdc.approve(address(compactContract), defaultAmount); BatchCompact memory compact_ = _getCompact(); + // Off-chain signature tests need OFF_CHAIN_NONCE format + compact_.nonce = _composeNonceUint(OFF_CHAIN_NONCE, user, 1); Mandate memory mandate_ = _getMandate(); bytes32 claimHash = _deriveClaimHash(compact_, mandate_); @@ -434,7 +437,7 @@ contract HybridERC7683_authorizeClaim is MockAllocator { allocatorData: allocatorSignature, sponsorSignature: '', sponsor: user, - nonce: defaultNonce, + nonce: compact_.nonce, expires: compact_.expires, witness: mandateHash, witnessTypestring: WITNESS_TYPESTRING_TRIBUNAL, @@ -451,6 +454,8 @@ contract HybridERC7683_authorizeClaim is MockAllocator { usdc.approve(address(compactContract), defaultAmount); BatchCompact memory compact_ = _getCompact(); + // Off-chain signature tests need OFF_CHAIN_NONCE format + compact_.nonce = _composeNonceUint(OFF_CHAIN_NONCE, user, 1); Mandate memory mandate_ = _getMandate(); bytes32 claimHash = _deriveClaimHash(compact_, mandate_); @@ -477,7 +482,7 @@ contract HybridERC7683_authorizeClaim is MockAllocator { allocatorData: allocatorSignature, // signed by attacker sponsorSignature: '', sponsor: user, - nonce: defaultNonce, + nonce: compact_.nonce, expires: compact_.expires, witness: mandateHash, witnessTypestring: WITNESS_TYPESTRING_TRIBUNAL, @@ -495,6 +500,8 @@ contract HybridERC7683_authorizeClaim is MockAllocator { usdc.approve(address(compactContract), defaultAmount); BatchCompact memory compact_ = _getCompact(); + // Off-chain signature tests need OFF_CHAIN_NONCE format + compact_.nonce = _composeNonceUint(OFF_CHAIN_NONCE, user, 1); Mandate memory mandate_ = _getMandate(); bytes32 claimHash = _deriveClaimHash(compact_, mandate_); @@ -522,7 +529,7 @@ contract HybridERC7683_authorizeClaim is MockAllocator { allocatorData: allocatorSignature, // allocator signature with a length of 66 bytes sponsorSignature: '', sponsor: user, - nonce: defaultNonce, + nonce: compact_.nonce, expires: compact_.expires, witness: mandateHash, witnessTypestring: WITNESS_TYPESTRING_TRIBUNAL, @@ -739,7 +746,7 @@ contract HybridERC7683_resolveFor is MockAllocator { originChainId: block.chainid, openDeadline: uint32(compact_.expires), fillDeadline: uint32(mandate_.fills[0].expires), - orderId: bytes32(defaultNonce), + orderId: _composeNonce(ON_CHAIN_NONCE, address(0), defaultNonce), maxSpent: maxSpent, minReceived: minReceived, fillInstructions: fillInstructions @@ -966,7 +973,7 @@ contract HybridERC7683_resolve is MockAllocator { originChainId: block.chainid, openDeadline: uint32(compact_.expires), fillDeadline: uint32(mandate_.fills[0].expires), - orderId: bytes32(defaultNonce), + orderId: _composeNonce(ON_CHAIN_NONCE, address(0), defaultNonce), maxSpent: maxSpent, minReceived: minReceived, fillInstructions: fillInstructions @@ -1035,6 +1042,6 @@ contract HybridERC7683_hybridAllocatorInheritance is MockAllocator { assertEq(registeredAmounts[0], defaultAmount); assertEq(usdc.balanceOf(address(compactContract)), defaultAmount); assertEq(compactContract.balanceOf(address(user), idsAndAmounts[0][0]), defaultAmount); - assertEq(nonce, 1); + assertEq(nonce, _composeNonceUint(ON_CHAIN_NONCE, address(0), 1)); } } diff --git a/test/OnChainAllocator.t.sol b/test/OnChainAllocator.t.sol index c822c86..8ff93b8 100644 --- a/test/OnChainAllocator.t.sol +++ b/test/OnChainAllocator.t.sol @@ -9,12 +9,25 @@ import {TheCompact} from '@uniswap/the-compact/TheCompact.sol'; import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; +import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; +import {ISignatureTransfer} from 'permit2/src/interfaces/ISignatureTransfer.sol'; import {IOnChainAllocation} from '@uniswap/the-compact/interfaces/IOnChainAllocation.sol'; import {OnChainAllocator} from 'src/allocators/OnChainAllocator.sol'; import {IOnChainAllocator} from 'src/interfaces/IOnChainAllocator.sol'; -import {BATCH_COMPACT_TYPEHASH, LOCK_TYPEHASH, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; +import {DepositDetails} from '@uniswap/the-compact/types/DepositDetails.sol'; +import { + BATCH_COMPACT_TYPEHASH, + BATCH_COMPACT_TYPESTRING_FRAGMENT_FIVE, + BATCH_COMPACT_TYPESTRING_FRAGMENT_FOUR, + BATCH_COMPACT_TYPESTRING_FRAGMENT_ONE, + BATCH_COMPACT_TYPESTRING_FRAGMENT_SIX, + BATCH_COMPACT_TYPESTRING_FRAGMENT_THREE, + BATCH_COMPACT_TYPESTRING_FRAGMENT_TWO, + LOCK_TYPEHASH, + Lock +} from '@uniswap/the-compact/types/EIP712Types.sol'; import {ERC6909} from '@solady/tokens/ERC6909.sol'; import {ResetPeriod} from '@uniswap/the-compact/types/ResetPeriod.sol'; @@ -64,6 +77,21 @@ contract OnChainAllocatorTest is Test, TestHelper { // For reentrancy testing MaliciousRecipient internal maliciousRecipient; + // Permit2 constants + address constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3; + bytes32 constant PERMIT2_DOMAIN_SEPARATOR_TYPEHASH = + keccak256('EIP712Domain(string name,uint256 chainId,address verifyingContract)'); + bytes32 constant TOKEN_PERMISSIONS_TYPEHASH = keccak256('TokenPermissions(address token,uint256 amount)'); + + // BatchActivation typehash (from the-compact/src/types/EIP712Types.sol) + bytes32 constant BATCH_COMPACT_BATCH_ACTIVATION_TYPEHASH = + 0xa794ed1a28cdf297ac45a3eee4643e35d29b295a389368da5f6baa420872c9b7; + + // PermitBatchWitnessTransferFrom typehash + string constant PERMIT_BATCH_WITNESS_TYPESTRING = + 'PermitBatchWitnessTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline,BatchActivation witness)BatchActivation(address activator,uint256[] ids,BatchCompact compact)BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments)Lock(bytes12 lockTag,address token,uint256 amount)TokenPermissions(address token,uint256 amount)'; + bytes32 constant PERMIT_BATCH_WITNESS_TYPEHASH = keccak256(bytes(PERMIT_BATCH_WITNESS_TYPESTRING)); + function setUp() public { // Deploy TheCompact at the hardcoded address used by Utility.sol // This is necessary because OnChainAllocator now inherits from Utility @@ -71,6 +99,9 @@ contract OnChainAllocatorTest is Test, TestHelper { compact = DeployTheCompact(new DeployTheCompact()).deployTheCompact(); assertEq(address(compact), address(0x00000000000000171ede64904551eeDF3C6C9788)); + // Deploy Permit2 at the expected address + _deployPermit2(); + arbiter = makeAddr('arbiter'); (user, userPK) = makeAddrAndKey('user'); allocator = new OnChainAllocator(); @@ -81,8 +112,15 @@ contract OnChainAllocatorTest is Test, TestHelper { recipient = makeAddr('recipient'); (caller, callerPK) = makeAddrAndKey('caller'); allocationCaller = new OnChainAllocationCaller(address(allocator), address(compact)); - deal(user, 1 ether); - usdc.mint(user, 1 ether); + deal(user, 10 ether); + usdc.mint(user, 10 ether); + dai.mint(user, 10 ether); + + // Approve Permit2 for tokens + vm.startPrank(user); + usdc.approve(PERMIT2, type(uint256).max); + dai.approve(PERMIT2, type(uint256).max); + vm.stopPrank(); defaultAmount = 1 ether; defaultExpiration = uint32(block.timestamp + 300); // 5 minutes fits 10-minute reset period @@ -92,12 +130,161 @@ contract OnChainAllocatorTest is Test, TestHelper { maliciousRecipient = new MaliciousRecipient(address(allocator), address(compact), address(allocationCaller)); } + function _deployPermit2() internal { + // Deploy Permit2 using the same pattern as the-compact tests + address permit2Deployer = address(0x4e59b44847b379578588920cA78FbF26c0B4956C); + address permit2DeployerDeployer = address(0x3fAB184622Dc19b6109349B94811493BF2a45362); + bytes memory permit2DeployerCreationCode = + hex'604580600e600039806000f350fe7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3'; + + vm.deal(permit2DeployerDeployer, 1e18); + vm.prank(permit2DeployerDeployer); + address deployedPermit2Deployer; + assembly ("memory-safe") { + deployedPermit2Deployer := + create(0, add(permit2DeployerCreationCode, 0x20), mload(permit2DeployerCreationCode)) + } + + bytes memory permit2CreationCalldata = + hex'0000000000000000000000000000000000000000d3af2663da51c1021500000060c0346100bb574660a052602081017f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a86681527f9ac997416e8ff9d2ff6bebeb7149f65cdae5e32e2b90440b566bb3044041d36a60408301524660608301523060808301526080825260a082019180831060018060401b038411176100a557826040525190206080526123c090816100c1823960805181611b47015260a05181611b210152f35b634e487b7160e01b600052604160045260246000fd5b600080fdfe6040608081526004908136101561001557600080fd5b600090813560e01c80630d58b1db1461126c578063137c29fe146110755780632a2d80d114610db75780632b67b57014610bde57806330f28b7a14610ade5780633644e51514610a9d57806336c7851614610a285780633ff9dcb1146109a85780634fe02b441461093f57806365d9723c146107ac57806387517c451461067a578063927da105146105c3578063cc53287f146104a3578063edd9444b1461033a5763fe8ec1a7146100c657600080fd5b346103365760c07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff833581811161033257610114903690860161164b565b60243582811161032e5761012b903690870161161a565b6101336114e6565b9160843585811161032a5761014b9036908a016115c1565b98909560a43590811161032657610164913691016115c1565b969095815190610173826113ff565b606b82527f5065726d697442617463685769746e6573735472616e7366657246726f6d285460208301527f6f6b656e5065726d697373696f6e735b5d207065726d69747465642c61646472838301527f657373207370656e6465722c75696e74323536206e6f6e63652c75696e74323560608301527f3620646561646c696e652c000000000000000000000000000000000000000000608083015282519a8b9181610222602085018096611f93565b918237018a8152039961025b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09b8c8101835282611437565b5190209085515161026b81611ebb565b908a5b8181106102f95750506102f6999a6102ed9183516102a081610294602082018095611f66565b03848101835282611437565b519020602089810151858b015195519182019687526040820192909252336060820152608081019190915260a081019390935260643560c08401528260e081015b03908101835282611437565b51902093611cf7565b80f35b8061031161030b610321938c5161175e565b51612054565b61031b828661175e565b52611f0a565b61026e565b8880fd5b8780fd5b8480fd5b8380fd5b5080fd5b5091346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff9080358281116103325761038b903690830161164b565b60243583811161032e576103a2903690840161161a565b9390926103ad6114e6565b9160643590811161049f576103c4913691016115c1565b949093835151976103d489611ebb565b98885b81811061047d5750506102f697988151610425816103f9602082018095611f66565b037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101835282611437565b5190206020860151828701519083519260208401947ffcf35f5ac6a2c28868dc44c302166470266239195f02b0ee408334829333b7668652840152336060840152608083015260a082015260a081526102ed8161141b565b808b61031b8261049461030b61049a968d5161175e565b9261175e565b6103d7565b8680fd5b5082346105bf57602090817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103325780359067ffffffffffffffff821161032e576104f49136910161161a565b929091845b848110610504578580f35b8061051a610515600193888861196c565b61197c565b61052f84610529848a8a61196c565b0161197c565b3389528385528589209173ffffffffffffffffffffffffffffffffffffffff80911692838b528652868a20911690818a5285528589207fffffffffffffffffffffffff000000000000000000000000000000000000000081541690558551918252848201527f89b1add15eff56b3dfe299ad94e01f2b52fbcb80ae1a3baea6ae8c04cb2b98a4853392a2016104f9565b8280fd5b50346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610676816105ff6114a0565b936106086114c3565b6106106114e6565b73ffffffffffffffffffffffffffffffffffffffff968716835260016020908152848420928816845291825283832090871683528152919020549251938316845260a083901c65ffffffffffff169084015260d09190911c604083015281906060820190565b0390f35b50346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336576106b26114a0565b906106bb6114c3565b916106c46114e6565b65ffffffffffff926064358481169081810361032a5779ffffffffffff0000000000000000000000000000000000000000947fda9fa7c1b00402c17d0161b249b1ab8bbec047c5a52207b9c112deffd817036b94338a5260016020527fffffffffffff0000000000000000000000000000000000000000000000000000858b209873ffffffffffffffffffffffffffffffffffffffff809416998a8d5260205283878d209b169a8b8d52602052868c209486156000146107a457504216925b8454921697889360a01b16911617179055815193845260208401523392a480f35b905092610783565b5082346105bf5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576107e56114a0565b906107ee6114c3565b9265ffffffffffff604435818116939084810361032a57338852602091600183528489209673ffffffffffffffffffffffffffffffffffffffff80911697888b528452858a20981697888a5283528489205460d01c93848711156109175761ffff9085840316116108f05750907f55eb90d810e1700b35a8e7e25395ff7f2b2259abd7415ca2284dfb1c246418f393929133895260018252838920878a528252838920888a5282528389209079ffffffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffff000000000000000000000000000000000000000000000000000083549260d01b16911617905582519485528401523392a480f35b84517f24d35a26000000000000000000000000000000000000000000000000000000008152fd5b5084517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b503461033657807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336578060209273ffffffffffffffffffffffffffffffffffffffff61098f6114a0565b1681528084528181206024358252845220549051908152f35b5082346105bf57817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf577f3704902f963766a4e561bbaab6e6cdc1b1dd12f6e9e99648da8843b3f46b918d90359160243533855284602052818520848652602052818520818154179055815193845260208401523392a280f35b8234610a9a5760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610a9a57610a606114a0565b610a686114c3565b610a706114e6565b6064359173ffffffffffffffffffffffffffffffffffffffff8316830361032e576102f6936117a1565b80fd5b503461033657817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657602090610ad7611b1e565b9051908152f35b508290346105bf576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf57610b1a3661152a565b90807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c36011261033257610b4c611478565b9160e43567ffffffffffffffff8111610bda576102f694610b6f913691016115c1565b939092610b7c8351612054565b6020840151828501519083519260208401947f939c21a48a8dbe3a9a2404a1d46691e4d39f6583d6ec6b35714604c986d801068652840152336060840152608083015260a082015260a08152610bd18161141b565b51902091611c25565b8580fd5b509134610336576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610c186114a0565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc360160c08112610332576080855191610c51836113e3565b1261033257845190610c6282611398565b73ffffffffffffffffffffffffffffffffffffffff91602435838116810361049f578152604435838116810361049f57602082015265ffffffffffff606435818116810361032a5788830152608435908116810361049f576060820152815260a435938285168503610bda576020820194855260c4359087830182815260e43567ffffffffffffffff811161032657610cfe90369084016115c1565b929093804211610d88575050918591610d786102f6999a610d7e95610d238851611fbe565b90898c511690519083519260208401947ff3841cd1ff0085026a6327b620b67997ce40f282c88a8e905a7a5626e310f3d086528401526060830152608082015260808152610d70816113ff565b519020611bd9565b916120c7565b519251169161199d565b602492508a51917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b5091346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc93818536011261033257610df36114a0565b9260249081359267ffffffffffffffff9788851161032a578590853603011261049f578051978589018981108282111761104a578252848301358181116103265785019036602383011215610326578382013591610e50836115ef565b90610e5d85519283611437565b838252602093878584019160071b83010191368311611046578801905b828210610fe9575050508a526044610e93868801611509565b96838c01978852013594838b0191868352604435908111610fe557610ebb90369087016115c1565b959096804211610fba575050508998995151610ed681611ebb565b908b5b818110610f9757505092889492610d7892610f6497958351610f02816103f98682018095611f66565b5190209073ffffffffffffffffffffffffffffffffffffffff9a8b8b51169151928551948501957faf1b0d30d2cab0380e68f0689007e3254993c596f2fdd0aaa7f4d04f794408638752850152830152608082015260808152610d70816113ff565b51169082515192845b848110610f78578580f35b80610f918585610f8b600195875161175e565b5161199d565b01610f6d565b80610311610fac8e9f9e93610fb2945161175e565b51611fbe565b9b9a9b610ed9565b8551917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b8a80fd5b6080823603126110465785608091885161100281611398565b61100b85611509565b8152611018838601611509565b838201526110278a8601611607565b8a8201528d611037818701611607565b90820152815201910190610e7a565b8c80fd5b84896041867f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b5082346105bf576101407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576110b03661152a565b91807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c360112610332576110e2611478565b67ffffffffffffffff93906101043585811161049f5761110590369086016115c1565b90936101243596871161032a57611125610bd1966102f6983691016115c1565b969095825190611134826113ff565b606482527f5065726d69745769746e6573735472616e7366657246726f6d28546f6b656e5060208301527f65726d697373696f6e73207065726d69747465642c6164647265737320737065848301527f6e6465722c75696e74323536206e6f6e63652c75696e7432353620646561646c60608301527f696e652c0000000000000000000000000000000000000000000000000000000060808301528351948591816111e3602085018096611f93565b918237018b8152039361121c7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe095868101835282611437565b5190209261122a8651612054565b6020878101518589015195519182019687526040820192909252336060820152608081019190915260a081019390935260e43560c08401528260e081016102e1565b5082346105bf576020807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033257813567ffffffffffffffff92838211610bda5736602383011215610bda5781013592831161032e576024906007368386831b8401011161049f57865b8581106112e5578780f35b80821b83019060807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc83360301126103265761139288876001946060835161132c81611398565b611368608461133c8d8601611509565b9485845261134c60448201611509565b809785015261135d60648201611509565b809885015201611509565b918291015273ffffffffffffffffffffffffffffffffffffffff80808093169516931691166117a1565b016112da565b6080810190811067ffffffffffffffff8211176113b457604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6060810190811067ffffffffffffffff8211176113b457604052565b60a0810190811067ffffffffffffffff8211176113b457604052565b60c0810190811067ffffffffffffffff8211176113b457604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff8211176113b457604052565b60c4359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b600080fd5b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6024359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6044359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc01906080821261149b576040805190611563826113e3565b8082941261149b57805181810181811067ffffffffffffffff8211176113b457825260043573ffffffffffffffffffffffffffffffffffffffff8116810361149b578152602435602082015282526044356020830152606435910152565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020838186019501011161149b57565b67ffffffffffffffff81116113b45760051b60200190565b359065ffffffffffff8216820361149b57565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020808501948460061b01011161149b57565b91909160608184031261149b576040805191611666836113e3565b8294813567ffffffffffffffff9081811161149b57830182601f8201121561149b578035611693816115ef565b926116a087519485611437565b818452602094858086019360061b8501019381851161149b579086899897969594939201925b8484106116e3575050505050855280820135908501520135910152565b90919293949596978483031261149b578851908982019082821085831117611730578a928992845261171487611509565b81528287013583820152815201930191908897969594936116c6565b602460007f4e487b710000000000000000000000000000000000000000000000000000000081526041600452fd5b80518210156117725760209160051b010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b92919273ffffffffffffffffffffffffffffffffffffffff604060008284168152600160205282828220961695868252602052818120338252602052209485549565ffffffffffff8760a01c16804211611884575082871696838803611812575b5050611810955016926118b5565b565b878484161160001461184f57602488604051907ff96fb0710000000000000000000000000000000000000000000000000000000082526004820152fd5b7fffffffffffffffffffffffff000000000000000000000000000000000000000084846118109a031691161790553880611802565b602490604051907fd81b2f2e0000000000000000000000000000000000000000000000000000000082526004820152fd5b9060006064926020958295604051947f23b872dd0000000000000000000000000000000000000000000000000000000086526004860152602485015260448401525af13d15601f3d116001600051141617161561190e57565b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601460248201527f5452414e534645525f46524f4d5f4641494c45440000000000000000000000006044820152fd5b91908110156117725760061b0190565b3573ffffffffffffffffffffffffffffffffffffffff8116810361149b5790565b9065ffffffffffff908160608401511673ffffffffffffffffffffffffffffffffffffffff908185511694826020820151169280866040809401511695169560009187835260016020528383208984526020528383209916988983526020528282209184835460d01c03611af5579185611ace94927fc6a377bfc4eb120024a8ac08eef205be16b817020812c73223e81d1bdb9708ec98979694508715600014611ad35779ffffffffffff00000000000000000000000000000000000000009042165b60a01b167fffffffffffff00000000000000000000000000000000000000000000000000006001860160d01b1617179055519384938491604091949373ffffffffffffffffffffffffffffffffffffffff606085019616845265ffffffffffff809216602085015216910152565b0390a4565b5079ffffffffffff000000000000000000000000000000000000000087611a60565b600484517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b467f000000000000000000000000000000000000000000000000000000000000000003611b69577f000000000000000000000000000000000000000000000000000000000000000090565b60405160208101907f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a86682527f9ac997416e8ff9d2ff6bebeb7149f65cdae5e32e2b90440b566bb3044041d36a604082015246606082015230608082015260808152611bd3816113ff565b51902090565b611be1611b1e565b906040519060208201927f190100000000000000000000000000000000000000000000000000000000000084526022830152604282015260428152611bd381611398565b9192909360a435936040840151804211611cc65750602084510151808611611c955750918591610d78611c6594611c60602088015186611e47565b611bd9565b73ffffffffffffffffffffffffffffffffffffffff809151511692608435918216820361149b57611810936118b5565b602490604051907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b602490604051907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b959093958051519560409283830151804211611e175750848803611dee57611d2e918691610d7860209b611c608d88015186611e47565b60005b868110611d42575050505050505050565b611d4d81835161175e565b5188611d5a83878a61196c565b01359089810151808311611dbe575091818888886001968596611d84575b50505050505001611d31565b611db395611dad9273ffffffffffffffffffffffffffffffffffffffff6105159351169561196c565b916118b5565b803888888883611d78565b6024908651907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b600484517fff633a38000000000000000000000000000000000000000000000000000000008152fd5b6024908551907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b9073ffffffffffffffffffffffffffffffffffffffff600160ff83161b9216600052600060205260406000209060081c6000526020526040600020818154188091551615611e9157565b60046040517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b90611ec5826115ef565b611ed26040519182611437565b8281527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0611f0082946115ef565b0190602036910137565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114611f375760010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b805160208092019160005b828110611f7f575050505090565b835185529381019392810192600101611f71565b9081519160005b838110611fab575050016000815290565b8060208092840101518185015201611f9a565b60405160208101917f65626cad6cb96493bf6f5ebea28756c966f023ab9e8a83a7101849d5573b3678835273ffffffffffffffffffffffffffffffffffffffff8082511660408401526020820151166060830152606065ffffffffffff9182604082015116608085015201511660a082015260a0815260c0810181811067ffffffffffffffff8211176113b45760405251902090565b6040516020808201927f618358ac3db8dc274f0cd8829da7e234bd48cd73c4a740aede1adec9846d06a1845273ffffffffffffffffffffffffffffffffffffffff81511660408401520151606082015260608152611bd381611398565b919082604091031261149b576020823592013590565b6000843b61222e5750604182036121ac576120e4828201826120b1565b939092604010156117725760209360009360ff6040608095013560f81c5b60405194855216868401526040830152606082015282805260015afa156121a05773ffffffffffffffffffffffffffffffffffffffff806000511691821561217657160361214c57565b60046040517f815e1d64000000000000000000000000000000000000000000000000000000008152fd5b60046040517f8baa579f000000000000000000000000000000000000000000000000000000008152fd5b6040513d6000823e3d90fd5b60408203612204576121c0918101906120b1565b91601b7f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff84169360ff1c019060ff8211611f375760209360009360ff608094612102565b60046040517f4be6321b000000000000000000000000000000000000000000000000000000008152fd5b929391601f928173ffffffffffffffffffffffffffffffffffffffff60646020957fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0604051988997889687947f1626ba7e000000000000000000000000000000000000000000000000000000009e8f8752600487015260406024870152816044870152868601378b85828601015201168101030192165afa9081156123a857829161232a575b507fffffffff000000000000000000000000000000000000000000000000000000009150160361230057565b60046040517fb0669cbc000000000000000000000000000000000000000000000000000000008152fd5b90506020813d82116123a0575b8161234460209383611437565b810103126103365751907fffffffff0000000000000000000000000000000000000000000000000000000082168203610a9a57507fffffffff0000000000000000000000000000000000000000000000000000000090386122d4565b3d9150612337565b6040513d84823e3d90fdfea164736f6c6343000811000a'; + + (bool ok,) = permit2Deployer.call(permit2CreationCalldata); + require(ok && PERMIT2.code.length != 0, 'permit2 deployment failed'); + } + /* --------------------------------------------------------------------- */ /* Helpers */ /* --------------------------------------------------------------------- */ function _composeNonceUint(address a, uint256 nonce) internal pure returns (uint256) { - return (uint256(uint160(a)) << 96) | nonce; + // Nonce structure: command (8 bits) | address (160 bits) | nonce (88 bits) + // ON_CHAIN_NONCE = 0x01 + return (uint256(0x01) << 248) | (uint256(uint160(a)) << 88) | nonce; + } + + /* --------------------------------------------------------------------- */ + /* Permit2 Helper Functions */ + /* --------------------------------------------------------------------- */ + + function _getLockTag() internal view returns (bytes12) { + return _toLockTag(address(allocator), Scope.Multichain, ResetPeriod.TenMinutes); + } + + function _getPermit2DomainSeparator() internal view returns (bytes32) { + return keccak256( + abi.encode(PERMIT2_DOMAIN_SEPARATOR_TYPEHASH, keccak256(bytes('Permit2')), block.chainid, PERMIT2) + ); + } + + function _createTokenPermissions(address token, uint256 amount) + internal + pure + returns (ISignatureTransfer.TokenPermissions[] memory) + { + ISignatureTransfer.TokenPermissions[] memory permitted = new ISignatureTransfer.TokenPermissions[](1); + permitted[0] = ISignatureTransfer.TokenPermissions({token: token, amount: amount}); + return permitted; + } + + function _createTokenPermissions2(address token1, uint256 amount1, address token2, uint256 amount2) + internal + pure + returns (ISignatureTransfer.TokenPermissions[] memory) + { + ISignatureTransfer.TokenPermissions[] memory permitted = new ISignatureTransfer.TokenPermissions[](2); + permitted[0] = ISignatureTransfer.TokenPermissions({token: token1, amount: amount1}); + permitted[1] = ISignatureTransfer.TokenPermissions({token: token2, amount: amount2}); + return permitted; + } + + function _createDepositDetails(uint256 nonce, uint256 deadline, bytes12 lockTag) + internal + pure + returns (DepositDetails memory) + { + return DepositDetails({nonce: nonce, deadline: deadline, lockTag: lockTag}); + } + + function _hashTokenPermissions(ISignatureTransfer.TokenPermissions[] memory permitted) + internal + pure + returns (bytes32) + { + bytes32[] memory hashes = new bytes32[](permitted.length); + for (uint256 i = 0; i < permitted.length; i++) { + hashes[i] = keccak256(abi.encode(TOKEN_PERMISSIONS_TYPEHASH, permitted[i].token, permitted[i].amount)); + } + return keccak256(abi.encodePacked(hashes)); + } + + /// @dev Computes the Lock commitment hash with proper struct encoding + function _computeCommitmentHash(uint256 id, uint256 amount) internal pure returns (bytes32) { + bytes32 lockTypehash = keccak256('Lock(bytes12 lockTag,address token,uint256 amount)'); + bytes12 lockTag = bytes12(bytes32(id)); + address token = address(uint160(id)); + return keccak256(abi.encode(lockTypehash, lockTag, token, amount)); + } + + /// @dev Creates a permit2 nonce with the correct command byte and sponsor address + function _createPermit2Nonce(address sponsor, uint88 freeNonce) internal pure returns (uint256) { + // First byte: PERMIT2_NONCE (0x03) + // Next 20 bytes: sponsor address + // Last 11 bytes: free nonce + return uint256(0x03) << 248 | uint256(uint160(sponsor)) << 88 | uint256(freeNonce); + } + + function _createPermit2Signature( + ISignatureTransfer.TokenPermissions[] memory permitted, + DepositDetails memory details, + bytes32 claimHash, + uint256 signerPk + ) internal view returns (bytes memory) { + bytes12 lockTag = details.lockTag; + + // Check if first token is native + bool hasNative = permitted.length > 0 && permitted[0].token == address(0); + + // Create ids array - includes ALL tokens (including native) + uint256[] memory ids = new uint256[](permitted.length); + for (uint256 i = 0; i < permitted.length; i++) { + ids[i] = AllocatorLib.toId(lockTag, permitted[i].token); + } + bytes32 idsHash = keccak256(abi.encodePacked(ids)); + + // Create activation hash using the exact same typehash TheCompact uses + // The activator is the allocator (msg.sender to TheCompact) + bytes32 activationHash = + keccak256(abi.encode(BATCH_COMPACT_BATCH_ACTIVATION_TYPEHASH, address(allocator), idsHash, claimHash)); + + // Create permit batch hash - tokenPermissionsHash only includes ERC20 tokens (not native) + ISignatureTransfer.TokenPermissions[] memory erc20Permitted; + if (hasNative) { + erc20Permitted = new ISignatureTransfer.TokenPermissions[](permitted.length - 1); + for (uint256 i = 0; i < erc20Permitted.length; i++) { + erc20Permitted[i] = permitted[i + 1]; + } + } else { + erc20Permitted = permitted; + } + bytes32 tokenPermissionsHash = _hashTokenPermissions(erc20Permitted); + bytes32 permitBatchHash = keccak256( + abi.encode( + PERMIT_BATCH_WITNESS_TYPEHASH, + tokenPermissionsHash, + address(compact), + details.nonce, + details.deadline, + activationHash + ) + ); + + // Create digest + bytes32 domainSeparator = _getPermit2DomainSeparator(); + bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), domainSeparator, permitBatchHash)); + + // Sign + (bytes32 r, bytes32 vs) = vm.signCompact(signerPk, digest); + return abi.encodePacked(r, vs); } function _commitmentsHash(Lock[] memory commitments) internal pure returns (bytes32) { @@ -1083,8 +1270,7 @@ contract OnChainAllocatorTest is Test, TestHelper { ); assertEq(returnedNonce, _composeNonceUint(address(0), 1)); - // storage nonce is only incremented in executeAllocation - assertEq(allocator.nonces(caller), 0); + // storage nonce is only incremented in executeAllocation (on-chain allocations use address(0)) assertEq(allocator.nonces(address(0)), 0); } @@ -1125,8 +1311,8 @@ contract OnChainAllocatorTest is Test, TestHelper { vm.prank(address(allocationCaller)); usdc.approve(address(compact), amount); - // Check nonce previous to the allocation - assertEq(allocator.nonces(address(allocationCaller)), 0); + // Check nonce previous to the allocation (on-chain allocations use address(0)) + assertEq(allocator.nonces(address(0)), 0); // run the whole flow in a single tx through the helper allocationCaller.onChainAllocation( @@ -1134,8 +1320,7 @@ contract OnChainAllocatorTest is Test, TestHelper { ); vm.snapshotGasLastCall('onchain_execute_single'); - // nonce is scoped to (callerContract, recipient) - assertEq(allocator.nonces(address(allocationCaller)), 0); + // nonce is scoped to address(0) for on-chain allocations assertEq(allocator.nonces(address(0)), 1); uint256 expectedNonce = _composeNonceUint(address(0), 1); @@ -1325,7 +1510,12 @@ contract OnChainAllocatorTest is Test, TestHelper { // Compute the claimHash that AllocatorLib will recompute during execute. Lock[] memory commitments = _idsAndAmountsToCommitments(idsAndAmounts); bytes32 expectedClaimHash = _createClaimHash( - recipient, arbiter, _composeNonceUint(address(0), 1), defaultExpiration, commitments, bytes32(0) + recipient, + arbiter, + _composeNonceUint(address(0), 1), // On-chain allocations use address(0) in nonce + defaultExpiration, + commitments, + bytes32(0) ); vm.prank(user); vm.expectRevert( @@ -2174,6 +2364,395 @@ contract OnChainAllocatorTest is Test, TestHelper { 'Malicious recipient should still have his balance in TheCompact' ); } + + /* --------------------------------------------------------------------- */ + /* Tests for permit2Allocation */ + /* --------------------------------------------------------------------- */ + + function test_permit2Allocation_singleERC20() public { + bytes12 lockTag = _getLockTag(); + uint88 freeNonce = 1; + uint256 nonce = _createPermit2Nonce(user, freeNonce); + + // Prepare token permissions + ISignatureTransfer.TokenPermissions[] memory permitted = _createTokenPermissions(address(usdc), defaultAmount); + + // Prepare deposit details + DepositDetails memory details = _createDepositDetails(nonce, defaultExpiration, lockTag); + + // Compute ids + uint256[] memory ids = new uint256[](1); + ids[0] = AllocatorLib.toId(lockTag, address(usdc)); + + // Compute commitment hashes + bytes32[] memory commitmentHashes = new bytes32[](1); + commitmentHashes[0] = _computeCommitmentHash(ids[0], defaultAmount); + + // Compute claimHash (no witness) + bytes32 claimHash = keccak256( + abi.encode( + BATCH_COMPACT_TYPEHASH, + arbiter, + user, + nonce, + defaultExpiration, + keccak256(abi.encodePacked(commitmentHashes)) + ) + ); + + // Create Permit2 signature + bytes memory signature = _createPermit2Signature(permitted, details, claimHash, userPK); + + // Execute + Lock[] memory commitments = allocator.permit2Allocation(user, permitted, details, claimHash, '', signature); + vm.snapshotGasLastCall('onchain_permit2Allocation_singleERC20'); + + // Verify commitments + assertEq(commitments.length, 1); + assertEq(commitments[0].lockTag, lockTag); + assertEq(commitments[0].token, address(usdc)); + assertEq(commitments[0].amount, defaultAmount); + + // Verify claim is authorized + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = ids[0]; + idsAndAmounts[0][1] = defaultAmount; + assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); + + // Verify tokens are in compact + assertEq(usdc.balanceOf(address(compact)), defaultAmount); + assertEq(compact.balanceOf(user, ids[0]), defaultAmount); + } + + function test_permit2Allocation_multipleERC20() public { + bytes12 lockTag = _getLockTag(); + uint88 freeNonce = 1; + uint256 nonce = _createPermit2Nonce(user, freeNonce); + uint256 amount1 = defaultAmount; + uint256 amount2 = defaultAmount / 2; + + // Prepare token permissions (sorted by address for Permit2) + ISignatureTransfer.TokenPermissions[] memory permitted; + uint256[] memory ids = new uint256[](2); + bytes32[] memory commitmentHashes = new bytes32[](2); + + if (uint160(address(usdc)) < uint160(address(dai))) { + permitted = _createTokenPermissions2(address(usdc), amount1, address(dai), amount2); + ids[0] = AllocatorLib.toId(lockTag, address(usdc)); + ids[1] = AllocatorLib.toId(lockTag, address(dai)); + commitmentHashes[0] = _computeCommitmentHash(ids[0], amount1); + commitmentHashes[1] = _computeCommitmentHash(ids[1], amount2); + } else { + permitted = _createTokenPermissions2(address(dai), amount2, address(usdc), amount1); + ids[0] = AllocatorLib.toId(lockTag, address(dai)); + ids[1] = AllocatorLib.toId(lockTag, address(usdc)); + commitmentHashes[0] = _computeCommitmentHash(ids[0], amount2); + commitmentHashes[1] = _computeCommitmentHash(ids[1], amount1); + } + + // Prepare deposit details + DepositDetails memory details = _createDepositDetails(nonce, defaultExpiration, lockTag); + + // Compute claimHash (no witness) + bytes32 claimHash = keccak256( + abi.encode( + BATCH_COMPACT_TYPEHASH, + arbiter, + user, + nonce, + defaultExpiration, + keccak256(abi.encodePacked(commitmentHashes)) + ) + ); + + // Create Permit2 signature + bytes memory signature = _createPermit2Signature(permitted, details, claimHash, userPK); + + // Execute + Lock[] memory commitments = allocator.permit2Allocation(user, permitted, details, claimHash, '', signature); + vm.snapshotGasLastCall('onchain_permit2Allocation_multipleERC20'); + + // Verify commitments + assertEq(commitments.length, 2); + + // Verify claim is authorized + uint256[2][] memory idsAndAmounts = new uint256[2][](2); + idsAndAmounts[0][0] = ids[0]; + idsAndAmounts[0][1] = permitted[0].amount; + idsAndAmounts[1][0] = ids[1]; + idsAndAmounts[1][1] = permitted[1].amount; + assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); + + // Verify tokens are in compact + assertEq(usdc.balanceOf(address(compact)), amount1); + assertEq(dai.balanceOf(address(compact)), amount2); + } + + function test_permit2Allocation_revert_invalidNonceCommand() public { + bytes12 lockTag = _getLockTag(); + // Use ON_CHAIN_NONCE command (0x01) instead of PERMIT2_NONCE (0x03) + uint256 invalidNonce = uint256(0x01) << 248 | uint256(uint160(user)) << 88 | uint256(1); + + // Prepare token permissions + ISignatureTransfer.TokenPermissions[] memory permitted = _createTokenPermissions(address(usdc), defaultAmount); + + // Prepare deposit details with invalid nonce + DepositDetails memory details = _createDepositDetails(invalidNonce, defaultExpiration, lockTag); + + // Compute ids and commitment hashes (these are just for signature computation) + uint256[] memory ids = new uint256[](1); + ids[0] = AllocatorLib.toId(lockTag, address(usdc)); + bytes32[] memory commitmentHashes = new bytes32[](1); + commitmentHashes[0] = _computeCommitmentHash(ids[0], defaultAmount); + + bytes32 claimHash = keccak256( + abi.encode( + BATCH_COMPACT_TYPEHASH, + arbiter, + user, + invalidNonce, + defaultExpiration, + keccak256(abi.encodePacked(commitmentHashes)) + ) + ); + + bytes memory signature = _createPermit2Signature(permitted, details, claimHash, userPK); + + // Should revert with UnauthorizedNonce because command is not PERMIT2_NONCE + vm.expectRevert(abi.encodeWithSelector(AllocatorLib.UnauthorizedNonce.selector, bytes1(0x01), user)); + allocator.permit2Allocation(user, permitted, details, claimHash, '', signature); + } + + function test_permit2Allocation_revert_invalidExpiration() public { + bytes12 lockTag = _getLockTag(); + uint88 freeNonce = 1; + uint256 nonce = _createPermit2Nonce(user, freeNonce); + uint256 invalidDeadline = uint256(type(uint32).max) + 1; + + // Prepare token permissions + ISignatureTransfer.TokenPermissions[] memory permitted = _createTokenPermissions(address(usdc), defaultAmount); + + // Prepare deposit details with invalid expiration + DepositDetails memory details = _createDepositDetails(nonce, invalidDeadline, lockTag); + + bytes32 claimHash = bytes32(uint256(1)); // dummy claim hash + bytes memory signature = new bytes(64); // dummy signature + + // Should revert because deadline exceeds uint32 max + vm.expectRevert( + abi.encodeWithSelector(IOnChainAllocator.InvalidExpiration.selector, invalidDeadline, type(uint32).max) + ); + allocator.permit2Allocation(user, permitted, details, claimHash, '', signature); + } + + function test_permit2Allocation_revert_invalidAmount() public { + bytes12 lockTag = _getLockTag(); + uint88 freeNonce = 1; + uint256 nonce = _createPermit2Nonce(user, freeNonce); + uint256 largeAmount = uint256(type(uint224).max) + 1; + + // Fund user with enough tokens + usdc.mint(user, largeAmount); + vm.prank(user); + usdc.approve(PERMIT2, largeAmount); + + // Prepare token permissions with amount > uint224 max + ISignatureTransfer.TokenPermissions[] memory permitted = _createTokenPermissions(address(usdc), largeAmount); + + // Prepare deposit details + DepositDetails memory details = _createDepositDetails(nonce, defaultExpiration, lockTag); + + uint256[] memory ids = new uint256[](1); + ids[0] = AllocatorLib.toId(lockTag, address(usdc)); + bytes32[] memory commitmentHashes = new bytes32[](1); + commitmentHashes[0] = _computeCommitmentHash(ids[0], largeAmount); + + bytes32 claimHash = keccak256( + abi.encode( + BATCH_COMPACT_TYPEHASH, + arbiter, + user, + nonce, + defaultExpiration, + keccak256(abi.encodePacked(commitmentHashes)) + ) + ); + + bytes memory signature = _createPermit2Signature(permitted, details, claimHash, userPK); + + // Should revert because amount exceeds uint224 max + vm.expectRevert(abi.encodeWithSelector(IOnChainAllocator.InvalidAmount.selector, largeAmount)); + allocator.permit2Allocation(user, permitted, details, claimHash, '', signature); + } + + function test_permit2Allocation_fullClaimFlow() public { + bytes12 lockTag = _getLockTag(); + uint88 freeNonce = 1; + uint256 nonce = _createPermit2Nonce(user, freeNonce); + + // Prepare token permissions + ISignatureTransfer.TokenPermissions[] memory permitted = _createTokenPermissions(address(usdc), defaultAmount); + + // Prepare deposit details + DepositDetails memory details = _createDepositDetails(nonce, defaultExpiration, lockTag); + + // Compute ids + uint256 id = AllocatorLib.toId(lockTag, address(usdc)); + bytes32[] memory commitmentHashes = new bytes32[](1); + commitmentHashes[0] = _computeCommitmentHash(id, defaultAmount); + + // Compute claimHash (no witness) + bytes32 claimHash = keccak256( + abi.encode( + BATCH_COMPACT_TYPEHASH, + arbiter, + user, + nonce, + defaultExpiration, + keccak256(abi.encodePacked(commitmentHashes)) + ) + ); + + // Create Permit2 signature + bytes memory signature = _createPermit2Signature(permitted, details, claimHash, userPK); + + // Execute permit2Allocation + allocator.permit2Allocation(user, permitted, details, claimHash, '', signature); + + // Verify claim is authorized + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = id; + idsAndAmounts[0][1] = defaultAmount; + assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); + + // Now execute the claim + address claimRecipient = makeAddr('claimRecipient'); + Component[] memory portions = new Component[](1); + portions[0] = + Component({claimant: uint256(bytes32(abi.encodePacked(bytes12(0), claimRecipient))), amount: defaultAmount}); + + BatchClaimComponent[] memory claimComponents = new BatchClaimComponent[](1); + claimComponents[0] = BatchClaimComponent({id: id, allocatedAmount: defaultAmount, portions: portions}); + + BatchClaim memory claim = BatchClaim({ + allocatorData: '', + sponsorSignature: '', + sponsor: user, + nonce: nonce, + expires: details.deadline, + witness: bytes32(0), + witnessTypestring: '', + claims: claimComponents + }); + + // Execute claim + vm.prank(arbiter); + bytes32 returnedClaimHash = compact.batchClaim(claim); + assertEq(returnedClaimHash, claimHash); + + // Verify tokens transferred + assertEq(usdc.balanceOf(claimRecipient), defaultAmount); + assertEq(usdc.balanceOf(address(compact)), 0); + + // Verify claim is no longer authorized (deleted after execution) + assertFalse(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); + } + + function test_permit2Allocation_storesAllocationWithExpiration() public { + bytes12 lockTag = _getLockTag(); + uint88 freeNonce = 1; + uint256 nonce = _createPermit2Nonce(user, freeNonce); + + // Prepare token permissions + ISignatureTransfer.TokenPermissions[] memory permitted = _createTokenPermissions(address(usdc), defaultAmount); + + // Prepare deposit details + DepositDetails memory details = _createDepositDetails(nonce, defaultExpiration, lockTag); + + // Compute ids + uint256 id = AllocatorLib.toId(lockTag, address(usdc)); + bytes32[] memory commitmentHashes = new bytes32[](1); + commitmentHashes[0] = _computeCommitmentHash(id, defaultAmount); + + bytes32 claimHash = keccak256( + abi.encode( + BATCH_COMPACT_TYPEHASH, + arbiter, + user, + nonce, + defaultExpiration, + keccak256(abi.encodePacked(commitmentHashes)) + ) + ); + + bytes memory signature = _createPermit2Signature(permitted, details, claimHash, userPK); + + // Execute permit2Allocation + allocator.permit2Allocation(user, permitted, details, claimHash, '', signature); + + // Verify claim is authorized before expiration + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = id; + idsAndAmounts[0][1] = defaultAmount; + assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); + + // Warp time past expiration + vm.warp(defaultExpiration + 1); + + // Verify claim is no longer authorized after expiration + assertFalse(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); + } + + function test_permit2Allocation_blocksTransfersUntilExpiration() public { + bytes12 lockTag = _getLockTag(); + uint88 freeNonce = 1; + uint256 nonce = _createPermit2Nonce(user, freeNonce); + + // Prepare token permissions + ISignatureTransfer.TokenPermissions[] memory permitted = _createTokenPermissions(address(usdc), defaultAmount); + + // Prepare deposit details + DepositDetails memory details = _createDepositDetails(nonce, defaultExpiration, lockTag); + + // Compute ids and claimHash + uint256 id = AllocatorLib.toId(lockTag, address(usdc)); + bytes32[] memory commitmentHashes = new bytes32[](1); + commitmentHashes[0] = _computeCommitmentHash(id, defaultAmount); + + bytes32 claimHash = keccak256( + abi.encode( + BATCH_COMPACT_TYPEHASH, + arbiter, + user, + nonce, + defaultExpiration, + keccak256(abi.encodePacked(commitmentHashes)) + ) + ); + + bytes memory signature = _createPermit2Signature(permitted, details, claimHash, userPK); + + // Execute permit2Allocation + allocator.permit2Allocation(user, permitted, details, claimHash, '', signature); + + // Try to transfer - should fail because tokens are allocated + vm.prank(user); + vm.expectRevert( + abi.encodeWithSelector(IOnChainAllocator.InsufficientBalance.selector, user, id, 0, defaultAmount) + ); + compact.transfer(recipient, id, defaultAmount); + + // Warp past expiration + vm.warp(defaultExpiration + 1); + + // Now transfer should succeed since allocation expired + vm.prank(user); + compact.transfer(recipient, id, defaultAmount); + + // Verify transfer succeeded + assertEq(compact.balanceOf(user, id), 0); + assertEq(compact.balanceOf(recipient, id), defaultAmount); + } } /* ============================================================================ diff --git a/test/util/ERC7683TestHelper.sol b/test/util/ERC7683TestHelper.sol index cefdf9b..56ea6be 100644 --- a/test/util/ERC7683TestHelper.sol +++ b/test/util/ERC7683TestHelper.sol @@ -73,6 +73,11 @@ abstract contract MocksSetup is Test, TestHelper { uint256 NONCES_STORAGE_SLOT = 1; + // Nonce command constants (must match AllocatorLib) + bytes1 constant ON_CHAIN_NONCE = 0x01; + bytes1 constant OFF_CHAIN_NONCE = 0x02; + bytes1 constant PERMIT2_NONCE = 0x03; + function setUp() public virtual { (user, userPK) = makeAddrAndKey('user'); arbiter = makeAddr('arbiter'); @@ -98,12 +103,13 @@ abstract contract MocksSetup is Test, TestHelper { defaultNonce = defaultNonce_; } - function _composeNonceUint(address a, uint256 nonce) internal pure returns (uint256) { - return (uint256(uint160(a)) << 96) | nonce; + function _composeNonceUint(bytes1 command, address a, uint256 nonce) internal pure returns (uint256) { + // Nonce structure: command (8 bits) | address (160 bits) | nonce (88 bits) + return (uint256(uint8(command)) << 248) | (uint256(uint160(a)) << 88) | nonce; } - function _composeNonce(address a, uint256 nonce) internal pure returns (bytes32) { - return bytes32(_composeNonceUint(a, nonce)); + function _composeNonce(bytes1 command, address a, uint256 nonce) internal pure returns (bytes32) { + return bytes32(_composeNonceUint(command, a, nonce)); } } @@ -333,7 +339,7 @@ abstract contract GaslessCrossChainOrderData is CompactData { gaslessCrossChainOrder.originSettler = allocator; gaslessCrossChainOrder.user = compact_.sponsor; - gaslessCrossChainOrder.nonce = _composeNonceUint(compact_.sponsor, defaultNonce); + gaslessCrossChainOrder.nonce = defaultNonce; gaslessCrossChainOrder.originChainId = block.chainid; gaslessCrossChainOrder.openDeadline = uint32(_getClaimExpiration()); gaslessCrossChainOrder.fillDeadline = uint32(_getFillExpiration()); From 43686adf94356004f7262c89945e94a5f191976f Mon Sep 17 00:00:00 2001 From: mgretzke Date: Fri, 5 Dec 2025 13:14:22 +0100 Subject: [PATCH 4/9] recreate the claim hash for verification --- snapshots/ERC7683Allocator_openFor.json | 4 +- snapshots/HybridAllocatorTest.json | 18 +++---- snapshots/OnChainAllocatorTest.json | 16 +++--- src/allocators/HybridAllocator.sol | 6 ++- src/allocators/OnChainAllocator.sol | 8 ++- src/allocators/lib/AllocatorLib.sol | 66 ++++++++++++++++++++++++- src/interfaces/IHybridAllocator.sol | 27 ++++++++++ src/interfaces/IOnChainAllocator.sol | 26 ++++++++++ test/HybridAllocator.t.sol | 21 ++++---- test/OnChainAllocator.t.sol | 18 ++++--- 10 files changed, 170 insertions(+), 40 deletions(-) diff --git a/snapshots/ERC7683Allocator_openFor.json b/snapshots/ERC7683Allocator_openFor.json index f17d4c5..cbad145 100644 --- a/snapshots/ERC7683Allocator_openFor.json +++ b/snapshots/ERC7683Allocator_openFor.json @@ -1,3 +1,3 @@ { - "openFor_simpleOrder_userHimself": "172263" -} \ No newline at end of file + "openFor_simpleOrder_userHimself": "172263" +} diff --git a/snapshots/HybridAllocatorTest.json b/snapshots/HybridAllocatorTest.json index f163357..bfbf746 100644 --- a/snapshots/HybridAllocatorTest.json +++ b/snapshots/HybridAllocatorTest.json @@ -1,10 +1,10 @@ { - "allocateAndRegister_erc20Token": "187659", - "allocateAndRegister_erc20Token_emptyAmountInput": "188569", - "allocateAndRegister_multipleTokens": "223595", - "allocateAndRegister_nativeToken": "139222", - "allocateAndRegister_nativeToken_emptyAmountInput": "139058", - "allocateAndRegister_second_erc20Token": "114865", - "allocateAndRegister_second_nativeToken": "104858", - "hybrid_execute_single": "174805" -} \ No newline at end of file + "allocateAndRegister_erc20Token": "187659", + "allocateAndRegister_erc20Token_emptyAmountInput": "188569", + "allocateAndRegister_multipleTokens": "223595", + "allocateAndRegister_nativeToken": "139222", + "allocateAndRegister_nativeToken_emptyAmountInput": "139058", + "allocateAndRegister_second_erc20Token": "114865", + "allocateAndRegister_second_nativeToken": "104858", + "hybrid_execute_single": "174805" +} diff --git a/snapshots/OnChainAllocatorTest.json b/snapshots/OnChainAllocatorTest.json index 923520a..b47cb57 100644 --- a/snapshots/OnChainAllocatorTest.json +++ b/snapshots/OnChainAllocatorTest.json @@ -1,9 +1,9 @@ { - "allocateFor_success_withRegistration": "134193", - "allocate_and_delete_expired_allocation": "66373", - "allocate_erc20": "129644", - "allocate_native": "129404", - "allocate_second_erc20": "97656", - "onchain_execute_double": "346418", - "onchain_execute_single": "220035" -} \ No newline at end of file + "allocateFor_success_withRegistration": "134193", + "allocate_and_delete_expired_allocation": "66373", + "allocate_erc20": "129644", + "allocate_native": "129404", + "allocate_second_erc20": "97656", + "onchain_execute_double": "346418", + "onchain_execute_single": "220035" +} diff --git a/src/allocators/HybridAllocator.sol b/src/allocators/HybridAllocator.sol index 1c27fdc..7662590 100644 --- a/src/allocators/HybridAllocator.sol +++ b/src/allocators/HybridAllocator.sol @@ -186,15 +186,19 @@ contract HybridAllocator is IHybridAllocator { return (claimHash, registeredAmounts, nonce); } + /// @inheritdoc IHybridAllocator function permit2Allocation( + address arbiter, address depositor, ISignatureTransfer.TokenPermissions[] calldata permitted, DepositDetails calldata details, bytes32 claimHash, string calldata witness, + bytes32 witnessHash, bytes calldata signature ) external returns (Lock[] memory commitments) { - commitments = AL.permit2Allocation(depositor, permitted, details, claimHash, witness, signature); + commitments = + AL.permit2Allocation(arbiter, depositor, permitted, details, claimHash, witness, witnessHash, signature); // Allocate the claim claims[claimHash] = true; diff --git a/src/allocators/OnChainAllocator.sol b/src/allocators/OnChainAllocator.sol index e84b65e..5faf8f2 100644 --- a/src/allocators/OnChainAllocator.sol +++ b/src/allocators/OnChainAllocator.sol @@ -243,19 +243,23 @@ contract OnChainAllocator is IOnChainAllocator, Utility { return commitments; } + /// @inheritdoc IOnChainAllocator function permit2Allocation( + address arbiter, address depositor, ISignatureTransfer.TokenPermissions[] calldata permitted, DepositDetails calldata details, bytes32 claimHash, string calldata witness, + bytes32 witnessHash, bytes calldata signature ) external returns (Lock[] memory commitments) { if (details.deadline > type(uint32).max) { revert InvalidExpiration(details.deadline, type(uint32).max); } - commitments = AL.permit2Allocation(depositor, permitted, details, claimHash, witness, signature); + commitments = + AL.permit2Allocation(arbiter, depositor, permitted, details, claimHash, witness, witnessHash, signature); // Allocate the claim for (uint256 i = 0; i < commitments.length; i++) { @@ -269,7 +273,7 @@ contract OnChainAllocator is IOnChainAllocator, Utility { commitments[i].token, uint224(commitments[i].amount), depositor, - uint32(details.deadline), + uint32(details.deadline), // deadline is verified in the AllocatorLib.permit2Allocation function claimHash ); } diff --git a/src/allocators/lib/AllocatorLib.sol b/src/allocators/lib/AllocatorLib.sol index 468a573..16cf54f 100644 --- a/src/allocators/lib/AllocatorLib.sol +++ b/src/allocators/lib/AllocatorLib.sol @@ -4,7 +4,17 @@ pragma solidity ^0.8.27; import {ERC6909} from '@solady/tokens/ERC6909.sol'; import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; -import {LOCK_TYPEHASH, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; +import { + BATCH_COMPACT_TYPEHASH, + BATCH_COMPACT_TYPESTRING_FRAGMENT_FIVE, + BATCH_COMPACT_TYPESTRING_FRAGMENT_FOUR, + BATCH_COMPACT_TYPESTRING_FRAGMENT_ONE, + BATCH_COMPACT_TYPESTRING_FRAGMENT_SIX, + BATCH_COMPACT_TYPESTRING_FRAGMENT_THREE, + BATCH_COMPACT_TYPESTRING_FRAGMENT_TWO, + LOCK_TYPEHASH, + Lock +} from '@uniswap/the-compact/types/EIP712Types.sol'; import {ISignatureTransfer} from 'permit2/src/interfaces/ISignatureTransfer.sol'; import {CompactCategory} from 'the-compact/src/types/CompactCategory.sol'; @@ -54,13 +64,16 @@ library AllocatorLib { error InvalidAllocator(); error UnauthorizedNonce(bytes1 command, address sponsor); error InvalidCompactCall(address theCompact); + error InvalidClaim(bytes32 claimHash); function permit2Allocation( + address arbiter, address depositor, ISignatureTransfer.TokenPermissions[] calldata permitted, DepositDetails calldata details, bytes32 claimHash, // This claim hash is connected to the allocation. This does not guarantee, that the allocated tokens are connected to the claim hash. string calldata witness, + bytes32 witnessHash, bytes calldata signature ) internal returns (Lock[] memory commitments) { // Verifying the nonce is scoped to a permit2 allocation and to the sponsor @@ -176,6 +189,22 @@ library AllocatorLib { mstore(0x40, m) // Restore the memory pointer } + // Verify the claim hash includes the permit2 deadline as expiration + if ( + claimHash + != getClaimHash( + arbiter, + depositor, + details.nonce, + details.deadline, + getCommitmentsHashMemory(commitments), + witnessHash, + computeBatchCompactTypehash(witness) + ) + ) { + revert InvalidClaim(claimHash); + } + return commitments; } @@ -440,6 +469,22 @@ library AllocatorLib { return getCommitmentsHash(commitments, LOCK_TYPEHASH); } + function getCommitmentsHashMemory(Lock[] memory commitments) internal pure returns (bytes32 commitmentsHash) { + assembly ("memory-safe") { + let memoryPointer := mload(0x40) + let commitmentsLength := mload(commitments) + let commitmentsContent := add(commitments, 0x20) + let commitmentHashes := add(memoryPointer, 0x80) // leave space for typehash, lockTag, token and amount + mstore(memoryPointer, LOCK_TYPEHASH) + for { let i := 0 } lt(i, commitmentsLength) { i := add(i, 1) } { + let commitmentOffset := mload(add(commitmentsContent, mul(i, 0x20))) + mcopy(add(memoryPointer, 0x20), commitmentOffset, 0x60) // copy lockTag, token and amount to different memory + mstore(add(commitmentHashes, mul(i, 0x20)), keccak256(memoryPointer, 0x80)) + } + commitmentsHash := keccak256(commitmentHashes, mul(commitmentsLength, 0x20)) + } + } + function getClaimHash( address arbiter, address sponsor, @@ -462,6 +507,25 @@ library AllocatorLib { } } + function computeBatchCompactTypehash(string calldata witness) internal pure returns (bytes32 typeHash) { + assembly ("memory-safe") { + typeHash := BATCH_COMPACT_TYPEHASH + if witness.length { + let m := mload(0x40) + mstore(m, BATCH_COMPACT_TYPESTRING_FRAGMENT_ONE) + mstore(add(m, 0x20), BATCH_COMPACT_TYPESTRING_FRAGMENT_TWO) + mstore(add(m, 0x40), BATCH_COMPACT_TYPESTRING_FRAGMENT_THREE) + mstore(add(m, 0x60), BATCH_COMPACT_TYPESTRING_FRAGMENT_FOUR) + mstore(add(m, 0x88), BATCH_COMPACT_TYPESTRING_FRAGMENT_SIX) + mstore(add(m, 0x80), BATCH_COMPACT_TYPESTRING_FRAGMENT_FIVE) + let witnessStart := add(m, 0xa8) + calldatacopy(witnessStart, witness.offset, witness.length) + mstore8(add(witnessStart, witness.length), 0x29) // Closing parenthesis + typeHash := keccak256(m, add(0xa9, witness.length)) + } + } + } + function recoverSigner(bytes32 digest, bytes calldata signature) internal pure returns (address) { bytes32 r; bytes32 s; diff --git a/src/interfaces/IHybridAllocator.sol b/src/interfaces/IHybridAllocator.sol index fb2f917..ab5b4d5 100644 --- a/src/interfaces/IHybridAllocator.sol +++ b/src/interfaces/IHybridAllocator.sol @@ -2,6 +2,9 @@ pragma solidity ^0.8.27; import {IOnChainAllocation} from '@uniswap/the-compact/interfaces/IOnChainAllocation.sol'; +import {DepositDetails} from '@uniswap/the-compact/types/DepositDetails.sol'; +import {Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; +import {ISignatureTransfer} from 'permit2/src/interfaces/ISignatureTransfer.sol'; /// @title IHybridAllocator /// @notice Interface for hybrid allocators supporting both on-chain and off-chain authorization mechanisms @@ -57,4 +60,28 @@ interface IHybridAllocator is IOnChainAllocation { bytes32 typehash, bytes32 witness ) external payable returns (bytes32, uint256[] memory, uint256); + + /// @notice Deposits, registers and allocates a claim via Permit2 signature transfer + /// @dev Deposits the tokens subject to the order and registers the claim directly with the compact, then allocates the claim + /// @param arbiter The arbiter of the allocation + /// @param depositor The address depositing tokens and the sponsor of the claim (must sign the Permit2 message) + /// @param permitted The token permissions for the Permit2 transfer. Must match the commitments in the claim + /// @param details The deposit details including nonce, deadline, and lock tag + /// Nonce must match the nonce structure expected by the allocator + /// Deadline will be used as the expiration of the claim + /// @param claimHash The hash of the claim to register. Must match the claim hash recreated by the allocator + /// @param witness The witness typestring for the Permit2 signature (empty string if no witness) + /// @param witnessHash The hash of the witness data (bytes32(0) if no witness) + /// @param signature The Permit2 signature from the depositor, will be verified by the compact + /// @return commitments The lock commitments created by the allocation + function permit2Allocation( + address arbiter, + address depositor, + ISignatureTransfer.TokenPermissions[] calldata permitted, + DepositDetails calldata details, + bytes32 claimHash, + string calldata witness, + bytes32 witnessHash, + bytes calldata signature + ) external returns (Lock[] memory commitments); } diff --git a/src/interfaces/IOnChainAllocator.sol b/src/interfaces/IOnChainAllocator.sol index b79ff16..a87ae54 100644 --- a/src/interfaces/IOnChainAllocator.sol +++ b/src/interfaces/IOnChainAllocator.sol @@ -3,7 +3,9 @@ pragma solidity ^0.8.27; import {IOnChainAllocation} from '@uniswap/the-compact/interfaces/IOnChainAllocation.sol'; +import {DepositDetails} from '@uniswap/the-compact/types/DepositDetails.sol'; import {Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; +import {ISignatureTransfer} from 'permit2/src/interfaces/ISignatureTransfer.sol'; /// @title IOnChainAllocator /// @notice Interface for the on-chain token allocator that prevents double-spending in a fully decentralized manner @@ -95,4 +97,28 @@ interface IOnChainAllocator is IOnChainAllocation { bytes32 typehash, bytes32 witness ) external payable returns (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce); + + /// @notice Deposits, registers and allocates a claim via Permit2 signature transfer + /// @dev Deposits the tokens subject to the order and registers the claim directly with the compact, then allocates the claim + /// @param arbiter The arbiter of the allocation + /// @param depositor The address depositing tokens and the sponsor of the claim (must sign the Permit2 message) + /// @param permitted The token permissions for the Permit2 transfer. Must match the commitments in the claim + /// @param details The deposit details including nonce, deadline, and lock tag + /// Nonce must match the nonce structure expected by the allocator + /// Deadline will be used as the expiration of the claim + /// @param claimHash The hash of the claim to register. Must match the claim hash recreated by the allocator + /// @param witness The witness typestring for the Permit2 signature (empty string if no witness) + /// @param witnessHash The hash of the witness data (bytes32(0) if no witness) + /// @param signature The Permit2 signature from the depositor, will be verified by the compact + /// @return commitments The lock commitments created by the allocation + function permit2Allocation( + address arbiter, + address depositor, + ISignatureTransfer.TokenPermissions[] calldata permitted, + DepositDetails calldata details, + bytes32 claimHash, + string calldata witness, + bytes32 witnessHash, + bytes calldata signature + ) external returns (Lock[] memory commitments); } diff --git a/test/HybridAllocator.t.sol b/test/HybridAllocator.t.sol index 80e80ee..b550d6e 100644 --- a/test/HybridAllocator.t.sol +++ b/test/HybridAllocator.t.sol @@ -1616,7 +1616,8 @@ contract HybridAllocatorTest is Test, TestHelper { bytes memory signature = _createPermit2Signature(permitted, details, claimHash, userPrivateKey); // Execute - Lock[] memory commitments = allocator.permit2Allocation(user, permitted, details, claimHash, '', signature); + Lock[] memory commitments = + allocator.permit2Allocation(arbiter, user, permitted, details, claimHash, '', bytes32(0), signature); vm.snapshotGasLastCall('hybrid_permit2Allocation_singleERC20'); // Verify commitments @@ -1654,7 +1655,7 @@ contract HybridAllocatorTest is Test, TestHelper { // Create a witness value - using a simple Mandate(uint256 witness) struct // The witness is the hash of the witness struct: keccak256(abi.encode(WITNESS_TYPEHASH, witnessValue)) - uint256 witnessValue = 12345; + uint256 witnessValue = 12_345; bytes32 witness = keccak256(abi.encode(WITNESS_TYPEHASH, witnessValue)); // Compute claimHash WITH witness using BATCH_COMPACT_TYPEHASH_WITH_WITNESS @@ -1677,8 +1678,9 @@ contract HybridAllocatorTest is Test, TestHelper { // Execute with witness typestring // Note: The witness parameter should be just the inner content (e.g., "uint256 witness"), // not the full struct definition, as TheCompact wraps it in "Mandate(...)" - Lock[] memory commitments = - allocator.permit2Allocation(user, permitted, details, claimHash, WITNESS_STRING, signature); + Lock[] memory commitments = allocator.permit2Allocation( + arbiter, user, permitted, details, claimHash, WITNESS_STRING, witness, signature + ); vm.snapshotGasLastCall('hybrid_permit2Allocation_singleERC20_withWitness'); // Verify commitments @@ -1740,7 +1742,8 @@ contract HybridAllocatorTest is Test, TestHelper { bytes memory signature = _createPermit2Signature(permitted, details, claimHash, userPrivateKey); // Execute - Lock[] memory commitments = allocator.permit2Allocation(user, permitted, details, claimHash, '', signature); + Lock[] memory commitments = + allocator.permit2Allocation(arbiter, user, permitted, details, claimHash, '', bytes32(0), signature); vm.snapshotGasLastCall('hybrid_permit2Allocation_multipleERC20'); // Verify commitments @@ -1786,7 +1789,7 @@ contract HybridAllocatorTest is Test, TestHelper { // Should revert with UnauthorizedNonce because command is not PERMIT2_NONCE vm.expectRevert(abi.encodeWithSelector(AllocatorLib.UnauthorizedNonce.selector, bytes1(0x01), user)); - allocator.permit2Allocation(user, permitted, details, claimHash, '', signature); + allocator.permit2Allocation(arbiter, user, permitted, details, claimHash, '', bytes32(0), signature); } function test_permit2Allocation_revert_invalidNonceSponsor() public { @@ -1808,7 +1811,7 @@ contract HybridAllocatorTest is Test, TestHelper { // Should revert because sponsor in nonce doesn't match depositor vm.expectRevert(abi.encodeWithSelector(AllocatorLib.UnauthorizedNonce.selector, bytes1(0x03), wrongSponsor)); - allocator.permit2Allocation(user, permitted, details, claimHash, '', signature); + allocator.permit2Allocation(arbiter, user, permitted, details, claimHash, '', bytes32(0), signature); } function test_permit2Allocation_emitsAllocatedEvent() public { @@ -1848,7 +1851,7 @@ contract HybridAllocatorTest is Test, TestHelper { vm.expectEmit(true, true, true, true); emit IOnChainAllocation.Allocated(user, expectedCommitments, nonce, defaultExpiration, claimHash); - allocator.permit2Allocation(user, permitted, details, claimHash, '', signature); + allocator.permit2Allocation(arbiter, user, permitted, details, claimHash, '', bytes32(0), signature); } function test_permit2Allocation_fullClaimFlow() public { @@ -1883,7 +1886,7 @@ contract HybridAllocatorTest is Test, TestHelper { bytes memory signature = _createPermit2Signature(permitted, details, claimHash, userPrivateKey); // Execute permit2Allocation - allocator.permit2Allocation(user, permitted, details, claimHash, '', signature); + allocator.permit2Allocation(arbiter, user, permitted, details, claimHash, '', bytes32(0), signature); // Verify claim is authorized assertTrue(allocator.isClaimAuthorized(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); diff --git a/test/OnChainAllocator.t.sol b/test/OnChainAllocator.t.sol index 8ff93b8..be3ec62 100644 --- a/test/OnChainAllocator.t.sol +++ b/test/OnChainAllocator.t.sol @@ -2404,7 +2404,8 @@ contract OnChainAllocatorTest is Test, TestHelper { bytes memory signature = _createPermit2Signature(permitted, details, claimHash, userPK); // Execute - Lock[] memory commitments = allocator.permit2Allocation(user, permitted, details, claimHash, '', signature); + Lock[] memory commitments = + allocator.permit2Allocation(arbiter, user, permitted, details, claimHash, '', bytes32(0), signature); vm.snapshotGasLastCall('onchain_permit2Allocation_singleERC20'); // Verify commitments @@ -2469,7 +2470,8 @@ contract OnChainAllocatorTest is Test, TestHelper { bytes memory signature = _createPermit2Signature(permitted, details, claimHash, userPK); // Execute - Lock[] memory commitments = allocator.permit2Allocation(user, permitted, details, claimHash, '', signature); + Lock[] memory commitments = + allocator.permit2Allocation(arbiter, user, permitted, details, claimHash, '', bytes32(0), signature); vm.snapshotGasLastCall('onchain_permit2Allocation_multipleERC20'); // Verify commitments @@ -2520,7 +2522,7 @@ contract OnChainAllocatorTest is Test, TestHelper { // Should revert with UnauthorizedNonce because command is not PERMIT2_NONCE vm.expectRevert(abi.encodeWithSelector(AllocatorLib.UnauthorizedNonce.selector, bytes1(0x01), user)); - allocator.permit2Allocation(user, permitted, details, claimHash, '', signature); + allocator.permit2Allocation(arbiter, user, permitted, details, claimHash, '', bytes32(0), signature); } function test_permit2Allocation_revert_invalidExpiration() public { @@ -2542,7 +2544,7 @@ contract OnChainAllocatorTest is Test, TestHelper { vm.expectRevert( abi.encodeWithSelector(IOnChainAllocator.InvalidExpiration.selector, invalidDeadline, type(uint32).max) ); - allocator.permit2Allocation(user, permitted, details, claimHash, '', signature); + allocator.permit2Allocation(arbiter, user, permitted, details, claimHash, '', bytes32(0), signature); } function test_permit2Allocation_revert_invalidAmount() public { @@ -2582,7 +2584,7 @@ contract OnChainAllocatorTest is Test, TestHelper { // Should revert because amount exceeds uint224 max vm.expectRevert(abi.encodeWithSelector(IOnChainAllocator.InvalidAmount.selector, largeAmount)); - allocator.permit2Allocation(user, permitted, details, claimHash, '', signature); + allocator.permit2Allocation(arbiter, user, permitted, details, claimHash, '', bytes32(0), signature); } function test_permit2Allocation_fullClaimFlow() public { @@ -2617,7 +2619,7 @@ contract OnChainAllocatorTest is Test, TestHelper { bytes memory signature = _createPermit2Signature(permitted, details, claimHash, userPK); // Execute permit2Allocation - allocator.permit2Allocation(user, permitted, details, claimHash, '', signature); + allocator.permit2Allocation(arbiter, user, permitted, details, claimHash, '', bytes32(0), signature); // Verify claim is authorized uint256[2][] memory idsAndAmounts = new uint256[2][](1); @@ -2688,7 +2690,7 @@ contract OnChainAllocatorTest is Test, TestHelper { bytes memory signature = _createPermit2Signature(permitted, details, claimHash, userPK); // Execute permit2Allocation - allocator.permit2Allocation(user, permitted, details, claimHash, '', signature); + allocator.permit2Allocation(arbiter, user, permitted, details, claimHash, '', bytes32(0), signature); // Verify claim is authorized before expiration uint256[2][] memory idsAndAmounts = new uint256[2][](1); @@ -2733,7 +2735,7 @@ contract OnChainAllocatorTest is Test, TestHelper { bytes memory signature = _createPermit2Signature(permitted, details, claimHash, userPK); // Execute permit2Allocation - allocator.permit2Allocation(user, permitted, details, claimHash, '', signature); + allocator.permit2Allocation(arbiter, user, permitted, details, claimHash, '', bytes32(0), signature); // Try to transfer - should fail because tokens are allocated vm.prank(user); From 5b7cba939ad30cce1fe6f0bb4bf7be1e824d921f Mon Sep 17 00:00:00 2001 From: mgretzke Date: Fri, 5 Dec 2025 13:17:59 +0100 Subject: [PATCH 5/9] removed allocation router --- src/routers/AllocationRouter.sol | 150 -------- test/AllocationRouter.t.sol | 613 ------------------------------- 2 files changed, 763 deletions(-) delete mode 100644 src/routers/AllocationRouter.sol delete mode 100644 test/AllocationRouter.t.sol diff --git a/src/routers/AllocationRouter.sol b/src/routers/AllocationRouter.sol deleted file mode 100644 index e6e653f..0000000 --- a/src/routers/AllocationRouter.sol +++ /dev/null @@ -1,150 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.27; - -import {AllocatorLib as AL} from '../allocators/lib/AllocatorLib.sol'; -import {ISignatureTransfer} from 'permit2/src/interfaces/ISignatureTransfer.sol'; -import {IOnChainAllocation} from 'the-compact/src/interfaces/IOnChainAllocation.sol'; -import {ITheCompact} from 'the-compact/src/interfaces/ITheCompact.sol'; -import {CompactCategory} from 'the-compact/src/types/CompactCategory.sol'; -import {DepositDetails} from 'the-compact/src/types/DepositDetails.sol'; -import { - BATCH_COMPACT_TYPEHASH, - BATCH_COMPACT_TYPESTRING_FRAGMENT_FIVE, - BATCH_COMPACT_TYPESTRING_FRAGMENT_FOUR, - BATCH_COMPACT_TYPESTRING_FRAGMENT_ONE, - BATCH_COMPACT_TYPESTRING_FRAGMENT_SIX, - BATCH_COMPACT_TYPESTRING_FRAGMENT_THREE, - BATCH_COMPACT_TYPESTRING_FRAGMENT_TWO, - LOCK_TYPEHASH -} from 'the-compact/src/types/EIP712Types.sol'; - -/// @title AllocationRouter -/// @notice Router for depositing, registering and allocating tokens to a given address in a single transaction. -contract AllocationRouter { - // Storage slot seed for mapping allocator IDs to allocator addresses - uint256 private constant _ALLOCATOR_BY_ALLOCATOR_ID_SLOT_SEED = 0x000044036fc77deaed2300000000000000000000000; - - function depositRegisterAndAllocate( - address sponsor, - address arbiter, - ISignatureTransfer.TokenPermissions[] calldata permitted, - DepositDetails calldata details, - bytes32 witnessHash, - string calldata witness, - bytes calldata signature - ) external payable { - // Get the allocator address directly from the compact and revert if allocator is address(0) - address allocator = AL.getRegisteredAllocator(AL.splitAllocatorId(details.lockTag)); - - // Prepare the ids and amounts - uint256[2][] memory idsAndAmounts = new uint256[2][](permitted.length); - bytes32[] memory commitmentHashes = new bytes32[](permitted.length); - for (uint256 i = 0; i < permitted.length; i++) { - uint256 id = AL.toId(details.lockTag, permitted[i].token); - idsAndAmounts[i][0] = id; - idsAndAmounts[i][1] = permitted[i].amount; - - // Lock struct encoding: (bytes12 lockTag, address token, uint256 amount) - commitmentHashes[i] = - keccak256(abi.encode(LOCK_TYPEHASH, details.lockTag, permitted[i].token, permitted[i].amount)); - } - - bytes32 typeHash = _computeTypehash(witness); - - // Prepare allocation - uint256 nonce = IOnChainAllocation(allocator).prepareAllocation( - sponsor, idsAndAmounts, arbiter, details.deadline, typeHash, witnessHash, '' - ); - // The signature MUST validate a claim hash with the allocators nonce. The nonce can be different from details.nonce - - bytes32 claimHash = _computeClaimHash(typeHash, arbiter, sponsor, nonce, commitmentHashes, witnessHash, witness); - - // Deposit and register the tokens using permit2 - ITheCompact(AL.THE_COMPACT).batchDepositAndRegisterViaPermit2{value: msg.value}( - sponsor, permitted, details, claimHash, CompactCategory.BatchCompact, witness, signature - ); - - // Execute allocation - IOnChainAllocation(allocator).executeAllocation( - sponsor, idsAndAmounts, arbiter, details.deadline, typeHash, witnessHash, '' - ); - } - - function depositRegisterAndAllocate( - address sponsor, - address arbiter, - ISignatureTransfer.TokenPermissions[] calldata permitted, - DepositDetails calldata details, - bytes32 witnessHash, - string calldata witness, - bytes calldata signature, - bytes32 claimHash, - address allocator, - bytes32 typeHash - ) external payable { - // Prepare the ids and amounts - uint256[2][] memory idsAndAmounts = new uint256[2][](permitted.length); - for (uint256 i = 0; i < permitted.length; i++) { - idsAndAmounts[i][0] = AL.toId(details.lockTag, permitted[i].token); - idsAndAmounts[i][1] = permitted[i].amount; - } - - // Prepare allocation - IOnChainAllocation(allocator).prepareAllocation( - sponsor, idsAndAmounts, arbiter, details.deadline, typeHash, witnessHash, '' - ); - // The signature MUST validate a claim hash with the allocators nonce. The nonce can be different from details.nonce - - // Deposit and register the tokens using permit2 - ITheCompact(AL.THE_COMPACT).batchDepositAndRegisterViaPermit2{value: msg.value}( - sponsor, permitted, details, claimHash, CompactCategory.BatchCompact, witness, signature - ); - - // Execute allocation - IOnChainAllocation(allocator).executeAllocation( - sponsor, idsAndAmounts, arbiter, details.deadline, typeHash, witnessHash, '' - ); - } - - function _computeTypehash(string calldata witness) internal pure returns (bytes32 typeHash) { - assembly ("memory-safe") { - typeHash := BATCH_COMPACT_TYPEHASH - if witness.length { - let m := mload(0x40) - mstore(m, BATCH_COMPACT_TYPESTRING_FRAGMENT_ONE) - mstore(add(m, 0x20), BATCH_COMPACT_TYPESTRING_FRAGMENT_TWO) - mstore(add(m, 0x40), BATCH_COMPACT_TYPESTRING_FRAGMENT_THREE) - mstore(add(m, 0x60), BATCH_COMPACT_TYPESTRING_FRAGMENT_FOUR) - mstore(add(m, 0x88), BATCH_COMPACT_TYPESTRING_FRAGMENT_SIX) - mstore(add(m, 0x80), BATCH_COMPACT_TYPESTRING_FRAGMENT_FIVE) - let witnessStart := add(m, 0xa8) - calldatacopy(witnessStart, witness.offset, witness.length) - mstore8(add(witnessStart, witness.length), 0x29) // Closing parenthesis - typeHash := keccak256(m, add(0xa9, witness.length)) - } - } - } - - function _computeClaimHash( - bytes32 typeHash, - address arbiter, - address sponsor, - uint256 nonce, - bytes32[] memory commitmentHashes, - bytes32 witnessHash, - string calldata witness - ) internal pure returns (bytes32 claimHash) { - assembly ("memory-safe") { - let m := mload(0x40) - mstore(m, typeHash) - mstore(add(m, 0x20), arbiter) - mstore(add(m, 0x40), sponsor) - mstore(add(m, 0x60), nonce) - calldatacopy(add(m, 0x80), 0x84, 0x20) // details.deadline - mstore(add(m, 0xa0), keccak256(add(commitmentHashes, 0x20), mul(mload(commitmentHashes), 0x20))) // abi.encodePacked(commitmentHashes) - mstore(add(m, 0xc0), witnessHash) - claimHash := keccak256(m, sub(0xe0, mul(iszero(witness.length), 0x20))) // Exclude witnessHash for no-witness cases - } - } -} diff --git a/test/AllocationRouter.t.sol b/test/AllocationRouter.t.sol deleted file mode 100644 index 906cdfa..0000000 --- a/test/AllocationRouter.t.sol +++ /dev/null @@ -1,613 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.27; - -import {Test} from 'forge-std/Test.sol'; - -import {TheCompact} from '@uniswap/the-compact/TheCompact.sol'; -import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; -import {ISignatureTransfer} from 'permit2/src/interfaces/ISignatureTransfer.sol'; - -import {BatchClaim} from '@uniswap/the-compact/types/BatchClaims.sol'; -import {BatchClaimComponent, Component} from '@uniswap/the-compact/types/Components.sol'; -import {DepositDetails} from '@uniswap/the-compact/types/DepositDetails.sol'; -import { - BATCH_COMPACT_TYPEHASH, - BATCH_COMPACT_TYPESTRING_FRAGMENT_FIVE, - BATCH_COMPACT_TYPESTRING_FRAGMENT_FOUR, - BATCH_COMPACT_TYPESTRING_FRAGMENT_ONE, - BATCH_COMPACT_TYPESTRING_FRAGMENT_SIX, - BATCH_COMPACT_TYPESTRING_FRAGMENT_THREE, - BATCH_COMPACT_TYPESTRING_FRAGMENT_TWO, - Lock -} from '@uniswap/the-compact/types/EIP712Types.sol'; -import {ResetPeriod} from '@uniswap/the-compact/types/ResetPeriod.sol'; -import {Scope} from '@uniswap/the-compact/types/Scope.sol'; - -import {HybridAllocator} from 'src/allocators/HybridAllocator.sol'; - -import {AllocatorLib} from 'src/allocators/lib/AllocatorLib.sol'; -import {IHybridAllocator} from 'src/interfaces/IHybridAllocator.sol'; -import {AllocationRouter} from 'src/routers/AllocationRouter.sol'; -import {ERC20Mock} from 'src/test/ERC20Mock.sol'; - -import {DeployTheCompact} from 'test/util/DeployTheCompact.sol'; -import {TestHelper} from 'test/util/TestHelper.sol'; - -interface EIP712 { - function DOMAIN_SEPARATOR() external view returns (bytes32); -} - -contract AllocationRouterTest is Test, TestHelper { - TheCompact compact; - HybridAllocator allocator; - AllocationRouter router; - ERC20Mock usdc; - ERC20Mock dai; - - address arbiter; - address signer; - uint256 signerPrivateKey; - address sponsor; - uint256 sponsorPrivateKey; - - uint256 defaultAmount; - uint256 defaultExpiration; - - // Permit2 constants - address constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3; - bytes32 constant PERMIT2_DOMAIN_SEPARATOR_TYPEHASH = - keccak256('EIP712Domain(string name,uint256 chainId,address verifyingContract)'); - bytes32 constant TOKEN_PERMISSIONS_TYPEHASH = keccak256('TokenPermissions(address token,uint256 amount)'); - - // BatchActivation typehash without witness (from the-compact/src/types/EIP712Types.sol) - // keccak256(bytes("BatchActivation(address activator,uint256[] ids,BatchCompact compact)BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments)Lock(bytes12 lockTag,address token,uint256 amount)")) - bytes32 constant BATCH_COMPACT_BATCH_ACTIVATION_TYPEHASH = - 0xa794ed1a28cdf297ac45a3eee4643e35d29b295a389368da5f6baa420872c9b7; - - // PermitBatchWitnessTransferFrom typehash (no witness) - string constant PERMIT_BATCH_WITNESS_TYPESTRING = - 'PermitBatchWitnessTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline,BatchActivation witness)BatchActivation(address activator,uint256[] ids,BatchCompact compact)BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments)Lock(bytes12 lockTag,address token,uint256 amount)TokenPermissions(address token,uint256 amount)'; - bytes32 constant PERMIT_BATCH_WITNESS_TYPEHASH = keccak256(bytes(PERMIT_BATCH_WITNESS_TYPESTRING)); - - function setUp() public { - // Deploy TheCompact at hardcoded address - compact = DeployTheCompact(new DeployTheCompact()).deployTheCompact(); - assertEq(address(compact), AllocatorLib.THE_COMPACT); - - // Deploy Permit2 at the expected address - _deployPermit2(); - - // Setup accounts - arbiter = makeAddr('arbiter'); - (signer, signerPrivateKey) = makeAddrAndKey('signer'); - (sponsor, sponsorPrivateKey) = makeAddrAndKey('sponsor'); - - // Deploy allocator with signer - allocator = new HybridAllocator(signer); - - // Deploy router - router = new AllocationRouter(); - - // Deploy mock tokens - usdc = new ERC20Mock('USDC', 'USDC'); - dai = new ERC20Mock('DAI', 'DAI'); - - // Setup default values - defaultAmount = 1 ether; - defaultExpiration = block.timestamp + 1 days; - - // Fund sponsor - deal(sponsor, 10 ether); - usdc.mint(sponsor, 10 ether); - dai.mint(sponsor, 10 ether); - - // Approve Permit2 for tokens - vm.startPrank(sponsor); - usdc.approve(PERMIT2, type(uint256).max); - dai.approve(PERMIT2, type(uint256).max); - vm.stopPrank(); - } - - function _deployPermit2() internal { - // Deploy Permit2 using the same pattern as the-compact tests - address permit2Deployer = address(0x4e59b44847b379578588920cA78FbF26c0B4956C); - address permit2DeployerDeployer = address(0x3fAB184622Dc19b6109349B94811493BF2a45362); - bytes memory permit2DeployerCreationCode = - hex'604580600e600039806000f350fe7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3'; - - vm.deal(permit2DeployerDeployer, 1e18); - vm.prank(permit2DeployerDeployer); - address deployedPermit2Deployer; - assembly ("memory-safe") { - deployedPermit2Deployer := - create(0, add(permit2DeployerCreationCode, 0x20), mload(permit2DeployerCreationCode)) - } - - bytes memory permit2CreationCalldata = - hex'0000000000000000000000000000000000000000d3af2663da51c1021500000060c0346100bb574660a052602081017f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a86681527f9ac997416e8ff9d2ff6bebeb7149f65cdae5e32e2b90440b566bb3044041d36a60408301524660608301523060808301526080825260a082019180831060018060401b038411176100a557826040525190206080526123c090816100c1823960805181611b47015260a05181611b210152f35b634e487b7160e01b600052604160045260246000fd5b600080fdfe6040608081526004908136101561001557600080fd5b600090813560e01c80630d58b1db1461126c578063137c29fe146110755780632a2d80d114610db75780632b67b57014610bde57806330f28b7a14610ade5780633644e51514610a9d57806336c7851614610a285780633ff9dcb1146109a85780634fe02b441461093f57806365d9723c146107ac57806387517c451461067a578063927da105146105c3578063cc53287f146104a3578063edd9444b1461033a5763fe8ec1a7146100c657600080fd5b346103365760c07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff833581811161033257610114903690860161164b565b60243582811161032e5761012b903690870161161a565b6101336114e6565b9160843585811161032a5761014b9036908a016115c1565b98909560a43590811161032657610164913691016115c1565b969095815190610173826113ff565b606b82527f5065726d697442617463685769746e6573735472616e7366657246726f6d285460208301527f6f6b656e5065726d697373696f6e735b5d207065726d69747465642c61646472838301527f657373207370656e6465722c75696e74323536206e6f6e63652c75696e74323560608301527f3620646561646c696e652c000000000000000000000000000000000000000000608083015282519a8b9181610222602085018096611f93565b918237018a8152039961025b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09b8c8101835282611437565b5190209085515161026b81611ebb565b908a5b8181106102f95750506102f6999a6102ed9183516102a081610294602082018095611f66565b03848101835282611437565b519020602089810151858b015195519182019687526040820192909252336060820152608081019190915260a081019390935260643560c08401528260e081015b03908101835282611437565b51902093611cf7565b80f35b8061031161030b610321938c5161175e565b51612054565b61031b828661175e565b52611f0a565b61026e565b8880fd5b8780fd5b8480fd5b8380fd5b5080fd5b5091346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff9080358281116103325761038b903690830161164b565b60243583811161032e576103a2903690840161161a565b9390926103ad6114e6565b9160643590811161049f576103c4913691016115c1565b949093835151976103d489611ebb565b98885b81811061047d5750506102f697988151610425816103f9602082018095611f66565b037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101835282611437565b5190206020860151828701519083519260208401947ffcf35f5ac6a2c28868dc44c302166470266239195f02b0ee408334829333b7668652840152336060840152608083015260a082015260a081526102ed8161141b565b808b61031b8261049461030b61049a968d5161175e565b9261175e565b6103d7565b8680fd5b5082346105bf57602090817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103325780359067ffffffffffffffff821161032e576104f49136910161161a565b929091845b848110610504578580f35b8061051a610515600193888861196c565b61197c565b61052f84610529848a8a61196c565b0161197c565b3389528385528589209173ffffffffffffffffffffffffffffffffffffffff80911692838b528652868a20911690818a5285528589207fffffffffffffffffffffffff000000000000000000000000000000000000000081541690558551918252848201527f89b1add15eff56b3dfe299ad94e01f2b52fbcb80ae1a3baea6ae8c04cb2b98a4853392a2016104f9565b8280fd5b50346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610676816105ff6114a0565b936106086114c3565b6106106114e6565b73ffffffffffffffffffffffffffffffffffffffff968716835260016020908152848420928816845291825283832090871683528152919020549251938316845260a083901c65ffffffffffff169084015260d09190911c604083015281906060820190565b0390f35b50346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336576106b26114a0565b906106bb6114c3565b916106c46114e6565b65ffffffffffff926064358481169081810361032a5779ffffffffffff0000000000000000000000000000000000000000947fda9fa7c1b00402c17d0161b249b1ab8bbec047c5a52207b9c112deffd817036b94338a5260016020527fffffffffffff0000000000000000000000000000000000000000000000000000858b209873ffffffffffffffffffffffffffffffffffffffff809416998a8d5260205283878d209b169a8b8d52602052868c209486156000146107a457504216925b8454921697889360a01b16911617179055815193845260208401523392a480f35b905092610783565b5082346105bf5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576107e56114a0565b906107ee6114c3565b9265ffffffffffff604435818116939084810361032a57338852602091600183528489209673ffffffffffffffffffffffffffffffffffffffff80911697888b528452858a20981697888a5283528489205460d01c93848711156109175761ffff9085840316116108f05750907f55eb90d810e1700b35a8e7e25395ff7f2b2259abd7415ca2284dfb1c246418f393929133895260018252838920878a528252838920888a5282528389209079ffffffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffff000000000000000000000000000000000000000000000000000083549260d01b16911617905582519485528401523392a480f35b84517f24d35a26000000000000000000000000000000000000000000000000000000008152fd5b5084517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b503461033657807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336578060209273ffffffffffffffffffffffffffffffffffffffff61098f6114a0565b1681528084528181206024358252845220549051908152f35b5082346105bf57817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf577f3704902f963766a4e561bbaab6e6cdc1b1dd12f6e9e99648da8843b3f46b918d90359160243533855284602052818520848652602052818520818154179055815193845260208401523392a280f35b8234610a9a5760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610a9a57610a606114a0565b610a686114c3565b610a706114e6565b6064359173ffffffffffffffffffffffffffffffffffffffff8316830361032e576102f6936117a1565b80fd5b503461033657817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657602090610ad7611b1e565b9051908152f35b508290346105bf576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf57610b1a3661152a565b90807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c36011261033257610b4c611478565b9160e43567ffffffffffffffff8111610bda576102f694610b6f913691016115c1565b939092610b7c8351612054565b6020840151828501519083519260208401947f939c21a48a8dbe3a9a2404a1d46691e4d39f6583d6ec6b35714604c986d801068652840152336060840152608083015260a082015260a08152610bd18161141b565b51902091611c25565b8580fd5b509134610336576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610c186114a0565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc360160c08112610332576080855191610c51836113e3565b1261033257845190610c6282611398565b73ffffffffffffffffffffffffffffffffffffffff91602435838116810361049f578152604435838116810361049f57602082015265ffffffffffff606435818116810361032a5788830152608435908116810361049f576060820152815260a435938285168503610bda576020820194855260c4359087830182815260e43567ffffffffffffffff811161032657610cfe90369084016115c1565b929093804211610d88575050918591610d786102f6999a610d7e95610d238851611fbe565b90898c511690519083519260208401947ff3841cd1ff0085026a6327b620b67997ce40f282c88a8e905a7a5626e310f3d086528401526060830152608082015260808152610d70816113ff565b519020611bd9565b916120c7565b519251169161199d565b602492508a51917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b5091346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc93818536011261033257610df36114a0565b9260249081359267ffffffffffffffff9788851161032a578590853603011261049f578051978589018981108282111761104a578252848301358181116103265785019036602383011215610326578382013591610e50836115ef565b90610e5d85519283611437565b838252602093878584019160071b83010191368311611046578801905b828210610fe9575050508a526044610e93868801611509565b96838c01978852013594838b0191868352604435908111610fe557610ebb90369087016115c1565b959096804211610fba575050508998995151610ed681611ebb565b908b5b818110610f9757505092889492610d7892610f6497958351610f02816103f98682018095611f66565b5190209073ffffffffffffffffffffffffffffffffffffffff9a8b8b51169151928551948501957faf1b0d30d2cab0380e68f0689007e3254993c596f2fdd0aaa7f4d04f794408638752850152830152608082015260808152610d70816113ff565b51169082515192845b848110610f78578580f35b80610f918585610f8b600195875161175e565b5161199d565b01610f6d565b80610311610fac8e9f9e93610fb2945161175e565b51611fbe565b9b9a9b610ed9565b8551917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b8a80fd5b6080823603126110465785608091885161100281611398565b61100b85611509565b8152611018838601611509565b838201526110278a8601611607565b8a8201528d611037818701611607565b90820152815201910190610e7a565b8c80fd5b84896041867f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b5082346105bf576101407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576110b03661152a565b91807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c360112610332576110e2611478565b67ffffffffffffffff93906101043585811161049f5761110590369086016115c1565b90936101243596871161032a57611125610bd1966102f6983691016115c1565b969095825190611134826113ff565b606482527f5065726d69745769746e6573735472616e7366657246726f6d28546f6b656e5060208301527f65726d697373696f6e73207065726d69747465642c6164647265737320737065848301527f6e6465722c75696e74323536206e6f6e63652c75696e7432353620646561646c60608301527f696e652c0000000000000000000000000000000000000000000000000000000060808301528351948591816111e3602085018096611f93565b918237018b8152039361121c7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe095868101835282611437565b5190209261122a8651612054565b6020878101518589015195519182019687526040820192909252336060820152608081019190915260a081019390935260e43560c08401528260e081016102e1565b5082346105bf576020807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033257813567ffffffffffffffff92838211610bda5736602383011215610bda5781013592831161032e576024906007368386831b8401011161049f57865b8581106112e5578780f35b80821b83019060807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc83360301126103265761139288876001946060835161132c81611398565b611368608461133c8d8601611509565b9485845261134c60448201611509565b809785015261135d60648201611509565b809885015201611509565b918291015273ffffffffffffffffffffffffffffffffffffffff80808093169516931691166117a1565b016112da565b6080810190811067ffffffffffffffff8211176113b457604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6060810190811067ffffffffffffffff8211176113b457604052565b60a0810190811067ffffffffffffffff8211176113b457604052565b60c0810190811067ffffffffffffffff8211176113b457604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff8211176113b457604052565b60c4359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b600080fd5b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6024359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6044359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc01906080821261149b576040805190611563826113e3565b8082941261149b57805181810181811067ffffffffffffffff8211176113b457825260043573ffffffffffffffffffffffffffffffffffffffff8116810361149b578152602435602082015282526044356020830152606435910152565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020838186019501011161149b57565b67ffffffffffffffff81116113b45760051b60200190565b359065ffffffffffff8216820361149b57565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020808501948460061b01011161149b57565b91909160608184031261149b576040805191611666836113e3565b8294813567ffffffffffffffff9081811161149b57830182601f8201121561149b578035611693816115ef565b926116a087519485611437565b818452602094858086019360061b8501019381851161149b579086899897969594939201925b8484106116e3575050505050855280820135908501520135910152565b90919293949596978483031261149b578851908982019082821085831117611730578a928992845261171487611509565b81528287013583820152815201930191908897969594936116c6565b602460007f4e487b710000000000000000000000000000000000000000000000000000000081526041600452fd5b80518210156117725760209160051b010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b92919273ffffffffffffffffffffffffffffffffffffffff604060008284168152600160205282828220961695868252602052818120338252602052209485549565ffffffffffff8760a01c16804211611884575082871696838803611812575b5050611810955016926118b5565b565b878484161160001461184f57602488604051907ff96fb0710000000000000000000000000000000000000000000000000000000082526004820152fd5b7fffffffffffffffffffffffff000000000000000000000000000000000000000084846118109a031691161790553880611802565b602490604051907fd81b2f2e0000000000000000000000000000000000000000000000000000000082526004820152fd5b9060006064926020958295604051947f23b872dd0000000000000000000000000000000000000000000000000000000086526004860152602485015260448401525af13d15601f3d116001600051141617161561190e57565b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601460248201527f5452414e534645525f46524f4d5f4641494c45440000000000000000000000006044820152fd5b91908110156117725760061b0190565b3573ffffffffffffffffffffffffffffffffffffffff8116810361149b5790565b9065ffffffffffff908160608401511673ffffffffffffffffffffffffffffffffffffffff908185511694826020820151169280866040809401511695169560009187835260016020528383208984526020528383209916988983526020528282209184835460d01c03611af5579185611ace94927fc6a377bfc4eb120024a8ac08eef205be16b817020812c73223e81d1bdb9708ec98979694508715600014611ad35779ffffffffffff00000000000000000000000000000000000000009042165b60a01b167fffffffffffff00000000000000000000000000000000000000000000000000006001860160d01b1617179055519384938491604091949373ffffffffffffffffffffffffffffffffffffffff606085019616845265ffffffffffff809216602085015216910152565b0390a4565b5079ffffffffffff000000000000000000000000000000000000000087611a60565b600484517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b467f000000000000000000000000000000000000000000000000000000000000000003611b69577f000000000000000000000000000000000000000000000000000000000000000090565b60405160208101907f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a86682527f9ac997416e8ff9d2ff6bebeb7149f65cdae5e32e2b90440b566bb3044041d36a604082015246606082015230608082015260808152611bd3816113ff565b51902090565b611be1611b1e565b906040519060208201927f190100000000000000000000000000000000000000000000000000000000000084526022830152604282015260428152611bd381611398565b9192909360a435936040840151804211611cc65750602084510151808611611c955750918591610d78611c6594611c60602088015186611e47565b611bd9565b73ffffffffffffffffffffffffffffffffffffffff809151511692608435918216820361149b57611810936118b5565b602490604051907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b602490604051907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b959093958051519560409283830151804211611e175750848803611dee57611d2e918691610d7860209b611c608d88015186611e47565b60005b868110611d42575050505050505050565b611d4d81835161175e565b5188611d5a83878a61196c565b01359089810151808311611dbe575091818888886001968596611d84575b50505050505001611d31565b611db395611dad9273ffffffffffffffffffffffffffffffffffffffff6105159351169561196c565b916118b5565b803888888883611d78565b6024908651907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b600484517fff633a38000000000000000000000000000000000000000000000000000000008152fd5b6024908551907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b9073ffffffffffffffffffffffffffffffffffffffff600160ff83161b9216600052600060205260406000209060081c6000526020526040600020818154188091551615611e9157565b60046040517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b90611ec5826115ef565b611ed26040519182611437565b8281527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0611f0082946115ef565b0190602036910137565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114611f375760010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b805160208092019160005b828110611f7f575050505090565b835185529381019392810192600101611f71565b9081519160005b838110611fab575050016000815290565b8060208092840101518185015201611f9a565b60405160208101917f65626cad6cb96493bf6f5ebea28756c966f023ab9e8a83a7101849d5573b3678835273ffffffffffffffffffffffffffffffffffffffff8082511660408401526020820151166060830152606065ffffffffffff9182604082015116608085015201511660a082015260a0815260c0810181811067ffffffffffffffff8211176113b45760405251902090565b6040516020808201927f618358ac3db8dc274f0cd8829da7e234bd48cd73c4a740aede1adec9846d06a1845273ffffffffffffffffffffffffffffffffffffffff81511660408401520151606082015260608152611bd381611398565b919082604091031261149b576020823592013590565b6000843b61222e5750604182036121ac576120e4828201826120b1565b939092604010156117725760209360009360ff6040608095013560f81c5b60405194855216868401526040830152606082015282805260015afa156121a05773ffffffffffffffffffffffffffffffffffffffff806000511691821561217657160361214c57565b60046040517f815e1d64000000000000000000000000000000000000000000000000000000008152fd5b60046040517f8baa579f000000000000000000000000000000000000000000000000000000008152fd5b6040513d6000823e3d90fd5b60408203612204576121c0918101906120b1565b91601b7f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff84169360ff1c019060ff8211611f375760209360009360ff608094612102565b60046040517f4be6321b000000000000000000000000000000000000000000000000000000008152fd5b929391601f928173ffffffffffffffffffffffffffffffffffffffff60646020957fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0604051988997889687947f1626ba7e000000000000000000000000000000000000000000000000000000009e8f8752600487015260406024870152816044870152868601378b85828601015201168101030192165afa9081156123a857829161232a575b507fffffffff000000000000000000000000000000000000000000000000000000009150160361230057565b60046040517fb0669cbc000000000000000000000000000000000000000000000000000000008152fd5b90506020813d82116123a0575b8161234460209383611437565b810103126103365751907fffffffff0000000000000000000000000000000000000000000000000000000082168203610a9a57507fffffffff0000000000000000000000000000000000000000000000000000000090386122d4565b3d9150612337565b6040513d84823e3d90fdfea164736f6c6343000811000a'; - - (bool ok,) = permit2Deployer.call(permit2CreationCalldata); - require(ok && PERMIT2.code.length != 0, 'permit2 deployment failed'); - } - - /* ====================================================================== */ - /* Helper Functions */ - /* ====================================================================== */ - - function _getLockTag() internal view returns (bytes12) { - return _toLockTag(address(allocator), Scope.Multichain, ResetPeriod.TenMinutes); - } - - /// @dev Computes the typeHash the same way as AllocationRouter._depositRegisterAndAllocate - function _computeRouterTypeHash(string memory witness) internal pure returns (bytes32) { - return keccak256( - abi.encodePacked( - BATCH_COMPACT_TYPESTRING_FRAGMENT_ONE, - BATCH_COMPACT_TYPESTRING_FRAGMENT_TWO, - BATCH_COMPACT_TYPESTRING_FRAGMENT_THREE, - BATCH_COMPACT_TYPESTRING_FRAGMENT_FOUR, - BATCH_COMPACT_TYPESTRING_FRAGMENT_FIVE, - BATCH_COMPACT_TYPESTRING_FRAGMENT_SIX, - witness, - ')' - ) - ); - } - - function _getPermit2DomainSeparator() internal view returns (bytes32) { - return keccak256( - abi.encode(PERMIT2_DOMAIN_SEPARATOR_TYPEHASH, keccak256(bytes('Permit2')), block.chainid, PERMIT2) - ); - } - - function _createTokenPermissions(address token, uint256 amount) - internal - pure - returns (ISignatureTransfer.TokenPermissions[] memory) - { - ISignatureTransfer.TokenPermissions[] memory permitted = new ISignatureTransfer.TokenPermissions[](1); - permitted[0] = ISignatureTransfer.TokenPermissions({token: token, amount: amount}); - return permitted; - } - - function _createTokenPermissions2(address token1, uint256 amount1, address token2, uint256 amount2) - internal - pure - returns (ISignatureTransfer.TokenPermissions[] memory) - { - ISignatureTransfer.TokenPermissions[] memory permitted = new ISignatureTransfer.TokenPermissions[](2); - permitted[0] = ISignatureTransfer.TokenPermissions({token: token1, amount: amount1}); - permitted[1] = ISignatureTransfer.TokenPermissions({token: token2, amount: amount2}); - return permitted; - } - - function _createDepositDetails(uint256 nonce, uint256 deadline, bytes12 lockTag) - internal - pure - returns (DepositDetails memory) - { - return DepositDetails({nonce: nonce, deadline: deadline, lockTag: lockTag}); - } - - function _hashTokenPermissions(ISignatureTransfer.TokenPermissions[] memory permitted) - internal - pure - returns (bytes32) - { - bytes32[] memory hashes = new bytes32[](permitted.length); - for (uint256 i = 0; i < permitted.length; i++) { - hashes[i] = keccak256(abi.encode(TOKEN_PERMISSIONS_TYPEHASH, permitted[i].token, permitted[i].amount)); - } - return keccak256(abi.encodePacked(hashes)); - } - - function _createIds(bytes12 lockTag, address[] memory tokens) internal pure returns (uint256[] memory) { - uint256[] memory ids = new uint256[](tokens.length); - for (uint256 i = 0; i < tokens.length; i++) { - ids[i] = AllocatorLib.toId(lockTag, tokens[i]); - } - return ids; - } - - /// @dev Computes the Lock commitment hash with proper struct encoding - /// Lock struct: (bytes12 lockTag, address token, uint256 amount) - function _computeCommitmentHash(uint256 id, uint256 amount) internal pure returns (bytes32) { - bytes32 lockTypehash = keccak256('Lock(bytes12 lockTag,address token,uint256 amount)'); - bytes12 lockTag = bytes12(bytes32(id)); - address token = address(uint160(id)); - return keccak256(abi.encode(lockTypehash, lockTag, token, amount)); - } - - function _createPermit2Signature( - ISignatureTransfer.TokenPermissions[] memory permitted, - DepositDetails memory details, - bytes32 claimHash, - uint256 signerPk - ) internal view returns (bytes memory) { - bytes12 lockTag = details.lockTag; - - // Check if first token is native - bool hasNative = permitted.length > 0 && permitted[0].token == address(0); - - // Create ids array - includes ALL tokens (including native) - uint256[] memory ids = new uint256[](permitted.length); - for (uint256 i = 0; i < permitted.length; i++) { - ids[i] = AllocatorLib.toId(lockTag, permitted[i].token); - } - bytes32 idsHash = keccak256(abi.encodePacked(ids)); - - // Create activation hash using the exact same typehash TheCompact uses - // The activator is the router (msg.sender to TheCompact) - bytes32 activationHash = - keccak256(abi.encode(BATCH_COMPACT_BATCH_ACTIVATION_TYPEHASH, address(router), idsHash, claimHash)); - - // Create permit batch hash - tokenPermissionsHash only includes ERC20 tokens (not native) - ISignatureTransfer.TokenPermissions[] memory erc20Permitted; - if (hasNative) { - erc20Permitted = new ISignatureTransfer.TokenPermissions[](permitted.length - 1); - for (uint256 i = 0; i < erc20Permitted.length; i++) { - erc20Permitted[i] = permitted[i + 1]; - } - } else { - erc20Permitted = permitted; - } - bytes32 tokenPermissionsHash = _hashTokenPermissions(erc20Permitted); - bytes32 permitBatchHash = keccak256( - abi.encode( - PERMIT_BATCH_WITNESS_TYPEHASH, - tokenPermissionsHash, - address(compact), - details.nonce, - details.deadline, - activationHash - ) - ); - - // Create digest - bytes32 domainSeparator = _getPermit2DomainSeparator(); - bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), domainSeparator, permitBatchHash)); - - // Sign - (bytes32 r, bytes32 vs) = vm.signCompact(signerPk, digest); - return abi.encodePacked(r, vs); - } - - /* ====================================================================== */ - /* Tests for depositRegisterAndAllocate */ - /* ====================================================================== */ - - function test_depositRegisterAndAllocate_simple_singleERC20() public { - bytes12 lockTag = _getLockTag(); - uint256 nonce = 1; - - // Prepare token permissions - ISignatureTransfer.TokenPermissions[] memory permitted = _createTokenPermissions(address(usdc), defaultAmount); - - // Prepare deposit details - DepositDetails memory details = _createDepositDetails(nonce, defaultExpiration, lockTag); - - // Compute ids that the router will compute - uint256[] memory ids = new uint256[](1); - ids[0] = AllocatorLib.toId(lockTag, address(usdc)); - - // The router computes the claimHash internally using the nonce returned from prepareAllocation - // We need to predict what that nonce will be (allocator.nonces() + 1 = 0 + 1 = 1) - uint256 expectedNonce = 1; - - // Compute commitment hashes using proper Lock struct encoding - bytes32[] memory commitmentHashes = new bytes32[](1); - commitmentHashes[0] = _computeCommitmentHash(ids[0], defaultAmount); - - // For empty witness, router uses BATCH_COMPACT_TYPEHASH (no Mandate) - bytes32 typeHash = BATCH_COMPACT_TYPEHASH; - - // Compute claimHash WITHOUT witnessHash (empty witness case) - bytes32 claimHash = keccak256( - abi.encode( - typeHash, - arbiter, - sponsor, - expectedNonce, - details.deadline, - keccak256(abi.encodePacked(commitmentHashes)) - ) - ); - - // Create Permit2 signature - bytes memory signature = _createPermit2Signature(permitted, details, claimHash, sponsorPrivateKey); - - // Execute - router.depositRegisterAndAllocate(sponsor, arbiter, permitted, details, bytes32(0), '', signature); - vm.snapshotGasLastCall('depositRegisterAndAllocate_simple_singleERC20'); - - // Verify allocator nonce incremented - assertEq(allocator.nonces(), 1); - - // Verify claim is authorized - uint256[2][] memory idsAndAmounts = new uint256[2][](1); - idsAndAmounts[0][0] = ids[0]; - idsAndAmounts[0][1] = defaultAmount; - assertTrue( - allocator.isClaimAuthorized(claimHash, arbiter, sponsor, expectedNonce, details.deadline, idsAndAmounts, '') - ); - - // Verify tokens are in compact - assertEq(usdc.balanceOf(address(compact)), defaultAmount); - assertEq(compact.balanceOf(sponsor, ids[0]), defaultAmount); - } - - function test_depositRegisterAndAllocate_explicit_singleERC20() public { - bytes12 lockTag = _getLockTag(); - uint256 nonce = 1; - - // Prepare token permissions - ISignatureTransfer.TokenPermissions[] memory permitted = _createTokenPermissions(address(usdc), defaultAmount); - - // Prepare deposit details - DepositDetails memory details = _createDepositDetails(nonce, defaultExpiration, lockTag); - - // Compute ids - uint256[] memory ids = new uint256[](1); - ids[0] = AllocatorLib.toId(lockTag, address(usdc)); - - // Compute commitment hashes using proper Lock struct encoding - bytes32[] memory commitmentHashes = new bytes32[](1); - commitmentHashes[0] = _computeCommitmentHash(ids[0], defaultAmount); - - // For empty witness, use BATCH_COMPACT_TYPEHASH (no Mandate) - bytes32 typeHash = BATCH_COMPACT_TYPEHASH; - - // The explicit function uses the allocator's nonce after prepareAllocation increments it - // prepareAllocation returns nonces + 1 = 0 + 1 = 1 - uint256 expectedNonce = 1; - - // Compute claimHash WITHOUT witnessHash (since witness is empty, bytes32(0)) - // AllocatorLib.getClaimHash omits the witness when it's bytes32(0) - bytes32 claimHash = keccak256( - abi.encode( - typeHash, - arbiter, - sponsor, - expectedNonce, - details.deadline, - keccak256(abi.encodePacked(commitmentHashes)) - ) - ); - - // Create Permit2 signature - bytes memory signature = _createPermit2Signature(permitted, details, claimHash, sponsorPrivateKey); - - // Execute - router.depositRegisterAndAllocate( - sponsor, arbiter, permitted, details, bytes32(0), '', signature, claimHash, address(allocator), typeHash - ); - vm.snapshotGasLastCall('depositRegisterAndAllocate_explicit_singleERC20'); - - // Verify allocator nonce incremented - assertEq(allocator.nonces(), 1); - - // Verify claim is authorized - uint256[2][] memory idsAndAmounts = new uint256[2][](1); - idsAndAmounts[0][0] = ids[0]; - idsAndAmounts[0][1] = defaultAmount; - assertTrue( - allocator.isClaimAuthorized(claimHash, arbiter, sponsor, expectedNonce, details.deadline, idsAndAmounts, '') - ); - - // Verify tokens are in compact - assertEq(usdc.balanceOf(address(compact)), defaultAmount); - assertEq(compact.balanceOf(sponsor, ids[0]), defaultAmount); - } - - function test_depositRegisterAndAllocate_simple_multipleERC20() public { - bytes12 lockTag = _getLockTag(); - uint256 nonce = 1; - uint256 amount1 = defaultAmount; - uint256 amount2 = defaultAmount / 2; - - // Prepare token permissions (sorted by address) - ISignatureTransfer.TokenPermissions[] memory permitted; - uint256[] memory ids = new uint256[](2); - bytes32[] memory commitmentHashes = new bytes32[](2); - - if (uint160(address(usdc)) < uint160(address(dai))) { - permitted = _createTokenPermissions2(address(usdc), amount1, address(dai), amount2); - ids[0] = AllocatorLib.toId(lockTag, address(usdc)); - ids[1] = AllocatorLib.toId(lockTag, address(dai)); - commitmentHashes[0] = _computeCommitmentHash(ids[0], amount1); - commitmentHashes[1] = _computeCommitmentHash(ids[1], amount2); - } else { - permitted = _createTokenPermissions2(address(dai), amount2, address(usdc), amount1); - ids[0] = AllocatorLib.toId(lockTag, address(dai)); - ids[1] = AllocatorLib.toId(lockTag, address(usdc)); - commitmentHashes[0] = _computeCommitmentHash(ids[0], amount2); - commitmentHashes[1] = _computeCommitmentHash(ids[1], amount1); - } - - // Prepare deposit details - DepositDetails memory details = _createDepositDetails(nonce, defaultExpiration, lockTag); - - // Expected nonce from prepareAllocation - uint256 expectedNonce = 1; - - // For empty witness, router uses BATCH_COMPACT_TYPEHASH (no Mandate) - bytes32 typeHash = BATCH_COMPACT_TYPEHASH; - - // Compute claimHash WITHOUT witnessHash (empty witness case) - bytes32 claimHash = keccak256( - abi.encode( - typeHash, - arbiter, - sponsor, - expectedNonce, - details.deadline, - keccak256(abi.encodePacked(commitmentHashes)) - ) - ); - - // Create Permit2 signature - bytes memory signature = _createPermit2Signature(permitted, details, claimHash, sponsorPrivateKey); - - // Execute - router.depositRegisterAndAllocate(sponsor, arbiter, permitted, details, bytes32(0), '', signature); - vm.snapshotGasLastCall('depositRegisterAndAllocate_simple_multipleERC20'); - - // Verify allocator nonce incremented - assertEq(allocator.nonces(), 1); - - // Verify tokens are in compact - assertEq(usdc.balanceOf(address(compact)), amount1); - assertEq(dai.balanceOf(address(compact)), amount2); - } - - function test_depositRegisterAndAllocate_simple_nativeToken() public { - bytes12 lockTag = _getLockTag(); - uint256 nonce = 1; - - // Prepare token permissions with native token (address(0)) - // Native token must be first in the array for batchDepositAndRegisterViaPermit2 - ISignatureTransfer.TokenPermissions[] memory permitted = _createTokenPermissions(address(0), defaultAmount); - - // Prepare deposit details - DepositDetails memory details = _createDepositDetails(nonce, defaultExpiration, lockTag); - - // Compute ids - uint256[] memory ids = new uint256[](1); - ids[0] = AllocatorLib.toId(lockTag, address(0)); - - // Expected nonce from prepareAllocation - uint256 expectedNonce = 1; - - // Compute commitment hashes using proper Lock struct encoding - bytes32[] memory commitmentHashes = new bytes32[](1); - commitmentHashes[0] = _computeCommitmentHash(ids[0], defaultAmount); - - // For empty witness, use BATCH_COMPACT_TYPEHASH (no Mandate) - bytes32 typeHash = BATCH_COMPACT_TYPEHASH; - - // Compute claimHash WITHOUT witnessHash (empty witness case) - bytes32 claimHash = keccak256( - abi.encode( - typeHash, - arbiter, - sponsor, - expectedNonce, - details.deadline, - keccak256(abi.encodePacked(commitmentHashes)) - ) - ); - - // Create Permit2 signature - for native tokens, the idsHash in the activation - // still includes the native token id, so we pass the full permitted array - // (TheCompact strips the native token from the Permit2 call internally) - bytes memory signature = _createPermit2Signature(permitted, details, claimHash, sponsorPrivateKey); - - // Execute with value - need to use the explicit function which is payable - router.depositRegisterAndAllocate{value: defaultAmount}( - sponsor, arbiter, permitted, details, bytes32(0), '', signature, claimHash, address(allocator), typeHash - ); - vm.snapshotGasLastCall('depositRegisterAndAllocate_simple_nativeToken'); - - // Verify allocator nonce incremented - assertEq(allocator.nonces(), 1); - - // Verify native tokens are in compact - assertEq(address(compact).balance, defaultAmount); - assertEq(compact.balanceOf(sponsor, ids[0]), defaultAmount); - } - - /* ====================================================================== */ - /* Revert Tests */ - /* ====================================================================== */ - - function test_depositRegisterAndAllocate_revert_InvalidAllocator() public { - bytes12 invalidLockTag = bytes12(0); // Invalid lockTag with allocatorId = 0 - uint256 nonce = 1; - - // Prepare token permissions - ISignatureTransfer.TokenPermissions[] memory permitted = _createTokenPermissions(address(usdc), defaultAmount); - - // Prepare deposit details with invalid lockTag - DepositDetails memory details = _createDepositDetails(nonce, defaultExpiration, invalidLockTag); - - // Attempt to call should revert with InvalidAllocator - vm.expectRevert(AllocatorLib.InvalidAllocator.selector); - router.depositRegisterAndAllocate(sponsor, arbiter, permitted, details, bytes32(0), '', ''); - } - - /* ====================================================================== */ - /* Claim Flow Tests */ - /* ====================================================================== */ - - function test_depositRegisterAndAllocate_fullClaimFlow() public { - bytes12 lockTag = _getLockTag(); - uint256 nonce = 1; - - // Prepare token permissions - ISignatureTransfer.TokenPermissions[] memory permitted = _createTokenPermissions(address(usdc), defaultAmount); - - // Prepare deposit details - DepositDetails memory details = _createDepositDetails(nonce, defaultExpiration, lockTag); - - // Compute ids - uint256[] memory ids = new uint256[](1); - ids[0] = AllocatorLib.toId(lockTag, address(usdc)); - - // Expected nonce from prepareAllocation - uint256 expectedNonce = 1; - - // Compute commitment hashes using proper Lock struct encoding - bytes32[] memory commitmentHashes = new bytes32[](1); - commitmentHashes[0] = _computeCommitmentHash(ids[0], defaultAmount); - - // For empty witness, router uses BATCH_COMPACT_TYPEHASH (no Mandate) - bytes32 typeHash = BATCH_COMPACT_TYPEHASH; - - // Compute claimHash WITHOUT witnessHash (empty witness case) - bytes32 claimHash = keccak256( - abi.encode( - typeHash, - arbiter, - sponsor, - expectedNonce, - details.deadline, - keccak256(abi.encodePacked(commitmentHashes)) - ) - ); - - // Create Permit2 signature - bytes memory signature = _createPermit2Signature(permitted, details, claimHash, sponsorPrivateKey); - - // Execute deposit - router.depositRegisterAndAllocate(sponsor, arbiter, permitted, details, bytes32(0), '', signature); - - // Now execute the claim - address recipient = makeAddr('recipient'); - Component[] memory portions = new Component[](1); - portions[0] = - Component({claimant: uint256(bytes32(abi.encodePacked(bytes12(0), recipient))), amount: defaultAmount}); - - BatchClaimComponent[] memory claims = new BatchClaimComponent[](1); - claims[0] = BatchClaimComponent({id: ids[0], allocatedAmount: defaultAmount, portions: portions}); - - BatchClaim memory claim = BatchClaim({ - allocatorData: '', - sponsorSignature: '', - sponsor: sponsor, - nonce: expectedNonce, - expires: details.deadline, - witness: bytes32(0), - witnessTypestring: '', - claims: claims - }); - - // Execute claim - vm.prank(arbiter); - bytes32 returnedClaimHash = compact.batchClaim(claim); - assertEq(returnedClaimHash, claimHash); - - // Verify tokens transferred - assertEq(usdc.balanceOf(recipient), defaultAmount); - assertEq(usdc.balanceOf(address(compact)), 0); - } -} From 1ed1b77417c3bdc206e1cd3e248f6fd21fe044cc Mon Sep 17 00:00:00 2001 From: mgretzke Date: Tue, 9 Dec 2025 11:36:41 +0100 Subject: [PATCH 6/9] removed unused interface --- lib/the-compact | 2 +- src/interfaces/IAllocationRouter.sol | 51 ---------------------------- 2 files changed, 1 insertion(+), 52 deletions(-) delete mode 100644 src/interfaces/IAllocationRouter.sol diff --git a/lib/the-compact b/lib/the-compact index dff998d..c5c9ad7 160000 --- a/lib/the-compact +++ b/lib/the-compact @@ -1 +1 @@ -Subproject commit dff998d4539ad0ef50c7cdd18d51b3d1bd457ab2 +Subproject commit c5c9ad7aec1f4783f43f26187f7a0dbaa9c65078 diff --git a/src/interfaces/IAllocationRouter.sol b/src/interfaces/IAllocationRouter.sol deleted file mode 100644 index 92a6684..0000000 --- a/src/interfaces/IAllocationRouter.sol +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.27; - -import {ISignatureTransfer} from 'permit2/src/interfaces/ISignatureTransfer.sol'; -import {DepositDetails} from 'the-compact/src/types/DepositDetails.sol'; - -interface IAllocationRouter { - /// @notice Deposits, registers and allocates tokens to a given address in a single transaction. - /// @dev Calculates claim hash, type hash and allocator address internally. - /// @param sponsor The address of the sponsor. - /// @param arbiter The address of the arbiter. - /// @param permitted The array of token permissions. - /// @param details Includes the nonce, deadline and lock tag. - /// @param witnessHash The hash of the witness. - /// @param witness The witness string. - /// @param signature The signature of the permit2 transfer. - function depositRegisterAndAllocate( - address sponsor, - address arbiter, - ISignatureTransfer.TokenPermissions[] calldata permitted, - DepositDetails calldata details, - bytes32 witnessHash, - string calldata witness, - bytes calldata signature - ) external payable; - - /// @notice Deposits, registers and allocates tokens to a given address in a single transaction. - /// @dev Uses provided claim hash, type hash and allocator address. - /// @param sponsor The address of the sponsor. - /// @param arbiter The address of the arbiter. - /// @param permitted The array of token permissions. - /// @param details Includes the nonce, deadline and lock tag. - /// @param witnessHash The hash of the witness. - /// @param witness The witness string. - /// @param signature The signature of the permit2 transfer. - /// @param claimHash The hash of the claim registered in the compact. Claim nonce must match the expected allocator nonce. - /// @param allocator The address of the allocator contract. - /// @param typeHash The type hash of the batch compact. - function depositRegisterAndAllocate( - address sponsor, - address arbiter, - ISignatureTransfer.TokenPermissions[] calldata permitted, - DepositDetails calldata details, - bytes32 witnessHash, - string calldata witness, - bytes calldata signature, - bytes32 claimHash, - address allocator, - bytes32 typeHash - ) external payable; -} From 29247794387aabc72d405a74b7c1cac5b833aa96 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Thu, 11 Dec 2025 15:02:22 +0100 Subject: [PATCH 7/9] separate the claim expiration from the permit2 deadline --- lib/the-compact | 2 +- src/allocators/HybridAllocator.sol | 8 +++++--- src/allocators/OnChainAllocator.sol | 16 ++++++++++------ src/allocators/lib/AllocatorLib.sol | 5 +++-- src/interfaces/IHybridAllocator.sol | 1 + src/interfaces/IOnChainAllocator.sol | 1 + 6 files changed, 21 insertions(+), 12 deletions(-) diff --git a/lib/the-compact b/lib/the-compact index c5c9ad7..4118ca9 160000 --- a/lib/the-compact +++ b/lib/the-compact @@ -1 +1 @@ -Subproject commit c5c9ad7aec1f4783f43f26187f7a0dbaa9c65078 +Subproject commit 4118ca99876b14b261116069e7d3c137a0751e11 diff --git a/src/allocators/HybridAllocator.sol b/src/allocators/HybridAllocator.sol index 7662590..33b7a8a 100644 --- a/src/allocators/HybridAllocator.sol +++ b/src/allocators/HybridAllocator.sol @@ -190,6 +190,7 @@ contract HybridAllocator is IHybridAllocator { function permit2Allocation( address arbiter, address depositor, + uint256 expires, ISignatureTransfer.TokenPermissions[] calldata permitted, DepositDetails calldata details, bytes32 claimHash, @@ -197,13 +198,14 @@ contract HybridAllocator is IHybridAllocator { bytes32 witnessHash, bytes calldata signature ) external returns (Lock[] memory commitments) { - commitments = - AL.permit2Allocation(arbiter, depositor, permitted, details, claimHash, witness, witnessHash, signature); + commitments = AL.permit2Allocation( + arbiter, depositor, expires, permitted, details, claimHash, witness, witnessHash, signature + ); // Allocate the claim claims[claimHash] = true; - emit Allocated(depositor, commitments, details.nonce, details.deadline, claimHash); + emit Allocated(depositor, commitments, details.nonce, expires, claimHash); } /// @inheritdoc IOnChainAllocation diff --git a/src/allocators/OnChainAllocator.sol b/src/allocators/OnChainAllocator.sol index 5faf8f2..ba66522 100644 --- a/src/allocators/OnChainAllocator.sol +++ b/src/allocators/OnChainAllocator.sol @@ -247,6 +247,7 @@ contract OnChainAllocator is IOnChainAllocator, Utility { function permit2Allocation( address arbiter, address depositor, + uint256 expires, ISignatureTransfer.TokenPermissions[] calldata permitted, DepositDetails calldata details, bytes32 claimHash, @@ -254,12 +255,13 @@ contract OnChainAllocator is IOnChainAllocator, Utility { bytes32 witnessHash, bytes calldata signature ) external returns (Lock[] memory commitments) { - if (details.deadline > type(uint32).max) { - revert InvalidExpiration(details.deadline, type(uint32).max); + if (expires > type(uint32).max) { + revert InvalidExpiration(expires, type(uint32).max); } - commitments = - AL.permit2Allocation(arbiter, depositor, permitted, details, claimHash, witness, witnessHash, signature); + commitments = AL.permit2Allocation( + arbiter, depositor, expires, permitted, details, claimHash, witness, witnessHash, signature + ); // Allocate the claim for (uint256 i = 0; i < commitments.length; i++) { @@ -273,10 +275,12 @@ contract OnChainAllocator is IOnChainAllocator, Utility { commitments[i].token, uint224(commitments[i].amount), depositor, - uint32(details.deadline), // deadline is verified in the AllocatorLib.permit2Allocation function + uint32(expires), // expires is verified in the AllocatorLib.permit2Allocation function claimHash ); } + + emit Allocated(depositor, commitments, details.nonce, expires, claimHash); } /// @inheritdoc IOnChainAllocation @@ -332,7 +336,7 @@ contract OnChainAllocator is IOnChainAllocator, Utility { bytes32 typehash, bytes32 witness ) private returns (bytes32, Lock[] memory) { - (bytes32 claimHash, Lock[] memory commitments) = + (bytes32 claimHash, Lock[] memory commitments,) = AL.executeAllocation(uint248(nonce), recipient, idsAndAmounts, arbiter, expires, typehash, witness); // Allocate the claim diff --git a/src/allocators/lib/AllocatorLib.sol b/src/allocators/lib/AllocatorLib.sol index 16cf54f..3b7319f 100644 --- a/src/allocators/lib/AllocatorLib.sol +++ b/src/allocators/lib/AllocatorLib.sol @@ -69,6 +69,7 @@ library AllocatorLib { function permit2Allocation( address arbiter, address depositor, + uint256 expires, ISignatureTransfer.TokenPermissions[] calldata permitted, DepositDetails calldata details, bytes32 claimHash, // This claim hash is connected to the allocation. This does not guarantee, that the allocated tokens are connected to the claim hash. @@ -189,14 +190,14 @@ library AllocatorLib { mstore(0x40, m) // Restore the memory pointer } - // Verify the claim hash includes the permit2 deadline as expiration + // Verify the claim hash includes proposed expiration if ( claimHash != getClaimHash( arbiter, depositor, details.nonce, - details.deadline, + expires, getCommitmentsHashMemory(commitments), witnessHash, computeBatchCompactTypehash(witness) diff --git a/src/interfaces/IHybridAllocator.sol b/src/interfaces/IHybridAllocator.sol index ab5b4d5..3b0cd01 100644 --- a/src/interfaces/IHybridAllocator.sol +++ b/src/interfaces/IHybridAllocator.sol @@ -77,6 +77,7 @@ interface IHybridAllocator is IOnChainAllocation { function permit2Allocation( address arbiter, address depositor, + uint256 expires, ISignatureTransfer.TokenPermissions[] calldata permitted, DepositDetails calldata details, bytes32 claimHash, diff --git a/src/interfaces/IOnChainAllocator.sol b/src/interfaces/IOnChainAllocator.sol index a87ae54..71c6442 100644 --- a/src/interfaces/IOnChainAllocator.sol +++ b/src/interfaces/IOnChainAllocator.sol @@ -114,6 +114,7 @@ interface IOnChainAllocator is IOnChainAllocation { function permit2Allocation( address arbiter, address depositor, + uint256 expires, ISignatureTransfer.TokenPermissions[] calldata permitted, DepositDetails calldata details, bytes32 claimHash, From 37c260122368380463e987e184aa2ebce9b77f32 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Thu, 11 Dec 2025 21:23:01 +0100 Subject: [PATCH 8/9] Adding owner and demoting signer --- src/allocators/HybridAllocator.sol | 78 ++++++++++++++--------------- src/interfaces/IHybridAllocator.sol | 25 +++++++-- 2 files changed, 58 insertions(+), 45 deletions(-) diff --git a/src/allocators/HybridAllocator.sol b/src/allocators/HybridAllocator.sol index 33b7a8a..24ffe59 100644 --- a/src/allocators/HybridAllocator.sol +++ b/src/allocators/HybridAllocator.sol @@ -25,9 +25,9 @@ import {IHybridAllocator} from 'src/interfaces/IHybridAllocator.sol'; contract HybridAllocator is IHybridAllocator { event SignerAdded(address signer); event SignerRemoved(address signer); - event SignerReplacementProposed(address oldSigner, address newSigner); - event SignerReplaced(address oldSigner, address newSigner); - event AllocatorInitialized(address compact, address initialSigner, uint96 allocatorId); + event OwnerReplacementProposed(address newOwner); + event OwnerReplaced(address oldOwner, address newOwner); + event AllocatorInitialized(address compact, address owner, uint96 allocatorId); /// @notice The unique identifier for this allocator within The Compact protocol uint96 public immutable ALLOCATOR_ID; @@ -40,22 +40,22 @@ contract HybridAllocator is IHybridAllocator { /// The next 20 bytes are the sponsors address, followed by the freely chosen nonce within the next 11 bytes. /// This will prevent nonce collisions. uint88 public nonces; - /// @notice The total number of authorized signers for off-chain allocations - uint256 public signerCount; + /// @notice The owner of the allocator, authorized to add and remove signers + address public owner; /// @notice Mapping tracking which addresses are authorized signers for off-chain allocations mapping(address signer => bool isSigner) public signers; - mapping(address => address) public pendingSignerReplacement; + address private _pendingOwner; - modifier onlySigner() { - if (!signers[msg.sender]) { - revert CallerNotSigner(); + modifier onlyOwner() { + if (msg.sender != owner) { + revert CallerNotOwner(); } _; } - constructor(address signer_) { - if (signer_ == address(0)) { - revert InvalidSigner(); + constructor(address owner_, address signer_) { + if (owner_ == address(0)) { + revert InvalidOwner(); } _INITIAL_CHAIN_ID = block.chainid; _COMPACT_DOMAIN_SEPARATOR = ITheCompact(AL.THE_COMPACT).DOMAIN_SEPARATOR(); @@ -86,61 +86,59 @@ contract HybridAllocator is IHybridAllocator { ALLOCATOR_ID = allocatorId; } - signers[signer_] = true; - signerCount++; + owner = owner_; + addSigner(signer_); - emit AllocatorInitialized(AL.THE_COMPACT, signer_, ALLOCATOR_ID); - emit SignerAdded(signer_); + emit AllocatorInitialized(AL.THE_COMPACT, owner_, ALLOCATOR_ID); } /// @inheritdoc IHybridAllocator - function addSigner(address signer_) external onlySigner { + function addSigner(address signer_) public onlyOwner { if (signer_ == address(0) || signers[signer_]) { revert InvalidSigner(); } signers[signer_] = true; - signerCount++; emit SignerAdded(signer_); } /// @inheritdoc IHybridAllocator - function removeSigner(address signer_) external onlySigner { - if (signerCount == 1) { - revert LastSigner(); - } + function removeSigner(address signer_) public onlyOwner { if (!signers[signer_]) { revert InvalidSigner(); } - // Clear any pending replacement proposed by this signer - delete pendingSignerReplacement[signer_]; signers[signer_] = false; - signerCount--; emit SignerRemoved(signer_); } /// @inheritdoc IHybridAllocator - function replaceSigner(address newSigner_) external onlySigner { - if (newSigner_ == address(0) || signers[newSigner_]) { + function replaceSigner(address oldSigner_, address newSigner_) external onlyOwner { + if (oldSigner_ == newSigner_) { revert InvalidSigner(); } - address oldSigner = msg.sender; - pendingSignerReplacement[oldSigner] = newSigner_; - emit SignerReplacementProposed(oldSigner, newSigner_); + removeSigner(oldSigner_); + addSigner(newSigner_); } - function acceptSignerReplacement(address oldSigner_) external { - address newSigner_ = pendingSignerReplacement[oldSigner_]; - if (newSigner_ == address(0) || msg.sender != newSigner_) { - revert InvalidSigner(); + /// @inheritdoc IHybridAllocator + function proposeOwnerReplacement(address newOwner_) external onlyOwner { + if (newOwner_ == address(0)) { + revert InvalidOwner(); } - if (!signers[oldSigner_]) { - revert InvalidSigner(); + _pendingOwner = newOwner_; + emit OwnerReplacementProposed(newOwner_); + } + + /// @inheritdoc IHybridAllocator + function acceptOwnerReplacement() external { + if (msg.sender != _pendingOwner) { + revert InvalidOwner(); } - delete pendingSignerReplacement[oldSigner_]; - signers[oldSigner_] = false; - signers[newSigner_] = true; - emit SignerReplaced(oldSigner_, newSigner_); + + delete _pendingOwner; + address previousOwner = owner; + owner = msg.sender; + emit OwnerReplaced(previousOwner, msg.sender); } /// @inheritdoc IAllocator diff --git a/src/interfaces/IHybridAllocator.sol b/src/interfaces/IHybridAllocator.sol index 3b0cd01..b1124ee 100644 --- a/src/interfaces/IHybridAllocator.sol +++ b/src/interfaces/IHybridAllocator.sol @@ -16,30 +16,45 @@ interface IHybridAllocator is IOnChainAllocation { error InvalidAllocatorId(uint96 allocatorId, uint96 expectedAllocatorId); error InvalidCaller(address sender, address expectedSender); error InvalidSignature(); + error InvalidOwner(); error InvalidSigner(); - error CallerNotSigner(); - error LastSigner(); + error CallerNotOwner(); error InvalidValue(uint256 value, uint256 expectedValue); /** * @notice Add an offchain signer to the allocator. * @param signer_ The address of the signer to add. + * @dev The caller must be the owner. */ function addSigner(address signer_) external; /** * @notice Remove an offchain signer from the allocator. - * @dev The last signer cannot be removed. + * @dev The caller must be the owner. * @param signer_ The address of the signer to remove. */ function removeSigner(address signer_) external; /** * @notice Replace an offchain signer with a new one. - * @dev The caller must be the replaced signer. + * @dev The caller must be the owner. + * @param oldSigner_ The address of the old signer. * @param newSigner_ The address of the new signer. */ - function replaceSigner(address newSigner_) external; + function replaceSigner(address oldSigner_, address newSigner_) external; + + /** + * @notice Propose a new owner for the allocator. + * @dev The caller must be the current owner. + * @param newOwner_ The address of the new owner. + */ + function proposeOwnerReplacement(address newOwner_) external; + + /** + * @notice Accept the ownership replacement. + * @dev The caller must be the new (pending) owner. + */ + function acceptOwnerReplacement() external; /** * @notice Create an allocation and a registration on the compact by depositing the relevant tokens to the compact. From 2a465769d303857d80a51c71c71b906d826ac523 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Fri, 12 Dec 2025 12:22:47 +0100 Subject: [PATCH 9/9] fixes and tests --- snapshots/ERC7683Allocator_open.json | 2 +- snapshots/ERC7683Allocator_openFor.json | 4 +- snapshots/HybridAllocatorTest.json | 21 +- snapshots/OnChainAllocatorTest.json | 18 +- src/allocators/HybridAllocator.sol | 5 +- src/allocators/HybridERC7683.sol | 7 +- test/HybridAllocator.t.sol | 250 +++++++++++++----------- test/HybridERC7683.t.sol | 6 +- test/OnChainAllocator.t.sol | 48 +++-- 9 files changed, 210 insertions(+), 151 deletions(-) diff --git a/snapshots/ERC7683Allocator_open.json b/snapshots/ERC7683Allocator_open.json index f1166cc..3890742 100644 --- a/snapshots/ERC7683Allocator_open.json +++ b/snapshots/ERC7683Allocator_open.json @@ -1,3 +1,3 @@ { - "open_simpleOrder": "168837" + "open_simpleOrder": "168865" } \ No newline at end of file diff --git a/snapshots/ERC7683Allocator_openFor.json b/snapshots/ERC7683Allocator_openFor.json index cbad145..9eaebd2 100644 --- a/snapshots/ERC7683Allocator_openFor.json +++ b/snapshots/ERC7683Allocator_openFor.json @@ -1,3 +1,3 @@ { - "openFor_simpleOrder_userHimself": "172263" -} + "openFor_simpleOrder_userHimself": "172309" +} \ No newline at end of file diff --git a/snapshots/HybridAllocatorTest.json b/snapshots/HybridAllocatorTest.json index bfbf746..6a7e17e 100644 --- a/snapshots/HybridAllocatorTest.json +++ b/snapshots/HybridAllocatorTest.json @@ -1,10 +1,13 @@ { - "allocateAndRegister_erc20Token": "187659", - "allocateAndRegister_erc20Token_emptyAmountInput": "188569", - "allocateAndRegister_multipleTokens": "223595", - "allocateAndRegister_nativeToken": "139222", - "allocateAndRegister_nativeToken_emptyAmountInput": "139058", - "allocateAndRegister_second_erc20Token": "114865", - "allocateAndRegister_second_nativeToken": "104858", - "hybrid_execute_single": "174805" -} + "allocateAndRegister_erc20Token": "170490", + "allocateAndRegister_erc20Token_emptyAmountInput": "171400", + "allocateAndRegister_multipleTokens": "206420", + "allocateAndRegister_nativeToken": "122053", + "allocateAndRegister_nativeToken_emptyAmountInput": "121889", + "allocateAndRegister_second_erc20Token": "114796", + "allocateAndRegister_second_nativeToken": "104789", + "hybrid_execute_single": "157779", + "hybrid_permit2Allocation_multipleERC20": "253503", + "hybrid_permit2Allocation_singleERC20": "187123", + "hybrid_permit2Allocation_singleERC20_withWitness": "188140" +} \ No newline at end of file diff --git a/snapshots/OnChainAllocatorTest.json b/snapshots/OnChainAllocatorTest.json index b47cb57..9919922 100644 --- a/snapshots/OnChainAllocatorTest.json +++ b/snapshots/OnChainAllocatorTest.json @@ -1,9 +1,11 @@ { - "allocateFor_success_withRegistration": "134193", - "allocate_and_delete_expired_allocation": "66373", - "allocate_erc20": "129644", - "allocate_native": "129404", - "allocate_second_erc20": "97656", - "onchain_execute_double": "346418", - "onchain_execute_single": "220035" -} + "allocateFor_success_withRegistration": "134199", + "allocate_and_delete_expired_allocation": "66401", + "allocate_erc20": "129672", + "allocate_native": "129432", + "allocate_second_erc20": "97684", + "onchain_execute_double": "346560", + "onchain_execute_single": "220180", + "onchain_permit2Allocation_multipleERC20": "365724", + "onchain_permit2Allocation_singleERC20": "232132" +} \ No newline at end of file diff --git a/src/allocators/HybridAllocator.sol b/src/allocators/HybridAllocator.sol index 24ffe59..303b323 100644 --- a/src/allocators/HybridAllocator.sol +++ b/src/allocators/HybridAllocator.sol @@ -87,7 +87,10 @@ contract HybridAllocator is IHybridAllocator { } owner = owner_; - addSigner(signer_); + if (signer_ != address(0)) { + signers[signer_] = true; + emit SignerAdded(signer_); + } emit AllocatorInitialized(AL.THE_COMPACT, owner_, ALLOCATOR_ID); } diff --git a/src/allocators/HybridERC7683.sol b/src/allocators/HybridERC7683.sol index aec55a1..e3e1d78 100644 --- a/src/allocators/HybridERC7683.sol +++ b/src/allocators/HybridERC7683.sol @@ -5,7 +5,10 @@ pragma solidity ^0.8.27; import {ERC7683AllocatorLib as ERC7683AL} from './lib/ERC7683AllocatorLib.sol'; import {LibBytes} from '@solady/utils/LibBytes.sol'; -import {COMPACT_TYPEHASH_WITH_MANDATE, COMPACT_WITH_MANDATE_TYPESTRING} from '@uniswap/tribunal/types/TribunalTypeHashes.sol'; +import { + COMPACT_TYPEHASH_WITH_MANDATE, + COMPACT_WITH_MANDATE_TYPESTRING +} from '@uniswap/tribunal/types/TribunalTypeHashes.sol'; import {HybridAllocator} from 'src/allocators/HybridAllocator.sol'; import {IERC7683Allocator} from 'src/interfaces/IERC7683Allocator.sol'; @@ -18,7 +21,7 @@ import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; contract HybridERC7683 is HybridAllocator, IERC7683Allocator { error OnlyDepositsAllowed(); - constructor(address signer) HybridAllocator(signer) {} + constructor(address owner_, address signer_) HybridAllocator(owner_, signer_) {} /// @inheritdoc IOriginSettler function openFor(GaslessCrossChainOrder calldata order, bytes calldata sponsorSignature, bytes calldata) external { diff --git a/test/HybridAllocator.t.sol b/test/HybridAllocator.t.sol index b550d6e..499cb5c 100644 --- a/test/HybridAllocator.t.sol +++ b/test/HybridAllocator.t.sol @@ -37,8 +37,8 @@ import {OnChainAllocationCaller} from 'src/test/OnChainAllocationCaller.sol'; import {DeployTheCompact} from 'test/util/DeployTheCompact.sol'; contract HybridAllocatorFactory { - function deploy(bytes32 salt, address signer) external returns (address) { - return address(new HybridAllocator{salt: salt}(signer)); + function deploy(bytes32 salt, address owner, address signer) external returns (address) { + return address(new HybridAllocator{salt: salt}(owner, signer)); } } @@ -46,6 +46,7 @@ contract HybridAllocatorTest is Test, TestHelper { TheCompact compact; address arbiter; HybridAllocator allocator; + address owner; address signer; uint256 signerPrivateKey; ERC20Mock usdc; @@ -108,8 +109,9 @@ contract HybridAllocatorTest is Test, TestHelper { _deployPermit2(); arbiter = makeAddr('arbiter'); + owner = makeAddr('owner'); (signer, signerPrivateKey) = makeAddrAndKey('signer'); - allocator = new HybridAllocator(signer); + allocator = new HybridAllocator(owner, signer); usdc = new ERC20Mock('USDC', 'USDC'); dai = new ERC20Mock('DAI', 'DAI'); (user, userPrivateKey) = makeAddrAndKey('user'); @@ -364,9 +366,14 @@ contract HybridAllocatorTest is Test, TestHelper { return abi.encodePacked(r, vs); } - function test_constructor_revert_signerIsAddressZero() public { - vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.InvalidSigner.selector)); - new HybridAllocator(address(0)); + function test_constructor_revert_ownerIsAddressZero() public { + vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.InvalidOwner.selector)); + new HybridAllocator(address(0), signer); + } + + function test_constructor_skipSignerAssignmentIfAddressZero() public { + HybridAllocator allocator_ = new HybridAllocator(owner, address(0)); + assertFalse(allocator_.signers(signer)); } function test_checkAllocatorId() public view { @@ -377,8 +384,8 @@ contract HybridAllocatorTest is Test, TestHelper { assertEq(allocator.nonces(), 0); } - function test_checkSignerCount() public view { - assertEq(allocator.signerCount(), 1); + function test_checkOwner() public view { + assertEq(allocator.owner(), owner); } function test_checkSigners(address attacker) public view { @@ -854,7 +861,8 @@ contract HybridAllocatorTest is Test, TestHelper { assertEq(registeredAmounts[0], defaultAmount); assertEq(usdc.balanceOf(address(compact)), defaultAmount); assertEq(compact.balanceOf(address(user), idsAndAmounts[0][0]), defaultAmount); - assertEq(nonce, 1); + // HybridAllocator nonce format: command | counter (no address embedded) + assertEq(nonce, _composeNonceUint(ON_CHAIN_NONCE, address(0), 1)); } function test_allocateAndRegister_slot() public { @@ -899,7 +907,8 @@ contract HybridAllocatorTest is Test, TestHelper { bytes memory invalidSignature = abi.encodePacked(bytes32(0), bytes32(0), uint8(0)); // Forcing address(0) as signer - uint256 signersSlot = 0x03; + // Storage layout: slot 0 = claims mapping, slot 1 = nonces + owner (packed), slot 2 = signers mapping + uint256 signersSlot = 0x02; vm.store(address(allocator), keccak256(abi.encode(address(0), signersSlot)), bytes32(uint256(1))); assertTrue(allocator.signers(address(0))); @@ -1093,7 +1102,7 @@ contract HybridAllocatorTest is Test, TestHelper { compact.batchClaim(claim); } - function test_authorizeClaim_revert_oldSignatureAfterFork(uint128 nonce) public { + function test_authorizeClaim_revert_oldSignatureAfterFork(uint88 freeNonce) public { uint256[2][] memory idsAndAmounts = new uint256[2][](2); idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); idsAndAmounts[0][1] = defaultAmount; @@ -1107,6 +1116,9 @@ contract HybridAllocatorTest is Test, TestHelper { bytes32 witness = keccak256(abi.encode(WITNESS_TYPEHASH, 1)); + // Format nonce with OFF_CHAIN_NONCE command and user as sponsor + uint256 nonce = _composeNonceUint(OFF_CHAIN_NONCE, user, freeNonce); + bytes32 claimHash = _toBatchCompactHashWithWitness( BATCH_COMPACT_TYPEHASH_WITH_WITNESS, BatchCompact({ @@ -1386,61 +1398,47 @@ contract HybridAllocatorTest is Test, TestHelper { assertFalse(allocator.isClaimAuthorized(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); } - function test_addSigner_revert_CallerNotSigner(address attacker) public { + function test_addSigner_revert_CallerNotOwner(address attacker) public { vm.assume(attacker != address(0)); - vm.assume(attacker != signer); + vm.assume(attacker != owner); vm.prank(attacker); - vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.CallerNotSigner.selector)); + vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.CallerNotOwner.selector)); allocator.addSigner(attacker); - assertEq(allocator.signerCount(), 1); assertFalse(allocator.signers(attacker)); } function test_addSigner_revert_signerIsZero() public { - vm.prank(signer); + vm.prank(owner); vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.InvalidSigner.selector)); allocator.addSigner(address(0)); - assertEq(allocator.signerCount(), 1); assertFalse(allocator.signers(address(0))); } function test_addSigner_success(address newSigner) public { vm.assume(newSigner != signer); vm.assume(newSigner != address(0)); - vm.prank(signer); + vm.prank(owner); allocator.addSigner(newSigner); - assertEq(allocator.signerCount(), 2); assertTrue(allocator.signers(newSigner)); assertTrue(allocator.signers(signer)); } - function test_removeSigner_revert_CallerNotSigner(address attacker) public { - vm.assume(attacker != signer); + function test_removeSigner_revert_CallerNotOwner(address attacker) public { + vm.assume(attacker != owner); vm.prank(attacker); - vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.CallerNotSigner.selector)); + vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.CallerNotOwner.selector)); allocator.removeSigner(signer); - assertEq(allocator.signerCount(), 1); - assertTrue(allocator.signers(signer)); - } - - function test_removeSigner_revert_LastSigner() public { - vm.prank(signer); - vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.LastSigner.selector)); - allocator.removeSigner(signer); - assertEq(allocator.signerCount(), 1); assertTrue(allocator.signers(signer)); } function test_removeSigner_revert_InvalidSigner(address attacker) public { vm.assume(attacker != signer); vm.assume(attacker != address(this)); - vm.prank(signer); + vm.prank(owner); allocator.addSigner(address(this)); - assertEq(allocator.signerCount(), 2); - vm.prank(address(this)); + vm.prank(owner); vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.InvalidSigner.selector)); allocator.removeSigner(attacker); - assertEq(allocator.signerCount(), 2); assertTrue(allocator.signers(address(this))); assertTrue(allocator.signers(signer)); } @@ -1448,118 +1446,140 @@ contract HybridAllocatorTest is Test, TestHelper { function test_removeSigner_success(address newSigner) public { vm.assume(newSigner != signer); vm.assume(newSigner != address(0)); - vm.prank(signer); + vm.prank(owner); allocator.addSigner(newSigner); - assertEq(allocator.signerCount(), 2); - vm.prank(newSigner); + vm.prank(owner); allocator.removeSigner(signer); - assertEq(allocator.signerCount(), 1); assertFalse(allocator.signers(signer)); assertTrue(allocator.signers(newSigner)); } - function test_removeSigner_clearsPendingReplacement(address newSigner) public { - vm.assume(newSigner != signer); - vm.assume(newSigner != address(0)); - vm.prank(signer); - allocator.replaceSigner(newSigner); - // add a second signer so removal of proposer is permitted - address second = makeAddr('second'); - vm.prank(signer); - allocator.addSigner(second); - // remove the proposer while pending exists (now allowed) - vm.prank(second); - allocator.removeSigner(signer); - // re-add signer, ensure old pending cannot be accepted - vm.prank(newSigner); - vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.InvalidSigner.selector)); - allocator.acceptSignerReplacement(signer); - } - function test_removeSigner_success_deleteSelf(address newSigner) public { vm.assume(newSigner != signer); vm.assume(newSigner != address(0)); - vm.prank(signer); + vm.prank(owner); allocator.addSigner(newSigner); - assertEq(allocator.signerCount(), 2); - vm.prank(newSigner); + vm.prank(owner); allocator.removeSigner(newSigner); - assertEq(allocator.signerCount(), 1); assertTrue(allocator.signers(signer)); assertFalse(allocator.signers(newSigner)); } - function test_replaceSigner_revert_CallerNotSigner(address attacker) public { - vm.assume(attacker != signer); + function test_replaceSigner_revert_CallerNotOwner(address attacker) public { + vm.assume(attacker != owner); + address newSigner = makeAddr('newSigner'); vm.prank(attacker); - vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.CallerNotSigner.selector)); - allocator.replaceSigner(attacker); - assertEq(allocator.signerCount(), 1); - assertFalse(allocator.signers(attacker)); + vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.CallerNotOwner.selector)); + allocator.replaceSigner(signer, newSigner); + assertFalse(allocator.signers(newSigner)); } function test_replaceSigner_revert_signerIsZero() public { - vm.prank(signer); + vm.prank(owner); vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.InvalidSigner.selector)); - allocator.replaceSigner(address(0)); - assertEq(allocator.signerCount(), 1); + allocator.replaceSigner(signer, address(0)); assertFalse(allocator.signers(address(0))); } + function test_replaceSigner_revert_sameSigners() public { + vm.prank(owner); + vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.InvalidSigner.selector)); + allocator.replaceSigner(signer, signer); + assertTrue(allocator.signers(signer)); + } + function test_replaceSigner_success_twoStep(address newSigner) public { vm.assume(newSigner != signer); vm.assume(newSigner != address(0)); - vm.prank(signer); - allocator.replaceSigner(newSigner); - // Not active until accepted by new signer - assertTrue(allocator.signers(signer)); - assertFalse(allocator.signers(newSigner)); - - vm.prank(newSigner); - allocator.acceptSignerReplacement(signer); + vm.prank(owner); + allocator.replaceSigner(signer, newSigner); + // Immediate replacement - no two step anymore assertFalse(allocator.signers(signer)); assertTrue(allocator.signers(newSigner)); - assertEq(allocator.signerCount(), 1); } - function test_replaceSigner_multipleProposals_lastWins(address newSigner, address newSigner2) public { + function test_replaceSigner(address newSigner, address newSigner2) public { vm.assume(newSigner != signer); vm.assume(newSigner != address(0)); vm.assume(newSigner2 != signer); vm.assume(newSigner2 != address(0)); vm.assume(newSigner2 != newSigner); - vm.prank(signer); - allocator.replaceSigner(newSigner); - // can propose a second replacement; last wins - vm.prank(signer); - allocator.replaceSigner(newSigner2); - - // accepting first should now fail - vm.prank(newSigner); - vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.InvalidSigner.selector)); - allocator.acceptSignerReplacement(signer); - - // old signer can no longer propose; new signer can propose - vm.prank(newSigner); - vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.CallerNotSigner.selector)); - allocator.replaceSigner(newSigner2); + vm.prank(owner); + allocator.replaceSigner(signer, newSigner); + // Now newSigner is the signer + assertFalse(allocator.signers(signer)); + assertTrue(allocator.signers(newSigner)); - // accept the latest replacement - vm.prank(newSigner2); - allocator.acceptSignerReplacement(signer); + // Replace newSigner with newSigner2 + vm.prank(owner); + allocator.replaceSigner(newSigner, newSigner2); assertFalse(allocator.signers(signer)); assertFalse(allocator.signers(newSigner)); assertTrue(allocator.signers(newSigner2)); } + function test_proposeOwnerReplacement_revert_CallerNotOwner(address attacker) public { + vm.assume(attacker != owner); + address newOwner = makeAddr('newOwner'); + vm.prank(attacker); + vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.CallerNotOwner.selector)); + allocator.proposeOwnerReplacement(newOwner); + } + + function test_proposeOwnerReplacement_revert_zeroAddress() public { + vm.prank(owner); + vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.InvalidOwner.selector)); + allocator.proposeOwnerReplacement(address(0)); + } + + function test_acceptOwnerReplacement_revert_notPendingOwner(address attacker) public { + vm.assume(attacker != owner); + address newOwner = makeAddr('newOwner'); + vm.assume(attacker != newOwner); + + // First propose a new owner + vm.prank(owner); + allocator.proposeOwnerReplacement(newOwner); + + // Try to accept from wrong address + vm.prank(attacker); + vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.InvalidOwner.selector)); + allocator.acceptOwnerReplacement(); + } + + function test_ownerReplacement_success() public { + address newOwner = makeAddr('newOwner'); + + // Propose new owner + vm.prank(owner); + allocator.proposeOwnerReplacement(newOwner); + assertEq(allocator.owner(), owner); + + // Accept as new owner + vm.prank(newOwner); + allocator.acceptOwnerReplacement(); + assertEq(allocator.owner(), newOwner); + + // New owner can add signers + address newSigner = makeAddr('anotherSigner'); + vm.prank(newOwner); + allocator.addSigner(newSigner); + assertTrue(allocator.signers(newSigner)); + + // Old owner cannot add signers + vm.prank(owner); + vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.CallerNotOwner.selector)); + allocator.addSigner(makeAddr('yetAnotherSigner')); + } + function test_constructor_allowsPreRegisteredAllocator_create2() public { HybridAllocatorFactory factory = new HybridAllocatorFactory(); bytes32 salt = keccak256('hybrid-allocator-pre-registered'); - // initCode must match what the factory deploys: creationCode + abi.encode(signer) - bytes memory initCode = abi.encodePacked(type(HybridAllocator).creationCode, abi.encode(signer)); + // initCode must match what the factory deploys: creationCode + abi.encode(owner, signer) + bytes memory initCode = abi.encodePacked(type(HybridAllocator).creationCode, abi.encode(owner, signer)); bytes32 initCodeHash = keccak256(initCode); address expected = @@ -1570,7 +1590,7 @@ contract HybridAllocatorTest is Test, TestHelper { uint96 preId = compact.__registerAllocator(expected, proof); assertEq(_toAllocatorId(expected), preId); - address deployed = HybridAllocatorFactory(address(factory)).deploy(salt, signer); + address deployed = HybridAllocatorFactory(address(factory)).deploy(salt, owner, signer); assertEq(deployed, expected); HybridAllocator newAllocator = HybridAllocator(deployed); @@ -1616,8 +1636,9 @@ contract HybridAllocatorTest is Test, TestHelper { bytes memory signature = _createPermit2Signature(permitted, details, claimHash, userPrivateKey); // Execute - Lock[] memory commitments = - allocator.permit2Allocation(arbiter, user, permitted, details, claimHash, '', bytes32(0), signature); + Lock[] memory commitments = allocator.permit2Allocation( + arbiter, user, defaultExpiration, permitted, details, claimHash, '', bytes32(0), signature + ); vm.snapshotGasLastCall('hybrid_permit2Allocation_singleERC20'); // Verify commitments @@ -1679,7 +1700,7 @@ contract HybridAllocatorTest is Test, TestHelper { // Note: The witness parameter should be just the inner content (e.g., "uint256 witness"), // not the full struct definition, as TheCompact wraps it in "Mandate(...)" Lock[] memory commitments = allocator.permit2Allocation( - arbiter, user, permitted, details, claimHash, WITNESS_STRING, witness, signature + arbiter, user, defaultExpiration, permitted, details, claimHash, WITNESS_STRING, witness, signature ); vm.snapshotGasLastCall('hybrid_permit2Allocation_singleERC20_withWitness'); @@ -1742,8 +1763,9 @@ contract HybridAllocatorTest is Test, TestHelper { bytes memory signature = _createPermit2Signature(permitted, details, claimHash, userPrivateKey); // Execute - Lock[] memory commitments = - allocator.permit2Allocation(arbiter, user, permitted, details, claimHash, '', bytes32(0), signature); + Lock[] memory commitments = allocator.permit2Allocation( + arbiter, user, defaultExpiration, permitted, details, claimHash, '', bytes32(0), signature + ); vm.snapshotGasLastCall('hybrid_permit2Allocation_multipleERC20'); // Verify commitments @@ -1789,7 +1811,9 @@ contract HybridAllocatorTest is Test, TestHelper { // Should revert with UnauthorizedNonce because command is not PERMIT2_NONCE vm.expectRevert(abi.encodeWithSelector(AllocatorLib.UnauthorizedNonce.selector, bytes1(0x01), user)); - allocator.permit2Allocation(arbiter, user, permitted, details, claimHash, '', bytes32(0), signature); + allocator.permit2Allocation( + arbiter, user, defaultExpiration, permitted, details, claimHash, '', bytes32(0), signature + ); } function test_permit2Allocation_revert_invalidNonceSponsor() public { @@ -1811,7 +1835,9 @@ contract HybridAllocatorTest is Test, TestHelper { // Should revert because sponsor in nonce doesn't match depositor vm.expectRevert(abi.encodeWithSelector(AllocatorLib.UnauthorizedNonce.selector, bytes1(0x03), wrongSponsor)); - allocator.permit2Allocation(arbiter, user, permitted, details, claimHash, '', bytes32(0), signature); + allocator.permit2Allocation( + arbiter, user, defaultExpiration, permitted, details, claimHash, '', bytes32(0), signature + ); } function test_permit2Allocation_emitsAllocatedEvent() public { @@ -1851,7 +1877,9 @@ contract HybridAllocatorTest is Test, TestHelper { vm.expectEmit(true, true, true, true); emit IOnChainAllocation.Allocated(user, expectedCommitments, nonce, defaultExpiration, claimHash); - allocator.permit2Allocation(arbiter, user, permitted, details, claimHash, '', bytes32(0), signature); + allocator.permit2Allocation( + arbiter, user, defaultExpiration, permitted, details, claimHash, '', bytes32(0), signature + ); } function test_permit2Allocation_fullClaimFlow() public { @@ -1886,7 +1914,9 @@ contract HybridAllocatorTest is Test, TestHelper { bytes memory signature = _createPermit2Signature(permitted, details, claimHash, userPrivateKey); // Execute permit2Allocation - allocator.permit2Allocation(arbiter, user, permitted, details, claimHash, '', bytes32(0), signature); + allocator.permit2Allocation( + arbiter, user, defaultExpiration, permitted, details, claimHash, '', bytes32(0), signature + ); // Verify claim is authorized assertTrue(allocator.isClaimAuthorized(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); diff --git a/test/HybridERC7683.t.sol b/test/HybridERC7683.t.sol index be5323a..1fedc97 100644 --- a/test/HybridERC7683.t.sol +++ b/test/HybridERC7683.t.sol @@ -50,15 +50,17 @@ import {DeployTheCompact} from 'test/util/DeployTheCompact.sol'; contract MockAllocator is GaslessCrossChainOrderData, OnChainCrossChainOrderData { HybridERC7683 hybridERC7683Allocator; + address owner; address signer; uint256 signerPK; function setUp() public virtual override(GaslessCrossChainOrderData, OnChainCrossChainOrderData) { + owner = makeAddr('owner'); (signer, signerPK) = makeAddrAndKey('signer'); TheCompact compactContract_ = DeployTheCompact(new DeployTheCompact()).deployTheCompact(); assertEq(address(compactContract_), address(0x00000000000000171ede64904551eeDF3C6C9788)); - hybridERC7683Allocator = new HybridERC7683(signer); + hybridERC7683Allocator = new HybridERC7683(owner, signer); // HybridAllocator uses simplified nonce: command | counter (no address embedded) _setUp(address(hybridERC7683Allocator), compactContract_, _composeNonceUint(ON_CHAIN_NONCE, address(0), 1)); super.setUp(); @@ -1014,7 +1016,7 @@ contract HybridERC7683_hybridAllocatorInheritance is MockAllocator { function test_inheritsHybridAllocatorFunctionality() public view { // Test that it properly inherits from HybridAllocator assertEq(hybridERC7683Allocator.nonces(), 0); - assertEq(hybridERC7683Allocator.signerCount(), 1); + assertEq(hybridERC7683Allocator.owner(), owner); assertTrue(hybridERC7683Allocator.signers(signer)); assertEq(hybridERC7683Allocator.ALLOCATOR_ID(), _toAllocatorId(address(hybridERC7683Allocator))); } diff --git a/test/OnChainAllocator.t.sol b/test/OnChainAllocator.t.sol index be3ec62..91927eb 100644 --- a/test/OnChainAllocator.t.sol +++ b/test/OnChainAllocator.t.sol @@ -2033,7 +2033,9 @@ contract OnChainAllocatorTest is Test, TestHelper { idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); idsAndAmounts[0][1] = defaultAmount; - assertEq(nonce, 1); + // Nonce format: ON_CHAIN_NONCE | (sponsor << 88) | 1 + // When msg.sender is non-zero, sponsor in nonce becomes address(0) (see _getAndUpdateNonce) + assertEq(nonce, _composeNonceUint(address(0), 1)); assertEq(registeredAmounts.length, 1); assertEq(registeredAmounts[0], defaultAmount); // Ensure the allocation happened for the caller, not address(0) @@ -2404,8 +2406,9 @@ contract OnChainAllocatorTest is Test, TestHelper { bytes memory signature = _createPermit2Signature(permitted, details, claimHash, userPK); // Execute - Lock[] memory commitments = - allocator.permit2Allocation(arbiter, user, permitted, details, claimHash, '', bytes32(0), signature); + Lock[] memory commitments = allocator.permit2Allocation( + arbiter, user, defaultExpiration, permitted, details, claimHash, '', bytes32(0), signature + ); vm.snapshotGasLastCall('onchain_permit2Allocation_singleERC20'); // Verify commitments @@ -2470,8 +2473,9 @@ contract OnChainAllocatorTest is Test, TestHelper { bytes memory signature = _createPermit2Signature(permitted, details, claimHash, userPK); // Execute - Lock[] memory commitments = - allocator.permit2Allocation(arbiter, user, permitted, details, claimHash, '', bytes32(0), signature); + Lock[] memory commitments = allocator.permit2Allocation( + arbiter, user, defaultExpiration, permitted, details, claimHash, '', bytes32(0), signature + ); vm.snapshotGasLastCall('onchain_permit2Allocation_multipleERC20'); // Verify commitments @@ -2522,29 +2526,33 @@ contract OnChainAllocatorTest is Test, TestHelper { // Should revert with UnauthorizedNonce because command is not PERMIT2_NONCE vm.expectRevert(abi.encodeWithSelector(AllocatorLib.UnauthorizedNonce.selector, bytes1(0x01), user)); - allocator.permit2Allocation(arbiter, user, permitted, details, claimHash, '', bytes32(0), signature); + allocator.permit2Allocation( + arbiter, user, defaultExpiration, permitted, details, claimHash, '', bytes32(0), signature + ); } function test_permit2Allocation_revert_invalidExpiration() public { bytes12 lockTag = _getLockTag(); uint88 freeNonce = 1; uint256 nonce = _createPermit2Nonce(user, freeNonce); - uint256 invalidDeadline = uint256(type(uint32).max) + 1; + uint256 invalidExpiration = uint256(type(uint32).max) + 1; // Prepare token permissions ISignatureTransfer.TokenPermissions[] memory permitted = _createTokenPermissions(address(usdc), defaultAmount); - // Prepare deposit details with invalid expiration - DepositDetails memory details = _createDepositDetails(nonce, invalidDeadline, lockTag); + // Prepare deposit details - deadline can be valid, only expires matters + DepositDetails memory details = _createDepositDetails(nonce, defaultExpiration, lockTag); bytes32 claimHash = bytes32(uint256(1)); // dummy claim hash bytes memory signature = new bytes(64); // dummy signature - // Should revert because deadline exceeds uint32 max + // Should revert because expires exceeds uint32 max vm.expectRevert( - abi.encodeWithSelector(IOnChainAllocator.InvalidExpiration.selector, invalidDeadline, type(uint32).max) + abi.encodeWithSelector(IOnChainAllocator.InvalidExpiration.selector, invalidExpiration, type(uint32).max) + ); + allocator.permit2Allocation( + arbiter, user, invalidExpiration, permitted, details, claimHash, '', bytes32(0), signature ); - allocator.permit2Allocation(arbiter, user, permitted, details, claimHash, '', bytes32(0), signature); } function test_permit2Allocation_revert_invalidAmount() public { @@ -2584,7 +2592,9 @@ contract OnChainAllocatorTest is Test, TestHelper { // Should revert because amount exceeds uint224 max vm.expectRevert(abi.encodeWithSelector(IOnChainAllocator.InvalidAmount.selector, largeAmount)); - allocator.permit2Allocation(arbiter, user, permitted, details, claimHash, '', bytes32(0), signature); + allocator.permit2Allocation( + arbiter, user, defaultExpiration, permitted, details, claimHash, '', bytes32(0), signature + ); } function test_permit2Allocation_fullClaimFlow() public { @@ -2619,7 +2629,9 @@ contract OnChainAllocatorTest is Test, TestHelper { bytes memory signature = _createPermit2Signature(permitted, details, claimHash, userPK); // Execute permit2Allocation - allocator.permit2Allocation(arbiter, user, permitted, details, claimHash, '', bytes32(0), signature); + allocator.permit2Allocation( + arbiter, user, defaultExpiration, permitted, details, claimHash, '', bytes32(0), signature + ); // Verify claim is authorized uint256[2][] memory idsAndAmounts = new uint256[2][](1); @@ -2690,7 +2702,9 @@ contract OnChainAllocatorTest is Test, TestHelper { bytes memory signature = _createPermit2Signature(permitted, details, claimHash, userPK); // Execute permit2Allocation - allocator.permit2Allocation(arbiter, user, permitted, details, claimHash, '', bytes32(0), signature); + allocator.permit2Allocation( + arbiter, user, defaultExpiration, permitted, details, claimHash, '', bytes32(0), signature + ); // Verify claim is authorized before expiration uint256[2][] memory idsAndAmounts = new uint256[2][](1); @@ -2735,7 +2749,9 @@ contract OnChainAllocatorTest is Test, TestHelper { bytes memory signature = _createPermit2Signature(permitted, details, claimHash, userPK); // Execute permit2Allocation - allocator.permit2Allocation(arbiter, user, permitted, details, claimHash, '', bytes32(0), signature); + allocator.permit2Allocation( + arbiter, user, defaultExpiration, permitted, details, claimHash, '', bytes32(0), signature + ); // Try to transfer - should fail because tokens are allocated vm.prank(user);