From 2909eaa1519dad86060ab520c5bab0f48d3eb69c Mon Sep 17 00:00:00 2001 From: mgretzke Date: Thu, 27 Mar 2025 18:41:54 +0100 Subject: [PATCH 01/63] SimpleAllocator contract adaptations --- lib/forge-chronicles | 2 +- lib/forge-gas-snapshot | 2 +- lib/forge-std | 2 +- lib/openzeppelin-contracts | 2 +- lib/solady | 2 +- lib/the-compact | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/forge-chronicles b/lib/forge-chronicles index c2be7a4..d1fb566 160000 --- a/lib/forge-chronicles +++ b/lib/forge-chronicles @@ -1 +1 @@ -Subproject commit c2be7a462d87b8e3a72f7e2b739cdf12807a71e4 +Subproject commit d1fb566915f23a01f08747264c56f8925f15751a diff --git a/lib/forge-gas-snapshot b/lib/forge-gas-snapshot index 9161f7c..cf34ad1 160000 --- a/lib/forge-gas-snapshot +++ b/lib/forge-gas-snapshot @@ -1 +1 @@ -Subproject commit 9161f7c0b6c6788a89081e2b3b9c67592b71e689 +Subproject commit cf34ad1ed0a1f323e77557b9bce420f3385f7400 diff --git a/lib/forge-std b/lib/forge-std index 5475f85..60acb7a 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 5475f852e3f530d7e25dfb4596aa1f9baa8ffdfc +Subproject commit 60acb7aaadcce2d68e52986a0a66fe79f07d138f diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index acd4ff7..56fe41c 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit acd4ff74de833399287ed6b31b4debf6b2b35527 +Subproject commit 56fe41c5882fa05348e0abd87d3501fce7a94b15 diff --git a/lib/solady b/lib/solady index a8fbe33..6da40ac 160000 --- a/lib/solady +++ b/lib/solady @@ -1 +1 @@ -Subproject commit a8fbe33fdd8897a8a3f23d59193296d3a0fd41b7 +Subproject commit 6da40ac63da5657d6c4d3ce3e3a99bc5862dda53 diff --git a/lib/the-compact b/lib/the-compact index 99893a2..8079c4f 160000 --- a/lib/the-compact +++ b/lib/the-compact @@ -1 +1 @@ -Subproject commit 99893a28eb48de0f1a73f01b376d4daf0d05a56d +Subproject commit 8079c4f63ddc6161c6c9be4681f89a2df2827931 From e1c4cdd39a5770697e62accdd163697078c79c26 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Mon, 23 Jun 2025 11:52:10 +0200 Subject: [PATCH 02/63] updated Compact and fixed issues --- .gitmodules | 7 ++++--- test/util/TestHelper.sol | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 test/util/TestHelper.sol diff --git a/.gitmodules b/.gitmodules index bd60603..85474cd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,12 +7,13 @@ [submodule "lib/forge-gas-snapshot"] path = lib/forge-gas-snapshot url = https://github.com/marktoda/forge-gas-snapshot -[submodule "lib/the-compact"] - path = lib/the-compact - url = https://github.com/uniswap/the-compact [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts [submodule "lib/solady"] path = lib/solady url = https://github.com/vectorized/solady +[submodule "lib/the-compact"] + path = lib/the-compact + url = https://github.com/Uniswap/the-compact + branch = v1 diff --git a/test/util/TestHelper.sol b/test/util/TestHelper.sol new file mode 100644 index 0000000..1de334e --- /dev/null +++ b/test/util/TestHelper.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {IdLib} from 'lib/the-compact/src/lib/IdLib.sol'; + +import {ResetPeriod} from 'lib/the-compact/src/types/ResetPeriod.sol'; +import {Scope} from 'lib/the-compact/src/types/Scope.sol'; + +contract TestHelper { + function _toId(Scope scope, ResetPeriod resetPeriod, address allocator, address token) + internal + pure + returns (uint256 id) + { + uint96 allocatorId = _toAllocatorId(allocator); + bytes12 lockTag = IdLib.toLockTag(allocatorId, scope, resetPeriod); + return uint256(uint256(uint96(lockTag)) << 160) | uint256(uint160(token)); + } + + function _toAllocatorId(address allocator) internal pure returns (uint96 allocatorId) { + return IdLib.toAllocatorId(allocator); + } +} From 209b86b78bc43e8a708c66906463ed7027b78422 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Mon, 23 Jun 2025 13:52:28 +0200 Subject: [PATCH 03/63] fixed compile issues --- test/util/TestHelper.sol | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/util/TestHelper.sol b/test/util/TestHelper.sol index 1de334e..8fdfd2c 100644 --- a/test/util/TestHelper.sol +++ b/test/util/TestHelper.sol @@ -7,6 +7,15 @@ import {ResetPeriod} from 'lib/the-compact/src/types/ResetPeriod.sol'; import {Scope} from 'lib/the-compact/src/types/Scope.sol'; contract TestHelper { + function _toLockTag(address allocator, Scope scope, ResetPeriod resetPeriod) + internal + pure + returns (bytes12 lockTag) + { + uint96 allocatorId = _toAllocatorId(allocator); + return IdLib.toLockTag(allocatorId, scope, resetPeriod); + } + function _toId(Scope scope, ResetPeriod resetPeriod, address allocator, address token) internal pure From 2ac00e202147b4f441c2104fb4c828cfefc4fb18 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Wed, 2 Jul 2025 19:20:54 +0200 Subject: [PATCH 04/63] Added HybridAllocator and HybridERC7683 allocator --- src/allocators/HybridAllocator.sol | 230 +++++++++++++++++++++++++++++ src/allocators/HybridERC7683.sol | 215 +++++++++++++++++++++++++++ 2 files changed, 445 insertions(+) create mode 100644 src/allocators/HybridAllocator.sol create mode 100644 src/allocators/HybridERC7683.sol diff --git a/src/allocators/HybridAllocator.sol b/src/allocators/HybridAllocator.sol new file mode 100644 index 0000000..b251ac3 --- /dev/null +++ b/src/allocators/HybridAllocator.sol @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import {BatchCompact, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; +import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; +import {IERC7683Allocator} from 'src/interfaces/IERC7683Allocator.sol'; + +contract HybridAllocator is IAllocator { + uint96 public immutable ALLOCATOR_ID; + ITheCompact private immutable _COMPACT; + bytes32 private immutable _COMPACT_DOMAIN_SEPARATOR; + + mapping(bytes32 => bool) private claims; + + uint256 public nonce; + uint256 public signerCount; + mapping(address => bool) public signers; + + error Unsupported(); + error InvalidIds(); + error InvalidAllocatorId(uint96 allocatorId, uint96 expectedAllocatorId); + error InvalidCaller(address sender, address expectedSender); + error InvalidAllocatorData(uint256 length); + error InvalidSignature(); + error InvalidSigner(); + error LastSigner(); + error InvalidValue(uint256 value, uint256 expectedValue); + + event ClaimRegistered(address indexed sponsor, uint256[] registeredAmounts, uint256 nonce, bytes32 claimHash); + + modifier onlySigner() { + if (!signers[msg.sender]) { + revert InvalidSigner(); + } + _; + } + + constructor(address compact_, address signer_) { + _COMPACT = ITheCompact(compact_); + ALLOCATOR_ID = _COMPACT.__registerAllocator(address(this), ''); + _COMPACT_DOMAIN_SEPARATOR = _COMPACT.DOMAIN_SEPARATOR(); + + signers[signer_] = true; + signerCount++; + + // Block the first half of nonces for the offchain allocator + nonce = type(uint128).max; + } + + function addSigner(address signer_) external onlySigner { + signers[signer_] = true; + signerCount++; + } + + function removeSigner(address signer_) external onlySigner { + if (signerCount == 1) { + revert LastSigner(); + } + signers[signer_] = false; + signerCount--; + } + + function replaceSigner(address newSigner_) external onlySigner { + signers[msg.sender] = false; + signers[newSigner_] = true; + } + + function attest(address, /*operator*/ address, /*from*/ address, /*to*/ uint256, /*id*/ uint256 /*amount*/ ) + external + pure + returns (bytes4) + { + revert Unsupported(); + } + + function registerClaim( + address recipient, + uint256[2][] memory idsAndAmounts, + address arbiter, + uint256 expires, + bytes32 typehash, + bytes32 witness + ) public payable returns (bytes32, uint256[] memory, uint256) { + idsAndAmounts = _actualIdsAndAmounts(idsAndAmounts); + + (bytes32 claimHash, uint256[] memory registeredAmounts) = _COMPACT.batchDepositAndRegisterFor{value: msg.value}( + recipient, idsAndAmounts, arbiter, ++nonce, expires, typehash, witness + ); + + // Allocate the claim + claims[claimHash] = true; + + emit ClaimRegistered(recipient, registeredAmounts, nonce, claimHash); + + return (claimHash, registeredAmounts, nonce); + } + + function authorizeClaim( + bytes32 claimHash, + address, /*arbiter*/ + address, /*sponsor*/ + uint256, /*nonce*/ + uint256, /*expires*/ + uint256[2][] calldata, /*idsAndAmounts*/ + bytes calldata allocatorData_ + ) external returns (bytes4) { + if (msg.sender != address(_COMPACT)) { + revert InvalidCaller(msg.sender, address(_COMPACT)); + } + + // Check if the claim was allocated on chain + if (claims[claimHash]) { + delete claims[claimHash]; + + // Authorize the claim + return IAllocator.authorizeClaim.selector; + } + + // Check the allocator data for a valid signature by an authorized signer + bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), _COMPACT_DOMAIN_SEPARATOR, claimHash)); + if (!_checkAllocatorData(digest, allocatorData_)) { + revert InvalidSignature(); + } + + // Authorize the claim + return IAllocator.authorizeClaim.selector; + } + + function isClaimAuthorized( + bytes32 claimHash, // The message hash representing the claim. + address, /*arbiter*/ // The account tasked with verifying and submitting the claim. + address, /*sponsor*/ // The account to source the tokens from. + uint256, /*nonce*/ // A parameter to enforce replay protection, scoped to allocator. + uint256, /*expires*/ // The time at which the claim expires. + uint256[2][] calldata, /*idsAndAmounts*/ // The allocated token IDs and amounts. + bytes calldata allocatorData // Arbitrary data provided by the arbiter. + ) external view returns (bool) { + if (claims[claimHash]) { + return true; + } + + // Check the allocator data for a valid signature by an authorized allocator address + bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), _COMPACT_DOMAIN_SEPARATOR, claimHash)); + return _checkAllocatorData(digest, allocatorData); + } + + function _splitId(uint256 id) internal pure returns (uint96 allocatorId_, address token_) { + return (_splitAllocatorId(id), _splitToken(id)); + } + + function _splitAllocatorId(uint256 id) internal pure returns (uint96 allocatorId_) { + assembly ("memory-safe") { + allocatorId_ := shr(164, shl(4, id)) + } + return allocatorId_; + } + + function _splitToken(uint256 id) internal pure returns (address) { + return address(uint160(id)); + } + + function _createLock(uint256 id, uint256 amount) internal pure returns (Lock memory) { + return Lock({lockTag: bytes12(bytes32(id)), token: _splitToken(id), amount: amount}); + } + + function _checkAllocatorData(bytes32 digest, bytes memory allocatorData_) private view returns (bool) { + bytes32 r; + bytes32 s; + uint8 v; + + if (allocatorData_.length == 65) { + (r, s) = abi.decode(allocatorData_, (bytes32, bytes32)); + v = uint8(allocatorData_[64]); + } else if (allocatorData_.length == 64) { + bytes32 vs; + (r, vs) = abi.decode(allocatorData_, (bytes32, bytes32)); + v = uint8(uint256(vs >> 255) + 27); + s = vs << 1 >> 1; + } else { + return false; + } + + // Check if the signer is an authorized allocator address + return signers[ecrecover(digest, v, r, s)]; + } + + function _actualIdsAndAmounts(uint256[2][] memory idsAndAmounts) internal returns (uint256[2][] memory) { + uint256 idIndex = 0; + uint256 idsLength = idsAndAmounts.length; + if (idsLength == 0) { + revert InvalidIds(); + } + + // Check for native token - Native tokens must always be the first id + if (_splitToken(idsAndAmounts[0][0]) == address(0)) { + // Check allocator id + if (_splitAllocatorId(idsAndAmounts[0][0]) != ALLOCATOR_ID) { + revert InvalidAllocatorId(_splitAllocatorId(idsAndAmounts[0][0]), ALLOCATOR_ID); + } + if (idsAndAmounts[0][1] != 0 && msg.value != idsAndAmounts[0][1]) { + revert InvalidValue(msg.value, idsAndAmounts[0][1]); + } + idsAndAmounts[0][1] = msg.value; + + idIndex++; + } + + for (; idIndex < idsLength; idIndex++) { + (uint96 allocatorId_, address token_) = _splitId(idsAndAmounts[idIndex][0]); + + // Check allocator id + if (allocatorId_ != ALLOCATOR_ID) { + revert InvalidAllocatorId(allocatorId_, ALLOCATOR_ID); + } + + if (idsAndAmounts[idIndex][1] == 0) { + // Amount is derived from the allocators token balance + idsAndAmounts[idIndex][1] = IERC20(token_).balanceOf(address(this)); + } + + IERC20(token_).approve(address(_COMPACT), idsAndAmounts[idIndex][1]); + } + + return idsAndAmounts; + } +} diff --git a/src/allocators/HybridERC7683.sol b/src/allocators/HybridERC7683.sol new file mode 100644 index 0000000..858a817 --- /dev/null +++ b/src/allocators/HybridERC7683.sol @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import {LibBytes} from 'solady/utils/LibBytes.sol'; + +import {BatchClaim, Mandate} from './types/TribunalStructs.sol'; +import {BatchCompact, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; + +import {HybridAllocator} from 'src/allocators/HybridAllocator.sol'; +import {BATCH_COMPACT_WITNESS_TYPEHASH, MANDATE_TYPEHASH} from 'src/allocators/lib/TypeHashes.sol'; + +import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; + +contract HybridERC7683 is HybridAllocator, IOriginSettler { + // keccak256("OrderData(address arbiter,address sponsor,uint256 expires,uint256[2][] idsAndAmounts, + // uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt,uint256 targetBlock,uint256 maximumBlocksAfterTarget)") + bytes32 constant ORDERDATA_TYPEHASH = 0xf93147c220566c5d99eedaa4ddd899c0a796b3721e418131b49cc9eedde5054d; + + struct OrderData { + // BATCH COMPACT + address arbiter; // The account tasked with verifying and submitting the claim. + address sponsor; // The account to source the tokens from. + // uint256 nonce; // A parameter to enforce replay protection, scoped to allocator. + uint256 expires; // The time at which the claim expires. + uint256[2][] idsAndAmounts; // The ids of the ERC6909 tokens to allocate. + // MANDATE + uint256 chainId; // (implicit arg, included in EIP712 payload) + address tribunal; // (implicit arg, included in EIP712 payload) + address recipient; // Recipient of settled tokens + // uint256 expires; // Mandate expiration timestamp + address settlementToken; // Settlement token (address(0) for native) + uint256 minimumAmount; // Minimum settlement amount + uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in + uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline) + uint256[] decayCurve; // Block durations, fill increases, & claim decreases. + bytes32 salt; // Replay protection parameter + // ADDITIONAL INPUT + uint256 targetBlock; // The block number at the target chain on which the PGA is executed / the reverse dutch auction starts. + uint256 maximumBlocksAfterTarget; // Blocks after target block that are still fillable. + } + + error InvalidOrderDataType(bytes32 orderDataType, bytes32 expectedOrderDataType); + error InvalidOriginSettler(address originSettler, address expectedOriginSettler); + + constructor(address compact_, address signer_) HybridAllocator(compact_, signer_) {} + + function openFor( + GaslessCrossChainOrder calldata, /*order_*/ + bytes calldata, /*sponsorSignature_*/ + bytes calldata /*originFillerData*/ + ) external pure { + revert Unsupported(); + } + + function open(OnchainCrossChainOrder calldata order_) external { + // Check if orderDataType is the one expected by the allocator + if (order_.orderDataType != ORDERDATA_TYPEHASH) { + revert InvalidOrderDataType(order_.orderDataType, ORDERDATA_TYPEHASH); + } + + OrderData calldata orderData = _decodeOrderData(order_); + + // create witness hash + bytes32 witnessHash = keccak256( + abi.encode( + MANDATE_TYPEHASH, + orderData.chainId, + orderData.tribunal, + orderData.recipient, + order_.fillDeadline, + orderData.settlementToken, + orderData.minimumAmount, + orderData.baselinePriorityFee, + orderData.scalingFactor, + keccak256(abi.encodePacked(orderData.decayCurve)), + orderData.salt + ) + ); + + // register claim + (, uint256[] memory registeredAmounts, uint256 nonce_) = registerClaim( + orderData.sponsor, + orderData.idsAndAmounts, + orderData.arbiter, + orderData.expires, + BATCH_COMPACT_WITNESS_TYPEHASH, + witnessHash + ); + Lock[] memory locks = new Lock[](registeredAmounts.length); + for (uint256 i = 0; i < registeredAmounts.length; i++) { + locks[i] = _createLock(orderData.idsAndAmounts[i][0], registeredAmounts[i]); + } + + BatchCompact memory batchCompact = BatchCompact({ + arbiter: orderData.arbiter, + sponsor: orderData.sponsor, + nonce: nonce_, + expires: orderData.expires, + commitments: locks + }); + + // emit open event + emit Open( + bytes32(batchCompact.nonce), _convertToResolvedCrossChainOrder(orderData, order_.fillDeadline, batchCompact) + ); + } + + function resolveFor(GaslessCrossChainOrder calldata, /*order*/ bytes calldata /*originFillerData*/ ) + external + pure + returns (ResolvedCrossChainOrder memory) + { + revert Unsupported(); + } + + function resolve(OnchainCrossChainOrder calldata order) external view returns (ResolvedCrossChainOrder memory) { + OrderData calldata orderData = _decodeOrderData(order); + uint256 idsLength = orderData.idsAndAmounts.length; + Lock[] memory locks = new Lock[](idsLength); + for (uint256 i = 0; i < idsLength; i++) { + uint256 id = orderData.idsAndAmounts[i][0]; + locks[i] = _createLock(id, orderData.idsAndAmounts[i][1]); + } + BatchCompact memory batchCompact = BatchCompact({ + arbiter: orderData.arbiter, + sponsor: orderData.sponsor, + nonce: nonce + 1, // nonce is incremented by 1 when the claim is registered + expires: orderData.expires, + commitments: locks + }); + return _convertToResolvedCrossChainOrder(orderData, order.fillDeadline, batchCompact); + } + + /** + * OnChainCrossChainOrder + * - 1 word for fillDeadline + * - 1 word for orderDataType + * - 1 word for orderData offset + */ + function _decodeOrderData(OnchainCrossChainOrder calldata order_) + internal + pure + returns (OrderData calldata orderData) + { + // bytes calldata rawOrderData = LibBytes.dynamicStructInCalldata(order_.orderData, 0x40); + assembly ("memory-safe") { + orderData := order_ + } + } + + function _convertToResolvedCrossChainOrder( + OrderData calldata orderData, + uint256 fillDeadline, + BatchCompact memory batchCompact + ) internal view returns (ResolvedCrossChainOrder memory) { + Output[] memory maxSpent = new Output[](1); + maxSpent[0] = Output({ + token: bytes32(uint256(uint160(orderData.settlementToken))), + amount: type(uint256).max, + recipient: bytes32(uint256(uint160(orderData.recipient))), + chainId: orderData.chainId + }); + + uint256 idsLength = orderData.idsAndAmounts.length; + Output[] memory minReceived = new Output[](idsLength); + for (uint256 i = 0; i < idsLength; i++) { + minReceived[i] = Output({ + token: bytes32(uint256(uint160(orderData.idsAndAmounts[i][0]))), + amount: orderData.minimumAmount, + recipient: _convertAddressToBytes32(orderData.recipient), + chainId: block.chainid + }); + } + + Mandate memory mandate = Mandate({ + recipient: orderData.recipient, + expires: fillDeadline, + token: orderData.settlementToken, + minimumAmount: orderData.minimumAmount, + baselinePriorityFee: orderData.baselinePriorityFee, + scalingFactor: orderData.scalingFactor, + decayCurve: orderData.decayCurve, + salt: orderData.salt + }); + BatchClaim memory claim = BatchClaim({ + chainId: block.chainid, + compact: batchCompact, + sponsorSignature: '', // No signature required from the sponsor, the claim will be verified via the on chain registration. + allocatorSignature: '' // No signature required from this allocator, it will verify the claim on chain via ERC1271. + }); + + FillInstruction[] memory fillInstructions = new FillInstruction[](1); + fillInstructions[0] = FillInstruction({ + destinationChainId: orderData.chainId, + destinationSettler: _convertAddressToBytes32(orderData.tribunal), + originData: abi.encode(claim, mandate, orderData.targetBlock, orderData.maximumBlocksAfterTarget) + }); + + return ResolvedCrossChainOrder({ + user: orderData.sponsor, + originChainId: block.chainid, + openDeadline: uint32(fillDeadline), + fillDeadline: uint32(fillDeadline), + orderId: bytes32(batchCompact.nonce), + maxSpent: maxSpent, + minReceived: minReceived, + fillInstructions: fillInstructions + }); + } + + function _convertAddressToBytes32(address address_) internal pure returns (bytes32) { + return bytes32(uint256(uint160(address_))); + } +} From 9af925719268f133d90f4aad918425c4cf6822a1 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Wed, 2 Jul 2025 19:22:52 +0200 Subject: [PATCH 05/63] Updated ERC7683Allocator to support coverage (wip) --- snapshots/HybridAllocatorTest.json | 7 ++++++ src/allocators/lib/TypeHashes.sol | 13 ++++++++++ src/allocators/types/TribunalStructs.sol | 32 ++++++++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 snapshots/HybridAllocatorTest.json create mode 100644 src/allocators/lib/TypeHashes.sol create mode 100644 src/allocators/types/TribunalStructs.sol diff --git a/snapshots/HybridAllocatorTest.json b/snapshots/HybridAllocatorTest.json new file mode 100644 index 0000000..cd3ac4d --- /dev/null +++ b/snapshots/HybridAllocatorTest.json @@ -0,0 +1,7 @@ +{ + "registerClaim_erc20Token": "152710", + "registerClaim_erc20Token_emptyAmountInput": "154351", + "registerClaim_multipleTokens": "189856", + "registerClaim_nativeToken": "103613", + "registerClaim_nativeToken_emptyAmountInput": "103512" +} diff --git a/src/allocators/lib/TypeHashes.sol b/src/allocators/lib/TypeHashes.sol new file mode 100644 index 0000000..86df937 --- /dev/null +++ b/src/allocators/lib/TypeHashes.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +// keccak256("Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") +bytes32 constant MANDATE_TYPEHASH = 0x74d9c10530859952346f3e046aa2981a24bb7524b8394eb45a9deddced9d6501; + +// keccak256("BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments)Lock(bytes12 lockTag,address token,uint256 amount,Mandate mandate) +// Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") +bytes32 constant BATCH_COMPACT_WITNESS_TYPEHASH = 0xcddb20593d74f00cd789982c798bca41f8ba5f6835c95a771fd48b110d8b1249; + +// keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,bytes12 lockTag,address token,uint256 amount,Mandate mandate) +// Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") +bytes32 constant COMPACT_WITNESS_TYPEHASH = 0x2ec0d30491bb66a6eb554b9d53f490d79b54fc5f4963bed4b2bb8096b4790f1f; diff --git a/src/allocators/types/TribunalStructs.sol b/src/allocators/types/TribunalStructs.sol new file mode 100644 index 0000000..40ab38f --- /dev/null +++ b/src/allocators/types/TribunalStructs.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import {BatchCompact, Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; + +struct Claim { + uint256 chainId; // Claim processing chain ID + Compact compact; + bytes sponsorSignature; // Authorization from the sponsor + bytes allocatorSignature; // Authorization from the allocator +} + +struct BatchClaim { + uint256 chainId; // Claim processing chain ID + BatchCompact compact; + bytes sponsorSignature; // Authorization from the sponsor + bytes allocatorSignature; // Authorization from the allocator +} + +struct Mandate { + // uint256 chainId; // (implicit arg, included in EIP712 payload). + // address tribunal; // (implicit arg, included in EIP712 payload). + address recipient; // Recipient of filled tokens. + uint256 expires; // Mandate expiration timestamp. + address token; // Fill token (address(0) for native). + uint256 minimumAmount; // Minimum fill amount. + uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in. + uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline). + uint256[] decayCurve; // Block durations, fill increases, & claim decreases. + bytes32 salt; // Replay protection parameter. +} From 8b48afb0e6c9951e56e0860f9ddb3046853e155d Mon Sep 17 00:00:00 2001 From: mgretzke Date: Wed, 2 Jul 2025 19:23:19 +0200 Subject: [PATCH 06/63] Started working on tests --- test/HybridAllocator.t.sol | 728 +++++++++++++++++++++++++++++++++++++ test/util/TestHelper.sol | 104 ++++++ 2 files changed, 832 insertions(+) create mode 100644 test/HybridAllocator.t.sol diff --git a/test/HybridAllocator.t.sol b/test/HybridAllocator.t.sol new file mode 100644 index 0000000..7059cbf --- /dev/null +++ b/test/HybridAllocator.t.sol @@ -0,0 +1,728 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +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 {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 {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 {BATCH_COMPACT_WITNESS_TYPEHASH} from 'src/allocators/lib/TypeHashes.sol'; +import {ERC20Mock} from 'src/test/ERC20Mock.sol'; + +contract HybridAllocatorTest is Test, TestHelper { + TheCompact compact; + address arbiter; + HybridAllocator allocator; + address signer; + uint256 signerPrivateKey; + ERC20Mock usdc; + address user; + uint256 userPrivateKey; + uint256 defaultAmount; + uint256 defaultExpiration; + + BatchCompact batchCompact; + + function setUp() public { + compact = new TheCompact(); + arbiter = makeAddr('arbiter'); + (signer, signerPrivateKey) = makeAddrAndKey('signer'); + allocator = new HybridAllocator(address(compact), signer); + usdc = new ERC20Mock('USDC', 'USDC'); + (user, userPrivateKey) = makeAddrAndKey('user'); + deal(user, 1 ether); + usdc.mint(user, 1 ether); + defaultAmount = 1 ether; + defaultExpiration = vm.getBlockTimestamp() + 1 days; + + batchCompact.arbiter = arbiter; + batchCompact.sponsor = user; + batchCompact.nonce = uint256(type(uint128).max) + 1; + batchCompact.expires = defaultExpiration; + } + + function test_checkAllocatorId() public view { + assertEq(allocator.ALLOCATOR_ID(), _toAllocatorId(address(allocator))); + } + + function test_checkNonce() public view { + assertEq(allocator.nonce(), type(uint128).max); + } + + function test_checkSignerCount() public view { + assertEq(allocator.signerCount(), 1); + } + + function test_checkSigners(address attacker) public view { + vm.assume(attacker != signer); + + assertTrue(allocator.signers(signer)); + assertFalse(allocator.signers(attacker)); + } + + function test_registerClaim_revert_InvalidIds() public { + vm.expectRevert(HybridAllocator.InvalidIds.selector); + allocator.registerClaim(user, new uint256[2][](0), arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); + } + + function test_registerClaim_revert_InvalidAllocatorIdNative() public { + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = + _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(this), /* wrong address */ address(0)); + idsAndAmounts[0][1] = defaultAmount; + vm.expectRevert( + abi.encodeWithSelector( + HybridAllocator.InvalidAllocatorId.selector, _toAllocatorId(address(this)), allocator.ALLOCATOR_ID() + ) + ); + allocator.registerClaim{value: defaultAmount}( + user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, '' + ); + } + + function test_registerClaim_revert_InvalidAllocatorIdERC20() public { + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = + _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(this), /* wrong address */ address(usdc)); + idsAndAmounts[0][1] = defaultAmount; + vm.expectRevert( + abi.encodeWithSelector( + HybridAllocator.InvalidAllocatorId.selector, _toAllocatorId(address(this)), allocator.ALLOCATOR_ID() + ) + ); + allocator.registerClaim(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); + } + + function test_registerClaim_revert_InvalidValue() public { + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = + _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0) /* use native */ ); + idsAndAmounts[0][1] = defaultAmount; + vm.expectRevert(abi.encodeWithSelector(HybridAllocator.InvalidValue.selector, defaultAmount + 1, defaultAmount)); + allocator.registerClaim{value: defaultAmount + 1}( + user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, '' + ); + } + + function test_registerClaim_revert_zeroNativeTokensAmount() public { + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); + idsAndAmounts[0][1] = 0; + vm.expectRevert(abi.encodeWithSelector(ITheCompact.InvalidBatchDepositStructure.selector)); + allocator.registerClaim(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); + } + + function test_registerClaim_revert_zeroTokensAmount() public { + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); + idsAndAmounts[0][1] = 0; + vm.expectRevert(abi.encodeWithSelector(ITheCompact.InvalidDepositBalanceChange.selector)); + allocator.registerClaim(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); + } + + function test_registerClaim_revert_tokensNotProvided() public { + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); + idsAndAmounts[0][1] = defaultAmount; + vm.expectRevert(abi.encodeWithSignature('TransferFromFailed()')); + allocator.registerClaim(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); + } + + function test_registerClaim_revert_invalidTokenOrder() public { + uint256[2][] memory idsAndAmounts = new uint256[2][](2); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); + idsAndAmounts[0][1] = 0; + + idsAndAmounts[1][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); + idsAndAmounts[1][1] = 0; + + // Provide tokens + vm.prank(user); + usdc.transfer(address(allocator), defaultAmount); + assertEq(usdc.balanceOf(address(allocator)), defaultAmount); + + vm.expectRevert(); // Will revert when trying to approve tokens of address(0) + allocator.registerClaim{value: defaultAmount}( + user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, '' + ); + } + + function test_registerClaim_success_nativeToken() public { + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = + _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0) /* use native */ ); + idsAndAmounts[0][1] = defaultAmount; + (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = allocator.registerClaim{ + value: defaultAmount + }(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); + vm.snapshotGasLastCall('registerClaim_nativeToken'); + + assertTrue(compact.isRegistered(user, claimHash, BATCH_COMPACT_TYPEHASH)); + assertTrue(allocator.isClaimAuthorized(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); + assertEq(registeredAmounts[0], defaultAmount); + assertEq(registeredAmounts.length, 1); + assertEq(address(compact).balance, defaultAmount); + assertEq(compact.balanceOf(address(user), idsAndAmounts[0][0]), defaultAmount); + assertEq(nonce, type(uint128).max + 1); + } + + function test_registerClaim_success_erc20Token() public { + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); + idsAndAmounts[0][1] = defaultAmount; + + // Provide tokens + vm.prank(user); + usdc.transfer(address(allocator), defaultAmount); + assertEq(usdc.balanceOf(address(allocator)), defaultAmount); + + (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = + allocator.registerClaim(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); + vm.snapshotGasLastCall('registerClaim_erc20Token'); + + assertTrue(compact.isRegistered(user, claimHash, BATCH_COMPACT_TYPEHASH)); + assertTrue(allocator.isClaimAuthorized(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); + assertEq(registeredAmounts[0], defaultAmount); + assertEq(usdc.balanceOf(address(compact)), defaultAmount); + assertEq(compact.balanceOf(address(user), idsAndAmounts[0][0]), defaultAmount); + assertEq(nonce, type(uint128).max + 1); + } + + function test_registerClaim_success_nativeTokenWithEmptyAmountInput() public { + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = + _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0) /* use native */ ); + idsAndAmounts[0][1] = 0; + (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = allocator.registerClaim{ + value: defaultAmount + }(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); + vm.snapshotGasLastCall('registerClaim_nativeToken_emptyAmountInput'); + + assertTrue(compact.isRegistered(user, claimHash, BATCH_COMPACT_TYPEHASH)); + assertTrue(allocator.isClaimAuthorized(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); + assertEq(registeredAmounts[0], defaultAmount); + assertEq(address(compact).balance, defaultAmount); + assertEq(compact.balanceOf(address(user), idsAndAmounts[0][0]), defaultAmount); + assertEq(nonce, type(uint128).max + 1); + } + + function test_registerClaim_success_erc20TokenWithEmptyAmountInput() public { + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); + idsAndAmounts[0][1] = 0; + + // Provide tokens + vm.prank(user); + usdc.transfer(address(allocator), defaultAmount); + assertEq(usdc.balanceOf(address(allocator)), defaultAmount); + + (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = + allocator.registerClaim(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); + vm.snapshotGasLastCall('registerClaim_erc20Token_emptyAmountInput'); + + assertTrue(compact.isRegistered(user, claimHash, BATCH_COMPACT_TYPEHASH)); + assertTrue(allocator.isClaimAuthorized(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); + assertEq(registeredAmounts[0], defaultAmount); + assertEq(registeredAmounts.length, 1); + assertEq(usdc.balanceOf(address(compact)), defaultAmount); + assertEq(compact.balanceOf(address(user), idsAndAmounts[0][0]), defaultAmount); + assertEq(nonce, type(uint128).max + 1); + } + + function test_registerClaim_success_multipleTokens() public { + uint256[2][] memory idsAndAmounts = new uint256[2][](2); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); + idsAndAmounts[0][1] = 0; + + idsAndAmounts[1][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); + idsAndAmounts[1][1] = 0; + + // Provide tokens + vm.prank(user); + usdc.transfer(address(allocator), defaultAmount); + assertEq(usdc.balanceOf(address(allocator)), defaultAmount); + + (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = allocator.registerClaim{ + value: defaultAmount + }(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); + vm.snapshotGasLastCall('registerClaim_multipleTokens'); + + assertTrue(compact.isRegistered(user, claimHash, BATCH_COMPACT_TYPEHASH)); + assertTrue(allocator.isClaimAuthorized(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); + assertEq(registeredAmounts[0], defaultAmount); + assertEq(registeredAmounts[1], defaultAmount); + assertEq(registeredAmounts.length, 2); + assertEq(usdc.balanceOf(address(compact)), defaultAmount); + 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, type(uint128).max + 1); + } + + function test_registerClaim_checkNonceIncrements() public { + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); + idsAndAmounts[0][1] = 0; + + // Register first claim + allocator.registerClaim{value: 5e17}( + user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, '' + ); + // Register second claim + (bytes32 claimHash, uint256[] memory registeredAmounts,) = allocator.registerClaim{value: 5e17}( + user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, '' + ); + + assertTrue(compact.isRegistered(user, claimHash, BATCH_COMPACT_TYPEHASH)); + assertTrue(allocator.isClaimAuthorized(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); + assertEq(registeredAmounts[0], 5e17); + assertEq(registeredAmounts.length, 1); + assertEq(address(compact).balance, defaultAmount); + assertEq(compact.balanceOf(address(user), idsAndAmounts[0][0]), defaultAmount); + assertEq(allocator.nonce(), type(uint128).max + 1); + } + + function test_registerClaim_checkClaimHashNoWitness() public { + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); + idsAndAmounts[0][1] = 0; + + (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = allocator.registerClaim{ + value: defaultAmount + }(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); + BatchCompact memory batch = _updateBatchCompact(batchCompact, idsAndAmounts, registeredAmounts, nonce); + + bytes32 createdHash = _toBatchCompactHash(batch); + assertEq(createdHash, claimHash); + assertTrue(allocator.isClaimAuthorized(createdHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); + } + + function test_registerClaim_checkClaimHashWitness() public { + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); + idsAndAmounts[0][1] = 0; + + bytes32 witness = keccak256(abi.encode(WITNESS_TYPEHASH, 1)); + + (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = allocator.registerClaim{ + value: defaultAmount + }(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH_WITH_WITNESS, witness); + BatchCompact memory batch = _updateBatchCompact(batchCompact, idsAndAmounts, registeredAmounts, nonce); + bytes32 createdHash = _toBatchCompactHashWithWitness(BATCH_COMPACT_TYPEHASH_WITH_WITNESS, batch, witness); + assertEq(createdHash, claimHash); + assertTrue(allocator.isClaimAuthorized(createdHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); + } + + function test_isClaimAuthorized_unauthorized() public { + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); + idsAndAmounts[0][1] = 0; + + (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = allocator.registerClaim{ + value: defaultAmount + }(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); + BatchCompact memory batch = _updateBatchCompact(batchCompact, idsAndAmounts, registeredAmounts, nonce); + + // Use the same batchCompact, but add a witness + bytes32 falseHash = _toBatchCompactHashWithWitness(BATCH_COMPACT_TYPEHASH_WITH_WITNESS, batch, bytes32(0)); + assertNotEq(falseHash, claimHash); + assertFalse(allocator.isClaimAuthorized(falseHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); + } + + function test_isClaimAuthorized_withSigner_bytes64() public view { + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); + idsAndAmounts[0][1] = defaultAmount; + + bytes32 witness = keccak256(abi.encode(WITNESS_TYPEHASH, 1)); + BatchCompact memory batch = _updateBatchCompact(batchCompact, idsAndAmounts, 1); + + bytes32 claimHash = _toBatchCompactHashWithWitness(BATCH_COMPACT_TYPEHASH_WITH_WITNESS, batch, witness); + bytes32 digest = _toDigest(claimHash, compact.DOMAIN_SEPARATOR()); + (bytes32 r, bytes32 vs) = vm.signCompact(signerPrivateKey, digest); + bytes memory signature = abi.encodePacked(r, vs); + assertEq(signature.length, 64); + assertTrue( + allocator.isClaimAuthorized(claimHash, arbiter, user, 1, defaultExpiration, idsAndAmounts, signature) + ); + } + + function test_isClaimAuthorized_withSigner_bytes65() public view { + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); + idsAndAmounts[0][1] = defaultAmount; + + bytes32 witness = keccak256(abi.encode(WITNESS_TYPEHASH, 1)); + BatchCompact memory batch = _updateBatchCompact(batchCompact, idsAndAmounts, 1); + + bytes32 claimHash; + bytes memory signature; + { + claimHash = _toBatchCompactHashWithWitness(BATCH_COMPACT_TYPEHASH_WITH_WITNESS, batch, witness); + bytes32 digest = _toDigest(claimHash, compact.DOMAIN_SEPARATOR()); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, digest); + signature = abi.encodePacked(r, s, v); + } + assertEq(signature.length, 65); + assertTrue( + allocator.isClaimAuthorized(claimHash, arbiter, user, 1, defaultExpiration, idsAndAmounts, signature) + ); + } + + function test_isClaimAuthorized_invalidSignature() public { + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); + idsAndAmounts[0][1] = defaultAmount; + + bytes32 witness = keccak256(abi.encode(WITNESS_TYPEHASH, 1)); + BatchCompact memory batch = _updateBatchCompact(batchCompact, idsAndAmounts, 1); + + bytes32 claimHash = _toBatchCompactHashWithWitness(BATCH_COMPACT_TYPEHASH_WITH_WITNESS, batch, witness); + bytes32 digest = _toDigest(claimHash, compact.DOMAIN_SEPARATOR()); + + bytes memory signature; + { + (, uint256 attackerPK) = makeAddrAndKey('attacker'); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(attackerPK, digest); + signature = abi.encodePacked(r, s, v); + assertEq(signature.length, 65); + } + assertFalse( + allocator.isClaimAuthorized(claimHash, arbiter, user, 1, defaultExpiration, idsAndAmounts, signature) + ); + } + + function test_attest_revert_Unsupported() public { + uint256 id = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); + address target = makeAddr('target'); + + assertEq(usdc.balanceOf(user), defaultAmount); + + vm.expectRevert(abi.encodeWithSelector(HybridAllocator.Unsupported.selector)); + allocator.attest(signer, user, target, id, defaultAmount); + } + + function test_attest_revert_transferFailed() public { + uint256 id = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); + address target = makeAddr('target'); + + assertEq(usdc.balanceOf(user), defaultAmount); + vm.startPrank(user); + usdc.approve(address(compact), defaultAmount); + compact.depositERC20(address(usdc), bytes12(bytes32(id)), defaultAmount, user); + + vm.expectRevert(abi.encodeWithSelector(HybridAllocator.Unsupported.selector), address(allocator)); + compact.transfer(target, id, defaultAmount); + vm.stopPrank(); + } + + function test_authorizeClaim_revert_invalidCaller(address attacker) public { + vm.assume(attacker != address(compact)); + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); + idsAndAmounts[0][1] = 0; + + // Provide tokens + vm.prank(user); + usdc.transfer(address(allocator), defaultAmount); + assertEq(usdc.balanceOf(address(allocator)), defaultAmount); + assertEq(address(allocator).balance, 0); + + bytes32 witness = keccak256(abi.encode(WITNESS_TYPEHASH, 1)); + + vm.prank(user); + (bytes32 claimHash,,) = allocator.registerClaim{value: defaultAmount}( + user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH_WITH_WITNESS, witness + ); + + vm.prank(attacker); + vm.expectRevert(abi.encodeWithSelector(HybridAllocator.InvalidCaller.selector, attacker, address(compact))); + allocator.authorizeClaim(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), ''); + } + + function test_authorizeClaim_success_onChain() public { + uint256[2][] memory idsAndAmounts = new uint256[2][](2); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); + idsAndAmounts[0][1] = 0; + + idsAndAmounts[1][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); + idsAndAmounts[1][1] = 0; + + // Provide tokens + vm.prank(user); + usdc.transfer(address(allocator), defaultAmount); + assertEq(usdc.balanceOf(address(allocator)), defaultAmount); + assertEq(address(allocator).balance, 0); + + bytes32 witness = keccak256(abi.encode(WITNESS_TYPEHASH, 1)); + + vm.prank(user); + (bytes32 claimHash,, uint256 nonce) = allocator.registerClaim{value: defaultAmount}( + user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH_WITH_WITNESS, witness + ); + + bytes32 digest = _toDigest(claimHash, compact.DOMAIN_SEPARATOR()); + (bytes32 r, bytes32 vs) = vm.signCompact(userPrivateKey, digest); + bytes memory sponsorSignature = abi.encodePacked(r, vs); + + address target = makeAddr('target'); + + bytes32 returnedClaimHash; + { + Component[] memory portions = new Component[](1); + portions[0] = Component({ + claimant: uint256(bytes32(abi.encodePacked(bytes12(0), target))), // indicating a withdrawal + amount: defaultAmount + }); + + BatchClaimComponent[] memory claims = new BatchClaimComponent[](2); + claims[0] = + BatchClaimComponent({id: idsAndAmounts[0][0], allocatedAmount: defaultAmount, portions: portions}); + claims[1] = + BatchClaimComponent({id: idsAndAmounts[1][0], allocatedAmount: defaultAmount, portions: portions}); + + BatchClaim memory claim = BatchClaim({ + allocatorData: '', + sponsorSignature: sponsorSignature, + sponsor: user, + nonce: nonce, + expires: defaultExpiration, + witness: witness, + witnessTypestring: WITNESS_STRING, + claims: claims + }); + vm.prank(arbiter); + returnedClaimHash = compact.batchClaim(claim); + 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'); + 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(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( + compact.balanceOf(address(target), idsAndAmounts[0][0]), 0, 'target eth compact balance of 0 should be 0' + ); + assertEq(compact.balanceOf(address(user), idsAndAmounts[1][0]), 0, 'user usdc compact balance of 0 should be 0'); + assertEq( + compact.balanceOf(address(target), idsAndAmounts[1][0]), 0, 'target usdc compact balance of 0 should be 0' + ); + } + + function test_authorizeClaim_success_offChain(uint128 nonce) 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; + + idsAndAmounts[1][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); + idsAndAmounts[1][1] = defaultAmount; + + // Approve tokens + vm.prank(user); + usdc.approve(address(compact), defaultAmount); + + bytes32 witness = keccak256(abi.encode(WITNESS_TYPEHASH, 1)); + + bytes32 claimHash = _toBatchCompactHashWithWitness( + BATCH_COMPACT_TYPEHASH_WITH_WITNESS, + BatchCompact({ + arbiter: arbiter, + sponsor: user, + nonce: nonce, + expires: defaultExpiration, + commitments: _idsAndAmountsToCommitments(idsAndAmounts) + }), + witness + ); + + bytes32[2][] memory claimHashesAndTypehashes = new bytes32[2][](1); + claimHashesAndTypehashes[0][0] = claimHash; + claimHashesAndTypehashes[0][1] = BATCH_COMPACT_TYPEHASH_WITH_WITNESS; + + // Deposit and register + vm.prank(user); + compact.batchDepositAndRegisterMultiple{value: defaultAmount}(idsAndAmounts, claimHashesAndTypehashes); + + // Off chain signing the claim + bytes32 digest = _toDigest(claimHash, compact.DOMAIN_SEPARATOR()); + (bytes32 r, bytes32 vs) = vm.signCompact(signerPrivateKey, digest); + bytes memory allocatorData = abi.encodePacked(r, vs); + + address target = makeAddr('target'); + + BatchClaimComponent[] memory claims = new BatchClaimComponent[](2); + { + Component[] memory portions = new Component[](1); + portions[0] = Component({ + claimant: uint256(bytes32(abi.encodePacked(bytes12(0), target))), // indicating a withdrawal + amount: defaultAmount + }); + + claims[0] = + BatchClaimComponent({id: idsAndAmounts[0][0], allocatedAmount: defaultAmount, portions: portions}); + claims[1] = + BatchClaimComponent({id: idsAndAmounts[1][0], allocatedAmount: defaultAmount, portions: portions}); + } + + BatchClaim memory claim = BatchClaim({ + allocatorData: allocatorData, + sponsorSignature: '', + sponsor: user, + nonce: nonce, + expires: defaultExpiration, + witness: witness, + witnessTypestring: WITNESS_STRING, + claims: claims + }); + + vm.prank(arbiter); + bytes32 returnedClaimHash = compact.batchClaim(claim); + 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'); + 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(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( + compact.balanceOf(address(target), idsAndAmounts[0][0]), 0, 'target eth compact balance of 0 should be 0' + ); + assertEq(compact.balanceOf(address(user), idsAndAmounts[1][0]), 0, 'user usdc compact balance of 0 should be 0'); + assertEq( + compact.balanceOf(address(target), idsAndAmounts[1][0]), 0, 'target usdc compact balance of 0 should be 0' + ); + } + + function test_authorizeClaim_registrationDeleted() public { + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); + idsAndAmounts[0][1] = 0; + + vm.prank(user); + (bytes32 claimHash,, uint256 nonce) = allocator.registerClaim{value: defaultAmount}( + user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, '' + ); + + bytes32 digest = _toDigest(claimHash, compact.DOMAIN_SEPARATOR()); + (bytes32 r, bytes32 vs) = vm.signCompact(userPrivateKey, digest); + bytes memory sponsorSignature = abi.encodePacked(r, vs); + + address target = makeAddr('target'); + + Component[] memory portions = new Component[](1); + portions[0] = Component({ + claimant: uint256(bytes32(abi.encodePacked(bytes12(0), target))), // indicating a withdrawal + amount: defaultAmount + }); + + BatchClaimComponent[] memory claims = new BatchClaimComponent[](1); + claims[0] = BatchClaimComponent({id: idsAndAmounts[0][0], allocatedAmount: defaultAmount, portions: portions}); + + BatchClaim memory claim = BatchClaim({ + allocatorData: '', + sponsorSignature: sponsorSignature, + sponsor: user, + nonce: nonce, + expires: defaultExpiration, + witness: '', + witnessTypestring: '', + claims: claims + }); + + vm.prank(arbiter); + bytes32 returnedClaimHash = compact.batchClaim(claim); + assertEq(returnedClaimHash, claimHash); + + assertFalse(allocator.isClaimAuthorized(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); + } + + function test_addSigner_revert_InvalidSigner(address attacker) public { + vm.assume(attacker != signer); + vm.prank(attacker); + vm.expectRevert(abi.encodeWithSelector(HybridAllocator.InvalidSigner.selector)); + allocator.addSigner(attacker); + assertEq(allocator.signerCount(), 1); + assertFalse(allocator.signers(attacker)); + } + + function test_addSigner_success(address newSigner) public { + vm.assume(newSigner != signer); + vm.prank(signer); + allocator.addSigner(newSigner); + assertEq(allocator.signerCount(), 2); + assertTrue(allocator.signers(newSigner)); + assertTrue(allocator.signers(signer)); + } + + function test_removeSigner_revert_InvalidSigner(address attacker) public { + vm.assume(attacker != signer); + vm.prank(attacker); + vm.expectRevert(abi.encodeWithSelector(HybridAllocator.InvalidSigner.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(HybridAllocator.LastSigner.selector)); + allocator.removeSigner(signer); + assertEq(allocator.signerCount(), 1); + assertTrue(allocator.signers(signer)); + } + + function test_removeSigner_success(address newSigner) public { + vm.assume(newSigner != signer); + vm.prank(signer); + allocator.addSigner(newSigner); + assertEq(allocator.signerCount(), 2); + vm.prank(newSigner); + allocator.removeSigner(signer); + assertEq(allocator.signerCount(), 1); + assertFalse(allocator.signers(signer)); + assertTrue(allocator.signers(newSigner)); + } + + function test_removeSigner_success_deleteSelf(address newSigner) public { + vm.assume(newSigner != signer); + vm.prank(signer); + allocator.addSigner(newSigner); + assertEq(allocator.signerCount(), 2); + vm.prank(newSigner); + allocator.removeSigner(newSigner); + assertEq(allocator.signerCount(), 1); + assertTrue(allocator.signers(signer)); + assertFalse(allocator.signers(newSigner)); + } + + function test_replaceSigner_revert_InvalidSigner(address attacker) public { + vm.assume(attacker != signer); + vm.prank(attacker); + vm.expectRevert(abi.encodeWithSelector(HybridAllocator.InvalidSigner.selector)); + allocator.replaceSigner(attacker); + assertEq(allocator.signerCount(), 1); + assertFalse(allocator.signers(attacker)); + } + + function test_replaceSigner_success(address newSigner) public { + vm.assume(newSigner != signer); + vm.prank(signer); + allocator.replaceSigner(newSigner); + assertEq(allocator.signerCount(), 1); + assertFalse(allocator.signers(signer)); + assertTrue(allocator.signers(newSigner)); + } +} diff --git a/test/util/TestHelper.sol b/test/util/TestHelper.sol index 8fdfd2c..0729f24 100644 --- a/test/util/TestHelper.sol +++ b/test/util/TestHelper.sol @@ -2,11 +2,19 @@ pragma solidity ^0.8.27; import {IdLib} from 'lib/the-compact/src/lib/IdLib.sol'; +import {BATCH_COMPACT_TYPEHASH, BatchCompact, LOCK_TYPEHASH, Lock} from 'lib/the-compact/src/types/EIP712Types.sol'; import {ResetPeriod} from 'lib/the-compact/src/types/ResetPeriod.sol'; import {Scope} from 'lib/the-compact/src/types/Scope.sol'; contract TestHelper { + string constant BATCH_COMPACT_TYPESTRING_WITH_WITNESS = + '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_TYPEHASH_WITH_WITNESS = keccak256(bytes(BATCH_COMPACT_TYPESTRING_WITH_WITNESS)); + string constant WITNESS_STRING = 'uint256 witness'; + string constant WITNESS_TYPESTRING = string(abi.encodePacked('Mandate(', WITNESS_STRING, ')')); + bytes32 constant WITNESS_TYPEHASH = keccak256(bytes(WITNESS_TYPESTRING)); + function _toLockTag(address allocator, Scope scope, ResetPeriod resetPeriod) internal pure @@ -29,4 +37,100 @@ contract TestHelper { function _toAllocatorId(address allocator) internal pure returns (uint96 allocatorId) { return IdLib.toAllocatorId(allocator); } + + function _updateBatchCompact( + BatchCompact memory batchCompact, + uint256[2][] memory idsAndAmounts, + uint256[] memory registeredAmounts, + uint256 nonce + ) internal pure returns (BatchCompact memory) { + batchCompact.commitments = new Lock[](idsAndAmounts.length); + for (uint256 i = 0; i < idsAndAmounts.length; i++) { + batchCompact.commitments[i] = Lock({ + lockTag: bytes12(bytes32(idsAndAmounts[i][0])), + token: address(uint160(idsAndAmounts[i][0])), + amount: registeredAmounts[i] + }); + } + batchCompact.nonce = nonce; + return batchCompact; + } + + function _updateBatchCompact(BatchCompact memory batchCompact, uint256[2][] memory idsAndAmounts, uint256 nonce) + internal + pure + returns (BatchCompact memory) + { + batchCompact.commitments = new Lock[](idsAndAmounts.length); + for (uint256 i = 0; i < idsAndAmounts.length; i++) { + batchCompact.commitments[i] = Lock({ + lockTag: bytes12(bytes32(idsAndAmounts[i][0])), + token: address(uint160(idsAndAmounts[i][0])), + amount: idsAndAmounts[i][1] + }); + } + batchCompact.nonce = nonce; + return batchCompact; + } + + function _toBatchCompactHash(BatchCompact memory batchCompact) internal pure returns (bytes32) { + return keccak256( + abi.encode( + BATCH_COMPACT_TYPEHASH, + batchCompact.arbiter, + batchCompact.sponsor, + batchCompact.nonce, + batchCompact.expires, + _toBatchCompactCommitmentsHash(batchCompact.commitments) + ) + ); + } + + function _toBatchCompactHashWithWitness(bytes32 typeHash, BatchCompact memory batchCompact, bytes32 witness) + internal + pure + returns (bytes32) + { + return keccak256( + abi.encode( + typeHash, + batchCompact.arbiter, + batchCompact.sponsor, + batchCompact.nonce, + batchCompact.expires, + _toBatchCompactCommitmentsHash(batchCompact.commitments), + witness + ) + ); + } + + function _toBatchCompactCommitmentsHash(Lock[] memory commitments) internal pure returns (bytes32) { + bytes32[] memory commitmentsHashes = new bytes32[](commitments.length); + for (uint256 i = 0; i < commitments.length; i++) { + commitmentsHashes[i] = keccak256( + abi.encode(LOCK_TYPEHASH, commitments[i].lockTag, commitments[i].token, commitments[i].amount) + ); + } + return keccak256(abi.encodePacked(commitmentsHashes)); + } + + function _toDigest(bytes32 claimHash, bytes32 domainSeparator) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(bytes2(0x1901), domainSeparator, claimHash)); + } + + function _idsAndAmountsToCommitments(uint256[2][] memory idsAndAmounts) + internal + pure + returns (Lock[] memory commitments) + { + commitments = new Lock[](idsAndAmounts.length); + for (uint256 i = 0; i < idsAndAmounts.length; i++) { + commitments[i] = Lock({ + lockTag: bytes12(bytes32(idsAndAmounts[i][0])), + token: address(uint160(idsAndAmounts[i][0])), + amount: idsAndAmounts[i][1] + }); + } + return commitments; + } } From 68e1831d5a1d826bbc42527ea34eaba98670129e Mon Sep 17 00:00:00 2001 From: mgretzke Date: Thu, 10 Jul 2025 11:43:50 +0200 Subject: [PATCH 07/63] Cleaned up HybridAllocators and tests --- snapshots/HybridAllocatorTest.json | 10 ++-- src/allocators/HybridAllocator.sol | 7 +-- src/allocators/HybridERC7683.sol | 14 ++--- test/HybridAllocator.t.sol | 95 ++++++++++++++++++++++++++---- 4 files changed, 95 insertions(+), 31 deletions(-) diff --git a/snapshots/HybridAllocatorTest.json b/snapshots/HybridAllocatorTest.json index cd3ac4d..d77adc9 100644 --- a/snapshots/HybridAllocatorTest.json +++ b/snapshots/HybridAllocatorTest.json @@ -1,7 +1,7 @@ { - "registerClaim_erc20Token": "152710", - "registerClaim_erc20Token_emptyAmountInput": "154351", - "registerClaim_multipleTokens": "189856", - "registerClaim_nativeToken": "103613", - "registerClaim_nativeToken_emptyAmountInput": "103512" + "registerClaim_erc20Token": "152722", + "registerClaim_erc20Token_emptyAmountInput": "154363", + "registerClaim_multipleTokens": "189880", + "registerClaim_nativeToken": "103625", + "registerClaim_nativeToken_emptyAmountInput": "103524" } diff --git a/src/allocators/HybridAllocator.sol b/src/allocators/HybridAllocator.sol index b251ac3..eaa7af1 100644 --- a/src/allocators/HybridAllocator.sol +++ b/src/allocators/HybridAllocator.sol @@ -152,7 +152,8 @@ contract HybridAllocator is IAllocator { return (_splitAllocatorId(id), _splitToken(id)); } - function _splitAllocatorId(uint256 id) internal pure returns (uint96 allocatorId_) { + function _splitAllocatorId(uint256 id) internal pure returns (uint96) { + uint96 allocatorId_; assembly ("memory-safe") { allocatorId_ := shr(164, shl(4, id)) } @@ -163,10 +164,6 @@ contract HybridAllocator is IAllocator { return address(uint160(id)); } - function _createLock(uint256 id, uint256 amount) internal pure returns (Lock memory) { - return Lock({lockTag: bytes12(bytes32(id)), token: _splitToken(id), amount: amount}); - } - function _checkAllocatorData(bytes32 digest, bytes memory allocatorData_) private view returns (bool) { bytes32 r; bytes32 s; diff --git a/src/allocators/HybridERC7683.sol b/src/allocators/HybridERC7683.sol index 858a817..c9f2327 100644 --- a/src/allocators/HybridERC7683.sol +++ b/src/allocators/HybridERC7683.sol @@ -132,20 +132,14 @@ contract HybridERC7683 is HybridAllocator, IOriginSettler { return _convertToResolvedCrossChainOrder(orderData, order.fillDeadline, batchCompact); } - /** - * OnChainCrossChainOrder - * - 1 word for fillDeadline - * - 1 word for orderDataType - * - 1 word for orderData offset - */ function _decodeOrderData(OnchainCrossChainOrder calldata order_) internal pure returns (OrderData calldata orderData) { - // bytes calldata rawOrderData = LibBytes.dynamicStructInCalldata(order_.orderData, 0x40); + bytes calldata rawOrderData = LibBytes.dynamicStructInCalldata(order_.orderData, 0x00); assembly ("memory-safe") { - orderData := order_ + orderData := rawOrderData.offset } } @@ -212,4 +206,8 @@ contract HybridERC7683 is HybridAllocator, IOriginSettler { function _convertAddressToBytes32(address address_) internal pure returns (bytes32) { return bytes32(uint256(uint160(address_))); } + + function _createLock(uint256 id, uint256 amount) internal pure returns (Lock memory) { + return Lock({lockTag: bytes12(bytes32(id)), token: _splitToken(id), amount: amount}); + } } diff --git a/test/HybridAllocator.t.sol b/test/HybridAllocator.t.sol index 7059cbf..52666ca 100644 --- a/test/HybridAllocator.t.sol +++ b/test/HybridAllocator.t.sol @@ -171,7 +171,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, type(uint128).max + 1); + assertEq(nonce, uint256(type(uint128).max) + 1); } function test_registerClaim_success_erc20Token() public { @@ -193,7 +193,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, type(uint128).max + 1); + assertEq(nonce, uint256(type(uint128).max) + 1); } function test_registerClaim_success_nativeTokenWithEmptyAmountInput() public { @@ -211,7 +211,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, type(uint128).max + 1); + assertEq(nonce, uint256(type(uint128).max) + 1); } function test_registerClaim_success_erc20TokenWithEmptyAmountInput() public { @@ -234,7 +234,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, type(uint128).max + 1); + assertEq(nonce, uint256(type(uint128).max) + 1); } function test_registerClaim_success_multipleTokens() public { @@ -264,7 +264,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, type(uint128).max + 1); + assertEq(nonce, uint256(type(uint128).max) + 1); } function test_registerClaim_checkNonceIncrements() public { @@ -272,10 +272,14 @@ contract HybridAllocatorTest is Test, TestHelper { idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); idsAndAmounts[0][1] = 0; + assertEq(allocator.nonce(), type(uint128).max); + // Register first claim allocator.registerClaim{value: 5e17}( user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, '' ); + assertEq(allocator.nonce(), uint256(type(uint128).max) + 1); + // Register second claim (bytes32 claimHash, uint256[] memory registeredAmounts,) = allocator.registerClaim{value: 5e17}( user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, '' @@ -285,9 +289,8 @@ contract HybridAllocatorTest is Test, TestHelper { assertTrue(allocator.isClaimAuthorized(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); assertEq(registeredAmounts[0], 5e17); assertEq(registeredAmounts.length, 1); - assertEq(address(compact).balance, defaultAmount); - assertEq(compact.balanceOf(address(user), idsAndAmounts[0][0]), defaultAmount); - assertEq(allocator.nonce(), type(uint128).max + 1); + + assertEq(allocator.nonce(), uint256(type(uint128).max) + 2); } function test_registerClaim_checkClaimHashNoWitness() public { @@ -448,6 +451,76 @@ 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 { + uint256[2][] memory idsAndAmounts = new uint256[2][](2); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); + idsAndAmounts[0][1] = defaultAmount; + + idsAndAmounts[1][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); + idsAndAmounts[1][1] = defaultAmount; + + // Approve tokens + vm.prank(user); + usdc.approve(address(compact), defaultAmount); + + bytes32 witness = keccak256(abi.encode(WITNESS_TYPEHASH, 1)); + + bytes32 claimHash = _toBatchCompactHashWithWitness( + BATCH_COMPACT_TYPEHASH_WITH_WITNESS, + BatchCompact({ + arbiter: arbiter, + sponsor: user, + nonce: nonce, + expires: defaultExpiration, + commitments: _idsAndAmountsToCommitments(idsAndAmounts) + }), + witness + ); + + bytes32[2][] memory claimHashesAndTypehashes = new bytes32[2][](1); + claimHashesAndTypehashes[0][0] = claimHash; + claimHashesAndTypehashes[0][1] = BATCH_COMPACT_TYPEHASH_WITH_WITNESS; + + // Deposit and register + vm.prank(user); + compact.batchDepositAndRegisterMultiple{value: defaultAmount}(idsAndAmounts, claimHashesAndTypehashes); + + // Off chain signing the claim + bytes32 digest = _toDigest(claimHash, compact.DOMAIN_SEPARATOR()); + (address attacker, uint256 attackerPK) = makeAddrAndKey('attacker'); + (bytes32 r, bytes32 vs) = vm.signCompact(attackerPK, digest); + bytes memory allocatorData = abi.encodePacked(r, vs); + + BatchClaimComponent[] memory claims = new BatchClaimComponent[](2); + { + Component[] memory portions = new Component[](1); + portions[0] = Component({ + claimant: uint256(bytes32(abi.encodePacked(bytes12(0), attacker))), // indicating a withdrawal + amount: defaultAmount + }); + + claims[0] = + BatchClaimComponent({id: idsAndAmounts[0][0], allocatedAmount: defaultAmount, portions: portions}); + claims[1] = + BatchClaimComponent({id: idsAndAmounts[1][0], allocatedAmount: defaultAmount, portions: portions}); + } + + BatchClaim memory claim = BatchClaim({ + allocatorData: allocatorData, + sponsorSignature: '', + sponsor: user, + nonce: nonce, + expires: defaultExpiration, + witness: witness, + witnessTypestring: WITNESS_STRING, + claims: claims + }); + + vm.prank(arbiter); + vm.expectRevert(abi.encodeWithSelector(HybridAllocator.InvalidSignature.selector)); + compact.batchClaim(claim); + } + function test_authorizeClaim_success_onChain() public { uint256[2][] memory idsAndAmounts = new uint256[2][](2); idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); @@ -469,10 +542,6 @@ contract HybridAllocatorTest is Test, TestHelper { user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH_WITH_WITNESS, witness ); - bytes32 digest = _toDigest(claimHash, compact.DOMAIN_SEPARATOR()); - (bytes32 r, bytes32 vs) = vm.signCompact(userPrivateKey, digest); - bytes memory sponsorSignature = abi.encodePacked(r, vs); - address target = makeAddr('target'); bytes32 returnedClaimHash; @@ -491,7 +560,7 @@ contract HybridAllocatorTest is Test, TestHelper { BatchClaim memory claim = BatchClaim({ allocatorData: '', - sponsorSignature: sponsorSignature, + sponsorSignature: '', sponsor: user, nonce: nonce, expires: defaultExpiration, From a570f2718ea06cb8430bfaaf89467f3a98ccb670 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Thu, 10 Jul 2025 11:44:20 +0200 Subject: [PATCH 08/63] disabled IR for test contracts --- foundry.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/foundry.toml b/foundry.toml index f6597d1..9ca9585 100644 --- a/foundry.toml +++ b/foundry.toml @@ -21,6 +21,14 @@ remappings = [ "@solady=lib/solady/src", ] +additional_compiler_profiles = [ + { name = "test", via_ir = false } +] + +compilation_restrictions = [ + { paths = "test/**", via_ir = false } +] + [profile.ci] inherit = "default" optimizer_runs = 200 # Override optimizer runs to reduce the compact contract sizes From bb6206ae34df4042dd56384ccf8ccf3efa40a143 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Fri, 11 Jul 2025 17:01:49 +0200 Subject: [PATCH 09/63] optimizations and fixes --- foundry.toml | 4 +- snapshots/HybridAllocatorTest.json | 10 +-- src/allocators/HybridAllocator.sol | 89 ++++++++++++------------ src/allocators/HybridERC7683.sol | 108 ++++++++++++++++++++++++++--- test/HybridAllocator.t.sol | 16 +++++ 5 files changed, 165 insertions(+), 62 deletions(-) diff --git a/foundry.toml b/foundry.toml index 9ca9585..645b201 100644 --- a/foundry.toml +++ b/foundry.toml @@ -22,11 +22,11 @@ remappings = [ ] additional_compiler_profiles = [ - { name = "test", via_ir = false } + { name = "test", via_ir = false, optimizer = false } ] compilation_restrictions = [ - { paths = "test/**", via_ir = false } + { paths = "test/**", via_ir = false, optimizer = false } ] [profile.ci] diff --git a/snapshots/HybridAllocatorTest.json b/snapshots/HybridAllocatorTest.json index d77adc9..54b8c99 100644 --- a/snapshots/HybridAllocatorTest.json +++ b/snapshots/HybridAllocatorTest.json @@ -1,7 +1,7 @@ { - "registerClaim_erc20Token": "152722", - "registerClaim_erc20Token_emptyAmountInput": "154363", - "registerClaim_multipleTokens": "189880", - "registerClaim_nativeToken": "103625", - "registerClaim_nativeToken_emptyAmountInput": "103524" + "registerClaim_erc20Token": "152725", + "registerClaim_erc20Token_emptyAmountInput": "154389", + "registerClaim_multipleTokens": "189906", + "registerClaim_nativeToken": "103581", + "registerClaim_nativeToken_emptyAmountInput": "103480" } diff --git a/src/allocators/HybridAllocator.sol b/src/allocators/HybridAllocator.sol index eaa7af1..2c1b3db 100644 --- a/src/allocators/HybridAllocator.sol +++ b/src/allocators/HybridAllocator.sol @@ -11,10 +11,10 @@ import {IERC7683Allocator} from 'src/interfaces/IERC7683Allocator.sol'; contract HybridAllocator is IAllocator { uint96 public immutable ALLOCATOR_ID; - ITheCompact private immutable _COMPACT; - bytes32 private immutable _COMPACT_DOMAIN_SEPARATOR; + ITheCompact internal immutable _COMPACT; + bytes32 internal immutable _COMPACT_DOMAIN_SEPARATOR; - mapping(bytes32 => bool) private claims; + mapping(bytes32 => bool) internal claims; uint256 public nonce; uint256 public signerCount; @@ -107,10 +107,11 @@ contract HybridAllocator is IAllocator { uint256, /*expires*/ uint256[2][] calldata, /*idsAndAmounts*/ bytes calldata allocatorData_ - ) external returns (bytes4) { + ) external virtual returns (bytes4) { if (msg.sender != address(_COMPACT)) { revert InvalidCaller(msg.sender, address(_COMPACT)); } + // The compact will check the validity of the nonce and expiration // Check if the claim was allocated on chain if (claims[claimHash]) { @@ -122,7 +123,7 @@ contract HybridAllocator is IAllocator { // Check the allocator data for a valid signature by an authorized signer bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), _COMPACT_DOMAIN_SEPARATOR, claimHash)); - if (!_checkAllocatorData(digest, allocatorData_)) { + if (!_checkSignature(digest, allocatorData_)) { revert InvalidSignature(); } @@ -138,51 +139,14 @@ contract HybridAllocator is IAllocator { uint256, /*expires*/ // The time at which the claim expires. uint256[2][] calldata, /*idsAndAmounts*/ // The allocated token IDs and amounts. bytes calldata allocatorData // Arbitrary data provided by the arbiter. - ) external view returns (bool) { + ) external view virtual returns (bool) { if (claims[claimHash]) { return true; } // Check the allocator data for a valid signature by an authorized allocator address bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), _COMPACT_DOMAIN_SEPARATOR, claimHash)); - return _checkAllocatorData(digest, allocatorData); - } - - function _splitId(uint256 id) internal pure returns (uint96 allocatorId_, address token_) { - return (_splitAllocatorId(id), _splitToken(id)); - } - - function _splitAllocatorId(uint256 id) internal pure returns (uint96) { - uint96 allocatorId_; - assembly ("memory-safe") { - allocatorId_ := shr(164, shl(4, id)) - } - return allocatorId_; - } - - function _splitToken(uint256 id) internal pure returns (address) { - return address(uint160(id)); - } - - function _checkAllocatorData(bytes32 digest, bytes memory allocatorData_) private view returns (bool) { - bytes32 r; - bytes32 s; - uint8 v; - - if (allocatorData_.length == 65) { - (r, s) = abi.decode(allocatorData_, (bytes32, bytes32)); - v = uint8(allocatorData_[64]); - } else if (allocatorData_.length == 64) { - bytes32 vs; - (r, vs) = abi.decode(allocatorData_, (bytes32, bytes32)); - v = uint8(uint256(vs >> 255) + 27); - s = vs << 1 >> 1; - } else { - return false; - } - - // Check if the signer is an authorized allocator address - return signers[ecrecover(digest, v, r, s)]; + return _checkSignature(digest, allocatorData); } function _actualIdsAndAmounts(uint256[2][] memory idsAndAmounts) internal returns (uint256[2][] memory) { @@ -224,4 +188,41 @@ contract HybridAllocator is IAllocator { return idsAndAmounts; } + + function _checkSignature(bytes32 digest, bytes calldata signature) internal view returns (bool) { + bytes32 r; + bytes32 s; + uint8 v; + + if (signature.length == 65) { + (r, s) = abi.decode(signature, (bytes32, bytes32)); + v = uint8(signature[64]); + } else if (signature.length == 64) { + bytes32 vs; + (r, vs) = abi.decode(signature, (bytes32, bytes32)); + v = uint8(uint256(vs >> 255) + 27); + s = vs << 1 >> 1; + } else { + return false; + } + + // Check if the signer is an authorized allocator address + return signers[ecrecover(digest, v, r, s)]; + } + + function _splitId(uint256 id) internal pure returns (uint96 allocatorId_, address token_) { + return (_splitAllocatorId(id), _splitToken(id)); + } + + function _splitAllocatorId(uint256 id) internal pure returns (uint96) { + uint96 allocatorId_; + assembly ("memory-safe") { + allocatorId_ := shr(164, shl(4, id)) + } + return allocatorId_; + } + + function _splitToken(uint256 id) internal pure returns (address) { + return address(uint160(id)); + } } diff --git a/src/allocators/HybridERC7683.sol b/src/allocators/HybridERC7683.sol index c9f2327..9872628 100644 --- a/src/allocators/HybridERC7683.sol +++ b/src/allocators/HybridERC7683.sol @@ -2,9 +2,9 @@ pragma solidity ^0.8.27; -import {LibBytes} from 'solady/utils/LibBytes.sol'; - import {BatchClaim, Mandate} from './types/TribunalStructs.sol'; +import {LibBytes} from '@solady/utils/LibBytes.sol'; +import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; import {BatchCompact, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; import {HybridAllocator} from 'src/allocators/HybridAllocator.sol'; @@ -13,9 +13,18 @@ import {BATCH_COMPACT_WITNESS_TYPEHASH, MANDATE_TYPEHASH} from 'src/allocators/l import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; contract HybridERC7683 is HybridAllocator, IOriginSettler { + // the storage slot for the claims mapping + uint256 private constant _CLAIMS_STORAGE_SLOT = 0; + + // mask for an active claim + uint256 private constant _ACTIVE_CLAIM_MASK = 0x0000000000000000000000000000000000000000000000000000000000000001; + // keccak256("OrderData(address arbiter,address sponsor,uint256 expires,uint256[2][] idsAndAmounts, - // uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt,uint256 targetBlock,uint256 maximumBlocksAfterTarget)") - bytes32 constant ORDERDATA_TYPEHASH = 0xf93147c220566c5d99eedaa4ddd899c0a796b3721e418131b49cc9eedde5054d; + // uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt,uint128 targetBlock,uint120 maximumBlocksAfterTarget)") + bytes32 public constant ORDERDATA_TYPEHASH = 0x293fe9f0f9b73ba619b34355bb68bfd5c0f97350dd85623f69698b8a8ecb6e59; + + /// @notice keccak256("QualifiedClaim(bytes32 claimHash,uint256 targetBlock,uint256 maximumBlocksAfterTarget)") + bytes32 public constant QUALIFICATION_TYPEHASH = 0x59866b84bd1f6c909cf2a31efd20c59e6c902e50f2c196994e5aa85cdc7d7ce0; struct OrderData { // BATCH COMPACT @@ -36,8 +45,8 @@ contract HybridERC7683 is HybridAllocator, IOriginSettler { uint256[] decayCurve; // Block durations, fill increases, & claim decreases. bytes32 salt; // Replay protection parameter // ADDITIONAL INPUT - uint256 targetBlock; // The block number at the target chain on which the PGA is executed / the reverse dutch auction starts. - uint256 maximumBlocksAfterTarget; // Blocks after target block that are still fillable. + uint128 targetBlock; // The block number at the target chain on which the PGA is executed / the reverse dutch auction starts. + uint120 maximumBlocksAfterTarget; // Blocks after target block that are still fillable. } error InvalidOrderDataType(bytes32 orderDataType, bytes32 expectedOrderDataType); @@ -79,7 +88,7 @@ contract HybridERC7683 is HybridAllocator, IOriginSettler { ); // register claim - (, uint256[] memory registeredAmounts, uint256 nonce_) = registerClaim( + (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce_) = registerClaim( orderData.sponsor, orderData.idsAndAmounts, orderData.arbiter, @@ -87,6 +96,19 @@ contract HybridERC7683 is HybridAllocator, IOriginSettler { BATCH_COMPACT_WITNESS_TYPEHASH, witnessHash ); + + // store the allocator data with the claims mapping. + assembly ("memory-safe") { + let m := mload(0x40) + mstore(m, _CLAIMS_STORAGE_SLOT) + mstore(add(m, 0x20), claimHash) + let claimSlot := keccak256(m, 0x40) + let targetBlock := calldataload(and(orderData, 0x1a0)) + let maximumBlocksAfterTarget := calldataload(and(orderData, 0x1c0)) + let indicator := and(and(shl(128, targetBlock), shl(1, maximumBlocksAfterTarget)), _ACTIVE_CLAIM_MASK) + sstore(claimSlot, indicator) + } + Lock[] memory locks = new Lock[](registeredAmounts.length); for (uint256 i = 0; i < registeredAmounts.length; i++) { locks[i] = _createLock(orderData.idsAndAmounts[i][0], registeredAmounts[i]); @@ -106,6 +128,44 @@ contract HybridERC7683 is HybridAllocator, IOriginSettler { ); } + function authorizeClaim( + bytes32 claimHash, + address, /*arbiter*/ + address, /*sponsor*/ + uint256, /*nonce*/ + uint256, /*expires*/ + uint256[2][] calldata, /*idsAndAmounts*/ + bytes calldata allocatorData_ + ) external override returns (bytes4) { + if (msg.sender != address(_COMPACT)) { + revert InvalidCaller(msg.sender, address(_COMPACT)); + } + // The compact will check the validity of the nonce and expiration + + (bool validClaim, uint128 targetBlock, uint120 maximumBlocksAfterTarget) = + _checkClaim(claimHash, allocatorData_); + // Check if the claim was allocated on chain + if (validClaim) { + delete claims[claimHash]; + + // Authorize the claim + return IAllocator.authorizeClaim.selector; + } + + // Create the digest for the qualified claim hash + bytes32 qualifiedClaimHash = + keccak256(abi.encode(QUALIFICATION_TYPEHASH, claimHash, targetBlock, maximumBlocksAfterTarget)); + bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), _COMPACT_DOMAIN_SEPARATOR, qualifiedClaimHash)); + // Check the allocator data for a valid signature by an authorized signer + bytes calldata allocatorSignature = LibBytes.bytesInCalldata(allocatorData_, 0x40); + if (!_checkSignature(digest, allocatorSignature)) { + revert InvalidSignature(); + } + + // Authorize the claim + return IAllocator.authorizeClaim.selector; + } + function resolveFor(GaslessCrossChainOrder calldata, /*order*/ bytes calldata /*originFillerData*/ ) external pure @@ -132,8 +192,34 @@ contract HybridERC7683 is HybridAllocator, IOriginSettler { return _convertToResolvedCrossChainOrder(orderData, order.fillDeadline, batchCompact); } + function _checkClaim(bytes32 claimHash, bytes calldata allocatorData) + private + view + returns (bool valid, uint128 targetBlock, uint120 maximumBlocksAfterTarget) + { + assembly ("memory-safe") { + let m := mload(0x40) + mstore(m, _CLAIMS_STORAGE_SLOT) + mstore(add(m, 0x20), claimHash) + let claimSlot := keccak256(m, 0x40) + let data := sload(claimSlot) + + valid := and(data, _ACTIVE_CLAIM_MASK) + let storedTargetBlock := shr(128, data) + let storedMaximumBlocksAfterTarget := shr(129, shl(128, data)) + + targetBlock := calldataload(allocatorData.offset) + maximumBlocksAfterTarget := calldataload(add(allocatorData.offset, 0x20)) + valid := + and( + valid, + and(eq(storedTargetBlock, targetBlock), eq(storedMaximumBlocksAfterTarget, maximumBlocksAfterTarget)) + ) + } + } + function _decodeOrderData(OnchainCrossChainOrder calldata order_) - internal + private pure returns (OrderData calldata orderData) { @@ -147,7 +233,7 @@ contract HybridERC7683 is HybridAllocator, IOriginSettler { OrderData calldata orderData, uint256 fillDeadline, BatchCompact memory batchCompact - ) internal view returns (ResolvedCrossChainOrder memory) { + ) private view returns (ResolvedCrossChainOrder memory) { Output[] memory maxSpent = new Output[](1); maxSpent[0] = Output({ token: bytes32(uint256(uint160(orderData.settlementToken))), @@ -203,11 +289,11 @@ contract HybridERC7683 is HybridAllocator, IOriginSettler { }); } - function _convertAddressToBytes32(address address_) internal pure returns (bytes32) { + function _convertAddressToBytes32(address address_) private pure returns (bytes32) { return bytes32(uint256(uint160(address_))); } - function _createLock(uint256 id, uint256 amount) internal pure returns (Lock memory) { + function _createLock(uint256 id, uint256 amount) private pure returns (Lock memory) { return Lock({lockTag: bytes12(bytes32(id)), token: _splitToken(id), amount: amount}); } } diff --git a/test/HybridAllocator.t.sol b/test/HybridAllocator.t.sol index 52666ca..59b3be1 100644 --- a/test/HybridAllocator.t.sol +++ b/test/HybridAllocator.t.sol @@ -324,6 +324,22 @@ contract HybridAllocatorTest is Test, TestHelper { assertTrue(allocator.isClaimAuthorized(createdHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); } + function test_registerClaim_slot() public { + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); + idsAndAmounts[0][1] = 0; + + bytes32 witness = keccak256(abi.encode(WITNESS_TYPEHASH, 1)); + + (bytes32 claimHash,,) = allocator.registerClaim{value: defaultAmount}( + user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH_WITH_WITNESS, witness + ); + + bytes32 claimSlot = keccak256(abi.encode(claimHash, 0x00)); + bytes32 claimSlotData = vm.load(address(allocator), claimSlot); + assertEq(claimSlotData, bytes32(uint256(1))); + } + function test_isClaimAuthorized_unauthorized() public { uint256[2][] memory idsAndAmounts = new uint256[2][](1); idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); From ee65c4df37670dae067a290de1d6de9efe6ed3ac Mon Sep 17 00:00:00 2001 From: mgretzke Date: Mon, 14 Jul 2025 14:37:45 +0200 Subject: [PATCH 10/63] added HybridAllocator Interface --- src/allocators/HybridAllocator.sol | 24 +++++------- src/interfaces/IHybridAllocator.sol | 58 +++++++++++++++++++++++++++++ test/HybridAllocator.t.sol | 27 ++++++++------ 3 files changed, 83 insertions(+), 26 deletions(-) create mode 100644 src/interfaces/IHybridAllocator.sol diff --git a/src/allocators/HybridAllocator.sol b/src/allocators/HybridAllocator.sol index 2c1b3db..c06ec4e 100644 --- a/src/allocators/HybridAllocator.sol +++ b/src/allocators/HybridAllocator.sol @@ -6,10 +6,11 @@ import {BatchCompact, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; + import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; -import {IERC7683Allocator} from 'src/interfaces/IERC7683Allocator.sol'; +import {IHybridAllocator} from 'src/interfaces/IHybridAllocator.sol'; -contract HybridAllocator is IAllocator { +contract HybridAllocator is IHybridAllocator { uint96 public immutable ALLOCATOR_ID; ITheCompact internal immutable _COMPACT; bytes32 internal immutable _COMPACT_DOMAIN_SEPARATOR; @@ -20,18 +21,6 @@ contract HybridAllocator is IAllocator { uint256 public signerCount; mapping(address => bool) public signers; - error Unsupported(); - error InvalidIds(); - error InvalidAllocatorId(uint96 allocatorId, uint96 expectedAllocatorId); - error InvalidCaller(address sender, address expectedSender); - error InvalidAllocatorData(uint256 length); - error InvalidSignature(); - error InvalidSigner(); - error LastSigner(); - error InvalidValue(uint256 value, uint256 expectedValue); - - event ClaimRegistered(address indexed sponsor, uint256[] registeredAmounts, uint256 nonce, bytes32 claimHash); - modifier onlySigner() { if (!signers[msg.sender]) { revert InvalidSigner(); @@ -51,11 +40,13 @@ contract HybridAllocator is IAllocator { nonce = type(uint128).max; } + /// @inheritdoc IHybridAllocator function addSigner(address signer_) external onlySigner { signers[signer_] = true; signerCount++; } + /// @inheritdoc IHybridAllocator function removeSigner(address signer_) external onlySigner { if (signerCount == 1) { revert LastSigner(); @@ -64,11 +55,13 @@ contract HybridAllocator is IAllocator { signerCount--; } + /// @inheritdoc IHybridAllocator function replaceSigner(address newSigner_) external onlySigner { signers[msg.sender] = false; signers[newSigner_] = true; } + /// @inheritdoc IAllocator function attest(address, /*operator*/ address, /*from*/ address, /*to*/ uint256, /*id*/ uint256 /*amount*/ ) external pure @@ -77,6 +70,7 @@ contract HybridAllocator is IAllocator { revert Unsupported(); } + /// @inheritdoc IHybridAllocator function registerClaim( address recipient, uint256[2][] memory idsAndAmounts, @@ -99,6 +93,7 @@ contract HybridAllocator is IAllocator { return (claimHash, registeredAmounts, nonce); } + /// @inheritdoc IAllocator function authorizeClaim( bytes32 claimHash, address, /*arbiter*/ @@ -131,6 +126,7 @@ contract HybridAllocator is IAllocator { return IAllocator.authorizeClaim.selector; } + /// @inheritdoc IAllocator function isClaimAuthorized( bytes32 claimHash, // The message hash representing the claim. address, /*arbiter*/ // The account tasked with verifying and submitting the claim. diff --git a/src/interfaces/IHybridAllocator.sol b/src/interfaces/IHybridAllocator.sol new file mode 100644 index 0000000..cbaba70 --- /dev/null +++ b/src/interfaces/IHybridAllocator.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; + +interface IHybridAllocator is IAllocator { + error Unsupported(); + error InvalidIds(); + error InvalidAllocatorId(uint96 allocatorId, uint96 expectedAllocatorId); + error InvalidCaller(address sender, address expectedSender); + error InvalidAllocatorData(uint256 length); + error InvalidSignature(); + error InvalidSigner(); + error LastSigner(); + error InvalidValue(uint256 value, uint256 expectedValue); + + event ClaimRegistered(address indexed sponsor, uint256[] registeredAmounts, uint256 nonce, bytes32 claimHash); + + /** + * @notice Add an offchain signer to the allocator. + * @param signer_ The address of the signer to add. + */ + function addSigner(address signer_) external; + + /** + * @notice Remove an offchain signer from the allocator. + * @dev The last signer cannot be removed. + * @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. + * @param newSigner_ The address of the new signer. + */ + function replaceSigner(address newSigner_) external; + + /** + * @notice Register a claim in the allocator and depositing the relevant tokens to the compact. + * @dev If the provided amounts are zero, the contract will use its own token balance. + * @param recipient The address receiving the deposited tokens and the sponsor of the claim. + * @param idsAndAmounts The IDs and amounts of the tokens to register. Amounts can be zero. + * @param arbiter The address of the arbiter for the claim. + * @param expires The expiration time of the claim. + * @param typehash The typehash of the claim. + * @param witness The witness of the claim. + * @return The claim hash, the registered amounts, and the nonce. + */ + function registerClaim( + address recipient, + uint256[2][] memory idsAndAmounts, + address arbiter, + uint256 expires, + bytes32 typehash, + bytes32 witness + ) external payable returns (bytes32, uint256[] memory, uint256); +} diff --git a/test/HybridAllocator.t.sol b/test/HybridAllocator.t.sol index 59b3be1..baf3031 100644 --- a/test/HybridAllocator.t.sol +++ b/test/HybridAllocator.t.sol @@ -15,6 +15,7 @@ import {Scope} from '@uniswap/the-compact/types/Scope.sol'; import {Test} from 'forge-std/Test.sol'; import {HybridAllocator} from 'src/allocators/HybridAllocator.sol'; import {BATCH_COMPACT_WITNESS_TYPEHASH} from 'src/allocators/lib/TypeHashes.sol'; +import {IHybridAllocator} from 'src/interfaces/IHybridAllocator.sol'; import {ERC20Mock} from 'src/test/ERC20Mock.sol'; contract HybridAllocatorTest is Test, TestHelper { @@ -69,7 +70,7 @@ contract HybridAllocatorTest is Test, TestHelper { } function test_registerClaim_revert_InvalidIds() public { - vm.expectRevert(HybridAllocator.InvalidIds.selector); + vm.expectRevert(IHybridAllocator.InvalidIds.selector); allocator.registerClaim(user, new uint256[2][](0), arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); } @@ -80,7 +81,7 @@ contract HybridAllocatorTest is Test, TestHelper { idsAndAmounts[0][1] = defaultAmount; vm.expectRevert( abi.encodeWithSelector( - HybridAllocator.InvalidAllocatorId.selector, _toAllocatorId(address(this)), allocator.ALLOCATOR_ID() + IHybridAllocator.InvalidAllocatorId.selector, _toAllocatorId(address(this)), allocator.ALLOCATOR_ID() ) ); allocator.registerClaim{value: defaultAmount}( @@ -95,7 +96,7 @@ contract HybridAllocatorTest is Test, TestHelper { idsAndAmounts[0][1] = defaultAmount; vm.expectRevert( abi.encodeWithSelector( - HybridAllocator.InvalidAllocatorId.selector, _toAllocatorId(address(this)), allocator.ALLOCATOR_ID() + IHybridAllocator.InvalidAllocatorId.selector, _toAllocatorId(address(this)), allocator.ALLOCATOR_ID() ) ); allocator.registerClaim(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); @@ -106,7 +107,9 @@ contract HybridAllocatorTest is Test, TestHelper { idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0) /* use native */ ); idsAndAmounts[0][1] = defaultAmount; - vm.expectRevert(abi.encodeWithSelector(HybridAllocator.InvalidValue.selector, defaultAmount + 1, defaultAmount)); + vm.expectRevert( + abi.encodeWithSelector(IHybridAllocator.InvalidValue.selector, defaultAmount + 1, defaultAmount) + ); allocator.registerClaim{value: defaultAmount + 1}( user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, '' ); @@ -425,7 +428,7 @@ contract HybridAllocatorTest is Test, TestHelper { assertEq(usdc.balanceOf(user), defaultAmount); - vm.expectRevert(abi.encodeWithSelector(HybridAllocator.Unsupported.selector)); + vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.Unsupported.selector)); allocator.attest(signer, user, target, id, defaultAmount); } @@ -438,7 +441,7 @@ contract HybridAllocatorTest is Test, TestHelper { usdc.approve(address(compact), defaultAmount); compact.depositERC20(address(usdc), bytes12(bytes32(id)), defaultAmount, user); - vm.expectRevert(abi.encodeWithSelector(HybridAllocator.Unsupported.selector), address(allocator)); + vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.Unsupported.selector), address(allocator)); compact.transfer(target, id, defaultAmount); vm.stopPrank(); } @@ -463,7 +466,7 @@ contract HybridAllocatorTest is Test, TestHelper { ); vm.prank(attacker); - vm.expectRevert(abi.encodeWithSelector(HybridAllocator.InvalidCaller.selector, attacker, address(compact))); + vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.InvalidCaller.selector, attacker, address(compact))); allocator.authorizeClaim(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), ''); } @@ -533,7 +536,7 @@ contract HybridAllocatorTest is Test, TestHelper { }); vm.prank(arbiter); - vm.expectRevert(abi.encodeWithSelector(HybridAllocator.InvalidSignature.selector)); + vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.InvalidSignature.selector)); compact.batchClaim(claim); } @@ -737,7 +740,7 @@ contract HybridAllocatorTest is Test, TestHelper { function test_addSigner_revert_InvalidSigner(address attacker) public { vm.assume(attacker != signer); vm.prank(attacker); - vm.expectRevert(abi.encodeWithSelector(HybridAllocator.InvalidSigner.selector)); + vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.InvalidSigner.selector)); allocator.addSigner(attacker); assertEq(allocator.signerCount(), 1); assertFalse(allocator.signers(attacker)); @@ -755,7 +758,7 @@ contract HybridAllocatorTest is Test, TestHelper { function test_removeSigner_revert_InvalidSigner(address attacker) public { vm.assume(attacker != signer); vm.prank(attacker); - vm.expectRevert(abi.encodeWithSelector(HybridAllocator.InvalidSigner.selector)); + vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.InvalidSigner.selector)); allocator.removeSigner(signer); assertEq(allocator.signerCount(), 1); assertTrue(allocator.signers(signer)); @@ -763,7 +766,7 @@ contract HybridAllocatorTest is Test, TestHelper { function test_removeSigner_revert_LastSigner() public { vm.prank(signer); - vm.expectRevert(abi.encodeWithSelector(HybridAllocator.LastSigner.selector)); + vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.LastSigner.selector)); allocator.removeSigner(signer); assertEq(allocator.signerCount(), 1); assertTrue(allocator.signers(signer)); @@ -796,7 +799,7 @@ contract HybridAllocatorTest is Test, TestHelper { function test_replaceSigner_revert_InvalidSigner(address attacker) public { vm.assume(attacker != signer); vm.prank(attacker); - vm.expectRevert(abi.encodeWithSelector(HybridAllocator.InvalidSigner.selector)); + vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.InvalidSigner.selector)); allocator.replaceSigner(attacker); assertEq(allocator.signerCount(), 1); assertFalse(allocator.signers(attacker)); From 6b093568072bad3633329324fd4285d49ead12a0 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Mon, 14 Jul 2025 14:47:01 +0200 Subject: [PATCH 11/63] added HybridERC7683 Interface --- src/allocators/HybridERC7683.sol | 36 +++++++------------------------ src/interfaces/IHybridERC7683.sol | 33 ++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 28 deletions(-) create mode 100644 src/interfaces/IHybridERC7683.sol diff --git a/src/allocators/HybridERC7683.sol b/src/allocators/HybridERC7683.sol index 9872628..c40a3d9 100644 --- a/src/allocators/HybridERC7683.sol +++ b/src/allocators/HybridERC7683.sol @@ -9,10 +9,11 @@ import {BatchCompact, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; import {HybridAllocator} from 'src/allocators/HybridAllocator.sol'; import {BATCH_COMPACT_WITNESS_TYPEHASH, MANDATE_TYPEHASH} from 'src/allocators/lib/TypeHashes.sol'; +import {IHybridERC7683} from 'src/interfaces/IHybridERC7683.sol'; import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; -contract HybridERC7683 is HybridAllocator, IOriginSettler { +contract HybridERC7683 is HybridAllocator, IHybridERC7683 { // the storage slot for the claims mapping uint256 private constant _CLAIMS_STORAGE_SLOT = 0; @@ -26,34 +27,9 @@ contract HybridERC7683 is HybridAllocator, IOriginSettler { /// @notice keccak256("QualifiedClaim(bytes32 claimHash,uint256 targetBlock,uint256 maximumBlocksAfterTarget)") bytes32 public constant QUALIFICATION_TYPEHASH = 0x59866b84bd1f6c909cf2a31efd20c59e6c902e50f2c196994e5aa85cdc7d7ce0; - struct OrderData { - // BATCH COMPACT - address arbiter; // The account tasked with verifying and submitting the claim. - address sponsor; // The account to source the tokens from. - // uint256 nonce; // A parameter to enforce replay protection, scoped to allocator. - uint256 expires; // The time at which the claim expires. - uint256[2][] idsAndAmounts; // The ids of the ERC6909 tokens to allocate. - // MANDATE - uint256 chainId; // (implicit arg, included in EIP712 payload) - address tribunal; // (implicit arg, included in EIP712 payload) - address recipient; // Recipient of settled tokens - // uint256 expires; // Mandate expiration timestamp - address settlementToken; // Settlement token (address(0) for native) - uint256 minimumAmount; // Minimum settlement amount - uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in - uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline) - uint256[] decayCurve; // Block durations, fill increases, & claim decreases. - bytes32 salt; // Replay protection parameter - // ADDITIONAL INPUT - uint128 targetBlock; // The block number at the target chain on which the PGA is executed / the reverse dutch auction starts. - uint120 maximumBlocksAfterTarget; // Blocks after target block that are still fillable. - } - - error InvalidOrderDataType(bytes32 orderDataType, bytes32 expectedOrderDataType); - error InvalidOriginSettler(address originSettler, address expectedOriginSettler); - constructor(address compact_, address signer_) HybridAllocator(compact_, signer_) {} + /// @inheritdoc IOriginSettler function openFor( GaslessCrossChainOrder calldata, /*order_*/ bytes calldata, /*sponsorSignature_*/ @@ -62,6 +38,7 @@ contract HybridERC7683 is HybridAllocator, IOriginSettler { revert Unsupported(); } + /// @inheritdoc IOriginSettler function open(OnchainCrossChainOrder calldata order_) external { // Check if orderDataType is the one expected by the allocator if (order_.orderDataType != ORDERDATA_TYPEHASH) { @@ -128,6 +105,7 @@ contract HybridERC7683 is HybridAllocator, IOriginSettler { ); } + /// @inheritdoc IAllocator function authorizeClaim( bytes32 claimHash, address, /*arbiter*/ @@ -136,7 +114,7 @@ contract HybridERC7683 is HybridAllocator, IOriginSettler { uint256, /*expires*/ uint256[2][] calldata, /*idsAndAmounts*/ bytes calldata allocatorData_ - ) external override returns (bytes4) { + ) external override(HybridAllocator, IAllocator) returns (bytes4) { if (msg.sender != address(_COMPACT)) { revert InvalidCaller(msg.sender, address(_COMPACT)); } @@ -166,6 +144,7 @@ contract HybridERC7683 is HybridAllocator, IOriginSettler { return IAllocator.authorizeClaim.selector; } + /// @inheritdoc IOriginSettler function resolveFor(GaslessCrossChainOrder calldata, /*order*/ bytes calldata /*originFillerData*/ ) external pure @@ -174,6 +153,7 @@ contract HybridERC7683 is HybridAllocator, IOriginSettler { revert Unsupported(); } + /// @inheritdoc IOriginSettler function resolve(OnchainCrossChainOrder calldata order) external view returns (ResolvedCrossChainOrder memory) { OrderData calldata orderData = _decodeOrderData(order); uint256 idsLength = orderData.idsAndAmounts.length; diff --git a/src/interfaces/IHybridERC7683.sol b/src/interfaces/IHybridERC7683.sol new file mode 100644 index 0000000..8524d42 --- /dev/null +++ b/src/interfaces/IHybridERC7683.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; +import {IHybridAllocator} from 'src/interfaces/IHybridAllocator.sol'; + +interface IHybridERC7683 is IHybridAllocator, IOriginSettler { + struct OrderData { + // BATCH COMPACT + address arbiter; // The account tasked with verifying and submitting the claim. + address sponsor; // The account to source the tokens from. + // uint256 nonce; // A parameter to enforce replay protection, scoped to allocator. + uint256 expires; // The time at which the claim expires. + uint256[2][] idsAndAmounts; // The ids of the ERC6909 tokens to allocate. + // MANDATE + uint256 chainId; // (implicit arg, included in EIP712 payload) + address tribunal; // (implicit arg, included in EIP712 payload) + address recipient; // Recipient of settled tokens + // uint256 expires; // Mandate expiration timestamp + address settlementToken; // Settlement token (address(0) for native) + uint256 minimumAmount; // Minimum settlement amount + uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in + uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline) + uint256[] decayCurve; // Block durations, fill increases, & claim decreases. + bytes32 salt; // Replay protection parameter + // ADDITIONAL INPUT + uint128 targetBlock; // The block number at the target chain on which the PGA is executed / the reverse dutch auction starts. + uint120 maximumBlocksAfterTarget; // Blocks after target block that are still fillable. + } + + error InvalidOrderDataType(bytes32 orderDataType, bytes32 expectedOrderDataType); + error InvalidOriginSettler(address originSettler, address expectedOriginSettler); +} From a0263bb4e76a2dd0ca7fda8e72cfe95152dad740 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Tue, 15 Jul 2025 15:05:10 +0200 Subject: [PATCH 12/63] fixed theCompact sizes --- foundry.toml | 8 -------- snapshots/HybridAllocatorTest.json | 10 +++++----- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/foundry.toml b/foundry.toml index 645b201..f6597d1 100644 --- a/foundry.toml +++ b/foundry.toml @@ -21,14 +21,6 @@ remappings = [ "@solady=lib/solady/src", ] -additional_compiler_profiles = [ - { name = "test", via_ir = false, optimizer = false } -] - -compilation_restrictions = [ - { paths = "test/**", via_ir = false, optimizer = false } -] - [profile.ci] inherit = "default" optimizer_runs = 200 # Override optimizer runs to reduce the compact contract sizes diff --git a/snapshots/HybridAllocatorTest.json b/snapshots/HybridAllocatorTest.json index 54b8c99..af921ed 100644 --- a/snapshots/HybridAllocatorTest.json +++ b/snapshots/HybridAllocatorTest.json @@ -1,7 +1,7 @@ { - "registerClaim_erc20Token": "152725", - "registerClaim_erc20Token_emptyAmountInput": "154389", - "registerClaim_multipleTokens": "189906", - "registerClaim_nativeToken": "103581", - "registerClaim_nativeToken_emptyAmountInput": "103480" + "registerClaim_erc20Token": "142004", + "registerClaim_erc20Token_emptyAmountInput": "143062", + "registerClaim_multipleTokens": "176269", + "registerClaim_nativeToken": "96743", + "registerClaim_nativeToken_emptyAmountInput": "96651" } From fab5c1174eeee65870948e36d0e228b9124a3247 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Thu, 17 Jul 2025 17:26:40 +0200 Subject: [PATCH 13/63] HybridERC7683 claims slot fix --- src/allocators/HybridERC7683.sol | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/allocators/HybridERC7683.sol b/src/allocators/HybridERC7683.sol index c40a3d9..dc66f4c 100644 --- a/src/allocators/HybridERC7683.sol +++ b/src/allocators/HybridERC7683.sol @@ -14,9 +14,6 @@ import {IHybridERC7683} from 'src/interfaces/IHybridERC7683.sol'; import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; contract HybridERC7683 is HybridAllocator, IHybridERC7683 { - // the storage slot for the claims mapping - uint256 private constant _CLAIMS_STORAGE_SLOT = 0; - // mask for an active claim uint256 private constant _ACTIVE_CLAIM_MASK = 0x0000000000000000000000000000000000000000000000000000000000000001; @@ -76,10 +73,9 @@ contract HybridERC7683 is HybridAllocator, IHybridERC7683 { // store the allocator data with the claims mapping. assembly ("memory-safe") { - let m := mload(0x40) - mstore(m, _CLAIMS_STORAGE_SLOT) - mstore(add(m, 0x20), claimHash) - let claimSlot := keccak256(m, 0x40) + mstore(0x00, claimHash) + mstore(0x20, claims.slot) + let claimSlot := keccak256(0x00, 0x40) let targetBlock := calldataload(and(orderData, 0x1a0)) let maximumBlocksAfterTarget := calldataload(and(orderData, 0x1c0)) let indicator := and(and(shl(128, targetBlock), shl(1, maximumBlocksAfterTarget)), _ACTIVE_CLAIM_MASK) @@ -178,10 +174,9 @@ contract HybridERC7683 is HybridAllocator, IHybridERC7683 { returns (bool valid, uint128 targetBlock, uint120 maximumBlocksAfterTarget) { assembly ("memory-safe") { - let m := mload(0x40) - mstore(m, _CLAIMS_STORAGE_SLOT) - mstore(add(m, 0x20), claimHash) - let claimSlot := keccak256(m, 0x40) + mstore(0x00, claimHash) + mstore(0x20, claims.slot) + let claimSlot := keccak256(0x00, 0x40) let data := sload(claimSlot) valid := and(data, _ACTIVE_CLAIM_MASK) From bbee4d62335b7e1ca22347b5886faf2976ff5025 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Fri, 18 Jul 2025 13:10:55 +0200 Subject: [PATCH 14/63] interface clean up --- src/allocators/HybridAllocator.sol | 1 - src/allocators/HybridERC7683.sol | 22 +++++++++++----------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/allocators/HybridAllocator.sol b/src/allocators/HybridAllocator.sol index c06ec4e..c5603e3 100644 --- a/src/allocators/HybridAllocator.sol +++ b/src/allocators/HybridAllocator.sol @@ -6,7 +6,6 @@ import {BatchCompact, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; - import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; import {IHybridAllocator} from 'src/interfaces/IHybridAllocator.sol'; diff --git a/src/allocators/HybridERC7683.sol b/src/allocators/HybridERC7683.sol index dc66f4c..4f4a8fe 100644 --- a/src/allocators/HybridERC7683.sol +++ b/src/allocators/HybridERC7683.sol @@ -193,17 +193,6 @@ contract HybridERC7683 is HybridAllocator, IHybridERC7683 { } } - function _decodeOrderData(OnchainCrossChainOrder calldata order_) - private - pure - returns (OrderData calldata orderData) - { - bytes calldata rawOrderData = LibBytes.dynamicStructInCalldata(order_.orderData, 0x00); - assembly ("memory-safe") { - orderData := rawOrderData.offset - } - } - function _convertToResolvedCrossChainOrder( OrderData calldata orderData, uint256 fillDeadline, @@ -264,6 +253,17 @@ contract HybridERC7683 is HybridAllocator, IHybridERC7683 { }); } + function _decodeOrderData(OnchainCrossChainOrder calldata order_) + private + pure + returns (OrderData calldata orderData) + { + bytes calldata rawOrderData = LibBytes.dynamicStructInCalldata(order_.orderData, 0x00); + assembly ("memory-safe") { + orderData := rawOrderData.offset + } + } + function _convertAddressToBytes32(address address_) private pure returns (bytes32) { return bytes32(uint256(uint160(address_))); } From c34683b17676286e446b9559d07524ba6515085e Mon Sep 17 00:00:00 2001 From: mgretzke Date: Mon, 28 Jul 2025 15:55:33 +0200 Subject: [PATCH 15/63] implemented feedback --- snapshots/HybridAllocatorTest.json | 10 +-- src/allocators/HybridAllocator.sol | 21 +++--- src/allocators/HybridERC7683.sol | 2 +- src/interfaces/IHybridAllocator.sol | 14 ++-- test/HybridAllocator.t.sol | 108 ++++++++++++++-------------- 5 files changed, 78 insertions(+), 77 deletions(-) diff --git a/snapshots/HybridAllocatorTest.json b/snapshots/HybridAllocatorTest.json index af921ed..393df42 100644 --- a/snapshots/HybridAllocatorTest.json +++ b/snapshots/HybridAllocatorTest.json @@ -1,7 +1,7 @@ { - "registerClaim_erc20Token": "142004", - "registerClaim_erc20Token_emptyAmountInput": "143062", - "registerClaim_multipleTokens": "176269", - "registerClaim_nativeToken": "96743", - "registerClaim_nativeToken_emptyAmountInput": "96651" + "allocateAndRegister_erc20Token": "172609", + "allocateAndRegister_erc20Token_emptyAmountInput": "174338", + "allocateAndRegister_multipleTokens": "209855", + "allocateAndRegister_nativeToken": "122340", + "allocateAndRegister_nativeToken_emptyAmountInput": "122239" } diff --git a/src/allocators/HybridAllocator.sol b/src/allocators/HybridAllocator.sol index c5603e3..aad2973 100644 --- a/src/allocators/HybridAllocator.sol +++ b/src/allocators/HybridAllocator.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.27; +import {SafeTransferLib} from '@solady/utils/SafeTransferLib.sol'; import {BatchCompact, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; @@ -16,7 +17,8 @@ contract HybridAllocator is IHybridAllocator { mapping(bytes32 => bool) internal claims; - uint256 public nonce; + /// @dev The off chain allocator must use a uint256 nonce where the first 160 bits are the sponsors to ensure no nonce collisions + uint96 public nonce; uint256 public signerCount; mapping(address => bool) public signers; @@ -34,9 +36,6 @@ contract HybridAllocator is IHybridAllocator { signers[signer_] = true; signerCount++; - - // Block the first half of nonces for the offchain allocator - nonce = type(uint128).max; } /// @inheritdoc IHybridAllocator @@ -70,7 +69,7 @@ contract HybridAllocator is IHybridAllocator { } /// @inheritdoc IHybridAllocator - function registerClaim( + function allocateAndRegister( address recipient, uint256[2][] memory idsAndAmounts, address arbiter, @@ -166,19 +165,21 @@ contract HybridAllocator is IHybridAllocator { } for (; idIndex < idsLength; idIndex++) { - (uint96 allocatorId_, address token_) = _splitId(idsAndAmounts[idIndex][0]); + (uint96 allocatorId, address token) = _splitId(idsAndAmounts[idIndex][0]); // Check allocator id - if (allocatorId_ != ALLOCATOR_ID) { - revert InvalidAllocatorId(allocatorId_, ALLOCATOR_ID); + if (allocatorId != ALLOCATOR_ID) { + revert InvalidAllocatorId(allocatorId, ALLOCATOR_ID); } if (idsAndAmounts[idIndex][1] == 0) { // Amount is derived from the allocators token balance - idsAndAmounts[idIndex][1] = IERC20(token_).balanceOf(address(this)); + idsAndAmounts[idIndex][1] = IERC20(token).balanceOf(address(this)); } - IERC20(token_).approve(address(_COMPACT), idsAndAmounts[idIndex][1]); + if (IERC20(token).allowance(address(this), address(_COMPACT)) < idsAndAmounts[idIndex][1]) { + SafeTransferLib.safeApproveWithRetry(token, address(_COMPACT), type(uint256).max); + } } return idsAndAmounts; diff --git a/src/allocators/HybridERC7683.sol b/src/allocators/HybridERC7683.sol index 4f4a8fe..d3f71bc 100644 --- a/src/allocators/HybridERC7683.sol +++ b/src/allocators/HybridERC7683.sol @@ -62,7 +62,7 @@ contract HybridERC7683 is HybridAllocator, IHybridERC7683 { ); // register claim - (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce_) = registerClaim( + (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce_) = allocateAndRegister( orderData.sponsor, orderData.idsAndAmounts, orderData.arbiter, diff --git a/src/interfaces/IHybridAllocator.sol b/src/interfaces/IHybridAllocator.sol index cbaba70..36205cc 100644 --- a/src/interfaces/IHybridAllocator.sol +++ b/src/interfaces/IHybridAllocator.sol @@ -37,17 +37,17 @@ interface IHybridAllocator is IAllocator { function replaceSigner(address newSigner_) external; /** - * @notice Register a claim in the allocator and depositing the relevant tokens to the compact. + * @notice Create an allocation and a registration on the compact by depositing the relevant tokens to the compact. * @dev If the provided amounts are zero, the contract will use its own token balance. - * @param recipient The address receiving the deposited tokens and the sponsor of the claim. + * @param recipient The address receiving the deposited tokens and the sponsor of the compact. * @param idsAndAmounts The IDs and amounts of the tokens to register. Amounts can be zero. - * @param arbiter The address of the arbiter for the claim. - * @param expires The expiration time of the claim. - * @param typehash The typehash of the claim. - * @param witness The witness of the claim. + * @param arbiter The address of the arbiter for the compact. + * @param expires The expiration time of the compact. + * @param typehash The typehash of the compact. + * @param witness The witness of the compact. * @return The claim hash, the registered amounts, and the nonce. */ - function registerClaim( + function allocateAndRegister( address recipient, uint256[2][] memory idsAndAmounts, address arbiter, diff --git a/test/HybridAllocator.t.sol b/test/HybridAllocator.t.sol index baf3031..b11b8cb 100644 --- a/test/HybridAllocator.t.sol +++ b/test/HybridAllocator.t.sol @@ -46,7 +46,7 @@ contract HybridAllocatorTest is Test, TestHelper { batchCompact.arbiter = arbiter; batchCompact.sponsor = user; - batchCompact.nonce = uint256(type(uint128).max) + 1; + batchCompact.nonce = 1; batchCompact.expires = defaultExpiration; } @@ -55,7 +55,7 @@ contract HybridAllocatorTest is Test, TestHelper { } function test_checkNonce() public view { - assertEq(allocator.nonce(), type(uint128).max); + assertEq(allocator.nonce(), 0); } function test_checkSignerCount() public view { @@ -69,12 +69,12 @@ contract HybridAllocatorTest is Test, TestHelper { assertFalse(allocator.signers(attacker)); } - function test_registerClaim_revert_InvalidIds() public { + function test_allocateAndRegister_revert_InvalidIds() public { vm.expectRevert(IHybridAllocator.InvalidIds.selector); - allocator.registerClaim(user, new uint256[2][](0), arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); + allocator.allocateAndRegister(user, new uint256[2][](0), arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); } - function test_registerClaim_revert_InvalidAllocatorIdNative() public { + function test_allocateAndRegister_revert_InvalidAllocatorIdNative() public { uint256[2][] memory idsAndAmounts = new uint256[2][](1); idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(this), /* wrong address */ address(0)); @@ -84,12 +84,12 @@ contract HybridAllocatorTest is Test, TestHelper { IHybridAllocator.InvalidAllocatorId.selector, _toAllocatorId(address(this)), allocator.ALLOCATOR_ID() ) ); - allocator.registerClaim{value: defaultAmount}( + allocator.allocateAndRegister{value: defaultAmount}( user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, '' ); } - function test_registerClaim_revert_InvalidAllocatorIdERC20() public { + function test_allocateAndRegister_revert_InvalidAllocatorIdERC20() public { uint256[2][] memory idsAndAmounts = new uint256[2][](1); idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(this), /* wrong address */ address(usdc)); @@ -99,10 +99,10 @@ contract HybridAllocatorTest is Test, TestHelper { IHybridAllocator.InvalidAllocatorId.selector, _toAllocatorId(address(this)), allocator.ALLOCATOR_ID() ) ); - allocator.registerClaim(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); + allocator.allocateAndRegister(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); } - function test_registerClaim_revert_InvalidValue() public { + function test_allocateAndRegister_revert_InvalidValue() public { uint256[2][] memory idsAndAmounts = new uint256[2][](1); idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0) /* use native */ ); @@ -110,36 +110,36 @@ contract HybridAllocatorTest is Test, TestHelper { vm.expectRevert( abi.encodeWithSelector(IHybridAllocator.InvalidValue.selector, defaultAmount + 1, defaultAmount) ); - allocator.registerClaim{value: defaultAmount + 1}( + allocator.allocateAndRegister{value: defaultAmount + 1}( user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, '' ); } - function test_registerClaim_revert_zeroNativeTokensAmount() public { + function test_allocateAndRegister_revert_zeroNativeTokensAmount() public { uint256[2][] memory idsAndAmounts = new uint256[2][](1); idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); idsAndAmounts[0][1] = 0; vm.expectRevert(abi.encodeWithSelector(ITheCompact.InvalidBatchDepositStructure.selector)); - allocator.registerClaim(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); + allocator.allocateAndRegister(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); } - function test_registerClaim_revert_zeroTokensAmount() public { + function test_allocateAndRegister_revert_zeroTokensAmount() public { uint256[2][] memory idsAndAmounts = new uint256[2][](1); idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); idsAndAmounts[0][1] = 0; vm.expectRevert(abi.encodeWithSelector(ITheCompact.InvalidDepositBalanceChange.selector)); - allocator.registerClaim(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); + allocator.allocateAndRegister(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); } - function test_registerClaim_revert_tokensNotProvided() public { + function test_allocateAndRegister_revert_tokensNotProvided() public { uint256[2][] memory idsAndAmounts = new uint256[2][](1); idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); idsAndAmounts[0][1] = defaultAmount; vm.expectRevert(abi.encodeWithSignature('TransferFromFailed()')); - allocator.registerClaim(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); + allocator.allocateAndRegister(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); } - function test_registerClaim_revert_invalidTokenOrder() public { + function test_allocateAndRegister_revert_invalidTokenOrder() public { uint256[2][] memory idsAndAmounts = new uint256[2][](2); idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); idsAndAmounts[0][1] = 0; @@ -153,20 +153,20 @@ contract HybridAllocatorTest is Test, TestHelper { assertEq(usdc.balanceOf(address(allocator)), defaultAmount); vm.expectRevert(); // Will revert when trying to approve tokens of address(0) - allocator.registerClaim{value: defaultAmount}( + allocator.allocateAndRegister{value: defaultAmount}( user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, '' ); } - function test_registerClaim_success_nativeToken() public { + function test_allocateAndRegister_success_nativeToken() public { uint256[2][] memory idsAndAmounts = new uint256[2][](1); idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0) /* use native */ ); idsAndAmounts[0][1] = defaultAmount; - (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = allocator.registerClaim{ + (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = allocator.allocateAndRegister{ value: defaultAmount }(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); - vm.snapshotGasLastCall('registerClaim_nativeToken'); + vm.snapshotGasLastCall('allocateAndRegister_nativeToken'); assertTrue(compact.isRegistered(user, claimHash, BATCH_COMPACT_TYPEHASH)); assertTrue(allocator.isClaimAuthorized(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); @@ -174,10 +174,10 @@ 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, uint256(type(uint128).max) + 1); + assertEq(nonce, 1); } - function test_registerClaim_success_erc20Token() public { + function test_allocateAndRegister_success_erc20Token() public { uint256[2][] memory idsAndAmounts = new uint256[2][](1); idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); idsAndAmounts[0][1] = defaultAmount; @@ -188,36 +188,36 @@ contract HybridAllocatorTest is Test, TestHelper { assertEq(usdc.balanceOf(address(allocator)), defaultAmount); (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = - allocator.registerClaim(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); - vm.snapshotGasLastCall('registerClaim_erc20Token'); + allocator.allocateAndRegister(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); + vm.snapshotGasLastCall('allocateAndRegister_erc20Token'); assertTrue(compact.isRegistered(user, claimHash, BATCH_COMPACT_TYPEHASH)); assertTrue(allocator.isClaimAuthorized(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); assertEq(registeredAmounts[0], defaultAmount); assertEq(usdc.balanceOf(address(compact)), defaultAmount); assertEq(compact.balanceOf(address(user), idsAndAmounts[0][0]), defaultAmount); - assertEq(nonce, uint256(type(uint128).max) + 1); + assertEq(nonce, 1); } - function test_registerClaim_success_nativeTokenWithEmptyAmountInput() public { + function test_allocateAndRegister_success_nativeTokenWithEmptyAmountInput() public { uint256[2][] memory idsAndAmounts = new uint256[2][](1); idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0) /* use native */ ); idsAndAmounts[0][1] = 0; - (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = allocator.registerClaim{ + (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = allocator.allocateAndRegister{ value: defaultAmount }(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); - vm.snapshotGasLastCall('registerClaim_nativeToken_emptyAmountInput'); + vm.snapshotGasLastCall('allocateAndRegister_nativeToken_emptyAmountInput'); assertTrue(compact.isRegistered(user, claimHash, BATCH_COMPACT_TYPEHASH)); assertTrue(allocator.isClaimAuthorized(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); assertEq(registeredAmounts[0], defaultAmount); assertEq(address(compact).balance, defaultAmount); assertEq(compact.balanceOf(address(user), idsAndAmounts[0][0]), defaultAmount); - assertEq(nonce, uint256(type(uint128).max) + 1); + assertEq(nonce, 1); } - function test_registerClaim_success_erc20TokenWithEmptyAmountInput() public { + function test_allocateAndRegister_success_erc20TokenWithEmptyAmountInput() public { uint256[2][] memory idsAndAmounts = new uint256[2][](1); idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); idsAndAmounts[0][1] = 0; @@ -228,8 +228,8 @@ contract HybridAllocatorTest is Test, TestHelper { assertEq(usdc.balanceOf(address(allocator)), defaultAmount); (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = - allocator.registerClaim(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); - vm.snapshotGasLastCall('registerClaim_erc20Token_emptyAmountInput'); + allocator.allocateAndRegister(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); + vm.snapshotGasLastCall('allocateAndRegister_erc20Token_emptyAmountInput'); assertTrue(compact.isRegistered(user, claimHash, BATCH_COMPACT_TYPEHASH)); assertTrue(allocator.isClaimAuthorized(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); @@ -237,10 +237,10 @@ 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, uint256(type(uint128).max) + 1); + assertEq(nonce, 1); } - function test_registerClaim_success_multipleTokens() public { + function test_allocateAndRegister_success_multipleTokens() public { uint256[2][] memory idsAndAmounts = new uint256[2][](2); idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); idsAndAmounts[0][1] = 0; @@ -253,10 +253,10 @@ contract HybridAllocatorTest is Test, TestHelper { usdc.transfer(address(allocator), defaultAmount); assertEq(usdc.balanceOf(address(allocator)), defaultAmount); - (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = allocator.registerClaim{ + (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = allocator.allocateAndRegister{ value: defaultAmount }(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); - vm.snapshotGasLastCall('registerClaim_multipleTokens'); + vm.snapshotGasLastCall('allocateAndRegister_multipleTokens'); assertTrue(compact.isRegistered(user, claimHash, BATCH_COMPACT_TYPEHASH)); assertTrue(allocator.isClaimAuthorized(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); @@ -267,24 +267,24 @@ 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, uint256(type(uint128).max) + 1); + assertEq(nonce, 1); } - function test_registerClaim_checkNonceIncrements() public { + function test_allocateAndRegister_checkNonceIncrements() public { uint256[2][] memory idsAndAmounts = new uint256[2][](1); idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); idsAndAmounts[0][1] = 0; - assertEq(allocator.nonce(), type(uint128).max); + assertEq(allocator.nonce(), 0); // Register first claim - allocator.registerClaim{value: 5e17}( + allocator.allocateAndRegister{value: 5e17}( user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, '' ); - assertEq(allocator.nonce(), uint256(type(uint128).max) + 1); + assertEq(allocator.nonce(), 1); // Register second claim - (bytes32 claimHash, uint256[] memory registeredAmounts,) = allocator.registerClaim{value: 5e17}( + (bytes32 claimHash, uint256[] memory registeredAmounts,) = allocator.allocateAndRegister{value: 5e17}( user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, '' ); @@ -293,15 +293,15 @@ contract HybridAllocatorTest is Test, TestHelper { assertEq(registeredAmounts[0], 5e17); assertEq(registeredAmounts.length, 1); - assertEq(allocator.nonce(), uint256(type(uint128).max) + 2); + assertEq(allocator.nonce(), 2); } - function test_registerClaim_checkClaimHashNoWitness() public { + function test_allocateAndRegister_checkClaimHashNoWitness() public { uint256[2][] memory idsAndAmounts = new uint256[2][](1); idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); idsAndAmounts[0][1] = 0; - (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = allocator.registerClaim{ + (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = allocator.allocateAndRegister{ value: defaultAmount }(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); BatchCompact memory batch = _updateBatchCompact(batchCompact, idsAndAmounts, registeredAmounts, nonce); @@ -311,14 +311,14 @@ contract HybridAllocatorTest is Test, TestHelper { assertTrue(allocator.isClaimAuthorized(createdHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); } - function test_registerClaim_checkClaimHashWitness() public { + function test_allocateAndRegister_checkClaimHashWitness() public { uint256[2][] memory idsAndAmounts = new uint256[2][](1); idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); idsAndAmounts[0][1] = 0; bytes32 witness = keccak256(abi.encode(WITNESS_TYPEHASH, 1)); - (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = allocator.registerClaim{ + (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = allocator.allocateAndRegister{ value: defaultAmount }(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH_WITH_WITNESS, witness); BatchCompact memory batch = _updateBatchCompact(batchCompact, idsAndAmounts, registeredAmounts, nonce); @@ -327,14 +327,14 @@ contract HybridAllocatorTest is Test, TestHelper { assertTrue(allocator.isClaimAuthorized(createdHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); } - function test_registerClaim_slot() public { + function test_allocateAndRegister_slot() public { uint256[2][] memory idsAndAmounts = new uint256[2][](1); idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); idsAndAmounts[0][1] = 0; bytes32 witness = keccak256(abi.encode(WITNESS_TYPEHASH, 1)); - (bytes32 claimHash,,) = allocator.registerClaim{value: defaultAmount}( + (bytes32 claimHash,,) = allocator.allocateAndRegister{value: defaultAmount}( user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH_WITH_WITNESS, witness ); @@ -348,7 +348,7 @@ contract HybridAllocatorTest is Test, TestHelper { idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); idsAndAmounts[0][1] = 0; - (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = allocator.registerClaim{ + (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = allocator.allocateAndRegister{ value: defaultAmount }(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); BatchCompact memory batch = _updateBatchCompact(batchCompact, idsAndAmounts, registeredAmounts, nonce); @@ -461,7 +461,7 @@ contract HybridAllocatorTest is Test, TestHelper { bytes32 witness = keccak256(abi.encode(WITNESS_TYPEHASH, 1)); vm.prank(user); - (bytes32 claimHash,,) = allocator.registerClaim{value: defaultAmount}( + (bytes32 claimHash,,) = allocator.allocateAndRegister{value: defaultAmount}( user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH_WITH_WITNESS, witness ); @@ -557,7 +557,7 @@ contract HybridAllocatorTest is Test, TestHelper { bytes32 witness = keccak256(abi.encode(WITNESS_TYPEHASH, 1)); vm.prank(user); - (bytes32 claimHash,, uint256 nonce) = allocator.registerClaim{value: defaultAmount}( + (bytes32 claimHash,, uint256 nonce) = allocator.allocateAndRegister{value: defaultAmount}( user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH_WITH_WITNESS, witness ); @@ -700,7 +700,7 @@ contract HybridAllocatorTest is Test, TestHelper { idsAndAmounts[0][1] = 0; vm.prank(user); - (bytes32 claimHash,, uint256 nonce) = allocator.registerClaim{value: defaultAmount}( + (bytes32 claimHash,, uint256 nonce) = allocator.allocateAndRegister{value: defaultAmount}( user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, '' ); From c4662458741df7a33e3ef299736fb6b34257384e Mon Sep 17 00:00:00 2001 From: mgretzke Date: Tue, 29 Jul 2025 12:08:53 +0200 Subject: [PATCH 16/63] second registration snapshot --- snapshots/HybridAllocatorTest.json | 12 +++++---- test/HybridAllocator.t.sol | 39 +++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/snapshots/HybridAllocatorTest.json b/snapshots/HybridAllocatorTest.json index 393df42..0e4ef65 100644 --- a/snapshots/HybridAllocatorTest.json +++ b/snapshots/HybridAllocatorTest.json @@ -1,7 +1,9 @@ { - "allocateAndRegister_erc20Token": "172609", - "allocateAndRegister_erc20Token_emptyAmountInput": "174338", - "allocateAndRegister_multipleTokens": "209855", - "allocateAndRegister_nativeToken": "122340", - "allocateAndRegister_nativeToken_emptyAmountInput": "122239" + "allocateAndRegister_erc20Token": "185641", + "allocateAndRegister_erc20Token_emptyAmountInput": "186550", + "allocateAndRegister_multipleTokens": "220157", + "allocateAndRegister_nativeToken": "137180", + "allocateAndRegister_nativeToken_emptyAmountInput": "137016", + "allocateAndRegister_second_erc20Token": "112846", + "allocateAndRegister_second_nativeToken": "102816" } diff --git a/test/HybridAllocator.t.sol b/test/HybridAllocator.t.sol index b11b8cb..48242f6 100644 --- a/test/HybridAllocator.t.sol +++ b/test/HybridAllocator.t.sol @@ -270,7 +270,7 @@ contract HybridAllocatorTest is Test, TestHelper { assertEq(nonce, 1); } - function test_allocateAndRegister_checkNonceIncrements() public { + function test_allocateAndRegister_checkNonceIncrements_nativeToken() public { uint256[2][] memory idsAndAmounts = new uint256[2][](1); idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); idsAndAmounts[0][1] = 0; @@ -287,6 +287,7 @@ contract HybridAllocatorTest is Test, TestHelper { (bytes32 claimHash, uint256[] memory registeredAmounts,) = allocator.allocateAndRegister{value: 5e17}( user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, '' ); + vm.snapshotGasLastCall('allocateAndRegister_second_nativeToken'); assertTrue(compact.isRegistered(user, claimHash, BATCH_COMPACT_TYPEHASH)); assertTrue(allocator.isClaimAuthorized(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); @@ -296,6 +297,42 @@ contract HybridAllocatorTest is Test, TestHelper { assertEq(allocator.nonce(), 2); } + function test_allocateAndRegister_checkNonceIncrements_erc20Token() public { + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); + idsAndAmounts[0][1] = 0; + + assertEq(allocator.nonce(), 0); + + // Provide tokens + vm.prank(user); + usdc.transfer(address(allocator), defaultAmount / 2); + assertEq(usdc.balanceOf(address(allocator)), defaultAmount / 2); + + // Register first claim + allocator.allocateAndRegister(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); + assertEq(allocator.nonce(), 1); + + // Provide tokens + vm.prank(user); + usdc.transfer(address(allocator), defaultAmount / 2); + assertEq(usdc.balanceOf(address(allocator)), defaultAmount / 2); + + // Register second claim + (bytes32 claimHash, uint256[] memory registeredAmounts,) = + allocator.allocateAndRegister(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); + vm.snapshotGasLastCall('allocateAndRegister_second_erc20Token'); + + assertTrue(compact.isRegistered(user, claimHash, BATCH_COMPACT_TYPEHASH)); + assertTrue(allocator.isClaimAuthorized(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); + assertEq(registeredAmounts[0], defaultAmount / 2); + assertEq(registeredAmounts.length, 1); + assertEq(usdc.balanceOf(address(compact)), defaultAmount); + assertEq(compact.balanceOf(address(user), idsAndAmounts[0][0]), defaultAmount); + + assertEq(allocator.nonce(), 2); + } + function test_allocateAndRegister_checkClaimHashNoWitness() public { uint256[2][] memory idsAndAmounts = new uint256[2][](1); idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); From dc40666ac3b11f6b3c38569d0210c2ddcbd0e4fd Mon Sep 17 00:00:00 2001 From: mgretzke Date: Tue, 29 Jul 2025 15:13:36 +0200 Subject: [PATCH 17/63] implemented PR feedback --- snapshots/ERC7683Allocator_open.json | 3 +++ snapshots/ERC7683Allocator_openFor.json | 4 ++++ src/allocators/HybridAllocator.sol | 6 ++++++ test/HybridAllocator.t.sol | 21 +++++++++++++++++++++ 4 files changed, 34 insertions(+) create mode 100644 snapshots/ERC7683Allocator_open.json create mode 100644 snapshots/ERC7683Allocator_openFor.json diff --git a/snapshots/ERC7683Allocator_open.json b/snapshots/ERC7683Allocator_open.json new file mode 100644 index 0000000..a9382f3 --- /dev/null +++ b/snapshots/ERC7683Allocator_open.json @@ -0,0 +1,3 @@ +{ + "open_simpleOrder": "129755" +} diff --git a/snapshots/ERC7683Allocator_openFor.json b/snapshots/ERC7683Allocator_openFor.json new file mode 100644 index 0000000..d470ed2 --- /dev/null +++ b/snapshots/ERC7683Allocator_openFor.json @@ -0,0 +1,4 @@ +{ + "openFor_simpleOrder_relayed": "132712", + "openFor_simpleOrder_userHimself": "132712" +} diff --git a/src/allocators/HybridAllocator.sol b/src/allocators/HybridAllocator.sol index aad2973..8ad6135 100644 --- a/src/allocators/HybridAllocator.sol +++ b/src/allocators/HybridAllocator.sol @@ -40,6 +40,9 @@ contract HybridAllocator is IHybridAllocator { /// @inheritdoc IHybridAllocator function addSigner(address signer_) external onlySigner { + if (signer_ == address(0)) { + revert InvalidSigner(); + } signers[signer_] = true; signerCount++; } @@ -55,6 +58,9 @@ contract HybridAllocator is IHybridAllocator { /// @inheritdoc IHybridAllocator function replaceSigner(address newSigner_) external onlySigner { + if (newSigner_ == address(0)) { + revert InvalidSigner(); + } signers[msg.sender] = false; signers[newSigner_] = true; } diff --git a/test/HybridAllocator.t.sol b/test/HybridAllocator.t.sol index 48242f6..ab1a074 100644 --- a/test/HybridAllocator.t.sol +++ b/test/HybridAllocator.t.sol @@ -775,6 +775,7 @@ contract HybridAllocatorTest is Test, TestHelper { } function test_addSigner_revert_InvalidSigner(address attacker) public { + vm.assume(attacker != address(0)); vm.assume(attacker != signer); vm.prank(attacker); vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.InvalidSigner.selector)); @@ -783,8 +784,17 @@ contract HybridAllocatorTest is Test, TestHelper { assertFalse(allocator.signers(attacker)); } + function test_addSigner_revert_signerIsZero() public { + vm.prank(signer); + 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); allocator.addSigner(newSigner); assertEq(allocator.signerCount(), 2); @@ -811,6 +821,7 @@ contract HybridAllocatorTest is Test, TestHelper { function test_removeSigner_success(address newSigner) public { vm.assume(newSigner != signer); + vm.assume(newSigner != address(0)); vm.prank(signer); allocator.addSigner(newSigner); assertEq(allocator.signerCount(), 2); @@ -823,6 +834,7 @@ contract HybridAllocatorTest is Test, TestHelper { function test_removeSigner_success_deleteSelf(address newSigner) public { vm.assume(newSigner != signer); + vm.assume(newSigner != address(0)); vm.prank(signer); allocator.addSigner(newSigner); assertEq(allocator.signerCount(), 2); @@ -842,8 +854,17 @@ contract HybridAllocatorTest is Test, TestHelper { assertFalse(allocator.signers(attacker)); } + function test_replaceSigner_revert_signerIsZero() public { + vm.prank(signer); + vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.InvalidSigner.selector)); + allocator.replaceSigner(address(0)); + assertEq(allocator.signerCount(), 1); + assertFalse(allocator.signers(address(0))); + } + function test_replaceSigner_success(address newSigner) public { vm.assume(newSigner != signer); + vm.assume(newSigner != address(0)); vm.prank(signer); allocator.replaceSigner(newSigner); assertEq(allocator.signerCount(), 1); From 511d1a326a1da24ca59bc69a097de6a39b474266 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Fri, 1 Aug 2025 16:23:03 +0200 Subject: [PATCH 18/63] added missing files --- snapshots/ERC7683Allocator_open.json | 3 - snapshots/ERC7683Allocator_openFor.json | 4 - src/interfaces/ERC7683/IOriginSettler.sol | 133 ++++++++++++++++++++++ src/test/ERC20Mock.sol | 12 ++ 4 files changed, 145 insertions(+), 7 deletions(-) delete mode 100644 snapshots/ERC7683Allocator_open.json delete mode 100644 snapshots/ERC7683Allocator_openFor.json create mode 100644 src/interfaces/ERC7683/IOriginSettler.sol create mode 100644 src/test/ERC20Mock.sol diff --git a/snapshots/ERC7683Allocator_open.json b/snapshots/ERC7683Allocator_open.json deleted file mode 100644 index a9382f3..0000000 --- a/snapshots/ERC7683Allocator_open.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "open_simpleOrder": "129755" -} diff --git a/snapshots/ERC7683Allocator_openFor.json b/snapshots/ERC7683Allocator_openFor.json deleted file mode 100644 index d470ed2..0000000 --- a/snapshots/ERC7683Allocator_openFor.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "openFor_simpleOrder_relayed": "132712", - "openFor_simpleOrder_userHimself": "132712" -} diff --git a/src/interfaces/ERC7683/IOriginSettler.sol b/src/interfaces/ERC7683/IOriginSettler.sol new file mode 100644 index 0000000..026ae91 --- /dev/null +++ b/src/interfaces/ERC7683/IOriginSettler.sol @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/// @title IOriginSettler +/// @notice Standard interface for settlement contracts on the origin chain +interface IOriginSettler { + /// @title GaslessCrossChainOrder CrossChainOrder type + /// @notice Standard order struct to be signed by users, disseminated to fillers, and submitted to origin settler contracts + struct GaslessCrossChainOrder { + /// @dev The contract address that the order is meant to be settled by. + /// Fillers send this order to this contract address on the origin chain + address originSettler; + /// @dev The address of the user who is initiating the swap, + /// whose input tokens will be taken and escrowed + address user; + /// @dev Nonce to be used as replay protection for the order + uint256 nonce; + /// @dev The chainId of the origin chain + uint256 originChainId; + /// @dev The timestamp by which the order must be opened + uint32 openDeadline; + /// @dev The timestamp by which the order must be filled on the destination chain + uint32 fillDeadline; + /// @dev Type identifier for the order data. This is an EIP-712 typehash. + bytes32 orderDataType; + /// @dev Arbitrary implementation-specific data + /// Can be used to define tokens, amounts, destination chains, fees, settlement parameters, + /// or any other order-type specific information + bytes orderData; + } + + /// @title OnchainCrossChainOrder CrossChainOrder type + /// @notice Standard order struct for user-opened orders, where the user is the msg.sender. + struct OnchainCrossChainOrder { + /// @dev The timestamp by which the order must be filled on the destination chain + uint32 fillDeadline; + /// @dev Type identifier for the order data. This is an EIP-712 typehash. + bytes32 orderDataType; + /// @dev Arbitrary implementation-specific data + /// Can be used to define tokens, amounts, destination chains, fees, settlement parameters, + /// or any other order-type specific information + bytes orderData; + } + /// @title ResolvedCrossChainOrder type + /// @notice An implementation-generic representation of an order intended for filler consumption + /// @dev Defines all requirements for filling an order by unbundling the implementation-specific orderData. + /// @dev Intended to improve integration generalization by allowing fillers to compute the exact input and output information of any order + + struct ResolvedCrossChainOrder { + /// @dev The address of the user who is initiating the transfer + address user; + /// @dev The chainId of the origin chain + uint256 originChainId; + /// @dev The timestamp by which the order must be opened + uint32 openDeadline; + /// @dev The timestamp by which the order must be filled on the destination chain(s) + uint32 fillDeadline; + /// @dev The unique identifier for this order within this settlement system + bytes32 orderId; + /// @dev The max outputs that the filler will send. It's possible the actual amount depends on the state of the destination + /// chain (destination dutch auction, for instance), so these outputs should be considered a cap on filler liabilities. + Output[] maxSpent; + /// @dev The minimum outputs that must be given to the filler as part of order settlement. Similar to maxSpent, it's possible + /// that special order types may not be able to guarantee the exact amount at open time, so this should be considered + /// a floor on filler receipts. + Output[] minReceived; + /// @dev Each instruction in this array is parameterizes a single leg of the fill. This provides the filler with the information + /// necessary to perform the fill on the destination(s). + FillInstruction[] fillInstructions; + } + + /// @notice Tokens that must be received for a valid order fulfillment + struct Output { + /// @dev The address of the ERC20 token on the destination chain + /// @dev address(0) used as a sentinel for the native token + bytes32 token; + /// @dev The amount of the token to be sent + uint256 amount; + /// @dev The address to receive the output tokens + bytes32 recipient; + /// @dev The destination chain for this output + uint256 chainId; + } + + /// @title FillInstruction type + /// @notice Instructions to parameterize each leg of the fill + /// @dev Provides all the origin-generated information required to produce a valid fill leg + struct FillInstruction { + /// @dev The contract address that the order is meant to be settled by + uint256 destinationChainId; + /// @dev The contract address that the order is meant to be filled on + bytes32 destinationSettler; + /// @dev The data generated on the origin chain needed by the destinationSettler to process the fill + bytes originData; + } + + /// @notice Signals that an order has been opened + /// @param orderId a unique order identifier within this settlement system + /// @param resolvedOrder resolved order that would be returned by resolve if called instead of Open + event Open(bytes32 indexed orderId, ResolvedCrossChainOrder resolvedOrder); + + /// @notice Opens a gasless cross-chain order on behalf of a user. + /// @dev To be called by the filler. + /// @dev This method must emit the Open event + /// @param order The GaslessCrossChainOrder definition + /// @param signature The user's signature over the order + /// @param originFillerData Any filler-defined data required by the settler + function openFor(GaslessCrossChainOrder calldata order, bytes calldata signature, bytes calldata originFillerData) + external; + + /// @notice Opens a cross-chain order + /// @dev To be called by the user + /// @dev This method must emit the Open event + /// @param order The OnchainCrossChainOrder definition + function open(OnchainCrossChainOrder calldata order) external; + + /// @notice Resolves a specific GaslessCrossChainOrder into a generic ResolvedCrossChainOrder + /// @dev Intended to improve standardized integration of various order types and settlement contracts + /// @param order The GaslessCrossChainOrder definition + /// @param originFillerData Any filler-defined data required by the settler + /// @return ResolvedCrossChainOrder hydrated order data including the inputs and outputs of the order + function resolveFor(GaslessCrossChainOrder calldata order, bytes calldata originFillerData) + external + view + returns (ResolvedCrossChainOrder memory); + + /// @notice Resolves a specific OnchainCrossChainOrder into a generic ResolvedCrossChainOrder + /// @dev Intended to improve standardized integration of various order types and settlement contracts + /// @param order The OnchainCrossChainOrder definition + /// @return ResolvedCrossChainOrder hydrated order data including the inputs and outputs of the order + function resolve(OnchainCrossChainOrder calldata order) external view returns (ResolvedCrossChainOrder memory); +} diff --git a/src/test/ERC20Mock.sol b/src/test/ERC20Mock.sol new file mode 100644 index 0000000..f8a0ce1 --- /dev/null +++ b/src/test/ERC20Mock.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {ERC20} from '@openzeppelin/contracts/token/ERC20/ERC20.sol'; + +contract ERC20Mock is ERC20 { + constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {} + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} From 7262180fd97cf7dab74dd25a9e35b07769604f0a Mon Sep 17 00:00:00 2001 From: mgretzke Date: Mon, 4 Aug 2025 17:15:51 +0200 Subject: [PATCH 19/63] openFor function added to HybridERC7683 + tests --- src/allocators/HybridERC7683.sol | 197 +++-- src/allocators/lib/TypeHashes.sol | 5 +- src/interfaces/IHybridERC7683.sol | 47 +- test/HybridERC7683.t.sol | 1119 +++++++++++++++++++++++++++++ 4 files changed, 1301 insertions(+), 67 deletions(-) create mode 100644 test/HybridERC7683.t.sol diff --git a/src/allocators/HybridERC7683.sol b/src/allocators/HybridERC7683.sol index d3f71bc..25abb39 100644 --- a/src/allocators/HybridERC7683.sol +++ b/src/allocators/HybridERC7683.sol @@ -17,32 +17,37 @@ contract HybridERC7683 is HybridAllocator, IHybridERC7683 { // mask for an active claim uint256 private constant _ACTIVE_CLAIM_MASK = 0x0000000000000000000000000000000000000000000000000000000000000001; - // keccak256("OrderData(address arbiter,address sponsor,uint256 expires,uint256[2][] idsAndAmounts, - // uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt,uint128 targetBlock,uint120 maximumBlocksAfterTarget)") - bytes32 public constant ORDERDATA_TYPEHASH = 0x293fe9f0f9b73ba619b34355bb68bfd5c0f97350dd85623f69698b8a8ecb6e59; + /// @notice The typehash of the OrderDataOnChain struct + // keccak256("OrderDataOnChain(Order order,uint256 expires) + // Order(address arbiter,uint256[2][] idsAndAmounts,uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt,bytes32 qualification)") + bytes32 public constant ORDERDATA_ONCHAIN_TYPEHASH = + 0xd13cc04099540f243b0042f68c0edbce9aefe428c22e0354a24061c5d98c7276; + + /// @notice The typehash of the OrderDataGasless struct + // keccak256("OrderDataGasless(Order order) + // Order(address arbiter,uint256[2][] idsAndAmounts,uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt,bytes32 qualification)") + bytes32 public constant ORDERDATA_GASLESS_TYPEHASH = + 0xfba49b9453e7d260d702826a659947a671a3e6a970688a795c82065685236b52; /// @notice keccak256("QualifiedClaim(bytes32 claimHash,uint256 targetBlock,uint256 maximumBlocksAfterTarget)") bytes32 public constant QUALIFICATION_TYPEHASH = 0x59866b84bd1f6c909cf2a31efd20c59e6c902e50f2c196994e5aa85cdc7d7ce0; + uint256 private constant _INVALID_QUALIFICATION_ERROR_SIGNATURE = 0x7ac3c7d4; + constructor(address compact_, address signer_) HybridAllocator(compact_, signer_) {} /// @inheritdoc IOriginSettler function openFor( - GaslessCrossChainOrder calldata, /*order_*/ + GaslessCrossChainOrder calldata order, bytes calldata, /*sponsorSignature_*/ bytes calldata /*originFillerData*/ - ) external pure { - revert Unsupported(); - } - - /// @inheritdoc IOriginSettler - function open(OnchainCrossChainOrder calldata order_) external { + ) external { // Check if orderDataType is the one expected by the allocator - if (order_.orderDataType != ORDERDATA_TYPEHASH) { - revert InvalidOrderDataType(order_.orderDataType, ORDERDATA_TYPEHASH); + if (order.orderDataType != ORDERDATA_GASLESS_TYPEHASH) { + revert InvalidOrderDataType(order.orderDataType, ORDERDATA_GASLESS_TYPEHASH); } - OrderData calldata orderData = _decodeOrderData(order_); + (Order calldata orderData,) = _decodeOrderData(order.orderData, false); // create witness hash bytes32 witnessHash = keccak256( @@ -51,7 +56,7 @@ contract HybridERC7683 is HybridAllocator, IHybridERC7683 { orderData.chainId, orderData.tribunal, orderData.recipient, - order_.fillDeadline, + order.fillDeadline, orderData.settlementToken, orderData.minimumAmount, orderData.baselinePriorityFee, @@ -63,25 +68,68 @@ contract HybridERC7683 is HybridAllocator, IHybridERC7683 { // register claim (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce_) = allocateAndRegister( - orderData.sponsor, + order.user, orderData.idsAndAmounts, orderData.arbiter, - orderData.expires, + order.openDeadline, BATCH_COMPACT_WITNESS_TYPEHASH, witnessHash ); - // store the allocator data with the claims mapping. - assembly ("memory-safe") { - mstore(0x00, claimHash) - mstore(0x20, claims.slot) - let claimSlot := keccak256(0x00, 0x40) - let targetBlock := calldataload(and(orderData, 0x1a0)) - let maximumBlocksAfterTarget := calldataload(and(orderData, 0x1c0)) - let indicator := and(and(shl(128, targetBlock), shl(1, maximumBlocksAfterTarget)), _ACTIVE_CLAIM_MASK) - sstore(claimSlot, indicator) + _storeQualification(claimHash, orderData.qualification); + + Lock[] memory locks = new Lock[](registeredAmounts.length); + for (uint256 i = 0; i < registeredAmounts.length; i++) { + locks[i] = _createLock(orderData.idsAndAmounts[i][0], registeredAmounts[i]); + } + + BatchCompact memory batchCompact = BatchCompact({ + arbiter: orderData.arbiter, + sponsor: order.user, + nonce: nonce_, + expires: order.openDeadline, + commitments: locks + }); + + // emit open event + emit Open( + bytes32(batchCompact.nonce), _convertToResolvedCrossChainOrder(orderData, order.fillDeadline, batchCompact) + ); + } + + /// @inheritdoc IOriginSettler + function open(OnchainCrossChainOrder calldata order) external { + // Check if orderDataType is the one expected by the allocator + if (order.orderDataType != ORDERDATA_ONCHAIN_TYPEHASH) { + revert InvalidOrderDataType(order.orderDataType, ORDERDATA_ONCHAIN_TYPEHASH); } + (Order calldata orderData, uint256 expires) = _decodeOrderData(order.orderData, true); + + // create witness hash + bytes32 witnessHash = keccak256( + abi.encode( + MANDATE_TYPEHASH, + orderData.chainId, + orderData.tribunal, + orderData.recipient, + order.fillDeadline, + orderData.settlementToken, + orderData.minimumAmount, + orderData.baselinePriorityFee, + orderData.scalingFactor, + keccak256(abi.encodePacked(orderData.decayCurve)), + orderData.salt + ) + ); + + // register claim + (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce_) = allocateAndRegister( + msg.sender, orderData.idsAndAmounts, orderData.arbiter, expires, BATCH_COMPACT_WITNESS_TYPEHASH, witnessHash + ); + + _storeQualification(claimHash, orderData.qualification); + Lock[] memory locks = new Lock[](registeredAmounts.length); for (uint256 i = 0; i < registeredAmounts.length; i++) { locks[i] = _createLock(orderData.idsAndAmounts[i][0], registeredAmounts[i]); @@ -89,15 +137,15 @@ contract HybridERC7683 is HybridAllocator, IHybridERC7683 { BatchCompact memory batchCompact = BatchCompact({ arbiter: orderData.arbiter, - sponsor: orderData.sponsor, + sponsor: msg.sender, nonce: nonce_, - expires: orderData.expires, + expires: expires, commitments: locks }); // emit open event emit Open( - bytes32(batchCompact.nonce), _convertToResolvedCrossChainOrder(orderData, order_.fillDeadline, batchCompact) + bytes32(batchCompact.nonce), _convertToResolvedCrossChainOrder(orderData, order.fillDeadline, batchCompact) ); } @@ -126,6 +174,8 @@ contract HybridERC7683 is HybridAllocator, IHybridERC7683 { return IAllocator.authorizeClaim.selector; } + if (allocatorData_.length != 0xe0 && allocatorData_.length != 0xc0) revert InvalidSignature(); + // Create the digest for the qualified claim hash bytes32 qualifiedClaimHash = keccak256(abi.encode(QUALIFICATION_TYPEHASH, claimHash, targetBlock, maximumBlocksAfterTarget)); @@ -141,17 +191,42 @@ contract HybridERC7683 is HybridAllocator, IHybridERC7683 { } /// @inheritdoc IOriginSettler - function resolveFor(GaslessCrossChainOrder calldata, /*order*/ bytes calldata /*originFillerData*/ ) + function resolveFor(GaslessCrossChainOrder calldata order, bytes calldata /*originFillerData*/ ) external - pure + view returns (ResolvedCrossChainOrder memory) { - revert Unsupported(); + // Check if orderDataType is the one expected by the allocator + if (order.orderDataType != ORDERDATA_GASLESS_TYPEHASH) { + revert InvalidOrderDataType(order.orderDataType, ORDERDATA_GASLESS_TYPEHASH); + } + + (Order calldata orderData,) = _decodeOrderData(order.orderData, false); + + Lock[] memory locks = new Lock[](orderData.idsAndAmounts.length); + for (uint256 i = 0; i < orderData.idsAndAmounts.length; i++) { + locks[i] = _createLock(orderData.idsAndAmounts[i][0], orderData.idsAndAmounts[i][1]); + } + + BatchCompact memory batchCompact = BatchCompact({ + arbiter: orderData.arbiter, + sponsor: order.user, + nonce: nonce + 1, + expires: order.openDeadline, + commitments: locks + }); + + return _convertToResolvedCrossChainOrder(orderData, order.fillDeadline, batchCompact); } /// @inheritdoc IOriginSettler function resolve(OnchainCrossChainOrder calldata order) external view returns (ResolvedCrossChainOrder memory) { - OrderData calldata orderData = _decodeOrderData(order); + // Check if orderDataType is the one expected by the allocator + if (order.orderDataType != ORDERDATA_ONCHAIN_TYPEHASH) { + revert InvalidOrderDataType(order.orderDataType, ORDERDATA_ONCHAIN_TYPEHASH); + } + + (Order calldata orderData, uint256 expires) = _decodeOrderData(order.orderData, true); uint256 idsLength = orderData.idsAndAmounts.length; Lock[] memory locks = new Lock[](idsLength); for (uint256 i = 0; i < idsLength; i++) { @@ -160,14 +235,31 @@ contract HybridERC7683 is HybridAllocator, IHybridERC7683 { } BatchCompact memory batchCompact = BatchCompact({ arbiter: orderData.arbiter, - sponsor: orderData.sponsor, + sponsor: msg.sender, nonce: nonce + 1, // nonce is incremented by 1 when the claim is registered - expires: orderData.expires, + expires: expires, commitments: locks }); return _convertToResolvedCrossChainOrder(orderData, order.fillDeadline, batchCompact); } + function _storeQualification(bytes32 claimHash, bytes32 qualification) private { + // store the allocator data with the claims mapping. + assembly ("memory-safe") { + if and(qualification, _ACTIVE_CLAIM_MASK) { + mstore(0, _INVALID_QUALIFICATION_ERROR_SIGNATURE) + mstore(0x20, qualification) + revert(0x1c, 0x24) + } + + mstore(0x00, claimHash) + mstore(0x20, claims.slot) + let claimSlot := keccak256(0x00, 0x40) + let indicator := or(qualification, _ACTIVE_CLAIM_MASK) + sstore(claimSlot, indicator) + } + } + function _checkClaim(bytes32 claimHash, bytes calldata allocatorData) private view @@ -180,8 +272,8 @@ contract HybridERC7683 is HybridAllocator, IHybridERC7683 { let data := sload(claimSlot) valid := and(data, _ACTIVE_CLAIM_MASK) - let storedTargetBlock := shr(128, data) - let storedMaximumBlocksAfterTarget := shr(129, shl(128, data)) + let storedTargetBlock := shr(57, data) + let storedMaximumBlocksAfterTarget := shr(200, shl(199, data)) targetBlock := calldataload(allocatorData.offset) maximumBlocksAfterTarget := calldataload(add(allocatorData.offset, 0x20)) @@ -194,7 +286,7 @@ contract HybridERC7683 is HybridAllocator, IHybridERC7683 { } function _convertToResolvedCrossChainOrder( - OrderData calldata orderData, + Order calldata orderData, uint256 fillDeadline, BatchCompact memory batchCompact ) private view returns (ResolvedCrossChainOrder memory) { @@ -238,13 +330,18 @@ contract HybridERC7683 is HybridAllocator, IHybridERC7683 { fillInstructions[0] = FillInstruction({ destinationChainId: orderData.chainId, destinationSettler: _convertAddressToBytes32(orderData.tribunal), - originData: abi.encode(claim, mandate, orderData.targetBlock, orderData.maximumBlocksAfterTarget) + originData: abi.encode( + claim, + mandate, + uint200(bytes25(orderData.qualification >> 1)), + uint56(uint256(orderData.qualification >> 1)) + ) }); return ResolvedCrossChainOrder({ - user: orderData.sponsor, + user: batchCompact.sponsor, originChainId: block.chainid, - openDeadline: uint32(fillDeadline), + openDeadline: uint32(batchCompact.expires), fillDeadline: uint32(fillDeadline), orderId: bytes32(batchCompact.nonce), maxSpent: maxSpent, @@ -253,14 +350,26 @@ contract HybridERC7683 is HybridAllocator, IHybridERC7683 { }); } - function _decodeOrderData(OnchainCrossChainOrder calldata order_) + function _decodeOrderData(bytes calldata orderData, bool isOnChain) private pure - returns (OrderData calldata orderData) + returns (Order calldata order, uint256 expires) { - bytes calldata rawOrderData = LibBytes.dynamicStructInCalldata(order_.orderData, 0x00); + // orderData includes the OrderData(OnChain/Gasless) struct, and the nested Order struct. + // 0x00: OrderDataOnChain.offset + // 0x20: OrderDataOnChain.order.offset + // 0x40: OrderDataOnChain.expires + + // 0x00: OrderDataGasless.offset + // 0x20: OrderDataGasless.order.offset + assembly ("memory-safe") { - orderData := rawOrderData.offset + let l := sub(orderData.length, 0x20) + let s := calldataload(add(orderData.offset, 0x20)) // Relative offset of `orderBytes` from `orderData.offset` and the `OrderData...` struct. + order := add(orderData.offset, add(s, 0x20)) // Add 0x20 since the OrderStruct is within the `OrderData...` struct + if shr(64, or(s, or(l, orderData.offset))) { revert(l, 0x00) } + + expires := mul(calldataload(add(orderData.offset, 0x40)), isOnChain) } } diff --git a/src/allocators/lib/TypeHashes.sol b/src/allocators/lib/TypeHashes.sol index 86df937..ba30237 100644 --- a/src/allocators/lib/TypeHashes.sol +++ b/src/allocators/lib/TypeHashes.sol @@ -4,9 +4,10 @@ pragma solidity ^0.8.27; // keccak256("Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") bytes32 constant MANDATE_TYPEHASH = 0x74d9c10530859952346f3e046aa2981a24bb7524b8394eb45a9deddced9d6501; -// keccak256("BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments)Lock(bytes12 lockTag,address token,uint256 amount,Mandate mandate) +// keccak256("BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments,Mandate mandate) +// Lock(bytes12 lockTag,address token,uint256 amount) // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") -bytes32 constant BATCH_COMPACT_WITNESS_TYPEHASH = 0xcddb20593d74f00cd789982c798bca41f8ba5f6835c95a771fd48b110d8b1249; +bytes32 constant BATCH_COMPACT_WITNESS_TYPEHASH = 0x5ede122c736b60a8b718f83dcfb5d6e4aa27c9714d0c7bc9ca86562b8f878463; // keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,bytes12 lockTag,address token,uint256 amount,Mandate mandate) // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") diff --git a/src/interfaces/IHybridERC7683.sol b/src/interfaces/IHybridERC7683.sol index 8524d42..7db3d8c 100644 --- a/src/interfaces/IHybridERC7683.sol +++ b/src/interfaces/IHybridERC7683.sol @@ -1,33 +1,38 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.27; +import {Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; import {IHybridAllocator} from 'src/interfaces/IHybridAllocator.sol'; interface IHybridERC7683 is IHybridAllocator, IOriginSettler { - struct OrderData { - // BATCH COMPACT - address arbiter; // The account tasked with verifying and submitting the claim. - address sponsor; // The account to source the tokens from. - // uint256 nonce; // A parameter to enforce replay protection, scoped to allocator. - uint256 expires; // The time at which the claim expires. - uint256[2][] idsAndAmounts; // The ids of the ERC6909 tokens to allocate. - // MANDATE - uint256 chainId; // (implicit arg, included in EIP712 payload) - address tribunal; // (implicit arg, included in EIP712 payload) - address recipient; // Recipient of settled tokens - // uint256 expires; // Mandate expiration timestamp - address settlementToken; // Settlement token (address(0) for native) - uint256 minimumAmount; // Minimum settlement amount - uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in - uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline) - uint256[] decayCurve; // Block durations, fill increases, & claim decreases. - bytes32 salt; // Replay protection parameter - // ADDITIONAL INPUT - uint128 targetBlock; // The block number at the target chain on which the PGA is executed / the reverse dutch auction starts. - uint120 maximumBlocksAfterTarget; // Blocks after target block that are still fillable. + struct OrderDataOnChain { + Order order; // The remaining BatchCompact and Mandate data + uint256 expires; // COMPACT - The time at which the claim expires and the user is able to withdraw their funds. + } + + struct OrderDataGasless { + Order order; // The remaining BatchCompact and Mandate data + } + + /// @dev The data that OnChain and Gasless orders have in common + struct Order { + address arbiter; // COMPACT - The account tasked with verifying and submitting the claim. + uint256[2][] idsAndAmounts; // COMPACT - The token IDs and amounts to allocate. + uint256 chainId; // MANDATE - (implicit arg, included in EIP712 payload) + address tribunal; // MANDATE - (implicit arg, included in EIP712 payload) + address recipient; // MANDATE - Recipient of settled tokens + // uint256 expires; // MANDATE - Mandate expiration timestamp, which equals the fill deadline + address settlementToken; // MANDATE - Settlement token (address(0) for native) + uint256 minimumAmount; // MANDATE - Minimum settlement amount + uint256 baselinePriorityFee; // MANDATE - Base fee threshold where scaling kicks in + uint256 scalingFactor; // MANDATE - Fee scaling multiplier (1e18 baseline) + uint256[] decayCurve; // MANDATE - Block durations, fill increases, & claim decreases. + bytes32 salt; // MANDATE - Replay protection parameter + bytes32 qualification; // ADDITIONAL INPUT - [uint199 targetBlock, uint56 maximumBlocksAfterTarget, uint1(0)] The block number at the target chain on which the PGA is executed / the reverse dutch auction starts & blocks after target block that are still fillable. } error InvalidOrderDataType(bytes32 orderDataType, bytes32 expectedOrderDataType); error InvalidOriginSettler(address originSettler, address expectedOriginSettler); + error InvalidQualification(bytes32 qualification); } diff --git a/test/HybridERC7683.t.sol b/test/HybridERC7683.t.sol new file mode 100644 index 0000000..e22ab60 --- /dev/null +++ b/test/HybridERC7683.t.sol @@ -0,0 +1,1119 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; +import {ERC6909} from '@solady/tokens/ERC6909.sol'; +import {TheCompact} from '@uniswap/the-compact/TheCompact.sol'; +import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; + +import {IdLib} from '@uniswap/the-compact/lib/IdLib.sol'; + +import {BatchClaim} from '@uniswap/the-compact/types/BatchClaims.sol'; +import {BatchClaimComponent, Component} from '@uniswap/the-compact/types/Components.sol'; +import {BatchCompact, COMPACT_TYPEHASH, LOCK_TYPEHASH, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; +import {ForcedWithdrawalStatus} from '@uniswap/the-compact/types/ForcedWithdrawalStatus.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 {TestHelper} from 'test/util/TestHelper.sol'; + +import {HybridERC7683} from 'src/allocators/HybridERC7683.sol'; + +import {BATCH_COMPACT_WITNESS_TYPEHASH, MANDATE_TYPEHASH} from 'src/allocators/lib/TypeHashes.sol'; +import {BatchClaim as TribunalClaim, Mandate} from 'src/allocators/types/TribunalStructs.sol'; +import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; +import {IHybridAllocator} from 'src/interfaces/IHybridAllocator.sol'; +import {IHybridERC7683} from 'src/interfaces/IHybridERC7683.sol'; + +import {ERC20Mock} from 'src/test/ERC20Mock.sol'; + +import {console} from 'forge-std/console.sol'; + +abstract contract MocksSetup is Test, TestHelper { + address user; + uint256 userPK; + address attacker; + uint256 attackerPK; + address arbiter; + address tribunal; + address signer; + uint256 signerPK; + ERC20Mock usdc; + TheCompact compactContract; + HybridERC7683 hybridERC7683Allocator; + bytes12 usdcLockTag; + uint256 usdcId; + + ResetPeriod defaultResetPeriod = ResetPeriod.OneMinute; + Scope defaultScope = Scope.Multichain; + uint256 defaultResetPeriodTimestamp = 60; + uint256 defaultAmount = 1000; + uint256 defaultNonce; + uint256 defaultOutputChainId = 130; + address defaultOutputToken = makeAddr('outputToken'); + uint256 defaultMinimumAmount = 1000; + uint256 defaultBaselinePriorityFee = 0; + uint256 defaultScalingFactor = 0; + uint256[] defaultDecayCurve = new uint256[](0); + bytes32 defaultSalt = bytes32(0x0000000000000000000000000000000000000000000000000000000000000007); + uint200 defaultTargetBlock = 100; + uint56 defaultMaximumBlocksAfterTarget = 10; + + uint256[2][] defaultIdsAndAmounts = new uint256[2][](1); + Lock[] defaultCommitments; + + bytes32 ORDERDATA_GASLESS_TYPEHASH; + bytes32 ORDERDATA_ONCHAIN_TYPEHASH; + + function setUp() public virtual { + (user, userPK) = makeAddrAndKey('user'); + (attacker, attackerPK) = makeAddrAndKey('attacker'); + (signer, signerPK) = makeAddrAndKey('signer'); + arbiter = makeAddr('arbiter'); + tribunal = makeAddr('tribunal'); + usdc = new ERC20Mock('USDC', 'USDC'); + compactContract = new TheCompact(); + hybridERC7683Allocator = new HybridERC7683(address(compactContract), signer); + + // Mint tokens to user + deal(user, 1 ether); + usdc.mint(user, 1 ether); + + usdcLockTag = _toLockTag(address(hybridERC7683Allocator), defaultScope, defaultResetPeriod); + usdcId = _toId(defaultScope, defaultResetPeriod, address(hybridERC7683Allocator), address(usdc)); + defaultNonce = 1; + + ORDERDATA_GASLESS_TYPEHASH = hybridERC7683Allocator.ORDERDATA_GASLESS_TYPEHASH(); + ORDERDATA_ONCHAIN_TYPEHASH = hybridERC7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH(); + } +} + +abstract contract CreateHash is MocksSetup { + struct Allocator { + bytes32 hash; + } + + // stringified types + string EIP712_DOMAIN_TYPE = 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'; // Hashed inside the function + // EIP712 domain type + string name = 'The Compact'; + string version = '1'; + + string compactWitnessTypeString = + 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,bytes12 lockTag,address token,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; + string batchCompactWitnessTypeString = + 'BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments,Mandate mandate)Lock(bytes12 lockTag,address token,uint256 amount)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; + string mandateTypeString = + 'Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; + string witnessTypeString = + 'uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt'; + + function _hashCompact(BatchCompact memory data, Mandate memory mandate, address verifyingContract) + internal + view + returns (bytes32 digest) + { + bytes32 compactHash = _hashCompact(data, mandate); + // hash typed data + digest = keccak256( + abi.encodePacked( + '\x19\x01', // backslash is needed to escape the character + _domainSeparator(verifyingContract), + compactHash + ) + ); + } + + function _hashCompact(BatchCompact memory data, Mandate memory mandate) + internal + view + returns (bytes32 compactHash) + { + bytes32 mandateHash = _hashMandate(mandate); + compactHash = keccak256( + abi.encode( + keccak256(bytes(batchCompactWitnessTypeString)), + data.arbiter, + data.sponsor, + data.nonce, + data.expires, + _hashCommitments(data.commitments), + mandateHash + ) + ); + } + + function _hashMandate(Mandate memory mandate) internal view returns (bytes32) { + return keccak256( + abi.encode( + keccak256(bytes(mandateTypeString)), + defaultOutputChainId, + tribunal, + mandate.recipient, + mandate.expires, + mandate.token, + mandate.minimumAmount, + mandate.baselinePriorityFee, + mandate.scalingFactor, + keccak256(abi.encodePacked(mandate.decayCurve)), + mandate.salt + ) + ); + } + + function _hashCommitments(Lock[] memory commitments) internal pure returns (bytes32) { + bytes32[] memory hashes = new bytes32[](commitments.length); + for (uint256 i = 0; i < commitments.length; i++) { + hashes[i] = keccak256( + abi.encode(LOCK_TYPEHASH, commitments[i].lockTag, commitments[i].token, commitments[i].amount) + ); + } + return keccak256(abi.encodePacked(hashes)); + } + + function _getTypeHash() internal view returns (bytes32) { + return keccak256(bytes(batchCompactWitnessTypeString)); + } + + function _domainSeparator(address verifyingContract) internal view returns (bytes32) { + return keccak256( + abi.encode( + keccak256(bytes(EIP712_DOMAIN_TYPE)), + keccak256(bytes(name)), + keccak256(bytes(version)), + block.chainid, + verifyingContract + ) + ); + } + + function _signMessage(bytes32 hash_, uint256 signerPK_) internal pure returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPK_, hash_); + return abi.encodePacked(r, s, v); + } + + function _hashAndSign(BatchCompact memory data, Mandate memory mandate, address verifyingContract, uint256 signerPK) + internal + view + returns (bytes memory) + { + bytes32 hash = _hashCompact(data, mandate, verifyingContract); + bytes memory signature = _signMessage(hash, signerPK); + return signature; + } + + function _createWitnessHash(Mandate memory mandate) internal view returns (bytes32) { + return keccak256( + abi.encode( + MANDATE_TYPEHASH, + defaultOutputChainId, + tribunal, + mandate.recipient, + mandate.expires, + mandate.token, + mandate.minimumAmount, + mandate.baselinePriorityFee, + mandate.scalingFactor, + keccak256(abi.encodePacked(mandate.decayCurve)), + mandate.salt + ) + ); + } + + function _allocatorData(uint200 targetBlock_, uint56 maximumBlocksAfterTarget_) internal pure returns (bytes32) { + return bytes32(uint256(targetBlock_) << 57 | uint256(maximumBlocksAfterTarget_) << 1); + } +} + +abstract contract CompactData is CreateHash { + BatchCompact internal compact; + Mandate internal mandate; + + function setUp() public virtual override { + super.setUp(); + + defaultIdsAndAmounts[0][0] = usdcId; + defaultIdsAndAmounts[0][1] = defaultAmount; + + defaultCommitments.push(Lock({lockTag: usdcLockTag, token: address(usdc), amount: defaultAmount})); + + compact.arbiter = arbiter; + compact.sponsor = user; + compact.nonce = defaultNonce; + compact.expires = _getClaimExpiration(); + compact.commitments = defaultCommitments; + + mandate.recipient = user; + mandate.expires = _getFillExpiration(); + mandate.token = defaultOutputToken; + mandate.minimumAmount = defaultMinimumAmount; + mandate.baselinePriorityFee = defaultBaselinePriorityFee; + mandate.scalingFactor = defaultScalingFactor; + mandate.decayCurve = defaultDecayCurve; + mandate.salt = defaultSalt; + } + + function _getCompact() internal returns (BatchCompact memory) { + compact.expires = _getClaimExpiration(); + return compact; + } + + function _getMandate() internal returns (Mandate memory) { + mandate.expires = _getFillExpiration(); + return mandate; + } + + function _getFillExpiration() internal view returns (uint256) { + return vm.getBlockTimestamp() + defaultResetPeriodTimestamp - 1; + } + + function _getClaimExpiration() internal view returns (uint256) { + return vm.getBlockTimestamp() + defaultResetPeriodTimestamp; + } +} + +abstract contract GaslessCrossChainOrderData is CompactData { + IOriginSettler.GaslessCrossChainOrder private gaslessCrossChainOrder; + + function setUp() public virtual override { + super.setUp(); + + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + gaslessCrossChainOrder.originSettler = address(hybridERC7683Allocator); + gaslessCrossChainOrder.user = compact_.sponsor; + gaslessCrossChainOrder.nonce = compact_.nonce; + gaslessCrossChainOrder.originChainId = block.chainid; + gaslessCrossChainOrder.openDeadline = uint32(compact_.expires); + gaslessCrossChainOrder.fillDeadline = uint32(mandate_.expires); + gaslessCrossChainOrder.orderDataType = hybridERC7683Allocator.ORDERDATA_GASLESS_TYPEHASH(); + gaslessCrossChainOrder.orderData = abi.encode( + IHybridERC7683.OrderDataGasless({ + order: IHybridERC7683.Order({ + arbiter: compact_.arbiter, + idsAndAmounts: defaultIdsAndAmounts, + chainId: defaultOutputChainId, + tribunal: tribunal, + recipient: mandate_.recipient, + settlementToken: mandate_.token, + minimumAmount: mandate_.minimumAmount, + baselinePriorityFee: mandate_.baselinePriorityFee, + scalingFactor: mandate_.scalingFactor, + decayCurve: mandate_.decayCurve, + salt: mandate_.salt, + qualification: _allocatorData(defaultTargetBlock, defaultMaximumBlocksAfterTarget) + }) + }) + ); + } + + function _getGaslessCrossChainOrder( + address allocator, + BatchCompact memory compact_, + Mandate memory mandate_, + uint256 chainId_, + bytes32 orderDataGaslessTypeHash_, + address verifyingContract, + uint256 signerPK + ) internal view returns (IOriginSettler.GaslessCrossChainOrder memory, bytes memory signature) { + IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_ = IOriginSettler.GaslessCrossChainOrder({ + originSettler: allocator, + user: compact_.sponsor, + nonce: compact_.nonce, + originChainId: chainId_, + openDeadline: uint32(compact_.expires), + fillDeadline: uint32(mandate_.expires), + orderDataType: orderDataGaslessTypeHash_, + orderData: abi.encode( + IHybridERC7683.OrderDataGasless({ + order: IHybridERC7683.Order({ + arbiter: compact_.arbiter, + idsAndAmounts: defaultIdsAndAmounts, + chainId: defaultOutputChainId, + tribunal: tribunal, + recipient: mandate_.recipient, + settlementToken: mandate_.token, + minimumAmount: mandate_.minimumAmount, + baselinePriorityFee: mandate_.baselinePriorityFee, + scalingFactor: mandate_.scalingFactor, + decayCurve: mandate_.decayCurve, + salt: mandate_.salt, + qualification: _allocatorData(defaultTargetBlock, defaultMaximumBlocksAfterTarget) + }) + }) + ) + }); + + (bytes memory signature_) = _hashAndSign(compact_, mandate_, verifyingContract, signerPK); + return (gaslessCrossChainOrder_, signature_); + } + + function _getGaslessCrossChainOrder() + internal + returns (IOriginSettler.GaslessCrossChainOrder memory, bytes memory signature) + { + (bytes memory signature_) = _hashAndSign(_getCompact(), _getMandate(), address(compactContract), userPK); + return (gaslessCrossChainOrder, signature_); + } +} + +abstract contract OnChainCrossChainOrderData is CompactData { + IOriginSettler.OnchainCrossChainOrder private onchainCrossChainOrder; + + function setUp() public virtual override { + super.setUp(); + + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + onchainCrossChainOrder.fillDeadline = uint32(mandate_.expires); + onchainCrossChainOrder.orderDataType = hybridERC7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH(); + onchainCrossChainOrder.orderData = abi.encode( + IHybridERC7683.OrderDataOnChain({ + order: IHybridERC7683.Order({ + arbiter: compact_.arbiter, + idsAndAmounts: defaultIdsAndAmounts, + chainId: defaultOutputChainId, + tribunal: tribunal, + recipient: mandate_.recipient, + settlementToken: mandate_.token, + minimumAmount: mandate_.minimumAmount, + baselinePriorityFee: mandate_.baselinePriorityFee, + scalingFactor: mandate_.scalingFactor, + decayCurve: mandate_.decayCurve, + salt: mandate_.salt, + qualification: _allocatorData(defaultTargetBlock, defaultMaximumBlocksAfterTarget) + }), + expires: compact_.expires + }) + ); + } + + function _getOnChainCrossChainOrder() internal view returns (IOriginSettler.OnchainCrossChainOrder memory) { + return onchainCrossChainOrder; + } + + function _getOnChainCrossChainOrder(BatchCompact memory compact_, Mandate memory mandate_, bytes32 orderDataType_) + internal + view + returns (IOriginSettler.OnchainCrossChainOrder memory) + { + IOriginSettler.OnchainCrossChainOrder memory onchainCrossChainOrder_ = IOriginSettler.OnchainCrossChainOrder({ + fillDeadline: uint32(mandate_.expires), + orderDataType: orderDataType_, + orderData: abi.encode( + IHybridERC7683.OrderDataOnChain({ + order: IHybridERC7683.Order({ + arbiter: compact_.arbiter, + idsAndAmounts: defaultIdsAndAmounts, + chainId: defaultOutputChainId, + tribunal: tribunal, + recipient: mandate_.recipient, + settlementToken: mandate_.token, + minimumAmount: mandate_.minimumAmount, + baselinePriorityFee: mandate_.baselinePriorityFee, + scalingFactor: mandate_.scalingFactor, + decayCurve: mandate_.decayCurve, + salt: mandate_.salt, + qualification: _allocatorData(defaultTargetBlock, defaultMaximumBlocksAfterTarget) + }), + expires: compact_.expires + }) + ) + }); + return onchainCrossChainOrder_; + } +} + +contract HybridERC7683_open is OnChainCrossChainOrderData { + function test_revert_InvalidOrderDataType() public { + // Order data type is invalid + bytes32 falseOrderDataType = keccak256('false'); + IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_ = _getOnChainCrossChainOrder(); + onChainCrossChainOrder_.orderDataType = falseOrderDataType; + + vm.prank(user); + vm.expectRevert( + abi.encodeWithSelector( + IHybridERC7683.InvalidOrderDataType.selector, + falseOrderDataType, + hybridERC7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH() + ) + ); + hybridERC7683Allocator.open(onChainCrossChainOrder_); + } + + function test_revert_InvalidQualification() public { + // Provide tokens for allocation + vm.prank(user); + usdc.transfer(address(hybridERC7683Allocator), defaultAmount); + + IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_ = _getOnChainCrossChainOrder(); + (IHybridERC7683.OrderDataOnChain memory orderDataOnChain) = + abi.decode(onChainCrossChainOrder_.orderData, (IHybridERC7683.OrderDataOnChain)); + orderDataOnChain.order.qualification = bytes32(uint256(1)); + onChainCrossChainOrder_.orderData = abi.encode(orderDataOnChain); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(IHybridERC7683.InvalidQualification.selector, bytes32(uint256(1)))); + hybridERC7683Allocator.open(onChainCrossChainOrder_); + } + + function test_orderDataType() public view { + assertEq(hybridERC7683Allocator.ORDERDATA_GASLESS_TYPEHASH(), ORDERDATA_GASLESS_TYPEHASH); + } + + function test_successful() public { + // Provide tokens for allocation + vm.prank(user); + usdc.transfer(address(hybridERC7683Allocator), defaultAmount); + + IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_ = _getOnChainCrossChainOrder(); + IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); + IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); + IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); + maxSpent[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(defaultOutputToken))), + amount: type(uint256).max, + recipient: bytes32(uint256(uint160(user))), + chainId: defaultOutputChainId + }); + minReceived[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(address(usdc)))), + amount: defaultMinimumAmount, + recipient: bytes32(uint256(uint160(user))), + chainId: block.chainid + }); + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + TribunalClaim memory claim = + TribunalClaim({chainId: block.chainid, compact: compact_, sponsorSignature: '', allocatorSignature: ''}); + fillInstructions[0] = IOriginSettler.FillInstruction({ + destinationChainId: defaultOutputChainId, + destinationSettler: bytes32(uint256(uint160(tribunal))), + originData: abi.encode(claim, mandate_, defaultTargetBlock, defaultMaximumBlocksAfterTarget) + }); + + IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ + user: user, + originChainId: block.chainid, + openDeadline: uint32(compact_.expires), + fillDeadline: uint32(mandate_.expires), + orderId: bytes32(defaultNonce), + maxSpent: maxSpent, + minReceived: minReceived, + fillInstructions: fillInstructions + }); + vm.prank(user); + vm.expectEmit(true, false, false, true, address(hybridERC7683Allocator)); + emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); + hybridERC7683Allocator.open(onChainCrossChainOrder_); + } +} + +contract HybridERC7683_openFor is GaslessCrossChainOrderData { + function test_revert_InvalidOrderDataType() public { + // Order data type is invalid + bytes32 falseOrderDataType = keccak256('false'); + + vm.prank(user); + vm.expectRevert( + abi.encodeWithSelector( + IHybridERC7683.InvalidOrderDataType.selector, + falseOrderDataType, + hybridERC7683Allocator.ORDERDATA_GASLESS_TYPEHASH() + ) + ); + (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = + _getGaslessCrossChainOrder(); + falseGaslessCrossChainOrder.orderDataType = falseOrderDataType; + hybridERC7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); + } + + function test_orderDataType() public view { + assertEq(hybridERC7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH(), ORDERDATA_ONCHAIN_TYPEHASH); + } + + function test_successful_userHimself() public { + // Provide tokens for allocation + vm.prank(user); + usdc.transfer(address(hybridERC7683Allocator), defaultAmount); + + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = + _getGaslessCrossChainOrder(); + IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); + IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); + IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); + maxSpent[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(defaultOutputToken))), + amount: type(uint256).max, + recipient: bytes32(uint256(uint160(user))), + chainId: defaultOutputChainId + }); + minReceived[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(address(usdc)))), + amount: defaultMinimumAmount, + recipient: bytes32(uint256(uint160(user))), + chainId: block.chainid + }); + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + TribunalClaim memory claim = + TribunalClaim({chainId: block.chainid, compact: compact_, sponsorSignature: '', allocatorSignature: ''}); + fillInstructions[0] = IOriginSettler.FillInstruction({ + destinationChainId: defaultOutputChainId, + destinationSettler: bytes32(uint256(uint160(tribunal))), + originData: abi.encode(claim, mandate_, defaultTargetBlock, defaultMaximumBlocksAfterTarget) + }); + + IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ + user: user, + originChainId: block.chainid, + openDeadline: uint32(compact_.expires), + fillDeadline: uint32(mandate_.expires), + orderId: bytes32(defaultNonce), + maxSpent: maxSpent, + minReceived: minReceived, + fillInstructions: fillInstructions + }); + vm.prank(user); + vm.expectEmit(true, false, false, true, address(hybridERC7683Allocator)); + emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); + hybridERC7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); + } + + function test_successful_relayed() public { + // Provide tokens for allocation + vm.prank(user); + usdc.transfer(address(hybridERC7683Allocator), defaultAmount); + + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = + _getGaslessCrossChainOrder(); + IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); + IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); + IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); + maxSpent[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(defaultOutputToken))), + amount: type(uint256).max, + recipient: bytes32(uint256(uint160(user))), + chainId: defaultOutputChainId + }); + minReceived[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(address(usdc)))), + amount: defaultMinimumAmount, + recipient: bytes32(uint256(uint160(user))), + chainId: block.chainid + }); + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + TribunalClaim memory claim = + TribunalClaim({chainId: block.chainid, compact: compact_, sponsorSignature: '', allocatorSignature: ''}); + fillInstructions[0] = IOriginSettler.FillInstruction({ + destinationChainId: defaultOutputChainId, + destinationSettler: bytes32(uint256(uint160(tribunal))), + originData: abi.encode(claim, mandate_, defaultTargetBlock, defaultMaximumBlocksAfterTarget) + }); + + IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ + user: user, + originChainId: block.chainid, + openDeadline: uint32(compact_.expires), + fillDeadline: uint32(mandate_.expires), + orderId: bytes32(defaultNonce), + maxSpent: maxSpent, + minReceived: minReceived, + fillInstructions: fillInstructions + }); + vm.prank(makeAddr('filler')); + vm.expectEmit(true, false, false, true, address(hybridERC7683Allocator)); + emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); + hybridERC7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); + } +} + +contract HybridERC7683_authorizeClaim is OnChainCrossChainOrderData, GaslessCrossChainOrderData { + function setUp() public override(OnChainCrossChainOrderData, GaslessCrossChainOrderData) { + super.setUp(); + } + + function test_revert_InvalidCaller() public { + vm.expectRevert( + abi.encodeWithSelector(IHybridAllocator.InvalidCaller.selector, address(this), address(compactContract)) + ); + hybridERC7683Allocator.authorizeClaim(bytes32(0), address(0), address(0), 0, 0, new uint256[2][](0), bytes('')); + } + + function test_successful_onChainClaim() public { + // Provide tokens for allocation + vm.prank(user); + usdc.transfer(address(hybridERC7683Allocator), defaultAmount); + + IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_ = _getOnChainCrossChainOrder(); + + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + vm.prank(user); + hybridERC7683Allocator.open(onChainCrossChainOrder_); + + address filler = makeAddr('filler'); + + BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); + { + Component[] memory components = new Component[](1); + components[0] = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); + BatchClaimComponent memory batchClaimComponent = + BatchClaimComponent({id: usdcId, allocatedAmount: defaultAmount, portions: components}); + batchClaimComponents[0] = batchClaimComponent; + } + BatchClaim memory claim = BatchClaim({ + allocatorData: abi.encode(defaultTargetBlock, defaultMaximumBlocksAfterTarget), + sponsorSignature: '', + sponsor: user, + nonce: compact_.nonce, + expires: compact_.expires, + witness: _createWitnessHash(mandate_), + witnessTypestring: witnessTypeString, + claims: batchClaimComponents + }); + vm.prank(arbiter); + compactContract.batchClaim(claim); + + assertEq(compactContract.balanceOf(user, usdcId), 0); + assertEq(usdc.balanceOf(filler), defaultAmount); + } + + function test_successful_signatureClaim() public { + // Provide tokens for allocation + vm.prank(user); + usdc.transfer(address(hybridERC7683Allocator), defaultAmount); + + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = + _getGaslessCrossChainOrder(); + + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + bytes32 claimHash = _hashCompact(compact_, mandate_); + + vm.prank(user); + hybridERC7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); + + address filler = makeAddr('filler'); + + // Create qualified claim hash for off-chain verification + bytes32 qualifiedClaimHash = + keccak256(abi.encode(hybridERC7683Allocator.QUALIFICATION_TYPEHASH(), claimHash, uint128(0), uint120(0))); + bytes32 digest = + keccak256(abi.encodePacked(bytes2(0x1901), compactContract.DOMAIN_SEPARATOR(), qualifiedClaimHash)); + + // Sign with the signer + bytes memory allocatorSignature = _signMessage(digest, signerPK); + + Component[] memory components = new Component[](1); + components[0] = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); + BatchClaimComponent memory batchClaimComponent = + BatchClaimComponent({id: usdcId, allocatedAmount: defaultAmount, portions: components}); + BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); + batchClaimComponents[0] = batchClaimComponent; + BatchClaim memory claim = BatchClaim({ + allocatorData: abi.encode(defaultTargetBlock, defaultMaximumBlocksAfterTarget, allocatorSignature), + sponsorSignature: '', + sponsor: user, + nonce: defaultNonce, + expires: compact_.expires, + witness: _createWitnessHash(mandate_), + witnessTypestring: witnessTypeString, + claims: batchClaimComponents + }); + vm.prank(arbiter); + compactContract.batchClaim(claim); + + assertEq(compactContract.balanceOf(user, usdcId), 0); + assertEq(usdc.balanceOf(filler), defaultAmount); + } + + function test_successful_signatureOnly() public { + // Provide tokens for allocation + vm.prank(user); + usdc.approve(address(compactContract), defaultAmount); + + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + bytes32 claimHash = _hashCompact(compact_, mandate_); + + vm.prank(user); + compactContract.depositERC20AndRegister( + address(usdc), usdcLockTag, defaultAmount, claimHash, BATCH_COMPACT_WITNESS_TYPEHASH + ); + + address filler = makeAddr('filler'); + + // Create qualified claim hash for off-chain verification + bytes32 qualifiedClaimHash = keccak256( + abi.encode( + hybridERC7683Allocator.QUALIFICATION_TYPEHASH(), + claimHash, + defaultTargetBlock, + defaultMaximumBlocksAfterTarget + ) + ); + bytes32 digest = + keccak256(abi.encodePacked(bytes2(0x1901), compactContract.DOMAIN_SEPARATOR(), qualifiedClaimHash)); + console.log('digest'); + console.logBytes32(digest); + + // Sign with wrong signer + bytes memory allocatorSignature = _signMessage(digest, signerPK); + + BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); + { + Component[] memory components = new Component[](1); + components[0] = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); + BatchClaimComponent memory batchClaimComponent = + BatchClaimComponent({id: usdcId, allocatedAmount: defaultAmount, portions: components}); + batchClaimComponents[0] = batchClaimComponent; + } + BatchClaim memory claim = BatchClaim({ + allocatorData: abi.encode(defaultTargetBlock, defaultMaximumBlocksAfterTarget, allocatorSignature), + sponsorSignature: '', + sponsor: user, + nonce: defaultNonce, + expires: compact_.expires, + witness: _createWitnessHash(mandate_), + witnessTypestring: witnessTypeString, + claims: batchClaimComponents + }); + + vm.prank(arbiter); + compactContract.batchClaim(claim); + } + + function test_revert_InvalidSignature() public { + // Provide tokens for allocation + vm.prank(user); + usdc.approve(address(compactContract), defaultAmount); + + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + bytes32 claimHash = _hashCompact(compact_, mandate_); + + vm.prank(user); + compactContract.depositERC20AndRegister( + address(usdc), usdcLockTag, defaultAmount, claimHash, BATCH_COMPACT_WITNESS_TYPEHASH + ); + + address filler = makeAddr('filler'); + + // Create qualified claim hash for off-chain verification + bytes32 qualifiedClaimHash = keccak256( + abi.encode( + hybridERC7683Allocator.QUALIFICATION_TYPEHASH(), + claimHash, + defaultTargetBlock, + defaultMaximumBlocksAfterTarget + ) + ); + bytes32 digest = + keccak256(abi.encodePacked(bytes2(0x1901), compactContract.DOMAIN_SEPARATOR(), qualifiedClaimHash)); + console.log('digest'); + console.logBytes32(digest); + + // Sign with wrong signer + bytes memory allocatorSignature = _signMessage(digest, attackerPK); + + BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); + { + Component[] memory components = new Component[](1); + components[0] = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); + BatchClaimComponent memory batchClaimComponent = + BatchClaimComponent({id: usdcId, allocatedAmount: defaultAmount, portions: components}); + batchClaimComponents[0] = batchClaimComponent; + } + BatchClaim memory claim = BatchClaim({ + allocatorData: abi.encode(defaultTargetBlock, defaultMaximumBlocksAfterTarget, allocatorSignature), + sponsorSignature: '', + sponsor: user, + nonce: defaultNonce, + expires: compact_.expires, + witness: _createWitnessHash(mandate_), + witnessTypestring: witnessTypeString, + claims: batchClaimComponents + }); + + vm.prank(arbiter); + vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.InvalidSignature.selector)); + compactContract.batchClaim(claim); + } + + function test_revert_InvalidSignature_length() public { + // Provide tokens for allocation + vm.prank(user); + usdc.approve(address(compactContract), defaultAmount); + + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + bytes32 claimHash = _hashCompact(compact_, mandate_); + + vm.prank(user); + compactContract.depositERC20AndRegister( + address(usdc), usdcLockTag, defaultAmount, claimHash, BATCH_COMPACT_WITNESS_TYPEHASH + ); + + address filler = makeAddr('filler'); + + // Create qualified claim hash for off-chain verification + bytes32 qualifiedClaimHash = keccak256( + abi.encode( + hybridERC7683Allocator.QUALIFICATION_TYPEHASH(), + claimHash, + defaultTargetBlock, + defaultMaximumBlocksAfterTarget + ) + ); + bytes32 digest = + keccak256(abi.encodePacked(bytes2(0x1901), compactContract.DOMAIN_SEPARATOR(), qualifiedClaimHash)); + console.log('digest'); + console.logBytes32(digest); + + // Sign with wrong signer + bytes memory allocatorSignature = _signMessage(digest, signerPK); + + BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); + { + Component[] memory components = new Component[](1); + components[0] = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); + BatchClaimComponent memory batchClaimComponent = + BatchClaimComponent({id: usdcId, allocatedAmount: defaultAmount, portions: components}); + batchClaimComponents[0] = batchClaimComponent; + } + BatchClaim memory claim = BatchClaim({ + allocatorData: abi.encode(defaultTargetBlock, defaultMaximumBlocksAfterTarget, allocatorSignature, uint8(0)), // wrong length + sponsorSignature: '', + sponsor: user, + nonce: defaultNonce, + expires: compact_.expires, + witness: _createWitnessHash(mandate_), + witnessTypestring: witnessTypeString, + claims: batchClaimComponents + }); + + vm.prank(arbiter); + vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.InvalidSignature.selector)); + compactContract.batchClaim(claim); + } +} + +contract HybridERC7683_resolveFor is GaslessCrossChainOrderData { + function test_revert_InvalidOrderDataType() public { + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_,) = _getGaslessCrossChainOrder(); + bytes32 falseOrderDataType = keccak256('false'); + gaslessCrossChainOrder_.orderDataType = falseOrderDataType; + vm.expectRevert( + abi.encodeWithSelector( + IHybridERC7683.InvalidOrderDataType.selector, + falseOrderDataType, + hybridERC7683Allocator.ORDERDATA_GASLESS_TYPEHASH() + ) + ); + hybridERC7683Allocator.resolveFor(gaslessCrossChainOrder_, ''); + } + + function test_resolveFor_successful() public { + // Provide tokens for allocation so allocator holds funds + vm.prank(user); + usdc.transfer(address(hybridERC7683Allocator), defaultAmount); + + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_,) = _getGaslessCrossChainOrder(); + + IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); + IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); + IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); + maxSpent[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(defaultOutputToken))), + amount: type(uint256).max, + recipient: bytes32(uint256(uint160(user))), + chainId: defaultOutputChainId + }); + minReceived[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(address(usdc)))), + amount: defaultMinimumAmount, + recipient: bytes32(uint256(uint160(user))), + chainId: block.chainid + }); + + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + TribunalClaim memory claim = + TribunalClaim({chainId: block.chainid, compact: compact_, sponsorSignature: '', allocatorSignature: ''}); + fillInstructions[0] = IOriginSettler.FillInstruction({ + destinationChainId: defaultOutputChainId, + destinationSettler: bytes32(uint256(uint160(tribunal))), + originData: abi.encode(claim, mandate_, defaultTargetBlock, defaultMaximumBlocksAfterTarget) + }); + + IOriginSettler.ResolvedCrossChainOrder memory expected = IOriginSettler.ResolvedCrossChainOrder({ + user: user, + originChainId: block.chainid, + openDeadline: uint32(compact_.expires), + fillDeadline: uint32(mandate_.expires), + orderId: bytes32(defaultNonce), + maxSpent: maxSpent, + minReceived: minReceived, + fillInstructions: fillInstructions + }); + + vm.prank(user); + IOriginSettler.ResolvedCrossChainOrder memory resolved = + hybridERC7683Allocator.resolveFor(gaslessCrossChainOrder_, ''); + assertEq(resolved.user, expected.user); + assertEq(resolved.originChainId, expected.originChainId); + assertEq(resolved.openDeadline, expected.openDeadline); + assertEq(resolved.fillDeadline, expected.fillDeadline); + assertEq(resolved.orderId, expected.orderId); + assertEq(resolved.maxSpent.length, expected.maxSpent.length); + assertEq(resolved.maxSpent[0].token, expected.maxSpent[0].token); + assertEq(resolved.maxSpent[0].amount, expected.maxSpent[0].amount); + assertEq(resolved.maxSpent[0].recipient, expected.maxSpent[0].recipient); + assertEq(resolved.maxSpent[0].chainId, expected.maxSpent[0].chainId); + assertEq(resolved.minReceived.length, expected.minReceived.length); + assertEq(resolved.minReceived[0].token, expected.minReceived[0].token); + assertEq(resolved.minReceived[0].amount, expected.minReceived[0].amount); + assertEq(resolved.minReceived[0].recipient, expected.minReceived[0].recipient); + assertEq(resolved.minReceived[0].chainId, expected.minReceived[0].chainId); + assertEq(resolved.fillInstructions.length, expected.fillInstructions.length); + assertEq(resolved.fillInstructions[0].destinationChainId, expected.fillInstructions[0].destinationChainId); + assertEq(resolved.fillInstructions[0].destinationSettler, expected.fillInstructions[0].destinationSettler); + assertEq(resolved.fillInstructions[0].originData, expected.fillInstructions[0].originData); + } +} + +contract HybridERC7683_resolve is OnChainCrossChainOrderData { + function test_revert_InvalidOrderDataType() public { + IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_ = _getOnChainCrossChainOrder(); + onChainCrossChainOrder_.orderDataType = keccak256('false'); + vm.expectRevert( + abi.encodeWithSelector( + IHybridERC7683.InvalidOrderDataType.selector, + onChainCrossChainOrder_.orderDataType, + hybridERC7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH() + ) + ); + hybridERC7683Allocator.resolve(onChainCrossChainOrder_); + } + + function test_resolve_successful() public { + IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); + IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); + IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); + maxSpent[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(defaultOutputToken))), + amount: type(uint256).max, + recipient: bytes32(uint256(uint160(user))), + chainId: defaultOutputChainId + }); + minReceived[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(address(usdc)))), + amount: defaultMinimumAmount, + recipient: bytes32(uint256(uint160(user))), + chainId: block.chainid + }); + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + TribunalClaim memory claim = + TribunalClaim({chainId: block.chainid, compact: compact_, sponsorSignature: '', allocatorSignature: ''}); + fillInstructions[0] = IOriginSettler.FillInstruction({ + destinationChainId: defaultOutputChainId, + destinationSettler: bytes32(uint256(uint160(tribunal))), + originData: abi.encode(claim, mandate_, defaultTargetBlock, defaultMaximumBlocksAfterTarget) + }); + + IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ + user: user, + originChainId: block.chainid, + openDeadline: uint32(compact_.expires), + fillDeadline: uint32(mandate_.expires), + orderId: bytes32(defaultNonce), + maxSpent: maxSpent, + minReceived: minReceived, + fillInstructions: fillInstructions + }); + IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_ = + _getOnChainCrossChainOrder(compact_, mandate_, hybridERC7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH()); + vm.prank(user); + IOriginSettler.ResolvedCrossChainOrder memory resolved = hybridERC7683Allocator.resolve(onChainCrossChainOrder_); + assertEq(resolved.user, resolvedCrossChainOrder.user); + assertEq(resolved.originChainId, resolvedCrossChainOrder.originChainId); + assertEq(resolved.openDeadline, resolvedCrossChainOrder.openDeadline); + assertEq(resolved.fillDeadline, resolvedCrossChainOrder.fillDeadline); + assertEq(resolved.orderId, resolvedCrossChainOrder.orderId); + assertEq(resolved.maxSpent.length, resolvedCrossChainOrder.maxSpent.length); + assertEq(resolved.maxSpent[0].token, resolvedCrossChainOrder.maxSpent[0].token); + assertEq(resolved.maxSpent[0].amount, resolvedCrossChainOrder.maxSpent[0].amount); + assertEq(resolved.maxSpent[0].recipient, resolvedCrossChainOrder.maxSpent[0].recipient); + assertEq(resolved.maxSpent[0].chainId, resolvedCrossChainOrder.maxSpent[0].chainId); + assertEq(resolved.minReceived.length, resolvedCrossChainOrder.minReceived.length); + assertEq(resolved.minReceived[0].token, resolvedCrossChainOrder.minReceived[0].token); + assertEq(resolved.minReceived[0].amount, resolvedCrossChainOrder.minReceived[0].amount); + assertEq(resolved.minReceived[0].recipient, resolvedCrossChainOrder.minReceived[0].recipient); + assertEq(resolved.minReceived[0].chainId, resolvedCrossChainOrder.minReceived[0].chainId); + assertEq(resolved.fillInstructions.length, resolvedCrossChainOrder.fillInstructions.length); + assertEq( + resolved.fillInstructions[0].destinationChainId, + resolvedCrossChainOrder.fillInstructions[0].destinationChainId + ); + assertEq( + resolved.fillInstructions[0].destinationSettler, + resolvedCrossChainOrder.fillInstructions[0].destinationSettler + ); + assertEq(resolved.fillInstructions[0].originData, resolvedCrossChainOrder.fillInstructions[0].originData); + } +} + +contract HybridERC7683_qualificationTypehash is MocksSetup { + function test_qualificationTypehash() public view { + assertEq( + hybridERC7683Allocator.QUALIFICATION_TYPEHASH(), + 0x59866b84bd1f6c909cf2a31efd20c59e6c902e50f2c196994e5aa85cdc7d7ce0 + ); + } +} + +contract HybridERC7683_hybridAllocatorInheritance is MocksSetup { + function test_inheritsHybridAllocatorFunctionality() public view { + // Test that it properly inherits from HybridAllocator + assertEq(hybridERC7683Allocator.nonce(), 0); + assertEq(hybridERC7683Allocator.signerCount(), 1); + assertTrue(hybridERC7683Allocator.signers(signer)); + assertEq(hybridERC7683Allocator.ALLOCATOR_ID(), _toAllocatorId(address(hybridERC7683Allocator))); + } + + function test_allocateAndRegister() public { + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = usdcId; + idsAndAmounts[0][1] = defaultAmount; + + // Provide tokens + vm.prank(user); + usdc.transfer(address(hybridERC7683Allocator), defaultAmount); + + (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = hybridERC7683Allocator + .allocateAndRegister(user, idsAndAmounts, arbiter, _getClaimExpiration(), BATCH_COMPACT_WITNESS_TYPEHASH, ''); + + assertTrue(compactContract.isRegistered(user, claimHash, BATCH_COMPACT_WITNESS_TYPEHASH)); + assertTrue( + hybridERC7683Allocator.isClaimAuthorized(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), '') + ); + assertEq(registeredAmounts[0], defaultAmount); + assertEq(usdc.balanceOf(address(compactContract)), defaultAmount); + assertEq(compactContract.balanceOf(address(user), idsAndAmounts[0][0]), defaultAmount); + assertEq(nonce, 1); + } + + function _getClaimExpiration() internal view returns (uint256) { + return vm.getBlockTimestamp() + defaultResetPeriodTimestamp; + } +} From 442265cf7ef0029a815c3f76ba187f3f3e3cd92f Mon Sep 17 00:00:00 2001 From: mgretzke Date: Fri, 8 Aug 2025 14:07:11 +0200 Subject: [PATCH 20/63] PR comments and tests --- src/allocators/HybridAllocator.sol | 8 +++-- test/HybridAllocator.t.sol | 25 +++++++++++++++ test/HybridERC7683.t.sol | 50 +++++++++++++++++++++++++----- 3 files changed, 73 insertions(+), 10 deletions(-) diff --git a/src/allocators/HybridAllocator.sol b/src/allocators/HybridAllocator.sol index 8ad6135..14d7d9d 100644 --- a/src/allocators/HybridAllocator.sol +++ b/src/allocators/HybridAllocator.sol @@ -17,7 +17,7 @@ contract HybridAllocator is IHybridAllocator { mapping(bytes32 => bool) internal claims; - /// @dev The off chain allocator must use a uint256 nonce where the first 160 bits are the sponsors to ensure no nonce collisions + /// @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 nonce; uint256 public signerCount; mapping(address => bool) public signers; @@ -30,6 +30,9 @@ contract HybridAllocator is IHybridAllocator { } constructor(address compact_, address signer_) { + if (signer_ == address(0)) { + revert InvalidSigner(); + } _COMPACT = ITheCompact(compact_); ALLOCATOR_ID = _COMPACT.__registerAllocator(address(this), ''); _COMPACT_DOMAIN_SEPARATOR = _COMPACT.DOMAIN_SEPARATOR(); @@ -209,7 +212,8 @@ contract HybridAllocator is IHybridAllocator { } // Check if the signer is an authorized allocator address - return signers[ecrecover(digest, v, r, s)]; + address signer = ecrecover(digest, v, r, s); + return signers[signer] && signer != address(0); } function _splitId(uint256 id) internal pure returns (uint96 allocatorId_, address token_) { diff --git a/test/HybridAllocator.t.sol b/test/HybridAllocator.t.sol index ab1a074..0624a1e 100644 --- a/test/HybridAllocator.t.sol +++ b/test/HybridAllocator.t.sol @@ -50,6 +50,11 @@ contract HybridAllocatorTest is Test, TestHelper { batchCompact.expires = defaultExpiration; } + function test_constructor_revert_signerIsAddressZero() public { + vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.InvalidSigner.selector)); + new HybridAllocator(address(compact), address(0)); + } + function test_checkAllocatorId() public view { assertEq(allocator.ALLOCATOR_ID(), _toAllocatorId(address(allocator))); } @@ -396,6 +401,26 @@ contract HybridAllocatorTest is Test, TestHelper { assertFalse(allocator.isClaimAuthorized(falseHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); } + function test_isClaimAuthorized_signerZeroAddress() public { + // Create an arbitrary claim hash that has not been registered. + bytes32 claimHash = keccak256('invalid'); + assertEq(ecrecover(claimHash, 0, bytes32(0), bytes32(0)), address(0)); + + // Craft a 65-byte signature that will make ecrecover return the zero address: + // r = 0, s = 0, v = 0 (v not 27/28). + bytes memory invalidSignature = abi.encodePacked(bytes32(0), bytes32(0), uint8(0)); + + // Forcing address(0) as signer + uint256 signersSlot = 0x03; + vm.store(address(allocator), keccak256(abi.encode(address(0), signersSlot)), bytes32(uint256(1))); + + assertTrue(allocator.signers(address(0))); + + assertFalse( + allocator.isClaimAuthorized(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), invalidSignature) + ); + } + function test_isClaimAuthorized_withSigner_bytes64() public view { uint256[2][] memory idsAndAmounts = new uint256[2][](1); idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); diff --git a/test/HybridERC7683.t.sol b/test/HybridERC7683.t.sol index e22ab60..a9874d4 100644 --- a/test/HybridERC7683.t.sol +++ b/test/HybridERC7683.t.sol @@ -29,8 +29,6 @@ import {IHybridERC7683} from 'src/interfaces/IHybridERC7683.sol'; import {ERC20Mock} from 'src/test/ERC20Mock.sol'; -import {console} from 'forge-std/console.sol'; - abstract contract MocksSetup is Test, TestHelper { address user; uint256 userPK; @@ -462,6 +460,48 @@ contract HybridERC7683_open is OnChainCrossChainOrderData { hybridERC7683Allocator.open(onChainCrossChainOrder_); } + function test_revert_ManipulatedOrderData() public { + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + + // register a claim + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 typeHash = _getTypeHash(); + compactContract.register(claimHash, typeHash); + + vm.stopPrank(); + + (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); + + // Manipulate the order data + uint256 outOfBounds = type(uint256).max; // uint256(type(uint64).max) + 1; + + bytes memory callData = abi.encodeWithSelector(IOriginSettler.open.selector, onChainCrossChainOrder_); + // 0x00 selector + // 0x24 OnchainCrossChainOrder.offset + // 0x44 OnchainCrossChainOrder.fillDeadline + // 0x64 OnchainCrossChainOrder.orderDataType + // 0x84 OnchainCrossChainOrder.orderData.offset + // 0xa4 OnchainCrossChainOrder.orderData.length + // 0xc4 OnchainCrossChainOrder.OrderDataOnChain.offset + // 0xe4 OnchainCrossChainOrder.OrderDataOnChain.Order.offset + + assembly ("memory-safe") { + mstore(add(callData, 0xe4), outOfBounds) + } + + vm.prank(user); + (bool success, bytes memory returnData) = address(hybridERC7683Allocator).call(callData); + assertEq(success, false); + assertEq(returnData.length, 0); + } + function test_orderDataType() public view { assertEq(hybridERC7683Allocator.ORDERDATA_GASLESS_TYPEHASH(), ORDERDATA_GASLESS_TYPEHASH); } @@ -763,8 +803,6 @@ contract HybridERC7683_authorizeClaim is OnChainCrossChainOrderData, GaslessCros ); bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), compactContract.DOMAIN_SEPARATOR(), qualifiedClaimHash)); - console.log('digest'); - console.logBytes32(digest); // Sign with wrong signer bytes memory allocatorSignature = _signMessage(digest, signerPK); @@ -819,8 +857,6 @@ contract HybridERC7683_authorizeClaim is OnChainCrossChainOrderData, GaslessCros ); bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), compactContract.DOMAIN_SEPARATOR(), qualifiedClaimHash)); - console.log('digest'); - console.logBytes32(digest); // Sign with wrong signer bytes memory allocatorSignature = _signMessage(digest, attackerPK); @@ -876,8 +912,6 @@ contract HybridERC7683_authorizeClaim is OnChainCrossChainOrderData, GaslessCros ); bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), compactContract.DOMAIN_SEPARATOR(), qualifiedClaimHash)); - console.log('digest'); - console.logBytes32(digest); // Sign with wrong signer bytes memory allocatorSignature = _signMessage(digest, signerPK); From aabaf1d6076a2a6058c4ff48f60306b8edfbb41a Mon Sep 17 00:00:00 2001 From: mgretzke Date: Fri, 20 Jun 2025 12:58:28 +0200 Subject: [PATCH 21/63] progress to V1 --- src/allocators/ERC7683Allocator.sol | 470 +++++++++++ src/interfaces/IERC7683Allocator.sol | 112 +++ test/ERC7683Allocator.t.sol | 1172 ++++++++++++++++++++++++++ 3 files changed, 1754 insertions(+) create mode 100644 src/allocators/ERC7683Allocator.sol create mode 100644 src/interfaces/IERC7683Allocator.sol create mode 100644 test/ERC7683Allocator.t.sol diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol new file mode 100644 index 0000000..397f9d9 --- /dev/null +++ b/src/allocators/ERC7683Allocator.sol @@ -0,0 +1,470 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import {IERC7683Allocator} from '../interfaces/IERC7683Allocator.sol'; +import {SimpleAllocator} from './SimpleAllocator.sol'; +import {Claim, Mandate} from './types/TribunalStructs.sol'; + +import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; +import {ECDSA} from '@openzeppelin/contracts/utils/cryptography/ECDSA.sol'; +import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; +import {Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; + +contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { + /// @notice The typehash of the OrderData struct + // keccak256("OrderData(address arbiter,address sponsor,uint256 nonce,uint256 id,uint256 amount, + // uint256 chainId,address tribunal,address recipient,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt,uint256 targetBlock,uint256 maximumBlocksAfterTarget)") + bytes32 public constant ORDERDATA_TYPEHASH = 0x9687614112a074c792f7035dc9365f34672a3aa8d3c312500bd47ddcaa0383b5; + + /// @notice The typehash of the OrderDataGasless struct + // keccak256("OrderDataGasless(address arbiter,uint256 id,uint256 amount, + // uint256 chainId,address tribunal,address recipient,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") + bytes32 public constant ORDERDATA_GASLESS_TYPEHASH = + 0xe9b624fa654c7f07ce16d31bf0165a4030d4022f62987afad8ef9d30fc8a0b88; + + /// @notice keccak256("QualifiedClaim(bytes32 claimHash,uint256 targetBlock,uint256 maximumBlocksAfterTarget)") + bytes32 public constant QUALIFICATION_TYPEHASH = 0x59866b84bd1f6c909cf2a31efd20c59e6c902e50f2c196994e5aa85cdc7d7ce0; + + /// @notice keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate) + // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") + bytes32 public constant COMPACT_WITNESS_TYPEHASH = + 0xfd9cda0e5e31a3a3476cb5b57b07e2a4d6a12815506f69c880696448cd9897a5; + + /// @notice keccak256("Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") + bytes32 internal constant MANDATE_TYPEHASH = 0x74d9c10530859952346f3e046aa2981a24bb7524b8394eb45a9deddced9d6501; + + /// @notice uint256(uint8(keccak256("ERC7683Allocator.nonce"))) + uint8 internal constant NONCE_MASTER_SLOT_SEED = 0x39; + + bytes32 immutable _COMPACT_DOMAIN_SEPARATOR; + + constructor(address compactContract_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_) + SimpleAllocator(compactContract_, minWithdrawalDelay_, maxWithdrawalDelay_) + { + _COMPACT_DOMAIN_SEPARATOR = ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(); + } + + /// @inheritdoc IERC7683Allocator + function openFor(GaslessCrossChainOrder calldata order_, bytes calldata sponsorSignature_, bytes calldata) + external + { + // With the users signature, we can create locks in the name of the user + + // Check if orderDataType is the one expected by the allocator + if (order_.orderDataType != ORDERDATA_GASLESS_TYPEHASH) { + revert InvalidOrderDataType(order_.orderDataType, ORDERDATA_GASLESS_TYPEHASH); + } + if (order_.originSettler != address(this)) { + revert InvalidOriginSettler(order_.originSettler, address(this)); + } + + // Decode the orderData + OrderDataGasless memory orderDataGasless = abi.decode(order_.orderData, (OrderDataGasless)); + + OrderData memory orderData = + _convertGaslessOrderData(order_.user, order_.nonce, order_.openDeadline, orderDataGasless); + + _open(orderData, order_.fillDeadline, order_.user, sponsorSignature_); + } + + /// @inheritdoc IERC7683Allocator + function open(OnchainCrossChainOrder calldata order) external { + // Check if orderDataType is the one expected by the allocator + if (order.orderDataType != ORDERDATA_TYPEHASH) { + revert InvalidOrderDataType(order.orderDataType, ORDERDATA_TYPEHASH); + } + + // Decode the orderData + OrderData memory orderData = abi.decode(order.orderData, (OrderData)); + _checkMsgSender(orderData.sponsor); + + _open(orderData, order.fillDeadline, msg.sender, ''); + } + + /// @inheritdoc IERC7683Allocator + function resolveFor(GaslessCrossChainOrder calldata order_, bytes calldata) + external + view + returns (ResolvedCrossChainOrder memory) + { + OrderDataGasless memory orderDataGasless = abi.decode(order_.orderData, (OrderDataGasless)); + + OrderData memory orderData = + _convertGaslessOrderData(order_.user, order_.nonce, order_.openDeadline, orderDataGasless); + return _resolveOrder(order_.user, order_.fillDeadline, orderData, ''); + } + + /// @inheritdoc IERC7683Allocator + function resolve(OnchainCrossChainOrder calldata order_) external view returns (ResolvedCrossChainOrder memory) { + OrderData memory orderData = abi.decode(order_.orderData, (OrderData)); + return _resolveOrder(orderData.sponsor, order_.fillDeadline, orderData, ''); + } + + function registerClaim( + bytes32 claimHash, // The message hash representing the claim. + address, /* caller */ // The account initiating the registration. + address arbiter, // The account tasked with verifying and submitting the claim. + address sponsor, // The account to source the tokens from. + uint256 nonce, // A parameter to enforce replay protection, scoped to allocator. + uint256 expires, // The time at which the claim expires. + uint256[2][] calldata idsAndAmounts, // The allocated token IDs and amounts. + bytes calldata allocatorData // Arbitrary data provided by the caller. + ) external override onlyCompact returns (bytes4) { + if (idsAndAmounts.length > 1) { + revert BatchCompactsNotSupported(); + } + + // Enforce a nonce where the most significant 96 bits are the nonce and the least significant 160 bits are the sponsor + uint96 nonceWithoutAddress = _nonceValidation(sponsor, nonce); + // Set a nonce or revert if it is already used + _setNonce(sponsor, nonceWithoutAddress); + + // We trust the compact to check the nonce and that this contract is the allocator connected to the id + _checkExpiration(expires); + bytes32 tokenHash = _checkForActiveAllocation(sponsor, idsAndAmounts[0][0]); + _checkForcedWithdrawal(sponsor, expires, idsAndAmounts[0][0]); + _checkBalance(sponsor, idsAndAmounts[0][0], idsAndAmounts[0][1]); // TODO: Should the Compact check this prior to the callback? + + OderDataCallback memory orderDataCallback = abi.decode(allocatorData, (OderDataCallback)); + bytes32 qualifiedClaimHash = keccak256( + abi.encode( + QUALIFICATION_TYPEHASH, + claimHash, + orderDataCallback.targetBlock, + orderDataCallback.maximumBlocksAfterTarget + ) + ); + + _lockTokens(tokenHash, idsAndAmounts[0][1], orderDataCallback.expires, qualifiedClaimHash); + + OrderData memory orderData = OrderData({ + arbiter: arbiter, + sponsor: sponsor, + nonce: nonce, + expires: expires, + id: idsAndAmounts[0][0], + amount: idsAndAmounts[0][1], + chainId: orderDataCallback.chainId, + tribunal: orderDataCallback.tribunal, + recipient: orderDataCallback.recipient, + token: orderDataCallback.token, + minimumAmount: orderDataCallback.minimumAmount, + baselinePriorityFee: orderDataCallback.baselinePriorityFee, + scalingFactor: orderDataCallback.scalingFactor, + decayCurve: orderDataCallback.decayCurve, + salt: orderDataCallback.salt, + targetBlock: orderDataCallback.targetBlock, + maximumBlocksAfterTarget: orderDataCallback.maximumBlocksAfterTarget + }); + // Emit an open event + emit Open(bytes32(nonce), _resolveOrder(sponsor, uint32(orderDataCallback.expires), orderData, '')); + + return this.registerClaim.selector; + } + + function authorizeClaim( + bytes32 claimHash, // The message hash representing the claim. + address, /* arbiter */ // The account tasked with verifying and submitting the claim. + address sponsor, // The account to source the tokens from. + uint256, /* nonce */ // A parameter to enforce replay protection, scoped to allocator. + uint256, /* expires */ // The time at which the claim expires. + uint256[2][] calldata idsAndAmounts, // The allocated token IDs and amounts. + bytes calldata allocatorData // Arbitrary data provided by the arbiter. + ) external override onlyCompact returns (bytes4) { + uint256 length = idsAndAmounts.length; + if (length > 1) { + revert BatchCompactsNotSupported(); + } + (uint256 targetBlock, uint256 maximumBlocksAfterTarget) = abi.decode(allocatorData, (uint256, uint256)); + claimHash = keccak256(abi.encode(QUALIFICATION_TYPEHASH, claimHash, targetBlock, maximumBlocksAfterTarget)); + + if (!_claim[claimHash]) { + revert InvalidLock(claimHash, 0); + } + delete _claim[claimHash]; + + // Delete all allocations connected to the claim + for (uint256 i = 0; i < length; ++i) { + bytes32 tokenHash = _getTokenHash(idsAndAmounts[i][0], sponsor); + delete _allocation[tokenHash]; + } + + // We expect the Compact to verify the expiration date is still valid and the nonce has not yet been consumed + + return this.authorizeClaim.selector; + } + + function allocatorDataSpecification() + external + pure + override + returns ( + uint256 specificationId, // An identifier indicating a required "standard" for allocatorData. + string memory claimEncoding, // The encoding of the `allocatorData` payload on claim processing. + string memory registrationEncoding, // The encoding of the `allocatorData` payload on claim registration. + bytes memory context // Any additional context as defined by the specificationId. + ) + { + specificationId = 0; + claimEncoding = ''; + registrationEncoding = ''; + context = ''; + } + + /// @inheritdoc IERC7683Allocator + function getCompactWitnessTypeString() external pure returns (string memory) { + return + 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt))'; + } + + /// @inheritdoc IERC7683Allocator + function checkNonce(address sponsor_, uint256 nonce_) external view returns (bool nonceFree_) { + uint96 nonceWithoutAddress = _nonceValidation(sponsor_, nonce_); + uint96 wordPos = uint96(nonceWithoutAddress / 256); + uint96 bitPos = uint96(nonceWithoutAddress % 256); + assembly ("memory-safe") { + let masterSlot := or(shl(248, NONCE_MASTER_SLOT_SEED), or(shl(88, sponsor_), wordPos)) + nonceFree_ := iszero(and(sload(masterSlot), shl(bitPos, 1))) + } + return nonceFree_; + } + + /// @inheritdoc IERC7683Allocator + function createFillerData(address claimant_) external pure returns (bytes memory fillerData) { + fillerData = abi.encode(claimant_); + return fillerData; + } + + function _open(OrderData memory orderData_, uint32 fillDeadline_, address sponsor_, bytes memory sponsorSignature_) + internal + { + // Enforce a nonce where the most significant 96 bits are the nonce and the least significant 160 bits are the sponsor + uint96 nonceWithoutAddress = _nonceValidation(sponsor_, orderData_.nonce); + // Set a nonce or revert if it is already used + _setNonce(sponsor_, nonceWithoutAddress); + + // We do not enforce a specific tribunal or arbiter. This will allow to support new arbiters and tribunals after the deployment of the allocator + // Going with an immutable arbiter and tribunal would limit support for new chains with a fully decentralized allocator + + bytes32 tokenHash = + _verifyAllocation(sponsor_, orderData_.id, orderData_.amount, orderData_.expires, orderData_.nonce); + + // Create the Compact claim hash + bytes32 claimHash = keccak256( + abi.encode( + COMPACT_WITNESS_TYPEHASH, + orderData_.arbiter, + sponsor_, + orderData_.nonce, + orderData_.expires, + orderData_.id, + orderData_.amount, + keccak256( + abi.encode( + MANDATE_TYPEHASH, + orderData_.chainId, + orderData_.tribunal, + orderData_.recipient, + fillDeadline_, + orderData_.token, + orderData_.minimumAmount, + orderData_.baselinePriorityFee, + orderData_.scalingFactor, + keccak256(abi.encodePacked(orderData_.decayCurve)), + orderData_.salt + ) + ) + ) + ); + + // We check for the length, which means this could also be triggered by a zero length signature provided in the openFor function. This enables relaying of orders if the claim was registered on the compact. + if (sponsorSignature_.length > 0) { + bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), _COMPACT_DOMAIN_SEPARATOR, claimHash)); + // confirm the signature matches the digest + address signer = ECDSA.recover(digest, sponsorSignature_); + if (sponsor_ != signer) { + revert InvalidSignature(sponsor_, signer); + } + } else { + // confirm the claim hash is registered on the compact + (bool isActive, uint256 registrationExpiration) = + ITheCompact(COMPACT_CONTRACT).getRegistrationStatus(sponsor_, claimHash, COMPACT_WITNESS_TYPEHASH); + if (!isActive || registrationExpiration < orderData_.expires) { + revert InvalidRegistration(sponsor_, claimHash); + } + } + + bytes32 qualifiedClaimHash = keccak256( + abi.encode(QUALIFICATION_TYPEHASH, claimHash, orderData_.targetBlock, orderData_.maximumBlocksAfterTarget) + ); + + _lockTokens(tokenHash, orderData_.amount, orderData_.expires, qualifiedClaimHash); + + // Emit an open event + emit Open(bytes32(orderData_.nonce), _resolveOrder(sponsor_, fillDeadline_, orderData_, sponsorSignature_)); + } + + function _verifyAllocation(address sponsor, uint256 id, uint256 amount, uint256 expires, uint256 nonce) + internal + view + returns (bytes32 tokenHash_) + { + // Check for a valid allocation + tokenHash_ = _checkForActiveAllocation(sponsor, id); + _checkAllocator(id); + _checkExpiration(expires); + _checkNonce(nonce); + _checkForcedWithdrawal(sponsor, expires, id); + _checkBalance(sponsor, id, amount); + + return tokenHash_; + } + + function _lockTokens(bytes32 tokenHash, uint256 amount, uint256 expires, bytes32 claimHash) internal { + // Lock the tokens + _claim[claimHash] = true; + _allocation[tokenHash] = _allocationData(amount, expires); + } + + function _resolveOrder( + address sponsor, + uint32 fillDeadline, + OrderData memory orderData, + bytes memory sponsorSignature + ) internal view returns (ResolvedCrossChainOrder memory) { + FillInstruction[] memory fillInstructions = new FillInstruction[](1); + + Mandate memory mandate = Mandate({ + recipient: orderData.recipient, + expires: fillDeadline, + token: orderData.token, + minimumAmount: orderData.minimumAmount, + baselinePriorityFee: orderData.baselinePriorityFee, + scalingFactor: orderData.scalingFactor, + decayCurve: orderData.decayCurve, + salt: orderData.salt + }); + Claim memory claim = Claim({ + chainId: block.chainid, + compact: Compact({ + arbiter: orderData.arbiter, + sponsor: sponsor, + nonce: orderData.nonce, + expires: orderData.expires, + id: orderData.id, + amount: orderData.amount + }), + sponsorSignature: sponsorSignature, + allocatorSignature: '' // No signature required from this allocator, it will verify the claim on chain via ERC1271. + }); + + fillInstructions[0] = FillInstruction({ + destinationChainId: orderData.chainId, + destinationSettler: _addressToBytes32(orderData.tribunal), + originData: abi.encode(claim, mandate, orderData.targetBlock, orderData.maximumBlocksAfterTarget) + }); + + Output memory spent = Output({ + token: _addressToBytes32(orderData.token), + amount: type(uint256).max, + recipient: _addressToBytes32(orderData.recipient), + chainId: orderData.chainId + }); + Output memory received = Output({ + token: _addressToBytes32(_idToToken(orderData.id)), + amount: orderData.amount, + recipient: bytes32(0), + chainId: block.chainid + }); + + Output[] memory maxSpent = new Output[](1); + maxSpent[0] = spent; + Output[] memory minReceived = new Output[](1); + minReceived[0] = received; + + ResolvedCrossChainOrder memory resolvedOrder = ResolvedCrossChainOrder({ + user: sponsor, + originChainId: block.chainid, + openDeadline: uint32(orderData.expires), + fillDeadline: fillDeadline, + orderId: bytes32(orderData.nonce), + maxSpent: maxSpent, + minReceived: minReceived, + fillInstructions: fillInstructions + }); + return resolvedOrder; + } + + function _nonceValidation(address sponsor_, uint256 nonce_) internal pure returns (uint96 nonce) { + // Enforce a nonce where the least significant 96 bits are the nonce and the most significant 160 bits are the sponsors address + // This ensures that the nonce is unique for a given sponsor + address expectedSponsor; + assembly ("memory-safe") { + expectedSponsor := shr(96, nonce_) + nonce := shr(160, shl(160, nonce_)) + } + if (expectedSponsor != sponsor_) { + revert InvalidNonce(nonce_); + } + } + + function _setNonce(address sponsor_, uint96 nonce_) internal { + bool used; + uint96 wordPos = nonce_ / 256; // uint96 divided by 256 means it becomes a uint88 (11 bytes) + uint96 bitPos = nonce_ % 256; + assembly ("memory-safe") { + // [NONCE_MASTER_SLOT_SEED - 1 byte][sponsor address - 20 bytes][wordPos - 11 bytes] + let masterSlot := or(shl(248, NONCE_MASTER_SLOT_SEED), or(shl(88, sponsor_), wordPos)) + let previouslyUsedNonces := sload(masterSlot) + if and(previouslyUsedNonces, shl(bitPos, 1)) { used := 1 } + { + let usedNonces := or(previouslyUsedNonces, shl(bitPos, 1)) + sstore(masterSlot, usedNonces) + } + } + if (used) { + revert NonceAlreadyInUse(uint256(bytes32(abi.encodePacked(sponsor_, nonce_)))); + } + } + + function _idToToken(uint256 id_) internal pure returns (address token_) { + assembly ("memory-safe") { + token_ := shr(96, shl(96, id_)) + } + } + + function _addressToBytes32(address address_) internal pure returns (bytes32 output_) { + assembly ("memory-safe") { + output_ := shr(96, shl(96, address_)) + } + } + + function _convertGaslessOrderData( + address sponsor_, + uint256 nonce_, + uint32 openDeadline_, + OrderDataGasless memory orderDataGasless_ + ) internal pure returns (OrderData memory orderData_) { + orderData_ = OrderData({ + arbiter: orderDataGasless_.arbiter, + sponsor: sponsor_, + nonce: nonce_, + expires: openDeadline_, + id: orderDataGasless_.id, + amount: orderDataGasless_.amount, + chainId: orderDataGasless_.chainId, + tribunal: orderDataGasless_.tribunal, + recipient: orderDataGasless_.recipient, + token: orderDataGasless_.token, + minimumAmount: orderDataGasless_.minimumAmount, + baselinePriorityFee: orderDataGasless_.baselinePriorityFee, + scalingFactor: orderDataGasless_.scalingFactor, + decayCurve: orderDataGasless_.decayCurve, + salt: orderDataGasless_.salt, + targetBlock: 0, + maximumBlocksAfterTarget: 0 + }); + return orderData_; + } +} diff --git a/src/interfaces/IERC7683Allocator.sol b/src/interfaces/IERC7683Allocator.sol new file mode 100644 index 0000000..9a3369f --- /dev/null +++ b/src/interfaces/IERC7683Allocator.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import {IOriginSettler} from './ERC7683/IOriginSettler.sol'; + +interface IERC7683Allocator is IOriginSettler { + struct OrderData { + // COMPACT + address arbiter; // The account tasked with verifying and submitting the claim. + address sponsor; // The account to source the tokens from. + uint256 nonce; // A parameter to enforce replay protection, scoped to allocator. + uint256 expires; // The time at which the claim expires. + uint256 id; // The token ID of the ERC6909 token to allocate. + uint256 amount; // The amount of ERC6909 tokens to allocate. + // MANDATE + uint256 chainId; // (implicit arg, included in EIP712 payload) + address tribunal; // (implicit arg, included in EIP712 payload) + address recipient; // Recipient of settled tokens + // uint256 expires; // Mandate expiration timestamp + address token; // Settlement token (address(0) for native) + uint256 minimumAmount; // Minimum settlement amount + uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in + uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline) + uint256[] decayCurve; // Block durations, fill increases, & claim decreases. + bytes32 salt; // Replay protection parameter + // ADDITIONAL INPUT + uint256 targetBlock; // The block number at the target chain on which the PGA is executed / the reverse dutch auction starts. + uint256 maximumBlocksAfterTarget; // Blocks after target block that are still fillable. + } + + struct OrderDataGasless { + // COMPACT + address arbiter; // The account tasked with verifying and submitting the claim. + // address sponsor; // The account to source the tokens from. + // uint256 nonce; // A parameter to enforce replay protection, scoped to allocator. + // uint256 expires; // The time at which the claim expires. + uint256 id; // The token ID of the ERC6909 token to allocate. + uint256 amount; // The amount of ERC6909 tokens to allocate. + // MANDATE + uint256 chainId; // (implicit arg, included in EIP712 payload) + address tribunal; // (implicit arg, included in EIP712 payload) + address recipient; // Recipient of settled tokens + // uint256 expires; // Mandate expiration timestamp + address token; // Settlement token (address(0) for native) + uint256 minimumAmount; // Minimum settlement amount + uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in + uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline) + uint256[] decayCurve; // Block durations, fill increases, & claim decreases. + bytes32 salt; // Replay protection parameter + } + + struct OderDataCallback { + // COMPACT + // address arbiter; // The account tasked with verifying and submitting the claim. + // address sponsor; // The account to source the tokens from. + // uint256 nonce; // A parameter to enforce replay protection, scoped to allocator. + // uint256 expires; // The time at which the claim expires. + // uint256 id; // The token ID of the ERC6909 token to allocate. + // uint256 amount; // The amount of ERC6909 tokens to allocate. + // MANDATE + uint256 chainId; // (implicit arg, included in EIP712 payload) + address tribunal; // (implicit arg, included in EIP712 payload) + address recipient; // Recipient of settled tokens + uint256 expires; // Mandate expiration timestamp + address token; // Settlement token (address(0) for native) + uint256 minimumAmount; // Minimum settlement amount + uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in + uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline) + uint256[] decayCurve; // Block durations, fill increases, & claim decreases. + bytes32 salt; // Replay protection parameter + // ADDITIONAL INPUT + uint256 targetBlock; // The block number at the target chain on which the PGA is executed / the reverse dutch auction starts. + uint256 maximumBlocksAfterTarget; // Blocks after target block that are still fillable. + } + + error InvalidOriginSettler(address originSettler, address expectedOriginSettler); + error InvalidOrderDataType(bytes32 orderDataType, bytes32 expectedOrderDataType); + error InvalidNonce(uint256 nonce); + error NonceAlreadyInUse(uint256 nonce); + error InvalidSignature(address signer, address expectedSigner); + error InvalidRegistration(address sponsor, bytes32 claimHash); + error BatchCompactsNotSupported(); + + /// @inheritdoc IOriginSettler + function openFor(GaslessCrossChainOrder calldata order, bytes calldata signature, bytes calldata originFillerData) + external; + + /// @inheritdoc IOriginSettler + /// @dev Requires the user to have previously registered the claim hash on the compact + function open(OnchainCrossChainOrder calldata order) external; + + /// @inheritdoc IOriginSettler + function resolveFor(GaslessCrossChainOrder calldata order, bytes calldata originFillerData) + external + view + returns (ResolvedCrossChainOrder memory); + + /// @inheritdoc IOriginSettler + function resolve(OnchainCrossChainOrder calldata order) external view returns (ResolvedCrossChainOrder memory); + + /// @notice Returns the type string of the compact including the witness + function getCompactWitnessTypeString() external pure returns (string memory); + + /// @notice Checks if a nonce is free to be used + /// @dev The nonce is the most significant 96 bits. The least significant 160 bits must be the sponsor address + function checkNonce(address sponsor_, uint256 nonce_) external view returns (bool nonceFree_); + + /// @notice Creates the filler data for the open event to be used on the IDestinationSettler + /// @param claimant_ The address claiming the origin tokens after a successful fill (typically the address of the filler) + function createFillerData(address claimant_) external pure returns (bytes memory fillerData); +} diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol new file mode 100644 index 0000000..2c259b6 --- /dev/null +++ b/test/ERC7683Allocator.t.sol @@ -0,0 +1,1172 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; +import {ERC6909} from '@solady/tokens/ERC6909.sol'; +import {TheCompact} from '@uniswap/the-compact/TheCompact.sol'; +import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; + +import {IdLib} from '@uniswap/the-compact/lib/IdLib.sol'; +import {COMPACT_TYPEHASH, Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; +import {ForcedWithdrawalStatus} from '@uniswap/the-compact/types/ForcedWithdrawalStatus.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 {console} from 'forge-std/console.sol'; +import {ERC7683Allocator} from 'src/allocators/ERC7683Allocator.sol'; +import {Claim, Mandate} from 'src/allocators/types/TribunalStructs.sol'; +import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; +import {IERC7683Allocator} from 'src/interfaces/IERC7683Allocator.sol'; + +import {ISimpleAllocator} from 'src/interfaces/ISimpleAllocator.sol'; +import {ERC20Mock} from 'src/test/ERC20Mock.sol'; +import {TheCompactMock} from 'src/test/TheCompactMock.sol'; + +abstract contract MocksSetup is Test { + address user; + uint256 userPK; + address attacker; + uint256 attackerPK; + address arbiter; + address tribunal; + ERC20Mock usdc; + TheCompact compactContract; + ERC7683Allocator erc7683Allocator; + uint256 usdcId; + + ResetPeriod defaultResetPeriod = ResetPeriod.OneMinute; + Scope defaultScope = Scope.Multichain; + uint256 defaultResetPeriodTimestamp = 60; + uint256 defaultAmount = 1000; + uint256 defaultNonce; + uint256 defaultOutputChainId = 130; + address defaultOutputToken = makeAddr('outputToken'); + uint256 defaultMinimumAmount = 1000; + uint256 defaultBaselinePriorityFee = 0; + uint256 defaultScalingFactor = 0; + uint256[] defaultDecayCurve = new uint256[](0); + bytes32 defaultSalt = bytes32(0); + uint256 defaultTargetBlock = 100; + uint256 defaultMaximumBlocksAfterTarget = 10; + + bytes32 ORDERDATA_GASLESS_TYPEHASH; + bytes32 ORDERDATA_TYPEHASH; + + function setUp() public virtual { + (user, userPK) = makeAddrAndKey('user'); + arbiter = makeAddr('arbiter'); + tribunal = makeAddr('tribunal'); + usdc = new ERC20Mock('USDC', 'USDC'); + compactContract = new TheCompact(); + erc7683Allocator = new ERC7683Allocator(address(compactContract), 5, 100); + Lock memory lock = Lock({ + token: address(usdc), + allocator: address(erc7683Allocator), + resetPeriod: defaultResetPeriod, + scope: defaultScope + }); + usdcId = IdLib.toId(lock); + (attacker, attackerPK) = makeAddrAndKey('attacker'); + defaultNonce = uint256(bytes32(abi.encodePacked(user, uint96(1)))); + + ORDERDATA_GASLESS_TYPEHASH = erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH(); + ORDERDATA_TYPEHASH = erc7683Allocator.ORDERDATA_TYPEHASH(); + } +} + +abstract contract CreateHash is MocksSetup { + struct Allocator { + bytes32 hash; + } + + // stringified types + string EIP712_DOMAIN_TYPE = 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'; // Hashed inside the function + // EIP712 domain type + string name = 'The Compact'; + string version = '0'; + + string compactWitnessTypeString = + 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; + string mandateTypeString = + 'Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; + string witnessTypeString = + 'Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; + + function _hashCompact(Compact memory data, Mandate memory mandate, address verifyingContract) + internal + view + returns (bytes32) + { + bytes32 compactHash = _hashCompact(data, mandate); + // hash typed data + return keccak256( + abi.encodePacked( + '\x19\x01', // backslash is needed to escape the character + _domainSeparator(verifyingContract), + compactHash + ) + ); + } + + function _hashCompact(Compact memory data, Mandate memory mandate) internal view returns (bytes32 compactHash) { + return keccak256( + abi.encode( + keccak256(bytes(compactWitnessTypeString)), + data.arbiter, + data.sponsor, + data.nonce, + data.expires, + data.id, + data.amount, + keccak256( + abi.encode( + keccak256(bytes(mandateTypeString)), + defaultOutputChainId, + tribunal, + mandate.recipient, + mandate.expires, + mandate.token, + mandate.minimumAmount, + mandate.baselinePriorityFee, + mandate.scalingFactor, + keccak256(abi.encodePacked(mandate.decayCurve)), + mandate.salt + ) + ) + ) + ); + } + + function _getTypeHash() internal view returns (bytes32) { + return keccak256(bytes(compactWitnessTypeString)); + } + + function _domainSeparator(address verifyingContract) internal view returns (bytes32) { + return keccak256( + abi.encode( + keccak256(bytes(EIP712_DOMAIN_TYPE)), + keccak256(bytes(name)), + keccak256(bytes(version)), + block.chainid, + verifyingContract + ) + ); + } + + function _signMessage(bytes32 hash_, uint256 signerPK_) internal pure returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPK_, hash_); + return abi.encodePacked(r, s, v); + } + + function _hashAndSign(Compact memory data, Mandate memory mandate, address verifyingContract, uint256 signerPK) + internal + view + returns (bytes memory) + { + bytes32 hash = _hashCompact(data, mandate, verifyingContract); + bytes memory signature = _signMessage(hash, signerPK); + return signature; + } +} + +abstract contract CompactData is CreateHash { + Compact private compact; + Mandate private mandate; + + function setUp() public virtual override { + super.setUp(); + + compact = Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + expires: _getClaimExpiration(), + id: usdcId, + amount: defaultAmount + }); + + mandate = Mandate({ + recipient: user, + expires: _getFillExpiration(), + token: defaultOutputToken, + minimumAmount: defaultMinimumAmount, + baselinePriorityFee: defaultBaselinePriorityFee, + scalingFactor: defaultScalingFactor, + decayCurve: defaultDecayCurve, + salt: defaultSalt + }); + } + + function _getCompact() internal returns (Compact memory) { + compact.expires = _getClaimExpiration(); + return compact; + } + + function _getMandate() internal returns (Mandate memory) { + mandate.expires = _getFillExpiration(); + return mandate; + } + + function _getFillExpiration() internal view returns (uint256) { + return vm.getBlockTimestamp() + defaultResetPeriodTimestamp - 1; + } + + function _getClaimExpiration() internal view returns (uint256) { + return vm.getBlockTimestamp() + defaultResetPeriodTimestamp; + } +} + +abstract contract GaslessCrossChainOrderData is CompactData { + IOriginSettler.GaslessCrossChainOrder private gaslessCrossChainOrder; + + function setUp() public virtual override { + super.setUp(); + + Compact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + gaslessCrossChainOrder = IOriginSettler.GaslessCrossChainOrder({ + originSettler: address(erc7683Allocator), + user: compact_.sponsor, + nonce: compact_.nonce, + originChainId: block.chainid, + openDeadline: uint32(_getClaimExpiration()), + fillDeadline: uint32(_getFillExpiration()), + orderDataType: erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH(), + orderData: abi.encode( + IERC7683Allocator.OrderDataGasless({ + arbiter: compact_.arbiter, + id: compact_.id, + amount: compact_.amount, + chainId: defaultOutputChainId, + tribunal: tribunal, + recipient: mandate_.recipient, + token: mandate_.token, + minimumAmount: mandate_.minimumAmount, + baselinePriorityFee: mandate_.baselinePriorityFee, + scalingFactor: mandate_.scalingFactor, + decayCurve: mandate_.decayCurve, + salt: mandate_.salt + }) + ) + }); + } + + function _getGaslessCrossChainOrder( + address allocator, + Compact memory compact_, + Mandate memory mandate_, + uint256 chainId_, + bytes32 orderDataGaslessTypeHash_, + address verifyingContract, + uint256 signerPK + ) internal view returns (IOriginSettler.GaslessCrossChainOrder memory, bytes memory signature) { + IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_ = IOriginSettler.GaslessCrossChainOrder({ + originSettler: allocator, + user: compact_.sponsor, + nonce: compact_.nonce, + originChainId: chainId_, + openDeadline: uint32(compact_.expires), + fillDeadline: uint32(mandate_.expires), + orderDataType: orderDataGaslessTypeHash_, + orderData: abi.encode( + IERC7683Allocator.OrderDataGasless({ + arbiter: compact_.arbiter, + id: compact_.id, + amount: compact_.amount, + chainId: defaultOutputChainId, + tribunal: tribunal, + recipient: mandate_.recipient, + token: mandate_.token, + minimumAmount: mandate_.minimumAmount, + baselinePriorityFee: mandate_.baselinePriorityFee, + scalingFactor: mandate_.scalingFactor, + decayCurve: mandate_.decayCurve, + salt: mandate_.salt + }) + ) + }); + + (bytes memory signature_) = _hashAndSign(compact_, mandate_, verifyingContract, signerPK); + return (gaslessCrossChainOrder_, signature_); + } + + function _getGaslessCrossChainOrder() + internal + returns (IOriginSettler.GaslessCrossChainOrder memory, bytes memory signature) + { + (bytes memory signature_) = _hashAndSign(_getCompact(), _getMandate(), address(compactContract), userPK); + return (gaslessCrossChainOrder, signature_); + } +} + +abstract contract OnChainCrossChainOrderData is CompactData { + IOriginSettler.OnchainCrossChainOrder private onchainCrossChainOrder; + + function setUp() public virtual override { + super.setUp(); + + Compact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + onchainCrossChainOrder = IOriginSettler.OnchainCrossChainOrder({ + fillDeadline: uint32(_getFillExpiration()), + orderDataType: erc7683Allocator.ORDERDATA_TYPEHASH(), + orderData: abi.encode( + IERC7683Allocator.OrderData({ + arbiter: compact_.arbiter, + sponsor: compact_.sponsor, + nonce: compact_.nonce, + expires: compact_.expires, + id: compact_.id, + amount: compact_.amount, + chainId: defaultOutputChainId, + tribunal: tribunal, + recipient: mandate_.recipient, + token: mandate_.token, + minimumAmount: mandate_.minimumAmount, + baselinePriorityFee: mandate_.baselinePriorityFee, + scalingFactor: mandate_.scalingFactor, + decayCurve: mandate_.decayCurve, + salt: mandate_.salt, + targetBlock: defaultTargetBlock, + maximumBlocksAfterTarget: defaultMaximumBlocksAfterTarget + }) + ) + }); + } + + function _getOnChainCrossChainOrder() internal view returns (IOriginSettler.OnchainCrossChainOrder memory) { + return onchainCrossChainOrder; + } + + function _getOnChainCrossChainOrder(Compact memory compact_, Mandate memory mandate_, bytes32 orderDataType_) + internal + view + returns (IOriginSettler.OnchainCrossChainOrder memory) + { + IOriginSettler.OnchainCrossChainOrder memory onchainCrossChainOrder_ = IOriginSettler.OnchainCrossChainOrder({ + fillDeadline: uint32(mandate_.expires), + orderDataType: orderDataType_, + orderData: abi.encode( + IERC7683Allocator.OrderData({ + arbiter: compact_.arbiter, + sponsor: compact_.sponsor, + nonce: compact_.nonce, + expires: compact_.expires, + id: compact_.id, + amount: compact_.amount, + chainId: defaultOutputChainId, + tribunal: tribunal, + recipient: mandate_.recipient, + token: mandate_.token, + minimumAmount: mandate_.minimumAmount, + baselinePriorityFee: mandate_.baselinePriorityFee, + scalingFactor: mandate_.scalingFactor, + decayCurve: mandate_.decayCurve, + salt: mandate_.salt, + targetBlock: defaultTargetBlock, + maximumBlocksAfterTarget: defaultMaximumBlocksAfterTarget + }) + ) + }); + return onchainCrossChainOrder_; + } +} + +abstract contract Deposited is MocksSetup { + function setUp() public virtual override { + super.setUp(); + + vm.startPrank(user); + + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit( + address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user + ); + + vm.stopPrank(); + } +} + +contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { + function test_revert_InvalidOrderDataType() public { + // Order data type is invalid + bytes32 falseOrderDataType = keccak256('false'); + + vm.prank(user); + vm.expectRevert( + abi.encodeWithSelector( + IERC7683Allocator.InvalidOrderDataType.selector, + falseOrderDataType, + erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH() + ) + ); + (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = + _getGaslessCrossChainOrder(); + falseGaslessCrossChainOrder.orderDataType = falseOrderDataType; + erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); + } + + function test_revert_InvalidDecoding() public { + // Decoding fails because of additional data + vm.prank(user); + vm.expectRevert(); + (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = + _getGaslessCrossChainOrder(); + falseGaslessCrossChainOrder.orderData = abi.encode(falseGaslessCrossChainOrder.orderData, uint8(1)); + erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); + } + + function test_revert_InvalidOriginSettler() public { + // Origin settler is not the allocator + address falseOriginSettler = makeAddr('falseOriginSettler'); + vm.expectRevert( + abi.encodeWithSelector( + IERC7683Allocator.InvalidOriginSettler.selector, falseOriginSettler, address(erc7683Allocator) + ) + ); + (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = + _getGaslessCrossChainOrder( + falseOriginSettler, + _getCompact(), + _getMandate(), + block.chainid, + ORDERDATA_GASLESS_TYPEHASH, + address(erc7683Allocator), + userPK + ); + vm.prank(user); + erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); + } + + function test_revert_InvalidNonce() public { + // Nonce is invalid because the least significant 160 bits are not the sponsor address + Compact memory compact_ = _getCompact(); + compact_.nonce = uint256(bytes32(abi.encodePacked(uint96(1), attacker))); + vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidNonce.selector, compact_.nonce)); + (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = + _getGaslessCrossChainOrder( + address(erc7683Allocator), + compact_, + _getMandate(), + block.chainid, + ORDERDATA_GASLESS_TYPEHASH, + address(erc7683Allocator), + userPK + ); + vm.prank(user); + erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); + } + + function test_revert_InvalidSponsorSignature() public { + // Sponsor signature is invalid + + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit( + address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user + ); + vm.stopPrank(); + + // Create a malicious signature + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = + _getGaslessCrossChainOrder( + address(erc7683Allocator), + _getCompact(), + _getMandate(), + block.chainid, + ORDERDATA_GASLESS_TYPEHASH, + address(compactContract), + attackerPK + ); + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidSignature.selector, user, attacker)); + erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); + } + + function test_successful_userHimself() public { + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit( + address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user + ); + vm.stopPrank(); + + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = + _getGaslessCrossChainOrder(); + IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); + IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); + IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); + maxSpent[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(defaultOutputToken))), + amount: type(uint256).max, + recipient: bytes32(uint256(uint160(user))), + chainId: defaultOutputChainId + }); + minReceived[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(address(usdc)))), + amount: defaultAmount, + recipient: '', + chainId: block.chainid + }); + Claim memory claim = Claim({ + chainId: block.chainid, + compact: _getCompact(), + sponsorSignature: sponsorSignature, + allocatorSignature: '' + }); + fillInstructions[0] = IOriginSettler.FillInstruction({ + destinationChainId: defaultOutputChainId, + destinationSettler: bytes32(uint256(uint160(tribunal))), + originData: abi.encode(claim, _getMandate(), uint256(0), uint256(0)) + }); + + IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ + user: user, + originChainId: block.chainid, + openDeadline: uint32(_getClaimExpiration()), + fillDeadline: uint32(_getFillExpiration()), + orderId: bytes32(defaultNonce), + maxSpent: maxSpent, + minReceived: minReceived, + fillInstructions: fillInstructions + }); + vm.prank(user); + vm.expectEmit(true, false, false, true, address(erc7683Allocator)); + emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); + erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); + } + + function test_successful_relayed() public { + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit( + address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user + ); + vm.stopPrank(); + + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = + _getGaslessCrossChainOrder(); + IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); + IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); + IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); + maxSpent[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(defaultOutputToken))), + amount: type(uint256).max, + recipient: bytes32(uint256(uint160(user))), + chainId: defaultOutputChainId + }); + minReceived[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(address(usdc)))), + amount: defaultAmount, + recipient: '', + chainId: block.chainid + }); + Claim memory claim = Claim({ + chainId: block.chainid, + compact: _getCompact(), + sponsorSignature: sponsorSignature, + allocatorSignature: '' + }); + fillInstructions[0] = IOriginSettler.FillInstruction({ + destinationChainId: defaultOutputChainId, + destinationSettler: bytes32(uint256(uint160(tribunal))), + originData: abi.encode(claim, _getMandate(), uint256(0), uint256(0)) + }); + + IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ + user: user, + originChainId: block.chainid, + openDeadline: uint32(_getClaimExpiration()), + fillDeadline: uint32(_getFillExpiration()), + orderId: bytes32(defaultNonce), + maxSpent: maxSpent, + minReceived: minReceived, + fillInstructions: fillInstructions + }); + vm.prank(makeAddr('filler')); + vm.expectEmit(true, false, false, true, address(erc7683Allocator)); + emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); + erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); + } + + function test_revert_NonceAlreadyInUse() public { + // Nonce is already used + + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit( + address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user + ); + vm.stopPrank(); + + // use the nonce once + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = + _getGaslessCrossChainOrder(); + vm.prank(user); + erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); + + // try to use the nonce again + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder2, bytes memory sponsorSignature2) = + _getGaslessCrossChainOrder(); + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.NonceAlreadyInUse.selector, defaultNonce)); + erc7683Allocator.openFor(gaslessCrossChainOrder2, sponsorSignature2, ''); + } +} + +contract ERC7683Allocator_open is OnChainCrossChainOrderData { + function test_revert_InvalidOrderDataType() public { + // Order data type is invalid + bytes32 falseOrderDataType = keccak256('false'); + IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_ = _getOnChainCrossChainOrder(); + onChainCrossChainOrder_.orderDataType = falseOrderDataType; + + vm.prank(user); + vm.expectRevert( + abi.encodeWithSelector( + IERC7683Allocator.InvalidOrderDataType.selector, + falseOrderDataType, + erc7683Allocator.ORDERDATA_TYPEHASH() + ) + ); + erc7683Allocator.open(onChainCrossChainOrder_); + } + + function test_revert_InvalidSponsor() public { + IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_ = _getOnChainCrossChainOrder(); + + vm.prank(attacker); + vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidSponsor.selector, user, attacker)); + erc7683Allocator.open(onChainCrossChainOrder_); + } + + function test_revert_InvalidRegistration_Unavailable() public { + // we deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit( + address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user + ); + + // we do NOT register a claim + + vm.stopPrank(); + + (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); + + Compact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + bytes32 claimHash = _hashCompact(compact_, mandate_); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidRegistration.selector, user, claimHash)); + erc7683Allocator.open(onChainCrossChainOrder_); + } + + function test_revert_InvalidRegistration_Expired() public { + // we deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit( + address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user + ); + + // we register a claim with a expiration that is too short + Compact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 typeHash = _getTypeHash(); + compactContract.register(claimHash, typeHash, defaultResetPeriodTimestamp - 1); + + vm.stopPrank(); + (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidRegistration.selector, user, claimHash)); + erc7683Allocator.open(onChainCrossChainOrder_); + } + + function test_successful() public { + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit( + address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user + ); + + // register a claim + Compact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 typeHash = _getTypeHash(); + compactContract.register(claimHash, typeHash, defaultResetPeriodTimestamp); + + vm.stopPrank(); + + (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); + IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); + IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); + IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); + maxSpent[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(defaultOutputToken))), + amount: type(uint256).max, + recipient: bytes32(uint256(uint160(user))), + chainId: defaultOutputChainId + }); + minReceived[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(address(usdc)))), + amount: defaultAmount, + recipient: '', + chainId: block.chainid + }); + Claim memory claim = + Claim({chainId: block.chainid, compact: _getCompact(), sponsorSignature: '', allocatorSignature: ''}); + fillInstructions[0] = IOriginSettler.FillInstruction({ + destinationChainId: defaultOutputChainId, + destinationSettler: bytes32(uint256(uint160(tribunal))), + originData: abi.encode(claim, _getMandate(), defaultTargetBlock, defaultMaximumBlocksAfterTarget) + }); + + IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ + user: user, + originChainId: block.chainid, + openDeadline: uint32(_getClaimExpiration()), + fillDeadline: uint32(_getFillExpiration()), + orderId: bytes32(defaultNonce), + maxSpent: maxSpent, + minReceived: minReceived, + fillInstructions: fillInstructions + }); + vm.prank(user); + vm.expectEmit(true, false, false, true, address(erc7683Allocator)); + emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); + erc7683Allocator.open(onChainCrossChainOrder_); + } +} + +contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, GaslessCrossChainOrderData { + function setUp() public override(OnChainCrossChainOrderData, GaslessCrossChainOrderData) { + super.setUp(); + } + + function test_revert_InvalidLock() public { + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit( + address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user + ); + + // register a claim + Compact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 typeHash = _getTypeHash(); + compactContract.register(claimHash, typeHash, defaultResetPeriodTimestamp); + + address filler = makeAddr('filler'); + vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); + vm.assertEq(compactContract.balanceOf(filler, usdcId), 0); + + vm.stopPrank(); + + // we do NOT open the order or lock the tokens + + // claim should be fail, because we mess with the nonce + QualifiedClaimWithWitness memory claim = QualifiedClaimWithWitness({ + allocatorSignature: '', + sponsorSignature: '', + sponsor: user, + nonce: defaultNonce, + expires: compact_.expires, + witness: keccak256(abi.encode(keccak256(bytes(mandateTypeString)), mandate_)), + witnessTypestring: witnessTypeString, + qualificationTypehash: erc7683Allocator.QUALIFICATION_TYPEHASH(), + qualificationPayload: abi.encode(defaultTargetBlock, defaultMaximumBlocksAfterTarget), + id: usdcId, + allocatedAmount: defaultAmount, + claimant: filler, + amount: defaultAmount + }); + vm.prank(arbiter); + vm.expectRevert(abi.encodeWithSelector(0x8baa579f)); // check for the InvalidSignature() error in the Compact contract + compactContract.claim(claim); + + vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); + vm.assertEq(compactContract.balanceOf(filler, usdcId), 0); + } + + function test_isValidSignature_successful_open() public { + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit( + address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user + ); + + // register a claim + Compact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 typeHash = _getTypeHash(); + compactContract.register(claimHash, typeHash, defaultResetPeriodTimestamp); + + address filler = makeAddr('filler'); + vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); + vm.assertEq(compactContract.balanceOf(filler, usdcId), 0); + + // we open the order and lock the tokens + (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); + erc7683Allocator.open(onChainCrossChainOrder_); + vm.stopPrank(); + + // claim should be successful + QualifiedClaimWithWitness memory claim = QualifiedClaimWithWitness({ + allocatorSignature: '', + sponsorSignature: '', + sponsor: user, + nonce: defaultNonce, + expires: compact_.expires, + witness: keccak256( + abi.encode( + keccak256(bytes(mandateTypeString)), + defaultOutputChainId, + tribunal, + mandate_.recipient, + mandate_.expires, + mandate_.token, + mandate_.minimumAmount, + mandate_.baselinePriorityFee, + mandate_.scalingFactor, + keccak256(abi.encodePacked(mandate_.decayCurve)), + mandate_.salt + ) + ), + witnessTypestring: witnessTypeString, + qualificationTypehash: erc7683Allocator.QUALIFICATION_TYPEHASH(), + qualificationPayload: abi.encode(defaultTargetBlock, defaultMaximumBlocksAfterTarget), + id: usdcId, + allocatedAmount: defaultAmount, + claimant: filler, + amount: defaultAmount + }); + vm.prank(arbiter); + compactContract.claim(claim); + + vm.assertEq(compactContract.balanceOf(user, usdcId), 0); + vm.assertEq(compactContract.balanceOf(filler, usdcId), defaultAmount); + } + + function test_isValidSignature_successful_openFor() public { + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit( + address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user + ); + + // register a claim + Compact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 typeHash = _getTypeHash(); + compactContract.register(claimHash, typeHash, defaultResetPeriodTimestamp); + + address filler = makeAddr('filler'); + vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); + vm.assertEq(compactContract.balanceOf(filler, usdcId), 0); + + // we open the order and lock the tokens + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = + _getGaslessCrossChainOrder(); + erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); + vm.stopPrank(); + + // claim should be successful + QualifiedClaimWithWitness memory claim = QualifiedClaimWithWitness({ + allocatorSignature: '', + sponsorSignature: '', + sponsor: user, + nonce: defaultNonce, + expires: compact_.expires, + witness: keccak256( + abi.encode( + keccak256(bytes(mandateTypeString)), + defaultOutputChainId, + tribunal, + mandate_.recipient, + mandate_.expires, + mandate_.token, + mandate_.minimumAmount, + mandate_.baselinePriorityFee, + mandate_.scalingFactor, + keccak256(abi.encodePacked(mandate_.decayCurve)), + mandate_.salt + ) + ), + witnessTypestring: witnessTypeString, + qualificationTypehash: erc7683Allocator.QUALIFICATION_TYPEHASH(), + qualificationPayload: abi.encode(uint256(0), uint256(0)), + id: usdcId, + allocatedAmount: defaultAmount, + claimant: filler, + amount: defaultAmount + }); + vm.prank(arbiter); + compactContract.claim(claim); + + vm.assertEq(compactContract.balanceOf(user, usdcId), 0); + vm.assertEq(compactContract.balanceOf(filler, usdcId), defaultAmount); + } +} + +contract ERC7683Allocator_resolveFor is GaslessCrossChainOrderData { + function test_resolve_successful() public { + // WITH THE CURRENT ERC7683 DESIGN, THE SPONSOR SIGNATURE IS NOT PROVIDED TO THE RESOLVE FUNCTION + // WHILE THE ResolvedCrossChainOrder WITHOUT THE SIGNATURE COULD STILL BE USED TO SIMULATE THE FILL, + // ACTUALLY USING THIS DATA WOULD RESULT IN A LOSS OF THE REWARD TOKENS FOR THE FILLER. + // THIS FEELS RISKY. + // THE CURRENT ALTERNATIVE WOULD BE HAVE THE INPUT SIGNATURE BEING LEFT EMPTY AND INSTEAD BE PROVIDED IN THE THE orderData OF THE GaslessCrossChainOrderData. + // THIS IS BOTH NOT IDEAL, SO CURRENTLY CHECKING FOR A SOLUTION. + + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = + _getGaslessCrossChainOrder(); + IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); + IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); + IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); + maxSpent[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(defaultOutputToken))), + amount: type(uint256).max, + recipient: bytes32(uint256(uint160(user))), + chainId: defaultOutputChainId + }); + minReceived[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(address(usdc)))), + amount: defaultAmount, + recipient: '', + chainId: block.chainid + }); + Claim memory claim = Claim({ + chainId: block.chainid, + compact: _getCompact(), + sponsorSignature: '', // sponsorSignature, // THE SIGNATURE MUST BE ADDED MANUALLY BY THE FILLER WITH THE CURRENT SYSTEM, BEFORE FILLING THE ORDER ON THE TARGET CHAIN + allocatorSignature: '' + }); + fillInstructions[0] = IOriginSettler.FillInstruction({ + destinationChainId: defaultOutputChainId, + destinationSettler: bytes32(uint256(uint160(tribunal))), + originData: abi.encode(claim, _getMandate(), uint256(0), uint256(0)) + }); + + IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ + user: user, + originChainId: block.chainid, + openDeadline: uint32(_getClaimExpiration()), + fillDeadline: uint32(_getFillExpiration()), + orderId: bytes32(defaultNonce), + maxSpent: maxSpent, + minReceived: minReceived, + fillInstructions: fillInstructions + }); + IOriginSettler.ResolvedCrossChainOrder memory resolved = + erc7683Allocator.resolveFor(gaslessCrossChainOrder_, ''); + assertEq(resolved.user, resolvedCrossChainOrder.user); + assertEq(resolved.originChainId, resolvedCrossChainOrder.originChainId); + assertEq(resolved.openDeadline, resolvedCrossChainOrder.openDeadline); + assertEq(resolved.fillDeadline, resolvedCrossChainOrder.fillDeadline); + assertEq(resolved.orderId, resolvedCrossChainOrder.orderId); + assertEq(resolved.maxSpent.length, resolvedCrossChainOrder.maxSpent.length); + assertEq(resolved.maxSpent[0].token, resolvedCrossChainOrder.maxSpent[0].token); + assertEq(resolved.maxSpent[0].amount, resolvedCrossChainOrder.maxSpent[0].amount); + assertEq(resolved.maxSpent[0].recipient, resolvedCrossChainOrder.maxSpent[0].recipient); + assertEq(resolved.maxSpent[0].chainId, resolvedCrossChainOrder.maxSpent[0].chainId); + assertEq(resolved.minReceived.length, resolvedCrossChainOrder.minReceived.length); + assertEq(resolved.minReceived[0].token, resolvedCrossChainOrder.minReceived[0].token); + assertEq(resolved.minReceived[0].amount, resolvedCrossChainOrder.minReceived[0].amount); + assertEq(resolved.minReceived[0].recipient, resolvedCrossChainOrder.minReceived[0].recipient); + assertEq(resolved.minReceived[0].chainId, resolvedCrossChainOrder.minReceived[0].chainId); + assertEq(resolved.fillInstructions.length, resolvedCrossChainOrder.fillInstructions.length); + assertEq( + resolved.fillInstructions[0].destinationChainId, + resolvedCrossChainOrder.fillInstructions[0].destinationChainId + ); + assertEq( + resolved.fillInstructions[0].destinationSettler, + resolvedCrossChainOrder.fillInstructions[0].destinationSettler + ); + assertEq(resolved.fillInstructions[0].originData, resolvedCrossChainOrder.fillInstructions[0].originData); + } +} + +contract ERC7683Allocator_resolve is OnChainCrossChainOrderData { + function test_resolve_successful() public { + (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); + IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); + IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); + IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); + maxSpent[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(defaultOutputToken))), + amount: type(uint256).max, + recipient: bytes32(uint256(uint160(user))), + chainId: defaultOutputChainId + }); + minReceived[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(address(usdc)))), + amount: defaultAmount, + recipient: '', + chainId: block.chainid + }); + Claim memory claim = + Claim({chainId: block.chainid, compact: _getCompact(), sponsorSignature: '', allocatorSignature: ''}); + fillInstructions[0] = IOriginSettler.FillInstruction({ + destinationChainId: defaultOutputChainId, + destinationSettler: bytes32(uint256(uint160(tribunal))), + originData: abi.encode(claim, _getMandate(), defaultTargetBlock, defaultMaximumBlocksAfterTarget) + }); + + IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ + user: user, + originChainId: block.chainid, + openDeadline: uint32(_getClaimExpiration()), + fillDeadline: uint32(_getFillExpiration()), + orderId: bytes32(defaultNonce), + maxSpent: maxSpent, + minReceived: minReceived, + fillInstructions: fillInstructions + }); + IOriginSettler.ResolvedCrossChainOrder memory resolved = erc7683Allocator.resolve(onChainCrossChainOrder_); + assertEq(resolved.user, resolvedCrossChainOrder.user); + assertEq(resolved.originChainId, resolvedCrossChainOrder.originChainId); + assertEq(resolved.openDeadline, resolvedCrossChainOrder.openDeadline); + assertEq(resolved.fillDeadline, resolvedCrossChainOrder.fillDeadline); + assertEq(resolved.orderId, resolvedCrossChainOrder.orderId); + assertEq(resolved.maxSpent.length, resolvedCrossChainOrder.maxSpent.length); + assertEq(resolved.maxSpent[0].token, resolvedCrossChainOrder.maxSpent[0].token); + assertEq(resolved.maxSpent[0].amount, resolvedCrossChainOrder.maxSpent[0].amount); + assertEq(resolved.maxSpent[0].recipient, resolvedCrossChainOrder.maxSpent[0].recipient); + assertEq(resolved.maxSpent[0].chainId, resolvedCrossChainOrder.maxSpent[0].chainId); + assertEq(resolved.minReceived.length, resolvedCrossChainOrder.minReceived.length); + assertEq(resolved.minReceived[0].token, resolvedCrossChainOrder.minReceived[0].token); + assertEq(resolved.minReceived[0].amount, resolvedCrossChainOrder.minReceived[0].amount); + assertEq(resolved.minReceived[0].recipient, resolvedCrossChainOrder.minReceived[0].recipient); + assertEq(resolved.minReceived[0].chainId, resolvedCrossChainOrder.minReceived[0].chainId); + assertEq(resolved.fillInstructions.length, resolvedCrossChainOrder.fillInstructions.length); + assertEq( + resolved.fillInstructions[0].destinationChainId, + resolvedCrossChainOrder.fillInstructions[0].destinationChainId + ); + assertEq( + resolved.fillInstructions[0].destinationSettler, + resolvedCrossChainOrder.fillInstructions[0].destinationSettler + ); + assertEq(resolved.fillInstructions[0].originData, resolvedCrossChainOrder.fillInstructions[0].originData); + } +} + +contract ERC7683Allocator_getCompactWitnessTypeString is MocksSetup { + function test_getCompactWitnessTypeString() public view { + assertEq( + erc7683Allocator.getCompactWitnessTypeString(), + 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt))' + ); + } +} + +contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData { + function test_revert_invalidNonce(uint256 nonce_) public { + address expectedSponsor; + assembly ("memory-safe") { + expectedSponsor := shr(96, nonce_) + } + vm.assume(user != expectedSponsor); + + vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidNonce.selector, nonce_)); + erc7683Allocator.checkNonce(user, nonce_); + } + + function test_checkNonce_unused(uint96 nonce_) public view { + address sponsor = user; + uint256 nonce; + assembly ("memory-safe") { + nonce := or(shl(96, sponsor), shr(160, shl(160, nonce_))) + } + assertEq(erc7683Allocator.checkNonce(sponsor, nonce), true); + } + + function test_checkNonce_used() public { + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit( + address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user + ); + + // register a claim + Compact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 typeHash = _getTypeHash(); + compactContract.register(claimHash, typeHash, defaultResetPeriodTimestamp); + + (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); + erc7683Allocator.open(onChainCrossChainOrder_); + + vm.assertEq(erc7683Allocator.checkNonce(user, defaultNonce), false); + vm.stopPrank(); + } + + function test_checkNonce_fuzz(uint8 nonce_) public { + uint256 nonce = uint256(bytes32(abi.encodePacked(user, uint96(nonce_)))); + + bool sameNonce = nonce == defaultNonce; + + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit( + address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user + ); + + // register a claim + Compact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 typeHash = _getTypeHash(); + compactContract.register(claimHash, typeHash, defaultResetPeriodTimestamp); + + (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); + erc7683Allocator.open(onChainCrossChainOrder_); + + vm.assertEq(erc7683Allocator.checkNonce(user, nonce), !sameNonce); + + vm.stopPrank(); + } +} From 4d5d9815de0fd331ef1fb315be3627e05c5168b4 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Mon, 23 Jun 2025 11:52:10 +0200 Subject: [PATCH 22/63] updated Compact and fixed issues --- test/ERC7683Allocator.t.sol | 77 ++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index 2c259b6..a100a00 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -8,15 +8,19 @@ import {TheCompact} from '@uniswap/the-compact/TheCompact.sol'; import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; import {IdLib} from '@uniswap/the-compact/lib/IdLib.sol'; + +import {Claim} from '@uniswap/the-compact/types/Claims.sol'; +import {Component} from '@uniswap/the-compact/types/Components.sol'; import {COMPACT_TYPEHASH, Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; import {ForcedWithdrawalStatus} from '@uniswap/the-compact/types/ForcedWithdrawalStatus.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 {Test, console} from 'forge-std/Test.sol'; + +import {TestHelper} from 'test/util/TestHelper.sol'; -import {console} from 'forge-std/console.sol'; import {ERC7683Allocator} from 'src/allocators/ERC7683Allocator.sol'; -import {Claim, Mandate} from 'src/allocators/types/TribunalStructs.sol'; +import {Claim as TribunalClaim, Mandate} from 'src/allocators/types/TribunalStructs.sol'; import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; import {IERC7683Allocator} from 'src/interfaces/IERC7683Allocator.sol'; @@ -24,7 +28,7 @@ import {ISimpleAllocator} from 'src/interfaces/ISimpleAllocator.sol'; import {ERC20Mock} from 'src/test/ERC20Mock.sol'; import {TheCompactMock} from 'src/test/TheCompactMock.sol'; -abstract contract MocksSetup is Test { +abstract contract MocksSetup is Test, TestHelper { address user; uint256 userPK; address attacker; @@ -61,13 +65,8 @@ abstract contract MocksSetup is Test { usdc = new ERC20Mock('USDC', 'USDC'); compactContract = new TheCompact(); erc7683Allocator = new ERC7683Allocator(address(compactContract), 5, 100); - Lock memory lock = Lock({ - token: address(usdc), - allocator: address(erc7683Allocator), - resetPeriod: defaultResetPeriod, - scope: defaultScope - }); - usdcId = IdLib.toId(lock); + + usdcId = _toId(defaultScope, defaultResetPeriod, address(erc7683Allocator), address(usdc)); (attacker, attackerPK) = makeAddrAndKey('attacker'); defaultNonce = uint256(bytes32(abi.encodePacked(user, uint96(1)))); @@ -517,7 +516,7 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { recipient: '', chainId: block.chainid }); - Claim memory claim = Claim({ + TribunalClaim memory claim = TribunalClaim({ chainId: block.chainid, compact: _getCompact(), sponsorSignature: sponsorSignature, @@ -572,7 +571,7 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { recipient: '', chainId: block.chainid }); - Claim memory claim = Claim({ + TribunalClaim memory claim = TribunalClaim({ chainId: block.chainid, compact: _getCompact(), sponsorSignature: sponsorSignature, @@ -737,8 +736,12 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { recipient: '', chainId: block.chainid }); - Claim memory claim = - Claim({chainId: block.chainid, compact: _getCompact(), sponsorSignature: '', allocatorSignature: ''}); + TribunalClaim memory claim = TribunalClaim({ + chainId: block.chainid, + compact: _getCompact(), + sponsorSignature: '', + allocatorSignature: '' + }); fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), @@ -793,20 +796,20 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Gasles // we do NOT open the order or lock the tokens // claim should be fail, because we mess with the nonce - QualifiedClaimWithWitness memory claim = QualifiedClaimWithWitness({ - allocatorSignature: '', + Component memory component = Component({claimant: filler, amount: defaultAmount}); + Claim memory claim = Claim({ + allocatorData: abi.encode( + erc7683Allocator.QUALIFICATION_TYPEHASH(), defaultTargetBlock, defaultMaximumBlocksAfterTarget + ), sponsorSignature: '', sponsor: user, nonce: defaultNonce, expires: compact_.expires, witness: keccak256(abi.encode(keccak256(bytes(mandateTypeString)), mandate_)), witnessTypestring: witnessTypeString, - qualificationTypehash: erc7683Allocator.QUALIFICATION_TYPEHASH(), - qualificationPayload: abi.encode(defaultTargetBlock, defaultMaximumBlocksAfterTarget), id: usdcId, allocatedAmount: defaultAmount, - claimant: filler, - amount: defaultAmount + claimants: component }); vm.prank(arbiter); vm.expectRevert(abi.encodeWithSelector(0x8baa579f)); // check for the InvalidSignature() error in the Compact contract @@ -843,8 +846,11 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Gasles vm.stopPrank(); // claim should be successful - QualifiedClaimWithWitness memory claim = QualifiedClaimWithWitness({ - allocatorSignature: '', + Component memory component = Component({claimant: filler, amount: defaultAmount}); + Claim memory claim = Claim({ + allocatorData: abi.encode( + erc7683Allocator.QUALIFICATION_TYPEHASH(), defaultTargetBlock, defaultMaximumBlocksAfterTarget + ), sponsorSignature: '', sponsor: user, nonce: defaultNonce, @@ -865,12 +871,9 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Gasles ) ), witnessTypestring: witnessTypeString, - qualificationTypehash: erc7683Allocator.QUALIFICATION_TYPEHASH(), - qualificationPayload: abi.encode(defaultTargetBlock, defaultMaximumBlocksAfterTarget), id: usdcId, allocatedAmount: defaultAmount, - claimant: filler, - amount: defaultAmount + claimants: component }); vm.prank(arbiter); compactContract.claim(claim); @@ -907,8 +910,9 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Gasles vm.stopPrank(); // claim should be successful - QualifiedClaimWithWitness memory claim = QualifiedClaimWithWitness({ - allocatorSignature: '', + Component memory component = Component({claimant: filler, amount: defaultAmount}); + Claim memory claim = Claim({ + allocatorData: abi.encode(erc7683Allocator.QUALIFICATION_TYPEHASH(), uint256(0), uint256(0)), sponsorSignature: '', sponsor: user, nonce: defaultNonce, @@ -929,12 +933,9 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Gasles ) ), witnessTypestring: witnessTypeString, - qualificationTypehash: erc7683Allocator.QUALIFICATION_TYPEHASH(), - qualificationPayload: abi.encode(uint256(0), uint256(0)), id: usdcId, allocatedAmount: defaultAmount, - claimant: filler, - amount: defaultAmount + claimants: component }); vm.prank(arbiter); compactContract.claim(claim); @@ -970,7 +971,7 @@ contract ERC7683Allocator_resolveFor is GaslessCrossChainOrderData { recipient: '', chainId: block.chainid }); - Claim memory claim = Claim({ + TribunalClaim memory claim = TribunalClaim({ chainId: block.chainid, compact: _getCompact(), sponsorSignature: '', // sponsorSignature, // THE SIGNATURE MUST BE ADDED MANUALLY BY THE FILLER WITH THE CURRENT SYSTEM, BEFORE FILLING THE ORDER ON THE TARGET CHAIN @@ -1040,8 +1041,12 @@ contract ERC7683Allocator_resolve is OnChainCrossChainOrderData { recipient: '', chainId: block.chainid }); - Claim memory claim = - Claim({chainId: block.chainid, compact: _getCompact(), sponsorSignature: '', allocatorSignature: ''}); + TribunalClaim memory claim = TribunalClaim({ + chainId: block.chainid, + compact: _getCompact(), + sponsorSignature: '', + allocatorSignature: '' + }); fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), From 309757b7188dc2a73822eafd5e1839694bc35c62 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Mon, 23 Jun 2025 13:52:28 +0200 Subject: [PATCH 23/63] fixed compile issues --- src/allocators/ERC7683Allocator.sol | 66 ++++++------- src/interfaces/IERC7683Allocator.sol | 12 ++- test/ERC7683Allocator.t.sol | 133 ++++++++++----------------- 3 files changed, 90 insertions(+), 121 deletions(-) diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index 397f9d9..e5c023d 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -122,8 +122,9 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { // We trust the compact to check the nonce and that this contract is the allocator connected to the id _checkExpiration(expires); - bytes32 tokenHash = _checkForActiveAllocation(sponsor, idsAndAmounts[0][0]); - _checkForcedWithdrawal(sponsor, expires, idsAndAmounts[0][0]); + (bytes12 lockTag, address token) = _separateId(idsAndAmounts[0][0]); + bytes32 tokenHash = _checkForActiveAllocation(sponsor, lockTag, token); + _checkForcedWithdrawal(sponsor, expires, lockTag, token); _checkBalance(sponsor, idsAndAmounts[0][0], idsAndAmounts[0][1]); // TODO: Should the Compact check this prior to the callback? OderDataCallback memory orderDataCallback = abi.decode(allocatorData, (OderDataCallback)); @@ -143,12 +144,13 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { sponsor: sponsor, nonce: nonce, expires: expires, - id: idsAndAmounts[0][0], + lockTag: lockTag, + inputToken: token, amount: idsAndAmounts[0][1], chainId: orderDataCallback.chainId, tribunal: orderDataCallback.tribunal, recipient: orderDataCallback.recipient, - token: orderDataCallback.token, + settlementToken: orderDataCallback.settlementToken, minimumAmount: orderDataCallback.minimumAmount, baselinePriorityFee: orderDataCallback.baselinePriorityFee, scalingFactor: orderDataCallback.scalingFactor, @@ -247,8 +249,9 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { // We do not enforce a specific tribunal or arbiter. This will allow to support new arbiters and tribunals after the deployment of the allocator // Going with an immutable arbiter and tribunal would limit support for new chains with a fully decentralized allocator - bytes32 tokenHash = - _verifyAllocation(sponsor_, orderData_.id, orderData_.amount, orderData_.expires, orderData_.nonce); + bytes32 tokenHash = _verifyAllocation( + sponsor_, orderData_.lockTag, orderData_.inputToken, orderData_.amount, orderData_.expires, orderData_.nonce + ); // Create the Compact claim hash bytes32 claimHash = keccak256( @@ -258,7 +261,8 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { sponsor_, orderData_.nonce, orderData_.expires, - orderData_.id, + orderData_.lockTag, + orderData_.inputToken, orderData_.amount, keccak256( abi.encode( @@ -267,7 +271,7 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { orderData_.tribunal, orderData_.recipient, fillDeadline_, - orderData_.token, + orderData_.settlementToken, orderData_.minimumAmount, orderData_.baselinePriorityFee, orderData_.scalingFactor, @@ -288,9 +292,8 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { } } else { // confirm the claim hash is registered on the compact - (bool isActive, uint256 registrationExpiration) = - ITheCompact(COMPACT_CONTRACT).getRegistrationStatus(sponsor_, claimHash, COMPACT_WITNESS_TYPEHASH); - if (!isActive || registrationExpiration < orderData_.expires) { + (bool isActive) = ITheCompact(COMPACT_CONTRACT).isRegistered(sponsor_, claimHash, COMPACT_WITNESS_TYPEHASH); + if (!isActive) { revert InvalidRegistration(sponsor_, claimHash); } } @@ -305,18 +308,21 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { emit Open(bytes32(orderData_.nonce), _resolveOrder(sponsor_, fillDeadline_, orderData_, sponsorSignature_)); } - function _verifyAllocation(address sponsor, uint256 id, uint256 amount, uint256 expires, uint256 nonce) - internal - view - returns (bytes32 tokenHash_) - { + function _verifyAllocation( + address sponsor, + bytes12 lockTag, + address token, + uint256 amount, + uint256 expires, + uint256 nonce + ) internal view returns (bytes32 tokenHash_) { // Check for a valid allocation - tokenHash_ = _checkForActiveAllocation(sponsor, id); - _checkAllocator(id); + tokenHash_ = _checkForActiveAllocation(sponsor, lockTag, token); + _checkAllocator(lockTag, token); _checkExpiration(expires); _checkNonce(nonce); - _checkForcedWithdrawal(sponsor, expires, id); - _checkBalance(sponsor, id, amount); + _checkForcedWithdrawal(sponsor, expires, lockTag, token); + _checkBalance(sponsor, _getTokenId(lockTag, token), amount); return tokenHash_; } @@ -338,7 +344,7 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { Mandate memory mandate = Mandate({ recipient: orderData.recipient, expires: fillDeadline, - token: orderData.token, + token: orderData.settlementToken, minimumAmount: orderData.minimumAmount, baselinePriorityFee: orderData.baselinePriorityFee, scalingFactor: orderData.scalingFactor, @@ -352,7 +358,8 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { sponsor: sponsor, nonce: orderData.nonce, expires: orderData.expires, - id: orderData.id, + lockTag: orderData.lockTag, + token: orderData.inputToken, amount: orderData.amount }), sponsorSignature: sponsorSignature, @@ -366,13 +373,13 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { }); Output memory spent = Output({ - token: _addressToBytes32(orderData.token), + token: _addressToBytes32(orderData.settlementToken), amount: type(uint256).max, recipient: _addressToBytes32(orderData.recipient), chainId: orderData.chainId }); Output memory received = Output({ - token: _addressToBytes32(_idToToken(orderData.id)), + token: _addressToBytes32(orderData.inputToken), amount: orderData.amount, recipient: bytes32(0), chainId: block.chainid @@ -428,12 +435,6 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { } } - function _idToToken(uint256 id_) internal pure returns (address token_) { - assembly ("memory-safe") { - token_ := shr(96, shl(96, id_)) - } - } - function _addressToBytes32(address address_) internal pure returns (bytes32 output_) { assembly ("memory-safe") { output_ := shr(96, shl(96, address_)) @@ -451,12 +452,13 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { sponsor: sponsor_, nonce: nonce_, expires: openDeadline_, - id: orderDataGasless_.id, + lockTag: orderDataGasless_.lockTag, + inputToken: orderDataGasless_.inputToken, amount: orderDataGasless_.amount, chainId: orderDataGasless_.chainId, tribunal: orderDataGasless_.tribunal, recipient: orderDataGasless_.recipient, - token: orderDataGasless_.token, + settlementToken: orderDataGasless_.settlementToken, minimumAmount: orderDataGasless_.minimumAmount, baselinePriorityFee: orderDataGasless_.baselinePriorityFee, scalingFactor: orderDataGasless_.scalingFactor, diff --git a/src/interfaces/IERC7683Allocator.sol b/src/interfaces/IERC7683Allocator.sol index 9a3369f..14be819 100644 --- a/src/interfaces/IERC7683Allocator.sol +++ b/src/interfaces/IERC7683Allocator.sol @@ -11,14 +11,15 @@ interface IERC7683Allocator is IOriginSettler { address sponsor; // The account to source the tokens from. uint256 nonce; // A parameter to enforce replay protection, scoped to allocator. uint256 expires; // The time at which the claim expires. - uint256 id; // The token ID of the ERC6909 token to allocate. + bytes12 lockTag; // The token ID of the ERC6909 token to allocate. + address inputToken; // The token address of the ERC6909 token to allocate. uint256 amount; // The amount of ERC6909 tokens to allocate. // MANDATE uint256 chainId; // (implicit arg, included in EIP712 payload) address tribunal; // (implicit arg, included in EIP712 payload) address recipient; // Recipient of settled tokens // uint256 expires; // Mandate expiration timestamp - address token; // Settlement token (address(0) for native) + address settlementToken; // Settlement token (address(0) for native) uint256 minimumAmount; // Minimum settlement amount uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline) @@ -35,14 +36,15 @@ interface IERC7683Allocator is IOriginSettler { // address sponsor; // The account to source the tokens from. // uint256 nonce; // A parameter to enforce replay protection, scoped to allocator. // uint256 expires; // The time at which the claim expires. - uint256 id; // The token ID of the ERC6909 token to allocate. + bytes12 lockTag; // The lock tag of the ERC6909 token to allocate. + address inputToken; // The token address of the ERC6909 token to allocate. uint256 amount; // The amount of ERC6909 tokens to allocate. // MANDATE uint256 chainId; // (implicit arg, included in EIP712 payload) address tribunal; // (implicit arg, included in EIP712 payload) address recipient; // Recipient of settled tokens // uint256 expires; // Mandate expiration timestamp - address token; // Settlement token (address(0) for native) + address settlementToken; // Settlement token (address(0) for native) uint256 minimumAmount; // Minimum settlement amount uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline) @@ -63,7 +65,7 @@ interface IERC7683Allocator is IOriginSettler { address tribunal; // (implicit arg, included in EIP712 payload) address recipient; // Recipient of settled tokens uint256 expires; // Mandate expiration timestamp - address token; // Settlement token (address(0) for native) + address settlementToken; // Settlement token (address(0) for native) uint256 minimumAmount; // Minimum settlement amount uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline) diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index a100a00..d742078 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -38,6 +38,7 @@ abstract contract MocksSetup is Test, TestHelper { ERC20Mock usdc; TheCompact compactContract; ERC7683Allocator erc7683Allocator; + bytes12 usdcLockTag; uint256 usdcId; ResetPeriod defaultResetPeriod = ResetPeriod.OneMinute; @@ -66,6 +67,7 @@ abstract contract MocksSetup is Test, TestHelper { compactContract = new TheCompact(); erc7683Allocator = new ERC7683Allocator(address(compactContract), 5, 100); + usdcLockTag = _toLockTag(address(erc7683Allocator), defaultScope, defaultResetPeriod); usdcId = _toId(defaultScope, defaultResetPeriod, address(erc7683Allocator), address(usdc)); (attacker, attackerPK) = makeAddrAndKey('attacker'); defaultNonce = uint256(bytes32(abi.encodePacked(user, uint96(1)))); @@ -117,7 +119,8 @@ abstract contract CreateHash is MocksSetup { data.sponsor, data.nonce, data.expires, - data.id, + data.lockTag, + data.token, data.amount, keccak256( abi.encode( @@ -182,7 +185,8 @@ abstract contract CompactData is CreateHash { sponsor: user, nonce: defaultNonce, expires: _getClaimExpiration(), - id: usdcId, + lockTag: usdcLockTag, + token: address(usdc), amount: defaultAmount }); @@ -237,12 +241,13 @@ abstract contract GaslessCrossChainOrderData is CompactData { orderData: abi.encode( IERC7683Allocator.OrderDataGasless({ arbiter: compact_.arbiter, - id: compact_.id, + lockTag: compact_.lockTag, + inputToken: compact_.token, amount: compact_.amount, chainId: defaultOutputChainId, tribunal: tribunal, recipient: mandate_.recipient, - token: mandate_.token, + settlementToken: mandate_.token, minimumAmount: mandate_.minimumAmount, baselinePriorityFee: mandate_.baselinePriorityFee, scalingFactor: mandate_.scalingFactor, @@ -273,12 +278,13 @@ abstract contract GaslessCrossChainOrderData is CompactData { orderData: abi.encode( IERC7683Allocator.OrderDataGasless({ arbiter: compact_.arbiter, - id: compact_.id, + lockTag: compact_.lockTag, + inputToken: compact_.token, amount: compact_.amount, chainId: defaultOutputChainId, tribunal: tribunal, recipient: mandate_.recipient, - token: mandate_.token, + settlementToken: mandate_.token, minimumAmount: mandate_.minimumAmount, baselinePriorityFee: mandate_.baselinePriorityFee, scalingFactor: mandate_.scalingFactor, @@ -319,12 +325,13 @@ abstract contract OnChainCrossChainOrderData is CompactData { sponsor: compact_.sponsor, nonce: compact_.nonce, expires: compact_.expires, - id: compact_.id, + lockTag: compact_.lockTag, + inputToken: compact_.token, amount: compact_.amount, chainId: defaultOutputChainId, tribunal: tribunal, recipient: mandate_.recipient, - token: mandate_.token, + settlementToken: mandate_.token, minimumAmount: mandate_.minimumAmount, baselinePriorityFee: mandate_.baselinePriorityFee, scalingFactor: mandate_.scalingFactor, @@ -355,12 +362,13 @@ abstract contract OnChainCrossChainOrderData is CompactData { sponsor: compact_.sponsor, nonce: compact_.nonce, expires: compact_.expires, - id: compact_.id, + lockTag: compact_.lockTag, + inputToken: compact_.token, amount: compact_.amount, chainId: defaultOutputChainId, tribunal: tribunal, recipient: mandate_.recipient, - token: mandate_.token, + settlementToken: mandate_.token, minimumAmount: mandate_.minimumAmount, baselinePriorityFee: mandate_.baselinePriorityFee, scalingFactor: mandate_.scalingFactor, @@ -383,9 +391,7 @@ abstract contract Deposited is MocksSetup { usdc.mint(user, defaultAmount); usdc.approve(address(compactContract), defaultAmount); - compactContract.deposit( - address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user - ); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); vm.stopPrank(); } @@ -468,9 +474,7 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { vm.startPrank(user); usdc.mint(user, defaultAmount); usdc.approve(address(compactContract), defaultAmount); - compactContract.deposit( - address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user - ); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); vm.stopPrank(); // Create a malicious signature @@ -494,9 +498,7 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { vm.startPrank(user); usdc.mint(user, defaultAmount); usdc.approve(address(compactContract), defaultAmount); - compactContract.deposit( - address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user - ); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); vm.stopPrank(); (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = @@ -549,9 +551,7 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { vm.startPrank(user); usdc.mint(user, defaultAmount); usdc.approve(address(compactContract), defaultAmount); - compactContract.deposit( - address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user - ); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); vm.stopPrank(); (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = @@ -606,9 +606,7 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { vm.startPrank(user); usdc.mint(user, defaultAmount); usdc.approve(address(compactContract), defaultAmount); - compactContract.deposit( - address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user - ); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); vm.stopPrank(); // use the nonce once @@ -648,7 +646,7 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_ = _getOnChainCrossChainOrder(); vm.prank(attacker); - vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidSponsor.selector, user, attacker)); + vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidSignature.selector, user, attacker)); erc7683Allocator.open(onChainCrossChainOrder_); } @@ -657,9 +655,7 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { vm.startPrank(user); usdc.mint(user, defaultAmount); usdc.approve(address(compactContract), defaultAmount); - compactContract.deposit( - address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user - ); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // we do NOT register a claim @@ -676,39 +672,12 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { erc7683Allocator.open(onChainCrossChainOrder_); } - function test_revert_InvalidRegistration_Expired() public { - // we deposit tokens - vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); - compactContract.deposit( - address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user - ); - - // we register a claim with a expiration that is too short - Compact memory compact_ = _getCompact(); - Mandate memory mandate_ = _getMandate(); - - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash, defaultResetPeriodTimestamp - 1); - - vm.stopPrank(); - (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); - - vm.prank(user); - vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidRegistration.selector, user, claimHash)); - erc7683Allocator.open(onChainCrossChainOrder_); - } - function test_successful() public { // Deposit tokens vm.startPrank(user); usdc.mint(user, defaultAmount); usdc.approve(address(compactContract), defaultAmount); - compactContract.deposit( - address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user - ); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim Compact memory compact_ = _getCompact(); @@ -716,7 +685,7 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { bytes32 claimHash = _hashCompact(compact_, mandate_); bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash, defaultResetPeriodTimestamp); + compactContract.register(claimHash, typeHash); vm.stopPrank(); @@ -775,9 +744,7 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Gasles vm.startPrank(user); usdc.mint(user, defaultAmount); usdc.approve(address(compactContract), defaultAmount); - compactContract.deposit( - address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user - ); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim Compact memory compact_ = _getCompact(); @@ -785,7 +752,7 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Gasles bytes32 claimHash = _hashCompact(compact_, mandate_); bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash, defaultResetPeriodTimestamp); + compactContract.register(claimHash, typeHash); address filler = makeAddr('filler'); vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); @@ -796,7 +763,9 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Gasles // we do NOT open the order or lock the tokens // claim should be fail, because we mess with the nonce - Component memory component = Component({claimant: filler, amount: defaultAmount}); + Component memory component = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); + Component[] memory components = new Component[](1); + components[0] = component; Claim memory claim = Claim({ allocatorData: abi.encode( erc7683Allocator.QUALIFICATION_TYPEHASH(), defaultTargetBlock, defaultMaximumBlocksAfterTarget @@ -809,7 +778,7 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Gasles witnessTypestring: witnessTypeString, id: usdcId, allocatedAmount: defaultAmount, - claimants: component + claimants: components }); vm.prank(arbiter); vm.expectRevert(abi.encodeWithSelector(0x8baa579f)); // check for the InvalidSignature() error in the Compact contract @@ -824,9 +793,7 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Gasles vm.startPrank(user); usdc.mint(user, defaultAmount); usdc.approve(address(compactContract), defaultAmount); - compactContract.deposit( - address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user - ); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim Compact memory compact_ = _getCompact(); @@ -834,7 +801,7 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Gasles bytes32 claimHash = _hashCompact(compact_, mandate_); bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash, defaultResetPeriodTimestamp); + compactContract.register(claimHash, typeHash); address filler = makeAddr('filler'); vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); @@ -846,7 +813,9 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Gasles vm.stopPrank(); // claim should be successful - Component memory component = Component({claimant: filler, amount: defaultAmount}); + Component memory component = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); + Component[] memory components = new Component[](1); + components[0] = component; Claim memory claim = Claim({ allocatorData: abi.encode( erc7683Allocator.QUALIFICATION_TYPEHASH(), defaultTargetBlock, defaultMaximumBlocksAfterTarget @@ -873,7 +842,7 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Gasles witnessTypestring: witnessTypeString, id: usdcId, allocatedAmount: defaultAmount, - claimants: component + claimants: components }); vm.prank(arbiter); compactContract.claim(claim); @@ -887,9 +856,7 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Gasles vm.startPrank(user); usdc.mint(user, defaultAmount); usdc.approve(address(compactContract), defaultAmount); - compactContract.deposit( - address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user - ); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim Compact memory compact_ = _getCompact(); @@ -897,7 +864,7 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Gasles bytes32 claimHash = _hashCompact(compact_, mandate_); bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash, defaultResetPeriodTimestamp); + compactContract.register(claimHash, typeHash); address filler = makeAddr('filler'); vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); @@ -910,7 +877,9 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Gasles vm.stopPrank(); // claim should be successful - Component memory component = Component({claimant: filler, amount: defaultAmount}); + Component memory component = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); + Component[] memory components = new Component[](1); + components[0] = component; Claim memory claim = Claim({ allocatorData: abi.encode(erc7683Allocator.QUALIFICATION_TYPEHASH(), uint256(0), uint256(0)), sponsorSignature: '', @@ -935,7 +904,7 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Gasles witnessTypestring: witnessTypeString, id: usdcId, allocatedAmount: defaultAmount, - claimants: component + claimants: components }); vm.prank(arbiter); compactContract.claim(claim); @@ -1127,9 +1096,7 @@ contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData { vm.startPrank(user); usdc.mint(user, defaultAmount); usdc.approve(address(compactContract), defaultAmount); - compactContract.deposit( - address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user - ); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim Compact memory compact_ = _getCompact(); @@ -1137,7 +1104,7 @@ contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData { bytes32 claimHash = _hashCompact(compact_, mandate_); bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash, defaultResetPeriodTimestamp); + compactContract.register(claimHash, typeHash); (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); erc7683Allocator.open(onChainCrossChainOrder_); @@ -1155,9 +1122,7 @@ contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData { vm.startPrank(user); usdc.mint(user, defaultAmount); usdc.approve(address(compactContract), defaultAmount); - compactContract.deposit( - address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user - ); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim Compact memory compact_ = _getCompact(); @@ -1165,7 +1130,7 @@ contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData { bytes32 claimHash = _hashCompact(compact_, mandate_); bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash, defaultResetPeriodTimestamp); + compactContract.register(claimHash, typeHash); (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); erc7683Allocator.open(onChainCrossChainOrder_); From 5df526c605a3b27435b7875f94797927a3e992de Mon Sep 17 00:00:00 2001 From: mgretzke Date: Mon, 23 Jun 2025 14:04:47 +0200 Subject: [PATCH 24/63] removed local IAllocator --- src/allocators/ERC7683Allocator.sol | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index e5c023d..b4a55ad 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -197,23 +197,6 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { return this.authorizeClaim.selector; } - function allocatorDataSpecification() - external - pure - override - returns ( - uint256 specificationId, // An identifier indicating a required "standard" for allocatorData. - string memory claimEncoding, // The encoding of the `allocatorData` payload on claim processing. - string memory registrationEncoding, // The encoding of the `allocatorData` payload on claim registration. - bytes memory context // Any additional context as defined by the specificationId. - ) - { - specificationId = 0; - claimEncoding = ''; - registrationEncoding = ''; - context = ''; - } - /// @inheritdoc IERC7683Allocator function getCompactWitnessTypeString() external pure returns (string memory) { return From e7a911df567825372fbe254e1d1f91e85fdc761a Mon Sep 17 00:00:00 2001 From: mgretzke Date: Wed, 2 Jul 2025 19:22:52 +0200 Subject: [PATCH 25/63] Updated ERC7683Allocator to support coverage (wip) --- src/allocators/ERC7683Allocator.sol | 525 ++++++++++++++++++---------- 1 file changed, 350 insertions(+), 175 deletions(-) diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index b4a55ad..15a95a4 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -8,8 +8,11 @@ import {Claim, Mandate} from './types/TribunalStructs.sol'; import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; import {ECDSA} from '@openzeppelin/contracts/utils/cryptography/ECDSA.sol'; + +import {LibBytes} from '@solady/utils/LibBytes.sol'; import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; import {Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; +import {console} from 'forge-std/console.sol'; contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { /// @notice The typehash of the OrderData struct @@ -37,6 +40,10 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { /// @notice uint256(uint8(keccak256("ERC7683Allocator.nonce"))) uint8 internal constant NONCE_MASTER_SLOT_SEED = 0x39; + uint8 internal constant ORDERDATA_OFFSET = 0xa0; + + uint8 internal constant ORDERDATA_GASLESS_OFFSET = 0x40; + bytes32 immutable _COMPACT_DOMAIN_SEPARATOR; constructor(address compactContract_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_) @@ -60,12 +67,27 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { } // Decode the orderData - OrderDataGasless memory orderDataGasless = abi.decode(order_.orderData, (OrderDataGasless)); + bytes calldata orderDataGaslessBytes = LibBytes.dynamicStructInCalldata(order_.orderData, 0x00); + uint256 mandateOffset; + assembly ("memory-safe") { + mandateOffset := add(orderDataGaslessBytes.offset, 0xa0) // mandate starts at orderData.chainId (0x80). Add 0x20, as the offset points at the length + } + + // Extract the resolved order early to reduce stack pressure + ResolvedCrossChainOrder memory resolvedOrder = _resolveOrder( + order_.user, + order_.nonce, + order_.openDeadline, + order_.fillDeadline, + orderDataGaslessBytes, + ORDERDATA_GASLESS_OFFSET, + sponsorSignature_ + ); - OrderData memory orderData = - _convertGaslessOrderData(order_.user, order_.nonce, order_.openDeadline, orderDataGasless); + // Extract mandateHash early to reduce stack pressure + bytes32 mandateHash = _mandateHash(mandateOffset, order_.fillDeadline); - _open(orderData, order_.fillDeadline, order_.user, sponsorSignature_); + _open(orderDataGaslessBytes, ORDERDATA_GASLESS_OFFSET, sponsorSignature_, mandateHash, resolvedOrder); } /// @inheritdoc IERC7683Allocator @@ -76,10 +98,35 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { } // Decode the orderData - OrderData memory orderData = abi.decode(order.orderData, (OrderData)); + bytes calldata orderDataBytes = LibBytes.dynamicStructInCalldata(order.orderData, 0x00); + OrderData calldata orderData; + uint256 mandateOffset; + assembly ("memory-safe") { + orderData := orderDataBytes.offset + mandateOffset := add(orderDataBytes.offset, 0xe0) // mandate starts at orderData.chainId + } + + console.log('-1'); + _checkMsgSender(orderData.sponsor); + console.log('0'); + + // Extract mandateHash early to reduce stack pressure + bytes32 mandateHash = _mandateHash(mandateOffset, order.fillDeadline); + // Extract the resolved order early to reduce stack pressure + ResolvedCrossChainOrder memory resolvedOrder = _resolveOrder( + orderData.sponsor, + orderData.nonce, + uint32(orderData.expires), + order.fillDeadline, + orderDataBytes, + ORDERDATA_OFFSET, + LibBytes.emptyCalldata() + ); + + console.log('1'); - _open(orderData, order.fillDeadline, msg.sender, ''); + _open(orderDataBytes, ORDERDATA_OFFSET, LibBytes.emptyCalldata(), mandateHash, resolvedOrder); } /// @inheritdoc IERC7683Allocator @@ -88,83 +135,106 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { view returns (ResolvedCrossChainOrder memory) { - OrderDataGasless memory orderDataGasless = abi.decode(order_.orderData, (OrderDataGasless)); + bytes calldata orderDataGaslessBytes = LibBytes.dynamicStructInCalldata(order_.orderData, 0x00); + OrderDataGasless calldata orderDataGasless; + assembly ("memory-safe") { + orderDataGasless := orderDataGaslessBytes.offset + } - OrderData memory orderData = - _convertGaslessOrderData(order_.user, order_.nonce, order_.openDeadline, orderDataGasless); - return _resolveOrder(order_.user, order_.fillDeadline, orderData, ''); + return _resolveOrder( + order_.user, + order_.nonce, + order_.openDeadline, + order_.fillDeadline, + orderDataGaslessBytes, + ORDERDATA_GASLESS_OFFSET, + LibBytes.emptyCalldata() + ); } /// @inheritdoc IERC7683Allocator function resolve(OnchainCrossChainOrder calldata order_) external view returns (ResolvedCrossChainOrder memory) { - OrderData memory orderData = abi.decode(order_.orderData, (OrderData)); - return _resolveOrder(orderData.sponsor, order_.fillDeadline, orderData, ''); - } - - function registerClaim( - bytes32 claimHash, // The message hash representing the claim. - address, /* caller */ // The account initiating the registration. - address arbiter, // The account tasked with verifying and submitting the claim. - address sponsor, // The account to source the tokens from. - uint256 nonce, // A parameter to enforce replay protection, scoped to allocator. - uint256 expires, // The time at which the claim expires. - uint256[2][] calldata idsAndAmounts, // The allocated token IDs and amounts. - bytes calldata allocatorData // Arbitrary data provided by the caller. - ) external override onlyCompact returns (bytes4) { - if (idsAndAmounts.length > 1) { - revert BatchCompactsNotSupported(); + bytes calldata orderDataBytes = LibBytes.dynamicStructInCalldata(order_.orderData, 0x00); + OrderData calldata orderData; + assembly ("memory-safe") { + orderData := orderDataBytes.offset } - - // Enforce a nonce where the most significant 96 bits are the nonce and the least significant 160 bits are the sponsor - uint96 nonceWithoutAddress = _nonceValidation(sponsor, nonce); - // Set a nonce or revert if it is already used - _setNonce(sponsor, nonceWithoutAddress); - - // We trust the compact to check the nonce and that this contract is the allocator connected to the id - _checkExpiration(expires); - (bytes12 lockTag, address token) = _separateId(idsAndAmounts[0][0]); - bytes32 tokenHash = _checkForActiveAllocation(sponsor, lockTag, token); - _checkForcedWithdrawal(sponsor, expires, lockTag, token); - _checkBalance(sponsor, idsAndAmounts[0][0], idsAndAmounts[0][1]); // TODO: Should the Compact check this prior to the callback? - - OderDataCallback memory orderDataCallback = abi.decode(allocatorData, (OderDataCallback)); - bytes32 qualifiedClaimHash = keccak256( - abi.encode( - QUALIFICATION_TYPEHASH, - claimHash, - orderDataCallback.targetBlock, - orderDataCallback.maximumBlocksAfterTarget - ) + _nonceValidation(orderData.sponsor, orderData.nonce); + return _resolveOrder( + orderData.sponsor, + orderData.nonce, + orderData.expires, + order_.fillDeadline, + orderDataBytes, + ORDERDATA_OFFSET, + LibBytes.emptyCalldata() ); - - _lockTokens(tokenHash, idsAndAmounts[0][1], orderDataCallback.expires, qualifiedClaimHash); - - OrderData memory orderData = OrderData({ - arbiter: arbiter, - sponsor: sponsor, - nonce: nonce, - expires: expires, - lockTag: lockTag, - inputToken: token, - amount: idsAndAmounts[0][1], - chainId: orderDataCallback.chainId, - tribunal: orderDataCallback.tribunal, - recipient: orderDataCallback.recipient, - settlementToken: orderDataCallback.settlementToken, - minimumAmount: orderDataCallback.minimumAmount, - baselinePriorityFee: orderDataCallback.baselinePriorityFee, - scalingFactor: orderDataCallback.scalingFactor, - decayCurve: orderDataCallback.decayCurve, - salt: orderDataCallback.salt, - targetBlock: orderDataCallback.targetBlock, - maximumBlocksAfterTarget: orderDataCallback.maximumBlocksAfterTarget - }); - // Emit an open event - emit Open(bytes32(nonce), _resolveOrder(sponsor, uint32(orderDataCallback.expires), orderData, '')); - - return this.registerClaim.selector; } + // function registerClaim( + // bytes32 claimHash, // The message hash representing the claim. + // address, /* caller */ // The account initiating the registration. + // address arbiter, // The account tasked with verifying and submitting the claim. + // address sponsor, // The account to source the tokens from. + // uint256 nonce, // A parameter to enforce replay protection, scoped to allocator. + // uint256 expires, // The time at which the claim expires. + // uint256[2][] calldata idsAndAmounts, // The allocated token IDs and amounts. + // bytes calldata allocatorData // Arbitrary data provided by the caller. + // ) external override onlyCompact returns (bytes4) { + // if (idsAndAmounts.length > 1) { + // revert BatchCompactsNotSupported(); + // } + + // // Enforce a nonce where the most significant 96 bits are the nonce and the least significant 160 bits are the sponsor + // uint96 nonceWithoutAddress = _nonceValidation(sponsor, nonce); + // // Set a nonce or revert if it is already used + // _setNonce(sponsor, nonceWithoutAddress); + + // // We trust the compact to check the nonce and that this contract is the allocator connected to the id + // _checkExpiration(expires); + // (bytes12 lockTag, address token) = _separateId(idsAndAmounts[0][0]); + // bytes32 tokenHash = _checkForActiveAllocation(sponsor, lockTag, token); + // _checkForcedWithdrawal(sponsor, expires, lockTag, token); + // _checkBalance(sponsor, idsAndAmounts[0][0], idsAndAmounts[0][1]); // TODO: Should the Compact check this prior to the callback? + + // OderDataCallback memory orderDataCallback = abi.decode(allocatorData, (OderDataCallback)); + // bytes32 qualifiedClaimHash = keccak256( + // abi.encode( + // QUALIFICATION_TYPEHASH, + // claimHash, + // orderDataCallback.targetBlock, + // orderDataCallback.maximumBlocksAfterTarget + // ) + // ); + + // _lockTokens(tokenHash, idsAndAmounts[0][1], orderDataCallback.expires, qualifiedClaimHash); + + // OrderData memory orderData = OrderData({ + // arbiter: arbiter, + // sponsor: sponsor, + // nonce: nonce, + // expires: expires, + // lockTag: lockTag, + // inputToken: token, + // amount: idsAndAmounts[0][1], + // chainId: orderDataCallback.chainId, + // tribunal: orderDataCallback.tribunal, + // recipient: orderDataCallback.recipient, + // settlementToken: orderDataCallback.settlementToken, + // minimumAmount: orderDataCallback.minimumAmount, + // baselinePriorityFee: orderDataCallback.baselinePriorityFee, + // scalingFactor: orderDataCallback.scalingFactor, + // decayCurve: orderDataCallback.decayCurve, + // salt: orderDataCallback.salt, + // targetBlock: orderDataCallback.targetBlock, + // maximumBlocksAfterTarget: orderDataCallback.maximumBlocksAfterTarget + // }); + // // Emit an open event + // emit Open(bytes32(nonce), _resolveOrder(sponsor, uint32(orderDataCallback.expires), orderData, '')); + + // return this.registerClaim.selector; + // } + function authorizeClaim( bytes32 claimHash, // The message hash representing the claim. address, /* arbiter */ // The account tasked with verifying and submitting the claim. @@ -221,91 +291,120 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { return fillerData; } - function _open(OrderData memory orderData_, uint32 fillDeadline_, address sponsor_, bytes memory sponsorSignature_) - internal - { + function _open( + bytes calldata orderData_, + uint256 lockTagOffset, + bytes calldata sponsorSignature_, + bytes32 mandateHash_, + ResolvedCrossChainOrder memory resolvedOrder_ + ) internal { + uint256 nonce_ = uint256(resolvedOrder_.orderId); // Enforce a nonce where the most significant 96 bits are the nonce and the least significant 160 bits are the sponsor - uint96 nonceWithoutAddress = _nonceValidation(sponsor_, orderData_.nonce); + uint96 nonceWithoutAddress = _nonceValidation(resolvedOrder_.user, nonce_); // Set a nonce or revert if it is already used - _setNonce(sponsor_, nonceWithoutAddress); + _setNonce(resolvedOrder_.user, nonceWithoutAddress); // We do not enforce a specific tribunal or arbiter. This will allow to support new arbiters and tribunals after the deployment of the allocator // Going with an immutable arbiter and tribunal would limit support for new chains with a fully decentralized allocator - bytes32 tokenHash = _verifyAllocation( - sponsor_, orderData_.lockTag, orderData_.inputToken, orderData_.amount, orderData_.expires, orderData_.nonce - ); + bytes32 tokenHash = + _verifyAllocation(resolvedOrder_.user, nonce_, resolvedOrder_.openDeadline, orderData_, lockTagOffset); + + console.log('2'); + + uint256 lockTagAbsoluteOffset; + assembly ("memory-safe") { + lockTagAbsoluteOffset := add(orderData_.offset, lockTagOffset) + } // Create the Compact claim hash - bytes32 claimHash = keccak256( - abi.encode( - COMPACT_WITNESS_TYPEHASH, - orderData_.arbiter, - sponsor_, - orderData_.nonce, - orderData_.expires, - orderData_.lockTag, - orderData_.inputToken, - orderData_.amount, - keccak256( - abi.encode( - MANDATE_TYPEHASH, - orderData_.chainId, - orderData_.tribunal, - orderData_.recipient, - fillDeadline_, - orderData_.settlementToken, - orderData_.minimumAmount, - orderData_.baselinePriorityFee, - orderData_.scalingFactor, - keccak256(abi.encodePacked(orderData_.decayCurve)), - orderData_.salt - ) - ) - ) - ); + bytes32 claimHash; + assembly ("memory-safe") { + let m := mload(0x40) + mstore(m, COMPACT_WITNESS_TYPEHASH) + calldatacopy(add(m, 0x20), orderData_.offset, 0x20) // arbiter + mstore(add(m, 0x40), mload(resolvedOrder_)) // sponsor (first item in resolvedOrder_) + mstore(add(m, 0x60), nonce_) // nonce + mstore(add(m, 0x80), add(resolvedOrder_, 0x40)) // Compact.expires (third item in resolvedOrder_) + calldatacopy(add(m, 0xa0), lockTagAbsoluteOffset, 0x60) // lockTag, inputToken, amount + mstore(add(m, 0x100), mandateHash_) + claimHash := keccak256(m, 0x120) + } + + console.log('3'); // We check for the length, which means this could also be triggered by a zero length signature provided in the openFor function. This enables relaying of orders if the claim was registered on the compact. if (sponsorSignature_.length > 0) { bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), _COMPACT_DOMAIN_SEPARATOR, claimHash)); // confirm the signature matches the digest - address signer = ECDSA.recover(digest, sponsorSignature_); - if (sponsor_ != signer) { - revert InvalidSignature(sponsor_, signer); + address signer_ = ECDSA.recover(digest, sponsorSignature_); + if (resolvedOrder_.user != signer_) { + revert InvalidSignature(resolvedOrder_.user, signer_); } } else { // confirm the claim hash is registered on the compact - (bool isActive) = ITheCompact(COMPACT_CONTRACT).isRegistered(sponsor_, claimHash, COMPACT_WITNESS_TYPEHASH); + (bool isActive) = + ITheCompact(COMPACT_CONTRACT).isRegistered(resolvedOrder_.user, claimHash, COMPACT_WITNESS_TYPEHASH); if (!isActive) { - revert InvalidRegistration(sponsor_, claimHash); + revert InvalidRegistration(resolvedOrder_.user, claimHash); } } - bytes32 qualifiedClaimHash = keccak256( - abi.encode(QUALIFICATION_TYPEHASH, claimHash, orderData_.targetBlock, orderData_.maximumBlocksAfterTarget) - ); + console.log('4'); - _lockTokens(tokenHash, orderData_.amount, orderData_.expires, qualifiedClaimHash); + bytes32 qualifiedClaimHash; + assembly ("memory-safe") { + let m := mload(0x40) + mstore(m, QUALIFICATION_TYPEHASH) + mstore(add(m, 0x20), claimHash) + + mstore(add(m, 0x40), 0x0) // clear targetBlock + mstore(add(m, 0x60), 0x0) // clear maximumBlocksAfterTarget + if eq(lockTagOffset, ORDERDATA_OFFSET) { + // if data is of type OrderData, copy targetBlock and maximumBlocksAfterTarget + calldatacopy(add(m, 0x40), add(lockTagAbsoluteOffset, 0x180), 0x40) // targetBlock, maximumBlocksAfterTarget + } + qualifiedClaimHash := keccak256(m, 0x80) + } + uint256 amount; + assembly ("memory-safe") { + // load amount from orderData_ + amount := calldataload(add(lockTagAbsoluteOffset, 0x40)) + let m := mload(0x40) + } + + console.log('5'); + + _lockTokens(tokenHash, amount, resolvedOrder_.openDeadline, qualifiedClaimHash); // Emit an open event - emit Open(bytes32(orderData_.nonce), _resolveOrder(sponsor_, fillDeadline_, orderData_, sponsorSignature_)); + emit Open(bytes32(nonce_), resolvedOrder_); } function _verifyAllocation( - address sponsor, - bytes12 lockTag, - address token, - uint256 amount, - uint256 expires, - uint256 nonce + address sponsor_, + uint256 nonce_, + uint32 openDeadline_, + bytes calldata orderData_, + uint256 lockTagOffset ) internal view returns (bytes32 tokenHash_) { + bytes12 lockTag; + address inputToken; + uint256 amount; + + assembly ("memory-safe") { + lockTag := calldataload(add(orderData_.offset, lockTagOffset)) + inputToken := calldataload(add(orderData_.offset, add(lockTagOffset, 0x20))) + amount := calldataload(add(orderData_.offset, add(lockTagOffset, 0x40))) + } + // Check for a valid allocation - tokenHash_ = _checkForActiveAllocation(sponsor, lockTag, token); - _checkAllocator(lockTag, token); - _checkExpiration(expires); - _checkNonce(nonce); - _checkForcedWithdrawal(sponsor, expires, lockTag, token); - _checkBalance(sponsor, _getTokenId(lockTag, token), amount); + tokenHash_ = _checkForActiveAllocation(sponsor_, lockTag, inputToken); + _checkAllocator(lockTag, inputToken); + _checkExpiration(openDeadline_); + _checkNonce(nonce_); + _checkForcedWithdrawal(sponsor_, openDeadline_, lockTag, inputToken); + _checkBalance(sponsor_, _getTokenId(lockTag, inputToken), amount); return tokenHash_; } @@ -318,71 +417,112 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { function _resolveOrder( address sponsor, + uint256 nonce, + uint256 expires, uint32 fillDeadline, - OrderData memory orderData, - bytes memory sponsorSignature + bytes calldata orderData, + uint256 lockTagOffset, + bytes calldata sponsorSignature ) internal view returns (ResolvedCrossChainOrder memory) { - FillInstruction[] memory fillInstructions = new FillInstruction[](1); - - Mandate memory mandate = Mandate({ - recipient: orderData.recipient, - expires: fillDeadline, - token: orderData.settlementToken, - minimumAmount: orderData.minimumAmount, - baselinePriorityFee: orderData.baselinePriorityFee, - scalingFactor: orderData.scalingFactor, - decayCurve: orderData.decayCurve, - salt: orderData.salt + ResolvedCrossChainOrder memory resolvedOrder = ResolvedCrossChainOrder({ + user: sponsor, + originChainId: block.chainid, + openDeadline: uint32(expires), + fillDeadline: fillDeadline, + orderId: bytes32(nonce), + maxSpent: new Output[](0), + minReceived: new Output[](0), + fillInstructions: new FillInstruction[](0) }); + + Compact memory compact; + + assembly ("memory-safe") { + let m := mload(0x40) + + calldatacopy(m, orderData.offset, 0x20) // arbiter + mstore(add(m, 0x20), sponsor) // sponsor + mstore(add(m, 0x40), nonce) // nonce + mstore(add(m, 0x60), expires) // expires + calldatacopy(add(m, 0x80), add(orderData.offset, lockTagOffset), 0x60) // lockTag, inputToken, amount + + compact := m + mstore(0x40, add(m, 0xe0)) // update free memory pointer + } + Claim memory claim = Claim({ chainId: block.chainid, - compact: Compact({ - arbiter: orderData.arbiter, - sponsor: sponsor, - nonce: orderData.nonce, - expires: orderData.expires, - lockTag: orderData.lockTag, - token: orderData.inputToken, - amount: orderData.amount - }), + compact: compact, sponsorSignature: sponsorSignature, allocatorSignature: '' // No signature required from this allocator, it will verify the claim on chain via ERC1271. }); + Mandate memory mandate; + assembly ("memory-safe") { + let m := mload(0x40) + + let mandateOffset := add(add(orderData.offset, lockTagOffset), 0x60) + + calldatacopy(m, add(mandateOffset, 0x40), 0x20) // recipient + mstore(add(m, 0x20), fillDeadline) // expires + calldatacopy(add(m, 0x40), add(mandateOffset, 0x60), 0xc0) // settlementToken, minimumAmount, baselinePriorityFee, scalingFactor, decayCurve.offset, salt + mstore(add(m, 0xc0), add(m, 0x100)) // update decayCurve.offset to point to the absolute memory location of decayCurve.length + + let decayCurveOffset := calldataload(add(mandateOffset, 0xe0)) + let decayCurveLength := calldataload(decayCurveOffset) + + calldatacopy(add(m, 0x100), decayCurveOffset, add(decayCurveLength, 0x20)) // decayCurve.length, decayCurve.content + + mandate := m + + let length := add(0x120, decayCurveLength) + + mstore(0x40, add(m, length)) // update free memory pointer + } + + uint256 chainId; + bytes32 tribunal; + uint256 targetBlock; + uint256 maximumBlocksAfterTarget; + + assembly ("memory-safe") { + let mandateOffset := add(add(orderData.offset, lockTagOffset), 0x60) + chainId := calldataload(mandateOffset) + tribunal := calldataload(add(mandateOffset, 0x20)) + let isOrderData := eq(lockTagOffset, ORDERDATA_OFFSET) + // Multiply the data calldata value by 0 if OrderDataGasless, as OrderDataGasless does not support targetBlock and maximumBlocksAfterTarget + targetBlock := mul(calldataload(add(mandateOffset, mul(0x120, isOrderData))), isOrderData) // Multiply targetBlock offset by 0 if OrderDataGasless to prevent out of bounds calldata read + maximumBlocksAfterTarget := mul(calldataload(add(mandateOffset, mul(0x140, isOrderData))), isOrderData) // Multiply maximumBlocksAfterTarget offset by 0 if OrderDataGasless to prevent out of bounds calldata read + } + + FillInstruction[] memory fillInstructions = new FillInstruction[](1); fillInstructions[0] = FillInstruction({ - destinationChainId: orderData.chainId, - destinationSettler: _addressToBytes32(orderData.tribunal), - originData: abi.encode(claim, mandate, orderData.targetBlock, orderData.maximumBlocksAfterTarget) + destinationChainId: chainId, + destinationSettler: tribunal, + originData: abi.encode(claim, mandate, targetBlock, maximumBlocksAfterTarget) }); + resolvedOrder.fillInstructions = fillInstructions; Output memory spent = Output({ - token: _addressToBytes32(orderData.settlementToken), + token: _addressToBytes32(mandate.token), amount: type(uint256).max, - recipient: _addressToBytes32(orderData.recipient), - chainId: orderData.chainId + recipient: _addressToBytes32(mandate.recipient), + chainId: chainId }); + Output[] memory maxSpent = new Output[](1); + maxSpent[0] = spent; + resolvedOrder.maxSpent = maxSpent; + Output memory received = Output({ - token: _addressToBytes32(orderData.inputToken), - amount: orderData.amount, + token: _addressToBytes32(compact.token), + amount: compact.amount, recipient: bytes32(0), chainId: block.chainid }); - - Output[] memory maxSpent = new Output[](1); - maxSpent[0] = spent; Output[] memory minReceived = new Output[](1); minReceived[0] = received; + resolvedOrder.minReceived = minReceived; - ResolvedCrossChainOrder memory resolvedOrder = ResolvedCrossChainOrder({ - user: sponsor, - originChainId: block.chainid, - openDeadline: uint32(orderData.expires), - fillDeadline: fillDeadline, - orderId: bytes32(orderData.nonce), - maxSpent: maxSpent, - minReceived: minReceived, - fillInstructions: fillInstructions - }); return resolvedOrder; } @@ -424,11 +564,46 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { } } + function _mandateHash(uint256 mandateOffset, uint32 fillDeadline) internal pure returns (bytes32 mandateHash_) { + // total mandate length: x140 + decayCurve.length + assembly ("memory-safe") { + let m := mload(0x40) + + let decayCurveOffset := calldataload(add(mandateOffset, 0xe0)) + let decayCurveLength := calldataload(decayCurveOffset) + + let l := add(0x140, decayCurveLength) // length if decayCurve.length was 0 (0x120) + decayCurve.length + + mstore(m, MANDATE_TYPEHASH) + calldatacopy(add(m, 0x20), mandateOffset, 0x60) // chainid, tribunal and recipient + mstore(add(m, 0x80), fillDeadline) + calldatacopy(add(m, 0xa0), add(mandateOffset, 0x60), 0xc0) // settlementToken, minimumAmount, baselinePriorityFee, scalingFactor, decayCurve.offset, salt + mstore(add(m, 0x120), add(m, 0x160)) // update decayCurve.offset to point to the absolute memory location of decayCurve.length + calldatacopy(add(m, 0x160), decayCurveOffset, add(decayCurveLength, 0x20)) // decayCurve.length, decayCurve.content + + mandateHash_ := keccak256(m, add(l, 0x20)) // mandate typehash + mandate data length + } + + // 0x00: MANDATE_TYPEHASH + // 0x20: chainid + // 0x40: tribunal + // 0x60: recipient + // 0x80: fillDeadline + // 0xa0: settlementToken + // 0xc0: minimumAmount + // 0xe0: baselinePriorityFee + // 0x100: scalingFactor + // 0x120: decayCurve.offset + // 0x140: salt + // 0x160: decayCurve.length + // 0x180: decayCurve.content + } + function _convertGaslessOrderData( address sponsor_, uint256 nonce_, uint32 openDeadline_, - OrderDataGasless memory orderDataGasless_ + OrderDataGasless calldata orderDataGasless_ ) internal pure returns (OrderData memory orderData_) { orderData_ = OrderData({ arbiter: orderDataGasless_.arbiter, From bec325a59bd66ddf547b5746c6ec294eedcaab05 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Wed, 2 Jul 2025 19:23:19 +0200 Subject: [PATCH 26/63] Started working on tests --- test/ERC7683Allocator.t.sol | 1415 +++++++++++++++++++---------------- 1 file changed, 765 insertions(+), 650 deletions(-) diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index d742078..110a9fa 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -112,6 +112,7 @@ abstract contract CreateHash is MocksSetup { } function _hashCompact(Compact memory data, Mandate memory mandate) internal view returns (bytes32 compactHash) { + bytes32 mandateHash = _hashMandate(mandate); return keccak256( abi.encode( keccak256(bytes(compactWitnessTypeString)), @@ -122,21 +123,25 @@ abstract contract CreateHash is MocksSetup { data.lockTag, data.token, data.amount, - keccak256( - abi.encode( - keccak256(bytes(mandateTypeString)), - defaultOutputChainId, - tribunal, - mandate.recipient, - mandate.expires, - mandate.token, - mandate.minimumAmount, - mandate.baselinePriorityFee, - mandate.scalingFactor, - keccak256(abi.encodePacked(mandate.decayCurve)), - mandate.salt - ) - ) + mandateHash + ) + ); + } + + function _hashMandate(Mandate memory mandate) internal view returns (bytes32) { + return keccak256( + abi.encode( + keccak256(bytes(mandateTypeString)), + defaultOutputChainId, + tribunal, + mandate.recipient, + mandate.expires, + mandate.token, + mandate.minimumAmount, + mandate.baselinePriorityFee, + mandate.scalingFactor, + keccak256(abi.encodePacked(mandate.decayCurve)), + mandate.salt ) ); } @@ -397,233 +402,6 @@ abstract contract Deposited is MocksSetup { } } -contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { - function test_revert_InvalidOrderDataType() public { - // Order data type is invalid - bytes32 falseOrderDataType = keccak256('false'); - - vm.prank(user); - vm.expectRevert( - abi.encodeWithSelector( - IERC7683Allocator.InvalidOrderDataType.selector, - falseOrderDataType, - erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH() - ) - ); - (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = - _getGaslessCrossChainOrder(); - falseGaslessCrossChainOrder.orderDataType = falseOrderDataType; - erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); - } - - function test_revert_InvalidDecoding() public { - // Decoding fails because of additional data - vm.prank(user); - vm.expectRevert(); - (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = - _getGaslessCrossChainOrder(); - falseGaslessCrossChainOrder.orderData = abi.encode(falseGaslessCrossChainOrder.orderData, uint8(1)); - erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); - } - - function test_revert_InvalidOriginSettler() public { - // Origin settler is not the allocator - address falseOriginSettler = makeAddr('falseOriginSettler'); - vm.expectRevert( - abi.encodeWithSelector( - IERC7683Allocator.InvalidOriginSettler.selector, falseOriginSettler, address(erc7683Allocator) - ) - ); - (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = - _getGaslessCrossChainOrder( - falseOriginSettler, - _getCompact(), - _getMandate(), - block.chainid, - ORDERDATA_GASLESS_TYPEHASH, - address(erc7683Allocator), - userPK - ); - vm.prank(user); - erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); - } - - function test_revert_InvalidNonce() public { - // Nonce is invalid because the least significant 160 bits are not the sponsor address - Compact memory compact_ = _getCompact(); - compact_.nonce = uint256(bytes32(abi.encodePacked(uint96(1), attacker))); - vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidNonce.selector, compact_.nonce)); - (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = - _getGaslessCrossChainOrder( - address(erc7683Allocator), - compact_, - _getMandate(), - block.chainid, - ORDERDATA_GASLESS_TYPEHASH, - address(erc7683Allocator), - userPK - ); - vm.prank(user); - erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); - } - - function test_revert_InvalidSponsorSignature() public { - // Sponsor signature is invalid - - // Deposit tokens - vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); - compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); - vm.stopPrank(); - - // Create a malicious signature - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = - _getGaslessCrossChainOrder( - address(erc7683Allocator), - _getCompact(), - _getMandate(), - block.chainid, - ORDERDATA_GASLESS_TYPEHASH, - address(compactContract), - attackerPK - ); - vm.prank(user); - vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidSignature.selector, user, attacker)); - erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); - } - - function test_successful_userHimself() public { - // Deposit tokens - vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); - compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); - vm.stopPrank(); - - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = - _getGaslessCrossChainOrder(); - IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); - IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); - IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); - maxSpent[0] = IOriginSettler.Output({ - token: bytes32(uint256(uint160(defaultOutputToken))), - amount: type(uint256).max, - recipient: bytes32(uint256(uint160(user))), - chainId: defaultOutputChainId - }); - minReceived[0] = IOriginSettler.Output({ - token: bytes32(uint256(uint160(address(usdc)))), - amount: defaultAmount, - recipient: '', - chainId: block.chainid - }); - TribunalClaim memory claim = TribunalClaim({ - chainId: block.chainid, - compact: _getCompact(), - sponsorSignature: sponsorSignature, - allocatorSignature: '' - }); - fillInstructions[0] = IOriginSettler.FillInstruction({ - destinationChainId: defaultOutputChainId, - destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, _getMandate(), uint256(0), uint256(0)) - }); - - IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ - user: user, - originChainId: block.chainid, - openDeadline: uint32(_getClaimExpiration()), - fillDeadline: uint32(_getFillExpiration()), - orderId: bytes32(defaultNonce), - maxSpent: maxSpent, - minReceived: minReceived, - fillInstructions: fillInstructions - }); - vm.prank(user); - vm.expectEmit(true, false, false, true, address(erc7683Allocator)); - emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); - erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); - } - - function test_successful_relayed() public { - // Deposit tokens - vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); - compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); - vm.stopPrank(); - - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = - _getGaslessCrossChainOrder(); - IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); - IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); - IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); - maxSpent[0] = IOriginSettler.Output({ - token: bytes32(uint256(uint160(defaultOutputToken))), - amount: type(uint256).max, - recipient: bytes32(uint256(uint160(user))), - chainId: defaultOutputChainId - }); - minReceived[0] = IOriginSettler.Output({ - token: bytes32(uint256(uint160(address(usdc)))), - amount: defaultAmount, - recipient: '', - chainId: block.chainid - }); - TribunalClaim memory claim = TribunalClaim({ - chainId: block.chainid, - compact: _getCompact(), - sponsorSignature: sponsorSignature, - allocatorSignature: '' - }); - fillInstructions[0] = IOriginSettler.FillInstruction({ - destinationChainId: defaultOutputChainId, - destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, _getMandate(), uint256(0), uint256(0)) - }); - - IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ - user: user, - originChainId: block.chainid, - openDeadline: uint32(_getClaimExpiration()), - fillDeadline: uint32(_getFillExpiration()), - orderId: bytes32(defaultNonce), - maxSpent: maxSpent, - minReceived: minReceived, - fillInstructions: fillInstructions - }); - vm.prank(makeAddr('filler')); - vm.expectEmit(true, false, false, true, address(erc7683Allocator)); - emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); - erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); - } - - function test_revert_NonceAlreadyInUse() public { - // Nonce is already used - - // Deposit tokens - vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); - compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); - vm.stopPrank(); - - // use the nonce once - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = - _getGaslessCrossChainOrder(); - vm.prank(user); - erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); - - // try to use the nonce again - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder2, bytes memory sponsorSignature2) = - _getGaslessCrossChainOrder(); - vm.prank(user); - vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.NonceAlreadyInUse.selector, defaultNonce)); - erc7683Allocator.openFor(gaslessCrossChainOrder2, sponsorSignature2, ''); - } -} - contract ERC7683Allocator_open is OnChainCrossChainOrderData { function test_revert_InvalidOrderDataType() public { // Order data type is invalid @@ -646,11 +424,11 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_ = _getOnChainCrossChainOrder(); vm.prank(attacker); - vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidSignature.selector, user, attacker)); + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidCaller.selector, attacker, user)); erc7683Allocator.open(onChainCrossChainOrder_); } - function test_revert_InvalidRegistration_Unavailable() public { + function test_revert_InvalidRegistration() public { // we deposit tokens vm.startPrank(user); usdc.mint(user, defaultAmount); @@ -734,409 +512,746 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { } } -contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, GaslessCrossChainOrderData { - function setUp() public override(OnChainCrossChainOrderData, GaslessCrossChainOrderData) { - super.setUp(); - } - - function test_revert_InvalidLock() public { - // Deposit tokens - vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); - compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); - - // register a claim - Compact memory compact_ = _getCompact(); - Mandate memory mandate_ = _getMandate(); - - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); - - address filler = makeAddr('filler'); - vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); - vm.assertEq(compactContract.balanceOf(filler, usdcId), 0); - - vm.stopPrank(); - - // we do NOT open the order or lock the tokens - - // claim should be fail, because we mess with the nonce - Component memory component = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); - Component[] memory components = new Component[](1); - components[0] = component; - Claim memory claim = Claim({ - allocatorData: abi.encode( - erc7683Allocator.QUALIFICATION_TYPEHASH(), defaultTargetBlock, defaultMaximumBlocksAfterTarget - ), - sponsorSignature: '', - sponsor: user, - nonce: defaultNonce, - expires: compact_.expires, - witness: keccak256(abi.encode(keccak256(bytes(mandateTypeString)), mandate_)), - witnessTypestring: witnessTypeString, - id: usdcId, - allocatedAmount: defaultAmount, - claimants: components - }); - vm.prank(arbiter); - vm.expectRevert(abi.encodeWithSelector(0x8baa579f)); // check for the InvalidSignature() error in the Compact contract - compactContract.claim(claim); - - vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); - vm.assertEq(compactContract.balanceOf(filler, usdcId), 0); - } - - function test_isValidSignature_successful_open() public { - // Deposit tokens - vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); - compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); - - // register a claim - Compact memory compact_ = _getCompact(); - Mandate memory mandate_ = _getMandate(); - - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); - - address filler = makeAddr('filler'); - vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); - vm.assertEq(compactContract.balanceOf(filler, usdcId), 0); - - // we open the order and lock the tokens - (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); - erc7683Allocator.open(onChainCrossChainOrder_); - vm.stopPrank(); - - // claim should be successful - Component memory component = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); - Component[] memory components = new Component[](1); - components[0] = component; - Claim memory claim = Claim({ - allocatorData: abi.encode( - erc7683Allocator.QUALIFICATION_TYPEHASH(), defaultTargetBlock, defaultMaximumBlocksAfterTarget - ), - sponsorSignature: '', - sponsor: user, - nonce: defaultNonce, - expires: compact_.expires, - witness: keccak256( - abi.encode( - keccak256(bytes(mandateTypeString)), - defaultOutputChainId, - tribunal, - mandate_.recipient, - mandate_.expires, - mandate_.token, - mandate_.minimumAmount, - mandate_.baselinePriorityFee, - mandate_.scalingFactor, - keccak256(abi.encodePacked(mandate_.decayCurve)), - mandate_.salt - ) - ), - witnessTypestring: witnessTypeString, - id: usdcId, - allocatedAmount: defaultAmount, - claimants: components - }); - vm.prank(arbiter); - compactContract.claim(claim); - - vm.assertEq(compactContract.balanceOf(user, usdcId), 0); - vm.assertEq(compactContract.balanceOf(filler, usdcId), defaultAmount); - } - - function test_isValidSignature_successful_openFor() public { - // Deposit tokens - vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); - compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); - - // register a claim - Compact memory compact_ = _getCompact(); - Mandate memory mandate_ = _getMandate(); - - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); - - address filler = makeAddr('filler'); - vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); - vm.assertEq(compactContract.balanceOf(filler, usdcId), 0); - - // we open the order and lock the tokens - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = - _getGaslessCrossChainOrder(); - erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); - vm.stopPrank(); - - // claim should be successful - Component memory component = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); - Component[] memory components = new Component[](1); - components[0] = component; - Claim memory claim = Claim({ - allocatorData: abi.encode(erc7683Allocator.QUALIFICATION_TYPEHASH(), uint256(0), uint256(0)), - sponsorSignature: '', - sponsor: user, - nonce: defaultNonce, - expires: compact_.expires, - witness: keccak256( - abi.encode( - keccak256(bytes(mandateTypeString)), - defaultOutputChainId, - tribunal, - mandate_.recipient, - mandate_.expires, - mandate_.token, - mandate_.minimumAmount, - mandate_.baselinePriorityFee, - mandate_.scalingFactor, - keccak256(abi.encodePacked(mandate_.decayCurve)), - mandate_.salt - ) - ), - witnessTypestring: witnessTypeString, - id: usdcId, - allocatedAmount: defaultAmount, - claimants: components - }); - vm.prank(arbiter); - compactContract.claim(claim); - - vm.assertEq(compactContract.balanceOf(user, usdcId), 0); - vm.assertEq(compactContract.balanceOf(filler, usdcId), defaultAmount); - } -} - -contract ERC7683Allocator_resolveFor is GaslessCrossChainOrderData { - function test_resolve_successful() public { - // WITH THE CURRENT ERC7683 DESIGN, THE SPONSOR SIGNATURE IS NOT PROVIDED TO THE RESOLVE FUNCTION - // WHILE THE ResolvedCrossChainOrder WITHOUT THE SIGNATURE COULD STILL BE USED TO SIMULATE THE FILL, - // ACTUALLY USING THIS DATA WOULD RESULT IN A LOSS OF THE REWARD TOKENS FOR THE FILLER. - // THIS FEELS RISKY. - // THE CURRENT ALTERNATIVE WOULD BE HAVE THE INPUT SIGNATURE BEING LEFT EMPTY AND INSTEAD BE PROVIDED IN THE THE orderData OF THE GaslessCrossChainOrderData. - // THIS IS BOTH NOT IDEAL, SO CURRENTLY CHECKING FOR A SOLUTION. - - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = - _getGaslessCrossChainOrder(); - IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); - IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); - IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); - maxSpent[0] = IOriginSettler.Output({ - token: bytes32(uint256(uint160(defaultOutputToken))), - amount: type(uint256).max, - recipient: bytes32(uint256(uint160(user))), - chainId: defaultOutputChainId - }); - minReceived[0] = IOriginSettler.Output({ - token: bytes32(uint256(uint160(address(usdc)))), - amount: defaultAmount, - recipient: '', - chainId: block.chainid - }); - TribunalClaim memory claim = TribunalClaim({ - chainId: block.chainid, - compact: _getCompact(), - sponsorSignature: '', // sponsorSignature, // THE SIGNATURE MUST BE ADDED MANUALLY BY THE FILLER WITH THE CURRENT SYSTEM, BEFORE FILLING THE ORDER ON THE TARGET CHAIN - allocatorSignature: '' - }); - fillInstructions[0] = IOriginSettler.FillInstruction({ - destinationChainId: defaultOutputChainId, - destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, _getMandate(), uint256(0), uint256(0)) - }); - - IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ - user: user, - originChainId: block.chainid, - openDeadline: uint32(_getClaimExpiration()), - fillDeadline: uint32(_getFillExpiration()), - orderId: bytes32(defaultNonce), - maxSpent: maxSpent, - minReceived: minReceived, - fillInstructions: fillInstructions - }); - IOriginSettler.ResolvedCrossChainOrder memory resolved = - erc7683Allocator.resolveFor(gaslessCrossChainOrder_, ''); - assertEq(resolved.user, resolvedCrossChainOrder.user); - assertEq(resolved.originChainId, resolvedCrossChainOrder.originChainId); - assertEq(resolved.openDeadline, resolvedCrossChainOrder.openDeadline); - assertEq(resolved.fillDeadline, resolvedCrossChainOrder.fillDeadline); - assertEq(resolved.orderId, resolvedCrossChainOrder.orderId); - assertEq(resolved.maxSpent.length, resolvedCrossChainOrder.maxSpent.length); - assertEq(resolved.maxSpent[0].token, resolvedCrossChainOrder.maxSpent[0].token); - assertEq(resolved.maxSpent[0].amount, resolvedCrossChainOrder.maxSpent[0].amount); - assertEq(resolved.maxSpent[0].recipient, resolvedCrossChainOrder.maxSpent[0].recipient); - assertEq(resolved.maxSpent[0].chainId, resolvedCrossChainOrder.maxSpent[0].chainId); - assertEq(resolved.minReceived.length, resolvedCrossChainOrder.minReceived.length); - assertEq(resolved.minReceived[0].token, resolvedCrossChainOrder.minReceived[0].token); - assertEq(resolved.minReceived[0].amount, resolvedCrossChainOrder.minReceived[0].amount); - assertEq(resolved.minReceived[0].recipient, resolvedCrossChainOrder.minReceived[0].recipient); - assertEq(resolved.minReceived[0].chainId, resolvedCrossChainOrder.minReceived[0].chainId); - assertEq(resolved.fillInstructions.length, resolvedCrossChainOrder.fillInstructions.length); - assertEq( - resolved.fillInstructions[0].destinationChainId, - resolvedCrossChainOrder.fillInstructions[0].destinationChainId - ); - assertEq( - resolved.fillInstructions[0].destinationSettler, - resolvedCrossChainOrder.fillInstructions[0].destinationSettler - ); - assertEq(resolved.fillInstructions[0].originData, resolvedCrossChainOrder.fillInstructions[0].originData); - } -} - -contract ERC7683Allocator_resolve is OnChainCrossChainOrderData { - function test_resolve_successful() public { - (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); - IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); - IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); - IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); - maxSpent[0] = IOriginSettler.Output({ - token: bytes32(uint256(uint160(defaultOutputToken))), - amount: type(uint256).max, - recipient: bytes32(uint256(uint160(user))), - chainId: defaultOutputChainId - }); - minReceived[0] = IOriginSettler.Output({ - token: bytes32(uint256(uint160(address(usdc)))), - amount: defaultAmount, - recipient: '', - chainId: block.chainid - }); - TribunalClaim memory claim = TribunalClaim({ - chainId: block.chainid, - compact: _getCompact(), - sponsorSignature: '', - allocatorSignature: '' - }); - fillInstructions[0] = IOriginSettler.FillInstruction({ - destinationChainId: defaultOutputChainId, - destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, _getMandate(), defaultTargetBlock, defaultMaximumBlocksAfterTarget) - }); - - IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ - user: user, - originChainId: block.chainid, - openDeadline: uint32(_getClaimExpiration()), - fillDeadline: uint32(_getFillExpiration()), - orderId: bytes32(defaultNonce), - maxSpent: maxSpent, - minReceived: minReceived, - fillInstructions: fillInstructions - }); - IOriginSettler.ResolvedCrossChainOrder memory resolved = erc7683Allocator.resolve(onChainCrossChainOrder_); - assertEq(resolved.user, resolvedCrossChainOrder.user); - assertEq(resolved.originChainId, resolvedCrossChainOrder.originChainId); - assertEq(resolved.openDeadline, resolvedCrossChainOrder.openDeadline); - assertEq(resolved.fillDeadline, resolvedCrossChainOrder.fillDeadline); - assertEq(resolved.orderId, resolvedCrossChainOrder.orderId); - assertEq(resolved.maxSpent.length, resolvedCrossChainOrder.maxSpent.length); - assertEq(resolved.maxSpent[0].token, resolvedCrossChainOrder.maxSpent[0].token); - assertEq(resolved.maxSpent[0].amount, resolvedCrossChainOrder.maxSpent[0].amount); - assertEq(resolved.maxSpent[0].recipient, resolvedCrossChainOrder.maxSpent[0].recipient); - assertEq(resolved.maxSpent[0].chainId, resolvedCrossChainOrder.maxSpent[0].chainId); - assertEq(resolved.minReceived.length, resolvedCrossChainOrder.minReceived.length); - assertEq(resolved.minReceived[0].token, resolvedCrossChainOrder.minReceived[0].token); - assertEq(resolved.minReceived[0].amount, resolvedCrossChainOrder.minReceived[0].amount); - assertEq(resolved.minReceived[0].recipient, resolvedCrossChainOrder.minReceived[0].recipient); - assertEq(resolved.minReceived[0].chainId, resolvedCrossChainOrder.minReceived[0].chainId); - assertEq(resolved.fillInstructions.length, resolvedCrossChainOrder.fillInstructions.length); - assertEq( - resolved.fillInstructions[0].destinationChainId, - resolvedCrossChainOrder.fillInstructions[0].destinationChainId - ); - assertEq( - resolved.fillInstructions[0].destinationSettler, - resolvedCrossChainOrder.fillInstructions[0].destinationSettler - ); - assertEq(resolved.fillInstructions[0].originData, resolvedCrossChainOrder.fillInstructions[0].originData); - } -} - -contract ERC7683Allocator_getCompactWitnessTypeString is MocksSetup { - function test_getCompactWitnessTypeString() public view { - assertEq( - erc7683Allocator.getCompactWitnessTypeString(), - 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt))' - ); - } -} - -contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData { - function test_revert_invalidNonce(uint256 nonce_) public { - address expectedSponsor; - assembly ("memory-safe") { - expectedSponsor := shr(96, nonce_) - } - vm.assume(user != expectedSponsor); - - vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidNonce.selector, nonce_)); - erc7683Allocator.checkNonce(user, nonce_); - } - - function test_checkNonce_unused(uint96 nonce_) public view { - address sponsor = user; - uint256 nonce; - assembly ("memory-safe") { - nonce := or(shl(96, sponsor), shr(160, shl(160, nonce_))) - } - assertEq(erc7683Allocator.checkNonce(sponsor, nonce), true); - } - - function test_checkNonce_used() public { - // Deposit tokens - vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); - compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); - - // register a claim - Compact memory compact_ = _getCompact(); - Mandate memory mandate_ = _getMandate(); - - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); - - (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); - erc7683Allocator.open(onChainCrossChainOrder_); - - vm.assertEq(erc7683Allocator.checkNonce(user, defaultNonce), false); - vm.stopPrank(); - } - - function test_checkNonce_fuzz(uint8 nonce_) public { - uint256 nonce = uint256(bytes32(abi.encodePacked(user, uint96(nonce_)))); - - bool sameNonce = nonce == defaultNonce; - - // Deposit tokens - vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); - compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); - - // register a claim - Compact memory compact_ = _getCompact(); - Mandate memory mandate_ = _getMandate(); - - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); - - (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); - erc7683Allocator.open(onChainCrossChainOrder_); - - vm.assertEq(erc7683Allocator.checkNonce(user, nonce), !sameNonce); - - vm.stopPrank(); - } -} +// contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { +// function test_revert_InvalidOrderDataType() public { +// // Order data type is invalid +// bytes32 falseOrderDataType = keccak256('false'); + +// vm.prank(user); +// vm.expectRevert( +// abi.encodeWithSelector( +// IERC7683Allocator.InvalidOrderDataType.selector, +// falseOrderDataType, +// erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH() +// ) +// ); +// (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = +// _getGaslessCrossChainOrder(); +// falseGaslessCrossChainOrder.orderDataType = falseOrderDataType; +// erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); +// } + +// function test_revert_InvalidDecoding() public { +// // Decoding fails because of additional data +// vm.prank(user); +// vm.expectRevert(); +// (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = +// _getGaslessCrossChainOrder(); +// falseGaslessCrossChainOrder.orderData = abi.encode(falseGaslessCrossChainOrder.orderData, uint8(1)); +// erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); +// } + +// function test_revert_InvalidOriginSettler() public { +// // Origin settler is not the allocator +// address falseOriginSettler = makeAddr('falseOriginSettler'); +// vm.expectRevert( +// abi.encodeWithSelector( +// IERC7683Allocator.InvalidOriginSettler.selector, falseOriginSettler, address(erc7683Allocator) +// ) +// ); +// (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = +// _getGaslessCrossChainOrder( +// falseOriginSettler, +// _getCompact(), +// _getMandate(), +// block.chainid, +// ORDERDATA_GASLESS_TYPEHASH, +// address(erc7683Allocator), +// userPK +// ); +// vm.prank(user); +// erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); +// } + +// function test_revert_InvalidNonce() public { +// // Nonce is invalid because the least significant 160 bits are not the sponsor address +// Compact memory compact_ = _getCompact(); +// compact_.nonce = uint256(bytes32(abi.encodePacked(uint96(1), attacker))); +// vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidNonce.selector, compact_.nonce)); +// (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = +// _getGaslessCrossChainOrder( +// address(erc7683Allocator), +// compact_, +// _getMandate(), +// block.chainid, +// ORDERDATA_GASLESS_TYPEHASH, +// address(erc7683Allocator), +// userPK +// ); +// vm.prank(user); +// erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); +// } + +// function test_revert_InvalidSponsorSignature() public { +// // Sponsor signature is invalid + +// // Deposit tokens +// vm.startPrank(user); +// usdc.mint(user, defaultAmount); +// usdc.approve(address(compactContract), defaultAmount); +// compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); +// vm.stopPrank(); + +// // Create a malicious signature +// (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = +// _getGaslessCrossChainOrder( +// address(erc7683Allocator), +// _getCompact(), +// _getMandate(), +// block.chainid, +// ORDERDATA_GASLESS_TYPEHASH, +// address(compactContract), +// attackerPK +// ); +// vm.prank(user); +// vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidSignature.selector, user, attacker)); +// erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); +// } + +// function test_successful_userHimself() public { +// // Deposit tokens +// vm.startPrank(user); +// usdc.mint(user, defaultAmount); +// usdc.approve(address(compactContract), defaultAmount); +// compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); +// vm.stopPrank(); + +// (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = +// _getGaslessCrossChainOrder(); +// IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); +// IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); +// IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); +// maxSpent[0] = IOriginSettler.Output({ +// token: bytes32(uint256(uint160(defaultOutputToken))), +// amount: type(uint256).max, +// recipient: bytes32(uint256(uint160(user))), +// chainId: defaultOutputChainId +// }); +// minReceived[0] = IOriginSettler.Output({ +// token: bytes32(uint256(uint160(address(usdc)))), +// amount: defaultAmount, +// recipient: '', +// chainId: block.chainid +// }); +// TribunalClaim memory claim = TribunalClaim({ +// chainId: block.chainid, +// compact: _getCompact(), +// sponsorSignature: sponsorSignature, +// allocatorSignature: '' +// }); +// fillInstructions[0] = IOriginSettler.FillInstruction({ +// destinationChainId: defaultOutputChainId, +// destinationSettler: bytes32(uint256(uint160(tribunal))), +// originData: abi.encode(claim, _getMandate(), uint256(0), uint256(0)) +// }); + +// IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ +// user: user, +// originChainId: block.chainid, +// openDeadline: uint32(_getClaimExpiration()), +// fillDeadline: uint32(_getFillExpiration()), +// orderId: bytes32(defaultNonce), +// maxSpent: maxSpent, +// minReceived: minReceived, +// fillInstructions: fillInstructions +// }); +// vm.prank(user); +// vm.expectEmit(true, false, false, true, address(erc7683Allocator)); +// emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); +// erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); +// } + +// function test_successful_relayed() public { +// // Deposit tokens +// vm.startPrank(user); +// usdc.mint(user, defaultAmount); +// usdc.approve(address(compactContract), defaultAmount); +// compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); +// vm.stopPrank(); + +// (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = +// _getGaslessCrossChainOrder(); +// IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); +// IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); +// IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); +// maxSpent[0] = IOriginSettler.Output({ +// token: bytes32(uint256(uint160(defaultOutputToken))), +// amount: type(uint256).max, +// recipient: bytes32(uint256(uint160(user))), +// chainId: defaultOutputChainId +// }); +// minReceived[0] = IOriginSettler.Output({ +// token: bytes32(uint256(uint160(address(usdc)))), +// amount: defaultAmount, +// recipient: '', +// chainId: block.chainid +// }); +// TribunalClaim memory claim = TribunalClaim({ +// chainId: block.chainid, +// compact: _getCompact(), +// sponsorSignature: sponsorSignature, +// allocatorSignature: '' +// }); +// fillInstructions[0] = IOriginSettler.FillInstruction({ +// destinationChainId: defaultOutputChainId, +// destinationSettler: bytes32(uint256(uint160(tribunal))), +// originData: abi.encode(claim, _getMandate(), uint256(0), uint256(0)) +// }); + +// IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ +// user: user, +// originChainId: block.chainid, +// openDeadline: uint32(_getClaimExpiration()), +// fillDeadline: uint32(_getFillExpiration()), +// orderId: bytes32(defaultNonce), +// maxSpent: maxSpent, +// minReceived: minReceived, +// fillInstructions: fillInstructions +// }); +// vm.prank(makeAddr('filler')); +// vm.expectEmit(true, false, false, true, address(erc7683Allocator)); +// emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); +// erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); +// } + +// function test_revert_NonceAlreadyInUse() public { +// // Nonce is already used + +// // Deposit tokens +// vm.startPrank(user); +// usdc.mint(user, defaultAmount); +// usdc.approve(address(compactContract), defaultAmount); +// compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); +// vm.stopPrank(); + +// // use the nonce once +// (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = +// _getGaslessCrossChainOrder(); +// vm.prank(user); +// erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); + +// // try to use the nonce again +// (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder2, bytes memory sponsorSignature2) = +// _getGaslessCrossChainOrder(); +// vm.prank(user); +// vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.NonceAlreadyInUse.selector, defaultNonce)); +// erc7683Allocator.openFor(gaslessCrossChainOrder2, sponsorSignature2, ''); +// } +// } + +// contract ERC7683Allocator_open is OnChainCrossChainOrderData { +// function test_revert_InvalidOrderDataType() public { +// // Order data type is invalid +// bytes32 falseOrderDataType = keccak256('false'); +// IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_ = _getOnChainCrossChainOrder(); +// onChainCrossChainOrder_.orderDataType = falseOrderDataType; + +// vm.prank(user); +// vm.expectRevert( +// abi.encodeWithSelector( +// IERC7683Allocator.InvalidOrderDataType.selector, +// falseOrderDataType, +// erc7683Allocator.ORDERDATA_TYPEHASH() +// ) +// ); +// erc7683Allocator.open(onChainCrossChainOrder_); +// } + +// function test_revert_InvalidSponsor() public { +// IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_ = _getOnChainCrossChainOrder(); + +// vm.prank(attacker); +// vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidSignature.selector, user, attacker)); +// erc7683Allocator.open(onChainCrossChainOrder_); +// } + +// function test_revert_InvalidRegistration_Unavailable() public { +// // we deposit tokens +// vm.startPrank(user); +// usdc.mint(user, defaultAmount); +// usdc.approve(address(compactContract), defaultAmount); +// compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + +// // we do NOT register a claim + +// vm.stopPrank(); + +// (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); + +// Compact memory compact_ = _getCompact(); +// Mandate memory mandate_ = _getMandate(); +// bytes32 claimHash = _hashCompact(compact_, mandate_); + +// vm.prank(user); +// vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidRegistration.selector, user, claimHash)); +// erc7683Allocator.open(onChainCrossChainOrder_); +// } + +// function test_successful() public { +// // Deposit tokens +// vm.startPrank(user); +// usdc.mint(user, defaultAmount); +// usdc.approve(address(compactContract), defaultAmount); +// compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + +// // register a claim +// Compact memory compact_ = _getCompact(); +// Mandate memory mandate_ = _getMandate(); + +// bytes32 claimHash = _hashCompact(compact_, mandate_); +// bytes32 typeHash = _getTypeHash(); +// compactContract.register(claimHash, typeHash); + +// vm.stopPrank(); + +// (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); +// IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); +// IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); +// IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); +// maxSpent[0] = IOriginSettler.Output({ +// token: bytes32(uint256(uint160(defaultOutputToken))), +// amount: type(uint256).max, +// recipient: bytes32(uint256(uint160(user))), +// chainId: defaultOutputChainId +// }); +// minReceived[0] = IOriginSettler.Output({ +// token: bytes32(uint256(uint160(address(usdc)))), +// amount: defaultAmount, +// recipient: '', +// chainId: block.chainid +// }); +// TribunalClaim memory claim = TribunalClaim({ +// chainId: block.chainid, +// compact: _getCompact(), +// sponsorSignature: '', +// allocatorSignature: '' +// }); +// fillInstructions[0] = IOriginSettler.FillInstruction({ +// destinationChainId: defaultOutputChainId, +// destinationSettler: bytes32(uint256(uint160(tribunal))), +// originData: abi.encode(claim, _getMandate(), defaultTargetBlock, defaultMaximumBlocksAfterTarget) +// }); + +// IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ +// user: user, +// originChainId: block.chainid, +// openDeadline: uint32(_getClaimExpiration()), +// fillDeadline: uint32(_getFillExpiration()), +// orderId: bytes32(defaultNonce), +// maxSpent: maxSpent, +// minReceived: minReceived, +// fillInstructions: fillInstructions +// }); +// vm.prank(user); +// vm.expectEmit(true, false, false, true, address(erc7683Allocator)); +// emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); +// erc7683Allocator.open(onChainCrossChainOrder_); +// } +// } + +// contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, GaslessCrossChainOrderData { +// function setUp() public override(OnChainCrossChainOrderData, GaslessCrossChainOrderData) { +// super.setUp(); +// } + +// function test_revert_InvalidLock() public { +// // Deposit tokens +// vm.startPrank(user); +// usdc.mint(user, defaultAmount); +// usdc.approve(address(compactContract), defaultAmount); +// compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + +// // register a claim +// Compact memory compact_ = _getCompact(); +// Mandate memory mandate_ = _getMandate(); + +// bytes32 claimHash = _hashCompact(compact_, mandate_); +// bytes32 typeHash = _getTypeHash(); +// compactContract.register(claimHash, typeHash); + +// address filler = makeAddr('filler'); +// vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); +// vm.assertEq(compactContract.balanceOf(filler, usdcId), 0); + +// vm.stopPrank(); + +// // we do NOT open the order or lock the tokens + +// // claim should be fail, because we mess with the nonce +// Component memory component = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); +// Component[] memory components = new Component[](1); +// components[0] = component; +// Claim memory claim = Claim({ +// allocatorData: abi.encode( +// erc7683Allocator.QUALIFICATION_TYPEHASH(), defaultTargetBlock, defaultMaximumBlocksAfterTarget +// ), +// sponsorSignature: '', +// sponsor: user, +// nonce: defaultNonce, +// expires: compact_.expires, +// witness: keccak256(abi.encode(keccak256(bytes(mandateTypeString)), mandate_)), +// witnessTypestring: witnessTypeString, +// id: usdcId, +// allocatedAmount: defaultAmount, +// claimants: components +// }); +// vm.prank(arbiter); +// vm.expectRevert(abi.encodeWithSelector(0x8baa579f)); // check for the InvalidSignature() error in the Compact contract +// compactContract.claim(claim); + +// vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); +// vm.assertEq(compactContract.balanceOf(filler, usdcId), 0); +// } + +// function test_isValidSignature_successful_open() public { +// // Deposit tokens +// vm.startPrank(user); +// usdc.mint(user, defaultAmount); +// usdc.approve(address(compactContract), defaultAmount); +// compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + +// // register a claim +// Compact memory compact_ = _getCompact(); +// Mandate memory mandate_ = _getMandate(); + +// bytes32 claimHash = _hashCompact(compact_, mandate_); +// bytes32 typeHash = _getTypeHash(); +// compactContract.register(claimHash, typeHash); + +// address filler = makeAddr('filler'); +// vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); +// vm.assertEq(compactContract.balanceOf(filler, usdcId), 0); + +// // we open the order and lock the tokens +// (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); +// erc7683Allocator.open(onChainCrossChainOrder_); +// vm.stopPrank(); + +// // claim should be successful +// Component memory component = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); +// Component[] memory components = new Component[](1); +// components[0] = component; +// Claim memory claim = Claim({ +// allocatorData: abi.encode( +// erc7683Allocator.QUALIFICATION_TYPEHASH(), defaultTargetBlock, defaultMaximumBlocksAfterTarget +// ), +// sponsorSignature: '', +// sponsor: user, +// nonce: defaultNonce, +// expires: compact_.expires, +// witness: keccak256( +// abi.encode( +// keccak256(bytes(mandateTypeString)), +// defaultOutputChainId, +// tribunal, +// mandate_.recipient, +// mandate_.expires, +// mandate_.token, +// mandate_.minimumAmount, +// mandate_.baselinePriorityFee, +// mandate_.scalingFactor, +// keccak256(abi.encodePacked(mandate_.decayCurve)), +// mandate_.salt +// ) +// ), +// witnessTypestring: witnessTypeString, +// id: usdcId, +// allocatedAmount: defaultAmount, +// claimants: components +// }); +// vm.prank(arbiter); +// compactContract.claim(claim); + +// vm.assertEq(compactContract.balanceOf(user, usdcId), 0); +// vm.assertEq(compactContract.balanceOf(filler, usdcId), defaultAmount); +// } + +// function test_isValidSignature_successful_openFor() public { +// // Deposit tokens +// vm.startPrank(user); +// usdc.mint(user, defaultAmount); +// usdc.approve(address(compactContract), defaultAmount); +// compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + +// // register a claim +// Compact memory compact_ = _getCompact(); +// Mandate memory mandate_ = _getMandate(); + +// bytes32 claimHash = _hashCompact(compact_, mandate_); +// bytes32 typeHash = _getTypeHash(); +// compactContract.register(claimHash, typeHash); + +// address filler = makeAddr('filler'); +// vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); +// vm.assertEq(compactContract.balanceOf(filler, usdcId), 0); + +// // we open the order and lock the tokens +// (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = +// _getGaslessCrossChainOrder(); +// erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); +// vm.stopPrank(); + +// // claim should be successful +// Component memory component = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); +// Component[] memory components = new Component[](1); +// components[0] = component; +// Claim memory claim = Claim({ +// allocatorData: abi.encode(erc7683Allocator.QUALIFICATION_TYPEHASH(), uint256(0), uint256(0)), +// sponsorSignature: '', +// sponsor: user, +// nonce: defaultNonce, +// expires: compact_.expires, +// witness: keccak256( +// abi.encode( +// keccak256(bytes(mandateTypeString)), +// defaultOutputChainId, +// tribunal, +// mandate_.recipient, +// mandate_.expires, +// mandate_.token, +// mandate_.minimumAmount, +// mandate_.baselinePriorityFee, +// mandate_.scalingFactor, +// keccak256(abi.encodePacked(mandate_.decayCurve)), +// mandate_.salt +// ) +// ), +// witnessTypestring: witnessTypeString, +// id: usdcId, +// allocatedAmount: defaultAmount, +// claimants: components +// }); +// vm.prank(arbiter); +// compactContract.claim(claim); + +// vm.assertEq(compactContract.balanceOf(user, usdcId), 0); +// vm.assertEq(compactContract.balanceOf(filler, usdcId), defaultAmount); +// } +// } + +// contract ERC7683Allocator_resolveFor is GaslessCrossChainOrderData { +// function test_resolve_successful() public { +// // WITH THE CURRENT ERC7683 DESIGN, THE SPONSOR SIGNATURE IS NOT PROVIDED TO THE RESOLVE FUNCTION +// // WHILE THE ResolvedCrossChainOrder WITHOUT THE SIGNATURE COULD STILL BE USED TO SIMULATE THE FILL, +// // ACTUALLY USING THIS DATA WOULD RESULT IN A LOSS OF THE REWARD TOKENS FOR THE FILLER. +// // THIS FEELS RISKY. +// // THE CURRENT ALTERNATIVE WOULD BE HAVE THE INPUT SIGNATURE BEING LEFT EMPTY AND INSTEAD BE PROVIDED IN THE THE orderData OF THE GaslessCrossChainOrderData. +// // THIS IS BOTH NOT IDEAL, SO CURRENTLY CHECKING FOR A SOLUTION. + +// (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = +// _getGaslessCrossChainOrder(); +// IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); +// IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); +// IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); +// maxSpent[0] = IOriginSettler.Output({ +// token: bytes32(uint256(uint160(defaultOutputToken))), +// amount: type(uint256).max, +// recipient: bytes32(uint256(uint160(user))), +// chainId: defaultOutputChainId +// }); +// minReceived[0] = IOriginSettler.Output({ +// token: bytes32(uint256(uint160(address(usdc)))), +// amount: defaultAmount, +// recipient: '', +// chainId: block.chainid +// }); +// TribunalClaim memory claim = TribunalClaim({ +// chainId: block.chainid, +// compact: _getCompact(), +// sponsorSignature: '', // sponsorSignature, // THE SIGNATURE MUST BE ADDED MANUALLY BY THE FILLER WITH THE CURRENT SYSTEM, BEFORE FILLING THE ORDER ON THE TARGET CHAIN +// allocatorSignature: '' +// }); +// fillInstructions[0] = IOriginSettler.FillInstruction({ +// destinationChainId: defaultOutputChainId, +// destinationSettler: bytes32(uint256(uint160(tribunal))), +// originData: abi.encode(claim, _getMandate(), uint256(0), uint256(0)) +// }); + +// IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ +// user: user, +// originChainId: block.chainid, +// openDeadline: uint32(_getClaimExpiration()), +// fillDeadline: uint32(_getFillExpiration()), +// orderId: bytes32(defaultNonce), +// maxSpent: maxSpent, +// minReceived: minReceived, +// fillInstructions: fillInstructions +// }); +// IOriginSettler.ResolvedCrossChainOrder memory resolved = +// erc7683Allocator.resolveFor(gaslessCrossChainOrder_, ''); +// assertEq(resolved.user, resolvedCrossChainOrder.user); +// assertEq(resolved.originChainId, resolvedCrossChainOrder.originChainId); +// assertEq(resolved.openDeadline, resolvedCrossChainOrder.openDeadline); +// assertEq(resolved.fillDeadline, resolvedCrossChainOrder.fillDeadline); +// assertEq(resolved.orderId, resolvedCrossChainOrder.orderId); +// assertEq(resolved.maxSpent.length, resolvedCrossChainOrder.maxSpent.length); +// assertEq(resolved.maxSpent[0].token, resolvedCrossChainOrder.maxSpent[0].token); +// assertEq(resolved.maxSpent[0].amount, resolvedCrossChainOrder.maxSpent[0].amount); +// assertEq(resolved.maxSpent[0].recipient, resolvedCrossChainOrder.maxSpent[0].recipient); +// assertEq(resolved.maxSpent[0].chainId, resolvedCrossChainOrder.maxSpent[0].chainId); +// assertEq(resolved.minReceived.length, resolvedCrossChainOrder.minReceived.length); +// assertEq(resolved.minReceived[0].token, resolvedCrossChainOrder.minReceived[0].token); +// assertEq(resolved.minReceived[0].amount, resolvedCrossChainOrder.minReceived[0].amount); +// assertEq(resolved.minReceived[0].recipient, resolvedCrossChainOrder.minReceived[0].recipient); +// assertEq(resolved.minReceived[0].chainId, resolvedCrossChainOrder.minReceived[0].chainId); +// assertEq(resolved.fillInstructions.length, resolvedCrossChainOrder.fillInstructions.length); +// assertEq( +// resolved.fillInstructions[0].destinationChainId, +// resolvedCrossChainOrder.fillInstructions[0].destinationChainId +// ); +// assertEq( +// resolved.fillInstructions[0].destinationSettler, +// resolvedCrossChainOrder.fillInstructions[0].destinationSettler +// ); +// assertEq(resolved.fillInstructions[0].originData, resolvedCrossChainOrder.fillInstructions[0].originData); +// } +// } + +// contract ERC7683Allocator_resolve is OnChainCrossChainOrderData { +// function test_resolve_successful() public { +// (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); +// IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); +// IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); +// IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); +// maxSpent[0] = IOriginSettler.Output({ +// token: bytes32(uint256(uint160(defaultOutputToken))), +// amount: type(uint256).max, +// recipient: bytes32(uint256(uint160(user))), +// chainId: defaultOutputChainId +// }); +// minReceived[0] = IOriginSettler.Output({ +// token: bytes32(uint256(uint160(address(usdc)))), +// amount: defaultAmount, +// recipient: '', +// chainId: block.chainid +// }); +// TribunalClaim memory claim = TribunalClaim({ +// chainId: block.chainid, +// compact: _getCompact(), +// sponsorSignature: '', +// allocatorSignature: '' +// }); +// fillInstructions[0] = IOriginSettler.FillInstruction({ +// destinationChainId: defaultOutputChainId, +// destinationSettler: bytes32(uint256(uint160(tribunal))), +// originData: abi.encode(claim, _getMandate(), defaultTargetBlock, defaultMaximumBlocksAfterTarget) +// }); + +// IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ +// user: user, +// originChainId: block.chainid, +// openDeadline: uint32(_getClaimExpiration()), +// fillDeadline: uint32(_getFillExpiration()), +// orderId: bytes32(defaultNonce), +// maxSpent: maxSpent, +// minReceived: minReceived, +// fillInstructions: fillInstructions +// }); +// IOriginSettler.ResolvedCrossChainOrder memory resolved = erc7683Allocator.resolve(onChainCrossChainOrder_); +// assertEq(resolved.user, resolvedCrossChainOrder.user); +// assertEq(resolved.originChainId, resolvedCrossChainOrder.originChainId); +// assertEq(resolved.openDeadline, resolvedCrossChainOrder.openDeadline); +// assertEq(resolved.fillDeadline, resolvedCrossChainOrder.fillDeadline); +// assertEq(resolved.orderId, resolvedCrossChainOrder.orderId); +// assertEq(resolved.maxSpent.length, resolvedCrossChainOrder.maxSpent.length); +// assertEq(resolved.maxSpent[0].token, resolvedCrossChainOrder.maxSpent[0].token); +// assertEq(resolved.maxSpent[0].amount, resolvedCrossChainOrder.maxSpent[0].amount); +// assertEq(resolved.maxSpent[0].recipient, resolvedCrossChainOrder.maxSpent[0].recipient); +// assertEq(resolved.maxSpent[0].chainId, resolvedCrossChainOrder.maxSpent[0].chainId); +// assertEq(resolved.minReceived.length, resolvedCrossChainOrder.minReceived.length); +// assertEq(resolved.minReceived[0].token, resolvedCrossChainOrder.minReceived[0].token); +// assertEq(resolved.minReceived[0].amount, resolvedCrossChainOrder.minReceived[0].amount); +// assertEq(resolved.minReceived[0].recipient, resolvedCrossChainOrder.minReceived[0].recipient); +// assertEq(resolved.minReceived[0].chainId, resolvedCrossChainOrder.minReceived[0].chainId); +// assertEq(resolved.fillInstructions.length, resolvedCrossChainOrder.fillInstructions.length); +// assertEq( +// resolved.fillInstructions[0].destinationChainId, +// resolvedCrossChainOrder.fillInstructions[0].destinationChainId +// ); +// assertEq( +// resolved.fillInstructions[0].destinationSettler, +// resolvedCrossChainOrder.fillInstructions[0].destinationSettler +// ); +// assertEq(resolved.fillInstructions[0].originData, resolvedCrossChainOrder.fillInstructions[0].originData); +// } +// } + +// contract ERC7683Allocator_getCompactWitnessTypeString is MocksSetup { +// function test_getCompactWitnessTypeString() public view { +// assertEq( +// erc7683Allocator.getCompactWitnessTypeString(), +// 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt))' +// ); +// } +// } + +// contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData { +// function test_revert_invalidNonce(uint256 nonce_) public { +// address expectedSponsor; +// assembly ("memory-safe") { +// expectedSponsor := shr(96, nonce_) +// } +// vm.assume(user != expectedSponsor); + +// vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidNonce.selector, nonce_)); +// erc7683Allocator.checkNonce(user, nonce_); +// } + +// function test_checkNonce_unused(uint96 nonce_) public view { +// address sponsor = user; +// uint256 nonce; +// assembly ("memory-safe") { +// nonce := or(shl(96, sponsor), shr(160, shl(160, nonce_))) +// } +// assertEq(erc7683Allocator.checkNonce(sponsor, nonce), true); +// } + +// function test_checkNonce_used() public { +// // Deposit tokens +// vm.startPrank(user); +// usdc.mint(user, defaultAmount); +// usdc.approve(address(compactContract), defaultAmount); +// compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + +// // register a claim +// Compact memory compact_ = _getCompact(); +// Mandate memory mandate_ = _getMandate(); + +// bytes32 claimHash = _hashCompact(compact_, mandate_); +// bytes32 typeHash = _getTypeHash(); +// compactContract.register(claimHash, typeHash); + +// (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); +// erc7683Allocator.open(onChainCrossChainOrder_); + +// vm.assertEq(erc7683Allocator.checkNonce(user, defaultNonce), false); +// vm.stopPrank(); +// } + +// function test_checkNonce_fuzz(uint8 nonce_) public { +// uint256 nonce = uint256(bytes32(abi.encodePacked(user, uint96(nonce_)))); + +// bool sameNonce = nonce == defaultNonce; + +// // Deposit tokens +// vm.startPrank(user); +// usdc.mint(user, defaultAmount); +// usdc.approve(address(compactContract), defaultAmount); +// compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + +// // register a claim +// Compact memory compact_ = _getCompact(); +// Mandate memory mandate_ = _getMandate(); + +// bytes32 claimHash = _hashCompact(compact_, mandate_); +// bytes32 typeHash = _getTypeHash(); +// compactContract.register(claimHash, typeHash); + +// (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); +// erc7683Allocator.open(onChainCrossChainOrder_); + +// vm.assertEq(erc7683Allocator.checkNonce(user, nonce), !sameNonce); + +// vm.stopPrank(); +// } +// } From faaa4b0a197d95cf5bb99903e9d66447e421fc55 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Thu, 10 Jul 2025 11:42:06 +0200 Subject: [PATCH 27/63] improved ERC7683Allocator and tests --- src/allocators/ERC7683Allocator.sol | 112 ++- test/ERC7683Allocator.t.sol | 1255 +++++++++++++-------------- 2 files changed, 671 insertions(+), 696 deletions(-) diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index 15a95a4..428357b 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -12,27 +12,26 @@ import {ECDSA} from '@openzeppelin/contracts/utils/cryptography/ECDSA.sol'; import {LibBytes} from '@solady/utils/LibBytes.sol'; import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; import {Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; -import {console} from 'forge-std/console.sol'; contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { /// @notice The typehash of the OrderData struct - // keccak256("OrderData(address arbiter,address sponsor,uint256 nonce,uint256 id,uint256 amount, - // uint256 chainId,address tribunal,address recipient,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt,uint256 targetBlock,uint256 maximumBlocksAfterTarget)") - bytes32 public constant ORDERDATA_TYPEHASH = 0x9687614112a074c792f7035dc9365f34672a3aa8d3c312500bd47ddcaa0383b5; + // keccak256("OrderData(address arbiter,address sponsor,uint256 nonce,uint256 expires,bytes12 lockTag,address inputToken,uint256 amount, + // uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt,uint256 targetBlock,uint256 maximumBlocksAfterTarget)") + bytes32 public constant ORDERDATA_TYPEHASH = 0x2ec2bf7ae42e14efd81070a06d7410420dad2cf15d1d09c8a7d77d82f9e5eae5; /// @notice The typehash of the OrderDataGasless struct - // keccak256("OrderDataGasless(address arbiter,uint256 id,uint256 amount, - // uint256 chainId,address tribunal,address recipient,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") + // keccak256("OrderDataGasless(address arbiter,bytes12 lockTag,address inputToken,uint256 amount, + // uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") bytes32 public constant ORDERDATA_GASLESS_TYPEHASH = - 0xe9b624fa654c7f07ce16d31bf0165a4030d4022f62987afad8ef9d30fc8a0b88; + 0x29d853cc0f7a1e24319ad92f2404fd0ff5806cd6ac6f6325dfaa7c547074e912; /// @notice keccak256("QualifiedClaim(bytes32 claimHash,uint256 targetBlock,uint256 maximumBlocksAfterTarget)") bytes32 public constant QUALIFICATION_TYPEHASH = 0x59866b84bd1f6c909cf2a31efd20c59e6c902e50f2c196994e5aa85cdc7d7ce0; - /// @notice keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate) + /// @notice keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,bytes12 lockTag,address token,uint256 amount,Mandate mandate) // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") bytes32 public constant COMPACT_WITNESS_TYPEHASH = - 0xfd9cda0e5e31a3a3476cb5b57b07e2a4d6a12815506f69c880696448cd9897a5; + 0x2ec0d30491bb66a6eb554b9d53f490d79b54fc5f4963bed4b2bb8096b4790f1f; /// @notice keccak256("Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") bytes32 internal constant MANDATE_TYPEHASH = 0x74d9c10530859952346f3e046aa2981a24bb7524b8394eb45a9deddced9d6501; @@ -40,9 +39,9 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { /// @notice uint256(uint8(keccak256("ERC7683Allocator.nonce"))) uint8 internal constant NONCE_MASTER_SLOT_SEED = 0x39; - uint8 internal constant ORDERDATA_OFFSET = 0xa0; + uint8 internal constant ORDERDATA_LOCKTAG_OFFSET = 0x80; - uint8 internal constant ORDERDATA_GASLESS_OFFSET = 0x40; + uint8 internal constant ORDERDATA_GASLESS_LOCKTAG_OFFSET = 0x20; bytes32 immutable _COMPACT_DOMAIN_SEPARATOR; @@ -70,7 +69,7 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { bytes calldata orderDataGaslessBytes = LibBytes.dynamicStructInCalldata(order_.orderData, 0x00); uint256 mandateOffset; assembly ("memory-safe") { - mandateOffset := add(orderDataGaslessBytes.offset, 0xa0) // mandate starts at orderData.chainId (0x80). Add 0x20, as the offset points at the length + mandateOffset := add(orderDataGaslessBytes.offset, 0x80) // mandate starts at orderData.chainId (0x80) } // Extract the resolved order early to reduce stack pressure @@ -80,14 +79,14 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { order_.openDeadline, order_.fillDeadline, orderDataGaslessBytes, - ORDERDATA_GASLESS_OFFSET, + ORDERDATA_GASLESS_LOCKTAG_OFFSET, sponsorSignature_ ); // Extract mandateHash early to reduce stack pressure - bytes32 mandateHash = _mandateHash(mandateOffset, order_.fillDeadline); + bytes32 mandateHash = _mandateHash(orderDataGaslessBytes, mandateOffset, order_.fillDeadline); - _open(orderDataGaslessBytes, ORDERDATA_GASLESS_OFFSET, sponsorSignature_, mandateHash, resolvedOrder); + _open(orderDataGaslessBytes, ORDERDATA_GASLESS_LOCKTAG_OFFSET, sponsorSignature_, mandateHash, resolvedOrder); } /// @inheritdoc IERC7683Allocator @@ -106,13 +105,11 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { mandateOffset := add(orderDataBytes.offset, 0xe0) // mandate starts at orderData.chainId } - console.log('-1'); - _checkMsgSender(orderData.sponsor); - console.log('0'); // Extract mandateHash early to reduce stack pressure - bytes32 mandateHash = _mandateHash(mandateOffset, order.fillDeadline); + bytes32 mandateHash = _mandateHash(orderDataBytes, mandateOffset, order.fillDeadline); + // Extract the resolved order early to reduce stack pressure ResolvedCrossChainOrder memory resolvedOrder = _resolveOrder( orderData.sponsor, @@ -120,13 +117,11 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { uint32(orderData.expires), order.fillDeadline, orderDataBytes, - ORDERDATA_OFFSET, + ORDERDATA_LOCKTAG_OFFSET, LibBytes.emptyCalldata() ); - console.log('1'); - - _open(orderDataBytes, ORDERDATA_OFFSET, LibBytes.emptyCalldata(), mandateHash, resolvedOrder); + _open(orderDataBytes, ORDERDATA_LOCKTAG_OFFSET, LibBytes.emptyCalldata(), mandateHash, resolvedOrder); } /// @inheritdoc IERC7683Allocator @@ -136,10 +131,6 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { returns (ResolvedCrossChainOrder memory) { bytes calldata orderDataGaslessBytes = LibBytes.dynamicStructInCalldata(order_.orderData, 0x00); - OrderDataGasless calldata orderDataGasless; - assembly ("memory-safe") { - orderDataGasless := orderDataGaslessBytes.offset - } return _resolveOrder( order_.user, @@ -147,7 +138,7 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { order_.openDeadline, order_.fillDeadline, orderDataGaslessBytes, - ORDERDATA_GASLESS_OFFSET, + ORDERDATA_GASLESS_LOCKTAG_OFFSET, LibBytes.emptyCalldata() ); } @@ -166,7 +157,7 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { orderData.expires, order_.fillDeadline, orderDataBytes, - ORDERDATA_OFFSET, + ORDERDATA_LOCKTAG_OFFSET, LibBytes.emptyCalldata() ); } @@ -310,29 +301,23 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { bytes32 tokenHash = _verifyAllocation(resolvedOrder_.user, nonce_, resolvedOrder_.openDeadline, orderData_, lockTagOffset); - console.log('2'); - + // Create the Compact claim hash + bytes32 claimHash; uint256 lockTagAbsoluteOffset; assembly ("memory-safe") { lockTagAbsoluteOffset := add(orderData_.offset, lockTagOffset) - } - // Create the Compact claim hash - bytes32 claimHash; - assembly ("memory-safe") { let m := mload(0x40) mstore(m, COMPACT_WITNESS_TYPEHASH) calldatacopy(add(m, 0x20), orderData_.offset, 0x20) // arbiter mstore(add(m, 0x40), mload(resolvedOrder_)) // sponsor (first item in resolvedOrder_) mstore(add(m, 0x60), nonce_) // nonce - mstore(add(m, 0x80), add(resolvedOrder_, 0x40)) // Compact.expires (third item in resolvedOrder_) + mstore(add(m, 0x80), mload(add(resolvedOrder_, 0x40))) // Compact.expires (third item in resolvedOrder_) calldatacopy(add(m, 0xa0), lockTagAbsoluteOffset, 0x60) // lockTag, inputToken, amount mstore(add(m, 0x100), mandateHash_) claimHash := keccak256(m, 0x120) } - console.log('3'); - // We check for the length, which means this could also be triggered by a zero length signature provided in the openFor function. This enables relaying of orders if the claim was registered on the compact. if (sponsorSignature_.length > 0) { bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), _COMPACT_DOMAIN_SEPARATOR, claimHash)); @@ -350,8 +335,6 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { } } - console.log('4'); - bytes32 qualifiedClaimHash; assembly ("memory-safe") { let m := mload(0x40) @@ -360,7 +343,7 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { mstore(add(m, 0x40), 0x0) // clear targetBlock mstore(add(m, 0x60), 0x0) // clear maximumBlocksAfterTarget - if eq(lockTagOffset, ORDERDATA_OFFSET) { + if eq(lockTagOffset, ORDERDATA_LOCKTAG_OFFSET) { // if data is of type OrderData, copy targetBlock and maximumBlocksAfterTarget calldatacopy(add(m, 0x40), add(lockTagAbsoluteOffset, 0x180), 0x40) // targetBlock, maximumBlocksAfterTarget } @@ -373,8 +356,6 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { let m := mload(0x40) } - console.log('5'); - _lockTokens(tokenHash, amount, resolvedOrder_.openDeadline, qualifiedClaimHash); // Emit an open event @@ -463,21 +444,23 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { let mandateOffset := add(add(orderData.offset, lockTagOffset), 0x60) + // Skip the chainId and tribunal, as they are implicit arguments in the Tribunal.Mandate + calldatacopy(m, add(mandateOffset, 0x40), 0x20) // recipient mstore(add(m, 0x20), fillDeadline) // expires calldatacopy(add(m, 0x40), add(mandateOffset, 0x60), 0xc0) // settlementToken, minimumAmount, baselinePriorityFee, scalingFactor, decayCurve.offset, salt - mstore(add(m, 0xc0), add(m, 0x100)) // update decayCurve.offset to point to the absolute memory location of decayCurve.length + mstore(add(m, 0xc0), add(m, 0x100)) // update decayCurve.offset to point to the relative memory location of decayCurve.length within Mandate let decayCurveOffset := calldataload(add(mandateOffset, 0xe0)) - let decayCurveLength := calldataload(decayCurveOffset) + let decayCurveLength := calldataload(add(orderData.offset, decayCurveOffset)) - calldatacopy(add(m, 0x100), decayCurveOffset, add(decayCurveLength, 0x20)) // decayCurve.length, decayCurve.content + calldatacopy(add(m, 0x100), add(orderData.offset, decayCurveOffset), add(decayCurveLength, 0x20)) // decayCurve.length, decayCurve.content mandate := m - let length := add(0x120, decayCurveLength) + let totalMandateLength := add(0x120, decayCurveLength) - mstore(0x40, add(m, length)) // update free memory pointer + mstore(0x40, add(m, totalMandateLength)) // update free memory pointer } uint256 chainId; @@ -489,7 +472,7 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { let mandateOffset := add(add(orderData.offset, lockTagOffset), 0x60) chainId := calldataload(mandateOffset) tribunal := calldataload(add(mandateOffset, 0x20)) - let isOrderData := eq(lockTagOffset, ORDERDATA_OFFSET) + let isOrderData := eq(lockTagOffset, ORDERDATA_LOCKTAG_OFFSET) // Multiply the data calldata value by 0 if OrderDataGasless, as OrderDataGasless does not support targetBlock and maximumBlocksAfterTarget targetBlock := mul(calldataload(add(mandateOffset, mul(0x120, isOrderData))), isOrderData) // Multiply targetBlock offset by 0 if OrderDataGasless to prevent out of bounds calldata read maximumBlocksAfterTarget := mul(calldataload(add(mandateOffset, mul(0x140, isOrderData))), isOrderData) // Multiply maximumBlocksAfterTarget offset by 0 if OrderDataGasless to prevent out of bounds calldata read @@ -564,24 +547,29 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { } } - function _mandateHash(uint256 mandateOffset, uint32 fillDeadline) internal pure returns (bytes32 mandateHash_) { - // total mandate length: x140 + decayCurve.length + function _mandateHash(bytes calldata orderData, uint256 mandateOffset, uint32 fillDeadline) + internal + pure + returns (bytes32 mandateHash_) + { + // total mandate length: x160 + decayCurve.length assembly ("memory-safe") { - let m := mload(0x40) - let decayCurveOffset := calldataload(add(mandateOffset, 0xe0)) - let decayCurveLength := calldataload(decayCurveOffset) - - let l := add(0x140, decayCurveLength) // length if decayCurve.length was 0 (0x120) + decayCurve.length + let decayCurveLength := calldataload(add(orderData.offset, decayCurveOffset)) + let m := mload(0x40) mstore(m, MANDATE_TYPEHASH) calldatacopy(add(m, 0x20), mandateOffset, 0x60) // chainid, tribunal and recipient - mstore(add(m, 0x80), fillDeadline) + mstore(add(m, 0x80), fillDeadline) // mandate.expires calldatacopy(add(m, 0xa0), add(mandateOffset, 0x60), 0xc0) // settlementToken, minimumAmount, baselinePriorityFee, scalingFactor, decayCurve.offset, salt - mstore(add(m, 0x120), add(m, 0x160)) // update decayCurve.offset to point to the absolute memory location of decayCurve.length - calldatacopy(add(m, 0x160), decayCurveOffset, add(decayCurveLength, 0x20)) // decayCurve.length, decayCurve.content - mandateHash_ := keccak256(m, add(l, 0x20)) // mandate typehash + mandate data length + for { let i := 0 } lt(i, decayCurveLength) { i := add(i, 0x20) } { + mstore(add(m, add(0x160, i)), calldataload(add(add(orderData.offset, decayCurveOffset), add(i, 0x20)))) // copy the content of decayCurve to memory + } + + mstore(add(m, 0x120), keccak256(add(m, 0x160), mul(decayCurveLength, 0x20))) // create and store the decayCurve hash + + mandateHash_ := keccak256(m, 0x160) // mandate typehash + mandate data length } // 0x00: MANDATE_TYPEHASH @@ -593,10 +581,8 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { // 0xc0: minimumAmount // 0xe0: baselinePriorityFee // 0x100: scalingFactor - // 0x120: decayCurve.offset + // 0x120: decayCurve hash // 0x140: salt - // 0x160: decayCurve.length - // 0x180: decayCurve.content } function _convertGaslessOrderData( diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index 110a9fa..d65b601 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -52,7 +52,7 @@ abstract contract MocksSetup is Test, TestHelper { uint256 defaultBaselinePriorityFee = 0; uint256 defaultScalingFactor = 0; uint256[] defaultDecayCurve = new uint256[](0); - bytes32 defaultSalt = bytes32(0); + bytes32 defaultSalt = bytes32(0x0000000000000000000000000000000000000000000000000000000000000007); uint256 defaultTargetBlock = 100; uint256 defaultMaximumBlocksAfterTarget = 10; @@ -86,34 +86,36 @@ abstract contract CreateHash is MocksSetup { string EIP712_DOMAIN_TYPE = 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'; // Hashed inside the function // EIP712 domain type string name = 'The Compact'; - string version = '0'; + string version = '1'; string compactWitnessTypeString = - 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; + 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,bytes12 lockTag,address token,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; string mandateTypeString = 'Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; string witnessTypeString = - 'Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; + 'uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt'; function _hashCompact(Compact memory data, Mandate memory mandate, address verifyingContract) internal view - returns (bytes32) + returns (bytes32 digest) { bytes32 compactHash = _hashCompact(data, mandate); // hash typed data - return keccak256( + digest = keccak256( abi.encodePacked( '\x19\x01', // backslash is needed to escape the character _domainSeparator(verifyingContract), compactHash ) ); + console.log('digest'); + console.logBytes32(digest); } function _hashCompact(Compact memory data, Mandate memory mandate) internal view returns (bytes32 compactHash) { bytes32 mandateHash = _hashMandate(mandate); - return keccak256( + compactHash = keccak256( abi.encode( keccak256(bytes(compactWitnessTypeString)), data.arbiter, @@ -126,6 +128,12 @@ abstract contract CreateHash is MocksSetup { mandateHash ) ); + console.log('mandateHash'); + console.logBytes32(mandateHash); + console.log('compactHash'); + console.logBytes32(compactHash); + console.log('typehash'); + console.logBytes32(keccak256(bytes(compactWitnessTypeString))); } function _hashMandate(Mandate memory mandate) internal view returns (bytes32) { @@ -512,165 +520,297 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { } } -// contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { +contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { + function test_revert_InvalidOrderDataType() public { + // Order data type is invalid + bytes32 falseOrderDataType = keccak256('false'); + + vm.prank(user); + vm.expectRevert( + abi.encodeWithSelector( + IERC7683Allocator.InvalidOrderDataType.selector, + falseOrderDataType, + erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH() + ) + ); + (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = + _getGaslessCrossChainOrder(); + falseGaslessCrossChainOrder.orderDataType = falseOrderDataType; + erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); + } + + function test_revert_InvalidDecoding() public { + // Decoding fails because of additional data + vm.prank(user); + vm.expectRevert(); + (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = + _getGaslessCrossChainOrder(); + falseGaslessCrossChainOrder.orderData = abi.encode(falseGaslessCrossChainOrder.orderData, uint8(1)); + erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); + } + + function test_revert_InvalidOriginSettler() public { + // Origin settler is not the allocator + address falseOriginSettler = makeAddr('falseOriginSettler'); + vm.expectRevert( + abi.encodeWithSelector( + IERC7683Allocator.InvalidOriginSettler.selector, falseOriginSettler, address(erc7683Allocator) + ) + ); + (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = + _getGaslessCrossChainOrder( + falseOriginSettler, + _getCompact(), + _getMandate(), + block.chainid, + ORDERDATA_GASLESS_TYPEHASH, + address(erc7683Allocator), + userPK + ); + vm.prank(user); + erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); + } + + function test_revert_InvalidNonce() public { + // Nonce is invalid because the least significant 160 bits are not the sponsor address + Compact memory compact_ = _getCompact(); + compact_.nonce = uint256(bytes32(abi.encodePacked(uint96(1), attacker))); + vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidNonce.selector, compact_.nonce)); + (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = + _getGaslessCrossChainOrder( + address(erc7683Allocator), + compact_, + _getMandate(), + block.chainid, + ORDERDATA_GASLESS_TYPEHASH, + address(erc7683Allocator), + userPK + ); + vm.prank(user); + erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); + } + + function test_revert_InvalidSponsorSignature() public { + // Sponsor signature is invalid + + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + vm.stopPrank(); + + // Create a malicious signature + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = + _getGaslessCrossChainOrder( + address(erc7683Allocator), + _getCompact(), + _getMandate(), + block.chainid, + ORDERDATA_GASLESS_TYPEHASH, + address(compactContract), + attackerPK + ); + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidSignature.selector, user, attacker)); + erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); + } + + function test_successful_userHimself() public { + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + vm.stopPrank(); + + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = + _getGaslessCrossChainOrder(); + IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); + IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); + IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); + maxSpent[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(defaultOutputToken))), + amount: type(uint256).max, + recipient: bytes32(uint256(uint160(user))), + chainId: defaultOutputChainId + }); + minReceived[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(address(usdc)))), + amount: defaultAmount, + recipient: '', + chainId: block.chainid + }); + TribunalClaim memory claim = TribunalClaim({ + chainId: block.chainid, + compact: _getCompact(), + sponsorSignature: sponsorSignature, + allocatorSignature: '' + }); + fillInstructions[0] = IOriginSettler.FillInstruction({ + destinationChainId: defaultOutputChainId, + destinationSettler: bytes32(uint256(uint160(tribunal))), + originData: abi.encode(claim, _getMandate(), uint256(0), uint256(0)) + }); + + IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ + user: user, + originChainId: block.chainid, + openDeadline: uint32(_getClaimExpiration()), + fillDeadline: uint32(_getFillExpiration()), + orderId: bytes32(defaultNonce), + maxSpent: maxSpent, + minReceived: minReceived, + fillInstructions: fillInstructions + }); + vm.prank(user); + vm.expectEmit(true, false, false, true, address(erc7683Allocator)); + emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); + erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); + } + + function test_successful_relayed() public { + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + vm.stopPrank(); + + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = + _getGaslessCrossChainOrder(); + IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); + IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); + IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); + maxSpent[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(defaultOutputToken))), + amount: type(uint256).max, + recipient: bytes32(uint256(uint160(user))), + chainId: defaultOutputChainId + }); + minReceived[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(address(usdc)))), + amount: defaultAmount, + recipient: '', + chainId: block.chainid + }); + TribunalClaim memory claim = TribunalClaim({ + chainId: block.chainid, + compact: _getCompact(), + sponsorSignature: sponsorSignature, + allocatorSignature: '' + }); + fillInstructions[0] = IOriginSettler.FillInstruction({ + destinationChainId: defaultOutputChainId, + destinationSettler: bytes32(uint256(uint160(tribunal))), + originData: abi.encode(claim, _getMandate(), uint256(0), uint256(0)) + }); + + IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ + user: user, + originChainId: block.chainid, + openDeadline: uint32(_getClaimExpiration()), + fillDeadline: uint32(_getFillExpiration()), + orderId: bytes32(defaultNonce), + maxSpent: maxSpent, + minReceived: minReceived, + fillInstructions: fillInstructions + }); + vm.prank(makeAddr('filler')); + vm.expectEmit(true, false, false, true, address(erc7683Allocator)); + emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); + erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); + } + + function test_revert_NonceAlreadyInUse() public { + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + vm.stopPrank(); + + // use the nonce once + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = + _getGaslessCrossChainOrder(); + vm.prank(user); + erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); + + // try to use the nonce again + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder2, bytes memory sponsorSignature2) = + _getGaslessCrossChainOrder(); + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.NonceAlreadyInUse.selector, defaultNonce)); + erc7683Allocator.openFor(gaslessCrossChainOrder2, sponsorSignature2, ''); + } +} + +// contract ERC7683Allocator_open is OnChainCrossChainOrderData { // function test_revert_InvalidOrderDataType() public { // // Order data type is invalid // bytes32 falseOrderDataType = keccak256('false'); +// IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_ = _getOnChainCrossChainOrder(); +// onChainCrossChainOrder_.orderDataType = falseOrderDataType; // vm.prank(user); // vm.expectRevert( // abi.encodeWithSelector( // IERC7683Allocator.InvalidOrderDataType.selector, // falseOrderDataType, -// erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH() +// erc7683Allocator.ORDERDATA_TYPEHASH() // ) // ); -// (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = -// _getGaslessCrossChainOrder(); -// falseGaslessCrossChainOrder.orderDataType = falseOrderDataType; -// erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); -// } - -// function test_revert_InvalidDecoding() public { -// // Decoding fails because of additional data -// vm.prank(user); -// vm.expectRevert(); -// (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = -// _getGaslessCrossChainOrder(); -// falseGaslessCrossChainOrder.orderData = abi.encode(falseGaslessCrossChainOrder.orderData, uint8(1)); -// erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); +// erc7683Allocator.open(onChainCrossChainOrder_); // } -// function test_revert_InvalidOriginSettler() public { -// // Origin settler is not the allocator -// address falseOriginSettler = makeAddr('falseOriginSettler'); -// vm.expectRevert( -// abi.encodeWithSelector( -// IERC7683Allocator.InvalidOriginSettler.selector, falseOriginSettler, address(erc7683Allocator) -// ) -// ); -// (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = -// _getGaslessCrossChainOrder( -// falseOriginSettler, -// _getCompact(), -// _getMandate(), -// block.chainid, -// ORDERDATA_GASLESS_TYPEHASH, -// address(erc7683Allocator), -// userPK -// ); -// vm.prank(user); -// erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); -// } +// function test_revert_InvalidSponsor() public { +// IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_ = _getOnChainCrossChainOrder(); -// function test_revert_InvalidNonce() public { -// // Nonce is invalid because the least significant 160 bits are not the sponsor address -// Compact memory compact_ = _getCompact(); -// compact_.nonce = uint256(bytes32(abi.encodePacked(uint96(1), attacker))); -// vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidNonce.selector, compact_.nonce)); -// (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = -// _getGaslessCrossChainOrder( -// address(erc7683Allocator), -// compact_, -// _getMandate(), -// block.chainid, -// ORDERDATA_GASLESS_TYPEHASH, -// address(erc7683Allocator), -// userPK -// ); -// vm.prank(user); -// erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); +// vm.prank(attacker); +// vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidSignature.selector, user, attacker)); +// erc7683Allocator.open(onChainCrossChainOrder_); // } -// function test_revert_InvalidSponsorSignature() public { -// // Sponsor signature is invalid - -// // Deposit tokens +// function test_revert_InvalidRegistration_Unavailable() public { +// // we deposit tokens // vm.startPrank(user); // usdc.mint(user, defaultAmount); // usdc.approve(address(compactContract), defaultAmount); // compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); -// vm.stopPrank(); -// // Create a malicious signature -// (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = -// _getGaslessCrossChainOrder( -// address(erc7683Allocator), -// _getCompact(), -// _getMandate(), -// block.chainid, -// ORDERDATA_GASLESS_TYPEHASH, -// address(compactContract), -// attackerPK -// ); -// vm.prank(user); -// vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidSignature.selector, user, attacker)); -// erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); -// } +// // we do NOT register a claim -// function test_successful_userHimself() public { -// // Deposit tokens -// vm.startPrank(user); -// usdc.mint(user, defaultAmount); -// usdc.approve(address(compactContract), defaultAmount); -// compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // vm.stopPrank(); -// (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = -// _getGaslessCrossChainOrder(); -// IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); -// IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); -// IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); -// maxSpent[0] = IOriginSettler.Output({ -// token: bytes32(uint256(uint160(defaultOutputToken))), -// amount: type(uint256).max, -// recipient: bytes32(uint256(uint160(user))), -// chainId: defaultOutputChainId -// }); -// minReceived[0] = IOriginSettler.Output({ -// token: bytes32(uint256(uint160(address(usdc)))), -// amount: defaultAmount, -// recipient: '', -// chainId: block.chainid -// }); -// TribunalClaim memory claim = TribunalClaim({ -// chainId: block.chainid, -// compact: _getCompact(), -// sponsorSignature: sponsorSignature, -// allocatorSignature: '' -// }); -// fillInstructions[0] = IOriginSettler.FillInstruction({ -// destinationChainId: defaultOutputChainId, -// destinationSettler: bytes32(uint256(uint160(tribunal))), -// originData: abi.encode(claim, _getMandate(), uint256(0), uint256(0)) -// }); +// (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); + +// Compact memory compact_ = _getCompact(); +// Mandate memory mandate_ = _getMandate(); +// bytes32 claimHash = _hashCompact(compact_, mandate_); -// IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ -// user: user, -// originChainId: block.chainid, -// openDeadline: uint32(_getClaimExpiration()), -// fillDeadline: uint32(_getFillExpiration()), -// orderId: bytes32(defaultNonce), -// maxSpent: maxSpent, -// minReceived: minReceived, -// fillInstructions: fillInstructions -// }); // vm.prank(user); -// vm.expectEmit(true, false, false, true, address(erc7683Allocator)); -// emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); -// erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); +// vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidRegistration.selector, user, claimHash)); +// erc7683Allocator.open(onChainCrossChainOrder_); // } -// function test_successful_relayed() public { +// function test_successful() public { // // Deposit tokens // vm.startPrank(user); // usdc.mint(user, defaultAmount); // usdc.approve(address(compactContract), defaultAmount); // compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + +// // register a claim +// Compact memory compact_ = _getCompact(); +// Mandate memory mandate_ = _getMandate(); + +// bytes32 claimHash = _hashCompact(compact_, mandate_); +// bytes32 typeHash = _getTypeHash(); +// compactContract.register(claimHash, typeHash); + // vm.stopPrank(); -// (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = -// _getGaslessCrossChainOrder(); +// (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); // IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); // IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); // IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); @@ -689,13 +829,13 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { // TribunalClaim memory claim = TribunalClaim({ // chainId: block.chainid, // compact: _getCompact(), -// sponsorSignature: sponsorSignature, +// sponsorSignature: '', // allocatorSignature: '' // }); // fillInstructions[0] = IOriginSettler.FillInstruction({ // destinationChainId: defaultOutputChainId, // destinationSettler: bytes32(uint256(uint160(tribunal))), -// originData: abi.encode(claim, _getMandate(), uint256(0), uint256(0)) +// originData: abi.encode(claim, _getMandate(), defaultTargetBlock, defaultMaximumBlocksAfterTarget) // }); // IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ @@ -708,550 +848,399 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { // minReceived: minReceived, // fillInstructions: fillInstructions // }); -// vm.prank(makeAddr('filler')); +// vm.prank(user); // vm.expectEmit(true, false, false, true, address(erc7683Allocator)); // emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); -// erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); +// erc7683Allocator.open(onChainCrossChainOrder_); // } +// } -// function test_revert_NonceAlreadyInUse() public { -// // Nonce is already used - -// // Deposit tokens -// vm.startPrank(user); -// usdc.mint(user, defaultAmount); -// usdc.approve(address(compactContract), defaultAmount); -// compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); -// vm.stopPrank(); +contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, GaslessCrossChainOrderData { + function setUp() public override(OnChainCrossChainOrderData, GaslessCrossChainOrderData) { + super.setUp(); + } -// // use the nonce once -// (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = -// _getGaslessCrossChainOrder(); -// vm.prank(user); -// erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); - -// // try to use the nonce again -// (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder2, bytes memory sponsorSignature2) = -// _getGaslessCrossChainOrder(); -// vm.prank(user); -// vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.NonceAlreadyInUse.selector, defaultNonce)); -// erc7683Allocator.openFor(gaslessCrossChainOrder2, sponsorSignature2, ''); -// } -// } - -// contract ERC7683Allocator_open is OnChainCrossChainOrderData { -// function test_revert_InvalidOrderDataType() public { -// // Order data type is invalid -// bytes32 falseOrderDataType = keccak256('false'); -// IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_ = _getOnChainCrossChainOrder(); -// onChainCrossChainOrder_.orderDataType = falseOrderDataType; - -// vm.prank(user); -// vm.expectRevert( -// abi.encodeWithSelector( -// IERC7683Allocator.InvalidOrderDataType.selector, -// falseOrderDataType, -// erc7683Allocator.ORDERDATA_TYPEHASH() -// ) -// ); -// erc7683Allocator.open(onChainCrossChainOrder_); -// } - -// function test_revert_InvalidSponsor() public { -// IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_ = _getOnChainCrossChainOrder(); - -// vm.prank(attacker); -// vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidSignature.selector, user, attacker)); -// erc7683Allocator.open(onChainCrossChainOrder_); -// } - -// function test_revert_InvalidRegistration_Unavailable() public { -// // we deposit tokens -// vm.startPrank(user); -// usdc.mint(user, defaultAmount); -// usdc.approve(address(compactContract), defaultAmount); -// compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); - -// // we do NOT register a claim - -// vm.stopPrank(); - -// (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); - -// Compact memory compact_ = _getCompact(); -// Mandate memory mandate_ = _getMandate(); -// bytes32 claimHash = _hashCompact(compact_, mandate_); - -// vm.prank(user); -// vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidRegistration.selector, user, claimHash)); -// erc7683Allocator.open(onChainCrossChainOrder_); -// } - -// function test_successful() public { -// // Deposit tokens -// vm.startPrank(user); -// usdc.mint(user, defaultAmount); -// usdc.approve(address(compactContract), defaultAmount); -// compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); - -// // register a claim -// Compact memory compact_ = _getCompact(); -// Mandate memory mandate_ = _getMandate(); - -// bytes32 claimHash = _hashCompact(compact_, mandate_); -// bytes32 typeHash = _getTypeHash(); -// compactContract.register(claimHash, typeHash); - -// vm.stopPrank(); - -// (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); -// IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); -// IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); -// IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); -// maxSpent[0] = IOriginSettler.Output({ -// token: bytes32(uint256(uint160(defaultOutputToken))), -// amount: type(uint256).max, -// recipient: bytes32(uint256(uint160(user))), -// chainId: defaultOutputChainId -// }); -// minReceived[0] = IOriginSettler.Output({ -// token: bytes32(uint256(uint160(address(usdc)))), -// amount: defaultAmount, -// recipient: '', -// chainId: block.chainid -// }); -// TribunalClaim memory claim = TribunalClaim({ -// chainId: block.chainid, -// compact: _getCompact(), -// sponsorSignature: '', -// allocatorSignature: '' -// }); -// fillInstructions[0] = IOriginSettler.FillInstruction({ -// destinationChainId: defaultOutputChainId, -// destinationSettler: bytes32(uint256(uint160(tribunal))), -// originData: abi.encode(claim, _getMandate(), defaultTargetBlock, defaultMaximumBlocksAfterTarget) -// }); - -// IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ -// user: user, -// originChainId: block.chainid, -// openDeadline: uint32(_getClaimExpiration()), -// fillDeadline: uint32(_getFillExpiration()), -// orderId: bytes32(defaultNonce), -// maxSpent: maxSpent, -// minReceived: minReceived, -// fillInstructions: fillInstructions -// }); -// vm.prank(user); -// vm.expectEmit(true, false, false, true, address(erc7683Allocator)); -// emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); -// erc7683Allocator.open(onChainCrossChainOrder_); -// } -// } - -// contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, GaslessCrossChainOrderData { -// function setUp() public override(OnChainCrossChainOrderData, GaslessCrossChainOrderData) { -// super.setUp(); -// } - -// function test_revert_InvalidLock() public { -// // Deposit tokens -// vm.startPrank(user); -// usdc.mint(user, defaultAmount); -// usdc.approve(address(compactContract), defaultAmount); -// compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + function test_revert_InvalidLock() public { + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); -// // register a claim -// Compact memory compact_ = _getCompact(); -// Mandate memory mandate_ = _getMandate(); + // register a claim + Compact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); -// bytes32 claimHash = _hashCompact(compact_, mandate_); -// bytes32 typeHash = _getTypeHash(); -// compactContract.register(claimHash, typeHash); + bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 typeHash = _getTypeHash(); + compactContract.register(claimHash, typeHash); -// address filler = makeAddr('filler'); -// vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); -// vm.assertEq(compactContract.balanceOf(filler, usdcId), 0); + address filler = makeAddr('filler'); + vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); + vm.assertEq(compactContract.balanceOf(filler, usdcId), 0); -// vm.stopPrank(); + vm.stopPrank(); -// // we do NOT open the order or lock the tokens + // we do NOT open the order or lock the tokens -// // claim should be fail, because we mess with the nonce -// Component memory component = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); -// Component[] memory components = new Component[](1); -// components[0] = component; -// Claim memory claim = Claim({ -// allocatorData: abi.encode( -// erc7683Allocator.QUALIFICATION_TYPEHASH(), defaultTargetBlock, defaultMaximumBlocksAfterTarget -// ), -// sponsorSignature: '', -// sponsor: user, -// nonce: defaultNonce, -// expires: compact_.expires, -// witness: keccak256(abi.encode(keccak256(bytes(mandateTypeString)), mandate_)), -// witnessTypestring: witnessTypeString, -// id: usdcId, -// allocatedAmount: defaultAmount, -// claimants: components -// }); -// vm.prank(arbiter); -// vm.expectRevert(abi.encodeWithSelector(0x8baa579f)); // check for the InvalidSignature() error in the Compact contract -// compactContract.claim(claim); + // claim should be fail, because we mess with the nonce + Component memory component = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); + Component[] memory components = new Component[](1); + components[0] = component; + Claim memory claim = Claim({ + allocatorData: abi.encode( + erc7683Allocator.QUALIFICATION_TYPEHASH(), defaultTargetBlock, defaultMaximumBlocksAfterTarget + ), + sponsorSignature: '', + sponsor: user, + nonce: defaultNonce, + expires: compact_.expires, + witness: keccak256(abi.encode(keccak256(bytes(mandateTypeString)), mandate_)), + witnessTypestring: witnessTypeString, + id: usdcId, + allocatedAmount: defaultAmount, + claimants: components + }); + vm.prank(arbiter); + vm.expectRevert(abi.encodeWithSelector(0x8baa579f)); // check for the InvalidSignature() error in the Compact contract + compactContract.claim(claim); -// vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); -// vm.assertEq(compactContract.balanceOf(filler, usdcId), 0); -// } + vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); + vm.assertEq(compactContract.balanceOf(filler, usdcId), 0); + } -// function test_isValidSignature_successful_open() public { -// // Deposit tokens -// vm.startPrank(user); -// usdc.mint(user, defaultAmount); -// usdc.approve(address(compactContract), defaultAmount); -// compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + function test_isValidSignature_successful_open() public { + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); -// // register a claim -// Compact memory compact_ = _getCompact(); -// Mandate memory mandate_ = _getMandate(); + // register a claim + Compact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); -// bytes32 claimHash = _hashCompact(compact_, mandate_); -// bytes32 typeHash = _getTypeHash(); -// compactContract.register(claimHash, typeHash); + bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 typeHash = _getTypeHash(); + compactContract.register(claimHash, typeHash); -// address filler = makeAddr('filler'); -// vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); -// vm.assertEq(compactContract.balanceOf(filler, usdcId), 0); + address filler = makeAddr('filler'); + vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); + vm.assertEq(usdc.balanceOf(filler), 0); -// // we open the order and lock the tokens -// (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); -// erc7683Allocator.open(onChainCrossChainOrder_); -// vm.stopPrank(); + // we open the order and lock the tokens + (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); + erc7683Allocator.open(onChainCrossChainOrder_); + vm.stopPrank(); -// // claim should be successful -// Component memory component = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); -// Component[] memory components = new Component[](1); -// components[0] = component; -// Claim memory claim = Claim({ -// allocatorData: abi.encode( -// erc7683Allocator.QUALIFICATION_TYPEHASH(), defaultTargetBlock, defaultMaximumBlocksAfterTarget -// ), -// sponsorSignature: '', -// sponsor: user, -// nonce: defaultNonce, -// expires: compact_.expires, -// witness: keccak256( -// abi.encode( -// keccak256(bytes(mandateTypeString)), -// defaultOutputChainId, -// tribunal, -// mandate_.recipient, -// mandate_.expires, -// mandate_.token, -// mandate_.minimumAmount, -// mandate_.baselinePriorityFee, -// mandate_.scalingFactor, -// keccak256(abi.encodePacked(mandate_.decayCurve)), -// mandate_.salt -// ) -// ), -// witnessTypestring: witnessTypeString, -// id: usdcId, -// allocatedAmount: defaultAmount, -// claimants: components -// }); -// vm.prank(arbiter); -// compactContract.claim(claim); + // claim should be successful + bytes32 witness = keccak256( + abi.encode( + keccak256(bytes(mandateTypeString)), + defaultOutputChainId, + tribunal, + mandate_.recipient, + mandate_.expires, + mandate_.token, + mandate_.minimumAmount, + mandate_.baselinePriorityFee, + mandate_.scalingFactor, + keccak256(abi.encodePacked(mandate_.decayCurve)), + mandate_.salt + ) + ); + Component[] memory components = new Component[](1); + components[0] = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); + Claim memory claim = Claim({ + allocatorData: abi.encode(defaultTargetBlock, defaultMaximumBlocksAfterTarget), + sponsorSignature: '', + sponsor: user, + nonce: defaultNonce, + expires: compact_.expires, + witness: witness, + witnessTypestring: witnessTypeString, + id: usdcId, + allocatedAmount: defaultAmount, + claimants: components + }); + vm.prank(arbiter); + compactContract.claim(claim); -// vm.assertEq(compactContract.balanceOf(user, usdcId), 0); -// vm.assertEq(compactContract.balanceOf(filler, usdcId), defaultAmount); -// } + vm.assertEq(compactContract.balanceOf(user, usdcId), 0); + vm.assertEq(usdc.balanceOf(filler), defaultAmount); + } -// function test_isValidSignature_successful_openFor() public { -// // Deposit tokens -// vm.startPrank(user); -// usdc.mint(user, defaultAmount); -// usdc.approve(address(compactContract), defaultAmount); -// compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + function test_isValidSignature_successful_openFor() public { + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); -// // register a claim -// Compact memory compact_ = _getCompact(); -// Mandate memory mandate_ = _getMandate(); + // register a claim + Compact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); -// bytes32 claimHash = _hashCompact(compact_, mandate_); -// bytes32 typeHash = _getTypeHash(); -// compactContract.register(claimHash, typeHash); + bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 typeHash = _getTypeHash(); + compactContract.register(claimHash, typeHash); -// address filler = makeAddr('filler'); -// vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); -// vm.assertEq(compactContract.balanceOf(filler, usdcId), 0); + address filler = makeAddr('filler'); + vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); + vm.assertEq(usdc.balanceOf(filler), 0); -// // we open the order and lock the tokens -// (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = -// _getGaslessCrossChainOrder(); -// erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); -// vm.stopPrank(); + // we open the order and lock the tokens + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = + _getGaslessCrossChainOrder(); + erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); + vm.stopPrank(); -// // claim should be successful -// Component memory component = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); -// Component[] memory components = new Component[](1); -// components[0] = component; -// Claim memory claim = Claim({ -// allocatorData: abi.encode(erc7683Allocator.QUALIFICATION_TYPEHASH(), uint256(0), uint256(0)), -// sponsorSignature: '', -// sponsor: user, -// nonce: defaultNonce, -// expires: compact_.expires, -// witness: keccak256( -// abi.encode( -// keccak256(bytes(mandateTypeString)), -// defaultOutputChainId, -// tribunal, -// mandate_.recipient, -// mandate_.expires, -// mandate_.token, -// mandate_.minimumAmount, -// mandate_.baselinePriorityFee, -// mandate_.scalingFactor, -// keccak256(abi.encodePacked(mandate_.decayCurve)), -// mandate_.salt -// ) -// ), -// witnessTypestring: witnessTypeString, -// id: usdcId, -// allocatedAmount: defaultAmount, -// claimants: components -// }); -// vm.prank(arbiter); -// compactContract.claim(claim); + // claim should be successful + Component[] memory components = new Component[](1); + components[0] = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); + Claim memory claim = Claim({ + allocatorData: abi.encode(uint256(0), uint256(0)), + sponsorSignature: '', + sponsor: compact_.sponsor, + nonce: compact_.nonce, + expires: compact_.expires, + witness: _hashMandate(mandate_), + witnessTypestring: witnessTypeString, + id: uint256(bytes32(compact_.lockTag) | bytes32(uint256(uint160(compact_.token)))), + allocatedAmount: compact_.amount, + claimants: components + }); + vm.prank(arbiter); + compactContract.claim(claim); -// vm.assertEq(compactContract.balanceOf(user, usdcId), 0); -// vm.assertEq(compactContract.balanceOf(filler, usdcId), defaultAmount); -// } -// } + vm.assertEq(compactContract.balanceOf(user, usdcId), 0); + vm.assertEq(usdc.balanceOf(filler), defaultAmount); + } +} -// contract ERC7683Allocator_resolveFor is GaslessCrossChainOrderData { -// function test_resolve_successful() public { -// // WITH THE CURRENT ERC7683 DESIGN, THE SPONSOR SIGNATURE IS NOT PROVIDED TO THE RESOLVE FUNCTION -// // WHILE THE ResolvedCrossChainOrder WITHOUT THE SIGNATURE COULD STILL BE USED TO SIMULATE THE FILL, -// // ACTUALLY USING THIS DATA WOULD RESULT IN A LOSS OF THE REWARD TOKENS FOR THE FILLER. -// // THIS FEELS RISKY. -// // THE CURRENT ALTERNATIVE WOULD BE HAVE THE INPUT SIGNATURE BEING LEFT EMPTY AND INSTEAD BE PROVIDED IN THE THE orderData OF THE GaslessCrossChainOrderData. -// // THIS IS BOTH NOT IDEAL, SO CURRENTLY CHECKING FOR A SOLUTION. - -// (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = -// _getGaslessCrossChainOrder(); -// IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); -// IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); -// IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); -// maxSpent[0] = IOriginSettler.Output({ -// token: bytes32(uint256(uint160(defaultOutputToken))), -// amount: type(uint256).max, -// recipient: bytes32(uint256(uint160(user))), -// chainId: defaultOutputChainId -// }); -// minReceived[0] = IOriginSettler.Output({ -// token: bytes32(uint256(uint160(address(usdc)))), -// amount: defaultAmount, -// recipient: '', -// chainId: block.chainid -// }); -// TribunalClaim memory claim = TribunalClaim({ -// chainId: block.chainid, -// compact: _getCompact(), -// sponsorSignature: '', // sponsorSignature, // THE SIGNATURE MUST BE ADDED MANUALLY BY THE FILLER WITH THE CURRENT SYSTEM, BEFORE FILLING THE ORDER ON THE TARGET CHAIN -// allocatorSignature: '' -// }); -// fillInstructions[0] = IOriginSettler.FillInstruction({ -// destinationChainId: defaultOutputChainId, -// destinationSettler: bytes32(uint256(uint160(tribunal))), -// originData: abi.encode(claim, _getMandate(), uint256(0), uint256(0)) -// }); +contract ERC7683Allocator_resolveFor is GaslessCrossChainOrderData { + function test_resolve_successful() public { + // WITH THE CURRENT ERC7683 DESIGN, THE SPONSOR SIGNATURE IS NOT PROVIDED TO THE RESOLVE FUNCTION + // WHILE THE ResolvedCrossChainOrder WITHOUT THE SIGNATURE COULD STILL BE USED TO SIMULATE THE FILL, + // ACTUALLY USING THIS DATA WOULD RESULT IN A LOSS OF THE REWARD TOKENS FOR THE FILLER. + // THIS FEELS RISKY. + // THE CURRENT ALTERNATIVE WOULD BE HAVE THE INPUT SIGNATURE BEING LEFT EMPTY AND INSTEAD BE PROVIDED IN THE THE orderData OF THE GaslessCrossChainOrderData. + // THIS IS BOTH NOT IDEAL, SO CURRENTLY CHECKING FOR A SOLUTION. + + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = + _getGaslessCrossChainOrder(); + IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); + IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); + IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); + maxSpent[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(defaultOutputToken))), + amount: type(uint256).max, + recipient: bytes32(uint256(uint160(user))), + chainId: defaultOutputChainId + }); + minReceived[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(address(usdc)))), + amount: defaultAmount, + recipient: '', + chainId: block.chainid + }); + TribunalClaim memory claim = TribunalClaim({ + chainId: block.chainid, + compact: _getCompact(), + sponsorSignature: '', // sponsorSignature, // THE SIGNATURE MUST BE ADDED MANUALLY BY THE FILLER WITH THE CURRENT SYSTEM, BEFORE FILLING THE ORDER ON THE TARGET CHAIN + allocatorSignature: '' + }); + fillInstructions[0] = IOriginSettler.FillInstruction({ + destinationChainId: defaultOutputChainId, + destinationSettler: bytes32(uint256(uint160(tribunal))), + originData: abi.encode(claim, _getMandate(), uint256(0), uint256(0)) + }); -// IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ -// user: user, -// originChainId: block.chainid, -// openDeadline: uint32(_getClaimExpiration()), -// fillDeadline: uint32(_getFillExpiration()), -// orderId: bytes32(defaultNonce), -// maxSpent: maxSpent, -// minReceived: minReceived, -// fillInstructions: fillInstructions -// }); -// IOriginSettler.ResolvedCrossChainOrder memory resolved = -// erc7683Allocator.resolveFor(gaslessCrossChainOrder_, ''); -// assertEq(resolved.user, resolvedCrossChainOrder.user); -// assertEq(resolved.originChainId, resolvedCrossChainOrder.originChainId); -// assertEq(resolved.openDeadline, resolvedCrossChainOrder.openDeadline); -// assertEq(resolved.fillDeadline, resolvedCrossChainOrder.fillDeadline); -// assertEq(resolved.orderId, resolvedCrossChainOrder.orderId); -// assertEq(resolved.maxSpent.length, resolvedCrossChainOrder.maxSpent.length); -// assertEq(resolved.maxSpent[0].token, resolvedCrossChainOrder.maxSpent[0].token); -// assertEq(resolved.maxSpent[0].amount, resolvedCrossChainOrder.maxSpent[0].amount); -// assertEq(resolved.maxSpent[0].recipient, resolvedCrossChainOrder.maxSpent[0].recipient); -// assertEq(resolved.maxSpent[0].chainId, resolvedCrossChainOrder.maxSpent[0].chainId); -// assertEq(resolved.minReceived.length, resolvedCrossChainOrder.minReceived.length); -// assertEq(resolved.minReceived[0].token, resolvedCrossChainOrder.minReceived[0].token); -// assertEq(resolved.minReceived[0].amount, resolvedCrossChainOrder.minReceived[0].amount); -// assertEq(resolved.minReceived[0].recipient, resolvedCrossChainOrder.minReceived[0].recipient); -// assertEq(resolved.minReceived[0].chainId, resolvedCrossChainOrder.minReceived[0].chainId); -// assertEq(resolved.fillInstructions.length, resolvedCrossChainOrder.fillInstructions.length); -// assertEq( -// resolved.fillInstructions[0].destinationChainId, -// resolvedCrossChainOrder.fillInstructions[0].destinationChainId -// ); -// assertEq( -// resolved.fillInstructions[0].destinationSettler, -// resolvedCrossChainOrder.fillInstructions[0].destinationSettler -// ); -// assertEq(resolved.fillInstructions[0].originData, resolvedCrossChainOrder.fillInstructions[0].originData); -// } -// } + IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ + user: user, + originChainId: block.chainid, + openDeadline: uint32(_getClaimExpiration()), + fillDeadline: uint32(_getFillExpiration()), + orderId: bytes32(defaultNonce), + maxSpent: maxSpent, + minReceived: minReceived, + fillInstructions: fillInstructions + }); + IOriginSettler.ResolvedCrossChainOrder memory resolved = + erc7683Allocator.resolveFor(gaslessCrossChainOrder_, ''); + assertEq(resolved.user, resolvedCrossChainOrder.user); + assertEq(resolved.originChainId, resolvedCrossChainOrder.originChainId); + assertEq(resolved.openDeadline, resolvedCrossChainOrder.openDeadline); + assertEq(resolved.fillDeadline, resolvedCrossChainOrder.fillDeadline); + assertEq(resolved.orderId, resolvedCrossChainOrder.orderId); + assertEq(resolved.maxSpent.length, resolvedCrossChainOrder.maxSpent.length); + assertEq(resolved.maxSpent[0].token, resolvedCrossChainOrder.maxSpent[0].token); + assertEq(resolved.maxSpent[0].amount, resolvedCrossChainOrder.maxSpent[0].amount); + assertEq(resolved.maxSpent[0].recipient, resolvedCrossChainOrder.maxSpent[0].recipient); + assertEq(resolved.maxSpent[0].chainId, resolvedCrossChainOrder.maxSpent[0].chainId); + assertEq(resolved.minReceived.length, resolvedCrossChainOrder.minReceived.length); + assertEq(resolved.minReceived[0].token, resolvedCrossChainOrder.minReceived[0].token); + assertEq(resolved.minReceived[0].amount, resolvedCrossChainOrder.minReceived[0].amount); + assertEq(resolved.minReceived[0].recipient, resolvedCrossChainOrder.minReceived[0].recipient); + assertEq(resolved.minReceived[0].chainId, resolvedCrossChainOrder.minReceived[0].chainId); + assertEq(resolved.fillInstructions.length, resolvedCrossChainOrder.fillInstructions.length); + assertEq( + resolved.fillInstructions[0].destinationChainId, + resolvedCrossChainOrder.fillInstructions[0].destinationChainId + ); + assertEq( + resolved.fillInstructions[0].destinationSettler, + resolvedCrossChainOrder.fillInstructions[0].destinationSettler + ); + assertEq(resolved.fillInstructions[0].originData, resolvedCrossChainOrder.fillInstructions[0].originData); + } +} -// contract ERC7683Allocator_resolve is OnChainCrossChainOrderData { -// function test_resolve_successful() public { -// (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); -// IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); -// IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); -// IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); -// maxSpent[0] = IOriginSettler.Output({ -// token: bytes32(uint256(uint160(defaultOutputToken))), -// amount: type(uint256).max, -// recipient: bytes32(uint256(uint160(user))), -// chainId: defaultOutputChainId -// }); -// minReceived[0] = IOriginSettler.Output({ -// token: bytes32(uint256(uint160(address(usdc)))), -// amount: defaultAmount, -// recipient: '', -// chainId: block.chainid -// }); -// TribunalClaim memory claim = TribunalClaim({ -// chainId: block.chainid, -// compact: _getCompact(), -// sponsorSignature: '', -// allocatorSignature: '' -// }); -// fillInstructions[0] = IOriginSettler.FillInstruction({ -// destinationChainId: defaultOutputChainId, -// destinationSettler: bytes32(uint256(uint160(tribunal))), -// originData: abi.encode(claim, _getMandate(), defaultTargetBlock, defaultMaximumBlocksAfterTarget) -// }); +contract ERC7683Allocator_resolve is OnChainCrossChainOrderData { + function test_resolve_successful() public { + (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); + IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); + IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); + IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); + maxSpent[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(defaultOutputToken))), + amount: type(uint256).max, + recipient: bytes32(uint256(uint160(user))), + chainId: defaultOutputChainId + }); + minReceived[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(address(usdc)))), + amount: defaultAmount, + recipient: '', + chainId: block.chainid + }); + TribunalClaim memory claim = TribunalClaim({ + chainId: block.chainid, + compact: _getCompact(), + sponsorSignature: '', + allocatorSignature: '' + }); + fillInstructions[0] = IOriginSettler.FillInstruction({ + destinationChainId: defaultOutputChainId, + destinationSettler: bytes32(uint256(uint160(tribunal))), + originData: abi.encode(claim, _getMandate(), defaultTargetBlock, defaultMaximumBlocksAfterTarget) + }); -// IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ -// user: user, -// originChainId: block.chainid, -// openDeadline: uint32(_getClaimExpiration()), -// fillDeadline: uint32(_getFillExpiration()), -// orderId: bytes32(defaultNonce), -// maxSpent: maxSpent, -// minReceived: minReceived, -// fillInstructions: fillInstructions -// }); -// IOriginSettler.ResolvedCrossChainOrder memory resolved = erc7683Allocator.resolve(onChainCrossChainOrder_); -// assertEq(resolved.user, resolvedCrossChainOrder.user); -// assertEq(resolved.originChainId, resolvedCrossChainOrder.originChainId); -// assertEq(resolved.openDeadline, resolvedCrossChainOrder.openDeadline); -// assertEq(resolved.fillDeadline, resolvedCrossChainOrder.fillDeadline); -// assertEq(resolved.orderId, resolvedCrossChainOrder.orderId); -// assertEq(resolved.maxSpent.length, resolvedCrossChainOrder.maxSpent.length); -// assertEq(resolved.maxSpent[0].token, resolvedCrossChainOrder.maxSpent[0].token); -// assertEq(resolved.maxSpent[0].amount, resolvedCrossChainOrder.maxSpent[0].amount); -// assertEq(resolved.maxSpent[0].recipient, resolvedCrossChainOrder.maxSpent[0].recipient); -// assertEq(resolved.maxSpent[0].chainId, resolvedCrossChainOrder.maxSpent[0].chainId); -// assertEq(resolved.minReceived.length, resolvedCrossChainOrder.minReceived.length); -// assertEq(resolved.minReceived[0].token, resolvedCrossChainOrder.minReceived[0].token); -// assertEq(resolved.minReceived[0].amount, resolvedCrossChainOrder.minReceived[0].amount); -// assertEq(resolved.minReceived[0].recipient, resolvedCrossChainOrder.minReceived[0].recipient); -// assertEq(resolved.minReceived[0].chainId, resolvedCrossChainOrder.minReceived[0].chainId); -// assertEq(resolved.fillInstructions.length, resolvedCrossChainOrder.fillInstructions.length); -// assertEq( -// resolved.fillInstructions[0].destinationChainId, -// resolvedCrossChainOrder.fillInstructions[0].destinationChainId -// ); -// assertEq( -// resolved.fillInstructions[0].destinationSettler, -// resolvedCrossChainOrder.fillInstructions[0].destinationSettler -// ); -// assertEq(resolved.fillInstructions[0].originData, resolvedCrossChainOrder.fillInstructions[0].originData); -// } -// } + IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ + user: user, + originChainId: block.chainid, + openDeadline: uint32(_getClaimExpiration()), + fillDeadline: uint32(_getFillExpiration()), + orderId: bytes32(defaultNonce), + maxSpent: maxSpent, + minReceived: minReceived, + fillInstructions: fillInstructions + }); + IOriginSettler.ResolvedCrossChainOrder memory resolved = erc7683Allocator.resolve(onChainCrossChainOrder_); + assertEq(resolved.user, resolvedCrossChainOrder.user); + assertEq(resolved.originChainId, resolvedCrossChainOrder.originChainId); + assertEq(resolved.openDeadline, resolvedCrossChainOrder.openDeadline); + assertEq(resolved.fillDeadline, resolvedCrossChainOrder.fillDeadline); + assertEq(resolved.orderId, resolvedCrossChainOrder.orderId); + assertEq(resolved.maxSpent.length, resolvedCrossChainOrder.maxSpent.length); + assertEq(resolved.maxSpent[0].token, resolvedCrossChainOrder.maxSpent[0].token); + assertEq(resolved.maxSpent[0].amount, resolvedCrossChainOrder.maxSpent[0].amount); + assertEq(resolved.maxSpent[0].recipient, resolvedCrossChainOrder.maxSpent[0].recipient); + assertEq(resolved.maxSpent[0].chainId, resolvedCrossChainOrder.maxSpent[0].chainId); + assertEq(resolved.minReceived.length, resolvedCrossChainOrder.minReceived.length); + assertEq(resolved.minReceived[0].token, resolvedCrossChainOrder.minReceived[0].token); + assertEq(resolved.minReceived[0].amount, resolvedCrossChainOrder.minReceived[0].amount); + assertEq(resolved.minReceived[0].recipient, resolvedCrossChainOrder.minReceived[0].recipient); + assertEq(resolved.minReceived[0].chainId, resolvedCrossChainOrder.minReceived[0].chainId); + assertEq(resolved.fillInstructions.length, resolvedCrossChainOrder.fillInstructions.length); + assertEq( + resolved.fillInstructions[0].destinationChainId, + resolvedCrossChainOrder.fillInstructions[0].destinationChainId + ); + assertEq( + resolved.fillInstructions[0].destinationSettler, + resolvedCrossChainOrder.fillInstructions[0].destinationSettler + ); + assertEq(resolved.fillInstructions[0].originData, resolvedCrossChainOrder.fillInstructions[0].originData); + } +} -// contract ERC7683Allocator_getCompactWitnessTypeString is MocksSetup { -// function test_getCompactWitnessTypeString() public view { -// assertEq( -// erc7683Allocator.getCompactWitnessTypeString(), -// 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt))' -// ); -// } -// } +contract ERC7683Allocator_getCompactWitnessTypeString is MocksSetup { + function test_getCompactWitnessTypeString() public view { + assertEq( + erc7683Allocator.getCompactWitnessTypeString(), + 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt))' + ); + } +} -// contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData { -// function test_revert_invalidNonce(uint256 nonce_) public { -// address expectedSponsor; -// assembly ("memory-safe") { -// expectedSponsor := shr(96, nonce_) -// } -// vm.assume(user != expectedSponsor); +contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData { + function test_revert_invalidNonce(uint256 nonce_) public { + address expectedSponsor; + assembly ("memory-safe") { + expectedSponsor := shr(96, nonce_) + } + vm.assume(user != expectedSponsor); -// vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidNonce.selector, nonce_)); -// erc7683Allocator.checkNonce(user, nonce_); -// } + vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidNonce.selector, nonce_)); + erc7683Allocator.checkNonce(user, nonce_); + } -// function test_checkNonce_unused(uint96 nonce_) public view { -// address sponsor = user; -// uint256 nonce; -// assembly ("memory-safe") { -// nonce := or(shl(96, sponsor), shr(160, shl(160, nonce_))) -// } -// assertEq(erc7683Allocator.checkNonce(sponsor, nonce), true); -// } + function test_checkNonce_unused(uint96 nonce_) public view { + address sponsor = user; + uint256 nonce; + assembly ("memory-safe") { + nonce := or(shl(96, sponsor), shr(160, shl(160, nonce_))) + } + assertEq(erc7683Allocator.checkNonce(sponsor, nonce), true); + } -// function test_checkNonce_used() public { -// // Deposit tokens -// vm.startPrank(user); -// usdc.mint(user, defaultAmount); -// usdc.approve(address(compactContract), defaultAmount); -// compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + function test_checkNonce_used() public { + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); -// // register a claim -// Compact memory compact_ = _getCompact(); -// Mandate memory mandate_ = _getMandate(); + // register a claim + Compact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); -// bytes32 claimHash = _hashCompact(compact_, mandate_); -// bytes32 typeHash = _getTypeHash(); -// compactContract.register(claimHash, typeHash); + bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 typeHash = _getTypeHash(); + compactContract.register(claimHash, typeHash); -// (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); -// erc7683Allocator.open(onChainCrossChainOrder_); + (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); + erc7683Allocator.open(onChainCrossChainOrder_); -// vm.assertEq(erc7683Allocator.checkNonce(user, defaultNonce), false); -// vm.stopPrank(); -// } + vm.assertEq(erc7683Allocator.checkNonce(user, defaultNonce), false); + vm.stopPrank(); + } -// function test_checkNonce_fuzz(uint8 nonce_) public { -// uint256 nonce = uint256(bytes32(abi.encodePacked(user, uint96(nonce_)))); + function test_checkNonce_fuzz(uint8 nonce_) public { + uint256 nonce = uint256(bytes32(abi.encodePacked(user, uint96(nonce_)))); -// bool sameNonce = nonce == defaultNonce; + bool sameNonce = nonce == defaultNonce; -// // Deposit tokens -// vm.startPrank(user); -// usdc.mint(user, defaultAmount); -// usdc.approve(address(compactContract), defaultAmount); -// compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); -// // register a claim -// Compact memory compact_ = _getCompact(); -// Mandate memory mandate_ = _getMandate(); + // register a claim + Compact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); -// bytes32 claimHash = _hashCompact(compact_, mandate_); -// bytes32 typeHash = _getTypeHash(); -// compactContract.register(claimHash, typeHash); + bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 typeHash = _getTypeHash(); + compactContract.register(claimHash, typeHash); -// (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); -// erc7683Allocator.open(onChainCrossChainOrder_); + (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); + erc7683Allocator.open(onChainCrossChainOrder_); -// vm.assertEq(erc7683Allocator.checkNonce(user, nonce), !sameNonce); + vm.assertEq(erc7683Allocator.checkNonce(user, nonce), !sameNonce); -// vm.stopPrank(); -// } -// } + vm.stopPrank(); + } +} From ae5b81de3606abd7e24931e28c7abe599856837d Mon Sep 17 00:00:00 2001 From: mgretzke Date: Thu, 10 Jul 2025 16:03:41 +0200 Subject: [PATCH 28/63] removed logs in tests --- test/ERC7683Allocator.t.sol | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index d65b601..ba892e1 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -109,8 +109,6 @@ abstract contract CreateHash is MocksSetup { compactHash ) ); - console.log('digest'); - console.logBytes32(digest); } function _hashCompact(Compact memory data, Mandate memory mandate) internal view returns (bytes32 compactHash) { @@ -128,12 +126,6 @@ abstract contract CreateHash is MocksSetup { mandateHash ) ); - console.log('mandateHash'); - console.logBytes32(mandateHash); - console.log('compactHash'); - console.logBytes32(compactHash); - console.log('typehash'); - console.logBytes32(keccak256(bytes(compactWitnessTypeString))); } function _hashMandate(Mandate memory mandate) internal view returns (bytes32) { From 602447731e17f776e50a6d70075f8d5bb6b2bb75 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Fri, 11 Jul 2025 17:01:49 +0200 Subject: [PATCH 29/63] optimizations and fixes --- foundry.toml | 8 +++ src/allocators/ERC7683Allocator.sol | 80 +++-------------------------- test/ERC7683Allocator.t.sol | 2 +- 3 files changed, 16 insertions(+), 74 deletions(-) diff --git a/foundry.toml b/foundry.toml index f6597d1..645b201 100644 --- a/foundry.toml +++ b/foundry.toml @@ -21,6 +21,14 @@ remappings = [ "@solady=lib/solady/src", ] +additional_compiler_profiles = [ + { name = "test", via_ir = false, optimizer = false } +] + +compilation_restrictions = [ + { paths = "test/**", via_ir = false, optimizer = false } +] + [profile.ci] inherit = "default" optimizer_runs = 200 # Override optimizer runs to reduce the compact contract sizes diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index 428357b..d9a238c 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -43,6 +43,10 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { uint8 internal constant ORDERDATA_GASLESS_LOCKTAG_OFFSET = 0x20; + uint16 internal constant ORDERDATA_MANDATE_OFFSET = 0x1a4; // orderData.offset = 0xc4 + Mandate.chainId offset = 0xe0; + + uint16 internal constant ORDERDATA_GASLESS_MANDATE_OFFSET = 0x224; // orderData.offset = 0x1a4 + Mandate.chainId offset = 0x80; + bytes32 immutable _COMPACT_DOMAIN_SEPARATOR; constructor(address compactContract_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_) @@ -67,10 +71,6 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { // Decode the orderData bytes calldata orderDataGaslessBytes = LibBytes.dynamicStructInCalldata(order_.orderData, 0x00); - uint256 mandateOffset; - assembly ("memory-safe") { - mandateOffset := add(orderDataGaslessBytes.offset, 0x80) // mandate starts at orderData.chainId (0x80) - } // Extract the resolved order early to reduce stack pressure ResolvedCrossChainOrder memory resolvedOrder = _resolveOrder( @@ -84,7 +84,7 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { ); // Extract mandateHash early to reduce stack pressure - bytes32 mandateHash = _mandateHash(orderDataGaslessBytes, mandateOffset, order_.fillDeadline); + bytes32 mandateHash = _mandateHash(orderDataGaslessBytes, ORDERDATA_GASLESS_MANDATE_OFFSET, order_.fillDeadline); _open(orderDataGaslessBytes, ORDERDATA_GASLESS_LOCKTAG_OFFSET, sponsorSignature_, mandateHash, resolvedOrder); } @@ -99,16 +99,14 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { // Decode the orderData bytes calldata orderDataBytes = LibBytes.dynamicStructInCalldata(order.orderData, 0x00); OrderData calldata orderData; - uint256 mandateOffset; assembly ("memory-safe") { orderData := orderDataBytes.offset - mandateOffset := add(orderDataBytes.offset, 0xe0) // mandate starts at orderData.chainId } _checkMsgSender(orderData.sponsor); // Extract mandateHash early to reduce stack pressure - bytes32 mandateHash = _mandateHash(orderDataBytes, mandateOffset, order.fillDeadline); + bytes32 mandateHash = _mandateHash(orderDataBytes, ORDERDATA_MANDATE_OFFSET, order.fillDeadline); // Extract the resolved order early to reduce stack pressure ResolvedCrossChainOrder memory resolvedOrder = _resolveOrder( @@ -162,70 +160,6 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { ); } - // function registerClaim( - // bytes32 claimHash, // The message hash representing the claim. - // address, /* caller */ // The account initiating the registration. - // address arbiter, // The account tasked with verifying and submitting the claim. - // address sponsor, // The account to source the tokens from. - // uint256 nonce, // A parameter to enforce replay protection, scoped to allocator. - // uint256 expires, // The time at which the claim expires. - // uint256[2][] calldata idsAndAmounts, // The allocated token IDs and amounts. - // bytes calldata allocatorData // Arbitrary data provided by the caller. - // ) external override onlyCompact returns (bytes4) { - // if (idsAndAmounts.length > 1) { - // revert BatchCompactsNotSupported(); - // } - - // // Enforce a nonce where the most significant 96 bits are the nonce and the least significant 160 bits are the sponsor - // uint96 nonceWithoutAddress = _nonceValidation(sponsor, nonce); - // // Set a nonce or revert if it is already used - // _setNonce(sponsor, nonceWithoutAddress); - - // // We trust the compact to check the nonce and that this contract is the allocator connected to the id - // _checkExpiration(expires); - // (bytes12 lockTag, address token) = _separateId(idsAndAmounts[0][0]); - // bytes32 tokenHash = _checkForActiveAllocation(sponsor, lockTag, token); - // _checkForcedWithdrawal(sponsor, expires, lockTag, token); - // _checkBalance(sponsor, idsAndAmounts[0][0], idsAndAmounts[0][1]); // TODO: Should the Compact check this prior to the callback? - - // OderDataCallback memory orderDataCallback = abi.decode(allocatorData, (OderDataCallback)); - // bytes32 qualifiedClaimHash = keccak256( - // abi.encode( - // QUALIFICATION_TYPEHASH, - // claimHash, - // orderDataCallback.targetBlock, - // orderDataCallback.maximumBlocksAfterTarget - // ) - // ); - - // _lockTokens(tokenHash, idsAndAmounts[0][1], orderDataCallback.expires, qualifiedClaimHash); - - // OrderData memory orderData = OrderData({ - // arbiter: arbiter, - // sponsor: sponsor, - // nonce: nonce, - // expires: expires, - // lockTag: lockTag, - // inputToken: token, - // amount: idsAndAmounts[0][1], - // chainId: orderDataCallback.chainId, - // tribunal: orderDataCallback.tribunal, - // recipient: orderDataCallback.recipient, - // settlementToken: orderDataCallback.settlementToken, - // minimumAmount: orderDataCallback.minimumAmount, - // baselinePriorityFee: orderDataCallback.baselinePriorityFee, - // scalingFactor: orderDataCallback.scalingFactor, - // decayCurve: orderDataCallback.decayCurve, - // salt: orderDataCallback.salt, - // targetBlock: orderDataCallback.targetBlock, - // maximumBlocksAfterTarget: orderDataCallback.maximumBlocksAfterTarget - // }); - // // Emit an open event - // emit Open(bytes32(nonce), _resolveOrder(sponsor, uint32(orderDataCallback.expires), orderData, '')); - - // return this.registerClaim.selector; - // } - function authorizeClaim( bytes32 claimHash, // The message hash representing the claim. address, /* arbiter */ // The account tasked with verifying and submitting the claim. @@ -261,7 +195,7 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { /// @inheritdoc IERC7683Allocator function getCompactWitnessTypeString() external pure returns (string memory) { return - 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt))'; + 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,bytes12 lockTag,address token,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; } /// @inheritdoc IERC7683Allocator diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index ba892e1..768aeb0 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -1161,7 +1161,7 @@ contract ERC7683Allocator_getCompactWitnessTypeString is MocksSetup { function test_getCompactWitnessTypeString() public view { assertEq( erc7683Allocator.getCompactWitnessTypeString(), - 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt))' + 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,bytes12 lockTag,address token,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)' ); } } From 5baf3e6937f1d2a498dae5ab735726bbb5eb3299 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Mon, 14 Jul 2025 13:11:22 +0200 Subject: [PATCH 30/63] nonce redesign --- src/allocators/ERC7683Allocator.sol | 58 ++++++++-------------------- src/interfaces/IERC7683Allocator.sol | 3 +- test/ERC7683Allocator.t.sol | 10 ++--- 3 files changed, 22 insertions(+), 49 deletions(-) diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index d9a238c..212a535 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -36,9 +36,6 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { /// @notice keccak256("Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") bytes32 internal constant MANDATE_TYPEHASH = 0x74d9c10530859952346f3e046aa2981a24bb7524b8394eb45a9deddced9d6501; - /// @notice uint256(uint8(keccak256("ERC7683Allocator.nonce"))) - uint8 internal constant NONCE_MASTER_SLOT_SEED = 0x39; - uint8 internal constant ORDERDATA_LOCKTAG_OFFSET = 0x80; uint8 internal constant ORDERDATA_GASLESS_LOCKTAG_OFFSET = 0x20; @@ -199,13 +196,13 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { } /// @inheritdoc IERC7683Allocator - function checkNonce(address sponsor_, uint256 nonce_) external view returns (bool nonceFree_) { - uint96 nonceWithoutAddress = _nonceValidation(sponsor_, nonce_); - uint96 wordPos = uint96(nonceWithoutAddress / 256); - uint96 bitPos = uint96(nonceWithoutAddress % 256); + function checkNonce(uint256 nonce_, address sponsor_) external view returns (bool nonceFree_) { + _nonceValidation(sponsor_, nonce_); + uint256 word = nonce_ / 256; + uint256 bit = nonce_ % 256; assembly ("memory-safe") { - let masterSlot := or(shl(248, NONCE_MASTER_SLOT_SEED), or(shl(88, sponsor_), wordPos)) - nonceFree_ := iszero(and(sload(masterSlot), shl(bitPos, 1))) + let nonceBitmap := sload(or(NONCE_MASTER_SLOT_SEED, word)) + nonceFree_ := iszero(and(nonceBitmap, shl(bit, 1))) } return nonceFree_; } @@ -225,15 +222,15 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { ) internal { uint256 nonce_ = uint256(resolvedOrder_.orderId); // Enforce a nonce where the most significant 96 bits are the nonce and the least significant 160 bits are the sponsor - uint96 nonceWithoutAddress = _nonceValidation(resolvedOrder_.user, nonce_); + _nonceValidation(resolvedOrder_.user, nonce_); // Set a nonce or revert if it is already used - _setNonce(resolvedOrder_.user, nonceWithoutAddress); + _checkAndSetNonce(nonce_); // We do not enforce a specific tribunal or arbiter. This will allow to support new arbiters and tribunals after the deployment of the allocator // Going with an immutable arbiter and tribunal would limit support for new chains with a fully decentralized allocator bytes32 tokenHash = - _verifyAllocation(resolvedOrder_.user, nonce_, resolvedOrder_.openDeadline, orderData_, lockTagOffset); + _verifyAllocation(resolvedOrder_.user, resolvedOrder_.openDeadline, orderData_, lockTagOffset); // Create the Compact claim hash bytes32 claimHash; @@ -296,13 +293,11 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { emit Open(bytes32(nonce_), resolvedOrder_); } - function _verifyAllocation( - address sponsor_, - uint256 nonce_, - uint32 openDeadline_, - bytes calldata orderData_, - uint256 lockTagOffset - ) internal view returns (bytes32 tokenHash_) { + function _verifyAllocation(address sponsor_, uint32 openDeadline_, bytes calldata orderData_, uint256 lockTagOffset) + internal + view + returns (bytes32 tokenHash_) + { bytes12 lockTag; address inputToken; uint256 amount; @@ -315,9 +310,8 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { // Check for a valid allocation tokenHash_ = _checkForActiveAllocation(sponsor_, lockTag, inputToken); - _checkAllocator(lockTag, inputToken); + _checkAllocator(lockTag); _checkExpiration(openDeadline_); - _checkNonce(nonce_); _checkForcedWithdrawal(sponsor_, openDeadline_, lockTag, inputToken); _checkBalance(sponsor_, _getTokenId(lockTag, inputToken), amount); @@ -443,38 +437,18 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { return resolvedOrder; } - function _nonceValidation(address sponsor_, uint256 nonce_) internal pure returns (uint96 nonce) { + function _nonceValidation(address sponsor_, uint256 nonce_) internal pure { // Enforce a nonce where the least significant 96 bits are the nonce and the most significant 160 bits are the sponsors address // This ensures that the nonce is unique for a given sponsor address expectedSponsor; assembly ("memory-safe") { expectedSponsor := shr(96, nonce_) - nonce := shr(160, shl(160, nonce_)) } if (expectedSponsor != sponsor_) { revert InvalidNonce(nonce_); } } - function _setNonce(address sponsor_, uint96 nonce_) internal { - bool used; - uint96 wordPos = nonce_ / 256; // uint96 divided by 256 means it becomes a uint88 (11 bytes) - uint96 bitPos = nonce_ % 256; - assembly ("memory-safe") { - // [NONCE_MASTER_SLOT_SEED - 1 byte][sponsor address - 20 bytes][wordPos - 11 bytes] - let masterSlot := or(shl(248, NONCE_MASTER_SLOT_SEED), or(shl(88, sponsor_), wordPos)) - let previouslyUsedNonces := sload(masterSlot) - if and(previouslyUsedNonces, shl(bitPos, 1)) { used := 1 } - { - let usedNonces := or(previouslyUsedNonces, shl(bitPos, 1)) - sstore(masterSlot, usedNonces) - } - } - if (used) { - revert NonceAlreadyInUse(uint256(bytes32(abi.encodePacked(sponsor_, nonce_)))); - } - } - function _addressToBytes32(address address_) internal pure returns (bytes32 output_) { assembly ("memory-safe") { output_ := shr(96, shl(96, address_)) diff --git a/src/interfaces/IERC7683Allocator.sol b/src/interfaces/IERC7683Allocator.sol index 14be819..9842889 100644 --- a/src/interfaces/IERC7683Allocator.sol +++ b/src/interfaces/IERC7683Allocator.sol @@ -79,7 +79,6 @@ interface IERC7683Allocator is IOriginSettler { error InvalidOriginSettler(address originSettler, address expectedOriginSettler); error InvalidOrderDataType(bytes32 orderDataType, bytes32 expectedOrderDataType); error InvalidNonce(uint256 nonce); - error NonceAlreadyInUse(uint256 nonce); error InvalidSignature(address signer, address expectedSigner); error InvalidRegistration(address sponsor, bytes32 claimHash); error BatchCompactsNotSupported(); @@ -106,7 +105,7 @@ interface IERC7683Allocator is IOriginSettler { /// @notice Checks if a nonce is free to be used /// @dev The nonce is the most significant 96 bits. The least significant 160 bits must be the sponsor address - function checkNonce(address sponsor_, uint256 nonce_) external view returns (bool nonceFree_); + function checkNonce(uint256 nonce_, address sponsor_) external view returns (bool nonceFree_); /// @notice Creates the filler data for the open event to be used on the IDestinationSettler /// @param claimant_ The address claiming the origin tokens after a successful fill (typically the address of the filler) diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index 768aeb0..5bca0ee 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -732,7 +732,7 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder2, bytes memory sponsorSignature2) = _getGaslessCrossChainOrder(); vm.prank(user); - vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.NonceAlreadyInUse.selector, defaultNonce)); + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.NonceAlreadyInUse.selector, defaultNonce)); erc7683Allocator.openFor(gaslessCrossChainOrder2, sponsorSignature2, ''); } } @@ -1175,7 +1175,7 @@ contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData { vm.assume(user != expectedSponsor); vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidNonce.selector, nonce_)); - erc7683Allocator.checkNonce(user, nonce_); + erc7683Allocator.checkNonce(nonce_, user); } function test_checkNonce_unused(uint96 nonce_) public view { @@ -1184,7 +1184,7 @@ contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData { assembly ("memory-safe") { nonce := or(shl(96, sponsor), shr(160, shl(160, nonce_))) } - assertEq(erc7683Allocator.checkNonce(sponsor, nonce), true); + assertEq(erc7683Allocator.checkNonce(nonce, user), true); } function test_checkNonce_used() public { @@ -1205,7 +1205,7 @@ contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData { (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); erc7683Allocator.open(onChainCrossChainOrder_); - vm.assertEq(erc7683Allocator.checkNonce(user, defaultNonce), false); + vm.assertEq(erc7683Allocator.checkNonce(defaultNonce, user), false); vm.stopPrank(); } @@ -1231,7 +1231,7 @@ contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData { (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); erc7683Allocator.open(onChainCrossChainOrder_); - vm.assertEq(erc7683Allocator.checkNonce(user, nonce), !sameNonce); + vm.assertEq(erc7683Allocator.checkNonce(nonce, user), !sameNonce); vm.stopPrank(); } From 8adc58c6c1db72c9f615093f70c85c011819eaa6 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Fri, 18 Jul 2025 12:29:50 +0200 Subject: [PATCH 31/63] cleanup + 64 bytes signature support --- src/allocators/ERC7683Allocator.sol | 92 +++++++++++++++------------- src/interfaces/IERC7683Allocator.sol | 3 +- 2 files changed, 53 insertions(+), 42 deletions(-) diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index 212a535..1915e81 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -5,10 +5,9 @@ pragma solidity ^0.8.27; import {IERC7683Allocator} from '../interfaces/IERC7683Allocator.sol'; import {SimpleAllocator} from './SimpleAllocator.sol'; import {Claim, Mandate} from './types/TribunalStructs.sol'; +import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; -import {ECDSA} from '@openzeppelin/contracts/utils/cryptography/ECDSA.sol'; - import {LibBytes} from '@solady/utils/LibBytes.sol'; import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; import {Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; @@ -157,6 +156,7 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { ); } + /// @inheritdoc IAllocator function authorizeClaim( bytes32 claimHash, // The message hash representing the claim. address, /* arbiter */ // The account tasked with verifying and submitting the claim. @@ -165,7 +165,7 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { uint256, /* expires */ // The time at which the claim expires. uint256[2][] calldata idsAndAmounts, // The allocated token IDs and amounts. bytes calldata allocatorData // Arbitrary data provided by the arbiter. - ) external override onlyCompact returns (bytes4) { + ) external override(SimpleAllocator, IAllocator) onlyCompact returns (bytes4) { uint256 length = idsAndAmounts.length; if (length > 1) { revert BatchCompactsNotSupported(); @@ -189,6 +189,26 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { return this.authorizeClaim.selector; } + /// @inheritdoc IAllocator + function isClaimAuthorized( + bytes32 claimHash, + address, /*arbiter*/ // The account tasked with verifying and submitting the claim. + address, /*sponsor*/ // The account to source the tokens from. + uint256, /*nonce*/ // A parameter to enforce replay protection, scoped to allocator. + uint256 expires, // The time at which the claim expires. + uint256[2][] calldata idsAndAmounts, // The allocated token IDs and amounts. + bytes calldata allocatorData // Arbitrary data provided by the arbiter. + ) external view override(SimpleAllocator, IAllocator) returns (bool) { + uint256 length = idsAndAmounts.length; + if (length > 1) { + revert BatchCompactsNotSupported(); + } + (uint256 targetBlock, uint256 maximumBlocksAfterTarget) = abi.decode(allocatorData, (uint256, uint256)); + claimHash = keccak256(abi.encode(QUALIFICATION_TYPEHASH, claimHash, targetBlock, maximumBlocksAfterTarget)); + + return _claim[claimHash] && expires > block.timestamp; + } + /// @inheritdoc IERC7683Allocator function getCompactWitnessTypeString() external pure returns (string memory) { return @@ -253,7 +273,7 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { if (sponsorSignature_.length > 0) { bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), _COMPACT_DOMAIN_SEPARATOR, claimHash)); // confirm the signature matches the digest - address signer_ = ECDSA.recover(digest, sponsorSignature_); + address signer_ = _recoverSigner(digest, sponsorSignature_); if (resolvedOrder_.user != signer_) { revert InvalidSignature(resolvedOrder_.user, signer_); } @@ -284,7 +304,6 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { assembly ("memory-safe") { // load amount from orderData_ amount := calldataload(add(lockTagAbsoluteOffset, 0x40)) - let m := mload(0x40) } _lockTokens(tokenHash, amount, resolvedOrder_.openDeadline, qualifiedClaimHash); @@ -293,6 +312,12 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { emit Open(bytes32(nonce_), resolvedOrder_); } + function _lockTokens(bytes32 tokenHash, uint256 amount, uint256 expires, bytes32 claimHash) internal { + // Lock the tokens + _claim[claimHash] = true; + _allocation[tokenHash] = _allocationData(amount, expires); + } + function _verifyAllocation(address sponsor_, uint32 openDeadline_, bytes calldata orderData_, uint256 lockTagOffset) internal view @@ -318,12 +343,6 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { return tokenHash_; } - function _lockTokens(bytes32 tokenHash, uint256 amount, uint256 expires, bytes32 claimHash) internal { - // Lock the tokens - _claim[claimHash] = true; - _allocation[tokenHash] = _allocationData(amount, expires); - } - function _resolveOrder( address sponsor, uint256 nonce, @@ -449,10 +468,24 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { } } - function _addressToBytes32(address address_) internal pure returns (bytes32 output_) { - assembly ("memory-safe") { - output_ := shr(96, shl(96, address_)) + function _recoverSigner(bytes32 digest, bytes calldata signature) internal pure returns (address) { + bytes32 r; + bytes32 s; + uint8 v; + + if (signature.length == 65) { + (r, s) = abi.decode(signature, (bytes32, bytes32)); + v = uint8(signature[64]); + } else if (signature.length == 64) { + bytes32 vs; + (r, vs) = abi.decode(signature, (bytes32, bytes32)); + v = uint8(uint256(vs >> 255) + 27); + s = vs << 1 >> 1; + } else { + return address(0); } + + return ecrecover(digest, v, r, s); } function _mandateHash(bytes calldata orderData, uint256 mandateOffset, uint32 fillDeadline) @@ -493,32 +526,9 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { // 0x140: salt } - function _convertGaslessOrderData( - address sponsor_, - uint256 nonce_, - uint32 openDeadline_, - OrderDataGasless calldata orderDataGasless_ - ) internal pure returns (OrderData memory orderData_) { - orderData_ = OrderData({ - arbiter: orderDataGasless_.arbiter, - sponsor: sponsor_, - nonce: nonce_, - expires: openDeadline_, - lockTag: orderDataGasless_.lockTag, - inputToken: orderDataGasless_.inputToken, - amount: orderDataGasless_.amount, - chainId: orderDataGasless_.chainId, - tribunal: orderDataGasless_.tribunal, - recipient: orderDataGasless_.recipient, - settlementToken: orderDataGasless_.settlementToken, - minimumAmount: orderDataGasless_.minimumAmount, - baselinePriorityFee: orderDataGasless_.baselinePriorityFee, - scalingFactor: orderDataGasless_.scalingFactor, - decayCurve: orderDataGasless_.decayCurve, - salt: orderDataGasless_.salt, - targetBlock: 0, - maximumBlocksAfterTarget: 0 - }); - return orderData_; + function _addressToBytes32(address address_) internal pure returns (bytes32 output_) { + assembly ("memory-safe") { + output_ := shr(96, shl(96, address_)) + } } } diff --git a/src/interfaces/IERC7683Allocator.sol b/src/interfaces/IERC7683Allocator.sol index 9842889..8aef568 100644 --- a/src/interfaces/IERC7683Allocator.sol +++ b/src/interfaces/IERC7683Allocator.sol @@ -3,8 +3,9 @@ pragma solidity ^0.8.27; import {IOriginSettler} from './ERC7683/IOriginSettler.sol'; +import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; -interface IERC7683Allocator is IOriginSettler { +interface IERC7683Allocator is IOriginSettler, IAllocator { struct OrderData { // COMPACT address arbiter; // The account tasked with verifying and submitting the claim. From dca36f40621706608ab7e4526177e7184553c441 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Fri, 18 Jul 2025 13:10:55 +0200 Subject: [PATCH 32/63] interface clean up --- src/allocators/ERC7683Allocator.sol | 9 +++--- src/interfaces/IERC7683Allocator.sol | 41 ---------------------------- 2 files changed, 5 insertions(+), 45 deletions(-) diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index 1915e81..df231af 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.27; +import {IOriginSettler} from '../interfaces/ERC7683/IOriginSettler.sol'; import {IERC7683Allocator} from '../interfaces/IERC7683Allocator.sol'; import {SimpleAllocator} from './SimpleAllocator.sol'; import {Claim, Mandate} from './types/TribunalStructs.sol'; @@ -51,7 +52,7 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { _COMPACT_DOMAIN_SEPARATOR = ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(); } - /// @inheritdoc IERC7683Allocator + /// @inheritdoc IOriginSettler function openFor(GaslessCrossChainOrder calldata order_, bytes calldata sponsorSignature_, bytes calldata) external { @@ -85,7 +86,7 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { _open(orderDataGaslessBytes, ORDERDATA_GASLESS_LOCKTAG_OFFSET, sponsorSignature_, mandateHash, resolvedOrder); } - /// @inheritdoc IERC7683Allocator + /// @inheritdoc IOriginSettler function open(OnchainCrossChainOrder calldata order) external { // Check if orderDataType is the one expected by the allocator if (order.orderDataType != ORDERDATA_TYPEHASH) { @@ -118,7 +119,7 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { _open(orderDataBytes, ORDERDATA_LOCKTAG_OFFSET, LibBytes.emptyCalldata(), mandateHash, resolvedOrder); } - /// @inheritdoc IERC7683Allocator + /// @inheritdoc IOriginSettler function resolveFor(GaslessCrossChainOrder calldata order_, bytes calldata) external view @@ -137,7 +138,7 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { ); } - /// @inheritdoc IERC7683Allocator + /// @inheritdoc IOriginSettler function resolve(OnchainCrossChainOrder calldata order_) external view returns (ResolvedCrossChainOrder memory) { bytes calldata orderDataBytes = LibBytes.dynamicStructInCalldata(order_.orderData, 0x00); OrderData calldata orderData; diff --git a/src/interfaces/IERC7683Allocator.sol b/src/interfaces/IERC7683Allocator.sol index 8aef568..2d26a38 100644 --- a/src/interfaces/IERC7683Allocator.sol +++ b/src/interfaces/IERC7683Allocator.sol @@ -53,30 +53,6 @@ interface IERC7683Allocator is IOriginSettler, IAllocator { bytes32 salt; // Replay protection parameter } - struct OderDataCallback { - // COMPACT - // address arbiter; // The account tasked with verifying and submitting the claim. - // address sponsor; // The account to source the tokens from. - // uint256 nonce; // A parameter to enforce replay protection, scoped to allocator. - // uint256 expires; // The time at which the claim expires. - // uint256 id; // The token ID of the ERC6909 token to allocate. - // uint256 amount; // The amount of ERC6909 tokens to allocate. - // MANDATE - uint256 chainId; // (implicit arg, included in EIP712 payload) - address tribunal; // (implicit arg, included in EIP712 payload) - address recipient; // Recipient of settled tokens - uint256 expires; // Mandate expiration timestamp - address settlementToken; // Settlement token (address(0) for native) - uint256 minimumAmount; // Minimum settlement amount - uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in - uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline) - uint256[] decayCurve; // Block durations, fill increases, & claim decreases. - bytes32 salt; // Replay protection parameter - // ADDITIONAL INPUT - uint256 targetBlock; // The block number at the target chain on which the PGA is executed / the reverse dutch auction starts. - uint256 maximumBlocksAfterTarget; // Blocks after target block that are still fillable. - } - error InvalidOriginSettler(address originSettler, address expectedOriginSettler); error InvalidOrderDataType(bytes32 orderDataType, bytes32 expectedOrderDataType); error InvalidNonce(uint256 nonce); @@ -84,23 +60,6 @@ interface IERC7683Allocator is IOriginSettler, IAllocator { error InvalidRegistration(address sponsor, bytes32 claimHash); error BatchCompactsNotSupported(); - /// @inheritdoc IOriginSettler - function openFor(GaslessCrossChainOrder calldata order, bytes calldata signature, bytes calldata originFillerData) - external; - - /// @inheritdoc IOriginSettler - /// @dev Requires the user to have previously registered the claim hash on the compact - function open(OnchainCrossChainOrder calldata order) external; - - /// @inheritdoc IOriginSettler - function resolveFor(GaslessCrossChainOrder calldata order, bytes calldata originFillerData) - external - view - returns (ResolvedCrossChainOrder memory); - - /// @inheritdoc IOriginSettler - function resolve(OnchainCrossChainOrder calldata order) external view returns (ResolvedCrossChainOrder memory); - /// @notice Returns the type string of the compact including the witness function getCompactWitnessTypeString() external pure returns (string memory); From 966805ed7cd934adcdc2f28501bc8c3ec7e00263 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Mon, 21 Jul 2025 15:20:23 +0200 Subject: [PATCH 33/63] OnChainAllocator contract --- src/allocators/OnChainAllocator.sol | 299 +++++++++++++++++++++++++++ src/interfaces/IOnChainAllocator.sol | 73 +++++++ 2 files changed, 372 insertions(+) create mode 100644 src/allocators/OnChainAllocator.sol create mode 100644 src/interfaces/IOnChainAllocator.sol diff --git a/src/allocators/OnChainAllocator.sol b/src/allocators/OnChainAllocator.sol new file mode 100644 index 0000000..0261869 --- /dev/null +++ b/src/allocators/OnChainAllocator.sol @@ -0,0 +1,299 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import {IOnChainAllocator} from '../interfaces/IOnChainAllocator.sol'; +import {ERC6909} from '@solady/tokens/ERC6909.sol'; +import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; +import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; +import {LOCK_TYPEHASH, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; + +contract OnChainAllocator is IOnChainAllocator { + address public immutable COMPACT_CONTRACT; + + uint96 public immutable ALLOCATOR_ID; + + mapping(bytes32 tokenHash => Allocation[] allocations) internal _allocations; + + uint256 public nonce; + + modifier onlyCompact() { + if (msg.sender != COMPACT_CONTRACT) { + revert InvalidCaller(msg.sender, COMPACT_CONTRACT); + } + _; + } + + constructor(address compactContract_) { + COMPACT_CONTRACT = compactContract_; + ALLOCATOR_ID = ITheCompact(COMPACT_CONTRACT).__registerAllocator(address(this), ''); + } + + function registerAllocation( + uint256[2][] memory idsAndAmounts, + address arbiter, + uint32 expires, + bytes32 typehash, + bytes32 witness + ) public payable returns (bytes32 claimHash, uint256 claimNonce) { + uint256 minResetPeriod; + + bytes32 commitmentsHash = _getCommitmentsHash(idsAndAmounts); + claimHash = keccak256(abi.encode(typehash, arbiter, msg.sender, ++nonce, expires, commitmentsHash, witness)); + + for (uint256 i = 0; i < idsAndAmounts.length; i++) { + // TODO: Discuss which checks to leave in, and which to remove. + // Some of the checks are decreasing the responsibility of the sponsor, others of the filler. + + // Check the allocator id fits this allocator + if (_splitAllocatorId(idsAndAmounts[i][0]) != ALLOCATOR_ID) { + revert InvalidAllocator(_splitAllocatorId(idsAndAmounts[i][0]), ALLOCATOR_ID); + } + + // Check the amount fits in the supported range + if (idsAndAmounts[i][1] > type(uint224).max) { + revert InvalidAmount(idsAndAmounts[i][1]); + } + + // Get the reset period for the token id + uint256 duration = _toSeconds(idsAndAmounts[i][0]); + if (duration < minResetPeriod) { + minResetPeriod = duration; + } + + // Ensure no forcedWithdrawal is active for the token id + (, uint256 forcedWithdrawal) = + ITheCompact(COMPACT_CONTRACT).getForcedWithdrawalStatus(msg.sender, idsAndAmounts[i][0]); + if (forcedWithdrawal != 0 && forcedWithdrawal < expires) { + revert ForceWithdrawalAvailable(expires, forcedWithdrawal); + } + + // Check the balance of the recipient is sufficient + bytes32 tokenHash = _getTokenHash(idsAndAmounts[i][0], msg.sender); + uint256 allocatedBalance = _allocatedBalance(tokenHash); + uint256 balance = ERC6909(COMPACT_CONTRACT).balanceOf(msg.sender, idsAndAmounts[i][0]); + if (allocatedBalance + idsAndAmounts[i][1] > balance) { + revert InsufficientBalance( + msg.sender, idsAndAmounts[i][0], balance, allocatedBalance + idsAndAmounts[i][1] + ); + } + + // Store the allocation + _allocations[tokenHash].push( + Allocation({expires: expires, amount: uint224(idsAndAmounts[i][1]), claimHash: claimHash}) + ); + } + // Ensure expiration is not bigger then the smallest reset period + if (expires > block.timestamp + minResetPeriod) { + revert InvalidExpiration(expires); + } + + emit AllocationRegistered(msg.sender, claimHash, nonce, expires, idsAndAmounts); + + return (claimHash, nonce); + } + + /// @inheritdoc IAllocator + function attest(address, address from_, address, uint256 id_, uint256 amount_) external returns (bytes4) { + // Can be called by anyone, as this will only clean up expired allocations. + uint256 balance = ERC6909(COMPACT_CONTRACT).balanceOf(from_, id_); + + // Check unlocked balance + bytes32 tokenHash = _getTokenHash(id_, from_); + uint256 fullAmount = amount_ + _allocatedBalance(tokenHash); + + if (balance < fullAmount) { + revert InsufficientBalance(from_, id_, balance, fullAmount); + } + + return this.attest.selector; + } + + /// @inheritdoc IAllocator + function authorizeClaim( + bytes32 claimHash, // The message hash representing the claim. + address, /*arbiter*/ // The account tasked with verifying and submitting the claim. + address sponsor, // The account sponsoring the claim. + uint256, /*nonce*/ // A parameter to enforce replay protection, scoped to allocator. + uint256, /*expires*/ // The time at which the claim expires. + uint256[2][] calldata idsAndAmounts, // The allocated token IDs and amounts. + bytes calldata /*allocatorData*/ // Arbitrary data provided by the arbiter. + ) external virtual onlyCompact returns (bytes4) { + for (uint256 i = 0; i < idsAndAmounts.length; i++) { + bytes32 tokenHash = _getTokenHash(idsAndAmounts[i][0], sponsor); + + if (_verifyClaim(tokenHash, claimHash)) { + // Continue even if the claim is already verified to delete the other allocations. + continue; + } + + // claim could not be verified + revert InvalidClaim(claimHash); + } + + return this.authorizeClaim.selector; + } + + /// @inheritdoc IAllocator + function isClaimAuthorized( + bytes32 claimHash, + address, /*arbiter*/ // The account tasked with verifying and submitting the claim. + address sponsor, // The account sponsoring the claim. + uint256, /*nonce*/ // A parameter to enforce replay protection, scoped to allocator. + uint256 expires, // The time at which the claim expires. + uint256[2][] calldata idsAndAmounts, // The allocated token IDs and amounts. + bytes calldata /*allocatorData*/ // Arbitrary data provided by the arbiter. + ) external view virtual returns (bool) { + if (expires < block.timestamp) { + return false; + } + + // We only need to check the first id to confirm or deny the claim. + bytes32 tokenHash = _getTokenHash(idsAndAmounts[0][0], sponsor); + Allocation[] memory allocations = _allocations[tokenHash]; + for (uint256 j = 0; j < allocations.length; j++) { + if (allocations[j].claimHash == claimHash) { + return true; + } + } + + return false; + } + + function _allocatedBalance(bytes32 tokenHash) internal returns (uint256 allocatedBalance) { + // using assembly to only read the allocated balance + expiration slot and skipping the claimHash slot + assembly ("memory-safe") { + // TODO: caching will optimize for a claim that includes the same token multiple times. Is it worth the 200 gas? + allocatedBalance := tload(tokenHash) + if iszero(allocatedBalance) { + // no previous cached balance, calculate the allocated balance + mstore(0x00, tokenHash) + mstore(0x20, _allocations.slot) + // retrieve the array length slot + let arrayLengthSlog := keccak256(0x00, 0x40) + let origLength := sload(arrayLengthSlog) + let length := origLength + // retrieve the arrays content slot + let contentSlot := keccak256(0x00, 0x20) + for { let i := 0 } lt(i, length) {} { + let slot := add(contentSlot, mul(i, 0x40)) // 0x40 to skip the claimHash slot + let content := sload(slot) + let expiration := shr(224, shl(224, content)) + if lt(expiration, timestamp()) { + // allocation expired, remove it + let lastSlot := add(contentSlot, mul(sub(length, 1), 0x40)) + if iszero(eq(slot, lastSlot)) { + // is not the last allocation of the array + let contentLast1 := sload(lastSlot) + let contentLast2 := sload(add(lastSlot, 0x20)) + sstore(slot, contentLast1) + sstore(add(slot, 0x20), contentLast2) + } + // remove the last allocation + length := sub(length, 1) + sstore(lastSlot, 0) + sstore(add(lastSlot, 0x20), 0) + + // repeat the loop at the same index + continue + } + + let amount := shr(32, content) + allocatedBalance := add(allocatedBalance, amount) + + // jump to the next allocation + i := add(i, 1) + } + + if lt(length, origLength) { + // update the array length + sstore(arrayLengthSlog, length) + } + + // Cache the allocated balance in case the token is part of the same claim again. + tstore(tokenHash, allocatedBalance) + } + } + return allocatedBalance; + } + + function _verifyClaim(bytes32 tokenHash, bytes32 claimHash) internal returns (bool verified) { + // using assembly to only read the claimHash slot and skip the expires/amount slot + assembly ("memory-safe") { + mstore(0x00, tokenHash) + mstore(0x20, _allocations.slot) + let lengthSlot := keccak256(0x00, 0x40) + let length := sload(lengthSlot) + mstore(0x00, lengthSlot) + let contentSlot := keccak256(0x00, 0x20) + for { let i := 0 } lt(i, length) { i := add(i, 1) } { + let slot2 := add(contentSlot, add(mul(i, 0x40), 0x20)) // add 0x20 to skip the expires/amount slot + let content2 := sload(slot2) + if eq(content2, claimHash) { + // delete the allocation + let lastSlot := add(contentSlot, mul(sub(length, 1), 0x40)) + if iszero(eq(sub(slot2, 0x20), lastSlot)) { + // is not the last allocation of the array + let contentLast1 := sload(lastSlot) + let contentLast2 := sload(add(lastSlot, 0x20)) + sstore(sub(slot2, 0x20), contentLast1) + sstore(slot2, contentLast2) + } + + sstore(lastSlot, 0) + sstore(add(lastSlot, 0x20), 0) + + // update the array length + sstore(lengthSlot, sub(length, 1)) + + // We return at the first match, no matter if the allocated amounts match. + // If the claim includes the same token multiple times (amounts mismatch), + // we will enter this function again until all entries are deleted. + verified := 1 + break + } + } + } + } + + function _getCommitmentsHash(uint256[2][] memory idsAndAmounts) internal pure returns (bytes32) { + bytes32 commitmentsHash; + for (uint256 i = 0; i < idsAndAmounts.length; i++) { + commitmentsHash = keccak256( + abi.encode( + LOCK_TYPEHASH, + idsAndAmounts[i][0] >> 160 << 160, + idsAndAmounts[i][0] << 96 >> 96, + idsAndAmounts[i][1] + ) + ); + } + return commitmentsHash; + } + + function _getTokenHash(uint256 id_, address sponsor_) internal pure returns (bytes32) { + return keccak256(abi.encode(id_, sponsor_)); + } + + function _splitAllocatorId(uint256 id) internal pure returns (uint96) { + uint96 allocatorId_; + assembly ("memory-safe") { + allocatorId_ := shr(164, shl(4, id)) + } + return allocatorId_; + } + + function _toSeconds(uint256 id) internal pure returns (uint256 duration) { + assembly ("memory-safe") { + let resetPeriod := shr(253, shl(1, id)) + + // Bitpacked durations in 24-bit segments: + // 278d00 094890 015180 000f3c 000258 00003c 00000f 000001 + // 30 days 7 days 1 day 1 hour 10 min 1 min 15 sec 1 sec + let bitpacked := 0x278d00094890015180000f3c00025800003c00000f000001 + + // Shift right by period * 24 bits & mask the least significant 24 bits. + duration := and(shr(mul(resetPeriod, 24), bitpacked), 0xffffff) + } + } +} diff --git a/src/interfaces/IOnChainAllocator.sol b/src/interfaces/IOnChainAllocator.sol new file mode 100644 index 0000000..f5acd58 --- /dev/null +++ b/src/interfaces/IOnChainAllocator.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; +import {Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; + +interface IOnChainAllocator is IAllocator { + struct Allocation { + uint32 expires; + uint224 amount; + bytes32 claimHash; + } + + /// @notice Thrown if a claim is already active + error ClaimActive(address sponsor); + + /// @notice Thrown if the caller is invalid + error InvalidCaller(address caller, address expected); + + /// @notice Thrown if the nonce has already been consumed on the compact contract + error NonceAlreadyInUse(uint256 nonce); + + /// @notice Thrown if the sponsor does not have enough balance to lock the amount + error InsufficientBalance(address sponsor, uint256 id, uint256 balance, uint256 expectedBalance); + + /// @notice Thrown if the provided expiration is not valid + error InvalidExpiration(uint256 expires); + + /// @notice Thrown if the expiration is longer then the tokens forced withdrawal time + error ForceWithdrawalAvailable(uint256 expires, uint256 forcedWithdrawalExpiration); + + /// @notice Thrown if the allocator is not the one expected + error InvalidAllocator(uint96 allocatorId, uint96 expectedAllocatorId); + + /// @notice Thrown if the provided lock is not available or expired + error InvalidClaim(bytes32 claimHash); + + /// @notice Thrown if the current allocation is bigger then uint224 + /// @dev Allocations above uint224 do not support attestations + error ExtensiveAllocationActive(address sponsor, uint256 id); + + /// @notice Thrown if the provided amount is not valid + error InvalidAmount(uint256 amount); + + /// @notice Emitted when a lock is successfully created + /// @param sponsor The address of the sponsor + /// @param claimHash The hash of the claim + /// @param nonce The nonce of the claim + /// @param expires The expiration of the claim + /// @param idsAndAmounts The ids and amounts of the allocations + event AllocationRegistered( + address indexed sponsor, + bytes32 indexed claimHash, + uint256 indexed nonce, + uint256 expires, + uint256[2][] idsAndAmounts + ); + + /// @notice Registers an allocation for a set of tokens + /// @param idsAndAmounts The ids and amounts of the allocations + /// @param arbiter The arbiter of the allocation + /// @param expires The expiration of the allocation + /// @param typehash The typehash of the allocation + /// @param witness The witness of the allocation + function registerAllocation( + uint256[2][] memory idsAndAmounts, + address arbiter, + uint32 expires, + bytes32 typehash, + bytes32 witness + ) external payable returns (bytes32 claimHash, uint256 claimNonce); +} From d01def0316143a34fbeefd5ddb281c18f98019a8 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Mon, 21 Jul 2025 17:33:37 +0200 Subject: [PATCH 34/63] nonces updated and registerAllocationFor added --- src/allocators/OnChainAllocator.sol | 165 ++++++++++++++++++--------- src/interfaces/IOnChainAllocator.sol | 23 +++- 2 files changed, 134 insertions(+), 54 deletions(-) diff --git a/src/allocators/OnChainAllocator.sol b/src/allocators/OnChainAllocator.sol index 0261869..0be84d5 100644 --- a/src/allocators/OnChainAllocator.sol +++ b/src/allocators/OnChainAllocator.sol @@ -10,12 +10,12 @@ import {LOCK_TYPEHASH, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; contract OnChainAllocator is IOnChainAllocator { address public immutable COMPACT_CONTRACT; - + bytes32 public immutable COMPACT_DOMAIN_SEPARATOR; uint96 public immutable ALLOCATOR_ID; mapping(bytes32 tokenHash => Allocation[] allocations) internal _allocations; - uint256 public nonce; + mapping(address user => uint256 nonce) public nonces; modifier onlyCompact() { if (msg.sender != COMPACT_CONTRACT) { @@ -26,71 +26,47 @@ contract OnChainAllocator is IOnChainAllocator { constructor(address compactContract_) { COMPACT_CONTRACT = compactContract_; + COMPACT_DOMAIN_SEPARATOR = ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(); ALLOCATOR_ID = ITheCompact(COMPACT_CONTRACT).__registerAllocator(address(this), ''); } + /// @inheritdoc IOnChainAllocator function registerAllocation( - uint256[2][] memory idsAndAmounts, + uint256[2][] calldata idsAndAmounts, address arbiter, uint32 expires, bytes32 typehash, bytes32 witness - ) public payable returns (bytes32 claimHash, uint256 claimNonce) { - uint256 minResetPeriod; - - bytes32 commitmentsHash = _getCommitmentsHash(idsAndAmounts); - claimHash = keccak256(abi.encode(typehash, arbiter, msg.sender, ++nonce, expires, commitmentsHash, witness)); - - for (uint256 i = 0; i < idsAndAmounts.length; i++) { - // TODO: Discuss which checks to leave in, and which to remove. - // Some of the checks are decreasing the responsibility of the sponsor, others of the filler. + ) public returns (bytes32 claimHash, uint256 claimNonce) { + (claimHash, claimNonce) = _registerAllocation(msg.sender, idsAndAmounts, arbiter, expires, typehash, witness); - // Check the allocator id fits this allocator - if (_splitAllocatorId(idsAndAmounts[i][0]) != ALLOCATOR_ID) { - revert InvalidAllocator(_splitAllocatorId(idsAndAmounts[i][0]), ALLOCATOR_ID); - } + emit AllocationRegistered(msg.sender, claimHash, claimNonce, expires, idsAndAmounts); - // Check the amount fits in the supported range - if (idsAndAmounts[i][1] > type(uint224).max) { - revert InvalidAmount(idsAndAmounts[i][1]); - } - - // Get the reset period for the token id - uint256 duration = _toSeconds(idsAndAmounts[i][0]); - if (duration < minResetPeriod) { - minResetPeriod = duration; - } - - // Ensure no forcedWithdrawal is active for the token id - (, uint256 forcedWithdrawal) = - ITheCompact(COMPACT_CONTRACT).getForcedWithdrawalStatus(msg.sender, idsAndAmounts[i][0]); - if (forcedWithdrawal != 0 && forcedWithdrawal < expires) { - revert ForceWithdrawalAvailable(expires, forcedWithdrawal); - } - - // Check the balance of the recipient is sufficient - bytes32 tokenHash = _getTokenHash(idsAndAmounts[i][0], msg.sender); - uint256 allocatedBalance = _allocatedBalance(tokenHash); - uint256 balance = ERC6909(COMPACT_CONTRACT).balanceOf(msg.sender, idsAndAmounts[i][0]); - if (allocatedBalance + idsAndAmounts[i][1] > balance) { - revert InsufficientBalance( - msg.sender, idsAndAmounts[i][0], balance, allocatedBalance + idsAndAmounts[i][1] - ); - } + return (claimHash, claimNonce); + } - // Store the allocation - _allocations[tokenHash].push( - Allocation({expires: expires, amount: uint224(idsAndAmounts[i][1]), claimHash: claimHash}) - ); - } - // Ensure expiration is not bigger then the smallest reset period - if (expires > block.timestamp + minResetPeriod) { - revert InvalidExpiration(expires); + /// @inheritdoc IOnChainAllocator + function registerAllocationFor( + address sponsor, + uint256[2][] calldata idsAndAmounts, + address arbiter, + uint32 expires, + bytes32 typehash, + bytes32 witness, + bytes calldata signature + ) public returns (bytes32 claimHash, uint256 claimNonce) { + (claimHash, claimNonce) = _registerAllocation(sponsor, idsAndAmounts, arbiter, expires, typehash, witness); + + // Verify the signature + bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), COMPACT_DOMAIN_SEPARATOR, claimHash)); + address signer = _recoverSigner(digest, signature); + if (signer != sponsor) { + revert InvalidSignature(signer, sponsor); } - emit AllocationRegistered(msg.sender, claimHash, nonce, expires, idsAndAmounts); + emit AllocationRegistered(sponsor, claimHash, claimNonce, expires, idsAndAmounts); - return (claimHash, nonce); + return (claimHash, claimNonce); } /// @inheritdoc IAllocator @@ -160,6 +136,69 @@ contract OnChainAllocator is IOnChainAllocator { return false; } + function _registerAllocation( + address sponsor, + uint256[2][] calldata idsAndAmounts, + address arbiter, + uint32 expires, + bytes32 typehash, + bytes32 witness + ) internal returns (bytes32 claimHash, uint256 nonce) { + bytes32 commitmentsHash = _getCommitmentsHash(idsAndAmounts); + nonce = ++nonces[sponsor]; + claimHash = keccak256(abi.encode(typehash, arbiter, sponsor, nonce, expires, commitmentsHash, witness)); + uint256 minResetPeriod; + + for (uint256 i = 0; i < idsAndAmounts.length; i++) { + // TODO: Discuss which checks to leave in, and which to remove. + // Some of the checks are decreasing the responsibility of the sponsor, others of the filler. + + // Check the allocator id fits this allocator + if (_splitAllocatorId(idsAndAmounts[i][0]) != ALLOCATOR_ID) { + revert InvalidAllocator(_splitAllocatorId(idsAndAmounts[i][0]), ALLOCATOR_ID); + } + + // Check the amount fits in the supported range + if (idsAndAmounts[i][1] > type(uint224).max) { + revert InvalidAmount(idsAndAmounts[i][1]); + } + + // Get the reset period for the token id + uint256 duration = _toSeconds(idsAndAmounts[i][0]); + if (duration < minResetPeriod) { + minResetPeriod = duration; + } + + // Ensure no forcedWithdrawal is active for the token id + (, uint256 forcedWithdrawal) = + ITheCompact(COMPACT_CONTRACT).getForcedWithdrawalStatus(sponsor, idsAndAmounts[i][0]); + if (forcedWithdrawal != 0 && forcedWithdrawal < expires) { + revert ForceWithdrawalAvailable(expires, forcedWithdrawal); + } + + // Check the balance of the recipient is sufficient + bytes32 tokenHash = _getTokenHash(idsAndAmounts[i][0], sponsor); + uint256 allocatedBalance = _allocatedBalance(tokenHash); + uint256 balance = ERC6909(COMPACT_CONTRACT).balanceOf(sponsor, idsAndAmounts[i][0]); + if (allocatedBalance + idsAndAmounts[i][1] > balance) { + revert InsufficientBalance( + sponsor, idsAndAmounts[i][0], balance, allocatedBalance + idsAndAmounts[i][1] + ); + } + + // Store the allocation + _allocations[tokenHash].push( + Allocation({expires: expires, amount: uint224(idsAndAmounts[i][1]), claimHash: claimHash}) + ); + } + // Ensure expiration is not bigger then the smallest reset period + if (expires > block.timestamp + minResetPeriod) { + revert InvalidExpiration(expires); + } + + return (claimHash, nonce); + } + function _allocatedBalance(bytes32 tokenHash) internal returns (uint256 allocatedBalance) { // using assembly to only read the allocated balance + expiration slot and skipping the claimHash slot assembly ("memory-safe") { @@ -256,6 +295,26 @@ contract OnChainAllocator is IOnChainAllocator { } } + function _recoverSigner(bytes32 digest, bytes calldata signature) internal pure returns (address) { + bytes32 r; + bytes32 s; + uint8 v; + + if (signature.length == 65) { + (r, s) = abi.decode(signature, (bytes32, bytes32)); + v = uint8(signature[64]); + } else if (signature.length == 64) { + bytes32 vs; + (r, vs) = abi.decode(signature, (bytes32, bytes32)); + v = uint8(uint256(vs >> 255) + 27); + s = vs << 1 >> 1; + } else { + return address(0); + } + + return ecrecover(digest, v, r, s); + } + function _getCommitmentsHash(uint256[2][] memory idsAndAmounts) internal pure returns (bytes32) { bytes32 commitmentsHash; for (uint256 i = 0; i < idsAndAmounts.length; i++) { diff --git a/src/interfaces/IOnChainAllocator.sol b/src/interfaces/IOnChainAllocator.sol index f5acd58..7b72aa7 100644 --- a/src/interfaces/IOnChainAllocator.sol +++ b/src/interfaces/IOnChainAllocator.sol @@ -43,6 +43,9 @@ interface IOnChainAllocator is IAllocator { /// @notice Thrown if the provided amount is not valid error InvalidAmount(uint256 amount); + /// @notice Thrown if the signature is invalid + error InvalidSignature(address signer, address expectedSigner); + /// @notice Emitted when a lock is successfully created /// @param sponsor The address of the sponsor /// @param claimHash The hash of the claim @@ -69,5 +72,23 @@ interface IOnChainAllocator is IAllocator { uint32 expires, bytes32 typehash, bytes32 witness - ) external payable returns (bytes32 claimHash, uint256 claimNonce); + ) external returns (bytes32 claimHash, uint256 claimNonce); + + /// @notice Registers an allocation for a set of tokens on behalf of a sponsor + /// @param sponsor The address of the sponsor + /// @param idsAndAmounts The ids and amounts of the allocations + /// @param arbiter The arbiter of the allocation + /// @param expires The expiration of the allocation + /// @param typehash The typehash of the allocation + /// @param witness The witness of the allocation + /// @param signature The signature of the allocation + function registerAllocationFor( + address sponsor, + uint256[2][] memory idsAndAmounts, + address arbiter, + uint32 expires, + bytes32 typehash, + bytes32 witness, + bytes calldata signature + ) external returns (bytes32 claimHash, uint256 claimNonce); } From 591b243218c0802517a1618c8f52b532b45c7fcc Mon Sep 17 00:00:00 2001 From: mgretzke Date: Mon, 21 Jul 2025 17:40:52 +0200 Subject: [PATCH 35/63] Added support for relaying via on chain registration --- src/allocators/OnChainAllocator.sol | 20 ++++++++++++++------ src/interfaces/IOnChainAllocator.sol | 5 ++++- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/allocators/OnChainAllocator.sol b/src/allocators/OnChainAllocator.sol index 0be84d5..efcc5f4 100644 --- a/src/allocators/OnChainAllocator.sol +++ b/src/allocators/OnChainAllocator.sol @@ -57,13 +57,21 @@ contract OnChainAllocator is IOnChainAllocator { ) public returns (bytes32 claimHash, uint256 claimNonce) { (claimHash, claimNonce) = _registerAllocation(sponsor, idsAndAmounts, arbiter, expires, typehash, witness); - // Verify the signature - bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), COMPACT_DOMAIN_SEPARATOR, claimHash)); - address signer = _recoverSigner(digest, signature); - if (signer != sponsor) { - revert InvalidSignature(signer, sponsor); + // We check for the length, which means this could also be triggered by a zero length signature provided in the openFor function. + // This enables relaying of orders if the claim was registered on the compact. + if (signature.length > 0) { + // confirm the provided signature is valid + bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), COMPACT_DOMAIN_SEPARATOR, claimHash)); + address signer_ = _recoverSigner(digest, signature); + if (sponsor != signer_) { + revert InvalidSignature(sponsor, signer_); + } + } else { + // confirm the claim hash is registered on the compact + if (!ITheCompact(COMPACT_CONTRACT).isRegistered(sponsor, claimHash, typehash)) { + revert InvalidRegistration(sponsor, claimHash); + } } - emit AllocationRegistered(sponsor, claimHash, claimNonce, expires, idsAndAmounts); return (claimHash, claimNonce); diff --git a/src/interfaces/IOnChainAllocator.sol b/src/interfaces/IOnChainAllocator.sol index 7b72aa7..f040b46 100644 --- a/src/interfaces/IOnChainAllocator.sol +++ b/src/interfaces/IOnChainAllocator.sol @@ -43,9 +43,12 @@ interface IOnChainAllocator is IAllocator { /// @notice Thrown if the provided amount is not valid error InvalidAmount(uint256 amount); - /// @notice Thrown if the signature is invalid + /// @notice Thrown if the provided signature is invalid error InvalidSignature(address signer, address expectedSigner); + /// @notice Thrown if the claim hash is not registered on the compact + error InvalidRegistration(address sponsor, bytes32 claimHash); + /// @notice Emitted when a lock is successfully created /// @param sponsor The address of the sponsor /// @param claimHash The hash of the claim From 1359945a534c0ee2fd4700565213ee428cfb8e8d Mon Sep 17 00:00:00 2001 From: mgretzke Date: Wed, 23 Jul 2025 13:22:32 +0200 Subject: [PATCH 36/63] Use commitments instead of idsAndAmounts --- src/allocators/OnChainAllocator.sol | 152 ++++++++++++++++----------- src/interfaces/IOnChainAllocator.sol | 18 ++-- 2 files changed, 95 insertions(+), 75 deletions(-) diff --git a/src/allocators/OnChainAllocator.sol b/src/allocators/OnChainAllocator.sol index efcc5f4..7706274 100644 --- a/src/allocators/OnChainAllocator.sol +++ b/src/allocators/OnChainAllocator.sol @@ -32,15 +32,15 @@ contract OnChainAllocator is IOnChainAllocator { /// @inheritdoc IOnChainAllocator function registerAllocation( - uint256[2][] calldata idsAndAmounts, + Lock[] calldata commitments, address arbiter, uint32 expires, bytes32 typehash, bytes32 witness ) public returns (bytes32 claimHash, uint256 claimNonce) { - (claimHash, claimNonce) = _registerAllocation(msg.sender, idsAndAmounts, arbiter, expires, typehash, witness); + (claimHash, claimNonce) = _registerAllocation(msg.sender, commitments, arbiter, expires, typehash, witness); - emit AllocationRegistered(msg.sender, claimHash, claimNonce, expires, idsAndAmounts); + emit AllocationRegistered(msg.sender, claimHash, claimNonce, expires, commitments); return (claimHash, claimNonce); } @@ -48,14 +48,14 @@ contract OnChainAllocator is IOnChainAllocator { /// @inheritdoc IOnChainAllocator function registerAllocationFor( address sponsor, - uint256[2][] calldata idsAndAmounts, + Lock[] calldata commitments, address arbiter, uint32 expires, bytes32 typehash, bytes32 witness, bytes calldata signature ) public returns (bytes32 claimHash, uint256 claimNonce) { - (claimHash, claimNonce) = _registerAllocation(sponsor, idsAndAmounts, arbiter, expires, typehash, witness); + (claimHash, claimNonce) = _registerAllocation(sponsor, commitments, arbiter, expires, typehash, witness); // We check for the length, which means this could also be triggered by a zero length signature provided in the openFor function. // This enables relaying of orders if the claim was registered on the compact. @@ -72,7 +72,7 @@ contract OnChainAllocator is IOnChainAllocator { revert InvalidRegistration(sponsor, claimHash); } } - emit AllocationRegistered(sponsor, claimHash, claimNonce, expires, idsAndAmounts); + emit AllocationRegistered(sponsor, claimHash, claimNonce, expires, commitments); return (claimHash, claimNonce); } @@ -102,7 +102,7 @@ contract OnChainAllocator is IOnChainAllocator { uint256, /*expires*/ // The time at which the claim expires. uint256[2][] calldata idsAndAmounts, // The allocated token IDs and amounts. bytes calldata /*allocatorData*/ // Arbitrary data provided by the arbiter. - ) external virtual onlyCompact returns (bytes4) { + ) public virtual onlyCompact returns (bytes4) { for (uint256 i = 0; i < idsAndAmounts.length; i++) { bytes32 tokenHash = _getTokenHash(idsAndAmounts[i][0], sponsor); @@ -127,7 +127,7 @@ contract OnChainAllocator is IOnChainAllocator { uint256 expires, // The time at which the claim expires. uint256[2][] calldata idsAndAmounts, // The allocated token IDs and amounts. bytes calldata /*allocatorData*/ // Arbitrary data provided by the arbiter. - ) external view virtual returns (bool) { + ) public view virtual returns (bool) { if (expires < block.timestamp) { return false; } @@ -146,58 +146,25 @@ contract OnChainAllocator is IOnChainAllocator { function _registerAllocation( address sponsor, - uint256[2][] calldata idsAndAmounts, + Lock[] calldata commitments, address arbiter, uint32 expires, bytes32 typehash, bytes32 witness ) internal returns (bytes32 claimHash, uint256 nonce) { - bytes32 commitmentsHash = _getCommitmentsHash(idsAndAmounts); + bytes32 commitmentsHash = _getCommitmentsHash(commitments); nonce = ++nonces[sponsor]; claimHash = keccak256(abi.encode(typehash, arbiter, sponsor, nonce, expires, commitmentsHash, witness)); - uint256 minResetPeriod; - - for (uint256 i = 0; i < idsAndAmounts.length; i++) { - // TODO: Discuss which checks to leave in, and which to remove. - // Some of the checks are decreasing the responsibility of the sponsor, others of the filler. - - // Check the allocator id fits this allocator - if (_splitAllocatorId(idsAndAmounts[i][0]) != ALLOCATOR_ID) { - revert InvalidAllocator(_splitAllocatorId(idsAndAmounts[i][0]), ALLOCATOR_ID); - } - // Check the amount fits in the supported range - if (idsAndAmounts[i][1] > type(uint224).max) { - revert InvalidAmount(idsAndAmounts[i][1]); - } - - // Get the reset period for the token id - uint256 duration = _toSeconds(idsAndAmounts[i][0]); - if (duration < minResetPeriod) { - minResetPeriod = duration; - } - - // Ensure no forcedWithdrawal is active for the token id - (, uint256 forcedWithdrawal) = - ITheCompact(COMPACT_CONTRACT).getForcedWithdrawalStatus(sponsor, idsAndAmounts[i][0]); - if (forcedWithdrawal != 0 && forcedWithdrawal < expires) { - revert ForceWithdrawalAvailable(expires, forcedWithdrawal); - } - - // Check the balance of the recipient is sufficient - bytes32 tokenHash = _getTokenHash(idsAndAmounts[i][0], sponsor); - uint256 allocatedBalance = _allocatedBalance(tokenHash); - uint256 balance = ERC6909(COMPACT_CONTRACT).balanceOf(sponsor, idsAndAmounts[i][0]); - if (allocatedBalance + idsAndAmounts[i][1] > balance) { - revert InsufficientBalance( - sponsor, idsAndAmounts[i][0], balance, allocatedBalance + idsAndAmounts[i][1] - ); - } + uint256 minResetPeriod; + for (uint256 i = 0; i < commitments.length; i++) { + minResetPeriod = _checkInput(commitments[i], sponsor, expires, minResetPeriod); + bytes32 tokenHash = _checkBalance(sponsor, commitments[i]); // Store the allocation - _allocations[tokenHash].push( - Allocation({expires: expires, amount: uint224(idsAndAmounts[i][1]), claimHash: claimHash}) - ); + uint224 amount = uint224(commitments[i].amount); + Allocation memory allocation = Allocation({expires: expires, amount: amount, claimHash: claimHash}); + _allocations[tokenHash].push(allocation); } // Ensure expiration is not bigger then the smallest reset period if (expires > block.timestamp + minResetPeriod) { @@ -207,6 +174,51 @@ contract OnChainAllocator is IOnChainAllocator { return (claimHash, nonce); } + function _checkInput(Lock calldata commitment, address sponsor, uint32 expires, uint256 minResetPeriod) + internal + view + returns (uint256) + { + // TODO: Discuss which checks to leave in, and which to remove. + // Some of the checks are decreasing the responsibility of the sponsor, others of the filler. + + // Check the allocator id fits this allocator + if (_splitAllocatorId(commitment.lockTag) != ALLOCATOR_ID) { + revert InvalidAllocator(_splitAllocatorId(commitment.lockTag), ALLOCATOR_ID); + } + + // Check the amount fits in the supported range + if (commitment.amount > type(uint224).max) { + revert InvalidAmount(commitment.amount); + } + + // Get the reset period for the token id + uint256 duration = _toSeconds(commitment.lockTag); + if (duration < minResetPeriod) { + minResetPeriod = duration; + } + + // Ensure no forcedWithdrawal is active for the token id + (, uint256 forcedWithdrawal) = ITheCompact(COMPACT_CONTRACT).getForcedWithdrawalStatus( + sponsor, _toId(commitment.lockTag, commitment.token) + ); + if (forcedWithdrawal != 0 && forcedWithdrawal < expires) { + revert ForceWithdrawalAvailable(expires, forcedWithdrawal); + } + + return minResetPeriod; + } + + function _checkBalance(address sponsor, Lock calldata commitment) internal returns (bytes32 tokenHash) { + // Check the balance of the recipient is sufficient + tokenHash = _getTokenHash(commitment, sponsor); + uint256 balance = ERC6909(COMPACT_CONTRACT).balanceOf(sponsor, _toId(commitment.lockTag, commitment.token)); + uint256 requiredBalance = _allocatedBalance(tokenHash) + commitment.amount; + if (requiredBalance > balance) { + revert InsufficientBalance(sponsor, _toId(commitment.lockTag, commitment.token), balance, requiredBalance); + } + } + function _allocatedBalance(bytes32 tokenHash) internal returns (uint256 allocatedBalance) { // using assembly to only read the allocated balance + expiration slot and skipping the claimHash slot assembly ("memory-safe") { @@ -323,36 +335,48 @@ contract OnChainAllocator is IOnChainAllocator { return ecrecover(digest, v, r, s); } - function _getCommitmentsHash(uint256[2][] memory idsAndAmounts) internal pure returns (bytes32) { + function _getCommitmentsHash(Lock[] memory commitments) internal pure returns (bytes32) { bytes32 commitmentsHash; - for (uint256 i = 0; i < idsAndAmounts.length; i++) { + for (uint256 i = 0; i < commitments.length; i++) { commitmentsHash = keccak256( - abi.encode( - LOCK_TYPEHASH, - idsAndAmounts[i][0] >> 160 << 160, - idsAndAmounts[i][0] << 96 >> 96, - idsAndAmounts[i][1] - ) + abi.encode(LOCK_TYPEHASH, commitments[i].lockTag, commitments[i].token, commitments[i].amount) ); } return commitmentsHash; } - function _getTokenHash(uint256 id_, address sponsor_) internal pure returns (bytes32) { - return keccak256(abi.encode(id_, sponsor_)); + function _getTokenHash(Lock calldata commitment, address sponsor) internal pure returns (bytes32 tokenHash) { + assembly ("memory-safe") { + mstore(0x00, calldataload(commitment)) + mstore(0x0c, shl(96, calldataload(add(commitment, 0x20)))) + mstore(0x20, sponsor) + tokenHash := keccak256(0x00, 0x40) + } + return tokenHash; + } + + function _getTokenHash(uint256 id, address sponsor) internal pure returns (bytes32 tokenHash) { + return keccak256(abi.encode(id, sponsor)); } - function _splitAllocatorId(uint256 id) internal pure returns (uint96) { + function _splitAllocatorId(bytes12 lockTag) internal pure returns (uint96) { uint96 allocatorId_; assembly ("memory-safe") { - allocatorId_ := shr(164, shl(4, id)) + allocatorId_ := shr(164, shl(4, lockTag)) } return allocatorId_; } - function _toSeconds(uint256 id) internal pure returns (uint256 duration) { + function _toId(bytes12 lockTag, address token) internal pure returns (uint256 id) { + assembly ("memory-safe") { + id := or(lockTag, token) + } + return id; + } + + function _toSeconds(bytes12 lockTag) internal pure returns (uint256 duration) { assembly ("memory-safe") { - let resetPeriod := shr(253, shl(1, id)) + let resetPeriod := shr(253, shl(1, lockTag)) // Bitpacked durations in 24-bit segments: // 278d00 094890 015180 000f3c 000258 00003c 00000f 000001 diff --git a/src/interfaces/IOnChainAllocator.sol b/src/interfaces/IOnChainAllocator.sol index f040b46..4336830 100644 --- a/src/interfaces/IOnChainAllocator.sol +++ b/src/interfaces/IOnChainAllocator.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.27; import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; -import {Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; +import {Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; interface IOnChainAllocator is IAllocator { struct Allocation { @@ -54,23 +54,19 @@ interface IOnChainAllocator is IAllocator { /// @param claimHash The hash of the claim /// @param nonce The nonce of the claim /// @param expires The expiration of the claim - /// @param idsAndAmounts The ids and amounts of the allocations + /// @param commitments The commitments of the allocations event AllocationRegistered( - address indexed sponsor, - bytes32 indexed claimHash, - uint256 indexed nonce, - uint256 expires, - uint256[2][] idsAndAmounts + address indexed sponsor, bytes32 indexed claimHash, uint256 indexed nonce, uint256 expires, Lock[] commitments ); /// @notice Registers an allocation for a set of tokens - /// @param idsAndAmounts The ids and amounts of the allocations + /// @param commitments The commitments of the allocations /// @param arbiter The arbiter of the allocation /// @param expires The expiration of the allocation /// @param typehash The typehash of the allocation /// @param witness The witness of the allocation function registerAllocation( - uint256[2][] memory idsAndAmounts, + Lock[] memory commitments, address arbiter, uint32 expires, bytes32 typehash, @@ -79,7 +75,7 @@ interface IOnChainAllocator is IAllocator { /// @notice Registers an allocation for a set of tokens on behalf of a sponsor /// @param sponsor The address of the sponsor - /// @param idsAndAmounts The ids and amounts of the allocations + /// @param commitments The commitments of the allocations /// @param arbiter The arbiter of the allocation /// @param expires The expiration of the allocation /// @param typehash The typehash of the allocation @@ -87,7 +83,7 @@ interface IOnChainAllocator is IAllocator { /// @param signature The signature of the allocation function registerAllocationFor( address sponsor, - uint256[2][] memory idsAndAmounts, + Lock[] memory commitments, address arbiter, uint32 expires, bytes32 typehash, From f64074b18cacad0315406d4deb28bc2d8de3ec5d Mon Sep 17 00:00:00 2001 From: mgretzke Date: Wed, 23 Jul 2025 13:22:58 +0200 Subject: [PATCH 37/63] ERC7683Allocated using OnChainAllocator --- src/allocators/ERC7683Allocator.sol | 563 +++++++++++---------------- src/interfaces/IERC7683Allocator.sol | 44 +-- 2 files changed, 236 insertions(+), 371 deletions(-) diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index df231af..bcd2e81 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -4,51 +4,44 @@ pragma solidity ^0.8.27; import {IOriginSettler} from '../interfaces/ERC7683/IOriginSettler.sol'; import {IERC7683Allocator} from '../interfaces/IERC7683Allocator.sol'; -import {SimpleAllocator} from './SimpleAllocator.sol'; -import {Claim, Mandate} from './types/TribunalStructs.sol'; +import {OnChainAllocator} from './OnChainAllocator.sol'; +import {BatchClaim, Mandate} from './types/TribunalStructs.sol'; import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; import {LibBytes} from '@solady/utils/LibBytes.sol'; import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; -import {Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; +import {BatchCompact, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; -contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { - /// @notice The typehash of the OrderData struct - // keccak256("OrderData(address arbiter,address sponsor,uint256 nonce,uint256 expires,bytes12 lockTag,address inputToken,uint256 amount, - // uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt,uint256 targetBlock,uint256 maximumBlocksAfterTarget)") - bytes32 public constant ORDERDATA_TYPEHASH = 0x2ec2bf7ae42e14efd81070a06d7410420dad2cf15d1d09c8a7d77d82f9e5eae5; +contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { + /// @notice The typehash of the OrderDataOnChain struct + // keccak256("OrderDataOnChain(address arbiter,uint256 expires,Order order,uint200 targetBlock,uint56 maximumBlocksAfterTarget) + // Lock(bytes12 lockTag,address token,uint256 amount) + // Order(Lock[] commitments,uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") + bytes32 public constant ORDERDATA_ONCHAIN_TYPEHASH = + 0x95d7f00c299b34a562258ba851472a8d9bd0d8a1b88fce3a37b7d27ca06e77c4; /// @notice The typehash of the OrderDataGasless struct - // keccak256("OrderDataGasless(address arbiter,bytes12 lockTag,address inputToken,uint256 amount, - // uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") + // keccak256("OrderDataGasless(address arbiter,Order order, + // Lock(bytes12 lockTag,address token,uint256 amount) + // Order(Lock[] commitments,uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") bytes32 public constant ORDERDATA_GASLESS_TYPEHASH = - 0x29d853cc0f7a1e24319ad92f2404fd0ff5806cd6ac6f6325dfaa7c547074e912; + 0xfddec069b5987011094d234ae1829188ce34de94b23d06395ba4e6e4749a544e; - /// @notice keccak256("QualifiedClaim(bytes32 claimHash,uint256 targetBlock,uint256 maximumBlocksAfterTarget)") - bytes32 public constant QUALIFICATION_TYPEHASH = 0x59866b84bd1f6c909cf2a31efd20c59e6c902e50f2c196994e5aa85cdc7d7ce0; - - /// @notice keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,bytes12 lockTag,address token,uint256 amount,Mandate mandate) + /// @notice keccak256("BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments,Mandate mandate) + // Lock(bytes12 lockTag,address token,uint256 amount) // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") bytes32 public constant COMPACT_WITNESS_TYPEHASH = - 0x2ec0d30491bb66a6eb554b9d53f490d79b54fc5f4963bed4b2bb8096b4790f1f; + 0x5ede122c736b60a8b718f83dcfb5d6e4aa27c9714d0c7bc9ca86562b8f878463; /// @notice keccak256("Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") - bytes32 internal constant MANDATE_TYPEHASH = 0x74d9c10530859952346f3e046aa2981a24bb7524b8394eb45a9deddced9d6501; - - uint8 internal constant ORDERDATA_LOCKTAG_OFFSET = 0x80; - - uint8 internal constant ORDERDATA_GASLESS_LOCKTAG_OFFSET = 0x20; - - uint16 internal constant ORDERDATA_MANDATE_OFFSET = 0x1a4; // orderData.offset = 0xc4 + Mandate.chainId offset = 0xe0; - - uint16 internal constant ORDERDATA_GASLESS_MANDATE_OFFSET = 0x224; // orderData.offset = 0x1a4 + Mandate.chainId offset = 0x80; + bytes32 private constant MANDATE_TYPEHASH = 0x74d9c10530859952346f3e046aa2981a24bb7524b8394eb45a9deddced9d6501; bytes32 immutable _COMPACT_DOMAIN_SEPARATOR; - constructor(address compactContract_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_) - SimpleAllocator(compactContract_, minWithdrawalDelay_, maxWithdrawalDelay_) - { + mapping(bytes32 claimHash => bytes32 qualification) public qualifications; + + constructor(address compactContract_) OnChainAllocator(compactContract_) { _COMPACT_DOMAIN_SEPARATOR = ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(); } @@ -56,67 +49,88 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { function openFor(GaslessCrossChainOrder calldata order_, bytes calldata sponsorSignature_, bytes calldata) external { - // With the users signature, we can create locks in the name of the user - // Check if orderDataType is the one expected by the allocator if (order_.orderDataType != ORDERDATA_GASLESS_TYPEHASH) { revert InvalidOrderDataType(order_.orderDataType, ORDERDATA_GASLESS_TYPEHASH); } + /// TODO: Potentially useless check, since the allocator Id gets checked later. if (order_.originSettler != address(this)) { revert InvalidOriginSettler(order_.originSettler, address(this)); } + // Early revert if the expected nonce is not the next nonce + if (order_.nonce != nonces[order_.user] + 1) { + revert InvalidNonce(order_.nonce, nonces[order_.user] + 1); + } // Decode the orderData - bytes calldata orderDataGaslessBytes = LibBytes.dynamicStructInCalldata(order_.orderData, 0x00); + (address arbiter,, Order calldata orderData,,) = _decodeOrderData(order_.orderData, 0x20); - // Extract the resolved order early to reduce stack pressure ResolvedCrossChainOrder memory resolvedOrder = _resolveOrder( + arbiter, order_.user, order_.nonce, order_.openDeadline, order_.fillDeadline, - orderDataGaslessBytes, - ORDERDATA_GASLESS_LOCKTAG_OFFSET, - sponsorSignature_ + orderData, + sponsorSignature_, + 0, + 0 ); - // Extract mandateHash early to reduce stack pressure - bytes32 mandateHash = _mandateHash(orderDataGaslessBytes, ORDERDATA_GASLESS_MANDATE_OFFSET, order_.fillDeadline); + bytes32 mandateHash = _mandateHash(orderData, order_.fillDeadline); - _open(orderDataGaslessBytes, ORDERDATA_GASLESS_LOCKTAG_OFFSET, sponsorSignature_, mandateHash, resolvedOrder); + _open( + arbiter, + order_.user, + order_.openDeadline, + orderData, + sponsorSignature_, + mandateHash, + bytes32(0), + resolvedOrder + ); } /// @inheritdoc IOriginSettler function open(OnchainCrossChainOrder calldata order) external { // Check if orderDataType is the one expected by the allocator - if (order.orderDataType != ORDERDATA_TYPEHASH) { - revert InvalidOrderDataType(order.orderDataType, ORDERDATA_TYPEHASH); + if (order.orderDataType != ORDERDATA_ONCHAIN_TYPEHASH) { + revert InvalidOrderDataType(order.orderDataType, ORDERDATA_ONCHAIN_TYPEHASH); } // Decode the orderData - bytes calldata orderDataBytes = LibBytes.dynamicStructInCalldata(order.orderData, 0x00); - OrderData calldata orderData; - assembly ("memory-safe") { - orderData := orderDataBytes.offset - } - - _checkMsgSender(orderData.sponsor); + ( + address arbiter, + uint32 expires, + Order calldata orderData, + uint200 targetBlock, + uint56 maximumBlocksAfterTarget + ) = _decodeOrderData(order.orderData, 0x40); - // Extract mandateHash early to reduce stack pressure - bytes32 mandateHash = _mandateHash(orderDataBytes, ORDERDATA_MANDATE_OFFSET, order.fillDeadline); + bytes32 mandateHash = _mandateHash(orderData, order.fillDeadline); - // Extract the resolved order early to reduce stack pressure ResolvedCrossChainOrder memory resolvedOrder = _resolveOrder( - orderData.sponsor, - orderData.nonce, - uint32(orderData.expires), + arbiter, + msg.sender, + nonces[msg.sender] + 1, + expires, order.fillDeadline, - orderDataBytes, - ORDERDATA_LOCKTAG_OFFSET, - LibBytes.emptyCalldata() + orderData, + LibBytes.emptyCalldata(), + targetBlock, + maximumBlocksAfterTarget ); - _open(orderDataBytes, ORDERDATA_LOCKTAG_OFFSET, LibBytes.emptyCalldata(), mandateHash, resolvedOrder); + _open( + arbiter, + msg.sender, + expires, + orderData, + LibBytes.emptyCalldata(), + mandateHash, + bytes32(abi.encodePacked(targetBlock, maximumBlocksAfterTarget)), + resolvedOrder + ); } /// @inheritdoc IOriginSettler @@ -125,67 +139,79 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { view returns (ResolvedCrossChainOrder memory) { - bytes calldata orderDataGaslessBytes = LibBytes.dynamicStructInCalldata(order_.orderData, 0x00); + // Check if orderDataType is the one expected by the allocator + if (order_.orderDataType != ORDERDATA_GASLESS_TYPEHASH) { + revert InvalidOrderDataType(order_.orderDataType, ORDERDATA_GASLESS_TYPEHASH); + } + /// TODO: Potentially useless check, since the allocator Id gets checked later. + if (order_.originSettler != address(this)) { + revert InvalidOriginSettler(order_.originSettler, address(this)); + } + // Early revert if the expected nonce is not the next nonce + if (order_.nonce != nonces[order_.user] + 1) { + revert InvalidNonce(order_.nonce, nonces[order_.user] + 1); + } + + // Decode the orderData + (address arbiter,, Order calldata orderData,,) = _decodeOrderData(order_.orderData, 0x20); return _resolveOrder( + arbiter, order_.user, order_.nonce, order_.openDeadline, order_.fillDeadline, - orderDataGaslessBytes, - ORDERDATA_GASLESS_LOCKTAG_OFFSET, - LibBytes.emptyCalldata() + orderData, + LibBytes.emptyCalldata(), + 0, + 0 ); } /// @inheritdoc IOriginSettler - function resolve(OnchainCrossChainOrder calldata order_) external view returns (ResolvedCrossChainOrder memory) { - bytes calldata orderDataBytes = LibBytes.dynamicStructInCalldata(order_.orderData, 0x00); - OrderData calldata orderData; - assembly ("memory-safe") { - orderData := orderDataBytes.offset + function resolve(OnchainCrossChainOrder calldata order) external view returns (ResolvedCrossChainOrder memory) { + // Check if orderDataType is the one expected by the allocator + if (order.orderDataType != ORDERDATA_ONCHAIN_TYPEHASH) { + revert InvalidOrderDataType(order.orderDataType, ORDERDATA_ONCHAIN_TYPEHASH); } - _nonceValidation(orderData.sponsor, orderData.nonce); + + // Decode the orderData + ( + address arbiter, + uint32 expires, + Order calldata orderData, + uint200 targetBlock, + uint56 maximumBlocksAfterTarget + ) = _decodeOrderData(order.orderData, 0x40); + return _resolveOrder( - orderData.sponsor, - orderData.nonce, - orderData.expires, - order_.fillDeadline, - orderDataBytes, - ORDERDATA_LOCKTAG_OFFSET, - LibBytes.emptyCalldata() + arbiter, + msg.sender, + nonces[msg.sender] + 1, + expires, + order.fillDeadline, + orderData, + LibBytes.emptyCalldata(), + targetBlock, + maximumBlocksAfterTarget ); } /// @inheritdoc IAllocator function authorizeClaim( - bytes32 claimHash, // The message hash representing the claim. - address, /* arbiter */ // The account tasked with verifying and submitting the claim. - address sponsor, // The account to source the tokens from. - uint256, /* nonce */ // A parameter to enforce replay protection, scoped to allocator. - uint256, /* expires */ // The time at which the claim expires. + bytes32 claimHash, + address, /*arbiter*/ // The account tasked with verifying and submitting the claim. + address sponsor, // The account sponsoring the claim. + uint256, /*nonce*/ // A parameter to enforce replay protection, scoped to allocator. + uint256 expires, // The time at which the claim expires. uint256[2][] calldata idsAndAmounts, // The allocated token IDs and amounts. bytes calldata allocatorData // Arbitrary data provided by the arbiter. - ) external override(SimpleAllocator, IAllocator) onlyCompact returns (bytes4) { - uint256 length = idsAndAmounts.length; - if (length > 1) { - revert BatchCompactsNotSupported(); - } - (uint256 targetBlock, uint256 maximumBlocksAfterTarget) = abi.decode(allocatorData, (uint256, uint256)); - claimHash = keccak256(abi.encode(QUALIFICATION_TYPEHASH, claimHash, targetBlock, maximumBlocksAfterTarget)); + ) public override(OnChainAllocator, IAllocator) onlyCompact returns (bytes4) { + super.authorizeClaim(claimHash, address(0), sponsor, 0, expires, idsAndAmounts, allocatorData); - if (!_claim[claimHash]) { - revert InvalidLock(claimHash, 0); + if (qualifications[claimHash] != bytes32(allocatorData)) { + revert InvalidAllocatorData(bytes32(allocatorData), qualifications[claimHash]); } - delete _claim[claimHash]; - - // Delete all allocations connected to the claim - for (uint256 i = 0; i < length; ++i) { - bytes32 tokenHash = _getTokenHash(idsAndAmounts[i][0], sponsor); - delete _allocation[tokenHash]; - } - - // We expect the Compact to verify the expiration date is still valid and the nonce has not yet been consumed return this.authorizeClaim.selector; } @@ -194,38 +220,31 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { function isClaimAuthorized( bytes32 claimHash, address, /*arbiter*/ // The account tasked with verifying and submitting the claim. - address, /*sponsor*/ // The account to source the tokens from. + address sponsor, // The account to source the tokens from. uint256, /*nonce*/ // A parameter to enforce replay protection, scoped to allocator. uint256 expires, // The time at which the claim expires. uint256[2][] calldata idsAndAmounts, // The allocated token IDs and amounts. bytes calldata allocatorData // Arbitrary data provided by the arbiter. - ) external view override(SimpleAllocator, IAllocator) returns (bool) { - uint256 length = idsAndAmounts.length; - if (length > 1) { - revert BatchCompactsNotSupported(); + ) public view override(OnChainAllocator, IAllocator) returns (bool) { + if ( + !super.isClaimAuthorized(claimHash, address(0), sponsor, 0, expires, idsAndAmounts, LibBytes.emptyCalldata()) + ) { + return false; } - (uint256 targetBlock, uint256 maximumBlocksAfterTarget) = abi.decode(allocatorData, (uint256, uint256)); - claimHash = keccak256(abi.encode(QUALIFICATION_TYPEHASH, claimHash, targetBlock, maximumBlocksAfterTarget)); - return _claim[claimHash] && expires > block.timestamp; + return qualifications[claimHash] == bytes32(allocatorData); } /// @inheritdoc IERC7683Allocator function getCompactWitnessTypeString() external pure returns (string memory) { return - 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,bytes12 lockTag,address token,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; + 'BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments,Mandate mandate)Lock(bytes12 lockTag,address token,uint256 amount)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; } /// @inheritdoc IERC7683Allocator - function checkNonce(uint256 nonce_, address sponsor_) external view returns (bool nonceFree_) { - _nonceValidation(sponsor_, nonce_); - uint256 word = nonce_ / 256; - uint256 bit = nonce_ % 256; - assembly ("memory-safe") { - let nonceBitmap := sload(or(NONCE_MASTER_SLOT_SEED, word)) - nonceFree_ := iszero(and(nonceBitmap, shl(bit, 1))) - } - return nonceFree_; + function checkNonce(uint256 nonce_, address sponsor_) external view returns (bool nonceValid) { + nonceValid = nonces[sponsor_] + 1 == nonce_; + return nonceValid; } /// @inheritdoc IERC7683Allocator @@ -235,123 +254,59 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { } function _open( - bytes calldata orderData_, - uint256 lockTagOffset, + address arbiter, + address sponsor, + uint32 expires, + Order calldata orderData, bytes calldata sponsorSignature_, bytes32 mandateHash_, + bytes32 qualification_, ResolvedCrossChainOrder memory resolvedOrder_ ) internal { - uint256 nonce_ = uint256(resolvedOrder_.orderId); - // Enforce a nonce where the most significant 96 bits are the nonce and the least significant 160 bits are the sponsor - _nonceValidation(resolvedOrder_.user, nonce_); - // Set a nonce or revert if it is already used - _checkAndSetNonce(nonce_); - - // We do not enforce a specific tribunal or arbiter. This will allow to support new arbiters and tribunals after the deployment of the allocator - // Going with an immutable arbiter and tribunal would limit support for new chains with a fully decentralized allocator - - bytes32 tokenHash = - _verifyAllocation(resolvedOrder_.user, resolvedOrder_.openDeadline, orderData_, lockTagOffset); - - // Create the Compact claim hash - bytes32 claimHash; - uint256 lockTagAbsoluteOffset; - assembly ("memory-safe") { - lockTagAbsoluteOffset := add(orderData_.offset, lockTagOffset) - - let m := mload(0x40) - mstore(m, COMPACT_WITNESS_TYPEHASH) - calldatacopy(add(m, 0x20), orderData_.offset, 0x20) // arbiter - mstore(add(m, 0x40), mload(resolvedOrder_)) // sponsor (first item in resolvedOrder_) - mstore(add(m, 0x60), nonce_) // nonce - mstore(add(m, 0x80), mload(add(resolvedOrder_, 0x40))) // Compact.expires (third item in resolvedOrder_) - calldatacopy(add(m, 0xa0), lockTagAbsoluteOffset, 0x60) // lockTag, inputToken, amount - mstore(add(m, 0x100), mandateHash_) - claimHash := keccak256(m, 0x120) - } - - // We check for the length, which means this could also be triggered by a zero length signature provided in the openFor function. This enables relaying of orders if the claim was registered on the compact. - if (sponsorSignature_.length > 0) { - bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), _COMPACT_DOMAIN_SEPARATOR, claimHash)); - // confirm the signature matches the digest - address signer_ = _recoverSigner(digest, sponsorSignature_); - if (resolvedOrder_.user != signer_) { - revert InvalidSignature(resolvedOrder_.user, signer_); - } - } else { - // confirm the claim hash is registered on the compact - (bool isActive) = - ITheCompact(COMPACT_CONTRACT).isRegistered(resolvedOrder_.user, claimHash, COMPACT_WITNESS_TYPEHASH); - if (!isActive) { - revert InvalidRegistration(resolvedOrder_.user, claimHash); - } - } - - bytes32 qualifiedClaimHash; - assembly ("memory-safe") { - let m := mload(0x40) - mstore(m, QUALIFICATION_TYPEHASH) - mstore(add(m, 0x20), claimHash) - - mstore(add(m, 0x40), 0x0) // clear targetBlock - mstore(add(m, 0x60), 0x0) // clear maximumBlocksAfterTarget - if eq(lockTagOffset, ORDERDATA_LOCKTAG_OFFSET) { - // if data is of type OrderData, copy targetBlock and maximumBlocksAfterTarget - calldatacopy(add(m, 0x40), add(lockTagAbsoluteOffset, 0x180), 0x40) // targetBlock, maximumBlocksAfterTarget - } - qualifiedClaimHash := keccak256(m, 0x80) - } - uint256 amount; - assembly ("memory-safe") { - // load amount from orderData_ - amount := calldataload(add(lockTagAbsoluteOffset, 0x40)) - } + // Register the allocation on chain + (bytes32 claimHash, uint256 nonce) = registerAllocationFor( + sponsor, orderData.commitments, arbiter, expires, COMPACT_WITNESS_TYPEHASH, mandateHash_, sponsorSignature_ + ); - _lockTokens(tokenHash, amount, resolvedOrder_.openDeadline, qualifiedClaimHash); + qualifications[claimHash] = qualification_; // Emit an open event - emit Open(bytes32(nonce_), resolvedOrder_); + emit Open(bytes32(nonce), resolvedOrder_); } - function _lockTokens(bytes32 tokenHash, uint256 amount, uint256 expires, bytes32 claimHash) internal { - // Lock the tokens - _claim[claimHash] = true; - _allocation[tokenHash] = _allocationData(amount, expires); - } - - function _verifyAllocation(address sponsor_, uint32 openDeadline_, bytes calldata orderData_, uint256 lockTagOffset) + function _decodeOrderData(bytes calldata orderData_, uint256 offset) internal - view - returns (bytes32 tokenHash_) + pure + returns ( + address arbiter, + uint32 expires, + Order calldata orderData, + uint200 targetBlock, + uint56 maximumBlocksAfterTarget + ) { - bytes12 lockTag; - address inputToken; - uint256 amount; - + bytes calldata orderDataBytes = LibBytes.dynamicStructInCalldata(orderData_, offset); assembly ("memory-safe") { - lockTag := calldataload(add(orderData_.offset, lockTagOffset)) - inputToken := calldataload(add(orderData_.offset, add(lockTagOffset, 0x20))) - amount := calldataload(add(orderData_.offset, add(lockTagOffset, 0x40))) + let isOnChain := eq(offset, 0x40) + arbiter := calldataload(orderData_.offset) + expires := mul(calldataload(add(orderData_.offset, 0x20)), isOnChain) + orderData := orderDataBytes.offset + targetBlock := mul(calldataload(0x120), isOnChain) + maximumBlocksAfterTarget := mul(calldataload(0x140), isOnChain) } - - // Check for a valid allocation - tokenHash_ = _checkForActiveAllocation(sponsor_, lockTag, inputToken); - _checkAllocator(lockTag); - _checkExpiration(openDeadline_); - _checkForcedWithdrawal(sponsor_, openDeadline_, lockTag, inputToken); - _checkBalance(sponsor_, _getTokenId(lockTag, inputToken), amount); - - return tokenHash_; + return (arbiter, expires, orderData, targetBlock, maximumBlocksAfterTarget); } function _resolveOrder( + address arbiter, address sponsor, uint256 nonce, - uint256 expires, + uint32 expires, uint32 fillDeadline, - bytes calldata orderData, - uint256 lockTagOffset, - bytes calldata sponsorSignature + Order calldata orderData, + bytes calldata sponsorSignature, + uint200 targetBlock, + uint56 maximumBlocksAfterTarget ) internal view returns (ResolvedCrossChainOrder memory) { ResolvedCrossChainOrder memory resolvedOrder = ResolvedCrossChainOrder({ user: sponsor, @@ -364,72 +319,36 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { fillInstructions: new FillInstruction[](0) }); - Compact memory compact; - - assembly ("memory-safe") { - let m := mload(0x40) - - calldatacopy(m, orderData.offset, 0x20) // arbiter - mstore(add(m, 0x20), sponsor) // sponsor - mstore(add(m, 0x40), nonce) // nonce - mstore(add(m, 0x60), expires) // expires - calldatacopy(add(m, 0x80), add(orderData.offset, lockTagOffset), 0x60) // lockTag, inputToken, amount - - compact := m - mstore(0x40, add(m, 0xe0)) // update free memory pointer - } + BatchCompact memory compact = BatchCompact({ + arbiter: arbiter, + sponsor: sponsor, + nonce: nonce, + expires: expires, + commitments: orderData.commitments + }); - Claim memory claim = Claim({ + BatchClaim memory claim = BatchClaim({ chainId: block.chainid, compact: compact, sponsorSignature: sponsorSignature, allocatorSignature: '' // No signature required from this allocator, it will verify the claim on chain via ERC1271. }); - Mandate memory mandate; - assembly ("memory-safe") { - let m := mload(0x40) - - let mandateOffset := add(add(orderData.offset, lockTagOffset), 0x60) - - // Skip the chainId and tribunal, as they are implicit arguments in the Tribunal.Mandate - - calldatacopy(m, add(mandateOffset, 0x40), 0x20) // recipient - mstore(add(m, 0x20), fillDeadline) // expires - calldatacopy(add(m, 0x40), add(mandateOffset, 0x60), 0xc0) // settlementToken, minimumAmount, baselinePriorityFee, scalingFactor, decayCurve.offset, salt - mstore(add(m, 0xc0), add(m, 0x100)) // update decayCurve.offset to point to the relative memory location of decayCurve.length within Mandate - - let decayCurveOffset := calldataload(add(mandateOffset, 0xe0)) - let decayCurveLength := calldataload(add(orderData.offset, decayCurveOffset)) - - calldatacopy(add(m, 0x100), add(orderData.offset, decayCurveOffset), add(decayCurveLength, 0x20)) // decayCurve.length, decayCurve.content - - mandate := m - - let totalMandateLength := add(0x120, decayCurveLength) - - mstore(0x40, add(m, totalMandateLength)) // update free memory pointer - } - - uint256 chainId; - bytes32 tribunal; - uint256 targetBlock; - uint256 maximumBlocksAfterTarget; - - assembly ("memory-safe") { - let mandateOffset := add(add(orderData.offset, lockTagOffset), 0x60) - chainId := calldataload(mandateOffset) - tribunal := calldataload(add(mandateOffset, 0x20)) - let isOrderData := eq(lockTagOffset, ORDERDATA_LOCKTAG_OFFSET) - // Multiply the data calldata value by 0 if OrderDataGasless, as OrderDataGasless does not support targetBlock and maximumBlocksAfterTarget - targetBlock := mul(calldataload(add(mandateOffset, mul(0x120, isOrderData))), isOrderData) // Multiply targetBlock offset by 0 if OrderDataGasless to prevent out of bounds calldata read - maximumBlocksAfterTarget := mul(calldataload(add(mandateOffset, mul(0x140, isOrderData))), isOrderData) // Multiply maximumBlocksAfterTarget offset by 0 if OrderDataGasless to prevent out of bounds calldata read - } + Mandate memory mandate = Mandate({ + recipient: orderData.recipient, + expires: fillDeadline, + token: orderData.settlementToken, + minimumAmount: orderData.minimumAmount, + baselinePriorityFee: orderData.baselinePriorityFee, + scalingFactor: orderData.scalingFactor, + decayCurve: orderData.decayCurve, + salt: orderData.salt + }); FillInstruction[] memory fillInstructions = new FillInstruction[](1); fillInstructions[0] = FillInstruction({ - destinationChainId: chainId, - destinationSettler: tribunal, + destinationChainId: orderData.chainId, + destinationSettler: _addressToBytes32(orderData.tribunal), originData: abi.encode(claim, mandate, targetBlock, maximumBlocksAfterTarget) }); resolvedOrder.fillInstructions = fillInstructions; @@ -438,93 +357,49 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { token: _addressToBytes32(mandate.token), amount: type(uint256).max, recipient: _addressToBytes32(mandate.recipient), - chainId: chainId + chainId: orderData.chainId }); Output[] memory maxSpent = new Output[](1); maxSpent[0] = spent; resolvedOrder.maxSpent = maxSpent; - Output memory received = Output({ - token: _addressToBytes32(compact.token), - amount: compact.amount, - recipient: bytes32(0), - chainId: block.chainid - }); - Output[] memory minReceived = new Output[](1); - minReceived[0] = received; - resolvedOrder.minReceived = minReceived; + resolvedOrder.minReceived = _createMinimumReceived(orderData.commitments); return resolvedOrder; } - function _nonceValidation(address sponsor_, uint256 nonce_) internal pure { - // Enforce a nonce where the least significant 96 bits are the nonce and the most significant 160 bits are the sponsors address - // This ensures that the nonce is unique for a given sponsor - address expectedSponsor; - assembly ("memory-safe") { - expectedSponsor := shr(96, nonce_) - } - if (expectedSponsor != sponsor_) { - revert InvalidNonce(nonce_); + function _createMinimumReceived(Lock[] calldata commitments) internal view returns (Output[] memory) { + Output[] memory minReceived = new Output[](commitments.length); + + for (uint256 i = 0; i < commitments.length; i++) { + Output memory received = Output({ + token: _addressToBytes32(commitments[i].token), + amount: commitments[i].amount, + recipient: bytes32(0), + chainId: block.chainid + }); + minReceived[i] = received; } + return minReceived; } - function _recoverSigner(bytes32 digest, bytes calldata signature) internal pure returns (address) { - bytes32 r; - bytes32 s; - uint8 v; - - if (signature.length == 65) { - (r, s) = abi.decode(signature, (bytes32, bytes32)); - v = uint8(signature[64]); - } else if (signature.length == 64) { - bytes32 vs; - (r, vs) = abi.decode(signature, (bytes32, bytes32)); - v = uint8(uint256(vs >> 255) + 27); - s = vs << 1 >> 1; - } else { - return address(0); - } - - return ecrecover(digest, v, r, s); - } - - function _mandateHash(bytes calldata orderData, uint256 mandateOffset, uint32 fillDeadline) - internal - pure - returns (bytes32 mandateHash_) - { - // total mandate length: x160 + decayCurve.length - assembly ("memory-safe") { - let decayCurveOffset := calldataload(add(mandateOffset, 0xe0)) - let decayCurveLength := calldataload(add(orderData.offset, decayCurveOffset)) - - let m := mload(0x40) - mstore(m, MANDATE_TYPEHASH) - calldatacopy(add(m, 0x20), mandateOffset, 0x60) // chainid, tribunal and recipient - mstore(add(m, 0x80), fillDeadline) // mandate.expires - calldatacopy(add(m, 0xa0), add(mandateOffset, 0x60), 0xc0) // settlementToken, minimumAmount, baselinePriorityFee, scalingFactor, decayCurve.offset, salt - - for { let i := 0 } lt(i, decayCurveLength) { i := add(i, 0x20) } { - mstore(add(m, add(0x160, i)), calldataload(add(add(orderData.offset, decayCurveOffset), add(i, 0x20)))) // copy the content of decayCurve to memory - } - - mstore(add(m, 0x120), keccak256(add(m, 0x160), mul(decayCurveLength, 0x20))) // create and store the decayCurve hash - - mandateHash_ := keccak256(m, 0x160) // mandate typehash + mandate data length - } - - // 0x00: MANDATE_TYPEHASH - // 0x20: chainid - // 0x40: tribunal - // 0x60: recipient - // 0x80: fillDeadline - // 0xa0: settlementToken - // 0xc0: minimumAmount - // 0xe0: baselinePriorityFee - // 0x100: scalingFactor - // 0x120: decayCurve hash - // 0x140: salt + function _mandateHash(Order calldata orderData, uint32 fillDeadline) internal pure returns (bytes32 mandateHash_) { + bytes32 decayCurveHash = keccak256(abi.encodePacked(orderData.decayCurve)); + mandateHash_ = keccak256( + abi.encode( + MANDATE_TYPEHASH, + orderData.chainId, + orderData.tribunal, + orderData.recipient, + fillDeadline, + orderData.settlementToken, + orderData.minimumAmount, + orderData.baselinePriorityFee, + orderData.scalingFactor, + decayCurveHash, + orderData.salt + ) + ); } function _addressToBytes32(address address_) internal pure returns (bytes32 output_) { diff --git a/src/interfaces/IERC7683Allocator.sol b/src/interfaces/IERC7683Allocator.sol index 2d26a38..2ed0551 100644 --- a/src/interfaces/IERC7683Allocator.sol +++ b/src/interfaces/IERC7683Allocator.sol @@ -4,42 +4,33 @@ pragma solidity ^0.8.27; import {IOriginSettler} from './ERC7683/IOriginSettler.sol'; import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; +import {Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; interface IERC7683Allocator is IOriginSettler, IAllocator { - struct OrderData { - // COMPACT + struct OrderDataOnChain { + // BATCH_COMPACT address arbiter; // The account tasked with verifying and submitting the claim. - address sponsor; // The account to source the tokens from. - uint256 nonce; // A parameter to enforce replay protection, scoped to allocator. + // address sponsor; // The account to source the tokens from. + // uint256 nonce; // A parameter to enforce replay protection, scoped to allocator. uint256 expires; // The time at which the claim expires. - bytes12 lockTag; // The token ID of the ERC6909 token to allocate. - address inputToken; // The token address of the ERC6909 token to allocate. - uint256 amount; // The amount of ERC6909 tokens to allocate. - // MANDATE - uint256 chainId; // (implicit arg, included in EIP712 payload) - address tribunal; // (implicit arg, included in EIP712 payload) - address recipient; // Recipient of settled tokens - // uint256 expires; // Mandate expiration timestamp - address settlementToken; // Settlement token (address(0) for native) - uint256 minimumAmount; // Minimum settlement amount - uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in - uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline) - uint256[] decayCurve; // Block durations, fill increases, & claim decreases. - bytes32 salt; // Replay protection parameter + Order order; // The remaining BatchCompact and Mandate data // ADDITIONAL INPUT - uint256 targetBlock; // The block number at the target chain on which the PGA is executed / the reverse dutch auction starts. - uint256 maximumBlocksAfterTarget; // Blocks after target block that are still fillable. + uint200 targetBlock; // The block number at the target chain on which the PGA is executed / the reverse dutch auction starts. + uint56 maximumBlocksAfterTarget; // Blocks after target block that are still fillable. } struct OrderDataGasless { - // COMPACT + // BATCH_COMPACT address arbiter; // The account tasked with verifying and submitting the claim. // address sponsor; // The account to source the tokens from. // uint256 nonce; // A parameter to enforce replay protection, scoped to allocator. // uint256 expires; // The time at which the claim expires. - bytes12 lockTag; // The lock tag of the ERC6909 token to allocate. - address inputToken; // The token address of the ERC6909 token to allocate. - uint256 amount; // The amount of ERC6909 tokens to allocate. + Order order; // The remaining BatchCompact and Mandate data + } + + /// @dev The data that OnChain and Gasless orders have in common + struct Order { + Lock[] commitments; // The token IDs and amounts to allocate. // MANDATE uint256 chainId; // (implicit arg, included in EIP712 payload) address tribunal; // (implicit arg, included in EIP712 payload) @@ -55,10 +46,9 @@ interface IERC7683Allocator is IOriginSettler, IAllocator { error InvalidOriginSettler(address originSettler, address expectedOriginSettler); error InvalidOrderDataType(bytes32 orderDataType, bytes32 expectedOrderDataType); - error InvalidNonce(uint256 nonce); - error InvalidSignature(address signer, address expectedSigner); - error InvalidRegistration(address sponsor, bytes32 claimHash); + error InvalidNonce(uint256 nonce, uint256 expectedNonce); error BatchCompactsNotSupported(); + error InvalidAllocatorData(bytes32 expectedAllocatorData, bytes32 actualAllocatorData); /// @notice Returns the type string of the compact including the witness function getCompactWitnessTypeString() external pure returns (string memory); From cdef204b9a794a88c710d4d3c852309943d799dd Mon Sep 17 00:00:00 2001 From: mgretzke Date: Mon, 28 Jul 2025 15:47:10 +0200 Subject: [PATCH 38/63] optimization and fixing issues --- src/allocators/ERC7683Allocator.sol | 70 +++-- src/allocators/OnChainAllocator.sol | 118 ++++---- src/interfaces/IOnChainAllocator.sol | 14 +- test/ERC7683Allocator.t.sol | 398 +++++++++++++-------------- 4 files changed, 306 insertions(+), 294 deletions(-) diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index bcd2e81..f7bfeb0 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -22,16 +22,16 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { 0x95d7f00c299b34a562258ba851472a8d9bd0d8a1b88fce3a37b7d27ca06e77c4; /// @notice The typehash of the OrderDataGasless struct - // keccak256("OrderDataGasless(address arbiter,Order order, + // keccak256("OrderDataGasless(address arbiter,Order order) // Lock(bytes12 lockTag,address token,uint256 amount) // Order(Lock[] commitments,uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") bytes32 public constant ORDERDATA_GASLESS_TYPEHASH = - 0xfddec069b5987011094d234ae1829188ce34de94b23d06395ba4e6e4749a544e; + 0xdebd9e7866045b7f0ce8613ffbb31daa3fa5c6e6ac228316ba9f57fda63b7489; /// @notice keccak256("BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments,Mandate mandate) // Lock(bytes12 lockTag,address token,uint256 amount) // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") - bytes32 public constant COMPACT_WITNESS_TYPEHASH = + bytes32 public constant BATCH_COMPACT_WITNESS_TYPEHASH = 0x5ede122c736b60a8b718f83dcfb5d6e4aa27c9714d0c7bc9ca86562b8f878463; /// @notice keccak256("Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") @@ -63,7 +63,7 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { } // Decode the orderData - (address arbiter,, Order calldata orderData,,) = _decodeOrderData(order_.orderData, 0x20); + (address arbiter,, Order calldata orderData,,) = _decodeOrderData(order_.orderData, 0x40); ResolvedCrossChainOrder memory resolvedOrder = _resolveOrder( arbiter, @@ -105,7 +105,7 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { Order calldata orderData, uint200 targetBlock, uint56 maximumBlocksAfterTarget - ) = _decodeOrderData(order.orderData, 0x40); + ) = _decodeOrderData(order.orderData, 0x60); bytes32 mandateHash = _mandateHash(orderData, order.fillDeadline); @@ -153,7 +153,7 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { } // Decode the orderData - (address arbiter,, Order calldata orderData,,) = _decodeOrderData(order_.orderData, 0x20); + (address arbiter,, Order calldata orderData,,) = _decodeOrderData(order_.orderData, 0x40); return _resolveOrder( arbiter, @@ -182,7 +182,7 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { Order calldata orderData, uint200 targetBlock, uint56 maximumBlocksAfterTarget - ) = _decodeOrderData(order.orderData, 0x40); + ) = _decodeOrderData(order.orderData, 0x60); return _resolveOrder( arbiter, @@ -264,8 +264,14 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { ResolvedCrossChainOrder memory resolvedOrder_ ) internal { // Register the allocation on chain - (bytes32 claimHash, uint256 nonce) = registerAllocationFor( - sponsor, orderData.commitments, arbiter, expires, COMPACT_WITNESS_TYPEHASH, mandateHash_, sponsorSignature_ + (bytes32 claimHash, uint256 nonce) = allocateFor( + sponsor, + orderData.commitments, + arbiter, + expires, + BATCH_COMPACT_WITNESS_TYPEHASH, + mandateHash_, + sponsorSignature_ ); qualifications[claimHash] = qualification_; @@ -274,27 +280,53 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { emit Open(bytes32(nonce), resolvedOrder_); } - function _decodeOrderData(bytes calldata orderData_, uint256 offset) + function _decodeOrderData(bytes calldata orderData, uint256 offset) internal pure returns ( address arbiter, uint32 expires, - Order calldata orderData, + Order calldata order, uint200 targetBlock, uint56 maximumBlocksAfterTarget ) { - bytes calldata orderDataBytes = LibBytes.dynamicStructInCalldata(orderData_, offset); + // orderData includes the OrderData(OnChain/Gasless) struct, and the nested Order struct. + // 0x00: OrderDataOnChain.offset + // 0x20: OrderDataOnChain.arbiter + // 0x40: OrderDataOnChain.expires + // 0x60: OrderDataOnChain.order.offset + + // 0x00: OrderDataGasless.offset + // 0x20: OrderDataGasless.arbiter + // 0x40: OrderDataGasless.order.offset + assembly ("memory-safe") { - let isOnChain := eq(offset, 0x40) - arbiter := calldataload(orderData_.offset) - expires := mul(calldataload(add(orderData_.offset, 0x20)), isOnChain) - orderData := orderDataBytes.offset - targetBlock := mul(calldataload(0x120), isOnChain) - maximumBlocksAfterTarget := mul(calldataload(0x140), isOnChain) + let l := sub(orderData.length, 0x20) + let s := calldataload(add(orderData.offset, offset)) // Relative offset of `orderBytes` from `orderData.offset` and the `OrderData...` struct. + order := add(orderData.offset, add(s, 0x20)) // Add 0x20 since the OrderStruct is within the `OrderData...` struct + if or(shr(64, or(s, or(l, orderData.offset))), gt(offset, l)) { revert(l, 0x00) } + + let isOnChain := eq(offset, 0x60) + arbiter := calldataload(add(orderData.offset, 0x20)) + expires := mul(calldataload(add(orderData.offset, 0x40)), isOnChain) + targetBlock := mul(calldataload(0x124), isOnChain) + maximumBlocksAfterTarget := mul(calldataload(0x144), isOnChain) + } + + return (arbiter, expires, order, targetBlock, maximumBlocksAfterTarget); + } + + /// @dev Returns a slice representing a dynamic struct in the calldata. Performs bounds checks. + function _dynamicStructInCalldata(bytes calldata a, uint256 offset) internal pure returns (bytes calldata result) { + /// @solidity memory-safe-assembly + assembly { + let l := sub(a.length, 0x20) + let s := calldataload(add(a.offset, offset)) // Relative offset of `result` from `a.offset`. + result.offset := add(a.offset, add(s, 0x20)) // Add 0x20 since the OrderDataStruct is inside another struct + result.length := sub(a.length, add(s, 0x20)) + if or(shr(64, or(s, or(l, a.offset))), gt(offset, l)) { revert(l, 0x00) } } - return (arbiter, expires, orderData, targetBlock, maximumBlocksAfterTarget); } function _resolveOrder( diff --git a/src/allocators/OnChainAllocator.sol b/src/allocators/OnChainAllocator.sol index 7706274..d910055 100644 --- a/src/allocators/OnChainAllocator.sol +++ b/src/allocators/OnChainAllocator.sol @@ -31,14 +31,11 @@ contract OnChainAllocator is IOnChainAllocator { } /// @inheritdoc IOnChainAllocator - function registerAllocation( - Lock[] calldata commitments, - address arbiter, - uint32 expires, - bytes32 typehash, - bytes32 witness - ) public returns (bytes32 claimHash, uint256 claimNonce) { - (claimHash, claimNonce) = _registerAllocation(msg.sender, commitments, arbiter, expires, typehash, witness); + function allocate(Lock[] calldata commitments, address arbiter, uint32 expires, bytes32 typehash, bytes32 witness) + public + returns (bytes32 claimHash, uint256 claimNonce) + { + (claimHash, claimNonce) = _allocate(msg.sender, commitments, arbiter, expires, typehash, witness); emit AllocationRegistered(msg.sender, claimHash, claimNonce, expires, commitments); @@ -46,7 +43,7 @@ contract OnChainAllocator is IOnChainAllocator { } /// @inheritdoc IOnChainAllocator - function registerAllocationFor( + function allocateFor( address sponsor, Lock[] calldata commitments, address arbiter, @@ -55,7 +52,7 @@ contract OnChainAllocator is IOnChainAllocator { bytes32 witness, bytes calldata signature ) public returns (bytes32 claimHash, uint256 claimNonce) { - (claimHash, claimNonce) = _registerAllocation(sponsor, commitments, arbiter, expires, typehash, witness); + (claimHash, claimNonce) = _allocate(sponsor, commitments, arbiter, expires, typehash, witness); // We check for the length, which means this could also be triggered by a zero length signature provided in the openFor function. // This enables relaying of orders if the claim was registered on the compact. @@ -144,7 +141,7 @@ contract OnChainAllocator is IOnChainAllocator { return false; } - function _registerAllocation( + function _allocate( address sponsor, Lock[] calldata commitments, address arbiter, @@ -156,7 +153,7 @@ contract OnChainAllocator is IOnChainAllocator { nonce = ++nonces[sponsor]; claimHash = keccak256(abi.encode(typehash, arbiter, sponsor, nonce, expires, commitmentsHash, witness)); - uint256 minResetPeriod; + uint256 minResetPeriod = type(uint256).max; for (uint256 i = 0; i < commitments.length; i++) { minResetPeriod = _checkInput(commitments[i], sponsor, expires, minResetPeriod); bytes32 tokenHash = _checkBalance(sponsor, commitments[i]); @@ -168,7 +165,7 @@ contract OnChainAllocator is IOnChainAllocator { } // Ensure expiration is not bigger then the smallest reset period if (expires > block.timestamp + minResetPeriod) { - revert InvalidExpiration(expires); + revert InvalidExpiration(expires, block.timestamp + minResetPeriod); } return (claimHash, nonce); @@ -179,9 +176,6 @@ contract OnChainAllocator is IOnChainAllocator { view returns (uint256) { - // TODO: Discuss which checks to leave in, and which to remove. - // Some of the checks are decreasing the responsibility of the sponsor, others of the filler. - // Check the allocator id fits this allocator if (_splitAllocatorId(commitment.lockTag) != ALLOCATOR_ID) { revert InvalidAllocator(_splitAllocatorId(commitment.lockTag), ALLOCATOR_ID); @@ -222,55 +216,48 @@ contract OnChainAllocator is IOnChainAllocator { function _allocatedBalance(bytes32 tokenHash) internal returns (uint256 allocatedBalance) { // using assembly to only read the allocated balance + expiration slot and skipping the claimHash slot assembly ("memory-safe") { - // TODO: caching will optimize for a claim that includes the same token multiple times. Is it worth the 200 gas? - allocatedBalance := tload(tokenHash) - if iszero(allocatedBalance) { - // no previous cached balance, calculate the allocated balance - mstore(0x00, tokenHash) - mstore(0x20, _allocations.slot) - // retrieve the array length slot - let arrayLengthSlog := keccak256(0x00, 0x40) - let origLength := sload(arrayLengthSlog) - let length := origLength - // retrieve the arrays content slot - let contentSlot := keccak256(0x00, 0x20) - for { let i := 0 } lt(i, length) {} { - let slot := add(contentSlot, mul(i, 0x40)) // 0x40 to skip the claimHash slot - let content := sload(slot) - let expiration := shr(224, shl(224, content)) - if lt(expiration, timestamp()) { - // allocation expired, remove it - let lastSlot := add(contentSlot, mul(sub(length, 1), 0x40)) - if iszero(eq(slot, lastSlot)) { - // is not the last allocation of the array - let contentLast1 := sload(lastSlot) - let contentLast2 := sload(add(lastSlot, 0x20)) - sstore(slot, contentLast1) - sstore(add(slot, 0x20), contentLast2) - } - // remove the last allocation - length := sub(length, 1) - sstore(lastSlot, 0) - sstore(add(lastSlot, 0x20), 0) - - // repeat the loop at the same index - continue + // no previous cached balance, calculate the allocated balance + mstore(0x00, tokenHash) + mstore(0x20, _allocations.slot) + // retrieve the array length slot + let arrayLengthSlot := keccak256(0x00, 0x40) + let origLength := sload(arrayLengthSlot) + let length := origLength + // retrieve the arrays content slot + let contentSlot := keccak256(0x00, 0x20) + for { let i := 0 } lt(i, length) {} { + let slot := add(contentSlot, mul(i, 2)) // 0x40 to skip the claimHash slot + let content := sload(slot) + let expiration := shr(224, shl(224, content)) + if lt(expiration, timestamp()) { + // allocation expired, remove it + let lastSlot := add(contentSlot, mul(sub(length, 1), 2)) + if iszero(eq(slot, lastSlot)) { + // is not the last allocation of the array + let contentLast1 := sload(lastSlot) + let contentLast2 := sload(add(lastSlot, 1)) + sstore(slot, contentLast1) + sstore(add(slot, 1), contentLast2) } + // remove the last allocation + length := sub(length, 1) + sstore(lastSlot, 0) + sstore(add(lastSlot, 1), 0) - let amount := shr(32, content) - allocatedBalance := add(allocatedBalance, amount) - - // jump to the next allocation - i := add(i, 1) + // repeat the loop at the same index + continue } - if lt(length, origLength) { - // update the array length - sstore(arrayLengthSlog, length) - } + let amount := shr(32, content) + allocatedBalance := add(allocatedBalance, amount) - // Cache the allocated balance in case the token is part of the same claim again. - tstore(tokenHash, allocatedBalance) + // jump to the next allocation + i := add(i, 1) + } + + if lt(length, origLength) { + // update the array length + sstore(arrayLengthSlot, length) } } return allocatedBalance; @@ -286,7 +273,7 @@ contract OnChainAllocator is IOnChainAllocator { mstore(0x00, lengthSlot) let contentSlot := keccak256(0x00, 0x20) for { let i := 0 } lt(i, length) { i := add(i, 1) } { - let slot2 := add(contentSlot, add(mul(i, 0x40), 0x20)) // add 0x20 to skip the expires/amount slot + let slot2 := add(contentSlot, add(mul(i, 2), 1)) // add 0x20 to skip the expires/amount slot let content2 := sload(slot2) if eq(content2, claimHash) { // delete the allocation @@ -336,13 +323,13 @@ contract OnChainAllocator is IOnChainAllocator { } function _getCommitmentsHash(Lock[] memory commitments) internal pure returns (bytes32) { - bytes32 commitmentsHash; + bytes32[] memory commitmentsHashes = new bytes32[](commitments.length); for (uint256 i = 0; i < commitments.length; i++) { - commitmentsHash = keccak256( + commitmentsHashes[i] = keccak256( abi.encode(LOCK_TYPEHASH, commitments[i].lockTag, commitments[i].token, commitments[i].amount) ); } - return commitmentsHash; + return keccak256(abi.encodePacked(commitmentsHashes)); } function _getTokenHash(Lock calldata commitment, address sponsor) internal pure returns (bytes32 tokenHash) { @@ -356,7 +343,8 @@ contract OnChainAllocator is IOnChainAllocator { } function _getTokenHash(uint256 id, address sponsor) internal pure returns (bytes32 tokenHash) { - return keccak256(abi.encode(id, sponsor)); + tokenHash = keccak256(abi.encode(id, sponsor)); + return tokenHash; } function _splitAllocatorId(bytes12 lockTag) internal pure returns (uint96) { diff --git a/src/interfaces/IOnChainAllocator.sol b/src/interfaces/IOnChainAllocator.sol index 4336830..9376032 100644 --- a/src/interfaces/IOnChainAllocator.sol +++ b/src/interfaces/IOnChainAllocator.sol @@ -25,7 +25,7 @@ interface IOnChainAllocator is IAllocator { error InsufficientBalance(address sponsor, uint256 id, uint256 balance, uint256 expectedBalance); /// @notice Thrown if the provided expiration is not valid - error InvalidExpiration(uint256 expires); + error InvalidExpiration(uint256 expires, uint256 minExpiration); /// @notice Thrown if the expiration is longer then the tokens forced withdrawal time error ForceWithdrawalAvailable(uint256 expires, uint256 forcedWithdrawalExpiration); @@ -65,13 +65,9 @@ interface IOnChainAllocator is IAllocator { /// @param expires The expiration of the allocation /// @param typehash The typehash of the allocation /// @param witness The witness of the allocation - function registerAllocation( - Lock[] memory commitments, - address arbiter, - uint32 expires, - bytes32 typehash, - bytes32 witness - ) external returns (bytes32 claimHash, uint256 claimNonce); + function allocate(Lock[] memory commitments, address arbiter, uint32 expires, bytes32 typehash, bytes32 witness) + external + returns (bytes32 claimHash, uint256 claimNonce); /// @notice Registers an allocation for a set of tokens on behalf of a sponsor /// @param sponsor The address of the sponsor @@ -81,7 +77,7 @@ interface IOnChainAllocator is IAllocator { /// @param typehash The typehash of the allocation /// @param witness The witness of the allocation /// @param signature The signature of the allocation - function registerAllocationFor( + function allocateFor( address sponsor, Lock[] memory commitments, address arbiter, diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index 5bca0ee..5bbbeac 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -9,9 +9,9 @@ import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; import {IdLib} from '@uniswap/the-compact/lib/IdLib.sol'; -import {Claim} from '@uniswap/the-compact/types/Claims.sol'; -import {Component} from '@uniswap/the-compact/types/Components.sol'; -import {COMPACT_TYPEHASH, Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; +import {BatchClaim} from '@uniswap/the-compact/types/BatchClaims.sol'; +import {BatchClaimComponent, Component} from '@uniswap/the-compact/types/Components.sol'; +import {BatchCompact, COMPACT_TYPEHASH, LOCK_TYPEHASH, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; import {ForcedWithdrawalStatus} from '@uniswap/the-compact/types/ForcedWithdrawalStatus.sol'; import {ResetPeriod} from '@uniswap/the-compact/types/ResetPeriod.sol'; import {Scope} from '@uniswap/the-compact/types/Scope.sol'; @@ -20,9 +20,10 @@ import {Test, console} from 'forge-std/Test.sol'; import {TestHelper} from 'test/util/TestHelper.sol'; import {ERC7683Allocator} from 'src/allocators/ERC7683Allocator.sol'; -import {Claim as TribunalClaim, Mandate} from 'src/allocators/types/TribunalStructs.sol'; +import {BatchClaim as TribunalClaim, Mandate} from 'src/allocators/types/TribunalStructs.sol'; import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; import {IERC7683Allocator} from 'src/interfaces/IERC7683Allocator.sol'; +import {IOnChainAllocator} from 'src/interfaces/IOnchainAllocator.sol'; import {ISimpleAllocator} from 'src/interfaces/ISimpleAllocator.sol'; import {ERC20Mock} from 'src/test/ERC20Mock.sol'; @@ -53,11 +54,14 @@ abstract contract MocksSetup is Test, TestHelper { uint256 defaultScalingFactor = 0; uint256[] defaultDecayCurve = new uint256[](0); bytes32 defaultSalt = bytes32(0x0000000000000000000000000000000000000000000000000000000000000007); - uint256 defaultTargetBlock = 100; - uint256 defaultMaximumBlocksAfterTarget = 10; + uint200 defaultTargetBlock = 100; + uint56 defaultMaximumBlocksAfterTarget = 10; + + uint256[2][] defaultIdsAndAmounts = new uint256[2][](1); + Lock[] defaultCommitments; bytes32 ORDERDATA_GASLESS_TYPEHASH; - bytes32 ORDERDATA_TYPEHASH; + bytes32 ORDERDATA_ONCHAIN_TYPEHASH; function setUp() public virtual { (user, userPK) = makeAddrAndKey('user'); @@ -65,15 +69,19 @@ abstract contract MocksSetup is Test, TestHelper { tribunal = makeAddr('tribunal'); usdc = new ERC20Mock('USDC', 'USDC'); compactContract = new TheCompact(); - erc7683Allocator = new ERC7683Allocator(address(compactContract), 5, 100); + erc7683Allocator = new ERC7683Allocator(address(compactContract)); usdcLockTag = _toLockTag(address(erc7683Allocator), defaultScope, defaultResetPeriod); usdcId = _toId(defaultScope, defaultResetPeriod, address(erc7683Allocator), address(usdc)); (attacker, attackerPK) = makeAddrAndKey('attacker'); - defaultNonce = uint256(bytes32(abi.encodePacked(user, uint96(1)))); + defaultNonce = 1; - ORDERDATA_GASLESS_TYPEHASH = erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH(); - ORDERDATA_TYPEHASH = erc7683Allocator.ORDERDATA_TYPEHASH(); + ORDERDATA_GASLESS_TYPEHASH = keccak256( + 'OrderDataGasless(address arbiter,Order order)Lock(bytes12 lockTag,address token,uint256 amount)Order(Lock[] commitments,uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)' + ); + ORDERDATA_ONCHAIN_TYPEHASH = keccak256( + 'OrderDataOnChain(address arbiter,uint256 expires,Order order,uint200 targetBlock,uint56 maximumBlocksAfterTarget)Lock(bytes12 lockTag,address token,uint256 amount)Order(Lock[] commitments,uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)' + ); } } @@ -90,12 +98,14 @@ abstract contract CreateHash is MocksSetup { string compactWitnessTypeString = 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,bytes12 lockTag,address token,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; + string batchCompactWitnessTypeString = + 'BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments,Mandate mandate)Lock(bytes12 lockTag,address token,uint256 amount)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; string mandateTypeString = 'Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; string witnessTypeString = 'uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt'; - function _hashCompact(Compact memory data, Mandate memory mandate, address verifyingContract) + function _hashCompact(BatchCompact memory data, Mandate memory mandate, address verifyingContract) internal view returns (bytes32 digest) @@ -111,18 +121,36 @@ abstract contract CreateHash is MocksSetup { ); } - function _hashCompact(Compact memory data, Mandate memory mandate) internal view returns (bytes32 compactHash) { + function _hashCompact(BatchCompact memory data, Mandate memory mandate) + internal + view + returns (bytes32 compactHash) + { bytes32 mandateHash = _hashMandate(mandate); + console.log('__________TEST_VALUES________'); + console.log('compactWitnessTypeHash'); + console.logBytes32(keccak256(bytes(batchCompactWitnessTypeString))); + console.log('-arbiter-'); + console.logAddress(data.arbiter); + console.log('-sponsor-'); + console.logAddress(data.sponsor); + console.log('-nonce-'); + console.logUint(data.nonce); + console.log('-expires-'); + console.logUint(data.expires); + console.log('-commitments-'); + console.logBytes32(_hashCommitments(data.commitments)); + console.log('-mandateHash-'); + console.logBytes32(mandateHash); + console.log('________TEST_VALUES_END______'); compactHash = keccak256( abi.encode( - keccak256(bytes(compactWitnessTypeString)), + keccak256(bytes(batchCompactWitnessTypeString)), data.arbiter, data.sponsor, data.nonce, data.expires, - data.lockTag, - data.token, - data.amount, + _hashCommitments(data.commitments), mandateHash ) ); @@ -146,8 +174,18 @@ abstract contract CreateHash is MocksSetup { ); } + function _hashCommitments(Lock[] memory commitments) internal pure returns (bytes32) { + bytes32[] memory hashes = new bytes32[](commitments.length); + for (uint256 i = 0; i < commitments.length; i++) { + hashes[i] = keccak256( + abi.encode(LOCK_TYPEHASH, commitments[i].lockTag, commitments[i].token, commitments[i].amount) + ); + } + return keccak256(abi.encodePacked(hashes)); + } + function _getTypeHash() internal view returns (bytes32) { - return keccak256(bytes(compactWitnessTypeString)); + return keccak256(bytes(batchCompactWitnessTypeString)); } function _domainSeparator(address verifyingContract) internal view returns (bytes32) { @@ -167,7 +205,7 @@ abstract contract CreateHash is MocksSetup { return abi.encodePacked(r, s, v); } - function _hashAndSign(Compact memory data, Mandate memory mandate, address verifyingContract, uint256 signerPK) + function _hashAndSign(BatchCompact memory data, Mandate memory mandate, address verifyingContract, uint256 signerPK) internal view returns (bytes memory) @@ -179,35 +217,34 @@ abstract contract CreateHash is MocksSetup { } abstract contract CompactData is CreateHash { - Compact private compact; + BatchCompact private compact; Mandate private mandate; function setUp() public virtual override { super.setUp(); - compact = Compact({ - arbiter: arbiter, - sponsor: user, - nonce: defaultNonce, - expires: _getClaimExpiration(), - lockTag: usdcLockTag, - token: address(usdc), - amount: defaultAmount - }); + defaultIdsAndAmounts[0][0] = usdcId; + defaultIdsAndAmounts[0][1] = defaultAmount; - mandate = Mandate({ - recipient: user, - expires: _getFillExpiration(), - token: defaultOutputToken, - minimumAmount: defaultMinimumAmount, - baselinePriorityFee: defaultBaselinePriorityFee, - scalingFactor: defaultScalingFactor, - decayCurve: defaultDecayCurve, - salt: defaultSalt - }); + defaultCommitments.push(Lock({lockTag: usdcLockTag, token: address(usdc), amount: defaultAmount})); + + compact.arbiter = arbiter; + compact.sponsor = user; + compact.nonce = defaultNonce; + compact.expires = _getClaimExpiration(); + compact.commitments = defaultCommitments; + + mandate.recipient = user; + mandate.expires = _getFillExpiration(); + mandate.token = defaultOutputToken; + mandate.minimumAmount = defaultMinimumAmount; + mandate.baselinePriorityFee = defaultBaselinePriorityFee; + mandate.scalingFactor = defaultScalingFactor; + mandate.decayCurve = defaultDecayCurve; + mandate.salt = defaultSalt; } - function _getCompact() internal returns (Compact memory) { + function _getCompact() internal returns (BatchCompact memory) { compact.expires = _getClaimExpiration(); return compact; } @@ -232,23 +269,21 @@ abstract contract GaslessCrossChainOrderData is CompactData { function setUp() public virtual override { super.setUp(); - Compact memory compact_ = _getCompact(); + BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - gaslessCrossChainOrder = IOriginSettler.GaslessCrossChainOrder({ - originSettler: address(erc7683Allocator), - user: compact_.sponsor, - nonce: compact_.nonce, - originChainId: block.chainid, - openDeadline: uint32(_getClaimExpiration()), - fillDeadline: uint32(_getFillExpiration()), - orderDataType: erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH(), - orderData: abi.encode( - IERC7683Allocator.OrderDataGasless({ - arbiter: compact_.arbiter, - lockTag: compact_.lockTag, - inputToken: compact_.token, - amount: compact_.amount, + gaslessCrossChainOrder.originSettler = address(erc7683Allocator); + gaslessCrossChainOrder.user = compact_.sponsor; + gaslessCrossChainOrder.nonce = compact_.nonce; + gaslessCrossChainOrder.originChainId = block.chainid; + gaslessCrossChainOrder.openDeadline = uint32(_getClaimExpiration()); + gaslessCrossChainOrder.fillDeadline = uint32(_getFillExpiration()); + gaslessCrossChainOrder.orderDataType = erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH(); + gaslessCrossChainOrder.orderData = abi.encode( + IERC7683Allocator.OrderDataGasless({ + arbiter: compact_.arbiter, + order: IERC7683Allocator.Order({ + commitments: compact_.commitments, chainId: defaultOutputChainId, tribunal: tribunal, recipient: mandate_.recipient, @@ -259,13 +294,13 @@ abstract contract GaslessCrossChainOrderData is CompactData { decayCurve: mandate_.decayCurve, salt: mandate_.salt }) - ) - }); + }) + ); } function _getGaslessCrossChainOrder( address allocator, - Compact memory compact_, + BatchCompact memory compact_, Mandate memory mandate_, uint256 chainId_, bytes32 orderDataGaslessTypeHash_, @@ -283,18 +318,18 @@ abstract contract GaslessCrossChainOrderData is CompactData { orderData: abi.encode( IERC7683Allocator.OrderDataGasless({ arbiter: compact_.arbiter, - lockTag: compact_.lockTag, - inputToken: compact_.token, - amount: compact_.amount, - chainId: defaultOutputChainId, - tribunal: tribunal, - recipient: mandate_.recipient, - settlementToken: mandate_.token, - minimumAmount: mandate_.minimumAmount, - baselinePriorityFee: mandate_.baselinePriorityFee, - scalingFactor: mandate_.scalingFactor, - decayCurve: mandate_.decayCurve, - salt: mandate_.salt + order: IERC7683Allocator.Order({ + commitments: compact_.commitments, + chainId: defaultOutputChainId, + tribunal: tribunal, + recipient: mandate_.recipient, + settlementToken: mandate_.token, + minimumAmount: mandate_.minimumAmount, + baselinePriorityFee: mandate_.baselinePriorityFee, + scalingFactor: mandate_.scalingFactor, + decayCurve: mandate_.decayCurve, + salt: mandate_.salt + }) }) ) }); @@ -318,21 +353,17 @@ abstract contract OnChainCrossChainOrderData is CompactData { function setUp() public virtual override { super.setUp(); - Compact memory compact_ = _getCompact(); + BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - onchainCrossChainOrder = IOriginSettler.OnchainCrossChainOrder({ - fillDeadline: uint32(_getFillExpiration()), - orderDataType: erc7683Allocator.ORDERDATA_TYPEHASH(), - orderData: abi.encode( - IERC7683Allocator.OrderData({ - arbiter: compact_.arbiter, - sponsor: compact_.sponsor, - nonce: compact_.nonce, - expires: compact_.expires, - lockTag: compact_.lockTag, - inputToken: compact_.token, - amount: compact_.amount, + onchainCrossChainOrder.fillDeadline = uint32(_getFillExpiration()); + onchainCrossChainOrder.orderDataType = erc7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH(); + onchainCrossChainOrder.orderData = abi.encode( + IERC7683Allocator.OrderDataOnChain({ + arbiter: compact_.arbiter, + expires: compact_.expires, + order: IERC7683Allocator.Order({ + commitments: compact_.commitments, chainId: defaultOutputChainId, tribunal: tribunal, recipient: mandate_.recipient, @@ -341,19 +372,19 @@ abstract contract OnChainCrossChainOrderData is CompactData { baselinePriorityFee: mandate_.baselinePriorityFee, scalingFactor: mandate_.scalingFactor, decayCurve: mandate_.decayCurve, - salt: mandate_.salt, - targetBlock: defaultTargetBlock, - maximumBlocksAfterTarget: defaultMaximumBlocksAfterTarget - }) - ) - }); + salt: mandate_.salt + }), + targetBlock: defaultTargetBlock, + maximumBlocksAfterTarget: defaultMaximumBlocksAfterTarget + }) + ); } function _getOnChainCrossChainOrder() internal view returns (IOriginSettler.OnchainCrossChainOrder memory) { return onchainCrossChainOrder; } - function _getOnChainCrossChainOrder(Compact memory compact_, Mandate memory mandate_, bytes32 orderDataType_) + function _getOnChainCrossChainOrder(BatchCompact memory compact_, Mandate memory mandate_, bytes32 orderDataType_) internal view returns (IOriginSettler.OnchainCrossChainOrder memory) @@ -362,23 +393,21 @@ abstract contract OnChainCrossChainOrderData is CompactData { fillDeadline: uint32(mandate_.expires), orderDataType: orderDataType_, orderData: abi.encode( - IERC7683Allocator.OrderData({ + IERC7683Allocator.OrderDataOnChain({ arbiter: compact_.arbiter, - sponsor: compact_.sponsor, - nonce: compact_.nonce, expires: compact_.expires, - lockTag: compact_.lockTag, - inputToken: compact_.token, - amount: compact_.amount, - chainId: defaultOutputChainId, - tribunal: tribunal, - recipient: mandate_.recipient, - settlementToken: mandate_.token, - minimumAmount: mandate_.minimumAmount, - baselinePriorityFee: mandate_.baselinePriorityFee, - scalingFactor: mandate_.scalingFactor, - decayCurve: mandate_.decayCurve, - salt: mandate_.salt, + order: IERC7683Allocator.Order({ + commitments: compact_.commitments, + chainId: defaultOutputChainId, + tribunal: tribunal, + recipient: mandate_.recipient, + settlementToken: mandate_.token, + minimumAmount: mandate_.minimumAmount, + baselinePriorityFee: mandate_.baselinePriorityFee, + scalingFactor: mandate_.scalingFactor, + decayCurve: mandate_.decayCurve, + salt: mandate_.salt + }), targetBlock: defaultTargetBlock, maximumBlocksAfterTarget: defaultMaximumBlocksAfterTarget }) @@ -414,18 +443,14 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { abi.encodeWithSelector( IERC7683Allocator.InvalidOrderDataType.selector, falseOrderDataType, - erc7683Allocator.ORDERDATA_TYPEHASH() + erc7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH() ) ); erc7683Allocator.open(onChainCrossChainOrder_); } - function test_revert_InvalidSponsor() public { - IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_ = _getOnChainCrossChainOrder(); - - vm.prank(attacker); - vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidCaller.selector, attacker, user)); - erc7683Allocator.open(onChainCrossChainOrder_); + function test_orderDataType() public view { + assertEq(erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH(), ORDERDATA_GASLESS_TYPEHASH); } function test_revert_InvalidRegistration() public { @@ -441,12 +466,12 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); - Compact memory compact_ = _getCompact(); + BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); bytes32 claimHash = _hashCompact(compact_, mandate_); vm.prank(user); - vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidRegistration.selector, user, claimHash)); + vm.expectRevert(abi.encodeWithSelector(IOnChainAllocator.InvalidRegistration.selector, user, claimHash)); erc7683Allocator.open(onChainCrossChainOrder_); } @@ -458,7 +483,7 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim - Compact memory compact_ = _getCompact(); + BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); bytes32 claimHash = _hashCompact(compact_, mandate_); @@ -531,6 +556,10 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); } + function test_orderDataType() public view { + assertEq(erc7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH(), ORDERDATA_ONCHAIN_TYPEHASH); + } + function test_revert_InvalidDecoding() public { // Decoding fails because of additional data vm.prank(user); @@ -563,11 +592,12 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); } - function test_revert_InvalidNonce() public { - // Nonce is invalid because the least significant 160 bits are not the sponsor address - Compact memory compact_ = _getCompact(); - compact_.nonce = uint256(bytes32(abi.encodePacked(uint96(1), attacker))); - vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidNonce.selector, compact_.nonce)); + function test_revert_InvalidNonce(uint256 nonce) public { + vm.assume(nonce != defaultNonce); + + BatchCompact memory compact_ = _getCompact(); + compact_.nonce = nonce; + vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidNonce.selector, compact_.nonce, defaultNonce)); (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = _getGaslessCrossChainOrder( address(erc7683Allocator), @@ -604,7 +634,7 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { attackerPK ); vm.prank(user); - vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidSignature.selector, user, attacker)); + vm.expectRevert(abi.encodeWithSelector(IOnChainAllocator.InvalidSignature.selector, user, attacker)); erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); } @@ -714,7 +744,8 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); } - function test_revert_NonceAlreadyInUse() public { + function test_revert_NonceAlreadyInUse(uint256 nonce) public { + vm.assume(nonce != defaultNonce); // Deposit tokens vm.startPrank(user); usdc.mint(user, defaultAmount); @@ -722,18 +753,13 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); vm.stopPrank(); - // use the nonce once - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = + // try to use a future nonce + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder, bytes memory sponsorSignature) = _getGaslessCrossChainOrder(); + gaslessCrossChainOrder.nonce = nonce; vm.prank(user); - erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); - - // try to use the nonce again - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder2, bytes memory sponsorSignature2) = - _getGaslessCrossChainOrder(); - vm.prank(user); - vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.NonceAlreadyInUse.selector, defaultNonce)); - erc7683Allocator.openFor(gaslessCrossChainOrder2, sponsorSignature2, ''); + vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidNonce.selector, nonce, defaultNonce)); + erc7683Allocator.openFor(gaslessCrossChainOrder, sponsorSignature, ''); } } @@ -860,7 +886,7 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Gasles compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim - Compact memory compact_ = _getCompact(); + BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); bytes32 claimHash = _hashCompact(compact_, mandate_); @@ -879,23 +905,23 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Gasles Component memory component = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); Component[] memory components = new Component[](1); components[0] = component; - Claim memory claim = Claim({ - allocatorData: abi.encode( - erc7683Allocator.QUALIFICATION_TYPEHASH(), defaultTargetBlock, defaultMaximumBlocksAfterTarget - ), + BatchClaimComponent memory batchClaimComponent = + BatchClaimComponent({id: usdcId, allocatedAmount: defaultAmount, portions: components}); + BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); + batchClaimComponents[0] = batchClaimComponent; + BatchClaim memory claim = BatchClaim({ + allocatorData: abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget), sponsorSignature: '', sponsor: user, nonce: defaultNonce, expires: compact_.expires, witness: keccak256(abi.encode(keccak256(bytes(mandateTypeString)), mandate_)), witnessTypestring: witnessTypeString, - id: usdcId, - allocatedAmount: defaultAmount, - claimants: components + claims: batchClaimComponents }); vm.prank(arbiter); vm.expectRevert(abi.encodeWithSelector(0x8baa579f)); // check for the InvalidSignature() error in the Compact contract - compactContract.claim(claim); + compactContract.batchClaim(claim); vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); vm.assertEq(compactContract.balanceOf(filler, usdcId), 0); @@ -909,7 +935,7 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Gasles compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim - Compact memory compact_ = _getCompact(); + BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); bytes32 claimHash = _hashCompact(compact_, mandate_); @@ -943,20 +969,22 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Gasles ); Component[] memory components = new Component[](1); components[0] = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); - Claim memory claim = Claim({ - allocatorData: abi.encode(defaultTargetBlock, defaultMaximumBlocksAfterTarget), + BatchClaimComponent memory batchClaimComponent = + BatchClaimComponent({id: usdcId, allocatedAmount: defaultAmount, portions: components}); + BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); + batchClaimComponents[0] = batchClaimComponent; + BatchClaim memory claim = BatchClaim({ + allocatorData: abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget), sponsorSignature: '', sponsor: user, nonce: defaultNonce, expires: compact_.expires, witness: witness, witnessTypestring: witnessTypeString, - id: usdcId, - allocatedAmount: defaultAmount, - claimants: components + claims: batchClaimComponents }); vm.prank(arbiter); - compactContract.claim(claim); + compactContract.batchClaim(claim); vm.assertEq(compactContract.balanceOf(user, usdcId), 0); vm.assertEq(usdc.balanceOf(filler), defaultAmount); @@ -970,7 +998,7 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Gasles compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim - Compact memory compact_ = _getCompact(); + BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); bytes32 claimHash = _hashCompact(compact_, mandate_); @@ -990,20 +1018,22 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Gasles // claim should be successful Component[] memory components = new Component[](1); components[0] = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); - Claim memory claim = Claim({ - allocatorData: abi.encode(uint256(0), uint256(0)), + BatchClaimComponent memory batchClaimComponent = + BatchClaimComponent({id: usdcId, allocatedAmount: defaultAmount, portions: components}); + BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); + batchClaimComponents[0] = batchClaimComponent; + BatchClaim memory claim = BatchClaim({ + allocatorData: abi.encodePacked(uint200(0), uint56(0)), sponsorSignature: '', sponsor: compact_.sponsor, nonce: compact_.nonce, expires: compact_.expires, witness: _hashMandate(mandate_), witnessTypestring: witnessTypeString, - id: uint256(bytes32(compact_.lockTag) | bytes32(uint256(uint160(compact_.token)))), - allocatedAmount: compact_.amount, - claimants: components + claims: batchClaimComponents }); vm.prank(arbiter); - compactContract.claim(claim); + compactContract.batchClaim(claim); vm.assertEq(compactContract.balanceOf(user, usdcId), 0); vm.assertEq(usdc.balanceOf(filler), defaultAmount); @@ -1128,6 +1158,7 @@ contract ERC7683Allocator_resolve is OnChainCrossChainOrderData { minReceived: minReceived, fillInstructions: fillInstructions }); + vm.prank(user); IOriginSettler.ResolvedCrossChainOrder memory resolved = erc7683Allocator.resolve(onChainCrossChainOrder_); assertEq(resolved.user, resolvedCrossChainOrder.user); assertEq(resolved.originChainId, resolvedCrossChainOrder.originChainId); @@ -1161,33 +1192,24 @@ contract ERC7683Allocator_getCompactWitnessTypeString is MocksSetup { function test_getCompactWitnessTypeString() public view { assertEq( erc7683Allocator.getCompactWitnessTypeString(), - 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,bytes12 lockTag,address token,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)' + 'BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments,Mandate mandate)Lock(bytes12 lockTag,address token,uint256 amount)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)' ); } } contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData { - function test_revert_invalidNonce(uint256 nonce_) public { - address expectedSponsor; - assembly ("memory-safe") { - expectedSponsor := shr(96, nonce_) - } - vm.assume(user != expectedSponsor); + function test_invalidNonce(uint256 nonce_, address otherUser) public view { + vm.assume(nonce_ != defaultNonce); - vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidNonce.selector, nonce_)); - erc7683Allocator.checkNonce(nonce_, user); + assertFalse(erc7683Allocator.checkNonce(nonce_, otherUser)); } - function test_checkNonce_unused(uint96 nonce_) public view { - address sponsor = user; - uint256 nonce; - assembly ("memory-safe") { - nonce := or(shl(96, sponsor), shr(160, shl(160, nonce_))) - } - assertEq(erc7683Allocator.checkNonce(nonce, user), true); + function test_nextFreeNonce(address otherUser) public view { + assertTrue(erc7683Allocator.checkNonce(defaultNonce, otherUser)); } - function test_checkNonce_used() public { + function test_usedNonce(address otherUser) public { + vm.assume(otherUser != user); // Deposit tokens vm.startPrank(user); usdc.mint(user, defaultAmount); @@ -1195,7 +1217,7 @@ contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData { compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim - Compact memory compact_ = _getCompact(); + BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); bytes32 claimHash = _hashCompact(compact_, mandate_); @@ -1205,34 +1227,8 @@ contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData { (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); erc7683Allocator.open(onChainCrossChainOrder_); - vm.assertEq(erc7683Allocator.checkNonce(defaultNonce, user), false); - vm.stopPrank(); - } - - function test_checkNonce_fuzz(uint8 nonce_) public { - uint256 nonce = uint256(bytes32(abi.encodePacked(user, uint96(nonce_)))); - - bool sameNonce = nonce == defaultNonce; - - // Deposit tokens - vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); - compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); - - // register a claim - Compact memory compact_ = _getCompact(); - Mandate memory mandate_ = _getMandate(); - - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); - - (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); - erc7683Allocator.open(onChainCrossChainOrder_); - - vm.assertEq(erc7683Allocator.checkNonce(nonce, user), !sameNonce); - + vm.assertFalse(erc7683Allocator.checkNonce(defaultNonce, user)); + vm.assertTrue(erc7683Allocator.checkNonce(defaultNonce, otherUser)); vm.stopPrank(); } } From 7802e1f4d9e6f33ab0e1b77319df7d086e815776 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Tue, 29 Jul 2025 14:51:52 +0200 Subject: [PATCH 39/63] moved arbiter into Order struct --- snapshots/ERC7683Allocator_open.json | 3 + snapshots/ERC7683Allocator_openFor.json | 4 ++ src/allocators/ERC7683Allocator.sol | 85 +++++++------------------ src/interfaces/IERC7683Allocator.sol | 40 +++++------- test/ERC7683Allocator.t.sol | 29 +++------ 5 files changed, 52 insertions(+), 109 deletions(-) create mode 100644 snapshots/ERC7683Allocator_open.json create mode 100644 snapshots/ERC7683Allocator_openFor.json diff --git a/snapshots/ERC7683Allocator_open.json b/snapshots/ERC7683Allocator_open.json new file mode 100644 index 0000000..a560c22 --- /dev/null +++ b/snapshots/ERC7683Allocator_open.json @@ -0,0 +1,3 @@ +{ + "open_simpleOrder": "184302" +} diff --git a/snapshots/ERC7683Allocator_openFor.json b/snapshots/ERC7683Allocator_openFor.json new file mode 100644 index 0000000..ce3ef96 --- /dev/null +++ b/snapshots/ERC7683Allocator_openFor.json @@ -0,0 +1,4 @@ +{ + "openFor_simpleOrder_relayed": "168020", + "openFor_simpleOrder_userHimself": "168020" +} diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index f7bfeb0..5f54380 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -63,32 +63,15 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { } // Decode the orderData - (address arbiter,, Order calldata orderData,,) = _decodeOrderData(order_.orderData, 0x40); + (, Order calldata orderData,,) = _decodeOrderData(order_.orderData, false); ResolvedCrossChainOrder memory resolvedOrder = _resolveOrder( - arbiter, - order_.user, - order_.nonce, - order_.openDeadline, - order_.fillDeadline, - orderData, - sponsorSignature_, - 0, - 0 + order_.user, order_.nonce, order_.openDeadline, order_.fillDeadline, orderData, sponsorSignature_, 0, 0 ); bytes32 mandateHash = _mandateHash(orderData, order_.fillDeadline); - _open( - arbiter, - order_.user, - order_.openDeadline, - orderData, - sponsorSignature_, - mandateHash, - bytes32(0), - resolvedOrder - ); + _open(order_.user, order_.openDeadline, orderData, sponsorSignature_, mandateHash, bytes32(0), resolvedOrder); } /// @inheritdoc IOriginSettler @@ -99,18 +82,12 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { } // Decode the orderData - ( - address arbiter, - uint32 expires, - Order calldata orderData, - uint200 targetBlock, - uint56 maximumBlocksAfterTarget - ) = _decodeOrderData(order.orderData, 0x60); + (uint32 expires, Order calldata orderData, uint200 targetBlock, uint56 maximumBlocksAfterTarget) = + _decodeOrderData(order.orderData, true); bytes32 mandateHash = _mandateHash(orderData, order.fillDeadline); ResolvedCrossChainOrder memory resolvedOrder = _resolveOrder( - arbiter, msg.sender, nonces[msg.sender] + 1, expires, @@ -122,7 +99,6 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { ); _open( - arbiter, msg.sender, expires, orderData, @@ -153,10 +129,9 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { } // Decode the orderData - (address arbiter,, Order calldata orderData,,) = _decodeOrderData(order_.orderData, 0x40); + (, Order calldata orderData,,) = _decodeOrderData(order_.orderData, false); return _resolveOrder( - arbiter, order_.user, order_.nonce, order_.openDeadline, @@ -176,16 +151,10 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { } // Decode the orderData - ( - address arbiter, - uint32 expires, - Order calldata orderData, - uint200 targetBlock, - uint56 maximumBlocksAfterTarget - ) = _decodeOrderData(order.orderData, 0x60); + (uint32 expires, Order calldata orderData, uint200 targetBlock, uint56 maximumBlocksAfterTarget) = + _decodeOrderData(order.orderData, true); return _resolveOrder( - arbiter, msg.sender, nonces[msg.sender] + 1, expires, @@ -254,7 +223,6 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { } function _open( - address arbiter, address sponsor, uint32 expires, Order calldata orderData, @@ -267,7 +235,7 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { (bytes32 claimHash, uint256 nonce) = allocateFor( sponsor, orderData.commitments, - arbiter, + orderData.arbiter, expires, BATCH_COMPACT_WITNESS_TYPEHASH, mandateHash_, @@ -280,41 +248,33 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { emit Open(bytes32(nonce), resolvedOrder_); } - function _decodeOrderData(bytes calldata orderData, uint256 offset) + function _decodeOrderData(bytes calldata orderData, bool onChain) internal pure - returns ( - address arbiter, - uint32 expires, - Order calldata order, - uint200 targetBlock, - uint56 maximumBlocksAfterTarget - ) + returns (uint32 expires, Order calldata order, uint200 targetBlock, uint56 maximumBlocksAfterTarget) { // orderData includes the OrderData(OnChain/Gasless) struct, and the nested Order struct. // 0x00: OrderDataOnChain.offset - // 0x20: OrderDataOnChain.arbiter + // 0x20: OrderDataOnChain.order.offset // 0x40: OrderDataOnChain.expires - // 0x60: OrderDataOnChain.order.offset + // 0x60: OrderDataOnChain.targetBlock + // 0x80: OrderDataOnChain.maximumBlocksAfterTarget // 0x00: OrderDataGasless.offset - // 0x20: OrderDataGasless.arbiter - // 0x40: OrderDataGasless.order.offset + // 0x20: OrderDataGasless.order.offset assembly ("memory-safe") { let l := sub(orderData.length, 0x20) - let s := calldataload(add(orderData.offset, offset)) // Relative offset of `orderBytes` from `orderData.offset` and the `OrderData...` struct. + let s := calldataload(add(orderData.offset, 0x20)) // Relative offset of `orderBytes` from `orderData.offset` and the `OrderData...` struct. order := add(orderData.offset, add(s, 0x20)) // Add 0x20 since the OrderStruct is within the `OrderData...` struct - if or(shr(64, or(s, or(l, orderData.offset))), gt(offset, l)) { revert(l, 0x00) } + if shr(64, or(s, or(l, orderData.offset))) { revert(l, 0x00) } - let isOnChain := eq(offset, 0x60) - arbiter := calldataload(add(orderData.offset, 0x20)) - expires := mul(calldataload(add(orderData.offset, 0x40)), isOnChain) - targetBlock := mul(calldataload(0x124), isOnChain) - maximumBlocksAfterTarget := mul(calldataload(0x144), isOnChain) + expires := mul(calldataload(add(orderData.offset, 0x40)), onChain) + targetBlock := mul(calldataload(add(orderData.offset, 0x60)), onChain) + maximumBlocksAfterTarget := mul(calldataload(add(orderData.offset, 0x80)), onChain) } - return (arbiter, expires, order, targetBlock, maximumBlocksAfterTarget); + return (expires, order, targetBlock, maximumBlocksAfterTarget); } /// @dev Returns a slice representing a dynamic struct in the calldata. Performs bounds checks. @@ -330,7 +290,6 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { } function _resolveOrder( - address arbiter, address sponsor, uint256 nonce, uint32 expires, @@ -352,7 +311,7 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { }); BatchCompact memory compact = BatchCompact({ - arbiter: arbiter, + arbiter: orderData.arbiter, sponsor: sponsor, nonce: nonce, expires: expires, diff --git a/src/interfaces/IERC7683Allocator.sol b/src/interfaces/IERC7683Allocator.sol index 2ed0551..c901df5 100644 --- a/src/interfaces/IERC7683Allocator.sol +++ b/src/interfaces/IERC7683Allocator.sol @@ -8,40 +8,30 @@ import {Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; interface IERC7683Allocator is IOriginSettler, IAllocator { struct OrderDataOnChain { - // BATCH_COMPACT - address arbiter; // The account tasked with verifying and submitting the claim. - // address sponsor; // The account to source the tokens from. - // uint256 nonce; // A parameter to enforce replay protection, scoped to allocator. - uint256 expires; // The time at which the claim expires. Order order; // The remaining BatchCompact and Mandate data - // ADDITIONAL INPUT - uint200 targetBlock; // The block number at the target chain on which the PGA is executed / the reverse dutch auction starts. - uint56 maximumBlocksAfterTarget; // Blocks after target block that are still fillable. + uint256 expires; // COMPACT - The time at which the claim expires and the user is able to withdraw their funds. + uint200 targetBlock; // ADDITIONAL INPUT - The block number at the target chain on which the PGA is executed / the reverse dutch auction starts. + uint56 maximumBlocksAfterTarget; // ADDITIONAL INPUT - Blocks after target block that are still fillable. } struct OrderDataGasless { - // BATCH_COMPACT - address arbiter; // The account tasked with verifying and submitting the claim. - // address sponsor; // The account to source the tokens from. - // uint256 nonce; // A parameter to enforce replay protection, scoped to allocator. - // uint256 expires; // The time at which the claim expires. Order order; // The remaining BatchCompact and Mandate data } /// @dev The data that OnChain and Gasless orders have in common struct Order { - Lock[] commitments; // The token IDs and amounts to allocate. - // MANDATE - uint256 chainId; // (implicit arg, included in EIP712 payload) - address tribunal; // (implicit arg, included in EIP712 payload) - address recipient; // Recipient of settled tokens - // uint256 expires; // Mandate expiration timestamp - address settlementToken; // Settlement token (address(0) for native) - uint256 minimumAmount; // Minimum settlement amount - uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in - uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline) - uint256[] decayCurve; // Block durations, fill increases, & claim decreases. - bytes32 salt; // Replay protection parameter + address arbiter; // COMPACT - The account tasked with verifying and submitting the claim. + Lock[] commitments; // COMPACT - The token IDs and amounts to allocate. + uint256 chainId; // MANDATE - (implicit arg, included in EIP712 payload) + address tribunal; // MANDATE - (implicit arg, included in EIP712 payload) + address recipient; // MANDATE - Recipient of settled tokens + // uint256 expires; // MANDATE - Mandate expiration timestamp, which equals the fill deadline + address settlementToken; // MANDATE - Settlement token (address(0) for native) + uint256 minimumAmount; // MANDATE - Minimum settlement amount + uint256 baselinePriorityFee; // MANDATE - Base fee threshold where scaling kicks in + uint256 scalingFactor; // MANDATE - Fee scaling multiplier (1e18 baseline) + uint256[] decayCurve; // MANDATE - Block durations, fill increases, & claim decreases. + bytes32 salt; // MANDATE - Replay protection parameter } error InvalidOriginSettler(address originSettler, address expectedOriginSettler); diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index 5bbbeac..57fb8e8 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -15,7 +15,7 @@ import {BatchCompact, COMPACT_TYPEHASH, LOCK_TYPEHASH, Lock} from '@uniswap/the- import {ForcedWithdrawalStatus} from '@uniswap/the-compact/types/ForcedWithdrawalStatus.sol'; import {ResetPeriod} from '@uniswap/the-compact/types/ResetPeriod.sol'; import {Scope} from '@uniswap/the-compact/types/Scope.sol'; -import {Test, console} from 'forge-std/Test.sol'; +import {Test} from 'forge-std/Test.sol'; import {TestHelper} from 'test/util/TestHelper.sol'; @@ -127,22 +127,6 @@ abstract contract CreateHash is MocksSetup { returns (bytes32 compactHash) { bytes32 mandateHash = _hashMandate(mandate); - console.log('__________TEST_VALUES________'); - console.log('compactWitnessTypeHash'); - console.logBytes32(keccak256(bytes(batchCompactWitnessTypeString))); - console.log('-arbiter-'); - console.logAddress(data.arbiter); - console.log('-sponsor-'); - console.logAddress(data.sponsor); - console.log('-nonce-'); - console.logUint(data.nonce); - console.log('-expires-'); - console.logUint(data.expires); - console.log('-commitments-'); - console.logBytes32(_hashCommitments(data.commitments)); - console.log('-mandateHash-'); - console.logBytes32(mandateHash); - console.log('________TEST_VALUES_END______'); compactHash = keccak256( abi.encode( keccak256(bytes(batchCompactWitnessTypeString)), @@ -281,8 +265,8 @@ abstract contract GaslessCrossChainOrderData is CompactData { gaslessCrossChainOrder.orderDataType = erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH(); gaslessCrossChainOrder.orderData = abi.encode( IERC7683Allocator.OrderDataGasless({ - arbiter: compact_.arbiter, order: IERC7683Allocator.Order({ + arbiter: compact_.arbiter, commitments: compact_.commitments, chainId: defaultOutputChainId, tribunal: tribunal, @@ -317,8 +301,8 @@ abstract contract GaslessCrossChainOrderData is CompactData { orderDataType: orderDataGaslessTypeHash_, orderData: abi.encode( IERC7683Allocator.OrderDataGasless({ - arbiter: compact_.arbiter, order: IERC7683Allocator.Order({ + arbiter: compact_.arbiter, commitments: compact_.commitments, chainId: defaultOutputChainId, tribunal: tribunal, @@ -360,9 +344,9 @@ abstract contract OnChainCrossChainOrderData is CompactData { onchainCrossChainOrder.orderDataType = erc7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH(); onchainCrossChainOrder.orderData = abi.encode( IERC7683Allocator.OrderDataOnChain({ - arbiter: compact_.arbiter, expires: compact_.expires, order: IERC7683Allocator.Order({ + arbiter: compact_.arbiter, commitments: compact_.commitments, chainId: defaultOutputChainId, tribunal: tribunal, @@ -394,9 +378,9 @@ abstract contract OnChainCrossChainOrderData is CompactData { orderDataType: orderDataType_, orderData: abi.encode( IERC7683Allocator.OrderDataOnChain({ - arbiter: compact_.arbiter, expires: compact_.expires, order: IERC7683Allocator.Order({ + arbiter: compact_.arbiter, commitments: compact_.commitments, chainId: defaultOutputChainId, tribunal: tribunal, @@ -534,6 +518,7 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { vm.expectEmit(true, false, false, true, address(erc7683Allocator)); emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); erc7683Allocator.open(onChainCrossChainOrder_); + vm.snapshotGasLastCall('open_simpleOrder'); } } @@ -689,6 +674,7 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { vm.expectEmit(true, false, false, true, address(erc7683Allocator)); emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); + vm.snapshotGasLastCall('openFor_simpleOrder_userHimself'); } function test_successful_relayed() public { @@ -742,6 +728,7 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { vm.expectEmit(true, false, false, true, address(erc7683Allocator)); emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); + vm.snapshotGasLastCall('openFor_simpleOrder_relayed'); } function test_revert_NonceAlreadyInUse(uint256 nonce) public { From 329fe07af026e0bd28f53372a60eef97a3287254 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Wed, 30 Jul 2025 14:54:31 +0200 Subject: [PATCH 40/63] ERC7683Allocator full coverage --- snapshots/ERC7683Allocator_open.json | 2 +- snapshots/ERC7683Allocator_openFor.json | 4 +- src/allocators/ERC7683Allocator.sol | 27 +- test/ERC7683Allocator.t.sol | 370 +++++++++++++++++++++++- 4 files changed, 371 insertions(+), 32 deletions(-) diff --git a/snapshots/ERC7683Allocator_open.json b/snapshots/ERC7683Allocator_open.json index a560c22..0aee4ca 100644 --- a/snapshots/ERC7683Allocator_open.json +++ b/snapshots/ERC7683Allocator_open.json @@ -1,3 +1,3 @@ { - "open_simpleOrder": "184302" + "open_simpleOrder": "184296" } diff --git a/snapshots/ERC7683Allocator_openFor.json b/snapshots/ERC7683Allocator_openFor.json index ce3ef96..19a5aa2 100644 --- a/snapshots/ERC7683Allocator_openFor.json +++ b/snapshots/ERC7683Allocator_openFor.json @@ -1,4 +1,4 @@ { - "openFor_simpleOrder_relayed": "168020", - "openFor_simpleOrder_userHimself": "168020" + "openFor_simpleOrder_relayed": "168012", + "openFor_simpleOrder_userHimself": "168012" } diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index 5f54380..b297140 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -8,7 +8,6 @@ import {OnChainAllocator} from './OnChainAllocator.sol'; import {BatchClaim, Mandate} from './types/TribunalStructs.sol'; import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; -import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; import {LibBytes} from '@solady/utils/LibBytes.sol'; import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; import {BatchCompact, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; @@ -37,13 +36,9 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { /// @notice keccak256("Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") bytes32 private constant MANDATE_TYPEHASH = 0x74d9c10530859952346f3e046aa2981a24bb7524b8394eb45a9deddced9d6501; - bytes32 immutable _COMPACT_DOMAIN_SEPARATOR; - mapping(bytes32 claimHash => bytes32 qualification) public qualifications; - constructor(address compactContract_) OnChainAllocator(compactContract_) { - _COMPACT_DOMAIN_SEPARATOR = ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(); - } + constructor(address compactContract_) OnChainAllocator(compactContract_) {} /// @inheritdoc IOriginSettler function openFor(GaslessCrossChainOrder calldata order_, bytes calldata sponsorSignature_, bytes calldata) @@ -212,14 +207,12 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { /// @inheritdoc IERC7683Allocator function checkNonce(uint256 nonce_, address sponsor_) external view returns (bool nonceValid) { - nonceValid = nonces[sponsor_] + 1 == nonce_; - return nonceValid; + return nonces[sponsor_] + 1 == nonce_; } /// @inheritdoc IERC7683Allocator function createFillerData(address claimant_) external pure returns (bytes memory fillerData) { - fillerData = abi.encode(claimant_); - return fillerData; + return abi.encode(claimant_); } function _open( @@ -273,20 +266,6 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { targetBlock := mul(calldataload(add(orderData.offset, 0x60)), onChain) maximumBlocksAfterTarget := mul(calldataload(add(orderData.offset, 0x80)), onChain) } - - return (expires, order, targetBlock, maximumBlocksAfterTarget); - } - - /// @dev Returns a slice representing a dynamic struct in the calldata. Performs bounds checks. - function _dynamicStructInCalldata(bytes calldata a, uint256 offset) internal pure returns (bytes calldata result) { - /// @solidity memory-safe-assembly - assembly { - let l := sub(a.length, 0x20) - let s := calldataload(add(a.offset, offset)) // Relative offset of `result` from `a.offset`. - result.offset := add(a.offset, add(s, 0x20)) // Add 0x20 since the OrderDataStruct is inside another struct - result.length := sub(a.length, add(s, 0x20)) - if or(shr(64, or(s, or(l, a.offset))), gt(offset, l)) { revert(l, 0x00) } - } } function _resolveOrder( diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index 57fb8e8..fc42a57 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -437,6 +437,48 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { assertEq(erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH(), ORDERDATA_GASLESS_TYPEHASH); } + function test_revert_ManipulatedOrderData() public { + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + + // register a claim + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 typeHash = _getTypeHash(); + compactContract.register(claimHash, typeHash); + + vm.stopPrank(); + + (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); + + // Manipulate the order data + uint256 outOfBounds = type(uint256).max; // uint256(type(uint64).max) + 1; + + bytes memory callData = abi.encodeWithSelector(IOriginSettler.open.selector, onChainCrossChainOrder_); + // 0x00 selector + // 0x24 OnchainCrossChainOrder.offset + // 0x44 OnchainCrossChainOrder.fillDeadline + // 0x64 OnchainCrossChainOrder.orderDataType + // 0x84 OnchainCrossChainOrder.orderData.offset + // 0xa4 OnchainCrossChainOrder.orderData.length + // 0xc4 OnchainCrossChainOrder.OrderDataOnChain.offset + // 0xe4 OnchainCrossChainOrder.OrderDataOnChain.Order.offset + + assembly ("memory-safe") { + mstore(add(callData, 0xe4), outOfBounds) + } + + vm.prank(user); + (bool success, bytes memory returnData) = address(erc7683Allocator).call(callData); + assertEq(success, false); + assertEq(returnData.length, 0); + } + function test_revert_InvalidRegistration() public { // we deposit tokens vm.startPrank(user); @@ -860,12 +902,12 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { // } // } -contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, GaslessCrossChainOrderData { +contract ERC7683Allocator_authorizeClaim is OnChainCrossChainOrderData, GaslessCrossChainOrderData { function setUp() public override(OnChainCrossChainOrderData, GaslessCrossChainOrderData) { super.setUp(); } - function test_revert_InvalidLock() public { + function test_revert_InvalidSignature() public { // Deposit tokens vm.startPrank(user); usdc.mint(user, defaultAmount); @@ -888,7 +930,7 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Gasles // we do NOT open the order or lock the tokens - // claim should be fail, because we mess with the nonce + // claim should fail, because we did not open an order Component memory component = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); Component[] memory components = new Component[](1); components[0] = component; @@ -914,7 +956,267 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Gasles vm.assertEq(compactContract.balanceOf(filler, usdcId), 0); } - function test_isValidSignature_successful_open() public { + function test_revert_InvalidAllocatorData() public { + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + + // register a claim + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 typeHash = _getTypeHash(); + compactContract.register(claimHash, typeHash); + + address filler = makeAddr('filler'); + vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); + vm.assertEq(usdc.balanceOf(filler), 0); + + // we open the order and lock the tokens + (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); + erc7683Allocator.open(onChainCrossChainOrder_); + vm.stopPrank(); + + // claim should be successful + bytes32 witness = keccak256( + abi.encode( + keccak256(bytes(mandateTypeString)), + defaultOutputChainId, + tribunal, + mandate_.recipient, + mandate_.expires, + mandate_.token, + mandate_.minimumAmount, + mandate_.baselinePriorityFee, + mandate_.scalingFactor, + keccak256(abi.encodePacked(mandate_.decayCurve)), + mandate_.salt + ) + ); + Component[] memory components = new Component[](1); + components[0] = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); + BatchClaimComponent memory batchClaimComponent = + BatchClaimComponent({id: usdcId, allocatedAmount: defaultAmount, portions: components}); + BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); + batchClaimComponents[0] = batchClaimComponent; + BatchClaim memory claim = BatchClaim({ + allocatorData: abi.encodePacked(defaultTargetBlock + 1, defaultMaximumBlocksAfterTarget), + sponsorSignature: '', + sponsor: user, + nonce: defaultNonce, + expires: compact_.expires, + witness: witness, + witnessTypestring: witnessTypeString, + claims: batchClaimComponents + }); + vm.prank(arbiter); + vm.expectRevert( + abi.encodeWithSelector( + IERC7683Allocator.InvalidAllocatorData.selector, + bytes32(claim.allocatorData), + bytes32(abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget)) + ) + ); + compactContract.batchClaim(claim); + } + + function test_successful_open() public { + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + + // register a claim + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 typeHash = _getTypeHash(); + compactContract.register(claimHash, typeHash); + + address filler = makeAddr('filler'); + vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); + vm.assertEq(usdc.balanceOf(filler), 0); + + // we open the order and lock the tokens + (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); + erc7683Allocator.open(onChainCrossChainOrder_); + vm.stopPrank(); + + // claim should be successful + bytes32 witness = keccak256( + abi.encode( + keccak256(bytes(mandateTypeString)), + defaultOutputChainId, + tribunal, + mandate_.recipient, + mandate_.expires, + mandate_.token, + mandate_.minimumAmount, + mandate_.baselinePriorityFee, + mandate_.scalingFactor, + keccak256(abi.encodePacked(mandate_.decayCurve)), + mandate_.salt + ) + ); + Component[] memory components = new Component[](1); + components[0] = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); + BatchClaimComponent memory batchClaimComponent = + BatchClaimComponent({id: usdcId, allocatedAmount: defaultAmount, portions: components}); + BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); + batchClaimComponents[0] = batchClaimComponent; + BatchClaim memory claim = BatchClaim({ + allocatorData: abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget), + sponsorSignature: '', + sponsor: user, + nonce: defaultNonce, + expires: compact_.expires, + witness: witness, + witnessTypestring: witnessTypeString, + claims: batchClaimComponents + }); + vm.prank(arbiter); + compactContract.batchClaim(claim); + + vm.assertEq(compactContract.balanceOf(user, usdcId), 0); + vm.assertEq(usdc.balanceOf(filler), defaultAmount); + } + + function test_successful_openFor() public { + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + + // register a claim + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 typeHash = _getTypeHash(); + compactContract.register(claimHash, typeHash); + + address filler = makeAddr('filler'); + vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); + vm.assertEq(usdc.balanceOf(filler), 0); + + // we open the order and lock the tokens + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = + _getGaslessCrossChainOrder(); + erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); + vm.stopPrank(); + + // claim should be successful + Component[] memory components = new Component[](1); + components[0] = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); + BatchClaimComponent memory batchClaimComponent = + BatchClaimComponent({id: usdcId, allocatedAmount: defaultAmount, portions: components}); + BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); + batchClaimComponents[0] = batchClaimComponent; + BatchClaim memory claim = BatchClaim({ + allocatorData: abi.encodePacked(uint200(0), uint56(0)), + sponsorSignature: '', + sponsor: compact_.sponsor, + nonce: compact_.nonce, + expires: compact_.expires, + witness: _hashMandate(mandate_), + witnessTypestring: witnessTypeString, + claims: batchClaimComponents + }); + vm.prank(arbiter); + compactContract.batchClaim(claim); + + vm.assertEq(compactContract.balanceOf(user, usdcId), 0); + vm.assertEq(usdc.balanceOf(filler), defaultAmount); + } +} + +contract ERC7683Allocator_isClaimAuthorized is OnChainCrossChainOrderData, GaslessCrossChainOrderData { + function setUp() public override(OnChainCrossChainOrderData, GaslessCrossChainOrderData) { + super.setUp(); + } + + function test_failed_noClaimAllocated() public { + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + + // register a claim + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 typeHash = _getTypeHash(); + compactContract.register(claimHash, typeHash); + + address filler = makeAddr('filler'); + vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); + vm.assertEq(compactContract.balanceOf(filler, usdcId), 0); + + vm.stopPrank(); + + // we do NOT open the order or lock the tokens + + // isClaimAuthorized should be false, because we did not allocate the claim + assertFalse( + erc7683Allocator.isClaimAuthorized( + claimHash, + compact_.arbiter, + compact_.sponsor, + compact_.nonce, + compact_.expires, + defaultIdsAndAmounts, + abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget) + ) + ); + } + + function test_failed_invalidAllocatorData() public { + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + + // register a claim + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 typeHash = _getTypeHash(); + compactContract.register(claimHash, typeHash); + + address filler = makeAddr('filler'); + vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); + vm.assertEq(usdc.balanceOf(filler), 0); + + // we open the order and lock the tokens + (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); + erc7683Allocator.open(onChainCrossChainOrder_); + vm.stopPrank(); + + // isClaimAuthorized should be false, because the allocator data is invalid + assertFalse( + erc7683Allocator.isClaimAuthorized( + claimHash, + compact_.arbiter, + compact_.sponsor, + compact_.nonce, + compact_.expires, + defaultIdsAndAmounts, + abi.encodePacked(defaultTargetBlock + 1, defaultMaximumBlocksAfterTarget) // invalid allocator data + ) + ); + } + + function test_successful_open() public { // Deposit tokens vm.startPrank(user); usdc.mint(user, defaultAmount); @@ -977,7 +1279,7 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Gasles vm.assertEq(usdc.balanceOf(filler), defaultAmount); } - function test_isValidSignature_successful_openFor() public { + function test_successful_openFor() public { // Deposit tokens vm.startPrank(user); usdc.mint(user, defaultAmount); @@ -1028,6 +1330,44 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Gasles } contract ERC7683Allocator_resolveFor is GaslessCrossChainOrderData { + function test_revert_InvalidOrderDataType() public { + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = + _getGaslessCrossChainOrder(); + gaslessCrossChainOrder_.orderDataType = keccak256('false'); + vm.expectRevert( + abi.encodeWithSelector( + IERC7683Allocator.InvalidOrderDataType.selector, + gaslessCrossChainOrder_.orderDataType, + erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH() + ) + ); + erc7683Allocator.resolveFor(gaslessCrossChainOrder_, ''); + } + + function test_revert_InvalidOriginSettler() public { + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = + _getGaslessCrossChainOrder(); + gaslessCrossChainOrder_.originSettler = makeAddr('invalid'); + vm.expectRevert( + abi.encodeWithSelector( + IERC7683Allocator.InvalidOriginSettler.selector, + gaslessCrossChainOrder_.originSettler, + address(erc7683Allocator) + ) + ); + erc7683Allocator.resolveFor(gaslessCrossChainOrder_, ''); + } + + function test_revert_InvalidNonce() public { + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = + _getGaslessCrossChainOrder(); + gaslessCrossChainOrder_.nonce = defaultNonce + 1; + vm.expectRevert( + abi.encodeWithSelector(IERC7683Allocator.InvalidNonce.selector, gaslessCrossChainOrder_.nonce, defaultNonce) + ); + erc7683Allocator.resolveFor(gaslessCrossChainOrder_, ''); + } + function test_resolve_successful() public { // WITH THE CURRENT ERC7683 DESIGN, THE SPONSOR SIGNATURE IS NOT PROVIDED TO THE RESOLVE FUNCTION // WHILE THE ResolvedCrossChainOrder WITHOUT THE SIGNATURE COULD STILL BE USED TO SIMULATE THE FILL, @@ -1106,6 +1446,19 @@ contract ERC7683Allocator_resolveFor is GaslessCrossChainOrderData { } contract ERC7683Allocator_resolve is OnChainCrossChainOrderData { + function test_revert_InvalidOrderDataType() public { + (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); + onChainCrossChainOrder_.orderDataType = keccak256('false'); + vm.expectRevert( + abi.encodeWithSelector( + IERC7683Allocator.InvalidOrderDataType.selector, + onChainCrossChainOrder_.orderDataType, + erc7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH() + ) + ); + erc7683Allocator.resolve(onChainCrossChainOrder_); + } + function test_resolve_successful() public { (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); @@ -1219,3 +1572,10 @@ contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData { vm.stopPrank(); } } + +contract ERC7683Allocator_createFillerData is OnChainCrossChainOrderData { + function test_createFillerData(address claimant) public view { + bytes memory fillerData = erc7683Allocator.createFillerData(claimant); + assertEq(abi.decode(fillerData, (address)), claimant); + } +} From a898385faa12eb6561795faad726734bb88493f9 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Fri, 1 Aug 2025 14:11:30 +0200 Subject: [PATCH 41/63] added allocateAndRegister on OnChainAllocator --- snapshots/ERC7683Allocator_open.json | 2 +- snapshots/ERC7683Allocator_openFor.json | 4 +- src/allocators/ERC7683Allocator.sol | 164 +++++++++++++++--------- src/allocators/OnChainAllocator.sol | 60 ++++++++- src/interfaces/IERC7683Allocator.sol | 10 +- src/interfaces/IOnChainAllocator.sol | 17 +++ test/ERC7683Allocator.t.sol | 122 ++++++++++++++---- 7 files changed, 288 insertions(+), 91 deletions(-) diff --git a/snapshots/ERC7683Allocator_open.json b/snapshots/ERC7683Allocator_open.json index 0aee4ca..aa034e4 100644 --- a/snapshots/ERC7683Allocator_open.json +++ b/snapshots/ERC7683Allocator_open.json @@ -1,3 +1,3 @@ { - "open_simpleOrder": "184296" + "open_simpleOrder": "184608" } diff --git a/snapshots/ERC7683Allocator_openFor.json b/snapshots/ERC7683Allocator_openFor.json index 19a5aa2..47fe632 100644 --- a/snapshots/ERC7683Allocator_openFor.json +++ b/snapshots/ERC7683Allocator_openFor.json @@ -1,4 +1,4 @@ { - "openFor_simpleOrder_relayed": "168012", - "openFor_simpleOrder_userHimself": "168012" + "openFor_simpleOrder_relayed": "166882", + "openFor_simpleOrder_userHimself": "166914" } diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index b297140..a9c7e67 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -14,18 +14,18 @@ import {BatchCompact, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { /// @notice The typehash of the OrderDataOnChain struct - // keccak256("OrderDataOnChain(address arbiter,uint256 expires,Order order,uint200 targetBlock,uint56 maximumBlocksAfterTarget) + // keccak256("OrderDataOnChain(Order order,uint256 expires) // Lock(bytes12 lockTag,address token,uint256 amount) - // Order(Lock[] commitments,uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") + // Order(address arbiter,Lock[] commitments,uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt,bytes32 qualification)") bytes32 public constant ORDERDATA_ONCHAIN_TYPEHASH = - 0x95d7f00c299b34a562258ba851472a8d9bd0d8a1b88fce3a37b7d27ca06e77c4; + 0x037a34e1ded3bcc84f59dfc185efc3553c509ebab317153a8dddefce2eaee6f0; /// @notice The typehash of the OrderDataGasless struct - // keccak256("OrderDataGasless(address arbiter,Order order) + // keccak256("OrderDataGasless(Order order,bool deposit) // Lock(bytes12 lockTag,address token,uint256 amount) - // Order(Lock[] commitments,uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") + // Order(address arbiter,Lock[] commitments,uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt,bytes32 qualification)") bytes32 public constant ORDERDATA_GASLESS_TYPEHASH = - 0xdebd9e7866045b7f0ce8613ffbb31daa3fa5c6e6ac228316ba9f57fda63b7489; + 0x79e4af6feaa84a46fd69ed25e4595e9f6e8690ba3a6c564bfa235542f9faf55c; /// @notice keccak256("BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments,Mandate mandate) // Lock(bytes12 lockTag,address token,uint256 amount) @@ -41,32 +41,49 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { constructor(address compactContract_) OnChainAllocator(compactContract_) {} /// @inheritdoc IOriginSettler - function openFor(GaslessCrossChainOrder calldata order_, bytes calldata sponsorSignature_, bytes calldata) - external - { + function openFor( + GaslessCrossChainOrder calldata order_, + bytes calldata sponsorSignature_, + bytes calldata fillerData + ) external { // Check if orderDataType is the one expected by the allocator if (order_.orderDataType != ORDERDATA_GASLESS_TYPEHASH) { revert InvalidOrderDataType(order_.orderDataType, ORDERDATA_GASLESS_TYPEHASH); } - /// TODO: Potentially useless check, since the allocator Id gets checked later. if (order_.originSettler != address(this)) { revert InvalidOriginSettler(order_.originSettler, address(this)); } + + // Decode the orderData + (Order calldata orderData, uint32 deposit) = _decodeOrderData(order_.orderData); + + uint160 caller = uint160(deposit * uint160(msg.sender)); // for a deposit, the nonce will be scoped to the caller + user + bytes32 nonceIdentifier = _toNonceId(address(caller), order_.user); + // Early revert if the expected nonce is not the next nonce - if (order_.nonce != nonces[order_.user] + 1) { - revert InvalidNonce(order_.nonce, nonces[order_.user] + 1); + if (order_.nonce != nonces[nonceIdentifier] + 1) { + revert InvalidNonce(order_.nonce, nonces[nonceIdentifier] + 1); } - // Decode the orderData - (, Order calldata orderData,,) = _decodeOrderData(order_.orderData, false); + bytes32 qualification = bytes32(uint256(orderData.qualification) * deposit); // delete qualification if not a deposit ResolvedCrossChainOrder memory resolvedOrder = _resolveOrder( - order_.user, order_.nonce, order_.openDeadline, order_.fillDeadline, orderData, sponsorSignature_, 0, 0 + order_.user, + order_.nonce, + order_.openDeadline, + order_.fillDeadline, + orderData, + sponsorSignature_, + qualification ); bytes32 mandateHash = _mandateHash(orderData, order_.fillDeadline); - _open(order_.user, order_.openDeadline, orderData, sponsorSignature_, mandateHash, bytes32(0), resolvedOrder); + if (deposit == 0) { + _open(order_.user, order_.openDeadline, orderData, sponsorSignature_, mandateHash, resolvedOrder); + } else { + _openAndRegister(order_.user, order_.openDeadline, orderData, mandateHash, resolvedOrder); + } } /// @inheritdoc IOriginSettler @@ -77,31 +94,21 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { } // Decode the orderData - (uint32 expires, Order calldata orderData, uint200 targetBlock, uint56 maximumBlocksAfterTarget) = - _decodeOrderData(order.orderData, true); + (Order calldata orderData, uint32 expires) = _decodeOrderData(order.orderData); bytes32 mandateHash = _mandateHash(orderData, order.fillDeadline); ResolvedCrossChainOrder memory resolvedOrder = _resolveOrder( msg.sender, - nonces[msg.sender] + 1, + nonces[_toNonceId(address(0), msg.sender)] + 1, expires, order.fillDeadline, orderData, LibBytes.emptyCalldata(), - targetBlock, - maximumBlocksAfterTarget + orderData.qualification ); - _open( - msg.sender, - expires, - orderData, - LibBytes.emptyCalldata(), - mandateHash, - bytes32(abi.encodePacked(targetBlock, maximumBlocksAfterTarget)), - resolvedOrder - ); + _open(msg.sender, expires, orderData, LibBytes.emptyCalldata(), mandateHash, resolvedOrder); } /// @inheritdoc IOriginSettler @@ -114,17 +121,22 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { if (order_.orderDataType != ORDERDATA_GASLESS_TYPEHASH) { revert InvalidOrderDataType(order_.orderDataType, ORDERDATA_GASLESS_TYPEHASH); } - /// TODO: Potentially useless check, since the allocator Id gets checked later. if (order_.originSettler != address(this)) { revert InvalidOriginSettler(order_.originSettler, address(this)); } + + // Decode the orderData + (Order calldata orderData, uint32 deposit) = _decodeOrderData(order_.orderData); + + uint160 caller = uint160(deposit * uint160(msg.sender)); // for a deposit, the nonce will be scoped to the caller + user + bytes32 nonceIdentifier = _toNonceId(address(caller), order_.user); + // Early revert if the expected nonce is not the next nonce - if (order_.nonce != nonces[order_.user] + 1) { - revert InvalidNonce(order_.nonce, nonces[order_.user] + 1); + if (order_.nonce != nonces[nonceIdentifier] + 1) { + revert InvalidNonce(order_.nonce, nonces[nonceIdentifier] + 1); } - // Decode the orderData - (, Order calldata orderData,,) = _decodeOrderData(order_.orderData, false); + bytes32 qualification = bytes32(uint256(orderData.qualification) * deposit); return _resolveOrder( order_.user, @@ -133,8 +145,7 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { order_.fillDeadline, orderData, LibBytes.emptyCalldata(), - 0, - 0 + qualification ); } @@ -146,18 +157,16 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { } // Decode the orderData - (uint32 expires, Order calldata orderData, uint200 targetBlock, uint56 maximumBlocksAfterTarget) = - _decodeOrderData(order.orderData, true); + (Order calldata orderData, uint32 expires) = _decodeOrderData(order.orderData); return _resolveOrder( msg.sender, - nonces[msg.sender] + 1, + nonces[_toNonceId(address(0), msg.sender)] + 1, expires, order.fillDeadline, orderData, LibBytes.emptyCalldata(), - targetBlock, - maximumBlocksAfterTarget + orderData.qualification ); } @@ -206,8 +215,18 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { } /// @inheritdoc IERC7683Allocator - function checkNonce(uint256 nonce_, address sponsor_) external view returns (bool nonceValid) { - return nonces[sponsor_] + 1 == nonce_; + function checkNonce(GaslessCrossChainOrder calldata order_, address caller) + external + view + returns (bool nonceValid) + { + (, uint32 deposit) = _decodeOrderData(order_.orderData); + + caller = address(uint160(deposit * uint160(caller))); // for a deposit, the nonce will be scoped to the caller + user + bytes32 nonceIdentifier = _toNonceId(caller, order_.user); + + // Early revert if the expected nonce is not the next nonce + return (order_.nonce == nonces[nonceIdentifier] + 1); } /// @inheritdoc IERC7683Allocator @@ -219,10 +238,9 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { address sponsor, uint32 expires, Order calldata orderData, - bytes calldata sponsorSignature_, - bytes32 mandateHash_, - bytes32 qualification_, - ResolvedCrossChainOrder memory resolvedOrder_ + bytes calldata sponsorSignature, + bytes32 mandateHash, + ResolvedCrossChainOrder memory resolvedOrder ) internal { // Register the allocation on chain (bytes32 claimHash, uint256 nonce) = allocateFor( @@ -231,27 +249,54 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { orderData.arbiter, expires, BATCH_COMPACT_WITNESS_TYPEHASH, - mandateHash_, - sponsorSignature_ + mandateHash, + sponsorSignature + ); + + if (sponsor == msg.sender && orderData.qualification != bytes32(0)) { + qualifications[claimHash] = orderData.qualification; + } + + // Emit an open event + emit Open(bytes32(nonce), resolvedOrder); + } + + function _openAndRegister( + address sponsor, + uint32 expires, + Order calldata orderData, + bytes32 mandateHash_, + ResolvedCrossChainOrder memory resolvedOrder_ + ) internal { + // Register the allocation on chain + (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = allocateAndRegister( + sponsor, orderData.commitments, orderData.arbiter, expires, BATCH_COMPACT_WITNESS_TYPEHASH, mandateHash_ ); - qualifications[claimHash] = qualification_; + for (uint256 i = 0; i < registeredAmounts.length; i++) { + if (registeredAmounts[i] != orderData.commitments[i].amount) { + // FOT tokens unsupported + revert UnsupportedToken(orderData.commitments[i].token); + } + } + + if (orderData.qualification != bytes32(0)) { + qualifications[claimHash] = orderData.qualification; + } // Emit an open event emit Open(bytes32(nonce), resolvedOrder_); } - function _decodeOrderData(bytes calldata orderData, bool onChain) + function _decodeOrderData(bytes calldata orderData) internal pure - returns (uint32 expires, Order calldata order, uint200 targetBlock, uint56 maximumBlocksAfterTarget) + returns (Order calldata order, uint32 additionalInput) { // orderData includes the OrderData(OnChain/Gasless) struct, and the nested Order struct. // 0x00: OrderDataOnChain.offset // 0x20: OrderDataOnChain.order.offset // 0x40: OrderDataOnChain.expires - // 0x60: OrderDataOnChain.targetBlock - // 0x80: OrderDataOnChain.maximumBlocksAfterTarget // 0x00: OrderDataGasless.offset // 0x20: OrderDataGasless.order.offset @@ -262,9 +307,7 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { order := add(orderData.offset, add(s, 0x20)) // Add 0x20 since the OrderStruct is within the `OrderData...` struct if shr(64, or(s, or(l, orderData.offset))) { revert(l, 0x00) } - expires := mul(calldataload(add(orderData.offset, 0x40)), onChain) - targetBlock := mul(calldataload(add(orderData.offset, 0x60)), onChain) - maximumBlocksAfterTarget := mul(calldataload(add(orderData.offset, 0x80)), onChain) + additionalInput := calldataload(add(orderData.offset, 0x40)) } } @@ -275,8 +318,7 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { uint32 fillDeadline, Order calldata orderData, bytes calldata sponsorSignature, - uint200 targetBlock, - uint56 maximumBlocksAfterTarget + bytes32 qualification ) internal view returns (ResolvedCrossChainOrder memory) { ResolvedCrossChainOrder memory resolvedOrder = ResolvedCrossChainOrder({ user: sponsor, @@ -319,7 +361,7 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { fillInstructions[0] = FillInstruction({ destinationChainId: orderData.chainId, destinationSettler: _addressToBytes32(orderData.tribunal), - originData: abi.encode(claim, mandate, targetBlock, maximumBlocksAfterTarget) + originData: abi.encode(claim, mandate, uint200(bytes25(qualification)), uint56(uint256(qualification))) }); resolvedOrder.fillInstructions = fillInstructions; diff --git a/src/allocators/OnChainAllocator.sol b/src/allocators/OnChainAllocator.sol index d910055..08d5856 100644 --- a/src/allocators/OnChainAllocator.sol +++ b/src/allocators/OnChainAllocator.sol @@ -3,7 +3,9 @@ pragma solidity ^0.8.27; import {IOnChainAllocator} from '../interfaces/IOnChainAllocator.sol'; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {ERC6909} from '@solady/tokens/ERC6909.sol'; +import {SafeTransferLib} from '@solady/utils/SafeTransferLib.sol'; import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; import {LOCK_TYPEHASH, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; @@ -15,7 +17,7 @@ contract OnChainAllocator is IOnChainAllocator { mapping(bytes32 tokenHash => Allocation[] allocations) internal _allocations; - mapping(address user => uint256 nonce) public nonces; + mapping(bytes32 user => uint256 nonce) public nonces; modifier onlyCompact() { if (msg.sender != COMPACT_CONTRACT) { @@ -74,6 +76,56 @@ contract OnChainAllocator is IOnChainAllocator { return (claimHash, claimNonce); } + /// @inheritdoc IOnChainAllocator + function allocateAndRegister( + address recipient, + Lock[] calldata commitments, + address arbiter, + uint32 expires, + bytes32 typehash, + bytes32 witness + ) public returns (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) { + nonce = ++nonces[_toNonceId(msg.sender, recipient)]; // prevents griefing of frontrunning nonces + uint256[2][] memory idsAndAmounts = new uint256[2][](commitments.length); + uint256 minResetPeriod = type(uint256).max; + for (uint256 i = 0; i < commitments.length; i++) { + minResetPeriod = _checkInput(commitments[i], recipient, expires, minResetPeriod); + + // Store the allocation + idsAndAmounts[i][0] = _toId(commitments[i].lockTag, commitments[i].token); + uint224 amount = uint224(commitments[i].amount); + if (amount == 0) { + amount = uint224(IERC20(commitments[i].token).balanceOf(address(this))); + } + idsAndAmounts[i][1] = amount; + + if (IERC20(commitments[i].token).allowance(address(this), COMPACT_CONTRACT) < amount) { + SafeTransferLib.safeApproveWithRetry(commitments[i].token, COMPACT_CONTRACT, type(uint256).max); + } + } + // Ensure expiration is not bigger then the smallest reset period + if (expires > block.timestamp + minResetPeriod) { + revert InvalidExpiration(expires, block.timestamp + minResetPeriod); + } + + (claimHash, registeredAmounts) = ITheCompact(COMPACT_CONTRACT).batchDepositAndRegisterFor( + recipient, idsAndAmounts, arbiter, nonce, expires, typehash, witness + ); + + // Store the allocation + for (uint256 i = 0; i < registeredAmounts.length; i++) { + bytes32 tokenHash = _getTokenHash(commitments[i], recipient); + + Allocation memory allocation = + Allocation({expires: expires, amount: uint224(registeredAmounts[i]), claimHash: claimHash}); + _allocations[tokenHash].push(allocation); + } + + emit AllocationRegistered(recipient, claimHash, nonce, expires, commitments); + + return (claimHash, registeredAmounts, nonce); + } + /// @inheritdoc IAllocator function attest(address, address from_, address, uint256 id_, uint256 amount_) external returns (bytes4) { // Can be called by anyone, as this will only clean up expired allocations. @@ -150,7 +202,7 @@ contract OnChainAllocator is IOnChainAllocator { bytes32 witness ) internal returns (bytes32 claimHash, uint256 nonce) { bytes32 commitmentsHash = _getCommitmentsHash(commitments); - nonce = ++nonces[sponsor]; + nonce = ++nonces[_toNonceId(address(0), sponsor)]; // address(0) as caller allows anyone to relay claimHash = keccak256(abi.encode(typehash, arbiter, sponsor, nonce, expires, commitmentsHash, witness)); uint256 minResetPeriod = type(uint256).max; @@ -362,6 +414,10 @@ contract OnChainAllocator is IOnChainAllocator { return id; } + function _toNonceId(address caller, address sponsor) internal pure returns (bytes32 nonce) { + return keccak256(abi.encode(caller, sponsor)); + } + function _toSeconds(bytes12 lockTag) internal pure returns (uint256 duration) { assembly ("memory-safe") { let resetPeriod := shr(253, shl(1, lockTag)) diff --git a/src/interfaces/IERC7683Allocator.sol b/src/interfaces/IERC7683Allocator.sol index c901df5..f095091 100644 --- a/src/interfaces/IERC7683Allocator.sol +++ b/src/interfaces/IERC7683Allocator.sol @@ -10,12 +10,11 @@ interface IERC7683Allocator is IOriginSettler, IAllocator { struct OrderDataOnChain { Order order; // The remaining BatchCompact and Mandate data uint256 expires; // COMPACT - The time at which the claim expires and the user is able to withdraw their funds. - uint200 targetBlock; // ADDITIONAL INPUT - The block number at the target chain on which the PGA is executed / the reverse dutch auction starts. - uint56 maximumBlocksAfterTarget; // ADDITIONAL INPUT - Blocks after target block that are still fillable. } struct OrderDataGasless { Order order; // The remaining BatchCompact and Mandate data + bool deposit; // Weather the order includes a deposit of the relevant tokens. This allows to skip a sponsor confirmation } /// @dev The data that OnChain and Gasless orders have in common @@ -32,6 +31,7 @@ interface IERC7683Allocator is IOriginSettler, IAllocator { uint256 scalingFactor; // MANDATE - Fee scaling multiplier (1e18 baseline) uint256[] decayCurve; // MANDATE - Block durations, fill increases, & claim decreases. bytes32 salt; // MANDATE - Replay protection parameter + bytes32 qualification; // ADDITIONAL INPUT - abi.encodePacked(uint200 targetBlock, uint56 maximumBlocksAfterTarget) The block number at the target chain on which the PGA is executed / the reverse dutch auction starts & blocks after target block that are still fillable. } error InvalidOriginSettler(address originSettler, address expectedOriginSettler); @@ -39,13 +39,17 @@ interface IERC7683Allocator is IOriginSettler, IAllocator { error InvalidNonce(uint256 nonce, uint256 expectedNonce); error BatchCompactsNotSupported(); error InvalidAllocatorData(bytes32 expectedAllocatorData, bytes32 actualAllocatorData); + error UnsupportedToken(address token); /// @notice Returns the type string of the compact including the witness function getCompactWitnessTypeString() external pure returns (string memory); /// @notice Checks if a nonce is free to be used /// @dev The nonce is the most significant 96 bits. The least significant 160 bits must be the sponsor address - function checkNonce(uint256 nonce_, address sponsor_) external view returns (bool nonceFree_); + function checkNonce(GaslessCrossChainOrder calldata order_, address caller) + external + view + returns (bool nonceFree_); /// @notice Creates the filler data for the open event to be used on the IDestinationSettler /// @param claimant_ The address claiming the origin tokens after a successful fill (typically the address of the filler) diff --git a/src/interfaces/IOnChainAllocator.sol b/src/interfaces/IOnChainAllocator.sol index 9376032..7aadd47 100644 --- a/src/interfaces/IOnChainAllocator.sol +++ b/src/interfaces/IOnChainAllocator.sol @@ -86,4 +86,21 @@ interface IOnChainAllocator is IAllocator { bytes32 witness, bytes calldata signature ) external returns (bytes32 claimHash, uint256 claimNonce); + + /// @notice Registers an allocation for a set of tokens on behalf of a recipient + /// @dev The msg.sender needs to provide the tokens of the registered allocation + /// @param recipient The recipient of the allocation + /// @param commitments The commitments of the allocations + /// @param arbiter The arbiter of the allocation + /// @param expires The expiration of the allocation + /// @param typehash The typehash of the allocation + /// @param witness The witness of the allocation + function allocateAndRegister( + address recipient, + Lock[] calldata commitments, + address arbiter, + uint32 expires, + bytes32 typehash, + bytes32 witness + ) external returns (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce); } diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index fc42a57..b9ad265 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -63,6 +63,8 @@ abstract contract MocksSetup is Test, TestHelper { bytes32 ORDERDATA_GASLESS_TYPEHASH; bytes32 ORDERDATA_ONCHAIN_TYPEHASH; + uint256 NONCES_STORAGE_SLOT = 1; + function setUp() public virtual { (user, userPK) = makeAddrAndKey('user'); arbiter = makeAddr('arbiter'); @@ -77,10 +79,10 @@ abstract contract MocksSetup is Test, TestHelper { defaultNonce = 1; ORDERDATA_GASLESS_TYPEHASH = keccak256( - 'OrderDataGasless(address arbiter,Order order)Lock(bytes12 lockTag,address token,uint256 amount)Order(Lock[] commitments,uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)' + 'OrderDataGasless(Order order,bool deposit)Lock(bytes12 lockTag,address token,uint256 amount)Order(address arbiter,Lock[] commitments,uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt,bytes32 qualification)' ); ORDERDATA_ONCHAIN_TYPEHASH = keccak256( - 'OrderDataOnChain(address arbiter,uint256 expires,Order order,uint200 targetBlock,uint56 maximumBlocksAfterTarget)Lock(bytes12 lockTag,address token,uint256 amount)Order(Lock[] commitments,uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)' + 'OrderDataOnChain(Order order,uint256 expires)Lock(bytes12 lockTag,address token,uint256 amount)Order(address arbiter,Lock[] commitments,uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt,bytes32 qualification)' ); } } @@ -276,8 +278,10 @@ abstract contract GaslessCrossChainOrderData is CompactData { baselinePriorityFee: mandate_.baselinePriorityFee, scalingFactor: mandate_.scalingFactor, decayCurve: mandate_.decayCurve, - salt: mandate_.salt - }) + salt: mandate_.salt, + qualification: bytes32(0) + }), + deposit: false }) ); } @@ -312,8 +316,10 @@ abstract contract GaslessCrossChainOrderData is CompactData { baselinePriorityFee: mandate_.baselinePriorityFee, scalingFactor: mandate_.scalingFactor, decayCurve: mandate_.decayCurve, - salt: mandate_.salt - }) + salt: mandate_.salt, + qualification: bytes32(0) + }), + deposit: false }) ) }); @@ -329,6 +335,18 @@ abstract contract GaslessCrossChainOrderData is CompactData { (bytes memory signature_) = _hashAndSign(_getCompact(), _getMandate(), address(compactContract), userPK); return (gaslessCrossChainOrder, signature_); } + + function _manipulateDeposit(IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bool deposit) + internal + pure + returns (IOriginSettler.GaslessCrossChainOrder memory) + { + bytes memory orderData = gaslessCrossChainOrder_.orderData; + assembly ("memory-safe") { + mstore(add(orderData, 0x60), deposit) + } + return gaslessCrossChainOrder_; + } } abstract contract OnChainCrossChainOrderData is CompactData { @@ -344,7 +362,6 @@ abstract contract OnChainCrossChainOrderData is CompactData { onchainCrossChainOrder.orderDataType = erc7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH(); onchainCrossChainOrder.orderData = abi.encode( IERC7683Allocator.OrderDataOnChain({ - expires: compact_.expires, order: IERC7683Allocator.Order({ arbiter: compact_.arbiter, commitments: compact_.commitments, @@ -356,10 +373,10 @@ abstract contract OnChainCrossChainOrderData is CompactData { baselinePriorityFee: mandate_.baselinePriorityFee, scalingFactor: mandate_.scalingFactor, decayCurve: mandate_.decayCurve, - salt: mandate_.salt + salt: mandate_.salt, + qualification: bytes32(abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget)) }), - targetBlock: defaultTargetBlock, - maximumBlocksAfterTarget: defaultMaximumBlocksAfterTarget + expires: compact_.expires }) ); } @@ -378,7 +395,6 @@ abstract contract OnChainCrossChainOrderData is CompactData { orderDataType: orderDataType_, orderData: abi.encode( IERC7683Allocator.OrderDataOnChain({ - expires: compact_.expires, order: IERC7683Allocator.Order({ arbiter: compact_.arbiter, commitments: compact_.commitments, @@ -390,10 +406,10 @@ abstract contract OnChainCrossChainOrderData is CompactData { baselinePriorityFee: mandate_.baselinePriorityFee, scalingFactor: mandate_.scalingFactor, decayCurve: mandate_.decayCurve, - salt: mandate_.salt + salt: mandate_.salt, + qualification: bytes32(abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget)) }), - targetBlock: defaultTargetBlock, - maximumBlocksAfterTarget: defaultMaximumBlocksAfterTarget + expires: compact_.expires }) ) }); @@ -1537,15 +1553,66 @@ contract ERC7683Allocator_getCompactWitnessTypeString is MocksSetup { } } -contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData { - function test_invalidNonce(uint256 nonce_, address otherUser) public view { - vm.assume(nonce_ != defaultNonce); +contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData, GaslessCrossChainOrderData { + function setUp() public override(OnChainCrossChainOrderData, GaslessCrossChainOrderData) { + super.setUp(); + } + + function test_invalidNonce_noDeposit(uint256 nonce, address targetUser, address caller) public { + vm.assume(nonce != defaultNonce); + + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = + _getGaslessCrossChainOrder(); + gaslessCrossChainOrder_.user = targetUser; + gaslessCrossChainOrder_.nonce = nonce; + gaslessCrossChainOrder_ = _manipulateDeposit(gaslessCrossChainOrder_, false); + + assertFalse(erc7683Allocator.checkNonce(gaslessCrossChainOrder_, caller)); + } + + function test_invalidNonce_withDeposit(uint256 nonce, address targetUser, address caller) public { + vm.assume(nonce != defaultNonce); + + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = + _getGaslessCrossChainOrder(); + gaslessCrossChainOrder_.user = targetUser; + gaslessCrossChainOrder_.nonce = nonce; + gaslessCrossChainOrder_ = _manipulateDeposit(gaslessCrossChainOrder_, true); - assertFalse(erc7683Allocator.checkNonce(nonce_, otherUser)); + assertFalse(erc7683Allocator.checkNonce(gaslessCrossChainOrder_, caller)); } - function test_nextFreeNonce(address otherUser) public view { - assertTrue(erc7683Allocator.checkNonce(defaultNonce, otherUser)); + function test_freeNonce_noDeposit(uint256 nonce, address targetUser, address caller) public { + vm.assume(nonce > 0); + vm.assume(nonce < type(uint256).max); + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = + _getGaslessCrossChainOrder(); + gaslessCrossChainOrder_.user = targetUser; + gaslessCrossChainOrder_.nonce = nonce + 1; + gaslessCrossChainOrder_ = _manipulateDeposit(gaslessCrossChainOrder_, false); + + bytes32 nonceSlot = keccak256(abi.encode(keccak256(abi.encode(address(0), targetUser)), NONCES_STORAGE_SLOT)); + assertEq(vm.load(address(erc7683Allocator), nonceSlot), 0); + vm.store(address(erc7683Allocator), nonceSlot, bytes32(nonce)); + + assertTrue(erc7683Allocator.checkNonce(gaslessCrossChainOrder_, caller)); + } + + function test_freeNonce_withDeposit(uint256 nonce, address targetUser, address caller) public { + vm.assume(nonce > 0); + vm.assume(nonce < type(uint256).max); + + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = + _getGaslessCrossChainOrder(); + gaslessCrossChainOrder_.user = targetUser; + gaslessCrossChainOrder_.nonce = nonce + 1; + gaslessCrossChainOrder_ = _manipulateDeposit(gaslessCrossChainOrder_, true); + + bytes32 nonceSlot = keccak256(abi.encode(keccak256(abi.encode(caller, targetUser)), NONCES_STORAGE_SLOT)); + assertEq(vm.load(address(erc7683Allocator), nonceSlot), 0); + vm.store(address(erc7683Allocator), nonceSlot, bytes32(nonce)); + + assertTrue(erc7683Allocator.checkNonce(gaslessCrossChainOrder_, caller)); } function test_usedNonce(address otherUser) public { @@ -1567,8 +1634,19 @@ contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData { (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); erc7683Allocator.open(onChainCrossChainOrder_); - vm.assertFalse(erc7683Allocator.checkNonce(defaultNonce, user)); - vm.assertTrue(erc7683Allocator.checkNonce(defaultNonce, otherUser)); + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = + _getGaslessCrossChainOrder(); + + bytes32 nonceSlot = keccak256(abi.encode(keccak256(abi.encode(address(0), user)), NONCES_STORAGE_SLOT)); + assertEq(uint256(vm.load(address(erc7683Allocator), nonceSlot)), 1, 'nonce slot not correct'); + + gaslessCrossChainOrder_.user = user; + gaslessCrossChainOrder_.nonce = defaultNonce + 1; + vm.assertTrue(erc7683Allocator.checkNonce(gaslessCrossChainOrder_, address(this)), 'user nonce free'); + + gaslessCrossChainOrder_.nonce = defaultNonce; + gaslessCrossChainOrder_.user = otherUser; + vm.assertTrue(erc7683Allocator.checkNonce(gaslessCrossChainOrder_, address(this)), 'other user nonce not free'); vm.stopPrank(); } } From 254c4d15dd209e200b6a185a8e86308896fe06ea Mon Sep 17 00:00:00 2001 From: mgretzke Date: Fri, 1 Aug 2025 14:16:35 +0200 Subject: [PATCH 42/63] remove fillerData input --- src/allocators/ERC7683Allocator.sol | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index a9c7e67..56b3f3a 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -41,11 +41,9 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { constructor(address compactContract_) OnChainAllocator(compactContract_) {} /// @inheritdoc IOriginSettler - function openFor( - GaslessCrossChainOrder calldata order_, - bytes calldata sponsorSignature_, - bytes calldata fillerData - ) external { + function openFor(GaslessCrossChainOrder calldata order_, bytes calldata sponsorSignature_, bytes calldata) + external + { // Check if orderDataType is the one expected by the allocator if (order_.orderDataType != ORDERDATA_GASLESS_TYPEHASH) { revert InvalidOrderDataType(order_.orderDataType, ORDERDATA_GASLESS_TYPEHASH); From bae6cbccbe851b3cae0453ed2ba60681022a85b2 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Fri, 1 Aug 2025 17:05:12 +0200 Subject: [PATCH 43/63] removed unused input --- foundry.toml | 8 -------- test/ERC7683Allocator.t.sol | 2 -- 2 files changed, 10 deletions(-) diff --git a/foundry.toml b/foundry.toml index 645b201..f6597d1 100644 --- a/foundry.toml +++ b/foundry.toml @@ -21,14 +21,6 @@ remappings = [ "@solady=lib/solady/src", ] -additional_compiler_profiles = [ - { name = "test", via_ir = false, optimizer = false } -] - -compilation_restrictions = [ - { paths = "test/**", via_ir = false, optimizer = false } -] - [profile.ci] inherit = "default" optimizer_runs = 200 # Override optimizer runs to reduce the compact contract sizes diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index b9ad265..33cb81c 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -25,9 +25,7 @@ import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; import {IERC7683Allocator} from 'src/interfaces/IERC7683Allocator.sol'; import {IOnChainAllocator} from 'src/interfaces/IOnchainAllocator.sol'; -import {ISimpleAllocator} from 'src/interfaces/ISimpleAllocator.sol'; import {ERC20Mock} from 'src/test/ERC20Mock.sol'; -import {TheCompactMock} from 'src/test/TheCompactMock.sol'; abstract contract MocksSetup is Test, TestHelper { address user; From cad510201cff740190e8f7387f9c016d3d574e71 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Thu, 7 Aug 2025 18:38:36 +0200 Subject: [PATCH 44/63] tests, fixes, improvements --- snapshots/ERC7683Allocator_open.json | 2 +- snapshots/ERC7683Allocator_openFor.json | 4 +- snapshots/OnChainAllocatorTest.json | 6 + src/allocators/ERC7683Allocator.sol | 15 +- src/allocators/OnChainAllocator.sol | 66 +- src/interfaces/IOnChainAllocator.sol | 4 +- test/ERC7683Allocator.t.sol | 50 +- test/OnChainAllocator.t.sol | 868 ++++++++++++++++++++++++ 8 files changed, 979 insertions(+), 36 deletions(-) create mode 100644 snapshots/OnChainAllocatorTest.json create mode 100644 test/OnChainAllocator.t.sol diff --git a/snapshots/ERC7683Allocator_open.json b/snapshots/ERC7683Allocator_open.json index aa034e4..4df0727 100644 --- a/snapshots/ERC7683Allocator_open.json +++ b/snapshots/ERC7683Allocator_open.json @@ -1,3 +1,3 @@ { - "open_simpleOrder": "184608" + "open_simpleOrder": "184518" } diff --git a/snapshots/ERC7683Allocator_openFor.json b/snapshots/ERC7683Allocator_openFor.json index 47fe632..41d19ca 100644 --- a/snapshots/ERC7683Allocator_openFor.json +++ b/snapshots/ERC7683Allocator_openFor.json @@ -1,4 +1,4 @@ { - "openFor_simpleOrder_relayed": "166882", - "openFor_simpleOrder_userHimself": "166914" + "openFor_simpleOrder_relayed": "166876", + "openFor_simpleOrder_userHimself": "166908" } diff --git a/snapshots/OnChainAllocatorTest.json b/snapshots/OnChainAllocatorTest.json new file mode 100644 index 0000000..618301d --- /dev/null +++ b/snapshots/OnChainAllocatorTest.json @@ -0,0 +1,6 @@ +{ + "allocate_and_delete_expired_allocation": "66012", + "allocate_erc20": "129283", + "allocate_native": "129043", + "allocate_second_erc20": "97295" +} diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index 56b3f3a..939c2ba 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -264,26 +264,23 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { uint32 expires, Order calldata orderData, bytes32 mandateHash_, - ResolvedCrossChainOrder memory resolvedOrder_ + ResolvedCrossChainOrder memory resolvedOrder ) internal { // Register the allocation on chain (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = allocateAndRegister( sponsor, orderData.commitments, orderData.arbiter, expires, BATCH_COMPACT_WITNESS_TYPEHASH, mandateHash_ ); - for (uint256 i = 0; i < registeredAmounts.length; i++) { - if (registeredAmounts[i] != orderData.commitments[i].amount) { - // FOT tokens unsupported - revert UnsupportedToken(orderData.commitments[i].token); - } - } - if (orderData.qualification != bytes32(0)) { qualifications[claimHash] = orderData.qualification; } + for (uint256 i = 0; i < orderData.commitments.length; i++) { + resolvedOrder.minReceived[i].amount = registeredAmounts[i]; + } + // Emit an open event - emit Open(bytes32(nonce), resolvedOrder_); + emit Open(bytes32(nonce), resolvedOrder); } function _decodeOrderData(bytes calldata orderData) diff --git a/src/allocators/OnChainAllocator.sol b/src/allocators/OnChainAllocator.sol index 08d5856..7732618 100644 --- a/src/allocators/OnChainAllocator.sol +++ b/src/allocators/OnChainAllocator.sol @@ -10,6 +10,8 @@ import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; import {LOCK_TYPEHASH, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; +import {console} from 'forge-std/console.sol'; + contract OnChainAllocator is IOnChainAllocator { address public immutable COMPACT_CONTRACT; bytes32 public immutable COMPACT_DOMAIN_SEPARATOR; @@ -40,8 +42,6 @@ contract OnChainAllocator is IOnChainAllocator { (claimHash, claimNonce) = _allocate(msg.sender, commitments, arbiter, expires, typehash, witness); emit AllocationRegistered(msg.sender, claimHash, claimNonce, expires, commitments); - - return (claimHash, claimNonce); } /// @inheritdoc IOnChainAllocator @@ -62,8 +62,8 @@ contract OnChainAllocator is IOnChainAllocator { // confirm the provided signature is valid bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), COMPACT_DOMAIN_SEPARATOR, claimHash)); address signer_ = _recoverSigner(digest, signature); - if (sponsor != signer_) { - revert InvalidSignature(sponsor, signer_); + if (sponsor != signer_ || signer_ == address(0)) { + revert InvalidSignature(signer_, sponsor); } } else { // confirm the claim hash is registered on the compact @@ -72,8 +72,6 @@ contract OnChainAllocator is IOnChainAllocator { } } emit AllocationRegistered(sponsor, claimHash, claimNonce, expires, commitments); - - return (claimHash, claimNonce); } /// @inheritdoc IOnChainAllocator @@ -104,7 +102,7 @@ contract OnChainAllocator is IOnChainAllocator { } } // Ensure expiration is not bigger then the smallest reset period - if (expires > block.timestamp + minResetPeriod) { + if (expires >= block.timestamp + minResetPeriod) { revert InvalidExpiration(expires, block.timestamp + minResetPeriod); } @@ -133,10 +131,11 @@ contract OnChainAllocator is IOnChainAllocator { // Check unlocked balance bytes32 tokenHash = _getTokenHash(id_, from_); - uint256 fullAmount = amount_ + _allocatedBalance(tokenHash); + uint256 allocatedBalance = _allocatedBalance(tokenHash); + uint256 fullAmount = amount_ + allocatedBalance; if (balance < fullAmount) { - revert InsufficientBalance(from_, id_, balance, fullAmount); + revert InsufficientBalance(from_, id_, balance - allocatedBalance, amount_); } return this.attest.selector; @@ -201,9 +200,12 @@ contract OnChainAllocator is IOnChainAllocator { bytes32 typehash, bytes32 witness ) internal returns (bytes32 claimHash, uint256 nonce) { - bytes32 commitmentsHash = _getCommitmentsHash(commitments); + if (expires < block.timestamp) { + revert InvalidExpiration(expires, block.timestamp); + } + nonce = ++nonces[_toNonceId(address(0), sponsor)]; // address(0) as caller allows anyone to relay - claimHash = keccak256(abi.encode(typehash, arbiter, sponsor, nonce, expires, commitmentsHash, witness)); + claimHash = _getClaimHash(commitments, arbiter, sponsor, nonce, expires, witness, typehash); uint256 minResetPeriod = type(uint256).max; for (uint256 i = 0; i < commitments.length; i++) { @@ -216,8 +218,8 @@ contract OnChainAllocator is IOnChainAllocator { _allocations[tokenHash].push(allocation); } // Ensure expiration is not bigger then the smallest reset period - if (expires > block.timestamp + minResetPeriod) { - revert InvalidExpiration(expires, block.timestamp + minResetPeriod); + if (expires >= block.timestamp + minResetPeriod) { + revert InvalidExpiration(expires, block.timestamp + minResetPeriod - 1); } return (claimHash, nonce); @@ -248,7 +250,7 @@ contract OnChainAllocator is IOnChainAllocator { (, uint256 forcedWithdrawal) = ITheCompact(COMPACT_CONTRACT).getForcedWithdrawalStatus( sponsor, _toId(commitment.lockTag, commitment.token) ); - if (forcedWithdrawal != 0 && forcedWithdrawal < expires) { + if (forcedWithdrawal != 0 && forcedWithdrawal <= expires) { revert ForceWithdrawalAvailable(expires, forcedWithdrawal); } @@ -259,9 +261,12 @@ contract OnChainAllocator is IOnChainAllocator { // Check the balance of the recipient is sufficient tokenHash = _getTokenHash(commitment, sponsor); uint256 balance = ERC6909(COMPACT_CONTRACT).balanceOf(sponsor, _toId(commitment.lockTag, commitment.token)); - uint256 requiredBalance = _allocatedBalance(tokenHash) + commitment.amount; + uint256 allocatedBalance = _allocatedBalance(tokenHash); + uint256 requiredBalance = allocatedBalance + commitment.amount; if (requiredBalance > balance) { - revert InsufficientBalance(sponsor, _toId(commitment.lockTag, commitment.token), balance, requiredBalance); + revert InsufficientBalance( + sponsor, _toId(commitment.lockTag, commitment.token), balance - allocatedBalance, commitment.amount + ); } } @@ -276,6 +281,7 @@ contract OnChainAllocator is IOnChainAllocator { let origLength := sload(arrayLengthSlot) let length := origLength // retrieve the arrays content slot + mstore(0x00, arrayLengthSlot) let contentSlot := keccak256(0x00, 0x20) for { let i := 0 } lt(i, length) {} { let slot := add(contentSlot, mul(i, 2)) // 0x40 to skip the claimHash slot @@ -312,7 +318,6 @@ contract OnChainAllocator is IOnChainAllocator { sstore(arrayLengthSlot, length) } } - return allocatedBalance; } function _verifyClaim(bytes32 tokenHash, bytes32 claimHash) internal returns (bool verified) { @@ -391,12 +396,10 @@ contract OnChainAllocator is IOnChainAllocator { mstore(0x20, sponsor) tokenHash := keccak256(0x00, 0x40) } - return tokenHash; } function _getTokenHash(uint256 id, address sponsor) internal pure returns (bytes32 tokenHash) { tokenHash = keccak256(abi.encode(id, sponsor)); - return tokenHash; } function _splitAllocatorId(bytes12 lockTag) internal pure returns (uint96) { @@ -411,7 +414,6 @@ contract OnChainAllocator is IOnChainAllocator { assembly ("memory-safe") { id := or(lockTag, token) } - return id; } function _toNonceId(address caller, address sponsor) internal pure returns (bytes32 nonce) { @@ -431,4 +433,28 @@ contract OnChainAllocator is IOnChainAllocator { duration := and(shr(mul(resetPeriod, 24), bitpacked), 0xffffff) } } + + function _getClaimHash( + Lock[] calldata commitments, + address arbiter, + address sponsor, + uint256 nonce, + uint32 expires, + bytes32 witness, + bytes32 typehash + ) internal pure returns (bytes32 claimHash) { + bytes32 commitmentsHash = _getCommitmentsHash(commitments); + + assembly ("memory-safe") { + let m := mload(0x40) + mstore(m, typehash) + mstore(add(m, 0x20), arbiter) + mstore(add(m, 0x40), sponsor) + mstore(add(m, 0x60), nonce) + mstore(add(m, 0x80), expires) + mstore(add(m, 0xa0), commitmentsHash) + mstore(add(m, 0xc0), witness) + claimHash := keccak256(m, sub(0xe0, mul(iszero(witness), 0x20))) + } + } } diff --git a/src/interfaces/IOnChainAllocator.sol b/src/interfaces/IOnChainAllocator.sol index 7aadd47..07b0acd 100644 --- a/src/interfaces/IOnChainAllocator.sol +++ b/src/interfaces/IOnChainAllocator.sol @@ -22,10 +22,10 @@ interface IOnChainAllocator is IAllocator { error NonceAlreadyInUse(uint256 nonce); /// @notice Thrown if the sponsor does not have enough balance to lock the amount - error InsufficientBalance(address sponsor, uint256 id, uint256 balance, uint256 expectedBalance); + error InsufficientBalance(address sponsor, uint256 id, uint256 availableBalance, uint256 expectedBalance); /// @notice Thrown if the provided expiration is not valid - error InvalidExpiration(uint256 expires, uint256 minExpiration); + error InvalidExpiration(uint256 expires, uint256 expectedExpiration); /// @notice Thrown if the expiration is longer then the tokens forced withdrawal time error ForceWithdrawalAvailable(uint256 expires, uint256 forcedWithdrawalExpiration); diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index 33cb81c..ac35577 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -42,7 +42,7 @@ abstract contract MocksSetup is Test, TestHelper { ResetPeriod defaultResetPeriod = ResetPeriod.OneMinute; Scope defaultScope = Scope.Multichain; - uint256 defaultResetPeriodTimestamp = 60; + uint256 defaultResetPeriodTimestamp = 60 - 1; uint256 defaultAmount = 1000; uint256 defaultNonce; uint256 defaultOutputChainId = 130; @@ -675,7 +675,7 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { attackerPK ); vm.prank(user); - vm.expectRevert(abi.encodeWithSelector(IOnChainAllocator.InvalidSignature.selector, user, attacker)); + vm.expectRevert(abi.encodeWithSelector(IOnChainAllocator.InvalidSignature.selector, attacker, user)); erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); } @@ -1655,3 +1655,49 @@ contract ERC7683Allocator_createFillerData is OnChainCrossChainOrderData { assertEq(abi.decode(fillerData, (address)), claimant); } } + +// ------------------------------------------------------------ +// Tests for _openAndRegister path via openFor with deposit true +// ------------------------------------------------------------ +contract ERC7683Allocator_openForDeposit is GaslessCrossChainOrderData { + // function test_openFor_withDeposit_success_emptyInputs() public { + // usdc.mint(address(erc7683Allocator), defaultAmount); + + // BatchCompact memory compact_ = _getCompact(); + // compact_.commitments[0].amount = 0; + // Mandate memory mandate_ = _getMandate(); + + // (IOriginSettler.GaslessCrossChainOrder memory order_, bytes memory sig) = _getGaslessCrossChainOrder( + // address(erc7683Allocator), + // compact_, + // mandate_, + // block.chainid, + // ORDERDATA_GASLESS_TYPEHASH, + // address(compactContract), + // userPK + // ); + // order_ = _manipulateDeposit(order_, true); + + // vm.prank(user); + // erc7683Allocator.openFor(order_, sig, ''); + // } + + function test_openFor_withDeposit_success() public { + uint256 amount = defaultAmount; + usdc.mint(address(erc7683Allocator), amount); + + (IOriginSettler.GaslessCrossChainOrder memory order_, bytes memory sig) = _getGaslessCrossChainOrder(); + order_ = _manipulateDeposit(order_, true); + + uint256 id = usdcId; + vm.prank(user); + erc7683Allocator.openFor(order_, sig, ''); + + assertEq(ERC6909(address(compactContract)).balanceOf(user, id), amount); + + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + bytes32 claimHash = _hashCompact(compact_, mandate_); + assertTrue(compactContract.isRegistered(user, claimHash, erc7683Allocator.BATCH_COMPACT_WITNESS_TYPEHASH())); + } +} diff --git a/test/OnChainAllocator.t.sol b/test/OnChainAllocator.t.sol new file mode 100644 index 0000000..074b539 --- /dev/null +++ b/test/OnChainAllocator.t.sol @@ -0,0 +1,868 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +/* + Tests for OnChainAllocator.sol + These largely mirror the structure & style of HybridAllocator.t.sol while + focussing on the purely-on-chain allocation flow (no signature logic except + the permit-style path in allocateFor). +*/ + +import {Test} from 'forge-std/Test.sol'; +import {console} from 'forge-std/console.sol'; + +import {ERC20Mock} from 'src/test/ERC20Mock.sol'; + +import {TheCompact} from '@uniswap/the-compact/TheCompact.sol'; + +import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; +import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.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 {ERC6909} from '@solady/tokens/ERC6909.sol'; +import {ResetPeriod} from '@uniswap/the-compact/types/ResetPeriod.sol'; +import {Scope} from '@uniswap/the-compact/types/Scope.sol'; + +import {TestHelper} from 'test/util/TestHelper.sol'; + +contract OnChainAllocatorTest is Test, TestHelper { + TheCompact internal compact; + OnChainAllocator internal allocator; + + address internal arbiter; + address internal user; + uint256 internal userPK; + + ERC20Mock internal usdc; + ERC20Mock internal dai; + + address internal recipient; + address internal caller; + uint256 internal callerPK; + + uint256 internal defaultAmount; + uint32 internal defaultExpiration; + + function setUp() public { + compact = new TheCompact(); + arbiter = makeAddr('arbiter'); + (user, userPK) = makeAddrAndKey('user'); + allocator = new OnChainAllocator(address(compact)); + + usdc = new ERC20Mock('USDC', 'USDC'); + dai = new ERC20Mock('DAI', 'DAI'); + + recipient = makeAddr('recipient'); + (caller, callerPK) = makeAddrAndKey('caller'); + deal(user, 1 ether); + usdc.mint(user, 1 ether); + + defaultAmount = 1 ether; + defaultExpiration = uint32(block.timestamp + 300); // 5 minutes fits 10-minute reset period + } + + /* --------------------------------------------------------------------- */ + /* Helpers */ + /* --------------------------------------------------------------------- */ + + function _commitmentsHash(Lock[] memory commitments) internal pure returns (bytes32) { + bytes32[] memory hashes = new bytes32[](commitments.length); + for (uint256 i = 0; i < commitments.length; i++) { + hashes[i] = keccak256( + abi.encode(LOCK_TYPEHASH, commitments[i].lockTag, commitments[i].token, commitments[i].amount) + ); + } + return keccak256(abi.encodePacked(hashes)); + } + + function _makeLock(address token, uint256 amount) internal view returns (Lock memory l) { + bytes12 lockTag = _toLockTag(address(allocator), Scope.Multichain, ResetPeriod.TenMinutes); + l = Lock({lockTag: lockTag, token: token, amount: amount}); + } + + function _createClaimHash( + address sponsor, + address arbiter_, + uint256 nonce, + uint256 expiration, + Lock[] memory commitments, + bytes32 witness + ) internal pure returns (bytes32) { + bytes32 commitmentsHash = _commitmentsHash(commitments); + if (witness == bytes32(0)) { + return keccak256(abi.encode(BATCH_COMPACT_TYPEHASH, arbiter_, sponsor, nonce, expiration, commitmentsHash)); + } else { + return keccak256( + abi.encode( + BATCH_COMPACT_TYPEHASH_WITH_WITNESS, arbiter_, sponsor, nonce, expiration, commitmentsHash, witness + ) + ); + } + } + + /* --------------------------------------------------------------------- */ + /* allocate() */ + /* --------------------------------------------------------------------- */ + + function test_allocate_revert_InvalidExpiration() public { + Lock[] memory commitments = new Lock[](1); + commitments[0] = _makeLock(address(0), defaultAmount); + + // Deposit native token to Compact first so allocation is backed + bytes12 lockTag = commitments[0].lockTag; + vm.prank(user); + compact.depositNative{value: defaultAmount}(lockTag, user); + + uint256 expiration = vm.getBlockTimestamp() + 600; // 10 min reset period + + vm.prank(user); + vm.expectRevert( + abi.encodeWithSelector(IOnChainAllocator.InvalidExpiration.selector, expiration, expiration - 1) + ); + allocator.allocate(commitments, arbiter, uint32(expiration), BATCH_COMPACT_TYPEHASH, bytes32(0)); + } + + function test_allocate_revert_ForceWithdrawalAvailable() public { + Lock[] memory commitments = new Lock[](1); + commitments[0] = _makeLock(address(0), defaultAmount); + + // Deposit native token to Compact first so allocation is backed + bytes12 lockTag = commitments[0].lockTag; + vm.prank(user); + compact.depositNative{value: defaultAmount}(lockTag, user); + + // Enable forced withdrawal + vm.prank(user); + (uint256 withdrawableAt) = compact.enableForcedWithdrawal( + _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)) + ); + + vm.prank(user); + vm.expectRevert( + abi.encodeWithSelector(IOnChainAllocator.ForceWithdrawalAvailable.selector, withdrawableAt, withdrawableAt) + ); + allocator.allocate(commitments, arbiter, uint32(withdrawableAt), BATCH_COMPACT_TYPEHASH, bytes32(0)); + + vm.prank(user); + allocator.allocate(commitments, arbiter, uint32(withdrawableAt - 1), BATCH_COMPACT_TYPEHASH, bytes32(0)); + } + + function test_allocate_revert_InvalidAmount() public { + Lock[] memory commitments = new Lock[](1); + commitments[0] = _makeLock(address(0), uint256(type(uint224).max) + 1); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(IOnChainAllocator.InvalidAmount.selector, commitments[0].amount)); + allocator.allocate(commitments, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0)); + } + + function test_allocate_revert_InvalidAllocator() public { + Lock[] memory commitments = new Lock[](1); + commitments[0] = _makeLock(address(0), defaultAmount); + commitments[0].lockTag = bytes12(commitments[0].lockTag & bytes12(0x110000000000000000000000)); + + vm.prank(user); + vm.expectRevert( + abi.encodeWithSelector(IOnChainAllocator.InvalidAllocator.selector, 0, allocator.ALLOCATOR_ID()) + ); + allocator.allocate(commitments, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0)); + } + + function test_allocate_success_nativeToken() public { + Lock[] memory commitments = new Lock[](1); + commitments[0] = _makeLock(address(0), defaultAmount); + + // Deposit native token to Compact first so allocation is backed + bytes12 lockTag = commitments[0].lockTag; + vm.prank(user); + compact.depositNative{value: defaultAmount}(lockTag, user); + + vm.prank(user); + (bytes32 claimHash, uint256 nonce) = + allocator.allocate(commitments, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0)); + vm.snapshotGasLastCall('allocate_native'); + + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); + idsAndAmounts[0][1] = defaultAmount; + + assertEq(nonce, 1); + assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); + } + + function test_allocate_success_erc20() public { + Lock[] memory commitments = new Lock[](1); + commitments[0] = _makeLock(address(usdc), defaultAmount); + + // Deposit ERC20 into Compact so allocation is backed + vm.prank(user); + usdc.approve(address(compact), defaultAmount); + vm.prank(user); + compact.depositERC20(address(usdc), commitments[0].lockTag, defaultAmount, user); + + vm.prank(user); + (bytes32 claimHash, uint256 nonce) = + allocator.allocate(commitments, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0)); + vm.snapshotGasLastCall('allocate_erc20'); + + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); + idsAndAmounts[0][1] = defaultAmount; + + assertEq(nonce, 1); + assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); + } + + function test_allocate_success_erc20_multipleAllocations() public { + uint256 amount = defaultAmount / 2; + Lock[] memory commitments = new Lock[](1); + commitments[0] = _makeLock(address(usdc), amount); + + // Deposit ERC20 into Compact so allocation is backed + vm.prank(user); + usdc.approve(address(compact), defaultAmount); + vm.prank(user); + compact.depositERC20(address(usdc), commitments[0].lockTag, defaultAmount, user); + + vm.prank(user); + (bytes32 claimHash, uint256 nonce) = + allocator.allocate(commitments, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0)); + vm.snapshotGasLastCall('allocate_erc20'); + + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); + idsAndAmounts[0][1] = amount; + + assertEq(nonce, 1); + assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); + + vm.prank(user); + (claimHash, nonce) = + allocator.allocate(commitments, arbiter, defaultExpiration + 10, BATCH_COMPACT_TYPEHASH, bytes32(0)); + vm.snapshotGasLastCall('allocate_second_erc20'); + + assertEq(nonce, 2); + assertTrue( + allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration + 10, idsAndAmounts, '') + ); + + // expire the first allocation and allocate again + vm.warp(defaultExpiration + 1); + vm.prank(user); + (claimHash, nonce) = + allocator.allocate(commitments, arbiter, defaultExpiration + 10, BATCH_COMPACT_TYPEHASH, bytes32(0)); + vm.snapshotGasLastCall('allocate_and_delete_expired_allocation'); + + assertEq(nonce, 3); + assertTrue( + allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration + 10, idsAndAmounts, '') + ); + } + + function test_allocate_fuzz(uint128 depositAmount, uint128 firstAmount, uint128 secondAmount, bytes32 witness) + public + { + vm.assume(depositAmount > 0); + vm.assume(firstAmount <= depositAmount); + + Lock[] memory commitments = new Lock[](1); + commitments[0] = _makeLock(address(usdc), firstAmount); + + // Deposit ERC20 into Compact so allocation is backed + vm.startPrank(user); + usdc.mint(user, depositAmount); + usdc.approve(address(compact), depositAmount); + compact.depositERC20(address(usdc), commitments[0].lockTag, depositAmount, user); + vm.stopPrank(); + + bytes32 claimHash = _createClaimHash(user, arbiter, 1, defaultExpiration, commitments, witness); + + // first allocation + + bytes32 typehash = witness == bytes32(0) ? BATCH_COMPACT_TYPEHASH : BATCH_COMPACT_TYPEHASH_WITH_WITNESS; + + vm.prank(user); + vm.expectEmit(true, true, true, true); + emit IOnChainAllocator.AllocationRegistered(user, claimHash, 1, defaultExpiration, commitments); + (bytes32 returnedClaimHash, uint256 nonce) = + allocator.allocate(commitments, arbiter, defaultExpiration, typehash, witness); + + assertEq(returnedClaimHash, claimHash); + + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); + idsAndAmounts[0][1] = firstAmount; + + assertEq(nonce, 1); + assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); + + // second allocation + commitments[0].amount = secondAmount; + + vm.prank(user); + if (uint256(secondAmount) + uint256(firstAmount) > depositAmount) { + // expect a revert of the second allocation + vm.expectRevert( + abi.encodeWithSelector( + IOnChainAllocator.InsufficientBalance.selector, + user, + _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)), + depositAmount - firstAmount, + secondAmount + ) + ); + } else { + // expect a successful second allocation + claimHash = _createClaimHash(user, arbiter, 2, defaultExpiration, commitments, witness); + vm.expectEmit(true, true, true, true); + emit IOnChainAllocator.AllocationRegistered(user, claimHash, 2, defaultExpiration, commitments); + } + (claimHash, nonce) = allocator.allocate(commitments, arbiter, defaultExpiration, typehash, witness); + + if (uint256(secondAmount) + uint256(firstAmount) <= depositAmount) { + // Check the allocations + idsAndAmounts[0][1] = secondAmount; + + assertEq(nonce, 2); + assertTrue( + allocator.isClaimAuthorized(claimHash, arbiter, user, 1, /*nonce*/ defaultExpiration, idsAndAmounts, '') + ); + assertTrue( + allocator.isClaimAuthorized(claimHash, arbiter, user, 2, /*nonce*/ defaultExpiration, idsAndAmounts, '') + ); + + uint256 amountToAttest = depositAmount - (uint256(secondAmount) + uint256(firstAmount)); + + assertEq( + allocator.attest(address(this), user, address(this), idsAndAmounts[0][0], amountToAttest), + IAllocator.attest.selector + ); + vm.expectRevert( + abi.encodeWithSelector( + IOnChainAllocator.InsufficientBalance.selector, + user, + idsAndAmounts[0][0], + amountToAttest, + amountToAttest + 1 + ) + ); + allocator.attest(address(this), user, address(this), idsAndAmounts[0][0], amountToAttest + 1); + } else if (secondAmount <= depositAmount) { + // Second allocation should be possible after the first one is expired + vm.warp(defaultExpiration + 1); + uint32 expiration = defaultExpiration + 100; + vm.prank(user); + (claimHash, nonce) = allocator.allocate(commitments, arbiter, expiration, typehash, witness); + assertEq(nonce, 2); + assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, expiration, idsAndAmounts, '')); + } + } + + /* --------------------------------------------------------------------- */ + /* allocateFor() */ + /* --------------------------------------------------------------------- */ + function test_allocateFor_revert_InvalidExpiration(address relayer) public { + Lock[] memory commitments = new Lock[](1); + commitments[0] = _makeLock(address(0), defaultAmount); + vm.prank(user); + compact.depositNative{value: defaultAmount}(commitments[0].lockTag, user); + + vm.warp(defaultExpiration + 1); + + vm.prank(relayer); + vm.expectRevert( + abi.encodeWithSelector(IOnChainAllocator.InvalidExpiration.selector, defaultExpiration, block.timestamp) + ); + allocator.allocateFor(user, commitments, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, 0x0, ''); + } + + function test_allocateFor_revert_InvalidSignature() public { + Lock[] memory commitments = new Lock[](1); + commitments[0] = _makeLock(address(0), defaultAmount); + vm.prank(user); + compact.depositNative{value: defaultAmount}(commitments[0].lockTag, user); + + (address attacker, uint256 attackerPK) = makeAddrAndKey('attacker'); + + // build digest exactly like allocator expects + bytes32 nonceKey = keccak256(abi.encode(address(0), user)); + uint256 nonceBefore = allocator.nonces(nonceKey); + bytes32 commitmentsHash = _commitmentsHash(commitments); + bytes32 claimHash = keccak256( + abi.encode(BATCH_COMPACT_TYPEHASH, arbiter, user, nonceBefore + 1, defaultExpiration, commitmentsHash) + ); + bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), compact.DOMAIN_SEPARATOR(), claimHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(attackerPK, digest); + bytes memory badSig = abi.encodePacked(r, s, v); + + vm.prank(attacker); + vm.expectRevert(abi.encodeWithSelector(IOnChainAllocator.InvalidSignature.selector, attacker, user)); + allocator.allocateFor(user, commitments, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, 0x0, badSig); + } + + function test_allocateFor_revert_InvalidSignature_invalidSignatureLength(address relayer) public { + Lock[] memory commitments = new Lock[](1); + commitments[0] = _makeLock(address(0), defaultAmount); + vm.prank(user); + compact.depositNative{value: defaultAmount}(commitments[0].lockTag, user); + + // build digest exactly like allocator expects + bytes32 nonceKey = keccak256(abi.encode(address(0), user)); + uint256 nonceBefore = allocator.nonces(nonceKey); + bytes32 commitmentsHash = _commitmentsHash(commitments); + bytes32 claimHash = keccak256( + abi.encode(BATCH_COMPACT_TYPEHASH, arbiter, user, nonceBefore + 1, defaultExpiration, commitmentsHash) + ); + bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), compact.DOMAIN_SEPARATOR(), claimHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPK, digest); + bytes memory sig = abi.encode(r, s, v); // wrong length because not packed: 96 bytes instead of 65 bytes + + vm.prank(relayer); + vm.expectRevert(abi.encodeWithSelector(IOnChainAllocator.InvalidSignature.selector, address(0), user)); + allocator.allocateFor(user, commitments, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, 0x0, sig); + } + + function test_allocateFor_success_withCompactSignature(address relayer) public { + Lock[] memory commitments = new Lock[](1); + commitments[0] = _makeLock(address(0), defaultAmount); + vm.prank(user); + compact.depositNative{value: defaultAmount}(commitments[0].lockTag, user); + + // build digest exactly like allocator expects + bytes32 nonceKey = keccak256(abi.encode(address(0), user)); + uint256 nonceBefore = allocator.nonces(nonceKey); + bytes32 commitmentsHash = _commitmentsHash(commitments); + bytes32 claimHash = keccak256( + abi.encode(BATCH_COMPACT_TYPEHASH, arbiter, user, nonceBefore + 1, defaultExpiration, commitmentsHash) + ); + bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), compact.DOMAIN_SEPARATOR(), claimHash)); + (bytes32 r, bytes32 vs) = vm.signCompact(userPK, digest); + bytes memory sig = abi.encodePacked(r, vs); + + vm.prank(relayer); + (bytes32 returnedHash, uint256 nonce) = + allocator.allocateFor(user, commitments, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, 0x0, sig); + + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); + idsAndAmounts[0][1] = defaultAmount; + + assertEq(returnedHash, claimHash); + assertEq(nonce, nonceBefore + 1); + assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); + } + + function test_allocateFor_success_withSignature(address relayer) public { + Lock[] memory commitments = new Lock[](1); + commitments[0] = _makeLock(address(0), defaultAmount); + vm.prank(user); + compact.depositNative{value: defaultAmount}(commitments[0].lockTag, user); + + // build digest exactly like allocator expects + bytes32 nonceKey = keccak256(abi.encode(address(0), user)); + uint256 nonceBefore = allocator.nonces(nonceKey); + bytes32 commitmentsHash = _commitmentsHash(commitments); + bytes32 claimHash = keccak256( + abi.encode(BATCH_COMPACT_TYPEHASH, arbiter, user, nonceBefore + 1, defaultExpiration, commitmentsHash) + ); + bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), compact.DOMAIN_SEPARATOR(), claimHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPK, digest); + bytes memory sig = abi.encodePacked(r, s, v); + + vm.prank(relayer); + (bytes32 returnedHash, uint256 nonce) = + allocator.allocateFor(user, commitments, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, 0x0, sig); + + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); + idsAndAmounts[0][1] = defaultAmount; + + assertEq(returnedHash, claimHash); + assertEq(nonce, nonceBefore + 1); + assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); + } + + function test_allocateFor_success_withWitness(address relayer) public { + Lock[] memory commitments = new Lock[](1); + commitments[0] = _makeLock(address(0), defaultAmount); + vm.prank(user); + compact.depositNative{value: defaultAmount}(commitments[0].lockTag, user); + + // build digest exactly like allocator expects + bytes32 nonceKey = keccak256(abi.encode(address(0), user)); + uint256 nonceBefore = allocator.nonces(nonceKey); + bytes32 witness = bytes32(keccak256('witness')); + bytes32 commitmentsHash = _commitmentsHash(commitments); + bytes32 claimHash = keccak256( + abi.encode( + BATCH_COMPACT_TYPEHASH, arbiter, user, nonceBefore + 1, defaultExpiration, commitmentsHash, witness + ) + ); + bytes memory sig; + { + bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), compact.DOMAIN_SEPARATOR(), claimHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPK, digest); + sig = abi.encodePacked(r, s, v); + } + vm.prank(relayer); + (bytes32 returnedHash, uint256 nonce) = + allocator.allocateFor(user, commitments, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, witness, sig); + + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); + idsAndAmounts[0][1] = defaultAmount; + + assertEq(returnedHash, claimHash); + assertEq(nonce, nonceBefore + 1); + assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); + + assertEq(allocator.nonces(nonceKey), nonceBefore + 1); + } + + /* --------------------------------------------------------------------- */ + /* isClaimAuthorized() */ + /* --------------------------------------------------------------------- */ + + function test_isClaimAuthorized_false_notAuthorized() public view { + assertFalse(allocator.isClaimAuthorized(bytes32(0), arbiter, user, 0, 0, new uint256[2][](0), '')); + } + + function test_isClaimAuthorized_false_expired() public { + Lock[] memory commitments = new Lock[](1); + commitments[0] = _makeLock(address(0), defaultAmount); + vm.prank(user); + compact.depositNative{value: defaultAmount}(commitments[0].lockTag, user); + + vm.prank(user); + (bytes32 claimHash, uint256 nonce) = + allocator.allocate(commitments, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0)); + + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); + idsAndAmounts[0][1] = defaultAmount; + + vm.prank(user); + assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); + + vm.warp(defaultExpiration + 1); + vm.prank(user); + assertFalse(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); + } + + function test_isClaimAuthorized_success() public { + Lock[] memory commitments = new Lock[](1); + commitments[0] = _makeLock(address(0), defaultAmount); + vm.prank(user); + compact.depositNative{value: defaultAmount}(commitments[0].lockTag, user); + + vm.prank(user); + (bytes32 claimHash, uint256 nonce) = + allocator.allocate(commitments, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0)); + + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); + idsAndAmounts[0][1] = defaultAmount; + + vm.prank(user); + assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); + + vm.warp(defaultExpiration); + vm.prank(user); + assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); + } + + /* --------------------------------------------------------------------- */ + /* authorizeClaim() */ + /* --------------------------------------------------------------------- */ + + function test_authorizeClaim_invalidCaller() public { + vm.expectRevert( + abi.encodeWithSelector(IOnChainAllocator.InvalidCaller.selector, address(this), address(compact)) + ); + allocator.authorizeClaim(bytes32(0), arbiter, user, 0, 0, new uint256[2][](0), ''); + } + + function test_authorizeClaim_success() public { + // register claim via allocate() + Lock[] memory commitments = new Lock[](1); + commitments[0] = _makeLock(address(0), defaultAmount); + + // back with native deposit + bytes12 lt = commitments[0].lockTag; + vm.prank(user); + compact.depositNative{value: defaultAmount}(lt, user); + vm.prank(user); + (bytes32 claimHash,) = + allocator.allocate(commitments, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0)); + + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + uint256 idNat = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); + idsAndAmounts[0][0] = idNat; + idsAndAmounts[0][1] = defaultAmount; + + // call from Compact contract address + vm.prank(address(compact)); + bytes4 sel = allocator.authorizeClaim(claimHash, arbiter, user, 1, defaultExpiration, idsAndAmounts, ''); + assertEq(sel, IAllocator.authorizeClaim.selector); + + // check deletion of the allocation + vm.prank(address(compact)); + vm.expectRevert(abi.encodeWithSelector(IOnChainAllocator.InvalidClaim.selector, claimHash)); + allocator.authorizeClaim(claimHash, arbiter, user, 1, defaultExpiration, idsAndAmounts, ''); + } + + /* --------------------------------------------------------------------- */ + /* attest */ + /* --------------------------------------------------------------------- */ + + function test_attest_revert_InsufficientBalance() public { + uint256 id = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); + vm.prank(user); + vm.expectRevert( + abi.encodeWithSelector(IOnChainAllocator.InsufficientBalance.selector, user, id, 0, defaultAmount) + ); + allocator.attest(address(0), user, address(0), id, defaultAmount); + } + + function test_attest_revert_InsufficientBalance_previousAllocation() public { + Lock[] memory commitments = new Lock[](1); + commitments[0] = _makeLock(address(0), defaultAmount); + vm.prank(user); + compact.depositNative{value: defaultAmount}(commitments[0].lockTag, user); + + vm.prank(user); + allocator.allocate(commitments, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0)); + + uint256 id = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(IOnChainAllocator.InsufficientBalance.selector, user, id, 0, 1)); + allocator.attest(address(this), user, address(this), id, 1); + } + + function test_attest_success_previousAllocation() public { + Lock[] memory commitments = new Lock[](1); + commitments[0] = _makeLock(address(0), defaultAmount - 1); + vm.prank(user); + compact.depositNative{value: defaultAmount}(commitments[0].lockTag, user); + + vm.prank(user); + allocator.allocate(commitments, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0)); + + uint256 id = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); + vm.prank(user); + vm.assertEq(allocator.attest(address(this), user, address(this), id, 1), allocator.attest.selector); + } + + function test_attest_success() public { + uint256 id = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); + // deposit id to Compact for user + vm.prank(user); + compact.depositNative{value: defaultAmount}(bytes12(bytes32(id)), user); + + vm.prank(user); + bytes4 sel = allocator.attest(address(0), user, address(0), id, defaultAmount); + assertEq(sel, allocator.attest.selector); + } + + /* --------------------------------------------------------------------- */ + /* allocateAndRegister() */ + /* --------------------------------------------------------------------- */ + + function test_allocateAndRegister_revert_InvalidExpiration() public { + Lock[] memory commitments = new Lock[](1); + commitments[0] = _makeLock(address(usdc), defaultAmount); + + // Fund allocator with tokens + usdc.mint(address(allocator), defaultAmount); + + uint256 expiration = block.timestamp + 600; + vm.prank(caller); + vm.expectRevert( + abi.encodeWithSelector(IOnChainAllocator.InvalidExpiration.selector, expiration, block.timestamp + 600) + ); + allocator.allocateAndRegister( + recipient, commitments, arbiter, uint32(expiration), BATCH_COMPACT_TYPEHASH, bytes32(0) + ); + } + + function test_allocateAndRegister_revert_InvalidAmount() public { + Lock[] memory commitments = new Lock[](1); + commitments[0] = _makeLock(address(usdc), uint256(type(uint224).max) + 1); + + vm.prank(caller); + vm.expectRevert(abi.encodeWithSelector(IOnChainAllocator.InvalidAmount.selector, commitments[0].amount)); + allocator.allocateAndRegister( + recipient, commitments, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0) + ); + } + + function test_allocateAndRegister_revert_InvalidAllocator() public { + Lock[] memory commitments = new Lock[](1); + commitments[0] = _makeLock(address(usdc), defaultAmount); + commitments[0].lockTag = bytes12(commitments[0].lockTag & bytes12(0x110000000000000000000000)); + + vm.prank(caller); + vm.expectRevert( + abi.encodeWithSelector(IOnChainAllocator.InvalidAllocator.selector, 0, allocator.ALLOCATOR_ID()) + ); + allocator.allocateAndRegister( + recipient, commitments, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0) + ); + } + + function test_allocateAndRegister_success_singleERC20() public { + Lock[] memory commitments = new Lock[](1); + commitments[0] = _makeLock(address(usdc), defaultAmount); + + usdc.mint(address(allocator), defaultAmount); + + vm.prank(caller); + (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = allocator.allocateAndRegister( + recipient, commitments, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0) + ); + + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); + idsAndAmounts[0][1] = defaultAmount; + + assertEq(nonce, 1); + assertEq(registeredAmounts.length, 1); + assertEq(registeredAmounts[0], defaultAmount); + assertEq(ERC6909(address(compact)).balanceOf(recipient, idsAndAmounts[0][0]), defaultAmount); + assertTrue( + allocator.isClaimAuthorized(claimHash, arbiter, recipient, nonce, defaultExpiration, idsAndAmounts, '') + ); + assertTrue(compact.isRegistered(recipient, claimHash, BATCH_COMPACT_TYPEHASH)); + bytes32 claimHashRecreated = + _createClaimHash(recipient, arbiter, nonce, defaultExpiration, commitments, bytes32(0)); + assertEq(claimHashRecreated, claimHash); + } + + function test_allocateAndRegister_success_singleERC20_withWitness(bytes32 witness) public { + Lock[] memory commitments = new Lock[](1); + commitments[0] = _makeLock(address(usdc), defaultAmount); + + bytes32 typehash = witness == bytes32(0) ? BATCH_COMPACT_TYPEHASH : BATCH_COMPACT_TYPEHASH_WITH_WITNESS; + + usdc.mint(address(allocator), defaultAmount); + + vm.prank(caller); + (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = + allocator.allocateAndRegister(recipient, commitments, arbiter, defaultExpiration, typehash, witness); + + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); + idsAndAmounts[0][1] = defaultAmount; + + assertEq(nonce, 1); + assertEq(registeredAmounts.length, 1); + assertEq(registeredAmounts[0], defaultAmount); + assertEq(ERC6909(address(compact)).balanceOf(recipient, idsAndAmounts[0][0]), defaultAmount); + assertTrue( + allocator.isClaimAuthorized(claimHash, arbiter, recipient, nonce, defaultExpiration, idsAndAmounts, '') + ); + assertTrue(compact.isRegistered(recipient, claimHash, typehash)); + bytes32 claimHashRecreated = + _createClaimHash(recipient, arbiter, nonce, defaultExpiration, commitments, witness); + assertEq(claimHashRecreated, claimHash); + } + + function test_allocateAndRegister_success_amountZeroDepositsFullBalance(bytes32 witness) public { + uint256 depositAmount = 5 ether; + usdc.mint(address(allocator), depositAmount); + + Lock[] memory commitments = new Lock[](1); + commitments[0] = _makeLock(address(usdc), 0); + + vm.prank(caller); + (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = allocator.allocateAndRegister( + recipient, commitments, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, witness + ); + + uint256 id = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); + + assertEq(registeredAmounts[0], depositAmount); + assertEq(usdc.balanceOf(address(allocator)), 0); + assertEq(ERC6909(address(compact)).balanceOf(recipient, id), depositAmount); + + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = id; + idsAndAmounts[0][1] = depositAmount; + + assertTrue( + allocator.isClaimAuthorized(claimHash, arbiter, recipient, nonce, defaultExpiration, idsAndAmounts, '') + ); + } + + function test_allocateAndRegister_success_multipleERC20() public { + uint256 amount1 = 1 ether; + uint256 amount2 = 2 ether; + + usdc.mint(address(allocator), amount1); + dai.mint(address(allocator), amount2); + + Lock[] memory commitments = new Lock[](2); + commitments[0] = _makeLock(address(usdc), amount1); + commitments[1] = _makeLock(address(dai), amount2); + + vm.prank(caller); + (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = allocator.allocateAndRegister( + recipient, commitments, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0) + ); + + uint256 id1 = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); + uint256 id2 = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(dai)); + + assertEq(registeredAmounts.length, 2); + assertEq(registeredAmounts[0], amount1); + assertEq(registeredAmounts[1], amount2); + + assertEq(ERC6909(address(compact)).balanceOf(recipient, id1), amount1); + assertEq(ERC6909(address(compact)).balanceOf(recipient, id2), amount2); + + uint256[2][] memory idsAndAmounts = new uint256[2][](2); + idsAndAmounts[0][0] = id1; + idsAndAmounts[0][1] = amount1; + idsAndAmounts[1][0] = id2; + idsAndAmounts[1][1] = amount2; + + assertTrue( + allocator.isClaimAuthorized(claimHash, arbiter, recipient, nonce, defaultExpiration, idsAndAmounts, '') + ); + } + + function test_allocateAndRegister_tokensImmediatelyAllocated() public { + uint256 amount1 = 1 ether; + uint256 amount2 = 2 ether; + + usdc.mint(address(allocator), amount1); + dai.mint(address(allocator), amount2); + + Lock[] memory commitments = new Lock[](2); + commitments[0] = _makeLock(address(usdc), amount1); + commitments[1] = _makeLock(address(dai), amount2); + + vm.prank(caller); + allocator.allocateAndRegister( + recipient, commitments, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0) + ); + + uint256 id1 = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); + uint256 id2 = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(dai)); + + assertEq(ERC6909(address(compact)).balanceOf(recipient, id1), amount1); + assertEq(ERC6909(address(compact)).balanceOf(recipient, id2), amount2); + + // Try to send a single unit of the tokens + + vm.expectRevert(abi.encodeWithSelector(IOnChainAllocator.InsufficientBalance.selector, recipient, id1, 0, 1)); + allocator.attest(address(this), recipient, address(this), id1, 1); + + vm.expectRevert(abi.encodeWithSelector(IOnChainAllocator.InsufficientBalance.selector, recipient, id2, 0, 1)); + allocator.attest(address(this), recipient, address(this), id2, 1); + } +} From 891743c59e751d15b20252996ad7685033acf5a1 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Fri, 8 Aug 2025 13:03:43 +0200 Subject: [PATCH 45/63] sanitize and natspec --- src/allocators/ERC7683Allocator.sol | 23 +++++++++++++++++++++++ src/allocators/OnChainAllocator.sol | 4 ++++ 2 files changed, 27 insertions(+) diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index 939c2ba..641b577 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -12,6 +12,10 @@ import {LibBytes} from '@solady/utils/LibBytes.sol'; import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; import {BatchCompact, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; +/// @title ERC7683Allocator +/// @notice Allocates tokens deposited into the compact and broadcasts orders following the ERC7683 standard. +/// @dev The contract ensures tokens can not be double spent by a user in a fully decentralized manner. +/// @dev Users can open orders for themselves or for others by providing a signature or the tokens directly. contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { /// @notice The typehash of the OrderDataOnChain struct // keccak256("OrderDataOnChain(Order order,uint256 expires) @@ -54,6 +58,7 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { // Decode the orderData (Order calldata orderData, uint32 deposit) = _decodeOrderData(order_.orderData); + deposit = _sanitize(deposit); uint160 caller = uint160(deposit * uint160(msg.sender)); // for a deposit, the nonce will be scoped to the caller + user bytes32 nonceIdentifier = _toNonceId(address(caller), order_.user); @@ -93,6 +98,7 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { // Decode the orderData (Order calldata orderData, uint32 expires) = _decodeOrderData(order.orderData); + expires = _sanitize(expires); bytes32 mandateHash = _mandateHash(orderData, order.fillDeadline); @@ -125,6 +131,7 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { // Decode the orderData (Order calldata orderData, uint32 deposit) = _decodeOrderData(order_.orderData); + deposit = _sanitize(deposit); uint160 caller = uint160(deposit * uint160(msg.sender)); // for a deposit, the nonce will be scoped to the caller + user bytes32 nonceIdentifier = _toNonceId(address(caller), order_.user); @@ -156,6 +163,7 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { // Decode the orderData (Order calldata orderData, uint32 expires) = _decodeOrderData(order.orderData); + expires = _sanitize(expires); return _resolveOrder( msg.sender, @@ -219,6 +227,7 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { returns (bool nonceValid) { (, uint32 deposit) = _decodeOrderData(order_.orderData); + deposit = _sanitize(deposit); caller = address(uint160(deposit * uint160(caller))); // for a deposit, the nonce will be scoped to the caller + user bytes32 nonceIdentifier = _toNonceId(caller, order_.user); @@ -414,4 +423,18 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { output_ := shr(96, shl(96, address_)) } } + + function _sanitize(uint32 value) internal pure returns (uint32) { + assembly ("memory-safe") { + value := shr(224, shl(224, value)) + } + return value; + } + + function _sanitize(bool value) internal pure returns (bool) { + assembly ("memory-safe") { + value := iszero(iszero(value)) + } + return value; + } } diff --git a/src/allocators/OnChainAllocator.sol b/src/allocators/OnChainAllocator.sol index 7732618..29ab18d 100644 --- a/src/allocators/OnChainAllocator.sol +++ b/src/allocators/OnChainAllocator.sol @@ -12,6 +12,10 @@ import {LOCK_TYPEHASH, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; import {console} from 'forge-std/console.sol'; +/// @title OnChainAllocator +/// @notice Allocates tokens deposited into the compact. +/// @dev The contract ensures tokens can not be double spent by a user in a fully decentralized manner. +/// @dev Users can open orders for themselves or for others by providing a signature or the tokens directly. contract OnChainAllocator is IOnChainAllocator { address public immutable COMPACT_CONTRACT; bytes32 public immutable COMPACT_DOMAIN_SEPARATOR; From 09ca4db091e78f430c64364e6723a27815adeb15 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Fri, 8 Aug 2025 16:52:11 +0200 Subject: [PATCH 46/63] small fixes --- snapshots/ERC7683Allocator_openFor.json | 4 +- src/allocators/ERC7683Allocator.sol | 15 +- src/allocators/OnChainAllocator.sol | 2 - test/ERC7683Allocator.t.sol | 212 +++++++++--------------- test/OnChainAllocator.t.sol | 92 +++++++++- 5 files changed, 176 insertions(+), 149 deletions(-) diff --git a/snapshots/ERC7683Allocator_openFor.json b/snapshots/ERC7683Allocator_openFor.json index 41d19ca..c7773a8 100644 --- a/snapshots/ERC7683Allocator_openFor.json +++ b/snapshots/ERC7683Allocator_openFor.json @@ -1,4 +1,4 @@ { - "openFor_simpleOrder_relayed": "166876", - "openFor_simpleOrder_userHimself": "166908" + "openFor_simpleOrder_relayed": "166894", + "openFor_simpleOrder_userHimself": "166926" } diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index 641b577..c3ff229 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -9,7 +9,6 @@ import {BatchClaim, Mandate} from './types/TribunalStructs.sol'; import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; import {LibBytes} from '@solady/utils/LibBytes.sol'; -import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; import {BatchCompact, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; /// @title ERC7683Allocator @@ -58,7 +57,7 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { // Decode the orderData (Order calldata orderData, uint32 deposit) = _decodeOrderData(order_.orderData); - deposit = _sanitize(deposit); + deposit = _sanitizeBool(deposit); uint160 caller = uint160(deposit * uint160(msg.sender)); // for a deposit, the nonce will be scoped to the caller + user bytes32 nonceIdentifier = _toNonceId(address(caller), order_.user); @@ -98,7 +97,7 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { // Decode the orderData (Order calldata orderData, uint32 expires) = _decodeOrderData(order.orderData); - expires = _sanitize(expires); + expires = _sanitizeUint32(expires); bytes32 mandateHash = _mandateHash(orderData, order.fillDeadline); @@ -131,7 +130,7 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { // Decode the orderData (Order calldata orderData, uint32 deposit) = _decodeOrderData(order_.orderData); - deposit = _sanitize(deposit); + deposit = _sanitizeBool(deposit); uint160 caller = uint160(deposit * uint160(msg.sender)); // for a deposit, the nonce will be scoped to the caller + user bytes32 nonceIdentifier = _toNonceId(address(caller), order_.user); @@ -163,7 +162,7 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { // Decode the orderData (Order calldata orderData, uint32 expires) = _decodeOrderData(order.orderData); - expires = _sanitize(expires); + expires = _sanitizeUint32(expires); return _resolveOrder( msg.sender, @@ -227,7 +226,7 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { returns (bool nonceValid) { (, uint32 deposit) = _decodeOrderData(order_.orderData); - deposit = _sanitize(deposit); + deposit = _sanitizeBool(deposit); caller = address(uint160(deposit * uint160(caller))); // for a deposit, the nonce will be scoped to the caller + user bytes32 nonceIdentifier = _toNonceId(caller, order_.user); @@ -424,14 +423,14 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { } } - function _sanitize(uint32 value) internal pure returns (uint32) { + function _sanitizeUint32(uint32 value) internal pure returns (uint32) { assembly ("memory-safe") { value := shr(224, shl(224, value)) } return value; } - function _sanitize(bool value) internal pure returns (bool) { + function _sanitizeBool(uint32 value) internal pure returns (uint32) { assembly ("memory-safe") { value := iszero(iszero(value)) } diff --git a/src/allocators/OnChainAllocator.sol b/src/allocators/OnChainAllocator.sol index 29ab18d..a656caf 100644 --- a/src/allocators/OnChainAllocator.sol +++ b/src/allocators/OnChainAllocator.sol @@ -10,8 +10,6 @@ import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; import {LOCK_TYPEHASH, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; -import {console} from 'forge-std/console.sol'; - /// @title OnChainAllocator /// @notice Allocates tokens deposited into the compact. /// @dev The contract ensures tokens can not be double spent by a user in a fully decentralized manner. diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index ac35577..6e34bcb 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -291,7 +291,8 @@ abstract contract GaslessCrossChainOrderData is CompactData { uint256 chainId_, bytes32 orderDataGaslessTypeHash_, address verifyingContract, - uint256 signerPK + uint256 signerPK, + bytes32 qualification ) internal view returns (IOriginSettler.GaslessCrossChainOrder memory, bytes memory signature) { IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_ = IOriginSettler.GaslessCrossChainOrder({ originSettler: allocator, @@ -315,7 +316,7 @@ abstract contract GaslessCrossChainOrderData is CompactData { scalingFactor: mandate_.scalingFactor, decayCurve: mandate_.decayCurve, salt: mandate_.salt, - qualification: bytes32(0) + qualification: qualification }), deposit: false }) @@ -627,7 +628,8 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { block.chainid, ORDERDATA_GASLESS_TYPEHASH, address(erc7683Allocator), - userPK + userPK, + bytes32(0) ); vm.prank(user); erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); @@ -647,7 +649,8 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { block.chainid, ORDERDATA_GASLESS_TYPEHASH, address(erc7683Allocator), - userPK + userPK, + bytes32(0) ); vm.prank(user); erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); @@ -672,7 +675,8 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { block.chainid, ORDERDATA_GASLESS_TYPEHASH, address(compactContract), - attackerPK + attackerPK, + bytes32(0) ); vm.prank(user); vm.expectRevert(abi.encodeWithSelector(IOnChainAllocator.InvalidSignature.selector, attacker, user)); @@ -806,116 +810,6 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { } } -// contract ERC7683Allocator_open is OnChainCrossChainOrderData { -// function test_revert_InvalidOrderDataType() public { -// // Order data type is invalid -// bytes32 falseOrderDataType = keccak256('false'); -// IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_ = _getOnChainCrossChainOrder(); -// onChainCrossChainOrder_.orderDataType = falseOrderDataType; - -// vm.prank(user); -// vm.expectRevert( -// abi.encodeWithSelector( -// IERC7683Allocator.InvalidOrderDataType.selector, -// falseOrderDataType, -// erc7683Allocator.ORDERDATA_TYPEHASH() -// ) -// ); -// erc7683Allocator.open(onChainCrossChainOrder_); -// } - -// function test_revert_InvalidSponsor() public { -// IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_ = _getOnChainCrossChainOrder(); - -// vm.prank(attacker); -// vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidSignature.selector, user, attacker)); -// erc7683Allocator.open(onChainCrossChainOrder_); -// } - -// function test_revert_InvalidRegistration_Unavailable() public { -// // we deposit tokens -// vm.startPrank(user); -// usdc.mint(user, defaultAmount); -// usdc.approve(address(compactContract), defaultAmount); -// compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); - -// // we do NOT register a claim - -// vm.stopPrank(); - -// (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); - -// Compact memory compact_ = _getCompact(); -// Mandate memory mandate_ = _getMandate(); -// bytes32 claimHash = _hashCompact(compact_, mandate_); - -// vm.prank(user); -// vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidRegistration.selector, user, claimHash)); -// erc7683Allocator.open(onChainCrossChainOrder_); -// } - -// function test_successful() public { -// // Deposit tokens -// vm.startPrank(user); -// usdc.mint(user, defaultAmount); -// usdc.approve(address(compactContract), defaultAmount); -// compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); - -// // register a claim -// Compact memory compact_ = _getCompact(); -// Mandate memory mandate_ = _getMandate(); - -// bytes32 claimHash = _hashCompact(compact_, mandate_); -// bytes32 typeHash = _getTypeHash(); -// compactContract.register(claimHash, typeHash); - -// vm.stopPrank(); - -// (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); -// IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); -// IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); -// IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); -// maxSpent[0] = IOriginSettler.Output({ -// token: bytes32(uint256(uint160(defaultOutputToken))), -// amount: type(uint256).max, -// recipient: bytes32(uint256(uint160(user))), -// chainId: defaultOutputChainId -// }); -// minReceived[0] = IOriginSettler.Output({ -// token: bytes32(uint256(uint160(address(usdc)))), -// amount: defaultAmount, -// recipient: '', -// chainId: block.chainid -// }); -// TribunalClaim memory claim = TribunalClaim({ -// chainId: block.chainid, -// compact: _getCompact(), -// sponsorSignature: '', -// allocatorSignature: '' -// }); -// fillInstructions[0] = IOriginSettler.FillInstruction({ -// destinationChainId: defaultOutputChainId, -// destinationSettler: bytes32(uint256(uint160(tribunal))), -// originData: abi.encode(claim, _getMandate(), defaultTargetBlock, defaultMaximumBlocksAfterTarget) -// }); - -// IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ -// user: user, -// originChainId: block.chainid, -// openDeadline: uint32(_getClaimExpiration()), -// fillDeadline: uint32(_getFillExpiration()), -// orderId: bytes32(defaultNonce), -// maxSpent: maxSpent, -// minReceived: minReceived, -// fillInstructions: fillInstructions -// }); -// vm.prank(user); -// vm.expectEmit(true, false, false, true, address(erc7683Allocator)); -// emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); -// erc7683Allocator.open(onChainCrossChainOrder_); -// } -// } - contract ERC7683Allocator_authorizeClaim is OnChainCrossChainOrderData, GaslessCrossChainOrderData { function setUp() public override(OnChainCrossChainOrderData, GaslessCrossChainOrderData) { super.setUp(); @@ -1660,27 +1554,28 @@ contract ERC7683Allocator_createFillerData is OnChainCrossChainOrderData { // Tests for _openAndRegister path via openFor with deposit true // ------------------------------------------------------------ contract ERC7683Allocator_openForDeposit is GaslessCrossChainOrderData { - // function test_openFor_withDeposit_success_emptyInputs() public { - // usdc.mint(address(erc7683Allocator), defaultAmount); - - // BatchCompact memory compact_ = _getCompact(); - // compact_.commitments[0].amount = 0; - // Mandate memory mandate_ = _getMandate(); - - // (IOriginSettler.GaslessCrossChainOrder memory order_, bytes memory sig) = _getGaslessCrossChainOrder( - // address(erc7683Allocator), - // compact_, - // mandate_, - // block.chainid, - // ORDERDATA_GASLESS_TYPEHASH, - // address(compactContract), - // userPK - // ); - // order_ = _manipulateDeposit(order_, true); - - // vm.prank(user); - // erc7683Allocator.openFor(order_, sig, ''); - // } + function test_openFor_withDeposit_success_emptyInputs() public { + usdc.mint(address(erc7683Allocator), defaultAmount); + + BatchCompact memory compact_ = _getCompact(); + compact_.commitments[0].amount = 0; + Mandate memory mandate_ = _getMandate(); + + (IOriginSettler.GaslessCrossChainOrder memory order_, bytes memory sig) = _getGaslessCrossChainOrder( + address(erc7683Allocator), + compact_, + mandate_, + block.chainid, + ORDERDATA_GASLESS_TYPEHASH, + address(compactContract), + userPK, + bytes32(0) + ); + order_ = _manipulateDeposit(order_, true); + + vm.prank(user); + erc7683Allocator.openFor(order_, sig, ''); + } function test_openFor_withDeposit_success() public { uint256 amount = defaultAmount; @@ -1700,4 +1595,49 @@ contract ERC7683Allocator_openForDeposit is GaslessCrossChainOrderData { bytes32 claimHash = _hashCompact(compact_, mandate_); assertTrue(compactContract.isRegistered(user, claimHash, erc7683Allocator.BATCH_COMPACT_WITNESS_TYPEHASH())); } + + function test_openFor_withDeposit_success_withQualification() public { + uint256 amount = defaultAmount; + usdc.mint(address(erc7683Allocator), amount); + + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + (IOriginSettler.GaslessCrossChainOrder memory order_, bytes memory sig) = _getGaslessCrossChainOrder( + address(erc7683Allocator), + compact_, + mandate_, + block.chainid, + ORDERDATA_GASLESS_TYPEHASH, + address(compactContract), + userPK, + bytes32(abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget)) + ); + order_ = _manipulateDeposit(order_, true); + + uint256 id = usdcId; + vm.prank(user); + erc7683Allocator.openFor(order_, sig, ''); + + assertEq(ERC6909(address(compactContract)).balanceOf(user, id), amount); + + bytes32 claimHash = _hashCompact(compact_, mandate_); + assertTrue(compactContract.isRegistered(user, claimHash, erc7683Allocator.BATCH_COMPACT_WITNESS_TYPEHASH())); + + assertEq( + erc7683Allocator.qualifications(claimHash), + bytes32(abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget)) + ); + assertTrue( + erc7683Allocator.isClaimAuthorized( + claimHash, + compact_.arbiter, + compact_.sponsor, + 0, + compact_.expires, + defaultIdsAndAmounts, + abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget) + ) + ); + } } diff --git a/test/OnChainAllocator.t.sol b/test/OnChainAllocator.t.sol index 074b539..e01b800 100644 --- a/test/OnChainAllocator.t.sol +++ b/test/OnChainAllocator.t.sol @@ -9,7 +9,6 @@ pragma solidity ^0.8.27; */ import {Test} from 'forge-std/Test.sol'; -import {console} from 'forge-std/console.sol'; import {ERC20Mock} from 'src/test/ERC20Mock.sol'; @@ -160,6 +159,24 @@ contract OnChainAllocatorTest is Test, TestHelper { allocator.allocate(commitments, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0)); } + function test_allocate_revert_InsufficientBalance() public { + Lock[] memory commitments = new Lock[](1); + commitments[0] = _makeLock(address(0), defaultAmount); + + // No deposit made for native token – balance is zero, should revert. + vm.prank(user); + vm.expectRevert( + abi.encodeWithSelector( + IOnChainAllocator.InsufficientBalance.selector, + user, + _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)), + 0, + defaultAmount + ) + ); + allocator.allocate(commitments, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0)); + } + function test_allocate_revert_InvalidAllocator() public { Lock[] memory commitments = new Lock[](1); commitments[0] = _makeLock(address(0), defaultAmount); @@ -523,6 +540,79 @@ contract OnChainAllocatorTest is Test, TestHelper { assertEq(allocator.nonces(nonceKey), nonceBefore + 1); } + function test_allocateFor_revert_InvalidRegistration(address relayer) public { + // Build commitments with native token deposit backing + Lock[] memory commitments = new Lock[](1); + commitments[0] = _makeLock(address(0), defaultAmount); + vm.prank(user); + compact.depositNative{value: defaultAmount}(commitments[0].lockTag, user); + + // Nonce that allocateFor will use + bytes32 nonceKey = keccak256(abi.encode(address(0), user)); + uint256 nonceBefore = allocator.nonces(nonceKey); + uint256 expectedNonce = nonceBefore + 1; + + // Compute claimHash that allocateFor will create internally + bytes32 claimHash = _createClaimHash(user, arbiter, expectedNonce, defaultExpiration, commitments, bytes32(0)); + + // Expect InvalidRegistration revert because claimHash is NOT registered on The Compact + vm.prank(relayer); + vm.expectRevert(abi.encodeWithSelector(IOnChainAllocator.InvalidRegistration.selector, user, claimHash)); + allocator.allocateFor( + user, + commitments, + arbiter, + defaultExpiration, + BATCH_COMPACT_TYPEHASH, + bytes32(0), + '' // empty signature triggers registration check + ); + } + + function test_allocateFor_success_noSignature(address relayer) public { + Lock[] memory commitments = new Lock[](1); + commitments[0] = _makeLock(address(0), defaultAmount); + + // Determine nonce as allocator will use + bytes32 nonceKey = keccak256(abi.encode(address(0), user)); + uint256 nonceBefore = allocator.nonces(nonceKey); + uint256 expectedNonce = nonceBefore + 1; + + // Pre-compute claimHash that `allocateFor` will produce + bytes32 claimHash = _createClaimHash(user, arbiter, expectedNonce, defaultExpiration, commitments, bytes32(0)); + + // Prepare ids & amounts for native token deposit + registration + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); + idsAndAmounts[0][1] = defaultAmount; + + // Prepare claimHashes+typehashes array for registration + bytes32[2][] memory claimHashesAndTypehashes = new bytes32[2][](1); + claimHashesAndTypehashes[0][0] = claimHash; + claimHashesAndTypehashes[0][1] = BATCH_COMPACT_TYPEHASH; + + // User deposits native token & registers the compact directly on TheCompact + vm.prank(user); + compact.batchDepositAndRegisterMultiple{value: defaultAmount}(idsAndAmounts, claimHashesAndTypehashes); + + // Relayer submits allocateFor WITHOUT any signature (length == 0) + vm.prank(relayer); + (bytes32 returnedHash, uint256 nonce) = allocator.allocateFor( + user, + commitments, + arbiter, + defaultExpiration, + BATCH_COMPACT_TYPEHASH, + bytes32(0), + '' // empty signature triggers the "registered" code path + ); + + // Assertions + assertEq(returnedHash, claimHash); + assertEq(nonce, expectedNonce); + assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); + } + /* --------------------------------------------------------------------- */ /* isClaimAuthorized() */ /* --------------------------------------------------------------------- */ From a2ef8346cb2aaf3f77d61c7ee58e04f910a8f9f1 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Sat, 9 Aug 2025 00:06:24 +0200 Subject: [PATCH 47/63] added support for IOnChainAllocator --- snapshots/ERC7683Allocator_open.json | 2 +- snapshots/HybridAllocatorTest.json | 14 ++--- snapshots/OnChainAllocatorTest.json | 9 +-- src/allocators/HybridAllocator.sol | 87 ++++++++++++++++++++++++++-- src/allocators/HybridERC7683.sol | 4 +- src/allocators/OnChainAllocator.sol | 4 ++ src/interfaces/IHybridAllocator.sol | 1 + test/HybridAllocator.t.sol | 14 ++--- test/HybridERC7683.t.sol | 2 +- test/OnChainAllocator.t.sol | 4 +- 10 files changed, 113 insertions(+), 28 deletions(-) diff --git a/snapshots/ERC7683Allocator_open.json b/snapshots/ERC7683Allocator_open.json index 4df0727..0286f70 100644 --- a/snapshots/ERC7683Allocator_open.json +++ b/snapshots/ERC7683Allocator_open.json @@ -1,3 +1,3 @@ { - "open_simpleOrder": "184518" + "open_simpleOrder": "184636" } diff --git a/snapshots/HybridAllocatorTest.json b/snapshots/HybridAllocatorTest.json index 0e4ef65..0881895 100644 --- a/snapshots/HybridAllocatorTest.json +++ b/snapshots/HybridAllocatorTest.json @@ -1,9 +1,9 @@ { - "allocateAndRegister_erc20Token": "185641", - "allocateAndRegister_erc20Token_emptyAmountInput": "186550", - "allocateAndRegister_multipleTokens": "220157", - "allocateAndRegister_nativeToken": "137180", - "allocateAndRegister_nativeToken_emptyAmountInput": "137016", - "allocateAndRegister_second_erc20Token": "112846", - "allocateAndRegister_second_nativeToken": "102816" + "allocateAndRegister_erc20Token": "185714", + "allocateAndRegister_erc20Token_emptyAmountInput": "186623", + "allocateAndRegister_multipleTokens": "220230", + "allocateAndRegister_nativeToken": "137229", + "allocateAndRegister_nativeToken_emptyAmountInput": "137065", + "allocateAndRegister_second_erc20Token": "112919", + "allocateAndRegister_second_nativeToken": "102865" } diff --git a/snapshots/OnChainAllocatorTest.json b/snapshots/OnChainAllocatorTest.json index 618301d..e34d7b9 100644 --- a/snapshots/OnChainAllocatorTest.json +++ b/snapshots/OnChainAllocatorTest.json @@ -1,6 +1,7 @@ { - "allocate_and_delete_expired_allocation": "66012", - "allocate_erc20": "129283", - "allocate_native": "129043", - "allocate_second_erc20": "97295" + "allocateFor_success_withRegistration": "133879", + "allocate_and_delete_expired_allocation": "66039", + "allocate_erc20": "129310", + "allocate_native": "129070", + "allocate_second_erc20": "97322" } diff --git a/src/allocators/HybridAllocator.sol b/src/allocators/HybridAllocator.sol index 14d7d9d..344aef6 100644 --- a/src/allocators/HybridAllocator.sol +++ b/src/allocators/HybridAllocator.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.27; import {SafeTransferLib} from '@solady/utils/SafeTransferLib.sol'; -import {BatchCompact, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; +import {BatchCompact, LOCK_TYPEHASH, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; @@ -18,7 +18,7 @@ contract HybridAllocator is IHybridAllocator { mapping(bytes32 => bool) 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 nonce; + uint96 public nonces; uint256 public signerCount; mapping(address => bool) public signers; @@ -89,18 +89,49 @@ contract HybridAllocator is IHybridAllocator { idsAndAmounts = _actualIdsAndAmounts(idsAndAmounts); (bytes32 claimHash, uint256[] memory registeredAmounts) = _COMPACT.batchDepositAndRegisterFor{value: msg.value}( - recipient, idsAndAmounts, arbiter, ++nonce, expires, typehash, witness + recipient, idsAndAmounts, arbiter, ++nonces, expires, typehash, witness ); // Allocate the claim claims[claimHash] = true; - emit ClaimRegistered(recipient, registeredAmounts, nonce, claimHash); + emit ClaimRegistered(recipient, registeredAmounts, nonces, claimHash); - return (claimHash, registeredAmounts, nonce); + return (claimHash, registeredAmounts, nonces); } + function allocateFor( + address sponsor, + Lock[] calldata commitments, + address arbiter, + uint32 expires, + bytes32 typehash, + bytes32 witness, + bytes calldata /*signature*/ + ) public returns (bytes32 claimHash, uint256 claimNonce) { + for (uint256 i = 0; i < commitments.length; i++) { + uint96 allocatorId = _splitAllocatorId(commitments[i].lockTag); + + // Check allocator id + if (allocatorId != ALLOCATOR_ID) { + revert InvalidAllocatorId(allocatorId, ALLOCATOR_ID); + } + } + + claimHash = _getClaimHash(commitments, arbiter, sponsor, ++nonces, expires, witness, typehash); + + // confirm the claim hash is registered on the compact + if (!_COMPACT.isRegistered(sponsor, claimHash, typehash)) { + revert InvalidRegistration(sponsor, claimHash); + } + + // Allocate the claim + claims[claimHash] = true; + + return (claimHash, nonces); + } /// @inheritdoc IAllocator + function authorizeClaim( bytes32 claimHash, address, /*arbiter*/ @@ -152,6 +183,10 @@ contract HybridAllocator is IHybridAllocator { return _checkSignature(digest, allocatorData); } + function requestNonce(address /*sponsor*/ ) public view returns (uint256 nonce) { + return nonces + 1; + } + function _actualIdsAndAmounts(uint256[2][] memory idsAndAmounts) internal returns (uint256[2][] memory) { uint256 idIndex = 0; uint256 idsLength = idsAndAmounts.length; @@ -228,7 +263,49 @@ contract HybridAllocator is IHybridAllocator { return allocatorId_; } + function _splitAllocatorId(bytes12 lockTag) internal pure returns (uint96) { + uint96 allocatorId; + assembly ("memory-safe") { + allocatorId := shr(164, shl(4, lockTag)) + } + return allocatorId; + } + function _splitToken(uint256 id) internal pure returns (address) { return address(uint160(id)); } + + function _getClaimHash( + Lock[] calldata commitments, + address arbiter, + address sponsor, + uint256 nonce, + uint32 expires, + bytes32 witness, + bytes32 typehash + ) internal pure returns (bytes32 claimHash) { + bytes32 commitmentsHash = _getCommitmentsHash(commitments); + + assembly ("memory-safe") { + let m := mload(0x40) + mstore(m, typehash) + mstore(add(m, 0x20), arbiter) + mstore(add(m, 0x40), sponsor) + mstore(add(m, 0x60), nonce) + mstore(add(m, 0x80), expires) + mstore(add(m, 0xa0), commitmentsHash) + mstore(add(m, 0xc0), witness) + claimHash := keccak256(m, sub(0xe0, mul(iszero(witness), 0x20))) + } + } + + function _getCommitmentsHash(Lock[] memory commitments) internal pure returns (bytes32) { + bytes32[] memory commitmentsHashes = new bytes32[](commitments.length); + for (uint256 i = 0; i < commitments.length; i++) { + commitmentsHashes[i] = keccak256( + abi.encode(LOCK_TYPEHASH, commitments[i].lockTag, commitments[i].token, commitments[i].amount) + ); + } + return keccak256(abi.encodePacked(commitmentsHashes)); + } } diff --git a/src/allocators/HybridERC7683.sol b/src/allocators/HybridERC7683.sol index 25abb39..7789210 100644 --- a/src/allocators/HybridERC7683.sol +++ b/src/allocators/HybridERC7683.sol @@ -211,7 +211,7 @@ contract HybridERC7683 is HybridAllocator, IHybridERC7683 { BatchCompact memory batchCompact = BatchCompact({ arbiter: orderData.arbiter, sponsor: order.user, - nonce: nonce + 1, + nonce: nonces + 1, expires: order.openDeadline, commitments: locks }); @@ -236,7 +236,7 @@ contract HybridERC7683 is HybridAllocator, IHybridERC7683 { BatchCompact memory batchCompact = BatchCompact({ arbiter: orderData.arbiter, sponsor: msg.sender, - nonce: nonce + 1, // nonce is incremented by 1 when the claim is registered + nonce: nonces + 1, // nonce is incremented by 1 when the claim is registered expires: expires, commitments: locks }); diff --git a/src/allocators/OnChainAllocator.sol b/src/allocators/OnChainAllocator.sol index a656caf..6d27ed4 100644 --- a/src/allocators/OnChainAllocator.sol +++ b/src/allocators/OnChainAllocator.sol @@ -194,6 +194,10 @@ contract OnChainAllocator is IOnChainAllocator { return false; } + function requestNonce(address sponsor) public view returns (uint256 nonce) { + return nonces[_toNonceId(address(0), sponsor)] + 1; + } + function _allocate( address sponsor, Lock[] calldata commitments, diff --git a/src/interfaces/IHybridAllocator.sol b/src/interfaces/IHybridAllocator.sol index 36205cc..ddf8459 100644 --- a/src/interfaces/IHybridAllocator.sol +++ b/src/interfaces/IHybridAllocator.sol @@ -13,6 +13,7 @@ interface IHybridAllocator is IAllocator { error InvalidSigner(); error LastSigner(); error InvalidValue(uint256 value, uint256 expectedValue); + error InvalidRegistration(address sponsor, bytes32 claimHash); event ClaimRegistered(address indexed sponsor, uint256[] registeredAmounts, uint256 nonce, bytes32 claimHash); diff --git a/test/HybridAllocator.t.sol b/test/HybridAllocator.t.sol index 0624a1e..a0a23cf 100644 --- a/test/HybridAllocator.t.sol +++ b/test/HybridAllocator.t.sol @@ -60,7 +60,7 @@ contract HybridAllocatorTest is Test, TestHelper { } function test_checkNonce() public view { - assertEq(allocator.nonce(), 0); + assertEq(allocator.nonces(), 0); } function test_checkSignerCount() public view { @@ -280,13 +280,13 @@ contract HybridAllocatorTest is Test, TestHelper { idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); idsAndAmounts[0][1] = 0; - assertEq(allocator.nonce(), 0); + assertEq(allocator.nonces(), 0); // Register first claim allocator.allocateAndRegister{value: 5e17}( user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, '' ); - assertEq(allocator.nonce(), 1); + assertEq(allocator.nonces(), 1); // Register second claim (bytes32 claimHash, uint256[] memory registeredAmounts,) = allocator.allocateAndRegister{value: 5e17}( @@ -299,7 +299,7 @@ contract HybridAllocatorTest is Test, TestHelper { assertEq(registeredAmounts[0], 5e17); assertEq(registeredAmounts.length, 1); - assertEq(allocator.nonce(), 2); + assertEq(allocator.nonces(), 2); } function test_allocateAndRegister_checkNonceIncrements_erc20Token() public { @@ -307,7 +307,7 @@ contract HybridAllocatorTest is Test, TestHelper { idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); idsAndAmounts[0][1] = 0; - assertEq(allocator.nonce(), 0); + assertEq(allocator.nonces(), 0); // Provide tokens vm.prank(user); @@ -316,7 +316,7 @@ contract HybridAllocatorTest is Test, TestHelper { // Register first claim allocator.allocateAndRegister(user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); - assertEq(allocator.nonce(), 1); + assertEq(allocator.nonces(), 1); // Provide tokens vm.prank(user); @@ -335,7 +335,7 @@ contract HybridAllocatorTest is Test, TestHelper { assertEq(usdc.balanceOf(address(compact)), defaultAmount); assertEq(compact.balanceOf(address(user), idsAndAmounts[0][0]), defaultAmount); - assertEq(allocator.nonce(), 2); + assertEq(allocator.nonces(), 2); } function test_allocateAndRegister_checkClaimHashNoWitness() public { diff --git a/test/HybridERC7683.t.sol b/test/HybridERC7683.t.sol index a9874d4..ff34d16 100644 --- a/test/HybridERC7683.t.sol +++ b/test/HybridERC7683.t.sol @@ -1119,7 +1119,7 @@ contract HybridERC7683_qualificationTypehash is MocksSetup { contract HybridERC7683_hybridAllocatorInheritance is MocksSetup { function test_inheritsHybridAllocatorFunctionality() public view { // Test that it properly inherits from HybridAllocator - assertEq(hybridERC7683Allocator.nonce(), 0); + assertEq(hybridERC7683Allocator.nonces(), 0); assertEq(hybridERC7683Allocator.signerCount(), 1); assertTrue(hybridERC7683Allocator.signers(signer)); assertEq(hybridERC7683Allocator.ALLOCATOR_ID(), _toAllocatorId(address(hybridERC7683Allocator))); diff --git a/test/OnChainAllocator.t.sol b/test/OnChainAllocator.t.sol index e01b800..5871bc6 100644 --- a/test/OnChainAllocator.t.sol +++ b/test/OnChainAllocator.t.sol @@ -569,7 +569,8 @@ contract OnChainAllocatorTest is Test, TestHelper { ); } - function test_allocateFor_success_noSignature(address relayer) public { + function test_allocateFor_success_noSignature() public { + address relayer = makeAddr('relayer'); Lock[] memory commitments = new Lock[](1); commitments[0] = _makeLock(address(0), defaultAmount); @@ -606,6 +607,7 @@ contract OnChainAllocatorTest is Test, TestHelper { bytes32(0), '' // empty signature triggers the "registered" code path ); + vm.snapshotGasLastCall('allocateFor_success_withRegistration'); // Assertions assertEq(returnedHash, claimHash); From c011063367b7cc227a69a1f9860d068ee62275eb Mon Sep 17 00:00:00 2001 From: mgretzke Date: Sat, 9 Aug 2025 17:20:19 +0200 Subject: [PATCH 48/63] improved contracts --- snapshots/ERC7683Allocator_open.json | 2 +- snapshots/ERC7683Allocator_openFor.json | 4 +- snapshots/HybridAllocatorTest.json | 14 +-- snapshots/OnChainAllocatorTest.json | 10 +- src/allocators/HybridAllocator.sol | 123 +++++++++++++++++----- src/allocators/OnChainAllocator.sol | 134 +++++++++++++++++++++--- src/interfaces/IHybridAllocator.sol | 10 +- src/interfaces/IOnChainAllocator.sol | 15 +-- test/OnChainAllocator.t.sol | 4 +- 9 files changed, 253 insertions(+), 63 deletions(-) diff --git a/snapshots/ERC7683Allocator_open.json b/snapshots/ERC7683Allocator_open.json index 0286f70..c7de9f5 100644 --- a/snapshots/ERC7683Allocator_open.json +++ b/snapshots/ERC7683Allocator_open.json @@ -1,3 +1,3 @@ { - "open_simpleOrder": "184636" + "open_simpleOrder": "184615" } diff --git a/snapshots/ERC7683Allocator_openFor.json b/snapshots/ERC7683Allocator_openFor.json index c7773a8..fc03a16 100644 --- a/snapshots/ERC7683Allocator_openFor.json +++ b/snapshots/ERC7683Allocator_openFor.json @@ -1,4 +1,4 @@ { - "openFor_simpleOrder_relayed": "166894", - "openFor_simpleOrder_userHimself": "166926" + "openFor_simpleOrder_relayed": "166824", + "openFor_simpleOrder_userHimself": "166856" } diff --git a/snapshots/HybridAllocatorTest.json b/snapshots/HybridAllocatorTest.json index 0881895..9404a76 100644 --- a/snapshots/HybridAllocatorTest.json +++ b/snapshots/HybridAllocatorTest.json @@ -1,9 +1,9 @@ { - "allocateAndRegister_erc20Token": "185714", - "allocateAndRegister_erc20Token_emptyAmountInput": "186623", - "allocateAndRegister_multipleTokens": "220230", - "allocateAndRegister_nativeToken": "137229", - "allocateAndRegister_nativeToken_emptyAmountInput": "137065", - "allocateAndRegister_second_erc20Token": "112919", - "allocateAndRegister_second_nativeToken": "102865" + "allocateAndRegister_erc20Token": "187766", + "allocateAndRegister_erc20Token_emptyAmountInput": "188676", + "allocateAndRegister_multipleTokens": "223672", + "allocateAndRegister_nativeToken": "139305", + "allocateAndRegister_nativeToken_emptyAmountInput": "139141", + "allocateAndRegister_second_erc20Token": "114972", + "allocateAndRegister_second_nativeToken": "104941" } diff --git a/snapshots/OnChainAllocatorTest.json b/snapshots/OnChainAllocatorTest.json index e34d7b9..feaafe8 100644 --- a/snapshots/OnChainAllocatorTest.json +++ b/snapshots/OnChainAllocatorTest.json @@ -1,7 +1,7 @@ { - "allocateFor_success_withRegistration": "133879", - "allocate_and_delete_expired_allocation": "66039", - "allocate_erc20": "129310", - "allocate_native": "129070", - "allocate_second_erc20": "97322" + "allocateFor_success_withRegistration": "133866", + "allocate_and_delete_expired_allocation": "66034", + "allocate_erc20": "129305", + "allocate_native": "129065", + "allocate_second_erc20": "97317" } diff --git a/src/allocators/HybridAllocator.sol b/src/allocators/HybridAllocator.sol index 344aef6..87eeab1 100644 --- a/src/allocators/HybridAllocator.sol +++ b/src/allocators/HybridAllocator.sol @@ -6,6 +6,8 @@ import {SafeTransferLib} from '@solady/utils/SafeTransferLib.sol'; import {BatchCompact, LOCK_TYPEHASH, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +import {ERC6909} from '@solady/tokens/ERC6909.sol'; import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; import {IHybridAllocator} from 'src/interfaces/IHybridAllocator.sol'; @@ -14,6 +16,8 @@ contract HybridAllocator is IHybridAllocator { uint96 public immutable ALLOCATOR_ID; ITheCompact internal immutable _COMPACT; bytes32 internal immutable _COMPACT_DOMAIN_SEPARATOR; + // bytes4(keccak256('prepareAllocation(address,uint256[2][],address,uint256,bytes32,bytes32,bytes)')); + bytes4 public constant PREPARE_ALLOCATION_SELECTOR = 0x7ef6597a; mapping(bytes32 => bool) internal claims; @@ -92,46 +96,119 @@ contract HybridAllocator is IHybridAllocator { recipient, idsAndAmounts, arbiter, ++nonces, expires, typehash, witness ); + Lock[] memory commitments = new Lock[](idsAndAmounts.length); + for (uint256 i = 0; i < idsAndAmounts.length; i++) { + commitments[i] = Lock({ + lockTag: bytes12(bytes32(idsAndAmounts[i][0])), + token: address(uint160(idsAndAmounts[i][0])), + amount: registeredAmounts[i] + }); + } + // Allocate the claim claims[claimHash] = true; - emit ClaimRegistered(recipient, registeredAmounts, nonces, claimHash); + emit Allocated(recipient, commitments, nonces, expires, claimHash); return (claimHash, registeredAmounts, nonces); } - function allocateFor( - address sponsor, - Lock[] calldata commitments, + function prepareAllocation( + address recipient, + uint256[2][] calldata idsAndAmounts, address arbiter, - uint32 expires, + uint256 expires, bytes32 typehash, bytes32 witness, - bytes calldata /*signature*/ - ) public returns (bytes32 claimHash, uint256 claimNonce) { - for (uint256 i = 0; i < commitments.length; i++) { - uint96 allocatorId = _splitAllocatorId(commitments[i].lockTag); + bytes calldata /* orderData */ + ) external returns (uint256 nonce) { + uint256[] memory ids = new uint256[](idsAndAmounts.length); + for (uint256 i = 0; i < idsAndAmounts.length; i++) { + uint256 id = idsAndAmounts[i][0]; + // Store Id for the identifier + ids[i] = id; + + // Store the current balance to calculate the deposited amounts in `executeAllocation` + uint256 currentBalance = ERC6909(address(_COMPACT)).balanceOf(recipient, id); + assembly ("memory-safe") { + tstore(or(PREPARE_ALLOCATION_SELECTOR, id), currentBalance) + } + } - // Check allocator id - if (allocatorId != ALLOCATOR_ID) { - revert InvalidAllocatorId(allocatorId, ALLOCATOR_ID); + // Store the nonce for the identifier to ensure the same data is used in `executeAllocation` + bytes32 identifier = + keccak256(abi.encode(PREPARE_ALLOCATION_SELECTOR, recipient, ids, arbiter, expires, typehash, witness)); + nonce = nonces + 1; + assembly ("memory-safe") { + tstore(identifier, nonce) + } + + return nonce; + } + + function executeAllocation( + address recipient, + uint256[2][] calldata idsAndAmounts, + address arbiter, + uint256 expires, + bytes32 typehash, + bytes32 witness, + bytes calldata /* orderData */ + ) external { + uint256[] memory ids = new uint256[](idsAndAmounts.length); + Lock[] memory commitments = new Lock[](idsAndAmounts.length); + bytes32[] memory commitmentHashes = new bytes32[](idsAndAmounts.length); + + // Check actual balance changes + for (uint256 i = 0; i < idsAndAmounts.length; i++) { + uint256 id = idsAndAmounts[i][0]; + // Store Id for the identifier + ids[i] = id; + + // calculate Balance + uint256 oldBalance; + assembly ("memory-safe") { + oldBalance := tload(or(PREPARE_ALLOCATION_SELECTOR, id)) } + uint256 newBalance = ERC6909(address(_COMPACT)).balanceOf(recipient, id); + if (newBalance <= oldBalance) { + revert InvalidValue(newBalance, oldBalance + 1); + } + + // Create commitments + bytes12 lockTag = bytes12(bytes32(id)); + address token = address(uint160(id)); + uint256 amount = newBalance - oldBalance; + commitmentHashes[i] = keccak256(abi.encode(LOCK_TYPEHASH, lockTag, token, amount)); + commitments[i] = Lock({lockTag: lockTag, token: token, amount: amount}); } - claimHash = _getClaimHash(commitments, arbiter, sponsor, ++nonces, expires, witness, typehash); + // Ensure preparation was called with the same data + bytes32 identifier = + keccak256(abi.encode(PREPARE_ALLOCATION_SELECTOR, recipient, ids, arbiter, expires, typehash, witness)); + uint256 nonce; + assembly ("memory-safe") { + nonce := tload(identifier) + } + if (nonce != ++nonces) { + revert InvalidPreparation(); + } - // confirm the claim hash is registered on the compact - if (!_COMPACT.isRegistered(sponsor, claimHash, typehash)) { - revert InvalidRegistration(sponsor, claimHash); + // Check for a valid registration with the actual data + bytes32 claimHash = _getClaimHash( + arbiter, recipient, nonce, expires, keccak256(abi.encodePacked(commitmentHashes)), witness, typehash + ); + if (!_COMPACT.isRegistered(recipient, claimHash, typehash)) { + revert InvalidRegistration(recipient, claimHash); } // Allocate the claim claims[claimHash] = true; - return (claimHash, nonces); + emit Allocated(recipient, commitments, nonce, expires, claimHash); } - /// @inheritdoc IAllocator + /// @inheritdoc IAllocator function authorizeClaim( bytes32 claimHash, address, /*arbiter*/ @@ -183,10 +260,6 @@ contract HybridAllocator is IHybridAllocator { return _checkSignature(digest, allocatorData); } - function requestNonce(address /*sponsor*/ ) public view returns (uint256 nonce) { - return nonces + 1; - } - function _actualIdsAndAmounts(uint256[2][] memory idsAndAmounts) internal returns (uint256[2][] memory) { uint256 idIndex = 0; uint256 idsLength = idsAndAmounts.length; @@ -276,16 +349,14 @@ contract HybridAllocator is IHybridAllocator { } function _getClaimHash( - Lock[] calldata commitments, address arbiter, address sponsor, uint256 nonce, - uint32 expires, + uint256 expires, + bytes32 commitmentsHash, bytes32 witness, bytes32 typehash ) internal pure returns (bytes32 claimHash) { - bytes32 commitmentsHash = _getCommitmentsHash(commitments); - assembly ("memory-safe") { let m := mload(0x40) mstore(m, typehash) diff --git a/src/allocators/OnChainAllocator.sol b/src/allocators/OnChainAllocator.sol index 6d27ed4..b76c4bd 100644 --- a/src/allocators/OnChainAllocator.sol +++ b/src/allocators/OnChainAllocator.sol @@ -18,6 +18,8 @@ contract OnChainAllocator is IOnChainAllocator { address public immutable COMPACT_CONTRACT; bytes32 public immutable COMPACT_DOMAIN_SEPARATOR; uint96 public immutable ALLOCATOR_ID; + // bytes4(keccak256('prepareAllocation(address,uint256[2][],address,uint256,bytes32,bytes32,bytes)')); + bytes4 public constant PREPARE_ALLOCATION_SELECTOR = 0x7ef6597a; mapping(bytes32 tokenHash => Allocation[] allocations) internal _allocations; @@ -43,7 +45,7 @@ contract OnChainAllocator is IOnChainAllocator { { (claimHash, claimNonce) = _allocate(msg.sender, commitments, arbiter, expires, typehash, witness); - emit AllocationRegistered(msg.sender, claimHash, claimNonce, expires, commitments); + emit Allocated(msg.sender, commitments, claimNonce, expires, claimHash); } /// @inheritdoc IOnChainAllocator @@ -73,7 +75,7 @@ contract OnChainAllocator is IOnChainAllocator { revert InvalidRegistration(sponsor, claimHash); } } - emit AllocationRegistered(sponsor, claimHash, claimNonce, expires, commitments); + emit Allocated(sponsor, commitments, claimNonce, expires, claimHash); } /// @inheritdoc IOnChainAllocator @@ -112,6 +114,8 @@ contract OnChainAllocator is IOnChainAllocator { recipient, idsAndAmounts, arbiter, nonce, expires, typehash, witness ); + Lock[] memory registeredCommitments = commitments; + // Store the allocation for (uint256 i = 0; i < registeredAmounts.length; i++) { bytes32 tokenHash = _getTokenHash(commitments[i], recipient); @@ -119,13 +123,124 @@ contract OnChainAllocator is IOnChainAllocator { Allocation memory allocation = Allocation({expires: expires, amount: uint224(registeredAmounts[i]), claimHash: claimHash}); _allocations[tokenHash].push(allocation); + + // Update the allocations with the actual registered amounts + registeredCommitments[i].amount = registeredAmounts[i]; } - emit AllocationRegistered(recipient, claimHash, nonce, expires, commitments); + emit Allocated(recipient, registeredCommitments, nonce, expires, claimHash); return (claimHash, registeredAmounts, nonce); } + function prepareAllocation( + address recipient, + uint256[2][] calldata idsAndAmounts, + address arbiter, + uint256 expires, + bytes32 typehash, + bytes32 witness, + bytes calldata /* orderData */ + ) external returns (uint256 nonce) { + uint256[] memory ids = new uint256[](idsAndAmounts.length); + for (uint256 i = 0; i < idsAndAmounts.length; i++) { + uint256 id = idsAndAmounts[i][0]; + ids[i] = id; + uint256 currentBalance = ERC6909(COMPACT_CONTRACT).balanceOf(recipient, id); + assembly ("memory-safe") { + tstore(or(PREPARE_ALLOCATION_SELECTOR, id), currentBalance) + } + + // Check the amount fits in the supported range + if (idsAndAmounts[i][1] > type(uint224).max) { + revert InvalidAmount(idsAndAmounts[i][1]); + } + } + + // Store the nonce for the identifier to ensure the same data is used in `executeAllocation` and protect against replay attacks + bytes32 identifier = keccak256( + abi.encode(PREPARE_ALLOCATION_SELECTOR, recipient, ids, arbiter, uint32(expires), typehash, witness) + ); + nonce = nonces[_toNonceId(msg.sender, recipient)] + 1; + assembly ("memory-safe") { + tstore(identifier, nonce) + } + + return nonce; + } + + function executeAllocation( + address recipient, + uint256[2][] calldata idsAndAmounts, + address arbiter, + uint256 expires, + bytes32 typehash, + bytes32 witness, + bytes calldata /* orderData */ + ) external { + uint256[] memory ids = new uint256[](idsAndAmounts.length); + Lock[] memory commitments = new Lock[](idsAndAmounts.length); + bytes32[] memory commitmentHashes = new bytes32[](idsAndAmounts.length); + expires = uint32(expires); + + // Check actual balance changes + for (uint256 i = 0; i < idsAndAmounts.length; i++) { + uint256 id = idsAndAmounts[i][0]; + ids[i] = id; + uint256 oldBalance; + assembly ("memory-safe") { + oldBalance := tload(or(PREPARE_ALLOCATION_SELECTOR, id)) + tstore(or(PREPARE_ALLOCATION_SELECTOR, id), 0) + } + uint256 newBalance = ERC6909(COMPACT_CONTRACT).balanceOf(recipient, id); + if (newBalance <= oldBalance) { + revert InsufficientBalance(recipient, id, newBalance, oldBalance + 1); + } + uint256 amount = newBalance - oldBalance; + + // Check the amount fits in the supported range + if (amount > type(uint224).max) { + revert InvalidAmount(amount); + } + + // Create commitments + bytes12 lockTag = bytes12(bytes32(id)); + address token = address(uint160(id)); + commitmentHashes[i] = keccak256(abi.encode(LOCK_TYPEHASH, lockTag, token, amount)); + commitments[i] = Lock({lockTag: lockTag, token: token, amount: amount}); + } + + // Check preparation was called with the same data + bytes32 identifier = + keccak256(abi.encode(PREPARE_ALLOCATION_SELECTOR, recipient, ids, arbiter, expires, typehash, witness)); + uint256 nonce; + assembly ("memory-safe") { + nonce := tload(identifier) + tstore(identifier, 0) + } + if (nonce != ++nonces[_toNonceId(msg.sender, recipient)]) { + revert InvalidPreparation(); + } + + // Check for a valid registration with the actual data + bytes32 claimHash = _getClaimHash( + arbiter, recipient, nonce, expires, keccak256(abi.encodePacked(commitmentHashes)), witness, typehash + ); + if (!ITheCompact(COMPACT_CONTRACT).isRegistered(recipient, claimHash, typehash)) { + revert InvalidRegistration(recipient, claimHash); + } + + // Allocate the claim + for (uint256 i = 0; i < idsAndAmounts.length; i++) { + bytes32 tokenHash = _getTokenHash(idsAndAmounts[i][0], recipient); + Allocation memory allocation = + Allocation({expires: uint32(expires), amount: uint224(commitments[i].amount), claimHash: claimHash}); + _allocations[tokenHash].push(allocation); + } + + emit Allocated(recipient, commitments, nonce, expires, claimHash); + } + /// @inheritdoc IAllocator function attest(address, address from_, address, uint256 id_, uint256 amount_) external returns (bytes4) { // Can be called by anyone, as this will only clean up expired allocations. @@ -194,10 +309,6 @@ contract OnChainAllocator is IOnChainAllocator { return false; } - function requestNonce(address sponsor) public view returns (uint256 nonce) { - return nonces[_toNonceId(address(0), sponsor)] + 1; - } - function _allocate( address sponsor, Lock[] calldata commitments, @@ -211,7 +322,8 @@ contract OnChainAllocator is IOnChainAllocator { } nonce = ++nonces[_toNonceId(address(0), sponsor)]; // address(0) as caller allows anyone to relay - claimHash = _getClaimHash(commitments, arbiter, sponsor, nonce, expires, witness, typehash); + bytes32 commitmentsHash = _getCommitmentsHash(commitments); + claimHash = _getClaimHash(arbiter, sponsor, nonce, expires, commitmentsHash, witness, typehash); uint256 minResetPeriod = type(uint256).max; for (uint256 i = 0; i < commitments.length; i++) { @@ -441,16 +553,14 @@ contract OnChainAllocator is IOnChainAllocator { } function _getClaimHash( - Lock[] calldata commitments, address arbiter, address sponsor, uint256 nonce, - uint32 expires, + uint256 expires, + bytes32 commitmentsHash, bytes32 witness, bytes32 typehash ) internal pure returns (bytes32 claimHash) { - bytes32 commitmentsHash = _getCommitmentsHash(commitments); - assembly ("memory-safe") { let m := mload(0x40) mstore(m, typehash) diff --git a/src/interfaces/IHybridAllocator.sol b/src/interfaces/IHybridAllocator.sol index ddf8459..1251dea 100644 --- a/src/interfaces/IHybridAllocator.sol +++ b/src/interfaces/IHybridAllocator.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.27; import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; +import {Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; interface IHybridAllocator is IAllocator { error Unsupported(); @@ -14,8 +15,15 @@ interface IHybridAllocator is IAllocator { error LastSigner(); error InvalidValue(uint256 value, uint256 expectedValue); error InvalidRegistration(address sponsor, bytes32 claimHash); + error InvalidPreparation(); - event ClaimRegistered(address indexed sponsor, uint256[] registeredAmounts, uint256 nonce, bytes32 claimHash); + /// @notice Emitted when a tokens are successfully allocated + /// @param sponsor The address of the sponsor + /// @param commitments The commitments of the allocations + /// @param nonce The nonce of the allocation + /// @param expires The expiration of the allocation + /// @param claimHash The hash of the allocation + event Allocated(address indexed sponsor, Lock[] commitments, uint256 nonce, uint256 expires, bytes32 claimHash); /** * @notice Add an offchain signer to the allocator. diff --git a/src/interfaces/IOnChainAllocator.sol b/src/interfaces/IOnChainAllocator.sol index 07b0acd..677f250 100644 --- a/src/interfaces/IOnChainAllocator.sol +++ b/src/interfaces/IOnChainAllocator.sol @@ -49,15 +49,16 @@ interface IOnChainAllocator is IAllocator { /// @notice Thrown if the claim hash is not registered on the compact error InvalidRegistration(address sponsor, bytes32 claimHash); - /// @notice Emitted when a lock is successfully created + /// @notice Thrown if the preparation was not called within the same transaction or the data is not the same + error InvalidPreparation(); + + /// @notice Emitted when a tokens are successfully allocated /// @param sponsor The address of the sponsor - /// @param claimHash The hash of the claim - /// @param nonce The nonce of the claim - /// @param expires The expiration of the claim /// @param commitments The commitments of the allocations - event AllocationRegistered( - address indexed sponsor, bytes32 indexed claimHash, uint256 indexed nonce, uint256 expires, Lock[] commitments - ); + /// @param nonce The nonce of the allocation + /// @param expires The expiration of the allocation + /// @param claimHash The hash of the allocation + event Allocated(address indexed sponsor, Lock[] commitments, uint256 nonce, uint256 expires, bytes32 claimHash); /// @notice Registers an allocation for a set of tokens /// @param commitments The commitments of the allocations diff --git a/test/OnChainAllocator.t.sol b/test/OnChainAllocator.t.sol index 5871bc6..d28ff1b 100644 --- a/test/OnChainAllocator.t.sol +++ b/test/OnChainAllocator.t.sol @@ -304,7 +304,7 @@ contract OnChainAllocatorTest is Test, TestHelper { vm.prank(user); vm.expectEmit(true, true, true, true); - emit IOnChainAllocator.AllocationRegistered(user, claimHash, 1, defaultExpiration, commitments); + emit IOnChainAllocator.Allocated(user, commitments, 1, defaultExpiration, claimHash); (bytes32 returnedClaimHash, uint256 nonce) = allocator.allocate(commitments, arbiter, defaultExpiration, typehash, witness); @@ -336,7 +336,7 @@ contract OnChainAllocatorTest is Test, TestHelper { // expect a successful second allocation claimHash = _createClaimHash(user, arbiter, 2, defaultExpiration, commitments, witness); vm.expectEmit(true, true, true, true); - emit IOnChainAllocator.AllocationRegistered(user, claimHash, 2, defaultExpiration, commitments); + emit IOnChainAllocator.Allocated(user, commitments, 2, defaultExpiration, claimHash); } (claimHash, nonce) = allocator.allocate(commitments, arbiter, defaultExpiration, typehash, witness); From ad42c2959cfdd61608736efc0855104827f64bb4 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Sun, 10 Aug 2025 00:07:14 +0200 Subject: [PATCH 49/63] eliminate double logic + fixes + tests --- snapshots/ERC7683Allocator_open.json | 2 +- snapshots/ERC7683Allocator_openFor.json | 4 +- snapshots/HybridAllocatorTest.json | 11 +- snapshots/OnChainAllocatorTest.json | 12 +- src/allocators/HybridAllocator.sol | 152 +------------ src/allocators/HybridERC7683.sol | 13 +- src/allocators/OnChainAllocator.sol | 284 ++++++++---------------- src/allocators/lib/AllocatorLib.sol | 215 ++++++++++++++++++ src/interfaces/IOnChainAllocation.sol | 60 +++++ src/test/OnChainAllocationCaller.sol | 53 +++++ test/HybridAllocator.t.sol | 151 +++++++++++++ test/OnChainAllocator.t.sol | 258 +++++++++++++++++++++ 12 files changed, 864 insertions(+), 351 deletions(-) create mode 100644 src/allocators/lib/AllocatorLib.sol create mode 100644 src/interfaces/IOnChainAllocation.sol create mode 100644 src/test/OnChainAllocationCaller.sol diff --git a/snapshots/ERC7683Allocator_open.json b/snapshots/ERC7683Allocator_open.json index c7de9f5..8afb718 100644 --- a/snapshots/ERC7683Allocator_open.json +++ b/snapshots/ERC7683Allocator_open.json @@ -1,3 +1,3 @@ { - "open_simpleOrder": "184615" + "open_simpleOrder": "184684" } diff --git a/snapshots/ERC7683Allocator_openFor.json b/snapshots/ERC7683Allocator_openFor.json index fc03a16..5cd0f22 100644 --- a/snapshots/ERC7683Allocator_openFor.json +++ b/snapshots/ERC7683Allocator_openFor.json @@ -1,4 +1,4 @@ { - "openFor_simpleOrder_relayed": "166824", - "openFor_simpleOrder_userHimself": "166856" + "openFor_simpleOrder_relayed": "166923", + "openFor_simpleOrder_userHimself": "166957" } diff --git a/snapshots/HybridAllocatorTest.json b/snapshots/HybridAllocatorTest.json index 9404a76..f98edfa 100644 --- a/snapshots/HybridAllocatorTest.json +++ b/snapshots/HybridAllocatorTest.json @@ -1,9 +1,10 @@ { - "allocateAndRegister_erc20Token": "187766", - "allocateAndRegister_erc20Token_emptyAmountInput": "188676", - "allocateAndRegister_multipleTokens": "223672", + "allocateAndRegister_erc20Token": "187769", + "allocateAndRegister_erc20Token_emptyAmountInput": "188679", + "allocateAndRegister_multipleTokens": "223675", "allocateAndRegister_nativeToken": "139305", "allocateAndRegister_nativeToken_emptyAmountInput": "139141", - "allocateAndRegister_second_erc20Token": "114972", - "allocateAndRegister_second_nativeToken": "104941" + "allocateAndRegister_second_erc20Token": "114975", + "allocateAndRegister_second_nativeToken": "104941", + "hybrid_execute_single": "174260" } diff --git a/snapshots/OnChainAllocatorTest.json b/snapshots/OnChainAllocatorTest.json index feaafe8..aa74cd1 100644 --- a/snapshots/OnChainAllocatorTest.json +++ b/snapshots/OnChainAllocatorTest.json @@ -1,7 +1,9 @@ { - "allocateFor_success_withRegistration": "133866", - "allocate_and_delete_expired_allocation": "66034", - "allocate_erc20": "129305", - "allocate_native": "129065", - "allocate_second_erc20": "97317" + "allocateFor_success_withRegistration": "133994", + "allocate_and_delete_expired_allocation": "66140", + "allocate_erc20": "129411", + "allocate_native": "129171", + "allocate_second_erc20": "97423", + "onchain_execute_double": "347964", + "onchain_execute_single": "220065" } diff --git a/src/allocators/HybridAllocator.sol b/src/allocators/HybridAllocator.sol index 87eeab1..e421932 100644 --- a/src/allocators/HybridAllocator.sol +++ b/src/allocators/HybridAllocator.sol @@ -7,6 +7,7 @@ import {BatchCompact, LOCK_TYPEHASH, Lock} from '@uniswap/the-compact/types/EIP7 import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {AllocatorLib as AL} from './lib/AllocatorLib.sol'; import {ERC6909} from '@solady/tokens/ERC6909.sol'; import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; @@ -122,28 +123,8 @@ contract HybridAllocator is IHybridAllocator { bytes32 witness, bytes calldata /* orderData */ ) external returns (uint256 nonce) { - uint256[] memory ids = new uint256[](idsAndAmounts.length); - for (uint256 i = 0; i < idsAndAmounts.length; i++) { - uint256 id = idsAndAmounts[i][0]; - // Store Id for the identifier - ids[i] = id; - - // Store the current balance to calculate the deposited amounts in `executeAllocation` - uint256 currentBalance = ERC6909(address(_COMPACT)).balanceOf(recipient, id); - assembly ("memory-safe") { - tstore(or(PREPARE_ALLOCATION_SELECTOR, id), currentBalance) - } - } - - // Store the nonce for the identifier to ensure the same data is used in `executeAllocation` - bytes32 identifier = - keccak256(abi.encode(PREPARE_ALLOCATION_SELECTOR, recipient, ids, arbiter, expires, typehash, witness)); nonce = nonces + 1; - assembly ("memory-safe") { - tstore(identifier, nonce) - } - - return nonce; + AL.prepareAllocation(address(_COMPACT), nonce, recipient, idsAndAmounts, arbiter, expires, typehash, witness); } function executeAllocation( @@ -155,52 +136,11 @@ contract HybridAllocator is IHybridAllocator { bytes32 witness, bytes calldata /* orderData */ ) external { - uint256[] memory ids = new uint256[](idsAndAmounts.length); - Lock[] memory commitments = new Lock[](idsAndAmounts.length); - bytes32[] memory commitmentHashes = new bytes32[](idsAndAmounts.length); + uint256 nonce = ++nonces; - // Check actual balance changes - for (uint256 i = 0; i < idsAndAmounts.length; i++) { - uint256 id = idsAndAmounts[i][0]; - // Store Id for the identifier - ids[i] = id; - - // calculate Balance - uint256 oldBalance; - assembly ("memory-safe") { - oldBalance := tload(or(PREPARE_ALLOCATION_SELECTOR, id)) - } - uint256 newBalance = ERC6909(address(_COMPACT)).balanceOf(recipient, id); - if (newBalance <= oldBalance) { - revert InvalidValue(newBalance, oldBalance + 1); - } - - // Create commitments - bytes12 lockTag = bytes12(bytes32(id)); - address token = address(uint160(id)); - uint256 amount = newBalance - oldBalance; - commitmentHashes[i] = keccak256(abi.encode(LOCK_TYPEHASH, lockTag, token, amount)); - commitments[i] = Lock({lockTag: lockTag, token: token, amount: amount}); - } - - // Ensure preparation was called with the same data - bytes32 identifier = - keccak256(abi.encode(PREPARE_ALLOCATION_SELECTOR, recipient, ids, arbiter, expires, typehash, witness)); - uint256 nonce; - assembly ("memory-safe") { - nonce := tload(identifier) - } - if (nonce != ++nonces) { - revert InvalidPreparation(); - } - - // Check for a valid registration with the actual data - bytes32 claimHash = _getClaimHash( - arbiter, recipient, nonce, expires, keccak256(abi.encodePacked(commitmentHashes)), witness, typehash + (bytes32 claimHash, Lock[] memory commitments) = AL.executeAllocation( + address(_COMPACT), nonce, recipient, idsAndAmounts, arbiter, expires, typehash, witness ); - if (!_COMPACT.isRegistered(recipient, claimHash, typehash)) { - revert InvalidRegistration(recipient, claimHash); - } // Allocate the claim claims[claimHash] = true; @@ -268,10 +208,10 @@ contract HybridAllocator is IHybridAllocator { } // Check for native token - Native tokens must always be the first id - if (_splitToken(idsAndAmounts[0][0]) == address(0)) { + if (AL.splitToken(idsAndAmounts[0][0]) == address(0)) { // Check allocator id - if (_splitAllocatorId(idsAndAmounts[0][0]) != ALLOCATOR_ID) { - revert InvalidAllocatorId(_splitAllocatorId(idsAndAmounts[0][0]), ALLOCATOR_ID); + if (AL.splitAllocatorId(idsAndAmounts[0][0]) != ALLOCATOR_ID) { + revert InvalidAllocatorId(AL.splitAllocatorId(idsAndAmounts[0][0]), ALLOCATOR_ID); } if (idsAndAmounts[0][1] != 0 && msg.value != idsAndAmounts[0][1]) { revert InvalidValue(msg.value, idsAndAmounts[0][1]); @@ -282,7 +222,7 @@ contract HybridAllocator is IHybridAllocator { } for (; idIndex < idsLength; idIndex++) { - (uint96 allocatorId, address token) = _splitId(idsAndAmounts[idIndex][0]); + (uint96 allocatorId, address token) = AL.splitId(idsAndAmounts[idIndex][0]); // Check allocator id if (allocatorId != ALLOCATOR_ID) { @@ -303,80 +243,8 @@ contract HybridAllocator is IHybridAllocator { } function _checkSignature(bytes32 digest, bytes calldata signature) internal view returns (bool) { - bytes32 r; - bytes32 s; - uint8 v; - - if (signature.length == 65) { - (r, s) = abi.decode(signature, (bytes32, bytes32)); - v = uint8(signature[64]); - } else if (signature.length == 64) { - bytes32 vs; - (r, vs) = abi.decode(signature, (bytes32, bytes32)); - v = uint8(uint256(vs >> 255) + 27); - s = vs << 1 >> 1; - } else { - return false; - } - // Check if the signer is an authorized allocator address - address signer = ecrecover(digest, v, r, s); + address signer = AL.recoverSigner(digest, signature); return signers[signer] && signer != address(0); } - - function _splitId(uint256 id) internal pure returns (uint96 allocatorId_, address token_) { - return (_splitAllocatorId(id), _splitToken(id)); - } - - function _splitAllocatorId(uint256 id) internal pure returns (uint96) { - uint96 allocatorId_; - assembly ("memory-safe") { - allocatorId_ := shr(164, shl(4, id)) - } - return allocatorId_; - } - - function _splitAllocatorId(bytes12 lockTag) internal pure returns (uint96) { - uint96 allocatorId; - assembly ("memory-safe") { - allocatorId := shr(164, shl(4, lockTag)) - } - return allocatorId; - } - - function _splitToken(uint256 id) internal pure returns (address) { - return address(uint160(id)); - } - - function _getClaimHash( - address arbiter, - address sponsor, - uint256 nonce, - uint256 expires, - bytes32 commitmentsHash, - bytes32 witness, - bytes32 typehash - ) internal pure returns (bytes32 claimHash) { - assembly ("memory-safe") { - let m := mload(0x40) - mstore(m, typehash) - mstore(add(m, 0x20), arbiter) - mstore(add(m, 0x40), sponsor) - mstore(add(m, 0x60), nonce) - mstore(add(m, 0x80), expires) - mstore(add(m, 0xa0), commitmentsHash) - mstore(add(m, 0xc0), witness) - claimHash := keccak256(m, sub(0xe0, mul(iszero(witness), 0x20))) - } - } - - function _getCommitmentsHash(Lock[] memory commitments) internal pure returns (bytes32) { - bytes32[] memory commitmentsHashes = new bytes32[](commitments.length); - for (uint256 i = 0; i < commitments.length; i++) { - commitmentsHashes[i] = keccak256( - abi.encode(LOCK_TYPEHASH, commitments[i].lockTag, commitments[i].token, commitments[i].amount) - ); - } - return keccak256(abi.encodePacked(commitmentsHashes)); - } } diff --git a/src/allocators/HybridERC7683.sol b/src/allocators/HybridERC7683.sol index 7789210..7531c67 100644 --- a/src/allocators/HybridERC7683.sol +++ b/src/allocators/HybridERC7683.sol @@ -11,6 +11,7 @@ import {HybridAllocator} from 'src/allocators/HybridAllocator.sol'; import {BATCH_COMPACT_WITNESS_TYPEHASH, MANDATE_TYPEHASH} from 'src/allocators/lib/TypeHashes.sol'; import {IHybridERC7683} from 'src/interfaces/IHybridERC7683.sol'; +import {AllocatorLib as AL} from 'src/allocators/lib/AllocatorLib.sol'; import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; contract HybridERC7683 is HybridAllocator, IHybridERC7683 { @@ -80,7 +81,7 @@ contract HybridERC7683 is HybridAllocator, IHybridERC7683 { Lock[] memory locks = new Lock[](registeredAmounts.length); for (uint256 i = 0; i < registeredAmounts.length; i++) { - locks[i] = _createLock(orderData.idsAndAmounts[i][0], registeredAmounts[i]); + locks[i] = AL.toLock(orderData.idsAndAmounts[i][0], registeredAmounts[i]); } BatchCompact memory batchCompact = BatchCompact({ @@ -132,7 +133,7 @@ contract HybridERC7683 is HybridAllocator, IHybridERC7683 { Lock[] memory locks = new Lock[](registeredAmounts.length); for (uint256 i = 0; i < registeredAmounts.length; i++) { - locks[i] = _createLock(orderData.idsAndAmounts[i][0], registeredAmounts[i]); + locks[i] = AL.toLock(orderData.idsAndAmounts[i][0], registeredAmounts[i]); } BatchCompact memory batchCompact = BatchCompact({ @@ -205,7 +206,7 @@ contract HybridERC7683 is HybridAllocator, IHybridERC7683 { Lock[] memory locks = new Lock[](orderData.idsAndAmounts.length); for (uint256 i = 0; i < orderData.idsAndAmounts.length; i++) { - locks[i] = _createLock(orderData.idsAndAmounts[i][0], orderData.idsAndAmounts[i][1]); + locks[i] = AL.toLock(orderData.idsAndAmounts[i][0], orderData.idsAndAmounts[i][1]); } BatchCompact memory batchCompact = BatchCompact({ @@ -231,7 +232,7 @@ contract HybridERC7683 is HybridAllocator, IHybridERC7683 { Lock[] memory locks = new Lock[](idsLength); for (uint256 i = 0; i < idsLength; i++) { uint256 id = orderData.idsAndAmounts[i][0]; - locks[i] = _createLock(id, orderData.idsAndAmounts[i][1]); + locks[i] = AL.toLock(id, orderData.idsAndAmounts[i][1]); } BatchCompact memory batchCompact = BatchCompact({ arbiter: orderData.arbiter, @@ -376,8 +377,4 @@ contract HybridERC7683 is HybridAllocator, IHybridERC7683 { function _convertAddressToBytes32(address address_) private pure returns (bytes32) { return bytes32(uint256(uint160(address_))); } - - function _createLock(uint256 id, uint256 amount) private pure returns (Lock memory) { - return Lock({lockTag: bytes12(bytes32(id)), token: _splitToken(id), amount: amount}); - } } diff --git a/src/allocators/OnChainAllocator.sol b/src/allocators/OnChainAllocator.sol index b76c4bd..f7dfd58 100644 --- a/src/allocators/OnChainAllocator.sol +++ b/src/allocators/OnChainAllocator.sol @@ -3,6 +3,8 @@ pragma solidity ^0.8.27; import {IOnChainAllocator} from '../interfaces/IOnChainAllocator.sol'; + +import {AllocatorLib as AL} from './lib/AllocatorLib.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {ERC6909} from '@solady/tokens/ERC6909.sol'; import {SafeTransferLib} from '@solady/utils/SafeTransferLib.sol'; @@ -18,8 +20,6 @@ contract OnChainAllocator is IOnChainAllocator { address public immutable COMPACT_CONTRACT; bytes32 public immutable COMPACT_DOMAIN_SEPARATOR; uint96 public immutable ALLOCATOR_ID; - // bytes4(keccak256('prepareAllocation(address,uint256[2][],address,uint256,bytes32,bytes32,bytes)')); - bytes4 public constant PREPARE_ALLOCATION_SELECTOR = 0x7ef6597a; mapping(bytes32 tokenHash => Allocation[] allocations) internal _allocations; @@ -65,7 +65,7 @@ contract OnChainAllocator is IOnChainAllocator { if (signature.length > 0) { // confirm the provided signature is valid bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), COMPACT_DOMAIN_SEPARATOR, claimHash)); - address signer_ = _recoverSigner(digest, signature); + address signer_ = AL.recoverSigner(digest, signature); if (sponsor != signer_ || signer_ == address(0)) { revert InvalidSignature(signer_, sponsor); } @@ -88,19 +88,22 @@ contract OnChainAllocator is IOnChainAllocator { bytes32 witness ) public returns (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) { nonce = ++nonces[_toNonceId(msg.sender, recipient)]; // prevents griefing of frontrunning nonces + uint256[2][] memory idsAndAmounts = new uint256[2][](commitments.length); + uint256 minResetPeriod = type(uint256).max; for (uint256 i = 0; i < commitments.length; i++) { minResetPeriod = _checkInput(commitments[i], recipient, expires, minResetPeriod); - - // Store the allocation - idsAndAmounts[i][0] = _toId(commitments[i].lockTag, commitments[i].token); + idsAndAmounts[i][0] = AL.toId(commitments[i].lockTag, commitments[i].token); uint224 amount = uint224(commitments[i].amount); + + // If the amount is 0, we use the balance of the contract to deposit. if (amount == 0) { amount = uint224(IERC20(commitments[i].token).balanceOf(address(this))); } idsAndAmounts[i][1] = amount; + // Approve the compact contract to spend the tokens. if (IERC20(commitments[i].token).allowance(address(this), COMPACT_CONTRACT) < amount) { SafeTransferLib.safeApproveWithRetry(commitments[i].token, COMPACT_CONTRACT, type(uint256).max); } @@ -110,27 +113,38 @@ contract OnChainAllocator is IOnChainAllocator { revert InvalidExpiration(expires, block.timestamp + minResetPeriod); } + // Deposit the tokens and register the claim in the compact (claimHash, registeredAmounts) = ITheCompact(COMPACT_CONTRACT).batchDepositAndRegisterFor( recipient, idsAndAmounts, arbiter, nonce, expires, typehash, witness ); - Lock[] memory registeredCommitments = commitments; + // Update the commitments and store the allocation + Lock[] memory registeredCommitments = + _updateCommitmentsAndStoreAllocation(recipient, registeredAmounts, commitments, expires, claimHash); - // Store the allocation - for (uint256 i = 0; i < registeredAmounts.length; i++) { - bytes32 tokenHash = _getTokenHash(commitments[i], recipient); + emit Allocated(recipient, registeredCommitments, nonce, expires, claimHash); - Allocation memory allocation = - Allocation({expires: expires, amount: uint224(registeredAmounts[i]), claimHash: claimHash}); - _allocations[tokenHash].push(allocation); + return (claimHash, registeredAmounts, nonce); + } + function _updateCommitmentsAndStoreAllocation( + address recipient, + uint256[] memory registeredAmounts, + Lock[] memory commitments, + uint32 expires, + bytes32 claimHash + ) internal returns (Lock[] memory) { + // Store the allocation + for (uint256 i = 0; i < registeredAmounts.length; i++) { // Update the allocations with the actual registered amounts - registeredCommitments[i].amount = registeredAmounts[i]; - } + uint224 amount = uint224(registeredAmounts[i]); + commitments[i].amount = amount; - emit Allocated(recipient, registeredCommitments, nonce, expires, claimHash); + // Store the allocation + _storeAllocation(commitments[i].lockTag, commitments[i].token, amount, recipient, expires, claimHash); + } - return (claimHash, registeredAmounts, nonce); + return commitments; } function prepareAllocation( @@ -142,29 +156,9 @@ contract OnChainAllocator is IOnChainAllocator { bytes32 witness, bytes calldata /* orderData */ ) external returns (uint256 nonce) { - uint256[] memory ids = new uint256[](idsAndAmounts.length); - for (uint256 i = 0; i < idsAndAmounts.length; i++) { - uint256 id = idsAndAmounts[i][0]; - ids[i] = id; - uint256 currentBalance = ERC6909(COMPACT_CONTRACT).balanceOf(recipient, id); - assembly ("memory-safe") { - tstore(or(PREPARE_ALLOCATION_SELECTOR, id), currentBalance) - } - - // Check the amount fits in the supported range - if (idsAndAmounts[i][1] > type(uint224).max) { - revert InvalidAmount(idsAndAmounts[i][1]); - } - } - - // Store the nonce for the identifier to ensure the same data is used in `executeAllocation` and protect against replay attacks - bytes32 identifier = keccak256( - abi.encode(PREPARE_ALLOCATION_SELECTOR, recipient, ids, arbiter, uint32(expires), typehash, witness) - ); - nonce = nonces[_toNonceId(msg.sender, recipient)] + 1; - assembly ("memory-safe") { - tstore(identifier, nonce) - } + bytes32 nonceId = _toNonceId(msg.sender, recipient); + nonce = nonces[nonceId] + 1; + AL.prepareAllocation(COMPACT_CONTRACT, nonce, recipient, idsAndAmounts, arbiter, expires, typehash, witness); return nonce; } @@ -178,67 +172,45 @@ contract OnChainAllocator is IOnChainAllocator { bytes32 witness, bytes calldata /* orderData */ ) external { - uint256[] memory ids = new uint256[](idsAndAmounts.length); - Lock[] memory commitments = new Lock[](idsAndAmounts.length); - bytes32[] memory commitmentHashes = new bytes32[](idsAndAmounts.length); - expires = uint32(expires); - - // Check actual balance changes - for (uint256 i = 0; i < idsAndAmounts.length; i++) { - uint256 id = idsAndAmounts[i][0]; - ids[i] = id; - uint256 oldBalance; - assembly ("memory-safe") { - oldBalance := tload(or(PREPARE_ALLOCATION_SELECTOR, id)) - tstore(or(PREPARE_ALLOCATION_SELECTOR, id), 0) - } - uint256 newBalance = ERC6909(COMPACT_CONTRACT).balanceOf(recipient, id); - if (newBalance <= oldBalance) { - revert InsufficientBalance(recipient, id, newBalance, oldBalance + 1); - } - uint256 amount = newBalance - oldBalance; - - // Check the amount fits in the supported range - if (amount > type(uint224).max) { - revert InvalidAmount(amount); - } + uint256 nonce = ++nonces[_toNonceId(msg.sender, recipient)]; + uint32 expiration = uint32(expires); - // Create commitments - bytes12 lockTag = bytes12(bytes32(id)); - address token = address(uint160(id)); - commitmentHashes[i] = keccak256(abi.encode(LOCK_TYPEHASH, lockTag, token, amount)); - commitments[i] = Lock({lockTag: lockTag, token: token, amount: amount}); - } + (bytes32 claimHash, Lock[] memory commitments) = + _executeAllocation(nonce, recipient, idsAndAmounts, arbiter, expiration, typehash, witness); - // Check preparation was called with the same data - bytes32 identifier = - keccak256(abi.encode(PREPARE_ALLOCATION_SELECTOR, recipient, ids, arbiter, expires, typehash, witness)); - uint256 nonce; - assembly ("memory-safe") { - nonce := tload(identifier) - tstore(identifier, 0) - } - if (nonce != ++nonces[_toNonceId(msg.sender, recipient)]) { - revert InvalidPreparation(); - } + emit Allocated(recipient, commitments, nonce, expiration, claimHash); + } - // Check for a valid registration with the actual data - bytes32 claimHash = _getClaimHash( - arbiter, recipient, nonce, expires, keccak256(abi.encodePacked(commitmentHashes)), witness, typehash - ); - if (!ITheCompact(COMPACT_CONTRACT).isRegistered(recipient, claimHash, typehash)) { - revert InvalidRegistration(recipient, claimHash); - } + function _executeAllocation( + uint256 nonce, + address recipient, + uint256[2][] calldata idsAndAmounts, + address arbiter, + uint32 expires, + bytes32 typehash, + bytes32 witness + ) internal returns (bytes32, Lock[] memory) { + (bytes32 claimHash, Lock[] memory commitments) = + AL.executeAllocation(COMPACT_CONTRACT, nonce, recipient, idsAndAmounts, arbiter, expires, typehash, witness); // Allocate the claim - for (uint256 i = 0; i < idsAndAmounts.length; i++) { - bytes32 tokenHash = _getTokenHash(idsAndAmounts[i][0], recipient); - Allocation memory allocation = - Allocation({expires: uint32(expires), amount: uint224(commitments[i].amount), claimHash: claimHash}); - _allocations[tokenHash].push(allocation); + 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), + recipient, + expires, + claimHash + ); } - emit Allocated(recipient, commitments, nonce, expires, claimHash); + return (claimHash, commitments); } /// @inheritdoc IAllocator @@ -322,8 +294,8 @@ contract OnChainAllocator is IOnChainAllocator { } nonce = ++nonces[_toNonceId(address(0), sponsor)]; // address(0) as caller allows anyone to relay - bytes32 commitmentsHash = _getCommitmentsHash(commitments); - claimHash = _getClaimHash(arbiter, sponsor, nonce, expires, commitmentsHash, witness, typehash); + bytes32 commitmentsHash = AL.getCommitmentsHash(commitments); + claimHash = AL.getClaimHash(arbiter, sponsor, nonce, expires, commitmentsHash, witness, typehash); uint256 minResetPeriod = type(uint256).max; for (uint256 i = 0; i < commitments.length; i++) { @@ -332,8 +304,7 @@ contract OnChainAllocator is IOnChainAllocator { // Store the allocation uint224 amount = uint224(commitments[i].amount); - Allocation memory allocation = Allocation({expires: expires, amount: amount, claimHash: claimHash}); - _allocations[tokenHash].push(allocation); + _storeAllocation(tokenHash, amount, expires, claimHash); } // Ensure expiration is not bigger then the smallest reset period if (expires >= block.timestamp + minResetPeriod) { @@ -349,8 +320,8 @@ contract OnChainAllocator is IOnChainAllocator { returns (uint256) { // Check the allocator id fits this allocator - if (_splitAllocatorId(commitment.lockTag) != ALLOCATOR_ID) { - revert InvalidAllocator(_splitAllocatorId(commitment.lockTag), ALLOCATOR_ID); + if (AL.splitAllocatorId(commitment.lockTag) != ALLOCATOR_ID) { + revert InvalidAllocator(AL.splitAllocatorId(commitment.lockTag), ALLOCATOR_ID); } // Check the amount fits in the supported range @@ -359,14 +330,14 @@ contract OnChainAllocator is IOnChainAllocator { } // Get the reset period for the token id - uint256 duration = _toSeconds(commitment.lockTag); + uint256 duration = AL.toSeconds(commitment.lockTag); if (duration < minResetPeriod) { minResetPeriod = duration; } // Ensure no forcedWithdrawal is active for the token id (, uint256 forcedWithdrawal) = ITheCompact(COMPACT_CONTRACT).getForcedWithdrawalStatus( - sponsor, _toId(commitment.lockTag, commitment.token) + sponsor, AL.toId(commitment.lockTag, commitment.token) ); if (forcedWithdrawal != 0 && forcedWithdrawal <= expires) { revert ForceWithdrawalAvailable(expires, forcedWithdrawal); @@ -377,17 +348,34 @@ contract OnChainAllocator is IOnChainAllocator { function _checkBalance(address sponsor, Lock calldata commitment) internal returns (bytes32 tokenHash) { // Check the balance of the recipient is sufficient - tokenHash = _getTokenHash(commitment, sponsor); - uint256 balance = ERC6909(COMPACT_CONTRACT).balanceOf(sponsor, _toId(commitment.lockTag, commitment.token)); + tokenHash = _getTokenHash(commitment.lockTag, commitment.token, sponsor); + uint256 balance = ERC6909(COMPACT_CONTRACT).balanceOf(sponsor, AL.toId(commitment.lockTag, commitment.token)); uint256 allocatedBalance = _allocatedBalance(tokenHash); uint256 requiredBalance = allocatedBalance + commitment.amount; if (requiredBalance > balance) { revert InsufficientBalance( - sponsor, _toId(commitment.lockTag, commitment.token), balance - allocatedBalance, commitment.amount + sponsor, AL.toId(commitment.lockTag, commitment.token), balance - allocatedBalance, commitment.amount ); } } + function _storeAllocation( + bytes12 lockTag, + address token, + uint224 amount, + address recipient, + uint32 expires, + bytes32 claimHash + ) internal { + bytes32 tokenHash = _getTokenHash(lockTag, token, recipient); + _storeAllocation(tokenHash, amount, expires, claimHash); + } + + function _storeAllocation(bytes32 tokenHash, uint224 amount, uint32 expires, bytes32 claimHash) internal { + Allocation memory allocation = Allocation({expires: expires, amount: amount, claimHash: claimHash}); + _allocations[tokenHash].push(allocation); + } + function _allocatedBalance(bytes32 tokenHash) internal returns (uint256 allocatedBalance) { // using assembly to only read the allocated balance + expiration slot and skipping the claimHash slot assembly ("memory-safe") { @@ -477,40 +465,10 @@ contract OnChainAllocator is IOnChainAllocator { } } - function _recoverSigner(bytes32 digest, bytes calldata signature) internal pure returns (address) { - bytes32 r; - bytes32 s; - uint8 v; - - if (signature.length == 65) { - (r, s) = abi.decode(signature, (bytes32, bytes32)); - v = uint8(signature[64]); - } else if (signature.length == 64) { - bytes32 vs; - (r, vs) = abi.decode(signature, (bytes32, bytes32)); - v = uint8(uint256(vs >> 255) + 27); - s = vs << 1 >> 1; - } else { - return address(0); - } - - return ecrecover(digest, v, r, s); - } - - function _getCommitmentsHash(Lock[] memory commitments) internal pure returns (bytes32) { - bytes32[] memory commitmentsHashes = new bytes32[](commitments.length); - for (uint256 i = 0; i < commitments.length; i++) { - commitmentsHashes[i] = keccak256( - abi.encode(LOCK_TYPEHASH, commitments[i].lockTag, commitments[i].token, commitments[i].amount) - ); - } - return keccak256(abi.encodePacked(commitmentsHashes)); - } - - function _getTokenHash(Lock calldata commitment, address sponsor) internal pure returns (bytes32 tokenHash) { + function _getTokenHash(bytes12 lockTag, address token, address sponsor) internal pure returns (bytes32 tokenHash) { assembly ("memory-safe") { - mstore(0x00, calldataload(commitment)) - mstore(0x0c, shl(96, calldataload(add(commitment, 0x20)))) + mstore(0x00, lockTag) + mstore(0x0c, shl(96, token)) mstore(0x20, sponsor) tokenHash := keccak256(0x00, 0x40) } @@ -520,57 +478,7 @@ contract OnChainAllocator is IOnChainAllocator { tokenHash = keccak256(abi.encode(id, sponsor)); } - function _splitAllocatorId(bytes12 lockTag) internal pure returns (uint96) { - uint96 allocatorId_; - assembly ("memory-safe") { - allocatorId_ := shr(164, shl(4, lockTag)) - } - return allocatorId_; - } - - function _toId(bytes12 lockTag, address token) internal pure returns (uint256 id) { - assembly ("memory-safe") { - id := or(lockTag, token) - } - } - function _toNonceId(address caller, address sponsor) internal pure returns (bytes32 nonce) { return keccak256(abi.encode(caller, sponsor)); } - - function _toSeconds(bytes12 lockTag) internal pure returns (uint256 duration) { - assembly ("memory-safe") { - let resetPeriod := shr(253, shl(1, lockTag)) - - // Bitpacked durations in 24-bit segments: - // 278d00 094890 015180 000f3c 000258 00003c 00000f 000001 - // 30 days 7 days 1 day 1 hour 10 min 1 min 15 sec 1 sec - let bitpacked := 0x278d00094890015180000f3c00025800003c00000f000001 - - // Shift right by period * 24 bits & mask the least significant 24 bits. - duration := and(shr(mul(resetPeriod, 24), bitpacked), 0xffffff) - } - } - - function _getClaimHash( - address arbiter, - address sponsor, - uint256 nonce, - uint256 expires, - bytes32 commitmentsHash, - bytes32 witness, - bytes32 typehash - ) internal pure returns (bytes32 claimHash) { - assembly ("memory-safe") { - let m := mload(0x40) - mstore(m, typehash) - mstore(add(m, 0x20), arbiter) - mstore(add(m, 0x40), sponsor) - mstore(add(m, 0x60), nonce) - mstore(add(m, 0x80), expires) - mstore(add(m, 0xa0), commitmentsHash) - mstore(add(m, 0xc0), witness) - claimHash := keccak256(m, sub(0xe0, mul(iszero(witness), 0x20))) - } - } } diff --git a/src/allocators/lib/AllocatorLib.sol b/src/allocators/lib/AllocatorLib.sol new file mode 100644 index 0000000..daa98b0 --- /dev/null +++ b/src/allocators/lib/AllocatorLib.sol @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: MIT +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'; + +library AllocatorLib { + // bytes4(keccak256('prepareAllocation(address,uint256[2][],address,uint256,bytes32,bytes32,bytes)')); + bytes4 public constant PREPARE_ALLOCATION_SELECTOR = 0x7ef6597a; + + error InvalidBalanceChange(uint256 newBalance, uint256 oldBalance); + error InvalidPreparation(); + error InvalidRegistration(address recipient, bytes32 claimHash, bytes32 typehash); + + function prepareAllocation( + address compactContract, + uint256 nonce, + address recipient, + uint256[2][] calldata idsAndAmounts, + address arbiter, + uint256 expires, + bytes32 typehash, + bytes32 witness + ) internal { + uint256[] memory ids = new uint256[](idsAndAmounts.length); + for (uint256 i = 0; i < idsAndAmounts.length; i++) { + uint256 id = idsAndAmounts[i][0]; + // Store Id for the identifier + ids[i] = id; + + // Store the current balance to calculate the deposited amounts in `executeAllocation` + uint256 currentBalance = ERC6909(compactContract).balanceOf(recipient, id); + assembly ("memory-safe") { + tstore(or(PREPARE_ALLOCATION_SELECTOR, id), currentBalance) + } + } + + // Store the nonce for the identifier to ensure the same data is used in `executeAllocation` and protect against replay attacks + bytes32 identifier = keccak256( + abi.encode(PREPARE_ALLOCATION_SELECTOR, recipient, ids, arbiter, uint32(expires), typehash, witness) + ); + assembly ("memory-safe") { + tstore(identifier, nonce) + } + } + + function executeAllocation( + address compactContract, + uint256 nonce, + address recipient, + uint256[2][] calldata idsAndAmounts, + address arbiter, + uint256 expires, + bytes32 typehash, + bytes32 witness + ) internal view returns (bytes32 claimHash, Lock[] memory commitments) { + uint256[] memory ids = new uint256[](idsAndAmounts.length); + commitments = new Lock[](idsAndAmounts.length); + bytes32[] memory commitmentHashes = new bytes32[](idsAndAmounts.length); + + // Check actual balance changes + for (uint256 i = 0; i < idsAndAmounts.length; i++) { + // Store Id for the identifier + ids[i] = idsAndAmounts[i][0]; + + uint256 amount = _calculateBalanceChange(compactContract, recipient, ids[i]); + + // Create commitments + bytes12 lockTag = bytes12(bytes32(ids[i])); + address token = address(uint160(ids[i])); + commitmentHashes[i] = keccak256(abi.encode(LOCK_TYPEHASH, lockTag, token, amount)); + commitments[i] = Lock({lockTag: lockTag, token: token, amount: amount}); + } + + // Ensure preparation was called with the same data + bytes32 identifier = + keccak256(abi.encode(PREPARE_ALLOCATION_SELECTOR, recipient, ids, arbiter, expires, typehash, witness)); + uint256 storedNonce; + assembly ("memory-safe") { + storedNonce := tload(identifier) + } + if (nonce != storedNonce) { + revert InvalidPreparation(); + } + + // Check for a valid registration with the actual data + claimHash = getClaimHash( + arbiter, recipient, storedNonce, expires, keccak256(abi.encodePacked(commitmentHashes)), witness, typehash + ); + if (!ITheCompact(compactContract).isRegistered(recipient, claimHash, typehash)) { + revert InvalidRegistration(recipient, claimHash, typehash); + } + + return (claimHash, commitments); + } + + function getCommitmentsHash(Lock[] memory commitments) internal pure returns (bytes32) { + bytes32[] memory commitmentsHashes = new bytes32[](commitments.length); + for (uint256 i = 0; i < commitments.length; i++) { + commitmentsHashes[i] = keccak256( + abi.encode(LOCK_TYPEHASH, commitments[i].lockTag, commitments[i].token, commitments[i].amount) + ); + } + return keccak256(abi.encodePacked(commitmentsHashes)); + } + + function getClaimHash( + address arbiter, + address sponsor, + uint256 nonce, + uint256 expires, + bytes32 commitmentsHash, + bytes32 witness, + bytes32 typehash + ) internal pure returns (bytes32 claimHash) { + assembly ("memory-safe") { + let m := mload(0x40) + mstore(m, typehash) + mstore(add(m, 0x20), arbiter) + mstore(add(m, 0x40), sponsor) + mstore(add(m, 0x60), nonce) + mstore(add(m, 0x80), expires) + mstore(add(m, 0xa0), commitmentsHash) + mstore(add(m, 0xc0), witness) + claimHash := keccak256(m, sub(0xe0, mul(iszero(witness), 0x20))) + } + } + + function recoverSigner(bytes32 digest, bytes calldata signature) internal pure returns (address) { + bytes32 r; + bytes32 s; + uint8 v; + + if (signature.length == 65) { + (r, s) = abi.decode(signature, (bytes32, bytes32)); + v = uint8(signature[64]); + } else if (signature.length == 64) { + bytes32 vs; + (r, vs) = abi.decode(signature, (bytes32, bytes32)); + v = uint8(uint256(vs >> 255) + 27); + s = vs << 1 >> 1; + } else { + return address(0); + } + + return ecrecover(digest, v, r, s); + } + + function splitId(uint256 id) internal pure returns (uint96 allocatorId_, address token_) { + return (splitAllocatorId(id), splitToken(id)); + } + + function splitAllocatorId(uint256 id) internal pure returns (uint96) { + uint96 allocatorId_; + assembly ("memory-safe") { + allocatorId_ := shr(164, shl(4, id)) + } + return allocatorId_; + } + + function splitAllocatorId(bytes12 lockTag) internal pure returns (uint96) { + uint96 allocatorId_; + assembly ("memory-safe") { + allocatorId_ := shr(164, shl(4, lockTag)) + } + return allocatorId_; + } + + function splitToken(uint256 id) internal pure returns (address) { + return address(uint160(id)); + } + + function toId(bytes12 lockTag, address token) internal pure returns (uint256 id) { + assembly ("memory-safe") { + id := or(lockTag, token) + } + } + + function toLock(uint256 id, uint256 amount) internal pure returns (Lock memory) { + return Lock({lockTag: bytes12(bytes32(id)), token: splitToken(id), amount: amount}); + } + + function toSeconds(bytes12 lockTag) internal pure returns (uint256 duration) { + assembly ("memory-safe") { + let resetPeriod := shr(253, shl(1, lockTag)) + + // Bitpacked durations in 24-bit segments: + // 278d00 094890 015180 000f3c 000258 00003c 00000f 000001 + // 30 days 7 days 1 day 1 hour 10 min 1 min 15 sec 1 sec + let bitpacked := 0x278d00094890015180000f3c00025800003c00000f000001 + + // Shift right by period * 24 bits & mask the least significant 24 bits. + duration := and(shr(mul(resetPeriod, 24), bitpacked), 0xffffff) + } + } + + function _calculateBalanceChange(address compactContract, address recipient, uint256 id) + private + view + returns (uint256 amount) + { + // Calculate the balance + uint256 oldBalance; + assembly ("memory-safe") { + oldBalance := tload(or(PREPARE_ALLOCATION_SELECTOR, id)) + } + uint256 newBalance = ERC6909(compactContract).balanceOf(recipient, id); + if (newBalance <= oldBalance) { + revert InvalidBalanceChange(newBalance, oldBalance); + } + return newBalance - oldBalance; + } +} diff --git a/src/interfaces/IOnChainAllocation.sol b/src/interfaces/IOnChainAllocation.sol new file mode 100644 index 0000000..7ff6e47 --- /dev/null +++ b/src/interfaces/IOnChainAllocation.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; +import {Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; + +interface IOnChainAllocation is IAllocator { + error InvalidPreparation(); + error InvalidRegistration(address sponsor, bytes32 claimHash); + + /// @notice Emitted when a tokens are successfully allocated + /// @param sponsor The address of the sponsor + /// @param commitments The commitments of the allocations + /// @param nonce The nonce of the allocation + /// @param expires The expiration of the allocation + /// @param claimHash The hash of the allocation + event Allocated(address indexed sponsor, Lock[] commitments, uint256 nonce, uint256 expires, bytes32 claimHash); + + /** + * @notice Allows to create an allocation on behalf of a recipient without the contract being in control over the funds. + * @notice Will typically be used in combination with `batchDepositAndRegisterFor` on the compact. + * @dev Must be called before `executeAllocation` to ensure a valid balance change has occurred for the recipient. + * @param recipient The account to receive the tokens. + * @param idsAndAmounts The ids and amounts to allocate. + * @param arbiter The account tasked with verifying and submitting the claim. + * @param expires The time at which the claim expires. + * @param typehash The typehash of the claim. + * @param witness The witness of the claim. + * @return nonce The next valid nonce. It is only guaranteed that the nonce is valid within the same transaction.. + */ + function prepareAllocation( + address recipient, + uint256[2][] calldata idsAndAmounts, + address arbiter, + uint256 expires, + bytes32 typehash, + bytes32 witness, + bytes calldata orderData + ) external returns (uint256 nonce); + + /** + * @notice Executes an allocation on behalf of a recipient. + * @dev Must be called after `prepareAllocation` to ensure a valid balance change has occurred for the recipient. + * @param recipient The account to receive the tokens. + * @param idsAndAmounts The ids and amounts to allocate. + * @param arbiter The account tasked with verifying and submitting the claim. + * @param expires The time at which the claim expires. + * @param typehash The typehash of the claim. + * @param witness The witness of the claim. + */ + function executeAllocation( + address recipient, + uint256[2][] calldata idsAndAmounts, + address arbiter, + uint256 expires, + bytes32 typehash, + bytes32 witness, + bytes calldata orderData + ) external; +} diff --git a/src/test/OnChainAllocationCaller.sol b/src/test/OnChainAllocationCaller.sol new file mode 100644 index 0000000..133caf9 --- /dev/null +++ b/src/test/OnChainAllocationCaller.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {IOnChainAllocation} from '../interfaces/IOnChainAllocation.sol'; +import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; + +contract OnChainAllocationCaller { + IOnChainAllocation public immutable ALLOCATOR; + ITheCompact public immutable COMPACT; + + constructor(address allocator_, address compact_) { + ALLOCATOR = IOnChainAllocation(allocator_); + COMPACT = ITheCompact(compact_); + } + + function onChainAllocation( + address recipient, + uint256[2][] calldata idsAndAmounts, + address arbiter, + uint256 expires, + bytes32 typehash, + bytes32 witness, + uint8 todo + ) external { + uint256 nonce; + if (todo == 0) { + // Correctly deposit and register + nonce = ALLOCATOR.prepareAllocation(recipient, idsAndAmounts, arbiter, expires, typehash, witness, ''); + ITheCompact(COMPACT).batchDepositAndRegisterFor( + recipient, idsAndAmounts, arbiter, nonce, expires, typehash, witness + ); + } else if (todo == 1) { + // Only deposit, do not register + nonce = ALLOCATOR.prepareAllocation(recipient, idsAndAmounts, arbiter, expires, typehash, witness, ''); + ITheCompact(COMPACT).batchDeposit(idsAndAmounts, recipient); + } else if (todo == 2) { + // Do not prepare, but deposit and register + ITheCompact(COMPACT).batchDepositAndRegisterFor( + recipient, idsAndAmounts, arbiter, nonce, expires, typehash, witness + ); + } else if (todo == 3) { + nonce = ALLOCATOR.prepareAllocation(recipient, idsAndAmounts, arbiter, expires, typehash, witness, ''); + } else { + // Correctly deposit and register + nonce = ALLOCATOR.prepareAllocation(recipient, idsAndAmounts, arbiter, expires, typehash, witness, ''); + ITheCompact(COMPACT).batchDepositAndRegisterFor( + recipient, idsAndAmounts, arbiter, nonce, expires, typehash, witness + ); + ALLOCATOR.executeAllocation(recipient, idsAndAmounts, arbiter, expires, typehash, witness, ''); + } + ALLOCATOR.executeAllocation(recipient, idsAndAmounts, arbiter, expires, typehash, witness, ''); + } +} diff --git a/test/HybridAllocator.t.sol b/test/HybridAllocator.t.sol index a0a23cf..52b35c4 100644 --- a/test/HybridAllocator.t.sol +++ b/test/HybridAllocator.t.sol @@ -14,9 +14,12 @@ import {Scope} from '@uniswap/the-compact/types/Scope.sol'; import {Test} from 'forge-std/Test.sol'; import {HybridAllocator} from 'src/allocators/HybridAllocator.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'; import {ERC20Mock} from 'src/test/ERC20Mock.sol'; +import {OnChainAllocationCaller} from 'src/test/OnChainAllocationCaller.sol'; contract HybridAllocatorTest is Test, TestHelper { TheCompact compact; @@ -30,6 +33,8 @@ contract HybridAllocatorTest is Test, TestHelper { uint256 defaultAmount; uint256 defaultExpiration; + OnChainAllocationCaller allocationCaller; + BatchCompact batchCompact; function setUp() public { @@ -44,12 +49,32 @@ contract HybridAllocatorTest is Test, TestHelper { defaultAmount = 1 ether; defaultExpiration = vm.getBlockTimestamp() + 1 days; + allocationCaller = new OnChainAllocationCaller(address(allocator), address(compact)); + batchCompact.arbiter = arbiter; batchCompact.sponsor = user; batchCompact.nonce = 1; batchCompact.expires = defaultExpiration; } + 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); + arr[0][1] = amount; + } + + function _idsAndAmounts2(address tokenA, uint256 amountA, address tokenB, uint256 amountB) + internal + view + returns (uint256[2][] memory arr) + { + arr = new uint256[2][](2); + arr[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), tokenA); + arr[0][1] = amountA; + arr[1][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), tokenB); + arr[1][1] = amountB; + } + function test_constructor_revert_signerIsAddressZero() public { vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.InvalidSigner.selector)); new HybridAllocator(address(compact), address(0)); @@ -74,6 +99,132 @@ contract HybridAllocatorTest is Test, TestHelper { assertFalse(allocator.signers(attacker)); } + function test_prepareAllocation_returnsNonce_andDoesNotIncrement() public { + uint256[2][] memory idsAndAmounts = _idsAndAmounts(address(usdc), defaultAmount); + uint96 beforeNonces = allocator.nonces(); + // call prepare directly + uint256 returnedNonce = allocator.prepareAllocation( + user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0), '' + ); + assertEq(returnedNonce, uint256(beforeNonces) + 1); + // storage not incremented yet + assertEq(allocator.nonces(), beforeNonces); + } + + function test_executeAllocation_success_viaCaller_singleERC20() public { + uint256 amount = defaultAmount; + uint256[2][] memory idsAndAmounts = _idsAndAmounts(address(usdc), amount); + // fund caller and approve + usdc.mint(address(allocationCaller), amount); + vm.prank(address(allocationCaller)); + usdc.approve(address(compact), amount); + + // run flow in one tx + vm.prank(user); + allocationCaller.onChainAllocation( + user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, '', 0 + ); + vm.snapshotGasLastCall('hybrid_execute_single'); + + // nonces incremented + assertEq(allocator.nonces(), 1); + + // derive claim hash and ensure isClaimAuthorized is true + Lock[] memory commitments = _idsAndAmountsToCommitments(idsAndAmounts); + bytes32 claimHash = _toBatchCompactHash( + BatchCompact({ + arbiter: arbiter, + sponsor: user, + nonce: allocator.nonces(), + expires: defaultExpiration, + commitments: commitments + }) + ); + assertTrue(allocator.isClaimAuthorized(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), '')); + } + + function test_executeAllocation_revert_InvalidPreparation() public { + uint256 amount = defaultAmount; + uint256[2][] memory idsAndAmounts = _idsAndAmounts(address(usdc), amount); + // fund caller and approve + usdc.mint(address(allocationCaller), amount); + vm.prank(address(allocationCaller)); + usdc.approve(address(compact), amount); + + // todo=2: deposit+register without prepare -> expect AllocatorLib.InvalidPreparation + vm.prank(user); + vm.expectRevert(AllocatorLib.InvalidPreparation.selector); + allocationCaller.onChainAllocation( + user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, '', 2 + ); + } + + function test_executeAllocation_revert_InvalidRegistration() public { + uint256 amount = defaultAmount; + uint256[2][] memory idsAndAmounts = _idsAndAmounts(address(usdc), amount); + // fund caller and approve + usdc.mint(address(allocationCaller), amount); + vm.prank(address(allocationCaller)); + usdc.approve(address(compact), amount); + + // Compute expected claim hash for the deposit-only path + Lock[] memory commitments = _idsAndAmountsToCommitments(idsAndAmounts); + uint256 expectedNonce = uint256(allocator.nonces()) + 1; // prepare will use this + bytes32 expectedClaimHash = _toBatchCompactHash( + BatchCompact({ + arbiter: arbiter, + sponsor: user, + nonce: expectedNonce, + expires: defaultExpiration, + commitments: commitments + }) + ); + + // todo=1: deposit only, no register -> AllocatorLib.InvalidRegistration + vm.prank(user); + vm.expectRevert( + abi.encodeWithSelector( + AllocatorLib.InvalidRegistration.selector, user, expectedClaimHash, BATCH_COMPACT_TYPEHASH + ) + ); + allocationCaller.onChainAllocation( + user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, '', 1 + ); + } + + function test_executeAllocation_revert_InvalidBalanceChange_noDeposit() public { + uint256 amount = defaultAmount; + uint256[2][] memory idsAndAmounts = _idsAndAmounts(address(usdc), amount); + // give user prior ERC6909 balance so (oldBalance > 0) + bytes12 lockTag = _toLockTag(address(allocator), Scope.Multichain, ResetPeriod.TenMinutes); + vm.startPrank(user); + usdc.mint(user, amount); + usdc.approve(address(compact), amount); + compact.depositERC20(address(usdc), lockTag, amount, user); + vm.stopPrank(); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSignature('InvalidBalanceChange(uint256,uint256)', amount, amount)); + allocationCaller.onChainAllocation( + user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, '', 3 + ); + } + + function test_executeAllocation_revert_InvalidPreparation_replaySameTx() public { + uint256 amount = defaultAmount; + uint256[2][] memory idsAndAmounts = _idsAndAmounts(address(usdc), amount); + // fund caller and approve + usdc.mint(address(allocationCaller), amount); + vm.prank(address(allocationCaller)); + usdc.approve(address(compact), amount); + + vm.prank(user); + vm.expectRevert(AllocatorLib.InvalidPreparation.selector); + allocationCaller.onChainAllocation( + user, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, '', 4 + ); + } + function test_allocateAndRegister_revert_InvalidIds() public { vm.expectRevert(IHybridAllocator.InvalidIds.selector); allocator.allocateAndRegister(user, new uint256[2][](0), arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); diff --git a/test/OnChainAllocator.t.sol b/test/OnChainAllocator.t.sol index d28ff1b..0ddbdd5 100644 --- a/test/OnChainAllocator.t.sol +++ b/test/OnChainAllocator.t.sol @@ -26,6 +26,8 @@ import {ERC6909} from '@solady/tokens/ERC6909.sol'; import {ResetPeriod} from '@uniswap/the-compact/types/ResetPeriod.sol'; import {Scope} from '@uniswap/the-compact/types/Scope.sol'; +import {AllocatorLib} from 'src/allocators/lib/AllocatorLib.sol'; +import {OnChainAllocationCaller} from 'src/test/OnChainAllocationCaller.sol'; import {TestHelper} from 'test/util/TestHelper.sol'; contract OnChainAllocatorTest is Test, TestHelper { @@ -43,6 +45,8 @@ contract OnChainAllocatorTest is Test, TestHelper { address internal caller; uint256 internal callerPK; + OnChainAllocationCaller internal allocationCaller; + uint256 internal defaultAmount; uint32 internal defaultExpiration; @@ -57,6 +61,7 @@ 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); @@ -781,6 +786,259 @@ contract OnChainAllocatorTest is Test, TestHelper { ); } + /* --------------------------------------------------------------------- */ + /* prepareAllocation / executeAllocation */ + /* --------------------------------------------------------------------- */ + + function _idsAndAmountsFor(address token, uint256 amount) + internal + view + returns (uint256[2][] memory idsAndAmounts) + { + idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), token); + idsAndAmounts[0][1] = amount; + } + + function _idsAndAmountsFor2(address tokenA, uint256 amountA, address tokenB, uint256 amountB) + internal + view + returns (uint256[2][] memory idsAndAmounts) + { + idsAndAmounts = new uint256[2][](2); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), tokenA); + idsAndAmounts[0][1] = amountA; + idsAndAmounts[1][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), tokenB); + idsAndAmounts[1][1] = amountB; + } + + function test_prepareAllocation_returnsNonce_and_doesNotIncrementStorage() public { + uint256[2][] memory idsAndAmounts = _idsAndAmountsFor(address(usdc), defaultAmount); + + // call from an arbitrary EOA (caller) + vm.prank(caller); + uint256 returnedNonce = allocator.prepareAllocation( + recipient, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0), '' + ); + + assertEq(returnedNonce, 1); + // storage nonce is only incremented in executeAllocation + bytes32 nonceKey = keccak256(abi.encode(caller, recipient)); + assertEq(allocator.nonces(nonceKey), 0); + } + + function test_executeAllocation_success_viaCaller_singleERC20() public { + uint256 amount = defaultAmount; + uint256[2][] memory idsAndAmounts = _idsAndAmountsFor(address(usdc), amount); + + // fund and approve from allocationCaller + usdc.mint(address(allocationCaller), amount); + vm.prank(address(allocationCaller)); + usdc.approve(address(compact), amount); + + // run the whole flow in a single tx through the helper + vm.prank(user); + allocationCaller.onChainAllocation( + recipient, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0), 0 + ); + vm.snapshotGasLastCall('onchain_execute_single'); + + // nonce is scoped to (callerContract, recipient) + bytes32 nonceKey = keccak256(abi.encode(address(allocationCaller), recipient)); + assertEq(allocator.nonces(nonceKey), 1); + + // compute claim hash and check authorization + Lock[] memory commitments = _idsAndAmountsToCommitments(idsAndAmounts); + bytes32 claimHash = _createClaimHash(recipient, arbiter, 1, defaultExpiration, commitments, bytes32(0)); + + assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, recipient, 1, defaultExpiration, idsAndAmounts, '')); + } + + function test_executeAllocation_revert_InvalidPreparation() public { + uint256 amount = defaultAmount; + uint256[2][] memory idsAndAmounts = _idsAndAmountsFor(address(usdc), amount); + + // fund and approve from allocationCaller + usdc.mint(address(allocationCaller), amount); + vm.prank(address(allocationCaller)); + usdc.approve(address(compact), amount); + + // todo = 2: deposit+register without prepareAllocation -> executeAllocation must revert InvalidPreparation + vm.prank(user); + vm.expectRevert(AllocatorLib.InvalidPreparation.selector); + allocationCaller.onChainAllocation( + recipient, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0), 2 + ); + } + + function test_executeAllocation_revert_InvalidRegistration() public { + uint256 amount = defaultAmount; + uint256[2][] memory idsAndAmounts = _idsAndAmountsFor(address(usdc), amount); + + // fund and approve from allocationCaller + usdc.mint(address(allocationCaller), amount); + vm.prank(address(allocationCaller)); + usdc.approve(address(compact), amount); + + // todo = 1: deposit only (no registration) -> executeAllocation must revert InvalidRegistration + // Expect the precise error and arguments from AllocatorLib + // Compute the claimHash that AllocatorLib will recompute during execute. + Lock[] memory commitments = _idsAndAmountsToCommitments(idsAndAmounts); + bytes32 expectedClaimHash = _createClaimHash(recipient, arbiter, 1, defaultExpiration, commitments, bytes32(0)); + vm.prank(user); + vm.expectRevert( + abi.encodeWithSelector( + AllocatorLib.InvalidRegistration.selector, recipient, expectedClaimHash, BATCH_COMPACT_TYPEHASH + ) + ); + allocationCaller.onChainAllocation( + recipient, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0), 1 + ); + } + + function test_executeAllocation_revert_InvalidBalanceChange_onZeroAmountSecondId() public { + uint256 amountA = defaultAmount; + uint256 amountB = 0; // no deposit for second id -> balance unchanged -> InvalidBalanceChange + uint256[2][] memory idsAndAmounts = _idsAndAmountsFor2(address(usdc), amountA, address(dai), amountB); + + // fund and approve only the first token + usdc.mint(address(allocationCaller), amountA); + vm.startPrank(address(allocationCaller)); + usdc.approve(address(compact), amountA); + // approve DAI even if amount is zero to avoid allowance issues + dai.approve(address(compact), 0); + vm.stopPrank(); + + // Even though registration will succeed (with 0 for the second id), executeAllocation should revert + vm.prank(user); + // Revert happens inside TheCompact deposit logic before executeAllocation runs + // Use the selector for InvalidDepositBalanceChange() + vm.expectRevert(bytes4(keccak256('InvalidDepositBalanceChange()'))); + allocationCaller.onChainAllocation( + recipient, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0), 0 + ); + } + + function test_executeAllocation_success_twoIds() public { + uint256 amountA = defaultAmount; + uint256 amountB = defaultAmount / 2; + uint256[2][] memory idsAndAmounts = _idsAndAmountsFor2(address(usdc), amountA, address(dai), amountB); + + // fund & approve caller for both tokens + usdc.mint(address(allocationCaller), amountA); + dai.mint(address(allocationCaller), amountB); + vm.startPrank(address(allocationCaller)); + usdc.approve(address(compact), amountA); + dai.approve(address(compact), amountB); + vm.stopPrank(); + + vm.prank(user); + allocationCaller.onChainAllocation( + recipient, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0), 0 + ); + vm.snapshotGasLastCall('onchain_execute_double'); + + // authorization with the measured amounts + bytes32 nonceKey = keccak256(abi.encode(address(allocationCaller), recipient)); + uint256 nonce = allocator.nonces(nonceKey); + assertEq(nonce, 1); + + assertTrue( + allocator.isClaimAuthorized( + _createClaimHash( + recipient, arbiter, nonce, defaultExpiration, _idsAndAmountsToCommitments(idsAndAmounts), bytes32(0) + ), + arbiter, + recipient, + nonce, + defaultExpiration, + idsAndAmounts, + '' + ) + ); + } + + function test_executeAllocation_revert_InvalidBalanceChange_noDeposit() public { + // Prepare only, no deposit → newBalance <= oldBalance → InvalidBalanceChange + uint256 amount = defaultAmount; + uint256[2][] memory idsAndAmounts = _idsAndAmountsFor(address(usdc), amount); + + // Give recipient a prior ERC6909 balance so revert is not (0,0) + bytes12 lockTag = _toLockTag(address(allocator), Scope.Multichain, ResetPeriod.TenMinutes); + vm.startPrank(user); + usdc.mint(user, amount); + usdc.approve(address(compact), amount); + compact.depositERC20(address(usdc), lockTag, amount, recipient); + vm.stopPrank(); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSignature('InvalidBalanceChange(uint256,uint256)', amount, amount)); + allocationCaller.onChainAllocation( + recipient, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0), 3 + ); + } + + function test_executeAllocation_revert_InvalidPreparation_replaySameTx() public { + // First execute succeeds; second execute in same tx (without new prepare) must fail with InvalidPreparation + uint256 amount = defaultAmount; + uint256[2][] memory idsAndAmounts = _idsAndAmountsFor(address(usdc), amount); + + // fund and approve caller for deposit + usdc.mint(address(allocationCaller), amount); + vm.prank(address(allocationCaller)); + usdc.approve(address(compact), amount); + + vm.prank(user); + vm.expectRevert(AllocatorLib.InvalidPreparation.selector); + // todo=4 triggers deposit+register + execute, then a second execute at function end + allocationCaller.onChainAllocation( + recipient, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0), 4 + ); + } + + function test_executeAllocation_fullAllocation_preventsFurtherAllocate() public { + uint256 amount = defaultAmount; + uint256[2][] memory idsAndAmounts = _idsAndAmountsFor(address(usdc), amount); + + // fund and approve caller for deposit + usdc.mint(address(allocationCaller), amount); + vm.prank(address(allocationCaller)); + usdc.approve(address(compact), amount); + + // perform correct prepare + deposit + register + execute + vm.prank(user); + allocationCaller.onChainAllocation( + recipient, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0), 0 + ); + + // Now the whole balance is allocated for recipient; another allocate should fail + Lock[] memory commitments = new Lock[](1); + commitments[0] = _makeLock(address(usdc), 1); + + uint256 id = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); + + vm.prank(recipient); + vm.expectRevert(abi.encodeWithSelector(IOnChainAllocator.InsufficientBalance.selector, recipient, id, 0, 1)); + allocator.allocate(commitments, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0)); + } + + function test_executeAllocation_revert_InvalidAmount_largeDeposit() public { + // Deposit an amount > uint224.max so executeAllocation reverts on range check + uint256 largeAmount = uint256(type(uint224).max) + 1; + uint256[2][] memory idsAndAmounts = _idsAndAmountsFor(address(usdc), largeAmount); + + // fund and approve caller for large amount + usdc.mint(address(allocationCaller), largeAmount); + vm.prank(address(allocationCaller)); + usdc.approve(address(compact), largeAmount); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(IOnChainAllocator.InvalidAmount.selector, largeAmount)); + allocationCaller.onChainAllocation( + recipient, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0), 0 + ); + } + function test_allocateAndRegister_revert_InvalidAmount() public { Lock[] memory commitments = new Lock[](1); commitments[0] = _makeLock(address(usdc), uint256(type(uint224).max) + 1); From ce4fff2607831ab08f4c487ace9c19120eeb16ea Mon Sep 17 00:00:00 2001 From: mgretzke Date: Sun, 10 Aug 2025 00:44:35 +0200 Subject: [PATCH 50/63] fixed issues --- snapshots/ERC7683Allocator_open.json | 2 +- snapshots/ERC7683Allocator_openFor.json | 4 +- snapshots/HybridAllocatorTest.json | 2 +- snapshots/OnChainAllocatorTest.json | 4 +- src/allocators/OnChainAllocator.sol | 31 ++++++------ src/allocators/lib/AllocatorLib.sol | 13 +++-- test/OnChainAllocator.t.sol | 63 +++++++++++++++++++++++++ 7 files changed, 94 insertions(+), 25 deletions(-) diff --git a/snapshots/ERC7683Allocator_open.json b/snapshots/ERC7683Allocator_open.json index 8afb718..a4c31c3 100644 --- a/snapshots/ERC7683Allocator_open.json +++ b/snapshots/ERC7683Allocator_open.json @@ -1,3 +1,3 @@ { - "open_simpleOrder": "184684" + "open_simpleOrder": "184704" } diff --git a/snapshots/ERC7683Allocator_openFor.json b/snapshots/ERC7683Allocator_openFor.json index 5cd0f22..4988ce8 100644 --- a/snapshots/ERC7683Allocator_openFor.json +++ b/snapshots/ERC7683Allocator_openFor.json @@ -1,4 +1,4 @@ { - "openFor_simpleOrder_relayed": "166923", - "openFor_simpleOrder_userHimself": "166957" + "openFor_simpleOrder_relayed": "166911", + "openFor_simpleOrder_userHimself": "166943" } diff --git a/snapshots/HybridAllocatorTest.json b/snapshots/HybridAllocatorTest.json index f98edfa..1ed904b 100644 --- a/snapshots/HybridAllocatorTest.json +++ b/snapshots/HybridAllocatorTest.json @@ -6,5 +6,5 @@ "allocateAndRegister_nativeToken_emptyAmountInput": "139141", "allocateAndRegister_second_erc20Token": "114975", "allocateAndRegister_second_nativeToken": "104941", - "hybrid_execute_single": "174260" + "hybrid_execute_single": "174518" } diff --git a/snapshots/OnChainAllocatorTest.json b/snapshots/OnChainAllocatorTest.json index aa74cd1..41bd749 100644 --- a/snapshots/OnChainAllocatorTest.json +++ b/snapshots/OnChainAllocatorTest.json @@ -4,6 +4,6 @@ "allocate_erc20": "129411", "allocate_native": "129171", "allocate_second_erc20": "97423", - "onchain_execute_double": "347964", - "onchain_execute_single": "220065" + "onchain_execute_double": "348306", + "onchain_execute_single": "220296" } diff --git a/src/allocators/OnChainAllocator.sol b/src/allocators/OnChainAllocator.sol index f7dfd58..e67d6ec 100644 --- a/src/allocators/OnChainAllocator.sol +++ b/src/allocators/OnChainAllocator.sol @@ -157,8 +157,9 @@ contract OnChainAllocator is IOnChainAllocator { bytes calldata /* orderData */ ) external returns (uint256 nonce) { bytes32 nonceId = _toNonceId(msg.sender, recipient); + uint32 expiration = uint32(expires); nonce = nonces[nonceId] + 1; - AL.prepareAllocation(COMPACT_CONTRACT, nonce, recipient, idsAndAmounts, arbiter, expires, typehash, witness); + AL.prepareAllocation(COMPACT_CONTRACT, nonce, recipient, idsAndAmounts, arbiter, expiration, typehash, witness); return nonce; } @@ -436,21 +437,23 @@ contract OnChainAllocator is IOnChainAllocator { mstore(0x00, lengthSlot) let contentSlot := keccak256(0x00, 0x20) for { let i := 0 } lt(i, length) { i := add(i, 1) } { - let slot2 := add(contentSlot, add(mul(i, 2), 1)) // add 0x20 to skip the expires/amount slot - let content2 := sload(slot2) - if eq(content2, claimHash) { - // delete the allocation - let lastSlot := add(contentSlot, mul(sub(length, 1), 0x40)) - if iszero(eq(sub(slot2, 0x20), lastSlot)) { - // is not the last allocation of the array - let contentLast1 := sload(lastSlot) - let contentLast2 := sload(add(lastSlot, 0x20)) - sstore(sub(slot2, 0x20), contentLast1) - sstore(slot2, contentLast2) + // Each allocation occupies two consecutive slots: + // first: packed expires/amount; second: claimHash + let first := add(contentSlot, mul(i, 2)) + let second := add(first, 1) + if eq(sload(second), claimHash) { + // Swap-and-pop delete + let lastFirst := add(contentSlot, mul(sub(length, 1), 2)) + let lastSecond := add(lastFirst, 1) + if iszero(eq(first, lastFirst)) { + let contentLast1 := sload(lastFirst) + let contentLast2 := sload(lastSecond) + sstore(first, contentLast1) + sstore(second, contentLast2) } - sstore(lastSlot, 0) - sstore(add(lastSlot, 0x20), 0) + sstore(lastFirst, 0) + sstore(lastSecond, 0) // update the array length sstore(lengthSlot, sub(length, 1)) diff --git a/src/allocators/lib/AllocatorLib.sol b/src/allocators/lib/AllocatorLib.sol index daa98b0..02a55c1 100644 --- a/src/allocators/lib/AllocatorLib.sol +++ b/src/allocators/lib/AllocatorLib.sol @@ -33,14 +33,15 @@ library AllocatorLib { // Store the current balance to calculate the deposited amounts in `executeAllocation` uint256 currentBalance = ERC6909(compactContract).balanceOf(recipient, id); assembly ("memory-safe") { - tstore(or(PREPARE_ALLOCATION_SELECTOR, id), currentBalance) + mstore(0x00, PREPARE_ALLOCATION_SELECTOR) + mstore(0x20, id) + tstore(keccak256(0x00, 0x40), currentBalance) } } // Store the nonce for the identifier to ensure the same data is used in `executeAllocation` and protect against replay attacks - bytes32 identifier = keccak256( - abi.encode(PREPARE_ALLOCATION_SELECTOR, recipient, ids, arbiter, uint32(expires), typehash, witness) - ); + bytes32 identifier = + keccak256(abi.encode(PREPARE_ALLOCATION_SELECTOR, recipient, ids, arbiter, expires, typehash, witness)); assembly ("memory-safe") { tstore(identifier, nonce) } @@ -204,7 +205,9 @@ library AllocatorLib { // Calculate the balance uint256 oldBalance; assembly ("memory-safe") { - oldBalance := tload(or(PREPARE_ALLOCATION_SELECTOR, id)) + mstore(0x00, PREPARE_ALLOCATION_SELECTOR) + mstore(0x20, id) + oldBalance := tload(keccak256(0x00, 0x40)) } uint256 newBalance = ERC6909(compactContract).balanceOf(recipient, id); if (newBalance <= oldBalance) { diff --git a/test/OnChainAllocator.t.sol b/test/OnChainAllocator.t.sol index 0ddbdd5..4d725c0 100644 --- a/test/OnChainAllocator.t.sol +++ b/test/OnChainAllocator.t.sol @@ -712,6 +712,69 @@ contract OnChainAllocatorTest is Test, TestHelper { allocator.authorizeClaim(claimHash, arbiter, user, 1, defaultExpiration, idsAndAmounts, ''); } + function test_authorizeClaim_deletesMiddleOfMultipleAllocations_correctly() public { + // Prepare a large ERC20 deposit so three allocations can be made for the same id. + uint256 amount1 = 1 ether; + uint256 amount2 = 2 ether; + uint256 amount3 = 3 ether; + uint256 total = amount1 + amount2 + amount3; + + // Deposit ERC20 into Compact for the user + bytes12 lockTag = _toLockTag(address(allocator), Scope.Multichain, ResetPeriod.TenMinutes); + vm.startPrank(user); + usdc.mint(user, total); + usdc.approve(address(compact), total); + compact.depositERC20(address(usdc), lockTag, total, user); + vm.stopPrank(); + + // Make three allocations for the same id with increasing expirations + Lock[] memory commitments = new Lock[](1); + commitments[0] = Lock({lockTag: lockTag, token: address(usdc), amount: amount1}); + bytes32 claimHash1; + { + vm.prank(user); + (claimHash1,) = allocator.allocate(commitments, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, ''); + } + + bytes32 claimHash2; + { + commitments[0].amount = amount2; + vm.prank(user); + (claimHash2,) = allocator.allocate(commitments, arbiter, defaultExpiration + 10, BATCH_COMPACT_TYPEHASH, ''); + } + + bytes32 claimHash3; + { + commitments[0].amount = amount3; + vm.prank(user); + (claimHash3,) = allocator.allocate(commitments, arbiter, defaultExpiration + 20, BATCH_COMPACT_TYPEHASH, ''); + } + + // idsAndAmounts used by authorizeClaim (amount is not used for verification but keep it consistent) + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); + idsAndAmounts[0][1] = amount1; + + // 1) Delete the MIDDLE allocation first (claimHash2). This exercises swap-and-pop correctness. + vm.prank(address(compact)); + bytes4 sel = allocator.authorizeClaim(claimHash2, arbiter, user, 0, defaultExpiration, idsAndAmounts, ''); + assertEq(sel, IAllocator.authorizeClaim.selector); + + // 2) The other allocations must still be present and independently deletable. + vm.prank(address(compact)); + sel = allocator.authorizeClaim(claimHash3, arbiter, user, 0, defaultExpiration, idsAndAmounts, ''); + assertEq(sel, IAllocator.authorizeClaim.selector); + + vm.prank(address(compact)); + sel = allocator.authorizeClaim(claimHash1, arbiter, user, 0, defaultExpiration, idsAndAmounts, ''); + assertEq(sel, IAllocator.authorizeClaim.selector); + + // 3) All allocations are deleted now; reusing any claim should revert. + vm.prank(address(compact)); + vm.expectRevert(abi.encodeWithSelector(IOnChainAllocator.InvalidClaim.selector, claimHash1)); + allocator.authorizeClaim(claimHash1, arbiter, user, 0, defaultExpiration, idsAndAmounts, ''); + } + /* --------------------------------------------------------------------- */ /* attest */ /* --------------------------------------------------------------------- */ From a1c74eb14d75fd51bfeae8e454fc09e9f9f2b48b Mon Sep 17 00:00:00 2001 From: mgretzke Date: Sun, 10 Aug 2025 21:06:43 +0200 Subject: [PATCH 51/63] improved signer management --- src/allocators/HybridAllocator.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/allocators/HybridAllocator.sol b/src/allocators/HybridAllocator.sol index e421932..86efadb 100644 --- a/src/allocators/HybridAllocator.sol +++ b/src/allocators/HybridAllocator.sol @@ -48,7 +48,7 @@ contract HybridAllocator is IHybridAllocator { /// @inheritdoc IHybridAllocator function addSigner(address signer_) external onlySigner { - if (signer_ == address(0)) { + if (signer_ == address(0) || signers[signer_]) { revert InvalidSigner(); } signers[signer_] = true; @@ -57,7 +57,7 @@ contract HybridAllocator is IHybridAllocator { /// @inheritdoc IHybridAllocator function removeSigner(address signer_) external onlySigner { - if (signerCount == 1) { + if (signerCount == 1 || !signers[signer_]) { revert LastSigner(); } signers[signer_] = false; @@ -66,7 +66,7 @@ contract HybridAllocator is IHybridAllocator { /// @inheritdoc IHybridAllocator function replaceSigner(address newSigner_) external onlySigner { - if (newSigner_ == address(0)) { + if (newSigner_ == address(0) || signers[newSigner_]) { revert InvalidSigner(); } signers[msg.sender] = false; From 242fb55790007f73f9d546b221440f91caf863b2 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Sun, 10 Aug 2025 21:30:29 +0200 Subject: [PATCH 52/63] use IOnChainAllocation directly from the-compact --- lib/forge-std | 2 +- lib/openzeppelin-contracts | 2 +- lib/solady | 2 +- lib/the-compact | 2 +- snapshots/HybridAllocatorTest.json | 18 ++++---- snapshots/OnChainAllocatorTest.json | 16 +++---- src/interfaces/IHybridAllocator.sol | 14 +------ src/interfaces/IOnChainAllocation.sol | 60 --------------------------- src/interfaces/IOnChainAllocator.sol | 18 +------- src/test/OnChainAllocationCaller.sol | 2 +- test/ERC7683Allocator.t.sol | 5 ++- test/OnChainAllocator.t.sol | 7 ++-- 12 files changed, 33 insertions(+), 115 deletions(-) delete mode 100644 src/interfaces/IOnChainAllocation.sol diff --git a/lib/forge-std b/lib/forge-std index 60acb7a..c7be2a3 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 60acb7aaadcce2d68e52986a0a66fe79f07d138f +Subproject commit c7be2a3481f9e51230880bb0949072c7e3a4da82 diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index 56fe41c..99eda22 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 56fe41c5882fa05348e0abd87d3501fce7a94b15 +Subproject commit 99eda2225c0246c265c902475c47ec0c6321f119 diff --git a/lib/solady b/lib/solady index 6da40ac..834bbc4 160000 --- a/lib/solady +++ b/lib/solady @@ -1 +1 @@ -Subproject commit 6da40ac63da5657d6c4d3ce3e3a99bc5862dda53 +Subproject commit 834bbc4fd366ca8bce8c532a0e3b34eca6be709c diff --git a/lib/the-compact b/lib/the-compact index 8079c4f..c5c9ad7 160000 --- a/lib/the-compact +++ b/lib/the-compact @@ -1 +1 @@ -Subproject commit 8079c4f63ddc6161c6c9be4681f89a2df2827931 +Subproject commit c5c9ad7aec1f4783f43f26187f7a0dbaa9c65078 diff --git a/snapshots/HybridAllocatorTest.json b/snapshots/HybridAllocatorTest.json index 1ed904b..77b955b 100644 --- a/snapshots/HybridAllocatorTest.json +++ b/snapshots/HybridAllocatorTest.json @@ -1,10 +1,10 @@ { - "allocateAndRegister_erc20Token": "187769", - "allocateAndRegister_erc20Token_emptyAmountInput": "188679", - "allocateAndRegister_multipleTokens": "223675", - "allocateAndRegister_nativeToken": "139305", - "allocateAndRegister_nativeToken_emptyAmountInput": "139141", - "allocateAndRegister_second_erc20Token": "114975", - "allocateAndRegister_second_nativeToken": "104941", - "hybrid_execute_single": "174518" -} + "allocateAndRegister_erc20Token": "187690", + "allocateAndRegister_erc20Token_emptyAmountInput": "188600", + "allocateAndRegister_multipleTokens": "223596", + "allocateAndRegister_nativeToken": "139226", + "allocateAndRegister_nativeToken_emptyAmountInput": "139062", + "allocateAndRegister_second_erc20Token": "114896", + "allocateAndRegister_second_nativeToken": "104862", + "hybrid_execute_single": "174439" +} \ No newline at end of file diff --git a/snapshots/OnChainAllocatorTest.json b/snapshots/OnChainAllocatorTest.json index 41bd749..6553d18 100644 --- a/snapshots/OnChainAllocatorTest.json +++ b/snapshots/OnChainAllocatorTest.json @@ -1,9 +1,9 @@ { - "allocateFor_success_withRegistration": "133994", - "allocate_and_delete_expired_allocation": "66140", - "allocate_erc20": "129411", - "allocate_native": "129171", - "allocate_second_erc20": "97423", - "onchain_execute_double": "348306", - "onchain_execute_single": "220296" -} + "allocateFor_success_withRegistration": "133994", + "allocate_and_delete_expired_allocation": "66140", + "allocate_erc20": "129411", + "allocate_native": "129171", + "allocate_second_erc20": "97423", + "onchain_execute_double": "348227", + "onchain_execute_single": "220217" +} \ No newline at end of file diff --git a/src/interfaces/IHybridAllocator.sol b/src/interfaces/IHybridAllocator.sol index 1251dea..bf03104 100644 --- a/src/interfaces/IHybridAllocator.sol +++ b/src/interfaces/IHybridAllocator.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.27; -import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; +import {IOnChainAllocation} from '@uniswap/the-compact/interfaces/IOnChainAllocation.sol'; import {Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; -interface IHybridAllocator is IAllocator { +interface IHybridAllocator is IOnChainAllocation { error Unsupported(); error InvalidIds(); error InvalidAllocatorId(uint96 allocatorId, uint96 expectedAllocatorId); @@ -14,16 +14,6 @@ interface IHybridAllocator is IAllocator { error InvalidSigner(); error LastSigner(); error InvalidValue(uint256 value, uint256 expectedValue); - error InvalidRegistration(address sponsor, bytes32 claimHash); - error InvalidPreparation(); - - /// @notice Emitted when a tokens are successfully allocated - /// @param sponsor The address of the sponsor - /// @param commitments The commitments of the allocations - /// @param nonce The nonce of the allocation - /// @param expires The expiration of the allocation - /// @param claimHash The hash of the allocation - event Allocated(address indexed sponsor, Lock[] commitments, uint256 nonce, uint256 expires, bytes32 claimHash); /** * @notice Add an offchain signer to the allocator. diff --git a/src/interfaces/IOnChainAllocation.sol b/src/interfaces/IOnChainAllocation.sol deleted file mode 100644 index 7ff6e47..0000000 --- a/src/interfaces/IOnChainAllocation.sol +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.27; - -import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; -import {Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; - -interface IOnChainAllocation is IAllocator { - error InvalidPreparation(); - error InvalidRegistration(address sponsor, bytes32 claimHash); - - /// @notice Emitted when a tokens are successfully allocated - /// @param sponsor The address of the sponsor - /// @param commitments The commitments of the allocations - /// @param nonce The nonce of the allocation - /// @param expires The expiration of the allocation - /// @param claimHash The hash of the allocation - event Allocated(address indexed sponsor, Lock[] commitments, uint256 nonce, uint256 expires, bytes32 claimHash); - - /** - * @notice Allows to create an allocation on behalf of a recipient without the contract being in control over the funds. - * @notice Will typically be used in combination with `batchDepositAndRegisterFor` on the compact. - * @dev Must be called before `executeAllocation` to ensure a valid balance change has occurred for the recipient. - * @param recipient The account to receive the tokens. - * @param idsAndAmounts The ids and amounts to allocate. - * @param arbiter The account tasked with verifying and submitting the claim. - * @param expires The time at which the claim expires. - * @param typehash The typehash of the claim. - * @param witness The witness of the claim. - * @return nonce The next valid nonce. It is only guaranteed that the nonce is valid within the same transaction.. - */ - function prepareAllocation( - address recipient, - uint256[2][] calldata idsAndAmounts, - address arbiter, - uint256 expires, - bytes32 typehash, - bytes32 witness, - bytes calldata orderData - ) external returns (uint256 nonce); - - /** - * @notice Executes an allocation on behalf of a recipient. - * @dev Must be called after `prepareAllocation` to ensure a valid balance change has occurred for the recipient. - * @param recipient The account to receive the tokens. - * @param idsAndAmounts The ids and amounts to allocate. - * @param arbiter The account tasked with verifying and submitting the claim. - * @param expires The time at which the claim expires. - * @param typehash The typehash of the claim. - * @param witness The witness of the claim. - */ - function executeAllocation( - address recipient, - uint256[2][] calldata idsAndAmounts, - address arbiter, - uint256 expires, - bytes32 typehash, - bytes32 witness, - bytes calldata orderData - ) external; -} diff --git a/src/interfaces/IOnChainAllocator.sol b/src/interfaces/IOnChainAllocator.sol index 677f250..a27ad39 100644 --- a/src/interfaces/IOnChainAllocator.sol +++ b/src/interfaces/IOnChainAllocator.sol @@ -2,10 +2,10 @@ pragma solidity ^0.8.27; -import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; +import {IOnChainAllocation} from '@uniswap/the-compact/interfaces/IOnChainAllocation.sol'; import {Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; -interface IOnChainAllocator is IAllocator { +interface IOnChainAllocator is IOnChainAllocation { struct Allocation { uint32 expires; uint224 amount; @@ -46,20 +46,6 @@ interface IOnChainAllocator is IAllocator { /// @notice Thrown if the provided signature is invalid error InvalidSignature(address signer, address expectedSigner); - /// @notice Thrown if the claim hash is not registered on the compact - error InvalidRegistration(address sponsor, bytes32 claimHash); - - /// @notice Thrown if the preparation was not called within the same transaction or the data is not the same - error InvalidPreparation(); - - /// @notice Emitted when a tokens are successfully allocated - /// @param sponsor The address of the sponsor - /// @param commitments The commitments of the allocations - /// @param nonce The nonce of the allocation - /// @param expires The expiration of the allocation - /// @param claimHash The hash of the allocation - event Allocated(address indexed sponsor, Lock[] commitments, uint256 nonce, uint256 expires, bytes32 claimHash); - /// @notice Registers an allocation for a set of tokens /// @param commitments The commitments of the allocations /// @param arbiter The arbiter of the allocation diff --git a/src/test/OnChainAllocationCaller.sol b/src/test/OnChainAllocationCaller.sol index 133caf9..1db8bff 100644 --- a/src/test/OnChainAllocationCaller.sol +++ b/src/test/OnChainAllocationCaller.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.27; -import {IOnChainAllocation} from '../interfaces/IOnChainAllocation.sol'; +import {IOnChainAllocation} from '@uniswap/the-compact/interfaces/IOnChainAllocation.sol'; import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; contract OnChainAllocationCaller { diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index 6e34bcb..4aef2e9 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -19,11 +19,12 @@ import {Test} from 'forge-std/Test.sol'; import {TestHelper} from 'test/util/TestHelper.sol'; +import {IOnChainAllocation} from '@uniswap/the-compact/interfaces/IOnChainAllocation.sol'; import {ERC7683Allocator} from 'src/allocators/ERC7683Allocator.sol'; import {BatchClaim as TribunalClaim, Mandate} from 'src/allocators/types/TribunalStructs.sol'; import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; import {IERC7683Allocator} from 'src/interfaces/IERC7683Allocator.sol'; -import {IOnChainAllocator} from 'src/interfaces/IOnchainAllocator.sol'; +import {IOnChainAllocator} from 'src/interfaces/IOnChainAllocator.sol'; import {ERC20Mock} from 'src/test/ERC20Mock.sol'; @@ -512,7 +513,7 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { bytes32 claimHash = _hashCompact(compact_, mandate_); vm.prank(user); - vm.expectRevert(abi.encodeWithSelector(IOnChainAllocator.InvalidRegistration.selector, user, claimHash)); + vm.expectRevert(abi.encodeWithSelector(IOnChainAllocation.InvalidRegistration.selector, user, claimHash)); erc7683Allocator.open(onChainCrossChainOrder_); } diff --git a/test/OnChainAllocator.t.sol b/test/OnChainAllocator.t.sol index 4d725c0..fd7bd18 100644 --- a/test/OnChainAllocator.t.sol +++ b/test/OnChainAllocator.t.sol @@ -17,6 +17,7 @@ import {TheCompact} from '@uniswap/the-compact/TheCompact.sol'; import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; +import {IOnChainAllocation} from '@uniswap/the-compact/interfaces/IOnChainAllocation.sol'; import {OnChainAllocator} from 'src/allocators/OnChainAllocator.sol'; import {IOnChainAllocator} from 'src/interfaces/IOnChainAllocator.sol'; @@ -309,7 +310,7 @@ contract OnChainAllocatorTest is Test, TestHelper { vm.prank(user); vm.expectEmit(true, true, true, true); - emit IOnChainAllocator.Allocated(user, commitments, 1, defaultExpiration, claimHash); + emit IOnChainAllocation.Allocated(user, commitments, 1, defaultExpiration, claimHash); (bytes32 returnedClaimHash, uint256 nonce) = allocator.allocate(commitments, arbiter, defaultExpiration, typehash, witness); @@ -341,7 +342,7 @@ contract OnChainAllocatorTest is Test, TestHelper { // expect a successful second allocation claimHash = _createClaimHash(user, arbiter, 2, defaultExpiration, commitments, witness); vm.expectEmit(true, true, true, true); - emit IOnChainAllocator.Allocated(user, commitments, 2, defaultExpiration, claimHash); + emit IOnChainAllocation.Allocated(user, commitments, 2, defaultExpiration, claimHash); } (claimHash, nonce) = allocator.allocate(commitments, arbiter, defaultExpiration, typehash, witness); @@ -562,7 +563,7 @@ contract OnChainAllocatorTest is Test, TestHelper { // Expect InvalidRegistration revert because claimHash is NOT registered on The Compact vm.prank(relayer); - vm.expectRevert(abi.encodeWithSelector(IOnChainAllocator.InvalidRegistration.selector, user, claimHash)); + vm.expectRevert(abi.encodeWithSelector(IOnChainAllocation.InvalidRegistration.selector, user, claimHash)); allocator.allocateFor( user, commitments, From 1f54dfe5afedadf18a26b0d45fef1de7ac4b6595 Mon Sep 17 00:00:00 2001 From: KumaCrypto Date: Mon, 18 Aug 2025 15:43:38 +0100 Subject: [PATCH 53/63] chore: remove unused imports --- ...hain-55d324d7-a2d8-4fdd-a836-48ff3942a26d.json | 1 + remappings.txt | 15 +++++++++++++++ src/allocators/HybridAllocator.sol | 3 +-- src/allocators/OnChainAllocator.sol | 2 +- src/interfaces/IHybridAllocator.sol | 1 - src/interfaces/IHybridERC7683.sol | 1 - 6 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 .wake/extension/local-local-chain-55d324d7-a2d8-4fdd-a836-48ff3942a26d.json create mode 100644 remappings.txt diff --git a/.wake/extension/local-local-chain-55d324d7-a2d8-4fdd-a836-48ff3942a26d.json b/.wake/extension/local-local-chain-55d324d7-a2d8-4fdd-a836-48ff3942a26d.json new file mode 100644 index 0000000..e6938b6 --- /dev/null +++ b/.wake/extension/local-local-chain-55d324d7-a2d8-4fdd-a836-48ff3942a26d.json @@ -0,0 +1 @@ +{"type":"local_node","id":"local-chain-55d324d7-a2d8-4fdd-a836-48ff3942a26d","displayName":"Local Chain 1","state":{"accounts":[{"address":"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266","balance":1e+22},{"address":"0x70997970c51812dc3a010c7d01b50e0d17dc79c8","balance":1e+22},{"address":"0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc","balance":1e+22},{"address":"0x90f79bf6eb2c4f870365e785982e1f101e93b906","balance":1e+22},{"address":"0x15d34aaf54267db7d7c367839aaf71a00a2c6a65","balance":1e+22},{"address":"0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc","balance":1e+22},{"address":"0x976ea74026e726554db657fa54763abd0c3a0aa9","balance":1e+22},{"address":"0x14dc79964da2c08b23698b3d3cc7ca32193d9955","balance":1e+22},{"address":"0x23618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f","balance":1e+22},{"address":"0xa0ee7a142d267c1f36714e4a8f75612f20a79720","balance":1e+22}],"deployment":[],"history":[]},"network":{"wakeDump":{"metadata":{"labels":{},"deployedLibraries":{}},"chainDump":"0x1f8b08000000000000ffed564d6b243710fd2f3acfa1f451926a8e266473c82104f6b42ca6542ad98d67ba9dee767682f17f0f9a191b76314b96f42510f545adaff754925ebd67530e933c98fdb3199f8e4567b3377002b33345476d830c3cff7569fb87c5eccc3a1c7559f9f8789e68cdcedcf1727b188ec36af6fe3a6e670a2fda54cddebe4dde993ab436c8d361bda29a9d799cf5cf99c7cad30f11f90ec17298caad9e4497e5b653e3b1de3ece83688fc3b5fd3ce68e17b3ef54af3fd741f6e5656758647a1ad7a5cf81930d5512510c959d402ece47cac5572f9284bdb3e42b11e239d2d3d8573987e0c0e7ba8193b36a4941a87071e195aa4cf5d26d766659a799ef3ac98e0f278bd507e686c1c5544baa497c4cd913734b9601d849e4b829a6f3d166cd567d43a92d350ce26bc4965a11e0d2b038abb96d89e92504a9b596c804d0d815cc58ab2352f0d5bac6c1912fb2256650a412420ea9f8449832e64c0e84536ea5b928500261fc0ee65708a9fdbba2e023d80800d94670909d4f193c66979d0f0d3358b418c177aa36bb56b164c80e1d42ff22d81001a4f977f69a80285102419badabe2192c48aa600b8242b5a9df6dc95bc697a0252a2d6a71125a4ee0236aca48d9a96d16ac922f047153cc149553001735b988186a89981a6348d173a9d077ce4c9b62524484542d23161117090573611b5bf1a9664b0538d44def2e836a621b5c7531896d3e261b34706e09a375cd01274a0eb6c46c9e5a8d8a96b9e6dc620ba2914bce2eb944d25a2be45cdcec3c5f7a865ad62ed3f270fb9abb2e522d0f8bd97f7a36f7cab5b73e9b479e755c7fe1e57eab14b2dcb3ff38ca41974b92ab22a16aae2a096b622e190bc654a2480d96abb72ea02d1432a7607d837e3ead06a8817c4866678ec3f89a7e7f80c6caabfe3e4deb56fb5a671e17967598c6e56d5d8c9a6db3c91611448ead651f5023398196a3620959c116a2285c05c046e75a41f5d197e0ba03985574785c972db91ea6bbe5e6304dc74d16fcbffcb78b79d73c7ee36aef78f9f5e243fba315129fafcd1f17ad6fc3be76b031532dd5f5d4a9a775e69f78e5574d3a0ea72d65e52a8def2cd66d2b2ffab3ea6f3a7fe8beb43ba3422c7ceefc32acf775e62f7cd8f4dd76e7fbe19be85c1cf2cda5c7bcd9f4b3c6de28cb34de7411def2a9cffac7932eebf2166bf505240447b989151b885b69413251ec9926b8c41aac8618a8900fc28190c89694d1958c685ebed63ab3fff47967a6e351e773fde5f37bfdf743cf4183f0e1f6acbc8bd98f4f87c3cbdf3d511d30470d0000"},"type":"Local Chain","config":{"sessionId":"local-chain-55d324d7-a2d8-4fdd-a836-48ff3942a26d","type":"anvil","uri":"ws://127.0.0.1:51470"}},"stateFingerprint":"3a82e3a6d1ac2e4daae286769e6107faa655d321408b0b1f9df23ede1a63b531","persistence":{"isDirty":false,"isAutosaveEnabled":true,"lastSaveTimestamp":1755168035355}} \ No newline at end of file diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..ae2b15e --- /dev/null +++ b/remappings.txt @@ -0,0 +1,15 @@ +forge-std/=lib/forge-std/src/ +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ +@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ +@uniswap/the-compact/=lib/the-compact/src/ +@solady/=lib/solady/src/ +ds-test/=lib/the-compact/lib/permit2/lib/forge-std/lib/ds-test/src/ +erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/ +forge-gas-snapshot/=lib/forge-gas-snapshot/src/ +halmos-cheatcodes/=lib/openzeppelin-contracts/lib/halmos-cheatcodes/src/ +openzeppelin-contracts/=lib/openzeppelin-contracts/ +permit2/=lib/the-compact/lib/permit2/ +solady/=lib/solady/src/ +soledge/=lib/the-compact/lib/soledge/src/ +solmate/=lib/the-compact/lib/permit2/lib/solmate/ +the-compact/=lib/the-compact/ diff --git a/src/allocators/HybridAllocator.sol b/src/allocators/HybridAllocator.sol index 86efadb..8693192 100644 --- a/src/allocators/HybridAllocator.sol +++ b/src/allocators/HybridAllocator.sol @@ -3,12 +3,11 @@ pragma solidity ^0.8.27; import {SafeTransferLib} from '@solady/utils/SafeTransferLib.sol'; -import {BatchCompact, LOCK_TYPEHASH, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; +import {Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {AllocatorLib as AL} from './lib/AllocatorLib.sol'; -import {ERC6909} from '@solady/tokens/ERC6909.sol'; import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; import {IHybridAllocator} from 'src/interfaces/IHybridAllocator.sol'; diff --git a/src/allocators/OnChainAllocator.sol b/src/allocators/OnChainAllocator.sol index e67d6ec..564aa23 100644 --- a/src/allocators/OnChainAllocator.sol +++ b/src/allocators/OnChainAllocator.sol @@ -10,7 +10,7 @@ import {ERC6909} from '@solady/tokens/ERC6909.sol'; import {SafeTransferLib} from '@solady/utils/SafeTransferLib.sol'; import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; -import {LOCK_TYPEHASH, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; +import {Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; /// @title OnChainAllocator /// @notice Allocates tokens deposited into the compact. diff --git a/src/interfaces/IHybridAllocator.sol b/src/interfaces/IHybridAllocator.sol index bf03104..d7f7a19 100644 --- a/src/interfaces/IHybridAllocator.sol +++ b/src/interfaces/IHybridAllocator.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.27; import {IOnChainAllocation} from '@uniswap/the-compact/interfaces/IOnChainAllocation.sol'; -import {Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; interface IHybridAllocator is IOnChainAllocation { error Unsupported(); diff --git a/src/interfaces/IHybridERC7683.sol b/src/interfaces/IHybridERC7683.sol index 7db3d8c..07a0958 100644 --- a/src/interfaces/IHybridERC7683.sol +++ b/src/interfaces/IHybridERC7683.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.27; -import {Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; import {IHybridAllocator} from 'src/interfaces/IHybridAllocator.sol'; From 94d1b02e68e8644fca45b9554330125b12686eae Mon Sep 17 00:00:00 2001 From: KumaCrypto Date: Mon, 18 Aug 2025 15:46:17 +0100 Subject: [PATCH 54/63] chore: remove unused variable --- ...hain-55d324d7-a2d8-4fdd-a836-48ff3942a26d.json | 1 - remappings.txt | 15 --------------- src/allocators/HybridAllocator.sol | 2 -- 3 files changed, 18 deletions(-) delete mode 100644 .wake/extension/local-local-chain-55d324d7-a2d8-4fdd-a836-48ff3942a26d.json delete mode 100644 remappings.txt diff --git a/.wake/extension/local-local-chain-55d324d7-a2d8-4fdd-a836-48ff3942a26d.json b/.wake/extension/local-local-chain-55d324d7-a2d8-4fdd-a836-48ff3942a26d.json deleted file mode 100644 index e6938b6..0000000 --- a/.wake/extension/local-local-chain-55d324d7-a2d8-4fdd-a836-48ff3942a26d.json +++ /dev/null @@ -1 +0,0 @@ -{"type":"local_node","id":"local-chain-55d324d7-a2d8-4fdd-a836-48ff3942a26d","displayName":"Local Chain 1","state":{"accounts":[{"address":"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266","balance":1e+22},{"address":"0x70997970c51812dc3a010c7d01b50e0d17dc79c8","balance":1e+22},{"address":"0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc","balance":1e+22},{"address":"0x90f79bf6eb2c4f870365e785982e1f101e93b906","balance":1e+22},{"address":"0x15d34aaf54267db7d7c367839aaf71a00a2c6a65","balance":1e+22},{"address":"0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc","balance":1e+22},{"address":"0x976ea74026e726554db657fa54763abd0c3a0aa9","balance":1e+22},{"address":"0x14dc79964da2c08b23698b3d3cc7ca32193d9955","balance":1e+22},{"address":"0x23618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f","balance":1e+22},{"address":"0xa0ee7a142d267c1f36714e4a8f75612f20a79720","balance":1e+22}],"deployment":[],"history":[]},"network":{"wakeDump":{"metadata":{"labels":{},"deployedLibraries":{}},"chainDump":"0x1f8b08000000000000ffed564d6b243710fd2f3acfa1f451926a8e266473c82104f6b42ca6542ad98d67ba9dee767682f17f0f9a191b76314b96f42510f545adaff754925ebd67530e933c98fdb3199f8e4567b3377002b33345476d830c3cff7569fb87c5eccc3a1c7559f9f8789e68cdcedcf1727b188ec36af6fe3a6e670a2fda54cddebe4dde993ab436c8d361bda29a9d799cf5cf99c7cad30f11f90ec17298caad9e4497e5b653e3b1de3ece83688fc3b5fd3ce68e17b3ef54af3fd741f6e5656758647a1ad7a5cf81930d5512510c959d402ece47cac5572f9284bdb3e42b11e239d2d3d8573987e0c0e7ba8193b36a4941a87071e195aa4cf5d26d766659a799ef3ac98e0f278bd507e686c1c5544baa497c4cd913734b9601d849e4b829a6f3d166cd567d43a92d350ce26bc4965a11e0d2b038abb96d89e92504a9b596c804d0d815cc58ab2352f0d5bac6c1912fb2256650a412420ea9f8449832e64c0e84536ea5b928500261fc0ee65708a9fdbba2e023d80800d94670909d4f193c66979d0f0d3358b418c177aa36bb56b164c80e1d42ff22d81001a4f977f69a80285102419badabe2192c48aa600b8242b5a9df6dc95bc697a0252a2d6a71125a4ee0236aca48d9a96d16ac922f047153cc149553001735b988186a89981a6348d173a9d077ce4c9b62524484542d23161117090573611b5bf1a9664b0538d44def2e836a621b5c7531896d3e261b34706e09a375cd01274a0eb6c46c9e5a8d8a96b9e6dc620ba2914bce2eb944d25a2be45cdcec3c5f7a865ad62ed3f270fb9abb2e522d0f8bd97f7a36f7cab5b73e9b479e755c7fe1e57eab14b2dcb3ff38ca41974b92ab22a16aae2a096b622e190bc654a2480d96abb72ea02d1432a7607d837e3ead06a8817c4866678ec3f89a7e7f80c6caabfe3e4deb56fb5a671e17967598c6e56d5d8c9a6db3c91611448ead651f5023398196a3620959c116a2285c05c046e75a41f5d197e0ba03985574785c972db91ea6bbe5e6304dc74d16fcbffcb78b79d73c7ee36aef78f9f5e243fba315129fafcd1f17ad6fc3be76b031532dd5f5d4a9a775e69f78e5574d3a0ea72d65e52a8def2cd66d2b2ffab3ea6f3a7fe8beb43ba3422c7ceefc32acf775e62f7cd8f4dd76e7fbe19be85c1cf2cda5c7bcd9f4b3c6de28cb34de7411def2a9cffac7932eebf2166bf505240447b989151b885b69413251ec9926b8c41aac8618a8900fc28190c89694d1958c685ebed63ab3fff47967a6e351e773fde5f37bfdf743cf4183f0e1f6acbc8bd98f4f87c3cbdf3d511d30470d0000"},"type":"Local Chain","config":{"sessionId":"local-chain-55d324d7-a2d8-4fdd-a836-48ff3942a26d","type":"anvil","uri":"ws://127.0.0.1:51470"}},"stateFingerprint":"3a82e3a6d1ac2e4daae286769e6107faa655d321408b0b1f9df23ede1a63b531","persistence":{"isDirty":false,"isAutosaveEnabled":true,"lastSaveTimestamp":1755168035355}} \ No newline at end of file diff --git a/remappings.txt b/remappings.txt deleted file mode 100644 index ae2b15e..0000000 --- a/remappings.txt +++ /dev/null @@ -1,15 +0,0 @@ -forge-std/=lib/forge-std/src/ -@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ -@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ -@uniswap/the-compact/=lib/the-compact/src/ -@solady/=lib/solady/src/ -ds-test/=lib/the-compact/lib/permit2/lib/forge-std/lib/ds-test/src/ -erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/ -forge-gas-snapshot/=lib/forge-gas-snapshot/src/ -halmos-cheatcodes/=lib/openzeppelin-contracts/lib/halmos-cheatcodes/src/ -openzeppelin-contracts/=lib/openzeppelin-contracts/ -permit2/=lib/the-compact/lib/permit2/ -solady/=lib/solady/src/ -soledge/=lib/the-compact/lib/soledge/src/ -solmate/=lib/the-compact/lib/permit2/lib/solmate/ -the-compact/=lib/the-compact/ diff --git a/src/allocators/HybridAllocator.sol b/src/allocators/HybridAllocator.sol index 8693192..e59737c 100644 --- a/src/allocators/HybridAllocator.sol +++ b/src/allocators/HybridAllocator.sol @@ -16,8 +16,6 @@ contract HybridAllocator is IHybridAllocator { uint96 public immutable ALLOCATOR_ID; ITheCompact internal immutable _COMPACT; bytes32 internal immutable _COMPACT_DOMAIN_SEPARATOR; - // bytes4(keccak256('prepareAllocation(address,uint256[2][],address,uint256,bytes32,bytes32,bytes)')); - bytes4 public constant PREPARE_ALLOCATION_SELECTOR = 0x7ef6597a; mapping(bytes32 => bool) internal claims; From 8484341b532479d7ed03c671539ef167aa795947 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Tue, 19 Aug 2025 18:03:22 +0200 Subject: [PATCH 55/63] updated nonce setup --- src/allocators/OnChainAllocator.sol | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/allocators/OnChainAllocator.sol b/src/allocators/OnChainAllocator.sol index e67d6ec..24f141a 100644 --- a/src/allocators/OnChainAllocator.sol +++ b/src/allocators/OnChainAllocator.sol @@ -23,7 +23,7 @@ contract OnChainAllocator is IOnChainAllocator { mapping(bytes32 tokenHash => Allocation[] allocations) internal _allocations; - mapping(bytes32 user => uint256 nonce) public nonces; + mapping(bytes32 user => uint96 nonce) public nonces; modifier onlyCompact() { if (msg.sender != COMPACT_CONTRACT) { @@ -87,7 +87,7 @@ contract OnChainAllocator is IOnChainAllocator { bytes32 typehash, bytes32 witness ) public returns (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) { - nonce = ++nonces[_toNonceId(msg.sender, recipient)]; // prevents griefing of frontrunning nonces + nonce = _getAndUpdateNonce(msg.sender, recipient); uint256[2][] memory idsAndAmounts = new uint256[2][](commitments.length); @@ -156,9 +156,8 @@ contract OnChainAllocator is IOnChainAllocator { bytes32 witness, bytes calldata /* orderData */ ) external returns (uint256 nonce) { - bytes32 nonceId = _toNonceId(msg.sender, recipient); uint32 expiration = uint32(expires); - nonce = nonces[nonceId] + 1; + nonce = _getAndUpdateNonce(msg.sender, recipient); AL.prepareAllocation(COMPACT_CONTRACT, nonce, recipient, idsAndAmounts, arbiter, expiration, typehash, witness); return nonce; @@ -173,7 +172,7 @@ contract OnChainAllocator is IOnChainAllocator { bytes32 witness, bytes calldata /* orderData */ ) external { - uint256 nonce = ++nonces[_toNonceId(msg.sender, recipient)]; + uint256 nonce = _getAndUpdateNonce(msg.sender, recipient); uint32 expiration = uint32(expires); (bytes32 claimHash, Lock[] memory commitments) = @@ -294,7 +293,7 @@ contract OnChainAllocator is IOnChainAllocator { revert InvalidExpiration(expires, block.timestamp); } - nonce = ++nonces[_toNonceId(address(0), sponsor)]; // address(0) as caller allows anyone to relay + nonce = _getAndUpdateNonce(address(0), sponsor); // address(0) as caller allows anyone to relay bytes32 commitmentsHash = AL.getCommitmentsHash(commitments); claimHash = AL.getClaimHash(arbiter, sponsor, nonce, expires, commitmentsHash, witness, typehash); @@ -468,6 +467,18 @@ contract OnChainAllocator is IOnChainAllocator { } } + function _getAndUpdateNonce(address calling, address sponsor) internal returns (uint256 nonce) { + assembly ("memory-safe") { + calling := xor(calling, mul(sponsor, iszero(calling))) + mstore(0x00, calling) + mstore(0x20, nonces.slot) + let nonceSlot := keccak256(0x00, 0x40) + let nonce96 := sload(nonceSlot) + nonce := or(shl(96, calling), add(nonce96, 1)) + sstore(nonceSlot, add(nonce96, 1)) + } + } + function _getTokenHash(bytes12 lockTag, address token, address sponsor) internal pure returns (bytes32 tokenHash) { assembly ("memory-safe") { mstore(0x00, lockTag) @@ -480,8 +491,4 @@ contract OnChainAllocator is IOnChainAllocator { function _getTokenHash(uint256 id, address sponsor) internal pure returns (bytes32 tokenHash) { tokenHash = keccak256(abi.encode(id, sponsor)); } - - function _toNonceId(address caller, address sponsor) internal pure returns (bytes32 nonce) { - return keccak256(abi.encode(caller, sponsor)); - } } From d51cc9deba9ce04aa5eb0ae65c1e48b560348513 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Tue, 19 Aug 2025 18:05:00 +0200 Subject: [PATCH 56/63] Reworked ERC7683 contracts --- .gitmodules | 4 + foundry.toml | 1 + src/allocators/ERC7683Allocator.sol | 452 ++++----------------- src/allocators/HybridERC7683.sol | 423 +++++-------------- src/allocators/lib/ERC7683AllocatorLib.sol | 295 ++++++++++++++ src/allocators/types/TribunalStructs.sol | 56 ++- src/interfaces/IERC7683Allocator.sol | 25 +- 7 files changed, 539 insertions(+), 717 deletions(-) create mode 100644 src/allocators/lib/ERC7683AllocatorLib.sol diff --git a/.gitmodules b/.gitmodules index 85474cd..0351d97 100644 --- a/.gitmodules +++ b/.gitmodules @@ -17,3 +17,7 @@ path = lib/the-compact url = https://github.com/Uniswap/the-compact branch = v1 +[submodule "lib/tribunal"] + path = lib/tribunal + url = https://github.com/Uniswap/tribunal + branch = new-mandate diff --git a/foundry.toml b/foundry.toml index f6597d1..720c746 100644 --- a/foundry.toml +++ b/foundry.toml @@ -18,6 +18,7 @@ remappings = [ "@openzeppelin/contracts=lib/openzeppelin-contracts/contracts", "@openzeppelin/contracts-upgradeable=lib/openzeppelin-contracts-upgradeable/contracts", "@uniswap/the-compact=lib/the-compact/src", + "@uniswap/tribunal=lib/tribunal/src", "@solady=lib/solady/src", ] diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index c3ff229..eaf1758 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -5,8 +5,21 @@ pragma solidity ^0.8.27; import {IOriginSettler} from '../interfaces/ERC7683/IOriginSettler.sol'; import {IERC7683Allocator} from '../interfaces/IERC7683Allocator.sol'; import {OnChainAllocator} from './OnChainAllocator.sol'; -import {BatchClaim, Mandate} from './types/TribunalStructs.sol'; +import {AllocatorLib as AL} from './lib/AllocatorLib.sol'; +import {ERC7683AllocatorLib as ERC7683AL} from './lib/ERC7683AllocatorLib.sol'; + +import {Tribunal} from '@uniswap/tribunal/Tribunal.sol'; +import {Fill, Mandate, RecipientCallback} from '@uniswap/tribunal/types/TribunalStructs.sol'; + import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; +import { + COMPACT_TYPEHASH_WITH_MANDATE, + COMPACT_WITH_MANDATE_TYPESTRING, + MANDATE_BATCH_COMPACT_TYPEHASH, + MANDATE_FILL_TYPEHASH, + MANDATE_RECIPIENT_CALLBACK_TYPEHASH, + MANDATE_TYPEHASH +} from '@uniswap/tribunal/types/TribunalTypeHashes.sol'; import {LibBytes} from '@solady/utils/LibBytes.sol'; import {BatchCompact, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; @@ -16,424 +29,129 @@ import {BatchCompact, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; /// @dev The contract ensures tokens can not be double spent by a user in a fully decentralized manner. /// @dev Users can open orders for themselves or for others by providing a signature or the tokens directly. contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { - /// @notice The typehash of the OrderDataOnChain struct - // keccak256("OrderDataOnChain(Order order,uint256 expires) - // Lock(bytes12 lockTag,address token,uint256 amount) - // Order(address arbiter,Lock[] commitments,uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt,bytes32 qualification)") - bytes32 public constant ORDERDATA_ONCHAIN_TYPEHASH = - 0x037a34e1ded3bcc84f59dfc185efc3553c509ebab317153a8dddefce2eaee6f0; - - /// @notice The typehash of the OrderDataGasless struct - // keccak256("OrderDataGasless(Order order,bool deposit) - // Lock(bytes12 lockTag,address token,uint256 amount) - // Order(address arbiter,Lock[] commitments,uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt,bytes32 qualification)") - bytes32 public constant ORDERDATA_GASLESS_TYPEHASH = - 0x79e4af6feaa84a46fd69ed25e4595e9f6e8690ba3a6c564bfa235542f9faf55c; - - /// @notice keccak256("BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments,Mandate mandate) - // Lock(bytes12 lockTag,address token,uint256 amount) - // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") - bytes32 public constant BATCH_COMPACT_WITNESS_TYPEHASH = - 0x5ede122c736b60a8b718f83dcfb5d6e4aa27c9714d0c7bc9ca86562b8f878463; - - /// @notice keccak256("Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") - bytes32 private constant MANDATE_TYPEHASH = 0x74d9c10530859952346f3e046aa2981a24bb7524b8394eb45a9deddced9d6501; - - mapping(bytes32 claimHash => bytes32 qualification) public qualifications; - - constructor(address compactContract_) OnChainAllocator(compactContract_) {} + constructor(address compact) OnChainAllocator(compact) {} /// @inheritdoc IOriginSettler - function openFor(GaslessCrossChainOrder calldata order_, bytes calldata sponsorSignature_, bytes calldata) - external - { - // Check if orderDataType is the one expected by the allocator - if (order_.orderDataType != ORDERDATA_GASLESS_TYPEHASH) { - revert InvalidOrderDataType(order_.orderDataType, ORDERDATA_GASLESS_TYPEHASH); - } - if (order_.originSettler != address(this)) { - revert InvalidOriginSettler(order_.originSettler, address(this)); - } - - // Decode the orderData - (Order calldata orderData, uint32 deposit) = _decodeOrderData(order_.orderData); - deposit = _sanitizeBool(deposit); + function openFor(GaslessCrossChainOrder calldata order, bytes calldata sponsorSignature, bytes calldata) external { + ( + IERC7683Allocator.Order calldata orderData, + uint32 deposit, + bytes32 mandateHash, + IOriginSettler.ResolvedCrossChainOrder memory resolvedOrder + ) = ERC7683AL.openForPreparation(order, sponsorSignature); - uint160 caller = uint160(deposit * uint160(msg.sender)); // for a deposit, the nonce will be scoped to the caller + user - bytes32 nonceIdentifier = _toNonceId(address(caller), order_.user); + uint160 caller = uint160(deposit * uint160(msg.sender)); // for a deposit, the nonce will be scoped to the caller // Early revert if the expected nonce is not the next nonce - if (order_.nonce != nonces[nonceIdentifier] + 1) { - revert InvalidNonce(order_.nonce, nonces[nonceIdentifier] + 1); + if (order.nonce != _getNonce(address(caller), order.user)) { + revert InvalidNonce(order.nonce, _getNonce(address(caller), order.user)); } - bytes32 qualification = bytes32(uint256(orderData.qualification) * deposit); // delete qualification if not a deposit - - ResolvedCrossChainOrder memory resolvedOrder = _resolveOrder( - order_.user, - order_.nonce, - order_.openDeadline, - order_.fillDeadline, - orderData, - sponsorSignature_, - qualification - ); - - bytes32 mandateHash = _mandateHash(orderData, order_.fillDeadline); - + uint256 nonce; if (deposit == 0) { - _open(order_.user, order_.openDeadline, orderData, sponsorSignature_, mandateHash, resolvedOrder); + // Register the allocation on chain + (, nonce) = allocateFor( + order.user, + orderData.commitments, + orderData.arbiter, + order.openDeadline, + COMPACT_TYPEHASH_WITH_MANDATE, + mandateHash, + sponsorSignature + ); } else { - _openAndRegister(order_.user, order_.openDeadline, orderData, mandateHash, resolvedOrder); + // Register the allocation on chain + uint256[] memory registeredAmounts; + (, registeredAmounts, nonce) = allocateAndRegister( + order.user, + orderData.commitments, + orderData.arbiter, + order.openDeadline, + COMPACT_TYPEHASH_WITH_MANDATE, + mandateHash + ); + + for (uint256 i = 0; i < orderData.commitments.length; i++) { + resolvedOrder.minReceived[i].amount = registeredAmounts[i]; + } } + // Emit an open event + emit Open(bytes32(nonce), resolvedOrder); } /// @inheritdoc IOriginSettler function open(OnchainCrossChainOrder calldata order) external { - // Check if orderDataType is the one expected by the allocator - if (order.orderDataType != ORDERDATA_ONCHAIN_TYPEHASH) { - revert InvalidOrderDataType(order.orderDataType, ORDERDATA_ONCHAIN_TYPEHASH); - } - - // Decode the orderData - (Order calldata orderData, uint32 expires) = _decodeOrderData(order.orderData); - expires = _sanitizeUint32(expires); + (IERC7683Allocator.Order calldata orderData, uint32 expires, bytes32 mandateHash, bytes32[] memory fillHashes) = + ERC7683AL.openPreparation(order); - bytes32 mandateHash = _mandateHash(orderData, order.fillDeadline); + // Register the allocation on chain + (, uint256 nonce) = + allocate(orderData.commitments, orderData.arbiter, expires, COMPACT_TYPEHASH_WITH_MANDATE, mandateHash); - ResolvedCrossChainOrder memory resolvedOrder = _resolveOrder( - msg.sender, - nonces[_toNonceId(address(0), msg.sender)] + 1, - expires, - order.fillDeadline, - orderData, - LibBytes.emptyCalldata(), - orderData.qualification - ); + ResolvedCrossChainOrder memory resolvedOrder = + ERC7683AL.resolveOrder(msg.sender, nonce, expires, fillHashes, orderData, LibBytes.emptyCalldata()); - _open(msg.sender, expires, orderData, LibBytes.emptyCalldata(), mandateHash, resolvedOrder); + // Emit an open event + emit Open(bytes32(nonce), resolvedOrder); } /// @inheritdoc IOriginSettler - function resolveFor(GaslessCrossChainOrder calldata order_, bytes calldata) + function resolveFor(GaslessCrossChainOrder calldata order, bytes calldata) external view returns (ResolvedCrossChainOrder memory) { - // Check if orderDataType is the one expected by the allocator - if (order_.orderDataType != ORDERDATA_GASLESS_TYPEHASH) { - revert InvalidOrderDataType(order_.orderDataType, ORDERDATA_GASLESS_TYPEHASH); - } - if (order_.originSettler != address(this)) { - revert InvalidOriginSettler(order_.originSettler, address(this)); - } - - // Decode the orderData - (Order calldata orderData, uint32 deposit) = _decodeOrderData(order_.orderData); - deposit = _sanitizeBool(deposit); + (, uint32 deposit,, IOriginSettler.ResolvedCrossChainOrder memory resolvedOrder) = + ERC7683AL.openForPreparation(order, LibBytes.emptyCalldata()); uint160 caller = uint160(deposit * uint160(msg.sender)); // for a deposit, the nonce will be scoped to the caller + user - bytes32 nonceIdentifier = _toNonceId(address(caller), order_.user); // Early revert if the expected nonce is not the next nonce - if (order_.nonce != nonces[nonceIdentifier] + 1) { - revert InvalidNonce(order_.nonce, nonces[nonceIdentifier] + 1); + if (order.nonce != _getNonce(address(caller), order.user)) { + revert InvalidNonce(order.nonce, _getNonce(address(caller), order.user)); } - bytes32 qualification = bytes32(uint256(orderData.qualification) * deposit); - - return _resolveOrder( - order_.user, - order_.nonce, - order_.openDeadline, - order_.fillDeadline, - orderData, - LibBytes.emptyCalldata(), - qualification - ); + return resolvedOrder; } /// @inheritdoc IOriginSettler function resolve(OnchainCrossChainOrder calldata order) external view returns (ResolvedCrossChainOrder memory) { - // Check if orderDataType is the one expected by the allocator - if (order.orderDataType != ORDERDATA_ONCHAIN_TYPEHASH) { - revert InvalidOrderDataType(order.orderDataType, ORDERDATA_ONCHAIN_TYPEHASH); - } + (IERC7683Allocator.Order calldata orderData, uint32 expires,, bytes32[] memory fillHashes) = + ERC7683AL.openPreparation(order); - // Decode the orderData - (Order calldata orderData, uint32 expires) = _decodeOrderData(order.orderData); - expires = _sanitizeUint32(expires); - - return _resolveOrder( - msg.sender, - nonces[_toNonceId(address(0), msg.sender)] + 1, - expires, - order.fillDeadline, - orderData, - LibBytes.emptyCalldata(), - orderData.qualification + return ERC7683AL.resolveOrder( + msg.sender, _getNonce(address(0), msg.sender), expires, fillHashes, orderData, LibBytes.emptyCalldata() ); } - /// @inheritdoc IAllocator - function authorizeClaim( - bytes32 claimHash, - address, /*arbiter*/ // The account tasked with verifying and submitting the claim. - address sponsor, // The account sponsoring the claim. - uint256, /*nonce*/ // A parameter to enforce replay protection, scoped to allocator. - uint256 expires, // The time at which the claim expires. - uint256[2][] calldata idsAndAmounts, // The allocated token IDs and amounts. - bytes calldata allocatorData // Arbitrary data provided by the arbiter. - ) public override(OnChainAllocator, IAllocator) onlyCompact returns (bytes4) { - super.authorizeClaim(claimHash, address(0), sponsor, 0, expires, idsAndAmounts, allocatorData); - - if (qualifications[claimHash] != bytes32(allocatorData)) { - revert InvalidAllocatorData(bytes32(allocatorData), qualifications[claimHash]); - } - - return this.authorizeClaim.selector; - } - - /// @inheritdoc IAllocator - function isClaimAuthorized( - bytes32 claimHash, - address, /*arbiter*/ // The account tasked with verifying and submitting the claim. - address sponsor, // The account to source the tokens from. - uint256, /*nonce*/ // A parameter to enforce replay protection, scoped to allocator. - uint256 expires, // The time at which the claim expires. - uint256[2][] calldata idsAndAmounts, // The allocated token IDs and amounts. - bytes calldata allocatorData // Arbitrary data provided by the arbiter. - ) public view override(OnChainAllocator, IAllocator) returns (bool) { - if ( - !super.isClaimAuthorized(claimHash, address(0), sponsor, 0, expires, idsAndAmounts, LibBytes.emptyCalldata()) - ) { - return false; - } - - return qualifications[claimHash] == bytes32(allocatorData); - } - /// @inheritdoc IERC7683Allocator function getCompactWitnessTypeString() external pure returns (string memory) { - return - 'BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments,Mandate mandate)Lock(bytes12 lockTag,address token,uint256 amount)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; + return COMPACT_WITH_MANDATE_TYPESTRING; } /// @inheritdoc IERC7683Allocator - function checkNonce(GaslessCrossChainOrder calldata order_, address caller) - external - view - returns (bool nonceValid) - { - (, uint32 deposit) = _decodeOrderData(order_.orderData); - deposit = _sanitizeBool(deposit); + function getNonce(GaslessCrossChainOrder calldata order, address caller) external view returns (uint256 nonce) { + (, uint32 deposit) = ERC7683AL.decodeOrderData(order.orderData); + deposit = ERC7683AL.sanitizeBool(deposit); caller = address(uint160(deposit * uint160(caller))); // for a deposit, the nonce will be scoped to the caller + user - bytes32 nonceIdentifier = _toNonceId(caller, order_.user); - // Early revert if the expected nonce is not the next nonce - return (order_.nonce == nonces[nonceIdentifier] + 1); + return _getNonce(address(caller), order.user); } /// @inheritdoc IERC7683Allocator - function createFillerData(address claimant_) external pure returns (bytes memory fillerData) { - return abi.encode(claimant_); - } - - function _open( - address sponsor, - uint32 expires, - Order calldata orderData, - bytes calldata sponsorSignature, - bytes32 mandateHash, - ResolvedCrossChainOrder memory resolvedOrder - ) internal { - // Register the allocation on chain - (bytes32 claimHash, uint256 nonce) = allocateFor( - sponsor, - orderData.commitments, - orderData.arbiter, - expires, - BATCH_COMPACT_WITNESS_TYPEHASH, - mandateHash, - sponsorSignature - ); - - if (sponsor == msg.sender && orderData.qualification != bytes32(0)) { - qualifications[claimHash] = orderData.qualification; - } - - // Emit an open event - emit Open(bytes32(nonce), resolvedOrder); - } - - function _openAndRegister( - address sponsor, - uint32 expires, - Order calldata orderData, - bytes32 mandateHash_, - ResolvedCrossChainOrder memory resolvedOrder - ) internal { - // Register the allocation on chain - (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = allocateAndRegister( - sponsor, orderData.commitments, orderData.arbiter, expires, BATCH_COMPACT_WITNESS_TYPEHASH, mandateHash_ - ); - - if (orderData.qualification != bytes32(0)) { - qualifications[claimHash] = orderData.qualification; - } - - for (uint256 i = 0; i < orderData.commitments.length; i++) { - resolvedOrder.minReceived[i].amount = registeredAmounts[i]; - } - - // Emit an open event - emit Open(bytes32(nonce), resolvedOrder); - } - - function _decodeOrderData(bytes calldata orderData) - internal - pure - returns (Order calldata order, uint32 additionalInput) - { - // orderData includes the OrderData(OnChain/Gasless) struct, and the nested Order struct. - // 0x00: OrderDataOnChain.offset - // 0x20: OrderDataOnChain.order.offset - // 0x40: OrderDataOnChain.expires - - // 0x00: OrderDataGasless.offset - // 0x20: OrderDataGasless.order.offset - - assembly ("memory-safe") { - let l := sub(orderData.length, 0x20) - let s := calldataload(add(orderData.offset, 0x20)) // Relative offset of `orderBytes` from `orderData.offset` and the `OrderData...` struct. - order := add(orderData.offset, add(s, 0x20)) // Add 0x20 since the OrderStruct is within the `OrderData...` struct - if shr(64, or(s, or(l, orderData.offset))) { revert(l, 0x00) } - - additionalInput := calldataload(add(orderData.offset, 0x40)) - } - } - - function _resolveOrder( - address sponsor, - uint256 nonce, - uint32 expires, - uint32 fillDeadline, - Order calldata orderData, - bytes calldata sponsorSignature, - bytes32 qualification - ) internal view returns (ResolvedCrossChainOrder memory) { - ResolvedCrossChainOrder memory resolvedOrder = ResolvedCrossChainOrder({ - user: sponsor, - originChainId: block.chainid, - openDeadline: uint32(expires), - fillDeadline: fillDeadline, - orderId: bytes32(nonce), - maxSpent: new Output[](0), - minReceived: new Output[](0), - fillInstructions: new FillInstruction[](0) - }); - - BatchCompact memory compact = BatchCompact({ - arbiter: orderData.arbiter, - sponsor: sponsor, - nonce: nonce, - expires: expires, - commitments: orderData.commitments - }); - - BatchClaim memory claim = BatchClaim({ - chainId: block.chainid, - compact: compact, - sponsorSignature: sponsorSignature, - allocatorSignature: '' // No signature required from this allocator, it will verify the claim on chain via ERC1271. - }); - - Mandate memory mandate = Mandate({ - recipient: orderData.recipient, - expires: fillDeadline, - token: orderData.settlementToken, - minimumAmount: orderData.minimumAmount, - baselinePriorityFee: orderData.baselinePriorityFee, - scalingFactor: orderData.scalingFactor, - decayCurve: orderData.decayCurve, - salt: orderData.salt - }); - - FillInstruction[] memory fillInstructions = new FillInstruction[](1); - fillInstructions[0] = FillInstruction({ - destinationChainId: orderData.chainId, - destinationSettler: _addressToBytes32(orderData.tribunal), - originData: abi.encode(claim, mandate, uint200(bytes25(qualification)), uint56(uint256(qualification))) - }); - resolvedOrder.fillInstructions = fillInstructions; - - Output memory spent = Output({ - token: _addressToBytes32(mandate.token), - amount: type(uint256).max, - recipient: _addressToBytes32(mandate.recipient), - chainId: orderData.chainId - }); - Output[] memory maxSpent = new Output[](1); - maxSpent[0] = spent; - resolvedOrder.maxSpent = maxSpent; - - resolvedOrder.minReceived = _createMinimumReceived(orderData.commitments); - - return resolvedOrder; - } - - function _createMinimumReceived(Lock[] calldata commitments) internal view returns (Output[] memory) { - Output[] memory minReceived = new Output[](commitments.length); - - for (uint256 i = 0; i < commitments.length; i++) { - Output memory received = Output({ - token: _addressToBytes32(commitments[i].token), - amount: commitments[i].amount, - recipient: bytes32(0), - chainId: block.chainid - }); - minReceived[i] = received; - } - return minReceived; - } - - function _mandateHash(Order calldata orderData, uint32 fillDeadline) internal pure returns (bytes32 mandateHash_) { - bytes32 decayCurveHash = keccak256(abi.encodePacked(orderData.decayCurve)); - mandateHash_ = keccak256( - abi.encode( - MANDATE_TYPEHASH, - orderData.chainId, - orderData.tribunal, - orderData.recipient, - fillDeadline, - orderData.settlementToken, - orderData.minimumAmount, - orderData.baselinePriorityFee, - orderData.scalingFactor, - decayCurveHash, - orderData.salt - ) - ); - } - - function _addressToBytes32(address address_) internal pure returns (bytes32 output_) { - assembly ("memory-safe") { - output_ := shr(96, shl(96, address_)) - } - } - - function _sanitizeUint32(uint32 value) internal pure returns (uint32) { - assembly ("memory-safe") { - value := shr(224, shl(224, value)) - } - return value; + function createFillerData(address claimant) external pure returns (bytes memory fillerData) { + return abi.encode(claimant); } - function _sanitizeBool(uint32 value) internal pure returns (uint32) { + function _getNonce(address calling, address sponsor) internal view returns (uint256 nonce) { assembly ("memory-safe") { - value := iszero(iszero(value)) + calling := xor(calling, mul(sponsor, iszero(calling))) + mstore(0x00, calling) + mstore(0x20, nonces.slot) + let nonceSlot := keccak256(0x00, 0x40) + let nonce96 := sload(nonceSlot) + nonce := or(shl(96, calling), add(nonce96, 1)) } - return value; } } diff --git a/src/allocators/HybridERC7683.sol b/src/allocators/HybridERC7683.sol index 7531c67..a4bcbfc 100644 --- a/src/allocators/HybridERC7683.sol +++ b/src/allocators/HybridERC7683.sol @@ -2,193 +2,103 @@ pragma solidity ^0.8.27; -import {BatchClaim, Mandate} from './types/TribunalStructs.sol'; +import {ERC7683AllocatorLib as ERC7683AL} from './lib/ERC7683AllocatorLib.sol'; import {LibBytes} from '@solady/utils/LibBytes.sol'; import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; import {BatchCompact, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; - +import {Tribunal} from '@uniswap/tribunal/Tribunal.sol'; +import {Fill, Mandate} from '@uniswap/tribunal/types/TribunalStructs.sol'; + +import { + COMPACT_TYPEHASH_WITH_MANDATE, + COMPACT_WITH_MANDATE_TYPESTRING, + MANDATE_BATCH_COMPACT_TYPEHASH, + MANDATE_FILL_TYPEHASH, + MANDATE_RECIPIENT_CALLBACK_TYPEHASH, + MANDATE_TYPEHASH +} from '@uniswap/tribunal/types/TribunalTypeHashes.sol'; import {HybridAllocator} from 'src/allocators/HybridAllocator.sol'; -import {BATCH_COMPACT_WITNESS_TYPEHASH, MANDATE_TYPEHASH} from 'src/allocators/lib/TypeHashes.sol'; -import {IHybridERC7683} from 'src/interfaces/IHybridERC7683.sol'; +import {IERC7683Allocator} from 'src/interfaces/IERC7683Allocator.sol'; import {AllocatorLib as AL} from 'src/allocators/lib/AllocatorLib.sol'; import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; -contract HybridERC7683 is HybridAllocator, IHybridERC7683 { - // mask for an active claim - uint256 private constant _ACTIVE_CLAIM_MASK = 0x0000000000000000000000000000000000000000000000000000000000000001; - - /// @notice The typehash of the OrderDataOnChain struct - // keccak256("OrderDataOnChain(Order order,uint256 expires) - // Order(address arbiter,uint256[2][] idsAndAmounts,uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt,bytes32 qualification)") - bytes32 public constant ORDERDATA_ONCHAIN_TYPEHASH = - 0xd13cc04099540f243b0042f68c0edbce9aefe428c22e0354a24061c5d98c7276; - - /// @notice The typehash of the OrderDataGasless struct - // keccak256("OrderDataGasless(Order order) - // Order(address arbiter,uint256[2][] idsAndAmounts,uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt,bytes32 qualification)") - bytes32 public constant ORDERDATA_GASLESS_TYPEHASH = - 0xfba49b9453e7d260d702826a659947a671a3e6a970688a795c82065685236b52; - - /// @notice keccak256("QualifiedClaim(bytes32 claimHash,uint256 targetBlock,uint256 maximumBlocksAfterTarget)") - bytes32 public constant QUALIFICATION_TYPEHASH = 0x59866b84bd1f6c909cf2a31efd20c59e6c902e50f2c196994e5aa85cdc7d7ce0; +contract HybridERC7683 is HybridAllocator, IERC7683Allocator { + error OnlyDepositsAllowed(); - uint256 private constant _INVALID_QUALIFICATION_ERROR_SIGNATURE = 0x7ac3c7d4; - - constructor(address compact_, address signer_) HybridAllocator(compact_, signer_) {} + constructor(address compact, address signer) HybridAllocator(compact, signer) {} /// @inheritdoc IOriginSettler - function openFor( - GaslessCrossChainOrder calldata order, - bytes calldata, /*sponsorSignature_*/ - bytes calldata /*originFillerData*/ - ) external { - // Check if orderDataType is the one expected by the allocator - if (order.orderDataType != ORDERDATA_GASLESS_TYPEHASH) { - revert InvalidOrderDataType(order.orderDataType, ORDERDATA_GASLESS_TYPEHASH); + function openFor(GaslessCrossChainOrder calldata order, bytes calldata sponsorSignature, bytes calldata) external { + ( + IERC7683Allocator.Order calldata orderData, + uint32 deposit, + bytes32 mandateHash, + IOriginSettler.ResolvedCrossChainOrder memory resolvedOrder + ) = ERC7683AL.openForPreparation(order, sponsorSignature); + + uint160 caller = uint160(deposit * uint160(msg.sender)); // for a deposit, the nonce will be scoped to the caller + + // Early revert if the expected nonce is not the next nonce + if (order.nonce != nonces + 1) { + revert InvalidNonce(order.nonce, nonces + 1); } - (Order calldata orderData,) = _decodeOrderData(order.orderData, false); - - // create witness hash - bytes32 witnessHash = keccak256( - abi.encode( - MANDATE_TYPEHASH, - orderData.chainId, - orderData.tribunal, - orderData.recipient, - order.fillDeadline, - orderData.settlementToken, - orderData.minimumAmount, - orderData.baselinePriorityFee, - orderData.scalingFactor, - keccak256(abi.encodePacked(orderData.decayCurve)), - orderData.salt - ) - ); - - // register claim - (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce_) = allocateAndRegister( - order.user, - orderData.idsAndAmounts, - orderData.arbiter, - order.openDeadline, - BATCH_COMPACT_WITNESS_TYPEHASH, - witnessHash - ); - - _storeQualification(claimHash, orderData.qualification); + uint256 nonce; + if (deposit == 0) { + // Hybrid Allocator requires a deposit + revert OnlyDepositsAllowed(); + } else { + // Create idsAndAmounts + uint256[2][] memory idsAndAmounts = new uint256[2][](orderData.commitments.length); + for (uint256 i = 0; i < orderData.commitments.length; i++) { + idsAndAmounts[i][0] = AL.toId(orderData.commitments[i].lockTag, orderData.commitments[i].token); + idsAndAmounts[i][1] = orderData.commitments[i].amount; + } - Lock[] memory locks = new Lock[](registeredAmounts.length); - for (uint256 i = 0; i < registeredAmounts.length; i++) { - locks[i] = AL.toLock(orderData.idsAndAmounts[i][0], registeredAmounts[i]); + // Register the allocation on chain + uint256[] memory registeredAmounts; + (, registeredAmounts, nonce) = allocateAndRegister( + order.user, + idsAndAmounts, + orderData.arbiter, + order.openDeadline, + COMPACT_TYPEHASH_WITH_MANDATE, + mandateHash + ); + + for (uint256 i = 0; i < orderData.commitments.length; i++) { + resolvedOrder.minReceived[i].amount = registeredAmounts[i]; + } } - - BatchCompact memory batchCompact = BatchCompact({ - arbiter: orderData.arbiter, - sponsor: order.user, - nonce: nonce_, - expires: order.openDeadline, - commitments: locks - }); - - // emit open event - emit Open( - bytes32(batchCompact.nonce), _convertToResolvedCrossChainOrder(orderData, order.fillDeadline, batchCompact) - ); + // Emit an open event + emit Open(bytes32(nonce), resolvedOrder); } /// @inheritdoc IOriginSettler function open(OnchainCrossChainOrder calldata order) external { - // Check if orderDataType is the one expected by the allocator - if (order.orderDataType != ORDERDATA_ONCHAIN_TYPEHASH) { - revert InvalidOrderDataType(order.orderDataType, ORDERDATA_ONCHAIN_TYPEHASH); - } - - (Order calldata orderData, uint256 expires) = _decodeOrderData(order.orderData, true); - - // create witness hash - bytes32 witnessHash = keccak256( - abi.encode( - MANDATE_TYPEHASH, - orderData.chainId, - orderData.tribunal, - orderData.recipient, - order.fillDeadline, - orderData.settlementToken, - orderData.minimumAmount, - orderData.baselinePriorityFee, - orderData.scalingFactor, - keccak256(abi.encodePacked(orderData.decayCurve)), - orderData.salt - ) - ); - - // register claim - (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce_) = allocateAndRegister( - msg.sender, orderData.idsAndAmounts, orderData.arbiter, expires, BATCH_COMPACT_WITNESS_TYPEHASH, witnessHash - ); - - _storeQualification(claimHash, orderData.qualification); - - Lock[] memory locks = new Lock[](registeredAmounts.length); - for (uint256 i = 0; i < registeredAmounts.length; i++) { - locks[i] = AL.toLock(orderData.idsAndAmounts[i][0], registeredAmounts[i]); + (IERC7683Allocator.Order calldata orderData, uint32 expires, bytes32 mandateHash, bytes32[] memory fillHashes) = + ERC7683AL.openPreparation(order); + + // Create idsAndAmounts + uint256[2][] memory idsAndAmounts = new uint256[2][](orderData.commitments.length); + for (uint256 i = 0; i < orderData.commitments.length; i++) { + idsAndAmounts[i][0] = AL.toId(orderData.commitments[i].lockTag, orderData.commitments[i].token); + idsAndAmounts[i][1] = orderData.commitments[i].amount; } - BatchCompact memory batchCompact = BatchCompact({ - arbiter: orderData.arbiter, - sponsor: msg.sender, - nonce: nonce_, - expires: expires, - commitments: locks - }); - - // emit open event - emit Open( - bytes32(batchCompact.nonce), _convertToResolvedCrossChainOrder(orderData, order.fillDeadline, batchCompact) + // deposit the the tokens into the compact and register the claim + (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = allocateAndRegister( + msg.sender, idsAndAmounts, orderData.arbiter, expires, COMPACT_TYPEHASH_WITH_MANDATE, mandateHash ); - } - - /// @inheritdoc IAllocator - function authorizeClaim( - bytes32 claimHash, - address, /*arbiter*/ - address, /*sponsor*/ - uint256, /*nonce*/ - uint256, /*expires*/ - uint256[2][] calldata, /*idsAndAmounts*/ - bytes calldata allocatorData_ - ) external override(HybridAllocator, IAllocator) returns (bytes4) { - if (msg.sender != address(_COMPACT)) { - revert InvalidCaller(msg.sender, address(_COMPACT)); - } - // The compact will check the validity of the nonce and expiration - - (bool validClaim, uint128 targetBlock, uint120 maximumBlocksAfterTarget) = - _checkClaim(claimHash, allocatorData_); - // Check if the claim was allocated on chain - if (validClaim) { - delete claims[claimHash]; - - // Authorize the claim - return IAllocator.authorizeClaim.selector; - } - - if (allocatorData_.length != 0xe0 && allocatorData_.length != 0xc0) revert InvalidSignature(); - - // Create the digest for the qualified claim hash - bytes32 qualifiedClaimHash = - keccak256(abi.encode(QUALIFICATION_TYPEHASH, claimHash, targetBlock, maximumBlocksAfterTarget)); - bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), _COMPACT_DOMAIN_SEPARATOR, qualifiedClaimHash)); - // Check the allocator data for a valid signature by an authorized signer - bytes calldata allocatorSignature = LibBytes.bytesInCalldata(allocatorData_, 0x40); - if (!_checkSignature(digest, allocatorSignature)) { - revert InvalidSignature(); + ResolvedCrossChainOrder memory resolvedOrder = + ERC7683AL.resolveOrder(msg.sender, nonce, expires, fillHashes, orderData, LibBytes.emptyCalldata()); + for (uint256 i = 0; i < orderData.commitments.length; i++) { + resolvedOrder.minReceived[i].amount = registeredAmounts[i]; } - // Authorize the claim - return IAllocator.authorizeClaim.selector; + // Emit an open event + emit Open(bytes32(nonce), resolvedOrder); } /// @inheritdoc IOriginSettler @@ -197,184 +107,49 @@ contract HybridERC7683 is HybridAllocator, IHybridERC7683 { view returns (ResolvedCrossChainOrder memory) { - // Check if orderDataType is the one expected by the allocator - if (order.orderDataType != ORDERDATA_GASLESS_TYPEHASH) { - revert InvalidOrderDataType(order.orderDataType, ORDERDATA_GASLESS_TYPEHASH); - } - - (Order calldata orderData,) = _decodeOrderData(order.orderData, false); + (, uint32 deposit,, IOriginSettler.ResolvedCrossChainOrder memory resolvedOrder) = + ERC7683AL.openForPreparation(order, LibBytes.emptyCalldata()); - Lock[] memory locks = new Lock[](orderData.idsAndAmounts.length); - for (uint256 i = 0; i < orderData.idsAndAmounts.length; i++) { - locks[i] = AL.toLock(orderData.idsAndAmounts[i][0], orderData.idsAndAmounts[i][1]); + // Revert if the nonce is not the next nonce + if (order.nonce != nonces + 1) { + revert InvalidNonce(order.nonce, nonces + 1); } - BatchCompact memory batchCompact = BatchCompact({ - arbiter: orderData.arbiter, - sponsor: order.user, - nonce: nonces + 1, - expires: order.openDeadline, - commitments: locks - }); + if (deposit == 0) { + // Hybrid Allocator requires a deposit + revert OnlyDepositsAllowed(); + } - return _convertToResolvedCrossChainOrder(orderData, order.fillDeadline, batchCompact); + return resolvedOrder; } /// @inheritdoc IOriginSettler function resolve(OnchainCrossChainOrder calldata order) external view returns (ResolvedCrossChainOrder memory) { - // Check if orderDataType is the one expected by the allocator - if (order.orderDataType != ORDERDATA_ONCHAIN_TYPEHASH) { - revert InvalidOrderDataType(order.orderDataType, ORDERDATA_ONCHAIN_TYPEHASH); - } - - (Order calldata orderData, uint256 expires) = _decodeOrderData(order.orderData, true); - uint256 idsLength = orderData.idsAndAmounts.length; - Lock[] memory locks = new Lock[](idsLength); - for (uint256 i = 0; i < idsLength; i++) { - uint256 id = orderData.idsAndAmounts[i][0]; - locks[i] = AL.toLock(id, orderData.idsAndAmounts[i][1]); - } - BatchCompact memory batchCompact = BatchCompact({ - arbiter: orderData.arbiter, - sponsor: msg.sender, - nonce: nonces + 1, // nonce is incremented by 1 when the claim is registered - expires: expires, - commitments: locks - }); - return _convertToResolvedCrossChainOrder(orderData, order.fillDeadline, batchCompact); - } + (IERC7683Allocator.Order calldata orderData, uint32 expires,, bytes32[] memory fillHashes) = + ERC7683AL.openPreparation(order); - function _storeQualification(bytes32 claimHash, bytes32 qualification) private { - // store the allocator data with the claims mapping. - assembly ("memory-safe") { - if and(qualification, _ACTIVE_CLAIM_MASK) { - mstore(0, _INVALID_QUALIFICATION_ERROR_SIGNATURE) - mstore(0x20, qualification) - revert(0x1c, 0x24) - } - - mstore(0x00, claimHash) - mstore(0x20, claims.slot) - let claimSlot := keccak256(0x00, 0x40) - let indicator := or(qualification, _ACTIVE_CLAIM_MASK) - sstore(claimSlot, indicator) - } + return ERC7683AL.resolveOrder(msg.sender, nonces + 1, expires, fillHashes, orderData, LibBytes.emptyCalldata()); } - function _checkClaim(bytes32 claimHash, bytes calldata allocatorData) - private - view - returns (bool valid, uint128 targetBlock, uint120 maximumBlocksAfterTarget) - { - assembly ("memory-safe") { - mstore(0x00, claimHash) - mstore(0x20, claims.slot) - let claimSlot := keccak256(0x00, 0x40) - let data := sload(claimSlot) - - valid := and(data, _ACTIVE_CLAIM_MASK) - let storedTargetBlock := shr(57, data) - let storedMaximumBlocksAfterTarget := shr(200, shl(199, data)) - - targetBlock := calldataload(allocatorData.offset) - maximumBlocksAfterTarget := calldataload(add(allocatorData.offset, 0x20)) - valid := - and( - valid, - and(eq(storedTargetBlock, targetBlock), eq(storedMaximumBlocksAfterTarget, maximumBlocksAfterTarget)) - ) - } + function getCompactWitnessTypeString() external pure returns (string memory) { + return COMPACT_WITH_MANDATE_TYPESTRING; } - function _convertToResolvedCrossChainOrder( - Order calldata orderData, - uint256 fillDeadline, - BatchCompact memory batchCompact - ) private view returns (ResolvedCrossChainOrder memory) { - Output[] memory maxSpent = new Output[](1); - maxSpent[0] = Output({ - token: bytes32(uint256(uint160(orderData.settlementToken))), - amount: type(uint256).max, - recipient: bytes32(uint256(uint160(orderData.recipient))), - chainId: orderData.chainId - }); + /// @inheritdoc IERC7683Allocator + function getNonce(GaslessCrossChainOrder calldata order, address) external view returns (uint256 nonce) { + (, uint32 deposit) = ERC7683AL.decodeOrderData(order.orderData); + deposit = ERC7683AL.sanitizeBool(deposit); - uint256 idsLength = orderData.idsAndAmounts.length; - Output[] memory minReceived = new Output[](idsLength); - for (uint256 i = 0; i < idsLength; i++) { - minReceived[i] = Output({ - token: bytes32(uint256(uint160(orderData.idsAndAmounts[i][0]))), - amount: orderData.minimumAmount, - recipient: _convertAddressToBytes32(orderData.recipient), - chainId: block.chainid - }); + if (deposit == 0) { + // Hybrid Allocator requires a deposit + revert OnlyDepositsAllowed(); } - Mandate memory mandate = Mandate({ - recipient: orderData.recipient, - expires: fillDeadline, - token: orderData.settlementToken, - minimumAmount: orderData.minimumAmount, - baselinePriorityFee: orderData.baselinePriorityFee, - scalingFactor: orderData.scalingFactor, - decayCurve: orderData.decayCurve, - salt: orderData.salt - }); - BatchClaim memory claim = BatchClaim({ - chainId: block.chainid, - compact: batchCompact, - sponsorSignature: '', // No signature required from the sponsor, the claim will be verified via the on chain registration. - allocatorSignature: '' // No signature required from this allocator, it will verify the claim on chain via ERC1271. - }); - - FillInstruction[] memory fillInstructions = new FillInstruction[](1); - fillInstructions[0] = FillInstruction({ - destinationChainId: orderData.chainId, - destinationSettler: _convertAddressToBytes32(orderData.tribunal), - originData: abi.encode( - claim, - mandate, - uint200(bytes25(orderData.qualification >> 1)), - uint56(uint256(orderData.qualification >> 1)) - ) - }); - - return ResolvedCrossChainOrder({ - user: batchCompact.sponsor, - originChainId: block.chainid, - openDeadline: uint32(batchCompact.expires), - fillDeadline: uint32(fillDeadline), - orderId: bytes32(batchCompact.nonce), - maxSpent: maxSpent, - minReceived: minReceived, - fillInstructions: fillInstructions - }); - } - - function _decodeOrderData(bytes calldata orderData, bool isOnChain) - private - pure - returns (Order calldata order, uint256 expires) - { - // orderData includes the OrderData(OnChain/Gasless) struct, and the nested Order struct. - // 0x00: OrderDataOnChain.offset - // 0x20: OrderDataOnChain.order.offset - // 0x40: OrderDataOnChain.expires - - // 0x00: OrderDataGasless.offset - // 0x20: OrderDataGasless.order.offset - - assembly ("memory-safe") { - let l := sub(orderData.length, 0x20) - let s := calldataload(add(orderData.offset, 0x20)) // Relative offset of `orderBytes` from `orderData.offset` and the `OrderData...` struct. - order := add(orderData.offset, add(s, 0x20)) // Add 0x20 since the OrderStruct is within the `OrderData...` struct - if shr(64, or(s, or(l, orderData.offset))) { revert(l, 0x00) } - - expires := mul(calldataload(add(orderData.offset, 0x40)), isOnChain) - } + return nonces + 1; } - function _convertAddressToBytes32(address address_) private pure returns (bytes32) { - return bytes32(uint256(uint160(address_))); + /// @inheritdoc IERC7683Allocator + function createFillerData(address claimant) external pure returns (bytes memory fillerData) { + return abi.encode(claimant); } } diff --git a/src/allocators/lib/ERC7683AllocatorLib.sol b/src/allocators/lib/ERC7683AllocatorLib.sol new file mode 100644 index 0000000..025ac90 --- /dev/null +++ b/src/allocators/lib/ERC7683AllocatorLib.sol @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import {AllocatorLib as AL} from './AllocatorLib.sol'; + +import {BatchCompact, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; +import {Tribunal} from '@uniswap/tribunal/Tribunal.sol'; +import {Fill, Mandate, RecipientCallback} from '@uniswap/tribunal/types/TribunalStructs.sol'; +import { + COMPACT_TYPEHASH_WITH_MANDATE, + COMPACT_WITH_MANDATE_TYPESTRING, + MANDATE_BATCH_COMPACT_TYPEHASH, + MANDATE_FILL_TYPEHASH, + MANDATE_RECIPIENT_CALLBACK_TYPEHASH, + MANDATE_TYPEHASH +} from '@uniswap/tribunal/types/TribunalTypeHashes.sol'; + +import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; + +import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; +import {IERC7683Allocator} from 'src/interfaces/IERC7683Allocator.sol'; + +library ERC7683AllocatorLib { + /// @notice The typehash of the OrderDataOnChain struct + // keccak256("OrderDataOnChain(Order order,uint32 expires) + // Mandate(address adjuster,Mandate_Fill[] fills) + // Mandate_BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Mandate_Lock[] commitments) + // Mandate_Fill(uint256 chainId,address tribunal,uint256 expires,address fillToken,uint256 minimumFillAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] priceCurve,address recipient,Mandate_RecipientCallback[] recipientCallback,bytes32 salt) + // Mandate_Lock(bytes12 lockTag,address token,uint256 amount) + // Mandate_RecipientCallback(uint256 chainId,Mandate_BatchCompact compact,bytes context) + // Order(address arbiter,Lock[] commitments,Mandate mandate)") + bytes32 public constant ORDERDATA_ONCHAIN_TYPEHASH = + 0x66007c95a1c1d0004397bbd6448c24b58a18d5d17e75321cdd119aa3b879d98e; + + /// @notice The typehash of the OrderDataGasless struct + // keccak256("OrderDataGasless(Order order,bool deposit) + // Mandate(address adjuster,Mandate_Fill[] fills) + // Mandate_BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Mandate_Lock[] commitments) + // Mandate_Fill(uint256 chainId,address tribunal,uint256 expires,address fillToken,uint256 minimumFillAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] priceCurve,address recipient,Mandate_RecipientCallback[] recipientCallback,bytes32 salt) + // Mandate_Lock(bytes12 lockTag,address token,uint256 amount) + // Mandate_RecipientCallback(uint256 chainId,Mandate_BatchCompact compact,bytes context) + // Order(address arbiter,Lock[] commitments,Mandate mandate)") + bytes32 public constant ORDERDATA_GASLESS_TYPEHASH = + 0xca948fccb29bd545dea3361927ce0b7d8b680a214439e24af475831131515b4c; + + error InvalidRecipientCallbackLength(); + error InvalidOrderDataType(bytes32 orderDataType, bytes32 expectedOrderDataType); + error InvalidOriginSettler(address originSettler, address expectedOriginSettler); + error InvalidOrderData(bytes orderData); + + function openForPreparation(IOriginSettler.GaslessCrossChainOrder calldata order, bytes calldata sponsorSignature) + internal + view + returns ( + IERC7683Allocator.Order calldata orderData, + uint32 deposit, + bytes32 mandateHash, + IOriginSettler.ResolvedCrossChainOrder memory resolvedOrder + ) + { + // Check if orderDataType is the one expected by the allocator + if (order.orderDataType != ORDERDATA_GASLESS_TYPEHASH) { + revert InvalidOrderDataType(order.orderDataType, ORDERDATA_GASLESS_TYPEHASH); + } + // Check if the originSettler is the allocator + if (order.originSettler != address(this)) { + revert InvalidOriginSettler(order.originSettler, address(this)); + } + + // Decode the orderData + (orderData, deposit) = decodeOrderData(order.orderData); + deposit = sanitizeBool(deposit); + + // Ensure a valid mandate is provided + if (orderData.mandate.fills.length == 0 || order.fillDeadline != orderData.mandate.fills[0].expires) { + revert InvalidOrderData(order.orderData); + } + bytes32[] memory fillHashes; + (mandateHash, fillHashes) = hashMandate(orderData.mandate); + resolvedOrder = + resolveOrder(order.user, order.nonce, order.openDeadline, fillHashes, orderData, sponsorSignature); + } + + function openPreparation(IOriginSettler.OnchainCrossChainOrder calldata order) + internal + pure + returns ( + IERC7683Allocator.Order calldata orderData, + uint32 expires, + bytes32 mandateHash, + bytes32[] memory fillHashes + ) + { + // Check if orderDataType is the one expected by the allocator + if (order.orderDataType != ORDERDATA_ONCHAIN_TYPEHASH) { + revert InvalidOrderDataType(order.orderDataType, ORDERDATA_ONCHAIN_TYPEHASH); + } + + // Decode the orderData + (orderData, expires) = decodeOrderData(order.orderData); + expires = sanitizeUint32(expires); + + // Ensure a valid mandate is provided + if (orderData.mandate.fills.length == 0 || order.fillDeadline != orderData.mandate.fills[0].expires) { + revert InvalidOrderData(order.orderData); + } + + (mandateHash, fillHashes) = hashMandate(orderData.mandate); + } + + function decodeOrderData(bytes calldata orderData) + internal + pure + returns (IERC7683Allocator.Order calldata order, uint32 additionalInput) + { + // orderData includes the OrderData(OnChain/Gasless) struct, and the nested Order struct. + // 0x00: OrderDataOnChain.offset + // 0x20: OrderDataOnChain.order.offset + // 0x40: OrderDataOnChain.expires + + // 0x00: OrderDataGasless.offset + // 0x20: OrderDataGasless.order.offset + + assembly ("memory-safe") { + let l := sub(orderData.length, 0x20) + let s := calldataload(add(orderData.offset, 0x20)) // Relative offset of `orderBytes` from `orderData.offset` and the `OrderData...` struct. + order := add(orderData.offset, add(s, 0x20)) // Add 0x20 since the OrderStruct is within the `OrderData...` struct + if shr(64, or(s, or(l, orderData.offset))) { revert(l, 0x00) } + + additionalInput := calldataload(add(orderData.offset, 0x40)) + } + } + + function resolveOrder( + address sponsor, + uint256 nonce, + uint32 expires, + bytes32[] memory fillHashes, + IERC7683Allocator.Order calldata orderData, + bytes calldata sponsorSignature + ) internal view returns (IOriginSettler.ResolvedCrossChainOrder memory) { + Fill memory mainFill = orderData.mandate.fills[0]; + + IOriginSettler.ResolvedCrossChainOrder memory resolvedOrder = IOriginSettler.ResolvedCrossChainOrder({ + user: sponsor, + originChainId: block.chainid, + openDeadline: expires, + fillDeadline: uint32(mainFill.expires), + orderId: bytes32(nonce), + maxSpent: new IERC7683Allocator.Output[](0), + minReceived: new IERC7683Allocator.Output[](0), + fillInstructions: new IERC7683Allocator.FillInstruction[](0) + }); + + BatchCompact memory compact = BatchCompact({ + arbiter: orderData.arbiter, + sponsor: sponsor, + nonce: nonce, + expires: expires, + commitments: orderData.commitments + }); + + Tribunal.BatchClaim memory claim = Tribunal.BatchClaim({ + chainId: block.chainid, + compact: compact, + sponsorSignature: sponsorSignature, + allocatorSignature: '' // No signature required from this allocator, it will verify the claim on chain via ERC1271. + }); + + IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); + fillInstructions[0] = IOriginSettler.FillInstruction({ + destinationChainId: mainFill.chainId, + destinationSettler: addressToBytes32(mainFill.tribunal), + originData: abi.encode(claim, mainFill, orderData.mandate.adjuster, fillHashes) + }); + resolvedOrder.fillInstructions = fillInstructions; + + IOriginSettler.Output memory spent = IOriginSettler.Output({ + token: addressToBytes32(mainFill.fillToken), + amount: type(uint256).max, + recipient: addressToBytes32(mainFill.recipient), + chainId: mainFill.chainId + }); + IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); + maxSpent[0] = spent; + resolvedOrder.maxSpent = maxSpent; + + resolvedOrder.minReceived = createMinimumReceived(orderData.commitments); + + return resolvedOrder; + } + + function createMinimumReceived(Lock[] calldata commitments) + internal + view + returns (IOriginSettler.Output[] memory) + { + IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](commitments.length); + + for (uint256 i = 0; i < commitments.length; i++) { + IOriginSettler.Output memory received = IOriginSettler.Output({ + token: addressToBytes32(commitments[i].token), + amount: commitments[i].amount, + recipient: bytes32(0), + chainId: block.chainid + }); + minReceived[i] = received; + } + return minReceived; + } + + function hashMandate(Mandate calldata mandate) internal pure returns (bytes32, bytes32[] memory fillHashes) { + fillHashes = new bytes32[](mandate.fills.length); + for (uint256 i = 0; i < mandate.fills.length; i++) { + fillHashes[i] = hashFill(mandate.fills[i]); + } + return ( + keccak256(abi.encode(MANDATE_TYPEHASH, mandate.adjuster, keccak256(abi.encodePacked(fillHashes)))), + fillHashes + ); + } + + function hashFill(Fill calldata fill) internal pure returns (bytes32) { + bytes32 priceCurveHash = keccak256(abi.encodePacked(fill.priceCurve)); + return keccak256( + abi.encode( + MANDATE_FILL_TYPEHASH, + fill.chainId, + fill.tribunal, + fill.expires, + fill.fillToken, + fill.minimumFillAmount, + fill.baselinePriorityFee, + fill.scalingFactor, + priceCurveHash, + fill.recipient, + hashRecipientCallback(fill.recipientCallback), + fill.salt + ) + ); + } + + function hashRecipientCallback(RecipientCallback[] calldata recipientCallback) internal pure returns (bytes32) { + if (recipientCallback.length == 0) { + // empty hash + return 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; + } else if (recipientCallback.length != 1) { + revert InvalidRecipientCallbackLength(); + } + + RecipientCallback calldata callback = recipientCallback[0]; + + return keccak256( + abi.encodePacked( + keccak256( + abi.encode( + MANDATE_RECIPIENT_CALLBACK_TYPEHASH, + callback.chainId, + AL.getClaimHash( + callback.compact.arbiter, + callback.compact.sponsor, + callback.compact.nonce, + callback.compact.expires, + AL.getCommitmentsHash(callback.compact.commitments), + callback.mandateHash, + MANDATE_BATCH_COMPACT_TYPEHASH + ), + callback.context + ) + ) + ) + ); + } + + function addressToBytes32(address input) internal pure returns (bytes32 output) { + assembly ("memory-safe") { + output := shr(96, shl(96, input)) + } + } + + function sanitizeUint32(uint32 value) internal pure returns (uint32) { + assembly ("memory-safe") { + value := shr(224, shl(224, value)) + } + return value; + } + + function sanitizeBool(uint32 value) internal pure returns (uint32) { + assembly ("memory-safe") { + value := iszero(iszero(value)) + } + return value; + } +} diff --git a/src/allocators/types/TribunalStructs.sol b/src/allocators/types/TribunalStructs.sol index 40ab38f..e8c8f4f 100644 --- a/src/allocators/types/TribunalStructs.sol +++ b/src/allocators/types/TribunalStructs.sol @@ -18,15 +18,55 @@ struct BatchClaim { bytes allocatorSignature; // Authorization from the allocator } +// struct Mandate { +// // uint256 chainId; // (implicit arg, included in EIP712 payload). +// // address tribunal; // (implicit arg, included in EIP712 payload). +// address recipient; // Recipient of filled tokens. +// uint256 expires; // Mandate expiration timestamp. +// address token; // Fill token (address(0) for native). +// uint256 minimumAmount; // Minimum fill amount. +// uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in. +// uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline). +// uint256[] decayCurve; // Block durations, fill increases, & claim decreases. +// bytes32 salt; // Replay protection parameter. +// } + +// Parent mandate signed by the sponsor on source chain. Note that the EIP-712 payload differs slightly from the structs declared here (mainly around utilizing full mandates rather than mandate hashes). struct Mandate { - // uint256 chainId; // (implicit arg, included in EIP712 payload). - // address tribunal; // (implicit arg, included in EIP712 payload). - address recipient; // Recipient of filled tokens. - uint256 expires; // Mandate expiration timestamp. - address token; // Fill token (address(0) for native). - uint256 minimumAmount; // Minimum fill amount. + address adjuster; + Fill[] fills; // Arbitrary-length array; note that in EIP-712 payload this is Mandate_Fill +} + +// Mandate_Fill in EIP-712 payload +struct Fill { + uint256 chainId; // Same-chain if value matches chainId(), otherwise cross-chain + address tribunal; // Contract where the fill is performed. + uint256 expires; // Fill expiration timestamp. + address fillToken; // Intermediate fill token (address(0) for native, same address for no action). + uint256 minimumFillAmount; // Minimum fill amount. uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in. uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline). - uint256[] decayCurve; // Block durations, fill increases, & claim decreases. - bytes32 salt; // Replay protection parameter. + uint256[] priceCurve; // Block durations and uint240 additional scaling factors per each duration. + address recipient; // Recipient of the tokens — address(0) or tribunal indicate that funds will be pulled by the directive. + RecipientCallback[] recipientCallback; // Array of length 0 or 1; note that in EIP-712 payload this is Mandate_RecipientCallback[] + bytes32 salt; +} + +// If a callback is specified, tribunal will follow up with a call to the recipient with fill details (including realized fill amount), a new compact and hash of an accompanying mandate, a target chainId, and context +// Note that this does not directly map to the EIP-712 payload (which contains a Mandate_BatchCompact containing the full `Mandate mandate` rather than BatchCompact + mandateHash) +// Mandate_RecipientCallback in EIP-712 payload +struct RecipientCallback { + uint256 chainId; + BatchCompact compact; + bytes32 mandateHash; + bytes context; +} + +// Arguments signed for by adjuster. +struct Adjustment { + // bytes32 claimHash included in EIP-712 payload but not provided as an argument. + uint256 fillIndex; + uint256 targetBlock; + uint256[] supplementalPriceCurve; // Additional scaling factor specified duration on price curve. + bytes32 validityConditions; // Optional value consisting of a number of blocks past the target and a exclusive filler address. } diff --git a/src/interfaces/IERC7683Allocator.sol b/src/interfaces/IERC7683Allocator.sol index f095091..794b44c 100644 --- a/src/interfaces/IERC7683Allocator.sol +++ b/src/interfaces/IERC7683Allocator.sol @@ -5,11 +5,12 @@ pragma solidity ^0.8.27; import {IOriginSettler} from './ERC7683/IOriginSettler.sol'; import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; import {Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; +import {Adjustment, Fill, Mandate, RecipientCallback} from '@uniswap/tribunal/types/TribunalStructs.sol'; interface IERC7683Allocator is IOriginSettler, IAllocator { struct OrderDataOnChain { Order order; // The remaining BatchCompact and Mandate data - uint256 expires; // COMPACT - The time at which the claim expires and the user is able to withdraw their funds. + uint32 expires; // COMPACT - The time at which the claim expires and the user is able to withdraw their funds. } struct OrderDataGasless { @@ -21,35 +22,23 @@ interface IERC7683Allocator is IOriginSettler, IAllocator { struct Order { address arbiter; // COMPACT - The account tasked with verifying and submitting the claim. Lock[] commitments; // COMPACT - The token IDs and amounts to allocate. - uint256 chainId; // MANDATE - (implicit arg, included in EIP712 payload) - address tribunal; // MANDATE - (implicit arg, included in EIP712 payload) - address recipient; // MANDATE - Recipient of settled tokens - // uint256 expires; // MANDATE - Mandate expiration timestamp, which equals the fill deadline - address settlementToken; // MANDATE - Settlement token (address(0) for native) - uint256 minimumAmount; // MANDATE - Minimum settlement amount - uint256 baselinePriorityFee; // MANDATE - Base fee threshold where scaling kicks in - uint256 scalingFactor; // MANDATE - Fee scaling multiplier (1e18 baseline) - uint256[] decayCurve; // MANDATE - Block durations, fill increases, & claim decreases. - bytes32 salt; // MANDATE - Replay protection parameter - bytes32 qualification; // ADDITIONAL INPUT - abi.encodePacked(uint200 targetBlock, uint56 maximumBlocksAfterTarget) The block number at the target chain on which the PGA is executed / the reverse dutch auction starts & blocks after target block that are still fillable. + Mandate mandate; // MANDATE - Mandate struct fom tribunal } error InvalidOriginSettler(address originSettler, address expectedOriginSettler); error InvalidOrderDataType(bytes32 orderDataType, bytes32 expectedOrderDataType); error InvalidNonce(uint256 nonce, uint256 expectedNonce); - error BatchCompactsNotSupported(); error InvalidAllocatorData(bytes32 expectedAllocatorData, bytes32 actualAllocatorData); error UnsupportedToken(address token); + error InvalidOrderData(bytes orderData); + error InvalidRecipientCallbackLength(); /// @notice Returns the type string of the compact including the witness function getCompactWitnessTypeString() external pure returns (string memory); - /// @notice Checks if a nonce is free to be used + /// @notice Returns the nonce for a given order and caller /// @dev The nonce is the most significant 96 bits. The least significant 160 bits must be the sponsor address - function checkNonce(GaslessCrossChainOrder calldata order_, address caller) - external - view - returns (bool nonceFree_); + function getNonce(GaslessCrossChainOrder calldata order_, address caller) external view returns (uint256 nonce); /// @notice Creates the filler data for the open event to be used on the IDestinationSettler /// @param claimant_ The address claiming the origin tokens after a successful fill (typically the address of the filler) From 4b647fbf32988f33190be54526e20429329fb5cd Mon Sep 17 00:00:00 2001 From: mgretzke Date: Fri, 22 Aug 2025 19:12:50 +0200 Subject: [PATCH 57/63] fixes and updated tests --- .gitmodules | 1 - lib/tribunal | 1 + snapshots/ERC7683Allocator_open.json | 4 +- snapshots/ERC7683Allocator_openFor.json | 5 +- snapshots/OnChainAllocatorTest.json | 14 +- src/allocators/ERC7683Allocator.sol | 29 +- src/allocators/HybridERC7683.sol | 24 +- src/allocators/OnChainAllocator.sol | 15 +- src/allocators/types/TribunalStructs.sol | 72 -- src/interfaces/IHybridAllocator.sol | 1 - src/interfaces/IHybridERC7683.sol | 38 - test/ERC7683Allocator.t.sol | 1099 +++++----------------- test/HybridERC7683.t.sol | 729 ++++---------- test/OnChainAllocator.t.sol | 133 +-- test/util/ERC7683TestHelper.sol | 427 +++++++++ 15 files changed, 960 insertions(+), 1632 deletions(-) create mode 160000 lib/tribunal delete mode 100644 src/allocators/types/TribunalStructs.sol delete mode 100644 src/interfaces/IHybridERC7683.sol create mode 100644 test/util/ERC7683TestHelper.sol diff --git a/.gitmodules b/.gitmodules index 0351d97..831c842 100644 --- a/.gitmodules +++ b/.gitmodules @@ -20,4 +20,3 @@ [submodule "lib/tribunal"] path = lib/tribunal url = https://github.com/Uniswap/tribunal - branch = new-mandate diff --git a/lib/tribunal b/lib/tribunal new file mode 160000 index 0000000..21b6e80 --- /dev/null +++ b/lib/tribunal @@ -0,0 +1 @@ +Subproject commit 21b6e8096d8d940789eeb63c5d281e490fd63025 diff --git a/snapshots/ERC7683Allocator_open.json b/snapshots/ERC7683Allocator_open.json index a4c31c3..9c7f3d9 100644 --- a/snapshots/ERC7683Allocator_open.json +++ b/snapshots/ERC7683Allocator_open.json @@ -1,3 +1,3 @@ { - "open_simpleOrder": "184704" -} + "open_simpleOrder": "168301" +} \ No newline at end of file diff --git a/snapshots/ERC7683Allocator_openFor.json b/snapshots/ERC7683Allocator_openFor.json index 4988ce8..8180834 100644 --- a/snapshots/ERC7683Allocator_openFor.json +++ b/snapshots/ERC7683Allocator_openFor.json @@ -1,4 +1,3 @@ { - "openFor_simpleOrder_relayed": "166911", - "openFor_simpleOrder_userHimself": "166943" -} + "openFor_simpleOrder_userHimself": "171902" +} \ No newline at end of file diff --git a/snapshots/OnChainAllocatorTest.json b/snapshots/OnChainAllocatorTest.json index 6553d18..ec5af8f 100644 --- a/snapshots/OnChainAllocatorTest.json +++ b/snapshots/OnChainAllocatorTest.json @@ -1,9 +1,9 @@ { - "allocateFor_success_withRegistration": "133994", - "allocate_and_delete_expired_allocation": "66140", - "allocate_erc20": "129411", - "allocate_native": "129171", - "allocate_second_erc20": "97423", - "onchain_execute_double": "348227", - "onchain_execute_single": "220217" + "allocateFor_success_withRegistration": "133753", + "allocate_and_delete_expired_allocation": "65921", + "allocate_erc20": "129192", + "allocate_native": "128952", + "allocate_second_erc20": "97204", + "onchain_execute_double": "347707", + "onchain_execute_single": "219699" } \ No newline at end of file diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index eaf1758..cf4cb55 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -12,6 +12,7 @@ import {Tribunal} from '@uniswap/tribunal/Tribunal.sol'; import {Fill, Mandate, RecipientCallback} from '@uniswap/tribunal/types/TribunalStructs.sol'; import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; +import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; import { COMPACT_TYPEHASH_WITH_MANDATE, COMPACT_WITH_MANDATE_TYPESTRING, @@ -43,7 +44,7 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { uint160 caller = uint160(deposit * uint160(msg.sender)); // for a deposit, the nonce will be scoped to the caller // Early revert if the expected nonce is not the next nonce - if (order.nonce != _getNonce(address(caller), order.user)) { + if (deposit == 0 && order.nonce != _getNonce(address(caller), order.user)) { revert InvalidNonce(order.nonce, _getNonce(address(caller), order.user)); } @@ -60,7 +61,7 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { sponsorSignature ); } else { - // Register the allocation on chain + // Register the allocation on chain by using a deposit uint256[] memory registeredAmounts; (, registeredAmounts, nonce) = allocateAndRegister( order.user, @@ -71,6 +72,10 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { mandateHash ); + // We ignore the order.nonce and use the one assigned by the hybrid allocator + resolvedOrder.orderId = bytes32(nonce); + + // Update the resolved order with the registered amounts for (uint256 i = 0; i < orderData.commitments.length; i++) { resolvedOrder.minReceived[i].amount = registeredAmounts[i]; } @@ -85,9 +90,14 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { ERC7683AL.openPreparation(order); // Register the allocation on chain - (, uint256 nonce) = + (bytes32 claimHash, uint256 nonce) = allocate(orderData.commitments, orderData.arbiter, expires, COMPACT_TYPEHASH_WITH_MANDATE, mandateHash); + // Ensure a registration exists before opening the order + if (!ITheCompact(COMPACT_CONTRACT).isRegistered(msg.sender, claimHash, COMPACT_TYPEHASH_WITH_MANDATE)) { + revert InvalidRegistration(msg.sender, claimHash); + } + ResolvedCrossChainOrder memory resolvedOrder = ERC7683AL.resolveOrder(msg.sender, nonce, expires, fillHashes, orderData, LibBytes.emptyCalldata()); @@ -106,7 +116,7 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { uint160 caller = uint160(deposit * uint160(msg.sender)); // for a deposit, the nonce will be scoped to the caller + user - // Early revert if the expected nonce is not the next nonce + // Revert if the expected nonce is not the next nonce if (order.nonce != _getNonce(address(caller), order.user)) { revert InvalidNonce(order.nonce, _getNonce(address(caller), order.user)); } @@ -143,15 +153,4 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { function createFillerData(address claimant) external pure returns (bytes memory fillerData) { return abi.encode(claimant); } - - function _getNonce(address calling, address sponsor) internal view returns (uint256 nonce) { - assembly ("memory-safe") { - calling := xor(calling, mul(sponsor, iszero(calling))) - mstore(0x00, calling) - mstore(0x20, nonces.slot) - let nonceSlot := keccak256(0x00, 0x40) - let nonce96 := sload(nonceSlot) - nonce := or(shl(96, calling), add(nonce96, 1)) - } - } } diff --git a/src/allocators/HybridERC7683.sol b/src/allocators/HybridERC7683.sol index a4bcbfc..1b97dc3 100644 --- a/src/allocators/HybridERC7683.sol +++ b/src/allocators/HybridERC7683.sol @@ -37,14 +37,6 @@ contract HybridERC7683 is HybridAllocator, IERC7683Allocator { IOriginSettler.ResolvedCrossChainOrder memory resolvedOrder ) = ERC7683AL.openForPreparation(order, sponsorSignature); - uint160 caller = uint160(deposit * uint160(msg.sender)); // for a deposit, the nonce will be scoped to the caller - - // Early revert if the expected nonce is not the next nonce - if (order.nonce != nonces + 1) { - revert InvalidNonce(order.nonce, nonces + 1); - } - - uint256 nonce; if (deposit == 0) { // Hybrid Allocator requires a deposit revert OnlyDepositsAllowed(); @@ -56,9 +48,8 @@ contract HybridERC7683 is HybridAllocator, IERC7683Allocator { idsAndAmounts[i][1] = orderData.commitments[i].amount; } - // Register the allocation on chain - uint256[] memory registeredAmounts; - (, registeredAmounts, nonce) = allocateAndRegister( + // Register the allocation on chain by using a deposit + (, uint256[] memory registeredAmounts, uint256 nonce) = allocateAndRegister( order.user, idsAndAmounts, orderData.arbiter, @@ -67,12 +58,17 @@ contract HybridERC7683 is HybridAllocator, IERC7683Allocator { mandateHash ); + // We ignore the order.nonce and use the one assigned by the hybrid allocator + resolvedOrder.orderId = bytes32(nonce); + + // Update the resolved order with the registered amounts for (uint256 i = 0; i < orderData.commitments.length; i++) { resolvedOrder.minReceived[i].amount = registeredAmounts[i]; } + + // Emit an open event + emit Open(bytes32(nonce), resolvedOrder); } - // Emit an open event - emit Open(bytes32(nonce), resolvedOrder); } /// @inheritdoc IOriginSettler @@ -88,7 +84,7 @@ contract HybridERC7683 is HybridAllocator, IERC7683Allocator { } // deposit the the tokens into the compact and register the claim - (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = allocateAndRegister( + (, uint256[] memory registeredAmounts, uint256 nonce) = allocateAndRegister( msg.sender, idsAndAmounts, orderData.arbiter, expires, COMPACT_TYPEHASH_WITH_MANDATE, mandateHash ); ResolvedCrossChainOrder memory resolvedOrder = diff --git a/src/allocators/OnChainAllocator.sol b/src/allocators/OnChainAllocator.sol index 24f141a..71e2d77 100644 --- a/src/allocators/OnChainAllocator.sol +++ b/src/allocators/OnChainAllocator.sol @@ -23,7 +23,7 @@ contract OnChainAllocator is IOnChainAllocator { mapping(bytes32 tokenHash => Allocation[] allocations) internal _allocations; - mapping(bytes32 user => uint96 nonce) public nonces; + mapping(address user => uint96 nonce) public nonces; modifier onlyCompact() { if (msg.sender != COMPACT_CONTRACT) { @@ -157,7 +157,7 @@ contract OnChainAllocator is IOnChainAllocator { bytes calldata /* orderData */ ) external returns (uint256 nonce) { uint32 expiration = uint32(expires); - nonce = _getAndUpdateNonce(msg.sender, recipient); + nonce = _getNonce(msg.sender, recipient); AL.prepareAllocation(COMPACT_CONTRACT, nonce, recipient, idsAndAmounts, arbiter, expiration, typehash, witness); return nonce; @@ -479,6 +479,17 @@ contract OnChainAllocator is IOnChainAllocator { } } + function _getNonce(address calling, address sponsor) internal view returns (uint256 nonce) { + assembly ("memory-safe") { + calling := xor(calling, mul(sponsor, iszero(calling))) + mstore(0x00, calling) + mstore(0x20, nonces.slot) + let nonceSlot := keccak256(0x00, 0x40) + let nonce96 := sload(nonceSlot) + nonce := or(shl(96, calling), add(nonce96, 1)) + } + } + function _getTokenHash(bytes12 lockTag, address token, address sponsor) internal pure returns (bytes32 tokenHash) { assembly ("memory-safe") { mstore(0x00, lockTag) diff --git a/src/allocators/types/TribunalStructs.sol b/src/allocators/types/TribunalStructs.sol deleted file mode 100644 index e8c8f4f..0000000 --- a/src/allocators/types/TribunalStructs.sol +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.27; - -import {BatchCompact, Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; - -struct Claim { - uint256 chainId; // Claim processing chain ID - Compact compact; - bytes sponsorSignature; // Authorization from the sponsor - bytes allocatorSignature; // Authorization from the allocator -} - -struct BatchClaim { - uint256 chainId; // Claim processing chain ID - BatchCompact compact; - bytes sponsorSignature; // Authorization from the sponsor - bytes allocatorSignature; // Authorization from the allocator -} - -// struct Mandate { -// // uint256 chainId; // (implicit arg, included in EIP712 payload). -// // address tribunal; // (implicit arg, included in EIP712 payload). -// address recipient; // Recipient of filled tokens. -// uint256 expires; // Mandate expiration timestamp. -// address token; // Fill token (address(0) for native). -// uint256 minimumAmount; // Minimum fill amount. -// uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in. -// uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline). -// uint256[] decayCurve; // Block durations, fill increases, & claim decreases. -// bytes32 salt; // Replay protection parameter. -// } - -// Parent mandate signed by the sponsor on source chain. Note that the EIP-712 payload differs slightly from the structs declared here (mainly around utilizing full mandates rather than mandate hashes). -struct Mandate { - address adjuster; - Fill[] fills; // Arbitrary-length array; note that in EIP-712 payload this is Mandate_Fill -} - -// Mandate_Fill in EIP-712 payload -struct Fill { - uint256 chainId; // Same-chain if value matches chainId(), otherwise cross-chain - address tribunal; // Contract where the fill is performed. - uint256 expires; // Fill expiration timestamp. - address fillToken; // Intermediate fill token (address(0) for native, same address for no action). - uint256 minimumFillAmount; // Minimum fill amount. - uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in. - uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline). - uint256[] priceCurve; // Block durations and uint240 additional scaling factors per each duration. - address recipient; // Recipient of the tokens — address(0) or tribunal indicate that funds will be pulled by the directive. - RecipientCallback[] recipientCallback; // Array of length 0 or 1; note that in EIP-712 payload this is Mandate_RecipientCallback[] - bytes32 salt; -} - -// If a callback is specified, tribunal will follow up with a call to the recipient with fill details (including realized fill amount), a new compact and hash of an accompanying mandate, a target chainId, and context -// Note that this does not directly map to the EIP-712 payload (which contains a Mandate_BatchCompact containing the full `Mandate mandate` rather than BatchCompact + mandateHash) -// Mandate_RecipientCallback in EIP-712 payload -struct RecipientCallback { - uint256 chainId; - BatchCompact compact; - bytes32 mandateHash; - bytes context; -} - -// Arguments signed for by adjuster. -struct Adjustment { - // bytes32 claimHash included in EIP-712 payload but not provided as an argument. - uint256 fillIndex; - uint256 targetBlock; - uint256[] supplementalPriceCurve; // Additional scaling factor specified duration on price curve. - bytes32 validityConditions; // Optional value consisting of a number of blocks past the target and a exclusive filler address. -} diff --git a/src/interfaces/IHybridAllocator.sol b/src/interfaces/IHybridAllocator.sol index bf03104..bb4c355 100644 --- a/src/interfaces/IHybridAllocator.sol +++ b/src/interfaces/IHybridAllocator.sol @@ -9,7 +9,6 @@ interface IHybridAllocator is IOnChainAllocation { error InvalidIds(); error InvalidAllocatorId(uint96 allocatorId, uint96 expectedAllocatorId); error InvalidCaller(address sender, address expectedSender); - error InvalidAllocatorData(uint256 length); error InvalidSignature(); error InvalidSigner(); error LastSigner(); diff --git a/src/interfaces/IHybridERC7683.sol b/src/interfaces/IHybridERC7683.sol deleted file mode 100644 index 7db3d8c..0000000 --- a/src/interfaces/IHybridERC7683.sol +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.27; - -import {Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; -import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; -import {IHybridAllocator} from 'src/interfaces/IHybridAllocator.sol'; - -interface IHybridERC7683 is IHybridAllocator, IOriginSettler { - struct OrderDataOnChain { - Order order; // The remaining BatchCompact and Mandate data - uint256 expires; // COMPACT - The time at which the claim expires and the user is able to withdraw their funds. - } - - struct OrderDataGasless { - Order order; // The remaining BatchCompact and Mandate data - } - - /// @dev The data that OnChain and Gasless orders have in common - struct Order { - address arbiter; // COMPACT - The account tasked with verifying and submitting the claim. - uint256[2][] idsAndAmounts; // COMPACT - The token IDs and amounts to allocate. - uint256 chainId; // MANDATE - (implicit arg, included in EIP712 payload) - address tribunal; // MANDATE - (implicit arg, included in EIP712 payload) - address recipient; // MANDATE - Recipient of settled tokens - // uint256 expires; // MANDATE - Mandate expiration timestamp, which equals the fill deadline - address settlementToken; // MANDATE - Settlement token (address(0) for native) - uint256 minimumAmount; // MANDATE - Minimum settlement amount - uint256 baselinePriorityFee; // MANDATE - Base fee threshold where scaling kicks in - uint256 scalingFactor; // MANDATE - Fee scaling multiplier (1e18 baseline) - uint256[] decayCurve; // MANDATE - Block durations, fill increases, & claim decreases. - bytes32 salt; // MANDATE - Replay protection parameter - bytes32 qualification; // ADDITIONAL INPUT - [uint199 targetBlock, uint56 maximumBlocksAfterTarget, uint1(0)] The block number at the target chain on which the PGA is executed / the reverse dutch auction starts & blocks after target block that are still fillable. - } - - error InvalidOrderDataType(bytes32 orderDataType, bytes32 expectedOrderDataType); - error InvalidOriginSettler(address originSettler, address expectedOriginSettler); - error InvalidQualification(bytes32 qualification); -} diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index 4aef2e9..f71de5b 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -20,418 +20,44 @@ import {Test} from 'forge-std/Test.sol'; import {TestHelper} from 'test/util/TestHelper.sol'; import {IOnChainAllocation} from '@uniswap/the-compact/interfaces/IOnChainAllocation.sol'; + +import {Tribunal} from '@uniswap/tribunal/Tribunal.sol'; +import {Fill, Mandate, RecipientCallback} from '@uniswap/tribunal/types/TribunalStructs.sol'; +import { + COMPACT_TYPEHASH_WITH_MANDATE, + MANDATE_BATCH_COMPACT_TYPEHASH, + MANDATE_FILL_TYPEHASH, + MANDATE_RECIPIENT_CALLBACK_TYPEHASH, + MANDATE_TYPEHASH, + WITNESS_TYPESTRING as WITNESS_TYPESTRING_TRIBUNAL +} from '@uniswap/tribunal/types/TribunalTypeHashes.sol'; import {ERC7683Allocator} from 'src/allocators/ERC7683Allocator.sol'; -import {BatchClaim as TribunalClaim, Mandate} from 'src/allocators/types/TribunalStructs.sol'; +import {ERC7683AllocatorLib as ERC7683AL} from 'src/allocators/lib/ERC7683AllocatorLib.sol'; import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; import {IERC7683Allocator} from 'src/interfaces/IERC7683Allocator.sol'; import {IOnChainAllocator} from 'src/interfaces/IOnChainAllocator.sol'; import {ERC20Mock} from 'src/test/ERC20Mock.sol'; -abstract contract MocksSetup is Test, TestHelper { - address user; - uint256 userPK; - address attacker; - uint256 attackerPK; - address arbiter; - address tribunal; - ERC20Mock usdc; - TheCompact compactContract; - ERC7683Allocator erc7683Allocator; - bytes12 usdcLockTag; - uint256 usdcId; - - ResetPeriod defaultResetPeriod = ResetPeriod.OneMinute; - Scope defaultScope = Scope.Multichain; - uint256 defaultResetPeriodTimestamp = 60 - 1; - uint256 defaultAmount = 1000; - uint256 defaultNonce; - uint256 defaultOutputChainId = 130; - address defaultOutputToken = makeAddr('outputToken'); - uint256 defaultMinimumAmount = 1000; - uint256 defaultBaselinePriorityFee = 0; - uint256 defaultScalingFactor = 0; - uint256[] defaultDecayCurve = new uint256[](0); - bytes32 defaultSalt = bytes32(0x0000000000000000000000000000000000000000000000000000000000000007); - uint200 defaultTargetBlock = 100; - uint56 defaultMaximumBlocksAfterTarget = 10; - - uint256[2][] defaultIdsAndAmounts = new uint256[2][](1); - Lock[] defaultCommitments; - - bytes32 ORDERDATA_GASLESS_TYPEHASH; - bytes32 ORDERDATA_ONCHAIN_TYPEHASH; - - uint256 NONCES_STORAGE_SLOT = 1; - - function setUp() public virtual { - (user, userPK) = makeAddrAndKey('user'); - arbiter = makeAddr('arbiter'); - tribunal = makeAddr('tribunal'); - usdc = new ERC20Mock('USDC', 'USDC'); - compactContract = new TheCompact(); - erc7683Allocator = new ERC7683Allocator(address(compactContract)); - - usdcLockTag = _toLockTag(address(erc7683Allocator), defaultScope, defaultResetPeriod); - usdcId = _toId(defaultScope, defaultResetPeriod, address(erc7683Allocator), address(usdc)); - (attacker, attackerPK) = makeAddrAndKey('attacker'); - defaultNonce = 1; - - ORDERDATA_GASLESS_TYPEHASH = keccak256( - 'OrderDataGasless(Order order,bool deposit)Lock(bytes12 lockTag,address token,uint256 amount)Order(address arbiter,Lock[] commitments,uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt,bytes32 qualification)' - ); - ORDERDATA_ONCHAIN_TYPEHASH = keccak256( - 'OrderDataOnChain(Order order,uint256 expires)Lock(bytes12 lockTag,address token,uint256 amount)Order(address arbiter,Lock[] commitments,uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt,bytes32 qualification)' - ); - } -} - -abstract contract CreateHash is MocksSetup { - struct Allocator { - bytes32 hash; - } - - // stringified types - string EIP712_DOMAIN_TYPE = 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'; // Hashed inside the function - // EIP712 domain type - string name = 'The Compact'; - string version = '1'; - - string compactWitnessTypeString = - 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,bytes12 lockTag,address token,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; - string batchCompactWitnessTypeString = - 'BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments,Mandate mandate)Lock(bytes12 lockTag,address token,uint256 amount)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; - string mandateTypeString = - 'Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; - string witnessTypeString = - 'uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt'; - - function _hashCompact(BatchCompact memory data, Mandate memory mandate, address verifyingContract) - internal - view - returns (bytes32 digest) - { - bytes32 compactHash = _hashCompact(data, mandate); - // hash typed data - digest = keccak256( - abi.encodePacked( - '\x19\x01', // backslash is needed to escape the character - _domainSeparator(verifyingContract), - compactHash - ) - ); - } - - function _hashCompact(BatchCompact memory data, Mandate memory mandate) - internal - view - returns (bytes32 compactHash) - { - bytes32 mandateHash = _hashMandate(mandate); - compactHash = keccak256( - abi.encode( - keccak256(bytes(batchCompactWitnessTypeString)), - data.arbiter, - data.sponsor, - data.nonce, - data.expires, - _hashCommitments(data.commitments), - mandateHash - ) - ); - } - - function _hashMandate(Mandate memory mandate) internal view returns (bytes32) { - return keccak256( - abi.encode( - keccak256(bytes(mandateTypeString)), - defaultOutputChainId, - tribunal, - mandate.recipient, - mandate.expires, - mandate.token, - mandate.minimumAmount, - mandate.baselinePriorityFee, - mandate.scalingFactor, - keccak256(abi.encodePacked(mandate.decayCurve)), - mandate.salt - ) - ); - } - - function _hashCommitments(Lock[] memory commitments) internal pure returns (bytes32) { - bytes32[] memory hashes = new bytes32[](commitments.length); - for (uint256 i = 0; i < commitments.length; i++) { - hashes[i] = keccak256( - abi.encode(LOCK_TYPEHASH, commitments[i].lockTag, commitments[i].token, commitments[i].amount) - ); - } - return keccak256(abi.encodePacked(hashes)); - } - - function _getTypeHash() internal view returns (bytes32) { - return keccak256(bytes(batchCompactWitnessTypeString)); - } - - function _domainSeparator(address verifyingContract) internal view returns (bytes32) { - return keccak256( - abi.encode( - keccak256(bytes(EIP712_DOMAIN_TYPE)), - keccak256(bytes(name)), - keccak256(bytes(version)), - block.chainid, - verifyingContract - ) - ); - } - - function _signMessage(bytes32 hash_, uint256 signerPK_) internal pure returns (bytes memory) { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPK_, hash_); - return abi.encodePacked(r, s, v); - } - - function _hashAndSign(BatchCompact memory data, Mandate memory mandate, address verifyingContract, uint256 signerPK) - internal - view - returns (bytes memory) - { - bytes32 hash = _hashCompact(data, mandate, verifyingContract); - bytes memory signature = _signMessage(hash, signerPK); - return signature; - } -} - -abstract contract CompactData is CreateHash { - BatchCompact private compact; - Mandate private mandate; - - function setUp() public virtual override { - super.setUp(); - - defaultIdsAndAmounts[0][0] = usdcId; - defaultIdsAndAmounts[0][1] = defaultAmount; - - defaultCommitments.push(Lock({lockTag: usdcLockTag, token: address(usdc), amount: defaultAmount})); - - compact.arbiter = arbiter; - compact.sponsor = user; - compact.nonce = defaultNonce; - compact.expires = _getClaimExpiration(); - compact.commitments = defaultCommitments; - - mandate.recipient = user; - mandate.expires = _getFillExpiration(); - mandate.token = defaultOutputToken; - mandate.minimumAmount = defaultMinimumAmount; - mandate.baselinePriorityFee = defaultBaselinePriorityFee; - mandate.scalingFactor = defaultScalingFactor; - mandate.decayCurve = defaultDecayCurve; - mandate.salt = defaultSalt; - } - - function _getCompact() internal returns (BatchCompact memory) { - compact.expires = _getClaimExpiration(); - return compact; - } - - function _getMandate() internal returns (Mandate memory) { - mandate.expires = _getFillExpiration(); - return mandate; - } - - function _getFillExpiration() internal view returns (uint256) { - return vm.getBlockTimestamp() + defaultResetPeriodTimestamp - 1; - } - - function _getClaimExpiration() internal view returns (uint256) { - return vm.getBlockTimestamp() + defaultResetPeriodTimestamp; - } -} - -abstract contract GaslessCrossChainOrderData is CompactData { - IOriginSettler.GaslessCrossChainOrder private gaslessCrossChainOrder; - - function setUp() public virtual override { - super.setUp(); - - BatchCompact memory compact_ = _getCompact(); - Mandate memory mandate_ = _getMandate(); - - gaslessCrossChainOrder.originSettler = address(erc7683Allocator); - gaslessCrossChainOrder.user = compact_.sponsor; - gaslessCrossChainOrder.nonce = compact_.nonce; - gaslessCrossChainOrder.originChainId = block.chainid; - gaslessCrossChainOrder.openDeadline = uint32(_getClaimExpiration()); - gaslessCrossChainOrder.fillDeadline = uint32(_getFillExpiration()); - gaslessCrossChainOrder.orderDataType = erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH(); - gaslessCrossChainOrder.orderData = abi.encode( - IERC7683Allocator.OrderDataGasless({ - order: IERC7683Allocator.Order({ - arbiter: compact_.arbiter, - commitments: compact_.commitments, - chainId: defaultOutputChainId, - tribunal: tribunal, - recipient: mandate_.recipient, - settlementToken: mandate_.token, - minimumAmount: mandate_.minimumAmount, - baselinePriorityFee: mandate_.baselinePriorityFee, - scalingFactor: mandate_.scalingFactor, - decayCurve: mandate_.decayCurve, - salt: mandate_.salt, - qualification: bytes32(0) - }), - deposit: false - }) - ); - } - - function _getGaslessCrossChainOrder( - address allocator, - BatchCompact memory compact_, - Mandate memory mandate_, - uint256 chainId_, - bytes32 orderDataGaslessTypeHash_, - address verifyingContract, - uint256 signerPK, - bytes32 qualification - ) internal view returns (IOriginSettler.GaslessCrossChainOrder memory, bytes memory signature) { - IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_ = IOriginSettler.GaslessCrossChainOrder({ - originSettler: allocator, - user: compact_.sponsor, - nonce: compact_.nonce, - originChainId: chainId_, - openDeadline: uint32(compact_.expires), - fillDeadline: uint32(mandate_.expires), - orderDataType: orderDataGaslessTypeHash_, - orderData: abi.encode( - IERC7683Allocator.OrderDataGasless({ - order: IERC7683Allocator.Order({ - arbiter: compact_.arbiter, - commitments: compact_.commitments, - chainId: defaultOutputChainId, - tribunal: tribunal, - recipient: mandate_.recipient, - settlementToken: mandate_.token, - minimumAmount: mandate_.minimumAmount, - baselinePriorityFee: mandate_.baselinePriorityFee, - scalingFactor: mandate_.scalingFactor, - decayCurve: mandate_.decayCurve, - salt: mandate_.salt, - qualification: qualification - }), - deposit: false - }) - ) - }); - - (bytes memory signature_) = _hashAndSign(compact_, mandate_, verifyingContract, signerPK); - return (gaslessCrossChainOrder_, signature_); - } - - function _getGaslessCrossChainOrder() - internal - returns (IOriginSettler.GaslessCrossChainOrder memory, bytes memory signature) - { - (bytes memory signature_) = _hashAndSign(_getCompact(), _getMandate(), address(compactContract), userPK); - return (gaslessCrossChainOrder, signature_); - } - - function _manipulateDeposit(IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bool deposit) - internal - pure - returns (IOriginSettler.GaslessCrossChainOrder memory) - { - bytes memory orderData = gaslessCrossChainOrder_.orderData; - assembly ("memory-safe") { - mstore(add(orderData, 0x60), deposit) - } - return gaslessCrossChainOrder_; - } -} - -abstract contract OnChainCrossChainOrderData is CompactData { - IOriginSettler.OnchainCrossChainOrder private onchainCrossChainOrder; - - function setUp() public virtual override { - super.setUp(); - - BatchCompact memory compact_ = _getCompact(); - Mandate memory mandate_ = _getMandate(); - - onchainCrossChainOrder.fillDeadline = uint32(_getFillExpiration()); - onchainCrossChainOrder.orderDataType = erc7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH(); - onchainCrossChainOrder.orderData = abi.encode( - IERC7683Allocator.OrderDataOnChain({ - order: IERC7683Allocator.Order({ - arbiter: compact_.arbiter, - commitments: compact_.commitments, - chainId: defaultOutputChainId, - tribunal: tribunal, - recipient: mandate_.recipient, - settlementToken: mandate_.token, - minimumAmount: mandate_.minimumAmount, - baselinePriorityFee: mandate_.baselinePriorityFee, - scalingFactor: mandate_.scalingFactor, - decayCurve: mandate_.decayCurve, - salt: mandate_.salt, - qualification: bytes32(abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget)) - }), - expires: compact_.expires - }) - ); - } +import { + CompactData, + GaslessCrossChainOrderData, + MocksSetup, + OnChainCrossChainOrderData +} from 'test/util/ERC7683TestHelper.sol'; - function _getOnChainCrossChainOrder() internal view returns (IOriginSettler.OnchainCrossChainOrder memory) { - return onchainCrossChainOrder; - } - - function _getOnChainCrossChainOrder(BatchCompact memory compact_, Mandate memory mandate_, bytes32 orderDataType_) - internal - view - returns (IOriginSettler.OnchainCrossChainOrder memory) - { - IOriginSettler.OnchainCrossChainOrder memory onchainCrossChainOrder_ = IOriginSettler.OnchainCrossChainOrder({ - fillDeadline: uint32(mandate_.expires), - orderDataType: orderDataType_, - orderData: abi.encode( - IERC7683Allocator.OrderDataOnChain({ - order: IERC7683Allocator.Order({ - arbiter: compact_.arbiter, - commitments: compact_.commitments, - chainId: defaultOutputChainId, - tribunal: tribunal, - recipient: mandate_.recipient, - settlementToken: mandate_.token, - minimumAmount: mandate_.minimumAmount, - baselinePriorityFee: mandate_.baselinePriorityFee, - scalingFactor: mandate_.scalingFactor, - decayCurve: mandate_.decayCurve, - salt: mandate_.salt, - qualification: bytes32(abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget)) - }), - expires: compact_.expires - }) - ) - }); - return onchainCrossChainOrder_; - } -} +contract MockAllocator is GaslessCrossChainOrderData, OnChainCrossChainOrderData { + ERC7683Allocator erc7683Allocator; -abstract contract Deposited is MocksSetup { - function setUp() public virtual override { + function setUp() public virtual override(GaslessCrossChainOrderData, OnChainCrossChainOrderData) { + TheCompact compactContract_ = new TheCompact(); + erc7683Allocator = new ERC7683Allocator(address(compactContract_)); + _setUp(address(erc7683Allocator), compactContract_, _composeNonceUint(user, 1)); super.setUp(); - - vm.startPrank(user); - - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); - compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); - - vm.stopPrank(); } } -contract ERC7683Allocator_open is OnChainCrossChainOrderData { +contract ERC7683Allocator_open is MockAllocator { function test_revert_InvalidOrderDataType() public { // Order data type is invalid bytes32 falseOrderDataType = keccak256('false'); @@ -441,32 +67,26 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { vm.prank(user); vm.expectRevert( abi.encodeWithSelector( - IERC7683Allocator.InvalidOrderDataType.selector, - falseOrderDataType, - erc7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH() + IERC7683Allocator.InvalidOrderDataType.selector, falseOrderDataType, ORDERDATA_ONCHAIN_TYPEHASH ) ); erc7683Allocator.open(onChainCrossChainOrder_); } - function test_orderDataType() public view { - assertEq(erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH(), ORDERDATA_GASLESS_TYPEHASH); - } + // Removed redundant typehash equality check; we compare against library in setup. function test_revert_ManipulatedOrderData() public { // Deposit tokens vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); + (bytes32 mandateHash,) = _hashMandate(mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandateHash); + compactContract.register(claimHash, COMPACT_TYPEHASH_WITH_MANDATE); vm.stopPrank(); @@ -498,19 +118,18 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { function test_revert_InvalidRegistration() public { // we deposit tokens vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // we do NOT register a claim vm.stopPrank(); - (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); - BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - bytes32 claimHash = _hashCompact(compact_, mandate_); + (bytes32 mandateHash,) = _hashMandate(mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandateHash); + (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = + _getOnChainCrossChainOrder(compact_, mandate_); vm.prank(user); vm.expectRevert(abi.encodeWithSelector(IOnChainAllocation.InvalidRegistration.selector, user, claimHash)); @@ -520,17 +139,15 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { function test_successful() public { // Deposit tokens vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); + (bytes32 mHash,) = _hashMandate(mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mHash); + compactContract.register(claimHash, COMPACT_TYPEHASH_WITH_MANDATE); vm.stopPrank(); @@ -550,7 +167,7 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { recipient: '', chainId: block.chainid }); - TribunalClaim memory claim = TribunalClaim({ + Tribunal.BatchClaim memory claim = Tribunal.BatchClaim({ chainId: block.chainid, compact: _getCompact(), sponsorSignature: '', @@ -559,7 +176,7 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, _getMandate(), defaultTargetBlock, defaultMaximumBlocksAfterTarget) + originData: abi.encode(claim, _getMandate().fills[0], adjuster, _buildFillHashes(_getMandate())) }); IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ @@ -573,14 +190,14 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { fillInstructions: fillInstructions }); vm.prank(user); - vm.expectEmit(true, false, false, true, address(erc7683Allocator)); + vm.expectEmit(true, false, false, false, address(erc7683Allocator)); emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); erc7683Allocator.open(onChainCrossChainOrder_); vm.snapshotGasLastCall('open_simpleOrder'); } } -contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { +contract ERC7683Allocator_openFor is MockAllocator { function test_revert_InvalidOrderDataType() public { // Order data type is invalid bytes32 falseOrderDataType = keccak256('false'); @@ -588,29 +205,23 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { vm.prank(user); vm.expectRevert( abi.encodeWithSelector( - IERC7683Allocator.InvalidOrderDataType.selector, - falseOrderDataType, - erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH() + IERC7683Allocator.InvalidOrderDataType.selector, falseOrderDataType, ORDERDATA_GASLESS_TYPEHASH ) ); - (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = - _getGaslessCrossChainOrder(); + IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder = _getGaslessCrossChainOrder(); falseGaslessCrossChainOrder.orderDataType = falseOrderDataType; - erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); + erc7683Allocator.openFor(falseGaslessCrossChainOrder, '', ''); } - function test_orderDataType() public view { - assertEq(erc7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH(), ORDERDATA_ONCHAIN_TYPEHASH); - } + // removed redundant typehash getter check function test_revert_InvalidDecoding() public { // Decoding fails because of additional data vm.prank(user); vm.expectRevert(); - (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = - _getGaslessCrossChainOrder(); + IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder = _getGaslessCrossChainOrder(); falseGaslessCrossChainOrder.orderData = abi.encode(falseGaslessCrossChainOrder.orderData, uint8(1)); - erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); + erc7683Allocator.openFor(falseGaslessCrossChainOrder, '', ''); } function test_revert_InvalidOriginSettler() public { @@ -621,19 +232,10 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { IERC7683Allocator.InvalidOriginSettler.selector, falseOriginSettler, address(erc7683Allocator) ) ); - (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = - _getGaslessCrossChainOrder( - falseOriginSettler, - _getCompact(), - _getMandate(), - block.chainid, - ORDERDATA_GASLESS_TYPEHASH, - address(erc7683Allocator), - userPK, - bytes32(0) - ); + IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder = _getGaslessCrossChainOrder(); + falseGaslessCrossChainOrder.originSettler = falseOriginSettler; vm.prank(user); - erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); + erc7683Allocator.openFor(falseGaslessCrossChainOrder, '', ''); } function test_revert_InvalidNonce(uint256 nonce) public { @@ -642,58 +244,91 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { BatchCompact memory compact_ = _getCompact(); compact_.nonce = nonce; vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidNonce.selector, compact_.nonce, defaultNonce)); - (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = - _getGaslessCrossChainOrder( - address(erc7683Allocator), - compact_, - _getMandate(), - block.chainid, - ORDERDATA_GASLESS_TYPEHASH, - address(erc7683Allocator), - userPK, - bytes32(0) - ); + IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder = _getGaslessCrossChainOrder(); + falseGaslessCrossChainOrder.nonce = nonce; vm.prank(user); - erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); + erc7683Allocator.openFor(falseGaslessCrossChainOrder, '', ''); } - function test_revert_InvalidSponsorSignature() public { - // Sponsor signature is invalid - + function test_successful_userHimself() public { // Deposit tokens vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + (bytes32 mandateHash,) = _hashMandate(mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandateHash); + + // Register the claim to allow to open the order + compactContract.register(claimHash, COMPACT_TYPEHASH_WITH_MANDATE); + vm.stopPrank(); - // Create a malicious signature - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = - _getGaslessCrossChainOrder( - address(erc7683Allocator), - _getCompact(), - _getMandate(), - block.chainid, - ORDERDATA_GASLESS_TYPEHASH, - address(compactContract), - attackerPK, - bytes32(0) - ); + IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_ = + _getGaslessCrossChainOrder(compact_, mandate_, false); + IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); + IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); + IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); + maxSpent[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(defaultOutputToken))), + amount: type(uint256).max, + recipient: bytes32(uint256(uint160(user))), + chainId: defaultOutputChainId + }); + minReceived[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(address(usdc)))), + amount: defaultAmount, + recipient: '', + chainId: block.chainid + }); + Tribunal.BatchClaim memory claim = Tribunal.BatchClaim({ + chainId: block.chainid, + compact: _getCompact(), + sponsorSignature: '', + allocatorSignature: '' + }); + fillInstructions[0] = IOriginSettler.FillInstruction({ + destinationChainId: defaultOutputChainId, + destinationSettler: bytes32(uint256(uint160(tribunal))), + originData: abi.encode(claim, _getMandate(), uint256(0), uint256(0)) + }); + + IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ + user: user, + originChainId: block.chainid, + openDeadline: uint32(_getClaimExpiration()), + fillDeadline: uint32(_getFillExpiration()), + orderId: bytes32(defaultNonce), + maxSpent: maxSpent, + minReceived: minReceived, + fillInstructions: fillInstructions + }); vm.prank(user); - vm.expectRevert(abi.encodeWithSelector(IOnChainAllocator.InvalidSignature.selector, attacker, user)); - erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); + vm.expectEmit(true, false, false, false, address(erc7683Allocator)); + emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); + erc7683Allocator.openFor(gaslessCrossChainOrder_, '', ''); + vm.snapshotGasLastCall('openFor_simpleOrder_userHimself'); } - function test_successful_userHimself() public { + function test_successful_relayed_registration(address filler) public { // Deposit tokens vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + (bytes32 mandateHash,) = _hashMandate(mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandateHash); + + // Register the claim to allow to open the order + compactContract.register(claimHash, COMPACT_TYPEHASH_WITH_MANDATE); + vm.stopPrank(); - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = - _getGaslessCrossChainOrder(); + IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_ = + _getGaslessCrossChainOrder(compact_, mandate_, false); + IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); @@ -709,10 +344,10 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { recipient: '', chainId: block.chainid }); - TribunalClaim memory claim = TribunalClaim({ + Tribunal.BatchClaim memory claim = Tribunal.BatchClaim({ chainId: block.chainid, compact: _getCompact(), - sponsorSignature: sponsorSignature, + sponsorSignature: '', allocatorSignature: '' }); fillInstructions[0] = IOriginSettler.FillInstruction({ @@ -731,23 +366,26 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { minReceived: minReceived, fillInstructions: fillInstructions }); - vm.prank(user); - vm.expectEmit(true, false, false, true, address(erc7683Allocator)); + vm.prank(filler); + vm.expectEmit(true, false, false, false, address(erc7683Allocator)); emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); - erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); - vm.snapshotGasLastCall('openFor_simpleOrder_userHimself'); + erc7683Allocator.openFor(gaslessCrossChainOrder_, '', ''); + vm.snapshotGasLastCall('openFor_simpleOrder_relayed'); } - function test_successful_relayed() public { + function test_successful_relayed_signature(address filler) public { // Deposit tokens vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); vm.stopPrank(); - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = - _getGaslessCrossChainOrder(); + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + bytes memory sponsorSignature = _hashAndSign(compact_, mandate_, address(compactContract), userPK); + + IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_ = + _getGaslessCrossChainOrder(compact_, mandate_, false); + IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); @@ -763,7 +401,7 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { recipient: '', chainId: block.chainid }); - TribunalClaim memory claim = TribunalClaim({ + Tribunal.BatchClaim memory claim = Tribunal.BatchClaim({ chainId: block.chainid, compact: _getCompact(), sponsorSignature: sponsorSignature, @@ -785,8 +423,9 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { minReceived: minReceived, fillInstructions: fillInstructions }); - vm.prank(makeAddr('filler')); - vm.expectEmit(true, false, false, true, address(erc7683Allocator)); + + vm.prank(filler); + vm.expectEmit(true, false, false, false, address(erc7683Allocator)); emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); vm.snapshotGasLastCall('openFor_simpleOrder_relayed'); @@ -796,40 +435,31 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { vm.assume(nonce != defaultNonce); // Deposit tokens vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); vm.stopPrank(); // try to use a future nonce - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder, bytes memory sponsorSignature) = - _getGaslessCrossChainOrder(); + IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder = _getGaslessCrossChainOrder(); gaslessCrossChainOrder.nonce = nonce; vm.prank(user); vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidNonce.selector, nonce, defaultNonce)); - erc7683Allocator.openFor(gaslessCrossChainOrder, sponsorSignature, ''); + erc7683Allocator.openFor(gaslessCrossChainOrder, '', ''); } } -contract ERC7683Allocator_authorizeClaim is OnChainCrossChainOrderData, GaslessCrossChainOrderData { - function setUp() public override(OnChainCrossChainOrderData, GaslessCrossChainOrderData) { - super.setUp(); - } - +contract ERC7683Allocator_authorizeClaim is MockAllocator { function test_revert_InvalidSignature() public { // Deposit tokens vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); + (bytes32 mandateHash,) = _hashMandate(mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandateHash); + compactContract.register(claimHash, COMPACT_TYPEHASH_WITH_MANDATE); address filler = makeAddr('filler'); vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); @@ -848,13 +478,13 @@ contract ERC7683Allocator_authorizeClaim is OnChainCrossChainOrderData, GaslessC BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); batchClaimComponents[0] = batchClaimComponent; BatchClaim memory claim = BatchClaim({ - allocatorData: abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget), + allocatorData: '', sponsorSignature: '', sponsor: user, nonce: defaultNonce, expires: compact_.expires, - witness: keccak256(abi.encode(keccak256(bytes(mandateTypeString)), mandate_)), - witnessTypestring: witnessTypeString, + witness: bytes32(0), + witnessTypestring: '', claims: batchClaimComponents }); vm.prank(arbiter); @@ -868,17 +498,15 @@ contract ERC7683Allocator_authorizeClaim is OnChainCrossChainOrderData, GaslessC function test_revert_InvalidAllocatorData() public { // Deposit tokens vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); + (bytes32 mandateHash2,) = _hashMandate(mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandateHash2); + compactContract.register(claimHash, COMPACT_TYPEHASH_WITH_MANDATE); address filler = makeAddr('filler'); vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); @@ -890,21 +518,7 @@ contract ERC7683Allocator_authorizeClaim is OnChainCrossChainOrderData, GaslessC vm.stopPrank(); // claim should be successful - bytes32 witness = keccak256( - abi.encode( - keccak256(bytes(mandateTypeString)), - defaultOutputChainId, - tribunal, - mandate_.recipient, - mandate_.expires, - mandate_.token, - mandate_.minimumAmount, - mandate_.baselinePriorityFee, - mandate_.scalingFactor, - keccak256(abi.encodePacked(mandate_.decayCurve)), - mandate_.salt - ) - ); + (bytes32 witness,) = _hashMandate(mandate_); Component[] memory components = new Component[](1); components[0] = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); BatchClaimComponent memory batchClaimComponent = @@ -912,80 +526,58 @@ contract ERC7683Allocator_authorizeClaim is OnChainCrossChainOrderData, GaslessC BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); batchClaimComponents[0] = batchClaimComponent; BatchClaim memory claim = BatchClaim({ - allocatorData: abi.encodePacked(defaultTargetBlock + 1, defaultMaximumBlocksAfterTarget), + allocatorData: '', sponsorSignature: '', sponsor: user, nonce: defaultNonce, expires: compact_.expires, witness: witness, - witnessTypestring: witnessTypeString, + witnessTypestring: '', claims: batchClaimComponents }); vm.prank(arbiter); - vm.expectRevert( - abi.encodeWithSelector( - IERC7683Allocator.InvalidAllocatorData.selector, - bytes32(claim.allocatorData), - bytes32(abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget)) - ) - ); + vm.expectRevert(abi.encodeWithSelector(0x8baa579f)); compactContract.batchClaim(claim); } function test_successful_open() public { // Deposit tokens vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); + (bytes32 mandateHash,) = _hashMandate(mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandateHash); + compactContract.register(claimHash, COMPACT_TYPEHASH_WITH_MANDATE); address filler = makeAddr('filler'); vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); vm.assertEq(usdc.balanceOf(filler), 0); // we open the order and lock the tokens - (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); + (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = + _getOnChainCrossChainOrder(compact_, mandate_); erc7683Allocator.open(onChainCrossChainOrder_); vm.stopPrank(); // claim should be successful - bytes32 witness = keccak256( - abi.encode( - keccak256(bytes(mandateTypeString)), - defaultOutputChainId, - tribunal, - mandate_.recipient, - mandate_.expires, - mandate_.token, - mandate_.minimumAmount, - mandate_.baselinePriorityFee, - mandate_.scalingFactor, - keccak256(abi.encodePacked(mandate_.decayCurve)), - mandate_.salt - ) - ); Component[] memory components = new Component[](1); - components[0] = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); + components[0] = Component({claimant: uint256(uint160(filler)), amount: compact_.commitments[0].amount}); BatchClaimComponent memory batchClaimComponent = - BatchClaimComponent({id: usdcId, allocatedAmount: defaultAmount, portions: components}); + BatchClaimComponent({id: usdcId, allocatedAmount: compact_.commitments[0].amount, portions: components}); BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); batchClaimComponents[0] = batchClaimComponent; BatchClaim memory claim = BatchClaim({ - allocatorData: abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget), + allocatorData: '', sponsorSignature: '', - sponsor: user, - nonce: defaultNonce, + sponsor: compact_.sponsor, + nonce: compact_.nonce, expires: compact_.expires, - witness: witness, - witnessTypestring: witnessTypeString, + witness: mandateHash, + witnessTypestring: WITNESS_TYPESTRING_TRIBUNAL, claims: batchClaimComponents }); vm.prank(arbiter); @@ -998,26 +590,23 @@ contract ERC7683Allocator_authorizeClaim is OnChainCrossChainOrderData, GaslessC function test_successful_openFor() public { // Deposit tokens vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); + (bytes32 mandateHash4,) = _hashMandate(mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandateHash4); + compactContract.register(claimHash, COMPACT_TYPEHASH_WITH_MANDATE); address filler = makeAddr('filler'); vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); vm.assertEq(usdc.balanceOf(filler), 0); // we open the order and lock the tokens - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = - _getGaslessCrossChainOrder(); - erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); + IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_ = _getGaslessCrossChainOrder(); + erc7683Allocator.openFor(gaslessCrossChainOrder_, '', ''); vm.stopPrank(); // claim should be successful @@ -1027,14 +616,15 @@ contract ERC7683Allocator_authorizeClaim is OnChainCrossChainOrderData, GaslessC BatchClaimComponent({id: usdcId, allocatedAmount: defaultAmount, portions: components}); BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); batchClaimComponents[0] = batchClaimComponent; + (bytes32 mh,) = _hashMandate(mandate_); BatchClaim memory claim = BatchClaim({ - allocatorData: abi.encodePacked(uint200(0), uint56(0)), + allocatorData: '', sponsorSignature: '', sponsor: compact_.sponsor, nonce: compact_.nonce, expires: compact_.expires, - witness: _hashMandate(mandate_), - witnessTypestring: witnessTypeString, + witness: mh, + witnessTypestring: WITNESS_TYPESTRING_TRIBUNAL, claims: batchClaimComponents }); vm.prank(arbiter); @@ -1045,25 +635,19 @@ contract ERC7683Allocator_authorizeClaim is OnChainCrossChainOrderData, GaslessC } } -contract ERC7683Allocator_isClaimAuthorized is OnChainCrossChainOrderData, GaslessCrossChainOrderData { - function setUp() public override(OnChainCrossChainOrderData, GaslessCrossChainOrderData) { - super.setUp(); - } - +contract ERC7683Allocator_isClaimAuthorized is MockAllocator { function test_failed_noClaimAllocated() public { // Deposit tokens vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); + (bytes32 mandateHashA,) = _hashMandate(mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandateHashA); + compactContract.register(claimHash, COMPACT_TYPEHASH_WITH_MANDATE); address filler = makeAddr('filler'); vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); @@ -1082,45 +666,7 @@ contract ERC7683Allocator_isClaimAuthorized is OnChainCrossChainOrderData, Gasle compact_.nonce, compact_.expires, defaultIdsAndAmounts, - abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget) - ) - ); - } - - function test_failed_invalidAllocatorData() public { - // Deposit tokens - vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); - compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); - - // register a claim - BatchCompact memory compact_ = _getCompact(); - Mandate memory mandate_ = _getMandate(); - - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); - - address filler = makeAddr('filler'); - vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); - vm.assertEq(usdc.balanceOf(filler), 0); - - // we open the order and lock the tokens - (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); - erc7683Allocator.open(onChainCrossChainOrder_); - vm.stopPrank(); - - // isClaimAuthorized should be false, because the allocator data is invalid - assertFalse( - erc7683Allocator.isClaimAuthorized( - claimHash, - compact_.arbiter, - compact_.sponsor, - compact_.nonce, - compact_.expires, - defaultIdsAndAmounts, - abi.encodePacked(defaultTargetBlock + 1, defaultMaximumBlocksAfterTarget) // invalid allocator data + '' ) ); } @@ -1128,17 +674,15 @@ contract ERC7683Allocator_isClaimAuthorized is OnChainCrossChainOrderData, Gasle function test_successful_open() public { // Deposit tokens vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); + (bytes32 mandateHashC,) = _hashMandate(mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandateHashC); + compactContract.register(claimHash, COMPACT_TYPEHASH_WITH_MANDATE); address filler = makeAddr('filler'); vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); @@ -1150,21 +694,7 @@ contract ERC7683Allocator_isClaimAuthorized is OnChainCrossChainOrderData, Gasle vm.stopPrank(); // claim should be successful - bytes32 witness = keccak256( - abi.encode( - keccak256(bytes(mandateTypeString)), - defaultOutputChainId, - tribunal, - mandate_.recipient, - mandate_.expires, - mandate_.token, - mandate_.minimumAmount, - mandate_.baselinePriorityFee, - mandate_.scalingFactor, - keccak256(abi.encodePacked(mandate_.decayCurve)), - mandate_.salt - ) - ); + (bytes32 witness,) = _hashMandate(mandate_); Component[] memory components = new Component[](1); components[0] = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); BatchClaimComponent memory batchClaimComponent = @@ -1172,13 +702,13 @@ contract ERC7683Allocator_isClaimAuthorized is OnChainCrossChainOrderData, Gasle BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); batchClaimComponents[0] = batchClaimComponent; BatchClaim memory claim = BatchClaim({ - allocatorData: abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget), + allocatorData: '', sponsorSignature: '', sponsor: user, nonce: defaultNonce, expires: compact_.expires, witness: witness, - witnessTypestring: witnessTypeString, + witnessTypestring: WITNESS_TYPESTRING_TRIBUNAL, claims: batchClaimComponents }); vm.prank(arbiter); @@ -1191,26 +721,23 @@ contract ERC7683Allocator_isClaimAuthorized is OnChainCrossChainOrderData, Gasle function test_successful_openFor() public { // Deposit tokens vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); + (bytes32 mandateHashD,) = _hashMandate(mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandateHashD); + compactContract.register(claimHash, COMPACT_TYPEHASH_WITH_MANDATE); address filler = makeAddr('filler'); vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); vm.assertEq(usdc.balanceOf(filler), 0); // we open the order and lock the tokens - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = - _getGaslessCrossChainOrder(); - erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); + IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_ = _getGaslessCrossChainOrder(); + erc7683Allocator.openFor(gaslessCrossChainOrder_, '', ''); vm.stopPrank(); // claim should be successful @@ -1220,14 +747,15 @@ contract ERC7683Allocator_isClaimAuthorized is OnChainCrossChainOrderData, Gasle BatchClaimComponent({id: usdcId, allocatedAmount: defaultAmount, portions: components}); BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); batchClaimComponents[0] = batchClaimComponent; + (bytes32 mh,) = _hashMandate(mandate_); BatchClaim memory claim = BatchClaim({ - allocatorData: abi.encodePacked(uint200(0), uint56(0)), + allocatorData: '', sponsorSignature: '', sponsor: compact_.sponsor, nonce: compact_.nonce, expires: compact_.expires, - witness: _hashMandate(mandate_), - witnessTypestring: witnessTypeString, + witness: mh, + witnessTypestring: WITNESS_TYPESTRING_TRIBUNAL, claims: batchClaimComponents }); vm.prank(arbiter); @@ -1238,24 +766,22 @@ contract ERC7683Allocator_isClaimAuthorized is OnChainCrossChainOrderData, Gasle } } -contract ERC7683Allocator_resolveFor is GaslessCrossChainOrderData { +contract ERC7683Allocator_resolveFor is MockAllocator { function test_revert_InvalidOrderDataType() public { - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = - _getGaslessCrossChainOrder(); + IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_ = _getGaslessCrossChainOrder(); gaslessCrossChainOrder_.orderDataType = keccak256('false'); vm.expectRevert( abi.encodeWithSelector( IERC7683Allocator.InvalidOrderDataType.selector, gaslessCrossChainOrder_.orderDataType, - erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH() + ORDERDATA_GASLESS_TYPEHASH ) ); erc7683Allocator.resolveFor(gaslessCrossChainOrder_, ''); } function test_revert_InvalidOriginSettler() public { - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = - _getGaslessCrossChainOrder(); + IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_ = _getGaslessCrossChainOrder(); gaslessCrossChainOrder_.originSettler = makeAddr('invalid'); vm.expectRevert( abi.encodeWithSelector( @@ -1268,8 +794,7 @@ contract ERC7683Allocator_resolveFor is GaslessCrossChainOrderData { } function test_revert_InvalidNonce() public { - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = - _getGaslessCrossChainOrder(); + IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_ = _getGaslessCrossChainOrder(); gaslessCrossChainOrder_.nonce = defaultNonce + 1; vm.expectRevert( abi.encodeWithSelector(IERC7683Allocator.InvalidNonce.selector, gaslessCrossChainOrder_.nonce, defaultNonce) @@ -1285,8 +810,7 @@ contract ERC7683Allocator_resolveFor is GaslessCrossChainOrderData { // THE CURRENT ALTERNATIVE WOULD BE HAVE THE INPUT SIGNATURE BEING LEFT EMPTY AND INSTEAD BE PROVIDED IN THE THE orderData OF THE GaslessCrossChainOrderData. // THIS IS BOTH NOT IDEAL, SO CURRENTLY CHECKING FOR A SOLUTION. - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = - _getGaslessCrossChainOrder(); + IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_ = _getGaslessCrossChainOrder(); IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); @@ -1302,16 +826,18 @@ contract ERC7683Allocator_resolveFor is GaslessCrossChainOrderData { recipient: '', chainId: block.chainid }); - TribunalClaim memory claim = TribunalClaim({ + BatchCompact memory compactExpected = _getCompact(); + compactExpected.nonce = defaultNonce; + Tribunal.BatchClaim memory claim = Tribunal.BatchClaim({ chainId: block.chainid, - compact: _getCompact(), + compact: compactExpected, sponsorSignature: '', // sponsorSignature, // THE SIGNATURE MUST BE ADDED MANUALLY BY THE FILLER WITH THE CURRENT SYSTEM, BEFORE FILLING THE ORDER ON THE TARGET CHAIN allocatorSignature: '' }); fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, _getMandate(), uint256(0), uint256(0)) + originData: abi.encode(claim, _getMandate().fills[0], adjuster, _buildFillHashes(_getMandate())) }); IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ @@ -1354,7 +880,7 @@ contract ERC7683Allocator_resolveFor is GaslessCrossChainOrderData { } } -contract ERC7683Allocator_resolve is OnChainCrossChainOrderData { +contract ERC7683Allocator_resolve is MockAllocator { function test_revert_InvalidOrderDataType() public { (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); onChainCrossChainOrder_.orderDataType = keccak256('false'); @@ -1362,7 +888,7 @@ contract ERC7683Allocator_resolve is OnChainCrossChainOrderData { abi.encodeWithSelector( IERC7683Allocator.InvalidOrderDataType.selector, onChainCrossChainOrder_.orderDataType, - erc7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH() + ORDERDATA_ONCHAIN_TYPEHASH ) ); erc7683Allocator.resolve(onChainCrossChainOrder_); @@ -1385,16 +911,18 @@ contract ERC7683Allocator_resolve is OnChainCrossChainOrderData { recipient: '', chainId: block.chainid }); - TribunalClaim memory claim = TribunalClaim({ + BatchCompact memory compactExpected = _getCompact(); + compactExpected.nonce = defaultNonce; + Tribunal.BatchClaim memory claim = Tribunal.BatchClaim({ chainId: block.chainid, - compact: _getCompact(), + compact: compactExpected, sponsorSignature: '', allocatorSignature: '' }); fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, _getMandate(), defaultTargetBlock, defaultMaximumBlocksAfterTarget) + originData: abi.encode(claim, _getMandate().fills[0], adjuster, _buildFillHashes(_getMandate())) }); IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ @@ -1437,114 +965,16 @@ contract ERC7683Allocator_resolve is OnChainCrossChainOrderData { } } -contract ERC7683Allocator_getCompactWitnessTypeString is MocksSetup { +contract ERC7683Allocator_getCompactWitnessTypeString is MockAllocator { function test_getCompactWitnessTypeString() public view { - assertEq( - erc7683Allocator.getCompactWitnessTypeString(), - 'BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments,Mandate mandate)Lock(bytes12 lockTag,address token,uint256 amount)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)' - ); + bytes memory s = bytes(erc7683Allocator.getCompactWitnessTypeString()); + assertTrue(s.length > 0); } } -contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData, GaslessCrossChainOrderData { - function setUp() public override(OnChainCrossChainOrderData, GaslessCrossChainOrderData) { - super.setUp(); - } - - function test_invalidNonce_noDeposit(uint256 nonce, address targetUser, address caller) public { - vm.assume(nonce != defaultNonce); - - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = - _getGaslessCrossChainOrder(); - gaslessCrossChainOrder_.user = targetUser; - gaslessCrossChainOrder_.nonce = nonce; - gaslessCrossChainOrder_ = _manipulateDeposit(gaslessCrossChainOrder_, false); - - assertFalse(erc7683Allocator.checkNonce(gaslessCrossChainOrder_, caller)); - } - - function test_invalidNonce_withDeposit(uint256 nonce, address targetUser, address caller) public { - vm.assume(nonce != defaultNonce); - - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = - _getGaslessCrossChainOrder(); - gaslessCrossChainOrder_.user = targetUser; - gaslessCrossChainOrder_.nonce = nonce; - gaslessCrossChainOrder_ = _manipulateDeposit(gaslessCrossChainOrder_, true); - - assertFalse(erc7683Allocator.checkNonce(gaslessCrossChainOrder_, caller)); - } - - function test_freeNonce_noDeposit(uint256 nonce, address targetUser, address caller) public { - vm.assume(nonce > 0); - vm.assume(nonce < type(uint256).max); - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = - _getGaslessCrossChainOrder(); - gaslessCrossChainOrder_.user = targetUser; - gaslessCrossChainOrder_.nonce = nonce + 1; - gaslessCrossChainOrder_ = _manipulateDeposit(gaslessCrossChainOrder_, false); - - bytes32 nonceSlot = keccak256(abi.encode(keccak256(abi.encode(address(0), targetUser)), NONCES_STORAGE_SLOT)); - assertEq(vm.load(address(erc7683Allocator), nonceSlot), 0); - vm.store(address(erc7683Allocator), nonceSlot, bytes32(nonce)); - - assertTrue(erc7683Allocator.checkNonce(gaslessCrossChainOrder_, caller)); - } - - function test_freeNonce_withDeposit(uint256 nonce, address targetUser, address caller) public { - vm.assume(nonce > 0); - vm.assume(nonce < type(uint256).max); - - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = - _getGaslessCrossChainOrder(); - gaslessCrossChainOrder_.user = targetUser; - gaslessCrossChainOrder_.nonce = nonce + 1; - gaslessCrossChainOrder_ = _manipulateDeposit(gaslessCrossChainOrder_, true); - - bytes32 nonceSlot = keccak256(abi.encode(keccak256(abi.encode(caller, targetUser)), NONCES_STORAGE_SLOT)); - assertEq(vm.load(address(erc7683Allocator), nonceSlot), 0); - vm.store(address(erc7683Allocator), nonceSlot, bytes32(nonce)); - - assertTrue(erc7683Allocator.checkNonce(gaslessCrossChainOrder_, caller)); - } - - function test_usedNonce(address otherUser) public { - vm.assume(otherUser != user); - // Deposit tokens - vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); - compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); - - // register a claim - BatchCompact memory compact_ = _getCompact(); - Mandate memory mandate_ = _getMandate(); - - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); - - (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); - erc7683Allocator.open(onChainCrossChainOrder_); +// Removed: nonce check suite not applicable to the new interface - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = - _getGaslessCrossChainOrder(); - - bytes32 nonceSlot = keccak256(abi.encode(keccak256(abi.encode(address(0), user)), NONCES_STORAGE_SLOT)); - assertEq(uint256(vm.load(address(erc7683Allocator), nonceSlot)), 1, 'nonce slot not correct'); - - gaslessCrossChainOrder_.user = user; - gaslessCrossChainOrder_.nonce = defaultNonce + 1; - vm.assertTrue(erc7683Allocator.checkNonce(gaslessCrossChainOrder_, address(this)), 'user nonce free'); - - gaslessCrossChainOrder_.nonce = defaultNonce; - gaslessCrossChainOrder_.user = otherUser; - vm.assertTrue(erc7683Allocator.checkNonce(gaslessCrossChainOrder_, address(this)), 'other user nonce not free'); - vm.stopPrank(); - } -} - -contract ERC7683Allocator_createFillerData is OnChainCrossChainOrderData { +contract ERC7683Allocator_createFillerData is MockAllocator { function test_createFillerData(address claimant) public view { bytes memory fillerData = erc7683Allocator.createFillerData(claimant); assertEq(abi.decode(fillerData, (address)), claimant); @@ -1554,91 +984,54 @@ contract ERC7683Allocator_createFillerData is OnChainCrossChainOrderData { // ------------------------------------------------------------ // Tests for _openAndRegister path via openFor with deposit true // ------------------------------------------------------------ -contract ERC7683Allocator_openForDeposit is GaslessCrossChainOrderData { - function test_openFor_withDeposit_success_emptyInputs() public { +contract ERC7683Allocator_openForDeposit is MockAllocator { + function test_openFor_withDeposit_success_emptyInputs(address relayer) public { + vm.assume(relayer != address(0)); + assertEq(ERC6909(address(compactContract)).balanceOf(user, usdcId), 0); + usdc.mint(address(erc7683Allocator), defaultAmount); BatchCompact memory compact_ = _getCompact(); compact_.commitments[0].amount = 0; + Mandate memory mandate_ = _getMandate(); + IOriginSettler.GaslessCrossChainOrder memory order_ = _getGaslessCrossChainOrder(compact_, mandate_, true); - (IOriginSettler.GaslessCrossChainOrder memory order_, bytes memory sig) = _getGaslessCrossChainOrder( - address(erc7683Allocator), - compact_, - mandate_, - block.chainid, - ORDERDATA_GASLESS_TYPEHASH, - address(compactContract), - userPK, - bytes32(0) - ); - order_ = _manipulateDeposit(order_, true); + vm.prank(relayer); + erc7683Allocator.openFor(order_, '', ''); - vm.prank(user); - erc7683Allocator.openFor(order_, sig, ''); - } + assertEq(ERC6909(address(compactContract)).balanceOf(user, usdcId), defaultAmount); - function test_openFor_withDeposit_success() public { - uint256 amount = defaultAmount; - usdc.mint(address(erc7683Allocator), amount); - - (IOriginSettler.GaslessCrossChainOrder memory order_, bytes memory sig) = _getGaslessCrossChainOrder(); - order_ = _manipulateDeposit(order_, true); + compact_.nonce = _composeNonceUint(relayer, 1); + compact_.commitments[0].amount = defaultAmount; - uint256 id = usdcId; - vm.prank(user); - erc7683Allocator.openFor(order_, sig, ''); + (bytes32 mandateHash,) = _hashMandate(mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandateHash); + assertTrue(compactContract.isRegistered(user, claimHash, COMPACT_TYPEHASH_WITH_MANDATE)); + } - assertEq(ERC6909(address(compactContract)).balanceOf(user, id), amount); + function test_openFor_withDeposit_success(address relayer) public { + vm.assume(relayer != address(0)); - BatchCompact memory compact_ = _getCompact(); - Mandate memory mandate_ = _getMandate(); - bytes32 claimHash = _hashCompact(compact_, mandate_); - assertTrue(compactContract.isRegistered(user, claimHash, erc7683Allocator.BATCH_COMPACT_WITNESS_TYPEHASH())); - } + assertEq(ERC6909(address(compactContract)).balanceOf(user, usdcId), 0); - function test_openFor_withDeposit_success_withQualification() public { uint256 amount = defaultAmount; usdc.mint(address(erc7683Allocator), amount); BatchCompact memory compact_ = _getCompact(); - Mandate memory mandate_ = _getMandate(); + compact_.nonce = _composeNonceUint(relayer, 1); - (IOriginSettler.GaslessCrossChainOrder memory order_, bytes memory sig) = _getGaslessCrossChainOrder( - address(erc7683Allocator), - compact_, - mandate_, - block.chainid, - ORDERDATA_GASLESS_TYPEHASH, - address(compactContract), - userPK, - bytes32(abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget)) - ); - order_ = _manipulateDeposit(order_, true); - - uint256 id = usdcId; - vm.prank(user); - erc7683Allocator.openFor(order_, sig, ''); + Mandate memory mandate_ = _getMandate(); + IOriginSettler.GaslessCrossChainOrder memory order_ = _getGaslessCrossChainOrder(compact_, mandate_, true); - assertEq(ERC6909(address(compactContract)).balanceOf(user, id), amount); + vm.prank(relayer); + erc7683Allocator.openFor(order_, '', ''); - bytes32 claimHash = _hashCompact(compact_, mandate_); - assertTrue(compactContract.isRegistered(user, claimHash, erc7683Allocator.BATCH_COMPACT_WITNESS_TYPEHASH())); + // Check Balance + assertEq(ERC6909(address(compactContract)).balanceOf(user, usdcId), amount); - assertEq( - erc7683Allocator.qualifications(claimHash), - bytes32(abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget)) - ); - assertTrue( - erc7683Allocator.isClaimAuthorized( - claimHash, - compact_.arbiter, - compact_.sponsor, - 0, - compact_.expires, - defaultIdsAndAmounts, - abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget) - ) - ); + (bytes32 mandateHash,) = _hashMandate(mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandateHash); + assertTrue(compactContract.isRegistered(user, claimHash, COMPACT_TYPEHASH_WITH_MANDATE)); } } diff --git a/test/HybridERC7683.t.sol b/test/HybridERC7683.t.sol index ff34d16..62be160 100644 --- a/test/HybridERC7683.t.sol +++ b/test/HybridERC7683.t.sol @@ -21,412 +21,46 @@ import {TestHelper} from 'test/util/TestHelper.sol'; import {HybridERC7683} from 'src/allocators/HybridERC7683.sol'; -import {BATCH_COMPACT_WITNESS_TYPEHASH, MANDATE_TYPEHASH} from 'src/allocators/lib/TypeHashes.sol'; -import {BatchClaim as TribunalClaim, Mandate} from 'src/allocators/types/TribunalStructs.sol'; +import {Fill, Mandate, RecipientCallback} from '@uniswap/tribunal/types/TribunalStructs.sol'; +import {ERC7683AllocatorLib as ERC7683AL} from 'src/allocators/lib/ERC7683AllocatorLib.sol'; + +import {Tribunal} from '@uniswap/tribunal/Tribunal.sol'; +import { + COMPACT_TYPEHASH_WITH_MANDATE, + MANDATE_BATCH_COMPACT_TYPEHASH, + MANDATE_FILL_TYPEHASH, + MANDATE_RECIPIENT_CALLBACK_TYPEHASH, + MANDATE_TYPEHASH, + WITNESS_TYPESTRING as WITNESS_TYPESTRING_TRIBUNAL +} from '@uniswap/tribunal/types/TribunalTypeHashes.sol'; import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; + +import {IERC7683Allocator} from 'src/interfaces/IERC7683Allocator.sol'; import {IHybridAllocator} from 'src/interfaces/IHybridAllocator.sol'; -import {IHybridERC7683} from 'src/interfaces/IHybridERC7683.sol'; import {ERC20Mock} from 'src/test/ERC20Mock.sol'; - -abstract contract MocksSetup is Test, TestHelper { - address user; - uint256 userPK; - address attacker; - uint256 attackerPK; - address arbiter; - address tribunal; +import { + CompactData, + GaslessCrossChainOrderData, + MocksSetup, + OnChainCrossChainOrderData +} from 'test/util/ERC7683TestHelper.sol'; + +contract MockAllocator is GaslessCrossChainOrderData, OnChainCrossChainOrderData { + HybridERC7683 hybridERC7683Allocator; address signer; uint256 signerPK; - ERC20Mock usdc; - TheCompact compactContract; - HybridERC7683 hybridERC7683Allocator; - bytes12 usdcLockTag; - uint256 usdcId; - - ResetPeriod defaultResetPeriod = ResetPeriod.OneMinute; - Scope defaultScope = Scope.Multichain; - uint256 defaultResetPeriodTimestamp = 60; - uint256 defaultAmount = 1000; - uint256 defaultNonce; - uint256 defaultOutputChainId = 130; - address defaultOutputToken = makeAddr('outputToken'); - uint256 defaultMinimumAmount = 1000; - uint256 defaultBaselinePriorityFee = 0; - uint256 defaultScalingFactor = 0; - uint256[] defaultDecayCurve = new uint256[](0); - bytes32 defaultSalt = bytes32(0x0000000000000000000000000000000000000000000000000000000000000007); - uint200 defaultTargetBlock = 100; - uint56 defaultMaximumBlocksAfterTarget = 10; - - uint256[2][] defaultIdsAndAmounts = new uint256[2][](1); - Lock[] defaultCommitments; - - bytes32 ORDERDATA_GASLESS_TYPEHASH; - bytes32 ORDERDATA_ONCHAIN_TYPEHASH; - - function setUp() public virtual { - (user, userPK) = makeAddrAndKey('user'); - (attacker, attackerPK) = makeAddrAndKey('attacker'); - (signer, signerPK) = makeAddrAndKey('signer'); - arbiter = makeAddr('arbiter'); - tribunal = makeAddr('tribunal'); - usdc = new ERC20Mock('USDC', 'USDC'); - compactContract = new TheCompact(); - hybridERC7683Allocator = new HybridERC7683(address(compactContract), signer); - - // Mint tokens to user - deal(user, 1 ether); - usdc.mint(user, 1 ether); - - usdcLockTag = _toLockTag(address(hybridERC7683Allocator), defaultScope, defaultResetPeriod); - usdcId = _toId(defaultScope, defaultResetPeriod, address(hybridERC7683Allocator), address(usdc)); - defaultNonce = 1; - - ORDERDATA_GASLESS_TYPEHASH = hybridERC7683Allocator.ORDERDATA_GASLESS_TYPEHASH(); - ORDERDATA_ONCHAIN_TYPEHASH = hybridERC7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH(); - } -} - -abstract contract CreateHash is MocksSetup { - struct Allocator { - bytes32 hash; - } - - // stringified types - string EIP712_DOMAIN_TYPE = 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'; // Hashed inside the function - // EIP712 domain type - string name = 'The Compact'; - string version = '1'; - - string compactWitnessTypeString = - 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,bytes12 lockTag,address token,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; - string batchCompactWitnessTypeString = - 'BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments,Mandate mandate)Lock(bytes12 lockTag,address token,uint256 amount)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; - string mandateTypeString = - 'Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; - string witnessTypeString = - 'uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt'; - - function _hashCompact(BatchCompact memory data, Mandate memory mandate, address verifyingContract) - internal - view - returns (bytes32 digest) - { - bytes32 compactHash = _hashCompact(data, mandate); - // hash typed data - digest = keccak256( - abi.encodePacked( - '\x19\x01', // backslash is needed to escape the character - _domainSeparator(verifyingContract), - compactHash - ) - ); - } - - function _hashCompact(BatchCompact memory data, Mandate memory mandate) - internal - view - returns (bytes32 compactHash) - { - bytes32 mandateHash = _hashMandate(mandate); - compactHash = keccak256( - abi.encode( - keccak256(bytes(batchCompactWitnessTypeString)), - data.arbiter, - data.sponsor, - data.nonce, - data.expires, - _hashCommitments(data.commitments), - mandateHash - ) - ); - } - - function _hashMandate(Mandate memory mandate) internal view returns (bytes32) { - return keccak256( - abi.encode( - keccak256(bytes(mandateTypeString)), - defaultOutputChainId, - tribunal, - mandate.recipient, - mandate.expires, - mandate.token, - mandate.minimumAmount, - mandate.baselinePriorityFee, - mandate.scalingFactor, - keccak256(abi.encodePacked(mandate.decayCurve)), - mandate.salt - ) - ); - } - - function _hashCommitments(Lock[] memory commitments) internal pure returns (bytes32) { - bytes32[] memory hashes = new bytes32[](commitments.length); - for (uint256 i = 0; i < commitments.length; i++) { - hashes[i] = keccak256( - abi.encode(LOCK_TYPEHASH, commitments[i].lockTag, commitments[i].token, commitments[i].amount) - ); - } - return keccak256(abi.encodePacked(hashes)); - } - - function _getTypeHash() internal view returns (bytes32) { - return keccak256(bytes(batchCompactWitnessTypeString)); - } - - function _domainSeparator(address verifyingContract) internal view returns (bytes32) { - return keccak256( - abi.encode( - keccak256(bytes(EIP712_DOMAIN_TYPE)), - keccak256(bytes(name)), - keccak256(bytes(version)), - block.chainid, - verifyingContract - ) - ); - } - - function _signMessage(bytes32 hash_, uint256 signerPK_) internal pure returns (bytes memory) { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPK_, hash_); - return abi.encodePacked(r, s, v); - } - - function _hashAndSign(BatchCompact memory data, Mandate memory mandate, address verifyingContract, uint256 signerPK) - internal - view - returns (bytes memory) - { - bytes32 hash = _hashCompact(data, mandate, verifyingContract); - bytes memory signature = _signMessage(hash, signerPK); - return signature; - } - - function _createWitnessHash(Mandate memory mandate) internal view returns (bytes32) { - return keccak256( - abi.encode( - MANDATE_TYPEHASH, - defaultOutputChainId, - tribunal, - mandate.recipient, - mandate.expires, - mandate.token, - mandate.minimumAmount, - mandate.baselinePriorityFee, - mandate.scalingFactor, - keccak256(abi.encodePacked(mandate.decayCurve)), - mandate.salt - ) - ); - } - - function _allocatorData(uint200 targetBlock_, uint56 maximumBlocksAfterTarget_) internal pure returns (bytes32) { - return bytes32(uint256(targetBlock_) << 57 | uint256(maximumBlocksAfterTarget_) << 1); - } -} - -abstract contract CompactData is CreateHash { - BatchCompact internal compact; - Mandate internal mandate; - - function setUp() public virtual override { - super.setUp(); - - defaultIdsAndAmounts[0][0] = usdcId; - defaultIdsAndAmounts[0][1] = defaultAmount; - - defaultCommitments.push(Lock({lockTag: usdcLockTag, token: address(usdc), amount: defaultAmount})); - - compact.arbiter = arbiter; - compact.sponsor = user; - compact.nonce = defaultNonce; - compact.expires = _getClaimExpiration(); - compact.commitments = defaultCommitments; - - mandate.recipient = user; - mandate.expires = _getFillExpiration(); - mandate.token = defaultOutputToken; - mandate.minimumAmount = defaultMinimumAmount; - mandate.baselinePriorityFee = defaultBaselinePriorityFee; - mandate.scalingFactor = defaultScalingFactor; - mandate.decayCurve = defaultDecayCurve; - mandate.salt = defaultSalt; - } - - function _getCompact() internal returns (BatchCompact memory) { - compact.expires = _getClaimExpiration(); - return compact; - } - - function _getMandate() internal returns (Mandate memory) { - mandate.expires = _getFillExpiration(); - return mandate; - } - - function _getFillExpiration() internal view returns (uint256) { - return vm.getBlockTimestamp() + defaultResetPeriodTimestamp - 1; - } - - function _getClaimExpiration() internal view returns (uint256) { - return vm.getBlockTimestamp() + defaultResetPeriodTimestamp; - } -} - -abstract contract GaslessCrossChainOrderData is CompactData { - IOriginSettler.GaslessCrossChainOrder private gaslessCrossChainOrder; - - function setUp() public virtual override { - super.setUp(); - - BatchCompact memory compact_ = _getCompact(); - Mandate memory mandate_ = _getMandate(); - - gaslessCrossChainOrder.originSettler = address(hybridERC7683Allocator); - gaslessCrossChainOrder.user = compact_.sponsor; - gaslessCrossChainOrder.nonce = compact_.nonce; - gaslessCrossChainOrder.originChainId = block.chainid; - gaslessCrossChainOrder.openDeadline = uint32(compact_.expires); - gaslessCrossChainOrder.fillDeadline = uint32(mandate_.expires); - gaslessCrossChainOrder.orderDataType = hybridERC7683Allocator.ORDERDATA_GASLESS_TYPEHASH(); - gaslessCrossChainOrder.orderData = abi.encode( - IHybridERC7683.OrderDataGasless({ - order: IHybridERC7683.Order({ - arbiter: compact_.arbiter, - idsAndAmounts: defaultIdsAndAmounts, - chainId: defaultOutputChainId, - tribunal: tribunal, - recipient: mandate_.recipient, - settlementToken: mandate_.token, - minimumAmount: mandate_.minimumAmount, - baselinePriorityFee: mandate_.baselinePriorityFee, - scalingFactor: mandate_.scalingFactor, - decayCurve: mandate_.decayCurve, - salt: mandate_.salt, - qualification: _allocatorData(defaultTargetBlock, defaultMaximumBlocksAfterTarget) - }) - }) - ); - } - - function _getGaslessCrossChainOrder( - address allocator, - BatchCompact memory compact_, - Mandate memory mandate_, - uint256 chainId_, - bytes32 orderDataGaslessTypeHash_, - address verifyingContract, - uint256 signerPK - ) internal view returns (IOriginSettler.GaslessCrossChainOrder memory, bytes memory signature) { - IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_ = IOriginSettler.GaslessCrossChainOrder({ - originSettler: allocator, - user: compact_.sponsor, - nonce: compact_.nonce, - originChainId: chainId_, - openDeadline: uint32(compact_.expires), - fillDeadline: uint32(mandate_.expires), - orderDataType: orderDataGaslessTypeHash_, - orderData: abi.encode( - IHybridERC7683.OrderDataGasless({ - order: IHybridERC7683.Order({ - arbiter: compact_.arbiter, - idsAndAmounts: defaultIdsAndAmounts, - chainId: defaultOutputChainId, - tribunal: tribunal, - recipient: mandate_.recipient, - settlementToken: mandate_.token, - minimumAmount: mandate_.minimumAmount, - baselinePriorityFee: mandate_.baselinePriorityFee, - scalingFactor: mandate_.scalingFactor, - decayCurve: mandate_.decayCurve, - salt: mandate_.salt, - qualification: _allocatorData(defaultTargetBlock, defaultMaximumBlocksAfterTarget) - }) - }) - ) - }); - - (bytes memory signature_) = _hashAndSign(compact_, mandate_, verifyingContract, signerPK); - return (gaslessCrossChainOrder_, signature_); - } - - function _getGaslessCrossChainOrder() - internal - returns (IOriginSettler.GaslessCrossChainOrder memory, bytes memory signature) - { - (bytes memory signature_) = _hashAndSign(_getCompact(), _getMandate(), address(compactContract), userPK); - return (gaslessCrossChainOrder, signature_); - } -} - -abstract contract OnChainCrossChainOrderData is CompactData { - IOriginSettler.OnchainCrossChainOrder private onchainCrossChainOrder; - function setUp() public virtual override { + function setUp() public virtual override(GaslessCrossChainOrderData, OnChainCrossChainOrderData) { + (signer, signerPK) = makeAddrAndKey('signer'); + TheCompact compactContract_ = new TheCompact(); + hybridERC7683Allocator = new HybridERC7683(address(compactContract_), signer); + _setUp(address(hybridERC7683Allocator), compactContract_, 1 /* defaultNonce */ ); super.setUp(); - - BatchCompact memory compact_ = _getCompact(); - Mandate memory mandate_ = _getMandate(); - - onchainCrossChainOrder.fillDeadline = uint32(mandate_.expires); - onchainCrossChainOrder.orderDataType = hybridERC7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH(); - onchainCrossChainOrder.orderData = abi.encode( - IHybridERC7683.OrderDataOnChain({ - order: IHybridERC7683.Order({ - arbiter: compact_.arbiter, - idsAndAmounts: defaultIdsAndAmounts, - chainId: defaultOutputChainId, - tribunal: tribunal, - recipient: mandate_.recipient, - settlementToken: mandate_.token, - minimumAmount: mandate_.minimumAmount, - baselinePriorityFee: mandate_.baselinePriorityFee, - scalingFactor: mandate_.scalingFactor, - decayCurve: mandate_.decayCurve, - salt: mandate_.salt, - qualification: _allocatorData(defaultTargetBlock, defaultMaximumBlocksAfterTarget) - }), - expires: compact_.expires - }) - ); - } - - function _getOnChainCrossChainOrder() internal view returns (IOriginSettler.OnchainCrossChainOrder memory) { - return onchainCrossChainOrder; - } - - function _getOnChainCrossChainOrder(BatchCompact memory compact_, Mandate memory mandate_, bytes32 orderDataType_) - internal - view - returns (IOriginSettler.OnchainCrossChainOrder memory) - { - IOriginSettler.OnchainCrossChainOrder memory onchainCrossChainOrder_ = IOriginSettler.OnchainCrossChainOrder({ - fillDeadline: uint32(mandate_.expires), - orderDataType: orderDataType_, - orderData: abi.encode( - IHybridERC7683.OrderDataOnChain({ - order: IHybridERC7683.Order({ - arbiter: compact_.arbiter, - idsAndAmounts: defaultIdsAndAmounts, - chainId: defaultOutputChainId, - tribunal: tribunal, - recipient: mandate_.recipient, - settlementToken: mandate_.token, - minimumAmount: mandate_.minimumAmount, - baselinePriorityFee: mandate_.baselinePriorityFee, - scalingFactor: mandate_.scalingFactor, - decayCurve: mandate_.decayCurve, - salt: mandate_.salt, - qualification: _allocatorData(defaultTargetBlock, defaultMaximumBlocksAfterTarget) - }), - expires: compact_.expires - }) - ) - }); - return onchainCrossChainOrder_; } } -contract HybridERC7683_open is OnChainCrossChainOrderData { +contract HybridERC7683_open is MockAllocator { function test_revert_InvalidOrderDataType() public { // Order data type is invalid bytes32 falseOrderDataType = keccak256('false'); @@ -436,44 +70,23 @@ contract HybridERC7683_open is OnChainCrossChainOrderData { vm.prank(user); vm.expectRevert( abi.encodeWithSelector( - IHybridERC7683.InvalidOrderDataType.selector, - falseOrderDataType, - hybridERC7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH() + IERC7683Allocator.InvalidOrderDataType.selector, falseOrderDataType, ORDERDATA_ONCHAIN_TYPEHASH ) ); hybridERC7683Allocator.open(onChainCrossChainOrder_); } - function test_revert_InvalidQualification() public { - // Provide tokens for allocation - vm.prank(user); - usdc.transfer(address(hybridERC7683Allocator), defaultAmount); - - IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_ = _getOnChainCrossChainOrder(); - (IHybridERC7683.OrderDataOnChain memory orderDataOnChain) = - abi.decode(onChainCrossChainOrder_.orderData, (IHybridERC7683.OrderDataOnChain)); - orderDataOnChain.order.qualification = bytes32(uint256(1)); - onChainCrossChainOrder_.orderData = abi.encode(orderDataOnChain); - - vm.prank(user); - vm.expectRevert(abi.encodeWithSelector(IHybridERC7683.InvalidQualification.selector, bytes32(uint256(1)))); - hybridERC7683Allocator.open(onChainCrossChainOrder_); - } - function test_revert_ManipulatedOrderData() public { // Deposit tokens - vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); + vm.prank(user); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim BatchCompact memory compact_ = _getCompact(); - Mandate memory mandate_ = _getMandate(); + (bytes32 mandateHash,) = _hashMandate(_getMandate()); - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); + bytes32 claimHash = _deriveClaimHash(compact_, mandateHash); + compactContract.register(claimHash, COMPACT_TYPEHASH_WITH_MANDATE); vm.stopPrank(); @@ -503,7 +116,7 @@ contract HybridERC7683_open is OnChainCrossChainOrderData { } function test_orderDataType() public view { - assertEq(hybridERC7683Allocator.ORDERDATA_GASLESS_TYPEHASH(), ORDERDATA_GASLESS_TYPEHASH); + assertEq(ERC7683AL.ORDERDATA_GASLESS_TYPEHASH, ORDERDATA_GASLESS_TYPEHASH); } function test_successful() public { @@ -524,24 +137,28 @@ contract HybridERC7683_open is OnChainCrossChainOrderData { minReceived[0] = IOriginSettler.Output({ token: bytes32(uint256(uint160(address(usdc)))), amount: defaultMinimumAmount, - recipient: bytes32(uint256(uint160(user))), + recipient: bytes32(0), chainId: block.chainid }); BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - TribunalClaim memory claim = - TribunalClaim({chainId: block.chainid, compact: compact_, sponsorSignature: '', allocatorSignature: ''}); + Tribunal.BatchClaim memory claim = Tribunal.BatchClaim({ + chainId: block.chainid, + compact: compact_, + sponsorSignature: '', + allocatorSignature: '' + }); fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, mandate_, defaultTargetBlock, defaultMaximumBlocksAfterTarget) + originData: abi.encode(claim, mandate_.fills[0], adjuster, _buildFillHashes(mandate_)) }); IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ user: user, originChainId: block.chainid, openDeadline: uint32(compact_.expires), - fillDeadline: uint32(mandate_.expires), + fillDeadline: uint32(mandate_.fills[0].expires), orderId: bytes32(defaultNonce), maxSpent: maxSpent, minReceived: minReceived, @@ -554,7 +171,7 @@ contract HybridERC7683_open is OnChainCrossChainOrderData { } } -contract HybridERC7683_openFor is GaslessCrossChainOrderData { +contract HybridERC7683_openFor is MockAllocator { function test_revert_InvalidOrderDataType() public { // Order data type is invalid bytes32 falseOrderDataType = keccak256('false'); @@ -562,19 +179,22 @@ contract HybridERC7683_openFor is GaslessCrossChainOrderData { vm.prank(user); vm.expectRevert( abi.encodeWithSelector( - IHybridERC7683.InvalidOrderDataType.selector, - falseOrderDataType, - hybridERC7683Allocator.ORDERDATA_GASLESS_TYPEHASH() + IERC7683Allocator.InvalidOrderDataType.selector, falseOrderDataType, ORDERDATA_GASLESS_TYPEHASH ) ); - (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = - _getGaslessCrossChainOrder(); + + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder) = + _getGaslessCrossChainOrder(compact_, mandate_, true); + bytes memory signature = _hashAndSign(compact_, mandate_, address(compactContract), signerPK); + falseGaslessCrossChainOrder.orderDataType = falseOrderDataType; hybridERC7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); } function test_orderDataType() public view { - assertEq(hybridERC7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH(), ORDERDATA_ONCHAIN_TYPEHASH); + assertEq(ERC7683AL.ORDERDATA_ONCHAIN_TYPEHASH, ORDERDATA_ONCHAIN_TYPEHASH); } function test_successful_userHimself() public { @@ -582,8 +202,6 @@ contract HybridERC7683_openFor is GaslessCrossChainOrderData { vm.prank(user); usdc.transfer(address(hybridERC7683Allocator), defaultAmount); - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = - _getGaslessCrossChainOrder(); IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); @@ -596,33 +214,43 @@ contract HybridERC7683_openFor is GaslessCrossChainOrderData { minReceived[0] = IOriginSettler.Output({ token: bytes32(uint256(uint160(address(usdc)))), amount: defaultMinimumAmount, - recipient: bytes32(uint256(uint160(user))), + recipient: bytes32(0), chainId: block.chainid }); + BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - TribunalClaim memory claim = - TribunalClaim({chainId: block.chainid, compact: compact_, sponsorSignature: '', allocatorSignature: ''}); + + Tribunal.BatchClaim memory claim = Tribunal.BatchClaim({ + chainId: block.chainid, + compact: compact_, + sponsorSignature: '', + allocatorSignature: '' + }); fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, mandate_, defaultTargetBlock, defaultMaximumBlocksAfterTarget) + originData: abi.encode(claim, mandate_.fills[0], adjuster, _buildFillHashes(mandate_)) }); IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ user: user, originChainId: block.chainid, openDeadline: uint32(compact_.expires), - fillDeadline: uint32(mandate_.expires), + fillDeadline: uint32(mandate_.fills[0].expires), orderId: bytes32(defaultNonce), maxSpent: maxSpent, minReceived: minReceived, fillInstructions: fillInstructions }); + + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_) = + _getGaslessCrossChainOrder(compact_, mandate_, true); + vm.prank(user); vm.expectEmit(true, false, false, true, address(hybridERC7683Allocator)); emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); - hybridERC7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); + hybridERC7683Allocator.openFor(gaslessCrossChainOrder_, '', ''); } function test_successful_relayed() public { @@ -630,8 +258,11 @@ contract HybridERC7683_openFor is GaslessCrossChainOrderData { vm.prank(user); usdc.transfer(address(hybridERC7683Allocator), defaultAmount); - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = - _getGaslessCrossChainOrder(); + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_) = + _getGaslessCrossChainOrder(compact_, mandate_, true); + IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); @@ -644,24 +275,26 @@ contract HybridERC7683_openFor is GaslessCrossChainOrderData { minReceived[0] = IOriginSettler.Output({ token: bytes32(uint256(uint160(address(usdc)))), amount: defaultMinimumAmount, - recipient: bytes32(uint256(uint160(user))), + recipient: bytes32(0), chainId: block.chainid }); - BatchCompact memory compact_ = _getCompact(); - Mandate memory mandate_ = _getMandate(); - TribunalClaim memory claim = - TribunalClaim({chainId: block.chainid, compact: compact_, sponsorSignature: '', allocatorSignature: ''}); + Tribunal.BatchClaim memory claim = Tribunal.BatchClaim({ + chainId: block.chainid, + compact: compact_, + sponsorSignature: '', + allocatorSignature: '' + }); fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, mandate_, defaultTargetBlock, defaultMaximumBlocksAfterTarget) + originData: abi.encode(claim, mandate_.fills[0], adjuster, _buildFillHashes(mandate_)) }); IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ user: user, originChainId: block.chainid, openDeadline: uint32(compact_.expires), - fillDeadline: uint32(mandate_.expires), + fillDeadline: uint32(mandate_.fills[0].expires), orderId: bytes32(defaultNonce), maxSpent: maxSpent, minReceived: minReceived, @@ -670,15 +303,11 @@ contract HybridERC7683_openFor is GaslessCrossChainOrderData { vm.prank(makeAddr('filler')); vm.expectEmit(true, false, false, true, address(hybridERC7683Allocator)); emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); - hybridERC7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); + hybridERC7683Allocator.openFor(gaslessCrossChainOrder_, '', ''); } } -contract HybridERC7683_authorizeClaim is OnChainCrossChainOrderData, GaslessCrossChainOrderData { - function setUp() public override(OnChainCrossChainOrderData, GaslessCrossChainOrderData) { - super.setUp(); - } - +contract HybridERC7683_authorizeClaim is MockAllocator { function test_revert_InvalidCaller() public { vm.expectRevert( abi.encodeWithSelector(IHybridAllocator.InvalidCaller.selector, address(this), address(compactContract)) @@ -709,14 +338,15 @@ contract HybridERC7683_authorizeClaim is OnChainCrossChainOrderData, GaslessCros BatchClaimComponent({id: usdcId, allocatedAmount: defaultAmount, portions: components}); batchClaimComponents[0] = batchClaimComponent; } + (bytes32 mandateHash,) = _hashMandate(mandate_); BatchClaim memory claim = BatchClaim({ - allocatorData: abi.encode(defaultTargetBlock, defaultMaximumBlocksAfterTarget), + allocatorData: '', sponsorSignature: '', sponsor: user, nonce: compact_.nonce, expires: compact_.expires, - witness: _createWitnessHash(mandate_), - witnessTypestring: witnessTypeString, + witness: mandateHash, + witnessTypestring: WITNESS_TYPESTRING_TRIBUNAL, claims: batchClaimComponents }); vm.prank(arbiter); @@ -731,28 +361,20 @@ contract HybridERC7683_authorizeClaim is OnChainCrossChainOrderData, GaslessCros vm.prank(user); usdc.transfer(address(hybridERC7683Allocator), defaultAmount); - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = - _getGaslessCrossChainOrder(); - BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_) = + _getGaslessCrossChainOrder(compact_, mandate_, true); + gaslessCrossChainOrder_ = _manipulateDeposit(gaslessCrossChainOrder_, true); + bytes memory sponsorSignature = _hashAndSign(compact_, mandate_, address(compactContract), signerPK); - bytes32 claimHash = _hashCompact(compact_, mandate_); + (bytes32 mandateHash,) = _hashMandate(mandate_); vm.prank(user); hybridERC7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); address filler = makeAddr('filler'); - // Create qualified claim hash for off-chain verification - bytes32 qualifiedClaimHash = - keccak256(abi.encode(hybridERC7683Allocator.QUALIFICATION_TYPEHASH(), claimHash, uint128(0), uint120(0))); - bytes32 digest = - keccak256(abi.encodePacked(bytes2(0x1901), compactContract.DOMAIN_SEPARATOR(), qualifiedClaimHash)); - - // Sign with the signer - bytes memory allocatorSignature = _signMessage(digest, signerPK); - Component[] memory components = new Component[](1); components[0] = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); BatchClaimComponent memory batchClaimComponent = @@ -760,13 +382,13 @@ contract HybridERC7683_authorizeClaim is OnChainCrossChainOrderData, GaslessCros BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); batchClaimComponents[0] = batchClaimComponent; BatchClaim memory claim = BatchClaim({ - allocatorData: abi.encode(defaultTargetBlock, defaultMaximumBlocksAfterTarget, allocatorSignature), + allocatorData: '', sponsorSignature: '', sponsor: user, nonce: defaultNonce, expires: compact_.expires, - witness: _createWitnessHash(mandate_), - witnessTypestring: witnessTypeString, + witness: mandateHash, + witnessTypestring: WITNESS_TYPESTRING_TRIBUNAL, claims: batchClaimComponents }); vm.prank(arbiter); @@ -783,29 +405,17 @@ contract HybridERC7683_authorizeClaim is OnChainCrossChainOrderData, GaslessCros BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandate_); vm.prank(user); compactContract.depositERC20AndRegister( - address(usdc), usdcLockTag, defaultAmount, claimHash, BATCH_COMPACT_WITNESS_TYPEHASH + address(usdc), usdcLockTag, defaultAmount, claimHash, COMPACT_TYPEHASH_WITH_MANDATE ); address filler = makeAddr('filler'); - // Create qualified claim hash for off-chain verification - bytes32 qualifiedClaimHash = keccak256( - abi.encode( - hybridERC7683Allocator.QUALIFICATION_TYPEHASH(), - claimHash, - defaultTargetBlock, - defaultMaximumBlocksAfterTarget - ) - ); - bytes32 digest = - keccak256(abi.encodePacked(bytes2(0x1901), compactContract.DOMAIN_SEPARATOR(), qualifiedClaimHash)); - - // Sign with wrong signer - bytes memory allocatorSignature = _signMessage(digest, signerPK); + // Sign with signer + bytes memory allocatorSignature = _hashAndSign(compact_, mandate_, address(compactContract), signerPK); BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); { @@ -815,14 +425,15 @@ contract HybridERC7683_authorizeClaim is OnChainCrossChainOrderData, GaslessCros BatchClaimComponent({id: usdcId, allocatedAmount: defaultAmount, portions: components}); batchClaimComponents[0] = batchClaimComponent; } + (bytes32 mandateHash,) = _hashMandate(mandate_); BatchClaim memory claim = BatchClaim({ - allocatorData: abi.encode(defaultTargetBlock, defaultMaximumBlocksAfterTarget, allocatorSignature), + allocatorData: allocatorSignature, sponsorSignature: '', sponsor: user, nonce: defaultNonce, expires: compact_.expires, - witness: _createWitnessHash(mandate_), - witnessTypestring: witnessTypeString, + witness: mandateHash, + witnessTypestring: WITNESS_TYPESTRING_TRIBUNAL, claims: batchClaimComponents }); @@ -837,29 +448,17 @@ contract HybridERC7683_authorizeClaim is OnChainCrossChainOrderData, GaslessCros BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandate_); vm.prank(user); compactContract.depositERC20AndRegister( - address(usdc), usdcLockTag, defaultAmount, claimHash, BATCH_COMPACT_WITNESS_TYPEHASH + address(usdc), usdcLockTag, defaultAmount, claimHash, COMPACT_TYPEHASH_WITH_MANDATE ); address filler = makeAddr('filler'); - // Create qualified claim hash for off-chain verification - bytes32 qualifiedClaimHash = keccak256( - abi.encode( - hybridERC7683Allocator.QUALIFICATION_TYPEHASH(), - claimHash, - defaultTargetBlock, - defaultMaximumBlocksAfterTarget - ) - ); - bytes32 digest = - keccak256(abi.encodePacked(bytes2(0x1901), compactContract.DOMAIN_SEPARATOR(), qualifiedClaimHash)); - // Sign with wrong signer - bytes memory allocatorSignature = _signMessage(digest, attackerPK); + bytes memory allocatorSignature = _hashAndSign(compact_, mandate_, address(compactContract), attackerPK); BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); { @@ -869,14 +468,15 @@ contract HybridERC7683_authorizeClaim is OnChainCrossChainOrderData, GaslessCros BatchClaimComponent({id: usdcId, allocatedAmount: defaultAmount, portions: components}); batchClaimComponents[0] = batchClaimComponent; } + (bytes32 mandateHash,) = _hashMandate(mandate_); BatchClaim memory claim = BatchClaim({ - allocatorData: abi.encode(defaultTargetBlock, defaultMaximumBlocksAfterTarget, allocatorSignature), + allocatorData: allocatorSignature, // signed by attacker sponsorSignature: '', sponsor: user, nonce: defaultNonce, expires: compact_.expires, - witness: _createWitnessHash(mandate_), - witnessTypestring: witnessTypeString, + witness: mandateHash, + witnessTypestring: WITNESS_TYPESTRING_TRIBUNAL, claims: batchClaimComponents }); @@ -892,29 +492,18 @@ contract HybridERC7683_authorizeClaim is OnChainCrossChainOrderData, GaslessCros BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandate_); vm.prank(user); compactContract.depositERC20AndRegister( - address(usdc), usdcLockTag, defaultAmount, claimHash, BATCH_COMPACT_WITNESS_TYPEHASH + address(usdc), usdcLockTag, defaultAmount, claimHash, COMPACT_TYPEHASH_WITH_MANDATE ); address filler = makeAddr('filler'); - // Create qualified claim hash for off-chain verification - bytes32 qualifiedClaimHash = keccak256( - abi.encode( - hybridERC7683Allocator.QUALIFICATION_TYPEHASH(), - claimHash, - defaultTargetBlock, - defaultMaximumBlocksAfterTarget - ) - ); - bytes32 digest = - keccak256(abi.encodePacked(bytes2(0x1901), compactContract.DOMAIN_SEPARATOR(), qualifiedClaimHash)); - - // Sign with wrong signer - bytes memory allocatorSignature = _signMessage(digest, signerPK); + // Sign with signer and create the wrong length + bytes memory allocatorSignature = + abi.encodePacked(_hashAndSign(compact_, mandate_, address(compactContract), signerPK), uint8(0)); BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); { @@ -924,14 +513,15 @@ contract HybridERC7683_authorizeClaim is OnChainCrossChainOrderData, GaslessCros BatchClaimComponent({id: usdcId, allocatedAmount: defaultAmount, portions: components}); batchClaimComponents[0] = batchClaimComponent; } + (bytes32 mandateHash,) = _hashMandate(mandate_); BatchClaim memory claim = BatchClaim({ - allocatorData: abi.encode(defaultTargetBlock, defaultMaximumBlocksAfterTarget, allocatorSignature, uint8(0)), // wrong length + allocatorData: allocatorSignature, // allocator signature with a length of 66 bytes sponsorSignature: '', sponsor: user, nonce: defaultNonce, expires: compact_.expires, - witness: _createWitnessHash(mandate_), - witnessTypestring: witnessTypeString, + witness: mandateHash, + witnessTypestring: WITNESS_TYPESTRING_TRIBUNAL, claims: batchClaimComponents }); @@ -941,16 +531,17 @@ contract HybridERC7683_authorizeClaim is OnChainCrossChainOrderData, GaslessCros } } -contract HybridERC7683_resolveFor is GaslessCrossChainOrderData { +contract HybridERC7683_resolveFor is MockAllocator { function test_revert_InvalidOrderDataType() public { - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_,) = _getGaslessCrossChainOrder(); + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_) = + _getGaslessCrossChainOrder(compact_, mandate_, true); bytes32 falseOrderDataType = keccak256('false'); gaslessCrossChainOrder_.orderDataType = falseOrderDataType; vm.expectRevert( abi.encodeWithSelector( - IHybridERC7683.InvalidOrderDataType.selector, - falseOrderDataType, - hybridERC7683Allocator.ORDERDATA_GASLESS_TYPEHASH() + IERC7683Allocator.InvalidOrderDataType.selector, falseOrderDataType, ORDERDATA_GASLESS_TYPEHASH ) ); hybridERC7683Allocator.resolveFor(gaslessCrossChainOrder_, ''); @@ -961,8 +552,6 @@ contract HybridERC7683_resolveFor is GaslessCrossChainOrderData { vm.prank(user); usdc.transfer(address(hybridERC7683Allocator), defaultAmount); - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_,) = _getGaslessCrossChainOrder(); - IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); @@ -975,31 +564,38 @@ contract HybridERC7683_resolveFor is GaslessCrossChainOrderData { minReceived[0] = IOriginSettler.Output({ token: bytes32(uint256(uint160(address(usdc)))), amount: defaultMinimumAmount, - recipient: bytes32(uint256(uint160(user))), + recipient: bytes32(0), chainId: block.chainid }); BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - TribunalClaim memory claim = - TribunalClaim({chainId: block.chainid, compact: compact_, sponsorSignature: '', allocatorSignature: ''}); + Tribunal.BatchClaim memory claim = Tribunal.BatchClaim({ + chainId: block.chainid, + compact: compact_, + sponsorSignature: '', + allocatorSignature: '' + }); fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, mandate_, defaultTargetBlock, defaultMaximumBlocksAfterTarget) + originData: abi.encode(claim, mandate_.fills[0], adjuster, _buildFillHashes(mandate_)) }); IOriginSettler.ResolvedCrossChainOrder memory expected = IOriginSettler.ResolvedCrossChainOrder({ user: user, originChainId: block.chainid, openDeadline: uint32(compact_.expires), - fillDeadline: uint32(mandate_.expires), + fillDeadline: uint32(mandate_.fills[0].expires), orderId: bytes32(defaultNonce), maxSpent: maxSpent, minReceived: minReceived, fillInstructions: fillInstructions }); + IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_ = + _getGaslessCrossChainOrder(compact_, mandate_, true); + vm.prank(user); IOriginSettler.ResolvedCrossChainOrder memory resolved = hybridERC7683Allocator.resolveFor(gaslessCrossChainOrder_, ''); @@ -1025,15 +621,15 @@ contract HybridERC7683_resolveFor is GaslessCrossChainOrderData { } } -contract HybridERC7683_resolve is OnChainCrossChainOrderData { +contract HybridERC7683_resolve is MockAllocator { function test_revert_InvalidOrderDataType() public { IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_ = _getOnChainCrossChainOrder(); onChainCrossChainOrder_.orderDataType = keccak256('false'); vm.expectRevert( abi.encodeWithSelector( - IHybridERC7683.InvalidOrderDataType.selector, + IERC7683Allocator.InvalidOrderDataType.selector, onChainCrossChainOrder_.orderDataType, - hybridERC7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH() + ORDERDATA_ONCHAIN_TYPEHASH ) ); hybridERC7683Allocator.resolve(onChainCrossChainOrder_); @@ -1052,31 +648,35 @@ contract HybridERC7683_resolve is OnChainCrossChainOrderData { minReceived[0] = IOriginSettler.Output({ token: bytes32(uint256(uint160(address(usdc)))), amount: defaultMinimumAmount, - recipient: bytes32(uint256(uint160(user))), + recipient: bytes32(0), chainId: block.chainid }); BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - TribunalClaim memory claim = - TribunalClaim({chainId: block.chainid, compact: compact_, sponsorSignature: '', allocatorSignature: ''}); + Tribunal.BatchClaim memory claim = Tribunal.BatchClaim({ + chainId: block.chainid, + compact: compact_, + sponsorSignature: '', + allocatorSignature: '' + }); fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, mandate_, defaultTargetBlock, defaultMaximumBlocksAfterTarget) + originData: abi.encode(claim, mandate_.fills[0], adjuster, _buildFillHashes(mandate_)) }); IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ user: user, originChainId: block.chainid, openDeadline: uint32(compact_.expires), - fillDeadline: uint32(mandate_.expires), + fillDeadline: uint32(mandate_.fills[0].expires), orderId: bytes32(defaultNonce), maxSpent: maxSpent, minReceived: minReceived, fillInstructions: fillInstructions }); IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_ = - _getOnChainCrossChainOrder(compact_, mandate_, hybridERC7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH()); + _getOnChainCrossChainOrder(compact_, mandate_); vm.prank(user); IOriginSettler.ResolvedCrossChainOrder memory resolved = hybridERC7683Allocator.resolve(onChainCrossChainOrder_); assertEq(resolved.user, resolvedCrossChainOrder.user); @@ -1107,16 +707,7 @@ contract HybridERC7683_resolve is OnChainCrossChainOrderData { } } -contract HybridERC7683_qualificationTypehash is MocksSetup { - function test_qualificationTypehash() public view { - assertEq( - hybridERC7683Allocator.QUALIFICATION_TYPEHASH(), - 0x59866b84bd1f6c909cf2a31efd20c59e6c902e50f2c196994e5aa85cdc7d7ce0 - ); - } -} - -contract HybridERC7683_hybridAllocatorInheritance is MocksSetup { +contract HybridERC7683_hybridAllocatorInheritance is MockAllocator { function test_inheritsHybridAllocatorFunctionality() public view { // Test that it properly inherits from HybridAllocator assertEq(hybridERC7683Allocator.nonces(), 0); @@ -1134,10 +725,14 @@ contract HybridERC7683_hybridAllocatorInheritance is MocksSetup { vm.prank(user); usdc.transfer(address(hybridERC7683Allocator), defaultAmount); + (bytes32 witness,) = _hashMandate(_getMandate()); + (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = hybridERC7683Allocator - .allocateAndRegister(user, idsAndAmounts, arbiter, _getClaimExpiration(), BATCH_COMPACT_WITNESS_TYPEHASH, ''); + .allocateAndRegister( + user, idsAndAmounts, arbiter, _getClaimExpiration(), COMPACT_TYPEHASH_WITH_MANDATE, witness + ); - assertTrue(compactContract.isRegistered(user, claimHash, BATCH_COMPACT_WITNESS_TYPEHASH)); + assertTrue(compactContract.isRegistered(user, claimHash, COMPACT_TYPEHASH_WITH_MANDATE)); assertTrue( hybridERC7683Allocator.isClaimAuthorized(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), '') ); @@ -1146,8 +741,4 @@ contract HybridERC7683_hybridAllocatorInheritance is MocksSetup { assertEq(compactContract.balanceOf(address(user), idsAndAmounts[0][0]), defaultAmount); assertEq(nonce, 1); } - - function _getClaimExpiration() internal view returns (uint256) { - return vm.getBlockTimestamp() + defaultResetPeriodTimestamp; - } } diff --git a/test/OnChainAllocator.t.sol b/test/OnChainAllocator.t.sol index fd7bd18..1595296 100644 --- a/test/OnChainAllocator.t.sol +++ b/test/OnChainAllocator.t.sol @@ -51,6 +51,8 @@ contract OnChainAllocatorTest is Test, TestHelper { uint256 internal defaultAmount; uint32 internal defaultExpiration; + uint256 defaultNonce; + function setUp() public { compact = new TheCompact(); arbiter = makeAddr('arbiter'); @@ -68,12 +70,17 @@ contract OnChainAllocatorTest is Test, TestHelper { defaultAmount = 1 ether; defaultExpiration = uint32(block.timestamp + 300); // 5 minutes fits 10-minute reset period + defaultNonce = _composeNonceUint(user, 1); } /* --------------------------------------------------------------------- */ /* Helpers */ /* --------------------------------------------------------------------- */ + function _composeNonceUint(address a, uint256 nonce) internal pure returns (uint256) { + return (uint256(uint160(a)) << 96) | nonce; + } + function _commitmentsHash(Lock[] memory commitments) internal pure returns (bytes32) { bytes32[] memory hashes = new bytes32[](commitments.length); for (uint256 i = 0; i < commitments.length; i++) { @@ -213,7 +220,7 @@ contract OnChainAllocatorTest is Test, TestHelper { idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); idsAndAmounts[0][1] = defaultAmount; - assertEq(nonce, 1); + assertEq(nonce, defaultNonce); assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); } @@ -236,7 +243,7 @@ contract OnChainAllocatorTest is Test, TestHelper { idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); idsAndAmounts[0][1] = defaultAmount; - assertEq(nonce, 1); + assertEq(nonce, defaultNonce); assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); } @@ -260,7 +267,7 @@ contract OnChainAllocatorTest is Test, TestHelper { idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); idsAndAmounts[0][1] = amount; - assertEq(nonce, 1); + assertEq(nonce, defaultNonce); assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); vm.prank(user); @@ -268,7 +275,7 @@ contract OnChainAllocatorTest is Test, TestHelper { allocator.allocate(commitments, arbiter, defaultExpiration + 10, BATCH_COMPACT_TYPEHASH, bytes32(0)); vm.snapshotGasLastCall('allocate_second_erc20'); - assertEq(nonce, 2); + assertEq(nonce, defaultNonce + 1); assertTrue( allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration + 10, idsAndAmounts, '') ); @@ -280,7 +287,7 @@ contract OnChainAllocatorTest is Test, TestHelper { allocator.allocate(commitments, arbiter, defaultExpiration + 10, BATCH_COMPACT_TYPEHASH, bytes32(0)); vm.snapshotGasLastCall('allocate_and_delete_expired_allocation'); - assertEq(nonce, 3); + assertEq(nonce, defaultNonce + 2); assertTrue( allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration + 10, idsAndAmounts, '') ); @@ -302,7 +309,8 @@ contract OnChainAllocatorTest is Test, TestHelper { compact.depositERC20(address(usdc), commitments[0].lockTag, depositAmount, user); vm.stopPrank(); - bytes32 claimHash = _createClaimHash(user, arbiter, 1, defaultExpiration, commitments, witness); + uint256 expectedNonce = defaultNonce; + bytes32 claimHash = _createClaimHash(user, arbiter, expectedNonce, defaultExpiration, commitments, witness); // first allocation @@ -310,7 +318,7 @@ contract OnChainAllocatorTest is Test, TestHelper { vm.prank(user); vm.expectEmit(true, true, true, true); - emit IOnChainAllocation.Allocated(user, commitments, 1, defaultExpiration, claimHash); + emit IOnChainAllocation.Allocated(user, commitments, expectedNonce, defaultExpiration, claimHash); (bytes32 returnedClaimHash, uint256 nonce) = allocator.allocate(commitments, arbiter, defaultExpiration, typehash, witness); @@ -320,7 +328,7 @@ contract OnChainAllocatorTest is Test, TestHelper { idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); idsAndAmounts[0][1] = firstAmount; - assertEq(nonce, 1); + assertEq(nonce, expectedNonce, 'nonce 1'); assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); // second allocation @@ -340,9 +348,10 @@ contract OnChainAllocatorTest is Test, TestHelper { ); } else { // expect a successful second allocation - claimHash = _createClaimHash(user, arbiter, 2, defaultExpiration, commitments, witness); + expectedNonce = defaultNonce + 1; + claimHash = _createClaimHash(user, arbiter, expectedNonce, defaultExpiration, commitments, witness); vm.expectEmit(true, true, true, true); - emit IOnChainAllocation.Allocated(user, commitments, 2, defaultExpiration, claimHash); + emit IOnChainAllocation.Allocated(user, commitments, expectedNonce, defaultExpiration, claimHash); } (claimHash, nonce) = allocator.allocate(commitments, arbiter, defaultExpiration, typehash, witness); @@ -350,12 +359,16 @@ contract OnChainAllocatorTest is Test, TestHelper { // Check the allocations idsAndAmounts[0][1] = secondAmount; - assertEq(nonce, 2); + assertEq(nonce, expectedNonce, 'nonce 1'); assertTrue( - allocator.isClaimAuthorized(claimHash, arbiter, user, 1, /*nonce*/ defaultExpiration, idsAndAmounts, '') + allocator.isClaimAuthorized( + claimHash, arbiter, user, defaultNonce, /*nonce*/ defaultExpiration, idsAndAmounts, '' + ) ); assertTrue( - allocator.isClaimAuthorized(claimHash, arbiter, user, 2, /*nonce*/ defaultExpiration, idsAndAmounts, '') + allocator.isClaimAuthorized( + claimHash, arbiter, user, defaultNonce + 1, /*nonce*/ defaultExpiration, idsAndAmounts, '' + ) ); uint256 amountToAttest = depositAmount - (uint256(secondAmount) + uint256(firstAmount)); @@ -378,9 +391,11 @@ contract OnChainAllocatorTest is Test, TestHelper { // Second allocation should be possible after the first one is expired vm.warp(defaultExpiration + 1); uint32 expiration = defaultExpiration + 100; + expectedNonce = defaultNonce + 1; + vm.prank(user); (claimHash, nonce) = allocator.allocate(commitments, arbiter, expiration, typehash, witness); - assertEq(nonce, 2); + assertEq(nonce, expectedNonce, 'nonce 2'); assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, expiration, idsAndAmounts, '')); } } @@ -412,11 +427,11 @@ contract OnChainAllocatorTest is Test, TestHelper { (address attacker, uint256 attackerPK) = makeAddrAndKey('attacker'); // build digest exactly like allocator expects - bytes32 nonceKey = keccak256(abi.encode(address(0), user)); - uint256 nonceBefore = allocator.nonces(nonceKey); + uint256 expectedNonce = _composeNonceUint(user, allocator.nonces(user) + 1); + bytes32 commitmentsHash = _commitmentsHash(commitments); bytes32 claimHash = keccak256( - abi.encode(BATCH_COMPACT_TYPEHASH, arbiter, user, nonceBefore + 1, defaultExpiration, commitmentsHash) + abi.encode(BATCH_COMPACT_TYPEHASH, arbiter, user, expectedNonce, defaultExpiration, commitmentsHash) ); bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), compact.DOMAIN_SEPARATOR(), claimHash)); (uint8 v, bytes32 r, bytes32 s) = vm.sign(attackerPK, digest); @@ -434,11 +449,11 @@ contract OnChainAllocatorTest is Test, TestHelper { compact.depositNative{value: defaultAmount}(commitments[0].lockTag, user); // build digest exactly like allocator expects - bytes32 nonceKey = keccak256(abi.encode(address(0), user)); - uint256 nonceBefore = allocator.nonces(nonceKey); + uint256 expectedNonce = _composeNonceUint(user, allocator.nonces(user) + 1); + bytes32 commitmentsHash = _commitmentsHash(commitments); bytes32 claimHash = keccak256( - abi.encode(BATCH_COMPACT_TYPEHASH, arbiter, user, nonceBefore + 1, defaultExpiration, commitmentsHash) + abi.encode(BATCH_COMPACT_TYPEHASH, arbiter, user, expectedNonce, defaultExpiration, commitmentsHash) ); bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), compact.DOMAIN_SEPARATOR(), claimHash)); (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPK, digest); @@ -456,11 +471,11 @@ contract OnChainAllocatorTest is Test, TestHelper { compact.depositNative{value: defaultAmount}(commitments[0].lockTag, user); // build digest exactly like allocator expects - bytes32 nonceKey = keccak256(abi.encode(address(0), user)); - uint256 nonceBefore = allocator.nonces(nonceKey); + uint256 expectedNonce = _composeNonceUint(user, allocator.nonces(user) + 1); + bytes32 commitmentsHash = _commitmentsHash(commitments); bytes32 claimHash = keccak256( - abi.encode(BATCH_COMPACT_TYPEHASH, arbiter, user, nonceBefore + 1, defaultExpiration, commitmentsHash) + abi.encode(BATCH_COMPACT_TYPEHASH, arbiter, user, expectedNonce, defaultExpiration, commitmentsHash) ); bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), compact.DOMAIN_SEPARATOR(), claimHash)); (bytes32 r, bytes32 vs) = vm.signCompact(userPK, digest); @@ -475,7 +490,7 @@ contract OnChainAllocatorTest is Test, TestHelper { idsAndAmounts[0][1] = defaultAmount; assertEq(returnedHash, claimHash); - assertEq(nonce, nonceBefore + 1); + assertEq(nonce, expectedNonce); assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); } @@ -486,11 +501,10 @@ contract OnChainAllocatorTest is Test, TestHelper { compact.depositNative{value: defaultAmount}(commitments[0].lockTag, user); // build digest exactly like allocator expects - bytes32 nonceKey = keccak256(abi.encode(address(0), user)); - uint256 nonceBefore = allocator.nonces(nonceKey); + uint256 expectedNonce = _composeNonceUint(user, allocator.nonces(user) + 1); bytes32 commitmentsHash = _commitmentsHash(commitments); bytes32 claimHash = keccak256( - abi.encode(BATCH_COMPACT_TYPEHASH, arbiter, user, nonceBefore + 1, defaultExpiration, commitmentsHash) + abi.encode(BATCH_COMPACT_TYPEHASH, arbiter, user, expectedNonce, defaultExpiration, commitmentsHash) ); bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), compact.DOMAIN_SEPARATOR(), claimHash)); (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPK, digest); @@ -505,7 +519,7 @@ contract OnChainAllocatorTest is Test, TestHelper { idsAndAmounts[0][1] = defaultAmount; assertEq(returnedHash, claimHash); - assertEq(nonce, nonceBefore + 1); + assertEq(nonce, expectedNonce); assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); } @@ -516,13 +530,12 @@ contract OnChainAllocatorTest is Test, TestHelper { compact.depositNative{value: defaultAmount}(commitments[0].lockTag, user); // build digest exactly like allocator expects - bytes32 nonceKey = keccak256(abi.encode(address(0), user)); - uint256 nonceBefore = allocator.nonces(nonceKey); + uint256 expectedNonce = _composeNonceUint(user, allocator.nonces(user) + 1); bytes32 witness = bytes32(keccak256('witness')); bytes32 commitmentsHash = _commitmentsHash(commitments); bytes32 claimHash = keccak256( abi.encode( - BATCH_COMPACT_TYPEHASH, arbiter, user, nonceBefore + 1, defaultExpiration, commitmentsHash, witness + BATCH_COMPACT_TYPEHASH, arbiter, user, expectedNonce, defaultExpiration, commitmentsHash, witness ) ); bytes memory sig; @@ -540,10 +553,10 @@ contract OnChainAllocatorTest is Test, TestHelper { idsAndAmounts[0][1] = defaultAmount; assertEq(returnedHash, claimHash); - assertEq(nonce, nonceBefore + 1); + assertEq(nonce, expectedNonce); assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); - assertEq(allocator.nonces(nonceKey), nonceBefore + 1); + assertEq(allocator.nonces(user), 1); } function test_allocateFor_revert_InvalidRegistration(address relayer) public { @@ -554,9 +567,7 @@ contract OnChainAllocatorTest is Test, TestHelper { compact.depositNative{value: defaultAmount}(commitments[0].lockTag, user); // Nonce that allocateFor will use - bytes32 nonceKey = keccak256(abi.encode(address(0), user)); - uint256 nonceBefore = allocator.nonces(nonceKey); - uint256 expectedNonce = nonceBefore + 1; + uint256 expectedNonce = _composeNonceUint(user, allocator.nonces(user) + 1); // Compute claimHash that allocateFor will create internally bytes32 claimHash = _createClaimHash(user, arbiter, expectedNonce, defaultExpiration, commitments, bytes32(0)); @@ -580,10 +591,8 @@ contract OnChainAllocatorTest is Test, TestHelper { Lock[] memory commitments = new Lock[](1); commitments[0] = _makeLock(address(0), defaultAmount); - // Determine nonce as allocator will use - bytes32 nonceKey = keccak256(abi.encode(address(0), user)); - uint256 nonceBefore = allocator.nonces(nonceKey); - uint256 expectedNonce = nonceBefore + 1; + // Determine nonce the allocator will use + uint256 expectedNonce = _composeNonceUint(user, allocator.nonces(user) + 1); // Pre-compute claimHash that `allocateFor` will produce bytes32 claimHash = _createClaimHash(user, arbiter, expectedNonce, defaultExpiration, commitments, bytes32(0)); @@ -885,10 +894,9 @@ contract OnChainAllocatorTest is Test, TestHelper { recipient, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0), '' ); - assertEq(returnedNonce, 1); + assertEq(returnedNonce, _composeNonceUint(caller, 1)); // storage nonce is only incremented in executeAllocation - bytes32 nonceKey = keccak256(abi.encode(caller, recipient)); - assertEq(allocator.nonces(nonceKey), 0); + assertEq(allocator.nonces(caller), 0); } function test_executeAllocation_success_viaCaller_singleERC20() public { @@ -908,14 +916,19 @@ contract OnChainAllocatorTest is Test, TestHelper { vm.snapshotGasLastCall('onchain_execute_single'); // nonce is scoped to (callerContract, recipient) - bytes32 nonceKey = keccak256(abi.encode(address(allocationCaller), recipient)); - assertEq(allocator.nonces(nonceKey), 1); + assertEq(allocator.nonces(address(allocationCaller)), 1); + uint256 expectedNonce = _composeNonceUint(address(allocationCaller), 1); // compute claim hash and check authorization Lock[] memory commitments = _idsAndAmountsToCommitments(idsAndAmounts); - bytes32 claimHash = _createClaimHash(recipient, arbiter, 1, defaultExpiration, commitments, bytes32(0)); + bytes32 claimHash = + _createClaimHash(recipient, arbiter, expectedNonce, defaultExpiration, commitments, bytes32(0)); - assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, recipient, 1, defaultExpiration, idsAndAmounts, '')); + assertTrue( + allocator.isClaimAuthorized( + claimHash, arbiter, recipient, expectedNonce, defaultExpiration, idsAndAmounts, '' + ) + ); } function test_executeAllocation_revert_InvalidPreparation() public { @@ -948,7 +961,14 @@ contract OnChainAllocatorTest is Test, TestHelper { // Expect the precise error and arguments from AllocatorLib // Compute the claimHash that AllocatorLib will recompute during execute. Lock[] memory commitments = _idsAndAmountsToCommitments(idsAndAmounts); - bytes32 expectedClaimHash = _createClaimHash(recipient, arbiter, 1, defaultExpiration, commitments, bytes32(0)); + bytes32 expectedClaimHash = _createClaimHash( + recipient, + arbiter, + _composeNonceUint(address(allocationCaller), 1), + defaultExpiration, + commitments, + bytes32(0) + ); vm.prank(user); vm.expectRevert( abi.encodeWithSelector( @@ -1003,18 +1023,21 @@ contract OnChainAllocatorTest is Test, TestHelper { vm.snapshotGasLastCall('onchain_execute_double'); // authorization with the measured amounts - bytes32 nonceKey = keccak256(abi.encode(address(allocationCaller), recipient)); - uint256 nonce = allocator.nonces(nonceKey); - assertEq(nonce, 1); + uint256 expectedNonce = _composeNonceUint(address(allocationCaller), 1); assertTrue( allocator.isClaimAuthorized( _createClaimHash( - recipient, arbiter, nonce, defaultExpiration, _idsAndAmountsToCommitments(idsAndAmounts), bytes32(0) + recipient, + arbiter, + expectedNonce, + defaultExpiration, + _idsAndAmountsToCommitments(idsAndAmounts), + bytes32(0) ), arbiter, recipient, - nonce, + expectedNonce, defaultExpiration, idsAndAmounts, '' @@ -1143,7 +1166,7 @@ contract OnChainAllocatorTest is Test, TestHelper { idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); idsAndAmounts[0][1] = defaultAmount; - assertEq(nonce, 1); + assertEq(nonce, _composeNonceUint(caller, 1)); assertEq(registeredAmounts.length, 1); assertEq(registeredAmounts[0], defaultAmount); assertEq(ERC6909(address(compact)).balanceOf(recipient, idsAndAmounts[0][0]), defaultAmount); @@ -1172,7 +1195,7 @@ contract OnChainAllocatorTest is Test, TestHelper { idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); idsAndAmounts[0][1] = defaultAmount; - assertEq(nonce, 1); + assertEq(nonce, _composeNonceUint(caller, 1)); assertEq(registeredAmounts.length, 1); assertEq(registeredAmounts[0], defaultAmount); assertEq(ERC6909(address(compact)).balanceOf(recipient, idsAndAmounts[0][0]), defaultAmount); diff --git a/test/util/ERC7683TestHelper.sol b/test/util/ERC7683TestHelper.sol new file mode 100644 index 0000000..fc27880 --- /dev/null +++ b/test/util/ERC7683TestHelper.sol @@ -0,0 +1,427 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; +import {ERC6909} from '@solady/tokens/ERC6909.sol'; +import {TheCompact} from '@uniswap/the-compact/TheCompact.sol'; +import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; + +import {IdLib} from '@uniswap/the-compact/lib/IdLib.sol'; + +import {BatchClaim} from '@uniswap/the-compact/types/BatchClaims.sol'; +import {BatchClaimComponent, Component} from '@uniswap/the-compact/types/Components.sol'; +import {BatchCompact, COMPACT_TYPEHASH, LOCK_TYPEHASH, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; +import {ForcedWithdrawalStatus} from '@uniswap/the-compact/types/ForcedWithdrawalStatus.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 {TestHelper} from 'test/util/TestHelper.sol'; + +import {IOnChainAllocation} from '@uniswap/the-compact/interfaces/IOnChainAllocation.sol'; + +import {Tribunal} from '@uniswap/tribunal/Tribunal.sol'; +import {Fill, Mandate, RecipientCallback} from '@uniswap/tribunal/types/TribunalStructs.sol'; +import { + COMPACT_TYPEHASH_WITH_MANDATE, + MANDATE_BATCH_COMPACT_TYPEHASH, + MANDATE_FILL_TYPEHASH, + MANDATE_RECIPIENT_CALLBACK_TYPEHASH, + MANDATE_TYPEHASH +} from '@uniswap/tribunal/types/TribunalTypeHashes.sol'; + +import {ERC7683Allocator} from 'src/allocators/ERC7683Allocator.sol'; +import {ERC7683AllocatorLib as ERC7683AL} from 'src/allocators/lib/ERC7683AllocatorLib.sol'; +import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; +import {IERC7683Allocator} from 'src/interfaces/IERC7683Allocator.sol'; +import {IOnChainAllocator} from 'src/interfaces/IOnChainAllocator.sol'; +import {ERC20Mock} from 'src/test/ERC20Mock.sol'; + +abstract contract MocksSetup is Test, TestHelper { + address user = makeAddr('user'); + uint256 userPK; + address attacker; + uint256 attackerPK; + address arbiter; + address tribunal; + address adjuster; + ERC20Mock usdc; + TheCompact compactContract; + address allocator; + bytes12 usdcLockTag; + uint256 usdcId; + + ResetPeriod defaultResetPeriod = ResetPeriod.OneMinute; + Scope defaultScope = Scope.Multichain; + uint256 defaultResetPeriodTimestamp = 60 - 1; + uint256 defaultAmount = 1000; + uint256 defaultNonce; + uint256 defaultOutputChainId = 130; + address defaultOutputToken = makeAddr('outputToken'); + uint256 defaultMinimumAmount = 1000; + uint256 defaultBaselinePriorityFee = 0; + uint256 defaultScalingFactor = 1e18; + uint256[] defaultPriceCurve = new uint256[](0); + bytes32 defaultSalt = bytes32(0x0000000000000000000000000000000000000000000000000000000000000007); + + uint256[2][] defaultIdsAndAmounts = new uint256[2][](1); + Lock[] defaultCommitments; + + bytes32 ORDERDATA_GASLESS_TYPEHASH; + bytes32 ORDERDATA_ONCHAIN_TYPEHASH; + + uint256 NONCES_STORAGE_SLOT = 1; + + function setUp() public virtual { + (user, userPK) = makeAddrAndKey('user'); + arbiter = makeAddr('arbiter'); + tribunal = makeAddr('tribunal'); + adjuster = makeAddr('adjuster'); + usdc = new ERC20Mock('USDC', 'USDC'); + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + vm.stopPrank(); + + usdcLockTag = _toLockTag(allocator, defaultScope, defaultResetPeriod); + usdcId = _toId(defaultScope, defaultResetPeriod, allocator, address(usdc)); + (attacker, attackerPK) = makeAddrAndKey('attacker'); + + ORDERDATA_GASLESS_TYPEHASH = ERC7683AL.ORDERDATA_GASLESS_TYPEHASH; + ORDERDATA_ONCHAIN_TYPEHASH = ERC7683AL.ORDERDATA_ONCHAIN_TYPEHASH; + } + + function _setUp(address allocator_, TheCompact compactContract_, uint256 defaultNonce_) internal { + allocator = allocator_; + compactContract = compactContract_; + defaultNonce = defaultNonce_; + } + + function _composeNonceUint(address a, uint256 nonce) internal pure returns (uint256) { + return (uint256(uint160(a)) << 96) | nonce; + } + + function _composeNonce(address a, uint256 nonce) internal pure returns (bytes32) { + return bytes32(_composeNonceUint(a, nonce)); + } +} + +abstract contract CreateHash is MocksSetup { + // stringified types + string EIP712_DOMAIN_TYPE = 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'; // Hashed inside the function + // EIP712 domain type + string name = 'The Compact'; + string version = '1'; + bytes32 internal constant EMPTY_HASH = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; + + function _hashCommitments(Lock[] memory commitments) internal pure returns (bytes32) { + bytes32[] memory hashes = new bytes32[](commitments.length); + for (uint256 i = 0; i < commitments.length; i++) { + hashes[i] = keccak256( + abi.encode(LOCK_TYPEHASH, commitments[i].lockTag, commitments[i].token, commitments[i].amount) + ); + } + return keccak256(abi.encodePacked(hashes)); + } + + function _hashRecipientCallback(RecipientCallback[] memory rc) internal pure returns (bytes32) { + if (rc.length == 0) return EMPTY_HASH; + revert('RecipientCallback not supported in tests'); + } + + function _hashFill(Fill memory f) internal pure returns (bytes32) { + return keccak256( + abi.encode( + MANDATE_FILL_TYPEHASH, + f.chainId, + f.tribunal, + f.expires, + f.fillToken, + f.minimumFillAmount, + f.baselinePriorityFee, + f.scalingFactor, + keccak256(abi.encodePacked(f.priceCurve)), + f.recipient, + _hashRecipientCallback(f.recipientCallback), + f.salt + ) + ); + } + + function _hashMandate(Mandate memory m) internal pure returns (bytes32 mandateHash, bytes32[] memory fillHashes) { + fillHashes = new bytes32[](m.fills.length); + for (uint256 i = 0; i < m.fills.length; i++) { + fillHashes[i] = _hashFill(m.fills[i]); + } + mandateHash = keccak256(abi.encode(MANDATE_TYPEHASH, m.adjuster, keccak256(abi.encodePacked(fillHashes)))); + } + + function _deriveClaimHash(BatchCompact memory compact, Mandate memory mandate) internal pure returns (bytes32) { + (bytes32 mandateHash,) = _hashMandate(mandate); + return _deriveClaimHash(compact, mandateHash); + } + + function _deriveClaimHash(BatchCompact memory compact, bytes32 mandateHash) internal pure returns (bytes32) { + bytes32 commitmentsHash = _staticHashCommitments(compact.commitments); + return keccak256( + abi.encode( + COMPACT_TYPEHASH_WITH_MANDATE, + compact.arbiter, + compact.sponsor, + compact.nonce, + compact.expires, + commitmentsHash, + mandateHash + ) + ); + } + + function _staticHashCommitments(Lock[] memory commitments) private pure returns (bytes32) { + bytes32[] memory hashes = new bytes32[](commitments.length); + for (uint256 i = 0; i < commitments.length; i++) { + hashes[i] = keccak256( + abi.encode(LOCK_TYPEHASH, commitments[i].lockTag, commitments[i].token, commitments[i].amount) + ); + } + return keccak256(abi.encodePacked(hashes)); + } + + function _buildFillHashes(Mandate memory m) internal pure returns (bytes32[] memory hashes) { + hashes = new bytes32[](m.fills.length); + for (uint256 i = 0; i < m.fills.length; i++) { + hashes[i] = _hashFill(m.fills[i]); + } + } + + function _createDigest(BatchCompact memory data, Mandate memory mandate, address verifyingContract) + internal + view + returns (bytes32 digest) + { + (bytes32 mandateHash,) = _hashMandate(mandate); + bytes32 compactHash = _deriveClaimHash(data, mandateHash); + // hash typed data + digest = keccak256( + abi.encodePacked( + '\x19\x01', // backslash is needed to escape the character + _domainSeparator(verifyingContract), + compactHash + ) + ); + } + + function _domainSeparator(address verifyingContract) internal view returns (bytes32) { + return keccak256( + abi.encode( + keccak256(bytes(EIP712_DOMAIN_TYPE)), + keccak256(bytes(name)), + keccak256(bytes(version)), + block.chainid, + verifyingContract + ) + ); + } + + function _signMessage(bytes32 hash_, uint256 signerPK_) internal pure returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPK_, hash_); + return abi.encodePacked(r, s, v); + } + + function _hashAndSign(BatchCompact memory data, Mandate memory mandate, address verifyingContract, uint256 signerPK) + internal + view + returns (bytes memory) + { + bytes32 hash = _createDigest(data, mandate, verifyingContract); + bytes memory signature = _signMessage(hash, signerPK); + return signature; + } +} + +abstract contract CompactData is CreateHash { + BatchCompact private compact; + Mandate private mandate; + + function setUp() public virtual override { + super.setUp(); + + defaultIdsAndAmounts[0][0] = usdcId; + defaultIdsAndAmounts[0][1] = defaultAmount; + + defaultCommitments.push(Lock({lockTag: usdcLockTag, token: address(usdc), amount: defaultAmount})); + + compact.arbiter = arbiter; + compact.sponsor = user; + compact.nonce = defaultNonce; + compact.expires = _getClaimExpiration(); + compact.commitments = defaultCommitments; + + Fill memory mainFill; + mainFill.chainId = defaultOutputChainId; + mainFill.tribunal = tribunal; + mainFill.expires = _getFillExpiration(); + mainFill.fillToken = defaultOutputToken; + mainFill.minimumFillAmount = defaultMinimumAmount; + mainFill.baselinePriorityFee = defaultBaselinePriorityFee; + mainFill.scalingFactor = defaultScalingFactor; + mainFill.priceCurve = defaultPriceCurve; + mainFill.recipient = user; + mainFill.salt = defaultSalt; + + mandate.adjuster = adjuster; + mandate.fills = new Fill[](1); + mandate.fills[0] = mainFill; + } + + function _getCompact() internal returns (BatchCompact memory) { + compact.expires = _getClaimExpiration(); + return compact; + } + + function _getMandate() internal returns (Mandate memory) { + mandate.fills[0].expires = _getFillExpiration(); + return mandate; + } + + function _getFillExpiration() internal view returns (uint256) { + return vm.getBlockTimestamp() + defaultResetPeriodTimestamp - 1; + } + + function _getClaimExpiration() internal view returns (uint256) { + return vm.getBlockTimestamp() + defaultResetPeriodTimestamp; + } +} + +abstract contract GaslessCrossChainOrderData is CompactData { + IOriginSettler.GaslessCrossChainOrder private gaslessCrossChainOrder; + + function setUp() public virtual override { + super.setUp(); + + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + gaslessCrossChainOrder.originSettler = allocator; + gaslessCrossChainOrder.user = compact_.sponsor; + gaslessCrossChainOrder.nonce = _composeNonceUint(compact_.sponsor, defaultNonce); + gaslessCrossChainOrder.originChainId = block.chainid; + gaslessCrossChainOrder.openDeadline = uint32(_getClaimExpiration()); + gaslessCrossChainOrder.fillDeadline = uint32(_getFillExpiration()); + gaslessCrossChainOrder.orderDataType = ORDERDATA_GASLESS_TYPEHASH; + gaslessCrossChainOrder.orderData = abi.encode( + IERC7683Allocator.OrderDataGasless({ + order: IERC7683Allocator.Order({ + arbiter: compact_.arbiter, + commitments: compact_.commitments, + mandate: mandate_ + }), + deposit: false + }) + ); + } + + function _getGaslessCrossChainOrder() internal view returns (IOriginSettler.GaslessCrossChainOrder memory) { + return gaslessCrossChainOrder; + } + + function _getGaslessCrossChainOrder(BatchCompact memory compact_, Mandate memory mandate_, bool deposit) + internal + view + returns (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_) + { + gaslessCrossChainOrder_.originSettler = allocator; + gaslessCrossChainOrder_.user = compact_.sponsor; + gaslessCrossChainOrder_.nonce = compact_.nonce; + gaslessCrossChainOrder_.originChainId = block.chainid; + gaslessCrossChainOrder_.openDeadline = uint32(compact_.expires); + gaslessCrossChainOrder_.fillDeadline = uint32(mandate_.fills[0].expires); + gaslessCrossChainOrder_.orderDataType = ORDERDATA_GASLESS_TYPEHASH; + gaslessCrossChainOrder_.orderData = abi.encode( + IERC7683Allocator.OrderDataGasless({ + order: IERC7683Allocator.Order({ + arbiter: compact_.arbiter, + commitments: compact_.commitments, + mandate: mandate_ + }), + deposit: deposit + }) + ); + return gaslessCrossChainOrder_; + } + + function _manipulateDeposit(IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bool deposit) + internal + pure + returns (IOriginSettler.GaslessCrossChainOrder memory) + { + bytes memory orderData = gaslessCrossChainOrder_.orderData; + assembly ("memory-safe") { + mstore(add(orderData, 0x60), deposit) + } + return gaslessCrossChainOrder_; + } +} + +abstract contract OnChainCrossChainOrderData is CompactData { + IOriginSettler.OnchainCrossChainOrder private onchainCrossChainOrder; + + function setUp() public virtual override { + super.setUp(); + + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + onchainCrossChainOrder.fillDeadline = uint32(_getFillExpiration()); + onchainCrossChainOrder.orderDataType = ORDERDATA_ONCHAIN_TYPEHASH; + onchainCrossChainOrder.orderData = abi.encode( + IERC7683Allocator.OrderDataOnChain({ + order: IERC7683Allocator.Order({ + arbiter: compact_.arbiter, + commitments: compact_.commitments, + mandate: mandate_ + }), + expires: uint32(compact_.expires) + }) + ); + } + + function _getOnChainCrossChainOrder() internal view returns (IOriginSettler.OnchainCrossChainOrder memory) { + return onchainCrossChainOrder; + } + + function _getOnChainCrossChainOrder(BatchCompact memory compact_, Mandate memory mandate_) + internal + view + returns (IOriginSettler.OnchainCrossChainOrder memory) + { + IOriginSettler.OnchainCrossChainOrder memory onchainCrossChainOrder_ = IOriginSettler.OnchainCrossChainOrder({ + fillDeadline: uint32(mandate_.fills[0].expires), + orderDataType: ORDERDATA_ONCHAIN_TYPEHASH, + orderData: abi.encode( + IERC7683Allocator.OrderDataOnChain({ + order: IERC7683Allocator.Order({ + arbiter: compact_.arbiter, + commitments: compact_.commitments, + mandate: mandate_ + }), + expires: uint32(compact_.expires) + }) + ) + }); + return onchainCrossChainOrder_; + } +} + +abstract contract Deposited is MocksSetup { + function setUp() public virtual override { + super.setUp(); + + vm.startPrank(user); + + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + + vm.stopPrank(); + } +} From 37fb6bf776c1f899543bb6e80a343a4a9e346727 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Tue, 19 Aug 2025 18:03:22 +0200 Subject: [PATCH 58/63] updated nonce setup --- src/allocators/OnChainAllocator.sol | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/allocators/OnChainAllocator.sol b/src/allocators/OnChainAllocator.sol index 564aa23..c2f8c09 100644 --- a/src/allocators/OnChainAllocator.sol +++ b/src/allocators/OnChainAllocator.sol @@ -23,7 +23,7 @@ contract OnChainAllocator is IOnChainAllocator { mapping(bytes32 tokenHash => Allocation[] allocations) internal _allocations; - mapping(bytes32 user => uint256 nonce) public nonces; + mapping(bytes32 user => uint96 nonce) public nonces; modifier onlyCompact() { if (msg.sender != COMPACT_CONTRACT) { @@ -87,7 +87,7 @@ contract OnChainAllocator is IOnChainAllocator { bytes32 typehash, bytes32 witness ) public returns (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) { - nonce = ++nonces[_toNonceId(msg.sender, recipient)]; // prevents griefing of frontrunning nonces + nonce = _getAndUpdateNonce(msg.sender, recipient); uint256[2][] memory idsAndAmounts = new uint256[2][](commitments.length); @@ -156,9 +156,8 @@ contract OnChainAllocator is IOnChainAllocator { bytes32 witness, bytes calldata /* orderData */ ) external returns (uint256 nonce) { - bytes32 nonceId = _toNonceId(msg.sender, recipient); uint32 expiration = uint32(expires); - nonce = nonces[nonceId] + 1; + nonce = _getAndUpdateNonce(msg.sender, recipient); AL.prepareAllocation(COMPACT_CONTRACT, nonce, recipient, idsAndAmounts, arbiter, expiration, typehash, witness); return nonce; @@ -173,7 +172,7 @@ contract OnChainAllocator is IOnChainAllocator { bytes32 witness, bytes calldata /* orderData */ ) external { - uint256 nonce = ++nonces[_toNonceId(msg.sender, recipient)]; + uint256 nonce = _getAndUpdateNonce(msg.sender, recipient); uint32 expiration = uint32(expires); (bytes32 claimHash, Lock[] memory commitments) = @@ -294,7 +293,7 @@ contract OnChainAllocator is IOnChainAllocator { revert InvalidExpiration(expires, block.timestamp); } - nonce = ++nonces[_toNonceId(address(0), sponsor)]; // address(0) as caller allows anyone to relay + nonce = _getAndUpdateNonce(address(0), sponsor); // address(0) as caller allows anyone to relay bytes32 commitmentsHash = AL.getCommitmentsHash(commitments); claimHash = AL.getClaimHash(arbiter, sponsor, nonce, expires, commitmentsHash, witness, typehash); @@ -468,6 +467,18 @@ contract OnChainAllocator is IOnChainAllocator { } } + function _getAndUpdateNonce(address calling, address sponsor) internal returns (uint256 nonce) { + assembly ("memory-safe") { + calling := xor(calling, mul(sponsor, iszero(calling))) + mstore(0x00, calling) + mstore(0x20, nonces.slot) + let nonceSlot := keccak256(0x00, 0x40) + let nonce96 := sload(nonceSlot) + nonce := or(shl(96, calling), add(nonce96, 1)) + sstore(nonceSlot, add(nonce96, 1)) + } + } + function _getTokenHash(bytes12 lockTag, address token, address sponsor) internal pure returns (bytes32 tokenHash) { assembly ("memory-safe") { mstore(0x00, lockTag) @@ -480,8 +491,4 @@ contract OnChainAllocator is IOnChainAllocator { function _getTokenHash(uint256 id, address sponsor) internal pure returns (bytes32 tokenHash) { tokenHash = keccak256(abi.encode(id, sponsor)); } - - function _toNonceId(address caller, address sponsor) internal pure returns (bytes32 nonce) { - return keccak256(abi.encode(caller, sponsor)); - } } From 20ead3758e818597aa12693e62e2f0a97b415257 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Tue, 19 Aug 2025 18:05:00 +0200 Subject: [PATCH 59/63] Reworked ERC7683 contracts --- .gitmodules | 4 + foundry.toml | 1 + src/allocators/ERC7683Allocator.sol | 452 ++++----------------- src/allocators/HybridERC7683.sol | 423 +++++-------------- src/allocators/lib/ERC7683AllocatorLib.sol | 295 ++++++++++++++ src/allocators/types/TribunalStructs.sol | 56 ++- src/interfaces/IERC7683Allocator.sol | 25 +- 7 files changed, 539 insertions(+), 717 deletions(-) create mode 100644 src/allocators/lib/ERC7683AllocatorLib.sol diff --git a/.gitmodules b/.gitmodules index 85474cd..0351d97 100644 --- a/.gitmodules +++ b/.gitmodules @@ -17,3 +17,7 @@ path = lib/the-compact url = https://github.com/Uniswap/the-compact branch = v1 +[submodule "lib/tribunal"] + path = lib/tribunal + url = https://github.com/Uniswap/tribunal + branch = new-mandate diff --git a/foundry.toml b/foundry.toml index f6597d1..720c746 100644 --- a/foundry.toml +++ b/foundry.toml @@ -18,6 +18,7 @@ remappings = [ "@openzeppelin/contracts=lib/openzeppelin-contracts/contracts", "@openzeppelin/contracts-upgradeable=lib/openzeppelin-contracts-upgradeable/contracts", "@uniswap/the-compact=lib/the-compact/src", + "@uniswap/tribunal=lib/tribunal/src", "@solady=lib/solady/src", ] diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index c3ff229..eaf1758 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -5,8 +5,21 @@ pragma solidity ^0.8.27; import {IOriginSettler} from '../interfaces/ERC7683/IOriginSettler.sol'; import {IERC7683Allocator} from '../interfaces/IERC7683Allocator.sol'; import {OnChainAllocator} from './OnChainAllocator.sol'; -import {BatchClaim, Mandate} from './types/TribunalStructs.sol'; +import {AllocatorLib as AL} from './lib/AllocatorLib.sol'; +import {ERC7683AllocatorLib as ERC7683AL} from './lib/ERC7683AllocatorLib.sol'; + +import {Tribunal} from '@uniswap/tribunal/Tribunal.sol'; +import {Fill, Mandate, RecipientCallback} from '@uniswap/tribunal/types/TribunalStructs.sol'; + import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; +import { + COMPACT_TYPEHASH_WITH_MANDATE, + COMPACT_WITH_MANDATE_TYPESTRING, + MANDATE_BATCH_COMPACT_TYPEHASH, + MANDATE_FILL_TYPEHASH, + MANDATE_RECIPIENT_CALLBACK_TYPEHASH, + MANDATE_TYPEHASH +} from '@uniswap/tribunal/types/TribunalTypeHashes.sol'; import {LibBytes} from '@solady/utils/LibBytes.sol'; import {BatchCompact, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; @@ -16,424 +29,129 @@ import {BatchCompact, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; /// @dev The contract ensures tokens can not be double spent by a user in a fully decentralized manner. /// @dev Users can open orders for themselves or for others by providing a signature or the tokens directly. contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { - /// @notice The typehash of the OrderDataOnChain struct - // keccak256("OrderDataOnChain(Order order,uint256 expires) - // Lock(bytes12 lockTag,address token,uint256 amount) - // Order(address arbiter,Lock[] commitments,uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt,bytes32 qualification)") - bytes32 public constant ORDERDATA_ONCHAIN_TYPEHASH = - 0x037a34e1ded3bcc84f59dfc185efc3553c509ebab317153a8dddefce2eaee6f0; - - /// @notice The typehash of the OrderDataGasless struct - // keccak256("OrderDataGasless(Order order,bool deposit) - // Lock(bytes12 lockTag,address token,uint256 amount) - // Order(address arbiter,Lock[] commitments,uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt,bytes32 qualification)") - bytes32 public constant ORDERDATA_GASLESS_TYPEHASH = - 0x79e4af6feaa84a46fd69ed25e4595e9f6e8690ba3a6c564bfa235542f9faf55c; - - /// @notice keccak256("BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments,Mandate mandate) - // Lock(bytes12 lockTag,address token,uint256 amount) - // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") - bytes32 public constant BATCH_COMPACT_WITNESS_TYPEHASH = - 0x5ede122c736b60a8b718f83dcfb5d6e4aa27c9714d0c7bc9ca86562b8f878463; - - /// @notice keccak256("Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") - bytes32 private constant MANDATE_TYPEHASH = 0x74d9c10530859952346f3e046aa2981a24bb7524b8394eb45a9deddced9d6501; - - mapping(bytes32 claimHash => bytes32 qualification) public qualifications; - - constructor(address compactContract_) OnChainAllocator(compactContract_) {} + constructor(address compact) OnChainAllocator(compact) {} /// @inheritdoc IOriginSettler - function openFor(GaslessCrossChainOrder calldata order_, bytes calldata sponsorSignature_, bytes calldata) - external - { - // Check if orderDataType is the one expected by the allocator - if (order_.orderDataType != ORDERDATA_GASLESS_TYPEHASH) { - revert InvalidOrderDataType(order_.orderDataType, ORDERDATA_GASLESS_TYPEHASH); - } - if (order_.originSettler != address(this)) { - revert InvalidOriginSettler(order_.originSettler, address(this)); - } - - // Decode the orderData - (Order calldata orderData, uint32 deposit) = _decodeOrderData(order_.orderData); - deposit = _sanitizeBool(deposit); + function openFor(GaslessCrossChainOrder calldata order, bytes calldata sponsorSignature, bytes calldata) external { + ( + IERC7683Allocator.Order calldata orderData, + uint32 deposit, + bytes32 mandateHash, + IOriginSettler.ResolvedCrossChainOrder memory resolvedOrder + ) = ERC7683AL.openForPreparation(order, sponsorSignature); - uint160 caller = uint160(deposit * uint160(msg.sender)); // for a deposit, the nonce will be scoped to the caller + user - bytes32 nonceIdentifier = _toNonceId(address(caller), order_.user); + uint160 caller = uint160(deposit * uint160(msg.sender)); // for a deposit, the nonce will be scoped to the caller // Early revert if the expected nonce is not the next nonce - if (order_.nonce != nonces[nonceIdentifier] + 1) { - revert InvalidNonce(order_.nonce, nonces[nonceIdentifier] + 1); + if (order.nonce != _getNonce(address(caller), order.user)) { + revert InvalidNonce(order.nonce, _getNonce(address(caller), order.user)); } - bytes32 qualification = bytes32(uint256(orderData.qualification) * deposit); // delete qualification if not a deposit - - ResolvedCrossChainOrder memory resolvedOrder = _resolveOrder( - order_.user, - order_.nonce, - order_.openDeadline, - order_.fillDeadline, - orderData, - sponsorSignature_, - qualification - ); - - bytes32 mandateHash = _mandateHash(orderData, order_.fillDeadline); - + uint256 nonce; if (deposit == 0) { - _open(order_.user, order_.openDeadline, orderData, sponsorSignature_, mandateHash, resolvedOrder); + // Register the allocation on chain + (, nonce) = allocateFor( + order.user, + orderData.commitments, + orderData.arbiter, + order.openDeadline, + COMPACT_TYPEHASH_WITH_MANDATE, + mandateHash, + sponsorSignature + ); } else { - _openAndRegister(order_.user, order_.openDeadline, orderData, mandateHash, resolvedOrder); + // Register the allocation on chain + uint256[] memory registeredAmounts; + (, registeredAmounts, nonce) = allocateAndRegister( + order.user, + orderData.commitments, + orderData.arbiter, + order.openDeadline, + COMPACT_TYPEHASH_WITH_MANDATE, + mandateHash + ); + + for (uint256 i = 0; i < orderData.commitments.length; i++) { + resolvedOrder.minReceived[i].amount = registeredAmounts[i]; + } } + // Emit an open event + emit Open(bytes32(nonce), resolvedOrder); } /// @inheritdoc IOriginSettler function open(OnchainCrossChainOrder calldata order) external { - // Check if orderDataType is the one expected by the allocator - if (order.orderDataType != ORDERDATA_ONCHAIN_TYPEHASH) { - revert InvalidOrderDataType(order.orderDataType, ORDERDATA_ONCHAIN_TYPEHASH); - } - - // Decode the orderData - (Order calldata orderData, uint32 expires) = _decodeOrderData(order.orderData); - expires = _sanitizeUint32(expires); + (IERC7683Allocator.Order calldata orderData, uint32 expires, bytes32 mandateHash, bytes32[] memory fillHashes) = + ERC7683AL.openPreparation(order); - bytes32 mandateHash = _mandateHash(orderData, order.fillDeadline); + // Register the allocation on chain + (, uint256 nonce) = + allocate(orderData.commitments, orderData.arbiter, expires, COMPACT_TYPEHASH_WITH_MANDATE, mandateHash); - ResolvedCrossChainOrder memory resolvedOrder = _resolveOrder( - msg.sender, - nonces[_toNonceId(address(0), msg.sender)] + 1, - expires, - order.fillDeadline, - orderData, - LibBytes.emptyCalldata(), - orderData.qualification - ); + ResolvedCrossChainOrder memory resolvedOrder = + ERC7683AL.resolveOrder(msg.sender, nonce, expires, fillHashes, orderData, LibBytes.emptyCalldata()); - _open(msg.sender, expires, orderData, LibBytes.emptyCalldata(), mandateHash, resolvedOrder); + // Emit an open event + emit Open(bytes32(nonce), resolvedOrder); } /// @inheritdoc IOriginSettler - function resolveFor(GaslessCrossChainOrder calldata order_, bytes calldata) + function resolveFor(GaslessCrossChainOrder calldata order, bytes calldata) external view returns (ResolvedCrossChainOrder memory) { - // Check if orderDataType is the one expected by the allocator - if (order_.orderDataType != ORDERDATA_GASLESS_TYPEHASH) { - revert InvalidOrderDataType(order_.orderDataType, ORDERDATA_GASLESS_TYPEHASH); - } - if (order_.originSettler != address(this)) { - revert InvalidOriginSettler(order_.originSettler, address(this)); - } - - // Decode the orderData - (Order calldata orderData, uint32 deposit) = _decodeOrderData(order_.orderData); - deposit = _sanitizeBool(deposit); + (, uint32 deposit,, IOriginSettler.ResolvedCrossChainOrder memory resolvedOrder) = + ERC7683AL.openForPreparation(order, LibBytes.emptyCalldata()); uint160 caller = uint160(deposit * uint160(msg.sender)); // for a deposit, the nonce will be scoped to the caller + user - bytes32 nonceIdentifier = _toNonceId(address(caller), order_.user); // Early revert if the expected nonce is not the next nonce - if (order_.nonce != nonces[nonceIdentifier] + 1) { - revert InvalidNonce(order_.nonce, nonces[nonceIdentifier] + 1); + if (order.nonce != _getNonce(address(caller), order.user)) { + revert InvalidNonce(order.nonce, _getNonce(address(caller), order.user)); } - bytes32 qualification = bytes32(uint256(orderData.qualification) * deposit); - - return _resolveOrder( - order_.user, - order_.nonce, - order_.openDeadline, - order_.fillDeadline, - orderData, - LibBytes.emptyCalldata(), - qualification - ); + return resolvedOrder; } /// @inheritdoc IOriginSettler function resolve(OnchainCrossChainOrder calldata order) external view returns (ResolvedCrossChainOrder memory) { - // Check if orderDataType is the one expected by the allocator - if (order.orderDataType != ORDERDATA_ONCHAIN_TYPEHASH) { - revert InvalidOrderDataType(order.orderDataType, ORDERDATA_ONCHAIN_TYPEHASH); - } + (IERC7683Allocator.Order calldata orderData, uint32 expires,, bytes32[] memory fillHashes) = + ERC7683AL.openPreparation(order); - // Decode the orderData - (Order calldata orderData, uint32 expires) = _decodeOrderData(order.orderData); - expires = _sanitizeUint32(expires); - - return _resolveOrder( - msg.sender, - nonces[_toNonceId(address(0), msg.sender)] + 1, - expires, - order.fillDeadline, - orderData, - LibBytes.emptyCalldata(), - orderData.qualification + return ERC7683AL.resolveOrder( + msg.sender, _getNonce(address(0), msg.sender), expires, fillHashes, orderData, LibBytes.emptyCalldata() ); } - /// @inheritdoc IAllocator - function authorizeClaim( - bytes32 claimHash, - address, /*arbiter*/ // The account tasked with verifying and submitting the claim. - address sponsor, // The account sponsoring the claim. - uint256, /*nonce*/ // A parameter to enforce replay protection, scoped to allocator. - uint256 expires, // The time at which the claim expires. - uint256[2][] calldata idsAndAmounts, // The allocated token IDs and amounts. - bytes calldata allocatorData // Arbitrary data provided by the arbiter. - ) public override(OnChainAllocator, IAllocator) onlyCompact returns (bytes4) { - super.authorizeClaim(claimHash, address(0), sponsor, 0, expires, idsAndAmounts, allocatorData); - - if (qualifications[claimHash] != bytes32(allocatorData)) { - revert InvalidAllocatorData(bytes32(allocatorData), qualifications[claimHash]); - } - - return this.authorizeClaim.selector; - } - - /// @inheritdoc IAllocator - function isClaimAuthorized( - bytes32 claimHash, - address, /*arbiter*/ // The account tasked with verifying and submitting the claim. - address sponsor, // The account to source the tokens from. - uint256, /*nonce*/ // A parameter to enforce replay protection, scoped to allocator. - uint256 expires, // The time at which the claim expires. - uint256[2][] calldata idsAndAmounts, // The allocated token IDs and amounts. - bytes calldata allocatorData // Arbitrary data provided by the arbiter. - ) public view override(OnChainAllocator, IAllocator) returns (bool) { - if ( - !super.isClaimAuthorized(claimHash, address(0), sponsor, 0, expires, idsAndAmounts, LibBytes.emptyCalldata()) - ) { - return false; - } - - return qualifications[claimHash] == bytes32(allocatorData); - } - /// @inheritdoc IERC7683Allocator function getCompactWitnessTypeString() external pure returns (string memory) { - return - 'BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments,Mandate mandate)Lock(bytes12 lockTag,address token,uint256 amount)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; + return COMPACT_WITH_MANDATE_TYPESTRING; } /// @inheritdoc IERC7683Allocator - function checkNonce(GaslessCrossChainOrder calldata order_, address caller) - external - view - returns (bool nonceValid) - { - (, uint32 deposit) = _decodeOrderData(order_.orderData); - deposit = _sanitizeBool(deposit); + function getNonce(GaslessCrossChainOrder calldata order, address caller) external view returns (uint256 nonce) { + (, uint32 deposit) = ERC7683AL.decodeOrderData(order.orderData); + deposit = ERC7683AL.sanitizeBool(deposit); caller = address(uint160(deposit * uint160(caller))); // for a deposit, the nonce will be scoped to the caller + user - bytes32 nonceIdentifier = _toNonceId(caller, order_.user); - // Early revert if the expected nonce is not the next nonce - return (order_.nonce == nonces[nonceIdentifier] + 1); + return _getNonce(address(caller), order.user); } /// @inheritdoc IERC7683Allocator - function createFillerData(address claimant_) external pure returns (bytes memory fillerData) { - return abi.encode(claimant_); - } - - function _open( - address sponsor, - uint32 expires, - Order calldata orderData, - bytes calldata sponsorSignature, - bytes32 mandateHash, - ResolvedCrossChainOrder memory resolvedOrder - ) internal { - // Register the allocation on chain - (bytes32 claimHash, uint256 nonce) = allocateFor( - sponsor, - orderData.commitments, - orderData.arbiter, - expires, - BATCH_COMPACT_WITNESS_TYPEHASH, - mandateHash, - sponsorSignature - ); - - if (sponsor == msg.sender && orderData.qualification != bytes32(0)) { - qualifications[claimHash] = orderData.qualification; - } - - // Emit an open event - emit Open(bytes32(nonce), resolvedOrder); - } - - function _openAndRegister( - address sponsor, - uint32 expires, - Order calldata orderData, - bytes32 mandateHash_, - ResolvedCrossChainOrder memory resolvedOrder - ) internal { - // Register the allocation on chain - (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = allocateAndRegister( - sponsor, orderData.commitments, orderData.arbiter, expires, BATCH_COMPACT_WITNESS_TYPEHASH, mandateHash_ - ); - - if (orderData.qualification != bytes32(0)) { - qualifications[claimHash] = orderData.qualification; - } - - for (uint256 i = 0; i < orderData.commitments.length; i++) { - resolvedOrder.minReceived[i].amount = registeredAmounts[i]; - } - - // Emit an open event - emit Open(bytes32(nonce), resolvedOrder); - } - - function _decodeOrderData(bytes calldata orderData) - internal - pure - returns (Order calldata order, uint32 additionalInput) - { - // orderData includes the OrderData(OnChain/Gasless) struct, and the nested Order struct. - // 0x00: OrderDataOnChain.offset - // 0x20: OrderDataOnChain.order.offset - // 0x40: OrderDataOnChain.expires - - // 0x00: OrderDataGasless.offset - // 0x20: OrderDataGasless.order.offset - - assembly ("memory-safe") { - let l := sub(orderData.length, 0x20) - let s := calldataload(add(orderData.offset, 0x20)) // Relative offset of `orderBytes` from `orderData.offset` and the `OrderData...` struct. - order := add(orderData.offset, add(s, 0x20)) // Add 0x20 since the OrderStruct is within the `OrderData...` struct - if shr(64, or(s, or(l, orderData.offset))) { revert(l, 0x00) } - - additionalInput := calldataload(add(orderData.offset, 0x40)) - } - } - - function _resolveOrder( - address sponsor, - uint256 nonce, - uint32 expires, - uint32 fillDeadline, - Order calldata orderData, - bytes calldata sponsorSignature, - bytes32 qualification - ) internal view returns (ResolvedCrossChainOrder memory) { - ResolvedCrossChainOrder memory resolvedOrder = ResolvedCrossChainOrder({ - user: sponsor, - originChainId: block.chainid, - openDeadline: uint32(expires), - fillDeadline: fillDeadline, - orderId: bytes32(nonce), - maxSpent: new Output[](0), - minReceived: new Output[](0), - fillInstructions: new FillInstruction[](0) - }); - - BatchCompact memory compact = BatchCompact({ - arbiter: orderData.arbiter, - sponsor: sponsor, - nonce: nonce, - expires: expires, - commitments: orderData.commitments - }); - - BatchClaim memory claim = BatchClaim({ - chainId: block.chainid, - compact: compact, - sponsorSignature: sponsorSignature, - allocatorSignature: '' // No signature required from this allocator, it will verify the claim on chain via ERC1271. - }); - - Mandate memory mandate = Mandate({ - recipient: orderData.recipient, - expires: fillDeadline, - token: orderData.settlementToken, - minimumAmount: orderData.minimumAmount, - baselinePriorityFee: orderData.baselinePriorityFee, - scalingFactor: orderData.scalingFactor, - decayCurve: orderData.decayCurve, - salt: orderData.salt - }); - - FillInstruction[] memory fillInstructions = new FillInstruction[](1); - fillInstructions[0] = FillInstruction({ - destinationChainId: orderData.chainId, - destinationSettler: _addressToBytes32(orderData.tribunal), - originData: abi.encode(claim, mandate, uint200(bytes25(qualification)), uint56(uint256(qualification))) - }); - resolvedOrder.fillInstructions = fillInstructions; - - Output memory spent = Output({ - token: _addressToBytes32(mandate.token), - amount: type(uint256).max, - recipient: _addressToBytes32(mandate.recipient), - chainId: orderData.chainId - }); - Output[] memory maxSpent = new Output[](1); - maxSpent[0] = spent; - resolvedOrder.maxSpent = maxSpent; - - resolvedOrder.minReceived = _createMinimumReceived(orderData.commitments); - - return resolvedOrder; - } - - function _createMinimumReceived(Lock[] calldata commitments) internal view returns (Output[] memory) { - Output[] memory minReceived = new Output[](commitments.length); - - for (uint256 i = 0; i < commitments.length; i++) { - Output memory received = Output({ - token: _addressToBytes32(commitments[i].token), - amount: commitments[i].amount, - recipient: bytes32(0), - chainId: block.chainid - }); - minReceived[i] = received; - } - return minReceived; - } - - function _mandateHash(Order calldata orderData, uint32 fillDeadline) internal pure returns (bytes32 mandateHash_) { - bytes32 decayCurveHash = keccak256(abi.encodePacked(orderData.decayCurve)); - mandateHash_ = keccak256( - abi.encode( - MANDATE_TYPEHASH, - orderData.chainId, - orderData.tribunal, - orderData.recipient, - fillDeadline, - orderData.settlementToken, - orderData.minimumAmount, - orderData.baselinePriorityFee, - orderData.scalingFactor, - decayCurveHash, - orderData.salt - ) - ); - } - - function _addressToBytes32(address address_) internal pure returns (bytes32 output_) { - assembly ("memory-safe") { - output_ := shr(96, shl(96, address_)) - } - } - - function _sanitizeUint32(uint32 value) internal pure returns (uint32) { - assembly ("memory-safe") { - value := shr(224, shl(224, value)) - } - return value; + function createFillerData(address claimant) external pure returns (bytes memory fillerData) { + return abi.encode(claimant); } - function _sanitizeBool(uint32 value) internal pure returns (uint32) { + function _getNonce(address calling, address sponsor) internal view returns (uint256 nonce) { assembly ("memory-safe") { - value := iszero(iszero(value)) + calling := xor(calling, mul(sponsor, iszero(calling))) + mstore(0x00, calling) + mstore(0x20, nonces.slot) + let nonceSlot := keccak256(0x00, 0x40) + let nonce96 := sload(nonceSlot) + nonce := or(shl(96, calling), add(nonce96, 1)) } - return value; } } diff --git a/src/allocators/HybridERC7683.sol b/src/allocators/HybridERC7683.sol index 7531c67..a4bcbfc 100644 --- a/src/allocators/HybridERC7683.sol +++ b/src/allocators/HybridERC7683.sol @@ -2,193 +2,103 @@ pragma solidity ^0.8.27; -import {BatchClaim, Mandate} from './types/TribunalStructs.sol'; +import {ERC7683AllocatorLib as ERC7683AL} from './lib/ERC7683AllocatorLib.sol'; import {LibBytes} from '@solady/utils/LibBytes.sol'; import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; import {BatchCompact, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; - +import {Tribunal} from '@uniswap/tribunal/Tribunal.sol'; +import {Fill, Mandate} from '@uniswap/tribunal/types/TribunalStructs.sol'; + +import { + COMPACT_TYPEHASH_WITH_MANDATE, + COMPACT_WITH_MANDATE_TYPESTRING, + MANDATE_BATCH_COMPACT_TYPEHASH, + MANDATE_FILL_TYPEHASH, + MANDATE_RECIPIENT_CALLBACK_TYPEHASH, + MANDATE_TYPEHASH +} from '@uniswap/tribunal/types/TribunalTypeHashes.sol'; import {HybridAllocator} from 'src/allocators/HybridAllocator.sol'; -import {BATCH_COMPACT_WITNESS_TYPEHASH, MANDATE_TYPEHASH} from 'src/allocators/lib/TypeHashes.sol'; -import {IHybridERC7683} from 'src/interfaces/IHybridERC7683.sol'; +import {IERC7683Allocator} from 'src/interfaces/IERC7683Allocator.sol'; import {AllocatorLib as AL} from 'src/allocators/lib/AllocatorLib.sol'; import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; -contract HybridERC7683 is HybridAllocator, IHybridERC7683 { - // mask for an active claim - uint256 private constant _ACTIVE_CLAIM_MASK = 0x0000000000000000000000000000000000000000000000000000000000000001; - - /// @notice The typehash of the OrderDataOnChain struct - // keccak256("OrderDataOnChain(Order order,uint256 expires) - // Order(address arbiter,uint256[2][] idsAndAmounts,uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt,bytes32 qualification)") - bytes32 public constant ORDERDATA_ONCHAIN_TYPEHASH = - 0xd13cc04099540f243b0042f68c0edbce9aefe428c22e0354a24061c5d98c7276; - - /// @notice The typehash of the OrderDataGasless struct - // keccak256("OrderDataGasless(Order order) - // Order(address arbiter,uint256[2][] idsAndAmounts,uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt,bytes32 qualification)") - bytes32 public constant ORDERDATA_GASLESS_TYPEHASH = - 0xfba49b9453e7d260d702826a659947a671a3e6a970688a795c82065685236b52; - - /// @notice keccak256("QualifiedClaim(bytes32 claimHash,uint256 targetBlock,uint256 maximumBlocksAfterTarget)") - bytes32 public constant QUALIFICATION_TYPEHASH = 0x59866b84bd1f6c909cf2a31efd20c59e6c902e50f2c196994e5aa85cdc7d7ce0; +contract HybridERC7683 is HybridAllocator, IERC7683Allocator { + error OnlyDepositsAllowed(); - uint256 private constant _INVALID_QUALIFICATION_ERROR_SIGNATURE = 0x7ac3c7d4; - - constructor(address compact_, address signer_) HybridAllocator(compact_, signer_) {} + constructor(address compact, address signer) HybridAllocator(compact, signer) {} /// @inheritdoc IOriginSettler - function openFor( - GaslessCrossChainOrder calldata order, - bytes calldata, /*sponsorSignature_*/ - bytes calldata /*originFillerData*/ - ) external { - // Check if orderDataType is the one expected by the allocator - if (order.orderDataType != ORDERDATA_GASLESS_TYPEHASH) { - revert InvalidOrderDataType(order.orderDataType, ORDERDATA_GASLESS_TYPEHASH); + function openFor(GaslessCrossChainOrder calldata order, bytes calldata sponsorSignature, bytes calldata) external { + ( + IERC7683Allocator.Order calldata orderData, + uint32 deposit, + bytes32 mandateHash, + IOriginSettler.ResolvedCrossChainOrder memory resolvedOrder + ) = ERC7683AL.openForPreparation(order, sponsorSignature); + + uint160 caller = uint160(deposit * uint160(msg.sender)); // for a deposit, the nonce will be scoped to the caller + + // Early revert if the expected nonce is not the next nonce + if (order.nonce != nonces + 1) { + revert InvalidNonce(order.nonce, nonces + 1); } - (Order calldata orderData,) = _decodeOrderData(order.orderData, false); - - // create witness hash - bytes32 witnessHash = keccak256( - abi.encode( - MANDATE_TYPEHASH, - orderData.chainId, - orderData.tribunal, - orderData.recipient, - order.fillDeadline, - orderData.settlementToken, - orderData.minimumAmount, - orderData.baselinePriorityFee, - orderData.scalingFactor, - keccak256(abi.encodePacked(orderData.decayCurve)), - orderData.salt - ) - ); - - // register claim - (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce_) = allocateAndRegister( - order.user, - orderData.idsAndAmounts, - orderData.arbiter, - order.openDeadline, - BATCH_COMPACT_WITNESS_TYPEHASH, - witnessHash - ); - - _storeQualification(claimHash, orderData.qualification); + uint256 nonce; + if (deposit == 0) { + // Hybrid Allocator requires a deposit + revert OnlyDepositsAllowed(); + } else { + // Create idsAndAmounts + uint256[2][] memory idsAndAmounts = new uint256[2][](orderData.commitments.length); + for (uint256 i = 0; i < orderData.commitments.length; i++) { + idsAndAmounts[i][0] = AL.toId(orderData.commitments[i].lockTag, orderData.commitments[i].token); + idsAndAmounts[i][1] = orderData.commitments[i].amount; + } - Lock[] memory locks = new Lock[](registeredAmounts.length); - for (uint256 i = 0; i < registeredAmounts.length; i++) { - locks[i] = AL.toLock(orderData.idsAndAmounts[i][0], registeredAmounts[i]); + // Register the allocation on chain + uint256[] memory registeredAmounts; + (, registeredAmounts, nonce) = allocateAndRegister( + order.user, + idsAndAmounts, + orderData.arbiter, + order.openDeadline, + COMPACT_TYPEHASH_WITH_MANDATE, + mandateHash + ); + + for (uint256 i = 0; i < orderData.commitments.length; i++) { + resolvedOrder.minReceived[i].amount = registeredAmounts[i]; + } } - - BatchCompact memory batchCompact = BatchCompact({ - arbiter: orderData.arbiter, - sponsor: order.user, - nonce: nonce_, - expires: order.openDeadline, - commitments: locks - }); - - // emit open event - emit Open( - bytes32(batchCompact.nonce), _convertToResolvedCrossChainOrder(orderData, order.fillDeadline, batchCompact) - ); + // Emit an open event + emit Open(bytes32(nonce), resolvedOrder); } /// @inheritdoc IOriginSettler function open(OnchainCrossChainOrder calldata order) external { - // Check if orderDataType is the one expected by the allocator - if (order.orderDataType != ORDERDATA_ONCHAIN_TYPEHASH) { - revert InvalidOrderDataType(order.orderDataType, ORDERDATA_ONCHAIN_TYPEHASH); - } - - (Order calldata orderData, uint256 expires) = _decodeOrderData(order.orderData, true); - - // create witness hash - bytes32 witnessHash = keccak256( - abi.encode( - MANDATE_TYPEHASH, - orderData.chainId, - orderData.tribunal, - orderData.recipient, - order.fillDeadline, - orderData.settlementToken, - orderData.minimumAmount, - orderData.baselinePriorityFee, - orderData.scalingFactor, - keccak256(abi.encodePacked(orderData.decayCurve)), - orderData.salt - ) - ); - - // register claim - (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce_) = allocateAndRegister( - msg.sender, orderData.idsAndAmounts, orderData.arbiter, expires, BATCH_COMPACT_WITNESS_TYPEHASH, witnessHash - ); - - _storeQualification(claimHash, orderData.qualification); - - Lock[] memory locks = new Lock[](registeredAmounts.length); - for (uint256 i = 0; i < registeredAmounts.length; i++) { - locks[i] = AL.toLock(orderData.idsAndAmounts[i][0], registeredAmounts[i]); + (IERC7683Allocator.Order calldata orderData, uint32 expires, bytes32 mandateHash, bytes32[] memory fillHashes) = + ERC7683AL.openPreparation(order); + + // Create idsAndAmounts + uint256[2][] memory idsAndAmounts = new uint256[2][](orderData.commitments.length); + for (uint256 i = 0; i < orderData.commitments.length; i++) { + idsAndAmounts[i][0] = AL.toId(orderData.commitments[i].lockTag, orderData.commitments[i].token); + idsAndAmounts[i][1] = orderData.commitments[i].amount; } - BatchCompact memory batchCompact = BatchCompact({ - arbiter: orderData.arbiter, - sponsor: msg.sender, - nonce: nonce_, - expires: expires, - commitments: locks - }); - - // emit open event - emit Open( - bytes32(batchCompact.nonce), _convertToResolvedCrossChainOrder(orderData, order.fillDeadline, batchCompact) + // deposit the the tokens into the compact and register the claim + (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = allocateAndRegister( + msg.sender, idsAndAmounts, orderData.arbiter, expires, COMPACT_TYPEHASH_WITH_MANDATE, mandateHash ); - } - - /// @inheritdoc IAllocator - function authorizeClaim( - bytes32 claimHash, - address, /*arbiter*/ - address, /*sponsor*/ - uint256, /*nonce*/ - uint256, /*expires*/ - uint256[2][] calldata, /*idsAndAmounts*/ - bytes calldata allocatorData_ - ) external override(HybridAllocator, IAllocator) returns (bytes4) { - if (msg.sender != address(_COMPACT)) { - revert InvalidCaller(msg.sender, address(_COMPACT)); - } - // The compact will check the validity of the nonce and expiration - - (bool validClaim, uint128 targetBlock, uint120 maximumBlocksAfterTarget) = - _checkClaim(claimHash, allocatorData_); - // Check if the claim was allocated on chain - if (validClaim) { - delete claims[claimHash]; - - // Authorize the claim - return IAllocator.authorizeClaim.selector; - } - - if (allocatorData_.length != 0xe0 && allocatorData_.length != 0xc0) revert InvalidSignature(); - - // Create the digest for the qualified claim hash - bytes32 qualifiedClaimHash = - keccak256(abi.encode(QUALIFICATION_TYPEHASH, claimHash, targetBlock, maximumBlocksAfterTarget)); - bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), _COMPACT_DOMAIN_SEPARATOR, qualifiedClaimHash)); - // Check the allocator data for a valid signature by an authorized signer - bytes calldata allocatorSignature = LibBytes.bytesInCalldata(allocatorData_, 0x40); - if (!_checkSignature(digest, allocatorSignature)) { - revert InvalidSignature(); + ResolvedCrossChainOrder memory resolvedOrder = + ERC7683AL.resolveOrder(msg.sender, nonce, expires, fillHashes, orderData, LibBytes.emptyCalldata()); + for (uint256 i = 0; i < orderData.commitments.length; i++) { + resolvedOrder.minReceived[i].amount = registeredAmounts[i]; } - // Authorize the claim - return IAllocator.authorizeClaim.selector; + // Emit an open event + emit Open(bytes32(nonce), resolvedOrder); } /// @inheritdoc IOriginSettler @@ -197,184 +107,49 @@ contract HybridERC7683 is HybridAllocator, IHybridERC7683 { view returns (ResolvedCrossChainOrder memory) { - // Check if orderDataType is the one expected by the allocator - if (order.orderDataType != ORDERDATA_GASLESS_TYPEHASH) { - revert InvalidOrderDataType(order.orderDataType, ORDERDATA_GASLESS_TYPEHASH); - } - - (Order calldata orderData,) = _decodeOrderData(order.orderData, false); + (, uint32 deposit,, IOriginSettler.ResolvedCrossChainOrder memory resolvedOrder) = + ERC7683AL.openForPreparation(order, LibBytes.emptyCalldata()); - Lock[] memory locks = new Lock[](orderData.idsAndAmounts.length); - for (uint256 i = 0; i < orderData.idsAndAmounts.length; i++) { - locks[i] = AL.toLock(orderData.idsAndAmounts[i][0], orderData.idsAndAmounts[i][1]); + // Revert if the nonce is not the next nonce + if (order.nonce != nonces + 1) { + revert InvalidNonce(order.nonce, nonces + 1); } - BatchCompact memory batchCompact = BatchCompact({ - arbiter: orderData.arbiter, - sponsor: order.user, - nonce: nonces + 1, - expires: order.openDeadline, - commitments: locks - }); + if (deposit == 0) { + // Hybrid Allocator requires a deposit + revert OnlyDepositsAllowed(); + } - return _convertToResolvedCrossChainOrder(orderData, order.fillDeadline, batchCompact); + return resolvedOrder; } /// @inheritdoc IOriginSettler function resolve(OnchainCrossChainOrder calldata order) external view returns (ResolvedCrossChainOrder memory) { - // Check if orderDataType is the one expected by the allocator - if (order.orderDataType != ORDERDATA_ONCHAIN_TYPEHASH) { - revert InvalidOrderDataType(order.orderDataType, ORDERDATA_ONCHAIN_TYPEHASH); - } - - (Order calldata orderData, uint256 expires) = _decodeOrderData(order.orderData, true); - uint256 idsLength = orderData.idsAndAmounts.length; - Lock[] memory locks = new Lock[](idsLength); - for (uint256 i = 0; i < idsLength; i++) { - uint256 id = orderData.idsAndAmounts[i][0]; - locks[i] = AL.toLock(id, orderData.idsAndAmounts[i][1]); - } - BatchCompact memory batchCompact = BatchCompact({ - arbiter: orderData.arbiter, - sponsor: msg.sender, - nonce: nonces + 1, // nonce is incremented by 1 when the claim is registered - expires: expires, - commitments: locks - }); - return _convertToResolvedCrossChainOrder(orderData, order.fillDeadline, batchCompact); - } + (IERC7683Allocator.Order calldata orderData, uint32 expires,, bytes32[] memory fillHashes) = + ERC7683AL.openPreparation(order); - function _storeQualification(bytes32 claimHash, bytes32 qualification) private { - // store the allocator data with the claims mapping. - assembly ("memory-safe") { - if and(qualification, _ACTIVE_CLAIM_MASK) { - mstore(0, _INVALID_QUALIFICATION_ERROR_SIGNATURE) - mstore(0x20, qualification) - revert(0x1c, 0x24) - } - - mstore(0x00, claimHash) - mstore(0x20, claims.slot) - let claimSlot := keccak256(0x00, 0x40) - let indicator := or(qualification, _ACTIVE_CLAIM_MASK) - sstore(claimSlot, indicator) - } + return ERC7683AL.resolveOrder(msg.sender, nonces + 1, expires, fillHashes, orderData, LibBytes.emptyCalldata()); } - function _checkClaim(bytes32 claimHash, bytes calldata allocatorData) - private - view - returns (bool valid, uint128 targetBlock, uint120 maximumBlocksAfterTarget) - { - assembly ("memory-safe") { - mstore(0x00, claimHash) - mstore(0x20, claims.slot) - let claimSlot := keccak256(0x00, 0x40) - let data := sload(claimSlot) - - valid := and(data, _ACTIVE_CLAIM_MASK) - let storedTargetBlock := shr(57, data) - let storedMaximumBlocksAfterTarget := shr(200, shl(199, data)) - - targetBlock := calldataload(allocatorData.offset) - maximumBlocksAfterTarget := calldataload(add(allocatorData.offset, 0x20)) - valid := - and( - valid, - and(eq(storedTargetBlock, targetBlock), eq(storedMaximumBlocksAfterTarget, maximumBlocksAfterTarget)) - ) - } + function getCompactWitnessTypeString() external pure returns (string memory) { + return COMPACT_WITH_MANDATE_TYPESTRING; } - function _convertToResolvedCrossChainOrder( - Order calldata orderData, - uint256 fillDeadline, - BatchCompact memory batchCompact - ) private view returns (ResolvedCrossChainOrder memory) { - Output[] memory maxSpent = new Output[](1); - maxSpent[0] = Output({ - token: bytes32(uint256(uint160(orderData.settlementToken))), - amount: type(uint256).max, - recipient: bytes32(uint256(uint160(orderData.recipient))), - chainId: orderData.chainId - }); + /// @inheritdoc IERC7683Allocator + function getNonce(GaslessCrossChainOrder calldata order, address) external view returns (uint256 nonce) { + (, uint32 deposit) = ERC7683AL.decodeOrderData(order.orderData); + deposit = ERC7683AL.sanitizeBool(deposit); - uint256 idsLength = orderData.idsAndAmounts.length; - Output[] memory minReceived = new Output[](idsLength); - for (uint256 i = 0; i < idsLength; i++) { - minReceived[i] = Output({ - token: bytes32(uint256(uint160(orderData.idsAndAmounts[i][0]))), - amount: orderData.minimumAmount, - recipient: _convertAddressToBytes32(orderData.recipient), - chainId: block.chainid - }); + if (deposit == 0) { + // Hybrid Allocator requires a deposit + revert OnlyDepositsAllowed(); } - Mandate memory mandate = Mandate({ - recipient: orderData.recipient, - expires: fillDeadline, - token: orderData.settlementToken, - minimumAmount: orderData.minimumAmount, - baselinePriorityFee: orderData.baselinePriorityFee, - scalingFactor: orderData.scalingFactor, - decayCurve: orderData.decayCurve, - salt: orderData.salt - }); - BatchClaim memory claim = BatchClaim({ - chainId: block.chainid, - compact: batchCompact, - sponsorSignature: '', // No signature required from the sponsor, the claim will be verified via the on chain registration. - allocatorSignature: '' // No signature required from this allocator, it will verify the claim on chain via ERC1271. - }); - - FillInstruction[] memory fillInstructions = new FillInstruction[](1); - fillInstructions[0] = FillInstruction({ - destinationChainId: orderData.chainId, - destinationSettler: _convertAddressToBytes32(orderData.tribunal), - originData: abi.encode( - claim, - mandate, - uint200(bytes25(orderData.qualification >> 1)), - uint56(uint256(orderData.qualification >> 1)) - ) - }); - - return ResolvedCrossChainOrder({ - user: batchCompact.sponsor, - originChainId: block.chainid, - openDeadline: uint32(batchCompact.expires), - fillDeadline: uint32(fillDeadline), - orderId: bytes32(batchCompact.nonce), - maxSpent: maxSpent, - minReceived: minReceived, - fillInstructions: fillInstructions - }); - } - - function _decodeOrderData(bytes calldata orderData, bool isOnChain) - private - pure - returns (Order calldata order, uint256 expires) - { - // orderData includes the OrderData(OnChain/Gasless) struct, and the nested Order struct. - // 0x00: OrderDataOnChain.offset - // 0x20: OrderDataOnChain.order.offset - // 0x40: OrderDataOnChain.expires - - // 0x00: OrderDataGasless.offset - // 0x20: OrderDataGasless.order.offset - - assembly ("memory-safe") { - let l := sub(orderData.length, 0x20) - let s := calldataload(add(orderData.offset, 0x20)) // Relative offset of `orderBytes` from `orderData.offset` and the `OrderData...` struct. - order := add(orderData.offset, add(s, 0x20)) // Add 0x20 since the OrderStruct is within the `OrderData...` struct - if shr(64, or(s, or(l, orderData.offset))) { revert(l, 0x00) } - - expires := mul(calldataload(add(orderData.offset, 0x40)), isOnChain) - } + return nonces + 1; } - function _convertAddressToBytes32(address address_) private pure returns (bytes32) { - return bytes32(uint256(uint160(address_))); + /// @inheritdoc IERC7683Allocator + function createFillerData(address claimant) external pure returns (bytes memory fillerData) { + return abi.encode(claimant); } } diff --git a/src/allocators/lib/ERC7683AllocatorLib.sol b/src/allocators/lib/ERC7683AllocatorLib.sol new file mode 100644 index 0000000..025ac90 --- /dev/null +++ b/src/allocators/lib/ERC7683AllocatorLib.sol @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import {AllocatorLib as AL} from './AllocatorLib.sol'; + +import {BatchCompact, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; +import {Tribunal} from '@uniswap/tribunal/Tribunal.sol'; +import {Fill, Mandate, RecipientCallback} from '@uniswap/tribunal/types/TribunalStructs.sol'; +import { + COMPACT_TYPEHASH_WITH_MANDATE, + COMPACT_WITH_MANDATE_TYPESTRING, + MANDATE_BATCH_COMPACT_TYPEHASH, + MANDATE_FILL_TYPEHASH, + MANDATE_RECIPIENT_CALLBACK_TYPEHASH, + MANDATE_TYPEHASH +} from '@uniswap/tribunal/types/TribunalTypeHashes.sol'; + +import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; + +import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; +import {IERC7683Allocator} from 'src/interfaces/IERC7683Allocator.sol'; + +library ERC7683AllocatorLib { + /// @notice The typehash of the OrderDataOnChain struct + // keccak256("OrderDataOnChain(Order order,uint32 expires) + // Mandate(address adjuster,Mandate_Fill[] fills) + // Mandate_BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Mandate_Lock[] commitments) + // Mandate_Fill(uint256 chainId,address tribunal,uint256 expires,address fillToken,uint256 minimumFillAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] priceCurve,address recipient,Mandate_RecipientCallback[] recipientCallback,bytes32 salt) + // Mandate_Lock(bytes12 lockTag,address token,uint256 amount) + // Mandate_RecipientCallback(uint256 chainId,Mandate_BatchCompact compact,bytes context) + // Order(address arbiter,Lock[] commitments,Mandate mandate)") + bytes32 public constant ORDERDATA_ONCHAIN_TYPEHASH = + 0x66007c95a1c1d0004397bbd6448c24b58a18d5d17e75321cdd119aa3b879d98e; + + /// @notice The typehash of the OrderDataGasless struct + // keccak256("OrderDataGasless(Order order,bool deposit) + // Mandate(address adjuster,Mandate_Fill[] fills) + // Mandate_BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Mandate_Lock[] commitments) + // Mandate_Fill(uint256 chainId,address tribunal,uint256 expires,address fillToken,uint256 minimumFillAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] priceCurve,address recipient,Mandate_RecipientCallback[] recipientCallback,bytes32 salt) + // Mandate_Lock(bytes12 lockTag,address token,uint256 amount) + // Mandate_RecipientCallback(uint256 chainId,Mandate_BatchCompact compact,bytes context) + // Order(address arbiter,Lock[] commitments,Mandate mandate)") + bytes32 public constant ORDERDATA_GASLESS_TYPEHASH = + 0xca948fccb29bd545dea3361927ce0b7d8b680a214439e24af475831131515b4c; + + error InvalidRecipientCallbackLength(); + error InvalidOrderDataType(bytes32 orderDataType, bytes32 expectedOrderDataType); + error InvalidOriginSettler(address originSettler, address expectedOriginSettler); + error InvalidOrderData(bytes orderData); + + function openForPreparation(IOriginSettler.GaslessCrossChainOrder calldata order, bytes calldata sponsorSignature) + internal + view + returns ( + IERC7683Allocator.Order calldata orderData, + uint32 deposit, + bytes32 mandateHash, + IOriginSettler.ResolvedCrossChainOrder memory resolvedOrder + ) + { + // Check if orderDataType is the one expected by the allocator + if (order.orderDataType != ORDERDATA_GASLESS_TYPEHASH) { + revert InvalidOrderDataType(order.orderDataType, ORDERDATA_GASLESS_TYPEHASH); + } + // Check if the originSettler is the allocator + if (order.originSettler != address(this)) { + revert InvalidOriginSettler(order.originSettler, address(this)); + } + + // Decode the orderData + (orderData, deposit) = decodeOrderData(order.orderData); + deposit = sanitizeBool(deposit); + + // Ensure a valid mandate is provided + if (orderData.mandate.fills.length == 0 || order.fillDeadline != orderData.mandate.fills[0].expires) { + revert InvalidOrderData(order.orderData); + } + bytes32[] memory fillHashes; + (mandateHash, fillHashes) = hashMandate(orderData.mandate); + resolvedOrder = + resolveOrder(order.user, order.nonce, order.openDeadline, fillHashes, orderData, sponsorSignature); + } + + function openPreparation(IOriginSettler.OnchainCrossChainOrder calldata order) + internal + pure + returns ( + IERC7683Allocator.Order calldata orderData, + uint32 expires, + bytes32 mandateHash, + bytes32[] memory fillHashes + ) + { + // Check if orderDataType is the one expected by the allocator + if (order.orderDataType != ORDERDATA_ONCHAIN_TYPEHASH) { + revert InvalidOrderDataType(order.orderDataType, ORDERDATA_ONCHAIN_TYPEHASH); + } + + // Decode the orderData + (orderData, expires) = decodeOrderData(order.orderData); + expires = sanitizeUint32(expires); + + // Ensure a valid mandate is provided + if (orderData.mandate.fills.length == 0 || order.fillDeadline != orderData.mandate.fills[0].expires) { + revert InvalidOrderData(order.orderData); + } + + (mandateHash, fillHashes) = hashMandate(orderData.mandate); + } + + function decodeOrderData(bytes calldata orderData) + internal + pure + returns (IERC7683Allocator.Order calldata order, uint32 additionalInput) + { + // orderData includes the OrderData(OnChain/Gasless) struct, and the nested Order struct. + // 0x00: OrderDataOnChain.offset + // 0x20: OrderDataOnChain.order.offset + // 0x40: OrderDataOnChain.expires + + // 0x00: OrderDataGasless.offset + // 0x20: OrderDataGasless.order.offset + + assembly ("memory-safe") { + let l := sub(orderData.length, 0x20) + let s := calldataload(add(orderData.offset, 0x20)) // Relative offset of `orderBytes` from `orderData.offset` and the `OrderData...` struct. + order := add(orderData.offset, add(s, 0x20)) // Add 0x20 since the OrderStruct is within the `OrderData...` struct + if shr(64, or(s, or(l, orderData.offset))) { revert(l, 0x00) } + + additionalInput := calldataload(add(orderData.offset, 0x40)) + } + } + + function resolveOrder( + address sponsor, + uint256 nonce, + uint32 expires, + bytes32[] memory fillHashes, + IERC7683Allocator.Order calldata orderData, + bytes calldata sponsorSignature + ) internal view returns (IOriginSettler.ResolvedCrossChainOrder memory) { + Fill memory mainFill = orderData.mandate.fills[0]; + + IOriginSettler.ResolvedCrossChainOrder memory resolvedOrder = IOriginSettler.ResolvedCrossChainOrder({ + user: sponsor, + originChainId: block.chainid, + openDeadline: expires, + fillDeadline: uint32(mainFill.expires), + orderId: bytes32(nonce), + maxSpent: new IERC7683Allocator.Output[](0), + minReceived: new IERC7683Allocator.Output[](0), + fillInstructions: new IERC7683Allocator.FillInstruction[](0) + }); + + BatchCompact memory compact = BatchCompact({ + arbiter: orderData.arbiter, + sponsor: sponsor, + nonce: nonce, + expires: expires, + commitments: orderData.commitments + }); + + Tribunal.BatchClaim memory claim = Tribunal.BatchClaim({ + chainId: block.chainid, + compact: compact, + sponsorSignature: sponsorSignature, + allocatorSignature: '' // No signature required from this allocator, it will verify the claim on chain via ERC1271. + }); + + IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); + fillInstructions[0] = IOriginSettler.FillInstruction({ + destinationChainId: mainFill.chainId, + destinationSettler: addressToBytes32(mainFill.tribunal), + originData: abi.encode(claim, mainFill, orderData.mandate.adjuster, fillHashes) + }); + resolvedOrder.fillInstructions = fillInstructions; + + IOriginSettler.Output memory spent = IOriginSettler.Output({ + token: addressToBytes32(mainFill.fillToken), + amount: type(uint256).max, + recipient: addressToBytes32(mainFill.recipient), + chainId: mainFill.chainId + }); + IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); + maxSpent[0] = spent; + resolvedOrder.maxSpent = maxSpent; + + resolvedOrder.minReceived = createMinimumReceived(orderData.commitments); + + return resolvedOrder; + } + + function createMinimumReceived(Lock[] calldata commitments) + internal + view + returns (IOriginSettler.Output[] memory) + { + IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](commitments.length); + + for (uint256 i = 0; i < commitments.length; i++) { + IOriginSettler.Output memory received = IOriginSettler.Output({ + token: addressToBytes32(commitments[i].token), + amount: commitments[i].amount, + recipient: bytes32(0), + chainId: block.chainid + }); + minReceived[i] = received; + } + return minReceived; + } + + function hashMandate(Mandate calldata mandate) internal pure returns (bytes32, bytes32[] memory fillHashes) { + fillHashes = new bytes32[](mandate.fills.length); + for (uint256 i = 0; i < mandate.fills.length; i++) { + fillHashes[i] = hashFill(mandate.fills[i]); + } + return ( + keccak256(abi.encode(MANDATE_TYPEHASH, mandate.adjuster, keccak256(abi.encodePacked(fillHashes)))), + fillHashes + ); + } + + function hashFill(Fill calldata fill) internal pure returns (bytes32) { + bytes32 priceCurveHash = keccak256(abi.encodePacked(fill.priceCurve)); + return keccak256( + abi.encode( + MANDATE_FILL_TYPEHASH, + fill.chainId, + fill.tribunal, + fill.expires, + fill.fillToken, + fill.minimumFillAmount, + fill.baselinePriorityFee, + fill.scalingFactor, + priceCurveHash, + fill.recipient, + hashRecipientCallback(fill.recipientCallback), + fill.salt + ) + ); + } + + function hashRecipientCallback(RecipientCallback[] calldata recipientCallback) internal pure returns (bytes32) { + if (recipientCallback.length == 0) { + // empty hash + return 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; + } else if (recipientCallback.length != 1) { + revert InvalidRecipientCallbackLength(); + } + + RecipientCallback calldata callback = recipientCallback[0]; + + return keccak256( + abi.encodePacked( + keccak256( + abi.encode( + MANDATE_RECIPIENT_CALLBACK_TYPEHASH, + callback.chainId, + AL.getClaimHash( + callback.compact.arbiter, + callback.compact.sponsor, + callback.compact.nonce, + callback.compact.expires, + AL.getCommitmentsHash(callback.compact.commitments), + callback.mandateHash, + MANDATE_BATCH_COMPACT_TYPEHASH + ), + callback.context + ) + ) + ) + ); + } + + function addressToBytes32(address input) internal pure returns (bytes32 output) { + assembly ("memory-safe") { + output := shr(96, shl(96, input)) + } + } + + function sanitizeUint32(uint32 value) internal pure returns (uint32) { + assembly ("memory-safe") { + value := shr(224, shl(224, value)) + } + return value; + } + + function sanitizeBool(uint32 value) internal pure returns (uint32) { + assembly ("memory-safe") { + value := iszero(iszero(value)) + } + return value; + } +} diff --git a/src/allocators/types/TribunalStructs.sol b/src/allocators/types/TribunalStructs.sol index 40ab38f..e8c8f4f 100644 --- a/src/allocators/types/TribunalStructs.sol +++ b/src/allocators/types/TribunalStructs.sol @@ -18,15 +18,55 @@ struct BatchClaim { bytes allocatorSignature; // Authorization from the allocator } +// struct Mandate { +// // uint256 chainId; // (implicit arg, included in EIP712 payload). +// // address tribunal; // (implicit arg, included in EIP712 payload). +// address recipient; // Recipient of filled tokens. +// uint256 expires; // Mandate expiration timestamp. +// address token; // Fill token (address(0) for native). +// uint256 minimumAmount; // Minimum fill amount. +// uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in. +// uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline). +// uint256[] decayCurve; // Block durations, fill increases, & claim decreases. +// bytes32 salt; // Replay protection parameter. +// } + +// Parent mandate signed by the sponsor on source chain. Note that the EIP-712 payload differs slightly from the structs declared here (mainly around utilizing full mandates rather than mandate hashes). struct Mandate { - // uint256 chainId; // (implicit arg, included in EIP712 payload). - // address tribunal; // (implicit arg, included in EIP712 payload). - address recipient; // Recipient of filled tokens. - uint256 expires; // Mandate expiration timestamp. - address token; // Fill token (address(0) for native). - uint256 minimumAmount; // Minimum fill amount. + address adjuster; + Fill[] fills; // Arbitrary-length array; note that in EIP-712 payload this is Mandate_Fill +} + +// Mandate_Fill in EIP-712 payload +struct Fill { + uint256 chainId; // Same-chain if value matches chainId(), otherwise cross-chain + address tribunal; // Contract where the fill is performed. + uint256 expires; // Fill expiration timestamp. + address fillToken; // Intermediate fill token (address(0) for native, same address for no action). + uint256 minimumFillAmount; // Minimum fill amount. uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in. uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline). - uint256[] decayCurve; // Block durations, fill increases, & claim decreases. - bytes32 salt; // Replay protection parameter. + uint256[] priceCurve; // Block durations and uint240 additional scaling factors per each duration. + address recipient; // Recipient of the tokens — address(0) or tribunal indicate that funds will be pulled by the directive. + RecipientCallback[] recipientCallback; // Array of length 0 or 1; note that in EIP-712 payload this is Mandate_RecipientCallback[] + bytes32 salt; +} + +// If a callback is specified, tribunal will follow up with a call to the recipient with fill details (including realized fill amount), a new compact and hash of an accompanying mandate, a target chainId, and context +// Note that this does not directly map to the EIP-712 payload (which contains a Mandate_BatchCompact containing the full `Mandate mandate` rather than BatchCompact + mandateHash) +// Mandate_RecipientCallback in EIP-712 payload +struct RecipientCallback { + uint256 chainId; + BatchCompact compact; + bytes32 mandateHash; + bytes context; +} + +// Arguments signed for by adjuster. +struct Adjustment { + // bytes32 claimHash included in EIP-712 payload but not provided as an argument. + uint256 fillIndex; + uint256 targetBlock; + uint256[] supplementalPriceCurve; // Additional scaling factor specified duration on price curve. + bytes32 validityConditions; // Optional value consisting of a number of blocks past the target and a exclusive filler address. } diff --git a/src/interfaces/IERC7683Allocator.sol b/src/interfaces/IERC7683Allocator.sol index f095091..794b44c 100644 --- a/src/interfaces/IERC7683Allocator.sol +++ b/src/interfaces/IERC7683Allocator.sol @@ -5,11 +5,12 @@ pragma solidity ^0.8.27; import {IOriginSettler} from './ERC7683/IOriginSettler.sol'; import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; import {Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; +import {Adjustment, Fill, Mandate, RecipientCallback} from '@uniswap/tribunal/types/TribunalStructs.sol'; interface IERC7683Allocator is IOriginSettler, IAllocator { struct OrderDataOnChain { Order order; // The remaining BatchCompact and Mandate data - uint256 expires; // COMPACT - The time at which the claim expires and the user is able to withdraw their funds. + uint32 expires; // COMPACT - The time at which the claim expires and the user is able to withdraw their funds. } struct OrderDataGasless { @@ -21,35 +22,23 @@ interface IERC7683Allocator is IOriginSettler, IAllocator { struct Order { address arbiter; // COMPACT - The account tasked with verifying and submitting the claim. Lock[] commitments; // COMPACT - The token IDs and amounts to allocate. - uint256 chainId; // MANDATE - (implicit arg, included in EIP712 payload) - address tribunal; // MANDATE - (implicit arg, included in EIP712 payload) - address recipient; // MANDATE - Recipient of settled tokens - // uint256 expires; // MANDATE - Mandate expiration timestamp, which equals the fill deadline - address settlementToken; // MANDATE - Settlement token (address(0) for native) - uint256 minimumAmount; // MANDATE - Minimum settlement amount - uint256 baselinePriorityFee; // MANDATE - Base fee threshold where scaling kicks in - uint256 scalingFactor; // MANDATE - Fee scaling multiplier (1e18 baseline) - uint256[] decayCurve; // MANDATE - Block durations, fill increases, & claim decreases. - bytes32 salt; // MANDATE - Replay protection parameter - bytes32 qualification; // ADDITIONAL INPUT - abi.encodePacked(uint200 targetBlock, uint56 maximumBlocksAfterTarget) The block number at the target chain on which the PGA is executed / the reverse dutch auction starts & blocks after target block that are still fillable. + Mandate mandate; // MANDATE - Mandate struct fom tribunal } error InvalidOriginSettler(address originSettler, address expectedOriginSettler); error InvalidOrderDataType(bytes32 orderDataType, bytes32 expectedOrderDataType); error InvalidNonce(uint256 nonce, uint256 expectedNonce); - error BatchCompactsNotSupported(); error InvalidAllocatorData(bytes32 expectedAllocatorData, bytes32 actualAllocatorData); error UnsupportedToken(address token); + error InvalidOrderData(bytes orderData); + error InvalidRecipientCallbackLength(); /// @notice Returns the type string of the compact including the witness function getCompactWitnessTypeString() external pure returns (string memory); - /// @notice Checks if a nonce is free to be used + /// @notice Returns the nonce for a given order and caller /// @dev The nonce is the most significant 96 bits. The least significant 160 bits must be the sponsor address - function checkNonce(GaslessCrossChainOrder calldata order_, address caller) - external - view - returns (bool nonceFree_); + function getNonce(GaslessCrossChainOrder calldata order_, address caller) external view returns (uint256 nonce); /// @notice Creates the filler data for the open event to be used on the IDestinationSettler /// @param claimant_ The address claiming the origin tokens after a successful fill (typically the address of the filler) From 1208bcb1bb39591914d148139be7d407d4833c3a Mon Sep 17 00:00:00 2001 From: mgretzke Date: Fri, 22 Aug 2025 19:12:50 +0200 Subject: [PATCH 60/63] fixes and updated tests --- .gitmodules | 1 - lib/tribunal | 1 + snapshots/ERC7683Allocator_open.json | 4 +- snapshots/ERC7683Allocator_openFor.json | 5 +- snapshots/OnChainAllocatorTest.json | 14 +- src/allocators/ERC7683Allocator.sol | 29 +- src/allocators/HybridERC7683.sol | 24 +- src/allocators/OnChainAllocator.sol | 15 +- src/allocators/types/TribunalStructs.sol | 72 -- src/interfaces/IHybridAllocator.sol | 1 - src/interfaces/IHybridERC7683.sol | 37 - test/ERC7683Allocator.t.sol | 1099 +++++----------------- test/HybridERC7683.t.sol | 729 ++++---------- test/OnChainAllocator.t.sol | 133 +-- test/util/ERC7683TestHelper.sol | 427 +++++++++ 15 files changed, 960 insertions(+), 1631 deletions(-) create mode 160000 lib/tribunal delete mode 100644 src/allocators/types/TribunalStructs.sol delete mode 100644 src/interfaces/IHybridERC7683.sol create mode 100644 test/util/ERC7683TestHelper.sol diff --git a/.gitmodules b/.gitmodules index 0351d97..831c842 100644 --- a/.gitmodules +++ b/.gitmodules @@ -20,4 +20,3 @@ [submodule "lib/tribunal"] path = lib/tribunal url = https://github.com/Uniswap/tribunal - branch = new-mandate diff --git a/lib/tribunal b/lib/tribunal new file mode 160000 index 0000000..21b6e80 --- /dev/null +++ b/lib/tribunal @@ -0,0 +1 @@ +Subproject commit 21b6e8096d8d940789eeb63c5d281e490fd63025 diff --git a/snapshots/ERC7683Allocator_open.json b/snapshots/ERC7683Allocator_open.json index a4c31c3..9c7f3d9 100644 --- a/snapshots/ERC7683Allocator_open.json +++ b/snapshots/ERC7683Allocator_open.json @@ -1,3 +1,3 @@ { - "open_simpleOrder": "184704" -} + "open_simpleOrder": "168301" +} \ No newline at end of file diff --git a/snapshots/ERC7683Allocator_openFor.json b/snapshots/ERC7683Allocator_openFor.json index 4988ce8..8180834 100644 --- a/snapshots/ERC7683Allocator_openFor.json +++ b/snapshots/ERC7683Allocator_openFor.json @@ -1,4 +1,3 @@ { - "openFor_simpleOrder_relayed": "166911", - "openFor_simpleOrder_userHimself": "166943" -} + "openFor_simpleOrder_userHimself": "171902" +} \ No newline at end of file diff --git a/snapshots/OnChainAllocatorTest.json b/snapshots/OnChainAllocatorTest.json index 6553d18..ec5af8f 100644 --- a/snapshots/OnChainAllocatorTest.json +++ b/snapshots/OnChainAllocatorTest.json @@ -1,9 +1,9 @@ { - "allocateFor_success_withRegistration": "133994", - "allocate_and_delete_expired_allocation": "66140", - "allocate_erc20": "129411", - "allocate_native": "129171", - "allocate_second_erc20": "97423", - "onchain_execute_double": "348227", - "onchain_execute_single": "220217" + "allocateFor_success_withRegistration": "133753", + "allocate_and_delete_expired_allocation": "65921", + "allocate_erc20": "129192", + "allocate_native": "128952", + "allocate_second_erc20": "97204", + "onchain_execute_double": "347707", + "onchain_execute_single": "219699" } \ No newline at end of file diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index eaf1758..cf4cb55 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -12,6 +12,7 @@ import {Tribunal} from '@uniswap/tribunal/Tribunal.sol'; import {Fill, Mandate, RecipientCallback} from '@uniswap/tribunal/types/TribunalStructs.sol'; import {IAllocator} from '@uniswap/the-compact/interfaces/IAllocator.sol'; +import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; import { COMPACT_TYPEHASH_WITH_MANDATE, COMPACT_WITH_MANDATE_TYPESTRING, @@ -43,7 +44,7 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { uint160 caller = uint160(deposit * uint160(msg.sender)); // for a deposit, the nonce will be scoped to the caller // Early revert if the expected nonce is not the next nonce - if (order.nonce != _getNonce(address(caller), order.user)) { + if (deposit == 0 && order.nonce != _getNonce(address(caller), order.user)) { revert InvalidNonce(order.nonce, _getNonce(address(caller), order.user)); } @@ -60,7 +61,7 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { sponsorSignature ); } else { - // Register the allocation on chain + // Register the allocation on chain by using a deposit uint256[] memory registeredAmounts; (, registeredAmounts, nonce) = allocateAndRegister( order.user, @@ -71,6 +72,10 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { mandateHash ); + // We ignore the order.nonce and use the one assigned by the hybrid allocator + resolvedOrder.orderId = bytes32(nonce); + + // Update the resolved order with the registered amounts for (uint256 i = 0; i < orderData.commitments.length; i++) { resolvedOrder.minReceived[i].amount = registeredAmounts[i]; } @@ -85,9 +90,14 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { ERC7683AL.openPreparation(order); // Register the allocation on chain - (, uint256 nonce) = + (bytes32 claimHash, uint256 nonce) = allocate(orderData.commitments, orderData.arbiter, expires, COMPACT_TYPEHASH_WITH_MANDATE, mandateHash); + // Ensure a registration exists before opening the order + if (!ITheCompact(COMPACT_CONTRACT).isRegistered(msg.sender, claimHash, COMPACT_TYPEHASH_WITH_MANDATE)) { + revert InvalidRegistration(msg.sender, claimHash); + } + ResolvedCrossChainOrder memory resolvedOrder = ERC7683AL.resolveOrder(msg.sender, nonce, expires, fillHashes, orderData, LibBytes.emptyCalldata()); @@ -106,7 +116,7 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { uint160 caller = uint160(deposit * uint160(msg.sender)); // for a deposit, the nonce will be scoped to the caller + user - // Early revert if the expected nonce is not the next nonce + // Revert if the expected nonce is not the next nonce if (order.nonce != _getNonce(address(caller), order.user)) { revert InvalidNonce(order.nonce, _getNonce(address(caller), order.user)); } @@ -143,15 +153,4 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { function createFillerData(address claimant) external pure returns (bytes memory fillerData) { return abi.encode(claimant); } - - function _getNonce(address calling, address sponsor) internal view returns (uint256 nonce) { - assembly ("memory-safe") { - calling := xor(calling, mul(sponsor, iszero(calling))) - mstore(0x00, calling) - mstore(0x20, nonces.slot) - let nonceSlot := keccak256(0x00, 0x40) - let nonce96 := sload(nonceSlot) - nonce := or(shl(96, calling), add(nonce96, 1)) - } - } } diff --git a/src/allocators/HybridERC7683.sol b/src/allocators/HybridERC7683.sol index a4bcbfc..1b97dc3 100644 --- a/src/allocators/HybridERC7683.sol +++ b/src/allocators/HybridERC7683.sol @@ -37,14 +37,6 @@ contract HybridERC7683 is HybridAllocator, IERC7683Allocator { IOriginSettler.ResolvedCrossChainOrder memory resolvedOrder ) = ERC7683AL.openForPreparation(order, sponsorSignature); - uint160 caller = uint160(deposit * uint160(msg.sender)); // for a deposit, the nonce will be scoped to the caller - - // Early revert if the expected nonce is not the next nonce - if (order.nonce != nonces + 1) { - revert InvalidNonce(order.nonce, nonces + 1); - } - - uint256 nonce; if (deposit == 0) { // Hybrid Allocator requires a deposit revert OnlyDepositsAllowed(); @@ -56,9 +48,8 @@ contract HybridERC7683 is HybridAllocator, IERC7683Allocator { idsAndAmounts[i][1] = orderData.commitments[i].amount; } - // Register the allocation on chain - uint256[] memory registeredAmounts; - (, registeredAmounts, nonce) = allocateAndRegister( + // Register the allocation on chain by using a deposit + (, uint256[] memory registeredAmounts, uint256 nonce) = allocateAndRegister( order.user, idsAndAmounts, orderData.arbiter, @@ -67,12 +58,17 @@ contract HybridERC7683 is HybridAllocator, IERC7683Allocator { mandateHash ); + // We ignore the order.nonce and use the one assigned by the hybrid allocator + resolvedOrder.orderId = bytes32(nonce); + + // Update the resolved order with the registered amounts for (uint256 i = 0; i < orderData.commitments.length; i++) { resolvedOrder.minReceived[i].amount = registeredAmounts[i]; } + + // Emit an open event + emit Open(bytes32(nonce), resolvedOrder); } - // Emit an open event - emit Open(bytes32(nonce), resolvedOrder); } /// @inheritdoc IOriginSettler @@ -88,7 +84,7 @@ contract HybridERC7683 is HybridAllocator, IERC7683Allocator { } // deposit the the tokens into the compact and register the claim - (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = allocateAndRegister( + (, uint256[] memory registeredAmounts, uint256 nonce) = allocateAndRegister( msg.sender, idsAndAmounts, orderData.arbiter, expires, COMPACT_TYPEHASH_WITH_MANDATE, mandateHash ); ResolvedCrossChainOrder memory resolvedOrder = diff --git a/src/allocators/OnChainAllocator.sol b/src/allocators/OnChainAllocator.sol index c2f8c09..ac0fe19 100644 --- a/src/allocators/OnChainAllocator.sol +++ b/src/allocators/OnChainAllocator.sol @@ -23,7 +23,7 @@ contract OnChainAllocator is IOnChainAllocator { mapping(bytes32 tokenHash => Allocation[] allocations) internal _allocations; - mapping(bytes32 user => uint96 nonce) public nonces; + mapping(address user => uint96 nonce) public nonces; modifier onlyCompact() { if (msg.sender != COMPACT_CONTRACT) { @@ -157,7 +157,7 @@ contract OnChainAllocator is IOnChainAllocator { bytes calldata /* orderData */ ) external returns (uint256 nonce) { uint32 expiration = uint32(expires); - nonce = _getAndUpdateNonce(msg.sender, recipient); + nonce = _getNonce(msg.sender, recipient); AL.prepareAllocation(COMPACT_CONTRACT, nonce, recipient, idsAndAmounts, arbiter, expiration, typehash, witness); return nonce; @@ -479,6 +479,17 @@ contract OnChainAllocator is IOnChainAllocator { } } + function _getNonce(address calling, address sponsor) internal view returns (uint256 nonce) { + assembly ("memory-safe") { + calling := xor(calling, mul(sponsor, iszero(calling))) + mstore(0x00, calling) + mstore(0x20, nonces.slot) + let nonceSlot := keccak256(0x00, 0x40) + let nonce96 := sload(nonceSlot) + nonce := or(shl(96, calling), add(nonce96, 1)) + } + } + function _getTokenHash(bytes12 lockTag, address token, address sponsor) internal pure returns (bytes32 tokenHash) { assembly ("memory-safe") { mstore(0x00, lockTag) diff --git a/src/allocators/types/TribunalStructs.sol b/src/allocators/types/TribunalStructs.sol deleted file mode 100644 index e8c8f4f..0000000 --- a/src/allocators/types/TribunalStructs.sol +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.27; - -import {BatchCompact, Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; - -struct Claim { - uint256 chainId; // Claim processing chain ID - Compact compact; - bytes sponsorSignature; // Authorization from the sponsor - bytes allocatorSignature; // Authorization from the allocator -} - -struct BatchClaim { - uint256 chainId; // Claim processing chain ID - BatchCompact compact; - bytes sponsorSignature; // Authorization from the sponsor - bytes allocatorSignature; // Authorization from the allocator -} - -// struct Mandate { -// // uint256 chainId; // (implicit arg, included in EIP712 payload). -// // address tribunal; // (implicit arg, included in EIP712 payload). -// address recipient; // Recipient of filled tokens. -// uint256 expires; // Mandate expiration timestamp. -// address token; // Fill token (address(0) for native). -// uint256 minimumAmount; // Minimum fill amount. -// uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in. -// uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline). -// uint256[] decayCurve; // Block durations, fill increases, & claim decreases. -// bytes32 salt; // Replay protection parameter. -// } - -// Parent mandate signed by the sponsor on source chain. Note that the EIP-712 payload differs slightly from the structs declared here (mainly around utilizing full mandates rather than mandate hashes). -struct Mandate { - address adjuster; - Fill[] fills; // Arbitrary-length array; note that in EIP-712 payload this is Mandate_Fill -} - -// Mandate_Fill in EIP-712 payload -struct Fill { - uint256 chainId; // Same-chain if value matches chainId(), otherwise cross-chain - address tribunal; // Contract where the fill is performed. - uint256 expires; // Fill expiration timestamp. - address fillToken; // Intermediate fill token (address(0) for native, same address for no action). - uint256 minimumFillAmount; // Minimum fill amount. - uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in. - uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline). - uint256[] priceCurve; // Block durations and uint240 additional scaling factors per each duration. - address recipient; // Recipient of the tokens — address(0) or tribunal indicate that funds will be pulled by the directive. - RecipientCallback[] recipientCallback; // Array of length 0 or 1; note that in EIP-712 payload this is Mandate_RecipientCallback[] - bytes32 salt; -} - -// If a callback is specified, tribunal will follow up with a call to the recipient with fill details (including realized fill amount), a new compact and hash of an accompanying mandate, a target chainId, and context -// Note that this does not directly map to the EIP-712 payload (which contains a Mandate_BatchCompact containing the full `Mandate mandate` rather than BatchCompact + mandateHash) -// Mandate_RecipientCallback in EIP-712 payload -struct RecipientCallback { - uint256 chainId; - BatchCompact compact; - bytes32 mandateHash; - bytes context; -} - -// Arguments signed for by adjuster. -struct Adjustment { - // bytes32 claimHash included in EIP-712 payload but not provided as an argument. - uint256 fillIndex; - uint256 targetBlock; - uint256[] supplementalPriceCurve; // Additional scaling factor specified duration on price curve. - bytes32 validityConditions; // Optional value consisting of a number of blocks past the target and a exclusive filler address. -} diff --git a/src/interfaces/IHybridAllocator.sol b/src/interfaces/IHybridAllocator.sol index d7f7a19..998e415 100644 --- a/src/interfaces/IHybridAllocator.sol +++ b/src/interfaces/IHybridAllocator.sol @@ -8,7 +8,6 @@ interface IHybridAllocator is IOnChainAllocation { error InvalidIds(); error InvalidAllocatorId(uint96 allocatorId, uint96 expectedAllocatorId); error InvalidCaller(address sender, address expectedSender); - error InvalidAllocatorData(uint256 length); error InvalidSignature(); error InvalidSigner(); error LastSigner(); diff --git a/src/interfaces/IHybridERC7683.sol b/src/interfaces/IHybridERC7683.sol deleted file mode 100644 index 07a0958..0000000 --- a/src/interfaces/IHybridERC7683.sol +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.27; - -import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; -import {IHybridAllocator} from 'src/interfaces/IHybridAllocator.sol'; - -interface IHybridERC7683 is IHybridAllocator, IOriginSettler { - struct OrderDataOnChain { - Order order; // The remaining BatchCompact and Mandate data - uint256 expires; // COMPACT - The time at which the claim expires and the user is able to withdraw their funds. - } - - struct OrderDataGasless { - Order order; // The remaining BatchCompact and Mandate data - } - - /// @dev The data that OnChain and Gasless orders have in common - struct Order { - address arbiter; // COMPACT - The account tasked with verifying and submitting the claim. - uint256[2][] idsAndAmounts; // COMPACT - The token IDs and amounts to allocate. - uint256 chainId; // MANDATE - (implicit arg, included in EIP712 payload) - address tribunal; // MANDATE - (implicit arg, included in EIP712 payload) - address recipient; // MANDATE - Recipient of settled tokens - // uint256 expires; // MANDATE - Mandate expiration timestamp, which equals the fill deadline - address settlementToken; // MANDATE - Settlement token (address(0) for native) - uint256 minimumAmount; // MANDATE - Minimum settlement amount - uint256 baselinePriorityFee; // MANDATE - Base fee threshold where scaling kicks in - uint256 scalingFactor; // MANDATE - Fee scaling multiplier (1e18 baseline) - uint256[] decayCurve; // MANDATE - Block durations, fill increases, & claim decreases. - bytes32 salt; // MANDATE - Replay protection parameter - bytes32 qualification; // ADDITIONAL INPUT - [uint199 targetBlock, uint56 maximumBlocksAfterTarget, uint1(0)] The block number at the target chain on which the PGA is executed / the reverse dutch auction starts & blocks after target block that are still fillable. - } - - error InvalidOrderDataType(bytes32 orderDataType, bytes32 expectedOrderDataType); - error InvalidOriginSettler(address originSettler, address expectedOriginSettler); - error InvalidQualification(bytes32 qualification); -} diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index 4aef2e9..f71de5b 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -20,418 +20,44 @@ import {Test} from 'forge-std/Test.sol'; import {TestHelper} from 'test/util/TestHelper.sol'; import {IOnChainAllocation} from '@uniswap/the-compact/interfaces/IOnChainAllocation.sol'; + +import {Tribunal} from '@uniswap/tribunal/Tribunal.sol'; +import {Fill, Mandate, RecipientCallback} from '@uniswap/tribunal/types/TribunalStructs.sol'; +import { + COMPACT_TYPEHASH_WITH_MANDATE, + MANDATE_BATCH_COMPACT_TYPEHASH, + MANDATE_FILL_TYPEHASH, + MANDATE_RECIPIENT_CALLBACK_TYPEHASH, + MANDATE_TYPEHASH, + WITNESS_TYPESTRING as WITNESS_TYPESTRING_TRIBUNAL +} from '@uniswap/tribunal/types/TribunalTypeHashes.sol'; import {ERC7683Allocator} from 'src/allocators/ERC7683Allocator.sol'; -import {BatchClaim as TribunalClaim, Mandate} from 'src/allocators/types/TribunalStructs.sol'; +import {ERC7683AllocatorLib as ERC7683AL} from 'src/allocators/lib/ERC7683AllocatorLib.sol'; import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; import {IERC7683Allocator} from 'src/interfaces/IERC7683Allocator.sol'; import {IOnChainAllocator} from 'src/interfaces/IOnChainAllocator.sol'; import {ERC20Mock} from 'src/test/ERC20Mock.sol'; -abstract contract MocksSetup is Test, TestHelper { - address user; - uint256 userPK; - address attacker; - uint256 attackerPK; - address arbiter; - address tribunal; - ERC20Mock usdc; - TheCompact compactContract; - ERC7683Allocator erc7683Allocator; - bytes12 usdcLockTag; - uint256 usdcId; - - ResetPeriod defaultResetPeriod = ResetPeriod.OneMinute; - Scope defaultScope = Scope.Multichain; - uint256 defaultResetPeriodTimestamp = 60 - 1; - uint256 defaultAmount = 1000; - uint256 defaultNonce; - uint256 defaultOutputChainId = 130; - address defaultOutputToken = makeAddr('outputToken'); - uint256 defaultMinimumAmount = 1000; - uint256 defaultBaselinePriorityFee = 0; - uint256 defaultScalingFactor = 0; - uint256[] defaultDecayCurve = new uint256[](0); - bytes32 defaultSalt = bytes32(0x0000000000000000000000000000000000000000000000000000000000000007); - uint200 defaultTargetBlock = 100; - uint56 defaultMaximumBlocksAfterTarget = 10; - - uint256[2][] defaultIdsAndAmounts = new uint256[2][](1); - Lock[] defaultCommitments; - - bytes32 ORDERDATA_GASLESS_TYPEHASH; - bytes32 ORDERDATA_ONCHAIN_TYPEHASH; - - uint256 NONCES_STORAGE_SLOT = 1; - - function setUp() public virtual { - (user, userPK) = makeAddrAndKey('user'); - arbiter = makeAddr('arbiter'); - tribunal = makeAddr('tribunal'); - usdc = new ERC20Mock('USDC', 'USDC'); - compactContract = new TheCompact(); - erc7683Allocator = new ERC7683Allocator(address(compactContract)); - - usdcLockTag = _toLockTag(address(erc7683Allocator), defaultScope, defaultResetPeriod); - usdcId = _toId(defaultScope, defaultResetPeriod, address(erc7683Allocator), address(usdc)); - (attacker, attackerPK) = makeAddrAndKey('attacker'); - defaultNonce = 1; - - ORDERDATA_GASLESS_TYPEHASH = keccak256( - 'OrderDataGasless(Order order,bool deposit)Lock(bytes12 lockTag,address token,uint256 amount)Order(address arbiter,Lock[] commitments,uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt,bytes32 qualification)' - ); - ORDERDATA_ONCHAIN_TYPEHASH = keccak256( - 'OrderDataOnChain(Order order,uint256 expires)Lock(bytes12 lockTag,address token,uint256 amount)Order(address arbiter,Lock[] commitments,uint256 chainId,address tribunal,address recipient,address settlementToken,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt,bytes32 qualification)' - ); - } -} - -abstract contract CreateHash is MocksSetup { - struct Allocator { - bytes32 hash; - } - - // stringified types - string EIP712_DOMAIN_TYPE = 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'; // Hashed inside the function - // EIP712 domain type - string name = 'The Compact'; - string version = '1'; - - string compactWitnessTypeString = - 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,bytes12 lockTag,address token,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; - string batchCompactWitnessTypeString = - 'BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments,Mandate mandate)Lock(bytes12 lockTag,address token,uint256 amount)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; - string mandateTypeString = - 'Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; - string witnessTypeString = - 'uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt'; - - function _hashCompact(BatchCompact memory data, Mandate memory mandate, address verifyingContract) - internal - view - returns (bytes32 digest) - { - bytes32 compactHash = _hashCompact(data, mandate); - // hash typed data - digest = keccak256( - abi.encodePacked( - '\x19\x01', // backslash is needed to escape the character - _domainSeparator(verifyingContract), - compactHash - ) - ); - } - - function _hashCompact(BatchCompact memory data, Mandate memory mandate) - internal - view - returns (bytes32 compactHash) - { - bytes32 mandateHash = _hashMandate(mandate); - compactHash = keccak256( - abi.encode( - keccak256(bytes(batchCompactWitnessTypeString)), - data.arbiter, - data.sponsor, - data.nonce, - data.expires, - _hashCommitments(data.commitments), - mandateHash - ) - ); - } - - function _hashMandate(Mandate memory mandate) internal view returns (bytes32) { - return keccak256( - abi.encode( - keccak256(bytes(mandateTypeString)), - defaultOutputChainId, - tribunal, - mandate.recipient, - mandate.expires, - mandate.token, - mandate.minimumAmount, - mandate.baselinePriorityFee, - mandate.scalingFactor, - keccak256(abi.encodePacked(mandate.decayCurve)), - mandate.salt - ) - ); - } - - function _hashCommitments(Lock[] memory commitments) internal pure returns (bytes32) { - bytes32[] memory hashes = new bytes32[](commitments.length); - for (uint256 i = 0; i < commitments.length; i++) { - hashes[i] = keccak256( - abi.encode(LOCK_TYPEHASH, commitments[i].lockTag, commitments[i].token, commitments[i].amount) - ); - } - return keccak256(abi.encodePacked(hashes)); - } - - function _getTypeHash() internal view returns (bytes32) { - return keccak256(bytes(batchCompactWitnessTypeString)); - } - - function _domainSeparator(address verifyingContract) internal view returns (bytes32) { - return keccak256( - abi.encode( - keccak256(bytes(EIP712_DOMAIN_TYPE)), - keccak256(bytes(name)), - keccak256(bytes(version)), - block.chainid, - verifyingContract - ) - ); - } - - function _signMessage(bytes32 hash_, uint256 signerPK_) internal pure returns (bytes memory) { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPK_, hash_); - return abi.encodePacked(r, s, v); - } - - function _hashAndSign(BatchCompact memory data, Mandate memory mandate, address verifyingContract, uint256 signerPK) - internal - view - returns (bytes memory) - { - bytes32 hash = _hashCompact(data, mandate, verifyingContract); - bytes memory signature = _signMessage(hash, signerPK); - return signature; - } -} - -abstract contract CompactData is CreateHash { - BatchCompact private compact; - Mandate private mandate; - - function setUp() public virtual override { - super.setUp(); - - defaultIdsAndAmounts[0][0] = usdcId; - defaultIdsAndAmounts[0][1] = defaultAmount; - - defaultCommitments.push(Lock({lockTag: usdcLockTag, token: address(usdc), amount: defaultAmount})); - - compact.arbiter = arbiter; - compact.sponsor = user; - compact.nonce = defaultNonce; - compact.expires = _getClaimExpiration(); - compact.commitments = defaultCommitments; - - mandate.recipient = user; - mandate.expires = _getFillExpiration(); - mandate.token = defaultOutputToken; - mandate.minimumAmount = defaultMinimumAmount; - mandate.baselinePriorityFee = defaultBaselinePriorityFee; - mandate.scalingFactor = defaultScalingFactor; - mandate.decayCurve = defaultDecayCurve; - mandate.salt = defaultSalt; - } - - function _getCompact() internal returns (BatchCompact memory) { - compact.expires = _getClaimExpiration(); - return compact; - } - - function _getMandate() internal returns (Mandate memory) { - mandate.expires = _getFillExpiration(); - return mandate; - } - - function _getFillExpiration() internal view returns (uint256) { - return vm.getBlockTimestamp() + defaultResetPeriodTimestamp - 1; - } - - function _getClaimExpiration() internal view returns (uint256) { - return vm.getBlockTimestamp() + defaultResetPeriodTimestamp; - } -} - -abstract contract GaslessCrossChainOrderData is CompactData { - IOriginSettler.GaslessCrossChainOrder private gaslessCrossChainOrder; - - function setUp() public virtual override { - super.setUp(); - - BatchCompact memory compact_ = _getCompact(); - Mandate memory mandate_ = _getMandate(); - - gaslessCrossChainOrder.originSettler = address(erc7683Allocator); - gaslessCrossChainOrder.user = compact_.sponsor; - gaslessCrossChainOrder.nonce = compact_.nonce; - gaslessCrossChainOrder.originChainId = block.chainid; - gaslessCrossChainOrder.openDeadline = uint32(_getClaimExpiration()); - gaslessCrossChainOrder.fillDeadline = uint32(_getFillExpiration()); - gaslessCrossChainOrder.orderDataType = erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH(); - gaslessCrossChainOrder.orderData = abi.encode( - IERC7683Allocator.OrderDataGasless({ - order: IERC7683Allocator.Order({ - arbiter: compact_.arbiter, - commitments: compact_.commitments, - chainId: defaultOutputChainId, - tribunal: tribunal, - recipient: mandate_.recipient, - settlementToken: mandate_.token, - minimumAmount: mandate_.minimumAmount, - baselinePriorityFee: mandate_.baselinePriorityFee, - scalingFactor: mandate_.scalingFactor, - decayCurve: mandate_.decayCurve, - salt: mandate_.salt, - qualification: bytes32(0) - }), - deposit: false - }) - ); - } - - function _getGaslessCrossChainOrder( - address allocator, - BatchCompact memory compact_, - Mandate memory mandate_, - uint256 chainId_, - bytes32 orderDataGaslessTypeHash_, - address verifyingContract, - uint256 signerPK, - bytes32 qualification - ) internal view returns (IOriginSettler.GaslessCrossChainOrder memory, bytes memory signature) { - IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_ = IOriginSettler.GaslessCrossChainOrder({ - originSettler: allocator, - user: compact_.sponsor, - nonce: compact_.nonce, - originChainId: chainId_, - openDeadline: uint32(compact_.expires), - fillDeadline: uint32(mandate_.expires), - orderDataType: orderDataGaslessTypeHash_, - orderData: abi.encode( - IERC7683Allocator.OrderDataGasless({ - order: IERC7683Allocator.Order({ - arbiter: compact_.arbiter, - commitments: compact_.commitments, - chainId: defaultOutputChainId, - tribunal: tribunal, - recipient: mandate_.recipient, - settlementToken: mandate_.token, - minimumAmount: mandate_.minimumAmount, - baselinePriorityFee: mandate_.baselinePriorityFee, - scalingFactor: mandate_.scalingFactor, - decayCurve: mandate_.decayCurve, - salt: mandate_.salt, - qualification: qualification - }), - deposit: false - }) - ) - }); - - (bytes memory signature_) = _hashAndSign(compact_, mandate_, verifyingContract, signerPK); - return (gaslessCrossChainOrder_, signature_); - } - - function _getGaslessCrossChainOrder() - internal - returns (IOriginSettler.GaslessCrossChainOrder memory, bytes memory signature) - { - (bytes memory signature_) = _hashAndSign(_getCompact(), _getMandate(), address(compactContract), userPK); - return (gaslessCrossChainOrder, signature_); - } - - function _manipulateDeposit(IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bool deposit) - internal - pure - returns (IOriginSettler.GaslessCrossChainOrder memory) - { - bytes memory orderData = gaslessCrossChainOrder_.orderData; - assembly ("memory-safe") { - mstore(add(orderData, 0x60), deposit) - } - return gaslessCrossChainOrder_; - } -} - -abstract contract OnChainCrossChainOrderData is CompactData { - IOriginSettler.OnchainCrossChainOrder private onchainCrossChainOrder; - - function setUp() public virtual override { - super.setUp(); - - BatchCompact memory compact_ = _getCompact(); - Mandate memory mandate_ = _getMandate(); - - onchainCrossChainOrder.fillDeadline = uint32(_getFillExpiration()); - onchainCrossChainOrder.orderDataType = erc7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH(); - onchainCrossChainOrder.orderData = abi.encode( - IERC7683Allocator.OrderDataOnChain({ - order: IERC7683Allocator.Order({ - arbiter: compact_.arbiter, - commitments: compact_.commitments, - chainId: defaultOutputChainId, - tribunal: tribunal, - recipient: mandate_.recipient, - settlementToken: mandate_.token, - minimumAmount: mandate_.minimumAmount, - baselinePriorityFee: mandate_.baselinePriorityFee, - scalingFactor: mandate_.scalingFactor, - decayCurve: mandate_.decayCurve, - salt: mandate_.salt, - qualification: bytes32(abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget)) - }), - expires: compact_.expires - }) - ); - } +import { + CompactData, + GaslessCrossChainOrderData, + MocksSetup, + OnChainCrossChainOrderData +} from 'test/util/ERC7683TestHelper.sol'; - function _getOnChainCrossChainOrder() internal view returns (IOriginSettler.OnchainCrossChainOrder memory) { - return onchainCrossChainOrder; - } - - function _getOnChainCrossChainOrder(BatchCompact memory compact_, Mandate memory mandate_, bytes32 orderDataType_) - internal - view - returns (IOriginSettler.OnchainCrossChainOrder memory) - { - IOriginSettler.OnchainCrossChainOrder memory onchainCrossChainOrder_ = IOriginSettler.OnchainCrossChainOrder({ - fillDeadline: uint32(mandate_.expires), - orderDataType: orderDataType_, - orderData: abi.encode( - IERC7683Allocator.OrderDataOnChain({ - order: IERC7683Allocator.Order({ - arbiter: compact_.arbiter, - commitments: compact_.commitments, - chainId: defaultOutputChainId, - tribunal: tribunal, - recipient: mandate_.recipient, - settlementToken: mandate_.token, - minimumAmount: mandate_.minimumAmount, - baselinePriorityFee: mandate_.baselinePriorityFee, - scalingFactor: mandate_.scalingFactor, - decayCurve: mandate_.decayCurve, - salt: mandate_.salt, - qualification: bytes32(abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget)) - }), - expires: compact_.expires - }) - ) - }); - return onchainCrossChainOrder_; - } -} +contract MockAllocator is GaslessCrossChainOrderData, OnChainCrossChainOrderData { + ERC7683Allocator erc7683Allocator; -abstract contract Deposited is MocksSetup { - function setUp() public virtual override { + function setUp() public virtual override(GaslessCrossChainOrderData, OnChainCrossChainOrderData) { + TheCompact compactContract_ = new TheCompact(); + erc7683Allocator = new ERC7683Allocator(address(compactContract_)); + _setUp(address(erc7683Allocator), compactContract_, _composeNonceUint(user, 1)); super.setUp(); - - vm.startPrank(user); - - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); - compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); - - vm.stopPrank(); } } -contract ERC7683Allocator_open is OnChainCrossChainOrderData { +contract ERC7683Allocator_open is MockAllocator { function test_revert_InvalidOrderDataType() public { // Order data type is invalid bytes32 falseOrderDataType = keccak256('false'); @@ -441,32 +67,26 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { vm.prank(user); vm.expectRevert( abi.encodeWithSelector( - IERC7683Allocator.InvalidOrderDataType.selector, - falseOrderDataType, - erc7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH() + IERC7683Allocator.InvalidOrderDataType.selector, falseOrderDataType, ORDERDATA_ONCHAIN_TYPEHASH ) ); erc7683Allocator.open(onChainCrossChainOrder_); } - function test_orderDataType() public view { - assertEq(erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH(), ORDERDATA_GASLESS_TYPEHASH); - } + // Removed redundant typehash equality check; we compare against library in setup. function test_revert_ManipulatedOrderData() public { // Deposit tokens vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); + (bytes32 mandateHash,) = _hashMandate(mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandateHash); + compactContract.register(claimHash, COMPACT_TYPEHASH_WITH_MANDATE); vm.stopPrank(); @@ -498,19 +118,18 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { function test_revert_InvalidRegistration() public { // we deposit tokens vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // we do NOT register a claim vm.stopPrank(); - (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); - BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - bytes32 claimHash = _hashCompact(compact_, mandate_); + (bytes32 mandateHash,) = _hashMandate(mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandateHash); + (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = + _getOnChainCrossChainOrder(compact_, mandate_); vm.prank(user); vm.expectRevert(abi.encodeWithSelector(IOnChainAllocation.InvalidRegistration.selector, user, claimHash)); @@ -520,17 +139,15 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { function test_successful() public { // Deposit tokens vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); + (bytes32 mHash,) = _hashMandate(mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mHash); + compactContract.register(claimHash, COMPACT_TYPEHASH_WITH_MANDATE); vm.stopPrank(); @@ -550,7 +167,7 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { recipient: '', chainId: block.chainid }); - TribunalClaim memory claim = TribunalClaim({ + Tribunal.BatchClaim memory claim = Tribunal.BatchClaim({ chainId: block.chainid, compact: _getCompact(), sponsorSignature: '', @@ -559,7 +176,7 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, _getMandate(), defaultTargetBlock, defaultMaximumBlocksAfterTarget) + originData: abi.encode(claim, _getMandate().fills[0], adjuster, _buildFillHashes(_getMandate())) }); IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ @@ -573,14 +190,14 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { fillInstructions: fillInstructions }); vm.prank(user); - vm.expectEmit(true, false, false, true, address(erc7683Allocator)); + vm.expectEmit(true, false, false, false, address(erc7683Allocator)); emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); erc7683Allocator.open(onChainCrossChainOrder_); vm.snapshotGasLastCall('open_simpleOrder'); } } -contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { +contract ERC7683Allocator_openFor is MockAllocator { function test_revert_InvalidOrderDataType() public { // Order data type is invalid bytes32 falseOrderDataType = keccak256('false'); @@ -588,29 +205,23 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { vm.prank(user); vm.expectRevert( abi.encodeWithSelector( - IERC7683Allocator.InvalidOrderDataType.selector, - falseOrderDataType, - erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH() + IERC7683Allocator.InvalidOrderDataType.selector, falseOrderDataType, ORDERDATA_GASLESS_TYPEHASH ) ); - (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = - _getGaslessCrossChainOrder(); + IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder = _getGaslessCrossChainOrder(); falseGaslessCrossChainOrder.orderDataType = falseOrderDataType; - erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); + erc7683Allocator.openFor(falseGaslessCrossChainOrder, '', ''); } - function test_orderDataType() public view { - assertEq(erc7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH(), ORDERDATA_ONCHAIN_TYPEHASH); - } + // removed redundant typehash getter check function test_revert_InvalidDecoding() public { // Decoding fails because of additional data vm.prank(user); vm.expectRevert(); - (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = - _getGaslessCrossChainOrder(); + IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder = _getGaslessCrossChainOrder(); falseGaslessCrossChainOrder.orderData = abi.encode(falseGaslessCrossChainOrder.orderData, uint8(1)); - erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); + erc7683Allocator.openFor(falseGaslessCrossChainOrder, '', ''); } function test_revert_InvalidOriginSettler() public { @@ -621,19 +232,10 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { IERC7683Allocator.InvalidOriginSettler.selector, falseOriginSettler, address(erc7683Allocator) ) ); - (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = - _getGaslessCrossChainOrder( - falseOriginSettler, - _getCompact(), - _getMandate(), - block.chainid, - ORDERDATA_GASLESS_TYPEHASH, - address(erc7683Allocator), - userPK, - bytes32(0) - ); + IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder = _getGaslessCrossChainOrder(); + falseGaslessCrossChainOrder.originSettler = falseOriginSettler; vm.prank(user); - erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); + erc7683Allocator.openFor(falseGaslessCrossChainOrder, '', ''); } function test_revert_InvalidNonce(uint256 nonce) public { @@ -642,58 +244,91 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { BatchCompact memory compact_ = _getCompact(); compact_.nonce = nonce; vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidNonce.selector, compact_.nonce, defaultNonce)); - (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = - _getGaslessCrossChainOrder( - address(erc7683Allocator), - compact_, - _getMandate(), - block.chainid, - ORDERDATA_GASLESS_TYPEHASH, - address(erc7683Allocator), - userPK, - bytes32(0) - ); + IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder = _getGaslessCrossChainOrder(); + falseGaslessCrossChainOrder.nonce = nonce; vm.prank(user); - erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); + erc7683Allocator.openFor(falseGaslessCrossChainOrder, '', ''); } - function test_revert_InvalidSponsorSignature() public { - // Sponsor signature is invalid - + function test_successful_userHimself() public { // Deposit tokens vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + (bytes32 mandateHash,) = _hashMandate(mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandateHash); + + // Register the claim to allow to open the order + compactContract.register(claimHash, COMPACT_TYPEHASH_WITH_MANDATE); + vm.stopPrank(); - // Create a malicious signature - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = - _getGaslessCrossChainOrder( - address(erc7683Allocator), - _getCompact(), - _getMandate(), - block.chainid, - ORDERDATA_GASLESS_TYPEHASH, - address(compactContract), - attackerPK, - bytes32(0) - ); + IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_ = + _getGaslessCrossChainOrder(compact_, mandate_, false); + IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); + IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); + IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); + maxSpent[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(defaultOutputToken))), + amount: type(uint256).max, + recipient: bytes32(uint256(uint160(user))), + chainId: defaultOutputChainId + }); + minReceived[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(address(usdc)))), + amount: defaultAmount, + recipient: '', + chainId: block.chainid + }); + Tribunal.BatchClaim memory claim = Tribunal.BatchClaim({ + chainId: block.chainid, + compact: _getCompact(), + sponsorSignature: '', + allocatorSignature: '' + }); + fillInstructions[0] = IOriginSettler.FillInstruction({ + destinationChainId: defaultOutputChainId, + destinationSettler: bytes32(uint256(uint160(tribunal))), + originData: abi.encode(claim, _getMandate(), uint256(0), uint256(0)) + }); + + IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ + user: user, + originChainId: block.chainid, + openDeadline: uint32(_getClaimExpiration()), + fillDeadline: uint32(_getFillExpiration()), + orderId: bytes32(defaultNonce), + maxSpent: maxSpent, + minReceived: minReceived, + fillInstructions: fillInstructions + }); vm.prank(user); - vm.expectRevert(abi.encodeWithSelector(IOnChainAllocator.InvalidSignature.selector, attacker, user)); - erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); + vm.expectEmit(true, false, false, false, address(erc7683Allocator)); + emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); + erc7683Allocator.openFor(gaslessCrossChainOrder_, '', ''); + vm.snapshotGasLastCall('openFor_simpleOrder_userHimself'); } - function test_successful_userHimself() public { + function test_successful_relayed_registration(address filler) public { // Deposit tokens vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + (bytes32 mandateHash,) = _hashMandate(mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandateHash); + + // Register the claim to allow to open the order + compactContract.register(claimHash, COMPACT_TYPEHASH_WITH_MANDATE); + vm.stopPrank(); - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = - _getGaslessCrossChainOrder(); + IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_ = + _getGaslessCrossChainOrder(compact_, mandate_, false); + IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); @@ -709,10 +344,10 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { recipient: '', chainId: block.chainid }); - TribunalClaim memory claim = TribunalClaim({ + Tribunal.BatchClaim memory claim = Tribunal.BatchClaim({ chainId: block.chainid, compact: _getCompact(), - sponsorSignature: sponsorSignature, + sponsorSignature: '', allocatorSignature: '' }); fillInstructions[0] = IOriginSettler.FillInstruction({ @@ -731,23 +366,26 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { minReceived: minReceived, fillInstructions: fillInstructions }); - vm.prank(user); - vm.expectEmit(true, false, false, true, address(erc7683Allocator)); + vm.prank(filler); + vm.expectEmit(true, false, false, false, address(erc7683Allocator)); emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); - erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); - vm.snapshotGasLastCall('openFor_simpleOrder_userHimself'); + erc7683Allocator.openFor(gaslessCrossChainOrder_, '', ''); + vm.snapshotGasLastCall('openFor_simpleOrder_relayed'); } - function test_successful_relayed() public { + function test_successful_relayed_signature(address filler) public { // Deposit tokens vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); vm.stopPrank(); - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = - _getGaslessCrossChainOrder(); + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + bytes memory sponsorSignature = _hashAndSign(compact_, mandate_, address(compactContract), userPK); + + IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_ = + _getGaslessCrossChainOrder(compact_, mandate_, false); + IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); @@ -763,7 +401,7 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { recipient: '', chainId: block.chainid }); - TribunalClaim memory claim = TribunalClaim({ + Tribunal.BatchClaim memory claim = Tribunal.BatchClaim({ chainId: block.chainid, compact: _getCompact(), sponsorSignature: sponsorSignature, @@ -785,8 +423,9 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { minReceived: minReceived, fillInstructions: fillInstructions }); - vm.prank(makeAddr('filler')); - vm.expectEmit(true, false, false, true, address(erc7683Allocator)); + + vm.prank(filler); + vm.expectEmit(true, false, false, false, address(erc7683Allocator)); emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); vm.snapshotGasLastCall('openFor_simpleOrder_relayed'); @@ -796,40 +435,31 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { vm.assume(nonce != defaultNonce); // Deposit tokens vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); vm.stopPrank(); // try to use a future nonce - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder, bytes memory sponsorSignature) = - _getGaslessCrossChainOrder(); + IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder = _getGaslessCrossChainOrder(); gaslessCrossChainOrder.nonce = nonce; vm.prank(user); vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidNonce.selector, nonce, defaultNonce)); - erc7683Allocator.openFor(gaslessCrossChainOrder, sponsorSignature, ''); + erc7683Allocator.openFor(gaslessCrossChainOrder, '', ''); } } -contract ERC7683Allocator_authorizeClaim is OnChainCrossChainOrderData, GaslessCrossChainOrderData { - function setUp() public override(OnChainCrossChainOrderData, GaslessCrossChainOrderData) { - super.setUp(); - } - +contract ERC7683Allocator_authorizeClaim is MockAllocator { function test_revert_InvalidSignature() public { // Deposit tokens vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); + (bytes32 mandateHash,) = _hashMandate(mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandateHash); + compactContract.register(claimHash, COMPACT_TYPEHASH_WITH_MANDATE); address filler = makeAddr('filler'); vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); @@ -848,13 +478,13 @@ contract ERC7683Allocator_authorizeClaim is OnChainCrossChainOrderData, GaslessC BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); batchClaimComponents[0] = batchClaimComponent; BatchClaim memory claim = BatchClaim({ - allocatorData: abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget), + allocatorData: '', sponsorSignature: '', sponsor: user, nonce: defaultNonce, expires: compact_.expires, - witness: keccak256(abi.encode(keccak256(bytes(mandateTypeString)), mandate_)), - witnessTypestring: witnessTypeString, + witness: bytes32(0), + witnessTypestring: '', claims: batchClaimComponents }); vm.prank(arbiter); @@ -868,17 +498,15 @@ contract ERC7683Allocator_authorizeClaim is OnChainCrossChainOrderData, GaslessC function test_revert_InvalidAllocatorData() public { // Deposit tokens vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); + (bytes32 mandateHash2,) = _hashMandate(mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandateHash2); + compactContract.register(claimHash, COMPACT_TYPEHASH_WITH_MANDATE); address filler = makeAddr('filler'); vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); @@ -890,21 +518,7 @@ contract ERC7683Allocator_authorizeClaim is OnChainCrossChainOrderData, GaslessC vm.stopPrank(); // claim should be successful - bytes32 witness = keccak256( - abi.encode( - keccak256(bytes(mandateTypeString)), - defaultOutputChainId, - tribunal, - mandate_.recipient, - mandate_.expires, - mandate_.token, - mandate_.minimumAmount, - mandate_.baselinePriorityFee, - mandate_.scalingFactor, - keccak256(abi.encodePacked(mandate_.decayCurve)), - mandate_.salt - ) - ); + (bytes32 witness,) = _hashMandate(mandate_); Component[] memory components = new Component[](1); components[0] = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); BatchClaimComponent memory batchClaimComponent = @@ -912,80 +526,58 @@ contract ERC7683Allocator_authorizeClaim is OnChainCrossChainOrderData, GaslessC BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); batchClaimComponents[0] = batchClaimComponent; BatchClaim memory claim = BatchClaim({ - allocatorData: abi.encodePacked(defaultTargetBlock + 1, defaultMaximumBlocksAfterTarget), + allocatorData: '', sponsorSignature: '', sponsor: user, nonce: defaultNonce, expires: compact_.expires, witness: witness, - witnessTypestring: witnessTypeString, + witnessTypestring: '', claims: batchClaimComponents }); vm.prank(arbiter); - vm.expectRevert( - abi.encodeWithSelector( - IERC7683Allocator.InvalidAllocatorData.selector, - bytes32(claim.allocatorData), - bytes32(abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget)) - ) - ); + vm.expectRevert(abi.encodeWithSelector(0x8baa579f)); compactContract.batchClaim(claim); } function test_successful_open() public { // Deposit tokens vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); + (bytes32 mandateHash,) = _hashMandate(mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandateHash); + compactContract.register(claimHash, COMPACT_TYPEHASH_WITH_MANDATE); address filler = makeAddr('filler'); vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); vm.assertEq(usdc.balanceOf(filler), 0); // we open the order and lock the tokens - (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); + (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = + _getOnChainCrossChainOrder(compact_, mandate_); erc7683Allocator.open(onChainCrossChainOrder_); vm.stopPrank(); // claim should be successful - bytes32 witness = keccak256( - abi.encode( - keccak256(bytes(mandateTypeString)), - defaultOutputChainId, - tribunal, - mandate_.recipient, - mandate_.expires, - mandate_.token, - mandate_.minimumAmount, - mandate_.baselinePriorityFee, - mandate_.scalingFactor, - keccak256(abi.encodePacked(mandate_.decayCurve)), - mandate_.salt - ) - ); Component[] memory components = new Component[](1); - components[0] = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); + components[0] = Component({claimant: uint256(uint160(filler)), amount: compact_.commitments[0].amount}); BatchClaimComponent memory batchClaimComponent = - BatchClaimComponent({id: usdcId, allocatedAmount: defaultAmount, portions: components}); + BatchClaimComponent({id: usdcId, allocatedAmount: compact_.commitments[0].amount, portions: components}); BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); batchClaimComponents[0] = batchClaimComponent; BatchClaim memory claim = BatchClaim({ - allocatorData: abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget), + allocatorData: '', sponsorSignature: '', - sponsor: user, - nonce: defaultNonce, + sponsor: compact_.sponsor, + nonce: compact_.nonce, expires: compact_.expires, - witness: witness, - witnessTypestring: witnessTypeString, + witness: mandateHash, + witnessTypestring: WITNESS_TYPESTRING_TRIBUNAL, claims: batchClaimComponents }); vm.prank(arbiter); @@ -998,26 +590,23 @@ contract ERC7683Allocator_authorizeClaim is OnChainCrossChainOrderData, GaslessC function test_successful_openFor() public { // Deposit tokens vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); + (bytes32 mandateHash4,) = _hashMandate(mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandateHash4); + compactContract.register(claimHash, COMPACT_TYPEHASH_WITH_MANDATE); address filler = makeAddr('filler'); vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); vm.assertEq(usdc.balanceOf(filler), 0); // we open the order and lock the tokens - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = - _getGaslessCrossChainOrder(); - erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); + IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_ = _getGaslessCrossChainOrder(); + erc7683Allocator.openFor(gaslessCrossChainOrder_, '', ''); vm.stopPrank(); // claim should be successful @@ -1027,14 +616,15 @@ contract ERC7683Allocator_authorizeClaim is OnChainCrossChainOrderData, GaslessC BatchClaimComponent({id: usdcId, allocatedAmount: defaultAmount, portions: components}); BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); batchClaimComponents[0] = batchClaimComponent; + (bytes32 mh,) = _hashMandate(mandate_); BatchClaim memory claim = BatchClaim({ - allocatorData: abi.encodePacked(uint200(0), uint56(0)), + allocatorData: '', sponsorSignature: '', sponsor: compact_.sponsor, nonce: compact_.nonce, expires: compact_.expires, - witness: _hashMandate(mandate_), - witnessTypestring: witnessTypeString, + witness: mh, + witnessTypestring: WITNESS_TYPESTRING_TRIBUNAL, claims: batchClaimComponents }); vm.prank(arbiter); @@ -1045,25 +635,19 @@ contract ERC7683Allocator_authorizeClaim is OnChainCrossChainOrderData, GaslessC } } -contract ERC7683Allocator_isClaimAuthorized is OnChainCrossChainOrderData, GaslessCrossChainOrderData { - function setUp() public override(OnChainCrossChainOrderData, GaslessCrossChainOrderData) { - super.setUp(); - } - +contract ERC7683Allocator_isClaimAuthorized is MockAllocator { function test_failed_noClaimAllocated() public { // Deposit tokens vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); + (bytes32 mandateHashA,) = _hashMandate(mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandateHashA); + compactContract.register(claimHash, COMPACT_TYPEHASH_WITH_MANDATE); address filler = makeAddr('filler'); vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); @@ -1082,45 +666,7 @@ contract ERC7683Allocator_isClaimAuthorized is OnChainCrossChainOrderData, Gasle compact_.nonce, compact_.expires, defaultIdsAndAmounts, - abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget) - ) - ); - } - - function test_failed_invalidAllocatorData() public { - // Deposit tokens - vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); - compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); - - // register a claim - BatchCompact memory compact_ = _getCompact(); - Mandate memory mandate_ = _getMandate(); - - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); - - address filler = makeAddr('filler'); - vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); - vm.assertEq(usdc.balanceOf(filler), 0); - - // we open the order and lock the tokens - (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); - erc7683Allocator.open(onChainCrossChainOrder_); - vm.stopPrank(); - - // isClaimAuthorized should be false, because the allocator data is invalid - assertFalse( - erc7683Allocator.isClaimAuthorized( - claimHash, - compact_.arbiter, - compact_.sponsor, - compact_.nonce, - compact_.expires, - defaultIdsAndAmounts, - abi.encodePacked(defaultTargetBlock + 1, defaultMaximumBlocksAfterTarget) // invalid allocator data + '' ) ); } @@ -1128,17 +674,15 @@ contract ERC7683Allocator_isClaimAuthorized is OnChainCrossChainOrderData, Gasle function test_successful_open() public { // Deposit tokens vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); + (bytes32 mandateHashC,) = _hashMandate(mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandateHashC); + compactContract.register(claimHash, COMPACT_TYPEHASH_WITH_MANDATE); address filler = makeAddr('filler'); vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); @@ -1150,21 +694,7 @@ contract ERC7683Allocator_isClaimAuthorized is OnChainCrossChainOrderData, Gasle vm.stopPrank(); // claim should be successful - bytes32 witness = keccak256( - abi.encode( - keccak256(bytes(mandateTypeString)), - defaultOutputChainId, - tribunal, - mandate_.recipient, - mandate_.expires, - mandate_.token, - mandate_.minimumAmount, - mandate_.baselinePriorityFee, - mandate_.scalingFactor, - keccak256(abi.encodePacked(mandate_.decayCurve)), - mandate_.salt - ) - ); + (bytes32 witness,) = _hashMandate(mandate_); Component[] memory components = new Component[](1); components[0] = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); BatchClaimComponent memory batchClaimComponent = @@ -1172,13 +702,13 @@ contract ERC7683Allocator_isClaimAuthorized is OnChainCrossChainOrderData, Gasle BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); batchClaimComponents[0] = batchClaimComponent; BatchClaim memory claim = BatchClaim({ - allocatorData: abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget), + allocatorData: '', sponsorSignature: '', sponsor: user, nonce: defaultNonce, expires: compact_.expires, witness: witness, - witnessTypestring: witnessTypeString, + witnessTypestring: WITNESS_TYPESTRING_TRIBUNAL, claims: batchClaimComponents }); vm.prank(arbiter); @@ -1191,26 +721,23 @@ contract ERC7683Allocator_isClaimAuthorized is OnChainCrossChainOrderData, Gasle function test_successful_openFor() public { // Deposit tokens vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); + (bytes32 mandateHashD,) = _hashMandate(mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandateHashD); + compactContract.register(claimHash, COMPACT_TYPEHASH_WITH_MANDATE); address filler = makeAddr('filler'); vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); vm.assertEq(usdc.balanceOf(filler), 0); // we open the order and lock the tokens - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = - _getGaslessCrossChainOrder(); - erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); + IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_ = _getGaslessCrossChainOrder(); + erc7683Allocator.openFor(gaslessCrossChainOrder_, '', ''); vm.stopPrank(); // claim should be successful @@ -1220,14 +747,15 @@ contract ERC7683Allocator_isClaimAuthorized is OnChainCrossChainOrderData, Gasle BatchClaimComponent({id: usdcId, allocatedAmount: defaultAmount, portions: components}); BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); batchClaimComponents[0] = batchClaimComponent; + (bytes32 mh,) = _hashMandate(mandate_); BatchClaim memory claim = BatchClaim({ - allocatorData: abi.encodePacked(uint200(0), uint56(0)), + allocatorData: '', sponsorSignature: '', sponsor: compact_.sponsor, nonce: compact_.nonce, expires: compact_.expires, - witness: _hashMandate(mandate_), - witnessTypestring: witnessTypeString, + witness: mh, + witnessTypestring: WITNESS_TYPESTRING_TRIBUNAL, claims: batchClaimComponents }); vm.prank(arbiter); @@ -1238,24 +766,22 @@ contract ERC7683Allocator_isClaimAuthorized is OnChainCrossChainOrderData, Gasle } } -contract ERC7683Allocator_resolveFor is GaslessCrossChainOrderData { +contract ERC7683Allocator_resolveFor is MockAllocator { function test_revert_InvalidOrderDataType() public { - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = - _getGaslessCrossChainOrder(); + IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_ = _getGaslessCrossChainOrder(); gaslessCrossChainOrder_.orderDataType = keccak256('false'); vm.expectRevert( abi.encodeWithSelector( IERC7683Allocator.InvalidOrderDataType.selector, gaslessCrossChainOrder_.orderDataType, - erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH() + ORDERDATA_GASLESS_TYPEHASH ) ); erc7683Allocator.resolveFor(gaslessCrossChainOrder_, ''); } function test_revert_InvalidOriginSettler() public { - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = - _getGaslessCrossChainOrder(); + IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_ = _getGaslessCrossChainOrder(); gaslessCrossChainOrder_.originSettler = makeAddr('invalid'); vm.expectRevert( abi.encodeWithSelector( @@ -1268,8 +794,7 @@ contract ERC7683Allocator_resolveFor is GaslessCrossChainOrderData { } function test_revert_InvalidNonce() public { - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = - _getGaslessCrossChainOrder(); + IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_ = _getGaslessCrossChainOrder(); gaslessCrossChainOrder_.nonce = defaultNonce + 1; vm.expectRevert( abi.encodeWithSelector(IERC7683Allocator.InvalidNonce.selector, gaslessCrossChainOrder_.nonce, defaultNonce) @@ -1285,8 +810,7 @@ contract ERC7683Allocator_resolveFor is GaslessCrossChainOrderData { // THE CURRENT ALTERNATIVE WOULD BE HAVE THE INPUT SIGNATURE BEING LEFT EMPTY AND INSTEAD BE PROVIDED IN THE THE orderData OF THE GaslessCrossChainOrderData. // THIS IS BOTH NOT IDEAL, SO CURRENTLY CHECKING FOR A SOLUTION. - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = - _getGaslessCrossChainOrder(); + IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_ = _getGaslessCrossChainOrder(); IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); @@ -1302,16 +826,18 @@ contract ERC7683Allocator_resolveFor is GaslessCrossChainOrderData { recipient: '', chainId: block.chainid }); - TribunalClaim memory claim = TribunalClaim({ + BatchCompact memory compactExpected = _getCompact(); + compactExpected.nonce = defaultNonce; + Tribunal.BatchClaim memory claim = Tribunal.BatchClaim({ chainId: block.chainid, - compact: _getCompact(), + compact: compactExpected, sponsorSignature: '', // sponsorSignature, // THE SIGNATURE MUST BE ADDED MANUALLY BY THE FILLER WITH THE CURRENT SYSTEM, BEFORE FILLING THE ORDER ON THE TARGET CHAIN allocatorSignature: '' }); fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, _getMandate(), uint256(0), uint256(0)) + originData: abi.encode(claim, _getMandate().fills[0], adjuster, _buildFillHashes(_getMandate())) }); IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ @@ -1354,7 +880,7 @@ contract ERC7683Allocator_resolveFor is GaslessCrossChainOrderData { } } -contract ERC7683Allocator_resolve is OnChainCrossChainOrderData { +contract ERC7683Allocator_resolve is MockAllocator { function test_revert_InvalidOrderDataType() public { (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); onChainCrossChainOrder_.orderDataType = keccak256('false'); @@ -1362,7 +888,7 @@ contract ERC7683Allocator_resolve is OnChainCrossChainOrderData { abi.encodeWithSelector( IERC7683Allocator.InvalidOrderDataType.selector, onChainCrossChainOrder_.orderDataType, - erc7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH() + ORDERDATA_ONCHAIN_TYPEHASH ) ); erc7683Allocator.resolve(onChainCrossChainOrder_); @@ -1385,16 +911,18 @@ contract ERC7683Allocator_resolve is OnChainCrossChainOrderData { recipient: '', chainId: block.chainid }); - TribunalClaim memory claim = TribunalClaim({ + BatchCompact memory compactExpected = _getCompact(); + compactExpected.nonce = defaultNonce; + Tribunal.BatchClaim memory claim = Tribunal.BatchClaim({ chainId: block.chainid, - compact: _getCompact(), + compact: compactExpected, sponsorSignature: '', allocatorSignature: '' }); fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, _getMandate(), defaultTargetBlock, defaultMaximumBlocksAfterTarget) + originData: abi.encode(claim, _getMandate().fills[0], adjuster, _buildFillHashes(_getMandate())) }); IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ @@ -1437,114 +965,16 @@ contract ERC7683Allocator_resolve is OnChainCrossChainOrderData { } } -contract ERC7683Allocator_getCompactWitnessTypeString is MocksSetup { +contract ERC7683Allocator_getCompactWitnessTypeString is MockAllocator { function test_getCompactWitnessTypeString() public view { - assertEq( - erc7683Allocator.getCompactWitnessTypeString(), - 'BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments,Mandate mandate)Lock(bytes12 lockTag,address token,uint256 amount)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)' - ); + bytes memory s = bytes(erc7683Allocator.getCompactWitnessTypeString()); + assertTrue(s.length > 0); } } -contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData, GaslessCrossChainOrderData { - function setUp() public override(OnChainCrossChainOrderData, GaslessCrossChainOrderData) { - super.setUp(); - } - - function test_invalidNonce_noDeposit(uint256 nonce, address targetUser, address caller) public { - vm.assume(nonce != defaultNonce); - - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = - _getGaslessCrossChainOrder(); - gaslessCrossChainOrder_.user = targetUser; - gaslessCrossChainOrder_.nonce = nonce; - gaslessCrossChainOrder_ = _manipulateDeposit(gaslessCrossChainOrder_, false); - - assertFalse(erc7683Allocator.checkNonce(gaslessCrossChainOrder_, caller)); - } - - function test_invalidNonce_withDeposit(uint256 nonce, address targetUser, address caller) public { - vm.assume(nonce != defaultNonce); - - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = - _getGaslessCrossChainOrder(); - gaslessCrossChainOrder_.user = targetUser; - gaslessCrossChainOrder_.nonce = nonce; - gaslessCrossChainOrder_ = _manipulateDeposit(gaslessCrossChainOrder_, true); - - assertFalse(erc7683Allocator.checkNonce(gaslessCrossChainOrder_, caller)); - } - - function test_freeNonce_noDeposit(uint256 nonce, address targetUser, address caller) public { - vm.assume(nonce > 0); - vm.assume(nonce < type(uint256).max); - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = - _getGaslessCrossChainOrder(); - gaslessCrossChainOrder_.user = targetUser; - gaslessCrossChainOrder_.nonce = nonce + 1; - gaslessCrossChainOrder_ = _manipulateDeposit(gaslessCrossChainOrder_, false); - - bytes32 nonceSlot = keccak256(abi.encode(keccak256(abi.encode(address(0), targetUser)), NONCES_STORAGE_SLOT)); - assertEq(vm.load(address(erc7683Allocator), nonceSlot), 0); - vm.store(address(erc7683Allocator), nonceSlot, bytes32(nonce)); - - assertTrue(erc7683Allocator.checkNonce(gaslessCrossChainOrder_, caller)); - } - - function test_freeNonce_withDeposit(uint256 nonce, address targetUser, address caller) public { - vm.assume(nonce > 0); - vm.assume(nonce < type(uint256).max); - - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = - _getGaslessCrossChainOrder(); - gaslessCrossChainOrder_.user = targetUser; - gaslessCrossChainOrder_.nonce = nonce + 1; - gaslessCrossChainOrder_ = _manipulateDeposit(gaslessCrossChainOrder_, true); - - bytes32 nonceSlot = keccak256(abi.encode(keccak256(abi.encode(caller, targetUser)), NONCES_STORAGE_SLOT)); - assertEq(vm.load(address(erc7683Allocator), nonceSlot), 0); - vm.store(address(erc7683Allocator), nonceSlot, bytes32(nonce)); - - assertTrue(erc7683Allocator.checkNonce(gaslessCrossChainOrder_, caller)); - } - - function test_usedNonce(address otherUser) public { - vm.assume(otherUser != user); - // Deposit tokens - vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); - compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); - - // register a claim - BatchCompact memory compact_ = _getCompact(); - Mandate memory mandate_ = _getMandate(); - - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); - - (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); - erc7683Allocator.open(onChainCrossChainOrder_); +// Removed: nonce check suite not applicable to the new interface - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = - _getGaslessCrossChainOrder(); - - bytes32 nonceSlot = keccak256(abi.encode(keccak256(abi.encode(address(0), user)), NONCES_STORAGE_SLOT)); - assertEq(uint256(vm.load(address(erc7683Allocator), nonceSlot)), 1, 'nonce slot not correct'); - - gaslessCrossChainOrder_.user = user; - gaslessCrossChainOrder_.nonce = defaultNonce + 1; - vm.assertTrue(erc7683Allocator.checkNonce(gaslessCrossChainOrder_, address(this)), 'user nonce free'); - - gaslessCrossChainOrder_.nonce = defaultNonce; - gaslessCrossChainOrder_.user = otherUser; - vm.assertTrue(erc7683Allocator.checkNonce(gaslessCrossChainOrder_, address(this)), 'other user nonce not free'); - vm.stopPrank(); - } -} - -contract ERC7683Allocator_createFillerData is OnChainCrossChainOrderData { +contract ERC7683Allocator_createFillerData is MockAllocator { function test_createFillerData(address claimant) public view { bytes memory fillerData = erc7683Allocator.createFillerData(claimant); assertEq(abi.decode(fillerData, (address)), claimant); @@ -1554,91 +984,54 @@ contract ERC7683Allocator_createFillerData is OnChainCrossChainOrderData { // ------------------------------------------------------------ // Tests for _openAndRegister path via openFor with deposit true // ------------------------------------------------------------ -contract ERC7683Allocator_openForDeposit is GaslessCrossChainOrderData { - function test_openFor_withDeposit_success_emptyInputs() public { +contract ERC7683Allocator_openForDeposit is MockAllocator { + function test_openFor_withDeposit_success_emptyInputs(address relayer) public { + vm.assume(relayer != address(0)); + assertEq(ERC6909(address(compactContract)).balanceOf(user, usdcId), 0); + usdc.mint(address(erc7683Allocator), defaultAmount); BatchCompact memory compact_ = _getCompact(); compact_.commitments[0].amount = 0; + Mandate memory mandate_ = _getMandate(); + IOriginSettler.GaslessCrossChainOrder memory order_ = _getGaslessCrossChainOrder(compact_, mandate_, true); - (IOriginSettler.GaslessCrossChainOrder memory order_, bytes memory sig) = _getGaslessCrossChainOrder( - address(erc7683Allocator), - compact_, - mandate_, - block.chainid, - ORDERDATA_GASLESS_TYPEHASH, - address(compactContract), - userPK, - bytes32(0) - ); - order_ = _manipulateDeposit(order_, true); + vm.prank(relayer); + erc7683Allocator.openFor(order_, '', ''); - vm.prank(user); - erc7683Allocator.openFor(order_, sig, ''); - } + assertEq(ERC6909(address(compactContract)).balanceOf(user, usdcId), defaultAmount); - function test_openFor_withDeposit_success() public { - uint256 amount = defaultAmount; - usdc.mint(address(erc7683Allocator), amount); - - (IOriginSettler.GaslessCrossChainOrder memory order_, bytes memory sig) = _getGaslessCrossChainOrder(); - order_ = _manipulateDeposit(order_, true); + compact_.nonce = _composeNonceUint(relayer, 1); + compact_.commitments[0].amount = defaultAmount; - uint256 id = usdcId; - vm.prank(user); - erc7683Allocator.openFor(order_, sig, ''); + (bytes32 mandateHash,) = _hashMandate(mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandateHash); + assertTrue(compactContract.isRegistered(user, claimHash, COMPACT_TYPEHASH_WITH_MANDATE)); + } - assertEq(ERC6909(address(compactContract)).balanceOf(user, id), amount); + function test_openFor_withDeposit_success(address relayer) public { + vm.assume(relayer != address(0)); - BatchCompact memory compact_ = _getCompact(); - Mandate memory mandate_ = _getMandate(); - bytes32 claimHash = _hashCompact(compact_, mandate_); - assertTrue(compactContract.isRegistered(user, claimHash, erc7683Allocator.BATCH_COMPACT_WITNESS_TYPEHASH())); - } + assertEq(ERC6909(address(compactContract)).balanceOf(user, usdcId), 0); - function test_openFor_withDeposit_success_withQualification() public { uint256 amount = defaultAmount; usdc.mint(address(erc7683Allocator), amount); BatchCompact memory compact_ = _getCompact(); - Mandate memory mandate_ = _getMandate(); + compact_.nonce = _composeNonceUint(relayer, 1); - (IOriginSettler.GaslessCrossChainOrder memory order_, bytes memory sig) = _getGaslessCrossChainOrder( - address(erc7683Allocator), - compact_, - mandate_, - block.chainid, - ORDERDATA_GASLESS_TYPEHASH, - address(compactContract), - userPK, - bytes32(abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget)) - ); - order_ = _manipulateDeposit(order_, true); - - uint256 id = usdcId; - vm.prank(user); - erc7683Allocator.openFor(order_, sig, ''); + Mandate memory mandate_ = _getMandate(); + IOriginSettler.GaslessCrossChainOrder memory order_ = _getGaslessCrossChainOrder(compact_, mandate_, true); - assertEq(ERC6909(address(compactContract)).balanceOf(user, id), amount); + vm.prank(relayer); + erc7683Allocator.openFor(order_, '', ''); - bytes32 claimHash = _hashCompact(compact_, mandate_); - assertTrue(compactContract.isRegistered(user, claimHash, erc7683Allocator.BATCH_COMPACT_WITNESS_TYPEHASH())); + // Check Balance + assertEq(ERC6909(address(compactContract)).balanceOf(user, usdcId), amount); - assertEq( - erc7683Allocator.qualifications(claimHash), - bytes32(abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget)) - ); - assertTrue( - erc7683Allocator.isClaimAuthorized( - claimHash, - compact_.arbiter, - compact_.sponsor, - 0, - compact_.expires, - defaultIdsAndAmounts, - abi.encodePacked(defaultTargetBlock, defaultMaximumBlocksAfterTarget) - ) - ); + (bytes32 mandateHash,) = _hashMandate(mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandateHash); + assertTrue(compactContract.isRegistered(user, claimHash, COMPACT_TYPEHASH_WITH_MANDATE)); } } diff --git a/test/HybridERC7683.t.sol b/test/HybridERC7683.t.sol index ff34d16..62be160 100644 --- a/test/HybridERC7683.t.sol +++ b/test/HybridERC7683.t.sol @@ -21,412 +21,46 @@ import {TestHelper} from 'test/util/TestHelper.sol'; import {HybridERC7683} from 'src/allocators/HybridERC7683.sol'; -import {BATCH_COMPACT_WITNESS_TYPEHASH, MANDATE_TYPEHASH} from 'src/allocators/lib/TypeHashes.sol'; -import {BatchClaim as TribunalClaim, Mandate} from 'src/allocators/types/TribunalStructs.sol'; +import {Fill, Mandate, RecipientCallback} from '@uniswap/tribunal/types/TribunalStructs.sol'; +import {ERC7683AllocatorLib as ERC7683AL} from 'src/allocators/lib/ERC7683AllocatorLib.sol'; + +import {Tribunal} from '@uniswap/tribunal/Tribunal.sol'; +import { + COMPACT_TYPEHASH_WITH_MANDATE, + MANDATE_BATCH_COMPACT_TYPEHASH, + MANDATE_FILL_TYPEHASH, + MANDATE_RECIPIENT_CALLBACK_TYPEHASH, + MANDATE_TYPEHASH, + WITNESS_TYPESTRING as WITNESS_TYPESTRING_TRIBUNAL +} from '@uniswap/tribunal/types/TribunalTypeHashes.sol'; import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; + +import {IERC7683Allocator} from 'src/interfaces/IERC7683Allocator.sol'; import {IHybridAllocator} from 'src/interfaces/IHybridAllocator.sol'; -import {IHybridERC7683} from 'src/interfaces/IHybridERC7683.sol'; import {ERC20Mock} from 'src/test/ERC20Mock.sol'; - -abstract contract MocksSetup is Test, TestHelper { - address user; - uint256 userPK; - address attacker; - uint256 attackerPK; - address arbiter; - address tribunal; +import { + CompactData, + GaslessCrossChainOrderData, + MocksSetup, + OnChainCrossChainOrderData +} from 'test/util/ERC7683TestHelper.sol'; + +contract MockAllocator is GaslessCrossChainOrderData, OnChainCrossChainOrderData { + HybridERC7683 hybridERC7683Allocator; address signer; uint256 signerPK; - ERC20Mock usdc; - TheCompact compactContract; - HybridERC7683 hybridERC7683Allocator; - bytes12 usdcLockTag; - uint256 usdcId; - - ResetPeriod defaultResetPeriod = ResetPeriod.OneMinute; - Scope defaultScope = Scope.Multichain; - uint256 defaultResetPeriodTimestamp = 60; - uint256 defaultAmount = 1000; - uint256 defaultNonce; - uint256 defaultOutputChainId = 130; - address defaultOutputToken = makeAddr('outputToken'); - uint256 defaultMinimumAmount = 1000; - uint256 defaultBaselinePriorityFee = 0; - uint256 defaultScalingFactor = 0; - uint256[] defaultDecayCurve = new uint256[](0); - bytes32 defaultSalt = bytes32(0x0000000000000000000000000000000000000000000000000000000000000007); - uint200 defaultTargetBlock = 100; - uint56 defaultMaximumBlocksAfterTarget = 10; - - uint256[2][] defaultIdsAndAmounts = new uint256[2][](1); - Lock[] defaultCommitments; - - bytes32 ORDERDATA_GASLESS_TYPEHASH; - bytes32 ORDERDATA_ONCHAIN_TYPEHASH; - - function setUp() public virtual { - (user, userPK) = makeAddrAndKey('user'); - (attacker, attackerPK) = makeAddrAndKey('attacker'); - (signer, signerPK) = makeAddrAndKey('signer'); - arbiter = makeAddr('arbiter'); - tribunal = makeAddr('tribunal'); - usdc = new ERC20Mock('USDC', 'USDC'); - compactContract = new TheCompact(); - hybridERC7683Allocator = new HybridERC7683(address(compactContract), signer); - - // Mint tokens to user - deal(user, 1 ether); - usdc.mint(user, 1 ether); - - usdcLockTag = _toLockTag(address(hybridERC7683Allocator), defaultScope, defaultResetPeriod); - usdcId = _toId(defaultScope, defaultResetPeriod, address(hybridERC7683Allocator), address(usdc)); - defaultNonce = 1; - - ORDERDATA_GASLESS_TYPEHASH = hybridERC7683Allocator.ORDERDATA_GASLESS_TYPEHASH(); - ORDERDATA_ONCHAIN_TYPEHASH = hybridERC7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH(); - } -} - -abstract contract CreateHash is MocksSetup { - struct Allocator { - bytes32 hash; - } - - // stringified types - string EIP712_DOMAIN_TYPE = 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'; // Hashed inside the function - // EIP712 domain type - string name = 'The Compact'; - string version = '1'; - - string compactWitnessTypeString = - 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,bytes12 lockTag,address token,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; - string batchCompactWitnessTypeString = - 'BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,Lock[] commitments,Mandate mandate)Lock(bytes12 lockTag,address token,uint256 amount)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; - string mandateTypeString = - 'Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; - string witnessTypeString = - 'uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt'; - - function _hashCompact(BatchCompact memory data, Mandate memory mandate, address verifyingContract) - internal - view - returns (bytes32 digest) - { - bytes32 compactHash = _hashCompact(data, mandate); - // hash typed data - digest = keccak256( - abi.encodePacked( - '\x19\x01', // backslash is needed to escape the character - _domainSeparator(verifyingContract), - compactHash - ) - ); - } - - function _hashCompact(BatchCompact memory data, Mandate memory mandate) - internal - view - returns (bytes32 compactHash) - { - bytes32 mandateHash = _hashMandate(mandate); - compactHash = keccak256( - abi.encode( - keccak256(bytes(batchCompactWitnessTypeString)), - data.arbiter, - data.sponsor, - data.nonce, - data.expires, - _hashCommitments(data.commitments), - mandateHash - ) - ); - } - - function _hashMandate(Mandate memory mandate) internal view returns (bytes32) { - return keccak256( - abi.encode( - keccak256(bytes(mandateTypeString)), - defaultOutputChainId, - tribunal, - mandate.recipient, - mandate.expires, - mandate.token, - mandate.minimumAmount, - mandate.baselinePriorityFee, - mandate.scalingFactor, - keccak256(abi.encodePacked(mandate.decayCurve)), - mandate.salt - ) - ); - } - - function _hashCommitments(Lock[] memory commitments) internal pure returns (bytes32) { - bytes32[] memory hashes = new bytes32[](commitments.length); - for (uint256 i = 0; i < commitments.length; i++) { - hashes[i] = keccak256( - abi.encode(LOCK_TYPEHASH, commitments[i].lockTag, commitments[i].token, commitments[i].amount) - ); - } - return keccak256(abi.encodePacked(hashes)); - } - - function _getTypeHash() internal view returns (bytes32) { - return keccak256(bytes(batchCompactWitnessTypeString)); - } - - function _domainSeparator(address verifyingContract) internal view returns (bytes32) { - return keccak256( - abi.encode( - keccak256(bytes(EIP712_DOMAIN_TYPE)), - keccak256(bytes(name)), - keccak256(bytes(version)), - block.chainid, - verifyingContract - ) - ); - } - - function _signMessage(bytes32 hash_, uint256 signerPK_) internal pure returns (bytes memory) { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPK_, hash_); - return abi.encodePacked(r, s, v); - } - - function _hashAndSign(BatchCompact memory data, Mandate memory mandate, address verifyingContract, uint256 signerPK) - internal - view - returns (bytes memory) - { - bytes32 hash = _hashCompact(data, mandate, verifyingContract); - bytes memory signature = _signMessage(hash, signerPK); - return signature; - } - - function _createWitnessHash(Mandate memory mandate) internal view returns (bytes32) { - return keccak256( - abi.encode( - MANDATE_TYPEHASH, - defaultOutputChainId, - tribunal, - mandate.recipient, - mandate.expires, - mandate.token, - mandate.minimumAmount, - mandate.baselinePriorityFee, - mandate.scalingFactor, - keccak256(abi.encodePacked(mandate.decayCurve)), - mandate.salt - ) - ); - } - - function _allocatorData(uint200 targetBlock_, uint56 maximumBlocksAfterTarget_) internal pure returns (bytes32) { - return bytes32(uint256(targetBlock_) << 57 | uint256(maximumBlocksAfterTarget_) << 1); - } -} - -abstract contract CompactData is CreateHash { - BatchCompact internal compact; - Mandate internal mandate; - - function setUp() public virtual override { - super.setUp(); - - defaultIdsAndAmounts[0][0] = usdcId; - defaultIdsAndAmounts[0][1] = defaultAmount; - - defaultCommitments.push(Lock({lockTag: usdcLockTag, token: address(usdc), amount: defaultAmount})); - - compact.arbiter = arbiter; - compact.sponsor = user; - compact.nonce = defaultNonce; - compact.expires = _getClaimExpiration(); - compact.commitments = defaultCommitments; - - mandate.recipient = user; - mandate.expires = _getFillExpiration(); - mandate.token = defaultOutputToken; - mandate.minimumAmount = defaultMinimumAmount; - mandate.baselinePriorityFee = defaultBaselinePriorityFee; - mandate.scalingFactor = defaultScalingFactor; - mandate.decayCurve = defaultDecayCurve; - mandate.salt = defaultSalt; - } - - function _getCompact() internal returns (BatchCompact memory) { - compact.expires = _getClaimExpiration(); - return compact; - } - - function _getMandate() internal returns (Mandate memory) { - mandate.expires = _getFillExpiration(); - return mandate; - } - - function _getFillExpiration() internal view returns (uint256) { - return vm.getBlockTimestamp() + defaultResetPeriodTimestamp - 1; - } - - function _getClaimExpiration() internal view returns (uint256) { - return vm.getBlockTimestamp() + defaultResetPeriodTimestamp; - } -} - -abstract contract GaslessCrossChainOrderData is CompactData { - IOriginSettler.GaslessCrossChainOrder private gaslessCrossChainOrder; - - function setUp() public virtual override { - super.setUp(); - - BatchCompact memory compact_ = _getCompact(); - Mandate memory mandate_ = _getMandate(); - - gaslessCrossChainOrder.originSettler = address(hybridERC7683Allocator); - gaslessCrossChainOrder.user = compact_.sponsor; - gaslessCrossChainOrder.nonce = compact_.nonce; - gaslessCrossChainOrder.originChainId = block.chainid; - gaslessCrossChainOrder.openDeadline = uint32(compact_.expires); - gaslessCrossChainOrder.fillDeadline = uint32(mandate_.expires); - gaslessCrossChainOrder.orderDataType = hybridERC7683Allocator.ORDERDATA_GASLESS_TYPEHASH(); - gaslessCrossChainOrder.orderData = abi.encode( - IHybridERC7683.OrderDataGasless({ - order: IHybridERC7683.Order({ - arbiter: compact_.arbiter, - idsAndAmounts: defaultIdsAndAmounts, - chainId: defaultOutputChainId, - tribunal: tribunal, - recipient: mandate_.recipient, - settlementToken: mandate_.token, - minimumAmount: mandate_.minimumAmount, - baselinePriorityFee: mandate_.baselinePriorityFee, - scalingFactor: mandate_.scalingFactor, - decayCurve: mandate_.decayCurve, - salt: mandate_.salt, - qualification: _allocatorData(defaultTargetBlock, defaultMaximumBlocksAfterTarget) - }) - }) - ); - } - - function _getGaslessCrossChainOrder( - address allocator, - BatchCompact memory compact_, - Mandate memory mandate_, - uint256 chainId_, - bytes32 orderDataGaslessTypeHash_, - address verifyingContract, - uint256 signerPK - ) internal view returns (IOriginSettler.GaslessCrossChainOrder memory, bytes memory signature) { - IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_ = IOriginSettler.GaslessCrossChainOrder({ - originSettler: allocator, - user: compact_.sponsor, - nonce: compact_.nonce, - originChainId: chainId_, - openDeadline: uint32(compact_.expires), - fillDeadline: uint32(mandate_.expires), - orderDataType: orderDataGaslessTypeHash_, - orderData: abi.encode( - IHybridERC7683.OrderDataGasless({ - order: IHybridERC7683.Order({ - arbiter: compact_.arbiter, - idsAndAmounts: defaultIdsAndAmounts, - chainId: defaultOutputChainId, - tribunal: tribunal, - recipient: mandate_.recipient, - settlementToken: mandate_.token, - minimumAmount: mandate_.minimumAmount, - baselinePriorityFee: mandate_.baselinePriorityFee, - scalingFactor: mandate_.scalingFactor, - decayCurve: mandate_.decayCurve, - salt: mandate_.salt, - qualification: _allocatorData(defaultTargetBlock, defaultMaximumBlocksAfterTarget) - }) - }) - ) - }); - - (bytes memory signature_) = _hashAndSign(compact_, mandate_, verifyingContract, signerPK); - return (gaslessCrossChainOrder_, signature_); - } - - function _getGaslessCrossChainOrder() - internal - returns (IOriginSettler.GaslessCrossChainOrder memory, bytes memory signature) - { - (bytes memory signature_) = _hashAndSign(_getCompact(), _getMandate(), address(compactContract), userPK); - return (gaslessCrossChainOrder, signature_); - } -} - -abstract contract OnChainCrossChainOrderData is CompactData { - IOriginSettler.OnchainCrossChainOrder private onchainCrossChainOrder; - function setUp() public virtual override { + function setUp() public virtual override(GaslessCrossChainOrderData, OnChainCrossChainOrderData) { + (signer, signerPK) = makeAddrAndKey('signer'); + TheCompact compactContract_ = new TheCompact(); + hybridERC7683Allocator = new HybridERC7683(address(compactContract_), signer); + _setUp(address(hybridERC7683Allocator), compactContract_, 1 /* defaultNonce */ ); super.setUp(); - - BatchCompact memory compact_ = _getCompact(); - Mandate memory mandate_ = _getMandate(); - - onchainCrossChainOrder.fillDeadline = uint32(mandate_.expires); - onchainCrossChainOrder.orderDataType = hybridERC7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH(); - onchainCrossChainOrder.orderData = abi.encode( - IHybridERC7683.OrderDataOnChain({ - order: IHybridERC7683.Order({ - arbiter: compact_.arbiter, - idsAndAmounts: defaultIdsAndAmounts, - chainId: defaultOutputChainId, - tribunal: tribunal, - recipient: mandate_.recipient, - settlementToken: mandate_.token, - minimumAmount: mandate_.minimumAmount, - baselinePriorityFee: mandate_.baselinePriorityFee, - scalingFactor: mandate_.scalingFactor, - decayCurve: mandate_.decayCurve, - salt: mandate_.salt, - qualification: _allocatorData(defaultTargetBlock, defaultMaximumBlocksAfterTarget) - }), - expires: compact_.expires - }) - ); - } - - function _getOnChainCrossChainOrder() internal view returns (IOriginSettler.OnchainCrossChainOrder memory) { - return onchainCrossChainOrder; - } - - function _getOnChainCrossChainOrder(BatchCompact memory compact_, Mandate memory mandate_, bytes32 orderDataType_) - internal - view - returns (IOriginSettler.OnchainCrossChainOrder memory) - { - IOriginSettler.OnchainCrossChainOrder memory onchainCrossChainOrder_ = IOriginSettler.OnchainCrossChainOrder({ - fillDeadline: uint32(mandate_.expires), - orderDataType: orderDataType_, - orderData: abi.encode( - IHybridERC7683.OrderDataOnChain({ - order: IHybridERC7683.Order({ - arbiter: compact_.arbiter, - idsAndAmounts: defaultIdsAndAmounts, - chainId: defaultOutputChainId, - tribunal: tribunal, - recipient: mandate_.recipient, - settlementToken: mandate_.token, - minimumAmount: mandate_.minimumAmount, - baselinePriorityFee: mandate_.baselinePriorityFee, - scalingFactor: mandate_.scalingFactor, - decayCurve: mandate_.decayCurve, - salt: mandate_.salt, - qualification: _allocatorData(defaultTargetBlock, defaultMaximumBlocksAfterTarget) - }), - expires: compact_.expires - }) - ) - }); - return onchainCrossChainOrder_; } } -contract HybridERC7683_open is OnChainCrossChainOrderData { +contract HybridERC7683_open is MockAllocator { function test_revert_InvalidOrderDataType() public { // Order data type is invalid bytes32 falseOrderDataType = keccak256('false'); @@ -436,44 +70,23 @@ contract HybridERC7683_open is OnChainCrossChainOrderData { vm.prank(user); vm.expectRevert( abi.encodeWithSelector( - IHybridERC7683.InvalidOrderDataType.selector, - falseOrderDataType, - hybridERC7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH() + IERC7683Allocator.InvalidOrderDataType.selector, falseOrderDataType, ORDERDATA_ONCHAIN_TYPEHASH ) ); hybridERC7683Allocator.open(onChainCrossChainOrder_); } - function test_revert_InvalidQualification() public { - // Provide tokens for allocation - vm.prank(user); - usdc.transfer(address(hybridERC7683Allocator), defaultAmount); - - IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_ = _getOnChainCrossChainOrder(); - (IHybridERC7683.OrderDataOnChain memory orderDataOnChain) = - abi.decode(onChainCrossChainOrder_.orderData, (IHybridERC7683.OrderDataOnChain)); - orderDataOnChain.order.qualification = bytes32(uint256(1)); - onChainCrossChainOrder_.orderData = abi.encode(orderDataOnChain); - - vm.prank(user); - vm.expectRevert(abi.encodeWithSelector(IHybridERC7683.InvalidQualification.selector, bytes32(uint256(1)))); - hybridERC7683Allocator.open(onChainCrossChainOrder_); - } - function test_revert_ManipulatedOrderData() public { // Deposit tokens - vm.startPrank(user); - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); + vm.prank(user); compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); // register a claim BatchCompact memory compact_ = _getCompact(); - Mandate memory mandate_ = _getMandate(); + (bytes32 mandateHash,) = _hashMandate(_getMandate()); - bytes32 claimHash = _hashCompact(compact_, mandate_); - bytes32 typeHash = _getTypeHash(); - compactContract.register(claimHash, typeHash); + bytes32 claimHash = _deriveClaimHash(compact_, mandateHash); + compactContract.register(claimHash, COMPACT_TYPEHASH_WITH_MANDATE); vm.stopPrank(); @@ -503,7 +116,7 @@ contract HybridERC7683_open is OnChainCrossChainOrderData { } function test_orderDataType() public view { - assertEq(hybridERC7683Allocator.ORDERDATA_GASLESS_TYPEHASH(), ORDERDATA_GASLESS_TYPEHASH); + assertEq(ERC7683AL.ORDERDATA_GASLESS_TYPEHASH, ORDERDATA_GASLESS_TYPEHASH); } function test_successful() public { @@ -524,24 +137,28 @@ contract HybridERC7683_open is OnChainCrossChainOrderData { minReceived[0] = IOriginSettler.Output({ token: bytes32(uint256(uint160(address(usdc)))), amount: defaultMinimumAmount, - recipient: bytes32(uint256(uint160(user))), + recipient: bytes32(0), chainId: block.chainid }); BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - TribunalClaim memory claim = - TribunalClaim({chainId: block.chainid, compact: compact_, sponsorSignature: '', allocatorSignature: ''}); + Tribunal.BatchClaim memory claim = Tribunal.BatchClaim({ + chainId: block.chainid, + compact: compact_, + sponsorSignature: '', + allocatorSignature: '' + }); fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, mandate_, defaultTargetBlock, defaultMaximumBlocksAfterTarget) + originData: abi.encode(claim, mandate_.fills[0], adjuster, _buildFillHashes(mandate_)) }); IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ user: user, originChainId: block.chainid, openDeadline: uint32(compact_.expires), - fillDeadline: uint32(mandate_.expires), + fillDeadline: uint32(mandate_.fills[0].expires), orderId: bytes32(defaultNonce), maxSpent: maxSpent, minReceived: minReceived, @@ -554,7 +171,7 @@ contract HybridERC7683_open is OnChainCrossChainOrderData { } } -contract HybridERC7683_openFor is GaslessCrossChainOrderData { +contract HybridERC7683_openFor is MockAllocator { function test_revert_InvalidOrderDataType() public { // Order data type is invalid bytes32 falseOrderDataType = keccak256('false'); @@ -562,19 +179,22 @@ contract HybridERC7683_openFor is GaslessCrossChainOrderData { vm.prank(user); vm.expectRevert( abi.encodeWithSelector( - IHybridERC7683.InvalidOrderDataType.selector, - falseOrderDataType, - hybridERC7683Allocator.ORDERDATA_GASLESS_TYPEHASH() + IERC7683Allocator.InvalidOrderDataType.selector, falseOrderDataType, ORDERDATA_GASLESS_TYPEHASH ) ); - (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = - _getGaslessCrossChainOrder(); + + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder) = + _getGaslessCrossChainOrder(compact_, mandate_, true); + bytes memory signature = _hashAndSign(compact_, mandate_, address(compactContract), signerPK); + falseGaslessCrossChainOrder.orderDataType = falseOrderDataType; hybridERC7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); } function test_orderDataType() public view { - assertEq(hybridERC7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH(), ORDERDATA_ONCHAIN_TYPEHASH); + assertEq(ERC7683AL.ORDERDATA_ONCHAIN_TYPEHASH, ORDERDATA_ONCHAIN_TYPEHASH); } function test_successful_userHimself() public { @@ -582,8 +202,6 @@ contract HybridERC7683_openFor is GaslessCrossChainOrderData { vm.prank(user); usdc.transfer(address(hybridERC7683Allocator), defaultAmount); - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = - _getGaslessCrossChainOrder(); IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); @@ -596,33 +214,43 @@ contract HybridERC7683_openFor is GaslessCrossChainOrderData { minReceived[0] = IOriginSettler.Output({ token: bytes32(uint256(uint160(address(usdc)))), amount: defaultMinimumAmount, - recipient: bytes32(uint256(uint160(user))), + recipient: bytes32(0), chainId: block.chainid }); + BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - TribunalClaim memory claim = - TribunalClaim({chainId: block.chainid, compact: compact_, sponsorSignature: '', allocatorSignature: ''}); + + Tribunal.BatchClaim memory claim = Tribunal.BatchClaim({ + chainId: block.chainid, + compact: compact_, + sponsorSignature: '', + allocatorSignature: '' + }); fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, mandate_, defaultTargetBlock, defaultMaximumBlocksAfterTarget) + originData: abi.encode(claim, mandate_.fills[0], adjuster, _buildFillHashes(mandate_)) }); IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ user: user, originChainId: block.chainid, openDeadline: uint32(compact_.expires), - fillDeadline: uint32(mandate_.expires), + fillDeadline: uint32(mandate_.fills[0].expires), orderId: bytes32(defaultNonce), maxSpent: maxSpent, minReceived: minReceived, fillInstructions: fillInstructions }); + + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_) = + _getGaslessCrossChainOrder(compact_, mandate_, true); + vm.prank(user); vm.expectEmit(true, false, false, true, address(hybridERC7683Allocator)); emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); - hybridERC7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); + hybridERC7683Allocator.openFor(gaslessCrossChainOrder_, '', ''); } function test_successful_relayed() public { @@ -630,8 +258,11 @@ contract HybridERC7683_openFor is GaslessCrossChainOrderData { vm.prank(user); usdc.transfer(address(hybridERC7683Allocator), defaultAmount); - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = - _getGaslessCrossChainOrder(); + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_) = + _getGaslessCrossChainOrder(compact_, mandate_, true); + IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); @@ -644,24 +275,26 @@ contract HybridERC7683_openFor is GaslessCrossChainOrderData { minReceived[0] = IOriginSettler.Output({ token: bytes32(uint256(uint160(address(usdc)))), amount: defaultMinimumAmount, - recipient: bytes32(uint256(uint160(user))), + recipient: bytes32(0), chainId: block.chainid }); - BatchCompact memory compact_ = _getCompact(); - Mandate memory mandate_ = _getMandate(); - TribunalClaim memory claim = - TribunalClaim({chainId: block.chainid, compact: compact_, sponsorSignature: '', allocatorSignature: ''}); + Tribunal.BatchClaim memory claim = Tribunal.BatchClaim({ + chainId: block.chainid, + compact: compact_, + sponsorSignature: '', + allocatorSignature: '' + }); fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, mandate_, defaultTargetBlock, defaultMaximumBlocksAfterTarget) + originData: abi.encode(claim, mandate_.fills[0], adjuster, _buildFillHashes(mandate_)) }); IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ user: user, originChainId: block.chainid, openDeadline: uint32(compact_.expires), - fillDeadline: uint32(mandate_.expires), + fillDeadline: uint32(mandate_.fills[0].expires), orderId: bytes32(defaultNonce), maxSpent: maxSpent, minReceived: minReceived, @@ -670,15 +303,11 @@ contract HybridERC7683_openFor is GaslessCrossChainOrderData { vm.prank(makeAddr('filler')); vm.expectEmit(true, false, false, true, address(hybridERC7683Allocator)); emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); - hybridERC7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); + hybridERC7683Allocator.openFor(gaslessCrossChainOrder_, '', ''); } } -contract HybridERC7683_authorizeClaim is OnChainCrossChainOrderData, GaslessCrossChainOrderData { - function setUp() public override(OnChainCrossChainOrderData, GaslessCrossChainOrderData) { - super.setUp(); - } - +contract HybridERC7683_authorizeClaim is MockAllocator { function test_revert_InvalidCaller() public { vm.expectRevert( abi.encodeWithSelector(IHybridAllocator.InvalidCaller.selector, address(this), address(compactContract)) @@ -709,14 +338,15 @@ contract HybridERC7683_authorizeClaim is OnChainCrossChainOrderData, GaslessCros BatchClaimComponent({id: usdcId, allocatedAmount: defaultAmount, portions: components}); batchClaimComponents[0] = batchClaimComponent; } + (bytes32 mandateHash,) = _hashMandate(mandate_); BatchClaim memory claim = BatchClaim({ - allocatorData: abi.encode(defaultTargetBlock, defaultMaximumBlocksAfterTarget), + allocatorData: '', sponsorSignature: '', sponsor: user, nonce: compact_.nonce, expires: compact_.expires, - witness: _createWitnessHash(mandate_), - witnessTypestring: witnessTypeString, + witness: mandateHash, + witnessTypestring: WITNESS_TYPESTRING_TRIBUNAL, claims: batchClaimComponents }); vm.prank(arbiter); @@ -731,28 +361,20 @@ contract HybridERC7683_authorizeClaim is OnChainCrossChainOrderData, GaslessCros vm.prank(user); usdc.transfer(address(hybridERC7683Allocator), defaultAmount); - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = - _getGaslessCrossChainOrder(); - BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_) = + _getGaslessCrossChainOrder(compact_, mandate_, true); + gaslessCrossChainOrder_ = _manipulateDeposit(gaslessCrossChainOrder_, true); + bytes memory sponsorSignature = _hashAndSign(compact_, mandate_, address(compactContract), signerPK); - bytes32 claimHash = _hashCompact(compact_, mandate_); + (bytes32 mandateHash,) = _hashMandate(mandate_); vm.prank(user); hybridERC7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); address filler = makeAddr('filler'); - // Create qualified claim hash for off-chain verification - bytes32 qualifiedClaimHash = - keccak256(abi.encode(hybridERC7683Allocator.QUALIFICATION_TYPEHASH(), claimHash, uint128(0), uint120(0))); - bytes32 digest = - keccak256(abi.encodePacked(bytes2(0x1901), compactContract.DOMAIN_SEPARATOR(), qualifiedClaimHash)); - - // Sign with the signer - bytes memory allocatorSignature = _signMessage(digest, signerPK); - Component[] memory components = new Component[](1); components[0] = Component({claimant: uint256(uint160(filler)), amount: defaultAmount}); BatchClaimComponent memory batchClaimComponent = @@ -760,13 +382,13 @@ contract HybridERC7683_authorizeClaim is OnChainCrossChainOrderData, GaslessCros BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); batchClaimComponents[0] = batchClaimComponent; BatchClaim memory claim = BatchClaim({ - allocatorData: abi.encode(defaultTargetBlock, defaultMaximumBlocksAfterTarget, allocatorSignature), + allocatorData: '', sponsorSignature: '', sponsor: user, nonce: defaultNonce, expires: compact_.expires, - witness: _createWitnessHash(mandate_), - witnessTypestring: witnessTypeString, + witness: mandateHash, + witnessTypestring: WITNESS_TYPESTRING_TRIBUNAL, claims: batchClaimComponents }); vm.prank(arbiter); @@ -783,29 +405,17 @@ contract HybridERC7683_authorizeClaim is OnChainCrossChainOrderData, GaslessCros BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandate_); vm.prank(user); compactContract.depositERC20AndRegister( - address(usdc), usdcLockTag, defaultAmount, claimHash, BATCH_COMPACT_WITNESS_TYPEHASH + address(usdc), usdcLockTag, defaultAmount, claimHash, COMPACT_TYPEHASH_WITH_MANDATE ); address filler = makeAddr('filler'); - // Create qualified claim hash for off-chain verification - bytes32 qualifiedClaimHash = keccak256( - abi.encode( - hybridERC7683Allocator.QUALIFICATION_TYPEHASH(), - claimHash, - defaultTargetBlock, - defaultMaximumBlocksAfterTarget - ) - ); - bytes32 digest = - keccak256(abi.encodePacked(bytes2(0x1901), compactContract.DOMAIN_SEPARATOR(), qualifiedClaimHash)); - - // Sign with wrong signer - bytes memory allocatorSignature = _signMessage(digest, signerPK); + // Sign with signer + bytes memory allocatorSignature = _hashAndSign(compact_, mandate_, address(compactContract), signerPK); BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); { @@ -815,14 +425,15 @@ contract HybridERC7683_authorizeClaim is OnChainCrossChainOrderData, GaslessCros BatchClaimComponent({id: usdcId, allocatedAmount: defaultAmount, portions: components}); batchClaimComponents[0] = batchClaimComponent; } + (bytes32 mandateHash,) = _hashMandate(mandate_); BatchClaim memory claim = BatchClaim({ - allocatorData: abi.encode(defaultTargetBlock, defaultMaximumBlocksAfterTarget, allocatorSignature), + allocatorData: allocatorSignature, sponsorSignature: '', sponsor: user, nonce: defaultNonce, expires: compact_.expires, - witness: _createWitnessHash(mandate_), - witnessTypestring: witnessTypeString, + witness: mandateHash, + witnessTypestring: WITNESS_TYPESTRING_TRIBUNAL, claims: batchClaimComponents }); @@ -837,29 +448,17 @@ contract HybridERC7683_authorizeClaim is OnChainCrossChainOrderData, GaslessCros BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandate_); vm.prank(user); compactContract.depositERC20AndRegister( - address(usdc), usdcLockTag, defaultAmount, claimHash, BATCH_COMPACT_WITNESS_TYPEHASH + address(usdc), usdcLockTag, defaultAmount, claimHash, COMPACT_TYPEHASH_WITH_MANDATE ); address filler = makeAddr('filler'); - // Create qualified claim hash for off-chain verification - bytes32 qualifiedClaimHash = keccak256( - abi.encode( - hybridERC7683Allocator.QUALIFICATION_TYPEHASH(), - claimHash, - defaultTargetBlock, - defaultMaximumBlocksAfterTarget - ) - ); - bytes32 digest = - keccak256(abi.encodePacked(bytes2(0x1901), compactContract.DOMAIN_SEPARATOR(), qualifiedClaimHash)); - // Sign with wrong signer - bytes memory allocatorSignature = _signMessage(digest, attackerPK); + bytes memory allocatorSignature = _hashAndSign(compact_, mandate_, address(compactContract), attackerPK); BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); { @@ -869,14 +468,15 @@ contract HybridERC7683_authorizeClaim is OnChainCrossChainOrderData, GaslessCros BatchClaimComponent({id: usdcId, allocatedAmount: defaultAmount, portions: components}); batchClaimComponents[0] = batchClaimComponent; } + (bytes32 mandateHash,) = _hashMandate(mandate_); BatchClaim memory claim = BatchClaim({ - allocatorData: abi.encode(defaultTargetBlock, defaultMaximumBlocksAfterTarget, allocatorSignature), + allocatorData: allocatorSignature, // signed by attacker sponsorSignature: '', sponsor: user, nonce: defaultNonce, expires: compact_.expires, - witness: _createWitnessHash(mandate_), - witnessTypestring: witnessTypeString, + witness: mandateHash, + witnessTypestring: WITNESS_TYPESTRING_TRIBUNAL, claims: batchClaimComponents }); @@ -892,29 +492,18 @@ contract HybridERC7683_authorizeClaim is OnChainCrossChainOrderData, GaslessCros BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 claimHash = _deriveClaimHash(compact_, mandate_); vm.prank(user); compactContract.depositERC20AndRegister( - address(usdc), usdcLockTag, defaultAmount, claimHash, BATCH_COMPACT_WITNESS_TYPEHASH + address(usdc), usdcLockTag, defaultAmount, claimHash, COMPACT_TYPEHASH_WITH_MANDATE ); address filler = makeAddr('filler'); - // Create qualified claim hash for off-chain verification - bytes32 qualifiedClaimHash = keccak256( - abi.encode( - hybridERC7683Allocator.QUALIFICATION_TYPEHASH(), - claimHash, - defaultTargetBlock, - defaultMaximumBlocksAfterTarget - ) - ); - bytes32 digest = - keccak256(abi.encodePacked(bytes2(0x1901), compactContract.DOMAIN_SEPARATOR(), qualifiedClaimHash)); - - // Sign with wrong signer - bytes memory allocatorSignature = _signMessage(digest, signerPK); + // Sign with signer and create the wrong length + bytes memory allocatorSignature = + abi.encodePacked(_hashAndSign(compact_, mandate_, address(compactContract), signerPK), uint8(0)); BatchClaimComponent[] memory batchClaimComponents = new BatchClaimComponent[](1); { @@ -924,14 +513,15 @@ contract HybridERC7683_authorizeClaim is OnChainCrossChainOrderData, GaslessCros BatchClaimComponent({id: usdcId, allocatedAmount: defaultAmount, portions: components}); batchClaimComponents[0] = batchClaimComponent; } + (bytes32 mandateHash,) = _hashMandate(mandate_); BatchClaim memory claim = BatchClaim({ - allocatorData: abi.encode(defaultTargetBlock, defaultMaximumBlocksAfterTarget, allocatorSignature, uint8(0)), // wrong length + allocatorData: allocatorSignature, // allocator signature with a length of 66 bytes sponsorSignature: '', sponsor: user, nonce: defaultNonce, expires: compact_.expires, - witness: _createWitnessHash(mandate_), - witnessTypestring: witnessTypeString, + witness: mandateHash, + witnessTypestring: WITNESS_TYPESTRING_TRIBUNAL, claims: batchClaimComponents }); @@ -941,16 +531,17 @@ contract HybridERC7683_authorizeClaim is OnChainCrossChainOrderData, GaslessCros } } -contract HybridERC7683_resolveFor is GaslessCrossChainOrderData { +contract HybridERC7683_resolveFor is MockAllocator { function test_revert_InvalidOrderDataType() public { - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_,) = _getGaslessCrossChainOrder(); + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_) = + _getGaslessCrossChainOrder(compact_, mandate_, true); bytes32 falseOrderDataType = keccak256('false'); gaslessCrossChainOrder_.orderDataType = falseOrderDataType; vm.expectRevert( abi.encodeWithSelector( - IHybridERC7683.InvalidOrderDataType.selector, - falseOrderDataType, - hybridERC7683Allocator.ORDERDATA_GASLESS_TYPEHASH() + IERC7683Allocator.InvalidOrderDataType.selector, falseOrderDataType, ORDERDATA_GASLESS_TYPEHASH ) ); hybridERC7683Allocator.resolveFor(gaslessCrossChainOrder_, ''); @@ -961,8 +552,6 @@ contract HybridERC7683_resolveFor is GaslessCrossChainOrderData { vm.prank(user); usdc.transfer(address(hybridERC7683Allocator), defaultAmount); - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_,) = _getGaslessCrossChainOrder(); - IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); @@ -975,31 +564,38 @@ contract HybridERC7683_resolveFor is GaslessCrossChainOrderData { minReceived[0] = IOriginSettler.Output({ token: bytes32(uint256(uint160(address(usdc)))), amount: defaultMinimumAmount, - recipient: bytes32(uint256(uint160(user))), + recipient: bytes32(0), chainId: block.chainid }); BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - TribunalClaim memory claim = - TribunalClaim({chainId: block.chainid, compact: compact_, sponsorSignature: '', allocatorSignature: ''}); + Tribunal.BatchClaim memory claim = Tribunal.BatchClaim({ + chainId: block.chainid, + compact: compact_, + sponsorSignature: '', + allocatorSignature: '' + }); fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, mandate_, defaultTargetBlock, defaultMaximumBlocksAfterTarget) + originData: abi.encode(claim, mandate_.fills[0], adjuster, _buildFillHashes(mandate_)) }); IOriginSettler.ResolvedCrossChainOrder memory expected = IOriginSettler.ResolvedCrossChainOrder({ user: user, originChainId: block.chainid, openDeadline: uint32(compact_.expires), - fillDeadline: uint32(mandate_.expires), + fillDeadline: uint32(mandate_.fills[0].expires), orderId: bytes32(defaultNonce), maxSpent: maxSpent, minReceived: minReceived, fillInstructions: fillInstructions }); + IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_ = + _getGaslessCrossChainOrder(compact_, mandate_, true); + vm.prank(user); IOriginSettler.ResolvedCrossChainOrder memory resolved = hybridERC7683Allocator.resolveFor(gaslessCrossChainOrder_, ''); @@ -1025,15 +621,15 @@ contract HybridERC7683_resolveFor is GaslessCrossChainOrderData { } } -contract HybridERC7683_resolve is OnChainCrossChainOrderData { +contract HybridERC7683_resolve is MockAllocator { function test_revert_InvalidOrderDataType() public { IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_ = _getOnChainCrossChainOrder(); onChainCrossChainOrder_.orderDataType = keccak256('false'); vm.expectRevert( abi.encodeWithSelector( - IHybridERC7683.InvalidOrderDataType.selector, + IERC7683Allocator.InvalidOrderDataType.selector, onChainCrossChainOrder_.orderDataType, - hybridERC7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH() + ORDERDATA_ONCHAIN_TYPEHASH ) ); hybridERC7683Allocator.resolve(onChainCrossChainOrder_); @@ -1052,31 +648,35 @@ contract HybridERC7683_resolve is OnChainCrossChainOrderData { minReceived[0] = IOriginSettler.Output({ token: bytes32(uint256(uint160(address(usdc)))), amount: defaultMinimumAmount, - recipient: bytes32(uint256(uint160(user))), + recipient: bytes32(0), chainId: block.chainid }); BatchCompact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - TribunalClaim memory claim = - TribunalClaim({chainId: block.chainid, compact: compact_, sponsorSignature: '', allocatorSignature: ''}); + Tribunal.BatchClaim memory claim = Tribunal.BatchClaim({ + chainId: block.chainid, + compact: compact_, + sponsorSignature: '', + allocatorSignature: '' + }); fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, mandate_, defaultTargetBlock, defaultMaximumBlocksAfterTarget) + originData: abi.encode(claim, mandate_.fills[0], adjuster, _buildFillHashes(mandate_)) }); IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ user: user, originChainId: block.chainid, openDeadline: uint32(compact_.expires), - fillDeadline: uint32(mandate_.expires), + fillDeadline: uint32(mandate_.fills[0].expires), orderId: bytes32(defaultNonce), maxSpent: maxSpent, minReceived: minReceived, fillInstructions: fillInstructions }); IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_ = - _getOnChainCrossChainOrder(compact_, mandate_, hybridERC7683Allocator.ORDERDATA_ONCHAIN_TYPEHASH()); + _getOnChainCrossChainOrder(compact_, mandate_); vm.prank(user); IOriginSettler.ResolvedCrossChainOrder memory resolved = hybridERC7683Allocator.resolve(onChainCrossChainOrder_); assertEq(resolved.user, resolvedCrossChainOrder.user); @@ -1107,16 +707,7 @@ contract HybridERC7683_resolve is OnChainCrossChainOrderData { } } -contract HybridERC7683_qualificationTypehash is MocksSetup { - function test_qualificationTypehash() public view { - assertEq( - hybridERC7683Allocator.QUALIFICATION_TYPEHASH(), - 0x59866b84bd1f6c909cf2a31efd20c59e6c902e50f2c196994e5aa85cdc7d7ce0 - ); - } -} - -contract HybridERC7683_hybridAllocatorInheritance is MocksSetup { +contract HybridERC7683_hybridAllocatorInheritance is MockAllocator { function test_inheritsHybridAllocatorFunctionality() public view { // Test that it properly inherits from HybridAllocator assertEq(hybridERC7683Allocator.nonces(), 0); @@ -1134,10 +725,14 @@ contract HybridERC7683_hybridAllocatorInheritance is MocksSetup { vm.prank(user); usdc.transfer(address(hybridERC7683Allocator), defaultAmount); + (bytes32 witness,) = _hashMandate(_getMandate()); + (bytes32 claimHash, uint256[] memory registeredAmounts, uint256 nonce) = hybridERC7683Allocator - .allocateAndRegister(user, idsAndAmounts, arbiter, _getClaimExpiration(), BATCH_COMPACT_WITNESS_TYPEHASH, ''); + .allocateAndRegister( + user, idsAndAmounts, arbiter, _getClaimExpiration(), COMPACT_TYPEHASH_WITH_MANDATE, witness + ); - assertTrue(compactContract.isRegistered(user, claimHash, BATCH_COMPACT_WITNESS_TYPEHASH)); + assertTrue(compactContract.isRegistered(user, claimHash, COMPACT_TYPEHASH_WITH_MANDATE)); assertTrue( hybridERC7683Allocator.isClaimAuthorized(claimHash, address(0), address(0), 0, 0, new uint256[2][](0), '') ); @@ -1146,8 +741,4 @@ contract HybridERC7683_hybridAllocatorInheritance is MocksSetup { assertEq(compactContract.balanceOf(address(user), idsAndAmounts[0][0]), defaultAmount); assertEq(nonce, 1); } - - function _getClaimExpiration() internal view returns (uint256) { - return vm.getBlockTimestamp() + defaultResetPeriodTimestamp; - } } diff --git a/test/OnChainAllocator.t.sol b/test/OnChainAllocator.t.sol index fd7bd18..1595296 100644 --- a/test/OnChainAllocator.t.sol +++ b/test/OnChainAllocator.t.sol @@ -51,6 +51,8 @@ contract OnChainAllocatorTest is Test, TestHelper { uint256 internal defaultAmount; uint32 internal defaultExpiration; + uint256 defaultNonce; + function setUp() public { compact = new TheCompact(); arbiter = makeAddr('arbiter'); @@ -68,12 +70,17 @@ contract OnChainAllocatorTest is Test, TestHelper { defaultAmount = 1 ether; defaultExpiration = uint32(block.timestamp + 300); // 5 minutes fits 10-minute reset period + defaultNonce = _composeNonceUint(user, 1); } /* --------------------------------------------------------------------- */ /* Helpers */ /* --------------------------------------------------------------------- */ + function _composeNonceUint(address a, uint256 nonce) internal pure returns (uint256) { + return (uint256(uint160(a)) << 96) | nonce; + } + function _commitmentsHash(Lock[] memory commitments) internal pure returns (bytes32) { bytes32[] memory hashes = new bytes32[](commitments.length); for (uint256 i = 0; i < commitments.length; i++) { @@ -213,7 +220,7 @@ contract OnChainAllocatorTest is Test, TestHelper { idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(0)); idsAndAmounts[0][1] = defaultAmount; - assertEq(nonce, 1); + assertEq(nonce, defaultNonce); assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); } @@ -236,7 +243,7 @@ contract OnChainAllocatorTest is Test, TestHelper { idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); idsAndAmounts[0][1] = defaultAmount; - assertEq(nonce, 1); + assertEq(nonce, defaultNonce); assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); } @@ -260,7 +267,7 @@ contract OnChainAllocatorTest is Test, TestHelper { idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); idsAndAmounts[0][1] = amount; - assertEq(nonce, 1); + assertEq(nonce, defaultNonce); assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); vm.prank(user); @@ -268,7 +275,7 @@ contract OnChainAllocatorTest is Test, TestHelper { allocator.allocate(commitments, arbiter, defaultExpiration + 10, BATCH_COMPACT_TYPEHASH, bytes32(0)); vm.snapshotGasLastCall('allocate_second_erc20'); - assertEq(nonce, 2); + assertEq(nonce, defaultNonce + 1); assertTrue( allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration + 10, idsAndAmounts, '') ); @@ -280,7 +287,7 @@ contract OnChainAllocatorTest is Test, TestHelper { allocator.allocate(commitments, arbiter, defaultExpiration + 10, BATCH_COMPACT_TYPEHASH, bytes32(0)); vm.snapshotGasLastCall('allocate_and_delete_expired_allocation'); - assertEq(nonce, 3); + assertEq(nonce, defaultNonce + 2); assertTrue( allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration + 10, idsAndAmounts, '') ); @@ -302,7 +309,8 @@ contract OnChainAllocatorTest is Test, TestHelper { compact.depositERC20(address(usdc), commitments[0].lockTag, depositAmount, user); vm.stopPrank(); - bytes32 claimHash = _createClaimHash(user, arbiter, 1, defaultExpiration, commitments, witness); + uint256 expectedNonce = defaultNonce; + bytes32 claimHash = _createClaimHash(user, arbiter, expectedNonce, defaultExpiration, commitments, witness); // first allocation @@ -310,7 +318,7 @@ contract OnChainAllocatorTest is Test, TestHelper { vm.prank(user); vm.expectEmit(true, true, true, true); - emit IOnChainAllocation.Allocated(user, commitments, 1, defaultExpiration, claimHash); + emit IOnChainAllocation.Allocated(user, commitments, expectedNonce, defaultExpiration, claimHash); (bytes32 returnedClaimHash, uint256 nonce) = allocator.allocate(commitments, arbiter, defaultExpiration, typehash, witness); @@ -320,7 +328,7 @@ contract OnChainAllocatorTest is Test, TestHelper { idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); idsAndAmounts[0][1] = firstAmount; - assertEq(nonce, 1); + assertEq(nonce, expectedNonce, 'nonce 1'); assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); // second allocation @@ -340,9 +348,10 @@ contract OnChainAllocatorTest is Test, TestHelper { ); } else { // expect a successful second allocation - claimHash = _createClaimHash(user, arbiter, 2, defaultExpiration, commitments, witness); + expectedNonce = defaultNonce + 1; + claimHash = _createClaimHash(user, arbiter, expectedNonce, defaultExpiration, commitments, witness); vm.expectEmit(true, true, true, true); - emit IOnChainAllocation.Allocated(user, commitments, 2, defaultExpiration, claimHash); + emit IOnChainAllocation.Allocated(user, commitments, expectedNonce, defaultExpiration, claimHash); } (claimHash, nonce) = allocator.allocate(commitments, arbiter, defaultExpiration, typehash, witness); @@ -350,12 +359,16 @@ contract OnChainAllocatorTest is Test, TestHelper { // Check the allocations idsAndAmounts[0][1] = secondAmount; - assertEq(nonce, 2); + assertEq(nonce, expectedNonce, 'nonce 1'); assertTrue( - allocator.isClaimAuthorized(claimHash, arbiter, user, 1, /*nonce*/ defaultExpiration, idsAndAmounts, '') + allocator.isClaimAuthorized( + claimHash, arbiter, user, defaultNonce, /*nonce*/ defaultExpiration, idsAndAmounts, '' + ) ); assertTrue( - allocator.isClaimAuthorized(claimHash, arbiter, user, 2, /*nonce*/ defaultExpiration, idsAndAmounts, '') + allocator.isClaimAuthorized( + claimHash, arbiter, user, defaultNonce + 1, /*nonce*/ defaultExpiration, idsAndAmounts, '' + ) ); uint256 amountToAttest = depositAmount - (uint256(secondAmount) + uint256(firstAmount)); @@ -378,9 +391,11 @@ contract OnChainAllocatorTest is Test, TestHelper { // Second allocation should be possible after the first one is expired vm.warp(defaultExpiration + 1); uint32 expiration = defaultExpiration + 100; + expectedNonce = defaultNonce + 1; + vm.prank(user); (claimHash, nonce) = allocator.allocate(commitments, arbiter, expiration, typehash, witness); - assertEq(nonce, 2); + assertEq(nonce, expectedNonce, 'nonce 2'); assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, expiration, idsAndAmounts, '')); } } @@ -412,11 +427,11 @@ contract OnChainAllocatorTest is Test, TestHelper { (address attacker, uint256 attackerPK) = makeAddrAndKey('attacker'); // build digest exactly like allocator expects - bytes32 nonceKey = keccak256(abi.encode(address(0), user)); - uint256 nonceBefore = allocator.nonces(nonceKey); + uint256 expectedNonce = _composeNonceUint(user, allocator.nonces(user) + 1); + bytes32 commitmentsHash = _commitmentsHash(commitments); bytes32 claimHash = keccak256( - abi.encode(BATCH_COMPACT_TYPEHASH, arbiter, user, nonceBefore + 1, defaultExpiration, commitmentsHash) + abi.encode(BATCH_COMPACT_TYPEHASH, arbiter, user, expectedNonce, defaultExpiration, commitmentsHash) ); bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), compact.DOMAIN_SEPARATOR(), claimHash)); (uint8 v, bytes32 r, bytes32 s) = vm.sign(attackerPK, digest); @@ -434,11 +449,11 @@ contract OnChainAllocatorTest is Test, TestHelper { compact.depositNative{value: defaultAmount}(commitments[0].lockTag, user); // build digest exactly like allocator expects - bytes32 nonceKey = keccak256(abi.encode(address(0), user)); - uint256 nonceBefore = allocator.nonces(nonceKey); + uint256 expectedNonce = _composeNonceUint(user, allocator.nonces(user) + 1); + bytes32 commitmentsHash = _commitmentsHash(commitments); bytes32 claimHash = keccak256( - abi.encode(BATCH_COMPACT_TYPEHASH, arbiter, user, nonceBefore + 1, defaultExpiration, commitmentsHash) + abi.encode(BATCH_COMPACT_TYPEHASH, arbiter, user, expectedNonce, defaultExpiration, commitmentsHash) ); bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), compact.DOMAIN_SEPARATOR(), claimHash)); (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPK, digest); @@ -456,11 +471,11 @@ contract OnChainAllocatorTest is Test, TestHelper { compact.depositNative{value: defaultAmount}(commitments[0].lockTag, user); // build digest exactly like allocator expects - bytes32 nonceKey = keccak256(abi.encode(address(0), user)); - uint256 nonceBefore = allocator.nonces(nonceKey); + uint256 expectedNonce = _composeNonceUint(user, allocator.nonces(user) + 1); + bytes32 commitmentsHash = _commitmentsHash(commitments); bytes32 claimHash = keccak256( - abi.encode(BATCH_COMPACT_TYPEHASH, arbiter, user, nonceBefore + 1, defaultExpiration, commitmentsHash) + abi.encode(BATCH_COMPACT_TYPEHASH, arbiter, user, expectedNonce, defaultExpiration, commitmentsHash) ); bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), compact.DOMAIN_SEPARATOR(), claimHash)); (bytes32 r, bytes32 vs) = vm.signCompact(userPK, digest); @@ -475,7 +490,7 @@ contract OnChainAllocatorTest is Test, TestHelper { idsAndAmounts[0][1] = defaultAmount; assertEq(returnedHash, claimHash); - assertEq(nonce, nonceBefore + 1); + assertEq(nonce, expectedNonce); assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); } @@ -486,11 +501,10 @@ contract OnChainAllocatorTest is Test, TestHelper { compact.depositNative{value: defaultAmount}(commitments[0].lockTag, user); // build digest exactly like allocator expects - bytes32 nonceKey = keccak256(abi.encode(address(0), user)); - uint256 nonceBefore = allocator.nonces(nonceKey); + uint256 expectedNonce = _composeNonceUint(user, allocator.nonces(user) + 1); bytes32 commitmentsHash = _commitmentsHash(commitments); bytes32 claimHash = keccak256( - abi.encode(BATCH_COMPACT_TYPEHASH, arbiter, user, nonceBefore + 1, defaultExpiration, commitmentsHash) + abi.encode(BATCH_COMPACT_TYPEHASH, arbiter, user, expectedNonce, defaultExpiration, commitmentsHash) ); bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), compact.DOMAIN_SEPARATOR(), claimHash)); (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPK, digest); @@ -505,7 +519,7 @@ contract OnChainAllocatorTest is Test, TestHelper { idsAndAmounts[0][1] = defaultAmount; assertEq(returnedHash, claimHash); - assertEq(nonce, nonceBefore + 1); + assertEq(nonce, expectedNonce); assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); } @@ -516,13 +530,12 @@ contract OnChainAllocatorTest is Test, TestHelper { compact.depositNative{value: defaultAmount}(commitments[0].lockTag, user); // build digest exactly like allocator expects - bytes32 nonceKey = keccak256(abi.encode(address(0), user)); - uint256 nonceBefore = allocator.nonces(nonceKey); + uint256 expectedNonce = _composeNonceUint(user, allocator.nonces(user) + 1); bytes32 witness = bytes32(keccak256('witness')); bytes32 commitmentsHash = _commitmentsHash(commitments); bytes32 claimHash = keccak256( abi.encode( - BATCH_COMPACT_TYPEHASH, arbiter, user, nonceBefore + 1, defaultExpiration, commitmentsHash, witness + BATCH_COMPACT_TYPEHASH, arbiter, user, expectedNonce, defaultExpiration, commitmentsHash, witness ) ); bytes memory sig; @@ -540,10 +553,10 @@ contract OnChainAllocatorTest is Test, TestHelper { idsAndAmounts[0][1] = defaultAmount; assertEq(returnedHash, claimHash); - assertEq(nonce, nonceBefore + 1); + assertEq(nonce, expectedNonce); assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); - assertEq(allocator.nonces(nonceKey), nonceBefore + 1); + assertEq(allocator.nonces(user), 1); } function test_allocateFor_revert_InvalidRegistration(address relayer) public { @@ -554,9 +567,7 @@ contract OnChainAllocatorTest is Test, TestHelper { compact.depositNative{value: defaultAmount}(commitments[0].lockTag, user); // Nonce that allocateFor will use - bytes32 nonceKey = keccak256(abi.encode(address(0), user)); - uint256 nonceBefore = allocator.nonces(nonceKey); - uint256 expectedNonce = nonceBefore + 1; + uint256 expectedNonce = _composeNonceUint(user, allocator.nonces(user) + 1); // Compute claimHash that allocateFor will create internally bytes32 claimHash = _createClaimHash(user, arbiter, expectedNonce, defaultExpiration, commitments, bytes32(0)); @@ -580,10 +591,8 @@ contract OnChainAllocatorTest is Test, TestHelper { Lock[] memory commitments = new Lock[](1); commitments[0] = _makeLock(address(0), defaultAmount); - // Determine nonce as allocator will use - bytes32 nonceKey = keccak256(abi.encode(address(0), user)); - uint256 nonceBefore = allocator.nonces(nonceKey); - uint256 expectedNonce = nonceBefore + 1; + // Determine nonce the allocator will use + uint256 expectedNonce = _composeNonceUint(user, allocator.nonces(user) + 1); // Pre-compute claimHash that `allocateFor` will produce bytes32 claimHash = _createClaimHash(user, arbiter, expectedNonce, defaultExpiration, commitments, bytes32(0)); @@ -885,10 +894,9 @@ contract OnChainAllocatorTest is Test, TestHelper { recipient, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0), '' ); - assertEq(returnedNonce, 1); + assertEq(returnedNonce, _composeNonceUint(caller, 1)); // storage nonce is only incremented in executeAllocation - bytes32 nonceKey = keccak256(abi.encode(caller, recipient)); - assertEq(allocator.nonces(nonceKey), 0); + assertEq(allocator.nonces(caller), 0); } function test_executeAllocation_success_viaCaller_singleERC20() public { @@ -908,14 +916,19 @@ contract OnChainAllocatorTest is Test, TestHelper { vm.snapshotGasLastCall('onchain_execute_single'); // nonce is scoped to (callerContract, recipient) - bytes32 nonceKey = keccak256(abi.encode(address(allocationCaller), recipient)); - assertEq(allocator.nonces(nonceKey), 1); + assertEq(allocator.nonces(address(allocationCaller)), 1); + uint256 expectedNonce = _composeNonceUint(address(allocationCaller), 1); // compute claim hash and check authorization Lock[] memory commitments = _idsAndAmountsToCommitments(idsAndAmounts); - bytes32 claimHash = _createClaimHash(recipient, arbiter, 1, defaultExpiration, commitments, bytes32(0)); + bytes32 claimHash = + _createClaimHash(recipient, arbiter, expectedNonce, defaultExpiration, commitments, bytes32(0)); - assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, recipient, 1, defaultExpiration, idsAndAmounts, '')); + assertTrue( + allocator.isClaimAuthorized( + claimHash, arbiter, recipient, expectedNonce, defaultExpiration, idsAndAmounts, '' + ) + ); } function test_executeAllocation_revert_InvalidPreparation() public { @@ -948,7 +961,14 @@ contract OnChainAllocatorTest is Test, TestHelper { // Expect the precise error and arguments from AllocatorLib // Compute the claimHash that AllocatorLib will recompute during execute. Lock[] memory commitments = _idsAndAmountsToCommitments(idsAndAmounts); - bytes32 expectedClaimHash = _createClaimHash(recipient, arbiter, 1, defaultExpiration, commitments, bytes32(0)); + bytes32 expectedClaimHash = _createClaimHash( + recipient, + arbiter, + _composeNonceUint(address(allocationCaller), 1), + defaultExpiration, + commitments, + bytes32(0) + ); vm.prank(user); vm.expectRevert( abi.encodeWithSelector( @@ -1003,18 +1023,21 @@ contract OnChainAllocatorTest is Test, TestHelper { vm.snapshotGasLastCall('onchain_execute_double'); // authorization with the measured amounts - bytes32 nonceKey = keccak256(abi.encode(address(allocationCaller), recipient)); - uint256 nonce = allocator.nonces(nonceKey); - assertEq(nonce, 1); + uint256 expectedNonce = _composeNonceUint(address(allocationCaller), 1); assertTrue( allocator.isClaimAuthorized( _createClaimHash( - recipient, arbiter, nonce, defaultExpiration, _idsAndAmountsToCommitments(idsAndAmounts), bytes32(0) + recipient, + arbiter, + expectedNonce, + defaultExpiration, + _idsAndAmountsToCommitments(idsAndAmounts), + bytes32(0) ), arbiter, recipient, - nonce, + expectedNonce, defaultExpiration, idsAndAmounts, '' @@ -1143,7 +1166,7 @@ contract OnChainAllocatorTest is Test, TestHelper { idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); idsAndAmounts[0][1] = defaultAmount; - assertEq(nonce, 1); + assertEq(nonce, _composeNonceUint(caller, 1)); assertEq(registeredAmounts.length, 1); assertEq(registeredAmounts[0], defaultAmount); assertEq(ERC6909(address(compact)).balanceOf(recipient, idsAndAmounts[0][0]), defaultAmount); @@ -1172,7 +1195,7 @@ contract OnChainAllocatorTest is Test, TestHelper { idsAndAmounts[0][0] = _toId(Scope.Multichain, ResetPeriod.TenMinutes, address(allocator), address(usdc)); idsAndAmounts[0][1] = defaultAmount; - assertEq(nonce, 1); + assertEq(nonce, _composeNonceUint(caller, 1)); assertEq(registeredAmounts.length, 1); assertEq(registeredAmounts[0], defaultAmount); assertEq(ERC6909(address(compact)).balanceOf(recipient, idsAndAmounts[0][0]), defaultAmount); diff --git a/test/util/ERC7683TestHelper.sol b/test/util/ERC7683TestHelper.sol new file mode 100644 index 0000000..fc27880 --- /dev/null +++ b/test/util/ERC7683TestHelper.sol @@ -0,0 +1,427 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; +import {ERC6909} from '@solady/tokens/ERC6909.sol'; +import {TheCompact} from '@uniswap/the-compact/TheCompact.sol'; +import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; + +import {IdLib} from '@uniswap/the-compact/lib/IdLib.sol'; + +import {BatchClaim} from '@uniswap/the-compact/types/BatchClaims.sol'; +import {BatchClaimComponent, Component} from '@uniswap/the-compact/types/Components.sol'; +import {BatchCompact, COMPACT_TYPEHASH, LOCK_TYPEHASH, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; +import {ForcedWithdrawalStatus} from '@uniswap/the-compact/types/ForcedWithdrawalStatus.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 {TestHelper} from 'test/util/TestHelper.sol'; + +import {IOnChainAllocation} from '@uniswap/the-compact/interfaces/IOnChainAllocation.sol'; + +import {Tribunal} from '@uniswap/tribunal/Tribunal.sol'; +import {Fill, Mandate, RecipientCallback} from '@uniswap/tribunal/types/TribunalStructs.sol'; +import { + COMPACT_TYPEHASH_WITH_MANDATE, + MANDATE_BATCH_COMPACT_TYPEHASH, + MANDATE_FILL_TYPEHASH, + MANDATE_RECIPIENT_CALLBACK_TYPEHASH, + MANDATE_TYPEHASH +} from '@uniswap/tribunal/types/TribunalTypeHashes.sol'; + +import {ERC7683Allocator} from 'src/allocators/ERC7683Allocator.sol'; +import {ERC7683AllocatorLib as ERC7683AL} from 'src/allocators/lib/ERC7683AllocatorLib.sol'; +import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; +import {IERC7683Allocator} from 'src/interfaces/IERC7683Allocator.sol'; +import {IOnChainAllocator} from 'src/interfaces/IOnChainAllocator.sol'; +import {ERC20Mock} from 'src/test/ERC20Mock.sol'; + +abstract contract MocksSetup is Test, TestHelper { + address user = makeAddr('user'); + uint256 userPK; + address attacker; + uint256 attackerPK; + address arbiter; + address tribunal; + address adjuster; + ERC20Mock usdc; + TheCompact compactContract; + address allocator; + bytes12 usdcLockTag; + uint256 usdcId; + + ResetPeriod defaultResetPeriod = ResetPeriod.OneMinute; + Scope defaultScope = Scope.Multichain; + uint256 defaultResetPeriodTimestamp = 60 - 1; + uint256 defaultAmount = 1000; + uint256 defaultNonce; + uint256 defaultOutputChainId = 130; + address defaultOutputToken = makeAddr('outputToken'); + uint256 defaultMinimumAmount = 1000; + uint256 defaultBaselinePriorityFee = 0; + uint256 defaultScalingFactor = 1e18; + uint256[] defaultPriceCurve = new uint256[](0); + bytes32 defaultSalt = bytes32(0x0000000000000000000000000000000000000000000000000000000000000007); + + uint256[2][] defaultIdsAndAmounts = new uint256[2][](1); + Lock[] defaultCommitments; + + bytes32 ORDERDATA_GASLESS_TYPEHASH; + bytes32 ORDERDATA_ONCHAIN_TYPEHASH; + + uint256 NONCES_STORAGE_SLOT = 1; + + function setUp() public virtual { + (user, userPK) = makeAddrAndKey('user'); + arbiter = makeAddr('arbiter'); + tribunal = makeAddr('tribunal'); + adjuster = makeAddr('adjuster'); + usdc = new ERC20Mock('USDC', 'USDC'); + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + vm.stopPrank(); + + usdcLockTag = _toLockTag(allocator, defaultScope, defaultResetPeriod); + usdcId = _toId(defaultScope, defaultResetPeriod, allocator, address(usdc)); + (attacker, attackerPK) = makeAddrAndKey('attacker'); + + ORDERDATA_GASLESS_TYPEHASH = ERC7683AL.ORDERDATA_GASLESS_TYPEHASH; + ORDERDATA_ONCHAIN_TYPEHASH = ERC7683AL.ORDERDATA_ONCHAIN_TYPEHASH; + } + + function _setUp(address allocator_, TheCompact compactContract_, uint256 defaultNonce_) internal { + allocator = allocator_; + compactContract = compactContract_; + defaultNonce = defaultNonce_; + } + + function _composeNonceUint(address a, uint256 nonce) internal pure returns (uint256) { + return (uint256(uint160(a)) << 96) | nonce; + } + + function _composeNonce(address a, uint256 nonce) internal pure returns (bytes32) { + return bytes32(_composeNonceUint(a, nonce)); + } +} + +abstract contract CreateHash is MocksSetup { + // stringified types + string EIP712_DOMAIN_TYPE = 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'; // Hashed inside the function + // EIP712 domain type + string name = 'The Compact'; + string version = '1'; + bytes32 internal constant EMPTY_HASH = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; + + function _hashCommitments(Lock[] memory commitments) internal pure returns (bytes32) { + bytes32[] memory hashes = new bytes32[](commitments.length); + for (uint256 i = 0; i < commitments.length; i++) { + hashes[i] = keccak256( + abi.encode(LOCK_TYPEHASH, commitments[i].lockTag, commitments[i].token, commitments[i].amount) + ); + } + return keccak256(abi.encodePacked(hashes)); + } + + function _hashRecipientCallback(RecipientCallback[] memory rc) internal pure returns (bytes32) { + if (rc.length == 0) return EMPTY_HASH; + revert('RecipientCallback not supported in tests'); + } + + function _hashFill(Fill memory f) internal pure returns (bytes32) { + return keccak256( + abi.encode( + MANDATE_FILL_TYPEHASH, + f.chainId, + f.tribunal, + f.expires, + f.fillToken, + f.minimumFillAmount, + f.baselinePriorityFee, + f.scalingFactor, + keccak256(abi.encodePacked(f.priceCurve)), + f.recipient, + _hashRecipientCallback(f.recipientCallback), + f.salt + ) + ); + } + + function _hashMandate(Mandate memory m) internal pure returns (bytes32 mandateHash, bytes32[] memory fillHashes) { + fillHashes = new bytes32[](m.fills.length); + for (uint256 i = 0; i < m.fills.length; i++) { + fillHashes[i] = _hashFill(m.fills[i]); + } + mandateHash = keccak256(abi.encode(MANDATE_TYPEHASH, m.adjuster, keccak256(abi.encodePacked(fillHashes)))); + } + + function _deriveClaimHash(BatchCompact memory compact, Mandate memory mandate) internal pure returns (bytes32) { + (bytes32 mandateHash,) = _hashMandate(mandate); + return _deriveClaimHash(compact, mandateHash); + } + + function _deriveClaimHash(BatchCompact memory compact, bytes32 mandateHash) internal pure returns (bytes32) { + bytes32 commitmentsHash = _staticHashCommitments(compact.commitments); + return keccak256( + abi.encode( + COMPACT_TYPEHASH_WITH_MANDATE, + compact.arbiter, + compact.sponsor, + compact.nonce, + compact.expires, + commitmentsHash, + mandateHash + ) + ); + } + + function _staticHashCommitments(Lock[] memory commitments) private pure returns (bytes32) { + bytes32[] memory hashes = new bytes32[](commitments.length); + for (uint256 i = 0; i < commitments.length; i++) { + hashes[i] = keccak256( + abi.encode(LOCK_TYPEHASH, commitments[i].lockTag, commitments[i].token, commitments[i].amount) + ); + } + return keccak256(abi.encodePacked(hashes)); + } + + function _buildFillHashes(Mandate memory m) internal pure returns (bytes32[] memory hashes) { + hashes = new bytes32[](m.fills.length); + for (uint256 i = 0; i < m.fills.length; i++) { + hashes[i] = _hashFill(m.fills[i]); + } + } + + function _createDigest(BatchCompact memory data, Mandate memory mandate, address verifyingContract) + internal + view + returns (bytes32 digest) + { + (bytes32 mandateHash,) = _hashMandate(mandate); + bytes32 compactHash = _deriveClaimHash(data, mandateHash); + // hash typed data + digest = keccak256( + abi.encodePacked( + '\x19\x01', // backslash is needed to escape the character + _domainSeparator(verifyingContract), + compactHash + ) + ); + } + + function _domainSeparator(address verifyingContract) internal view returns (bytes32) { + return keccak256( + abi.encode( + keccak256(bytes(EIP712_DOMAIN_TYPE)), + keccak256(bytes(name)), + keccak256(bytes(version)), + block.chainid, + verifyingContract + ) + ); + } + + function _signMessage(bytes32 hash_, uint256 signerPK_) internal pure returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPK_, hash_); + return abi.encodePacked(r, s, v); + } + + function _hashAndSign(BatchCompact memory data, Mandate memory mandate, address verifyingContract, uint256 signerPK) + internal + view + returns (bytes memory) + { + bytes32 hash = _createDigest(data, mandate, verifyingContract); + bytes memory signature = _signMessage(hash, signerPK); + return signature; + } +} + +abstract contract CompactData is CreateHash { + BatchCompact private compact; + Mandate private mandate; + + function setUp() public virtual override { + super.setUp(); + + defaultIdsAndAmounts[0][0] = usdcId; + defaultIdsAndAmounts[0][1] = defaultAmount; + + defaultCommitments.push(Lock({lockTag: usdcLockTag, token: address(usdc), amount: defaultAmount})); + + compact.arbiter = arbiter; + compact.sponsor = user; + compact.nonce = defaultNonce; + compact.expires = _getClaimExpiration(); + compact.commitments = defaultCommitments; + + Fill memory mainFill; + mainFill.chainId = defaultOutputChainId; + mainFill.tribunal = tribunal; + mainFill.expires = _getFillExpiration(); + mainFill.fillToken = defaultOutputToken; + mainFill.minimumFillAmount = defaultMinimumAmount; + mainFill.baselinePriorityFee = defaultBaselinePriorityFee; + mainFill.scalingFactor = defaultScalingFactor; + mainFill.priceCurve = defaultPriceCurve; + mainFill.recipient = user; + mainFill.salt = defaultSalt; + + mandate.adjuster = adjuster; + mandate.fills = new Fill[](1); + mandate.fills[0] = mainFill; + } + + function _getCompact() internal returns (BatchCompact memory) { + compact.expires = _getClaimExpiration(); + return compact; + } + + function _getMandate() internal returns (Mandate memory) { + mandate.fills[0].expires = _getFillExpiration(); + return mandate; + } + + function _getFillExpiration() internal view returns (uint256) { + return vm.getBlockTimestamp() + defaultResetPeriodTimestamp - 1; + } + + function _getClaimExpiration() internal view returns (uint256) { + return vm.getBlockTimestamp() + defaultResetPeriodTimestamp; + } +} + +abstract contract GaslessCrossChainOrderData is CompactData { + IOriginSettler.GaslessCrossChainOrder private gaslessCrossChainOrder; + + function setUp() public virtual override { + super.setUp(); + + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + gaslessCrossChainOrder.originSettler = allocator; + gaslessCrossChainOrder.user = compact_.sponsor; + gaslessCrossChainOrder.nonce = _composeNonceUint(compact_.sponsor, defaultNonce); + gaslessCrossChainOrder.originChainId = block.chainid; + gaslessCrossChainOrder.openDeadline = uint32(_getClaimExpiration()); + gaslessCrossChainOrder.fillDeadline = uint32(_getFillExpiration()); + gaslessCrossChainOrder.orderDataType = ORDERDATA_GASLESS_TYPEHASH; + gaslessCrossChainOrder.orderData = abi.encode( + IERC7683Allocator.OrderDataGasless({ + order: IERC7683Allocator.Order({ + arbiter: compact_.arbiter, + commitments: compact_.commitments, + mandate: mandate_ + }), + deposit: false + }) + ); + } + + function _getGaslessCrossChainOrder() internal view returns (IOriginSettler.GaslessCrossChainOrder memory) { + return gaslessCrossChainOrder; + } + + function _getGaslessCrossChainOrder(BatchCompact memory compact_, Mandate memory mandate_, bool deposit) + internal + view + returns (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_) + { + gaslessCrossChainOrder_.originSettler = allocator; + gaslessCrossChainOrder_.user = compact_.sponsor; + gaslessCrossChainOrder_.nonce = compact_.nonce; + gaslessCrossChainOrder_.originChainId = block.chainid; + gaslessCrossChainOrder_.openDeadline = uint32(compact_.expires); + gaslessCrossChainOrder_.fillDeadline = uint32(mandate_.fills[0].expires); + gaslessCrossChainOrder_.orderDataType = ORDERDATA_GASLESS_TYPEHASH; + gaslessCrossChainOrder_.orderData = abi.encode( + IERC7683Allocator.OrderDataGasless({ + order: IERC7683Allocator.Order({ + arbiter: compact_.arbiter, + commitments: compact_.commitments, + mandate: mandate_ + }), + deposit: deposit + }) + ); + return gaslessCrossChainOrder_; + } + + function _manipulateDeposit(IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bool deposit) + internal + pure + returns (IOriginSettler.GaslessCrossChainOrder memory) + { + bytes memory orderData = gaslessCrossChainOrder_.orderData; + assembly ("memory-safe") { + mstore(add(orderData, 0x60), deposit) + } + return gaslessCrossChainOrder_; + } +} + +abstract contract OnChainCrossChainOrderData is CompactData { + IOriginSettler.OnchainCrossChainOrder private onchainCrossChainOrder; + + function setUp() public virtual override { + super.setUp(); + + BatchCompact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + onchainCrossChainOrder.fillDeadline = uint32(_getFillExpiration()); + onchainCrossChainOrder.orderDataType = ORDERDATA_ONCHAIN_TYPEHASH; + onchainCrossChainOrder.orderData = abi.encode( + IERC7683Allocator.OrderDataOnChain({ + order: IERC7683Allocator.Order({ + arbiter: compact_.arbiter, + commitments: compact_.commitments, + mandate: mandate_ + }), + expires: uint32(compact_.expires) + }) + ); + } + + function _getOnChainCrossChainOrder() internal view returns (IOriginSettler.OnchainCrossChainOrder memory) { + return onchainCrossChainOrder; + } + + function _getOnChainCrossChainOrder(BatchCompact memory compact_, Mandate memory mandate_) + internal + view + returns (IOriginSettler.OnchainCrossChainOrder memory) + { + IOriginSettler.OnchainCrossChainOrder memory onchainCrossChainOrder_ = IOriginSettler.OnchainCrossChainOrder({ + fillDeadline: uint32(mandate_.fills[0].expires), + orderDataType: ORDERDATA_ONCHAIN_TYPEHASH, + orderData: abi.encode( + IERC7683Allocator.OrderDataOnChain({ + order: IERC7683Allocator.Order({ + arbiter: compact_.arbiter, + commitments: compact_.commitments, + mandate: mandate_ + }), + expires: uint32(compact_.expires) + }) + ) + }); + return onchainCrossChainOrder_; + } +} + +abstract contract Deposited is MocksSetup { + function setUp() public virtual override { + super.setUp(); + + vm.startPrank(user); + + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.depositERC20(address(usdc), usdcLockTag, defaultAmount, user); + + vm.stopPrank(); + } +} From de477e7bc69f99bf80cd57a36eeb95a79dc838fd Mon Sep 17 00:00:00 2001 From: mgretzke Date: Fri, 22 Aug 2025 19:22:36 +0200 Subject: [PATCH 61/63] snapshot update --- snapshots/HybridAllocatorTest.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/snapshots/HybridAllocatorTest.json b/snapshots/HybridAllocatorTest.json index 77b955b..c5840e6 100644 --- a/snapshots/HybridAllocatorTest.json +++ b/snapshots/HybridAllocatorTest.json @@ -1,10 +1,10 @@ { - "allocateAndRegister_erc20Token": "187690", - "allocateAndRegister_erc20Token_emptyAmountInput": "188600", - "allocateAndRegister_multipleTokens": "223596", - "allocateAndRegister_nativeToken": "139226", - "allocateAndRegister_nativeToken_emptyAmountInput": "139062", - "allocateAndRegister_second_erc20Token": "114896", - "allocateAndRegister_second_nativeToken": "104862", - "hybrid_execute_single": "174439" + "allocateAndRegister_erc20Token": "187668", + "allocateAndRegister_erc20Token_emptyAmountInput": "188578", + "allocateAndRegister_multipleTokens": "223574", + "allocateAndRegister_nativeToken": "139204", + "allocateAndRegister_nativeToken_emptyAmountInput": "139040", + "allocateAndRegister_second_erc20Token": "114874", + "allocateAndRegister_second_nativeToken": "104840", + "hybrid_execute_single": "174395" } \ No newline at end of file From 72ef9bbed8b493ed24ade68e311ba9f2c918a192 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Mon, 25 Aug 2025 13:14:27 +0200 Subject: [PATCH 62/63] natspec, fixes and cleanup --- snapshots/ERC7683Allocator_openFor.json | 2 +- src/allocators/ERC7683Allocator.sol | 19 ++++----- src/allocators/HybridERC7683.sol | 14 ++++--- src/allocators/lib/AllocatorLib.sol | 11 ++++-- src/allocators/lib/ERC7683AllocatorLib.sol | 45 ++++++++++++++++++++-- test/OnChainAllocator.t.sol | 4 +- test/util/ERC7683TestHelper.sol | 34 +++++++++++++++- 7 files changed, 102 insertions(+), 27 deletions(-) diff --git a/snapshots/ERC7683Allocator_openFor.json b/snapshots/ERC7683Allocator_openFor.json index 8180834..c719551 100644 --- a/snapshots/ERC7683Allocator_openFor.json +++ b/snapshots/ERC7683Allocator_openFor.json @@ -1,3 +1,3 @@ { - "openFor_simpleOrder_userHimself": "171902" + "openFor_simpleOrder_userHimself": "171750" } \ No newline at end of file diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index cf4cb55..f43bc8c 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -29,6 +29,7 @@ import {BatchCompact, Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; /// @notice Allocates tokens deposited into the compact and broadcasts orders following the ERC7683 standard. /// @dev The contract ensures tokens can not be double spent by a user in a fully decentralized manner. /// @dev Users can open orders for themselves or for others by providing a signature or the tokens directly. +/// @custom:security-contact security@uniswap.org contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { constructor(address compact) OnChainAllocator(compact) {} @@ -41,11 +42,9 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { IOriginSettler.ResolvedCrossChainOrder memory resolvedOrder ) = ERC7683AL.openForPreparation(order, sponsorSignature); - uint160 caller = uint160(deposit * uint160(msg.sender)); // for a deposit, the nonce will be scoped to the caller - - // Early revert if the expected nonce is not the next nonce - if (deposit == 0 && order.nonce != _getNonce(address(caller), order.user)) { - revert InvalidNonce(order.nonce, _getNonce(address(caller), order.user)); + // Early revert if the expected nonce is not the next nonce and the order does not include a deposit + if (deposit == 0 && order.nonce != _getNonce(address(0), order.user)) { + revert InvalidNonce(order.nonce, _getNonce(address(0), order.user)); } uint256 nonce; @@ -72,7 +71,7 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { mandateHash ); - // We ignore the order.nonce and use the one assigned by the hybrid allocator + // We ignore order.nonce and use the one assigned by the hybrid allocator resolvedOrder.orderId = bytes32(nonce); // Update the resolved order with the registered amounts @@ -114,11 +113,9 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { (, uint32 deposit,, IOriginSettler.ResolvedCrossChainOrder memory resolvedOrder) = ERC7683AL.openForPreparation(order, LibBytes.emptyCalldata()); - uint160 caller = uint160(deposit * uint160(msg.sender)); // for a deposit, the nonce will be scoped to the caller + user - - // Revert if the expected nonce is not the next nonce - if (order.nonce != _getNonce(address(caller), order.user)) { - revert InvalidNonce(order.nonce, _getNonce(address(caller), order.user)); + // Revert if the expected nonce is not the next nonce and the order does not include a deposit + if (deposit == 0 && order.nonce != _getNonce(address(0), order.user)) { + revert InvalidNonce(order.nonce, _getNonce(address(0), order.user)); } return resolvedOrder; diff --git a/src/allocators/HybridERC7683.sol b/src/allocators/HybridERC7683.sol index 1b97dc3..3e7854e 100644 --- a/src/allocators/HybridERC7683.sol +++ b/src/allocators/HybridERC7683.sol @@ -23,6 +23,9 @@ import {IERC7683Allocator} from 'src/interfaces/IERC7683Allocator.sol'; import {AllocatorLib as AL} from 'src/allocators/lib/AllocatorLib.sol'; import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; +/// @title HybridERC7683 +/// @notice Hybrid allocator that can be used with the ERC7683 standard and the Uniswap Tribunal as the destination settler. +/// @custom:security-contact security@uniswap.org contract HybridERC7683 is HybridAllocator, IERC7683Allocator { error OnlyDepositsAllowed(); @@ -89,6 +92,8 @@ contract HybridERC7683 is HybridAllocator, IERC7683Allocator { ); ResolvedCrossChainOrder memory resolvedOrder = ERC7683AL.resolveOrder(msg.sender, nonce, expires, fillHashes, orderData, LibBytes.emptyCalldata()); + + // Update the resolved order with the registered amounts for (uint256 i = 0; i < orderData.commitments.length; i++) { resolvedOrder.minReceived[i].amount = registeredAmounts[i]; } @@ -106,16 +111,14 @@ contract HybridERC7683 is HybridAllocator, IERC7683Allocator { (, uint32 deposit,, IOriginSettler.ResolvedCrossChainOrder memory resolvedOrder) = ERC7683AL.openForPreparation(order, LibBytes.emptyCalldata()); - // Revert if the nonce is not the next nonce - if (order.nonce != nonces + 1) { - revert InvalidNonce(order.nonce, nonces + 1); - } - if (deposit == 0) { // Hybrid Allocator requires a deposit revert OnlyDepositsAllowed(); } + // We ignore the order.nonce and use the one assigned by the hybrid allocator + resolvedOrder.orderId = bytes32(uint256(nonces) + 1); + return resolvedOrder; } @@ -127,6 +130,7 @@ contract HybridERC7683 is HybridAllocator, IERC7683Allocator { return ERC7683AL.resolveOrder(msg.sender, nonces + 1, expires, fillHashes, orderData, LibBytes.emptyCalldata()); } + /// @inheritdoc IERC7683Allocator function getCompactWitnessTypeString() external pure returns (string memory) { return COMPACT_WITH_MANDATE_TYPESTRING; } diff --git a/src/allocators/lib/AllocatorLib.sol b/src/allocators/lib/AllocatorLib.sol index 02a55c1..ef06512 100644 --- a/src/allocators/lib/AllocatorLib.sol +++ b/src/allocators/lib/AllocatorLib.sol @@ -97,16 +97,19 @@ library AllocatorLib { return (claimHash, commitments); } - function getCommitmentsHash(Lock[] memory commitments) internal pure returns (bytes32) { + function getCommitmentsHash(Lock[] memory commitments, bytes32 typehash) internal pure returns (bytes32) { bytes32[] memory commitmentsHashes = new bytes32[](commitments.length); for (uint256 i = 0; i < commitments.length; i++) { - commitmentsHashes[i] = keccak256( - abi.encode(LOCK_TYPEHASH, commitments[i].lockTag, commitments[i].token, commitments[i].amount) - ); + commitmentsHashes[i] = + keccak256(abi.encode(typehash, commitments[i].lockTag, commitments[i].token, commitments[i].amount)); } return keccak256(abi.encodePacked(commitmentsHashes)); } + function getCommitmentsHash(Lock[] memory commitments) internal pure returns (bytes32) { + return getCommitmentsHash(commitments, LOCK_TYPEHASH); + } + function getClaimHash( address arbiter, address sponsor, diff --git a/src/allocators/lib/ERC7683AllocatorLib.sol b/src/allocators/lib/ERC7683AllocatorLib.sol index 025ac90..af4c04b 100644 --- a/src/allocators/lib/ERC7683AllocatorLib.sol +++ b/src/allocators/lib/ERC7683AllocatorLib.sol @@ -12,6 +12,7 @@ import { COMPACT_WITH_MANDATE_TYPESTRING, MANDATE_BATCH_COMPACT_TYPEHASH, MANDATE_FILL_TYPEHASH, + MANDATE_LOCK_TYPEHASH, MANDATE_RECIPIENT_CALLBACK_TYPEHASH, MANDATE_TYPEHASH } from '@uniswap/tribunal/types/TribunalTypeHashes.sol'; @@ -21,6 +22,9 @@ import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; import {IERC7683Allocator} from 'src/interfaces/IERC7683Allocator.sol'; +/// @title ERC7683AllocatorLib +/// @notice Library for ERC7683 allocator contracts that interact with the Uniswap Tribunal as the destination settler. +/// @custom:security-contact security@uniswap.org library ERC7683AllocatorLib { /// @notice The typehash of the OrderDataOnChain struct // keccak256("OrderDataOnChain(Order order,uint32 expires) @@ -49,6 +53,13 @@ library ERC7683AllocatorLib { error InvalidOriginSettler(address originSettler, address expectedOriginSettler); error InvalidOrderData(bytes orderData); + /// @notice Checks and decodes the order data for a gasless cross-chain order. + /// @param order The gasless cross-chain order used to prepare the data. + /// @param sponsorSignature The sponsor signature of the order. + /// @return orderData The decoded order data. + /// @return deposit The deposit of the order. + /// @return mandateHash The mandate hash of the order. + /// @return resolvedOrder The resolved order with the fill instructions. function openForPreparation(IOriginSettler.GaslessCrossChainOrder calldata order, bytes calldata sponsorSignature) internal view @@ -82,6 +93,12 @@ library ERC7683AllocatorLib { resolveOrder(order.user, order.nonce, order.openDeadline, fillHashes, orderData, sponsorSignature); } + /// @notice Checks and decodes the order data for an on-chain cross-chain order. + /// @param order The on-chain cross-chain order used to prepare the data. + /// @return orderData The decoded order data. + /// @return expires The expiration of the order. + /// @return mandateHash The mandate hash of the order. + /// @return fillHashes The fill hashes of the order. function openPreparation(IOriginSettler.OnchainCrossChainOrder calldata order) internal pure @@ -109,6 +126,10 @@ library ERC7683AllocatorLib { (mandateHash, fillHashes) = hashMandate(orderData.mandate); } + /// @notice Decodes the order data for an on-chain or gasless cross-chain order. + /// @param orderData The order data to decode. + /// @return order The decoded order. + /// @return additionalInput The additional input of the order, either the expiration or the deposit. function decodeOrderData(bytes calldata orderData) internal pure @@ -121,6 +142,7 @@ library ERC7683AllocatorLib { // 0x00: OrderDataGasless.offset // 0x20: OrderDataGasless.order.offset + // 0x40: OrderDataGasless.deposit assembly ("memory-safe") { let l := sub(orderData.length, 0x20) @@ -132,6 +154,14 @@ library ERC7683AllocatorLib { } } + /// @notice Resolves the order into the ERC7683 standard format. + /// @param sponsor The sponsor of the order. + /// @param nonce The nonce of the order. + /// @param expires The expiration of the order. + /// @param fillHashes The fill hashes of the orders Mandate. + /// @param orderData The decoded order data of either the OnChain or Gasless order. + /// @param sponsorSignature The sponsor signature of the order proving their intention to execute the order. + /// @return resolvedOrder The resolved order with the fill instructions. function resolveOrder( address sponsor, uint256 nonce, @@ -165,7 +195,7 @@ library ERC7683AllocatorLib { chainId: block.chainid, compact: compact, sponsorSignature: sponsorSignature, - allocatorSignature: '' // No signature required from this allocator, it will verify the claim on chain via ERC1271. + allocatorSignature: '' // No signature required from this allocator, it will verify the claim via the compacts `authorizeClaim` callback. }); IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); @@ -191,6 +221,9 @@ library ERC7683AllocatorLib { return resolvedOrder; } + /// @notice Creates the minimum received Output array for an order. + /// @param commitments The sponsor's commitments of the order. + /// @return minReceived The minimum received Output array for the order. function createMinimumReceived(Lock[] calldata commitments) internal view @@ -202,7 +235,7 @@ library ERC7683AllocatorLib { IOriginSettler.Output memory received = IOriginSettler.Output({ token: addressToBytes32(commitments[i].token), amount: commitments[i].amount, - recipient: bytes32(0), + recipient: bytes32(0), // Leave empty since these tokens will be received by the filler chainId: block.chainid }); minReceived[i] = received; @@ -210,6 +243,10 @@ library ERC7683AllocatorLib { return minReceived; } + /// @notice Hashes the mandate of the order. + /// @param mandate The mandate of the order. + /// @return mandateHash The hash of the full mandate. + /// @return fillHashes The hashes of the fills within the mandate. function hashMandate(Mandate calldata mandate) internal pure returns (bytes32, bytes32[] memory fillHashes) { fillHashes = new bytes32[](mandate.fills.length); for (uint256 i = 0; i < mandate.fills.length; i++) { @@ -221,6 +258,7 @@ library ERC7683AllocatorLib { ); } + /// @notice Hashes a fill of the mandate. function hashFill(Fill calldata fill) internal pure returns (bytes32) { bytes32 priceCurveHash = keccak256(abi.encodePacked(fill.priceCurve)); return keccak256( @@ -241,6 +279,7 @@ library ERC7683AllocatorLib { ); } + /// @notice Hashes a recipient callback of the fill. function hashRecipientCallback(RecipientCallback[] calldata recipientCallback) internal pure returns (bytes32) { if (recipientCallback.length == 0) { // empty hash @@ -262,7 +301,7 @@ library ERC7683AllocatorLib { callback.compact.sponsor, callback.compact.nonce, callback.compact.expires, - AL.getCommitmentsHash(callback.compact.commitments), + AL.getCommitmentsHash(callback.compact.commitments, MANDATE_LOCK_TYPEHASH), callback.mandateHash, MANDATE_BATCH_COMPACT_TYPEHASH ), diff --git a/test/OnChainAllocator.t.sol b/test/OnChainAllocator.t.sol index 1595296..0314499 100644 --- a/test/OnChainAllocator.t.sol +++ b/test/OnChainAllocator.t.sol @@ -908,8 +908,10 @@ 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); + // run the whole flow in a single tx through the helper - vm.prank(user); allocationCaller.onChainAllocation( recipient, idsAndAmounts, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, bytes32(0), 0 ); diff --git a/test/util/ERC7683TestHelper.sol b/test/util/ERC7683TestHelper.sol index fc27880..cefdf9b 100644 --- a/test/util/ERC7683TestHelper.sol +++ b/test/util/ERC7683TestHelper.sol @@ -26,6 +26,7 @@ import { COMPACT_TYPEHASH_WITH_MANDATE, MANDATE_BATCH_COMPACT_TYPEHASH, MANDATE_FILL_TYPEHASH, + MANDATE_LOCK_TYPEHASH, MANDATE_RECIPIENT_CALLBACK_TYPEHASH, MANDATE_TYPEHASH } from '@uniswap/tribunal/types/TribunalTypeHashes.sol'; @@ -125,8 +126,37 @@ abstract contract CreateHash is MocksSetup { } function _hashRecipientCallback(RecipientCallback[] memory rc) internal pure returns (bytes32) { - if (rc.length == 0) return EMPTY_HASH; - revert('RecipientCallback not supported in tests'); + if (rc.length == 0) { + return EMPTY_HASH; + } else if (rc.length != 1) { + revert('RecipientCallback not supported in tests'); + } else { + RecipientCallback memory rc_ = rc[0]; + bytes32[] memory commitmentsHashes = new bytes32[](rc_.compact.commitments.length); + for (uint256 i = 0; i < rc_.compact.commitments.length; i++) { + commitmentsHashes[i] = keccak256( + abi.encode( + MANDATE_LOCK_TYPEHASH, + rc_.compact.commitments[i].lockTag, + rc_.compact.commitments[i].token, + rc_.compact.commitments[i].amount + ) + ); + } + bytes32 commitmentsHash = keccak256(abi.encodePacked(commitmentsHashes)); + + return keccak256( + abi.encode( + MANDATE_RECIPIENT_CALLBACK_TYPEHASH, + rc_.compact.arbiter, + rc_.compact.sponsor, + rc_.compact.nonce, + rc_.compact.expires, + commitmentsHash, + rc_.mandateHash + ) + ); + } } function _hashFill(Fill memory f) internal pure returns (bytes32) { From e38e273415007cdb6939277597daf5f1a629d200 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Mon, 25 Aug 2025 13:21:21 +0200 Subject: [PATCH 63/63] nonce fix --- src/allocators/ERC7683Allocator.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index f43bc8c..2484833 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -71,7 +71,7 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { mandateHash ); - // We ignore order.nonce and use the one assigned by the hybrid allocator + // We ignore order.nonce and use the one assigned by the allocator resolvedOrder.orderId = bytes32(nonce); // Update the resolved order with the registered amounts @@ -116,6 +116,9 @@ contract ERC7683Allocator is OnChainAllocator, IERC7683Allocator { // Revert if the expected nonce is not the next nonce and the order does not include a deposit if (deposit == 0 && order.nonce != _getNonce(address(0), order.user)) { revert InvalidNonce(order.nonce, _getNonce(address(0), order.user)); + } else if (deposit == 1) { + // We ignore the order.nonce and use the one assigned by the allocator + resolvedOrder.orderId = bytes32(_getNonce(msg.sender, order.user)); } return resolvedOrder;