diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3c10181..0a4ac33 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,3 +45,15 @@ jobs: id: test env: FORGE_SNAPSHOT_CHECK: true + + - name: Run Forge coverage + run: | + forge coverage --report lcov + id: coverage + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./lcov.info + fail_ci_if_error: true diff --git a/.gitignore b/.gitignore index f0e124c..2c4419a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Compiler files cache/ out/ +lcov.info # Ignores development broadcast logs !/broadcast diff --git a/README.md b/README.md index 82fee9a..c7f9864 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ > :warning: This is an early-stage contract under active development; it has not yet been properly tested, reviewed, or audited. +> NOTE: this README is based on Version 0 of The Compact; it still needs to be updated for Version 1, which is currently under active development. + ## Summary The Compact is an ownerless ERC6909 contract that facilitates the voluntary formation (and, if necessary, eventual dissolution) of reusable resource locks. @@ -35,8 +37,11 @@ $ git clone git@github.com:Uniswap/the-compact.git && cd the-compact # install dependencies & libraries $ forge install -# run basic tests +# run basic tests & gas snapshots $ forge test + +# run coverage & generate report +$ forge coverage --report lcov ``` ## Usage diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..e006a2f --- /dev/null +++ b/codecov.yml @@ -0,0 +1,52 @@ +codecov: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + range: "70...100" + status: + project: + default: + # basic + target: auto + threshold: 0% + base: auto + # advanced + branches: null + if_no_uploads: error + if_not_found: success + if_ci_failed: error + only_pulls: false + flags: null + paths: null + patch: + default: + # basic + target: auto + threshold: 0% + base: auto + # advanced + branches: null + if_no_uploads: error + if_not_found: success + if_ci_failed: error + only_pulls: false + flags: null + paths: null + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +comment: + layout: "reach,diff,flags,files,footer" + behavior: default + require_changes: no + require_base: no + require_head: yes + branches: null diff --git a/snapshots/TheCompactTest.json b/snapshots/TheCompactTest.json index 2780f0c..ea9fb13 100644 --- a/snapshots/TheCompactTest.json +++ b/snapshots/TheCompactTest.json @@ -1,29 +1,29 @@ { - "batchClaimRegisteredWithDepositWithWitness": "145160", - "batchDepositAndRegisterWithWitnessViaPermit2": "219004", - "claim": "86514", - "claimAndWithdraw": "114496", - "depositAndRegisterViaPermit2": "123247", - "depositBatchSingleERC20": "66450", - "depositBatchSingleNative": "26935", - "depositBatchViaPermit2NativeAndERC20": "127573", - "depositBatchViaPermit2SingleERC20": "102735", - "depositERC20AndURI": "65883", - "depositERC20Basic": "65883", - "depositERC20ViaPermit2AndURI": "98371", - "depositETHAndURI": "25953", + "batchClaimRegisteredWithDepositWithWitness": "145278", + "batchDepositAndRegisterWithWitnessViaPermit2": "218447", + "claim": "86555", + "claimAndWithdraw": "114537", + "depositAndRegisterViaPermit2": "122980", + "depositBatchSingleERC20": "66488", + "depositBatchSingleNative": "26957", + "depositBatchViaPermit2NativeAndERC20": "127465", + "depositBatchViaPermit2SingleERC20": "102627", + "depositERC20AndURI": "65965", + "depositERC20Basic": "65965", + "depositERC20ViaPermit2AndURI": "97718", + "depositETHAndURI": "26019", "depositETHBasic": "26177", - "depositRegisterFor": "78131", - "exogenousSplitBatchMultichainClaimWithWitness": "119870", - "exogenousSplitMultichainClaimWithWitness": "91532", - "qualified_basicTransfer": "59816", - "register": "24458", - "splitBatchClaimWithWitness": "145154", - "splitBatchMultichainClaimWithWitness": "92127", - "splitBatchTransfer": "117228", - "splitBatchWithdrawal": "146028", - "splitClaimWithWitness": "89649", - "splitMultichainClaimWithWitness": "90333", - "splitTransfer": "86967", - "splitWithdrawal": "95819" + "depositRegisterFor": "78197", + "exogenousSplitBatchMultichainClaimWithWitness": "120030", + "exogenousSplitMultichainClaimWithWitness": "91645", + "qualified_basicTransfer": "59904", + "register": "24502", + "splitBatchClaimWithWitness": "145278", + "splitBatchMultichainClaimWithWitness": "92279", + "splitBatchTransfer": "117294", + "splitBatchWithdrawal": "146094", + "splitClaimWithWitness": "89690", + "splitMultichainClaimWithWitness": "90402", + "splitTransfer": "87055", + "splitWithdrawal": "95907" } \ No newline at end of file diff --git a/src/TheCompact.sol b/src/TheCompact.sol index 4cdc546..1c25d0f 100644 --- a/src/TheCompact.sol +++ b/src/TheCompact.sol @@ -11,6 +11,7 @@ import { Scope } from "./types/Scope.sol"; import { ResetPeriod } from "./types/ResetPeriod.sol"; import { ForcedWithdrawalStatus } from "./types/ForcedWithdrawalStatus.sol"; import { EmissaryStatus } from "./types/EmissaryStatus.sol"; +import { DepositDetails } from "./types/DepositDetails.sol"; import { TheCompactLogic } from "./lib/TheCompactLogic.sol"; @@ -139,39 +140,31 @@ contract TheCompact is ITheCompact, ERC6909, TheCompactLogic { } function deposit( - address token, - uint256, // amount - uint256, // nonce - uint256, // deadline + ISignatureTransfer.PermitTransferFrom calldata permit, address, // depositor bytes12, // lockTag address recipient, bytes calldata signature ) external returns (uint256) { - return _depositViaPermit2(token, recipient, signature); + return _depositViaPermit2(permit.permitted.token, recipient, signature); } function depositAndRegister( - address token, - uint256, // amount - uint256, // nonce - uint256, // deadline + ISignatureTransfer.PermitTransferFrom calldata permit, address depositor, // also recipient bytes12, // lockTag bytes32 claimHash, - CompactCategory compactCategory, + CompactCategory, // compactCategory string calldata witness, bytes calldata signature ) external returns (uint256) { - return _depositAndRegisterViaPermit2(token, depositor, claimHash, compactCategory, witness, signature); + return _depositAndRegisterViaPermit2(permit.permitted.token, depositor, claimHash, witness, signature); } function deposit( address, // depositor ISignatureTransfer.TokenPermissions[] calldata permitted, - uint256, // nonce - uint256, // deadline - bytes12, // lockTag + DepositDetails calldata, address recipient, bytes calldata signature ) external payable returns (uint256[] memory) { @@ -181,15 +174,13 @@ contract TheCompact is ITheCompact, ERC6909, TheCompactLogic { function depositAndRegister( address depositor, ISignatureTransfer.TokenPermissions[] calldata permitted, - uint256, // nonce - uint256, // deadline - bytes12, // lockTag, - bytes32 claimHash, - CompactCategory compactCategory, + DepositDetails calldata, + bytes32, // claimHash + CompactCategory, // compactCategory string calldata witness, bytes calldata signature ) external payable returns (uint256[] memory) { - return _depositBatchAndRegisterViaPermit2(depositor, permitted, claimHash, compactCategory, witness, signature); + return _depositBatchAndRegisterViaPermit2(depositor, permitted, witness, signature); } function allocatedTransfer(SplitTransfer calldata transfer) external returns (bool) { @@ -244,12 +235,14 @@ contract TheCompact is ITheCompact, ERC6909, TheCompactLogic { claimHash = HashLib.toFlatMessageHashWithWitness(sponsor, id, amount, arbiter, nonce, expires, typehash, witness); - // Initialize idsAndAmounts array. - uint256[2][] memory idsAndAmounts = new uint256[2][](1); - idsAndAmounts[0] = [id, amount]; + { + // Initialize idsAndAmounts array. + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0] = [id, amount]; - // TOOD: support registering exogenous domain separators by passing notarized chainId - claimHash.hasValidSponsor(sponsor, sponsorSignature, _domainSeparator(), idsAndAmounts); + // TODO: support registering exogenous domain separators by passing notarized chainId? + claimHash.hasValidSponsor(sponsor, sponsorSignature, _domainSeparator(), idsAndAmounts); + } sponsor.registerCompact(claimHash, typehash); } @@ -264,6 +257,19 @@ contract TheCompact is ITheCompact, ERC6909, TheCompactLogic { bytes32 witness, bytes calldata sponsorSignature ) external returns (bytes32 claimHash) { + _enforceConsistentAllocators(idsAndAmounts); + + claimHash = HashLib.toFlatBatchClaimWithWitnessMessageHash( + sponsor, idsAndAmounts, arbiter, nonce, expires, typehash, witness + ); + + // TOOD: support registering exogenous domain separators by passing notarized chainId + claimHash.hasValidSponsor(sponsor, sponsorSignature, _domainSeparator(), idsAndAmounts); + + sponsor.registerCompact(claimHash, typehash); + } + + function _enforceConsistentAllocators(uint256[2][] calldata idsAndAmounts) internal view { // Retrieve the total number of IDs and amounts in the batch. uint256 totalIds = idsAndAmounts.length; @@ -288,15 +294,6 @@ contract TheCompact is ITheCompact, ERC6909, TheCompactLogic { if (errorBuffer.asBool()) { revert InconsistentAllocators(); } - - claimHash = HashLib.toFlatBatchClaimWithWitnessMessageHash( - sponsor, idsAndAmounts, arbiter, nonce, expires, typehash, witness - ); - - // TOOD: support registering exogenous domain separators by passing notarized chainId - claimHash.hasValidSponsor(sponsor, sponsorSignature, _domainSeparator(), idsAndAmounts); - - sponsor.registerCompact(claimHash, typehash); } function assignEmissary(bytes12 lockTag, address emissary) external returns (bool) { diff --git a/src/interfaces/ITheCompact.sol b/src/interfaces/ITheCompact.sol index bd56aac..25a7982 100644 --- a/src/interfaces/ITheCompact.sol +++ b/src/interfaces/ITheCompact.sol @@ -8,6 +8,7 @@ import { Scope } from "../types/Scope.sol"; import { CompactCategory } from "../types/CompactCategory.sol"; import { ISignatureTransfer } from "permit2/src/interfaces/ISignatureTransfer.sol"; import { BasicTransfer, SplitTransfer } from "../types/Claims.sol"; +import { DepositDetails } from "../types/DepositDetails.sol"; import { BatchTransfer, SplitBatchTransfer } from "../types/BatchClaims.sol"; @@ -274,21 +275,14 @@ interface ITheCompact { * implementation details of the respective token. The Permit2 authorization signed by the * depositor must contain a CompactDeposit witness containing the allocator, the reset period, * the scope, and the intended recipient of the deposit. - * @param token The address of the ERC20 token to deposit. - * @param amount The amount of tokens to deposit. - * @param nonce The Permit2 nonce for the signature. - * @param deadline The timestamp until which the signature is valid. - * @param depositor The account signing the permit2 authorization and depositing the tokens. + * @param permit The permit data signed by the depositor. * @param lockTag The lock tag containing allocator ID, reset period, and scope. * @param recipient The address that will receive the corresponding the ERC6909 tokens. * @param signature The Permit2 signature from the depositor authorizing the deposit. * @return id The ERC6909 token identifier of the associated resource lock. */ function deposit( - address token, - uint256 amount, - uint256 nonce, - uint256 deadline, + ISignatureTransfer.PermitTransferFrom calldata permit, address depositor, bytes12 lockTag, address recipient, @@ -305,12 +299,9 @@ interface ITheCompact { * signed by the depositor must contain an Activation witness containing the id of the resource * lock and an associated Compact, BatchCompact, or MultichainCompact payload matching the * specified compact category. - * @param token The address of the ERC20 token to deposit. - * @param amount The amount of tokens to deposit. - * @param nonce The Permit2 nonce for the signature. - * @param deadline The timestamp until which the signature is valid. + * @param permit The permit data signed by the depositor. * @param depositor The account signing the permit2 authorization and depositing the tokens. - * @param lockTag The lock tag containing allocator ID, reset period, and scope. + * @param lockTag The lock tag containing allocator ID, reset period, and scope. * @param claimHash A bytes32 hash derived from the details of the compact. * @param compactCategory The category of the compact being registered (Compact, BatchCompact, or MultichainCompact). * @param witness Additional data used in generating the claim hash. @@ -318,10 +309,7 @@ interface ITheCompact { * @return id The ERC6909 token identifier of the associated resource lock. */ function depositAndRegister( - address token, - uint256 amount, - uint256 nonce, - uint256 deadline, + ISignatureTransfer.PermitTransferFrom calldata permit, address depositor, bytes12 lockTag, bytes32 claimHash, @@ -342,10 +330,8 @@ interface ITheCompact { * CompactDeposit witness containing the allocator, the reset period, the scope, and the * intended recipient of the deposits. * @param depositor The account signing the permit2 authorization and depositing the tokens. - * @param permitted Array of token permissions specifying the deposited tokens and amounts. - * @param nonce The Permit2 nonce for the signature. - * @param deadline The timestamp until which the signature is valid. - * @param lockTag The lock tag containing allocator ID, reset period, and scope. + * @param permitted The permit data signed by the depositor. + * @param details The details of the deposit. * @param recipient The address that will receive the corresponding ERC6909 tokens. * @param signature The Permit2 signature from the depositor authorizing the deposits. * @return ids Array of ERC6909 token identifiers for the associated resource locks. @@ -353,9 +339,7 @@ interface ITheCompact { function deposit( address depositor, ISignatureTransfer.TokenPermissions[] calldata permitted, - uint256 nonce, - uint256 deadline, - bytes12 lockTag, + DepositDetails calldata details, address recipient, bytes calldata signature ) external payable returns (uint256[] memory ids); @@ -374,9 +358,7 @@ interface ITheCompact { * specified compact category. * @param depositor The account signing the permit2 authorization and depositing the tokens. * @param permitted Array of token permissions specifying the deposited tokens and amounts. - * @param nonce The Permit2 nonce for the signature. - * @param deadline The timestamp until which the signature is valid. - * @param lockTag The lock tag containing allocator ID, reset period, and scope. + * @param details The details of the deposit. * @param claimHash A bytes32 hash derived from the details of the compact. * @param compactCategory The category of the compact being registered (Compact, BatchCompact, or MultichainCompact). * @param witness Additional data used in generating the claim hash. @@ -386,9 +368,7 @@ interface ITheCompact { function depositAndRegister( address depositor, ISignatureTransfer.TokenPermissions[] calldata permitted, - uint256 nonce, - uint256 deadline, - bytes12 lockTag, + DepositDetails calldata details, bytes32 claimHash, CompactCategory compactCategory, string calldata witness, diff --git a/src/lib/AllocatorLib.sol b/src/lib/AllocatorLib.sol index 6a0a8f6..7bbb4fa 100644 --- a/src/lib/AllocatorLib.sol +++ b/src/lib/AllocatorLib.sol @@ -26,9 +26,6 @@ library AllocatorLib { // Retrieve the free memory pointer; memory will be left dirtieed. let m := mload(0x40) - // Derive offset to start of data for the call from memory pointer. - let dataStart := add(m, 0x1c) - // Get length of idsAndAmounts array, both in elements and as data. let totalIdsAndAmounts := mload(idsAndAmounts) let totalIdsAndAmountsDataLength := shl(6, totalIdsAndAmounts) @@ -44,20 +41,22 @@ library AllocatorLib { mstore(add(m, 0xe0), add(totalIdsAndAmountsDataLength, 0x100)) mstore(add(m, 0x100), totalIdsAndAmounts) - // Copy each element from idsAndAmounts array in memory. - let dstStart := add(m, 0x120) - let totalIdsAndAmountsTimesOneWord := shl(5, totalIdsAndAmounts) - for { let i := 0 } lt(i, totalIdsAndAmountsTimesOneWord) { i := add(i, 0x20) } { - let dstPos := add(dstStart, shl(1, i)) - let srcPos := mload(add(idsAndAmounts, add(i, 0x20))) - mstore(dstPos, mload(srcPos)) - mstore(add(dstPos, 0x20), mload(add(srcPos, 0x20))) - } + { + // Copy each element from idsAndAmounts array in memory. + let dstStart := add(m, 0x120) + let totalIdsAndAmountsTimesOneWord := shl(5, totalIdsAndAmounts) + for { let i := 0 } lt(i, totalIdsAndAmountsTimesOneWord) { i := add(i, 0x20) } { + let dstPos := add(dstStart, shl(1, i)) + let srcPos := mload(add(idsAndAmounts, add(i, 0x20))) + mstore(dstPos, mload(srcPos)) + mstore(add(dstPos, 0x20), mload(add(srcPos, 0x20))) + } - // Copy allocator data from calldata. - let allocatorDataMemoryOffset := add(dstStart, totalIdsAndAmountsDataLength) - mstore(allocatorDataMemoryOffset, allocatorData.length) - calldatacopy(add(allocatorDataMemoryOffset, 0x20), allocatorData.offset, allocatorData.length) + // Copy allocator data from calldata. + let allocatorDataMemoryOffset := add(dstStart, totalIdsAndAmountsDataLength) + mstore(allocatorDataMemoryOffset, allocatorData.length) + calldatacopy(add(allocatorDataMemoryOffset, 0x20), allocatorData.offset, allocatorData.length) + } // Ensure sure initial scratch space is cleared as an added precaution. mstore(0, 0) @@ -68,7 +67,7 @@ library AllocatorLib { gas(), allocator, 0, - dataStart, + add(m, 0x1c), add(0x124, add(totalIdsAndAmountsDataLength, allocatorData.length)), 0, 0x20 diff --git a/src/lib/ClaimHashFunctionCastLib.sol b/src/lib/ClaimHashFunctionCastLib.sol index a916f95..17c8f24 100644 --- a/src/lib/ClaimHashFunctionCastLib.sol +++ b/src/lib/ClaimHashFunctionCastLib.sol @@ -68,13 +68,13 @@ library ClaimHashFunctionCastLib { /** * @notice Function cast to provide a Claim calldata struct while * treating it as a uint256 representing a calldata pointer location. - * @param fnIn Function pointer to `HashLib.toMessageHashWithWitness(uint256, uint256)`. + * @param fnIn Function pointer to `HashLib.toMessageHashWithWitness(uint256)`. * @return fnOut Modified function used in `ClaimHashLib.toMessageHashes(Claim calldata)`. */ - function usingClaim(function (uint256, uint256) internal view returns (bytes32, bytes32) fnIn) + function usingClaim(function (uint256) internal view returns (bytes32, bytes32) fnIn) internal pure - returns (function (Claim calldata, uint256) internal view returns (bytes32, bytes32) fnOut) + returns (function (Claim calldata) internal view returns (bytes32, bytes32) fnOut) { assembly ("memory-safe") { fnOut := fnIn diff --git a/src/lib/ClaimHashLib.sol b/src/lib/ClaimHashLib.sol index 6c47f37..c7c1c48 100644 --- a/src/lib/ClaimHashLib.sol +++ b/src/lib/ClaimHashLib.sol @@ -25,6 +25,7 @@ import { HashLib } from "./HashLib.sol"; */ library ClaimHashLib { using ClaimHashFunctionCastLib for function(uint256, uint256) internal pure returns (uint256); + using ClaimHashFunctionCastLib for function(uint256) internal view returns (bytes32, bytes32); using ClaimHashFunctionCastLib for function(uint256, uint256) internal view returns (bytes32, bytes32); using ClaimHashFunctionCastLib @@ -73,7 +74,7 @@ library ClaimHashLib { ///// CATEGORY 2: Claim with witness message & type hashes ///// function toMessageHashes(Claim calldata claim) internal view returns (bytes32 claimHash, bytes32 typehash) { - return HashLib.toMessageHashWithWitness.usingClaim()(claim, 0); + return HashLib.toMessageHashWithWitness.usingClaim()(claim); } function toMessageHashes(BatchClaim calldata claim) internal view returns (bytes32 claimHash, bytes32 typehash) { diff --git a/src/lib/ClaimProcessorLib.sol b/src/lib/ClaimProcessorLib.sol index 077a2a9..794a236 100644 --- a/src/lib/ClaimProcessorLib.sol +++ b/src/lib/ClaimProcessorLib.sol @@ -63,29 +63,19 @@ library ClaimProcessorLib { uint256[2][] memory idsAndAmounts, uint256 shortestResetPeriod ) internal returns (address sponsor) { - // Declare variables for signatures and parameters that will be extracted from calldata. - bytes calldata allocatorData; - bytes calldata sponsorSignature; + // Extract sponsor, nonce, and expires from calldata. uint256 nonce; uint256 expires; - assembly ("memory-safe") { - // Extract allocator signature from calldata using offset stored at calldataPointer. - let allocatorDataPtr := add(calldataPointer, calldataload(calldataPointer)) - allocatorData.offset := add(0x20, allocatorDataPtr) - allocatorData.length := calldataload(allocatorDataPtr) - - // Extract sponsor signature from calldata using offset stored at calldataPointer + 0x20. - let sponsorSignaturePtr := add(calldataPointer, calldataload(add(calldataPointer, 0x20))) - sponsorSignature.offset := add(0x20, sponsorSignaturePtr) - sponsorSignature.length := calldataload(sponsorSignaturePtr) - - // Extract sponsor address, sanitizing upper 96 bits. + // Extract sponsor address from calldata, sanitizing upper 96 bits. sponsor := shr(0x60, shl(0x60, calldataload(add(calldataPointer, 0x40)))) - // Extract nonce and expiration timestamp. + // Extract nonce and expiration timestamp from calldata. nonce := calldataload(add(calldataPointer, 0x60)) expires := calldataload(add(calldataPointer, 0x80)) + + // Swap domain separator for provided sponsorDomainSeparator if a nonzero value was supplied. + sponsorDomainSeparator := add(sponsorDomainSeparator, mul(iszero(sponsorDomainSeparator), domainSeparator)) } // Ensure that the claim hasn't expired. @@ -94,21 +84,72 @@ library ClaimProcessorLib { // Retrieve allocator address and consume nonce, ensuring it has not already been consumed. address allocator = allocatorId.fromRegisteredAllocatorIdWithConsumed(nonce); + // Validate that the sponsor has authorized the claim. + _validateSponsor( + sponsor, messageHash, calldataPointer, sponsorDomainSeparator, typehash, idsAndAmounts, shortestResetPeriod + ); + + // Validate that the allocator has authorized the claim. + _validateAllocator(allocator, sponsor, messageHash, calldataPointer, idsAndAmounts, nonce, expires); + + // Emit claim event. + sponsor.emitClaim(messageHash, allocator, nonce); + } + + function _validateSponsor( + address sponsor, + bytes32 messageHash, + uint256 calldataPointer, + bytes32 sponsorDomainSeparator, + bytes32 typehash, + uint256[2][] memory idsAndAmounts, + uint256 shortestResetPeriod + ) internal view { + bytes calldata sponsorSignature; assembly ("memory-safe") { - // Swap domain separator for provided sponsorDomainSeparator if a nonzero value was supplied. - sponsorDomainSeparator := add(sponsorDomainSeparator, mul(iszero(sponsorDomainSeparator), domainSeparator)) + // Extract sponsor signature from calldata using offset stored at calldataPointer + 0x20. + let sponsorSignaturePtr := add(calldataPointer, calldataload(add(calldataPointer, 0x20))) + sponsorSignature.offset := add(0x20, sponsorSignaturePtr) + sponsorSignature.length := calldataload(sponsorSignaturePtr) } // Validate sponsor authorization through either ECDSA, direct registration, EIP1271, or emissary. messageHash.hasValidSponsorOrRegistration( sponsor, sponsorSignature, sponsorDomainSeparator, idsAndAmounts, typehash, shortestResetPeriod ); + } + function _validateAllocator( + address allocator, + address sponsor, + bytes32 messageHash, + bytes calldata allocatorData, + uint256[2][] memory idsAndAmounts, + uint256 nonce, + uint256 expires + ) internal { // Validate allocator authorization through the allocator interface. allocator.callAuthorizeClaim(messageHash, sponsor, nonce, expires, idsAndAmounts, allocatorData); + } - // Emit claim event. - sponsor.emitClaim(messageHash, allocator, nonce); + function _validateAllocator( + address allocator, + address sponsor, + bytes32 messageHash, + uint256 calldataPointer, + uint256[2][] memory idsAndAmounts, + uint256 nonce, + uint256 expires + ) internal { + // Extract allocator signature from calldata using offset stored at calldataPointer. + bytes calldata allocatorData; + assembly ("memory-safe") { + let allocatorDataPtr := add(calldataPointer, calldataload(calldataPointer)) + allocatorData.offset := add(0x20, allocatorDataPtr) + allocatorData.length := calldataload(allocatorDataPtr) + } + + _validateAllocator(allocator, sponsor, messageHash, allocatorData, idsAndAmounts, nonce, expires); } /** diff --git a/src/lib/ComponentLib.sol b/src/lib/ComponentLib.sol index a398025..67e70bc 100644 --- a/src/lib/ComponentLib.sol +++ b/src/lib/ComponentLib.sol @@ -211,7 +211,6 @@ library ComponentLib { ) internal { // Declare variable for SplitBatchClaimComponent array that will be extracted from calldata. SplitBatchClaimComponent[] calldata claims; - assembly ("memory-safe") { // Extract array of split batch claim components. let claimsPtr := add(calldataPointer, calldataload(add(calldataPointer, offsetToId))) @@ -219,6 +218,40 @@ library ComponentLib { claims.length := calldataload(claimsPtr) } + // Parse into idsAndAmounts & extract shortest reset period & first allocatorId. + (uint256[2][] memory idsAndAmounts, uint96 firstAllocatorId, uint256 shortestResetPeriod) = + _buildIdsAndAmounts(claims, sponsorDomainSeparator); + + // Validate the claim and extract the sponsor address. + address sponsor = validation( + messageHash, + firstAllocatorId, + calldataPointer, + domainSeparator, + sponsorDomainSeparator, + typehash, + idsAndAmounts, + shortestResetPeriod.asResetPeriod().toSeconds() + ); + + unchecked { + // Process each claim component. + for (uint256 i = 0; i < idsAndAmounts.length; ++i) { + SplitBatchClaimComponent calldata claimComponent = claims[i]; + + // Process each split component, verifying total amount and executing operations. + claimComponent.portions.verifyAndProcessSplitComponents( + sponsor, claimComponent.id, claimComponent.allocatedAmount + ); + } + } + } + + function _buildIdsAndAmounts(SplitBatchClaimComponent[] calldata claims, bytes32 sponsorDomainSeparator) + internal + pure + returns (uint256[2][] memory idsAndAmounts, uint96 firstAllocatorId, uint256 shortestResetPeriod) + { uint256 totalClaims = claims.length; if (totalClaims == 0) { revert NoIdsAndAmountsProvided(); @@ -227,11 +260,11 @@ library ComponentLib { // Extract allocator id and amount from first claim for validation. SplitBatchClaimComponent calldata claimComponent = claims[0]; uint256 id = claimComponent.id; - uint96 firstAllocatorId = id.toAllocatorId(); - uint256 shortestResetPeriod = id.toResetPeriod().asUint256(); + firstAllocatorId = id.toAllocatorId(); + shortestResetPeriod = id.toResetPeriod().asUint256(); // Initialize idsAndAmounts array and register the first element. - uint256[2][] memory idsAndAmounts = new uint256[2][](totalClaims); + idsAndAmounts = new uint256[2][](totalClaims); idsAndAmounts[0] = [id, claimComponent.allocatedAmount]; // Initialize error tracking variable. @@ -255,28 +288,6 @@ library ComponentLib { // Revert if any errors occurred. _revertWithInvalidBatchAllocationIfError(errorBuffer); - - // Validate the claim and extract the sponsor address. - address sponsor = validation( - messageHash, - firstAllocatorId, - calldataPointer, - domainSeparator, - sponsorDomainSeparator, - typehash, - idsAndAmounts, - shortestResetPeriod.asResetPeriod().toSeconds() - ); - - // Process each claim component. - for (uint256 i = 0; i < totalClaims; ++i) { - claimComponent = claims[i]; - - // Process each split component, verifying total amount and executing operations. - claimComponent.portions.verifyAndProcessSplitComponents( - sponsor, claimComponent.id, claimComponent.allocatedAmount - ); - } } } diff --git a/src/lib/DepositViaPermit2Logic.sol b/src/lib/DepositViaPermit2Logic.sol index ae8a817..2e65119 100644 --- a/src/lib/DepositViaPermit2Logic.sol +++ b/src/lib/DepositViaPermit2Logic.sol @@ -108,7 +108,6 @@ contract DepositViaPermit2Logic is DepositLogic { * @param token The address of the ERC20 token to deposit. * @param depositor The account signing the permit2 authorization and depositing the tokens. * @param claimHash A bytes32 hash derived from the details of the compact. - * @param compactCategory The category of the compact being registered (Compact, BatchCompact, or MultichainCompact). * @param witness Additional data used in generating the claim hash. * @param signature The Permit2 signature from the depositor authorizing the deposit. * @return The ERC6909 token identifier of the associated resource lock. @@ -117,29 +116,46 @@ contract DepositViaPermit2Logic is DepositLogic { address token, address depositor, // also recipient bytes32 claimHash, - CompactCategory compactCategory, string calldata witness, bytes calldata signature ) internal returns (uint256) { - // Set reentrancy lock, get initial balance, and begin preparing Permit2 call data. - (uint256 id, uint256 initialBalance, uint256 m, uint256 typestringMemoryLocation) = - _setReentrancyLockAndStartPreparingPermit2Call(token); + uint256 id; + uint256 initialBalance; + bytes32 compactTypehash; + { + uint256 m; + { + uint256 typestringMemoryLocation; + + // Set reentrancy lock, get initial balance, and begin preparing Permit2 call data. + (id, initialBalance, m, typestringMemoryLocation) = + _setReentrancyLockAndStartPreparingPermit2Call(token); + + CompactCategory compactCategory; + bytes32 activationTypehash; + assembly ("memory-safe") { + compactCategory := calldataload(0xe4) + if gt(compactCategory, 2) { revert(0, 0) } + } - // Continue preparing Permit2 call data and get activation and compact typehashes. - (bytes32 activationTypehash, bytes32 compactTypehash) = - typestringMemoryLocation.writeWitnessAndGetTypehashes(compactCategory, witness, bool(false).asStubborn()); + // Continue preparing Permit2 call data and get activation and compact typehashes. + (activationTypehash, compactTypehash) = typestringMemoryLocation.writeWitnessAndGetTypehashes( + compactCategory, witness, bool(false).asStubborn() + ); - // Derive the activation witness hash and store it. - activationTypehash.deriveAndWriteWitnessHash(id, claimHash, m, 0x100); + // Derive the activation witness hash and store it. + activationTypehash.deriveAndWriteWitnessHash(id, claimHash, m, 0x100); + } - // Derive signature offset value. - uint256 signatureOffsetValue; - assembly ("memory-safe") { - signatureOffsetValue := and(add(mload(add(m, 0x160)), 0x17f), not(0x1f)) - } + // Derive signature offset value. + uint256 signatureOffsetValue; + assembly ("memory-safe") { + signatureOffsetValue := and(add(mload(add(m, 0x160)), 0x17f), not(0x1f)) + } - // Write the signature and perform the Permit2 call. - _writeSignatureAndPerformPermit2Call(m, uint256(0x140).asStubborn(), signatureOffsetValue, signature); + // Write the signature and perform the Permit2 call. + _writeSignatureAndPerformPermit2Call(m, uint256(0x140).asStubborn(), signatureOffsetValue, signature); + } // Deposit tokens based on the balance change from the Permit2 call. _checkBalanceAndDeposit(token, depositor, id, initialBalance); @@ -183,28 +199,30 @@ contract DepositViaPermit2Logic is DepositLogic { uint256[] memory initialTokenBalances ) = _preprocessAndPerformInitialNativeDeposit(permitted, recipient); - // Derive the CompactDeposit witness hash. - bytes32 witness = uint256(0x84).asStubborn().deriveCompactDepositWitnessHash(); + { + // Derive the CompactDeposit witness hash. + bytes32 witness = uint256(0x84).asStubborn().deriveCompactDepositWitnessHash(); - // Begin preparing Permit2 call data. - (uint256 m, uint256 typestringMemoryLocation) = - totalTokensLessInitialNative.beginPreparingBatchDepositPermit2Calldata(firstUnderlyingTokenIsNative); + // Begin preparing Permit2 call data. + (uint256 m, uint256 typestringMemoryLocation) = + totalTokensLessInitialNative.beginPreparingBatchDepositPermit2Calldata(firstUnderlyingTokenIsNative); - // Insert the CompactDeposit typestring fragment. - typestringMemoryLocation.insertCompactDepositTypestring(); + // Insert the CompactDeposit typestring fragment. + typestringMemoryLocation.insertCompactDepositTypestring(); - // Declare variable for signature offset value. - uint256 signatureOffsetValue; - assembly ("memory-safe") { - // Store the CompactDeposit witness hash. - mstore(add(m, 0x80), witness) + // Declare variable for signature offset value. + uint256 signatureOffsetValue; + assembly ("memory-safe") { + // Store the CompactDeposit witness hash. + mstore(add(m, 0x80), witness) - // Derive signature offset value. - signatureOffsetValue := add(0x200, shl(7, totalTokensLessInitialNative)) - } + // Derive signature offset value. + signatureOffsetValue := add(0x200, shl(7, totalTokensLessInitialNative)) + } - // Write the signature and perform the Permit2 call. - _writeSignatureAndPerformPermit2Call(m, uint256(0xc0).asStubborn(), signatureOffsetValue, signature); + // Write the signature and perform the Permit2 call. + _writeSignatureAndPerformPermit2Call(m, uint256(0xc0).asStubborn(), signatureOffsetValue, signature); + } // Deposit tokens based on balance changes from Permit2 call and clear reentrancy lock. _verifyBalancesAndPerformDeposits(ids, permitted, initialTokenBalances, recipient, firstUnderlyingTokenIsNative); @@ -226,74 +244,97 @@ contract DepositViaPermit2Logic is DepositLogic { * Compact, BatchCompact, or MultichainCompact payload matching the specified compact category. * @param depositor The account signing the permit2 authorization and depositing the tokens. * @param permitted Array of token permissions specifying the deposited tokens and amounts. - * @param claimHash A bytes32 hash derived from the details of the compact. - * @param compactCategory The category of the compact being registered (Compact, BatchCompact, or MultichainCompact). * @param witness Additional data used in generating the claim hash. * @param signature The Permit2 signature from the depositor authorizing the deposits. - * @return Array of ERC6909 token identifiers for the associated resource locks. + * @return ids Array of ERC6909 token identifiers for the associated resource locks. */ function _depositBatchAndRegisterViaPermit2( address depositor, ISignatureTransfer.TokenPermissions[] calldata permitted, - bytes32 claimHash, - CompactCategory compactCategory, string calldata witness, bytes calldata signature - ) internal returns (uint256[] memory) { - // Set reentrancy guard, perform initial native deposit if present, and get initial token balances. - ( - uint256 totalTokensLessInitialNative, - bool firstUnderlyingTokenIsNative, - uint256[] memory ids, - uint256[] memory initialTokenBalances - ) = _preprocessAndPerformInitialNativeDeposit(permitted, depositor); - - // Derive the hash of the resource lock ids. - uint256 idsHash; - assembly ("memory-safe") { - idsHash := - keccak256(add(ids, 0x20), shl(5, add(totalTokensLessInitialNative, firstUnderlyingTokenIsNative))) - } - - // Begin preparing Permit2 call data. - (uint256 m, uint256 typestringMemoryLocation) = - totalTokensLessInitialNative.beginPreparingBatchDepositPermit2Calldata(firstUnderlyingTokenIsNative); - - // Prepare the typestring fragment and get batch activation and compact typehashes. - (bytes32 activationTypehash, bytes32 compactTypehash) = - typestringMemoryLocation.writeWitnessAndGetTypehashes(compactCategory, witness, bool(true).asStubborn()); - - // Derive the batch activation witness hash and store it. - activationTypehash.deriveAndWriteWitnessHash(idsHash, claimHash, m, 0x80); - - // Declare variable for signature offset value. - uint256 signatureOffsetValue; - assembly ("memory-safe") { - // Get the length of the witness. - let witnessLength := witness.length + ) internal returns (uint256[] memory ids) { + bool firstUnderlyingTokenIsNative; + uint256[] memory initialTokenBalances; + bytes32 compactTypehash; + + { + uint256 totalTokensLessInitialNative; + + // Set reentrancy guard, perform initial native deposit if present, and get initial token balances. + (totalTokensLessInitialNative, firstUnderlyingTokenIsNative, ids, initialTokenBalances) = + _preprocessAndPerformInitialNativeDeposit(permitted, depositor); + + { + uint256 m; + CompactCategory compactCategory; + { + // Derive the hash of the resource lock ids. + uint256 idsHash; + bytes32 activationTypehash; + uint256 typestringMemoryLocation; + + assembly ("memory-safe") { + idsHash := + keccak256( + add(ids, 0x20), shl(5, add(totalTokensLessInitialNative, firstUnderlyingTokenIsNative)) + ) + + compactCategory := calldataload(0xc4) + if gt(compactCategory, 2) { revert(0, 0) } + } + + // Begin preparing Permit2 call data. + (m, typestringMemoryLocation) = totalTokensLessInitialNative + .beginPreparingBatchDepositPermit2Calldata(firstUnderlyingTokenIsNative); + + // Prepare the typestring fragment and get batch activation and compact typehashes. + (activationTypehash, compactTypehash) = typestringMemoryLocation.writeWitnessAndGetTypehashes( + compactCategory, witness, bool(true).asStubborn() + ); + + bytes32 claimHash; + assembly { + claimHash := calldataload(0xa4) + } + + // Derive the batch activation witness hash and store it. + activationTypehash.deriveAndWriteWitnessHash(idsHash, claimHash, m, 0x80); + } - // Derive the total memory offset for the witness. - let totalWitnessMemoryOffset := - and( - add( - add(0xf3, add(witnessLength, iszero(iszero(witnessLength)))), - add(mul(eq(compactCategory, 1), 0x0b), shl(6, eq(compactCategory, 2))) - ), - not(0x1f) - ) + // Declare variable for signature offset value. + uint256 signatureOffsetValue; + assembly ("memory-safe") { + // Derive the total memory offset for the witness. + let totalWitnessMemoryOffset := + and( + add( + add(0xf3, add(witness.length, iszero(iszero(witness.length)))), + add(mul(eq(compactCategory, 1), 0x0b), shl(6, eq(compactCategory, 2))) + ), + not(0x1f) + ) + + // Derive the signature offset value. + signatureOffsetValue := + add(add(0x180, shl(7, totalTokensLessInitialNative)), totalWitnessMemoryOffset) + } - // Derive the signature offset value. - signatureOffsetValue := add(add(0x180, shl(7, totalTokensLessInitialNative)), totalWitnessMemoryOffset) + // Write the signature and perform the Permit2 call. + _writeSignatureAndPerformPermit2Call(m, uint256(0xc0).asStubborn(), signatureOffsetValue, signature); + } } - // Write the signature and perform the Permit2 call. - _writeSignatureAndPerformPermit2Call(m, uint256(0xc0).asStubborn(), signatureOffsetValue, signature); - // Deposit tokens based on balance changes from Permit2 call and clear reentrancy lock. _verifyBalancesAndPerformDeposits(ids, permitted, initialTokenBalances, depositor, firstUnderlyingTokenIsNative); + bytes32 registeredClaimHash; + assembly { + registeredClaimHash := calldataload(0xa4) + } + // Register the compact. - depositor.registerCompact(claimHash, compactTypehash); + depositor.registerCompact(registeredClaimHash, compactTypehash); // Return the ERC6909 token identifiers of the associated resource locks. return ids; diff --git a/src/lib/HashLib.sol b/src/lib/HashLib.sol index 93488c8..7d300ae 100644 --- a/src/lib/HashLib.sol +++ b/src/lib/HashLib.sol @@ -274,15 +274,10 @@ library HashLib { * @notice Internal view function for deriving the EIP-712 message hash for * a claim with a witness. * @param claim Pointer to the claim location in calldata. - * @param qualificationOffset Additional offset from claim pointer to ID from most compact case. * @return messageHash The EIP-712 compliant message hash. * @return typehash The EIP-712 typehash. */ - function toMessageHashWithWitness(uint256 claim, uint256 qualificationOffset) - internal - view - returns (bytes32 messageHash, bytes32 typehash) - { + function toMessageHashWithWitness(uint256 claim) internal view returns (bytes32 messageHash, bytes32 typehash) { assembly ("memory-safe") { // Retrieve the free memory pointer; memory will be left dirtied. let m := mload(0x40) @@ -312,8 +307,8 @@ library HashLib { calldatacopy(add(m, 0x40), add(claim, 0x40), 0x60) // Prepare final components of message data: id, amount, & witness. - mstore(add(m, 0xa0), calldataload(add(claim, add(0xe0, qualificationOffset)))) // id - mstore(add(m, 0xc0), calldataload(add(claim, add(0x100, qualificationOffset)))) // amount + mstore(add(m, 0xa0), calldataload(add(claim, 0xe0))) // id + mstore(add(m, 0xc0), calldataload(add(claim, 0x100))) // amount mstore(add(m, 0xe0), calldataload(add(claim, 0xa0))) // witness // Derive the message hash from the prepared data. @@ -519,14 +514,17 @@ library HashLib { mstore(add(m, 0x20), caller()) // arbiter mstore(add(m, 0x40), chainid()) - // Determine if the segment typestring has a witness. - let hasWitness := iszero(eq(segmentTypehash, SEGMENT_TYPEHASH)) + let segmentHash + { + // Determine if the segment typestring has a witness. + let hasWitness := iszero(eq(segmentTypehash, SEGMENT_TYPEHASH)) - // If the segment has a witness, store the witness in memory. - if hasWitness { mstore(add(m, 0x80), calldataload(add(claim, 0xa0))) } // witness + // If the segment has a witness, store the witness in memory. + if hasWitness { mstore(add(m, 0x80), calldataload(add(claim, 0xa0))) } // witness - // Derive the segment hash from the prepared data and write it to memory. - let segmentHash := keccak256(m, add(0x80, mul(0x20, hasWitness))) + // Derive the segment hash from the prepared data and write it to memory. + segmentHash := keccak256(m, add(0x80, mul(0x20, hasWitness))) + } // Derive the pointer to the additional chains and retrieve the length. let claimWithAdditionalOffset := add(claim, additionalOffset) diff --git a/src/lib/MetadataLib.sol b/src/lib/MetadataLib.sol index 7f90233..9c2e5e5 100644 --- a/src/lib/MetadataLib.sol +++ b/src/lib/MetadataLib.sol @@ -59,38 +59,45 @@ library MetadataLib { } function toURI(Lock memory lock, uint256 id) internal view returns (string memory uri) { - string memory tokenAddress = lock.token.isNullAddress() ? "Native Token" : lock.token.toHexStringChecksummed(); - string memory allocator = lock.allocator.toHexStringChecksummed(); - string memory resetPeriod = lock.resetPeriod.toString(); - string memory scope = lock.scope.toString(); - string memory tokenName = lock.token.readNameWithDefaultValue(); - string memory tokenSymbol = lock.token.readSymbolWithDefaultValue(); - string memory tokenDecimals = uint256(lock.token.readDecimals()).toString(); + string memory attributes; + string memory description; + string memory name; + { + string memory tokenAddress = + lock.token.isNullAddress() ? "Native Token" : lock.token.toHexStringChecksummed(); + string memory tokenSymbol = lock.token.readSymbolWithDefaultValue(); + string memory allocator = lock.allocator.toHexStringChecksummed(); + string memory resetPeriod = lock.resetPeriod.toString(); + string memory scope = lock.scope.toString(); + string memory tokenName = lock.token.readNameWithDefaultValue(); + string memory tokenDecimals = uint256(lock.token.readDecimals()).toString(); + attributes = string.concat( + "\"attributes\": [", + toAttributeString("ID", id.toString(), false), + toAttributeString("Token Address", tokenAddress, false), + toAttributeString("Token Name", tokenName, false), + toAttributeString("Token Symbol", tokenSymbol, false), + toAttributeString("Token Decimals", tokenDecimals, false), + toAttributeString("Allocator", allocator, false), + toAttributeString("Scope", scope, false), + toAttributeString("Reset Period", resetPeriod, true), + "]}" + ); - string memory name = string.concat("{\"name\": \"Compact ", tokenSymbol, "\","); - string memory description = string.concat( - "\"description\": \"Compact ", - tokenName, - " (", - tokenAddress, - ") resource lock with allocator ", - allocator, - " and reset period of ", - resetPeriod, - "\"," - ); - string memory attributes = string.concat( - "\"attributes\": [", - toAttributeString("ID", id.toString(), false), - toAttributeString("Token Address", tokenAddress, false), - toAttributeString("Token Name", tokenName, false), - toAttributeString("Token Symbol", tokenSymbol, false), - toAttributeString("Token Decimals", tokenDecimals, false), - toAttributeString("Allocator", allocator, false), - toAttributeString("Scope", scope, false), - toAttributeString("Reset Period", resetPeriod, true), - "]}" - ); + description = string.concat( + "\"description\": \"Compact ", + tokenName, + " (", + tokenAddress, + ") resource lock with allocator ", + allocator, + " and reset period of ", + resetPeriod, + "\"," + ); + + name = string.concat("{\"name\": \"Compact ", tokenSymbol, "\","); + } // Note: this just returns a default image; replace with a dynamic image based on attributes string memory image = diff --git a/src/types/DepositDetails.sol b/src/types/DepositDetails.sol new file mode 100644 index 0000000..3de1f10 --- /dev/null +++ b/src/types/DepositDetails.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +struct DepositDetails { + uint256 nonce; + uint256 deadline; + bytes12 lockTag; +} diff --git a/test/TheCompact.t.sol b/test/TheCompact.t.sol index aef5872..fc51ab6 100644 --- a/test/TheCompact.t.sol +++ b/test/TheCompact.t.sol @@ -8,6 +8,7 @@ import { Compact, BatchCompact, Segment } from "../src/types/EIP712Types.sol"; import { ResetPeriod } from "../src/types/ResetPeriod.sol"; import { Scope } from "../src/types/Scope.sol"; import { CompactCategory } from "../src/types/CompactCategory.sol"; +import { DepositDetails } from "../src/types/DepositDetails.sol"; import { ISignatureTransfer } from "permit2/src/interfaces/ISignatureTransfer.sol"; import { HashLib } from "../src/lib/HashLib.sol"; @@ -43,6 +44,64 @@ interface ImmutableCreate2Factory { returns (address deploymentAddress); } +struct CreateClaimHashWithWitnessArgs { + bytes32 typehash; + address arbiter; + address sponsor; + uint256 nonce; + uint256 expires; + uint256 id; + uint256 amount; + bytes32 witness; +} + +struct CreateBatchClaimHashWithWitnessArgs { + bytes32 typehash; + address arbiter; + address sponsor; + uint256 nonce; + uint256 expires; + bytes32 idsAndAmountsHash; + bytes32 witness; +} + +struct CreatePermitBatchWitnessDigestArgs { + bytes32 domainSeparator; + bytes32 tokenPermissionsHash; + address spender; + uint256 nonce; + uint256 deadline; + bytes32 activationTypehash; + bytes32 idsHash; + bytes32 claimHash; +} + +struct SetupPermitCallExpectationArgs { + bytes32 activationTypehash; + uint256[] ids; + bytes32 claimHash; + uint256 nonce; + uint256 deadline; + bytes signature; +} + +struct TestParams { + address recipient; + ResetPeriod resetPeriod; + Scope scope; + uint256 amount; + uint256 nonce; + uint256 deadline; +} + +struct LockDetails { + address token; + address allocator; + ResetPeriod resetPeriod; + Scope scope; + bytes12 lockTag; +} + contract TheCompactTest is Test { using IdLib for uint96; @@ -59,6 +118,14 @@ contract TheCompactTest is Test { bytes32 permit2EIP712DomainHash = keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); address alwaysOKAllocator; + string constant compactTypestring = + "Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount)"; + string constant compactWitnessTypestring = + "Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,CompactWitness witness)CompactWitness(uint256 witnessArgument)"; + string constant witnessTypestring = "CompactWitness witness)CompactWitness(uint256 witnessArgument)"; + bytes32 constant compactTypehash = keccak256(bytes(compactTypestring)); + bytes32 constant compactWithWitnessTypehash = keccak256(bytes(compactWitnessTypestring)); + function setUp() public virtual { address permit2Deployer = address(0x4e59b44847b379578588920cA78FbF26c0B4956C); address deployedPermit2Deployer; @@ -337,13 +404,15 @@ contract TheCompactTest is Test { | uint256(uint160(address(0))) ); - uint256[2][] memory idsAndAmounts = new uint256[2][](1); - idsAndAmounts[0] = [id, amount]; + { + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0] = [id, amount]; - vm.prank(swapper); - bool ok = theCompact.deposit{ value: amount }(idsAndAmounts, recipient); - vm.snapshotGasLastCall("depositBatchSingleNative"); - assert(ok); + vm.prank(swapper); + bool ok = theCompact.deposit{ value: amount }(idsAndAmounts, recipient); + vm.snapshotGasLastCall("depositBatchSingleNative"); + assert(ok); + } ( address derivedToken, @@ -386,13 +455,15 @@ contract TheCompactTest is Test { | uint256(uint160(address(token))) ); - uint256[2][] memory idsAndAmounts = new uint256[2][](1); - idsAndAmounts[0] = [id, amount]; + { + uint256[2][] memory idsAndAmounts = new uint256[2][](1); + idsAndAmounts[0] = [id, amount]; - vm.prank(swapper); - bool ok = theCompact.deposit(idsAndAmounts, recipient); - vm.snapshotGasLastCall("depositBatchSingleERC20"); - assert(ok); + vm.prank(swapper); + bool ok = theCompact.deposit(idsAndAmounts, recipient); + vm.snapshotGasLastCall("depositBatchSingleERC20"); + assert(ok); + } ( address derivedToken, @@ -421,26 +492,16 @@ contract TheCompactTest is Test { assert(bytes(theCompact.tokenURI(id)).length > 0); } - function test_depositERC20ViaPermit2AndURI() public virtual { - address recipient = 0x1111111111111111111111111111111111111111; - ResetPeriod resetPeriod = ResetPeriod.TenMinutes; - Scope scope = Scope.Multichain; - uint256 amount = 1e18; - uint256 nonce = 0; - uint256 deadline = block.timestamp + 1000; - - vm.prank(allocator); - uint96 allocatorId = theCompact.__registerAllocator(allocator, ""); - - bytes12 lockTag = - bytes12(bytes32((uint256(scope) << 255) | (uint256(resetPeriod) << 252) | (uint256(allocatorId) << 160))); - - bytes32 domainSeparator = - keccak256(abi.encode(permit2EIP712DomainHash, keccak256(bytes("Permit2")), block.chainid, address(permit2))); - - assertEq(domainSeparator, EIP712(permit2).DOMAIN_SEPARATOR()); - - bytes32 digest = keccak256( + function _createPermitWitnessDigest( + bytes32 domainSeparator, + address permitToken, + uint256 amount, + address spender, + uint256 nonce, + uint256 deadline, + bytes32 witnessHash + ) internal pure returns (bytes32) { + return keccak256( abi.encodePacked( bytes2(0x1901), domainSeparator, @@ -450,364 +511,493 @@ contract TheCompactTest is Test { "PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline,CompactDeposit witness)CompactDeposit(bytes12 lockTag,address recipient)TokenPermissions(address token,uint256 amount)" ), keccak256( - abi.encode( - keccak256("TokenPermissions(address token,uint256 amount)"), address(token), amount - ) + abi.encode(keccak256("TokenPermissions(address token,uint256 amount)"), permitToken, amount) ), - address(theCompact), // spender + spender, nonce, deadline, - keccak256( - abi.encode( - keccak256("CompactDeposit(bytes12 lockTag,address recipient)"), lockTag, recipient - ) - ) + witnessHash ) ) ) ); + } + + function test_depositERC20ViaPermit2AndURI() public virtual { + // Setup test variables + TestParams memory params; + params.recipient = 0x1111111111111111111111111111111111111111; + params.resetPeriod = ResetPeriod.TenMinutes; + params.scope = Scope.Multichain; + params.amount = 1e18; + params.nonce = 0; + params.deadline = block.timestamp + 1000; + + // Register allocator and create lock tag + uint96 allocatorId; + bytes12 lockTag; + { + vm.prank(allocator); + allocatorId = theCompact.__registerAllocator(allocator, ""); - (bytes32 r, bytes32 vs) = vm.signCompact(swapperPrivateKey, digest); - bytes memory signature = abi.encodePacked(r, vs); + lockTag = bytes12( + bytes32( + (uint256(params.scope) << 255) | (uint256(params.resetPeriod) << 252) + | (uint256(allocatorId) << 160) + ) + ); + } - vm.expectCall( - address(permit2), - abi.encodeWithSignature( - "permitWitnessTransferFrom(((address,uint256),uint256,uint256),(address,uint256),address,bytes32,string,bytes)", - ISignatureTransfer.PermitTransferFrom({ - permitted: ISignatureTransfer.TokenPermissions({ token: address(token), amount: amount }), - nonce: nonce, - deadline: deadline - }), - ISignatureTransfer.SignatureTransferDetails({ to: address(theCompact), requestedAmount: amount }), - swapper, - keccak256( - abi.encode(keccak256("CompactDeposit(bytes12 lockTag,address recipient)"), lockTag, recipient) - ), - "CompactDeposit witness)CompactDeposit(bytes12 lockTag,address recipient)TokenPermissions(address token,uint256 amount)", - signature - ) - ); + // Create domain separator and digest + bytes32 domainSeparator; + bytes32 digest; + { + domainSeparator = keccak256( + abi.encode(permit2EIP712DomainHash, keccak256(bytes("Permit2")), block.chainid, address(permit2)) + ); + + assertEq(domainSeparator, EIP712(permit2).DOMAIN_SEPARATOR()); - uint256 id = theCompact.deposit(address(token), amount, nonce, deadline, swapper, lockTag, recipient, signature); - vm.snapshotGasLastCall("depositERC20ViaPermit2AndURI"); + bytes32 witnessHash = keccak256( + abi.encode(keccak256("CompactDeposit(bytes12 lockTag,address recipient)"), lockTag, params.recipient) + ); + + digest = _createPermitWitnessDigest( + domainSeparator, + address(token), + params.amount, + address(theCompact), + params.nonce, + params.deadline, + witnessHash + ); + } + + // Create signature and permit + bytes memory signature; + ISignatureTransfer.PermitTransferFrom memory permit; + { + (bytes32 r, bytes32 vs) = vm.signCompact(swapperPrivateKey, digest); + signature = abi.encodePacked(r, vs); + + permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({ token: address(token), amount: params.amount }), + nonce: params.nonce, + deadline: params.deadline + }); + } + + // Setup expectations and make deposit + uint256 id; + { + bytes32 witnessHash = keccak256( + abi.encode(keccak256("CompactDeposit(bytes12 lockTag,address recipient)"), lockTag, params.recipient) + ); + + vm.expectCall( + address(permit2), + abi.encodeWithSignature( + "permitWitnessTransferFrom(((address,uint256),uint256,uint256),(address,uint256),address,bytes32,string,bytes)", + permit, + ISignatureTransfer.SignatureTransferDetails({ + to: address(theCompact), + requestedAmount: params.amount + }), + swapper, + witnessHash, + "CompactDeposit witness)CompactDeposit(bytes12 lockTag,address recipient)TokenPermissions(address token,uint256 amount)", + signature + ) + ); + + id = theCompact.deposit(permit, swapper, lockTag, params.recipient, signature); + vm.snapshotGasLastCall("depositERC20ViaPermit2AndURI"); + } + + // Verify lock details + { + LockDetails memory lockDetails; + (lockDetails.token, lockDetails.allocator, lockDetails.resetPeriod, lockDetails.scope, lockDetails.lockTag) + = theCompact.getLockDetails(id); + + assertEq(lockDetails.token, address(token)); + assertEq(lockDetails.allocator, allocator); + assertEq(uint256(lockDetails.resetPeriod), uint256(params.resetPeriod)); + assertEq(uint256(lockDetails.scope), uint256(params.scope)); + assertEq( + id, + (uint256(params.scope) << 255) | (uint256(params.resetPeriod) << 252) | (uint256(allocatorId) << 160) + | uint256(uint160(address(token))) + ); + assertEq(lockDetails.lockTag, lockTag); + } + + // Verify balances and token URI + { + assertEq(token.balanceOf(address(theCompact)), params.amount); + assertEq(theCompact.balanceOf(params.recipient, id), params.amount); + assert(bytes(theCompact.tokenURI(id)).length > 0); + } + } + + function _verifyLockDetails( + uint256 id, + TestParams memory params, + LockDetails memory expectedDetails, + uint96 allocatorId + ) internal view { + LockDetails memory actualDetails; ( - address derivedToken, - address derivedAllocator, - ResetPeriod derivedResetPeriod, - Scope derivedScope, - bytes12 derivedLockTag + actualDetails.token, + actualDetails.allocator, + actualDetails.resetPeriod, + actualDetails.scope, + actualDetails.lockTag ) = theCompact.getLockDetails(id); - assertEq(derivedToken, address(token)); - assertEq(derivedAllocator, allocator); - assertEq(uint256(derivedResetPeriod), uint256(resetPeriod)); - assertEq(uint256(derivedScope), uint256(scope)); + + assertEq(actualDetails.token, expectedDetails.token); + assertEq(actualDetails.allocator, expectedDetails.allocator); + assertEq(uint256(actualDetails.resetPeriod), uint256(params.resetPeriod)); + assertEq(uint256(actualDetails.scope), uint256(params.scope)); assertEq( id, - (uint256(scope) << 255) | (uint256(resetPeriod) << 252) | (uint256(allocatorId) << 160) - | uint256(uint160(address(token))) + (uint256(params.scope) << 255) | (uint256(params.resetPeriod) << 252) | (uint256(allocatorId) << 160) + | uint256(uint160(expectedDetails.token)) ); - assertEq(derivedLockTag, lockTag); - - assertEq(token.balanceOf(address(theCompact)), amount); - assertEq(theCompact.balanceOf(recipient, id), amount); - assert(bytes(theCompact.tokenURI(id)).length > 0); + assertEq(actualDetails.lockTag, expectedDetails.lockTag); } function test_depositBatchViaPermit2SingleERC20() public virtual { - address recipient = 0x1111111111111111111111111111111111111111; - ResetPeriod resetPeriod = ResetPeriod.TenMinutes; - Scope scope = Scope.Multichain; - uint256 amount = 1e18; - uint256 nonce = 0; - uint256 deadline = block.timestamp + 1000; + // Setup test variables + TestParams memory params; + params.recipient = 0x1111111111111111111111111111111111111111; + params.resetPeriod = ResetPeriod.TenMinutes; + params.scope = Scope.Multichain; + params.amount = 1e18; + params.nonce = 0; + params.deadline = block.timestamp + 1000; + + // Register allocator and create lock tag + uint96 allocatorId; + bytes12 lockTag; + { + (allocatorId, lockTag) = _registerAllocator(allocator); + } - vm.prank(allocator); - uint96 allocatorId = theCompact.__registerAllocator(allocator, ""); + // Create domain separator + bytes32 domainSeparator; + { + domainSeparator = keccak256( + abi.encode(permit2EIP712DomainHash, keccak256(bytes("Permit2")), block.chainid, address(permit2)) + ); - bytes12 lockTag = - bytes12(bytes32((uint256(scope) << 255) | (uint256(resetPeriod) << 252) | (uint256(allocatorId) << 160))); + assertEq(domainSeparator, EIP712(permit2).DOMAIN_SEPARATOR()); + } - bytes32 domainSeparator = - keccak256(abi.encode(permit2EIP712DomainHash, keccak256(bytes("Permit2")), block.chainid, address(permit2))); + // Prepare tokens and amounts arrays + address[] memory tokens; + uint256[] memory amounts; + { + tokens = new address[](1); + amounts = new uint256[](1); + tokens[0] = address(token); + amounts[0] = params.amount; + } - assertEq(domainSeparator, EIP712(permit2).DOMAIN_SEPARATOR()); + // Create signature and token permissions + bytes memory signature; + ISignatureTransfer.PermitBatchTransferFrom memory permit; + ISignatureTransfer.SignatureTransferDetails[] memory signatureTransferDetails; - bytes32 digest = keccak256( - abi.encodePacked( - bytes2(0x1901), - domainSeparator, - keccak256( - abi.encode( - keccak256( - "PermitBatchWitnessTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline,CompactDeposit witness)CompactDeposit(bytes12 lockTag,address recipient)TokenPermissions(address token,uint256 amount)" - ), - keccak256( - abi.encode( - keccak256( - abi.encode( - keccak256("TokenPermissions(address token,uint256 amount)"), - address(token), - amount - ) - ) - ) - ), - address(theCompact), // spender - nonce, - deadline, - keccak256( - abi.encode( - keccak256("CompactDeposit(bytes12 lockTag,address recipient)"), lockTag, recipient - ) - ) - ) - ) - ) - ); + { + ISignatureTransfer.TokenPermissions[] memory tokenPermissions; + { + (signature, tokenPermissions) = _createPermit2BatchSignature( + tokens, amounts, params.nonce, params.deadline, lockTag, params.recipient, swapperPrivateKey + ); + } + + // Create permit and transfer details + { + signatureTransferDetails = new ISignatureTransfer.SignatureTransferDetails[](1); + signatureTransferDetails[0] = ISignatureTransfer.SignatureTransferDetails({ + to: address(theCompact), + requestedAmount: params.amount + }); + + permit = ISignatureTransfer.PermitBatchTransferFrom({ + permitted: tokenPermissions, + nonce: params.nonce, + deadline: params.deadline + }); + } + } - (bytes32 r, bytes32 vs) = vm.signCompact(swapperPrivateKey, digest); - bytes memory signature = abi.encodePacked(r, vs); + uint256[] memory ids; - ISignatureTransfer.TokenPermissions[] memory tokenPermissions = new ISignatureTransfer.TokenPermissions[](1); - tokenPermissions[0] = ISignatureTransfer.TokenPermissions({ token: address(token), amount: amount }); + { + // Prepare deposit details and expectations + DepositDetails memory details; - ISignatureTransfer.SignatureTransferDetails[] memory signatureTransferDetails = - new ISignatureTransfer.SignatureTransferDetails[](1); - signatureTransferDetails[0] = - ISignatureTransfer.SignatureTransferDetails({ to: address(theCompact), requestedAmount: amount }); + { + details = DepositDetails({ nonce: params.nonce, deadline: params.deadline, lockTag: lockTag }); - vm.expectCall( - address(permit2), - abi.encodeWithSignature( - "permitWitnessTransferFrom(((address,uint256)[],uint256,uint256),(address,uint256)[],address,bytes32,string,bytes)", - ISignatureTransfer.PermitBatchTransferFrom({ - permitted: tokenPermissions, - nonce: nonce, - deadline: deadline - }), - signatureTransferDetails, - swapper, - keccak256( - abi.encode(keccak256("CompactDeposit(bytes12 lockTag,address recipient)"), lockTag, recipient) - ), - "CompactDeposit witness)CompactDeposit(bytes12 lockTag,address recipient)TokenPermissions(address token,uint256 amount)", - signature - ) - ); + bytes32 witnessHash = keccak256( + abi.encode( + keccak256("CompactDeposit(bytes12 lockTag,address recipient)"), lockTag, params.recipient + ) + ); + + vm.expectCall( + address(permit2), + abi.encodeWithSignature( + "permitWitnessTransferFrom(((address,uint256)[],uint256,uint256),(address,uint256)[],address,bytes32,string,bytes)", + permit, + signatureTransferDetails, + swapper, + witnessHash, + "CompactDeposit witness)CompactDeposit(bytes12 lockTag,address recipient)TokenPermissions(address token,uint256 amount)", + signature + ) + ); + } - uint256[] memory ids = - theCompact.deposit(swapper, tokenPermissions, nonce, deadline, lockTag, recipient, signature); - vm.snapshotGasLastCall("depositBatchViaPermit2SingleERC20"); + // Make deposit + { + ids = theCompact.deposit(swapper, permit.permitted, details, params.recipient, signature); + vm.snapshotGasLastCall("depositBatchViaPermit2SingleERC20"); - assertEq(ids.length, 1); + assertEq(ids.length, 1); + } + } - ( - address derivedToken, - address derivedAllocator, - ResetPeriod derivedResetPeriod, - Scope derivedScope, - bytes12 derivedLockTag - ) = theCompact.getLockDetails(ids[0]); - assertEq(derivedToken, address(token)); - assertEq(derivedAllocator, allocator); - assertEq(uint256(derivedResetPeriod), uint256(resetPeriod)); - assertEq(uint256(derivedScope), uint256(scope)); - assertEq( - ids[0], - (uint256(scope) << 255) | (uint256(resetPeriod) << 252) | (uint256(allocatorId) << 160) - | uint256(uint160(address(token))) - ); - assertEq(derivedLockTag, lockTag); + // Verify lock details + { + LockDetails memory expectedDetails; + expectedDetails.token = address(token); + expectedDetails.allocator = allocator; + expectedDetails.resetPeriod = params.resetPeriod; + expectedDetails.scope = params.scope; + expectedDetails.lockTag = lockTag; + + _verifyLockDetails(ids[0], params, expectedDetails, allocatorId); + } - assertEq(token.balanceOf(address(theCompact)), amount); - assertEq(theCompact.balanceOf(recipient, ids[0]), amount); - assert(bytes(theCompact.tokenURI(ids[0])).length > 0); + // Verify balances and token URI + { + assertEq(token.balanceOf(address(theCompact)), params.amount); + assertEq(theCompact.balanceOf(params.recipient, ids[0]), params.amount); + assert(bytes(theCompact.tokenURI(ids[0])).length > 0); + } } function test_depositBatchViaPermit2NativeAndERC20() public virtual { - address recipient = 0x1111111111111111111111111111111111111111; - ResetPeriod resetPeriod = ResetPeriod.TenMinutes; - Scope scope = Scope.Multichain; - uint256 amount = 1e18; - uint256 nonce = 0; - uint256 deadline = block.timestamp + 1000; + // Setup test variables + TestParams memory params; + params.recipient = 0x1111111111111111111111111111111111111111; + params.resetPeriod = ResetPeriod.TenMinutes; + params.scope = Scope.Multichain; + params.amount = 1e18; + params.nonce = 0; + params.deadline = block.timestamp + 1000; + + // Register allocator and create lock tag + uint96 allocatorId; + bytes12 lockTag; + { + (allocatorId, lockTag) = _registerAllocator(allocator); + } - vm.prank(allocator); - uint96 allocatorId = theCompact.__registerAllocator(allocator, ""); + // Create domain separator + bytes32 domainSeparator; + { + domainSeparator = keccak256( + abi.encode(permit2EIP712DomainHash, keccak256(bytes("Permit2")), block.chainid, address(permit2)) + ); - bytes12 lockTag = - bytes12(bytes32((uint256(scope) << 255) | (uint256(resetPeriod) << 252) | (uint256(allocatorId) << 160))); + assertEq(domainSeparator, EIP712(permit2).DOMAIN_SEPARATOR()); + } - bytes32 domainSeparator = - keccak256(abi.encode(permit2EIP712DomainHash, keccak256(bytes("Permit2")), block.chainid, address(permit2))); + // Prepare tokens and amounts arrays + address[] memory tokens; + uint256[] memory amounts; + { + tokens = new address[](2); + amounts = new uint256[](2); + tokens[0] = address(0); + amounts[0] = params.amount; + tokens[1] = address(token); + amounts[1] = params.amount; + } - assertEq(domainSeparator, EIP712(permit2).DOMAIN_SEPARATOR()); + // Create signature and token permissions + bytes memory signature; + ISignatureTransfer.TokenPermissions[] memory tokenPermissions; + { + (signature, tokenPermissions) = _createPermit2BatchSignature( + tokens, amounts, params.nonce, params.deadline, lockTag, params.recipient, swapperPrivateKey + ); + } - bytes32 digest = keccak256( - abi.encodePacked( - bytes2(0x1901), - domainSeparator, - keccak256( - abi.encode( - keccak256( - "PermitBatchWitnessTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline,CompactDeposit witness)CompactDeposit(bytes12 lockTag,address recipient)TokenPermissions(address token,uint256 amount)" - ), - keccak256( - abi.encode( - keccak256( - abi.encode( - keccak256("TokenPermissions(address token,uint256 amount)"), - address(token), - amount - ) - ) - ) - ), - address(theCompact), // spender - nonce, - deadline, - keccak256( - abi.encode( - keccak256("CompactDeposit(bytes12 lockTag,address recipient)"), lockTag, recipient - ) + uint256[] memory ids; + + { + { + // Create permit and transfer details for the ERC20 token + ISignatureTransfer.SignatureTransferDetails[] memory signatureTransferDetails; + ISignatureTransfer.PermitBatchTransferFrom memory permitOnCall; + { + ISignatureTransfer.TokenPermissions[] memory tokenPermissionsOnCall; + { + tokenPermissionsOnCall = new ISignatureTransfer.TokenPermissions[](1); + tokenPermissionsOnCall[0] = + ISignatureTransfer.TokenPermissions({ token: address(token), amount: params.amount }); + } + + { + signatureTransferDetails = new ISignatureTransfer.SignatureTransferDetails[](1); + signatureTransferDetails[0] = ISignatureTransfer.SignatureTransferDetails({ + to: address(theCompact), + requestedAmount: params.amount + }); + } + + { + permitOnCall = ISignatureTransfer.PermitBatchTransferFrom({ + permitted: tokenPermissionsOnCall, + nonce: params.nonce, + deadline: params.deadline + }); + } + } + + bytes32 witnessHash; + { + witnessHash = keccak256( + abi.encode( + keccak256("CompactDeposit(bytes12 lockTag,address recipient)"), lockTag, params.recipient ) - ) - ) - ) - ); + ); + + vm.expectCall( + address(permit2), + abi.encodeWithSignature( + "permitWitnessTransferFrom(((address,uint256)[],uint256,uint256),(address,uint256)[],address,bytes32,string,bytes)", + permitOnCall, + signatureTransferDetails, + swapper, + witnessHash, + "CompactDeposit witness)CompactDeposit(bytes12 lockTag,address recipient)TokenPermissions(address token,uint256 amount)", + signature + ) + ); + } + } + + // Make deposit + { + DepositDetails memory details = + DepositDetails({ nonce: params.nonce, deadline: params.deadline, lockTag: lockTag }); + + ids = theCompact.deposit{ value: params.amount }( + swapper, tokenPermissions, details, params.recipient, signature + ); + vm.snapshotGasLastCall("depositBatchViaPermit2NativeAndERC20"); + + assertEq(ids.length, 2); + } + } - (bytes32 r, bytes32 vs) = vm.signCompact(swapperPrivateKey, digest); - bytes memory signature = abi.encodePacked(r, vs); + // Verify lock details for ETH (native token) + { + LockDetails memory expectedDetails; + expectedDetails.token = address(0); + expectedDetails.allocator = allocator; + expectedDetails.resetPeriod = params.resetPeriod; + expectedDetails.scope = params.scope; + expectedDetails.lockTag = lockTag; + + _verifyLockDetails(ids[0], params, expectedDetails, allocatorId); + } - ISignatureTransfer.TokenPermissions[] memory tokenPermissions = new ISignatureTransfer.TokenPermissions[](2); - tokenPermissions[0] = ISignatureTransfer.TokenPermissions({ token: address(0), amount: amount }); - tokenPermissions[1] = ISignatureTransfer.TokenPermissions({ token: address(token), amount: amount }); + // Verify id for ERC20 token + { + assertEq( + ids[1], + (uint256(params.scope) << 255) | (uint256(params.resetPeriod) << 252) | (uint256(allocatorId) << 160) + | uint256(uint160(address(token))) + ); + } - ISignatureTransfer.TokenPermissions[] memory tokenPermissionsOnCall = - new ISignatureTransfer.TokenPermissions[](1); - tokenPermissionsOnCall[0] = ISignatureTransfer.TokenPermissions({ token: address(token), amount: amount }); + // Verify balances and token URIs + { + assertEq(token.balanceOf(address(theCompact)), params.amount); + assertEq(address(theCompact).balance, params.amount); + assertEq(theCompact.balanceOf(params.recipient, ids[0]), params.amount); + assertEq(theCompact.balanceOf(params.recipient, ids[1]), params.amount); + assert(bytes(theCompact.tokenURI(ids[0])).length > 0); + } + } - ISignatureTransfer.SignatureTransferDetails[] memory signatureTransferDetails = - new ISignatureTransfer.SignatureTransferDetails[](1); - signatureTransferDetails[0] = - ISignatureTransfer.SignatureTransferDetails({ to: address(theCompact), requestedAmount: amount }); + function test_splitTransfer() public { + // Setup test parameters + TestParams memory params; + params.resetPeriod = ResetPeriod.TenMinutes; + params.scope = Scope.Multichain; + params.amount = 1e18; + params.nonce = 0; + params.deadline = block.timestamp + 1000; + + // Recipient information + address recipientOne = 0x1111111111111111111111111111111111111111; + address recipientTwo = 0x2222222222222222222222222222222222222222; + uint256 amountOne = 4e17; + uint256 amountTwo = 6e17; - vm.expectCall( - address(permit2), - abi.encodeWithSignature( - "permitWitnessTransferFrom(((address,uint256)[],uint256,uint256),(address,uint256)[],address,bytes32,string,bytes)", - ISignatureTransfer.PermitBatchTransferFrom({ - permitted: tokenPermissionsOnCall, - nonce: nonce, - deadline: deadline - }), - signatureTransferDetails, - swapper, - keccak256( - abi.encode(keccak256("CompactDeposit(bytes12 lockTag,address recipient)"), lockTag, recipient) - ), - "CompactDeposit witness)CompactDeposit(bytes12 lockTag,address recipient)TokenPermissions(address token,uint256 amount)", - signature - ) - ); - - uint256[] memory ids = theCompact.deposit{ value: amount }( - swapper, tokenPermissions, nonce, deadline, lockTag, recipient, signature - ); - vm.snapshotGasLastCall("depositBatchViaPermit2NativeAndERC20"); - - assertEq(ids.length, 2); - - ( - address derivedToken, - address derivedAllocator, - ResetPeriod derivedResetPeriod, - Scope derivedScope, - bytes12 deriviedLockTag - ) = theCompact.getLockDetails(ids[0]); - assertEq(derivedToken, address(0)); - assertEq(derivedAllocator, allocator); - assertEq(uint256(derivedResetPeriod), uint256(resetPeriod)); - assertEq(uint256(derivedScope), uint256(scope)); - assertEq( - ids[0], - (uint256(scope) << 255) | (uint256(resetPeriod) << 252) | (uint256(allocatorId) << 160) - | uint256(uint160(address(0))) - ); - assertEq( - ids[1], - (uint256(scope) << 255) | (uint256(resetPeriod) << 252) | (uint256(allocatorId) << 160) - | uint256(uint160(address(token))) - ); - assertEq(deriviedLockTag, lockTag); - - assertEq(token.balanceOf(address(theCompact)), amount); - assertEq(address(theCompact).balance, amount); - assertEq(theCompact.balanceOf(recipient, ids[0]), amount); - assertEq(theCompact.balanceOf(recipient, ids[1]), amount); - assert(bytes(theCompact.tokenURI(ids[0])).length > 0); - } - - function test_splitTransfer() public { - ResetPeriod resetPeriod = ResetPeriod.TenMinutes; - Scope scope = Scope.Multichain; - uint256 amount = 1e18; - uint256 nonce = 0; - uint256 expiration = block.timestamp + 1000; - address recipientOne = 0x1111111111111111111111111111111111111111; - address recipientTwo = 0x2222222222222222222222222222222222222222; - uint256 amountOne = 4e17; - uint256 amountTwo = 6e17; - - vm.prank(allocator); - uint96 allocatorId = theCompact.__registerAllocator(allocator, ""); - - bytes12 lockTag = - bytes12(bytes32((uint256(scope) << 255) | (uint256(resetPeriod) << 252) | (uint256(allocatorId) << 160))); + // Register allocator and create lock tag + uint256 id; + { + uint96 allocatorId; + bytes12 lockTag; + (allocatorId, lockTag) = _registerAllocator(allocator); - vm.prank(swapper); - uint256 id = theCompact.deposit(address(token), lockTag, amount, swapper); - assertEq(theCompact.balanceOf(swapper, id), amount); + // Make deposit + id = _makeDeposit(swapper, address(token), params.amount, lockTag); + } - bytes32 digest = keccak256( - abi.encodePacked( - bytes2(0x1901), - theCompact.DOMAIN_SEPARATOR(), - keccak256( - abi.encode( - keccak256( - "Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount)" - ), - swapper, - swapper, - nonce, - expiration, - id, - amount - ) - ) - ) - ); + // Create digest and allocator signature + bytes memory allocatorData; + { + bytes32 claimHash = + _createClaimHash(compactTypehash, swapper, swapper, params.nonce, params.deadline, id, params.amount); - (bytes32 r, bytes32 vs) = vm.signCompact(allocatorPrivateKey, digest); - bytes memory allocatorData = abi.encodePacked(r, vs); + bytes32 digest = _createDigest(theCompact.DOMAIN_SEPARATOR(), claimHash); - uint256 claimantOne = abi.decode(abi.encodePacked(bytes12(bytes32(id)), recipientOne), (uint256)); - uint256 claimantTwo = abi.decode(abi.encodePacked(bytes12(bytes32(id)), recipientTwo), (uint256)); + bytes32 r; + bytes32 vs; + (r, vs) = vm.signCompact(allocatorPrivateKey, digest); + allocatorData = abi.encodePacked(r, vs); + } - SplitComponent memory splitOne = SplitComponent({ claimant: claimantOne, amount: amountOne }); + // Prepare recipients + SplitComponent[] memory recipients; + { + uint256 claimantOne = abi.decode(abi.encodePacked(bytes12(bytes32(id)), recipientOne), (uint256)); + uint256 claimantTwo = abi.decode(abi.encodePacked(bytes12(bytes32(id)), recipientTwo), (uint256)); - SplitComponent memory splitTwo = SplitComponent({ claimant: claimantTwo, amount: amountTwo }); + SplitComponent memory splitOne = SplitComponent({ claimant: claimantOne, amount: amountOne }); + SplitComponent memory splitTwo = SplitComponent({ claimant: claimantTwo, amount: amountTwo }); - SplitComponent[] memory recipients = new SplitComponent[](2); - recipients[0] = splitOne; - recipients[1] = splitTwo; + recipients = new SplitComponent[](2); + recipients[0] = splitOne; + recipients[1] = splitTwo; + } + // Create and execute transfer SplitTransfer memory transfer = SplitTransfer({ - nonce: nonce, - expires: expiration, + nonce: params.nonce, + expires: params.deadline, allocatorData: allocatorData, id: id, recipients: recipients @@ -818,7 +1008,8 @@ contract TheCompactTest is Test { vm.snapshotGasLastCall("splitTransfer"); assert(status); - assertEq(token.balanceOf(address(theCompact)), amount); + // Verify balances + assertEq(token.balanceOf(address(theCompact)), params.amount); assertEq(token.balanceOf(recipientOne), 0); assertEq(theCompact.balanceOf(swapper, id), 0); assertEq(theCompact.balanceOf(recipientOne, id), amountOne); @@ -826,239 +1017,304 @@ contract TheCompactTest is Test { } function test_qualified_basicTransfer() public { - ResetPeriod resetPeriod = ResetPeriod.TenMinutes; - Scope scope = Scope.Multichain; - uint256 amount = 1e18; - uint256 nonce = 0; - uint256 expiration = block.timestamp + 1000; - address recipient = 0x1111111111111111111111111111111111111111; - - allocator = address(new QualifiedAllocator(vm.addr(allocatorPrivateKey), address(theCompact))); - - vm.prank(allocator); - uint96 allocatorId = theCompact.__registerAllocator(allocator, ""); - - bytes12 lockTag = - bytes12(bytes32((uint256(scope) << 255) | (uint256(resetPeriod) << 252) | (uint256(allocatorId) << 160))); - - vm.prank(swapper); - uint256 id = theCompact.deposit(address(token), lockTag, amount, swapper); - assertEq(theCompact.balanceOf(swapper, id), amount); + // Setup test parameters + TestParams memory params; + params.resetPeriod = ResetPeriod.TenMinutes; + params.scope = Scope.Multichain; + params.amount = 1e18; + params.nonce = 0; + params.deadline = block.timestamp + 1000; + params.recipient = 0x1111111111111111111111111111111111111111; + + // Setup qualified allocator + uint256 id; + { + allocator = address(new QualifiedAllocator(vm.addr(allocatorPrivateKey), address(theCompact))); - bytes32 claimHash = keccak256( - abi.encode( - keccak256( - "Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount)" - ), - swapper, - swapper, - nonce, - expiration, - id, - amount - ) - ); + // Register allocator and create lock tag + uint96 allocatorId; + bytes12 lockTag; + (allocatorId, lockTag) = _registerAllocator(allocator); - bytes32 qualificationArgument = keccak256("qualification"); + // Make deposit + id = _makeDeposit(swapper, address(token), params.amount, lockTag); + } - bytes32 qualifiedDigest = keccak256( - abi.encodePacked( - bytes2(0x1901), - theCompact.DOMAIN_SEPARATOR(), - keccak256( - abi.encode( - keccak256("QualifiedClaim(bytes32 claimHash,bytes32 qualificationArg)"), - claimHash, - qualificationArgument - ) - ) - ) - ); + // Create qualified digest and allocator signature + bytes memory allocatorData; + bytes32 qualificationArgument; + bytes32 claimHash; + { + claimHash = + _createClaimHash(compactTypehash, swapper, swapper, params.nonce, params.deadline, id, params.amount); + + qualificationArgument = keccak256("qualification"); + + { + bytes32 qualifiedDigest; + { + bytes32 qualifiedHash = keccak256( + abi.encode( + keccak256("QualifiedClaim(bytes32 claimHash,bytes32 qualificationArg)"), + claimHash, + qualificationArgument + ) + ); - (bytes32 r, bytes32 vs) = vm.signCompact(allocatorPrivateKey, qualifiedDigest); - bytes memory allocatorData = abi.encodePacked(r, vs); + qualifiedDigest = _createDigest(theCompact.DOMAIN_SEPARATOR(), qualifiedHash); + } - uint256 claimant = abi.decode(abi.encodePacked(bytes12(bytes32(id)), recipient), (uint256)); + bytes32 r; + bytes32 vs; + (r, vs) = vm.signCompact(allocatorPrivateKey, qualifiedDigest); + allocatorData = abi.encodePacked(r, vs); + } + } - SplitComponent memory split = SplitComponent({ claimant: claimant, amount: amount }); + // Prepare recipients + SplitComponent[] memory recipients; + { + uint256 claimant = abi.decode(abi.encodePacked(bytes12(bytes32(id)), params.recipient), (uint256)); - SplitComponent[] memory recipients = new SplitComponent[](1); - recipients[0] = split; + SplitComponent memory split = SplitComponent({ claimant: claimant, amount: params.amount }); - SplitTransfer memory transfer = SplitTransfer({ - nonce: nonce, - expires: expiration, - allocatorData: abi.encode(allocatorData, qualificationArgument), - id: id, - recipients: recipients - }); + recipients = new SplitComponent[](1); + recipients[0] = split; + } - vm.prank(swapper); - bool status = theCompact.allocatedTransfer(transfer); - vm.snapshotGasLastCall("qualified_basicTransfer"); - assert(status); + // Create and execute transfer + bool status; + { + SplitTransfer memory transfer = SplitTransfer({ + nonce: params.nonce, + expires: params.deadline, + allocatorData: abi.encode(allocatorData, qualificationArgument), + id: id, + recipients: recipients + }); + + vm.prank(swapper); + status = theCompact.allocatedTransfer(transfer); + vm.snapshotGasLastCall("qualified_basicTransfer"); + assert(status); + } - assertEq(token.balanceOf(address(theCompact)), amount); - assertEq(token.balanceOf(recipient), 0); - assertEq(theCompact.balanceOf(swapper, id), 0); - assertEq(theCompact.balanceOf(recipient, id), amount); + // Verify balances + { + assertEq(token.balanceOf(address(theCompact)), params.amount); + assertEq(token.balanceOf(params.recipient), 0); + assertEq(theCompact.balanceOf(swapper, id), 0); + assertEq(theCompact.balanceOf(params.recipient, id), params.amount); + } } function test_splitWithdrawal() public { - ResetPeriod resetPeriod = ResetPeriod.TenMinutes; - Scope scope = Scope.Multichain; - uint256 amount = 1e18; - uint256 nonce = 0; - uint256 expiration = block.timestamp + 1000; + // Setup test parameters + TestParams memory params; + params.amount = 1e18; + params.nonce = 0; + params.deadline = block.timestamp + 1000; + + // Recipient information address recipientOne = 0x1111111111111111111111111111111111111111; address recipientTwo = 0x2222222222222222222222222222222222222222; uint256 amountOne = 4e17; uint256 amountTwo = 6e17; - vm.prank(allocator); - uint96 allocatorId = theCompact.__registerAllocator(allocator, ""); - - bytes12 lockTag = - bytes12(bytes32((uint256(scope) << 255) | (uint256(resetPeriod) << 252) | (uint256(allocatorId) << 160))); - - vm.prank(swapper); - uint256 id = theCompact.deposit(address(token), lockTag, amount, swapper); - assertEq(theCompact.balanceOf(swapper, id), amount); - - bytes32 digest = keccak256( - abi.encodePacked( - bytes2(0x1901), - theCompact.DOMAIN_SEPARATOR(), - keccak256( - abi.encode( - keccak256( - "Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount)" - ), - swapper, - swapper, - nonce, - expiration, - id, - amount - ) - ) - ) - ); - - (bytes32 r, bytes32 vs) = vm.signCompact(allocatorPrivateKey, digest); - bytes memory allocatorData = abi.encodePacked(r, vs); - - uint256 claimantOne = abi.decode(abi.encodePacked(bytes12(0), recipientOne), (uint256)); - uint256 claimantTwo = abi.decode(abi.encodePacked(bytes12(0), recipientTwo), (uint256)); - - SplitComponent memory splitOne = SplitComponent({ claimant: claimantOne, amount: amountOne }); + // Register allocator and create lock tag + uint256 id; + { + (, bytes12 lockTag) = _registerAllocator(allocator); - SplitComponent memory splitTwo = SplitComponent({ claimant: claimantTwo, amount: amountTwo }); + // Make deposit + id = _makeDeposit(swapper, address(token), params.amount, lockTag); + } - SplitComponent[] memory recipients = new SplitComponent[](2); - recipients[0] = splitOne; - recipients[1] = splitTwo; + // Create digest and allocator signature + bytes memory allocatorData; + { + bytes32 digest; + { + bytes32 claimHash = _createClaimHash( + compactTypehash, swapper, swapper, params.nonce, params.deadline, id, params.amount + ); + + digest = _createDigest(theCompact.DOMAIN_SEPARATOR(), claimHash); + } + + bytes32 r; + bytes32 vs; + (r, vs) = vm.signCompact(allocatorPrivateKey, digest); + allocatorData = abi.encodePacked(r, vs); + } - SplitTransfer memory transfer = SplitTransfer({ - nonce: nonce, - expires: expiration, - allocatorData: allocatorData, - id: id, - recipients: recipients - }); + // Prepare recipients + SplitComponent[] memory recipients; + { + uint256 claimantOne; + uint256 claimantTwo; + { + claimantOne = abi.decode(abi.encodePacked(bytes12(0), recipientOne), (uint256)); + claimantTwo = abi.decode(abi.encodePacked(bytes12(0), recipientTwo), (uint256)); + } + + { + SplitComponent memory splitOne; + SplitComponent memory splitTwo; + + splitOne = SplitComponent({ claimant: claimantOne, amount: amountOne }); + splitTwo = SplitComponent({ claimant: claimantTwo, amount: amountTwo }); + + recipients = new SplitComponent[](2); + recipients[0] = splitOne; + recipients[1] = splitTwo; + } + } - vm.prank(swapper); - bool status = theCompact.allocatedTransfer(transfer); - vm.snapshotGasLastCall("splitWithdrawal"); - assert(status); + // Create and execute transfer + { + SplitTransfer memory transfer; + { + transfer = SplitTransfer({ + nonce: params.nonce, + expires: params.deadline, + allocatorData: allocatorData, + id: id, + recipients: recipients + }); + } + + { + vm.prank(swapper); + bool status = theCompact.allocatedTransfer(transfer); + vm.snapshotGasLastCall("splitWithdrawal"); + assert(status); + } + } - assertEq(token.balanceOf(address(theCompact)), 0); - assertEq(token.balanceOf(recipientOne), amountOne); - assertEq(token.balanceOf(recipientTwo), amountTwo); - assertEq(theCompact.balanceOf(swapper, id), 0); - assertEq(theCompact.balanceOf(recipientOne, id), 0); - assertEq(theCompact.balanceOf(recipientTwo, id), 0); + // Verify balances + { + assertEq(token.balanceOf(address(theCompact)), 0); + assertEq(token.balanceOf(recipientOne), amountOne); + assertEq(token.balanceOf(recipientTwo), amountTwo); + assertEq(theCompact.balanceOf(swapper, id), 0); + assertEq(theCompact.balanceOf(recipientOne, id), 0); + assertEq(theCompact.balanceOf(recipientTwo, id), 0); + } } function test_splitBatchTransfer() public { - ResetPeriod resetPeriod = ResetPeriod.TenMinutes; - Scope scope = Scope.Multichain; + // Setup test parameters + TestParams memory params; + params.resetPeriod = ResetPeriod.TenMinutes; + params.scope = Scope.Multichain; + params.nonce = 0; + params.deadline = block.timestamp + 1000; + + // Amount information uint256 amountOne = 1e18; uint256 amountTwo = 6e17; uint256 amountThree = 4e17; - uint256 nonce = 0; - uint256 expiration = block.timestamp + 1000; + + // Recipient information address recipientOne = 0x1111111111111111111111111111111111111111; address recipientTwo = 0x2222222222222222222222222222222222222222; - vm.prank(allocator); - uint96 allocatorId = theCompact.__registerAllocator(allocator, ""); - - bytes12 lockTag = - bytes12(bytes32((uint256(scope) << 255) | (uint256(resetPeriod) << 252) | (uint256(allocatorId) << 160))); + // Register allocator and make deposits + uint256 idOne; + uint256 idTwo; + { + uint96 allocatorId; + bytes12 lockTag; + (allocatorId, lockTag) = _registerAllocator(allocator); - vm.startPrank(swapper); - uint256 idOne = theCompact.deposit(address(token), lockTag, amountOne, swapper); - uint256 idTwo = theCompact.deposit{ value: amountTwo + amountThree }(lockTag, swapper); - vm.stopPrank(); + idOne = _makeDeposit(swapper, address(token), amountOne, lockTag); + idTwo = _makeDeposit(swapper, amountTwo + amountThree, lockTag); - assertEq(theCompact.balanceOf(swapper, idOne), amountOne); - assertEq(theCompact.balanceOf(swapper, idTwo), amountTwo + amountThree); + assertEq(theCompact.balanceOf(swapper, idOne), amountOne); + assertEq(theCompact.balanceOf(swapper, idTwo), amountTwo + amountThree); + } - uint256[2][] memory idsAndAmounts = new uint256[2][](2); - idsAndAmounts[0] = [idOne, amountOne]; - idsAndAmounts[1] = [idTwo, amountTwo + amountThree]; + // Create idsAndAmounts array + uint256[2][] memory idsAndAmounts; + { + idsAndAmounts = new uint256[2][](2); + idsAndAmounts[0] = [idOne, amountOne]; + idsAndAmounts[1] = [idTwo, amountTwo + amountThree]; + } - bytes32 digest = keccak256( - abi.encodePacked( - bytes2(0x1901), - theCompact.DOMAIN_SEPARATOR(), - keccak256( + // Create digest and allocator signature + bytes memory allocatorData; + { + bytes32 digest; + { + bytes32 batchCompactHash = keccak256( abi.encode( keccak256( "BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256[2][] idsAndAmounts)" ), swapper, swapper, - nonce, - expiration, + params.nonce, + params.deadline, keccak256(abi.encodePacked(idsAndAmounts)) ) - ) - ) - ); + ); - (bytes32 r, bytes32 vs) = vm.signCompact(allocatorPrivateKey, digest); - bytes memory allocatorData = abi.encodePacked(r, vs); + digest = _createDigest(theCompact.DOMAIN_SEPARATOR(), batchCompactHash); + } + + bytes32 r; + bytes32 vs; + (r, vs) = vm.signCompact(allocatorPrivateKey, digest); + allocatorData = abi.encodePacked(r, vs); + } + // Prepare transfers + SplitBatchTransfer memory transfer; SplitByIdComponent[] memory transfers = new SplitByIdComponent[](2); + { + SplitComponent[] memory portionsOne; - uint256 claimantOne = abi.decode(abi.encodePacked(bytes12(bytes32(idOne)), recipientOne), (uint256)); - uint256 claimantTwo = abi.decode(abi.encodePacked(bytes12(bytes32(idTwo)), recipientOne), (uint256)); - uint256 claimantThree = abi.decode(abi.encodePacked(bytes12(bytes32(idTwo)), recipientTwo), (uint256)); + uint256 claimantOne = abi.decode(abi.encodePacked(bytes12(bytes32(idOne)), recipientOne), (uint256)); - SplitComponent[] memory portionsOne = new SplitComponent[](1); - portionsOne[0] = SplitComponent({ claimant: claimantOne, amount: amountOne }); + portionsOne = new SplitComponent[](1); + portionsOne[0] = SplitComponent({ claimant: claimantOne, amount: amountOne }); - SplitComponent[] memory portionsTwo = new SplitComponent[](2); - portionsTwo[0] = SplitComponent({ claimant: claimantTwo, amount: amountTwo }); - portionsTwo[1] = SplitComponent({ claimant: claimantThree, amount: amountThree }); + transfers[0] = SplitByIdComponent({ id: idOne, portions: portionsOne }); + } - transfers[0] = SplitByIdComponent({ id: idOne, portions: portionsOne }); - transfers[1] = SplitByIdComponent({ id: idTwo, portions: portionsTwo }); + { + SplitComponent[] memory portionsTwo; + uint256 claimantTwo = abi.decode(abi.encodePacked(bytes12(bytes32(idTwo)), recipientOne), (uint256)); + uint256 claimantThree = abi.decode(abi.encodePacked(bytes12(bytes32(idTwo)), recipientTwo), (uint256)); - SplitBatchTransfer memory transfer = SplitBatchTransfer({ - nonce: nonce, - expires: expiration, - allocatorData: allocatorData, - transfers: transfers - }); + portionsTwo = new SplitComponent[](2); + portionsTwo[0] = SplitComponent({ claimant: claimantTwo, amount: amountTwo }); + portionsTwo[1] = SplitComponent({ claimant: claimantThree, amount: amountThree }); - vm.prank(swapper); - bool status = theCompact.allocatedTransfer(transfer); - vm.snapshotGasLastCall("splitBatchTransfer"); - assert(status); + transfers[1] = SplitByIdComponent({ id: idTwo, portions: portionsTwo }); + } + + { + // Create batch transfer + transfer = SplitBatchTransfer({ + nonce: params.nonce, + expires: params.deadline, + allocatorData: allocatorData, + transfers: transfers + }); + } + // Execute transfer + { + vm.prank(swapper); + bool status = theCompact.allocatedTransfer(transfer); + vm.snapshotGasLastCall("splitBatchTransfer"); + assert(status); + } + + // Verify balances assertEq(token.balanceOf(recipientOne), 0); assertEq(token.balanceOf(recipientTwo), 0); assertEq(theCompact.balanceOf(swapper, idOne), 0); @@ -1069,84 +1325,120 @@ contract TheCompactTest is Test { } function test_splitBatchWithdrawal() public { - ResetPeriod resetPeriod = ResetPeriod.TenMinutes; - Scope scope = Scope.Multichain; + // Setup test parameters + TestParams memory params; + params.resetPeriod = ResetPeriod.TenMinutes; + params.scope = Scope.Multichain; + params.nonce = 0; + params.deadline = block.timestamp + 1000; + + // Amount information uint256 amountOne = 1e18; uint256 amountTwo = 6e17; uint256 amountThree = 4e17; - uint256 nonce = 0; - uint256 expiration = block.timestamp + 1000; + + // Recipient information address recipientOne = 0x1111111111111111111111111111111111111111; address recipientTwo = 0x2222222222222222222222222222222222222222; - vm.prank(allocator); - uint96 allocatorId = theCompact.__registerAllocator(allocator, ""); - - bytes12 lockTag = - bytes12(bytes32((uint256(scope) << 255) | (uint256(resetPeriod) << 252) | (uint256(allocatorId) << 160))); + // Register allocator and make deposits + uint256 idOne; + uint256 idTwo; + { + uint96 allocatorId; + bytes12 lockTag; + (allocatorId, lockTag) = _registerAllocator(allocator); - vm.startPrank(swapper); - uint256 idOne = theCompact.deposit(address(token), lockTag, amountOne, swapper); - uint256 idTwo = theCompact.deposit{ value: amountTwo + amountThree }(lockTag, swapper); - vm.stopPrank(); + idOne = _makeDeposit(swapper, address(token), amountOne, lockTag); + idTwo = _makeDeposit(swapper, amountTwo + amountThree, lockTag); - assertEq(theCompact.balanceOf(swapper, idOne), amountOne); - assertEq(theCompact.balanceOf(swapper, idTwo), amountTwo + amountThree); + assertEq(theCompact.balanceOf(swapper, idOne), amountOne); + assertEq(theCompact.balanceOf(swapper, idTwo), amountTwo + amountThree); + } - uint256[2][] memory idsAndAmounts = new uint256[2][](2); - idsAndAmounts[0] = [idOne, amountOne]; - idsAndAmounts[1] = [idTwo, amountTwo + amountThree]; + // Create idsAndAmounts array + uint256[2][] memory idsAndAmounts; + { + idsAndAmounts = new uint256[2][](2); + idsAndAmounts[0] = [idOne, amountOne]; + idsAndAmounts[1] = [idTwo, amountTwo + amountThree]; + } - bytes32 digest = keccak256( - abi.encodePacked( - bytes2(0x1901), - theCompact.DOMAIN_SEPARATOR(), - keccak256( + // Create digest and allocator signature + bytes memory allocatorData; + { + bytes32 digest; + { + bytes32 batchCompactHash = keccak256( abi.encode( keccak256( "BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256[2][] idsAndAmounts)" ), swapper, swapper, - nonce, - expiration, + params.nonce, + params.deadline, keccak256(abi.encodePacked(idsAndAmounts)) ) - ) - ) - ); + ); - (bytes32 r, bytes32 vs) = vm.signCompact(allocatorPrivateKey, digest); - bytes memory allocatorData = abi.encodePacked(r, vs); + digest = _createDigest(theCompact.DOMAIN_SEPARATOR(), batchCompactHash); + } - SplitByIdComponent[] memory transfers = new SplitByIdComponent[](2); + bytes32 r; + bytes32 vs; + (r, vs) = vm.signCompact(allocatorPrivateKey, digest); + allocatorData = abi.encodePacked(r, vs); + } - uint256 claimantOne = abi.decode(abi.encodePacked(bytes12(0), recipientOne), (uint256)); - uint256 claimantTwo = abi.decode(abi.encodePacked(bytes12(0), recipientOne), (uint256)); - uint256 claimantThree = abi.decode(abi.encodePacked(bytes12(0), recipientTwo), (uint256)); + // Prepare transfers + SplitBatchTransfer memory transfer; + SplitByIdComponent[] memory transfers = new SplitByIdComponent[](2); + { + // First transfer + { + SplitComponent[] memory portionsOne; + uint256 claimantOne = abi.decode(abi.encodePacked(bytes12(0), recipientOne), (uint256)); + + portionsOne = new SplitComponent[](1); + portionsOne[0] = SplitComponent({ claimant: claimantOne, amount: amountOne }); + + transfers[0] = SplitByIdComponent({ id: idOne, portions: portionsOne }); + } + + // Second transfer + { + SplitComponent[] memory portionsTwo; + uint256 claimantTwo = abi.decode(abi.encodePacked(bytes12(0), recipientOne), (uint256)); + uint256 claimantThree = abi.decode(abi.encodePacked(bytes12(0), recipientTwo), (uint256)); + + portionsTwo = new SplitComponent[](2); + portionsTwo[0] = SplitComponent({ claimant: claimantTwo, amount: amountTwo }); + portionsTwo[1] = SplitComponent({ claimant: claimantThree, amount: amountThree }); + + transfers[1] = SplitByIdComponent({ id: idTwo, portions: portionsTwo }); + } + + // Create batch transfer + { + transfer = SplitBatchTransfer({ + nonce: params.nonce, + expires: params.deadline, + allocatorData: allocatorData, + transfers: transfers + }); + } + } - SplitComponent[] memory portionsOne = new SplitComponent[](1); - portionsOne[0] = SplitComponent({ claimant: claimantOne, amount: amountOne }); - - SplitComponent[] memory portionsTwo = new SplitComponent[](2); - portionsTwo[0] = SplitComponent({ claimant: claimantTwo, amount: amountTwo }); - portionsTwo[1] = SplitComponent({ claimant: claimantThree, amount: amountThree }); - - transfers[0] = SplitByIdComponent({ id: idOne, portions: portionsOne }); - transfers[1] = SplitByIdComponent({ id: idTwo, portions: portionsTwo }); - - SplitBatchTransfer memory transfer = SplitBatchTransfer({ - nonce: nonce, - expires: expiration, - allocatorData: allocatorData, - transfers: transfers - }); - - vm.prank(swapper); - bool status = theCompact.allocatedTransfer(transfer); - vm.snapshotGasLastCall("splitBatchWithdrawal"); - assert(status); + // Execute transfer + { + vm.prank(swapper); + bool status = theCompact.allocatedTransfer(transfer); + vm.snapshotGasLastCall("splitBatchWithdrawal"); + assert(status); + } + // Verify balances assertEq(token.balanceOf(recipientOne), amountOne); assertEq(token.balanceOf(recipientTwo), 0); assertEq(recipientOne.balance, amountTwo); @@ -1159,1188 +1451,1596 @@ contract TheCompactTest is Test { } function test_registerAndClaim() public { - ResetPeriod resetPeriod = ResetPeriod.TenMinutes; - Scope scope = Scope.Multichain; - uint256 amount = 1e18; - uint256 nonce = 0; - uint256 expires = block.timestamp + 1000; + Claim memory claim; + claim.sponsor = swapper; + claim.nonce = 0; + claim.expires = block.timestamp + 1000; + claim.allocatedAmount = 1e18; + address arbiter = 0x2222222222222222222222222222222222222222; address recipientOne = 0x1111111111111111111111111111111111111111; address recipientTwo = 0x3333333333333333333333333333333333333333; uint256 amountOne = 4e17; uint256 amountTwo = 6e17; - vm.prank(allocator); - uint96 allocatorId = theCompact.__registerAllocator(allocator, ""); - - bytes12 lockTag = - bytes12(bytes32((uint256(scope) << 255) | (uint256(resetPeriod) << 252) | (uint256(allocatorId) << 160))); - - vm.prank(swapper); - uint256 id = theCompact.deposit{ value: amount }(lockTag, swapper); - assertEq(theCompact.balanceOf(swapper, id), amount); - - string memory witnessTypestring = "CompactWitness witness)CompactWitness(uint256 witnessArgument)"; - uint256 witnessArgument = 234; - bytes32 witness = keccak256(abi.encode(keccak256("CompactWitness(uint256 witnessArgument)"), witnessArgument)); - - string memory compactTypestring = - "Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,CompactWitness witness)CompactWitness(uint256 witnessArgument)"; - - bytes32 typehash = keccak256(bytes(compactTypestring)); + { + (, bytes12 lockTag) = _registerAllocator(allocator); - bytes32 claimHash = keccak256(abi.encode(typehash, arbiter, swapper, nonce, expires, id, amount, witness)); + claim.id = _makeDeposit(swapper, claim.allocatedAmount, lockTag); + claim.witness = _createCompactWitness(234); + } - bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), theCompact.DOMAIN_SEPARATOR(), claimHash)); + bytes32 claimHash; + { + CreateClaimHashWithWitnessArgs memory args; + args.typehash = compactWithWitnessTypehash; + args.arbiter = arbiter; + args.sponsor = claim.sponsor; + args.nonce = claim.nonce; + args.expires = claim.expires; + args.id = claim.id; + args.amount = claim.allocatedAmount; + args.witness = claim.witness; + + claimHash = _createClaimHashWithWitness(args); + } - (bytes32 r, bytes32 vs) = vm.signCompact(allocatorPrivateKey, digest); - bytes memory allocatorData = abi.encodePacked(r, vs); + bytes32 digest = _createDigest(theCompact.DOMAIN_SEPARATOR(), claimHash); - uint256 claimantOne = abi.decode(abi.encodePacked(bytes12(bytes32(id)), recipientOne), (uint256)); - uint256 claimantTwo = abi.decode(abi.encodePacked(bytes12(bytes32(id)), recipientTwo), (uint256)); + { + (bytes32 r, bytes32 vs) = vm.signCompact(allocatorPrivateKey, digest); + claim.allocatorData = abi.encodePacked(r, vs); + } - SplitComponent memory splitOne = SplitComponent({ claimant: claimantOne, amount: amountOne }); + uint256 claimantOne = abi.decode(abi.encodePacked(bytes12(bytes32(claim.id)), recipientOne), (uint256)); + uint256 claimantTwo = abi.decode(abi.encodePacked(bytes12(bytes32(claim.id)), recipientTwo), (uint256)); - SplitComponent memory splitTwo = SplitComponent({ claimant: claimantTwo, amount: amountTwo }); + SplitComponent[] memory recipients; + { + SplitComponent memory splitOne = SplitComponent({ claimant: claimantOne, amount: amountOne }); - SplitComponent[] memory recipients = new SplitComponent[](2); - recipients[0] = splitOne; - recipients[1] = splitTwo; + SplitComponent memory splitTwo = SplitComponent({ claimant: claimantTwo, amount: amountTwo }); - bytes memory sponsorSignature = ""; + recipients = new SplitComponent[](2); + recipients[0] = splitOne; + recipients[1] = splitTwo; + } - Claim memory claim = Claim( - allocatorData, sponsorSignature, swapper, nonce, expires, witness, witnessTypestring, id, amount, recipients - ); + claim.sponsorSignature = ""; + claim.witnessTypestring = witnessTypestring; + claim.claimants = recipients; vm.prank(swapper); - (bool status) = theCompact.register(claimHash, typehash); - vm.snapshotGasLastCall("register"); - assert(status); + { + (bool status) = theCompact.register(claimHash, compactWithWitnessTypehash); + vm.snapshotGasLastCall("register"); + assert(status); + } - (bool isActive, uint256 registeredAt) = theCompact.getRegistrationStatus(swapper, claimHash, typehash); - assert(isActive); - assertEq(registeredAt, block.timestamp); + { + (bool isActive, uint256 registeredAt) = + theCompact.getRegistrationStatus(swapper, claimHash, compactWithWitnessTypehash); + assert(isActive); + assertEq(registeredAt, block.timestamp); + } vm.prank(arbiter); (bytes32 returnedClaimHash) = theCompact.claim(claim); vm.snapshotGasLastCall("claim"); assertEq(returnedClaimHash, claimHash); - assertEq(address(theCompact).balance, amount); + assertEq(address(theCompact).balance, claim.allocatedAmount); 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); + assertEq(theCompact.balanceOf(swapper, claim.id), 0); + assertEq(theCompact.balanceOf(recipientOne, claim.id), amountOne); + assertEq(theCompact.balanceOf(recipientTwo, claim.id), amountTwo); } function test_registerForAndClaim() public { - ResetPeriod resetPeriod = ResetPeriod.TenMinutes; - Scope scope = Scope.Multichain; - uint256 amount = 1e18; - uint256 nonce = 0; - uint256 expires = block.timestamp + 1000; - address claimant = 0x1111111111111111111111111111111111111111; + // Setup test parameters + TestParams memory params; + params.resetPeriod = ResetPeriod.TenMinutes; + params.scope = Scope.Multichain; + params.amount = 1e18; + params.nonce = 0; + params.deadline = block.timestamp + 1000; + params.recipient = 0x1111111111111111111111111111111111111111; + + // Additional parameters address arbiter = 0x2222222222222222222222222222222222222222; - address swapperSponsor = makeAddr("swapperSponsor"); - vm.prank(allocator); - uint96 allocatorId = theCompact.__registerAllocator(allocator, ""); - - bytes12 lockTag = - bytes12(bytes32((uint256(scope) << 255) | (uint256(resetPeriod) << 252) | (uint256(allocatorId) << 160))); + // Register allocator and setup tokens + uint256 id; + bytes12 lockTag; + { + uint96 allocatorId; + (allocatorId, lockTag) = _registerAllocator(allocator); - vm.prank(swapper); - token.transfer(swapperSponsor, amount); + vm.prank(swapper); + token.transfer(swapperSponsor, params.amount); - vm.prank(swapperSponsor); - token.approve(address(theCompact), 1e18); + vm.prank(swapperSponsor); + token.approve(address(theCompact), params.amount); + } - string memory witnessTypestring = "Witness witness)Witness(uint256 witnessArgument)"; + // Create witness and deposit/register + bytes32 registeredClaimHash; + bytes32 witness; uint256 witnessArgument = 234; - bytes32 witness = keccak256(abi.encode(witnessArgument)); - - bytes32 typehash = keccak256( - "Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Witness witness)Witness(uint256 witnessArgument)" - ); - - vm.prank(swapperSponsor); - (uint256 id, bytes32 registeredClaimHash) = theCompact.depositAndRegisterFor( - address(swapper), address(token), lockTag, amount, arbiter, nonce, expires, typehash, witness - ); - vm.snapshotGasLastCall("depositRegisterFor"); - - assertEq(theCompact.balanceOf(swapper, id), amount); - assertEq(token.balanceOf(address(theCompact)), amount); - - bytes32 claimHash = keccak256(abi.encode(typehash, arbiter, swapper, nonce, expires, id, amount, witness)); - assertEq(registeredClaimHash, claimHash); - - bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), theCompact.DOMAIN_SEPARATOR(), claimHash)); - - (bool isActive, uint256 registeredAt) = theCompact.getRegistrationStatus(swapper, claimHash, typehash); - assert(isActive); - assertEq(registeredAt, block.timestamp); - - bytes memory sponsorSignature = ""; + { + witness = keccak256(abi.encode(witnessArgument)); + + vm.prank(swapperSponsor); + (id, registeredClaimHash) = theCompact.depositAndRegisterFor( + address(swapper), + address(token), + lockTag, + params.amount, + arbiter, + params.nonce, + params.deadline, + compactWithWitnessTypehash, + witness + ); + vm.snapshotGasLastCall("depositRegisterFor"); - (bytes32 r, bytes32 vs) = vm.signCompact(allocatorPrivateKey, digest); - bytes memory allocatorSignature = abi.encodePacked(r, vs); + assertEq(theCompact.balanceOf(swapper, id), params.amount); + assertEq(token.balanceOf(address(theCompact)), params.amount); + } - SplitComponent[] memory recipients = new SplitComponent[](1); - recipients[0] = SplitComponent({ - claimant: uint256(bytes32(abi.encodePacked(bytes12(bytes32(id)), claimant))), - amount: amount - }); + // Verify claim hash + bytes32 claimHash; + { + CreateClaimHashWithWitnessArgs memory args; + args.typehash = compactWithWitnessTypehash; + args.arbiter = arbiter; + args.sponsor = swapper; + args.nonce = params.nonce; + args.expires = params.deadline; + args.id = id; + args.amount = params.amount; + args.witness = witness; + + claimHash = _createClaimHashWithWitness(args); + assertEq(registeredClaimHash, claimHash); + + { + bool isActive; + uint256 registeredAt; + (isActive, registeredAt) = + theCompact.getRegistrationStatus(swapper, claimHash, compactWithWitnessTypehash); + assert(isActive); + assertEq(registeredAt, block.timestamp); + } + } - Claim memory claim = Claim( - allocatorSignature, - sponsorSignature, - swapper, - nonce, - expires, - witness, - witnessTypestring, - id, - amount, - recipients - ); + // Prepare claim + Claim memory claim; + { + // Create digest and get allocator signature + bytes memory allocatorSignature; + { + bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), theCompact.DOMAIN_SEPARATOR(), claimHash)); + + bytes32 r; + bytes32 vs; + (r, vs) = vm.signCompact(allocatorPrivateKey, digest); + allocatorSignature = abi.encodePacked(r, vs); + } + + // Create recipients + SplitComponent[] memory recipients; + { + recipients = new SplitComponent[](1); + + uint256 claimantId = uint256(bytes32(abi.encodePacked(bytes12(bytes32(id)), params.recipient))); + + recipients[0] = SplitComponent({ claimant: claimantId, amount: params.amount }); + } + + // Build the claim + claim = Claim( + allocatorSignature, + "", // sponsorSignature + swapper, + params.nonce, + params.deadline, + witness, + witnessTypestring, + id, + params.amount, + recipients + ); + } - vm.prank(arbiter); - bytes32 returnedClaimHash = theCompact.claim(claim); - assertEq(returnedClaimHash, claimHash); + // Execute claim + { + vm.prank(arbiter); + bytes32 returnedClaimHash = theCompact.claim(claim); + assertEq(returnedClaimHash, claimHash); + } - assertEq(token.balanceOf(address(theCompact)), amount); + // Verify balances + assertEq(token.balanceOf(address(theCompact)), params.amount); assertEq(theCompact.balanceOf(swapper, id), 0); - assertEq(theCompact.balanceOf(claimant, id), amount); + assertEq(theCompact.balanceOf(params.recipient, id), params.amount); } function test_claimAndWithdraw() public { - ResetPeriod resetPeriod = ResetPeriod.TenMinutes; - Scope scope = Scope.Multichain; - uint256 amount = 1e18; - uint256 nonce = 0; - uint256 expires = block.timestamp + 1000; + // Initialize claim struct + Claim memory claim; + claim.sponsor = swapper; + claim.nonce = 0; + claim.expires = block.timestamp + 1000; + claim.allocatedAmount = 1e18; + + // Recipient information address recipientOne = 0x1111111111111111111111111111111111111111; address recipientTwo = 0x3333333333333333333333333333333333333333; uint256 amountOne = 4e17; uint256 amountTwo = 6e17; address arbiter = 0x2222222222222222222222222222222222222222; - vm.prank(allocator); - uint96 allocatorId = theCompact.__registerAllocator(allocator, ""); - - bytes12 lockTag = - bytes12(bytes32((uint256(scope) << 255) | (uint256(resetPeriod) << 252) | (uint256(allocatorId) << 160))); - - vm.prank(swapper); - uint256 id = theCompact.deposit{ value: amount }(lockTag, swapper); - assertEq(theCompact.balanceOf(swapper, id), amount); - - string memory witnessTypestring = "CompactWitness witness)CompactWitness(uint256 witnessArgument)"; - uint256 witnessArgument = 234; - bytes32 witness = keccak256(abi.encode(keccak256("CompactWitness(uint256 witnessArgument)"), witnessArgument)); - - string memory compactTypestring = - "Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,CompactWitness witness)CompactWitness(uint256 witnessArgument)"; - - bytes32 typehash = keccak256(bytes(compactTypestring)); - - bytes32 claimHash = keccak256(abi.encode(typehash, arbiter, swapper, nonce, expires, id, amount, witness)); - - bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), theCompact.DOMAIN_SEPARATOR(), claimHash)); + // Register allocator, make deposit and create witness + { + bytes12 lockTag; + { + uint96 allocatorId; + (allocatorId, lockTag) = _registerAllocator(allocator); + } + + claim.id = _makeDeposit(swapper, claim.allocatedAmount, lockTag); + claim.witness = _createCompactWitness(234); + } - (bytes32 r, bytes32 vs) = vm.signCompact(swapperPrivateKey, digest); - bytes memory sponsorSignature = abi.encodePacked(r, vs); + // Create claim hash + bytes32 claimHash; + { + CreateClaimHashWithWitnessArgs memory args; + args.typehash = compactWithWitnessTypehash; + args.arbiter = arbiter; + args.sponsor = claim.sponsor; + args.nonce = claim.nonce; + args.expires = claim.expires; + args.id = claim.id; + args.amount = claim.allocatedAmount; + args.witness = claim.witness; + + claimHash = _createClaimHashWithWitness(args); + } - (r, vs) = vm.signCompact(allocatorPrivateKey, digest); - bytes memory allocatorData = abi.encodePacked(r, vs); + // Create signatures + bytes32 digest = _createDigest(theCompact.DOMAIN_SEPARATOR(), claimHash); - uint256 claimantOne = abi.decode(abi.encodePacked(bytes12(0), recipientOne), (uint256)); - uint256 claimantTwo = abi.decode(abi.encodePacked(bytes12(0), recipientTwo), (uint256)); + { + bytes32 r; + bytes32 vs; + + // Create sponsor signature + { + (r, vs) = vm.signCompact(swapperPrivateKey, digest); + claim.sponsorSignature = abi.encodePacked(r, vs); + } + + // Create allocator signature + { + (r, vs) = vm.signCompact(allocatorPrivateKey, digest); + claim.allocatorData = abi.encodePacked(r, vs); + } + } - SplitComponent memory splitOne = SplitComponent({ claimant: claimantOne, amount: amountOne }); + // Prepare recipients + { + uint256 claimantOne = abi.decode(abi.encodePacked(bytes12(0), recipientOne), (uint256)); + uint256 claimantTwo = abi.decode(abi.encodePacked(bytes12(0), recipientTwo), (uint256)); - SplitComponent memory splitTwo = SplitComponent({ claimant: claimantTwo, amount: amountTwo }); + SplitComponent[] memory recipients; + { + SplitComponent memory splitOne = SplitComponent({ claimant: claimantOne, amount: amountOne }); + SplitComponent memory splitTwo = SplitComponent({ claimant: claimantTwo, amount: amountTwo }); - SplitComponent[] memory recipients = new SplitComponent[](2); - recipients[0] = splitOne; - recipients[1] = splitTwo; + recipients = new SplitComponent[](2); + recipients[0] = splitOne; + recipients[1] = splitTwo; + } - Claim memory claim = Claim( - allocatorData, sponsorSignature, swapper, nonce, expires, witness, witnessTypestring, id, amount, recipients - ); + claim.witnessTypestring = witnessTypestring; + claim.claimants = recipients; + } - vm.prank(arbiter); - (bytes32 returnedClaimHash) = theCompact.claim(claim); - vm.snapshotGasLastCall("claimAndWithdraw"); - assertEq(returnedClaimHash, claimHash); + // Execute claim + bytes32 returnedClaimHash; + { + vm.prank(arbiter); + returnedClaimHash = theCompact.claim(claim); + vm.snapshotGasLastCall("claimAndWithdraw"); + assertEq(returnedClaimHash, claimHash); + } + // Verify balances assertEq(address(theCompact).balance, 0); assertEq(recipientOne.balance, amountOne); assertEq(recipientTwo.balance, amountTwo); - assertEq(theCompact.balanceOf(swapper, id), 0); - assertEq(theCompact.balanceOf(recipientOne, id), 0); - assertEq(theCompact.balanceOf(recipientTwo, id), 0); + assertEq(theCompact.balanceOf(swapper, claim.id), 0); + assertEq(theCompact.balanceOf(recipientOne, claim.id), 0); + assertEq(theCompact.balanceOf(recipientTwo, claim.id), 0); } function test_depositAndRegisterWithWitnessViaPermit2ThenClaim() public virtual { - ResetPeriod resetPeriod = ResetPeriod.TenMinutes; - Scope scope = Scope.Multichain; - uint256 amount = 1e18; - uint256 amountOne = 4e17; - uint256 amountTwo = 6e17; - uint256 nonce = 0; - uint256 deadline = block.timestamp + 1000; - uint256 expires = block.timestamp + 1000; - address recipientOne = 0x1111111111111111111111111111111111111111; - address recipientTwo = 0x3333333333333333333333333333333333333333; - address arbiter = 0x2222222222222222222222222222222222222222; + // Setup test parameters + TestParams memory params; + params.resetPeriod = ResetPeriod.TenMinutes; + params.scope = Scope.Multichain; + params.amount = 1e18; + params.nonce = 0; + params.deadline = block.timestamp + 1000; + + // Initialize claim + Claim memory claim; + claim.sponsor = swapper; + claim.nonce = params.nonce; + claim.expires = block.timestamp + 1000; + claim.allocatedAmount = params.amount; + claim.witnessTypestring = witnessTypestring; + claim.sponsorSignature = ""; + + // Create domain separator + bytes32 domainSeparator; + { + domainSeparator = keccak256( + abi.encode(permit2EIP712DomainHash, keccak256(bytes("Permit2")), block.chainid, address(permit2)) + ); + assertEq(domainSeparator, EIP712(permit2).DOMAIN_SEPARATOR()); + } - vm.prank(allocator); - uint96 allocatorId = theCompact.__registerAllocator(allocator, ""); + // Create witness and id + LockDetails memory expectedDetails; + uint96 allocatorId; + { + // Register allocator and setup + bytes12 lockTag; + { + (allocatorId, lockTag) = _registerAllocator(allocator); + } + expectedDetails.lockTag = lockTag; + + uint256 witnessArgument = 234; + claim.witness = _createCompactWitness(witnessArgument); + claim.id = uint256(bytes32(lockTag)) | uint256(uint160(address(token))); + } - bytes12 lockTag = - bytes12(bytes32((uint256(scope) << 255) | (uint256(resetPeriod) << 252) | (uint256(allocatorId) << 160))); + // Create claim hash + bytes32 claimHash; + { + CreateClaimHashWithWitnessArgs memory args; + args.typehash = compactWithWitnessTypehash; + args.arbiter = 0x2222222222222222222222222222222222222222; + args.sponsor = claim.sponsor; + args.nonce = claim.nonce; + args.expires = claim.expires; + args.id = claim.id; + args.amount = claim.allocatedAmount; + args.witness = claim.witness; + + claimHash = _createClaimHashWithWitness(args); + } - bytes32 domainSeparator = - keccak256(abi.encode(permit2EIP712DomainHash, keccak256(bytes("Permit2")), block.chainid, address(permit2))); + // Create activation typehash and permit signature + bytes memory signature; + ISignatureTransfer.PermitTransferFrom memory permit; + { + bytes32 activationTypehash = + keccak256(bytes(string.concat("Activation(uint256 id,Compact compact)", compactWitnessTypestring))); - assertEq(domainSeparator, EIP712(permit2).DOMAIN_SEPARATOR()); + { + bytes32 tokenPermissionsHash = keccak256( + abi.encode( + keccak256("TokenPermissions(address token,uint256 amount)"), address(token), params.amount + ) + ); + + bytes32 permitWitnessHash; + { + bytes32 activationHash = keccak256(abi.encode(activationTypehash, claim.id, claimHash)); + + permitWitnessHash = keccak256( + abi.encode( + keccak256( + "PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline,Activation witness)Activation(uint256 id,Compact compact)Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,CompactWitness witness)CompactWitness(uint256 witnessArgument)TokenPermissions(address token,uint256 amount)" + ), + tokenPermissionsHash, + address(theCompact), // spender + params.nonce, + params.deadline, + activationHash + ) + ); + } + + bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), domainSeparator, permitWitnessHash)); + + bytes32 r; + bytes32 vs; + (r, vs) = vm.signCompact(swapperPrivateKey, digest); + signature = abi.encodePacked(r, vs); + } + + // Create permit + { + permit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({ token: address(token), amount: params.amount }), + nonce: params.nonce, + deadline: params.deadline + }); + } + + // Setup expectation for permitWitnessTransferFrom call + { + bytes32 activationHash = keccak256(abi.encode(activationTypehash, claim.id, claimHash)); + + vm.expectCall( + address(permit2), + abi.encodeWithSignature( + "permitWitnessTransferFrom(((address,uint256),uint256,uint256),(address,uint256),address,bytes32,string,bytes)", + permit, + ISignatureTransfer.SignatureTransferDetails({ + to: address(theCompact), + requestedAmount: params.amount + }), + swapper, + activationHash, + "Activation witness)Activation(uint256 id,Compact compact)Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,CompactWitness witness)CompactWitness(uint256 witnessArgument)TokenPermissions(address token,uint256 amount)", + signature + ) + ); + } + } - string memory witnessTypestring = "CompactWitness witness)CompactWitness(uint256 witnessArgument)"; - uint256 witnessArgument = 234; - bytes32 witness = - keccak256(abi.encode(keccak256(bytes("CompactWitness(uint256 witnessArgument)")), witnessArgument)); + // Deposit and register + { + uint256 returnedId = theCompact.depositAndRegister( + permit, + swapper, + expectedDetails.lockTag, + claimHash, + CompactCategory.Compact, + witnessTypestring, + signature + ); + vm.snapshotGasLastCall("depositAndRegisterViaPermit2"); + assertEq(returnedId, claim.id); + + bool isActive; + uint256 registeredAt; + (isActive, registeredAt) = theCompact.getRegistrationStatus(swapper, claimHash, compactWithWitnessTypehash); + assert(isActive); + assertEq(registeredAt, block.timestamp); + } - string memory compactTypestring = - "Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,CompactWitness witness)CompactWitness(uint256 witnessArgument)"; + // Verify lock details + { + expectedDetails.token = address(token); + expectedDetails.allocator = allocator; + expectedDetails.resetPeriod = params.resetPeriod; + expectedDetails.scope = params.scope; - bytes32 typehash = keccak256(bytes(compactTypestring)); + _verifyLockDetails(claim.id, params, expectedDetails, allocatorId); - uint256 id = (uint256(scope) << 255) | (uint256(resetPeriod) << 252) | (uint256(allocatorId) << 160) - | uint256(uint160(address(token))); + assertEq(token.balanceOf(address(theCompact)), params.amount); + assertEq(theCompact.balanceOf(swapper, claim.id), params.amount); + } - bytes32 claimHash = keccak256(abi.encode(typehash, arbiter, swapper, nonce, expires, id, amount, witness)); + // Create allocator signature + { + bytes32 digest = _createDigest(theCompact.DOMAIN_SEPARATOR(), claimHash); - bytes32 activationTypehash = - keccak256(bytes(string.concat("Activation(uint256 id,Compact compact)", compactTypestring))); + bytes32 r; + bytes32 vs; + (r, vs) = vm.signCompact(allocatorPrivateKey, digest); + claim.allocatorData = abi.encodePacked(r, vs); + } - bytes32 digest = keccak256( - abi.encodePacked( - bytes2(0x1901), - domainSeparator, - keccak256( - abi.encode( - keccak256( - "PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline,Activation witness)Activation(uint256 id,Compact compact)Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,CompactWitness witness)CompactWitness(uint256 witnessArgument)TokenPermissions(address token,uint256 amount)" - ), - keccak256( - abi.encode( - keccak256("TokenPermissions(address token,uint256 amount)"), address(token), amount - ) - ), - address(theCompact), // spender - nonce, - deadline, - keccak256(abi.encode(activationTypehash, id, claimHash)) - ) - ) - ) - ); + uint256 amountOne = 4e17; + uint256 amountTwo = 6e17; - (bytes32 r, bytes32 vs) = vm.signCompact(swapperPrivateKey, digest); - bytes memory signature = abi.encodePacked(r, vs); + // Create split components + { + uint256 claimantOne = abi.decode( + abi.encodePacked(bytes12(bytes32(claim.id)), 0x1111111111111111111111111111111111111111), (uint256) + ); + uint256 claimantTwo = abi.decode( + abi.encodePacked(bytes12(bytes32(claim.id)), 0x3333333333333333333333333333333333333333), (uint256) + ); + + SplitComponent[] memory recipients; + { + SplitComponent memory splitOne = SplitComponent({ claimant: claimantOne, amount: amountOne }); + SplitComponent memory splitTwo = SplitComponent({ claimant: claimantTwo, amount: amountTwo }); + + recipients = new SplitComponent[](2); + recipients[0] = splitOne; + recipients[1] = splitTwo; + + claim.claimants = recipients; + } + } - vm.expectCall( - address(permit2), - abi.encodeWithSignature( - "permitWitnessTransferFrom(((address,uint256),uint256,uint256),(address,uint256),address,bytes32,string,bytes)", - ISignatureTransfer.PermitTransferFrom({ - permitted: ISignatureTransfer.TokenPermissions({ token: address(token), amount: amount }), - nonce: nonce, - deadline: deadline - }), - ISignatureTransfer.SignatureTransferDetails({ to: address(theCompact), requestedAmount: amount }), - swapper, - keccak256(abi.encode(activationTypehash, id, claimHash)), - "Activation witness)Activation(uint256 id,Compact compact)Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,CompactWitness witness)CompactWitness(uint256 witnessArgument)TokenPermissions(address token,uint256 amount)", - signature - ) - ); + // Execute claim + bytes32 returnedClaimHash; + { + vm.prank(0x2222222222222222222222222222222222222222); + returnedClaimHash = theCompact.claim(claim); + vm.snapshotGasLastCall("claim"); + assertEq(returnedClaimHash, claimHash); + } - uint256 returnedId = theCompact.depositAndRegister( - address(token), - amount, - nonce, - deadline, - swapper, - lockTag, - claimHash, - CompactCategory.Compact, - witnessTypestring, - signature - ); - vm.snapshotGasLastCall("depositAndRegisterViaPermit2"); - assertEq(returnedId, id); + // Verify balances + assertEq(token.balanceOf(address(theCompact)), params.amount); + assertEq(theCompact.balanceOf(swapper, claim.id), 0); + assertEq(theCompact.balanceOf(0x1111111111111111111111111111111111111111, claim.id), amountOne); + assertEq(theCompact.balanceOf(0x3333333333333333333333333333333333333333, claim.id), amountTwo); + } - (bool isActive, uint256 registeredAt) = theCompact.getRegistrationStatus(swapper, claimHash, typehash); - assert(isActive); - assertEq(registeredAt, block.timestamp); + function test_batchDepositAndRegisterWithWitnessViaPermit2ThenClaim() public virtual { + // Setup test parameters + TestParams memory params; + params.resetPeriod = ResetPeriod.TenMinutes; + params.scope = Scope.Multichain; + params.nonce = 0; + params.deadline = block.timestamp + 1000; + + // Initialize claim data + BatchClaim memory claim; + claim.sponsor = swapper; + claim.nonce = params.nonce; + claim.expires = block.timestamp + 1000; + claim.witnessTypestring = witnessTypestring; + + // Register allocator and setup basic variables + uint96 allocatorId; + bytes12 lockTag; + { + (allocatorId, lockTag) = _registerAllocator(allocator); + } - ( - address derivedToken, - address derivedAllocator, - ResetPeriod derivedResetPeriod, - Scope derivedScope, - bytes12 derivedLockTag - ) = theCompact.getLockDetails(id); - assertEq(derivedToken, address(token)); - assertEq(derivedAllocator, allocator); - assertEq(uint256(derivedResetPeriod), uint256(resetPeriod)); - assertEq(uint256(derivedScope), uint256(scope)); - assertEq(derivedLockTag, lockTag); + // Create domain separator + bytes32 domainSeparator; + { + domainSeparator = keccak256( + abi.encode(permit2EIP712DomainHash, keccak256(bytes("Permit2")), block.chainid, address(permit2)) + ); + assertEq(domainSeparator, EIP712(permit2).DOMAIN_SEPARATOR()); + } - assertEq(token.balanceOf(address(theCompact)), amount); - assertEq(theCompact.balanceOf(swapper, id), amount); + // Create witness and typestring + bytes32 typehash; + { + claim.witness = _createCompactWitness(234); - digest = keccak256(abi.encodePacked(bytes2(0x1901), theCompact.DOMAIN_SEPARATOR(), claimHash)); + string memory typestring = + "BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256[2][] idsAndAmounts,CompactWitness witness)CompactWitness(uint256 witnessArgument)"; + typehash = keccak256(bytes(typestring)); + } - bytes memory sponsorSignature = ""; + // Create ids and idsAndAmounts + uint256[] memory ids; + uint256[2][] memory idsAndAmounts; + { + uint256 id = (uint256(params.scope) << 255) | (uint256(params.resetPeriod) << 252) + | (uint256(allocatorId) << 160) | uint256(uint160(address(0))); + uint256 anotherId = (uint256(params.scope) << 255) | (uint256(params.resetPeriod) << 252) + | (uint256(allocatorId) << 160) | uint256(uint160(address(token))); + uint256 aThirdId = (uint256(params.scope) << 255) | (uint256(params.resetPeriod) << 252) + | (uint256(allocatorId) << 160) | uint256(uint160(address(anotherToken))); + + ids = new uint256[](3); + idsAndAmounts = new uint256[2][](3); + + ids[0] = id; + ids[1] = anotherId; + ids[2] = aThirdId; + + idsAndAmounts[0][0] = id; + idsAndAmounts[0][1] = 1e18; // amount + idsAndAmounts[1][0] = anotherId; + idsAndAmounts[1][1] = 1e18; // anotherAmount + idsAndAmounts[2][0] = aThirdId; + idsAndAmounts[2][1] = 1e18; // aThirdAmount + } - (r, vs) = vm.signCompact(allocatorPrivateKey, digest); - bytes memory allocatorData = abi.encodePacked(r, vs); + // Create claim hash + bytes32 claimHash; + { + CreateBatchClaimHashWithWitnessArgs memory args; + { + args.typehash = typehash; + args.arbiter = 0x2222222222222222222222222222222222222222; + args.sponsor = claim.sponsor; + args.nonce = params.nonce; + args.expires = claim.expires; + args.idsAndAmountsHash = keccak256(abi.encodePacked(idsAndAmounts)); + args.witness = claim.witness; + } + + claimHash = _createBatchClaimHashWithWitness(args); + } - uint256 claimantOne = abi.decode(abi.encodePacked(bytes12(bytes32(id)), recipientOne), (uint256)); - uint256 claimantTwo = abi.decode(abi.encodePacked(bytes12(bytes32(id)), recipientTwo), (uint256)); + // Create activation typehash + bytes32 activationTypehash; + { + string memory typestring = + "BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256[2][] idsAndAmounts,CompactWitness witness)CompactWitness(uint256 witnessArgument)"; + activationTypehash = + keccak256(bytes(string.concat("BatchActivation(uint256[] ids,BatchCompact compact)", typestring))); + } - SplitComponent memory splitOne = SplitComponent({ claimant: claimantOne, amount: amountOne }); + // Create token permissions and signature + ISignatureTransfer.TokenPermissions[] memory tokenPermissions; + bytes memory signature; + { + tokenPermissions = new ISignatureTransfer.TokenPermissions[](3); + tokenPermissions[0] = ISignatureTransfer.TokenPermissions({ token: address(0), amount: 1e18 }); + tokenPermissions[1] = ISignatureTransfer.TokenPermissions({ token: address(token), amount: 1e18 }); + tokenPermissions[2] = ISignatureTransfer.TokenPermissions({ token: address(anotherToken), amount: 1e18 }); + + // Create signature + { + bytes32 tokenPermissionsHash; + { + bytes32[] memory tokenPermissionsHashes = new bytes32[](2); + + tokenPermissionsHashes[0] = keccak256( + abi.encode(keccak256("TokenPermissions(address token,uint256 amount)"), tokenPermissions[0]) + ); + tokenPermissionsHashes[0] = keccak256( + abi.encode(keccak256("TokenPermissions(address token,uint256 amount)"), tokenPermissions[1]) + ); + tokenPermissionsHashes[1] = keccak256( + abi.encode(keccak256("TokenPermissions(address token,uint256 amount)"), tokenPermissions[2]) + ); + + tokenPermissionsHash = keccak256(abi.encodePacked(tokenPermissionsHashes)); + } + + bytes32 digest; + { + CreatePermitBatchWitnessDigestArgs memory args; + { + args.domainSeparator = domainSeparator; + args.tokenPermissionsHash = tokenPermissionsHash; + args.spender = address(theCompact); + args.nonce = params.nonce; + args.deadline = params.deadline; + args.activationTypehash = activationTypehash; + args.idsHash = keccak256(abi.encodePacked(ids)); + args.claimHash = claimHash; + } + + digest = _createPermitBatchWitnessDigest(args); + } + + bytes32 r; + bytes32 vs; + (r, vs) = vm.signCompact(swapperPrivateKey, digest); + signature = abi.encodePacked(r, vs); + } + } - SplitComponent memory splitTwo = SplitComponent({ claimant: claimantTwo, amount: amountTwo }); + { + SetupPermitCallExpectationArgs memory args; + args.activationTypehash = activationTypehash; + args.ids = ids; + args.claimHash = claimHash; + args.nonce = params.nonce; + args.deadline = params.deadline; + args.signature = signature; + + _setupPermitCallExpectation(args); + } - SplitComponent[] memory recipients = new SplitComponent[](2); - recipients[0] = splitOne; - recipients[1] = splitTwo; + // Deposit and register + uint256[] memory returnedIds; + { + DepositDetails memory depositDetails; + depositDetails.nonce = params.nonce; + depositDetails.deadline = params.deadline; + depositDetails.lockTag = lockTag; - Claim memory claim = Claim( - allocatorData, sponsorSignature, swapper, nonce, expires, witness, witnessTypestring, id, amount, recipients - ); + returnedIds = theCompact.depositAndRegister{ value: 1e18 }( + swapper, + tokenPermissions, + depositDetails, + claimHash, + CompactCategory.BatchCompact, + witnessTypestring, + signature + ); + vm.snapshotGasLastCall("batchDepositAndRegisterWithWitnessViaPermit2"); + + assertEq(returnedIds.length, 3); + assertEq(returnedIds[0], ids[0]); + assertEq(returnedIds[1], ids[1]); + assertEq(returnedIds[2], ids[2]); + + assertEq(theCompact.balanceOf(swapper, ids[0]), 1e18); + assertEq(theCompact.balanceOf(swapper, ids[1]), 1e18); + assertEq(theCompact.balanceOf(swapper, ids[2]), 1e18); + + bool isActive; + uint256 registeredAt; + (isActive, registeredAt) = theCompact.getRegistrationStatus(swapper, claimHash, typehash); + assert(isActive); + assertEq(registeredAt, block.timestamp); + } - vm.prank(arbiter); - (bytes32 returnedClaimHash) = theCompact.claim(claim); - vm.snapshotGasLastCall("claim"); - assertEq(returnedClaimHash, claimHash); + // Regenerate claim hash + { + CreateBatchClaimHashWithWitnessArgs memory args; + { + args.typehash = keccak256( + "BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256[2][] idsAndAmounts,CompactWitness witness)CompactWitness(uint256 witnessArgument)" + ); + args.arbiter = 0x2222222222222222222222222222222222222222; + args.sponsor = swapper; + args.nonce = params.nonce; + args.expires = claim.expires; + args.idsAndAmountsHash = keccak256(abi.encodePacked(idsAndAmounts)); + args.witness = claim.witness; + } + + claimHash = _createBatchClaimHashWithWitness(args); + } - assertEq(token.balanceOf(address(theCompact)), amount); - assertEq(theCompact.balanceOf(swapper, id), 0); - assertEq(theCompact.balanceOf(recipientOne, id), amountOne); - assertEq(theCompact.balanceOf(recipientTwo, id), amountTwo); - } - - function test_splitClaimWithWitness() 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); - uint96 allocatorId = theCompact.__registerAllocator(allocator, ""); - - bytes12 lockTag = - bytes12(bytes32((uint256(scope) << 255) | (uint256(resetPeriod) << 252) | (uint256(allocatorId) << 160))); - - vm.prank(swapper); - uint256 id = theCompact.deposit{ value: amount }(lockTag, swapper); - assertEq(theCompact.balanceOf(swapper, id), amount); - - string memory witnessTypestring = "Witness witness)Witness(uint256 witnessArgument)"; - uint256 witnessArgument = 234; - bytes32 witness = keccak256(abi.encode(witnessArgument)); - - bytes32 claimHash = keccak256( - abi.encode( - keccak256( - "Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Witness witness)Witness(uint256 witnessArgument)" - ), - arbiter, - swapper, - nonce, - expires, - id, - amount, - 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 allocatorData = abi.encodePacked(r, vs); - - uint256 claimantOne = abi.decode(abi.encodePacked(bytes12(bytes32(id)), recipientOne), (uint256)); - uint256 claimantTwo = abi.decode(abi.encodePacked(bytes12(bytes32(id)), recipientTwo), (uint256)); - - SplitComponent memory splitOne = SplitComponent({ claimant: claimantOne, amount: amountOne }); - - SplitComponent memory splitTwo = SplitComponent({ claimant: claimantTwo, amount: amountTwo }); - - SplitComponent[] memory recipients = new SplitComponent[](2); - recipients[0] = splitOne; - recipients[1] = splitTwo; - - Claim memory claim = Claim( - allocatorData, sponsorSignature, swapper, nonce, expires, witness, witnessTypestring, id, amount, recipients - ); - - vm.prank(arbiter); - (bytes32 returnedClaimHash) = theCompact.claim(claim); - vm.snapshotGasLastCall("splitClaimWithWitness"); - assertEq(returnedClaimHash, claimHash); - - 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_batchDepositAndRegisterWithWitnessViaPermit2ThenClaim() public virtual { - ResetPeriod resetPeriod = ResetPeriod.TenMinutes; - Scope scope = Scope.Multichain; - uint256 nonce = 0; - uint256 deadline = block.timestamp + 1000; - uint256 expires = block.timestamp + 1000; - address recipientOne = 0x1111111111111111111111111111111111111111; - address recipientTwo = 0x3333333333333333333333333333333333333333; - address arbiter = 0x2222222222222222222222222222222222222222; - uint256 amountOne = 4e17; - uint256 amountTwo = 6e17; - uint256 amount = 1e18; - uint256 anotherAmount = 1e18; - uint256 aThirdAmount = 1e18; - - vm.prank(allocator); - uint96 allocatorId = theCompact.__registerAllocator(allocator, ""); - - bytes12 lockTag = - bytes12(bytes32((uint256(scope) << 255) | (uint256(resetPeriod) << 252) | (uint256(allocatorId) << 160))); - - bytes32 domainSeparator = - keccak256(abi.encode(permit2EIP712DomainHash, keccak256(bytes("Permit2")), block.chainid, address(permit2))); - - assertEq(domainSeparator, EIP712(permit2).DOMAIN_SEPARATOR()); - - string memory witnessTypestring = "CompactWitness witness)CompactWitness(uint256 witnessArgument)"; - uint256 witnessArgument = 234; - bytes32 witness = - keccak256(abi.encode(keccak256(bytes("CompactWitness(uint256 witnessArgument)")), witnessArgument)); - - string memory compactTypestring = - "BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256[2][] idsAndAmounts,CompactWitness witness)CompactWitness(uint256 witnessArgument)"; - - bytes32 typehash = keccak256(bytes(compactTypestring)); - - uint256 id = (uint256(scope) << 255) | (uint256(resetPeriod) << 252) | (uint256(allocatorId) << 160) - | uint256(uint160(address(0))); - uint256 anotherId = (uint256(scope) << 255) | (uint256(resetPeriod) << 252) | (uint256(allocatorId) << 160) - | uint256(uint160(address(token))); - uint256 aThirdId = (uint256(scope) << 255) | (uint256(resetPeriod) << 252) | (uint256(allocatorId) << 160) - | uint256(uint160(address(anotherToken))); - - uint256[] memory ids = new uint256[](3); - uint256[2][] memory idsAndAmounts = new uint256[2][](3); - ids[0] = id; - ids[1] = anotherId; - ids[2] = aThirdId; - - idsAndAmounts[0][0] = id; - idsAndAmounts[0][1] = amount; - idsAndAmounts[1][0] = anotherId; - idsAndAmounts[1][1] = anotherAmount; - idsAndAmounts[2][0] = aThirdId; - idsAndAmounts[2][1] = aThirdAmount; - - bytes32 claimHash = keccak256( - abi.encode(typehash, arbiter, swapper, nonce, expires, keccak256(abi.encodePacked(idsAndAmounts)), witness) - ); - - bytes32 activationTypehash = - keccak256(bytes(string.concat("BatchActivation(uint256[] ids,BatchCompact compact)", compactTypestring))); - - ISignatureTransfer.TokenPermissions[] memory tokenPermissions = new ISignatureTransfer.TokenPermissions[](3); - bytes32[] memory tokenPermissionsHashes = new bytes32[](2); - tokenPermissions[0] = ISignatureTransfer.TokenPermissions({ token: address(0), amount: amount }); - tokenPermissionsHashes[0] = - keccak256(abi.encode(keccak256("TokenPermissions(address token,uint256 amount)"), tokenPermissions[0])); - tokenPermissions[1] = ISignatureTransfer.TokenPermissions({ token: address(token), amount: anotherAmount }); - tokenPermissionsHashes[0] = - keccak256(abi.encode(keccak256("TokenPermissions(address token,uint256 amount)"), tokenPermissions[1])); - tokenPermissions[2] = - ISignatureTransfer.TokenPermissions({ token: address(anotherToken), amount: aThirdAmount }); - tokenPermissionsHashes[1] = - keccak256(abi.encode(keccak256("TokenPermissions(address token,uint256 amount)"), tokenPermissions[2])); - bytes32 tokenPermissionsHash = keccak256(abi.encodePacked(tokenPermissionsHashes)); - - bytes32 digest = keccak256( - abi.encodePacked( - bytes2(0x1901), - domainSeparator, - keccak256( - abi.encode( - keccak256( - "PermitBatchWitnessTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline,BatchActivation witness)BatchActivation(uint256[] ids,BatchCompact compact)BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256[2][] idsAndAmounts,CompactWitness witness)CompactWitness(uint256 witnessArgument)TokenPermissions(address token,uint256 amount)" - ), - tokenPermissionsHash, - address(theCompact), // spender - nonce, - deadline, - keccak256(abi.encode(activationTypehash, keccak256(abi.encodePacked(ids)), claimHash)) - ) - ) - ) - ); - - (bytes32 r, bytes32 vs) = vm.signCompact(swapperPrivateKey, digest); - bytes memory signature = abi.encodePacked(r, vs); - - ISignatureTransfer.TokenPermissions[] memory tokenPermissionsOnCall = - new ISignatureTransfer.TokenPermissions[](2); - tokenPermissionsOnCall[0] = ISignatureTransfer.TokenPermissions({ token: address(token), amount: amount }); - tokenPermissionsOnCall[1] = - ISignatureTransfer.TokenPermissions({ token: address(anotherToken), amount: amount }); - - ISignatureTransfer.SignatureTransferDetails[] memory signatureTransferDetails = - new ISignatureTransfer.SignatureTransferDetails[](2); - signatureTransferDetails[0] = - ISignatureTransfer.SignatureTransferDetails({ to: address(theCompact), requestedAmount: anotherAmount }); - signatureTransferDetails[1] = - ISignatureTransfer.SignatureTransferDetails({ to: address(theCompact), requestedAmount: aThirdAmount }); - - vm.expectCall( - address(permit2), - abi.encodeWithSignature( - "permitWitnessTransferFrom(((address,uint256)[],uint256,uint256),(address,uint256)[],address,bytes32,string,bytes)", - ISignatureTransfer.PermitBatchTransferFrom({ - permitted: tokenPermissionsOnCall, - nonce: nonce, - deadline: deadline - }), - signatureTransferDetails, - swapper, - keccak256(abi.encode(activationTypehash, keccak256(abi.encodePacked(ids)), claimHash)), - "BatchActivation witness)BatchActivation(uint256[] ids,BatchCompact compact)BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256[2][] idsAndAmounts,CompactWitness witness)CompactWitness(uint256 witnessArgument)TokenPermissions(address token,uint256 amount)", - signature - ) - ); - - uint256[] memory returnedIds = theCompact.depositAndRegister{ value: amount }( - swapper, - tokenPermissions, - nonce, - deadline, - lockTag, - claimHash, - CompactCategory.BatchCompact, - witnessTypestring, - signature - ); - vm.snapshotGasLastCall("batchDepositAndRegisterWithWitnessViaPermit2"); - assertEq(returnedIds.length, 3); - assertEq(returnedIds[0], id); - assertEq(returnedIds[1], anotherId); - assertEq(returnedIds[2], aThirdId); - - assertEq(theCompact.balanceOf(swapper, id), amount); - assertEq(theCompact.balanceOf(swapper, anotherId), anotherAmount); - assertEq(theCompact.balanceOf(swapper, aThirdId), aThirdAmount); - - (bool isActive, uint256 registeredAt) = theCompact.getRegistrationStatus(swapper, claimHash, typehash); - assert(isActive); - assertEq(registeredAt, block.timestamp); - - claimHash = keccak256( - abi.encode( - keccak256( - "BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256[2][] idsAndAmounts,CompactWitness witness)CompactWitness(uint256 witnessArgument)" - ), - arbiter, - swapper, - nonce, - expires, - keccak256(abi.encodePacked(idsAndAmounts)), - witness - ) - ); - - digest = keccak256(abi.encodePacked(bytes2(0x1901), theCompact.DOMAIN_SEPARATOR(), claimHash)); - - (r, vs) = vm.signCompact(swapperPrivateKey, digest); - bytes memory sponsorSignature = abi.encodePacked(r, vs); - - (r, vs) = vm.signCompact(allocatorPrivateKey, digest); - bytes memory allocatorData = abi.encodePacked(r, vs); - - uint256 claimantOne = abi.decode(abi.encodePacked(bytes12(bytes32(id)), recipientOne), (uint256)); - uint256 claimantTwo = abi.decode(abi.encodePacked(bytes12(bytes32(id)), recipientTwo), (uint256)); - uint256 claimantThree = abi.decode(abi.encodePacked(bytes12(bytes32(anotherId)), recipientOne), (uint256)); - uint256 claimantFour = abi.decode(abi.encodePacked(bytes12(bytes32(aThirdId)), recipientTwo), (uint256)); - - SplitBatchClaimComponent[] memory claims = new SplitBatchClaimComponent[](3); - SplitComponent[] memory portions = new SplitComponent[](2); - portions[0] = SplitComponent({ claimant: claimantOne, amount: amountOne }); - portions[1] = SplitComponent({ claimant: claimantTwo, amount: amountTwo }); - claims[0] = SplitBatchClaimComponent({ id: id, allocatedAmount: amount, portions: portions }); - SplitComponent[] memory anotherPortion = new SplitComponent[](1); - anotherPortion[0] = SplitComponent({ claimant: claimantThree, amount: anotherAmount }); - claims[1] = - SplitBatchClaimComponent({ id: anotherId, allocatedAmount: anotherAmount, portions: anotherPortion }); - SplitComponent[] memory aThirdPortion = new SplitComponent[](1); - aThirdPortion[0] = SplitComponent({ claimant: claimantFour, amount: aThirdAmount }); - claims[2] = SplitBatchClaimComponent({ id: aThirdId, allocatedAmount: aThirdAmount, portions: aThirdPortion }); - - BatchClaim memory claim = - BatchClaim(allocatorData, sponsorSignature, swapper, nonce, expires, witness, witnessTypestring, claims); - - vm.prank(arbiter); - (bytes32 returnedClaimHash) = theCompact.claim(claim); - vm.snapshotGasLastCall("batchClaimRegisteredWithDepositWithWitness"); - assertEq(returnedClaimHash, claimHash); - - assertEq(address(theCompact).balance, amount); - assertEq(token.balanceOf(address(theCompact)), anotherAmount); - assertEq(anotherToken.balanceOf(address(theCompact)), aThirdAmount); - - assertEq(theCompact.balanceOf(recipientOne, id), amountOne); - assertEq(theCompact.balanceOf(recipientTwo, id), amountTwo); - assertEq(theCompact.balanceOf(recipientOne, anotherId), anotherAmount); - assertEq(theCompact.balanceOf(recipientTwo, aThirdId), aThirdAmount); - } - - function test_splitBatchClaimWithWitness() public { - uint256 amount = 1e18; - uint256 anotherAmount = 1e18; - uint256 aThirdAmount = 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); - uint96 allocatorId = theCompact.__registerAllocator(allocator, ""); - - bytes12 lockTag = bytes12( - bytes32( - (uint256(Scope.Multichain) << 255) | (uint256(ResetPeriod.TenMinutes) << 252) - | (uint256(allocatorId) << 160) - ) - ); - - vm.startPrank(swapper); - uint256 id = theCompact.deposit{ value: amount }(lockTag, swapper); - - uint256 anotherId = theCompact.deposit(address(token), lockTag, anotherAmount, swapper); - assertEq(theCompact.balanceOf(swapper, anotherId), anotherAmount); - - uint256 aThirdId = theCompact.deposit(address(anotherToken), lockTag, 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 allocatorData = abi.encodePacked(r, vs); - - uint256 claimantOne = abi.decode(abi.encodePacked(bytes12(bytes32(id)), recipientOne), (uint256)); - uint256 claimantTwo = abi.decode(abi.encodePacked(bytes12(bytes32(id)), recipientTwo), (uint256)); - uint256 claimantThree = abi.decode(abi.encodePacked(bytes12(bytes32(anotherId)), recipientOne), (uint256)); - uint256 claimantFour = abi.decode(abi.encodePacked(bytes12(bytes32(aThirdId)), recipientTwo), (uint256)); + // Create signatures for claim + { + bytes32 digest = _createDigest(theCompact.DOMAIN_SEPARATOR(), claimHash); + + { + bytes32 r; + bytes32 vs; + (r, vs) = vm.signCompact(swapperPrivateKey, digest); + claim.sponsorSignature = abi.encodePacked(r, vs); + } + + { + bytes32 r; + bytes32 vs; + (r, vs) = vm.signCompact(allocatorPrivateKey, digest); + claim.allocatorData = abi.encodePacked(r, vs); + } + } - SplitBatchClaimComponent[] memory claims = new SplitBatchClaimComponent[](3); - SplitComponent[] memory portions = new SplitComponent[](2); - portions[0] = SplitComponent({ claimant: claimantOne, amount: amountOne }); - portions[1] = SplitComponent({ claimant: claimantTwo, amount: amountTwo }); - claims[0] = SplitBatchClaimComponent({ id: id, allocatedAmount: amount, portions: portions }); - SplitComponent[] memory anotherPortion = new SplitComponent[](1); - anotherPortion[0] = SplitComponent({ claimant: claimantThree, amount: anotherAmount }); - claims[1] = - SplitBatchClaimComponent({ id: anotherId, allocatedAmount: anotherAmount, portions: anotherPortion }); - SplitComponent[] memory aThirdPortion = new SplitComponent[](1); - aThirdPortion[0] = SplitComponent({ claimant: claimantFour, amount: aThirdAmount }); - claims[2] = SplitBatchClaimComponent({ id: aThirdId, allocatedAmount: aThirdAmount, portions: aThirdPortion }); + // Create claim components + { + SplitBatchClaimComponent[] memory claims = new SplitBatchClaimComponent[](3); + { + uint256 claimantOne = abi.decode( + abi.encodePacked(bytes12(bytes32(ids[0])), 0x1111111111111111111111111111111111111111), (uint256) + ); + uint256 claimantTwo = abi.decode( + abi.encodePacked(bytes12(bytes32(ids[0])), 0x3333333333333333333333333333333333333333), (uint256) + ); + uint256 claimantThree = abi.decode( + abi.encodePacked(bytes12(bytes32(ids[1])), 0x1111111111111111111111111111111111111111), (uint256) + ); + uint256 claimantFour = abi.decode( + abi.encodePacked(bytes12(bytes32(ids[2])), 0x3333333333333333333333333333333333333333), (uint256) + ); + + { + SplitComponent[] memory portions = new SplitComponent[](2); + portions[0] = SplitComponent({ claimant: claimantOne, amount: 4e17 }); + portions[1] = SplitComponent({ claimant: claimantTwo, amount: 6e17 }); + claims[0] = SplitBatchClaimComponent({ id: ids[0], allocatedAmount: 1e18, portions: portions }); + } + + { + SplitComponent[] memory anotherPortion = new SplitComponent[](1); + anotherPortion[0] = SplitComponent({ claimant: claimantThree, amount: 1e18 }); + claims[1] = + SplitBatchClaimComponent({ id: ids[1], allocatedAmount: 1e18, portions: anotherPortion }); + } + + { + SplitComponent[] memory aThirdPortion = new SplitComponent[](1); + aThirdPortion[0] = SplitComponent({ claimant: claimantFour, amount: 1e18 }); + claims[2] = SplitBatchClaimComponent({ id: ids[2], allocatedAmount: 1e18, portions: aThirdPortion }); + } + } - BatchClaim memory claim = - BatchClaim(allocatorData, sponsorSignature, swapper, nonce, expires, witness, witnessTypestring, claims); + claim.claims = claims; + } - vm.prank(arbiter); - (bytes32 returnedClaimHash) = theCompact.claim(claim); - vm.snapshotGasLastCall("splitBatchClaimWithWitness"); - assertEq(returnedClaimHash, claimHash); + // Execute claim + bytes32 returnedClaimHash; + { + vm.prank(0x2222222222222222222222222222222222222222); + returnedClaimHash = theCompact.claim(claim); + vm.snapshotGasLastCall("batchClaimRegisteredWithDepositWithWitness"); + assertEq(returnedClaimHash, claimHash); + } - assertEq(address(theCompact).balance, amount); - assertEq(token.balanceOf(address(theCompact)), anotherAmount); - assertEq(anotherToken.balanceOf(address(theCompact)), aThirdAmount); + // Verify balances + assertEq(address(theCompact).balance, 1e18); + assertEq(token.balanceOf(address(theCompact)), 1e18); + assertEq(anotherToken.balanceOf(address(theCompact)), 1e18); - assertEq(theCompact.balanceOf(recipientOne, id), amountOne); - assertEq(theCompact.balanceOf(recipientTwo, id), amountTwo); - assertEq(theCompact.balanceOf(recipientOne, anotherId), anotherAmount); - assertEq(theCompact.balanceOf(recipientTwo, aThirdId), aThirdAmount); + assertEq(theCompact.balanceOf(0x1111111111111111111111111111111111111111, ids[0]), 4e17); + assertEq(theCompact.balanceOf(0x3333333333333333333333333333333333333333, ids[0]), 6e17); + assertEq(theCompact.balanceOf(0x1111111111111111111111111111111111111111, ids[1]), 1e18); + assertEq(theCompact.balanceOf(0x3333333333333333333333333333333333333333, ids[2]), 1e18); } - function test_splitMultichainClaimWithWitness() public { - ResetPeriod resetPeriod = ResetPeriod.TenMinutes; - Scope scope = Scope.Multichain; - uint256 amount = 1e18; - uint256 anotherAmount = 1e18; - uint256 nonce = 0; - uint256 expires = block.timestamp + 1000; - address arbiter = 0x2222222222222222222222222222222222222222; - uint256 anotherChainId = 7171717; - - address recipientOne = 0x1111111111111111111111111111111111111111; - address recipientTwo = 0x3333333333333333333333333333333333333333; - uint256 amountOne = 4e17; - uint256 amountTwo = 6e17; - - vm.prank(allocator); - uint96 allocatorId = theCompact.__registerAllocator(allocator, ""); - - bytes12 lockTag = - bytes12(bytes32((uint256(scope) << 255) | (uint256(resetPeriod) << 252) | (uint256(allocatorId) << 160))); - - vm.startPrank(swapper); - uint256 id = theCompact.deposit{ value: amount }(lockTag, swapper); - uint256 anotherId = theCompact.deposit(address(token), lockTag, 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( - "Segment(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( - "Segment(address arbiter,uint256 chainId,uint256[2][] idsAndAmounts,Witness witness)Witness(uint256 witnessArgument)" - ), - arbiter, - anotherChainId, - keccak256(abi.encodePacked(idsAndAmountsTwo)), - witness - ) - ); - - bytes32 allocationHashThree = keccak256( - abi.encode( - keccak256( - "Segment(address arbiter,uint256 chainId,uint256[2][] idsAndAmounts,Witness witness)Witness(uint256 witnessArgument)" - ), - arbiter, - 41414141, - keccak256(abi.encodePacked(idsAndAmountsTwo)), - witness - ) - ); - - bytes32 claimHash = keccak256( - abi.encode( - keccak256( - "MultichainCompact(address sponsor,uint256 nonce,uint256 expires,Segment[] segments)Segment(address arbiter,uint256 chainId,uint256[2][] idsAndAmounts,Witness witness)Witness(uint256 witnessArgument)" - ), - swapper, - nonce, - expires, - keccak256(abi.encodePacked(allocationHashOne, allocationHashTwo, allocationHashThree)) - ) - ); - - 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); - - (r, vs) = vm.signCompact(allocatorPrivateKey, digest); - bytes memory allocatorData = abi.encodePacked(r, vs); - - bytes32[] memory additionalChains = new bytes32[](2); - additionalChains[0] = allocationHashTwo; - additionalChains[1] = allocationHashThree; + function test_splitClaimWithWitness() public { + // Setup test parameters + TestParams memory params; + params.resetPeriod = ResetPeriod.TenMinutes; + params.scope = Scope.Multichain; + params.nonce = 0; + params.deadline = block.timestamp + 1000; + + // Initialize claim + Claim memory claim; + claim.sponsor = swapper; + claim.nonce = params.nonce; + claim.expires = block.timestamp + 1000; + claim.allocatedAmount = 1e18; + claim.witnessTypestring = witnessTypestring; + + // Register allocator and make deposit + { + bytes12 lockTag; + { + uint96 allocatorId; + (allocatorId, lockTag) = _registerAllocator(allocator); + } - uint256 claimantOne = abi.decode(abi.encodePacked(bytes12(bytes32(id)), recipientOne), (uint256)); - uint256 claimantTwo = abi.decode(abi.encodePacked(bytes12(bytes32(id)), recipientTwo), (uint256)); + claim.id = _makeDeposit(swapper, 1e18, lockTag); - SplitComponent memory splitOne = SplitComponent({ claimant: claimantOne, amount: amountOne }); + // Create witness + uint256 witnessArgument = 234; + claim.witness = keccak256(abi.encode(witnessArgument)); + } - SplitComponent memory splitTwo = SplitComponent({ claimant: claimantTwo, amount: amountTwo }); + // Create claim hash + bytes32 claimHash; + { + CreateClaimHashWithWitnessArgs memory args; + { + args.typehash = compactWithWitnessTypehash; + args.arbiter = 0x2222222222222222222222222222222222222222; + args.sponsor = claim.sponsor; + args.nonce = claim.nonce; + args.expires = claim.expires; + args.id = claim.id; + args.amount = claim.allocatedAmount; + args.witness = claim.witness; + } + + claimHash = _createClaimHashWithWitness(args); + } - SplitComponent[] memory recipients = new SplitComponent[](2); - recipients[0] = splitOne; - recipients[1] = splitTwo; + // Create signatures + { + bytes32 digest = _createDigest(theCompact.DOMAIN_SEPARATOR(), claimHash); + + { + bytes32 r; + bytes32 vs; + (r, vs) = vm.signCompact(swapperPrivateKey, digest); + claim.sponsorSignature = abi.encodePacked(r, vs); + } + + { + bytes32 r; + bytes32 vs; + (r, vs) = vm.signCompact(allocatorPrivateKey, digest); + claim.allocatorData = abi.encodePacked(r, vs); + } + } - MultichainClaim memory claim = MultichainClaim( - allocatorData, - sponsorSignature, - swapper, - nonce, - expires, - witness, - witnessTypestring, - additionalChains, - id, - amount, - recipients - ); + // Create split components + { + uint256 claimantOne = abi.decode( + abi.encodePacked(bytes12(bytes32(claim.id)), 0x1111111111111111111111111111111111111111), (uint256) + ); + uint256 claimantTwo = abi.decode( + abi.encodePacked(bytes12(bytes32(claim.id)), 0x3333333333333333333333333333333333333333), (uint256) + ); + + SplitComponent[] memory recipients; + { + SplitComponent memory splitOne = SplitComponent({ claimant: claimantOne, amount: 4e17 }); + SplitComponent memory splitTwo = SplitComponent({ claimant: claimantTwo, amount: 6e17 }); + + recipients = new SplitComponent[](2); + recipients[0] = splitOne; + recipients[1] = splitTwo; + + claim.claimants = recipients; + } + } - uint256 snapshotId = vm.snapshotState(); - vm.prank(arbiter); - (bytes32 returnedClaimHash) = theCompact.claim(claim); - vm.snapshotGasLastCall("splitMultichainClaimWithWitness"); - assertEq(returnedClaimHash, claimHash); + // Execute claim + bytes32 returnedClaimHash; + { + vm.prank(0x2222222222222222222222222222222222222222); + returnedClaimHash = theCompact.claim(claim); + vm.snapshotGasLastCall("splitClaimWithWitness"); + assertEq(returnedClaimHash, claimHash); + } - assertEq(address(theCompact).balance, amount); - assertEq(recipientOne.balance, 0); - assertEq(recipientTwo.balance, 0); - assertEq(theCompact.balanceOf(recipientOne, id), amountOne); - assertEq(theCompact.balanceOf(recipientTwo, id), amountTwo); - 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, claimHash)); - - (r, vs) = vm.signCompact(allocatorPrivateKey, digest); - bytes memory exogenousallocatorData = abi.encodePacked(r, vs); - - additionalChains[0] = allocationHashOne; - additionalChains[1] = allocationHashThree; - uint256 chainIndex = 0; - - ExogenousMultichainClaim memory anotherClaim = ExogenousMultichainClaim( - exogenousallocatorData, - sponsorSignature, - swapper, - nonce, - expires, - witness, - witnessTypestring, - additionalChains, - chainIndex, - notarizedChainId, - anotherId, - anotherAmount, - recipients - ); + // Verify balances + { + assertEq(address(theCompact).balance, 1e18); + assertEq(0x1111111111111111111111111111111111111111.balance, 0); + assertEq(0x3333333333333333333333333333333333333333.balance, 0); + assertEq(theCompact.balanceOf(swapper, claim.id), 0); - vm.prank(arbiter); - (returnedClaimHash) = theCompact.claim(anotherClaim); - vm.snapshotGasLastCall("exogenousSplitMultichainClaimWithWitness"); - assertEq(returnedClaimHash, claimHash); + assertEq(theCompact.balanceOf(0x1111111111111111111111111111111111111111, claim.id), 4e17); + assertEq(theCompact.balanceOf(0x3333333333333333333333333333333333333333, claim.id), 6e17); + } + } - assertEq(theCompact.balanceOf(swapper, anotherId), 0); - assertEq(theCompact.balanceOf(recipientOne, anotherId), amountOne); - assertEq(theCompact.balanceOf(recipientTwo, anotherId), amountTwo); + function test_splitBatchClaimWithWitness() public { + // Setup test parameters + TestParams memory params; + params.nonce = 0; + params.deadline = block.timestamp + 1000; + + // Initialize batch claim + BatchClaim memory claim; + claim.sponsor = swapper; + claim.nonce = params.nonce; + claim.expires = block.timestamp + 1000; + claim.witnessTypestring = witnessTypestring; + + // Register allocator and make deposits + uint256 id; + uint256 anotherId; + uint256 aThirdId; + { + bytes12 lockTag; + { + uint96 allocatorId; + (allocatorId, lockTag) = _registerAllocator(allocator); + } + + id = _makeDeposit(swapper, 1e18, lockTag); + anotherId = _makeDeposit(swapper, address(token), 1e18, lockTag); + aThirdId = _makeDeposit(swapper, address(anotherToken), 1e18, lockTag); + + assertEq(theCompact.balanceOf(swapper, id), 1e18); + assertEq(theCompact.balanceOf(swapper, anotherId), 1e18); + assertEq(theCompact.balanceOf(swapper, aThirdId), 1e18); + } - // change back - vm.chainId(notarizedChainId); - assertEq(block.chainid, notarizedChainId); - } + // Create idsAndAmounts and witness + uint256[2][] memory idsAndAmounts; + { + idsAndAmounts = new uint256[2][](3); + idsAndAmounts[0] = [id, 1e18]; + idsAndAmounts[1] = [anotherId, 1e18]; + idsAndAmounts[2] = [aThirdId, 1e18]; - function test_splitBatchMultichainClaimWithWitness() public { - uint256 amount = 1e18; - uint256 anotherAmount = 1e18; - uint256 aThirdAmount = 1e18; - uint256 expires = block.timestamp + 1000; - address arbiter = 0x2222222222222222222222222222222222222222; + uint256 witnessArgument = 234; + claim.witness = keccak256(abi.encode(witnessArgument)); + } - address recipientOne = 0x1111111111111111111111111111111111111111; - address recipientTwo = 0x3333333333333333333333333333333333333333; - uint256 amountOne = 4e17; - uint256 amountTwo = 6e17; + // Create claim hash + bytes32 claimHash; + { + CreateBatchClaimHashWithWitnessArgs memory args; + { + args.typehash = keccak256( + "BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256[2][] idsAndAmounts,CompactWitness witness)CompactWitness(uint256 witnessArgument)" + ); + args.arbiter = 0x2222222222222222222222222222222222222222; + args.sponsor = claim.sponsor; + args.nonce = claim.nonce; + args.expires = claim.expires; + args.idsAndAmountsHash = keccak256(abi.encodePacked(idsAndAmounts)); + args.witness = claim.witness; + } + + claimHash = _createBatchClaimHashWithWitness(args); + } - vm.prank(allocator); - uint96 allocatorId = theCompact.__registerAllocator(allocator, ""); + // Create signatures + { + bytes32 digest = _createDigest(theCompact.DOMAIN_SEPARATOR(), claimHash); + + { + bytes32 r; + bytes32 vs; + (r, vs) = vm.signCompact(swapperPrivateKey, digest); + claim.sponsorSignature = abi.encodePacked(r, vs); + } + + { + bytes32 r; + bytes32 vs; + (r, vs) = vm.signCompact(allocatorPrivateKey, digest); + claim.allocatorData = abi.encodePacked(r, vs); + } + } - bytes12 lockTag = bytes12( - bytes32( - (uint256(Scope.Multichain) << 255) | (uint256(ResetPeriod.TenMinutes) << 252) - | (uint256(allocatorId) << 160) - ) - ); + // Create batch claim components + { + SplitBatchClaimComponent[] memory claims = new SplitBatchClaimComponent[](3); - vm.startPrank(swapper); - uint256 id = theCompact.deposit{ value: amount }(lockTag, swapper); + // First claim component + { + uint256 claimantOne = abi.decode( + abi.encodePacked(bytes12(bytes32(id)), 0x1111111111111111111111111111111111111111), (uint256) + ); + uint256 claimantTwo = abi.decode( + abi.encodePacked(bytes12(bytes32(id)), 0x3333333333333333333333333333333333333333), (uint256) + ); - uint256 anotherId = theCompact.deposit(address(token), lockTag, anotherAmount, swapper); - assertEq(theCompact.balanceOf(swapper, anotherId), anotherAmount); + SplitComponent[] memory portions = new SplitComponent[](2); + portions[0] = SplitComponent({ claimant: claimantOne, amount: 4e17 }); + portions[1] = SplitComponent({ claimant: claimantTwo, amount: 6e17 }); - uint256 aThirdId = theCompact.deposit(address(anotherToken), lockTag, aThirdAmount, swapper); - assertEq(theCompact.balanceOf(swapper, aThirdId), aThirdAmount); + claims[0] = SplitBatchClaimComponent({ id: id, allocatedAmount: 1e18, portions: portions }); + } - vm.stopPrank(); + // Second claim component + { + uint256 claimantThree = abi.decode( + abi.encodePacked(bytes12(bytes32(anotherId)), 0x1111111111111111111111111111111111111111), (uint256) + ); - assertEq(theCompact.balanceOf(swapper, id), amount); - assertEq(theCompact.balanceOf(swapper, anotherId), anotherAmount); - assertEq(theCompact.balanceOf(swapper, aThirdId), aThirdAmount); + SplitComponent[] memory anotherPortion = new SplitComponent[](1); + anotherPortion[0] = SplitComponent({ claimant: claimantThree, amount: 1e18 }); - uint256[2][] memory idsAndAmounts = new uint256[2][](3); - idsAndAmounts[0] = [id, amount]; - idsAndAmounts[1] = [anotherId, anotherAmount]; - idsAndAmounts[2] = [aThirdId, aThirdAmount]; + claims[1] = SplitBatchClaimComponent({ id: anotherId, allocatedAmount: 1e18, portions: anotherPortion }); + } - uint256 anotherChainId = 7171717; + // Third claim component + { + uint256 claimantFour = abi.decode( + abi.encodePacked(bytes12(bytes32(aThirdId)), 0x3333333333333333333333333333333333333333), (uint256) + ); - uint256[2][] memory idsAndAmountsOne = new uint256[2][](1); - idsAndAmountsOne[0] = [id, amount]; + SplitComponent[] memory aThirdPortion = new SplitComponent[](1); + aThirdPortion[0] = SplitComponent({ claimant: claimantFour, amount: 1e18 }); - uint256[2][] memory idsAndAmountsTwo = new uint256[2][](2); - idsAndAmountsTwo[0] = [anotherId, anotherAmount]; - idsAndAmountsTwo[1] = [aThirdId, aThirdAmount]; + claims[2] = SplitBatchClaimComponent({ id: aThirdId, allocatedAmount: 1e18, portions: aThirdPortion }); + } - uint256 witnessArgument = 234; - bytes32 witness = keccak256(abi.encode(witnessArgument)); + claim.claims = claims; + } - bytes32 allocationHashOne = keccak256( - abi.encode( - keccak256( - "Segment(address arbiter,uint256 chainId,uint256[2][] idsAndAmounts,Witness witness)Witness(uint256 witnessArgument)" - ), - arbiter, - block.chainid, - keccak256(abi.encodePacked(idsAndAmountsOne)), - witness - ) - ); + // Execute claim + bytes32 returnedClaimHash; + { + vm.prank(0x2222222222222222222222222222222222222222); + returnedClaimHash = theCompact.claim(claim); + vm.snapshotGasLastCall("splitBatchClaimWithWitness"); + assertEq(returnedClaimHash, claimHash); + } - bytes32 allocationHashTwo = keccak256( - abi.encode( - keccak256( - "Segment(address arbiter,uint256 chainId,uint256[2][] idsAndAmounts,Witness witness)Witness(uint256 witnessArgument)" - ), - arbiter, - anotherChainId, - keccak256(abi.encodePacked(idsAndAmountsTwo)), - witness - ) - ); + // Verify balances + { + assertEq(address(theCompact).balance, 1e18); + assertEq(token.balanceOf(address(theCompact)), 1e18); + assertEq(anotherToken.balanceOf(address(theCompact)), 1e18); + + assertEq(theCompact.balanceOf(0x1111111111111111111111111111111111111111, id), 4e17); + assertEq(theCompact.balanceOf(0x3333333333333333333333333333333333333333, id), 6e17); + assertEq(theCompact.balanceOf(0x1111111111111111111111111111111111111111, anotherId), 1e18); + assertEq(theCompact.balanceOf(0x3333333333333333333333333333333333333333, aThirdId), 1e18); + } + } - bytes32[] memory additionalChains = new bytes32[](1); - additionalChains[0] = allocationHashTwo; + function test_splitMultichainClaimWithWitness() public { + // Setup test parameters + TestParams memory params; + params.resetPeriod = ResetPeriod.TenMinutes; + params.scope = Scope.Multichain; + params.nonce = 0; + params.deadline = block.timestamp + 1000; + + // Initialize multichain claim + MultichainClaim memory claim; + claim.sponsor = swapper; + claim.nonce = params.nonce; + claim.expires = params.deadline; + claim.witnessTypestring = witnessTypestring; + + // Set up chain IDs + uint256 anotherChainId = 7171717; + uint256 thirdChainId = 41414141; - bytes32 claimHash = keccak256( - abi.encode( - keccak256( - "MultichainCompact(address sponsor,uint256 nonce,uint256 expires,Segment[] segments)Segment(address arbiter,uint256 chainId,uint256[2][] idsAndAmounts,Witness witness)Witness(uint256 witnessArgument)" - ), - swapper, - 0, - expires, - keccak256(abi.encodePacked(allocationHashOne, allocationHashTwo)) - ) - ); + // Register allocator and make deposits + uint256 id; + uint256 anotherId; + { + bytes12 lockTag; + { + uint96 allocatorId; + (allocatorId, lockTag) = _registerAllocator(allocator); + } - bytes32 initialDomainSeparator = theCompact.DOMAIN_SEPARATOR(); + id = _makeDeposit(swapper, 1e18, lockTag); + anotherId = _makeDeposit(swapper, address(token), 1e18, lockTag); - bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), theCompact.DOMAIN_SEPARATOR(), claimHash)); + claim.id = id; + claim.allocatedAmount = 1e18; + } - (bytes32 r, bytes32 vs) = vm.signCompact(swapperPrivateKey, digest); - bytes memory sponsorSignature = abi.encodePacked(r, vs); + // Create ids and amounts arrays + uint256[2][] memory idsAndAmountsOne; + uint256[2][] memory idsAndAmountsTwo; + { + idsAndAmountsOne = new uint256[2][](1); + idsAndAmountsOne[0] = [id, 1e18]; - (r, vs) = vm.signCompact(allocatorPrivateKey, digest); + idsAndAmountsTwo = new uint256[2][](1); + idsAndAmountsTwo[0] = [anotherId, 1e18]; + } - SplitBatchClaimComponent[] memory claims = new SplitBatchClaimComponent[](1); - SplitComponent[] memory recipients = new SplitComponent[](2); + // Create witness + { + uint256 witnessArgument = 234; + claim.witness = keccak256(abi.encode(witnessArgument)); + } + // Create allocation hashes + bytes32[] memory allocationHashes = new bytes32[](3); { - uint256 claimantOne = abi.decode(abi.encodePacked(bytes12(bytes32(id)), recipientOne), (uint256)); - uint256 claimantTwo = abi.decode(abi.encodePacked(bytes12(bytes32(id)), recipientTwo), (uint256)); + bytes32 segmentTypehash = keccak256( + "Segment(address arbiter,uint256 chainId,uint256[2][] idsAndAmounts,CompactWitness witness)CompactWitness(uint256 witnessArgument)" + ); + + allocationHashes[0] = keccak256( + abi.encode( + segmentTypehash, + 0x2222222222222222222222222222222222222222, // arbiter + block.chainid, + keccak256(abi.encodePacked(idsAndAmountsOne)), + claim.witness + ) + ); + + allocationHashes[1] = keccak256( + abi.encode( + segmentTypehash, + 0x2222222222222222222222222222222222222222, // arbiter + anotherChainId, + keccak256(abi.encodePacked(idsAndAmountsTwo)), + claim.witness + ) + ); + + allocationHashes[2] = keccak256( + abi.encode( + segmentTypehash, + 0x2222222222222222222222222222222222222222, // arbiter + thirdChainId, + keccak256(abi.encodePacked(idsAndAmountsTwo)), + claim.witness + ) + ); + } - SplitComponent memory splitOne = SplitComponent({ claimant: claimantOne, amount: amountOne }); + // Create multichain claim hash + bytes32 claimHash; + { + bytes32 multichainTypehash = keccak256( + "MultichainCompact(address sponsor,uint256 nonce,uint256 expires,Segment[] segments)Segment(address arbiter,uint256 chainId,uint256[2][] idsAndAmounts,CompactWitness witness)CompactWitness(uint256 witnessArgument)" + ); + + claimHash = keccak256( + abi.encode( + multichainTypehash, + claim.sponsor, + claim.nonce, + claim.expires, + keccak256(abi.encodePacked(allocationHashes)) + ) + ); + } - SplitComponent memory splitTwo = SplitComponent({ claimant: claimantTwo, amount: amountTwo }); + // Store initial domain separator + bytes32 initialDomainSeparator = theCompact.DOMAIN_SEPARATOR(); - recipients[0] = splitOne; - recipients[1] = splitTwo; + // Create signatures + { + bytes32 digest = _createDigest(initialDomainSeparator, claimHash); + + { + bytes32 r; + bytes32 vs; + (r, vs) = vm.signCompact(swapperPrivateKey, digest); + claim.sponsorSignature = abi.encodePacked(r, vs); + } + + { + bytes32 r; + bytes32 vs; + (r, vs) = vm.signCompact(allocatorPrivateKey, digest); + claim.allocatorData = abi.encodePacked(r, vs); + } + } - claims[0] = SplitBatchClaimComponent({ id: id, allocatedAmount: amount, portions: recipients }); + // Set up additional chains + { + bytes32[] memory additionalChains = new bytes32[](2); + additionalChains[0] = allocationHashes[1]; + additionalChains[1] = allocationHashes[2]; + claim.additionalChains = additionalChains; } - BatchMultichainClaim memory claim; - claim.sponsorSignature = sponsorSignature; + // Create split components + { + uint256 claimantOne = abi.decode( + abi.encodePacked(bytes12(bytes32(id)), 0x1111111111111111111111111111111111111111), (uint256) + ); + uint256 claimantTwo = abi.decode( + abi.encodePacked(bytes12(bytes32(id)), 0x3333333333333333333333333333333333333333), (uint256) + ); + + SplitComponent[] memory recipients; + { + SplitComponent memory splitOne = SplitComponent({ claimant: claimantOne, amount: 4e17 }); + SplitComponent memory splitTwo = SplitComponent({ claimant: claimantTwo, amount: 6e17 }); + + recipients = new SplitComponent[](2); + recipients[0] = splitOne; + recipients[1] = splitTwo; + } + + claim.claimants = recipients; + } + // Execute claim and verify - first part { uint256 snapshotId = vm.snapshotState(); - claim.allocatorData = abi.encodePacked(r, vs); - claim.sponsor = swapper; - claim.nonce = 0; - claim.expires = expires; - claim.witness = witness; - claim.witnessTypestring = "Witness witness)Witness(uint256 witnessArgument)"; - claim.additionalChains = additionalChains; - claim.claims = claims; + { + vm.prank(0x2222222222222222222222222222222222222222); + bytes32 returnedClaimHash = theCompact.claim(claim); + vm.snapshotGasLastCall("splitMultichainClaimWithWitness"); + assertEq(returnedClaimHash, claimHash); - vm.prank(arbiter); - (bytes32 returnedClaimHash) = theCompact.claim(claim); - vm.snapshotGasLastCall("splitBatchMultichainClaimWithWitness"); - assertEq(returnedClaimHash, claimHash); + assertEq(address(theCompact).balance, 1e18); + assertEq(0x1111111111111111111111111111111111111111.balance, 0); + assertEq(0x3333333333333333333333333333333333333333.balance, 0); + assertEq(theCompact.balanceOf(0x1111111111111111111111111111111111111111, id), 4e17); + assertEq(theCompact.balanceOf(0x3333333333333333333333333333333333333333, id), 6e17); + } - 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); - vm.revertToStateAndDelete(snapshotId); + vm.revertToAndDelete(snapshotId); + } + + // Change to "new chain" and execute exogenous claim + { + // Save current chain ID and switch to another + uint256 notarizedChainId = abi.decode(abi.encode(block.chainid), (uint256)); + vm.chainId(anotherChainId); + assertEq(block.chainid, anotherChainId); + + // Get new domain separator + bytes32 anotherDomainSeparator = theCompact.DOMAIN_SEPARATOR(); + assert(initialDomainSeparator != anotherDomainSeparator); + + // Create exogenous allocator signature + bytes memory exogenousAllocatorData; + { + bytes32 digest = _createDigest(anotherDomainSeparator, claimHash); + + bytes32 r; + bytes32 vs; + (r, vs) = vm.signCompact(allocatorPrivateKey, digest); + exogenousAllocatorData = abi.encodePacked(r, vs); + } + + // Set up exogenous claim + ExogenousMultichainClaim memory anotherClaim; + { + bytes32[] memory additionalChains = new bytes32[](2); + additionalChains[0] = allocationHashes[0]; + additionalChains[1] = allocationHashes[2]; + + anotherClaim.allocatorData = exogenousAllocatorData; + anotherClaim.sponsorSignature = claim.sponsorSignature; + anotherClaim.sponsor = claim.sponsor; + anotherClaim.nonce = claim.nonce; + anotherClaim.expires = claim.expires; + anotherClaim.witness = claim.witness; + anotherClaim.witnessTypestring = claim.witnessTypestring; + anotherClaim.additionalChains = additionalChains; + anotherClaim.chainIndex = 0; + anotherClaim.notarizedChainId = notarizedChainId; // Changed from exogenousChainId to notarizedChainId + anotherClaim.id = anotherId; + anotherClaim.allocatedAmount = 1e18; + anotherClaim.claimants = claim.claimants; + } + + // Execute exogenous claim + { + vm.prank(0x2222222222222222222222222222222222222222); + bytes32 returnedClaimHash = theCompact.claim(anotherClaim); + vm.snapshotGasLastCall("exogenousSplitMultichainClaimWithWitness"); + assertEq(returnedClaimHash, claimHash); + + assertEq(theCompact.balanceOf(swapper, anotherId), 0); + assertEq(theCompact.balanceOf(0x1111111111111111111111111111111111111111, anotherId), 4e17); + assertEq(theCompact.balanceOf(0x3333333333333333333333333333333333333333, anotherId), 6e17); + } + + // Change back to original chain + vm.chainId(notarizedChainId); + assertEq(block.chainid, notarizedChainId); } + } + + function test_splitBatchMultichainClaimWithWitness() public { + // Setup test parameters + TestParams memory params; + params.deadline = block.timestamp + 1000; + + // Initialize batch multichain claim + BatchMultichainClaim memory claim; + claim.sponsor = swapper; + claim.nonce = 0; + claim.expires = params.deadline; + claim.witnessTypestring = witnessTypestring; + + // Set up chain IDs + uint256 anotherChainId = 7171717; - // 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); + // Register allocator and make deposits + uint256[] memory ids = new uint256[](3); + { + bytes12 lockTag; + { + uint96 allocatorId; + (allocatorId, lockTag) = _registerAllocator(allocator); + } - assert(initialDomainSeparator != theCompact.DOMAIN_SEPARATOR()); + ids[0] = _makeDeposit(swapper, 1e18, lockTag); + ids[1] = _makeDeposit(swapper, address(token), 1e18, lockTag); + assertEq(theCompact.balanceOf(swapper, ids[1]), 1e18); - digest = keccak256(abi.encodePacked(bytes2(0x1901), theCompact.DOMAIN_SEPARATOR(), claimHash)); + ids[2] = _makeDeposit(swapper, address(anotherToken), 1e18, lockTag); + assertEq(theCompact.balanceOf(swapper, ids[2]), 1e18); - (r, vs) = vm.signCompact(allocatorPrivateKey, digest); + vm.stopPrank(); - additionalChains[0] = allocationHashOne; + assertEq(theCompact.balanceOf(swapper, ids[0]), 1e18); + assertEq(theCompact.balanceOf(swapper, ids[1]), 1e18); + assertEq(theCompact.balanceOf(swapper, ids[2]), 1e18); + } + // Create idsAndAmounts arrays + uint256[2][] memory idsAndAmountsOne; + uint256[2][] memory idsAndAmountsTwo; { - uint256 claimantOne = abi.decode(abi.encodePacked(bytes12(bytes32(anotherId)), recipientOne), (uint256)); - uint256 claimantTwo = abi.decode(abi.encodePacked(bytes12(bytes32(anotherId)), recipientTwo), (uint256)); - recipients[0].claimant = claimantOne; - recipients[1].claimant = claimantTwo; + idsAndAmountsOne = new uint256[2][](1); + idsAndAmountsOne[0] = [ids[0], 1e18]; + + idsAndAmountsTwo = new uint256[2][](2); + idsAndAmountsTwo[0] = [ids[1], 1e18]; + idsAndAmountsTwo[1] = [ids[2], 1e18]; + } - uint256 claimantThree = abi.decode(abi.encodePacked(bytes12(bytes32(aThirdId)), recipientOne), (uint256)); + // Create witness + { + uint256 witnessArgument = 234; + claim.witness = keccak256(abi.encode(witnessArgument)); + } - SplitComponent memory anotherSplit = SplitComponent({ claimant: claimantThree, amount: anotherAmount }); + // Create allocation hashes + bytes32 allocationHashOne; + bytes32 allocationHashTwo; + { + bytes32 segmentTypehash = keccak256( + "Segment(address arbiter,uint256 chainId,uint256[2][] idsAndAmounts,CompactWitness witness)CompactWitness(uint256 witnessArgument)" + ); + + allocationHashOne = keccak256( + abi.encode( + segmentTypehash, + 0x2222222222222222222222222222222222222222, // arbiter + block.chainid, + keccak256(abi.encodePacked(idsAndAmountsOne)), + claim.witness + ) + ); + + allocationHashTwo = keccak256( + abi.encode( + segmentTypehash, + 0x2222222222222222222222222222222222222222, // arbiter + anotherChainId, + keccak256(abi.encodePacked(idsAndAmountsTwo)), + claim.witness + ) + ); + } - SplitComponent[] memory anotherRecipient = new SplitComponent[](1); - anotherRecipient[0] = anotherSplit; + // Create additional chains + { + bytes32[] memory additionalChains = new bytes32[](1); + additionalChains[0] = allocationHashTwo; + claim.additionalChains = additionalChains; + } - claims = new SplitBatchClaimComponent[](2); - claims[0] = - SplitBatchClaimComponent({ id: anotherId, allocatedAmount: anotherAmount, portions: anotherRecipient }); - claims[1] = SplitBatchClaimComponent({ id: aThirdId, allocatedAmount: aThirdAmount, portions: recipients }); + // Create multichain claim hash + bytes32 claimHash; + { + bytes32 multichainTypehash = keccak256( + "MultichainCompact(address sponsor,uint256 nonce,uint256 expires,Segment[] segments)Segment(address arbiter,uint256 chainId,uint256[2][] idsAndAmounts,CompactWitness witness)CompactWitness(uint256 witnessArgument)" + ); + + claimHash = keccak256( + abi.encode( + multichainTypehash, + claim.sponsor, + claim.nonce, + claim.expires, + keccak256(abi.encodePacked(allocationHashOne, allocationHashTwo)) + ) + ); } - ExogenousBatchMultichainClaim memory anotherClaim; - anotherClaim.allocatorData = abi.encodePacked(r, vs); - anotherClaim.sponsorSignature = sponsorSignature; - anotherClaim.witnessTypestring = "Witness witness)Witness(uint256 witnessArgument)"; - anotherClaim.additionalChains = additionalChains; + // Store initial domain separator + bytes32 initialDomainSeparator = theCompact.DOMAIN_SEPARATOR(); + // Create signatures { - anotherClaim.sponsor = swapper; - anotherClaim.nonce = 0; - anotherClaim.expires = expires; - anotherClaim.witness = witness; - anotherClaim.chainIndex = 0; - anotherClaim.notarizedChainId = notarizedChainId; - anotherClaim.claims = claims; + bytes32 digest = _createDigest(theCompact.DOMAIN_SEPARATOR(), claimHash); + + { + (bytes32 r, bytes32 vs) = vm.signCompact(swapperPrivateKey, digest); + claim.sponsorSignature = abi.encodePacked(r, vs); + (r, vs) = vm.signCompact(allocatorPrivateKey, digest); + claim.allocatorData = abi.encodePacked(r, vs); + } } + // Create batch claim components + SplitBatchClaimComponent[] memory claims = new SplitBatchClaimComponent[](1); { - vm.prank(arbiter); - (bytes32 returnedClaimHash) = theCompact.claim(anotherClaim); - vm.snapshotGasLastCall("exogenousSplitBatchMultichainClaimWithWitness"); - assertEq(returnedClaimHash, claimHash); + SplitComponent[] memory recipients = new SplitComponent[](2); + { + uint256 claimantOne = abi.decode( + abi.encodePacked(bytes12(bytes32(ids[0])), 0x1111111111111111111111111111111111111111), (uint256) + ); + uint256 claimantTwo = abi.decode( + abi.encodePacked(bytes12(bytes32(ids[0])), 0x3333333333333333333333333333333333333333), (uint256) + ); + + SplitComponent memory splitOne = SplitComponent({ claimant: claimantOne, amount: 4e17 }); + SplitComponent memory splitTwo = SplitComponent({ claimant: claimantTwo, amount: 6e17 }); + + recipients[0] = splitOne; + recipients[1] = splitTwo; + } + claims[0] = SplitBatchClaimComponent({ id: ids[0], allocatedAmount: 1e18, portions: recipients }); } - assertEq(theCompact.balanceOf(swapper, anotherId), 0); - assertEq(theCompact.balanceOf(recipientOne, anotherId), anotherAmount); - assertEq(theCompact.balanceOf(recipientOne, aThirdId), amountOne); - assertEq(theCompact.balanceOf(recipientTwo, aThirdId), amountTwo); + // Execute claim and verify - first part + { + uint256 snapshotId = vm.snapshotState(); + + { + claim.claims = claims; + + vm.prank(0x2222222222222222222222222222222222222222); + bytes32 returnedClaimHash = theCompact.claim(claim); + vm.snapshotGasLastCall("splitBatchMultichainClaimWithWitness"); + assertEq(returnedClaimHash, claimHash); + + assertEq(address(theCompact).balance, 1e18); + assertEq(0x1111111111111111111111111111111111111111.balance, 0); + assertEq(0x3333333333333333333333333333333333333333.balance, 0); + assertEq(theCompact.balanceOf(swapper, ids[0]), 0); + assertEq(theCompact.balanceOf(0x1111111111111111111111111111111111111111, ids[0]), 4e17); + assertEq(theCompact.balanceOf(0x3333333333333333333333333333333333333333, ids[0]), 6e17); + } - // change back - vm.chainId(notarizedChainId); - assertEq(block.chainid, notarizedChainId); + vm.revertToStateAndDelete(snapshotId); + } + + // Change to "new chain" and execute exogenous claim + { + // Save current chain ID and switch to another + uint256 notarizedChainId = abi.decode(abi.encode(block.chainid), (uint256)); + vm.chainId(anotherChainId); + assertEq(block.chainid, anotherChainId); + + assert(initialDomainSeparator != theCompact.DOMAIN_SEPARATOR()); + + // Prepare additional chains + bytes32[] memory additionalChains = new bytes32[](1); + additionalChains[0] = allocationHashOne; + + // Create new recipients for different IDs + SplitBatchClaimComponent[] memory newClaims = new SplitBatchClaimComponent[](2); + { + // First claim component + { + uint256 claimantOne = abi.decode( + abi.encodePacked(bytes12(bytes32(ids[1])), 0x1111111111111111111111111111111111111111), + (uint256) + ); + + SplitComponent[] memory anotherRecipient = new SplitComponent[](1); + anotherRecipient[0] = SplitComponent({ claimant: claimantOne, amount: 1e18 }); + + newClaims[0] = + SplitBatchClaimComponent({ id: ids[1], allocatedAmount: 1e18, portions: anotherRecipient }); + } + + // Second claim component + { + uint256 claimantOne = abi.decode( + abi.encodePacked(bytes12(bytes32(ids[2])), 0x1111111111111111111111111111111111111111), + (uint256) + ); + uint256 claimantTwo = abi.decode( + abi.encodePacked(bytes12(bytes32(ids[2])), 0x3333333333333333333333333333333333333333), + (uint256) + ); + + SplitComponent[] memory aThirdPortion = new SplitComponent[](2); + aThirdPortion[0] = SplitComponent({ claimant: claimantOne, amount: 4e17 }); + aThirdPortion[1] = SplitComponent({ claimant: claimantTwo, amount: 6e17 }); + + newClaims[1] = + SplitBatchClaimComponent({ id: ids[2], allocatedAmount: 1e18, portions: aThirdPortion }); + } + } + + // Set up exogenous claim + ExogenousBatchMultichainClaim memory anotherClaim; + + // Create exogenous allocator signature + { + bytes32 digest = _createDigest(theCompact.DOMAIN_SEPARATOR(), claimHash); + (bytes32 r, bytes32 vs) = vm.signCompact(allocatorPrivateKey, digest); + anotherClaim.allocatorData = abi.encodePacked(r, vs); + } + { + anotherClaim.sponsorSignature = claim.sponsorSignature; + anotherClaim.sponsor = claim.sponsor; + anotherClaim.nonce = claim.nonce; + anotherClaim.expires = claim.expires; + anotherClaim.witness = claim.witness; + anotherClaim.witnessTypestring = "CompactWitness witness)CompactWitness(uint256 witnessArgument)"; + anotherClaim.additionalChains = additionalChains; + anotherClaim.chainIndex = 0; + anotherClaim.notarizedChainId = notarizedChainId; + anotherClaim.claims = newClaims; + } + + // Execute exogenous claim + { + vm.prank(0x2222222222222222222222222222222222222222); + bytes32 returnedClaimHash = theCompact.claim(anotherClaim); + vm.snapshotGasLastCall("exogenousSplitBatchMultichainClaimWithWitness"); + assertEq(returnedClaimHash, claimHash); + } + + // Verify balances + assertEq(theCompact.balanceOf(swapper, ids[1]), 0); + assertEq(theCompact.balanceOf(0x1111111111111111111111111111111111111111, ids[1]), 1e18); + assertEq(theCompact.balanceOf(0x1111111111111111111111111111111111111111, ids[2]), 4e17); + assertEq(theCompact.balanceOf(0x3333333333333333333333333333333333333333, ids[2]), 6e17); + + // Change back to original chain + vm.chainId(notarizedChainId); + assertEq(block.chainid, notarizedChainId); + } } function test_claimAndWithdraw_withEmissary() public { - ResetPeriod resetPeriod = ResetPeriod.TenMinutes; - Scope scope = Scope.Multichain; uint256 amount = 1e18; uint256 nonce = 0; uint256 expires = block.timestamp + 1000; @@ -2350,51 +3050,63 @@ contract TheCompactTest is Test { uint256 amountTwo = 6e17; address arbiter = 0x2222222222222222222222222222222222222222; - vm.prank(allocator); - uint96 allocatorId = theCompact.__registerAllocator(allocator, ""); + (, bytes12 lockTag) = _registerAllocator(allocator); address emissary = address(new AlwaysOKEmissary()); - bytes12 lockTag = allocatorId.toLockTag(scope, resetPeriod); vm.prank(swapper); theCompact.assignEmissary(lockTag, emissary); - vm.prank(swapper); - uint256 id = theCompact.deposit{ value: amount }(lockTag, swapper); - assertEq(theCompact.balanceOf(swapper, id), amount); - - string memory witnessTypestring = "CompactWitness witness)CompactWitness(uint256 witnessArgument)"; - uint256 witnessArgument = 234; - bytes32 witness = keccak256(abi.encode(keccak256("CompactWitness(uint256 witnessArgument)"), witnessArgument)); - - string memory compactTypestring = - "Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,CompactWitness witness)CompactWitness(uint256 witnessArgument)"; + uint256 id = _makeDeposit(swapper, amount, lockTag); - bytes32 typehash = keccak256(bytes(compactTypestring)); + bytes32 claimHash; + bytes32 witness = _createCompactWitness(234); + { + CreateClaimHashWithWitnessArgs memory args; + args.typehash = compactWithWitnessTypehash; + args.arbiter = arbiter; + args.sponsor = swapper; + args.nonce = nonce; + args.expires = expires; + args.id = id; + args.amount = amount; + args.witness = witness; + + claimHash = _createClaimHashWithWitness(args); + } - bytes32 claimHash = keccak256(abi.encode(typehash, arbiter, swapper, nonce, expires, id, amount, witness)); + Claim memory claim; + { + bytes32 digest = _createDigest(theCompact.DOMAIN_SEPARATOR(), claimHash); - bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), theCompact.DOMAIN_SEPARATOR(), claimHash)); + (bytes32 r, bytes32 vs) = vm.signCompact(swapperPrivateKey, digest); + claim.sponsorSignature = hex"41414141414141414141"; - (bytes32 r, bytes32 vs) = vm.signCompact(swapperPrivateKey, digest); - bytes memory sponsorSignature = hex"41414141414141414141"; + (r, vs) = vm.signCompact(allocatorPrivateKey, digest); + claim.allocatorData = abi.encodePacked(r, vs); + } - (r, vs) = vm.signCompact(allocatorPrivateKey, digest); - bytes memory allocatorData = abi.encodePacked(r, vs); + { + uint256 claimantOne = abi.decode(abi.encodePacked(bytes12(0), recipientOne), (uint256)); + uint256 claimantTwo = abi.decode(abi.encodePacked(bytes12(0), recipientTwo), (uint256)); - uint256 claimantOne = abi.decode(abi.encodePacked(bytes12(0), recipientOne), (uint256)); - uint256 claimantTwo = abi.decode(abi.encodePacked(bytes12(0), recipientTwo), (uint256)); + SplitComponent memory splitOne = SplitComponent({ claimant: claimantOne, amount: amountOne }); - SplitComponent memory splitOne = SplitComponent({ claimant: claimantOne, amount: amountOne }); + SplitComponent memory splitTwo = SplitComponent({ claimant: claimantTwo, amount: amountTwo }); - SplitComponent memory splitTwo = SplitComponent({ claimant: claimantTwo, amount: amountTwo }); + SplitComponent[] memory recipients = new SplitComponent[](2); + recipients[0] = splitOne; + recipients[1] = splitTwo; - SplitComponent[] memory recipients = new SplitComponent[](2); - recipients[0] = splitOne; - recipients[1] = splitTwo; + claim.claimants = recipients; + } - Claim memory claim = Claim( - allocatorData, sponsorSignature, swapper, nonce, expires, witness, witnessTypestring, id, amount, recipients - ); + claim.sponsor = swapper; + claim.nonce = nonce; + claim.expires = expires; + claim.witness = witness; + claim.witnessTypestring = witnessTypestring; + claim.id = id; + claim.allocatedAmount = amount; vm.prank(arbiter); (bytes32 returnedClaimHash) = theCompact.claim(claim); @@ -2413,17 +3125,9 @@ contract TheCompactTest is Test { address recipient = 0x1111111111111111111111111111111111111111; uint256 amount = 1e18; - uint96 allocatorId = theCompact.__registerAllocator(alwaysOKAllocator, ""); - - bytes12 lockTag = bytes12( - bytes32( - (uint256(Scope.Multichain) << 255) | (uint256(ResetPeriod.TenMinutes) << 252) - | (uint256(allocatorId) << 160) - ) - ); + (, bytes12 lockTag) = _registerAllocator(alwaysOKAllocator); - vm.prank(swapper); - uint256 id = theCompact.deposit{ value: amount }(lockTag); + uint256 id = _makeDeposit(swapper, amount, lockTag); assertEq(address(theCompact).balance, amount); assertEq(theCompact.balanceOf(swapper, id), amount); @@ -2481,4 +3185,214 @@ contract TheCompactTest is Test { // if the loop exits, the address is the 0 address return 40; } + + /** + * Helper function to create a lock tag with the given parameters + */ + function _createLockTag(ResetPeriod resetPeriod, Scope scope, uint96 allocatorId) internal pure returns (bytes12) { + return bytes12(bytes32((uint256(scope) << 255) | (uint256(resetPeriod) << 252) | (uint256(allocatorId) << 160))); + } + + /** + * Helper function to create and register an allocator + */ + function _registerAllocator(address _allocator) internal returns (uint96 allocatorId, bytes12 lockTag) { + vm.prank(_allocator); + allocatorId = theCompact.__registerAllocator(_allocator, ""); + lockTag = _createLockTag(ResetPeriod.TenMinutes, Scope.Multichain, allocatorId); + return (allocatorId, lockTag); + } + + /** + * Helper function to create a witness hash for a compact witness + */ + function _createCompactWitness(uint256 _witnessArgument) internal pure returns (bytes32 witness) { + witness = keccak256(abi.encode(keccak256("CompactWitness(uint256 witnessArgument)"), _witnessArgument)); + return witness; + } + + function _createClaimHash( + bytes32 typehash, + address arbiter, + address sponsor, + uint256 nonce, + uint256 expires, + uint256 id, + uint256 amount + ) internal pure returns (bytes32) { + return keccak256(abi.encode(typehash, arbiter, sponsor, nonce, expires, id, amount)); + } + + /** + * Helper function to create a claim hash with witness + */ + function _createClaimHashWithWitness(CreateClaimHashWithWitnessArgs memory args) internal pure returns (bytes32) { + return keccak256( + abi.encode( + args.typehash, args.arbiter, args.sponsor, args.nonce, args.expires, args.id, args.amount, args.witness + ) + ); + } + + /** + * Helper function to create a batch claim hash with witness + */ + function _createBatchClaimHashWithWitness(CreateBatchClaimHashWithWitnessArgs memory args) + internal + pure + returns (bytes32) + { + return keccak256( + abi.encode( + args.typehash, + args.arbiter, + args.sponsor, + args.nonce, + args.expires, + args.idsAndAmountsHash, + args.witness + ) + ); + } + + function _createDigest(bytes32 domainSeparator, bytes32 hashValue) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(bytes2(0x1901), domainSeparator, hashValue)); + } + + /** + * Helper function to create a permit batch witness digest + */ + function _createPermitBatchWitnessDigest(CreatePermitBatchWitnessDigestArgs memory args) + internal + pure + returns (bytes32) + { + bytes32 activationHash = keccak256(abi.encode(args.activationTypehash, args.idsHash, args.claimHash)); + + bytes32 permitBatchHash = keccak256( + abi.encode( + keccak256( + "PermitBatchWitnessTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline,BatchActivation witness)BatchActivation(uint256[] ids,BatchCompact compact)BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256[2][] idsAndAmounts,CompactWitness witness)CompactWitness(uint256 witnessArgument)TokenPermissions(address token,uint256 amount)" + ), + args.tokenPermissionsHash, + args.spender, + args.nonce, + args.deadline, + activationHash + ) + ); + + return keccak256(abi.encodePacked(bytes2(0x1901), args.domainSeparator, permitBatchHash)); + } + + function _setupPermitCallExpectation(SetupPermitCallExpectationArgs memory args) internal { + // Create token permissions for the call + ISignatureTransfer.TokenPermissions[] memory tokenPermissionsOnCall = + new ISignatureTransfer.TokenPermissions[](2); + tokenPermissionsOnCall[0] = ISignatureTransfer.TokenPermissions({ token: address(token), amount: 1e18 }); + tokenPermissionsOnCall[1] = ISignatureTransfer.TokenPermissions({ token: address(anotherToken), amount: 1e18 }); + + // Create signature transfer details + ISignatureTransfer.SignatureTransferDetails[] memory signatureTransferDetails = + new ISignatureTransfer.SignatureTransferDetails[](2); + signatureTransferDetails[0] = + ISignatureTransfer.SignatureTransferDetails({ to: address(theCompact), requestedAmount: 1e18 }); + signatureTransferDetails[1] = + ISignatureTransfer.SignatureTransferDetails({ to: address(theCompact), requestedAmount: 1e18 }); + + // Create activation hash + bytes32 activationHash = + keccak256(abi.encode(args.activationTypehash, keccak256(abi.encodePacked(args.ids)), args.claimHash)); + + // Setup expectation + vm.expectCall( + address(permit2), + abi.encodeWithSignature( + "permitWitnessTransferFrom(((address,uint256)[],uint256,uint256),(address,uint256)[],address,bytes32,string,bytes)", + ISignatureTransfer.PermitBatchTransferFrom({ + permitted: tokenPermissionsOnCall, + nonce: args.nonce, + deadline: args.deadline + }), + signatureTransferDetails, + swapper, + activationHash, + "BatchActivation witness)BatchActivation(uint256[] ids,BatchCompact compact)BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256[2][] idsAndAmounts,CompactWitness witness)CompactWitness(uint256 witnessArgument)TokenPermissions(address token,uint256 amount)", + args.signature + ) + ); + } + + function _createPermit2BatchSignature( + address[] memory tokens, + uint256[] memory amounts, + uint256 nonce, + uint256 deadline, + bytes12 lockTag, + address recipient, + uint256 privateKey + ) internal view returns (bytes memory signature, ISignatureTransfer.TokenPermissions[] memory tokenPermissions) { + // Create token permissions array + tokenPermissions = new ISignatureTransfer.TokenPermissions[](tokens.length); + for (uint256 i = 0; i < tokens.length; i++) { + tokenPermissions[i] = ISignatureTransfer.TokenPermissions({ token: tokens[i], amount: amounts[i] }); + } + + // Create digest for signing + bytes32 domainSeparator = + keccak256(abi.encode(permit2EIP712DomainHash, keccak256(bytes("Permit2")), block.chainid, address(permit2))); + + bytes32 tokenPermissionsPortion = keccak256( + abi.encode( + keccak256( + abi.encode( + keccak256("TokenPermissions(address token,uint256 amount)"), + tokens[tokens.length > 1 ? 1 : 0], + amounts[tokens.length > 1 ? 1 : 0] + ) + ) + ) + ); + + bytes32 digest = keccak256( + abi.encodePacked( + bytes2(0x1901), + domainSeparator, + keccak256( + abi.encode( + keccak256( + "PermitBatchWitnessTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline,CompactDeposit witness)CompactDeposit(bytes12 lockTag,address recipient)TokenPermissions(address token,uint256 amount)" + ), + tokenPermissionsPortion, + address(theCompact), // spender + nonce, + deadline, + keccak256( + abi.encode( + keccak256("CompactDeposit(bytes12 lockTag,address recipient)"), lockTag, recipient + ) + ) + ) + ) + ) + ); + + // Sign the digest + (bytes32 r, bytes32 vs) = vm.signCompact(privateKey, digest); + signature = abi.encodePacked(r, vs); + + return (signature, tokenPermissions); + } + + function _makeDeposit(address guy, uint256 amount, bytes12 lockTag) internal returns (uint256 id) { + vm.prank(guy); + id = theCompact.deposit{ value: amount }(lockTag, guy); + assertEq(theCompact.balanceOf(guy, id), amount); + } + + function _makeDeposit(address guy, address asset, uint256 amount, bytes12 lockTag) internal returns (uint256 id) { + vm.prank(guy); + id = theCompact.deposit(asset, lockTag, amount, guy); + assertEq(theCompact.balanceOf(guy, id), amount); + } }