diff --git a/src/TheCompact.sol b/src/TheCompact.sol index dbe8587..5e96ad9 100644 --- a/src/TheCompact.sol +++ b/src/TheCompact.sol @@ -112,7 +112,7 @@ contract TheCompact is ITheCompact, ERC6909, TheCompactLogic { CompactCategory compactCategory, string calldata witness, bytes calldata signature - ) external returns (uint256) { + ) external returns (uint256) { // Completed PR return _depositAndRegisterViaPermit2(token, depositor, resetPeriod, claimHash, compactCategory, witness, signature); } @@ -146,12 +146,16 @@ contract TheCompact is ITheCompact, ERC6909, TheCompactLogic { return _depositBatchAndRegisterViaPermit2(depositor, permitted, resetPeriod, claimHash, compactCategory, witness, signature); } - function allocatedTransfer(BasicTransfer calldata transfer) external returns (bool) { - return _processBasicTransfer(transfer, _release); + // THIS REQUIRES ALWAYS THE MSG.SENDER TO BE THE SPONSOR. WHY DO WE NOT WORK WITH A 'FROM' INPUT AND CHECK FOR AN APPROVAL? + function allocatedTransfer(BasicTransfer calldata transfer) external returns (bool) { // Completed PR + // _release is a function pointer to the _release function in the SharedLogic.sol contract + return _processBasicTransfer(transfer, _release); // TransferLogic.sol } - function allocatedWithdrawal(BasicTransfer calldata withdrawal) external returns (bool) { - return _processBasicTransfer(withdrawal, _withdraw); + // THIS REQUIRES ALWAYS THE MSG.SENDER TO BE THE SPONSOR. WHY DO WE NOT WORK WITH A 'FROM' INPUT AND CHECK FOR AN APPROVAL? + function allocatedWithdrawal(BasicTransfer calldata withdrawal) external returns (bool) { // Completed PR + // _withdraw is a function pointer to the _withdraw function in the SharedLogic.sol contract + return _processBasicTransfer(withdrawal, _withdraw); // TransferLogic.sol } function allocatedTransfer(SplitTransfer calldata transfer) external returns (bool) { @@ -178,34 +182,34 @@ contract TheCompact is ITheCompact, ERC6909, TheCompactLogic { return _processSplitBatchTransfer(withdrawal, _withdraw); } - function enableForcedWithdrawal(uint256 id) external returns (uint256) { - return _enableForcedWithdrawal(id); + function enableForcedWithdrawal(uint256 id) external returns (uint256) { // Completed PR + return _enableForcedWithdrawal(id); // WithdrawalLogic.sol } - function disableForcedWithdrawal(uint256 id) external returns (bool) { - return _disableForcedWithdrawal(id); + function disableForcedWithdrawal(uint256 id) external returns (bool) { // Completed PR + return _disableForcedWithdrawal(id); // WithdrawalLogic.sol } - function forcedWithdrawal(uint256 id, address recipient, uint256 amount) external returns (bool) { - return _processForcedWithdrawal(id, recipient, amount); + function forcedWithdrawal(uint256 id, address recipient, uint256 amount) external returns (bool) { // Completed PR + return _processForcedWithdrawal(id, recipient, amount); // WithdrawalLogic.sol } - function register(bytes32 claimHash, bytes32 typehash, uint256 duration) external returns (bool) { - _register(msg.sender, claimHash, typehash, duration); + function register(bytes32 claimHash, bytes32 typehash, uint256 duration) external returns (bool) { // Completed PR + _register(msg.sender, claimHash, typehash, duration); // RegistrationLogic.sol return true; } function getRegistrationStatus(address sponsor, bytes32 claimHash, bytes32 typehash) external view returns (bool isActive, uint256 expires) { - expires = _getRegistrationStatus(sponsor, claimHash, typehash); + expires = _getRegistrationStatus(sponsor, claimHash, typehash); // RegistrationLogic.sol isActive = expires > block.timestamp; - } + } // Completed PR function register(bytes32[2][] calldata claimHashesAndTypehashes, uint256 duration) external returns (bool) { return _registerBatch(claimHashesAndTypehashes, duration); } function consume(uint256[] calldata nonces) external returns (bool) { - return _consume(nonces); + return _consume(nonces); // AllocatorLogic.sol } function __registerAllocator(address allocator, bytes calldata proof) external returns (uint96) { @@ -216,6 +220,7 @@ contract TheCompact is ITheCompact, ERC6909, TheCompactLogic { return _getForcedWithdrawalStatus(account, id); } + // CAN WE NAME THE OUTPUT OF THIS FUNCTION TO MAKE IT CLEAR WHAT ADDRESS IS THE ALLOCATOR AND WHICH IS THE TOKEN? function getLockDetails(uint256 id) external view returns (address, address, ResetPeriod, Scope) { return _getLockDetails(id); } diff --git a/src/interfaces/ITheCompact.sol b/src/interfaces/ITheCompact.sol index 6c10b3a..b7df3d0 100644 --- a/src/interfaces/ITheCompact.sol +++ b/src/interfaces/ITheCompact.sol @@ -312,6 +312,7 @@ interface ITheCompact { /** * @notice Transfers ERC6909 tokens to a single recipient with allocator approval. + * @dev The sponsor for this transfer will always be the msg.sender. * @param transfer A BasicTransfer struct containing the following: * - allocatorSignature Authorization signature from the allocator. * - nonce Parameter enforcing replay protection, scoped to the allocator. diff --git a/src/lib/AllocatorLogic.sol b/src/lib/AllocatorLogic.sol index 72d8927..58a2e7e 100644 --- a/src/lib/AllocatorLogic.sol +++ b/src/lib/AllocatorLogic.sol @@ -31,9 +31,13 @@ contract AllocatorLogic { * @return Whether all nonces were successfully marked as consumed. */ function _consume(uint256[] calldata nonces) internal returns (bool) { + // NATSPEC COMMENT STATES THIS FUNCTION IS USED IN THE CLAIM PROCESS, BUT I CAN'T FIND ANY OTHER CALLS TO IT THEN FROM TheCompact.consume() + // NOTE: this may not be necessary, consider removing msg.sender.usingAllocatorId().mustHaveARegisteredAllocator(); + // THE CALL INDEED DOES NOT SEEM TO BE NECESSARY, BUT IT IS NICE TO PREVENT SPONSORS FROM CALLING THIS FUNCTION BY MISTAKE. + unchecked { uint256 i; @@ -41,9 +45,9 @@ contract AllocatorLogic { i := nonces.offset } - uint256 end = i + (nonces.length << 5); + uint256 end = i + (nonces.length << 5); // nonces.length << 5 = nonces.length * 32 uint256 nonce; - for (; i < end; i += 0x20) { + for (; i < end; i += 0x20) { // loop over each nonce in the array assembly ("memory-safe") { nonce := calldataload(i) } diff --git a/src/lib/ConsumerLib.sol b/src/lib/ConsumerLib.sol index ab0ad6c..a01b21b 100644 --- a/src/lib/ConsumerLib.sol +++ b/src/lib/ConsumerLib.sol @@ -9,8 +9,8 @@ pragma solidity ^0.8.27; */ library ConsumerLib { // Storage scope identifiers for nonce buckets. - uint256 private constant _ALLOCATOR_NONCE_SCOPE = 0x03f37b1a; - uint256 private constant _SPONSOR_NONCE_SCOPE = 0x8ccd9613; + uint256 private constant _ALLOCATOR_NONCE_SCOPE = 0x03f37b1a; // WHERE IS THIS COMING FROM? + uint256 private constant _SPONSOR_NONCE_SCOPE = 0x8ccd9613; // WHERE IS THIS COMING FROM? // Error thrown when attempting to consume an already-consumed nonce. error InvalidNonce(address account, uint256 nonce); @@ -74,21 +74,37 @@ library ConsumerLib { // derive the nonce bucket slot: // keccak256(_CONSUMER_NONCE_SCOPE ++ account ++ nonce[0:31]) - mstore(0x20, account) - mstore(0x0c, scope) - mstore(0x40, nonce) - let bucketSlot := keccak256(0x28, 0x37) + mstore(0x20, account) // 32 bytes offset + mstore(0x0c, scope) // 12 bytes offset + mstore(0x40, nonce) // 64 bytes offset + + // Memory looks now as follows: + // + // [ 40 bytes ][ 4 bytes ][ 20 bytes ][ 32 bytes ] <- memory content + // [ --- ][ scope ][ account ][ nonce ] + // [ 0 bytes ][ 40 bytes ][ 44 bytes ][ 64 bytes ] <- memory offset + // _ALOC_NCE_SCPE allocator + + // WE COULD SAFELY USE THE FIRST 56 BYTES, THIS WAY WE DO NOT HAVE TO STORE AND REWRITE THE FREE MEMORY POINTER + + let bucketSlot := keccak256(0x28, 0x37) // hash from 40 bytes (0x28) to 95 bytes (55 bytes length (0x37)) // Retrieve nonce bucket and check if nonce has been consumed. let bucketValue := sload(bucketSlot) let bit := shl(and(0xff, nonce), 1) + // We first mask the nonce with 0xff (000...0000 1111 1111) to get the last 8 bits + // The last 8 bits can have a value between 0 and 255. + // We now shift 1 (0000...0001) to the left by this value. + // This will result in a storage slot to be filled with up to 256 bits / nonces, rather then requiring a single slot per nonce. + + // check if the bit has already been set in the bucket if and(bit, bucketValue) { // `InvalidNonce(address,uint256)` with padding for `account`. mstore(0x0c, 0xdbc205b1000000000000000000000000) revert(0x1c, 0x44) } - // Invalidate the nonce by setting its bit. + // Invalidate the nonce by setting its bit. We use 'or' to set the bit additional to the existing bits/nonces. sstore(bucketSlot, or(bucketValue, bit)) // Restore the free memory pointer. diff --git a/src/lib/DepositLogic.sol b/src/lib/DepositLogic.sol index d858bde..7972ec3 100644 --- a/src/lib/DepositLogic.sol +++ b/src/lib/DepositLogic.sol @@ -14,7 +14,7 @@ contract DepositLogic is ConstructorLogic { using SafeTransferLib for address; // Storage slot seed for ERC6909 state, used in computing balance slots. - uint256 private constant _ERC6909_MASTER_SLOT_SEED = 0xedcaa89a82293940; // WHERE IS THIS COMING FROM? + uint256 private constant _ERC6909_MASTER_SLOT_SEED = 0xedcaa89a82293940; // WHERE IS THIS COMING FROM? // WE HAVE THIS TWICE, LETS STICK TO ONE SOURCE OF TRUTH // keccak256(bytes("Transfer(address,address,address,uint256,uint256)")). uint256 private constant _TRANSFER_EVENT_SIGNATURE = 0x1b3d7edb2e9c0b0e7c525b20aaaef0f5940d2ed71663c7d39266ecafac728859; @@ -64,9 +64,9 @@ contract DepositLogic is ConstructorLogic { mstore(0x14, to) // Length of 160 bits mstore(0x00, id) // length of 256 bits // -----------SLOT 1----------- -----------SLOT 2----------- - // master: | - 256 bytes - | [0000000000000000000][--64 bits--] - // to: | - 160 bytes - [[0000] | [---160 bits---]] - // id: | [---------256 bits---------] | - 256 bytes - + // master: | - 256 bits - | [0000000000000000000][--64 bits--] + // to: | - 160 bits - [[0000] | [---160 bits---]] + // id: | [---------256 bits---------] | - 256 bits - let toBalanceSlot := keccak256(0x00, 0x40) diff --git a/src/lib/DepositViaPermit2Lib.sol b/src/lib/DepositViaPermit2Lib.sol index 9f15929..cecb71a 100644 --- a/src/lib/DepositViaPermit2Lib.sol +++ b/src/lib/DepositViaPermit2Lib.sol @@ -139,13 +139,21 @@ library DepositViaPermit2Lib { pure returns (bytes32 activationTypehash, bytes32 compactTypehash) { + // memory location is 352 + + // Memory looks now as follows: + // -> memory pointer offset + // [ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ] <- memory content + // [ selector ][ token ][ amount ][ nonce ][ deadline ][ this addr ][ amount ][ depositor ][ --- ][ 320 ] + // [ 0 bytes ][ 32 bytes ][ 64 bytes ][ 96 bytes ][ 128 bytes ][ 160 bytes ][ 192 bytes][ 224 bytes ][ 256 bytes ][ 288 bytes ] <- memory offset + assembly ("memory-safe") { // Internal assembly function for writing the witness and typehashes. // Used to enable leaving the inline assembly scope early when the // witness is empty (no-witness case). function writeWitnessAndGetTypehashes(memLocation, c, witnessOffset, witnessLength, usesBatch) -> derivedActivationTypehash, derivedCompactTypehash { // Derive memory offset for the witness typestring data. - let memoryOffset := add(memLocation, 0x20) + let memoryOffset := add(memLocation, 0x20) // memoryOffset = 352 + 32 = 384 // Declare variables for start of Activation and Category-specific data. let activationStart @@ -154,67 +162,115 @@ library DepositViaPermit2Lib { // Handle non-batch cases. if iszero(usesBatch) { // Prepare initial Activation witness typestring fragment. - mstore(add(memoryOffset, 0x09), PERMIT2_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_TWO) - mstore(memoryOffset, PERMIT2_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_ONE) + mstore(add(memoryOffset, 0x09), PERMIT2_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_TWO) // length of 9 bytes + mstore(memoryOffset, PERMIT2_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_ONE) // overrides empty bits of fragment two // Set memory pointers for Activation and Category-specific data start. - activationStart := add(memoryOffset, 0x13) - categorySpecificStart := add(memoryOffset, 0x29) + activationStart := add(memoryOffset, 0x13) // activationStart = 384 + 19 = 403 + categorySpecificStart := add(memoryOffset, 0x29) // categorySpecificStart = 384 + 41 = 425 + + // ... + // [ 32 bytes ][ 32 bytes ][ 32 bytes ][ 9 bytes ] <- memory content + // [ --- ][ --- ][ fragment1 ][ fragment2 ] + // [ 320 bytes ][ 352 bytes ][ 384 bytes ][ 416 bytes ] <- memory offset } // Proceed with batch case if preparation of activation has not begun. if iszero(activationStart) { // Prepare initial BatchActivation witness typestring fragment. - mstore(add(memoryOffset, 0x16), PERMIT2_BATCH_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_TWO) - mstore(memoryOffset, PERMIT2_BATCH_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_ONE) + mstore(add(memoryOffset, 0x16), PERMIT2_BATCH_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_TWO) // length of 22 bytes + mstore(memoryOffset, PERMIT2_BATCH_DEPOSIT_WITH_ACTIVATION_TYPESTRING_FRAGMENT_ONE) // overrides empty bits of fragment two // Set memory pointers for Activation and Category-specific data. - activationStart := add(memoryOffset, 0x18) - categorySpecificStart := add(memoryOffset, 0x36) + activationStart := add(memoryOffset, 0x18) // activationStart = 384 + 24 = 408 + categorySpecificStart := add(memoryOffset, 0x36) // categorySpecificStart = 384 + 54 = 438 + + // ... + // [ 32 bytes ][ 32 bytes ][ 32 bytes ][ 22 bytes ] <- memory content + // [ --- ][ --- ][ fragment1 ][ fragment2 ] + // [ 320 bytes ][ 352 bytes ][ 384 bytes ][ 416 bytes ] <- memory offset } // Declare variable for end of Category-specific data. let categorySpecificEnd - // Handle Compact (non-batch, single-chain) case. + // Handle CompactCategory.Compact (non-batch, single-chain) case. if iszero(c) { // Prepare next typestring fragment using Compact witness typestring. 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) + mstore(add(categorySpecificStart, 0x50), PERMIT2_ACTIVATION_COMPACT_TYPESTRING_FRAGMENT_FOUR) // length of 16 bytes + mstore(add(categorySpecificStart, 0x40), PERMIT2_ACTIVATION_COMPACT_TYPESTRING_FRAGMENT_THREE) // overrides empty bits of fragment four // Set memory pointers for Activation and Category-specific data end. - categorySpecificEnd := add(categorySpecificStart, 0x70) - categorySpecificStart := add(categorySpecificStart, 0x10) + categorySpecificEnd := add(categorySpecificStart, 0x70) // categorySpecificEnd = 425/438 + 112 = 537/550 + categorySpecificStart := add(categorySpecificStart, 0x10) // categorySpecificStart = 425/438 + 16 = 441/454 + + // if 'usesBatch' is false, memory looks like this: + // ... + // [ 32 bytes ][ 32 bytes ][ 32 bytes ][ 9 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 16 bytes ] <- memory content + // [ --- ][ --- ][ fragment1 ][ fragment2 ][ c fragment1 ][ c fragment2 ][ c fragment3 ][ c fragment4 ] + // [ 320 bytes ][ 352 bytes ][ 384 bytes ][ 416 bytes ][ 425 bytes ][ 457 bytes ][ 489 bytes ][ 521 bytes ] <- memory offset + + // if 'usesBatch' is true, memory looks like this: + // ... + // [ 32 bytes ][ 32 bytes ][ 32 bytes ][ 22 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 16 bytes ] <- memory content + // [ --- ][ --- ][ fragment1 ][ fragment2 ][ c fragment1 ][ c fragment2 ][ c fragment3 ][ c fragment4 ] + // [ 320 bytes ][ 352 bytes ][ 384 bytes ][ 416 bytes ][ 438 bytes ][ 470 bytes ][ 502 bytes ][ 534 bytes ] <- memory offset } - // Handle BatchCompact (single-chain) case. + // Handle CompactCategory.BatchCompact (single-chain) case. if iszero(sub(c, 1)) { // Prepare next typestring fragment using BatchCompact witness typestring. 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) + mstore(add(categorySpecificStart, 0x5b), PERMIT2_ACTIVATION_BATCH_COMPACT_TYPESTRING_FRAGMENT_FOUR) // length of 27 bytes + mstore(add(categorySpecificStart, 0x40), PERMIT2_ACTIVATION_BATCH_COMPACT_TYPESTRING_FRAGMENT_THREE) // overrides empty bits of fragment four // Set memory pointers for Activation and Category-specific data end. - categorySpecificEnd := add(categorySpecificStart, 0x7b) - categorySpecificStart := add(categorySpecificStart, 0x15) + categorySpecificEnd := add(categorySpecificStart, 0x7b) // categorySpecificEnd = 425/438 + 123 = 548/561 + categorySpecificStart := add(categorySpecificStart, 0x15) // categorySpecificStart = 425/438 + 21 = 446/459 + + // if 'usesBatch' is false, memory looks like this: + // ... + // [ 32 bytes ][ 32 bytes ][ 32 bytes ][ 9 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 27 bytes ] <- memory content + // [ --- ][ --- ][ fragment1 ][ fragment2 ][ c fragment1 ][ c fragment2 ][ c fragment3 ][ c fragment4 ] + // [ 320 bytes ][ 352 bytes ][ 384 bytes ][ 416 bytes ][ 425 bytes ][ 457 bytes ][ 489 bytes ][ 521 bytes ] <- memory offset + + // if 'usesBatch' is true, memory looks like this: + // ... + // [ 32 bytes ][ 32 bytes ][ 32 bytes ][ 22 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 27 bytes ] <- memory content + // [ --- ][ --- ][ fragment1 ][ fragment2 ][ c fragment1 ][ c fragment2 ][ c fragment3 ][ c fragment4 ] + // [ 320 bytes ][ 352 bytes ][ 384 bytes ][ 416 bytes ][ 438 bytes ][ 470 bytes ][ 502 bytes ][ 534 bytes ] <- memory offset } - // Handle MultichainCompact case if preparation of compact fragment has not begun. + // Handle CompactCategory.MultichainCompact case if preparation of compact fragment has not begun. if iszero(categorySpecificEnd) { // Prepare next typestring fragment using Multichain & Segment witness typestring. 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) + mstore(add(categorySpecificStart, 0x90), PERMIT2_ACTIVATION_MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_SIX) // length of 16 bytes + mstore(add(categorySpecificStart, 0x80), PERMIT2_ACTIVATION_MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_FIVE) // overrides empty bits of fragment six + // FRAGMENT FIVE NEEDS TO BE STARTING AT 0x80 TO NOT OVERRIDE FRAGMENT FOUR AND SIX NEEDS TO BE STARTING AT 0x90 // Set memory pointers for Activation and Category-specific data end. - categorySpecificEnd := add(categorySpecificStart, 0x90) - categorySpecificStart := add(categorySpecificStart, 0x1a) + categorySpecificEnd := add(categorySpecificStart, 0xb0) // categorySpecificEnd = 425/438 + 176 = 601/614 + categorySpecificStart := add(categorySpecificStart, 0x1a) // categorySpecificStart = 425/438 + 26 = 451/464 + // ENDS BASED ON WRONG 0x60 VALUE CALCULATION, SO 0xb0 INSTEAD OF 0x90 + + // if 'usesBatch' is false, memory looks like this: + // ... + // [ 32 bytes ][ 32 bytes ][ 32 bytes ][ 9 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 16 bytes ] <- memory content + // [ --- ][ --- ][ fragment1 ][ fragment2 ][ c fragment1 ][ c fragment2 ][ c fragment3 ][ c fragment4 ][ c fragment5 ][ c fragment6 ] + // [ 320 bytes ][ 352 bytes ][ 384 bytes ][ 416 bytes ][ 425 bytes ][ 457 bytes ][ 489 bytes ][ 521 bytes ][ 553 bytes ][ 585 bytes ] <- memory offset (601) + + // if 'usesBatch' is true, memory looks like this: + // ... + // [ 32 bytes ][ 32 bytes ][ 32 bytes ][ 22 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 16 bytes ] <- memory content + // [ --- ][ --- ][ fragment1 ][ fragment2 ][ c fragment1 ][ c fragment2 ][ c fragment3 ][ c fragment4 ][ c fragment5 ][ c fragment6 ] + // [ 320 bytes ][ 352 bytes ][ 384 bytes ][ 416 bytes ][ 438 bytes ][ 470 bytes ][ 502 bytes ][ 534 bytes ][ 566 bytes ][ 598 bytes ] <- memory offset (614) } // Handle no-witness cases. @@ -268,23 +324,83 @@ library DepositViaPermit2Lib { // Leave the inline assembly scope early. leave } + // NEED TO CHECK NO-WITNESS CASES + // Copy the supplied compact witness from calldata. - calldatacopy(categorySpecificEnd, witnessOffset, witnessLength) + calldatacopy(categorySpecificEnd, witnessOffset, witnessLength) // add the witness after the typestring in memory + + // Memory example for non-batch, single-chain compact: + // ... + // [ 32 bytes ][ 32 bytes ][ 32 bytes ][ 9 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 16 bytes ] <- memory content + // [ --- ][ --- ][ fragment1 ][ fragment2 ][ c fragment1 ][ c fragment2 ][ c fragment3 ][ c fragment4 ] + // [ 320 bytes ][ 352 bytes ][ 384 bytes ][ 416 bytes ][ 425 bytes ][ 457 bytes ][ 489 bytes ][ 521 bytes ] <- memory offset + // ... + // [ xx bytes ] <- memory content + // [ witness ] + // [ 537 bytes ] <- memory offset // Insert tokenPermissions typestring fragment. let tokenPermissionsFragmentStart := add(categorySpecificEnd, witnessLength) - mstore(add(tokenPermissionsFragmentStart, 0x0e), TOKEN_PERMISSIONS_TYPESTRING_FRAGMENT_TWO) - mstore(sub(tokenPermissionsFragmentStart, 1), TOKEN_PERMISSIONS_TYPESTRING_FRAGMENT_ONE) - - // Derive total length of typestring and store at start of memory. + // we skip the first byte of fragment one, so the offset is 0x0e (14 bytes) instead of 0x0f (15 bytes) + mstore(add(tokenPermissionsFragmentStart, 0x0e), TOKEN_PERMISSIONS_TYPESTRING_FRAGMENT_TWO) // length of 15 bytes + mstore(sub(tokenPermissionsFragmentStart, 1), TOKEN_PERMISSIONS_TYPESTRING_FRAGMENT_ONE) // overrides empty bits of fragment two + + // THIS WILL OVERRIDE THE LAST BYTE OF THE WITNESS DATA, WHICH IS ALSO SUPPOSED TO BE A ')', SO IT WOULD NOT CHANGE ANYTHING. + // AFTER TALKING TO 0age ABOUT THIS, A SOLUTION FOR VERSION 1 WOULD BE TO LIMIT THE WITNESSDATA TO THE INPUT OF THE STRUCT. + // + // EXAMPLE OF A PREVIOUS WITNESS: + // Witness witness)Witness(uint256 witnessArgument) + // THE NEW WITNESS WOULD LOOK LIKE THIS: + // uint256 witnessArgument + // + // THIS WOULD LEAD TO SMALLER CALLDATA. IT ALSO ENSURES THE REQUIREMENT OF EIP-712 THAT ALL STRUCT DEFINITIONS + // ARE ALPHANUMERICALLY ORDERED IN THE TYPESTRING, SO LESS PRONE TO ERRORS BY OTHER DEVELOPERS. + + + // Memory example for non-batch, single-chain compact: + // ... + // [ 32 bytes ][ 32 bytes ][ 32 bytes ][ 9 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 16 bytes ] <- memory content + // [ --- ][ --- ][ fragment1 ][ fragment2 ][ c fragment1 ][ c fragment2 ][ c fragment3 ][ c fragment4 ] + // [ 320 bytes ][ 352 bytes ][ 384 bytes ][ 416 bytes ][ 425 bytes ][ 457 bytes ][ 489 bytes ][ 521 bytes ] <- memory offset + // ... + // [ ?? bytes ][ 31 bytes ][ 15 bytes ] <- memory content + // [ witness ][ tknFragment1 ][ tknFragment2 ] + // [ 537 bytes ][ 569(?) bytes ][ 600 (?) bytes] <- memory offset + + + // Derive total length of typestring and store at start (352 bytes) of memory. (0x2e (46 bytes) = 32 bytes - 1 byte + 15 bytes) mstore(memLocation, sub(add(tokenPermissionsFragmentStart, 0x2e), memoryOffset)) + // Example calculation for non-batch, single-chain compact: (569 + 46) - 384 = 231 + + // Memory example for non-batch, single-chain compact: + // ... + // [ 32 bytes ][ 32 bytes ][ 32 bytes ][ 9 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 16 bytes ] <- memory content + // [ --- ][ 231 ][ fragment1 ][ fragment2 ][ c fragment1 ][ c fragment2 ][ c fragment3 ][ c fragment4 ] + // [ 320 bytes ][ 352 bytes ][ 384 bytes ][ 416 bytes ][ 425 bytes ][ 457 bytes ][ 489 bytes ][ 521 bytes ] <- memory offset + // ... + // [ ?? bytes ][ 31 bytes ][ 15 bytes ] <- memory content + // [ witness ][ tknFragment1 ][ tknFragment2 ] + // [ 537 bytes ][ 569(?) bytes ][ 600 (?) bytes] <- memory offset // Derive activation typehash. derivedActivationTypehash := keccak256(activationStart, sub(tokenPermissionsFragmentStart, activationStart)) + // Data hashed: + // PERMIT2_(BATCH_)DEPOSIT_WITH_ACTIVATION_TYPESTRING (minus the first 19/24 bytes) + // PERMIT2_ACTIVATION_(BATCH/MULTICHAIN_)COMPACT_TYPESTRING + // witness calldata // Derive compact typehash. derivedCompactTypehash := keccak256(categorySpecificStart, sub(tokenPermissionsFragmentStart, categorySpecificStart)) + // Data hashed: + // PERMIT2_ACTIVATION_(BATCH/MULTICHAIN_)COMPACT_TYPESTRING + // witness calldata + + // Example of the full witness typestring for a non-batch deposit with a single-chained compact + // registration with an witness input of "Witness witness)Witness(uint256 witnessArgument)": + // Activation witness)Activation(uint256 id,Compact compact)Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Witness witness)Witness(uint256 witnessArgument)TokenPermissions(address token,uint256 amount) + // -------------------[ activation witness typestring .....................................................................................................................................................] + // ---------------------------------------------------------[ compact witness typestring ..................................................................................................................] } // Execute internal assembly function and store derived typehashes. @@ -311,7 +427,7 @@ library DepositViaPermit2Lib { mstore(0x20, idOrIdsHash) mstore(0x40, claimHash) - // Derive activation witness hash and write it to specified memory location. + // Derive activation witness hash and write it to specified (256 bytes offset) memory location. mstore(add(memoryPointer, offset), keccak256(0, 0x60)) // Restore the free memory pointer. @@ -336,10 +452,29 @@ library DepositViaPermit2Lib { // NOTE: none of these arguments are sanitized; the assumption is that they have to // match the signed values anyway, so *should* be fine not to sanitize them but could // optionally check that there are no dirty upper bits on any of them. - calldatacopy(add(m, 0x20), calldataOffset, 0x80) + calldatacopy(add(m, 0x20), calldataOffset, 0x80) + // offset of 0xa4 = 164 bytes + // length of 0x80 = 128 bytes // Derive the CompactDeposit witness hash from the prepared data. - witnessHash := keccak256(m, 0xa0) + witnessHash := keccak256(m, 0xa0) // length of 0xa0 = 160 bytes + // [witness type hash - 32 bytes][calldata copy - 128 bytes] + + // Example calldata to prove locations: + // + // 0x10d82672| 4 bytes + // 0...0c36442b4a4522e871399cd717abdd847ab11fe88| 32 bytes token + // 0...00000000000000000000000000000000000000001| 32 bytes amount + // 0...00000000000000000000000000000000000000002| 32 bytes nonce + // 0...00000000000000000000000000000000000000003| 32 bytes deadline + // 0...0c36442b4a4522e871399cd717abdd847ab11fe88| 32 bytes depositor + // 0...0c36442b4a4522e871399cd717abdd847ab11fe88| 32 bytes allocator <- offset of calldata + // 0...00000000000000000000000000000000000000004| 32 bytes resetPeriod + // 0...00000000000000000000000000000000000000001| 32 bytes scope + // 0...0c36442b4a4522e871399cd717abdd847ab11fe88| 32 bytes recipient <- length of calldatacopy + // 0...00000000000000000000000000000000000000140| 32 bytes signature offset + // 0...00000000000000000000000000000000000000002| 32 bytes signature length + // 12340000000000000000000000000000000000000...0| 32 bytes signature data } } @@ -350,15 +485,15 @@ library DepositViaPermit2Lib { */ function insertCompactDepositTypestring(uint256 memoryLocation) internal pure { assembly ("memory-safe") { - // Write the length of the typestring. + // Store the length (150 bytes) of the typestring. mstore(memoryLocation, 0x96) // Write the data for the typestring. - mstore(add(memoryLocation, 0x20), COMPACT_DEPOSIT_TYPESTRING_FRAGMENT_ONE) - mstore(add(memoryLocation, 0x40), COMPACT_DEPOSIT_TYPESTRING_FRAGMENT_TWO) - mstore(add(memoryLocation, 0x60), COMPACT_DEPOSIT_TYPESTRING_FRAGMENT_THREE) - mstore(add(memoryLocation, 0x96), COMPACT_DEPOSIT_TYPESTRING_FRAGMENT_FIVE) - mstore(add(memoryLocation, 0x80), COMPACT_DEPOSIT_TYPESTRING_FRAGMENT_FOUR) + mstore(add(memoryLocation, 0x20), COMPACT_DEPOSIT_TYPESTRING_FRAGMENT_ONE) // offset of 32 bytes + mstore(add(memoryLocation, 0x40), COMPACT_DEPOSIT_TYPESTRING_FRAGMENT_TWO) // offset of 64 bytes + mstore(add(memoryLocation, 0x60), COMPACT_DEPOSIT_TYPESTRING_FRAGMENT_THREE) // offset of 96 bytes + mstore(add(memoryLocation, 0x96), COMPACT_DEPOSIT_TYPESTRING_FRAGMENT_FIVE) // offset of 150 bytes (length only 22 bytes), so first 10 bytes are empty + mstore(add(memoryLocation, 0x80), COMPACT_DEPOSIT_TYPESTRING_FRAGMENT_FOUR) // offset of 128 bytes (overrides first 10 empty bytes of fragment 5) } } } diff --git a/src/lib/DepositViaPermit2Logic.sol b/src/lib/DepositViaPermit2Logic.sol index c272083..de12cc8 100644 --- a/src/lib/DepositViaPermit2Logic.sol +++ b/src/lib/DepositViaPermit2Logic.sol @@ -48,7 +48,7 @@ contract DepositViaPermit2Logic is DepositLogic { /** * @notice Internal function for depositing ERC20 tokens using Permit2 authorization. The * depositor must approve Permit2 to transfer the tokens on its behalf unless the token in - * question automatically grants approval to Permit2. The ERC6909 token amount received by the + * question automatically grants approval to Permit2. The ERC6909 token amount received * by the recipient is derived from the difference between the starting and ending balance held * in the resource lock, which may differ from the amount transferred depending on the * implementation details of the respective token. The Permit2 authorization signed by the @@ -61,21 +61,69 @@ contract DepositViaPermit2Logic is DepositLogic { */ function _depositViaPermit2(address token, address recipient, bytes calldata signature) internal returns (uint256) { // Derive the CompactDeposit witness hash. - bytes32 witness = uint256(0xa4).asStubborn().deriveCompactDepositWitnessHash(); + bytes32 witness = uint256(0xa4).asStubborn().deriveCompactDepositWitnessHash(); // witness data has an offest of 164 bytes in the calldata + // Memory is now set up as follows: + // -> memory pointer offset + // [ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ] <- memory content + // [ PDWFH ][ allocator ][ resetPeriod ][ scope ][ recipient ] + // [ 0 bytes ][ 32 bytes ][ 64 bytes ][ 96 bytes ][ 128 bytes ] <- memory offset + + // Note: Do NOT modify the memory from this point forward without careful memory management! // Set reentrancy lock, get initial balance, and begin preparing Permit2 call data. (uint256 id, uint256 initialBalance, uint256 m, uint256 typestringMemoryLocation) = _setReentrancyLockAndStartPreparingPermit2Call(token); + // Memory gets overwritten and looks now as follows: + // -> memory pointer offset + // [ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ] <- memory content + // [ selector ][ token ][ amount ][ nonce ][ deadline ][ this addr ][ amount ][ depositor ][ --- ][ 320 ] + // [ 0 bytes ][ 32 bytes ][ 64 bytes ][ 96 bytes ][ 128 bytes ][ 160 bytes ][ 192 bytes][ 224 bytes ][ 256 bytes ][ 288 bytes ] <- memory offset + // Insert the CompactDeposit typestring fragment. typestringMemoryLocation.insertCompactDepositTypestring(); + // Memory is now set up as follows: + // -> memory pointer offset + // [ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ] <- memory content + // [ selector ][ token ][ amount ][ nonce ][ deadline ][ this addr ][ amount ][ depositor ][ --- ][ 320 ] + // [ 0 bytes ][ 32 bytes ][ 64 bytes ][ 96 bytes ][ 128 bytes ][ 160 bytes ][ 192 bytes][ 224 bytes ][ 256 bytes ][ 288 bytes ] <- memory offset + // ... + // [ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 22 bytes ] <- memory content + // [ --- ][ frgmtLength ][ fragment1 ][ fragment2 ][ fragment3 ][ fragment4 ][ fragment5 ] + // [ 320 bytes ][ 352 bytes ][ 384 bytes ][ 416 bytes ][ 448 bytes ][ 480 bytes ][ 512 bytes ] <- memory offset + // Store the CompactDeposit witness hash. assembly ("memory-safe") { + // The witness hash is stored at an offset of 256 bytes (between the depositor and the number "320") mstore(add(m, 0x100), witness) } + // Memory is now set up as follows: + // -> memory pointer offset + // [ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ] <- memory content + // [ selector ][ token ][ amount ][ nonce ][ deadline ][ this addr ][ amount ][ depositor ][ witnessHash ][ 320 ] + // [ 0 bytes ][ 32 bytes ][ 64 bytes ][ 96 bytes ][ 128 bytes ][ 160 bytes ][ 192 bytes][ 224 bytes ][ 256 bytes ][ 288 bytes ] <- memory offset + // ... + // Write the signature and perform the Permit2 call. _writeSignatureAndPerformPermit2Call(m, uint256(0x140).asStubborn(), uint256(0x200).asStubborn(), signature); + // Explanation on why we point at 0x200 (512) instead of 0x220 (544): + // While the signature is actually at memory pointer + 544 bytes (first length, then data), the pointer to the signature still needs to be 512 bytes. + // The reason for this is, that the pointer is relative to the calldata and the signature is not accounted for in the memory pointer offset. + // The signature length will therefor actually be at 512 bytes within the calldata, since the arguments start at memory pointer + 32 bytes. + + // Memory is now set up as follows: + // -> memory pointer offset + // [ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ] <- memory content + // [ selector ][ token ][ amount ][ nonce ][ deadline ][ this addr ][ amount ][ depositor ][ witnessHash ][ 320 ] + // [ 0 bytes ][ 32 bytes ][ 64 bytes ][ 96 bytes ][ 128 bytes ][ 160 bytes ][ 192 bytes][ 224 bytes ][ 256 bytes ][ 288 bytes ] <- memory offset + // ... + // [ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 22 bytes ][ 10 bytes ][ 32 bytes ][ length of sig ] <- memory content + // [ 512 ][ frgmtLength ][ fragment1 ][ fragment2 ][ fragment3 ][ fragment4 ][ fragment5 ][ --- ][ sigLength ][ signature ] + // [ 320 bytes ][ 352 bytes ][ 384 bytes ][ 416 bytes ][ 448 bytes ][ 480 bytes ][ 512 bytes ][ 534 bytes ][ 544 bytes ][ 576 bytes ] <- memory offset + + // Note: It is now safe to modify the memory again from this point forward. + // Deposit tokens based on the balance change from the Permit2 call. _checkBalanceAndDeposit(token, recipient, id, initialBalance); @@ -117,22 +165,77 @@ contract DepositViaPermit2Logic is DepositLogic { ) 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); + // Memory looks now as follows: + // -> memory pointer offset + // [ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ] <- memory content + // [ selector ][ token ][ amount ][ nonce ][ deadline ][ this addr ][ amount ][ depositor ][ --- ][ 320 ] + // [ 0 bytes ][ 32 bytes ][ 64 bytes ][ 96 bytes ][ 128 bytes ][ 160 bytes ][ 192 bytes][ 224 bytes ][ 256 bytes ][ 288 bytes ] <- memory offset // Continue preparing Permit2 call data and get activation and compact typehashes. (bytes32 activationTypehash, bytes32 compactTypehash) = typestringMemoryLocation.writeWitnessAndGetTypehashes(compactCategory, witness, bool(false).asStubborn()); - // Derive the activation witness hash and store it. + // Memory example for non-batch, single-chain compact: + // ... + // [ 32 bytes ][ 32 bytes ][ 32 bytes ][ 9 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 16 bytes ] <- memory content + // [ --- ][ 231 ][ fragment1 ][ fragment2 ][ c fragment1 ][ c fragment2 ][ c fragment3 ][ c fragment4 ] + // [ 320 bytes ][ 352 bytes ][ 384 bytes ][ 416 bytes ][ 425 bytes ][ 457 bytes ][ 489 bytes ][ 521 bytes ] <- memory offset + // ... + // [ ?? bytes ][ 31 bytes ][ 15 bytes ] <- memory content + // [ witness ][ tknFragment1 ][ tknFragment2 ] + // [ 537 bytes ][ 569(?) bytes ][ 600 (?) bytes] <- memory offset + + + // Derive the activation witness hash (keccak256(activationTypehash, id, claimHash)) and store it at an offset of 256 bytes (0x100). activationTypehash.deriveAndWriteWitnessHash(id, claimHash, m, 0x100); + // Memory looks now as follows: + // -> memory pointer offset + // [ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ] <- memory content + // [ selector ][ token ][ amount ][ nonce ][ deadline ][ this addr ][ amount ][ depositor ][ actWitnessHash ][ 320 ] + // [ 0 bytes ][ 32 bytes ][ 64 bytes ][ 96 bytes ][ 128 bytes ][ 160 bytes ][ 192 bytes][ 224 bytes ][ 256 bytes ][ 288 bytes ] <- memory offset + // ... (example for non-batch, single-chain compact) ... + // [ 32 bytes ][ 32 bytes ][ 32 bytes ][ 9 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 16 bytes ] <- memory content + // [ --- ][ 231 ][ fragment1 ][ fragment2 ][ c fragment1 ][ c fragment2 ][ c fragment3 ][ c fragment4 ] + // [ 320 bytes ][ 352 bytes ][ 384 bytes ][ 416 bytes ][ 425 bytes ][ 457 bytes ][ 489 bytes ][ 521 bytes ] <- memory offset + // ... + // [ ?? bytes ][ 31 bytes ][ 15 bytes ] <- memory content + // [ witness ][ tknFragment1 ][ tknFragment2 ] + // [ 537 bytes ][ 569(?) bytes ][ 600 (?) bytes] <- memory offset + // Derive signature offset value. uint256 signatureOffsetValue; assembly ("memory-safe") { signatureOffsetValue := and(add(mload(add(m, 0x160)), 0x17f), not(0x1f)) } + // mload(m + 352 (0x160)) = 231 (for non-batch, single-chain compact) + // 231 + 383 (0x17f) = 614 bytes (for non-batch, single-chain compact) + // and(614, not(0x1f)) = 608 bytes (for non-batch, single-chain compact) + // signatureOffsetValue = 608 (for non-batch, single-chain compact) + // + // Explanation of and(..., not(0x1f)): + // 0x1f = 0001 1111 + // not(0x1f) = 1110 0000 + // and([...], 1110 0000) will eliminate the last 5 bits, effectively rounding down to the nearest multiple of 32 // Write the signature and perform the Permit2 call. _writeSignatureAndPerformPermit2Call(m, uint256(0x140).asStubborn(), signatureOffsetValue, signature); + // Memory looks now as follows: + // -> memory pointer offset + // [ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ] <- memory content + // [ selector ][ token ][ amount ][ nonce ][ deadline ][ this addr ][ amount ][ depositor ][ actWitnessHash ][ 320 (ts pointer) ] + // [ 0 bytes ][ 32 bytes ][ 64 bytes ][ 96 bytes ][ 128 bytes ][ 160 bytes ][ 192 bytes][ 224 bytes ][ 256 bytes ][ 288 bytes ] <- memory offset + // ... (example for non-batch, single-chain compact) ... + // [ 32 bytes ][ 32 bytes ][ 32 bytes ][ 9 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 16 bytes ] <- memory content + // [ 608 (sig pointer) ][ 231 (ts length) ][ fragment1 ][ fragment2 ][ c fragment1 ][ c fragment2 ][ c fragment3 ][ c fragment4 ] + // [ 320 bytes ][ 352 bytes ][ 384 bytes ][ 416 bytes ][ 425 bytes ][ 457 bytes ][ 489 bytes ][ 521 bytes ] <- memory offset + // ... + // [ ?? bytes ][ 31 bytes ][ 15 bytes ][ 25 bytes ][ 32 bytes ][ 64/65 bytes ] <- memory content + // [ witness ][ tknFragment1 ][ tknFragment2 ][ ------------- ][ sigLength ][ signature ] + // [ 537 bytes ][ 569(?) bytes ][ 600 (?) bytes][ 615 (?) bytes ][ 640 (?) bytes ][ 672 (?) bytes ] <- memory offset + + // WHAT IS THE TOKEN FRAGMENT USED FOR? IT IS NOT INCLUDED IN THE activationTypehash. WHY DOES THE FINAL WITNESS HASH LOOK LIKE IT IS? + // Deposit tokens based on the balance change from the Permit2 call. _checkBalanceAndDeposit(token, depositor, id, initialBalance); @@ -358,11 +461,27 @@ contract DepositViaPermit2Logic is DepositLogic { // Retrieve allocator, reset period, & scope. assembly ("memory-safe") { - allocator := calldataload(0xa4) - resetPeriod := calldataload(0xc4) - scope := calldataload(0xe4) + allocator := calldataload(0xa4) // load at offset of 0xa4 = 164 bytes + resetPeriod := calldataload(0xc4) // load at offset of 0xc4 = 196 bytes + scope := calldataload(0xe4) // load at offset of 0xe4 = 228 bytes } + // Example calldata to prove locations: + // + // 0x10d82672| 4 bytes + // 0...0c36442b4a4522e871399cd717abdd847ab11fe88| 32 bytes token <- offset of 4 bytes + // 0...00000000000000000000000000000000000000001| 32 bytes amount <- offset of 36 bytes + // 0...00000000000000000000000000000000000000002| 32 bytes nonce <- offset of 68 bytes + // 0...00000000000000000000000000000000000000003| 32 bytes deadline <- offset of 100 bytes + // 0...0c36442b4a4522e871399cd717abdd847ab11fe88| 32 bytes depositor <- offset of 132 bytes + // 0...0c36442b4a4522e871399cd717abdd847ab11fe88| 32 bytes allocator <- offset of 164 bytes + // 0...00000000000000000000000000000000000000004| 32 bytes resetPeriod <- offset of 196 bytes + // 0...00000000000000000000000000000000000000001| 32 bytes scope <- offset of 228 bytes + // 0...0c36442b4a4522e871399cd717abdd847ab11fe88| 32 bytes recipient <- offset of 260 bytes + // 0...00000000000000000000000000000000000000140| 32 bytes signature offset + // 0...00000000000000000000000000000000000000002| 32 bytes signature length + // 12340000000000000000000000000000000000000...0| 32 bytes signature data + // Get the ERC6909 token identifier of the associated resource lock. id = token.excludingNative().toIdIfRegistered(scope, resetPeriod, allocator); @@ -375,18 +494,29 @@ contract DepositViaPermit2Logic is DepositLogic { // Begin preparing Permit2 call data. mstore(m, _PERMIT_WITNESS_TRANSFER_FROM_SELECTOR) + // Copy calldata from 4-132 bytes (see proof above) at memory offset of 32 bytes calldatacopy(add(m, 0x20), 0x04, 0x80) // token, amount, nonce, deadline + // store an empty address at memory offset of 160 bytes mstore(add(m, 0xa0), address()) + // store the amount (36 bytes into the calldata) at memory offset of 192 bytes mstore(add(m, 0xc0), calldataload(0x24)) // amount + // store the depositor (132 bytes into the calldata) at memory offset of 224 bytes mstore(add(m, 0xe0), calldataload(0x84)) // depositor - mstore(add(m, 0x120), 0x140) + // store the pointer 320 at memory offset of 288 bytes (320 bytes is the pointer relative to the start of the calldata, thats why it is not 352) + mstore(add(m, 0x120), 0x140) // the 32 bytes gap at 256 bytes will be filled with the witness hash - // Derive the memory location for the typestring. - typestringMemoryLocation := add(m, 0x160) + // Derive the memory location for the typestring (352 bytes into the memory) + typestringMemoryLocation := add(m, 0x160) // the 32 bytes gap at 320 is left empty for the witness pointer // NOTE: strongly consider allocating memory here as the inline assembly scope // is being left (it *should* be fine for now as the function between assembly // blocks does not allocate any new memory). + + // Memory is now set up as follows: + // -> memory pointer offset + // [ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ] <- memory content + // [ selector ][ token ][ amount ][ nonce ][ deadline ][ this addr ][ amount ][ depositor ][ --- ][ 320 ] + // [ 0 bytes ][ 32 bytes ][ 64 bytes ][ 96 bytes ][ 128 bytes ][ 160 bytes ][ 192 bytes][ 224 bytes ][ 256 bytes ][ 288 bytes ] <- memory offset } } @@ -407,7 +537,7 @@ contract DepositViaPermit2Logic is DepositLogic { // Retrieve signature length and derive signature memory offset. let signatureLength := signature.length - let signatureMemoryOffset := add(m, add(0x20, signatureOffsetValue)) + let signatureMemoryOffset := add(m, add(0x20, signatureOffsetValue)) // memory + 32 + signatureOffsetValue (544 for TheCompact.deposit()) // Write the signature length. mstore(signatureMemoryOffset, signatureLength) @@ -415,8 +545,54 @@ contract DepositViaPermit2Logic is DepositLogic { // Copy the signature from calldata to memory. calldatacopy(add(signatureMemoryOffset, 0x20), signature.offset, signatureLength) + // Memory is now set up as follows (for TheCompact.deposit()): + // -> memory pointer offset + // [ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ] <- memory content + // [ selector ][ token ][ amount ][ nonce ][ deadline ][ this addr ][ amount ][ depositor ][ witnessHash ][ 320 ] + // [ 0 bytes ][ 32 bytes ][ 64 bytes ][ 96 bytes ][ 128 bytes ][ 160 bytes ][ 192 bytes][ 224 bytes ][ 256 bytes ][ 288 bytes ] <- memory offset + // ... + // [ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 22 bytes ][ 10 bytes ][ 32 bytes ][ 64/65 bytes ] <- memory content + // [ 512 ][ frgmtLength ][ fragment1 ][ fragment2 ][ fragment3 ][ fragment4 ][ fragment5 ][ --- ][ sigLength ][ signature ] + // [ 320 bytes ][ 352 bytes ][ 384 bytes ][ 416 bytes ][ 448 bytes ][ 480 bytes ][ 512 bytes ][ 534 bytes ][ 544 bytes ][ 576 bytes ] <- memory offset + + // Perform the Permit2 call. if iszero(and(isPermit2Deployed, call(gas(), _PERMIT2, 0, add(m, 0x1c), add(0x24, add(signatureOffsetValue, signatureLength)), 0, 0))) { + // send from memory: m + bytes 28 to (576 + signatureLength) + + // Calldata: + // 0x137c29fe <- permit2.permitWitnessTransferFrom function selector + // token,amount <- TokenPermissions struct in PermitTransferFrom struct + // nonce,deadline <- PermitTransferFrom struct + // address(this) <- to in SignatureTransferDetails struct + // amount <- requestedAmount in SignatureTransferDetails struct + // depositor <- owner + // witnessHash <- witness + // pointer(320) <- wittnessTypeString + // pointer(512) <- signature + // length,fragment <- wittnessTypeString + // length,signature <- signature + + + + // Example calldata of how a call to permitWitnessTransferFrom needs to look like: + // + // 0x137c29fe <- permit2.permitWitnessTransferFrom function selector + // 00000000000000000000000071159a834d69273CCA5C9404C3D549AE7C67B2EA <- offset 0 bytes (0x00) <- address token + // 0000000000000000000000000000000000000000000000000000000000000001 <- offset 32 bytes (0x20) <- uint256 amount + // 0000000000000000000000000000000000000000000000000000000000000002 <- offset 64 bytes (0x40) <- uint256 nonce + // 0000000000000000000000000000000000000000000000000000000000000003 <- offset 96 bytes (0x60) <- uint256 deadline + // 00000000000000000000000071159a834d69273CCA5C9404C3D549AE7C67B2EA <- offset 128 bytes (0x80) <- address to + // 0000000000000000000000000000000000000000000000000000000000000004 <- offset 160 bytes (0xa0) <- uint256 requestedAmount + // 00000000000000000000000071159a834d69273CCA5C9404C3D549AE7C67B2EA <- offset 192 bytes (0xc0) <- address owner + // 9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658 <- offset 224 bytes (0xe0) <- bytes32 witness + // 0000000000000000000000000000000000000000000000000000000000000140 <- offset 256 bytes (0x100) <- string witnessTypeString (pointer) + // 0000000000000000000000000000000000000000000000000000000000000180 <- offset 288 bytes (0x120) <- bytes signature (pointer) + // 0000000000000000000000000000000000000000000000000000000000000015 <- offset 320 bytes (0x140) <- witnessTypeString length + // 5769746e6573732875696e743235362074657374290000000000000000000000 <- offset 352 bytes (0x160) <- witnessTypeString + // 0000000000000000000000000000000000000000000000000000000000000002 <- offset 384 bytes (0x180) <- signature length + // 1234000000000000000000000000000000000000000000000000000000000000 <- offset 416 bytes (0x1a0) <- signature + // Bubble up if the call failed and there's data. // NOTE: consider evaluating remaining gas to protect against revert bombing if returndatasize() { diff --git a/src/lib/EventLib.sol b/src/lib/EventLib.sol index 4f7728e..9266fd9 100644 --- a/src/lib/EventLib.sol +++ b/src/lib/EventLib.sol @@ -15,6 +15,8 @@ library EventLib { // keccak256(bytes("ForcedWithdrawalStatusUpdated(address,uint256,bool,uint256)")). uint256 private constant _FORCED_WITHDRAWAL_STATUS_UPDATED_SIGNATURE = 0xe27f5e0382cf5347965fc81d5c81cd141897fe9ce402d22c496b7c2ddc84e5fd; + // I FEEL LIKE WE ARE MISSING A LITTLE BIT OF CONSISTENCY WITH THIS LIBRARY, WHY DO WE NOT DEPLOY ALL EVENTS WITH IT? + /** * @notice Internal function for emitting claim events. The sponsor and allocator * addresses are sanitized before emission. diff --git a/src/lib/HashLib.sol b/src/lib/HashLib.sol index 7313fae..2d276e4 100644 --- a/src/lib/HashLib.sol +++ b/src/lib/HashLib.sol @@ -62,8 +62,33 @@ library HashLib { // Remaining data copied from calldata: nonce, expires, id & amount. calldatacopy(add(m, 0x60), add(transfer, 0x20), 0x80) + // Explaining the 'transfer' struct pointer: + // Example calldata: + // + // 0xdd589cfc + // 0...00000000000000000000000000000000000000020 <- offset 0 <- 0x00 <- transfer struct pointer + // 0...000000000000000000000000000000000000000c0 <- offset 32 <- 0x20 <- allocatorSig pointer + // 0...00000000000000000000000000000000000000001 <- offset 64 <- 0x40 <- uint256 nonce + // 0...00000000000000000000000000000000000000002 <- offset 96 <- 0x60 <- uint256 expires + // 0...00000000000000000000000000000000000000003 <- offset 128 <- 0x80 <- uint256 id + // 0...00000000000000000000000000000000000000004 <- offset 160 <- 0xa0 <- uint256 amount + // 0...071159a834d69273cca5c9404c3d549ae7c67b2ea <- offset 192 <- 0xc0 <- address recipient + // 0...00000000000000000000000000000000000000002 <- offset 224 <- 0xe0 <- allocatorSig length <- allocatorSig pointer points here + // 12340000000000000000000000000000000000000...0 <- offset 256 <- 0x100 <- allocatorSig data + // + // add(transfer, 0x20) => transfer used within assembly references the transfer struct pointer. + // So add(transfer, 0x20) equals to: add((0x20), 0x20) => 0x40 which is the offset of the nonce in the calldata. + + + // Memory looks now as follows: + // -> memory pointer offset + // [ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ][ 32 bytes ] <- memory content + // [ typeHash ][ msg.sender ][ msg.sender ][ nonce ][ expires ][ id ][ amount ] + // [ 0 bytes ][ 32 bytes ][ 64 bytes ][ 96 bytes ][ 128 bytes ][ 160 bytes ][ 192 bytes] <- memory offset + // arbiter sponsor + // Derive the message hash from the prepared data. - messageHash := keccak256(m, 0xe0) + messageHash := keccak256(m, 0xe0) // hash from 0 to 224 bytes (0xe0) } } diff --git a/src/lib/IdLib.sol b/src/lib/IdLib.sol index b170c31..4a8c410 100644 --- a/src/lib/IdLib.sol +++ b/src/lib/IdLib.sol @@ -264,6 +264,11 @@ library IdLib { assembly ("memory-safe") { // extract 2nd, 3rd & 4th uppermost bits resetPeriod := and(shr(252, id), 7) + + // shift 252 bits to the right, now only the uppermost 4 bits remain. + // The first, uppermost bit is the scope, which we don't need in this case. + // To get rid of the first bit, we run the value through a bitwise AND operation with 7 (0000...0000 0111). + // Now, only the 3 bits of the reset period remain. } } @@ -334,7 +339,7 @@ library IdLib { function toCompactFlag(address allocator) internal pure returns (uint8 compactFlag) { assembly ("memory-safe") { // Extract the uppermost 72 bits of the address. - let x := shr(168, shl(96, allocator)) + let x := shr(184, shl(96, allocator)) // Propagate the highest set bit. x := or(x, shr(1, x)) diff --git a/src/lib/RegistrationLib.sol b/src/lib/RegistrationLib.sol index 148dfff..5b2e0cd 100644 --- a/src/lib/RegistrationLib.sol +++ b/src/lib/RegistrationLib.sol @@ -48,6 +48,8 @@ library RegistrationLib { // Derive and load active registration storage slot to get current expiration. let cutoffSlot := keccak256(add(m, 0x1c), 0x58) // hash bits 224-928 + // LETS MOVE THE SLOT CREATION CODE TO ANOTHER INTERNAL FUNCTION, SINCE IT IS USED IN MULTIPLE PLACES. + // Compute new expiration based on current timestamp and supplied duration. let expires := add(timestamp(), duration) @@ -60,6 +62,8 @@ library RegistrationLib { revert(0x1c, 0x24) } + // WHY DO WE NOT LIMIT THE DURATION TO THE RESET PERIOD OF THE TOKEN? THIS WAY, EVERYONE KNOWS THAT A REGISTERED CLAIM IS ACTUALLY VALID. + // Store new expiration in active registration storage slot. sstore(cutoffSlot, expires) @@ -155,6 +159,8 @@ library RegistrationLib { mstore(add(m, 0x34), claimHash) mstore(add(m, 0x54), typehash) + // LETS MOVE THE SLOT CREATION CODE TO ANOTHER INTERNAL FUNCTION, SINCE IT IS USED IN MULTIPLE PLACES. + // Derive and load active registration storage slot to get current expiration. expires := sload(keccak256(add(m, 0x1c), 0x58)) } diff --git a/src/lib/RegistrationLogic.sol b/src/lib/RegistrationLogic.sol index 81e5726..fb6c5e5 100644 --- a/src/lib/RegistrationLogic.sol +++ b/src/lib/RegistrationLogic.sol @@ -25,6 +25,8 @@ contract RegistrationLogic { * @param duration The duration for which the registration remains valid. */ function _register(address sponsor, bytes32 claimHash, bytes32 typehash, uint256 duration) internal { + // WHY CAN A USER REGISTER A CLAIM FOR A TOKEN THAT THEY ENABLED A FORCE WITHDRAWAL FOR? + sponsor.registerCompactWithSpecificDuration(claimHash, typehash, duration); } diff --git a/src/lib/SharedLogic.sol b/src/lib/SharedLogic.sol index 87a32b6..5166a86 100644 --- a/src/lib/SharedLogic.sol +++ b/src/lib/SharedLogic.sol @@ -16,7 +16,7 @@ contract SharedLogic is ConstructorLogic { using SafeTransferLib for address; // Storage slot seed for ERC6909 state, used in computing balance slots. - uint256 private constant _ERC6909_MASTER_SLOT_SEED = 0xedcaa89a82293940; + uint256 private constant _ERC6909_MASTER_SLOT_SEED = 0xedcaa89a82293940; // WE HAVE THIS TWICE, LETS STICK TO ONE SOURCE OF TRUTH // keccak256(bytes("Transfer(address,address,address,uint256,uint256)")). uint256 private constant _TRANSFER_EVENT_SIGNATURE = 0x1b3d7edb2e9c0b0e7c525b20aaaef0f5940d2ed71663c7d39266ecafac728859; @@ -35,11 +35,15 @@ contract SharedLogic is ConstructorLogic { function _release(address from, address to, uint256 id, uint256 amount) internal returns (bool) { assembly ("memory-safe") { // Compute the sender's balance slot using the master slot seed. - mstore(0x20, _ERC6909_MASTER_SLOT_SEED) - mstore(0x14, from) - mstore(0x00, id) + mstore(0x20, _ERC6909_MASTER_SLOT_SEED) // length of 32 bytes (offset of 20 bytes) + mstore(0x14, from) // length of 20 bytes (offset of 20 bytes) + mstore(0x00, id) // length of 32 bytes (offset of 0 bytes) let fromBalanceSlot := keccak256(0x00, 0x40) + // WE HAVE THE SAME LOGIC FOR THE BALANCE IN MULTIPLE PLACES AND CONTRACTS (DEPOSITLOGIC.SOL), POSSIBLE TO HAVE THIS COMING FROM ONE INTERNAL FUNCTION? + // ESPECIALLY BECAUSE THE LOGIC IS HUGELY RELEVANT FOR THE CONTRACT AND WITH THE GAP BETWEEN FROM AND THE MASTER SLOT SEED, + // IT IS EASY TO END UP WITH ONE VERSION WITH THE GAP AND ONE WITHOUT. + // Load from sender's current balance. let fromBalance := sload(fromBalanceSlot) @@ -53,6 +57,7 @@ contract SharedLogic is ConstructorLogic { sstore(fromBalanceSlot, sub(fromBalance, amount)) // Compute the recipient's balance slot and update balance. + // The _ERC6909_MASTER_SLOT_SEED is still available from previously mstore(0x14, to) mstore(0x00, id) let toBalanceSlot := keccak256(0x00, 0x40) @@ -118,6 +123,7 @@ contract SharedLogic is ConstructorLogic { } } + // Balance checks are done after the token transfer to use an amount that reflects the real change in balance. assembly ("memory-safe") { // Compute the sender's balance slot using the master slot seed. mstore(0x20, _ERC6909_MASTER_SLOT_SEED) @@ -128,6 +134,8 @@ contract SharedLogic is ConstructorLogic { // Load from sender's current balance. let fromBalance := sload(fromBalanceSlot) + // SAME COMMENT AS ABOVE, LETS UNIFY THIS BALANCE / SLOT RETRIEVAL LOGIC INTO ONE INTERNAL FUNCTION. + // Revert if insufficient balance. if gt(amount, fromBalance) { mstore(0x00, 0xf4d678b8) // `InsufficientBalance()`. diff --git a/src/lib/WithdrawalLogic.sol b/src/lib/WithdrawalLogic.sol index 59b8512..7f1d7b9 100644 --- a/src/lib/WithdrawalLogic.sol +++ b/src/lib/WithdrawalLogic.sol @@ -23,7 +23,7 @@ contract WithdrawalLogic is SharedLogic { // Storage scope for forced withdrawal activation times: // slot: keccak256(_FORCED_WITHDRAWAL_ACTIVATIONS_SCOPE ++ account ++ id) => activates. - uint256 private constant _FORCED_WITHDRAWAL_ACTIVATIONS_SCOPE = 0x41d0e04b; + uint256 private constant _FORCED_WITHDRAWAL_ACTIVATIONS_SCOPE = 0x41d0e04b; // WHERE IS THIS COMING FROM? /** * @notice Internal function for initiating a forced withdrawal. Computes the withdrawable @@ -44,8 +44,15 @@ contract WithdrawalLogic is SharedLogic { assembly ("memory-safe") { // Store the time at which the forced withdrawal is enabled. sstore(cutoffTimeSlotLocation, withdrawableAt) + + // WHY DO WE OVERRIDE A PREVIOUS FORCE WITHDRAWAL TIME WITH A LATER ONE? RATHER REVERT? } + // AN ALLOCATOR NEEDS TO KEEP TRACK OF THE FORCE WITHDRAWAL STATE OF A USER PRIOR TO AN ALLOCATION. + // THE ALLOCATOR ALSO NEEDS TO MAKE SURE THAT LOCK TIMES NEVER EXCEED THE RESET PERIOD OF A TOKEN. + + // SHOULD AN ONGOING FORCE WITHDRAWAL BLOCK NEW DEPOSITS OF THE USER TO THE UNDERLYING TOKEN? + // emit the ForcedWithdrawalStatusUpdated event. id.emitForcedWithdrawalStatusUpdatedEvent(withdrawableAt); } @@ -75,6 +82,9 @@ contract WithdrawalLogic is SharedLogic { sstore(cutoffTimeSlotLocation, 0) } + // AN ALLOCATOR NEEDS TO KEEP TRACK OF THE FORCE WITHDRAWAL STATE OF A USER PRIOR TO AN ALLOCATION. + // THE ALLOCATOR ALSO NEEDS TO MAKE SURE THAT LOCK TIMES NEVER EXCEED THE RESET PERIOD OF A TOKEN. + // emit the ForcedWithdrawalStatusUpdated event. id.emitForcedWithdrawalStatusUpdatedEvent(uint256(0).asStubborn()); @@ -104,9 +114,15 @@ contract WithdrawalLogic is SharedLogic { mstore(0, 0x9287bcb0) mstore(0x20, id) revert(0x1c, 0x24) + + // COULD PROVIDE ADDITIONAL INFORMATION IN THE REVERT, SUCH AS THE 'WITHDRAWABLE AT' TIME. } } + // THE FORCED WITHDRAWAL WILL STAY ENABLED AFTER PROCESSING. SO AT ANY TIME, THE USER CAN CONTINUE TO WITHDRAW. + // SO AT THE MOMENT, A FORCED WITHDRAWAL IS A STATE, THAT CAN STAY ENABLED WHICH REQUIRES TO DISCONTINUE ALL THE SUPPORT FOR THE TOKEN. + // SHOULD A FORCED WITHDRAWAL NOT RATHER BE THIS "LAST RESORT" EXIT? SO ALL OF THE USERS TOKENS WILL GET WITHDRAWN AND NO DEPOSITS ARE ALLOWED ANYMORE DURING THE RESET PERIOD? + // Process the withdrawal. return _withdraw(msg.sender, recipient, id, amount); } @@ -150,6 +166,15 @@ contract WithdrawalLogic is SharedLogic { mstore(0, _FORCED_WITHDRAWAL_ACTIVATIONS_SCOPE) mstore(0x34, id) + // -----------SLOT 1----------- -----------SLOT 2----------- -----------SLOT 3----------- + // master: | [[00000000000][--32 bits--]] | - 256 bits - + // account: | - 160 bits - [[0000] | [---160 bits---]] + // id: | - 256 bits - | - 160 bits - [----------|-256 bits-----------] - 96 bits - + + // WHY DO WE USE THIS MASTER -> ACCOUNT -> ID STRUCTURE? FOR THE BALANCE SLOT, WE USE ID -> ACCOUNT -> MASTER. + // THIS WOULD ALLOW US TO SKIP THE EXTRA WORK WITH THE FREE MEMORY POINTER, AS WELL AS BEING MORE CONSISTENT. + // WOULD IN THEORY ALLOW FOR AN INTERNAL FUNCTION THAT GENERATES ALL OF THESE SLOTS, BASED ON THE MASTER SLOT SEED. + // Compute storage slot from packed data. cutoffTimeSlotLocation := keccak256(0x1c, 0x38) diff --git a/test/lib/IdLib.t.sol b/test/lib/IdLib.t.sol new file mode 100644 index 0000000..11cc0e1 --- /dev/null +++ b/test/lib/IdLib.t.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import { Test, console } from "forge-std/Test.sol"; + +import { IdLib } from "../../src/lib/IdLib.sol"; + +contract IdLibTest is Test { + function test_toCompactFlag() public pure { + address testAddress = 0x000000000044449b4B19c2B8477Dbc403Cc4DA4e; + uint8 compactFlag = IdLib.toCompactFlag(testAddress); + assertEq(compactFlag, (10 - 3)); + } +}