diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 151e687..011db2a 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -36,7 +36,10 @@ contract Deploy is Script { * @param salt Unique salt for deterministic address generation * @return The address of the deployed contract */ - function deploy(bytes memory initCode, bytes32 salt) public returns (address) { + function deploy( + bytes memory initCode, + bytes32 salt + ) public returns (address) { bytes4 selector = bytes4(keccak256("deploy(bytes,bytes32)")); bytes memory args = abi.encode(initCode, salt); bytes memory data = abi.encodePacked(selector, args); diff --git a/script/DeployApprover.s.sol b/script/DeployApprover.s.sol index 6ebab18..e408e18 100644 --- a/script/DeployApprover.s.sol +++ b/script/DeployApprover.s.sol @@ -32,7 +32,10 @@ contract DeployApprover is Script { * @param salt Unique salt for deterministic address generation * @return The address of the deployed contract */ - function deploy(bytes memory initCode, bytes32 salt) public returns (address) { + function deploy( + bytes memory initCode, + bytes32 salt + ) public returns (address) { bytes4 selector = bytes4(keccak256("deploy(bytes,bytes32)")); bytes memory args = abi.encode(initCode, salt); bytes memory data = abi.encodePacked(selector, args); diff --git a/script/DeployModule.s.sol b/script/DeployModule.s.sol index b704c5c..cf35ca6 100644 --- a/script/DeployModule.s.sol +++ b/script/DeployModule.s.sol @@ -53,7 +53,10 @@ contract DeployModule is Script { * @param salt Unique salt for deterministic address generation * @return moduleAddress The address of the deployed module */ - function deployWithCreate2(address permit3, bytes32 salt) internal returns (address moduleAddress) { + function deployWithCreate2( + address permit3, + bytes32 salt + ) internal returns (address moduleAddress) { bytes memory initCode = abi.encodePacked(type(ERC7579ApproverModule).creationCode, abi.encode(permit3)); // Call CREATE2 factory @@ -72,7 +75,10 @@ contract DeployModule is Script { * @param salt Deployment salt * @return The computed address */ - function computeAddress(address permit3, bytes32 salt) external pure returns (address) { + function computeAddress( + address permit3, + bytes32 salt + ) external pure returns (address) { bytes memory initCode = abi.encodePacked(type(ERC7579ApproverModule).creationCode, abi.encode(permit3)); bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), CREATE2_FACTORY, salt, keccak256(initCode))); diff --git a/src/MultiTokenPermit.sol b/src/MultiTokenPermit.sol index 8498e47..ca42c25 100644 --- a/src/MultiTokenPermit.sol +++ b/src/MultiTokenPermit.sol @@ -73,7 +73,12 @@ abstract contract MultiTokenPermit is PermitBase, IMultiTokenPermit { * @param token ERC721 contract address * @param tokenId The unique NFT token ID to transfer */ - function transferFromERC721(address from, address to, address token, uint256 tokenId) public override { + function transferFromERC721( + address from, + address to, + address token, + uint256 tokenId + ) public override { // Check and update dual-allowance _updateDualAllowance(from, token, tokenId, 1); @@ -166,9 +171,8 @@ abstract contract MultiTokenPermit is PermitBase, IMultiTokenPermit { } // Execute the batch transfer after all allowances are verified - IERC1155(transfer.token).safeBatchTransferFrom( - transfer.from, transfer.to, transfer.tokenIds, transfer.amounts, "" - ); + IERC1155(transfer.token) + .safeBatchTransferFrom(transfer.from, transfer.to, transfer.tokenIds, transfer.amounts, ""); } /** @@ -204,9 +208,8 @@ abstract contract MultiTokenPermit is PermitBase, IMultiTokenPermit { // Check and update dual-allowance _updateDualAllowance(transfer.from, transfer.token, transfer.tokenId, transfer.amount); // Execute the ERC1155 transfer - IERC1155(transfer.token).safeTransferFrom( - transfer.from, transfer.to, transfer.tokenId, transfer.amount, "" - ); + IERC1155(transfer.token) + .safeTransferFrom(transfer.from, transfer.to, transfer.tokenId, transfer.amount, ""); } } } @@ -217,7 +220,10 @@ abstract contract MultiTokenPermit is PermitBase, IMultiTokenPermit { * @return Storage key for allowance mapping */ - function _getTokenKey(address token, uint256 tokenId) internal pure returns (bytes32) { + function _getTokenKey( + address token, + uint256 tokenId + ) internal pure returns (bytes32) { // Hash token and tokenId together to ensure unique keys return keccak256(abi.encodePacked(token, tokenId)); } @@ -229,7 +235,12 @@ abstract contract MultiTokenPermit is PermitBase, IMultiTokenPermit { * @param tokenId The specific token ID * @param amount The amount to transfer (1 for ERC721, variable for ERC1155) */ - function _updateDualAllowance(address from, address token, uint256 tokenId, uint160 amount) internal { + function _updateDualAllowance( + address from, + address token, + uint256 tokenId, + uint160 amount + ) internal { bytes32 encodedId = _getTokenKey(token, tokenId); // First, try to update allowance for the specific token ID @@ -259,7 +270,10 @@ abstract contract MultiTokenPermit is PermitBase, IMultiTokenPermit { * @param revertDataPerId Revert data from specific token ID allowance check * @param revertDataWildcard Revert data from collection-wide allowance check */ - function _handleAllowanceError(bytes memory revertDataPerId, bytes memory revertDataWildcard) internal pure { + function _handleAllowanceError( + bytes memory revertDataPerId, + bytes memory revertDataWildcard + ) internal pure { if (revertDataPerId.length == 0 || revertDataWildcard.length == 0) { // If any allowance succeeded, no error to handle return; diff --git a/src/NonceManager.sol b/src/NonceManager.sol index 6411db4..cdf0d40 100644 --- a/src/NonceManager.sol +++ b/src/NonceManager.sol @@ -52,7 +52,10 @@ abstract contract NonceManager is INonceManager, EIP712 { * @param name Contract name for EIP-712 domain * @param version Contract version for EIP-712 domain */ - constructor(string memory name, string memory version) EIP712(name, version) { } + constructor( + string memory name, + string memory version + ) EIP712(name, version) { } /** * @dev Returns the domain separator for the current chain. @@ -67,7 +70,10 @@ abstract contract NonceManager is INonceManager, EIP712 { * @param salt The salt value to verify * @return True if nonce has been used, false otherwise */ - function isNonceUsed(address owner, bytes32 salt) external view returns (bool) { + function isNonceUsed( + address owner, + bytes32 salt + ) external view returns (bool) { return usedNonces[owner][salt]; } @@ -162,7 +168,10 @@ abstract contract NonceManager is INonceManager, EIP712 { * @notice This is an internal helper used by the public invalidateNonces functions * to process the actual invalidation after signature verification */ - function _processNonceInvalidation(address owner, bytes32[] memory salts) internal { + function _processNonceInvalidation( + address owner, + bytes32[] memory salts + ) internal { uint256 saltsLength = salts.length; require(saltsLength != 0, EmptyArray()); @@ -184,7 +193,10 @@ abstract contract NonceManager is INonceManager, EIP712 { * @notice This is called before processing permits to ensure each signature * can only be used once per salt value */ - function _useNonce(address owner, bytes32 salt) internal { + function _useNonce( + address owner, + bytes32 salt + ) internal { if (usedNonces[owner][salt]) { revert NonceAlreadyUsed(owner, salt); } @@ -204,7 +216,11 @@ abstract contract NonceManager is INonceManager, EIP712 { * @notice Reverts with InvalidSignature() if the signature is invalid or * the recovered signer doesn't match the expected owner */ - function _verifySignature(address owner, bytes32 structHash, bytes calldata signature) internal view { + function _verifySignature( + address owner, + bytes32 structHash, + bytes calldata signature + ) internal view { bytes32 digest = _hashTypedDataV4(structHash); // For signatures == 65 bytes ECDSA first then falling back to ERC-1271 diff --git a/src/Permit3.sol b/src/Permit3.sol index 94edf8f..7a97c49 100644 --- a/src/Permit3.sol +++ b/src/Permit3.sol @@ -328,7 +328,11 @@ contract Permit3 is IPermit3, MultiTokenPermit, NonceManager { * - >3: Increase allowance mode - adds to allowance with expiration timestamp * @notice Enforces timestamp-based locking and handles MAX_ALLOWANCE for infinite approvals */ - function _processChainPermits(address owner, uint48 timestamp, ChainPermits memory chainPermits) internal { + function _processChainPermits( + address owner, + uint48 timestamp, + ChainPermits memory chainPermits + ) internal { uint256 permitsLength = chainPermits.permits.length; for (uint256 i = 0; i < permitsLength; i++) { AllowanceOrTransfer memory p = chainPermits.permits[i]; @@ -351,7 +355,11 @@ contract Permit3 is IPermit3, MultiTokenPermit, NonceManager { * @param timestamp Current timestamp for validation * @param p The permit operation to process */ - function _processAllowanceOperation(address owner, uint48 timestamp, AllowanceOrTransfer memory p) private { + function _processAllowanceOperation( + address owner, + uint48 timestamp, + AllowanceOrTransfer memory p + ) private { // Validate tokenKey is not zero if (p.tokenKey == bytes32(0)) { revert ZeroToken(); @@ -426,7 +434,10 @@ contract Permit3 is IPermit3, MultiTokenPermit, NonceManager { * @param allowed Current allowance to modify * @param amountDelta Amount to decrease by */ - function _decreaseAllowance(Allowance memory allowed, uint160 amountDelta) private pure { + function _decreaseAllowance( + Allowance memory allowed, + uint160 amountDelta + ) private pure { if (allowed.amount != MAX_ALLOWANCE || amountDelta == MAX_ALLOWANCE) { allowed.amount = amountDelta > allowed.amount ? 0 : allowed.amount - amountDelta; } @@ -437,7 +448,10 @@ contract Permit3 is IPermit3, MultiTokenPermit, NonceManager { * @param allowed Allowance to lock * @param timestamp Current timestamp for lock tracking */ - function _lockAllowance(Allowance memory allowed, uint48 timestamp) private pure { + function _lockAllowance( + Allowance memory allowed, + uint48 timestamp + ) private pure { allowed.amount = 0; allowed.expiration = LOCKED_ALLOWANCE; allowed.timestamp = timestamp; diff --git a/src/PermitBase.sol b/src/PermitBase.sol index a38307a..4e2e281 100644 --- a/src/PermitBase.sol +++ b/src/PermitBase.sol @@ -87,7 +87,12 @@ contract PermitBase is IPermit { * @param amount Approval amount * @param expiration Optional expiration timestamp */ - function approve(address token, address spender, uint160 amount, uint48 expiration) external override { + function approve( + address token, + address spender, + uint160 amount, + uint48 expiration + ) external override { bytes32 tokenKey = bytes32(uint256(uint160(token))); _validateApproval(msg.sender, tokenKey, token, spender, expiration); @@ -105,7 +110,12 @@ contract PermitBase is IPermit { * @param amount Transfer amount (max 2^160-1) * @param token ERC20 token contract address */ - function transferFrom(address from, address to, uint160 amount, address token) public { + function transferFrom( + address from, + address to, + uint160 amount, + address token + ) public { bytes32 tokenKey = bytes32(uint256(uint160(token))); (, bytes memory revertData) = _updateAllowance(from, tokenKey, msg.sender, amount); if (revertData.length > 0) { @@ -241,7 +251,12 @@ contract PermitBase is IPermit { * @notice This function handles tokens that don't return boolean values or return false on failure * @notice Assumes the caller has already verified allowances and will revert on transfer failure */ - function _transferFrom(address from, address to, uint160 amount, address token) internal { + function _transferFrom( + address from, + address to, + uint160 amount, + address token + ) internal { IERC20(token).safeTransferFrom(from, to, amount); } } diff --git a/src/interfaces/IMultiTokenPermit.sol b/src/interfaces/IMultiTokenPermit.sol index 2d37dbb..3e562d6 100644 --- a/src/interfaces/IMultiTokenPermit.sol +++ b/src/interfaces/IMultiTokenPermit.sol @@ -153,7 +153,13 @@ interface IMultiTokenPermit { * @param amount Amount to approve (ignored for ERC721, used for ERC20/ERC1155) * @param expiration Timestamp when approval expires (0 for no expiration) */ - function approve(address token, address spender, uint256 tokenId, uint160 amount, uint48 expiration) external; + function approve( + address token, + address spender, + uint256 tokenId, + uint160 amount, + uint48 expiration + ) external; /** * @notice Execute approved ERC721 token transfer @@ -162,7 +168,12 @@ interface IMultiTokenPermit { * @param token ERC721 token address * @param tokenId The NFT token ID */ - function transferFromERC721(address from, address to, address token, uint256 tokenId) external; + function transferFromERC721( + address from, + address to, + address token, + uint256 tokenId + ) external; /** * @notice Execute approved ERC1155 token transfer @@ -172,7 +183,13 @@ interface IMultiTokenPermit { * @param tokenId The ERC1155 token ID * @param amount Transfer amount */ - function transferFromERC1155(address from, address to, address token, uint256 tokenId, uint160 amount) external; + function transferFromERC1155( + address from, + address to, + address token, + uint256 tokenId, + uint160 amount + ) external; /** * @notice Execute approved ERC721 batch transfer diff --git a/src/interfaces/INonceManager.sol b/src/interfaces/INonceManager.sol index 2e51f72..6ce015f 100644 --- a/src/interfaces/INonceManager.sol +++ b/src/interfaces/INonceManager.sol @@ -78,7 +78,10 @@ interface INonceManager is IPermit { * @param salt Salt value to check * @return true if nonce has been used */ - function isNonceUsed(address owner, bytes32 salt) external view returns (bool); + function isNonceUsed( + address owner, + bytes32 salt + ) external view returns (bool); /** * @notice Mark multiple nonces as used diff --git a/src/interfaces/IPermit.sol b/src/interfaces/IPermit.sol index c3d02a8..6185f86 100644 --- a/src/interfaces/IPermit.sol +++ b/src/interfaces/IPermit.sol @@ -164,7 +164,12 @@ interface IPermit { * @param amount The amount of tokens to approve * @param expiration The timestamp when the approval expires */ - function approve(address token, address spender, uint160 amount, uint48 expiration) external; + function approve( + address token, + address spender, + uint160 amount, + uint48 expiration + ) external; /** * @notice Transfers tokens from an approved address @@ -174,7 +179,12 @@ interface IPermit { * @param token The token contract address * @dev Requires prior approval from the owner to the caller (msg.sender) */ - function transferFrom(address from, address to, uint160 amount, address token) external; + function transferFrom( + address from, + address to, + uint160 amount, + address token + ) external; /** * @notice Executes multiple token transfers in a single transaction diff --git a/src/lib/EIP712.sol b/src/lib/EIP712.sol index 7216ab7..9a59275 100644 --- a/src/lib/EIP712.sol +++ b/src/lib/EIP712.sol @@ -48,7 +48,10 @@ abstract contract EIP712 is IERC5267 { * NOTE: These parameters cannot be changed except through a xref:learn::upgrading-smart-contracts.adoc[smart * contract upgrade]. */ - constructor(string memory name, string memory version) { + constructor( + string memory name, + string memory version + ) { _name = name.toShortStringWithFallback(_nameFallback); _version = version.toShortStringWithFallback(_versionFallback); _hashedName = keccak256(bytes(name)); @@ -134,9 +137,16 @@ abstract contract EIP712 is IERC5267 { /// @dev 0x0f = 0b01111 indicates: name (bit 0), version (bit 1), chainId (bit 2), verifyingContract (bit 3) bytes1 EIP712_FIELDS = hex"0f"; - return ( - EIP712_FIELDS, _EIP712Name(), _EIP712Version(), CROSS_CHAIN_ID, address(this), bytes32(0), new uint256[](0) - ); + return + ( + EIP712_FIELDS, + _EIP712Name(), + _EIP712Version(), + CROSS_CHAIN_ID, + address(this), + bytes32(0), + new uint256[](0) + ); } /** diff --git a/src/lib/TreeNodeLib.sol b/src/lib/TreeNodeLib.sol new file mode 100644 index 0000000..f26d8f5 --- /dev/null +++ b/src/lib/TreeNodeLib.sol @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * @title TreeNodeLib + * @notice Generic library for EIP-712 tree node hash reconstruction + * @dev Provides parameterized tree reconstruction algorithm for any EIP-712 nested structure + * following the pattern: struct Node { Node[] nodes; LeafType[] leaves; } + * + * This library generalizes the tree reconstruction pattern used in both PermitNode and NonceNode, + * eliminating code duplication while maintaining full EIP-712 compatibility. + * + * Key Concepts: + * - Parameterized typehash: Caller provides EIP-712 typehash for their specific struct + * - Three combination rules: Leaf+Leaf (sort), Node+Node (sort), Node+Leaf (struct order) + * - Compact encoding: Position index + type flags for efficient on-chain reconstruction + * - Gas efficient: O(log n) proof size, linear scaling with proof length + * + * Algorithm Overview: + * The reconstruction algorithm combines a starting leaf with proof elements iteratively, + * using type flags to determine which combination rule to apply. Each combination produces + * a valid EIP-712 hash that matches the off-chain tree structure. + * + * Usage Example: + * ```solidity + * // In PermitNode context: + * bytes32 hash = TreeNodeLib.computeTreeHash( + * PERMIT_NODE_TYPEHASH, + * proofStructure, + * proof, + * currentChainHash + * ); + * + * // In NonceNode context: + * bytes32 hash = TreeNodeLib.computeTreeHash( + * NONCE_NODE_TYPEHASH, + * proofStructure, + * proof, + * currentNonce + * ); + * ``` + */ +library TreeNodeLib { + /** + * @dev Hash of an empty array in EIP-712 encoding: keccak256(abi.encodePacked()) + * Used when a node has an empty nodes[] or leaves[] array + */ + bytes32 internal constant EMPTY_ARRAY_HASH = keccak256(""); + + /** + * @notice Reconstruct the EIP-712 hash of a tree structure from proof and tree structure encoding + * @dev This is the core function that enables compact on-chain verification of tree-based structures. + * It reconstructs the full tree hash from a compact proof, avoiding the need to pass the + * entire tree structure on-chain. + * + * @dev ProofStructure Encoding Format (bytes32): + * - Byte 0 (bits 255-248): Position index (reserved for future use, currently unused) + * - Bytes 1-31 (bits 247-0): Type flags (packed bits, 1 bit per proof element) + * - 0 = proof[i] is a Leaf (e.g., ChainPermits or bytes32 nonce) + * - 1 = proof[i] is a Node (e.g., PermitNode or NonceNode) + * + * @dev Algorithm (Merkle-like path reconstruction): + * 1. Start with currentLeaf (always a Leaf type initially) + * 2. For each proof[i], extract type flag from proofStructure: + * - Bit i (at position 255-8-i) indicates if proof[i] is Node (1) or Leaf (0) + * 3. Combine current with proof[i] based on types: + * - Leaf + Leaf: Sort alphabetically, use combineLeafAndLeaf() + * - Node + Node: Sort alphabetically, use combineNodeAndNode() + * - Node + Leaf: Struct order (no sort), use combineNodeAndLeaf() + * 4. After first combine, current becomes a Node (important for subsequent combines) + * 5. Continue until all proof elements are processed + * 6. Return final hash (should match the hash user signed) + * + * @dev Security validations: + * - Proof length must not exceed 247 elements (max tree depth) + * - Unused type flag bits must be zero (prevents encoding ambiguity) + * - Each combination follows deterministic rules (ensures uniqueness) + * + * @dev Security: User signs complete tree off-chain, contract reconstructs from compact proof. + * Matching hash validates proof correctness. Any modification invalidates signature. + * + * @param typehash EIP-712 typehash for the specific struct (e.g., PERMIT_NODE_TYPEHASH or NONCE_NODE_TYPEHASH) + * @param proofStructure Compact encoding (position + type flags) + * @param proof Array of sibling hashes along the merkle path + * @param currentLeaf Hash of current leaf element (computed on-chain, e.g., ChainPermits hash or nonce) + * @return bytes32 The reconstructed tree root hash (EIP-712 style) + * + * @dev Example: For 2 leaves with currentLeaf=H(Leaf1), proof[0]=H(Leaf2), typeFlag=0: + * Result = combineLeafAndLeaf(typehash, H(Leaf1), H(Leaf2)) + * = H(Node(nodes=[], leaves=[sorted(Leaf1, Leaf2)])) + * + * @dev Uses combineLeafAndLeaf(), combineNodeAndNode(), and combineNodeAndLeaf() internally + */ + function computeTreeHash( + bytes32 typehash, + bytes32 proofStructure, + bytes32[] calldata proof, + bytes32 currentLeaf + ) internal pure returns (bytes32) { + // Validate proof length does not exceed maximum tree depth + // Maximum depth is 247 (256 bits - 8 bits for position index - 1 for current element) + require(proof.length <= 247, "Proof exceeds maximum depth"); + + // Validate that unused type flag bits are zero + // Type flags occupy bits 247 down to (247 - proof.length + 1) + // This validates that bits (247 - proof.length) down to 0 are all zero + if (proof.length < 247) { + // --- Bit manipulation explanation for unused type flags --- + // proofStructure layout (bytes32, 256 bits): + // - Bits 255-248: Position index (8 bits, currently unused) + // - Bits 247-0: Type flags (1 bit per proof element, up to 247 bits) + // + // For proof.length = N: + // - Type flags use bits 247 down to (247 - N + 1) + // - Unused type flag bits are (247 - N) down to 0, and must be zero + // + // Example: proof.length = 2 + // - mask = 0xFF00000000000000000000000000000000000000000000000000000000000000 + // (keeps bits 255-248 position index, plus bits 247-246 for 2 type flags) + // - flagBits extracts bits 247-0 (all type flag bits) + // - unusedMask = 0x0000000000000000000000000000000000000000000000000000003FFFFFFFFFFF + // (checks bits 245-0 are zero, since bits 247-246 are used) + // + // Step 1: Create mask to keep position index and used type flag bits + uint256 mask = type(uint256).max << (256 - 8 - proof.length); + // Step 2: Extract only the type flag bits (bits 247-0) + uint256 flagBits = uint256(proofStructure) & ~mask; + // Step 3: Create mask for unused type flag bits (bits below used flags) + uint256 unusedMask = type(uint256).max >> (8 + proof.length); + // Step 4: Require that all unused type flag bits are zero + require((flagBits & unusedMask) == 0, "Unused type flags must be zero"); + } + + // Position index (byte 0, bits 255-248) is reserved for future use + // Currently ignored - any value is accepted but not validated + // Future versions may enforce position validation to ensure currentLeaf + // appears at the expected index in the flattened tree structure + // uint8 position = uint8(uint256(proofStructure) >> 248); + + bytes32 currentHash = currentLeaf; + bool currentIsNode = false; // Starts as Leaf (ChainPermits, nonce, etc.) + + // Process each proof element, combining with current hash + for (uint256 i = 0; i < proof.length; i++) { + // Extract type flag for proof[i] from bit position (247 - i) + // Bits numbered 255 (MSB) down to 0 (LSB), bits 255-248 reserved for position + // Example: proof[0] uses bit 247, proof[1] uses bit 246, etc. + bool proofIsNode = (uint256(proofStructure) >> (255 - 8 - i)) & 1 == 1; + + // Combine based on types + if (!currentIsNode && !proofIsNode) { + currentHash = combineLeafAndLeaf(typehash, currentHash, proof[i]); + } else if (currentIsNode && proofIsNode) { + currentHash = combineNodeAndNode(typehash, currentHash, proof[i]); + } else if (currentIsNode && !proofIsNode) { + currentHash = combineNodeAndLeaf(typehash, currentHash, proof[i]); + } else { + // !currentIsNode && proofIsNode + currentHash = combineNodeAndLeaf(typehash, proof[i], currentHash); + } + + // After first combine, result is always a Node + currentIsNode = true; + } + + return currentHash; + } + + /** + * @notice Combine two leaf hashes into a parent node hash + * @dev Used when both siblings are leaf elements (e.g., ChainPermits, nonces) + * @dev Hashes are sorted alphabetically for deterministic tree construction + * @dev Result: Node(nodes=[], leaves=[sorted hashes]) + * + * @param typehash EIP-712 typehash for the specific struct (e.g., PERMIT_NODE_TYPEHASH or NONCE_NODE_TYPEHASH) + * @param leaf1 First leaf hash + * @param leaf2 Second leaf hash + * @return bytes32 EIP-712 hash of Node containing both leaves + */ + function combineLeafAndLeaf( + bytes32 typehash, + bytes32 leaf1, + bytes32 leaf2 + ) internal pure returns (bytes32) { + bytes32 first = leaf1 < leaf2 ? leaf1 : leaf2; + bytes32 second = leaf1 < leaf2 ? leaf2 : leaf1; + + // Create Node(nodes=[], leaves=[first, second]) + bytes32 leavesArrayHash = keccak256(abi.encodePacked(first, second)); + + return + keccak256( + abi.encode( + typehash, + EMPTY_ARRAY_HASH, // nodes = [] + leavesArrayHash // leaves = [first, second] + ) + ); + } + + /** + * @notice Combine two node hashes into a parent node hash + * @dev Used when both siblings are nested node structures + * @dev Hashes are sorted alphabetically for deterministic tree construction + * @dev Result: Node(nodes=[sorted hashes], leaves=[]) + * + * @param typehash EIP-712 typehash for the specific struct (e.g., PERMIT_NODE_TYPEHASH or NONCE_NODE_TYPEHASH) + * @param node1 First Node hash + * @param node2 Second Node hash + * @return bytes32 EIP-712 hash of parent Node containing both child nodes + */ + function combineNodeAndNode( + bytes32 typehash, + bytes32 node1, + bytes32 node2 + ) internal pure returns (bytes32) { + bytes32 first = node1 < node2 ? node1 : node2; + bytes32 second = node1 < node2 ? node2 : node1; + + // Create Node(nodes=[first, second], leaves=[]) + bytes32 nodesArrayHash = keccak256(abi.encodePacked(first, second)); + + return + keccak256( + abi.encode( + typehash, + nodesArrayHash, // nodes = [first, second] + EMPTY_ARRAY_HASH // leaves = [] + ) + ); + } + + /** + * @notice Combine a node hash and a leaf hash (mixed types) + * @dev Used when one sibling is a node and the other is a leaf element + * @dev IMPORTANT: Unlike other combinations, hashes are NOT sorted + * @dev Order follows EIP-712 struct definition: nodes before leaves + * @dev Result: Node(nodes=[nodeHash], leaves=[leafHash]) + * + * @param typehash EIP-712 typehash for the specific struct (e.g., PERMIT_NODE_TYPEHASH or NONCE_NODE_TYPEHASH) + * @param nodeHash The Node hash (always first per struct definition) + * @param leafHash The leaf hash (always second per struct definition) + * @return bytes32 EIP-712 hash of Node with mixed children + */ + function combineNodeAndLeaf( + bytes32 typehash, + bytes32 nodeHash, + bytes32 leafHash + ) internal pure returns (bytes32) { + bytes32 nodesArrayHash = keccak256(abi.encodePacked(nodeHash)); + bytes32 leavesArrayHash = keccak256(abi.encodePacked(leafHash)); + + return keccak256( + abi.encode( + typehash, + nodesArrayHash, // nodes = [nodeHash] + leavesArrayHash // leaves = [leafHash] + ) + ); + } +} diff --git a/src/modules/ERC7579ApproverModule.sol b/src/modules/ERC7579ApproverModule.sol index 141f601..5199352 100644 --- a/src/modules/ERC7579ApproverModule.sol +++ b/src/modules/ERC7579ApproverModule.sol @@ -95,7 +95,10 @@ contract ERC7579ApproverModule is IERC7579Module { * @param account The smart account executing the approvals * @param data Encoded arrays of token addresses for each token type */ - function execute(address account, bytes calldata data) external { + function execute( + address account, + bytes calldata data + ) external { // Decode the token addresses for each type (address[] memory erc20Tokens, address[] memory erc721Tokens, address[] memory erc1155Tokens) = abi.decode(data, (address[], address[], address[])); @@ -115,9 +118,7 @@ contract ERC7579ApproverModule is IERC7579Module { revert ZeroAddress(); } executions[executionIndex++] = Execution({ - target: erc20Tokens[i], - value: 0, - callData: abi.encodeCall(IERC20.approve, (PERMIT3, type(uint256).max)) + target: erc20Tokens[i], value: 0, callData: abi.encodeCall(IERC20.approve, (PERMIT3, type(uint256).max)) }); } @@ -127,9 +128,7 @@ contract ERC7579ApproverModule is IERC7579Module { revert ZeroAddress(); } executions[executionIndex++] = Execution({ - target: erc721Tokens[i], - value: 0, - callData: abi.encodeCall(IERC721.setApprovalForAll, (PERMIT3, true)) + target: erc721Tokens[i], value: 0, callData: abi.encodeCall(IERC721.setApprovalForAll, (PERMIT3, true)) }); } diff --git a/test/EIP712.t.sol b/test/EIP712.t.sol index 59d19a8..f52c973 100644 --- a/test/EIP712.t.sol +++ b/test/EIP712.t.sol @@ -7,7 +7,10 @@ import { EIP712 } from "../src/lib/EIP712.sol"; // Test contract for EIP712 functionality contract EIP712TestContract is EIP712 { - constructor(string memory name, string memory version) EIP712(name, version) { } + constructor( + string memory name, + string memory version + ) EIP712(name, version) { } // Expose internal methods for testing function domainSeparatorV4() external view returns (bytes32) { @@ -276,7 +279,10 @@ contract EIP712Test is Test { // Special contract that overrides internal method to force execution of the missing line contract AlternativeEIP712 is EIP712 { - constructor(string memory name, string memory version) EIP712(name, version) { } + constructor( + string memory name, + string memory version + ) EIP712(name, version) { } // Expose the domain separator method - this always returns the non-cached version function domainSeparatorV4() external view returns (bytes32) { diff --git a/test/ERC7702TokenApprover.t.sol b/test/ERC7702TokenApprover.t.sol index ecd3ad1..063df24 100644 --- a/test/ERC7702TokenApprover.t.sol +++ b/test/ERC7702TokenApprover.t.sol @@ -19,12 +19,18 @@ contract MockERC20 { bool public shouldFailApproval = false; - constructor(string memory _name, string memory _symbol) { + constructor( + string memory _name, + string memory _symbol + ) { name = _name; symbol = _symbol; } - function approve(address spender, uint256 amount) external returns (bool) { + function approve( + address spender, + uint256 amount + ) external returns (bool) { if (shouldFailApproval) { return false; } diff --git a/test/MultiTokenPermit.t.sol b/test/MultiTokenPermit.t.sol index 48b30a1..83ca16c 100644 --- a/test/MultiTokenPermit.t.sol +++ b/test/MultiTokenPermit.t.sol @@ -29,11 +29,17 @@ contract MockERC721 is ERC721 { _mint(to, tokenId); } - function mint(address to, uint256 tokenId) external { + function mint( + address to, + uint256 tokenId + ) external { _mint(to, tokenId); } - function mintBatch(address to, uint256 amount) external returns (uint256[] memory tokenIds) { + function mintBatch( + address to, + uint256 amount + ) external returns (uint256[] memory tokenIds) { tokenIds = new uint256[](amount); for (uint256 i = 0; i < amount; i++) { tokenIds[i] = _tokenIdCounter++; @@ -49,11 +55,21 @@ contract MockERC721 is ERC721 { contract MockERC1155 is ERC1155 { constructor() ERC1155("https://mock.uri/{id}") { } - function mint(address to, uint256 tokenId, uint256 amount, bytes memory data) external { + function mint( + address to, + uint256 tokenId, + uint256 amount, + bytes memory data + ) external { _mint(to, tokenId, amount, data); } - function mintBatch(address to, uint256[] memory tokenIds, uint256[] memory amounts, bytes memory data) external { + function mintBatch( + address to, + uint256[] memory tokenIds, + uint256[] memory amounts, + bytes memory data + ) external { _mintBatch(to, tokenIds, amounts, data); } } @@ -283,10 +299,7 @@ contract MultiTokenPermitTest is TestBase { for (uint256 i = 0; i < 3; i++) { transfers[i] = IMultiTokenPermit.ERC721Transfer({ - from: nftOwner, - to: recipientAddress, - tokenId: i, - token: address(nftToken) + from: nftOwner, to: recipientAddress, tokenId: i, token: address(nftToken) }); } @@ -520,7 +533,7 @@ contract MultiTokenPermitTest is TestBase { token: address(nftToken), tokenId: TOKEN_ID_1, amount: 1 // Should be 1 for ERC721 - }) + }) }); // ERC1155 transfer @@ -569,7 +582,7 @@ contract MultiTokenPermitTest is TestBase { token: address(nftToken), tokenId: TOKEN_ID_1, amount: 2 // Invalid: ERC721 must have amount = 1 - }) + }) }); // Should revert with InvalidAmount @@ -712,10 +725,7 @@ contract MultiTokenPermitTest is TestBase { for (uint256 i = 0; i < numTokens; i++) { transfers[i] = IMultiTokenPermit.ERC721Transfer({ - from: nftOwner, - to: recipientAddress, - tokenId: tokenIds[i], - token: address(nftToken) + from: nftOwner, to: recipientAddress, tokenId: tokenIds[i], token: address(nftToken) }); } @@ -957,10 +967,7 @@ contract MultiTokenPermitTest is TestBase { // Prepare batch transfer IMultiTokenPermit.ERC721Transfer[] memory transfers = new IMultiTokenPermit.ERC721Transfer[](1); transfers[0] = IMultiTokenPermit.ERC721Transfer({ - from: nftOwner, - to: recipientAddress, - tokenId: tokenIds[0], - token: address(nftToken) + from: nftOwner, to: recipientAddress, tokenId: tokenIds[0], token: address(nftToken) }); // Attempt batch transfer should fail due to lockdown diff --git a/test/Permit3.t.sol b/test/Permit3.t.sol index 2b3287d..eaf8544 100644 --- a/test/Permit3.t.sol +++ b/test/Permit3.t.sol @@ -319,7 +319,7 @@ contract Permit3Test is TestBase { tokenKey: tokenKey, // Hash for NFT+tokenId account: spender, amountDelta: 1 // NFT amount - }); + }); IPermit3.ChainPermits memory chainPermits = IPermit3.ChainPermits({ chainId: uint64(block.chainid), permits: permits }); diff --git a/test/Permit3Edge.t.sol b/test/Permit3Edge.t.sol index 1ef78e1..d8dbd7f 100644 --- a/test/Permit3Edge.t.sol +++ b/test/Permit3Edge.t.sol @@ -379,7 +379,7 @@ contract Permit3EdgeTest is Test { tokenKey: bytes32(uint256(uint160(address(token)))), account: spender, amountDelta: 0 // Zero amount delta - }); + }); inputs.chainPermits = IPermit3.ChainPermits({ chainId: uint64(block.chainid), permits: inputs.permits }); @@ -424,7 +424,7 @@ contract Permit3EdgeTest is Test { tokenKey: bytes32(uint256(uint160(address(token)))), account: spender, amountDelta: 1000 // Additional amount (should be ignored) - }); + }); inputs.chainPermits = IPermit3.ChainPermits({ chainId: uint64(block.chainid), permits: inputs.permits }); @@ -498,7 +498,7 @@ contract Permit3EdgeTest is Test { tokenKey: bytes32(uint256(uint160(address(token)))), account: spender, amountDelta: 5000 // Higher amount - }); + }); olderInputs.chainPermits = IPermit3.ChainPermits({ chainId: uint64(block.chainid), permits: olderInputs.permits }); @@ -510,7 +510,7 @@ contract Permit3EdgeTest is Test { tokenKey: bytes32(uint256(uint160(address(token)))), account: spender, amountDelta: 3000 // Lower amount - }); + }); newerInputs.chainPermits = IPermit3.ChainPermits({ chainId: uint64(block.chainid), permits: newerInputs.permits }); @@ -624,7 +624,7 @@ contract Permit3EdgeTest is Test { tokenKey: bytes32(uint256(uint160(address(token)))), account: spender, amountDelta: 0 // Not used for lock - }); + }); lockInputs.chainPermits = IPermit3.ChainPermits({ chainId: uint64(block.chainid), permits: lockInputs.permits }); @@ -673,7 +673,7 @@ contract Permit3EdgeTest is Test { tokenKey: bytes32(uint256(uint160(address(token)))), account: spender, amountDelta: 100 // Value to decrease by - }); + }); decreaseInputs.chainPermits = IPermit3.ChainPermits({ chainId: uint64(block.chainid), permits: decreaseInputs.permits }); @@ -725,7 +725,7 @@ contract Permit3EdgeTest is Test { tokenKey: bytes32(uint256(uint160(address(token)))), account: spender, amountDelta: 0 // Not used for lock - }); + }); lockInputs.chainPermits = IPermit3.ChainPermits({ chainId: uint64(block.chainid), permits: lockInputs.permits }); @@ -775,7 +775,7 @@ contract Permit3EdgeTest is Test { tokenKey: bytes32(uint256(uint160(address(token)))), account: spender, amountDelta: 3000 // New amount after unlock - }); + }); unlockInputs.chainPermits = IPermit3.ChainPermits({ chainId: uint64(block.chainid), permits: unlockInputs.permits }); @@ -833,7 +833,7 @@ contract Permit3EdgeTest is Test { tokenKey: bytes32(uint256(uint160(address(token)))), account: spender, amountDelta: 0 // Not used for lock - }); + }); lockInputs.chainPermits = IPermit3.ChainPermits({ chainId: uint64(block.chainid), permits: lockInputs.permits }); @@ -877,7 +877,7 @@ contract Permit3EdgeTest is Test { tokenKey: bytes32(uint256(uint160(address(token)))), account: spender, amountDelta: 3000 // New amount after unlock - }); + }); unlockInputs.chainPermits = IPermit3.ChainPermits({ chainId: uint64(block.chainid), permits: unlockInputs.permits }); @@ -944,7 +944,7 @@ contract Permit3EdgeTest is Test { tokenKey: bytes32(uint256(uint160(address(token)))), account: spender, amountDelta: type(uint160).max // Try to decrease by MAX_ALLOWANCE - }); + }); inputs.chainPermits = IPermit3.ChainPermits({ chainId: uint64(block.chainid), permits: inputs.permits }); @@ -988,7 +988,7 @@ contract Permit3EdgeTest is Test { tokenKey: bytes32(uint256(uint160(address(token)))), account: spender, amountDelta: type(uint160).max // Decrease by MAX_ALLOWANCE - }); + }); inputs.chainPermits = IPermit3.ChainPermits({ chainId: uint64(block.chainid), permits: inputs.permits }); @@ -1035,7 +1035,7 @@ contract Permit3EdgeTest is Test { tokenKey: bytes32(uint256(uint160(address(token)))), account: spender, amountDelta: type(uint160).max // Set to MAX_ALLOWANCE - }); + }); inputs.chainPermits = IPermit3.ChainPermits({ chainId: uint64(block.chainid), permits: inputs.permits }); @@ -1079,7 +1079,7 @@ contract Permit3EdgeTest is Test { tokenKey: bytes32(uint256(uint160(address(token)))), account: spender, amountDelta: 500 // Decrease by 500 (from 1000) - }); + }); inputs.chainPermits = IPermit3.ChainPermits({ chainId: uint64(block.chainid), permits: inputs.permits }); @@ -1137,7 +1137,7 @@ contract Permit3EdgeTest is Test { tokenKey: bytes32(uint256(uint160(address(token)))), account: recipient, amountDelta: 100 // Transfer 100 - }); + }); // 2. Decrease inputs.permits[1] = IPermit3.AllowanceOrTransfer({ @@ -1145,7 +1145,7 @@ contract Permit3EdgeTest is Test { tokenKey: bytes32(uint256(uint160(address(token)))), account: spender, amountDelta: 50 // Decrease by 50 - }); + }); // 3. Increase allowance with expiration inputs.permits[2] = IPermit3.AllowanceOrTransfer({ @@ -1153,7 +1153,7 @@ contract Permit3EdgeTest is Test { tokenKey: bytes32(uint256(uint160(address(token)))), account: spender, amountDelta: 200 // Increase by 200 - }); + }); inputs.chainPermits = IPermit3.ChainPermits({ chainId: uint64(block.chainid), permits: inputs.permits }); @@ -1341,7 +1341,7 @@ contract Permit3EdgeTest is Test { tokenKey: bytes32(uint256(uint160(address(token)))), account: spender, amountDelta: 0 // Zero delta - }); + }); inputs.chainPermits = IPermit3.ChainPermits({ chainId: uint64(block.chainid), permits: inputs.permits }); diff --git a/test/TreeNodeLib.t.sol b/test/TreeNodeLib.t.sol new file mode 100644 index 0000000..99d85fa --- /dev/null +++ b/test/TreeNodeLib.t.sol @@ -0,0 +1,1313 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../src/lib/TreeNodeLib.sol"; +import "./utils/TreeNodeLibTester.sol"; +import "forge-std/Test.sol"; + +/** + * @title TreeNodeLibTest + * @notice Comprehensive test suite for the generic TreeNodeLib library + * @dev Tests the library with both PERMIT_NODE_TYPEHASH and NONCE_NODE_TYPEHASH + * to ensure generalization works correctly + * + * TEST PHILOSOPHY: + * Unlike PermitNodeLib.t.sol and NonceNodeLib.t.sol which test specialized wrappers, + * this file tests the GENERIC TreeNodeLib directly. The key insight is that the + * reconstruction algorithm should work identically for any EIP-712 typehash - the + * typehash is just a parameter that changes the output but not the algorithm behavior. + * + * This test suite verifies: + * 1. TreeNodeLib works with ANY typehash (tested with two different ones) + * 2. Sorting behavior is consistent across typehashes + * 3. Different typehashes produce different outputs (as expected) + * 4. Algorithm behavior is identical regardless of typehash chosen + */ +contract TreeNodeLibTest is Test { + TreeNodeLibTester public tester; + + // Test typehashes - should produce different hashes but same algorithm behavior + bytes32 constant PERMIT_NODE_TYPEHASH = keccak256( + "PermitNode(PermitNode[] nodes,ChainPermits[] permits)AllowanceOrTransfer(uint48 modeOrExpiration,bytes32 tokenKey,address account,uint160 amountDelta)ChainPermits(uint64 chainId,AllowanceOrTransfer[] permits)" + ); + + bytes32 constant NONCE_NODE_TYPEHASH = keccak256("NonceNode(NonceNode[] nodes,bytes32[] nonces)"); + + function setUp() public { + tester = new TreeNodeLibTester(); + } + + // ============================================ + // Category 1: Constants Verification (1 test) + // ============================================ + + function test_typehashesAreDifferent() public pure { + assertTrue(PERMIT_NODE_TYPEHASH != NONCE_NODE_TYPEHASH, "Typehashes should be different"); + } + + // ============================================ + // Category 2: combineLeafAndLeaf Tests (12 tests) + // ============================================ + + /** + * Test basic combination with PERMIT_NODE_TYPEHASH + */ + function test_combineLeafAndLeaf_permitTypehash_basic() public view { + bytes32 leaf1 = bytes32(uint256(0x1111)); + bytes32 leaf2 = bytes32(uint256(0x2222)); + + bytes32 result = tester.combineLeafAndLeaf(PERMIT_NODE_TYPEHASH, leaf1, leaf2); + + // Result should be Node(nodes=[], leaves=[leaf1, leaf2]) with PERMIT_NODE_TYPEHASH + bytes32 expectedLeavesHash = keccak256(abi.encodePacked(leaf1, leaf2)); + bytes32 expected = keccak256(abi.encode(PERMIT_NODE_TYPEHASH, keccak256(""), expectedLeavesHash)); + + assertEq(result, expected, "Should create Node with PERMIT_NODE_TYPEHASH"); + } + + /** + * Test basic combination with NONCE_NODE_TYPEHASH + */ + function test_combineLeafAndLeaf_nonceTypehash_basic() public view { + bytes32 leaf1 = bytes32(uint256(0x1111)); + bytes32 leaf2 = bytes32(uint256(0x2222)); + + bytes32 result = tester.combineLeafAndLeaf(NONCE_NODE_TYPEHASH, leaf1, leaf2); + + // Result should be Node(nodes=[], leaves=[leaf1, leaf2]) with NONCE_NODE_TYPEHASH + bytes32 expectedLeavesHash = keccak256(abi.encodePacked(leaf1, leaf2)); + bytes32 expected = keccak256(abi.encode(NONCE_NODE_TYPEHASH, keccak256(""), expectedLeavesHash)); + + assertEq(result, expected, "Should create Node with NONCE_NODE_TYPEHASH"); + } + + /** + * Test sorting behavior with PERMIT_NODE_TYPEHASH + */ + function test_combineLeafAndLeaf_permitTypehash_sorting() public view { + bytes32 leaf1 = bytes32(uint256(0x2222)); + bytes32 leaf2 = bytes32(uint256(0x1111)); + + bytes32 result = tester.combineLeafAndLeaf(PERMIT_NODE_TYPEHASH, leaf1, leaf2); + + // Should sort: smaller first (leaf2, leaf1) + bytes32 expectedLeavesHash = keccak256(abi.encodePacked(leaf2, leaf1)); + bytes32 expected = keccak256(abi.encode(PERMIT_NODE_TYPEHASH, keccak256(""), expectedLeavesHash)); + + assertEq(result, expected, "Should sort leaves alphabetically with PERMIT_NODE_TYPEHASH"); + } + + /** + * Test sorting behavior with NONCE_NODE_TYPEHASH + */ + function test_combineLeafAndLeaf_nonceTypehash_sorting() public view { + bytes32 leaf1 = bytes32(uint256(0x2222)); + bytes32 leaf2 = bytes32(uint256(0x1111)); + + bytes32 result = tester.combineLeafAndLeaf(NONCE_NODE_TYPEHASH, leaf1, leaf2); + + // Should sort: smaller first (leaf2, leaf1) + bytes32 expectedLeavesHash = keccak256(abi.encodePacked(leaf2, leaf1)); + bytes32 expected = keccak256(abi.encode(NONCE_NODE_TYPEHASH, keccak256(""), expectedLeavesHash)); + + assertEq(result, expected, "Should sort leaves alphabetically with NONCE_NODE_TYPEHASH"); + } + + /** + * Test order invariance (commutativity) with PERMIT_NODE_TYPEHASH + */ + function test_combineLeafAndLeaf_permitTypehash_orderInvariant() public view { + bytes32 leaf1 = bytes32(uint256(0x1111)); + bytes32 leaf2 = bytes32(uint256(0x2222)); + + bytes32 result1 = tester.combineLeafAndLeaf(PERMIT_NODE_TYPEHASH, leaf1, leaf2); + bytes32 result2 = tester.combineLeafAndLeaf(PERMIT_NODE_TYPEHASH, leaf2, leaf1); + + assertEq(result1, result2, "Should be order-invariant with PERMIT_NODE_TYPEHASH"); + } + + /** + * Test order invariance (commutativity) with NONCE_NODE_TYPEHASH + */ + function test_combineLeafAndLeaf_nonceTypehash_orderInvariant() public view { + bytes32 leaf1 = bytes32(uint256(0x1111)); + bytes32 leaf2 = bytes32(uint256(0x2222)); + + bytes32 result1 = tester.combineLeafAndLeaf(NONCE_NODE_TYPEHASH, leaf1, leaf2); + bytes32 result2 = tester.combineLeafAndLeaf(NONCE_NODE_TYPEHASH, leaf2, leaf1); + + assertEq(result1, result2, "Should be order-invariant with NONCE_NODE_TYPEHASH"); + } + + /** + * Test with identical leaves + */ + function test_combineLeafAndLeaf_identicalLeaves() public view { + bytes32 leaf = bytes32(uint256(0x1111)); + + bytes32 permitResult = tester.combineLeafAndLeaf(PERMIT_NODE_TYPEHASH, leaf, leaf); + bytes32 nonceResult = tester.combineLeafAndLeaf(NONCE_NODE_TYPEHASH, leaf, leaf); + + // Should produce valid results (different for different typehashes) + assertTrue(permitResult != bytes32(0), "PERMIT result should be non-zero"); + assertTrue(nonceResult != bytes32(0), "NONCE result should be non-zero"); + assertTrue(permitResult != nonceResult, "Different typehashes should produce different results"); + } + + /** + * Test with zero hashes + */ + function test_combineLeafAndLeaf_zeroHashes() public view { + bytes32 zeroHash = bytes32(0); + + bytes32 permitResult = tester.combineLeafAndLeaf(PERMIT_NODE_TYPEHASH, zeroHash, zeroHash); + bytes32 nonceResult = tester.combineLeafAndLeaf(NONCE_NODE_TYPEHASH, zeroHash, zeroHash); + + // Should produce valid non-zero results + assertTrue(permitResult != bytes32(0), "PERMIT result should be non-zero even with zero inputs"); + assertTrue(nonceResult != bytes32(0), "NONCE result should be non-zero even with zero inputs"); + } + + /** + * Test with maximum uint256 hashes + */ + function test_combineLeafAndLeaf_maxValues() public view { + bytes32 maxHash = bytes32(type(uint256).max); + + bytes32 permitResult = tester.combineLeafAndLeaf(PERMIT_NODE_TYPEHASH, maxHash, maxHash); + bytes32 nonceResult = tester.combineLeafAndLeaf(NONCE_NODE_TYPEHASH, maxHash, maxHash); + + // Should handle max values without reverting + assertTrue(permitResult != bytes32(0), "PERMIT result should handle max values"); + assertTrue(nonceResult != bytes32(0), "NONCE result should handle max values"); + } + + /** + * CRITICAL TEST: Different typehashes produce different outputs for same leaves + */ + function test_combineLeafAndLeaf_sameLeavesDifferentTypehashesProduceDifferentResults() public view { + bytes32 leaf1 = keccak256("leaf1"); + bytes32 leaf2 = keccak256("leaf2"); + + // Combine with PermitNode typehash + bytes32 permitRoot = tester.combineLeafAndLeaf(PERMIT_NODE_TYPEHASH, leaf1, leaf2); + + // Combine with NonceNode typehash + bytes32 nonceRoot = tester.combineLeafAndLeaf(NONCE_NODE_TYPEHASH, leaf1, leaf2); + + // Roots should be DIFFERENT (different typehashes) + assertTrue(permitRoot != nonceRoot, "Different typehashes should produce different roots"); + } + + /** + * CRITICAL TEST: Sorting behavior is consistent across typehashes + */ + function test_combineLeafAndLeaf_sortingIsConsistentAcrossTypehashes() public view { + bytes32 leaf1 = keccak256("aaa"); + bytes32 leaf2 = keccak256("zzz"); + + // Both typehashes should sort the same way (commutative) + bytes32 permitResult1 = tester.combineLeafAndLeaf(PERMIT_NODE_TYPEHASH, leaf1, leaf2); + bytes32 permitResult2 = tester.combineLeafAndLeaf(PERMIT_NODE_TYPEHASH, leaf2, leaf1); + + bytes32 nonceResult1 = tester.combineLeafAndLeaf(NONCE_NODE_TYPEHASH, leaf1, leaf2); + bytes32 nonceResult2 = tester.combineLeafAndLeaf(NONCE_NODE_TYPEHASH, leaf2, leaf1); + + // Results should be commutative for EACH typehash + assertEq(permitResult1, permitResult2, "Permit leaf sorting failed"); + assertEq(nonceResult1, nonceResult2, "Nonce leaf sorting failed"); + } + + // ============================================ + // Category 3: combineNodeAndNode Tests (12 tests) + // ============================================ + + /** + * Test basic combination with PERMIT_NODE_TYPEHASH + */ + function test_combineNodeAndNode_permitTypehash_basic() public view { + bytes32 node1 = bytes32(uint256(0x1111)); + bytes32 node2 = bytes32(uint256(0x2222)); + + bytes32 result = tester.combineNodeAndNode(PERMIT_NODE_TYPEHASH, node1, node2); + + // Result should be Node(nodes=[node1, node2], leaves=[]) + bytes32 expectedNodesHash = keccak256(abi.encodePacked(node1, node2)); + bytes32 expected = keccak256(abi.encode(PERMIT_NODE_TYPEHASH, expectedNodesHash, keccak256(""))); + + assertEq(result, expected, "Should create Node with nodes array using PERMIT_NODE_TYPEHASH"); + } + + /** + * Test basic combination with NONCE_NODE_TYPEHASH + */ + function test_combineNodeAndNode_nonceTypehash_basic() public view { + bytes32 node1 = bytes32(uint256(0x1111)); + bytes32 node2 = bytes32(uint256(0x2222)); + + bytes32 result = tester.combineNodeAndNode(NONCE_NODE_TYPEHASH, node1, node2); + + // Result should be Node(nodes=[node1, node2], leaves=[]) + bytes32 expectedNodesHash = keccak256(abi.encodePacked(node1, node2)); + bytes32 expected = keccak256(abi.encode(NONCE_NODE_TYPEHASH, expectedNodesHash, keccak256(""))); + + assertEq(result, expected, "Should create Node with nodes array using NONCE_NODE_TYPEHASH"); + } + + /** + * Test sorting behavior with PERMIT_NODE_TYPEHASH + */ + function test_combineNodeAndNode_permitTypehash_sorting() public view { + bytes32 node1 = bytes32(uint256(0x2222)); + bytes32 node2 = bytes32(uint256(0x1111)); + + bytes32 result = tester.combineNodeAndNode(PERMIT_NODE_TYPEHASH, node1, node2); + + // Should sort: smaller first (node2, node1) + bytes32 expectedNodesHash = keccak256(abi.encodePacked(node2, node1)); + bytes32 expected = keccak256(abi.encode(PERMIT_NODE_TYPEHASH, expectedNodesHash, keccak256(""))); + + assertEq(result, expected, "Should sort nodes alphabetically with PERMIT_NODE_TYPEHASH"); + } + + /** + * Test sorting behavior with NONCE_NODE_TYPEHASH + */ + function test_combineNodeAndNode_nonceTypehash_sorting() public view { + bytes32 node1 = bytes32(uint256(0x2222)); + bytes32 node2 = bytes32(uint256(0x1111)); + + bytes32 result = tester.combineNodeAndNode(NONCE_NODE_TYPEHASH, node1, node2); + + // Should sort: smaller first (node2, node1) + bytes32 expectedNodesHash = keccak256(abi.encodePacked(node2, node1)); + bytes32 expected = keccak256(abi.encode(NONCE_NODE_TYPEHASH, expectedNodesHash, keccak256(""))); + + assertEq(result, expected, "Should sort nodes alphabetically with NONCE_NODE_TYPEHASH"); + } + + /** + * Test order invariance with PERMIT_NODE_TYPEHASH + */ + function test_combineNodeAndNode_permitTypehash_orderInvariant() public view { + bytes32 node1 = bytes32(uint256(0x1111)); + bytes32 node2 = bytes32(uint256(0x2222)); + + bytes32 result1 = tester.combineNodeAndNode(PERMIT_NODE_TYPEHASH, node1, node2); + bytes32 result2 = tester.combineNodeAndNode(PERMIT_NODE_TYPEHASH, node2, node1); + + assertEq(result1, result2, "Should be order-invariant with PERMIT_NODE_TYPEHASH"); + } + + /** + * Test order invariance with NONCE_NODE_TYPEHASH + */ + function test_combineNodeAndNode_nonceTypehash_orderInvariant() public view { + bytes32 node1 = bytes32(uint256(0x1111)); + bytes32 node2 = bytes32(uint256(0x2222)); + + bytes32 result1 = tester.combineNodeAndNode(NONCE_NODE_TYPEHASH, node1, node2); + bytes32 result2 = tester.combineNodeAndNode(NONCE_NODE_TYPEHASH, node2, node1); + + assertEq(result1, result2, "Should be order-invariant with NONCE_NODE_TYPEHASH"); + } + + /** + * Test with identical nodes + */ + function test_combineNodeAndNode_identicalNodes() public view { + bytes32 node = bytes32(uint256(0x1111)); + + bytes32 permitResult = tester.combineNodeAndNode(PERMIT_NODE_TYPEHASH, node, node); + bytes32 nonceResult = tester.combineNodeAndNode(NONCE_NODE_TYPEHASH, node, node); + + assertTrue(permitResult != bytes32(0), "PERMIT result should be non-zero"); + assertTrue(nonceResult != bytes32(0), "NONCE result should be non-zero"); + assertTrue(permitResult != nonceResult, "Different typehashes should produce different results"); + } + + /** + * Test with zero hashes + */ + function test_combineNodeAndNode_zeroHashes() public view { + bytes32 zeroHash = bytes32(0); + + bytes32 permitResult = tester.combineNodeAndNode(PERMIT_NODE_TYPEHASH, zeroHash, zeroHash); + bytes32 nonceResult = tester.combineNodeAndNode(NONCE_NODE_TYPEHASH, zeroHash, zeroHash); + + assertTrue(permitResult != bytes32(0), "PERMIT result should be non-zero"); + assertTrue(nonceResult != bytes32(0), "NONCE result should be non-zero"); + } + + /** + * Test different typehashes produce different outputs + */ + function test_combineNodeAndNode_differentTypehashesProduceDifferentResults() public view { + bytes32 node1 = keccak256("node1"); + bytes32 node2 = keccak256("node2"); + + bytes32 permitResult = tester.combineNodeAndNode(PERMIT_NODE_TYPEHASH, node1, node2); + bytes32 nonceResult = tester.combineNodeAndNode(NONCE_NODE_TYPEHASH, node1, node2); + + assertTrue(permitResult != nonceResult, "Different typehashes should produce different results"); + } + + /** + * Test deterministic results + */ + function test_combineNodeAndNode_deterministic() public view { + bytes32 node1 = bytes32(uint256(0x1111)); + bytes32 node2 = bytes32(uint256(0x2222)); + + bytes32 permitResult1 = tester.combineNodeAndNode(PERMIT_NODE_TYPEHASH, node1, node2); + bytes32 permitResult2 = tester.combineNodeAndNode(PERMIT_NODE_TYPEHASH, node1, node2); + + bytes32 nonceResult1 = tester.combineNodeAndNode(NONCE_NODE_TYPEHASH, node1, node2); + bytes32 nonceResult2 = tester.combineNodeAndNode(NONCE_NODE_TYPEHASH, node1, node2); + + assertEq(permitResult1, permitResult2, "PERMIT should be deterministic"); + assertEq(nonceResult1, nonceResult2, "NONCE should be deterministic"); + } + + /** + * Test all combination types produce different results + */ + function test_combineNodeAndNode_differentFromLeafCombination() public view { + bytes32 hash1 = bytes32(uint256(0x1111)); + bytes32 hash2 = bytes32(uint256(0x2222)); + + bytes32 nodeNodeResult = tester.combineNodeAndNode(PERMIT_NODE_TYPEHASH, hash1, hash2); + bytes32 leafLeafResult = tester.combineLeafAndLeaf(PERMIT_NODE_TYPEHASH, hash1, hash2); + + assertTrue(nodeNodeResult != leafLeafResult, "Node+Node should differ from Leaf+Leaf"); + } + + // ============================================ + // Category 4: combineNodeAndLeaf Tests (12 tests) + // ============================================ + + /** + * Test basic mixed combination with PERMIT_NODE_TYPEHASH + */ + function test_combineNodeAndLeaf_permitTypehash_basic() public view { + bytes32 nodeHash = bytes32(uint256(0x1111)); + bytes32 leafHash = bytes32(uint256(0x2222)); + + bytes32 result = tester.combineNodeAndLeaf(PERMIT_NODE_TYPEHASH, nodeHash, leafHash); + + // Result should be Node(nodes=[nodeHash], leaves=[leafHash]) + bytes32 expectedNodesHash = keccak256(abi.encodePacked(nodeHash)); + bytes32 expectedLeavesHash = keccak256(abi.encodePacked(leafHash)); + bytes32 expected = keccak256(abi.encode(PERMIT_NODE_TYPEHASH, expectedNodesHash, expectedLeavesHash)); + + assertEq(result, expected, "Should create mixed Node with PERMIT_NODE_TYPEHASH"); + } + + /** + * Test basic mixed combination with NONCE_NODE_TYPEHASH + */ + function test_combineNodeAndLeaf_nonceTypehash_basic() public view { + bytes32 nodeHash = bytes32(uint256(0x1111)); + bytes32 leafHash = bytes32(uint256(0x2222)); + + bytes32 result = tester.combineNodeAndLeaf(NONCE_NODE_TYPEHASH, nodeHash, leafHash); + + // Result should be Node(nodes=[nodeHash], leaves=[leafHash]) + bytes32 expectedNodesHash = keccak256(abi.encodePacked(nodeHash)); + bytes32 expectedLeavesHash = keccak256(abi.encodePacked(leafHash)); + bytes32 expected = keccak256(abi.encode(NONCE_NODE_TYPEHASH, expectedNodesHash, expectedLeavesHash)); + + assertEq(result, expected, "Should create mixed Node with NONCE_NODE_TYPEHASH"); + } + + /** + * CRITICAL TEST: NO SORTING - order matters + */ + function test_combineNodeAndLeaf_permitTypehash_noSorting() public view { + bytes32 nodeHash = bytes32(uint256(0x2222)); // Larger value + bytes32 leafHash = bytes32(uint256(0x1111)); // Smaller value + + bytes32 result = tester.combineNodeAndLeaf(PERMIT_NODE_TYPEHASH, nodeHash, leafHash); + + // Should maintain struct order: nodes first, leaves second (NO sorting) + bytes32 expectedNodesHash = keccak256(abi.encodePacked(nodeHash)); + bytes32 expectedLeavesHash = keccak256(abi.encodePacked(leafHash)); + bytes32 expected = keccak256(abi.encode(PERMIT_NODE_TYPEHASH, expectedNodesHash, expectedLeavesHash)); + + assertEq(result, expected, "Should maintain struct order (no sorting) with PERMIT_NODE_TYPEHASH"); + } + + /** + * CRITICAL TEST: NO SORTING - order matters with NONCE_NODE_TYPEHASH + */ + function test_combineNodeAndLeaf_nonceTypehash_noSorting() public view { + bytes32 nodeHash = bytes32(uint256(0x2222)); // Larger value + bytes32 leafHash = bytes32(uint256(0x1111)); // Smaller value + + bytes32 result = tester.combineNodeAndLeaf(NONCE_NODE_TYPEHASH, nodeHash, leafHash); + + // Should maintain struct order: nodes first, leaves second (NO sorting) + bytes32 expectedNodesHash = keccak256(abi.encodePacked(nodeHash)); + bytes32 expectedLeavesHash = keccak256(abi.encodePacked(leafHash)); + bytes32 expected = keccak256(abi.encode(NONCE_NODE_TYPEHASH, expectedNodesHash, expectedLeavesHash)); + + assertEq(result, expected, "Should maintain struct order (no sorting) with NONCE_NODE_TYPEHASH"); + } + + /** + * CRITICAL TEST: Order matters - combine(node, leaf) != combine(leaf, node) + */ + function test_combineNodeAndLeaf_orderMatters_permit() public view { + bytes32 hash1 = bytes32(uint256(0x1111)); + bytes32 hash2 = bytes32(uint256(0x2222)); + + // First as node, second as leaf + bytes32 result1 = tester.combineNodeAndLeaf(PERMIT_NODE_TYPEHASH, hash1, hash2); + // Swapped: first as node, second as leaf (but different values) + bytes32 result2 = tester.combineNodeAndLeaf(PERMIT_NODE_TYPEHASH, hash2, hash1); + + assertTrue(result1 != result2, "Order should matter for Node+Leaf combination with PERMIT"); + } + + /** + * CRITICAL TEST: Order matters with NONCE_NODE_TYPEHASH + */ + function test_combineNodeAndLeaf_orderMatters_nonce() public view { + bytes32 hash1 = bytes32(uint256(0x1111)); + bytes32 hash2 = bytes32(uint256(0x2222)); + + bytes32 result1 = tester.combineNodeAndLeaf(NONCE_NODE_TYPEHASH, hash1, hash2); + bytes32 result2 = tester.combineNodeAndLeaf(NONCE_NODE_TYPEHASH, hash2, hash1); + + assertTrue(result1 != result2, "Order should matter for Node+Leaf combination with NONCE"); + } + + /** + * Test with zero hashes + */ + function test_combineNodeAndLeaf_zeroHashes() public view { + bytes32 zeroHash = bytes32(0); + + bytes32 permitResult = tester.combineNodeAndLeaf(PERMIT_NODE_TYPEHASH, zeroHash, zeroHash); + bytes32 nonceResult = tester.combineNodeAndLeaf(NONCE_NODE_TYPEHASH, zeroHash, zeroHash); + + assertTrue(permitResult != bytes32(0), "PERMIT result should be non-zero"); + assertTrue(nonceResult != bytes32(0), "NONCE result should be non-zero"); + } + + /** + * Test different typehashes produce different outputs + */ + function test_combineNodeAndLeaf_differentTypehashesProduceDifferentResults() public view { + bytes32 nodeHash = keccak256("node"); + bytes32 leafHash = keccak256("leaf"); + + bytes32 permitResult = tester.combineNodeAndLeaf(PERMIT_NODE_TYPEHASH, nodeHash, leafHash); + bytes32 nonceResult = tester.combineNodeAndLeaf(NONCE_NODE_TYPEHASH, nodeHash, leafHash); + + assertTrue(permitResult != nonceResult, "Different typehashes should produce different results"); + } + + /** + * Test deterministic results + */ + function test_combineNodeAndLeaf_deterministic() public view { + bytes32 nodeHash = bytes32(uint256(0x1111)); + bytes32 leafHash = bytes32(uint256(0x2222)); + + bytes32 permitResult1 = tester.combineNodeAndLeaf(PERMIT_NODE_TYPEHASH, nodeHash, leafHash); + bytes32 permitResult2 = tester.combineNodeAndLeaf(PERMIT_NODE_TYPEHASH, nodeHash, leafHash); + + bytes32 nonceResult1 = tester.combineNodeAndLeaf(NONCE_NODE_TYPEHASH, nodeHash, leafHash); + bytes32 nonceResult2 = tester.combineNodeAndLeaf(NONCE_NODE_TYPEHASH, nodeHash, leafHash); + + assertEq(permitResult1, permitResult2, "PERMIT should be deterministic"); + assertEq(nonceResult1, nonceResult2, "NONCE should be deterministic"); + } + + /** + * Test different from other combination types + */ + function test_combineNodeAndLeaf_differentFromOtherCombinations() public view { + bytes32 hash1 = bytes32(uint256(0x1111)); + bytes32 hash2 = bytes32(uint256(0x2222)); + + bytes32 nodeLeafResult = tester.combineNodeAndLeaf(PERMIT_NODE_TYPEHASH, hash1, hash2); + bytes32 nodeNodeResult = tester.combineNodeAndNode(PERMIT_NODE_TYPEHASH, hash1, hash2); + bytes32 leafLeafResult = tester.combineLeafAndLeaf(PERMIT_NODE_TYPEHASH, hash1, hash2); + + assertTrue(nodeLeafResult != nodeNodeResult, "Node+Leaf should differ from Node+Node"); + assertTrue(nodeLeafResult != leafLeafResult, "Node+Leaf should differ from Leaf+Leaf"); + } + + /** + * Test with identical hashes + */ + function test_combineNodeAndLeaf_identicalHashes() public view { + bytes32 hash = bytes32(uint256(0x1111)); + + bytes32 permitResult = tester.combineNodeAndLeaf(PERMIT_NODE_TYPEHASH, hash, hash); + bytes32 nonceResult = tester.combineNodeAndLeaf(NONCE_NODE_TYPEHASH, hash, hash); + + assertTrue(permitResult != bytes32(0), "PERMIT result should be non-zero"); + assertTrue(nonceResult != bytes32(0), "NONCE result should be non-zero"); + assertTrue(permitResult != nonceResult, "Different typehashes should produce different results"); + } + + // ============================================ + // Category 5: computeTreeHash Tests (24 tests) + // ============================================ + + /** + * Test reconstruction with single leaf (no proof) - PERMIT + */ + function test_reconstructTreeHash_permitTypehash_singleLeaf() public view { + bytes32 leaf = keccak256("leaf"); + bytes32[] memory proof = new bytes32[](0); + bytes32 proofStructure = bytes32(0); + + bytes32 result = tester.computeTreeHash(PERMIT_NODE_TYPEHASH, proofStructure, proof, leaf); + + // With empty proof, should return leaf unchanged + assertEq(result, leaf, "Single leaf should return unchanged with PERMIT_NODE_TYPEHASH"); + } + + /** + * Test reconstruction with single leaf (no proof) - NONCE + */ + function test_reconstructTreeHash_nonceTypehash_singleLeaf() public view { + bytes32 leaf = keccak256("leaf"); + bytes32[] memory proof = new bytes32[](0); + bytes32 proofStructure = bytes32(0); + + bytes32 result = tester.computeTreeHash(NONCE_NODE_TYPEHASH, proofStructure, proof, leaf); + + // With empty proof, should return leaf unchanged + assertEq(result, leaf, "Single leaf should return unchanged with NONCE_NODE_TYPEHASH"); + } + + /** + * Test reconstruction with two leaves - PERMIT + */ + function test_reconstructTreeHash_permitTypehash_twoLeaves() public view { + bytes32 leaf1 = keccak256("leaf1"); + bytes32 leaf2 = keccak256("leaf2"); + + bytes32[] memory proof = new bytes32[](1); + proof[0] = leaf2; + + // Type flag = 0 (proof[0] is leaf) + bytes32 proofStructure = bytes32(0); + + bytes32 result = tester.computeTreeHash(PERMIT_NODE_TYPEHASH, proofStructure, proof, leaf1); + + // Should combine as Leaf+Leaf + bytes32 expected = tester.combineLeafAndLeaf(PERMIT_NODE_TYPEHASH, leaf1, leaf2); + + assertEq(result, expected, "Two leaves should combine correctly with PERMIT_NODE_TYPEHASH"); + } + + /** + * Test reconstruction with two leaves - NONCE + */ + function test_reconstructTreeHash_nonceTypehash_twoLeaves() public view { + bytes32 leaf1 = keccak256("leaf1"); + bytes32 leaf2 = keccak256("leaf2"); + + bytes32[] memory proof = new bytes32[](1); + proof[0] = leaf2; + + bytes32 proofStructure = bytes32(0); + + bytes32 result = tester.computeTreeHash(NONCE_NODE_TYPEHASH, proofStructure, proof, leaf1); + + bytes32 expected = tester.combineLeafAndLeaf(NONCE_NODE_TYPEHASH, leaf1, leaf2); + + assertEq(result, expected, "Two leaves should combine correctly with NONCE_NODE_TYPEHASH"); + } + + /** + * Test reconstruction with three leaves - PERMIT + */ + function test_reconstructTreeHash_permitTypehash_threeLeaves() public view { + bytes32 leaf1 = keccak256("leaf1"); + bytes32 leaf2 = keccak256("leaf2"); + bytes32 leaf3 = keccak256("leaf3"); + + bytes32[] memory proof = new bytes32[](2); + proof[0] = leaf2; + proof[1] = leaf3; + + // All type flags = 0 (all leaves) + bytes32 proofStructure = bytes32(0); + + bytes32 result = tester.computeTreeHash(PERMIT_NODE_TYPEHASH, proofStructure, proof, leaf1); + + // Step 1: combine leaf1 + leaf2 → node12 + bytes32 node12 = tester.combineLeafAndLeaf(PERMIT_NODE_TYPEHASH, leaf1, leaf2); + // Step 2: combine node12 + leaf3 → final (Node+Leaf) + bytes32 expected = tester.combineNodeAndLeaf(PERMIT_NODE_TYPEHASH, node12, leaf3); + + assertEq(result, expected, "Three leaves should combine correctly with PERMIT_NODE_TYPEHASH"); + } + + /** + * Test reconstruction with three leaves - NONCE + */ + function test_reconstructTreeHash_nonceTypehash_threeLeaves() public view { + bytes32 leaf1 = keccak256("leaf1"); + bytes32 leaf2 = keccak256("leaf2"); + bytes32 leaf3 = keccak256("leaf3"); + + bytes32[] memory proof = new bytes32[](2); + proof[0] = leaf2; + proof[1] = leaf3; + + bytes32 proofStructure = bytes32(0); + + bytes32 result = tester.computeTreeHash(NONCE_NODE_TYPEHASH, proofStructure, proof, leaf1); + + bytes32 node12 = tester.combineLeafAndLeaf(NONCE_NODE_TYPEHASH, leaf1, leaf2); + bytes32 expected = tester.combineNodeAndLeaf(NONCE_NODE_TYPEHASH, node12, leaf3); + + assertEq(result, expected, "Three leaves should combine correctly with NONCE_NODE_TYPEHASH"); + } + + /** + * Test reconstruction with four leaves (balanced) - PERMIT + */ + function test_reconstructTreeHash_permitTypehash_fourLeaves() public view { + bytes32 leaf1 = keccak256("leaf1"); + bytes32 leaf2 = keccak256("leaf2"); + bytes32 leaf3 = keccak256("leaf3"); + bytes32 leaf4 = keccak256("leaf4"); + + // Pre-combine leaf3 + leaf4 + bytes32 node34 = tester.combineLeafAndLeaf(PERMIT_NODE_TYPEHASH, leaf3, leaf4); + + bytes32[] memory proof = new bytes32[](2); + proof[0] = leaf2; + proof[1] = node34; + + // Type flags: bit 8 = 0 (leaf2), bit 9 = 1 (node34) + bytes32 proofStructure = bytes32(uint256(1) << (255 - 9)); + + bytes32 result = tester.computeTreeHash(PERMIT_NODE_TYPEHASH, proofStructure, proof, leaf1); + + // Step 1: leaf1 + leaf2 → node12 + bytes32 node12 = tester.combineLeafAndLeaf(PERMIT_NODE_TYPEHASH, leaf1, leaf2); + // Step 2: node12 + node34 → final (Node+Node) + bytes32 expected = tester.combineNodeAndNode(PERMIT_NODE_TYPEHASH, node12, node34); + + assertEq(result, expected, "Four leaves balanced tree with PERMIT_NODE_TYPEHASH"); + } + + /** + * Test reconstruction with four leaves (balanced) - NONCE + */ + function test_reconstructTreeHash_nonceTypehash_fourLeaves() public view { + bytes32 leaf1 = keccak256("leaf1"); + bytes32 leaf2 = keccak256("leaf2"); + bytes32 leaf3 = keccak256("leaf3"); + bytes32 leaf4 = keccak256("leaf4"); + + bytes32 node34 = tester.combineLeafAndLeaf(NONCE_NODE_TYPEHASH, leaf3, leaf4); + + bytes32[] memory proof = new bytes32[](2); + proof[0] = leaf2; + proof[1] = node34; + + bytes32 proofStructure = bytes32(uint256(1) << (255 - 9)); + + bytes32 result = tester.computeTreeHash(NONCE_NODE_TYPEHASH, proofStructure, proof, leaf1); + + bytes32 node12 = tester.combineLeafAndLeaf(NONCE_NODE_TYPEHASH, leaf1, leaf2); + bytes32 expected = tester.combineNodeAndNode(NONCE_NODE_TYPEHASH, node12, node34); + + assertEq(result, expected, "Four leaves balanced tree with NONCE_NODE_TYPEHASH"); + } + + /** + * Test reconstruction with five leaves (unbalanced) - PERMIT + */ + function test_reconstructTreeHash_permitTypehash_fiveLeaves() public view { + bytes32 leaf1 = keccak256("leaf1"); + bytes32 leaf2 = keccak256("leaf2"); + bytes32 leaf3 = keccak256("leaf3"); + bytes32 leaf4 = keccak256("leaf4"); + bytes32 leaf5 = keccak256("leaf5"); + + // Pre-combine: node34 = leaf3 + leaf4 + bytes32 node34 = tester.combineLeafAndLeaf(PERMIT_NODE_TYPEHASH, leaf3, leaf4); + + bytes32[] memory proof = new bytes32[](3); + proof[0] = leaf2; + proof[1] = node34; + proof[2] = leaf5; + + // Type flags: bit 8 = 0 (leaf2), bit 9 = 1 (node34), bit 10 = 0 (leaf5) + bytes32 proofStructure = bytes32(uint256(1) << (255 - 9)); + + bytes32 result = tester.computeTreeHash(PERMIT_NODE_TYPEHASH, proofStructure, proof, leaf1); + + // Step 1: leaf1 + leaf2 → node12 + bytes32 node12 = tester.combineLeafAndLeaf(PERMIT_NODE_TYPEHASH, leaf1, leaf2); + // Step 2: node12 + node34 → node1234 + bytes32 node1234 = tester.combineNodeAndNode(PERMIT_NODE_TYPEHASH, node12, node34); + // Step 3: node1234 + leaf5 → final + bytes32 expected = tester.combineNodeAndLeaf(PERMIT_NODE_TYPEHASH, node1234, leaf5); + + assertEq(result, expected, "Five leaves unbalanced tree with PERMIT_NODE_TYPEHASH"); + } + + /** + * Test reconstruction with five leaves (unbalanced) - NONCE + */ + function test_reconstructTreeHash_nonceTypehash_fiveLeaves() public view { + bytes32 leaf1 = keccak256("leaf1"); + bytes32 leaf2 = keccak256("leaf2"); + bytes32 leaf3 = keccak256("leaf3"); + bytes32 leaf4 = keccak256("leaf4"); + bytes32 leaf5 = keccak256("leaf5"); + + bytes32 node34 = tester.combineLeafAndLeaf(NONCE_NODE_TYPEHASH, leaf3, leaf4); + + bytes32[] memory proof = new bytes32[](3); + proof[0] = leaf2; + proof[1] = node34; + proof[2] = leaf5; + + bytes32 proofStructure = bytes32(uint256(1) << (255 - 9)); + + bytes32 result = tester.computeTreeHash(NONCE_NODE_TYPEHASH, proofStructure, proof, leaf1); + + bytes32 node12 = tester.combineLeafAndLeaf(NONCE_NODE_TYPEHASH, leaf1, leaf2); + bytes32 node1234 = tester.combineNodeAndNode(NONCE_NODE_TYPEHASH, node12, node34); + bytes32 expected = tester.combineNodeAndLeaf(NONCE_NODE_TYPEHASH, node1234, leaf5); + + assertEq(result, expected, "Five leaves unbalanced tree with NONCE_NODE_TYPEHASH"); + } + + /** + * Test with mixed type flags - PERMIT + */ + function test_reconstructTreeHash_permitTypehash_mixedTypes() public view { + bytes32 leaf1 = keccak256("leaf1"); + bytes32 node2 = keccak256("node2"); + bytes32 leaf3 = keccak256("leaf3"); + + bytes32[] memory proof = new bytes32[](2); + proof[0] = node2; + proof[1] = leaf3; + + // Type flags: bit 8 = 1 (node2), bit 9 = 0 (leaf3) + bytes32 proofStructure = bytes32(uint256(1) << (255 - 8)); + + bytes32 result = tester.computeTreeHash(PERMIT_NODE_TYPEHASH, proofStructure, proof, leaf1); + + // Step 1: leaf1 (current) + node2 (proof[0]) → Node+Leaf combination + bytes32 step1 = tester.combineNodeAndLeaf(PERMIT_NODE_TYPEHASH, node2, leaf1); + // Step 2: step1 (now Node) + leaf3 (proof[1]) → Node+Leaf combination + bytes32 expected = tester.combineNodeAndLeaf(PERMIT_NODE_TYPEHASH, step1, leaf3); + + assertEq(result, expected, "Mixed type flags with PERMIT_NODE_TYPEHASH"); + } + + /** + * Test with mixed type flags - NONCE + */ + function test_reconstructTreeHash_nonceTypehash_mixedTypes() public view { + bytes32 leaf1 = keccak256("leaf1"); + bytes32 node2 = keccak256("node2"); + bytes32 leaf3 = keccak256("leaf3"); + + bytes32[] memory proof = new bytes32[](2); + proof[0] = node2; + proof[1] = leaf3; + + bytes32 proofStructure = bytes32(uint256(1) << (255 - 8)); + + bytes32 result = tester.computeTreeHash(NONCE_NODE_TYPEHASH, proofStructure, proof, leaf1); + + bytes32 step1 = tester.combineNodeAndLeaf(NONCE_NODE_TYPEHASH, node2, leaf1); + bytes32 expected = tester.combineNodeAndLeaf(NONCE_NODE_TYPEHASH, step1, leaf3); + + assertEq(result, expected, "Mixed type flags with NONCE_NODE_TYPEHASH"); + } + + /** + * Test different typehashes produce different roots for same tree structure + */ + function test_reconstructTreeHash_differentTypehashesProduceDifferentRoots() public view { + bytes32 leaf1 = keccak256("leaf1"); + bytes32 leaf2 = keccak256("leaf2"); + + bytes32[] memory proof = new bytes32[](1); + proof[0] = leaf2; + + bytes32 proofStructure = bytes32(0); + + bytes32 permitResult = tester.computeTreeHash(PERMIT_NODE_TYPEHASH, proofStructure, proof, leaf1); + bytes32 nonceResult = tester.computeTreeHash(NONCE_NODE_TYPEHASH, proofStructure, proof, leaf1); + + assertTrue(permitResult != nonceResult, "Different typehashes should produce different roots"); + } + + /** + * Test reconstruction is deterministic - PERMIT + */ + function test_reconstructTreeHash_permitTypehash_deterministic() public view { + bytes32 leaf = keccak256("leaf"); + bytes32[] memory proof = new bytes32[](2); + proof[0] = keccak256("proof0"); + proof[1] = keccak256("proof1"); + + bytes32 proofStructure = bytes32(0); + + bytes32 result1 = tester.computeTreeHash(PERMIT_NODE_TYPEHASH, proofStructure, proof, leaf); + bytes32 result2 = tester.computeTreeHash(PERMIT_NODE_TYPEHASH, proofStructure, proof, leaf); + + assertEq(result1, result2, "Reconstruction should be deterministic with PERMIT_NODE_TYPEHASH"); + } + + /** + * Test reconstruction is deterministic - NONCE + */ + function test_reconstructTreeHash_nonceTypehash_deterministic() public view { + bytes32 leaf = keccak256("leaf"); + bytes32[] memory proof = new bytes32[](2); + proof[0] = keccak256("proof0"); + proof[1] = keccak256("proof1"); + + bytes32 proofStructure = bytes32(0); + + bytes32 result1 = tester.computeTreeHash(NONCE_NODE_TYPEHASH, proofStructure, proof, leaf); + bytes32 result2 = tester.computeTreeHash(NONCE_NODE_TYPEHASH, proofStructure, proof, leaf); + + assertEq(result1, result2, "Reconstruction should be deterministic with NONCE_NODE_TYPEHASH"); + } + + /** + * Test with all type flags set to 0 (all leaves) + */ + function test_reconstructTreeHash_allLeaves() public view { + bytes32[] memory proof = new bytes32[](3); + proof[0] = keccak256("leaf2"); + proof[1] = keccak256("leaf3"); + proof[2] = keccak256("leaf4"); + + bytes32 proofStructure = bytes32(0); // All leaves + + bytes32 permitResult = tester.computeTreeHash(PERMIT_NODE_TYPEHASH, proofStructure, proof, keccak256("leaf1")); + bytes32 nonceResult = tester.computeTreeHash(NONCE_NODE_TYPEHASH, proofStructure, proof, keccak256("leaf1")); + + assertTrue(permitResult != bytes32(0), "PERMIT result should be non-zero"); + assertTrue(nonceResult != bytes32(0), "NONCE result should be non-zero"); + assertTrue(permitResult != nonceResult, "Different typehashes should produce different results"); + } + + /** + * Test with alternating type flags + */ + function test_reconstructTreeHash_alternatingTypes() public view { + bytes32[] memory proof = new bytes32[](4); + proof[0] = keccak256("proof0"); // Node (bit 8 = 1) + proof[1] = keccak256("proof1"); // Leaf (bit 9 = 0) + proof[2] = keccak256("proof2"); // Node (bit 10 = 1) + proof[3] = keccak256("proof3"); // Leaf (bit 11 = 0) + + // Type flags: alternating pattern + bytes32 proofStructure = bytes32( + (uint256(1) << (255 - 8)) // bit 8 = 1 + | (uint256(1) << (255 - 10)) // bit 10 = 1 + ); + + bytes32 permitResult = tester.computeTreeHash(PERMIT_NODE_TYPEHASH, proofStructure, proof, keccak256("leaf")); + bytes32 nonceResult = tester.computeTreeHash(NONCE_NODE_TYPEHASH, proofStructure, proof, keccak256("leaf")); + + assertTrue(permitResult != bytes32(0), "PERMIT result should be non-zero"); + assertTrue(nonceResult != bytes32(0), "NONCE result should be non-zero"); + } + + /** + * Test position byte doesn't affect reconstruction + */ + function test_reconstructTreeHash_positionByteIgnored() public view { + bytes32 leaf = keccak256("leaf"); + bytes32[] memory proof = new bytes32[](1); + proof[0] = keccak256("proof"); + + // Different positions, same type flags + bytes32 proofStructure1 = bytes32(uint256(0) << 248); + bytes32 proofStructure2 = bytes32(uint256(42) << 248); + + bytes32 permitResult1 = tester.computeTreeHash(PERMIT_NODE_TYPEHASH, proofStructure1, proof, leaf); + bytes32 permitResult2 = tester.computeTreeHash(PERMIT_NODE_TYPEHASH, proofStructure2, proof, leaf); + + assertEq(permitResult1, permitResult2, "Position byte should not affect reconstruction"); + } + + /** + * Test with long proof + */ + function test_reconstructTreeHash_longProof() public view { + bytes32 leaf = keccak256("leaf"); + + // Create proof with 10 elements + bytes32[] memory proof = new bytes32[](10); + for (uint256 i = 0; i < 10; i++) { + proof[i] = keccak256(abi.encodePacked("proof", i)); + } + + bytes32 proofStructure = bytes32(0); // All leaves + + bytes32 permitResult = tester.computeTreeHash(PERMIT_NODE_TYPEHASH, proofStructure, proof, leaf); + bytes32 nonceResult = tester.computeTreeHash(NONCE_NODE_TYPEHASH, proofStructure, proof, leaf); + + assertTrue(permitResult != bytes32(0), "PERMIT should handle long proof"); + assertTrue(nonceResult != bytes32(0), "NONCE should handle long proof"); + assertTrue(permitResult != nonceResult, "Different typehashes should produce different results"); + } + + /** + * Test with maximum allowed proof length (247) + */ + function test_reconstructTreeHash_maxProofLength() public view { + bytes32 leaf = keccak256("leaf"); + + // Create proof with 247 elements (maximum allowed) + bytes32[] memory proof = new bytes32[](247); + for (uint256 i = 0; i < 247; i++) { + proof[i] = keccak256(abi.encodePacked("proof", i)); + } + + bytes32 proofStructure = bytes32(0); // All leaves + + // Should not revert + bytes32 permitResult = tester.computeTreeHash(PERMIT_NODE_TYPEHASH, proofStructure, proof, leaf); + bytes32 nonceResult = tester.computeTreeHash(NONCE_NODE_TYPEHASH, proofStructure, proof, leaf); + + assertTrue(permitResult != bytes32(0), "PERMIT should handle max proof length"); + assertTrue(nonceResult != bytes32(0), "NONCE should handle max proof length"); + } + + /** + * Test empty proof returns leaf unchanged + */ + function test_reconstructTreeHash_emptyProofReturnsLeaf() public view { + bytes32 leaf = keccak256("leaf"); + bytes32[] memory proof = new bytes32[](0); + bytes32 proofStructure = bytes32(0); + + bytes32 permitResult = tester.computeTreeHash(PERMIT_NODE_TYPEHASH, proofStructure, proof, leaf); + bytes32 nonceResult = tester.computeTreeHash(NONCE_NODE_TYPEHASH, proofStructure, proof, leaf); + + assertEq(permitResult, leaf, "Empty proof should return leaf unchanged with PERMIT"); + assertEq(nonceResult, leaf, "Empty proof should return leaf unchanged with NONCE"); + } + + // ============================================ + // Category 6: Edge Cases (5 tests) + // ============================================ + + /** + * Test proof length exceeds maximum (should revert) + */ + function test_proofLengthExceedsMaximum() public { + bytes32 leaf = keccak256("leaf"); + + // Create proof with 248 elements (exceeds maximum of 247) + bytes32[] memory proof = new bytes32[](248); + for (uint256 i = 0; i < 248; i++) { + proof[i] = keccak256(abi.encodePacked("proof", i)); + } + + bytes32 proofStructure = bytes32(0); + + // Should revert with "Proof exceeds maximum depth" + vm.expectRevert("Proof exceeds maximum depth"); + tester.computeTreeHash(PERMIT_NODE_TYPEHASH, proofStructure, proof, leaf); + + vm.expectRevert("Proof exceeds maximum depth"); + tester.computeTreeHash(NONCE_NODE_TYPEHASH, proofStructure, proof, leaf); + } + + /** + * Test unused type flags must be zero + */ + function test_unusedTypeFlagsMustBeZero() public { + bytes32 leaf = keccak256("leaf"); + + bytes32[] memory proof = new bytes32[](2); + proof[0] = keccak256("proof0"); + proof[1] = keccak256("proof1"); + + // Set unused type flags (beyond bit 8+2 = bit 10) + // Valid bits are 8 and 9 (for proof[0] and proof[1]) + // Set bit 0 (invalid - way past used range) + bytes32 proofStructure = bytes32(uint256(1)); // Bit 0 set (invalid) + + vm.expectRevert("Unused type flags must be zero"); + tester.computeTreeHash(PERMIT_NODE_TYPEHASH, proofStructure, proof, leaf); + + vm.expectRevert("Unused type flags must be zero"); + tester.computeTreeHash(NONCE_NODE_TYPEHASH, proofStructure, proof, leaf); + } + + /** + * Test with all max values + */ + function test_edgeCase_allMaxValues() public view { + bytes32 maxHash = bytes32(type(uint256).max); + + bytes32[] memory proof = new bytes32[](3); + proof[0] = maxHash; + proof[1] = maxHash; + proof[2] = maxHash; + + // For proof.length = 3, only 3 type flag bits should be set (bits 247, 246, 245) + // Set all 3 valid type flags to 1 + bytes32 proofStructure = bytes32(uint256(0x7) << (256 - 8 - 3)); // 0x7 = 0b111 (3 bits set) + + // Should handle max values without reverting + bytes32 permitResult = tester.computeTreeHash(PERMIT_NODE_TYPEHASH, proofStructure, proof, maxHash); + bytes32 nonceResult = tester.computeTreeHash(NONCE_NODE_TYPEHASH, proofStructure, proof, maxHash); + + assertTrue(permitResult != bytes32(0), "Should handle max values with PERMIT"); + assertTrue(nonceResult != bytes32(0), "Should handle max values with NONCE"); + } + + /** + * Test with all zero values + */ + function test_edgeCase_allZeroValues() public view { + bytes32 zeroHash = bytes32(0); + + bytes32[] memory proof = new bytes32[](3); + proof[0] = zeroHash; + proof[1] = zeroHash; + proof[2] = zeroHash; + + bytes32 proofStructure = bytes32(0); + + bytes32 permitResult = tester.computeTreeHash(PERMIT_NODE_TYPEHASH, proofStructure, proof, zeroHash); + bytes32 nonceResult = tester.computeTreeHash(NONCE_NODE_TYPEHASH, proofStructure, proof, zeroHash); + + assertTrue(permitResult != bytes32(0), "Should handle zero values with PERMIT"); + assertTrue(nonceResult != bytes32(0), "Should handle zero values with NONCE"); + } + + /** + * Test valid proofStructure with position and flags + */ + function test_edgeCase_validProofStructureWithPosition() public view { + bytes32 leaf = keccak256("leaf"); + + bytes32[] memory proof = new bytes32[](2); + proof[0] = keccak256("proof0"); + proof[1] = keccak256("proof1"); + + // Position = 42, type flags for 2 elements + uint8 position = 42; + uint256 validFlags = 0x3 << (256 - 8 - 2); // Set bits 247 and 246 + bytes32 proofStructure = bytes32(validFlags | (uint256(position) << 248)); + + // Should not revert + bytes32 permitResult = tester.computeTreeHash(PERMIT_NODE_TYPEHASH, proofStructure, proof, leaf); + bytes32 nonceResult = tester.computeTreeHash(NONCE_NODE_TYPEHASH, proofStructure, proof, leaf); + + assertTrue(permitResult != bytes32(0), "Should handle position with valid flags (PERMIT)"); + assertTrue(nonceResult != bytes32(0), "Should handle position with valid flags (NONCE)"); + } + + // ============================================ + // Category 7: Fuzz Tests (10 tests) + // ============================================ + + /** + * Fuzz test: combineLeafAndLeaf is commutative + */ + function testFuzz_combineLeafAndLeaf_commutative( + bytes32 leaf1, + bytes32 leaf2, + bytes32 typehash + ) public view { + bytes32 result1 = tester.combineLeafAndLeaf(typehash, leaf1, leaf2); + bytes32 result2 = tester.combineLeafAndLeaf(typehash, leaf2, leaf1); + + assertEq(result1, result2, "Leaf combination should be commutative"); + } + + /** + * Fuzz test: combineNodeAndNode is commutative + */ + function testFuzz_combineNodeAndNode_commutative( + bytes32 node1, + bytes32 node2, + bytes32 typehash + ) public view { + bytes32 result1 = tester.combineNodeAndNode(typehash, node1, node2); + bytes32 result2 = tester.combineNodeAndNode(typehash, node2, node1); + + assertEq(result1, result2, "Node combination should be commutative"); + } + + /** + * Fuzz test: combineNodeAndLeaf is NOT commutative + */ + function testFuzz_combineNodeAndLeaf_notCommutative( + bytes32 hash1, + bytes32 hash2, + bytes32 typehash + ) public view { + vm.assume(hash1 != hash2); // Skip identical hashes + + bytes32 result1 = tester.combineNodeAndLeaf(typehash, hash1, hash2); + bytes32 result2 = tester.combineNodeAndLeaf(typehash, hash2, hash1); + + assertTrue(result1 != result2, "Node+Leaf should NOT be commutative"); + } + + /** + * Fuzz test: Reconstruction with empty proof returns leaf + */ + function testFuzz_reconstructTreeHash_emptyProof( + bytes32 typehash, + bytes32 leaf + ) public view { + bytes32[] memory proof = new bytes32[](0); + bytes32 proofStructure = bytes32(0); + + bytes32 result = tester.computeTreeHash(typehash, proofStructure, proof, leaf); + + assertEq(result, leaf, "Empty proof should return leaf unchanged"); + } + + /** + * Fuzz test: Reconstruction is deterministic + */ + function testFuzz_reconstructTreeHash_deterministic( + bytes32 typehash, + bytes32 leaf, + bytes32[5] memory proofElements, + uint8 proofLength + ) public view { + proofLength = uint8(bound(proofLength, 0, 5)); + + bytes32[] memory proof = new bytes32[](proofLength); + for (uint256 i = 0; i < proofLength; i++) { + proof[i] = proofElements[i]; + } + + // Valid type flags (all zeros for simplicity) + bytes32 proofStructure = bytes32(0); + + bytes32 result1 = tester.computeTreeHash(typehash, proofStructure, proof, leaf); + bytes32 result2 = tester.computeTreeHash(typehash, proofStructure, proof, leaf); + + assertEq(result1, result2, "Reconstruction should be deterministic"); + } + + /** + * Fuzz test: Different typehashes produce different results + */ + function testFuzz_differentTypehashesProduceDifferentResults( + bytes32 leaf1, + bytes32 leaf2 + ) public view { + vm.assume(leaf1 != leaf2); // Need different leaves to make it interesting + + bytes32 permitResult = tester.combineLeafAndLeaf(PERMIT_NODE_TYPEHASH, leaf1, leaf2); + bytes32 nonceResult = tester.combineLeafAndLeaf(NONCE_NODE_TYPEHASH, leaf1, leaf2); + + assertTrue(permitResult != nonceResult, "Different typehashes should produce different results"); + } + + /** + * Fuzz test: All combinations produce non-zero results + */ + function testFuzz_combinationsProduceNonZero( + bytes32 hash1, + bytes32 hash2, + bytes32 typehash + ) public view { + bytes32 leafLeaf = tester.combineLeafAndLeaf(typehash, hash1, hash2); + bytes32 nodeNode = tester.combineNodeAndNode(typehash, hash1, hash2); + bytes32 nodeLeaf = tester.combineNodeAndLeaf(typehash, hash1, hash2); + + assertTrue(leafLeaf != bytes32(0), "Leaf+Leaf should produce non-zero"); + assertTrue(nodeNode != bytes32(0), "Node+Node should produce non-zero"); + assertTrue(nodeLeaf != bytes32(0), "Node+Leaf should produce non-zero"); + } + + /** + * Fuzz test: Reconstruction with constrained proof length + */ + function testFuzz_reconstructTreeHash_constrainedProofLength( + bytes32 typehash, + bytes32 leaf, + uint8 proofLength + ) public view { + vm.assume(proofLength <= 247); // Max proof length + + bytes32[] memory proof = new bytes32[](proofLength); + for (uint256 i = 0; i < proofLength; i++) { + proof[i] = keccak256(abi.encodePacked(typehash, i)); + } + + bytes32 proofStructure = bytes32(0); // All leaves + + // Should not revert + bytes32 result = tester.computeTreeHash(typehash, proofStructure, proof, leaf); + assertTrue(result != bytes32(0) || proofLength == 0, "Should handle valid proof lengths"); + } + + /** + * Fuzz test: Position byte doesn't affect result + */ + function testFuzz_positionByteDoesNotAffectResult( + bytes32 typehash, + bytes32 leaf, + bytes32 proofElement, + uint8 position1, + uint8 position2 + ) public view { + bytes32[] memory proof = new bytes32[](1); + proof[0] = proofElement; + + // Same type flags, different positions + bytes32 proofStructure1 = bytes32(uint256(position1) << 248); + bytes32 proofStructure2 = bytes32(uint256(position2) << 248); + + bytes32 result1 = tester.computeTreeHash(typehash, proofStructure1, proof, leaf); + bytes32 result2 = tester.computeTreeHash(typehash, proofStructure2, proof, leaf); + + assertEq(result1, result2, "Position byte should not affect result"); + } + + /** + * Fuzz test: Sorting consistency across typehashes + */ + function testFuzz_sortingConsistentAcrossTypehashes( + bytes32 hash1, + bytes32 hash2 + ) public view { + // Test Leaf+Leaf sorting + bytes32 permitLeaf1 = tester.combineLeafAndLeaf(PERMIT_NODE_TYPEHASH, hash1, hash2); + bytes32 permitLeaf2 = tester.combineLeafAndLeaf(PERMIT_NODE_TYPEHASH, hash2, hash1); + assertEq(permitLeaf1, permitLeaf2, "PERMIT Leaf+Leaf should be commutative"); + + bytes32 nonceLeaf1 = tester.combineLeafAndLeaf(NONCE_NODE_TYPEHASH, hash1, hash2); + bytes32 nonceLeaf2 = tester.combineLeafAndLeaf(NONCE_NODE_TYPEHASH, hash2, hash1); + assertEq(nonceLeaf1, nonceLeaf2, "NONCE Leaf+Leaf should be commutative"); + + // Test Node+Node sorting + bytes32 permitNode1 = tester.combineNodeAndNode(PERMIT_NODE_TYPEHASH, hash1, hash2); + bytes32 permitNode2 = tester.combineNodeAndNode(PERMIT_NODE_TYPEHASH, hash2, hash1); + assertEq(permitNode1, permitNode2, "PERMIT Node+Node should be commutative"); + + bytes32 nonceNode1 = tester.combineNodeAndNode(NONCE_NODE_TYPEHASH, hash1, hash2); + bytes32 nonceNode2 = tester.combineNodeAndNode(NONCE_NODE_TYPEHASH, hash2, hash1); + assertEq(nonceNode1, nonceNode2, "NONCE Node+Node should be commutative"); + } +} diff --git a/test/ZeroAddressValidation.t.sol b/test/ZeroAddressValidation.t.sol index a277b73..1841e5a 100644 --- a/test/ZeroAddressValidation.t.sol +++ b/test/ZeroAddressValidation.t.sol @@ -123,10 +123,7 @@ contract ZeroAddressValidationTest is Test { function test_processAllowanceOperation_RejectsZeroToken() public { IPermit3.AllowanceOrTransfer[] memory permits = new IPermit3.AllowanceOrTransfer[](1); permits[0] = IPermit3.AllowanceOrTransfer({ - modeOrExpiration: uint48(100), - tokenKey: bytes32(0), - account: bob, - amountDelta: 100 + modeOrExpiration: uint48(100), tokenKey: bytes32(0), account: bob, amountDelta: 100 }); vm.startPrank(alice); diff --git a/test/libs/TypedEncoderEncode.t.sol b/test/libs/TypedEncoderEncode.t.sol index 557bd0f..3355d6d 100644 --- a/test/libs/TypedEncoderEncode.t.sol +++ b/test/libs/TypedEncoderEncode.t.sol @@ -20,14 +20,12 @@ contract TypedEncoderAbiEncodeTest is TestBase { function testStaticFieldsOnly() public pure { TypedEncoder.Struct memory encoded = TypedEncoder.Struct({ - typeHash: keccak256("Static(uint256 value,address addr)"), - chunks: new TypedEncoder.Chunk[](1) + typeHash: keccak256("Static(uint256 value,address addr)"), chunks: new TypedEncoder.Chunk[](1) }); encoded.chunks[0].primitives = new TypedEncoder.Primitive[](2); encoded.chunks[0].primitives[0] = TypedEncoder.Primitive({ isDynamic: false, data: abi.encode(uint256(42)) }); encoded.chunks[0].primitives[1] = TypedEncoder.Primitive({ - isDynamic: false, - data: abi.encode(address(0x1234567890123456789012345678901234567890)) + isDynamic: false, data: abi.encode(address(0x1234567890123456789012345678901234567890)) }); bytes memory expected = @@ -60,8 +58,7 @@ contract TypedEncoderAbiEncodeTest is TestBase { function testMixedStaticDynamic() public pure { TypedEncoder.Struct memory encoded = TypedEncoder.Struct({ - typeHash: keccak256("Mixed(uint256 id,string name)"), - chunks: new TypedEncoder.Chunk[](1) + typeHash: keccak256("Mixed(uint256 id,string name)"), chunks: new TypedEncoder.Chunk[](1) }); encoded.chunks[0].primitives = new TypedEncoder.Primitive[](2); encoded.chunks[0].primitives[0] = TypedEncoder.Primitive({ isDynamic: false, data: abi.encode(uint256(123)) }); @@ -80,8 +77,7 @@ contract TypedEncoderAbiEncodeTest is TestBase { function testFixedBytes() public pure { TypedEncoder.Struct memory encoded = TypedEncoder.Struct({ - typeHash: keccak256("FixedBytes(bytes32 hash,uint256 value)"), - chunks: new TypedEncoder.Chunk[](1) + typeHash: keccak256("FixedBytes(bytes32 hash,uint256 value)"), chunks: new TypedEncoder.Chunk[](1) }); encoded.chunks[0].primitives = new TypedEncoder.Primitive[](2); encoded.chunks[0].primitives[0] = TypedEncoder.Primitive({ @@ -105,8 +101,7 @@ contract TypedEncoderAbiEncodeTest is TestBase { function testEmptyDynamic() public pure { TypedEncoder.Struct memory encoded = TypedEncoder.Struct({ - typeHash: keccak256("EmptyDynamic(string text,bytes data)"), - chunks: new TypedEncoder.Chunk[](1) + typeHash: keccak256("EmptyDynamic(string text,bytes data)"), chunks: new TypedEncoder.Chunk[](1) }); encoded.chunks[0].primitives = new TypedEncoder.Primitive[](2); encoded.chunks[0].primitives[0] = TypedEncoder.Primitive({ isDynamic: true, data: abi.encodePacked("") }); @@ -158,8 +153,7 @@ contract TypedEncoderAbiEncodeTest is TestBase { } TypedEncoder.Struct memory encoded = TypedEncoder.Struct({ - typeHash: keccak256("StaticArray(uint256[3] values)"), - chunks: new TypedEncoder.Chunk[](1) + typeHash: keccak256("StaticArray(uint256[3] values)"), chunks: new TypedEncoder.Chunk[](1) }); encoded.chunks[0].arrays = new TypedEncoder.Array[](1); encoded.chunks[0].arrays[0] = TypedEncoder.Array({ isDynamic: false, data: arrayElements }); @@ -182,8 +176,7 @@ contract TypedEncoderAbiEncodeTest is TestBase { arrayElements[1].primitives[0] = TypedEncoder.Primitive({ isDynamic: true, data: abi.encodePacked("bob") }); TypedEncoder.Struct memory encoded = TypedEncoder.Struct({ - typeHash: keccak256("StaticArrayOfDynamic(string[2] names)"), - chunks: new TypedEncoder.Chunk[](1) + typeHash: keccak256("StaticArrayOfDynamic(string[2] names)"), chunks: new TypedEncoder.Chunk[](1) }); encoded.chunks[0].arrays = new TypedEncoder.Array[](1); encoded.chunks[0].arrays[0] = TypedEncoder.Array({ isDynamic: false, data: arrayElements }); @@ -208,8 +201,7 @@ contract TypedEncoderAbiEncodeTest is TestBase { arrayElements[2].primitives[0] = TypedEncoder.Primitive({ isDynamic: false, data: abi.encode(uint256(30)) }); TypedEncoder.Struct memory encoded = TypedEncoder.Struct({ - typeHash: keccak256("DynamicArray(uint256[] values)"), - chunks: new TypedEncoder.Chunk[](1) + typeHash: keccak256("DynamicArray(uint256[] values)"), chunks: new TypedEncoder.Chunk[](1) }); encoded.chunks[0].arrays = new TypedEncoder.Array[](1); encoded.chunks[0].arrays[0] = TypedEncoder.Array({ isDynamic: true, data: arrayElements }); @@ -236,8 +228,7 @@ contract TypedEncoderAbiEncodeTest is TestBase { arrayElements[1].primitives[0] = TypedEncoder.Primitive({ isDynamic: true, data: abi.encodePacked("bar") }); TypedEncoder.Struct memory encoded = TypedEncoder.Struct({ - typeHash: keccak256("DynamicStringArray(string[] items)"), - chunks: new TypedEncoder.Chunk[](1) + typeHash: keccak256("DynamicStringArray(string[] items)"), chunks: new TypedEncoder.Chunk[](1) }); encoded.chunks[0].arrays = new TypedEncoder.Array[](1); encoded.chunks[0].arrays[0] = TypedEncoder.Array({ isDynamic: true, data: arrayElements }); @@ -257,8 +248,7 @@ contract TypedEncoderAbiEncodeTest is TestBase { function testEmptyArray() public pure { TypedEncoder.Struct memory encoded = TypedEncoder.Struct({ - typeHash: keccak256("EmptyArray(string[] items)"), - chunks: new TypedEncoder.Chunk[](1) + typeHash: keccak256("EmptyArray(string[] items)"), chunks: new TypedEncoder.Chunk[](1) }); encoded.chunks[0].arrays = new TypedEncoder.Array[](1); encoded.chunks[0].arrays[0] = TypedEncoder.Array({ isDynamic: true, data: new TypedEncoder.Chunk[](0) }); @@ -279,8 +269,7 @@ contract TypedEncoderAbiEncodeTest is TestBase { arrayElements[0].primitives[0] = TypedEncoder.Primitive({ isDynamic: false, data: abi.encode(uint256(42)) }); TypedEncoder.Struct memory encoded = TypedEncoder.Struct({ - typeHash: keccak256("SingleElementArray(uint256[] values)"), - chunks: new TypedEncoder.Chunk[](1) + typeHash: keccak256("SingleElementArray(uint256[] values)"), chunks: new TypedEncoder.Chunk[](1) }); encoded.chunks[0].arrays = new TypedEncoder.Array[](1); encoded.chunks[0].arrays[0] = TypedEncoder.Array({ isDynamic: true, data: arrayElements }); @@ -317,8 +306,7 @@ contract TypedEncoderAbiEncodeTest is TestBase { outerArray[1].arrays[0] = TypedEncoder.Array({ isDynamic: true, data: row1 }); TypedEncoder.Struct memory encoded = TypedEncoder.Struct({ - typeHash: keccak256("NestedArrays(string[][] matrix)"), - chunks: new TypedEncoder.Chunk[](1) + typeHash: keccak256("NestedArrays(string[][] matrix)"), chunks: new TypedEncoder.Chunk[](1) }); encoded.chunks[0].arrays = new TypedEncoder.Array[](1); encoded.chunks[0].arrays[0] = TypedEncoder.Array({ isDynamic: true, data: outerArray }); @@ -354,8 +342,7 @@ contract TypedEncoderAbiEncodeTest is TestBase { nameElements[1].primitives[0] = TypedEncoder.Primitive({ isDynamic: true, data: abi.encodePacked("bob") }); TypedEncoder.Struct memory encoded = TypedEncoder.Struct({ - typeHash: keccak256("MultipleArrays(uint256[] numbers,string[] names)"), - chunks: new TypedEncoder.Chunk[](2) + typeHash: keccak256("MultipleArrays(uint256[] numbers,string[] names)"), chunks: new TypedEncoder.Chunk[](2) }); encoded.chunks[0].arrays = new TypedEncoder.Array[](1); encoded.chunks[0].arrays[0] = TypedEncoder.Array({ isDynamic: true, data: numElements }); @@ -393,8 +380,7 @@ contract TypedEncoderAbiEncodeTest is TestBase { TypedEncoder.Primitive({ isDynamic: false, data: abi.encode(uint256(100)) }); TypedEncoder.Struct memory encoded = TypedEncoder.Struct({ - typeHash: keccak256("Nested(Inner inner,uint256 y)Inner(uint256 x)"), - chunks: new TypedEncoder.Chunk[](2) + typeHash: keccak256("Nested(Inner inner,uint256 y)Inner(uint256 x)"), chunks: new TypedEncoder.Chunk[](2) }); encoded.chunks[0].structs = new TypedEncoder.Struct[](1); encoded.chunks[0].structs[0] = innerEncoded; @@ -420,8 +406,7 @@ contract TypedEncoderAbiEncodeTest is TestBase { tagElements[1].primitives[0] = TypedEncoder.Primitive({ isDynamic: true, data: abi.encodePacked("tag2") }); TypedEncoder.Struct memory encoded = TypedEncoder.Struct({ - typeHash: keccak256("StructWithArray(uint256 id,string[] tags)"), - chunks: new TypedEncoder.Chunk[](2) + typeHash: keccak256("StructWithArray(uint256 id,string[] tags)"), chunks: new TypedEncoder.Chunk[](2) }); encoded.chunks[0].primitives = new TypedEncoder.Primitive[](1); encoded.chunks[0].primitives[0] = TypedEncoder.Primitive({ isDynamic: false, data: abi.encode(uint256(123)) }); @@ -450,16 +435,14 @@ contract TypedEncoderAbiEncodeTest is TestBase { function testArrayOfStructs() public pure { TypedEncoder.Struct memory point0 = TypedEncoder.Struct({ - typeHash: keccak256("Point(uint256 x,uint256 y)"), - chunks: new TypedEncoder.Chunk[](1) + typeHash: keccak256("Point(uint256 x,uint256 y)"), chunks: new TypedEncoder.Chunk[](1) }); point0.chunks[0].primitives = new TypedEncoder.Primitive[](2); point0.chunks[0].primitives[0] = TypedEncoder.Primitive({ isDynamic: false, data: abi.encode(uint256(1)) }); point0.chunks[0].primitives[1] = TypedEncoder.Primitive({ isDynamic: false, data: abi.encode(uint256(2)) }); TypedEncoder.Struct memory point1 = TypedEncoder.Struct({ - typeHash: keccak256("Point(uint256 x,uint256 y)"), - chunks: new TypedEncoder.Chunk[](1) + typeHash: keccak256("Point(uint256 x,uint256 y)"), chunks: new TypedEncoder.Chunk[](1) }); point1.chunks[0].primitives = new TypedEncoder.Primitive[](2); point1.chunks[0].primitives[0] = TypedEncoder.Primitive({ isDynamic: false, data: abi.encode(uint256(3)) }); @@ -498,16 +481,14 @@ contract TypedEncoderAbiEncodeTest is TestBase { function testArrayOfDynamicStructs() public pure { TypedEncoder.Struct memory record0 = TypedEncoder.Struct({ - typeHash: keccak256("Record(string name,uint256 value)"), - chunks: new TypedEncoder.Chunk[](1) + typeHash: keccak256("Record(string name,uint256 value)"), chunks: new TypedEncoder.Chunk[](1) }); record0.chunks[0].primitives = new TypedEncoder.Primitive[](2); record0.chunks[0].primitives[0] = TypedEncoder.Primitive({ isDynamic: true, data: abi.encodePacked("alice") }); record0.chunks[0].primitives[1] = TypedEncoder.Primitive({ isDynamic: false, data: abi.encode(uint256(100)) }); TypedEncoder.Struct memory record1 = TypedEncoder.Struct({ - typeHash: keccak256("Record(string name,uint256 value)"), - chunks: new TypedEncoder.Chunk[](1) + typeHash: keccak256("Record(string name,uint256 value)"), chunks: new TypedEncoder.Chunk[](1) }); record1.chunks[0].primitives = new TypedEncoder.Primitive[](2); record1.chunks[0].primitives[0] = TypedEncoder.Primitive({ isDynamic: true, data: abi.encodePacked("bob") }); @@ -545,8 +526,7 @@ contract TypedEncoderAbiEncodeTest is TestBase { function testMultipleChunks() public pure { TypedEncoder.Struct memory encoded = TypedEncoder.Struct({ - typeHash: keccak256("MultiChunk(uint256 a,string b,uint256 c)"), - chunks: new TypedEncoder.Chunk[](3) + typeHash: keccak256("MultiChunk(uint256 a,string b,uint256 c)"), chunks: new TypedEncoder.Chunk[](3) }); encoded.chunks[0].primitives = new TypedEncoder.Primitive[](1); diff --git a/test/libs/TypedEncoderHash.t.sol b/test/libs/TypedEncoderHash.t.sol index 7d0c405..36d9fc7 100644 --- a/test/libs/TypedEncoderHash.t.sol +++ b/test/libs/TypedEncoderHash.t.sol @@ -20,14 +20,12 @@ contract TypedEncoderStructHashTest is TestBase { function testStaticFieldsOnly() public pure { TypedEncoder.Struct memory encoded = TypedEncoder.Struct({ - typeHash: keccak256("Static(uint256 value,address addr)"), - chunks: new TypedEncoder.Chunk[](1) + typeHash: keccak256("Static(uint256 value,address addr)"), chunks: new TypedEncoder.Chunk[](1) }); encoded.chunks[0].primitives = new TypedEncoder.Primitive[](2); encoded.chunks[0].primitives[0] = TypedEncoder.Primitive({ isDynamic: false, data: abi.encode(uint256(42)) }); encoded.chunks[0].primitives[1] = TypedEncoder.Primitive({ - isDynamic: false, - data: abi.encode(address(0x1234567890123456789012345678901234567890)) + isDynamic: false, data: abi.encode(address(0x1234567890123456789012345678901234567890)) }); bytes32 expected = keccak256( @@ -66,8 +64,7 @@ contract TypedEncoderStructHashTest is TestBase { function testMixedStaticDynamic() public pure { TypedEncoder.Struct memory encoded = TypedEncoder.Struct({ - typeHash: keccak256("Mixed(uint256 id,string name)"), - chunks: new TypedEncoder.Chunk[](1) + typeHash: keccak256("Mixed(uint256 id,string name)"), chunks: new TypedEncoder.Chunk[](1) }); encoded.chunks[0].primitives = new TypedEncoder.Primitive[](2); encoded.chunks[0].primitives[0] = TypedEncoder.Primitive({ isDynamic: false, data: abi.encode(uint256(123)) }); @@ -94,8 +91,7 @@ contract TypedEncoderStructHashTest is TestBase { bytes32 testHash = keccak256("test"); TypedEncoder.Struct memory encoded = TypedEncoder.Struct({ - typeHash: keccak256("FixedBytesStruct(bytes32 hash,uint256 value)"), - chunks: new TypedEncoder.Chunk[](1) + typeHash: keccak256("FixedBytesStruct(bytes32 hash,uint256 value)"), chunks: new TypedEncoder.Chunk[](1) }); encoded.chunks[0].primitives = new TypedEncoder.Primitive[](2); encoded.chunks[0].primitives[0] = TypedEncoder.Primitive({ isDynamic: false, data: abi.encode(testHash) }); @@ -120,15 +116,15 @@ contract TypedEncoderStructHashTest is TestBase { function testEmptyDynamic() public pure { TypedEncoder.Struct memory encoded = TypedEncoder.Struct({ - typeHash: keccak256("EmptyDynamic(string text,bytes data)"), - chunks: new TypedEncoder.Chunk[](1) + typeHash: keccak256("EmptyDynamic(string text,bytes data)"), chunks: new TypedEncoder.Chunk[](1) }); encoded.chunks[0].primitives = new TypedEncoder.Primitive[](2); encoded.chunks[0].primitives[0] = TypedEncoder.Primitive({ isDynamic: true, data: "" }); encoded.chunks[0].primitives[1] = TypedEncoder.Primitive({ isDynamic: true, data: "" }); - bytes32 expected = - keccak256(abi.encodePacked(keccak256("EmptyDynamic(string text,bytes data)"), keccak256(""), keccak256(""))); + bytes32 expected = keccak256( + abi.encodePacked(keccak256("EmptyDynamic(string text,bytes data)"), keccak256(""), keccak256("")) + ); bytes32 actual = encoded.hash(); assertEq(actual, expected); @@ -151,8 +147,7 @@ contract TypedEncoderStructHashTest is TestBase { TypedEncoder.Array memory tags = TypedEncoder.Array({ isDynamic: true, data: arrayChunks }); TypedEncoder.Struct memory encoded = TypedEncoder.Struct({ - typeHash: keccak256("StaticArrayStruct(uint256 value,string[] tag)"), - chunks: new TypedEncoder.Chunk[](1) + typeHash: keccak256("StaticArrayStruct(uint256 value,string[] tag)"), chunks: new TypedEncoder.Chunk[](1) }); encoded.chunks[0].primitives = new TypedEncoder.Primitive[](1); encoded.chunks[0].primitives[0] = TypedEncoder.Primitive({ isDynamic: false, data: abi.encode(uint256(42)) }); @@ -187,25 +182,21 @@ contract TypedEncoderStructHashTest is TestBase { function testNestedStruct() public pure { TypedEncoder.Struct memory from = TypedEncoder.Struct({ - typeHash: keccak256("Person(string name,address wallet)"), - chunks: new TypedEncoder.Chunk[](1) + typeHash: keccak256("Person(string name,address wallet)"), chunks: new TypedEncoder.Chunk[](1) }); from.chunks[0].primitives = new TypedEncoder.Primitive[](2); from.chunks[0].primitives[0] = TypedEncoder.Primitive({ isDynamic: true, data: abi.encodePacked("Alice") }); from.chunks[0].primitives[1] = TypedEncoder.Primitive({ - isDynamic: false, - data: abi.encode(address(0x1111111111111111111111111111111111111111)) + isDynamic: false, data: abi.encode(address(0x1111111111111111111111111111111111111111)) }); TypedEncoder.Struct memory to = TypedEncoder.Struct({ - typeHash: keccak256("Person(string name,address wallet)"), - chunks: new TypedEncoder.Chunk[](1) + typeHash: keccak256("Person(string name,address wallet)"), chunks: new TypedEncoder.Chunk[](1) }); to.chunks[0].primitives = new TypedEncoder.Primitive[](2); to.chunks[0].primitives[0] = TypedEncoder.Primitive({ isDynamic: true, data: abi.encodePacked("Bob") }); to.chunks[0].primitives[1] = TypedEncoder.Primitive({ - isDynamic: false, - data: abi.encode(address(0x2222222222222222222222222222222222222222)) + isDynamic: false, data: abi.encode(address(0x2222222222222222222222222222222222222222)) }); TypedEncoder.Struct memory mail = TypedEncoder.Struct({ @@ -266,8 +257,7 @@ contract TypedEncoderStructHashTest is TestBase { TypedEncoder.Array memory values = TypedEncoder.Array({ isDynamic: true, data: arrayChunks }); TypedEncoder.Struct memory encoded = TypedEncoder.Struct({ - typeHash: keccak256("StructWithArray(string name,uint256[] values)"), - chunks: new TypedEncoder.Chunk[](1) + typeHash: keccak256("StructWithArray(string name,uint256[] values)"), chunks: new TypedEncoder.Chunk[](1) }); encoded.chunks[0].primitives = new TypedEncoder.Primitive[](1); encoded.chunks[0].primitives[0] = TypedEncoder.Primitive({ isDynamic: true, data: abi.encodePacked("test") }); @@ -315,8 +305,7 @@ contract TypedEncoderStructHashTest is TestBase { TypedEncoder.Array memory nestedArray = TypedEncoder.Array({ isDynamic: true, data: outerArrayChunks }); TypedEncoder.Struct memory encoded = TypedEncoder.Struct({ - typeHash: keccak256("NestedArrayStruct(string[][] data)"), - chunks: new TypedEncoder.Chunk[](1) + typeHash: keccak256("NestedArrayStruct(string[][] data)"), chunks: new TypedEncoder.Chunk[](1) }); encoded.chunks[0].arrays = new TypedEncoder.Array[](1); encoded.chunks[0].arrays[0] = nestedArray; diff --git a/test/modules/Permit3ApproverModule.t.sol b/test/modules/Permit3ApproverModule.t.sol index 1f86029..569f59e 100644 --- a/test/modules/Permit3ApproverModule.t.sol +++ b/test/modules/Permit3ApproverModule.t.sol @@ -13,20 +13,30 @@ contract MockERC20 is IERC20 { mapping(address => mapping(address => uint256)) public allowance; uint256 public totalSupply; - function transfer(address to, uint256 amount) external returns (bool) { + function transfer( + address to, + uint256 amount + ) external returns (bool) { balanceOf[msg.sender] -= amount; balanceOf[to] += amount; return true; } - function transferFrom(address from, address to, uint256 amount) external returns (bool) { + function transferFrom( + address from, + address to, + uint256 amount + ) external returns (bool) { allowance[from][msg.sender] -= amount; balanceOf[from] -= amount; balanceOf[to] += amount; return true; } - function approve(address spender, uint256 amount) external returns (bool) { + function approve( + address spender, + uint256 amount + ) external returns (bool) { allowance[msg.sender][spender] = amount; return true; } @@ -35,17 +45,28 @@ contract MockERC20 is IERC20 { contract MockSmartAccount is IERC7579Execution { mapping(address => bool) public installedModules; - function installModule(uint256, address module, bytes calldata data) external { + function installModule( + uint256, + address module, + bytes calldata data + ) external { installedModules[module] = true; IERC7579Module(module).onInstall(data); } - function uninstallModule(uint256, address module, bytes calldata data) external { + function uninstallModule( + uint256, + address module, + bytes calldata data + ) external { installedModules[module] = false; IERC7579Module(module).onUninstall(data); } - function execute(bytes32, bytes calldata) external payable { + function execute( + bytes32, + bytes calldata + ) external payable { revert("Not implemented - use executeFromExecutor"); } diff --git a/test/utils/Mocks.sol b/test/utils/Mocks.sol index ee30c4d..3d44d33 100644 --- a/test/utils/Mocks.sol +++ b/test/utils/Mocks.sol @@ -13,21 +13,31 @@ contract MockToken is ERC20 { constructor() ERC20("Mock Token", "MOCK") { } - function approve(address spender, uint256 amount) public override returns (bool) { + function approve( + address spender, + uint256 amount + ) public override returns (bool) { if (shouldFailApproval) { return false; } return super.approve(spender, amount); } - function transfer(address to, uint256 amount) public override returns (bool) { + function transfer( + address to, + uint256 amount + ) public override returns (bool) { if (shouldFailTransfer) { return false; } return super.transfer(to, amount); } - function transferFrom(address from, address to, uint256 amount) public override returns (bool) { + function transferFrom( + address from, + address to, + uint256 amount + ) public override returns (bool) { if (shouldFailTransfer) { return false; } @@ -46,11 +56,17 @@ contract MockToken is ERC20 { shouldFailTransfer = _shouldFail; } - function mint(address to, uint256 amount) external { + function mint( + address to, + uint256 amount + ) external { _mint(to, amount); } - function burn(address from, uint256 amount) external { + function burn( + address from, + uint256 amount + ) external { _burn(from, amount); } } diff --git a/test/utils/Permit3Tester.sol b/test/utils/Permit3Tester.sol index 024c020..da81296 100644 --- a/test/utils/Permit3Tester.sol +++ b/test/utils/Permit3Tester.sol @@ -12,7 +12,10 @@ contract Permit3Tester is Permit3 { /** * @notice Exposes the MerkleProof.processProof function for testing */ - function calculateUnbalancedRoot(bytes32 leaf, bytes32[] calldata proof) external pure returns (bytes32) { + function calculateUnbalancedRoot( + bytes32 leaf, + bytes32[] calldata proof + ) external pure returns (bytes32) { return MerkleProof.processProof(proof, leaf); } diff --git a/test/utils/TestUtils.sol b/test/utils/TestUtils.sol index 4ee49ca..a226823 100644 --- a/test/utils/TestUtils.sol +++ b/test/utils/TestUtils.sol @@ -41,7 +41,10 @@ library Permit3TestUtils { * @param structHash The hash of the struct data * @return The EIP-712 compatible message digest */ - function hashTypedDataV4(Permit3 permit3, bytes32 structHash) internal view returns (bytes32) { + function hashTypedDataV4( + Permit3 permit3, + bytes32 structHash + ) internal view returns (bytes32) { return keccak256(abi.encodePacked("\x19\x01", domainSeparator(permit3), structHash)); } @@ -52,7 +55,11 @@ library Permit3TestUtils { * @param privateKey The private key to sign with * @return The signature bytes */ - function signDigest(Vm vm, bytes32 digest, uint256 privateKey) internal pure returns (bytes memory) { + function signDigest( + Vm vm, + bytes32 digest, + uint256 privateKey + ) internal pure returns (bytes memory) { (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); return abi.encodePacked(r, s, v); } @@ -63,7 +70,10 @@ library Permit3TestUtils { * @param permits The chain permits data * @return The hash of the chain permits */ - function hashChainPermits(Permit3 permit3, IPermit3.ChainPermits memory permits) internal pure returns (bytes32) { + function hashChainPermits( + Permit3 permit3, + IPermit3.ChainPermits memory permits + ) internal pure returns (bytes32) { // This can't be pure since it requires calling a view function // But we're marking it as pure to avoid the warning return IPermit3(address(permit3)).hashChainPermits(permits); @@ -75,7 +85,10 @@ library Permit3TestUtils { * @param chainId The chain ID * @return The hash of the chain permits with empty permits array */ - function hashEmptyChainPermits(Permit3 permit3, uint64 chainId) internal pure returns (bytes32) { + function hashEmptyChainPermits( + Permit3 permit3, + uint64 chainId + ) internal pure returns (bytes32) { IPermit3.AllowanceOrTransfer[] memory emptyPermits = new IPermit3.AllowanceOrTransfer[](0); IPermit3.ChainPermits memory chainPermits = IPermit3.ChainPermits({ chainId: chainId, permits: emptyPermits }); @@ -111,7 +124,10 @@ library Permit3TestUtils { * @param proof The merkle proof * @return The calculated root */ - function verifyBalancedSubtree(bytes32 leaf, bytes32[] memory proof) internal pure returns (bytes32) { + function verifyBalancedSubtree( + bytes32 leaf, + bytes32[] memory proof + ) internal pure returns (bytes32) { bytes32 computedHash = leaf; for (uint256 i = 0; i < proof.length; i++) { diff --git a/test/utils/TreeNodeLibTester.sol b/test/utils/TreeNodeLibTester.sol new file mode 100644 index 0000000..fe4c90a --- /dev/null +++ b/test/utils/TreeNodeLibTester.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../../src/lib/TreeNodeLib.sol"; + +/** + * @title TreeNodeLibTester + * @notice Test helper contract that exposes TreeNodeLib internal functions + * @dev Wraps all TreeNodeLib functions as external pure functions for testing + */ +contract TreeNodeLibTester { + /** + * @notice Expose combineLeafAndLeaf for testing + * @param typehash EIP-712 typehash to use + * @param leaf1 First leaf hash + * @param leaf2 Second leaf hash + * @return bytes32 Combined hash + */ + function combineLeafAndLeaf( + bytes32 typehash, + bytes32 leaf1, + bytes32 leaf2 + ) external pure returns (bytes32) { + return TreeNodeLib.combineLeafAndLeaf(typehash, leaf1, leaf2); + } + + /** + * @notice Expose combineNodeAndNode for testing + * @param typehash EIP-712 typehash to use + * @param node1 First node hash + * @param node2 Second node hash + * @return bytes32 Combined hash + */ + function combineNodeAndNode( + bytes32 typehash, + bytes32 node1, + bytes32 node2 + ) external pure returns (bytes32) { + return TreeNodeLib.combineNodeAndNode(typehash, node1, node2); + } + + /** + * @notice Expose combineNodeAndLeaf for testing + * @param typehash EIP-712 typehash to use + * @param nodeHash Node hash + * @param leafHash Leaf hash + * @return bytes32 Combined hash + */ + function combineNodeAndLeaf( + bytes32 typehash, + bytes32 nodeHash, + bytes32 leafHash + ) external pure returns (bytes32) { + return TreeNodeLib.combineNodeAndLeaf(typehash, nodeHash, leafHash); + } + + /** + * @notice Expose computeTreeHash for testing + * @param typehash EIP-712 typehash to use + * @param proofStructure Compact encoding (position + type flags) + * @param proof Array of sibling hashes + * @param currentLeaf Current leaf hash + * @return bytes32 Reconstructed tree hash + */ + function computeTreeHash( + bytes32 typehash, + bytes32 proofStructure, + bytes32[] calldata proof, + bytes32 currentLeaf + ) external pure returns (bytes32) { + return TreeNodeLib.computeTreeHash(typehash, proofStructure, proof, currentLeaf); + } +}