Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions CNAME
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
legion-rouge.vercel.app
Empty file modified deploy/execute_config.sh
100755 → 100644
Empty file.
Empty file modified deploy/verify_config.sh
100755 → 100644
Empty file.
2 changes: 1 addition & 1 deletion lib/forge-std
Submodule forge-std updated 60 files
+1 −1 .github/CODEOWNERS
+6 −0 .github/dependabot.yml
+61 −31 .github/workflows/ci.yml
+6 −1 .github/workflows/sync.yml
+3 −3 CONTRIBUTING.md
+61 −13 README.md
+1 −1 RELEASE_CHECKLIST.md
+3 −12 foundry.toml
+2 −2 package.json
+2 −12 scripts/vm.py
+2 −2 src/Base.sol
+1 −1 src/Config.sol
+2 −2 src/LibVariable.sol
+2 −2 src/Script.sol
+28 −13 src/StdAssertions.sol
+24 −8 src/StdChains.sol
+13 −17 src/StdCheats.sol
+30 −10 src/StdConfig.sol
+2 −2 src/StdConstants.sol
+2 −2 src/StdError.sol
+22 −4 src/StdInvariant.sol
+13 −14 src/StdJson.sol
+6 −2 src/StdMath.sol
+13 −11 src/StdStorage.sol
+2 −2 src/StdStyle.sol
+10 −18 src/StdToml.sol
+6 −14 src/StdUtils.sol
+2 −4 src/Test.sol
+82 −43 src/Vm.sol
+10 −19 src/console.sol
+2 −2 src/console2.sol
+2 −2 src/interfaces/IERC1155.sol
+2 −2 src/interfaces/IERC165.sol
+2 −2 src/interfaces/IERC20.sol
+9 −9 src/interfaces/IERC4626.sol
+2 −2 src/interfaces/IERC6909.sol
+2 −2 src/interfaces/IERC721.sol
+12 −17 src/interfaces/IERC7540.sol
+9 −9 src/interfaces/IERC7575.sol
+3 −8 src/interfaces/IMulticall3.sol
+691 −1,380 src/safeconsole.sol
+2 −2 test/CommonBase.t.sol
+34 −5 test/Config.t.sol
+19 −1 test/LibVariable.t.sol
+2 −2 test/StdAssertions.t.sol
+24 −24 test/StdChains.t.sol
+19 −20 test/StdCheats.t.sol
+2 −2 test/StdConstants.t.sol
+3 −4 test/StdError.t.sol
+2 −2 test/StdJson.t.sol
+8 −8 test/StdMath.t.sol
+24 −27 test/StdStorage.t.sol
+2 −2 test/StdStyle.t.sol
+2 −2 test/StdToml.t.sol
+13 −13 test/StdUtils.t.sol
+4 −4 test/Vm.t.sol
+2 −4 test/compilation/CompilationScript.sol
+2 −4 test/compilation/CompilationScriptBase.sol
+2 −4 test/compilation/CompilationTest.sol
+2 −4 test/compilation/CompilationTestBase.sol
Empty file modified prep/check-bytecode-changes.js
100755 → 100644
Empty file.
47 changes: 44 additions & 3 deletions src/IthacaAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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))`.
Comment thread
Dargon789 marked this conversation as resolved.
function unwrapAndValidateSignature(bytes32 digest, bytes calldata signature)
public
view
Expand All @@ -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;
Comment thread
Dargon789 marked this conversation as resolved.
}

if (merkle) {
return _verifyMerkleSig(digest, signature);
}

Key memory key = getKey(keyHash);
Expand Down Expand Up @@ -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";
}
}
69 changes: 67 additions & 2 deletions test/Account.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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
Expand Down
11 changes: 6 additions & 5 deletions test/Base.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -282,15 +283,15 @@ 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);
}

function _estimateGasForSecp256r1Key(bytes32 keyHash, Orchestrator.Intent memory i)
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);
}
Expand Down
5 changes: 3 additions & 2 deletions test/Orchestrator.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
3 changes: 2 additions & 1 deletion test/SimulateExecute.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading