diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 00000000..428e29b8 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +clear && forge fmt && forge snapshot --isolate --match-contract Benchmark --via-ir && git add snapshots diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..26d33521 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/account.iml b/.idea/account.iml new file mode 100644 index 00000000..1ba0d660 --- /dev/null +++ b/.idea/account.iml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/caches/deviceStreaming.xml b/.idea/caches/deviceStreaming.xml new file mode 100644 index 00000000..cc4b430a --- /dev/null +++ b/.idea/caches/deviceStreaming.xml @@ -0,0 +1,1698 @@ + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 00000000..4bec4ea8 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,117 @@ + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000..a55e7a17 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask2agent.xml b/.idea/copilot.data.migration.ask2agent.xml new file mode 100644 index 00000000..1f2ea11e --- /dev/null +++ b/.idea/copilot.data.migration.ask2agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/markdown.xml b/.idea/markdown.xml new file mode 100644 index 00000000..c61ea334 --- /dev/null +++ b/.idea/markdown.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..4ae0bf91 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..1995e14d --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/IthacaAccount.sol b/src/IthacaAccount.sol index ddef33fe..a8ffdbca 100644 --- a/src/IthacaAccount.sol +++ b/src/IthacaAccount.sol @@ -23,6 +23,7 @@ import {LibNonce} from "./libraries/LibNonce.sol"; import {TokenTransferLib} from "./libraries/TokenTransferLib.sol"; import {LibTStack} from "./libraries/LibTStack.sol"; import {IIthacaAccount} from "./interfaces/IIthacaAccount.sol"; +import {MerkleProofLib} from "solady/utils/MerkleProofLib.sol"; /// @title Account /// @notice A account contract for EOAs with EIP7702. @@ -490,9 +491,43 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { return isMultichain ? _hashTypedDataSansChainId(structHash) : _hashTypedData(structHash); } + /// @dev Verifies the merkle sig + /// - Note: Each leaf of the merkle tree should be a standard digest. + /// - The signature for using merkle verification is encoded as: + /// - bytes signature = abi.encode(bytes32[] proof, bytes32 root, bytes rootSig) + function _verifyMerkleSig(bytes32 digest, bytes calldata signature) + internal + view + returns (bool isValid, bytes32 keyHash) + { + bytes32[] calldata proof; + bytes32 root; + bytes calldata rootSig; + + assembly ("memory-safe") { + let proofOffset := add(signature.offset, calldataload(signature.offset)) + proof.length := calldataload(proofOffset) + proof.offset := add(proofOffset, 0x20) + + root := calldataload(add(signature.offset, 0x20)) + + let rootSigOffset := add(signature.offset, calldataload(add(signature.offset, 0x40))) + rootSig.length := calldataload(rootSigOffset) + rootSig.offset := add(rootSigOffset, 0x20) + } + + if (MerkleProofLib.verifyCalldata(proof, root, digest)) { + (isValid, keyHash) = unwrapAndValidateSignature(root, rootSig); + + return (isValid, keyHash); + } + + return (false, bytes32(0)); + } + /// @dev Returns if the signature is valid, along with its `keyHash`. /// The `signature` is a wrapped signature, given by - /// `abi.encodePacked(bytes(innerSignature), bytes32(keyHash), bool(prehash))`. + /// `abi.encode(bytes(innerSignature), bytes32(keyHash), bool(prehash), bool(merkle))`. function unwrapAndValidateSignature(bytes32 digest, bytes calldata signature) public view @@ -507,14 +542,20 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { return (ECDSA.recoverCalldata(digest, signature) == address(this), 0); } + bool merkle; unchecked { - uint256 n = signature.length - 0x21; + uint256 n = signature.length - 0x22; keyHash = LibBytes.loadCalldata(signature, n); signature = LibBytes.truncatedCalldata(signature, n); // Do the prehash if last byte is non-zero. if (uint256(LibBytes.loadCalldata(signature, n + 1)) & 0xff != 0) { digest = EfficientHashLib.sha2(digest); // `sha256(abi.encode(digest))`. } + merkle = uint256(LibBytes.loadCalldata(signature, n + 2)) & 0xff != 0; + } + + if (merkle) { + return _verifyMerkleSig(digest, signature); } Key memory key = getKey(keyHash); diff --git a/test/Base.t.sol b/test/Base.t.sol index 4e353a9a..aa0d05ad 100644 --- a/test/Base.t.sol +++ b/test/Base.t.sol @@ -219,7 +219,7 @@ contract BaseTest is SoladyTest { { (bytes32 r, bytes32 s) = vm.signP256(privateKey, digest); s = P256.normalized(s); - return abi.encodePacked(abi.encode(r, s), keyHash, uint8(prehash ? 1 : 0)); + return abi.encodePacked(abi.encode(r, s), keyHash, uint8(prehash ? 1 : 0), uint8(0)); } function _secp256k1Sig(uint256 privateKey, bytes32 keyHash, bytes32 digest) @@ -236,7 +236,8 @@ contract BaseTest is SoladyTest { returns (bytes memory) { (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); - return abi.encodePacked(abi.encodePacked(r, s, v), keyHash, uint8(prehash ? 1 : 0)); + return + abi.encodePacked(abi.encodePacked(r, s, v), keyHash, uint8(prehash ? 1 : 0), uint8(0)); } function _multiSig(MultiSigKey memory k, bytes32 keyHash, bool preHash, bytes32 digest) @@ -249,7 +250,7 @@ contract BaseTest is SoladyTest { signatures[i] = _sig(k.owners[i], digest); } - return abi.encodePacked(abi.encode(signatures), keyHash, uint8(preHash ? 1 : 0)); + return abi.encodePacked(abi.encode(signatures), keyHash, uint8(preHash ? 1 : 0), uint8(0)); } function _estimateGasForEOAKey(Orchestrator.Intent memory i) @@ -281,7 +282,7 @@ contract BaseTest is SoladyTest { { (uint8 v, bytes32 r, bytes32 s) = vm.sign(uint128(_randomUniform()), bytes32(_randomUniform())); - i.signature = abi.encodePacked(abi.encodePacked(r, s, v), keyHash, uint8(0)); + i.signature = abi.encodePacked(abi.encodePacked(r, s, v), keyHash, uint8(0), uint8(0)); return _estimateGas(i); } @@ -289,7 +290,7 @@ contract BaseTest is SoladyTest { internal returns (uint256 gExecute, uint256 gCombined, uint256 gUsed) { - i.signature = abi.encodePacked(keccak256("a"), keccak256("b"), keyHash, uint8(0)); + i.signature = abi.encodePacked(keccak256("a"), keccak256("b"), keyHash, uint8(0), uint8(0)); return _estimateGas(i); }