diff --git a/foundry.toml b/foundry.toml index 0a02cca..6371db5 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,7 +3,7 @@ solc = '0.8.28' evm_version='cancun' via_ir = true # optimizer_runs = 4_294_967_295 -optimizer_runs = 80_000 +optimizer_runs = 25_000 bytecode_hash = 'none' src = "src" out = "out" diff --git a/src/TheCompact.sol b/src/TheCompact.sol index 175c017..b9728e1 100644 --- a/src/TheCompact.sol +++ b/src/TheCompact.sol @@ -640,6 +640,34 @@ contract TheCompact is ITheCompact, ERC6909, Extsload { return _processExogenousMultichainClaimWithWitness(claimPayload, _release); } + function claim(QualifiedMultichainClaimWithWitness calldata claimPayload) + external + returns (bool) + { + return _processQualifiedMultichainClaimWithWitness(claimPayload, _release); + } + + function claimAndWithdraw(QualifiedMultichainClaimWithWitness calldata claimPayload) + external + returns (bool) + { + return _processQualifiedMultichainClaimWithWitness(claimPayload, _release); + } + + function claim(ExogenousQualifiedMultichainClaimWithWitness calldata claimPayload) + external + returns (bool) + { + return _processExogenousQualifiedMultichainClaimWithWitness(claimPayload, _release); + } + + function claimAndWithdraw(ExogenousQualifiedMultichainClaimWithWitness calldata claimPayload) + external + returns (bool) + { + return _processExogenousQualifiedMultichainClaimWithWitness(claimPayload, _release); + } + function enableForcedWithdrawal(uint256 id) external returns (uint256 withdrawableAt) { // overflow check not necessary as reset period is capped unchecked { @@ -908,6 +936,24 @@ contract TheCompact is ITheCompact, ERC6909, Extsload { ); } + function _notExpiredAndWithValidSignaturesQualifiedExogenousWithWitness( + bytes32 messageHash, + bytes32 qualificationMessageHash, + ExogenousQualifiedMultichainClaimWithWitness calldata claimPayload, + address allocator + ) internal view { + _notExpiredAndSignedByBoth( + claimPayload.expires, + claimPayload.notarizedChainId.toNotarizedDomainSeparator(), + messageHash, + claimPayload.sponsor, + claimPayload.sponsorSignature, + qualificationMessageHash, + allocator, + claimPayload.allocatorSignature + ); + } + // NOTE: this function expects that there's at least one array element function _notExpiredAndWithValidSignaturesBatch(BatchClaim calldata claimPayload) internal @@ -1160,6 +1206,30 @@ contract TheCompact is ITheCompact, ERC6909, Extsload { ); } + function _processQualifiedMultichainClaimWithWitness( + QualifiedMultichainClaimWithWitness calldata claimPayload, + function(address, address, uint256, uint256) internal returns (bool) operation + ) internal returns (bool) { + (bytes32 messageHash, bytes32 qualificationMessageHash) = claimPayload.toMessageHash(); + address allocator = claimPayload.id.toRegisteredAllocatorWithConsumed(claimPayload.nonce); + + _notExpiredAndWithValidQualifiedSignatures.usingQualifiedMultichainClaimWithWitness()( + messageHash, qualificationMessageHash, claimPayload, allocator + ); + + claimPayload.amount.withinAllocated(claimPayload.allocatedAmount); + + return _emitAndOperate( + claimPayload.sponsor, + claimPayload.claimant, + claimPayload.id, + messageHash, + claimPayload.amount, + allocator, + operation + ); + } + function _processExogenousMultichainClaim( ExogenousMultichainClaim calldata claimPayload, function(address, address, uint256, uint256) internal returns (bool) operation @@ -1253,6 +1323,37 @@ contract TheCompact is ITheCompact, ERC6909, Extsload { ); } + function _processExogenousQualifiedMultichainClaimWithWitness( + ExogenousQualifiedMultichainClaimWithWitness calldata claimPayload, + function(address, address, uint256, uint256) internal returns (bool) operation + ) internal returns (bool) { + (bytes32 messageHash, bytes32 qualificationMessageHash) = claimPayload.toMessageHash(); + + uint256 id = claimPayload.id; + uint256 amount = claimPayload.amount; + address allocator = id.toRegisteredAllocatorWithConsumed(claimPayload.nonce); + + _notExpiredAndWithValidSignaturesQualifiedExogenousWithWitness( + messageHash, qualificationMessageHash, claimPayload, allocator + ); + + if (id.toScope() != Scope.Multichain) { + revert InvalidScope(id); + } + + amount.withinAllocated(claimPayload.allocatedAmount); + + return _emitAndOperate( + claimPayload.sponsor, + claimPayload.claimant, + id, + messageHash, + amount, + allocator, + operation + ); + } + function _verifyAndProcessSplitComponents( address sponsor, bytes32 messageHash, diff --git a/src/lib/FunctionCastLib.sol b/src/lib/FunctionCastLib.sol index 1f8d401..891034c 100644 --- a/src/lib/FunctionCastLib.sol +++ b/src/lib/FunctionCastLib.sol @@ -32,7 +32,8 @@ import { QualifiedMultichainClaim, ExogenousMultichainClaim, ExogenousQualifiedMultichainClaim, - MultichainClaimWithWitness + MultichainClaimWithWitness, + QualifiedMultichainClaimWithWitness } from "../types/MultichainClaims.sol"; import { @@ -541,4 +542,19 @@ library FunctionCastLib { fnOut := fnIn } } + + function usingQualifiedMultichainClaimWithWitness( + function(bytes32, bytes32, QualifiedClaim calldata, address) internal view fnIn + ) + internal + pure + returns ( + function(bytes32, bytes32, QualifiedMultichainClaimWithWitness calldata, address) internal view + fnOut + ) + { + assembly ("memory-safe") { + fnOut := fnIn + } + } } diff --git a/src/lib/HashLib.sol b/src/lib/HashLib.sol index bc3dd9b..fbc4854 100644 --- a/src/lib/HashLib.sol +++ b/src/lib/HashLib.sol @@ -635,6 +635,48 @@ library HashLib { } } + function usingQualifiedMultichainClaimWithWitness( + function (MultichainClaim calldata, uint256, bytes32, bytes32) + internal + view + returns (bytes32) fnIn + ) + internal + pure + returns ( + function (QualifiedMultichainClaimWithWitness calldata, uint256, bytes32, bytes32) + internal + view + returns (bytes32) fnOut + ) + { + assembly ("memory-safe") { + fnOut := fnIn + } + } + + function usingQualifiedMultichainClaimWithWitness( + function ( + QualifiedClaim calldata, + bytes32, + uint256 + ) internal pure returns (bytes32) fnIn + ) + internal + pure + returns ( + function ( + QualifiedMultichainClaimWithWitness calldata, + bytes32, + uint256 + ) internal pure returns (bytes32) fnOut + ) + { + assembly ("memory-safe") { + fnOut := fnIn + } + } + function usingQualifiedMultichainClaim( function ( QualifiedClaim calldata, @@ -677,6 +719,26 @@ library HashLib { } } + function usingExogenousQualifiedMultichainClaimWithWitness( + function (ExogenousMultichainClaim calldata, uint256, bytes32, bytes32) + internal + view + returns (bytes32) fnIn + ) + internal + pure + returns ( + function (ExogenousQualifiedMultichainClaimWithWitness calldata, uint256, bytes32, bytes32) + internal + view + returns (bytes32) fnOut + ) + { + assembly ("memory-safe") { + fnOut := fnIn + } + } + function usingExogenousMultichainClaimWithWitness( function ( QualifiedClaim calldata, @@ -747,6 +809,22 @@ library HashLib { )(claim, 0x40, allocationTypehash, multichainCompactTypehash); } + function toMessageHash(QualifiedMultichainClaimWithWitness calldata claim) + internal + view + returns (bytes32 messageHash, bytes32 qualificationMessageHash) + { + (bytes32 allocationTypehash, bytes32 multichainCompactTypehash) = + usingQualifiedMultichainClaimWithWitness(getMultichainTypehashes)(claim); + + messageHash = usingQualifiedMultichainClaimWithWitness(toMultichainClaimMessageHash)( + claim, 0x80, allocationTypehash, multichainCompactTypehash + ); + qualificationMessageHash = usingQualifiedMultichainClaimWithWitness( + toQualificationMessageHash + )(claim, messageHash, 0x40); + } + function toMessageHash(QualifiedMultichainClaim calldata claim) internal view @@ -805,6 +883,28 @@ library HashLib { } } + function usingExogenousQualifiedMultichainClaimWithWitness( + function( + QualifiedClaim calldata, + bytes32, + uint256 + ) internal pure returns (bytes32) fnIn + ) + internal + pure + returns ( + function( + ExogenousQualifiedMultichainClaimWithWitness calldata, + bytes32, + uint256 + ) internal pure returns (bytes32) fnOut + ) + { + assembly ("memory-safe") { + fnOut := fnIn + } + } + function usingExogenousMultichainClaimWithWitness( function ( MultichainClaimWithWitness calldata @@ -823,6 +923,42 @@ library HashLib { } } + function usingExogenousQualifiedMultichainClaimWithWitness( + function ( + MultichainClaimWithWitness calldata + ) internal pure returns (bytes32, bytes32) fnIn + ) + internal + pure + returns ( + function ( + ExogenousQualifiedMultichainClaimWithWitness calldata + ) internal pure returns (bytes32, bytes32) fnOut + ) + { + assembly ("memory-safe") { + fnOut := fnIn + } + } + + function usingQualifiedMultichainClaimWithWitness( + function ( + MultichainClaimWithWitness calldata + ) internal pure returns (bytes32, bytes32) fnIn + ) + internal + pure + returns ( + function ( + QualifiedMultichainClaimWithWitness calldata + ) internal pure returns (bytes32, bytes32) fnOut + ) + { + assembly ("memory-safe") { + fnOut := fnIn + } + } + function getMultichainTypehashes(MultichainClaimWithWitness calldata claim) internal pure @@ -930,6 +1066,22 @@ library HashLib { )(claim, messageHash, 0); } + function toMessageHash(ExogenousQualifiedMultichainClaimWithWitness calldata claim) + internal + view + returns (bytes32 messageHash, bytes32 qualificationMessageHash) + { + (bytes32 allocationTypehash, bytes32 multichainCompactTypehash) = + usingExogenousQualifiedMultichainClaimWithWitness(getMultichainTypehashes)(claim); + + messageHash = usingExogenousQualifiedMultichainClaimWithWitness( + toExogenousMultichainClaimMessageHash + )(claim, 0x80, allocationTypehash, multichainCompactTypehash); + qualificationMessageHash = usingExogenousQualifiedMultichainClaimWithWitness( + toQualificationMessageHash + )(claim, messageHash, 0x40); + } + function toPermit2WitnessHash( address allocator, address depositor, diff --git a/test/TheCompact.t.sol b/test/TheCompact.t.sol index bed1681..854588a 100644 --- a/test/TheCompact.t.sol +++ b/test/TheCompact.t.sol @@ -42,7 +42,9 @@ import { QualifiedMultichainClaim, ExogenousQualifiedMultichainClaim, MultichainClaimWithWitness, - ExogenousMultichainClaimWithWitness + ExogenousMultichainClaimWithWitness, + QualifiedMultichainClaimWithWitness, + ExogenousQualifiedMultichainClaimWithWitness } from "../src/types/MultichainClaims.sol"; import { @@ -3227,4 +3229,185 @@ contract TheCompactTest is Test { vm.chainId(notarizedChainId); assertEq(block.chainid, notarizedChainId); } + + function test_qualifiedMultichainClaimWithWitness() public { + ResetPeriod resetPeriod = ResetPeriod.TenMinutes; + Scope scope = Scope.Multichain; + uint256 amount = 1e18; + uint256 anotherAmount = 1e18; + uint256 nonce = 0; + uint256 expires = block.timestamp + 1000; + address claimant = 0x1111111111111111111111111111111111111111; + address arbiter = 0x2222222222222222222222222222222222222222; + uint256 anotherChainId = 7171717; + + vm.prank(allocator); + theCompact.__register(allocator, ""); + + vm.startPrank(swapper); + uint256 id = theCompact.deposit{ value: amount }(allocator, resetPeriod, scope, swapper); + uint256 anotherId = theCompact.deposit( + address(token), + allocator, + ResetPeriod.TenMinutes, + Scope.Multichain, + anotherAmount, + swapper + ); + vm.stopPrank(); + + assertEq(theCompact.balanceOf(swapper, id), amount); + assertEq(theCompact.balanceOf(swapper, anotherId), anotherAmount); + + uint256[2][] memory idsAndAmountsOne = new uint256[2][](1); + idsAndAmountsOne[0] = [id, amount]; + + uint256[2][] memory idsAndAmountsTwo = new uint256[2][](1); + idsAndAmountsTwo[0] = [anotherId, anotherAmount]; + + string memory witnessTypestring = "Witness witness)Witness(uint256 witnessArgument)"; + uint256 witnessArgument = 234; + bytes32 witness = keccak256(abi.encode(witnessArgument)); + + bytes32 allocationHashOne = keccak256( + abi.encode( + keccak256( + "Allocation(address arbiter,uint256 chainId,uint256[2][] idsAndAmounts,Witness witness)Witness(uint256 witnessArgument)" + ), + arbiter, + block.chainid, + keccak256(abi.encodePacked(idsAndAmountsOne)), + witness + ) + ); + + bytes32 allocationHashTwo = keccak256( + abi.encode( + keccak256( + "Allocation(address arbiter,uint256 chainId,uint256[2][] idsAndAmounts,Witness witness)Witness(uint256 witnessArgument)" + ), + arbiter, + anotherChainId, + keccak256(abi.encodePacked(idsAndAmountsTwo)), + witness + ) + ); + + bytes32 claimHash = keccak256( + abi.encode( + keccak256( + "MultichainCompact(address sponsor,uint256 nonce,uint256 expires,Allocation[] allocations)Allocation(address arbiter,uint256 chainId,uint256[2][] idsAndAmounts,Witness witness)Witness(uint256 witnessArgument)" + ), + swapper, + nonce, + expires, + keccak256(abi.encodePacked(allocationHashOne, allocationHashTwo)) + ) + ); + + bytes32 initialDomainSeparator = theCompact.DOMAIN_SEPARATOR(); + + bytes32 digest = + keccak256(abi.encodePacked(bytes2(0x1901), initialDomainSeparator, 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), initialDomainSeparator, qualifiedClaimHash)); + + (r, vs) = vm.signCompact(allocatorPrivateKey, digest); + bytes memory allocatorSignature = abi.encodePacked(r, vs); + + bytes32[] memory additionalChains = new bytes32[](1); + additionalChains[0] = allocationHashTwo; + + QualifiedMultichainClaimWithWitness memory claim = QualifiedMultichainClaimWithWitness( + allocatorSignature, + sponsorSignature, + swapper, + nonce, + expires, + witness, + witnessTypestring, + qualificationTypehash, + qualificationPayload, + additionalChains, + id, + amount, + claimant, + amount + ); + + uint256 snapshotId = vm.snapshot(); + vm.prank(arbiter); + (bool status) = theCompact.claim(claim); + assert(status); + + assertEq(address(theCompact).balance, amount); + assertEq(claimant.balance, 0); + assertEq(theCompact.balanceOf(swapper, id), 0); + assertEq(theCompact.balanceOf(claimant, id), amount); + vm.revertToAndDelete(snapshotId); + + // change to "new chain" (this hack is so the original one gets stored) + uint256 notarizedChainId = abi.decode(abi.encode(block.chainid), (uint256)); + assert(notarizedChainId != anotherChainId); + vm.chainId(anotherChainId); + assertEq(block.chainid, anotherChainId); + assert(notarizedChainId != anotherChainId); + + bytes32 anotherDomainSeparator = theCompact.DOMAIN_SEPARATOR(); + + assert(initialDomainSeparator != anotherDomainSeparator); + + digest = + keccak256(abi.encodePacked(bytes2(0x1901), anotherDomainSeparator, qualifiedClaimHash)); + + (r, vs) = vm.signCompact(allocatorPrivateKey, digest); + bytes memory exogenousAllocatorSignature = abi.encodePacked(r, vs); + + additionalChains[0] = allocationHashOne; + uint256 chainIndex = 0; + + ExogenousQualifiedMultichainClaimWithWitness memory anotherClaim = + ExogenousQualifiedMultichainClaimWithWitness( + exogenousAllocatorSignature, + sponsorSignature, + swapper, + nonce, + expires, + witness, + witnessTypestring, + qualificationTypehash, + qualificationPayload, + additionalChains, + chainIndex, + notarizedChainId, + anotherId, + anotherAmount, + claimant, + anotherAmount + ); + + vm.prank(arbiter); + (bool exogenousStatus) = theCompact.claim(anotherClaim); + assert(exogenousStatus); + + assertEq(theCompact.balanceOf(swapper, anotherId), 0); + assertEq(theCompact.balanceOf(claimant, anotherId), anotherAmount); + + // change back + vm.chainId(notarizedChainId); + assertEq(block.chainid, notarizedChainId); + } }