From f4c7993967161ef4b6e708a5f640c00aecb50830 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 5 Aug 2025 22:58:33 +0000 Subject: [PATCH 01/55] feat: add ERC20 transfer benchmark for Porto with passkey MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add testERC20TransferViaPortoOrchestratorWithPasskey() benchmark to isolate passkey authentication costs from spend limit enforcement costs. - Uses secp256k1 passkey for transaction signing - Sets execution permissions for ERC20 transfers - Requires spend limits (set to max) for passkey operations - Gas cost: 116,094 (vs 97,030 without passkey, 117,083 with restrictive limits) - Provides clean measurement of passkey overhead (~19k gas) Resolves #272 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Tanishk Goyal --- test/Benchmark.t.sol | 46 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/test/Benchmark.t.sol b/test/Benchmark.t.sol index c169de27..dd430a5d 100644 --- a/test/Benchmark.t.sol +++ b/test/Benchmark.t.sol @@ -315,6 +315,52 @@ contract BenchmarkTest is BaseTest { vm.resumeGasMetering(); } + 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 { vm.pauseGasMetering(); From ab8f1a4dd24aa2de4fe5c434c9a4b356aecdd189 Mon Sep 17 00:00:00 2001 From: o-az Date: Tue, 16 Sep 2025 09:58:16 -0700 Subject: [PATCH 02/55] chore: use `auto-assign-pr.yml` org action --- .github/workflows/auto-assign.yaml | 37 +----------------------------- 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/.github/workflows/auto-assign.yaml b/.github/workflows/auto-assign.yaml index f256bdc4..0ecd2e5e 100644 --- a/.github/workflows/auto-assign.yaml +++ b/.github/workflows/auto-assign.yaml @@ -9,39 +9,4 @@ jobs: permissions: issues: write pull-requests: write - runs-on: ubuntu-latest - steps: - - name: Auto assign PR to author - if: > - github.event.pull_request.user.type != 'Bot' && - ( - github.event.pull_request.author_association == 'MEMBER' || - github.event.pull_request.author_association == 'OWNER' || - github.event.pull_request.author_association == 'COLLABORATOR' - ) - uses: actions/github-script@v7 - with: - script: | - const pr = context.payload.pull_request - const prAuthor = pr.user.login - - console.log(`PR #${pr.number} author: ${prAuthor}`) - console.log(`Current assignees: ${pr.assignees.map(a => a.login).join(', ') || 'none'}`) - - if (pr.assignees && pr.assignees.length > 0) { - console.log('PR already has assignees, skipping') - return - } - - try { - await github.rest.issues.addAssignees({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - assignees: [prAuthor] - }) - console.log(`Successfully assigned PR #${pr.number} to ${prAuthor}`) - } catch (error) { - console.error(`Failed to assign PR: ${error.message}`) - throw error - } + uses: ithacaxyz/ci/.github/workflows/auto-assign-pr.yml@main From 2ecce316b8bad62c40f31a5ceeb500726d926d1c Mon Sep 17 00:00:00 2001 From: Tanishk Goyal Date: Wed, 24 Sep 2025 22:22:07 +0530 Subject: [PATCH 03/55] feat: add merkle sigs natively into the account --- snapshots/BenchmarkTest.json | 30 +++++++++--------- src/IthacaAccount.sol | 59 ++++++++++++++++++++++++++++++++++-- 2 files changed, 72 insertions(+), 17 deletions(-) diff --git a/snapshots/BenchmarkTest.json b/snapshots/BenchmarkTest.json index 2d5a899d..b32e01a9 100644 --- a/snapshots/BenchmarkTest.json +++ b/snapshots/BenchmarkTest.json @@ -16,11 +16,11 @@ "testERC20Transfer_ERC4337MinimalAccount": "171509", "testERC20Transfer_ERC4337MinimalAccount_AppSponsor": "168500", "testERC20Transfer_ERC4337MinimalAccount_ERC20SelfPay": "199831", - "testERC20Transfer_IthacaAccount": "128059", - "testERC20Transfer_IthacaAccountWithSpendLimits": "193602", - "testERC20Transfer_IthacaAccount_AppSponsor": "138595", - "testERC20Transfer_IthacaAccount_AppSponsor_ERC20": "143884", - "testERC20Transfer_IthacaAccount_ERC20SelfPay": "128548", + "testERC20Transfer_IthacaAccount": "128100", + "testERC20Transfer_IthacaAccountWithSpendLimits": "193589", + "testERC20Transfer_IthacaAccount_AppSponsor": "138636", + "testERC20Transfer_IthacaAccount_AppSponsor_ERC20": "143937", + "testERC20Transfer_IthacaAccount_ERC20SelfPay": "128589", "testERC20Transfer_Safe4337": "197561", "testERC20Transfer_Safe4337_AppSponsor": "191725", "testERC20Transfer_Safe4337_ERC20SelfPay": "221464", @@ -29,25 +29,25 @@ "testERC20Transfer_ZerodevKernel_ERC20SelfPay": "235489", "testERC20Transfer_batch100_AlchemyModularAccount": "10109066", "testERC20Transfer_batch100_AlchemyModularAccount_ERC20SelfPay": "11609298", - "testERC20Transfer_batch100_IthacaAccount": "7531912", - "testERC20Transfer_batch100_IthacaAccount_AppSponsor": "8140216", - "testERC20Transfer_batch100_IthacaAccount_AppSponsor_ERC20": "7966120", - "testERC20Transfer_batch100_IthacaAccount_ERC20SelfPay": "7353064", + "testERC20Transfer_batch100_IthacaAccount": "7535892", + "testERC20Transfer_batch100_IthacaAccount_AppSponsor": "8144196", + "testERC20Transfer_batch100_IthacaAccount_AppSponsor_ERC20": "7970208", + "testERC20Transfer_batch100_IthacaAccount_ERC20SelfPay": "7357152", "testERC20Transfer_batch100_ZerodevKernel": "12631318", "testERC20Transfer_batch100_ZerodevKernel_ERC20SelfPay": "14149937", "testNativeTransfer_AlchemyModularAccount": "180829", "testNativeTransfer_CoinbaseSmartWallet": "178916", - "testNativeTransfer_IthacaAccount": "129415", - "testNativeTransfer_IthacaAccount_AppSponsor": "139982", - "testNativeTransfer_IthacaAccount_ERC20SelfPay": "137204", + "testNativeTransfer_IthacaAccount": "129456", + "testNativeTransfer_IthacaAccount_AppSponsor": "140023", + "testNativeTransfer_IthacaAccount_ERC20SelfPay": "137245", "testNativeTransfer_Safe4337": "198595", "testNativeTransfer_ZerodevKernel": "208635", "testUniswapV2Swap_AlchemyModularAccount": "238647", "testUniswapV2Swap_CoinbaseSmartWallet": "237451", "testUniswapV2Swap_ERC4337MinimalAccount": "230691", - "testUniswapV2Swap_IthacaAccount": "187179", - "testUniswapV2Swap_IthacaAccount_AppSponsor": "197691", - "testUniswapV2Swap_IthacaAccount_ERC20SelfPay": "192480", + "testUniswapV2Swap_IthacaAccount": "187244", + "testUniswapV2Swap_IthacaAccount_AppSponsor": "197744", + "testUniswapV2Swap_IthacaAccount_ERC20SelfPay": "192533", "testUniswapV2Swap_Safe4337": "257333", "testUniswapV2Swap_ZerodevKernel": "266367" } \ No newline at end of file diff --git a/src/IthacaAccount.sol b/src/IthacaAccount.sol index 45b2d6c8..cbe96ff3 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,52 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { return isMultichain ? _hashTypedDataSansChainId(structHash) : _hashTypedData(structHash); } + /// @dev Verifies the merkle sig for the multi chain intents. + /// - Note: Each leaf of the merkle tree should be a standard intent digest, computed with chainId. + /// - Leaf intents do NOT need to have the multichain nonce prefix. + /// - The signature for multi chain intents 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) + } + + console.logBytes32(root); + + console.log("Proof length: ", proof.length); + console.logBytes32(proof[0]); + + if (MerkleProofLib.verifyCalldata(proof, root, digest)) { + console.log("Entered here"); + (isValid, keyHash) = unwrapAndValidateSignature(root, rootSig); + + console.logBytes32(keyHash); + console.log(isValid); + 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 +551,25 @@ 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) { + console.log("prehash"); digest = EfficientHashLib.sha2(digest); // `sha256(abi.encode(digest))`. } + merkle = uint256(LibBytes.loadCalldata(signature, n + 2)) & 0xff != 0; + } + + console.logBytes(signature); + + console.log("Reached here", merkle); + if (merkle) { + console.log("merkle"); + return _verifyMerkleSig(digest, signature); } Key memory key = getKey(keyHash); From 557774798309a3b79c070634f20d96b2a465c52b Mon Sep 17 00:00:00 2001 From: Tanishk Goyal Date: Wed, 24 Sep 2025 22:23:47 +0530 Subject: [PATCH 04/55] fix: tests --- src/IthacaAccount.sol | 20 +++---------------- test/Account.t.sol | 40 ++++++++++++++++++++++++++++++++++++-- test/Base.t.sol | 11 ++++++----- test/Orchestrator.t.sol | 5 +++-- test/SimulateExecute.t.sol | 3 ++- 5 files changed, 52 insertions(+), 27 deletions(-) diff --git a/src/IthacaAccount.sol b/src/IthacaAccount.sol index cbe96ff3..1dc7f18b 100644 --- a/src/IthacaAccount.sol +++ b/src/IthacaAccount.sol @@ -491,10 +491,9 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { return isMultichain ? _hashTypedDataSansChainId(structHash) : _hashTypedData(structHash); } - /// @dev Verifies the merkle sig for the multi chain intents. - /// - Note: Each leaf of the merkle tree should be a standard intent digest, computed with chainId. - /// - Leaf intents do NOT need to have the multichain nonce prefix. - /// - The signature for multi chain intents using merkle verification is encoded as: + /// @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 @@ -517,17 +516,9 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { rootSig.offset := add(rootSigOffset, 0x20) } - console.logBytes32(root); - - console.log("Proof length: ", proof.length); - console.logBytes32(proof[0]); - if (MerkleProofLib.verifyCalldata(proof, root, digest)) { - console.log("Entered here"); (isValid, keyHash) = unwrapAndValidateSignature(root, rootSig); - console.logBytes32(keyHash); - console.log(isValid); return (isValid, keyHash); } @@ -558,17 +549,12 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { signature = LibBytes.truncatedCalldata(signature, n); // Do the prehash if last byte is non-zero. if (uint256(LibBytes.loadCalldata(signature, n + 1)) & 0xff != 0) { - console.log("prehash"); digest = EfficientHashLib.sha2(digest); // `sha256(abi.encode(digest))`. } merkle = uint256(LibBytes.loadCalldata(signature, n + 2)) & 0xff != 0; } - console.logBytes(signature); - - console.log("Reached here", merkle); if (merkle) { - console.log("merkle"); return _verifyMerkleSig(digest, signature); } diff --git a/test/Account.t.sol b/test/Account.t.sol index e4b8310b..67e3fe50 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,41 @@ contract AccountTest is BaseTest { } } + function testMerkleSignature(bytes32 randomDigest) public { + DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); + PassKey memory k = _randomSecp256k1PassKey(); + + k.k.isSuperAdmin = true; + + vm.prank(d.eoa); + d.d.authorize(k.k); + + bytes32 actualDigest = keccak256("actualDigest"); + + Merkle merkle = new Merkle(); + + bytes32[] memory leaves = new bytes32[](2); + leaves[0] = actualDigest; + leaves[1] = randomDigest; + bytes32 root = merkle.getRoot(leaves); + + bytes32[] memory proof = merkle.getProof(leaves, 0); + + 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(actualDigest, sig); + + assertEq(isValid, true); + assertEq(keyHash, k.keyHash); + + // Test some random digest + bytes32 randomDigest2 = keccak256("randomDigest2"); + (isValid, keyHash) = d.d.unwrapAndValidateSignature(randomDigest2, sig); + assertEq(isValid, false); + assertEq(keyHash, bytes32(0)); + } + function testSignatureCheckerApproval(bytes32) public { DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); PassKey memory k = _randomSecp256k1PassKey(); @@ -93,8 +130,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 1765a651..f9f91ac4 100644 --- a/test/Base.t.sol +++ b/test/Base.t.sol @@ -215,7 +215,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) @@ -232,7 +232,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) @@ -245,7 +246,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) @@ -277,7 +278,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); } @@ -285,7 +286,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 c36a3248..c5cc3cc1 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 681266f7..aa3261cd 100644 --- a/test/SimulateExecute.t.sol +++ b/test/SimulateExecute.t.sol @@ -300,7 +300,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); From 995a3e17a01c2d9e12bcdbc9384cfdd4f00b1d4c Mon Sep 17 00:00:00 2001 From: Tanishk Goyal Date: Wed, 24 Sep 2025 22:29:40 +0530 Subject: [PATCH 05/55] test: add more sophisticated fuzz test --- test/Account.t.sol | 69 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 20 deletions(-) diff --git a/test/Account.t.sol b/test/Account.t.sol index 67e3fe50..21c7c36e 100644 --- a/test/Account.t.sol +++ b/test/Account.t.sol @@ -70,39 +70,68 @@ contract AccountTest is BaseTest { } } - function testMerkleSignature(bytes32 randomDigest) public { + 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); - bytes32 actualDigest = keccak256("actualDigest"); - - Merkle merkle = new Merkle(); + // Fuzz number of leaves (2 to 256) + uint256 numLeaves = bound(seed, 2, 256); + bytes32[] memory leaves = new bytes32[](numLeaves); - bytes32[] memory leaves = new bytes32[](2); - leaves[0] = actualDigest; - leaves[1] = randomDigest; - bytes32 root = merkle.getRoot(leaves); + // Generate random leaves + for (uint256 i = 0; i < numLeaves; i++) { + leaves[i] = keccak256(abi.encodePacked("leaf", i, seed)); + } - bytes32[] memory proof = merkle.getProof(leaves, 0); + // Pick a random valid index + uint256 validIndex = seed % numLeaves; + bytes32 validDigest = leaves[validIndex]; - bytes memory rootSig = abi.encode(proof, root, _sig(k, root)); - bytes memory sig = abi.encodePacked(rootSig, bytes32(0), uint8(0), uint8(1)); + Merkle merkle = new Merkle(); + bytes32 root = merkle.getRoot(leaves); + bytes32[] memory proof = merkle.getProof(leaves, validIndex); - (bool isValid, bytes32 keyHash) = d.d.unwrapAndValidateSignature(actualDigest, sig); + // 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)); + } - assertEq(isValid, true); - assertEq(keyHash, k.keyHash); + // 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 some random digest - bytes32 randomDigest2 = keccak256("randomDigest2"); - (isValid, keyHash) = d.d.unwrapAndValidateSignature(randomDigest2, sig); - assertEq(isValid, false); - assertEq(keyHash, bytes32(0)); + // 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 { From 4918a12d97acfad5294fbbc151c7fa12c8c13b26 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 24 Sep 2025 17:05:28 +0000 Subject: [PATCH 06/55] chore: bump contract versions due to bytecode changes - Contracts updated: IthacaAccount --- src/IthacaAccount.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/IthacaAccount.sol b/src/IthacaAccount.sol index 1dc7f18b..a8ffdbca 100644 --- a/src/IthacaAccount.sol +++ b/src/IthacaAccount.sol @@ -796,6 +796,6 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { returns (string memory name, string memory version) { name = "IthacaAccount"; - version = "0.5.10"; + version = "0.5.11"; } } From 68f4e3a3415ac4fe31324ea7687043894ea69e9f Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Wed, 19 Nov 2025 20:04:56 +0700 Subject: [PATCH 07/55] Add .circleci/config.yml (#1) Add CircleCI configuration file to set up a basic pipeline with a say-hello job and workflow CI: Add .circleci/config.yml to define a say-hello job that checks out the code and prints a greeting using the cimg/base Docker image Add a workflow to orchestrate the say-hello job under the CircleCI 2.1 engine --- .circleci/config.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..bb68b14a --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,31 @@ +# Use the latest 2.1 version of CircleCI pipeline process engine. +# See: https://circleci.com/docs/reference/configuration-reference +version: 2.1 + +# Define a job to be invoked later in a workflow. +# See: https://circleci.com/docs/guides/orchestrate/jobs-steps/#jobs-overview & https://circleci.com/docs/reference/configuration-reference/#jobs +jobs: + say-hello: + # Specify the execution environment. You can specify an image from Docker Hub or use one of our convenience images from CircleCI's Developer Hub. + # See: https://circleci.com/docs/guides/execution-managed/executor-intro/ & https://circleci.com/docs/reference/configuration-reference/#executor-job + docker: + # Specify the version you desire here + # See: https://circleci.com/developer/images/image/cimg/base + - image: cimg/base:current + + # Add steps to the job + # See: https://circleci.com/docs/guides/orchestrate/jobs-steps/#steps-overview & https://circleci.com/docs/reference/configuration-reference/#steps + steps: + # Checkout the code as the first step. + - checkout + - run: + name: "Say hello" + command: "echo Hello, World!" + +# Orchestrate jobs using workflows +# See: https://circleci.com/docs/guides/orchestrate/workflows/ & https://circleci.com/docs/reference/configuration-reference/#workflows +workflows: + say-hello-workflow: # This is the name of the workflow, feel free to change it to better match your workflow. + # Inside the workflow, you define the jobs you want to run. + jobs: + - say-hello \ No newline at end of file From 2f484ce9c031409dbed91c1f614af40e9c459385 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Wed, 19 Nov 2025 20:41:44 +0700 Subject: [PATCH 08/55] Update ci.yaml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6915c959..4f705020 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 --rerun -vvvvv - name: Format contracts and generate snapshots if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository From cef256e2a32296ea45b3fc0484c3ccdd70ac35ca Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Wed, 19 Nov 2025 20:54:18 +0700 Subject: [PATCH 09/55] Update ci.yaml (#2) CI: Update Forge test command to use --rerun and increase verbosity to -vvvvv Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6915c959..4f705020 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 --rerun -vvvvv - name: Format contracts and generate snapshots if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository From bcd85272adebd7a41c04fb3a1453dcfbc60b45a6 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Thu, 20 Nov 2025 02:23:30 +0700 Subject: [PATCH 10/55] Update ci.yaml (#4) Enhance the CI test step to automatically rerun failed tests and increase verbosity in Forge. CI: Enable the --rerun flag for Forge tests to retry failures automatically Increase Forge test verbosity from -vvv to -vvvvv Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> From 01aafc57598cdcf2dc25569ba24cd0b150d95c20 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Thu, 20 Nov 2025 13:47:48 +0700 Subject: [PATCH 11/55] Update ci.yaml (#5) Change Forge test invocation from "forge test --rerun -vvvvv" to "forge test -vvv" Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4f705020..6915c959 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 --rerun -vvvvv + forge test -vvv - name: Format contracts and generate snapshots if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository From 21aa94248bd541d55366d5164d97ec61780f8f0b Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Thu, 20 Nov 2025 13:56:48 +0700 Subject: [PATCH 12/55] Update ci.yaml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From a317ddb944a1f5ed7294092790902da6b255f73f Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Sat, 20 Dec 2025 20:50:05 +0700 Subject: [PATCH 13/55] Create CNAME --- CNAME | 1 + 1 file changed, 1 insertion(+) create mode 100644 CNAME 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 From 28ff37f5bacd9d0a4a56db643d72d0e33cf13c87 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Sun, 21 Dec 2025 20:37:13 +0700 Subject: [PATCH 14/55] Revert "Merge branch 'master'" This reverts commit 6c02fbff6486bb206c08af83d948830ace042353, reversing changes made to a317ddb944a1f5ed7294092790902da6b255f73f. --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4f705020..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 --rerun -vvvvv + 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 From 8a9cd8144fb5581326ab2ac354f25caa09adf69c Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Tue, 6 Jan 2026 01:31:07 +0700 Subject: [PATCH 15/55] Update ci.yaml (#10) reback use test CI and automation chores (ithacaxyz#394) https://github.com/Dargon789/account/commit/7dd8a5d91c162b89316e367f0fb159f47abfeab0 Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6de4b9e5..6915c959 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 --no-match-contract UpgradeTests + forge test -vvv - name: Format contracts and generate snapshots if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository From e8df74714a03ecaea93656c1a8a2d1e05c2ebe7c Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Thu, 22 Jan 2026 16:04:21 +0700 Subject: [PATCH 16/55] Potential fix for code scanning alert no. 3: Workflow does not contain permissions (#15) https://github.com/Dargon789/account/security/code-scanning/3 Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/test-infra.yml | 2 ++ 1 file changed, 2 insertions(+) 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: From 966955bb84affdc64bf82f0c891f0b25c1a9d770 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Thu, 22 Jan 2026 16:15:33 +0700 Subject: [PATCH 17/55] Update ci.yaml (#16) reback use test CI and automation chores (ithacaxyz#394) https://github.com/Dargon789/account/commit/7dd8a5d91c162b89316e367f0fb159f47abfeab0 Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> From acf68a660779fe2e646d869252b63e86001f1972 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Tue, 27 Jan 2026 18:05:10 +0700 Subject: [PATCH 18/55] Delete CNAME --- CNAME | 1 - 1 file changed, 1 deletion(-) delete mode 100644 CNAME diff --git a/CNAME b/CNAME deleted file mode 100644 index a37c284a..00000000 --- a/CNAME +++ /dev/null @@ -1 +0,0 @@ -legion-rouge.vercel.app \ No newline at end of file From ab82267f7768a825595afb454a3ab688167b9ce2 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Wed, 28 Jan 2026 22:16:33 +0700 Subject: [PATCH 19/55] Update Brutalizer.sol --- test/utils/Brutalizer.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/utils/Brutalizer.sol b/test/utils/Brutalizer.sol index 1c6447ae..969c5f86 100644 --- a/test/utils/Brutalizer.sol +++ b/test/utils/Brutalizer.sol @@ -825,7 +825,9 @@ contract Brutalizer { let remainder := and(length, 0x1f) if remainder { if shl(mul(8, remainder), lastWord) { notZeroRightPadded := 1 } } // Check if the memory allocated is sufficient. - if length { if gt(add(add(s, 0x20), length), mload(0x40)) { insufficientMalloc := 1 } } + if length { + if gt(add(add(s, 0x20), length), mload(0x40)) { insufficientMalloc := 1 } + } } if (notZeroRightPadded) revert("Not zero right padded!"); if (insufficientMalloc) revert("Insufficient memory allocation!"); From 585f8e002ce2b82c67d397207c6e290b262d11df Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Wed, 28 Jan 2026 22:24:22 +0700 Subject: [PATCH 20/55] Potential fix for code scanning alert no. 2: Workflow does not contain permissions (#17) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/manual-deployment.yml | 3 +++ 1 file changed, 3 insertions(+) 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: From 5fc1664818d82e93dfc0c542f0ba9cce8619da0e Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Wed, 28 Jan 2026 23:00:03 +0700 Subject: [PATCH 21/55] Refactor deployment scripts and update configs Migrates deployment scripts to use fork-based configuration loading, removes legacy CircleCI and GitHub workflow files, and introduces a registry-based contract deployment record. Updates .env.example, README, and foundry configuration. Removes LayerZero vendor and test files, adds devtools submodule, and improves LayerZeroSettler configuration logic for multi-chain deployments. --- .changeset/README.md | 8 + .changeset/config.json | 11 + .circleci/config.yml | 31 - .env.example | 33 +- .github/workflows/auto-assign.yaml | 12 - .github/workflows/ci.yaml | 31 +- .github/workflows/version-check.yaml | 31 +- .gitmodules | 3 + CHANGELOG.md | 19 - README.md | 33 +- deploy/ConfigureLayerZeroSettler.s.sol | 207 ++---- deploy/DeployMain.s.sol | 550 ++++++++------- deploy/FundSigners.s.sol | 118 ++-- deploy/README.md | 323 +++------ deploy/config.toml | 229 +++--- deploy/execute_config.sh | 486 ------------- ...0000000000000000000000000000000000001.json | 1 + ...0000000000000000000000000000000000001.json | 10 + deploy/verify_config.sh | 553 --------------- docs/4337CallGraph.md | 32 - docs/IthacaCallGraph.md | 25 - docs/benchmarks.jpeg | Bin 291106 -> 0 bytes foundry.lock | 20 +- foundry.toml | 14 +- gas-snapshots/.gas-snapshot-main | 11 + src/interfaces/IMulticall3.sol | 38 - src/vendor/layerzero/interfaces/IOAppCore.sol | 54 -- .../interfaces/IOAppMsgInspector.sol | 25 - .../layerzero/interfaces/IOAppReceiver.sol | 27 - src/vendor/layerzero/mocks/EndpointV2Mock.sol | 418 ----------- src/vendor/layerzero/oapp/OApp.sol | 38 - src/vendor/layerzero/oapp/OAppCore.sol | 104 --- src/vendor/layerzero/oapp/OAppReceiver.sol | 143 ---- src/vendor/layerzero/oapp/OAppSender.sol | 136 ---- test/MultiSigSigner.t.sol | 656 ------------------ test/UpgradeTests.t.sol | 566 --------------- test/utils/interfaces/ISafe.sol | 57 -- .../mocks/MockPayerWithSignatureOptimized.sol | 84 --- 38 files changed, 752 insertions(+), 4385 deletions(-) create mode 100644 .changeset/README.md create mode 100644 .changeset/config.json delete mode 100644 .circleci/config.yml delete mode 100644 .github/workflows/auto-assign.yaml delete mode 100755 deploy/execute_config.sh create mode 100644 deploy/registry/deployment_11155420_0x0000000000000000000000000000000000000000000000000000000000000001.json create mode 100644 deploy/registry/deployment_84532_0x0000000000000000000000000000000000000000000000000000000000000001.json delete mode 100755 deploy/verify_config.sh delete mode 100644 docs/4337CallGraph.md delete mode 100644 docs/IthacaCallGraph.md delete mode 100644 docs/benchmarks.jpeg create mode 100644 gas-snapshots/.gas-snapshot-main delete mode 100644 src/interfaces/IMulticall3.sol delete mode 100644 src/vendor/layerzero/interfaces/IOAppCore.sol delete mode 100644 src/vendor/layerzero/interfaces/IOAppMsgInspector.sol delete mode 100644 src/vendor/layerzero/interfaces/IOAppReceiver.sol delete mode 100644 src/vendor/layerzero/mocks/EndpointV2Mock.sol delete mode 100644 src/vendor/layerzero/oapp/OApp.sol delete mode 100644 src/vendor/layerzero/oapp/OAppCore.sol delete mode 100644 src/vendor/layerzero/oapp/OAppReceiver.sol delete mode 100644 src/vendor/layerzero/oapp/OAppSender.sol delete mode 100644 test/MultiSigSigner.t.sol delete mode 100644 test/UpgradeTests.t.sol delete mode 100644 test/utils/interfaces/ISafe.sol delete mode 100644 test/utils/mocks/MockPayerWithSignatureOptimized.sol diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 00000000..e5b6d8d6 --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 00000000..d88011f6 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "restricted", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index bb68b14a..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,31 +0,0 @@ -# Use the latest 2.1 version of CircleCI pipeline process engine. -# See: https://circleci.com/docs/reference/configuration-reference -version: 2.1 - -# Define a job to be invoked later in a workflow. -# See: https://circleci.com/docs/guides/orchestrate/jobs-steps/#jobs-overview & https://circleci.com/docs/reference/configuration-reference/#jobs -jobs: - say-hello: - # Specify the execution environment. You can specify an image from Docker Hub or use one of our convenience images from CircleCI's Developer Hub. - # See: https://circleci.com/docs/guides/execution-managed/executor-intro/ & https://circleci.com/docs/reference/configuration-reference/#executor-job - docker: - # Specify the version you desire here - # See: https://circleci.com/developer/images/image/cimg/base - - image: cimg/base:current - - # Add steps to the job - # See: https://circleci.com/docs/guides/orchestrate/jobs-steps/#steps-overview & https://circleci.com/docs/reference/configuration-reference/#steps - steps: - # Checkout the code as the first step. - - checkout - - run: - name: "Say hello" - command: "echo Hello, World!" - -# Orchestrate jobs using workflows -# See: https://circleci.com/docs/guides/orchestrate/workflows/ & https://circleci.com/docs/reference/configuration-reference/#workflows -workflows: - say-hello-workflow: # This is the name of the workflow, feel free to change it to better match your workflow. - # Inside the workflow, you define the jobs you want to run. - jobs: - - say-hello \ No newline at end of file diff --git a/.env.example b/.env.example index 7e85472c..1d880964 100644 --- a/.env.example +++ b/.env.example @@ -2,28 +2,41 @@ # Copy this file to .env and fill in with actual values # ============================================ -# UPGRADE TESTS +# PRIVATE KEY (Required for deployment) # ============================================ -UPGRADE_TEST_RPC_URL=https://base-mainnet.g.alchemy.com/v2/YOUR_API_KEY # Base -UPGRADE_TEST_OLD_PROXY=0x7C27e3AEcbF42879B64d76f604dC3430F4886462 -UPGRADE_TEST_OLD_VERSION=0.5.10 +# NEVER commit your actual private key! +PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 # ============================================ -# DEPLOYMENT SCRIPTS +# RPC ENDPOINTS (Required) # ============================================ -PRIVATE_KEY= - # Format: RPC_{chainId} # Mainnet chains RPC_1=https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY # Ethereum +RPC_42161=https://arb-mainnet.g.alchemy.com/v2/YOUR_API_KEY # Arbitrum +RPC_8453=https://base-mainnet.g.alchemy.com/v2/YOUR_API_KEY # Base # Testnet chains RPC_11155111=https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY # Sepolia -# Test mnemonic for funding script -GAS_SIGNER_MNEMONIC="dash between kangaroo vacant gaze glass way sudden retire output retire evil" - +# Local development +RPC_28405=https://porto-dev-paros.rpc.ithaca.xyz # Porto Devnet +RPC_28406=https://porto-dev-tinos.rpc.ithaca.xyz # Porto Devnet +RPC_28407=https://porto-dev-leros.rpc.ithaca.xyz # Porto Devnet +# ============================================ +# VERIFICATION API KEYS (Optional) +# ============================================ +# Format: VERIFICATION_KEY_{chainId} +# Get API keys from respective block explorers +VERIFICATION_KEY_1=YOUR_ETHERSCAN_API_KEY # https://etherscan.io +VERIFICATION_KEY_42161=YOUR_ARBISCAN_API_KEY # https://arbiscan.io +VERIFICATION_KEY_8453=YOUR_BASESCAN_API_KEY # https://basescan.org +# ============================================ +# CONFIGURATION +# ============================================ +# All deployment configuration is in deploy/deploy-config.json +# See deploy/README.md for detailed instructions \ No newline at end of file diff --git a/.github/workflows/auto-assign.yaml b/.github/workflows/auto-assign.yaml deleted file mode 100644 index 0ecd2e5e..00000000 --- a/.github/workflows/auto-assign.yaml +++ /dev/null @@ -1,12 +0,0 @@ -name: Auto Assign PR to Author - -on: - pull_request: - types: [opened, reopened] - -jobs: - auto-assign: - permissions: - issues: write - pull-requests: write - uses: ithacaxyz/ci/.github/workflows/auto-assign-pr.yml@main diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6915c959..95878eae 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -5,10 +5,6 @@ on: push: branches: [main] -permissions: - contents: write - pull-requests: write - jobs: test: name: Tests @@ -21,31 +17,22 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly + version: nightly - name: Build run: | forge build + - name: Check formatting + run: | + forge fmt --check + - name: Run tests - env: - UPGRADE_TEST_RPC_URL: ${{ secrets.UPGRADE_TEST_RPC_URL }} - UPGRADE_TEST_OLD_PROXY: ${{ secrets.UPGRADE_TEST_OLD_PROXY }} - UPGRADE_TEST_OLD_VERSION: ${{ secrets.UPGRADE_TEST_OLD_VERSION }} run: | forge test -vvv - - name: Format contracts and generate snapshots - if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository - run: | - forge fmt - forge snapshot --isolate --match-contract Benchmark --via-ir + - name: Snapshot main branch + run: git fetch origin main && git worktree prune &&rm -rf .snapshot_worktree && git worktree add .snapshot_worktree origin/main && (cd .snapshot_worktree && forge snapshot --match-contract Benchmark --snap .temp-snapshot) && cp .snapshot_worktree/.temp-snapshot gas-snapshots/.gas-snapshot-main && git worktree remove --force .snapshot_worktree && git worktree prune - - name: Commit formatting and snapshot changes - if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository - uses: stefanzweifel/git-auto-commit-action@v5 - with: - commit_message: "chore: fmt contracts and update gas snapshots" - file_pattern: "*.sol snapshots/" - commit_user_name: github-actions[bot] - commit_user_email: github-actions[bot]@users.noreply.github.com + - name: Compare gas snapshots + run: forge snapshot --match-contract Benchmark --diff gas-snapshots/.gas-snapshot-main diff --git a/.github/workflows/version-check.yaml b/.github/workflows/version-check.yaml index f5946cb5..059fa1e4 100644 --- a/.github/workflows/version-check.yaml +++ b/.github/workflows/version-check.yaml @@ -57,22 +57,31 @@ jobs: - name: Bump version if needed if: steps.check.outputs.needs_version_bump == 'true' run: | + # Configure git + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + # Get the contracts that need bumping CONTRACTS_TO_BUMP="${{ steps.check.outputs.contracts_to_bump }}" - + echo "Bumping versions for contracts: $CONTRACTS_TO_BUMP" - + # Update Solidity files using the upgrade script with specific contracts CONTRACTS_TO_BUMP="$CONTRACTS_TO_BUMP" node prep/update-version.js - - - name: Commit version bump changes - if: steps.check.outputs.needs_version_bump == 'true' - uses: stefanzweifel/git-auto-commit-action@v5 - with: - commit_message: "chore: bump contract versions due to bytecode changes - Contracts updated: ${{ steps.check.outputs.contracts_to_bump }}" - file_pattern: "src/*.sol" - commit_user_name: github-actions[bot] - commit_user_email: github-actions[bot]@users.noreply.github.com + + # Commit changes (only Solidity files, not package.json) + git add src/*.sol + git commit -m "chore: bump contract versions due to bytecode changes - Contracts updated: $CONTRACTS_TO_BUMP" + + # Pull latest changes and rebase + # Pull latest changes and rebase + if ! git pull origin ${{ github.event.pull_request.head.ref }} --rebase; then + echo "Failed to rebase version bump changes. Manual intervention required." + exit 1 + fi + + # Push to the PR branch + git push origin HEAD:${{ github.event.pull_request.head.ref }} - name: Create PR comment if: steps.check.outputs.needs_version_bump == 'true' diff --git a/.gitmodules b/.gitmodules index 28a7bc52..7aed494c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,6 +7,9 @@ [submodule "lib/LayerZero-v2"] path = lib/LayerZero-v2 url = https://github.com/LayerZero-Labs/LayerZero-v2 +[submodule "lib/devtools"] + path = lib/devtools + url = https://github.com/LayerZero-Labs/devtools [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fd21859..f8bdceaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,24 +1,5 @@ # porto-account -> Note: After v0.5.5, all changelogs will be published along with the release notes. -> From here on, this file is deprecated. - -## 0.5.5 - -### Patch Changes -- Update foundry config, to not include metadata in the bytecode. This ensures that the contract bytecode doesn't change because of some other change in the repository. - - -## 0.5.4 - -### Patch Changes - -- SimpleFunder supports multiple orchestrators instead of single immutable orchestrator - - Replaced immutable `ORCHESTRATOR` with `orchestrators` mapping and `setOrchestrators()` function - - Maintained backward compatibility with old `fund()` signature - - Added `supported_orchestrators` config field for deployment - - Version bumped to "0.1.5" - ## 0.5.0 ### Minor Changes diff --git a/README.md b/README.md index aa22490b..ea941768 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/ithacaxyz/account) +> 🚧 **Work In Progress** +> This repository is under active development. Contracts are **unaudited**, and the codebase may have **breaking changes** without notice. +> A bug bounty is live on Base Mainnet — [details here](docs/bug-bounty.md). + **All-in-one EIP-7702 powered account contract, coupled with [Porto](https://github.com/ithacaxyz/porto)** Every app needs an account, traditionally requiring separate services for auth, payments, and recovery. Doing this in a way that empowers users with control over their funds and their data is the core challenge of the crypto space. While crypto wallets have made great strides, users still face a fragmented experience - juggling private keys, managing account balances across networks, @@ -19,24 +23,17 @@ We believe that unstoppable crypto-powered accounts should be excellent througho # Features out of the box -- Secure Login: Using WebAuthN-compatible credentials like PassKeys. -- Call Batching: Send multiple calls in 1. -- Gas Sponsorship: Allow anyone to pay for your fees in any ERC20 or ETH. -- Access Control: Whitelist receivers, function selectors and arguments. -- Session Keys: Allow transactions without confirmations if they pass low-security access control policies. -- Multi-sig Support: If a call is outside of a certain access control policy, require multiple signatures. -- Interop: Transaction on any chain invisibly. - -## Benchmarks - -![Benchmarks](docs/benchmarks.jpeg) - -Gas benchmark implementations are in the [test repository](test/Benchmark.t.sol). We currently benchmark against leading ERC-4337 accounts. To generate the benchmarks, use `forge snapshot --isolate`. - -## Security -Contracts were audited in a 2 week engagement by @MiloTruck @rholterhus @kadenzipfel - -We also maintain an active bug bounty program, you can find more details about it [here](https://porto.sh/contracts/security-and-bug-bounty) +- [x] Secure Login: Using WebAuthN-compatible credentials like PassKeys. +- [x] Call Batching: Send multiple calls in 1. +- [x] Gas Sponsorship: Allow anyone to pay for your fees in any ERC20 or ETH. +- [x] Access Control: Whitelist receivers, function selectors and arguments. +- [x] Session Keys: Allow transactions without confirmations if they pass low-security access control policies. +- [x] Multi-sig Support: If a call is outside of a certain access control policy, require multiple signatures. +- [x] Interop: Transaction on any chain invisibly. +- [ ] Timelocks: Add a time delay between transaction verification and execution, for additional safety. +- [ ] Optimized for L2: Using BLS signatures. +- [ ] Privacy: Using stealth addresses, confidential ERC20 tokens, and privacy pool integrations. +- [ ] Account Recovery & Identity: Using ZK {Email, OAUth, Passport} and more. ## Getting Help diff --git a/deploy/ConfigureLayerZeroSettler.s.sol b/deploy/ConfigureLayerZeroSettler.s.sol index 63187a5f..4e505329 100644 --- a/deploy/ConfigureLayerZeroSettler.s.sol +++ b/deploy/ConfigureLayerZeroSettler.s.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.23; import {Script} from "forge-std/Script.sol"; import {console} from "forge-std/Script.sol"; -import {Config} from "forge-std/Config.sol"; import {ILayerZeroEndpointV2} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; import {SetConfigParam} from @@ -37,7 +36,7 @@ import {LayerZeroSettler} from "../src/LayerZeroSettler.sol"; * --private-key $L0_SETTLER_OWNER_PK \ * "[84532,11155420]" */ -contract ConfigureLayerZeroSettler is Script, Config { +contract ConfigureLayerZeroSettler is Script { // Configuration type constants (matching ULN302) uint32 constant CONFIG_TYPE_EXECUTOR = 1; uint32 constant CONFIG_TYPE_ULN = 2; @@ -47,7 +46,6 @@ contract ConfigureLayerZeroSettler is Script, Config { string name; address layerZeroSettlerAddress; address layerZeroEndpoint; - address l0SettlerSigner; uint32 eid; address sendUln302; address receiveUln302; @@ -61,7 +59,6 @@ contract ConfigureLayerZeroSettler is Script, Config { // Fork ids for chain switching mapping(uint256 => uint256) public forkIds; - mapping(uint256 => bool) public isForkInitialized; mapping(uint256 => LayerZeroChainConfig) public chainConfigs; uint256[] public configuredChainIds; @@ -69,12 +66,8 @@ contract ConfigureLayerZeroSettler is Script, Config { * @notice Configure all chains with LayerZero configuration */ function run() external { - // Load configuration and setup forks - string memory configPath = string.concat(vm.projectRoot(), "/deploy/config.toml"); - _loadConfigAndForks(configPath, false); - - // Get all chain IDs from configuration - uint256[] memory chainIds = config.getChainIds(); + // Get all chain IDs from fork configuration + uint256[] memory chainIds = vm.readForkChainIds(); run(chainIds); } @@ -86,15 +79,12 @@ contract ConfigureLayerZeroSettler is Script, Config { console.log("Loading configuration from deploy/config.toml"); console.log("Configuring", chainIds.length, "chains"); - // If config not already loaded, load it - if (address(config) == address(0)) { - string memory configPath = string.concat(vm.projectRoot(), "/deploy/config.toml"); - _loadConfigAndForks(configPath, false); - } - // Load configurations for all chains loadConfigurations(chainIds); + // Create forks for all chains that have LayerZero config + createForks(); + // Configure each chain for (uint256 i = 0; i < configuredChainIds.length; i++) { configureChain(configuredChainIds[i]); @@ -110,22 +100,27 @@ contract ConfigureLayerZeroSettler is Script, Config { for (uint256 i = 0; i < chainIds.length; i++) { uint256 chainId = chainIds[i]; - // Switch to the fork for this chain (already created by _loadConfigAndForks) - vm.selectFork(forkOf[chainId]); + // Create fork to read configuration + string memory rpcUrl = vm.envString(string.concat("RPC_", vm.toString(chainId))); + uint256 forkId = vm.createFork(rpcUrl); + vm.selectFork(forkId); // Try to load LayerZero configuration - LayerZeroChainConfig memory chainConfig = loadChainConfig(chainId); - - // Only add chains that have LayerZero configuration - if (chainConfig.layerZeroSettlerAddress != address(0)) { - chainConfigs[chainId] = chainConfig; + LayerZeroChainConfig memory config = loadChainConfig(chainId); + + // Only store if chain has LayerZero configuration + if (config.sendUln302 != address(0)) { + chainConfigs[chainId] = config; configuredChainIds.push(chainId); - forkIds[chainId] = forkOf[chainId]; - isForkInitialized[forkOf[chainId]] = true; + forkIds[chainId] = forkId; console.log( string.concat( - " Loaded LayerZero config for ", chainConfig.name, " (", vm.toString(chainId), ")" + " Loaded LayerZero config for ", + config.name, + " (", + vm.toString(chainId), + ")" ) ); } @@ -135,68 +130,62 @@ contract ConfigureLayerZeroSettler is Script, Config { } /** - * @notice Load configuration for a single chain using StdConfig + * @notice Load configuration for a single chain from fork variables */ function loadChainConfig(uint256 chainId) internal view - returns (LayerZeroChainConfig memory chainConfig) + returns (LayerZeroChainConfig memory config) { - chainConfig.chainId = chainId; + config.chainId = chainId; // Load basic chain info - required - chainConfig.name = config.get(chainId, "name").toString(); + config.name = vm.readForkString("name"); - // Try to load LayerZero settler deployed address first, then fall back to expected address - address settlerAddr = config.get(chainId, "layerzero_settler_deployed").toAddress(); - if (settlerAddr == address(0)) { - // Fall back to expected address from config - settlerAddr = config.get(chainId, "layerzero_settler_address").toAddress(); - } - if (settlerAddr == address(0)) { + // Try to load LayerZero settler address - if not present, this chain doesn't have LayerZero config + try vm.readForkAddress("layerzero_settler_address") returns (address addr) { + config.layerZeroSettlerAddress = addr; + } catch { // No LayerZero settler configured for this chain - this is ok, return empty config - return chainConfig; + return config; } - chainConfig.layerZeroSettlerAddress = settlerAddr; // If we have a LayerZero settler, all other LayerZero fields are required - chainConfig.layerZeroEndpoint = config.get(chainId, "layerzero_endpoint").toAddress(); - chainConfig.l0SettlerSigner = config.get(chainId, "l0_settler_signer").toAddress(); - chainConfig.eid = uint32(config.get(chainId, "layerzero_eid").toUint256()); - chainConfig.sendUln302 = config.get(chainId, "layerzero_send_uln302").toAddress(); - chainConfig.receiveUln302 = config.get(chainId, "layerzero_receive_uln302").toAddress(); + config.layerZeroEndpoint = vm.readForkAddress("layerzero_endpoint"); + config.eid = uint32(vm.readForkUint("layerzero_eid")); + config.sendUln302 = vm.readForkAddress("layerzero_send_uln302"); + config.receiveUln302 = vm.readForkAddress("layerzero_receive_uln302"); // Load destination chain IDs - required for LayerZero configuration - chainConfig.destinationChainIds = config.get(chainId, "layerzero_destination_chain_ids").toUint256Array(); + config.destinationChainIds = vm.readForkUintArray("layerzero_destination_chain_ids"); // Load DVN configuration - required and optional DVN arrays - string[] memory requiredDVNNames = config.get(chainId, "layerzero_required_dvns").toStringArray(); - string[] memory optionalDVNNames = config.get(chainId, "layerzero_optional_dvns").toStringArray(); + string[] memory requiredDVNNames = vm.readForkStringArray("layerzero_required_dvns"); + string[] memory optionalDVNNames = vm.readForkStringArray("layerzero_optional_dvns"); // Resolve DVN names to addresses - chainConfig.requiredDVNs = resolveDVNAddresses(chainId, requiredDVNNames); - chainConfig.optionalDVNs = resolveDVNAddresses(chainId, optionalDVNNames); + config.requiredDVNs = resolveDVNAddresses(requiredDVNNames); + config.optionalDVNs = resolveDVNAddresses(optionalDVNNames); // Load optional DVN threshold - required field - chainConfig.optionalDVNThreshold = uint8(config.get(chainId, "layerzero_optional_dvn_threshold").toUint256()); + config.optionalDVNThreshold = uint8(vm.readForkUint("layerzero_optional_dvn_threshold")); // Load confirmations - required field - chainConfig.confirmations = uint64(config.get(chainId, "layerzero_confirmations").toUint256()); + config.confirmations = uint64(vm.readForkUint("layerzero_confirmations")); // Load max message size - required field - chainConfig.maxMessageSize = uint32(config.get(chainId, "layerzero_max_message_size").toUint256()); + config.maxMessageSize = uint32(vm.readForkUint("layerzero_max_message_size")); - return chainConfig; + return config; } /** - * @notice Resolve DVN names to addresses using StdConfig - * @dev Takes DVN variable names and looks up their addresses using config.get - * @param chainId The chain ID to resolve DVN addresses for + * @notice Resolve DVN names to addresses by reading from fork variables + * @dev Takes DVN variable names and looks up their addresses using vm.readForkAddress * @param dvnNames Array of DVN variable names from config (e.g., "dvn_layerzero_labs") * @return addresses Array of resolved DVN addresses */ - function resolveDVNAddresses(uint256 chainId, string[] memory dvnNames) + function resolveDVNAddresses(string[] memory dvnNames) internal view returns (address[] memory) @@ -204,7 +193,7 @@ contract ConfigureLayerZeroSettler is Script, Config { address[] memory addresses = new address[](dvnNames.length); for (uint256 i = 0; i < dvnNames.length; i++) { - addresses[i] = config.get(chainId, dvnNames[i]).toAddress(); + addresses[i] = vm.readForkAddress(dvnNames[i]); require( addresses[i] != address(0), string.concat("DVN address not configured for: ", dvnNames[i]) @@ -214,8 +203,18 @@ contract ConfigureLayerZeroSettler is Script, Config { return addresses; } - function _selectFork(uint256 chainId) internal { - vm.selectFork(forkOf[chainId]); + /** + * @notice Create forks for all configured chains + */ + function createForks() internal { + console.log("\n=== Creating forks for configured chains ==="); + + for (uint256 i = 0; i < configuredChainIds.length; i++) { + uint256 chainId = configuredChainIds[i]; + LayerZeroChainConfig memory config = chainConfigs[chainId]; + + console.log(" Fork created for", config.name, "fork ID:", forkIds[chainId]); + } } /** @@ -228,59 +227,31 @@ contract ConfigureLayerZeroSettler is Script, Config { console.log(string.concat("Configuring ", config.name, " (", vm.toString(chainId), ")")); console.log(" LayerZero Settler:", config.layerZeroSettlerAddress); console.log(" Endpoint:", config.layerZeroEndpoint); - console.log(" L0SettlerSigner:", config.l0SettlerSigner); console.log(" EID:", config.eid); // Switch to the source chain - _selectFork(chainId); + vm.selectFork(forkIds[chainId]); LayerZeroSettler settler = LayerZeroSettler(payable(config.layerZeroSettlerAddress)); - - // Set or update the endpoint on the settler - address currentEndpoint = address(settler.endpoint()); - if (currentEndpoint != config.layerZeroEndpoint) { - if (currentEndpoint == address(0)) { - console.log(" Setting endpoint to:", config.layerZeroEndpoint); - } else { - console.log(" Updating endpoint from:", currentEndpoint); - console.log(" To:", config.layerZeroEndpoint); - } - vm.broadcast(); - settler.setEndpoint(config.layerZeroEndpoint); - console.log(" Endpoint configured successfully"); - } else { - console.log(" Endpoint already set to:", config.layerZeroEndpoint); - } - - // Set or update the L0SettlerSigner on the settler - address currentSigner = settler.l0SettlerSigner(); - if (currentSigner != config.l0SettlerSigner) { - if (currentSigner == address(0)) { - console.log(" Setting L0SettlerSigner to:", config.l0SettlerSigner); - } else { - console.log(" Updating L0SettlerSigner from:", currentSigner); - console.log(" To:", config.l0SettlerSigner); - } - vm.broadcast(); - settler.setL0SettlerSigner(config.l0SettlerSigner); - console.log(" L0SettlerSigner configured successfully"); - } else { - console.log(" L0SettlerSigner already set to:", config.l0SettlerSigner); - } - ILayerZeroEndpointV2 endpoint = ILayerZeroEndpointV2(config.layerZeroEndpoint); // Configure pathways to all destinations for (uint256 i = 0; i < config.destinationChainIds.length; i++) { uint256 destChainId = config.destinationChainIds[i]; + // Skip if destination not configured + if (forkIds[destChainId] == 0) { + console.log(" Skipping unconfigured destination:", destChainId); + continue; + } + LayerZeroChainConfig memory destConfig = chainConfigs[destChainId]; console.log(string.concat("\n Configuring pathway to ", destConfig.name)); console.log(" Destination EID:", destConfig.eid); // Set executor config (self-execution model) - setExecutorConfig(config, settler, endpoint, destConfig.eid); + setExecutorConfig(settler, endpoint, destConfig.eid); // Set send ULN config setSendUlnConfig( @@ -295,31 +266,11 @@ contract ConfigureLayerZeroSettler is Script, Config { ); // Switch to destination chain to set receive config - _selectFork(destChainId); - - // Ensure destination settler has endpoint set before configuring - LayerZeroSettler destSettler = - LayerZeroSettler(payable(destConfig.layerZeroSettlerAddress)); - address currentDestEndpoint = address(destSettler.endpoint()); - if (currentDestEndpoint != destConfig.layerZeroEndpoint) { - if (currentDestEndpoint == address(0)) { - console.log( - " Setting endpoint on destination:", destConfig.layerZeroEndpoint - ); - } else { - console.log(" Updating endpoint on destination from:", currentDestEndpoint); - console.log(" To:", destConfig.layerZeroEndpoint); - } - vm.broadcast(); - destSettler.setEndpoint(destConfig.layerZeroEndpoint); - console.log(" Destination endpoint configured successfully"); - } else { - console.log(" Destination endpoint already set:", destConfig.layerZeroEndpoint); - } + vm.selectFork(forkIds[destChainId]); // Set receive ULN config on destination setReceiveUlnConfig( - destSettler, + LayerZeroSettler(payable(destConfig.layerZeroSettlerAddress)), ILayerZeroEndpointV2(destConfig.layerZeroEndpoint), config.eid, // Source EID destConfig.receiveUln302, @@ -330,7 +281,7 @@ contract ConfigureLayerZeroSettler is Script, Config { ); // Switch back to source chain - _selectFork(chainId); + vm.selectFork(forkIds[chainId]); } console.log("\n Configuration complete for", config.name); @@ -341,24 +292,12 @@ contract ConfigureLayerZeroSettler is Script, Config { // ============================================ function setExecutorConfig( - LayerZeroChainConfig memory config, LayerZeroSettler settler, ILayerZeroEndpointV2 endpoint, uint32 destEid ) internal { - console.log(" Setting executor config:"); - console.log(" Executor (self-execution):", address(settler)); - console.log(" Max message size:", config.maxMessageSize); - console.log(" Send ULN302:", config.sendUln302); - - bytes memory executorConfig = abi.encode(config.maxMessageSize, settler); - SetConfigParam[] memory params = new SetConfigParam[](1); - params[0] = - SetConfigParam({eid: destEid, configType: CONFIG_TYPE_EXECUTOR, config: executorConfig}); - - vm.broadcast(); - endpoint.setConfig(address(settler), config.sendUln302, params); - console.log(" Executor config set"); + // LayerZeroSettler uses self-execution model, no executor config needed + console.log(" Using self-execution model (no executor config)"); } function setSendUlnConfig( @@ -398,7 +337,7 @@ contract ConfigureLayerZeroSettler is Script, Config { }); // Get the L0 settler owner who should be the delegate - address l0SettlerOwner = config.get(vm.getChainId(), "l0_settler_owner").toAddress(); + address l0SettlerOwner = vm.readForkAddress("l0_settler_owner"); console.log(" L0 Settler Owner (delegate):", l0SettlerOwner); vm.broadcast(); diff --git a/deploy/DeployMain.s.sol b/deploy/DeployMain.s.sol index ee513799..5ebbb109 100644 --- a/deploy/DeployMain.s.sol +++ b/deploy/DeployMain.s.sol @@ -3,8 +3,7 @@ pragma solidity ^0.8.23; import {Script, console} from "forge-std/Script.sol"; import {VmSafe} from "forge-std/Vm.sol"; -import {Config} from "forge-std/Config.sol"; -import {Variable, TypeKind} from "forge-std/LibVariable.sol"; +import {stdToml} from "forge-std/StdToml.sol"; import {SafeSingletonDeployer} from "./SafeSingletonDeployer.sol"; // Import contracts to deploy @@ -16,7 +15,6 @@ import {SimpleFunder} from "../src/SimpleFunder.sol"; import {Escrow} from "../src/Escrow.sol"; import {SimpleSettler} from "../src/SimpleSettler.sol"; import {LayerZeroSettler} from "../src/LayerZeroSettler.sol"; -import {ExperimentERC20} from "./mock/ExperimentalERC20.sol"; /** * @title DeployMain @@ -51,26 +49,23 @@ import {ExperimentERC20} from "./mock/ExperimentalERC20.sol"; * --private-key $PRIVATE_KEY \ * "[1]" "/deploy/custom-config.toml" */ -contract DeployMain is Script, Config, SafeSingletonDeployer { +contract DeployMain is Script, SafeSingletonDeployer { + using stdToml for string; // Chain configuration struct struct ChainConfig { uint256 chainId; string name; bool isTestnet; + address pauseAuthority; address funderOwner; address funderSigner; address settlerOwner; address l0SettlerOwner; - address l0SettlerSigner; address layerZeroEndpoint; - address[] oldOrchestrators; uint32 layerZeroEid; bytes32 salt; string[] contracts; // Array of contract names to deploy - // EXP Token configuration (testnet only) - address expMinterAddress; - uint256 expMintAmount; } struct DeployedContracts { @@ -82,8 +77,6 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { address layerZeroSettler; address simpleFunder; address simulator; - address expToken; // EXP token (testnet only) - address exp2Token; // EXP2 token (testnet only) } // State @@ -91,7 +84,9 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { mapping(uint256 => DeployedContracts) internal deployedContracts; uint256[] internal targetChainIds; - // Config path + // Paths and config + string internal registryPath; + string internal configContent; // For unified config string internal configPath = "/deploy/config.toml"; // Events for tracking @@ -109,17 +104,9 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { * @notice Deploy to all chains in config */ function run() external { - // Load configuration and setup forks (enable write-back to save deployed addresses) - string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); - _loadConfigAndForks(fullConfigPath, true); - - // Get all available chain IDs from configuration - targetChainIds = config.getChainIds(); - require(targetChainIds.length > 0, "No chains found in configuration"); - - // Load configuration for each chain - loadConfigurations(); - loadDeployedContracts(); + // Get all available chain IDs from fork configuration + uint256[] memory chainIds = vm.readForkChainIds(); + initializeDeployment(chainIds); executeDeployment(); } @@ -128,20 +115,11 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { * @param chainIds Array of chain IDs to deploy to (empty array = all chains) */ function run(uint256[] memory chainIds) external { - // Load configuration and setup forks (enable write-back to save deployed addresses) - string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); - _loadConfigAndForks(fullConfigPath, true); - // If empty array, get all available chains if (chainIds.length == 0) { - chainIds = config.getChainIds(); + chainIds = vm.readForkChainIds(); } - targetChainIds = chainIds; - require(targetChainIds.length > 0, "No chains found in configuration"); - - // Load configuration for each chain - loadConfigurations(); - loadDeployedContracts(); + initializeDeployment(chainIds); executeDeployment(); } @@ -151,25 +129,47 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { * @param _configPath Path to custom TOML config file */ function run(uint256[] memory chainIds, string memory _configPath) external { - configPath = _configPath; - - // Load configuration and setup forks (enable write-back to save deployed addresses) - string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); - _loadConfigAndForks(fullConfigPath, true); - // If empty array, get all available chains if (chainIds.length == 0) { - chainIds = config.getChainIds(); + chainIds = vm.readForkChainIds(); } + initializeDeployment(chainIds, _configPath); + executeDeployment(); + } + + /** + * @notice Initialize deployment with target chains using TOML config + * @param chainIds Array of chain IDs to deploy to + */ + function initializeDeployment(uint256[] memory chainIds) internal { + require(chainIds.length > 0, "No chains found in configuration"); + + // Load unified configuration + string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); + configContent = vm.readFile(fullConfigPath); + + // Load registry path from config.toml + registryPath = configContent.readString(".profile.deployment.registry_path"); + + // Store target chain IDs targetChainIds = chainIds; - require(targetChainIds.length > 0, "No chains found in configuration"); - + // Load configuration for each chain loadConfigurations(); + + // Load existing deployed contracts from registry loadDeployedContracts(); - executeDeployment(); } + /** + * @notice Initialize deployment with custom config path + * @param chainIds Array of chain IDs to deploy to + * @param _configPath Path to the config file + */ + function initializeDeployment(uint256[] memory chainIds, string memory _configPath) internal { + configPath = _configPath; + initializeDeployment(chainIds); + } /** * @notice Load configurations for all target chains @@ -178,15 +178,20 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { for (uint256 i = 0; i < targetChainIds.length; i++) { uint256 chainId = targetChainIds[i]; - // Switch to the fork for this chain (already created by _loadConfigAndForks) - vm.selectFork(forkOf[chainId]); + // Use the RPC_{chainId} environment variable directly + // This matches the naming convention in config.toml + string memory rpcUrl = vm.envString(string.concat("RPC_", vm.toString(chainId))); + + // Create fork using the RPC URL + uint256 forkId = vm.createFork(rpcUrl); + vm.selectFork(forkId); // Verify we're on the correct chain require(block.chainid == chainId, "Chain ID mismatch"); - // Load configuration using new StdConfig pattern - ChainConfig memory chainConfig = loadChainConfigFromStdConfig(chainId); - chainConfigs[chainId] = chainConfig; + // Load configuration from fork variables + ChainConfig memory config = loadChainConfigFromFork(chainId); + chainConfigs[chainId] = config; } // Log the loaded configuration for verification @@ -194,66 +199,48 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { } /** - * @notice Load chain configuration using StdConfig + * @notice Load chain configuration from the currently active fork * @param chainId The chain ID we're loading config for */ - function loadChainConfigFromStdConfig(uint256 chainId) internal view returns (ChainConfig memory) { - ChainConfig memory chainConfig; + function loadChainConfigFromFork(uint256 chainId) internal view returns (ChainConfig memory) { + ChainConfig memory config; - chainConfig.chainId = chainId; + config.chainId = chainId; - // Use StdConfig to read variables - chainConfig.name = config.get(chainId, "name").toString(); - chainConfig.isTestnet = config.get(chainId, "is_testnet").toBool(); + // Use vm.readFork* functions to read variables from the active fork + config.name = vm.readForkString("name"); + config.isTestnet = vm.readForkBool("is_testnet"); // Load addresses - chainConfig.funderOwner = config.get(chainId, "funder_owner").toAddress(); - chainConfig.funderSigner = config.get(chainId, "funder_signer").toAddress(); - chainConfig.settlerOwner = config.get(chainId, "settler_owner").toAddress(); - chainConfig.l0SettlerOwner = config.get(chainId, "l0_settler_owner").toAddress(); - chainConfig.l0SettlerSigner = config.get(chainId, "l0_settler_signer").toAddress(); - chainConfig.layerZeroEndpoint = config.get(chainId, "layerzero_endpoint").toAddress(); + config.pauseAuthority = vm.readForkAddress("pause_authority"); + config.funderOwner = vm.readForkAddress("funder_owner"); + config.funderSigner = vm.readForkAddress("funder_signer"); + config.settlerOwner = vm.readForkAddress("settler_owner"); + config.l0SettlerOwner = vm.readForkAddress("l0_settler_owner"); + config.layerZeroEndpoint = vm.readForkAddress("layerzero_endpoint"); // Load other configuration - chainConfig.layerZeroEid = uint32(config.get(chainId, "layerzero_eid").toUint256()); - chainConfig.salt = config.get(chainId, "salt").toBytes32(); - - // Load EXP token configuration (testnet only) - if (chainConfig.isTestnet) { - chainConfig.expMinterAddress = config.get(chainId, "exp_minter_address").toAddress(); - chainConfig.expMintAmount = config.get(chainId, "exp_mint_amount").toUint256(); - } + config.layerZeroEid = uint32(vm.readForkUint("layerzero_eid")); + config.salt = vm.readForkBytes32("salt"); // Load contracts list - required field, will revert if not present - string[] memory contractsList = config.get(chainId, "contracts").toStringArray(); + string[] memory contractsList = vm.readForkStringArray("contracts"); // Check if user specified "ALL" to deploy all contracts if ( contractsList.length == 1 && keccak256(bytes(contractsList[0])) == keccak256(bytes("ALL")) ) { - string[] memory baseContracts = getAllContracts(); - // For testnets with ALL specified, append ExpToken - if (chainConfig.isTestnet) { - string[] memory testnetContracts = new string[](baseContracts.length + 1); - for (uint256 i = 0; i < baseContracts.length; i++) { - testnetContracts[i] = baseContracts[i]; - } - testnetContracts[baseContracts.length] = "ExpToken"; - chainConfig.contracts = testnetContracts; - } else { - // For non-testnets, use base contracts (no ExpToken) - chainConfig.contracts = baseContracts; - } + config.contracts = getAllContracts(); } else { - chainConfig.contracts = contractsList; + config.contracts = contractsList; } - return chainConfig; + return config; } /** - * @notice Get all available contracts (excluding ExpToken) + * @notice Get all available contracts */ function getAllContracts() internal pure returns (string[] memory) { string[] memory contracts = new string[](8); @@ -286,40 +273,31 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { } /** - * @notice Load deployed contracts from config + * @notice Load deployed contracts from registry */ function loadDeployedContracts() internal { for (uint256 i = 0; i < targetChainIds.length; i++) { uint256 chainId = targetChainIds[i]; - - DeployedContracts memory deployed; - - // Read deployed contract addresses from config, defaulting to address(0) if not set - deployed.orchestrator = tryGetAddress(chainId, "orchestrator_deployed"); - deployed.ithacaAccount = tryGetAddress(chainId, "ithaca_account_deployed"); - deployed.accountProxy = tryGetAddress(chainId, "account_proxy_deployed"); - deployed.escrow = tryGetAddress(chainId, "escrow_deployed"); - deployed.simpleSettler = tryGetAddress(chainId, "simple_settler_deployed"); - deployed.layerZeroSettler = tryGetAddress(chainId, "layerzero_settler_deployed"); - deployed.simpleFunder = tryGetAddress(chainId, "simple_funder_deployed"); - deployed.simulator = tryGetAddress(chainId, "simulator_deployed"); - deployed.expToken = tryGetAddress(chainId, "exp_token_deployed"); - deployed.exp2Token = tryGetAddress(chainId, "exp2_token_deployed"); - - deployedContracts[chainId] = deployed; - } - } - - /** - * @notice Try to get an address from config, return address(0) if not found - */ - function tryGetAddress(uint256 chainId, string memory key) internal view returns (address) { - Variable memory variable = config.get(chainId, key); - // Check if variable exists (TypeKind.None means missing key) - if (variable.ty.kind == TypeKind.None) { - return address(0); + bytes32 salt = chainConfigs[chainId].salt; + string memory registryFile = getRegistryFilename(chainId, salt); + + try vm.readFile(registryFile) returns (string memory registryJson) { + // Use individual parsing for flexibility with missing fields + DeployedContracts memory deployed; + deployed.ithacaAccount = tryReadAddress(registryJson, ".IthacaAccount"); + deployed.accountProxy = tryReadAddress(registryJson, ".AccountProxy"); + deployed.escrow = tryReadAddress(registryJson, ".Escrow"); + deployed.orchestrator = tryReadAddress(registryJson, ".Orchestrator"); + deployed.simpleSettler = tryReadAddress(registryJson, ".SimpleSettler"); + deployed.layerZeroSettler = tryReadAddress(registryJson, ".LayerZeroSettler"); + deployed.simpleFunder = tryReadAddress(registryJson, ".SimpleFunder"); + deployed.simulator = tryReadAddress(registryJson, ".Simulator"); + + deployedContracts[chainId] = deployed; + } catch { + // No registry file exists yet + } } - return variable.toAddress(); } /** @@ -337,8 +315,8 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { console.log("Funder Owner:", config.funderOwner); console.log("Funder Signer:", config.funderSigner); console.log("L0 Settler Owner:", config.l0SettlerOwner); - console.log("L0 Settler Signer:", config.l0SettlerSigner); console.log("Settler Owner:", config.settlerOwner); + console.log("Pause Authority:", config.pauseAuthority); console.log("LayerZero Endpoint:", config.layerZeroEndpoint); console.log("LayerZero EID:", config.layerZeroEid); console.log("Salt:"); @@ -445,7 +423,7 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { } /** - * @notice Save deployed contract address to config + * @notice Save deployed contract address to registry */ function saveDeployedContract( uint256 chainId, @@ -469,38 +447,124 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { deployedContracts[chainId].simpleSettler = contractAddress; } else if (keccak256(bytes(contractName)) == keccak256("LayerZeroSettler")) { deployedContracts[chainId].layerZeroSettler = contractAddress; - } else if (keccak256(bytes(contractName)) == keccak256("ExpToken")) { - deployedContracts[chainId].expToken = contractAddress; - } else if (keccak256(bytes(contractName)) == keccak256("Exp2Token")) { - deployedContracts[chainId].exp2Token = contractAddress; - } - - // Only write to config file during actual broadcasts, not simulations - if (vm.isContext(VmSafe.ForgeContext.ScriptBroadcast) || vm.isContext(VmSafe.ForgeContext.ScriptResume)) { - if (keccak256(bytes(contractName)) == keccak256("Orchestrator")) { - config.set(chainId, "orchestrator_deployed", contractAddress); - } else if (keccak256(bytes(contractName)) == keccak256("IthacaAccount")) { - config.set(chainId, "ithaca_account_deployed", contractAddress); - } else if (keccak256(bytes(contractName)) == keccak256("AccountProxy")) { - config.set(chainId, "account_proxy_deployed", contractAddress); - } else if (keccak256(bytes(contractName)) == keccak256("Simulator")) { - config.set(chainId, "simulator_deployed", contractAddress); - } else if (keccak256(bytes(contractName)) == keccak256("SimpleFunder")) { - config.set(chainId, "simple_funder_deployed", contractAddress); - } else if (keccak256(bytes(contractName)) == keccak256("Escrow")) { - config.set(chainId, "escrow_deployed", contractAddress); - } else if (keccak256(bytes(contractName)) == keccak256("SimpleSettler")) { - config.set(chainId, "simple_settler_deployed", contractAddress); - } else if (keccak256(bytes(contractName)) == keccak256("LayerZeroSettler")) { - config.set(chainId, "layerzero_settler_deployed", contractAddress); - } else if (keccak256(bytes(contractName)) == keccak256("ExpToken")) { - config.set(chainId, "exp_token_deployed", contractAddress); - } else if (keccak256(bytes(contractName)) == keccak256("Exp2Token")) { - config.set(chainId, "exp2_token_deployed", contractAddress); - } } + + // Write to registry file + writeToRegistry(chainId, contractName, contractAddress); + } + + /** + * @notice Write to registry file + */ + function writeToRegistry(uint256 chainId, string memory contractName, address contractAddress) + internal + { + // Only save registry during actual broadcasts, not dry runs + if ( + !vm.isContext(VmSafe.ForgeContext.ScriptBroadcast) + && !vm.isContext(VmSafe.ForgeContext.ScriptResume) + ) { + return; + } + + DeployedContracts memory deployed = deployedContracts[chainId]; + + string memory json = "{"; + bool first = true; + + if (deployed.orchestrator != address(0)) { + json = string.concat(json, '"Orchestrator": "', vm.toString(deployed.orchestrator), '"'); + first = false; + } + + if (deployed.ithacaAccount != address(0)) { + if (!first) json = string.concat(json, ","); + json = + string.concat(json, '"IthacaAccount": "', vm.toString(deployed.ithacaAccount), '"'); + first = false; + } + + if (deployed.accountProxy != address(0)) { + if (!first) json = string.concat(json, ","); + json = string.concat(json, '"AccountProxy": "', vm.toString(deployed.accountProxy), '"'); + first = false; + } + + if (deployed.simulator != address(0)) { + if (!first) json = string.concat(json, ","); + json = string.concat(json, '"Simulator": "', vm.toString(deployed.simulator), '"'); + first = false; + } + + if (deployed.simpleFunder != address(0)) { + if (!first) json = string.concat(json, ","); + json = string.concat(json, '"SimpleFunder": "', vm.toString(deployed.simpleFunder), '"'); + first = false; + } + + if (deployed.escrow != address(0)) { + if (!first) json = string.concat(json, ","); + json = string.concat(json, '"Escrow": "', vm.toString(deployed.escrow), '"'); + first = false; + } + + if (deployed.simpleSettler != address(0)) { + if (!first) json = string.concat(json, ","); + json = + string.concat(json, '"SimpleSettler": "', vm.toString(deployed.simpleSettler), '"'); + first = false; + } + + if (deployed.layerZeroSettler != address(0)) { + if (!first) json = string.concat(json, ","); + json = string.concat( + json, '"LayerZeroSettler": "', vm.toString(deployed.layerZeroSettler), '"' + ); + } + + json = string.concat(json, "}"); + + bytes32 salt = chainConfigs[chainId].salt; + string memory registryFile = getRegistryFilename(chainId, salt); + vm.writeFile(registryFile, json); + } + + /** + * @notice Get registry filename based on chainId and salt + */ + function getRegistryFilename(uint256 chainId, bytes32 salt) + internal + view + returns (string memory) + { + string memory filename = string.concat( + vm.projectRoot(), + "/", + registryPath, + "deployment_", + vm.toString(chainId), + "_", + vm.toString(salt), + ".json" + ); + return filename; } + /** + * @notice Try to read an address from JSON + */ + function tryReadAddress(string memory json, string memory key) + internal + pure + returns (address) + { + try vm.parseJson(json, key) returns (bytes memory data) { + if (data.length > 0) { + return abi.decode(data, (address)); + } + } catch {} + return address(0); + } /** * @notice Verify Safe Singleton Factory is deployed @@ -599,8 +663,6 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { deploySimpleSettler(chainId, config, deployed); } else if (nameHash == keccak256("LayerZeroSettler")) { deployLayerZeroSettler(chainId, config, deployed); - } else if (nameHash == keccak256("ExpToken")) { - deployExpToken(chainId, config, deployed); } else { console.log("Warning: Unknown contract name:", contractName); } @@ -611,12 +673,17 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - bytes memory creationCode = type(Orchestrator).creationCode; - address orchestrator = - deployContractWithCreate2(chainId, creationCode, "", "Orchestrator"); - - saveDeployedContract(chainId, "Orchestrator", orchestrator); - deployed.orchestrator = orchestrator; + if (deployed.orchestrator == address(0)) { + bytes memory creationCode = type(Orchestrator).creationCode; + bytes memory args = abi.encode(config.pauseAuthority); + address orchestrator = + deployContractWithCreate2(chainId, creationCode, args, "Orchestrator"); + + saveDeployedContract(chainId, "Orchestrator", orchestrator); + deployed.orchestrator = orchestrator; + } else { + console.log("Orchestrator already deployed:", deployed.orchestrator); + } } function deployIthacaAccount( @@ -625,15 +692,22 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { DeployedContracts memory deployed ) internal { // Ensure Orchestrator is deployed first (dependency) - require(deployed.orchestrator != address(0), "Orchestrator must be deployed before IthacaAccount"); + if (deployed.orchestrator == address(0)) { + console.log("Deploying Orchestrator first (dependency for IthacaAccount)..."); + deployOrchestrator(chainId, config, deployed); + } - bytes memory creationCode = type(IthacaAccount).creationCode; - bytes memory args = abi.encode(deployed.orchestrator); - address ithacaAccount = - deployContractWithCreate2(chainId, creationCode, args, "IthacaAccount"); + if (deployed.ithacaAccount == address(0)) { + bytes memory creationCode = type(IthacaAccount).creationCode; + bytes memory args = abi.encode(deployed.orchestrator); + address ithacaAccount = + deployContractWithCreate2(chainId, creationCode, args, "IthacaAccount"); - saveDeployedContract(chainId, "IthacaAccount", ithacaAccount); - deployed.ithacaAccount = ithacaAccount; + saveDeployedContract(chainId, "IthacaAccount", ithacaAccount); + deployed.ithacaAccount = ithacaAccount; + } else { + console.log("IthacaAccount already deployed:", deployed.ithacaAccount); + } } function deployAccountProxy( @@ -642,14 +716,21 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { DeployedContracts memory deployed ) internal { // Ensure IthacaAccount is deployed first (dependency) - require(deployed.ithacaAccount != address(0), "IthacaAccount must be deployed before AccountProxy"); + if (deployed.ithacaAccount == address(0)) { + console.log("Deploying IthacaAccount first (dependency for AccountProxy)..."); + deployIthacaAccount(chainId, config, deployed); + } - bytes memory proxyCode = LibEIP7702.proxyInitCode(deployed.ithacaAccount, address(0)); - address accountProxy = deployContractWithCreate2(chainId, proxyCode, "", "AccountProxy"); + if (deployed.accountProxy == address(0)) { + bytes memory proxyCode = LibEIP7702.proxyInitCode(deployed.ithacaAccount, address(0)); + address accountProxy = deployContractWithCreate2(chainId, proxyCode, "", "AccountProxy"); - require(accountProxy != address(0), "Account proxy deployment failed"); - saveDeployedContract(chainId, "AccountProxy", accountProxy); - deployed.accountProxy = accountProxy; + require(accountProxy != address(0), "Account proxy deployment failed"); + saveDeployedContract(chainId, "AccountProxy", accountProxy); + deployed.accountProxy = accountProxy; + } else { + console.log("AccountProxy already deployed:", deployed.accountProxy); + } } function deploySimulator( @@ -657,11 +738,15 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - bytes memory creationCode = type(Simulator).creationCode; - address simulator = deployContractWithCreate2(chainId, creationCode, "", "Simulator"); + if (deployed.simulator == address(0)) { + bytes memory creationCode = type(Simulator).creationCode; + address simulator = deployContractWithCreate2(chainId, creationCode, "", "Simulator"); - saveDeployedContract(chainId, "Simulator", simulator); - deployed.simulator = simulator; + saveDeployedContract(chainId, "Simulator", simulator); + deployed.simulator = simulator; + } else { + console.log("Simulator already deployed:", deployed.simulator); + } } function deploySimpleFunder( @@ -669,13 +754,23 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - bytes memory creationCode = type(SimpleFunder).creationCode; + // Ensure Orchestrator is deployed first (dependency) + if (deployed.orchestrator == address(0)) { + console.log("Deploying Orchestrator first (dependency for SimpleFunder)..."); + deployOrchestrator(chainId, config, deployed); + } - bytes memory args = abi.encode(config.funderSigner, config.funderOwner); - address funder = deployContractWithCreate2(chainId, creationCode, args, "SimpleFunder"); + if (deployed.simpleFunder == address(0)) { + bytes memory creationCode = type(SimpleFunder).creationCode; + bytes memory args = + abi.encode(config.funderSigner, deployed.orchestrator, config.funderOwner); + address funder = deployContractWithCreate2(chainId, creationCode, args, "SimpleFunder"); - saveDeployedContract(chainId, "SimpleFunder", funder); - deployed.simpleFunder = funder; + saveDeployedContract(chainId, "SimpleFunder", funder); + deployed.simpleFunder = funder; + } else { + console.log("SimpleFunder already deployed:", deployed.simpleFunder); + } } function deployEscrow( @@ -683,11 +778,15 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - bytes memory creationCode = type(Escrow).creationCode; - address escrow = deployContractWithCreate2(chainId, creationCode, "", "Escrow"); + if (deployed.escrow == address(0)) { + bytes memory creationCode = type(Escrow).creationCode; + address escrow = deployContractWithCreate2(chainId, creationCode, "", "Escrow"); - saveDeployedContract(chainId, "Escrow", escrow); - deployed.escrow = escrow; + saveDeployedContract(chainId, "Escrow", escrow); + deployed.escrow = escrow; + } else { + console.log("Escrow already deployed:", deployed.escrow); + } } function deploySimpleSettler( @@ -695,14 +794,18 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - bytes memory creationCode = type(SimpleSettler).creationCode; - bytes memory args = abi.encode(config.settlerOwner); - address settler = - deployContractWithCreate2(chainId, creationCode, args, "SimpleSettler"); - - console.log(" Owner:", config.settlerOwner); - saveDeployedContract(chainId, "SimpleSettler", settler); - deployed.simpleSettler = settler; + if (deployed.simpleSettler == address(0)) { + bytes memory creationCode = type(SimpleSettler).creationCode; + bytes memory args = abi.encode(config.settlerOwner); + address settler = + deployContractWithCreate2(chainId, creationCode, args, "SimpleSettler"); + + console.log(" Owner:", config.settlerOwner); + saveDeployedContract(chainId, "SimpleSettler", settler); + deployed.simpleSettler = settler; + } else { + console.log("SimpleSettler already deployed:", deployed.simpleSettler); + } } function deployLayerZeroSettler( @@ -710,62 +813,19 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - bytes memory creationCode = type(LayerZeroSettler).creationCode; - bytes memory args = abi.encode(config.l0SettlerOwner, config.l0SettlerSigner); - address settler = - deployContractWithCreate2(chainId, creationCode, args, "LayerZeroSettler"); - - console.log(" Owner:", config.l0SettlerOwner); - console.log(" L0SettlerSigner:", config.l0SettlerSigner); - console.log(" Endpoint to be configured:", config.layerZeroEndpoint); - console.log(" EID:", config.layerZeroEid); - console.log( - " Note: Endpoint must be set by owner via ConfigureLayerZeroSettler script" - ); - - saveDeployedContract(chainId, "LayerZeroSettler", settler); - deployed.layerZeroSettler = settler; - } - - function deployExpToken( - uint256 chainId, - ChainConfig memory config, - DeployedContracts memory deployed - ) internal { - // Only deploy on testnets - if (!config.isTestnet) { - console.log("Skipping ExpToken deployment - not a testnet"); - return; + if (deployed.layerZeroSettler == address(0)) { + bytes memory creationCode = type(LayerZeroSettler).creationCode; + bytes memory args = abi.encode(config.layerZeroEndpoint, config.l0SettlerOwner); + address settler = + deployContractWithCreate2(chainId, creationCode, args, "LayerZeroSettler"); + + console.log(" Endpoint:", config.layerZeroEndpoint); + console.log(" Owner:", config.l0SettlerOwner); + console.log(" EID:", config.layerZeroEid); + saveDeployedContract(chainId, "LayerZeroSettler", settler); + deployed.layerZeroSettler = settler; + } else { + console.log("LayerZeroSettler already deployed:", deployed.layerZeroSettler); } - - bytes memory creationCode = type(ExperimentERC20).creationCode; - - // Deploy EXP token - // Hardcode name and symbol to "EXP" - bytes memory args = abi.encode("EXP", "EXP", 1 ether); - address expToken = deployContractWithCreate2(chainId, creationCode, args, "ExpToken"); - - // Mint initial tokens to the configured minter address - ExperimentERC20(payable(expToken)).mint(config.expMinterAddress, config.expMintAmount); - - console.log(" EXP Name/Symbol: EXP"); - console.log(" EXP Address:", expToken); - saveDeployedContract(chainId, "ExpToken", expToken); - deployed.expToken = expToken; - - // Deploy EXP2 token - // Hardcode name and symbol to "EXP2" - bytes memory args2 = abi.encode("EXP2", "EXP2", 1 ether); - address exp2Token = deployContractWithCreate2(chainId, creationCode, args2, "Exp2Token"); - - // Mint initial tokens to the configured minter address (same as EXP) - ExperimentERC20(payable(exp2Token)).mint(config.expMinterAddress, config.expMintAmount); - - console.log(" EXP2 Name/Symbol: EXP2"); - console.log(" EXP2 Address:", exp2Token); - console.log(" Minter:", config.expMinterAddress); - console.log(" Mint Amount (each):", config.expMintAmount); - saveDeployedContract(chainId, "Exp2Token", exp2Token); - deployed.exp2Token = exp2Token; } } diff --git a/deploy/FundSigners.s.sol b/deploy/FundSigners.s.sol index 42618c6e..02820e27 100644 --- a/deploy/FundSigners.s.sol +++ b/deploy/FundSigners.s.sol @@ -2,14 +2,12 @@ pragma solidity ^0.8.23; import {Script, console} from "forge-std/Script.sol"; -import {Config} from "forge-std/Config.sol"; +import {stdToml} from "forge-std/StdToml.sol"; // SimpleFunder interface for setting gas wallets interface ISimpleFunder { function setGasWallet(address[] memory wallets, bool isGasWallet) external; function gasWallets(address) external view returns (bool); - function setOrchestrators(address[] memory ocs, bool val) external; - function orchestrators(address) external view returns (bool); } /** @@ -48,7 +46,9 @@ interface ISimpleFunder { * --private-key $PRIVATE_KEY \ * "[84532]" 5 */ -contract FundSigners is Script, Config { +contract FundSigners is Script { + using stdToml for string; + /** * @notice Configuration for funding on a specific chain */ @@ -59,7 +59,6 @@ contract FundSigners is Script, Config { uint256 targetBalance; address simpleFunderAddress; uint256 defaultNumSigners; - address[] supportedOrchestrators; } /** @@ -88,18 +87,18 @@ contract FundSigners is Script, Config { uint256 private totalEthDistributed; uint256 private chainsProcessed; + string internal configContent; string internal configPath = "/deploy/config.toml"; /** * @notice Fund all configured chains with default number of signers */ function run() external { - // Load configuration and setup forks - string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); - _loadConfigAndForks(fullConfigPath, false); - - // Get all chain IDs from configuration - uint256[] memory chainIds = config.getChainIds(); + // Default to common testnets + uint256[] memory chainIds = new uint256[](3); + chainIds[0] = 11155111; // Sepolia + chainIds[1] = 84532; // Base Sepolia + chainIds[2] = 11155420; // Optimism Sepolia uint256 numSigners = 10; // Default execute(chainIds, numSigners); } @@ -109,10 +108,6 @@ contract FundSigners is Script, Config { * @param chainIds Array of chain IDs to fund */ function run(uint256[] memory chainIds) external { - // Load configuration and setup forks - string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); - _loadConfigAndForks(fullConfigPath, false); - uint256 numSigners = 10; // Default execute(chainIds, numSigners); } @@ -123,10 +118,6 @@ contract FundSigners is Script, Config { * @param numSigners Number of signers to fund (starting from index 0) */ function run(uint256[] memory chainIds, uint256 numSigners) external { - // Load configuration and setup forks - string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); - _loadConfigAndForks(fullConfigPath, false); - execute(chainIds, numSigners); } @@ -137,6 +128,9 @@ contract FundSigners is Script, Config { console.log("=== Signer Funding Script (TOML Config) ==="); console.log("Number of signers to fund:", numSigners); + // Load configuration + loadConfig(); + // Get mnemonic from environment string memory mnemonic = vm.envString("GAS_SIGNER_MNEMONIC"); require(bytes(mnemonic).length > 0, "GAS_SIGNER_MNEMONIC not set"); @@ -217,9 +211,6 @@ contract FundSigners is Script, Config { if (config.simpleFunderAddress != address(0) && config.simpleFunderAddress.code.length > 0) { setGasWalletsInSimpleFunder(config.simpleFunderAddress, signers); - setOrchestratorsInSimpleFunder( - config.simpleFunderAddress, config.supportedOrchestrators - ); } vm.stopBroadcast(); @@ -421,41 +412,6 @@ contract FundSigners is Script, Config { ); } - /** - * @notice Set orchestrators in SimpleFunder contract - */ - function setOrchestratorsInSimpleFunder(address simpleFunder, address[] memory orchestrators) - internal - { - if (orchestrators.length == 0) { - console.log("No orchestrators configured, skipping orchestrator setup"); - return; - } - - ISimpleFunder funder = ISimpleFunder(simpleFunder); - - console.log("\nSetting orchestrators in SimpleFunder:"); - console.log(" SimpleFunder address:", simpleFunder); - console.log( - string.concat(" Setting ", vm.toString(orchestrators.length), " orchestrators...") - ); - - for (uint256 i = 0; i < orchestrators.length; i++) { - console.log( - string.concat( - " Orchestrator ", vm.toString(i), ": ", vm.toString(orchestrators[i]) - ) - ); - } - - funder.setOrchestrators(orchestrators, true); - console.log( - string.concat( - " Successfully set all ", vm.toString(orchestrators.length), " orchestrators" - ) - ); - } - /** * @notice Derive signer addresses from mnemonic */ @@ -475,29 +431,45 @@ contract FundSigners is Script, Config { return signers; } + /** + * @notice Load configuration from TOML + */ + function loadConfig() internal { + string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); + configContent = vm.readFile(fullConfigPath); + } + /** * @notice Get chain funding configuration */ function getChainFundingConfig(uint256 chainId) internal returns (ChainFundingConfig memory) { - // Switch to the fork for this chain (already created by _loadConfigAndForks) - vm.selectFork(forkOf[chainId]); - - ChainFundingConfig memory chainConfig; - chainConfig.chainId = chainId; - chainConfig.name = config.get(chainId, "name").toString(); - chainConfig.isTestnet = config.get(chainId, "is_testnet").toBool(); - chainConfig.targetBalance = config.get(chainId, "target_balance").toUint256(); - chainConfig.simpleFunderAddress = config.get(chainId, "simple_funder_deployed").toAddress(); + // Create fork and read configuration from fork variables + string memory rpcUrl = vm.envString(string.concat("RPC_", vm.toString(chainId))); + uint256 forkId = vm.createFork(rpcUrl); + vm.selectFork(forkId); + + ChainFundingConfig memory config; + config.chainId = chainId; + config.name = vm.readForkString("name"); + config.isTestnet = vm.readForkBool("is_testnet"); + config.targetBalance = vm.readForkUint("target_balance"); + + // Try to read SimpleFunder address - may not be deployed on all chains + try vm.readForkAddress("simple_funder_address") returns (address addr) { + config.simpleFunderAddress = addr; + } catch { + // SimpleFunder not configured for this chain + config.simpleFunderAddress = address(0); + } // Try to read default number of signers - uint256 numSigners = config.get(chainId, "default_num_signers").toUint256(); - chainConfig.defaultNumSigners = numSigners == 0 ? 10 : numSigners; // Default fallback - - // Read supported orchestrators - required field - chainConfig.supportedOrchestrators = - config.get(chainId, "supported_orchestrators").toAddressArray(); + try vm.readForkUint("default_num_signers") returns (uint256 num) { + config.defaultNumSigners = num; + } catch { + config.defaultNumSigners = 10; // Default fallback + } - return chainConfig; + return config; } /** diff --git a/deploy/README.md b/deploy/README.md index b23c7606..cb27d1ef 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -1,6 +1,6 @@ # Deployment System -Unified deployment and configuration system for the Ithaca Account Abstraction System. +Unified deployment and configuration system for the Ithaca Account Abstraction System. We use a single TOML config for fast and easy scripting. ## Available Scripts @@ -55,90 +55,69 @@ optimism-sepolia = { key = "${ETHERSCAN_API_KEY}" } ## Configuration Structure -All configuration is in `deploy/config.toml` using the StdConfig format: +All configuration is in `deploy/config.toml`: ```toml -[base-sepolia] -endpoint_url = "${RPC_84532}" +[profile.deployment] +registry_path = "deploy/registry/" -[base-sepolia.bool] -is_testnet = true +[forks.base-sepolia] +rpc_url = "${RPC_84532}" -[base-sepolia.address] +[forks.base-sepolia.vars] # Chain identification -funder_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" -funder_signer = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" -settler_owner = "0x0000000000000000000000000000000000000004" -l0_settler_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" -l0_settler_signer = "0x0000000000000000000000000000000000000006" -layerzero_endpoint = "0x6EDCE65403992e310A62460808c4b910D972f10f" -simple_funder_address = "0x09F6eF9525efAdb6167dFe71fFcfbE306De07988" -layerzero_settler_address = "0xd71d3c3ff2249A67cEa12030b20E66734fB1f833" -layerzero_send_uln302 = "0xC1868e054425D378095A003EcbA3823a5D0135C9" -layerzero_receive_uln302 = "0x12523de19dc41c91F7d2093E0CFbB76b17012C8d" -dvn_layerzero_labs = "0xe1a12515F9AB2764b887bF60B923Ca494EBbB2d6" -dvn_google_cloud = "0xFc9d8E5d3FaB22fB6E93E9E2C90916E9dCa83Ade" -exp_minter_address = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" -supported_orchestrators = ["0xEd7c1e839381c489Dcd1ED3CE1B0e79DaE714f77"] - -# Deployed contract addresses - automatically written during deployment -orchestrator_deployed = "0xC662Af195CD57bC330552f3E2Be5E03Ef69cB041" -ithaca_account_deployed = "0x49627C39cC7f39f95540C2100f18608f2365a59f" -account_proxy_deployed = "0xD2a48e4635fCB2437d2e482122137F06C8433706" -simulator_deployed = "0x332A5Cd675D9d26c4af3BF618A7175d0D623CABA" -simple_funder_deployed = "0x1ADE5D4CE3183D913791DEcaeaD42Fff193AeF8F" -escrow_deployed = "0xD1c7e21f2a50A2cDDCFaf554b998a800C3C35aD1" -simple_settler_deployed = "0x3291f7Ce832997920874d70d68A8186B388024F5" -layerzero_settler_deployed = "0xBDb45dA9e075a9fCbdf8fAa9d0c93A21b3D8671a" -exp_token_deployed = "0xaeB83430528fB0DeE5E15bF07A5056B6c0b37809" -exp2_token_deployed = "0x246c631Dac318a13023e98aB925499930c9801fB" - -[base-sepolia.uint] chain_id = 84532 +name = "Base Sepolia" +is_testnet = true + +# Contract ownership +pause_authority = "0x..." # Can pause contracts +funder_owner = "0x..." # Owns SimpleFunder +funder_signer = "0x..." # Signs funding operations +settler_owner = "0x..." # Owns SimpleSettler +l0_settler_owner = "0x..." # Owns LayerZeroSettler + +# Deployment configuration +salt = "0x0000..." # CREATE2 salt (SAVE THIS!) +contracts = ["ALL"] # Or specific: ["Orchestrator", "IthacaAccount"] + +# Funding configuration (only needed for Funding Script) +target_balance = 1000000000000000 # Target balance per signer (0.001 ETH) +simple_funder_address = "0x..." # SimpleFunder address +default_num_signers = 10 # Number of signers to fund + +# LayerZero configuration (only needed for ConfigureLayerZero) +layerzero_settler_address = "0x..." +layerzero_endpoint = "0x..." layerzero_eid = 40245 -target_balance = "1000000000000000" # Target balance per signer (0.001 ETH) - must be quoted for large numbers -default_num_signers = 10 # Number of signers to fund -layerzero_confirmations = 1 -layerzero_max_message_size = 10000 -layerzero_optional_dvn_threshold = 0 -exp_mint_amount = "5000000000000000000000" # Amount to mint (in wei) - must be quoted +layerzero_send_uln302 = "0x..." +layerzero_receive_uln302 = "0x..." layerzero_destination_chain_ids = [11155420] - -[base-sepolia.bytes32] -salt = "0x0000000000000000000000000000000000000000000000000000000000005678" # CREATE2 salt (SAVE THIS!) - -[base-sepolia.string] -name = "Base Sepolia" -contracts = ["ALL"] # Or specific: ["Orchestrator", "IthacaAccount"] layerzero_required_dvns = ["dvn_layerzero_labs"] layerzero_optional_dvns = [] -``` - -### Deployed Address Management - -**Automatic Address Writing**: The deployment scripts automatically write deployed contract addresses to the config file during broadcast operations: +layerzero_optional_dvn_threshold = 0 +layerzero_confirmations = 1 +layerzero_max_message_size = 10000 -- ✅ **During `--broadcast`**: Deployed addresses are written to config.toml as `contract_name_deployed = "0x..."` -- ❌ **During simulation** (no `--broadcast`): No config writes occur - safe for testing -- 📍 **Address detection**: Scripts always check actual on-chain state, not config file data -- 🔄 **State synchronization**: Config file stays in sync with actual deployments +dvn_layerzero_labs = "0x..." +dvn_google_cloud = "0x..." +``` ### Available Contracts - **Orchestrator** -- **IthacaAccount** -- **AccountProxy** -- **Simulator** -- **SimpleFunder** +- **IthacaAccount** +- **AccountProxy** +- **Simulator** +- **SimpleFunder** - **Escrow** (Only needed for Interop Chains) - **SimpleSettler** (Only needed for Interop testing) - **LayerZeroSettler** (Only needed for Interop Chains) -- **ExpToken** - Test ERC20 tokens (Testnet only, automatically included with "ALL") -- **ALL** - Deploys all contracts (+ ExpToken on testnets) +- **ALL** - Deploys all contracts -**Dependencies**: -IthacaAccount requires Orchestrator; -AccountProxy requires IthacaAccount; +**Dependencies**: +IthacaAccount requires Orchestrator; +AccountProxy requires IthacaAccount; SimpleFunder requires Orchestrator. ## Quick Start - Complete Workflow @@ -170,7 +149,8 @@ forge script deploy/FundSigners.s.sol:FundSigners \ --private-key $PRIVATE_KEY \ "[84532,11155420]" -# 5. Fund SimpleFunder contract +# 5. Fund SimpleFunder contract +SIMPLE_FUNDER=$(cat deploy/registry/deployment_84532_*.json | jq -r .SimpleFunder) forge script deploy/FundSimpleFunder.s.sol:FundSimpleFunder \ --broadcast --multi --slow \ @@ -194,7 +174,7 @@ forge script deploy/FundSimpleFunder.s.sol:FundSimpleFunder \ forge script deploy/DeployMain.s.sol:DeployMain \ --broadcast --verify --multi --slow \ --sig "run()" \ - --private-key $PRIVATE_KEY + --private-key $PRIVATE_KEY # Deploy to specific chains forge script deploy/DeployMain.s.sol:DeployMain \ @@ -227,8 +207,7 @@ forge script deploy/DeployMain.s.sol:DeployMain \ **Purpose**: Configure LayerZero messaging pathways between chains. -**Prerequisites**: - +**Prerequisites**: - LayerZeroSettler deployed on source and destination chains - Caller must be l0_settler_owner @@ -251,18 +230,15 @@ forge script deploy/ConfigureLayerZeroSettler.s.sol:ConfigureLayerZeroSettler \ **Purpose**: Fund signers and register them as gas wallets in SimpleFunder. -**Prerequisites**: - +**Prerequisites**: - SimpleFunder deployed - Caller must be funder_owner - GAS_SIGNER_MNEMONIC environment variable set **What it does**: - 1. Derives signer addresses from mnemonic 2. Tops up signers below target_balance 3. Registers signers as gas wallets in SimpleFunder -4. Sets configured orchestrators in SimpleFunder ```bash # Fund default number of signers (from config) @@ -305,7 +281,6 @@ forge script deploy/FundSimpleFunder.s.sol:FundSimpleFunder \ ``` **Parameters**: - - SimpleFunder address (same across chains if using CREATE2) - Array of (chainId, tokenAddress, amount) - Use `0x0000000000000000000000000000000000000000` for native ETH @@ -322,36 +297,37 @@ forge script deploy/FundSimpleFunder.s.sol:FundSimpleFunder \ All contracts deploy via Safe Singleton Factory (`0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7`) for deterministic addresses. **Key Points**: - - Same salt + same bytecode = same address on every chain - Addresses can be predicted before deployment - **⚠️ SAVE YOUR SALT VALUES** - Required for deploying to same addresses on new chains -- **Deployment decisions based on on-chain state** - Scripts check actual deployed contracts, not config file data -## Adding New Chains +## Registry Files -1. Add configuration to `deploy/config.toml`: +Deployment addresses are saved in `deploy/registry/deployment_{chainId}_{salt}.json`: -```toml -[new-chain] -endpoint_url = "${RPC_CHAINID}" +```json +{ + "Orchestrator": "0xb33adF2c2257a94314d408255aC843fd53B1a7e1", + "IthacaAccount": "0x5a87ef243CDA70a855828d4989Fad61B56A467d3", + "AccountProxy": "0x4ACD713815fbb363a89D9Ff046C56cEdC7EF3ad7", + "SimpleFunder": "0xA47C5C472449979a2F37dF2971627cD6587bADb8" +} +``` -[new-chain.bool] -is_testnet = true +Registry files are for reference only - deployment decisions are based on on-chain state. -[new-chain.address] -funder_owner = "0x..." -# ... all required fields +## Adding New Chains -[new-chain.uint] -chain_id = CHAINID -# ... all required fields +1. Add configuration to `deploy/config.toml`: -[new-chain.bytes32] -salt = "0x0000000000000000000000000000000000000000000000000000000000005678" +```toml +[forks.new-chain] +rpc_url = "${RPC_CHAINID}" -[new-chain.string] +[forks.new-chain.vars] +chain_id = CHAINID name = "Chain Name" +# ... all required fields contracts = ["ALL"] ``` @@ -376,22 +352,18 @@ forge script deploy/DeployMain.s.sol:DeployMain \ ### Common Issues **"No chains found in configuration"** - - Verify config.toml has properly configured chains - Check RPC URLs are set for target chains **"Safe Singleton Factory not deployed"** - - Factory must exist at `0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7` - Most major chains have this deployed **Contract already deployed** - - Normal for CREATE2 - existing contracts are skipped - Change salt value to deploy to new addresses **RPC errors** - - Verify RPC URLs are correct and accessible - Check rate limits on public RPCs - Consider paid RPC services for production @@ -404,145 +376,38 @@ forge script deploy/DeployMain.s.sol:DeployMain \ 4. **Commit registry files** - Provides deployment history 5. **Use `--multi --slow`** - Ensures proper multi-chain ordering 6. **Verify while deploying** - Use `--verify` flag -7. **Large numbers must be quoted in TOML** - Use `"1000000000000000000"` not `1000000000000000000` ## Configuration Field Reference -| Field | Used By | Purpose | -| --------------------------------------- | ------------------------------ | ------------------------------------------------ | -| `chain_id`, `name`, `is_testnet` | All scripts | Chain identification | -| `pause_authority` | DeployMain | Contract pause permissions | -| `funder_owner`, `funder_signer` | DeployMain, FundSigners | SimpleFunder control | -| `settler_owner` | DeployMain | SimpleSettler ownership | -| `l0_settler_owner` | DeployMain, ConfigureLayerZero | LayerZeroSettler ownership | -| `salt` | DeployMain | CREATE2 deployment salt | -| `contracts` | DeployMain | Which contracts to deploy | -| `target_balance` | FundSigners | Minimum signer balance | -| `simple_funder_address` | FundSigners, FundSimpleFunder | SimpleFunder location | -| `default_num_signers` | FundSigners | Number of signers | -| `supported_orchestrators` | FundSigners | Orchestrator addresses to enable in SimpleFunder | -| `layerzero_*` fields | ConfigureLayerZeroSettler | LayerZero configuration | -| `exp_minter_address`, `exp_mint_amount` | DeployMain (testnet) | ExpToken deployment | -| `*_deployed` fields | All scripts | Auto-written deployed contract addresses | - -## ExpToken Deployment (Testnets Only) - -### Automatic ExpToken Deployment +| Field | Used By | Purpose | +|-------|---------|---------| +| `chain_id`, `name`, `is_testnet` | All scripts | Chain identification | +| `pause_authority` | DeployMain | Contract pause permissions | +| `funder_owner`, `funder_signer` | DeployMain, FundSigners | SimpleFunder control | +| `settler_owner` | DeployMain | SimpleSettler ownership | +| `l0_settler_owner` | DeployMain, ConfigureLayerZero | LayerZeroSettler ownership | +| `salt` | DeployMain | CREATE2 deployment salt | +| `contracts` | DeployMain | Which contracts to deploy | +| `target_balance` | FundSigners | Minimum signer balance | +| `simple_funder_address` | FundSigners, FundSimpleFunder | SimpleFunder location | +| `default_num_signers` | FundSigners | Number of signers | +| `layerzero_*` fields | ConfigureLayerZeroSettler | LayerZero configuration | -**Purpose**: Deploy EXP and EXP2 test tokens automatically on testnet chains. +## Test Token Deployment -**Behavior**: +### DeployEXP - Test ERC20 Token -- **Testnets** (`is_testnet = true`): ExpToken automatically included when using `["ALL"]` contracts -- **Production** (`is_testnet = false`): ExpToken never deployed, regardless of configuration -- **Two tokens deployed**: "EXP" and "EXP2" with hardcoded names -- **Same configuration**: Both tokens use the same minter address and mint amount - -### Configuration Requirements - -For testnet chains, **both fields are required** (deployment will fail if missing): - -```toml -[testnet-name.bool] -is_testnet = true - -[testnet-name.address] -exp_minter_address = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" # REQUIRED - -[testnet-name.uint] -exp_mint_amount = "5000000000000000000000" # REQUIRED (5000 tokens in wei) - -[testnet-name.string] -contracts = ["ALL"] # ExpToken automatically included for testnets -``` - -### Deployment Details - -**Two tokens are deployed**: - -1. **EXP Token**: Name and symbol "EXP" -2. **EXP2 Token**: Name and symbol "EXP2" - -**Both tokens**: - -- Use CREATE2 for deterministic addresses -- Mint `exp_mint_amount` tokens to `exp_minter_address` -- Are deployed at the end of the contract deployment sequence -- Are saved to registry files as "ExpToken" and "Exp2Token" - -### Examples - -**Base Sepolia (Testnet)**: - -```toml -[base-sepolia.bool] -is_testnet = true - -[base-sepolia.address] -exp_minter_address = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" - -[base-sepolia.uint] -exp_mint_amount = "5000000000000000000000" - -[base-sepolia.string] -contracts = ["ALL"] # Deploys 8 core contracts + ExpToken -``` - -## Supporting Bash Scripts - -### Overview - -There are two main scripts that handle multi-chain deployments and configuration verification: - -- **`deploy/execute_config.sh`** - Brings up the whole environment, by calling all scripts correctly. -- **`deploy/verify_config.sh`** - Verifies that the values in the config.toml are all set and configured correctly. - -### Usage - -#### Execute Deployment - -```bash -# Deploy to specific chains -./deploy/execute_config.sh 84532 11155420 - -# Deploy to all chains in config.toml -./deploy/execute_config.sh -``` - -The script performs these steps: -1. Validates configuration for selected chains -2. Deploys core contracts (IthacaAccount, SimpleFunder, SimpleSettler, etc.) -3. Configures LayerZero cross-chain messaging -4. Funds signer accounts with gas tokens -5. Verifies all deployments match configuration - -#### Verify Configuration +**Purpose**: Deploy a simple test ERC20 token for testing. +**Usage**: ```bash -# Verify specific chains -./deploy/verify_config.sh 84532 11155420 - -# Verify all chains in config.toml -./deploy/verify_config.sh +# Deploy test token to a specific chain +forge script deploy/DeployEXP.s.sol:DeployEXP \ + --broadcast \ + --sig "run()" \ + --private-key $PRIVATE_KEY \ + --rpc-url $RPC_ ``` -The script checks: -- Required environment variables (RPC URLs, private keys) -- Contract deployment addresses match config.toml -- Signer accounts have sufficient gas balances -- LayerZero endpoints and DVN configurations -- Cross-chain pathway configurations - -### Configuration - -Both scripts read from `deploy/config.toml` which defines per-chain: -- RPC endpoints and chain metadata -- Contract addresses and owners -- LayerZero endpoint configurations -- Cross-chain destination mappings -- Gas funding amounts - -Environment variables required: -- `PRIVATE_KEY` - Deployer private key -- `GAS_SIGNER_MNEMONIC` - Mnemonic for signer accounts -- `RPC_` - RPC URL for each chain (e.g., `RPC_84532`) \ No newline at end of file +The test token includes basic ERC20 functionality and can be minted by anyone. +It should only be used for testing purposes. \ No newline at end of file diff --git a/deploy/config.toml b/deploy/config.toml index 32d4fd67..0644abd6 100644 --- a/deploy/config.toml +++ b/deploy/config.toml @@ -1,155 +1,136 @@ -[11155111] -endpoint_url = "${RPC_11155111}" +# Ithaca Account Deployment Configuration +# Single source of truth for all deployment and fork configurations +# This file extends foundry.toml and contains both Foundry fork configs and deployment-specific settings -[11155111.bool] -is_testnet = true +# ============================================ +# GLOBAL DEPLOYMENT CONFIGURATION +# ============================================ + +[profile.deployment] +registry_path = "deploy/registry/" + +# ============================================ +# FORK CONFIGURATIONS (for vm.readFork* cheatcodes) +# ============================================ -[11155111.address] +# Sepolia +[forks.sepolia] +rpc_url = "${RPC_11155111}" + +[forks.sepolia.vars] +chain_id = 11155111 +name = "Sepolia" +is_testnet = true +pause_authority = "0x0000000000000000000000000000000000000001" funder_owner = "0x0000000000000000000000000000000000000003" funder_signer = "0x0000000000000000000000000000000000000002" settler_owner = "0x0000000000000000000000000000000000000004" l0_settler_owner = "0x0000000000000000000000000000000000000005" -l0_settler_signer = "0x0000000000000000000000000000000000000006" layerzero_endpoint = "0x6EDCE65403992e310A62460808c4b910D972f10f" +layerzero_eid = 40161 +salt = "0x0000000000000000000000000000000000000000000000000000000000000000" +target_balance = 50000000000000000 # 0.05 ether +# Specify contracts to deploy. Use ["ALL"] to deploy all available contracts +contracts = ["LayerZeroSettler"] +# SimpleFunder configuration +simple_funder_address = "0x2AD8F6a3bB1126a777606eaFa9da9b95530d9597" # TODO: automate write when foundry upgrades +default_num_signers = 10 +# LayerZero configuration (copied from Base Sepolia for testing) +# TODO: Set correct addresses here. layerzero_settler_address = "0x4225041FF3DB1C7d7a1029406bB80C7298767aca" layerzero_send_uln302 = "0xC1868e054425D378095A003EcbA3823a5D0135C9" layerzero_receive_uln302 = "0x12523de19dc41c91F7d2093E0CFbB76b17012C8d" -dvn_layerzero_labs = "0x8eebf8b423B73bFCa51a1Db4B7354AA0bFCA9193" -dvn_google_cloud = "0x8e5a7b5959C5A7C732b87dCE401E07F5819eEC2d" -exp_minter_address = "0x0000000000000000000000000000000000000003" -supported_orchestrators = ["0xEd7c1e839381c489Dcd1ED3CE1B0e79DaE714f77"] -orchestrator_deployed = "0xCa58a1296c5AE231A2963A26E11f722bfe65acd4" -ithaca_account_deployed = "0xC203A827A6DB2d09fbF052f88e7aD794EEbf8928" -account_proxy_deployed = "0x0B5d691F3A98B705756ab196CFe4af97f35e06E4" -simulator_deployed = "0x3306F2DD87887885f4de81aF6BD88b648E467f4e" -simple_funder_deployed = "0xa0BF9a30fD55A45BF09309513734B8194fBE3934" -escrow_deployed = "0x669a3aB5816d1ADdbFdecE2F6B238D323F056248" -simple_settler_deployed = "0xFB0F29c658145DbDb1Bf974C8A342F4E78BF9801" -layerzero_settler_deployed = "0xC93D0142D960F7B33440F09B13917C195e09ffA0" -exp_token_deployed = "0x4D335fa317FB8d5493b83e06161134ec611Bc188" -exp2_token_deployed = "0x0348152303686f2197b24981571FCf8c36c66Cc5" - -[11155111.uint] -chain_id = 11155111 -layerzero_eid = 40161 -target_balance = "50000000000000000" -default_num_signers = 10 +layerzero_destination_chain_ids = [84532, 11155420] +layerzero_required_dvns = ["dvn_layerzero_labs"] # optimism sepolia +layerzero_optional_dvns = [] +layerzero_optional_dvn_threshold = 0 layerzero_confirmations = 1 layerzero_max_message_size = 10000 -layerzero_optional_dvn_threshold = 0 -exp_mint_amount = "1000000000000000000000" -layerzero_destination_chain_ids = [ - 84532, - 11155420, -] - -[11155111.bytes32] -salt = "0xdf08a9fa957a9ac004f84f3a8ab7318a7be55d59948f46497bd3717865295b61" - -[11155111.string] -name = "Sepolia" -contracts = ["ALL"] -layerzero_required_dvns = ["dvn_layerzero_labs"] -layerzero_optional_dvns = [] +# DVN addresses +dvn_layerzero_labs = "0x8eebf8b423B73bFCa51a1Db4B7354AA0bFCA9193" +dvn_google_cloud = "0x8e5a7b5959C5A7C732b87dCE401E07F5819eEC2d" -[84532] -endpoint_url = "${RPC_84532}" +# Base Sepolia +[forks.base-sepolia] +rpc_url = "${RPC_84532}" -[84532.bool] +[forks.base-sepolia.vars] +orchestrator_address = "0x0000000000000000000000000000000000000000" +# Deployment Script +chain_id = 84532 +name = "Base Sepolia" is_testnet = true - -[84532.address] -funder_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" -funder_signer = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" -settler_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" -l0_settler_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" -l0_settler_signer = "0x0000000000000000000000000000000000000006" -layerzero_endpoint = "0x6EDCE65403992e310A62460808c4b910D972f10f" -layerzero_settler_address = "0xd71d3c3ff2249A67cEa12030b20E66734fB1f833" +pause_authority = "0x0000000000000000000000000000000000000001" # Set to separate PAUSE KEY +funder_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" # Set to Master Key +funder_signer = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" # Set to Master Key +settler_owner = "0x0000000000000000000000000000000000000004" # Set to Master Key +l0_settler_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" # Set to Master Key +layerzero_endpoint = "0x6EDCE65403992e310A62460808c4b910D972f10f" # Get from LayerZero docs +layerzero_eid = 40245 # Get from LayerZero docs +salt = "0x00000000000000000000000000000000000000000000000000000000000000c7" # Almost alwayse be bytes32(0) +# Specify contracts to deploy. Use ["ALL"] to deploy all available contracts +contracts = ["ALL"] +# Funding Script +target_balance = 1000000000000000 # 0.001 ether + +# SimpleFunder configuration +simple_funder_address = "0x5e6c8c24A53275e2C6C3DC5f1c5C027Ec13439Ba" +default_num_signers = 10 +# LayerZero configuration +layerzero_settler_address = "0x4225041FF3DB1C7d7a1029406bB80C7298767aca" layerzero_send_uln302 = "0xC1868e054425D378095A003EcbA3823a5D0135C9" layerzero_receive_uln302 = "0x12523de19dc41c91F7d2093E0CFbB76b17012C8d" -dvn_layerzero_labs = "0xe1a12515F9AB2764b887bF60B923Ca494EBbB2d6" -dvn_google_cloud = "0xFc9d8E5d3FaB22fB6E93E9E2C90916E9dCa83Ade" -exp_minter_address = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" -supported_orchestrators = ["0xEd7c1e839381c489Dcd1ED3CE1B0e79DaE714f77"] -orchestrator_deployed = "0xcA91Dba3dab46478FC466ef76491AaB78dbC2d0f" -ithaca_account_deployed = "0x1E7a350c76CCd750470930B5026fEa26F44cf937" -account_proxy_deployed = "0xDe938C3642205782d52535d12eC9dFb4D1bC832b" -simulator_deployed = "0xb100Dc6e1BA9e54965fBB865F65Ac42b28BC25E8" -simple_funder_deployed = "0x57C3563C06CeDda15F7622A83a1BdBa84125351D" -escrow_deployed = "0xCd075ceb5Cd463a9233a8085fc915767139F655c" -simple_settler_deployed = "0x5aE547Dc236c31d30f19a5B9040992728Ac441A0" -layerzero_settler_deployed = "0x23D18C5b5Df22fDcf6dDE73b0f1a76ee5761d206" -exp_token_deployed = "0xa8071DA5e994cB8e3eB56CaD0FBB6ca424dD8dc0" -exp2_token_deployed = "0x61727778216127D0843A99A3e91e99C27e9f3BC7" - -[84532.uint] -chain_id = 84532 -layerzero_eid = 40245 -target_balance = "1000000000000000" -default_num_signers = 10 -layerzero_confirmations = 1 -layerzero_max_message_size = 10000 -layerzero_optional_dvn_threshold = 0 -exp_mint_amount = "5000000000000000000000" -layerzero_destination_chain_ids = [11155420] - -[84532.bytes32] -salt = "0xdf08a9fa957a9ac004f84f3a8ab7318a7be55d59948f46497bd3717865295b61" - -[84532.string] -name = "Base Sepolia" -contracts = ["ALL"] +layerzero_destination_chain_ids = [11155420] # optimism sepolia layerzero_required_dvns = ["dvn_layerzero_labs"] layerzero_optional_dvns = [] +layerzero_optional_dvn_threshold = 0 +layerzero_confirmations = 1 +layerzero_max_message_size = 10000 +# DVN addresses +dvn_layerzero_labs = "0xe1a12515F9AB2764b887bF60B923Ca494EBbB2d6" +dvn_google_cloud = "0xFc9d8E5d3FaB22fB6E93E9E2C90916E9dCa83Ade" -[11155420] -endpoint_url = "${RPC_11155420}" +# Optimism Sepolia +[forks.optimism-sepolia] +rpc_url = "${RPC_11155420}" -[11155420.bool] +[forks.optimism-sepolia.vars] +chain_id = 11155420 +name = "Optimism Sepolia" is_testnet = true - -[11155420.address] -funder_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" -funder_signer = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" -settler_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" +pause_authority = "0x0000000000000000000000000000000000000001" +funder_owner = "0x0000000000000000000000000000000000000003" +funder_signer = "0x0000000000000000000000000000000000000002" +settler_owner = "0x0000000000000000000000000000000000000004" l0_settler_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" -l0_settler_signer = "0x0000000000000000000000000000000000000006" layerzero_endpoint = "0x6EDCE65403992e310A62460808c4b910D972f10f" -layerzero_settler_address = "0x9F610182BD096ab7734C1365A44f48dD0A574d0D" +layerzero_eid = 40232 +salt = "0x0000000000000000000000000000000000000000000000000000000000000001" +target_balance = 10000000000000000 # 0.01 ether +# Specify contracts to deploy. Use ["ALL"] to deploy all available contracts +contracts = ["ALL"] # Deploy all contracts +# SimpleFunder configuration +simple_funder_address = "0x2AD8F6a3bB1126a777606eaFa9da9b95530d9597" + +default_num_signers = 10 +# LayerZero configuration +layerzero_settler_address = "0x4225041FF3DB1C7d7a1029406bB80C7298767aca" layerzero_send_uln302 = "0xB31D2cb502E25B30C651842C7C3293c51Fe6d16f" layerzero_receive_uln302 = "0x9284fd59B95b9143AF0b9795CAC16eb3C723C9Ca" +layerzero_destination_chain_ids = [84532, 11155111] +layerzero_required_dvns = ["dvn_layerzero_labs"] +layerzero_optional_dvns = [] +layerzero_optional_dvn_threshold = 0 +layerzero_confirmations = 1 +layerzero_max_message_size = 10000 +# DVN addresses dvn_layerzero_labs = "0x28B6140ead70cb2Fb669705b3598ffB4BEaA060b" dvn_google_cloud = "0x28b8B8Ea5C695EFa657B6b86a4a8E90ccbA93E9e" -exp_minter_address = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" -supported_orchestrators = ["0xEd7c1e839381c489Dcd1ED3CE1B0e79DaE714f77"] -orchestrator_deployed = "0xcA91Dba3dab46478FC466ef76491AaB78dbC2d0f" -ithaca_account_deployed = "0x1E7a350c76CCd750470930B5026fEa26F44cf937" -account_proxy_deployed = "0xDe938C3642205782d52535d12eC9dFb4D1bC832b" -simulator_deployed = "0xb100Dc6e1BA9e54965fBB865F65Ac42b28BC25E8" -simple_funder_deployed = "0x57C3563C06CeDda15F7622A83a1BdBa84125351D" -escrow_deployed = "0xCd075ceb5Cd463a9233a8085fc915767139F655c" -simple_settler_deployed = "0x5aE547Dc236c31d30f19a5B9040992728Ac441A0" -layerzero_settler_deployed = "0x23D18C5b5Df22fDcf6dDE73b0f1a76ee5761d206" -exp_token_deployed = "0xa8071DA5e994cB8e3eB56CaD0FBB6ca424dD8dc0" -exp2_token_deployed = "0x61727778216127D0843A99A3e91e99C27e9f3BC7" -[11155420.uint] -chain_id = 11155420 -layerzero_eid = 40232 -target_balance = "1000000000000000" -default_num_signers = 10 -layerzero_confirmations = 1 -layerzero_max_message_size = 10000 -layerzero_optional_dvn_threshold = 0 -exp_mint_amount = "2000000000000000000000" -layerzero_destination_chain_ids = [84532] -[11155420.bytes32] -salt = "0xdf08a9fa957a9ac004f84f3a8ab7318a7be55d59948f46497bd3717865295b61" -[11155420.string] -name = "Optimism Sepolia" -contracts = ["ALL"] -layerzero_required_dvns = ["dvn_layerzero_labs"] -layerzero_optional_dvns = [] +# Master Key has gas funds on all chains. +# 1. Deployment Key ( Master Key ) +# 2. Funder Owner Key ( Master Key ) +# 3. Gas Signers ( Funding is included in the fund signers script + setting them as gas wallets ) +# 4. Simple Funder Address ( Funds need to come from GK after bridging, no automation ) diff --git a/deploy/execute_config.sh b/deploy/execute_config.sh deleted file mode 100755 index 8e459b82..00000000 --- a/deploy/execute_config.sh +++ /dev/null @@ -1,486 +0,0 @@ -#!/bin/bash - -# Comprehensive deployment testing script for multiple chains -# This script deploys contracts, configures LayerZero, and funds signers -# -# Usage: bash deploy/test_deployment.sh [chain_id1] [chain_id2] ... -# -# Examples: -# bash deploy/test_deployment.sh # Deploy to all chains in config.toml -# bash deploy/test_deployment.sh 84532 # Deploy only to Base Sepolia -# bash deploy/test_deployment.sh 84532 11155420 # Deploy to Base Sepolia and Optimism Sepolia -# bash deploy/test_deployment.sh 11155111 84532 11155420 # Deploy to all three chains - -set -e # Exit on any error - -# Color codes for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Function to print colored output -log_info() { - echo -e "${GREEN}[INFO]${NC} $1" -} - -log_error() { - echo -e "${RED}[ERROR]${NC} $1" -} - -log_warning() { - echo -e "${YELLOW}[WARNING]${NC} $1" -} - -# Function to check if a command succeeded -check_success() { - if [ $? -eq 0 ]; then - log_info "$1 succeeded" - else - log_error "$1 failed" - exit 1 - fi -} - -# Function to get all chain IDs from config.toml -get_all_chains() { - grep '^\[' deploy/config.toml | grep -v '\.' | sed 's/\[//g' | sed 's/\]//g' -} - -# Parse command-line arguments -REQUESTED_CHAINS=() -if [ $# -gt 0 ]; then - # User provided specific chain IDs to deploy to - REQUESTED_CHAINS=("$@") - log_info "Requested deployment for chain IDs: ${REQUESTED_CHAINS[*]}" -else - # Get all chains from config.toml - REQUESTED_CHAINS=($(get_all_chains)) - log_info "No specific chains requested, will deploy to all chains in config.toml: ${REQUESTED_CHAINS[*]}" -fi - -# Start deployment process -log_info "Starting comprehensive deployment test" -if [ ${#REQUESTED_CHAINS[@]} -gt 0 ]; then - log_info "Deploying to chains: ${REQUESTED_CHAINS[*]}" -fi -echo "========================================================================" - -# Step 1: Load environment variables -log_info "Loading environment variables from .env" -if [ ! -f .env ]; then - log_error ".env file not found!" - exit 1 -fi - -source .env - -# Step 2: Validate required environment variables -log_info "Validating required environment variables" - -if [ -z "$PRIVATE_KEY" ]; then - log_error "PRIVATE_KEY is not set in .env" - exit 1 -fi - -# Check RPC URLs for requested chains -for chain_id in "${REQUESTED_CHAINS[@]}"; do - RPC_VAR="RPC_${chain_id}" - if [ -z "${!RPC_VAR}" ]; then - log_error "$RPC_VAR is not set in .env for chain $chain_id" - log_info "Please set $RPC_VAR in your .env file" - exit 1 - fi -done - -if [ -z "$GAS_SIGNER_MNEMONIC" ]; then - log_warning "GAS_SIGNER_MNEMONIC is not set - FundSigners may fail" -fi - -log_info "All required environment variables are set" -echo "" - -# Step 3: Generate random salt for new deployment addresses -log_info "Generating random salt for new contract addresses" - -# Generate a random 32-byte hex string for salt -RANDOM_SALT="0x$(openssl rand -hex 32)" -log_info "Generated salt: $RANDOM_SALT" - -# Update salt in config.toml for all requested chains -for chain_id in "${REQUESTED_CHAINS[@]}"; do - if grep -q "^\[${chain_id}\.bytes32\]" deploy/config.toml; then - # Get chain name if available - CHAIN_NAME=$(sed -n "/^\[${chain_id}\.string\]/,/^\[/p" deploy/config.toml | grep "^name" | cut -d'"' -f2) - if [ -z "$CHAIN_NAME" ]; then - CHAIN_NAME="Chain $chain_id" - fi - - log_info "Updating salt for $CHAIN_NAME ($chain_id) in config.toml" - sed -i.bak "/^\[${chain_id}\.bytes32\]/,/^\[/ s/^salt = .*/salt = \"$RANDOM_SALT\"/" deploy/config.toml - else - log_warning "No bytes32 section found for chain $chain_id, skipping salt update" - fi -done - -# Clean up backup files -rm -f deploy/config.toml.bak - -log_info "Salt updated in config.toml for all requested chains" -log_warning "⚠️ IMPORTANT: Save this salt value if you need to deploy to the same addresses on other chains: $RANDOM_SALT" -echo "" - -# Step 4: Deploy contracts to Base Sepolia and Optimism Sepolia -echo "========================================================================" -# Build chain array string for forge script -CHAIN_ARRAY="[" -for i in "${!REQUESTED_CHAINS[@]}"; do - if [ $i -gt 0 ]; then - CHAIN_ARRAY="${CHAIN_ARRAY}," - fi - CHAIN_ARRAY="${CHAIN_ARRAY}${REQUESTED_CHAINS[$i]}" -done -CHAIN_ARRAY="${CHAIN_ARRAY}]" - -log_info "STEP 1: Deploying contracts to chains: $CHAIN_ARRAY" -echo "========================================================================" - -forge script deploy/DeployMain.s.sol:DeployMain \ - --broadcast --multi --slow \ - --sig "run(uint256[])" "$CHAIN_ARRAY" \ - --private-key $PRIVATE_KEY \ - -vvv - -check_success "Contract deployment" -echo "" - -# Step 5: Configure LayerZero for cross-chain communication -echo "========================================================================" -log_info "STEP 2: Configuring LayerZero for cross-chain communication" -echo "========================================================================" - -# Note: Using the same PRIVATE_KEY as requested (instead of L0_SETTLER_OWNER_PK) -forge script deploy/ConfigureLayerZeroSettler.s.sol:ConfigureLayerZeroSettler \ - --broadcast --multi --slow \ - --sig "run(uint256[])" "$CHAIN_ARRAY" \ - --private-key $PRIVATE_KEY \ - -vvv - -check_success "LayerZero configuration" -echo "" - -# Base Sepolia LayerZero verification -if [ ! -z "$LAYERZERO_SETTLER_BASE" ]; then - log_info "Base Sepolia LayerZero configuration:" - - # Get LayerZero endpoint from config - LZ_ENDPOINT_BASE=$(sed -n "/^\[84532\.address\]/,/^\[/p" deploy/config.toml | grep "layerzero_endpoint" | cut -d'"' -f2) - LZ_EID_BASE=$(sed -n "/^\[84532\.uint\]/,/^\[/p" deploy/config.toml | grep "layerzero_eid" | awk -F' = ' '{print $2}') - LZ_SEND_ULN_BASE=$(sed -n "/^\[84532\.address\]/,/^\[/p" deploy/config.toml | grep "layerzero_send_uln302" | cut -d'"' -f2) - LZ_RECEIVE_ULN_BASE=$(sed -n "/^\[84532\.address\]/,/^\[/p" deploy/config.toml | grep "layerzero_receive_uln302" | cut -d'"' -f2) - - # Check if endpoint is set on LayerZeroSettler - CURRENT_ENDPOINT=$(cast call $LAYERZERO_SETTLER_BASE "endpoint()(address)" --rpc-url $RPC_84532 2>/dev/null || echo "0x0000000000000000000000000000000000000000") - - if [ "$CURRENT_ENDPOINT" == "$LZ_ENDPOINT_BASE" ]; then - log_info " ✓ Endpoint correctly set to $LZ_ENDPOINT_BASE" - else - log_error " ✗ Endpoint mismatch: Expected $LZ_ENDPOINT_BASE, got $CURRENT_ENDPOINT" - fi - - # Check L0SettlerSigner - LZ_SIGNER=$(cast call $LAYERZERO_SETTLER_BASE "l0SettlerSigner()(address)" --rpc-url $RPC_84532 2>/dev/null || echo "0x0000000000000000000000000000000000000000") - log_info " L0SettlerSigner: $LZ_SIGNER" -fi - -# Optimism Sepolia LayerZero verification -if [ ! -z "$LAYERZERO_SETTLER_OP" ]; then - log_info "Optimism Sepolia LayerZero configuration:" - - # Get LayerZero endpoint from config - these are in the address section - LZ_ENDPOINT_OP=$(sed -n "/^\[11155420\.address\]/,/^\[/p" deploy/config.toml | grep "layerzero_endpoint" | cut -d'"' -f2) - LZ_EID_OP=$(sed -n "/^\[11155420\.uint\]/,/^\[/p" deploy/config.toml | grep "layerzero_eid" | awk -F' = ' '{print $2}') - LZ_SEND_ULN_OP=$(sed -n "/^\[11155420\.address\]/,/^\[/p" deploy/config.toml | grep "layerzero_send_uln302" | cut -d'"' -f2) - LZ_RECEIVE_ULN_OP=$(sed -n "/^\[11155420\.address\]/,/^\[/p" deploy/config.toml | grep "layerzero_receive_uln302" | cut -d'"' -f2) - - # Check if endpoint is set on LayerZeroSettler - CURRENT_ENDPOINT=$(cast call $LAYERZERO_SETTLER_OP "endpoint()(address)" --rpc-url $RPC_11155420 2>/dev/null || echo "0x0000000000000000000000000000000000000000") - - if [ "$CURRENT_ENDPOINT" == "$LZ_ENDPOINT_OP" ]; then - log_info " ✓ Endpoint correctly set to $LZ_ENDPOINT_OP" - else - log_error " ✗ Endpoint mismatch: Expected $LZ_ENDPOINT_OP, got $CURRENT_ENDPOINT" - fi - - # Check L0SettlerSigner - LZ_SIGNER=$(cast call $LAYERZERO_SETTLER_OP "l0SettlerSigner()(address)" --rpc-url $RPC_11155420 2>/dev/null || echo "0x0000000000000000000000000000000000000000") - log_info " L0SettlerSigner: $LZ_SIGNER" -fi - -# Verify cross-chain pathway configuration -log_info "Cross-chain pathway verification:" - -# Get EIDs for both chains - they are in the uint sections -LZ_EID_BASE=$(sed -n "/^\[84532\.uint\]/,/^\[/p" deploy/config.toml | grep "layerzero_eid" | awk -F' = ' '{print $2}') -LZ_EID_OP=$(sed -n "/^\[11155420\.uint\]/,/^\[/p" deploy/config.toml | grep "layerzero_eid" | awk -F' = ' '{print $2}') - -# Check Base Sepolia -> Optimism Sepolia pathway -if [ ! -z "$LAYERZERO_SETTLER_BASE" ] && [ ! -z "$LZ_EID_OP" ] && [ ! -z "$LZ_ENDPOINT_BASE" ] && [ ! -z "$LZ_SEND_ULN_BASE" ]; then - log_info " Base Sepolia -> Optimism Sepolia pathway:" - - # Check executor configuration using endpoint.getConfig() - # CONFIG_TYPE_EXECUTOR = 1 - EXECUTOR_CONFIG_BYTES=$(cast call $LZ_ENDPOINT_BASE "getConfig(address,address,uint32,uint32)(bytes)" "$LAYERZERO_SETTLER_BASE" "$LZ_SEND_ULN_BASE" "$LZ_EID_OP" "1" --rpc-url $RPC_84532 2>/dev/null || echo "0x") - - if [ "$EXECUTOR_CONFIG_BYTES" != "0x" ] && [ ! -z "$EXECUTOR_CONFIG_BYTES" ]; then - # The executor config is encoded as (uint32 maxMessageSize, address executor) - # We need to decode the bytes - first 32 bytes is maxMessageSize, next 32 bytes is executor address - # Remove 0x prefix and get the executor address (last 40 hex chars of the second 32-byte word) - EXECUTOR_HEX=$(echo "$EXECUTOR_CONFIG_BYTES" | sed 's/0x//' | tail -c 41) - if [ ! -z "$EXECUTOR_HEX" ]; then - EXECUTOR_ADDR="0x$EXECUTOR_HEX" - - if [ "$(echo $EXECUTOR_ADDR | tr '[:upper:]' '[:lower:]')" == "$(echo $LAYERZERO_SETTLER_BASE | tr '[:upper:]' '[:lower:]')" ]; then - log_info " ✓ Executor correctly set to LayerZeroSettler" - else - log_warning " ⚠ Executor not set to LayerZeroSettler (self-execution model)" - fi - else - log_warning " ⚠ Could not parse executor configuration" - fi - else - log_warning " ⚠ Executor configuration not set" - fi - - # Check ULN configuration using endpoint.getConfig() - # CONFIG_TYPE_ULN = 2 - ULN_CONFIG_BYTES=$(cast call $LZ_ENDPOINT_BASE "getConfig(address,address,uint32,uint32)(bytes)" "$LAYERZERO_SETTLER_BASE" "$LZ_SEND_ULN_BASE" "$LZ_EID_OP" "2" --rpc-url $RPC_84532 2>/dev/null || echo "0x") - - if [ "$ULN_CONFIG_BYTES" != "0x" ] && [ ! -z "$ULN_CONFIG_BYTES" ] && [ ${#ULN_CONFIG_BYTES} -gt 10 ]; then - log_info " ✓ ULN configuration is set" - else - log_warning " ⚠ ULN configuration not set" - fi -fi - -# Check Optimism Sepolia -> Base Sepolia pathway -if [ ! -z "$LAYERZERO_SETTLER_OP" ] && [ ! -z "$LZ_EID_BASE" ] && [ ! -z "$LZ_ENDPOINT_OP" ] && [ ! -z "$LZ_SEND_ULN_OP" ]; then - log_info " Optimism Sepolia -> Base Sepolia pathway:" - - # Check executor configuration using endpoint.getConfig() - # CONFIG_TYPE_EXECUTOR = 1 - EXECUTOR_CONFIG_BYTES=$(cast call $LZ_ENDPOINT_OP "getConfig(address,address,uint32,uint32)(bytes)" "$LAYERZERO_SETTLER_OP" "$LZ_SEND_ULN_OP" "$LZ_EID_BASE" "1" --rpc-url $RPC_11155420 2>/dev/null || echo "0x") - - if [ "$EXECUTOR_CONFIG_BYTES" != "0x" ] && [ ! -z "$EXECUTOR_CONFIG_BYTES" ]; then - # The executor config is encoded as (uint32 maxMessageSize, address executor) - # Remove 0x prefix and get the executor address (last 40 hex chars of the second 32-byte word) - EXECUTOR_HEX=$(echo "$EXECUTOR_CONFIG_BYTES" | sed 's/0x//' | tail -c 41) - if [ ! -z "$EXECUTOR_HEX" ]; then - EXECUTOR_ADDR="0x$EXECUTOR_HEX" - - if [ "$(echo $EXECUTOR_ADDR | tr '[:upper:]' '[:lower:]')" == "$(echo $LAYERZERO_SETTLER_OP | tr '[:upper:]' '[:lower:]')" ]; then - log_info " ✓ Executor correctly set to LayerZeroSettler" - else - log_warning " ⚠ Executor not set to LayerZeroSettler (self-execution model)" - fi - else - log_warning " ⚠ Could not parse executor configuration" - fi - else - log_warning " ⚠ Executor configuration not set" - fi - - # Check ULN configuration using endpoint.getConfig() - # CONFIG_TYPE_ULN = 2 - ULN_CONFIG_BYTES=$(cast call $LZ_ENDPOINT_OP "getConfig(address,address,uint32,uint32)(bytes)" "$LAYERZERO_SETTLER_OP" "$LZ_SEND_ULN_OP" "$LZ_EID_BASE" "2" --rpc-url $RPC_11155420 2>/dev/null || echo "0x") - - if [ "$ULN_CONFIG_BYTES" != "0x" ] && [ ! -z "$ULN_CONFIG_BYTES" ] && [ ${#ULN_CONFIG_BYTES} -gt 10 ]; then - log_info " ✓ ULN configuration is set" - else - log_warning " ⚠ ULN configuration not set" - fi -fi - -echo "" - -# Step 3: Fund signers and set them as gas wallets -echo "========================================================================" -log_info "STEP 3: Funding signers and setting them as gas wallets" -echo "========================================================================" - -if [ -z "$GAS_SIGNER_MNEMONIC" ]; then - log_error "Cannot proceed with FundSigners - GAS_SIGNER_MNEMONIC not set" - exit 1 -fi - -forge script deploy/FundSigners.s.sol:FundSigners \ - --broadcast --multi --slow \ - --sig "run(uint256[])" "$CHAIN_ARRAY" \ - --private-key $PRIVATE_KEY \ - -vvv - -check_success "Signer funding" -echo "" - -# Final Step: Run comprehensive verification for all deployed contracts and configurations -echo "========================================================================" -log_info "FINAL VERIFICATION: Running comprehensive verification" -echo "========================================================================" - -log_info "Running verification script for all deployments and configurations..." -bash deploy/verify_config.sh "${REQUESTED_CHAINS[@]}" -check_success "Comprehensive verification" - -echo "" -echo "========================================================================" -log_info "DEPLOYMENT COMPLETED SUCCESSFULLY!" -echo "========================================================================" -log_info "✅ All contracts deployed to requested chains" -log_info "✅ LayerZero configured for cross-chain communication" -log_info "✅ Signers funded and set as gas wallets" -log_info "✅ All verifications passed" -echo "" -log_info "Deployment and configuration completed successfully for chains: ${REQUESTED_CHAINS[*]}" -exit 0 - -# The old verification code below is no longer needed since verify_config.sh handles everything -: ' -# Old verification code starts here - -# Derive signer addresses from mnemonic (first 3 for verification) -log_info "Checking signer balances..." - -# First signer address (derived from the mnemonic) -SIGNER_0="0x33097354Acf259e1fD19fB91159BAE6ccf912Fdb" -SIGNER_1="0x49e1f963ddb4122BD3ccC786eB8F9983dABa8658" -SIGNER_2="0x46C66f82B32f04bf04D05ED92e10b57188BF408A" - -# Get target balance from config -TARGET_BALANCE_BASE=$(sed -n "/^\[84532\.uint\]/,/^\[/p" deploy/config.toml | grep "target_balance" | awk -F' = ' '{print $2}' | tr -d '"') - -# Check balances on Base Sepolia -log_info "Base Sepolia (84532) signer balances (target: $TARGET_BALANCE_BASE wei):" -BALANCE_0_BASE=$(cast balance $SIGNER_0 --rpc-url $RPC_84532 2>/dev/null || echo "0") -BALANCE_1_BASE=$(cast balance $SIGNER_1 --rpc-url $RPC_84532 2>/dev/null || echo "0") -BALANCE_2_BASE=$(cast balance $SIGNER_2 --rpc-url $RPC_84532 2>/dev/null || echo "0") - -# Check if balances meet target -if [ "$BALANCE_0_BASE" -ge "$TARGET_BALANCE_BASE" ]; then - log_info " Signer 0 ($SIGNER_0): $BALANCE_0_BASE wei ✓" -else - log_warning " Signer 0 ($SIGNER_0): $BALANCE_0_BASE wei (below target)" -fi - -if [ "$BALANCE_1_BASE" -ge "$TARGET_BALANCE_BASE" ]; then - log_info " Signer 1 ($SIGNER_1): $BALANCE_1_BASE wei ✓" -else - log_warning " Signer 1 ($SIGNER_1): $BALANCE_1_BASE wei (below target)" -fi - -if [ "$BALANCE_2_BASE" -ge "$TARGET_BALANCE_BASE" ]; then - log_info " Signer 2 ($SIGNER_2): $BALANCE_2_BASE wei ✓" -else - log_warning " Signer 2 ($SIGNER_2): $BALANCE_2_BASE wei (below target)" -fi - -# Get target balance from config -TARGET_BALANCE_OP=$(sed -n "/^\[11155420\.uint\]/,/^\[/p" deploy/config.toml | grep "target_balance" | awk -F' = ' '{print $2}' | tr -d '"') - -# Check balances on Optimism Sepolia -log_info "Optimism Sepolia (11155420) signer balances (target: $TARGET_BALANCE_OP wei):" -BALANCE_0_OP=$(cast balance $SIGNER_0 --rpc-url $RPC_11155420 2>/dev/null || echo "0") -BALANCE_1_OP=$(cast balance $SIGNER_1 --rpc-url $RPC_11155420 2>/dev/null || echo "0") -BALANCE_2_OP=$(cast balance $SIGNER_2 --rpc-url $RPC_11155420 2>/dev/null || echo "0") - -# Check if balances meet target -if [ "$BALANCE_0_OP" -ge "$TARGET_BALANCE_OP" ]; then - log_info " Signer 0 ($SIGNER_0): $BALANCE_0_OP wei ✓" -else - log_warning " Signer 0 ($SIGNER_0): $BALANCE_0_OP wei (below target)" -fi - -if [ "$BALANCE_1_OP" -ge "$TARGET_BALANCE_OP" ]; then - log_info " Signer 1 ($SIGNER_1): $BALANCE_1_OP wei ✓" -else - log_warning " Signer 1 ($SIGNER_1): $BALANCE_1_OP wei (below target)" -fi - -if [ "$BALANCE_2_OP" -ge "$TARGET_BALANCE_OP" ]; then - log_info " Signer 2 ($SIGNER_2): $BALANCE_2_OP wei ✓" -else - log_warning " Signer 2 ($SIGNER_2): $BALANCE_2_OP wei (below target)" -fi - -# Verify gas wallets and orchestrators in SimpleFunder -log_info "Checking SimpleFunder configuration..." - -# Read orchestrator addresses from config.toml -# supported_orchestrators is an array like ["0xAddr1", "0xAddr2"] -# For simplicity, we'll extract the first orchestrator address -ORCHESTRATOR_BASE_CONFIG=$(sed -n "/^\[84532\.address\]/,/^\[/p" deploy/config.toml | grep "supported_orchestrators" | sed 's/.*\["\([^"]*\)".*/\1/') -ORCHESTRATOR_OP_CONFIG=$(sed -n "/^\[11155420\.address\]/,/^\[/p" deploy/config.toml | grep "supported_orchestrators" | sed 's/.*\["\([^"]*\)".*/\1/') - -# For Base Sepolia -if [ ! -z "$SIMPLE_FUNDER_BASE" ]; then - log_info "Base Sepolia SimpleFunder ($SIMPLE_FUNDER_BASE):" - - # Check if signers are gas wallets (using mapping gasWallets(address) => bool) - IS_GAS_WALLET_0=$(cast call $SIMPLE_FUNDER_BASE "gasWallets(address)(bool)" $SIGNER_0 --rpc-url $RPC_84532 2>/dev/null || echo "false") - - if [ "$IS_GAS_WALLET_0" == "true" ]; then - log_info " ✓ Signer 0 is registered as gas wallet" - else - log_warning " ✗ Signer 0 is NOT registered as gas wallet" - fi - - # Check orchestrator configuration (using mapping orchestrators(address) => bool) - if [ ! -z "$ORCHESTRATOR_BASE_CONFIG" ]; then - IS_SUPPORTED=$(cast call $SIMPLE_FUNDER_BASE "orchestrators(address)(bool)" $ORCHESTRATOR_BASE_CONFIG --rpc-url $RPC_84532 2>/dev/null || echo "false") - - if [ "$IS_SUPPORTED" == "true" ]; then - log_info " ✓ Orchestrator $ORCHESTRATOR_BASE_CONFIG is supported" - else - log_warning " ✗ Orchestrator $ORCHESTRATOR_BASE_CONFIG is NOT supported" - fi - fi -fi - -# For Optimism Sepolia -if [ ! -z "$SIMPLE_FUNDER_OP" ]; then - log_info "Optimism Sepolia SimpleFunder ($SIMPLE_FUNDER_OP):" - - # Check if signers are gas wallets - IS_GAS_WALLET_0=$(cast call $SIMPLE_FUNDER_OP "gasWallets(address)(bool)" $SIGNER_0 --rpc-url $RPC_11155420 2>/dev/null || echo "false") - - if [ "$IS_GAS_WALLET_0" == "true" ]; then - log_info " ✓ Signer 0 is registered as gas wallet" - else - log_warning " ✗ Signer 0 is NOT registered as gas wallet" - fi - - # Check orchestrator configuration - if [ ! -z "$ORCHESTRATOR_OP_CONFIG" ]; then - IS_SUPPORTED=$(cast call $SIMPLE_FUNDER_OP "orchestrators(address)(bool)" $ORCHESTRATOR_OP_CONFIG --rpc-url $RPC_11155420 2>/dev/null || echo "false") - - if [ "$IS_SUPPORTED" == "true" ]; then - log_info " ✓ Orchestrator $ORCHESTRATOR_OP_CONFIG is supported" - else - log_warning " ✗ Orchestrator $ORCHESTRATOR_OP_CONFIG is NOT supported" - fi - fi -fi - -echo "" - -# Step 7: Summary -echo "========================================================================" -log_info "DEPLOYMENT TEST COMPLETED SUCCESSFULLY!" -echo "========================================================================" -log_info "✅ Contracts deployed to Base Sepolia and Optimism Sepolia" -log_info "✅ LayerZero configured for cross-chain communication" -log_info "✅ Signers funded and set as gas wallets" -echo "" - -echo "" -log_info "All deployment steps completed successfully!" -' # End of commented old verification code \ No newline at end of file diff --git a/deploy/registry/deployment_11155420_0x0000000000000000000000000000000000000000000000000000000000000001.json b/deploy/registry/deployment_11155420_0x0000000000000000000000000000000000000000000000000000000000000001.json new file mode 100644 index 00000000..23cb5e8b --- /dev/null +++ b/deploy/registry/deployment_11155420_0x0000000000000000000000000000000000000000000000000000000000000001.json @@ -0,0 +1 @@ +{"Orchestrator": "0xb33adF2c2257a94314d408255aC843fd53B1a7e1","IthacaAccount": "0x5a87ef243CDA70a855828d4989Fad61B56A467d3","AccountProxy": "0x4ACD713815fbb363a89D9Ff046C56cEdC7EF3ad7","Simulator": "0x65Ae218EB1987b8bd0F9eeb38D1B344726D41dA5","SimpleFunder": "0xA47C5C472449979a2F37dF2971627cD6587bADb8","Escrow": "0x24F50280cE3B51Ab1967F048746FB7ba3C7B4067","SimpleSettler": "0xb934afBB50b8aBBe24959f9398fE024BEe9Bf716","LayerZeroSettler": "0xB89f4A85d38C3A2407854269527fabD3b61fd56a"} \ No newline at end of file diff --git a/deploy/registry/deployment_84532_0x0000000000000000000000000000000000000000000000000000000000000001.json b/deploy/registry/deployment_84532_0x0000000000000000000000000000000000000000000000000000000000000001.json new file mode 100644 index 00000000..c37d42b4 --- /dev/null +++ b/deploy/registry/deployment_84532_0x0000000000000000000000000000000000000000000000000000000000000001.json @@ -0,0 +1,10 @@ +{ + "Orchestrator": "0xb33adF2c2257a94314d408255aC843fd53B1a7e1", + "IthacaAccount": "0x5a87ef243CDA70a855828d4989Fad61B56A467d3", + "AccountProxy": "0x4ACD713815fbb363a89D9Ff046C56cEdC7EF3ad7", + "Simulator": "0x65Ae218EB1987b8bd0F9eeb38D1B344726D41dA5", + "SimpleFunder": "0xA47C5C472449979a2F37dF2971627cD6587bADb8", + "Escrow": "0x24F50280cE3B51Ab1967F048746FB7ba3C7B4067", + "SimpleSettler": "0xb934afBB50b8aBBe24959f9398fE024BEe9Bf716", + "LayerZeroSettler": "0xB89f4A85d38C3A2407854269527fabD3b61fd56a" +} diff --git a/deploy/verify_config.sh b/deploy/verify_config.sh deleted file mode 100755 index af3a0fcd..00000000 --- a/deploy/verify_config.sh +++ /dev/null @@ -1,553 +0,0 @@ -#!/bin/bash - -# Verification script for deployed contracts and configuration -# This script performs all verification checks from test_deployment.sh -# without running any deployments or modifications -# -# Usage: bash deploy/verify_config.sh [chain_id1] [chain_id2] ... -# -# Examples: -# bash deploy/verify_config.sh # Verify all chains in config.toml -# bash deploy/verify_config.sh 84532 # Verify only Base Sepolia -# bash deploy/verify_config.sh 84532 11155420 # Verify Base Sepolia and Optimism Sepolia -# bash deploy/verify_config.sh 11155111 # Verify only Sepolia - -set -e # Exit on any error - -# Color codes for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Function to print colored output -log_info() { - echo -e "${GREEN}[INFO]${NC} $1" -} - -log_error() { - echo -e "${RED}[ERROR]${NC} $1" -} - -log_warning() { - echo -e "${YELLOW}[WARNING]${NC} $1" -} - -# Parse command-line arguments -REQUESTED_CHAINS=() -if [ $# -gt 0 ]; then - # User provided specific chain IDs to check - REQUESTED_CHAINS=("$@") - log_info "Requested verification for chain IDs: ${REQUESTED_CHAINS[*]}" -else - log_info "No specific chains requested, will verify all chains in config.toml" -fi - -# Start verification process -echo "========================================================================" -log_info "Configuration and Deployment Verification Script" -log_info "This script verifies the current state without making any changes" -if [ ${#REQUESTED_CHAINS[@]} -gt 0 ]; then - log_info "Verifying chains: ${REQUESTED_CHAINS[*]}" -else - log_info "Verifying all chains in config.toml" -fi -echo "========================================================================" - -# Load environment variables -log_info "Loading environment variables from .env" -if [ ! -f .env ]; then - log_error ".env file not found!" - exit 1 -fi - -source .env - -# Validate required environment variables (just check core ones) -log_info "Validating required environment variables" -required_vars=("PRIVATE_KEY" "GAS_SIGNER_MNEMONIC") -missing_vars=() - -for var in "${required_vars[@]}"; do - if [ -z "${!var}" ]; then - missing_vars+=("$var") - fi -done - -if [ ${#missing_vars[@]} -gt 0 ]; then - log_error "Missing required environment variables: ${missing_vars[*]}" - exit 1 -fi - -log_info "Core environment variables are set" -echo "" - -# Function to extract value from TOML for a specific chain section -extract_value() { - local chain=$1 - local key=$2 - local config_file=$3 - - # Get the next section after the chain section - local next_section=$(awk "/^\[$chain\]/,0" "$config_file" | grep '^\[' | grep -v "^\[$chain" | head -1) - - if [ -z "$next_section" ]; then - # This is the last section, get everything after it - awk "/^\[$chain\]/,0" "$config_file" | grep "^$key" | head -1 | cut -d'"' -f2 - else - # Get content between this section and the next - awk "/^\[$chain\]/,/^${next_section//[/\\[}/ { if (/^$key/) print }" "$config_file" | head -1 | cut -d'"' -f2 - fi -} - -# Function to extract uint value from TOML for a specific chain section -extract_uint_value() { - local chain=$1 - local key=$2 - local config_file=$3 - - # Use sed to extract the section and grep for the key - sed -n "/^\[$chain\.uint\]/,/^\[/p" "$config_file" | grep "^$key" | head -1 | awk -F' = ' '{print $2}' | tr -d '"' -} - -# Get all chain sections from config.toml (excluding .uint, .bytes32, .address, .bool, .string subsections) -get_chains() { - grep '^\[' deploy/config.toml | grep -v '\.' | sed 's/\[//g' | sed 's/\]//g' -} - -# Function to extract address value from TOML for a specific chain section -extract_address_value() { - local chain=$1 - local key=$2 - local config_file=$3 - - # Use sed to extract the section and grep for the key - sed -n "/^\[$chain\.address\]/,/^\[/p" "$config_file" | grep "^$key" | head -1 | cut -d'"' -f2 -} - -# Function to extract string value from TOML for a specific chain section -extract_string_value() { - local chain=$1 - local key=$2 - local config_file=$3 - - # Use sed to extract the section and grep for the key - sed -n "/^\[$chain\.string\]/,/^\[/p" "$config_file" | grep "^$key" | head -1 | cut -d'"' -f2 -} - -# ======================================================================== -# SECTION 1: Verify Contract Deployments -# ======================================================================== - -echo "========================================================================" -log_info "SECTION 1: Verifying Contract Deployments" -echo "========================================================================" - -log_info "Reading deployed contract addresses from config.toml..." - -# Contract types to verify -CONTRACT_TYPES=("orchestrator_deployed:Orchestrator" "ithaca_account_deployed:IthacaAccount" "account_proxy_deployed:AccountProxy" "simulator_deployed:Simulator" "simple_funder_deployed:SimpleFunder" "escrow_deployed:Escrow" "simple_settler_deployed:SimpleSettler" "layerzero_settler_deployed:LayerZeroSettler" "exp_token_deployed:ExpToken" "exp2_token_deployed:Exp2Token") - -# Get chains to verify -if [ ${#REQUESTED_CHAINS[@]} -gt 0 ]; then - # Use the chains requested by the user - CHAINS=("${REQUESTED_CHAINS[@]}") - log_info "Verifying ${#CHAINS[@]} requested chain(s): ${CHAINS[*]}" -else - # Get all chains from config.toml - CHAINS=($(get_chains)) - log_info "Found ${#CHAINS[@]} chain(s) in config: ${CHAINS[*]}" -fi -echo "" - -TOTAL_FAILED=0 - -# Iterate over each chain -for chain in "${CHAINS[@]}"; do - # Verify this chain exists in config - if ! grep -q "^\[$chain\]" deploy/config.toml; then - log_error "Chain ID $chain not found in config.toml" - continue - fi - # The chain variable IS the chain ID now - CHAIN_ID=$chain - - # Get chain name for display purposes - CHAIN_NAME=$(extract_string_value "$chain" "name" "deploy/config.toml") - if [ -z "$CHAIN_NAME" ]; then - CHAIN_NAME="Chain $CHAIN_ID" - fi - - log_info "Checking deployed contracts on $CHAIN_NAME (ID: $CHAIN_ID)..." - - # Get RPC URL from environment variable - RPC_VAR="RPC_${CHAIN_ID}" - RPC_URL=${!RPC_VAR} - - if [ -z "$RPC_URL" ]; then - log_warning " RPC URL not set for chain $chain (ID: $CHAIN_ID), skipping..." - log_warning " Set environment variable $RPC_VAR to enable verification" - continue - fi - - log_info " Chain ID: $CHAIN_ID" - - CHAIN_FAILED=0 - - # Check each contract type - for contract_type in "${CONTRACT_TYPES[@]}"; do - IFS=":" read -r key name <<< "$contract_type" - - # Extract address for this contract on this chain from the .address section - addr=$(extract_address_value "$chain" "$key" "deploy/config.toml") - - if [ ! -z "$addr" ]; then - CODE=$(cast code $addr --rpc-url $RPC_URL 2>/dev/null || echo "0x") - if [ "$CODE" != "0x" ] && [ ! -z "$CODE" ]; then - log_info " ✓ $name deployed at $addr" - else - log_error " ✗ $name NOT found at $addr" - CHAIN_FAILED=$((CHAIN_FAILED + 1)) - fi - else - log_warning " ⚠ $name address not found in config.toml" - fi - done - - if [ $CHAIN_FAILED -eq 0 ]; then - log_info " ✅ All contracts verified on $CHAIN_NAME" - else - log_error " ❌ $CHAIN_FAILED contract(s) failed verification on $CHAIN_NAME" - TOTAL_FAILED=$((TOTAL_FAILED + CHAIN_FAILED)) - fi - echo "" -done - -if [ $TOTAL_FAILED -eq 0 ]; then - log_info "✅ All contracts verified successfully across all chains!" -else - log_error "❌ Total contract verification failures: $TOTAL_FAILED" -fi -echo "" - -# ======================================================================== -# SECTION 2: Verify LayerZero Configuration -# ======================================================================== - -echo "========================================================================" -log_info "SECTION 2: Verifying LayerZero Configuration" -echo "========================================================================" - -log_info "Checking LayerZero configuration on all chains..." - -# Store LayerZero info for each chain (using arrays instead of associative arrays for compatibility) -LZ_CHAINS=() -LZ_SETTLERS=() -LZ_ENDPOINTS=() -LZ_EIDS=() -LZ_SEND_ULNS=() -LZ_RECEIVE_ULNS=() -LZ_CHAIN_IDS=() -LZ_RPC_URLS=() - -# Collect LayerZero configuration for each chain -LZ_INDEX=0 -for chain in "${CHAINS[@]}"; do - # Skip if chain doesn't exist in config - if ! grep -q "^\[$chain\]" deploy/config.toml; then - continue - fi - # Chain variable IS the chain ID now - CHAIN_ID=$chain - - # Get LayerZero settler address - settler=$(extract_address_value "$chain" "layerzero_settler_deployed" "deploy/config.toml") - - if [ -z "$settler" ]; then - CHAIN_NAME=$(extract_string_value "$chain" "name" "deploy/config.toml") - log_info " No LayerZero settler deployed on $CHAIN_NAME (ID: $CHAIN_ID), skipping LayerZero checks..." - continue - fi - - LZ_CHAINS+=("$chain") - LZ_SETTLERS+=("$settler") - - # Get other LayerZero config from .address section - LZ_ENDPOINTS+=($(extract_address_value "$chain" "layerzero_endpoint" "deploy/config.toml")) - LZ_SEND_ULNS+=($(extract_address_value "$chain" "layerzero_send_uln302" "deploy/config.toml")) - LZ_RECEIVE_ULNS+=($(extract_address_value "$chain" "layerzero_receive_uln302" "deploy/config.toml")) - - # Get EID from .uint section - LZ_EIDS+=($(extract_uint_value "$chain" "layerzero_eid" "deploy/config.toml")) - - LZ_CHAIN_IDS+=("$CHAIN_ID") - - # Get RPC URL - RPC_VAR="RPC_${CHAIN_ID}" - LZ_RPC_URLS+=(${!RPC_VAR}) -done - -# Verify LayerZero configuration for each chain -for i in "${!LZ_CHAINS[@]}"; do - chain=${LZ_CHAINS[$i]} - CHAIN_NAME=$(extract_string_value "$chain" "name" "deploy/config.toml") - if [ -z "$CHAIN_NAME" ]; then - CHAIN_NAME="Chain $chain" - fi - log_info "$CHAIN_NAME LayerZero configuration:" - - settler=${LZ_SETTLERS[$i]} - endpoint=${LZ_ENDPOINTS[$i]} - eid=${LZ_EIDS[$i]} - rpc_url=${LZ_RPC_URLS[$i]} - - if [ -z "$rpc_url" ]; then - log_warning " RPC URL not available for $CHAIN_NAME, skipping checks..." - continue - fi - - log_info " Settler: $settler" - log_info " Expected Endpoint: $endpoint" - log_info " EID: $eid" - - # Check if endpoint is set on LayerZeroSettler - CURRENT_ENDPOINT=$(cast call $settler "endpoint()(address)" --rpc-url $rpc_url 2>/dev/null || echo "0x0000000000000000000000000000000000000000") - - if [ "$CURRENT_ENDPOINT" == "$endpoint" ]; then - log_info " ✓ Endpoint correctly set to $endpoint" - else - log_error " ✗ Endpoint mismatch: Expected $endpoint, got $CURRENT_ENDPOINT" - fi - - # Check L0SettlerSigner - LZ_SIGNER=$(cast call $settler "l0SettlerSigner()(address)" --rpc-url $rpc_url 2>/dev/null || echo "0x0000000000000000000000000000000000000000") - log_info " L0SettlerSigner: $LZ_SIGNER" - echo "" -done - -# Verify cross-chain pathway configuration -log_info "Cross-chain pathway verification:" - -# Check pathways between all pairs of chains -for i in "${!LZ_CHAINS[@]}"; do - for j in "${!LZ_CHAINS[@]}"; do - if [ "$i" == "$j" ]; then - continue - fi - - source_chain_id=${LZ_CHAINS[$i]} - dest_chain_id=${LZ_CHAINS[$j]} - - source_chain=$(extract_string_value "$source_chain_id" "name" "deploy/config.toml") - dest_chain=$(extract_string_value "$dest_chain_id" "name" "deploy/config.toml") - if [ -z "$source_chain" ]; then - source_chain="Chain $source_chain_id" - fi - if [ -z "$dest_chain" ]; then - dest_chain="Chain $dest_chain_id" - fi - - source_settler=${LZ_SETTLERS[$i]} - source_endpoint=${LZ_ENDPOINTS[$i]} - source_send_uln=${LZ_SEND_ULNS[$i]} - source_rpc=${LZ_RPC_URLS[$i]} - dest_eid=${LZ_EIDS[$j]} - - if [ -z "$source_rpc" ]; then - continue - fi - - log_info " $source_chain -> $dest_chain pathway:" - - # Check executor configuration using endpoint.getConfig() - # CONFIG_TYPE_EXECUTOR = 1 - EXECUTOR_CONFIG_BYTES=$(cast call $source_endpoint "getConfig(address,address,uint32,uint32)(bytes)" "$source_settler" "$source_send_uln" "$dest_eid" "1" --rpc-url $source_rpc 2>/dev/null || echo "0x") - - if [ "$EXECUTOR_CONFIG_BYTES" != "0x" ] && [ ! -z "$EXECUTOR_CONFIG_BYTES" ]; then - # Try to decode executor address using cast - DECODED=$(cast abi-decode "f()(uint32,address)" "$EXECUTOR_CONFIG_BYTES" 2>/dev/null || echo "") - if [ ! -z "$DECODED" ]; then - EXECUTOR_ADDR=$(echo "$DECODED" | tail -1) - if [ "$(echo $EXECUTOR_ADDR | tr '[:upper:]' '[:lower:]')" == "$(echo $source_settler | tr '[:upper:]' '[:lower:]')" ]; then - log_info " ✓ Executor correctly set to LayerZeroSettler" - else - log_error " ⚠ Executor not set to LayerZeroSettler (self-execution model)" - fi - else - log_error " ⚠ Could not parse executor configuration" - fi - else - log_error " ⚠ Executor configuration not set" - fi - - # Check ULN configuration using endpoint.getConfig() - # CONFIG_TYPE_ULN = 2 - ULN_CONFIG_BYTES=$(cast call $source_endpoint "getConfig(address,address,uint32,uint32)(bytes)" "$source_settler" "$source_send_uln" "$dest_eid" "2" --rpc-url $source_rpc 2>/dev/null || echo "0x") - - if [ "$ULN_CONFIG_BYTES" != "0x" ] && [ ! -z "$ULN_CONFIG_BYTES" ] && [ ${#ULN_CONFIG_BYTES} -gt 10 ]; then - log_info " ✓ ULN configuration is set" - else - log_error " ⚠ ULN configuration not set" - fi - done -done -echo "" - -# ======================================================================== -# SECTION 3: Verify Signer Funding and Gas Wallet Configuration -# ======================================================================== - -echo "========================================================================" -log_info "SECTION 3: Verifying Signer Funding and Gas Wallet Configuration" -echo "========================================================================" - -# Derive signer addresses from mnemonic (first 3 for verification) -log_info "Checking signer balances..." - -# Derive signer addresses from mnemonic -log_info "Deriving signer addresses from gas wallet mnemonic..." -SIGNER_0=$(cast wallet address --mnemonic "$GAS_SIGNER_MNEMONIC" --mnemonic-index 0) -SIGNER_1=$(cast wallet address --mnemonic "$GAS_SIGNER_MNEMONIC" --mnemonic-index 1) -SIGNER_2=$(cast wallet address --mnemonic "$GAS_SIGNER_MNEMONIC" --mnemonic-index 2) - -log_info "Signer addresses:" -log_info " Signer 0: $SIGNER_0" -log_info " Signer 1: $SIGNER_1" -log_info " Signer 2: $SIGNER_2" - -# Check balances on all chains -for chain in "${CHAINS[@]}"; do - # Skip if chain doesn't exist in config - if ! grep -q "^\[$chain\]" deploy/config.toml; then - continue - fi - # Chain variable IS the chain ID now - CHAIN_ID=$chain - RPC_VAR="RPC_${CHAIN_ID}" - RPC_URL=${!RPC_VAR} - - # Get chain name for display - CHAIN_NAME=$(extract_string_value "$chain" "name" "deploy/config.toml") - if [ -z "$CHAIN_NAME" ]; then - CHAIN_NAME="Chain $CHAIN_ID" - fi - - if [ -z "$RPC_URL" ]; then - log_warning " RPC URL not set for $CHAIN_NAME, skipping signer balance checks..." - continue - fi - - # Get target balance from config - TARGET_BALANCE=$(extract_uint_value "$chain" "target_balance" "deploy/config.toml") - - if [ -z "$TARGET_BALANCE" ]; then - log_error " Target balance not set for $CHAIN_NAME" - continue - fi - - log_info "$CHAIN_NAME (Chain ID: $CHAIN_ID) signer balances (target: $TARGET_BALANCE wei):" - - # Check each signer's balance - for i in 0 1 2; do - SIGNER_VAR="SIGNER_${i}" - SIGNER_ADDR=${!SIGNER_VAR} - - BALANCE=$(cast balance $SIGNER_ADDR --rpc-url $RPC_URL 2>/dev/null || echo "0") - - if [ "$BALANCE" -ge "$TARGET_BALANCE" ] 2>/dev/null; then - log_info " Signer $i ($SIGNER_ADDR): $BALANCE wei ✓" - else - log_error " Signer $i ($SIGNER_ADDR): $BALANCE wei (below target)" - fi - done - echo "" -done - -# Verify gas wallets and orchestrators in SimpleFunder -log_info "Checking SimpleFunder configuration..." - -for chain in "${CHAINS[@]}"; do - # Skip if chain doesn't exist in config - if ! grep -q "^\[$chain\]" deploy/config.toml; then - continue - fi - - # Get SimpleFunder address - SIMPLE_FUNDER=$(extract_address_value "$chain" "simple_funder_deployed" "deploy/config.toml") - - # Get chain name for display - CHAIN_NAME=$(extract_string_value "$chain" "name" "deploy/config.toml") - if [ -z "$CHAIN_NAME" ]; then - CHAIN_NAME="Chain $chain" - fi - - if [ -z "$SIMPLE_FUNDER" ]; then - log_info " No SimpleFunder deployed on $CHAIN_NAME, skipping..." - continue - fi - - # Chain variable IS the chain ID now - CHAIN_ID=$chain - RPC_VAR="RPC_${CHAIN_ID}" - RPC_URL=${!RPC_VAR} - - if [ -z "$RPC_URL" ]; then - log_warning " RPC URL not set for $CHAIN_NAME, skipping SimpleFunder checks..." - continue - fi - - log_info "$CHAIN_NAME SimpleFunder ($SIMPLE_FUNDER):" - - # Check if signers are gas wallets - GAS_WALLET_OK=0 - for i in 0 1 2; do - SIGNER_VAR="SIGNER_${i}" - SIGNER_ADDR=${!SIGNER_VAR} - IS_GAS_WALLET=$(cast call $SIMPLE_FUNDER "gasWallets(address)(bool)" $SIGNER_ADDR --rpc-url $RPC_URL 2>/dev/null || echo "false") - - if [ "$IS_GAS_WALLET" == "true" ]; then - log_info " ✓ Signer $i is registered as gas wallet" - GAS_WALLET_OK=$((GAS_WALLET_OK + 1)) - else - log_error " ✗ Signer $i is NOT registered as gas wallet" - fi - done - - # Check orchestrator configuration - # Extract orchestrator from supported_orchestrators array in .address section - ORCHESTRATOR_CONFIG=$(extract_address_value "$chain" "supported_orchestrators" "deploy/config.toml" | sed 's/.*\["\([^"]*\)".*/\1/') - - if [ ! -z "$ORCHESTRATOR_CONFIG" ]; then - IS_SUPPORTED=$(cast call $SIMPLE_FUNDER "orchestrators(address)(bool)" $ORCHESTRATOR_CONFIG --rpc-url $RPC_URL 2>/dev/null || echo "false") - - if [ "$IS_SUPPORTED" == "true" ]; then - log_info " ✓ Orchestrator $ORCHESTRATOR_CONFIG is supported" - else - log_error " ✗ Orchestrator $ORCHESTRATOR_CONFIG is NOT supported" - fi - fi - - # Check funder and owner - FUNDER_ADDR=$(cast call $SIMPLE_FUNDER "funder()(address)" --rpc-url $RPC_URL 2>/dev/null || echo "0x0") - OWNER_ADDR=$(cast call $SIMPLE_FUNDER "owner()(address)" --rpc-url $RPC_URL 2>/dev/null || echo "0x0") - log_info " Funder: $FUNDER_ADDR" - log_info " Owner: $OWNER_ADDR" - echo "" -done - -# ======================================================================== -# FINAL SUMMARY -# ======================================================================== - -echo "========================================================================" -log_info "VERIFICATION COMPLETE" -echo "========================================================================" - -log_info "Summary:" -log_info " • Contract Deployments: Checked" -log_info " • LayerZero Configuration: Checked" -log_info " • Signer Funding: Checked" -log_info " • Gas Wallet Configuration: Checked" -echo "" -log_info "Run 'bash deploy/test_deployment.sh' for full deployment and configuration" -log_info "This script only verifies the current state without making changes" \ No newline at end of file diff --git a/docs/4337CallGraph.md b/docs/4337CallGraph.md deleted file mode 100644 index ec9e788d..00000000 --- a/docs/4337CallGraph.md +++ /dev/null @@ -1,32 +0,0 @@ -sequenceDiagram - participant User as User - participant Bundler as Bundler - participant EntryPoint as EntryPoint - participant Paymaster as Paymaster - participant Account as ERC4337 Account - - User->>Bundler: Submit signed UserOperation + paymasterAndData - Bundler->>EntryPoint: handleOps([userOp]) - - Note over EntryPoint: Validation Phase - EntryPoint->>Account: validateUserOp(userOp, userOpHash, missingAccountFunds) - Account-->>EntryPoint: validationData - - Note over EntryPoint: Paymaster validation & deposit check - EntryPoint->>Paymaster: validatePaymasterUserOp(userOp, userOpHash, maxCost) - Note over Paymaster: Validate user agreed to pay in USDC - Paymaster-->>EntryPoint: (context, validationData) - - - Note over EntryPoint: Execution Phase - EntryPoint->>Account: execute(userOp.callData) - - Note over EntryPoint: Payment Phase - ETH movement - EntryPoint-->>Bundler: ETH transfer (actualGasCost) - - EntryPoint->>Paymaster: postOp(mode, context, actualGasCost) - Note over Paymaster: Calculate USDC amount needed - Account-->>Paymaster: USDC transfer (calculated amount) - - EntryPoint-->>Bundler: Execution result - Bundler-->>User: UserOp result \ No newline at end of file diff --git a/docs/IthacaCallGraph.md b/docs/IthacaCallGraph.md deleted file mode 100644 index 7c35733c..00000000 --- a/docs/IthacaCallGraph.md +++ /dev/null @@ -1,25 +0,0 @@ -sequenceDiagram - participant User as User - participant Relayer as Relayer - participant Orch as Orchestrator - participant Account as IthacaAccount - - User->>Relayer: Submit signed intent - Relayer->>Orch: execute(encodedIntent) - - Note over Orch: Verify signature - Orch->>Account: unwrapAndValidateSignature(digest, signature) - Account-->>Orch: (isValid, keyHash) - - Note over Orch: Increment nonce - Orch->>Account: checkAndIncrementNonce(nonce) - - Note over Orch: Process payment - Orch->>Account: pay(paymentAmount, keyHash, digest, intent) - Account-->>Relayer: ERC20 transfer (paymentAmount) - - Note over Orch: Execute intent - Orch->>Account: execute(executionData) - - Orch-->>Relayer: Return success/error code - Relayer-->>User: Execution result \ No newline at end of file diff --git a/docs/benchmarks.jpeg b/docs/benchmarks.jpeg deleted file mode 100644 index 43c3a861ff2d95bd0346c92a384d144e32b149ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 291106 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

bO!3Y`Y|3U%=Ww#mitDQA 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 eid -// - Rationale: chainId was a term we initially used to describe an endpoint on a specific chain. Since -// LayerZero supports non-EVMs we could not map the classic EVM chainIds to the LayerZero chainIds, making it -// confusing for developers. With the addition of EndpointV2 and its backward compatible nature, we would have -// two chainIds per chain that has Endpoint(V1), further confusing developers. We have decided to change the -// name to Endpoint Id, or eid, for simplicity and clarity. -// -adapterParams -> options -// -userApplication -> oapp. Omnichain Application -// -srcAddress -> sender -// -dstAddress -> receiver -// - Rationale: The sender/receiver on EVM is the address. However, on non-EVM chains, the sender/receiver could -// represented as a public key, or some other identifier. The term sender/receiver is more generic -// -payload -> message. -// - Rationale: The term payload is used in the context of a packet, which is a combination of the message and GUID -contract EndpointV2Mock is - ILayerZeroEndpointV2, - MessagingChannel, - MessageLibManager, - MessagingComposer, - MessagingContext -{ - address public lzToken; - - mapping(address oapp => address delegate) public delegates; - - /// @param _eid the unique Endpoint Id for this deploy that all other Endpoints can use to send to it - // @dev oz4/5 breaking change... Ownable constructor - constructor(uint32 _eid, address _owner) Ownable(_owner) MessagingChannel(_eid) {} - - /// @dev MESSAGING STEP 0 - /// @notice This view function gives the application built on top of LayerZero the ability to requests a quote - /// with the same parameters as they would to send their message. Since the quotes are given on chain there is a - /// race condition in which the prices could change between the time the user gets their quote and the time they - /// submit their message. If the price moves up and the user doesn't send enough funds the transaction will revert, - /// if the price goes down the _refundAddress provided by the app will be refunded the difference. - /// @param _params the messaging parameters - /// @param _sender the sender of the message - function quote(MessagingParams calldata _params, address _sender) - external - view - returns (MessagingFee memory) - { - // lzToken must be set to support payInLzToken - if (_params.payInLzToken && lzToken == address(0x0)) revert Errors.LZ_LzTokenUnavailable(); - - // get the correct outbound nonce - uint64 nonce = outboundNonce[_sender][_params.dstEid][_params.receiver] + 1; - - // construct the packet with a GUID - Packet memory packet = Packet({ - nonce: nonce, - srcEid: eid, - sender: _sender, - dstEid: _params.dstEid, - receiver: _params.receiver, - guid: GUID.generate(nonce, eid, _sender, _params.dstEid, _params.receiver), - message: _params.message - }); - - // get the send library by sender and dst eid - // use _ to avoid variable shadowing - address _sendLibrary = getSendLibrary(_sender, _params.dstEid); - - return ISendLib(_sendLibrary).quote(packet, _params.options, _params.payInLzToken); - } - - /// @dev MESSAGING STEP 1 - OApp need to transfer the fees to the endpoint before sending the message - /// @param _params the messaging parameters - /// @param _refundAddress the address to refund both the native and lzToken - function send(MessagingParams calldata _params, address _refundAddress) - external - payable - sendContext(_params.dstEid, msg.sender) - returns (MessagingReceipt memory) - { - if (_params.payInLzToken && lzToken == address(0x0)) { - revert Errors.LZ_LzTokenUnavailable(); - } - - // send message - (MessagingReceipt memory receipt, address _sendLibrary) = _send(msg.sender, _params); - - // OApp can simulate with 0 native value it will fail with error including the required fee, which can be provided in the actual call - // this trick can be used to avoid the need to write the quote() function - // however, without the quote view function it will be hard to compose an oapp on chain - uint256 suppliedNative = _suppliedNative(); - uint256 suppliedLzToken = _suppliedLzToken(_params.payInLzToken); - _assertMessagingFee(receipt.fee, suppliedNative, suppliedLzToken); - - // handle lz token fees - _payToken(lzToken, receipt.fee.lzTokenFee, suppliedLzToken, _sendLibrary, _refundAddress); - - // handle native fees - _payNative(receipt.fee.nativeFee, suppliedNative, _sendLibrary, _refundAddress); - - return receipt; - } - - /// @dev internal function for sending the messages used by all external send methods - /// @param _sender the address of the application sending the message to the destination chain - /// @param _params the messaging parameters - function _send(address _sender, MessagingParams calldata _params) - internal - returns (MessagingReceipt memory, address) - { - // get the correct outbound nonce - uint64 latestNonce = _outbound(_sender, _params.dstEid, _params.receiver); - - // construct the packet with a GUID - Packet memory packet = Packet({ - nonce: latestNonce, - srcEid: eid, - sender: _sender, - dstEid: _params.dstEid, - receiver: _params.receiver, - guid: GUID.generate(latestNonce, eid, _sender, _params.dstEid, _params.receiver), - message: _params.message - }); - - // get the send library by sender and dst eid - address _sendLibrary = getSendLibrary(_sender, _params.dstEid); - - // messageLib always returns encodedPacket with guid - (MessagingFee memory fee, bytes memory encodedPacket) = - ISendLib(_sendLibrary).send(packet, _params.options, _params.payInLzToken); - - // Emit packet information for DVNs, Executors, and any other offchain infrastructure to only listen - // for this one event to perform their actions. - emit PacketSent(encodedPacket, _params.options, _sendLibrary); - - return (MessagingReceipt(packet.guid, latestNonce, fee), _sendLibrary); - } - - /// @dev MESSAGING STEP 2 - on the destination chain - /// @dev configured receive library verifies a message - /// @param _origin a struct holding the srcEid, nonce, and sender of the message - /// @param _receiver the receiver of the message - /// @param _payloadHash the payload hash of the message - function verify(Origin calldata _origin, address _receiver, bytes32 _payloadHash) external { - if (!isValidReceiveLibrary(_receiver, _origin.srcEid, msg.sender)) { - revert Errors.LZ_InvalidReceiveLibrary(); - } - - uint64 lazyNonce = lazyInboundNonce[_receiver][_origin.srcEid][_origin.sender]; - if (!_initializable(_origin, _receiver, lazyNonce)) { - revert Errors.LZ_PathNotInitializable(); - } - if (!_verifiable(_origin, _receiver, lazyNonce)) revert Errors.LZ_PathNotVerifiable(); - - // insert the message into the message channel - _inbound(_receiver, _origin.srcEid, _origin.sender, _origin.nonce, _payloadHash); - emit PacketVerified(_origin, _receiver, _payloadHash); - } - - /// @dev MESSAGING STEP 3 - the last step - /// @dev execute a verified message to the designated receiver - /// @dev the execution provides the execution context (caller, extraData) to the receiver. the receiver can optionally assert the caller and validate the untrusted extraData - /// @dev cant reentrant because the payload is cleared before execution - /// @param _origin the origin of the message - /// @param _receiver the receiver of the message - /// @param _guid the guid of the message - /// @param _message the message - /// @param _extraData the extra data provided by the executor. this data is untrusted and should be validated. - function lzReceive( - Origin calldata _origin, - address _receiver, - bytes32 _guid, - bytes calldata _message, - bytes calldata _extraData - ) external payable { - // clear the payload first to prevent reentrancy, and then execute the message - _clearPayload( - _receiver, - _origin.srcEid, - _origin.sender, - _origin.nonce, - abi.encodePacked(_guid, _message) - ); - ILayerZeroReceiver(_receiver) - .lzReceive{value: msg.value}(_origin, _guid, _message, msg.sender, _extraData); - emit PacketDelivered(_origin, _receiver); - } - - /// @param _origin the origin of the message - /// @param _receiver the receiver of the message - /// @param _guid the guid of the message - /// @param _message the message - /// @param _extraData the extra data provided by the executor. - /// @param _reason the reason for failure - function lzReceiveAlert( - Origin calldata _origin, - address _receiver, - bytes32 _guid, - uint256 _gas, - uint256 _value, - bytes calldata _message, - bytes calldata _extraData, - bytes calldata _reason - ) external { - emit LzReceiveAlert( - _receiver, msg.sender, _origin, _guid, _gas, _value, _message, _extraData, _reason - ); - } - - /// @dev Oapp uses this interface to clear a message. - /// @dev this is a PULL mode versus the PUSH mode of lzReceive - /// @dev the cleared message can be ignored by the app (effectively burnt) - /// @dev authenticated by oapp - /// @param _origin the origin of the message - /// @param _guid the guid of the message - /// @param _message the message - function clear(address _oapp, Origin calldata _origin, bytes32 _guid, bytes calldata _message) - external - { - _assertAuthorized(_oapp); - - bytes memory payload = abi.encodePacked(_guid, _message); - _clearPayload(_oapp, _origin.srcEid, _origin.sender, _origin.nonce, payload); - emit PacketDelivered(_origin, _oapp); - } - - /// @dev allows reconfiguration to recover from wrong configurations - /// @dev users should never approve the EndpointV2 contract to spend their non-layerzero tokens - /// @dev override this function if the endpoint is charging ERC20 tokens as native - /// @dev only owner - /// @param _lzToken the new layer zero token address - function setLzToken(address _lzToken) public virtual onlyOwner { - lzToken = _lzToken; - emit LzTokenSet(_lzToken); - } - - /// @dev recover the token sent to this contract by mistake - /// @dev only owner - /// @param _token the token to recover. if 0x0 then it is native token - /// @param _to the address to send the token to - /// @param _amount the amount to send - function recoverToken(address _token, address _to, uint256 _amount) external onlyOwner { - Transfer.nativeOrToken(_token, _to, _amount); - } - - /// @dev handling token payments on endpoint. the sender must approve the endpoint to spend the token - /// @dev internal function - /// @param _token the token to pay - /// @param _required the amount required - /// @param _supplied the amount supplied - /// @param _receiver the receiver of the token - function _payToken( - address _token, - uint256 _required, - uint256 _supplied, - address _receiver, - address _refundAddress - ) internal { - if (_required > 0) { - Transfer.token(_token, _receiver, _required); - } - if (_required < _supplied) { - unchecked { - // refund the excess - Transfer.token(_token, _refundAddress, _supplied - _required); - } - } - } - - /// @dev handling native token payments on endpoint - /// @dev override this if the endpoint is charging ERC20 tokens as native - /// @dev internal function - /// @param _required the amount required - /// @param _supplied the amount supplied - /// @param _receiver the receiver of the native token - /// @param _refundAddress the address to refund the excess to - function _payNative( - uint256 _required, - uint256 _supplied, - address _receiver, - address _refundAddress - ) internal virtual { - if (_required > 0) { - Transfer.native(_receiver, _required); - } - if (_required < _supplied) { - unchecked { - // refund the excess - Transfer.native(_refundAddress, _supplied - _required); - } - } - } - - /// @dev get the balance of the lzToken as the supplied lzToken fee if payInLzToken is true - function _suppliedLzToken(bool _payInLzToken) internal view returns (uint256 supplied) { - if (_payInLzToken) { - supplied = IERC20(lzToken).balanceOf(address(this)); - - // if payInLzToken is true, the supplied fee must be greater than 0 to prevent a race condition - // in which an oapp sending a message with lz token and the lz token is set to a new token between the tx - // being sent and the tx being mined. if the required lz token fee is 0 and the old lz token would be - // locked in the contract instead of being refunded - if (supplied == 0) revert Errors.LZ_ZeroLzTokenFee(); - } - } - - /// @dev override this if the endpoint is charging ERC20 tokens as native - function _suppliedNative() internal view virtual returns (uint256) { - return msg.value; - } - - /// @dev Assert the required fees and the supplied fees are enough - function _assertMessagingFee( - MessagingFee memory _required, - uint256 _suppliedNativeFee, - uint256 _suppliedLzTokenFee - ) internal pure { - if (_required.nativeFee > _suppliedNativeFee || _required.lzTokenFee > _suppliedLzTokenFee) - { - revert Errors.LZ_InsufficientFee( - _required.nativeFee, _suppliedNativeFee, _required.lzTokenFee, _suppliedLzTokenFee - ); - } - } - - /// @dev override this if the endpoint is charging ERC20 tokens as native - /// @return 0x0 if using native. otherwise the address of the native ERC20 token - function nativeToken() external view virtual returns (address) { - return address(0x0); - } - - /// @notice delegate is authorized by the oapp to configure anything in layerzero - function setDelegate(address _delegate) external { - delegates[msg.sender] = _delegate; - emit DelegateSet(msg.sender, _delegate); - } - - // ========================= Internal ========================= - function _initializable(Origin calldata _origin, address _receiver, uint64 _lazyInboundNonce) - internal - view - returns (bool) - { - return _lazyInboundNonce > 0 // allowInitializePath already checked - || ILayerZeroReceiver(_receiver).allowInitializePath(_origin); - } - - /// @dev bytes(0) payloadHash can never be submitted - function _verifiable(Origin calldata _origin, address _receiver, uint64 _lazyInboundNonce) - internal - view - returns (bool) - { - return _origin.nonce > _lazyInboundNonce // either initializing an empty slot or reverifying - || inboundPayloadHash[_receiver][_origin.srcEid][_origin.sender][_origin.nonce] - != EMPTY_PAYLOAD_HASH; // only allow reverifying if it hasn't been executed - } - - /// @dev assert the caller to either be the oapp or the delegate - function _assertAuthorized(address _oapp) - internal - view - override(MessagingChannel, MessageLibManager) - { - if (msg.sender != _oapp && msg.sender != delegates[_oapp]) { - revert Errors.LZ_Unauthorized(); - } - } - - // ========================= VIEW FUNCTIONS FOR OFFCHAIN ONLY ========================= - // Not involved in any state transition function. - // ==================================================================================== - function initializable(Origin calldata _origin, address _receiver) - external - view - returns (bool) - { - return _initializable( - _origin, _receiver, lazyInboundNonce[_receiver][_origin.srcEid][_origin.sender] - ); - } - - function verifiable(Origin calldata _origin, address _receiver) external view returns (bool) { - return _verifiable( - _origin, _receiver, lazyInboundNonce[_receiver][_origin.srcEid][_origin.sender] - ); - } -} diff --git a/src/vendor/layerzero/oapp/OApp.sol b/src/vendor/layerzero/oapp/OApp.sol deleted file mode 100644 index a4d9576a..00000000 --- a/src/vendor/layerzero/oapp/OApp.sol +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -// @dev Import the 'MessagingFee' and 'MessagingReceipt' so it's exposed to OApp implementers -// solhint-disable-next-line no-unused-import -import {OAppSender, MessagingFee, MessagingReceipt} from "./OAppSender.sol"; -// @dev Import the 'Origin' so it's exposed to OApp implementers -// solhint-disable-next-line no-unused-import -import {OAppReceiver, Origin} from "./OAppReceiver.sol"; -import {OAppCore} from "./OAppCore.sol"; - -/** - * @title OApp - * @dev Abstract contract serving as the base for OApp implementation, combining OAppSender and OAppReceiver functionality. - */ -abstract contract OApp is OAppSender, OAppReceiver { - /** - * @dev Constructor to initialize the OApp with the provided owner. - * @param _delegate The delegate capable of making OApp configurations inside of the endpoint. - */ - constructor(address _delegate) OAppCore(_delegate) {} - - /** - * @notice Retrieves the OApp version information. - * @return senderVersion The version of the OAppSender.sol implementation. - * @return receiverVersion The version of the OAppReceiver.sol implementation. - */ - function oAppVersion() - public - pure - virtual - override(OAppSender, OAppReceiver) - returns (uint64 senderVersion, uint64 receiverVersion) - { - return (SENDER_VERSION, RECEIVER_VERSION); - } -} diff --git a/src/vendor/layerzero/oapp/OAppCore.sol b/src/vendor/layerzero/oapp/OAppCore.sol deleted file mode 100644 index 3eff1be5..00000000 --- a/src/vendor/layerzero/oapp/OAppCore.sol +++ /dev/null @@ -1,104 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {IOAppCore, ILayerZeroEndpointV2} from "../interfaces/IOAppCore.sol"; - -/** - * @title OAppCore - * @dev Abstract contract implementing the IOAppCore interface with basic OApp configurations. - */ -abstract contract OAppCore is IOAppCore, Ownable { - // The LayerZero endpoint associated with the given OApp - ILayerZeroEndpointV2 public endpoint; - - // Mapping to store peers associated with corresponding endpoints - mapping(uint32 eid => bytes32 peer) internal _peers; - - /** - * @dev Constructor to initialize the OAppCore with the provided delegate. - * @param _delegate The delegate capable of making OApp configurations inside of the endpoint. - * - * @dev The delegate typically should be set as the owner of the contract. - */ - constructor(address _delegate) { - if (_delegate == address(0)) revert InvalidDelegate(); - _transferOwnership(_delegate); - } - - /** - * @notice Sets the LayerZero endpoint for this OApp. - * @param _endpoint The address of the LayerZero endpoint. - * @dev Can only be called by the owner. - * @dev Should be called immediately after deployment. - * Delegate is set to the owner of the OAppCore contract. - */ - function setEndpoint(address _endpoint) external onlyOwner { - if (_endpoint == address(0)) revert InvalidDelegate(); - endpoint = ILayerZeroEndpointV2(_endpoint); - endpoint.setDelegate(msg.sender); - } - - /** - * @notice Sets the peer address (OApp instance) for a corresponding endpoint. - * @param _eid The endpoint ID. - * @param _peer The address of the peer to be associated with the corresponding endpoint. - * - * @dev Only the owner/admin of the OApp can call this function. - * @dev Indicates that the peer is trusted to send LayerZero messages to this OApp. - * @dev Set this to bytes32(0) to remove the peer address. - * @dev Peer is a bytes32 to accommodate non-evm chains. - */ - function setPeer(uint32 _eid, bytes32 _peer) public virtual onlyOwner { - _setPeer(_eid, _peer); - } - - /** - * @notice Sets the peer address (OApp instance) for a corresponding endpoint. - * @param _eid The endpoint ID. - * @param _peer The address of the peer to be associated with the corresponding endpoint. - * - * @dev Indicates that the peer is trusted to send LayerZero messages to this OApp. - * @dev Set this to bytes32(0) to remove the peer address. - * @dev Peer is a bytes32 to accommodate non-evm chains. - */ - function _setPeer(uint32 _eid, bytes32 _peer) internal virtual { - _peers[_eid] = _peer; - emit PeerSet(_eid, _peer); - } - - /** - * @notice Internal function to get the peer address associated with a specific endpoint; reverts if NOT set. - * ie. the peer is set to bytes32(0). - * @param _eid The endpoint ID. - * @return peer The address of the peer associated with the specified endpoint. - */ - function _getPeerOrRevert(uint32 _eid) internal view virtual returns (bytes32) { - bytes32 peer = peers(_eid); - if (peer == bytes32(0)) revert NoPeer(_eid); - return peer; - } - - /** - * @notice Retrieves the peer (OApp instance) for a corresponding endpoint. - * @param _eid The endpoint ID. - * @return peer The address of the peer associated with the specified endpoint. - * @dev This function is virtual to allow overriding in derived contracts. - */ - function peers(uint32 _eid) public view virtual returns (bytes32 peer) { - return _peers[_eid]; - } - - /** - * @notice Sets the delegate address for the OApp. - * @param _delegate The address of the delegate to be set. - * - * @dev Only the owner/admin of the OApp can call this function. - * @dev Provides the ability for a delegate to set configs, on behalf of the OApp, directly on the Endpoint contract. - */ - function setDelegate(address _delegate) public onlyOwner { - if (address(endpoint) == address(0)) revert InvalidDelegate(); - endpoint.setDelegate(_delegate); - } -} diff --git a/src/vendor/layerzero/oapp/OAppReceiver.sol b/src/vendor/layerzero/oapp/OAppReceiver.sol deleted file mode 100644 index d3c34ad0..00000000 --- a/src/vendor/layerzero/oapp/OAppReceiver.sol +++ /dev/null @@ -1,143 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -import {IOAppReceiver, Origin} from "../interfaces/IOAppReceiver.sol"; -import {OAppCore} from "./OAppCore.sol"; - -/** - * @title OAppReceiver - * @dev Abstract contract implementing the ILayerZeroReceiver interface and extending OAppCore for OApp receivers. - */ -abstract contract OAppReceiver is IOAppReceiver, OAppCore { - // Custom error message for when the caller is not the registered endpoint/ - error OnlyEndpoint(address addr); - - // @dev The version of the OAppReceiver implementation. - // @dev Version is bumped when changes are made to this contract. - uint64 internal constant RECEIVER_VERSION = 2; - - /** - * @notice Retrieves the OApp version information. - * @return senderVersion The version of the OAppSender.sol contract. - * @return receiverVersion The version of the OAppReceiver.sol contract. - * - * @dev Providing 0 as the default for OAppSender version. Indicates that the OAppSender is not implemented. - * ie. this is a RECEIVE only OApp. - * @dev If the OApp uses both OAppSender and OAppReceiver, then this needs to be override returning the correct versions. - */ - function oAppVersion() - public - view - virtual - returns (uint64 senderVersion, uint64 receiverVersion) - { - return (0, RECEIVER_VERSION); - } - - /** - * @notice Indicates whether an address is an approved composeMsg sender to the Endpoint. - * @dev _origin The origin information containing the source endpoint and sender address. - * - srcEid: The source chain endpoint ID. - * - sender: The sender address on the src chain. - * - nonce: The nonce of the message. - * @dev _message The lzReceive payload. - * @param _sender The sender address. - * @return isSender Is a valid sender. - * - * @dev Applications can optionally choose to implement separate composeMsg senders that are NOT the bridging layer. - * @dev The default sender IS the OAppReceiver implementer. - */ - function isComposeMsgSender( - Origin calldata, /*_origin*/ - bytes calldata, /*_message*/ - address _sender - ) - public - view - virtual - returns (bool) - { - return _sender == address(this); - } - - /** - * @notice Checks if the path initialization is allowed based on the provided origin. - * @param origin The origin information containing the source endpoint and sender address. - * @return Whether the path has been initialized. - * - * @dev This indicates to the endpoint that the OApp has enabled msgs for this particular path to be received. - * @dev This defaults to assuming if a peer has been set, its initialized. - * Can be overridden by the OApp if there is other logic to determine this. - */ - function allowInitializePath(Origin calldata origin) public view virtual returns (bool) { - return peers(origin.srcEid) == origin.sender; - } - - /** - * @notice Retrieves the next nonce for a given source endpoint and sender address. - * @dev _srcEid The source endpoint ID. - * @dev _sender The sender address. - * @return nonce The next nonce. - * - * @dev The path nonce starts from 1. If 0 is returned it means that there is NO nonce ordered enforcement. - * @dev Is required by the off-chain executor to determine the OApp expects msg execution is ordered. - * @dev This is also enforced by the OApp. - * @dev By default this is NOT enabled. ie. nextNonce is hardcoded to return 0. - */ - function nextNonce( - uint32, - /*_srcEid*/ - bytes32 /*_sender*/ - ) - public - view - virtual - returns (uint64 nonce) - { - return 0; - } - - /** - * @dev Entry point for receiving messages or packets from the endpoint. - * @param _origin The origin information containing the source endpoint and sender address. - * - srcEid: The source chain endpoint ID. - * - sender: The sender address on the src chain. - * - nonce: The nonce of the message. - * @param _guid The unique identifier for the received LayerZero message. - * @param _message The payload of the received message. - * @param _executor The address of the executor for the received message. - * @param _extraData Additional arbitrary data provided by the corresponding executor. - * - * @dev Entry point for receiving msg/packet from the LayerZero endpoint. - */ - function lzReceive( - Origin calldata _origin, - bytes32 _guid, - bytes calldata _message, - address _executor, - bytes calldata _extraData - ) public payable virtual { - // Ensures that only the endpoint can attempt to lzReceive() messages to this OApp. - if (address(endpoint) != msg.sender) revert OnlyEndpoint(msg.sender); - - // Ensure that the sender matches the expected peer for the source endpoint. - if (_getPeerOrRevert(_origin.srcEid) != _origin.sender) { - revert OnlyPeer(_origin.srcEid, _origin.sender); - } - - // Call the internal OApp implementation of lzReceive. - _lzReceive(_origin, _guid, _message, _executor, _extraData); - } - - /** - * @dev Internal function to implement lzReceive logic without needing to copy the basic parameter validation. - */ - function _lzReceive( - Origin calldata _origin, - bytes32 _guid, - bytes calldata _message, - address _executor, - bytes calldata _extraData - ) internal virtual; -} diff --git a/src/vendor/layerzero/oapp/OAppSender.sol b/src/vendor/layerzero/oapp/OAppSender.sol deleted file mode 100644 index 6fdf8ef4..00000000 --- a/src/vendor/layerzero/oapp/OAppSender.sol +++ /dev/null @@ -1,136 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { - MessagingParams, - MessagingFee, - MessagingReceipt -} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; -import {OAppCore} from "./OAppCore.sol"; - -/** - * @title OAppSender - * @dev Abstract contract implementing the OAppSender functionality for sending messages to a LayerZero endpoint. - */ -abstract contract OAppSender is OAppCore { - using SafeERC20 for IERC20; - - // Custom error messages - error NotEnoughNative(uint256 msgValue); - error LzTokenUnavailable(); - - // @dev The version of the OAppSender implementation. - // @dev Version is bumped when changes are made to this contract. - uint64 internal constant SENDER_VERSION = 1; - - /** - * @notice Retrieves the OApp version information. - * @return senderVersion The version of the OAppSender.sol contract. - * @return receiverVersion The version of the OAppReceiver.sol contract. - * - * @dev Providing 0 as the default for OAppReceiver version. Indicates that the OAppReceiver is not implemented. - * ie. this is a SEND only OApp. - * @dev If the OApp uses both OAppSender and OAppReceiver, then this needs to be override returning the correct versions - */ - function oAppVersion() - public - view - virtual - returns (uint64 senderVersion, uint64 receiverVersion) - { - return (SENDER_VERSION, 0); - } - - /** - * @dev Internal function to interact with the LayerZero EndpointV2.quote() for fee calculation. - * @param _dstEid The destination endpoint ID. - * @param _message The message payload. - * @param _options Additional options for the message. - * @param _payInLzToken Flag indicating whether to pay the fee in LZ tokens. - * @return fee The calculated MessagingFee for the message. - * - nativeFee: The native fee for the message. - * - lzTokenFee: The LZ token fee for the message. - */ - function _quote( - uint32 _dstEid, - bytes memory _message, - bytes memory _options, - bool _payInLzToken - ) internal view virtual returns (MessagingFee memory fee) { - return endpoint.quote( - MessagingParams(_dstEid, _getPeerOrRevert(_dstEid), _message, _options, _payInLzToken), - address(this) - ); - } - - /** - * @dev Internal function to interact with the LayerZero EndpointV2.send() for sending a message. - * @param _dstEid The destination endpoint ID. - * @param _message The message payload. - * @param _options Additional options for the message. - * @param _fee The calculated LayerZero fee for the message. - * - nativeFee: The native fee. - * - lzTokenFee: The lzToken fee. - * @param _refundAddress The address to receive any excess fee values sent to the endpoint. - * @return receipt The receipt for the sent message. - * - guid: The unique identifier for the sent message. - * - nonce: The nonce of the sent message. - * - fee: The LayerZero fee incurred for the message. - */ - function _lzSend( - uint32 _dstEid, - bytes memory _message, - bytes memory _options, - MessagingFee memory _fee, - address _refundAddress - ) internal virtual returns (MessagingReceipt memory receipt) { - // @dev Push corresponding fees to the endpoint, any excess is sent back to the _refundAddress from the endpoint. - uint256 messageValue = _payNative(_fee.nativeFee); - if (_fee.lzTokenFee > 0) _payLzToken(_fee.lzTokenFee); - - return endpoint. - // solhint-disable-next-line check-send-result - send{ - value: messageValue - }( - MessagingParams( - _dstEid, _getPeerOrRevert(_dstEid), _message, _options, _fee.lzTokenFee > 0 - ), - _refundAddress - ); - } - - /** - * @dev Internal function to pay the native fee associated with the message. - * @param _nativeFee The native fee to be paid. - * @return nativeFee The amount of native currency paid. - * - * @dev If the OApp needs to initiate MULTIPLE LayerZero messages in a single transaction, - * this will need to be overridden because msg.value would contain multiple lzFees. - * @dev Should be overridden in the event the LayerZero endpoint requires a different native currency. - * @dev Some EVMs use an ERC20 as a method for paying transactions/gasFees. - * @dev The endpoint is EITHER/OR, ie. it will NOT support both types of native payment at a time. - */ - function _payNative(uint256 _nativeFee) internal virtual returns (uint256 nativeFee) { - if (msg.value != _nativeFee) revert NotEnoughNative(msg.value); - return _nativeFee; - } - - /** - * @dev Internal function to pay the LZ token fee associated with the message. - * @param _lzTokenFee The LZ token fee to be paid. - * - * @dev If the caller is trying to pay in the specified lzToken, then the lzTokenFee is passed to the endpoint. - * @dev Any excess sent, is passed back to the specified _refundAddress in the _lzSend(). - */ - function _payLzToken(uint256 _lzTokenFee) internal virtual { - // @dev Cannot cache the token because it is not immutable in the endpoint. - address lzToken = endpoint.lzToken(); - if (lzToken == address(0)) revert LzTokenUnavailable(); - - // Pay LZ token fee by sending tokens to the endpoint. - IERC20(lzToken).safeTransferFrom(msg.sender, address(endpoint), _lzTokenFee); - } -} diff --git a/test/MultiSigSigner.t.sol b/test/MultiSigSigner.t.sol deleted file mode 100644 index 497e0618..00000000 --- a/test/MultiSigSigner.t.sol +++ /dev/null @@ -1,656 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import "./Base.t.sol"; -import {MultiSigSigner} from "../src/MultiSigSigner.sol"; -import {IthacaAccount} from "../src/IthacaAccount.sol"; -import {ERC7821} from "solady/accounts/ERC7821.sol"; - -contract MultiSigSignerTest is BaseTest { - MultiSigSigner multiSigSigner; - DelegatedEOA delegatedAccount; - - struct MultiSigTestTemps { - PassKey[] owners; - bytes32[] ownerKeyHashes; - uint256 threshold; - MultiSigKey multiSigKey; - bytes32 digest; - } - - function setUp() public override { - super.setUp(); - multiSigSigner = new MultiSigSigner(); - delegatedAccount = _randomEIP7702DelegatedEOA(); - } - - function test_DuplicateOwnerSignatures() public { - MultiSigTestTemps memory t; - - // Setup: Create a multisig with threshold 2 but only 1 owner - t.threshold = 2; - t.owners = new PassKey[](2); - t.owners[0] = _randomPassKey(); - t.owners[1] = _randomPassKey(); - - // Create the multisig key configuration - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: true, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) - }); - t.multiSigKey.threshold = t.threshold; - t.multiSigKey.owners = t.owners; - - // Authorize keys in the delegated account - vm.startPrank(delegatedAccount.eoa); - delegatedAccount.d.authorize(t.multiSigKey.k); - delegatedAccount.d.authorize(t.owners[0].k); - - // Initialize multisig config with threshold=2 but only 1 owner - t.ownerKeyHashes = new bytes32[](1); - t.ownerKeyHashes[0] = _hash(t.owners[0].k); - - // This should revert because threshold > number of owners - vm.expectRevert(MultiSigSigner.InvalidThreshold.selector); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); - - // Now test with a valid config: threshold=2, 2 owners - t.ownerKeyHashes = new bytes32[](2); - t.ownerKeyHashes[0] = _hash(t.owners[0].k); - t.ownerKeyHashes[1] = _hash(t.owners[1].k); - - multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); - - vm.stopPrank(); - - // Create a digest to sign - t.digest = keccak256("test message"); - - // Create signature array with the same signature duplicated - // Note: Each owner currently only has 1 signer power. - bytes[] memory signatures = new bytes[](2); - signatures[0] = _sig(t.owners[0], t.digest); - signatures[1] = signatures[0]; // Duplicate signature of the first owner - - // Call isValidSignatureWithKeyHash - vm.prank(address(delegatedAccount.d)); - bytes4 result = multiSigSigner.isValidSignatureWithKeyHash( - t.digest, _hash(t.multiSigKey.k), abi.encode(signatures) - ); - - // Same owner should not be able to sign multiple times - assertEq(result, bytes4(0xffffffff), "Duplicate signature should not be valid"); - - // Add the first owner twice in the owner key hash - vm.startPrank(address(delegatedAccount.eoa)); - delegatedAccount.d.setContextKeyHash(_hash(t.multiSigKey.k)); - multiSigSigner.addOwner(_hash(t.multiSigKey.k), _hash(t.owners[0].k)); - vm.stopPrank(); - - // Now it should be valid, because the first owner has 2 signer powers. - vm.prank(address(delegatedAccount.d)); - result = multiSigSigner.isValidSignatureWithKeyHash( - t.digest, _hash(t.multiSigKey.k), abi.encode(signatures) - ); - - assertEq(result, bytes4(0x8afc93b4), "Duplicate signature should be valid now"); - } - - function test_InitConfig() public { - MultiSigTestTemps memory t; - - // Setup owners - uint256 numOwners = 3; - t.owners = new PassKey[](numOwners); - t.ownerKeyHashes = new bytes32[](numOwners); - - for (uint256 i = 0; i < numOwners; i++) { - t.owners[i] = _randomPassKey(); - t.ownerKeyHashes[i] = _hash(t.owners[i].k); - } - - t.threshold = 2; - - // Create multisig key - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: false, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) - }); - - vm.prank(address(delegatedAccount.d)); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); - - // Verify config was set correctly - (uint256 storedThreshold, bytes32[] memory storedOwners) = - multiSigSigner.getConfig(address(delegatedAccount.d), _hash(t.multiSigKey.k)); - - assertEq(storedThreshold, t.threshold); - assertEq(storedOwners.length, t.ownerKeyHashes.length); - - for (uint256 i = 0; i < storedOwners.length; i++) { - assertEq(storedOwners[i], t.ownerKeyHashes[i]); - } - } - - function test_InitConfig_RevertsOnReinit() public { - MultiSigTestTemps memory t; - - t.owners = new PassKey[](1); - t.owners[0] = _randomPassKey(); - t.ownerKeyHashes = new bytes32[](1); - t.ownerKeyHashes[0] = _hash(t.owners[0].k); - t.threshold = 1; - - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: false, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) - }); - - // First initialization should succeed - vm.startPrank(address(delegatedAccount.d)); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); - - // Second initialization should revert - vm.expectRevert(MultiSigSigner.ConfigAlreadySet.selector); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); - vm.stopPrank(); - } - - function test_InitConfig_InvalidThreshold() public { - MultiSigTestTemps memory t; - - t.owners = new PassKey[](2); - t.ownerKeyHashes = new bytes32[](2); - for (uint256 i = 0; i < 2; i++) { - t.owners[i] = _randomPassKey(); - t.ownerKeyHashes[i] = _hash(t.owners[i].k); - } - - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: false, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) - }); - - vm.startPrank(address(delegatedAccount.d)); - - // Test threshold = 0 - vm.expectRevert(MultiSigSigner.InvalidThreshold.selector); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), 0, t.ownerKeyHashes); - - // Test threshold > number of owners - vm.expectRevert(MultiSigSigner.InvalidThreshold.selector); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), 3, t.ownerKeyHashes); - - vm.stopPrank(); - } - - function test_AddOwner() public { - MultiSigTestTemps memory t; - - // Initial setup with 2 owners - t.owners = new PassKey[](2); - t.ownerKeyHashes = new bytes32[](2); - for (uint256 i = 0; i < 2; i++) { - t.owners[i] = _randomPassKey(); - t.ownerKeyHashes[i] = _hash(t.owners[i].k); - } - t.threshold = 2; - - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: true, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) - }); - - // Initialize config - vm.startPrank(address(delegatedAccount.d)); - delegatedAccount.d.authorize(t.multiSigKey.k); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); - - // Add a new owner - PassKey memory newOwner = _randomPassKey(); - bytes32 newOwnerKeyHash = _hash(newOwner.k); - - // Set context key hash - delegatedAccount.d.setContextKeyHash(_hash(t.multiSigKey.k)); - multiSigSigner.addOwner(_hash(t.multiSigKey.k), newOwnerKeyHash); - - // Verify owner was added - (, bytes32[] memory storedOwners) = - multiSigSigner.getConfig(address(delegatedAccount.d), _hash(t.multiSigKey.k)); - - assertEq(storedOwners.length, 3); - assertEq(storedOwners[2], newOwnerKeyHash); - - vm.stopPrank(); - } - - function test_AddOwner_InvalidKeyHash() public { - MultiSigTestTemps memory t; - - t.owners = new PassKey[](1); - t.owners[0] = _randomPassKey(); - t.ownerKeyHashes = new bytes32[](1); - t.ownerKeyHashes[0] = _hash(t.owners[0].k); - - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: true, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) - }); - - vm.startPrank(address(delegatedAccount.d)); - delegatedAccount.d.authorize(t.multiSigKey.k); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), 1, t.ownerKeyHashes); - - // Try to add owner with wrong context key hash - PassKey memory newOwner = _randomPassKey(); - - // Set wrong context key hash - delegatedAccount.d.setContextKeyHash(bytes32(uint256(123))); - - vm.expectRevert(MultiSigSigner.InvalidKeyHash.selector); - multiSigSigner.addOwner(_hash(t.multiSigKey.k), _hash(newOwner.k)); - - vm.stopPrank(); - } - - function test_RemoveOwner() public { - MultiSigTestTemps memory t; - - // Setup with 3 owners - t.owners = new PassKey[](3); - t.ownerKeyHashes = new bytes32[](3); - for (uint256 i = 0; i < 3; i++) { - t.owners[i] = _randomPassKey(); - t.ownerKeyHashes[i] = _hash(t.owners[i].k); - } - t.threshold = 2; - - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: true, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) - }); - - vm.startPrank(address(delegatedAccount.d)); - delegatedAccount.d.authorize(t.multiSigKey.k); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); - - // Remove the second owner - delegatedAccount.d.setContextKeyHash(_hash(t.multiSigKey.k)); - multiSigSigner.removeOwner(_hash(t.multiSigKey.k), t.ownerKeyHashes[1]); - - // Verify owner was removed - (, bytes32[] memory storedOwners) = - multiSigSigner.getConfig(address(delegatedAccount.d), _hash(t.multiSigKey.k)); - - assertEq(storedOwners.length, 2); - // The last owner should have replaced the removed one - assertEq(storedOwners[1], t.ownerKeyHashes[2]); - - vm.stopPrank(); - } - - function test_RemoveOwner_OwnerNotFound() public { - MultiSigTestTemps memory t; - - t.owners = new PassKey[](2); - t.ownerKeyHashes = new bytes32[](2); - for (uint256 i = 0; i < 2; i++) { - t.owners[i] = _randomPassKey(); - t.ownerKeyHashes[i] = _hash(t.owners[i].k); - } - - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: true, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) - }); - - vm.startPrank(address(delegatedAccount.d)); - delegatedAccount.d.authorize(t.multiSigKey.k); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), 1, t.ownerKeyHashes); - - // Try to remove non-existent owner - delegatedAccount.d.setContextKeyHash(_hash(t.multiSigKey.k)); - vm.expectRevert(MultiSigSigner.OwnerNotFound.selector); - multiSigSigner.removeOwner(_hash(t.multiSigKey.k), bytes32(uint256(999))); - - vm.stopPrank(); - } - - function test_RemoveOwner_ThresholdViolation() public { - MultiSigTestTemps memory t; - - // Setup with 2 owners and threshold 2 - t.owners = new PassKey[](2); - t.ownerKeyHashes = new bytes32[](2); - for (uint256 i = 0; i < 2; i++) { - t.owners[i] = _randomPassKey(); - t.ownerKeyHashes[i] = _hash(t.owners[i].k); - } - t.threshold = 2; - - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: true, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) - }); - - vm.startPrank(address(delegatedAccount.d)); - delegatedAccount.d.authorize(t.multiSigKey.k); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); - - // Try to remove owner when it would violate threshold - delegatedAccount.d.setContextKeyHash(_hash(t.multiSigKey.k)); - vm.expectRevert(MultiSigSigner.InvalidThreshold.selector); - multiSigSigner.removeOwner(_hash(t.multiSigKey.k), t.ownerKeyHashes[0]); - - vm.stopPrank(); - } - - function test_SetThreshold() public { - MultiSigTestTemps memory t; - - // Setup with 3 owners - t.owners = new PassKey[](3); - t.ownerKeyHashes = new bytes32[](3); - for (uint256 i = 0; i < 3; i++) { - t.owners[i] = _randomPassKey(); - t.ownerKeyHashes[i] = _hash(t.owners[i].k); - } - t.threshold = 1; - - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: true, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) - }); - - vm.startPrank(address(delegatedAccount.d)); - delegatedAccount.d.authorize(t.multiSigKey.k); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); - - // Change threshold to 2 - delegatedAccount.d.setContextKeyHash(_hash(t.multiSigKey.k)); - multiSigSigner.setThreshold(_hash(t.multiSigKey.k), 2); - - // Verify threshold was changed - (uint256 storedThreshold,) = - multiSigSigner.getConfig(address(delegatedAccount.d), _hash(t.multiSigKey.k)); - - assertEq(storedThreshold, 2); - - // Change threshold to 3 - multiSigSigner.setThreshold(_hash(t.multiSigKey.k), 3); - - (storedThreshold,) = - multiSigSigner.getConfig(address(delegatedAccount.d), _hash(t.multiSigKey.k)); - - assertEq(storedThreshold, 3); - - vm.stopPrank(); - } - - function test_SetThreshold_Invalid() public { - MultiSigTestTemps memory t; - - t.owners = new PassKey[](2); - t.ownerKeyHashes = new bytes32[](2); - for (uint256 i = 0; i < 2; i++) { - t.owners[i] = _randomPassKey(); - t.ownerKeyHashes[i] = _hash(t.owners[i].k); - } - - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: true, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) - }); - - vm.startPrank(address(delegatedAccount.d)); - delegatedAccount.d.authorize(t.multiSigKey.k); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), 1, t.ownerKeyHashes); - delegatedAccount.d.setContextKeyHash(_hash(t.multiSigKey.k)); - - // Test threshold = 0 - vm.expectRevert(MultiSigSigner.InvalidThreshold.selector); - multiSigSigner.setThreshold(_hash(t.multiSigKey.k), 0); - - // Test threshold > number of owners - vm.expectRevert(MultiSigSigner.InvalidThreshold.selector); - multiSigSigner.setThreshold(_hash(t.multiSigKey.k), 3); - - vm.stopPrank(); - } - - function test_ValidSignature_MeetsThreshold() public { - MultiSigTestTemps memory t; - - // Setup with 3 owners, threshold 2 - t.owners = new PassKey[](3); - t.ownerKeyHashes = new bytes32[](3); - - vm.startPrank(delegatedAccount.eoa); - for (uint256 i = 0; i < 3; i++) { - t.owners[i] = _randomPassKey(); - t.ownerKeyHashes[i] = _hash(t.owners[i].k); - delegatedAccount.d.authorize(t.owners[i].k); - } - - t.threshold = 2; - - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: true, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) - }); - - delegatedAccount.d.authorize(t.multiSigKey.k); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); - vm.stopPrank(); - - // Create a digest to sign - t.digest = keccak256("test message"); - - // Create signatures from 2 owners (meets threshold) - bytes[] memory signatures = new bytes[](2); - signatures[0] = _sig(t.owners[0], t.digest); - signatures[1] = _sig(t.owners[1], t.digest); - - // Validate signature - vm.prank(address(delegatedAccount.d)); - bytes4 result = multiSigSigner.isValidSignatureWithKeyHash( - t.digest, _hash(t.multiSigKey.k), abi.encode(signatures) - ); - - assertEq(result, bytes4(0x8afc93b4), "Valid signature should return MAGIC_VALUE"); - } - - function test_InvalidSignature_BelowThreshold() public { - MultiSigTestTemps memory t; - - // Setup with 3 owners, threshold 2 - t.owners = new PassKey[](3); - t.ownerKeyHashes = new bytes32[](3); - - vm.startPrank(delegatedAccount.eoa); - for (uint256 i = 0; i < 3; i++) { - t.owners[i] = _randomPassKey(); - t.ownerKeyHashes[i] = _hash(t.owners[i].k); - delegatedAccount.d.authorize(t.owners[i].k); - } - - t.threshold = 2; - - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: true, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) - }); - - delegatedAccount.d.authorize(t.multiSigKey.k); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); - vm.stopPrank(); - - // Create a digest to sign - t.digest = keccak256("test message"); - - // Create signature from only 1 owner (below threshold) - bytes[] memory signatures = new bytes[](1); - signatures[0] = _sig(t.owners[0], t.digest); - - // Validate signature - vm.prank(address(delegatedAccount.d)); - bytes4 result = multiSigSigner.isValidSignatureWithKeyHash( - t.digest, _hash(t.multiSigKey.k), abi.encode(signatures) - ); - - assertEq(result, bytes4(0xffffffff), "Invalid signature should return FAIL_VALUE"); - } - - function test_InvalidSignature_NonOwner() public { - MultiSigTestTemps memory t; - - // Setup with 2 owners - t.owners = new PassKey[](2); - t.ownerKeyHashes = new bytes32[](2); - - vm.startPrank(delegatedAccount.eoa); - for (uint256 i = 0; i < 2; i++) { - t.owners[i] = _randomPassKey(); - t.ownerKeyHashes[i] = _hash(t.owners[i].k); - delegatedAccount.d.authorize(t.owners[i].k); - } - - t.threshold = 2; - - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: true, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) - }); - - delegatedAccount.d.authorize(t.multiSigKey.k); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); - - // Create a non-owner key - PassKey memory nonOwner = _randomPassKey(); - delegatedAccount.d.authorize(nonOwner.k); - - vm.stopPrank(); - - // Create a digest to sign - t.digest = keccak256("test message"); - - // Create signatures with one owner and one non-owner - bytes[] memory signatures = new bytes[](2); - signatures[0] = _sig(t.owners[0], t.digest); - signatures[1] = _sig(nonOwner, t.digest); - - // Validate signature - vm.prank(address(delegatedAccount.d)); - bytes4 result = multiSigSigner.isValidSignatureWithKeyHash( - t.digest, _hash(t.multiSigKey.k), abi.encode(signatures) - ); - - assertEq(result, bytes4(0xffffffff), "Non-owner signature should return FAIL_VALUE"); - } - - function testFuzz_InitConfig(uint256 numOwners, uint256 threshold) public { - numOwners = bound(numOwners, 1, 10); - threshold = bound(threshold, 1, numOwners); - - MultiSigTestTemps memory t; - t.owners = new PassKey[](numOwners); - t.ownerKeyHashes = new bytes32[](numOwners); - - for (uint256 i = 0; i < numOwners; i++) { - t.owners[i] = _randomPassKey(); - t.ownerKeyHashes[i] = _hash(t.owners[i].k); - } - - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: false, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(uint96(_random()))) - }); - - vm.prank(address(delegatedAccount.d)); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), threshold, t.ownerKeyHashes); - - (uint256 storedThreshold, bytes32[] memory storedOwners) = - multiSigSigner.getConfig(address(delegatedAccount.d), _hash(t.multiSigKey.k)); - - assertEq(storedThreshold, threshold); - assertEq(storedOwners.length, numOwners); - } - - function testFuzz_SignatureValidation(uint256 numOwners, uint256 threshold, uint256 numSigners) - public - { - numOwners = bound(numOwners, 1, 10); - threshold = bound(threshold, 1, numOwners); - numSigners = bound(numSigners, 0, numOwners); - - MultiSigTestTemps memory t; - t.owners = new PassKey[](numOwners); - t.ownerKeyHashes = new bytes32[](numOwners); - - vm.startPrank(delegatedAccount.eoa); - for (uint256 i = 0; i < numOwners; i++) { - t.owners[i] = _randomPassKey(); - t.ownerKeyHashes[i] = _hash(t.owners[i].k); - delegatedAccount.d.authorize(t.owners[i].k); - } - - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: true, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(uint96(_random()))) - }); - - delegatedAccount.d.authorize(t.multiSigKey.k); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), threshold, t.ownerKeyHashes); - vm.stopPrank(); - - t.digest = keccak256(abi.encode("test", _random())); - - bytes[] memory signatures = new bytes[](numSigners); - for (uint256 i = 0; i < numSigners; i++) { - signatures[i] = _sig(t.owners[i], t.digest); - } - - vm.prank(address(delegatedAccount.d)); - bytes4 result = multiSigSigner.isValidSignatureWithKeyHash( - t.digest, _hash(t.multiSigKey.k), abi.encode(signatures) - ); - - if (numSigners >= threshold) { - assertEq(result, bytes4(0x8afc93b4), "Should be valid when signatures >= threshold"); - } else { - assertEq(result, bytes4(0xffffffff), "Should be invalid when signatures < threshold"); - } - } -} diff --git a/test/UpgradeTests.t.sol b/test/UpgradeTests.t.sol deleted file mode 100644 index d0d28d09..00000000 --- a/test/UpgradeTests.t.sol +++ /dev/null @@ -1,566 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.4; - -import "./utils/SoladyTest.sol"; -import {IthacaAccount} from "./utils/mocks/MockAccount.sol"; -import {GuardedExecutor} from "../src/IthacaAccount.sol"; -import {BaseTest} from "./Base.t.sol"; -import {EIP7702Proxy} from "solady/accounts/EIP7702Proxy.sol"; -import {LibEIP7702} from "solady/accounts/LibEIP7702.sol"; -import {MockPaymentToken} from "./utils/mocks/MockPaymentToken.sol"; -import {LibClone} from "solady/utils/LibClone.sol"; -import {Orchestrator, MockOrchestrator} from "./utils/mocks/MockOrchestrator.sol"; -import {ERC7821} from "solady/accounts/ERC7821.sol"; - -contract UpgradeTests is BaseTest { - address payable public oldProxyAddress; - address public oldImplementation; - address public newImplementation; - - // Test EOA that will be delegated to the proxy - address public userEOA; - uint256 public userEOAKey; - IthacaAccount public userAccount; - - // Test keys - PassKey public p256Key; - PassKey public p256SuperAdminKey; - PassKey public secp256k1Key; - PassKey public secp256k1SuperAdminKey; - PassKey public webAuthnP256Key; - PassKey public externalKey; - - // Test tokens - MockPaymentToken public mockToken1; - MockPaymentToken public mockToken2; - - // Random addresses for testing transfers - address[] public randomRecipients; - - // State capture - using simple mappings to avoid memory-to-storage issues - // Pre-upgrade state - bytes32[] preKeyHashes; - mapping(bytes32 => IthacaAccount.Key) preKeys; - mapping(bytes32 => bool) preAuthorized; - uint256 preEthBalance; - uint256 preToken1Balance; - uint256 preToken2Balance; - uint256 preNonce; - // Get expected old version from environment variable - string public expectedOldVersion = vm.envString("UPGRADE_TEST_OLD_VERSION"); - - // Post-upgrade state - bytes32[] postKeyHashes; - mapping(bytes32 => IthacaAccount.Key) postKeys; - mapping(bytes32 => bool) postAuthorized; - uint256 postEthBalance; - uint256 postToken1Balance; - uint256 postToken2Balance; - uint256 postNonce; - - function setUp() public override { - super.setUp(); - - // Fork the network to get the proxy bytecode - string memory rpcUrl = vm.envString("UPGRADE_TEST_RPC_URL"); - vm.createSelectFork(rpcUrl); - - // Deploy test tokens - mockToken1 = new MockPaymentToken(); - mockToken2 = new MockPaymentToken(); - - // Setup random recipients - for (uint256 i = 0; i < 5; i++) { - randomRecipients.push(_randomAddress()); - } - - // Get old proxy address from environment - oldProxyAddress = payable(vm.envAddress("UPGRADE_TEST_OLD_PROXY")); - - // Setup delegated EOA - (userEOA, userEOAKey) = _randomUniqueSigner(); - - vm.etch(userEOA, abi.encodePacked(hex"ef0100", address(oldProxyAddress))); - - userAccount = IthacaAccount(payable(userEOA)); - - // Get the bytecode of the old proxy from the forked network - bytes memory proxyBytecode = oldProxyAddress.code; - require(proxyBytecode.length > 0, "No bytecode at old proxy address"); - - newImplementation = address(new IthacaAccount(address(oc))); - - // Generate test keys - p256Key = _randomSecp256r1PassKey(); - p256Key.k.isSuperAdmin = false; - p256Key.k.expiry = 0; // Never expires - - p256SuperAdminKey = _randomSecp256r1PassKey(); - p256SuperAdminKey.k.isSuperAdmin = true; - p256SuperAdminKey.k.expiry = uint40(block.timestamp + 365 days); // Expires in 1 year - - secp256k1Key = _randomSecp256k1PassKey(); - secp256k1Key.k.isSuperAdmin = false; - secp256k1Key.k.expiry = 0; - - secp256k1SuperAdminKey = _randomSecp256k1PassKey(); - secp256k1SuperAdminKey.k.isSuperAdmin = true; - secp256k1SuperAdminKey.k.expiry = uint40(block.timestamp + 30 days); // Expires in 30 days - - webAuthnP256Key = _randomSecp256r1PassKey(); - webAuthnP256Key.k.keyType = IthacaAccount.KeyType.WebAuthnP256; - webAuthnP256Key.k.isSuperAdmin = false; - webAuthnP256Key.k.expiry = 0; - - // Setup external key - address externalSigner = _randomAddress(); - bytes12 salt = bytes12(uint96(_randomUniform())); - externalKey.k.keyType = IthacaAccount.KeyType.External; - externalKey.k.publicKey = abi.encodePacked(externalSigner, salt); - externalKey.k.isSuperAdmin = false; - externalKey.k.expiry = uint40(block.timestamp + 7 days); - externalKey.keyHash = _hash(externalKey.k); - } - - function test_ComprehensiveUpgrade() public { - // Step 2: Setup the old account with various configurations - _setupOldAccountState(); - - // Step 3: Capture pre-upgrade state - _capturePreUpgradeState(); - - // Step 4: Perform upgrade - _performUpgrade(); - - // Step 5: Capture post-upgrade state - _capturePostUpgradeState(); - - // Step 6: Verify state preservation - _verifyStatePreservation(); - - // Step 7: Test post-upgrade functionality - _testPostUpgradeFunctionality(); - } - - function _performUpgrade() internal { - // Get the version from the new implementation for comparison - IthacaAccount newImpl = IthacaAccount(payable(newImplementation)); - (,, string memory expectedNewVersion,,,,) = newImpl.eip712Domain(); - - // Check version before upgrade matches expected old version - (,, string memory versionBefore,,,,) = userAccount.eip712Domain(); - assertEq( - keccak256(bytes(versionBefore)), - keccak256(bytes(expectedOldVersion)), - string.concat("Version before upgrade should be ", expectedOldVersion) - ); - - // Perform the upgrade - bytes memory upgradeCalldata = - abi.encodeWithSelector(IthacaAccount.upgradeProxyAccount.selector, newImplementation); - - vm.prank(userEOA); - (bool success,) = userEOA.call(upgradeCalldata); - require(success, "Upgrade failed"); - - // Check version after upgrade - (,, string memory versionAfter,,,,) = userAccount.eip712Domain(); - - // Verify the version matches the new implementation version after upgrade - assertEq( - keccak256(bytes(versionAfter)), - keccak256(bytes(expectedNewVersion)), - string.concat("Version after upgrade should be ", expectedNewVersion) - ); - } - - function _setupOldAccountState() internal { - // Authorize various keys - vm.startPrank(userEOA); - - userAccount.authorize(p256Key.k); - p256Key.keyHash = _hash(p256Key.k); - - userAccount.authorize(secp256k1Key.k); - secp256k1Key.keyHash = _hash(secp256k1Key.k); - - userAccount.authorize(secp256k1SuperAdminKey.k); - secp256k1SuperAdminKey.keyHash = _hash(secp256k1SuperAdminKey.k); - - userAccount.authorize(webAuthnP256Key.k); - webAuthnP256Key.keyHash = _hash(webAuthnP256Key.k); - - externalKey.keyHash = userAccount.authorize(externalKey.k); - - // Setup spending limits for different keys - _setupSpendingLimits(); - - // Setup execution permissions - _setupExecutionPermissions(); - - // Fund the account - _fundAccount(); - - vm.stopPrank(); - } - - function _setupSpendingLimits() internal { - // Only set spending limits for non-super admin keys - - // Daily ETH limit for secp256k1Key (not a super admin) - if (secp256k1Key.keyHash != bytes32(0)) { - userAccount.setSpendLimit( - secp256k1Key.keyHash, address(0), GuardedExecutor.SpendPeriod.Day, 1 ether - ); - - // Weekly ETH limit for secp256k1Key - userAccount.setSpendLimit( - secp256k1Key.keyHash, address(0), GuardedExecutor.SpendPeriod.Week, 5 ether - ); - - // Monthly token1 limit for secp256k1Key - userAccount.setSpendLimit( - secp256k1Key.keyHash, - address(mockToken1), - GuardedExecutor.SpendPeriod.Month, - 1000e18 - ); - } - - // Daily token2 limit for webAuthnP256Key (not a super admin) - if (webAuthnP256Key.keyHash != bytes32(0)) { - userAccount.setSpendLimit( - webAuthnP256Key.keyHash, - address(mockToken2), - GuardedExecutor.SpendPeriod.Day, - 100e18 - ); - } - - // Hour ETH limit for externalKey (not a super admin) - if (externalKey.keyHash != bytes32(0)) { - userAccount.setSpendLimit( - externalKey.keyHash, address(0), GuardedExecutor.SpendPeriod.Hour, 0.1 ether - ); - } - - // Forever limit for p256Key (if authorized and not a super admin) - if (p256Key.keyHash != bytes32(0) && !p256Key.k.isSuperAdmin) { - userAccount.setSpendLimit( - p256Key.keyHash, address(0), GuardedExecutor.SpendPeriod.Forever, 10 ether - ); - } - } - - function _setupExecutionPermissions() internal { - // Setup canExecute permissions (only for non-super admin keys) - address target1 = address(0x1234); - address target2 = address(0x5678); - bytes4 selector1 = bytes4(keccak256("transfer(address,uint256)")); - bytes4 selector2 = bytes4(keccak256("approve(address,uint256)")); - - // Only set for non-super admin secp256k1Key - if (secp256k1Key.keyHash != bytes32(0) && !secp256k1Key.k.isSuperAdmin) { - userAccount.setCanExecute(secp256k1Key.keyHash, target1, selector1, true); - userAccount.setCanExecute(secp256k1Key.keyHash, target2, selector2, true); - } - - // Only set for p256Key if it's not a super admin - if (p256Key.keyHash != bytes32(0) && !p256Key.k.isSuperAdmin) { - userAccount.setCanExecute(p256Key.keyHash, target1, selector2, true); - } - - // Only set for webAuthnP256Key if it's not a super admin - if (webAuthnP256Key.keyHash != bytes32(0) && !webAuthnP256Key.k.isSuperAdmin) { - userAccount.setCanExecute(webAuthnP256Key.keyHash, target2, selector1, true); - } - - // Setup call checkers (only for non-super admin keys) - address checker1 = address(0xAAAA); - address checker2 = address(0xBBBB); - - if (secp256k1Key.keyHash != bytes32(0) && !secp256k1Key.k.isSuperAdmin) { - userAccount.setCallChecker(secp256k1Key.keyHash, target1, checker1); - } - - if (webAuthnP256Key.keyHash != bytes32(0) && !webAuthnP256Key.k.isSuperAdmin) { - userAccount.setCallChecker(webAuthnP256Key.keyHash, target2, checker2); - } - } - - function _fundAccount() internal { - // Fund with ETH - vm.deal(address(userAccount), 10 ether); - - // Fund with tokens - mockToken1.mint(address(userAccount), 10000e18); - mockToken2.mint(address(userAccount), 5000e18); - } - - function _capturePreUpgradeState() internal { - // Capture authorized keys - (, bytes32[] memory keyHashes) = userAccount.getKeys(); - - // Clear and populate pre-upgrade key hashes - delete preKeyHashes; - for (uint256 i = 0; i < keyHashes.length; i++) { - preKeyHashes.push(keyHashes[i]); - bytes32 keyHash = keyHashes[i]; - preKeys[keyHash] = userAccount.getKey(keyHash); - preAuthorized[keyHash] = true; - } - - // Capture balances - preEthBalance = address(userAccount).balance; - preToken1Balance = mockToken1.balanceOf(address(userAccount)); - preToken2Balance = mockToken2.balanceOf(address(userAccount)); - - // Capture nonce - preNonce = userAccount.getNonce(0); - } - - function _capturePostUpgradeState() internal { - // Capture authorized keys - (, bytes32[] memory keyHashes) = userAccount.getKeys(); - - // Clear and populate post-upgrade key hashes - delete postKeyHashes; - for (uint256 i = 0; i < keyHashes.length; i++) { - postKeyHashes.push(keyHashes[i]); - bytes32 keyHash = keyHashes[i]; - postKeys[keyHash] = userAccount.getKey(keyHash); - postAuthorized[keyHash] = true; - } - - // Capture balances - postEthBalance = address(userAccount).balance; - postToken1Balance = mockToken1.balanceOf(address(userAccount)); - postToken2Balance = mockToken2.balanceOf(address(userAccount)); - - // Capture nonce - postNonce = userAccount.getNonce(0); - } - - function _verifyStatePreservation() internal view { - // Verify all keys are preserved - assertEq(preKeyHashes.length, postKeyHashes.length, "Number of authorized keys changed"); - - for (uint256 i = 0; i < preKeyHashes.length; i++) { - bytes32 keyHash = preKeyHashes[i]; - - assertTrue(postAuthorized[keyHash], "Key was deauthorized during upgrade"); - - IthacaAccount.Key memory preKey = preKeys[keyHash]; - IthacaAccount.Key memory postKey = postKeys[keyHash]; - - assertEq(preKey.expiry, postKey.expiry, "Key expiry changed"); - assertEq(uint8(preKey.keyType), uint8(postKey.keyType), "Key type changed"); - assertEq(preKey.isSuperAdmin, postKey.isSuperAdmin, "Key super admin status changed"); - assertEq(preKey.publicKey, postKey.publicKey, "Key public key changed"); - } - - // Verify balances preserved - assertEq(preEthBalance, postEthBalance, "ETH balance changed"); - assertEq(preToken1Balance, postToken1Balance, "Token1 balance changed"); - assertEq(preToken2Balance, postToken2Balance, "Token2 balance changed"); - - // Verify nonce preserved - assertEq(preNonce, postNonce, "Nonce changed"); - } - - function _testPostUpgradeFunctionality() internal { - vm.startPrank(userEOA); - - // Test 1: P256 keys can now be super admins (new in v0.5.7+) - PassKey memory newP256SuperAdmin = _randomSecp256r1PassKey(); - newP256SuperAdmin.k.isSuperAdmin = true; - newP256SuperAdmin.k.expiry = 0; - - // This should succeed in upgraded version - bytes32 newP256KeyHash = userAccount.authorize(newP256SuperAdmin.k); - IthacaAccount.Key memory retrievedKey = userAccount.getKey(newP256KeyHash); - assertEq( - uint8(retrievedKey.keyType), uint8(IthacaAccount.KeyType.P256), "Key type mismatch" - ); - assertTrue(retrievedKey.isSuperAdmin, "P256 should be super admin after upgrade"); - - // Test 2: Add a new non-super-admin key and set spending limit - PassKey memory newRegularKey = _randomSecp256k1PassKey(); - newRegularKey.k.isSuperAdmin = false; - newRegularKey.k.expiry = 0; - - bytes32 newRegularKeyHash = userAccount.authorize(newRegularKey.k); - - // Set spending limit for the regular key (not super admin) - userAccount.setSpendLimit( - newRegularKeyHash, address(0), GuardedExecutor.SpendPeriod.Week, 2 ether - ); - - GuardedExecutor.SpendInfo[] memory spendInfos = userAccount.spendInfos(newRegularKeyHash); - bool foundWeeklyLimit = false; - for (uint256 i = 0; i < spendInfos.length; i++) { - if ( - spendInfos[i].period == GuardedExecutor.SpendPeriod.Week - && spendInfos[i].token == address(0) - ) { - assertEq(spendInfos[i].limit, 2 ether, "Weekly limit not set correctly"); - foundWeeklyLimit = true; - break; - } - } - assertTrue(foundWeeklyLimit, "Weekly limit not found"); - - // Test 3: Verify keys can still be used (without actual execution) - // We verify the key is still authorized and has correct properties - IthacaAccount.Key memory existingKey = userAccount.getKey(secp256k1Key.keyHash); - assertEq( - uint8(existingKey.keyType), uint8(IthacaAccount.KeyType.Secp256k1), "Key type changed" - ); - assertFalse(existingKey.isSuperAdmin, "Key admin status changed"); - - // Test 4: Test revoke and re-authorize with a new key - // Create a new key to test revoke/re-authorize functionality - PassKey memory testRevokeKey = _randomSecp256k1PassKey(); - testRevokeKey.k.isSuperAdmin = false; - testRevokeKey.k.expiry = 0; - - bytes32 testRevokeKeyHash = userAccount.authorize(testRevokeKey.k); - - // Now revoke it - userAccount.revoke(testRevokeKeyHash); - - // Verify key is revoked by checking it no longer exists - // After revocation, getKey will revert with KeyDoesNotExist - vm.expectRevert(abi.encodeWithSelector(IthacaAccount.KeyDoesNotExist.selector)); - userAccount.getKey(testRevokeKeyHash); - - // Re-authorize - bytes32 reauthorizedHash = userAccount.authorize(testRevokeKey.k); - assertEq(reauthorizedHash, testRevokeKeyHash, "Key hash changed on re-authorization"); - - vm.stopPrank(); - } - - function test_UpgradeWithSpendLimitEnabledFlag() public { - // This test verifies the spend limit enabled flag feature added in newer versions - - vm.startPrank(userEOA); - - // Authorize a key with spending limits - PassKey memory testKey = _randomSecp256k1PassKey(); - bytes32 keyHash = userAccount.authorize(testKey.k); - - // Set spending limit - userAccount.setSpendLimit(keyHash, address(0), GuardedExecutor.SpendPeriod.Day, 0.5 ether); - - // Fund account - vm.deal(address(userAccount), 5 ether); - - vm.stopPrank(); - - // Perform upgrade - _performUpgrade(); - - // Verify spending limits still work after upgrade - GuardedExecutor.SpendInfo[] memory spendInfos = userAccount.spendInfos(keyHash); - assertEq(spendInfos.length, 1, "Spending limit not preserved"); - assertEq(spendInfos[0].limit, 0.5 ether, "Spending limit value changed"); - - vm.stopPrank(); - } - - function test_UpgradeWithMultipleKeyTypes() public { - // Test upgrade with all key types authorized - - vm.startPrank(userEOA); - - // Authorize all key types - PassKey[] memory keys = new PassKey[](4); - keys[0] = _randomSecp256r1PassKey(); - keys[1] = _randomSecp256k1PassKey(); - keys[2] = _randomSecp256r1PassKey(); - keys[2].k.keyType = IthacaAccount.KeyType.WebAuthnP256; - keys[3].k.keyType = IthacaAccount.KeyType.External; - keys[3].k.publicKey = abi.encodePacked(_randomAddress(), bytes12(uint96(_randomUniform()))); - keys[3].keyHash = _hash(keys[3].k); - - bytes32[] memory keyHashes = new bytes32[](4); - for (uint256 i = 0; i < keys.length; i++) { - // Some key types might fail in old versions, handle gracefully - try userAccount.authorize(keys[i].k) returns (bytes32 kh) { - keyHashes[i] = kh; - } catch { - // Skip if authorization fails - } - } - - // Capture authorized count before upgrade - (, bytes32[] memory keyHashesBefore) = userAccount.getKeys(); - uint256 authorizedCountBefore = keyHashesBefore.length; - - vm.stopPrank(); - - // Perform upgrade - _performUpgrade(); - - // Verify all keys preserved - (, bytes32[] memory keyHashesAfter) = userAccount.getKeys(); - uint256 authorizedCountAfter = keyHashesAfter.length; - assertEq(authorizedCountBefore, authorizedCountAfter, "Key count changed during upgrade"); - - vm.stopPrank(); - } - - function test_UpgradePreservesComplexSpendingState() public { - // Test that complex spending state with partially spent limits is preserved - - vm.startPrank(userEOA); - - // Setup key and limits - PassKey memory spendKey = _randomSecp256k1PassKey(); - bytes32 keyHash = userAccount.authorize(spendKey.k); - - // Set multiple spending limits - userAccount.setSpendLimit(keyHash, address(0), GuardedExecutor.SpendPeriod.Day, 1 ether); - userAccount.setSpendLimit(keyHash, address(0), GuardedExecutor.SpendPeriod.Week, 3 ether); - userAccount.setSpendLimit(keyHash, address(0), GuardedExecutor.SpendPeriod.Month, 10 ether); - - // Fund account - vm.deal(address(userAccount), 20 ether); - vm.stopPrank(); - - // Capture spending state before upgrade - GuardedExecutor.SpendInfo[] memory spendsBefore = userAccount.spendInfos(keyHash); - - // Verify spending limits are set - uint256 limitsCount = 0; - for (uint256 i = 0; i < spendsBefore.length; i++) { - if (spendsBefore[i].token == address(0)) { - limitsCount++; - } - } - assertEq(limitsCount, 3, "Should have 3 ETH spending limits"); - - // Perform upgrade - _performUpgrade(); - - // Verify spending state preserved - GuardedExecutor.SpendInfo[] memory spendsAfter = userAccount.spendInfos(keyHash); - - // Verify all limits still exist - uint256 limitsCountAfter = 0; - for (uint256 i = 0; i < spendsAfter.length; i++) { - if (spendsAfter[i].token == address(0)) { - limitsCountAfter++; - } - } - assertEq(limitsCountAfter, 3, "Spending limits not preserved after upgrade"); - - // Verify limits match - assertEq(spendsBefore.length, spendsAfter.length, "Number of spending limits changed"); - for (uint256 i = 0; i < spendsBefore.length; i++) { - assertEq(spendsBefore[i].limit, spendsAfter[i].limit, "Limit value changed"); - assertEq(uint8(spendsBefore[i].period), uint8(spendsAfter[i].period), "Period changed"); - } - } -} diff --git a/test/utils/interfaces/ISafe.sol b/test/utils/interfaces/ISafe.sol deleted file mode 100644 index d9c3d3e1..00000000 --- a/test/utils/interfaces/ISafe.sol +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.28; - -import "./IERC4337EntryPoint.sol"; - -interface ISafe { - function setup( - address[] calldata _owners, - uint256 _threshold, - address to, - bytes calldata data, - address fallbackHandler, - address paymentToken, - uint256 payment, - address paymentReceiver - ) external; - - function enableModule(address module) external; - - function execTransaction( - address to, - uint256 value, - bytes calldata data, - uint8 operation, - uint256 safeTxGas, - uint256 baseGas, - uint256 gasPrice, - address gasToken, - address payable refundReceiver, - bytes memory signatures - ) external payable returns (bool success); - - function getFallbackHandler() external view returns (address); -} - -interface ISafeProxyFactory { - function createProxyWithNonce(address _singleton, bytes memory initializer, uint256 saltNonce) - external - returns (address proxy); -} - -interface ISafe4337Module { - function SUPPORTED_ENTRYPOINT() external view returns (address); - - function getOperationHash(UserOperation calldata userOp) - external - view - returns (bytes32 operationHash); - - function executeUserOp(address to, uint256 value, bytes calldata data, uint8 operation) external; - - function domainSeparator() external view returns (bytes32); -} - -interface IAddModulesLib { - function enableModules(address[] memory modules) external; -} diff --git a/test/utils/mocks/MockPayerWithSignatureOptimized.sol b/test/utils/mocks/MockPayerWithSignatureOptimized.sol deleted file mode 100644 index 1d7701bf..00000000 --- a/test/utils/mocks/MockPayerWithSignatureOptimized.sol +++ /dev/null @@ -1,84 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.4; - -import {TokenTransferLib} from "../../../src/libraries/TokenTransferLib.sol"; -import {Ownable} from "solady/auth/Ownable.sol"; -import {ECDSA} from "solady/utils/ECDSA.sol"; -import {ICommon} from "../../../src/interfaces/ICommon.sol"; -import {IOrchestrator} from "../../../src/interfaces/IOrchestrator.sol"; -/// @dev WARNING! This mock is strictly intended for testing purposes only. -/// Do NOT copy anything here into production code unless you really know what you are doing. - -contract MockPayerWithSignatureOptimized is Ownable { - error InvalidSignature(); - - address public signer; - - address public immutable APPROVED_ORCHESTRATOR; - - event Compensated( - address indexed paymentToken, - address indexed paymentRecipient, - uint256 paymentAmount, - address indexed eoa, - bytes32 keyHash - ); - - constructor(address orchestrator) { - APPROVED_ORCHESTRATOR = orchestrator; - _initializeOwner(msg.sender); - } - - function setSigner(address newSinger) public onlyOwner { - signer = newSinger; - } - - /// @dev `address(0)` denote native token (i.e. Ether). - function withdrawTokens(address token, address recipient, uint256 amount) - public - virtual - onlyOwner - { - TokenTransferLib.safeTransfer(token, recipient, amount); - } - - /// @dev Pays `paymentAmount` of `paymentToken` to the `paymentRecipient`. - /// The EOA and token details are extracted from the `encodedIntent`. - /// Reverts if the specified Orchestrator (`msg.sender`) is not approved. - /// NOTE: This mock no longer verifies signatures within the pay function itself, - /// aligning with the Account/Orchestrator pattern where verification happens before payment. - /// @param paymentAmount The amount to pay. - /// @param keyHash The key hash associated with the operation (not used in this mock's logic but kept for signature compatibility). - /// @param encodedIntent ABI encoded Intent struct. - function pay( - uint256 paymentAmount, - bytes32 keyHash, - bytes32 digest, - bytes calldata encodedIntent - ) public virtual { - if (msg.sender != APPROVED_ORCHESTRATOR) revert Unauthorized(); - - ICommon.Intent calldata u; - assembly { - let t := calldataload(encodedIntent.offset) - u := add(t, encodedIntent.offset) - } - - bytes32 signatureDigest = computeSignatureDigest(digest); - - if (ECDSA.recover(signatureDigest, u.paymentSignature) != signer) { - revert InvalidSignature(); - } - - TokenTransferLib.safeTransfer(u.paymentToken, u.paymentRecipient, paymentAmount); - - emit Compensated(u.paymentToken, u.paymentRecipient, paymentAmount, u.eoa, keyHash); - } - - function computeSignatureDigest(bytes32 intentDigest) public view returns (bytes32) { - // We shall just use this simplified hash instead of EIP712. - return keccak256(abi.encode(intentDigest, block.chainid, address(this))); - } - - receive() external payable {} -} From 7fe997153f75102ece5c4c50b80eaffbda41e4b0 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Wed, 28 Jan 2026 23:20:40 +0700 Subject: [PATCH 22/55] Fix commit user email format in CI workflow (#18) https://github.com/Dargon789/account/commit/30a2096cce0811834c5835a8698ec525c3923112 Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> From 06b0f0dea9f7aae6d4b242e5d75f568d8e124618 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Wed, 28 Jan 2026 23:26:31 +0700 Subject: [PATCH 23/55] Potential fix for code scanning alert no. 4: Workflow does not contain permissions (#19) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 95878eae..7dc9d0b2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,6 +8,8 @@ on: jobs: test: name: Tests + permissions: + contents: read runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 From 78e10760203fef7ba2b9935dda61da01866f2f51 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Wed, 28 Jan 2026 23:30:33 +0700 Subject: [PATCH 24/55] Potential fix for code scanning alert no. 1: Workflow does not contain permissions (#20) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/claude-code.yml | 2 ++ 1 file changed, 2 insertions(+) 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: From a5c546424561496a899d5bab144fe7432c588a65 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Wed, 28 Jan 2026 23:43:29 +0700 Subject: [PATCH 25/55] Update ci.yaml (#21) reback use test CI and automation chores (ithacaxyz#394) https://github.com/Dargon789/account/commit/7dd8a5d91c162b89316e367f0fb159f47abfeab0 Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> From 3e316a0ecde15e0eaf2ff402e550cb86f9658e27 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Thu, 29 Jan 2026 00:01:19 +0700 Subject: [PATCH 26/55] Master (#22) * Merge branch 'master' (#8) * Update ci.yaml (#2) CI: Update Forge test command to use --rerun and increase verbosity to -vvvvv Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#4) Enhance the CI test step to automatically rerun failed tests and increase verbosity in Forge. CI: Enable the --rerun flag for Forge tests to retry failures automatically Increase Forge test verbosity from -vvv to -vvvvv Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#5) Change Forge test invocation from "forge test --rerun -vvvvv" to "forge test -vvv" Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Create CNAME --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Revert "fix vm block accoount (#11)" (#13) Reverts #11 Summary by Sourcery CI: Update the Forge test command in the CI workflow to enable reruns and increase verbosity. This reverts commit 942017fb59217f59933bb4666f93f76f79066a16. --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7dc9d0b2..c5f35baa 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -31,7 +31,7 @@ jobs: - name: Run tests run: | - forge test -vvv + forge test --rerun -vvvvv - name: Snapshot main branch run: git fetch origin main && git worktree prune &&rm -rf .snapshot_worktree && git worktree add .snapshot_worktree origin/main && (cd .snapshot_worktree && forge snapshot --match-contract Benchmark --snap .temp-snapshot) && cp .snapshot_worktree/.temp-snapshot gas-snapshots/.gas-snapshot-main && git worktree remove --force .snapshot_worktree && git worktree prune From abebf0c3370e4f906e2a9ecd1110d8678b06a4f8 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Thu, 29 Jan 2026 00:11:45 +0700 Subject: [PATCH 27/55] Update ci.yaml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> From 35961517f7770b7fe93130049a4b438ed0af5983 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Thu, 29 Jan 2026 00:29:00 +0700 Subject: [PATCH 28/55] Refactor function signatures and formatting for consistency Updated function signatures in IFunder and SimpleFunder for single-line style, and improved code formatting in IthacaAccount, test/Account.t.sol, test/Benchmark.t.sol, and test/LayerZeroSettler.t.sol for readability and consistency. No logic changes were made. --- foundry.lock | 18 +++++++++--------- src/IthacaAccount.sol | 5 +++-- src/SimpleFunder.sol | 9 ++++----- src/interfaces/IFunder.sol | 7 ++----- test/Account.t.sol | 6 +++++- test/Benchmark.t.sol | 5 +++-- test/LayerZeroSettler.t.sol | 6 +++--- 7 files changed, 29 insertions(+), 27 deletions(-) diff --git a/foundry.lock b/foundry.lock index 5d4f797b..fbc90982 100644 --- a/foundry.lock +++ b/foundry.lock @@ -1,23 +1,23 @@ { + "lib/LayerZero-v2": { + "rev": "88428755be6caa71cb1d2926141d73c8989296b5" + }, + "lib/devtools": { + "rev": "01c1eaeb123e4364fbe96cdddf875640edf2566c" + }, + "lib/forge-std": { + "rev": "a3dca253700f19f15b1837c57c67b9388f5cc3fb" + }, "lib/murky": { "rev": "5feccd1253d7da820f7cccccdedf64471025455d" }, "lib/openzeppelin-contracts": { "rev": "69c8def5f222ff96f2b5beff05dfba996368aa79" }, - "lib/forge-std": { - "rev": "a3dca253700f19f15b1837c57c67b9388f5cc3fb" - }, "lib/solady": { "branch": { "name": "main", "rev": "834bbc4fd366ca8bce8c532a0e3b34eca6be709c" } - }, - "lib/LayerZero-v2": { - "rev": "88428755be6caa71cb1d2926141d73c8989296b5" - }, - "lib/devtools": { - "rev": "01c1eaeb123e4364fbe96cdddf875640edf2566c" } } \ No newline at end of file diff --git a/src/IthacaAccount.sol b/src/IthacaAccount.sol index f770cabc..c15368d6 100644 --- a/src/IthacaAccount.sol +++ b/src/IthacaAccount.sol @@ -614,8 +614,9 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { // `keccak256(abi.encode(key.keyType, keccak256(key.publicKey)))`. keyHash = hash(key); AccountStorage storage $ = _getAccountStorage(); - $.keyStorage[keyHash] - .set(abi.encodePacked(key.publicKey, key.expiry, key.keyType, key.isSuperAdmin)); + $.keyStorage[keyHash].set( + abi.encodePacked(key.publicKey, key.expiry, key.keyType, key.isSuperAdmin) + ); $.keyHashes.add(keyHash); } diff --git a/src/SimpleFunder.sol b/src/SimpleFunder.sol index e74947fb..959723d1 100644 --- a/src/SimpleFunder.sol +++ b/src/SimpleFunder.sol @@ -133,11 +133,10 @@ contract SimpleFunder is EIP712, Ownable, IFunder { /// @dev Allows the orchestrator to fund an account. /// The `digest` includes the intent nonce and the transfers. - function fund( - bytes32 digest, - ICommon.Transfer[] memory transfers, - bytes memory funderSignature - ) public override { + function fund(bytes32 digest, ICommon.Transfer[] memory transfers, bytes memory funderSignature) + public + override + { if (!orchestrators[msg.sender]) { revert OnlyOrchestrator(); } diff --git a/src/interfaces/IFunder.sol b/src/interfaces/IFunder.sol index 90c2547f..ddbacc0f 100644 --- a/src/interfaces/IFunder.sol +++ b/src/interfaces/IFunder.sol @@ -16,9 +16,6 @@ interface IFunderV4 { interface IFunder is IFunderV4 { /// @dev Checks if fund transfers are valid given a funderSignature. /// @dev Funder implementations must revert if the signature is invalid. - function fund( - bytes32 digest, - ICommon.Transfer[] memory transfers, - bytes memory funderSignature - ) external; + function fund(bytes32 digest, ICommon.Transfer[] memory transfers, bytes memory funderSignature) + external; } diff --git a/test/Account.t.sol b/test/Account.t.sol index 87c007d9..083e6b3a 100644 --- a/test/Account.t.sol +++ b/test/Account.t.sol @@ -462,6 +462,10 @@ contract AccountTest is BaseTest { vm.prank(address(d.d)); d.d.execute(_ERC7821_BATCH_EXECUTION_MODE, executionData); - assertEq(contextKeyHash, bytes32(0), "Context key hash should be zero for self-execution without opData"); + assertEq( + contextKeyHash, + bytes32(0), + "Context key hash should be zero for self-execution without opData" + ); } } diff --git a/test/Benchmark.t.sol b/test/Benchmark.t.sol index 2ce04524..c070c55c 100644 --- a/test/Benchmark.t.sol +++ b/test/Benchmark.t.sol @@ -120,8 +120,9 @@ contract BenchmarkTest is BaseTest { token0, token1, 1 ether, 1 ether, 1, 1, address(this), block.timestamp + 999 ); - IStakeManager(_ERC4337_ENTRYPOINT_V06_ADDR) - .depositTo{value: 1 ether}(_PIMLICO_PAYMASTER_V06); + IStakeManager(_ERC4337_ENTRYPOINT_V06_ADDR).depositTo{value: 1 ether}( + _PIMLICO_PAYMASTER_V06 + ); IStakeManager(_ERC4337_ENTRYPOINT_ADDR).depositTo{value: 1 ether}(_PIMLICO_PAYMASTER_V07); (paymasterSigner, paymasterPrivateKey) = makeAddrAndKey(""); diff --git a/test/LayerZeroSettler.t.sol b/test/LayerZeroSettler.t.sol index 941f2311..fe9be6ae 100644 --- a/test/LayerZeroSettler.t.sol +++ b/test/LayerZeroSettler.t.sol @@ -716,9 +716,9 @@ contract LayerZeroSettlerTest is Test { // Should revert with InvalidL0SettlerSignature vm.expectRevert(abi.encodeWithSelector(LayerZeroSettler.InvalidL0SettlerSignature.selector)); - settlerA.executeSend{ - value: fee - }(orchestrator, settlementId, settlerContext, invalidSignature); + settlerA.executeSend{value: fee}( + orchestrator, settlementId, settlerContext, invalidSignature + ); } function test_executeSend_preventReplay() public { From 05f08430b34f8852dd7e4e04b6fe98ea59dbfb37 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Tue, 10 Feb 2026 20:23:00 +0700 Subject: [PATCH 29/55] # Default ignored files --- .husky/pre-commit | 0 .idea/.gitignore | 3 + .idea/account.iml | 13 + .idea/caches/deviceStreaming.xml | 1698 ++++++++++++++++++++ .idea/codeStyles/Project.xml | 117 ++ .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/copilot.data.migration.ask2agent.xml | 6 + .idea/markdown.xml | 8 + .idea/modules.xml | 8 + .idea/vcs.xml | 22 + deploy/execute_config.sh | 0 deploy/verify_config.sh | 0 lib/forge-std | 2 +- prep/check-bytecode-changes.js | 0 14 files changed, 1881 insertions(+), 1 deletion(-) mode change 100755 => 100644 .husky/pre-commit create mode 100644 .idea/.gitignore create mode 100644 .idea/account.iml create mode 100644 .idea/caches/deviceStreaming.xml create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/copilot.data.migration.ask2agent.xml create mode 100644 .idea/markdown.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml mode change 100755 => 100644 deploy/execute_config.sh mode change 100755 => 100644 deploy/verify_config.sh mode change 100755 => 100644 prep/check-bytecode-changes.js diff --git a/.husky/pre-commit b/.husky/pre-commit old mode 100755 new mode 100644 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/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/forge-std b/lib/forge-std index c2cf7017..aeb45e9f 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit c2cf7017d27c1d20e74ace4dacb6c5ce4bbbe899 +Subproject commit aeb45e9f32ef8ca78f0aeda17596e9c46374da41 diff --git a/prep/check-bytecode-changes.js b/prep/check-bytecode-changes.js old mode 100755 new mode 100644 From 9745888cae52f0c5a4369f3a666f5a3dd6aff3c1 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Sun, 15 Feb 2026 22:16:31 +0700 Subject: [PATCH 30/55] .snapshot_worktree --- .husky/pre-commit | 0 deploy/execute_config.sh | 0 deploy/verify_config.sh | 0 lib/LayerZero-v2 | 2 +- lib/forge-std | 2 +- prep/check-bytecode-changes.js | 0 6 files changed, 2 insertions(+), 2 deletions(-) mode change 100755 => 100644 .husky/pre-commit mode change 100755 => 100644 deploy/execute_config.sh mode change 100755 => 100644 deploy/verify_config.sh mode change 100755 => 100644 prep/check-bytecode-changes.js diff --git a/.husky/pre-commit b/.husky/pre-commit old mode 100755 new mode 100644 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..aeb45e9f 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit c2cf7017d27c1d20e74ace4dacb6c5ce4bbbe899 +Subproject commit aeb45e9f32ef8ca78f0aeda17596e9c46374da41 diff --git a/prep/check-bytecode-changes.js b/prep/check-bytecode-changes.js old mode 100755 new mode 100644 From e730f4120ad57055b4abed36f60175adfb08c402 Mon Sep 17 00:00:00 2001 From: googleworkspace-bot Date: Fri, 13 Mar 2026 15:56:45 +0700 Subject: [PATCH 31/55] pre-commit --- .husky/pre-commit | 0 deploy/execute_config.sh | 0 deploy/verify_config.sh | 0 lib/forge-std | 2 +- prep/check-bytecode-changes.js | 0 5 files changed, 1 insertion(+), 1 deletion(-) mode change 100755 => 100644 .husky/pre-commit mode change 100755 => 100644 deploy/execute_config.sh mode change 100755 => 100644 deploy/verify_config.sh mode change 100755 => 100644 prep/check-bytecode-changes.js diff --git a/.husky/pre-commit b/.husky/pre-commit old mode 100755 new mode 100644 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/forge-std b/lib/forge-std index c2cf7017..0844d7e1 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit c2cf7017d27c1d20e74ace4dacb6c5ce4bbbe899 +Subproject commit 0844d7e1fc5e60d77b68e469bff60265f236c398 diff --git a/prep/check-bytecode-changes.js b/prep/check-bytecode-changes.js old mode 100755 new mode 100644 From 42416668eb6450d9fb45f00e74e0921eaa618a91 Mon Sep 17 00:00:00 2001 From: googleworkspace-bot Date: Fri, 13 Mar 2026 16:20:26 +0700 Subject: [PATCH 32/55] deploy execute_config.sh --- deploy/execute_config.sh | 0 deploy/verify_config.sh | 0 prep/check-bytecode-changes.js | 0 3 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 deploy/execute_config.sh mode change 100755 => 100644 deploy/verify_config.sh mode change 100755 => 100644 prep/check-bytecode-changes.js 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/prep/check-bytecode-changes.js b/prep/check-bytecode-changes.js old mode 100755 new mode 100644 From cbd478bfdb4285a3792129841643941141a5ae1f Mon Sep 17 00:00:00 2001 From: googleworkspace-bot Date: Fri, 13 Mar 2026 16:23:26 +0700 Subject: [PATCH 33/55] Update forge-std --- lib/forge-std | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/forge-std b/lib/forge-std index 0844d7e1..c2cf7017 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 0844d7e1fc5e60d77b68e469bff60265f236c398 +Subproject commit c2cf7017d27c1d20e74ace4dacb6c5ce4bbbe899 From f03357c4a9ebb83e48a90205dafc5943739bff00 Mon Sep 17 00:00:00 2001 From: googleworkspace-bot Date: Sat, 14 Mar 2026 15:09:51 +0700 Subject: [PATCH 34/55] Refactor function signatures and formatting for consistency --- prep/check-bytecode-changes.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 prep/check-bytecode-changes.js diff --git a/prep/check-bytecode-changes.js b/prep/check-bytecode-changes.js old mode 100755 new mode 100644 From ab9e8839428e1a9f047cb65d708ae02e270b23e9 Mon Sep 17 00:00:00 2001 From: googleworkspace-bot Date: Fri, 10 Apr 2026 07:28:05 +0700 Subject: [PATCH 35/55] Normalize file modes and update forge-std submodule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove executable bit from deploy/execute_config.sh, deploy/verify_config.sh, and prep/check-bytecode-changes.js (mode 100755 → 100644). Update lib/forge-std submodule from commit c2cf7017d27c1d20e74ace4dacb6c5ce4bbbe899 to 07853315f998f94dc724e464b1bab1270888ee64. No other content changes. --- deploy/execute_config.sh | 0 deploy/verify_config.sh | 0 lib/forge-std | 2 +- prep/check-bytecode-changes.js | 0 4 files changed, 1 insertion(+), 1 deletion(-) mode change 100755 => 100644 deploy/execute_config.sh mode change 100755 => 100644 deploy/verify_config.sh mode change 100755 => 100644 prep/check-bytecode-changes.js 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/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 From 36249c78e5e0dcb50bafa0a262e3c0b1cffba637 Mon Sep 17 00:00:00 2001 From: googleworkspace-bot Date: Sat, 11 Apr 2026 18:01:29 +0700 Subject: [PATCH 36/55] Update LayerZero-v2 --- lib/LayerZero-v2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From d54a37b96fbf4cfc1815c7bdf385cbc01fec2aff Mon Sep 17 00:00:00 2001 From: googleworkspace-bot Date: Sat, 11 Apr 2026 19:03:24 +0700 Subject: [PATCH 37/55] Update LayerZero-v2 --- lib/LayerZero-v2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 379fa7b0467720dd935259b8a2f210eeeecda1d3 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Sat, 11 Apr 2026 19:49:57 +0700 Subject: [PATCH 38/55] Dargon789 legion rouge (#61) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add merkle sigs natively into the account * fix: tests * test: add more sophisticated fuzz test * chore: bump contract versions due to bytecode changes - Contracts updated: IthacaAccount * Add .circleci/config.yml (#1) Add CircleCI configuration file to set up a basic pipeline with a say-hello job and workflow CI: Add .circleci/config.yml to define a say-hello job that checks out the code and prints a greeting using the cimg/base Docker image Add a workflow to orchestrate the say-hello job under the CircleCI 2.1 engine * Update ci.yaml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#2) CI: Update Forge test command to use --rerun and increase verbosity to -vvvvv Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#4) Enhance the CI test step to automatically rerun failed tests and increase verbosity in Forge. CI: Enable the --rerun flag for Forge tests to retry failures automatically Increase Forge test verbosity from -vvv to -vvvvv Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#5) Change Forge test invocation from "forge test --rerun -vvvvv" to "forge test -vvv" Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Create CNAME * Revert "Merge branch 'master'" This reverts commit 6c02fbff6486bb206c08af83d948830ace042353, reversing changes made to a317ddb944a1f5ed7294092790902da6b255f73f. * Create CNAME (#9) * Update ci.yaml (#2) CI: Update Forge test command to use --rerun and increase verbosity to -vvvvv Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#4) Enhance the CI test step to automatically rerun failed tests and increase verbosity in Forge. CI: Enable the --rerun flag for Forge tests to retry failures automatically Increase Forge test verbosity from -vvv to -vvvvv Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#5) Change Forge test invocation from "forge test --rerun -vvvvv" to "forge test -vvv" Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Create CNAME * Revert "Merge branch 'master'" This reverts commit 6c02fbff6486bb206c08af83d948830ace042353, reversing changes made to a317ddb944a1f5ed7294092790902da6b255f73f. --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Delete .circleci directory (#27) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * pre-commit * deploy execute_config.sh * Update forge-std * test: add tests for getContextKeyHash in Account.t.sol (#412) * Normalize file modes and update forge-std submodule Remove executable bit from deploy/execute_config.sh, deploy/verify_config.sh, and prep/check-bytecode-changes.js (mode 100755 → 100644). Update lib/forge-std submodule from commit c2cf7017d27c1d20e74ace4dacb6c5ce4bbbe899 to 07853315f998f94dc724e464b1bab1270888ee64. No other content changes. * Update LayerZero-v2 * Update LayerZero-v2 --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Tanishk Goyal Co-authored-by: GitHub Action Co-authored-by: googleworkspace-bot --- .github/workflows/ci.yaml | 2 +- CNAME | 1 + deploy/execute_config.sh | 0 deploy/verify_config.sh | 0 lib/LayerZero-v2 | 2 +- lib/forge-std | 2 +- prep/check-bytecode-changes.js | 0 src/IthacaAccount.sol | 47 +++++++++++++++++++++-- test/Account.t.sol | 69 +++++++++++++++++++++++++++++++++- test/Base.t.sol | 11 +++--- test/Orchestrator.t.sol | 5 ++- test/SimulateExecute.t.sol | 3 +- 12 files changed, 126 insertions(+), 16 deletions(-) create mode 100644 CNAME mode change 100755 => 100644 deploy/execute_config.sh mode change 100755 => 100644 deploy/verify_config.sh mode change 100755 => 100644 prep/check-bytecode-changes.js 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/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/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); From 0cb7a770acad764e0bd217f27a99bc2d0c5538b8 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Sat, 11 Apr 2026 20:29:55 +0700 Subject: [PATCH 39/55] Ithaca (#58) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add ERC20 transfer benchmark for Porto with passkey Add testERC20TransferViaPortoOrchestratorWithPasskey() benchmark to isolate passkey authentication costs from spend limit enforcement costs. - Uses secp256k1 passkey for transaction signing - Sets execution permissions for ERC20 transfers - Requires spend limits (set to max) for passkey operations - Gas cost: 116,094 (vs 97,030 without passkey, 117,083 with restrictive limits) - Provides clean measurement of passkey overhead (~19k gas) Resolves #272 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Tanishk Goyal * chore: use `auto-assign-pr.yml` org action * feat: add merkle sigs natively into the account * fix: tests * test: add more sophisticated fuzz test * chore: bump contract versions due to bytecode changes - Contracts updated: IthacaAccount * # Default ignored files * .snapshot_worktree * Delete .idea directory (#35) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Normalize file modes and update forge-std submodule Remove executable bit from deploy/execute_config.sh, deploy/verify_config.sh, and prep/check-bytecode-changes.js (mode 100755 → 100644). Update lib/forge-std submodule from commit c2cf7017d27c1d20e74ace4dacb6c5ce4bbbe899 to 07853315f998f94dc724e464b1bab1270888ee64. No other content changes. * Revert "Delete .idea directory (#35)" (#55) This reverts commit 512c204d2ce396d77dcf91b37ee514585ff337b3. --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Tanishk Goyal Co-authored-by: o-az Co-authored-by: Tanishk Goyal Co-authored-by: GitHub Action Co-authored-by: googleworkspace-bot --- .idea/.gitignore | 3 + .idea/account.iml | 13 + .idea/caches/deviceStreaming.xml | 1698 ++++++++++++++++++++ .idea/codeStyles/Project.xml | 117 ++ .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/copilot.data.migration.ask2agent.xml | 6 + .idea/markdown.xml | 8 + .idea/modules.xml | 8 + .idea/vcs.xml | 22 + test/Benchmark.t.sol | 46 + 10 files changed, 1926 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/account.iml create mode 100644 .idea/caches/deviceStreaming.xml create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/copilot.data.migration.ask2agent.xml create mode 100644 .idea/markdown.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml 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/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); From 430926dc5aa41d8cfdf9e81fabc003fcb82fe2d7 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Sat, 11 Apr 2026 21:03:06 +0700 Subject: [PATCH 40/55] Chore bump contract versions due to bytecode changes (#60) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add merkle sigs natively into the account * fix: tests * test: add more sophisticated fuzz test * chore: bump contract versions due to bytecode changes - Contracts updated: IthacaAccount * pre-commit * deploy execute_config.sh * Update forge-std * Normalize file modes and update forge-std submodule Remove executable bit from deploy/execute_config.sh, deploy/verify_config.sh, and prep/check-bytecode-changes.js (mode 100755 → 100644). Update lib/forge-std submodule from commit c2cf7017d27c1d20e74ace4dacb6c5ce4bbbe899 to 07853315f998f94dc724e464b1bab1270888ee64. No other content changes. * Update LayerZero-v2 * Update LayerZero-v2 --------- Co-authored-by: Tanishk Goyal Co-authored-by: GitHub Action Co-authored-by: googleworkspace-bot From de3b18806de911b846dd1daab9d0880df28fd022 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Sat, 11 Apr 2026 21:12:20 +0700 Subject: [PATCH 41/55] Legion rouge (#63) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add merkle sigs natively into the account * fix: tests * test: add more sophisticated fuzz test * chore: bump contract versions due to bytecode changes - Contracts updated: IthacaAccount * Add .circleci/config.yml (#1) Add CircleCI configuration file to set up a basic pipeline with a say-hello job and workflow CI: Add .circleci/config.yml to define a say-hello job that checks out the code and prints a greeting using the cimg/base Docker image Add a workflow to orchestrate the say-hello job under the CircleCI 2.1 engine * Update ci.yaml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#2) CI: Update Forge test command to use --rerun and increase verbosity to -vvvvv Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#4) Enhance the CI test step to automatically rerun failed tests and increase verbosity in Forge. CI: Enable the --rerun flag for Forge tests to retry failures automatically Increase Forge test verbosity from -vvv to -vvvvv Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#5) Change Forge test invocation from "forge test --rerun -vvvvv" to "forge test -vvv" Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Create CNAME * Revert "Merge branch 'master'" This reverts commit 6c02fbff6486bb206c08af83d948830ace042353, reversing changes made to a317ddb944a1f5ed7294092790902da6b255f73f. * Create CNAME (#9) * Update ci.yaml (#2) CI: Update Forge test command to use --rerun and increase verbosity to -vvvvv Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#4) Enhance the CI test step to automatically rerun failed tests and increase verbosity in Forge. CI: Enable the --rerun flag for Forge tests to retry failures automatically Increase Forge test verbosity from -vvv to -vvvvv Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#5) Change Forge test invocation from "forge test --rerun -vvvvv" to "forge test -vvv" Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Create CNAME * Revert "Merge branch 'master'" This reverts commit 6c02fbff6486bb206c08af83d948830ace042353, reversing changes made to a317ddb944a1f5ed7294092790902da6b255f73f. --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Delete .circleci directory (#27) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * test: add tests for getContextKeyHash in Account.t.sol (#412) * Normalize file modes and update forge-std submodule Remove executable bit from deploy/execute_config.sh, deploy/verify_config.sh, and prep/check-bytecode-changes.js (mode 100755 → 100644). Update lib/forge-std submodule from commit c2cf7017d27c1d20e74ace4dacb6c5ce4bbbe899 to 07853315f998f94dc724e464b1bab1270888ee64. No other content changes. * Update LayerZero-v2 --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Tanishk Goyal Co-authored-by: GitHub Action Co-authored-by: googleworkspace-bot From 7194a3182114111d2a5bcaa79823d06f9c058714 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Thu, 16 Apr 2026 01:43:43 +0000 Subject: [PATCH 42/55] feat: add merkle sigs natively into the account legion rouge (#70) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add merkle sigs natively into the account * fix: tests * test: add more sophisticated fuzz test * chore: bump contract versions due to bytecode changes - Contracts updated: IthacaAccount * Add .circleci/config.yml (#1) Add CircleCI configuration file to set up a basic pipeline with a say-hello job and workflow CI: Add .circleci/config.yml to define a say-hello job that checks out the code and prints a greeting using the cimg/base Docker image Add a workflow to orchestrate the say-hello job under the CircleCI 2.1 engine * Update ci.yaml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#2) CI: Update Forge test command to use --rerun and increase verbosity to -vvvvv Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#4) Enhance the CI test step to automatically rerun failed tests and increase verbosity in Forge. CI: Enable the --rerun flag for Forge tests to retry failures automatically Increase Forge test verbosity from -vvv to -vvvvv Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#5) Change Forge test invocation from "forge test --rerun -vvvvv" to "forge test -vvv" Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Create CNAME * Revert "Merge branch 'master'" This reverts commit 6c02fbff6486bb206c08af83d948830ace042353, reversing changes made to a317ddb944a1f5ed7294092790902da6b255f73f. * Create CNAME (#9) * Update ci.yaml (#2) CI: Update Forge test command to use --rerun and increase verbosity to -vvvvv Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#4) Enhance the CI test step to automatically rerun failed tests and increase verbosity in Forge. CI: Enable the --rerun flag for Forge tests to retry failures automatically Increase Forge test verbosity from -vvv to -vvvvv Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#5) Change Forge test invocation from "forge test --rerun -vvvvv" to "forge test -vvv" Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Create CNAME * Revert "Merge branch 'master'" This reverts commit 6c02fbff6486bb206c08af83d948830ace042353, reversing changes made to a317ddb944a1f5ed7294092790902da6b255f73f. --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Delete .circleci directory (#27) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * pre-commit * deploy execute_config.sh * Update forge-std * test: add tests for getContextKeyHash in Account.t.sol (#412) * Normalize file modes and update forge-std submodule Remove executable bit from deploy/execute_config.sh, deploy/verify_config.sh, and prep/check-bytecode-changes.js (mode 100755 → 100644). Update lib/forge-std submodule from commit c2cf7017d27c1d20e74ace4dacb6c5ce4bbbe899 to 07853315f998f94dc724e464b1bab1270888ee64. No other content changes. * Update LayerZero-v2 * Update LayerZero-v2 --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Tanishk Goyal Co-authored-by: GitHub Action Co-authored-by: googleworkspace-bot From d21b185a03b578daf994f04456b3c9c7e9e4a1ac Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Thu, 16 Apr 2026 23:01:30 +0000 Subject: [PATCH 43/55] Potential fix for code scanning alert no. 3: Workflow does not contain permissions (#66) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/test-infra.yml | 2 ++ 1 file changed, 2 insertions(+) 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: From bf50ece54e69eef4ec4c3cfc0a788ee6cab4ff08 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Fri, 17 Apr 2026 01:13:29 +0000 Subject: [PATCH 44/55] Potential fix for code scanning alert no. 2: Workflow does not contain permissions (#67) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/manual-deployment.yml | 3 +++ 1 file changed, 3 insertions(+) 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: From a001b1ed3da4b22c8ea4aebf308a39929fa1b5ab Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Fri, 17 Apr 2026 01:40:31 +0000 Subject: [PATCH 45/55] Potential fix for code scanning alert no. 1: Workflow does not contain permissions (#68) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/claude-code.yml | 2 ++ 1 file changed, 2 insertions(+) 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: From 8d284f43dab556bdabb57ddb2d83b13d22d9c5ae Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Fri, 17 Apr 2026 01:47:41 +0000 Subject: [PATCH 46/55] Ithaca (#80) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add ERC20 transfer benchmark for Porto with passkey Add testERC20TransferViaPortoOrchestratorWithPasskey() benchmark to isolate passkey authentication costs from spend limit enforcement costs. - Uses secp256k1 passkey for transaction signing - Sets execution permissions for ERC20 transfers - Requires spend limits (set to max) for passkey operations - Gas cost: 116,094 (vs 97,030 without passkey, 117,083 with restrictive limits) - Provides clean measurement of passkey overhead (~19k gas) Resolves #272 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Tanishk Goyal * chore: use `auto-assign-pr.yml` org action * feat: add merkle sigs natively into the account * fix: tests * test: add more sophisticated fuzz test * chore: bump contract versions due to bytecode changes - Contracts updated: IthacaAccount * # Default ignored files * .snapshot_worktree * Delete .idea directory (#35) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Normalize file modes and update forge-std submodule Remove executable bit from deploy/execute_config.sh, deploy/verify_config.sh, and prep/check-bytecode-changes.js (mode 100755 → 100644). Update lib/forge-std submodule from commit c2cf7017d27c1d20e74ace4dacb6c5ce4bbbe899 to 07853315f998f94dc724e464b1bab1270888ee64. No other content changes. * Revert "Delete .idea directory (#35)" (#55) This reverts commit 512c204d2ce396d77dcf91b37ee514585ff337b3. --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Tanishk Goyal Co-authored-by: o-az Co-authored-by: Tanishk Goyal Co-authored-by: GitHub Action Co-authored-by: googleworkspace-bot From 7fa59cd233cfa28d6c93af0e3489606dd9fb5802 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Fri, 17 Apr 2026 09:01:25 +0700 Subject: [PATCH 47/55] Delete CNAME --- CNAME | 1 - 1 file changed, 1 deletion(-) delete mode 100644 CNAME diff --git a/CNAME b/CNAME deleted file mode 100644 index a37c284a..00000000 --- a/CNAME +++ /dev/null @@ -1 +0,0 @@ -legion-rouge.vercel.app \ No newline at end of file From d73f1dc5c7ab19217a4b3f5416ca58e842390b4c Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Fri, 17 Apr 2026 02:43:35 +0000 Subject: [PATCH 48/55] ci(Mergify): configuration update (#81) Signed-off-by: Dargon789 --- .mergify.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .mergify.yml 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: {} From 80a315b8e5ae8a326a3c06ee0fac7ea10ae58006 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Fri, 17 Apr 2026 03:37:39 +0000 Subject: [PATCH 49/55] Revert "Ithaca (#58)" (#82) This reverts commit 0cb7a770acad764e0bd217f27a99bc2d0c5538b8. @gemini-code-assist Code Review This pull request cleans up the repository by removing various IDE-specific configuration files located in the .idea directory. Additionally, it removes the testERC20TransferViaPortoOrchestratorWithPasskey function from the BenchmarkTest contract in test/Benchmark.t.sol. I have no feedback to provide. --- .idea/.gitignore | 3 - .idea/account.iml | 13 - .idea/caches/deviceStreaming.xml | 1698 -------------------- .idea/codeStyles/Project.xml | 117 -- .idea/codeStyles/codeStyleConfig.xml | 5 - .idea/copilot.data.migration.ask2agent.xml | 6 - .idea/markdown.xml | 8 - .idea/modules.xml | 8 - .idea/vcs.xml | 22 - test/Benchmark.t.sol | 46 - 10 files changed, 1926 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/account.iml delete mode 100644 .idea/caches/deviceStreaming.xml delete mode 100644 .idea/codeStyles/Project.xml delete mode 100644 .idea/codeStyles/codeStyleConfig.xml delete mode 100644 .idea/copilot.data.migration.ask2agent.xml delete mode 100644 .idea/markdown.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d33521..00000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/account.iml b/.idea/account.iml deleted file mode 100644 index 1ba0d660..00000000 --- a/.idea/account.iml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/caches/deviceStreaming.xml b/.idea/caches/deviceStreaming.xml deleted file mode 100644 index cc4b430a..00000000 --- a/.idea/caches/deviceStreaming.xml +++ /dev/null @@ -1,1698 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index 4bec4ea8..00000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index a55e7a17..00000000 --- a/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask2agent.xml b/.idea/copilot.data.migration.ask2agent.xml deleted file mode 100644 index 1f2ea11e..00000000 --- a/.idea/copilot.data.migration.ask2agent.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/markdown.xml b/.idea/markdown.xml deleted file mode 100644 index c61ea334..00000000 --- a/.idea/markdown.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 4ae0bf91..00000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 1995e14d..00000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/test/Benchmark.t.sol b/test/Benchmark.t.sol index 13cbfcc1..2ce04524 100644 --- a/test/Benchmark.t.sol +++ b/test/Benchmark.t.sol @@ -1805,52 +1805,6 @@ 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); From 247bc8733fd6c7a0a3201e17d86ccee41b2d5708 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Mon, 20 Apr 2026 01:46:29 +0000 Subject: [PATCH 50/55] Update issue templates (#88) * Update issue templates * Update .github/ISSUE_TEMPLATE/feature_request.md Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update .github/ISSUE_TEMPLATE/bug_report.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update .github/ISSUE_TEMPLATE/bug_report.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update .github/ISSUE_TEMPLATE/custom.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/bug_report.md | 38 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/custom.md | 10 ++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/custom.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md 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. From cb23ac3bc44d071b243dbc865d181d65f255c6d6 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Mon, 20 Apr 2026 01:51:37 +0000 Subject: [PATCH 51/55] Legion (#72) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add ERC20 transfer benchmark for Porto with passkey Add testERC20TransferViaPortoOrchestratorWithPasskey() benchmark to isolate passkey authentication costs from spend limit enforcement costs. - Uses secp256k1 passkey for transaction signing - Sets execution permissions for ERC20 transfers - Requires spend limits (set to max) for passkey operations - Gas cost: 116,094 (vs 97,030 without passkey, 117,083 with restrictive limits) - Provides clean measurement of passkey overhead (~19k gas) Resolves #272 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Tanishk Goyal * feat: allow early refunds by recipient in escrow * feat: add callSansTo support to ERC7821 execute * chore: improvements and fixes to ERC7821Ithaca * chore: add address(0) replacement to new compute digest * fix: sanitize upper bits before checking replacement + tests * feat: do not add replay safe wrapper if sig is eoa * chore: bump contract versions due to bytecode changes - Contracts updated: IthacaAccount * chore: use `auto-assign-pr.yml` org action * fix: combine improvements from PRs #357, #314, and #379 Combines the following fixes: - PR #357: Replace SuperAdminCanSpendAnything with SuperAdminCanExecuteEverything in setCallChecker - PR #314: Fix typos across codebase (overriden→overridden, Calcualated→Calculated, etc.) - PR #379: Correct inline comment about approval amount (20-byte all-ones, not type(uint256).max) Co-Authored-By: GarmashAlex Co-Authored-By: sukrucildirr Co-Authored-By: Forostovec 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * chore: bump contract versions due to bytecode changes - Contracts updated: IthacaAccount * feat: subaccount design using spend function * test: complete subaccount flow with unit test * chore: bump contract versions due to bytecode changes - Contracts updated: IthacaAccount * feat: add merkle sigs natively into the account * fix: tests * test: add more sophisticated fuzz test * chore: bump contract versions due to bytecode changes - Contracts updated: IthacaAccount * feat: fix and simplify multichain design (#327) * feat: simplify multichain nonce design * chore: readd merkle verification prefix * chore: undo blank line addns * chore: lint * chore: redundant multichain bool * fix: lint * . * feat: remove intent struct (#365) * feat: simplify multichain nonce design * chore: readd merkle verification prefix * chore: undo blank line addns * chore: lint * . * ~50 failing tests down to 5 * down to 1 failing test * fixed failing test * chore: remove console logs and bench * . * Update test/Base.t.sol * Update src/Orchestrator.sol * Update test/utils/mocks/MockPayerWithSignatureOptimized.sol * chore: final cleanup, rebench * Update src/Orchestrator.sol * chore: bump contract versions due to bytecode changes - Contracts updated: IthacaAccount,Orchestrator,SimpleFunder,Simulator * chore: cleanup * rebase * fix --------- Co-authored-by: GitHub Action * feat: native merkle sig verification in Account * chore: fmt * chore: unify merkle sig flow for orchestrator multi chain intents * chore: bump contract versions due to bytecode changes - Contracts updated: Orchestrator,SimpleFunder,Simulator * Add .circleci/config.yml (#1) Add CircleCI configuration file to set up a basic pipeline with a say-hello job and workflow CI: Add .circleci/config.yml to define a say-hello job that checks out the code and prints a greeting using the cimg/base Docker image Add a workflow to orchestrate the say-hello job under the CircleCI 2.1 engine * Update ci.yaml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#2) CI: Update Forge test command to use --rerun and increase verbosity to -vvvvv Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#4) Enhance the CI test step to automatically rerun failed tests and increase verbosity in Forge. CI: Enable the --rerun flag for Forge tests to retry failures automatically Increase Forge test verbosity from -vvv to -vvvvv Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * save local changes before merge * Update ci.yaml (#5) Change Forge test invocation from "forge test --rerun -vvvvv" to "forge test -vvv" Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Create CNAME * Revert "Merge branch 'master'" This reverts commit 6c02fbff6486bb206c08af83d948830ace042353, reversing changes made to a317ddb944a1f5ed7294092790902da6b255f73f. * Create CNAME (#9) * Update ci.yaml (#2) CI: Update Forge test command to use --rerun and increase verbosity to -vvvvv Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#4) Enhance the CI test step to automatically rerun failed tests and increase verbosity in Forge. CI: Enable the --rerun flag for Forge tests to retry failures automatically Increase Forge test verbosity from -vvv to -vvvvv Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#5) Change Forge test invocation from "forge test --rerun -vvvvv" to "forge test -vvv" Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Create CNAME * Revert "Merge branch 'master'" This reverts commit 6c02fbff6486bb206c08af83d948830ace042353, reversing changes made to a317ddb944a1f5ed7294092790902da6b255f73f. --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#10) reback use test CI and automation chores (ithacaxyz#394) https://github.com/Dargon789/account/commit/7dd8a5d91c162b89316e367f0fb159f47abfeab0 Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 3: Workflow does not contain permissions (#15) https://github.com/Dargon789/account/security/code-scanning/3 Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Update ci.yaml (#16) reback use test CI and automation chores (ithacaxyz#394) https://github.com/Dargon789/account/commit/7dd8a5d91c162b89316e367f0fb159f47abfeab0 Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Delete CNAME * Update Brutalizer.sol * Potential fix for code scanning alert no. 2: Workflow does not contain permissions (#17) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Refactor deployment scripts and update configs Migrates deployment scripts to use fork-based configuration loading, removes legacy CircleCI and GitHub workflow files, and introduces a registry-based contract deployment record. Updates .env.example, README, and foundry configuration. Removes LayerZero vendor and test files, adds devtools submodule, and improves LayerZeroSettler configuration logic for multi-chain deployments. * Fix commit user email format in CI workflow (#18) https://github.com/Dargon789/account/commit/30a2096cce0811834c5835a8698ec525c3923112 Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 4: Workflow does not contain permissions (#19) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Potential fix for code scanning alert no. 1: Workflow does not contain permissions (#20) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Update ci.yaml (#21) reback use test CI and automation chores (ithacaxyz#394) https://github.com/Dargon789/account/commit/7dd8a5d91c162b89316e367f0fb159f47abfeab0 Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Master (#22) * Merge branch 'master' (#8) * Update ci.yaml (#2) CI: Update Forge test command to use --rerun and increase verbosity to -vvvvv Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#4) Enhance the CI test step to automatically rerun failed tests and increase verbosity in Forge. CI: Enable the --rerun flag for Forge tests to retry failures automatically Increase Forge test verbosity from -vvv to -vvvvv Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#5) Change Forge test invocation from "forge test --rerun -vvvvv" to "forge test -vvv" Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Create CNAME --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Revert "fix vm block accoount (#11)" (#13) Reverts #11 Summary by Sourcery CI: Update the Forge test command in the CI workflow to enable reruns and increase verbosity. This reverts commit 942017fb59217f59933bb4666f93f76f79066a16. --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Refactor function signatures and formatting for consistency Updated function signatures in IFunder and SimpleFunder for single-line style, and improved code formatting in IthacaAccount, test/Account.t.sol, test/Benchmark.t.sol, and test/LayerZeroSettler.t.sol for readability and consistency. No logic changes were made. * Add Multicall3 simulation support to Simulator Introduces Multicall3 simulation methods in Simulator.sol, enabling simulation of orchestrator calls with pre-execution calls via Multicall3. Adds IMulticall3 interface, updates related tests to cover multicall simulation flows, and includes minor formatting and interface signature changes for consistency. * feat: add multicall to simulator (ithacaxyz#402) (#24) * feat: add multicall to simulator (#402) * feat: add multicall to simulator * chore: add gas test * chore: review fixes * chore: fmt contracts and update gas snapshots * test: add tests for getContextKeyHash in Account.t.sol (#412) --------- Co-authored-by: howy <132113803+howydev@users.noreply.github.com> Co-authored-by: Roberto Delgado Ferrezuelo <108575058+robertodf99@users.noreply.github.com> * Fix commit user email formatting in CI workflow Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Revert "feat: add multicall to simulator (ithacaxyz#402) (#24)" (#26) This reverts commit 1aef3181ac3c14cb47135d61526a7ef79ba63ec1. * Delete .circleci directory (#27) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * # Default ignored files * Potential fix for code scanning alert no. 3: Workflow does not contain permissions (#32) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * .snapshot_worktree * Delete .idea directory (#35) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * pre-commit * pre-commit * deploy execute_config.sh * Update forge-std * Update check-bytecode-changes.js * pre-commit * chore: bump contract versions due to bytecode changes * chore: bump contract versions due to bytecode changes * chore: bump contract versions due to bytecode changes contracts update * v0.5.11 * chore: unify merkle sig flow for orchestrator multi chain intents * test: add tests for getContextKeyHash in Account.t.sol (#412) * Refactor function signatures and formatting for consistency * Legion (#38) * feat: add ERC20 transfer benchmark for Porto with passkey Add testERC20TransferViaPortoOrchestratorWithPasskey() benchmark to isolate passkey authentication costs from spend limit enforcement costs. - Uses secp256k1 passkey for transaction signing - Sets execution permissions for ERC20 transfers - Requires spend limits (set to max) for passkey operations - Gas cost: 116,094 (vs 97,030 without passkey, 117,083 with restrictive limits) - Provides clean measurement of passkey overhead (~19k gas) Resolves #272 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Tanishk Goyal * feat: allow early refunds by recipient in escrow * feat: add callSansTo support to ERC7821 execute * chore: improvements and fixes to ERC7821Ithaca * chore: add address(0) replacement to new compute digest * fix: sanitize upper bits before checking replacement + tests * feat: do not add replay safe wrapper if sig is eoa * chore: bump contract versions due to bytecode changes - Contracts updated: IthacaAccount * chore: use `auto-assign-pr.yml` org action * fix: combine improvements from PRs #357, #314, and #379 Combines the following fixes: - PR #357: Replace SuperAdminCanSpendAnything with SuperAdminCanExecuteEverything in setCallChecker - PR #314: Fix typos across codebase (overriden→overridden, Calcualated→Calculated, etc.) - PR #379: Correct inline comment about approval amount (20-byte all-ones, not type(uint256).max) Co-Authored-By: GarmashAlex Co-Authored-By: sukrucildirr Co-Authored-By: Forostovec 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * chore: bump contract versions due to bytecode changes - Contracts updated: IthacaAccount * feat: subaccount design using spend function * test: complete subaccount flow with unit test * chore: bump contract versions due to bytecode changes - Contracts updated: IthacaAccount * feat: add merkle sigs natively into the account * fix: tests * test: add more sophisticated fuzz test * chore: bump contract versions due to bytecode changes - Contracts updated: IthacaAccount * feat: fix and simplify multichain design (#327) * feat: simplify multichain nonce design * chore: readd merkle verification prefix * chore: undo blank line addns * chore: lint * chore: redundant multichain bool * fix: lint * . * feat: remove intent struct (#365) * feat: simplify multichain nonce design * chore: readd merkle verification prefix * chore: undo blank line addns * chore: lint * . * ~50 failing tests down to 5 * down to 1 failing test * fixed failing test * chore: remove console logs and bench * . * Update test/Base.t.sol * Update src/Orchestrator.sol * Update test/utils/mocks/MockPayerWithSignatureOptimized.sol * chore: final cleanup, rebench * Update src/Orchestrator.sol * chore: bump contract versions due to bytecode changes - Contracts updated: IthacaAccount,Orchestrator,SimpleFunder,Simulator * chore: cleanup * rebase * fix --------- Co-authored-by: GitHub Action * feat: native merkle sig verification in Account * chore: fmt * chore: unify merkle sig flow for orchestrator multi chain intents * chore: bump contract versions due to bytecode changes - Contracts updated: Orchestrator,SimpleFunder,Simulator * Add .circleci/config.yml (#1) Add CircleCI configuration file to set up a basic pipeline with a say-hello job and workflow CI: Add .circleci/config.yml to define a say-hello job that checks out the code and prints a greeting using the cimg/base Docker image Add a workflow to orchestrate the say-hello job under the CircleCI 2.1 engine * Update ci.yaml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#2) CI: Update Forge test command to use --rerun and increase verbosity to -vvvvv Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#4) Enhance the CI test step to automatically rerun failed tests and increase verbosity in Forge. CI: Enable the --rerun flag for Forge tests to retry failures automatically Increase Forge test verbosity from -vvv to -vvvvv Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#5) Change Forge test invocation from "forge test --rerun -vvvvv" to "forge test -vvv" Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Create CNAME * Revert "Merge branch 'master'" This reverts commit 6c02fbff6486bb206c08af83d948830ace042353, reversing changes made to a317ddb944a1f5ed7294092790902da6b255f73f. * Create CNAME (#9) * Update ci.yaml (#2) CI: Update Forge test command to use --rerun and increase verbosity to -vvvvv Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#4) Enhance the CI test step to automatically rerun failed tests and increase verbosity in Forge. CI: Enable the --rerun flag for Forge tests to retry failures automatically Increase Forge test verbosity from -vvv to -vvvvv Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#5) Change Forge test invocation from "forge test --rerun -vvvvv" to "forge test -vvv" Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Create CNAME * Revert "Merge branch 'master'" This reverts commit 6c02fbff6486bb206c08af83d948830ace042353, reversing changes made to a317ddb944a1f5ed7294092790902da6b255f73f. --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#10) reback use test CI and automation chores (ithacaxyz#394) https://github.com/Dargon789/account/commit/7dd8a5d91c162b89316e367f0fb159f47abfeab0 Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 3: Workflow does not contain permissions (#15) https://github.com/Dargon789/account/security/code-scanning/3 Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Update ci.yaml (#16) reback use test CI and automation chores (ithacaxyz#394) https://github.com/Dargon789/account/commit/7dd8a5d91c162b89316e367f0fb159f47abfeab0 Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Delete CNAME * Update Brutalizer.sol * Potential fix for code scanning alert no. 2: Workflow does not contain permissions (#17) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Refactor deployment scripts and update configs Migrates deployment scripts to use fork-based configuration loading, removes legacy CircleCI and GitHub workflow files, and introduces a registry-based contract deployment record. Updates .env.example, README, and foundry configuration. Removes LayerZero vendor and test files, adds devtools submodule, and improves LayerZeroSettler configuration logic for multi-chain deployments. * Fix commit user email format in CI workflow (#18) https://github.com/Dargon789/account/commit/30a2096cce0811834c5835a8698ec525c3923112 Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 4: Workflow does not contain permissions (#19) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Potential fix for code scanning alert no. 1: Workflow does not contain permissions (#20) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Update ci.yaml (#21) reback use test CI and automation chores (ithacaxyz#394) https://github.com/Dargon789/account/commit/7dd8a5d91c162b89316e367f0fb159f47abfeab0 Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Master (#22) * Merge branch 'master' (#8) * Update ci.yaml (#2) CI: Update Forge test command to use --rerun and increase verbosity to -vvvvv Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#4) Enhance the CI test step to automatically rerun failed tests and increase verbosity in Forge. CI: Enable the --rerun flag for Forge tests to retry failures automatically Increase Forge test verbosity from -vvv to -vvvvv Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml (#5) Change Forge test invocation from "forge test --rerun -vvvvv" to "forge test -vvv" Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Create CNAME --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Revert "fix vm block accoount (#11)" (#13) Reverts #11 Summary by Sourcery CI: Update the Forge test command in the CI workflow to enable reruns and increase verbosity. This reverts commit 942017fb59217f59933bb4666f93f76f79066a16. --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Update ci.yaml Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Refactor function signatures and formatting for consistency Updated function signatures in IFunder and SimpleFunder for single-line style, and improved code formatting in IthacaAccount, test/Account.t.sol, test/Benchmark.t.sol, and test/LayerZeroSettler.t.sol for readability and consistency. No logic changes were made. * Delete .circleci directory (#27) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * # Default ignored files * .snapshot_worktree * Delete .idea directory (#35) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * pre-commit * pre-commit * deploy execute_config.sh * Update forge-std * Update check-bytecode-changes.js * pre-commit * chore: bump contract versions due to bytecode changes * chore: bump contract versions due to bytecode changes * chore: bump contract versions due to bytecode changes contracts update * v0.5.11 * chore: unify merkle sig flow for orchestrator multi chain intents * test: add tests for getContextKeyHash in Account.t.sol (#412) * Refactor function signatures and formatting for consistency --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Tanishk Goyal Co-authored-by: Tanishk Goyal Co-authored-by: howy <132113803+howydev@users.noreply.github.com> Co-authored-by: GitHub Action Co-authored-by: o-az Co-authored-by: Claude Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: googleworkspace-bot * Add .idea configs and tweak .env comments Add IntelliJ IDEA project files under .idea and lib/.idea (modules, caches, iml, vcs, and a .gitignore) and update the lib/forge-std gitlink (submodule) reference. Also adjust .env.example by removing spaces before inline comments on RPC URLs (UPGRADE_TEST_RPC_URL, RPC_1, RPC_11155111) to standardize formatting. * Normalize file modes and update forge-std submodule Remove executable bit from deploy/execute_config.sh, deploy/verify_config.sh, and prep/check-bytecode-changes.js (mode 100755 → 100644). Update lib/forge-std submodule from commit c2cf7017d27c1d20e74ace4dacb6c5ce4bbbe899 to 07853315f998f94dc724e464b1bab1270888ee64. No other content changes. * Update LayerZero-v2 * Update LayerZero-v2 --------- Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Tanishk Goyal Co-authored-by: Tanishk Goyal Co-authored-by: howy <132113803+howydev@users.noreply.github.com> Co-authored-by: GitHub Action Co-authored-by: o-az Co-authored-by: Claude Co-authored-by: John Doe Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: Roberto Delgado Ferrezuelo <108575058+robertodf99@users.noreply.github.com> Co-authored-by: googleworkspace-bot --- .changeset/README.md | 8 + .changeset/config.json | 11 + .env.example | 6 +- .github/workflows/auto-assign.yaml | 12 - .github/workflows/version-check.yaml | 31 +- .gitmodules | 3 + .idea/misc.xml | 6 + CHANGELOG.md | 23 +- README.md | 33 +- deploy/ConfigureLayerZeroSettler.s.sol | 207 +- deploy/DeployMain.s.sol | 550 +++-- deploy/FundSigners.s.sol | 118 +- deploy/README.md | 323 +-- deploy/config.toml | 229 +- ...0000000000000000000000000000000000001.json | 1 + ...0000000000000000000000000000000000001.json | 10 + docs/4337CallGraph.md | 32 - docs/IthacaCallGraph.md | 25 - docs/benchmarks.jpeg | Bin 291106 -> 0 bytes foundry.lock | 4 +- foundry.toml | 14 +- gas-snapshots/.gas-snapshot-main | 11 + lib/.idea/.gitignore | 3 + lib/.idea/caches/deviceStreaming.xml | 2007 +++++++++++++++++ lib/.idea/lib.iml | 9 + lib/.idea/misc.xml | 6 + lib/.idea/modules.xml | 8 + lib/.idea/vcs.xml | 21 + lib/solady | 2 +- snapshots/BenchmarkTest.json | 102 +- src/Escrow.sol | 19 +- src/GuardedExecutor.sol | 8 +- src/IthacaAccount.sol | 22 +- src/MultiSigSigner.sol | 4 +- src/Orchestrator.sol | 371 +-- src/SimpleFunder.sol | 13 +- src/Simulator.sol | 90 +- src/interfaces/ICommon.sol | 80 - src/interfaces/IFunder.sol | 7 +- src/interfaces/IIthacaAccount.sol | 13 +- src/interfaces/IMulticall3.sol | 38 - src/interfaces/IOrchestrator.sol | 9 +- src/libraries/ERC7821Ithaca.sol | 347 +++ src/libraries/IntentHelpers.sol | 158 ++ src/vendor/layerzero/interfaces/IOAppCore.sol | 54 - .../interfaces/IOAppMsgInspector.sol | 25 - .../layerzero/interfaces/IOAppReceiver.sol | 27 - src/vendor/layerzero/mocks/EndpointV2Mock.sol | 418 ---- src/vendor/layerzero/oapp/OApp.sol | 38 - src/vendor/layerzero/oapp/OAppCore.sol | 104 - src/vendor/layerzero/oapp/OAppReceiver.sol | 143 -- src/vendor/layerzero/oapp/OAppSender.sol | 136 -- test/Account.t.sol | 45 - test/Base.t.sol | 11 +- test/Benchmark.t.sol | 17 +- test/Escrow.t.sol | 74 + test/GuardedExecutor.t.sol | 105 +- test/LayerZeroSettler.t.sol | 6 +- test/MultiSigSigner.t.sol | 656 ------ test/Orchestrator.t.sol | 210 +- test/SimulateExecute.t.sol | 38 +- test/SubAccounts.t.sol | 270 +++ test/UpgradeTests.t.sol | 566 ----- test/utils/Brutalizer.sol | 4 +- test/utils/interfaces/IPimlicoPaymaster.sol | 4 +- test/utils/interfaces/ISafe.sol | 57 - test/utils/mocks/MockOrchestrator.sol | 9 +- test/utils/mocks/MockPayerWithSignature.sol | 40 +- .../mocks/MockPayerWithSignatureOptimized.sol | 84 - test/utils/mocks/MockPayerWithState.sol | 39 +- utils/JsonBindings.sol | 767 +++++++ 71 files changed, 5009 insertions(+), 3932 deletions(-) create mode 100644 .changeset/README.md create mode 100644 .changeset/config.json delete mode 100644 .github/workflows/auto-assign.yaml create mode 100644 .idea/misc.xml create mode 100644 deploy/registry/deployment_11155420_0x0000000000000000000000000000000000000000000000000000000000000001.json create mode 100644 deploy/registry/deployment_84532_0x0000000000000000000000000000000000000000000000000000000000000001.json delete mode 100644 docs/4337CallGraph.md delete mode 100644 docs/IthacaCallGraph.md delete mode 100644 docs/benchmarks.jpeg create mode 100644 gas-snapshots/.gas-snapshot-main create mode 100644 lib/.idea/.gitignore create mode 100644 lib/.idea/caches/deviceStreaming.xml create mode 100644 lib/.idea/lib.iml create mode 100644 lib/.idea/misc.xml create mode 100644 lib/.idea/modules.xml create mode 100644 lib/.idea/vcs.xml delete mode 100644 src/interfaces/IMulticall3.sol create mode 100644 src/libraries/ERC7821Ithaca.sol create mode 100644 src/libraries/IntentHelpers.sol delete mode 100644 src/vendor/layerzero/interfaces/IOAppCore.sol delete mode 100644 src/vendor/layerzero/interfaces/IOAppMsgInspector.sol delete mode 100644 src/vendor/layerzero/interfaces/IOAppReceiver.sol delete mode 100644 src/vendor/layerzero/mocks/EndpointV2Mock.sol delete mode 100644 src/vendor/layerzero/oapp/OApp.sol delete mode 100644 src/vendor/layerzero/oapp/OAppCore.sol delete mode 100644 src/vendor/layerzero/oapp/OAppReceiver.sol delete mode 100644 src/vendor/layerzero/oapp/OAppSender.sol delete mode 100644 test/MultiSigSigner.t.sol create mode 100644 test/SubAccounts.t.sol delete mode 100644 test/UpgradeTests.t.sol delete mode 100644 test/utils/interfaces/ISafe.sol delete mode 100644 test/utils/mocks/MockPayerWithSignatureOptimized.sol create mode 100644 utils/JsonBindings.sol diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 00000000..e5b6d8d6 --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 00000000..d88011f6 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "restricted", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/.env.example b/.env.example index 7e85472c..dcfc7f4c 100644 --- a/.env.example +++ b/.env.example @@ -4,7 +4,7 @@ # ============================================ # UPGRADE TESTS # ============================================ -UPGRADE_TEST_RPC_URL=https://base-mainnet.g.alchemy.com/v2/YOUR_API_KEY # Base +UPGRADE_TEST_RPC_URL=https://base-mainnet.g.alchemy.com/v2/YOUR_API_KEY# Base UPGRADE_TEST_OLD_PROXY=0x7C27e3AEcbF42879B64d76f604dC3430F4886462 UPGRADE_TEST_OLD_VERSION=0.5.10 @@ -16,10 +16,10 @@ PRIVATE_KEY= # Format: RPC_{chainId} # Mainnet chains -RPC_1=https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY # Ethereum +RPC_1=https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY# Ethereum # Testnet chains -RPC_11155111=https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY # Sepolia +RPC_11155111=https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY# Sepolia # Test mnemonic for funding script GAS_SIGNER_MNEMONIC="dash between kangaroo vacant gaze glass way sudden retire output retire evil" diff --git a/.github/workflows/auto-assign.yaml b/.github/workflows/auto-assign.yaml deleted file mode 100644 index 0ecd2e5e..00000000 --- a/.github/workflows/auto-assign.yaml +++ /dev/null @@ -1,12 +0,0 @@ -name: Auto Assign PR to Author - -on: - pull_request: - types: [opened, reopened] - -jobs: - auto-assign: - permissions: - issues: write - pull-requests: write - uses: ithacaxyz/ci/.github/workflows/auto-assign-pr.yml@main diff --git a/.github/workflows/version-check.yaml b/.github/workflows/version-check.yaml index f5946cb5..059fa1e4 100644 --- a/.github/workflows/version-check.yaml +++ b/.github/workflows/version-check.yaml @@ -57,22 +57,31 @@ jobs: - name: Bump version if needed if: steps.check.outputs.needs_version_bump == 'true' run: | + # Configure git + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + # Get the contracts that need bumping CONTRACTS_TO_BUMP="${{ steps.check.outputs.contracts_to_bump }}" - + echo "Bumping versions for contracts: $CONTRACTS_TO_BUMP" - + # Update Solidity files using the upgrade script with specific contracts CONTRACTS_TO_BUMP="$CONTRACTS_TO_BUMP" node prep/update-version.js - - - name: Commit version bump changes - if: steps.check.outputs.needs_version_bump == 'true' - uses: stefanzweifel/git-auto-commit-action@v5 - with: - commit_message: "chore: bump contract versions due to bytecode changes - Contracts updated: ${{ steps.check.outputs.contracts_to_bump }}" - file_pattern: "src/*.sol" - commit_user_name: github-actions[bot] - commit_user_email: github-actions[bot]@users.noreply.github.com + + # Commit changes (only Solidity files, not package.json) + git add src/*.sol + git commit -m "chore: bump contract versions due to bytecode changes - Contracts updated: $CONTRACTS_TO_BUMP" + + # Pull latest changes and rebase + # Pull latest changes and rebase + if ! git pull origin ${{ github.event.pull_request.head.ref }} --rebase; then + echo "Failed to rebase version bump changes. Manual intervention required." + exit 1 + fi + + # Push to the PR branch + git push origin HEAD:${{ github.event.pull_request.head.ref }} - name: Create PR comment if: steps.check.outputs.needs_version_bump == 'true' diff --git a/.gitmodules b/.gitmodules index 28a7bc52..7aed494c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,6 +7,9 @@ [submodule "lib/LayerZero-v2"] path = lib/LayerZero-v2 url = https://github.com/LayerZero-Labs/LayerZero-v2 +[submodule "lib/devtools"] + path = lib/devtools + url = https://github.com/LayerZero-Labs/devtools [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..862d09bd --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fd21859..dae9f612 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,24 +1,5 @@ # porto-account -> Note: After v0.5.5, all changelogs will be published along with the release notes. -> From here on, this file is deprecated. - -## 0.5.5 - -### Patch Changes -- Update foundry config, to not include metadata in the bytecode. This ensures that the contract bytecode doesn't change because of some other change in the repository. - - -## 0.5.4 - -### Patch Changes - -- SimpleFunder supports multiple orchestrators instead of single immutable orchestrator - - Replaced immutable `ORCHESTRATOR` with `orchestrators` mapping and `setOrchestrators()` function - - Maintained backward compatibility with old `fund()` signature - - Added `supported_orchestrators` config field for deployment - - Version bumped to "0.1.5" - ## 0.5.0 ### Minor Changes @@ -113,7 +94,7 @@ - All fill related functions removed from EP. - EP is now completely stateless, also does not have a constructor. - PreCall with `nonce = type(uint256).max` is not replayable anymore. -- `OpDataTooShort` error, udpated to `OpDataError`, to enforce tighter validation of opdata. +- `OpDataTooShort` error, updated to `OpDataError`, to enforce tighter validation of opdata. - `checkAndIncrementNonce` function added to account. Can only be called by EP. - 6b3294a: Optimize `_isSuperAdmin` @@ -150,7 +131,7 @@ - Add back the INSUFFICIENT_GAS check, which prevents the relay from setting up the `execute` call on the account, in such a way causing it to intentionally fail. - For the relay, gExecute now has to be set atleast as `gExecute > (gCombined + 100_000) * 64/63)` + For the relay, gExecute now has to be set at least as `gExecute > (gCombined + 100_000) * 64/63)` ### Patch Changes diff --git a/README.md b/README.md index aa22490b..ea941768 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/ithacaxyz/account) +> 🚧 **Work In Progress** +> This repository is under active development. Contracts are **unaudited**, and the codebase may have **breaking changes** without notice. +> A bug bounty is live on Base Mainnet — [details here](docs/bug-bounty.md). + **All-in-one EIP-7702 powered account contract, coupled with [Porto](https://github.com/ithacaxyz/porto)** Every app needs an account, traditionally requiring separate services for auth, payments, and recovery. Doing this in a way that empowers users with control over their funds and their data is the core challenge of the crypto space. While crypto wallets have made great strides, users still face a fragmented experience - juggling private keys, managing account balances across networks, @@ -19,24 +23,17 @@ We believe that unstoppable crypto-powered accounts should be excellent througho # Features out of the box -- Secure Login: Using WebAuthN-compatible credentials like PassKeys. -- Call Batching: Send multiple calls in 1. -- Gas Sponsorship: Allow anyone to pay for your fees in any ERC20 or ETH. -- Access Control: Whitelist receivers, function selectors and arguments. -- Session Keys: Allow transactions without confirmations if they pass low-security access control policies. -- Multi-sig Support: If a call is outside of a certain access control policy, require multiple signatures. -- Interop: Transaction on any chain invisibly. - -## Benchmarks - -![Benchmarks](docs/benchmarks.jpeg) - -Gas benchmark implementations are in the [test repository](test/Benchmark.t.sol). We currently benchmark against leading ERC-4337 accounts. To generate the benchmarks, use `forge snapshot --isolate`. - -## Security -Contracts were audited in a 2 week engagement by @MiloTruck @rholterhus @kadenzipfel - -We also maintain an active bug bounty program, you can find more details about it [here](https://porto.sh/contracts/security-and-bug-bounty) +- [x] Secure Login: Using WebAuthN-compatible credentials like PassKeys. +- [x] Call Batching: Send multiple calls in 1. +- [x] Gas Sponsorship: Allow anyone to pay for your fees in any ERC20 or ETH. +- [x] Access Control: Whitelist receivers, function selectors and arguments. +- [x] Session Keys: Allow transactions without confirmations if they pass low-security access control policies. +- [x] Multi-sig Support: If a call is outside of a certain access control policy, require multiple signatures. +- [x] Interop: Transaction on any chain invisibly. +- [ ] Timelocks: Add a time delay between transaction verification and execution, for additional safety. +- [ ] Optimized for L2: Using BLS signatures. +- [ ] Privacy: Using stealth addresses, confidential ERC20 tokens, and privacy pool integrations. +- [ ] Account Recovery & Identity: Using ZK {Email, OAUth, Passport} and more. ## Getting Help diff --git a/deploy/ConfigureLayerZeroSettler.s.sol b/deploy/ConfigureLayerZeroSettler.s.sol index 63187a5f..4e505329 100644 --- a/deploy/ConfigureLayerZeroSettler.s.sol +++ b/deploy/ConfigureLayerZeroSettler.s.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.23; import {Script} from "forge-std/Script.sol"; import {console} from "forge-std/Script.sol"; -import {Config} from "forge-std/Config.sol"; import {ILayerZeroEndpointV2} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; import {SetConfigParam} from @@ -37,7 +36,7 @@ import {LayerZeroSettler} from "../src/LayerZeroSettler.sol"; * --private-key $L0_SETTLER_OWNER_PK \ * "[84532,11155420]" */ -contract ConfigureLayerZeroSettler is Script, Config { +contract ConfigureLayerZeroSettler is Script { // Configuration type constants (matching ULN302) uint32 constant CONFIG_TYPE_EXECUTOR = 1; uint32 constant CONFIG_TYPE_ULN = 2; @@ -47,7 +46,6 @@ contract ConfigureLayerZeroSettler is Script, Config { string name; address layerZeroSettlerAddress; address layerZeroEndpoint; - address l0SettlerSigner; uint32 eid; address sendUln302; address receiveUln302; @@ -61,7 +59,6 @@ contract ConfigureLayerZeroSettler is Script, Config { // Fork ids for chain switching mapping(uint256 => uint256) public forkIds; - mapping(uint256 => bool) public isForkInitialized; mapping(uint256 => LayerZeroChainConfig) public chainConfigs; uint256[] public configuredChainIds; @@ -69,12 +66,8 @@ contract ConfigureLayerZeroSettler is Script, Config { * @notice Configure all chains with LayerZero configuration */ function run() external { - // Load configuration and setup forks - string memory configPath = string.concat(vm.projectRoot(), "/deploy/config.toml"); - _loadConfigAndForks(configPath, false); - - // Get all chain IDs from configuration - uint256[] memory chainIds = config.getChainIds(); + // Get all chain IDs from fork configuration + uint256[] memory chainIds = vm.readForkChainIds(); run(chainIds); } @@ -86,15 +79,12 @@ contract ConfigureLayerZeroSettler is Script, Config { console.log("Loading configuration from deploy/config.toml"); console.log("Configuring", chainIds.length, "chains"); - // If config not already loaded, load it - if (address(config) == address(0)) { - string memory configPath = string.concat(vm.projectRoot(), "/deploy/config.toml"); - _loadConfigAndForks(configPath, false); - } - // Load configurations for all chains loadConfigurations(chainIds); + // Create forks for all chains that have LayerZero config + createForks(); + // Configure each chain for (uint256 i = 0; i < configuredChainIds.length; i++) { configureChain(configuredChainIds[i]); @@ -110,22 +100,27 @@ contract ConfigureLayerZeroSettler is Script, Config { for (uint256 i = 0; i < chainIds.length; i++) { uint256 chainId = chainIds[i]; - // Switch to the fork for this chain (already created by _loadConfigAndForks) - vm.selectFork(forkOf[chainId]); + // Create fork to read configuration + string memory rpcUrl = vm.envString(string.concat("RPC_", vm.toString(chainId))); + uint256 forkId = vm.createFork(rpcUrl); + vm.selectFork(forkId); // Try to load LayerZero configuration - LayerZeroChainConfig memory chainConfig = loadChainConfig(chainId); - - // Only add chains that have LayerZero configuration - if (chainConfig.layerZeroSettlerAddress != address(0)) { - chainConfigs[chainId] = chainConfig; + LayerZeroChainConfig memory config = loadChainConfig(chainId); + + // Only store if chain has LayerZero configuration + if (config.sendUln302 != address(0)) { + chainConfigs[chainId] = config; configuredChainIds.push(chainId); - forkIds[chainId] = forkOf[chainId]; - isForkInitialized[forkOf[chainId]] = true; + forkIds[chainId] = forkId; console.log( string.concat( - " Loaded LayerZero config for ", chainConfig.name, " (", vm.toString(chainId), ")" + " Loaded LayerZero config for ", + config.name, + " (", + vm.toString(chainId), + ")" ) ); } @@ -135,68 +130,62 @@ contract ConfigureLayerZeroSettler is Script, Config { } /** - * @notice Load configuration for a single chain using StdConfig + * @notice Load configuration for a single chain from fork variables */ function loadChainConfig(uint256 chainId) internal view - returns (LayerZeroChainConfig memory chainConfig) + returns (LayerZeroChainConfig memory config) { - chainConfig.chainId = chainId; + config.chainId = chainId; // Load basic chain info - required - chainConfig.name = config.get(chainId, "name").toString(); + config.name = vm.readForkString("name"); - // Try to load LayerZero settler deployed address first, then fall back to expected address - address settlerAddr = config.get(chainId, "layerzero_settler_deployed").toAddress(); - if (settlerAddr == address(0)) { - // Fall back to expected address from config - settlerAddr = config.get(chainId, "layerzero_settler_address").toAddress(); - } - if (settlerAddr == address(0)) { + // Try to load LayerZero settler address - if not present, this chain doesn't have LayerZero config + try vm.readForkAddress("layerzero_settler_address") returns (address addr) { + config.layerZeroSettlerAddress = addr; + } catch { // No LayerZero settler configured for this chain - this is ok, return empty config - return chainConfig; + return config; } - chainConfig.layerZeroSettlerAddress = settlerAddr; // If we have a LayerZero settler, all other LayerZero fields are required - chainConfig.layerZeroEndpoint = config.get(chainId, "layerzero_endpoint").toAddress(); - chainConfig.l0SettlerSigner = config.get(chainId, "l0_settler_signer").toAddress(); - chainConfig.eid = uint32(config.get(chainId, "layerzero_eid").toUint256()); - chainConfig.sendUln302 = config.get(chainId, "layerzero_send_uln302").toAddress(); - chainConfig.receiveUln302 = config.get(chainId, "layerzero_receive_uln302").toAddress(); + config.layerZeroEndpoint = vm.readForkAddress("layerzero_endpoint"); + config.eid = uint32(vm.readForkUint("layerzero_eid")); + config.sendUln302 = vm.readForkAddress("layerzero_send_uln302"); + config.receiveUln302 = vm.readForkAddress("layerzero_receive_uln302"); // Load destination chain IDs - required for LayerZero configuration - chainConfig.destinationChainIds = config.get(chainId, "layerzero_destination_chain_ids").toUint256Array(); + config.destinationChainIds = vm.readForkUintArray("layerzero_destination_chain_ids"); // Load DVN configuration - required and optional DVN arrays - string[] memory requiredDVNNames = config.get(chainId, "layerzero_required_dvns").toStringArray(); - string[] memory optionalDVNNames = config.get(chainId, "layerzero_optional_dvns").toStringArray(); + string[] memory requiredDVNNames = vm.readForkStringArray("layerzero_required_dvns"); + string[] memory optionalDVNNames = vm.readForkStringArray("layerzero_optional_dvns"); // Resolve DVN names to addresses - chainConfig.requiredDVNs = resolveDVNAddresses(chainId, requiredDVNNames); - chainConfig.optionalDVNs = resolveDVNAddresses(chainId, optionalDVNNames); + config.requiredDVNs = resolveDVNAddresses(requiredDVNNames); + config.optionalDVNs = resolveDVNAddresses(optionalDVNNames); // Load optional DVN threshold - required field - chainConfig.optionalDVNThreshold = uint8(config.get(chainId, "layerzero_optional_dvn_threshold").toUint256()); + config.optionalDVNThreshold = uint8(vm.readForkUint("layerzero_optional_dvn_threshold")); // Load confirmations - required field - chainConfig.confirmations = uint64(config.get(chainId, "layerzero_confirmations").toUint256()); + config.confirmations = uint64(vm.readForkUint("layerzero_confirmations")); // Load max message size - required field - chainConfig.maxMessageSize = uint32(config.get(chainId, "layerzero_max_message_size").toUint256()); + config.maxMessageSize = uint32(vm.readForkUint("layerzero_max_message_size")); - return chainConfig; + return config; } /** - * @notice Resolve DVN names to addresses using StdConfig - * @dev Takes DVN variable names and looks up their addresses using config.get - * @param chainId The chain ID to resolve DVN addresses for + * @notice Resolve DVN names to addresses by reading from fork variables + * @dev Takes DVN variable names and looks up their addresses using vm.readForkAddress * @param dvnNames Array of DVN variable names from config (e.g., "dvn_layerzero_labs") * @return addresses Array of resolved DVN addresses */ - function resolveDVNAddresses(uint256 chainId, string[] memory dvnNames) + function resolveDVNAddresses(string[] memory dvnNames) internal view returns (address[] memory) @@ -204,7 +193,7 @@ contract ConfigureLayerZeroSettler is Script, Config { address[] memory addresses = new address[](dvnNames.length); for (uint256 i = 0; i < dvnNames.length; i++) { - addresses[i] = config.get(chainId, dvnNames[i]).toAddress(); + addresses[i] = vm.readForkAddress(dvnNames[i]); require( addresses[i] != address(0), string.concat("DVN address not configured for: ", dvnNames[i]) @@ -214,8 +203,18 @@ contract ConfigureLayerZeroSettler is Script, Config { return addresses; } - function _selectFork(uint256 chainId) internal { - vm.selectFork(forkOf[chainId]); + /** + * @notice Create forks for all configured chains + */ + function createForks() internal { + console.log("\n=== Creating forks for configured chains ==="); + + for (uint256 i = 0; i < configuredChainIds.length; i++) { + uint256 chainId = configuredChainIds[i]; + LayerZeroChainConfig memory config = chainConfigs[chainId]; + + console.log(" Fork created for", config.name, "fork ID:", forkIds[chainId]); + } } /** @@ -228,59 +227,31 @@ contract ConfigureLayerZeroSettler is Script, Config { console.log(string.concat("Configuring ", config.name, " (", vm.toString(chainId), ")")); console.log(" LayerZero Settler:", config.layerZeroSettlerAddress); console.log(" Endpoint:", config.layerZeroEndpoint); - console.log(" L0SettlerSigner:", config.l0SettlerSigner); console.log(" EID:", config.eid); // Switch to the source chain - _selectFork(chainId); + vm.selectFork(forkIds[chainId]); LayerZeroSettler settler = LayerZeroSettler(payable(config.layerZeroSettlerAddress)); - - // Set or update the endpoint on the settler - address currentEndpoint = address(settler.endpoint()); - if (currentEndpoint != config.layerZeroEndpoint) { - if (currentEndpoint == address(0)) { - console.log(" Setting endpoint to:", config.layerZeroEndpoint); - } else { - console.log(" Updating endpoint from:", currentEndpoint); - console.log(" To:", config.layerZeroEndpoint); - } - vm.broadcast(); - settler.setEndpoint(config.layerZeroEndpoint); - console.log(" Endpoint configured successfully"); - } else { - console.log(" Endpoint already set to:", config.layerZeroEndpoint); - } - - // Set or update the L0SettlerSigner on the settler - address currentSigner = settler.l0SettlerSigner(); - if (currentSigner != config.l0SettlerSigner) { - if (currentSigner == address(0)) { - console.log(" Setting L0SettlerSigner to:", config.l0SettlerSigner); - } else { - console.log(" Updating L0SettlerSigner from:", currentSigner); - console.log(" To:", config.l0SettlerSigner); - } - vm.broadcast(); - settler.setL0SettlerSigner(config.l0SettlerSigner); - console.log(" L0SettlerSigner configured successfully"); - } else { - console.log(" L0SettlerSigner already set to:", config.l0SettlerSigner); - } - ILayerZeroEndpointV2 endpoint = ILayerZeroEndpointV2(config.layerZeroEndpoint); // Configure pathways to all destinations for (uint256 i = 0; i < config.destinationChainIds.length; i++) { uint256 destChainId = config.destinationChainIds[i]; + // Skip if destination not configured + if (forkIds[destChainId] == 0) { + console.log(" Skipping unconfigured destination:", destChainId); + continue; + } + LayerZeroChainConfig memory destConfig = chainConfigs[destChainId]; console.log(string.concat("\n Configuring pathway to ", destConfig.name)); console.log(" Destination EID:", destConfig.eid); // Set executor config (self-execution model) - setExecutorConfig(config, settler, endpoint, destConfig.eid); + setExecutorConfig(settler, endpoint, destConfig.eid); // Set send ULN config setSendUlnConfig( @@ -295,31 +266,11 @@ contract ConfigureLayerZeroSettler is Script, Config { ); // Switch to destination chain to set receive config - _selectFork(destChainId); - - // Ensure destination settler has endpoint set before configuring - LayerZeroSettler destSettler = - LayerZeroSettler(payable(destConfig.layerZeroSettlerAddress)); - address currentDestEndpoint = address(destSettler.endpoint()); - if (currentDestEndpoint != destConfig.layerZeroEndpoint) { - if (currentDestEndpoint == address(0)) { - console.log( - " Setting endpoint on destination:", destConfig.layerZeroEndpoint - ); - } else { - console.log(" Updating endpoint on destination from:", currentDestEndpoint); - console.log(" To:", destConfig.layerZeroEndpoint); - } - vm.broadcast(); - destSettler.setEndpoint(destConfig.layerZeroEndpoint); - console.log(" Destination endpoint configured successfully"); - } else { - console.log(" Destination endpoint already set:", destConfig.layerZeroEndpoint); - } + vm.selectFork(forkIds[destChainId]); // Set receive ULN config on destination setReceiveUlnConfig( - destSettler, + LayerZeroSettler(payable(destConfig.layerZeroSettlerAddress)), ILayerZeroEndpointV2(destConfig.layerZeroEndpoint), config.eid, // Source EID destConfig.receiveUln302, @@ -330,7 +281,7 @@ contract ConfigureLayerZeroSettler is Script, Config { ); // Switch back to source chain - _selectFork(chainId); + vm.selectFork(forkIds[chainId]); } console.log("\n Configuration complete for", config.name); @@ -341,24 +292,12 @@ contract ConfigureLayerZeroSettler is Script, Config { // ============================================ function setExecutorConfig( - LayerZeroChainConfig memory config, LayerZeroSettler settler, ILayerZeroEndpointV2 endpoint, uint32 destEid ) internal { - console.log(" Setting executor config:"); - console.log(" Executor (self-execution):", address(settler)); - console.log(" Max message size:", config.maxMessageSize); - console.log(" Send ULN302:", config.sendUln302); - - bytes memory executorConfig = abi.encode(config.maxMessageSize, settler); - SetConfigParam[] memory params = new SetConfigParam[](1); - params[0] = - SetConfigParam({eid: destEid, configType: CONFIG_TYPE_EXECUTOR, config: executorConfig}); - - vm.broadcast(); - endpoint.setConfig(address(settler), config.sendUln302, params); - console.log(" Executor config set"); + // LayerZeroSettler uses self-execution model, no executor config needed + console.log(" Using self-execution model (no executor config)"); } function setSendUlnConfig( @@ -398,7 +337,7 @@ contract ConfigureLayerZeroSettler is Script, Config { }); // Get the L0 settler owner who should be the delegate - address l0SettlerOwner = config.get(vm.getChainId(), "l0_settler_owner").toAddress(); + address l0SettlerOwner = vm.readForkAddress("l0_settler_owner"); console.log(" L0 Settler Owner (delegate):", l0SettlerOwner); vm.broadcast(); diff --git a/deploy/DeployMain.s.sol b/deploy/DeployMain.s.sol index ee513799..5ebbb109 100644 --- a/deploy/DeployMain.s.sol +++ b/deploy/DeployMain.s.sol @@ -3,8 +3,7 @@ pragma solidity ^0.8.23; import {Script, console} from "forge-std/Script.sol"; import {VmSafe} from "forge-std/Vm.sol"; -import {Config} from "forge-std/Config.sol"; -import {Variable, TypeKind} from "forge-std/LibVariable.sol"; +import {stdToml} from "forge-std/StdToml.sol"; import {SafeSingletonDeployer} from "./SafeSingletonDeployer.sol"; // Import contracts to deploy @@ -16,7 +15,6 @@ import {SimpleFunder} from "../src/SimpleFunder.sol"; import {Escrow} from "../src/Escrow.sol"; import {SimpleSettler} from "../src/SimpleSettler.sol"; import {LayerZeroSettler} from "../src/LayerZeroSettler.sol"; -import {ExperimentERC20} from "./mock/ExperimentalERC20.sol"; /** * @title DeployMain @@ -51,26 +49,23 @@ import {ExperimentERC20} from "./mock/ExperimentalERC20.sol"; * --private-key $PRIVATE_KEY \ * "[1]" "/deploy/custom-config.toml" */ -contract DeployMain is Script, Config, SafeSingletonDeployer { +contract DeployMain is Script, SafeSingletonDeployer { + using stdToml for string; // Chain configuration struct struct ChainConfig { uint256 chainId; string name; bool isTestnet; + address pauseAuthority; address funderOwner; address funderSigner; address settlerOwner; address l0SettlerOwner; - address l0SettlerSigner; address layerZeroEndpoint; - address[] oldOrchestrators; uint32 layerZeroEid; bytes32 salt; string[] contracts; // Array of contract names to deploy - // EXP Token configuration (testnet only) - address expMinterAddress; - uint256 expMintAmount; } struct DeployedContracts { @@ -82,8 +77,6 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { address layerZeroSettler; address simpleFunder; address simulator; - address expToken; // EXP token (testnet only) - address exp2Token; // EXP2 token (testnet only) } // State @@ -91,7 +84,9 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { mapping(uint256 => DeployedContracts) internal deployedContracts; uint256[] internal targetChainIds; - // Config path + // Paths and config + string internal registryPath; + string internal configContent; // For unified config string internal configPath = "/deploy/config.toml"; // Events for tracking @@ -109,17 +104,9 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { * @notice Deploy to all chains in config */ function run() external { - // Load configuration and setup forks (enable write-back to save deployed addresses) - string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); - _loadConfigAndForks(fullConfigPath, true); - - // Get all available chain IDs from configuration - targetChainIds = config.getChainIds(); - require(targetChainIds.length > 0, "No chains found in configuration"); - - // Load configuration for each chain - loadConfigurations(); - loadDeployedContracts(); + // Get all available chain IDs from fork configuration + uint256[] memory chainIds = vm.readForkChainIds(); + initializeDeployment(chainIds); executeDeployment(); } @@ -128,20 +115,11 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { * @param chainIds Array of chain IDs to deploy to (empty array = all chains) */ function run(uint256[] memory chainIds) external { - // Load configuration and setup forks (enable write-back to save deployed addresses) - string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); - _loadConfigAndForks(fullConfigPath, true); - // If empty array, get all available chains if (chainIds.length == 0) { - chainIds = config.getChainIds(); + chainIds = vm.readForkChainIds(); } - targetChainIds = chainIds; - require(targetChainIds.length > 0, "No chains found in configuration"); - - // Load configuration for each chain - loadConfigurations(); - loadDeployedContracts(); + initializeDeployment(chainIds); executeDeployment(); } @@ -151,25 +129,47 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { * @param _configPath Path to custom TOML config file */ function run(uint256[] memory chainIds, string memory _configPath) external { - configPath = _configPath; - - // Load configuration and setup forks (enable write-back to save deployed addresses) - string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); - _loadConfigAndForks(fullConfigPath, true); - // If empty array, get all available chains if (chainIds.length == 0) { - chainIds = config.getChainIds(); + chainIds = vm.readForkChainIds(); } + initializeDeployment(chainIds, _configPath); + executeDeployment(); + } + + /** + * @notice Initialize deployment with target chains using TOML config + * @param chainIds Array of chain IDs to deploy to + */ + function initializeDeployment(uint256[] memory chainIds) internal { + require(chainIds.length > 0, "No chains found in configuration"); + + // Load unified configuration + string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); + configContent = vm.readFile(fullConfigPath); + + // Load registry path from config.toml + registryPath = configContent.readString(".profile.deployment.registry_path"); + + // Store target chain IDs targetChainIds = chainIds; - require(targetChainIds.length > 0, "No chains found in configuration"); - + // Load configuration for each chain loadConfigurations(); + + // Load existing deployed contracts from registry loadDeployedContracts(); - executeDeployment(); } + /** + * @notice Initialize deployment with custom config path + * @param chainIds Array of chain IDs to deploy to + * @param _configPath Path to the config file + */ + function initializeDeployment(uint256[] memory chainIds, string memory _configPath) internal { + configPath = _configPath; + initializeDeployment(chainIds); + } /** * @notice Load configurations for all target chains @@ -178,15 +178,20 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { for (uint256 i = 0; i < targetChainIds.length; i++) { uint256 chainId = targetChainIds[i]; - // Switch to the fork for this chain (already created by _loadConfigAndForks) - vm.selectFork(forkOf[chainId]); + // Use the RPC_{chainId} environment variable directly + // This matches the naming convention in config.toml + string memory rpcUrl = vm.envString(string.concat("RPC_", vm.toString(chainId))); + + // Create fork using the RPC URL + uint256 forkId = vm.createFork(rpcUrl); + vm.selectFork(forkId); // Verify we're on the correct chain require(block.chainid == chainId, "Chain ID mismatch"); - // Load configuration using new StdConfig pattern - ChainConfig memory chainConfig = loadChainConfigFromStdConfig(chainId); - chainConfigs[chainId] = chainConfig; + // Load configuration from fork variables + ChainConfig memory config = loadChainConfigFromFork(chainId); + chainConfigs[chainId] = config; } // Log the loaded configuration for verification @@ -194,66 +199,48 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { } /** - * @notice Load chain configuration using StdConfig + * @notice Load chain configuration from the currently active fork * @param chainId The chain ID we're loading config for */ - function loadChainConfigFromStdConfig(uint256 chainId) internal view returns (ChainConfig memory) { - ChainConfig memory chainConfig; + function loadChainConfigFromFork(uint256 chainId) internal view returns (ChainConfig memory) { + ChainConfig memory config; - chainConfig.chainId = chainId; + config.chainId = chainId; - // Use StdConfig to read variables - chainConfig.name = config.get(chainId, "name").toString(); - chainConfig.isTestnet = config.get(chainId, "is_testnet").toBool(); + // Use vm.readFork* functions to read variables from the active fork + config.name = vm.readForkString("name"); + config.isTestnet = vm.readForkBool("is_testnet"); // Load addresses - chainConfig.funderOwner = config.get(chainId, "funder_owner").toAddress(); - chainConfig.funderSigner = config.get(chainId, "funder_signer").toAddress(); - chainConfig.settlerOwner = config.get(chainId, "settler_owner").toAddress(); - chainConfig.l0SettlerOwner = config.get(chainId, "l0_settler_owner").toAddress(); - chainConfig.l0SettlerSigner = config.get(chainId, "l0_settler_signer").toAddress(); - chainConfig.layerZeroEndpoint = config.get(chainId, "layerzero_endpoint").toAddress(); + config.pauseAuthority = vm.readForkAddress("pause_authority"); + config.funderOwner = vm.readForkAddress("funder_owner"); + config.funderSigner = vm.readForkAddress("funder_signer"); + config.settlerOwner = vm.readForkAddress("settler_owner"); + config.l0SettlerOwner = vm.readForkAddress("l0_settler_owner"); + config.layerZeroEndpoint = vm.readForkAddress("layerzero_endpoint"); // Load other configuration - chainConfig.layerZeroEid = uint32(config.get(chainId, "layerzero_eid").toUint256()); - chainConfig.salt = config.get(chainId, "salt").toBytes32(); - - // Load EXP token configuration (testnet only) - if (chainConfig.isTestnet) { - chainConfig.expMinterAddress = config.get(chainId, "exp_minter_address").toAddress(); - chainConfig.expMintAmount = config.get(chainId, "exp_mint_amount").toUint256(); - } + config.layerZeroEid = uint32(vm.readForkUint("layerzero_eid")); + config.salt = vm.readForkBytes32("salt"); // Load contracts list - required field, will revert if not present - string[] memory contractsList = config.get(chainId, "contracts").toStringArray(); + string[] memory contractsList = vm.readForkStringArray("contracts"); // Check if user specified "ALL" to deploy all contracts if ( contractsList.length == 1 && keccak256(bytes(contractsList[0])) == keccak256(bytes("ALL")) ) { - string[] memory baseContracts = getAllContracts(); - // For testnets with ALL specified, append ExpToken - if (chainConfig.isTestnet) { - string[] memory testnetContracts = new string[](baseContracts.length + 1); - for (uint256 i = 0; i < baseContracts.length; i++) { - testnetContracts[i] = baseContracts[i]; - } - testnetContracts[baseContracts.length] = "ExpToken"; - chainConfig.contracts = testnetContracts; - } else { - // For non-testnets, use base contracts (no ExpToken) - chainConfig.contracts = baseContracts; - } + config.contracts = getAllContracts(); } else { - chainConfig.contracts = contractsList; + config.contracts = contractsList; } - return chainConfig; + return config; } /** - * @notice Get all available contracts (excluding ExpToken) + * @notice Get all available contracts */ function getAllContracts() internal pure returns (string[] memory) { string[] memory contracts = new string[](8); @@ -286,40 +273,31 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { } /** - * @notice Load deployed contracts from config + * @notice Load deployed contracts from registry */ function loadDeployedContracts() internal { for (uint256 i = 0; i < targetChainIds.length; i++) { uint256 chainId = targetChainIds[i]; - - DeployedContracts memory deployed; - - // Read deployed contract addresses from config, defaulting to address(0) if not set - deployed.orchestrator = tryGetAddress(chainId, "orchestrator_deployed"); - deployed.ithacaAccount = tryGetAddress(chainId, "ithaca_account_deployed"); - deployed.accountProxy = tryGetAddress(chainId, "account_proxy_deployed"); - deployed.escrow = tryGetAddress(chainId, "escrow_deployed"); - deployed.simpleSettler = tryGetAddress(chainId, "simple_settler_deployed"); - deployed.layerZeroSettler = tryGetAddress(chainId, "layerzero_settler_deployed"); - deployed.simpleFunder = tryGetAddress(chainId, "simple_funder_deployed"); - deployed.simulator = tryGetAddress(chainId, "simulator_deployed"); - deployed.expToken = tryGetAddress(chainId, "exp_token_deployed"); - deployed.exp2Token = tryGetAddress(chainId, "exp2_token_deployed"); - - deployedContracts[chainId] = deployed; - } - } - - /** - * @notice Try to get an address from config, return address(0) if not found - */ - function tryGetAddress(uint256 chainId, string memory key) internal view returns (address) { - Variable memory variable = config.get(chainId, key); - // Check if variable exists (TypeKind.None means missing key) - if (variable.ty.kind == TypeKind.None) { - return address(0); + bytes32 salt = chainConfigs[chainId].salt; + string memory registryFile = getRegistryFilename(chainId, salt); + + try vm.readFile(registryFile) returns (string memory registryJson) { + // Use individual parsing for flexibility with missing fields + DeployedContracts memory deployed; + deployed.ithacaAccount = tryReadAddress(registryJson, ".IthacaAccount"); + deployed.accountProxy = tryReadAddress(registryJson, ".AccountProxy"); + deployed.escrow = tryReadAddress(registryJson, ".Escrow"); + deployed.orchestrator = tryReadAddress(registryJson, ".Orchestrator"); + deployed.simpleSettler = tryReadAddress(registryJson, ".SimpleSettler"); + deployed.layerZeroSettler = tryReadAddress(registryJson, ".LayerZeroSettler"); + deployed.simpleFunder = tryReadAddress(registryJson, ".SimpleFunder"); + deployed.simulator = tryReadAddress(registryJson, ".Simulator"); + + deployedContracts[chainId] = deployed; + } catch { + // No registry file exists yet + } } - return variable.toAddress(); } /** @@ -337,8 +315,8 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { console.log("Funder Owner:", config.funderOwner); console.log("Funder Signer:", config.funderSigner); console.log("L0 Settler Owner:", config.l0SettlerOwner); - console.log("L0 Settler Signer:", config.l0SettlerSigner); console.log("Settler Owner:", config.settlerOwner); + console.log("Pause Authority:", config.pauseAuthority); console.log("LayerZero Endpoint:", config.layerZeroEndpoint); console.log("LayerZero EID:", config.layerZeroEid); console.log("Salt:"); @@ -445,7 +423,7 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { } /** - * @notice Save deployed contract address to config + * @notice Save deployed contract address to registry */ function saveDeployedContract( uint256 chainId, @@ -469,38 +447,124 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { deployedContracts[chainId].simpleSettler = contractAddress; } else if (keccak256(bytes(contractName)) == keccak256("LayerZeroSettler")) { deployedContracts[chainId].layerZeroSettler = contractAddress; - } else if (keccak256(bytes(contractName)) == keccak256("ExpToken")) { - deployedContracts[chainId].expToken = contractAddress; - } else if (keccak256(bytes(contractName)) == keccak256("Exp2Token")) { - deployedContracts[chainId].exp2Token = contractAddress; - } - - // Only write to config file during actual broadcasts, not simulations - if (vm.isContext(VmSafe.ForgeContext.ScriptBroadcast) || vm.isContext(VmSafe.ForgeContext.ScriptResume)) { - if (keccak256(bytes(contractName)) == keccak256("Orchestrator")) { - config.set(chainId, "orchestrator_deployed", contractAddress); - } else if (keccak256(bytes(contractName)) == keccak256("IthacaAccount")) { - config.set(chainId, "ithaca_account_deployed", contractAddress); - } else if (keccak256(bytes(contractName)) == keccak256("AccountProxy")) { - config.set(chainId, "account_proxy_deployed", contractAddress); - } else if (keccak256(bytes(contractName)) == keccak256("Simulator")) { - config.set(chainId, "simulator_deployed", contractAddress); - } else if (keccak256(bytes(contractName)) == keccak256("SimpleFunder")) { - config.set(chainId, "simple_funder_deployed", contractAddress); - } else if (keccak256(bytes(contractName)) == keccak256("Escrow")) { - config.set(chainId, "escrow_deployed", contractAddress); - } else if (keccak256(bytes(contractName)) == keccak256("SimpleSettler")) { - config.set(chainId, "simple_settler_deployed", contractAddress); - } else if (keccak256(bytes(contractName)) == keccak256("LayerZeroSettler")) { - config.set(chainId, "layerzero_settler_deployed", contractAddress); - } else if (keccak256(bytes(contractName)) == keccak256("ExpToken")) { - config.set(chainId, "exp_token_deployed", contractAddress); - } else if (keccak256(bytes(contractName)) == keccak256("Exp2Token")) { - config.set(chainId, "exp2_token_deployed", contractAddress); - } } + + // Write to registry file + writeToRegistry(chainId, contractName, contractAddress); + } + + /** + * @notice Write to registry file + */ + function writeToRegistry(uint256 chainId, string memory contractName, address contractAddress) + internal + { + // Only save registry during actual broadcasts, not dry runs + if ( + !vm.isContext(VmSafe.ForgeContext.ScriptBroadcast) + && !vm.isContext(VmSafe.ForgeContext.ScriptResume) + ) { + return; + } + + DeployedContracts memory deployed = deployedContracts[chainId]; + + string memory json = "{"; + bool first = true; + + if (deployed.orchestrator != address(0)) { + json = string.concat(json, '"Orchestrator": "', vm.toString(deployed.orchestrator), '"'); + first = false; + } + + if (deployed.ithacaAccount != address(0)) { + if (!first) json = string.concat(json, ","); + json = + string.concat(json, '"IthacaAccount": "', vm.toString(deployed.ithacaAccount), '"'); + first = false; + } + + if (deployed.accountProxy != address(0)) { + if (!first) json = string.concat(json, ","); + json = string.concat(json, '"AccountProxy": "', vm.toString(deployed.accountProxy), '"'); + first = false; + } + + if (deployed.simulator != address(0)) { + if (!first) json = string.concat(json, ","); + json = string.concat(json, '"Simulator": "', vm.toString(deployed.simulator), '"'); + first = false; + } + + if (deployed.simpleFunder != address(0)) { + if (!first) json = string.concat(json, ","); + json = string.concat(json, '"SimpleFunder": "', vm.toString(deployed.simpleFunder), '"'); + first = false; + } + + if (deployed.escrow != address(0)) { + if (!first) json = string.concat(json, ","); + json = string.concat(json, '"Escrow": "', vm.toString(deployed.escrow), '"'); + first = false; + } + + if (deployed.simpleSettler != address(0)) { + if (!first) json = string.concat(json, ","); + json = + string.concat(json, '"SimpleSettler": "', vm.toString(deployed.simpleSettler), '"'); + first = false; + } + + if (deployed.layerZeroSettler != address(0)) { + if (!first) json = string.concat(json, ","); + json = string.concat( + json, '"LayerZeroSettler": "', vm.toString(deployed.layerZeroSettler), '"' + ); + } + + json = string.concat(json, "}"); + + bytes32 salt = chainConfigs[chainId].salt; + string memory registryFile = getRegistryFilename(chainId, salt); + vm.writeFile(registryFile, json); + } + + /** + * @notice Get registry filename based on chainId and salt + */ + function getRegistryFilename(uint256 chainId, bytes32 salt) + internal + view + returns (string memory) + { + string memory filename = string.concat( + vm.projectRoot(), + "/", + registryPath, + "deployment_", + vm.toString(chainId), + "_", + vm.toString(salt), + ".json" + ); + return filename; } + /** + * @notice Try to read an address from JSON + */ + function tryReadAddress(string memory json, string memory key) + internal + pure + returns (address) + { + try vm.parseJson(json, key) returns (bytes memory data) { + if (data.length > 0) { + return abi.decode(data, (address)); + } + } catch {} + return address(0); + } /** * @notice Verify Safe Singleton Factory is deployed @@ -599,8 +663,6 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { deploySimpleSettler(chainId, config, deployed); } else if (nameHash == keccak256("LayerZeroSettler")) { deployLayerZeroSettler(chainId, config, deployed); - } else if (nameHash == keccak256("ExpToken")) { - deployExpToken(chainId, config, deployed); } else { console.log("Warning: Unknown contract name:", contractName); } @@ -611,12 +673,17 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - bytes memory creationCode = type(Orchestrator).creationCode; - address orchestrator = - deployContractWithCreate2(chainId, creationCode, "", "Orchestrator"); - - saveDeployedContract(chainId, "Orchestrator", orchestrator); - deployed.orchestrator = orchestrator; + if (deployed.orchestrator == address(0)) { + bytes memory creationCode = type(Orchestrator).creationCode; + bytes memory args = abi.encode(config.pauseAuthority); + address orchestrator = + deployContractWithCreate2(chainId, creationCode, args, "Orchestrator"); + + saveDeployedContract(chainId, "Orchestrator", orchestrator); + deployed.orchestrator = orchestrator; + } else { + console.log("Orchestrator already deployed:", deployed.orchestrator); + } } function deployIthacaAccount( @@ -625,15 +692,22 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { DeployedContracts memory deployed ) internal { // Ensure Orchestrator is deployed first (dependency) - require(deployed.orchestrator != address(0), "Orchestrator must be deployed before IthacaAccount"); + if (deployed.orchestrator == address(0)) { + console.log("Deploying Orchestrator first (dependency for IthacaAccount)..."); + deployOrchestrator(chainId, config, deployed); + } - bytes memory creationCode = type(IthacaAccount).creationCode; - bytes memory args = abi.encode(deployed.orchestrator); - address ithacaAccount = - deployContractWithCreate2(chainId, creationCode, args, "IthacaAccount"); + if (deployed.ithacaAccount == address(0)) { + bytes memory creationCode = type(IthacaAccount).creationCode; + bytes memory args = abi.encode(deployed.orchestrator); + address ithacaAccount = + deployContractWithCreate2(chainId, creationCode, args, "IthacaAccount"); - saveDeployedContract(chainId, "IthacaAccount", ithacaAccount); - deployed.ithacaAccount = ithacaAccount; + saveDeployedContract(chainId, "IthacaAccount", ithacaAccount); + deployed.ithacaAccount = ithacaAccount; + } else { + console.log("IthacaAccount already deployed:", deployed.ithacaAccount); + } } function deployAccountProxy( @@ -642,14 +716,21 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { DeployedContracts memory deployed ) internal { // Ensure IthacaAccount is deployed first (dependency) - require(deployed.ithacaAccount != address(0), "IthacaAccount must be deployed before AccountProxy"); + if (deployed.ithacaAccount == address(0)) { + console.log("Deploying IthacaAccount first (dependency for AccountProxy)..."); + deployIthacaAccount(chainId, config, deployed); + } - bytes memory proxyCode = LibEIP7702.proxyInitCode(deployed.ithacaAccount, address(0)); - address accountProxy = deployContractWithCreate2(chainId, proxyCode, "", "AccountProxy"); + if (deployed.accountProxy == address(0)) { + bytes memory proxyCode = LibEIP7702.proxyInitCode(deployed.ithacaAccount, address(0)); + address accountProxy = deployContractWithCreate2(chainId, proxyCode, "", "AccountProxy"); - require(accountProxy != address(0), "Account proxy deployment failed"); - saveDeployedContract(chainId, "AccountProxy", accountProxy); - deployed.accountProxy = accountProxy; + require(accountProxy != address(0), "Account proxy deployment failed"); + saveDeployedContract(chainId, "AccountProxy", accountProxy); + deployed.accountProxy = accountProxy; + } else { + console.log("AccountProxy already deployed:", deployed.accountProxy); + } } function deploySimulator( @@ -657,11 +738,15 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - bytes memory creationCode = type(Simulator).creationCode; - address simulator = deployContractWithCreate2(chainId, creationCode, "", "Simulator"); + if (deployed.simulator == address(0)) { + bytes memory creationCode = type(Simulator).creationCode; + address simulator = deployContractWithCreate2(chainId, creationCode, "", "Simulator"); - saveDeployedContract(chainId, "Simulator", simulator); - deployed.simulator = simulator; + saveDeployedContract(chainId, "Simulator", simulator); + deployed.simulator = simulator; + } else { + console.log("Simulator already deployed:", deployed.simulator); + } } function deploySimpleFunder( @@ -669,13 +754,23 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - bytes memory creationCode = type(SimpleFunder).creationCode; + // Ensure Orchestrator is deployed first (dependency) + if (deployed.orchestrator == address(0)) { + console.log("Deploying Orchestrator first (dependency for SimpleFunder)..."); + deployOrchestrator(chainId, config, deployed); + } - bytes memory args = abi.encode(config.funderSigner, config.funderOwner); - address funder = deployContractWithCreate2(chainId, creationCode, args, "SimpleFunder"); + if (deployed.simpleFunder == address(0)) { + bytes memory creationCode = type(SimpleFunder).creationCode; + bytes memory args = + abi.encode(config.funderSigner, deployed.orchestrator, config.funderOwner); + address funder = deployContractWithCreate2(chainId, creationCode, args, "SimpleFunder"); - saveDeployedContract(chainId, "SimpleFunder", funder); - deployed.simpleFunder = funder; + saveDeployedContract(chainId, "SimpleFunder", funder); + deployed.simpleFunder = funder; + } else { + console.log("SimpleFunder already deployed:", deployed.simpleFunder); + } } function deployEscrow( @@ -683,11 +778,15 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - bytes memory creationCode = type(Escrow).creationCode; - address escrow = deployContractWithCreate2(chainId, creationCode, "", "Escrow"); + if (deployed.escrow == address(0)) { + bytes memory creationCode = type(Escrow).creationCode; + address escrow = deployContractWithCreate2(chainId, creationCode, "", "Escrow"); - saveDeployedContract(chainId, "Escrow", escrow); - deployed.escrow = escrow; + saveDeployedContract(chainId, "Escrow", escrow); + deployed.escrow = escrow; + } else { + console.log("Escrow already deployed:", deployed.escrow); + } } function deploySimpleSettler( @@ -695,14 +794,18 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - bytes memory creationCode = type(SimpleSettler).creationCode; - bytes memory args = abi.encode(config.settlerOwner); - address settler = - deployContractWithCreate2(chainId, creationCode, args, "SimpleSettler"); - - console.log(" Owner:", config.settlerOwner); - saveDeployedContract(chainId, "SimpleSettler", settler); - deployed.simpleSettler = settler; + if (deployed.simpleSettler == address(0)) { + bytes memory creationCode = type(SimpleSettler).creationCode; + bytes memory args = abi.encode(config.settlerOwner); + address settler = + deployContractWithCreate2(chainId, creationCode, args, "SimpleSettler"); + + console.log(" Owner:", config.settlerOwner); + saveDeployedContract(chainId, "SimpleSettler", settler); + deployed.simpleSettler = settler; + } else { + console.log("SimpleSettler already deployed:", deployed.simpleSettler); + } } function deployLayerZeroSettler( @@ -710,62 +813,19 @@ contract DeployMain is Script, Config, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - bytes memory creationCode = type(LayerZeroSettler).creationCode; - bytes memory args = abi.encode(config.l0SettlerOwner, config.l0SettlerSigner); - address settler = - deployContractWithCreate2(chainId, creationCode, args, "LayerZeroSettler"); - - console.log(" Owner:", config.l0SettlerOwner); - console.log(" L0SettlerSigner:", config.l0SettlerSigner); - console.log(" Endpoint to be configured:", config.layerZeroEndpoint); - console.log(" EID:", config.layerZeroEid); - console.log( - " Note: Endpoint must be set by owner via ConfigureLayerZeroSettler script" - ); - - saveDeployedContract(chainId, "LayerZeroSettler", settler); - deployed.layerZeroSettler = settler; - } - - function deployExpToken( - uint256 chainId, - ChainConfig memory config, - DeployedContracts memory deployed - ) internal { - // Only deploy on testnets - if (!config.isTestnet) { - console.log("Skipping ExpToken deployment - not a testnet"); - return; + if (deployed.layerZeroSettler == address(0)) { + bytes memory creationCode = type(LayerZeroSettler).creationCode; + bytes memory args = abi.encode(config.layerZeroEndpoint, config.l0SettlerOwner); + address settler = + deployContractWithCreate2(chainId, creationCode, args, "LayerZeroSettler"); + + console.log(" Endpoint:", config.layerZeroEndpoint); + console.log(" Owner:", config.l0SettlerOwner); + console.log(" EID:", config.layerZeroEid); + saveDeployedContract(chainId, "LayerZeroSettler", settler); + deployed.layerZeroSettler = settler; + } else { + console.log("LayerZeroSettler already deployed:", deployed.layerZeroSettler); } - - bytes memory creationCode = type(ExperimentERC20).creationCode; - - // Deploy EXP token - // Hardcode name and symbol to "EXP" - bytes memory args = abi.encode("EXP", "EXP", 1 ether); - address expToken = deployContractWithCreate2(chainId, creationCode, args, "ExpToken"); - - // Mint initial tokens to the configured minter address - ExperimentERC20(payable(expToken)).mint(config.expMinterAddress, config.expMintAmount); - - console.log(" EXP Name/Symbol: EXP"); - console.log(" EXP Address:", expToken); - saveDeployedContract(chainId, "ExpToken", expToken); - deployed.expToken = expToken; - - // Deploy EXP2 token - // Hardcode name and symbol to "EXP2" - bytes memory args2 = abi.encode("EXP2", "EXP2", 1 ether); - address exp2Token = deployContractWithCreate2(chainId, creationCode, args2, "Exp2Token"); - - // Mint initial tokens to the configured minter address (same as EXP) - ExperimentERC20(payable(exp2Token)).mint(config.expMinterAddress, config.expMintAmount); - - console.log(" EXP2 Name/Symbol: EXP2"); - console.log(" EXP2 Address:", exp2Token); - console.log(" Minter:", config.expMinterAddress); - console.log(" Mint Amount (each):", config.expMintAmount); - saveDeployedContract(chainId, "Exp2Token", exp2Token); - deployed.exp2Token = exp2Token; } } diff --git a/deploy/FundSigners.s.sol b/deploy/FundSigners.s.sol index 42618c6e..02820e27 100644 --- a/deploy/FundSigners.s.sol +++ b/deploy/FundSigners.s.sol @@ -2,14 +2,12 @@ pragma solidity ^0.8.23; import {Script, console} from "forge-std/Script.sol"; -import {Config} from "forge-std/Config.sol"; +import {stdToml} from "forge-std/StdToml.sol"; // SimpleFunder interface for setting gas wallets interface ISimpleFunder { function setGasWallet(address[] memory wallets, bool isGasWallet) external; function gasWallets(address) external view returns (bool); - function setOrchestrators(address[] memory ocs, bool val) external; - function orchestrators(address) external view returns (bool); } /** @@ -48,7 +46,9 @@ interface ISimpleFunder { * --private-key $PRIVATE_KEY \ * "[84532]" 5 */ -contract FundSigners is Script, Config { +contract FundSigners is Script { + using stdToml for string; + /** * @notice Configuration for funding on a specific chain */ @@ -59,7 +59,6 @@ contract FundSigners is Script, Config { uint256 targetBalance; address simpleFunderAddress; uint256 defaultNumSigners; - address[] supportedOrchestrators; } /** @@ -88,18 +87,18 @@ contract FundSigners is Script, Config { uint256 private totalEthDistributed; uint256 private chainsProcessed; + string internal configContent; string internal configPath = "/deploy/config.toml"; /** * @notice Fund all configured chains with default number of signers */ function run() external { - // Load configuration and setup forks - string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); - _loadConfigAndForks(fullConfigPath, false); - - // Get all chain IDs from configuration - uint256[] memory chainIds = config.getChainIds(); + // Default to common testnets + uint256[] memory chainIds = new uint256[](3); + chainIds[0] = 11155111; // Sepolia + chainIds[1] = 84532; // Base Sepolia + chainIds[2] = 11155420; // Optimism Sepolia uint256 numSigners = 10; // Default execute(chainIds, numSigners); } @@ -109,10 +108,6 @@ contract FundSigners is Script, Config { * @param chainIds Array of chain IDs to fund */ function run(uint256[] memory chainIds) external { - // Load configuration and setup forks - string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); - _loadConfigAndForks(fullConfigPath, false); - uint256 numSigners = 10; // Default execute(chainIds, numSigners); } @@ -123,10 +118,6 @@ contract FundSigners is Script, Config { * @param numSigners Number of signers to fund (starting from index 0) */ function run(uint256[] memory chainIds, uint256 numSigners) external { - // Load configuration and setup forks - string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); - _loadConfigAndForks(fullConfigPath, false); - execute(chainIds, numSigners); } @@ -137,6 +128,9 @@ contract FundSigners is Script, Config { console.log("=== Signer Funding Script (TOML Config) ==="); console.log("Number of signers to fund:", numSigners); + // Load configuration + loadConfig(); + // Get mnemonic from environment string memory mnemonic = vm.envString("GAS_SIGNER_MNEMONIC"); require(bytes(mnemonic).length > 0, "GAS_SIGNER_MNEMONIC not set"); @@ -217,9 +211,6 @@ contract FundSigners is Script, Config { if (config.simpleFunderAddress != address(0) && config.simpleFunderAddress.code.length > 0) { setGasWalletsInSimpleFunder(config.simpleFunderAddress, signers); - setOrchestratorsInSimpleFunder( - config.simpleFunderAddress, config.supportedOrchestrators - ); } vm.stopBroadcast(); @@ -421,41 +412,6 @@ contract FundSigners is Script, Config { ); } - /** - * @notice Set orchestrators in SimpleFunder contract - */ - function setOrchestratorsInSimpleFunder(address simpleFunder, address[] memory orchestrators) - internal - { - if (orchestrators.length == 0) { - console.log("No orchestrators configured, skipping orchestrator setup"); - return; - } - - ISimpleFunder funder = ISimpleFunder(simpleFunder); - - console.log("\nSetting orchestrators in SimpleFunder:"); - console.log(" SimpleFunder address:", simpleFunder); - console.log( - string.concat(" Setting ", vm.toString(orchestrators.length), " orchestrators...") - ); - - for (uint256 i = 0; i < orchestrators.length; i++) { - console.log( - string.concat( - " Orchestrator ", vm.toString(i), ": ", vm.toString(orchestrators[i]) - ) - ); - } - - funder.setOrchestrators(orchestrators, true); - console.log( - string.concat( - " Successfully set all ", vm.toString(orchestrators.length), " orchestrators" - ) - ); - } - /** * @notice Derive signer addresses from mnemonic */ @@ -475,29 +431,45 @@ contract FundSigners is Script, Config { return signers; } + /** + * @notice Load configuration from TOML + */ + function loadConfig() internal { + string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); + configContent = vm.readFile(fullConfigPath); + } + /** * @notice Get chain funding configuration */ function getChainFundingConfig(uint256 chainId) internal returns (ChainFundingConfig memory) { - // Switch to the fork for this chain (already created by _loadConfigAndForks) - vm.selectFork(forkOf[chainId]); - - ChainFundingConfig memory chainConfig; - chainConfig.chainId = chainId; - chainConfig.name = config.get(chainId, "name").toString(); - chainConfig.isTestnet = config.get(chainId, "is_testnet").toBool(); - chainConfig.targetBalance = config.get(chainId, "target_balance").toUint256(); - chainConfig.simpleFunderAddress = config.get(chainId, "simple_funder_deployed").toAddress(); + // Create fork and read configuration from fork variables + string memory rpcUrl = vm.envString(string.concat("RPC_", vm.toString(chainId))); + uint256 forkId = vm.createFork(rpcUrl); + vm.selectFork(forkId); + + ChainFundingConfig memory config; + config.chainId = chainId; + config.name = vm.readForkString("name"); + config.isTestnet = vm.readForkBool("is_testnet"); + config.targetBalance = vm.readForkUint("target_balance"); + + // Try to read SimpleFunder address - may not be deployed on all chains + try vm.readForkAddress("simple_funder_address") returns (address addr) { + config.simpleFunderAddress = addr; + } catch { + // SimpleFunder not configured for this chain + config.simpleFunderAddress = address(0); + } // Try to read default number of signers - uint256 numSigners = config.get(chainId, "default_num_signers").toUint256(); - chainConfig.defaultNumSigners = numSigners == 0 ? 10 : numSigners; // Default fallback - - // Read supported orchestrators - required field - chainConfig.supportedOrchestrators = - config.get(chainId, "supported_orchestrators").toAddressArray(); + try vm.readForkUint("default_num_signers") returns (uint256 num) { + config.defaultNumSigners = num; + } catch { + config.defaultNumSigners = 10; // Default fallback + } - return chainConfig; + return config; } /** diff --git a/deploy/README.md b/deploy/README.md index b23c7606..cb27d1ef 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -1,6 +1,6 @@ # Deployment System -Unified deployment and configuration system for the Ithaca Account Abstraction System. +Unified deployment and configuration system for the Ithaca Account Abstraction System. We use a single TOML config for fast and easy scripting. ## Available Scripts @@ -55,90 +55,69 @@ optimism-sepolia = { key = "${ETHERSCAN_API_KEY}" } ## Configuration Structure -All configuration is in `deploy/config.toml` using the StdConfig format: +All configuration is in `deploy/config.toml`: ```toml -[base-sepolia] -endpoint_url = "${RPC_84532}" +[profile.deployment] +registry_path = "deploy/registry/" -[base-sepolia.bool] -is_testnet = true +[forks.base-sepolia] +rpc_url = "${RPC_84532}" -[base-sepolia.address] +[forks.base-sepolia.vars] # Chain identification -funder_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" -funder_signer = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" -settler_owner = "0x0000000000000000000000000000000000000004" -l0_settler_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" -l0_settler_signer = "0x0000000000000000000000000000000000000006" -layerzero_endpoint = "0x6EDCE65403992e310A62460808c4b910D972f10f" -simple_funder_address = "0x09F6eF9525efAdb6167dFe71fFcfbE306De07988" -layerzero_settler_address = "0xd71d3c3ff2249A67cEa12030b20E66734fB1f833" -layerzero_send_uln302 = "0xC1868e054425D378095A003EcbA3823a5D0135C9" -layerzero_receive_uln302 = "0x12523de19dc41c91F7d2093E0CFbB76b17012C8d" -dvn_layerzero_labs = "0xe1a12515F9AB2764b887bF60B923Ca494EBbB2d6" -dvn_google_cloud = "0xFc9d8E5d3FaB22fB6E93E9E2C90916E9dCa83Ade" -exp_minter_address = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" -supported_orchestrators = ["0xEd7c1e839381c489Dcd1ED3CE1B0e79DaE714f77"] - -# Deployed contract addresses - automatically written during deployment -orchestrator_deployed = "0xC662Af195CD57bC330552f3E2Be5E03Ef69cB041" -ithaca_account_deployed = "0x49627C39cC7f39f95540C2100f18608f2365a59f" -account_proxy_deployed = "0xD2a48e4635fCB2437d2e482122137F06C8433706" -simulator_deployed = "0x332A5Cd675D9d26c4af3BF618A7175d0D623CABA" -simple_funder_deployed = "0x1ADE5D4CE3183D913791DEcaeaD42Fff193AeF8F" -escrow_deployed = "0xD1c7e21f2a50A2cDDCFaf554b998a800C3C35aD1" -simple_settler_deployed = "0x3291f7Ce832997920874d70d68A8186B388024F5" -layerzero_settler_deployed = "0xBDb45dA9e075a9fCbdf8fAa9d0c93A21b3D8671a" -exp_token_deployed = "0xaeB83430528fB0DeE5E15bF07A5056B6c0b37809" -exp2_token_deployed = "0x246c631Dac318a13023e98aB925499930c9801fB" - -[base-sepolia.uint] chain_id = 84532 +name = "Base Sepolia" +is_testnet = true + +# Contract ownership +pause_authority = "0x..." # Can pause contracts +funder_owner = "0x..." # Owns SimpleFunder +funder_signer = "0x..." # Signs funding operations +settler_owner = "0x..." # Owns SimpleSettler +l0_settler_owner = "0x..." # Owns LayerZeroSettler + +# Deployment configuration +salt = "0x0000..." # CREATE2 salt (SAVE THIS!) +contracts = ["ALL"] # Or specific: ["Orchestrator", "IthacaAccount"] + +# Funding configuration (only needed for Funding Script) +target_balance = 1000000000000000 # Target balance per signer (0.001 ETH) +simple_funder_address = "0x..." # SimpleFunder address +default_num_signers = 10 # Number of signers to fund + +# LayerZero configuration (only needed for ConfigureLayerZero) +layerzero_settler_address = "0x..." +layerzero_endpoint = "0x..." layerzero_eid = 40245 -target_balance = "1000000000000000" # Target balance per signer (0.001 ETH) - must be quoted for large numbers -default_num_signers = 10 # Number of signers to fund -layerzero_confirmations = 1 -layerzero_max_message_size = 10000 -layerzero_optional_dvn_threshold = 0 -exp_mint_amount = "5000000000000000000000" # Amount to mint (in wei) - must be quoted +layerzero_send_uln302 = "0x..." +layerzero_receive_uln302 = "0x..." layerzero_destination_chain_ids = [11155420] - -[base-sepolia.bytes32] -salt = "0x0000000000000000000000000000000000000000000000000000000000005678" # CREATE2 salt (SAVE THIS!) - -[base-sepolia.string] -name = "Base Sepolia" -contracts = ["ALL"] # Or specific: ["Orchestrator", "IthacaAccount"] layerzero_required_dvns = ["dvn_layerzero_labs"] layerzero_optional_dvns = [] -``` - -### Deployed Address Management - -**Automatic Address Writing**: The deployment scripts automatically write deployed contract addresses to the config file during broadcast operations: +layerzero_optional_dvn_threshold = 0 +layerzero_confirmations = 1 +layerzero_max_message_size = 10000 -- ✅ **During `--broadcast`**: Deployed addresses are written to config.toml as `contract_name_deployed = "0x..."` -- ❌ **During simulation** (no `--broadcast`): No config writes occur - safe for testing -- 📍 **Address detection**: Scripts always check actual on-chain state, not config file data -- 🔄 **State synchronization**: Config file stays in sync with actual deployments +dvn_layerzero_labs = "0x..." +dvn_google_cloud = "0x..." +``` ### Available Contracts - **Orchestrator** -- **IthacaAccount** -- **AccountProxy** -- **Simulator** -- **SimpleFunder** +- **IthacaAccount** +- **AccountProxy** +- **Simulator** +- **SimpleFunder** - **Escrow** (Only needed for Interop Chains) - **SimpleSettler** (Only needed for Interop testing) - **LayerZeroSettler** (Only needed for Interop Chains) -- **ExpToken** - Test ERC20 tokens (Testnet only, automatically included with "ALL") -- **ALL** - Deploys all contracts (+ ExpToken on testnets) +- **ALL** - Deploys all contracts -**Dependencies**: -IthacaAccount requires Orchestrator; -AccountProxy requires IthacaAccount; +**Dependencies**: +IthacaAccount requires Orchestrator; +AccountProxy requires IthacaAccount; SimpleFunder requires Orchestrator. ## Quick Start - Complete Workflow @@ -170,7 +149,8 @@ forge script deploy/FundSigners.s.sol:FundSigners \ --private-key $PRIVATE_KEY \ "[84532,11155420]" -# 5. Fund SimpleFunder contract +# 5. Fund SimpleFunder contract +SIMPLE_FUNDER=$(cat deploy/registry/deployment_84532_*.json | jq -r .SimpleFunder) forge script deploy/FundSimpleFunder.s.sol:FundSimpleFunder \ --broadcast --multi --slow \ @@ -194,7 +174,7 @@ forge script deploy/FundSimpleFunder.s.sol:FundSimpleFunder \ forge script deploy/DeployMain.s.sol:DeployMain \ --broadcast --verify --multi --slow \ --sig "run()" \ - --private-key $PRIVATE_KEY + --private-key $PRIVATE_KEY # Deploy to specific chains forge script deploy/DeployMain.s.sol:DeployMain \ @@ -227,8 +207,7 @@ forge script deploy/DeployMain.s.sol:DeployMain \ **Purpose**: Configure LayerZero messaging pathways between chains. -**Prerequisites**: - +**Prerequisites**: - LayerZeroSettler deployed on source and destination chains - Caller must be l0_settler_owner @@ -251,18 +230,15 @@ forge script deploy/ConfigureLayerZeroSettler.s.sol:ConfigureLayerZeroSettler \ **Purpose**: Fund signers and register them as gas wallets in SimpleFunder. -**Prerequisites**: - +**Prerequisites**: - SimpleFunder deployed - Caller must be funder_owner - GAS_SIGNER_MNEMONIC environment variable set **What it does**: - 1. Derives signer addresses from mnemonic 2. Tops up signers below target_balance 3. Registers signers as gas wallets in SimpleFunder -4. Sets configured orchestrators in SimpleFunder ```bash # Fund default number of signers (from config) @@ -305,7 +281,6 @@ forge script deploy/FundSimpleFunder.s.sol:FundSimpleFunder \ ``` **Parameters**: - - SimpleFunder address (same across chains if using CREATE2) - Array of (chainId, tokenAddress, amount) - Use `0x0000000000000000000000000000000000000000` for native ETH @@ -322,36 +297,37 @@ forge script deploy/FundSimpleFunder.s.sol:FundSimpleFunder \ All contracts deploy via Safe Singleton Factory (`0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7`) for deterministic addresses. **Key Points**: - - Same salt + same bytecode = same address on every chain - Addresses can be predicted before deployment - **⚠️ SAVE YOUR SALT VALUES** - Required for deploying to same addresses on new chains -- **Deployment decisions based on on-chain state** - Scripts check actual deployed contracts, not config file data -## Adding New Chains +## Registry Files -1. Add configuration to `deploy/config.toml`: +Deployment addresses are saved in `deploy/registry/deployment_{chainId}_{salt}.json`: -```toml -[new-chain] -endpoint_url = "${RPC_CHAINID}" +```json +{ + "Orchestrator": "0xb33adF2c2257a94314d408255aC843fd53B1a7e1", + "IthacaAccount": "0x5a87ef243CDA70a855828d4989Fad61B56A467d3", + "AccountProxy": "0x4ACD713815fbb363a89D9Ff046C56cEdC7EF3ad7", + "SimpleFunder": "0xA47C5C472449979a2F37dF2971627cD6587bADb8" +} +``` -[new-chain.bool] -is_testnet = true +Registry files are for reference only - deployment decisions are based on on-chain state. -[new-chain.address] -funder_owner = "0x..." -# ... all required fields +## Adding New Chains -[new-chain.uint] -chain_id = CHAINID -# ... all required fields +1. Add configuration to `deploy/config.toml`: -[new-chain.bytes32] -salt = "0x0000000000000000000000000000000000000000000000000000000000005678" +```toml +[forks.new-chain] +rpc_url = "${RPC_CHAINID}" -[new-chain.string] +[forks.new-chain.vars] +chain_id = CHAINID name = "Chain Name" +# ... all required fields contracts = ["ALL"] ``` @@ -376,22 +352,18 @@ forge script deploy/DeployMain.s.sol:DeployMain \ ### Common Issues **"No chains found in configuration"** - - Verify config.toml has properly configured chains - Check RPC URLs are set for target chains **"Safe Singleton Factory not deployed"** - - Factory must exist at `0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7` - Most major chains have this deployed **Contract already deployed** - - Normal for CREATE2 - existing contracts are skipped - Change salt value to deploy to new addresses **RPC errors** - - Verify RPC URLs are correct and accessible - Check rate limits on public RPCs - Consider paid RPC services for production @@ -404,145 +376,38 @@ forge script deploy/DeployMain.s.sol:DeployMain \ 4. **Commit registry files** - Provides deployment history 5. **Use `--multi --slow`** - Ensures proper multi-chain ordering 6. **Verify while deploying** - Use `--verify` flag -7. **Large numbers must be quoted in TOML** - Use `"1000000000000000000"` not `1000000000000000000` ## Configuration Field Reference -| Field | Used By | Purpose | -| --------------------------------------- | ------------------------------ | ------------------------------------------------ | -| `chain_id`, `name`, `is_testnet` | All scripts | Chain identification | -| `pause_authority` | DeployMain | Contract pause permissions | -| `funder_owner`, `funder_signer` | DeployMain, FundSigners | SimpleFunder control | -| `settler_owner` | DeployMain | SimpleSettler ownership | -| `l0_settler_owner` | DeployMain, ConfigureLayerZero | LayerZeroSettler ownership | -| `salt` | DeployMain | CREATE2 deployment salt | -| `contracts` | DeployMain | Which contracts to deploy | -| `target_balance` | FundSigners | Minimum signer balance | -| `simple_funder_address` | FundSigners, FundSimpleFunder | SimpleFunder location | -| `default_num_signers` | FundSigners | Number of signers | -| `supported_orchestrators` | FundSigners | Orchestrator addresses to enable in SimpleFunder | -| `layerzero_*` fields | ConfigureLayerZeroSettler | LayerZero configuration | -| `exp_minter_address`, `exp_mint_amount` | DeployMain (testnet) | ExpToken deployment | -| `*_deployed` fields | All scripts | Auto-written deployed contract addresses | - -## ExpToken Deployment (Testnets Only) - -### Automatic ExpToken Deployment +| Field | Used By | Purpose | +|-------|---------|---------| +| `chain_id`, `name`, `is_testnet` | All scripts | Chain identification | +| `pause_authority` | DeployMain | Contract pause permissions | +| `funder_owner`, `funder_signer` | DeployMain, FundSigners | SimpleFunder control | +| `settler_owner` | DeployMain | SimpleSettler ownership | +| `l0_settler_owner` | DeployMain, ConfigureLayerZero | LayerZeroSettler ownership | +| `salt` | DeployMain | CREATE2 deployment salt | +| `contracts` | DeployMain | Which contracts to deploy | +| `target_balance` | FundSigners | Minimum signer balance | +| `simple_funder_address` | FundSigners, FundSimpleFunder | SimpleFunder location | +| `default_num_signers` | FundSigners | Number of signers | +| `layerzero_*` fields | ConfigureLayerZeroSettler | LayerZero configuration | -**Purpose**: Deploy EXP and EXP2 test tokens automatically on testnet chains. +## Test Token Deployment -**Behavior**: +### DeployEXP - Test ERC20 Token -- **Testnets** (`is_testnet = true`): ExpToken automatically included when using `["ALL"]` contracts -- **Production** (`is_testnet = false`): ExpToken never deployed, regardless of configuration -- **Two tokens deployed**: "EXP" and "EXP2" with hardcoded names -- **Same configuration**: Both tokens use the same minter address and mint amount - -### Configuration Requirements - -For testnet chains, **both fields are required** (deployment will fail if missing): - -```toml -[testnet-name.bool] -is_testnet = true - -[testnet-name.address] -exp_minter_address = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" # REQUIRED - -[testnet-name.uint] -exp_mint_amount = "5000000000000000000000" # REQUIRED (5000 tokens in wei) - -[testnet-name.string] -contracts = ["ALL"] # ExpToken automatically included for testnets -``` - -### Deployment Details - -**Two tokens are deployed**: - -1. **EXP Token**: Name and symbol "EXP" -2. **EXP2 Token**: Name and symbol "EXP2" - -**Both tokens**: - -- Use CREATE2 for deterministic addresses -- Mint `exp_mint_amount` tokens to `exp_minter_address` -- Are deployed at the end of the contract deployment sequence -- Are saved to registry files as "ExpToken" and "Exp2Token" - -### Examples - -**Base Sepolia (Testnet)**: - -```toml -[base-sepolia.bool] -is_testnet = true - -[base-sepolia.address] -exp_minter_address = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" - -[base-sepolia.uint] -exp_mint_amount = "5000000000000000000000" - -[base-sepolia.string] -contracts = ["ALL"] # Deploys 8 core contracts + ExpToken -``` - -## Supporting Bash Scripts - -### Overview - -There are two main scripts that handle multi-chain deployments and configuration verification: - -- **`deploy/execute_config.sh`** - Brings up the whole environment, by calling all scripts correctly. -- **`deploy/verify_config.sh`** - Verifies that the values in the config.toml are all set and configured correctly. - -### Usage - -#### Execute Deployment - -```bash -# Deploy to specific chains -./deploy/execute_config.sh 84532 11155420 - -# Deploy to all chains in config.toml -./deploy/execute_config.sh -``` - -The script performs these steps: -1. Validates configuration for selected chains -2. Deploys core contracts (IthacaAccount, SimpleFunder, SimpleSettler, etc.) -3. Configures LayerZero cross-chain messaging -4. Funds signer accounts with gas tokens -5. Verifies all deployments match configuration - -#### Verify Configuration +**Purpose**: Deploy a simple test ERC20 token for testing. +**Usage**: ```bash -# Verify specific chains -./deploy/verify_config.sh 84532 11155420 - -# Verify all chains in config.toml -./deploy/verify_config.sh +# Deploy test token to a specific chain +forge script deploy/DeployEXP.s.sol:DeployEXP \ + --broadcast \ + --sig "run()" \ + --private-key $PRIVATE_KEY \ + --rpc-url $RPC_ ``` -The script checks: -- Required environment variables (RPC URLs, private keys) -- Contract deployment addresses match config.toml -- Signer accounts have sufficient gas balances -- LayerZero endpoints and DVN configurations -- Cross-chain pathway configurations - -### Configuration - -Both scripts read from `deploy/config.toml` which defines per-chain: -- RPC endpoints and chain metadata -- Contract addresses and owners -- LayerZero endpoint configurations -- Cross-chain destination mappings -- Gas funding amounts - -Environment variables required: -- `PRIVATE_KEY` - Deployer private key -- `GAS_SIGNER_MNEMONIC` - Mnemonic for signer accounts -- `RPC_` - RPC URL for each chain (e.g., `RPC_84532`) \ No newline at end of file +The test token includes basic ERC20 functionality and can be minted by anyone. +It should only be used for testing purposes. \ No newline at end of file diff --git a/deploy/config.toml b/deploy/config.toml index 32d4fd67..0644abd6 100644 --- a/deploy/config.toml +++ b/deploy/config.toml @@ -1,155 +1,136 @@ -[11155111] -endpoint_url = "${RPC_11155111}" +# Ithaca Account Deployment Configuration +# Single source of truth for all deployment and fork configurations +# This file extends foundry.toml and contains both Foundry fork configs and deployment-specific settings -[11155111.bool] -is_testnet = true +# ============================================ +# GLOBAL DEPLOYMENT CONFIGURATION +# ============================================ + +[profile.deployment] +registry_path = "deploy/registry/" + +# ============================================ +# FORK CONFIGURATIONS (for vm.readFork* cheatcodes) +# ============================================ -[11155111.address] +# Sepolia +[forks.sepolia] +rpc_url = "${RPC_11155111}" + +[forks.sepolia.vars] +chain_id = 11155111 +name = "Sepolia" +is_testnet = true +pause_authority = "0x0000000000000000000000000000000000000001" funder_owner = "0x0000000000000000000000000000000000000003" funder_signer = "0x0000000000000000000000000000000000000002" settler_owner = "0x0000000000000000000000000000000000000004" l0_settler_owner = "0x0000000000000000000000000000000000000005" -l0_settler_signer = "0x0000000000000000000000000000000000000006" layerzero_endpoint = "0x6EDCE65403992e310A62460808c4b910D972f10f" +layerzero_eid = 40161 +salt = "0x0000000000000000000000000000000000000000000000000000000000000000" +target_balance = 50000000000000000 # 0.05 ether +# Specify contracts to deploy. Use ["ALL"] to deploy all available contracts +contracts = ["LayerZeroSettler"] +# SimpleFunder configuration +simple_funder_address = "0x2AD8F6a3bB1126a777606eaFa9da9b95530d9597" # TODO: automate write when foundry upgrades +default_num_signers = 10 +# LayerZero configuration (copied from Base Sepolia for testing) +# TODO: Set correct addresses here. layerzero_settler_address = "0x4225041FF3DB1C7d7a1029406bB80C7298767aca" layerzero_send_uln302 = "0xC1868e054425D378095A003EcbA3823a5D0135C9" layerzero_receive_uln302 = "0x12523de19dc41c91F7d2093E0CFbB76b17012C8d" -dvn_layerzero_labs = "0x8eebf8b423B73bFCa51a1Db4B7354AA0bFCA9193" -dvn_google_cloud = "0x8e5a7b5959C5A7C732b87dCE401E07F5819eEC2d" -exp_minter_address = "0x0000000000000000000000000000000000000003" -supported_orchestrators = ["0xEd7c1e839381c489Dcd1ED3CE1B0e79DaE714f77"] -orchestrator_deployed = "0xCa58a1296c5AE231A2963A26E11f722bfe65acd4" -ithaca_account_deployed = "0xC203A827A6DB2d09fbF052f88e7aD794EEbf8928" -account_proxy_deployed = "0x0B5d691F3A98B705756ab196CFe4af97f35e06E4" -simulator_deployed = "0x3306F2DD87887885f4de81aF6BD88b648E467f4e" -simple_funder_deployed = "0xa0BF9a30fD55A45BF09309513734B8194fBE3934" -escrow_deployed = "0x669a3aB5816d1ADdbFdecE2F6B238D323F056248" -simple_settler_deployed = "0xFB0F29c658145DbDb1Bf974C8A342F4E78BF9801" -layerzero_settler_deployed = "0xC93D0142D960F7B33440F09B13917C195e09ffA0" -exp_token_deployed = "0x4D335fa317FB8d5493b83e06161134ec611Bc188" -exp2_token_deployed = "0x0348152303686f2197b24981571FCf8c36c66Cc5" - -[11155111.uint] -chain_id = 11155111 -layerzero_eid = 40161 -target_balance = "50000000000000000" -default_num_signers = 10 +layerzero_destination_chain_ids = [84532, 11155420] +layerzero_required_dvns = ["dvn_layerzero_labs"] # optimism sepolia +layerzero_optional_dvns = [] +layerzero_optional_dvn_threshold = 0 layerzero_confirmations = 1 layerzero_max_message_size = 10000 -layerzero_optional_dvn_threshold = 0 -exp_mint_amount = "1000000000000000000000" -layerzero_destination_chain_ids = [ - 84532, - 11155420, -] - -[11155111.bytes32] -salt = "0xdf08a9fa957a9ac004f84f3a8ab7318a7be55d59948f46497bd3717865295b61" - -[11155111.string] -name = "Sepolia" -contracts = ["ALL"] -layerzero_required_dvns = ["dvn_layerzero_labs"] -layerzero_optional_dvns = [] +# DVN addresses +dvn_layerzero_labs = "0x8eebf8b423B73bFCa51a1Db4B7354AA0bFCA9193" +dvn_google_cloud = "0x8e5a7b5959C5A7C732b87dCE401E07F5819eEC2d" -[84532] -endpoint_url = "${RPC_84532}" +# Base Sepolia +[forks.base-sepolia] +rpc_url = "${RPC_84532}" -[84532.bool] +[forks.base-sepolia.vars] +orchestrator_address = "0x0000000000000000000000000000000000000000" +# Deployment Script +chain_id = 84532 +name = "Base Sepolia" is_testnet = true - -[84532.address] -funder_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" -funder_signer = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" -settler_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" -l0_settler_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" -l0_settler_signer = "0x0000000000000000000000000000000000000006" -layerzero_endpoint = "0x6EDCE65403992e310A62460808c4b910D972f10f" -layerzero_settler_address = "0xd71d3c3ff2249A67cEa12030b20E66734fB1f833" +pause_authority = "0x0000000000000000000000000000000000000001" # Set to separate PAUSE KEY +funder_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" # Set to Master Key +funder_signer = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" # Set to Master Key +settler_owner = "0x0000000000000000000000000000000000000004" # Set to Master Key +l0_settler_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" # Set to Master Key +layerzero_endpoint = "0x6EDCE65403992e310A62460808c4b910D972f10f" # Get from LayerZero docs +layerzero_eid = 40245 # Get from LayerZero docs +salt = "0x00000000000000000000000000000000000000000000000000000000000000c7" # Almost alwayse be bytes32(0) +# Specify contracts to deploy. Use ["ALL"] to deploy all available contracts +contracts = ["ALL"] +# Funding Script +target_balance = 1000000000000000 # 0.001 ether + +# SimpleFunder configuration +simple_funder_address = "0x5e6c8c24A53275e2C6C3DC5f1c5C027Ec13439Ba" +default_num_signers = 10 +# LayerZero configuration +layerzero_settler_address = "0x4225041FF3DB1C7d7a1029406bB80C7298767aca" layerzero_send_uln302 = "0xC1868e054425D378095A003EcbA3823a5D0135C9" layerzero_receive_uln302 = "0x12523de19dc41c91F7d2093E0CFbB76b17012C8d" -dvn_layerzero_labs = "0xe1a12515F9AB2764b887bF60B923Ca494EBbB2d6" -dvn_google_cloud = "0xFc9d8E5d3FaB22fB6E93E9E2C90916E9dCa83Ade" -exp_minter_address = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" -supported_orchestrators = ["0xEd7c1e839381c489Dcd1ED3CE1B0e79DaE714f77"] -orchestrator_deployed = "0xcA91Dba3dab46478FC466ef76491AaB78dbC2d0f" -ithaca_account_deployed = "0x1E7a350c76CCd750470930B5026fEa26F44cf937" -account_proxy_deployed = "0xDe938C3642205782d52535d12eC9dFb4D1bC832b" -simulator_deployed = "0xb100Dc6e1BA9e54965fBB865F65Ac42b28BC25E8" -simple_funder_deployed = "0x57C3563C06CeDda15F7622A83a1BdBa84125351D" -escrow_deployed = "0xCd075ceb5Cd463a9233a8085fc915767139F655c" -simple_settler_deployed = "0x5aE547Dc236c31d30f19a5B9040992728Ac441A0" -layerzero_settler_deployed = "0x23D18C5b5Df22fDcf6dDE73b0f1a76ee5761d206" -exp_token_deployed = "0xa8071DA5e994cB8e3eB56CaD0FBB6ca424dD8dc0" -exp2_token_deployed = "0x61727778216127D0843A99A3e91e99C27e9f3BC7" - -[84532.uint] -chain_id = 84532 -layerzero_eid = 40245 -target_balance = "1000000000000000" -default_num_signers = 10 -layerzero_confirmations = 1 -layerzero_max_message_size = 10000 -layerzero_optional_dvn_threshold = 0 -exp_mint_amount = "5000000000000000000000" -layerzero_destination_chain_ids = [11155420] - -[84532.bytes32] -salt = "0xdf08a9fa957a9ac004f84f3a8ab7318a7be55d59948f46497bd3717865295b61" - -[84532.string] -name = "Base Sepolia" -contracts = ["ALL"] +layerzero_destination_chain_ids = [11155420] # optimism sepolia layerzero_required_dvns = ["dvn_layerzero_labs"] layerzero_optional_dvns = [] +layerzero_optional_dvn_threshold = 0 +layerzero_confirmations = 1 +layerzero_max_message_size = 10000 +# DVN addresses +dvn_layerzero_labs = "0xe1a12515F9AB2764b887bF60B923Ca494EBbB2d6" +dvn_google_cloud = "0xFc9d8E5d3FaB22fB6E93E9E2C90916E9dCa83Ade" -[11155420] -endpoint_url = "${RPC_11155420}" +# Optimism Sepolia +[forks.optimism-sepolia] +rpc_url = "${RPC_11155420}" -[11155420.bool] +[forks.optimism-sepolia.vars] +chain_id = 11155420 +name = "Optimism Sepolia" is_testnet = true - -[11155420.address] -funder_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" -funder_signer = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" -settler_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" +pause_authority = "0x0000000000000000000000000000000000000001" +funder_owner = "0x0000000000000000000000000000000000000003" +funder_signer = "0x0000000000000000000000000000000000000002" +settler_owner = "0x0000000000000000000000000000000000000004" l0_settler_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" -l0_settler_signer = "0x0000000000000000000000000000000000000006" layerzero_endpoint = "0x6EDCE65403992e310A62460808c4b910D972f10f" -layerzero_settler_address = "0x9F610182BD096ab7734C1365A44f48dD0A574d0D" +layerzero_eid = 40232 +salt = "0x0000000000000000000000000000000000000000000000000000000000000001" +target_balance = 10000000000000000 # 0.01 ether +# Specify contracts to deploy. Use ["ALL"] to deploy all available contracts +contracts = ["ALL"] # Deploy all contracts +# SimpleFunder configuration +simple_funder_address = "0x2AD8F6a3bB1126a777606eaFa9da9b95530d9597" + +default_num_signers = 10 +# LayerZero configuration +layerzero_settler_address = "0x4225041FF3DB1C7d7a1029406bB80C7298767aca" layerzero_send_uln302 = "0xB31D2cb502E25B30C651842C7C3293c51Fe6d16f" layerzero_receive_uln302 = "0x9284fd59B95b9143AF0b9795CAC16eb3C723C9Ca" +layerzero_destination_chain_ids = [84532, 11155111] +layerzero_required_dvns = ["dvn_layerzero_labs"] +layerzero_optional_dvns = [] +layerzero_optional_dvn_threshold = 0 +layerzero_confirmations = 1 +layerzero_max_message_size = 10000 +# DVN addresses dvn_layerzero_labs = "0x28B6140ead70cb2Fb669705b3598ffB4BEaA060b" dvn_google_cloud = "0x28b8B8Ea5C695EFa657B6b86a4a8E90ccbA93E9e" -exp_minter_address = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" -supported_orchestrators = ["0xEd7c1e839381c489Dcd1ED3CE1B0e79DaE714f77"] -orchestrator_deployed = "0xcA91Dba3dab46478FC466ef76491AaB78dbC2d0f" -ithaca_account_deployed = "0x1E7a350c76CCd750470930B5026fEa26F44cf937" -account_proxy_deployed = "0xDe938C3642205782d52535d12eC9dFb4D1bC832b" -simulator_deployed = "0xb100Dc6e1BA9e54965fBB865F65Ac42b28BC25E8" -simple_funder_deployed = "0x57C3563C06CeDda15F7622A83a1BdBa84125351D" -escrow_deployed = "0xCd075ceb5Cd463a9233a8085fc915767139F655c" -simple_settler_deployed = "0x5aE547Dc236c31d30f19a5B9040992728Ac441A0" -layerzero_settler_deployed = "0x23D18C5b5Df22fDcf6dDE73b0f1a76ee5761d206" -exp_token_deployed = "0xa8071DA5e994cB8e3eB56CaD0FBB6ca424dD8dc0" -exp2_token_deployed = "0x61727778216127D0843A99A3e91e99C27e9f3BC7" -[11155420.uint] -chain_id = 11155420 -layerzero_eid = 40232 -target_balance = "1000000000000000" -default_num_signers = 10 -layerzero_confirmations = 1 -layerzero_max_message_size = 10000 -layerzero_optional_dvn_threshold = 0 -exp_mint_amount = "2000000000000000000000" -layerzero_destination_chain_ids = [84532] -[11155420.bytes32] -salt = "0xdf08a9fa957a9ac004f84f3a8ab7318a7be55d59948f46497bd3717865295b61" -[11155420.string] -name = "Optimism Sepolia" -contracts = ["ALL"] -layerzero_required_dvns = ["dvn_layerzero_labs"] -layerzero_optional_dvns = [] +# Master Key has gas funds on all chains. +# 1. Deployment Key ( Master Key ) +# 2. Funder Owner Key ( Master Key ) +# 3. Gas Signers ( Funding is included in the fund signers script + setting them as gas wallets ) +# 4. Simple Funder Address ( Funds need to come from GK after bridging, no automation ) diff --git a/deploy/registry/deployment_11155420_0x0000000000000000000000000000000000000000000000000000000000000001.json b/deploy/registry/deployment_11155420_0x0000000000000000000000000000000000000000000000000000000000000001.json new file mode 100644 index 00000000..23cb5e8b --- /dev/null +++ b/deploy/registry/deployment_11155420_0x0000000000000000000000000000000000000000000000000000000000000001.json @@ -0,0 +1 @@ +{"Orchestrator": "0xb33adF2c2257a94314d408255aC843fd53B1a7e1","IthacaAccount": "0x5a87ef243CDA70a855828d4989Fad61B56A467d3","AccountProxy": "0x4ACD713815fbb363a89D9Ff046C56cEdC7EF3ad7","Simulator": "0x65Ae218EB1987b8bd0F9eeb38D1B344726D41dA5","SimpleFunder": "0xA47C5C472449979a2F37dF2971627cD6587bADb8","Escrow": "0x24F50280cE3B51Ab1967F048746FB7ba3C7B4067","SimpleSettler": "0xb934afBB50b8aBBe24959f9398fE024BEe9Bf716","LayerZeroSettler": "0xB89f4A85d38C3A2407854269527fabD3b61fd56a"} \ No newline at end of file diff --git a/deploy/registry/deployment_84532_0x0000000000000000000000000000000000000000000000000000000000000001.json b/deploy/registry/deployment_84532_0x0000000000000000000000000000000000000000000000000000000000000001.json new file mode 100644 index 00000000..c37d42b4 --- /dev/null +++ b/deploy/registry/deployment_84532_0x0000000000000000000000000000000000000000000000000000000000000001.json @@ -0,0 +1,10 @@ +{ + "Orchestrator": "0xb33adF2c2257a94314d408255aC843fd53B1a7e1", + "IthacaAccount": "0x5a87ef243CDA70a855828d4989Fad61B56A467d3", + "AccountProxy": "0x4ACD713815fbb363a89D9Ff046C56cEdC7EF3ad7", + "Simulator": "0x65Ae218EB1987b8bd0F9eeb38D1B344726D41dA5", + "SimpleFunder": "0xA47C5C472449979a2F37dF2971627cD6587bADb8", + "Escrow": "0x24F50280cE3B51Ab1967F048746FB7ba3C7B4067", + "SimpleSettler": "0xb934afBB50b8aBBe24959f9398fE024BEe9Bf716", + "LayerZeroSettler": "0xB89f4A85d38C3A2407854269527fabD3b61fd56a" +} diff --git a/docs/4337CallGraph.md b/docs/4337CallGraph.md deleted file mode 100644 index ec9e788d..00000000 --- a/docs/4337CallGraph.md +++ /dev/null @@ -1,32 +0,0 @@ -sequenceDiagram - participant User as User - participant Bundler as Bundler - participant EntryPoint as EntryPoint - participant Paymaster as Paymaster - participant Account as ERC4337 Account - - User->>Bundler: Submit signed UserOperation + paymasterAndData - Bundler->>EntryPoint: handleOps([userOp]) - - Note over EntryPoint: Validation Phase - EntryPoint->>Account: validateUserOp(userOp, userOpHash, missingAccountFunds) - Account-->>EntryPoint: validationData - - Note over EntryPoint: Paymaster validation & deposit check - EntryPoint->>Paymaster: validatePaymasterUserOp(userOp, userOpHash, maxCost) - Note over Paymaster: Validate user agreed to pay in USDC - Paymaster-->>EntryPoint: (context, validationData) - - - Note over EntryPoint: Execution Phase - EntryPoint->>Account: execute(userOp.callData) - - Note over EntryPoint: Payment Phase - ETH movement - EntryPoint-->>Bundler: ETH transfer (actualGasCost) - - EntryPoint->>Paymaster: postOp(mode, context, actualGasCost) - Note over Paymaster: Calculate USDC amount needed - Account-->>Paymaster: USDC transfer (calculated amount) - - EntryPoint-->>Bundler: Execution result - Bundler-->>User: UserOp result \ No newline at end of file diff --git a/docs/IthacaCallGraph.md b/docs/IthacaCallGraph.md deleted file mode 100644 index 7c35733c..00000000 --- a/docs/IthacaCallGraph.md +++ /dev/null @@ -1,25 +0,0 @@ -sequenceDiagram - participant User as User - participant Relayer as Relayer - participant Orch as Orchestrator - participant Account as IthacaAccount - - User->>Relayer: Submit signed intent - Relayer->>Orch: execute(encodedIntent) - - Note over Orch: Verify signature - Orch->>Account: unwrapAndValidateSignature(digest, signature) - Account-->>Orch: (isValid, keyHash) - - Note over Orch: Increment nonce - Orch->>Account: checkAndIncrementNonce(nonce) - - Note over Orch: Process payment - Orch->>Account: pay(paymentAmount, keyHash, digest, intent) - Account-->>Relayer: ERC20 transfer (paymentAmount) - - Note over Orch: Execute intent - Orch->>Account: execute(executionData) - - Orch-->>Relayer: Return success/error code - Relayer-->>User: Execution result \ No newline at end of file diff --git a/docs/benchmarks.jpeg b/docs/benchmarks.jpeg deleted file mode 100644 index 43c3a861ff2d95bd0346c92a384d144e32b149ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 291106 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
;oEQ?(kgNh9_JBkCduJ6BD~-@PrP*^y;Psm^ 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

bO!3Y`Y|3U%=Ww#mitDQA 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 + + + + + \ No newline at end of file diff --git a/lib/.idea/lib.iml b/lib/.idea/lib.iml new file mode 100644 index 00000000..d6ebd480 --- /dev/null +++ b/lib/.idea/lib.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/lib/.idea/misc.xml b/lib/.idea/misc.xml new file mode 100644 index 00000000..862d09bd --- /dev/null +++ b/lib/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/lib/.idea/modules.xml b/lib/.idea/modules.xml new file mode 100644 index 00000000..b0c4ff69 --- /dev/null +++ b/lib/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/lib/.idea/vcs.xml b/lib/.idea/vcs.xml new file mode 100644 index 00000000..d3a8bd59 --- /dev/null +++ b/lib/.idea/vcs.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/solady b/lib/solady index acd959aa..90db92ce 160000 --- a/lib/solady +++ b/lib/solady @@ -1 +1 @@ -Subproject commit acd959aa4bd04720d640bf4e6a5c71037510cc4b +Subproject commit 90db92ce173856605d24a554969f2c67cadbc7e9 diff --git a/snapshots/BenchmarkTest.json b/snapshots/BenchmarkTest.json index 82f7d2ae..dedb663c 100644 --- a/snapshots/BenchmarkTest.json +++ b/snapshots/BenchmarkTest.json @@ -1,53 +1,53 @@ { - "testERC20Transfer_AlchemyModularAccount": "179494", - "testERC20Transfer_AlchemyModularAccount_AppSponsor": "176436", - "testERC20Transfer_AlchemyModularAccount_ERC20SelfPay": "207779", - "testERC20Transfer_Batch100_AlchemyModularAccount_AppSponsor": "8897167", - "testERC20Transfer_Batch100_CoinbaseSmartWallet": "9952203", - "testERC20Transfer_Batch100_CoinbaseSmartWallet_AppSponsor": "8787327", - "testERC20Transfer_Batch100_CoinbaseSmartWallet_ERC20SelfPay": "11335605", - "testERC20Transfer_Batch100_Safe4337": "11685484", - "testERC20Transfer_Batch100_Safe4337_AppSponsor": "10198375", - "testERC20Transfer_Batch100_Safe4337_ERC20SelfPay": "12757523", - "testERC20Transfer_Batch100_ZerodevKernel_AppSponsor": "11427583", - "testERC20Transfer_CoinbaseSmartWallet": "177855", - "testERC20Transfer_CoinbaseSmartWallet_AppSponsor": "175259", - "testERC20Transfer_CoinbaseSmartWallet_ERC20SelfPay": "204919", - "testERC20Transfer_ERC4337MinimalAccount": "171509", - "testERC20Transfer_ERC4337MinimalAccount_AppSponsor": "168500", - "testERC20Transfer_ERC4337MinimalAccount_ERC20SelfPay": "199831", - "testERC20Transfer_IthacaAccount": "128195", - "testERC20Transfer_IthacaAccountWithSpendLimits": "193658", - "testERC20Transfer_IthacaAccount_AppSponsor": "138709", - "testERC20Transfer_IthacaAccount_AppSponsor_ERC20": "144010", - "testERC20Transfer_IthacaAccount_ERC20SelfPay": "128684", - "testERC20Transfer_Safe4337": "197561", - "testERC20Transfer_Safe4337_AppSponsor": "191725", - "testERC20Transfer_Safe4337_ERC20SelfPay": "221464", - "testERC20Transfer_ZerodevKernel": "207117", - "testERC20Transfer_ZerodevKernel_AppSponsor": "204120", - "testERC20Transfer_ZerodevKernel_ERC20SelfPay": "235489", - "testERC20Transfer_batch100_AlchemyModularAccount": "10109066", - "testERC20Transfer_batch100_AlchemyModularAccount_ERC20SelfPay": "11609298", - "testERC20Transfer_batch100_IthacaAccount": "7545392", - "testERC20Transfer_batch100_IthacaAccount_AppSponsor": "8151496", - "testERC20Transfer_batch100_IthacaAccount_AppSponsor_ERC20": "7977508", - "testERC20Transfer_batch100_IthacaAccount_ERC20SelfPay": "7366652", - "testERC20Transfer_batch100_ZerodevKernel": "12631318", - "testERC20Transfer_batch100_ZerodevKernel_ERC20SelfPay": "14149937", - "testNativeTransfer_AlchemyModularAccount": "180829", - "testNativeTransfer_CoinbaseSmartWallet": "178916", - "testNativeTransfer_IthacaAccount": "129551", - "testNativeTransfer_IthacaAccount_AppSponsor": "140096", - "testNativeTransfer_IthacaAccount_ERC20SelfPay": "137340", - "testNativeTransfer_Safe4337": "198595", - "testNativeTransfer_ZerodevKernel": "208635", - "testUniswapV2Swap_AlchemyModularAccount": "238647", - "testUniswapV2Swap_CoinbaseSmartWallet": "237451", - "testUniswapV2Swap_ERC4337MinimalAccount": "230691", - "testUniswapV2Swap_IthacaAccount": "187339", - "testUniswapV2Swap_IthacaAccount_AppSponsor": "197817", - "testUniswapV2Swap_IthacaAccount_ERC20SelfPay": "192628", - "testUniswapV2Swap_Safe4337": "257333", - "testUniswapV2Swap_ZerodevKernel": "266367" + "testERC20Transfer_AlchemyModularAccount": "159052", + "testERC20Transfer_AlchemyModularAccount_AppSponsor": "134278", + "testERC20Transfer_AlchemyModularAccount_ERC20SelfPay": "160169", + "testERC20Transfer_Batch100_AlchemyModularAccount_AppSponsor": "7053863", + "testERC20Transfer_Batch100_CoinbaseSmartWallet": "9801043", + "testERC20Transfer_Batch100_CoinbaseSmartWallet_AppSponsor": "6483523", + "testERC20Transfer_Batch100_CoinbaseSmartWallet_ERC20SelfPay": "8487729", + "testERC20Transfer_Batch100_Safe4337": "11165400", + "testERC20Transfer_Batch100_Safe4337_AppSponsor": "7525827", + "testERC20Transfer_Batch100_Safe4337_ERC20SelfPay": "9540939", + "testERC20Transfer_Batch100_ZerodevKernel_AppSponsor": "8956539", + "testERC20Transfer_CoinbaseSmartWallet": "150133", + "testERC20Transfer_CoinbaseSmartWallet_AppSponsor": "126009", + "testERC20Transfer_CoinbaseSmartWallet_ERC20SelfPay": "150241", + "testERC20Transfer_ERC4337MinimalAccount": "148602", + "testERC20Transfer_ERC4337MinimalAccount_AppSponsor": "123885", + "testERC20Transfer_ERC4337MinimalAccount_ERC20SelfPay": "149776", + "testERC20Transfer_IthacaAccount": "91666", + "testERC20Transfer_IthacaAccountWithSpendLimits": "113167", + "testERC20Transfer_IthacaAccount_AppSponsor": "99218", + "testERC20Transfer_IthacaAccount_AppSponsor_ERC20": "104319", + "testERC20Transfer_IthacaAccount_ERC20SelfPay": "91967", + "testERC20Transfer_Safe4337": "163675", + "testERC20Transfer_Safe4337_AppSponsor": "136323", + "testERC20Transfer_Safe4337_ERC20SelfPay": "160610", + "testERC20Transfer_ZerodevKernel": "175447", + "testERC20Transfer_ZerodevKernel_AppSponsor": "150734", + "testERC20Transfer_ZerodevKernel_ERC20SelfPay": "176663", + "testERC20Transfer_batch100_AlchemyModularAccount": "10438358", + "testERC20Transfer_batch100_AlchemyModularAccount_ERC20SelfPay": "9221814", + "testERC20Transfer_batch100_IthacaAccount": "6201928", + "testERC20Transfer_batch100_IthacaAccount_AppSponsor": "6758228", + "testERC20Transfer_batch100_IthacaAccount_AppSponsor_ERC20": "6565428", + "testERC20Transfer_batch100_IthacaAccount_ERC20SelfPay": "6004328", + "testERC20Transfer_batch100_ZerodevKernel": "12332690", + "testERC20Transfer_batch100_ZerodevKernel_ERC20SelfPay": "11134785", + "testNativeTransfer_AlchemyModularAccount": "168453", + "testNativeTransfer_CoinbaseSmartWallet": "159248", + "testNativeTransfer_IthacaAccount": "101064", + "testNativeTransfer_IthacaAccount_AppSponsor": "108635", + "testNativeTransfer_IthacaAccount_ERC20SelfPay": "101365", + "testNativeTransfer_Safe4337": "172763", + "testNativeTransfer_ZerodevKernel": "184855", + "testUniswapV2Swap_AlchemyModularAccount": "210111", + "testUniswapV2Swap_CoinbaseSmartWallet": "201623", + "testUniswapV2Swap_ERC4337MinimalAccount": "199690", + "testUniswapV2Swap_IthacaAccount": "142728", + "testUniswapV2Swap_IthacaAccount_AppSponsor": "150244", + "testUniswapV2Swap_IthacaAccount_ERC20SelfPay": "143029", + "testUniswapV2Swap_Safe4337": "215353", + "testUniswapV2Swap_ZerodevKernel": "226591" } \ No newline at end of file diff --git a/src/Escrow.sol b/src/Escrow.sol index a924bc59..6138cab9 100644 --- a/src/Escrow.sol +++ b/src/Escrow.sol @@ -132,12 +132,12 @@ contract Escrow is IEscrow { for (uint256 i = 0; i < escrowIds.length; i++) { Escrow storage _escrow = escrows[escrowIds[i]]; // If refund timestamp hasn't passed yet, then the refund is invalid. - if (block.timestamp <= _escrow.refundTimestamp) { + if (block.timestamp > _escrow.refundTimestamp || msg.sender == _escrow.recipient) { + _refundDepositor(escrowIds[i], _escrow); + _refundRecipient(escrowIds[i], _escrow); + } else { revert RefundInvalid(); } - - _refundDepositor(escrowIds[i], _escrow); - _refundRecipient(escrowIds[i], _escrow); } } @@ -147,10 +147,11 @@ contract Escrow is IEscrow { for (uint256 i = 0; i < escrowIds.length; i++) { Escrow storage _escrow = escrows[escrowIds[i]]; // If refund timestamp hasn't passed yet, then the refund is invalid. - if (block.timestamp <= _escrow.refundTimestamp) { + if (block.timestamp > _escrow.refundTimestamp || msg.sender == _escrow.depositor) { + _refundDepositor(escrowIds[i], _escrow); + } else { revert RefundInvalid(); } - _refundDepositor(escrowIds[i], _escrow); } } @@ -181,11 +182,11 @@ contract Escrow is IEscrow { Escrow storage _escrow = escrows[escrowIds[i]]; // If settlement is still within the deadline, then refund is invalid. - if (block.timestamp <= _escrow.refundTimestamp) { + if (block.timestamp > _escrow.refundTimestamp || msg.sender == _escrow.recipient) { + _refundRecipient(escrowIds[i], _escrow); + } else { revert RefundInvalid(); } - - _refundRecipient(escrowIds[i], _escrow); } } diff --git a/src/GuardedExecutor.sol b/src/GuardedExecutor.sol index cf5eb502..4d655929 100644 --- a/src/GuardedExecutor.sol +++ b/src/GuardedExecutor.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; -import {ERC7821} from "solady/accounts/ERC7821.sol"; import {LibSort} from "solady/utils/LibSort.sol"; import {LibBytes} from "solady/utils/LibBytes.sol"; import {LibZip} from "solady/utils/LibZip.sol"; @@ -13,6 +12,7 @@ import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {FixedPointMathLib as Math} from "solady/utils/FixedPointMathLib.sol"; import {DateTimeLib} from "solady/utils/DateTimeLib.sol"; import {ICallChecker} from "./interfaces/ICallChecker.sol"; +import {ERC7821Ithaca as ERC7821} from "./libraries/ERC7821Ithaca.sol"; /// @title GuardedExecutor /// @notice Mixin for spend limits and calldata execution guards. @@ -401,7 +401,7 @@ abstract contract GuardedExecutor is ERC7821 { checkKeyHashIsNonZero(keyHash) { if (keyHash != ANY_KEYHASH) { - if (_isSuperAdmin(keyHash)) revert SuperAdminCanSpendAnything(); + if (_isSuperAdmin(keyHash)) revert SuperAdminCanExecuteEverything(); } // It is ok even if we don't check for `_isSelfExecute` here, as we will still @@ -698,10 +698,10 @@ abstract contract GuardedExecutor is ERC7821 { // Configurables //////////////////////////////////////////////////////////////////////// - /// @dev To be overriden to return if `keyHash` corresponds to a super admin key. + /// @dev To be overridden to return if `keyHash` corresponds to a super admin key. function _isSuperAdmin(bytes32 keyHash) internal view virtual returns (bool); - /// @dev To be overriden to return the storage slot seed for a `keyHash`. + /// @dev To be overridden to return the storage slot seed for a `keyHash`. function _getGuardedExecutorKeyStorageSeed(bytes32 keyHash) internal view diff --git a/src/IthacaAccount.sol b/src/IthacaAccount.sol index f770cabc..a8ffdbca 100644 --- a/src/IthacaAccount.sol +++ b/src/IthacaAccount.sol @@ -275,8 +275,8 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { (bool isValid, bytes32 keyHash) = unwrapAndValidateSignature(digest, signature); if (LibBit.and(keyHash != 0, isValid)) { - isValid = _isSuperAdmin(keyHash) - || _getKeyExtraStorage(keyHash).checkers.contains(msg.sender); + isValid = + _isSuperAdmin(keyHash) || _getKeyExtraStorage(keyHash).checkers.contains(msg.sender); } // `bytes4(keccak256("isValidSignature(bytes32,bytes)")) = 0x1626ba7e`. // We use `0xffffffff` for invalid, in convention with the reference implementation. @@ -400,7 +400,12 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { } /// @dev Returns arrays of all (non-expired) authorized keys and their hashes. - function getKeys() public view virtual returns (Key[] memory keys, bytes32[] memory keyHashes) { + function getKeys() + public + view + virtual + returns (Key[] memory keys, bytes32[] memory keyHashes) + { uint256 totalCount = keyCount(); keys = new Key[](totalCount); @@ -614,8 +619,9 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { // `keccak256(abi.encode(key.keyType, keccak256(key.publicKey)))`. keyHash = hash(key); AccountStorage storage $ = _getAccountStorage(); - $.keyStorage[keyHash] - .set(abi.encodePacked(key.publicKey, key.expiry, key.keyType, key.isSuperAdmin)); + $.keyStorage[keyHash].set( + abi.encodePacked(key.publicKey, key.expiry, key.keyType, key.isSuperAdmin) + ); $.keyHashes.add(keyHash); } @@ -663,10 +669,12 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { if or(shr(64, t), lt(encodedIntent.length, 0x20)) { revert(0x00, 0x00) } } - if (!LibBit.and( + if ( + !LibBit.and( msg.sender == ORCHESTRATOR, LibBit.or(intent.eoa == address(this), intent.payer == address(this)) - )) { + ) + ) { revert Unauthorized(); } diff --git a/src/MultiSigSigner.sol b/src/MultiSigSigner.sol index 73d82df9..6f6fb272 100644 --- a/src/MultiSigSigner.sol +++ b/src/MultiSigSigner.sol @@ -12,7 +12,7 @@ contract MultiSigSigner is ISigner { //////////////////////////////////////////////////////////////////////// /// @dev The magic value returned by `isValidSignatureWithKeyHash` when the signature is valid. - /// - Calcualated as: bytes4(keccak256("isValidSignatureWithKeyHash(bytes32,bytes32,bytes)") + /// - Calculated as: bytes4(keccak256("isValidSignatureWithKeyHash(bytes32,bytes32,bytes)") bytes4 internal constant _MAGIC_VALUE = 0x8afc93b4; /// @dev The magic value returned by `isValidSignatureWithKeyHash` when the signature is invalid. @@ -175,7 +175,7 @@ contract MultiSigSigner is ISigner { /// for each owner key hash in the config. /// - Signature of a multi-sig should be encoded as abi.encode(bytes[] memory ownerSignatures) /// - For efficiency, place the signatures in the same order as the ownerKeyHashes in the config. - /// - Failing owner signatures are ignored, as long as valid signaturs > threshold. + /// - Failing owner signatures are ignored, as long as valid signatures > threshold. function isValidSignatureWithKeyHash(bytes32 digest, bytes32 keyHash, bytes memory signature) public view diff --git a/src/Orchestrator.sol b/src/Orchestrator.sol index 06586bcc..72815450 100644 --- a/src/Orchestrator.sol +++ b/src/Orchestrator.sol @@ -20,6 +20,7 @@ import {ICommon} from "./interfaces/ICommon.sol"; import {IFunder} from "./interfaces/IFunder.sol"; import {ISettler} from "./interfaces/ISettler.sol"; import {MerkleProofLib} from "solady/utils/MerkleProofLib.sol"; +import {IntentHelpers} from "./libraries/IntentHelpers.sol"; /// @title Orchestrator /// @notice Enables atomic verification, gas compensation and execution across eoas. @@ -41,7 +42,13 @@ import {MerkleProofLib} from "solady/utils/MerkleProofLib.sol"; /// This means once an Intent is signed, it is infeasible to /// alter or rearrange it to force it to fail. -contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGuardTransient { +contract Orchestrator is + IOrchestrator, + EIP712, + CallContextChecker, + ReentrancyGuardTransient, + IntentHelpers +{ using LibERC7579 for bytes32[]; using EfficientHashLib for bytes32[]; using LibBitmap for LibBitmap.Bitmap; @@ -110,12 +117,12 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu /// @dev For EIP712 signature digest calculation for the `execute` function. bytes32 public constant INTENT_TYPEHASH = keccak256( - "Intent(bool multichain,address eoa,Call[] calls,uint256 nonce,address payer,address paymentToken,uint256 paymentMaxAmount,uint256 combinedGas,bytes[] encodedPreCalls,bytes[] encodedFundTransfers,address settler,uint256 expiry)Call(address to,uint256 value,bytes data)" + "Intent(Call[] calls,address eoa,uint256 nonce,address payer,address paymentToken,uint256 paymentMaxAmount,uint256 combinedGas,uint256 expiry)Call(address to,uint256 value,bytes data)" ); /// @dev For EIP712 signature digest calculation for SignedCalls bytes32 public constant SIGNED_CALL_TYPEHASH = keccak256( - "SignedCall(bool multichain,address eoa,Call[] calls,uint256 nonce)Call(address to,uint256 value,bytes data)" + "SignedCall(address eoa,Call[] calls,uint256 nonce)Call(address to,uint256 value,bytes data)" ); /// @dev For EIP712 signature digest calculation for the `execute` function. @@ -127,6 +134,10 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu /// This constant is a pun for "chain ID 0". uint16 public constant MULTICHAIN_NONCE_PREFIX = 0xc1d0; + /// @dev Nonce prefix to signal that the payload should use merkle verification. + /// This constant is "mv" in hex. + uint16 public constant MERKLE_VERIFICATION = 0x6D76; + /// @dev For ensuring that the remaining gas is sufficient for a self-call with /// overhead for cleaning up after the self-call. This also has an added benefit /// of preventing the censorship vector of calling `execute` in a very deep call-stack. @@ -195,7 +206,7 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu /// If sufficient gas is provided, returns an error selector that is non-zero /// if there is an error during the payment, verification, and call execution. function execute(bytes calldata encodedIntent) - public + external payable virtual nonReentrant @@ -211,7 +222,6 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu public payable virtual - nonReentrant returns (bytes4[] memory errs) { // This allocation and loop was initially in assembly, but I've normified it for now. @@ -220,7 +230,7 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu // We reluctantly use regular Solidity to access `encodedIntents[i]`. // This generates an unnecessary check for `i < encodedIntents.length`, but helps // generate all the implicit calldata bound checks on `encodedIntents[i]`. - (, errs[i]) = _execute(encodedIntents[i], 0, _NORMAL_MODE_FLAG); + errs[i] = this.execute(encodedIntents[i]); } } @@ -232,11 +242,16 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu /// But the balance of tx.origin has to be greater than or equal to type(uint192).max, to prove that a state override has been made offchain, /// and this is not an onchain call. This mode has been added so that receipt logs can be generated for `eth_simulateV1` /// @return gasUsed The amount of gas used by the execution. (Only returned if `isStateOverride` is true) - function simulateExecute( - bool isStateOverride, - uint256 combinedGasOverride, - bytes calldata encodedIntent - ) external payable returns (uint256) { + function simulateExecute(bytes calldata encodedIntent) external payable returns (uint256) { + bool isStateOverride; + uint256 combinedGasOverride; + assembly ("memory-safe") { + let endOfBytes := add(encodedIntent.offset, encodedIntent.length) + isStateOverride := calldataload(sub(endOfBytes, 0x40)) + combinedGasOverride := calldataload(sub(endOfBytes, 0x20)) + encodedIntent.length := sub(encodedIntent.length, 0x40) + } + // If Simulation Fails, then it will revert here. (uint256 gUsed, bytes4 err) = _execute(encodedIntent, combinedGasOverride, _SIMULATION_MODE_FLAG); @@ -260,33 +275,7 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu } } - /// @dev Extracts the Intent from the calldata bytes, with minimal checks. - function _extractIntent(bytes calldata encodedIntent) - internal - view - virtual - returns (Intent calldata i) - { - // This function does NOT allocate memory to avoid quadratic memory expansion costs. - // Otherwise, it will be unfair to the Intents at the back of the batch. - - // `dynamicStructInCalldata` internally performs out-of-bounds checks. - bytes calldata intentCalldata = LibBytes.dynamicStructInCalldata(encodedIntent, 0x00); - assembly ("memory-safe") { - i := intentCalldata.offset - } - // These checks are included for more safety: Swiss Cheese Model. - // Ensures that all the dynamic children in `encodedIntent` are contained. - LibBytes.checkInCalldata(i.executionData, intentCalldata); - LibBytes.checkInCalldata(i.encodedPreCalls, intentCalldata); - LibBytes.checkInCalldata(i.encodedFundTransfers, intentCalldata); - LibBytes.checkInCalldata(i.funderSignature, intentCalldata); - LibBytes.checkInCalldata(i.settlerContext, intentCalldata); - LibBytes.checkInCalldata(i.signature, intentCalldata); - LibBytes.checkInCalldata(i.paymentSignature, intentCalldata); - } /// @dev Extracts the PreCall from the calldata bytes, with minimal checks. - function _extractPreCall(bytes calldata encodedPreCall) internal virtual @@ -303,7 +292,7 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu /// @dev Executes a single encoded intent. /// @dev If flags is non-zero, then all errors are bubbled up. /// Currently there can only be 2 modes - simulation mode, and execution mode. - /// But we use a uint256 for efficient stack operations, and more flexiblity in the future. + /// But we use a uint256 for efficient stack operations, and more flexibility in the future. /// Note: We keep the flags in the stack/memory (TSTORE doesn't work) to make sure they are reset in each new call context, /// to provide protection against attacks which could spoof the execute function to believe it is in simulation mode. function _execute(bytes calldata encodedIntent, uint256 combinedGasOverride, uint256 flags) @@ -311,12 +300,11 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu virtual returns (uint256 gUsed, bytes4 err) { - Intent calldata i = _extractIntent(encodedIntent); - - uint256 g = Math.coalesce(uint96(combinedGasOverride), i.combinedGas); + uint256 g = Math.coalesce(uint96(combinedGasOverride), _getCombinedGas()); uint256 gStart = gasleft(); + address eoa = _getEoa(); - if (i.paymentAmount > i.paymentMaxAmount) { + if (_getPaymentAmount() > _getPaymentMaxAmount()) { err = PaymentError.selector; if (flags == _SIMULATION_MODE_FLAG) { @@ -328,15 +316,20 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu // Check if there's sufficient gas left for the gas-limited self calls // via the 63/64 rule. This is for gas estimation. If the total amount of gas // for the whole transaction is insufficient, revert. - if (((gasleft() * 63) >> 6) < Math.saturatingAdd(g, _INNER_GAS_OVERHEAD)) { + uint256 gasAvailable = (gasleft() * 63) >> 6; + uint256 gasRequired = Math.saturatingAdd(g, _INNER_GAS_OVERHEAD); + + if (gasAvailable < gasRequired) { if (flags != _SIMULATION_MODE_FLAG) { revert InsufficientGas(); } } } - if (i.supportedAccountImplementation != address(0)) { - if (accountImplementationOf(i.eoa) != i.supportedAccountImplementation) { + address accountImpl = _getSupportedAccountImplementation(); + if (accountImpl != address(0)) { + address currentImpl = accountImplementationOf(eoa); + if (currentImpl != accountImpl) { err = UnsupportedAccountImplementation.selector; if (flags == _SIMULATION_MODE_FLAG) { revert UnsupportedAccountImplementation(); @@ -344,18 +337,16 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu } } - address payer = Math.coalesce(i.payer, i.eoa); + address payer = Math.coalesce(_getPayer(), eoa); // Early skip the entire pay-verify-call workflow if the payer lacks tokens, // so that less gas is wasted when the Intent fails. // For multi chain mode, we skip this check, as the funding happens inside the self call. - if (!i.isMultichain && LibBit.and(i.paymentAmount != 0, err == 0)) { - if (TokenTransferLib.balanceOf(i.paymentToken, payer) < i.paymentAmount) { - err = PaymentError.selector; + if (TokenTransferLib.balanceOf(_getPaymentToken(), payer) < _getPaymentAmount()) { + err = PaymentError.selector; - if (flags == _SIMULATION_MODE_FLAG) { - revert PaymentError(); - } + if (flags == _SIMULATION_MODE_FLAG) { + revert PaymentError(); } } @@ -364,11 +355,14 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu assembly ("memory-safe") { let m := mload(0x40) // Grab the free memory pointer. if iszero(err) { - // Copy the encoded user op to the memory to be ready to pass to the self call. - calldatacopy(add(m, 0x40), encodedIntent.offset, encodedIntent.length) mstore(m, 0x00000000) // `selfCallPayVerifyCall537021665()`. // The word after the function selector contains the simulation flags. mstore(add(m, 0x20), flags) + // Copy the encoded user op to the memory to be ready to pass to the self call. + // We skip adding the calldata offset and length since we don't read that, but + // add `flags` in the first slot. + calldatacopy(add(m, 0x60), encodedIntent.offset, encodedIntent.length) + mstore(0x00, 0) // Zeroize the return slot. // To prevent griefing, we need to do a non-reverting gas-limited self call. @@ -380,26 +374,27 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu address(), 0, add(m, 0x1c), - add(encodedIntent.length, 0x24), + add(encodedIntent.length, 0x44), 0x00, 0x20 ) err := mload(0x00) // The self call will do another self call to execute. - - if iszero(selfCallSuccess) { - // If it is a simulation, we simply revert with the full error. - if eq(flags, _SIMULATION_MODE_FLAG) { - returndatacopy(mload(0x40), 0x00, returndatasize()) - revert(mload(0x40), returndatasize()) - } - - // If we don't get an error selector, then we set this one. - if iszero(err) { err := shl(224, 0xad4db224) } // `VerifiedCallError()`. + } + } + assembly ("memory-safe") { + if iszero(selfCallSuccess) { + // If it is a simulation, we simply revert with the full error. + if eq(flags, _SIMULATION_MODE_FLAG) { + returndatacopy(mload(0x40), 0x00, returndatasize()) + revert(mload(0x40), returndatasize()) } + + // If we don't get an error selector, then we set this one. + if iszero(err) { err := shl(224, 0xad4db224) } // `VerifiedCallError()`. } } - emit IntentExecuted(i.eoa, i.nonce, selfCallSuccess, err); + emit IntentExecuted(_getEoa(), _getNonce(), selfCallSuccess, err); if (selfCallSuccess) { gUsed = Math.rawSub(gStart, gasleft()); } @@ -432,25 +427,36 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu function selfCallPayVerifyCall537021665() public payable { require(msg.sender == address(this)); - Intent calldata i; uint256 flags; assembly ("memory-safe") { - i := add(0x24, calldataload(0x24)) flags := calldataload(0x04) } // Check if intent has expired (only if expiry is set) // If expiry timestamp is set to 0, then expiry is considered to be infinite. - if (i.expiry != 0 && block.timestamp > i.expiry) { - revert IntentExpired(); + { + uint256 expiry = _getExpiry(); + if (expiry != 0 && block.timestamp > expiry) { + revert IntentExpired(); + } } - address eoa = i.eoa; - uint256 nonce = i.nonce; - bytes32 digest = _computeDigest(i); + address eoa = _getEoa(); + + // Start a calldata pointer to traverse all the inner dynamic bytes within. Every time we get the next dynamic bytes, + // the pointer is advanced. A memory pointer gives some efficiency and will produce relatively clean top-level code. + CalldataPointer memory ptr; + bytes32 digest = _computeDigest(ptr); - _fund(eoa, i.funder, digest, i.encodedFundTransfers, i.funderSignature); + { + bytes calldata fundData = _getNextBytes(ptr); + if (fundData.length > 0) { + (address funder, bytes calldata sig, bytes[] calldata transfers) = + _parseFundData(fundData); + _fund(_getEoa(), funder, digest, transfers, sig); + } + } // The chicken and egg problem: // A off-chain simulation of a successful Intent may not guarantee on-chain success. // The state may change in the window between simulation and actual on-chain execution. @@ -466,57 +472,82 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu // simulation, and suggests banning users that intentionally grief the simulation. // Handle the sub Intents after initialize (if any), and before the `_verify`. - if (i.encodedPreCalls.length != 0) _handlePreCalls(eoa, i.payer, flags, i.encodedPreCalls); - + { + bytes calldata preCallsBytes = _getNextBytes(ptr); + if (preCallsBytes.length > 0) { + _handlePreCalls(eoa, _getPayer(), flags, preCallsBytes); + } + } // If `_verify` is invalid, just revert. // The verification gas is determined by `executionData` and the account logic. // Off-chain simulation of `_verify` should suffice, provided that the eoa's // account is not changed, and the `keyHash` is not revoked // in the window between off-chain simulation and on-chain execution. - - bool isValid; bytes32 keyHash; - if (i.isMultichain) { - // For multi chain intents, we have to verify using merkle sigs. - (isValid, keyHash) = _verifyMerkleSig(digest, eoa, i.signature); - - // If this is an output intent, then send the digest as the settlementId - // on all input chains. - if (i.encodedFundTransfers.length > 0) { - // Output intent - ISettler(i.settler).send(digest, i.settlerContext); + { + bool isValid; + bytes calldata signature = _getNextBytes(ptr); + + uint256 nonce = _getNonce(); + (isValid, keyHash) = _verify(digest, eoa, signature); + + if (nonce >> 240 == MERKLE_VERIFICATION) { + bytes calldata settlerData = _getNextBytes(ptr); + // If this is an output intent, then send the digest as the settlementId + // on all input chains. + if (settlerData.length > 0) { + // Output intent - first 32 bytes of settler data contains the settler address + // Then, it contains 2 offsets then the real data + ISettler(address(uint160(uint256(bytes32(settlerData[:32]))))) + .send(digest, settlerData[96:]); + } } - } else { - (isValid, keyHash) = _verify(digest, eoa, i.signature); - } - if (flags == _SIMULATION_MODE_FLAG) { - isValid = true; - } + if (flags == _SIMULATION_MODE_FLAG) { + isValid = true; + } - if (!isValid) revert VerificationError(); + if (!isValid) { + revert VerificationError(); + } - _checkAndIncrementNonce(eoa, nonce); + _checkAndIncrementNonce(eoa, nonce); + } // Payment // If `_pay` fails, just revert. // Off-chain simulation of `_pay` should suffice, // provided that the token balance does not decrease in the window between // off-chain simulation and on-chain execution. - if (i.paymentAmount != 0) _pay(keyHash, digest, i); + { + uint256 paymentAmount = _getPaymentAmount(); + bytes calldata paymentSignature = _getNextBytes(ptr); + if (paymentAmount != 0) { + _pay( + paymentAmount, + keyHash, + digest, + eoa, + _getPayer(), + _getPaymentToken(), + _getPaymentRecipient(), + paymentSignature + ); + } + } // This re-encodes the ERC7579 `executionData` with the optional `opData`. // We expect that the account supports ERC7821 // (an extension of ERC7579 tailored for 7702 accounts). - bytes memory data = LibERC7579.reencodeBatchAsExecuteCalldata( + bytes memory executeData = LibERC7579.reencodeBatchAsExecuteCalldata( hex"01000000000078210001", // ERC7821 batch execution mode. - i.executionData, + _getExecutionData(), abi.encode(keyHash) // `opData`. ); assembly ("memory-safe") { mstore(0x00, 0) // Zeroize the return slot. - if iszero(call(gas(), eoa, 0, add(0x20, data), mload(data), 0x00, 0x20)) { + if iszero(call(gas(), eoa, 0, add(0x20, executeData), mload(executeData), 0x00, 0x20)) { if eq(flags, _SIMULATION_MODE_FLAG) { returndatacopy(mload(0x40), 0x00, returndatasize()) revert(mload(0x40), returndatasize()) @@ -539,10 +570,15 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu address parentEOA, address payer, uint256 flags, - bytes[] calldata encodedPreCalls + bytes calldata encodedPreCalls ) internal virtual { - for (uint256 j; j < encodedPreCalls.length; ++j) { - SignedCall calldata p = _extractPreCall(encodedPreCalls[j]); + bytes[] calldata calls; + assembly ("memory-safe") { + calls.length := calldataload(add(encodedPreCalls.offset, 0x20)) + calls.offset := add(encodedPreCalls.offset, 0x40) + } + for (uint256 j; j < calls.length; ++j) { + SignedCall calldata p = _extractPreCall(calls[j]); address eoa = Math.coalesce(p.eoa, parentEOA); uint256 nonce = p.nonce; @@ -598,28 +634,6 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu // Multi Chain Functions //////////////////////////////////////////////////////////////////////// - /// @dev Verifies the merkle sig for the multi chain intents. - /// - Note: Each leaf of the merkle tree should be a standard intent digest, computed with chainId. - /// - Leaf intents do NOT need to have the multichain nonce prefix. - /// - The signature for multi chain intents using merkle verification is encoded as: - /// - bytes signature = abi.encode(bytes32[] memory proof, bytes32 root, bytes memory rootSig) - function _verifyMerkleSig(bytes32 digest, address eoa, bytes memory signature) - internal - view - returns (bool isValid, bytes32 keyHash) - { - (bytes32[] memory proof, bytes32 root, bytes memory rootSig) = - abi.decode(signature, (bytes32[], bytes32, bytes)); - - if (MerkleProofLib.verify(proof, root, digest)) { - (isValid, keyHash) = IIthacaAccount(eoa).unwrapAndValidateSignature(root, rootSig); - - return (isValid, keyHash); - } - - return (false, bytes32(0)); - } - /// @dev Funds the eoa with with the encoded fund transfers, before executing the intent. /// - For ERC20 tokens, the funder needs to approve the orchestrator to pull funds. /// - For native assets like ETH, the funder needs to transfer the funds to the orchestrator @@ -629,7 +643,7 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu address eoa, address funder, bytes32 digest, - bytes[] memory encodedFundTransfers, + bytes[] calldata encodedFundTransfers, bytes memory funderSignature ) internal virtual { // Note: The fund function is mostly only used in the multi chain mode. @@ -664,51 +678,59 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu /// @dev Makes the `eoa` perform a payment to the `paymentRecipient` directly. /// This reverts if the payment is insufficient or fails. Otherwise returns nothing. - function _pay(bytes32 keyHash, bytes32 digest, Intent calldata i) internal virtual { - uint256 paymentAmount = i.paymentAmount; - uint256 requiredBalanceAfter = Math.saturatingAdd( - TokenTransferLib.balanceOf(i.paymentToken, i.paymentRecipient), paymentAmount - ); + function _pay( + uint256 paymentAmount, + bytes32 keyHash, + bytes32 intentDigest, + address eoa, + address payer, + address paymentToken, + address paymentRecipient, + bytes calldata paymentSignature + ) internal virtual { + uint256 currentBalance = TokenTransferLib.balanceOf(paymentToken, paymentRecipient); - address payer = Math.coalesce(i.payer, i.eoa); + uint256 requiredBalanceAfter = Math.saturatingAdd(currentBalance, paymentAmount); // Call the pay function on the account contract // Equivalent Solidity code: // IIthacaAccount(payer).pay(paymentAmount, keyHash, digest, abi.encode(i)); // Gas Savings: // Saves ~2k gas for normal use cases, by avoiding abi.encode and solidity external call overhead + address callee = Math.coalesce(payer, eoa); + assembly ("memory-safe") { let m := mload(0x40) // Load the free memory pointer - mstore(m, 0xf81d87a7) // `pay(uint256,bytes32,bytes32,bytes)` - mstore(add(m, 0x20), paymentAmount) // Add payment amount as first param + mstore(m, 0x38e11b2a) // `pay(uint256,bytes32,bytes32,address,address,address,address,bytes)` + mstore(add(m, 0x20), paymentAmount) // Add paymentAmount as first param mstore(add(m, 0x40), keyHash) // Add keyHash as second param - mstore(add(m, 0x60), digest) // Add digest as third param - mstore(add(m, 0x80), 0x80) // Add offset of encoded Intent as third param - - let encodedSize := sub(calldatasize(), i) + mstore(add(m, 0x60), intentDigest) // Add intentDigest as third param + mstore(add(m, 0x80), eoa) // Add eoa as fourth param + mstore(add(m, 0xa0), payer) // Add payer as fifth param + mstore(add(m, 0xc0), paymentToken) // Add paymentToken as sixth param + mstore(add(m, 0xe0), paymentRecipient) // Add paymentRecipient as seventh param + mstore(add(m, 0x100), 0x100) // Add offset for paymentSignature as eighth param - mstore(add(m, 0xa0), add(encodedSize, 0x20)) // Store length of encoded Intent at offset. - mstore(add(m, 0xc0), 0x20) // Offset at which the Intent struct starts in encoded Intent. - - // Copy the intent data to memory - calldatacopy(add(m, 0xe0), i, encodedSize) + // Store paymentSignature length and data + mstore(add(m, 0x120), paymentSignature.length) + calldatacopy(add(m, 0x140), paymentSignature.offset, paymentSignature.length) // We revert here, so that if the payment fails, the execution is also reverted. // The revert for payment is caught inside the selfCallPayVerify function. if iszero( call( gas(), // gas - payer, // address + callee, // address 0, // value add(m, 0x1c), // input memory offset - add(0xc4, encodedSize), // input size + add(0x124, paymentSignature.length), // input size 0x00, // output memory offset 0x20 // output size ) ) { revert(0x00, 0x20) } } - if (TokenTransferLib.balanceOf(i.paymentToken, i.paymentRecipient) < requiredBalanceAfter) { + if (TokenTransferLib.balanceOf(paymentToken, paymentRecipient) < requiredBalanceAfter) { revert PaymentError(); } } @@ -752,42 +774,39 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu /// @dev Computes the EIP712 digest for the PreCall. function _computeDigest(SignedCall calldata p) internal view virtual returns (bytes32) { - bool isMultichain = p.nonce >> 240 == MULTICHAIN_NONCE_PREFIX; // To avoid stack-too-deep. Faster than a regular Solidity array anyways. - bytes32[] memory f = EfficientHashLib.malloc(5); + bytes32[] memory f = EfficientHashLib.malloc(4); f.set(0, SIGNED_CALL_TYPEHASH); - f.set(1, LibBit.toUint(isMultichain)); - f.set(2, uint160(p.eoa)); - f.set(3, _executionDataHash(p.executionData)); - f.set(4, p.nonce); + f.set(1, uint160(p.eoa)); + f.set(2, _executionDataHash(p.executionData)); + f.set(3, p.nonce); - return isMultichain ? _hashTypedDataSansChainId(f.hash()) : _hashTypedData(f.hash()); + return p.nonce >> 240 == MULTICHAIN_NONCE_PREFIX + ? _hashTypedDataSansChainId(f.hash()) + : _hashTypedData(f.hash()); } - /// @dev Computes the EIP712 digest for the Intent. - /// If the the nonce starts with `MULTICHAIN_NONCE_PREFIX`, - /// the digest will be computed without the chain ID. - /// Otherwise, the digest will be computed with the chain ID. - function _computeDigest(Intent calldata i) internal view virtual returns (bytes32) { - bool isMultichain = i.nonce >> 240 == MULTICHAIN_NONCE_PREFIX; + /// @dev Computes the EIP712 digest for the Intent + /// @dev Also updates the passed in CalldataPointer to point to the start of the next dynamic bytes. + function _computeDigest(CalldataPointer memory p) internal view virtual returns (bytes32) { + bytes32 digest = INTENT_TYPEHASH; - // To avoid stack-too-deep. Faster than a regular Solidity array anyways. - bytes32[] memory f = EfficientHashLib.malloc(13); - f.set(0, INTENT_TYPEHASH); - f.set(1, LibBit.toUint(isMultichain)); - f.set(2, uint160(i.eoa)); - f.set(3, _executionDataHash(i.executionData)); - f.set(4, i.nonce); - f.set(5, uint160(i.payer)); - f.set(6, uint160(i.paymentToken)); - f.set(7, i.paymentMaxAmount); - f.set(8, i.combinedGas); - f.set(9, _encodedArrHash(i.encodedPreCalls)); - f.set(10, _encodedArrHash(i.encodedFundTransfers)); - f.set(11, uint160(i.settler)); - f.set(12, i.expiry); - - return isMultichain ? _hashTypedDataSansChainId(f.hash()) : _hashTypedData(f.hash()); + assembly ("memory-safe") { + let fmp := mload(0x40) + mstore(fmp, digest) + let length := calldataload(_EXECUTION_DATA_OFFSET) + calldatacopy(add(fmp, 0x20), add(_EXECUTION_DATA_OFFSET, 0x20), length) + mstore(add(fmp, 0x20), keccak256(add(fmp, 0x20), length)) + calldatacopy(add(fmp, 0x40), _EOA_OFFSET, 224) // copy 7 words + digest := keccak256(fmp, 288) // hash 9 words, inc typehash + + // update the offset for the memory ptr + mstore(p, add(add(_EXECUTION_DATA_OFFSET, 0x20), length)) + } + + return _getNonce() >> 240 == MULTICHAIN_NONCE_PREFIX + ? _hashTypedDataSansChainId(digest) + : _hashTypedData(digest); } /// @dev Helper function to return the hash of the `execuctionData`. @@ -840,7 +859,7 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu returns (string memory name, string memory version) { name = "Orchestrator"; - version = "0.5.5"; + version = "0.5.6"; } //////////////////////////////////////////////////////////////////////// diff --git a/src/SimpleFunder.sol b/src/SimpleFunder.sol index e74947fb..7109f028 100644 --- a/src/SimpleFunder.sol +++ b/src/SimpleFunder.sol @@ -58,7 +58,7 @@ contract SimpleFunder is EIP712, Ownable, IFunder { returns (string memory name, string memory version) { name = "SimpleFunder"; - version = "0.1.8"; + version = "0.1.9"; } //////////////////////////////////////////////////////////////////////// @@ -133,11 +133,10 @@ contract SimpleFunder is EIP712, Ownable, IFunder { /// @dev Allows the orchestrator to fund an account. /// The `digest` includes the intent nonce and the transfers. - function fund( - bytes32 digest, - ICommon.Transfer[] memory transfers, - bytes memory funderSignature - ) public override { + function fund(bytes32 digest, ICommon.Transfer[] memory transfers, bytes memory funderSignature) + public + override + { if (!orchestrators[msg.sender]) { revert OnlyOrchestrator(); } @@ -183,7 +182,7 @@ contract SimpleFunder is EIP712, Ownable, IFunder { if gt(amount, allowance) { mstore(m, 0x095ea7b3) // `approve(address,uint256)`. mstore(add(m, 0x20), caller()) - mstore(add(m, 0x40), 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) // type(uint256).max + mstore(add(m, 0x40), 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) // 20-byte all-ones sentinel (2^160-1), not uint256 max // Orchestrator checks for token transfer success, so we don't need to check it here. pop(call(gas(), token, 0, add(m, 0x1c), 0x44, 0x00, 0x00)) } diff --git a/src/Simulator.sol b/src/Simulator.sol index 1e468d6b..2a5e2a76 100644 --- a/src/Simulator.sol +++ b/src/Simulator.sol @@ -4,10 +4,12 @@ pragma solidity ^0.8.23; import {ICommon} from "./interfaces/ICommon.sol"; import {IMulticall3} from "./interfaces/IMulticall3.sol"; import {FixedPointMathLib as Math} from "solady/utils/FixedPointMathLib.sol"; +import {MockOrchestrator} from "../test/utils/mocks/MockOrchestrator.sol"; +import {IntentHelpers} from "./libraries/IntentHelpers.sol"; /// @title Simulator /// @notice A separate contract for calling the Orchestrator contract solely for gas simulation. -contract Simulator { +contract Simulator is IntentHelpers { //////////////////////////////////////////////////////////////////////// // EIP-5267 Support //////////////////////////////////////////////////////////////////////// @@ -54,15 +56,23 @@ contract Simulator { /// @dev Updates the payment amounts for the Intent passed in. function _updatePaymentAmounts( - ICommon.Intent memory u, + bytes memory u, uint256 gas, uint8 paymentPerGasPrecision, uint256 paymentPerGas ) internal pure { uint256 paymentAmount = Math.fullMulDiv(gas, paymentPerGas, 10 ** paymentPerGasPrecision); - u.paymentAmount += paymentAmount; - u.paymentMaxAmount += paymentAmount; + // 36 because we don't have the calldata offset, or the function selector + uint256 paymentOffset = _PAYMENT_AMOUNT_OFFSET - 36; + uint256 paymentMaxOffset = _PAYMENT_MAX_AMOUNT_OFFSET - 36; + assembly ("memory-safe") { + let currentPaymentAmount := mload(add(u, paymentOffset)) + mstore(add(u, paymentOffset), add(currentPaymentAmount, paymentAmount)) + + let currentPaymentMaxAmount := mload(add(u, paymentMaxOffset)) + mstore(add(u, paymentMaxOffset), add(currentPaymentMaxAmount, paymentAmount)) + } } /// @dev Performs a call to the Orchestrator, and returns the gas used by the Intent. @@ -101,10 +111,8 @@ contract Simulator { bytes calldata encodedIntent ) internal freeTempMemory returns (uint256) { bytes memory data = abi.encodeWithSignature( - "simulateExecute(bool,uint256,bytes)", - isStateOverride, - combinedGasOverride, - encodedIntent + "simulateExecute(bytes)", + abi.encodePacked(encodedIntent, isStateOverride, combinedGasOverride) ); return _callOrchestrator(oc, isStateOverride, data); } @@ -115,13 +123,10 @@ contract Simulator { address oc, bool isStateOverride, uint256 combinedGasOverride, - ICommon.Intent memory u + bytes memory u ) internal freeTempMemory returns (uint256) { bytes memory data = abi.encodeWithSignature( - "simulateExecute(bool,uint256,bytes)", - isStateOverride, - combinedGasOverride, - abi.encode(u) + "simulateExecute(bytes)", abi.encodePacked(u, isStateOverride, combinedGasOverride) ); return _callOrchestrator(oc, isStateOverride, data); } @@ -257,7 +262,7 @@ contract Simulator { /// @dev The closer this number is to 10_000, the more precise combined gas will be. But more iterations will be needed. /// @dev This number should always be > 10_000, to get correct results. //// If the increment is too small, the function might run out of gas while finding the combined gas value. - /// @param encodedIntent The encoded user operation + /// @param calldataIntent The encoded intent /// @return gasUsed The gas used in the successful simulation /// @return combinedGas The first combined gas value that gives a successful simulation. /// This function reverts if the primary simulation run with max combinedGas fails. @@ -268,10 +273,10 @@ contract Simulator { uint8 paymentPerGasPrecision, uint256 paymentPerGas, uint256 combinedGasIncrement, - bytes calldata encodedIntent + bytes calldata calldataIntent ) public payable virtual returns (uint256 gasUsed, uint256 combinedGas) { // 1. Primary Simulation Run to get initial gasUsed value with combinedGasOverride - gasUsed = _callOrchestratorCalldata(oc, false, type(uint256).max, encodedIntent); + gasUsed = _callOrchestratorCalldata(oc, false, type(uint256).max, calldataIntent); // If the simulation failed, bubble up the full revert. assembly ("memory-safe") { @@ -282,15 +287,21 @@ contract Simulator { } } - // Update payment amounts using the gasUsed value - ICommon.Intent memory u = abi.decode(encodedIntent, (ICommon.Intent)); + bytes memory memoryIntent = calldataIntent; - u.combinedGas += gasUsed; + // 36 because we don't have the calldata offset, or the function selector + uint256 offset = _COMBINED_GAS_OFFSET - 36; + assembly ("memory-safe") { + let currentCombinedGas := mload(add(memoryIntent, offset)) + let newCombinedGas := add(currentCombinedGas, gasUsed) + mstore(add(memoryIntent, offset), newCombinedGas) + combinedGas := newCombinedGas + } - _updatePaymentAmounts(u, u.combinedGas, paymentPerGasPrecision, paymentPerGas); + _updatePaymentAmounts(memoryIntent, combinedGas, paymentPerGasPrecision, paymentPerGas); while (true) { - gasUsed = _callOrchestratorMemory(oc, false, 0, u); + gasUsed = _callOrchestratorMemory(oc, false, 0, memoryIntent); // If the simulation failed, bubble up the full revert. assembly ("memory-safe") { @@ -305,22 +316,34 @@ contract Simulator { } if (gasUsed != 0) { - return (gasUsed, u.combinedGas); + // Get current combinedGas from bytes + assembly ("memory-safe") { + combinedGas := mload(add(memoryIntent, offset)) + } + return (gasUsed, combinedGas); } - uint256 gasIncrement = Math.mulDiv(u.combinedGas, combinedGasIncrement, 10_000); + // Get current combinedGas and calculate increment + assembly ("memory-safe") { + combinedGas := mload(add(memoryIntent, offset)) + } + uint256 gasIncrement = Math.mulDiv(combinedGas, combinedGasIncrement, 10_000); - _updatePaymentAmounts(u, gasIncrement, paymentPerGasPrecision, paymentPerGas); + _updatePaymentAmounts(memoryIntent, gasIncrement, paymentPerGasPrecision, paymentPerGas); - // Step up the combined gas, until we see a simulation passing - u.combinedGas += gasIncrement; + // Update combinedGas with increment using assembly + assembly ("memory-safe") { + let currentCombinedGas := mload(add(memoryIntent, offset)) + let newCombinedGas := add(currentCombinedGas, gasIncrement) + mstore(add(memoryIntent, offset), newCombinedGas) + } } } /// @dev Same as simulateCombinedGas, but with an additional verification run /// that generates a successful non reverting state override simulation. /// Which can be used in eth_simulateV1 to get the trace.\ - /// @param combinedGasVerificationOffset is a static value that is added after a succesful combinedGas is found. + /// @param combinedGasVerificationOffset is a static value that is added after a successful combinedGas is found. /// This can be used to account for variations in sig verification gas, for keytypes like P256. /// @param paymentPerGasPrecision The precision of the payment per gas value. /// paymentAmount = gas * paymentPerGas / (10 ** paymentPerGasPrecision) @@ -338,15 +361,18 @@ contract Simulator { combinedGas += combinedGasVerificationOffset; - ICommon.Intent memory u = abi.decode(encodedIntent, (ICommon.Intent)); + bytes memory encodedIntentCopy = encodedIntent; - _updatePaymentAmounts(u, combinedGas, paymentPerGasPrecision, paymentPerGas); + _updatePaymentAmounts(encodedIntentCopy, combinedGas, paymentPerGasPrecision, paymentPerGas); - u.combinedGas = combinedGas; + // 36 because we don't have the calldata offset, or the function selector + uint256 offset = _COMBINED_GAS_OFFSET - 36; + assembly ("memory-safe") { + mstore(add(encodedIntentCopy, offset), combinedGas) + } // Verification Run to generate the logs with the correct combinedGas and payment amounts. - gasUsed = _callOrchestratorMemory(oc, true, 0, u); - + gasUsed = _callOrchestratorMemory(oc, true, 0, encodedIntentCopy); // If the simulation failed, bubble up full revert assembly ("memory-safe") { if iszero(gasUsed) { diff --git a/src/interfaces/ICommon.sol b/src/interfaces/ICommon.sol index 54ccf388..9057b1f3 100644 --- a/src/interfaces/ICommon.sol +++ b/src/interfaces/ICommon.sol @@ -2,86 +2,6 @@ pragma solidity ^0.8.23; interface ICommon { - //////////////////////////////////////////////////////////////////////// - // Data Structures - //////////////////////////////////////////////////////////////////////// - /// @dev A struct to hold the intent fields. - /// Since L2s already include calldata compression with savings forwarded to users, - /// we don't need to be too concerned about calldata overhead - struct Intent { - //////////////////////////////////////////////////////////////////////// - // EIP-712 Fields - //////////////////////////////////////////////////////////////////////// - /// @dev The user's address. - address eoa; - /// @dev An encoded array of calls, using ERC7579 batch execution encoding. - /// `abi.encode(calls)`, where `calls` is of type `Call[]`. - /// This allows for more efficient safe forwarding to the EOA. - bytes executionData; - /// @dev Per delegated EOA. - /// This nonce is a 4337-style 2D nonce with some specializations: - /// - Upper 192 bits are used for the `seqKey` (sequence key). - /// The upper 16 bits of the `seqKey` is `MULTICHAIN_NONCE_PREFIX`, - /// then the Intent EIP712 hash will exclude the chain ID. - /// - Lower 64 bits are used for the sequential nonce corresponding to the `seqKey`. - uint256 nonce; - /// @dev The account paying the payment token. - /// If this is `address(0)`, it defaults to the `eoa`. - address payer; - /// @dev The ERC20 or native token used to pay for gas. - address paymentToken; - /// @dev The maximum amount of the token to pay. - uint256 paymentMaxAmount; - /// @dev The combined gas limit for payment, verification, and calling the EOA. - uint256 combinedGas; - /// @dev Optional array of encoded SignedCalls that will be verified and executed - /// before the validation of the overall Intent. - /// A PreCall will NOT have its gas limit or payment applied. - /// The overall Intent's gas limit and payment will be applied, encompassing all its PreCalls. - /// The execution of a PreCall will check and increment the nonce in the PreCall. - /// If at any point, any PreCall cannot be verified to be correct, or fails in execution, - /// the overall Intent will revert before validation, and execute will return a non-zero error. - bytes[] encodedPreCalls; - /// @dev Only relevant for multi chain intents. - /// There should not be any duplicate token addresses. Use address(0) for native token. - /// If native token is used, the first transfer should be the native token transfer. - /// If encodedFundTransfers is not empty, then the intent is considered the output intent. - bytes[] encodedFundTransfers; - /// @dev The settler address. - address settler; - /// @dev The expiry timestamp for the intent. The intent is invalid after this timestamp. - /// If expiry timestamp is set to 0, then expiry is considered to be infinite. - uint256 expiry; - //////////////////////////////////////////////////////////////////////// - // Additional Fields (Not included in EIP-712) - //////////////////////////////////////////////////////////////////////// - /// @dev Whether the intent should use the multichain mode - i.e verify with merkle sigs - /// and send the cross chain message. - bool isMultichain; - /// @dev The funder address. - address funder; - /// @dev The funder signature. - bytes funderSignature; - /// @dev The settler context data to be passed to the settler. - bytes settlerContext; - /// @dev The actual payment amount, requested by the filler. MUST be less than or equal to `paymentMaxAmount` - uint256 paymentAmount; - /// @dev The payment recipient for the ERC20 token. - /// Excluded from signature. The filler can replace this with their own address. - /// This enables multiple fillers, allowing for competitive filling, better uptime. - address paymentRecipient; - /// @dev The wrapped signature. - /// `abi.encodePacked(innerSignature, keyHash, prehash)`. - bytes signature; - /// @dev Optional payment signature to be passed into the `compensate` function - /// on the `payer`. This signature is NOT included in the EIP712 signature. - bytes paymentSignature; - /// @dev Optional. If non-zero, the EOA must use `supportedAccountImplementation`. - /// Otherwise, if left as `address(0)`, any EOA implementation will be supported. - /// This field is NOT included in the EIP712 signature. - address supportedAccountImplementation; - } - /// @dev A struct to hold the fields for a SignedCall. /// A SignedCall is a struct that contains a signed execution batch along with the nonce // and address of the user. diff --git a/src/interfaces/IFunder.sol b/src/interfaces/IFunder.sol index 90c2547f..ddbacc0f 100644 --- a/src/interfaces/IFunder.sol +++ b/src/interfaces/IFunder.sol @@ -16,9 +16,6 @@ interface IFunderV4 { interface IFunder is IFunderV4 { /// @dev Checks if fund transfers are valid given a funderSignature. /// @dev Funder implementations must revert if the signature is invalid. - function fund( - bytes32 digest, - ICommon.Transfer[] memory transfers, - bytes memory funderSignature - ) external; + function fund(bytes32 digest, ICommon.Transfer[] memory transfers, bytes memory funderSignature) + external; } diff --git a/src/interfaces/IIthacaAccount.sol b/src/interfaces/IIthacaAccount.sol index 099f466d..9056db7c 100644 --- a/src/interfaces/IIthacaAccount.sol +++ b/src/interfaces/IIthacaAccount.sol @@ -7,14 +7,23 @@ import {ICommon} from "../interfaces/ICommon.sol"; /// @notice Interface for the Account contract interface IIthacaAccount is ICommon { /// @dev Pays `paymentAmount` of `paymentToken` to the `paymentRecipient`. + /// @param paymentAmount The amount to pay /// @param keyHash The hash of the key used to authorize the operation - /// @param encodedIntent The encoded user operation /// @param intentDigest The digest of the user operation + /// @param eoa The EOA address + /// @param payer The payer address + /// @param paymentToken The token to pay with + /// @param paymentRecipient The recipient of the payment + /// @param paymentSignature The payment signature function pay( uint256 paymentAmount, bytes32 keyHash, bytes32 intentDigest, - bytes calldata encodedIntent + address eoa, + address payer, + address paymentToken, + address paymentRecipient, + bytes calldata paymentSignature ) external; /// @dev Returns if the signature is valid, along with its `keyHash`. diff --git a/src/interfaces/IMulticall3.sol b/src/interfaces/IMulticall3.sol deleted file mode 100644 index c0ab86f2..00000000 --- a/src/interfaces/IMulticall3.sol +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -/// @notice Interface for Multicall3 contract -interface IMulticall3 { - struct Call { - address target; - bytes callData; - } - - struct Call3 { - address target; - bool allowFailure; - bytes callData; - } - - struct Call3Value { - address target; - bool allowFailure; - uint256 value; - bytes callData; - } - - struct Result { - bool success; - bytes returnData; - } - - function aggregate(Call[] calldata calls) - external - payable - returns (uint256 blockNumber, bytes[] memory returnData); - - function aggregate3(Call3[] calldata calls) - external - payable - returns (Result[] memory returnData); -} diff --git a/src/interfaces/IOrchestrator.sol b/src/interfaces/IOrchestrator.sol index ffb4de93..05dfabb4 100644 --- a/src/interfaces/IOrchestrator.sol +++ b/src/interfaces/IOrchestrator.sol @@ -27,11 +27,10 @@ interface IOrchestrator is ICommon { /// But the balance of tx.origin has to be greater than or equal to uint192.max, to prove that a state override has been made offchain, /// and this is not an onchain call. This mode has been added so that receipt logs can be generated for `eth_simulateV1` /// @return gasUsed The amount of gas used by the execution. (Only returned if `isStateOverride` is true) - function simulateExecute( - bool isStateOverride, - uint256 combinedGasOverride, - bytes calldata encodedIntent - ) external payable returns (uint256 gasUsed); + function simulateExecute(bytes calldata encodedIntent) + external + payable + returns (uint256 gasUsed); /// @dev Allows the orchestrator owner to withdraw tokens. /// @param token The token address (0 for native token) diff --git a/src/libraries/ERC7821Ithaca.sol b/src/libraries/ERC7821Ithaca.sol new file mode 100644 index 00000000..5f726b42 --- /dev/null +++ b/src/libraries/ERC7821Ithaca.sol @@ -0,0 +1,347 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {Receiver} from "solady/accounts/Receiver.sol"; + +/// @notice Minimal batch executor mixin. +/// @author Solady (https://github.com/vectorized/solady/blob/main/src/accounts/ERC7821.sol) +/// +/// @dev This contract can be inherited to create fully-fledged smart accounts. +/// If you merely want to combine approve-swap transactions into a single transaction +/// using [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702), you will need to implement basic +/// [ERC-1271](https://eips.ethereum.org/EIPS/eip-1271) `isValidSignature` functionality to +/// validate signatures with `ecrecover` against the EOA address. This is necessary because some +/// signature checks skip `ecrecover` if the signer has code. For a basic EOA batch executor, +/// please refer to [BEBE](https://github.com/vectorized/bebe), which inherits from this class. +contract ERC7821Ithaca is Receiver { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STRUCTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Call struct for the `execute` function. + struct Call { + address to; // Replaced as `address(this)` if `address(0)`. Renamed to `to` for Ithaca Porto. + uint256 value; // Amount of native currency (i.e. Ether) to send. + bytes data; // Calldata to send with the call. + } + + struct CallSansTo { + uint256 value; // Amount of native currency (i.e. Ether) to send. + bytes data; // Calldata to send with the call. + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CUSTOM ERRORS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev The execution mode is not supported. + error UnsupportedExecutionMode(); + + /// @dev Cannot decode `executionData` as a batch of batches `abi.encode(bytes[])`. + error BatchOfBatchesDecodingError(); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* EXECUTION OPERATIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Executes the calls in `executionData`. + /// Reverts and bubbles up error if any call fails. + /// + /// `executionData` encoding (single batch): + /// - If `opData` is empty, `executionData` is simply `abi.encode(calls)`. + /// - Else, `executionData` is `abi.encode(calls, opData)`. + /// See: https://eips.ethereum.org/EIPS/eip-7579 + /// + /// `executionData` encoding (batch of batches): + /// - `executionData` is `abi.encode(bytes[])`, where each element in `bytes[]` + /// is an `executionData` for a single batch. + /// + /// Supported modes: + /// - `0x01000000000000000000...`: Single batch. Does not support optional `opData`. + /// - `0x01000000000078210001...`: Single batch. Supports optional `opData`. + /// - `0x01000000000078210002...`: Batch of batches. + /// - `0x01000000000078210003...`: Single batch with common `to` address and optional `opData`. + /// + /// For the "batch of batches" mode, each batch will be recursively passed into + /// `execute` internally with mode `0x01000000000078210001...`. + /// Useful for passing in batches signed by different signers. + /// + /// Authorization checks: + /// - If `opData` is empty, the implementation SHOULD require that + /// `msg.sender == address(this)`. + /// - If `opData` is not empty, the implementation SHOULD use the signature + /// encoded in `opData` to determine if the caller can perform the execution. + /// - If `msg.sender` is an authorized entry point, then `execute` MAY accept + /// calls from the entry point, and MAY use `opData` for specialized logic. + /// + /// `opData` may be used to store additional data for authentication, + /// paymaster data, gas limits, etc. + function execute(bytes32 mode, bytes calldata executionData) public payable virtual { + uint256 id = _executionModeId(mode); + if (id == 3) return _executeBatchOfBatches(mode, executionData); + if (id == 4) return _executeBatchCommonTo(mode, executionData); + Call[] calldata calls; + bytes calldata opData; + + /// @solidity memory-safe-assembly + assembly { + if iszero(id) { + mstore(0x00, 0x7f181275) // `UnsupportedExecutionMode()`. + revert(0x1c, 0x04) + } + // Use inline assembly to extract the calls and optional `opData` efficiently. + opData.length := 0 + let o := add(executionData.offset, calldataload(executionData.offset)) + calls.offset := add(o, 0x20) + calls.length := calldataload(o) + // If the offset of `executionData` allows for `opData`, and the mode supports it. + if gt(eq(id, 2), gt(0x40, calldataload(executionData.offset))) { + let q := add(executionData.offset, calldataload(add(0x20, executionData.offset))) + opData.offset := add(q, 0x20) + opData.length := calldataload(q) + } + // Bounds checking for `executionData` is skipped here for efficiency. + // This is safe if it is only used as an argument to `execute` externally. + // If `executionData` used as an argument to other functions externally, + // please perform the bounds checks via `LibERC7579.decodeBatchAndOpData` + /// or `abi.decode` in the other functions for safety. + } + _execute(mode, executionData, calls, opData); + } + + /// @dev Provided for execution mode support detection. + function supportsExecutionMode(bytes32 mode) public view virtual returns (bool result) { + return _executionModeId(mode) != 0; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INTERNAL HELPERS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev 0: invalid mode, 1: no `opData` support, 2: with `opData` support, 3: batch of batches. + function _executionModeId(bytes32 mode) internal view virtual returns (uint256 id) { + // Only supports atomic batched executions. + // For the encoding scheme, see: https://eips.ethereum.org/EIPS/eip-7579 + // Bytes Layout: + // - [0] ( 1 byte ) `0x01` for batch call. + // - [1] ( 1 byte ) `0x00` for revert on any failure. + // - [2..5] ( 4 bytes) Reserved by ERC7579 for future standardization. + // - [6..9] ( 4 bytes) `0x00000000` or `0x78210001` or `0x78210002`. + // - [10..31] (22 bytes) Unused. Free for use. + /// @solidity memory-safe-assembly + assembly { + let m := and(shr(mul(22, 8), mode), 0xffff00000000ffffffff) + id := eq(m, 0x01000000000000000000) // 1. + id := or(shl(1, eq(m, 0x01000000000078210001)), id) // 2. + id := or(mul(3, eq(m, 0x01000000000078210002)), id) // 3. + id := or(mul(4, eq(m, 0x01000000000078210003)), id) // 4. + } + } + + /// @dev For execution of a batch of batches with a common `to` address. + /// @dev if to == address(0), it will be replaced with address(this) + /// Execution Data: abi.encode(address to, CallSansTo[] calls, bytes opData) + function _executeBatchCommonTo(bytes32 mode, bytes calldata executionData) internal virtual { + address to; + CallSansTo[] calldata calls; + bytes calldata opData; + + /// @solidity memory-safe-assembly + assembly { + to := calldataload(executionData.offset) + + let callOffset := + add(executionData.offset, calldataload(add(0x20, executionData.offset))) + calls.offset := add(callOffset, 0x20) + calls.length := calldataload(callOffset) + + // This line is needed to ensure that opdata is valid in all code paths. + // Otherwise the compiler complains. + opData.length := 0 + // If the offset of `executionData` allows for `opData`, and the mode supports it. + if gt(calldataload(add(0x20, executionData.offset)), 0x40) { + let opDataOffset := + add(executionData.offset, calldataload(add(0x40, executionData.offset))) + opData.offset := add(opDataOffset, 0x20) + opData.length := calldataload(opDataOffset) + } + } + + _execute(mode, executionData, to, calls, opData); + } + + /// @dev For execution of a batch of batches. + function _executeBatchOfBatches(bytes32 mode, bytes calldata executionData) internal virtual { + // Replace with `0x0100________78210001...` while preserving optional and reserved fields. + mode ^= bytes32(uint256(3 << (22 * 8))); // `2 XOR 3 = 1`. + (uint256 n, uint256 o, uint256 e) = (0, 0, 0); + /// @solidity memory-safe-assembly + assembly { + let j := calldataload(executionData.offset) + let t := add(executionData.offset, j) + n := calldataload(t) // `batches.length`. + o := add(0x20, t) // Offset of `batches[0]`. + e := add(executionData.offset, executionData.length) // End of `executionData`. + // Do the bounds check on `executionData` treating it as `abi.encode(bytes[])`. + // Not too expensive, so we will just do it right here right now. + if or(shr(64, j), or(lt(executionData.length, 0x20), gt(add(o, shl(5, n)), e))) { + mstore(0x00, 0x3995943b) // `BatchOfBatchesDecodingError()`. + revert(0x1c, 0x04) + } + } + unchecked { + for (uint256 i; i != n; ++i) { + bytes calldata batch; + /// @solidity memory-safe-assembly + assembly { + let j := calldataload(add(o, shl(5, i))) + let t := add(o, j) + batch.offset := add(t, 0x20) + batch.length := calldataload(t) + // Validate that `batches[i]` is not out-of-bounds. + if or(shr(64, j), gt(add(batch.offset, batch.length), e)) { + mstore(0x00, 0x3995943b) // `BatchOfBatchesDecodingError()`. + revert(0x1c, 0x04) + } + } + execute(mode, batch); + } + } + } + + /// @dev Executes the calls. + /// Reverts and bubbles up error if any call fails. + /// The `mode` and `executionData` are passed along in case there's a need to use them. + function _execute( + bytes32 mode, + bytes calldata executionData, + Call[] calldata calls, + bytes calldata opData + ) internal virtual { + // Silence compiler warning on unused variables. + mode = mode; + executionData = executionData; + // Very basic auth to only allow this contract to be called by itself. + // Override this function to perform more complex auth with `opData`. + if (opData.length == uint256(0)) { + require(msg.sender == address(this)); + // Remember to return `_execute(calls, extraData)` when you override this function. + return _execute(calls, bytes32(0)); + } + revert(); // In your override, replace this with logic to operate on `opData`. + } + + /// @dev Executes the calls. + /// Reverts and bubbles up error if any call fails. + /// The `mode` and `executionData` are passed along in case there's a need to use them. + function _execute( + bytes32 mode, + bytes calldata executionData, + address to, + CallSansTo[] calldata calls, + bytes calldata opData + ) internal virtual { + // Silence compiler warning on unused variables. + mode = mode; + executionData = executionData; + // Very basic auth to only allow this contract to be called by itself. + // Override this function to perform more complex auth with `opData`. + if (opData.length == uint256(0)) { + require(msg.sender == address(this)); + // Remember to return `_execute(calls, extraData)` when you override this function. + return _execute(calls, to, bytes32(0)); + } + revert(); // In your override, replace this with logic to operate on `opData`. + } + + /// @dev Executes the calls. + /// Reverts and bubbles up error if any call fails. + /// `extraData` can be any supplementary data (e.g. a memory pointer, some hash). + function _execute(Call[] calldata calls, bytes32 extraData) internal virtual { + unchecked { + uint256 i; + if (calls.length == uint256(0)) return; + do { + (address to, uint256 value, bytes calldata data) = _get(calls, i); + _execute(to, value, data, extraData); + } while (++i != calls.length); + } + } + + /// @dev Executes the calls. + /// Reverts and bubbles up error if any call fails. + /// `extraData` can be any supplementary data (e.g. a memory pointer, some hash). + function _execute(CallSansTo[] calldata calls, address to, bytes32 keyHash) internal virtual { + unchecked { + uint256 i; + // If `to` is address(0), it will be replaced with address(this) + /// @solidity memory-safe-assembly + assembly { + let t := shr(96, shl(96, to)) + to := or(mul(address(), iszero(t)), t) + } + if (calls.length == uint256(0)) return; + do { + (uint256 value, bytes calldata data) = _get(calls, i); + _execute(to, value, data, keyHash); + } while (++i != calls.length); + } + } + + /// @dev Executes the call. + /// Reverts and bubbles up error if any call fails. + /// `extraData` can be any supplementary data (e.g. a memory pointer, some hash). + function _execute(address to, uint256 value, bytes calldata data, bytes32 extraData) + internal + virtual + { + /// @solidity memory-safe-assembly + assembly { + extraData := extraData // Silence unused variable compiler warning. + let m := mload(0x40) // Grab the free memory pointer. + calldatacopy(m, data.offset, data.length) + if iszero(call(gas(), to, value, m, data.length, codesize(), 0x00)) { + // Bubble up the revert if the call reverts. + returndatacopy(m, 0x00, returndatasize()) + revert(m, returndatasize()) + } + } + } + + /// @dev Convenience function for getting `calls[i]`, without bounds checks. + function _get(CallSansTo[] calldata calls, uint256 i) + internal + view + virtual + returns (uint256 value, bytes calldata data) + { + /// @solidity memory-safe-assembly + assembly { + let c := add(calls.offset, calldataload(add(calls.offset, shl(5, i)))) + value := calldataload(c) + let o := add(c, calldataload(add(c, 0x20))) + data.offset := add(o, 0x20) + data.length := calldataload(o) + } + } + + /// @dev Convenience function for getting `calls[i]`, without bounds checks. + function _get(Call[] calldata calls, uint256 i) + internal + view + virtual + returns (address to, uint256 value, bytes calldata data) + { + /// @solidity memory-safe-assembly + assembly { + let c := add(calls.offset, calldataload(add(calls.offset, shl(5, i)))) + // Replaces `to` with `address(this)` if `address(0)` is provided. + let t := shr(96, shl(96, calldataload(c))) + to := or(mul(address(), iszero(t)), t) + value := calldataload(add(c, 0x20)) + let o := add(c, calldataload(add(c, 0x40))) + data.offset := add(o, 0x20) + data.length := calldataload(o) + } + } +} diff --git a/src/libraries/IntentHelpers.sol b/src/libraries/IntentHelpers.sol new file mode 100644 index 00000000..fc4f2722 --- /dev/null +++ b/src/libraries/IntentHelpers.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {ICommon} from "../interfaces/ICommon.sol"; + +contract IntentHelpers { + /** + * The intent's layout in calldata: + * bytes4 fnSel // [0:4] + * uint256 offset; // [4:36] + * uint256 length; // [36:68] + * address supportedAccountImplementation; + * address eoa; + * uint256 nonce; + * address payer; + * address paymentToken; + * uint256 paymentMaxAmount; + * uint256 combinedGas; + * uint256 expiry; + * uint256 paymentAmount; + * address paymentRecipient; + * uint256 executionData.length + * bytes executionData; + * uint256 fundData.length + * bytes fundData; // abi.encode(funder, funderSignature, encodedFundTransfers) + * uint256 encodedPreCalls.length + * bytes encodedPreCalls; // abi.encode(bytes[]) + * uint256 signature.length + * bytes signature; + * uint256 settlerData.length + * bytes settlerData; // abi.encode(settler, settlerContext). To use settler, nonce needs to have `MERKLE_VERIFICATION` prefix + * uint256 paymentSignature.length + * bytes paymentSignature; + */ + uint256 internal constant _SUPPORTED_ACCOUNT_IMPLEMENTATION_OFFSET = 68; + uint256 internal constant _EOA_OFFSET = 88; + uint256 internal constant _NONCE_OFFSET = 120; + uint256 internal constant _PAYER_OFFSET = 152; + uint256 internal constant _PAYMENT_TOKEN_OFFSET = 184; + uint256 internal constant _PAYMENT_MAX_AMOUNT_OFFSET = 216; + uint256 internal constant _COMBINED_GAS_OFFSET = 248; + uint256 internal constant _EXPIRY_OFFSET = 280; + uint256 internal constant _PAYMENT_AMOUNT_OFFSET = 312; + uint256 internal constant _PAYMENT_RECIPIENT_OFFSET = 344; + uint256 internal constant _EXECUTION_DATA_OFFSET = 364; + + struct CalldataPointer { + uint256 offset; + } + + function _getSupportedAccountImplementation() internal pure returns (address) { + return address( + bytes20( + msg.data[ + _SUPPORTED_ACCOUNT_IMPLEMENTATION_OFFSET: + _SUPPORTED_ACCOUNT_IMPLEMENTATION_OFFSET + 20 + ] + ) + ); + } + + function _getEoa() internal pure returns (address a) { + assembly ("memory-safe") { + a := calldataload(_EOA_OFFSET) + } + } + + function _getNonce() internal pure returns (uint256 a) { + assembly ("memory-safe") { + a := calldataload(_NONCE_OFFSET) + } + } + + function _getPayer() internal pure returns (address a) { + assembly ("memory-safe") { + a := calldataload(_PAYER_OFFSET) + } + } + + function _getPaymentToken() internal pure returns (address a) { + assembly ("memory-safe") { + a := calldataload(_PAYMENT_TOKEN_OFFSET) + } + } + + function _getPaymentMaxAmount() internal pure returns (uint256 a) { + assembly ("memory-safe") { + a := calldataload(_PAYMENT_MAX_AMOUNT_OFFSET) + } + } + + function _getCombinedGas() internal pure returns (uint256 a) { + assembly ("memory-safe") { + a := calldataload(_COMBINED_GAS_OFFSET) + } + } + + function _getExpiry() internal pure returns (uint256 a) { + assembly ("memory-safe") { + a := calldataload(_EXPIRY_OFFSET) + } + } + + function _getPaymentAmount() internal pure returns (uint256 a) { + assembly ("memory-safe") { + a := calldataload(_PAYMENT_AMOUNT_OFFSET) + } + } + + function _getPaymentRecipient() internal pure returns (address a) { + return address(bytes20(msg.data[_PAYMENT_RECIPIENT_OFFSET:_PAYMENT_RECIPIENT_OFFSET + 20])); + } + + function _getExecutionData() internal pure returns (bytes calldata data) { + assembly ("memory-safe") { + data.length := calldataload(_EXECUTION_DATA_OFFSET) + data.offset := add(_EXECUTION_DATA_OFFSET, 0x20) + } + } + + /// @dev Splits data containing [uint256(len), bytes(data), ...] into [uint256(len), bytes(data)] and [...] + /// @dev Modifies the memory pointer to point to the next + function _getNextBytes(CalldataPointer memory p) + internal + pure + returns (bytes calldata returnData) + { + uint256 o = p.offset; + assembly ("memory-safe") { + returnData.length := calldataload(o) + returnData.offset := add(o, 0x20) + mstore(p, add(o, add(returnData.length, 0x20))) + } + } + + function _parseFundData(bytes calldata data) + internal + pure + returns (address funder, bytes calldata sig, bytes[] calldata transfers) + { + // fundData = abi.encode(funder, funderSignature, encodedFundTransfers) + // This gives a calldata layout of: + // 0x00: funder (20 bytes, left padded to 32 bytes) + // 0x20: offset to funderSignature (32 bytes) - we skip this and assume its starts at 0x60 + // 0x40: offset to encodedFundTransfers (32 bytes) - we skip this and assume it starts at 0x60+funderSignature.length+0x20 + // 0x60: funderSignature length (32 bytes) + // ... + assembly ("memory-safe") { + funder := calldataload(data.offset) + sig.offset := add(data.offset, 0x80) + sig.length := calldataload(add(data.offset, 0x60)) + // transfers array starts after sig, but needs to account for padding + let sigLenWords := mul(div(add(sig.length, 31), 32), 32) + transfers.length := calldataload(add(sig.offset, sigLenWords)) + transfers.offset := add(add(sig.offset, sigLenWords), 0x20) + } + } +} diff --git a/src/vendor/layerzero/interfaces/IOAppCore.sol b/src/vendor/layerzero/interfaces/IOAppCore.sol deleted file mode 100644 index 4a87d7a2..00000000 --- a/src/vendor/layerzero/interfaces/IOAppCore.sol +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -import { - ILayerZeroEndpointV2 -} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; - -/** - * @title IOAppCore - */ -interface IOAppCore { - // Custom error messages - error OnlyPeer(uint32 eid, bytes32 sender); - error NoPeer(uint32 eid); - error InvalidEndpointCall(); - error InvalidDelegate(); - - // Event emitted when a peer (OApp) is set for a corresponding endpoint - event PeerSet(uint32 eid, bytes32 peer); - - /** - * @notice Retrieves the OApp version information. - * @return senderVersion The version of the OAppSender.sol contract. - * @return receiverVersion The version of the OAppReceiver.sol contract. - */ - function oAppVersion() external view returns (uint64 senderVersion, uint64 receiverVersion); - - /** - * @notice Retrieves the LayerZero endpoint associated with the OApp. - * @return iEndpoint The LayerZero endpoint as an interface. - */ - function endpoint() external view returns (ILayerZeroEndpointV2 iEndpoint); - - /** - * @notice Retrieves the peer (OApp) associated with a corresponding endpoint. - * @param _eid The endpoint ID. - * @return peer The peer address (OApp instance) associated with the corresponding endpoint. - */ - function peers(uint32 _eid) external view returns (bytes32 peer); - - /** - * @notice Sets the peer address (OApp instance) for a corresponding endpoint. - * @param _eid The endpoint ID. - * @param _peer The address of the peer to be associated with the corresponding endpoint. - */ - function setPeer(uint32 _eid, bytes32 _peer) external; - - /** - * @notice Sets the delegate address for the OApp Core. - * @param _delegate The address of the delegate to be set. - */ - function setDelegate(address _delegate) external; -} diff --git a/src/vendor/layerzero/interfaces/IOAppMsgInspector.sol b/src/vendor/layerzero/interfaces/IOAppMsgInspector.sol deleted file mode 100644 index 4d3c82d6..00000000 --- a/src/vendor/layerzero/interfaces/IOAppMsgInspector.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -/** - * @title IOAppMsgInspector - * @dev Interface for the OApp Message Inspector, allowing examination of message and options contents. - */ -interface IOAppMsgInspector { - // Custom error message for inspection failure - error InspectionFailed(bytes message, bytes options); - - /** - * @notice Allows the inspector to examine LayerZero message contents and optionally throw a revert if invalid. - * @param _message The message payload to be inspected. - * @param _options Additional options or parameters for inspection. - * @return valid A boolean indicating whether the inspection passed (true) or failed (false). - * - * @dev Optionally done as a revert, OR use the boolean provided to handle the failure. - */ - function inspect(bytes calldata _message, bytes calldata _options) - external - view - returns (bool valid); -} diff --git a/src/vendor/layerzero/interfaces/IOAppReceiver.sol b/src/vendor/layerzero/interfaces/IOAppReceiver.sol deleted file mode 100644 index 0fe4317f..00000000 --- a/src/vendor/layerzero/interfaces/IOAppReceiver.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import { - ILayerZeroReceiver, - Origin -} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroReceiver.sol"; - -interface IOAppReceiver is ILayerZeroReceiver { - /** - * @notice Indicates whether an address is an approved composeMsg sender to the Endpoint. - * @param _origin The origin information containing the source endpoint and sender address. - * - srcEid: The source chain endpoint ID. - * - sender: The sender address on the src chain. - * - nonce: The nonce of the message. - * @param _message The lzReceive payload. - * @param _sender The sender address. - * @return isSender Is a valid sender. - * - * @dev Applications can optionally choose to implement a separate composeMsg sender that is NOT the bridging layer. - * @dev The default sender IS the OAppReceiver implementer. - */ - function isComposeMsgSender(Origin calldata _origin, bytes calldata _message, address _sender) - external - view - returns (bool isSender); -} diff --git a/src/vendor/layerzero/mocks/EndpointV2Mock.sol b/src/vendor/layerzero/mocks/EndpointV2Mock.sol deleted file mode 100644 index 40042d7d..00000000 --- a/src/vendor/layerzero/mocks/EndpointV2Mock.sol +++ /dev/null @@ -1,418 +0,0 @@ -// SPDX-License-Identifier: LZBL-1.2 - -pragma solidity ^0.8.20; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -// @dev oz4/5 breaking change... Ownable constructor -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; - -import { - MessagingFee, - MessagingParams, - MessagingReceipt, - Origin, - ILayerZeroEndpointV2 -} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; -import { - ISendLib, - Packet -} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ISendLib.sol"; -import { - ILayerZeroReceiver -} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroReceiver.sol"; -import {Errors} from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/Errors.sol"; -import {GUID} from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/GUID.sol"; -import {Transfer} from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/Transfer.sol"; -import {MessagingChannel} from "@layerzerolabs/lz-evm-protocol-v2/contracts/MessagingChannel.sol"; -import {MessagingComposer} from "@layerzerolabs/lz-evm-protocol-v2/contracts/MessagingComposer.sol"; -import {MessageLibManager} from "@layerzerolabs/lz-evm-protocol-v2/contracts/MessageLibManager.sol"; -import {MessagingContext} from "@layerzerolabs/lz-evm-protocol-v2/contracts/MessagingContext.sol"; - -// LayerZero EndpointV2 is fully backward compatible with LayerZero Endpoint(V1), but it also supports additional -// features that Endpoint(V1) does not support now and may not in the future. We have also changed some terminology -// to clarify pre-existing language that might have been confusing. -// -// The following is a list of terminology changes: -// -chainId -> eid -// - Rationale: chainId was a term we initially used to describe an endpoint on a specific chain. Since -// LayerZero supports non-EVMs we could not map the classic EVM chainIds to the LayerZero chainIds, making it -// confusing for developers. With the addition of EndpointV2 and its backward compatible nature, we would have -// two chainIds per chain that has Endpoint(V1), further confusing developers. We have decided to change the -// name to Endpoint Id, or eid, for simplicity and clarity. -// -adapterParams -> options -// -userApplication -> oapp. Omnichain Application -// -srcAddress -> sender -// -dstAddress -> receiver -// - Rationale: The sender/receiver on EVM is the address. However, on non-EVM chains, the sender/receiver could -// represented as a public key, or some other identifier. The term sender/receiver is more generic -// -payload -> message. -// - Rationale: The term payload is used in the context of a packet, which is a combination of the message and GUID -contract EndpointV2Mock is - ILayerZeroEndpointV2, - MessagingChannel, - MessageLibManager, - MessagingComposer, - MessagingContext -{ - address public lzToken; - - mapping(address oapp => address delegate) public delegates; - - /// @param _eid the unique Endpoint Id for this deploy that all other Endpoints can use to send to it - // @dev oz4/5 breaking change... Ownable constructor - constructor(uint32 _eid, address _owner) Ownable(_owner) MessagingChannel(_eid) {} - - /// @dev MESSAGING STEP 0 - /// @notice This view function gives the application built on top of LayerZero the ability to requests a quote - /// with the same parameters as they would to send their message. Since the quotes are given on chain there is a - /// race condition in which the prices could change between the time the user gets their quote and the time they - /// submit their message. If the price moves up and the user doesn't send enough funds the transaction will revert, - /// if the price goes down the _refundAddress provided by the app will be refunded the difference. - /// @param _params the messaging parameters - /// @param _sender the sender of the message - function quote(MessagingParams calldata _params, address _sender) - external - view - returns (MessagingFee memory) - { - // lzToken must be set to support payInLzToken - if (_params.payInLzToken && lzToken == address(0x0)) revert Errors.LZ_LzTokenUnavailable(); - - // get the correct outbound nonce - uint64 nonce = outboundNonce[_sender][_params.dstEid][_params.receiver] + 1; - - // construct the packet with a GUID - Packet memory packet = Packet({ - nonce: nonce, - srcEid: eid, - sender: _sender, - dstEid: _params.dstEid, - receiver: _params.receiver, - guid: GUID.generate(nonce, eid, _sender, _params.dstEid, _params.receiver), - message: _params.message - }); - - // get the send library by sender and dst eid - // use _ to avoid variable shadowing - address _sendLibrary = getSendLibrary(_sender, _params.dstEid); - - return ISendLib(_sendLibrary).quote(packet, _params.options, _params.payInLzToken); - } - - /// @dev MESSAGING STEP 1 - OApp need to transfer the fees to the endpoint before sending the message - /// @param _params the messaging parameters - /// @param _refundAddress the address to refund both the native and lzToken - function send(MessagingParams calldata _params, address _refundAddress) - external - payable - sendContext(_params.dstEid, msg.sender) - returns (MessagingReceipt memory) - { - if (_params.payInLzToken && lzToken == address(0x0)) { - revert Errors.LZ_LzTokenUnavailable(); - } - - // send message - (MessagingReceipt memory receipt, address _sendLibrary) = _send(msg.sender, _params); - - // OApp can simulate with 0 native value it will fail with error including the required fee, which can be provided in the actual call - // this trick can be used to avoid the need to write the quote() function - // however, without the quote view function it will be hard to compose an oapp on chain - uint256 suppliedNative = _suppliedNative(); - uint256 suppliedLzToken = _suppliedLzToken(_params.payInLzToken); - _assertMessagingFee(receipt.fee, suppliedNative, suppliedLzToken); - - // handle lz token fees - _payToken(lzToken, receipt.fee.lzTokenFee, suppliedLzToken, _sendLibrary, _refundAddress); - - // handle native fees - _payNative(receipt.fee.nativeFee, suppliedNative, _sendLibrary, _refundAddress); - - return receipt; - } - - /// @dev internal function for sending the messages used by all external send methods - /// @param _sender the address of the application sending the message to the destination chain - /// @param _params the messaging parameters - function _send(address _sender, MessagingParams calldata _params) - internal - returns (MessagingReceipt memory, address) - { - // get the correct outbound nonce - uint64 latestNonce = _outbound(_sender, _params.dstEid, _params.receiver); - - // construct the packet with a GUID - Packet memory packet = Packet({ - nonce: latestNonce, - srcEid: eid, - sender: _sender, - dstEid: _params.dstEid, - receiver: _params.receiver, - guid: GUID.generate(latestNonce, eid, _sender, _params.dstEid, _params.receiver), - message: _params.message - }); - - // get the send library by sender and dst eid - address _sendLibrary = getSendLibrary(_sender, _params.dstEid); - - // messageLib always returns encodedPacket with guid - (MessagingFee memory fee, bytes memory encodedPacket) = - ISendLib(_sendLibrary).send(packet, _params.options, _params.payInLzToken); - - // Emit packet information for DVNs, Executors, and any other offchain infrastructure to only listen - // for this one event to perform their actions. - emit PacketSent(encodedPacket, _params.options, _sendLibrary); - - return (MessagingReceipt(packet.guid, latestNonce, fee), _sendLibrary); - } - - /// @dev MESSAGING STEP 2 - on the destination chain - /// @dev configured receive library verifies a message - /// @param _origin a struct holding the srcEid, nonce, and sender of the message - /// @param _receiver the receiver of the message - /// @param _payloadHash the payload hash of the message - function verify(Origin calldata _origin, address _receiver, bytes32 _payloadHash) external { - if (!isValidReceiveLibrary(_receiver, _origin.srcEid, msg.sender)) { - revert Errors.LZ_InvalidReceiveLibrary(); - } - - uint64 lazyNonce = lazyInboundNonce[_receiver][_origin.srcEid][_origin.sender]; - if (!_initializable(_origin, _receiver, lazyNonce)) { - revert Errors.LZ_PathNotInitializable(); - } - if (!_verifiable(_origin, _receiver, lazyNonce)) revert Errors.LZ_PathNotVerifiable(); - - // insert the message into the message channel - _inbound(_receiver, _origin.srcEid, _origin.sender, _origin.nonce, _payloadHash); - emit PacketVerified(_origin, _receiver, _payloadHash); - } - - /// @dev MESSAGING STEP 3 - the last step - /// @dev execute a verified message to the designated receiver - /// @dev the execution provides the execution context (caller, extraData) to the receiver. the receiver can optionally assert the caller and validate the untrusted extraData - /// @dev cant reentrant because the payload is cleared before execution - /// @param _origin the origin of the message - /// @param _receiver the receiver of the message - /// @param _guid the guid of the message - /// @param _message the message - /// @param _extraData the extra data provided by the executor. this data is untrusted and should be validated. - function lzReceive( - Origin calldata _origin, - address _receiver, - bytes32 _guid, - bytes calldata _message, - bytes calldata _extraData - ) external payable { - // clear the payload first to prevent reentrancy, and then execute the message - _clearPayload( - _receiver, - _origin.srcEid, - _origin.sender, - _origin.nonce, - abi.encodePacked(_guid, _message) - ); - ILayerZeroReceiver(_receiver) - .lzReceive{value: msg.value}(_origin, _guid, _message, msg.sender, _extraData); - emit PacketDelivered(_origin, _receiver); - } - - /// @param _origin the origin of the message - /// @param _receiver the receiver of the message - /// @param _guid the guid of the message - /// @param _message the message - /// @param _extraData the extra data provided by the executor. - /// @param _reason the reason for failure - function lzReceiveAlert( - Origin calldata _origin, - address _receiver, - bytes32 _guid, - uint256 _gas, - uint256 _value, - bytes calldata _message, - bytes calldata _extraData, - bytes calldata _reason - ) external { - emit LzReceiveAlert( - _receiver, msg.sender, _origin, _guid, _gas, _value, _message, _extraData, _reason - ); - } - - /// @dev Oapp uses this interface to clear a message. - /// @dev this is a PULL mode versus the PUSH mode of lzReceive - /// @dev the cleared message can be ignored by the app (effectively burnt) - /// @dev authenticated by oapp - /// @param _origin the origin of the message - /// @param _guid the guid of the message - /// @param _message the message - function clear(address _oapp, Origin calldata _origin, bytes32 _guid, bytes calldata _message) - external - { - _assertAuthorized(_oapp); - - bytes memory payload = abi.encodePacked(_guid, _message); - _clearPayload(_oapp, _origin.srcEid, _origin.sender, _origin.nonce, payload); - emit PacketDelivered(_origin, _oapp); - } - - /// @dev allows reconfiguration to recover from wrong configurations - /// @dev users should never approve the EndpointV2 contract to spend their non-layerzero tokens - /// @dev override this function if the endpoint is charging ERC20 tokens as native - /// @dev only owner - /// @param _lzToken the new layer zero token address - function setLzToken(address _lzToken) public virtual onlyOwner { - lzToken = _lzToken; - emit LzTokenSet(_lzToken); - } - - /// @dev recover the token sent to this contract by mistake - /// @dev only owner - /// @param _token the token to recover. if 0x0 then it is native token - /// @param _to the address to send the token to - /// @param _amount the amount to send - function recoverToken(address _token, address _to, uint256 _amount) external onlyOwner { - Transfer.nativeOrToken(_token, _to, _amount); - } - - /// @dev handling token payments on endpoint. the sender must approve the endpoint to spend the token - /// @dev internal function - /// @param _token the token to pay - /// @param _required the amount required - /// @param _supplied the amount supplied - /// @param _receiver the receiver of the token - function _payToken( - address _token, - uint256 _required, - uint256 _supplied, - address _receiver, - address _refundAddress - ) internal { - if (_required > 0) { - Transfer.token(_token, _receiver, _required); - } - if (_required < _supplied) { - unchecked { - // refund the excess - Transfer.token(_token, _refundAddress, _supplied - _required); - } - } - } - - /// @dev handling native token payments on endpoint - /// @dev override this if the endpoint is charging ERC20 tokens as native - /// @dev internal function - /// @param _required the amount required - /// @param _supplied the amount supplied - /// @param _receiver the receiver of the native token - /// @param _refundAddress the address to refund the excess to - function _payNative( - uint256 _required, - uint256 _supplied, - address _receiver, - address _refundAddress - ) internal virtual { - if (_required > 0) { - Transfer.native(_receiver, _required); - } - if (_required < _supplied) { - unchecked { - // refund the excess - Transfer.native(_refundAddress, _supplied - _required); - } - } - } - - /// @dev get the balance of the lzToken as the supplied lzToken fee if payInLzToken is true - function _suppliedLzToken(bool _payInLzToken) internal view returns (uint256 supplied) { - if (_payInLzToken) { - supplied = IERC20(lzToken).balanceOf(address(this)); - - // if payInLzToken is true, the supplied fee must be greater than 0 to prevent a race condition - // in which an oapp sending a message with lz token and the lz token is set to a new token between the tx - // being sent and the tx being mined. if the required lz token fee is 0 and the old lz token would be - // locked in the contract instead of being refunded - if (supplied == 0) revert Errors.LZ_ZeroLzTokenFee(); - } - } - - /// @dev override this if the endpoint is charging ERC20 tokens as native - function _suppliedNative() internal view virtual returns (uint256) { - return msg.value; - } - - /// @dev Assert the required fees and the supplied fees are enough - function _assertMessagingFee( - MessagingFee memory _required, - uint256 _suppliedNativeFee, - uint256 _suppliedLzTokenFee - ) internal pure { - if (_required.nativeFee > _suppliedNativeFee || _required.lzTokenFee > _suppliedLzTokenFee) - { - revert Errors.LZ_InsufficientFee( - _required.nativeFee, _suppliedNativeFee, _required.lzTokenFee, _suppliedLzTokenFee - ); - } - } - - /// @dev override this if the endpoint is charging ERC20 tokens as native - /// @return 0x0 if using native. otherwise the address of the native ERC20 token - function nativeToken() external view virtual returns (address) { - return address(0x0); - } - - /// @notice delegate is authorized by the oapp to configure anything in layerzero - function setDelegate(address _delegate) external { - delegates[msg.sender] = _delegate; - emit DelegateSet(msg.sender, _delegate); - } - - // ========================= Internal ========================= - function _initializable(Origin calldata _origin, address _receiver, uint64 _lazyInboundNonce) - internal - view - returns (bool) - { - return _lazyInboundNonce > 0 // allowInitializePath already checked - || ILayerZeroReceiver(_receiver).allowInitializePath(_origin); - } - - /// @dev bytes(0) payloadHash can never be submitted - function _verifiable(Origin calldata _origin, address _receiver, uint64 _lazyInboundNonce) - internal - view - returns (bool) - { - return _origin.nonce > _lazyInboundNonce // either initializing an empty slot or reverifying - || inboundPayloadHash[_receiver][_origin.srcEid][_origin.sender][_origin.nonce] - != EMPTY_PAYLOAD_HASH; // only allow reverifying if it hasn't been executed - } - - /// @dev assert the caller to either be the oapp or the delegate - function _assertAuthorized(address _oapp) - internal - view - override(MessagingChannel, MessageLibManager) - { - if (msg.sender != _oapp && msg.sender != delegates[_oapp]) { - revert Errors.LZ_Unauthorized(); - } - } - - // ========================= VIEW FUNCTIONS FOR OFFCHAIN ONLY ========================= - // Not involved in any state transition function. - // ==================================================================================== - function initializable(Origin calldata _origin, address _receiver) - external - view - returns (bool) - { - return _initializable( - _origin, _receiver, lazyInboundNonce[_receiver][_origin.srcEid][_origin.sender] - ); - } - - function verifiable(Origin calldata _origin, address _receiver) external view returns (bool) { - return _verifiable( - _origin, _receiver, lazyInboundNonce[_receiver][_origin.srcEid][_origin.sender] - ); - } -} diff --git a/src/vendor/layerzero/oapp/OApp.sol b/src/vendor/layerzero/oapp/OApp.sol deleted file mode 100644 index a4d9576a..00000000 --- a/src/vendor/layerzero/oapp/OApp.sol +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -// @dev Import the 'MessagingFee' and 'MessagingReceipt' so it's exposed to OApp implementers -// solhint-disable-next-line no-unused-import -import {OAppSender, MessagingFee, MessagingReceipt} from "./OAppSender.sol"; -// @dev Import the 'Origin' so it's exposed to OApp implementers -// solhint-disable-next-line no-unused-import -import {OAppReceiver, Origin} from "./OAppReceiver.sol"; -import {OAppCore} from "./OAppCore.sol"; - -/** - * @title OApp - * @dev Abstract contract serving as the base for OApp implementation, combining OAppSender and OAppReceiver functionality. - */ -abstract contract OApp is OAppSender, OAppReceiver { - /** - * @dev Constructor to initialize the OApp with the provided owner. - * @param _delegate The delegate capable of making OApp configurations inside of the endpoint. - */ - constructor(address _delegate) OAppCore(_delegate) {} - - /** - * @notice Retrieves the OApp version information. - * @return senderVersion The version of the OAppSender.sol implementation. - * @return receiverVersion The version of the OAppReceiver.sol implementation. - */ - function oAppVersion() - public - pure - virtual - override(OAppSender, OAppReceiver) - returns (uint64 senderVersion, uint64 receiverVersion) - { - return (SENDER_VERSION, RECEIVER_VERSION); - } -} diff --git a/src/vendor/layerzero/oapp/OAppCore.sol b/src/vendor/layerzero/oapp/OAppCore.sol deleted file mode 100644 index 3eff1be5..00000000 --- a/src/vendor/layerzero/oapp/OAppCore.sol +++ /dev/null @@ -1,104 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {IOAppCore, ILayerZeroEndpointV2} from "../interfaces/IOAppCore.sol"; - -/** - * @title OAppCore - * @dev Abstract contract implementing the IOAppCore interface with basic OApp configurations. - */ -abstract contract OAppCore is IOAppCore, Ownable { - // The LayerZero endpoint associated with the given OApp - ILayerZeroEndpointV2 public endpoint; - - // Mapping to store peers associated with corresponding endpoints - mapping(uint32 eid => bytes32 peer) internal _peers; - - /** - * @dev Constructor to initialize the OAppCore with the provided delegate. - * @param _delegate The delegate capable of making OApp configurations inside of the endpoint. - * - * @dev The delegate typically should be set as the owner of the contract. - */ - constructor(address _delegate) { - if (_delegate == address(0)) revert InvalidDelegate(); - _transferOwnership(_delegate); - } - - /** - * @notice Sets the LayerZero endpoint for this OApp. - * @param _endpoint The address of the LayerZero endpoint. - * @dev Can only be called by the owner. - * @dev Should be called immediately after deployment. - * Delegate is set to the owner of the OAppCore contract. - */ - function setEndpoint(address _endpoint) external onlyOwner { - if (_endpoint == address(0)) revert InvalidDelegate(); - endpoint = ILayerZeroEndpointV2(_endpoint); - endpoint.setDelegate(msg.sender); - } - - /** - * @notice Sets the peer address (OApp instance) for a corresponding endpoint. - * @param _eid The endpoint ID. - * @param _peer The address of the peer to be associated with the corresponding endpoint. - * - * @dev Only the owner/admin of the OApp can call this function. - * @dev Indicates that the peer is trusted to send LayerZero messages to this OApp. - * @dev Set this to bytes32(0) to remove the peer address. - * @dev Peer is a bytes32 to accommodate non-evm chains. - */ - function setPeer(uint32 _eid, bytes32 _peer) public virtual onlyOwner { - _setPeer(_eid, _peer); - } - - /** - * @notice Sets the peer address (OApp instance) for a corresponding endpoint. - * @param _eid The endpoint ID. - * @param _peer The address of the peer to be associated with the corresponding endpoint. - * - * @dev Indicates that the peer is trusted to send LayerZero messages to this OApp. - * @dev Set this to bytes32(0) to remove the peer address. - * @dev Peer is a bytes32 to accommodate non-evm chains. - */ - function _setPeer(uint32 _eid, bytes32 _peer) internal virtual { - _peers[_eid] = _peer; - emit PeerSet(_eid, _peer); - } - - /** - * @notice Internal function to get the peer address associated with a specific endpoint; reverts if NOT set. - * ie. the peer is set to bytes32(0). - * @param _eid The endpoint ID. - * @return peer The address of the peer associated with the specified endpoint. - */ - function _getPeerOrRevert(uint32 _eid) internal view virtual returns (bytes32) { - bytes32 peer = peers(_eid); - if (peer == bytes32(0)) revert NoPeer(_eid); - return peer; - } - - /** - * @notice Retrieves the peer (OApp instance) for a corresponding endpoint. - * @param _eid The endpoint ID. - * @return peer The address of the peer associated with the specified endpoint. - * @dev This function is virtual to allow overriding in derived contracts. - */ - function peers(uint32 _eid) public view virtual returns (bytes32 peer) { - return _peers[_eid]; - } - - /** - * @notice Sets the delegate address for the OApp. - * @param _delegate The address of the delegate to be set. - * - * @dev Only the owner/admin of the OApp can call this function. - * @dev Provides the ability for a delegate to set configs, on behalf of the OApp, directly on the Endpoint contract. - */ - function setDelegate(address _delegate) public onlyOwner { - if (address(endpoint) == address(0)) revert InvalidDelegate(); - endpoint.setDelegate(_delegate); - } -} diff --git a/src/vendor/layerzero/oapp/OAppReceiver.sol b/src/vendor/layerzero/oapp/OAppReceiver.sol deleted file mode 100644 index d3c34ad0..00000000 --- a/src/vendor/layerzero/oapp/OAppReceiver.sol +++ /dev/null @@ -1,143 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -import {IOAppReceiver, Origin} from "../interfaces/IOAppReceiver.sol"; -import {OAppCore} from "./OAppCore.sol"; - -/** - * @title OAppReceiver - * @dev Abstract contract implementing the ILayerZeroReceiver interface and extending OAppCore for OApp receivers. - */ -abstract contract OAppReceiver is IOAppReceiver, OAppCore { - // Custom error message for when the caller is not the registered endpoint/ - error OnlyEndpoint(address addr); - - // @dev The version of the OAppReceiver implementation. - // @dev Version is bumped when changes are made to this contract. - uint64 internal constant RECEIVER_VERSION = 2; - - /** - * @notice Retrieves the OApp version information. - * @return senderVersion The version of the OAppSender.sol contract. - * @return receiverVersion The version of the OAppReceiver.sol contract. - * - * @dev Providing 0 as the default for OAppSender version. Indicates that the OAppSender is not implemented. - * ie. this is a RECEIVE only OApp. - * @dev If the OApp uses both OAppSender and OAppReceiver, then this needs to be override returning the correct versions. - */ - function oAppVersion() - public - view - virtual - returns (uint64 senderVersion, uint64 receiverVersion) - { - return (0, RECEIVER_VERSION); - } - - /** - * @notice Indicates whether an address is an approved composeMsg sender to the Endpoint. - * @dev _origin The origin information containing the source endpoint and sender address. - * - srcEid: The source chain endpoint ID. - * - sender: The sender address on the src chain. - * - nonce: The nonce of the message. - * @dev _message The lzReceive payload. - * @param _sender The sender address. - * @return isSender Is a valid sender. - * - * @dev Applications can optionally choose to implement separate composeMsg senders that are NOT the bridging layer. - * @dev The default sender IS the OAppReceiver implementer. - */ - function isComposeMsgSender( - Origin calldata, /*_origin*/ - bytes calldata, /*_message*/ - address _sender - ) - public - view - virtual - returns (bool) - { - return _sender == address(this); - } - - /** - * @notice Checks if the path initialization is allowed based on the provided origin. - * @param origin The origin information containing the source endpoint and sender address. - * @return Whether the path has been initialized. - * - * @dev This indicates to the endpoint that the OApp has enabled msgs for this particular path to be received. - * @dev This defaults to assuming if a peer has been set, its initialized. - * Can be overridden by the OApp if there is other logic to determine this. - */ - function allowInitializePath(Origin calldata origin) public view virtual returns (bool) { - return peers(origin.srcEid) == origin.sender; - } - - /** - * @notice Retrieves the next nonce for a given source endpoint and sender address. - * @dev _srcEid The source endpoint ID. - * @dev _sender The sender address. - * @return nonce The next nonce. - * - * @dev The path nonce starts from 1. If 0 is returned it means that there is NO nonce ordered enforcement. - * @dev Is required by the off-chain executor to determine the OApp expects msg execution is ordered. - * @dev This is also enforced by the OApp. - * @dev By default this is NOT enabled. ie. nextNonce is hardcoded to return 0. - */ - function nextNonce( - uint32, - /*_srcEid*/ - bytes32 /*_sender*/ - ) - public - view - virtual - returns (uint64 nonce) - { - return 0; - } - - /** - * @dev Entry point for receiving messages or packets from the endpoint. - * @param _origin The origin information containing the source endpoint and sender address. - * - srcEid: The source chain endpoint ID. - * - sender: The sender address on the src chain. - * - nonce: The nonce of the message. - * @param _guid The unique identifier for the received LayerZero message. - * @param _message The payload of the received message. - * @param _executor The address of the executor for the received message. - * @param _extraData Additional arbitrary data provided by the corresponding executor. - * - * @dev Entry point for receiving msg/packet from the LayerZero endpoint. - */ - function lzReceive( - Origin calldata _origin, - bytes32 _guid, - bytes calldata _message, - address _executor, - bytes calldata _extraData - ) public payable virtual { - // Ensures that only the endpoint can attempt to lzReceive() messages to this OApp. - if (address(endpoint) != msg.sender) revert OnlyEndpoint(msg.sender); - - // Ensure that the sender matches the expected peer for the source endpoint. - if (_getPeerOrRevert(_origin.srcEid) != _origin.sender) { - revert OnlyPeer(_origin.srcEid, _origin.sender); - } - - // Call the internal OApp implementation of lzReceive. - _lzReceive(_origin, _guid, _message, _executor, _extraData); - } - - /** - * @dev Internal function to implement lzReceive logic without needing to copy the basic parameter validation. - */ - function _lzReceive( - Origin calldata _origin, - bytes32 _guid, - bytes calldata _message, - address _executor, - bytes calldata _extraData - ) internal virtual; -} diff --git a/src/vendor/layerzero/oapp/OAppSender.sol b/src/vendor/layerzero/oapp/OAppSender.sol deleted file mode 100644 index 6fdf8ef4..00000000 --- a/src/vendor/layerzero/oapp/OAppSender.sol +++ /dev/null @@ -1,136 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { - MessagingParams, - MessagingFee, - MessagingReceipt -} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; -import {OAppCore} from "./OAppCore.sol"; - -/** - * @title OAppSender - * @dev Abstract contract implementing the OAppSender functionality for sending messages to a LayerZero endpoint. - */ -abstract contract OAppSender is OAppCore { - using SafeERC20 for IERC20; - - // Custom error messages - error NotEnoughNative(uint256 msgValue); - error LzTokenUnavailable(); - - // @dev The version of the OAppSender implementation. - // @dev Version is bumped when changes are made to this contract. - uint64 internal constant SENDER_VERSION = 1; - - /** - * @notice Retrieves the OApp version information. - * @return senderVersion The version of the OAppSender.sol contract. - * @return receiverVersion The version of the OAppReceiver.sol contract. - * - * @dev Providing 0 as the default for OAppReceiver version. Indicates that the OAppReceiver is not implemented. - * ie. this is a SEND only OApp. - * @dev If the OApp uses both OAppSender and OAppReceiver, then this needs to be override returning the correct versions - */ - function oAppVersion() - public - view - virtual - returns (uint64 senderVersion, uint64 receiverVersion) - { - return (SENDER_VERSION, 0); - } - - /** - * @dev Internal function to interact with the LayerZero EndpointV2.quote() for fee calculation. - * @param _dstEid The destination endpoint ID. - * @param _message The message payload. - * @param _options Additional options for the message. - * @param _payInLzToken Flag indicating whether to pay the fee in LZ tokens. - * @return fee The calculated MessagingFee for the message. - * - nativeFee: The native fee for the message. - * - lzTokenFee: The LZ token fee for the message. - */ - function _quote( - uint32 _dstEid, - bytes memory _message, - bytes memory _options, - bool _payInLzToken - ) internal view virtual returns (MessagingFee memory fee) { - return endpoint.quote( - MessagingParams(_dstEid, _getPeerOrRevert(_dstEid), _message, _options, _payInLzToken), - address(this) - ); - } - - /** - * @dev Internal function to interact with the LayerZero EndpointV2.send() for sending a message. - * @param _dstEid The destination endpoint ID. - * @param _message The message payload. - * @param _options Additional options for the message. - * @param _fee The calculated LayerZero fee for the message. - * - nativeFee: The native fee. - * - lzTokenFee: The lzToken fee. - * @param _refundAddress The address to receive any excess fee values sent to the endpoint. - * @return receipt The receipt for the sent message. - * - guid: The unique identifier for the sent message. - * - nonce: The nonce of the sent message. - * - fee: The LayerZero fee incurred for the message. - */ - function _lzSend( - uint32 _dstEid, - bytes memory _message, - bytes memory _options, - MessagingFee memory _fee, - address _refundAddress - ) internal virtual returns (MessagingReceipt memory receipt) { - // @dev Push corresponding fees to the endpoint, any excess is sent back to the _refundAddress from the endpoint. - uint256 messageValue = _payNative(_fee.nativeFee); - if (_fee.lzTokenFee > 0) _payLzToken(_fee.lzTokenFee); - - return endpoint. - // solhint-disable-next-line check-send-result - send{ - value: messageValue - }( - MessagingParams( - _dstEid, _getPeerOrRevert(_dstEid), _message, _options, _fee.lzTokenFee > 0 - ), - _refundAddress - ); - } - - /** - * @dev Internal function to pay the native fee associated with the message. - * @param _nativeFee The native fee to be paid. - * @return nativeFee The amount of native currency paid. - * - * @dev If the OApp needs to initiate MULTIPLE LayerZero messages in a single transaction, - * this will need to be overridden because msg.value would contain multiple lzFees. - * @dev Should be overridden in the event the LayerZero endpoint requires a different native currency. - * @dev Some EVMs use an ERC20 as a method for paying transactions/gasFees. - * @dev The endpoint is EITHER/OR, ie. it will NOT support both types of native payment at a time. - */ - function _payNative(uint256 _nativeFee) internal virtual returns (uint256 nativeFee) { - if (msg.value != _nativeFee) revert NotEnoughNative(msg.value); - return _nativeFee; - } - - /** - * @dev Internal function to pay the LZ token fee associated with the message. - * @param _lzTokenFee The LZ token fee to be paid. - * - * @dev If the caller is trying to pay in the specified lzToken, then the lzTokenFee is passed to the endpoint. - * @dev Any excess sent, is passed back to the specified _refundAddress in the _lzSend(). - */ - function _payLzToken(uint256 _lzTokenFee) internal virtual { - // @dev Cannot cache the token because it is not immutable in the endpoint. - address lzToken = endpoint.lzToken(); - if (lzToken == address(0)) revert LzTokenUnavailable(); - - // Pay LZ token fee by sending tokens to the endpoint. - IERC20(lzToken).safeTransferFrom(msg.sender, address(endpoint), _lzTokenFee); - } -} diff --git a/test/Account.t.sol b/test/Account.t.sol index 87c007d9..21c7c36e 100644 --- a/test/Account.t.sol +++ b/test/Account.t.sol @@ -419,49 +419,4 @@ contract AccountTest is BaseTest { uint256 keysCount137 = IthacaAccount(eoaAddress).keyCount(); assertEq(keysCount137, 2, "Keys should be added on chain 137"); } - - function testContextKeyHash() public { - DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); - - PassKey memory key = _randomPassKey(); - key.k.isSuperAdmin = true; - - vm.prank(d.eoa); - d.d.authorize(key.k); - - // Orchestrator workflow - vm.prank(d.d.ORCHESTRATOR()); - ERC7821.Call[] memory calls = new ERC7821.Call[](1); - calls[0].data = abi.encodeWithSelector(this.targetFunctionContextKeyHash.selector); - calls[0].to = address(this); - calls[0].value = 0; - bytes memory opData = abi.encode(key.keyHash); - bytes memory executionData = abi.encode(calls, opData); - d.d.execute(_ERC7821_BATCH_EXECUTION_MODE, executionData); - - assertEq(contextKeyHash, key.keyHash, "Context key hash mismatch orchestrator workflow"); - - // Reset context key hash - contextKeyHash = bytes32(0); - - // Workflow with opData - uint256 nonce = d.d.getNonce(0); - bytes memory signature = _sig(key, d.d.computeDigest(calls, nonce)); - opData = abi.encodePacked(nonce, signature); - executionData = abi.encode(calls, opData); - d.d.execute(_ERC7821_BATCH_EXECUTION_MODE, executionData); - - assertEq(contextKeyHash, key.keyHash, "Context key hash mismatch orchestrator workflow"); - - // Reset context key hash - contextKeyHash = bytes32(0); - - // Simple workflow without opData (self-execution) - bytes memory emptyOpData; - executionData = abi.encode(calls, emptyOpData); - vm.prank(address(d.d)); - d.d.execute(_ERC7821_BATCH_EXECUTION_MODE, executionData); - - assertEq(contextKeyHash, bytes32(0), "Context key hash should be zero for self-execution without opData"); - } } diff --git a/test/Base.t.sol b/test/Base.t.sol index 87e82660..aa0d05ad 100644 --- a/test/Base.t.sol +++ b/test/Base.t.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.4; import "./utils/SoladyTest.sol"; import {EIP7702Proxy} from "solady/accounts/EIP7702Proxy.sol"; import {LibEIP7702} from "solady/accounts/LibEIP7702.sol"; -import {ERC7821} from "solady/accounts/ERC7821.sol"; import {LibERC7579} from "solady/accounts/LibERC7579.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {EfficientHashLib} from "solady/utils/EfficientHashLib.sol"; @@ -24,6 +23,8 @@ import {IOrchestrator} from "../src/interfaces/IOrchestrator.sol"; import {Simulator} from "../src/Simulator.sol"; import {ICommon} from "../src/interfaces/ICommon.sol"; +import {ERC7821Ithaca as ERC7821} from "../src/libraries/ERC7821Ithaca.sol"; + contract BaseTest is SoladyTest { using LibRLP for LibRLP.List; @@ -34,7 +35,6 @@ contract BaseTest is SoladyTest { EIP7702Proxy eip7702Proxy; TargetFunctionPayload[] targetFunctionPayloads; Simulator simulator; - bytes32 contextKeyHash; struct TargetFunctionPayload { address by; @@ -56,6 +56,9 @@ contract BaseTest is SoladyTest { bytes32 internal constant _ERC7821_BATCH_EXECUTION_MODE = 0x0100000000007821000100000000000000000000000000000000000000000000; + bytes32 internal constant _ERC7821_BATCH_SANS_TO_EXECUTION_MODE = + 0x0100000000007821000300000000000000000000000000000000000000000000; + bytes32 internal constant _ERC7579_DELEGATE_CALL_MODE = 0xff00000000000000000000000000000000000000000000000000000000000000; @@ -95,10 +98,6 @@ contract BaseTest is SoladyTest { targetFunctionPayloads.push(TargetFunctionPayload(msg.sender, msg.value, data)); } - function targetFunctionContextKeyHash() public payable { - contextKeyHash = IthacaAccount(payable(msg.sender)).getContextKeyHash(); - } - function _setEIP7702Delegation(address eoa) internal { vm.etch(eoa, abi.encodePacked(hex"ef0100", address(account))); } diff --git a/test/Benchmark.t.sol b/test/Benchmark.t.sol index 2ce04524..e146fa43 100644 --- a/test/Benchmark.t.sol +++ b/test/Benchmark.t.sol @@ -120,8 +120,9 @@ contract BenchmarkTest is BaseTest { token0, token1, 1 ether, 1 ether, 1, 1, address(this), block.timestamp + 999 ); - IStakeManager(_ERC4337_ENTRYPOINT_V06_ADDR) - .depositTo{value: 1 ether}(_PIMLICO_PAYMASTER_V06); + IStakeManager(_ERC4337_ENTRYPOINT_V06_ADDR).depositTo{value: 1 ether}( + _PIMLICO_PAYMASTER_V06 + ); IStakeManager(_ERC4337_ENTRYPOINT_ADDR).depositTo{value: 1 ether}(_PIMLICO_PAYMASTER_V07); (paymasterSigner, paymasterPrivateKey) = makeAddrAndKey(""); @@ -1518,7 +1519,7 @@ contract BenchmarkTest is BaseTest { } } - function testERC20Transfer_IthacaAccount() public { + function testERC20Transfer_IthacaAccount1() public { DelegatedEOA[] memory delegatedEOAs = _createIthacaAccount(1); bytes memory payload = _transferExecutionData(address(paymentToken), address(0xbabe), 1 ether); @@ -1714,7 +1715,7 @@ contract BenchmarkTest is BaseTest { ) internal view returns (bytes[] memory) { bytes[] memory encodedIntents = new bytes[](delegatedEOAs.length); for (uint256 i = 0; i < delegatedEOAs.length; i++) { - Orchestrator.Intent memory u; + Intent memory u; u.eoa = delegatedEOAs[i].eoa; u.nonce = 0; u.combinedGas = 1000000; @@ -1750,14 +1751,14 @@ contract BenchmarkTest is BaseTest { _paymentType == PaymentType.APP_SPONSOR || _paymentType == PaymentType.APP_SPONSOR_ERC20 ) { - bytes32 digest = oc.computeDigest(u); + bytes32 digest = computeDigest(u); bytes32 signatureDigest = appSponsor.computeSignatureDigest(digest); u.paymentSignature = _eoaSig(paymasterPrivateKey, signatureDigest); } u.signature = _sig(delegatedEOAs[i], u); - encodedIntents[i] = abi.encodePacked(abi.encode(u), junk); + encodedIntents[i] = abi.encodePacked(encodeIntent(u), junk); } return encodedIntents; @@ -1785,7 +1786,7 @@ contract BenchmarkTest is BaseTest { d.d.setSpendLimit(k.keyHash, address(0), GuardedExecutor.SpendPeriod.Hour, 1 ether); vm.stopPrank(); - Orchestrator.Intent memory u; + Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.combinedGas = 1000000; @@ -1797,7 +1798,7 @@ contract BenchmarkTest is BaseTest { u.signature = _sig(k, u); bytes[] memory encodedIntents = new bytes[](1); - encodedIntents[0] = abi.encode(u); + encodedIntents[0] = encodeIntent(u); oc.execute(encodedIntents); vm.snapshotGasLastCall("testERC20Transfer_IthacaAccountWithSpendLimits"); diff --git a/test/Escrow.t.sol b/test/Escrow.t.sol index 57a6e8c0..ce105e8a 100644 --- a/test/Escrow.t.sol +++ b/test/Escrow.t.sol @@ -723,6 +723,80 @@ contract EscrowTest is BaseTest { assertEq(uint8(escrow.statuses(escrowId3)), uint8(IEscrow.EscrowStatus.CREATED)); } + // ========== Early Refund Functionality Tests ========== + + function testEarlyRefundFunctionality() public { + // Test 1: Recipient can trigger early refund for both parties + IEscrow.Escrow memory escrowData = _createEscrowData(1000, 800); + bytes32 escrowId = _createAndFundEscrow(escrowData); + bytes32[] memory escrowIds = new bytes32[](1); + escrowIds[0] = escrowId; + + uint256 depositorBalanceBefore = token.balanceOf(depositor); + uint256 recipientBalanceBefore = token.balanceOf(recipient); + + vm.prank(recipient); + escrow.refund(escrowIds); + + assertEq(token.balanceOf(depositor) - depositorBalanceBefore, 800); // Depositor gets refundAmount + assertEq(token.balanceOf(recipient) - recipientBalanceBefore, 200); // Recipient gets escrowAmount - refundAmount + assertEq(uint256(escrow.statuses(escrowId)), uint256(IEscrow.EscrowStatus.FINALIZED)); + + // Test 2: Recipient can trigger early refund for themselves only + escrowData.salt = bytes12(uint96(2)); + escrowId = _createAndFundEscrow(escrowData); + escrowIds[0] = escrowId; + + recipientBalanceBefore = token.balanceOf(recipient); + + vm.prank(recipient); + escrow.refundRecipient(escrowIds); + + assertEq(token.balanceOf(recipient) - recipientBalanceBefore, 200); // Recipient gets escrowAmount - refundAmount + assertEq(uint256(escrow.statuses(escrowId)), uint256(IEscrow.EscrowStatus.REFUND_RECIPIENT)); + + // Test 3: Depositor can trigger early refund for themselves only + escrowData.salt = bytes12(uint96(3)); + escrowId = _createAndFundEscrow(escrowData); + escrowIds[0] = escrowId; + + depositorBalanceBefore = token.balanceOf(depositor); + + vm.prank(depositor); + escrow.refundDepositor(escrowIds); + + assertEq(token.balanceOf(depositor) - depositorBalanceBefore, 800); // Depositor gets refundAmount + assertEq(uint256(escrow.statuses(escrowId)), uint256(IEscrow.EscrowStatus.REFUND_DEPOSIT)); + + // Test 4: Unauthorized users cannot trigger early refunds + escrowData.salt = bytes12(uint96(4)); + escrowId = _createAndFundEscrow(escrowData); + escrowIds[0] = escrowId; + + vm.expectRevert(bytes4(keccak256("RefundInvalid()"))); + vm.prank(randomUser); + escrow.refund(escrowIds); + + vm.expectRevert(bytes4(keccak256("RefundInvalid()"))); + vm.prank(attacker); + escrow.refundDepositor(escrowIds); + + vm.expectRevert(bytes4(keccak256("RefundInvalid()"))); + vm.prank(randomUser); + escrow.refundRecipient(escrowIds); + + // Test 5: Normal timeout refunds still work after early refunds + depositorBalanceBefore = token.balanceOf(depositor); + recipientBalanceBefore = token.balanceOf(recipient); + + vm.warp(block.timestamp + 2 hours); + vm.prank(randomUser); + escrow.refund(escrowIds); + + assertEq(token.balanceOf(depositor) - depositorBalanceBefore, 800); // Depositor gets refundAmount + assertEq(token.balanceOf(recipient) - recipientBalanceBefore, 200); // Recipient gets escrowAmount - refundAmount + } + // ========== Helper Functions ========== function _createEscrowData(uint256 escrowAmount, uint256 refundAmount) diff --git a/test/GuardedExecutor.t.sol b/test/GuardedExecutor.t.sol index a30c941e..d41e341f 100644 --- a/test/GuardedExecutor.t.sol +++ b/test/GuardedExecutor.t.sol @@ -25,7 +25,7 @@ contract GuardedExecutorTest is BaseTest { vm.prank(d.eoa); d.d.authorize(k.k); - Orchestrator.Intent memory u; + Intent memory u; u.eoa = d.eoa; u.combinedGas = 10000000; @@ -38,7 +38,8 @@ contract GuardedExecutorTest is BaseTest { u.executionData = abi.encode(calls); u.signature = _sig(k, u); assertEq( - oc.execute(abi.encode(u)), bytes4(keccak256("UnauthorizedCall(bytes32,address,bytes)")) + oc.execute(encodeIntent(u)), + bytes4(keccak256("UnauthorizedCall(bytes32,address,bytes)")) ); // Set the call checker. bytes32 forKeyHash = _randomChance(2) ? k.keyHash : _ANY_KEYHASH; @@ -56,7 +57,8 @@ contract GuardedExecutorTest is BaseTest { u.executionData = abi.encode(calls); u.signature = _sig(k, u); assertEq( - oc.execute(abi.encode(u)), bytes4(keccak256("UnauthorizedCall(bytes32,address,bytes)")) + oc.execute(encodeIntent(u)), + bytes4(keccak256("UnauthorizedCall(bytes32,address,bytes)")) ); // Try, now with the checker configured to authorize the call.. @@ -66,7 +68,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(abi.encode(u)), bytes4(0)); + assertEq(oc.execute(encodeIntent(u)), bytes4(0)); assertEq(counter.counter(), 1); // Try, now with the checker removed. @@ -79,7 +81,8 @@ contract GuardedExecutorTest is BaseTest { u.executionData = abi.encode(calls); u.signature = _sig(k, u); assertEq( - oc.execute(abi.encode(u)), bytes4(keccak256("UnauthorizedCall(bytes32,address,bytes)")) + oc.execute(encodeIntent(u)), + bytes4(keccak256("UnauthorizedCall(bytes32,address,bytes)")) ); } @@ -94,7 +97,7 @@ contract GuardedExecutorTest is BaseTest { d.d.setCanExecute(k.keyHash, address(paymentToken), _ANY_FN_SEL, true); vm.stopPrank(); - Orchestrator.Intent memory u; + Intent memory u; u.eoa = d.eoa; u.combinedGas = 10000000; @@ -112,7 +115,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("NoSpendPermissions()"))); + assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("NoSpendPermissions()"))); // Check that after the spend permission has been done, the token can be approved // and moved via `transferFrom`. @@ -127,7 +130,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); assertEq(paymentToken.balanceOf(address(0xb0b)), 0.1 ether); // Check that the spent has been increased. @@ -232,7 +235,7 @@ contract GuardedExecutorTest is BaseTest { vm.prank(address(0xb0b)); paymentToken.approve(d.eoa, 1 ether); - Orchestrator.Intent memory u; + Intent memory u; u.eoa = d.eoa; u.combinedGas = 10000000; @@ -258,12 +261,12 @@ contract GuardedExecutorTest is BaseTest { emit LogBool("transferToSelf:", transferToSelf); if (transferToSelf) { - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); assertEq(paymentToken.balanceOf(d.eoa), 0.1 ether); return; } - assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("NoSpendPermissions()"))); + assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("NoSpendPermissions()"))); vm.startPrank(d.eoa); d.d @@ -274,7 +277,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.signature = _sig(k, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); assertEq(paymentToken.balanceOf(address(0xdad)), 0.1 ether); assertEq(d.d.spendInfos(k.keyHash)[0].spent, 0.1 ether); } @@ -285,7 +288,7 @@ contract GuardedExecutorTest is BaseTest { } function testOnlySuperAdminAndEOACanSelfExecute() public { - Orchestrator.Intent memory u; + Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; u.combinedGas = 10000000; @@ -327,14 +330,14 @@ contract GuardedExecutorTest is BaseTest { u.executionData = abi.encode(calls); u.signature = _eoaSig(d.privateKey, u); - assertEq(oc.execute(abi.encode(u)), 0, "2"); + assertEq(oc.execute(encodeIntent(u)), 0, "2"); assertEq(d.d.x(), x, "3"); d.d.resetX(); u.nonce = d.d.getNonce(0); u.signature = _sig(kSuperAdmin, u); - assertEq(oc.execute(abi.encode(u)), 0, "4"); + assertEq(oc.execute(encodeIntent(u)), 0, "4"); assertEq(d.d.x(), x, "5"); d.d.resetX(); @@ -342,7 +345,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.signature = _sig(kRegular, u); assertEq( - oc.execute(abi.encode(u)), + oc.execute(encodeIntent(u)), bytes4(keccak256("UnauthorizedCall(bytes32,address,bytes)")), "6" ); @@ -353,7 +356,7 @@ contract GuardedExecutorTest is BaseTest { } function testSetAndRemoveSpendLimitRevertsForSuperAdmin() public { - Orchestrator.Intent memory u; + Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; @@ -371,11 +374,11 @@ contract GuardedExecutorTest is BaseTest { calls[0].data = abi.encodeWithSelector(IthacaAccount.authorize.selector, k.k); u.executionData = abi.encode(calls); - u.nonce = 0xc1d0 << 240; + u.nonce = d.d.getNonce(0); u.signature = _sig(d, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); } // Set spend limits. { @@ -386,7 +389,7 @@ contract GuardedExecutorTest is BaseTest { u.executionData = abi.encode(calls); u.signature = _sig(d, u); - assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("SuperAdminCanSpendAnything()"))); + assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("SuperAdminCanSpendAnything()"))); } // Remove spend limits. { @@ -397,14 +400,14 @@ contract GuardedExecutorTest is BaseTest { u.executionData = abi.encode(calls); u.signature = _sig(d, u); - assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("SuperAdminCanSpendAnything()"))); + assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("SuperAdminCanSpendAnything()"))); } } function testSetAndRemoveSpendLimit(uint256 amount) public { vm.warp(86400 * 100); - Orchestrator.Intent memory u; + Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; @@ -442,11 +445,11 @@ contract GuardedExecutorTest is BaseTest { calls[3] = _setSpendLimitCall(k, token, periods[1], 1 ether); u.executionData = abi.encode(calls); - u.nonce = 0xc1d0 << 240; + u.nonce = d.d.getNonce(0); u.signature = _eoaSig(d.privateKey, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); assertEq(d.d.spendInfos(k.keyHash).length, 2); } @@ -458,7 +461,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); infos = d.d.spendInfos(k.keyHash); for (uint256 i; i < infos.length; ++i) { @@ -474,7 +477,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); infos = d.d.spendInfos(k.keyHash); assertEq(infos.length, 1); @@ -490,7 +493,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); infos = d.d.spendInfos(k.keyHash); for (uint256 i; i < infos.length; ++i) { if (infos[i].period == periods[0]) { @@ -513,7 +516,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); infos = d.d.spendInfos(k.keyHash); for (uint256 i; i < infos.length; ++i) { @@ -534,7 +537,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); assertEq(d.d.spendInfos(k.keyHash).length, 0); } @@ -555,9 +558,9 @@ contract GuardedExecutorTest is BaseTest { && calls[0].data[2] == bytes1(uint8(0x24)) && calls[0].data[3] == bytes1(uint8(0xd5))) || amount == 0 ) { - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); } else { - assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("NoSpendPermissions()"))); + assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("NoSpendPermissions()"))); } } @@ -570,7 +573,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); infos = d.d.spendInfos(k.keyHash); for (uint256 i; i < infos.length; ++i) { @@ -587,7 +590,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); infos = d.d.spendInfos(k.keyHash); for (uint256 i; i < infos.length; ++i) { @@ -597,7 +600,7 @@ contract GuardedExecutorTest is BaseTest { } function testSetSpendLimitWithTwoPeriods() public { - Orchestrator.Intent memory u; + Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; @@ -628,11 +631,11 @@ contract GuardedExecutorTest is BaseTest { calls[5] = _setSpendLimitCall(k, token1, GuardedExecutor.SpendPeriod.Year, 1 ether); u.executionData = abi.encode(calls); - u.nonce = 0xc1d0 << 240; + u.nonce = d.d.getNonce(0); u.signature = _eoaSig(d.privateKey, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); assertEq(d.d.spendInfos(k.keyHash).length, 4); } @@ -642,11 +645,11 @@ contract GuardedExecutorTest is BaseTest { calls[0] = _transferCall2(token0, address(0xb0b), amount0); calls[1] = _transferCall2(token1, address(0xb0b), amount1); - u.nonce = 0; + u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); GuardedExecutor.SpendInfo[] memory infos = d.d.spendInfos(k.keyHash); for (uint256 i; i < infos.length; ++i) { if (infos[i].token == token0) assertEq(infos[i].spent, amount0); @@ -655,7 +658,7 @@ contract GuardedExecutorTest is BaseTest { } function testSpends(bytes32) public { - Orchestrator.Intent memory u; + Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; @@ -690,17 +693,17 @@ contract GuardedExecutorTest is BaseTest { } u.executionData = abi.encode(calls); - u.nonce = 0xc1d0 << 240; + u.nonce = d.d.getNonce(0); u.signature = _eoaSig(d.privateKey, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); assertEq(d.d.spendInfos(k.keyHash).length, tokens.length); } // Test spends. { - u.nonce = 0; + u.nonce = d.d.getNonce(0); _deployPermit2(); if (_randomChance(2)) { @@ -756,7 +759,7 @@ contract GuardedExecutorTest is BaseTest { u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); GuardedExecutor.SpendInfo[] memory infos = d.d.spendInfos(k.keyHash); for (uint256 i; i < infos.length; ++i) { assertEq(infos[i].spent, expectedSpents[0][infos[i].token]); @@ -806,7 +809,7 @@ contract GuardedExecutorTest is BaseTest { } function _testSpendWithPassKeyViaOrchestrator(PassKey memory k, address tokenToSpend) internal { - Orchestrator.Intent memory u; + Intent memory u; GuardedExecutor.SpendInfo memory info; uint256 gExecute; @@ -839,12 +842,12 @@ contract GuardedExecutorTest is BaseTest { ); u.executionData = abi.encode(calls); - u.nonce = 0xc1d0 << 240; (gExecute, u.combinedGas,) = _estimateGasForEOAKey(u); + u.signature = _eoaSig(d.privateKey, u); - assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); + assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); assertEq(d.d.spendInfos(k.keyHash).length, 2); assertEq(d.d.spendInfos(k.keyHash)[0].spent, 0); @@ -854,7 +857,7 @@ contract GuardedExecutorTest is BaseTest { // Prep Intent, and submit it. This Intent should pass. { - u.nonce = 0; + u.nonce++; ERC7821.Call[] memory calls = new ERC7821.Call[](1); calls[0] = _transferCall2(tokenToSpend, address(0xb0b), 0.6 ether); @@ -864,7 +867,7 @@ contract GuardedExecutorTest is BaseTest { u.signature = _sig(k, u); // Intent should pass. - assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); + assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); assertEq(_balanceOf(tokenToSpend, address(0xb0b)), 0.6 ether); assertEq(d.d.spendInfos(k.keyHash)[0].spent, 0.6 ether); @@ -878,7 +881,7 @@ contract GuardedExecutorTest is BaseTest { u.signature = _sig(k, u); // Intent should fail. - assertEq(oc.execute(abi.encode(u)), GuardedExecutor.ExceededSpendLimit.selector); + assertEq(oc.execute(encodeIntent(u)), GuardedExecutor.ExceededSpendLimit.selector); } // Prep Intent to try to exactly hit daily spend limit. This Intent should pass. @@ -892,7 +895,7 @@ contract GuardedExecutorTest is BaseTest { (gExecute, u.combinedGas,) = _estimateGas(k, u); u.signature = _sig(k, u); - assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); + assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); assertEq(_balanceOf(tokenToSpend, address(0xb0b)), 1 ether); assertEq(d.d.spendInfos(k.keyHash)[0].spent, 1 ether); } @@ -932,7 +935,7 @@ contract GuardedExecutorTest is BaseTest { u.signature = _sig(k, u); - assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); + assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); assertEq(_balanceOf(tokenToSpend, address(0xb0b)), 1.5 ether); assertEq(d.d.spendInfos(k.keyHash)[0].spent, 0.5 ether); } diff --git a/test/LayerZeroSettler.t.sol b/test/LayerZeroSettler.t.sol index 941f2311..fe9be6ae 100644 --- a/test/LayerZeroSettler.t.sol +++ b/test/LayerZeroSettler.t.sol @@ -716,9 +716,9 @@ contract LayerZeroSettlerTest is Test { // Should revert with InvalidL0SettlerSignature vm.expectRevert(abi.encodeWithSelector(LayerZeroSettler.InvalidL0SettlerSignature.selector)); - settlerA.executeSend{ - value: fee - }(orchestrator, settlementId, settlerContext, invalidSignature); + settlerA.executeSend{value: fee}( + orchestrator, settlementId, settlerContext, invalidSignature + ); } function test_executeSend_preventReplay() public { diff --git a/test/MultiSigSigner.t.sol b/test/MultiSigSigner.t.sol deleted file mode 100644 index 497e0618..00000000 --- a/test/MultiSigSigner.t.sol +++ /dev/null @@ -1,656 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import "./Base.t.sol"; -import {MultiSigSigner} from "../src/MultiSigSigner.sol"; -import {IthacaAccount} from "../src/IthacaAccount.sol"; -import {ERC7821} from "solady/accounts/ERC7821.sol"; - -contract MultiSigSignerTest is BaseTest { - MultiSigSigner multiSigSigner; - DelegatedEOA delegatedAccount; - - struct MultiSigTestTemps { - PassKey[] owners; - bytes32[] ownerKeyHashes; - uint256 threshold; - MultiSigKey multiSigKey; - bytes32 digest; - } - - function setUp() public override { - super.setUp(); - multiSigSigner = new MultiSigSigner(); - delegatedAccount = _randomEIP7702DelegatedEOA(); - } - - function test_DuplicateOwnerSignatures() public { - MultiSigTestTemps memory t; - - // Setup: Create a multisig with threshold 2 but only 1 owner - t.threshold = 2; - t.owners = new PassKey[](2); - t.owners[0] = _randomPassKey(); - t.owners[1] = _randomPassKey(); - - // Create the multisig key configuration - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: true, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) - }); - t.multiSigKey.threshold = t.threshold; - t.multiSigKey.owners = t.owners; - - // Authorize keys in the delegated account - vm.startPrank(delegatedAccount.eoa); - delegatedAccount.d.authorize(t.multiSigKey.k); - delegatedAccount.d.authorize(t.owners[0].k); - - // Initialize multisig config with threshold=2 but only 1 owner - t.ownerKeyHashes = new bytes32[](1); - t.ownerKeyHashes[0] = _hash(t.owners[0].k); - - // This should revert because threshold > number of owners - vm.expectRevert(MultiSigSigner.InvalidThreshold.selector); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); - - // Now test with a valid config: threshold=2, 2 owners - t.ownerKeyHashes = new bytes32[](2); - t.ownerKeyHashes[0] = _hash(t.owners[0].k); - t.ownerKeyHashes[1] = _hash(t.owners[1].k); - - multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); - - vm.stopPrank(); - - // Create a digest to sign - t.digest = keccak256("test message"); - - // Create signature array with the same signature duplicated - // Note: Each owner currently only has 1 signer power. - bytes[] memory signatures = new bytes[](2); - signatures[0] = _sig(t.owners[0], t.digest); - signatures[1] = signatures[0]; // Duplicate signature of the first owner - - // Call isValidSignatureWithKeyHash - vm.prank(address(delegatedAccount.d)); - bytes4 result = multiSigSigner.isValidSignatureWithKeyHash( - t.digest, _hash(t.multiSigKey.k), abi.encode(signatures) - ); - - // Same owner should not be able to sign multiple times - assertEq(result, bytes4(0xffffffff), "Duplicate signature should not be valid"); - - // Add the first owner twice in the owner key hash - vm.startPrank(address(delegatedAccount.eoa)); - delegatedAccount.d.setContextKeyHash(_hash(t.multiSigKey.k)); - multiSigSigner.addOwner(_hash(t.multiSigKey.k), _hash(t.owners[0].k)); - vm.stopPrank(); - - // Now it should be valid, because the first owner has 2 signer powers. - vm.prank(address(delegatedAccount.d)); - result = multiSigSigner.isValidSignatureWithKeyHash( - t.digest, _hash(t.multiSigKey.k), abi.encode(signatures) - ); - - assertEq(result, bytes4(0x8afc93b4), "Duplicate signature should be valid now"); - } - - function test_InitConfig() public { - MultiSigTestTemps memory t; - - // Setup owners - uint256 numOwners = 3; - t.owners = new PassKey[](numOwners); - t.ownerKeyHashes = new bytes32[](numOwners); - - for (uint256 i = 0; i < numOwners; i++) { - t.owners[i] = _randomPassKey(); - t.ownerKeyHashes[i] = _hash(t.owners[i].k); - } - - t.threshold = 2; - - // Create multisig key - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: false, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) - }); - - vm.prank(address(delegatedAccount.d)); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); - - // Verify config was set correctly - (uint256 storedThreshold, bytes32[] memory storedOwners) = - multiSigSigner.getConfig(address(delegatedAccount.d), _hash(t.multiSigKey.k)); - - assertEq(storedThreshold, t.threshold); - assertEq(storedOwners.length, t.ownerKeyHashes.length); - - for (uint256 i = 0; i < storedOwners.length; i++) { - assertEq(storedOwners[i], t.ownerKeyHashes[i]); - } - } - - function test_InitConfig_RevertsOnReinit() public { - MultiSigTestTemps memory t; - - t.owners = new PassKey[](1); - t.owners[0] = _randomPassKey(); - t.ownerKeyHashes = new bytes32[](1); - t.ownerKeyHashes[0] = _hash(t.owners[0].k); - t.threshold = 1; - - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: false, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) - }); - - // First initialization should succeed - vm.startPrank(address(delegatedAccount.d)); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); - - // Second initialization should revert - vm.expectRevert(MultiSigSigner.ConfigAlreadySet.selector); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); - vm.stopPrank(); - } - - function test_InitConfig_InvalidThreshold() public { - MultiSigTestTemps memory t; - - t.owners = new PassKey[](2); - t.ownerKeyHashes = new bytes32[](2); - for (uint256 i = 0; i < 2; i++) { - t.owners[i] = _randomPassKey(); - t.ownerKeyHashes[i] = _hash(t.owners[i].k); - } - - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: false, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) - }); - - vm.startPrank(address(delegatedAccount.d)); - - // Test threshold = 0 - vm.expectRevert(MultiSigSigner.InvalidThreshold.selector); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), 0, t.ownerKeyHashes); - - // Test threshold > number of owners - vm.expectRevert(MultiSigSigner.InvalidThreshold.selector); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), 3, t.ownerKeyHashes); - - vm.stopPrank(); - } - - function test_AddOwner() public { - MultiSigTestTemps memory t; - - // Initial setup with 2 owners - t.owners = new PassKey[](2); - t.ownerKeyHashes = new bytes32[](2); - for (uint256 i = 0; i < 2; i++) { - t.owners[i] = _randomPassKey(); - t.ownerKeyHashes[i] = _hash(t.owners[i].k); - } - t.threshold = 2; - - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: true, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) - }); - - // Initialize config - vm.startPrank(address(delegatedAccount.d)); - delegatedAccount.d.authorize(t.multiSigKey.k); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); - - // Add a new owner - PassKey memory newOwner = _randomPassKey(); - bytes32 newOwnerKeyHash = _hash(newOwner.k); - - // Set context key hash - delegatedAccount.d.setContextKeyHash(_hash(t.multiSigKey.k)); - multiSigSigner.addOwner(_hash(t.multiSigKey.k), newOwnerKeyHash); - - // Verify owner was added - (, bytes32[] memory storedOwners) = - multiSigSigner.getConfig(address(delegatedAccount.d), _hash(t.multiSigKey.k)); - - assertEq(storedOwners.length, 3); - assertEq(storedOwners[2], newOwnerKeyHash); - - vm.stopPrank(); - } - - function test_AddOwner_InvalidKeyHash() public { - MultiSigTestTemps memory t; - - t.owners = new PassKey[](1); - t.owners[0] = _randomPassKey(); - t.ownerKeyHashes = new bytes32[](1); - t.ownerKeyHashes[0] = _hash(t.owners[0].k); - - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: true, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) - }); - - vm.startPrank(address(delegatedAccount.d)); - delegatedAccount.d.authorize(t.multiSigKey.k); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), 1, t.ownerKeyHashes); - - // Try to add owner with wrong context key hash - PassKey memory newOwner = _randomPassKey(); - - // Set wrong context key hash - delegatedAccount.d.setContextKeyHash(bytes32(uint256(123))); - - vm.expectRevert(MultiSigSigner.InvalidKeyHash.selector); - multiSigSigner.addOwner(_hash(t.multiSigKey.k), _hash(newOwner.k)); - - vm.stopPrank(); - } - - function test_RemoveOwner() public { - MultiSigTestTemps memory t; - - // Setup with 3 owners - t.owners = new PassKey[](3); - t.ownerKeyHashes = new bytes32[](3); - for (uint256 i = 0; i < 3; i++) { - t.owners[i] = _randomPassKey(); - t.ownerKeyHashes[i] = _hash(t.owners[i].k); - } - t.threshold = 2; - - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: true, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) - }); - - vm.startPrank(address(delegatedAccount.d)); - delegatedAccount.d.authorize(t.multiSigKey.k); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); - - // Remove the second owner - delegatedAccount.d.setContextKeyHash(_hash(t.multiSigKey.k)); - multiSigSigner.removeOwner(_hash(t.multiSigKey.k), t.ownerKeyHashes[1]); - - // Verify owner was removed - (, bytes32[] memory storedOwners) = - multiSigSigner.getConfig(address(delegatedAccount.d), _hash(t.multiSigKey.k)); - - assertEq(storedOwners.length, 2); - // The last owner should have replaced the removed one - assertEq(storedOwners[1], t.ownerKeyHashes[2]); - - vm.stopPrank(); - } - - function test_RemoveOwner_OwnerNotFound() public { - MultiSigTestTemps memory t; - - t.owners = new PassKey[](2); - t.ownerKeyHashes = new bytes32[](2); - for (uint256 i = 0; i < 2; i++) { - t.owners[i] = _randomPassKey(); - t.ownerKeyHashes[i] = _hash(t.owners[i].k); - } - - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: true, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) - }); - - vm.startPrank(address(delegatedAccount.d)); - delegatedAccount.d.authorize(t.multiSigKey.k); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), 1, t.ownerKeyHashes); - - // Try to remove non-existent owner - delegatedAccount.d.setContextKeyHash(_hash(t.multiSigKey.k)); - vm.expectRevert(MultiSigSigner.OwnerNotFound.selector); - multiSigSigner.removeOwner(_hash(t.multiSigKey.k), bytes32(uint256(999))); - - vm.stopPrank(); - } - - function test_RemoveOwner_ThresholdViolation() public { - MultiSigTestTemps memory t; - - // Setup with 2 owners and threshold 2 - t.owners = new PassKey[](2); - t.ownerKeyHashes = new bytes32[](2); - for (uint256 i = 0; i < 2; i++) { - t.owners[i] = _randomPassKey(); - t.ownerKeyHashes[i] = _hash(t.owners[i].k); - } - t.threshold = 2; - - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: true, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) - }); - - vm.startPrank(address(delegatedAccount.d)); - delegatedAccount.d.authorize(t.multiSigKey.k); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); - - // Try to remove owner when it would violate threshold - delegatedAccount.d.setContextKeyHash(_hash(t.multiSigKey.k)); - vm.expectRevert(MultiSigSigner.InvalidThreshold.selector); - multiSigSigner.removeOwner(_hash(t.multiSigKey.k), t.ownerKeyHashes[0]); - - vm.stopPrank(); - } - - function test_SetThreshold() public { - MultiSigTestTemps memory t; - - // Setup with 3 owners - t.owners = new PassKey[](3); - t.ownerKeyHashes = new bytes32[](3); - for (uint256 i = 0; i < 3; i++) { - t.owners[i] = _randomPassKey(); - t.ownerKeyHashes[i] = _hash(t.owners[i].k); - } - t.threshold = 1; - - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: true, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) - }); - - vm.startPrank(address(delegatedAccount.d)); - delegatedAccount.d.authorize(t.multiSigKey.k); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); - - // Change threshold to 2 - delegatedAccount.d.setContextKeyHash(_hash(t.multiSigKey.k)); - multiSigSigner.setThreshold(_hash(t.multiSigKey.k), 2); - - // Verify threshold was changed - (uint256 storedThreshold,) = - multiSigSigner.getConfig(address(delegatedAccount.d), _hash(t.multiSigKey.k)); - - assertEq(storedThreshold, 2); - - // Change threshold to 3 - multiSigSigner.setThreshold(_hash(t.multiSigKey.k), 3); - - (storedThreshold,) = - multiSigSigner.getConfig(address(delegatedAccount.d), _hash(t.multiSigKey.k)); - - assertEq(storedThreshold, 3); - - vm.stopPrank(); - } - - function test_SetThreshold_Invalid() public { - MultiSigTestTemps memory t; - - t.owners = new PassKey[](2); - t.ownerKeyHashes = new bytes32[](2); - for (uint256 i = 0; i < 2; i++) { - t.owners[i] = _randomPassKey(); - t.ownerKeyHashes[i] = _hash(t.owners[i].k); - } - - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: true, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) - }); - - vm.startPrank(address(delegatedAccount.d)); - delegatedAccount.d.authorize(t.multiSigKey.k); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), 1, t.ownerKeyHashes); - delegatedAccount.d.setContextKeyHash(_hash(t.multiSigKey.k)); - - // Test threshold = 0 - vm.expectRevert(MultiSigSigner.InvalidThreshold.selector); - multiSigSigner.setThreshold(_hash(t.multiSigKey.k), 0); - - // Test threshold > number of owners - vm.expectRevert(MultiSigSigner.InvalidThreshold.selector); - multiSigSigner.setThreshold(_hash(t.multiSigKey.k), 3); - - vm.stopPrank(); - } - - function test_ValidSignature_MeetsThreshold() public { - MultiSigTestTemps memory t; - - // Setup with 3 owners, threshold 2 - t.owners = new PassKey[](3); - t.ownerKeyHashes = new bytes32[](3); - - vm.startPrank(delegatedAccount.eoa); - for (uint256 i = 0; i < 3; i++) { - t.owners[i] = _randomPassKey(); - t.ownerKeyHashes[i] = _hash(t.owners[i].k); - delegatedAccount.d.authorize(t.owners[i].k); - } - - t.threshold = 2; - - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: true, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) - }); - - delegatedAccount.d.authorize(t.multiSigKey.k); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); - vm.stopPrank(); - - // Create a digest to sign - t.digest = keccak256("test message"); - - // Create signatures from 2 owners (meets threshold) - bytes[] memory signatures = new bytes[](2); - signatures[0] = _sig(t.owners[0], t.digest); - signatures[1] = _sig(t.owners[1], t.digest); - - // Validate signature - vm.prank(address(delegatedAccount.d)); - bytes4 result = multiSigSigner.isValidSignatureWithKeyHash( - t.digest, _hash(t.multiSigKey.k), abi.encode(signatures) - ); - - assertEq(result, bytes4(0x8afc93b4), "Valid signature should return MAGIC_VALUE"); - } - - function test_InvalidSignature_BelowThreshold() public { - MultiSigTestTemps memory t; - - // Setup with 3 owners, threshold 2 - t.owners = new PassKey[](3); - t.ownerKeyHashes = new bytes32[](3); - - vm.startPrank(delegatedAccount.eoa); - for (uint256 i = 0; i < 3; i++) { - t.owners[i] = _randomPassKey(); - t.ownerKeyHashes[i] = _hash(t.owners[i].k); - delegatedAccount.d.authorize(t.owners[i].k); - } - - t.threshold = 2; - - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: true, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) - }); - - delegatedAccount.d.authorize(t.multiSigKey.k); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); - vm.stopPrank(); - - // Create a digest to sign - t.digest = keccak256("test message"); - - // Create signature from only 1 owner (below threshold) - bytes[] memory signatures = new bytes[](1); - signatures[0] = _sig(t.owners[0], t.digest); - - // Validate signature - vm.prank(address(delegatedAccount.d)); - bytes4 result = multiSigSigner.isValidSignatureWithKeyHash( - t.digest, _hash(t.multiSigKey.k), abi.encode(signatures) - ); - - assertEq(result, bytes4(0xffffffff), "Invalid signature should return FAIL_VALUE"); - } - - function test_InvalidSignature_NonOwner() public { - MultiSigTestTemps memory t; - - // Setup with 2 owners - t.owners = new PassKey[](2); - t.ownerKeyHashes = new bytes32[](2); - - vm.startPrank(delegatedAccount.eoa); - for (uint256 i = 0; i < 2; i++) { - t.owners[i] = _randomPassKey(); - t.ownerKeyHashes[i] = _hash(t.owners[i].k); - delegatedAccount.d.authorize(t.owners[i].k); - } - - t.threshold = 2; - - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: true, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) - }); - - delegatedAccount.d.authorize(t.multiSigKey.k); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); - - // Create a non-owner key - PassKey memory nonOwner = _randomPassKey(); - delegatedAccount.d.authorize(nonOwner.k); - - vm.stopPrank(); - - // Create a digest to sign - t.digest = keccak256("test message"); - - // Create signatures with one owner and one non-owner - bytes[] memory signatures = new bytes[](2); - signatures[0] = _sig(t.owners[0], t.digest); - signatures[1] = _sig(nonOwner, t.digest); - - // Validate signature - vm.prank(address(delegatedAccount.d)); - bytes4 result = multiSigSigner.isValidSignatureWithKeyHash( - t.digest, _hash(t.multiSigKey.k), abi.encode(signatures) - ); - - assertEq(result, bytes4(0xffffffff), "Non-owner signature should return FAIL_VALUE"); - } - - function testFuzz_InitConfig(uint256 numOwners, uint256 threshold) public { - numOwners = bound(numOwners, 1, 10); - threshold = bound(threshold, 1, numOwners); - - MultiSigTestTemps memory t; - t.owners = new PassKey[](numOwners); - t.ownerKeyHashes = new bytes32[](numOwners); - - for (uint256 i = 0; i < numOwners; i++) { - t.owners[i] = _randomPassKey(); - t.ownerKeyHashes[i] = _hash(t.owners[i].k); - } - - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: false, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(uint96(_random()))) - }); - - vm.prank(address(delegatedAccount.d)); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), threshold, t.ownerKeyHashes); - - (uint256 storedThreshold, bytes32[] memory storedOwners) = - multiSigSigner.getConfig(address(delegatedAccount.d), _hash(t.multiSigKey.k)); - - assertEq(storedThreshold, threshold); - assertEq(storedOwners.length, numOwners); - } - - function testFuzz_SignatureValidation(uint256 numOwners, uint256 threshold, uint256 numSigners) - public - { - numOwners = bound(numOwners, 1, 10); - threshold = bound(threshold, 1, numOwners); - numSigners = bound(numSigners, 0, numOwners); - - MultiSigTestTemps memory t; - t.owners = new PassKey[](numOwners); - t.ownerKeyHashes = new bytes32[](numOwners); - - vm.startPrank(delegatedAccount.eoa); - for (uint256 i = 0; i < numOwners; i++) { - t.owners[i] = _randomPassKey(); - t.ownerKeyHashes[i] = _hash(t.owners[i].k); - delegatedAccount.d.authorize(t.owners[i].k); - } - - t.multiSigKey.k = IthacaAccount.Key({ - expiry: 0, - keyType: IthacaAccount.KeyType.External, - isSuperAdmin: true, - publicKey: abi.encodePacked(address(multiSigSigner), bytes12(uint96(_random()))) - }); - - delegatedAccount.d.authorize(t.multiSigKey.k); - multiSigSigner.initConfig(_hash(t.multiSigKey.k), threshold, t.ownerKeyHashes); - vm.stopPrank(); - - t.digest = keccak256(abi.encode("test", _random())); - - bytes[] memory signatures = new bytes[](numSigners); - for (uint256 i = 0; i < numSigners; i++) { - signatures[i] = _sig(t.owners[i], t.digest); - } - - vm.prank(address(delegatedAccount.d)); - bytes4 result = multiSigSigner.isValidSignatureWithKeyHash( - t.digest, _hash(t.multiSigKey.k), abi.encode(signatures) - ); - - if (numSigners >= threshold) { - assertEq(result, bytes4(0x8afc93b4), "Should be valid when signatures >= threshold"); - } else { - assertEq(result, bytes4(0xffffffff), "Should be invalid when signatures < threshold"); - } - } -} diff --git a/test/Orchestrator.t.sol b/test/Orchestrator.t.sol index 9f2ada0c..66d1bba7 100644 --- a/test/Orchestrator.t.sol +++ b/test/Orchestrator.t.sol @@ -20,7 +20,7 @@ import {IEscrow} from "../src/interfaces/IEscrow.sol"; contract OrchestratorTest is BaseTest { struct _TestFullFlowTemps { - Orchestrator.Intent[] intents; + Intent[] intents; TargetFunctionPayload[] targetFunctionPayloads; DelegatedEOA[] delegatedEOAs; bytes[] encodedIntents; @@ -29,7 +29,7 @@ contract OrchestratorTest is BaseTest { function testFullFlow(uint256) public { _TestFullFlowTemps memory t; - t.intents = new Orchestrator.Intent[](_random() & 3); + t.intents = new Intent[](_random() & 3); t.targetFunctionPayloads = new TargetFunctionPayload[](t.intents.length); t.delegatedEOAs = new DelegatedEOA[](t.intents.length); t.encodedIntents = new bytes[](t.intents.length); @@ -38,7 +38,7 @@ contract OrchestratorTest is BaseTest { DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); t.delegatedEOAs[i] = d; - Orchestrator.Intent memory u = t.intents[i]; + Intent memory u = t.intents[i]; u.eoa = d.eoa; vm.deal(u.eoa, 2 ** 128 - 1); @@ -54,7 +54,7 @@ contract OrchestratorTest is BaseTest { u.combinedGas = 10000000; u.signature = _sig(d, u); - t.encodedIntents[i] = abi.encode(u); + t.encodedIntents[i] = encodeIntent(u); } bytes4[] memory errors = oc.execute(t.encodedIntents); @@ -74,11 +74,12 @@ contract OrchestratorTest is BaseTest { vm.deal(alice.eoa, 10 ether); vm.deal(bob.eoa, 10 ether); paymentToken.mint(alice.eoa, 50 ether); + paymentToken.mint(bob.eoa, 50 ether); bytes memory executionData = _transferExecutionData(address(paymentToken), address(0xabcd), 1 ether); - Orchestrator.Intent memory u; + Intent memory u; u.eoa = alice.eoa; u.nonce = 0; u.executionData = executionData; @@ -88,11 +89,9 @@ contract OrchestratorTest is BaseTest { u.paymentAmount = 0.1 ether; u.paymentMaxAmount = 0.5 ether; u.combinedGas = 10000000; - u.signature = ""; - u.signature = _sig(alice, u); - assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("PaymentError()"))); + assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("Unauthorized()"))); } function testExecuteWithSecp256k1PassKey() public { @@ -107,7 +106,7 @@ contract OrchestratorTest is BaseTest { vm.prank(d.eoa); d.d.authorize(k.k); - Orchestrator.Intent memory u; + Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.executionData = _transferExecutionData(address(paymentToken), address(0xabcd), 1 ether); @@ -131,7 +130,7 @@ contract OrchestratorTest is BaseTest { combinedGasVerificationOffset: 0 }) ); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); uint256 actualAmount = 0.1 ether; assertEq(paymentToken.balanceOf(address(oc)), actualAmount); assertEq(paymentToken.balanceOf(d.eoa), 50 ether - actualAmount - 1 ether); @@ -154,7 +153,7 @@ contract OrchestratorTest is BaseTest { calls[0].to = target; calls[0].data = abi.encodeWithSignature("revertWithData(bytes)", data); - Orchestrator.Intent memory u; + Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.executionData = abi.encode(calls); @@ -163,7 +162,7 @@ contract OrchestratorTest is BaseTest { u.signature = _sig(k, u); (bool success, bytes memory result) = - address(oc).call(abi.encodeWithSignature("simulateFailed(bytes)", abi.encode(u))); + address(oc).call(abi.encodeWithSignature("simulateFailed(bytes)", encodeIntent(u))); assertFalse(success); assertEq(result, abi.encodeWithSignature("ErrorWithData(bytes)", data)); @@ -174,7 +173,7 @@ contract OrchestratorTest is BaseTest { paymentToken.mint(d.eoa, 500 ether); - Orchestrator.Intent memory u; + Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.executionData = _transferExecutionData(address(paymentToken), address(0xabcd), 1 ether); @@ -195,7 +194,7 @@ contract OrchestratorTest is BaseTest { }) ); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); uint256 actualAmount = 10 ether; assertEq(paymentToken.balanceOf(address(this)), actualAmount); assertEq(paymentToken.balanceOf(d.eoa), 500 ether - actualAmount - 1 ether); @@ -212,7 +211,7 @@ contract OrchestratorTest is BaseTest { ds[i] = _randomEIP7702DelegatedEOA(); paymentToken.mint(ds[i].eoa, 1 ether); - Orchestrator.Intent memory u; + Intent memory u; u.eoa = ds[i].eoa; u.nonce = 0; u.executionData = @@ -224,7 +223,7 @@ contract OrchestratorTest is BaseTest { u.paymentMaxAmount = 0.5 ether; u.combinedGas = 10000000; u.signature = _sig(ds[i], u); - encodedIntents[i] = abi.encode(u); + encodedIntents[i] = encodeIntent(u); } bytes4[] memory errs = oc.execute(encodedIntents); @@ -247,7 +246,7 @@ contract OrchestratorTest is BaseTest { calls[i] = _transferCall(address(paymentToken), address(0xabcd), 0.5 ether); } - Orchestrator.Intent memory u; + Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.executionData = abi.encode(calls); @@ -260,7 +259,7 @@ contract OrchestratorTest is BaseTest { (uint256 gExecute,,) = _estimateGas(u); - assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); + assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); assertEq(paymentToken.balanceOf(address(0xabcd)), 0.5 ether * n); assertEq(paymentToken.balanceOf(d.eoa), 100 ether - (u.paymentAmount + 0.5 ether * n)); assertEq(d.d.getNonce(0), 1); @@ -271,7 +270,7 @@ contract OrchestratorTest is BaseTest { paymentToken.mint(d.eoa, 500 ether); - Orchestrator.Intent memory u; + Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.executionData = _transferExecutionData(address(paymentToken), address(0xabcd), 1 ether); @@ -313,7 +312,7 @@ contract OrchestratorTest is BaseTest { paymentToken.mint(d.eoa, 10 ether); // Create base intent with common fields - Orchestrator.Intent memory baseIntent; + Intent memory baseIntent; baseIntent.eoa = d.eoa; baseIntent.paymentToken = address(paymentToken); baseIntent.paymentAmount = 0.1 ether; @@ -322,50 +321,49 @@ contract OrchestratorTest is BaseTest { // Test case 1: Intent with no expiry (expiry = 0) should always be valid { - Orchestrator.Intent memory u = baseIntent; + Intent memory u = baseIntent; u.nonce = d.d.getNonce(0); u.executionData = _transferExecutionData(address(paymentToken), address(0xabcd), 1 ether); u.expiry = 0; // No expiry u.signature = _sig(d, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); assertEq(paymentToken.balanceOf(address(0xabcd)), 1 ether); } // Test case 2: Intent with future expiry should be valid { - Orchestrator.Intent memory u = baseIntent; + Intent memory u = baseIntent; u.nonce = d.d.getNonce(0); u.executionData = _transferExecutionData(address(paymentToken), address(0xbcde), 1 ether); u.expiry = block.timestamp + 1 hours; // Future expiry u.signature = _sig(d, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); assertEq(paymentToken.balanceOf(address(0xbcde)), 1 ether); } // Test case 3: Intent with past expiry should fail { - Orchestrator.Intent memory u = baseIntent; + Intent memory u = baseIntent; u.nonce = d.d.getNonce(0); // This will be 2 after the previous two intents u.executionData = _transferExecutionData(address(paymentToken), address(0xcdef), 1 ether); u.expiry = block.timestamp - 1; // Past expiry u.signature = _sig(d, u); - bytes4 result = oc.execute(abi.encode(u)); + bytes4 result = oc.execute(encodeIntent(u)); assertEq(result, bytes4(keccak256("IntentExpired()"))); assertEq(paymentToken.balanceOf(address(0xcdef)), 0); // Transfer should not happen } - // Test case 4: Batch execution with mixed expired and valid intents { bytes[] memory encodedIntents = new bytes[](3); // Create base intent for batch with smaller amounts - Orchestrator.Intent memory batchBase; + Intent memory batchBase; batchBase.eoa = d.eoa; batchBase.paymentToken = address(paymentToken); batchBase.paymentAmount = 0.05 ether; @@ -373,31 +371,31 @@ contract OrchestratorTest is BaseTest { batchBase.combinedGas = 10000000; // Valid intent with nonce 2 - Orchestrator.Intent memory u1 = batchBase; + Intent memory u1 = batchBase; u1.nonce = 2; u1.executionData = _transferExecutionData(address(paymentToken), address(0x1111), 0.5 ether); u1.expiry = block.timestamp + 1 hours; u1.signature = _sig(d, u1); - encodedIntents[0] = abi.encode(u1); + encodedIntents[0] = encodeIntent(u1); // Expired intent with nonce 3 - Orchestrator.Intent memory u2 = batchBase; + Intent memory u2 = batchBase; u2.nonce = 3; u2.executionData = _transferExecutionData(address(paymentToken), address(0x2222), 0.5 ether); u2.expiry = block.timestamp - 1; u2.signature = _sig(d, u2); - encodedIntents[1] = abi.encode(u2); + encodedIntents[1] = encodeIntent(u2); // Another valid intent with nonce 3 (since nonce 3 wasn't consumed due to expiry) - Orchestrator.Intent memory u3 = batchBase; + Intent memory u3 = batchBase; u3.nonce = 3; u3.executionData = _transferExecutionData(address(paymentToken), address(0x3333), 0.5 ether); u3.expiry = 0; // No expiry u3.signature = _sig(d, u3); - encodedIntents[2] = abi.encode(u3); + encodedIntents[2] = encodeIntent(u3); bytes4[] memory errors = oc.execute(encodedIntents); assertEq(errors.length, 3); @@ -424,7 +422,7 @@ contract OrchestratorTest is BaseTest { paymentToken.mint(ds[i].eoa, 1 ether); vm.deal(ds[i].eoa, 1 ether); - Orchestrator.Intent memory u; + Intent memory u; u.eoa = ds[i].eoa; u.nonce = 0; u.executionData = @@ -437,7 +435,7 @@ contract OrchestratorTest is BaseTest { u.combinedGas = 10000000; u.signature = _sig(ds[i], u); - encodeIntents[i] = abi.encode(u); + encodeIntents[i] = encodeIntent(u); } bytes memory data = abi.encodeWithSignature("execute(bytes[])", encodeIntents); @@ -463,7 +461,7 @@ contract OrchestratorTest is BaseTest { vm.prank(d.eoa); d.d.authorize(k.k); - Orchestrator.Intent memory u; + Intent memory u; u.eoa = d.eoa; u.executionData = _executionData(address(0), 0, bytes("")); u.nonce = 0x2; @@ -473,12 +471,12 @@ contract OrchestratorTest is BaseTest { u.combinedGas = 20000000; u.signature = _sig(k, u); - oc.execute(abi.encode(u)); + oc.execute(encodeIntent(u)); } function testInvalidateNonce(uint96 seqKey, uint64 seq, uint64 seq2) public { uint256 nonce = (uint256(seqKey) << 64) | uint256(seq); - Orchestrator.Intent memory u; + Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; @@ -518,9 +516,9 @@ contract OrchestratorTest is BaseTest { u.signature = _sig(d, u); if (seq > type(uint64).max - 2) { - assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("InvalidNonce()"))); + assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("InvalidNonce()"))); } else { - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); } } @@ -539,7 +537,7 @@ contract OrchestratorTest is BaseTest { p.paymentPerGas, p.combinedGasIncrement, p.combinedGasVerificationOffset, - abi.encode(p.u) + encodeIntent(p.u) ); vm.revertToStateAndDelete(snapshot); @@ -593,7 +591,7 @@ contract OrchestratorTest is BaseTest { preCall.signature = _eoaSig(payer.privateKey, oc.computeDigest(preCall)); // Create an Intent with the pre-call and fee payer - ICommon.Intent memory u; + Intent memory u; u.eoa = eoa; u.payer = address(payer.d); u.nonce = 0; @@ -615,7 +613,7 @@ contract OrchestratorTest is BaseTest { u.encodedPreCalls[0] = abi.encode(preCall); // Sign the intent with the ephemeral key and the payment with the payer - bytes32 digest = oc.computeDigest(u); + bytes32 digest = computeDigest(u); u.signature = _eoaSig(ephemeralPK, digest); u.paymentSignature = _eoaSig(payer.privateKey, digest); @@ -623,7 +621,7 @@ contract OrchestratorTest is BaseTest { uint256 payerBalanceBefore = paymentToken.balanceOf(address(payer.d)); assertFalse(MockAccount(payable(payer.eoa)).keyCount() > 0); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); // Verify the session key was authorized in the payer's account assertTrue(MockAccount(payable(payer.eoa)).keyCount() > 0); @@ -656,7 +654,7 @@ contract OrchestratorTest is BaseTest { function testInitAndTransferInOneShot(bytes32) public { _TestAuthorizeWithPreCallsAndTransferTemps memory t; - Orchestrator.Intent memory u; + Intent memory u; uint256 ephemeralPK = _randomPrivateKey(); t.eoa = vm.addr(ephemeralPK); @@ -745,14 +743,14 @@ contract OrchestratorTest is BaseTest { // Test without gas estimation. u.combinedGas = 10000000; u.signature = _sig(kSession, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); assertEq(_balanceOf(tokenToTransfer, address(0xabcd)), 0.5 ether); } function testAuthorizeWithPreCallsAndTransfer(bytes32) public { _TestAuthorizeWithPreCallsAndTransferTemps memory t; - Orchestrator.Intent memory u; + Intent memory u; Orchestrator.SignedCall memory pInit; if (_randomChance(2)) { @@ -902,21 +900,21 @@ contract OrchestratorTest is BaseTest { if (t.testInvalidPreCallEOA) { u.combinedGas = 10000000; u.signature = _sig(kSession, u); - assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("InvalidPreCallEOA()"))); + assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("InvalidPreCallEOA()"))); return; // Skip the rest. } if (t.testPreCallVerificationError) { u.combinedGas = 10000000; u.signature = _sig(kSession, u); - assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("PreCallVerificationError()"))); + assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("PreCallVerificationError()"))); return; // Skip the rest. } if (t.testPreCallError) { u.combinedGas = 10000000; u.signature = _sig(kSession, u); - assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("PreCallError()"))); + assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("PreCallError()"))); return; // Skip the rest. } @@ -933,12 +931,12 @@ contract OrchestratorTest is BaseTest { u.combinedGas = t.gCombined; u.signature = _sig(kSession, u); - assertEq(oc.execute{gas: t.gExecute}(abi.encode(u)), 0); + assertEq(oc.execute{gas: t.gExecute}(encodeIntent(u)), 0); } else { // Otherwise, test without gas estimation. u.combinedGas = 10000000; u.signature = _sig(kSession, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); } assertEq(paymentToken.balanceOf(address(0xabcd)), 0.5 ether); @@ -968,7 +966,7 @@ contract OrchestratorTest is BaseTest { // 1 ether in the EOA for execution. vm.deal(address(d.d), 1 ether); - Orchestrator.Intent memory u; + Intent memory u; u.eoa = d.eoa; u.payer = address(payer.d); @@ -981,7 +979,7 @@ contract OrchestratorTest is BaseTest { u.executionData = _transferExecutionData(address(0), address(0xabcd), 1 ether); u.paymentRecipient = address(0x12345); - bytes32 digest = oc.computeDigest(u); + bytes32 digest = computeDigest(u); uint256 snapshot = vm.snapshotState(); // To allow paymasters to be used in simulation mode. @@ -990,12 +988,12 @@ contract OrchestratorTest is BaseTest { vm.revertToStateAndDelete(snapshot); u.combinedGas = gCombined; - digest = oc.computeDigest(u); + digest = computeDigest(u); u.signature = _eoaSig(d.privateKey, digest); u.paymentSignature = _eoaSig(payer.privateKey, digest); uint256 payerBalanceBefore = _balanceOf(u.paymentToken, address(payer.d)); - assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); + assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); assertEq(d.d.getNonce(0), u.nonce + 1); assertEq(_balanceOf(u.paymentToken, u.paymentRecipient), u.paymentAmount); assertEq(_balanceOf(u.paymentToken, address(payer.d)), payerBalanceBefore - u.paymentAmount); @@ -1034,7 +1032,7 @@ contract OrchestratorTest is BaseTest { t.token = _randomChance(2) ? address(0) : address(paymentToken); t.isWithState = _randomChance(2); - Orchestrator.Intent memory u; + Intent memory u; t.d = _randomEIP7702DelegatedEOA(); vm.deal(t.d.eoa, type(uint192).max); @@ -1052,7 +1050,7 @@ contract OrchestratorTest is BaseTest { if (t.isWithState) { t.withState.increaseFunds(u.paymentToken, u.eoa, t.funds); } else { - bytes32 digest = oc.computeDigest(u); + bytes32 digest = computeDigest(u); digest = t.withSignature.computeSignatureDigest(digest); u.paymentSignature = _sig(t.withSignatureEOA, digest); t.corruptSignature = _randomChance(2); @@ -1070,7 +1068,7 @@ contract OrchestratorTest is BaseTest { t.withSignature.setApprovedOrchestrator(address(oc), false); } if ((t.unapprovedOrchestrator && u.paymentAmount != 0)) { - assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("Unauthorized()"))); + assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("Unauthorized()"))); if (u.paymentAmount != 0) { assertEq(t.d.d.getNonce(0), u.nonce); @@ -1081,7 +1079,7 @@ contract OrchestratorTest is BaseTest { } else if (t.isWithState && u.paymentAmount > t.funds && u.paymentAmount != 0) { // Arithmetic underflow error assertEq( - oc.execute(abi.encode(u)), + oc.execute(encodeIntent(u)), 0x4e487b7100000000000000000000000000000000000000000000000000000000 ); @@ -1099,7 +1097,7 @@ contract OrchestratorTest is BaseTest { } } else if ((!t.isWithState && t.corruptSignature && u.paymentAmount != 0)) { // Pre payment will not happen - assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("InvalidSignature()"))); + assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("InvalidSignature()"))); // If prePayment is 0, then nonce is incremented, because the prePayment doesn't fail. if (u.paymentAmount == 0) { assertEq(t.d.d.getNonce(0), u.nonce + 1); @@ -1109,7 +1107,7 @@ contract OrchestratorTest is BaseTest { assertEq(_balanceOf(t.token, u.payer), t.balanceBefore); assertEq(_balanceOf(address(0), address(0xabcd)), 0); } else { - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); assertEq(t.d.d.getNonce(0), u.nonce + 1); assertEq(_balanceOf(t.token, u.payer), t.balanceBefore - u.paymentAmount); assertEq(_balanceOf(address(0), address(0xabcd)), 1 ether); @@ -1128,7 +1126,7 @@ contract OrchestratorTest is BaseTest { t.testImplementationCheck = _randomChance(2); t.requireWrongImplementation = _randomChance(2); - Orchestrator.Intent memory u; + Intent memory u; vm.deal(t.d.eoa, type(uint192).max); u.eoa = t.d.eoa; @@ -1148,12 +1146,12 @@ contract OrchestratorTest is BaseTest { if (t.testImplementationCheck && t.requireWrongImplementation) { assertEq( - oc.execute(abi.encode(u)), bytes4(keccak256("UnsupportedAccountImplementation()")) + oc.execute(encodeIntent(u)), bytes4(keccak256("UnsupportedAccountImplementation()")) ); assertEq(t.d.d.getNonce(0), u.nonce); assertEq(_balanceOf(address(0), address(0xabcd)), 0); } else { - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); assertEq(t.d.d.getNonce(0), u.nonce + 1); assertEq(_balanceOf(address(0), address(0xabcd)), 1 ether); } @@ -1240,7 +1238,7 @@ contract OrchestratorTest is BaseTest { vm.expectRevert(bytes4(keccak256("InvalidKeyHash()"))); t.d.d.execute(_ERC7821_BATCH_EXECUTION_MODE, abi.encode(calls)); - Orchestrator.Intent memory u; + Intent memory u; u.eoa = t.d.eoa; u.nonce = t.d.d.getNonce(0); u.executionData = abi.encode(calls); @@ -1250,14 +1248,13 @@ contract OrchestratorTest is BaseTest { u.signature = _sig(t.multiSigKey, u); // Test unwrapAndValidateSignature - bytes32 digest = oc.computeDigest(u); + bytes32 digest = computeDigest(u); (bool isValid, bytes32 keyHash) = t.d.d.unwrapAndValidateSignature(digest, _sig(t.multiSigKey, digest)); assertEq(isValid, true); assertEq(keyHash, _hash(t.multiSigKey.k)); - - assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); + assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); (uint256 _threshold, bytes32[] memory o) = t.multiSigSigner.getConfig(address(t.d.d), _hash(t.multiSigKey.k)); @@ -1281,14 +1278,13 @@ contract OrchestratorTest is BaseTest { if (newThreshold == 0) { vm.expectRevert(bytes4(keccak256("InvalidThreshold()"))); } - (gExecute, gCombined,) = _estimateGasForMultiSigKey(t.multiSigKey, u); u.combinedGas = gCombined; u.signature = _sig(t.multiSigKey, u); if (newThreshold > 0) { - assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); + assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); (_threshold, o) = t.multiSigSigner.getConfig(address(t.d.d), _hash(t.multiSigKey.k)); assertEq(_threshold, newThreshold); @@ -1297,7 +1293,6 @@ contract OrchestratorTest is BaseTest { t.multiSigKey.threshold = newThreshold; } } - // Test removeOwner { uint256 removeIndex = _bound(_random(), 0, o.length - 1); @@ -1316,7 +1311,7 @@ contract OrchestratorTest is BaseTest { u.combinedGas = gCombined; u.signature = _sig(t.multiSigKey, u); - assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); + assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); (_threshold, o) = t.multiSigSigner.getConfig(address(t.d.d), _hash(t.multiSigKey.k)); assertEq(o.length, t.multiSigKey.owners.length); @@ -1337,9 +1332,9 @@ contract OrchestratorTest is BaseTest { DelegatedEOA d; PassKey k; // Intent data - ICommon.Intent baseIntent; - ICommon.Intent arbIntent; - ICommon.Intent outputIntent; + Intent baseIntent; + Intent arbIntent; + Intent outputIntent; // Merkle data bytes32[] leafs; bytes32 root; @@ -1427,12 +1422,11 @@ contract OrchestratorTest is BaseTest { // 1. Prepare the output intent first to get its digest as settlementId t.outputIntent.eoa = t.d.eoa; - t.outputIntent.nonce = t.d.d.getNonce(0); + t.outputIntent.nonce = t.d.d.getNonce(0x6D76 << 176); t.outputIntent.executionData = _transferExecutionData(address(t.usdcMainnet), t.friend, 1000); t.outputIntent.combinedGas = 1000000; t.outputIntent.settler = address(t.settler); - t.outputIntent.isMultichain = true; { bytes[] memory encodedFundTransfers = new bytes[](1); @@ -1451,13 +1445,12 @@ contract OrchestratorTest is BaseTest { // Compute the output intent digest to use as settlementId vm.chainId(1); // Mainnet - t.settlementId = oc.computeDigest(t.outputIntent); + t.settlementId = computeDigest(t.outputIntent); // Base Intent with escrow execution data t.baseIntent.eoa = t.d.eoa; - t.baseIntent.nonce = t.d.d.getNonce(0); + t.baseIntent.nonce = t.d.d.getNonce(0x6D76 << 176); t.baseIntent.combinedGas = 1000000; - t.baseIntent.isMultichain = true; // Create Base escrow execution data { @@ -1497,9 +1490,8 @@ contract OrchestratorTest is BaseTest { // Arbitrum Intent with escrow execution data t.arbIntent.eoa = t.d.eoa; - t.arbIntent.nonce = t.d.d.getNonce(0); + t.arbIntent.nonce = t.d.d.getNonce(0x6D76 << 176); t.arbIntent.combinedGas = 1000000; - t.arbIntent.isMultichain = true; // Create Arbitrum escrow execution data { IEscrow.Escrow[] memory escrows = new IEscrow.Escrow[](1); @@ -1546,13 +1538,14 @@ contract OrchestratorTest is BaseTest { // User has 600 USDC on base t.usdcBase.mint(t.d.eoa, 600); - t.encodedIntents[0] = abi.encode(t.baseIntent); + t.encodedIntents[0] = encodeIntent(t.baseIntent); // User escrows funds on Base vm.expectEmit(true, false, false, false, address(t.escrowBase)); emit Escrow.EscrowCreated(t.escrowIdBase); vm.prank(t.gasWallet); t.errs = oc.execute(t.encodedIntents); assertEq(uint256(bytes32(t.errs[0])), 0); + // Verify funds are escrowed, not transferred yet vm.assertEq(t.usdcBase.balanceOf(address(t.escrowBase)), 600); vm.assertEq(t.usdcBase.balanceOf(t.relay), 0); @@ -1563,7 +1556,7 @@ contract OrchestratorTest is BaseTest { // User has 500 USDC on arb t.usdcArb.mint(t.d.eoa, 500); // Unhappy case, try to send base intent to arb - t.encodedIntents[0] = abi.encode(t.baseIntent); + t.encodedIntents[0] = encodeIntent(t.baseIntent); vm.prank(t.gasWallet); t.errs = oc.execute(t.encodedIntents); assertEq( @@ -1575,15 +1568,15 @@ contract OrchestratorTest is BaseTest { bytes32[] memory wrongLeafs = new bytes32[](3); // Some random leaf - wrongLeafs[0] = oc.computeDigest(t.arbIntent); - wrongLeafs[1] = oc.computeDigest(t.arbIntent); - wrongLeafs[2] = oc.computeDigest(t.outputIntent); + wrongLeafs[0] = computeDigest(t.arbIntent); + wrongLeafs[1] = computeDigest(t.arbIntent); + wrongLeafs[2] = computeDigest(t.outputIntent); bytes memory correctSig = t.arbIntent.signature; t.arbIntent.signature = abi.encode(merkleHelper.getProof(wrongLeafs, 1), t.root, t.rootSig); - t.encodedIntents[0] = abi.encode(t.arbIntent); + t.encodedIntents[0] = encodeIntent(t.arbIntent); vm.prank(t.gasWallet); t.errs = oc.execute(t.encodedIntents); assertEq( @@ -1596,7 +1589,7 @@ contract OrchestratorTest is BaseTest { } // User escrows funds on Arb - t.encodedIntents[0] = abi.encode(t.arbIntent); + t.encodedIntents[0] = encodeIntent(t.arbIntent); vm.expectEmit(true, false, false, false, address(t.escrowArb)); emit Escrow.EscrowCreated(t.escrowIdArb); vm.prank(t.gasWallet); @@ -1622,7 +1615,7 @@ contract OrchestratorTest is BaseTest { emit SimpleSettler.Sent(address(oc), t.settlementId, 42161); // Arbitrum // Relay funds the user account, and the intended execution happens. - t.encodedIntents[0] = abi.encode(t.outputIntent); + t.encodedIntents[0] = encodeIntent(t.outputIntent); vm.prank(t.gasWallet); t.errs = oc.execute(t.encodedIntents); assertEq(uint256(bytes32(t.errs[0])), 0); @@ -1657,7 +1650,7 @@ contract OrchestratorTest is BaseTest { // Re-execute the escrow on Base (to recreate the state) t.usdcBase.mint(t.d.eoa, 600); - t.encodedIntents[0] = abi.encode(t.baseIntent); + t.encodedIntents[0] = encodeIntent(t.baseIntent); vm.expectEmit(true, false, false, false, address(t.escrowBase)); emit Escrow.EscrowCreated(t.escrowIdBase); vm.prank(t.gasWallet); @@ -1686,7 +1679,7 @@ contract OrchestratorTest is BaseTest { // Re-execute the escrow on Arbitrum (to recreate the state) t.usdcArb.mint(t.d.eoa, 500); - t.encodedIntents[0] = abi.encode(t.arbIntent); + t.encodedIntents[0] = encodeIntent(t.arbIntent); vm.expectEmit(true, false, false, false, address(t.escrowArb)); emit Escrow.EscrowCreated(t.escrowIdArb); vm.prank(t.gasWallet); @@ -1733,7 +1726,7 @@ contract OrchestratorTest is BaseTest { t.outputIntent.funderSignature = _eoaSig(wrongPrivateKey, t.leafs[2]); - t.encodedIntents[0] = abi.encode(t.outputIntent); + t.encodedIntents[0] = encodeIntent(t.outputIntent); vm.prank(t.gasWallet); t.errs = oc.execute(t.encodedIntents); @@ -1756,11 +1749,11 @@ contract OrchestratorTest is BaseTest { function _computeMerkleData(_TestMultiChainIntentTemps memory t) internal { t.leafs = new bytes32[](3); vm.chainId(8453); - t.leafs[0] = oc.computeDigest(t.baseIntent); + t.leafs[0] = computeDigest(t.baseIntent); vm.chainId(42161); - t.leafs[1] = oc.computeDigest(t.arbIntent); + t.leafs[1] = computeDigest(t.arbIntent); vm.chainId(1); - t.leafs[2] = oc.computeDigest(t.outputIntent); + t.leafs[2] = computeDigest(t.outputIntent); t.root = merkleHelper.getRoot(t.leafs); @@ -1769,8 +1762,23 @@ contract OrchestratorTest is BaseTest { t.outputIntent.funderSignature = _eoaSig(t.funderPrivateKey, t.leafs[2]); - t.baseIntent.signature = abi.encode(merkleHelper.getProof(t.leafs, 0), t.root, t.rootSig); - t.arbIntent.signature = abi.encode(merkleHelper.getProof(t.leafs, 1), t.root, t.rootSig); - t.outputIntent.signature = abi.encode(merkleHelper.getProof(t.leafs, 2), t.root, t.rootSig); + t.baseIntent.signature = abi.encodePacked( + abi.encode(merkleHelper.getProof(t.leafs, 0), t.root, t.rootSig), + bytes32(0), + uint8(0), + uint8(1) + ); + t.arbIntent.signature = abi.encodePacked( + abi.encode(merkleHelper.getProof(t.leafs, 1), t.root, t.rootSig), + bytes32(0), + uint8(0), + uint8(1) + ); + t.outputIntent.signature = abi.encodePacked( + abi.encode(merkleHelper.getProof(t.leafs, 2), t.root, t.rootSig), + bytes32(0), + uint8(0), + uint8(1) + ); } } diff --git a/test/SimulateExecute.t.sol b/test/SimulateExecute.t.sol index 1e4a89a4..8e2d52b5 100644 --- a/test/SimulateExecute.t.sol +++ b/test/SimulateExecute.t.sol @@ -58,7 +58,7 @@ contract SimulateExecuteTest is BaseTest { abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) ); - Orchestrator.Intent memory i; + Intent memory i; i.eoa = d.eoa; i.nonce = 0; i.executionData = t.executionData; @@ -79,19 +79,19 @@ contract SimulateExecuteTest is BaseTest { // If the caller does not have max balance, then the simulation should revert. vm.expectRevert(bytes4(keccak256("StateOverrideError()"))); (t.gUsed, t.gCombined) = - simulator.simulateV1Logs(address(oc), 0, 1, 11_000, 10_000, abi.encode(i)); + simulator.simulateV1Logs(address(oc), 0, 1, 11_000, 10_000, encodeIntent(i)); vm.expectRevert(bytes4(keccak256("StateOverrideError()"))); - oc.simulateExecute(true, type(uint256).max, abi.encode(i)); + oc.simulateExecute(encodeIntent(i, true, type(uint256).max)); vm.expectPartialRevert(bytes4(keccak256("SimulationPassed(uint256)"))); - oc.simulateExecute(false, type(uint256).max, abi.encode(i)); + oc.simulateExecute(encodeIntent(i, false, type(uint256).max)); uint256 snapshot = vm.snapshotState(); vm.deal(_ORIGIN_ADDRESS, type(uint192).max); (t.gUsed, t.gCombined) = - simulator.simulateV1Logs(address(oc), 2, 1e11, 11_000, 0, abi.encode(i)); + simulator.simulateV1Logs(address(oc), 2, 1e11, 11_000, 0, encodeIntent(i)); vm.revertToStateAndDelete(snapshot); i.combinedGas = t.gCombined; @@ -101,11 +101,11 @@ contract SimulateExecuteTest is BaseTest { i.signature = _sig(d, i); vm.expectRevert(bytes4(keccak256("InsufficientGas()"))); - oc.execute{gas: t.gExecute}(abi.encode(i)); + oc.execute{gas: t.gExecute}(encodeIntent(i)); t.gExecute = Math.mulDiv(t.gCombined + 110_000, 64, 63); - assertEq(oc.execute{gas: t.gExecute}(abi.encode(i)), 0); + assertEq(oc.execute{gas: t.gExecute}(encodeIntent(i)), 0); } function testSimulateExecuteNoRevertUnderfundedReverts() public { @@ -127,7 +127,7 @@ contract SimulateExecuteTest is BaseTest { abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) ); - Orchestrator.Intent memory i; + Intent memory i; i.eoa = d.eoa; i.nonce = 0; i.executionData = t.executionData; @@ -146,11 +146,11 @@ contract SimulateExecuteTest is BaseTest { } vm.expectRevert(bytes4(keccak256("PaymentError()"))); - simulator.simulateV1Logs(address(oc), 0, 1, 11_000, 0, abi.encode(i)); + simulator.simulateV1Logs(address(oc), 0, 1, 11_000, 0, encodeIntent(i)); deal(i.paymentToken, address(i.eoa), 0x112233112233112233112233); vm.expectRevert(bytes4(keccak256("PaymentError()"))); - simulator.simulateCombinedGas(address(oc), 0, 1, 11_000, abi.encode(i)); + simulator.simulateCombinedGas(address(oc), 0, 1, 11_000, encodeIntent(i)); } function testSimulateExecuteNoRevert() public { @@ -173,7 +173,7 @@ contract SimulateExecuteTest is BaseTest { abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) ); - Orchestrator.Intent memory i; + Intent memory i; i.eoa = d.eoa; i.nonce = 0; i.executionData = t.executionData; @@ -195,7 +195,7 @@ contract SimulateExecuteTest is BaseTest { vm.deal(_ORIGIN_ADDRESS, type(uint192).max); (t.gUsed, t.gCombined) = - simulator.simulateV1Logs(address(oc), 2, 1e11, 11_000, 0, abi.encode(i)); + simulator.simulateV1Logs(address(oc), 2, 1e11, 11_000, 0, encodeIntent(i)); vm.revertToStateAndDelete(snapshot); @@ -205,7 +205,7 @@ contract SimulateExecuteTest is BaseTest { i.signature = _sig(d, i); - assertEq(oc.execute{gas: t.gExecute}(abi.encode(i)), 0); + assertEq(oc.execute{gas: t.gExecute}(encodeIntent(i)), 0); assertEq(gasBurner.randomness(), t.randomness); } @@ -229,7 +229,7 @@ contract SimulateExecuteTest is BaseTest { abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) ); - Orchestrator.Intent memory i; + Intent memory i; i.eoa = d.eoa; i.nonce = 0; i.executionData = t.executionData; @@ -251,7 +251,7 @@ contract SimulateExecuteTest is BaseTest { vm.deal(_ORIGIN_ADDRESS, type(uint192).max); (t.gUsed, t.gCombined) = - simulator.simulateV1Logs(address(oc), 2, 1e11, 10_800, 0, abi.encode(i)); + simulator.simulateV1Logs(address(oc), 2, 1e11, 10_800, 0, encodeIntent(i)); vm.revertToStateAndDelete(snapshot); @@ -261,7 +261,7 @@ contract SimulateExecuteTest is BaseTest { i.signature = _sig(d, i); - assertEq(oc.execute{gas: t.gExecute}(abi.encode(i)), 0); + assertEq(oc.execute{gas: t.gExecute}(encodeIntent(i)), 0); assertEq(gasBurner.randomness(), t.randomness); } @@ -289,7 +289,7 @@ contract SimulateExecuteTest is BaseTest { abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) ); - Orchestrator.Intent memory i; + Intent memory i; i.eoa = d.eoa; i.nonce = 0; i.executionData = t.executionData; @@ -312,7 +312,7 @@ contract SimulateExecuteTest is BaseTest { vm.deal(_ORIGIN_ADDRESS, type(uint192).max); (t.gUsed, t.gCombined) = - simulator.simulateV1Logs(address(oc), 2, 1e11, 12_000, 10_000, abi.encode(i)); + simulator.simulateV1Logs(address(oc), 2, 1e11, 12_000, 10_000, encodeIntent(i)); vm.revertToStateAndDelete(snapshot); @@ -321,7 +321,7 @@ contract SimulateExecuteTest is BaseTest { i.signature = _sig(k, i); - assertEq(oc.execute{gas: t.gExecute}(abi.encode(i)), 0); + assertEq(oc.execute{gas: t.gExecute}(encodeIntent(i)), 0); assertEq(gasBurner.randomness(), t.randomness); } diff --git a/test/SubAccounts.t.sol b/test/SubAccounts.t.sol new file mode 100644 index 00000000..68417582 --- /dev/null +++ b/test/SubAccounts.t.sol @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "./Base.t.sol"; +import {ERC20} from "solady/tokens/ERC20.sol"; +import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; +import {ERC7821} from "solady/accounts/ERC7821.sol"; + +/// @title SubAccounts Test Suite +/// @notice Tests the parent-child account architecture with spending permissions +contract SubAccountsTest is BaseTest { + using SafeTransferLib for address; + + // Main account (parent) - controlled by user + DelegatedEOA mainAccount; + PassKey mainAccountKey; + + // Sub account (child) - controlled by DApp + DelegatedEOA subAccount; + PassKey dappKey; + + // Test tokens + MockPaymentToken usdc; + MockPaymentToken dai; + + // DApp that controls the sub account + address dappAddress; + address constant dappRecipient = address(0xDA99); + + // Spend IDs for tracking permissions + uint256 constant PARENT_SWEEP_SPEND_ID = 1; + uint256 constant CHILD_PULL_SPEND_ID = 2; + + function setUp() public override { + super.setUp(); + + // Setup tokens + usdc = new MockPaymentToken(); + dai = new MockPaymentToken(); + + // Setup main account (parent) + mainAccount = _randomEIP7702DelegatedEOA(); + mainAccountKey = _randomPassKey(); + mainAccountKey.k.isSuperAdmin = true; // Main account key needs to be super admin to call setSpend + vm.prank(address(mainAccount.d)); + mainAccount.d.authorize(mainAccountKey.k); + + // Setup sub account (controlled by DApp) + subAccount = _randomEIP7702DelegatedEOA(); + dappKey = _randomPassKey(); + dappKey.k.isSuperAdmin = true; // DApp has full control of sub account + vm.prank(address(subAccount.d)); + subAccount.d.authorize(dappKey.k); + + // Setup DApp address + dappAddress = vm.addr(uint256(keccak256("dapp"))); + } + + /// @notice Tests the complete subaccount flow with DApp integration + /// Flow: + /// 1. Create main account with funds + /// 2. Create sub account (controlled by DApp, no initial funds needed) + /// 3. Sub account grants parent sweep permission + /// 4. Main account grants sub account limited pull permission + /// 5. DApp executes bundle: pulls funds from main, sends to DApp + /// 6. Parent can sweep remaining funds back + function test_CompleteSubAccountFlowWithDApp() public { + // ============================================ + // STEP 1: Fund the main account + // ============================================ + uint256 mainAccountInitialBalance = 1000e6; // 1000 USDC + usdc.mint(address(mainAccount.eoa), mainAccountInitialBalance); + dai.mint(address(mainAccount.eoa), 500e18); // 500 DAI + + assertEq(usdc.balanceOf(address(mainAccount.eoa)), mainAccountInitialBalance); + assertEq(dai.balanceOf(address(mainAccount.eoa)), 500e18); + + // Sub account starts with 0 funds (DApp pays for gas) + assertEq(usdc.balanceOf(address(subAccount.eoa)), 0); + + // ============================================ + // STEP 2: Sub account grants parent sweep permission + // This allows parent to recover any funds from sub account + // ============================================ + { + // Create setSpend call to grant parent sweep permission + ERC7821.Call[] memory calls = new ERC7821.Call[](1); + + calls[0] = ERC7821.Call({ + to: address(subAccount.d), // Call to self + value: 0, + data: abi.encodeWithSelector( + IthacaAccount.setSpend.selector, + PARENT_SWEEP_SPEND_ID, + true, // isParent = true (can sweep everything) + uint32(0), // No expiry for parent + address(mainAccount.eoa), // Parent is the spender + new address[](0), // Tokens are ignored, because parent can sweep everything + new uint256[](0) // Limits are ignored, because parent can sweep everything + ) + }); + + // Execute as the subAccount EOA directly (no signature needed) + vm.prank(subAccount.eoa); + subAccount.d.execute(_ERC7821_BATCH_EXECUTION_MODE, abi.encode(calls)); + } + + // ============================================ + // STEP 3: Main account grants sub account limited pull permission + // This allows sub account to pull up to 100 USDC and 50 DAI + // ============================================ + { + // Create setSpend call to grant sub account limited permission + ERC7821.Call[] memory calls = new ERC7821.Call[](1); + address[] memory tokens = new address[](2); + tokens[0] = address(usdc); + tokens[1] = address(dai); + uint256[] memory limits = new uint256[](2); + limits[0] = 100e6; // 100 USDC limit + limits[1] = 50e18; // 50 DAI limit + + calls[0] = ERC7821.Call({ + to: address(mainAccount.d), // Call to self + value: 0, + data: abi.encodeWithSelector( + IthacaAccount.setSpend.selector, + CHILD_PULL_SPEND_ID, + false, // isParent = false (limited permissions) + uint32(block.timestamp + 30 days), // 30 day expiry + address(subAccount.eoa), // Sub account is the spender + tokens, + limits + ) + }); + + // Execute as the mainAccount EOA directly (no signature needed) + vm.prank(mainAccount.eoa); + mainAccount.d.execute(_ERC7821_BATCH_EXECUTION_MODE, abi.encode(calls)); + } + + // ============================================ + // STEP 4: Test direct spend call from subAccount to mainAccount + // ============================================ + uint256 amountToPull = 50e6; // 50 USDC + { + // SubAccount calls spend on mainAccount directly + address[] memory pullTokens = new address[](1); + pullTokens[0] = address(usdc); + uint256[] memory pullAmounts = new uint256[](1); + pullAmounts[0] = amountToPull; + + vm.prank(subAccount.eoa); + mainAccount.d.spend( + CHILD_PULL_SPEND_ID, pullTokens, pullAmounts, address(subAccount.eoa) + ); + + // Verify the transfer worked + assertEq( + usdc.balanceOf(address(mainAccount.eoa)), mainAccountInitialBalance - amountToPull + ); + assertEq(usdc.balanceOf(address(subAccount.eoa)), amountToPull); + } + + // ============================================ + // STEP 5: DApp executes bundle via sub account + // - Pulls more funds from main account + // - Then sends to DApp recipient + // ============================================ + uint256 secondPullAmount = 30e6; // 30 USDC + { + Orchestrator.Intent memory intent; + intent.eoa = address(subAccount.eoa); + intent.nonce = subAccount.d.getNonce(0); + intent.expiry = block.timestamp + 1 days; + intent.paymentToken = address(paymentToken); + intent.paymentAmount = 0 ether; + intent.paymentRecipient = address(0xfee); + intent.combinedGas = 1000000; + + // Create bundle: pull from main, then send to DApp + ERC7821.Call[] memory calls = new ERC7821.Call[](2); + + address[] memory pullTokens = new address[](1); + pullTokens[0] = address(usdc); + uint256[] memory pullAmounts = new uint256[](1); + pullAmounts[0] = secondPullAmount; + + calls[0] = ERC7821.Call({ + to: address(mainAccount.d), + value: 0, + data: abi.encodeWithSelector( + IthacaAccount.spend.selector, + CHILD_PULL_SPEND_ID, + pullTokens, + pullAmounts, + address(subAccount.eoa) + ) + }); + + // Send all funds to DApp (first pull + second pull) + calls[1] = ERC7821.Call({ + to: address(usdc), + value: 0, + data: abi.encodeWithSelector( + ERC20.transfer.selector, dappRecipient, amountToPull + secondPullAmount + ) + }); + + intent.executionData = abi.encode(calls); + intent.signature = _sig(dappKey, intent); + + // Execute the bundle + assertEq(oc.execute(abi.encode(intent)), 0); + + // Verify balances + assertEq( + usdc.balanceOf(address(mainAccount.eoa)), + mainAccountInitialBalance - amountToPull - secondPullAmount + ); + assertEq(usdc.balanceOf(address(subAccount.eoa)), 0); + assertEq(usdc.balanceOf(dappRecipient), amountToPull + secondPullAmount); + } + + // ============================================ + // STEP 6: Test that sub account cannot exceed limits (should fail) + // ============================================ + { + // Try to pull 21 more USDC (total would be 50+30+21 = 101, exceeding 100 limit) + address[] memory pullTokens = new address[](1); + pullTokens[0] = address(usdc); + uint256[] memory pullAmounts = new uint256[](1); + pullAmounts[0] = 21e6; // This would exceed the 100 USDC limit + + // Should revert due to exceeding limit + vm.prank(subAccount.eoa); + vm.expectRevert(); + mainAccount.d.spend( + CHILD_PULL_SPEND_ID, pullTokens, pullAmounts, address(subAccount.eoa) + ); + } + + // // ============================================ + // // STEP 7: Parent sweeps funds back from sub account + // // ============================================ + { + // Parent can sweep everything from sub account + address[] memory sweepTokens = new address[](1); + sweepTokens[0] = address(usdc); + uint256[] memory sweepAmounts = new uint256[](1); + sweepAmounts[0] = usdc.balanceOf(address(subAccount.eoa)); + + uint256 mainAccountPreBalance = usdc.balanceOf(address(mainAccount.eoa)); + + // Parent directly calls spend on sub account + vm.prank(mainAccount.eoa); + subAccount.d.spend( + PARENT_SWEEP_SPEND_ID, + sweepTokens, + sweepAmounts, + address(mainAccount.eoa) // Sweep back to parent + ); + + // Verify parent recovered all the funds + assertEq(usdc.balanceOf(address(subAccount.eoa)), 0); + assertEq( + usdc.balanceOf(address(mainAccount.eoa)), mainAccountPreBalance + sweepAmounts[0] + ); + } + } +} diff --git a/test/UpgradeTests.t.sol b/test/UpgradeTests.t.sol deleted file mode 100644 index d0d28d09..00000000 --- a/test/UpgradeTests.t.sol +++ /dev/null @@ -1,566 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.4; - -import "./utils/SoladyTest.sol"; -import {IthacaAccount} from "./utils/mocks/MockAccount.sol"; -import {GuardedExecutor} from "../src/IthacaAccount.sol"; -import {BaseTest} from "./Base.t.sol"; -import {EIP7702Proxy} from "solady/accounts/EIP7702Proxy.sol"; -import {LibEIP7702} from "solady/accounts/LibEIP7702.sol"; -import {MockPaymentToken} from "./utils/mocks/MockPaymentToken.sol"; -import {LibClone} from "solady/utils/LibClone.sol"; -import {Orchestrator, MockOrchestrator} from "./utils/mocks/MockOrchestrator.sol"; -import {ERC7821} from "solady/accounts/ERC7821.sol"; - -contract UpgradeTests is BaseTest { - address payable public oldProxyAddress; - address public oldImplementation; - address public newImplementation; - - // Test EOA that will be delegated to the proxy - address public userEOA; - uint256 public userEOAKey; - IthacaAccount public userAccount; - - // Test keys - PassKey public p256Key; - PassKey public p256SuperAdminKey; - PassKey public secp256k1Key; - PassKey public secp256k1SuperAdminKey; - PassKey public webAuthnP256Key; - PassKey public externalKey; - - // Test tokens - MockPaymentToken public mockToken1; - MockPaymentToken public mockToken2; - - // Random addresses for testing transfers - address[] public randomRecipients; - - // State capture - using simple mappings to avoid memory-to-storage issues - // Pre-upgrade state - bytes32[] preKeyHashes; - mapping(bytes32 => IthacaAccount.Key) preKeys; - mapping(bytes32 => bool) preAuthorized; - uint256 preEthBalance; - uint256 preToken1Balance; - uint256 preToken2Balance; - uint256 preNonce; - // Get expected old version from environment variable - string public expectedOldVersion = vm.envString("UPGRADE_TEST_OLD_VERSION"); - - // Post-upgrade state - bytes32[] postKeyHashes; - mapping(bytes32 => IthacaAccount.Key) postKeys; - mapping(bytes32 => bool) postAuthorized; - uint256 postEthBalance; - uint256 postToken1Balance; - uint256 postToken2Balance; - uint256 postNonce; - - function setUp() public override { - super.setUp(); - - // Fork the network to get the proxy bytecode - string memory rpcUrl = vm.envString("UPGRADE_TEST_RPC_URL"); - vm.createSelectFork(rpcUrl); - - // Deploy test tokens - mockToken1 = new MockPaymentToken(); - mockToken2 = new MockPaymentToken(); - - // Setup random recipients - for (uint256 i = 0; i < 5; i++) { - randomRecipients.push(_randomAddress()); - } - - // Get old proxy address from environment - oldProxyAddress = payable(vm.envAddress("UPGRADE_TEST_OLD_PROXY")); - - // Setup delegated EOA - (userEOA, userEOAKey) = _randomUniqueSigner(); - - vm.etch(userEOA, abi.encodePacked(hex"ef0100", address(oldProxyAddress))); - - userAccount = IthacaAccount(payable(userEOA)); - - // Get the bytecode of the old proxy from the forked network - bytes memory proxyBytecode = oldProxyAddress.code; - require(proxyBytecode.length > 0, "No bytecode at old proxy address"); - - newImplementation = address(new IthacaAccount(address(oc))); - - // Generate test keys - p256Key = _randomSecp256r1PassKey(); - p256Key.k.isSuperAdmin = false; - p256Key.k.expiry = 0; // Never expires - - p256SuperAdminKey = _randomSecp256r1PassKey(); - p256SuperAdminKey.k.isSuperAdmin = true; - p256SuperAdminKey.k.expiry = uint40(block.timestamp + 365 days); // Expires in 1 year - - secp256k1Key = _randomSecp256k1PassKey(); - secp256k1Key.k.isSuperAdmin = false; - secp256k1Key.k.expiry = 0; - - secp256k1SuperAdminKey = _randomSecp256k1PassKey(); - secp256k1SuperAdminKey.k.isSuperAdmin = true; - secp256k1SuperAdminKey.k.expiry = uint40(block.timestamp + 30 days); // Expires in 30 days - - webAuthnP256Key = _randomSecp256r1PassKey(); - webAuthnP256Key.k.keyType = IthacaAccount.KeyType.WebAuthnP256; - webAuthnP256Key.k.isSuperAdmin = false; - webAuthnP256Key.k.expiry = 0; - - // Setup external key - address externalSigner = _randomAddress(); - bytes12 salt = bytes12(uint96(_randomUniform())); - externalKey.k.keyType = IthacaAccount.KeyType.External; - externalKey.k.publicKey = abi.encodePacked(externalSigner, salt); - externalKey.k.isSuperAdmin = false; - externalKey.k.expiry = uint40(block.timestamp + 7 days); - externalKey.keyHash = _hash(externalKey.k); - } - - function test_ComprehensiveUpgrade() public { - // Step 2: Setup the old account with various configurations - _setupOldAccountState(); - - // Step 3: Capture pre-upgrade state - _capturePreUpgradeState(); - - // Step 4: Perform upgrade - _performUpgrade(); - - // Step 5: Capture post-upgrade state - _capturePostUpgradeState(); - - // Step 6: Verify state preservation - _verifyStatePreservation(); - - // Step 7: Test post-upgrade functionality - _testPostUpgradeFunctionality(); - } - - function _performUpgrade() internal { - // Get the version from the new implementation for comparison - IthacaAccount newImpl = IthacaAccount(payable(newImplementation)); - (,, string memory expectedNewVersion,,,,) = newImpl.eip712Domain(); - - // Check version before upgrade matches expected old version - (,, string memory versionBefore,,,,) = userAccount.eip712Domain(); - assertEq( - keccak256(bytes(versionBefore)), - keccak256(bytes(expectedOldVersion)), - string.concat("Version before upgrade should be ", expectedOldVersion) - ); - - // Perform the upgrade - bytes memory upgradeCalldata = - abi.encodeWithSelector(IthacaAccount.upgradeProxyAccount.selector, newImplementation); - - vm.prank(userEOA); - (bool success,) = userEOA.call(upgradeCalldata); - require(success, "Upgrade failed"); - - // Check version after upgrade - (,, string memory versionAfter,,,,) = userAccount.eip712Domain(); - - // Verify the version matches the new implementation version after upgrade - assertEq( - keccak256(bytes(versionAfter)), - keccak256(bytes(expectedNewVersion)), - string.concat("Version after upgrade should be ", expectedNewVersion) - ); - } - - function _setupOldAccountState() internal { - // Authorize various keys - vm.startPrank(userEOA); - - userAccount.authorize(p256Key.k); - p256Key.keyHash = _hash(p256Key.k); - - userAccount.authorize(secp256k1Key.k); - secp256k1Key.keyHash = _hash(secp256k1Key.k); - - userAccount.authorize(secp256k1SuperAdminKey.k); - secp256k1SuperAdminKey.keyHash = _hash(secp256k1SuperAdminKey.k); - - userAccount.authorize(webAuthnP256Key.k); - webAuthnP256Key.keyHash = _hash(webAuthnP256Key.k); - - externalKey.keyHash = userAccount.authorize(externalKey.k); - - // Setup spending limits for different keys - _setupSpendingLimits(); - - // Setup execution permissions - _setupExecutionPermissions(); - - // Fund the account - _fundAccount(); - - vm.stopPrank(); - } - - function _setupSpendingLimits() internal { - // Only set spending limits for non-super admin keys - - // Daily ETH limit for secp256k1Key (not a super admin) - if (secp256k1Key.keyHash != bytes32(0)) { - userAccount.setSpendLimit( - secp256k1Key.keyHash, address(0), GuardedExecutor.SpendPeriod.Day, 1 ether - ); - - // Weekly ETH limit for secp256k1Key - userAccount.setSpendLimit( - secp256k1Key.keyHash, address(0), GuardedExecutor.SpendPeriod.Week, 5 ether - ); - - // Monthly token1 limit for secp256k1Key - userAccount.setSpendLimit( - secp256k1Key.keyHash, - address(mockToken1), - GuardedExecutor.SpendPeriod.Month, - 1000e18 - ); - } - - // Daily token2 limit for webAuthnP256Key (not a super admin) - if (webAuthnP256Key.keyHash != bytes32(0)) { - userAccount.setSpendLimit( - webAuthnP256Key.keyHash, - address(mockToken2), - GuardedExecutor.SpendPeriod.Day, - 100e18 - ); - } - - // Hour ETH limit for externalKey (not a super admin) - if (externalKey.keyHash != bytes32(0)) { - userAccount.setSpendLimit( - externalKey.keyHash, address(0), GuardedExecutor.SpendPeriod.Hour, 0.1 ether - ); - } - - // Forever limit for p256Key (if authorized and not a super admin) - if (p256Key.keyHash != bytes32(0) && !p256Key.k.isSuperAdmin) { - userAccount.setSpendLimit( - p256Key.keyHash, address(0), GuardedExecutor.SpendPeriod.Forever, 10 ether - ); - } - } - - function _setupExecutionPermissions() internal { - // Setup canExecute permissions (only for non-super admin keys) - address target1 = address(0x1234); - address target2 = address(0x5678); - bytes4 selector1 = bytes4(keccak256("transfer(address,uint256)")); - bytes4 selector2 = bytes4(keccak256("approve(address,uint256)")); - - // Only set for non-super admin secp256k1Key - if (secp256k1Key.keyHash != bytes32(0) && !secp256k1Key.k.isSuperAdmin) { - userAccount.setCanExecute(secp256k1Key.keyHash, target1, selector1, true); - userAccount.setCanExecute(secp256k1Key.keyHash, target2, selector2, true); - } - - // Only set for p256Key if it's not a super admin - if (p256Key.keyHash != bytes32(0) && !p256Key.k.isSuperAdmin) { - userAccount.setCanExecute(p256Key.keyHash, target1, selector2, true); - } - - // Only set for webAuthnP256Key if it's not a super admin - if (webAuthnP256Key.keyHash != bytes32(0) && !webAuthnP256Key.k.isSuperAdmin) { - userAccount.setCanExecute(webAuthnP256Key.keyHash, target2, selector1, true); - } - - // Setup call checkers (only for non-super admin keys) - address checker1 = address(0xAAAA); - address checker2 = address(0xBBBB); - - if (secp256k1Key.keyHash != bytes32(0) && !secp256k1Key.k.isSuperAdmin) { - userAccount.setCallChecker(secp256k1Key.keyHash, target1, checker1); - } - - if (webAuthnP256Key.keyHash != bytes32(0) && !webAuthnP256Key.k.isSuperAdmin) { - userAccount.setCallChecker(webAuthnP256Key.keyHash, target2, checker2); - } - } - - function _fundAccount() internal { - // Fund with ETH - vm.deal(address(userAccount), 10 ether); - - // Fund with tokens - mockToken1.mint(address(userAccount), 10000e18); - mockToken2.mint(address(userAccount), 5000e18); - } - - function _capturePreUpgradeState() internal { - // Capture authorized keys - (, bytes32[] memory keyHashes) = userAccount.getKeys(); - - // Clear and populate pre-upgrade key hashes - delete preKeyHashes; - for (uint256 i = 0; i < keyHashes.length; i++) { - preKeyHashes.push(keyHashes[i]); - bytes32 keyHash = keyHashes[i]; - preKeys[keyHash] = userAccount.getKey(keyHash); - preAuthorized[keyHash] = true; - } - - // Capture balances - preEthBalance = address(userAccount).balance; - preToken1Balance = mockToken1.balanceOf(address(userAccount)); - preToken2Balance = mockToken2.balanceOf(address(userAccount)); - - // Capture nonce - preNonce = userAccount.getNonce(0); - } - - function _capturePostUpgradeState() internal { - // Capture authorized keys - (, bytes32[] memory keyHashes) = userAccount.getKeys(); - - // Clear and populate post-upgrade key hashes - delete postKeyHashes; - for (uint256 i = 0; i < keyHashes.length; i++) { - postKeyHashes.push(keyHashes[i]); - bytes32 keyHash = keyHashes[i]; - postKeys[keyHash] = userAccount.getKey(keyHash); - postAuthorized[keyHash] = true; - } - - // Capture balances - postEthBalance = address(userAccount).balance; - postToken1Balance = mockToken1.balanceOf(address(userAccount)); - postToken2Balance = mockToken2.balanceOf(address(userAccount)); - - // Capture nonce - postNonce = userAccount.getNonce(0); - } - - function _verifyStatePreservation() internal view { - // Verify all keys are preserved - assertEq(preKeyHashes.length, postKeyHashes.length, "Number of authorized keys changed"); - - for (uint256 i = 0; i < preKeyHashes.length; i++) { - bytes32 keyHash = preKeyHashes[i]; - - assertTrue(postAuthorized[keyHash], "Key was deauthorized during upgrade"); - - IthacaAccount.Key memory preKey = preKeys[keyHash]; - IthacaAccount.Key memory postKey = postKeys[keyHash]; - - assertEq(preKey.expiry, postKey.expiry, "Key expiry changed"); - assertEq(uint8(preKey.keyType), uint8(postKey.keyType), "Key type changed"); - assertEq(preKey.isSuperAdmin, postKey.isSuperAdmin, "Key super admin status changed"); - assertEq(preKey.publicKey, postKey.publicKey, "Key public key changed"); - } - - // Verify balances preserved - assertEq(preEthBalance, postEthBalance, "ETH balance changed"); - assertEq(preToken1Balance, postToken1Balance, "Token1 balance changed"); - assertEq(preToken2Balance, postToken2Balance, "Token2 balance changed"); - - // Verify nonce preserved - assertEq(preNonce, postNonce, "Nonce changed"); - } - - function _testPostUpgradeFunctionality() internal { - vm.startPrank(userEOA); - - // Test 1: P256 keys can now be super admins (new in v0.5.7+) - PassKey memory newP256SuperAdmin = _randomSecp256r1PassKey(); - newP256SuperAdmin.k.isSuperAdmin = true; - newP256SuperAdmin.k.expiry = 0; - - // This should succeed in upgraded version - bytes32 newP256KeyHash = userAccount.authorize(newP256SuperAdmin.k); - IthacaAccount.Key memory retrievedKey = userAccount.getKey(newP256KeyHash); - assertEq( - uint8(retrievedKey.keyType), uint8(IthacaAccount.KeyType.P256), "Key type mismatch" - ); - assertTrue(retrievedKey.isSuperAdmin, "P256 should be super admin after upgrade"); - - // Test 2: Add a new non-super-admin key and set spending limit - PassKey memory newRegularKey = _randomSecp256k1PassKey(); - newRegularKey.k.isSuperAdmin = false; - newRegularKey.k.expiry = 0; - - bytes32 newRegularKeyHash = userAccount.authorize(newRegularKey.k); - - // Set spending limit for the regular key (not super admin) - userAccount.setSpendLimit( - newRegularKeyHash, address(0), GuardedExecutor.SpendPeriod.Week, 2 ether - ); - - GuardedExecutor.SpendInfo[] memory spendInfos = userAccount.spendInfos(newRegularKeyHash); - bool foundWeeklyLimit = false; - for (uint256 i = 0; i < spendInfos.length; i++) { - if ( - spendInfos[i].period == GuardedExecutor.SpendPeriod.Week - && spendInfos[i].token == address(0) - ) { - assertEq(spendInfos[i].limit, 2 ether, "Weekly limit not set correctly"); - foundWeeklyLimit = true; - break; - } - } - assertTrue(foundWeeklyLimit, "Weekly limit not found"); - - // Test 3: Verify keys can still be used (without actual execution) - // We verify the key is still authorized and has correct properties - IthacaAccount.Key memory existingKey = userAccount.getKey(secp256k1Key.keyHash); - assertEq( - uint8(existingKey.keyType), uint8(IthacaAccount.KeyType.Secp256k1), "Key type changed" - ); - assertFalse(existingKey.isSuperAdmin, "Key admin status changed"); - - // Test 4: Test revoke and re-authorize with a new key - // Create a new key to test revoke/re-authorize functionality - PassKey memory testRevokeKey = _randomSecp256k1PassKey(); - testRevokeKey.k.isSuperAdmin = false; - testRevokeKey.k.expiry = 0; - - bytes32 testRevokeKeyHash = userAccount.authorize(testRevokeKey.k); - - // Now revoke it - userAccount.revoke(testRevokeKeyHash); - - // Verify key is revoked by checking it no longer exists - // After revocation, getKey will revert with KeyDoesNotExist - vm.expectRevert(abi.encodeWithSelector(IthacaAccount.KeyDoesNotExist.selector)); - userAccount.getKey(testRevokeKeyHash); - - // Re-authorize - bytes32 reauthorizedHash = userAccount.authorize(testRevokeKey.k); - assertEq(reauthorizedHash, testRevokeKeyHash, "Key hash changed on re-authorization"); - - vm.stopPrank(); - } - - function test_UpgradeWithSpendLimitEnabledFlag() public { - // This test verifies the spend limit enabled flag feature added in newer versions - - vm.startPrank(userEOA); - - // Authorize a key with spending limits - PassKey memory testKey = _randomSecp256k1PassKey(); - bytes32 keyHash = userAccount.authorize(testKey.k); - - // Set spending limit - userAccount.setSpendLimit(keyHash, address(0), GuardedExecutor.SpendPeriod.Day, 0.5 ether); - - // Fund account - vm.deal(address(userAccount), 5 ether); - - vm.stopPrank(); - - // Perform upgrade - _performUpgrade(); - - // Verify spending limits still work after upgrade - GuardedExecutor.SpendInfo[] memory spendInfos = userAccount.spendInfos(keyHash); - assertEq(spendInfos.length, 1, "Spending limit not preserved"); - assertEq(spendInfos[0].limit, 0.5 ether, "Spending limit value changed"); - - vm.stopPrank(); - } - - function test_UpgradeWithMultipleKeyTypes() public { - // Test upgrade with all key types authorized - - vm.startPrank(userEOA); - - // Authorize all key types - PassKey[] memory keys = new PassKey[](4); - keys[0] = _randomSecp256r1PassKey(); - keys[1] = _randomSecp256k1PassKey(); - keys[2] = _randomSecp256r1PassKey(); - keys[2].k.keyType = IthacaAccount.KeyType.WebAuthnP256; - keys[3].k.keyType = IthacaAccount.KeyType.External; - keys[3].k.publicKey = abi.encodePacked(_randomAddress(), bytes12(uint96(_randomUniform()))); - keys[3].keyHash = _hash(keys[3].k); - - bytes32[] memory keyHashes = new bytes32[](4); - for (uint256 i = 0; i < keys.length; i++) { - // Some key types might fail in old versions, handle gracefully - try userAccount.authorize(keys[i].k) returns (bytes32 kh) { - keyHashes[i] = kh; - } catch { - // Skip if authorization fails - } - } - - // Capture authorized count before upgrade - (, bytes32[] memory keyHashesBefore) = userAccount.getKeys(); - uint256 authorizedCountBefore = keyHashesBefore.length; - - vm.stopPrank(); - - // Perform upgrade - _performUpgrade(); - - // Verify all keys preserved - (, bytes32[] memory keyHashesAfter) = userAccount.getKeys(); - uint256 authorizedCountAfter = keyHashesAfter.length; - assertEq(authorizedCountBefore, authorizedCountAfter, "Key count changed during upgrade"); - - vm.stopPrank(); - } - - function test_UpgradePreservesComplexSpendingState() public { - // Test that complex spending state with partially spent limits is preserved - - vm.startPrank(userEOA); - - // Setup key and limits - PassKey memory spendKey = _randomSecp256k1PassKey(); - bytes32 keyHash = userAccount.authorize(spendKey.k); - - // Set multiple spending limits - userAccount.setSpendLimit(keyHash, address(0), GuardedExecutor.SpendPeriod.Day, 1 ether); - userAccount.setSpendLimit(keyHash, address(0), GuardedExecutor.SpendPeriod.Week, 3 ether); - userAccount.setSpendLimit(keyHash, address(0), GuardedExecutor.SpendPeriod.Month, 10 ether); - - // Fund account - vm.deal(address(userAccount), 20 ether); - vm.stopPrank(); - - // Capture spending state before upgrade - GuardedExecutor.SpendInfo[] memory spendsBefore = userAccount.spendInfos(keyHash); - - // Verify spending limits are set - uint256 limitsCount = 0; - for (uint256 i = 0; i < spendsBefore.length; i++) { - if (spendsBefore[i].token == address(0)) { - limitsCount++; - } - } - assertEq(limitsCount, 3, "Should have 3 ETH spending limits"); - - // Perform upgrade - _performUpgrade(); - - // Verify spending state preserved - GuardedExecutor.SpendInfo[] memory spendsAfter = userAccount.spendInfos(keyHash); - - // Verify all limits still exist - uint256 limitsCountAfter = 0; - for (uint256 i = 0; i < spendsAfter.length; i++) { - if (spendsAfter[i].token == address(0)) { - limitsCountAfter++; - } - } - assertEq(limitsCountAfter, 3, "Spending limits not preserved after upgrade"); - - // Verify limits match - assertEq(spendsBefore.length, spendsAfter.length, "Number of spending limits changed"); - for (uint256 i = 0; i < spendsBefore.length; i++) { - assertEq(spendsBefore[i].limit, spendsAfter[i].limit, "Limit value changed"); - assertEq(uint8(spendsBefore[i].period), uint8(spendsAfter[i].period), "Period changed"); - } - } -} diff --git a/test/utils/Brutalizer.sol b/test/utils/Brutalizer.sol index 1c6447ae..969c5f86 100644 --- a/test/utils/Brutalizer.sol +++ b/test/utils/Brutalizer.sol @@ -825,7 +825,9 @@ contract Brutalizer { let remainder := and(length, 0x1f) if remainder { if shl(mul(8, remainder), lastWord) { notZeroRightPadded := 1 } } // Check if the memory allocated is sufficient. - if length { if gt(add(add(s, 0x20), length), mload(0x40)) { insufficientMalloc := 1 } } + if length { + if gt(add(add(s, 0x20), length), mload(0x40)) { insufficientMalloc := 1 } + } } if (notZeroRightPadded) revert("Not zero right padded!"); if (insufficientMalloc) revert("Insufficient memory allocation!"); diff --git a/test/utils/interfaces/IPimlicoPaymaster.sol b/test/utils/interfaces/IPimlicoPaymaster.sol index 2665aedc..d3d62e0b 100644 --- a/test/utils/interfaces/IPimlicoPaymaster.sol +++ b/test/utils/interfaces/IPimlicoPaymaster.sol @@ -19,10 +19,10 @@ library PimlicoHelpers { /// @notice The length of the mode and allowAllBundlers bytes. uint8 constant MODE_AND_ALLOW_ALL_BUNDLERS_LENGTH = 1; - /// @notice The length of the ERC-20 config without singature. + /// @notice The length of the ERC-20 config without signature. uint8 constant ERC20_PAYMASTER_DATA_LENGTH = 117; - /// @notice The length of the verfiying config without singature. + /// @notice The length of the verifying config without signature. uint8 constant VERIFYING_PAYMASTER_DATA_LENGTH = 12; // 12 uint256 constant PAYMASTER_DATA_OFFSET = 52; diff --git a/test/utils/interfaces/ISafe.sol b/test/utils/interfaces/ISafe.sol deleted file mode 100644 index d9c3d3e1..00000000 --- a/test/utils/interfaces/ISafe.sol +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.28; - -import "./IERC4337EntryPoint.sol"; - -interface ISafe { - function setup( - address[] calldata _owners, - uint256 _threshold, - address to, - bytes calldata data, - address fallbackHandler, - address paymentToken, - uint256 payment, - address paymentReceiver - ) external; - - function enableModule(address module) external; - - function execTransaction( - address to, - uint256 value, - bytes calldata data, - uint8 operation, - uint256 safeTxGas, - uint256 baseGas, - uint256 gasPrice, - address gasToken, - address payable refundReceiver, - bytes memory signatures - ) external payable returns (bool success); - - function getFallbackHandler() external view returns (address); -} - -interface ISafeProxyFactory { - function createProxyWithNonce(address _singleton, bytes memory initializer, uint256 saltNonce) - external - returns (address proxy); -} - -interface ISafe4337Module { - function SUPPORTED_ENTRYPOINT() external view returns (address); - - function getOperationHash(UserOperation calldata userOp) - external - view - returns (bytes32 operationHash); - - function executeUserOp(address to, uint256 value, bytes calldata data, uint8 operation) external; - - function domainSeparator() external view returns (bytes32); -} - -interface IAddModulesLib { - function enableModules(address[] memory modules) external; -} diff --git a/test/utils/mocks/MockOrchestrator.sol b/test/utils/mocks/MockOrchestrator.sol index e21a2360..a0c3226e 100644 --- a/test/utils/mocks/MockOrchestrator.sol +++ b/test/utils/mocks/MockOrchestrator.sol @@ -15,8 +15,13 @@ contract MockOrchestrator is Orchestrator, Brutalizer { return _computeDigest(preCall); } - function computeDigest(Intent calldata intent) public view returns (bytes32) { - return _computeDigest(intent); + // Expose internal functions for testing + function hashTypedData(bytes32 structHash) public view returns (bytes32) { + return _hashTypedData(structHash); + } + + function hashTypedDataSansChainId(bytes32 structHash) public view returns (bytes32) { + return _hashTypedDataSansChainId(structHash); } function simulateFailed(bytes calldata encodedIntent) public payable virtual { diff --git a/test/utils/mocks/MockPayerWithSignature.sol b/test/utils/mocks/MockPayerWithSignature.sol index 1de4c6d5..60678b52 100644 --- a/test/utils/mocks/MockPayerWithSignature.sol +++ b/test/utils/mocks/MockPayerWithSignature.sol @@ -51,38 +51,44 @@ contract MockPayerWithSignature is Ownable { } /// @dev Pays `paymentAmount` of `paymentToken` to the `paymentRecipient`. - /// The EOA and token details are extracted from the `encodedIntent`. - /// Reverts if the specified Orchestrator (`msg.sender`) is not approved, - /// if the signature is invalid, or if the nonce has already been used. - /// @param paymentAmount The amount to pay. - /// @param keyHash The key hash associated with the operation (not used in this mock's logic but kept for signature compatibility). - /// @param digest The digest of the intent (used for nonce tracking). - /// @param encodedIntent ABI encoded Intent struct. + /// @param paymentAmount The amount to pay + /// @param keyHash The hash of the key used to authorize the operation + /// @param intentDigest The digest of the user operation + /// @param eoa The EOA address + /// @param payer The payer address + /// @param paymentToken The token to pay with + /// @param paymentRecipient The recipient of the payment + /// @param paymentSignature The payment signature function pay( uint256 paymentAmount, bytes32 keyHash, - bytes32 digest, - bytes calldata encodedIntent + bytes32 intentDigest, + address eoa, + address payer, + address paymentToken, + address paymentRecipient, + bytes calldata paymentSignature ) public virtual { if (!isApprovedOrchestrator[msg.sender]) revert Unauthorized(); // Check and set nonce to prevent replay attacks - if (paymasterNonces[digest]) { + if (paymasterNonces[intentDigest]) { revert PaymasterNonceError(); } - paymasterNonces[digest] = true; + paymasterNonces[intentDigest] = true; - ICommon.Intent memory u = abi.decode(encodedIntent, (ICommon.Intent)); + bytes32 signatureDigest = computeSignatureDigest(intentDigest); - bytes32 signatureDigest = computeSignatureDigest(digest); - - if (ECDSA.recover(signatureDigest, u.paymentSignature) != signer) { + if (ECDSA.recover(signatureDigest, paymentSignature) != signer) { revert InvalidSignature(); } - TokenTransferLib.safeTransfer(u.paymentToken, u.paymentRecipient, paymentAmount); + TokenTransferLib.safeTransfer(paymentToken, paymentRecipient, paymentAmount); + + emit Compensated(paymentToken, paymentRecipient, paymentAmount, eoa, keyHash); - emit Compensated(u.paymentToken, u.paymentRecipient, paymentAmount, u.eoa, keyHash); + // Unused parameters + payer; } function computeSignatureDigest(bytes32 intentDigest) public view returns (bytes32) { diff --git a/test/utils/mocks/MockPayerWithSignatureOptimized.sol b/test/utils/mocks/MockPayerWithSignatureOptimized.sol deleted file mode 100644 index 1d7701bf..00000000 --- a/test/utils/mocks/MockPayerWithSignatureOptimized.sol +++ /dev/null @@ -1,84 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.4; - -import {TokenTransferLib} from "../../../src/libraries/TokenTransferLib.sol"; -import {Ownable} from "solady/auth/Ownable.sol"; -import {ECDSA} from "solady/utils/ECDSA.sol"; -import {ICommon} from "../../../src/interfaces/ICommon.sol"; -import {IOrchestrator} from "../../../src/interfaces/IOrchestrator.sol"; -/// @dev WARNING! This mock is strictly intended for testing purposes only. -/// Do NOT copy anything here into production code unless you really know what you are doing. - -contract MockPayerWithSignatureOptimized is Ownable { - error InvalidSignature(); - - address public signer; - - address public immutable APPROVED_ORCHESTRATOR; - - event Compensated( - address indexed paymentToken, - address indexed paymentRecipient, - uint256 paymentAmount, - address indexed eoa, - bytes32 keyHash - ); - - constructor(address orchestrator) { - APPROVED_ORCHESTRATOR = orchestrator; - _initializeOwner(msg.sender); - } - - function setSigner(address newSinger) public onlyOwner { - signer = newSinger; - } - - /// @dev `address(0)` denote native token (i.e. Ether). - function withdrawTokens(address token, address recipient, uint256 amount) - public - virtual - onlyOwner - { - TokenTransferLib.safeTransfer(token, recipient, amount); - } - - /// @dev Pays `paymentAmount` of `paymentToken` to the `paymentRecipient`. - /// The EOA and token details are extracted from the `encodedIntent`. - /// Reverts if the specified Orchestrator (`msg.sender`) is not approved. - /// NOTE: This mock no longer verifies signatures within the pay function itself, - /// aligning with the Account/Orchestrator pattern where verification happens before payment. - /// @param paymentAmount The amount to pay. - /// @param keyHash The key hash associated with the operation (not used in this mock's logic but kept for signature compatibility). - /// @param encodedIntent ABI encoded Intent struct. - function pay( - uint256 paymentAmount, - bytes32 keyHash, - bytes32 digest, - bytes calldata encodedIntent - ) public virtual { - if (msg.sender != APPROVED_ORCHESTRATOR) revert Unauthorized(); - - ICommon.Intent calldata u; - assembly { - let t := calldataload(encodedIntent.offset) - u := add(t, encodedIntent.offset) - } - - bytes32 signatureDigest = computeSignatureDigest(digest); - - if (ECDSA.recover(signatureDigest, u.paymentSignature) != signer) { - revert InvalidSignature(); - } - - TokenTransferLib.safeTransfer(u.paymentToken, u.paymentRecipient, paymentAmount); - - emit Compensated(u.paymentToken, u.paymentRecipient, paymentAmount, u.eoa, keyHash); - } - - function computeSignatureDigest(bytes32 intentDigest) public view returns (bytes32) { - // We shall just use this simplified hash instead of EIP712. - return keccak256(abi.encode(intentDigest, block.chainid, address(this))); - } - - receive() external payable {} -} diff --git a/test/utils/mocks/MockPayerWithState.sol b/test/utils/mocks/MockPayerWithState.sol index 365db440..e5e4c35d 100644 --- a/test/utils/mocks/MockPayerWithState.sol +++ b/test/utils/mocks/MockPayerWithState.sol @@ -54,35 +54,42 @@ contract MockPayerWithState is Ownable { } /// @dev Pays `paymentAmount` of `paymentToken` to the `paymentRecipient`. - /// The EOA and token details are extracted from the `encodedIntent`. - /// Reverts if the specified Orchestrator (`msg.sender`) is not approved, - /// if the EOA lacks sufficient funds, or if the nonce has already been used. - /// @param paymentAmount The amount to pay. - /// @param keyHash The key hash associated with the operation (not used in this mock's logic but kept for signature compatibility). - /// @param digest The digest of the intent (used for nonce tracking). - /// @param encodedIntent ABI encoded Intent struct. + /// @param paymentAmount The amount to pay + /// @param keyHash The hash of the key used to authorize the operation + /// @param intentDigest The digest of the user operation + /// @param eoa The EOA address + /// @param payer The payer address + /// @param paymentToken The token to pay with + /// @param paymentRecipient The recipient of the payment + /// @param paymentSignature The payment signature function pay( uint256 paymentAmount, bytes32 keyHash, - bytes32 digest, - bytes calldata encodedIntent + bytes32 intentDigest, + address eoa, + address payer, + address paymentToken, + address paymentRecipient, + bytes calldata paymentSignature ) public virtual { if (!isApprovedOrchestrator[msg.sender]) revert Unauthorized(); // Check and set nonce to prevent replay attacks - if (paymasterNonces[digest]) { + if (paymasterNonces[intentDigest]) { revert PaymasterNonceError(); } - paymasterNonces[digest] = true; - - ICommon.Intent memory u = abi.decode(encodedIntent, (ICommon.Intent)); + paymasterNonces[intentDigest] = true; // We shall rely on arithmetic underflow error to revert if there's insufficient funds. - funds[u.paymentToken][u.eoa] -= paymentAmount; - TokenTransferLib.safeTransfer(u.paymentToken, u.paymentRecipient, paymentAmount); + funds[paymentToken][eoa] -= paymentAmount; + TokenTransferLib.safeTransfer(paymentToken, paymentRecipient, paymentAmount); // Emit the event for debugging. - emit Compensated(u.paymentToken, u.paymentRecipient, paymentAmount, u.eoa, keyHash); + emit Compensated(paymentToken, paymentRecipient, paymentAmount, eoa, keyHash); + + // Unused parameters + payer; + paymentSignature; } receive() external payable {} diff --git a/utils/JsonBindings.sol b/utils/JsonBindings.sol new file mode 100644 index 00000000..1a76c5b2 --- /dev/null +++ b/utils/JsonBindings.sol @@ -0,0 +1,767 @@ +// Automatically generated by forge bind-json. + +pragma solidity >=0.6.2 <0.9.0; +pragma experimental ABIEncoderV2; + +import {GuardedExecutor} from "src/GuardedExecutor.sol"; +import {IthacaAccount} from "src/IthacaAccount.sol"; +import {MultiSigSigner} from "src/MultiSigSigner.sol"; +import {ICommon} from "src/interfaces/ICommon.sol"; +import {IEscrow} from "src/interfaces/IEscrow.sol"; +import {LibTStack} from "src/libraries/LibTStack.sol"; +import {AccountTest} from "test/Account.t.sol"; +import {BaseTest} from "test/Base.t.sol"; +import {EnforcedOptionParam} from "test/LayerZeroSettler.t.sol"; +import {MultiSigSignerTest} from "test/MultiSigSigner.t.sol"; +import {OrchestratorTest} from "test/Orchestrator.t.sol"; +import {SimulateExecuteTest} from "test/SimulateExecute.t.sol"; +import {SignatureWrapper} from "test/utils/interfaces/ICoinbaseSmartWallet.sol"; +import {IERC4337EntryPoint, IStakeManager, PackedUserOperation, UserOperation} from "test/utils/interfaces/IERC4337EntryPoint.sol"; + +interface Vm { + function parseJsonTypeArray(string calldata json, string calldata key, string calldata typeDescription) external pure returns (bytes memory); + function parseJsonType(string calldata json, string calldata typeDescription) external pure returns (bytes memory); + function parseJsonType(string calldata json, string calldata key, string calldata typeDescription) external pure returns (bytes memory); + function serializeJsonType(string calldata typeDescription, bytes memory value) external pure returns (string memory json); + function serializeJsonType(string calldata objectKey, string calldata valueKey, string calldata typeDescription, bytes memory value) external returns (string memory json); +} + +library JsonBindings { + Vm constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + + string constant schema_Intent = "Intent(address eoa,bytes executionData,uint256 nonce,address payer,address paymentToken,uint256 paymentMaxAmount,uint256 combinedGas,bytes[] encodedPreCalls,bytes[] encodedFundTransfers,address settler,uint256 expiry,bool isMultichain,address funder,bytes funderSignature,bytes settlerContext,uint256 paymentAmount,address paymentRecipient,bytes signature,bytes paymentSignature,address supportedAccountImplementation)"; + string constant schema_SignedCall = "SignedCall(address eoa,bytes executionData,uint256 nonce,bytes signature)"; + string constant schema_Transfer = "Transfer(address token,uint256 amount)"; + string constant schema_SpendInfo = "SpendInfo(address token,uint8 period,uint256 limit,uint256 spent,uint256 lastUpdated,uint256 currentSpent,uint256 current)"; + string constant schema_CallCheckerInfo = "CallCheckerInfo(address target,address checker)"; + string constant schema_TokenPeriodSpend = "TokenPeriodSpend(uint256 limit,uint256 spent,uint256 lastUpdated)"; + string constant schema__ExecuteTemps = "_ExecuteTemps(DynamicArray approvedERC20s,DynamicArray approvalSpenders,DynamicArray erc20s,DynamicArray transferAmounts,DynamicArray permit2ERC20s,DynamicArray permit2Spenders)DynamicArray(uint256[] data)"; + string constant schema_TStack = "TStack(uint256 slot)"; + string constant schema_Key = "Key(uint40 expiry,uint8 keyType,bool isSuperAdmin,bytes publicKey)"; + string constant schema_KeyExtraStorage = "KeyExtraStorage(AddressSet checkers)AddressSet(uint256 _spacer)"; + string constant schema_Escrow = "Escrow(bytes12 salt,address depositor,address recipient,address token,uint256 escrowAmount,uint256 refundAmount,uint256 refundTimestamp,address settler,address sender,bytes32 settlementId,uint256 senderChainId)"; + string constant schema_Config = "Config(uint256 threshold,bytes32[] ownerKeyHashes)"; + string constant schema_TargetFunctionPayload = "TargetFunctionPayload(address by,uint256 value,bytes data)"; + string constant schema_PassKey = "PassKey(Key k,uint256 privateKey,bytes32 keyHash)Key(uint40 expiry,uint8 keyType,bool isSuperAdmin,bytes publicKey)"; + string constant schema_MultiSigKey = "MultiSigKey(Key k,uint256 threshold,PassKey[] owners)Key(uint40 expiry,uint8 keyType,bool isSuperAdmin,bytes publicKey)PassKey(Key k,uint256 privateKey,bytes32 keyHash)"; + string constant schema_DelegatedEOA = "DelegatedEOA(address eoa,uint256 privateKey,address d)"; + string constant schema__EstimateGasParams = "_EstimateGasParams(Intent u,uint8 paymentPerGasPrecision,uint256 paymentPerGas,uint256 combinedGasIncrement,uint256 combinedGasVerificationOffset)Intent(address eoa,bytes executionData,uint256 nonce,address payer,address paymentToken,uint256 paymentMaxAmount,uint256 combinedGas,bytes[] encodedPreCalls,bytes[] encodedFundTransfers,address settler,uint256 expiry,bool isMultichain,address funder,bytes funderSignature,bytes settlerContext,uint256 paymentAmount,address paymentRecipient,bytes signature,bytes paymentSignature,address supportedAccountImplementation)"; + string constant schema__TestExecuteWithSignatureTemps = "_TestExecuteWithSignatureTemps(TargetFunctionPayload[] targetFunctionPayloads,Call[] calls,uint256 n,uint256 nonce,bytes opData,bytes executionData)Call(address to,uint256 value,bytes data)TargetFunctionPayload(address by,uint256 value,bytes data)"; + string constant schema__TestUpgradeAccountWithPassKeyTemps = "_TestUpgradeAccountWithPassKeyTemps(uint256 randomVersion,address implementation,Call[] calls,uint256 nonce,bytes opData,bytes executionData)Call(address to,uint256 value,bytes data)"; + string constant schema_DepositInfo = "DepositInfo(uint256 deposit,bool staked,uint112 stake,uint32 unstakeDelaySec,uint48 withdrawTime)"; + string constant schema_StakeInfo = "StakeInfo(uint256 stake,uint256 unstakeDelaySec)"; + string constant schema_PackedUserOperation = "PackedUserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,bytes32 accountGasLimits,uint256 preVerificationGas,bytes32 gasFees,bytes paymasterAndData,bytes signature)"; + string constant schema_UserOperation = "UserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,uint256 callGasLimit,uint256 verificationGasLimit,uint256 preVerificationGas,uint256 maxFeePerGas,uint256 maxPriorityFeePerGas,bytes paymasterAndData,bytes signature)"; + string constant schema_UserOpsPerAggregator = "UserOpsPerAggregator(PackedUserOperation[] userOps,address aggregator,bytes signature)PackedUserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,bytes32 accountGasLimits,uint256 preVerificationGas,bytes32 gasFees,bytes paymasterAndData,bytes signature)"; + string constant schema_ReturnInfo = "ReturnInfo(uint256 preOpGas,uint256 prefund,uint256 accountValidationData,uint256 paymasterValidationData,bytes paymasterContext)"; + string constant schema_SignatureWrapper = "SignatureWrapper(uint8 ownerIndex,bytes signatureData)"; + string constant schema_EnforcedOptionParam = "EnforcedOptionParam(uint32 eid,uint16 msgType,bytes options)"; + string constant schema_MultiSigTestTemps = "MultiSigTestTemps(PassKey[] owners,bytes32[] ownerKeyHashes,uint256 threshold,MultiSigKey multiSigKey,bytes32 digest)Key(uint40 expiry,uint8 keyType,bool isSuperAdmin,bytes publicKey)MultiSigKey(Key k,uint256 threshold,PassKey[] owners)PassKey(Key k,uint256 privateKey,bytes32 keyHash)"; + string constant schema__TestFullFlowTemps = "_TestFullFlowTemps(Intent[] intents,TargetFunctionPayload[] targetFunctionPayloads,DelegatedEOA[] delegatedEOAs,bytes[] encodedIntents)DelegatedEOA(address eoa,uint256 privateKey,address d)Intent(address eoa,bytes executionData,uint256 nonce,address payer,address paymentToken,uint256 paymentMaxAmount,uint256 combinedGas,bytes[] encodedPreCalls,bytes[] encodedFundTransfers,address settler,uint256 expiry,bool isMultichain,address funder,bytes funderSignature,bytes settlerContext,uint256 paymentAmount,address paymentRecipient,bytes signature,bytes paymentSignature,address supportedAccountImplementation)TargetFunctionPayload(address by,uint256 value,bytes data)"; + string constant schema__TestAuthorizeWithPreCallsAndTransferTemps = "_TestAuthorizeWithPreCallsAndTransferTemps(uint256 gExecute,uint256 gCombined,uint256 gUsed,bool success,bytes result,bool testInvalidPreCallEOA,bool testPreCallVerificationError,bool testPreCallError,bool testInit,bool testEOACoalesce,bool testSkipNonce,uint192 superAdminNonceSeqKey,uint192 sessionNonceSeqKey,uint256 retrievedSuperAdminNonce,uint256 retrievedSessionNonce,PassKey kInit,DelegatedEOA d,address eoa)DelegatedEOA(address eoa,uint256 privateKey,address d)Key(uint40 expiry,uint8 keyType,bool isSuperAdmin,bytes publicKey)PassKey(Key k,uint256 privateKey,bytes32 keyHash)"; + string constant schema__TestPayViaAnotherPayerTemps = "_TestPayViaAnotherPayerTemps(address withState,address withSignature,DelegatedEOA withSignatureEOA,address token,uint256 funds,bool isWithState,bool corruptSignature,bool unapprovedOrchestrator,uint256 balanceBefore,DelegatedEOA d)DelegatedEOA(address eoa,uint256 privateKey,address d)"; + string constant schema__TestAccountImplementationVerificationTemps = "_TestAccountImplementationVerificationTemps(bool testImplementationCheck,bool requireWrongImplementation,DelegatedEOA d)DelegatedEOA(address eoa,uint256 privateKey,address d)"; + string constant schema__TestMultiSigTemps = "_TestMultiSigTemps(DelegatedEOA d,address multiSigSigner,uint256 numKeys,MultiSigKey multiSigKey)DelegatedEOA(address eoa,uint256 privateKey,address d)Key(uint40 expiry,uint8 keyType,bool isSuperAdmin,bytes publicKey)MultiSigKey(Key k,uint256 threshold,PassKey[] owners)PassKey(Key k,uint256 privateKey,bytes32 keyHash)"; + string constant schema__TestMultiChainIntentTemps = "_TestMultiChainIntentTemps(address funder,address settler,uint256 funderPrivateKey,address usdcMainnet,address usdcArb,address usdcBase,DelegatedEOA d,PassKey k,Intent baseIntent,Intent arbIntent,Intent outputIntent,bytes32[] leafs,bytes32 root,bytes rootSig,address gasWallet,address relay,address friend,address settlementOracle,address escrowBase,address escrowArb,bytes32 settlementId,bytes32 escrowIdBase,bytes32 escrowIdArb,bytes[] encodedIntents,bytes4[] errs,uint256 snapshot)DelegatedEOA(address eoa,uint256 privateKey,address d)Intent(address eoa,bytes executionData,uint256 nonce,address payer,address paymentToken,uint256 paymentMaxAmount,uint256 combinedGas,bytes[] encodedPreCalls,bytes[] encodedFundTransfers,address settler,uint256 expiry,bool isMultichain,address funder,bytes funderSignature,bytes settlerContext,uint256 paymentAmount,address paymentRecipient,bytes signature,bytes paymentSignature,address supportedAccountImplementation)Key(uint40 expiry,uint8 keyType,bool isSuperAdmin,bytes publicKey)PassKey(Key k,uint256 privateKey,bytes32 keyHash)"; + string constant schema__SimulateExecuteTemps = "_SimulateExecuteTemps(uint256 gasToBurn,uint256 randomness,uint256 gExecute,uint256 gCombined,uint256 gUsed,bytes executionData,bool success,bytes result)"; + + function serialize(ICommon.Intent memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema_Intent, abi.encode(value)); + } + + function serialize(ICommon.Intent memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema_Intent, abi.encode(value)); + } + + function deserializeIntent(string memory json) public pure returns (ICommon.Intent memory) { + return abi.decode(vm.parseJsonType(json, schema_Intent), (ICommon.Intent)); + } + + function deserializeIntent(string memory json, string memory path) public pure returns (ICommon.Intent memory) { + return abi.decode(vm.parseJsonType(json, path, schema_Intent), (ICommon.Intent)); + } + + function deserializeIntentArray(string memory json, string memory path) public pure returns (ICommon.Intent[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema_Intent), (ICommon.Intent[])); + } + + function serialize(ICommon.SignedCall memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema_SignedCall, abi.encode(value)); + } + + function serialize(ICommon.SignedCall memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema_SignedCall, abi.encode(value)); + } + + function deserializeSignedCall(string memory json) public pure returns (ICommon.SignedCall memory) { + return abi.decode(vm.parseJsonType(json, schema_SignedCall), (ICommon.SignedCall)); + } + + function deserializeSignedCall(string memory json, string memory path) public pure returns (ICommon.SignedCall memory) { + return abi.decode(vm.parseJsonType(json, path, schema_SignedCall), (ICommon.SignedCall)); + } + + function deserializeSignedCallArray(string memory json, string memory path) public pure returns (ICommon.SignedCall[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema_SignedCall), (ICommon.SignedCall[])); + } + + function serialize(ICommon.Transfer memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema_Transfer, abi.encode(value)); + } + + function serialize(ICommon.Transfer memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema_Transfer, abi.encode(value)); + } + + function deserializeTransfer(string memory json) public pure returns (ICommon.Transfer memory) { + return abi.decode(vm.parseJsonType(json, schema_Transfer), (ICommon.Transfer)); + } + + function deserializeTransfer(string memory json, string memory path) public pure returns (ICommon.Transfer memory) { + return abi.decode(vm.parseJsonType(json, path, schema_Transfer), (ICommon.Transfer)); + } + + function deserializeTransferArray(string memory json, string memory path) public pure returns (ICommon.Transfer[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema_Transfer), (ICommon.Transfer[])); + } + + function serialize(GuardedExecutor.SpendInfo memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema_SpendInfo, abi.encode(value)); + } + + function serialize(GuardedExecutor.SpendInfo memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema_SpendInfo, abi.encode(value)); + } + + function deserializeSpendInfo(string memory json) public pure returns (GuardedExecutor.SpendInfo memory) { + return abi.decode(vm.parseJsonType(json, schema_SpendInfo), (GuardedExecutor.SpendInfo)); + } + + function deserializeSpendInfo(string memory json, string memory path) public pure returns (GuardedExecutor.SpendInfo memory) { + return abi.decode(vm.parseJsonType(json, path, schema_SpendInfo), (GuardedExecutor.SpendInfo)); + } + + function deserializeSpendInfoArray(string memory json, string memory path) public pure returns (GuardedExecutor.SpendInfo[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema_SpendInfo), (GuardedExecutor.SpendInfo[])); + } + + function serialize(GuardedExecutor.CallCheckerInfo memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema_CallCheckerInfo, abi.encode(value)); + } + + function serialize(GuardedExecutor.CallCheckerInfo memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema_CallCheckerInfo, abi.encode(value)); + } + + function deserializeCallCheckerInfo(string memory json) public pure returns (GuardedExecutor.CallCheckerInfo memory) { + return abi.decode(vm.parseJsonType(json, schema_CallCheckerInfo), (GuardedExecutor.CallCheckerInfo)); + } + + function deserializeCallCheckerInfo(string memory json, string memory path) public pure returns (GuardedExecutor.CallCheckerInfo memory) { + return abi.decode(vm.parseJsonType(json, path, schema_CallCheckerInfo), (GuardedExecutor.CallCheckerInfo)); + } + + function deserializeCallCheckerInfoArray(string memory json, string memory path) public pure returns (GuardedExecutor.CallCheckerInfo[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema_CallCheckerInfo), (GuardedExecutor.CallCheckerInfo[])); + } + + function serialize(GuardedExecutor.TokenPeriodSpend memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema_TokenPeriodSpend, abi.encode(value)); + } + + function serialize(GuardedExecutor.TokenPeriodSpend memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema_TokenPeriodSpend, abi.encode(value)); + } + + function deserializeTokenPeriodSpend(string memory json) public pure returns (GuardedExecutor.TokenPeriodSpend memory) { + return abi.decode(vm.parseJsonType(json, schema_TokenPeriodSpend), (GuardedExecutor.TokenPeriodSpend)); + } + + function deserializeTokenPeriodSpend(string memory json, string memory path) public pure returns (GuardedExecutor.TokenPeriodSpend memory) { + return abi.decode(vm.parseJsonType(json, path, schema_TokenPeriodSpend), (GuardedExecutor.TokenPeriodSpend)); + } + + function deserializeTokenPeriodSpendArray(string memory json, string memory path) public pure returns (GuardedExecutor.TokenPeriodSpend[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema_TokenPeriodSpend), (GuardedExecutor.TokenPeriodSpend[])); + } + + function serialize(GuardedExecutor._ExecuteTemps memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema__ExecuteTemps, abi.encode(value)); + } + + function serialize(GuardedExecutor._ExecuteTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema__ExecuteTemps, abi.encode(value)); + } + + function deserialize_ExecuteTemps(string memory json) public pure returns (GuardedExecutor._ExecuteTemps memory) { + return abi.decode(vm.parseJsonType(json, schema__ExecuteTemps), (GuardedExecutor._ExecuteTemps)); + } + + function deserialize_ExecuteTemps(string memory json, string memory path) public pure returns (GuardedExecutor._ExecuteTemps memory) { + return abi.decode(vm.parseJsonType(json, path, schema__ExecuteTemps), (GuardedExecutor._ExecuteTemps)); + } + + function deserialize_ExecuteTempsArray(string memory json, string memory path) public pure returns (GuardedExecutor._ExecuteTemps[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema__ExecuteTemps), (GuardedExecutor._ExecuteTemps[])); + } + + function serialize(LibTStack.TStack memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema_TStack, abi.encode(value)); + } + + function serialize(LibTStack.TStack memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema_TStack, abi.encode(value)); + } + + function deserializeTStack(string memory json) public pure returns (LibTStack.TStack memory) { + return abi.decode(vm.parseJsonType(json, schema_TStack), (LibTStack.TStack)); + } + + function deserializeTStack(string memory json, string memory path) public pure returns (LibTStack.TStack memory) { + return abi.decode(vm.parseJsonType(json, path, schema_TStack), (LibTStack.TStack)); + } + + function deserializeTStackArray(string memory json, string memory path) public pure returns (LibTStack.TStack[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema_TStack), (LibTStack.TStack[])); + } + + function serialize(IthacaAccount.Key memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema_Key, abi.encode(value)); + } + + function serialize(IthacaAccount.Key memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema_Key, abi.encode(value)); + } + + function deserializeKey(string memory json) public pure returns (IthacaAccount.Key memory) { + return abi.decode(vm.parseJsonType(json, schema_Key), (IthacaAccount.Key)); + } + + function deserializeKey(string memory json, string memory path) public pure returns (IthacaAccount.Key memory) { + return abi.decode(vm.parseJsonType(json, path, schema_Key), (IthacaAccount.Key)); + } + + function deserializeKeyArray(string memory json, string memory path) public pure returns (IthacaAccount.Key[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema_Key), (IthacaAccount.Key[])); + } + + function serialize(IthacaAccount.KeyExtraStorage memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema_KeyExtraStorage, abi.encode(value)); + } + + function serialize(IthacaAccount.KeyExtraStorage memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema_KeyExtraStorage, abi.encode(value)); + } + + function deserializeKeyExtraStorage(string memory json) public pure returns (IthacaAccount.KeyExtraStorage memory) { + return abi.decode(vm.parseJsonType(json, schema_KeyExtraStorage), (IthacaAccount.KeyExtraStorage)); + } + + function deserializeKeyExtraStorage(string memory json, string memory path) public pure returns (IthacaAccount.KeyExtraStorage memory) { + return abi.decode(vm.parseJsonType(json, path, schema_KeyExtraStorage), (IthacaAccount.KeyExtraStorage)); + } + + function deserializeKeyExtraStorageArray(string memory json, string memory path) public pure returns (IthacaAccount.KeyExtraStorage[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema_KeyExtraStorage), (IthacaAccount.KeyExtraStorage[])); + } + + function serialize(IEscrow.Escrow memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema_Escrow, abi.encode(value)); + } + + function serialize(IEscrow.Escrow memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema_Escrow, abi.encode(value)); + } + + function deserializeEscrow(string memory json) public pure returns (IEscrow.Escrow memory) { + return abi.decode(vm.parseJsonType(json, schema_Escrow), (IEscrow.Escrow)); + } + + function deserializeEscrow(string memory json, string memory path) public pure returns (IEscrow.Escrow memory) { + return abi.decode(vm.parseJsonType(json, path, schema_Escrow), (IEscrow.Escrow)); + } + + function deserializeEscrowArray(string memory json, string memory path) public pure returns (IEscrow.Escrow[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema_Escrow), (IEscrow.Escrow[])); + } + + function serialize(MultiSigSigner.Config memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema_Config, abi.encode(value)); + } + + function serialize(MultiSigSigner.Config memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema_Config, abi.encode(value)); + } + + function deserializeConfig(string memory json) public pure returns (MultiSigSigner.Config memory) { + return abi.decode(vm.parseJsonType(json, schema_Config), (MultiSigSigner.Config)); + } + + function deserializeConfig(string memory json, string memory path) public pure returns (MultiSigSigner.Config memory) { + return abi.decode(vm.parseJsonType(json, path, schema_Config), (MultiSigSigner.Config)); + } + + function deserializeConfigArray(string memory json, string memory path) public pure returns (MultiSigSigner.Config[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema_Config), (MultiSigSigner.Config[])); + } + + function serialize(BaseTest.TargetFunctionPayload memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema_TargetFunctionPayload, abi.encode(value)); + } + + function serialize(BaseTest.TargetFunctionPayload memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema_TargetFunctionPayload, abi.encode(value)); + } + + function deserializeTargetFunctionPayload(string memory json) public pure returns (BaseTest.TargetFunctionPayload memory) { + return abi.decode(vm.parseJsonType(json, schema_TargetFunctionPayload), (BaseTest.TargetFunctionPayload)); + } + + function deserializeTargetFunctionPayload(string memory json, string memory path) public pure returns (BaseTest.TargetFunctionPayload memory) { + return abi.decode(vm.parseJsonType(json, path, schema_TargetFunctionPayload), (BaseTest.TargetFunctionPayload)); + } + + function deserializeTargetFunctionPayloadArray(string memory json, string memory path) public pure returns (BaseTest.TargetFunctionPayload[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema_TargetFunctionPayload), (BaseTest.TargetFunctionPayload[])); + } + + function serialize(BaseTest.PassKey memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema_PassKey, abi.encode(value)); + } + + function serialize(BaseTest.PassKey memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema_PassKey, abi.encode(value)); + } + + function deserializePassKey(string memory json) public pure returns (BaseTest.PassKey memory) { + return abi.decode(vm.parseJsonType(json, schema_PassKey), (BaseTest.PassKey)); + } + + function deserializePassKey(string memory json, string memory path) public pure returns (BaseTest.PassKey memory) { + return abi.decode(vm.parseJsonType(json, path, schema_PassKey), (BaseTest.PassKey)); + } + + function deserializePassKeyArray(string memory json, string memory path) public pure returns (BaseTest.PassKey[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema_PassKey), (BaseTest.PassKey[])); + } + + function serialize(BaseTest.MultiSigKey memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema_MultiSigKey, abi.encode(value)); + } + + function serialize(BaseTest.MultiSigKey memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema_MultiSigKey, abi.encode(value)); + } + + function deserializeMultiSigKey(string memory json) public pure returns (BaseTest.MultiSigKey memory) { + return abi.decode(vm.parseJsonType(json, schema_MultiSigKey), (BaseTest.MultiSigKey)); + } + + function deserializeMultiSigKey(string memory json, string memory path) public pure returns (BaseTest.MultiSigKey memory) { + return abi.decode(vm.parseJsonType(json, path, schema_MultiSigKey), (BaseTest.MultiSigKey)); + } + + function deserializeMultiSigKeyArray(string memory json, string memory path) public pure returns (BaseTest.MultiSigKey[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema_MultiSigKey), (BaseTest.MultiSigKey[])); + } + + function serialize(BaseTest.DelegatedEOA memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema_DelegatedEOA, abi.encode(value)); + } + + function serialize(BaseTest.DelegatedEOA memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema_DelegatedEOA, abi.encode(value)); + } + + function deserializeDelegatedEOA(string memory json) public pure returns (BaseTest.DelegatedEOA memory) { + return abi.decode(vm.parseJsonType(json, schema_DelegatedEOA), (BaseTest.DelegatedEOA)); + } + + function deserializeDelegatedEOA(string memory json, string memory path) public pure returns (BaseTest.DelegatedEOA memory) { + return abi.decode(vm.parseJsonType(json, path, schema_DelegatedEOA), (BaseTest.DelegatedEOA)); + } + + function deserializeDelegatedEOAArray(string memory json, string memory path) public pure returns (BaseTest.DelegatedEOA[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema_DelegatedEOA), (BaseTest.DelegatedEOA[])); + } + + function serialize(BaseTest._EstimateGasParams memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema__EstimateGasParams, abi.encode(value)); + } + + function serialize(BaseTest._EstimateGasParams memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema__EstimateGasParams, abi.encode(value)); + } + + function deserialize_EstimateGasParams(string memory json) public pure returns (BaseTest._EstimateGasParams memory) { + return abi.decode(vm.parseJsonType(json, schema__EstimateGasParams), (BaseTest._EstimateGasParams)); + } + + function deserialize_EstimateGasParams(string memory json, string memory path) public pure returns (BaseTest._EstimateGasParams memory) { + return abi.decode(vm.parseJsonType(json, path, schema__EstimateGasParams), (BaseTest._EstimateGasParams)); + } + + function deserialize_EstimateGasParamsArray(string memory json, string memory path) public pure returns (BaseTest._EstimateGasParams[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema__EstimateGasParams), (BaseTest._EstimateGasParams[])); + } + + function serialize(AccountTest._TestExecuteWithSignatureTemps memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema__TestExecuteWithSignatureTemps, abi.encode(value)); + } + + function serialize(AccountTest._TestExecuteWithSignatureTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema__TestExecuteWithSignatureTemps, abi.encode(value)); + } + + function deserialize_TestExecuteWithSignatureTemps(string memory json) public pure returns (AccountTest._TestExecuteWithSignatureTemps memory) { + return abi.decode(vm.parseJsonType(json, schema__TestExecuteWithSignatureTemps), (AccountTest._TestExecuteWithSignatureTemps)); + } + + function deserialize_TestExecuteWithSignatureTemps(string memory json, string memory path) public pure returns (AccountTest._TestExecuteWithSignatureTemps memory) { + return abi.decode(vm.parseJsonType(json, path, schema__TestExecuteWithSignatureTemps), (AccountTest._TestExecuteWithSignatureTemps)); + } + + function deserialize_TestExecuteWithSignatureTempsArray(string memory json, string memory path) public pure returns (AccountTest._TestExecuteWithSignatureTemps[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema__TestExecuteWithSignatureTemps), (AccountTest._TestExecuteWithSignatureTemps[])); + } + + function serialize(AccountTest._TestUpgradeAccountWithPassKeyTemps memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema__TestUpgradeAccountWithPassKeyTemps, abi.encode(value)); + } + + function serialize(AccountTest._TestUpgradeAccountWithPassKeyTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema__TestUpgradeAccountWithPassKeyTemps, abi.encode(value)); + } + + function deserialize_TestUpgradeAccountWithPassKeyTemps(string memory json) public pure returns (AccountTest._TestUpgradeAccountWithPassKeyTemps memory) { + return abi.decode(vm.parseJsonType(json, schema__TestUpgradeAccountWithPassKeyTemps), (AccountTest._TestUpgradeAccountWithPassKeyTemps)); + } + + function deserialize_TestUpgradeAccountWithPassKeyTemps(string memory json, string memory path) public pure returns (AccountTest._TestUpgradeAccountWithPassKeyTemps memory) { + return abi.decode(vm.parseJsonType(json, path, schema__TestUpgradeAccountWithPassKeyTemps), (AccountTest._TestUpgradeAccountWithPassKeyTemps)); + } + + function deserialize_TestUpgradeAccountWithPassKeyTempsArray(string memory json, string memory path) public pure returns (AccountTest._TestUpgradeAccountWithPassKeyTemps[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema__TestUpgradeAccountWithPassKeyTemps), (AccountTest._TestUpgradeAccountWithPassKeyTemps[])); + } + + function serialize(IStakeManager.DepositInfo memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema_DepositInfo, abi.encode(value)); + } + + function serialize(IStakeManager.DepositInfo memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema_DepositInfo, abi.encode(value)); + } + + function deserializeDepositInfo(string memory json) public pure returns (IStakeManager.DepositInfo memory) { + return abi.decode(vm.parseJsonType(json, schema_DepositInfo), (IStakeManager.DepositInfo)); + } + + function deserializeDepositInfo(string memory json, string memory path) public pure returns (IStakeManager.DepositInfo memory) { + return abi.decode(vm.parseJsonType(json, path, schema_DepositInfo), (IStakeManager.DepositInfo)); + } + + function deserializeDepositInfoArray(string memory json, string memory path) public pure returns (IStakeManager.DepositInfo[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema_DepositInfo), (IStakeManager.DepositInfo[])); + } + + function serialize(IStakeManager.StakeInfo memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema_StakeInfo, abi.encode(value)); + } + + function serialize(IStakeManager.StakeInfo memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema_StakeInfo, abi.encode(value)); + } + + function deserializeStakeInfo(string memory json) public pure returns (IStakeManager.StakeInfo memory) { + return abi.decode(vm.parseJsonType(json, schema_StakeInfo), (IStakeManager.StakeInfo)); + } + + function deserializeStakeInfo(string memory json, string memory path) public pure returns (IStakeManager.StakeInfo memory) { + return abi.decode(vm.parseJsonType(json, path, schema_StakeInfo), (IStakeManager.StakeInfo)); + } + + function deserializeStakeInfoArray(string memory json, string memory path) public pure returns (IStakeManager.StakeInfo[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema_StakeInfo), (IStakeManager.StakeInfo[])); + } + + function serialize(PackedUserOperation memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema_PackedUserOperation, abi.encode(value)); + } + + function serialize(PackedUserOperation memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema_PackedUserOperation, abi.encode(value)); + } + + function deserializePackedUserOperation(string memory json) public pure returns (PackedUserOperation memory) { + return abi.decode(vm.parseJsonType(json, schema_PackedUserOperation), (PackedUserOperation)); + } + + function deserializePackedUserOperation(string memory json, string memory path) public pure returns (PackedUserOperation memory) { + return abi.decode(vm.parseJsonType(json, path, schema_PackedUserOperation), (PackedUserOperation)); + } + + function deserializePackedUserOperationArray(string memory json, string memory path) public pure returns (PackedUserOperation[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema_PackedUserOperation), (PackedUserOperation[])); + } + + function serialize(UserOperation memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema_UserOperation, abi.encode(value)); + } + + function serialize(UserOperation memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema_UserOperation, abi.encode(value)); + } + + function deserializeUserOperation(string memory json) public pure returns (UserOperation memory) { + return abi.decode(vm.parseJsonType(json, schema_UserOperation), (UserOperation)); + } + + function deserializeUserOperation(string memory json, string memory path) public pure returns (UserOperation memory) { + return abi.decode(vm.parseJsonType(json, path, schema_UserOperation), (UserOperation)); + } + + function deserializeUserOperationArray(string memory json, string memory path) public pure returns (UserOperation[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema_UserOperation), (UserOperation[])); + } + + function serialize(IERC4337EntryPoint.UserOpsPerAggregator memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema_UserOpsPerAggregator, abi.encode(value)); + } + + function serialize(IERC4337EntryPoint.UserOpsPerAggregator memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema_UserOpsPerAggregator, abi.encode(value)); + } + + function deserializeUserOpsPerAggregator(string memory json) public pure returns (IERC4337EntryPoint.UserOpsPerAggregator memory) { + return abi.decode(vm.parseJsonType(json, schema_UserOpsPerAggregator), (IERC4337EntryPoint.UserOpsPerAggregator)); + } + + function deserializeUserOpsPerAggregator(string memory json, string memory path) public pure returns (IERC4337EntryPoint.UserOpsPerAggregator memory) { + return abi.decode(vm.parseJsonType(json, path, schema_UserOpsPerAggregator), (IERC4337EntryPoint.UserOpsPerAggregator)); + } + + function deserializeUserOpsPerAggregatorArray(string memory json, string memory path) public pure returns (IERC4337EntryPoint.UserOpsPerAggregator[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema_UserOpsPerAggregator), (IERC4337EntryPoint.UserOpsPerAggregator[])); + } + + function serialize(IERC4337EntryPoint.ReturnInfo memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema_ReturnInfo, abi.encode(value)); + } + + function serialize(IERC4337EntryPoint.ReturnInfo memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema_ReturnInfo, abi.encode(value)); + } + + function deserializeReturnInfo(string memory json) public pure returns (IERC4337EntryPoint.ReturnInfo memory) { + return abi.decode(vm.parseJsonType(json, schema_ReturnInfo), (IERC4337EntryPoint.ReturnInfo)); + } + + function deserializeReturnInfo(string memory json, string memory path) public pure returns (IERC4337EntryPoint.ReturnInfo memory) { + return abi.decode(vm.parseJsonType(json, path, schema_ReturnInfo), (IERC4337EntryPoint.ReturnInfo)); + } + + function deserializeReturnInfoArray(string memory json, string memory path) public pure returns (IERC4337EntryPoint.ReturnInfo[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema_ReturnInfo), (IERC4337EntryPoint.ReturnInfo[])); + } + + function serialize(SignatureWrapper memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema_SignatureWrapper, abi.encode(value)); + } + + function serialize(SignatureWrapper memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema_SignatureWrapper, abi.encode(value)); + } + + function deserializeSignatureWrapper(string memory json) public pure returns (SignatureWrapper memory) { + return abi.decode(vm.parseJsonType(json, schema_SignatureWrapper), (SignatureWrapper)); + } + + function deserializeSignatureWrapper(string memory json, string memory path) public pure returns (SignatureWrapper memory) { + return abi.decode(vm.parseJsonType(json, path, schema_SignatureWrapper), (SignatureWrapper)); + } + + function deserializeSignatureWrapperArray(string memory json, string memory path) public pure returns (SignatureWrapper[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema_SignatureWrapper), (SignatureWrapper[])); + } + + function serialize(EnforcedOptionParam memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema_EnforcedOptionParam, abi.encode(value)); + } + + function serialize(EnforcedOptionParam memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema_EnforcedOptionParam, abi.encode(value)); + } + + function deserializeEnforcedOptionParam(string memory json) public pure returns (EnforcedOptionParam memory) { + return abi.decode(vm.parseJsonType(json, schema_EnforcedOptionParam), (EnforcedOptionParam)); + } + + function deserializeEnforcedOptionParam(string memory json, string memory path) public pure returns (EnforcedOptionParam memory) { + return abi.decode(vm.parseJsonType(json, path, schema_EnforcedOptionParam), (EnforcedOptionParam)); + } + + function deserializeEnforcedOptionParamArray(string memory json, string memory path) public pure returns (EnforcedOptionParam[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema_EnforcedOptionParam), (EnforcedOptionParam[])); + } + + function serialize(MultiSigSignerTest.MultiSigTestTemps memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema_MultiSigTestTemps, abi.encode(value)); + } + + function serialize(MultiSigSignerTest.MultiSigTestTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema_MultiSigTestTemps, abi.encode(value)); + } + + function deserializeMultiSigTestTemps(string memory json) public pure returns (MultiSigSignerTest.MultiSigTestTemps memory) { + return abi.decode(vm.parseJsonType(json, schema_MultiSigTestTemps), (MultiSigSignerTest.MultiSigTestTemps)); + } + + function deserializeMultiSigTestTemps(string memory json, string memory path) public pure returns (MultiSigSignerTest.MultiSigTestTemps memory) { + return abi.decode(vm.parseJsonType(json, path, schema_MultiSigTestTemps), (MultiSigSignerTest.MultiSigTestTemps)); + } + + function deserializeMultiSigTestTempsArray(string memory json, string memory path) public pure returns (MultiSigSignerTest.MultiSigTestTemps[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema_MultiSigTestTemps), (MultiSigSignerTest.MultiSigTestTemps[])); + } + + function serialize(OrchestratorTest._TestFullFlowTemps memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema__TestFullFlowTemps, abi.encode(value)); + } + + function serialize(OrchestratorTest._TestFullFlowTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema__TestFullFlowTemps, abi.encode(value)); + } + + function deserialize_TestFullFlowTemps(string memory json) public pure returns (OrchestratorTest._TestFullFlowTemps memory) { + return abi.decode(vm.parseJsonType(json, schema__TestFullFlowTemps), (OrchestratorTest._TestFullFlowTemps)); + } + + function deserialize_TestFullFlowTemps(string memory json, string memory path) public pure returns (OrchestratorTest._TestFullFlowTemps memory) { + return abi.decode(vm.parseJsonType(json, path, schema__TestFullFlowTemps), (OrchestratorTest._TestFullFlowTemps)); + } + + function deserialize_TestFullFlowTempsArray(string memory json, string memory path) public pure returns (OrchestratorTest._TestFullFlowTemps[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema__TestFullFlowTemps), (OrchestratorTest._TestFullFlowTemps[])); + } + + function serialize(OrchestratorTest._TestAuthorizeWithPreCallsAndTransferTemps memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema__TestAuthorizeWithPreCallsAndTransferTemps, abi.encode(value)); + } + + function serialize(OrchestratorTest._TestAuthorizeWithPreCallsAndTransferTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema__TestAuthorizeWithPreCallsAndTransferTemps, abi.encode(value)); + } + + function deserialize_TestAuthorizeWithPreCallsAndTransferTemps(string memory json) public pure returns (OrchestratorTest._TestAuthorizeWithPreCallsAndTransferTemps memory) { + return abi.decode(vm.parseJsonType(json, schema__TestAuthorizeWithPreCallsAndTransferTemps), (OrchestratorTest._TestAuthorizeWithPreCallsAndTransferTemps)); + } + + function deserialize_TestAuthorizeWithPreCallsAndTransferTemps(string memory json, string memory path) public pure returns (OrchestratorTest._TestAuthorizeWithPreCallsAndTransferTemps memory) { + return abi.decode(vm.parseJsonType(json, path, schema__TestAuthorizeWithPreCallsAndTransferTemps), (OrchestratorTest._TestAuthorizeWithPreCallsAndTransferTemps)); + } + + function deserialize_TestAuthorizeWithPreCallsAndTransferTempsArray(string memory json, string memory path) public pure returns (OrchestratorTest._TestAuthorizeWithPreCallsAndTransferTemps[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema__TestAuthorizeWithPreCallsAndTransferTemps), (OrchestratorTest._TestAuthorizeWithPreCallsAndTransferTemps[])); + } + + function serialize(OrchestratorTest._TestPayViaAnotherPayerTemps memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema__TestPayViaAnotherPayerTemps, abi.encode(value)); + } + + function serialize(OrchestratorTest._TestPayViaAnotherPayerTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema__TestPayViaAnotherPayerTemps, abi.encode(value)); + } + + function deserialize_TestPayViaAnotherPayerTemps(string memory json) public pure returns (OrchestratorTest._TestPayViaAnotherPayerTemps memory) { + return abi.decode(vm.parseJsonType(json, schema__TestPayViaAnotherPayerTemps), (OrchestratorTest._TestPayViaAnotherPayerTemps)); + } + + function deserialize_TestPayViaAnotherPayerTemps(string memory json, string memory path) public pure returns (OrchestratorTest._TestPayViaAnotherPayerTemps memory) { + return abi.decode(vm.parseJsonType(json, path, schema__TestPayViaAnotherPayerTemps), (OrchestratorTest._TestPayViaAnotherPayerTemps)); + } + + function deserialize_TestPayViaAnotherPayerTempsArray(string memory json, string memory path) public pure returns (OrchestratorTest._TestPayViaAnotherPayerTemps[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema__TestPayViaAnotherPayerTemps), (OrchestratorTest._TestPayViaAnotherPayerTemps[])); + } + + function serialize(OrchestratorTest._TestAccountImplementationVerificationTemps memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema__TestAccountImplementationVerificationTemps, abi.encode(value)); + } + + function serialize(OrchestratorTest._TestAccountImplementationVerificationTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema__TestAccountImplementationVerificationTemps, abi.encode(value)); + } + + function deserialize_TestAccountImplementationVerificationTemps(string memory json) public pure returns (OrchestratorTest._TestAccountImplementationVerificationTemps memory) { + return abi.decode(vm.parseJsonType(json, schema__TestAccountImplementationVerificationTemps), (OrchestratorTest._TestAccountImplementationVerificationTemps)); + } + + function deserialize_TestAccountImplementationVerificationTemps(string memory json, string memory path) public pure returns (OrchestratorTest._TestAccountImplementationVerificationTemps memory) { + return abi.decode(vm.parseJsonType(json, path, schema__TestAccountImplementationVerificationTemps), (OrchestratorTest._TestAccountImplementationVerificationTemps)); + } + + function deserialize_TestAccountImplementationVerificationTempsArray(string memory json, string memory path) public pure returns (OrchestratorTest._TestAccountImplementationVerificationTemps[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema__TestAccountImplementationVerificationTemps), (OrchestratorTest._TestAccountImplementationVerificationTemps[])); + } + + function serialize(OrchestratorTest._TestMultiSigTemps memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema__TestMultiSigTemps, abi.encode(value)); + } + + function serialize(OrchestratorTest._TestMultiSigTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema__TestMultiSigTemps, abi.encode(value)); + } + + function deserialize_TestMultiSigTemps(string memory json) public pure returns (OrchestratorTest._TestMultiSigTemps memory) { + return abi.decode(vm.parseJsonType(json, schema__TestMultiSigTemps), (OrchestratorTest._TestMultiSigTemps)); + } + + function deserialize_TestMultiSigTemps(string memory json, string memory path) public pure returns (OrchestratorTest._TestMultiSigTemps memory) { + return abi.decode(vm.parseJsonType(json, path, schema__TestMultiSigTemps), (OrchestratorTest._TestMultiSigTemps)); + } + + function deserialize_TestMultiSigTempsArray(string memory json, string memory path) public pure returns (OrchestratorTest._TestMultiSigTemps[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema__TestMultiSigTemps), (OrchestratorTest._TestMultiSigTemps[])); + } + + function serialize(OrchestratorTest._TestMultiChainIntentTemps memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema__TestMultiChainIntentTemps, abi.encode(value)); + } + + function serialize(OrchestratorTest._TestMultiChainIntentTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema__TestMultiChainIntentTemps, abi.encode(value)); + } + + function deserialize_TestMultiChainIntentTemps(string memory json) public pure returns (OrchestratorTest._TestMultiChainIntentTemps memory) { + return abi.decode(vm.parseJsonType(json, schema__TestMultiChainIntentTemps), (OrchestratorTest._TestMultiChainIntentTemps)); + } + + function deserialize_TestMultiChainIntentTemps(string memory json, string memory path) public pure returns (OrchestratorTest._TestMultiChainIntentTemps memory) { + return abi.decode(vm.parseJsonType(json, path, schema__TestMultiChainIntentTemps), (OrchestratorTest._TestMultiChainIntentTemps)); + } + + function deserialize_TestMultiChainIntentTempsArray(string memory json, string memory path) public pure returns (OrchestratorTest._TestMultiChainIntentTemps[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema__TestMultiChainIntentTemps), (OrchestratorTest._TestMultiChainIntentTemps[])); + } + + function serialize(SimulateExecuteTest._SimulateExecuteTemps memory value) internal pure returns (string memory) { + return vm.serializeJsonType(schema__SimulateExecuteTemps, abi.encode(value)); + } + + function serialize(SimulateExecuteTest._SimulateExecuteTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { + return vm.serializeJsonType(objectKey, valueKey, schema__SimulateExecuteTemps, abi.encode(value)); + } + + function deserialize_SimulateExecuteTemps(string memory json) public pure returns (SimulateExecuteTest._SimulateExecuteTemps memory) { + return abi.decode(vm.parseJsonType(json, schema__SimulateExecuteTemps), (SimulateExecuteTest._SimulateExecuteTemps)); + } + + function deserialize_SimulateExecuteTemps(string memory json, string memory path) public pure returns (SimulateExecuteTest._SimulateExecuteTemps memory) { + return abi.decode(vm.parseJsonType(json, path, schema__SimulateExecuteTemps), (SimulateExecuteTest._SimulateExecuteTemps)); + } + + function deserialize_SimulateExecuteTempsArray(string memory json, string memory path) public pure returns (SimulateExecuteTest._SimulateExecuteTemps[] memory) { + return abi.decode(vm.parseJsonTypeArray(json, path, schema__SimulateExecuteTemps), (SimulateExecuteTest._SimulateExecuteTemps[])); + } +} From 946cc13ab1d62c911dc86d053de1a2a844d002b3 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Mon, 20 Apr 2026 01:56:32 +0000 Subject: [PATCH 52/55] Revert "Legion (#72)" (#90) This reverts commit cb23ac3bc44d071b243dbc865d181d65f255c6d6. --- .changeset/README.md | 8 - .changeset/config.json | 11 - .env.example | 6 +- .github/workflows/auto-assign.yaml | 12 + .github/workflows/version-check.yaml | 31 +- .gitmodules | 3 - .idea/misc.xml | 6 - CHANGELOG.md | 23 +- README.md | 33 +- deploy/ConfigureLayerZeroSettler.s.sol | 207 +- deploy/DeployMain.s.sol | 550 ++--- deploy/FundSigners.s.sol | 118 +- deploy/README.md | 323 ++- deploy/config.toml | 229 +- ...0000000000000000000000000000000000001.json | 1 - ...0000000000000000000000000000000000001.json | 10 - docs/4337CallGraph.md | 32 + docs/IthacaCallGraph.md | 25 + docs/benchmarks.jpeg | Bin 0 -> 291106 bytes foundry.lock | 4 +- foundry.toml | 14 +- gas-snapshots/.gas-snapshot-main | 11 - lib/.idea/.gitignore | 3 - lib/.idea/caches/deviceStreaming.xml | 2007 ----------------- lib/.idea/lib.iml | 9 - lib/.idea/misc.xml | 6 - lib/.idea/modules.xml | 8 - lib/.idea/vcs.xml | 21 - lib/solady | 2 +- snapshots/BenchmarkTest.json | 102 +- src/Escrow.sol | 19 +- src/GuardedExecutor.sol | 8 +- src/IthacaAccount.sol | 22 +- src/MultiSigSigner.sol | 4 +- src/Orchestrator.sol | 371 ++- src/SimpleFunder.sol | 13 +- src/Simulator.sol | 90 +- src/interfaces/ICommon.sol | 80 + src/interfaces/IFunder.sol | 7 +- src/interfaces/IIthacaAccount.sol | 13 +- src/interfaces/IMulticall3.sol | 38 + src/interfaces/IOrchestrator.sol | 9 +- src/libraries/ERC7821Ithaca.sol | 347 --- src/libraries/IntentHelpers.sol | 158 -- src/vendor/layerzero/interfaces/IOAppCore.sol | 54 + .../interfaces/IOAppMsgInspector.sol | 25 + .../layerzero/interfaces/IOAppReceiver.sol | 27 + src/vendor/layerzero/mocks/EndpointV2Mock.sol | 418 ++++ src/vendor/layerzero/oapp/OApp.sol | 38 + src/vendor/layerzero/oapp/OAppCore.sol | 104 + src/vendor/layerzero/oapp/OAppReceiver.sol | 143 ++ src/vendor/layerzero/oapp/OAppSender.sol | 136 ++ test/Account.t.sol | 45 + test/Base.t.sol | 11 +- test/Benchmark.t.sol | 17 +- test/Escrow.t.sol | 74 - test/GuardedExecutor.t.sol | 105 +- test/LayerZeroSettler.t.sol | 6 +- test/MultiSigSigner.t.sol | 656 ++++++ test/Orchestrator.t.sol | 210 +- test/SimulateExecute.t.sol | 38 +- test/SubAccounts.t.sol | 270 --- test/UpgradeTests.t.sol | 566 +++++ test/utils/Brutalizer.sol | 4 +- test/utils/interfaces/IPimlicoPaymaster.sol | 4 +- test/utils/interfaces/ISafe.sol | 57 + test/utils/mocks/MockOrchestrator.sol | 9 +- test/utils/mocks/MockPayerWithSignature.sol | 40 +- .../mocks/MockPayerWithSignatureOptimized.sol | 84 + test/utils/mocks/MockPayerWithState.sol | 39 +- utils/JsonBindings.sol | 767 ------- 71 files changed, 3932 insertions(+), 5009 deletions(-) delete mode 100644 .changeset/README.md delete mode 100644 .changeset/config.json create mode 100644 .github/workflows/auto-assign.yaml delete mode 100644 .idea/misc.xml delete mode 100644 deploy/registry/deployment_11155420_0x0000000000000000000000000000000000000000000000000000000000000001.json delete mode 100644 deploy/registry/deployment_84532_0x0000000000000000000000000000000000000000000000000000000000000001.json create mode 100644 docs/4337CallGraph.md create mode 100644 docs/IthacaCallGraph.md create mode 100644 docs/benchmarks.jpeg delete mode 100644 gas-snapshots/.gas-snapshot-main delete mode 100644 lib/.idea/.gitignore delete mode 100644 lib/.idea/caches/deviceStreaming.xml delete mode 100644 lib/.idea/lib.iml delete mode 100644 lib/.idea/misc.xml delete mode 100644 lib/.idea/modules.xml delete mode 100644 lib/.idea/vcs.xml create mode 100644 src/interfaces/IMulticall3.sol delete mode 100644 src/libraries/ERC7821Ithaca.sol delete mode 100644 src/libraries/IntentHelpers.sol create mode 100644 src/vendor/layerzero/interfaces/IOAppCore.sol create mode 100644 src/vendor/layerzero/interfaces/IOAppMsgInspector.sol create mode 100644 src/vendor/layerzero/interfaces/IOAppReceiver.sol create mode 100644 src/vendor/layerzero/mocks/EndpointV2Mock.sol create mode 100644 src/vendor/layerzero/oapp/OApp.sol create mode 100644 src/vendor/layerzero/oapp/OAppCore.sol create mode 100644 src/vendor/layerzero/oapp/OAppReceiver.sol create mode 100644 src/vendor/layerzero/oapp/OAppSender.sol create mode 100644 test/MultiSigSigner.t.sol delete mode 100644 test/SubAccounts.t.sol create mode 100644 test/UpgradeTests.t.sol create mode 100644 test/utils/interfaces/ISafe.sol create mode 100644 test/utils/mocks/MockPayerWithSignatureOptimized.sol delete mode 100644 utils/JsonBindings.sol diff --git a/.changeset/README.md b/.changeset/README.md deleted file mode 100644 index e5b6d8d6..00000000 --- a/.changeset/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Changesets - -Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works -with multi-package repos, or single-package repos to help you version and publish your code. You can -find the full documentation for it [in our repository](https://github.com/changesets/changesets) - -We have a quick list of common questions to get you started engaging with this project in -[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/config.json b/.changeset/config.json deleted file mode 100644 index d88011f6..00000000 --- a/.changeset/config.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", - "changelog": "@changesets/cli/changelog", - "commit": false, - "fixed": [], - "linked": [], - "access": "restricted", - "baseBranch": "main", - "updateInternalDependencies": "patch", - "ignore": [] -} diff --git a/.env.example b/.env.example index dcfc7f4c..7e85472c 100644 --- a/.env.example +++ b/.env.example @@ -4,7 +4,7 @@ # ============================================ # UPGRADE TESTS # ============================================ -UPGRADE_TEST_RPC_URL=https://base-mainnet.g.alchemy.com/v2/YOUR_API_KEY# Base +UPGRADE_TEST_RPC_URL=https://base-mainnet.g.alchemy.com/v2/YOUR_API_KEY # Base UPGRADE_TEST_OLD_PROXY=0x7C27e3AEcbF42879B64d76f604dC3430F4886462 UPGRADE_TEST_OLD_VERSION=0.5.10 @@ -16,10 +16,10 @@ PRIVATE_KEY= # Format: RPC_{chainId} # Mainnet chains -RPC_1=https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY# Ethereum +RPC_1=https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY # Ethereum # Testnet chains -RPC_11155111=https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY# Sepolia +RPC_11155111=https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY # Sepolia # Test mnemonic for funding script GAS_SIGNER_MNEMONIC="dash between kangaroo vacant gaze glass way sudden retire output retire evil" diff --git a/.github/workflows/auto-assign.yaml b/.github/workflows/auto-assign.yaml new file mode 100644 index 00000000..0ecd2e5e --- /dev/null +++ b/.github/workflows/auto-assign.yaml @@ -0,0 +1,12 @@ +name: Auto Assign PR to Author + +on: + pull_request: + types: [opened, reopened] + +jobs: + auto-assign: + permissions: + issues: write + pull-requests: write + uses: ithacaxyz/ci/.github/workflows/auto-assign-pr.yml@main diff --git a/.github/workflows/version-check.yaml b/.github/workflows/version-check.yaml index 059fa1e4..f5946cb5 100644 --- a/.github/workflows/version-check.yaml +++ b/.github/workflows/version-check.yaml @@ -57,31 +57,22 @@ jobs: - name: Bump version if needed if: steps.check.outputs.needs_version_bump == 'true' run: | - # Configure git - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - # Get the contracts that need bumping CONTRACTS_TO_BUMP="${{ steps.check.outputs.contracts_to_bump }}" - + echo "Bumping versions for contracts: $CONTRACTS_TO_BUMP" - + # Update Solidity files using the upgrade script with specific contracts CONTRACTS_TO_BUMP="$CONTRACTS_TO_BUMP" node prep/update-version.js - - # Commit changes (only Solidity files, not package.json) - git add src/*.sol - git commit -m "chore: bump contract versions due to bytecode changes - Contracts updated: $CONTRACTS_TO_BUMP" - - # Pull latest changes and rebase - # Pull latest changes and rebase - if ! git pull origin ${{ github.event.pull_request.head.ref }} --rebase; then - echo "Failed to rebase version bump changes. Manual intervention required." - exit 1 - fi - - # Push to the PR branch - git push origin HEAD:${{ github.event.pull_request.head.ref }} + + - name: Commit version bump changes + if: steps.check.outputs.needs_version_bump == 'true' + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "chore: bump contract versions due to bytecode changes - Contracts updated: ${{ steps.check.outputs.contracts_to_bump }}" + file_pattern: "src/*.sol" + commit_user_name: github-actions[bot] + commit_user_email: github-actions[bot]@users.noreply.github.com - name: Create PR comment if: steps.check.outputs.needs_version_bump == 'true' diff --git a/.gitmodules b/.gitmodules index 7aed494c..28a7bc52 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,9 +7,6 @@ [submodule "lib/LayerZero-v2"] path = lib/LayerZero-v2 url = https://github.com/LayerZero-Labs/LayerZero-v2 -[submodule "lib/devtools"] - path = lib/devtools - url = https://github.com/LayerZero-Labs/devtools [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 862d09bd..00000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index dae9f612..2fd21859 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # porto-account +> Note: After v0.5.5, all changelogs will be published along with the release notes. +> From here on, this file is deprecated. + +## 0.5.5 + +### Patch Changes +- Update foundry config, to not include metadata in the bytecode. This ensures that the contract bytecode doesn't change because of some other change in the repository. + + +## 0.5.4 + +### Patch Changes + +- SimpleFunder supports multiple orchestrators instead of single immutable orchestrator + - Replaced immutable `ORCHESTRATOR` with `orchestrators` mapping and `setOrchestrators()` function + - Maintained backward compatibility with old `fund()` signature + - Added `supported_orchestrators` config field for deployment + - Version bumped to "0.1.5" + ## 0.5.0 ### Minor Changes @@ -94,7 +113,7 @@ - All fill related functions removed from EP. - EP is now completely stateless, also does not have a constructor. - PreCall with `nonce = type(uint256).max` is not replayable anymore. -- `OpDataTooShort` error, updated to `OpDataError`, to enforce tighter validation of opdata. +- `OpDataTooShort` error, udpated to `OpDataError`, to enforce tighter validation of opdata. - `checkAndIncrementNonce` function added to account. Can only be called by EP. - 6b3294a: Optimize `_isSuperAdmin` @@ -131,7 +150,7 @@ - Add back the INSUFFICIENT_GAS check, which prevents the relay from setting up the `execute` call on the account, in such a way causing it to intentionally fail. - For the relay, gExecute now has to be set at least as `gExecute > (gCombined + 100_000) * 64/63)` + For the relay, gExecute now has to be set atleast as `gExecute > (gCombined + 100_000) * 64/63)` ### Patch Changes diff --git a/README.md b/README.md index ea941768..aa22490b 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,6 @@ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/ithacaxyz/account) -> 🚧 **Work In Progress** -> This repository is under active development. Contracts are **unaudited**, and the codebase may have **breaking changes** without notice. -> A bug bounty is live on Base Mainnet — [details here](docs/bug-bounty.md). - **All-in-one EIP-7702 powered account contract, coupled with [Porto](https://github.com/ithacaxyz/porto)** Every app needs an account, traditionally requiring separate services for auth, payments, and recovery. Doing this in a way that empowers users with control over their funds and their data is the core challenge of the crypto space. While crypto wallets have made great strides, users still face a fragmented experience - juggling private keys, managing account balances across networks, @@ -23,17 +19,24 @@ We believe that unstoppable crypto-powered accounts should be excellent througho # Features out of the box -- [x] Secure Login: Using WebAuthN-compatible credentials like PassKeys. -- [x] Call Batching: Send multiple calls in 1. -- [x] Gas Sponsorship: Allow anyone to pay for your fees in any ERC20 or ETH. -- [x] Access Control: Whitelist receivers, function selectors and arguments. -- [x] Session Keys: Allow transactions without confirmations if they pass low-security access control policies. -- [x] Multi-sig Support: If a call is outside of a certain access control policy, require multiple signatures. -- [x] Interop: Transaction on any chain invisibly. -- [ ] Timelocks: Add a time delay between transaction verification and execution, for additional safety. -- [ ] Optimized for L2: Using BLS signatures. -- [ ] Privacy: Using stealth addresses, confidential ERC20 tokens, and privacy pool integrations. -- [ ] Account Recovery & Identity: Using ZK {Email, OAUth, Passport} and more. +- Secure Login: Using WebAuthN-compatible credentials like PassKeys. +- Call Batching: Send multiple calls in 1. +- Gas Sponsorship: Allow anyone to pay for your fees in any ERC20 or ETH. +- Access Control: Whitelist receivers, function selectors and arguments. +- Session Keys: Allow transactions without confirmations if they pass low-security access control policies. +- Multi-sig Support: If a call is outside of a certain access control policy, require multiple signatures. +- Interop: Transaction on any chain invisibly. + +## Benchmarks + +![Benchmarks](docs/benchmarks.jpeg) + +Gas benchmark implementations are in the [test repository](test/Benchmark.t.sol). We currently benchmark against leading ERC-4337 accounts. To generate the benchmarks, use `forge snapshot --isolate`. + +## Security +Contracts were audited in a 2 week engagement by @MiloTruck @rholterhus @kadenzipfel + +We also maintain an active bug bounty program, you can find more details about it [here](https://porto.sh/contracts/security-and-bug-bounty) ## Getting Help diff --git a/deploy/ConfigureLayerZeroSettler.s.sol b/deploy/ConfigureLayerZeroSettler.s.sol index 4e505329..63187a5f 100644 --- a/deploy/ConfigureLayerZeroSettler.s.sol +++ b/deploy/ConfigureLayerZeroSettler.s.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.23; import {Script} from "forge-std/Script.sol"; import {console} from "forge-std/Script.sol"; +import {Config} from "forge-std/Config.sol"; import {ILayerZeroEndpointV2} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; import {SetConfigParam} from @@ -36,7 +37,7 @@ import {LayerZeroSettler} from "../src/LayerZeroSettler.sol"; * --private-key $L0_SETTLER_OWNER_PK \ * "[84532,11155420]" */ -contract ConfigureLayerZeroSettler is Script { +contract ConfigureLayerZeroSettler is Script, Config { // Configuration type constants (matching ULN302) uint32 constant CONFIG_TYPE_EXECUTOR = 1; uint32 constant CONFIG_TYPE_ULN = 2; @@ -46,6 +47,7 @@ contract ConfigureLayerZeroSettler is Script { string name; address layerZeroSettlerAddress; address layerZeroEndpoint; + address l0SettlerSigner; uint32 eid; address sendUln302; address receiveUln302; @@ -59,6 +61,7 @@ contract ConfigureLayerZeroSettler is Script { // Fork ids for chain switching mapping(uint256 => uint256) public forkIds; + mapping(uint256 => bool) public isForkInitialized; mapping(uint256 => LayerZeroChainConfig) public chainConfigs; uint256[] public configuredChainIds; @@ -66,8 +69,12 @@ contract ConfigureLayerZeroSettler is Script { * @notice Configure all chains with LayerZero configuration */ function run() external { - // Get all chain IDs from fork configuration - uint256[] memory chainIds = vm.readForkChainIds(); + // Load configuration and setup forks + string memory configPath = string.concat(vm.projectRoot(), "/deploy/config.toml"); + _loadConfigAndForks(configPath, false); + + // Get all chain IDs from configuration + uint256[] memory chainIds = config.getChainIds(); run(chainIds); } @@ -79,12 +86,15 @@ contract ConfigureLayerZeroSettler is Script { console.log("Loading configuration from deploy/config.toml"); console.log("Configuring", chainIds.length, "chains"); + // If config not already loaded, load it + if (address(config) == address(0)) { + string memory configPath = string.concat(vm.projectRoot(), "/deploy/config.toml"); + _loadConfigAndForks(configPath, false); + } + // Load configurations for all chains loadConfigurations(chainIds); - // Create forks for all chains that have LayerZero config - createForks(); - // Configure each chain for (uint256 i = 0; i < configuredChainIds.length; i++) { configureChain(configuredChainIds[i]); @@ -100,27 +110,22 @@ contract ConfigureLayerZeroSettler is Script { for (uint256 i = 0; i < chainIds.length; i++) { uint256 chainId = chainIds[i]; - // Create fork to read configuration - string memory rpcUrl = vm.envString(string.concat("RPC_", vm.toString(chainId))); - uint256 forkId = vm.createFork(rpcUrl); - vm.selectFork(forkId); + // Switch to the fork for this chain (already created by _loadConfigAndForks) + vm.selectFork(forkOf[chainId]); // Try to load LayerZero configuration - LayerZeroChainConfig memory config = loadChainConfig(chainId); - - // Only store if chain has LayerZero configuration - if (config.sendUln302 != address(0)) { - chainConfigs[chainId] = config; + LayerZeroChainConfig memory chainConfig = loadChainConfig(chainId); + + // Only add chains that have LayerZero configuration + if (chainConfig.layerZeroSettlerAddress != address(0)) { + chainConfigs[chainId] = chainConfig; configuredChainIds.push(chainId); - forkIds[chainId] = forkId; + forkIds[chainId] = forkOf[chainId]; + isForkInitialized[forkOf[chainId]] = true; console.log( string.concat( - " Loaded LayerZero config for ", - config.name, - " (", - vm.toString(chainId), - ")" + " Loaded LayerZero config for ", chainConfig.name, " (", vm.toString(chainId), ")" ) ); } @@ -130,62 +135,68 @@ contract ConfigureLayerZeroSettler is Script { } /** - * @notice Load configuration for a single chain from fork variables + * @notice Load configuration for a single chain using StdConfig */ function loadChainConfig(uint256 chainId) internal view - returns (LayerZeroChainConfig memory config) + returns (LayerZeroChainConfig memory chainConfig) { - config.chainId = chainId; + chainConfig.chainId = chainId; // Load basic chain info - required - config.name = vm.readForkString("name"); + chainConfig.name = config.get(chainId, "name").toString(); - // Try to load LayerZero settler address - if not present, this chain doesn't have LayerZero config - try vm.readForkAddress("layerzero_settler_address") returns (address addr) { - config.layerZeroSettlerAddress = addr; - } catch { + // Try to load LayerZero settler deployed address first, then fall back to expected address + address settlerAddr = config.get(chainId, "layerzero_settler_deployed").toAddress(); + if (settlerAddr == address(0)) { + // Fall back to expected address from config + settlerAddr = config.get(chainId, "layerzero_settler_address").toAddress(); + } + if (settlerAddr == address(0)) { // No LayerZero settler configured for this chain - this is ok, return empty config - return config; + return chainConfig; } + chainConfig.layerZeroSettlerAddress = settlerAddr; // If we have a LayerZero settler, all other LayerZero fields are required - config.layerZeroEndpoint = vm.readForkAddress("layerzero_endpoint"); - config.eid = uint32(vm.readForkUint("layerzero_eid")); - config.sendUln302 = vm.readForkAddress("layerzero_send_uln302"); - config.receiveUln302 = vm.readForkAddress("layerzero_receive_uln302"); + chainConfig.layerZeroEndpoint = config.get(chainId, "layerzero_endpoint").toAddress(); + chainConfig.l0SettlerSigner = config.get(chainId, "l0_settler_signer").toAddress(); + chainConfig.eid = uint32(config.get(chainId, "layerzero_eid").toUint256()); + chainConfig.sendUln302 = config.get(chainId, "layerzero_send_uln302").toAddress(); + chainConfig.receiveUln302 = config.get(chainId, "layerzero_receive_uln302").toAddress(); // Load destination chain IDs - required for LayerZero configuration - config.destinationChainIds = vm.readForkUintArray("layerzero_destination_chain_ids"); + chainConfig.destinationChainIds = config.get(chainId, "layerzero_destination_chain_ids").toUint256Array(); // Load DVN configuration - required and optional DVN arrays - string[] memory requiredDVNNames = vm.readForkStringArray("layerzero_required_dvns"); - string[] memory optionalDVNNames = vm.readForkStringArray("layerzero_optional_dvns"); + string[] memory requiredDVNNames = config.get(chainId, "layerzero_required_dvns").toStringArray(); + string[] memory optionalDVNNames = config.get(chainId, "layerzero_optional_dvns").toStringArray(); // Resolve DVN names to addresses - config.requiredDVNs = resolveDVNAddresses(requiredDVNNames); - config.optionalDVNs = resolveDVNAddresses(optionalDVNNames); + chainConfig.requiredDVNs = resolveDVNAddresses(chainId, requiredDVNNames); + chainConfig.optionalDVNs = resolveDVNAddresses(chainId, optionalDVNNames); // Load optional DVN threshold - required field - config.optionalDVNThreshold = uint8(vm.readForkUint("layerzero_optional_dvn_threshold")); + chainConfig.optionalDVNThreshold = uint8(config.get(chainId, "layerzero_optional_dvn_threshold").toUint256()); // Load confirmations - required field - config.confirmations = uint64(vm.readForkUint("layerzero_confirmations")); + chainConfig.confirmations = uint64(config.get(chainId, "layerzero_confirmations").toUint256()); // Load max message size - required field - config.maxMessageSize = uint32(vm.readForkUint("layerzero_max_message_size")); + chainConfig.maxMessageSize = uint32(config.get(chainId, "layerzero_max_message_size").toUint256()); - return config; + return chainConfig; } /** - * @notice Resolve DVN names to addresses by reading from fork variables - * @dev Takes DVN variable names and looks up their addresses using vm.readForkAddress + * @notice Resolve DVN names to addresses using StdConfig + * @dev Takes DVN variable names and looks up their addresses using config.get + * @param chainId The chain ID to resolve DVN addresses for * @param dvnNames Array of DVN variable names from config (e.g., "dvn_layerzero_labs") * @return addresses Array of resolved DVN addresses */ - function resolveDVNAddresses(string[] memory dvnNames) + function resolveDVNAddresses(uint256 chainId, string[] memory dvnNames) internal view returns (address[] memory) @@ -193,7 +204,7 @@ contract ConfigureLayerZeroSettler is Script { address[] memory addresses = new address[](dvnNames.length); for (uint256 i = 0; i < dvnNames.length; i++) { - addresses[i] = vm.readForkAddress(dvnNames[i]); + addresses[i] = config.get(chainId, dvnNames[i]).toAddress(); require( addresses[i] != address(0), string.concat("DVN address not configured for: ", dvnNames[i]) @@ -203,18 +214,8 @@ contract ConfigureLayerZeroSettler is Script { return addresses; } - /** - * @notice Create forks for all configured chains - */ - function createForks() internal { - console.log("\n=== Creating forks for configured chains ==="); - - for (uint256 i = 0; i < configuredChainIds.length; i++) { - uint256 chainId = configuredChainIds[i]; - LayerZeroChainConfig memory config = chainConfigs[chainId]; - - console.log(" Fork created for", config.name, "fork ID:", forkIds[chainId]); - } + function _selectFork(uint256 chainId) internal { + vm.selectFork(forkOf[chainId]); } /** @@ -227,31 +228,59 @@ contract ConfigureLayerZeroSettler is Script { console.log(string.concat("Configuring ", config.name, " (", vm.toString(chainId), ")")); console.log(" LayerZero Settler:", config.layerZeroSettlerAddress); console.log(" Endpoint:", config.layerZeroEndpoint); + console.log(" L0SettlerSigner:", config.l0SettlerSigner); console.log(" EID:", config.eid); // Switch to the source chain - vm.selectFork(forkIds[chainId]); + _selectFork(chainId); LayerZeroSettler settler = LayerZeroSettler(payable(config.layerZeroSettlerAddress)); + + // Set or update the endpoint on the settler + address currentEndpoint = address(settler.endpoint()); + if (currentEndpoint != config.layerZeroEndpoint) { + if (currentEndpoint == address(0)) { + console.log(" Setting endpoint to:", config.layerZeroEndpoint); + } else { + console.log(" Updating endpoint from:", currentEndpoint); + console.log(" To:", config.layerZeroEndpoint); + } + vm.broadcast(); + settler.setEndpoint(config.layerZeroEndpoint); + console.log(" Endpoint configured successfully"); + } else { + console.log(" Endpoint already set to:", config.layerZeroEndpoint); + } + + // Set or update the L0SettlerSigner on the settler + address currentSigner = settler.l0SettlerSigner(); + if (currentSigner != config.l0SettlerSigner) { + if (currentSigner == address(0)) { + console.log(" Setting L0SettlerSigner to:", config.l0SettlerSigner); + } else { + console.log(" Updating L0SettlerSigner from:", currentSigner); + console.log(" To:", config.l0SettlerSigner); + } + vm.broadcast(); + settler.setL0SettlerSigner(config.l0SettlerSigner); + console.log(" L0SettlerSigner configured successfully"); + } else { + console.log(" L0SettlerSigner already set to:", config.l0SettlerSigner); + } + ILayerZeroEndpointV2 endpoint = ILayerZeroEndpointV2(config.layerZeroEndpoint); // Configure pathways to all destinations for (uint256 i = 0; i < config.destinationChainIds.length; i++) { uint256 destChainId = config.destinationChainIds[i]; - // Skip if destination not configured - if (forkIds[destChainId] == 0) { - console.log(" Skipping unconfigured destination:", destChainId); - continue; - } - LayerZeroChainConfig memory destConfig = chainConfigs[destChainId]; console.log(string.concat("\n Configuring pathway to ", destConfig.name)); console.log(" Destination EID:", destConfig.eid); // Set executor config (self-execution model) - setExecutorConfig(settler, endpoint, destConfig.eid); + setExecutorConfig(config, settler, endpoint, destConfig.eid); // Set send ULN config setSendUlnConfig( @@ -266,11 +295,31 @@ contract ConfigureLayerZeroSettler is Script { ); // Switch to destination chain to set receive config - vm.selectFork(forkIds[destChainId]); + _selectFork(destChainId); + + // Ensure destination settler has endpoint set before configuring + LayerZeroSettler destSettler = + LayerZeroSettler(payable(destConfig.layerZeroSettlerAddress)); + address currentDestEndpoint = address(destSettler.endpoint()); + if (currentDestEndpoint != destConfig.layerZeroEndpoint) { + if (currentDestEndpoint == address(0)) { + console.log( + " Setting endpoint on destination:", destConfig.layerZeroEndpoint + ); + } else { + console.log(" Updating endpoint on destination from:", currentDestEndpoint); + console.log(" To:", destConfig.layerZeroEndpoint); + } + vm.broadcast(); + destSettler.setEndpoint(destConfig.layerZeroEndpoint); + console.log(" Destination endpoint configured successfully"); + } else { + console.log(" Destination endpoint already set:", destConfig.layerZeroEndpoint); + } // Set receive ULN config on destination setReceiveUlnConfig( - LayerZeroSettler(payable(destConfig.layerZeroSettlerAddress)), + destSettler, ILayerZeroEndpointV2(destConfig.layerZeroEndpoint), config.eid, // Source EID destConfig.receiveUln302, @@ -281,7 +330,7 @@ contract ConfigureLayerZeroSettler is Script { ); // Switch back to source chain - vm.selectFork(forkIds[chainId]); + _selectFork(chainId); } console.log("\n Configuration complete for", config.name); @@ -292,12 +341,24 @@ contract ConfigureLayerZeroSettler is Script { // ============================================ function setExecutorConfig( + LayerZeroChainConfig memory config, LayerZeroSettler settler, ILayerZeroEndpointV2 endpoint, uint32 destEid ) internal { - // LayerZeroSettler uses self-execution model, no executor config needed - console.log(" Using self-execution model (no executor config)"); + console.log(" Setting executor config:"); + console.log(" Executor (self-execution):", address(settler)); + console.log(" Max message size:", config.maxMessageSize); + console.log(" Send ULN302:", config.sendUln302); + + bytes memory executorConfig = abi.encode(config.maxMessageSize, settler); + SetConfigParam[] memory params = new SetConfigParam[](1); + params[0] = + SetConfigParam({eid: destEid, configType: CONFIG_TYPE_EXECUTOR, config: executorConfig}); + + vm.broadcast(); + endpoint.setConfig(address(settler), config.sendUln302, params); + console.log(" Executor config set"); } function setSendUlnConfig( @@ -337,7 +398,7 @@ contract ConfigureLayerZeroSettler is Script { }); // Get the L0 settler owner who should be the delegate - address l0SettlerOwner = vm.readForkAddress("l0_settler_owner"); + address l0SettlerOwner = config.get(vm.getChainId(), "l0_settler_owner").toAddress(); console.log(" L0 Settler Owner (delegate):", l0SettlerOwner); vm.broadcast(); diff --git a/deploy/DeployMain.s.sol b/deploy/DeployMain.s.sol index 5ebbb109..ee513799 100644 --- a/deploy/DeployMain.s.sol +++ b/deploy/DeployMain.s.sol @@ -3,7 +3,8 @@ pragma solidity ^0.8.23; import {Script, console} from "forge-std/Script.sol"; import {VmSafe} from "forge-std/Vm.sol"; -import {stdToml} from "forge-std/StdToml.sol"; +import {Config} from "forge-std/Config.sol"; +import {Variable, TypeKind} from "forge-std/LibVariable.sol"; import {SafeSingletonDeployer} from "./SafeSingletonDeployer.sol"; // Import contracts to deploy @@ -15,6 +16,7 @@ import {SimpleFunder} from "../src/SimpleFunder.sol"; import {Escrow} from "../src/Escrow.sol"; import {SimpleSettler} from "../src/SimpleSettler.sol"; import {LayerZeroSettler} from "../src/LayerZeroSettler.sol"; +import {ExperimentERC20} from "./mock/ExperimentalERC20.sol"; /** * @title DeployMain @@ -49,23 +51,26 @@ import {LayerZeroSettler} from "../src/LayerZeroSettler.sol"; * --private-key $PRIVATE_KEY \ * "[1]" "/deploy/custom-config.toml" */ -contract DeployMain is Script, SafeSingletonDeployer { - using stdToml for string; +contract DeployMain is Script, Config, SafeSingletonDeployer { // Chain configuration struct struct ChainConfig { uint256 chainId; string name; bool isTestnet; - address pauseAuthority; address funderOwner; address funderSigner; address settlerOwner; address l0SettlerOwner; + address l0SettlerSigner; address layerZeroEndpoint; + address[] oldOrchestrators; uint32 layerZeroEid; bytes32 salt; string[] contracts; // Array of contract names to deploy + // EXP Token configuration (testnet only) + address expMinterAddress; + uint256 expMintAmount; } struct DeployedContracts { @@ -77,6 +82,8 @@ contract DeployMain is Script, SafeSingletonDeployer { address layerZeroSettler; address simpleFunder; address simulator; + address expToken; // EXP token (testnet only) + address exp2Token; // EXP2 token (testnet only) } // State @@ -84,9 +91,7 @@ contract DeployMain is Script, SafeSingletonDeployer { mapping(uint256 => DeployedContracts) internal deployedContracts; uint256[] internal targetChainIds; - // Paths and config - string internal registryPath; - string internal configContent; // For unified config + // Config path string internal configPath = "/deploy/config.toml"; // Events for tracking @@ -104,9 +109,17 @@ contract DeployMain is Script, SafeSingletonDeployer { * @notice Deploy to all chains in config */ function run() external { - // Get all available chain IDs from fork configuration - uint256[] memory chainIds = vm.readForkChainIds(); - initializeDeployment(chainIds); + // Load configuration and setup forks (enable write-back to save deployed addresses) + string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); + _loadConfigAndForks(fullConfigPath, true); + + // Get all available chain IDs from configuration + targetChainIds = config.getChainIds(); + require(targetChainIds.length > 0, "No chains found in configuration"); + + // Load configuration for each chain + loadConfigurations(); + loadDeployedContracts(); executeDeployment(); } @@ -115,11 +128,20 @@ contract DeployMain is Script, SafeSingletonDeployer { * @param chainIds Array of chain IDs to deploy to (empty array = all chains) */ function run(uint256[] memory chainIds) external { + // Load configuration and setup forks (enable write-back to save deployed addresses) + string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); + _loadConfigAndForks(fullConfigPath, true); + // If empty array, get all available chains if (chainIds.length == 0) { - chainIds = vm.readForkChainIds(); + chainIds = config.getChainIds(); } - initializeDeployment(chainIds); + targetChainIds = chainIds; + require(targetChainIds.length > 0, "No chains found in configuration"); + + // Load configuration for each chain + loadConfigurations(); + loadDeployedContracts(); executeDeployment(); } @@ -129,47 +151,25 @@ contract DeployMain is Script, SafeSingletonDeployer { * @param _configPath Path to custom TOML config file */ function run(uint256[] memory chainIds, string memory _configPath) external { + configPath = _configPath; + + // Load configuration and setup forks (enable write-back to save deployed addresses) + string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); + _loadConfigAndForks(fullConfigPath, true); + // If empty array, get all available chains if (chainIds.length == 0) { - chainIds = vm.readForkChainIds(); + chainIds = config.getChainIds(); } - initializeDeployment(chainIds, _configPath); - executeDeployment(); - } - - /** - * @notice Initialize deployment with target chains using TOML config - * @param chainIds Array of chain IDs to deploy to - */ - function initializeDeployment(uint256[] memory chainIds) internal { - require(chainIds.length > 0, "No chains found in configuration"); - - // Load unified configuration - string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); - configContent = vm.readFile(fullConfigPath); - - // Load registry path from config.toml - registryPath = configContent.readString(".profile.deployment.registry_path"); - - // Store target chain IDs targetChainIds = chainIds; - + require(targetChainIds.length > 0, "No chains found in configuration"); + // Load configuration for each chain loadConfigurations(); - - // Load existing deployed contracts from registry loadDeployedContracts(); + executeDeployment(); } - /** - * @notice Initialize deployment with custom config path - * @param chainIds Array of chain IDs to deploy to - * @param _configPath Path to the config file - */ - function initializeDeployment(uint256[] memory chainIds, string memory _configPath) internal { - configPath = _configPath; - initializeDeployment(chainIds); - } /** * @notice Load configurations for all target chains @@ -178,20 +178,15 @@ contract DeployMain is Script, SafeSingletonDeployer { for (uint256 i = 0; i < targetChainIds.length; i++) { uint256 chainId = targetChainIds[i]; - // Use the RPC_{chainId} environment variable directly - // This matches the naming convention in config.toml - string memory rpcUrl = vm.envString(string.concat("RPC_", vm.toString(chainId))); - - // Create fork using the RPC URL - uint256 forkId = vm.createFork(rpcUrl); - vm.selectFork(forkId); + // Switch to the fork for this chain (already created by _loadConfigAndForks) + vm.selectFork(forkOf[chainId]); // Verify we're on the correct chain require(block.chainid == chainId, "Chain ID mismatch"); - // Load configuration from fork variables - ChainConfig memory config = loadChainConfigFromFork(chainId); - chainConfigs[chainId] = config; + // Load configuration using new StdConfig pattern + ChainConfig memory chainConfig = loadChainConfigFromStdConfig(chainId); + chainConfigs[chainId] = chainConfig; } // Log the loaded configuration for verification @@ -199,48 +194,66 @@ contract DeployMain is Script, SafeSingletonDeployer { } /** - * @notice Load chain configuration from the currently active fork + * @notice Load chain configuration using StdConfig * @param chainId The chain ID we're loading config for */ - function loadChainConfigFromFork(uint256 chainId) internal view returns (ChainConfig memory) { - ChainConfig memory config; + function loadChainConfigFromStdConfig(uint256 chainId) internal view returns (ChainConfig memory) { + ChainConfig memory chainConfig; - config.chainId = chainId; + chainConfig.chainId = chainId; - // Use vm.readFork* functions to read variables from the active fork - config.name = vm.readForkString("name"); - config.isTestnet = vm.readForkBool("is_testnet"); + // Use StdConfig to read variables + chainConfig.name = config.get(chainId, "name").toString(); + chainConfig.isTestnet = config.get(chainId, "is_testnet").toBool(); // Load addresses - config.pauseAuthority = vm.readForkAddress("pause_authority"); - config.funderOwner = vm.readForkAddress("funder_owner"); - config.funderSigner = vm.readForkAddress("funder_signer"); - config.settlerOwner = vm.readForkAddress("settler_owner"); - config.l0SettlerOwner = vm.readForkAddress("l0_settler_owner"); - config.layerZeroEndpoint = vm.readForkAddress("layerzero_endpoint"); + chainConfig.funderOwner = config.get(chainId, "funder_owner").toAddress(); + chainConfig.funderSigner = config.get(chainId, "funder_signer").toAddress(); + chainConfig.settlerOwner = config.get(chainId, "settler_owner").toAddress(); + chainConfig.l0SettlerOwner = config.get(chainId, "l0_settler_owner").toAddress(); + chainConfig.l0SettlerSigner = config.get(chainId, "l0_settler_signer").toAddress(); + chainConfig.layerZeroEndpoint = config.get(chainId, "layerzero_endpoint").toAddress(); // Load other configuration - config.layerZeroEid = uint32(vm.readForkUint("layerzero_eid")); - config.salt = vm.readForkBytes32("salt"); + chainConfig.layerZeroEid = uint32(config.get(chainId, "layerzero_eid").toUint256()); + chainConfig.salt = config.get(chainId, "salt").toBytes32(); + + // Load EXP token configuration (testnet only) + if (chainConfig.isTestnet) { + chainConfig.expMinterAddress = config.get(chainId, "exp_minter_address").toAddress(); + chainConfig.expMintAmount = config.get(chainId, "exp_mint_amount").toUint256(); + } // Load contracts list - required field, will revert if not present - string[] memory contractsList = vm.readForkStringArray("contracts"); + string[] memory contractsList = config.get(chainId, "contracts").toStringArray(); // Check if user specified "ALL" to deploy all contracts if ( contractsList.length == 1 && keccak256(bytes(contractsList[0])) == keccak256(bytes("ALL")) ) { - config.contracts = getAllContracts(); + string[] memory baseContracts = getAllContracts(); + // For testnets with ALL specified, append ExpToken + if (chainConfig.isTestnet) { + string[] memory testnetContracts = new string[](baseContracts.length + 1); + for (uint256 i = 0; i < baseContracts.length; i++) { + testnetContracts[i] = baseContracts[i]; + } + testnetContracts[baseContracts.length] = "ExpToken"; + chainConfig.contracts = testnetContracts; + } else { + // For non-testnets, use base contracts (no ExpToken) + chainConfig.contracts = baseContracts; + } } else { - config.contracts = contractsList; + chainConfig.contracts = contractsList; } - return config; + return chainConfig; } /** - * @notice Get all available contracts + * @notice Get all available contracts (excluding ExpToken) */ function getAllContracts() internal pure returns (string[] memory) { string[] memory contracts = new string[](8); @@ -273,31 +286,40 @@ contract DeployMain is Script, SafeSingletonDeployer { } /** - * @notice Load deployed contracts from registry + * @notice Load deployed contracts from config */ function loadDeployedContracts() internal { for (uint256 i = 0; i < targetChainIds.length; i++) { uint256 chainId = targetChainIds[i]; - bytes32 salt = chainConfigs[chainId].salt; - string memory registryFile = getRegistryFilename(chainId, salt); - - try vm.readFile(registryFile) returns (string memory registryJson) { - // Use individual parsing for flexibility with missing fields - DeployedContracts memory deployed; - deployed.ithacaAccount = tryReadAddress(registryJson, ".IthacaAccount"); - deployed.accountProxy = tryReadAddress(registryJson, ".AccountProxy"); - deployed.escrow = tryReadAddress(registryJson, ".Escrow"); - deployed.orchestrator = tryReadAddress(registryJson, ".Orchestrator"); - deployed.simpleSettler = tryReadAddress(registryJson, ".SimpleSettler"); - deployed.layerZeroSettler = tryReadAddress(registryJson, ".LayerZeroSettler"); - deployed.simpleFunder = tryReadAddress(registryJson, ".SimpleFunder"); - deployed.simulator = tryReadAddress(registryJson, ".Simulator"); - - deployedContracts[chainId] = deployed; - } catch { - // No registry file exists yet - } + + DeployedContracts memory deployed; + + // Read deployed contract addresses from config, defaulting to address(0) if not set + deployed.orchestrator = tryGetAddress(chainId, "orchestrator_deployed"); + deployed.ithacaAccount = tryGetAddress(chainId, "ithaca_account_deployed"); + deployed.accountProxy = tryGetAddress(chainId, "account_proxy_deployed"); + deployed.escrow = tryGetAddress(chainId, "escrow_deployed"); + deployed.simpleSettler = tryGetAddress(chainId, "simple_settler_deployed"); + deployed.layerZeroSettler = tryGetAddress(chainId, "layerzero_settler_deployed"); + deployed.simpleFunder = tryGetAddress(chainId, "simple_funder_deployed"); + deployed.simulator = tryGetAddress(chainId, "simulator_deployed"); + deployed.expToken = tryGetAddress(chainId, "exp_token_deployed"); + deployed.exp2Token = tryGetAddress(chainId, "exp2_token_deployed"); + + deployedContracts[chainId] = deployed; + } + } + + /** + * @notice Try to get an address from config, return address(0) if not found + */ + function tryGetAddress(uint256 chainId, string memory key) internal view returns (address) { + Variable memory variable = config.get(chainId, key); + // Check if variable exists (TypeKind.None means missing key) + if (variable.ty.kind == TypeKind.None) { + return address(0); } + return variable.toAddress(); } /** @@ -315,8 +337,8 @@ contract DeployMain is Script, SafeSingletonDeployer { console.log("Funder Owner:", config.funderOwner); console.log("Funder Signer:", config.funderSigner); console.log("L0 Settler Owner:", config.l0SettlerOwner); + console.log("L0 Settler Signer:", config.l0SettlerSigner); console.log("Settler Owner:", config.settlerOwner); - console.log("Pause Authority:", config.pauseAuthority); console.log("LayerZero Endpoint:", config.layerZeroEndpoint); console.log("LayerZero EID:", config.layerZeroEid); console.log("Salt:"); @@ -423,7 +445,7 @@ contract DeployMain is Script, SafeSingletonDeployer { } /** - * @notice Save deployed contract address to registry + * @notice Save deployed contract address to config */ function saveDeployedContract( uint256 chainId, @@ -447,124 +469,38 @@ contract DeployMain is Script, SafeSingletonDeployer { deployedContracts[chainId].simpleSettler = contractAddress; } else if (keccak256(bytes(contractName)) == keccak256("LayerZeroSettler")) { deployedContracts[chainId].layerZeroSettler = contractAddress; + } else if (keccak256(bytes(contractName)) == keccak256("ExpToken")) { + deployedContracts[chainId].expToken = contractAddress; + } else if (keccak256(bytes(contractName)) == keccak256("Exp2Token")) { + deployedContracts[chainId].exp2Token = contractAddress; + } + + // Only write to config file during actual broadcasts, not simulations + if (vm.isContext(VmSafe.ForgeContext.ScriptBroadcast) || vm.isContext(VmSafe.ForgeContext.ScriptResume)) { + if (keccak256(bytes(contractName)) == keccak256("Orchestrator")) { + config.set(chainId, "orchestrator_deployed", contractAddress); + } else if (keccak256(bytes(contractName)) == keccak256("IthacaAccount")) { + config.set(chainId, "ithaca_account_deployed", contractAddress); + } else if (keccak256(bytes(contractName)) == keccak256("AccountProxy")) { + config.set(chainId, "account_proxy_deployed", contractAddress); + } else if (keccak256(bytes(contractName)) == keccak256("Simulator")) { + config.set(chainId, "simulator_deployed", contractAddress); + } else if (keccak256(bytes(contractName)) == keccak256("SimpleFunder")) { + config.set(chainId, "simple_funder_deployed", contractAddress); + } else if (keccak256(bytes(contractName)) == keccak256("Escrow")) { + config.set(chainId, "escrow_deployed", contractAddress); + } else if (keccak256(bytes(contractName)) == keccak256("SimpleSettler")) { + config.set(chainId, "simple_settler_deployed", contractAddress); + } else if (keccak256(bytes(contractName)) == keccak256("LayerZeroSettler")) { + config.set(chainId, "layerzero_settler_deployed", contractAddress); + } else if (keccak256(bytes(contractName)) == keccak256("ExpToken")) { + config.set(chainId, "exp_token_deployed", contractAddress); + } else if (keccak256(bytes(contractName)) == keccak256("Exp2Token")) { + config.set(chainId, "exp2_token_deployed", contractAddress); + } } - - // Write to registry file - writeToRegistry(chainId, contractName, contractAddress); - } - - /** - * @notice Write to registry file - */ - function writeToRegistry(uint256 chainId, string memory contractName, address contractAddress) - internal - { - // Only save registry during actual broadcasts, not dry runs - if ( - !vm.isContext(VmSafe.ForgeContext.ScriptBroadcast) - && !vm.isContext(VmSafe.ForgeContext.ScriptResume) - ) { - return; - } - - DeployedContracts memory deployed = deployedContracts[chainId]; - - string memory json = "{"; - bool first = true; - - if (deployed.orchestrator != address(0)) { - json = string.concat(json, '"Orchestrator": "', vm.toString(deployed.orchestrator), '"'); - first = false; - } - - if (deployed.ithacaAccount != address(0)) { - if (!first) json = string.concat(json, ","); - json = - string.concat(json, '"IthacaAccount": "', vm.toString(deployed.ithacaAccount), '"'); - first = false; - } - - if (deployed.accountProxy != address(0)) { - if (!first) json = string.concat(json, ","); - json = string.concat(json, '"AccountProxy": "', vm.toString(deployed.accountProxy), '"'); - first = false; - } - - if (deployed.simulator != address(0)) { - if (!first) json = string.concat(json, ","); - json = string.concat(json, '"Simulator": "', vm.toString(deployed.simulator), '"'); - first = false; - } - - if (deployed.simpleFunder != address(0)) { - if (!first) json = string.concat(json, ","); - json = string.concat(json, '"SimpleFunder": "', vm.toString(deployed.simpleFunder), '"'); - first = false; - } - - if (deployed.escrow != address(0)) { - if (!first) json = string.concat(json, ","); - json = string.concat(json, '"Escrow": "', vm.toString(deployed.escrow), '"'); - first = false; - } - - if (deployed.simpleSettler != address(0)) { - if (!first) json = string.concat(json, ","); - json = - string.concat(json, '"SimpleSettler": "', vm.toString(deployed.simpleSettler), '"'); - first = false; - } - - if (deployed.layerZeroSettler != address(0)) { - if (!first) json = string.concat(json, ","); - json = string.concat( - json, '"LayerZeroSettler": "', vm.toString(deployed.layerZeroSettler), '"' - ); - } - - json = string.concat(json, "}"); - - bytes32 salt = chainConfigs[chainId].salt; - string memory registryFile = getRegistryFilename(chainId, salt); - vm.writeFile(registryFile, json); - } - - /** - * @notice Get registry filename based on chainId and salt - */ - function getRegistryFilename(uint256 chainId, bytes32 salt) - internal - view - returns (string memory) - { - string memory filename = string.concat( - vm.projectRoot(), - "/", - registryPath, - "deployment_", - vm.toString(chainId), - "_", - vm.toString(salt), - ".json" - ); - return filename; } - /** - * @notice Try to read an address from JSON - */ - function tryReadAddress(string memory json, string memory key) - internal - pure - returns (address) - { - try vm.parseJson(json, key) returns (bytes memory data) { - if (data.length > 0) { - return abi.decode(data, (address)); - } - } catch {} - return address(0); - } /** * @notice Verify Safe Singleton Factory is deployed @@ -663,6 +599,8 @@ contract DeployMain is Script, SafeSingletonDeployer { deploySimpleSettler(chainId, config, deployed); } else if (nameHash == keccak256("LayerZeroSettler")) { deployLayerZeroSettler(chainId, config, deployed); + } else if (nameHash == keccak256("ExpToken")) { + deployExpToken(chainId, config, deployed); } else { console.log("Warning: Unknown contract name:", contractName); } @@ -673,17 +611,12 @@ contract DeployMain is Script, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - if (deployed.orchestrator == address(0)) { - bytes memory creationCode = type(Orchestrator).creationCode; - bytes memory args = abi.encode(config.pauseAuthority); - address orchestrator = - deployContractWithCreate2(chainId, creationCode, args, "Orchestrator"); - - saveDeployedContract(chainId, "Orchestrator", orchestrator); - deployed.orchestrator = orchestrator; - } else { - console.log("Orchestrator already deployed:", deployed.orchestrator); - } + bytes memory creationCode = type(Orchestrator).creationCode; + address orchestrator = + deployContractWithCreate2(chainId, creationCode, "", "Orchestrator"); + + saveDeployedContract(chainId, "Orchestrator", orchestrator); + deployed.orchestrator = orchestrator; } function deployIthacaAccount( @@ -692,22 +625,15 @@ contract DeployMain is Script, SafeSingletonDeployer { DeployedContracts memory deployed ) internal { // Ensure Orchestrator is deployed first (dependency) - if (deployed.orchestrator == address(0)) { - console.log("Deploying Orchestrator first (dependency for IthacaAccount)..."); - deployOrchestrator(chainId, config, deployed); - } + require(deployed.orchestrator != address(0), "Orchestrator must be deployed before IthacaAccount"); - if (deployed.ithacaAccount == address(0)) { - bytes memory creationCode = type(IthacaAccount).creationCode; - bytes memory args = abi.encode(deployed.orchestrator); - address ithacaAccount = - deployContractWithCreate2(chainId, creationCode, args, "IthacaAccount"); + bytes memory creationCode = type(IthacaAccount).creationCode; + bytes memory args = abi.encode(deployed.orchestrator); + address ithacaAccount = + deployContractWithCreate2(chainId, creationCode, args, "IthacaAccount"); - saveDeployedContract(chainId, "IthacaAccount", ithacaAccount); - deployed.ithacaAccount = ithacaAccount; - } else { - console.log("IthacaAccount already deployed:", deployed.ithacaAccount); - } + saveDeployedContract(chainId, "IthacaAccount", ithacaAccount); + deployed.ithacaAccount = ithacaAccount; } function deployAccountProxy( @@ -716,21 +642,14 @@ contract DeployMain is Script, SafeSingletonDeployer { DeployedContracts memory deployed ) internal { // Ensure IthacaAccount is deployed first (dependency) - if (deployed.ithacaAccount == address(0)) { - console.log("Deploying IthacaAccount first (dependency for AccountProxy)..."); - deployIthacaAccount(chainId, config, deployed); - } + require(deployed.ithacaAccount != address(0), "IthacaAccount must be deployed before AccountProxy"); - if (deployed.accountProxy == address(0)) { - bytes memory proxyCode = LibEIP7702.proxyInitCode(deployed.ithacaAccount, address(0)); - address accountProxy = deployContractWithCreate2(chainId, proxyCode, "", "AccountProxy"); + bytes memory proxyCode = LibEIP7702.proxyInitCode(deployed.ithacaAccount, address(0)); + address accountProxy = deployContractWithCreate2(chainId, proxyCode, "", "AccountProxy"); - require(accountProxy != address(0), "Account proxy deployment failed"); - saveDeployedContract(chainId, "AccountProxy", accountProxy); - deployed.accountProxy = accountProxy; - } else { - console.log("AccountProxy already deployed:", deployed.accountProxy); - } + require(accountProxy != address(0), "Account proxy deployment failed"); + saveDeployedContract(chainId, "AccountProxy", accountProxy); + deployed.accountProxy = accountProxy; } function deploySimulator( @@ -738,15 +657,11 @@ contract DeployMain is Script, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - if (deployed.simulator == address(0)) { - bytes memory creationCode = type(Simulator).creationCode; - address simulator = deployContractWithCreate2(chainId, creationCode, "", "Simulator"); + bytes memory creationCode = type(Simulator).creationCode; + address simulator = deployContractWithCreate2(chainId, creationCode, "", "Simulator"); - saveDeployedContract(chainId, "Simulator", simulator); - deployed.simulator = simulator; - } else { - console.log("Simulator already deployed:", deployed.simulator); - } + saveDeployedContract(chainId, "Simulator", simulator); + deployed.simulator = simulator; } function deploySimpleFunder( @@ -754,23 +669,13 @@ contract DeployMain is Script, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - // Ensure Orchestrator is deployed first (dependency) - if (deployed.orchestrator == address(0)) { - console.log("Deploying Orchestrator first (dependency for SimpleFunder)..."); - deployOrchestrator(chainId, config, deployed); - } + bytes memory creationCode = type(SimpleFunder).creationCode; - if (deployed.simpleFunder == address(0)) { - bytes memory creationCode = type(SimpleFunder).creationCode; - bytes memory args = - abi.encode(config.funderSigner, deployed.orchestrator, config.funderOwner); - address funder = deployContractWithCreate2(chainId, creationCode, args, "SimpleFunder"); + bytes memory args = abi.encode(config.funderSigner, config.funderOwner); + address funder = deployContractWithCreate2(chainId, creationCode, args, "SimpleFunder"); - saveDeployedContract(chainId, "SimpleFunder", funder); - deployed.simpleFunder = funder; - } else { - console.log("SimpleFunder already deployed:", deployed.simpleFunder); - } + saveDeployedContract(chainId, "SimpleFunder", funder); + deployed.simpleFunder = funder; } function deployEscrow( @@ -778,15 +683,11 @@ contract DeployMain is Script, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - if (deployed.escrow == address(0)) { - bytes memory creationCode = type(Escrow).creationCode; - address escrow = deployContractWithCreate2(chainId, creationCode, "", "Escrow"); + bytes memory creationCode = type(Escrow).creationCode; + address escrow = deployContractWithCreate2(chainId, creationCode, "", "Escrow"); - saveDeployedContract(chainId, "Escrow", escrow); - deployed.escrow = escrow; - } else { - console.log("Escrow already deployed:", deployed.escrow); - } + saveDeployedContract(chainId, "Escrow", escrow); + deployed.escrow = escrow; } function deploySimpleSettler( @@ -794,18 +695,14 @@ contract DeployMain is Script, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - if (deployed.simpleSettler == address(0)) { - bytes memory creationCode = type(SimpleSettler).creationCode; - bytes memory args = abi.encode(config.settlerOwner); - address settler = - deployContractWithCreate2(chainId, creationCode, args, "SimpleSettler"); - - console.log(" Owner:", config.settlerOwner); - saveDeployedContract(chainId, "SimpleSettler", settler); - deployed.simpleSettler = settler; - } else { - console.log("SimpleSettler already deployed:", deployed.simpleSettler); - } + bytes memory creationCode = type(SimpleSettler).creationCode; + bytes memory args = abi.encode(config.settlerOwner); + address settler = + deployContractWithCreate2(chainId, creationCode, args, "SimpleSettler"); + + console.log(" Owner:", config.settlerOwner); + saveDeployedContract(chainId, "SimpleSettler", settler); + deployed.simpleSettler = settler; } function deployLayerZeroSettler( @@ -813,19 +710,62 @@ contract DeployMain is Script, SafeSingletonDeployer { ChainConfig memory config, DeployedContracts memory deployed ) internal { - if (deployed.layerZeroSettler == address(0)) { - bytes memory creationCode = type(LayerZeroSettler).creationCode; - bytes memory args = abi.encode(config.layerZeroEndpoint, config.l0SettlerOwner); - address settler = - deployContractWithCreate2(chainId, creationCode, args, "LayerZeroSettler"); - - console.log(" Endpoint:", config.layerZeroEndpoint); - console.log(" Owner:", config.l0SettlerOwner); - console.log(" EID:", config.layerZeroEid); - saveDeployedContract(chainId, "LayerZeroSettler", settler); - deployed.layerZeroSettler = settler; - } else { - console.log("LayerZeroSettler already deployed:", deployed.layerZeroSettler); + bytes memory creationCode = type(LayerZeroSettler).creationCode; + bytes memory args = abi.encode(config.l0SettlerOwner, config.l0SettlerSigner); + address settler = + deployContractWithCreate2(chainId, creationCode, args, "LayerZeroSettler"); + + console.log(" Owner:", config.l0SettlerOwner); + console.log(" L0SettlerSigner:", config.l0SettlerSigner); + console.log(" Endpoint to be configured:", config.layerZeroEndpoint); + console.log(" EID:", config.layerZeroEid); + console.log( + " Note: Endpoint must be set by owner via ConfigureLayerZeroSettler script" + ); + + saveDeployedContract(chainId, "LayerZeroSettler", settler); + deployed.layerZeroSettler = settler; + } + + function deployExpToken( + uint256 chainId, + ChainConfig memory config, + DeployedContracts memory deployed + ) internal { + // Only deploy on testnets + if (!config.isTestnet) { + console.log("Skipping ExpToken deployment - not a testnet"); + return; } + + bytes memory creationCode = type(ExperimentERC20).creationCode; + + // Deploy EXP token + // Hardcode name and symbol to "EXP" + bytes memory args = abi.encode("EXP", "EXP", 1 ether); + address expToken = deployContractWithCreate2(chainId, creationCode, args, "ExpToken"); + + // Mint initial tokens to the configured minter address + ExperimentERC20(payable(expToken)).mint(config.expMinterAddress, config.expMintAmount); + + console.log(" EXP Name/Symbol: EXP"); + console.log(" EXP Address:", expToken); + saveDeployedContract(chainId, "ExpToken", expToken); + deployed.expToken = expToken; + + // Deploy EXP2 token + // Hardcode name and symbol to "EXP2" + bytes memory args2 = abi.encode("EXP2", "EXP2", 1 ether); + address exp2Token = deployContractWithCreate2(chainId, creationCode, args2, "Exp2Token"); + + // Mint initial tokens to the configured minter address (same as EXP) + ExperimentERC20(payable(exp2Token)).mint(config.expMinterAddress, config.expMintAmount); + + console.log(" EXP2 Name/Symbol: EXP2"); + console.log(" EXP2 Address:", exp2Token); + console.log(" Minter:", config.expMinterAddress); + console.log(" Mint Amount (each):", config.expMintAmount); + saveDeployedContract(chainId, "Exp2Token", exp2Token); + deployed.exp2Token = exp2Token; } } diff --git a/deploy/FundSigners.s.sol b/deploy/FundSigners.s.sol index 02820e27..42618c6e 100644 --- a/deploy/FundSigners.s.sol +++ b/deploy/FundSigners.s.sol @@ -2,12 +2,14 @@ pragma solidity ^0.8.23; import {Script, console} from "forge-std/Script.sol"; -import {stdToml} from "forge-std/StdToml.sol"; +import {Config} from "forge-std/Config.sol"; // SimpleFunder interface for setting gas wallets interface ISimpleFunder { function setGasWallet(address[] memory wallets, bool isGasWallet) external; function gasWallets(address) external view returns (bool); + function setOrchestrators(address[] memory ocs, bool val) external; + function orchestrators(address) external view returns (bool); } /** @@ -46,9 +48,7 @@ interface ISimpleFunder { * --private-key $PRIVATE_KEY \ * "[84532]" 5 */ -contract FundSigners is Script { - using stdToml for string; - +contract FundSigners is Script, Config { /** * @notice Configuration for funding on a specific chain */ @@ -59,6 +59,7 @@ contract FundSigners is Script { uint256 targetBalance; address simpleFunderAddress; uint256 defaultNumSigners; + address[] supportedOrchestrators; } /** @@ -87,18 +88,18 @@ contract FundSigners is Script { uint256 private totalEthDistributed; uint256 private chainsProcessed; - string internal configContent; string internal configPath = "/deploy/config.toml"; /** * @notice Fund all configured chains with default number of signers */ function run() external { - // Default to common testnets - uint256[] memory chainIds = new uint256[](3); - chainIds[0] = 11155111; // Sepolia - chainIds[1] = 84532; // Base Sepolia - chainIds[2] = 11155420; // Optimism Sepolia + // Load configuration and setup forks + string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); + _loadConfigAndForks(fullConfigPath, false); + + // Get all chain IDs from configuration + uint256[] memory chainIds = config.getChainIds(); uint256 numSigners = 10; // Default execute(chainIds, numSigners); } @@ -108,6 +109,10 @@ contract FundSigners is Script { * @param chainIds Array of chain IDs to fund */ function run(uint256[] memory chainIds) external { + // Load configuration and setup forks + string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); + _loadConfigAndForks(fullConfigPath, false); + uint256 numSigners = 10; // Default execute(chainIds, numSigners); } @@ -118,6 +123,10 @@ contract FundSigners is Script { * @param numSigners Number of signers to fund (starting from index 0) */ function run(uint256[] memory chainIds, uint256 numSigners) external { + // Load configuration and setup forks + string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); + _loadConfigAndForks(fullConfigPath, false); + execute(chainIds, numSigners); } @@ -128,9 +137,6 @@ contract FundSigners is Script { console.log("=== Signer Funding Script (TOML Config) ==="); console.log("Number of signers to fund:", numSigners); - // Load configuration - loadConfig(); - // Get mnemonic from environment string memory mnemonic = vm.envString("GAS_SIGNER_MNEMONIC"); require(bytes(mnemonic).length > 0, "GAS_SIGNER_MNEMONIC not set"); @@ -211,6 +217,9 @@ contract FundSigners is Script { if (config.simpleFunderAddress != address(0) && config.simpleFunderAddress.code.length > 0) { setGasWalletsInSimpleFunder(config.simpleFunderAddress, signers); + setOrchestratorsInSimpleFunder( + config.simpleFunderAddress, config.supportedOrchestrators + ); } vm.stopBroadcast(); @@ -412,6 +421,41 @@ contract FundSigners is Script { ); } + /** + * @notice Set orchestrators in SimpleFunder contract + */ + function setOrchestratorsInSimpleFunder(address simpleFunder, address[] memory orchestrators) + internal + { + if (orchestrators.length == 0) { + console.log("No orchestrators configured, skipping orchestrator setup"); + return; + } + + ISimpleFunder funder = ISimpleFunder(simpleFunder); + + console.log("\nSetting orchestrators in SimpleFunder:"); + console.log(" SimpleFunder address:", simpleFunder); + console.log( + string.concat(" Setting ", vm.toString(orchestrators.length), " orchestrators...") + ); + + for (uint256 i = 0; i < orchestrators.length; i++) { + console.log( + string.concat( + " Orchestrator ", vm.toString(i), ": ", vm.toString(orchestrators[i]) + ) + ); + } + + funder.setOrchestrators(orchestrators, true); + console.log( + string.concat( + " Successfully set all ", vm.toString(orchestrators.length), " orchestrators" + ) + ); + } + /** * @notice Derive signer addresses from mnemonic */ @@ -431,45 +475,29 @@ contract FundSigners is Script { return signers; } - /** - * @notice Load configuration from TOML - */ - function loadConfig() internal { - string memory fullConfigPath = string.concat(vm.projectRoot(), configPath); - configContent = vm.readFile(fullConfigPath); - } - /** * @notice Get chain funding configuration */ function getChainFundingConfig(uint256 chainId) internal returns (ChainFundingConfig memory) { - // Create fork and read configuration from fork variables - string memory rpcUrl = vm.envString(string.concat("RPC_", vm.toString(chainId))); - uint256 forkId = vm.createFork(rpcUrl); - vm.selectFork(forkId); - - ChainFundingConfig memory config; - config.chainId = chainId; - config.name = vm.readForkString("name"); - config.isTestnet = vm.readForkBool("is_testnet"); - config.targetBalance = vm.readForkUint("target_balance"); - - // Try to read SimpleFunder address - may not be deployed on all chains - try vm.readForkAddress("simple_funder_address") returns (address addr) { - config.simpleFunderAddress = addr; - } catch { - // SimpleFunder not configured for this chain - config.simpleFunderAddress = address(0); - } + // Switch to the fork for this chain (already created by _loadConfigAndForks) + vm.selectFork(forkOf[chainId]); + + ChainFundingConfig memory chainConfig; + chainConfig.chainId = chainId; + chainConfig.name = config.get(chainId, "name").toString(); + chainConfig.isTestnet = config.get(chainId, "is_testnet").toBool(); + chainConfig.targetBalance = config.get(chainId, "target_balance").toUint256(); + chainConfig.simpleFunderAddress = config.get(chainId, "simple_funder_deployed").toAddress(); // Try to read default number of signers - try vm.readForkUint("default_num_signers") returns (uint256 num) { - config.defaultNumSigners = num; - } catch { - config.defaultNumSigners = 10; // Default fallback - } + uint256 numSigners = config.get(chainId, "default_num_signers").toUint256(); + chainConfig.defaultNumSigners = numSigners == 0 ? 10 : numSigners; // Default fallback + + // Read supported orchestrators - required field + chainConfig.supportedOrchestrators = + config.get(chainId, "supported_orchestrators").toAddressArray(); - return config; + return chainConfig; } /** diff --git a/deploy/README.md b/deploy/README.md index cb27d1ef..b23c7606 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -1,6 +1,6 @@ # Deployment System -Unified deployment and configuration system for the Ithaca Account Abstraction System. +Unified deployment and configuration system for the Ithaca Account Abstraction System. We use a single TOML config for fast and easy scripting. ## Available Scripts @@ -55,69 +55,90 @@ optimism-sepolia = { key = "${ETHERSCAN_API_KEY}" } ## Configuration Structure -All configuration is in `deploy/config.toml`: +All configuration is in `deploy/config.toml` using the StdConfig format: ```toml -[profile.deployment] -registry_path = "deploy/registry/" +[base-sepolia] +endpoint_url = "${RPC_84532}" -[forks.base-sepolia] -rpc_url = "${RPC_84532}" +[base-sepolia.bool] +is_testnet = true -[forks.base-sepolia.vars] +[base-sepolia.address] # Chain identification +funder_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" +funder_signer = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" +settler_owner = "0x0000000000000000000000000000000000000004" +l0_settler_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" +l0_settler_signer = "0x0000000000000000000000000000000000000006" +layerzero_endpoint = "0x6EDCE65403992e310A62460808c4b910D972f10f" +simple_funder_address = "0x09F6eF9525efAdb6167dFe71fFcfbE306De07988" +layerzero_settler_address = "0xd71d3c3ff2249A67cEa12030b20E66734fB1f833" +layerzero_send_uln302 = "0xC1868e054425D378095A003EcbA3823a5D0135C9" +layerzero_receive_uln302 = "0x12523de19dc41c91F7d2093E0CFbB76b17012C8d" +dvn_layerzero_labs = "0xe1a12515F9AB2764b887bF60B923Ca494EBbB2d6" +dvn_google_cloud = "0xFc9d8E5d3FaB22fB6E93E9E2C90916E9dCa83Ade" +exp_minter_address = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" +supported_orchestrators = ["0xEd7c1e839381c489Dcd1ED3CE1B0e79DaE714f77"] + +# Deployed contract addresses - automatically written during deployment +orchestrator_deployed = "0xC662Af195CD57bC330552f3E2Be5E03Ef69cB041" +ithaca_account_deployed = "0x49627C39cC7f39f95540C2100f18608f2365a59f" +account_proxy_deployed = "0xD2a48e4635fCB2437d2e482122137F06C8433706" +simulator_deployed = "0x332A5Cd675D9d26c4af3BF618A7175d0D623CABA" +simple_funder_deployed = "0x1ADE5D4CE3183D913791DEcaeaD42Fff193AeF8F" +escrow_deployed = "0xD1c7e21f2a50A2cDDCFaf554b998a800C3C35aD1" +simple_settler_deployed = "0x3291f7Ce832997920874d70d68A8186B388024F5" +layerzero_settler_deployed = "0xBDb45dA9e075a9fCbdf8fAa9d0c93A21b3D8671a" +exp_token_deployed = "0xaeB83430528fB0DeE5E15bF07A5056B6c0b37809" +exp2_token_deployed = "0x246c631Dac318a13023e98aB925499930c9801fB" + +[base-sepolia.uint] chain_id = 84532 -name = "Base Sepolia" -is_testnet = true - -# Contract ownership -pause_authority = "0x..." # Can pause contracts -funder_owner = "0x..." # Owns SimpleFunder -funder_signer = "0x..." # Signs funding operations -settler_owner = "0x..." # Owns SimpleSettler -l0_settler_owner = "0x..." # Owns LayerZeroSettler - -# Deployment configuration -salt = "0x0000..." # CREATE2 salt (SAVE THIS!) -contracts = ["ALL"] # Or specific: ["Orchestrator", "IthacaAccount"] - -# Funding configuration (only needed for Funding Script) -target_balance = 1000000000000000 # Target balance per signer (0.001 ETH) -simple_funder_address = "0x..." # SimpleFunder address -default_num_signers = 10 # Number of signers to fund - -# LayerZero configuration (only needed for ConfigureLayerZero) -layerzero_settler_address = "0x..." -layerzero_endpoint = "0x..." layerzero_eid = 40245 -layerzero_send_uln302 = "0x..." -layerzero_receive_uln302 = "0x..." -layerzero_destination_chain_ids = [11155420] -layerzero_required_dvns = ["dvn_layerzero_labs"] -layerzero_optional_dvns = [] -layerzero_optional_dvn_threshold = 0 +target_balance = "1000000000000000" # Target balance per signer (0.001 ETH) - must be quoted for large numbers +default_num_signers = 10 # Number of signers to fund layerzero_confirmations = 1 layerzero_max_message_size = 10000 +layerzero_optional_dvn_threshold = 0 +exp_mint_amount = "5000000000000000000000" # Amount to mint (in wei) - must be quoted +layerzero_destination_chain_ids = [11155420] + +[base-sepolia.bytes32] +salt = "0x0000000000000000000000000000000000000000000000000000000000005678" # CREATE2 salt (SAVE THIS!) -dvn_layerzero_labs = "0x..." -dvn_google_cloud = "0x..." +[base-sepolia.string] +name = "Base Sepolia" +contracts = ["ALL"] # Or specific: ["Orchestrator", "IthacaAccount"] +layerzero_required_dvns = ["dvn_layerzero_labs"] +layerzero_optional_dvns = [] ``` +### Deployed Address Management + +**Automatic Address Writing**: The deployment scripts automatically write deployed contract addresses to the config file during broadcast operations: + +- ✅ **During `--broadcast`**: Deployed addresses are written to config.toml as `contract_name_deployed = "0x..."` +- ❌ **During simulation** (no `--broadcast`): No config writes occur - safe for testing +- 📍 **Address detection**: Scripts always check actual on-chain state, not config file data +- 🔄 **State synchronization**: Config file stays in sync with actual deployments + ### Available Contracts - **Orchestrator** -- **IthacaAccount** -- **AccountProxy** -- **Simulator** -- **SimpleFunder** +- **IthacaAccount** +- **AccountProxy** +- **Simulator** +- **SimpleFunder** - **Escrow** (Only needed for Interop Chains) - **SimpleSettler** (Only needed for Interop testing) - **LayerZeroSettler** (Only needed for Interop Chains) -- **ALL** - Deploys all contracts +- **ExpToken** - Test ERC20 tokens (Testnet only, automatically included with "ALL") +- **ALL** - Deploys all contracts (+ ExpToken on testnets) -**Dependencies**: -IthacaAccount requires Orchestrator; -AccountProxy requires IthacaAccount; +**Dependencies**: +IthacaAccount requires Orchestrator; +AccountProxy requires IthacaAccount; SimpleFunder requires Orchestrator. ## Quick Start - Complete Workflow @@ -149,8 +170,7 @@ forge script deploy/FundSigners.s.sol:FundSigners \ --private-key $PRIVATE_KEY \ "[84532,11155420]" -# 5. Fund SimpleFunder contract -SIMPLE_FUNDER=$(cat deploy/registry/deployment_84532_*.json | jq -r .SimpleFunder) +# 5. Fund SimpleFunder contract forge script deploy/FundSimpleFunder.s.sol:FundSimpleFunder \ --broadcast --multi --slow \ @@ -174,7 +194,7 @@ forge script deploy/FundSimpleFunder.s.sol:FundSimpleFunder \ forge script deploy/DeployMain.s.sol:DeployMain \ --broadcast --verify --multi --slow \ --sig "run()" \ - --private-key $PRIVATE_KEY + --private-key $PRIVATE_KEY # Deploy to specific chains forge script deploy/DeployMain.s.sol:DeployMain \ @@ -207,7 +227,8 @@ forge script deploy/DeployMain.s.sol:DeployMain \ **Purpose**: Configure LayerZero messaging pathways between chains. -**Prerequisites**: +**Prerequisites**: + - LayerZeroSettler deployed on source and destination chains - Caller must be l0_settler_owner @@ -230,15 +251,18 @@ forge script deploy/ConfigureLayerZeroSettler.s.sol:ConfigureLayerZeroSettler \ **Purpose**: Fund signers and register them as gas wallets in SimpleFunder. -**Prerequisites**: +**Prerequisites**: + - SimpleFunder deployed - Caller must be funder_owner - GAS_SIGNER_MNEMONIC environment variable set **What it does**: + 1. Derives signer addresses from mnemonic 2. Tops up signers below target_balance 3. Registers signers as gas wallets in SimpleFunder +4. Sets configured orchestrators in SimpleFunder ```bash # Fund default number of signers (from config) @@ -281,6 +305,7 @@ forge script deploy/FundSimpleFunder.s.sol:FundSimpleFunder \ ``` **Parameters**: + - SimpleFunder address (same across chains if using CREATE2) - Array of (chainId, tokenAddress, amount) - Use `0x0000000000000000000000000000000000000000` for native ETH @@ -297,37 +322,36 @@ forge script deploy/FundSimpleFunder.s.sol:FundSimpleFunder \ All contracts deploy via Safe Singleton Factory (`0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7`) for deterministic addresses. **Key Points**: + - Same salt + same bytecode = same address on every chain - Addresses can be predicted before deployment - **⚠️ SAVE YOUR SALT VALUES** - Required for deploying to same addresses on new chains - -## Registry Files - -Deployment addresses are saved in `deploy/registry/deployment_{chainId}_{salt}.json`: - -```json -{ - "Orchestrator": "0xb33adF2c2257a94314d408255aC843fd53B1a7e1", - "IthacaAccount": "0x5a87ef243CDA70a855828d4989Fad61B56A467d3", - "AccountProxy": "0x4ACD713815fbb363a89D9Ff046C56cEdC7EF3ad7", - "SimpleFunder": "0xA47C5C472449979a2F37dF2971627cD6587bADb8" -} -``` - -Registry files are for reference only - deployment decisions are based on on-chain state. +- **Deployment decisions based on on-chain state** - Scripts check actual deployed contracts, not config file data ## Adding New Chains 1. Add configuration to `deploy/config.toml`: ```toml -[forks.new-chain] -rpc_url = "${RPC_CHAINID}" +[new-chain] +endpoint_url = "${RPC_CHAINID}" + +[new-chain.bool] +is_testnet = true + +[new-chain.address] +funder_owner = "0x..." +# ... all required fields -[forks.new-chain.vars] +[new-chain.uint] chain_id = CHAINID -name = "Chain Name" # ... all required fields + +[new-chain.bytes32] +salt = "0x0000000000000000000000000000000000000000000000000000000000005678" + +[new-chain.string] +name = "Chain Name" contracts = ["ALL"] ``` @@ -352,18 +376,22 @@ forge script deploy/DeployMain.s.sol:DeployMain \ ### Common Issues **"No chains found in configuration"** + - Verify config.toml has properly configured chains - Check RPC URLs are set for target chains **"Safe Singleton Factory not deployed"** + - Factory must exist at `0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7` - Most major chains have this deployed **Contract already deployed** + - Normal for CREATE2 - existing contracts are skipped - Change salt value to deploy to new addresses **RPC errors** + - Verify RPC URLs are correct and accessible - Check rate limits on public RPCs - Consider paid RPC services for production @@ -376,38 +404,145 @@ forge script deploy/DeployMain.s.sol:DeployMain \ 4. **Commit registry files** - Provides deployment history 5. **Use `--multi --slow`** - Ensures proper multi-chain ordering 6. **Verify while deploying** - Use `--verify` flag +7. **Large numbers must be quoted in TOML** - Use `"1000000000000000000"` not `1000000000000000000` ## Configuration Field Reference -| Field | Used By | Purpose | -|-------|---------|---------| -| `chain_id`, `name`, `is_testnet` | All scripts | Chain identification | -| `pause_authority` | DeployMain | Contract pause permissions | -| `funder_owner`, `funder_signer` | DeployMain, FundSigners | SimpleFunder control | -| `settler_owner` | DeployMain | SimpleSettler ownership | -| `l0_settler_owner` | DeployMain, ConfigureLayerZero | LayerZeroSettler ownership | -| `salt` | DeployMain | CREATE2 deployment salt | -| `contracts` | DeployMain | Which contracts to deploy | -| `target_balance` | FundSigners | Minimum signer balance | -| `simple_funder_address` | FundSigners, FundSimpleFunder | SimpleFunder location | -| `default_num_signers` | FundSigners | Number of signers | -| `layerzero_*` fields | ConfigureLayerZeroSettler | LayerZero configuration | +| Field | Used By | Purpose | +| --------------------------------------- | ------------------------------ | ------------------------------------------------ | +| `chain_id`, `name`, `is_testnet` | All scripts | Chain identification | +| `pause_authority` | DeployMain | Contract pause permissions | +| `funder_owner`, `funder_signer` | DeployMain, FundSigners | SimpleFunder control | +| `settler_owner` | DeployMain | SimpleSettler ownership | +| `l0_settler_owner` | DeployMain, ConfigureLayerZero | LayerZeroSettler ownership | +| `salt` | DeployMain | CREATE2 deployment salt | +| `contracts` | DeployMain | Which contracts to deploy | +| `target_balance` | FundSigners | Minimum signer balance | +| `simple_funder_address` | FundSigners, FundSimpleFunder | SimpleFunder location | +| `default_num_signers` | FundSigners | Number of signers | +| `supported_orchestrators` | FundSigners | Orchestrator addresses to enable in SimpleFunder | +| `layerzero_*` fields | ConfigureLayerZeroSettler | LayerZero configuration | +| `exp_minter_address`, `exp_mint_amount` | DeployMain (testnet) | ExpToken deployment | +| `*_deployed` fields | All scripts | Auto-written deployed contract addresses | + +## ExpToken Deployment (Testnets Only) + +### Automatic ExpToken Deployment -## Test Token Deployment +**Purpose**: Deploy EXP and EXP2 test tokens automatically on testnet chains. -### DeployEXP - Test ERC20 Token +**Behavior**: -**Purpose**: Deploy a simple test ERC20 token for testing. +- **Testnets** (`is_testnet = true`): ExpToken automatically included when using `["ALL"]` contracts +- **Production** (`is_testnet = false`): ExpToken never deployed, regardless of configuration +- **Two tokens deployed**: "EXP" and "EXP2" with hardcoded names +- **Same configuration**: Both tokens use the same minter address and mint amount + +### Configuration Requirements + +For testnet chains, **both fields are required** (deployment will fail if missing): + +```toml +[testnet-name.bool] +is_testnet = true + +[testnet-name.address] +exp_minter_address = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" # REQUIRED + +[testnet-name.uint] +exp_mint_amount = "5000000000000000000000" # REQUIRED (5000 tokens in wei) + +[testnet-name.string] +contracts = ["ALL"] # ExpToken automatically included for testnets +``` + +### Deployment Details + +**Two tokens are deployed**: + +1. **EXP Token**: Name and symbol "EXP" +2. **EXP2 Token**: Name and symbol "EXP2" + +**Both tokens**: + +- Use CREATE2 for deterministic addresses +- Mint `exp_mint_amount` tokens to `exp_minter_address` +- Are deployed at the end of the contract deployment sequence +- Are saved to registry files as "ExpToken" and "Exp2Token" + +### Examples + +**Base Sepolia (Testnet)**: + +```toml +[base-sepolia.bool] +is_testnet = true + +[base-sepolia.address] +exp_minter_address = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" + +[base-sepolia.uint] +exp_mint_amount = "5000000000000000000000" + +[base-sepolia.string] +contracts = ["ALL"] # Deploys 8 core contracts + ExpToken +``` + +## Supporting Bash Scripts + +### Overview + +There are two main scripts that handle multi-chain deployments and configuration verification: + +- **`deploy/execute_config.sh`** - Brings up the whole environment, by calling all scripts correctly. +- **`deploy/verify_config.sh`** - Verifies that the values in the config.toml are all set and configured correctly. + +### Usage + +#### Execute Deployment -**Usage**: ```bash -# Deploy test token to a specific chain -forge script deploy/DeployEXP.s.sol:DeployEXP \ - --broadcast \ - --sig "run()" \ - --private-key $PRIVATE_KEY \ - --rpc-url $RPC_ +# Deploy to specific chains +./deploy/execute_config.sh 84532 11155420 + +# Deploy to all chains in config.toml +./deploy/execute_config.sh +``` + +The script performs these steps: +1. Validates configuration for selected chains +2. Deploys core contracts (IthacaAccount, SimpleFunder, SimpleSettler, etc.) +3. Configures LayerZero cross-chain messaging +4. Funds signer accounts with gas tokens +5. Verifies all deployments match configuration + +#### Verify Configuration + +```bash +# Verify specific chains +./deploy/verify_config.sh 84532 11155420 + +# Verify all chains in config.toml +./deploy/verify_config.sh ``` -The test token includes basic ERC20 functionality and can be minted by anyone. -It should only be used for testing purposes. \ No newline at end of file +The script checks: +- Required environment variables (RPC URLs, private keys) +- Contract deployment addresses match config.toml +- Signer accounts have sufficient gas balances +- LayerZero endpoints and DVN configurations +- Cross-chain pathway configurations + +### Configuration + +Both scripts read from `deploy/config.toml` which defines per-chain: +- RPC endpoints and chain metadata +- Contract addresses and owners +- LayerZero endpoint configurations +- Cross-chain destination mappings +- Gas funding amounts + +Environment variables required: +- `PRIVATE_KEY` - Deployer private key +- `GAS_SIGNER_MNEMONIC` - Mnemonic for signer accounts +- `RPC_` - RPC URL for each chain (e.g., `RPC_84532`) \ No newline at end of file diff --git a/deploy/config.toml b/deploy/config.toml index 0644abd6..32d4fd67 100644 --- a/deploy/config.toml +++ b/deploy/config.toml @@ -1,136 +1,155 @@ -# Ithaca Account Deployment Configuration -# Single source of truth for all deployment and fork configurations -# This file extends foundry.toml and contains both Foundry fork configs and deployment-specific settings +[11155111] +endpoint_url = "${RPC_11155111}" -# ============================================ -# GLOBAL DEPLOYMENT CONFIGURATION -# ============================================ - -[profile.deployment] -registry_path = "deploy/registry/" - -# ============================================ -# FORK CONFIGURATIONS (for vm.readFork* cheatcodes) -# ============================================ - -# Sepolia -[forks.sepolia] -rpc_url = "${RPC_11155111}" - -[forks.sepolia.vars] -chain_id = 11155111 -name = "Sepolia" +[11155111.bool] is_testnet = true -pause_authority = "0x0000000000000000000000000000000000000001" + +[11155111.address] funder_owner = "0x0000000000000000000000000000000000000003" funder_signer = "0x0000000000000000000000000000000000000002" settler_owner = "0x0000000000000000000000000000000000000004" l0_settler_owner = "0x0000000000000000000000000000000000000005" +l0_settler_signer = "0x0000000000000000000000000000000000000006" layerzero_endpoint = "0x6EDCE65403992e310A62460808c4b910D972f10f" -layerzero_eid = 40161 -salt = "0x0000000000000000000000000000000000000000000000000000000000000000" -target_balance = 50000000000000000 # 0.05 ether -# Specify contracts to deploy. Use ["ALL"] to deploy all available contracts -contracts = ["LayerZeroSettler"] -# SimpleFunder configuration -simple_funder_address = "0x2AD8F6a3bB1126a777606eaFa9da9b95530d9597" # TODO: automate write when foundry upgrades -default_num_signers = 10 -# LayerZero configuration (copied from Base Sepolia for testing) -# TODO: Set correct addresses here. layerzero_settler_address = "0x4225041FF3DB1C7d7a1029406bB80C7298767aca" layerzero_send_uln302 = "0xC1868e054425D378095A003EcbA3823a5D0135C9" layerzero_receive_uln302 = "0x12523de19dc41c91F7d2093E0CFbB76b17012C8d" -layerzero_destination_chain_ids = [84532, 11155420] -layerzero_required_dvns = ["dvn_layerzero_labs"] # optimism sepolia -layerzero_optional_dvns = [] -layerzero_optional_dvn_threshold = 0 -layerzero_confirmations = 1 -layerzero_max_message_size = 10000 -# DVN addresses dvn_layerzero_labs = "0x8eebf8b423B73bFCa51a1Db4B7354AA0bFCA9193" dvn_google_cloud = "0x8e5a7b5959C5A7C732b87dCE401E07F5819eEC2d" +exp_minter_address = "0x0000000000000000000000000000000000000003" +supported_orchestrators = ["0xEd7c1e839381c489Dcd1ED3CE1B0e79DaE714f77"] +orchestrator_deployed = "0xCa58a1296c5AE231A2963A26E11f722bfe65acd4" +ithaca_account_deployed = "0xC203A827A6DB2d09fbF052f88e7aD794EEbf8928" +account_proxy_deployed = "0x0B5d691F3A98B705756ab196CFe4af97f35e06E4" +simulator_deployed = "0x3306F2DD87887885f4de81aF6BD88b648E467f4e" +simple_funder_deployed = "0xa0BF9a30fD55A45BF09309513734B8194fBE3934" +escrow_deployed = "0x669a3aB5816d1ADdbFdecE2F6B238D323F056248" +simple_settler_deployed = "0xFB0F29c658145DbDb1Bf974C8A342F4E78BF9801" +layerzero_settler_deployed = "0xC93D0142D960F7B33440F09B13917C195e09ffA0" +exp_token_deployed = "0x4D335fa317FB8d5493b83e06161134ec611Bc188" +exp2_token_deployed = "0x0348152303686f2197b24981571FCf8c36c66Cc5" + +[11155111.uint] +chain_id = 11155111 +layerzero_eid = 40161 +target_balance = "50000000000000000" +default_num_signers = 10 +layerzero_confirmations = 1 +layerzero_max_message_size = 10000 +layerzero_optional_dvn_threshold = 0 +exp_mint_amount = "1000000000000000000000" +layerzero_destination_chain_ids = [ + 84532, + 11155420, +] -# Base Sepolia -[forks.base-sepolia] -rpc_url = "${RPC_84532}" +[11155111.bytes32] +salt = "0xdf08a9fa957a9ac004f84f3a8ab7318a7be55d59948f46497bd3717865295b61" -[forks.base-sepolia.vars] -orchestrator_address = "0x0000000000000000000000000000000000000000" -# Deployment Script -chain_id = 84532 -name = "Base Sepolia" +[11155111.string] +name = "Sepolia" +contracts = ["ALL"] +layerzero_required_dvns = ["dvn_layerzero_labs"] +layerzero_optional_dvns = [] + +[84532] +endpoint_url = "${RPC_84532}" + +[84532.bool] is_testnet = true -pause_authority = "0x0000000000000000000000000000000000000001" # Set to separate PAUSE KEY -funder_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" # Set to Master Key -funder_signer = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" # Set to Master Key -settler_owner = "0x0000000000000000000000000000000000000004" # Set to Master Key -l0_settler_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" # Set to Master Key -layerzero_endpoint = "0x6EDCE65403992e310A62460808c4b910D972f10f" # Get from LayerZero docs -layerzero_eid = 40245 # Get from LayerZero docs -salt = "0x00000000000000000000000000000000000000000000000000000000000000c7" # Almost alwayse be bytes32(0) -# Specify contracts to deploy. Use ["ALL"] to deploy all available contracts -contracts = ["ALL"] -# Funding Script -target_balance = 1000000000000000 # 0.001 ether - -# SimpleFunder configuration -simple_funder_address = "0x5e6c8c24A53275e2C6C3DC5f1c5C027Ec13439Ba" -default_num_signers = 10 -# LayerZero configuration -layerzero_settler_address = "0x4225041FF3DB1C7d7a1029406bB80C7298767aca" + +[84532.address] +funder_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" +funder_signer = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" +settler_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" +l0_settler_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" +l0_settler_signer = "0x0000000000000000000000000000000000000006" +layerzero_endpoint = "0x6EDCE65403992e310A62460808c4b910D972f10f" +layerzero_settler_address = "0xd71d3c3ff2249A67cEa12030b20E66734fB1f833" layerzero_send_uln302 = "0xC1868e054425D378095A003EcbA3823a5D0135C9" layerzero_receive_uln302 = "0x12523de19dc41c91F7d2093E0CFbB76b17012C8d" -layerzero_destination_chain_ids = [11155420] # optimism sepolia -layerzero_required_dvns = ["dvn_layerzero_labs"] -layerzero_optional_dvns = [] -layerzero_optional_dvn_threshold = 0 -layerzero_confirmations = 1 -layerzero_max_message_size = 10000 -# DVN addresses dvn_layerzero_labs = "0xe1a12515F9AB2764b887bF60B923Ca494EBbB2d6" dvn_google_cloud = "0xFc9d8E5d3FaB22fB6E93E9E2C90916E9dCa83Ade" +exp_minter_address = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" +supported_orchestrators = ["0xEd7c1e839381c489Dcd1ED3CE1B0e79DaE714f77"] +orchestrator_deployed = "0xcA91Dba3dab46478FC466ef76491AaB78dbC2d0f" +ithaca_account_deployed = "0x1E7a350c76CCd750470930B5026fEa26F44cf937" +account_proxy_deployed = "0xDe938C3642205782d52535d12eC9dFb4D1bC832b" +simulator_deployed = "0xb100Dc6e1BA9e54965fBB865F65Ac42b28BC25E8" +simple_funder_deployed = "0x57C3563C06CeDda15F7622A83a1BdBa84125351D" +escrow_deployed = "0xCd075ceb5Cd463a9233a8085fc915767139F655c" +simple_settler_deployed = "0x5aE547Dc236c31d30f19a5B9040992728Ac441A0" +layerzero_settler_deployed = "0x23D18C5b5Df22fDcf6dDE73b0f1a76ee5761d206" +exp_token_deployed = "0xa8071DA5e994cB8e3eB56CaD0FBB6ca424dD8dc0" +exp2_token_deployed = "0x61727778216127D0843A99A3e91e99C27e9f3BC7" -# Optimism Sepolia -[forks.optimism-sepolia] -rpc_url = "${RPC_11155420}" +[84532.uint] +chain_id = 84532 +layerzero_eid = 40245 +target_balance = "1000000000000000" +default_num_signers = 10 +layerzero_confirmations = 1 +layerzero_max_message_size = 10000 +layerzero_optional_dvn_threshold = 0 +exp_mint_amount = "5000000000000000000000" +layerzero_destination_chain_ids = [11155420] -[forks.optimism-sepolia.vars] -chain_id = 11155420 -name = "Optimism Sepolia" +[84532.bytes32] +salt = "0xdf08a9fa957a9ac004f84f3a8ab7318a7be55d59948f46497bd3717865295b61" + +[84532.string] +name = "Base Sepolia" +contracts = ["ALL"] +layerzero_required_dvns = ["dvn_layerzero_labs"] +layerzero_optional_dvns = [] + +[11155420] +endpoint_url = "${RPC_11155420}" + +[11155420.bool] is_testnet = true -pause_authority = "0x0000000000000000000000000000000000000001" -funder_owner = "0x0000000000000000000000000000000000000003" -funder_signer = "0x0000000000000000000000000000000000000002" -settler_owner = "0x0000000000000000000000000000000000000004" + +[11155420.address] +funder_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" +funder_signer = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" +settler_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" l0_settler_owner = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" +l0_settler_signer = "0x0000000000000000000000000000000000000006" layerzero_endpoint = "0x6EDCE65403992e310A62460808c4b910D972f10f" -layerzero_eid = 40232 -salt = "0x0000000000000000000000000000000000000000000000000000000000000001" -target_balance = 10000000000000000 # 0.01 ether -# Specify contracts to deploy. Use ["ALL"] to deploy all available contracts -contracts = ["ALL"] # Deploy all contracts -# SimpleFunder configuration -simple_funder_address = "0x2AD8F6a3bB1126a777606eaFa9da9b95530d9597" - -default_num_signers = 10 -# LayerZero configuration -layerzero_settler_address = "0x4225041FF3DB1C7d7a1029406bB80C7298767aca" +layerzero_settler_address = "0x9F610182BD096ab7734C1365A44f48dD0A574d0D" layerzero_send_uln302 = "0xB31D2cb502E25B30C651842C7C3293c51Fe6d16f" layerzero_receive_uln302 = "0x9284fd59B95b9143AF0b9795CAC16eb3C723C9Ca" -layerzero_destination_chain_ids = [84532, 11155111] -layerzero_required_dvns = ["dvn_layerzero_labs"] -layerzero_optional_dvns = [] -layerzero_optional_dvn_threshold = 0 -layerzero_confirmations = 1 -layerzero_max_message_size = 10000 -# DVN addresses dvn_layerzero_labs = "0x28B6140ead70cb2Fb669705b3598ffB4BEaA060b" dvn_google_cloud = "0x28b8B8Ea5C695EFa657B6b86a4a8E90ccbA93E9e" +exp_minter_address = "0xB6918DaaB07e31556B45d7Fd2a33021Bc829adf4" +supported_orchestrators = ["0xEd7c1e839381c489Dcd1ED3CE1B0e79DaE714f77"] +orchestrator_deployed = "0xcA91Dba3dab46478FC466ef76491AaB78dbC2d0f" +ithaca_account_deployed = "0x1E7a350c76CCd750470930B5026fEa26F44cf937" +account_proxy_deployed = "0xDe938C3642205782d52535d12eC9dFb4D1bC832b" +simulator_deployed = "0xb100Dc6e1BA9e54965fBB865F65Ac42b28BC25E8" +simple_funder_deployed = "0x57C3563C06CeDda15F7622A83a1BdBa84125351D" +escrow_deployed = "0xCd075ceb5Cd463a9233a8085fc915767139F655c" +simple_settler_deployed = "0x5aE547Dc236c31d30f19a5B9040992728Ac441A0" +layerzero_settler_deployed = "0x23D18C5b5Df22fDcf6dDE73b0f1a76ee5761d206" +exp_token_deployed = "0xa8071DA5e994cB8e3eB56CaD0FBB6ca424dD8dc0" +exp2_token_deployed = "0x61727778216127D0843A99A3e91e99C27e9f3BC7" +[11155420.uint] +chain_id = 11155420 +layerzero_eid = 40232 +target_balance = "1000000000000000" +default_num_signers = 10 +layerzero_confirmations = 1 +layerzero_max_message_size = 10000 +layerzero_optional_dvn_threshold = 0 +exp_mint_amount = "2000000000000000000000" +layerzero_destination_chain_ids = [84532] +[11155420.bytes32] +salt = "0xdf08a9fa957a9ac004f84f3a8ab7318a7be55d59948f46497bd3717865295b61" -# Master Key has gas funds on all chains. -# 1. Deployment Key ( Master Key ) -# 2. Funder Owner Key ( Master Key ) -# 3. Gas Signers ( Funding is included in the fund signers script + setting them as gas wallets ) -# 4. Simple Funder Address ( Funds need to come from GK after bridging, no automation ) +[11155420.string] +name = "Optimism Sepolia" +contracts = ["ALL"] +layerzero_required_dvns = ["dvn_layerzero_labs"] +layerzero_optional_dvns = [] diff --git a/deploy/registry/deployment_11155420_0x0000000000000000000000000000000000000000000000000000000000000001.json b/deploy/registry/deployment_11155420_0x0000000000000000000000000000000000000000000000000000000000000001.json deleted file mode 100644 index 23cb5e8b..00000000 --- a/deploy/registry/deployment_11155420_0x0000000000000000000000000000000000000000000000000000000000000001.json +++ /dev/null @@ -1 +0,0 @@ -{"Orchestrator": "0xb33adF2c2257a94314d408255aC843fd53B1a7e1","IthacaAccount": "0x5a87ef243CDA70a855828d4989Fad61B56A467d3","AccountProxy": "0x4ACD713815fbb363a89D9Ff046C56cEdC7EF3ad7","Simulator": "0x65Ae218EB1987b8bd0F9eeb38D1B344726D41dA5","SimpleFunder": "0xA47C5C472449979a2F37dF2971627cD6587bADb8","Escrow": "0x24F50280cE3B51Ab1967F048746FB7ba3C7B4067","SimpleSettler": "0xb934afBB50b8aBBe24959f9398fE024BEe9Bf716","LayerZeroSettler": "0xB89f4A85d38C3A2407854269527fabD3b61fd56a"} \ No newline at end of file diff --git a/deploy/registry/deployment_84532_0x0000000000000000000000000000000000000000000000000000000000000001.json b/deploy/registry/deployment_84532_0x0000000000000000000000000000000000000000000000000000000000000001.json deleted file mode 100644 index c37d42b4..00000000 --- a/deploy/registry/deployment_84532_0x0000000000000000000000000000000000000000000000000000000000000001.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "Orchestrator": "0xb33adF2c2257a94314d408255aC843fd53B1a7e1", - "IthacaAccount": "0x5a87ef243CDA70a855828d4989Fad61B56A467d3", - "AccountProxy": "0x4ACD713815fbb363a89D9Ff046C56cEdC7EF3ad7", - "Simulator": "0x65Ae218EB1987b8bd0F9eeb38D1B344726D41dA5", - "SimpleFunder": "0xA47C5C472449979a2F37dF2971627cD6587bADb8", - "Escrow": "0x24F50280cE3B51Ab1967F048746FB7ba3C7B4067", - "SimpleSettler": "0xb934afBB50b8aBBe24959f9398fE024BEe9Bf716", - "LayerZeroSettler": "0xB89f4A85d38C3A2407854269527fabD3b61fd56a" -} diff --git a/docs/4337CallGraph.md b/docs/4337CallGraph.md new file mode 100644 index 00000000..ec9e788d --- /dev/null +++ b/docs/4337CallGraph.md @@ -0,0 +1,32 @@ +sequenceDiagram + participant User as User + participant Bundler as Bundler + participant EntryPoint as EntryPoint + participant Paymaster as Paymaster + participant Account as ERC4337 Account + + User->>Bundler: Submit signed UserOperation + paymasterAndData + Bundler->>EntryPoint: handleOps([userOp]) + + Note over EntryPoint: Validation Phase + EntryPoint->>Account: validateUserOp(userOp, userOpHash, missingAccountFunds) + Account-->>EntryPoint: validationData + + Note over EntryPoint: Paymaster validation & deposit check + EntryPoint->>Paymaster: validatePaymasterUserOp(userOp, userOpHash, maxCost) + Note over Paymaster: Validate user agreed to pay in USDC + Paymaster-->>EntryPoint: (context, validationData) + + + Note over EntryPoint: Execution Phase + EntryPoint->>Account: execute(userOp.callData) + + Note over EntryPoint: Payment Phase - ETH movement + EntryPoint-->>Bundler: ETH transfer (actualGasCost) + + EntryPoint->>Paymaster: postOp(mode, context, actualGasCost) + Note over Paymaster: Calculate USDC amount needed + Account-->>Paymaster: USDC transfer (calculated amount) + + EntryPoint-->>Bundler: Execution result + Bundler-->>User: UserOp result \ No newline at end of file diff --git a/docs/IthacaCallGraph.md b/docs/IthacaCallGraph.md new file mode 100644 index 00000000..7c35733c --- /dev/null +++ b/docs/IthacaCallGraph.md @@ -0,0 +1,25 @@ +sequenceDiagram + participant User as User + participant Relayer as Relayer + participant Orch as Orchestrator + participant Account as IthacaAccount + + User->>Relayer: Submit signed intent + Relayer->>Orch: execute(encodedIntent) + + Note over Orch: Verify signature + Orch->>Account: unwrapAndValidateSignature(digest, signature) + Account-->>Orch: (isValid, keyHash) + + Note over Orch: Increment nonce + Orch->>Account: checkAndIncrementNonce(nonce) + + Note over Orch: Process payment + Orch->>Account: pay(paymentAmount, keyHash, digest, intent) + Account-->>Relayer: ERC20 transfer (paymentAmount) + + Note over Orch: Execute intent + Orch->>Account: execute(executionData) + + Orch-->>Relayer: Return success/error code + Relayer-->>User: Execution result \ No newline at end of file diff --git a/docs/benchmarks.jpeg b/docs/benchmarks.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..43c3a861ff2d95bd0346c92a384d144e32b149ca GIT binary patch literal 291106 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

bO!3Y`Y|3U%=Ww#mitDQA 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 - - - - - \ No newline at end of file diff --git a/lib/.idea/lib.iml b/lib/.idea/lib.iml deleted file mode 100644 index d6ebd480..00000000 --- a/lib/.idea/lib.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/lib/.idea/misc.xml b/lib/.idea/misc.xml deleted file mode 100644 index 862d09bd..00000000 --- a/lib/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/lib/.idea/modules.xml b/lib/.idea/modules.xml deleted file mode 100644 index b0c4ff69..00000000 --- a/lib/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/lib/.idea/vcs.xml b/lib/.idea/vcs.xml deleted file mode 100644 index d3a8bd59..00000000 --- a/lib/.idea/vcs.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/lib/solady b/lib/solady index 90db92ce..acd959aa 160000 --- a/lib/solady +++ b/lib/solady @@ -1 +1 @@ -Subproject commit 90db92ce173856605d24a554969f2c67cadbc7e9 +Subproject commit acd959aa4bd04720d640bf4e6a5c71037510cc4b diff --git a/snapshots/BenchmarkTest.json b/snapshots/BenchmarkTest.json index dedb663c..82f7d2ae 100644 --- a/snapshots/BenchmarkTest.json +++ b/snapshots/BenchmarkTest.json @@ -1,53 +1,53 @@ { - "testERC20Transfer_AlchemyModularAccount": "159052", - "testERC20Transfer_AlchemyModularAccount_AppSponsor": "134278", - "testERC20Transfer_AlchemyModularAccount_ERC20SelfPay": "160169", - "testERC20Transfer_Batch100_AlchemyModularAccount_AppSponsor": "7053863", - "testERC20Transfer_Batch100_CoinbaseSmartWallet": "9801043", - "testERC20Transfer_Batch100_CoinbaseSmartWallet_AppSponsor": "6483523", - "testERC20Transfer_Batch100_CoinbaseSmartWallet_ERC20SelfPay": "8487729", - "testERC20Transfer_Batch100_Safe4337": "11165400", - "testERC20Transfer_Batch100_Safe4337_AppSponsor": "7525827", - "testERC20Transfer_Batch100_Safe4337_ERC20SelfPay": "9540939", - "testERC20Transfer_Batch100_ZerodevKernel_AppSponsor": "8956539", - "testERC20Transfer_CoinbaseSmartWallet": "150133", - "testERC20Transfer_CoinbaseSmartWallet_AppSponsor": "126009", - "testERC20Transfer_CoinbaseSmartWallet_ERC20SelfPay": "150241", - "testERC20Transfer_ERC4337MinimalAccount": "148602", - "testERC20Transfer_ERC4337MinimalAccount_AppSponsor": "123885", - "testERC20Transfer_ERC4337MinimalAccount_ERC20SelfPay": "149776", - "testERC20Transfer_IthacaAccount": "91666", - "testERC20Transfer_IthacaAccountWithSpendLimits": "113167", - "testERC20Transfer_IthacaAccount_AppSponsor": "99218", - "testERC20Transfer_IthacaAccount_AppSponsor_ERC20": "104319", - "testERC20Transfer_IthacaAccount_ERC20SelfPay": "91967", - "testERC20Transfer_Safe4337": "163675", - "testERC20Transfer_Safe4337_AppSponsor": "136323", - "testERC20Transfer_Safe4337_ERC20SelfPay": "160610", - "testERC20Transfer_ZerodevKernel": "175447", - "testERC20Transfer_ZerodevKernel_AppSponsor": "150734", - "testERC20Transfer_ZerodevKernel_ERC20SelfPay": "176663", - "testERC20Transfer_batch100_AlchemyModularAccount": "10438358", - "testERC20Transfer_batch100_AlchemyModularAccount_ERC20SelfPay": "9221814", - "testERC20Transfer_batch100_IthacaAccount": "6201928", - "testERC20Transfer_batch100_IthacaAccount_AppSponsor": "6758228", - "testERC20Transfer_batch100_IthacaAccount_AppSponsor_ERC20": "6565428", - "testERC20Transfer_batch100_IthacaAccount_ERC20SelfPay": "6004328", - "testERC20Transfer_batch100_ZerodevKernel": "12332690", - "testERC20Transfer_batch100_ZerodevKernel_ERC20SelfPay": "11134785", - "testNativeTransfer_AlchemyModularAccount": "168453", - "testNativeTransfer_CoinbaseSmartWallet": "159248", - "testNativeTransfer_IthacaAccount": "101064", - "testNativeTransfer_IthacaAccount_AppSponsor": "108635", - "testNativeTransfer_IthacaAccount_ERC20SelfPay": "101365", - "testNativeTransfer_Safe4337": "172763", - "testNativeTransfer_ZerodevKernel": "184855", - "testUniswapV2Swap_AlchemyModularAccount": "210111", - "testUniswapV2Swap_CoinbaseSmartWallet": "201623", - "testUniswapV2Swap_ERC4337MinimalAccount": "199690", - "testUniswapV2Swap_IthacaAccount": "142728", - "testUniswapV2Swap_IthacaAccount_AppSponsor": "150244", - "testUniswapV2Swap_IthacaAccount_ERC20SelfPay": "143029", - "testUniswapV2Swap_Safe4337": "215353", - "testUniswapV2Swap_ZerodevKernel": "226591" + "testERC20Transfer_AlchemyModularAccount": "179494", + "testERC20Transfer_AlchemyModularAccount_AppSponsor": "176436", + "testERC20Transfer_AlchemyModularAccount_ERC20SelfPay": "207779", + "testERC20Transfer_Batch100_AlchemyModularAccount_AppSponsor": "8897167", + "testERC20Transfer_Batch100_CoinbaseSmartWallet": "9952203", + "testERC20Transfer_Batch100_CoinbaseSmartWallet_AppSponsor": "8787327", + "testERC20Transfer_Batch100_CoinbaseSmartWallet_ERC20SelfPay": "11335605", + "testERC20Transfer_Batch100_Safe4337": "11685484", + "testERC20Transfer_Batch100_Safe4337_AppSponsor": "10198375", + "testERC20Transfer_Batch100_Safe4337_ERC20SelfPay": "12757523", + "testERC20Transfer_Batch100_ZerodevKernel_AppSponsor": "11427583", + "testERC20Transfer_CoinbaseSmartWallet": "177855", + "testERC20Transfer_CoinbaseSmartWallet_AppSponsor": "175259", + "testERC20Transfer_CoinbaseSmartWallet_ERC20SelfPay": "204919", + "testERC20Transfer_ERC4337MinimalAccount": "171509", + "testERC20Transfer_ERC4337MinimalAccount_AppSponsor": "168500", + "testERC20Transfer_ERC4337MinimalAccount_ERC20SelfPay": "199831", + "testERC20Transfer_IthacaAccount": "128195", + "testERC20Transfer_IthacaAccountWithSpendLimits": "193658", + "testERC20Transfer_IthacaAccount_AppSponsor": "138709", + "testERC20Transfer_IthacaAccount_AppSponsor_ERC20": "144010", + "testERC20Transfer_IthacaAccount_ERC20SelfPay": "128684", + "testERC20Transfer_Safe4337": "197561", + "testERC20Transfer_Safe4337_AppSponsor": "191725", + "testERC20Transfer_Safe4337_ERC20SelfPay": "221464", + "testERC20Transfer_ZerodevKernel": "207117", + "testERC20Transfer_ZerodevKernel_AppSponsor": "204120", + "testERC20Transfer_ZerodevKernel_ERC20SelfPay": "235489", + "testERC20Transfer_batch100_AlchemyModularAccount": "10109066", + "testERC20Transfer_batch100_AlchemyModularAccount_ERC20SelfPay": "11609298", + "testERC20Transfer_batch100_IthacaAccount": "7545392", + "testERC20Transfer_batch100_IthacaAccount_AppSponsor": "8151496", + "testERC20Transfer_batch100_IthacaAccount_AppSponsor_ERC20": "7977508", + "testERC20Transfer_batch100_IthacaAccount_ERC20SelfPay": "7366652", + "testERC20Transfer_batch100_ZerodevKernel": "12631318", + "testERC20Transfer_batch100_ZerodevKernel_ERC20SelfPay": "14149937", + "testNativeTransfer_AlchemyModularAccount": "180829", + "testNativeTransfer_CoinbaseSmartWallet": "178916", + "testNativeTransfer_IthacaAccount": "129551", + "testNativeTransfer_IthacaAccount_AppSponsor": "140096", + "testNativeTransfer_IthacaAccount_ERC20SelfPay": "137340", + "testNativeTransfer_Safe4337": "198595", + "testNativeTransfer_ZerodevKernel": "208635", + "testUniswapV2Swap_AlchemyModularAccount": "238647", + "testUniswapV2Swap_CoinbaseSmartWallet": "237451", + "testUniswapV2Swap_ERC4337MinimalAccount": "230691", + "testUniswapV2Swap_IthacaAccount": "187339", + "testUniswapV2Swap_IthacaAccount_AppSponsor": "197817", + "testUniswapV2Swap_IthacaAccount_ERC20SelfPay": "192628", + "testUniswapV2Swap_Safe4337": "257333", + "testUniswapV2Swap_ZerodevKernel": "266367" } \ No newline at end of file diff --git a/src/Escrow.sol b/src/Escrow.sol index 6138cab9..a924bc59 100644 --- a/src/Escrow.sol +++ b/src/Escrow.sol @@ -132,12 +132,12 @@ contract Escrow is IEscrow { for (uint256 i = 0; i < escrowIds.length; i++) { Escrow storage _escrow = escrows[escrowIds[i]]; // If refund timestamp hasn't passed yet, then the refund is invalid. - if (block.timestamp > _escrow.refundTimestamp || msg.sender == _escrow.recipient) { - _refundDepositor(escrowIds[i], _escrow); - _refundRecipient(escrowIds[i], _escrow); - } else { + if (block.timestamp <= _escrow.refundTimestamp) { revert RefundInvalid(); } + + _refundDepositor(escrowIds[i], _escrow); + _refundRecipient(escrowIds[i], _escrow); } } @@ -147,11 +147,10 @@ contract Escrow is IEscrow { for (uint256 i = 0; i < escrowIds.length; i++) { Escrow storage _escrow = escrows[escrowIds[i]]; // If refund timestamp hasn't passed yet, then the refund is invalid. - if (block.timestamp > _escrow.refundTimestamp || msg.sender == _escrow.depositor) { - _refundDepositor(escrowIds[i], _escrow); - } else { + if (block.timestamp <= _escrow.refundTimestamp) { revert RefundInvalid(); } + _refundDepositor(escrowIds[i], _escrow); } } @@ -182,11 +181,11 @@ contract Escrow is IEscrow { Escrow storage _escrow = escrows[escrowIds[i]]; // If settlement is still within the deadline, then refund is invalid. - if (block.timestamp > _escrow.refundTimestamp || msg.sender == _escrow.recipient) { - _refundRecipient(escrowIds[i], _escrow); - } else { + if (block.timestamp <= _escrow.refundTimestamp) { revert RefundInvalid(); } + + _refundRecipient(escrowIds[i], _escrow); } } diff --git a/src/GuardedExecutor.sol b/src/GuardedExecutor.sol index 4d655929..cf5eb502 100644 --- a/src/GuardedExecutor.sol +++ b/src/GuardedExecutor.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; +import {ERC7821} from "solady/accounts/ERC7821.sol"; import {LibSort} from "solady/utils/LibSort.sol"; import {LibBytes} from "solady/utils/LibBytes.sol"; import {LibZip} from "solady/utils/LibZip.sol"; @@ -12,7 +13,6 @@ import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {FixedPointMathLib as Math} from "solady/utils/FixedPointMathLib.sol"; import {DateTimeLib} from "solady/utils/DateTimeLib.sol"; import {ICallChecker} from "./interfaces/ICallChecker.sol"; -import {ERC7821Ithaca as ERC7821} from "./libraries/ERC7821Ithaca.sol"; /// @title GuardedExecutor /// @notice Mixin for spend limits and calldata execution guards. @@ -401,7 +401,7 @@ abstract contract GuardedExecutor is ERC7821 { checkKeyHashIsNonZero(keyHash) { if (keyHash != ANY_KEYHASH) { - if (_isSuperAdmin(keyHash)) revert SuperAdminCanExecuteEverything(); + if (_isSuperAdmin(keyHash)) revert SuperAdminCanSpendAnything(); } // It is ok even if we don't check for `_isSelfExecute` here, as we will still @@ -698,10 +698,10 @@ abstract contract GuardedExecutor is ERC7821 { // Configurables //////////////////////////////////////////////////////////////////////// - /// @dev To be overridden to return if `keyHash` corresponds to a super admin key. + /// @dev To be overriden to return if `keyHash` corresponds to a super admin key. function _isSuperAdmin(bytes32 keyHash) internal view virtual returns (bool); - /// @dev To be overridden to return the storage slot seed for a `keyHash`. + /// @dev To be overriden to return the storage slot seed for a `keyHash`. function _getGuardedExecutorKeyStorageSeed(bytes32 keyHash) internal view diff --git a/src/IthacaAccount.sol b/src/IthacaAccount.sol index a8ffdbca..f770cabc 100644 --- a/src/IthacaAccount.sol +++ b/src/IthacaAccount.sol @@ -275,8 +275,8 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { (bool isValid, bytes32 keyHash) = unwrapAndValidateSignature(digest, signature); if (LibBit.and(keyHash != 0, isValid)) { - isValid = - _isSuperAdmin(keyHash) || _getKeyExtraStorage(keyHash).checkers.contains(msg.sender); + isValid = _isSuperAdmin(keyHash) + || _getKeyExtraStorage(keyHash).checkers.contains(msg.sender); } // `bytes4(keccak256("isValidSignature(bytes32,bytes)")) = 0x1626ba7e`. // We use `0xffffffff` for invalid, in convention with the reference implementation. @@ -400,12 +400,7 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { } /// @dev Returns arrays of all (non-expired) authorized keys and their hashes. - function getKeys() - public - view - virtual - returns (Key[] memory keys, bytes32[] memory keyHashes) - { + function getKeys() public view virtual returns (Key[] memory keys, bytes32[] memory keyHashes) { uint256 totalCount = keyCount(); keys = new Key[](totalCount); @@ -619,9 +614,8 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { // `keccak256(abi.encode(key.keyType, keccak256(key.publicKey)))`. keyHash = hash(key); AccountStorage storage $ = _getAccountStorage(); - $.keyStorage[keyHash].set( - abi.encodePacked(key.publicKey, key.expiry, key.keyType, key.isSuperAdmin) - ); + $.keyStorage[keyHash] + .set(abi.encodePacked(key.publicKey, key.expiry, key.keyType, key.isSuperAdmin)); $.keyHashes.add(keyHash); } @@ -669,12 +663,10 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { if or(shr(64, t), lt(encodedIntent.length, 0x20)) { revert(0x00, 0x00) } } - if ( - !LibBit.and( + if (!LibBit.and( msg.sender == ORCHESTRATOR, LibBit.or(intent.eoa == address(this), intent.payer == address(this)) - ) - ) { + )) { revert Unauthorized(); } diff --git a/src/MultiSigSigner.sol b/src/MultiSigSigner.sol index 6f6fb272..73d82df9 100644 --- a/src/MultiSigSigner.sol +++ b/src/MultiSigSigner.sol @@ -12,7 +12,7 @@ contract MultiSigSigner is ISigner { //////////////////////////////////////////////////////////////////////// /// @dev The magic value returned by `isValidSignatureWithKeyHash` when the signature is valid. - /// - Calculated as: bytes4(keccak256("isValidSignatureWithKeyHash(bytes32,bytes32,bytes)") + /// - Calcualated as: bytes4(keccak256("isValidSignatureWithKeyHash(bytes32,bytes32,bytes)") bytes4 internal constant _MAGIC_VALUE = 0x8afc93b4; /// @dev The magic value returned by `isValidSignatureWithKeyHash` when the signature is invalid. @@ -175,7 +175,7 @@ contract MultiSigSigner is ISigner { /// for each owner key hash in the config. /// - Signature of a multi-sig should be encoded as abi.encode(bytes[] memory ownerSignatures) /// - For efficiency, place the signatures in the same order as the ownerKeyHashes in the config. - /// - Failing owner signatures are ignored, as long as valid signatures > threshold. + /// - Failing owner signatures are ignored, as long as valid signaturs > threshold. function isValidSignatureWithKeyHash(bytes32 digest, bytes32 keyHash, bytes memory signature) public view diff --git a/src/Orchestrator.sol b/src/Orchestrator.sol index 72815450..06586bcc 100644 --- a/src/Orchestrator.sol +++ b/src/Orchestrator.sol @@ -20,7 +20,6 @@ import {ICommon} from "./interfaces/ICommon.sol"; import {IFunder} from "./interfaces/IFunder.sol"; import {ISettler} from "./interfaces/ISettler.sol"; import {MerkleProofLib} from "solady/utils/MerkleProofLib.sol"; -import {IntentHelpers} from "./libraries/IntentHelpers.sol"; /// @title Orchestrator /// @notice Enables atomic verification, gas compensation and execution across eoas. @@ -42,13 +41,7 @@ import {IntentHelpers} from "./libraries/IntentHelpers.sol"; /// This means once an Intent is signed, it is infeasible to /// alter or rearrange it to force it to fail. -contract Orchestrator is - IOrchestrator, - EIP712, - CallContextChecker, - ReentrancyGuardTransient, - IntentHelpers -{ +contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGuardTransient { using LibERC7579 for bytes32[]; using EfficientHashLib for bytes32[]; using LibBitmap for LibBitmap.Bitmap; @@ -117,12 +110,12 @@ contract Orchestrator is /// @dev For EIP712 signature digest calculation for the `execute` function. bytes32 public constant INTENT_TYPEHASH = keccak256( - "Intent(Call[] calls,address eoa,uint256 nonce,address payer,address paymentToken,uint256 paymentMaxAmount,uint256 combinedGas,uint256 expiry)Call(address to,uint256 value,bytes data)" + "Intent(bool multichain,address eoa,Call[] calls,uint256 nonce,address payer,address paymentToken,uint256 paymentMaxAmount,uint256 combinedGas,bytes[] encodedPreCalls,bytes[] encodedFundTransfers,address settler,uint256 expiry)Call(address to,uint256 value,bytes data)" ); /// @dev For EIP712 signature digest calculation for SignedCalls bytes32 public constant SIGNED_CALL_TYPEHASH = keccak256( - "SignedCall(address eoa,Call[] calls,uint256 nonce)Call(address to,uint256 value,bytes data)" + "SignedCall(bool multichain,address eoa,Call[] calls,uint256 nonce)Call(address to,uint256 value,bytes data)" ); /// @dev For EIP712 signature digest calculation for the `execute` function. @@ -134,10 +127,6 @@ contract Orchestrator is /// This constant is a pun for "chain ID 0". uint16 public constant MULTICHAIN_NONCE_PREFIX = 0xc1d0; - /// @dev Nonce prefix to signal that the payload should use merkle verification. - /// This constant is "mv" in hex. - uint16 public constant MERKLE_VERIFICATION = 0x6D76; - /// @dev For ensuring that the remaining gas is sufficient for a self-call with /// overhead for cleaning up after the self-call. This also has an added benefit /// of preventing the censorship vector of calling `execute` in a very deep call-stack. @@ -206,7 +195,7 @@ contract Orchestrator is /// If sufficient gas is provided, returns an error selector that is non-zero /// if there is an error during the payment, verification, and call execution. function execute(bytes calldata encodedIntent) - external + public payable virtual nonReentrant @@ -222,6 +211,7 @@ contract Orchestrator is public payable virtual + nonReentrant returns (bytes4[] memory errs) { // This allocation and loop was initially in assembly, but I've normified it for now. @@ -230,7 +220,7 @@ contract Orchestrator is // We reluctantly use regular Solidity to access `encodedIntents[i]`. // This generates an unnecessary check for `i < encodedIntents.length`, but helps // generate all the implicit calldata bound checks on `encodedIntents[i]`. - errs[i] = this.execute(encodedIntents[i]); + (, errs[i]) = _execute(encodedIntents[i], 0, _NORMAL_MODE_FLAG); } } @@ -242,16 +232,11 @@ contract Orchestrator is /// But the balance of tx.origin has to be greater than or equal to type(uint192).max, to prove that a state override has been made offchain, /// and this is not an onchain call. This mode has been added so that receipt logs can be generated for `eth_simulateV1` /// @return gasUsed The amount of gas used by the execution. (Only returned if `isStateOverride` is true) - function simulateExecute(bytes calldata encodedIntent) external payable returns (uint256) { - bool isStateOverride; - uint256 combinedGasOverride; - assembly ("memory-safe") { - let endOfBytes := add(encodedIntent.offset, encodedIntent.length) - isStateOverride := calldataload(sub(endOfBytes, 0x40)) - combinedGasOverride := calldataload(sub(endOfBytes, 0x20)) - encodedIntent.length := sub(encodedIntent.length, 0x40) - } - + function simulateExecute( + bool isStateOverride, + uint256 combinedGasOverride, + bytes calldata encodedIntent + ) external payable returns (uint256) { // If Simulation Fails, then it will revert here. (uint256 gUsed, bytes4 err) = _execute(encodedIntent, combinedGasOverride, _SIMULATION_MODE_FLAG); @@ -275,7 +260,33 @@ contract Orchestrator is } } + /// @dev Extracts the Intent from the calldata bytes, with minimal checks. + function _extractIntent(bytes calldata encodedIntent) + internal + view + virtual + returns (Intent calldata i) + { + // This function does NOT allocate memory to avoid quadratic memory expansion costs. + // Otherwise, it will be unfair to the Intents at the back of the batch. + + // `dynamicStructInCalldata` internally performs out-of-bounds checks. + bytes calldata intentCalldata = LibBytes.dynamicStructInCalldata(encodedIntent, 0x00); + assembly ("memory-safe") { + i := intentCalldata.offset + } + // These checks are included for more safety: Swiss Cheese Model. + // Ensures that all the dynamic children in `encodedIntent` are contained. + LibBytes.checkInCalldata(i.executionData, intentCalldata); + LibBytes.checkInCalldata(i.encodedPreCalls, intentCalldata); + LibBytes.checkInCalldata(i.encodedFundTransfers, intentCalldata); + LibBytes.checkInCalldata(i.funderSignature, intentCalldata); + LibBytes.checkInCalldata(i.settlerContext, intentCalldata); + LibBytes.checkInCalldata(i.signature, intentCalldata); + LibBytes.checkInCalldata(i.paymentSignature, intentCalldata); + } /// @dev Extracts the PreCall from the calldata bytes, with minimal checks. + function _extractPreCall(bytes calldata encodedPreCall) internal virtual @@ -292,7 +303,7 @@ contract Orchestrator is /// @dev Executes a single encoded intent. /// @dev If flags is non-zero, then all errors are bubbled up. /// Currently there can only be 2 modes - simulation mode, and execution mode. - /// But we use a uint256 for efficient stack operations, and more flexibility in the future. + /// But we use a uint256 for efficient stack operations, and more flexiblity in the future. /// Note: We keep the flags in the stack/memory (TSTORE doesn't work) to make sure they are reset in each new call context, /// to provide protection against attacks which could spoof the execute function to believe it is in simulation mode. function _execute(bytes calldata encodedIntent, uint256 combinedGasOverride, uint256 flags) @@ -300,11 +311,12 @@ contract Orchestrator is virtual returns (uint256 gUsed, bytes4 err) { - uint256 g = Math.coalesce(uint96(combinedGasOverride), _getCombinedGas()); + Intent calldata i = _extractIntent(encodedIntent); + + uint256 g = Math.coalesce(uint96(combinedGasOverride), i.combinedGas); uint256 gStart = gasleft(); - address eoa = _getEoa(); - if (_getPaymentAmount() > _getPaymentMaxAmount()) { + if (i.paymentAmount > i.paymentMaxAmount) { err = PaymentError.selector; if (flags == _SIMULATION_MODE_FLAG) { @@ -316,20 +328,15 @@ contract Orchestrator is // Check if there's sufficient gas left for the gas-limited self calls // via the 63/64 rule. This is for gas estimation. If the total amount of gas // for the whole transaction is insufficient, revert. - uint256 gasAvailable = (gasleft() * 63) >> 6; - uint256 gasRequired = Math.saturatingAdd(g, _INNER_GAS_OVERHEAD); - - if (gasAvailable < gasRequired) { + if (((gasleft() * 63) >> 6) < Math.saturatingAdd(g, _INNER_GAS_OVERHEAD)) { if (flags != _SIMULATION_MODE_FLAG) { revert InsufficientGas(); } } } - address accountImpl = _getSupportedAccountImplementation(); - if (accountImpl != address(0)) { - address currentImpl = accountImplementationOf(eoa); - if (currentImpl != accountImpl) { + if (i.supportedAccountImplementation != address(0)) { + if (accountImplementationOf(i.eoa) != i.supportedAccountImplementation) { err = UnsupportedAccountImplementation.selector; if (flags == _SIMULATION_MODE_FLAG) { revert UnsupportedAccountImplementation(); @@ -337,16 +344,18 @@ contract Orchestrator is } } - address payer = Math.coalesce(_getPayer(), eoa); + address payer = Math.coalesce(i.payer, i.eoa); // Early skip the entire pay-verify-call workflow if the payer lacks tokens, // so that less gas is wasted when the Intent fails. // For multi chain mode, we skip this check, as the funding happens inside the self call. - if (TokenTransferLib.balanceOf(_getPaymentToken(), payer) < _getPaymentAmount()) { - err = PaymentError.selector; + if (!i.isMultichain && LibBit.and(i.paymentAmount != 0, err == 0)) { + if (TokenTransferLib.balanceOf(i.paymentToken, payer) < i.paymentAmount) { + err = PaymentError.selector; - if (flags == _SIMULATION_MODE_FLAG) { - revert PaymentError(); + if (flags == _SIMULATION_MODE_FLAG) { + revert PaymentError(); + } } } @@ -355,14 +364,11 @@ contract Orchestrator is assembly ("memory-safe") { let m := mload(0x40) // Grab the free memory pointer. if iszero(err) { + // Copy the encoded user op to the memory to be ready to pass to the self call. + calldatacopy(add(m, 0x40), encodedIntent.offset, encodedIntent.length) mstore(m, 0x00000000) // `selfCallPayVerifyCall537021665()`. // The word after the function selector contains the simulation flags. mstore(add(m, 0x20), flags) - // Copy the encoded user op to the memory to be ready to pass to the self call. - // We skip adding the calldata offset and length since we don't read that, but - // add `flags` in the first slot. - calldatacopy(add(m, 0x60), encodedIntent.offset, encodedIntent.length) - mstore(0x00, 0) // Zeroize the return slot. // To prevent griefing, we need to do a non-reverting gas-limited self call. @@ -374,27 +380,26 @@ contract Orchestrator is address(), 0, add(m, 0x1c), - add(encodedIntent.length, 0x44), + add(encodedIntent.length, 0x24), 0x00, 0x20 ) err := mload(0x00) // The self call will do another self call to execute. - } - } - assembly ("memory-safe") { - if iszero(selfCallSuccess) { - // If it is a simulation, we simply revert with the full error. - if eq(flags, _SIMULATION_MODE_FLAG) { - returndatacopy(mload(0x40), 0x00, returndatasize()) - revert(mload(0x40), returndatasize()) - } - // If we don't get an error selector, then we set this one. - if iszero(err) { err := shl(224, 0xad4db224) } // `VerifiedCallError()`. + if iszero(selfCallSuccess) { + // If it is a simulation, we simply revert with the full error. + if eq(flags, _SIMULATION_MODE_FLAG) { + returndatacopy(mload(0x40), 0x00, returndatasize()) + revert(mload(0x40), returndatasize()) + } + + // If we don't get an error selector, then we set this one. + if iszero(err) { err := shl(224, 0xad4db224) } // `VerifiedCallError()`. + } } } - emit IntentExecuted(_getEoa(), _getNonce(), selfCallSuccess, err); + emit IntentExecuted(i.eoa, i.nonce, selfCallSuccess, err); if (selfCallSuccess) { gUsed = Math.rawSub(gStart, gasleft()); } @@ -427,36 +432,25 @@ contract Orchestrator is function selfCallPayVerifyCall537021665() public payable { require(msg.sender == address(this)); + Intent calldata i; uint256 flags; assembly ("memory-safe") { + i := add(0x24, calldataload(0x24)) flags := calldataload(0x04) } // Check if intent has expired (only if expiry is set) // If expiry timestamp is set to 0, then expiry is considered to be infinite. - { - uint256 expiry = _getExpiry(); - if (expiry != 0 && block.timestamp > expiry) { - revert IntentExpired(); - } + if (i.expiry != 0 && block.timestamp > i.expiry) { + revert IntentExpired(); } - address eoa = _getEoa(); - - // Start a calldata pointer to traverse all the inner dynamic bytes within. Every time we get the next dynamic bytes, - // the pointer is advanced. A memory pointer gives some efficiency and will produce relatively clean top-level code. - CalldataPointer memory ptr; - bytes32 digest = _computeDigest(ptr); + address eoa = i.eoa; + uint256 nonce = i.nonce; + bytes32 digest = _computeDigest(i); - { - bytes calldata fundData = _getNextBytes(ptr); + _fund(eoa, i.funder, digest, i.encodedFundTransfers, i.funderSignature); - if (fundData.length > 0) { - (address funder, bytes calldata sig, bytes[] calldata transfers) = - _parseFundData(fundData); - _fund(_getEoa(), funder, digest, transfers, sig); - } - } // The chicken and egg problem: // A off-chain simulation of a successful Intent may not guarantee on-chain success. // The state may change in the window between simulation and actual on-chain execution. @@ -472,82 +466,57 @@ contract Orchestrator is // simulation, and suggests banning users that intentionally grief the simulation. // Handle the sub Intents after initialize (if any), and before the `_verify`. - { - bytes calldata preCallsBytes = _getNextBytes(ptr); - if (preCallsBytes.length > 0) { - _handlePreCalls(eoa, _getPayer(), flags, preCallsBytes); - } - } + if (i.encodedPreCalls.length != 0) _handlePreCalls(eoa, i.payer, flags, i.encodedPreCalls); + // If `_verify` is invalid, just revert. // The verification gas is determined by `executionData` and the account logic. // Off-chain simulation of `_verify` should suffice, provided that the eoa's // account is not changed, and the `keyHash` is not revoked // in the window between off-chain simulation and on-chain execution. + + bool isValid; bytes32 keyHash; - { - bool isValid; - bytes calldata signature = _getNextBytes(ptr); - - uint256 nonce = _getNonce(); - (isValid, keyHash) = _verify(digest, eoa, signature); - - if (nonce >> 240 == MERKLE_VERIFICATION) { - bytes calldata settlerData = _getNextBytes(ptr); - // If this is an output intent, then send the digest as the settlementId - // on all input chains. - if (settlerData.length > 0) { - // Output intent - first 32 bytes of settler data contains the settler address - // Then, it contains 2 offsets then the real data - ISettler(address(uint160(uint256(bytes32(settlerData[:32]))))) - .send(digest, settlerData[96:]); - } + if (i.isMultichain) { + // For multi chain intents, we have to verify using merkle sigs. + (isValid, keyHash) = _verifyMerkleSig(digest, eoa, i.signature); + + // If this is an output intent, then send the digest as the settlementId + // on all input chains. + if (i.encodedFundTransfers.length > 0) { + // Output intent + ISettler(i.settler).send(digest, i.settlerContext); } + } else { + (isValid, keyHash) = _verify(digest, eoa, i.signature); + } - if (flags == _SIMULATION_MODE_FLAG) { - isValid = true; - } + if (flags == _SIMULATION_MODE_FLAG) { + isValid = true; + } - if (!isValid) { - revert VerificationError(); - } + if (!isValid) revert VerificationError(); - _checkAndIncrementNonce(eoa, nonce); - } + _checkAndIncrementNonce(eoa, nonce); // Payment // If `_pay` fails, just revert. // Off-chain simulation of `_pay` should suffice, // provided that the token balance does not decrease in the window between // off-chain simulation and on-chain execution. - { - uint256 paymentAmount = _getPaymentAmount(); - bytes calldata paymentSignature = _getNextBytes(ptr); - if (paymentAmount != 0) { - _pay( - paymentAmount, - keyHash, - digest, - eoa, - _getPayer(), - _getPaymentToken(), - _getPaymentRecipient(), - paymentSignature - ); - } - } + if (i.paymentAmount != 0) _pay(keyHash, digest, i); // This re-encodes the ERC7579 `executionData` with the optional `opData`. // We expect that the account supports ERC7821 // (an extension of ERC7579 tailored for 7702 accounts). - bytes memory executeData = LibERC7579.reencodeBatchAsExecuteCalldata( + bytes memory data = LibERC7579.reencodeBatchAsExecuteCalldata( hex"01000000000078210001", // ERC7821 batch execution mode. - _getExecutionData(), + i.executionData, abi.encode(keyHash) // `opData`. ); assembly ("memory-safe") { mstore(0x00, 0) // Zeroize the return slot. - if iszero(call(gas(), eoa, 0, add(0x20, executeData), mload(executeData), 0x00, 0x20)) { + if iszero(call(gas(), eoa, 0, add(0x20, data), mload(data), 0x00, 0x20)) { if eq(flags, _SIMULATION_MODE_FLAG) { returndatacopy(mload(0x40), 0x00, returndatasize()) revert(mload(0x40), returndatasize()) @@ -570,15 +539,10 @@ contract Orchestrator is address parentEOA, address payer, uint256 flags, - bytes calldata encodedPreCalls + bytes[] calldata encodedPreCalls ) internal virtual { - bytes[] calldata calls; - assembly ("memory-safe") { - calls.length := calldataload(add(encodedPreCalls.offset, 0x20)) - calls.offset := add(encodedPreCalls.offset, 0x40) - } - for (uint256 j; j < calls.length; ++j) { - SignedCall calldata p = _extractPreCall(calls[j]); + for (uint256 j; j < encodedPreCalls.length; ++j) { + SignedCall calldata p = _extractPreCall(encodedPreCalls[j]); address eoa = Math.coalesce(p.eoa, parentEOA); uint256 nonce = p.nonce; @@ -634,6 +598,28 @@ contract Orchestrator is // Multi Chain Functions //////////////////////////////////////////////////////////////////////// + /// @dev Verifies the merkle sig for the multi chain intents. + /// - Note: Each leaf of the merkle tree should be a standard intent digest, computed with chainId. + /// - Leaf intents do NOT need to have the multichain nonce prefix. + /// - The signature for multi chain intents using merkle verification is encoded as: + /// - bytes signature = abi.encode(bytes32[] memory proof, bytes32 root, bytes memory rootSig) + function _verifyMerkleSig(bytes32 digest, address eoa, bytes memory signature) + internal + view + returns (bool isValid, bytes32 keyHash) + { + (bytes32[] memory proof, bytes32 root, bytes memory rootSig) = + abi.decode(signature, (bytes32[], bytes32, bytes)); + + if (MerkleProofLib.verify(proof, root, digest)) { + (isValid, keyHash) = IIthacaAccount(eoa).unwrapAndValidateSignature(root, rootSig); + + return (isValid, keyHash); + } + + return (false, bytes32(0)); + } + /// @dev Funds the eoa with with the encoded fund transfers, before executing the intent. /// - For ERC20 tokens, the funder needs to approve the orchestrator to pull funds. /// - For native assets like ETH, the funder needs to transfer the funds to the orchestrator @@ -643,7 +629,7 @@ contract Orchestrator is address eoa, address funder, bytes32 digest, - bytes[] calldata encodedFundTransfers, + bytes[] memory encodedFundTransfers, bytes memory funderSignature ) internal virtual { // Note: The fund function is mostly only used in the multi chain mode. @@ -678,59 +664,51 @@ contract Orchestrator is /// @dev Makes the `eoa` perform a payment to the `paymentRecipient` directly. /// This reverts if the payment is insufficient or fails. Otherwise returns nothing. - function _pay( - uint256 paymentAmount, - bytes32 keyHash, - bytes32 intentDigest, - address eoa, - address payer, - address paymentToken, - address paymentRecipient, - bytes calldata paymentSignature - ) internal virtual { - uint256 currentBalance = TokenTransferLib.balanceOf(paymentToken, paymentRecipient); + function _pay(bytes32 keyHash, bytes32 digest, Intent calldata i) internal virtual { + uint256 paymentAmount = i.paymentAmount; + uint256 requiredBalanceAfter = Math.saturatingAdd( + TokenTransferLib.balanceOf(i.paymentToken, i.paymentRecipient), paymentAmount + ); - uint256 requiredBalanceAfter = Math.saturatingAdd(currentBalance, paymentAmount); + address payer = Math.coalesce(i.payer, i.eoa); // Call the pay function on the account contract // Equivalent Solidity code: // IIthacaAccount(payer).pay(paymentAmount, keyHash, digest, abi.encode(i)); // Gas Savings: // Saves ~2k gas for normal use cases, by avoiding abi.encode and solidity external call overhead - address callee = Math.coalesce(payer, eoa); - assembly ("memory-safe") { let m := mload(0x40) // Load the free memory pointer - mstore(m, 0x38e11b2a) // `pay(uint256,bytes32,bytes32,address,address,address,address,bytes)` - mstore(add(m, 0x20), paymentAmount) // Add paymentAmount as first param + mstore(m, 0xf81d87a7) // `pay(uint256,bytes32,bytes32,bytes)` + mstore(add(m, 0x20), paymentAmount) // Add payment amount as first param mstore(add(m, 0x40), keyHash) // Add keyHash as second param - mstore(add(m, 0x60), intentDigest) // Add intentDigest as third param - mstore(add(m, 0x80), eoa) // Add eoa as fourth param - mstore(add(m, 0xa0), payer) // Add payer as fifth param - mstore(add(m, 0xc0), paymentToken) // Add paymentToken as sixth param - mstore(add(m, 0xe0), paymentRecipient) // Add paymentRecipient as seventh param - mstore(add(m, 0x100), 0x100) // Add offset for paymentSignature as eighth param + mstore(add(m, 0x60), digest) // Add digest as third param + mstore(add(m, 0x80), 0x80) // Add offset of encoded Intent as third param + + let encodedSize := sub(calldatasize(), i) - // Store paymentSignature length and data - mstore(add(m, 0x120), paymentSignature.length) - calldatacopy(add(m, 0x140), paymentSignature.offset, paymentSignature.length) + mstore(add(m, 0xa0), add(encodedSize, 0x20)) // Store length of encoded Intent at offset. + mstore(add(m, 0xc0), 0x20) // Offset at which the Intent struct starts in encoded Intent. + + // Copy the intent data to memory + calldatacopy(add(m, 0xe0), i, encodedSize) // We revert here, so that if the payment fails, the execution is also reverted. // The revert for payment is caught inside the selfCallPayVerify function. if iszero( call( gas(), // gas - callee, // address + payer, // address 0, // value add(m, 0x1c), // input memory offset - add(0x124, paymentSignature.length), // input size + add(0xc4, encodedSize), // input size 0x00, // output memory offset 0x20 // output size ) ) { revert(0x00, 0x20) } } - if (TokenTransferLib.balanceOf(paymentToken, paymentRecipient) < requiredBalanceAfter) { + if (TokenTransferLib.balanceOf(i.paymentToken, i.paymentRecipient) < requiredBalanceAfter) { revert PaymentError(); } } @@ -774,39 +752,42 @@ contract Orchestrator is /// @dev Computes the EIP712 digest for the PreCall. function _computeDigest(SignedCall calldata p) internal view virtual returns (bytes32) { + bool isMultichain = p.nonce >> 240 == MULTICHAIN_NONCE_PREFIX; // To avoid stack-too-deep. Faster than a regular Solidity array anyways. - bytes32[] memory f = EfficientHashLib.malloc(4); + bytes32[] memory f = EfficientHashLib.malloc(5); f.set(0, SIGNED_CALL_TYPEHASH); - f.set(1, uint160(p.eoa)); - f.set(2, _executionDataHash(p.executionData)); - f.set(3, p.nonce); + f.set(1, LibBit.toUint(isMultichain)); + f.set(2, uint160(p.eoa)); + f.set(3, _executionDataHash(p.executionData)); + f.set(4, p.nonce); - return p.nonce >> 240 == MULTICHAIN_NONCE_PREFIX - ? _hashTypedDataSansChainId(f.hash()) - : _hashTypedData(f.hash()); + return isMultichain ? _hashTypedDataSansChainId(f.hash()) : _hashTypedData(f.hash()); } - /// @dev Computes the EIP712 digest for the Intent - /// @dev Also updates the passed in CalldataPointer to point to the start of the next dynamic bytes. - function _computeDigest(CalldataPointer memory p) internal view virtual returns (bytes32) { - bytes32 digest = INTENT_TYPEHASH; + /// @dev Computes the EIP712 digest for the Intent. + /// If the the nonce starts with `MULTICHAIN_NONCE_PREFIX`, + /// the digest will be computed without the chain ID. + /// Otherwise, the digest will be computed with the chain ID. + function _computeDigest(Intent calldata i) internal view virtual returns (bytes32) { + bool isMultichain = i.nonce >> 240 == MULTICHAIN_NONCE_PREFIX; - assembly ("memory-safe") { - let fmp := mload(0x40) - mstore(fmp, digest) - let length := calldataload(_EXECUTION_DATA_OFFSET) - calldatacopy(add(fmp, 0x20), add(_EXECUTION_DATA_OFFSET, 0x20), length) - mstore(add(fmp, 0x20), keccak256(add(fmp, 0x20), length)) - calldatacopy(add(fmp, 0x40), _EOA_OFFSET, 224) // copy 7 words - digest := keccak256(fmp, 288) // hash 9 words, inc typehash - - // update the offset for the memory ptr - mstore(p, add(add(_EXECUTION_DATA_OFFSET, 0x20), length)) - } - - return _getNonce() >> 240 == MULTICHAIN_NONCE_PREFIX - ? _hashTypedDataSansChainId(digest) - : _hashTypedData(digest); + // To avoid stack-too-deep. Faster than a regular Solidity array anyways. + bytes32[] memory f = EfficientHashLib.malloc(13); + f.set(0, INTENT_TYPEHASH); + f.set(1, LibBit.toUint(isMultichain)); + f.set(2, uint160(i.eoa)); + f.set(3, _executionDataHash(i.executionData)); + f.set(4, i.nonce); + f.set(5, uint160(i.payer)); + f.set(6, uint160(i.paymentToken)); + f.set(7, i.paymentMaxAmount); + f.set(8, i.combinedGas); + f.set(9, _encodedArrHash(i.encodedPreCalls)); + f.set(10, _encodedArrHash(i.encodedFundTransfers)); + f.set(11, uint160(i.settler)); + f.set(12, i.expiry); + + return isMultichain ? _hashTypedDataSansChainId(f.hash()) : _hashTypedData(f.hash()); } /// @dev Helper function to return the hash of the `execuctionData`. @@ -859,7 +840,7 @@ contract Orchestrator is returns (string memory name, string memory version) { name = "Orchestrator"; - version = "0.5.6"; + version = "0.5.5"; } //////////////////////////////////////////////////////////////////////// diff --git a/src/SimpleFunder.sol b/src/SimpleFunder.sol index 7109f028..e74947fb 100644 --- a/src/SimpleFunder.sol +++ b/src/SimpleFunder.sol @@ -58,7 +58,7 @@ contract SimpleFunder is EIP712, Ownable, IFunder { returns (string memory name, string memory version) { name = "SimpleFunder"; - version = "0.1.9"; + version = "0.1.8"; } //////////////////////////////////////////////////////////////////////// @@ -133,10 +133,11 @@ contract SimpleFunder is EIP712, Ownable, IFunder { /// @dev Allows the orchestrator to fund an account. /// The `digest` includes the intent nonce and the transfers. - function fund(bytes32 digest, ICommon.Transfer[] memory transfers, bytes memory funderSignature) - public - override - { + function fund( + bytes32 digest, + ICommon.Transfer[] memory transfers, + bytes memory funderSignature + ) public override { if (!orchestrators[msg.sender]) { revert OnlyOrchestrator(); } @@ -182,7 +183,7 @@ contract SimpleFunder is EIP712, Ownable, IFunder { if gt(amount, allowance) { mstore(m, 0x095ea7b3) // `approve(address,uint256)`. mstore(add(m, 0x20), caller()) - mstore(add(m, 0x40), 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) // 20-byte all-ones sentinel (2^160-1), not uint256 max + mstore(add(m, 0x40), 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) // type(uint256).max // Orchestrator checks for token transfer success, so we don't need to check it here. pop(call(gas(), token, 0, add(m, 0x1c), 0x44, 0x00, 0x00)) } diff --git a/src/Simulator.sol b/src/Simulator.sol index 2a5e2a76..1e468d6b 100644 --- a/src/Simulator.sol +++ b/src/Simulator.sol @@ -4,12 +4,10 @@ pragma solidity ^0.8.23; import {ICommon} from "./interfaces/ICommon.sol"; import {IMulticall3} from "./interfaces/IMulticall3.sol"; import {FixedPointMathLib as Math} from "solady/utils/FixedPointMathLib.sol"; -import {MockOrchestrator} from "../test/utils/mocks/MockOrchestrator.sol"; -import {IntentHelpers} from "./libraries/IntentHelpers.sol"; /// @title Simulator /// @notice A separate contract for calling the Orchestrator contract solely for gas simulation. -contract Simulator is IntentHelpers { +contract Simulator { //////////////////////////////////////////////////////////////////////// // EIP-5267 Support //////////////////////////////////////////////////////////////////////// @@ -56,23 +54,15 @@ contract Simulator is IntentHelpers { /// @dev Updates the payment amounts for the Intent passed in. function _updatePaymentAmounts( - bytes memory u, + ICommon.Intent memory u, uint256 gas, uint8 paymentPerGasPrecision, uint256 paymentPerGas ) internal pure { uint256 paymentAmount = Math.fullMulDiv(gas, paymentPerGas, 10 ** paymentPerGasPrecision); - // 36 because we don't have the calldata offset, or the function selector - uint256 paymentOffset = _PAYMENT_AMOUNT_OFFSET - 36; - uint256 paymentMaxOffset = _PAYMENT_MAX_AMOUNT_OFFSET - 36; - assembly ("memory-safe") { - let currentPaymentAmount := mload(add(u, paymentOffset)) - mstore(add(u, paymentOffset), add(currentPaymentAmount, paymentAmount)) - - let currentPaymentMaxAmount := mload(add(u, paymentMaxOffset)) - mstore(add(u, paymentMaxOffset), add(currentPaymentMaxAmount, paymentAmount)) - } + u.paymentAmount += paymentAmount; + u.paymentMaxAmount += paymentAmount; } /// @dev Performs a call to the Orchestrator, and returns the gas used by the Intent. @@ -111,8 +101,10 @@ contract Simulator is IntentHelpers { bytes calldata encodedIntent ) internal freeTempMemory returns (uint256) { bytes memory data = abi.encodeWithSignature( - "simulateExecute(bytes)", - abi.encodePacked(encodedIntent, isStateOverride, combinedGasOverride) + "simulateExecute(bool,uint256,bytes)", + isStateOverride, + combinedGasOverride, + encodedIntent ); return _callOrchestrator(oc, isStateOverride, data); } @@ -123,10 +115,13 @@ contract Simulator is IntentHelpers { address oc, bool isStateOverride, uint256 combinedGasOverride, - bytes memory u + ICommon.Intent memory u ) internal freeTempMemory returns (uint256) { bytes memory data = abi.encodeWithSignature( - "simulateExecute(bytes)", abi.encodePacked(u, isStateOverride, combinedGasOverride) + "simulateExecute(bool,uint256,bytes)", + isStateOverride, + combinedGasOverride, + abi.encode(u) ); return _callOrchestrator(oc, isStateOverride, data); } @@ -262,7 +257,7 @@ contract Simulator is IntentHelpers { /// @dev The closer this number is to 10_000, the more precise combined gas will be. But more iterations will be needed. /// @dev This number should always be > 10_000, to get correct results. //// If the increment is too small, the function might run out of gas while finding the combined gas value. - /// @param calldataIntent The encoded intent + /// @param encodedIntent The encoded user operation /// @return gasUsed The gas used in the successful simulation /// @return combinedGas The first combined gas value that gives a successful simulation. /// This function reverts if the primary simulation run with max combinedGas fails. @@ -273,10 +268,10 @@ contract Simulator is IntentHelpers { uint8 paymentPerGasPrecision, uint256 paymentPerGas, uint256 combinedGasIncrement, - bytes calldata calldataIntent + bytes calldata encodedIntent ) public payable virtual returns (uint256 gasUsed, uint256 combinedGas) { // 1. Primary Simulation Run to get initial gasUsed value with combinedGasOverride - gasUsed = _callOrchestratorCalldata(oc, false, type(uint256).max, calldataIntent); + gasUsed = _callOrchestratorCalldata(oc, false, type(uint256).max, encodedIntent); // If the simulation failed, bubble up the full revert. assembly ("memory-safe") { @@ -287,21 +282,15 @@ contract Simulator is IntentHelpers { } } - bytes memory memoryIntent = calldataIntent; + // Update payment amounts using the gasUsed value + ICommon.Intent memory u = abi.decode(encodedIntent, (ICommon.Intent)); - // 36 because we don't have the calldata offset, or the function selector - uint256 offset = _COMBINED_GAS_OFFSET - 36; - assembly ("memory-safe") { - let currentCombinedGas := mload(add(memoryIntent, offset)) - let newCombinedGas := add(currentCombinedGas, gasUsed) - mstore(add(memoryIntent, offset), newCombinedGas) - combinedGas := newCombinedGas - } + u.combinedGas += gasUsed; - _updatePaymentAmounts(memoryIntent, combinedGas, paymentPerGasPrecision, paymentPerGas); + _updatePaymentAmounts(u, u.combinedGas, paymentPerGasPrecision, paymentPerGas); while (true) { - gasUsed = _callOrchestratorMemory(oc, false, 0, memoryIntent); + gasUsed = _callOrchestratorMemory(oc, false, 0, u); // If the simulation failed, bubble up the full revert. assembly ("memory-safe") { @@ -316,34 +305,22 @@ contract Simulator is IntentHelpers { } if (gasUsed != 0) { - // Get current combinedGas from bytes - assembly ("memory-safe") { - combinedGas := mload(add(memoryIntent, offset)) - } - return (gasUsed, combinedGas); + return (gasUsed, u.combinedGas); } - // Get current combinedGas and calculate increment - assembly ("memory-safe") { - combinedGas := mload(add(memoryIntent, offset)) - } - uint256 gasIncrement = Math.mulDiv(combinedGas, combinedGasIncrement, 10_000); + uint256 gasIncrement = Math.mulDiv(u.combinedGas, combinedGasIncrement, 10_000); - _updatePaymentAmounts(memoryIntent, gasIncrement, paymentPerGasPrecision, paymentPerGas); + _updatePaymentAmounts(u, gasIncrement, paymentPerGasPrecision, paymentPerGas); - // Update combinedGas with increment using assembly - assembly ("memory-safe") { - let currentCombinedGas := mload(add(memoryIntent, offset)) - let newCombinedGas := add(currentCombinedGas, gasIncrement) - mstore(add(memoryIntent, offset), newCombinedGas) - } + // Step up the combined gas, until we see a simulation passing + u.combinedGas += gasIncrement; } } /// @dev Same as simulateCombinedGas, but with an additional verification run /// that generates a successful non reverting state override simulation. /// Which can be used in eth_simulateV1 to get the trace.\ - /// @param combinedGasVerificationOffset is a static value that is added after a successful combinedGas is found. + /// @param combinedGasVerificationOffset is a static value that is added after a succesful combinedGas is found. /// This can be used to account for variations in sig verification gas, for keytypes like P256. /// @param paymentPerGasPrecision The precision of the payment per gas value. /// paymentAmount = gas * paymentPerGas / (10 ** paymentPerGasPrecision) @@ -361,18 +338,15 @@ contract Simulator is IntentHelpers { combinedGas += combinedGasVerificationOffset; - bytes memory encodedIntentCopy = encodedIntent; + ICommon.Intent memory u = abi.decode(encodedIntent, (ICommon.Intent)); - _updatePaymentAmounts(encodedIntentCopy, combinedGas, paymentPerGasPrecision, paymentPerGas); + _updatePaymentAmounts(u, combinedGas, paymentPerGasPrecision, paymentPerGas); - // 36 because we don't have the calldata offset, or the function selector - uint256 offset = _COMBINED_GAS_OFFSET - 36; - assembly ("memory-safe") { - mstore(add(encodedIntentCopy, offset), combinedGas) - } + u.combinedGas = combinedGas; // Verification Run to generate the logs with the correct combinedGas and payment amounts. - gasUsed = _callOrchestratorMemory(oc, true, 0, encodedIntentCopy); + gasUsed = _callOrchestratorMemory(oc, true, 0, u); + // If the simulation failed, bubble up full revert assembly ("memory-safe") { if iszero(gasUsed) { diff --git a/src/interfaces/ICommon.sol b/src/interfaces/ICommon.sol index 9057b1f3..54ccf388 100644 --- a/src/interfaces/ICommon.sol +++ b/src/interfaces/ICommon.sol @@ -2,6 +2,86 @@ pragma solidity ^0.8.23; interface ICommon { + //////////////////////////////////////////////////////////////////////// + // Data Structures + //////////////////////////////////////////////////////////////////////// + /// @dev A struct to hold the intent fields. + /// Since L2s already include calldata compression with savings forwarded to users, + /// we don't need to be too concerned about calldata overhead + struct Intent { + //////////////////////////////////////////////////////////////////////// + // EIP-712 Fields + //////////////////////////////////////////////////////////////////////// + /// @dev The user's address. + address eoa; + /// @dev An encoded array of calls, using ERC7579 batch execution encoding. + /// `abi.encode(calls)`, where `calls` is of type `Call[]`. + /// This allows for more efficient safe forwarding to the EOA. + bytes executionData; + /// @dev Per delegated EOA. + /// This nonce is a 4337-style 2D nonce with some specializations: + /// - Upper 192 bits are used for the `seqKey` (sequence key). + /// The upper 16 bits of the `seqKey` is `MULTICHAIN_NONCE_PREFIX`, + /// then the Intent EIP712 hash will exclude the chain ID. + /// - Lower 64 bits are used for the sequential nonce corresponding to the `seqKey`. + uint256 nonce; + /// @dev The account paying the payment token. + /// If this is `address(0)`, it defaults to the `eoa`. + address payer; + /// @dev The ERC20 or native token used to pay for gas. + address paymentToken; + /// @dev The maximum amount of the token to pay. + uint256 paymentMaxAmount; + /// @dev The combined gas limit for payment, verification, and calling the EOA. + uint256 combinedGas; + /// @dev Optional array of encoded SignedCalls that will be verified and executed + /// before the validation of the overall Intent. + /// A PreCall will NOT have its gas limit or payment applied. + /// The overall Intent's gas limit and payment will be applied, encompassing all its PreCalls. + /// The execution of a PreCall will check and increment the nonce in the PreCall. + /// If at any point, any PreCall cannot be verified to be correct, or fails in execution, + /// the overall Intent will revert before validation, and execute will return a non-zero error. + bytes[] encodedPreCalls; + /// @dev Only relevant for multi chain intents. + /// There should not be any duplicate token addresses. Use address(0) for native token. + /// If native token is used, the first transfer should be the native token transfer. + /// If encodedFundTransfers is not empty, then the intent is considered the output intent. + bytes[] encodedFundTransfers; + /// @dev The settler address. + address settler; + /// @dev The expiry timestamp for the intent. The intent is invalid after this timestamp. + /// If expiry timestamp is set to 0, then expiry is considered to be infinite. + uint256 expiry; + //////////////////////////////////////////////////////////////////////// + // Additional Fields (Not included in EIP-712) + //////////////////////////////////////////////////////////////////////// + /// @dev Whether the intent should use the multichain mode - i.e verify with merkle sigs + /// and send the cross chain message. + bool isMultichain; + /// @dev The funder address. + address funder; + /// @dev The funder signature. + bytes funderSignature; + /// @dev The settler context data to be passed to the settler. + bytes settlerContext; + /// @dev The actual payment amount, requested by the filler. MUST be less than or equal to `paymentMaxAmount` + uint256 paymentAmount; + /// @dev The payment recipient for the ERC20 token. + /// Excluded from signature. The filler can replace this with their own address. + /// This enables multiple fillers, allowing for competitive filling, better uptime. + address paymentRecipient; + /// @dev The wrapped signature. + /// `abi.encodePacked(innerSignature, keyHash, prehash)`. + bytes signature; + /// @dev Optional payment signature to be passed into the `compensate` function + /// on the `payer`. This signature is NOT included in the EIP712 signature. + bytes paymentSignature; + /// @dev Optional. If non-zero, the EOA must use `supportedAccountImplementation`. + /// Otherwise, if left as `address(0)`, any EOA implementation will be supported. + /// This field is NOT included in the EIP712 signature. + address supportedAccountImplementation; + } + /// @dev A struct to hold the fields for a SignedCall. /// A SignedCall is a struct that contains a signed execution batch along with the nonce // and address of the user. diff --git a/src/interfaces/IFunder.sol b/src/interfaces/IFunder.sol index ddbacc0f..90c2547f 100644 --- a/src/interfaces/IFunder.sol +++ b/src/interfaces/IFunder.sol @@ -16,6 +16,9 @@ interface IFunderV4 { interface IFunder is IFunderV4 { /// @dev Checks if fund transfers are valid given a funderSignature. /// @dev Funder implementations must revert if the signature is invalid. - function fund(bytes32 digest, ICommon.Transfer[] memory transfers, bytes memory funderSignature) - external; + function fund( + bytes32 digest, + ICommon.Transfer[] memory transfers, + bytes memory funderSignature + ) external; } diff --git a/src/interfaces/IIthacaAccount.sol b/src/interfaces/IIthacaAccount.sol index 9056db7c..099f466d 100644 --- a/src/interfaces/IIthacaAccount.sol +++ b/src/interfaces/IIthacaAccount.sol @@ -7,23 +7,14 @@ import {ICommon} from "../interfaces/ICommon.sol"; /// @notice Interface for the Account contract interface IIthacaAccount is ICommon { /// @dev Pays `paymentAmount` of `paymentToken` to the `paymentRecipient`. - /// @param paymentAmount The amount to pay /// @param keyHash The hash of the key used to authorize the operation + /// @param encodedIntent The encoded user operation /// @param intentDigest The digest of the user operation - /// @param eoa The EOA address - /// @param payer The payer address - /// @param paymentToken The token to pay with - /// @param paymentRecipient The recipient of the payment - /// @param paymentSignature The payment signature function pay( uint256 paymentAmount, bytes32 keyHash, bytes32 intentDigest, - address eoa, - address payer, - address paymentToken, - address paymentRecipient, - bytes calldata paymentSignature + bytes calldata encodedIntent ) external; /// @dev Returns if the signature is valid, along with its `keyHash`. diff --git a/src/interfaces/IMulticall3.sol b/src/interfaces/IMulticall3.sol new file mode 100644 index 00000000..c0ab86f2 --- /dev/null +++ b/src/interfaces/IMulticall3.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +/// @notice Interface for Multicall3 contract +interface IMulticall3 { + struct Call { + address target; + bytes callData; + } + + struct Call3 { + address target; + bool allowFailure; + bytes callData; + } + + struct Call3Value { + address target; + bool allowFailure; + uint256 value; + bytes callData; + } + + struct Result { + bool success; + bytes returnData; + } + + function aggregate(Call[] calldata calls) + external + payable + returns (uint256 blockNumber, bytes[] memory returnData); + + function aggregate3(Call3[] calldata calls) + external + payable + returns (Result[] memory returnData); +} diff --git a/src/interfaces/IOrchestrator.sol b/src/interfaces/IOrchestrator.sol index 05dfabb4..ffb4de93 100644 --- a/src/interfaces/IOrchestrator.sol +++ b/src/interfaces/IOrchestrator.sol @@ -27,10 +27,11 @@ interface IOrchestrator is ICommon { /// But the balance of tx.origin has to be greater than or equal to uint192.max, to prove that a state override has been made offchain, /// and this is not an onchain call. This mode has been added so that receipt logs can be generated for `eth_simulateV1` /// @return gasUsed The amount of gas used by the execution. (Only returned if `isStateOverride` is true) - function simulateExecute(bytes calldata encodedIntent) - external - payable - returns (uint256 gasUsed); + function simulateExecute( + bool isStateOverride, + uint256 combinedGasOverride, + bytes calldata encodedIntent + ) external payable returns (uint256 gasUsed); /// @dev Allows the orchestrator owner to withdraw tokens. /// @param token The token address (0 for native token) diff --git a/src/libraries/ERC7821Ithaca.sol b/src/libraries/ERC7821Ithaca.sol deleted file mode 100644 index 5f726b42..00000000 --- a/src/libraries/ERC7821Ithaca.sol +++ /dev/null @@ -1,347 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.4; - -import {Receiver} from "solady/accounts/Receiver.sol"; - -/// @notice Minimal batch executor mixin. -/// @author Solady (https://github.com/vectorized/solady/blob/main/src/accounts/ERC7821.sol) -/// -/// @dev This contract can be inherited to create fully-fledged smart accounts. -/// If you merely want to combine approve-swap transactions into a single transaction -/// using [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702), you will need to implement basic -/// [ERC-1271](https://eips.ethereum.org/EIPS/eip-1271) `isValidSignature` functionality to -/// validate signatures with `ecrecover` against the EOA address. This is necessary because some -/// signature checks skip `ecrecover` if the signer has code. For a basic EOA batch executor, -/// please refer to [BEBE](https://github.com/vectorized/bebe), which inherits from this class. -contract ERC7821Ithaca is Receiver { - /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ - /* STRUCTS */ - /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - - /// @dev Call struct for the `execute` function. - struct Call { - address to; // Replaced as `address(this)` if `address(0)`. Renamed to `to` for Ithaca Porto. - uint256 value; // Amount of native currency (i.e. Ether) to send. - bytes data; // Calldata to send with the call. - } - - struct CallSansTo { - uint256 value; // Amount of native currency (i.e. Ether) to send. - bytes data; // Calldata to send with the call. - } - - /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ - /* CUSTOM ERRORS */ - /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - - /// @dev The execution mode is not supported. - error UnsupportedExecutionMode(); - - /// @dev Cannot decode `executionData` as a batch of batches `abi.encode(bytes[])`. - error BatchOfBatchesDecodingError(); - - /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ - /* EXECUTION OPERATIONS */ - /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - - /// @dev Executes the calls in `executionData`. - /// Reverts and bubbles up error if any call fails. - /// - /// `executionData` encoding (single batch): - /// - If `opData` is empty, `executionData` is simply `abi.encode(calls)`. - /// - Else, `executionData` is `abi.encode(calls, opData)`. - /// See: https://eips.ethereum.org/EIPS/eip-7579 - /// - /// `executionData` encoding (batch of batches): - /// - `executionData` is `abi.encode(bytes[])`, where each element in `bytes[]` - /// is an `executionData` for a single batch. - /// - /// Supported modes: - /// - `0x01000000000000000000...`: Single batch. Does not support optional `opData`. - /// - `0x01000000000078210001...`: Single batch. Supports optional `opData`. - /// - `0x01000000000078210002...`: Batch of batches. - /// - `0x01000000000078210003...`: Single batch with common `to` address and optional `opData`. - /// - /// For the "batch of batches" mode, each batch will be recursively passed into - /// `execute` internally with mode `0x01000000000078210001...`. - /// Useful for passing in batches signed by different signers. - /// - /// Authorization checks: - /// - If `opData` is empty, the implementation SHOULD require that - /// `msg.sender == address(this)`. - /// - If `opData` is not empty, the implementation SHOULD use the signature - /// encoded in `opData` to determine if the caller can perform the execution. - /// - If `msg.sender` is an authorized entry point, then `execute` MAY accept - /// calls from the entry point, and MAY use `opData` for specialized logic. - /// - /// `opData` may be used to store additional data for authentication, - /// paymaster data, gas limits, etc. - function execute(bytes32 mode, bytes calldata executionData) public payable virtual { - uint256 id = _executionModeId(mode); - if (id == 3) return _executeBatchOfBatches(mode, executionData); - if (id == 4) return _executeBatchCommonTo(mode, executionData); - Call[] calldata calls; - bytes calldata opData; - - /// @solidity memory-safe-assembly - assembly { - if iszero(id) { - mstore(0x00, 0x7f181275) // `UnsupportedExecutionMode()`. - revert(0x1c, 0x04) - } - // Use inline assembly to extract the calls and optional `opData` efficiently. - opData.length := 0 - let o := add(executionData.offset, calldataload(executionData.offset)) - calls.offset := add(o, 0x20) - calls.length := calldataload(o) - // If the offset of `executionData` allows for `opData`, and the mode supports it. - if gt(eq(id, 2), gt(0x40, calldataload(executionData.offset))) { - let q := add(executionData.offset, calldataload(add(0x20, executionData.offset))) - opData.offset := add(q, 0x20) - opData.length := calldataload(q) - } - // Bounds checking for `executionData` is skipped here for efficiency. - // This is safe if it is only used as an argument to `execute` externally. - // If `executionData` used as an argument to other functions externally, - // please perform the bounds checks via `LibERC7579.decodeBatchAndOpData` - /// or `abi.decode` in the other functions for safety. - } - _execute(mode, executionData, calls, opData); - } - - /// @dev Provided for execution mode support detection. - function supportsExecutionMode(bytes32 mode) public view virtual returns (bool result) { - return _executionModeId(mode) != 0; - } - - /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ - /* INTERNAL HELPERS */ - /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - - /// @dev 0: invalid mode, 1: no `opData` support, 2: with `opData` support, 3: batch of batches. - function _executionModeId(bytes32 mode) internal view virtual returns (uint256 id) { - // Only supports atomic batched executions. - // For the encoding scheme, see: https://eips.ethereum.org/EIPS/eip-7579 - // Bytes Layout: - // - [0] ( 1 byte ) `0x01` for batch call. - // - [1] ( 1 byte ) `0x00` for revert on any failure. - // - [2..5] ( 4 bytes) Reserved by ERC7579 for future standardization. - // - [6..9] ( 4 bytes) `0x00000000` or `0x78210001` or `0x78210002`. - // - [10..31] (22 bytes) Unused. Free for use. - /// @solidity memory-safe-assembly - assembly { - let m := and(shr(mul(22, 8), mode), 0xffff00000000ffffffff) - id := eq(m, 0x01000000000000000000) // 1. - id := or(shl(1, eq(m, 0x01000000000078210001)), id) // 2. - id := or(mul(3, eq(m, 0x01000000000078210002)), id) // 3. - id := or(mul(4, eq(m, 0x01000000000078210003)), id) // 4. - } - } - - /// @dev For execution of a batch of batches with a common `to` address. - /// @dev if to == address(0), it will be replaced with address(this) - /// Execution Data: abi.encode(address to, CallSansTo[] calls, bytes opData) - function _executeBatchCommonTo(bytes32 mode, bytes calldata executionData) internal virtual { - address to; - CallSansTo[] calldata calls; - bytes calldata opData; - - /// @solidity memory-safe-assembly - assembly { - to := calldataload(executionData.offset) - - let callOffset := - add(executionData.offset, calldataload(add(0x20, executionData.offset))) - calls.offset := add(callOffset, 0x20) - calls.length := calldataload(callOffset) - - // This line is needed to ensure that opdata is valid in all code paths. - // Otherwise the compiler complains. - opData.length := 0 - // If the offset of `executionData` allows for `opData`, and the mode supports it. - if gt(calldataload(add(0x20, executionData.offset)), 0x40) { - let opDataOffset := - add(executionData.offset, calldataload(add(0x40, executionData.offset))) - opData.offset := add(opDataOffset, 0x20) - opData.length := calldataload(opDataOffset) - } - } - - _execute(mode, executionData, to, calls, opData); - } - - /// @dev For execution of a batch of batches. - function _executeBatchOfBatches(bytes32 mode, bytes calldata executionData) internal virtual { - // Replace with `0x0100________78210001...` while preserving optional and reserved fields. - mode ^= bytes32(uint256(3 << (22 * 8))); // `2 XOR 3 = 1`. - (uint256 n, uint256 o, uint256 e) = (0, 0, 0); - /// @solidity memory-safe-assembly - assembly { - let j := calldataload(executionData.offset) - let t := add(executionData.offset, j) - n := calldataload(t) // `batches.length`. - o := add(0x20, t) // Offset of `batches[0]`. - e := add(executionData.offset, executionData.length) // End of `executionData`. - // Do the bounds check on `executionData` treating it as `abi.encode(bytes[])`. - // Not too expensive, so we will just do it right here right now. - if or(shr(64, j), or(lt(executionData.length, 0x20), gt(add(o, shl(5, n)), e))) { - mstore(0x00, 0x3995943b) // `BatchOfBatchesDecodingError()`. - revert(0x1c, 0x04) - } - } - unchecked { - for (uint256 i; i != n; ++i) { - bytes calldata batch; - /// @solidity memory-safe-assembly - assembly { - let j := calldataload(add(o, shl(5, i))) - let t := add(o, j) - batch.offset := add(t, 0x20) - batch.length := calldataload(t) - // Validate that `batches[i]` is not out-of-bounds. - if or(shr(64, j), gt(add(batch.offset, batch.length), e)) { - mstore(0x00, 0x3995943b) // `BatchOfBatchesDecodingError()`. - revert(0x1c, 0x04) - } - } - execute(mode, batch); - } - } - } - - /// @dev Executes the calls. - /// Reverts and bubbles up error if any call fails. - /// The `mode` and `executionData` are passed along in case there's a need to use them. - function _execute( - bytes32 mode, - bytes calldata executionData, - Call[] calldata calls, - bytes calldata opData - ) internal virtual { - // Silence compiler warning on unused variables. - mode = mode; - executionData = executionData; - // Very basic auth to only allow this contract to be called by itself. - // Override this function to perform more complex auth with `opData`. - if (opData.length == uint256(0)) { - require(msg.sender == address(this)); - // Remember to return `_execute(calls, extraData)` when you override this function. - return _execute(calls, bytes32(0)); - } - revert(); // In your override, replace this with logic to operate on `opData`. - } - - /// @dev Executes the calls. - /// Reverts and bubbles up error if any call fails. - /// The `mode` and `executionData` are passed along in case there's a need to use them. - function _execute( - bytes32 mode, - bytes calldata executionData, - address to, - CallSansTo[] calldata calls, - bytes calldata opData - ) internal virtual { - // Silence compiler warning on unused variables. - mode = mode; - executionData = executionData; - // Very basic auth to only allow this contract to be called by itself. - // Override this function to perform more complex auth with `opData`. - if (opData.length == uint256(0)) { - require(msg.sender == address(this)); - // Remember to return `_execute(calls, extraData)` when you override this function. - return _execute(calls, to, bytes32(0)); - } - revert(); // In your override, replace this with logic to operate on `opData`. - } - - /// @dev Executes the calls. - /// Reverts and bubbles up error if any call fails. - /// `extraData` can be any supplementary data (e.g. a memory pointer, some hash). - function _execute(Call[] calldata calls, bytes32 extraData) internal virtual { - unchecked { - uint256 i; - if (calls.length == uint256(0)) return; - do { - (address to, uint256 value, bytes calldata data) = _get(calls, i); - _execute(to, value, data, extraData); - } while (++i != calls.length); - } - } - - /// @dev Executes the calls. - /// Reverts and bubbles up error if any call fails. - /// `extraData` can be any supplementary data (e.g. a memory pointer, some hash). - function _execute(CallSansTo[] calldata calls, address to, bytes32 keyHash) internal virtual { - unchecked { - uint256 i; - // If `to` is address(0), it will be replaced with address(this) - /// @solidity memory-safe-assembly - assembly { - let t := shr(96, shl(96, to)) - to := or(mul(address(), iszero(t)), t) - } - if (calls.length == uint256(0)) return; - do { - (uint256 value, bytes calldata data) = _get(calls, i); - _execute(to, value, data, keyHash); - } while (++i != calls.length); - } - } - - /// @dev Executes the call. - /// Reverts and bubbles up error if any call fails. - /// `extraData` can be any supplementary data (e.g. a memory pointer, some hash). - function _execute(address to, uint256 value, bytes calldata data, bytes32 extraData) - internal - virtual - { - /// @solidity memory-safe-assembly - assembly { - extraData := extraData // Silence unused variable compiler warning. - let m := mload(0x40) // Grab the free memory pointer. - calldatacopy(m, data.offset, data.length) - if iszero(call(gas(), to, value, m, data.length, codesize(), 0x00)) { - // Bubble up the revert if the call reverts. - returndatacopy(m, 0x00, returndatasize()) - revert(m, returndatasize()) - } - } - } - - /// @dev Convenience function for getting `calls[i]`, without bounds checks. - function _get(CallSansTo[] calldata calls, uint256 i) - internal - view - virtual - returns (uint256 value, bytes calldata data) - { - /// @solidity memory-safe-assembly - assembly { - let c := add(calls.offset, calldataload(add(calls.offset, shl(5, i)))) - value := calldataload(c) - let o := add(c, calldataload(add(c, 0x20))) - data.offset := add(o, 0x20) - data.length := calldataload(o) - } - } - - /// @dev Convenience function for getting `calls[i]`, without bounds checks. - function _get(Call[] calldata calls, uint256 i) - internal - view - virtual - returns (address to, uint256 value, bytes calldata data) - { - /// @solidity memory-safe-assembly - assembly { - let c := add(calls.offset, calldataload(add(calls.offset, shl(5, i)))) - // Replaces `to` with `address(this)` if `address(0)` is provided. - let t := shr(96, shl(96, calldataload(c))) - to := or(mul(address(), iszero(t)), t) - value := calldataload(add(c, 0x20)) - let o := add(c, calldataload(add(c, 0x40))) - data.offset := add(o, 0x20) - data.length := calldataload(o) - } - } -} diff --git a/src/libraries/IntentHelpers.sol b/src/libraries/IntentHelpers.sol deleted file mode 100644 index fc4f2722..00000000 --- a/src/libraries/IntentHelpers.sol +++ /dev/null @@ -1,158 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {ICommon} from "../interfaces/ICommon.sol"; - -contract IntentHelpers { - /** - * The intent's layout in calldata: - * bytes4 fnSel // [0:4] - * uint256 offset; // [4:36] - * uint256 length; // [36:68] - * address supportedAccountImplementation; - * address eoa; - * uint256 nonce; - * address payer; - * address paymentToken; - * uint256 paymentMaxAmount; - * uint256 combinedGas; - * uint256 expiry; - * uint256 paymentAmount; - * address paymentRecipient; - * uint256 executionData.length - * bytes executionData; - * uint256 fundData.length - * bytes fundData; // abi.encode(funder, funderSignature, encodedFundTransfers) - * uint256 encodedPreCalls.length - * bytes encodedPreCalls; // abi.encode(bytes[]) - * uint256 signature.length - * bytes signature; - * uint256 settlerData.length - * bytes settlerData; // abi.encode(settler, settlerContext). To use settler, nonce needs to have `MERKLE_VERIFICATION` prefix - * uint256 paymentSignature.length - * bytes paymentSignature; - */ - uint256 internal constant _SUPPORTED_ACCOUNT_IMPLEMENTATION_OFFSET = 68; - uint256 internal constant _EOA_OFFSET = 88; - uint256 internal constant _NONCE_OFFSET = 120; - uint256 internal constant _PAYER_OFFSET = 152; - uint256 internal constant _PAYMENT_TOKEN_OFFSET = 184; - uint256 internal constant _PAYMENT_MAX_AMOUNT_OFFSET = 216; - uint256 internal constant _COMBINED_GAS_OFFSET = 248; - uint256 internal constant _EXPIRY_OFFSET = 280; - uint256 internal constant _PAYMENT_AMOUNT_OFFSET = 312; - uint256 internal constant _PAYMENT_RECIPIENT_OFFSET = 344; - uint256 internal constant _EXECUTION_DATA_OFFSET = 364; - - struct CalldataPointer { - uint256 offset; - } - - function _getSupportedAccountImplementation() internal pure returns (address) { - return address( - bytes20( - msg.data[ - _SUPPORTED_ACCOUNT_IMPLEMENTATION_OFFSET: - _SUPPORTED_ACCOUNT_IMPLEMENTATION_OFFSET + 20 - ] - ) - ); - } - - function _getEoa() internal pure returns (address a) { - assembly ("memory-safe") { - a := calldataload(_EOA_OFFSET) - } - } - - function _getNonce() internal pure returns (uint256 a) { - assembly ("memory-safe") { - a := calldataload(_NONCE_OFFSET) - } - } - - function _getPayer() internal pure returns (address a) { - assembly ("memory-safe") { - a := calldataload(_PAYER_OFFSET) - } - } - - function _getPaymentToken() internal pure returns (address a) { - assembly ("memory-safe") { - a := calldataload(_PAYMENT_TOKEN_OFFSET) - } - } - - function _getPaymentMaxAmount() internal pure returns (uint256 a) { - assembly ("memory-safe") { - a := calldataload(_PAYMENT_MAX_AMOUNT_OFFSET) - } - } - - function _getCombinedGas() internal pure returns (uint256 a) { - assembly ("memory-safe") { - a := calldataload(_COMBINED_GAS_OFFSET) - } - } - - function _getExpiry() internal pure returns (uint256 a) { - assembly ("memory-safe") { - a := calldataload(_EXPIRY_OFFSET) - } - } - - function _getPaymentAmount() internal pure returns (uint256 a) { - assembly ("memory-safe") { - a := calldataload(_PAYMENT_AMOUNT_OFFSET) - } - } - - function _getPaymentRecipient() internal pure returns (address a) { - return address(bytes20(msg.data[_PAYMENT_RECIPIENT_OFFSET:_PAYMENT_RECIPIENT_OFFSET + 20])); - } - - function _getExecutionData() internal pure returns (bytes calldata data) { - assembly ("memory-safe") { - data.length := calldataload(_EXECUTION_DATA_OFFSET) - data.offset := add(_EXECUTION_DATA_OFFSET, 0x20) - } - } - - /// @dev Splits data containing [uint256(len), bytes(data), ...] into [uint256(len), bytes(data)] and [...] - /// @dev Modifies the memory pointer to point to the next - function _getNextBytes(CalldataPointer memory p) - internal - pure - returns (bytes calldata returnData) - { - uint256 o = p.offset; - assembly ("memory-safe") { - returnData.length := calldataload(o) - returnData.offset := add(o, 0x20) - mstore(p, add(o, add(returnData.length, 0x20))) - } - } - - function _parseFundData(bytes calldata data) - internal - pure - returns (address funder, bytes calldata sig, bytes[] calldata transfers) - { - // fundData = abi.encode(funder, funderSignature, encodedFundTransfers) - // This gives a calldata layout of: - // 0x00: funder (20 bytes, left padded to 32 bytes) - // 0x20: offset to funderSignature (32 bytes) - we skip this and assume its starts at 0x60 - // 0x40: offset to encodedFundTransfers (32 bytes) - we skip this and assume it starts at 0x60+funderSignature.length+0x20 - // 0x60: funderSignature length (32 bytes) - // ... - assembly ("memory-safe") { - funder := calldataload(data.offset) - sig.offset := add(data.offset, 0x80) - sig.length := calldataload(add(data.offset, 0x60)) - // transfers array starts after sig, but needs to account for padding - let sigLenWords := mul(div(add(sig.length, 31), 32), 32) - transfers.length := calldataload(add(sig.offset, sigLenWords)) - transfers.offset := add(add(sig.offset, sigLenWords), 0x20) - } - } -} diff --git a/src/vendor/layerzero/interfaces/IOAppCore.sol b/src/vendor/layerzero/interfaces/IOAppCore.sol new file mode 100644 index 00000000..4a87d7a2 --- /dev/null +++ b/src/vendor/layerzero/interfaces/IOAppCore.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import { + ILayerZeroEndpointV2 +} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; + +/** + * @title IOAppCore + */ +interface IOAppCore { + // Custom error messages + error OnlyPeer(uint32 eid, bytes32 sender); + error NoPeer(uint32 eid); + error InvalidEndpointCall(); + error InvalidDelegate(); + + // Event emitted when a peer (OApp) is set for a corresponding endpoint + event PeerSet(uint32 eid, bytes32 peer); + + /** + * @notice Retrieves the OApp version information. + * @return senderVersion The version of the OAppSender.sol contract. + * @return receiverVersion The version of the OAppReceiver.sol contract. + */ + function oAppVersion() external view returns (uint64 senderVersion, uint64 receiverVersion); + + /** + * @notice Retrieves the LayerZero endpoint associated with the OApp. + * @return iEndpoint The LayerZero endpoint as an interface. + */ + function endpoint() external view returns (ILayerZeroEndpointV2 iEndpoint); + + /** + * @notice Retrieves the peer (OApp) associated with a corresponding endpoint. + * @param _eid The endpoint ID. + * @return peer The peer address (OApp instance) associated with the corresponding endpoint. + */ + function peers(uint32 _eid) external view returns (bytes32 peer); + + /** + * @notice Sets the peer address (OApp instance) for a corresponding endpoint. + * @param _eid The endpoint ID. + * @param _peer The address of the peer to be associated with the corresponding endpoint. + */ + function setPeer(uint32 _eid, bytes32 _peer) external; + + /** + * @notice Sets the delegate address for the OApp Core. + * @param _delegate The address of the delegate to be set. + */ + function setDelegate(address _delegate) external; +} diff --git a/src/vendor/layerzero/interfaces/IOAppMsgInspector.sol b/src/vendor/layerzero/interfaces/IOAppMsgInspector.sol new file mode 100644 index 00000000..4d3c82d6 --- /dev/null +++ b/src/vendor/layerzero/interfaces/IOAppMsgInspector.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +/** + * @title IOAppMsgInspector + * @dev Interface for the OApp Message Inspector, allowing examination of message and options contents. + */ +interface IOAppMsgInspector { + // Custom error message for inspection failure + error InspectionFailed(bytes message, bytes options); + + /** + * @notice Allows the inspector to examine LayerZero message contents and optionally throw a revert if invalid. + * @param _message The message payload to be inspected. + * @param _options Additional options or parameters for inspection. + * @return valid A boolean indicating whether the inspection passed (true) or failed (false). + * + * @dev Optionally done as a revert, OR use the boolean provided to handle the failure. + */ + function inspect(bytes calldata _message, bytes calldata _options) + external + view + returns (bool valid); +} diff --git a/src/vendor/layerzero/interfaces/IOAppReceiver.sol b/src/vendor/layerzero/interfaces/IOAppReceiver.sol new file mode 100644 index 00000000..0fe4317f --- /dev/null +++ b/src/vendor/layerzero/interfaces/IOAppReceiver.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { + ILayerZeroReceiver, + Origin +} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroReceiver.sol"; + +interface IOAppReceiver is ILayerZeroReceiver { + /** + * @notice Indicates whether an address is an approved composeMsg sender to the Endpoint. + * @param _origin The origin information containing the source endpoint and sender address. + * - srcEid: The source chain endpoint ID. + * - sender: The sender address on the src chain. + * - nonce: The nonce of the message. + * @param _message The lzReceive payload. + * @param _sender The sender address. + * @return isSender Is a valid sender. + * + * @dev Applications can optionally choose to implement a separate composeMsg sender that is NOT the bridging layer. + * @dev The default sender IS the OAppReceiver implementer. + */ + function isComposeMsgSender(Origin calldata _origin, bytes calldata _message, address _sender) + external + view + returns (bool isSender); +} diff --git a/src/vendor/layerzero/mocks/EndpointV2Mock.sol b/src/vendor/layerzero/mocks/EndpointV2Mock.sol new file mode 100644 index 00000000..40042d7d --- /dev/null +++ b/src/vendor/layerzero/mocks/EndpointV2Mock.sol @@ -0,0 +1,418 @@ +// SPDX-License-Identifier: LZBL-1.2 + +pragma solidity ^0.8.20; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +// @dev oz4/5 breaking change... Ownable constructor +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +import { + MessagingFee, + MessagingParams, + MessagingReceipt, + Origin, + ILayerZeroEndpointV2 +} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import { + ISendLib, + Packet +} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ISendLib.sol"; +import { + ILayerZeroReceiver +} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroReceiver.sol"; +import {Errors} from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/Errors.sol"; +import {GUID} from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/GUID.sol"; +import {Transfer} from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/Transfer.sol"; +import {MessagingChannel} from "@layerzerolabs/lz-evm-protocol-v2/contracts/MessagingChannel.sol"; +import {MessagingComposer} from "@layerzerolabs/lz-evm-protocol-v2/contracts/MessagingComposer.sol"; +import {MessageLibManager} from "@layerzerolabs/lz-evm-protocol-v2/contracts/MessageLibManager.sol"; +import {MessagingContext} from "@layerzerolabs/lz-evm-protocol-v2/contracts/MessagingContext.sol"; + +// LayerZero EndpointV2 is fully backward compatible with LayerZero Endpoint(V1), but it also supports additional +// features that Endpoint(V1) does not support now and may not in the future. We have also changed some terminology +// to clarify pre-existing language that might have been confusing. +// +// The following is a list of terminology changes: +// -chainId -> eid +// - Rationale: chainId was a term we initially used to describe an endpoint on a specific chain. Since +// LayerZero supports non-EVMs we could not map the classic EVM chainIds to the LayerZero chainIds, making it +// confusing for developers. With the addition of EndpointV2 and its backward compatible nature, we would have +// two chainIds per chain that has Endpoint(V1), further confusing developers. We have decided to change the +// name to Endpoint Id, or eid, for simplicity and clarity. +// -adapterParams -> options +// -userApplication -> oapp. Omnichain Application +// -srcAddress -> sender +// -dstAddress -> receiver +// - Rationale: The sender/receiver on EVM is the address. However, on non-EVM chains, the sender/receiver could +// represented as a public key, or some other identifier. The term sender/receiver is more generic +// -payload -> message. +// - Rationale: The term payload is used in the context of a packet, which is a combination of the message and GUID +contract EndpointV2Mock is + ILayerZeroEndpointV2, + MessagingChannel, + MessageLibManager, + MessagingComposer, + MessagingContext +{ + address public lzToken; + + mapping(address oapp => address delegate) public delegates; + + /// @param _eid the unique Endpoint Id for this deploy that all other Endpoints can use to send to it + // @dev oz4/5 breaking change... Ownable constructor + constructor(uint32 _eid, address _owner) Ownable(_owner) MessagingChannel(_eid) {} + + /// @dev MESSAGING STEP 0 + /// @notice This view function gives the application built on top of LayerZero the ability to requests a quote + /// with the same parameters as they would to send their message. Since the quotes are given on chain there is a + /// race condition in which the prices could change between the time the user gets their quote and the time they + /// submit their message. If the price moves up and the user doesn't send enough funds the transaction will revert, + /// if the price goes down the _refundAddress provided by the app will be refunded the difference. + /// @param _params the messaging parameters + /// @param _sender the sender of the message + function quote(MessagingParams calldata _params, address _sender) + external + view + returns (MessagingFee memory) + { + // lzToken must be set to support payInLzToken + if (_params.payInLzToken && lzToken == address(0x0)) revert Errors.LZ_LzTokenUnavailable(); + + // get the correct outbound nonce + uint64 nonce = outboundNonce[_sender][_params.dstEid][_params.receiver] + 1; + + // construct the packet with a GUID + Packet memory packet = Packet({ + nonce: nonce, + srcEid: eid, + sender: _sender, + dstEid: _params.dstEid, + receiver: _params.receiver, + guid: GUID.generate(nonce, eid, _sender, _params.dstEid, _params.receiver), + message: _params.message + }); + + // get the send library by sender and dst eid + // use _ to avoid variable shadowing + address _sendLibrary = getSendLibrary(_sender, _params.dstEid); + + return ISendLib(_sendLibrary).quote(packet, _params.options, _params.payInLzToken); + } + + /// @dev MESSAGING STEP 1 - OApp need to transfer the fees to the endpoint before sending the message + /// @param _params the messaging parameters + /// @param _refundAddress the address to refund both the native and lzToken + function send(MessagingParams calldata _params, address _refundAddress) + external + payable + sendContext(_params.dstEid, msg.sender) + returns (MessagingReceipt memory) + { + if (_params.payInLzToken && lzToken == address(0x0)) { + revert Errors.LZ_LzTokenUnavailable(); + } + + // send message + (MessagingReceipt memory receipt, address _sendLibrary) = _send(msg.sender, _params); + + // OApp can simulate with 0 native value it will fail with error including the required fee, which can be provided in the actual call + // this trick can be used to avoid the need to write the quote() function + // however, without the quote view function it will be hard to compose an oapp on chain + uint256 suppliedNative = _suppliedNative(); + uint256 suppliedLzToken = _suppliedLzToken(_params.payInLzToken); + _assertMessagingFee(receipt.fee, suppliedNative, suppliedLzToken); + + // handle lz token fees + _payToken(lzToken, receipt.fee.lzTokenFee, suppliedLzToken, _sendLibrary, _refundAddress); + + // handle native fees + _payNative(receipt.fee.nativeFee, suppliedNative, _sendLibrary, _refundAddress); + + return receipt; + } + + /// @dev internal function for sending the messages used by all external send methods + /// @param _sender the address of the application sending the message to the destination chain + /// @param _params the messaging parameters + function _send(address _sender, MessagingParams calldata _params) + internal + returns (MessagingReceipt memory, address) + { + // get the correct outbound nonce + uint64 latestNonce = _outbound(_sender, _params.dstEid, _params.receiver); + + // construct the packet with a GUID + Packet memory packet = Packet({ + nonce: latestNonce, + srcEid: eid, + sender: _sender, + dstEid: _params.dstEid, + receiver: _params.receiver, + guid: GUID.generate(latestNonce, eid, _sender, _params.dstEid, _params.receiver), + message: _params.message + }); + + // get the send library by sender and dst eid + address _sendLibrary = getSendLibrary(_sender, _params.dstEid); + + // messageLib always returns encodedPacket with guid + (MessagingFee memory fee, bytes memory encodedPacket) = + ISendLib(_sendLibrary).send(packet, _params.options, _params.payInLzToken); + + // Emit packet information for DVNs, Executors, and any other offchain infrastructure to only listen + // for this one event to perform their actions. + emit PacketSent(encodedPacket, _params.options, _sendLibrary); + + return (MessagingReceipt(packet.guid, latestNonce, fee), _sendLibrary); + } + + /// @dev MESSAGING STEP 2 - on the destination chain + /// @dev configured receive library verifies a message + /// @param _origin a struct holding the srcEid, nonce, and sender of the message + /// @param _receiver the receiver of the message + /// @param _payloadHash the payload hash of the message + function verify(Origin calldata _origin, address _receiver, bytes32 _payloadHash) external { + if (!isValidReceiveLibrary(_receiver, _origin.srcEid, msg.sender)) { + revert Errors.LZ_InvalidReceiveLibrary(); + } + + uint64 lazyNonce = lazyInboundNonce[_receiver][_origin.srcEid][_origin.sender]; + if (!_initializable(_origin, _receiver, lazyNonce)) { + revert Errors.LZ_PathNotInitializable(); + } + if (!_verifiable(_origin, _receiver, lazyNonce)) revert Errors.LZ_PathNotVerifiable(); + + // insert the message into the message channel + _inbound(_receiver, _origin.srcEid, _origin.sender, _origin.nonce, _payloadHash); + emit PacketVerified(_origin, _receiver, _payloadHash); + } + + /// @dev MESSAGING STEP 3 - the last step + /// @dev execute a verified message to the designated receiver + /// @dev the execution provides the execution context (caller, extraData) to the receiver. the receiver can optionally assert the caller and validate the untrusted extraData + /// @dev cant reentrant because the payload is cleared before execution + /// @param _origin the origin of the message + /// @param _receiver the receiver of the message + /// @param _guid the guid of the message + /// @param _message the message + /// @param _extraData the extra data provided by the executor. this data is untrusted and should be validated. + function lzReceive( + Origin calldata _origin, + address _receiver, + bytes32 _guid, + bytes calldata _message, + bytes calldata _extraData + ) external payable { + // clear the payload first to prevent reentrancy, and then execute the message + _clearPayload( + _receiver, + _origin.srcEid, + _origin.sender, + _origin.nonce, + abi.encodePacked(_guid, _message) + ); + ILayerZeroReceiver(_receiver) + .lzReceive{value: msg.value}(_origin, _guid, _message, msg.sender, _extraData); + emit PacketDelivered(_origin, _receiver); + } + + /// @param _origin the origin of the message + /// @param _receiver the receiver of the message + /// @param _guid the guid of the message + /// @param _message the message + /// @param _extraData the extra data provided by the executor. + /// @param _reason the reason for failure + function lzReceiveAlert( + Origin calldata _origin, + address _receiver, + bytes32 _guid, + uint256 _gas, + uint256 _value, + bytes calldata _message, + bytes calldata _extraData, + bytes calldata _reason + ) external { + emit LzReceiveAlert( + _receiver, msg.sender, _origin, _guid, _gas, _value, _message, _extraData, _reason + ); + } + + /// @dev Oapp uses this interface to clear a message. + /// @dev this is a PULL mode versus the PUSH mode of lzReceive + /// @dev the cleared message can be ignored by the app (effectively burnt) + /// @dev authenticated by oapp + /// @param _origin the origin of the message + /// @param _guid the guid of the message + /// @param _message the message + function clear(address _oapp, Origin calldata _origin, bytes32 _guid, bytes calldata _message) + external + { + _assertAuthorized(_oapp); + + bytes memory payload = abi.encodePacked(_guid, _message); + _clearPayload(_oapp, _origin.srcEid, _origin.sender, _origin.nonce, payload); + emit PacketDelivered(_origin, _oapp); + } + + /// @dev allows reconfiguration to recover from wrong configurations + /// @dev users should never approve the EndpointV2 contract to spend their non-layerzero tokens + /// @dev override this function if the endpoint is charging ERC20 tokens as native + /// @dev only owner + /// @param _lzToken the new layer zero token address + function setLzToken(address _lzToken) public virtual onlyOwner { + lzToken = _lzToken; + emit LzTokenSet(_lzToken); + } + + /// @dev recover the token sent to this contract by mistake + /// @dev only owner + /// @param _token the token to recover. if 0x0 then it is native token + /// @param _to the address to send the token to + /// @param _amount the amount to send + function recoverToken(address _token, address _to, uint256 _amount) external onlyOwner { + Transfer.nativeOrToken(_token, _to, _amount); + } + + /// @dev handling token payments on endpoint. the sender must approve the endpoint to spend the token + /// @dev internal function + /// @param _token the token to pay + /// @param _required the amount required + /// @param _supplied the amount supplied + /// @param _receiver the receiver of the token + function _payToken( + address _token, + uint256 _required, + uint256 _supplied, + address _receiver, + address _refundAddress + ) internal { + if (_required > 0) { + Transfer.token(_token, _receiver, _required); + } + if (_required < _supplied) { + unchecked { + // refund the excess + Transfer.token(_token, _refundAddress, _supplied - _required); + } + } + } + + /// @dev handling native token payments on endpoint + /// @dev override this if the endpoint is charging ERC20 tokens as native + /// @dev internal function + /// @param _required the amount required + /// @param _supplied the amount supplied + /// @param _receiver the receiver of the native token + /// @param _refundAddress the address to refund the excess to + function _payNative( + uint256 _required, + uint256 _supplied, + address _receiver, + address _refundAddress + ) internal virtual { + if (_required > 0) { + Transfer.native(_receiver, _required); + } + if (_required < _supplied) { + unchecked { + // refund the excess + Transfer.native(_refundAddress, _supplied - _required); + } + } + } + + /// @dev get the balance of the lzToken as the supplied lzToken fee if payInLzToken is true + function _suppliedLzToken(bool _payInLzToken) internal view returns (uint256 supplied) { + if (_payInLzToken) { + supplied = IERC20(lzToken).balanceOf(address(this)); + + // if payInLzToken is true, the supplied fee must be greater than 0 to prevent a race condition + // in which an oapp sending a message with lz token and the lz token is set to a new token between the tx + // being sent and the tx being mined. if the required lz token fee is 0 and the old lz token would be + // locked in the contract instead of being refunded + if (supplied == 0) revert Errors.LZ_ZeroLzTokenFee(); + } + } + + /// @dev override this if the endpoint is charging ERC20 tokens as native + function _suppliedNative() internal view virtual returns (uint256) { + return msg.value; + } + + /// @dev Assert the required fees and the supplied fees are enough + function _assertMessagingFee( + MessagingFee memory _required, + uint256 _suppliedNativeFee, + uint256 _suppliedLzTokenFee + ) internal pure { + if (_required.nativeFee > _suppliedNativeFee || _required.lzTokenFee > _suppliedLzTokenFee) + { + revert Errors.LZ_InsufficientFee( + _required.nativeFee, _suppliedNativeFee, _required.lzTokenFee, _suppliedLzTokenFee + ); + } + } + + /// @dev override this if the endpoint is charging ERC20 tokens as native + /// @return 0x0 if using native. otherwise the address of the native ERC20 token + function nativeToken() external view virtual returns (address) { + return address(0x0); + } + + /// @notice delegate is authorized by the oapp to configure anything in layerzero + function setDelegate(address _delegate) external { + delegates[msg.sender] = _delegate; + emit DelegateSet(msg.sender, _delegate); + } + + // ========================= Internal ========================= + function _initializable(Origin calldata _origin, address _receiver, uint64 _lazyInboundNonce) + internal + view + returns (bool) + { + return _lazyInboundNonce > 0 // allowInitializePath already checked + || ILayerZeroReceiver(_receiver).allowInitializePath(_origin); + } + + /// @dev bytes(0) payloadHash can never be submitted + function _verifiable(Origin calldata _origin, address _receiver, uint64 _lazyInboundNonce) + internal + view + returns (bool) + { + return _origin.nonce > _lazyInboundNonce // either initializing an empty slot or reverifying + || inboundPayloadHash[_receiver][_origin.srcEid][_origin.sender][_origin.nonce] + != EMPTY_PAYLOAD_HASH; // only allow reverifying if it hasn't been executed + } + + /// @dev assert the caller to either be the oapp or the delegate + function _assertAuthorized(address _oapp) + internal + view + override(MessagingChannel, MessageLibManager) + { + if (msg.sender != _oapp && msg.sender != delegates[_oapp]) { + revert Errors.LZ_Unauthorized(); + } + } + + // ========================= VIEW FUNCTIONS FOR OFFCHAIN ONLY ========================= + // Not involved in any state transition function. + // ==================================================================================== + function initializable(Origin calldata _origin, address _receiver) + external + view + returns (bool) + { + return _initializable( + _origin, _receiver, lazyInboundNonce[_receiver][_origin.srcEid][_origin.sender] + ); + } + + function verifiable(Origin calldata _origin, address _receiver) external view returns (bool) { + return _verifiable( + _origin, _receiver, lazyInboundNonce[_receiver][_origin.srcEid][_origin.sender] + ); + } +} diff --git a/src/vendor/layerzero/oapp/OApp.sol b/src/vendor/layerzero/oapp/OApp.sol new file mode 100644 index 00000000..a4d9576a --- /dev/null +++ b/src/vendor/layerzero/oapp/OApp.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +// @dev Import the 'MessagingFee' and 'MessagingReceipt' so it's exposed to OApp implementers +// solhint-disable-next-line no-unused-import +import {OAppSender, MessagingFee, MessagingReceipt} from "./OAppSender.sol"; +// @dev Import the 'Origin' so it's exposed to OApp implementers +// solhint-disable-next-line no-unused-import +import {OAppReceiver, Origin} from "./OAppReceiver.sol"; +import {OAppCore} from "./OAppCore.sol"; + +/** + * @title OApp + * @dev Abstract contract serving as the base for OApp implementation, combining OAppSender and OAppReceiver functionality. + */ +abstract contract OApp is OAppSender, OAppReceiver { + /** + * @dev Constructor to initialize the OApp with the provided owner. + * @param _delegate The delegate capable of making OApp configurations inside of the endpoint. + */ + constructor(address _delegate) OAppCore(_delegate) {} + + /** + * @notice Retrieves the OApp version information. + * @return senderVersion The version of the OAppSender.sol implementation. + * @return receiverVersion The version of the OAppReceiver.sol implementation. + */ + function oAppVersion() + public + pure + virtual + override(OAppSender, OAppReceiver) + returns (uint64 senderVersion, uint64 receiverVersion) + { + return (SENDER_VERSION, RECEIVER_VERSION); + } +} diff --git a/src/vendor/layerzero/oapp/OAppCore.sol b/src/vendor/layerzero/oapp/OAppCore.sol new file mode 100644 index 00000000..3eff1be5 --- /dev/null +++ b/src/vendor/layerzero/oapp/OAppCore.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IOAppCore, ILayerZeroEndpointV2} from "../interfaces/IOAppCore.sol"; + +/** + * @title OAppCore + * @dev Abstract contract implementing the IOAppCore interface with basic OApp configurations. + */ +abstract contract OAppCore is IOAppCore, Ownable { + // The LayerZero endpoint associated with the given OApp + ILayerZeroEndpointV2 public endpoint; + + // Mapping to store peers associated with corresponding endpoints + mapping(uint32 eid => bytes32 peer) internal _peers; + + /** + * @dev Constructor to initialize the OAppCore with the provided delegate. + * @param _delegate The delegate capable of making OApp configurations inside of the endpoint. + * + * @dev The delegate typically should be set as the owner of the contract. + */ + constructor(address _delegate) { + if (_delegate == address(0)) revert InvalidDelegate(); + _transferOwnership(_delegate); + } + + /** + * @notice Sets the LayerZero endpoint for this OApp. + * @param _endpoint The address of the LayerZero endpoint. + * @dev Can only be called by the owner. + * @dev Should be called immediately after deployment. + * Delegate is set to the owner of the OAppCore contract. + */ + function setEndpoint(address _endpoint) external onlyOwner { + if (_endpoint == address(0)) revert InvalidDelegate(); + endpoint = ILayerZeroEndpointV2(_endpoint); + endpoint.setDelegate(msg.sender); + } + + /** + * @notice Sets the peer address (OApp instance) for a corresponding endpoint. + * @param _eid The endpoint ID. + * @param _peer The address of the peer to be associated with the corresponding endpoint. + * + * @dev Only the owner/admin of the OApp can call this function. + * @dev Indicates that the peer is trusted to send LayerZero messages to this OApp. + * @dev Set this to bytes32(0) to remove the peer address. + * @dev Peer is a bytes32 to accommodate non-evm chains. + */ + function setPeer(uint32 _eid, bytes32 _peer) public virtual onlyOwner { + _setPeer(_eid, _peer); + } + + /** + * @notice Sets the peer address (OApp instance) for a corresponding endpoint. + * @param _eid The endpoint ID. + * @param _peer The address of the peer to be associated with the corresponding endpoint. + * + * @dev Indicates that the peer is trusted to send LayerZero messages to this OApp. + * @dev Set this to bytes32(0) to remove the peer address. + * @dev Peer is a bytes32 to accommodate non-evm chains. + */ + function _setPeer(uint32 _eid, bytes32 _peer) internal virtual { + _peers[_eid] = _peer; + emit PeerSet(_eid, _peer); + } + + /** + * @notice Internal function to get the peer address associated with a specific endpoint; reverts if NOT set. + * ie. the peer is set to bytes32(0). + * @param _eid The endpoint ID. + * @return peer The address of the peer associated with the specified endpoint. + */ + function _getPeerOrRevert(uint32 _eid) internal view virtual returns (bytes32) { + bytes32 peer = peers(_eid); + if (peer == bytes32(0)) revert NoPeer(_eid); + return peer; + } + + /** + * @notice Retrieves the peer (OApp instance) for a corresponding endpoint. + * @param _eid The endpoint ID. + * @return peer The address of the peer associated with the specified endpoint. + * @dev This function is virtual to allow overriding in derived contracts. + */ + function peers(uint32 _eid) public view virtual returns (bytes32 peer) { + return _peers[_eid]; + } + + /** + * @notice Sets the delegate address for the OApp. + * @param _delegate The address of the delegate to be set. + * + * @dev Only the owner/admin of the OApp can call this function. + * @dev Provides the ability for a delegate to set configs, on behalf of the OApp, directly on the Endpoint contract. + */ + function setDelegate(address _delegate) public onlyOwner { + if (address(endpoint) == address(0)) revert InvalidDelegate(); + endpoint.setDelegate(_delegate); + } +} diff --git a/src/vendor/layerzero/oapp/OAppReceiver.sol b/src/vendor/layerzero/oapp/OAppReceiver.sol new file mode 100644 index 00000000..d3c34ad0 --- /dev/null +++ b/src/vendor/layerzero/oapp/OAppReceiver.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IOAppReceiver, Origin} from "../interfaces/IOAppReceiver.sol"; +import {OAppCore} from "./OAppCore.sol"; + +/** + * @title OAppReceiver + * @dev Abstract contract implementing the ILayerZeroReceiver interface and extending OAppCore for OApp receivers. + */ +abstract contract OAppReceiver is IOAppReceiver, OAppCore { + // Custom error message for when the caller is not the registered endpoint/ + error OnlyEndpoint(address addr); + + // @dev The version of the OAppReceiver implementation. + // @dev Version is bumped when changes are made to this contract. + uint64 internal constant RECEIVER_VERSION = 2; + + /** + * @notice Retrieves the OApp version information. + * @return senderVersion The version of the OAppSender.sol contract. + * @return receiverVersion The version of the OAppReceiver.sol contract. + * + * @dev Providing 0 as the default for OAppSender version. Indicates that the OAppSender is not implemented. + * ie. this is a RECEIVE only OApp. + * @dev If the OApp uses both OAppSender and OAppReceiver, then this needs to be override returning the correct versions. + */ + function oAppVersion() + public + view + virtual + returns (uint64 senderVersion, uint64 receiverVersion) + { + return (0, RECEIVER_VERSION); + } + + /** + * @notice Indicates whether an address is an approved composeMsg sender to the Endpoint. + * @dev _origin The origin information containing the source endpoint and sender address. + * - srcEid: The source chain endpoint ID. + * - sender: The sender address on the src chain. + * - nonce: The nonce of the message. + * @dev _message The lzReceive payload. + * @param _sender The sender address. + * @return isSender Is a valid sender. + * + * @dev Applications can optionally choose to implement separate composeMsg senders that are NOT the bridging layer. + * @dev The default sender IS the OAppReceiver implementer. + */ + function isComposeMsgSender( + Origin calldata, /*_origin*/ + bytes calldata, /*_message*/ + address _sender + ) + public + view + virtual + returns (bool) + { + return _sender == address(this); + } + + /** + * @notice Checks if the path initialization is allowed based on the provided origin. + * @param origin The origin information containing the source endpoint and sender address. + * @return Whether the path has been initialized. + * + * @dev This indicates to the endpoint that the OApp has enabled msgs for this particular path to be received. + * @dev This defaults to assuming if a peer has been set, its initialized. + * Can be overridden by the OApp if there is other logic to determine this. + */ + function allowInitializePath(Origin calldata origin) public view virtual returns (bool) { + return peers(origin.srcEid) == origin.sender; + } + + /** + * @notice Retrieves the next nonce for a given source endpoint and sender address. + * @dev _srcEid The source endpoint ID. + * @dev _sender The sender address. + * @return nonce The next nonce. + * + * @dev The path nonce starts from 1. If 0 is returned it means that there is NO nonce ordered enforcement. + * @dev Is required by the off-chain executor to determine the OApp expects msg execution is ordered. + * @dev This is also enforced by the OApp. + * @dev By default this is NOT enabled. ie. nextNonce is hardcoded to return 0. + */ + function nextNonce( + uint32, + /*_srcEid*/ + bytes32 /*_sender*/ + ) + public + view + virtual + returns (uint64 nonce) + { + return 0; + } + + /** + * @dev Entry point for receiving messages or packets from the endpoint. + * @param _origin The origin information containing the source endpoint and sender address. + * - srcEid: The source chain endpoint ID. + * - sender: The sender address on the src chain. + * - nonce: The nonce of the message. + * @param _guid The unique identifier for the received LayerZero message. + * @param _message The payload of the received message. + * @param _executor The address of the executor for the received message. + * @param _extraData Additional arbitrary data provided by the corresponding executor. + * + * @dev Entry point for receiving msg/packet from the LayerZero endpoint. + */ + function lzReceive( + Origin calldata _origin, + bytes32 _guid, + bytes calldata _message, + address _executor, + bytes calldata _extraData + ) public payable virtual { + // Ensures that only the endpoint can attempt to lzReceive() messages to this OApp. + if (address(endpoint) != msg.sender) revert OnlyEndpoint(msg.sender); + + // Ensure that the sender matches the expected peer for the source endpoint. + if (_getPeerOrRevert(_origin.srcEid) != _origin.sender) { + revert OnlyPeer(_origin.srcEid, _origin.sender); + } + + // Call the internal OApp implementation of lzReceive. + _lzReceive(_origin, _guid, _message, _executor, _extraData); + } + + /** + * @dev Internal function to implement lzReceive logic without needing to copy the basic parameter validation. + */ + function _lzReceive( + Origin calldata _origin, + bytes32 _guid, + bytes calldata _message, + address _executor, + bytes calldata _extraData + ) internal virtual; +} diff --git a/src/vendor/layerzero/oapp/OAppSender.sol b/src/vendor/layerzero/oapp/OAppSender.sol new file mode 100644 index 00000000..6fdf8ef4 --- /dev/null +++ b/src/vendor/layerzero/oapp/OAppSender.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { + MessagingParams, + MessagingFee, + MessagingReceipt +} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import {OAppCore} from "./OAppCore.sol"; + +/** + * @title OAppSender + * @dev Abstract contract implementing the OAppSender functionality for sending messages to a LayerZero endpoint. + */ +abstract contract OAppSender is OAppCore { + using SafeERC20 for IERC20; + + // Custom error messages + error NotEnoughNative(uint256 msgValue); + error LzTokenUnavailable(); + + // @dev The version of the OAppSender implementation. + // @dev Version is bumped when changes are made to this contract. + uint64 internal constant SENDER_VERSION = 1; + + /** + * @notice Retrieves the OApp version information. + * @return senderVersion The version of the OAppSender.sol contract. + * @return receiverVersion The version of the OAppReceiver.sol contract. + * + * @dev Providing 0 as the default for OAppReceiver version. Indicates that the OAppReceiver is not implemented. + * ie. this is a SEND only OApp. + * @dev If the OApp uses both OAppSender and OAppReceiver, then this needs to be override returning the correct versions + */ + function oAppVersion() + public + view + virtual + returns (uint64 senderVersion, uint64 receiverVersion) + { + return (SENDER_VERSION, 0); + } + + /** + * @dev Internal function to interact with the LayerZero EndpointV2.quote() for fee calculation. + * @param _dstEid The destination endpoint ID. + * @param _message The message payload. + * @param _options Additional options for the message. + * @param _payInLzToken Flag indicating whether to pay the fee in LZ tokens. + * @return fee The calculated MessagingFee for the message. + * - nativeFee: The native fee for the message. + * - lzTokenFee: The LZ token fee for the message. + */ + function _quote( + uint32 _dstEid, + bytes memory _message, + bytes memory _options, + bool _payInLzToken + ) internal view virtual returns (MessagingFee memory fee) { + return endpoint.quote( + MessagingParams(_dstEid, _getPeerOrRevert(_dstEid), _message, _options, _payInLzToken), + address(this) + ); + } + + /** + * @dev Internal function to interact with the LayerZero EndpointV2.send() for sending a message. + * @param _dstEid The destination endpoint ID. + * @param _message The message payload. + * @param _options Additional options for the message. + * @param _fee The calculated LayerZero fee for the message. + * - nativeFee: The native fee. + * - lzTokenFee: The lzToken fee. + * @param _refundAddress The address to receive any excess fee values sent to the endpoint. + * @return receipt The receipt for the sent message. + * - guid: The unique identifier for the sent message. + * - nonce: The nonce of the sent message. + * - fee: The LayerZero fee incurred for the message. + */ + function _lzSend( + uint32 _dstEid, + bytes memory _message, + bytes memory _options, + MessagingFee memory _fee, + address _refundAddress + ) internal virtual returns (MessagingReceipt memory receipt) { + // @dev Push corresponding fees to the endpoint, any excess is sent back to the _refundAddress from the endpoint. + uint256 messageValue = _payNative(_fee.nativeFee); + if (_fee.lzTokenFee > 0) _payLzToken(_fee.lzTokenFee); + + return endpoint. + // solhint-disable-next-line check-send-result + send{ + value: messageValue + }( + MessagingParams( + _dstEid, _getPeerOrRevert(_dstEid), _message, _options, _fee.lzTokenFee > 0 + ), + _refundAddress + ); + } + + /** + * @dev Internal function to pay the native fee associated with the message. + * @param _nativeFee The native fee to be paid. + * @return nativeFee The amount of native currency paid. + * + * @dev If the OApp needs to initiate MULTIPLE LayerZero messages in a single transaction, + * this will need to be overridden because msg.value would contain multiple lzFees. + * @dev Should be overridden in the event the LayerZero endpoint requires a different native currency. + * @dev Some EVMs use an ERC20 as a method for paying transactions/gasFees. + * @dev The endpoint is EITHER/OR, ie. it will NOT support both types of native payment at a time. + */ + function _payNative(uint256 _nativeFee) internal virtual returns (uint256 nativeFee) { + if (msg.value != _nativeFee) revert NotEnoughNative(msg.value); + return _nativeFee; + } + + /** + * @dev Internal function to pay the LZ token fee associated with the message. + * @param _lzTokenFee The LZ token fee to be paid. + * + * @dev If the caller is trying to pay in the specified lzToken, then the lzTokenFee is passed to the endpoint. + * @dev Any excess sent, is passed back to the specified _refundAddress in the _lzSend(). + */ + function _payLzToken(uint256 _lzTokenFee) internal virtual { + // @dev Cannot cache the token because it is not immutable in the endpoint. + address lzToken = endpoint.lzToken(); + if (lzToken == address(0)) revert LzTokenUnavailable(); + + // Pay LZ token fee by sending tokens to the endpoint. + IERC20(lzToken).safeTransferFrom(msg.sender, address(endpoint), _lzTokenFee); + } +} diff --git a/test/Account.t.sol b/test/Account.t.sol index 21c7c36e..87c007d9 100644 --- a/test/Account.t.sol +++ b/test/Account.t.sol @@ -419,4 +419,49 @@ contract AccountTest is BaseTest { uint256 keysCount137 = IthacaAccount(eoaAddress).keyCount(); assertEq(keysCount137, 2, "Keys should be added on chain 137"); } + + function testContextKeyHash() public { + DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); + + PassKey memory key = _randomPassKey(); + key.k.isSuperAdmin = true; + + vm.prank(d.eoa); + d.d.authorize(key.k); + + // Orchestrator workflow + vm.prank(d.d.ORCHESTRATOR()); + ERC7821.Call[] memory calls = new ERC7821.Call[](1); + calls[0].data = abi.encodeWithSelector(this.targetFunctionContextKeyHash.selector); + calls[0].to = address(this); + calls[0].value = 0; + bytes memory opData = abi.encode(key.keyHash); + bytes memory executionData = abi.encode(calls, opData); + d.d.execute(_ERC7821_BATCH_EXECUTION_MODE, executionData); + + assertEq(contextKeyHash, key.keyHash, "Context key hash mismatch orchestrator workflow"); + + // Reset context key hash + contextKeyHash = bytes32(0); + + // Workflow with opData + uint256 nonce = d.d.getNonce(0); + bytes memory signature = _sig(key, d.d.computeDigest(calls, nonce)); + opData = abi.encodePacked(nonce, signature); + executionData = abi.encode(calls, opData); + d.d.execute(_ERC7821_BATCH_EXECUTION_MODE, executionData); + + assertEq(contextKeyHash, key.keyHash, "Context key hash mismatch orchestrator workflow"); + + // Reset context key hash + contextKeyHash = bytes32(0); + + // Simple workflow without opData (self-execution) + bytes memory emptyOpData; + executionData = abi.encode(calls, emptyOpData); + vm.prank(address(d.d)); + d.d.execute(_ERC7821_BATCH_EXECUTION_MODE, executionData); + + assertEq(contextKeyHash, bytes32(0), "Context key hash should be zero for self-execution without opData"); + } } diff --git a/test/Base.t.sol b/test/Base.t.sol index aa0d05ad..87e82660 100644 --- a/test/Base.t.sol +++ b/test/Base.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.4; import "./utils/SoladyTest.sol"; import {EIP7702Proxy} from "solady/accounts/EIP7702Proxy.sol"; import {LibEIP7702} from "solady/accounts/LibEIP7702.sol"; +import {ERC7821} from "solady/accounts/ERC7821.sol"; import {LibERC7579} from "solady/accounts/LibERC7579.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {EfficientHashLib} from "solady/utils/EfficientHashLib.sol"; @@ -23,8 +24,6 @@ import {IOrchestrator} from "../src/interfaces/IOrchestrator.sol"; import {Simulator} from "../src/Simulator.sol"; import {ICommon} from "../src/interfaces/ICommon.sol"; -import {ERC7821Ithaca as ERC7821} from "../src/libraries/ERC7821Ithaca.sol"; - contract BaseTest is SoladyTest { using LibRLP for LibRLP.List; @@ -35,6 +34,7 @@ contract BaseTest is SoladyTest { EIP7702Proxy eip7702Proxy; TargetFunctionPayload[] targetFunctionPayloads; Simulator simulator; + bytes32 contextKeyHash; struct TargetFunctionPayload { address by; @@ -56,9 +56,6 @@ contract BaseTest is SoladyTest { bytes32 internal constant _ERC7821_BATCH_EXECUTION_MODE = 0x0100000000007821000100000000000000000000000000000000000000000000; - bytes32 internal constant _ERC7821_BATCH_SANS_TO_EXECUTION_MODE = - 0x0100000000007821000300000000000000000000000000000000000000000000; - bytes32 internal constant _ERC7579_DELEGATE_CALL_MODE = 0xff00000000000000000000000000000000000000000000000000000000000000; @@ -98,6 +95,10 @@ contract BaseTest is SoladyTest { targetFunctionPayloads.push(TargetFunctionPayload(msg.sender, msg.value, data)); } + function targetFunctionContextKeyHash() public payable { + contextKeyHash = IthacaAccount(payable(msg.sender)).getContextKeyHash(); + } + function _setEIP7702Delegation(address eoa) internal { vm.etch(eoa, abi.encodePacked(hex"ef0100", address(account))); } diff --git a/test/Benchmark.t.sol b/test/Benchmark.t.sol index e146fa43..2ce04524 100644 --- a/test/Benchmark.t.sol +++ b/test/Benchmark.t.sol @@ -120,9 +120,8 @@ contract BenchmarkTest is BaseTest { token0, token1, 1 ether, 1 ether, 1, 1, address(this), block.timestamp + 999 ); - IStakeManager(_ERC4337_ENTRYPOINT_V06_ADDR).depositTo{value: 1 ether}( - _PIMLICO_PAYMASTER_V06 - ); + IStakeManager(_ERC4337_ENTRYPOINT_V06_ADDR) + .depositTo{value: 1 ether}(_PIMLICO_PAYMASTER_V06); IStakeManager(_ERC4337_ENTRYPOINT_ADDR).depositTo{value: 1 ether}(_PIMLICO_PAYMASTER_V07); (paymasterSigner, paymasterPrivateKey) = makeAddrAndKey(""); @@ -1519,7 +1518,7 @@ contract BenchmarkTest is BaseTest { } } - function testERC20Transfer_IthacaAccount1() public { + function testERC20Transfer_IthacaAccount() public { DelegatedEOA[] memory delegatedEOAs = _createIthacaAccount(1); bytes memory payload = _transferExecutionData(address(paymentToken), address(0xbabe), 1 ether); @@ -1715,7 +1714,7 @@ contract BenchmarkTest is BaseTest { ) internal view returns (bytes[] memory) { bytes[] memory encodedIntents = new bytes[](delegatedEOAs.length); for (uint256 i = 0; i < delegatedEOAs.length; i++) { - Intent memory u; + Orchestrator.Intent memory u; u.eoa = delegatedEOAs[i].eoa; u.nonce = 0; u.combinedGas = 1000000; @@ -1751,14 +1750,14 @@ contract BenchmarkTest is BaseTest { _paymentType == PaymentType.APP_SPONSOR || _paymentType == PaymentType.APP_SPONSOR_ERC20 ) { - bytes32 digest = computeDigest(u); + bytes32 digest = oc.computeDigest(u); bytes32 signatureDigest = appSponsor.computeSignatureDigest(digest); u.paymentSignature = _eoaSig(paymasterPrivateKey, signatureDigest); } u.signature = _sig(delegatedEOAs[i], u); - encodedIntents[i] = abi.encodePacked(encodeIntent(u), junk); + encodedIntents[i] = abi.encodePacked(abi.encode(u), junk); } return encodedIntents; @@ -1786,7 +1785,7 @@ contract BenchmarkTest is BaseTest { d.d.setSpendLimit(k.keyHash, address(0), GuardedExecutor.SpendPeriod.Hour, 1 ether); vm.stopPrank(); - Intent memory u; + Orchestrator.Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.combinedGas = 1000000; @@ -1798,7 +1797,7 @@ contract BenchmarkTest is BaseTest { u.signature = _sig(k, u); bytes[] memory encodedIntents = new bytes[](1); - encodedIntents[0] = encodeIntent(u); + encodedIntents[0] = abi.encode(u); oc.execute(encodedIntents); vm.snapshotGasLastCall("testERC20Transfer_IthacaAccountWithSpendLimits"); diff --git a/test/Escrow.t.sol b/test/Escrow.t.sol index ce105e8a..57a6e8c0 100644 --- a/test/Escrow.t.sol +++ b/test/Escrow.t.sol @@ -723,80 +723,6 @@ contract EscrowTest is BaseTest { assertEq(uint8(escrow.statuses(escrowId3)), uint8(IEscrow.EscrowStatus.CREATED)); } - // ========== Early Refund Functionality Tests ========== - - function testEarlyRefundFunctionality() public { - // Test 1: Recipient can trigger early refund for both parties - IEscrow.Escrow memory escrowData = _createEscrowData(1000, 800); - bytes32 escrowId = _createAndFundEscrow(escrowData); - bytes32[] memory escrowIds = new bytes32[](1); - escrowIds[0] = escrowId; - - uint256 depositorBalanceBefore = token.balanceOf(depositor); - uint256 recipientBalanceBefore = token.balanceOf(recipient); - - vm.prank(recipient); - escrow.refund(escrowIds); - - assertEq(token.balanceOf(depositor) - depositorBalanceBefore, 800); // Depositor gets refundAmount - assertEq(token.balanceOf(recipient) - recipientBalanceBefore, 200); // Recipient gets escrowAmount - refundAmount - assertEq(uint256(escrow.statuses(escrowId)), uint256(IEscrow.EscrowStatus.FINALIZED)); - - // Test 2: Recipient can trigger early refund for themselves only - escrowData.salt = bytes12(uint96(2)); - escrowId = _createAndFundEscrow(escrowData); - escrowIds[0] = escrowId; - - recipientBalanceBefore = token.balanceOf(recipient); - - vm.prank(recipient); - escrow.refundRecipient(escrowIds); - - assertEq(token.balanceOf(recipient) - recipientBalanceBefore, 200); // Recipient gets escrowAmount - refundAmount - assertEq(uint256(escrow.statuses(escrowId)), uint256(IEscrow.EscrowStatus.REFUND_RECIPIENT)); - - // Test 3: Depositor can trigger early refund for themselves only - escrowData.salt = bytes12(uint96(3)); - escrowId = _createAndFundEscrow(escrowData); - escrowIds[0] = escrowId; - - depositorBalanceBefore = token.balanceOf(depositor); - - vm.prank(depositor); - escrow.refundDepositor(escrowIds); - - assertEq(token.balanceOf(depositor) - depositorBalanceBefore, 800); // Depositor gets refundAmount - assertEq(uint256(escrow.statuses(escrowId)), uint256(IEscrow.EscrowStatus.REFUND_DEPOSIT)); - - // Test 4: Unauthorized users cannot trigger early refunds - escrowData.salt = bytes12(uint96(4)); - escrowId = _createAndFundEscrow(escrowData); - escrowIds[0] = escrowId; - - vm.expectRevert(bytes4(keccak256("RefundInvalid()"))); - vm.prank(randomUser); - escrow.refund(escrowIds); - - vm.expectRevert(bytes4(keccak256("RefundInvalid()"))); - vm.prank(attacker); - escrow.refundDepositor(escrowIds); - - vm.expectRevert(bytes4(keccak256("RefundInvalid()"))); - vm.prank(randomUser); - escrow.refundRecipient(escrowIds); - - // Test 5: Normal timeout refunds still work after early refunds - depositorBalanceBefore = token.balanceOf(depositor); - recipientBalanceBefore = token.balanceOf(recipient); - - vm.warp(block.timestamp + 2 hours); - vm.prank(randomUser); - escrow.refund(escrowIds); - - assertEq(token.balanceOf(depositor) - depositorBalanceBefore, 800); // Depositor gets refundAmount - assertEq(token.balanceOf(recipient) - recipientBalanceBefore, 200); // Recipient gets escrowAmount - refundAmount - } - // ========== Helper Functions ========== function _createEscrowData(uint256 escrowAmount, uint256 refundAmount) diff --git a/test/GuardedExecutor.t.sol b/test/GuardedExecutor.t.sol index d41e341f..a30c941e 100644 --- a/test/GuardedExecutor.t.sol +++ b/test/GuardedExecutor.t.sol @@ -25,7 +25,7 @@ contract GuardedExecutorTest is BaseTest { vm.prank(d.eoa); d.d.authorize(k.k); - Intent memory u; + Orchestrator.Intent memory u; u.eoa = d.eoa; u.combinedGas = 10000000; @@ -38,8 +38,7 @@ contract GuardedExecutorTest is BaseTest { u.executionData = abi.encode(calls); u.signature = _sig(k, u); assertEq( - oc.execute(encodeIntent(u)), - bytes4(keccak256("UnauthorizedCall(bytes32,address,bytes)")) + oc.execute(abi.encode(u)), bytes4(keccak256("UnauthorizedCall(bytes32,address,bytes)")) ); // Set the call checker. bytes32 forKeyHash = _randomChance(2) ? k.keyHash : _ANY_KEYHASH; @@ -57,8 +56,7 @@ contract GuardedExecutorTest is BaseTest { u.executionData = abi.encode(calls); u.signature = _sig(k, u); assertEq( - oc.execute(encodeIntent(u)), - bytes4(keccak256("UnauthorizedCall(bytes32,address,bytes)")) + oc.execute(abi.encode(u)), bytes4(keccak256("UnauthorizedCall(bytes32,address,bytes)")) ); // Try, now with the checker configured to authorize the call.. @@ -68,7 +66,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(encodeIntent(u)), bytes4(0)); + assertEq(oc.execute(abi.encode(u)), bytes4(0)); assertEq(counter.counter(), 1); // Try, now with the checker removed. @@ -81,8 +79,7 @@ contract GuardedExecutorTest is BaseTest { u.executionData = abi.encode(calls); u.signature = _sig(k, u); assertEq( - oc.execute(encodeIntent(u)), - bytes4(keccak256("UnauthorizedCall(bytes32,address,bytes)")) + oc.execute(abi.encode(u)), bytes4(keccak256("UnauthorizedCall(bytes32,address,bytes)")) ); } @@ -97,7 +94,7 @@ contract GuardedExecutorTest is BaseTest { d.d.setCanExecute(k.keyHash, address(paymentToken), _ANY_FN_SEL, true); vm.stopPrank(); - Intent memory u; + Orchestrator.Intent memory u; u.eoa = d.eoa; u.combinedGas = 10000000; @@ -115,7 +112,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("NoSpendPermissions()"))); + assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("NoSpendPermissions()"))); // Check that after the spend permission has been done, the token can be approved // and moved via `transferFrom`. @@ -130,7 +127,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); assertEq(paymentToken.balanceOf(address(0xb0b)), 0.1 ether); // Check that the spent has been increased. @@ -235,7 +232,7 @@ contract GuardedExecutorTest is BaseTest { vm.prank(address(0xb0b)); paymentToken.approve(d.eoa, 1 ether); - Intent memory u; + Orchestrator.Intent memory u; u.eoa = d.eoa; u.combinedGas = 10000000; @@ -261,12 +258,12 @@ contract GuardedExecutorTest is BaseTest { emit LogBool("transferToSelf:", transferToSelf); if (transferToSelf) { - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); assertEq(paymentToken.balanceOf(d.eoa), 0.1 ether); return; } - assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("NoSpendPermissions()"))); + assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("NoSpendPermissions()"))); vm.startPrank(d.eoa); d.d @@ -277,7 +274,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.signature = _sig(k, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); assertEq(paymentToken.balanceOf(address(0xdad)), 0.1 ether); assertEq(d.d.spendInfos(k.keyHash)[0].spent, 0.1 ether); } @@ -288,7 +285,7 @@ contract GuardedExecutorTest is BaseTest { } function testOnlySuperAdminAndEOACanSelfExecute() public { - Intent memory u; + Orchestrator.Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; u.combinedGas = 10000000; @@ -330,14 +327,14 @@ contract GuardedExecutorTest is BaseTest { u.executionData = abi.encode(calls); u.signature = _eoaSig(d.privateKey, u); - assertEq(oc.execute(encodeIntent(u)), 0, "2"); + assertEq(oc.execute(abi.encode(u)), 0, "2"); assertEq(d.d.x(), x, "3"); d.d.resetX(); u.nonce = d.d.getNonce(0); u.signature = _sig(kSuperAdmin, u); - assertEq(oc.execute(encodeIntent(u)), 0, "4"); + assertEq(oc.execute(abi.encode(u)), 0, "4"); assertEq(d.d.x(), x, "5"); d.d.resetX(); @@ -345,7 +342,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.signature = _sig(kRegular, u); assertEq( - oc.execute(encodeIntent(u)), + oc.execute(abi.encode(u)), bytes4(keccak256("UnauthorizedCall(bytes32,address,bytes)")), "6" ); @@ -356,7 +353,7 @@ contract GuardedExecutorTest is BaseTest { } function testSetAndRemoveSpendLimitRevertsForSuperAdmin() public { - Intent memory u; + Orchestrator.Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; @@ -374,11 +371,11 @@ contract GuardedExecutorTest is BaseTest { calls[0].data = abi.encodeWithSelector(IthacaAccount.authorize.selector, k.k); u.executionData = abi.encode(calls); - u.nonce = d.d.getNonce(0); + u.nonce = 0xc1d0 << 240; u.signature = _sig(d, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); } // Set spend limits. { @@ -389,7 +386,7 @@ contract GuardedExecutorTest is BaseTest { u.executionData = abi.encode(calls); u.signature = _sig(d, u); - assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("SuperAdminCanSpendAnything()"))); + assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("SuperAdminCanSpendAnything()"))); } // Remove spend limits. { @@ -400,14 +397,14 @@ contract GuardedExecutorTest is BaseTest { u.executionData = abi.encode(calls); u.signature = _sig(d, u); - assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("SuperAdminCanSpendAnything()"))); + assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("SuperAdminCanSpendAnything()"))); } } function testSetAndRemoveSpendLimit(uint256 amount) public { vm.warp(86400 * 100); - Intent memory u; + Orchestrator.Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; @@ -445,11 +442,11 @@ contract GuardedExecutorTest is BaseTest { calls[3] = _setSpendLimitCall(k, token, periods[1], 1 ether); u.executionData = abi.encode(calls); - u.nonce = d.d.getNonce(0); + u.nonce = 0xc1d0 << 240; u.signature = _eoaSig(d.privateKey, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); assertEq(d.d.spendInfos(k.keyHash).length, 2); } @@ -461,7 +458,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); infos = d.d.spendInfos(k.keyHash); for (uint256 i; i < infos.length; ++i) { @@ -477,7 +474,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); infos = d.d.spendInfos(k.keyHash); assertEq(infos.length, 1); @@ -493,7 +490,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); infos = d.d.spendInfos(k.keyHash); for (uint256 i; i < infos.length; ++i) { if (infos[i].period == periods[0]) { @@ -516,7 +513,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); infos = d.d.spendInfos(k.keyHash); for (uint256 i; i < infos.length; ++i) { @@ -537,7 +534,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); assertEq(d.d.spendInfos(k.keyHash).length, 0); } @@ -558,9 +555,9 @@ contract GuardedExecutorTest is BaseTest { && calls[0].data[2] == bytes1(uint8(0x24)) && calls[0].data[3] == bytes1(uint8(0xd5))) || amount == 0 ) { - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); } else { - assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("NoSpendPermissions()"))); + assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("NoSpendPermissions()"))); } } @@ -573,7 +570,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); infos = d.d.spendInfos(k.keyHash); for (uint256 i; i < infos.length; ++i) { @@ -590,7 +587,7 @@ contract GuardedExecutorTest is BaseTest { u.nonce = d.d.getNonce(0); u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); infos = d.d.spendInfos(k.keyHash); for (uint256 i; i < infos.length; ++i) { @@ -600,7 +597,7 @@ contract GuardedExecutorTest is BaseTest { } function testSetSpendLimitWithTwoPeriods() public { - Intent memory u; + Orchestrator.Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; @@ -631,11 +628,11 @@ contract GuardedExecutorTest is BaseTest { calls[5] = _setSpendLimitCall(k, token1, GuardedExecutor.SpendPeriod.Year, 1 ether); u.executionData = abi.encode(calls); - u.nonce = d.d.getNonce(0); + u.nonce = 0xc1d0 << 240; u.signature = _eoaSig(d.privateKey, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); assertEq(d.d.spendInfos(k.keyHash).length, 4); } @@ -645,11 +642,11 @@ contract GuardedExecutorTest is BaseTest { calls[0] = _transferCall2(token0, address(0xb0b), amount0); calls[1] = _transferCall2(token1, address(0xb0b), amount1); - u.nonce = d.d.getNonce(0); + u.nonce = 0; u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); GuardedExecutor.SpendInfo[] memory infos = d.d.spendInfos(k.keyHash); for (uint256 i; i < infos.length; ++i) { if (infos[i].token == token0) assertEq(infos[i].spent, amount0); @@ -658,7 +655,7 @@ contract GuardedExecutorTest is BaseTest { } function testSpends(bytes32) public { - Intent memory u; + Orchestrator.Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; @@ -693,17 +690,17 @@ contract GuardedExecutorTest is BaseTest { } u.executionData = abi.encode(calls); - u.nonce = d.d.getNonce(0); + u.nonce = 0xc1d0 << 240; u.signature = _eoaSig(d.privateKey, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); assertEq(d.d.spendInfos(k.keyHash).length, tokens.length); } // Test spends. { - u.nonce = d.d.getNonce(0); + u.nonce = 0; _deployPermit2(); if (_randomChance(2)) { @@ -759,7 +756,7 @@ contract GuardedExecutorTest is BaseTest { u.executionData = abi.encode(calls); u.signature = _sig(k, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); GuardedExecutor.SpendInfo[] memory infos = d.d.spendInfos(k.keyHash); for (uint256 i; i < infos.length; ++i) { assertEq(infos[i].spent, expectedSpents[0][infos[i].token]); @@ -809,7 +806,7 @@ contract GuardedExecutorTest is BaseTest { } function _testSpendWithPassKeyViaOrchestrator(PassKey memory k, address tokenToSpend) internal { - Intent memory u; + Orchestrator.Intent memory u; GuardedExecutor.SpendInfo memory info; uint256 gExecute; @@ -842,12 +839,12 @@ contract GuardedExecutorTest is BaseTest { ); u.executionData = abi.encode(calls); + u.nonce = 0xc1d0 << 240; (gExecute, u.combinedGas,) = _estimateGasForEOAKey(u); - u.signature = _eoaSig(d.privateKey, u); - assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); + assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); assertEq(d.d.spendInfos(k.keyHash).length, 2); assertEq(d.d.spendInfos(k.keyHash)[0].spent, 0); @@ -857,7 +854,7 @@ contract GuardedExecutorTest is BaseTest { // Prep Intent, and submit it. This Intent should pass. { - u.nonce++; + u.nonce = 0; ERC7821.Call[] memory calls = new ERC7821.Call[](1); calls[0] = _transferCall2(tokenToSpend, address(0xb0b), 0.6 ether); @@ -867,7 +864,7 @@ contract GuardedExecutorTest is BaseTest { u.signature = _sig(k, u); // Intent should pass. - assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); + assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); assertEq(_balanceOf(tokenToSpend, address(0xb0b)), 0.6 ether); assertEq(d.d.spendInfos(k.keyHash)[0].spent, 0.6 ether); @@ -881,7 +878,7 @@ contract GuardedExecutorTest is BaseTest { u.signature = _sig(k, u); // Intent should fail. - assertEq(oc.execute(encodeIntent(u)), GuardedExecutor.ExceededSpendLimit.selector); + assertEq(oc.execute(abi.encode(u)), GuardedExecutor.ExceededSpendLimit.selector); } // Prep Intent to try to exactly hit daily spend limit. This Intent should pass. @@ -895,7 +892,7 @@ contract GuardedExecutorTest is BaseTest { (gExecute, u.combinedGas,) = _estimateGas(k, u); u.signature = _sig(k, u); - assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); + assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); assertEq(_balanceOf(tokenToSpend, address(0xb0b)), 1 ether); assertEq(d.d.spendInfos(k.keyHash)[0].spent, 1 ether); } @@ -935,7 +932,7 @@ contract GuardedExecutorTest is BaseTest { u.signature = _sig(k, u); - assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); + assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); assertEq(_balanceOf(tokenToSpend, address(0xb0b)), 1.5 ether); assertEq(d.d.spendInfos(k.keyHash)[0].spent, 0.5 ether); } diff --git a/test/LayerZeroSettler.t.sol b/test/LayerZeroSettler.t.sol index fe9be6ae..941f2311 100644 --- a/test/LayerZeroSettler.t.sol +++ b/test/LayerZeroSettler.t.sol @@ -716,9 +716,9 @@ contract LayerZeroSettlerTest is Test { // Should revert with InvalidL0SettlerSignature vm.expectRevert(abi.encodeWithSelector(LayerZeroSettler.InvalidL0SettlerSignature.selector)); - settlerA.executeSend{value: fee}( - orchestrator, settlementId, settlerContext, invalidSignature - ); + settlerA.executeSend{ + value: fee + }(orchestrator, settlementId, settlerContext, invalidSignature); } function test_executeSend_preventReplay() public { diff --git a/test/MultiSigSigner.t.sol b/test/MultiSigSigner.t.sol new file mode 100644 index 00000000..497e0618 --- /dev/null +++ b/test/MultiSigSigner.t.sol @@ -0,0 +1,656 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "./Base.t.sol"; +import {MultiSigSigner} from "../src/MultiSigSigner.sol"; +import {IthacaAccount} from "../src/IthacaAccount.sol"; +import {ERC7821} from "solady/accounts/ERC7821.sol"; + +contract MultiSigSignerTest is BaseTest { + MultiSigSigner multiSigSigner; + DelegatedEOA delegatedAccount; + + struct MultiSigTestTemps { + PassKey[] owners; + bytes32[] ownerKeyHashes; + uint256 threshold; + MultiSigKey multiSigKey; + bytes32 digest; + } + + function setUp() public override { + super.setUp(); + multiSigSigner = new MultiSigSigner(); + delegatedAccount = _randomEIP7702DelegatedEOA(); + } + + function test_DuplicateOwnerSignatures() public { + MultiSigTestTemps memory t; + + // Setup: Create a multisig with threshold 2 but only 1 owner + t.threshold = 2; + t.owners = new PassKey[](2); + t.owners[0] = _randomPassKey(); + t.owners[1] = _randomPassKey(); + + // Create the multisig key configuration + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: true, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) + }); + t.multiSigKey.threshold = t.threshold; + t.multiSigKey.owners = t.owners; + + // Authorize keys in the delegated account + vm.startPrank(delegatedAccount.eoa); + delegatedAccount.d.authorize(t.multiSigKey.k); + delegatedAccount.d.authorize(t.owners[0].k); + + // Initialize multisig config with threshold=2 but only 1 owner + t.ownerKeyHashes = new bytes32[](1); + t.ownerKeyHashes[0] = _hash(t.owners[0].k); + + // This should revert because threshold > number of owners + vm.expectRevert(MultiSigSigner.InvalidThreshold.selector); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); + + // Now test with a valid config: threshold=2, 2 owners + t.ownerKeyHashes = new bytes32[](2); + t.ownerKeyHashes[0] = _hash(t.owners[0].k); + t.ownerKeyHashes[1] = _hash(t.owners[1].k); + + multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); + + vm.stopPrank(); + + // Create a digest to sign + t.digest = keccak256("test message"); + + // Create signature array with the same signature duplicated + // Note: Each owner currently only has 1 signer power. + bytes[] memory signatures = new bytes[](2); + signatures[0] = _sig(t.owners[0], t.digest); + signatures[1] = signatures[0]; // Duplicate signature of the first owner + + // Call isValidSignatureWithKeyHash + vm.prank(address(delegatedAccount.d)); + bytes4 result = multiSigSigner.isValidSignatureWithKeyHash( + t.digest, _hash(t.multiSigKey.k), abi.encode(signatures) + ); + + // Same owner should not be able to sign multiple times + assertEq(result, bytes4(0xffffffff), "Duplicate signature should not be valid"); + + // Add the first owner twice in the owner key hash + vm.startPrank(address(delegatedAccount.eoa)); + delegatedAccount.d.setContextKeyHash(_hash(t.multiSigKey.k)); + multiSigSigner.addOwner(_hash(t.multiSigKey.k), _hash(t.owners[0].k)); + vm.stopPrank(); + + // Now it should be valid, because the first owner has 2 signer powers. + vm.prank(address(delegatedAccount.d)); + result = multiSigSigner.isValidSignatureWithKeyHash( + t.digest, _hash(t.multiSigKey.k), abi.encode(signatures) + ); + + assertEq(result, bytes4(0x8afc93b4), "Duplicate signature should be valid now"); + } + + function test_InitConfig() public { + MultiSigTestTemps memory t; + + // Setup owners + uint256 numOwners = 3; + t.owners = new PassKey[](numOwners); + t.ownerKeyHashes = new bytes32[](numOwners); + + for (uint256 i = 0; i < numOwners; i++) { + t.owners[i] = _randomPassKey(); + t.ownerKeyHashes[i] = _hash(t.owners[i].k); + } + + t.threshold = 2; + + // Create multisig key + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: false, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) + }); + + vm.prank(address(delegatedAccount.d)); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); + + // Verify config was set correctly + (uint256 storedThreshold, bytes32[] memory storedOwners) = + multiSigSigner.getConfig(address(delegatedAccount.d), _hash(t.multiSigKey.k)); + + assertEq(storedThreshold, t.threshold); + assertEq(storedOwners.length, t.ownerKeyHashes.length); + + for (uint256 i = 0; i < storedOwners.length; i++) { + assertEq(storedOwners[i], t.ownerKeyHashes[i]); + } + } + + function test_InitConfig_RevertsOnReinit() public { + MultiSigTestTemps memory t; + + t.owners = new PassKey[](1); + t.owners[0] = _randomPassKey(); + t.ownerKeyHashes = new bytes32[](1); + t.ownerKeyHashes[0] = _hash(t.owners[0].k); + t.threshold = 1; + + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: false, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) + }); + + // First initialization should succeed + vm.startPrank(address(delegatedAccount.d)); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); + + // Second initialization should revert + vm.expectRevert(MultiSigSigner.ConfigAlreadySet.selector); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); + vm.stopPrank(); + } + + function test_InitConfig_InvalidThreshold() public { + MultiSigTestTemps memory t; + + t.owners = new PassKey[](2); + t.ownerKeyHashes = new bytes32[](2); + for (uint256 i = 0; i < 2; i++) { + t.owners[i] = _randomPassKey(); + t.ownerKeyHashes[i] = _hash(t.owners[i].k); + } + + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: false, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) + }); + + vm.startPrank(address(delegatedAccount.d)); + + // Test threshold = 0 + vm.expectRevert(MultiSigSigner.InvalidThreshold.selector); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), 0, t.ownerKeyHashes); + + // Test threshold > number of owners + vm.expectRevert(MultiSigSigner.InvalidThreshold.selector); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), 3, t.ownerKeyHashes); + + vm.stopPrank(); + } + + function test_AddOwner() public { + MultiSigTestTemps memory t; + + // Initial setup with 2 owners + t.owners = new PassKey[](2); + t.ownerKeyHashes = new bytes32[](2); + for (uint256 i = 0; i < 2; i++) { + t.owners[i] = _randomPassKey(); + t.ownerKeyHashes[i] = _hash(t.owners[i].k); + } + t.threshold = 2; + + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: true, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) + }); + + // Initialize config + vm.startPrank(address(delegatedAccount.d)); + delegatedAccount.d.authorize(t.multiSigKey.k); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); + + // Add a new owner + PassKey memory newOwner = _randomPassKey(); + bytes32 newOwnerKeyHash = _hash(newOwner.k); + + // Set context key hash + delegatedAccount.d.setContextKeyHash(_hash(t.multiSigKey.k)); + multiSigSigner.addOwner(_hash(t.multiSigKey.k), newOwnerKeyHash); + + // Verify owner was added + (, bytes32[] memory storedOwners) = + multiSigSigner.getConfig(address(delegatedAccount.d), _hash(t.multiSigKey.k)); + + assertEq(storedOwners.length, 3); + assertEq(storedOwners[2], newOwnerKeyHash); + + vm.stopPrank(); + } + + function test_AddOwner_InvalidKeyHash() public { + MultiSigTestTemps memory t; + + t.owners = new PassKey[](1); + t.owners[0] = _randomPassKey(); + t.ownerKeyHashes = new bytes32[](1); + t.ownerKeyHashes[0] = _hash(t.owners[0].k); + + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: true, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) + }); + + vm.startPrank(address(delegatedAccount.d)); + delegatedAccount.d.authorize(t.multiSigKey.k); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), 1, t.ownerKeyHashes); + + // Try to add owner with wrong context key hash + PassKey memory newOwner = _randomPassKey(); + + // Set wrong context key hash + delegatedAccount.d.setContextKeyHash(bytes32(uint256(123))); + + vm.expectRevert(MultiSigSigner.InvalidKeyHash.selector); + multiSigSigner.addOwner(_hash(t.multiSigKey.k), _hash(newOwner.k)); + + vm.stopPrank(); + } + + function test_RemoveOwner() public { + MultiSigTestTemps memory t; + + // Setup with 3 owners + t.owners = new PassKey[](3); + t.ownerKeyHashes = new bytes32[](3); + for (uint256 i = 0; i < 3; i++) { + t.owners[i] = _randomPassKey(); + t.ownerKeyHashes[i] = _hash(t.owners[i].k); + } + t.threshold = 2; + + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: true, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) + }); + + vm.startPrank(address(delegatedAccount.d)); + delegatedAccount.d.authorize(t.multiSigKey.k); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); + + // Remove the second owner + delegatedAccount.d.setContextKeyHash(_hash(t.multiSigKey.k)); + multiSigSigner.removeOwner(_hash(t.multiSigKey.k), t.ownerKeyHashes[1]); + + // Verify owner was removed + (, bytes32[] memory storedOwners) = + multiSigSigner.getConfig(address(delegatedAccount.d), _hash(t.multiSigKey.k)); + + assertEq(storedOwners.length, 2); + // The last owner should have replaced the removed one + assertEq(storedOwners[1], t.ownerKeyHashes[2]); + + vm.stopPrank(); + } + + function test_RemoveOwner_OwnerNotFound() public { + MultiSigTestTemps memory t; + + t.owners = new PassKey[](2); + t.ownerKeyHashes = new bytes32[](2); + for (uint256 i = 0; i < 2; i++) { + t.owners[i] = _randomPassKey(); + t.ownerKeyHashes[i] = _hash(t.owners[i].k); + } + + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: true, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) + }); + + vm.startPrank(address(delegatedAccount.d)); + delegatedAccount.d.authorize(t.multiSigKey.k); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), 1, t.ownerKeyHashes); + + // Try to remove non-existent owner + delegatedAccount.d.setContextKeyHash(_hash(t.multiSigKey.k)); + vm.expectRevert(MultiSigSigner.OwnerNotFound.selector); + multiSigSigner.removeOwner(_hash(t.multiSigKey.k), bytes32(uint256(999))); + + vm.stopPrank(); + } + + function test_RemoveOwner_ThresholdViolation() public { + MultiSigTestTemps memory t; + + // Setup with 2 owners and threshold 2 + t.owners = new PassKey[](2); + t.ownerKeyHashes = new bytes32[](2); + for (uint256 i = 0; i < 2; i++) { + t.owners[i] = _randomPassKey(); + t.ownerKeyHashes[i] = _hash(t.owners[i].k); + } + t.threshold = 2; + + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: true, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) + }); + + vm.startPrank(address(delegatedAccount.d)); + delegatedAccount.d.authorize(t.multiSigKey.k); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); + + // Try to remove owner when it would violate threshold + delegatedAccount.d.setContextKeyHash(_hash(t.multiSigKey.k)); + vm.expectRevert(MultiSigSigner.InvalidThreshold.selector); + multiSigSigner.removeOwner(_hash(t.multiSigKey.k), t.ownerKeyHashes[0]); + + vm.stopPrank(); + } + + function test_SetThreshold() public { + MultiSigTestTemps memory t; + + // Setup with 3 owners + t.owners = new PassKey[](3); + t.ownerKeyHashes = new bytes32[](3); + for (uint256 i = 0; i < 3; i++) { + t.owners[i] = _randomPassKey(); + t.ownerKeyHashes[i] = _hash(t.owners[i].k); + } + t.threshold = 1; + + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: true, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) + }); + + vm.startPrank(address(delegatedAccount.d)); + delegatedAccount.d.authorize(t.multiSigKey.k); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); + + // Change threshold to 2 + delegatedAccount.d.setContextKeyHash(_hash(t.multiSigKey.k)); + multiSigSigner.setThreshold(_hash(t.multiSigKey.k), 2); + + // Verify threshold was changed + (uint256 storedThreshold,) = + multiSigSigner.getConfig(address(delegatedAccount.d), _hash(t.multiSigKey.k)); + + assertEq(storedThreshold, 2); + + // Change threshold to 3 + multiSigSigner.setThreshold(_hash(t.multiSigKey.k), 3); + + (storedThreshold,) = + multiSigSigner.getConfig(address(delegatedAccount.d), _hash(t.multiSigKey.k)); + + assertEq(storedThreshold, 3); + + vm.stopPrank(); + } + + function test_SetThreshold_Invalid() public { + MultiSigTestTemps memory t; + + t.owners = new PassKey[](2); + t.ownerKeyHashes = new bytes32[](2); + for (uint256 i = 0; i < 2; i++) { + t.owners[i] = _randomPassKey(); + t.ownerKeyHashes[i] = _hash(t.owners[i].k); + } + + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: true, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) + }); + + vm.startPrank(address(delegatedAccount.d)); + delegatedAccount.d.authorize(t.multiSigKey.k); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), 1, t.ownerKeyHashes); + delegatedAccount.d.setContextKeyHash(_hash(t.multiSigKey.k)); + + // Test threshold = 0 + vm.expectRevert(MultiSigSigner.InvalidThreshold.selector); + multiSigSigner.setThreshold(_hash(t.multiSigKey.k), 0); + + // Test threshold > number of owners + vm.expectRevert(MultiSigSigner.InvalidThreshold.selector); + multiSigSigner.setThreshold(_hash(t.multiSigKey.k), 3); + + vm.stopPrank(); + } + + function test_ValidSignature_MeetsThreshold() public { + MultiSigTestTemps memory t; + + // Setup with 3 owners, threshold 2 + t.owners = new PassKey[](3); + t.ownerKeyHashes = new bytes32[](3); + + vm.startPrank(delegatedAccount.eoa); + for (uint256 i = 0; i < 3; i++) { + t.owners[i] = _randomPassKey(); + t.ownerKeyHashes[i] = _hash(t.owners[i].k); + delegatedAccount.d.authorize(t.owners[i].k); + } + + t.threshold = 2; + + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: true, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) + }); + + delegatedAccount.d.authorize(t.multiSigKey.k); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); + vm.stopPrank(); + + // Create a digest to sign + t.digest = keccak256("test message"); + + // Create signatures from 2 owners (meets threshold) + bytes[] memory signatures = new bytes[](2); + signatures[0] = _sig(t.owners[0], t.digest); + signatures[1] = _sig(t.owners[1], t.digest); + + // Validate signature + vm.prank(address(delegatedAccount.d)); + bytes4 result = multiSigSigner.isValidSignatureWithKeyHash( + t.digest, _hash(t.multiSigKey.k), abi.encode(signatures) + ); + + assertEq(result, bytes4(0x8afc93b4), "Valid signature should return MAGIC_VALUE"); + } + + function test_InvalidSignature_BelowThreshold() public { + MultiSigTestTemps memory t; + + // Setup with 3 owners, threshold 2 + t.owners = new PassKey[](3); + t.ownerKeyHashes = new bytes32[](3); + + vm.startPrank(delegatedAccount.eoa); + for (uint256 i = 0; i < 3; i++) { + t.owners[i] = _randomPassKey(); + t.ownerKeyHashes[i] = _hash(t.owners[i].k); + delegatedAccount.d.authorize(t.owners[i].k); + } + + t.threshold = 2; + + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: true, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) + }); + + delegatedAccount.d.authorize(t.multiSigKey.k); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); + vm.stopPrank(); + + // Create a digest to sign + t.digest = keccak256("test message"); + + // Create signature from only 1 owner (below threshold) + bytes[] memory signatures = new bytes[](1); + signatures[0] = _sig(t.owners[0], t.digest); + + // Validate signature + vm.prank(address(delegatedAccount.d)); + bytes4 result = multiSigSigner.isValidSignatureWithKeyHash( + t.digest, _hash(t.multiSigKey.k), abi.encode(signatures) + ); + + assertEq(result, bytes4(0xffffffff), "Invalid signature should return FAIL_VALUE"); + } + + function test_InvalidSignature_NonOwner() public { + MultiSigTestTemps memory t; + + // Setup with 2 owners + t.owners = new PassKey[](2); + t.ownerKeyHashes = new bytes32[](2); + + vm.startPrank(delegatedAccount.eoa); + for (uint256 i = 0; i < 2; i++) { + t.owners[i] = _randomPassKey(); + t.ownerKeyHashes[i] = _hash(t.owners[i].k); + delegatedAccount.d.authorize(t.owners[i].k); + } + + t.threshold = 2; + + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: true, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(0)) + }); + + delegatedAccount.d.authorize(t.multiSigKey.k); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), t.threshold, t.ownerKeyHashes); + + // Create a non-owner key + PassKey memory nonOwner = _randomPassKey(); + delegatedAccount.d.authorize(nonOwner.k); + + vm.stopPrank(); + + // Create a digest to sign + t.digest = keccak256("test message"); + + // Create signatures with one owner and one non-owner + bytes[] memory signatures = new bytes[](2); + signatures[0] = _sig(t.owners[0], t.digest); + signatures[1] = _sig(nonOwner, t.digest); + + // Validate signature + vm.prank(address(delegatedAccount.d)); + bytes4 result = multiSigSigner.isValidSignatureWithKeyHash( + t.digest, _hash(t.multiSigKey.k), abi.encode(signatures) + ); + + assertEq(result, bytes4(0xffffffff), "Non-owner signature should return FAIL_VALUE"); + } + + function testFuzz_InitConfig(uint256 numOwners, uint256 threshold) public { + numOwners = bound(numOwners, 1, 10); + threshold = bound(threshold, 1, numOwners); + + MultiSigTestTemps memory t; + t.owners = new PassKey[](numOwners); + t.ownerKeyHashes = new bytes32[](numOwners); + + for (uint256 i = 0; i < numOwners; i++) { + t.owners[i] = _randomPassKey(); + t.ownerKeyHashes[i] = _hash(t.owners[i].k); + } + + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: false, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(uint96(_random()))) + }); + + vm.prank(address(delegatedAccount.d)); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), threshold, t.ownerKeyHashes); + + (uint256 storedThreshold, bytes32[] memory storedOwners) = + multiSigSigner.getConfig(address(delegatedAccount.d), _hash(t.multiSigKey.k)); + + assertEq(storedThreshold, threshold); + assertEq(storedOwners.length, numOwners); + } + + function testFuzz_SignatureValidation(uint256 numOwners, uint256 threshold, uint256 numSigners) + public + { + numOwners = bound(numOwners, 1, 10); + threshold = bound(threshold, 1, numOwners); + numSigners = bound(numSigners, 0, numOwners); + + MultiSigTestTemps memory t; + t.owners = new PassKey[](numOwners); + t.ownerKeyHashes = new bytes32[](numOwners); + + vm.startPrank(delegatedAccount.eoa); + for (uint256 i = 0; i < numOwners; i++) { + t.owners[i] = _randomPassKey(); + t.ownerKeyHashes[i] = _hash(t.owners[i].k); + delegatedAccount.d.authorize(t.owners[i].k); + } + + t.multiSigKey.k = IthacaAccount.Key({ + expiry: 0, + keyType: IthacaAccount.KeyType.External, + isSuperAdmin: true, + publicKey: abi.encodePacked(address(multiSigSigner), bytes12(uint96(_random()))) + }); + + delegatedAccount.d.authorize(t.multiSigKey.k); + multiSigSigner.initConfig(_hash(t.multiSigKey.k), threshold, t.ownerKeyHashes); + vm.stopPrank(); + + t.digest = keccak256(abi.encode("test", _random())); + + bytes[] memory signatures = new bytes[](numSigners); + for (uint256 i = 0; i < numSigners; i++) { + signatures[i] = _sig(t.owners[i], t.digest); + } + + vm.prank(address(delegatedAccount.d)); + bytes4 result = multiSigSigner.isValidSignatureWithKeyHash( + t.digest, _hash(t.multiSigKey.k), abi.encode(signatures) + ); + + if (numSigners >= threshold) { + assertEq(result, bytes4(0x8afc93b4), "Should be valid when signatures >= threshold"); + } else { + assertEq(result, bytes4(0xffffffff), "Should be invalid when signatures < threshold"); + } + } +} diff --git a/test/Orchestrator.t.sol b/test/Orchestrator.t.sol index 66d1bba7..9f2ada0c 100644 --- a/test/Orchestrator.t.sol +++ b/test/Orchestrator.t.sol @@ -20,7 +20,7 @@ import {IEscrow} from "../src/interfaces/IEscrow.sol"; contract OrchestratorTest is BaseTest { struct _TestFullFlowTemps { - Intent[] intents; + Orchestrator.Intent[] intents; TargetFunctionPayload[] targetFunctionPayloads; DelegatedEOA[] delegatedEOAs; bytes[] encodedIntents; @@ -29,7 +29,7 @@ contract OrchestratorTest is BaseTest { function testFullFlow(uint256) public { _TestFullFlowTemps memory t; - t.intents = new Intent[](_random() & 3); + t.intents = new Orchestrator.Intent[](_random() & 3); t.targetFunctionPayloads = new TargetFunctionPayload[](t.intents.length); t.delegatedEOAs = new DelegatedEOA[](t.intents.length); t.encodedIntents = new bytes[](t.intents.length); @@ -38,7 +38,7 @@ contract OrchestratorTest is BaseTest { DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); t.delegatedEOAs[i] = d; - Intent memory u = t.intents[i]; + Orchestrator.Intent memory u = t.intents[i]; u.eoa = d.eoa; vm.deal(u.eoa, 2 ** 128 - 1); @@ -54,7 +54,7 @@ contract OrchestratorTest is BaseTest { u.combinedGas = 10000000; u.signature = _sig(d, u); - t.encodedIntents[i] = encodeIntent(u); + t.encodedIntents[i] = abi.encode(u); } bytes4[] memory errors = oc.execute(t.encodedIntents); @@ -74,12 +74,11 @@ contract OrchestratorTest is BaseTest { vm.deal(alice.eoa, 10 ether); vm.deal(bob.eoa, 10 ether); paymentToken.mint(alice.eoa, 50 ether); - paymentToken.mint(bob.eoa, 50 ether); bytes memory executionData = _transferExecutionData(address(paymentToken), address(0xabcd), 1 ether); - Intent memory u; + Orchestrator.Intent memory u; u.eoa = alice.eoa; u.nonce = 0; u.executionData = executionData; @@ -89,9 +88,11 @@ contract OrchestratorTest is BaseTest { u.paymentAmount = 0.1 ether; u.paymentMaxAmount = 0.5 ether; u.combinedGas = 10000000; + u.signature = ""; + u.signature = _sig(alice, u); - assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("Unauthorized()"))); + assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("PaymentError()"))); } function testExecuteWithSecp256k1PassKey() public { @@ -106,7 +107,7 @@ contract OrchestratorTest is BaseTest { vm.prank(d.eoa); d.d.authorize(k.k); - Intent memory u; + Orchestrator.Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.executionData = _transferExecutionData(address(paymentToken), address(0xabcd), 1 ether); @@ -130,7 +131,7 @@ contract OrchestratorTest is BaseTest { combinedGasVerificationOffset: 0 }) ); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); uint256 actualAmount = 0.1 ether; assertEq(paymentToken.balanceOf(address(oc)), actualAmount); assertEq(paymentToken.balanceOf(d.eoa), 50 ether - actualAmount - 1 ether); @@ -153,7 +154,7 @@ contract OrchestratorTest is BaseTest { calls[0].to = target; calls[0].data = abi.encodeWithSignature("revertWithData(bytes)", data); - Intent memory u; + Orchestrator.Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.executionData = abi.encode(calls); @@ -162,7 +163,7 @@ contract OrchestratorTest is BaseTest { u.signature = _sig(k, u); (bool success, bytes memory result) = - address(oc).call(abi.encodeWithSignature("simulateFailed(bytes)", encodeIntent(u))); + address(oc).call(abi.encodeWithSignature("simulateFailed(bytes)", abi.encode(u))); assertFalse(success); assertEq(result, abi.encodeWithSignature("ErrorWithData(bytes)", data)); @@ -173,7 +174,7 @@ contract OrchestratorTest is BaseTest { paymentToken.mint(d.eoa, 500 ether); - Intent memory u; + Orchestrator.Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.executionData = _transferExecutionData(address(paymentToken), address(0xabcd), 1 ether); @@ -194,7 +195,7 @@ contract OrchestratorTest is BaseTest { }) ); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); uint256 actualAmount = 10 ether; assertEq(paymentToken.balanceOf(address(this)), actualAmount); assertEq(paymentToken.balanceOf(d.eoa), 500 ether - actualAmount - 1 ether); @@ -211,7 +212,7 @@ contract OrchestratorTest is BaseTest { ds[i] = _randomEIP7702DelegatedEOA(); paymentToken.mint(ds[i].eoa, 1 ether); - Intent memory u; + Orchestrator.Intent memory u; u.eoa = ds[i].eoa; u.nonce = 0; u.executionData = @@ -223,7 +224,7 @@ contract OrchestratorTest is BaseTest { u.paymentMaxAmount = 0.5 ether; u.combinedGas = 10000000; u.signature = _sig(ds[i], u); - encodedIntents[i] = encodeIntent(u); + encodedIntents[i] = abi.encode(u); } bytes4[] memory errs = oc.execute(encodedIntents); @@ -246,7 +247,7 @@ contract OrchestratorTest is BaseTest { calls[i] = _transferCall(address(paymentToken), address(0xabcd), 0.5 ether); } - Intent memory u; + Orchestrator.Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.executionData = abi.encode(calls); @@ -259,7 +260,7 @@ contract OrchestratorTest is BaseTest { (uint256 gExecute,,) = _estimateGas(u); - assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); + assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); assertEq(paymentToken.balanceOf(address(0xabcd)), 0.5 ether * n); assertEq(paymentToken.balanceOf(d.eoa), 100 ether - (u.paymentAmount + 0.5 ether * n)); assertEq(d.d.getNonce(0), 1); @@ -270,7 +271,7 @@ contract OrchestratorTest is BaseTest { paymentToken.mint(d.eoa, 500 ether); - Intent memory u; + Orchestrator.Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.executionData = _transferExecutionData(address(paymentToken), address(0xabcd), 1 ether); @@ -312,7 +313,7 @@ contract OrchestratorTest is BaseTest { paymentToken.mint(d.eoa, 10 ether); // Create base intent with common fields - Intent memory baseIntent; + Orchestrator.Intent memory baseIntent; baseIntent.eoa = d.eoa; baseIntent.paymentToken = address(paymentToken); baseIntent.paymentAmount = 0.1 ether; @@ -321,49 +322,50 @@ contract OrchestratorTest is BaseTest { // Test case 1: Intent with no expiry (expiry = 0) should always be valid { - Intent memory u = baseIntent; + Orchestrator.Intent memory u = baseIntent; u.nonce = d.d.getNonce(0); u.executionData = _transferExecutionData(address(paymentToken), address(0xabcd), 1 ether); u.expiry = 0; // No expiry u.signature = _sig(d, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); assertEq(paymentToken.balanceOf(address(0xabcd)), 1 ether); } // Test case 2: Intent with future expiry should be valid { - Intent memory u = baseIntent; + Orchestrator.Intent memory u = baseIntent; u.nonce = d.d.getNonce(0); u.executionData = _transferExecutionData(address(paymentToken), address(0xbcde), 1 ether); u.expiry = block.timestamp + 1 hours; // Future expiry u.signature = _sig(d, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); assertEq(paymentToken.balanceOf(address(0xbcde)), 1 ether); } // Test case 3: Intent with past expiry should fail { - Intent memory u = baseIntent; + Orchestrator.Intent memory u = baseIntent; u.nonce = d.d.getNonce(0); // This will be 2 after the previous two intents u.executionData = _transferExecutionData(address(paymentToken), address(0xcdef), 1 ether); u.expiry = block.timestamp - 1; // Past expiry u.signature = _sig(d, u); - bytes4 result = oc.execute(encodeIntent(u)); + bytes4 result = oc.execute(abi.encode(u)); assertEq(result, bytes4(keccak256("IntentExpired()"))); assertEq(paymentToken.balanceOf(address(0xcdef)), 0); // Transfer should not happen } + // Test case 4: Batch execution with mixed expired and valid intents { bytes[] memory encodedIntents = new bytes[](3); // Create base intent for batch with smaller amounts - Intent memory batchBase; + Orchestrator.Intent memory batchBase; batchBase.eoa = d.eoa; batchBase.paymentToken = address(paymentToken); batchBase.paymentAmount = 0.05 ether; @@ -371,31 +373,31 @@ contract OrchestratorTest is BaseTest { batchBase.combinedGas = 10000000; // Valid intent with nonce 2 - Intent memory u1 = batchBase; + Orchestrator.Intent memory u1 = batchBase; u1.nonce = 2; u1.executionData = _transferExecutionData(address(paymentToken), address(0x1111), 0.5 ether); u1.expiry = block.timestamp + 1 hours; u1.signature = _sig(d, u1); - encodedIntents[0] = encodeIntent(u1); + encodedIntents[0] = abi.encode(u1); // Expired intent with nonce 3 - Intent memory u2 = batchBase; + Orchestrator.Intent memory u2 = batchBase; u2.nonce = 3; u2.executionData = _transferExecutionData(address(paymentToken), address(0x2222), 0.5 ether); u2.expiry = block.timestamp - 1; u2.signature = _sig(d, u2); - encodedIntents[1] = encodeIntent(u2); + encodedIntents[1] = abi.encode(u2); // Another valid intent with nonce 3 (since nonce 3 wasn't consumed due to expiry) - Intent memory u3 = batchBase; + Orchestrator.Intent memory u3 = batchBase; u3.nonce = 3; u3.executionData = _transferExecutionData(address(paymentToken), address(0x3333), 0.5 ether); u3.expiry = 0; // No expiry u3.signature = _sig(d, u3); - encodedIntents[2] = encodeIntent(u3); + encodedIntents[2] = abi.encode(u3); bytes4[] memory errors = oc.execute(encodedIntents); assertEq(errors.length, 3); @@ -422,7 +424,7 @@ contract OrchestratorTest is BaseTest { paymentToken.mint(ds[i].eoa, 1 ether); vm.deal(ds[i].eoa, 1 ether); - Intent memory u; + Orchestrator.Intent memory u; u.eoa = ds[i].eoa; u.nonce = 0; u.executionData = @@ -435,7 +437,7 @@ contract OrchestratorTest is BaseTest { u.combinedGas = 10000000; u.signature = _sig(ds[i], u); - encodeIntents[i] = encodeIntent(u); + encodeIntents[i] = abi.encode(u); } bytes memory data = abi.encodeWithSignature("execute(bytes[])", encodeIntents); @@ -461,7 +463,7 @@ contract OrchestratorTest is BaseTest { vm.prank(d.eoa); d.d.authorize(k.k); - Intent memory u; + Orchestrator.Intent memory u; u.eoa = d.eoa; u.executionData = _executionData(address(0), 0, bytes("")); u.nonce = 0x2; @@ -471,12 +473,12 @@ contract OrchestratorTest is BaseTest { u.combinedGas = 20000000; u.signature = _sig(k, u); - oc.execute(encodeIntent(u)); + oc.execute(abi.encode(u)); } function testInvalidateNonce(uint96 seqKey, uint64 seq, uint64 seq2) public { uint256 nonce = (uint256(seqKey) << 64) | uint256(seq); - Intent memory u; + Orchestrator.Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; @@ -516,9 +518,9 @@ contract OrchestratorTest is BaseTest { u.signature = _sig(d, u); if (seq > type(uint64).max - 2) { - assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("InvalidNonce()"))); + assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("InvalidNonce()"))); } else { - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); } } @@ -537,7 +539,7 @@ contract OrchestratorTest is BaseTest { p.paymentPerGas, p.combinedGasIncrement, p.combinedGasVerificationOffset, - encodeIntent(p.u) + abi.encode(p.u) ); vm.revertToStateAndDelete(snapshot); @@ -591,7 +593,7 @@ contract OrchestratorTest is BaseTest { preCall.signature = _eoaSig(payer.privateKey, oc.computeDigest(preCall)); // Create an Intent with the pre-call and fee payer - Intent memory u; + ICommon.Intent memory u; u.eoa = eoa; u.payer = address(payer.d); u.nonce = 0; @@ -613,7 +615,7 @@ contract OrchestratorTest is BaseTest { u.encodedPreCalls[0] = abi.encode(preCall); // Sign the intent with the ephemeral key and the payment with the payer - bytes32 digest = computeDigest(u); + bytes32 digest = oc.computeDigest(u); u.signature = _eoaSig(ephemeralPK, digest); u.paymentSignature = _eoaSig(payer.privateKey, digest); @@ -621,7 +623,7 @@ contract OrchestratorTest is BaseTest { uint256 payerBalanceBefore = paymentToken.balanceOf(address(payer.d)); assertFalse(MockAccount(payable(payer.eoa)).keyCount() > 0); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); // Verify the session key was authorized in the payer's account assertTrue(MockAccount(payable(payer.eoa)).keyCount() > 0); @@ -654,7 +656,7 @@ contract OrchestratorTest is BaseTest { function testInitAndTransferInOneShot(bytes32) public { _TestAuthorizeWithPreCallsAndTransferTemps memory t; - Intent memory u; + Orchestrator.Intent memory u; uint256 ephemeralPK = _randomPrivateKey(); t.eoa = vm.addr(ephemeralPK); @@ -743,14 +745,14 @@ contract OrchestratorTest is BaseTest { // Test without gas estimation. u.combinedGas = 10000000; u.signature = _sig(kSession, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); assertEq(_balanceOf(tokenToTransfer, address(0xabcd)), 0.5 ether); } function testAuthorizeWithPreCallsAndTransfer(bytes32) public { _TestAuthorizeWithPreCallsAndTransferTemps memory t; - Intent memory u; + Orchestrator.Intent memory u; Orchestrator.SignedCall memory pInit; if (_randomChance(2)) { @@ -900,21 +902,21 @@ contract OrchestratorTest is BaseTest { if (t.testInvalidPreCallEOA) { u.combinedGas = 10000000; u.signature = _sig(kSession, u); - assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("InvalidPreCallEOA()"))); + assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("InvalidPreCallEOA()"))); return; // Skip the rest. } if (t.testPreCallVerificationError) { u.combinedGas = 10000000; u.signature = _sig(kSession, u); - assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("PreCallVerificationError()"))); + assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("PreCallVerificationError()"))); return; // Skip the rest. } if (t.testPreCallError) { u.combinedGas = 10000000; u.signature = _sig(kSession, u); - assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("PreCallError()"))); + assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("PreCallError()"))); return; // Skip the rest. } @@ -931,12 +933,12 @@ contract OrchestratorTest is BaseTest { u.combinedGas = t.gCombined; u.signature = _sig(kSession, u); - assertEq(oc.execute{gas: t.gExecute}(encodeIntent(u)), 0); + assertEq(oc.execute{gas: t.gExecute}(abi.encode(u)), 0); } else { // Otherwise, test without gas estimation. u.combinedGas = 10000000; u.signature = _sig(kSession, u); - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); } assertEq(paymentToken.balanceOf(address(0xabcd)), 0.5 ether); @@ -966,7 +968,7 @@ contract OrchestratorTest is BaseTest { // 1 ether in the EOA for execution. vm.deal(address(d.d), 1 ether); - Intent memory u; + Orchestrator.Intent memory u; u.eoa = d.eoa; u.payer = address(payer.d); @@ -979,7 +981,7 @@ contract OrchestratorTest is BaseTest { u.executionData = _transferExecutionData(address(0), address(0xabcd), 1 ether); u.paymentRecipient = address(0x12345); - bytes32 digest = computeDigest(u); + bytes32 digest = oc.computeDigest(u); uint256 snapshot = vm.snapshotState(); // To allow paymasters to be used in simulation mode. @@ -988,12 +990,12 @@ contract OrchestratorTest is BaseTest { vm.revertToStateAndDelete(snapshot); u.combinedGas = gCombined; - digest = computeDigest(u); + digest = oc.computeDigest(u); u.signature = _eoaSig(d.privateKey, digest); u.paymentSignature = _eoaSig(payer.privateKey, digest); uint256 payerBalanceBefore = _balanceOf(u.paymentToken, address(payer.d)); - assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); + assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); assertEq(d.d.getNonce(0), u.nonce + 1); assertEq(_balanceOf(u.paymentToken, u.paymentRecipient), u.paymentAmount); assertEq(_balanceOf(u.paymentToken, address(payer.d)), payerBalanceBefore - u.paymentAmount); @@ -1032,7 +1034,7 @@ contract OrchestratorTest is BaseTest { t.token = _randomChance(2) ? address(0) : address(paymentToken); t.isWithState = _randomChance(2); - Intent memory u; + Orchestrator.Intent memory u; t.d = _randomEIP7702DelegatedEOA(); vm.deal(t.d.eoa, type(uint192).max); @@ -1050,7 +1052,7 @@ contract OrchestratorTest is BaseTest { if (t.isWithState) { t.withState.increaseFunds(u.paymentToken, u.eoa, t.funds); } else { - bytes32 digest = computeDigest(u); + bytes32 digest = oc.computeDigest(u); digest = t.withSignature.computeSignatureDigest(digest); u.paymentSignature = _sig(t.withSignatureEOA, digest); t.corruptSignature = _randomChance(2); @@ -1068,7 +1070,7 @@ contract OrchestratorTest is BaseTest { t.withSignature.setApprovedOrchestrator(address(oc), false); } if ((t.unapprovedOrchestrator && u.paymentAmount != 0)) { - assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("Unauthorized()"))); + assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("Unauthorized()"))); if (u.paymentAmount != 0) { assertEq(t.d.d.getNonce(0), u.nonce); @@ -1079,7 +1081,7 @@ contract OrchestratorTest is BaseTest { } else if (t.isWithState && u.paymentAmount > t.funds && u.paymentAmount != 0) { // Arithmetic underflow error assertEq( - oc.execute(encodeIntent(u)), + oc.execute(abi.encode(u)), 0x4e487b7100000000000000000000000000000000000000000000000000000000 ); @@ -1097,7 +1099,7 @@ contract OrchestratorTest is BaseTest { } } else if ((!t.isWithState && t.corruptSignature && u.paymentAmount != 0)) { // Pre payment will not happen - assertEq(oc.execute(encodeIntent(u)), bytes4(keccak256("InvalidSignature()"))); + assertEq(oc.execute(abi.encode(u)), bytes4(keccak256("InvalidSignature()"))); // If prePayment is 0, then nonce is incremented, because the prePayment doesn't fail. if (u.paymentAmount == 0) { assertEq(t.d.d.getNonce(0), u.nonce + 1); @@ -1107,7 +1109,7 @@ contract OrchestratorTest is BaseTest { assertEq(_balanceOf(t.token, u.payer), t.balanceBefore); assertEq(_balanceOf(address(0), address(0xabcd)), 0); } else { - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); assertEq(t.d.d.getNonce(0), u.nonce + 1); assertEq(_balanceOf(t.token, u.payer), t.balanceBefore - u.paymentAmount); assertEq(_balanceOf(address(0), address(0xabcd)), 1 ether); @@ -1126,7 +1128,7 @@ contract OrchestratorTest is BaseTest { t.testImplementationCheck = _randomChance(2); t.requireWrongImplementation = _randomChance(2); - Intent memory u; + Orchestrator.Intent memory u; vm.deal(t.d.eoa, type(uint192).max); u.eoa = t.d.eoa; @@ -1146,12 +1148,12 @@ contract OrchestratorTest is BaseTest { if (t.testImplementationCheck && t.requireWrongImplementation) { assertEq( - oc.execute(encodeIntent(u)), bytes4(keccak256("UnsupportedAccountImplementation()")) + oc.execute(abi.encode(u)), bytes4(keccak256("UnsupportedAccountImplementation()")) ); assertEq(t.d.d.getNonce(0), u.nonce); assertEq(_balanceOf(address(0), address(0xabcd)), 0); } else { - assertEq(oc.execute(encodeIntent(u)), 0); + assertEq(oc.execute(abi.encode(u)), 0); assertEq(t.d.d.getNonce(0), u.nonce + 1); assertEq(_balanceOf(address(0), address(0xabcd)), 1 ether); } @@ -1238,7 +1240,7 @@ contract OrchestratorTest is BaseTest { vm.expectRevert(bytes4(keccak256("InvalidKeyHash()"))); t.d.d.execute(_ERC7821_BATCH_EXECUTION_MODE, abi.encode(calls)); - Intent memory u; + Orchestrator.Intent memory u; u.eoa = t.d.eoa; u.nonce = t.d.d.getNonce(0); u.executionData = abi.encode(calls); @@ -1248,13 +1250,14 @@ contract OrchestratorTest is BaseTest { u.signature = _sig(t.multiSigKey, u); // Test unwrapAndValidateSignature - bytes32 digest = computeDigest(u); + bytes32 digest = oc.computeDigest(u); (bool isValid, bytes32 keyHash) = t.d.d.unwrapAndValidateSignature(digest, _sig(t.multiSigKey, digest)); assertEq(isValid, true); assertEq(keyHash, _hash(t.multiSigKey.k)); - assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); + + assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); (uint256 _threshold, bytes32[] memory o) = t.multiSigSigner.getConfig(address(t.d.d), _hash(t.multiSigKey.k)); @@ -1278,13 +1281,14 @@ contract OrchestratorTest is BaseTest { if (newThreshold == 0) { vm.expectRevert(bytes4(keccak256("InvalidThreshold()"))); } + (gExecute, gCombined,) = _estimateGasForMultiSigKey(t.multiSigKey, u); u.combinedGas = gCombined; u.signature = _sig(t.multiSigKey, u); if (newThreshold > 0) { - assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); + assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); (_threshold, o) = t.multiSigSigner.getConfig(address(t.d.d), _hash(t.multiSigKey.k)); assertEq(_threshold, newThreshold); @@ -1293,6 +1297,7 @@ contract OrchestratorTest is BaseTest { t.multiSigKey.threshold = newThreshold; } } + // Test removeOwner { uint256 removeIndex = _bound(_random(), 0, o.length - 1); @@ -1311,7 +1316,7 @@ contract OrchestratorTest is BaseTest { u.combinedGas = gCombined; u.signature = _sig(t.multiSigKey, u); - assertEq(oc.execute{gas: gExecute}(encodeIntent(u)), 0); + assertEq(oc.execute{gas: gExecute}(abi.encode(u)), 0); (_threshold, o) = t.multiSigSigner.getConfig(address(t.d.d), _hash(t.multiSigKey.k)); assertEq(o.length, t.multiSigKey.owners.length); @@ -1332,9 +1337,9 @@ contract OrchestratorTest is BaseTest { DelegatedEOA d; PassKey k; // Intent data - Intent baseIntent; - Intent arbIntent; - Intent outputIntent; + ICommon.Intent baseIntent; + ICommon.Intent arbIntent; + ICommon.Intent outputIntent; // Merkle data bytes32[] leafs; bytes32 root; @@ -1422,11 +1427,12 @@ contract OrchestratorTest is BaseTest { // 1. Prepare the output intent first to get its digest as settlementId t.outputIntent.eoa = t.d.eoa; - t.outputIntent.nonce = t.d.d.getNonce(0x6D76 << 176); + t.outputIntent.nonce = t.d.d.getNonce(0); t.outputIntent.executionData = _transferExecutionData(address(t.usdcMainnet), t.friend, 1000); t.outputIntent.combinedGas = 1000000; t.outputIntent.settler = address(t.settler); + t.outputIntent.isMultichain = true; { bytes[] memory encodedFundTransfers = new bytes[](1); @@ -1445,12 +1451,13 @@ contract OrchestratorTest is BaseTest { // Compute the output intent digest to use as settlementId vm.chainId(1); // Mainnet - t.settlementId = computeDigest(t.outputIntent); + t.settlementId = oc.computeDigest(t.outputIntent); // Base Intent with escrow execution data t.baseIntent.eoa = t.d.eoa; - t.baseIntent.nonce = t.d.d.getNonce(0x6D76 << 176); + t.baseIntent.nonce = t.d.d.getNonce(0); t.baseIntent.combinedGas = 1000000; + t.baseIntent.isMultichain = true; // Create Base escrow execution data { @@ -1490,8 +1497,9 @@ contract OrchestratorTest is BaseTest { // Arbitrum Intent with escrow execution data t.arbIntent.eoa = t.d.eoa; - t.arbIntent.nonce = t.d.d.getNonce(0x6D76 << 176); + t.arbIntent.nonce = t.d.d.getNonce(0); t.arbIntent.combinedGas = 1000000; + t.arbIntent.isMultichain = true; // Create Arbitrum escrow execution data { IEscrow.Escrow[] memory escrows = new IEscrow.Escrow[](1); @@ -1538,14 +1546,13 @@ contract OrchestratorTest is BaseTest { // User has 600 USDC on base t.usdcBase.mint(t.d.eoa, 600); - t.encodedIntents[0] = encodeIntent(t.baseIntent); + t.encodedIntents[0] = abi.encode(t.baseIntent); // User escrows funds on Base vm.expectEmit(true, false, false, false, address(t.escrowBase)); emit Escrow.EscrowCreated(t.escrowIdBase); vm.prank(t.gasWallet); t.errs = oc.execute(t.encodedIntents); assertEq(uint256(bytes32(t.errs[0])), 0); - // Verify funds are escrowed, not transferred yet vm.assertEq(t.usdcBase.balanceOf(address(t.escrowBase)), 600); vm.assertEq(t.usdcBase.balanceOf(t.relay), 0); @@ -1556,7 +1563,7 @@ contract OrchestratorTest is BaseTest { // User has 500 USDC on arb t.usdcArb.mint(t.d.eoa, 500); // Unhappy case, try to send base intent to arb - t.encodedIntents[0] = encodeIntent(t.baseIntent); + t.encodedIntents[0] = abi.encode(t.baseIntent); vm.prank(t.gasWallet); t.errs = oc.execute(t.encodedIntents); assertEq( @@ -1568,15 +1575,15 @@ contract OrchestratorTest is BaseTest { bytes32[] memory wrongLeafs = new bytes32[](3); // Some random leaf - wrongLeafs[0] = computeDigest(t.arbIntent); - wrongLeafs[1] = computeDigest(t.arbIntent); - wrongLeafs[2] = computeDigest(t.outputIntent); + wrongLeafs[0] = oc.computeDigest(t.arbIntent); + wrongLeafs[1] = oc.computeDigest(t.arbIntent); + wrongLeafs[2] = oc.computeDigest(t.outputIntent); bytes memory correctSig = t.arbIntent.signature; t.arbIntent.signature = abi.encode(merkleHelper.getProof(wrongLeafs, 1), t.root, t.rootSig); - t.encodedIntents[0] = encodeIntent(t.arbIntent); + t.encodedIntents[0] = abi.encode(t.arbIntent); vm.prank(t.gasWallet); t.errs = oc.execute(t.encodedIntents); assertEq( @@ -1589,7 +1596,7 @@ contract OrchestratorTest is BaseTest { } // User escrows funds on Arb - t.encodedIntents[0] = encodeIntent(t.arbIntent); + t.encodedIntents[0] = abi.encode(t.arbIntent); vm.expectEmit(true, false, false, false, address(t.escrowArb)); emit Escrow.EscrowCreated(t.escrowIdArb); vm.prank(t.gasWallet); @@ -1615,7 +1622,7 @@ contract OrchestratorTest is BaseTest { emit SimpleSettler.Sent(address(oc), t.settlementId, 42161); // Arbitrum // Relay funds the user account, and the intended execution happens. - t.encodedIntents[0] = encodeIntent(t.outputIntent); + t.encodedIntents[0] = abi.encode(t.outputIntent); vm.prank(t.gasWallet); t.errs = oc.execute(t.encodedIntents); assertEq(uint256(bytes32(t.errs[0])), 0); @@ -1650,7 +1657,7 @@ contract OrchestratorTest is BaseTest { // Re-execute the escrow on Base (to recreate the state) t.usdcBase.mint(t.d.eoa, 600); - t.encodedIntents[0] = encodeIntent(t.baseIntent); + t.encodedIntents[0] = abi.encode(t.baseIntent); vm.expectEmit(true, false, false, false, address(t.escrowBase)); emit Escrow.EscrowCreated(t.escrowIdBase); vm.prank(t.gasWallet); @@ -1679,7 +1686,7 @@ contract OrchestratorTest is BaseTest { // Re-execute the escrow on Arbitrum (to recreate the state) t.usdcArb.mint(t.d.eoa, 500); - t.encodedIntents[0] = encodeIntent(t.arbIntent); + t.encodedIntents[0] = abi.encode(t.arbIntent); vm.expectEmit(true, false, false, false, address(t.escrowArb)); emit Escrow.EscrowCreated(t.escrowIdArb); vm.prank(t.gasWallet); @@ -1726,7 +1733,7 @@ contract OrchestratorTest is BaseTest { t.outputIntent.funderSignature = _eoaSig(wrongPrivateKey, t.leafs[2]); - t.encodedIntents[0] = encodeIntent(t.outputIntent); + t.encodedIntents[0] = abi.encode(t.outputIntent); vm.prank(t.gasWallet); t.errs = oc.execute(t.encodedIntents); @@ -1749,11 +1756,11 @@ contract OrchestratorTest is BaseTest { function _computeMerkleData(_TestMultiChainIntentTemps memory t) internal { t.leafs = new bytes32[](3); vm.chainId(8453); - t.leafs[0] = computeDigest(t.baseIntent); + t.leafs[0] = oc.computeDigest(t.baseIntent); vm.chainId(42161); - t.leafs[1] = computeDigest(t.arbIntent); + t.leafs[1] = oc.computeDigest(t.arbIntent); vm.chainId(1); - t.leafs[2] = computeDigest(t.outputIntent); + t.leafs[2] = oc.computeDigest(t.outputIntent); t.root = merkleHelper.getRoot(t.leafs); @@ -1762,23 +1769,8 @@ contract OrchestratorTest is BaseTest { t.outputIntent.funderSignature = _eoaSig(t.funderPrivateKey, t.leafs[2]); - t.baseIntent.signature = abi.encodePacked( - abi.encode(merkleHelper.getProof(t.leafs, 0), t.root, t.rootSig), - bytes32(0), - uint8(0), - uint8(1) - ); - t.arbIntent.signature = abi.encodePacked( - abi.encode(merkleHelper.getProof(t.leafs, 1), t.root, t.rootSig), - bytes32(0), - uint8(0), - uint8(1) - ); - t.outputIntent.signature = abi.encodePacked( - abi.encode(merkleHelper.getProof(t.leafs, 2), t.root, t.rootSig), - bytes32(0), - uint8(0), - uint8(1) - ); + t.baseIntent.signature = abi.encode(merkleHelper.getProof(t.leafs, 0), t.root, t.rootSig); + t.arbIntent.signature = abi.encode(merkleHelper.getProof(t.leafs, 1), t.root, t.rootSig); + t.outputIntent.signature = abi.encode(merkleHelper.getProof(t.leafs, 2), t.root, t.rootSig); } } diff --git a/test/SimulateExecute.t.sol b/test/SimulateExecute.t.sol index 8e2d52b5..1e4a89a4 100644 --- a/test/SimulateExecute.t.sol +++ b/test/SimulateExecute.t.sol @@ -58,7 +58,7 @@ contract SimulateExecuteTest is BaseTest { abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) ); - Intent memory i; + Orchestrator.Intent memory i; i.eoa = d.eoa; i.nonce = 0; i.executionData = t.executionData; @@ -79,19 +79,19 @@ contract SimulateExecuteTest is BaseTest { // If the caller does not have max balance, then the simulation should revert. vm.expectRevert(bytes4(keccak256("StateOverrideError()"))); (t.gUsed, t.gCombined) = - simulator.simulateV1Logs(address(oc), 0, 1, 11_000, 10_000, encodeIntent(i)); + simulator.simulateV1Logs(address(oc), 0, 1, 11_000, 10_000, abi.encode(i)); vm.expectRevert(bytes4(keccak256("StateOverrideError()"))); - oc.simulateExecute(encodeIntent(i, true, type(uint256).max)); + oc.simulateExecute(true, type(uint256).max, abi.encode(i)); vm.expectPartialRevert(bytes4(keccak256("SimulationPassed(uint256)"))); - oc.simulateExecute(encodeIntent(i, false, type(uint256).max)); + oc.simulateExecute(false, type(uint256).max, abi.encode(i)); uint256 snapshot = vm.snapshotState(); vm.deal(_ORIGIN_ADDRESS, type(uint192).max); (t.gUsed, t.gCombined) = - simulator.simulateV1Logs(address(oc), 2, 1e11, 11_000, 0, encodeIntent(i)); + simulator.simulateV1Logs(address(oc), 2, 1e11, 11_000, 0, abi.encode(i)); vm.revertToStateAndDelete(snapshot); i.combinedGas = t.gCombined; @@ -101,11 +101,11 @@ contract SimulateExecuteTest is BaseTest { i.signature = _sig(d, i); vm.expectRevert(bytes4(keccak256("InsufficientGas()"))); - oc.execute{gas: t.gExecute}(encodeIntent(i)); + oc.execute{gas: t.gExecute}(abi.encode(i)); t.gExecute = Math.mulDiv(t.gCombined + 110_000, 64, 63); - assertEq(oc.execute{gas: t.gExecute}(encodeIntent(i)), 0); + assertEq(oc.execute{gas: t.gExecute}(abi.encode(i)), 0); } function testSimulateExecuteNoRevertUnderfundedReverts() public { @@ -127,7 +127,7 @@ contract SimulateExecuteTest is BaseTest { abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) ); - Intent memory i; + Orchestrator.Intent memory i; i.eoa = d.eoa; i.nonce = 0; i.executionData = t.executionData; @@ -146,11 +146,11 @@ contract SimulateExecuteTest is BaseTest { } vm.expectRevert(bytes4(keccak256("PaymentError()"))); - simulator.simulateV1Logs(address(oc), 0, 1, 11_000, 0, encodeIntent(i)); + simulator.simulateV1Logs(address(oc), 0, 1, 11_000, 0, abi.encode(i)); deal(i.paymentToken, address(i.eoa), 0x112233112233112233112233); vm.expectRevert(bytes4(keccak256("PaymentError()"))); - simulator.simulateCombinedGas(address(oc), 0, 1, 11_000, encodeIntent(i)); + simulator.simulateCombinedGas(address(oc), 0, 1, 11_000, abi.encode(i)); } function testSimulateExecuteNoRevert() public { @@ -173,7 +173,7 @@ contract SimulateExecuteTest is BaseTest { abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) ); - Intent memory i; + Orchestrator.Intent memory i; i.eoa = d.eoa; i.nonce = 0; i.executionData = t.executionData; @@ -195,7 +195,7 @@ contract SimulateExecuteTest is BaseTest { vm.deal(_ORIGIN_ADDRESS, type(uint192).max); (t.gUsed, t.gCombined) = - simulator.simulateV1Logs(address(oc), 2, 1e11, 11_000, 0, encodeIntent(i)); + simulator.simulateV1Logs(address(oc), 2, 1e11, 11_000, 0, abi.encode(i)); vm.revertToStateAndDelete(snapshot); @@ -205,7 +205,7 @@ contract SimulateExecuteTest is BaseTest { i.signature = _sig(d, i); - assertEq(oc.execute{gas: t.gExecute}(encodeIntent(i)), 0); + assertEq(oc.execute{gas: t.gExecute}(abi.encode(i)), 0); assertEq(gasBurner.randomness(), t.randomness); } @@ -229,7 +229,7 @@ contract SimulateExecuteTest is BaseTest { abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) ); - Intent memory i; + Orchestrator.Intent memory i; i.eoa = d.eoa; i.nonce = 0; i.executionData = t.executionData; @@ -251,7 +251,7 @@ contract SimulateExecuteTest is BaseTest { vm.deal(_ORIGIN_ADDRESS, type(uint192).max); (t.gUsed, t.gCombined) = - simulator.simulateV1Logs(address(oc), 2, 1e11, 10_800, 0, encodeIntent(i)); + simulator.simulateV1Logs(address(oc), 2, 1e11, 10_800, 0, abi.encode(i)); vm.revertToStateAndDelete(snapshot); @@ -261,7 +261,7 @@ contract SimulateExecuteTest is BaseTest { i.signature = _sig(d, i); - assertEq(oc.execute{gas: t.gExecute}(encodeIntent(i)), 0); + assertEq(oc.execute{gas: t.gExecute}(abi.encode(i)), 0); assertEq(gasBurner.randomness(), t.randomness); } @@ -289,7 +289,7 @@ contract SimulateExecuteTest is BaseTest { abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) ); - Intent memory i; + Orchestrator.Intent memory i; i.eoa = d.eoa; i.nonce = 0; i.executionData = t.executionData; @@ -312,7 +312,7 @@ contract SimulateExecuteTest is BaseTest { vm.deal(_ORIGIN_ADDRESS, type(uint192).max); (t.gUsed, t.gCombined) = - simulator.simulateV1Logs(address(oc), 2, 1e11, 12_000, 10_000, encodeIntent(i)); + simulator.simulateV1Logs(address(oc), 2, 1e11, 12_000, 10_000, abi.encode(i)); vm.revertToStateAndDelete(snapshot); @@ -321,7 +321,7 @@ contract SimulateExecuteTest is BaseTest { i.signature = _sig(k, i); - assertEq(oc.execute{gas: t.gExecute}(encodeIntent(i)), 0); + assertEq(oc.execute{gas: t.gExecute}(abi.encode(i)), 0); assertEq(gasBurner.randomness(), t.randomness); } diff --git a/test/SubAccounts.t.sol b/test/SubAccounts.t.sol deleted file mode 100644 index 68417582..00000000 --- a/test/SubAccounts.t.sol +++ /dev/null @@ -1,270 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.4; - -import "./Base.t.sol"; -import {ERC20} from "solady/tokens/ERC20.sol"; -import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; -import {ERC7821} from "solady/accounts/ERC7821.sol"; - -/// @title SubAccounts Test Suite -/// @notice Tests the parent-child account architecture with spending permissions -contract SubAccountsTest is BaseTest { - using SafeTransferLib for address; - - // Main account (parent) - controlled by user - DelegatedEOA mainAccount; - PassKey mainAccountKey; - - // Sub account (child) - controlled by DApp - DelegatedEOA subAccount; - PassKey dappKey; - - // Test tokens - MockPaymentToken usdc; - MockPaymentToken dai; - - // DApp that controls the sub account - address dappAddress; - address constant dappRecipient = address(0xDA99); - - // Spend IDs for tracking permissions - uint256 constant PARENT_SWEEP_SPEND_ID = 1; - uint256 constant CHILD_PULL_SPEND_ID = 2; - - function setUp() public override { - super.setUp(); - - // Setup tokens - usdc = new MockPaymentToken(); - dai = new MockPaymentToken(); - - // Setup main account (parent) - mainAccount = _randomEIP7702DelegatedEOA(); - mainAccountKey = _randomPassKey(); - mainAccountKey.k.isSuperAdmin = true; // Main account key needs to be super admin to call setSpend - vm.prank(address(mainAccount.d)); - mainAccount.d.authorize(mainAccountKey.k); - - // Setup sub account (controlled by DApp) - subAccount = _randomEIP7702DelegatedEOA(); - dappKey = _randomPassKey(); - dappKey.k.isSuperAdmin = true; // DApp has full control of sub account - vm.prank(address(subAccount.d)); - subAccount.d.authorize(dappKey.k); - - // Setup DApp address - dappAddress = vm.addr(uint256(keccak256("dapp"))); - } - - /// @notice Tests the complete subaccount flow with DApp integration - /// Flow: - /// 1. Create main account with funds - /// 2. Create sub account (controlled by DApp, no initial funds needed) - /// 3. Sub account grants parent sweep permission - /// 4. Main account grants sub account limited pull permission - /// 5. DApp executes bundle: pulls funds from main, sends to DApp - /// 6. Parent can sweep remaining funds back - function test_CompleteSubAccountFlowWithDApp() public { - // ============================================ - // STEP 1: Fund the main account - // ============================================ - uint256 mainAccountInitialBalance = 1000e6; // 1000 USDC - usdc.mint(address(mainAccount.eoa), mainAccountInitialBalance); - dai.mint(address(mainAccount.eoa), 500e18); // 500 DAI - - assertEq(usdc.balanceOf(address(mainAccount.eoa)), mainAccountInitialBalance); - assertEq(dai.balanceOf(address(mainAccount.eoa)), 500e18); - - // Sub account starts with 0 funds (DApp pays for gas) - assertEq(usdc.balanceOf(address(subAccount.eoa)), 0); - - // ============================================ - // STEP 2: Sub account grants parent sweep permission - // This allows parent to recover any funds from sub account - // ============================================ - { - // Create setSpend call to grant parent sweep permission - ERC7821.Call[] memory calls = new ERC7821.Call[](1); - - calls[0] = ERC7821.Call({ - to: address(subAccount.d), // Call to self - value: 0, - data: abi.encodeWithSelector( - IthacaAccount.setSpend.selector, - PARENT_SWEEP_SPEND_ID, - true, // isParent = true (can sweep everything) - uint32(0), // No expiry for parent - address(mainAccount.eoa), // Parent is the spender - new address[](0), // Tokens are ignored, because parent can sweep everything - new uint256[](0) // Limits are ignored, because parent can sweep everything - ) - }); - - // Execute as the subAccount EOA directly (no signature needed) - vm.prank(subAccount.eoa); - subAccount.d.execute(_ERC7821_BATCH_EXECUTION_MODE, abi.encode(calls)); - } - - // ============================================ - // STEP 3: Main account grants sub account limited pull permission - // This allows sub account to pull up to 100 USDC and 50 DAI - // ============================================ - { - // Create setSpend call to grant sub account limited permission - ERC7821.Call[] memory calls = new ERC7821.Call[](1); - address[] memory tokens = new address[](2); - tokens[0] = address(usdc); - tokens[1] = address(dai); - uint256[] memory limits = new uint256[](2); - limits[0] = 100e6; // 100 USDC limit - limits[1] = 50e18; // 50 DAI limit - - calls[0] = ERC7821.Call({ - to: address(mainAccount.d), // Call to self - value: 0, - data: abi.encodeWithSelector( - IthacaAccount.setSpend.selector, - CHILD_PULL_SPEND_ID, - false, // isParent = false (limited permissions) - uint32(block.timestamp + 30 days), // 30 day expiry - address(subAccount.eoa), // Sub account is the spender - tokens, - limits - ) - }); - - // Execute as the mainAccount EOA directly (no signature needed) - vm.prank(mainAccount.eoa); - mainAccount.d.execute(_ERC7821_BATCH_EXECUTION_MODE, abi.encode(calls)); - } - - // ============================================ - // STEP 4: Test direct spend call from subAccount to mainAccount - // ============================================ - uint256 amountToPull = 50e6; // 50 USDC - { - // SubAccount calls spend on mainAccount directly - address[] memory pullTokens = new address[](1); - pullTokens[0] = address(usdc); - uint256[] memory pullAmounts = new uint256[](1); - pullAmounts[0] = amountToPull; - - vm.prank(subAccount.eoa); - mainAccount.d.spend( - CHILD_PULL_SPEND_ID, pullTokens, pullAmounts, address(subAccount.eoa) - ); - - // Verify the transfer worked - assertEq( - usdc.balanceOf(address(mainAccount.eoa)), mainAccountInitialBalance - amountToPull - ); - assertEq(usdc.balanceOf(address(subAccount.eoa)), amountToPull); - } - - // ============================================ - // STEP 5: DApp executes bundle via sub account - // - Pulls more funds from main account - // - Then sends to DApp recipient - // ============================================ - uint256 secondPullAmount = 30e6; // 30 USDC - { - Orchestrator.Intent memory intent; - intent.eoa = address(subAccount.eoa); - intent.nonce = subAccount.d.getNonce(0); - intent.expiry = block.timestamp + 1 days; - intent.paymentToken = address(paymentToken); - intent.paymentAmount = 0 ether; - intent.paymentRecipient = address(0xfee); - intent.combinedGas = 1000000; - - // Create bundle: pull from main, then send to DApp - ERC7821.Call[] memory calls = new ERC7821.Call[](2); - - address[] memory pullTokens = new address[](1); - pullTokens[0] = address(usdc); - uint256[] memory pullAmounts = new uint256[](1); - pullAmounts[0] = secondPullAmount; - - calls[0] = ERC7821.Call({ - to: address(mainAccount.d), - value: 0, - data: abi.encodeWithSelector( - IthacaAccount.spend.selector, - CHILD_PULL_SPEND_ID, - pullTokens, - pullAmounts, - address(subAccount.eoa) - ) - }); - - // Send all funds to DApp (first pull + second pull) - calls[1] = ERC7821.Call({ - to: address(usdc), - value: 0, - data: abi.encodeWithSelector( - ERC20.transfer.selector, dappRecipient, amountToPull + secondPullAmount - ) - }); - - intent.executionData = abi.encode(calls); - intent.signature = _sig(dappKey, intent); - - // Execute the bundle - assertEq(oc.execute(abi.encode(intent)), 0); - - // Verify balances - assertEq( - usdc.balanceOf(address(mainAccount.eoa)), - mainAccountInitialBalance - amountToPull - secondPullAmount - ); - assertEq(usdc.balanceOf(address(subAccount.eoa)), 0); - assertEq(usdc.balanceOf(dappRecipient), amountToPull + secondPullAmount); - } - - // ============================================ - // STEP 6: Test that sub account cannot exceed limits (should fail) - // ============================================ - { - // Try to pull 21 more USDC (total would be 50+30+21 = 101, exceeding 100 limit) - address[] memory pullTokens = new address[](1); - pullTokens[0] = address(usdc); - uint256[] memory pullAmounts = new uint256[](1); - pullAmounts[0] = 21e6; // This would exceed the 100 USDC limit - - // Should revert due to exceeding limit - vm.prank(subAccount.eoa); - vm.expectRevert(); - mainAccount.d.spend( - CHILD_PULL_SPEND_ID, pullTokens, pullAmounts, address(subAccount.eoa) - ); - } - - // // ============================================ - // // STEP 7: Parent sweeps funds back from sub account - // // ============================================ - { - // Parent can sweep everything from sub account - address[] memory sweepTokens = new address[](1); - sweepTokens[0] = address(usdc); - uint256[] memory sweepAmounts = new uint256[](1); - sweepAmounts[0] = usdc.balanceOf(address(subAccount.eoa)); - - uint256 mainAccountPreBalance = usdc.balanceOf(address(mainAccount.eoa)); - - // Parent directly calls spend on sub account - vm.prank(mainAccount.eoa); - subAccount.d.spend( - PARENT_SWEEP_SPEND_ID, - sweepTokens, - sweepAmounts, - address(mainAccount.eoa) // Sweep back to parent - ); - - // Verify parent recovered all the funds - assertEq(usdc.balanceOf(address(subAccount.eoa)), 0); - assertEq( - usdc.balanceOf(address(mainAccount.eoa)), mainAccountPreBalance + sweepAmounts[0] - ); - } - } -} diff --git a/test/UpgradeTests.t.sol b/test/UpgradeTests.t.sol new file mode 100644 index 00000000..d0d28d09 --- /dev/null +++ b/test/UpgradeTests.t.sol @@ -0,0 +1,566 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "./utils/SoladyTest.sol"; +import {IthacaAccount} from "./utils/mocks/MockAccount.sol"; +import {GuardedExecutor} from "../src/IthacaAccount.sol"; +import {BaseTest} from "./Base.t.sol"; +import {EIP7702Proxy} from "solady/accounts/EIP7702Proxy.sol"; +import {LibEIP7702} from "solady/accounts/LibEIP7702.sol"; +import {MockPaymentToken} from "./utils/mocks/MockPaymentToken.sol"; +import {LibClone} from "solady/utils/LibClone.sol"; +import {Orchestrator, MockOrchestrator} from "./utils/mocks/MockOrchestrator.sol"; +import {ERC7821} from "solady/accounts/ERC7821.sol"; + +contract UpgradeTests is BaseTest { + address payable public oldProxyAddress; + address public oldImplementation; + address public newImplementation; + + // Test EOA that will be delegated to the proxy + address public userEOA; + uint256 public userEOAKey; + IthacaAccount public userAccount; + + // Test keys + PassKey public p256Key; + PassKey public p256SuperAdminKey; + PassKey public secp256k1Key; + PassKey public secp256k1SuperAdminKey; + PassKey public webAuthnP256Key; + PassKey public externalKey; + + // Test tokens + MockPaymentToken public mockToken1; + MockPaymentToken public mockToken2; + + // Random addresses for testing transfers + address[] public randomRecipients; + + // State capture - using simple mappings to avoid memory-to-storage issues + // Pre-upgrade state + bytes32[] preKeyHashes; + mapping(bytes32 => IthacaAccount.Key) preKeys; + mapping(bytes32 => bool) preAuthorized; + uint256 preEthBalance; + uint256 preToken1Balance; + uint256 preToken2Balance; + uint256 preNonce; + // Get expected old version from environment variable + string public expectedOldVersion = vm.envString("UPGRADE_TEST_OLD_VERSION"); + + // Post-upgrade state + bytes32[] postKeyHashes; + mapping(bytes32 => IthacaAccount.Key) postKeys; + mapping(bytes32 => bool) postAuthorized; + uint256 postEthBalance; + uint256 postToken1Balance; + uint256 postToken2Balance; + uint256 postNonce; + + function setUp() public override { + super.setUp(); + + // Fork the network to get the proxy bytecode + string memory rpcUrl = vm.envString("UPGRADE_TEST_RPC_URL"); + vm.createSelectFork(rpcUrl); + + // Deploy test tokens + mockToken1 = new MockPaymentToken(); + mockToken2 = new MockPaymentToken(); + + // Setup random recipients + for (uint256 i = 0; i < 5; i++) { + randomRecipients.push(_randomAddress()); + } + + // Get old proxy address from environment + oldProxyAddress = payable(vm.envAddress("UPGRADE_TEST_OLD_PROXY")); + + // Setup delegated EOA + (userEOA, userEOAKey) = _randomUniqueSigner(); + + vm.etch(userEOA, abi.encodePacked(hex"ef0100", address(oldProxyAddress))); + + userAccount = IthacaAccount(payable(userEOA)); + + // Get the bytecode of the old proxy from the forked network + bytes memory proxyBytecode = oldProxyAddress.code; + require(proxyBytecode.length > 0, "No bytecode at old proxy address"); + + newImplementation = address(new IthacaAccount(address(oc))); + + // Generate test keys + p256Key = _randomSecp256r1PassKey(); + p256Key.k.isSuperAdmin = false; + p256Key.k.expiry = 0; // Never expires + + p256SuperAdminKey = _randomSecp256r1PassKey(); + p256SuperAdminKey.k.isSuperAdmin = true; + p256SuperAdminKey.k.expiry = uint40(block.timestamp + 365 days); // Expires in 1 year + + secp256k1Key = _randomSecp256k1PassKey(); + secp256k1Key.k.isSuperAdmin = false; + secp256k1Key.k.expiry = 0; + + secp256k1SuperAdminKey = _randomSecp256k1PassKey(); + secp256k1SuperAdminKey.k.isSuperAdmin = true; + secp256k1SuperAdminKey.k.expiry = uint40(block.timestamp + 30 days); // Expires in 30 days + + webAuthnP256Key = _randomSecp256r1PassKey(); + webAuthnP256Key.k.keyType = IthacaAccount.KeyType.WebAuthnP256; + webAuthnP256Key.k.isSuperAdmin = false; + webAuthnP256Key.k.expiry = 0; + + // Setup external key + address externalSigner = _randomAddress(); + bytes12 salt = bytes12(uint96(_randomUniform())); + externalKey.k.keyType = IthacaAccount.KeyType.External; + externalKey.k.publicKey = abi.encodePacked(externalSigner, salt); + externalKey.k.isSuperAdmin = false; + externalKey.k.expiry = uint40(block.timestamp + 7 days); + externalKey.keyHash = _hash(externalKey.k); + } + + function test_ComprehensiveUpgrade() public { + // Step 2: Setup the old account with various configurations + _setupOldAccountState(); + + // Step 3: Capture pre-upgrade state + _capturePreUpgradeState(); + + // Step 4: Perform upgrade + _performUpgrade(); + + // Step 5: Capture post-upgrade state + _capturePostUpgradeState(); + + // Step 6: Verify state preservation + _verifyStatePreservation(); + + // Step 7: Test post-upgrade functionality + _testPostUpgradeFunctionality(); + } + + function _performUpgrade() internal { + // Get the version from the new implementation for comparison + IthacaAccount newImpl = IthacaAccount(payable(newImplementation)); + (,, string memory expectedNewVersion,,,,) = newImpl.eip712Domain(); + + // Check version before upgrade matches expected old version + (,, string memory versionBefore,,,,) = userAccount.eip712Domain(); + assertEq( + keccak256(bytes(versionBefore)), + keccak256(bytes(expectedOldVersion)), + string.concat("Version before upgrade should be ", expectedOldVersion) + ); + + // Perform the upgrade + bytes memory upgradeCalldata = + abi.encodeWithSelector(IthacaAccount.upgradeProxyAccount.selector, newImplementation); + + vm.prank(userEOA); + (bool success,) = userEOA.call(upgradeCalldata); + require(success, "Upgrade failed"); + + // Check version after upgrade + (,, string memory versionAfter,,,,) = userAccount.eip712Domain(); + + // Verify the version matches the new implementation version after upgrade + assertEq( + keccak256(bytes(versionAfter)), + keccak256(bytes(expectedNewVersion)), + string.concat("Version after upgrade should be ", expectedNewVersion) + ); + } + + function _setupOldAccountState() internal { + // Authorize various keys + vm.startPrank(userEOA); + + userAccount.authorize(p256Key.k); + p256Key.keyHash = _hash(p256Key.k); + + userAccount.authorize(secp256k1Key.k); + secp256k1Key.keyHash = _hash(secp256k1Key.k); + + userAccount.authorize(secp256k1SuperAdminKey.k); + secp256k1SuperAdminKey.keyHash = _hash(secp256k1SuperAdminKey.k); + + userAccount.authorize(webAuthnP256Key.k); + webAuthnP256Key.keyHash = _hash(webAuthnP256Key.k); + + externalKey.keyHash = userAccount.authorize(externalKey.k); + + // Setup spending limits for different keys + _setupSpendingLimits(); + + // Setup execution permissions + _setupExecutionPermissions(); + + // Fund the account + _fundAccount(); + + vm.stopPrank(); + } + + function _setupSpendingLimits() internal { + // Only set spending limits for non-super admin keys + + // Daily ETH limit for secp256k1Key (not a super admin) + if (secp256k1Key.keyHash != bytes32(0)) { + userAccount.setSpendLimit( + secp256k1Key.keyHash, address(0), GuardedExecutor.SpendPeriod.Day, 1 ether + ); + + // Weekly ETH limit for secp256k1Key + userAccount.setSpendLimit( + secp256k1Key.keyHash, address(0), GuardedExecutor.SpendPeriod.Week, 5 ether + ); + + // Monthly token1 limit for secp256k1Key + userAccount.setSpendLimit( + secp256k1Key.keyHash, + address(mockToken1), + GuardedExecutor.SpendPeriod.Month, + 1000e18 + ); + } + + // Daily token2 limit for webAuthnP256Key (not a super admin) + if (webAuthnP256Key.keyHash != bytes32(0)) { + userAccount.setSpendLimit( + webAuthnP256Key.keyHash, + address(mockToken2), + GuardedExecutor.SpendPeriod.Day, + 100e18 + ); + } + + // Hour ETH limit for externalKey (not a super admin) + if (externalKey.keyHash != bytes32(0)) { + userAccount.setSpendLimit( + externalKey.keyHash, address(0), GuardedExecutor.SpendPeriod.Hour, 0.1 ether + ); + } + + // Forever limit for p256Key (if authorized and not a super admin) + if (p256Key.keyHash != bytes32(0) && !p256Key.k.isSuperAdmin) { + userAccount.setSpendLimit( + p256Key.keyHash, address(0), GuardedExecutor.SpendPeriod.Forever, 10 ether + ); + } + } + + function _setupExecutionPermissions() internal { + // Setup canExecute permissions (only for non-super admin keys) + address target1 = address(0x1234); + address target2 = address(0x5678); + bytes4 selector1 = bytes4(keccak256("transfer(address,uint256)")); + bytes4 selector2 = bytes4(keccak256("approve(address,uint256)")); + + // Only set for non-super admin secp256k1Key + if (secp256k1Key.keyHash != bytes32(0) && !secp256k1Key.k.isSuperAdmin) { + userAccount.setCanExecute(secp256k1Key.keyHash, target1, selector1, true); + userAccount.setCanExecute(secp256k1Key.keyHash, target2, selector2, true); + } + + // Only set for p256Key if it's not a super admin + if (p256Key.keyHash != bytes32(0) && !p256Key.k.isSuperAdmin) { + userAccount.setCanExecute(p256Key.keyHash, target1, selector2, true); + } + + // Only set for webAuthnP256Key if it's not a super admin + if (webAuthnP256Key.keyHash != bytes32(0) && !webAuthnP256Key.k.isSuperAdmin) { + userAccount.setCanExecute(webAuthnP256Key.keyHash, target2, selector1, true); + } + + // Setup call checkers (only for non-super admin keys) + address checker1 = address(0xAAAA); + address checker2 = address(0xBBBB); + + if (secp256k1Key.keyHash != bytes32(0) && !secp256k1Key.k.isSuperAdmin) { + userAccount.setCallChecker(secp256k1Key.keyHash, target1, checker1); + } + + if (webAuthnP256Key.keyHash != bytes32(0) && !webAuthnP256Key.k.isSuperAdmin) { + userAccount.setCallChecker(webAuthnP256Key.keyHash, target2, checker2); + } + } + + function _fundAccount() internal { + // Fund with ETH + vm.deal(address(userAccount), 10 ether); + + // Fund with tokens + mockToken1.mint(address(userAccount), 10000e18); + mockToken2.mint(address(userAccount), 5000e18); + } + + function _capturePreUpgradeState() internal { + // Capture authorized keys + (, bytes32[] memory keyHashes) = userAccount.getKeys(); + + // Clear and populate pre-upgrade key hashes + delete preKeyHashes; + for (uint256 i = 0; i < keyHashes.length; i++) { + preKeyHashes.push(keyHashes[i]); + bytes32 keyHash = keyHashes[i]; + preKeys[keyHash] = userAccount.getKey(keyHash); + preAuthorized[keyHash] = true; + } + + // Capture balances + preEthBalance = address(userAccount).balance; + preToken1Balance = mockToken1.balanceOf(address(userAccount)); + preToken2Balance = mockToken2.balanceOf(address(userAccount)); + + // Capture nonce + preNonce = userAccount.getNonce(0); + } + + function _capturePostUpgradeState() internal { + // Capture authorized keys + (, bytes32[] memory keyHashes) = userAccount.getKeys(); + + // Clear and populate post-upgrade key hashes + delete postKeyHashes; + for (uint256 i = 0; i < keyHashes.length; i++) { + postKeyHashes.push(keyHashes[i]); + bytes32 keyHash = keyHashes[i]; + postKeys[keyHash] = userAccount.getKey(keyHash); + postAuthorized[keyHash] = true; + } + + // Capture balances + postEthBalance = address(userAccount).balance; + postToken1Balance = mockToken1.balanceOf(address(userAccount)); + postToken2Balance = mockToken2.balanceOf(address(userAccount)); + + // Capture nonce + postNonce = userAccount.getNonce(0); + } + + function _verifyStatePreservation() internal view { + // Verify all keys are preserved + assertEq(preKeyHashes.length, postKeyHashes.length, "Number of authorized keys changed"); + + for (uint256 i = 0; i < preKeyHashes.length; i++) { + bytes32 keyHash = preKeyHashes[i]; + + assertTrue(postAuthorized[keyHash], "Key was deauthorized during upgrade"); + + IthacaAccount.Key memory preKey = preKeys[keyHash]; + IthacaAccount.Key memory postKey = postKeys[keyHash]; + + assertEq(preKey.expiry, postKey.expiry, "Key expiry changed"); + assertEq(uint8(preKey.keyType), uint8(postKey.keyType), "Key type changed"); + assertEq(preKey.isSuperAdmin, postKey.isSuperAdmin, "Key super admin status changed"); + assertEq(preKey.publicKey, postKey.publicKey, "Key public key changed"); + } + + // Verify balances preserved + assertEq(preEthBalance, postEthBalance, "ETH balance changed"); + assertEq(preToken1Balance, postToken1Balance, "Token1 balance changed"); + assertEq(preToken2Balance, postToken2Balance, "Token2 balance changed"); + + // Verify nonce preserved + assertEq(preNonce, postNonce, "Nonce changed"); + } + + function _testPostUpgradeFunctionality() internal { + vm.startPrank(userEOA); + + // Test 1: P256 keys can now be super admins (new in v0.5.7+) + PassKey memory newP256SuperAdmin = _randomSecp256r1PassKey(); + newP256SuperAdmin.k.isSuperAdmin = true; + newP256SuperAdmin.k.expiry = 0; + + // This should succeed in upgraded version + bytes32 newP256KeyHash = userAccount.authorize(newP256SuperAdmin.k); + IthacaAccount.Key memory retrievedKey = userAccount.getKey(newP256KeyHash); + assertEq( + uint8(retrievedKey.keyType), uint8(IthacaAccount.KeyType.P256), "Key type mismatch" + ); + assertTrue(retrievedKey.isSuperAdmin, "P256 should be super admin after upgrade"); + + // Test 2: Add a new non-super-admin key and set spending limit + PassKey memory newRegularKey = _randomSecp256k1PassKey(); + newRegularKey.k.isSuperAdmin = false; + newRegularKey.k.expiry = 0; + + bytes32 newRegularKeyHash = userAccount.authorize(newRegularKey.k); + + // Set spending limit for the regular key (not super admin) + userAccount.setSpendLimit( + newRegularKeyHash, address(0), GuardedExecutor.SpendPeriod.Week, 2 ether + ); + + GuardedExecutor.SpendInfo[] memory spendInfos = userAccount.spendInfos(newRegularKeyHash); + bool foundWeeklyLimit = false; + for (uint256 i = 0; i < spendInfos.length; i++) { + if ( + spendInfos[i].period == GuardedExecutor.SpendPeriod.Week + && spendInfos[i].token == address(0) + ) { + assertEq(spendInfos[i].limit, 2 ether, "Weekly limit not set correctly"); + foundWeeklyLimit = true; + break; + } + } + assertTrue(foundWeeklyLimit, "Weekly limit not found"); + + // Test 3: Verify keys can still be used (without actual execution) + // We verify the key is still authorized and has correct properties + IthacaAccount.Key memory existingKey = userAccount.getKey(secp256k1Key.keyHash); + assertEq( + uint8(existingKey.keyType), uint8(IthacaAccount.KeyType.Secp256k1), "Key type changed" + ); + assertFalse(existingKey.isSuperAdmin, "Key admin status changed"); + + // Test 4: Test revoke and re-authorize with a new key + // Create a new key to test revoke/re-authorize functionality + PassKey memory testRevokeKey = _randomSecp256k1PassKey(); + testRevokeKey.k.isSuperAdmin = false; + testRevokeKey.k.expiry = 0; + + bytes32 testRevokeKeyHash = userAccount.authorize(testRevokeKey.k); + + // Now revoke it + userAccount.revoke(testRevokeKeyHash); + + // Verify key is revoked by checking it no longer exists + // After revocation, getKey will revert with KeyDoesNotExist + vm.expectRevert(abi.encodeWithSelector(IthacaAccount.KeyDoesNotExist.selector)); + userAccount.getKey(testRevokeKeyHash); + + // Re-authorize + bytes32 reauthorizedHash = userAccount.authorize(testRevokeKey.k); + assertEq(reauthorizedHash, testRevokeKeyHash, "Key hash changed on re-authorization"); + + vm.stopPrank(); + } + + function test_UpgradeWithSpendLimitEnabledFlag() public { + // This test verifies the spend limit enabled flag feature added in newer versions + + vm.startPrank(userEOA); + + // Authorize a key with spending limits + PassKey memory testKey = _randomSecp256k1PassKey(); + bytes32 keyHash = userAccount.authorize(testKey.k); + + // Set spending limit + userAccount.setSpendLimit(keyHash, address(0), GuardedExecutor.SpendPeriod.Day, 0.5 ether); + + // Fund account + vm.deal(address(userAccount), 5 ether); + + vm.stopPrank(); + + // Perform upgrade + _performUpgrade(); + + // Verify spending limits still work after upgrade + GuardedExecutor.SpendInfo[] memory spendInfos = userAccount.spendInfos(keyHash); + assertEq(spendInfos.length, 1, "Spending limit not preserved"); + assertEq(spendInfos[0].limit, 0.5 ether, "Spending limit value changed"); + + vm.stopPrank(); + } + + function test_UpgradeWithMultipleKeyTypes() public { + // Test upgrade with all key types authorized + + vm.startPrank(userEOA); + + // Authorize all key types + PassKey[] memory keys = new PassKey[](4); + keys[0] = _randomSecp256r1PassKey(); + keys[1] = _randomSecp256k1PassKey(); + keys[2] = _randomSecp256r1PassKey(); + keys[2].k.keyType = IthacaAccount.KeyType.WebAuthnP256; + keys[3].k.keyType = IthacaAccount.KeyType.External; + keys[3].k.publicKey = abi.encodePacked(_randomAddress(), bytes12(uint96(_randomUniform()))); + keys[3].keyHash = _hash(keys[3].k); + + bytes32[] memory keyHashes = new bytes32[](4); + for (uint256 i = 0; i < keys.length; i++) { + // Some key types might fail in old versions, handle gracefully + try userAccount.authorize(keys[i].k) returns (bytes32 kh) { + keyHashes[i] = kh; + } catch { + // Skip if authorization fails + } + } + + // Capture authorized count before upgrade + (, bytes32[] memory keyHashesBefore) = userAccount.getKeys(); + uint256 authorizedCountBefore = keyHashesBefore.length; + + vm.stopPrank(); + + // Perform upgrade + _performUpgrade(); + + // Verify all keys preserved + (, bytes32[] memory keyHashesAfter) = userAccount.getKeys(); + uint256 authorizedCountAfter = keyHashesAfter.length; + assertEq(authorizedCountBefore, authorizedCountAfter, "Key count changed during upgrade"); + + vm.stopPrank(); + } + + function test_UpgradePreservesComplexSpendingState() public { + // Test that complex spending state with partially spent limits is preserved + + vm.startPrank(userEOA); + + // Setup key and limits + PassKey memory spendKey = _randomSecp256k1PassKey(); + bytes32 keyHash = userAccount.authorize(spendKey.k); + + // Set multiple spending limits + userAccount.setSpendLimit(keyHash, address(0), GuardedExecutor.SpendPeriod.Day, 1 ether); + userAccount.setSpendLimit(keyHash, address(0), GuardedExecutor.SpendPeriod.Week, 3 ether); + userAccount.setSpendLimit(keyHash, address(0), GuardedExecutor.SpendPeriod.Month, 10 ether); + + // Fund account + vm.deal(address(userAccount), 20 ether); + vm.stopPrank(); + + // Capture spending state before upgrade + GuardedExecutor.SpendInfo[] memory spendsBefore = userAccount.spendInfos(keyHash); + + // Verify spending limits are set + uint256 limitsCount = 0; + for (uint256 i = 0; i < spendsBefore.length; i++) { + if (spendsBefore[i].token == address(0)) { + limitsCount++; + } + } + assertEq(limitsCount, 3, "Should have 3 ETH spending limits"); + + // Perform upgrade + _performUpgrade(); + + // Verify spending state preserved + GuardedExecutor.SpendInfo[] memory spendsAfter = userAccount.spendInfos(keyHash); + + // Verify all limits still exist + uint256 limitsCountAfter = 0; + for (uint256 i = 0; i < spendsAfter.length; i++) { + if (spendsAfter[i].token == address(0)) { + limitsCountAfter++; + } + } + assertEq(limitsCountAfter, 3, "Spending limits not preserved after upgrade"); + + // Verify limits match + assertEq(spendsBefore.length, spendsAfter.length, "Number of spending limits changed"); + for (uint256 i = 0; i < spendsBefore.length; i++) { + assertEq(spendsBefore[i].limit, spendsAfter[i].limit, "Limit value changed"); + assertEq(uint8(spendsBefore[i].period), uint8(spendsAfter[i].period), "Period changed"); + } + } +} diff --git a/test/utils/Brutalizer.sol b/test/utils/Brutalizer.sol index 969c5f86..1c6447ae 100644 --- a/test/utils/Brutalizer.sol +++ b/test/utils/Brutalizer.sol @@ -825,9 +825,7 @@ contract Brutalizer { let remainder := and(length, 0x1f) if remainder { if shl(mul(8, remainder), lastWord) { notZeroRightPadded := 1 } } // Check if the memory allocated is sufficient. - if length { - if gt(add(add(s, 0x20), length), mload(0x40)) { insufficientMalloc := 1 } - } + if length { if gt(add(add(s, 0x20), length), mload(0x40)) { insufficientMalloc := 1 } } } if (notZeroRightPadded) revert("Not zero right padded!"); if (insufficientMalloc) revert("Insufficient memory allocation!"); diff --git a/test/utils/interfaces/IPimlicoPaymaster.sol b/test/utils/interfaces/IPimlicoPaymaster.sol index d3d62e0b..2665aedc 100644 --- a/test/utils/interfaces/IPimlicoPaymaster.sol +++ b/test/utils/interfaces/IPimlicoPaymaster.sol @@ -19,10 +19,10 @@ library PimlicoHelpers { /// @notice The length of the mode and allowAllBundlers bytes. uint8 constant MODE_AND_ALLOW_ALL_BUNDLERS_LENGTH = 1; - /// @notice The length of the ERC-20 config without signature. + /// @notice The length of the ERC-20 config without singature. uint8 constant ERC20_PAYMASTER_DATA_LENGTH = 117; - /// @notice The length of the verifying config without signature. + /// @notice The length of the verfiying config without singature. uint8 constant VERIFYING_PAYMASTER_DATA_LENGTH = 12; // 12 uint256 constant PAYMASTER_DATA_OFFSET = 52; diff --git a/test/utils/interfaces/ISafe.sol b/test/utils/interfaces/ISafe.sol new file mode 100644 index 00000000..d9c3d3e1 --- /dev/null +++ b/test/utils/interfaces/ISafe.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import "./IERC4337EntryPoint.sol"; + +interface ISafe { + function setup( + address[] calldata _owners, + uint256 _threshold, + address to, + bytes calldata data, + address fallbackHandler, + address paymentToken, + uint256 payment, + address paymentReceiver + ) external; + + function enableModule(address module) external; + + function execTransaction( + address to, + uint256 value, + bytes calldata data, + uint8 operation, + uint256 safeTxGas, + uint256 baseGas, + uint256 gasPrice, + address gasToken, + address payable refundReceiver, + bytes memory signatures + ) external payable returns (bool success); + + function getFallbackHandler() external view returns (address); +} + +interface ISafeProxyFactory { + function createProxyWithNonce(address _singleton, bytes memory initializer, uint256 saltNonce) + external + returns (address proxy); +} + +interface ISafe4337Module { + function SUPPORTED_ENTRYPOINT() external view returns (address); + + function getOperationHash(UserOperation calldata userOp) + external + view + returns (bytes32 operationHash); + + function executeUserOp(address to, uint256 value, bytes calldata data, uint8 operation) external; + + function domainSeparator() external view returns (bytes32); +} + +interface IAddModulesLib { + function enableModules(address[] memory modules) external; +} diff --git a/test/utils/mocks/MockOrchestrator.sol b/test/utils/mocks/MockOrchestrator.sol index a0c3226e..e21a2360 100644 --- a/test/utils/mocks/MockOrchestrator.sol +++ b/test/utils/mocks/MockOrchestrator.sol @@ -15,13 +15,8 @@ contract MockOrchestrator is Orchestrator, Brutalizer { return _computeDigest(preCall); } - // Expose internal functions for testing - function hashTypedData(bytes32 structHash) public view returns (bytes32) { - return _hashTypedData(structHash); - } - - function hashTypedDataSansChainId(bytes32 structHash) public view returns (bytes32) { - return _hashTypedDataSansChainId(structHash); + function computeDigest(Intent calldata intent) public view returns (bytes32) { + return _computeDigest(intent); } function simulateFailed(bytes calldata encodedIntent) public payable virtual { diff --git a/test/utils/mocks/MockPayerWithSignature.sol b/test/utils/mocks/MockPayerWithSignature.sol index 60678b52..1de4c6d5 100644 --- a/test/utils/mocks/MockPayerWithSignature.sol +++ b/test/utils/mocks/MockPayerWithSignature.sol @@ -51,44 +51,38 @@ contract MockPayerWithSignature is Ownable { } /// @dev Pays `paymentAmount` of `paymentToken` to the `paymentRecipient`. - /// @param paymentAmount The amount to pay - /// @param keyHash The hash of the key used to authorize the operation - /// @param intentDigest The digest of the user operation - /// @param eoa The EOA address - /// @param payer The payer address - /// @param paymentToken The token to pay with - /// @param paymentRecipient The recipient of the payment - /// @param paymentSignature The payment signature + /// The EOA and token details are extracted from the `encodedIntent`. + /// Reverts if the specified Orchestrator (`msg.sender`) is not approved, + /// if the signature is invalid, or if the nonce has already been used. + /// @param paymentAmount The amount to pay. + /// @param keyHash The key hash associated with the operation (not used in this mock's logic but kept for signature compatibility). + /// @param digest The digest of the intent (used for nonce tracking). + /// @param encodedIntent ABI encoded Intent struct. function pay( uint256 paymentAmount, bytes32 keyHash, - bytes32 intentDigest, - address eoa, - address payer, - address paymentToken, - address paymentRecipient, - bytes calldata paymentSignature + bytes32 digest, + bytes calldata encodedIntent ) public virtual { if (!isApprovedOrchestrator[msg.sender]) revert Unauthorized(); // Check and set nonce to prevent replay attacks - if (paymasterNonces[intentDigest]) { + if (paymasterNonces[digest]) { revert PaymasterNonceError(); } - paymasterNonces[intentDigest] = true; + paymasterNonces[digest] = true; - bytes32 signatureDigest = computeSignatureDigest(intentDigest); + ICommon.Intent memory u = abi.decode(encodedIntent, (ICommon.Intent)); - if (ECDSA.recover(signatureDigest, paymentSignature) != signer) { + bytes32 signatureDigest = computeSignatureDigest(digest); + + if (ECDSA.recover(signatureDigest, u.paymentSignature) != signer) { revert InvalidSignature(); } - TokenTransferLib.safeTransfer(paymentToken, paymentRecipient, paymentAmount); - - emit Compensated(paymentToken, paymentRecipient, paymentAmount, eoa, keyHash); + TokenTransferLib.safeTransfer(u.paymentToken, u.paymentRecipient, paymentAmount); - // Unused parameters - payer; + emit Compensated(u.paymentToken, u.paymentRecipient, paymentAmount, u.eoa, keyHash); } function computeSignatureDigest(bytes32 intentDigest) public view returns (bytes32) { diff --git a/test/utils/mocks/MockPayerWithSignatureOptimized.sol b/test/utils/mocks/MockPayerWithSignatureOptimized.sol new file mode 100644 index 00000000..1d7701bf --- /dev/null +++ b/test/utils/mocks/MockPayerWithSignatureOptimized.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {TokenTransferLib} from "../../../src/libraries/TokenTransferLib.sol"; +import {Ownable} from "solady/auth/Ownable.sol"; +import {ECDSA} from "solady/utils/ECDSA.sol"; +import {ICommon} from "../../../src/interfaces/ICommon.sol"; +import {IOrchestrator} from "../../../src/interfaces/IOrchestrator.sol"; +/// @dev WARNING! This mock is strictly intended for testing purposes only. +/// Do NOT copy anything here into production code unless you really know what you are doing. + +contract MockPayerWithSignatureOptimized is Ownable { + error InvalidSignature(); + + address public signer; + + address public immutable APPROVED_ORCHESTRATOR; + + event Compensated( + address indexed paymentToken, + address indexed paymentRecipient, + uint256 paymentAmount, + address indexed eoa, + bytes32 keyHash + ); + + constructor(address orchestrator) { + APPROVED_ORCHESTRATOR = orchestrator; + _initializeOwner(msg.sender); + } + + function setSigner(address newSinger) public onlyOwner { + signer = newSinger; + } + + /// @dev `address(0)` denote native token (i.e. Ether). + function withdrawTokens(address token, address recipient, uint256 amount) + public + virtual + onlyOwner + { + TokenTransferLib.safeTransfer(token, recipient, amount); + } + + /// @dev Pays `paymentAmount` of `paymentToken` to the `paymentRecipient`. + /// The EOA and token details are extracted from the `encodedIntent`. + /// Reverts if the specified Orchestrator (`msg.sender`) is not approved. + /// NOTE: This mock no longer verifies signatures within the pay function itself, + /// aligning with the Account/Orchestrator pattern where verification happens before payment. + /// @param paymentAmount The amount to pay. + /// @param keyHash The key hash associated with the operation (not used in this mock's logic but kept for signature compatibility). + /// @param encodedIntent ABI encoded Intent struct. + function pay( + uint256 paymentAmount, + bytes32 keyHash, + bytes32 digest, + bytes calldata encodedIntent + ) public virtual { + if (msg.sender != APPROVED_ORCHESTRATOR) revert Unauthorized(); + + ICommon.Intent calldata u; + assembly { + let t := calldataload(encodedIntent.offset) + u := add(t, encodedIntent.offset) + } + + bytes32 signatureDigest = computeSignatureDigest(digest); + + if (ECDSA.recover(signatureDigest, u.paymentSignature) != signer) { + revert InvalidSignature(); + } + + TokenTransferLib.safeTransfer(u.paymentToken, u.paymentRecipient, paymentAmount); + + emit Compensated(u.paymentToken, u.paymentRecipient, paymentAmount, u.eoa, keyHash); + } + + function computeSignatureDigest(bytes32 intentDigest) public view returns (bytes32) { + // We shall just use this simplified hash instead of EIP712. + return keccak256(abi.encode(intentDigest, block.chainid, address(this))); + } + + receive() external payable {} +} diff --git a/test/utils/mocks/MockPayerWithState.sol b/test/utils/mocks/MockPayerWithState.sol index e5e4c35d..365db440 100644 --- a/test/utils/mocks/MockPayerWithState.sol +++ b/test/utils/mocks/MockPayerWithState.sol @@ -54,42 +54,35 @@ contract MockPayerWithState is Ownable { } /// @dev Pays `paymentAmount` of `paymentToken` to the `paymentRecipient`. - /// @param paymentAmount The amount to pay - /// @param keyHash The hash of the key used to authorize the operation - /// @param intentDigest The digest of the user operation - /// @param eoa The EOA address - /// @param payer The payer address - /// @param paymentToken The token to pay with - /// @param paymentRecipient The recipient of the payment - /// @param paymentSignature The payment signature + /// The EOA and token details are extracted from the `encodedIntent`. + /// Reverts if the specified Orchestrator (`msg.sender`) is not approved, + /// if the EOA lacks sufficient funds, or if the nonce has already been used. + /// @param paymentAmount The amount to pay. + /// @param keyHash The key hash associated with the operation (not used in this mock's logic but kept for signature compatibility). + /// @param digest The digest of the intent (used for nonce tracking). + /// @param encodedIntent ABI encoded Intent struct. function pay( uint256 paymentAmount, bytes32 keyHash, - bytes32 intentDigest, - address eoa, - address payer, - address paymentToken, - address paymentRecipient, - bytes calldata paymentSignature + bytes32 digest, + bytes calldata encodedIntent ) public virtual { if (!isApprovedOrchestrator[msg.sender]) revert Unauthorized(); // Check and set nonce to prevent replay attacks - if (paymasterNonces[intentDigest]) { + if (paymasterNonces[digest]) { revert PaymasterNonceError(); } - paymasterNonces[intentDigest] = true; + paymasterNonces[digest] = true; + + ICommon.Intent memory u = abi.decode(encodedIntent, (ICommon.Intent)); // We shall rely on arithmetic underflow error to revert if there's insufficient funds. - funds[paymentToken][eoa] -= paymentAmount; - TokenTransferLib.safeTransfer(paymentToken, paymentRecipient, paymentAmount); + funds[u.paymentToken][u.eoa] -= paymentAmount; + TokenTransferLib.safeTransfer(u.paymentToken, u.paymentRecipient, paymentAmount); // Emit the event for debugging. - emit Compensated(paymentToken, paymentRecipient, paymentAmount, eoa, keyHash); - - // Unused parameters - payer; - paymentSignature; + emit Compensated(u.paymentToken, u.paymentRecipient, paymentAmount, u.eoa, keyHash); } receive() external payable {} diff --git a/utils/JsonBindings.sol b/utils/JsonBindings.sol deleted file mode 100644 index 1a76c5b2..00000000 --- a/utils/JsonBindings.sol +++ /dev/null @@ -1,767 +0,0 @@ -// Automatically generated by forge bind-json. - -pragma solidity >=0.6.2 <0.9.0; -pragma experimental ABIEncoderV2; - -import {GuardedExecutor} from "src/GuardedExecutor.sol"; -import {IthacaAccount} from "src/IthacaAccount.sol"; -import {MultiSigSigner} from "src/MultiSigSigner.sol"; -import {ICommon} from "src/interfaces/ICommon.sol"; -import {IEscrow} from "src/interfaces/IEscrow.sol"; -import {LibTStack} from "src/libraries/LibTStack.sol"; -import {AccountTest} from "test/Account.t.sol"; -import {BaseTest} from "test/Base.t.sol"; -import {EnforcedOptionParam} from "test/LayerZeroSettler.t.sol"; -import {MultiSigSignerTest} from "test/MultiSigSigner.t.sol"; -import {OrchestratorTest} from "test/Orchestrator.t.sol"; -import {SimulateExecuteTest} from "test/SimulateExecute.t.sol"; -import {SignatureWrapper} from "test/utils/interfaces/ICoinbaseSmartWallet.sol"; -import {IERC4337EntryPoint, IStakeManager, PackedUserOperation, UserOperation} from "test/utils/interfaces/IERC4337EntryPoint.sol"; - -interface Vm { - function parseJsonTypeArray(string calldata json, string calldata key, string calldata typeDescription) external pure returns (bytes memory); - function parseJsonType(string calldata json, string calldata typeDescription) external pure returns (bytes memory); - function parseJsonType(string calldata json, string calldata key, string calldata typeDescription) external pure returns (bytes memory); - function serializeJsonType(string calldata typeDescription, bytes memory value) external pure returns (string memory json); - function serializeJsonType(string calldata objectKey, string calldata valueKey, string calldata typeDescription, bytes memory value) external returns (string memory json); -} - -library JsonBindings { - Vm constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); - - string constant schema_Intent = "Intent(address eoa,bytes executionData,uint256 nonce,address payer,address paymentToken,uint256 paymentMaxAmount,uint256 combinedGas,bytes[] encodedPreCalls,bytes[] encodedFundTransfers,address settler,uint256 expiry,bool isMultichain,address funder,bytes funderSignature,bytes settlerContext,uint256 paymentAmount,address paymentRecipient,bytes signature,bytes paymentSignature,address supportedAccountImplementation)"; - string constant schema_SignedCall = "SignedCall(address eoa,bytes executionData,uint256 nonce,bytes signature)"; - string constant schema_Transfer = "Transfer(address token,uint256 amount)"; - string constant schema_SpendInfo = "SpendInfo(address token,uint8 period,uint256 limit,uint256 spent,uint256 lastUpdated,uint256 currentSpent,uint256 current)"; - string constant schema_CallCheckerInfo = "CallCheckerInfo(address target,address checker)"; - string constant schema_TokenPeriodSpend = "TokenPeriodSpend(uint256 limit,uint256 spent,uint256 lastUpdated)"; - string constant schema__ExecuteTemps = "_ExecuteTemps(DynamicArray approvedERC20s,DynamicArray approvalSpenders,DynamicArray erc20s,DynamicArray transferAmounts,DynamicArray permit2ERC20s,DynamicArray permit2Spenders)DynamicArray(uint256[] data)"; - string constant schema_TStack = "TStack(uint256 slot)"; - string constant schema_Key = "Key(uint40 expiry,uint8 keyType,bool isSuperAdmin,bytes publicKey)"; - string constant schema_KeyExtraStorage = "KeyExtraStorage(AddressSet checkers)AddressSet(uint256 _spacer)"; - string constant schema_Escrow = "Escrow(bytes12 salt,address depositor,address recipient,address token,uint256 escrowAmount,uint256 refundAmount,uint256 refundTimestamp,address settler,address sender,bytes32 settlementId,uint256 senderChainId)"; - string constant schema_Config = "Config(uint256 threshold,bytes32[] ownerKeyHashes)"; - string constant schema_TargetFunctionPayload = "TargetFunctionPayload(address by,uint256 value,bytes data)"; - string constant schema_PassKey = "PassKey(Key k,uint256 privateKey,bytes32 keyHash)Key(uint40 expiry,uint8 keyType,bool isSuperAdmin,bytes publicKey)"; - string constant schema_MultiSigKey = "MultiSigKey(Key k,uint256 threshold,PassKey[] owners)Key(uint40 expiry,uint8 keyType,bool isSuperAdmin,bytes publicKey)PassKey(Key k,uint256 privateKey,bytes32 keyHash)"; - string constant schema_DelegatedEOA = "DelegatedEOA(address eoa,uint256 privateKey,address d)"; - string constant schema__EstimateGasParams = "_EstimateGasParams(Intent u,uint8 paymentPerGasPrecision,uint256 paymentPerGas,uint256 combinedGasIncrement,uint256 combinedGasVerificationOffset)Intent(address eoa,bytes executionData,uint256 nonce,address payer,address paymentToken,uint256 paymentMaxAmount,uint256 combinedGas,bytes[] encodedPreCalls,bytes[] encodedFundTransfers,address settler,uint256 expiry,bool isMultichain,address funder,bytes funderSignature,bytes settlerContext,uint256 paymentAmount,address paymentRecipient,bytes signature,bytes paymentSignature,address supportedAccountImplementation)"; - string constant schema__TestExecuteWithSignatureTemps = "_TestExecuteWithSignatureTemps(TargetFunctionPayload[] targetFunctionPayloads,Call[] calls,uint256 n,uint256 nonce,bytes opData,bytes executionData)Call(address to,uint256 value,bytes data)TargetFunctionPayload(address by,uint256 value,bytes data)"; - string constant schema__TestUpgradeAccountWithPassKeyTemps = "_TestUpgradeAccountWithPassKeyTemps(uint256 randomVersion,address implementation,Call[] calls,uint256 nonce,bytes opData,bytes executionData)Call(address to,uint256 value,bytes data)"; - string constant schema_DepositInfo = "DepositInfo(uint256 deposit,bool staked,uint112 stake,uint32 unstakeDelaySec,uint48 withdrawTime)"; - string constant schema_StakeInfo = "StakeInfo(uint256 stake,uint256 unstakeDelaySec)"; - string constant schema_PackedUserOperation = "PackedUserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,bytes32 accountGasLimits,uint256 preVerificationGas,bytes32 gasFees,bytes paymasterAndData,bytes signature)"; - string constant schema_UserOperation = "UserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,uint256 callGasLimit,uint256 verificationGasLimit,uint256 preVerificationGas,uint256 maxFeePerGas,uint256 maxPriorityFeePerGas,bytes paymasterAndData,bytes signature)"; - string constant schema_UserOpsPerAggregator = "UserOpsPerAggregator(PackedUserOperation[] userOps,address aggregator,bytes signature)PackedUserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,bytes32 accountGasLimits,uint256 preVerificationGas,bytes32 gasFees,bytes paymasterAndData,bytes signature)"; - string constant schema_ReturnInfo = "ReturnInfo(uint256 preOpGas,uint256 prefund,uint256 accountValidationData,uint256 paymasterValidationData,bytes paymasterContext)"; - string constant schema_SignatureWrapper = "SignatureWrapper(uint8 ownerIndex,bytes signatureData)"; - string constant schema_EnforcedOptionParam = "EnforcedOptionParam(uint32 eid,uint16 msgType,bytes options)"; - string constant schema_MultiSigTestTemps = "MultiSigTestTemps(PassKey[] owners,bytes32[] ownerKeyHashes,uint256 threshold,MultiSigKey multiSigKey,bytes32 digest)Key(uint40 expiry,uint8 keyType,bool isSuperAdmin,bytes publicKey)MultiSigKey(Key k,uint256 threshold,PassKey[] owners)PassKey(Key k,uint256 privateKey,bytes32 keyHash)"; - string constant schema__TestFullFlowTemps = "_TestFullFlowTemps(Intent[] intents,TargetFunctionPayload[] targetFunctionPayloads,DelegatedEOA[] delegatedEOAs,bytes[] encodedIntents)DelegatedEOA(address eoa,uint256 privateKey,address d)Intent(address eoa,bytes executionData,uint256 nonce,address payer,address paymentToken,uint256 paymentMaxAmount,uint256 combinedGas,bytes[] encodedPreCalls,bytes[] encodedFundTransfers,address settler,uint256 expiry,bool isMultichain,address funder,bytes funderSignature,bytes settlerContext,uint256 paymentAmount,address paymentRecipient,bytes signature,bytes paymentSignature,address supportedAccountImplementation)TargetFunctionPayload(address by,uint256 value,bytes data)"; - string constant schema__TestAuthorizeWithPreCallsAndTransferTemps = "_TestAuthorizeWithPreCallsAndTransferTemps(uint256 gExecute,uint256 gCombined,uint256 gUsed,bool success,bytes result,bool testInvalidPreCallEOA,bool testPreCallVerificationError,bool testPreCallError,bool testInit,bool testEOACoalesce,bool testSkipNonce,uint192 superAdminNonceSeqKey,uint192 sessionNonceSeqKey,uint256 retrievedSuperAdminNonce,uint256 retrievedSessionNonce,PassKey kInit,DelegatedEOA d,address eoa)DelegatedEOA(address eoa,uint256 privateKey,address d)Key(uint40 expiry,uint8 keyType,bool isSuperAdmin,bytes publicKey)PassKey(Key k,uint256 privateKey,bytes32 keyHash)"; - string constant schema__TestPayViaAnotherPayerTemps = "_TestPayViaAnotherPayerTemps(address withState,address withSignature,DelegatedEOA withSignatureEOA,address token,uint256 funds,bool isWithState,bool corruptSignature,bool unapprovedOrchestrator,uint256 balanceBefore,DelegatedEOA d)DelegatedEOA(address eoa,uint256 privateKey,address d)"; - string constant schema__TestAccountImplementationVerificationTemps = "_TestAccountImplementationVerificationTemps(bool testImplementationCheck,bool requireWrongImplementation,DelegatedEOA d)DelegatedEOA(address eoa,uint256 privateKey,address d)"; - string constant schema__TestMultiSigTemps = "_TestMultiSigTemps(DelegatedEOA d,address multiSigSigner,uint256 numKeys,MultiSigKey multiSigKey)DelegatedEOA(address eoa,uint256 privateKey,address d)Key(uint40 expiry,uint8 keyType,bool isSuperAdmin,bytes publicKey)MultiSigKey(Key k,uint256 threshold,PassKey[] owners)PassKey(Key k,uint256 privateKey,bytes32 keyHash)"; - string constant schema__TestMultiChainIntentTemps = "_TestMultiChainIntentTemps(address funder,address settler,uint256 funderPrivateKey,address usdcMainnet,address usdcArb,address usdcBase,DelegatedEOA d,PassKey k,Intent baseIntent,Intent arbIntent,Intent outputIntent,bytes32[] leafs,bytes32 root,bytes rootSig,address gasWallet,address relay,address friend,address settlementOracle,address escrowBase,address escrowArb,bytes32 settlementId,bytes32 escrowIdBase,bytes32 escrowIdArb,bytes[] encodedIntents,bytes4[] errs,uint256 snapshot)DelegatedEOA(address eoa,uint256 privateKey,address d)Intent(address eoa,bytes executionData,uint256 nonce,address payer,address paymentToken,uint256 paymentMaxAmount,uint256 combinedGas,bytes[] encodedPreCalls,bytes[] encodedFundTransfers,address settler,uint256 expiry,bool isMultichain,address funder,bytes funderSignature,bytes settlerContext,uint256 paymentAmount,address paymentRecipient,bytes signature,bytes paymentSignature,address supportedAccountImplementation)Key(uint40 expiry,uint8 keyType,bool isSuperAdmin,bytes publicKey)PassKey(Key k,uint256 privateKey,bytes32 keyHash)"; - string constant schema__SimulateExecuteTemps = "_SimulateExecuteTemps(uint256 gasToBurn,uint256 randomness,uint256 gExecute,uint256 gCombined,uint256 gUsed,bytes executionData,bool success,bytes result)"; - - function serialize(ICommon.Intent memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_Intent, abi.encode(value)); - } - - function serialize(ICommon.Intent memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_Intent, abi.encode(value)); - } - - function deserializeIntent(string memory json) public pure returns (ICommon.Intent memory) { - return abi.decode(vm.parseJsonType(json, schema_Intent), (ICommon.Intent)); - } - - function deserializeIntent(string memory json, string memory path) public pure returns (ICommon.Intent memory) { - return abi.decode(vm.parseJsonType(json, path, schema_Intent), (ICommon.Intent)); - } - - function deserializeIntentArray(string memory json, string memory path) public pure returns (ICommon.Intent[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_Intent), (ICommon.Intent[])); - } - - function serialize(ICommon.SignedCall memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_SignedCall, abi.encode(value)); - } - - function serialize(ICommon.SignedCall memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_SignedCall, abi.encode(value)); - } - - function deserializeSignedCall(string memory json) public pure returns (ICommon.SignedCall memory) { - return abi.decode(vm.parseJsonType(json, schema_SignedCall), (ICommon.SignedCall)); - } - - function deserializeSignedCall(string memory json, string memory path) public pure returns (ICommon.SignedCall memory) { - return abi.decode(vm.parseJsonType(json, path, schema_SignedCall), (ICommon.SignedCall)); - } - - function deserializeSignedCallArray(string memory json, string memory path) public pure returns (ICommon.SignedCall[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_SignedCall), (ICommon.SignedCall[])); - } - - function serialize(ICommon.Transfer memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_Transfer, abi.encode(value)); - } - - function serialize(ICommon.Transfer memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_Transfer, abi.encode(value)); - } - - function deserializeTransfer(string memory json) public pure returns (ICommon.Transfer memory) { - return abi.decode(vm.parseJsonType(json, schema_Transfer), (ICommon.Transfer)); - } - - function deserializeTransfer(string memory json, string memory path) public pure returns (ICommon.Transfer memory) { - return abi.decode(vm.parseJsonType(json, path, schema_Transfer), (ICommon.Transfer)); - } - - function deserializeTransferArray(string memory json, string memory path) public pure returns (ICommon.Transfer[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_Transfer), (ICommon.Transfer[])); - } - - function serialize(GuardedExecutor.SpendInfo memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_SpendInfo, abi.encode(value)); - } - - function serialize(GuardedExecutor.SpendInfo memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_SpendInfo, abi.encode(value)); - } - - function deserializeSpendInfo(string memory json) public pure returns (GuardedExecutor.SpendInfo memory) { - return abi.decode(vm.parseJsonType(json, schema_SpendInfo), (GuardedExecutor.SpendInfo)); - } - - function deserializeSpendInfo(string memory json, string memory path) public pure returns (GuardedExecutor.SpendInfo memory) { - return abi.decode(vm.parseJsonType(json, path, schema_SpendInfo), (GuardedExecutor.SpendInfo)); - } - - function deserializeSpendInfoArray(string memory json, string memory path) public pure returns (GuardedExecutor.SpendInfo[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_SpendInfo), (GuardedExecutor.SpendInfo[])); - } - - function serialize(GuardedExecutor.CallCheckerInfo memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_CallCheckerInfo, abi.encode(value)); - } - - function serialize(GuardedExecutor.CallCheckerInfo memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_CallCheckerInfo, abi.encode(value)); - } - - function deserializeCallCheckerInfo(string memory json) public pure returns (GuardedExecutor.CallCheckerInfo memory) { - return abi.decode(vm.parseJsonType(json, schema_CallCheckerInfo), (GuardedExecutor.CallCheckerInfo)); - } - - function deserializeCallCheckerInfo(string memory json, string memory path) public pure returns (GuardedExecutor.CallCheckerInfo memory) { - return abi.decode(vm.parseJsonType(json, path, schema_CallCheckerInfo), (GuardedExecutor.CallCheckerInfo)); - } - - function deserializeCallCheckerInfoArray(string memory json, string memory path) public pure returns (GuardedExecutor.CallCheckerInfo[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_CallCheckerInfo), (GuardedExecutor.CallCheckerInfo[])); - } - - function serialize(GuardedExecutor.TokenPeriodSpend memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_TokenPeriodSpend, abi.encode(value)); - } - - function serialize(GuardedExecutor.TokenPeriodSpend memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_TokenPeriodSpend, abi.encode(value)); - } - - function deserializeTokenPeriodSpend(string memory json) public pure returns (GuardedExecutor.TokenPeriodSpend memory) { - return abi.decode(vm.parseJsonType(json, schema_TokenPeriodSpend), (GuardedExecutor.TokenPeriodSpend)); - } - - function deserializeTokenPeriodSpend(string memory json, string memory path) public pure returns (GuardedExecutor.TokenPeriodSpend memory) { - return abi.decode(vm.parseJsonType(json, path, schema_TokenPeriodSpend), (GuardedExecutor.TokenPeriodSpend)); - } - - function deserializeTokenPeriodSpendArray(string memory json, string memory path) public pure returns (GuardedExecutor.TokenPeriodSpend[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_TokenPeriodSpend), (GuardedExecutor.TokenPeriodSpend[])); - } - - function serialize(GuardedExecutor._ExecuteTemps memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema__ExecuteTemps, abi.encode(value)); - } - - function serialize(GuardedExecutor._ExecuteTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema__ExecuteTemps, abi.encode(value)); - } - - function deserialize_ExecuteTemps(string memory json) public pure returns (GuardedExecutor._ExecuteTemps memory) { - return abi.decode(vm.parseJsonType(json, schema__ExecuteTemps), (GuardedExecutor._ExecuteTemps)); - } - - function deserialize_ExecuteTemps(string memory json, string memory path) public pure returns (GuardedExecutor._ExecuteTemps memory) { - return abi.decode(vm.parseJsonType(json, path, schema__ExecuteTemps), (GuardedExecutor._ExecuteTemps)); - } - - function deserialize_ExecuteTempsArray(string memory json, string memory path) public pure returns (GuardedExecutor._ExecuteTemps[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema__ExecuteTemps), (GuardedExecutor._ExecuteTemps[])); - } - - function serialize(LibTStack.TStack memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_TStack, abi.encode(value)); - } - - function serialize(LibTStack.TStack memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_TStack, abi.encode(value)); - } - - function deserializeTStack(string memory json) public pure returns (LibTStack.TStack memory) { - return abi.decode(vm.parseJsonType(json, schema_TStack), (LibTStack.TStack)); - } - - function deserializeTStack(string memory json, string memory path) public pure returns (LibTStack.TStack memory) { - return abi.decode(vm.parseJsonType(json, path, schema_TStack), (LibTStack.TStack)); - } - - function deserializeTStackArray(string memory json, string memory path) public pure returns (LibTStack.TStack[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_TStack), (LibTStack.TStack[])); - } - - function serialize(IthacaAccount.Key memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_Key, abi.encode(value)); - } - - function serialize(IthacaAccount.Key memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_Key, abi.encode(value)); - } - - function deserializeKey(string memory json) public pure returns (IthacaAccount.Key memory) { - return abi.decode(vm.parseJsonType(json, schema_Key), (IthacaAccount.Key)); - } - - function deserializeKey(string memory json, string memory path) public pure returns (IthacaAccount.Key memory) { - return abi.decode(vm.parseJsonType(json, path, schema_Key), (IthacaAccount.Key)); - } - - function deserializeKeyArray(string memory json, string memory path) public pure returns (IthacaAccount.Key[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_Key), (IthacaAccount.Key[])); - } - - function serialize(IthacaAccount.KeyExtraStorage memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_KeyExtraStorage, abi.encode(value)); - } - - function serialize(IthacaAccount.KeyExtraStorage memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_KeyExtraStorage, abi.encode(value)); - } - - function deserializeKeyExtraStorage(string memory json) public pure returns (IthacaAccount.KeyExtraStorage memory) { - return abi.decode(vm.parseJsonType(json, schema_KeyExtraStorage), (IthacaAccount.KeyExtraStorage)); - } - - function deserializeKeyExtraStorage(string memory json, string memory path) public pure returns (IthacaAccount.KeyExtraStorage memory) { - return abi.decode(vm.parseJsonType(json, path, schema_KeyExtraStorage), (IthacaAccount.KeyExtraStorage)); - } - - function deserializeKeyExtraStorageArray(string memory json, string memory path) public pure returns (IthacaAccount.KeyExtraStorage[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_KeyExtraStorage), (IthacaAccount.KeyExtraStorage[])); - } - - function serialize(IEscrow.Escrow memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_Escrow, abi.encode(value)); - } - - function serialize(IEscrow.Escrow memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_Escrow, abi.encode(value)); - } - - function deserializeEscrow(string memory json) public pure returns (IEscrow.Escrow memory) { - return abi.decode(vm.parseJsonType(json, schema_Escrow), (IEscrow.Escrow)); - } - - function deserializeEscrow(string memory json, string memory path) public pure returns (IEscrow.Escrow memory) { - return abi.decode(vm.parseJsonType(json, path, schema_Escrow), (IEscrow.Escrow)); - } - - function deserializeEscrowArray(string memory json, string memory path) public pure returns (IEscrow.Escrow[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_Escrow), (IEscrow.Escrow[])); - } - - function serialize(MultiSigSigner.Config memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_Config, abi.encode(value)); - } - - function serialize(MultiSigSigner.Config memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_Config, abi.encode(value)); - } - - function deserializeConfig(string memory json) public pure returns (MultiSigSigner.Config memory) { - return abi.decode(vm.parseJsonType(json, schema_Config), (MultiSigSigner.Config)); - } - - function deserializeConfig(string memory json, string memory path) public pure returns (MultiSigSigner.Config memory) { - return abi.decode(vm.parseJsonType(json, path, schema_Config), (MultiSigSigner.Config)); - } - - function deserializeConfigArray(string memory json, string memory path) public pure returns (MultiSigSigner.Config[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_Config), (MultiSigSigner.Config[])); - } - - function serialize(BaseTest.TargetFunctionPayload memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_TargetFunctionPayload, abi.encode(value)); - } - - function serialize(BaseTest.TargetFunctionPayload memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_TargetFunctionPayload, abi.encode(value)); - } - - function deserializeTargetFunctionPayload(string memory json) public pure returns (BaseTest.TargetFunctionPayload memory) { - return abi.decode(vm.parseJsonType(json, schema_TargetFunctionPayload), (BaseTest.TargetFunctionPayload)); - } - - function deserializeTargetFunctionPayload(string memory json, string memory path) public pure returns (BaseTest.TargetFunctionPayload memory) { - return abi.decode(vm.parseJsonType(json, path, schema_TargetFunctionPayload), (BaseTest.TargetFunctionPayload)); - } - - function deserializeTargetFunctionPayloadArray(string memory json, string memory path) public pure returns (BaseTest.TargetFunctionPayload[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_TargetFunctionPayload), (BaseTest.TargetFunctionPayload[])); - } - - function serialize(BaseTest.PassKey memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_PassKey, abi.encode(value)); - } - - function serialize(BaseTest.PassKey memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_PassKey, abi.encode(value)); - } - - function deserializePassKey(string memory json) public pure returns (BaseTest.PassKey memory) { - return abi.decode(vm.parseJsonType(json, schema_PassKey), (BaseTest.PassKey)); - } - - function deserializePassKey(string memory json, string memory path) public pure returns (BaseTest.PassKey memory) { - return abi.decode(vm.parseJsonType(json, path, schema_PassKey), (BaseTest.PassKey)); - } - - function deserializePassKeyArray(string memory json, string memory path) public pure returns (BaseTest.PassKey[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_PassKey), (BaseTest.PassKey[])); - } - - function serialize(BaseTest.MultiSigKey memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_MultiSigKey, abi.encode(value)); - } - - function serialize(BaseTest.MultiSigKey memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_MultiSigKey, abi.encode(value)); - } - - function deserializeMultiSigKey(string memory json) public pure returns (BaseTest.MultiSigKey memory) { - return abi.decode(vm.parseJsonType(json, schema_MultiSigKey), (BaseTest.MultiSigKey)); - } - - function deserializeMultiSigKey(string memory json, string memory path) public pure returns (BaseTest.MultiSigKey memory) { - return abi.decode(vm.parseJsonType(json, path, schema_MultiSigKey), (BaseTest.MultiSigKey)); - } - - function deserializeMultiSigKeyArray(string memory json, string memory path) public pure returns (BaseTest.MultiSigKey[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_MultiSigKey), (BaseTest.MultiSigKey[])); - } - - function serialize(BaseTest.DelegatedEOA memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_DelegatedEOA, abi.encode(value)); - } - - function serialize(BaseTest.DelegatedEOA memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_DelegatedEOA, abi.encode(value)); - } - - function deserializeDelegatedEOA(string memory json) public pure returns (BaseTest.DelegatedEOA memory) { - return abi.decode(vm.parseJsonType(json, schema_DelegatedEOA), (BaseTest.DelegatedEOA)); - } - - function deserializeDelegatedEOA(string memory json, string memory path) public pure returns (BaseTest.DelegatedEOA memory) { - return abi.decode(vm.parseJsonType(json, path, schema_DelegatedEOA), (BaseTest.DelegatedEOA)); - } - - function deserializeDelegatedEOAArray(string memory json, string memory path) public pure returns (BaseTest.DelegatedEOA[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_DelegatedEOA), (BaseTest.DelegatedEOA[])); - } - - function serialize(BaseTest._EstimateGasParams memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema__EstimateGasParams, abi.encode(value)); - } - - function serialize(BaseTest._EstimateGasParams memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema__EstimateGasParams, abi.encode(value)); - } - - function deserialize_EstimateGasParams(string memory json) public pure returns (BaseTest._EstimateGasParams memory) { - return abi.decode(vm.parseJsonType(json, schema__EstimateGasParams), (BaseTest._EstimateGasParams)); - } - - function deserialize_EstimateGasParams(string memory json, string memory path) public pure returns (BaseTest._EstimateGasParams memory) { - return abi.decode(vm.parseJsonType(json, path, schema__EstimateGasParams), (BaseTest._EstimateGasParams)); - } - - function deserialize_EstimateGasParamsArray(string memory json, string memory path) public pure returns (BaseTest._EstimateGasParams[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema__EstimateGasParams), (BaseTest._EstimateGasParams[])); - } - - function serialize(AccountTest._TestExecuteWithSignatureTemps memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema__TestExecuteWithSignatureTemps, abi.encode(value)); - } - - function serialize(AccountTest._TestExecuteWithSignatureTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema__TestExecuteWithSignatureTemps, abi.encode(value)); - } - - function deserialize_TestExecuteWithSignatureTemps(string memory json) public pure returns (AccountTest._TestExecuteWithSignatureTemps memory) { - return abi.decode(vm.parseJsonType(json, schema__TestExecuteWithSignatureTemps), (AccountTest._TestExecuteWithSignatureTemps)); - } - - function deserialize_TestExecuteWithSignatureTemps(string memory json, string memory path) public pure returns (AccountTest._TestExecuteWithSignatureTemps memory) { - return abi.decode(vm.parseJsonType(json, path, schema__TestExecuteWithSignatureTemps), (AccountTest._TestExecuteWithSignatureTemps)); - } - - function deserialize_TestExecuteWithSignatureTempsArray(string memory json, string memory path) public pure returns (AccountTest._TestExecuteWithSignatureTemps[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema__TestExecuteWithSignatureTemps), (AccountTest._TestExecuteWithSignatureTemps[])); - } - - function serialize(AccountTest._TestUpgradeAccountWithPassKeyTemps memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema__TestUpgradeAccountWithPassKeyTemps, abi.encode(value)); - } - - function serialize(AccountTest._TestUpgradeAccountWithPassKeyTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema__TestUpgradeAccountWithPassKeyTemps, abi.encode(value)); - } - - function deserialize_TestUpgradeAccountWithPassKeyTemps(string memory json) public pure returns (AccountTest._TestUpgradeAccountWithPassKeyTemps memory) { - return abi.decode(vm.parseJsonType(json, schema__TestUpgradeAccountWithPassKeyTemps), (AccountTest._TestUpgradeAccountWithPassKeyTemps)); - } - - function deserialize_TestUpgradeAccountWithPassKeyTemps(string memory json, string memory path) public pure returns (AccountTest._TestUpgradeAccountWithPassKeyTemps memory) { - return abi.decode(vm.parseJsonType(json, path, schema__TestUpgradeAccountWithPassKeyTemps), (AccountTest._TestUpgradeAccountWithPassKeyTemps)); - } - - function deserialize_TestUpgradeAccountWithPassKeyTempsArray(string memory json, string memory path) public pure returns (AccountTest._TestUpgradeAccountWithPassKeyTemps[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema__TestUpgradeAccountWithPassKeyTemps), (AccountTest._TestUpgradeAccountWithPassKeyTemps[])); - } - - function serialize(IStakeManager.DepositInfo memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_DepositInfo, abi.encode(value)); - } - - function serialize(IStakeManager.DepositInfo memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_DepositInfo, abi.encode(value)); - } - - function deserializeDepositInfo(string memory json) public pure returns (IStakeManager.DepositInfo memory) { - return abi.decode(vm.parseJsonType(json, schema_DepositInfo), (IStakeManager.DepositInfo)); - } - - function deserializeDepositInfo(string memory json, string memory path) public pure returns (IStakeManager.DepositInfo memory) { - return abi.decode(vm.parseJsonType(json, path, schema_DepositInfo), (IStakeManager.DepositInfo)); - } - - function deserializeDepositInfoArray(string memory json, string memory path) public pure returns (IStakeManager.DepositInfo[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_DepositInfo), (IStakeManager.DepositInfo[])); - } - - function serialize(IStakeManager.StakeInfo memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_StakeInfo, abi.encode(value)); - } - - function serialize(IStakeManager.StakeInfo memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_StakeInfo, abi.encode(value)); - } - - function deserializeStakeInfo(string memory json) public pure returns (IStakeManager.StakeInfo memory) { - return abi.decode(vm.parseJsonType(json, schema_StakeInfo), (IStakeManager.StakeInfo)); - } - - function deserializeStakeInfo(string memory json, string memory path) public pure returns (IStakeManager.StakeInfo memory) { - return abi.decode(vm.parseJsonType(json, path, schema_StakeInfo), (IStakeManager.StakeInfo)); - } - - function deserializeStakeInfoArray(string memory json, string memory path) public pure returns (IStakeManager.StakeInfo[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_StakeInfo), (IStakeManager.StakeInfo[])); - } - - function serialize(PackedUserOperation memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_PackedUserOperation, abi.encode(value)); - } - - function serialize(PackedUserOperation memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_PackedUserOperation, abi.encode(value)); - } - - function deserializePackedUserOperation(string memory json) public pure returns (PackedUserOperation memory) { - return abi.decode(vm.parseJsonType(json, schema_PackedUserOperation), (PackedUserOperation)); - } - - function deserializePackedUserOperation(string memory json, string memory path) public pure returns (PackedUserOperation memory) { - return abi.decode(vm.parseJsonType(json, path, schema_PackedUserOperation), (PackedUserOperation)); - } - - function deserializePackedUserOperationArray(string memory json, string memory path) public pure returns (PackedUserOperation[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_PackedUserOperation), (PackedUserOperation[])); - } - - function serialize(UserOperation memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_UserOperation, abi.encode(value)); - } - - function serialize(UserOperation memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_UserOperation, abi.encode(value)); - } - - function deserializeUserOperation(string memory json) public pure returns (UserOperation memory) { - return abi.decode(vm.parseJsonType(json, schema_UserOperation), (UserOperation)); - } - - function deserializeUserOperation(string memory json, string memory path) public pure returns (UserOperation memory) { - return abi.decode(vm.parseJsonType(json, path, schema_UserOperation), (UserOperation)); - } - - function deserializeUserOperationArray(string memory json, string memory path) public pure returns (UserOperation[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_UserOperation), (UserOperation[])); - } - - function serialize(IERC4337EntryPoint.UserOpsPerAggregator memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_UserOpsPerAggregator, abi.encode(value)); - } - - function serialize(IERC4337EntryPoint.UserOpsPerAggregator memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_UserOpsPerAggregator, abi.encode(value)); - } - - function deserializeUserOpsPerAggregator(string memory json) public pure returns (IERC4337EntryPoint.UserOpsPerAggregator memory) { - return abi.decode(vm.parseJsonType(json, schema_UserOpsPerAggregator), (IERC4337EntryPoint.UserOpsPerAggregator)); - } - - function deserializeUserOpsPerAggregator(string memory json, string memory path) public pure returns (IERC4337EntryPoint.UserOpsPerAggregator memory) { - return abi.decode(vm.parseJsonType(json, path, schema_UserOpsPerAggregator), (IERC4337EntryPoint.UserOpsPerAggregator)); - } - - function deserializeUserOpsPerAggregatorArray(string memory json, string memory path) public pure returns (IERC4337EntryPoint.UserOpsPerAggregator[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_UserOpsPerAggregator), (IERC4337EntryPoint.UserOpsPerAggregator[])); - } - - function serialize(IERC4337EntryPoint.ReturnInfo memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_ReturnInfo, abi.encode(value)); - } - - function serialize(IERC4337EntryPoint.ReturnInfo memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_ReturnInfo, abi.encode(value)); - } - - function deserializeReturnInfo(string memory json) public pure returns (IERC4337EntryPoint.ReturnInfo memory) { - return abi.decode(vm.parseJsonType(json, schema_ReturnInfo), (IERC4337EntryPoint.ReturnInfo)); - } - - function deserializeReturnInfo(string memory json, string memory path) public pure returns (IERC4337EntryPoint.ReturnInfo memory) { - return abi.decode(vm.parseJsonType(json, path, schema_ReturnInfo), (IERC4337EntryPoint.ReturnInfo)); - } - - function deserializeReturnInfoArray(string memory json, string memory path) public pure returns (IERC4337EntryPoint.ReturnInfo[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_ReturnInfo), (IERC4337EntryPoint.ReturnInfo[])); - } - - function serialize(SignatureWrapper memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_SignatureWrapper, abi.encode(value)); - } - - function serialize(SignatureWrapper memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_SignatureWrapper, abi.encode(value)); - } - - function deserializeSignatureWrapper(string memory json) public pure returns (SignatureWrapper memory) { - return abi.decode(vm.parseJsonType(json, schema_SignatureWrapper), (SignatureWrapper)); - } - - function deserializeSignatureWrapper(string memory json, string memory path) public pure returns (SignatureWrapper memory) { - return abi.decode(vm.parseJsonType(json, path, schema_SignatureWrapper), (SignatureWrapper)); - } - - function deserializeSignatureWrapperArray(string memory json, string memory path) public pure returns (SignatureWrapper[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_SignatureWrapper), (SignatureWrapper[])); - } - - function serialize(EnforcedOptionParam memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_EnforcedOptionParam, abi.encode(value)); - } - - function serialize(EnforcedOptionParam memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_EnforcedOptionParam, abi.encode(value)); - } - - function deserializeEnforcedOptionParam(string memory json) public pure returns (EnforcedOptionParam memory) { - return abi.decode(vm.parseJsonType(json, schema_EnforcedOptionParam), (EnforcedOptionParam)); - } - - function deserializeEnforcedOptionParam(string memory json, string memory path) public pure returns (EnforcedOptionParam memory) { - return abi.decode(vm.parseJsonType(json, path, schema_EnforcedOptionParam), (EnforcedOptionParam)); - } - - function deserializeEnforcedOptionParamArray(string memory json, string memory path) public pure returns (EnforcedOptionParam[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_EnforcedOptionParam), (EnforcedOptionParam[])); - } - - function serialize(MultiSigSignerTest.MultiSigTestTemps memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema_MultiSigTestTemps, abi.encode(value)); - } - - function serialize(MultiSigSignerTest.MultiSigTestTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema_MultiSigTestTemps, abi.encode(value)); - } - - function deserializeMultiSigTestTemps(string memory json) public pure returns (MultiSigSignerTest.MultiSigTestTemps memory) { - return abi.decode(vm.parseJsonType(json, schema_MultiSigTestTemps), (MultiSigSignerTest.MultiSigTestTemps)); - } - - function deserializeMultiSigTestTemps(string memory json, string memory path) public pure returns (MultiSigSignerTest.MultiSigTestTemps memory) { - return abi.decode(vm.parseJsonType(json, path, schema_MultiSigTestTemps), (MultiSigSignerTest.MultiSigTestTemps)); - } - - function deserializeMultiSigTestTempsArray(string memory json, string memory path) public pure returns (MultiSigSignerTest.MultiSigTestTemps[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema_MultiSigTestTemps), (MultiSigSignerTest.MultiSigTestTemps[])); - } - - function serialize(OrchestratorTest._TestFullFlowTemps memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema__TestFullFlowTemps, abi.encode(value)); - } - - function serialize(OrchestratorTest._TestFullFlowTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema__TestFullFlowTemps, abi.encode(value)); - } - - function deserialize_TestFullFlowTemps(string memory json) public pure returns (OrchestratorTest._TestFullFlowTemps memory) { - return abi.decode(vm.parseJsonType(json, schema__TestFullFlowTemps), (OrchestratorTest._TestFullFlowTemps)); - } - - function deserialize_TestFullFlowTemps(string memory json, string memory path) public pure returns (OrchestratorTest._TestFullFlowTemps memory) { - return abi.decode(vm.parseJsonType(json, path, schema__TestFullFlowTemps), (OrchestratorTest._TestFullFlowTemps)); - } - - function deserialize_TestFullFlowTempsArray(string memory json, string memory path) public pure returns (OrchestratorTest._TestFullFlowTemps[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema__TestFullFlowTemps), (OrchestratorTest._TestFullFlowTemps[])); - } - - function serialize(OrchestratorTest._TestAuthorizeWithPreCallsAndTransferTemps memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema__TestAuthorizeWithPreCallsAndTransferTemps, abi.encode(value)); - } - - function serialize(OrchestratorTest._TestAuthorizeWithPreCallsAndTransferTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema__TestAuthorizeWithPreCallsAndTransferTemps, abi.encode(value)); - } - - function deserialize_TestAuthorizeWithPreCallsAndTransferTemps(string memory json) public pure returns (OrchestratorTest._TestAuthorizeWithPreCallsAndTransferTemps memory) { - return abi.decode(vm.parseJsonType(json, schema__TestAuthorizeWithPreCallsAndTransferTemps), (OrchestratorTest._TestAuthorizeWithPreCallsAndTransferTemps)); - } - - function deserialize_TestAuthorizeWithPreCallsAndTransferTemps(string memory json, string memory path) public pure returns (OrchestratorTest._TestAuthorizeWithPreCallsAndTransferTemps memory) { - return abi.decode(vm.parseJsonType(json, path, schema__TestAuthorizeWithPreCallsAndTransferTemps), (OrchestratorTest._TestAuthorizeWithPreCallsAndTransferTemps)); - } - - function deserialize_TestAuthorizeWithPreCallsAndTransferTempsArray(string memory json, string memory path) public pure returns (OrchestratorTest._TestAuthorizeWithPreCallsAndTransferTemps[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema__TestAuthorizeWithPreCallsAndTransferTemps), (OrchestratorTest._TestAuthorizeWithPreCallsAndTransferTemps[])); - } - - function serialize(OrchestratorTest._TestPayViaAnotherPayerTemps memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema__TestPayViaAnotherPayerTemps, abi.encode(value)); - } - - function serialize(OrchestratorTest._TestPayViaAnotherPayerTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema__TestPayViaAnotherPayerTemps, abi.encode(value)); - } - - function deserialize_TestPayViaAnotherPayerTemps(string memory json) public pure returns (OrchestratorTest._TestPayViaAnotherPayerTemps memory) { - return abi.decode(vm.parseJsonType(json, schema__TestPayViaAnotherPayerTemps), (OrchestratorTest._TestPayViaAnotherPayerTemps)); - } - - function deserialize_TestPayViaAnotherPayerTemps(string memory json, string memory path) public pure returns (OrchestratorTest._TestPayViaAnotherPayerTemps memory) { - return abi.decode(vm.parseJsonType(json, path, schema__TestPayViaAnotherPayerTemps), (OrchestratorTest._TestPayViaAnotherPayerTemps)); - } - - function deserialize_TestPayViaAnotherPayerTempsArray(string memory json, string memory path) public pure returns (OrchestratorTest._TestPayViaAnotherPayerTemps[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema__TestPayViaAnotherPayerTemps), (OrchestratorTest._TestPayViaAnotherPayerTemps[])); - } - - function serialize(OrchestratorTest._TestAccountImplementationVerificationTemps memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema__TestAccountImplementationVerificationTemps, abi.encode(value)); - } - - function serialize(OrchestratorTest._TestAccountImplementationVerificationTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema__TestAccountImplementationVerificationTemps, abi.encode(value)); - } - - function deserialize_TestAccountImplementationVerificationTemps(string memory json) public pure returns (OrchestratorTest._TestAccountImplementationVerificationTemps memory) { - return abi.decode(vm.parseJsonType(json, schema__TestAccountImplementationVerificationTemps), (OrchestratorTest._TestAccountImplementationVerificationTemps)); - } - - function deserialize_TestAccountImplementationVerificationTemps(string memory json, string memory path) public pure returns (OrchestratorTest._TestAccountImplementationVerificationTemps memory) { - return abi.decode(vm.parseJsonType(json, path, schema__TestAccountImplementationVerificationTemps), (OrchestratorTest._TestAccountImplementationVerificationTemps)); - } - - function deserialize_TestAccountImplementationVerificationTempsArray(string memory json, string memory path) public pure returns (OrchestratorTest._TestAccountImplementationVerificationTemps[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema__TestAccountImplementationVerificationTemps), (OrchestratorTest._TestAccountImplementationVerificationTemps[])); - } - - function serialize(OrchestratorTest._TestMultiSigTemps memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema__TestMultiSigTemps, abi.encode(value)); - } - - function serialize(OrchestratorTest._TestMultiSigTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema__TestMultiSigTemps, abi.encode(value)); - } - - function deserialize_TestMultiSigTemps(string memory json) public pure returns (OrchestratorTest._TestMultiSigTemps memory) { - return abi.decode(vm.parseJsonType(json, schema__TestMultiSigTemps), (OrchestratorTest._TestMultiSigTemps)); - } - - function deserialize_TestMultiSigTemps(string memory json, string memory path) public pure returns (OrchestratorTest._TestMultiSigTemps memory) { - return abi.decode(vm.parseJsonType(json, path, schema__TestMultiSigTemps), (OrchestratorTest._TestMultiSigTemps)); - } - - function deserialize_TestMultiSigTempsArray(string memory json, string memory path) public pure returns (OrchestratorTest._TestMultiSigTemps[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema__TestMultiSigTemps), (OrchestratorTest._TestMultiSigTemps[])); - } - - function serialize(OrchestratorTest._TestMultiChainIntentTemps memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema__TestMultiChainIntentTemps, abi.encode(value)); - } - - function serialize(OrchestratorTest._TestMultiChainIntentTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema__TestMultiChainIntentTemps, abi.encode(value)); - } - - function deserialize_TestMultiChainIntentTemps(string memory json) public pure returns (OrchestratorTest._TestMultiChainIntentTemps memory) { - return abi.decode(vm.parseJsonType(json, schema__TestMultiChainIntentTemps), (OrchestratorTest._TestMultiChainIntentTemps)); - } - - function deserialize_TestMultiChainIntentTemps(string memory json, string memory path) public pure returns (OrchestratorTest._TestMultiChainIntentTemps memory) { - return abi.decode(vm.parseJsonType(json, path, schema__TestMultiChainIntentTemps), (OrchestratorTest._TestMultiChainIntentTemps)); - } - - function deserialize_TestMultiChainIntentTempsArray(string memory json, string memory path) public pure returns (OrchestratorTest._TestMultiChainIntentTemps[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema__TestMultiChainIntentTemps), (OrchestratorTest._TestMultiChainIntentTemps[])); - } - - function serialize(SimulateExecuteTest._SimulateExecuteTemps memory value) internal pure returns (string memory) { - return vm.serializeJsonType(schema__SimulateExecuteTemps, abi.encode(value)); - } - - function serialize(SimulateExecuteTest._SimulateExecuteTemps memory value, string memory objectKey, string memory valueKey) internal returns (string memory) { - return vm.serializeJsonType(objectKey, valueKey, schema__SimulateExecuteTemps, abi.encode(value)); - } - - function deserialize_SimulateExecuteTemps(string memory json) public pure returns (SimulateExecuteTest._SimulateExecuteTemps memory) { - return abi.decode(vm.parseJsonType(json, schema__SimulateExecuteTemps), (SimulateExecuteTest._SimulateExecuteTemps)); - } - - function deserialize_SimulateExecuteTemps(string memory json, string memory path) public pure returns (SimulateExecuteTest._SimulateExecuteTemps memory) { - return abi.decode(vm.parseJsonType(json, path, schema__SimulateExecuteTemps), (SimulateExecuteTest._SimulateExecuteTemps)); - } - - function deserialize_SimulateExecuteTempsArray(string memory json, string memory path) public pure returns (SimulateExecuteTest._SimulateExecuteTemps[] memory) { - return abi.decode(vm.parseJsonTypeArray(json, path, schema__SimulateExecuteTemps), (SimulateExecuteTest._SimulateExecuteTemps[])); - } -} From cb5185f3580856cd93ad9ac2907659dcc615e01a Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Mon, 20 Apr 2026 02:57:42 +0000 Subject: [PATCH 53/55] Account (#91) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add merkle sigs natively into the account * fix: tests * test: add more sophisticated fuzz test * chore: bump contract versions due to bytecode changes - Contracts updated: IthacaAccount * pre-commit * deploy execute_config.sh * Update forge-std * Normalize file modes and update forge-std submodule Remove executable bit from deploy/execute_config.sh, deploy/verify_config.sh, and prep/check-bytecode-changes.js (mode 100755 → 100644). Update lib/forge-std submodule from commit c2cf7017d27c1d20e74ace4dacb6c5ce4bbbe899 to 07853315f998f94dc724e464b1bab1270888ee64. No other content changes. * Update LayerZero submodule & remove exec bits Normalize file permissions and advance submodule: change file modes from executable (100755) to non-executable (100644) for deploy/execute_config.sh, deploy/verify_config.sh, and prep/check-bytecode-changes.js. Update lib/LayerZero-v2 submodule commit from 88428755be6caa71cb1d2926141d73c8989296b5 to ab9b083410b9359285a5756807e1b6145d4711a7. * Update LayerZero-v2 * Update LayerZero-v2 --------- Co-authored-by: Tanishk Goyal Co-authored-by: GitHub Action Co-authored-by: googleworkspace-bot From 90b1293f5c2c93849d759fe55f2d40da25c7f27f Mon Sep 17 00:00:00 2001 From: googleworkspace-bot Date: Mon, 20 Apr 2026 13:08:27 +0700 Subject: [PATCH 54/55] clear && forge fmt && forge snapshot clear && forge fmt && forge snapshot --isolate --match-contract Benchmark --via-ir && git add snapshots --- .husky/pre-commit | 1 + src/IthacaAccount.sol | 47 ++++++++++++++++++++++++-- test/Account.t.sol | 69 ++++++++++++++++++++++++++++++++++++-- test/Base.t.sol | 11 +++--- test/Orchestrator.t.sol | 5 +-- test/SimulateExecute.t.sol | 3 +- 6 files changed, 123 insertions(+), 13 deletions(-) create mode 100644 .husky/pre-commit 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/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); From 792847fb196ed2ca606f9c55a0cb51e7a0ca43e3 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Mon, 20 Apr 2026 06:39:15 +0000 Subject: [PATCH 55/55] Account (#93) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add merkle sigs natively into the account * fix: tests * test: add more sophisticated fuzz test * chore: bump contract versions due to bytecode changes - Contracts updated: IthacaAccount * pre-commit * deploy execute_config.sh * Update forge-std * Normalize file modes and update forge-std submodule Remove executable bit from deploy/execute_config.sh, deploy/verify_config.sh, and prep/check-bytecode-changes.js (mode 100755 → 100644). Update lib/forge-std submodule from commit c2cf7017d27c1d20e74ace4dacb6c5ce4bbbe899 to 07853315f998f94dc724e464b1bab1270888ee64. No other content changes. * Update LayerZero submodule & remove exec bits Normalize file permissions and advance submodule: change file modes from executable (100755) to non-executable (100644) for deploy/execute_config.sh, deploy/verify_config.sh, and prep/check-bytecode-changes.js. Update lib/LayerZero-v2 submodule commit from 88428755be6caa71cb1d2926141d73c8989296b5 to ab9b083410b9359285a5756807e1b6145d4711a7. * Update LayerZero-v2 * Update LayerZero-v2 * clear && forge fmt && forge snapshot clear && forge fmt && forge snapshot --isolate --match-contract Benchmark --via-ir && git add snapshots --------- Co-authored-by: Tanishk Goyal Co-authored-by: GitHub Action Co-authored-by: googleworkspace-bot --- .husky/pre-commit | 1 + 1 file changed, 1 insertion(+) create mode 100644 .husky/pre-commit 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