From 6dd67d0f1c5cb88825a96d52956e32b62903c727 Mon Sep 17 00:00:00 2001 From: howy <132113803+howydev@users.noreply.github.com> Date: Fri, 26 Sep 2025 11:59:11 -0400 Subject: [PATCH 1/4] 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 * . --- src/IthacaAccount.sol | 10 +++--- src/Orchestrator.sol | 62 ++++++++++++++++++------------------ src/interfaces/ICommon.sol | 3 -- test/Account.t.sol | 9 +++--- test/Base.t.sol | 36 ++++++++++++--------- test/Benchmark.t.sol | 4 +-- test/GuardedExecutor.t.sol | 32 +++++++++---------- test/Orchestrator.t.sol | 64 ++++++++++++++++++-------------------- test/SimulateExecute.t.sol | 10 +++--- 9 files changed, 114 insertions(+), 116 deletions(-) diff --git a/src/IthacaAccount.sol b/src/IthacaAccount.sol index 0f0068ac..a922c9b9 100644 --- a/src/IthacaAccount.sol +++ b/src/IthacaAccount.sol @@ -478,11 +478,11 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { ) ); } - bool isMultichain = nonce >> 240 == MULTICHAIN_NONCE_PREFIX; - bytes32 structHash = EfficientHashLib.hash( - uint256(EXECUTE_TYPEHASH), LibBit.toUint(isMultichain), uint256(a.hash()), nonce - ); - return isMultichain ? _hashTypedDataSansChainId(structHash) : _hashTypedData(structHash); + bytes32 structHash = + EfficientHashLib.hash(uint256(EXECUTE_TYPEHASH), uint256(a.hash()), nonce); + return nonce >> 240 == MULTICHAIN_NONCE_PREFIX + ? _hashTypedDataSansChainId(structHash) + : _hashTypedData(structHash); } /// @dev Returns if the signature is valid, along with its `keyHash`. diff --git a/src/Orchestrator.sol b/src/Orchestrator.sol index 06586bcc..9d917502 100644 --- a/src/Orchestrator.sol +++ b/src/Orchestrator.sol @@ -127,6 +127,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. @@ -349,13 +353,11 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu // 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(i.paymentToken, payer) < i.paymentAmount) { + err = PaymentError.selector; - if (flags == _SIMULATION_MODE_FLAG) { - revert PaymentError(); - } + if (flags == _SIMULATION_MODE_FLAG) { + revert PaymentError(); } } @@ -476,7 +478,8 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu bool isValid; bytes32 keyHash; - if (i.isMultichain) { + + if (i.nonce >> 240 == MERKLE_VERIFICATION) { // For multi chain intents, we have to verify using merkle sigs. (isValid, keyHash) = _verifyMerkleSig(digest, eoa, i.signature); @@ -754,40 +757,35 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu 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()); } /// @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; - // To avoid stack-too-deep. Faster than a regular Solidity array anyways. - bytes32[] memory f = EfficientHashLib.malloc(13); + bytes32[] memory f = EfficientHashLib.malloc(12); 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()); + f.set(1, uint160(i.eoa)); + f.set(2, _executionDataHash(i.executionData)); + f.set(3, i.nonce); + f.set(4, uint160(i.payer)); + f.set(5, uint160(i.paymentToken)); + f.set(6, i.paymentMaxAmount); + f.set(7, i.combinedGas); + f.set(8, _encodedArrHash(i.encodedPreCalls)); + f.set(9, _encodedArrHash(i.encodedFundTransfers)); + f.set(10, uint160(i.settler)); + f.set(11, i.expiry); + + return i.nonce >> 240 == MULTICHAIN_NONCE_PREFIX + ? _hashTypedDataSansChainId(f.hash()) + : _hashTypedData(f.hash()); } /// @dev Helper function to return the hash of the `execuctionData`. diff --git a/src/interfaces/ICommon.sol b/src/interfaces/ICommon.sol index 54ccf388..e9c428e3 100644 --- a/src/interfaces/ICommon.sol +++ b/src/interfaces/ICommon.sol @@ -55,9 +55,6 @@ interface ICommon { //////////////////////////////////////////////////////////////////////// // 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. diff --git a/test/Account.t.sol b/test/Account.t.sol index e4b8310b..7500c4d3 100644 --- a/test/Account.t.sol +++ b/test/Account.t.sol @@ -303,7 +303,7 @@ contract AccountTest is BaseTest { } // Prepare main Intent structure (will be reused with same pre-calls) - Orchestrator.Intent memory baseIntent; + ICommon.Intent memory baseIntent; baseIntent.eoa = eoaAddress; baseIntent.paymentToken = address(paymentToken); baseIntent.paymentAmount = _bound(_random(), 0, 2 ** 32 - 1); @@ -327,12 +327,11 @@ contract AccountTest is BaseTest { vm.etch(eoaAddress, abi.encodePacked(hex"ef0100", impl)); // Use the prepared pre-calls on chain 1 - Orchestrator.Intent memory u1 = baseIntent; - u1.nonce = (0xc1d0 << 240) | 0; // Multichain nonce for main intent - u1.signature = _sig(adminKey, u1); + baseIntent.nonce = (0xc1d0 << 240) | 0; // Multichain nonce for main intent + baseIntent.signature = _sig(adminKey, oc.computeDigest(baseIntent)); // Execute on chain 1 - should succeed - assertEq(oc.execute(abi.encode(u1)), 0, "Execution should succeed on chain 1"); + assertEq(oc.execute(abi.encode(baseIntent)), 0, "Execution should succeed on chain 1"); // Verify keys were added on chain 1 uint256 keysCount1 = IthacaAccount(eoaAddress).keyCount(); diff --git a/test/Base.t.sol b/test/Base.t.sol index 1765a651..62c362ed 100644 --- a/test/Base.t.sol +++ b/test/Base.t.sol @@ -129,7 +129,7 @@ contract BaseTest is SoladyTest { k.keyHash = _hash(k.k); } - function _sig(DelegatedEOA memory d, Orchestrator.Intent memory i) + function _sig(DelegatedEOA memory d, ICommon.Intent memory i) internal view returns (bytes memory) @@ -141,7 +141,7 @@ contract BaseTest is SoladyTest { return _eoaSig(d.privateKey, digest); } - function _eoaSig(uint256 privateKey, Orchestrator.Intent memory i) + function _eoaSig(uint256 privateKey, ICommon.Intent memory i) internal view returns (bytes memory) @@ -154,11 +154,7 @@ contract BaseTest is SoladyTest { return abi.encodePacked(r, s, v); } - function _sig(PassKey memory k, Orchestrator.Intent memory i) - internal - view - returns (bytes memory) - { + function _sig(PassKey memory k, ICommon.Intent memory i) internal view returns (bytes memory) { return _sig(k, false, oc.computeDigest(i)); } @@ -184,7 +180,7 @@ contract BaseTest is SoladyTest { return _multiSig(k, _hash(k.k), false, digest); } - function _sig(MultiSigKey memory k, Orchestrator.Intent memory u) + function _sig(MultiSigKey memory k, ICommon.Intent memory u) internal view returns (bytes memory) @@ -248,7 +244,7 @@ contract BaseTest is SoladyTest { return abi.encodePacked(abi.encode(signatures), keyHash, uint8(preHash ? 1 : 0)); } - function _estimateGasForEOAKey(Orchestrator.Intent memory i) + function _estimateGasForEOAKey(ICommon.Intent memory i) internal returns (uint256 gExecute, uint256 gCombined, uint256 gUsed) { @@ -258,7 +254,7 @@ contract BaseTest is SoladyTest { return _estimateGas(i); } - function _estimateGas(PassKey memory k, Orchestrator.Intent memory i) + function _estimateGas(PassKey memory k, ICommon.Intent memory i) internal returns (uint256 gExecute, uint256 gCombined, uint256 gUsed) { @@ -271,7 +267,7 @@ contract BaseTest is SoladyTest { revert("Unsupported"); } - function _estimateGasForSecp256k1Key(bytes32 keyHash, Orchestrator.Intent memory i) + function _estimateGasForSecp256k1Key(bytes32 keyHash, ICommon.Intent memory i) internal returns (uint256 gExecute, uint256 gCombined, uint256 gUsed) { @@ -281,7 +277,7 @@ contract BaseTest is SoladyTest { return _estimateGas(i); } - function _estimateGasForSecp256r1Key(bytes32 keyHash, Orchestrator.Intent memory i) + function _estimateGasForSecp256r1Key(bytes32 keyHash, ICommon.Intent memory i) internal returns (uint256 gExecute, uint256 gCombined, uint256 gUsed) { @@ -290,7 +286,7 @@ contract BaseTest is SoladyTest { return _estimateGas(i); } - function _estimateGasForMultiSigKey(MultiSigKey memory k, Orchestrator.Intent memory u) + function _estimateGasForMultiSigKey(MultiSigKey memory k, ICommon.Intent memory u) internal returns (uint256 gExecute, uint256 gCombined, uint256 gUsed) { @@ -305,7 +301,7 @@ contract BaseTest is SoladyTest { ); } - function _estimateGas(Orchestrator.Intent memory i) + function _estimateGas(ICommon.Intent memory i) internal returns (uint256 gExecute, uint256 gCombined, uint256 gUsed) { @@ -324,7 +320,7 @@ contract BaseTest is SoladyTest { } struct _EstimateGasParams { - Orchestrator.Intent u; + ICommon.Intent u; uint8 paymentPerGasPrecision; uint256 paymentPerGas; uint256 combinedGasIncrement; @@ -490,4 +486,14 @@ contract BaseTest is SoladyTest { hex"3d604052610216565b60008060006ffffffffeffffffffffffffffffffffff60601b19808687098188890982838389096004098384858485093d510985868b8c096003090891508384828308850385848509089650838485858609600809850385868a880385088509089550505050808188880960020991505093509350939050565b81513d83015160408401516ffffffffeffffffffffffffffffffffff60601b19808384098183840982838388096004098384858485093d510985868a8b096003090896508384828308850385898a09089150610102848587890960020985868787880960080987038788878a0387088c0908848b523d8b015260408a0152565b505050505050505050565b81513d830151604084015185513d87015160408801518361013d578287523d870182905260408701819052610102565b80610157578587523d870185905260408701849052610102565b6ffffffffeffffffffffffffffffffffff60601b19808586098183840982818a099850828385830989099750508188830383838809089450818783038384898509870908935050826101be57836101be576101b28a89610082565b50505050505050505050565b808485098181860982828a09985082838a8b0884038483860386898a09080891506102088384868a0988098485848c09860386878789038f088a0908848d523d8d015260408c0152565b505050505050505050505050565b6020357fffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc6325513d6040357f7fffffff800000007fffffffffffffffde737d56d38bcf4279dce5617e3192a88111156102695782035b60206108005260206108205260206108405280610860526002830361088052826108a0526ffffffffeffffffffffffffffffffffff60601b198060031860205260603560803560203d60c061080060055afa60203d1416837f5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b8585873d5189898a09080908848384091484831085851016888710871510898b108b151016609f3611161616166103195760206080f35b60809182523d820152600160c08190527f6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c2966102009081527f4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f53d909101526102405261038992509050610100610082565b610397610200610400610082565b6103a7610100608061018061010d565b6103b7610200608061028061010d565b6103c861020061010061030061010d565b6103d961020061018061038061010d565b6103e9610400608061048061010d565b6103fa61040061010061050061010d565b61040b61040061018061058061010d565b61041c61040061020061060061010d565b61042c610600608061068061010d565b61043d61060061010061070061010d565b61044e61060061018061078061010d565b81815182350982825185098283846ffffffffeffffffffffffffffffffffff60601b193d515b82156105245781858609828485098384838809600409848586848509860986878a8b096003090885868384088703878384090886878887880960080988038889848b03870885090887888a8d096002098882830996508881820995508889888509600409945088898a8889098a098a8b86870960030908935088898687088a038a868709089a5088898284096002099950505050858687868709600809870387888b8a0386088409089850505050505b61018086891b60f71c16610600888a1b60f51c16176040810151801585151715610564578061055357506105fe565b81513d8301519750955093506105fe565b83858609848283098581890986878584098b0991508681880388858851090887838903898a8c88093d8a015109089350836105b957806105b9576105a9898c8c610008565b9a509b50995050505050506105fe565b8781820988818309898285099350898a8586088b038b838d038d8a8b0908089b50898a8287098b038b8c8f8e0388088909089c5050508788868b098209985050505050505b5082156106af5781858609828485098384838809600409848586848509860986878a8b096003090885868384088703878384090886878887880960080988038889848b03870885090887888a8d096002098882830996508881820995508889888509600409945088898a8889098a098a8b86870960030908935088898687088a038a868709089a5088898284096002099950505050858687868709600809870387888b8a0386088409089850505050505b61018086891b60f51c16610600888a1b60f31c161760408101518015851517156106ef57806106de5750610789565b81513d830151975095509350610789565b83858609848283098581890986878584098b0991508681880388858851090887838903898a8c88093d8a01510908935083610744578061074457610734898c8c610008565b9a509b5099505050505050610789565b8781820988818309898285099350898a8586088b038b838d038d8a8b0908089b50898a8287098b038b8c8f8e0388088909089c5050508788868b098209985050505050505b50600488019760fb19016104745750816107a2573d6040f35b81610860526002810361088052806108a0523d3d60c061080060055afa898983843d513d510987090614163d525050505050505050503d3df3fea264697066735822122063ce32ec0e56e7893a1f6101795ce2e38aca14dd12adb703c71fe3bee27da71e64736f6c634300081a0033"; vm.etch(address(0x100), verifierBytecode); } + + function _computeDigest(ICommon.Intent memory m, uint256 chainId) + internal + returns (bytes32 digest) + { + uint256 currChain = block.chainid; + vm.chainId(chainId); + digest = oc.computeDigest(m); + vm.chainId(currChain); + } } diff --git a/test/Benchmark.t.sol b/test/Benchmark.t.sol index 6b1b5c49..79f5aa3d 100644 --- a/test/Benchmark.t.sol +++ b/test/Benchmark.t.sol @@ -1714,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++) { - Orchestrator.Intent memory u; + ICommon.Intent memory u; u.eoa = delegatedEOAs[i].eoa; u.nonce = 0; u.combinedGas = 1000000; @@ -1780,7 +1780,7 @@ contract BenchmarkTest is BaseTest { d.d.setSpendLimit(k.keyHash, address(0), GuardedExecutor.SpendPeriod.Hour, 1 ether); vm.stopPrank(); - Orchestrator.Intent memory u; + ICommon.Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.combinedGas = 1000000; diff --git a/test/GuardedExecutor.t.sol b/test/GuardedExecutor.t.sol index 088bd597..86f89a2d 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; + ICommon.Intent memory u; u.eoa = d.eoa; u.combinedGas = 10000000; @@ -94,7 +94,7 @@ contract GuardedExecutorTest is BaseTest { d.d.setCanExecute(k.keyHash, address(paymentToken), _ANY_FN_SEL, true); vm.stopPrank(); - Orchestrator.Intent memory u; + ICommon.Intent memory u; u.eoa = d.eoa; u.combinedGas = 10000000; @@ -230,7 +230,7 @@ contract GuardedExecutorTest is BaseTest { vm.prank(address(0xb0b)); paymentToken.approve(d.eoa, 1 ether); - Orchestrator.Intent memory u; + ICommon.Intent memory u; u.eoa = d.eoa; u.combinedGas = 10000000; @@ -281,7 +281,7 @@ contract GuardedExecutorTest is BaseTest { } function testOnlySuperAdminAndEOACanSelfExecute() public { - Orchestrator.Intent memory u; + ICommon.Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; u.combinedGas = 10000000; @@ -349,7 +349,7 @@ contract GuardedExecutorTest is BaseTest { } function testSetAndRemoveSpendLimitRevertsForSuperAdmin() public { - Orchestrator.Intent memory u; + ICommon.Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; @@ -367,7 +367,7 @@ 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); @@ -400,7 +400,7 @@ contract GuardedExecutorTest is BaseTest { function testSetAndRemoveSpendLimit(uint256 amount) public { vm.warp(86400 * 100); - Orchestrator.Intent memory u; + ICommon.Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; @@ -438,7 +438,7 @@ 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); @@ -593,7 +593,7 @@ contract GuardedExecutorTest is BaseTest { } function testSetSpendLimitWithTwoPeriods() public { - Orchestrator.Intent memory u; + ICommon.Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; @@ -624,7 +624,7 @@ 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); @@ -638,7 +638,7 @@ 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); @@ -651,7 +651,7 @@ contract GuardedExecutorTest is BaseTest { } function testSpends(bytes32) public { - Orchestrator.Intent memory u; + ICommon.Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; @@ -686,7 +686,7 @@ 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); @@ -696,7 +696,7 @@ contract GuardedExecutorTest is BaseTest { // Test spends. { - u.nonce = 0; + u.nonce = d.d.getNonce(0); _deployPermit2(); if (_randomChance(2)) { @@ -835,9 +835,9 @@ 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); @@ -850,7 +850,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); diff --git a/test/Orchestrator.t.sol b/test/Orchestrator.t.sol index d79c4cab..2388a50d 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; + ICommon.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 ICommon.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]; + ICommon.Intent memory u = t.intents[i]; u.eoa = d.eoa; vm.deal(u.eoa, 2 ** 128 - 1); @@ -78,7 +78,7 @@ contract OrchestratorTest is BaseTest { bytes memory executionData = _transferExecutionData(address(paymentToken), address(0xabcd), 1 ether); - Orchestrator.Intent memory u; + ICommon.Intent memory u; u.eoa = alice.eoa; u.nonce = 0; u.executionData = executionData; @@ -107,7 +107,7 @@ contract OrchestratorTest is BaseTest { vm.prank(d.eoa); d.d.authorize(k.k); - Orchestrator.Intent memory u; + ICommon.Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.executionData = _transferExecutionData(address(paymentToken), address(0xabcd), 1 ether); @@ -154,7 +154,7 @@ contract OrchestratorTest is BaseTest { calls[0].to = target; calls[0].data = abi.encodeWithSignature("revertWithData(bytes)", data); - Orchestrator.Intent memory u; + ICommon.Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.executionData = abi.encode(calls); @@ -174,7 +174,7 @@ contract OrchestratorTest is BaseTest { paymentToken.mint(d.eoa, 500 ether); - Orchestrator.Intent memory u; + ICommon.Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.executionData = _transferExecutionData(address(paymentToken), address(0xabcd), 1 ether); @@ -212,7 +212,7 @@ contract OrchestratorTest is BaseTest { ds[i] = _randomEIP7702DelegatedEOA(); paymentToken.mint(ds[i].eoa, 1 ether); - Orchestrator.Intent memory u; + ICommon.Intent memory u; u.eoa = ds[i].eoa; u.nonce = 0; u.executionData = @@ -247,7 +247,7 @@ contract OrchestratorTest is BaseTest { calls[i] = _transferCall(address(paymentToken), address(0xabcd), 0.5 ether); } - Orchestrator.Intent memory u; + ICommon.Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.executionData = abi.encode(calls); @@ -271,7 +271,7 @@ contract OrchestratorTest is BaseTest { paymentToken.mint(d.eoa, 500 ether); - Orchestrator.Intent memory u; + ICommon.Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.executionData = _transferExecutionData(address(paymentToken), address(0xabcd), 1 ether); @@ -313,7 +313,7 @@ contract OrchestratorTest is BaseTest { paymentToken.mint(d.eoa, 10 ether); // Create base intent with common fields - Orchestrator.Intent memory baseIntent; + ICommon.Intent memory baseIntent; baseIntent.eoa = d.eoa; baseIntent.paymentToken = address(paymentToken); baseIntent.paymentAmount = 0.1 ether; @@ -322,7 +322,7 @@ contract OrchestratorTest is BaseTest { // Test case 1: Intent with no expiry (expiry = 0) should always be valid { - Orchestrator.Intent memory u = baseIntent; + ICommon.Intent memory u = baseIntent; u.nonce = d.d.getNonce(0); u.executionData = _transferExecutionData(address(paymentToken), address(0xabcd), 1 ether); @@ -335,7 +335,7 @@ contract OrchestratorTest is BaseTest { // Test case 2: Intent with future expiry should be valid { - Orchestrator.Intent memory u = baseIntent; + ICommon.Intent memory u = baseIntent; u.nonce = d.d.getNonce(0); u.executionData = _transferExecutionData(address(paymentToken), address(0xbcde), 1 ether); @@ -348,7 +348,7 @@ contract OrchestratorTest is BaseTest { // Test case 3: Intent with past expiry should fail { - Orchestrator.Intent memory u = baseIntent; + ICommon.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); @@ -365,7 +365,7 @@ contract OrchestratorTest is BaseTest { bytes[] memory encodedIntents = new bytes[](3); // Create base intent for batch with smaller amounts - Orchestrator.Intent memory batchBase; + ICommon.Intent memory batchBase; batchBase.eoa = d.eoa; batchBase.paymentToken = address(paymentToken); batchBase.paymentAmount = 0.05 ether; @@ -373,7 +373,7 @@ contract OrchestratorTest is BaseTest { batchBase.combinedGas = 10000000; // Valid intent with nonce 2 - Orchestrator.Intent memory u1 = batchBase; + ICommon.Intent memory u1 = batchBase; u1.nonce = 2; u1.executionData = _transferExecutionData(address(paymentToken), address(0x1111), 0.5 ether); @@ -382,7 +382,7 @@ contract OrchestratorTest is BaseTest { encodedIntents[0] = abi.encode(u1); // Expired intent with nonce 3 - Orchestrator.Intent memory u2 = batchBase; + ICommon.Intent memory u2 = batchBase; u2.nonce = 3; u2.executionData = _transferExecutionData(address(paymentToken), address(0x2222), 0.5 ether); @@ -391,7 +391,7 @@ contract OrchestratorTest is BaseTest { encodedIntents[1] = abi.encode(u2); // Another valid intent with nonce 3 (since nonce 3 wasn't consumed due to expiry) - Orchestrator.Intent memory u3 = batchBase; + ICommon.Intent memory u3 = batchBase; u3.nonce = 3; u3.executionData = _transferExecutionData(address(paymentToken), address(0x3333), 0.5 ether); @@ -424,7 +424,7 @@ contract OrchestratorTest is BaseTest { paymentToken.mint(ds[i].eoa, 1 ether); vm.deal(ds[i].eoa, 1 ether); - Orchestrator.Intent memory u; + ICommon.Intent memory u; u.eoa = ds[i].eoa; u.nonce = 0; u.executionData = @@ -463,7 +463,7 @@ contract OrchestratorTest is BaseTest { vm.prank(d.eoa); d.d.authorize(k.k); - Orchestrator.Intent memory u; + ICommon.Intent memory u; u.eoa = d.eoa; u.executionData = _executionData(address(0), 0, bytes("")); u.nonce = 0x2; @@ -478,7 +478,7 @@ contract OrchestratorTest is BaseTest { function testInvalidateNonce(uint96 seqKey, uint64 seq, uint64 seq2) public { uint256 nonce = (uint256(seqKey) << 64) | uint256(seq); - Orchestrator.Intent memory u; + ICommon.Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; @@ -656,7 +656,7 @@ contract OrchestratorTest is BaseTest { function testInitAndTransferInOneShot(bytes32) public { _TestAuthorizeWithPreCallsAndTransferTemps memory t; - Orchestrator.Intent memory u; + ICommon.Intent memory u; uint256 ephemeralPK = _randomPrivateKey(); t.eoa = vm.addr(ephemeralPK); @@ -752,7 +752,7 @@ contract OrchestratorTest is BaseTest { function testAuthorizeWithPreCallsAndTransfer(bytes32) public { _TestAuthorizeWithPreCallsAndTransferTemps memory t; - Orchestrator.Intent memory u; + ICommon.Intent memory u; Orchestrator.SignedCall memory pInit; if (_randomChance(2)) { @@ -967,7 +967,7 @@ contract OrchestratorTest is BaseTest { // 1 ether in the EOA for execution. vm.deal(address(d.d), 1 ether); - Orchestrator.Intent memory u; + ICommon.Intent memory u; u.eoa = d.eoa; u.payer = address(payer.d); @@ -1033,7 +1033,7 @@ contract OrchestratorTest is BaseTest { t.token = _randomChance(2) ? address(0) : address(paymentToken); t.isWithState = _randomChance(2); - Orchestrator.Intent memory u; + ICommon.Intent memory u; t.d = _randomEIP7702DelegatedEOA(); vm.deal(t.d.eoa, type(uint192).max); @@ -1127,7 +1127,7 @@ contract OrchestratorTest is BaseTest { t.testImplementationCheck = _randomChance(2); t.requireWrongImplementation = _randomChance(2); - Orchestrator.Intent memory u; + ICommon.Intent memory u; vm.deal(t.d.eoa, type(uint192).max); u.eoa = t.d.eoa; @@ -1239,7 +1239,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; + ICommon.Intent memory u; u.eoa = t.d.eoa; u.nonce = t.d.d.getNonce(0); u.executionData = abi.encode(calls); @@ -1426,12 +1426,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); @@ -1454,9 +1453,8 @@ contract OrchestratorTest is BaseTest { // 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 { @@ -1496,9 +1494,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); @@ -1552,6 +1549,7 @@ contract OrchestratorTest is BaseTest { 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); diff --git a/test/SimulateExecute.t.sol b/test/SimulateExecute.t.sol index 681266f7..14e76167 100644 --- a/test/SimulateExecute.t.sol +++ b/test/SimulateExecute.t.sol @@ -53,7 +53,7 @@ contract SimulateExecuteTest is BaseTest { abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) ); - Orchestrator.Intent memory i; + ICommon.Intent memory i; i.eoa = d.eoa; i.nonce = 0; i.executionData = t.executionData; @@ -122,7 +122,7 @@ contract SimulateExecuteTest is BaseTest { abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) ); - Orchestrator.Intent memory i; + ICommon.Intent memory i; i.eoa = d.eoa; i.nonce = 0; i.executionData = t.executionData; @@ -168,7 +168,7 @@ contract SimulateExecuteTest is BaseTest { abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) ); - Orchestrator.Intent memory i; + ICommon.Intent memory i; i.eoa = d.eoa; i.nonce = 0; i.executionData = t.executionData; @@ -224,7 +224,7 @@ contract SimulateExecuteTest is BaseTest { abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) ); - Orchestrator.Intent memory i; + ICommon.Intent memory i; i.eoa = d.eoa; i.nonce = 0; i.executionData = t.executionData; @@ -284,7 +284,7 @@ contract SimulateExecuteTest is BaseTest { abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) ); - Orchestrator.Intent memory i; + ICommon.Intent memory i; i.eoa = d.eoa; i.nonce = 0; i.executionData = t.executionData; From 4cde57e2386998159376e3c0b0e542a11455d723 Mon Sep 17 00:00:00 2001 From: howy <132113803+howydev@users.noreply.github.com> Date: Fri, 26 Sep 2025 12:14:29 -0400 Subject: [PATCH 2/4] 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 --- snapshots/BenchmarkTest.json | 30 +- src/Escrow.sol | 5 +- src/GuardedExecutor.sol | 16 +- src/IthacaAccount.sol | 59 ++- src/LayerZeroSettler.sol | 13 +- src/Orchestrator.sol | 335 ++++++++++-------- src/SimpleFunder.sol | 9 +- src/Simulator.sol | 88 +++-- src/interfaces/ICommon.sol | 77 ---- src/interfaces/IFunder.sol | 7 +- src/interfaces/IIthacaAccount.sol | 13 +- src/interfaces/IOrchestrator.sol | 9 +- src/libraries/IntentHelpers.sol | 158 +++++++++ src/libraries/LibNonce.sol | 4 +- src/vendor/layerzero/interfaces/IOAppCore.sol | 5 +- src/vendor/layerzero/mocks/EndpointV2Mock.sol | 13 +- src/vendor/layerzero/oapp/OAppReceiver.sol | 14 +- src/vendor/layerzero/oapp/OAppSender.sol | 16 +- test/Account.t.sol | 12 +- test/Base.t.sol | 155 ++++++-- test/Benchmark.t.sol | 62 ++-- test/GuardedExecutor.t.sol | 109 +++--- test/LayerZeroSettler.t.sol | 10 +- test/Orchestrator.t.sol | 183 +++++----- test/SimpleFunder.t.sol | 5 +- test/SimulateExecute.t.sol | 38 +- test/mocks/SendLibMock.sol | 15 +- test/utils/TestPlus.sol | 6 +- test/utils/interfaces/ISafe.sol | 3 +- test/utils/mocks/MockCallChecker.sol | 6 +- test/utils/mocks/MockERC4337Account.sol | 7 +- test/utils/mocks/MockOrchestrator.sol | 9 +- test/utils/mocks/MockPayerWithSignature.sol | 40 ++- .../mocks/MockPayerWithSignatureOptimized.sol | 40 ++- test/utils/mocks/MockPayerWithState.sol | 39 +- 35 files changed, 936 insertions(+), 674 deletions(-) create mode 100644 src/libraries/IntentHelpers.sol diff --git a/snapshots/BenchmarkTest.json b/snapshots/BenchmarkTest.json index 6c2cdec2..c323180e 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": "128100", - "testERC20Transfer_IthacaAccountWithSpendLimits": "193563", - "testERC20Transfer_IthacaAccount_AppSponsor": "138636", - "testERC20Transfer_IthacaAccount_AppSponsor_ERC20": "143937", - "testERC20Transfer_IthacaAccount_ERC20SelfPay": "128589", + "testERC20Transfer_IthacaAccount": "118043", + "testERC20Transfer_IthacaAccountWithSpendLimits": "184413", + "testERC20Transfer_IthacaAccount_AppSponsor": "129038", + "testERC20Transfer_IthacaAccount_AppSponsor_ERC20": "134327", + "testERC20Transfer_IthacaAccount_ERC20SelfPay": "118532", "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": "7535892", - "testERC20Transfer_batch100_IthacaAccount_AppSponsor": "8144196", - "testERC20Transfer_batch100_IthacaAccount_AppSponsor_ERC20": "7970208", - "testERC20Transfer_batch100_IthacaAccount_ERC20SelfPay": "7357152", + "testERC20Transfer_batch100_IthacaAccount": "6679204", + "testERC20Transfer_batch100_IthacaAccount_AppSponsor": "7333804", + "testERC20Transfer_batch100_IthacaAccount_AppSponsor_ERC20": "7160008", + "testERC20Transfer_batch100_IthacaAccount_ERC20SelfPay": "6500404", "testERC20Transfer_batch100_ZerodevKernel": "12631318", "testERC20Transfer_batch100_ZerodevKernel_ERC20SelfPay": "14149937", "testNativeTransfer_AlchemyModularAccount": "180829", "testNativeTransfer_CoinbaseSmartWallet": "178916", - "testNativeTransfer_IthacaAccount": "129456", - "testNativeTransfer_IthacaAccount_AppSponsor": "140023", - "testNativeTransfer_IthacaAccount_ERC20SelfPay": "137245", + "testNativeTransfer_IthacaAccount": "119427", + "testNativeTransfer_IthacaAccount_AppSponsor": "130423", + "testNativeTransfer_IthacaAccount_ERC20SelfPay": "127216", "testNativeTransfer_Safe4337": "198595", "testNativeTransfer_ZerodevKernel": "208635", "testUniswapV2Swap_AlchemyModularAccount": "238647", "testUniswapV2Swap_CoinbaseSmartWallet": "237451", "testUniswapV2Swap_ERC4337MinimalAccount": "230691", - "testUniswapV2Swap_IthacaAccount": "187244", - "testUniswapV2Swap_IthacaAccount_AppSponsor": "197744", - "testUniswapV2Swap_IthacaAccount_ERC20SelfPay": "192533", + "testUniswapV2Swap_IthacaAccount": "177131", + "testUniswapV2Swap_IthacaAccount_AppSponsor": "188126", + "testUniswapV2Swap_IthacaAccount_ERC20SelfPay": "182420", "testUniswapV2Swap_Safe4337": "257333", "testUniswapV2Swap_ZerodevKernel": "266367" } \ No newline at end of file diff --git a/src/Escrow.sol b/src/Escrow.sol index a924bc59..d6c7e2e4 100644 --- a/src/Escrow.sol +++ b/src/Escrow.sol @@ -235,8 +235,9 @@ contract Escrow is IEscrow { statuses[escrowId] = EscrowStatus.FINALIZED; // Check with the settler if the message has been sent from the correct sender and chainId. - bool isSettled = ISettler(_escrow.settler) - .read(_escrow.settlementId, _escrow.sender, _escrow.senderChainId); + bool isSettled = ISettler(_escrow.settler).read( + _escrow.settlementId, _escrow.sender, _escrow.senderChainId + ); if (!isSettled) { revert SettlementInvalid(); diff --git a/src/GuardedExecutor.sol b/src/GuardedExecutor.sol index cf5eb502..ab7e14f2 100644 --- a/src/GuardedExecutor.sol +++ b/src/GuardedExecutor.sol @@ -384,8 +384,9 @@ abstract contract GuardedExecutor is ERC7821 { if (_isSelfExecute(target, fnSel)) revert CannotSelfExecute(); // Impose a max capacity of 2048 for set enumeration, which should be more than enough. - _getGuardedExecutorKeyStorage(keyHash).canExecute - .update(_packCanExecute(target, fnSel), can, 2048); + _getGuardedExecutorKeyStorage(keyHash).canExecute.update( + _packCanExecute(target, fnSel), can, 2048 + ); emit CanExecuteSet(keyHash, target, fnSel, can); } @@ -408,7 +409,7 @@ abstract contract GuardedExecutor is ERC7821 { // check it in `canExecute` before any custom call checker. EnumerableMapLib.AddressToAddressMap storage checkers = - _getGuardedExecutorKeyStorage(keyHash).callCheckers; + _getGuardedExecutorKeyStorage(keyHash).callCheckers; // Impose a max capacity of 2048 for map enumeration, which should be more than enough. checkers.update(target, checker, checker != address(0), 2048); @@ -517,7 +518,12 @@ abstract contract GuardedExecutor is ERC7821 { /// @dev Returns an array of packed (`target`, `fnSel`) that `keyHash` is authorized to execute on. /// - `target` is in the upper 20 bytes. /// - `fnSel` is in the lower 4 bytes. - function canExecutePackedInfos(bytes32 keyHash) public view virtual returns (bytes32[] memory) { + function canExecutePackedInfos(bytes32 keyHash) + public + view + virtual + returns (bytes32[] memory) + { return _getGuardedExecutorKeyStorage(keyHash).canExecute.values(); } @@ -561,7 +567,7 @@ abstract contract GuardedExecutor is ERC7821 { returns (CallCheckerInfo[] memory results) { EnumerableMapLib.AddressToAddressMap storage checkers = - _getGuardedExecutorKeyStorage(keyHash).callCheckers; + _getGuardedExecutorKeyStorage(keyHash).callCheckers; results = new CallCheckerInfo[](checkers.length()); for (uint256 i; i < results.length; ++i) { (results[i].target, results[i].checker) = checkers.at(i); diff --git a/src/IthacaAccount.sol b/src/IthacaAccount.sol index a922c9b9..1e01a529 100644 --- a/src/IthacaAccount.sol +++ b/src/IthacaAccount.sol @@ -274,8 +274,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. @@ -399,7 +399,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); @@ -573,8 +578,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); } @@ -603,41 +609,28 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { uint256 paymentAmount, bytes32 keyHash, bytes32 intentDigest, - bytes calldata encodedIntent + address eoa, + address payer, + address paymentToken, + address paymentRecipient, + bytes calldata paymentSignature ) public virtual { - Intent calldata intent; - // Equivalent Solidity Code: (In the assembly intent is stored in calldata, instead of memory) - // Intent memory intent = abi.decode(encodedIntent, (Intent)); - // Gas Savings: - // ~2.5-3k gas for general cases, by avoiding duplicated bounds checks, and avoiding writing the intent to memory. - // Extracts the Intent from the calldata bytes, with minimal checks. - // NOTE: Only use this implementation if you are sure that the encodedIntent is coming from a trusted source. - // We can avoid standard bound checks here, because the Orchestrator already does these checks when it interacts with ALL the fields in the intent using solidity. - assembly ("memory-safe") { - let t := calldataload(encodedIntent.offset) - intent := add(t, encodedIntent.offset) - // Bounds check. We don't need to explicitly check the fields here. - // In the self call functions, we will use regular Solidity to access the - // dynamic fields like `signature`, which generate the implicit bounds checks. - if or(shr(64, t), lt(encodedIntent.length, 0x20)) { revert(0x00, 0x00) } - } - - if (!LibBit.and( - msg.sender == ORCHESTRATOR, - LibBit.or(intent.eoa == address(this), intent.payer == address(this)) - )) { + if ( + !LibBit.and( + msg.sender == ORCHESTRATOR, LibBit.or(eoa == address(this), payer == address(this)) + ) + ) { revert Unauthorized(); } // If this account is the paymaster, validate the paymaster signature. - if (intent.payer == address(this)) { + if (payer == address(this)) { if (_getAccountStorage().paymasterNonces[intentDigest]) { revert PaymasterNonceError(); } _getAccountStorage().paymasterNonces[intentDigest] = true; - (bool isValid, bytes32 k) = - unwrapAndValidateSignature(intentDigest, intent.paymentSignature); + (bool isValid, bytes32 k) = unwrapAndValidateSignature(intentDigest, paymentSignature); // Set the target key hash to the payer's. keyHash = k; @@ -654,13 +647,13 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { } } - TokenTransferLib.safeTransfer(intent.paymentToken, intent.paymentRecipient, paymentAmount); + TokenTransferLib.safeTransfer(paymentToken, paymentRecipient, paymentAmount); + // Increase spend. if (!(keyHash == bytes32(0) || _isSuperAdmin(keyHash))) { SpendStorage storage spends = _getGuardedExecutorKeyStorage(keyHash).spends; - _incrementSpent(spends.spends[intent.paymentToken], intent.paymentToken, paymentAmount); + _incrementSpent(spends.spends[paymentToken], paymentToken, paymentAmount); } - // Done to avoid compiler warnings. intentDigest = intentDigest; } diff --git a/src/LayerZeroSettler.sol b/src/LayerZeroSettler.sol index aa8d76a9..2695ac96 100644 --- a/src/LayerZeroSettler.sol +++ b/src/LayerZeroSettler.sol @@ -102,7 +102,13 @@ contract LayerZeroSettler is OApp, ISettler, EIP712 { } } - function _getPeerOrRevert(uint32 /* _eid */) internal view virtual override returns (bytes32) { + function _getPeerOrRevert(uint32 /* _eid */ ) + internal + view + virtual + override + returns (bytes32) + { // The peer address for all chains is automatically set to `address(this)` return bytes32(uint256(uint160(address(this)))); } @@ -133,10 +139,7 @@ contract LayerZeroSettler is OApp, ISettler, EIP712 { bytes calldata _payload, address, /*_executor*/ bytes calldata /*_extraData*/ - ) - internal - override - { + ) internal override { // Decode the settlement data (bytes32 settlementId, address sender, uint256 senderChainId) = abi.decode(_payload, (bytes32, address, uint256)); diff --git a/src/Orchestrator.sol b/src/Orchestrator.sol index 9d917502..b40519d1 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. @@ -199,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 @@ -215,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. @@ -224,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]); } } @@ -236,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); @@ -264,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 @@ -315,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) { @@ -332,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(); @@ -348,12 +337,12 @@ 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 (TokenTransferLib.balanceOf(i.paymentToken, payer) < i.paymentAmount) { + if (TokenTransferLib.balanceOf(_getPaymentToken(), payer) < _getPaymentAmount()) { err = PaymentError.selector; if (flags == _SIMULATION_MODE_FLAG) { @@ -366,42 +355,39 @@ 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. // If the self call is successful, we know that the payment has been made, // and the sequence for `nonce` has been incremented. // For more information, see `selfCallPayVerifyCall537021665()`. - selfCallSuccess := call( - g, - address(), - 0, - add(m, 0x1c), - add(encodedIntent.length, 0x24), - 0x00, - 0x20 - ) + selfCallSuccess := + call(g, address(), 0, add(m, 0x1c), 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()); } @@ -434,25 +420,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. @@ -468,58 +465,87 @@ 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; + { + bool isValid; + bytes calldata signature = _getNextBytes(ptr); + + uint256 nonce = _getNonce(); + + if (nonce >> 240 == MERKLE_VERIFICATION) { + // For multi chain intents, we have to verify using merkle sigs. + (isValid, keyHash) = _verifyMerkleSig(digest, eoa, signature); + + 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, signature); + } - if (i.nonce >> 240 == MERKLE_VERIFICATION) { - // For multi chain intents, we have to verify using merkle sigs. - (isValid, keyHash) = _verifyMerkleSig(digest, eoa, i.signature); + if (flags == _SIMULATION_MODE_FLAG) { + isValid = true; + } - // 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); + if (!isValid) { + revert VerificationError(); } - } else { - (isValid, keyHash) = _verify(digest, eoa, i.signature); - } - if (flags == _SIMULATION_MODE_FLAG) { - isValid = true; + _checkAndIncrementNonce(eoa, nonce); } - if (!isValid) revert VerificationError(); - - _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()) @@ -542,10 +568,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; @@ -632,7 +663,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. @@ -667,51 +698,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, 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. + 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 - // 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(); } } @@ -755,7 +794,6 @@ 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(4); f.set(0, SIGNED_CALL_TYPEHASH); @@ -763,31 +801,34 @@ contract Orchestrator is IOrchestrator, EIP712, CallContextChecker, ReentrancyGu f.set(2, _executionDataHash(p.executionData)); f.set(3, p.nonce); - return isMultichain ? _hashTypedDataSansChainId(f.hash()) : _hashTypedData(f.hash()); - } - - /// @dev Computes the EIP712 digest for the Intent. - function _computeDigest(Intent calldata i) internal view virtual returns (bytes32) { - // To avoid stack-too-deep. Faster than a regular Solidity array anyways. - bytes32[] memory f = EfficientHashLib.malloc(12); - f.set(0, INTENT_TYPEHASH); - f.set(1, uint160(i.eoa)); - f.set(2, _executionDataHash(i.executionData)); - f.set(3, i.nonce); - f.set(4, uint160(i.payer)); - f.set(5, uint160(i.paymentToken)); - f.set(6, i.paymentMaxAmount); - f.set(7, i.combinedGas); - f.set(8, _encodedArrHash(i.encodedPreCalls)); - f.set(9, _encodedArrHash(i.encodedFundTransfers)); - f.set(10, uint160(i.settler)); - f.set(11, i.expiry); - - return i.nonce >> 240 == MULTICHAIN_NONCE_PREFIX + return p.nonce >> 240 == MULTICHAIN_NONCE_PREFIX ? _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; + + 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`. function _executionDataHash(bytes calldata executionData) internal 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/Simulator.sol b/src/Simulator.sol index f70b05f4..06312ac5 100644 --- a/src/Simulator.sol +++ b/src/Simulator.sol @@ -3,10 +3,12 @@ pragma solidity ^0.8.23; import {ICommon} from "./interfaces/ICommon.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 //////////////////////////////////////////////////////////////////////// @@ -53,15 +55,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. @@ -100,10 +110,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); } @@ -114,13 +122,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); } @@ -163,7 +168,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. @@ -174,10 +179,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") { @@ -188,15 +193,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") { @@ -211,15 +222,27 @@ 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) + } } } @@ -244,15 +267,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 e9c428e3..9057b1f3 100644 --- a/src/interfaces/ICommon.sol +++ b/src/interfaces/ICommon.sol @@ -2,83 +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 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/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/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/libraries/LibNonce.sol b/src/libraries/LibNonce.sol index d4ceda58..ec67b396 100644 --- a/src/libraries/LibNonce.sol +++ b/src/libraries/LibNonce.sol @@ -33,7 +33,9 @@ library LibNonce { /// @dev Increments the sequence for the `seqKey` in nonce (i.e. upper 192 bits). /// This invalidates the nonces for the `seqKey`, up to (inclusive) `uint64(nonce)`. - function invalidate(mapping(uint192 => LibStorage.Ref) storage seqMap, uint256 nonce) internal { + function invalidate(mapping(uint192 => LibStorage.Ref) storage seqMap, uint256 nonce) + internal + { LibStorage.Ref storage s = seqMap[uint192(nonce >> 64)]; if (uint64(nonce) < s.value) revert NewSequenceMustBeLarger(); s.value = Math.rawAdd(Math.min(uint64(nonce), 2 ** 64 - 2), 1); diff --git a/src/vendor/layerzero/interfaces/IOAppCore.sol b/src/vendor/layerzero/interfaces/IOAppCore.sol index 4a87d7a2..05f0663f 100644 --- a/src/vendor/layerzero/interfaces/IOAppCore.sol +++ b/src/vendor/layerzero/interfaces/IOAppCore.sol @@ -2,9 +2,8 @@ pragma solidity ^0.8.20; -import { - ILayerZeroEndpointV2 -} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import {ILayerZeroEndpointV2} from + "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; /** * @title IOAppCore diff --git a/src/vendor/layerzero/mocks/EndpointV2Mock.sol b/src/vendor/layerzero/mocks/EndpointV2Mock.sol index 40042d7d..1fbe06e0 100644 --- a/src/vendor/layerzero/mocks/EndpointV2Mock.sol +++ b/src/vendor/layerzero/mocks/EndpointV2Mock.sol @@ -14,12 +14,10 @@ import { ILayerZeroEndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; import { - ISendLib, - Packet + 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 {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"; @@ -211,8 +209,9 @@ contract EndpointV2Mock is _origin.nonce, abi.encodePacked(_guid, _message) ); - ILayerZeroReceiver(_receiver) - .lzReceive{value: msg.value}(_origin, _guid, _message, msg.sender, _extraData); + ILayerZeroReceiver(_receiver).lzReceive{value: msg.value}( + _origin, _guid, _message, msg.sender, _extraData + ); emit PacketDelivered(_origin, _receiver); } diff --git a/src/vendor/layerzero/oapp/OAppReceiver.sol b/src/vendor/layerzero/oapp/OAppReceiver.sol index d3c34ad0..6cac2d03 100644 --- a/src/vendor/layerzero/oapp/OAppReceiver.sol +++ b/src/vendor/layerzero/oapp/OAppReceiver.sol @@ -52,12 +52,7 @@ abstract contract OAppReceiver is IOAppReceiver, OAppCore { Origin calldata, /*_origin*/ bytes calldata, /*_message*/ address _sender - ) - public - view - virtual - returns (bool) - { + ) public view virtual returns (bool) { return _sender == address(this); } @@ -89,12 +84,7 @@ abstract contract OAppReceiver is IOAppReceiver, OAppCore { uint32, /*_srcEid*/ bytes32 /*_sender*/ - ) - public - view - virtual - returns (uint64 nonce) - { + ) public view virtual returns (uint64 nonce) { return 0; } diff --git a/src/vendor/layerzero/oapp/OAppSender.sol b/src/vendor/layerzero/oapp/OAppSender.sol index 6fdf8ef4..639355e8 100644 --- a/src/vendor/layerzero/oapp/OAppSender.sol +++ b/src/vendor/layerzero/oapp/OAppSender.sol @@ -90,16 +90,14 @@ abstract contract OAppSender is OAppCore { uint256 messageValue = _payNative(_fee.nativeFee); if (_fee.lzTokenFee > 0) _payLzToken(_fee.lzTokenFee); - return endpoint. + return endpoint // solhint-disable-next-line check-send-result - send{ - value: messageValue - }( - MessagingParams( - _dstEid, _getPeerOrRevert(_dstEid), _message, _options, _fee.lzTokenFee > 0 - ), - _refundAddress - ); + .send{value: messageValue}( + MessagingParams( + _dstEid, _getPeerOrRevert(_dstEid), _message, _options, _fee.lzTokenFee > 0 + ), + _refundAddress + ); } /** diff --git a/test/Account.t.sol b/test/Account.t.sol index 7500c4d3..2b9abea6 100644 --- a/test/Account.t.sol +++ b/test/Account.t.sol @@ -93,12 +93,10 @@ 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(); bytes32 domain = keccak256( abi.encode( 0x035aff83d86937d35b32e04f0ddc6ff469290eef2f1b692d8a815c89404d4749, // DOMAIN_TYPEHASH with only verifyingContract - verifyingContract + d.eoa ) ); replaySafeDigest = keccak256(abi.encodePacked("\x19\x01", domain, replaySafeDigest)); @@ -303,7 +301,7 @@ contract AccountTest is BaseTest { } // Prepare main Intent structure (will be reused with same pre-calls) - ICommon.Intent memory baseIntent; + Intent memory baseIntent; baseIntent.eoa = eoaAddress; baseIntent.paymentToken = address(paymentToken); baseIntent.paymentAmount = _bound(_random(), 0, 2 ** 32 - 1); @@ -328,10 +326,10 @@ contract AccountTest is BaseTest { // Use the prepared pre-calls on chain 1 baseIntent.nonce = (0xc1d0 << 240) | 0; // Multichain nonce for main intent - baseIntent.signature = _sig(adminKey, oc.computeDigest(baseIntent)); + baseIntent.signature = _sig(adminKey, computeDigest(baseIntent)); // Execute on chain 1 - should succeed - assertEq(oc.execute(abi.encode(baseIntent)), 0, "Execution should succeed on chain 1"); + assertEq(oc.execute(encodeIntent(baseIntent)), 0, "Execution should succeed on chain 1"); // Verify keys were added on chain 1 uint256 keysCount1 = IthacaAccount(eoaAddress).keyCount(); @@ -347,7 +345,7 @@ contract AccountTest is BaseTest { vm.etch(eoaAddress, abi.encodePacked(hex"ef0100", impl)); // Execution should succeed due to multichain nonce in pre-calls - assertEq(oc.execute(abi.encode(baseIntent)), 0, "Should succeed due to multichain nonce"); + assertEq(oc.execute(encodeIntent(baseIntent)), 0, "Should succeed due to multichain nonce"); // Verify keys were added on chain 137 uint256 keysCount137 = IthacaAccount(eoaAddress).keyCount(); diff --git a/test/Base.t.sol b/test/Base.t.sol index 62c362ed..c32f7a59 100644 --- a/test/Base.t.sol +++ b/test/Base.t.sol @@ -23,8 +23,9 @@ import {GuardedExecutor} from "../src/IthacaAccount.sol"; import {IOrchestrator} from "../src/interfaces/IOrchestrator.sol"; import {Simulator} from "../src/Simulator.sol"; import {ICommon} from "../src/interfaces/ICommon.sol"; +import {IntentHelpers} from "../src/libraries/IntentHelpers.sol"; -contract BaseTest is SoladyTest { +contract BaseTest is SoladyTest, IntentHelpers { using LibRLP for LibRLP.List; MockOrchestrator oc; @@ -78,6 +79,106 @@ contract BaseTest is SoladyTest { MockAccount d; } + // The struct isnt used in prod, but makes test setup easier + struct Intent { + address supportedAccountImplementation; + address eoa; + uint256 nonce; + address payer; + address paymentToken; + uint256 paymentMaxAmount; + uint256 combinedGas; + uint256 expiry; + uint256 paymentAmount; + address paymentRecipient; + bytes executionData; + address funder; + bytes funderSignature; + bytes[] encodedFundTransfers; + bytes[] encodedPreCalls; + bytes signature; + address settler; // To use settler, nonce needs to have `MERKLE_VERIFICATION` prefix + bytes settlerContext; // To use settler, nonce needs to have `MERKLE_VERIFICATION` prefix + bytes paymentSignature; + } + + function computeDigest(Intent memory i) internal view returns (bytes32) { + bytes32 structHash = keccak256( + abi.encode( + oc.INTENT_TYPEHASH(), + keccak256(i.executionData), + i.eoa, + i.nonce, + i.payer, + i.paymentToken, + i.paymentMaxAmount, + i.combinedGas, + i.expiry + ) + ); + + // Apply EIP712 domain separator + bytes32 result = i.nonce >> 240 == oc.MULTICHAIN_NONCE_PREFIX() + ? oc.hashTypedDataSansChainId(structHash) + : oc.hashTypedData(structHash); + + return result; + } + + function encodeIntent(Intent memory i, bool isStateOverride, uint256 combinedGasOverride) + internal + pure + returns (bytes memory) + { + return abi.encodePacked(encodeIntent(i), isStateOverride, combinedGasOverride); + } + + function encodeIntent(Intent memory i) internal pure returns (bytes memory) { + bytes memory returnData = abi.encodePacked( + i.supportedAccountImplementation, + abi.encode(i.eoa), // pad the fields that go into 712 digest to 32b so we can do a calldatacopy + i.nonce, + abi.encode(i.payer), + abi.encode(i.paymentToken), + i.paymentMaxAmount, + i.combinedGas, + i.expiry, + i.paymentAmount, + i.paymentRecipient + ); + + { + bytes memory encodedPreCallsBytes = + i.encodedPreCalls.length > 0 ? abi.encode(i.encodedPreCalls) : bytes(""); + bytes memory fundDataBytes = i.funder != address(0) + ? abi.encode(i.funder, i.funderSignature, i.encodedFundTransfers) + : bytes(""); + + returnData = abi.encodePacked( + returnData, + uint256(i.executionData.length), + i.executionData, + uint256(fundDataBytes.length), + fundDataBytes, + uint256(encodedPreCallsBytes.length), + encodedPreCallsBytes, + uint256(i.signature.length), + i.signature + ); + } + + if (i.nonce >> 240 == 0x6D76) { + bytes memory settlerBytes = + i.settler != address(0) ? abi.encode(i.settler, i.settlerContext) : bytes(""); + + returnData = abi.encodePacked(returnData, uint256(settlerBytes.length), settlerBytes); + } + + returnData = + abi.encodePacked(returnData, uint256(i.paymentSignature.length), i.paymentSignature); + return returnData; + } + function setUp() public virtual { oc = new MockOrchestrator(); paymentToken = new MockPaymentToken(); @@ -129,11 +230,7 @@ contract BaseTest is SoladyTest { k.keyHash = _hash(k.k); } - function _sig(DelegatedEOA memory d, ICommon.Intent memory i) - internal - view - returns (bytes memory) - { + function _sig(DelegatedEOA memory d, Intent memory i) internal view returns (bytes memory) { return _eoaSig(d.privateKey, i); } @@ -141,12 +238,8 @@ contract BaseTest is SoladyTest { return _eoaSig(d.privateKey, digest); } - function _eoaSig(uint256 privateKey, ICommon.Intent memory i) - internal - view - returns (bytes memory) - { - return _eoaSig(privateKey, oc.computeDigest(i)); + function _eoaSig(uint256 privateKey, Intent memory i) internal view returns (bytes memory) { + return _eoaSig(privateKey, computeDigest(i)); } function _eoaSig(uint256 privateKey, bytes32 digest) internal pure returns (bytes memory) { @@ -154,8 +247,8 @@ contract BaseTest is SoladyTest { return abi.encodePacked(r, s, v); } - function _sig(PassKey memory k, ICommon.Intent memory i) internal view returns (bytes memory) { - return _sig(k, false, oc.computeDigest(i)); + function _sig(PassKey memory k, Intent memory i) internal view returns (bytes memory) { + return _sig(k, false, computeDigest(i)); } function _sig(PassKey memory k, bytes32 digest) internal pure returns (bytes memory) { @@ -180,12 +273,8 @@ contract BaseTest is SoladyTest { return _multiSig(k, _hash(k.k), false, digest); } - function _sig(MultiSigKey memory k, ICommon.Intent memory u) - internal - view - returns (bytes memory) - { - return _multiSig(k, _hash(k.k), false, oc.computeDigest(u)); + function _sig(MultiSigKey memory k, Intent memory u) internal view returns (bytes memory) { + return _multiSig(k, _hash(k.k), false, computeDigest(u)); } function _sig(MultiSigKey memory k, bool prehash, bytes32 digest) @@ -244,7 +333,7 @@ contract BaseTest is SoladyTest { return abi.encodePacked(abi.encode(signatures), keyHash, uint8(preHash ? 1 : 0)); } - function _estimateGasForEOAKey(ICommon.Intent memory i) + function _estimateGasForEOAKey(Intent memory i) internal returns (uint256 gExecute, uint256 gCombined, uint256 gUsed) { @@ -254,7 +343,7 @@ contract BaseTest is SoladyTest { return _estimateGas(i); } - function _estimateGas(PassKey memory k, ICommon.Intent memory i) + function _estimateGas(PassKey memory k, Intent memory i) internal returns (uint256 gExecute, uint256 gCombined, uint256 gUsed) { @@ -267,7 +356,7 @@ contract BaseTest is SoladyTest { revert("Unsupported"); } - function _estimateGasForSecp256k1Key(bytes32 keyHash, ICommon.Intent memory i) + function _estimateGasForSecp256k1Key(bytes32 keyHash, Intent memory i) internal returns (uint256 gExecute, uint256 gCombined, uint256 gUsed) { @@ -277,7 +366,7 @@ contract BaseTest is SoladyTest { return _estimateGas(i); } - function _estimateGasForSecp256r1Key(bytes32 keyHash, ICommon.Intent memory i) + function _estimateGasForSecp256r1Key(bytes32 keyHash, Intent memory i) internal returns (uint256 gExecute, uint256 gCombined, uint256 gUsed) { @@ -286,7 +375,7 @@ contract BaseTest is SoladyTest { return _estimateGas(i); } - function _estimateGasForMultiSigKey(MultiSigKey memory k, ICommon.Intent memory u) + function _estimateGasForMultiSigKey(MultiSigKey memory k, Intent memory u) internal returns (uint256 gExecute, uint256 gCombined, uint256 gUsed) { @@ -301,7 +390,7 @@ contract BaseTest is SoladyTest { ); } - function _estimateGas(ICommon.Intent memory i) + function _estimateGas(Intent memory i) internal returns (uint256 gExecute, uint256 gCombined, uint256 gUsed) { @@ -309,7 +398,7 @@ contract BaseTest is SoladyTest { vm.deal(_ORIGIN_ADDRESS, type(uint192).max); (gUsed, 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)); // gExecute > (100k + combinedGas) * 64/63 gExecute = Math.mulDiv(gCombined + 110_000, 64, 63); @@ -320,7 +409,7 @@ contract BaseTest is SoladyTest { } struct _EstimateGasParams { - ICommon.Intent u; + Intent u; uint8 paymentPerGasPrecision; uint256 paymentPerGas; uint256 combinedGasIncrement; @@ -344,7 +433,7 @@ contract BaseTest is SoladyTest { p.paymentPerGas, p.combinedGasIncrement, p.combinedGasVerificationOffset, - abi.encode(p.u) + encodeIntent(p.u) ); vm.revertToStateAndDelete(snapshot); } @@ -487,13 +576,11 @@ contract BaseTest is SoladyTest { vm.etch(address(0x100), verifierBytecode); } - function _computeDigest(ICommon.Intent memory m, uint256 chainId) - internal - returns (bytes32 digest) - { + function _computeDigest(Intent memory m, uint256 chainId) internal returns (bytes32 digest) { uint256 currChain = block.chainid; vm.chainId(chainId); - digest = oc.computeDigest(m); + digest = computeDigest(m); + vm.chainId(currChain); } } diff --git a/test/Benchmark.t.sol b/test/Benchmark.t.sol index 79f5aa3d..77001178 100644 --- a/test/Benchmark.t.sol +++ b/test/Benchmark.t.sol @@ -56,8 +56,7 @@ contract BenchmarkTest is BaseTest { address constant _ZERODEV_KERNEL_FACTORY_ADDR = 0x2577507b78c2008Ff367261CB6285d44ba5eF2E9; address constant _ZERODEV_KERNEL_ECDSA_VALIDATION = 0x845ADb2C711129d4f3966735eD98a9F09fC4cE57; - address constant _ALCHEMY_MODULAR_ACCOUNT_IMPL_ADDR = - 0x000000000000c5A9089039570Dd36455b5C07383; + address constant _ALCHEMY_MODULAR_ACCOUNT_IMPL_ADDR = 0x000000000000c5A9089039570Dd36455b5C07383; address constant _ALCHEMY_MODULAR_ACCOUNT_FACTORY_ADDR = 0x00000000000017c61b5bEe81050EC8eFc9c6fecd; @@ -93,6 +92,7 @@ contract BenchmarkTest is BaseTest { SELF_ERC20, APP_SPONSOR, // App sponsoring transaction cost (in native tokens) APP_SPONSOR_ERC20 // App sponsoring transaction cost (in ERC20 tokens) + } function setUp() public override { @@ -120,17 +120,20 @@ 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(""); - stdstore.target(_PIMLICO_PAYMASTER_V06).sig(IPimlicoPaymaster.signers.selector) - .with_key(paymasterSigner).checked_write(true); + stdstore.target(_PIMLICO_PAYMASTER_V06).sig(IPimlicoPaymaster.signers.selector).with_key( + paymasterSigner + ).checked_write(true); - stdstore.target(_PIMLICO_PAYMASTER_V07).sig(IPimlicoPaymaster.signers.selector) - .with_key(paymasterSigner).checked_write(true); + stdstore.target(_PIMLICO_PAYMASTER_V07).sig(IPimlicoPaymaster.signers.selector).with_key( + paymasterSigner + ).checked_write(true); _giveAccountSomeTokens(relayer); @@ -1224,18 +1227,17 @@ contract BenchmarkTest is BaseTest { for (uint256 i = 0; i < numAccounts; i++) { (eoas[i], privateKeys[i]) = makeAddrAndKey(string(abi.encodePacked("zerodev-kernel", i))); - accounts[i] = IKernelFactory(_ZERODEV_KERNEL_FACTORY_ADDR) - .createAccount( - abi.encodeWithSelector( - IKernel.initialize.selector, - validatorToIdentifier(_ZERODEV_KERNEL_ECDSA_VALIDATION), - address(0), // no hooks - abi.encodePacked(eoas[i]), // owner - hex"", // no hookData - new bytes[](0) // no init datas - ), - bytes32(uint256(i)) - ); + accounts[i] = IKernelFactory(_ZERODEV_KERNEL_FACTORY_ADDR).createAccount( + abi.encodeWithSelector( + IKernel.initialize.selector, + validatorToIdentifier(_ZERODEV_KERNEL_ECDSA_VALIDATION), + address(0), // no hooks + abi.encodePacked(eoas[i]), // owner + hex"", // no hookData + new bytes[](0) // no init datas + ), + bytes32(uint256(i)) + ); _giveAccountSomeTokens(accounts[i]); } } @@ -1518,7 +1520,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 +1716,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++) { - ICommon.Intent memory u; + Intent memory u; u.eoa = delegatedEOAs[i].eoa; u.nonce = 0; u.combinedGas = 1000000; @@ -1750,14 +1752,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; @@ -1771,16 +1773,16 @@ contract BenchmarkTest is BaseTest { vm.startPrank(d.eoa); d.d.authorize(k.k); - d.d - .setCanExecute( + d.d.setCanExecute( k.keyHash, address(paymentToken), bytes4(keccak256("transfer(address,uint256)")), true ); - d.d - .setSpendLimit(k.keyHash, address(paymentToken), GuardedExecutor.SpendPeriod.Hour, 1 ether); + d.d.setSpendLimit( + k.keyHash, address(paymentToken), GuardedExecutor.SpendPeriod.Hour, 1 ether + ); d.d.setSpendLimit(k.keyHash, address(0), GuardedExecutor.SpendPeriod.Hour, 1 ether); vm.stopPrank(); - ICommon.Intent memory u; + Intent memory u; u.eoa = d.eoa; u.nonce = 0; u.combinedGas = 1000000; @@ -1792,7 +1794,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/GuardedExecutor.t.sol b/test/GuardedExecutor.t.sol index 86f89a2d..4fc471fc 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); - ICommon.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(); - ICommon.Intent memory u; + Intent memory u; u.eoa = d.eoa; u.combinedGas = 10000000; @@ -112,20 +115,21 @@ 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`. vm.startPrank(d.eoa); - d.d - .setSpendLimit(k.keyHash, address(paymentToken), GuardedExecutor.SpendPeriod.Day, 1 ether); + d.d.setSpendLimit( + k.keyHash, address(paymentToken), GuardedExecutor.SpendPeriod.Day, 1 ether + ); vm.stopPrank(); 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. @@ -230,7 +234,7 @@ contract GuardedExecutorTest is BaseTest { vm.prank(address(0xb0b)); paymentToken.approve(d.eoa, 1 ether); - ICommon.Intent memory u; + Intent memory u; u.eoa = d.eoa; u.combinedGas = 10000000; @@ -256,21 +260,22 @@ 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 - .setSpendLimit(k.keyHash, address(paymentToken), GuardedExecutor.SpendPeriod.Day, 1 ether); + d.d.setSpendLimit( + k.keyHash, address(paymentToken), GuardedExecutor.SpendPeriod.Day, 1 ether + ); vm.stopPrank(); 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); } @@ -281,7 +286,7 @@ contract GuardedExecutorTest is BaseTest { } function testOnlySuperAdminAndEOACanSelfExecute() public { - ICommon.Intent memory u; + Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; u.combinedGas = 10000000; @@ -323,14 +328,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(); @@ -338,7 +343,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" ); @@ -349,7 +354,7 @@ contract GuardedExecutorTest is BaseTest { } function testSetAndRemoveSpendLimitRevertsForSuperAdmin() public { - ICommon.Intent memory u; + Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; @@ -371,7 +376,7 @@ contract GuardedExecutorTest is BaseTest { u.signature = _sig(d, u); - assertEq(oc.execute(abi.encode(u)), 0); + assertEq(oc.execute(encodeIntent(u)), 0); } // Set spend limits. { @@ -382,7 +387,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. { @@ -393,14 +398,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); - ICommon.Intent memory u; + Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; @@ -442,7 +447,7 @@ contract GuardedExecutorTest is BaseTest { 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); } @@ -454,7 +459,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) { @@ -470,7 +475,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); @@ -486,7 +491,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]) { @@ -509,7 +514,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) { @@ -530,7 +535,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); } @@ -546,14 +551,16 @@ contract GuardedExecutorTest is BaseTest { // If first 4bytes are 0xdfc924d5, then it's "anotherTransfer" call, and the spend limit will not catch it. if ( - (calls[0].data[0] == bytes1(uint8(0xdf)) + ( + calls[0].data[0] == bytes1(uint8(0xdf)) && calls[0].data[1] == bytes1(uint8(0xc9)) && calls[0].data[2] == bytes1(uint8(0x24)) - && calls[0].data[3] == bytes1(uint8(0xd5))) || amount == 0 + && 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()"))); } } @@ -566,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) { @@ -583,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) { @@ -593,7 +600,7 @@ contract GuardedExecutorTest is BaseTest { } function testSetSpendLimitWithTwoPeriods() public { - ICommon.Intent memory u; + Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; @@ -628,7 +635,7 @@ contract GuardedExecutorTest is BaseTest { 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,7 +649,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) { if (infos[i].token == token0) assertEq(infos[i].spent, amount0); @@ -651,7 +658,7 @@ contract GuardedExecutorTest is BaseTest { } function testSpends(bytes32) public { - ICommon.Intent memory u; + Intent memory u; DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); u.eoa = d.eoa; @@ -690,7 +697,7 @@ contract GuardedExecutorTest is BaseTest { 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); } @@ -752,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]); @@ -801,8 +808,10 @@ contract GuardedExecutorTest is BaseTest { _testSpendWithPassKeyViaOrchestrator(_randomSecp256k1PassKey(), address(0)); } - function _testSpendWithPassKeyViaOrchestrator(PassKey memory k, address tokenToSpend) internal { - Orchestrator.Intent memory u; + function _testSpendWithPassKeyViaOrchestrator(PassKey memory k, address tokenToSpend) + internal + { + Intent memory u; GuardedExecutor.SpendInfo memory info; uint256 gExecute; @@ -840,7 +849,7 @@ contract GuardedExecutorTest is BaseTest { 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); @@ -860,7 +869,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); @@ -874,7 +883,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. @@ -888,7 +897,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); } @@ -928,7 +937,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..6d3e8299 100644 --- a/test/LayerZeroSettler.t.sol +++ b/test/LayerZeroSettler.t.sol @@ -555,7 +555,9 @@ contract LayerZeroSettlerTest is Test { assertEq(address(settlerA).balance, balanceBefore + 5 ether); } - function testFuzz_send_differentSettlementIds(bytes32 settlementId, uint8 numEndpoints) public { + function testFuzz_send_differentSettlementIds(bytes32 settlementId, uint8 numEndpoints) + public + { vm.assume(numEndpoints > 0 && numEndpoints <= 3); uint32[] memory endpointIds = new uint32[](numEndpoints); @@ -716,9 +718,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/Orchestrator.t.sol b/test/Orchestrator.t.sol index 2388a50d..a024e3e5 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 { - ICommon.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 ICommon.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; - ICommon.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); - ICommon.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); - ICommon.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); - ICommon.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); - ICommon.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); - ICommon.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); } - ICommon.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); - ICommon.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 - ICommon.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 { - ICommon.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 { - ICommon.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 { - ICommon.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 - ICommon.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 - ICommon.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 - ICommon.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) - ICommon.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); - ICommon.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); - ICommon.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); - ICommon.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; - ICommon.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; - ICommon.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. } @@ -932,12 +930,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); @@ -967,7 +965,7 @@ contract OrchestratorTest is BaseTest { // 1 ether in the EOA for execution. vm.deal(address(d.d), 1 ether); - ICommon.Intent memory u; + Intent memory u; u.eoa = d.eoa; u.payer = address(payer.d); @@ -980,7 +978,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. @@ -989,12 +987,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); @@ -1033,7 +1031,7 @@ contract OrchestratorTest is BaseTest { t.token = _randomChance(2) ? address(0) : address(paymentToken); t.isWithState = _randomChance(2); - ICommon.Intent memory u; + Intent memory u; t.d = _randomEIP7702DelegatedEOA(); vm.deal(t.d.eoa, type(uint192).max); @@ -1051,7 +1049,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); @@ -1069,7 +1067,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); @@ -1080,7 +1078,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 ); @@ -1098,7 +1096,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); @@ -1108,7 +1106,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); @@ -1127,7 +1125,7 @@ contract OrchestratorTest is BaseTest { t.testImplementationCheck = _randomChance(2); t.requireWrongImplementation = _randomChance(2); - ICommon.Intent memory u; + Intent memory u; vm.deal(t.d.eoa, type(uint192).max); u.eoa = t.d.eoa; @@ -1147,12 +1145,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); } @@ -1239,7 +1237,7 @@ contract OrchestratorTest is BaseTest { vm.expectRevert(bytes4(keccak256("InvalidKeyHash()"))); t.d.d.execute(_ERC7821_BATCH_EXECUTION_MODE, abi.encode(calls)); - ICommon.Intent memory u; + Intent memory u; u.eoa = t.d.eoa; u.nonce = t.d.d.getNonce(0); u.executionData = abi.encode(calls); @@ -1249,14 +1247,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)); @@ -1280,14 +1277,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); @@ -1296,7 +1292,6 @@ contract OrchestratorTest is BaseTest { t.multiSigKey.threshold = newThreshold; } } - // Test removeOwner { uint256 removeIndex = _bound(_random(), 0, o.length - 1); @@ -1315,7 +1310,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); @@ -1336,9 +1331,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; @@ -1449,7 +1444,7 @@ 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; @@ -1479,9 +1474,7 @@ contract OrchestratorTest is BaseTest { calls[0] = ERC7821.Call({ to: address(t.usdcBase), value: 0, - data: abi.encodeWithSignature( - "approve(address,uint256)", address(t.escrowBase), 600 - ) + data: abi.encodeWithSignature("approve(address,uint256)", address(t.escrowBase), 600) }); // Then call escrow function calls[1] = ERC7821.Call({ @@ -1542,7 +1535,7 @@ 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); @@ -1560,7 +1553,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( @@ -1572,15 +1565,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( @@ -1593,7 +1586,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); @@ -1619,7 +1612,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); @@ -1654,7 +1647,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); @@ -1683,7 +1676,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); @@ -1730,7 +1723,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); @@ -1753,11 +1746,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); diff --git a/test/SimpleFunder.t.sol b/test/SimpleFunder.t.sol index b1c4608e..83114776 100644 --- a/test/SimpleFunder.t.sol +++ b/test/SimpleFunder.t.sol @@ -66,9 +66,8 @@ contract SimpleFunderTest is Test { ) ); - bytes32 structHash = keccak256( - abi.encode(WITHDRAWAL_TYPE_HASH, _token, _recipient, amount, deadline, nonce) - ); + bytes32 structHash = + keccak256(abi.encode(WITHDRAWAL_TYPE_HASH, _token, _recipient, amount, deadline, nonce)); return keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); } diff --git a/test/SimulateExecute.t.sol b/test/SimulateExecute.t.sol index 14e76167..0222deed 100644 --- a/test/SimulateExecute.t.sol +++ b/test/SimulateExecute.t.sol @@ -53,7 +53,7 @@ contract SimulateExecuteTest is BaseTest { abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) ); - ICommon.Intent memory i; + Intent memory i; i.eoa = d.eoa; i.nonce = 0; i.executionData = t.executionData; @@ -74,19 +74,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; @@ -96,11 +96,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 { @@ -122,7 +122,7 @@ contract SimulateExecuteTest is BaseTest { abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) ); - ICommon.Intent memory i; + Intent memory i; i.eoa = d.eoa; i.nonce = 0; i.executionData = t.executionData; @@ -141,11 +141,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 { @@ -168,7 +168,7 @@ contract SimulateExecuteTest is BaseTest { abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) ); - ICommon.Intent memory i; + Intent memory i; i.eoa = d.eoa; i.nonce = 0; i.executionData = t.executionData; @@ -190,7 +190,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); @@ -200,7 +200,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); } @@ -224,7 +224,7 @@ contract SimulateExecuteTest is BaseTest { abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) ); - ICommon.Intent memory i; + Intent memory i; i.eoa = d.eoa; i.nonce = 0; i.executionData = t.executionData; @@ -246,7 +246,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); @@ -256,7 +256,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); } @@ -284,7 +284,7 @@ contract SimulateExecuteTest is BaseTest { abi.encodeWithSignature("burnGas(uint256,uint256)", t.gasToBurn, t.randomness) ); - ICommon.Intent memory i; + Intent memory i; i.eoa = d.eoa; i.nonce = 0; i.executionData = t.executionData; @@ -306,7 +306,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); @@ -315,7 +315,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/mocks/SendLibMock.sol b/test/mocks/SendLibMock.sol index 3b3decc0..e0ee23be 100644 --- a/test/mocks/SendLibMock.sol +++ b/test/mocks/SendLibMock.sol @@ -7,15 +7,12 @@ import { Packet, MessagingFee } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ISendLib.sol"; -import { - SetConfigParam -} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/IMessageLibManager.sol"; -import { - MessageLibType -} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/IMessageLib.sol"; -import { - PacketV1Codec -} from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/PacketV1Codec.sol"; +import {SetConfigParam} from + "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/IMessageLibManager.sol"; +import {MessageLibType} from + "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/IMessageLib.sol"; +import {PacketV1Codec} from + "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/PacketV1Codec.sol"; contract SendLibMock is ISendLib, Ownable { using PacketV1Codec for bytes; diff --git a/test/utils/TestPlus.sol b/test/utils/TestPlus.sol index c22e1d87..ef7b1e97 100644 --- a/test/utils/TestPlus.sol +++ b/test/utils/TestPlus.sol @@ -568,7 +568,11 @@ contract TestPlus is Brutalizer { /// @dev Truncate the bytes to `n` bytes. /// Returns the result for function chaining. - function _truncateBytes(bytes memory b, uint256 n) internal pure returns (bytes memory result) { + function _truncateBytes(bytes memory b, uint256 n) + internal + pure + returns (bytes memory result) + { /// @solidity memory-safe-assembly assembly { if gt(mload(b), n) { mstore(b, n) } diff --git a/test/utils/interfaces/ISafe.sol b/test/utils/interfaces/ISafe.sol index d9c3d3e1..7606c373 100644 --- a/test/utils/interfaces/ISafe.sol +++ b/test/utils/interfaces/ISafe.sol @@ -47,7 +47,8 @@ interface ISafe4337Module { view returns (bytes32 operationHash); - function executeUserOp(address to, uint256 value, bytes calldata data, uint8 operation) external; + function executeUserOp(address to, uint256 value, bytes calldata data, uint8 operation) + external; function domainSeparator() external view returns (bytes32); } diff --git a/test/utils/mocks/MockCallChecker.sol b/test/utils/mocks/MockCallChecker.sol index 85b70a14..29e80b3d 100644 --- a/test/utils/mocks/MockCallChecker.sol +++ b/test/utils/mocks/MockCallChecker.sol @@ -20,7 +20,9 @@ contract MockCallChecker { view returns (bool) { - return (keyHash == authorizedKeyHash && target == authorizedTarget - && keccak256(data) == keccak256(authorizedData)); + return ( + keyHash == authorizedKeyHash && target == authorizedTarget + && keccak256(data) == keccak256(authorizedData) + ); } } diff --git a/test/utils/mocks/MockERC4337Account.sol b/test/utils/mocks/MockERC4337Account.sol index 303fb046..1666c518 100644 --- a/test/utils/mocks/MockERC4337Account.sol +++ b/test/utils/mocks/MockERC4337Account.sol @@ -4,7 +4,12 @@ pragma solidity ^0.8.4; import {ERC4337} from "solady/accounts/ERC4337.sol"; contract MockERC4337Account is ERC4337 { - function _domainNameAndVersion() internal pure override returns (string memory, string memory) { + function _domainNameAndVersion() + internal + pure + override + returns (string memory, string memory) + { return ("MockERC4337Account", "0.0.1"); } } 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 index 1d7701bf..c0b2ad84 100644 --- a/test/utils/mocks/MockPayerWithSignatureOptimized.sol +++ b/test/utils/mocks/MockPayerWithSignatureOptimized.sol @@ -43,36 +43,38 @@ contract MockPayerWithSignatureOptimized 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. - /// 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. + /// @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 (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); + bytes32 signatureDigest = computeSignatureDigest(intentDigest); - 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/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 {} From 8a0d824ca4658261aa798a99681625c281104613 Mon Sep 17 00:00:00 2001 From: howy <132113803+howydev@users.noreply.github.com> Date: Fri, 26 Sep 2025 12:25:53 -0400 Subject: [PATCH 3/4] feat: consolidate validate and increment nonce call --- snapshots/BenchmarkTest.json | 30 +++++++++++------------ src/IthacaAccount.sol | 9 +++++++ src/Orchestrator.sol | 40 +++++++++++++++++++++++-------- src/interfaces/IIthacaAccount.sol | 5 ++++ test/Orchestrator.t.sol | 2 +- 5 files changed, 60 insertions(+), 26 deletions(-) diff --git a/snapshots/BenchmarkTest.json b/snapshots/BenchmarkTest.json index c323180e..67bca809 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": "118043", - "testERC20Transfer_IthacaAccountWithSpendLimits": "184413", - "testERC20Transfer_IthacaAccount_AppSponsor": "129038", - "testERC20Transfer_IthacaAccount_AppSponsor_ERC20": "134327", - "testERC20Transfer_IthacaAccount_ERC20SelfPay": "118532", + "testERC20Transfer_IthacaAccount": "116837", + "testERC20Transfer_IthacaAccountWithSpendLimits": "183207", + "testERC20Transfer_IthacaAccount_AppSponsor": "127810", + "testERC20Transfer_IthacaAccount_AppSponsor_ERC20": "133099", + "testERC20Transfer_IthacaAccount_ERC20SelfPay": "117326", "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": "6679204", - "testERC20Transfer_batch100_IthacaAccount_AppSponsor": "7333804", - "testERC20Transfer_batch100_IthacaAccount_AppSponsor_ERC20": "7160008", - "testERC20Transfer_batch100_IthacaAccount_ERC20SelfPay": "6500404", + "testERC20Transfer_batch100_IthacaAccount": "6558604", + "testERC20Transfer_batch100_IthacaAccount_AppSponsor": "7211004", + "testERC20Transfer_batch100_IthacaAccount_AppSponsor_ERC20": "7037208", + "testERC20Transfer_batch100_IthacaAccount_ERC20SelfPay": "6379804", "testERC20Transfer_batch100_ZerodevKernel": "12631318", "testERC20Transfer_batch100_ZerodevKernel_ERC20SelfPay": "14149937", "testNativeTransfer_AlchemyModularAccount": "180829", "testNativeTransfer_CoinbaseSmartWallet": "178916", - "testNativeTransfer_IthacaAccount": "119427", - "testNativeTransfer_IthacaAccount_AppSponsor": "130423", - "testNativeTransfer_IthacaAccount_ERC20SelfPay": "127216", + "testNativeTransfer_IthacaAccount": "118221", + "testNativeTransfer_IthacaAccount_AppSponsor": "129195", + "testNativeTransfer_IthacaAccount_ERC20SelfPay": "126010", "testNativeTransfer_Safe4337": "198595", "testNativeTransfer_ZerodevKernel": "208635", "testUniswapV2Swap_AlchemyModularAccount": "238647", "testUniswapV2Swap_CoinbaseSmartWallet": "237451", "testUniswapV2Swap_ERC4337MinimalAccount": "230691", - "testUniswapV2Swap_IthacaAccount": "177131", - "testUniswapV2Swap_IthacaAccount_AppSponsor": "188126", - "testUniswapV2Swap_IthacaAccount_ERC20SelfPay": "182420", + "testUniswapV2Swap_IthacaAccount": "175925", + "testUniswapV2Swap_IthacaAccount_AppSponsor": "186898", + "testUniswapV2Swap_IthacaAccount_ERC20SelfPay": "181214", "testUniswapV2Swap_Safe4337": "257333", "testUniswapV2Swap_ZerodevKernel": "266367" } \ No newline at end of file diff --git a/src/IthacaAccount.sol b/src/IthacaAccount.sol index 1e01a529..7ad4a700 100644 --- a/src/IthacaAccount.sol +++ b/src/IthacaAccount.sol @@ -490,6 +490,15 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { : _hashTypedData(structHash); } + function validateSignatureAndNonce(bytes32 digest, uint256 nonce, bytes calldata signature) + external + override + returns (bool, bytes32) + { + LibNonce.checkAndIncrement(_getAccountStorage().nonceSeqs, nonce); + return unwrapAndValidateSignature(digest, signature); + } + /// @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))`. diff --git a/src/Orchestrator.sol b/src/Orchestrator.sol index b40519d1..4f383494 100644 --- a/src/Orchestrator.sol +++ b/src/Orchestrator.sol @@ -485,7 +485,7 @@ contract Orchestrator is if (nonce >> 240 == MERKLE_VERIFICATION) { // For multi chain intents, we have to verify using merkle sigs. - (isValid, keyHash) = _verifyMerkleSig(digest, eoa, signature); + (isValid, keyHash) = _verifyMerkleSigAndNonce(digest, eoa, signature, nonce); bytes calldata settlerData = _getNextBytes(ptr); // If this is an output intent, then send the digest as the settlementId @@ -498,7 +498,7 @@ contract Orchestrator is ); } } else { - (isValid, keyHash) = _verify(digest, eoa, signature); + (isValid, keyHash) = _verifySignatureAndNonce(digest, eoa, signature, nonce); } if (flags == _SIMULATION_MODE_FLAG) { @@ -508,8 +508,6 @@ contract Orchestrator is if (!isValid) { revert VerificationError(); } - - _checkAndIncrementNonce(eoa, nonce); } // Payment @@ -637,16 +635,17 @@ contract Orchestrator is /// - 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) - { + function _verifyMerkleSigAndNonce( + bytes32 digest, + address eoa, + bytes memory signature, + uint256 nonce + ) internal 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); + (isValid, keyHash) = IIthacaAccount(eoa).validateSignatureAndNonce(root, nonce, rootSig); return (isValid, keyHash); } @@ -755,6 +754,27 @@ contract Orchestrator is } } + /// @dev Calls `validateSignatureAndNonce` on the `eoa`. + function _verifySignatureAndNonce( + bytes32 digest, + address eoa, + bytes calldata sig, + uint256 nonce + ) internal returns (bool isValid, bytes32 keyHash) { + assembly ("memory-safe") { + let m := mload(0x40) + mstore(m, 0x29dd69f3) // `validateSignatureAndNonce(bytes32,uint256,bytes)`. + mstore(add(m, 0x20), digest) + mstore(add(m, 0x40), nonce) + mstore(add(m, 0x60), 0x60) + mstore(add(m, 0x80), sig.length) + calldatacopy(add(m, 0xa0), sig.offset, sig.length) + isValid := call(gas(), eoa, 0, add(m, 0x1c), add(sig.length, 0x84), 0x00, 0x40) + isValid := and(eq(mload(0x00), 1), and(gt(returndatasize(), 0x3f), isValid)) + keyHash := mload(0x20) + } + } + /// @dev Calls `unwrapAndValidateSignature` on the `eoa`. function _verify(bytes32 digest, address eoa, bytes calldata sig) internal diff --git a/src/interfaces/IIthacaAccount.sol b/src/interfaces/IIthacaAccount.sol index 9056db7c..a744217d 100644 --- a/src/interfaces/IIthacaAccount.sol +++ b/src/interfaces/IIthacaAccount.sol @@ -43,4 +43,9 @@ interface IIthacaAccount is ICommon { /// @dev Check and increment the nonce. function checkAndIncrementNonce(uint256 nonce) external payable; + + /// @dev Single call combining `unwrapAndValidateSignature` and `checkAndIncrementNonce`. + function validateSignatureAndNonce(bytes32 digest, uint256 nonce, bytes calldata signature) + external + returns (bool isValid, bytes32 keyHash); } diff --git a/test/Orchestrator.t.sol b/test/Orchestrator.t.sol index a024e3e5..70c554e0 100644 --- a/test/Orchestrator.t.sol +++ b/test/Orchestrator.t.sol @@ -516,7 +516,7 @@ 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(encodeIntent(u)), bytes4(keccak256("VerificationError()"))); } else { assertEq(oc.execute(encodeIntent(u)), 0); } From 050751a00a63e55f301f8be37f3fb6117db704fa Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 26 Sep 2025 16:29:12 +0000 Subject: [PATCH 4/4] chore: bump contract versions due to bytecode changes - Contracts updated: IthacaAccount,Orchestrator,SimpleFunder --- src/IthacaAccount.sol | 2 +- src/Orchestrator.sol | 2 +- src/SimpleFunder.sol | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/IthacaAccount.sol b/src/IthacaAccount.sol index 7ad4a700..064b3576 100644 --- a/src/IthacaAccount.sol +++ b/src/IthacaAccount.sol @@ -749,6 +749,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/src/Orchestrator.sol b/src/Orchestrator.sol index 4f383494..528ec6d0 100644 --- a/src/Orchestrator.sol +++ b/src/Orchestrator.sol @@ -899,7 +899,7 @@ contract Orchestrator is 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 959723d1..107e5b84 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"; } ////////////////////////////////////////////////////////////////////////