# EIP-7702 Set EOA account code # Overview EIP-7702 gives superpowers to EOAs. Specifically, it allows any EOA to set its code based on any existing smart contract. To do so, an EOA owner would sign an *authorization* that could then be submitted by anyone as part of the new transaction type. The code will be valid until replaced by another authorization. The authorization could be given for a single chain, or all chains at once. This setup allows an EOA to mimic a smart contract account, particularly allowing transaction bundling, gas sponsorships, and custom permissioning schemes. ## Motivation ### Flexible EIP-7702 offers some guardrails yet it is very flexible. Users can provide both single-chain and cross-chain authorizations, as well as choose what proxy to use. While most wallets would probably not allow the most flexible behavior, it still opens up a way for different vendors to experiment and innovate. ### Compatible with EIP-4337 It is easy to use EIP-7702 with EIP-4337, so most of the infrastructure (e.g. paymasters, bundlers, RPC endpoints) would just work. ### Compatible with existing smart accounts As EIP-7702 allows an EOA to set its code directly and supports writing to storage, it should be possible with little to no effort to use existing smart account implementations. ## Specification The EIP introduces a new transaction, “set code transaction”, where the `TransactionType` is `0x04` and the `TransactionPayload` is the RLP serialization of the following: ``` rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, value, data, access_list, authorization_list, signature_y_parity, signature_r, signature_s]) authorization_list = [[chain_id, address, nonce, y_parity, r, s], ...] ``` The fields `chain_id`, `nonce`, `max_priority_fee_per_gas`, `max_fee_per_gas`, `gas_limit`, `destination`, `value`, `data`, and `access_list` of the outer transaction follow the same semantics as EIP-4844. *Note, this means a null destination is not valid*. The `authorization_list` is a list of tuples that store the address to code which the signer desires to execute in the context of their EOA. The transaction is considered invalid if the length of `authorization_list` is zero. The `ReceiptPayload` for this transaction is `rlp([status, cumulative_transaction_gas_used, logs_bloom, logs])`. ### Behavior At the start of executing the transaction, for each `[chain_id, address, nonce, y_parity, r, s]` tuple: 1. Verify the chain id is either 0 or the chain’s current ID. 2. Verify the `nonce` is less than `2**64 - 1`. 3. `authority = ecrecover(keccak(0x05 || rlp([chain_id, address, nonce])), y_parity, r, s)` * `s` value must be less or equal than `secp256k1n/2`, as specified in EIP-2. 4. Add `authority` to `accessed_addresses` (as defined in EIP-2929.) 5. Verify the code of `authority` is either empty or already delegated. 6. Verify the nonce of `authority` is equal to `nonce`. In case `authority` does not exist in the trie, verify that `nonce` is equal to `0`. 7. Add `PER_EMPTY_ACCOUNT_COST - PER_AUTH_BASE_COST` gas to the global refund counter if `authority` exists in the trie. 8. Set the code of `authority` to be `0xef0100 || address`. This is a delegation designation. * As a special case, if `address` is `0x0000000000000000000000000000000000000000` do not write the designation. Clear the accounts code and reset the account’s code hash to the empty hash `0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470`. 9. Increase the nonce of `authority` by one. If any of the above steps fail, immediately stop processing that tuple and continue to the next tuple in the list. It will in the case of multiple tuples for the same authority, set the code using the address in the last valid occurrence. Note that the signer of an authorization tuple may be different than `tx.origin` of the transaction. If transaction execution results in failure (any exceptional condition or code reverting), setting delegation designations is *not* rolled back. # Reference * EIP: https://eips.ethereum.org/EIPS/eip-7702 * Discussions: https://ethereum-magicians.org/t/eip-set-eoa-account-code-for-one-transaction/19923 * Proxy pattern: https://gist.github.com/lightclient/7742e84fde4962f32928c6177eda7523 * Best practices: https://hackmd.io/@rimeissner/eip7702-best-practices # Examples ## Basic authorization This will set the EOA code to the [`Simple7702Account`](https://github.com/eth-infinitism/account-abstraction/blob/develop/contracts/accounts/Simple7702Account.sol) implementation. :::code-group ```ts twoslash [example.ts] import { walletClient } from "./client.js"; const accountImplementation = "0x4Cd241E8d1510e30b2076397afc7508Ae59C66c9"; const authorization = await walletClient.signAuthorization({ contractAddress: accountImplementation, executor: "self", }); const hash = await walletClient.sendTransaction({ authorizationList: [authorization], data: "0x", to: walletClient.account.address, }); console.log(`Delegated at tx ${hash}`); ``` ```ts twoslash [client.ts] filename="client.ts" import { http, type Account, type Chain, type Transport, type WalletClient, createWalletClient, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { sepolia } from "viem/chains"; const privateKey = "0x…"; const account = privateKeyToAccount(privateKey); console.log(`Account address = ${account.address}`); export const walletClient = createWalletClient({ account, chain: sepolia, transport: http(), }) as WalletClient; ``` ::: ## Transaction batching Once the EOA code is delegated to a smart account implementation, you can start making batched transactions. :::code-group ```ts twoslash [example.ts] import { zeroAddress } from "viem"; import accountAbi from "./abi.js"; import { account, walletClient } from "./client.js"; const hash = await walletClient.writeContract({ abi: accountAbi, address: account.address, functionName: "executeBatch", args: [ [ { target: zeroAddress, value: 1n, data: "0x", }, { target: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", value: 0n, data: "0xdeadbeef", }, ], ], }); console.log(`Transaction hash ${hash}`); ``` ```ts twoslash [client.ts] filename="client.ts" import { http, type Account, type Chain, type Transport, type WalletClient, createWalletClient, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { sepolia } from "viem/chains"; const privateKey = "0x…"; const account = privateKeyToAccount(privateKey); console.log(`Account address = ${account.address}`); const walletClient = createWalletClient({ account, chain: sepolia, transport: http(), }) as WalletClient; export { account, walletClient }; ``` ```ts twoslash [abi.ts] filename="abi.ts" const abi = [ { inputs: [], name: "ECDSAInvalidSignature", type: "error", }, { inputs: [ { internalType: "uint256", name: "length", type: "uint256", }, ], name: "ECDSAInvalidSignatureLength", type: "error", }, { inputs: [ { internalType: "bytes32", name: "s", type: "bytes32", }, ], name: "ECDSAInvalidSignatureS", type: "error", }, { inputs: [ { internalType: "uint256", name: "index", type: "uint256", }, { internalType: "bytes", name: "error", type: "bytes", }, ], name: "ExecuteError", type: "error", }, { stateMutability: "payable", type: "fallback", }, { inputs: [], name: "entryPoint", outputs: [ { internalType: "contract IEntryPoint", name: "", type: "address", }, ], stateMutability: "pure", type: "function", }, { inputs: [ { internalType: "address", name: "target", type: "address", }, { internalType: "uint256", name: "value", type: "uint256", }, { internalType: "bytes", name: "data", type: "bytes", }, ], name: "execute", outputs: [], stateMutability: "nonpayable", type: "function", }, { inputs: [ { components: [ { internalType: "address", name: "target", type: "address", }, { internalType: "uint256", name: "value", type: "uint256", }, { internalType: "bytes", name: "data", type: "bytes", }, ], internalType: "struct BaseAccount.Call[]", name: "calls", type: "tuple[]", }, ], name: "executeBatch", outputs: [], stateMutability: "nonpayable", type: "function", }, { inputs: [], name: "getNonce", outputs: [ { internalType: "uint256", name: "", type: "uint256", }, ], stateMutability: "view", type: "function", }, { inputs: [ { internalType: "bytes32", name: "hash", type: "bytes32", }, { internalType: "bytes", name: "signature", type: "bytes", }, ], name: "isValidSignature", outputs: [ { internalType: "bytes4", name: "magicValue", type: "bytes4", }, ], stateMutability: "view", type: "function", }, { inputs: [ { internalType: "address", name: "", type: "address", }, { internalType: "address", name: "", type: "address", }, { internalType: "uint256[]", name: "", type: "uint256[]", }, { internalType: "uint256[]", name: "", type: "uint256[]", }, { internalType: "bytes", name: "", type: "bytes", }, ], name: "onERC1155BatchReceived", outputs: [ { internalType: "bytes4", name: "", type: "bytes4", }, ], stateMutability: "nonpayable", type: "function", }, { inputs: [ { internalType: "address", name: "", type: "address", }, { internalType: "address", name: "", type: "address", }, { internalType: "uint256", name: "", type: "uint256", }, { internalType: "uint256", name: "", type: "uint256", }, { internalType: "bytes", name: "", type: "bytes", }, ], name: "onERC1155Received", outputs: [ { internalType: "bytes4", name: "", type: "bytes4", }, ], stateMutability: "nonpayable", type: "function", }, { inputs: [ { internalType: "address", name: "", type: "address", }, { internalType: "address", name: "", type: "address", }, { internalType: "uint256", name: "", type: "uint256", }, { internalType: "bytes", name: "", type: "bytes", }, ], name: "onERC721Received", outputs: [ { internalType: "bytes4", name: "", type: "bytes4", }, ], stateMutability: "nonpayable", type: "function", }, { inputs: [ { internalType: "bytes4", name: "id", type: "bytes4", }, ], name: "supportsInterface", outputs: [ { internalType: "bool", name: "", type: "bool", }, ], stateMutability: "pure", type: "function", }, { inputs: [ { components: [ { internalType: "address", name: "sender", type: "address", }, { internalType: "uint256", name: "nonce", type: "uint256", }, { internalType: "bytes", name: "initCode", type: "bytes", }, { internalType: "bytes", name: "callData", type: "bytes", }, { internalType: "bytes32", name: "accountGasLimits", type: "bytes32", }, { internalType: "uint256", name: "preVerificationGas", type: "uint256", }, { internalType: "bytes32", name: "gasFees", type: "bytes32", }, { internalType: "bytes", name: "paymasterAndData", type: "bytes", }, { internalType: "bytes", name: "signature", type: "bytes", }, ], internalType: "struct PackedUserOperation", name: "userOp", type: "tuple", }, { internalType: "bytes32", name: "userOpHash", type: "bytes32", }, { internalType: "uint256", name: "missingAccountFunds", type: "uint256", }, ], name: "validateUserOp", outputs: [ { internalType: "uint256", name: "validationData", type: "uint256", }, ], stateMutability: "nonpayable", type: "function", }, { stateMutability: "payable", type: "receive", }, ] as const; export default abi; ``` ::: ## Sending a user operation Signs and sends a UserOp using the ERC-4337 flow. Assumes the account is already initialized, and the EOA is the owner of the account. This way, you can also sponsor the transactions by using a paymaster. Note that the process is exactly the same as when using a regular smart account. To minimize the code surface, you can use a convenience library like [permissionless.js](https://github.com/pimlicolabs/permissionless.js). :::code-group ```ts twoslash [example.ts] // @noErrors // Vocs only resolves code-group imports one level deep, so `abi.ts` (imported // by `account.ts`, not by this file) is missing from the checked snippet. import { zeroAddress } from "viem"; import { smartAccount } from "./account.js"; import { bundlerClient } from "./client.js"; const userOpHash = await bundlerClient.sendUserOperation({ account: smartAccount, calls: [ { to: zeroAddress, value: 1n, }, { to: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", data: "0xdeadbeef", }, ], }); console.log(`User operation sent ${userOpHash}`); ``` ```ts twoslash [account.ts] filename="account.ts" import { decodeFunctionData, encodeFunctionData } from "viem"; import { entryPoint08Abi, entryPoint08Address, getUserOperationHash, toSmartAccount, } from "viem/account-abstraction"; import { privateKeyToAccount } from "viem/accounts"; import accountAbi from "./abi.js"; import { chain, publicClient } from "./client.js"; const ECDSA_MOCK_SIGNATURE = "0x81d4b4981670cb18f99f0b4a66446df1bf5b204d24cfcb659bf38ba27a4359b5711649ec2423c5e1247245eba2964679b6a1dbb85c992ae40b9b00c6935b02ff1b"; const privateKey = '0x…'; const account = privateKeyToAccount(privateKey); console.log(`Account address = ${account.address}`); const smartAccount = await toSmartAccount({ client: publicClient, entryPoint: { abi: entryPoint08Abi, address: entryPoint08Address, version: "0.8", }, async decodeCalls(data) { const calls = decodeFunctionData({ abi: accountAbi, data, }); if (calls.functionName === "execute") { return [ { to: calls.args[0], value: calls.args[1], data: calls.args[2], }, ]; } if (calls.functionName === "executeBatch") { return calls.args[0].map((arg) => ({ to: arg.target, value: arg.value, data: arg.data, })); } throw new Error(`Unable to decode data: ${data}`); }, async encodeCalls(calls) { if (calls.length === 1) { const call = calls[0]; return encodeFunctionData({ abi: accountAbi, functionName: "execute", args: [call.to, call.value || 0n, call.data || "0x"], }); } return encodeFunctionData({ abi: accountAbi, functionName: "executeBatch", args: [ calls.map((call) => ({ target: call.to, value: call.value || 0n, data: call.data || "0x", })), ], }); }, async getAddress() { return account.address; }, async getFactoryArgs() { throw new Error("Factory arguments not available"); }, async getNonce(args) { const nonce = await publicClient.readContract({ address: entryPoint08Address, abi: entryPoint08Abi, functionName: "getNonce", args: [account.address, args?.key || 0n], }); return nonce; }, async getStubSignature() { return ECDSA_MOCK_SIGNATURE; }, async signMessage(message) { return account.signMessage(message); }, async signTypedData(typedData) { return account.signTypedData(typedData); }, async signUserOperation(userOperation) { const hash = getUserOperationHash({ chainId: chain.id, entryPointAddress: entryPoint08Address, entryPointVersion: "0.8", userOperation: { ...userOperation, sender: account.address, }, }); return account.sign({ hash, }); }, }); export { smartAccount }; ``` ```ts twoslash [client.ts] filename="client.ts" import { http, createPublicClient } from "viem"; import { createBundlerClient } from "viem/account-abstraction"; import { sepolia } from "viem/chains"; const chain = sepolia; const publicClient = createPublicClient({ chain, transport: http(), }); const bundlerClient = createBundlerClient({ client: publicClient, transport: http(`https://public.pimlico.io/v2/${chain.id}/rpc`), }); export { chain, publicClient, bundlerClient }; ``` ```ts twoslash [abi.ts] filename="abi.ts" const abi = [ { inputs: [], name: "ECDSAInvalidSignature", type: "error", }, { inputs: [ { internalType: "uint256", name: "length", type: "uint256", }, ], name: "ECDSAInvalidSignatureLength", type: "error", }, { inputs: [ { internalType: "bytes32", name: "s", type: "bytes32", }, ], name: "ECDSAInvalidSignatureS", type: "error", }, { inputs: [ { internalType: "uint256", name: "index", type: "uint256", }, { internalType: "bytes", name: "error", type: "bytes", }, ], name: "ExecuteError", type: "error", }, { stateMutability: "payable", type: "fallback", }, { inputs: [], name: "entryPoint", outputs: [ { internalType: "contract IEntryPoint", name: "", type: "address", }, ], stateMutability: "pure", type: "function", }, { inputs: [ { internalType: "address", name: "target", type: "address", }, { internalType: "uint256", name: "value", type: "uint256", }, { internalType: "bytes", name: "data", type: "bytes", }, ], name: "execute", outputs: [], stateMutability: "nonpayable", type: "function", }, { inputs: [ { components: [ { internalType: "address", name: "target", type: "address", }, { internalType: "uint256", name: "value", type: "uint256", }, { internalType: "bytes", name: "data", type: "bytes", }, ], internalType: "struct BaseAccount.Call[]", name: "calls", type: "tuple[]", }, ], name: "executeBatch", outputs: [], stateMutability: "nonpayable", type: "function", }, { inputs: [], name: "getNonce", outputs: [ { internalType: "uint256", name: "", type: "uint256", }, ], stateMutability: "view", type: "function", }, { inputs: [ { internalType: "bytes32", name: "hash", type: "bytes32", }, { internalType: "bytes", name: "signature", type: "bytes", }, ], name: "isValidSignature", outputs: [ { internalType: "bytes4", name: "magicValue", type: "bytes4", }, ], stateMutability: "view", type: "function", }, { inputs: [ { internalType: "address", name: "", type: "address", }, { internalType: "address", name: "", type: "address", }, { internalType: "uint256[]", name: "", type: "uint256[]", }, { internalType: "uint256[]", name: "", type: "uint256[]", }, { internalType: "bytes", name: "", type: "bytes", }, ], name: "onERC1155BatchReceived", outputs: [ { internalType: "bytes4", name: "", type: "bytes4", }, ], stateMutability: "nonpayable", type: "function", }, { inputs: [ { internalType: "address", name: "", type: "address", }, { internalType: "address", name: "", type: "address", }, { internalType: "uint256", name: "", type: "uint256", }, { internalType: "uint256", name: "", type: "uint256", }, { internalType: "bytes", name: "", type: "bytes", }, ], name: "onERC1155Received", outputs: [ { internalType: "bytes4", name: "", type: "bytes4", }, ], stateMutability: "nonpayable", type: "function", }, { inputs: [ { internalType: "address", name: "", type: "address", }, { internalType: "address", name: "", type: "address", }, { internalType: "uint256", name: "", type: "uint256", }, { internalType: "bytes", name: "", type: "bytes", }, ], name: "onERC721Received", outputs: [ { internalType: "bytes4", name: "", type: "bytes4", }, ], stateMutability: "nonpayable", type: "function", }, { inputs: [ { internalType: "bytes4", name: "id", type: "bytes4", }, ], name: "supportsInterface", outputs: [ { internalType: "bool", name: "", type: "bool", }, ], stateMutability: "pure", type: "function", }, { inputs: [ { components: [ { internalType: "address", name: "sender", type: "address", }, { internalType: "uint256", name: "nonce", type: "uint256", }, { internalType: "bytes", name: "initCode", type: "bytes", }, { internalType: "bytes", name: "callData", type: "bytes", }, { internalType: "bytes32", name: "accountGasLimits", type: "bytes32", }, { internalType: "uint256", name: "preVerificationGas", type: "uint256", }, { internalType: "bytes32", name: "gasFees", type: "bytes32", }, { internalType: "bytes", name: "paymasterAndData", type: "bytes", }, { internalType: "bytes", name: "signature", type: "bytes", }, ], internalType: "struct PackedUserOperation", name: "userOp", type: "tuple", }, { internalType: "bytes32", name: "userOpHash", type: "bytes32", }, { internalType: "uint256", name: "missingAccountFunds", type: "uint256", }, ], name: "validateUserOp", outputs: [ { internalType: "uint256", name: "validationData", type: "uint256", }, ], stateMutability: "nonpayable", type: "function", }, { stateMutability: "payable", type: "receive", }, ] as const; export default abi; ``` ::: # Ecosystem ## Tooling & Infra * [Viem](https://viem.sh) * [Porto](https://github.com/ithacaxyz/porto) * [Pimlico](https://docs.pimlico.io/guides/eip7702/demo) * [ERC7579 account](https://github.com/erc7579/erc7579-implementation) * [Ithaca account](https://github.com/ithacaxyz/account) * [ERC6900 Reference implementation](https://github.com/erc6900/reference-implementation) * [Alchemy Modular Account](https://github.com/alchemyplatform/modular-account) * [ZeroDev Kernel](https://docs.zerodev.app/) * [Biconomy Nexus](https://docs.biconomy.io) * [Openfort](https://github.com/openfort-xyz/openfort-7702-delegator) * [Rhinestone](https://docs.rhinestone.dev) * [Etherspot](https://etherspot.io) * [Curvegrid's EIP-7702 Delegation Checker](https://eip7702.app) ## Wallets * [Ambire](https://www.ambire.com) * [MetaMask](https://metamask.io) * [Trust Wallet](https://trustwallet.com) * [TokenPocket](https://tokenpocket.pro) * [PillarX](https://pillarx.app) ## Apps * [Otim](https://otim.com) * [NFTs2Me](https://nfts2me.com) ## Demos * [Privy](https://x.com/privy_io/status/1867249205133373905) * [zklogin](https://x.com/oleh_bc/status/1862376837621665921) ([source](https://github.com/shield-labs-xyz/zklogin/pull/2)) * [Openfort](https://7702.openfort.xyz) ([source](https://github.com/openfort-xyz/sample-7702-delegator)) * [Alchemy Account Kit](https://x.com/AdamEgyed1/status/1886833087432081442) * [NFTs2Me Minting page](https://one-click-wonders.testnet.nfts2.me/)