Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions snapshots/BenchmarkTest.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
"testERC20Transfer_ERC4337MinimalAccount": "171906",
"testERC20Transfer_ERC4337MinimalAccount_AppSponsor": "168893",
"testERC20Transfer_ERC4337MinimalAccount_ERC20SelfPay": "217488",
"testERC20Transfer_IthacaAccount": "132464",
"testERC20Transfer_IthacaAccountWithSpendLimits": "193338",
"testERC20Transfer_IthacaAccount_AppSponsor": "143908",
"testERC20Transfer_IthacaAccount_ERC20SelfPay": "150117",
"testERC20Transfer_IthacaAccount": "130894",
"testERC20Transfer_IthacaAccountWithSpendLimits": "191803",
"testERC20Transfer_IthacaAccount_AppSponsor": "142380",
"testERC20Transfer_IthacaAccount_ERC20SelfPay": "148559",
"testERC20Transfer_Safe4337": "197515",
"testERC20Transfer_Safe4337_AppSponsor": "191679",
"testERC20Transfer_Safe4337_ERC20SelfPay": "238658",
Expand All @@ -28,24 +28,24 @@
"testERC20Transfer_ZerodevKernel_ERC20SelfPay": "252683",
"testERC20Transfer_batch100_AlchemyModularAccount": "10104466",
"testERC20Transfer_batch100_AlchemyModularAccount_ERC20SelfPay": "11635798",
"testERC20Transfer_batch100_IthacaAccount": "7746918",
"testERC20Transfer_batch100_IthacaAccount_AppSponsor": "8445974",
"testERC20Transfer_batch100_IthacaAccount_ERC20SelfPay": "7591594",
"testERC20Transfer_batch100_IthacaAccount": "7591490",
"testERC20Transfer_batch100_IthacaAccount_AppSponsor": "8291638",
"testERC20Transfer_batch100_IthacaAccount_ERC20SelfPay": "7436142",
"testERC20Transfer_batch100_ZerodevKernel": "12626718",
"testERC20Transfer_batch100_ZerodevKernel_ERC20SelfPay": "14176437",
"testNativeTransfer_AlchemyModularAccount": "180829",
"testNativeTransfer_CoinbaseSmartWallet": "178916",
"testNativeTransfer_IthacaAccount": "133866",
"testNativeTransfer_IthacaAccount_AppSponsor": "145341",
"testNativeTransfer_IthacaAccount_ERC20SelfPay": "158819",
"testNativeTransfer_IthacaAccount": "132319",
"testNativeTransfer_IthacaAccount_AppSponsor": "143801",
"testNativeTransfer_IthacaAccount_ERC20SelfPay": "157284",
"testNativeTransfer_Safe4337": "198595",
"testNativeTransfer_ZerodevKernel": "208635",
"testUniswapV2Swap_AlchemyModularAccount": "238767",
"testUniswapV2Swap_CoinbaseSmartWallet": "237571",
"testUniswapV2Swap_ERC4337MinimalAccount": "231254",
"testUniswapV2Swap_IthacaAccount": "191774",
"testUniswapV2Swap_IthacaAccount_AppSponsor": "203194",
"testUniswapV2Swap_IthacaAccount_ERC20SelfPay": "214227",
"testUniswapV2Swap_IthacaAccount": "190215",
"testUniswapV2Swap_IthacaAccount_AppSponsor": "201652",
"testUniswapV2Swap_IthacaAccount_ERC20SelfPay": "212680",
"testUniswapV2Swap_Safe4337": "257453",
"testUniswapV2Swap_ZerodevKernel": "266487"
}
19 changes: 14 additions & 5 deletions src/IthacaAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -478,11 +478,20 @@ 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);
}

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`.
Expand Down
103 changes: 59 additions & 44 deletions src/Orchestrator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ contract Orchestrator is
/// This constant is a pun for "chain ID 0".
uint16 public constant MULTICHAIN_NONCE_PREFIX = 0xc1d0;

/// @dev Nonce prefix to signal that the payload should use merkle verification.
/// This constant is "mv" in hex.
uint16 public constant MERKLE_VERIFICATION = 0x6D76;

/// @dev For ensuring that the remaining gas is sufficient for a self-call with
/// overhead for cleaning up after the self-call. This also has an added benefit
/// of preventing the censorship vector of calling `execute` in a very deep call-stack.
Expand Down Expand Up @@ -364,13 +368,11 @@ contract Orchestrator is
// 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();
}
}

Expand Down Expand Up @@ -484,9 +486,10 @@ contract Orchestrator is

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);
(isValid, keyHash) = _verifyMerkleSigAndNonce(digest, eoa, i.signature, nonce);

// If this is an output intent, then send the digest as the settlementId
// on all input chains.
Expand All @@ -495,7 +498,7 @@ contract Orchestrator is
ISettler(i.settler).send(digest, i.settlerContext);
}
} else {
(isValid, keyHash) = _verify(digest, eoa, i.signature);
(isValid, keyHash) = _verifySignatureAndNonce(digest, eoa, i.signature, nonce);
}

if (flags == _SIMULATION_MODE_FLAG) {
Expand All @@ -504,8 +507,6 @@ contract Orchestrator is

if (!isValid) revert VerificationError();

_checkAndIncrementNonce(eoa, nonce);

// Payment
// If `_pay` fails, just revert.
// Off-chain simulation of `_pay` should suffice,
Expand Down Expand Up @@ -609,18 +610,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 calldata 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);

return (isValid, keyHash);
return IIthacaAccount(eoa).validateSignatureAndNonce(root, nonce, rootSig);
}

return (false, bytes32(0));
Expand Down Expand Up @@ -719,6 +719,26 @@ contract Orchestrator is
}
}

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
Expand Down Expand Up @@ -760,40 +780,35 @@ contract Orchestrator is
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`.
Expand Down
3 changes: 0 additions & 3 deletions src/interfaces/ICommon.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions src/interfaces/IIthacaAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,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);
}
16 changes: 6 additions & 10 deletions test/Account.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,10 @@ contract AccountTest is BaseTest {
assertTrue(d.d.isValidSignature(digest, sig) == 0xFFFFFFFF);

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));
Expand Down Expand Up @@ -341,7 +338,7 @@ contract AccountTest is BaseTest {
d.d.execute(_ERC7821_BATCH_EXECUTION_MODE, executionData);

// Check that intent fails
Orchestrator.Intent memory u;
ICommon.Intent memory u;
u.eoa = d.eoa;
u.nonce = d.d.getNonce(0);
u.combinedGas = 1000000;
Expand Down Expand Up @@ -444,7 +441,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);
Expand All @@ -468,12 +465,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();
Expand Down
Loading
Loading