From 2dd08588b1c3e73000e8bd47d927db550c91b296 Mon Sep 17 00:00:00 2001 From: mgretzke Date: Tue, 26 Aug 2025 22:39:55 +0200 Subject: [PATCH 1/2] check and handle a chain fork --- src/allocators/HybridAllocator.sol | 10 ++++++++++ src/allocators/OnChainAllocator.sol | 7 +++++++ 2 files changed, 17 insertions(+) diff --git a/src/allocators/HybridAllocator.sol b/src/allocators/HybridAllocator.sol index e59737c..1a4082a 100644 --- a/src/allocators/HybridAllocator.sol +++ b/src/allocators/HybridAllocator.sol @@ -13,6 +13,7 @@ import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; import {IHybridAllocator} from 'src/interfaces/IHybridAllocator.sol'; contract HybridAllocator is IHybridAllocator { + uint256 private immutable _INITIAL_CHAIN_ID; uint96 public immutable ALLOCATOR_ID; ITheCompact internal immutable _COMPACT; bytes32 internal immutable _COMPACT_DOMAIN_SEPARATOR; @@ -35,6 +36,7 @@ contract HybridAllocator is IHybridAllocator { if (signer_ == address(0)) { revert InvalidSigner(); } + _INITIAL_CHAIN_ID = block.chainid; _COMPACT = ITheCompact(compact_); ALLOCATOR_ID = _COMPACT.__registerAllocator(address(this), ''); _COMPACT_DOMAIN_SEPARATOR = _COMPACT.DOMAIN_SEPARATOR(); @@ -170,6 +172,10 @@ contract HybridAllocator is IHybridAllocator { // Check the allocator data for a valid signature by an authorized signer bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), _COMPACT_DOMAIN_SEPARATOR, claimHash)); + if (block.chainid != _INITIAL_CHAIN_ID) { + // If the chain was forked, we can not use the cached domain separator + digest = keccak256(abi.encodePacked(bytes2(0x1901), _COMPACT.DOMAIN_SEPARATOR(), claimHash)); + } if (!_checkSignature(digest, allocatorData_)) { revert InvalidSignature(); } @@ -194,6 +200,10 @@ contract HybridAllocator is IHybridAllocator { // Check the allocator data for a valid signature by an authorized allocator address bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), _COMPACT_DOMAIN_SEPARATOR, claimHash)); + if (block.chainid != _INITIAL_CHAIN_ID) { + // If the chain was forked, we can not use the cached domain separator + digest = keccak256(abi.encodePacked(bytes2(0x1901), _COMPACT.DOMAIN_SEPARATOR(), claimHash)); + } return _checkSignature(digest, allocatorData); } diff --git a/src/allocators/OnChainAllocator.sol b/src/allocators/OnChainAllocator.sol index ac0fe19..61a799a 100644 --- a/src/allocators/OnChainAllocator.sol +++ b/src/allocators/OnChainAllocator.sol @@ -17,6 +17,7 @@ import {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 OnChainAllocator is IOnChainAllocator { + uint256 private immutable _INITIAL_CHAIN_ID; address public immutable COMPACT_CONTRACT; bytes32 public immutable COMPACT_DOMAIN_SEPARATOR; uint96 public immutable ALLOCATOR_ID; @@ -33,6 +34,7 @@ contract OnChainAllocator is IOnChainAllocator { } constructor(address compactContract_) { + _INITIAL_CHAIN_ID = block.chainid; COMPACT_CONTRACT = compactContract_; COMPACT_DOMAIN_SEPARATOR = ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(); ALLOCATOR_ID = ITheCompact(COMPACT_CONTRACT).__registerAllocator(address(this), ''); @@ -65,6 +67,11 @@ 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)); + if (block.chainid != _INITIAL_CHAIN_ID) { + digest = keccak256( + abi.encodePacked(bytes2(0x1901), ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(), claimHash) + ); + } address signer_ = AL.recoverSigner(digest, signature); if (sponsor != signer_ || signer_ == address(0)) { revert InvalidSignature(signer_, sponsor); From dfbb93cf7da536b4ffa98787ee6431e61e0d34cc Mon Sep 17 00:00:00 2001 From: mgretzke Date: Wed, 27 Aug 2025 16:22:03 +0200 Subject: [PATCH 2/2] added tests --- snapshots/ERC7683Allocator_open.json | 4 +- snapshots/ERC7683Allocator_openFor.json | 4 +- snapshots/HybridAllocatorTest.json | 18 +++--- snapshots/OnChainAllocatorTest.json | 16 ++--- test/HybridAllocator.t.sol | 82 +++++++++++++++++++++++++ test/OnChainAllocator.t.sol | 44 +++++++++++++ 6 files changed, 147 insertions(+), 21 deletions(-) diff --git a/snapshots/ERC7683Allocator_open.json b/snapshots/ERC7683Allocator_open.json index 9c7f3d9..341ce5b 100644 --- a/snapshots/ERC7683Allocator_open.json +++ b/snapshots/ERC7683Allocator_open.json @@ -1,3 +1,3 @@ { - "open_simpleOrder": "168301" -} \ No newline at end of file + "open_simpleOrder": "168301" +} diff --git a/snapshots/ERC7683Allocator_openFor.json b/snapshots/ERC7683Allocator_openFor.json index c719551..b84ca2e 100644 --- a/snapshots/ERC7683Allocator_openFor.json +++ b/snapshots/ERC7683Allocator_openFor.json @@ -1,3 +1,3 @@ { - "openFor_simpleOrder_userHimself": "171750" -} \ No newline at end of file + "openFor_simpleOrder_userHimself": "171753" +} diff --git a/snapshots/HybridAllocatorTest.json b/snapshots/HybridAllocatorTest.json index c5840e6..e41b300 100644 --- a/snapshots/HybridAllocatorTest.json +++ b/snapshots/HybridAllocatorTest.json @@ -1,10 +1,10 @@ { - "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 + "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" +} diff --git a/snapshots/OnChainAllocatorTest.json b/snapshots/OnChainAllocatorTest.json index ec5af8f..8ae1ba0 100644 --- a/snapshots/OnChainAllocatorTest.json +++ b/snapshots/OnChainAllocatorTest.json @@ -1,9 +1,9 @@ { - "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 + "allocateFor_success_withRegistration": "133756", + "allocate_and_delete_expired_allocation": "65921", + "allocate_erc20": "129192", + "allocate_native": "128952", + "allocate_second_erc20": "97204", + "onchain_execute_double": "347707", + "onchain_execute_single": "219699" +} diff --git a/test/HybridAllocator.t.sol b/test/HybridAllocator.t.sol index 52b35c4..018a26d 100644 --- a/test/HybridAllocator.t.sol +++ b/test/HybridAllocator.t.sol @@ -753,6 +753,88 @@ contract HybridAllocatorTest is Test, TestHelper { compact.batchClaim(claim); } + function test_authorizeClaim_revert_oldSignatureAfterFork(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 + }); + + uint256 snap = vm.snapshot(); + assertEq(block.chainid, 31_337); + + vm.prank(arbiter); + compact.batchClaim(claim); + // Call did not revert + + vm.revertTo(snap); + vm.chainId(1); + assertEq(block.chainid, 1); + + vm.prank(arbiter); + vm.expectRevert(abi.encodeWithSelector(IHybridAllocator.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)); diff --git a/test/OnChainAllocator.t.sol b/test/OnChainAllocator.t.sol index 0314499..8781b71 100644 --- a/test/OnChainAllocator.t.sol +++ b/test/OnChainAllocator.t.sol @@ -464,6 +464,50 @@ contract OnChainAllocatorTest is Test, TestHelper { allocator.allocateFor(user, commitments, arbiter, defaultExpiration, BATCH_COMPACT_TYPEHASH, 0x0, sig); } + function test_allocateFor_revert_oldSignatureAfterFork(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 + uint256 expectedNonce = _composeNonceUint(user, allocator.nonces(user) + 1); + bytes32 commitmentsHash = _commitmentsHash(commitments); + bytes32 claimHash = keccak256( + 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); + bytes memory sig = abi.encodePacked(r, s, v); + + uint256 snap = vm.snapshot(); + assertEq(block.chainid, 31_337); + + 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, expectedNonce); + assertTrue(allocator.isClaimAuthorized(claimHash, arbiter, user, nonce, defaultExpiration, idsAndAmounts, '')); + + vm.revertTo(snap); + vm.chainId(1); + assertEq(block.chainid, 1); + + vm.prank(relayer); + vm.expectRevert( + abi.encodeWithSelector( + IOnChainAllocator.InvalidSignature.selector, address(0x71efFb57bf7C717a0a5012186792C45A4851ef5d), 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);