From 6285c025410a7f84fab032b5f301236c4947c719 Mon Sep 17 00:00:00 2001 From: 0age <37939117+0age@users.noreply.github.com> Date: Sun, 13 Oct 2024 19:05:30 -0700 Subject: [PATCH] qualified split claim --- src/TheCompact.sol | 85 +++++++++++++++++++++++------- src/lib/HashLib.sol | 47 ++++++++++------- test/TheCompact.t.sol | 120 ++++++++++++++++++++++++++++++++---------- 3 files changed, 188 insertions(+), 64 deletions(-) diff --git a/src/TheCompact.sol b/src/TheCompact.sol index 004a837..933b48a 100644 --- a/src/TheCompact.sol +++ b/src/TheCompact.sol @@ -432,6 +432,14 @@ contract TheCompact is ITheCompact, ERC6909, Extsload { return _processSplitClaim(claimPayload, _withdraw); } + function claim(QualifiedSplitClaim calldata claimPayload) external returns (bool) { + return _processQualifiedSplitClaim(claimPayload, _release); + } + + function claimAndWithdraw(QualifiedSplitClaim calldata claimPayload) external returns (bool) { + return _processQualifiedSplitClaim(claimPayload, _withdraw); + } + function claim(BatchClaim calldata claimPayload) external returns (bool) { return _processBatchClaim(claimPayload, _release); } @@ -704,37 +712,28 @@ contract TheCompact is ITheCompact, ERC6909, Extsload { ); } - function _processSplitClaim( - SplitClaim calldata claimPayload, + function _verifyAndProcessSplitComponents( + address sponsor, + bytes32 messageHash, + uint256 id, + uint256 allocatedAmount, + SplitComponent[] calldata claimants, function(address, address, uint256, uint256) internal returns (bool) operation ) internal returns (bool) { - claimPayload.expires.later(); - - uint256 id = claimPayload.id; - address allocator = id.toRegisteredAllocatorWithConsumed(claimPayload.nonce); - - bytes32 messageHash = claimPayload.toMessageHash(); - bytes32 domainSeparator = _INITIAL_DOMAIN_SEPARATOR.toLatest(_INITIAL_CHAIN_ID); - messageHash.signedBy(claimPayload.sponsor, claimPayload.sponsorSignature, domainSeparator); - messageHash.signedBy(allocator, claimPayload.allocatorSignature, domainSeparator); - - uint256 totalClaims = claimPayload.claimants.length; - uint256 allocatedAmount = claimPayload.allocatedAmount; + uint256 totalClaims = claimants.length; uint256 spentAmount = 0; uint256 errorBuffer = (totalClaims == 0).asUint256(); unchecked { for (uint256 i = 0; i < totalClaims; ++i) { - SplitComponent calldata component = claimPayload.claimants[i]; + SplitComponent calldata component = claimants[i]; uint256 amount = component.amount; uint256 updatedSpentAmount = amount + spentAmount; errorBuffer |= (updatedSpentAmount < spentAmount).asUint256(); spentAmount = updatedSpentAmount; - emitAndOperate( - claimPayload.sponsor, component.claimant, id, messageHash, amount, operation - ); + emitAndOperate(sponsor, component.claimant, id, messageHash, amount, operation); } } @@ -752,6 +751,30 @@ contract TheCompact is ITheCompact, ERC6909, Extsload { return true; } + function _processSplitClaim( + SplitClaim calldata claimPayload, + function(address, address, uint256, uint256) internal returns (bool) operation + ) internal returns (bool) { + claimPayload.expires.later(); + + uint256 id = claimPayload.id; + address allocator = id.toRegisteredAllocatorWithConsumed(claimPayload.nonce); + + bytes32 messageHash = claimPayload.toMessageHash(); + bytes32 domainSeparator = _INITIAL_DOMAIN_SEPARATOR.toLatest(_INITIAL_CHAIN_ID); + messageHash.signedBy(claimPayload.sponsor, claimPayload.sponsorSignature, domainSeparator); + messageHash.signedBy(allocator, claimPayload.allocatorSignature, domainSeparator); + + return _verifyAndProcessSplitComponents( + claimPayload.sponsor, + messageHash, + id, + claimPayload.allocatedAmount, + claimPayload.claimants, + operation + ); + } + function _processQualifiedClaim( QualifiedClaim calldata claimPayload, function(address, address, uint256, uint256) internal returns (bool) operation @@ -778,6 +801,32 @@ contract TheCompact is ITheCompact, ERC6909, Extsload { ); } + function _processQualifiedSplitClaim( + QualifiedSplitClaim calldata claimPayload, + function(address, address, uint256, uint256) internal returns (bool) operation + ) internal returns (bool) { + claimPayload.expires.later(); + + uint256 id = claimPayload.id; + address allocator = id.toRegisteredAllocatorWithConsumed(claimPayload.nonce); + + (bytes32 messageHash, bytes32 qualificationMessageHash) = claimPayload.toMessageHash(); + bytes32 domainSeparator = _INITIAL_DOMAIN_SEPARATOR.toLatest(_INITIAL_CHAIN_ID); + messageHash.signedBy(claimPayload.sponsor, claimPayload.sponsorSignature, domainSeparator); + qualificationMessageHash.signedBy( + allocator, claimPayload.allocatorSignature, domainSeparator + ); + + return _verifyAndProcessSplitComponents( + claimPayload.sponsor, + messageHash, + id, + claimPayload.allocatedAmount, + claimPayload.claimants, + operation + ); + } + function _processClaimWithWitness( ClaimWithWitness calldata claimPayload, function(address, address, uint256, uint256) internal returns (bool) operation diff --git a/src/lib/HashLib.sol b/src/lib/HashLib.sol index 7822ca5..8580e75 100644 --- a/src/lib/HashLib.sol +++ b/src/lib/HashLib.sol @@ -159,6 +159,28 @@ library HashLib { qualificationMessageHash = toQualificationMessageHash(claim, messageHash, 0); } + function usingQualifiedSplitClaim( + function ( + QualifiedClaim calldata, + bytes32, + uint256 + ) internal pure returns (bytes32) fnIn + ) + internal + pure + returns ( + function( + QualifiedSplitClaim calldata, + bytes32, + uint256 + ) internal pure returns (bytes32) fnOut + ) + { + assembly { + fnOut := fnIn + } + } + function toQualificationMessageHash( QualifiedClaim calldata claim, bytes32 messageHash, @@ -308,7 +330,7 @@ library HashLib { } } - function toMessageHash(QualifiedSplitClaim memory claim) + function toMessageHash(QualifiedSplitClaim calldata claim) internal view returns (bytes32 messageHash, bytes32 qualificationMessageHash) @@ -316,27 +338,16 @@ library HashLib { 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 allocatedAmount := mload(add(claim, 0x80)) - mstore(m, COMPACT_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), id) - mstore(add(m, 0xc0), allocatedAmount) + mstore(add(m, 0x20), caller()) // arbiter: msg.sender + calldatacopy(add(m, 0x40), add(claim, 0x40), 0x60) // sponsor, nonce, expires + mstore(add(m, 0xa0), calldataload(add(claim, 0xe0))) + mstore(add(m, 0xc0), calldataload(add(claim, 0x100))) messageHash := keccak256(m, 0xe0) } - // TODO: optimize once we're using calldata - qualificationMessageHash = keccak256( - abi.encodePacked(claim.qualificationTypehash, messageHash, claim.qualificationPayload) - ); + qualificationMessageHash = + usingQualifiedSplitClaim(toQualificationMessageHash)(claim, messageHash, 0); } function toMessageHash(QualifiedSplitClaimWithWitness memory claim) diff --git a/test/TheCompact.t.sol b/test/TheCompact.t.sol index d1bc29a..2aa99a2 100644 --- a/test/TheCompact.t.sol +++ b/test/TheCompact.t.sol @@ -16,7 +16,8 @@ import { QualifiedClaim, ClaimWithWitness, QualifiedClaimWithWitness, - SplitClaim + SplitClaim, + QualifiedSplitClaim } from "../src/types/Claims.sol"; import { BatchTransfer, SplitBatchTransfer, BatchClaim } from "../src/types/BatchClaims.sol"; @@ -40,8 +41,6 @@ contract TheCompactTest is Test { address swapper; uint256 allocatorPrivateKey; address allocator; - address dummyOracle; - address dummyBatchOracle; bytes32 compactEIP712DomainHash = keccak256( "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" ); @@ -49,31 +48,6 @@ contract TheCompactTest is Test { keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); function setUp() public { - address deployedDummyOracle; - address deployedDummyBatchOracle; - assembly { - // deploy a contract that always returns one word of 0's followed by one word of f's - // minimal "constructor" 0x600b5981380380925939f3... (11 bytes) - // runtime code (9 bytes): - // Op Opcode Name Stack - // 60 40 PUSH1 0x40 [0x40] - // 3D RETURNDATASIZE [0, 0x40] - // 3D RETURNDATASIZE [0, 0, 0x40] - // 19 NOT [type(uint256).max, 0, 0x40] - // 60 20 PUSH1 0x20 [0x20, type(uint256).max, 0, 0x40] - // 52 MSTORE [0, 0x40] (Memory at 0x20 set to type(uint256).max) - // F3 RETURN [] (Returns 0x40 bytes from memory starting at 0x00) - mstore(0, 0x600b5981380380925939f360403d3d19602052f3) - deployedDummyOracle := create(0, 12, 20) - - // and this one returns [0, 0x40, 3, 0xfff, 0xfff, 0xfff] - mstore(0, 0x600b598138038092) - mstore(0x20, 0x5939f360c03d60406020600360403d1960603d1960803d1960a05252525252f3) - deployedDummyBatchOracle := create(0, 24, 40) - } - dummyOracle = deployedDummyOracle; - dummyBatchOracle = deployedDummyBatchOracle; - address permit2Deployer = address(0x4e59b44847b379578588920cA78FbF26c0B4956C); address deployedPermit2Deployer; address permit2DeployerDeployer = address(0x3fAB184622Dc19b6109349B94811493BF2a45362); @@ -1162,6 +1136,96 @@ contract TheCompactTest is Test { assertEq(theCompact.balanceOf(recipientTwo, id), amountTwo); } + function test_qualifiedSplitClaim() public { + ResetPeriod resetPeriod = ResetPeriod.TenMinutes; + Scope scope = Scope.Multichain; + uint256 amount = 1e18; + uint256 nonce = 0; + uint256 expires = block.timestamp + 1000; + address arbiter = 0x2222222222222222222222222222222222222222; + address recipientOne = 0x1111111111111111111111111111111111111111; + address recipientTwo = 0x3333333333333333333333333333333333333333; + uint256 amountOne = 4e17; + uint256 amountTwo = 6e17; + + vm.prank(allocator); + theCompact.__register(allocator, ""); + + vm.prank(swapper); + uint256 id = theCompact.deposit{ value: amount }(allocator, resetPeriod, scope, swapper); + assertEq(theCompact.balanceOf(swapper, id), amount); + + bytes32 claimHash = keccak256( + abi.encode( + keccak256( + "Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount)" + ), + arbiter, + swapper, + nonce, + expires, + id, + amount + ) + ); + + 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)); + + 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); + + digest = keccak256( + abi.encodePacked(bytes2(0x1901), theCompact.DOMAIN_SEPARATOR(), qualifiedClaimHash) + ); + + (r, vs) = vm.signCompact(allocatorPrivateKey, digest); + bytes memory allocatorSignature = abi.encodePacked(r, vs); + + SplitComponent memory splitOne = + SplitComponent({ claimant: recipientOne, amount: amountOne }); + + SplitComponent memory splitTwo = + SplitComponent({ claimant: recipientTwo, amount: amountTwo }); + + SplitComponent[] memory recipients = new SplitComponent[](2); + recipients[0] = splitOne; + recipients[1] = splitTwo; + + QualifiedSplitClaim memory claim = QualifiedSplitClaim( + allocatorSignature, + sponsorSignature, + swapper, + nonce, + expires, + qualificationTypehash, + qualificationPayload, + id, + amount, + recipients + ); + + vm.prank(arbiter); + (bool status) = theCompact.claim(claim); + assert(status); + + assertEq(address(theCompact).balance, amount); + assertEq(recipientOne.balance, 0); + assertEq(recipientTwo.balance, 0); + assertEq(theCompact.balanceOf(swapper, id), 0); + assertEq(theCompact.balanceOf(recipientOne, id), amountOne); + assertEq(theCompact.balanceOf(recipientTwo, id), amountTwo); + } + function test_batchClaim() public { uint256 amount = 1e18; uint256 anotherAmount = 1e18;