Skip to content
Draft
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
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,15 @@ We believe that unstoppable crypto-powered accounts should be excellent througho
## Benchmarks

![Benchmarks](docs/benchmarks.jpeg)
Gas benchmark implementations are in the [test repository](test/Benchmark.t.sol). We currently benchmark against leading ERC-4337 accounts. We use `forge snapshot --isolate` to generate the benchmarks.

Gas benchmark implementations are in the [test repository](test/Benchmark.t.sol). We currently benchmark against leading ERC-4337 accounts. To generate the benchmarks, use `forge snapshot --isolate`.
Our benchmarks measure the cost of common operations - an ERC20 token transfer, a native token transfer, and a Uni V2 swap. We bench across 3 different payment modes, self-paying in native tokens, paying in ERC-20 tokens, and using a paymaster.

For each of the above operations, we benchmark them in two ways:
1. Maximum cost - We use a fresh account, and benchmark an unbatched user operation or intent transaction.
2. Average cost - We use an account that's sent a user operation or intent previously, and the uo/intent is relayed in a batch of 10. The benchmarks calculate total cost, to get unit cost you'd need to divide by 10.

Across all benchmarks, the Porto smart account is the cheapest account.

## Security
Contracts were audited in a 2 week engagement by @MiloTruck @rholterhus @kadenzipfel
Expand Down
563 changes: 563 additions & 0 deletions output.txt

Large diffs are not rendered by default.

