diff --git a/src/allocators/HybridAllocator.sol b/src/allocators/HybridAllocator.sol index 1fbdd5a..4cb8ec0 100644 --- a/src/allocators/HybridAllocator.sol +++ b/src/allocators/HybridAllocator.sol @@ -26,6 +26,7 @@ contract HybridAllocator is IHybridAllocator { /// @notice The unique identifier for this allocator within The Compact protocol uint96 public immutable ALLOCATOR_ID; + uint256 private immutable _INITIAL_CHAIN_ID; ITheCompact internal immutable _COMPACT; bytes32 internal immutable _COMPACT_DOMAIN_SEPARATOR; @@ -50,6 +51,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(); @@ -213,6 +215,10 @@ contract HybridAllocator is IHybridAllocator { // Check the allocator data for a valid signature by an authorized signer bytes32 digest = _deriveDigest(claimHash, _COMPACT_DOMAIN_SEPARATOR); + if (block.chainid != _INITIAL_CHAIN_ID) { + // If the chain was forked, we can not use the cached domain separator + digest = _deriveDigest(claimHash, _COMPACT.DOMAIN_SEPARATOR()); + } if (!_checkSignature(digest, allocatorData_)) { revert InvalidSignature(); } @@ -237,6 +243,10 @@ contract HybridAllocator is IHybridAllocator { // Check the allocator data for a valid signature by an authorized allocator address bytes32 digest = _deriveDigest(claimHash, _COMPACT_DOMAIN_SEPARATOR); + if (block.chainid != _INITIAL_CHAIN_ID) { + // If the chain was forked, we can not use the cached domain separator + digest = _deriveDigest(claimHash, _COMPACT.DOMAIN_SEPARATOR()); + } return _checkSignature(digest, allocatorData); } diff --git a/src/allocators/OnChainAllocator.sol b/src/allocators/OnChainAllocator.sol index 5b9e9a3..abed41e 100644 --- a/src/allocators/OnChainAllocator.sol +++ b/src/allocators/OnChainAllocator.sol @@ -19,6 +19,8 @@ import {Lock} from '@uniswap/the-compact/types/EIP712Types.sol'; /// @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 OnChainAllocator is IOnChainAllocator { + /// @notice The chain id at the time of deployment + uint256 private immutable _INITIAL_CHAIN_ID; /// @notice The address of The Compact protocol contract for token management and claim registration address public immutable COMPACT_CONTRACT; /// @notice The EIP-712 domain separator for The Compact protocol, used for signature verification @@ -40,6 +42,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), ''); @@ -72,6 +75,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); diff --git a/test/HybridAllocator.t.sol b/test/HybridAllocator.t.sol index 5687280..07f0477 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 1dc89a5..a41f4d5 100644 --- a/test/OnChainAllocator.t.sol +++ b/test/OnChainAllocator.t.sol @@ -470,6 +470,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);