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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ xmlns:android
+
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:name
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ name
+
+ ^$
+
+
+
+
+
+
+
+
+ style
+
+ ^$
+
+
+
+
+
+
+
+
+ .*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+
+ http://schemas.android.com/apk/res/android
+
+
+ ANDROID_ATTRIBUTE_ORDER
+
+
+
+
+
+
+ .*
+
+ .*
+
+
+ BY_NAME
+
+
+
+
+
+
+
\ 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);
}