98 changes: 56 additions & 42 deletions snapshots/BenchmarkTest.json
Original file line number Diff line number Diff line change
@@ -1,53 +1,67 @@
{
"testERC20Transfer_AlchemyModularAccount": "179494",
"testERC20Transfer_AlchemyModularAccount_AppSponsor": "176436",
"testERC20Transfer_AlchemyModularAccount_ERC20SelfPay": "207779",
"testERC20Transfer_Batch100_AlchemyModularAccount_AppSponsor": "8897167",
"testERC20Transfer_Batch100_CoinbaseSmartWallet": "9952203",
"testERC20Transfer_Batch100_CoinbaseSmartWallet_AppSponsor": "8787327",
"testERC20Transfer_Batch100_CoinbaseSmartWallet_ERC20SelfPay": "11335605",
"testERC20Transfer_Batch100_Safe4337": "11685484",
"testERC20Transfer_Batch100_Safe4337_AppSponsor": "10198375",
"testERC20Transfer_Batch100_Safe4337_ERC20SelfPay": "12757523",
"testERC20Transfer_Batch100_ZerodevKernel_AppSponsor": "11427583",
"testERC20Transfer_CoinbaseSmartWallet": "177855",
"testERC20Transfer_CoinbaseSmartWallet_AppSponsor": "175259",
"testERC20Transfer_CoinbaseSmartWallet_ERC20SelfPay": "204919",
"testERC20Transfer_ERC4337MinimalAccount": "171509",
"testERC20Transfer_ERC4337MinimalAccount_AppSponsor": "168500",
"testERC20Transfer_ERC4337MinimalAccount_ERC20SelfPay": "199831",
"testERC20Transfer_IthacaAccount": "128100",
"testERC20Transfer_IthacaAccountWithSpendLimits": "193563",
"testERC20Transfer_IthacaAccount_AppSponsor": "138636",
"testERC20Transfer_IthacaAccount_AppSponsor_ERC20": "143937",
"testERC20Transfer_IthacaAccount_ERC20SelfPay": "128589",
"testERC20Transfer_Safe4337": "197561",
"testERC20Transfer_Safe4337_AppSponsor": "191725",
"testERC20Transfer_Safe4337_ERC20SelfPay": "221464",
"testERC20Transfer_ZerodevKernel": "207117",
"testERC20Transfer_ZerodevKernel_AppSponsor": "204120",
"testERC20Transfer_ZerodevKernel_ERC20SelfPay": "235489",
"testERC20Transfer_batch100_AlchemyModularAccount": "10109066",
"testERC20Transfer_batch100_AlchemyModularAccount_ERC20SelfPay": "11609298",
"testERC20Transfer_batch100_IthacaAccount": "7535892",
"testERC20Transfer_batch100_IthacaAccount_AppSponsor": "8144196",
"testERC20Transfer_batch100_IthacaAccount_AppSponsor_ERC20": "7970208",
"testERC20Transfer_batch100_IthacaAccount_ERC20SelfPay": "7357152",
"testERC20Transfer_batch100_ZerodevKernel": "12631318",
"testERC20Transfer_batch100_ZerodevKernel_ERC20SelfPay": "14149937",
"testERC20Transfer_AverageCost_AlchemyModularAccount": "701819",
"testERC20Transfer_AverageCost_AlchemyModularAccount_AppSponsor": "758729",
"testERC20Transfer_AverageCost_AlchemyModularAccount_ERC20SelfPay": "1029029",
"testERC20Transfer_AverageCost_CoinbaseSmartWallet": "685988",
"testERC20Transfer_AverageCost_CoinbaseSmartWallet_AppSponsor": "747597",
"testERC20Transfer_AverageCost_CoinbaseSmartWallet_ERC20SelfPay": "1001387",
"testERC20Transfer_AverageCost_IthacaAccount": "528758",
"testERC20Transfer_AverageCost_IthacaAccount_AppSponsor": "821937",
"testERC20Transfer_AverageCost_IthacaAccount_AppSponsor_ERC20": "810951",
"testERC20Transfer_AverageCost_IthacaAccount_ERC20SelfPay": "512924",
"testERC20Transfer_AverageCost_MockMinimalAccount": "481910",
"testERC20Transfer_AverageCost_MockMinimalAccount_AppSponsor": "785469",
"testERC20Transfer_AverageCost_MockMinimalAccount_AppSponsor_ERC20": "774483",
"testERC20Transfer_AverageCost_MockMinimalAccount_ERC20SelfPay": "467958",
"testERC20Transfer_AverageCost_Safe4337": "860874",
"testERC20Transfer_AverageCost_Safe4337_AppSponsor": "890102",
"testERC20Transfer_AverageCost_Safe4337_ERC20SelfPay": "1144272",
"testERC20Transfer_AverageCost_ZerodevKernel": "955637",
"testERC20Transfer_AverageCost_ZerodevKernel_AppSponsor": "1013218",
"testERC20Transfer_AverageCost_ZerodevKernel_ERC20SelfPay": "1283860",
"testERC20Transfer_MaximumCost_AlchemyModularAccount": "179494",
"testERC20Transfer_MaximumCost_AlchemyModularAccount_AppSponsor": "176436",
"testERC20Transfer_MaximumCost_AlchemyModularAccount_ERC20SelfPay": "207779",
"testERC20Transfer_MaximumCost_CoinbaseSmartWallet": "177855",
"testERC20Transfer_MaximumCost_CoinbaseSmartWallet_AppSponsor": "175259",
"testERC20Transfer_MaximumCost_CoinbaseSmartWallet_ERC20SelfPay": "204919",
"testERC20Transfer_MaximumCost_ERC4337MinimalAccount": "171509",
"testERC20Transfer_MaximumCost_ERC4337MinimalAccount_AppSponsor": "168500",
"testERC20Transfer_MaximumCost_ERC4337MinimalAccount_ERC20SelfPay": "199831",
"testERC20Transfer_MaximumCost_IthacaAccount": "118043",
"testERC20Transfer_MaximumCost_IthacaAccountWithSpendLimits": "184425",
"testERC20Transfer_MaximumCost_IthacaAccount_AppSponsor": "151415",
"testERC20Transfer_MaximumCost_IthacaAccount_AppSponsor_ERC20": "156704",
"testERC20Transfer_MaximumCost_IthacaAccount_ERC20SelfPay": "118532",
"testERC20Transfer_MaximumCost_MockMinimalAccount": "108857",
"testERC20Transfer_MaximumCost_MockMinimalAccount_AppSponsor": "143255",
"testERC20Transfer_MaximumCost_MockMinimalAccount_AppSponsor_ERC20": "148556",
"testERC20Transfer_MaximumCost_MockMinimalAccount_ERC20SelfPay": "109521",
"testERC20Transfer_MaximumCost_Safe4337": "197561",
"testERC20Transfer_MaximumCost_Safe4337_AppSponsor": "191725",
"testERC20Transfer_MaximumCost_Safe4337_ERC20SelfPay": "221464",
"testERC20Transfer_MaximumCost_ZerodevKernel": "207117",
"testERC20Transfer_MaximumCost_ZerodevKernel_AppSponsor": "204120",
"testERC20Transfer_MaximumCost_ZerodevKernel_ERC20SelfPay": "235489",
"testNativeTransfer_AlchemyModularAccount": "180829",
"testNativeTransfer_CoinbaseSmartWallet": "178916",
"testNativeTransfer_IthacaAccount": "129456",
"testNativeTransfer_IthacaAccount_AppSponsor": "140023",
"testNativeTransfer_IthacaAccount_ERC20SelfPay": "137245",
"testNativeTransfer_IthacaAccount": "119427",
"testNativeTransfer_IthacaAccount_AppSponsor": "152800",
"testNativeTransfer_IthacaAccount_ERC20SelfPay": "127216",
"testNativeTransfer_MockMinimalAccount": "110030",
"testNativeTransfer_MockMinimalAccount_AppSponsor": "144441",
"testNativeTransfer_MockMinimalAccount_ERC20SelfPay": "118006",
"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": "210503",
"testUniswapV2Swap_IthacaAccount_ERC20SelfPay": "182420",
"testUniswapV2Swap_MockMinimalAccount": "167917",
"testUniswapV2Swap_MockMinimalAccount_AppSponsor": "202327",
"testUniswapV2Swap_MockMinimalAccount_ERC20SelfPay": "173393",
"testUniswapV2Swap_Safe4337": "257333",
"testUniswapV2Swap_ZerodevKernel": "266367"
}
5 changes: 3 additions & 2 deletions src/Escrow.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
16 changes: 11 additions & 5 deletions src/GuardedExecutor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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);
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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);
Expand Down
69 changes: 31 additions & 38 deletions src/IthacaAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -478,11 +483,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`.
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
Expand Down
13 changes: 8 additions & 5 deletions src/LayerZeroSettler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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))));
}
Expand Down Expand Up @@ -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));
Expand Down
Loading
Loading