EIP-7702 Developer Guide
Introduction
EIP-7702, part of Ethereum's account abstraction roadmap, was introduced in the Pectra hard fork. It allows existing EOAs to adopt smart contract features by delegating control to a designated contract.
Regular EOA wallets upgrade to EIP-7702 smart accounts by authorizing a contract. The upgraded wallet has a "delegation indicator" that points to the authorized contract. When a transaction is sent to the EOA, it executes the code of the authorized contract. To downgrade back to a regular EOA, the wallet authorizes the burn address.
This upgrade allows users to:
- Batch transactions
- Use session keys
- Sponsor gas
- Pay with ERC20s
- Enjoy chain abstraction
- Use passkeys
All while retaining their familiar EOA address and without migrating funds.
Key Benefits
- Gas efficiency – No deployment required.
- Seamless UX – Enjoy AA features directly.
- Multichain support – Reuse authorizations across chains.
- Flexible integration – Simple to integrate for devs.
- Custom logic – Not limited to a fixed wallet type.
Technical Deep Dive
EIP-7702 complements ERC-4337, introducing a new transaction type 0x04
(SET_CODE_TX_TYPE) that lets EOAs delegate execution.
Delegation and Authorization
- Delegation: EOA chooses a smart contract for execution.
- Authorization: Signed message with chain ID, contract address, nonce, and signature.
New Transaction Type
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], ...]
authorization_list
address
is the delegated implementation.- Signer (the EOA) is derived from signature + payload.
Type 4 Behavior
For each tuple:
- Validate
chain_id
,nonce
. - Derive
authority = ecrecover(keccak(0x05 || rlp(...)))
. - Add authority to
accessed_addresses
. - Ensure code is empty or already delegated.
- Ensure nonce matches.
- Add gas refund if applicable.
- Set code:
0xef0100 || address
(delegation designation). - If
address == 0x0
, clear code. - Bump nonce.
Delegation is permanent unless explicitly revoked.
Resources
- Viem Guide
- BatchCallAndSponsor.sol
- MetaMask Delegator
- Awesome EIP-7702
- EIP-7702 Portal
- EIP-7851
- Privy React Recipe
- ZeroDev Blog
- ERC-5792 Demo
- Stats Dashboard
- Fusion Module Alt
- Stats
- Adoption Tracker
For Devs: Using EIP-7702
Prerequisites
npm i @startale-scs/aa-sdk viem
Generate credentials on SCS Portal. Retrieve:
bundlerUrl
paymasterUrl
paymasterId
implementationAddress
This guide applies to embedded EOA wallet flows only. External wallets like MetaMask have restrictions:
- External wallets (MetaMask, etc.) only allow delegation to their own smart account implementations
- Embedded wallets (Privy, etc.) provide full control over delegation targets
For web2 user onboarding: Use this EIP-7702 flow to upgrade EOAs to Startale smart accounts
For MetaMask users: Use the regular flow without EIP-7702, making MetaMask signer the main controller of the deployed Startale smart account
Setup
import { http, createPublicClient, createWalletClient, privateKeyToAccount, generatePrivateKey, encodeFunctionData } from "viem";
import { createSCSPaymasterClient, createSmartAccountClient, toStartaleSmartAccount } from "@startale-scs/aa-sdk";
import { soneiumMinato } from "viem/chains";
const signer = privateKeyToAccount(generatePrivateKey());
const chain = soneiumMinato;
const publicClient = createPublicClient({ transport: http(), chain });
const walletClient = createWalletClient({ account: signer, chain, transport: http() });
const paymaster = createSCSPaymasterClient({ transport: http(paymasterUrl) });
const scsContext = { calculateGasLimits: true, paymasterId };
A transaction where Authorizations are executed onchain, thereby upgrading EOAs to smart accounts. One set code txn can contain multiple Authorizations.
Manual Authorization
// put below in .env
// STARTALE_ACCOUNT_IMPLEMENTATION_ADDRESS=0x000000b8f5f723A680d3D7EE624Fe0bC84a6E05A
const implementationAddress = process.env.STARTALE_ACCOUNT_IMPLEMENTATION_ADDRESS as Address;
const authorization = await walletClient.signAuthorization({ contractAddress: implementationAddress });
const smartAccountClient = createSmartAccountClient({
account: await toStartaleSmartAccount({
signer,
chain,
transport: http(),
accountAddress: eoaAddress,
eip7702Auth: authorization,
}),
transport: http(bundlerUrl),
client: publicClient,
paymaster,
paymasterContext: scsContext,
});
Automatic Authorization
const smartAccountClient = createSmartAccountClient({
account: await toStartaleSmartAccount({
signer,
chain,
transport: http(),
accountAddress: eoaAddress,
eip7702Account: signer,
}),
transport: http(bundlerUrl),
client: publicClient,
paymaster,
paymasterContext: scsContext,
});
Sending Transactions
const callData = encodeFunctionData({ abi: CounterAbi, functionName: "count" });
const hash = await smartAccountClient.sendUserOperation({
calls: [{ to: counterContract, value: 0n, data: callData }],
});
const receipt = await smartAccountClient.waitForUserOperationReceipt({ hash });
console.log("receipt", receipt);
const isDelegated = await smartAccountClient.account.isDelegated();
console.log("isDelegated", isDelegated);
Helpers
const isDelegated = await smartAccountClient.account.isDelegated();
const tx = await smartAccountClient.account.unDelegate();
const unDelegateReceipt = await publicClient.waitForTransactionReceipt({ hash: tx });
External Wallets
Expect restrictions on which smart account implementations are supported. Likely flows:
- Prompt user to upgrade with pre-approved contract
- Limited to wallet provider’s implementation
Embedded Wallets
Dapps integrate against their own embedded wallets with full feature support. No dependency on external wallet compatibility.
Tutorial
How to upgrade an EOA to a Startale smart account and batch-mint NFTs gaslessly
// Follow steps from setup and authorization
import { http, createPublicClient, createWalletClient, privateKeyToAccount, generatePrivateKey } from "viem";
import { createSCSPaymasterClient, createSmartAccountClient, toStartaleSmartAccount } from "@startale-scs/aa-sdk";
const signer = privateKeyToAccount(generatePrivateKey());
const chain = soneiumMinato;
// Get this from SCS portal dashboard and put in env
const paymasterId = process.env.PAYMASTER_ID;
const publicClient = createPublicClient({ transport: http(), chain });
const walletClient = createWalletClient({ account: signer, chain, transport: http() });
const paymaster = createSCSPaymasterClient({ transport: http(paymasterUrl) });
const scsContext = { calculateGasLimits: true, paymasterId };
// Startale NFT contract address deployed on Soneium soneiumMinato
// NFT_CONTRACT_ADDRESS=0xa37f9d4E7E296C7eda4cB711738595B5f19AF8A7
const nftContract = process.env.NFT_CONTRACT_ADDRESS as Address;
// full NFT contract abi is here: https://pastebin.com/zscdV3cF
const NFTAbi = [
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "safeMint",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
];
// put below in .env
// STARTALE_ACCOUNT_IMPLEMENTATION_ADDRESS=0x000000b8f5f723A680d3D7EE624Fe0bC84a6E05A
const implementationAddress = process.env.STARTALE_ACCOUNT_IMPLEMENTATION_ADDRESS as Address;
const authorization = await walletClient.signAuthorization({ contractAddress: implementationAddress });
const smartAccountClient = createSmartAccountClient({
account: await toStartaleSmartAccount({
signer,
chain,
transport: http(),
accountAddress: eoaAddress,
eip7702Auth: authorization,
}),
transport: http(bundlerUrl),
client: publicClient,
paymaster,
paymasterContext: scsContext,
});
const mintNFTCallData = encodeFunctionData({
abi: NFTAbi,
functionName: "safeMint",
args: [smartAccountClient.account.address] // Receiver address is SA address itself
});
const hash = await smartAccountClient.sendUserOperation({
calls: [
{
to: nftContract as Address,
value: BigInt(0),
data: mintNFTCallData,
},
{
to: nftContract as Address,
value: BigInt(0),
data: mintNFTCallData,
},
],
// No need to pass anything else separately
});
const receipt = await smartAccountClient.waitForUserOperationReceipt({ hash });
console.log("receipt", receipt);