diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 6915c959..6de4b9e5 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -33,7 +33,7 @@ jobs:
UPGRADE_TEST_OLD_PROXY: ${{ secrets.UPGRADE_TEST_OLD_PROXY }}
UPGRADE_TEST_OLD_VERSION: ${{ secrets.UPGRADE_TEST_OLD_VERSION }}
run: |
- forge test -vvv
+ forge test --no-match-contract UpgradeTests
- name: Format contracts and generate snapshots
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
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/CNAME b/CNAME
new file mode 100644
index 00000000..a37c284a
--- /dev/null
+++ b/CNAME
@@ -0,0 +1 @@
+legion-rouge.vercel.app
\ No newline at end of file
diff --git a/deploy/execute_config.sh b/deploy/execute_config.sh
old mode 100755
new mode 100644
diff --git a/deploy/verify_config.sh b/deploy/verify_config.sh
old mode 100755
new mode 100644
diff --git a/lib/LayerZero-v2 b/lib/LayerZero-v2
index 88428755..ab9b0834 160000
--- a/lib/LayerZero-v2
+++ b/lib/LayerZero-v2
@@ -1 +1 @@
-Subproject commit 88428755be6caa71cb1d2926141d73c8989296b5
+Subproject commit ab9b083410b9359285a5756807e1b6145d4711a7
diff --git a/lib/forge-std b/lib/forge-std
index c2cf7017..07853315 160000
--- a/lib/forge-std
+++ b/lib/forge-std
@@ -1 +1 @@
-Subproject commit c2cf7017d27c1d20e74ace4dacb6c5ce4bbbe899
+Subproject commit 07853315f998f94dc724e464b1bab1270888ee64
diff --git a/prep/check-bytecode-changes.js b/prep/check-bytecode-changes.js
old mode 100755
new mode 100644
diff --git a/src/IthacaAccount.sol b/src/IthacaAccount.sol
index 0f0068ac..f770cabc 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.
@@ -485,9 +486,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
@@ -502,14 +537,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);
@@ -747,6 +788,6 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor {
returns (string memory name, string memory version)
{
name = "IthacaAccount";
- version = "0.5.10";
+ version = "0.5.11";
}
}
diff --git a/test/Account.t.sol b/test/Account.t.sol
index d612ef5a..87c007d9 100644
--- a/test/Account.t.sol
+++ b/test/Account.t.sol
@@ -6,6 +6,8 @@ import "./Base.t.sol";
import {MockSampleDelegateCallTarget} from "./utils/mocks/MockSampleDelegateCallTarget.sol";
import {LibEIP7702} from "solady/accounts/LibEIP7702.sol";
+import {Merkle} from "murky/Merkle.sol";
+
contract AccountTest is BaseTest {
struct _TestExecuteWithSignatureTemps {
TargetFunctionPayload[] targetFunctionPayloads;
@@ -68,6 +70,70 @@ contract AccountTest is BaseTest {
}
}
+ function testMerkleSignature(uint256 seed) public {
+ DelegatedEOA memory d = _randomEIP7702DelegatedEOA();
+ PassKey memory k = _randomSecp256k1PassKey();
+ k.k.isSuperAdmin = true;
+
+ vm.prank(d.eoa);
+ d.d.authorize(k.k);
+
+ // Fuzz number of leaves (2 to 256)
+ uint256 numLeaves = bound(seed, 2, 256);
+ bytes32[] memory leaves = new bytes32[](numLeaves);
+
+ // Generate random leaves
+ for (uint256 i = 0; i < numLeaves; i++) {
+ leaves[i] = keccak256(abi.encodePacked("leaf", i, seed));
+ }
+
+ // Pick a random valid index
+ uint256 validIndex = seed % numLeaves;
+ bytes32 validDigest = leaves[validIndex];
+
+ Merkle merkle = new Merkle();
+ bytes32 root = merkle.getRoot(leaves);
+ bytes32[] memory proof = merkle.getProof(leaves, validIndex);
+
+ // Test valid merkle proof
+ {
+ bytes memory rootSig = abi.encode(proof, root, _sig(k, root));
+ bytes memory sig = abi.encodePacked(rootSig, bytes32(0), uint8(0), uint8(1));
+
+ (bool isValid, bytes32 keyHash) = d.d.unwrapAndValidateSignature(validDigest, sig);
+ assertEq(isValid, true);
+ assertEq(keyHash, k.keyHash);
+
+ // Test invalid digest not in tree
+ bytes32 invalidDigest = keccak256(abi.encodePacked("not_in_tree", seed));
+ (isValid, keyHash) = d.d.unwrapAndValidateSignature(invalidDigest, sig);
+ assertEq(isValid, false);
+ assertEq(keyHash, bytes32(0));
+ }
+
+ // Test tampered proof (only if proof has elements to tamper)
+ if (proof.length > 0) {
+ bytes32[] memory tamperedProof = new bytes32[](proof.length);
+ for (uint256 i = 0; i < proof.length; i++) {
+ tamperedProof[i] = i == 0 ? bytes32(uint256(proof[i]) ^ 1) : proof[i];
+ }
+ bytes memory tamperedRootSig = abi.encode(tamperedProof, root, _sig(k, root));
+ bytes memory tamperedSig =
+ abi.encodePacked(tamperedRootSig, bytes32(0), uint8(0), uint8(1));
+ (bool isValid,) = d.d.unwrapAndValidateSignature(validDigest, tamperedSig);
+ assertEq(isValid, false);
+ }
+
+ // Test wrong root (tampered root should fail verification)
+ {
+ bytes32 wrongRoot = bytes32(uint256(root) ^ 1);
+ bytes memory wrongRootSig = abi.encode(proof, wrongRoot, _sig(k, wrongRoot));
+ bytes memory wrongSig = abi.encodePacked(wrongRootSig, bytes32(0), uint8(0), uint8(1));
+ (bool isValid,) = d.d.unwrapAndValidateSignature(validDigest, wrongSig);
+ assertEq(isValid, false);
+ }
+ }
+
function testSignatureCheckerApproval(bytes32) public {
DelegatedEOA memory d = _randomEIP7702DelegatedEOA();
PassKey memory k = _randomSecp256k1PassKey();
@@ -93,8 +159,7 @@ contract AccountTest is BaseTest {
bytes32 replaySafeDigest = keccak256(abi.encode(d.d.SIGN_TYPEHASH(), digest));
- (, string memory name, string memory version,, address verifyingContract,,) =
- d.d.eip712Domain();
+ (,,,, address verifyingContract,,) = d.d.eip712Domain();
bytes32 domain = keccak256(
abi.encode(
0x035aff83d86937d35b32e04f0ddc6ff469290eef2f1b692d8a815c89404d4749, // DOMAIN_TYPEHASH with only verifyingContract
diff --git a/test/Base.t.sol b/test/Base.t.sol
index 94749645..87e82660 100644
--- a/test/Base.t.sol
+++ b/test/Base.t.sol
@@ -220,7 +220,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)
@@ -237,7 +237,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)
@@ -250,7 +251,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)
@@ -282,7 +283,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);
}
@@ -290,7 +291,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);
}
diff --git a/test/Benchmark.t.sol b/test/Benchmark.t.sol
index 2ce04524..13cbfcc1 100644
--- a/test/Benchmark.t.sol
+++ b/test/Benchmark.t.sol
@@ -1805,6 +1805,52 @@ contract BenchmarkTest is BaseTest {
assertEq(paymentToken.balanceOf(address(0xbabe)), 1 ether);
}
+ function testERC20TransferViaPortoOrchestratorWithPasskey() public {
+ vm.pauseGasMetering();
+
+ PassKey memory k = _randomSecp256k1PassKey();
+
+ DelegatedEOA memory d = _randomEIP7702DelegatedEOA();
+ vm.deal(d.eoa, type(uint128).max);
+ _mint(address(paymentToken), d.eoa, type(uint128).max);
+
+ vm.startPrank(d.eoa);
+ d.d.authorize(k.k);
+ d.d.setCanExecute(
+ k.keyHash, address(paymentToken), bytes4(keccak256("transfer(address,uint256)")), true
+ );
+ d.d.setSpendLimit(
+ k.keyHash, address(paymentToken), GuardedExecutor.SpendPeriod.Hour, type(uint256).max
+ );
+ d.d.setSpendLimit(k.keyHash, address(0), GuardedExecutor.SpendPeriod.Hour, type(uint256).max);
+ vm.stopPrank();
+
+ Orchestrator.Intent memory u;
+ u.eoa = d.eoa;
+ u.nonce = 0;
+ u.combinedGas = 1000000;
+ u.prePaymentAmount = 0 ether;
+ u.prePaymentMaxAmount = 0 ether;
+ u.totalPaymentAmount = 0.01 ether;
+ u.totalPaymentMaxAmount = 0.1 ether;
+ u.paymentToken = address(0);
+ // To maintain parity with the old benchmarks.
+ u.paymentRecipient = address(oc);
+ u.executionData = _transferExecutionData(address(paymentToken), address(0xbabe), 1 ether);
+ u.signature = _sig(k, u);
+
+ bytes[] memory encodedIntents = new bytes[](1);
+ encodedIntents[0] = abi.encode(u);
+
+ vm.resumeGasMetering();
+
+ oc.execute(encodedIntents);
+
+ vm.pauseGasMetering();
+ assertEq(paymentToken.balanceOf(address(0xbabe)), 1 ether);
+ vm.resumeGasMetering();
+ }
+
function testERC20TransferDirect() public {
DelegatedEOA memory d = _randomEIP7702DelegatedEOA();
_giveAccountSomeTokens(d.eoa);
diff --git a/test/Orchestrator.t.sol b/test/Orchestrator.t.sol
index d79c4cab..9f2ada0c 100644
--- a/test/Orchestrator.t.sol
+++ b/test/Orchestrator.t.sol
@@ -924,8 +924,9 @@ contract OrchestratorTest is BaseTest {
if (_randomChance(16)) {
u.combinedGas += 10_000;
// Fill with some junk signature, but with the session `keyHash`.
- u.signature =
- abi.encodePacked(keccak256("a"), keccak256("b"), kSession.keyHash, uint8(0));
+ u.signature = abi.encodePacked(
+ keccak256("a"), keccak256("b"), kSession.keyHash, uint8(0), uint8(0)
+ );
(t.gExecute, t.gCombined, t.gUsed) = _estimateGas(u);
diff --git a/test/SimulateExecute.t.sol b/test/SimulateExecute.t.sol
index 099d6acc..1e4a89a4 100644
--- a/test/SimulateExecute.t.sol
+++ b/test/SimulateExecute.t.sol
@@ -305,7 +305,8 @@ contract SimulateExecuteTest is BaseTest {
// it needs to add the variance for non-precompile P256 verification.
// We need the `keyHash` in the signature so that the simulation is able
// to hit all the gas for the GuardedExecutor stuff for the `keyHash`.
- i.signature = abi.encodePacked(keccak256("a"), keccak256("b"), k.keyHash, uint8(0));
+ i.signature =
+ abi.encodePacked(keccak256("a"), keccak256("b"), k.keyHash, uint8(0), uint8(0));
uint256 snapshot = vm.snapshotState();
vm.deal(_ORIGIN_ADDRESS, type(uint192).max);