From 61f5b1ec85e2b0270e88ddb86772c3e4457ceca0 Mon Sep 17 00:00:00 2001 From: 0age <37939117+0age@users.noreply.github.com> Date: Mon, 14 Oct 2024 18:23:09 -0700 Subject: [PATCH] witness + qualified batch --- src/TheCompact.sol | 91 ++++++++++++++ src/lib/HashLib.sol | 126 ++++++++------------ src/types/EIP712Types.sol | 13 +- test/TheCompact.t.sol | 245 +++++++++++++++++++++++++++++++++++++- 4 files changed, 390 insertions(+), 85 deletions(-) diff --git a/src/TheCompact.sol b/src/TheCompact.sol index 1ddd9ed..b680cfa 100644 --- a/src/TheCompact.sol +++ b/src/TheCompact.sol @@ -478,6 +478,28 @@ contract TheCompact is ITheCompact, ERC6909, Extsload { return _processQualifiedBatchClaim(claimPayload, _release); } + function claim(BatchClaimWithWitness calldata claimPayload) external returns (bool) { + return _processBatchClaimWithWitness(claimPayload, _release); + } + + function claimAndWithdraw(BatchClaimWithWitness calldata claimPayload) + external + returns (bool) + { + return _processBatchClaimWithWitness(claimPayload, _release); + } + + function claim(QualifiedBatchClaimWithWitness calldata claimPayload) external returns (bool) { + return _processQualifiedBatchClaimWithWitness(claimPayload, _release); + } + + function claimAndWithdraw(QualifiedBatchClaimWithWitness calldata claimPayload) + external + returns (bool) + { + return _processQualifiedBatchClaimWithWitness(claimPayload, _release); + } + function enableForcedWithdrawal(uint256 id) external returns (uint256 withdrawableAt) { withdrawableAt = block.timestamp + id.toResetPeriod().toSeconds(); @@ -656,6 +678,23 @@ contract TheCompact is ITheCompact, ERC6909, Extsload { ); } + function _notExpiredAndWithValidSignaturesBatchWithWitness( + BatchClaimWithWitness calldata claimPayload + ) internal returns (bytes32 messageHash, uint96 allocatorId) { + claimPayload.expires.later(); + + allocatorId = claimPayload.claims[0].id.toAllocatorId(); + + messageHash = claimPayload.toMessageHash(); + bytes32 domainSeparator = _INITIAL_DOMAIN_SEPARATOR.toLatest(_INITIAL_CHAIN_ID); + messageHash.signedBy(claimPayload.sponsor, claimPayload.sponsorSignature, domainSeparator); + messageHash.signedBy( + allocatorId.fromRegisteredAllocatorIdWithConsumed(claimPayload.nonce), + claimPayload.allocatorSignature, + domainSeparator + ); + } + // NOTE: this function expects that there's at least one array element function _notExpiredAndWithValidSignaturesQualifiedBatch( QualifiedBatchClaim calldata claimPayload @@ -675,6 +714,24 @@ contract TheCompact is ITheCompact, ERC6909, Extsload { ); } + function _notExpiredAndWithValidSignaturesQualifiedBatchWithWitness( + QualifiedBatchClaimWithWitness calldata claimPayload + ) internal returns (bytes32 messageHash, uint96 allocatorId) { + bytes32 qualificationMessageHash; + claimPayload.expires.later(); + + allocatorId = claimPayload.claims[0].id.toAllocatorId(); + + (messageHash, qualificationMessageHash) = claimPayload.toMessageHash(); + bytes32 domainSeparator = _INITIAL_DOMAIN_SEPARATOR.toLatest(_INITIAL_CHAIN_ID); + messageHash.signedBy(claimPayload.sponsor, claimPayload.sponsorSignature, domainSeparator); + qualificationMessageHash.signedBy( + allocatorId.fromRegisteredAllocatorIdWithConsumed(claimPayload.nonce), + claimPayload.allocatorSignature, + domainSeparator + ); + } + function _notExpiredAndWithValidSignaturesQualified(QualifiedClaim calldata claimPayload) internal returns (bytes32 messageHash) @@ -1121,6 +1178,40 @@ contract TheCompact is ITheCompact, ERC6909, Extsload { ); } + function _processBatchClaimWithWitness( + BatchClaimWithWitness calldata batchClaim, + function(address, address, uint256, uint256) internal returns (bool) operation + ) internal returns (bool) { + (bytes32 messageHash, uint96 allocatorId) = + _notExpiredAndWithValidSignaturesBatchWithWitness(batchClaim); + + return _verifyAndProcessBatchComponents( + allocatorId, + batchClaim.sponsor, + batchClaim.claimant, + messageHash, + batchClaim.claims, + operation + ); + } + + function _processQualifiedBatchClaimWithWitness( + QualifiedBatchClaimWithWitness calldata batchClaim, + function(address, address, uint256, uint256) internal returns (bool) operation + ) internal returns (bool) { + (bytes32 messageHash, uint96 allocatorId) = + _notExpiredAndWithValidSignaturesQualifiedBatchWithWitness(batchClaim); + + return _verifyAndProcessBatchComponents( + allocatorId, + batchClaim.sponsor, + batchClaim.claimant, + messageHash, + batchClaim.claims, + operation + ); + } + function _processBatchPermit2Deposits( bool firstUnderlyingTokenIsNative, address recipient, diff --git a/src/lib/HashLib.sol b/src/lib/HashLib.sol index b969667..8419ecc 100644 --- a/src/lib/HashLib.sol +++ b/src/lib/HashLib.sol @@ -383,6 +383,32 @@ library HashLib { } } + function _toBatchMessageHashWithWitness( + BatchClaimWithWitness calldata claim, + BatchClaimComponent[] calldata claims + ) internal view returns (bytes32 messageHash) { + bytes32 idsAndAmountsHash = toIdsAndAmountsHash(claims); + + assembly ("memory-safe") { + let m := mload(0x40) // Grab the free memory pointer; memory will be left dirtied. + + // prepare full typestring + let witnessTypestringPtr := add(claim, calldataload(add(claim, 0xc0))) + let witnessTypestringLength := calldataload(witnessTypestringPtr) + mstore(m, BATCH_COMPACT_TYPESTRING_FRAGMENT_ONE) + mstore(add(m, 0x20), BATCH_COMPACT_TYPESTRING_FRAGMENT_TWO) + mstore(add(m, 0x46), BATCH_COMPACT_TYPESTRING_FRAGMENT_FOUR) + mstore(add(m, 0x40), BATCH_COMPACT_TYPESTRING_FRAGMENT_THREE) + calldatacopy(add(m, 0x66), add(0x20, witnessTypestringPtr), witnessTypestringLength) + mstore(m, keccak256(m, add(0x66, witnessTypestringLength))) // typehash + mstore(add(m, 0x20), caller()) // arbiter: msg.sender + calldatacopy(add(m, 0x40), add(claim, 0x40), 0x60) // sponsor, nonce, expires + mstore(add(m, 0xa0), idsAndAmountsHash) + mstore(add(m, 0xc0), calldataload(add(claim, 0xa0))) // witness + messageHash := keccak256(m, 0xe0) + } + } + function _usingQualifiedBatchClaim( function(BatchClaim calldata, BatchClaimComponent[] calldata) internal view returns (bytes32) fnIn @@ -399,6 +425,22 @@ library HashLib { } } + function _usingQualifiedBatchClaimWithWitness( + function(BatchClaimWithWitness calldata, BatchClaimComponent[] calldata) internal view returns (bytes32) + fnIn + ) + internal + pure + returns ( + function(QualifiedBatchClaimWithWitness calldata, BatchClaimComponent[] calldata) internal view returns (bytes32) + fnOut + ) + { + assembly { + fnOut := fnIn + } + } + function toMessageHash(QualifiedBatchClaim calldata claim) internal view @@ -412,96 +454,22 @@ library HashLib { ); } - function toMessageHash(BatchClaimWithWitness memory claim) + function toMessageHash(BatchClaimWithWitness calldata claim) internal view returns (bytes32 messageHash) { - // derive the typehash (TODO: make this more efficient especially once using calldata) - bytes32 typehash = keccak256( - abi.encodePacked( - BATCH_COMPACT_TYPESTRING_FRAGMENT_ONE, - BATCH_COMPACT_TYPESTRING_FRAGMENT_TWO, - BATCH_COMPACT_TYPESTRING_FRAGMENT_THREE, - BATCH_COMPACT_TYPESTRING_FRAGMENT_FOUR, - claim.witnessTypestring - ) - ); - bytes32 witness = claim.witness; - - // TODO: make this more efficient especially once using calldata - uint256[2][] memory idsAndAmounts = new uint256[2][](claim.claims.length); - for (uint256 i = 0; i < claim.claims.length; ++i) { - idsAndAmounts[i] = [claim.claims[i].id, claim.claims[i].allocatedAmount]; - } - bytes32 idsAndAmountsHash = keccak256(abi.encodePacked(idsAndAmounts)); - - assembly ("memory-safe") { - let m := mload(0x40) // Grab the free memory pointer; memory will be left dirtied. - - // TODO: calldatacopy this whole chunk at once as part of calldata implementation - let sponsor := mload(claim) - let expires := mload(add(claim, 0x20)) - let nonce := mload(add(claim, 0x40)) - - let id := mload(add(claim, 0x60)) - let amount := mload(add(claim, 0x80)) - - mstore(m, typehash) - mstore(add(m, 0x20), sponsor) - mstore(add(m, 0x40), expires) - mstore(add(m, 0x60), nonce) - mstore(add(m, 0x80), caller()) // arbiter: msg.sender - mstore(add(m, 0xa0), idsAndAmountsHash) - mstore(add(m, 0xc0), witness) - messageHash := keccak256(m, 0xe0) - } + return _toBatchMessageHashWithWitness(claim, claim.claims); } - function toMessageHash(QualifiedBatchClaimWithWitness memory claim) + function toMessageHash(QualifiedBatchClaimWithWitness calldata claim) internal view returns (bytes32 messageHash, bytes32 qualificationMessageHash) { - // derive the typehash (TODO: make this more efficient especially once using calldata) - bytes32 typehash = keccak256( - abi.encodePacked( - BATCH_COMPACT_TYPESTRING_FRAGMENT_ONE, - BATCH_COMPACT_TYPESTRING_FRAGMENT_TWO, - BATCH_COMPACT_TYPESTRING_FRAGMENT_THREE, - BATCH_COMPACT_TYPESTRING_FRAGMENT_FOUR, - claim.witnessTypestring - ) + messageHash = _usingQualifiedBatchClaimWithWitness(_toBatchMessageHashWithWitness)( + claim, claim.claims ); - bytes32 witness = claim.witness; - - // TODO: make this more efficient especially once using calldata - uint256[2][] memory idsAndAmounts = new uint256[2][](claim.claims.length); - for (uint256 i = 0; i < claim.claims.length; ++i) { - idsAndAmounts[i] = [claim.claims[i].id, claim.claims[i].allocatedAmount]; - } - bytes32 idsAndAmountsHash = keccak256(abi.encodePacked(idsAndAmounts)); - - assembly ("memory-safe") { - let m := mload(0x40) // Grab the free memory pointer; memory will be left dirtied. - - // TODO: calldatacopy this whole chunk at once as part of calldata implementation - let sponsor := mload(claim) - let expires := mload(add(claim, 0x20)) - let nonce := mload(add(claim, 0x40)) - - let id := mload(add(claim, 0x60)) - let amount := mload(add(claim, 0x80)) - - mstore(m, typehash) - mstore(add(m, 0x20), sponsor) - mstore(add(m, 0x40), expires) - mstore(add(m, 0x60), nonce) - mstore(add(m, 0x80), caller()) // arbiter: msg.sender - mstore(add(m, 0xa0), idsAndAmountsHash) - mstore(add(m, 0xc0), witness) - messageHash := keccak256(m, 0xe0) - } // TODO: optimize once we're using calldata qualificationMessageHash = keccak256( diff --git a/src/types/EIP712Types.sol b/src/types/EIP712Types.sol index f2ef3b7..8504d47 100644 --- a/src/types/EIP712Types.sol +++ b/src/types/EIP712Types.sol @@ -50,18 +50,21 @@ bytes32 constant BATCH_COMPACT_TYPEHASH = // abi.decode(bytes("BatchCompact(address arbiter,add"), (bytes32)) bytes32 constant BATCH_COMPACT_TYPESTRING_FRAGMENT_ONE = - abi.decode(bytes("BatchCompact(address arbiter,add"), (bytes32)); + 0x4261746368436f6d70616374286164647265737320617262697465722c616464; +//abi.decode(bytes("BatchCompact(address arbiter,add"), (bytes32)); // abi.decode(bytes("ress sponsor,uint256 nonce,uint2"), (bytes32)) bytes32 constant BATCH_COMPACT_TYPESTRING_FRAGMENT_TWO = - abi.decode(bytes("ress sponsor,uint256 nonce,uint2"), (bytes32)); + 0x726573732073706f6e736f722c75696e74323536206e6f6e63652c75696e7432; +//abi.decode(bytes("ress sponsor,uint256 nonce,uint2"), (bytes32)); // abi.decode(bytes("56 expires,uint256[2][] idsAndAm"), (bytes32)) bytes32 constant BATCH_COMPACT_TYPESTRING_FRAGMENT_THREE = - abi.decode(bytes("56 expires,uint256[2][] idsAndAm"), (bytes32)); + 0x353620657870697265732c75696e743235365b325d5b5d20696473416e64416d; +//abi.decode(bytes("56 expires,uint256[2][] idsAndAm"), (bytes32)); -// abi.decode(bytes("ounts,"), (bytes6)) -bytes6 constant BATCH_COMPACT_TYPESTRING_FRAGMENT_FOUR = 0x6f756e74732c; +// uint48(abi.decode(bytes("ounts,"), (bytes6))) +uint48 constant BATCH_COMPACT_TYPESTRING_FRAGMENT_FOUR = 0x6f756e74732c; // A multichain compact can declare tokens and amounts to allocate from multiple chains, // each designated by their chainId. Any allocated tokens must designate the Multichain diff --git a/test/TheCompact.t.sol b/test/TheCompact.t.sol index 58e02ff..669778f 100644 --- a/test/TheCompact.t.sol +++ b/test/TheCompact.t.sol @@ -25,7 +25,9 @@ import { BatchTransfer, SplitBatchTransfer, BatchClaim, - QualifiedBatchClaim + QualifiedBatchClaim, + BatchClaimWithWitness, + QualifiedBatchClaimWithWitness } from "../src/types/BatchClaims.sol"; import { @@ -1979,4 +1981,245 @@ contract TheCompactTest is Test { assertEq(theCompact.balanceOf(claimant, anotherId), anotherAmount); assertEq(theCompact.balanceOf(claimant, aThirdId), aThirdAmount); } + + function test_batchClaimWithWitness() public { + uint256 amount = 1e18; + uint256 anotherAmount = 1e18; + uint256 aThirdAmount = 1e18; + uint256 nonce = 0; + uint256 expires = block.timestamp + 1000; + address claimant = 0x1111111111111111111111111111111111111111; + address arbiter = 0x2222222222222222222222222222222222222222; + + vm.prank(allocator); + theCompact.__register(allocator, ""); + + vm.startPrank(swapper); + uint256 id = theCompact.deposit{ value: amount }( + allocator, ResetPeriod.TenMinutes, Scope.Multichain, swapper + ); + + uint256 anotherId = theCompact.deposit( + address(token), + allocator, + ResetPeriod.TenMinutes, + Scope.Multichain, + anotherAmount, + swapper + ); + assertEq(theCompact.balanceOf(swapper, anotherId), anotherAmount); + + uint256 aThirdId = theCompact.deposit( + address(anotherToken), + allocator, + ResetPeriod.TenMinutes, + Scope.Multichain, + aThirdAmount, + swapper + ); + assertEq(theCompact.balanceOf(swapper, aThirdId), aThirdAmount); + + vm.stopPrank(); + + assertEq(theCompact.balanceOf(swapper, id), amount); + assertEq(theCompact.balanceOf(swapper, anotherId), anotherAmount); + assertEq(theCompact.balanceOf(swapper, aThirdId), aThirdAmount); + + uint256[2][] memory idsAndAmounts = new uint256[2][](3); + idsAndAmounts[0] = [id, amount]; + idsAndAmounts[1] = [anotherId, anotherAmount]; + idsAndAmounts[2] = [aThirdId, aThirdAmount]; + + string memory witnessTypestring = "Witness witness)Witness(uint256 witnessArgument)"; + uint256 witnessArgument = 234; + bytes32 witness = keccak256(abi.encode(witnessArgument)); + + bytes32 claimHash = keccak256( + abi.encode( + keccak256( + "BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256[2][] idsAndAmounts,Witness witness)Witness(uint256 witnessArgument)" + ), + arbiter, + swapper, + nonce, + expires, + keccak256(abi.encodePacked(idsAndAmounts)), + witness + ) + ); + + bytes32 digest = + keccak256(abi.encodePacked(bytes2(0x1901), theCompact.DOMAIN_SEPARATOR(), claimHash)); + + (bytes32 r, bytes32 vs) = vm.signCompact(swapperPrivateKey, digest); + bytes memory sponsorSignature = abi.encodePacked(r, vs); + + (r, vs) = vm.signCompact(allocatorPrivateKey, digest); + bytes memory allocatorSignature = abi.encodePacked(r, vs); + + BatchClaimComponent[] memory claims = new BatchClaimComponent[](3); + claims[0] = BatchClaimComponent({ id: id, allocatedAmount: amount, amount: amount }); + claims[1] = BatchClaimComponent({ + id: anotherId, + allocatedAmount: anotherAmount, + amount: anotherAmount + }); + claims[2] = BatchClaimComponent({ + id: aThirdId, + allocatedAmount: aThirdAmount, + amount: aThirdAmount + }); + + BatchClaimWithWitness memory claim = BatchClaimWithWitness( + allocatorSignature, + sponsorSignature, + swapper, + nonce, + expires, + witness, + witnessTypestring, + claims, + claimant + ); + + vm.prank(arbiter); + (bool status) = theCompact.claim(claim); + assert(status); + + assertEq(address(theCompact).balance, amount); + assertEq(token.balanceOf(address(theCompact)), anotherAmount); + assertEq(anotherToken.balanceOf(address(theCompact)), aThirdAmount); + + assertEq(theCompact.balanceOf(claimant, id), amount); + assertEq(theCompact.balanceOf(claimant, anotherId), anotherAmount); + assertEq(theCompact.balanceOf(claimant, aThirdId), aThirdAmount); + } + + function test_qualifiedBatchClaimWithWitness() public { + uint256 amount = 1e18; + uint256 anotherAmount = 1e18; + uint256 aThirdAmount = 1e18; + uint256 nonce = 0; + uint256 expires = block.timestamp + 1000; + address claimant = 0x1111111111111111111111111111111111111111; + address arbiter = 0x2222222222222222222222222222222222222222; + + vm.prank(allocator); + theCompact.__register(allocator, ""); + + vm.startPrank(swapper); + uint256 id = theCompact.deposit{ value: amount }( + allocator, ResetPeriod.TenMinutes, Scope.Multichain, swapper + ); + + uint256 anotherId = theCompact.deposit( + address(token), + allocator, + ResetPeriod.TenMinutes, + Scope.Multichain, + anotherAmount, + swapper + ); + assertEq(theCompact.balanceOf(swapper, anotherId), anotherAmount); + + uint256 aThirdId = theCompact.deposit( + address(anotherToken), + allocator, + ResetPeriod.TenMinutes, + Scope.Multichain, + aThirdAmount, + swapper + ); + assertEq(theCompact.balanceOf(swapper, aThirdId), aThirdAmount); + + vm.stopPrank(); + + assertEq(theCompact.balanceOf(swapper, id), amount); + assertEq(theCompact.balanceOf(swapper, anotherId), anotherAmount); + assertEq(theCompact.balanceOf(swapper, aThirdId), aThirdAmount); + + uint256[2][] memory idsAndAmounts = new uint256[2][](3); + idsAndAmounts[0] = [id, amount]; + idsAndAmounts[1] = [anotherId, anotherAmount]; + idsAndAmounts[2] = [aThirdId, aThirdAmount]; + + string memory witnessTypestring = "Witness witness)Witness(uint256 witnessArgument)"; + uint256 witnessArgument = 234; + bytes32 witness = keccak256(abi.encode(witnessArgument)); + + bytes32 claimHash = keccak256( + abi.encode( + keccak256( + "BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256[2][] idsAndAmounts,Witness witness)Witness(uint256 witnessArgument)" + ), + arbiter, + swapper, + nonce, + expires, + keccak256(abi.encodePacked(idsAndAmounts)), + witness + ) + ); + + bytes32 digest = + keccak256(abi.encodePacked(bytes2(0x1901), theCompact.DOMAIN_SEPARATOR(), claimHash)); + + (bytes32 r, bytes32 vs) = vm.signCompact(swapperPrivateKey, digest); + bytes memory sponsorSignature = abi.encodePacked(r, vs); + + bytes32 qualificationTypehash = + keccak256("ExampleQualifiedClaim(bytes32 claimHash,uint256 qualifiedClaimArgument)"); + + uint256 qualifiedClaimArgument = 123; + bytes memory qualificationPayload = abi.encode(qualifiedClaimArgument); + + bytes32 qualifiedClaimHash = + keccak256(abi.encode(qualificationTypehash, claimHash, qualifiedClaimArgument)); + + digest = keccak256( + abi.encodePacked(bytes2(0x1901), theCompact.DOMAIN_SEPARATOR(), qualifiedClaimHash) + ); + + (r, vs) = vm.signCompact(allocatorPrivateKey, digest); + bytes memory allocatorSignature = abi.encodePacked(r, vs); + + BatchClaimComponent[] memory claims = new BatchClaimComponent[](3); + claims[0] = BatchClaimComponent({ id: id, allocatedAmount: amount, amount: amount }); + claims[1] = BatchClaimComponent({ + id: anotherId, + allocatedAmount: anotherAmount, + amount: anotherAmount + }); + claims[2] = BatchClaimComponent({ + id: aThirdId, + allocatedAmount: aThirdAmount, + amount: aThirdAmount + }); + + QualifiedBatchClaimWithWitness memory claim = QualifiedBatchClaimWithWitness( + allocatorSignature, + sponsorSignature, + swapper, + nonce, + expires, + witness, + witnessTypestring, + qualificationTypehash, + qualificationPayload, + claims, + claimant + ); + + vm.prank(arbiter); + (bool status) = theCompact.claim(claim); + assert(status); + + assertEq(address(theCompact).balance, amount); + assertEq(token.balanceOf(address(theCompact)), anotherAmount); + assertEq(anotherToken.balanceOf(address(theCompact)), aThirdAmount); + + assertEq(theCompact.balanceOf(claimant, id), amount); + assertEq(theCompact.balanceOf(claimant, anotherId), anotherAmount); + assertEq(theCompact.balanceOf(claimant, aThirdId), aThirdAmount); + } }