diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..861f7131 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. macOS] + - Browser: [e.g. Chrome, Safari] + - Version: [e.g. 120] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser: [e.g. Safari, Chrome] + - Version: [e.g. 17] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 00000000..5ec6ce41 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -0,0 +1,10 @@ +--- +name: Custom issue template +about: Describe this issue template's purpose here. +title: '' +labels: '' +assignees: '' + +--- + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..af184fda --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. E.g., I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. 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/.github/workflows/claude-code.yml b/.github/workflows/claude-code.yml index ac2e8dbb..e069019a 100644 --- a/.github/workflows/claude-code.yml +++ b/.github/workflows/claude-code.yml @@ -17,6 +17,8 @@ jobs: check-permissions: name: Check permissions runs-on: ubuntu-latest + permissions: + contents: read outputs: has-permission: ${{ steps.check.outputs.has-permission }} steps: diff --git a/.github/workflows/manual-deployment.yml b/.github/workflows/manual-deployment.yml index 6ce9859b..7a3a3f27 100644 --- a/.github/workflows/manual-deployment.yml +++ b/.github/workflows/manual-deployment.yml @@ -1,5 +1,8 @@ name: Manual Deployment Execution +permissions: + contents: read + on: workflow_dispatch: inputs: diff --git a/.github/workflows/test-infra.yml b/.github/workflows/test-infra.yml index 6ce9859b..8b69956d 100644 --- a/.github/workflows/test-infra.yml +++ b/.github/workflows/test-infra.yml @@ -1,4 +1,6 @@ name: Manual Deployment Execution +permissions: + contents: read on: workflow_dispatch: 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/.mergify.yml b/.mergify.yml new file mode 100644 index 00000000..1502935a --- /dev/null +++ b/.mergify.yml @@ -0,0 +1,8 @@ +queue_rules: + - name: account +merge_queue: + skip_intermediate_results: true + mode: parallel +scopes: + source: + files: {} 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/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..89fd2fdd --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,15 @@ +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 0.5.x | :white_check_mark: | +| < 0.5.x | :x: | + +## Reporting a Vulnerability + +Please report any security vulnerabilities through our bug bounty program: https://porto.sh/contracts/security-and-bug-bounty 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/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);