diff --git a/foundry.toml b/foundry.toml index 6c27aae..d9e7d95 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,7 +3,7 @@ solc = '0.8.28' evm_version='cancun' via_ir = true # optimizer_runs = 4_294_967_295 -optimizer_runs = 9 +optimizer_runs = 200 bytecode_hash = 'none' src = "src" out = "out" diff --git a/snapshots/TheCompactTest.json b/snapshots/TheCompactTest.json index 86584af..34c1a34 100644 --- a/snapshots/TheCompactTest.json +++ b/snapshots/TheCompactTest.json @@ -1,34 +1,34 @@ { - "basicTransfer": "57220", - "basicWithdrawal": "60350", - "batchClaim": "112331", - "batchClaimWithWitness": "112988", - "batchTransfer": "82936", - "batchWithdrawal": "101303", - "claim": "60821", - "claimAndWithdraw": "73704", - "claimWithWitness": "60004", - "depositBatchSingleERC20": "67765", - "depositBatchViaPermit2SingleERC20": "107820", - "depositERC20AndURI": "67076", - "depositERC20Basic": "67081", - "depositERC20ViaPermit2AndURI": "98237", - "depositETHAndURI": "26774", - "depositETHBasic": "28237", - "qualifiedBatchClaim": "113658", - "qualifiedBatchClaimWithWitness": "113117", - "qualifiedClaim": "60719", - "qualifiedClaimWithWitness": "59407", - "qualifiedSplitBatchClaim": "141263", - "qualifiedSplitBatchClaimWithWitness": "141228", - "qualifiedSplitClaim": "86947", - "qualifiedSplitClaimWithWitness": "87358", - "splitBatchClaim": "140743", - "splitBatchClaimWithWitness": "140674", - "splitBatchTransfer": "113646", - "splitBatchWithdrawal": "142938", - "splitClaim": "86873", - "splitClaimWithWitness": "86452", - "splitTransfer": "83176", - "splitWithdrawal": "94106" + "basicTransfer": "57231", + "basicWithdrawal": "60321", + "batchClaim": "112336", + "batchClaimWithWitness": "112999", + "batchTransfer": "82862", + "batchWithdrawal": "101233", + "claim": "57450", + "claimAndWithdraw": "73712", + "claimWithWitness": "59909", + "depositBatchSingleERC20": "67728", + "depositERC20AndURI": "67021", + "depositERC20Basic": "67026", + "depositERC20ViaPermit2AndURI": "98015", + "depositETHAndURI": "26694", + "depositETHBasic": "28186", + "qualifiedBatchClaim": "113663", + "qualifiedBatchClaimWithWitness": "113128", + "qualifiedClaim": "60727", + "qualifiedClaimWithWitness": "59312", + "qualifiedSplitBatchClaim": "141209", + "qualifiedSplitBatchClaimWithWitness": "141180", + "qualifiedSplitClaim": "86955", + "qualifiedSplitClaimWithWitness": "87256", + "register": "24890", + "splitBatchClaim": "140718", + "splitBatchClaimWithWitness": "140655", + "splitBatchTransfer": "113519", + "splitBatchWithdrawal": "142771", + "splitClaim": "86881", + "splitClaimWithWitness": "86379", + "splitTransfer": "83187", + "splitWithdrawal": "94053" } \ No newline at end of file diff --git a/src/TheCompact.sol b/src/TheCompact.sol index 344f358..ada8a2a 100644 --- a/src/TheCompact.sol +++ b/src/TheCompact.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.27; import { ITheCompact } from "./interfaces/ITheCompact.sol"; +import { CompactCategory } from "./types/CompactCategory.sol"; import { Lock } from "./types/Lock.sol"; import { Scope } from "./types/Scope.sol"; import { ResetPeriod } from "./types/ResetPeriod.sol"; @@ -83,7 +84,33 @@ import { ExogenousQualifiedSplitBatchMultichainClaimWithWitness } from "./types/BatchMultichainClaims.sol"; -import { COMPACT_TYPEHASH, BATCH_COMPACT_TYPEHASH, MULTICHAIN_COMPACT_TYPEHASH, PERMIT2_DEPOSIT_WITNESS_FRAGMENT_HASH } from "./types/EIP712Types.sol"; +import { + COMPACT_TYPEHASH, + BATCH_COMPACT_TYPEHASH, + MULTICHAIN_COMPACT_TYPEHASH, + PERMIT2_DEPOSIT_WITNESS_FRAGMENT_HASH, + PERMIT2_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_ONE, + PERMIT2_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_TWO, + TOKEN_PERMISSIONS_TYPESTRING_FRAGMENT_ONE, + TOKEN_PERMISSIONS_TYPESTRING_FRAGMENT_TWO, + COMPACT_ACTIVATION_TYPEHASH, + BATCH_COMPACT_ACTIVATION_TYPEHASH, + MULTICHAIN_COMPACT_ACTIVATION_TYPEHASH, + PERMIT2_ACTIVATION_COMPACT_TYPESTRING_FRAGMENT_ONE, + PERMIT2_ACTIVATION_COMPACT_TYPESTRING_FRAGMENT_TWO, + PERMIT2_ACTIVATION_COMPACT_TYPESTRING_FRAGMENT_THREE, + PERMIT2_ACTIVATION_COMPACT_TYPESTRING_FRAGMENT_FOUR, + PERMIT2_ACTIVATION_BATCH_COMPACT_TYPESTRING_FRAGMENT_ONE, + PERMIT2_ACTIVATION_BATCH_COMPACT_TYPESTRING_FRAGMENT_TWO, + PERMIT2_ACTIVATION_BATCH_COMPACT_TYPESTRING_FRAGMENT_THREE, + PERMIT2_ACTIVATION_BATCH_COMPACT_TYPESTRING_FRAGMENT_FOUR, + PERMIT2_ACTIVATION_MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_ONE, + PERMIT2_ACTIVATION_MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_TWO, + PERMIT2_ACTIVATION_MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_THREE, + PERMIT2_ACTIVATION_MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_FOUR, + PERMIT2_ACTIVATION_MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_FIVE, + PERMIT2_ACTIVATION_MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_SIX +} from "./types/EIP712Types.sol"; import { SplitComponent, TransferComponent, SplitByIdComponent, BatchClaimComponent, SplitBatchClaimComponent } from "./types/Components.sol"; @@ -159,6 +186,7 @@ contract TheCompact is ITheCompact, ERC6909, Tstorish { using IdLib for address; using IdLib for Lock; using IdLib for ResetPeriod; + using IdLib for CompactCategory; using SafeTransferLib for address; using FixedPointMathLib for uint256; using ConsumerLib for uint256; @@ -204,11 +232,13 @@ contract TheCompact is ITheCompact, ERC6909, Tstorish { uint256 private immutable _INITIAL_CHAIN_ID; bytes32 private immutable _INITIAL_DOMAIN_SEPARATOR; MetadataRenderer private immutable _METADATA_RENDERER; + bool private immutable _PERMIT2_INITIALLY_DEPLOYED; constructor() { _INITIAL_CHAIN_ID = block.chainid; _INITIAL_DOMAIN_SEPARATOR = block.chainid.toNotarizedDomainSeparator(); _METADATA_RENDERER = new MetadataRenderer(); + _PERMIT2_INITIALLY_DEPLOYED = _checkPermit2Deployment(); } function deposit(address allocator) external payable returns (uint256 id) { @@ -249,56 +279,6 @@ contract TheCompact is ITheCompact, ERC6909, Tstorish { return _processBatchDeposit(idsAndAmounts, msg.sender); } - function _processBatchDeposit(uint256[2][] calldata idsAndAmounts, address recipient) internal returns (bool) { - _setTstorish(_REENTRANCY_GUARD_SLOT, 1); - uint256 totalIds = idsAndAmounts.length; - bool firstUnderlyingTokenIsNative; - uint256 id; - - assembly ("memory-safe") { - let idsAndAmountsOffset := idsAndAmounts.offset - id := calldataload(idsAndAmountsOffset) - firstUnderlyingTokenIsNative := iszero(shr(96, shl(96, id))) - // Revert if: - // * the array is empty - // * the callvalue is zero but the first token is native - // * the callvalue is nonzero but the first token is non-native - // * the first token is non-native and the callvalue doesn't equal the first amount - if or(iszero(totalIds), or(eq(firstUnderlyingTokenIsNative, iszero(callvalue())), and(firstUnderlyingTokenIsNative, iszero(eq(callvalue(), calldataload(add(idsAndAmountsOffset, 0x20))))))) - { - // revert InvalidBatchDepositStructure() - mstore(0, 0xca0fc08e) - revert(0x1c, 0x04) - } - } - - uint96 currentAllocatorId = id.toRegisteredAllocatorId(); - - if (firstUnderlyingTokenIsNative) { - _deposit(recipient, id, msg.value); - } - - unchecked { - for (uint256 i = firstUnderlyingTokenIsNative.asUint256(); i < totalIds; ++i) { - uint256[2] calldata idAndAmount = idsAndAmounts[i]; - id = idAndAmount[0]; - uint256 amount = idAndAmount[1]; - - uint96 newAllocatorId = id.toAllocatorId(); - if (newAllocatorId != currentAllocatorId) { - newAllocatorId.mustHaveARegisteredAllocator(); - currentAllocatorId = newAllocatorId; - } - - _transferAndDeposit(id.toToken(), recipient, id, amount); - } - } - - _clearTstorish(_REENTRANCY_GUARD_SLOT); - - return true; - } - function deposit( address token, uint256, // amount @@ -379,6 +359,169 @@ contract TheCompact is ITheCompact, ERC6909, Tstorish { _clearTstorish(_REENTRANCY_GUARD_SLOT); } + function depositAndRegister( + address token, + uint256, // amount + uint256, // nonce + uint256, // deadline + address depositor, // also recipient + address allocator, + ResetPeriod resetPeriod, + Scope scope, + bytes32 claimHash, + CompactCategory compactCategory, + string calldata witness, + bytes calldata signature + ) external returns (uint256 id) { + _setTstorish(_REENTRANCY_GUARD_SLOT, 1); + id = token.excludingNative().toIdIfRegistered(scope, resetPeriod, allocator); + + address permit2 = address(_PERMIT2); + + uint256 initialBalance = token.balanceOf(address(this)); + + bool isPermit2Deployed = _isPermit2Deployed(); + + bytes32 compactTypehash; + + assembly ("memory-safe") { + function writeWitnessAndGetTypehashes(memoryLocation, c, witnessOffset, witnessLength) -> derivedActivationTypehash, derivedCompactTypehash { + let memoryOffset := add(memoryLocation, 0x20) + // 1. prepare initial witness string at offset + mstore(add(memoryOffset, 0x09), PERMIT2_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_TWO) + mstore(memoryOffset, PERMIT2_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_ONE) + let activationStart := add(memoryOffset, 0x13) + let categorySpecificStart := add(memoryOffset, 0x29) + let categorySpecificEnd + if iszero(c) { + mstore(categorySpecificStart, PERMIT2_ACTIVATION_COMPACT_TYPESTRING_FRAGMENT_ONE) + mstore(add(categorySpecificStart, 0x20), PERMIT2_ACTIVATION_COMPACT_TYPESTRING_FRAGMENT_TWO) + mstore(add(categorySpecificStart, 0x50), PERMIT2_ACTIVATION_COMPACT_TYPESTRING_FRAGMENT_FOUR) + mstore(add(categorySpecificStart, 0x40), PERMIT2_ACTIVATION_COMPACT_TYPESTRING_FRAGMENT_THREE) + categorySpecificEnd := add(categorySpecificStart, 0x70) + categorySpecificStart := add(categorySpecificStart, 0x10) + } + + if iszero(sub(c, 1)) { + mstore(categorySpecificStart, PERMIT2_ACTIVATION_BATCH_COMPACT_TYPESTRING_FRAGMENT_ONE) + mstore(add(categorySpecificStart, 0x20), PERMIT2_ACTIVATION_BATCH_COMPACT_TYPESTRING_FRAGMENT_TWO) + mstore(add(categorySpecificStart, 0x5b), PERMIT2_ACTIVATION_BATCH_COMPACT_TYPESTRING_FRAGMENT_FOUR) + mstore(add(categorySpecificStart, 0x40), PERMIT2_ACTIVATION_BATCH_COMPACT_TYPESTRING_FRAGMENT_THREE) + categorySpecificEnd := add(categorySpecificStart, 0x7b) + categorySpecificStart := add(categorySpecificStart, 0x15) + } + + if iszero(categorySpecificEnd) { + mstore(categorySpecificStart, PERMIT2_ACTIVATION_MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_ONE) + mstore(add(categorySpecificStart, 0x20), PERMIT2_ACTIVATION_MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_TWO) + mstore(add(categorySpecificStart, 0x40), PERMIT2_ACTIVATION_MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_THREE) + mstore(add(categorySpecificStart, 0x60), PERMIT2_ACTIVATION_MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_FOUR) + mstore(add(categorySpecificStart, 0x70), PERMIT2_ACTIVATION_MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_SIX) + mstore(add(categorySpecificStart, 0x60), PERMIT2_ACTIVATION_MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_FIVE) + categorySpecificEnd := add(categorySpecificStart, 0x90) + categorySpecificStart := add(categorySpecificStart, 0x1a) + } + + // 2. handle no-witness cases + if iszero(witnessLength) { + let indexWords := shl(5, c) + + mstore(add(categorySpecificEnd, 0x0e), TOKEN_PERMISSIONS_TYPESTRING_FRAGMENT_TWO) + mstore(sub(categorySpecificEnd, 1), TOKEN_PERMISSIONS_TYPESTRING_FRAGMENT_ONE) + mstore(memoryLocation, sub(add(categorySpecificEnd, 0x2e), memoryOffset)) + + let m := mload(0x40) + + mstore(0, COMPACT_ACTIVATION_TYPEHASH) + mstore(0x20, BATCH_COMPACT_ACTIVATION_TYPEHASH) + mstore(0x40, MULTICHAIN_COMPACT_ACTIVATION_TYPEHASH) + derivedActivationTypehash := mload(indexWords) + + mstore(0, COMPACT_TYPEHASH) + mstore(0x20, BATCH_COMPACT_TYPEHASH) + mstore(0x40, MULTICHAIN_COMPACT_TYPEHASH) + derivedCompactTypehash := mload(indexWords) + + mstore(0x40, m) + leave + } + + // 3. insert the supplied compact witness + calldatacopy(categorySpecificEnd, witnessOffset, witnessLength) + + // 4. insert tokenPermissions + let tokenPermissionsFragmentStart := add(categorySpecificEnd, witnessLength) + mstore(add(tokenPermissionsFragmentStart, 0x0e), TOKEN_PERMISSIONS_TYPESTRING_FRAGMENT_TWO) + mstore(sub(tokenPermissionsFragmentStart, 1), TOKEN_PERMISSIONS_TYPESTRING_FRAGMENT_ONE) + mstore(memoryLocation, sub(add(tokenPermissionsFragmentStart, 0x2e), memoryOffset)) + + // 5. derive the activation typehash + derivedActivationTypehash := keccak256(activationStart, sub(tokenPermissionsFragmentStart, activationStart)) + + // 6. derive the compact typehash + derivedCompactTypehash := keccak256(categorySpecificStart, sub(tokenPermissionsFragmentStart, categorySpecificStart)) + } + + let m := mload(0x40) // Grab the free memory pointer; memory will be left dirtied. + + let signatureLength := signature.length + let dataStart := add(m, 0x1c) + + mstore(m, _PERMIT_WITNESS_TRANSFER_FROM_SELECTOR) + calldatacopy(add(m, 0x20), 0x04, 0x80) // token, amount, nonce, deadline + mstore(add(m, 0xa0), address()) + mstore(add(m, 0xc0), calldataload(0x24)) // amount + mstore(add(m, 0xe0), calldataload(0x84)) // depositor + mstore(add(m, 0x120), 0x140) + + let permit2WitnessOffset := add(m, 0x160) + let activationTypehash + activationTypehash, compactTypehash := writeWitnessAndGetTypehashes(permit2WitnessOffset, compactCategory, witness.offset, witness.length) + let signatureOffsetValue := and(add(mload(permit2WitnessOffset), 0x17f), not(0x1f)) + mstore(add(m, 0x140), signatureOffsetValue) + let signatureOffset := add(m, add(signatureOffsetValue, 0x20)) + mstore(signatureOffset, signatureLength) + calldatacopy(add(signatureOffset, 0x20), signature.offset, signatureLength) + + mstore(0, activationTypehash) + mstore(0x20, id) + mstore(0x40, claimHash) + mstore(add(m, 0x100), keccak256(0, 0x60)) + mstore(0x40, m) + + if iszero(and(isPermit2Deployed, call(gas(), permit2, 0, add(m, 0x1c), add(0x24, add(signatureOffsetValue, signatureLength)), 0, 0))) { + // bubble up if the call failed and there's data + // NOTE: consider evaluating remaining gas to protect against revert bombing + if returndatasize() { + returndatacopy(0, 0, returndatasize()) + revert(0, returndatasize()) + } + + // TODO: add proper revert on no data + revert(0, 0) + } + } + + uint256 tokenBalance = token.balanceOf(address(this)); + + assembly ("memory-safe") { + if iszero(lt(initialBalance, tokenBalance)) { + // revert InvalidDepositBalanceChange() + mstore(0, 0x426d8dcf) + revert(0x1c, 0x04) + } + } + + unchecked { + _deposit(depositor, id, tokenBalance - initialBalance); + } + + _register(depositor, claimHash, compactTypehash); + + _clearTstorish(_REENTRANCY_GUARD_SLOT); + } + + /* TODO: put these two batch deposit methods back in after finding some room for them function deposit( address depositor, ISignatureTransfer.TokenPermissions[] calldata permitted, @@ -412,10 +555,126 @@ contract TheCompact is ITheCompact, ERC6909, Tstorish { uint256 initialId = address(0).toIdIfRegistered(scope, resetPeriod, allocator); return _processBatchPermit2Deposits( - firstUnderlyingTokenIsNative, recipient, initialId, totalTokens, permitted, depositor, nonce, deadline, allocator.toPermit2DepositWitnessHash(resetPeriod, scope, recipient), signature + firstUnderlyingTokenIsNative, + recipient, + initialId, + totalTokens, + permitted, + depositor, + nonce, + deadline, + allocator.toPermit2DepositWitnessHash(resetPeriod, scope, recipient), + "CompactDeposit witness)CompactDeposit(address allocator,uint8 resetPeriod,uint8 scope,address recipient)TokenPermissions(address token,uint256 amount)", + signature ); } + function depositAndRegister( + address depositor, + ISignatureTransfer.TokenPermissions[] calldata permitted, + address allocator, + ResetPeriod resetPeriod, + Scope scope, + address recipient, + uint256 nonce, + uint256 deadline, + bytes32 claimHash, + CompactCategory compactCategory, + string calldata witness, + bytes calldata signature + ) external payable returns (uint256[] memory ids) { + _setTstorish(_REENTRANCY_GUARD_SLOT, 1); + + uint256 totalTokens = permitted.length; + bool firstUnderlyingTokenIsNative; + assembly ("memory-safe") { + let permittedOffset := permitted.offset + firstUnderlyingTokenIsNative := iszero(shr(96, shl(96, add(permittedOffset, 0x20)))) + + // Revert if: + // * the array is empty + // * the callvalue is zero but the first token is native + // * the callvalue is nonzero but the first token is non-native + // * the first token is non-native and the callvalue doesn't equal the first amount + if or(iszero(totalTokens), or(eq(firstUnderlyingTokenIsNative, iszero(callvalue())), and(firstUnderlyingTokenIsNative, iszero(eq(callvalue(), calldataload(add(permittedOffset, 0x40))))))) + { + // revert InvalidBatchDepositStructure() + mstore(0, 0xca0fc08e) + revert(0x1c, 0x04) + } + } + + uint256 initialId = address(0).toIdIfRegistered(scope, resetPeriod, allocator); + + string memory activationTypestring; + string memory compactTypestring; + if (compactCategory == CompactCategory.Compact) { + compactTypestring = string.concat("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount", bytes(witness).length != 0 ? "," : "", witness, ")"); + activationTypestring = string.concat("BatchActivation(uint256[] ids,Compact compact)", compactTypestring); + } else if (compactCategory == CompactCategory.BatchCompact) { + compactTypestring = + string.concat("BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256[2][] idsAndAmounts", bytes(witness).length != 0 ? "," : "", witness, ")"); + activationTypestring = string.concat("BatchActivation(uint256[] ids,BatchCompact compact)", compactTypestring); + } else { + compactTypestring = string.concat( + "MultichainCompact(address sponsor,uint256 nonce,uint256 expires,Segment[] segments)Segment(address arbiter,uint256 chainId,uint256[2][] idsAndAmounts", + bytes(witness).length != 0 ? "," : "", + witness, + ")" + ); + activationTypestring = string.concat("BatchActivation(uint256[] ids,MultichainCompact compact)", compactTypestring); + } + + ids = new uint256[](totalTokens); + + uint256 totalTokensLessInitialNative; + unchecked { + totalTokensLessInitialNative = totalTokens - firstUnderlyingTokenIsNative.asUint256(); + } + + if (firstUnderlyingTokenIsNative) { + _deposit(recipient, initialId, msg.value); + ids[0] = initialId; + } + + (ISignatureTransfer.SignatureTransferDetails[] memory details, ISignatureTransfer.TokenPermissions[] memory permittedTokens, uint256[] memory initialTokenBalances) = + _preparePermit2ArraysAndGetBalances(ids, totalTokensLessInitialNative, firstUnderlyingTokenIsNative, permitted, initialId); + + ISignatureTransfer.PermitBatchTransferFrom memory permitTransferFrom = ISignatureTransfer.PermitBatchTransferFrom({ permitted: permittedTokens, nonce: nonce, deadline: deadline }); + + string memory witnessTypestring = string.concat("BatchActivation witness)", activationTypestring, "TokenPermissions(address token,uint256 amount)"); + + bytes32 witnessHash = keccak256(abi.encodePacked(keccak256(bytes(activationTypestring)), keccak256(abi.encodePacked(ids)), claimHash)); + + _PERMIT2.permitWitnessTransferFrom(permitTransferFrom, details, depositor, witnessHash, witnessTypestring, signature); + + uint256 tokenBalance; + uint256 initialBalance; + uint256 errorBuffer; + unchecked { + for (uint256 i = 0; i < totalTokensLessInitialNative; ++i) { + tokenBalance = permittedTokens[i].token.balanceOf(address(this)); + initialBalance = initialTokenBalances[i]; + errorBuffer |= (initialBalance >= tokenBalance).asUint256(); + + _deposit(recipient, ids[i + firstUnderlyingTokenIsNative.asUint256()], tokenBalance - initialBalance); + } + } + + assembly ("memory-safe") { + if errorBuffer { + // revert InvalidDepositBalanceChange() + mstore(0, 0x426d8dcf) + revert(0x1c, 0x04) + } + } + + _register(depositor, claimHash], keccak256(bytes(compactTypestring)); + + _clearTstorish(_REENTRANCY_GUARD_SLOT); + } + */ + function allocatedTransfer(BasicTransfer calldata transfer) external returns (bool) { return _processBasicTransfer(transfer, _release); } @@ -877,10 +1136,15 @@ contract TheCompact is ITheCompact, ERC6909, Tstorish { } function register(bytes32 claimHash, bytes32 typehash) external returns (bool) { - _registeredClaimHashes[msg.sender][claimHash] = typehash; + _register(msg.sender, claimHash, typehash); return true; } + function _register(address sponsor, bytes32 claimHash, bytes32 typehash) internal { + _registeredClaimHashes[sponsor][claimHash] = typehash; + emit CompactRegistered(sponsor, claimHash, typehash); + } + function register(bytes32[2][] calldata claimHashesAndTypehashes) external returns (bool) { return _registerFor(msg.sender, claimHashesAndTypehashes); } @@ -890,7 +1154,7 @@ contract TheCompact is ITheCompact, ERC6909, Tstorish { uint256 totalClaimHashes = claimHashesAndTypehashes.length; for (uint256 i = 0; i < totalClaimHashes; ++i) { bytes32[2] calldata claimHashAndTypehash = claimHashesAndTypehashes[i]; - _registeredClaimHashes[sponsor][claimHashAndTypehash[0]] = claimHashAndTypehash[1]; + _register(sponsor, claimHashAndTypehash[0], claimHashAndTypehash[1]); } } @@ -973,6 +1237,56 @@ contract TheCompact is ITheCompact, ERC6909, Tstorish { } } + function _processBatchDeposit(uint256[2][] calldata idsAndAmounts, address recipient) internal returns (bool) { + _setTstorish(_REENTRANCY_GUARD_SLOT, 1); + uint256 totalIds = idsAndAmounts.length; + bool firstUnderlyingTokenIsNative; + uint256 id; + + assembly ("memory-safe") { + let idsAndAmountsOffset := idsAndAmounts.offset + id := calldataload(idsAndAmountsOffset) + firstUnderlyingTokenIsNative := iszero(shr(96, shl(96, id))) + // Revert if: + // * the array is empty + // * the callvalue is zero but the first token is native + // * the callvalue is nonzero but the first token is non-native + // * the first token is non-native and the callvalue doesn't equal the first amount + if or(iszero(totalIds), or(eq(firstUnderlyingTokenIsNative, iszero(callvalue())), and(firstUnderlyingTokenIsNative, iszero(eq(callvalue(), calldataload(add(idsAndAmountsOffset, 0x20))))))) + { + // revert InvalidBatchDepositStructure() + mstore(0, 0xca0fc08e) + revert(0x1c, 0x04) + } + } + + uint96 currentAllocatorId = id.toRegisteredAllocatorId(); + + if (firstUnderlyingTokenIsNative) { + _deposit(recipient, id, msg.value); + } + + unchecked { + for (uint256 i = firstUnderlyingTokenIsNative.asUint256(); i < totalIds; ++i) { + uint256[2] calldata idAndAmount = idsAndAmounts[i]; + id = idAndAmount[0]; + uint256 amount = idAndAmount[1]; + + uint96 newAllocatorId = id.toAllocatorId(); + if (newAllocatorId != currentAllocatorId) { + newAllocatorId.mustHaveARegisteredAllocator(); + currentAllocatorId = newAllocatorId; + } + + _transferAndDeposit(id.toToken(), recipient, id, amount); + } + } + + _clearTstorish(_REENTRANCY_GUARD_SLOT); + + return true; + } + function _notExpiredAndSignedByAllocator(bytes32 messageHash, address allocator, BasicTransfer calldata transferPayload) internal { transferPayload.expires.later(); @@ -1555,8 +1869,12 @@ contract TheCompact is ITheCompact, ERC6909, Tstorish { function _typehashes(uint256 i) internal pure returns (bytes32 typehash) { assembly ("memory-safe") { - let j := sub(i, 1) - typehash := add(mul(iszero(i), COMPACT_TYPEHASH), add(mul(iszero(j), BATCH_COMPACT_TYPEHASH), mul(iszero(iszero(j)), MULTICHAIN_COMPACT_TYPEHASH))) + let m := mload(0x40) + mstore(0, COMPACT_TYPEHASH) + mstore(0x20, BATCH_COMPACT_TYPEHASH) + mstore(0x40, MULTICHAIN_COMPACT_TYPEHASH) + typehash := mload(shl(5, i)) + mstore(0x40, m) } } @@ -1768,6 +2086,7 @@ contract TheCompact is ITheCompact, ERC6909, Tstorish { uint256 nonce, uint256 deadline, bytes32 witness, + string memory witnessTypestring, bytes calldata signature ) internal returns (uint256[] memory ids) { _setTstorish(_REENTRANCY_GUARD_SLOT, 1); @@ -1789,14 +2108,7 @@ contract TheCompact is ITheCompact, ERC6909, Tstorish { ISignatureTransfer.PermitBatchTransferFrom memory permitTransferFrom = ISignatureTransfer.PermitBatchTransferFrom({ permitted: permittedTokens, nonce: nonce, deadline: deadline }); - _PERMIT2.permitWitnessTransferFrom( - permitTransferFrom, - details, - depositor, - witness, - "CompactDeposit witness)CompactDeposit(address allocator,uint8 resetPeriod,uint8 scope,address recipient)TokenPermissions(address token,uint256 amount)", - signature - ); + _PERMIT2.permitWitnessTransferFrom(permitTransferFrom, details, depositor, witness, witnessTypestring, signature); uint256 tokenBalance; uint256 initialBalance; @@ -1822,14 +2134,6 @@ contract TheCompact is ITheCompact, ERC6909, Tstorish { _clearTstorish(_REENTRANCY_GUARD_SLOT); } - function _verifyAndProcessBatchComponents( - uint96 allocatorId, - address sponsor, - address claimant, - BatchClaimComponent[] calldata claims, - function(address, address, uint256, uint256) internal returns (bool) operation - ) internal returns (bool) { } - function _verifyAndProcessSplitComponents( address sponsor, uint256 id, @@ -2113,4 +2417,19 @@ contract TheCompact is ITheCompact, ERC6909, Tstorish { } } } + + function _isPermit2Deployed() internal view returns (bool) { + if (_PERMIT2_INITIALLY_DEPLOYED) { + return true; + } + + return _checkPermit2Deployment(); + } + + function _checkPermit2Deployment() internal view returns (bool permit2Deployed) { + address permit2 = address(_PERMIT2); + assembly ("memory-safe") { + permit2Deployed := iszero(iszero(extcodesize(permit2))) + } + } } diff --git a/src/interfaces/ITheCompact.sol b/src/interfaces/ITheCompact.sol index 20d0401..e6f1954 100644 --- a/src/interfaces/ITheCompact.sol +++ b/src/interfaces/ITheCompact.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.27; import { ForcedWithdrawalStatus } from "../types/ForcedWithdrawalStatus.sol"; import { ResetPeriod } from "../types/ResetPeriod.sol"; import { Scope } from "../types/Scope.sol"; +import { CompactCategory } from "../types/CompactCategory.sol"; import { ISignatureTransfer } from "permit2/src/interfaces/ISignatureTransfer.sol"; import { BasicTransfer, @@ -82,6 +83,7 @@ interface ITheCompact { event ForcedWithdrawalEnabled(address indexed account, uint256 indexed id, uint256 withdrawableAt); event ForcedWithdrawalDisabled(address indexed account, uint256 indexed id); event AllocatorRegistered(uint96 allocatorId, address allocator); + event CompactRegistered(address indexed sponsor, bytes32 claimHash, bytes32 typehash); error InvalidToken(address token); error Expired(uint256 expiration); @@ -120,17 +122,20 @@ interface ITheCompact { bytes calldata signature ) external returns (uint256 id); - function deposit( + function depositAndRegister( + address token, + uint256 amount, + uint256 nonce, + uint256 deadline, address depositor, - ISignatureTransfer.TokenPermissions[] calldata permitted, address allocator, ResetPeriod resetPeriod, Scope scope, - address recipient, - uint256 nonce, - uint256 deadline, + bytes32 claimHash, + CompactCategory compactCategory, + string calldata witness, bytes calldata signature - ) external payable returns (uint256[] memory ids); + ) external returns (uint256 id); function allocatedTransfer(BasicTransfer calldata transfer) external returns (bool); diff --git a/src/lib/HashLib.sol b/src/lib/HashLib.sol index e093f96..e704835 100644 --- a/src/lib/HashLib.sol +++ b/src/lib/HashLib.sol @@ -22,24 +22,6 @@ import { MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_THREE, MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_FOUR, MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_FIVE, - PERMIT2_DEPOSIT_WITNESS_FRAGMENT_HASH, - PERMIT2_DEPOSIT_WITH_COMPACT_ACTIVATION_TYPEHASH, - PERMIT2_DEPOSIT_WITH_BATCH_COMPACT_ACTIVATION_TYPEHASH, - PERMIT2_DEPOSIT_WITH_MULTICHAIN_COMPACT_ACTIVATION_TYPEHASH, - PERMIT2_BATCH_DEPOSIT_WITH_COMPACT_ACTIVATION_TYPEHASH, - PERMIT2_BATCH_DEPOSIT_WITH_BATCH_COMPACT_ACTIVATION_TYPEHASH, - PERMIT2_BATCH_DEPOSIT_WITH_MULTICHAIN_COMPACT_ACTIVATION_TYPEHASH, - PERMIT2_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_ONE, - PERMIT2_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_TWO, - PERMIT2_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_THREE, - PERMIT2_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_FOUR, - PERMIT2_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_FIVE, - PERMIT2_BATCH_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_ONE, - PERMIT2_BATCH_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_TWO, - PERMIT2_BATCH_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_THREE, - PERMIT2_BATCH_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_FOUR, - PERMIT2_BATCH_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_FIVE, - PERMIT2_BATCH_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_SIX, PERMIT2_ACTIVATION_COMPACT_TYPESTRING_FRAGMENT_ONE, PERMIT2_ACTIVATION_COMPACT_TYPESTRING_FRAGMENT_TWO, PERMIT2_ACTIVATION_COMPACT_TYPESTRING_FRAGMENT_THREE, @@ -53,7 +35,16 @@ import { PERMIT2_ACTIVATION_MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_THREE, PERMIT2_ACTIVATION_MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_FOUR, PERMIT2_ACTIVATION_MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_FIVE, - PERMIT2_ACTIVATION_MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_SIX + PERMIT2_ACTIVATION_MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_SIX, + COMPACT_ACTIVATION_TYPEHASH, + BATCH_COMPACT_ACTIVATION_TYPEHASH, + MULTICHAIN_COMPACT_ACTIVATION_TYPEHASH, + COMPACT_BATCH_ACTIVATION_TYPEHASH, + BATCH_COMPACT_BATCH_ACTIVATION_TYPEHASH, + MULTICHAIN_COMPACT_BATCH_ACTIVATION_TYPEHASH, + TOKEN_PERMISSIONS_TYPESTRING_FRAGMENT_ONE, + TOKEN_PERMISSIONS_TYPESTRING_FRAGMENT_TWO, + PERMIT2_DEPOSIT_WITNESS_FRAGMENT_HASH } from "../types/EIP712Types.sol"; import { @@ -122,7 +113,6 @@ import { import { TransferComponent, SplitComponent, SplitByIdComponent, BatchClaimComponent, SplitBatchClaimComponent } from "../types/Components.sol"; -import { ActivatedCompactCategory } from "../types/ActivatedCompactCategory.sol"; import { ResetPeriod } from "../types/ResetPeriod.sol"; import { Scope } from "../types/Scope.sol"; @@ -921,90 +911,4 @@ library HashLib { messageHash := keccak256(m, 0xa0) } } - - function toPermit2ActivatedCompactTypehash(ActivatedCompactCategory category, string calldata witness) internal pure returns (bytes32 typehash) { - assembly ("memory-safe") { - function toTypehash(c, witnessOffset, witnessLength) -> t { - let m := mload(0x40) // Grab the free memory pointer; memory will be left dirtied - - let isBatch := gt(c, 2) - c := sub(c, mul(isBatch, 3)) - - // 1. handle no-witness cases or prepare first witness fragment based on deposit vs batch deposit - let fragmentTwoStart - if iszero(isBatch) { - if iszero(witnessLength) { - mstore(0, PERMIT2_DEPOSIT_WITH_COMPACT_ACTIVATION_TYPEHASH) - mstore(0x20, PERMIT2_DEPOSIT_WITH_BATCH_COMPACT_ACTIVATION_TYPEHASH) - mstore(0x40, PERMIT2_DEPOSIT_WITH_MULTICHAIN_COMPACT_ACTIVATION_TYPEHASH) - t := mload(shl(5, c)) - mstore(0x40, m) - leave - } - - mstore(m, PERMIT2_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_ONE) - mstore(add(m, 0x20), PERMIT2_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_TWO) - mstore(add(m, 0x40), PERMIT2_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_THREE) - mstore(add(m, 0x6d), PERMIT2_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_FIVE) - mstore(add(m, 0x60), PERMIT2_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_FOUR) - fragmentTwoStart := add(m, 0x8d) - } - - if iszero(fragmentTwoStart) { - if iszero(witnessLength) { - mstore(0, PERMIT2_BATCH_DEPOSIT_WITH_COMPACT_ACTIVATION_TYPEHASH) - mstore(0x20, PERMIT2_BATCH_DEPOSIT_WITH_BATCH_COMPACT_ACTIVATION_TYPEHASH) - mstore(0x40, PERMIT2_BATCH_DEPOSIT_WITH_MULTICHAIN_COMPACT_ACTIVATION_TYPEHASH) - t := mload(shl(5, c)) - mstore(0x40, m) - leave - } - - mstore(m, PERMIT2_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_ONE) - mstore(add(m, 0x20), PERMIT2_BATCH_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_TWO) - mstore(add(m, 0x40), PERMIT2_BATCH_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_THREE) - mstore(add(m, 0x60), PERMIT2_BATCH_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_FOUR) - mstore(add(m, 0x80), PERMIT2_BATCH_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_FIVE) - mstore8(add(m, 0xa0), PERMIT2_BATCH_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_SIX) - fragmentTwoStart := add(m, 0xa1) - } - - // 2. prepare second witness fragment based on compact category - let fragmentThreeStart - if iszero(c) { - mstore(fragmentTwoStart, PERMIT2_ACTIVATION_COMPACT_TYPESTRING_FRAGMENT_ONE) - mstore(add(fragmentTwoStart, 0x20), PERMIT2_ACTIVATION_COMPACT_TYPESTRING_FRAGMENT_TWO) - mstore(add(fragmentTwoStart, 0x50), PERMIT2_ACTIVATION_COMPACT_TYPESTRING_FRAGMENT_FOUR) - mstore(add(fragmentTwoStart, 0x40), PERMIT2_ACTIVATION_COMPACT_TYPESTRING_FRAGMENT_THREE) - fragmentThreeStart := add(fragmentTwoStart, 0x70) - } - - if iszero(sub(c, 1)) { - mstore(fragmentTwoStart, PERMIT2_ACTIVATION_BATCH_COMPACT_TYPESTRING_FRAGMENT_ONE) - mstore(add(fragmentTwoStart, 0x20), PERMIT2_ACTIVATION_BATCH_COMPACT_TYPESTRING_FRAGMENT_TWO) - mstore(add(fragmentTwoStart, 0x5b), PERMIT2_ACTIVATION_BATCH_COMPACT_TYPESTRING_FRAGMENT_FOUR) - mstore(add(fragmentTwoStart, 0x40), PERMIT2_ACTIVATION_BATCH_COMPACT_TYPESTRING_FRAGMENT_THREE) - fragmentThreeStart := add(fragmentTwoStart, 0x7b) - } - - if iszero(fragmentThreeStart) { - mstore(fragmentTwoStart, PERMIT2_ACTIVATION_MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_ONE) - mstore(add(fragmentTwoStart, 0x20), PERMIT2_ACTIVATION_MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_TWO) - mstore(add(fragmentTwoStart, 0x40), PERMIT2_ACTIVATION_MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_THREE) - mstore(add(fragmentTwoStart, 0x60), PERMIT2_ACTIVATION_MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_FOUR) - mstore(add(fragmentTwoStart, 0x90), PERMIT2_ACTIVATION_MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_SIX) - mstore(add(fragmentTwoStart, 0x80), PERMIT2_ACTIVATION_MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_FIVE) - fragmentThreeStart := add(fragmentTwoStart, 0xb0) - } - - // 3. insert the supplied witness (must also include TokenPermissions) - calldatacopy(fragmentThreeStart, witnessOffset, witnessLength) - - // 4. derive the typehash - t := keccak256(m, add(sub(fragmentThreeStart, m), witnessLength)) - } - - typehash := toTypehash(category, witness.offset, witness.length) - } - } } diff --git a/src/lib/IdLib.sol b/src/lib/IdLib.sol index a4dff4f..ddc3ef0 100644 --- a/src/lib/IdLib.sol +++ b/src/lib/IdLib.sol @@ -7,6 +7,7 @@ import { Lock } from "../types/Lock.sol"; import { MetadataLib } from "./MetadataLib.sol"; import { EfficiencyLib } from "./EfficiencyLib.sol"; import { SignatureCheckerLib } from "solady/utils/SignatureCheckerLib.sol"; +import { CompactCategory } from "../types/CompactCategory.sol"; library IdLib { using IdLib for uint96; diff --git a/src/types/ActivatedCompactCategory.sol b/src/types/ActivatedCompactCategory.sol deleted file mode 100644 index 54ecefa..0000000 --- a/src/types/ActivatedCompactCategory.sol +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.27; - -enum ActivatedCompactCategory { - Compact, - BatchCompact, - MultichainCompact, - BatchDepositCompact, - BatchDepositBatchCompact, - BatchDepositMultichainCompact -} diff --git a/src/types/CompactCategory.sol b/src/types/CompactCategory.sol new file mode 100644 index 0000000..f345f5e --- /dev/null +++ b/src/types/CompactCategory.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +enum CompactCategory { + Compact, + BatchCompact, + MultichainCompact +} diff --git a/src/types/EIP712Types.sol b/src/types/EIP712Types.sol index 346ca34..2285534 100644 --- a/src/types/EIP712Types.sol +++ b/src/types/EIP712Types.sol @@ -121,56 +121,35 @@ bytes32 constant EMISSARY_ASSIGNMENT_TYPEHASH = 0x5ca9a66b8bbf0d2316e90dfa3df465 /// @dev `keccak256(bytes("CompactDeposit(address allocator,uint8 resetPeriod,uint8 scope,address recipient)"))`. bytes32 constant PERMIT2_DEPOSIT_WITNESS_FRAGMENT_HASH = 0xe055493563385cc588fffacbffe2dab023fef807baa449530431169b0eeb5b69; -/// @dev `keccak256(bytes("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)TokenPermissions(address token,uint256 amount)"))`. -bytes32 constant PERMIT2_DEPOSIT_WITH_COMPACT_ACTIVATION_TYPEHASH = 0xf653f659d3a9d0c2d3b3e901b5f71b67ad949a927c0c27b41dcdf4ecddd9b489; +/// @dev `keccak256(bytes("Activation(uint256 id,Compact compact)Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount)"))`. +bytes32 constant COMPACT_ACTIVATION_TYPEHASH = 0x2bf981c42c7f423b06fa49ba996d2930887e2f1f53d9a26b8c7423ac1cf83e61; -/// @dev `keccak256(bytes("PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline,Activation witness)Activation(uint256 id,BatchCompact compact)BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256[2][] idsAndAmounts)TokenPermissions(address token,uint256 amount)"))`. -bytes32 constant PERMIT2_DEPOSIT_WITH_BATCH_COMPACT_ACTIVATION_TYPEHASH = 0x17513dd8a454440fed0be792cc9b0b440ce3fc8ee96c1b1b1a836d2846eb6756; +/// @dev `keccak256(bytes("Activation(uint256 id,BatchCompact compact)BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256[2][] idsAndAmounts)"))`. +bytes32 constant BATCH_COMPACT_ACTIVATION_TYPEHASH = 0xd14445d78213a5acddfa89171b0199de521c3b36738b835264cae18f5a53dbf3; -/// @dev `keccak256(bytes("PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline,Activation witness)Activation(uint256 id,MultichainCompact compact)MultichainCompact(address sponsor,uint256 nonce,uint256 expires,Segment[] segments)Segment(address arbiter,uint256 chainId,uint256[2][] idsAndAmounts)TokenPermissions(address token,uint256 amount)"))`. -bytes32 constant PERMIT2_DEPOSIT_WITH_MULTICHAIN_COMPACT_ACTIVATION_TYPEHASH = 0x6a707dd548c9a14542ee33ebe7c7bedffc0f1a3bc827c73459e910b3b7f7ebe1; +/// @dev `keccak256(bytes("Activation(uint256 id,MultichainCompact compact)MultichainCompact(address sponsor,uint256 nonce,uint256 expires,Segment[] segments)Segment(address arbiter,uint256 chainId,uint256[2][] idsAndAmounts)"))`. +bytes32 constant MULTICHAIN_COMPACT_ACTIVATION_TYPEHASH = 0x329b3c527a3c74b8cabc51c304669d1866b87352cafdf440ef2becd6dc261d1e; -/// @dev `keccak256(bytes("PermitBatchWitnessTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline,BatchActivation witness)BatchActivation(uint256[] ids,Compact compact)Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount)TokenPermissions(address token,uint256 amount)"))`. -bytes32 constant PERMIT2_BATCH_DEPOSIT_WITH_COMPACT_ACTIVATION_TYPEHASH = 0xc57615aa5f4f1313fa11445825d867465f56dfccd036e04265bfe0dd050822fd; +/// @dev `keccak256(bytes("BatchActivation(uint256[] ids,Compact compact)Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount)"))`. +bytes32 constant COMPACT_BATCH_ACTIVATION_TYPEHASH = 0x45012d42fad8c9e937cff5a2d750ee18713dd45aadcd718660d5523056618d99; -/// @dev `keccak256(bytes("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)TokenPermissions(address token,uint256 amount)"))`. -bytes32 constant PERMIT2_BATCH_DEPOSIT_WITH_BATCH_COMPACT_ACTIVATION_TYPEHASH = 0x95e426a66b0811209294f67c0bf2ae0aab045490466d04cd6bf18cb70295c0ad; +/// @dev `keccak256(bytes("BatchActivation(uint256[] ids,BatchCompact compact)BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256[2][] idsAndAmounts)"))`. +bytes32 constant BATCH_COMPACT_BATCH_ACTIVATION_TYPEHASH = 0xc2e16a823b8cdddfdf889991d7a461f0a19faf1f8e608f1c164495a52151cc3e; -/// @dev `keccak256(bytes("PermitBatchWitnessTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline,BatchActivation witness)BatchActivation(uint256[] ids,MultichainCompact compact)MultichainCompact(address sponsor,uint256 nonce,uint256 expires,Segment[] segments)Segment(address arbiter,uint256 chainId,uint256[2][] idsAndAmounts)TokenPermissions(address token,uint256 amount)"))`. -bytes32 constant PERMIT2_BATCH_DEPOSIT_WITH_MULTICHAIN_COMPACT_ACTIVATION_TYPEHASH = 0x142f91d42a44f5b5264d0eac9eddd2080a6c1649a31d76457dcc0ff612ff69d6; +/// @dev `keccak256(bytes("BatchActivation(uint256[] ids,MultichainCompact compact)MultichainCompact(address sponsor,uint256 nonce,uint256 expires,Segment[] segments)Segment(address arbiter,uint256 chainId,uint256[2][] idsAndAmounts)"))`. +bytes32 constant MULTICHAIN_COMPACT_BATCH_ACTIVATION_TYPEHASH = 0xd2f6ad391328936f118250f231e63c7e639f9756a9ebf972d81763870a772d87; -// abi.decode(bytes("PermitWitnessTransferFrom(TokenP"), (bytes32)) -bytes32 constant PERMIT2_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_ONE = 0x5065726d69745769746e6573735472616e7366657246726f6d28546f6b656e50; +// abi.decode(bytes("Activation witness)Activation(ui"), (bytes32)) +bytes32 constant PERMIT2_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_ONE = 0x41637469766174696f6e207769746e6573732941637469766174696f6e287569; -// abi.decode(bytes("ermissions permitted,address spe"), (bytes32)) -bytes32 constant PERMIT2_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_TWO = 0x65726d697373696f6e73207065726d69747465642c6164647265737320737065; +// uint72(abi.decode(bytes("nt256 id,"), (bytes9))) +uint72 constant PERMIT2_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_TWO = 0x6e743235362069642c; -// abi.decode(bytes("nder,uint256 nonce,uint256 deadl"), (bytes32)) -bytes32 constant PERMIT2_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_THREE = 0x6e6465722c75696e74323536206e6f6e63652c75696e7432353620646561646c; +// abi.decode(bytes("BatchActivation witness)BatchAct"), (bytes32)) +bytes32 constant PERMIT2_BATCH_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_ONE = 0x426174636841637469766174696f6e207769746e657373294261746368416374; -// abi.decode(bytes("ine,Activation witness)Activatio"), (bytes32)) -bytes32 constant PERMIT2_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_FOUR = 0x696e652c41637469766174696f6e207769746e6573732941637469766174696f; - -// uint104(abi.decode(bytes("n(uint256 id,"), (bytes13))) -uint104 constant PERMIT2_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_FIVE = 0x6e2875696e743235362069642c; - -// abi.decode(bytes("PermitBatchWitnessTransferFrom(T"), (bytes32)) -bytes32 constant PERMIT2_BATCH_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_ONE = 0x5065726d697442617463685769746e6573735472616e7366657246726f6d2854; - -// abi.decode(bytes("okenPermissions[] permitted,addr"), (bytes32)) -bytes32 constant PERMIT2_BATCH_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_TWO = 0x6f6b656e5065726d697373696f6e735b5d207065726d69747465642c61646472; - -// abi.decode(bytes("ess spender,uint256 nonce,uint25"), (bytes32)) -bytes32 constant PERMIT2_BATCH_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_THREE = 0x657373207370656e6465722c75696e74323536206e6f6e63652c75696e743235; - -// abi.decode(bytes("6 deadline,BatchActivation witne"), (bytes32)) -bytes32 constant PERMIT2_BATCH_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_FOUR = 0x3620646561646c696e652c426174636841637469766174696f6e207769746e65; - -// abi.decode(bytes("ss)BatchActivation(uint256[] ids"), (bytes32)) -bytes32 constant PERMIT2_BATCH_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_FIVE = 0x737329426174636841637469766174696f6e2875696e743235365b5d20696473; - -// uint8(abi.decode(bytes("n(uint256 id,"), (bytes1))) -uint8 constant PERMIT2_BATCH_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_SIX = 0x2c; +// uint176(abi.decode(bytes("ivation(uint256[] ids,"), (bytes22))) +uint176 constant PERMIT2_BATCH_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_TWO = 0x69766174696f6e2875696e743235365b5d206964732c; // abi.decode(bytes("Compact compact)Compact(address "), (bytes32)) bytes32 constant PERMIT2_ACTIVATION_COMPACT_TYPESTRING_FRAGMENT_ONE = 0x436f6d7061637420636f6d7061637429436f6d70616374286164647265737320; @@ -213,3 +192,9 @@ bytes32 constant PERMIT2_ACTIVATION_MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_FIVE // uint128(abi.decode(bytes("] idsAndAmounts,"), (bytes16))) uint128 constant PERMIT2_ACTIVATION_MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_SIX = 0x5d20696473416e64416d6f756e74732c; + +// abi.decode(bytes(")TokenPermissions(address token,"), (bytes32)) +bytes32 constant TOKEN_PERMISSIONS_TYPESTRING_FRAGMENT_ONE = 0x29546f6b656e5065726d697373696f6e73286164647265737320746f6b656e2c; + +// uint120(abi.decode(bytes("uint256 amount)"), (bytes15))) +uint120 constant TOKEN_PERMISSIONS_TYPESTRING_FRAGMENT_TWO = 0x75696e7432353620616d6f756e7429; diff --git a/test/TheCompact.t.sol b/test/TheCompact.t.sol index 4d266dd..3ab3759 100644 --- a/test/TheCompact.t.sol +++ b/test/TheCompact.t.sol @@ -7,6 +7,7 @@ import { MockERC20 } from "../lib/solady/test/utils/mocks/MockERC20.sol"; 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 { ISignatureTransfer } from "permit2/src/interfaces/ISignatureTransfer.sol"; import { HashLib } from "../src/lib/HashLib.sol"; @@ -341,6 +342,88 @@ contract TheCompactTest is Test { assert(bytes(theCompact.tokenURI(id)).length > 0); } + function test_depositAndRegisterViaPermit2ThenClaim() public { + ResetPeriod resetPeriod = ResetPeriod.TenMinutes; + Scope scope = Scope.Multichain; + uint256 amount = 1e18; + uint256 nonce = 0; + uint256 deadline = block.timestamp + 1000; + uint256 expires = block.timestamp + 1000; + address claimant = 0x1111111111111111111111111111111111111111; + address arbiter = 0x2222222222222222222222222222222222222222; + + vm.prank(allocator); + uint96 allocatorId = theCompact.__registerAllocator(allocator, ""); + + bytes32 domainSeparator = keccak256(abi.encode(permit2EIP712DomainHash, keccak256(bytes("Permit2")), block.chainid, address(permit2))); + + assertEq(domainSeparator, EIP712(permit2).DOMAIN_SEPARATOR()); + + string memory compactTypestring = "Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount)"; + + bytes32 typehash = keccak256(bytes(compactTypestring)); + + uint256 id = (uint256(scope) << 255) | (uint256(resetPeriod) << 252) | (uint256(allocatorId) << 160) | uint256(uint160(address(token))); + + bytes32 claimHash = keccak256(abi.encode(typehash, arbiter, swapper, nonce, expires, id, amount)); + + bytes32 activationTypehash = keccak256(bytes(string.concat("Activation(uint256 id,Compact compact)", compactTypestring))); + + 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)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)) + ) + ) + ) + ); + + (bytes32 r, bytes32 vs) = vm.signCompact(swapperPrivateKey, digest); + bytes memory signature = abi.encodePacked(r, vs); + + uint256 returnedId = theCompact.depositAndRegister(address(token), amount, nonce, deadline, swapper, allocator, resetPeriod, scope, claimHash, CompactCategory.Compact, "", signature); + vm.snapshotGasLastCall("depositAndRegisterViaPermit2"); + assertEq(returnedId, id); + + (address derivedToken, address derivedAllocator, ResetPeriod derivedResetPeriod, Scope derivedScope) = theCompact.getLockDetails(id); + assertEq(derivedToken, address(token)); + assertEq(derivedAllocator, allocator); + assertEq(uint256(derivedResetPeriod), uint256(resetPeriod)); + assertEq(uint256(derivedScope), uint256(scope)); + + assertEq(token.balanceOf(address(theCompact)), amount); + assertEq(theCompact.balanceOf(swapper, id), amount); + + digest = keccak256(abi.encodePacked(bytes2(0x1901), theCompact.DOMAIN_SEPARATOR(), claimHash)); + + bytes memory sponsorSignature = ""; + + (r, vs) = vm.signCompact(allocatorPrivateKey, digest); + bytes memory allocatorSignature = abi.encodePacked(r, vs); + + BasicClaim memory claim = BasicClaim(allocatorSignature, sponsorSignature, swapper, nonce, expires, id, amount, claimant, amount); + + vm.prank(arbiter); + (bool status) = theCompact.claim(claim); + vm.snapshotGasLastCall("claim"); + assert(status); + + assertEq(token.balanceOf(address(theCompact)), amount); + assertEq(theCompact.balanceOf(swapper, id), 0); + assertEq(theCompact.balanceOf(claimant, id), amount); + } + + /* TODO: add this test back once there's room for batch permit2 deposits again function test_depositBatchViaPermit2SingleERC20() public { address recipient = 0x1111111111111111111111111111111111111111; ResetPeriod resetPeriod = ResetPeriod.TenMinutes; @@ -397,6 +480,7 @@ contract TheCompactTest is Test { assertEq(theCompact.balanceOf(recipient, ids[0]), amount); assert(bytes(theCompact.tokenURI(ids[0])).length > 0); } + */ function test_basicTransfer() public { ResetPeriod resetPeriod = ResetPeriod.TenMinutes; @@ -896,6 +980,51 @@ contract TheCompactTest is Test { assertEq(theCompact.balanceOf(claimant, id), amount); } + function test_registerAndClaim() public { + ResetPeriod resetPeriod = ResetPeriod.TenMinutes; + Scope scope = Scope.Multichain; + uint256 amount = 1e18; + uint256 nonce = 0; + uint256 expires = block.timestamp + 1000; + address claimant = 0x1111111111111111111111111111111111111111; + address arbiter = 0x2222222222222222222222222222222222222222; + + vm.prank(allocator); + theCompact.__registerAllocator(allocator, ""); + + vm.prank(swapper); + uint256 id = theCompact.deposit{ value: amount }(allocator, resetPeriod, scope, swapper); + assertEq(theCompact.balanceOf(swapper, id), amount); + + bytes32 typehash = keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount)"); + + bytes32 claimHash = keccak256(abi.encode(typehash, arbiter, swapper, nonce, expires, id, amount)); + + bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), theCompact.DOMAIN_SEPARATOR(), claimHash)); + + vm.prank(swapper); + (bool status) = theCompact.register(claimHash, typehash); + vm.snapshotGasLastCall("register"); + assert(status); + + bytes memory sponsorSignature = ""; + + (bytes32 r, bytes32 vs) = vm.signCompact(allocatorPrivateKey, digest); + bytes memory allocatorSignature = abi.encodePacked(r, vs); + + BasicClaim memory claim = BasicClaim(allocatorSignature, sponsorSignature, swapper, nonce, expires, id, amount, claimant, amount); + + vm.prank(arbiter); + (status) = theCompact.claim(claim); + vm.snapshotGasLastCall("claim"); + assert(status); + + assertEq(address(theCompact).balance, amount); + assertEq(claimant.balance, 0); + assertEq(theCompact.balanceOf(swapper, id), 0); + assertEq(theCompact.balanceOf(claimant, id), amount); + } + function test_claimAndWithdraw() public { ResetPeriod resetPeriod = ResetPeriod.TenMinutes; Scope scope = Scope.Multichain; @@ -1039,6 +1168,93 @@ contract TheCompactTest is Test { assertEq(theCompact.balanceOf(claimant, id), amount); } + function test_depositAndRegisterWithWitnessViaPermit2ThenClaim() public { + ResetPeriod resetPeriod = ResetPeriod.TenMinutes; + Scope scope = Scope.Multichain; + uint256 amount = 1e18; + uint256 nonce = 0; + uint256 deadline = block.timestamp + 1000; + uint256 expires = block.timestamp + 1000; + address claimant = 0x1111111111111111111111111111111111111111; + address arbiter = 0x2222222222222222222222222222222222222222; + + vm.prank(allocator); + uint96 allocatorId = theCompact.__registerAllocator(allocator, ""); + + 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 = + "Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,CompactWitness witness)CompactWitness(uint256 witnessArgument)"; + + bytes32 typehash = keccak256(bytes(compactTypestring)); + + uint256 id = (uint256(scope) << 255) | (uint256(resetPeriod) << 252) | (uint256(allocatorId) << 160) | uint256(uint160(address(token))); + + bytes32 claimHash = keccak256(abi.encode(typehash, arbiter, swapper, nonce, expires, id, amount, witness)); + + bytes32 activationTypehash = keccak256(bytes(string.concat("Activation(uint256 id,Compact compact)", compactTypestring))); + + 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)) + ) + ) + ) + ); + + (bytes32 r, bytes32 vs) = vm.signCompact(swapperPrivateKey, digest); + bytes memory signature = abi.encodePacked(r, vs); + + uint256 returnedId = + theCompact.depositAndRegister(address(token), amount, nonce, deadline, swapper, allocator, resetPeriod, scope, claimHash, CompactCategory.Compact, witnessTypestring, signature); + vm.snapshotGasLastCall("depositAndRegisterViaPermit2"); + assertEq(returnedId, id); + + (address derivedToken, address derivedAllocator, ResetPeriod derivedResetPeriod, Scope derivedScope) = theCompact.getLockDetails(id); + assertEq(derivedToken, address(token)); + assertEq(derivedAllocator, allocator); + assertEq(uint256(derivedResetPeriod), uint256(resetPeriod)); + assertEq(uint256(derivedScope), uint256(scope)); + + assertEq(token.balanceOf(address(theCompact)), amount); + assertEq(theCompact.balanceOf(swapper, id), amount); + + digest = keccak256(abi.encodePacked(bytes2(0x1901), theCompact.DOMAIN_SEPARATOR(), claimHash)); + + bytes memory sponsorSignature = ""; + + (r, vs) = vm.signCompact(allocatorPrivateKey, digest); + bytes memory allocatorSignature = abi.encodePacked(r, vs); + + ClaimWithWitness memory claim = ClaimWithWitness(allocatorSignature, sponsorSignature, swapper, nonce, expires, witness, witnessTypestring, id, amount, claimant, amount); + + vm.prank(arbiter); + (bool status) = theCompact.claim(claim); + vm.snapshotGasLastCall("claim"); + assert(status); + + assertEq(token.balanceOf(address(theCompact)), amount); + assertEq(theCompact.balanceOf(swapper, id), 0); + assertEq(theCompact.balanceOf(claimant, id), amount); + } + function test_qualifiedClaimWithWitness() public { ResetPeriod resetPeriod = ResetPeriod.TenMinutes; Scope scope = Scope.Multichain;