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/lib/the-compact b/lib/the-compact index dff998d..4118ca9 160000 --- a/lib/the-compact +++ b/lib/the-compact @@ -1 +1 @@ -Subproject commit dff998d4539ad0ef50c7cdd18d51b3d1bd457ab2 +Subproject commit 4118ca99876b14b261116069e7d3c137a0751e11 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/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 f17d4c5..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 f163357..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 923520a..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 b678e6c..303b323 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 @@ -23,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; @@ -34,24 +36,26 @@ 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; - /// @notice The total number of authorized signers for off-chain allocations - uint256 public signerCount; + /// @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 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(); @@ -82,61 +86,62 @@ contract HybridAllocator is IHybridAllocator { ALLOCATOR_ID = allocatorId; } - signers[signer_] = true; - signerCount++; + owner = owner_; + if (signer_ != address(0)) { + signers[signer_] = true; + emit SignerAdded(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 @@ -160,9 +165,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 +182,31 @@ 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, nonce); + } + + /// @inheritdoc IHybridAllocator + function permit2Allocation( + address arbiter, + address depositor, + uint256 expires, + 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( + arbiter, depositor, expires, permitted, details, claimHash, witness, witnessHash, signature + ); - return (claimHash, registeredAmounts, nonces); + // Allocate the claim + claims[claimHash] = true; + + emit Allocated(depositor, commitments, details.nonce, expires, claimHash); } /// @inheritdoc IOnChainAllocation @@ -191,8 +219,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 +235,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 +250,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 +265,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..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 { @@ -105,7 +108,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 +118,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 +143,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..ba66522 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,46 @@ contract OnChainAllocator is IOnChainAllocator, Utility { return commitments; } + /// @inheritdoc IOnChainAllocator + function permit2Allocation( + address arbiter, + address depositor, + uint256 expires, + ISignatureTransfer.TokenPermissions[] calldata permitted, + DepositDetails calldata details, + bytes32 claimHash, + string calldata witness, + bytes32 witnessHash, + bytes calldata signature + ) external returns (Lock[] memory commitments) { + if (expires > type(uint32).max) { + revert InvalidExpiration(expires, type(uint32).max); + } + + commitments = AL.permit2Allocation( + arbiter, depositor, expires, permitted, details, claimHash, witness, witnessHash, 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(expires), // expires is verified in the AllocatorLib.permit2Allocation function + claimHash + ); + } + + emit Allocated(depositor, commitments, details.nonce, expires, claimHash); + } + /// @inheritdoc IOnChainAllocation function prepareAllocation( address recipient, @@ -255,9 +297,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 +319,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); @@ -293,8 +336,8 @@ contract OnChainAllocator is IOnChainAllocator, Utility { bytes32 typehash, bytes32 witness ) private returns (bytes32, Lock[] memory) { - (bytes32 claimHash, Lock[] memory commitments) = - AL.executeAllocation(nonce, recipient, idsAndAmounts, arbiter, expires, typehash, witness); + (bytes32 claimHash, Lock[] memory commitments,) = + AL.executeAllocation(uint248(nonce), recipient, idsAndAmounts, arbiter, expires, typehash, witness); // Allocate the claim for (uint256 i = 0; i < commitments.length; i++) { @@ -579,25 +622,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 aade9ea..3b7319f 100644 --- a/src/allocators/lib/AllocatorLib.sol +++ b/src/allocators/lib/AllocatorLib.sol @@ -4,7 +4,21 @@ 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'; +import {DepositDetails} from 'the-compact/src/types/DepositDetails.sol'; /// @title AllocatorLib /// @notice Library providing core functionality for atomic token allocation verification using transient storage @@ -21,17 +35,182 @@ 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; + + /// @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); + error InvalidClaim(bytes32 claimHash); + + 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. + 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 + 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 + } + + // Verify the claim hash includes proposed expiration + if ( + claimHash + != getClaimHash( + arbiter, + depositor, + details.nonce, + expires, + getCommitmentsHashMemory(commitments), + witnessHash, + computeBatchCompactTypehash(witness) + ) + ) { + revert InvalidClaim(claimHash); + } + + return commitments; + } function prepareAllocation( - uint256 nonce, + uint248 noncePreCommand, address recipient, uint256[2][] calldata idsAndAmounts, address arbiter, @@ -39,10 +218,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) @@ -100,19 +281,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(); @@ -152,7 +335,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() @@ -207,7 +390,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 { @@ -236,6 +419,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 @@ -264,6 +470,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, @@ -286,6 +508,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; @@ -318,6 +559,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)); } @@ -352,6 +613,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/IHybridAllocator.sol b/src/interfaces/IHybridAllocator.sol index fb2f917..b1124ee 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 @@ -13,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. @@ -57,4 +75,29 @@ 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, + uint256 expires, + 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..71c6442 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,29 @@ 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, + uint256 expires, + 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/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index 8416f1d..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(); } } @@ -75,6 +76,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 +234,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'); @@ -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..499cb5c 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'; @@ -23,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)); } } @@ -32,9 +46,11 @@ contract HybridAllocatorTest is Test, TestHelper { TheCompact compact; address arbiter; HybridAllocator allocator; + address owner; address signer; uint256 signerPrivateKey; ERC20Mock usdc; + ERC20Mock dai; address user; uint256 userPrivateKey; uint256 defaultAmount; @@ -44,20 +60,73 @@ 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'); + 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'); - 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 +135,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,9 +175,205 @@ contract HybridAllocatorTest is Test, TestHelper { arr[1][1] = amountB; } - function test_constructor_revert_signerIsAddressZero() public { - vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.InvalidSigner.selector)); - new HybridAllocator(address(0)); + /* ====================================================================== */ + /* 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_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 { @@ -97,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 { @@ -125,12 +412,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 +443,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 +483,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 +645,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 +667,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 +685,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 +708,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 +738,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 { @@ -570,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 { @@ -615,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))); @@ -692,7 +985,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 +995,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 +1029,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; @@ -806,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; @@ -820,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({ @@ -940,11 +1239,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 +1260,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 +1334,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( @@ -1085,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)); } @@ -1147,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 = @@ -1269,10 +1590,367 @@ 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); 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( + arbiter, user, defaultExpiration, permitted, details, claimHash, '', bytes32(0), 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 = 12_345; + 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( + arbiter, user, defaultExpiration, permitted, details, claimHash, WITNESS_STRING, witness, 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( + arbiter, user, defaultExpiration, permitted, details, claimHash, '', bytes32(0), 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( + arbiter, user, defaultExpiration, permitted, details, claimHash, '', bytes32(0), 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( + arbiter, user, defaultExpiration, permitted, details, claimHash, '', bytes32(0), 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( + arbiter, user, defaultExpiration, permitted, details, claimHash, '', bytes32(0), 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( + 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), '')); + + // 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..1fedc97 100644 --- a/test/HybridERC7683.t.sol +++ b/test/HybridERC7683.t.sol @@ -50,16 +50,19 @@ 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); - _setUp(address(hybridERC7683Allocator), compactContract_, 1 /* defaultNonce */ ); + 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(); } } @@ -408,6 +411,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 +439,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 +456,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 +484,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 +502,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 +531,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 +748,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 +975,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 @@ -1007,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))); } @@ -1035,6 +1044,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..91927eb 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( @@ -1843,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) @@ -2174,6 +2366,411 @@ 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( + arbiter, user, defaultExpiration, permitted, details, claimHash, '', bytes32(0), 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( + arbiter, user, defaultExpiration, permitted, details, claimHash, '', bytes32(0), 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( + 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 invalidExpiration = uint256(type(uint32).max) + 1; + + // Prepare token permissions + ISignatureTransfer.TokenPermissions[] memory permitted = _createTokenPermissions(address(usdc), defaultAmount); + + // 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 expires exceeds uint32 max + vm.expectRevert( + abi.encodeWithSelector(IOnChainAllocator.InvalidExpiration.selector, invalidExpiration, type(uint32).max) + ); + allocator.permit2Allocation( + arbiter, user, invalidExpiration, permitted, details, claimHash, '', bytes32(0), 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( + arbiter, user, defaultExpiration, permitted, details, claimHash, '', bytes32(0), 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( + arbiter, user, defaultExpiration, permitted, details, claimHash, '', bytes32(0), 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( + arbiter, user, defaultExpiration, permitted, details, claimHash, '', bytes32(0), 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( + arbiter, user, defaultExpiration, permitted, details, claimHash, '', bytes32(0), 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());