From 311cb9536d8b52024a88adc15190c22876a01adf Mon Sep 17 00:00:00 2001 From: 0age <37939117+0age@users.noreply.github.com> Date: Tue, 29 Oct 2024 22:28:37 -0700 Subject: [PATCH] use expirations for registrations and optimize storage --- snapshots/TheCompactTest.json | 72 +++++++++++++-------------- src/TheCompact.sol | 90 +++++++++++++++++++++++++--------- src/interfaces/ITheCompact.sol | 10 ++-- src/lib/IdLib.sol | 1 + test/TheCompact.t.sol | 22 ++++++++- 5 files changed, 130 insertions(+), 65 deletions(-) diff --git a/snapshots/TheCompactTest.json b/snapshots/TheCompactTest.json index 44d44f2..d28a77f 100644 --- a/snapshots/TheCompactTest.json +++ b/snapshots/TheCompactTest.json @@ -1,42 +1,42 @@ { - "basicTransfer": "57297", + "basicTransfer": "57341", "basicWithdrawal": "60321", - "batchClaim": "112336", - "batchClaimRegisteredWithDeposit": "112336", - "batchClaimRegisteredWithDepositWithWitness": "113049", - "batchClaimWithWitness": "113043", - "batchDepositAndRegisterViaPermit2": "221538", - "batchDepositAndRegisterWithWitnessViaPermit2": "221516", + "batchClaim": "112382", + "batchClaimRegisteredWithDeposit": "112382", + "batchClaimRegisteredWithDepositWithWitness": "113117", + "batchClaimWithWitness": "113111", + "batchDepositAndRegisterViaPermit2": "221918", + "batchDepositAndRegisterWithWitnessViaPermit2": "221896", "batchTransfer": "82919", - "batchWithdrawal": "101290", - "claim": "57450", - "claimAndWithdraw": "73756", - "claimWithWitness": "59909", - "depositAndRegisterViaPermit2": "123906", - "depositBatchSingleERC20": "67814", - "depositBatchSingleNative": "28117", - "depositBatchViaPermit2NativeAndERC20": "129518", - "depositBatchViaPermit2SingleERC20": "104645", - "depositERC20AndURI": "67051", - "depositERC20Basic": "67080", + "batchWithdrawal": "101334", + "claim": "57533", + "claimAndWithdraw": "73824", + "claimWithWitness": "59977", + "depositAndRegisterViaPermit2": "124252", + "depositBatchSingleERC20": "67846", + "depositBatchSingleNative": "28149", + "depositBatchViaPermit2NativeAndERC20": "129562", + "depositBatchViaPermit2SingleERC20": "104689", + "depositERC20AndURI": "67095", + "depositERC20Basic": "67124", "depositERC20ViaPermit2AndURI": "98277", - "depositETHAndURI": "26733", - "depositETHBasic": "28274", - "qualifiedBatchClaim": "113729", - "qualifiedBatchClaimWithWitness": "113172", - "qualifiedClaim": "60727", - "qualifiedClaimWithWitness": "59312", - "qualifiedSplitBatchClaim": "141275", - "qualifiedSplitBatchClaimWithWitness": "141246", - "qualifiedSplitClaim": "86999", - "qualifiedSplitClaimWithWitness": "87322", - "register": "24890", - "splitBatchClaim": "140762", - "splitBatchClaimWithWitness": "140699", - "splitBatchTransfer": "113576", + "depositETHAndURI": "26755", + "depositETHBasic": "28318", + "qualifiedBatchClaim": "113797", + "qualifiedBatchClaimWithWitness": "113240", + "qualifiedClaim": "60795", + "qualifiedClaimWithWitness": "59336", + "qualifiedSplitBatchClaim": "141343", + "qualifiedSplitBatchClaimWithWitness": "141314", + "qualifiedSplitClaim": "87067", + "qualifiedSplitClaimWithWitness": "87390", + "register": "25357", + "splitBatchClaim": "140830", + "splitBatchClaimWithWitness": "140767", + "splitBatchTransfer": "113620", "splitBatchWithdrawal": "142828", - "splitClaim": "86925", - "splitClaimWithWitness": "86379", - "splitTransfer": "83209", - "splitWithdrawal": "94141" + "splitClaim": "86993", + "splitClaimWithWitness": "86447", + "splitTransfer": "83253", + "splitWithdrawal": "94163" } \ No newline at end of file diff --git a/src/TheCompact.sol b/src/TheCompact.sol index fdc03ea..c1aca33 100644 --- a/src/TheCompact.sol +++ b/src/TheCompact.sol @@ -225,15 +225,17 @@ contract TheCompact is ITheCompact, ITheCompactClaims, ERC6909, Tstorish { /// @dev `keccak256(bytes("Claim(address,address,address,bytes32)"))`. uint256 private constant _CLAIM_EVENT_SIGNATURE = 0x770c32a2314b700d6239ee35ba23a9690f2fceb93a55d8c753e953059b3b18d4; + /// @dev `keccak256(bytes("CompactRegistered(address,bytes32,bytes32,uint256)"))`. + uint256 private constant _COMPACT_REGISTERED_SIGNATURE = 0xf78a2f33ff80ef4391f7449c748dc2d577a62cd645108f4f4069f4a7e0635b6a; + uint32 private constant _ATTEST_SELECTOR = 0x1a808f91; uint32 private constant _PERMIT_WITNESS_TRANSFER_FROM_SELECTOR = 0x137c29fe; uint32 private constant _BATCH_PERMIT_WITNESS_TRANSFER_FROM_SELECTOR = 0xfe8ec1a7; - // Rage-quit functionality (TODO: optimize storage layout) mapping(address => mapping(uint256 => uint256)) private _cutoffTime; - // TODO: optimize - mapping(address => mapping(bytes32 => bytes32)) private _registeredClaimHashes; + // slot: keccak256(_ACTIVE_REGISTRATIONS_SCOPE ++ sponsor ++ claimHash ++ typehash) => expires + uint256 private constant _ACTIVE_REGISTRATIONS_SCOPE = 0x68a30dd0; uint256 private immutable _INITIAL_CHAIN_ID; bytes32 private immutable _INITIAL_DOMAIN_SEPARATOR; @@ -258,7 +260,7 @@ contract TheCompact is ITheCompact, ITheCompactClaims, ERC6909, Tstorish { _deposit(msg.sender, id, msg.value); - _register(msg.sender, claimHash, typehash); + _register(msg.sender, claimHash, typehash, 0x258); } function deposit(address token, address allocator, uint256 amount) external returns (uint256) { @@ -268,7 +270,7 @@ contract TheCompact is ITheCompact, ITheCompactClaims, ERC6909, Tstorish { function depositAndRegister(address token, address allocator, uint256 amount, bytes32 claimHash, bytes32 typehash) external returns (uint256 id) { id = _performBasicERC20Deposit(token, allocator, amount, msg.sender); - _register(msg.sender, claimHash, typehash); + _register(msg.sender, claimHash, typehash, 0x258); } function _performBasicERC20Deposit(address token, address allocator, uint256 amount, address recipient) internal returns (uint256 id) { @@ -294,13 +296,17 @@ contract TheCompact is ITheCompact, ITheCompactClaims, ERC6909, Tstorish { } function deposit(uint256[2][] calldata idsAndAmounts, address recipient) external payable returns (bool) { - return _processBatchDeposit(idsAndAmounts, recipient); + _processBatchDeposit(idsAndAmounts, recipient); + + return true; } - function depositAndRegister(uint256[2][] calldata idsAndAmounts, bytes32[2][] calldata claimHashesAndTypehashes) external payable returns (bool) { - _registerFor(msg.sender, claimHashesAndTypehashes); + function depositAndRegister(uint256[2][] calldata idsAndAmounts, bytes32[2][] calldata claimHashesAndTypehashes, uint256 duration) external payable returns (bool) { + _processBatchDeposit(idsAndAmounts, msg.sender); - return _processBatchDeposit(idsAndAmounts, msg.sender); + _registerBatch(claimHashesAndTypehashes, duration); + + return true; } function deposit( @@ -376,7 +382,7 @@ contract TheCompact is ITheCompact, ITheCompactClaims, ERC6909, Tstorish { _checkBalanceAndDeposit(token, depositor, id, initialBalance); - _register(depositor, claimHash, compactTypehash); + _register(depositor, claimHash, compactTypehash, resetPeriod.toSeconds()); _clearTstorish(_REENTRANCY_GUARD_SLOT); @@ -587,7 +593,7 @@ contract TheCompact is ITheCompact, ITheCompactClaims, ERC6909, Tstorish { _verifyBalancesAndPerformDeposits(ids, permitted, initialTokenBalances, depositor, firstUnderlyingTokenIsNative); - _register(depositor, claimHash, compactTypehash); + _register(depositor, claimHash, compactTypehash, resetPeriod.toSeconds()); } function allocatedTransfer(BasicTransfer calldata transfer) external returns (bool) { @@ -1050,26 +1056,64 @@ contract TheCompact is ITheCompact, ITheCompactClaims, ERC6909, Tstorish { return _withdraw(msg.sender, recipient, id, amount); } - function register(bytes32 claimHash, bytes32 typehash) external returns (bool) { - _register(msg.sender, claimHash, typehash); + function register(bytes32 claimHash, bytes32 typehash, uint256 duration) external returns (bool) { + _register(msg.sender, claimHash, typehash, duration); return true; } - function _register(address sponsor, bytes32 claimHash, bytes32 typehash) internal { - _registeredClaimHashes[sponsor][claimHash] = typehash; - emit CompactRegistered(sponsor, claimHash, typehash); + function _getRegistrationStatus(address sponsor, bytes32 claimHash, bytes32 typehash) internal view returns (uint256 expires) { + assembly ("memory-safe") { + let m := mload(0x40) + mstore(add(m, 0x14), sponsor) + mstore(m, _ACTIVE_REGISTRATIONS_SCOPE) + mstore(add(m, 0x34), claimHash) + mstore(add(m, 0x54), typehash) + expires := sload(keccak256(add(m, 0x1c), 0x58)) + } + } + + function _hasNoActiveRegistration(address sponsor, bytes32 claimHash, bytes32 typehash) internal view returns (bool) { + return _getRegistrationStatus(sponsor, claimHash, typehash) <= block.timestamp; + } + + function getRegistrationStatus(address sponsor, bytes32 claimHash, bytes32 typehash) external view returns (bool isActive, uint256 expires) { + expires = _getRegistrationStatus(sponsor, claimHash, typehash); + isActive = expires > block.timestamp; } - function register(bytes32[2][] calldata claimHashesAndTypehashes) external returns (bool) { - return _registerFor(msg.sender, claimHashesAndTypehashes); + function _register(address sponsor, bytes32 claimHash, bytes32 typehash, uint256 duration) internal { + assembly ("memory-safe") { + let m := mload(0x40) + mstore(add(m, 0x14), sponsor) + mstore(m, _ACTIVE_REGISTRATIONS_SCOPE) + mstore(add(m, 0x34), claimHash) + mstore(add(m, 0x54), typehash) + let cutoffSlot := keccak256(add(m, 0x1c), 0x58) + + let expires := add(timestamp(), duration) + if or(lt(expires, sload(cutoffSlot)), gt(duration, 0x278d00)) { + // revert InvalidRegistrationDuration(uint256 duration) + mstore(0, 0x1f9a96f4) + mstore(0x20, duration) + revert(0x1c, 0x24) + } + + sstore(cutoffSlot, expires) + mstore(add(m, 0x74), expires) + log2(add(m, 0x34), 0x60, _COMPACT_REGISTERED_SIGNATURE, shr(0x60, shl(0x60, sponsor))) + } + } + + function register(bytes32[2][] calldata claimHashesAndTypehashes, uint256 duration) external returns (bool) { + return _registerBatch(claimHashesAndTypehashes, duration); } - function _registerFor(address sponsor, bytes32[2][] calldata claimHashesAndTypehashes) internal returns (bool) { + function _registerBatch(bytes32[2][] calldata claimHashesAndTypehashes, uint256 duration) internal returns (bool) { unchecked { uint256 totalClaimHashes = claimHashesAndTypehashes.length; for (uint256 i = 0; i < totalClaimHashes; ++i) { bytes32[2] calldata claimHashAndTypehash = claimHashesAndTypehashes[i]; - _register(sponsor, claimHashAndTypehash[0], claimHashAndTypehash[1]); + _register(msg.sender, claimHashAndTypehash[0], claimHashAndTypehash[1], duration); } } @@ -1152,7 +1196,7 @@ contract TheCompact is ITheCompact, ITheCompactClaims, ERC6909, Tstorish { } } - function _processBatchDeposit(uint256[2][] calldata idsAndAmounts, address recipient) internal returns (bool) { + function _processBatchDeposit(uint256[2][] calldata idsAndAmounts, address recipient) internal { _setTstorish(_REENTRANCY_GUARD_SLOT, 1); uint256 totalIds = idsAndAmounts.length; bool firstUnderlyingTokenIsNative; @@ -1198,8 +1242,6 @@ contract TheCompact is ITheCompact, ITheCompactClaims, ERC6909, Tstorish { } _clearTstorish(_REENTRANCY_GUARD_SLOT); - - return true; } function _notExpiredAndSignedByAllocator(bytes32 messageHash, address allocator, BasicTransfer calldata transferPayload) internal { @@ -1391,7 +1433,7 @@ contract TheCompact is ITheCompact, ITheCompactClaims, ERC6909, Tstorish { sponsorDomainSeparator := add(sponsorDomainSeparator, mul(iszero(sponsorDomainSeparator), domainSeparator)) } - if ((sponsorDomainSeparator != domainSeparator).or(sponsorSignature.length != 0) || _registeredClaimHashes[sponsor][messageHash] != typehash) { + if ((sponsorDomainSeparator != domainSeparator).or(sponsorSignature.length != 0) || _hasNoActiveRegistration(sponsor, messageHash, typehash)) { messageHash.signedBy(sponsor, sponsorSignature, sponsorDomainSeparator); } qualificationMessageHash.signedBy(allocator, allocatorSignature, domainSeparator); diff --git a/src/interfaces/ITheCompact.sol b/src/interfaces/ITheCompact.sol index b32b52f..0935fb2 100644 --- a/src/interfaces/ITheCompact.sol +++ b/src/interfaces/ITheCompact.sol @@ -22,7 +22,7 @@ interface ITheCompact { event Claim(address indexed sponsor, address indexed allocator, address indexed arbiter, bytes32 claimHash); event ForcedWithdrawalEnabled(address indexed account, uint256 indexed id, uint256 withdrawableAt); event ForcedWithdrawalDisabled(address indexed account, uint256 indexed id); - event CompactRegistered(address indexed sponsor, bytes32 claimHash, bytes32 typehash); + event CompactRegistered(address indexed sponsor, bytes32 claimHash, bytes32 typehash, uint256 expires); event AllocatorRegistered(uint96 allocatorId, address allocator); error InvalidToken(address token); @@ -38,6 +38,8 @@ interface ITheCompact { error InvalidScope(uint256 id); error InvalidDepositTokenOrdering(); error InvalidDepositBalanceChange(); + error Permit2CallFailed(); + error InvalidRegistrationDuration(uint256 duration); function deposit(address allocator) external payable returns (uint256 id); @@ -53,7 +55,7 @@ interface ITheCompact { function deposit(uint256[2][] calldata idsAndAmounts, address recipient) external payable returns (bool); - function depositAndRegister(uint256[2][] calldata idsAndAmounts, bytes32[2][] calldata claimHashesAndTypehashes) external payable returns (bool); + function depositAndRegister(uint256[2][] calldata idsAndAmounts, bytes32[2][] calldata claimHashesAndTypehashes, uint256 duration) external payable returns (bool); function deposit( address token, @@ -105,9 +107,9 @@ interface ITheCompact { function forcedWithdrawal(uint256 id, address recipient, uint256 amount) external returns (bool); - function register(bytes32 claimHash, bytes32 typehash) external returns (bool); + function register(bytes32 claimHash, bytes32 typehash, uint256 duration) external returns (bool); - function register(bytes32[2][] calldata claimHashesAndTypehashes) external returns (bool); + function register(bytes32[2][] calldata claimHashesAndTypehashes, uint256 duration) external returns (bool); function consume(uint256[] calldata nonces) external returns (bool); diff --git a/src/lib/IdLib.sol b/src/lib/IdLib.sol index ddc3ef0..516133f 100644 --- a/src/lib/IdLib.sol +++ b/src/lib/IdLib.sol @@ -13,6 +13,7 @@ library IdLib { using IdLib for uint96; using IdLib for uint256; using IdLib for address; + using IdLib for ResetPeriod; using MetadataLib for Lock; using EfficiencyLib for bool; using EfficiencyLib for uint8; diff --git a/test/TheCompact.t.sol b/test/TheCompact.t.sol index 3fc5684..b80f5a8 100644 --- a/test/TheCompact.t.sol +++ b/test/TheCompact.t.sol @@ -427,6 +427,10 @@ contract TheCompactTest is Test { vm.snapshotGasLastCall("depositAndRegisterViaPermit2"); assertEq(returnedId, id); + (bool isActive, uint256 expiresAt) = theCompact.getRegistrationStatus(swapper, claimHash, typehash); + assert(isActive); + assertEq(expiresAt, 0x258 + block.timestamp); + (address derivedToken, address derivedAllocator, ResetPeriod derivedResetPeriod, Scope derivedScope) = theCompact.getLockDetails(id); assertEq(derivedToken, address(token)); assertEq(derivedAllocator, allocator); @@ -1094,10 +1098,14 @@ contract TheCompactTest is Test { bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), theCompact.DOMAIN_SEPARATOR(), claimHash)); vm.prank(swapper); - (bool status) = theCompact.register(claimHash, typehash); + (bool status) = theCompact.register(claimHash, typehash, 1000); vm.snapshotGasLastCall("register"); assert(status); + (bool isActive, uint256 expiresAt) = theCompact.getRegistrationStatus(swapper, claimHash, typehash); + assert(isActive); + assertEq(expiresAt, block.timestamp + 1000); + bytes memory sponsorSignature = ""; (bytes32 r, bytes32 vs) = vm.signCompact(allocatorPrivateKey, digest); @@ -1318,6 +1326,10 @@ contract TheCompactTest is Test { vm.snapshotGasLastCall("depositAndRegisterViaPermit2"); assertEq(returnedId, id); + (bool isActive, uint256 expiresAt) = theCompact.getRegistrationStatus(swapper, claimHash, typehash); + assert(isActive); + assertEq(expiresAt, 0x258 + block.timestamp); + (address derivedToken, address derivedAllocator, ResetPeriod derivedResetPeriod, Scope derivedScope) = theCompact.getLockDetails(id); assertEq(derivedToken, address(token)); assertEq(derivedAllocator, allocator); @@ -1831,6 +1843,10 @@ contract TheCompactTest is Test { assertEq(theCompact.balanceOf(swapper, anotherId), anotherAmount); assertEq(theCompact.balanceOf(swapper, aThirdId), aThirdAmount); + (bool isActive, uint256 expiresAt) = theCompact.getRegistrationStatus(swapper, claimHash, typehash); + assert(isActive); + assertEq(expiresAt, 0x258 + block.timestamp); + claimHash = keccak256( abi.encode( keccak256("BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256[2][] idsAndAmounts)"), @@ -1965,6 +1981,10 @@ contract TheCompactTest is Test { assertEq(theCompact.balanceOf(swapper, anotherId), anotherAmount); assertEq(theCompact.balanceOf(swapper, aThirdId), aThirdAmount); + (bool isActive, uint256 expiresAt) = theCompact.getRegistrationStatus(swapper, claimHash, typehash); + assert(isActive); + assertEq(expiresAt, 0x258 + block.timestamp); + claimHash = keccak256( abi.encode( keccak256("BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256[2][] idsAndAmounts,CompactWitness witness)CompactWitness(uint256 witnessArgument)"),