Turn a Regular Wallet into a Smart Account with EIP 7702

Wait 5 sec.

Web3 has made huge strides in recent years, but UX and mainstream adoption remain major hurdles. According to surveys, key management is the biggest blocker: 46.8% of users cite “security concerns” as their top deterrent, and 80% say they need stronger confidence in account protection before transacting.Unfortunately, that fear is valid. Billions in crypto have been lost due to misplaced keys, seed phrase mishandling, and private key theft. For teams building web3 products, this creates a painful trade-off: intuitive, seamless UX often means compromising on security or self-custody.As the saying goes — pick only two: UX, security, or sovereignty.The community has been working to break this triangle. Among the most promising directions:EIP-7702 — lets EOAs temporarily delegate execution to a smart contract without changing the user’s address or history.EIP-4337 — standardizes smart contract accounts and brings the necessary infrastructure (like bundlers and paymasters) to make them work like web2 logins.MPC accounts — use secure key sharing to enable social login, recovery flows, and higher resilience, without giving up control.I’m a mentor at international web3 hackathons and regularly publish guides and open-source tools to help teams adopt modern UX architectures faster. These approaches are battle-tested in real products and adapted across blockchain stacks. Currently building at Unique Network — an infrastructure project on Polkadot focused on NFTs and developer tooling.In this article, I break down how to apply EIP-7702 in practice — using a minimal smart wallet as an example, with step-by-step delegation, initialization, and safe migration without state loss.What is EIP-7702Today's users won't tolerate poor UX. They expect smooth experiences that can be achieved through abstraction, including:Transaction batchingGas sponsorshipEnhanced security (social recovery, spending limits, limited delegation)Significant progress was made with smart contract wallets. However, the ecosystem carried the legacy burden of EOA accounts, preventing applications from leveraging the benefits of the Account Abstraction initiative, as there's a fundamental difference between what can be done with smart contract wallets and EOAs.Then, the Pectra hard fork introduced EIP-7702.EIP-7702 is a key step in Ethereum’s account abstraction roadmap. It gives existing Externally Owned Accounts (EOAs) access to smart contract capabilities — without requiring users to switch to a new address or give up their account history.With EIP-7702, an EOA can temporarily delegate execution to smart contract code while preserving its original structure. In simple terms, it lets you upgrade your regular wallet with smart account features like transaction batching, custom validation, and gas sponsorship — all while keeping your existing address.What this guide coversIn this developer guide, I walk through how to apply EIP-7702 by progressively building a delegation-based smart wallet — starting from a minimal example and gradually adding ownership checks, batch execution, and safer storage patterns.We’ll cover:How Type 4 transactions and authorization_list workHow to handle initialization, storage safety, and ownershipHow to batch transactions and improve UXWhere the edges are — and what EIP-7702 doesn’t solveAll code is written in Solidity and tested with Foundry. You’ll also find runnable examples and full tests on GitHub.Let’s get started.How EIP-7702 Works TechnicallyEIP-7702 introduces a delegation mechanism that allows an EOA to temporarily point to smart contract code while keeping its original structure and history. This is enabled by a new transaction type (0x04) that includes an authorization_list.\chain_idnoncemax_priority_fee_per_gasmax_fee_per_gasgas_limitdestinationvaluedataaccess_listauthorization_listsignature_y_paritysignature_rsignature_s\The authorization_list property is the most essential part of the new transaction type.Authorization listThe authorization_list contains signatures from EOA owners authorizing their accounts to delegate execution to specific smart contracts. It's a list of tuples that store the contract address to which the signer wants to delegate execution in the context of their EOA.\authorization_list = [[chain_id, address, nonce, y_parity, r, s], ...]// ^^^^^^^// This is the contract address that the EOA (the signer of this tuple) will delegate to\The authorization_list is specifically designed to contain signature components (y_parity, r, s), enabling sponsored transactions where the delegator doesn't need to pay gas fees. Set code transactions can be executed by wallets or applications on behalf of users.The authorization list is designed as an array to allow applications to migrate multiple accounts in a single transaction. However, this doesn't mean you can set various delegators for one EOA — only one delegation target is allowed per accountThe set code transaction goes through a specific validation process, and if successful, it sets the code for the EOA. The code set on an EOA account is called a delegation designator and has a specific format:\// EIP-7702 delegation designator structurebytes memory delegationCode = abi.encodePacked( hex"ef0100", address(targetContract));// Total: 23 bytes\Where:0xef — EIP-3541 reserved byte indicating special protocol code (not executable bytecode)01 — EIP-7702 feature identifier within the EIP-3541 reserved space00 — version of the EIP-7702 delegation format (currently version 0)address (targetContract) — the final 20 bytes contain the full Ethereum address of the smart contract that will execute all function calls made to this EOANow your account points to a smart contract and can execute its logic.Initialization challengesThis brings up an important implementation detail. When we delegate our EOA to, say, an ownable wallet, how does this wallet know which address is authorized to execute a transaction? When a traditional wallet is deployed, its constructor runs and sets up the initial state, configuring owners, signature requirements, and other essential parameters. But when you delegate to an existing contract, your EOA's storage starts completely blank.This is because EIP-7702 delegates code execution, not storage. Your EOA maintains its own storage space, separate from the implementation contract. This means the contract you delegate to must provide an initialization mechanism to set up the required state in your EOA's storage space.Delegating to a Smart Contract WalletFull code examples are available on GitHub: github.com/Maksandre/try-eip-7702Let's start by implementing a simplified smart wallet to understand the basic mechanics, then improve it step by step.For this guide, we'll use Foundry to build and test the implementation.Our first version has a single method to execute a transaction:\// SmartWallet.sol// SPDX-License-Identifier: MITpragma solidity ^0.8.20;contract NotReallySmartWallet { function execute(address to, uint256 value, bytes calldata data) external payable { (bool success,) = to.call{value: value}(data); require(success, "Transaction reverted"); } fallback() external payable {} receive() external payable {}}\Next, we’ll write a test to understand how delegation works using Forge cheatcodes. We’ll use vm.signAndAttachDelegation, which mocks a SET_CODE_TX_TYPE for an EOA and a smart contract.Setup steps:Create two addresses: Alice (prefunded) and Bob (empty)Deploy our test wallet contractMake sure Alice’s EOA initially has no code\contract NotReallySmartWalletTest is Test { address public ALICE_ADDRESS; uint256 public ALICE_PRIVATE_KEY; address public BOB_ADDRESS; // Test parameters uint256 constant INITIAL_ALICE_BALANCE = 10 ether; uint256 constant TRANSFER_AMOUNT = 1 ether; // The contract that Alice will delegate execution to NotReallySmartWallet public delegationTarget; function setUp() public { // Generate addresses and keys dynamically (ALICE_ADDRESS, ALICE_PRIVATE_KEY) = makeAddrAndKey("alice"); BOB_ADDRESS = makeAddr("bob"); // Fund Alice's account vm.deal(ALICE_ADDRESS, INITIAL_ALICE_BALANCE); // Deploy the delegation contract delegationTarget = new NotReallySmartWallet(); }}\In our test, we start by delegating Alice's account to the predeployed `SmartWallet` contract.Then we check that Alice's account now has a delegation designator code. Remember, it has a special format starting with `ef0100`:\assertEq( ALICE_ADDRESS.code, abi.encodePacked(hex"ef0100", address(delegationTarget)));\This is important: EOAs can no longer be identified by empty code. Instead, check if the code starts with 0xef0100 — that's a delegated EOA under EIP-7702.Now we can call Alice’s EOA like any contract:\NotReallySmartWallet(ALICE_ADDRESS).execute( BOB_ADDRESS, TRANSFER_AMOUNT, "");\Verify account balances after the transaction to ensure it succeeded:\// Verify final balancesassertEq(BOB_ADDRESS.balance, TRANSFER_AMOUNT);assertEq( ALICE_ADDRESS.balance, INITIAL_ALICE_BALANCE - TRANSFER_AMOUNT);Building UX, Security, and ControlAt this point, our contract is working — but with no access control or real safety. Let's fix that.1. The Lack of Access ControlWe don’t have any authorization mechanism — anyone can call execute on the account. To demonstrate this, you can simply change the transaction origin from Alice to Bob, and the test will still pass:\vm.prank(/* ALICE_ADDRESS -> */ BOB_ADDRESS);NotReallySmartWallet(ALICE_ADDRESS).execute( BOB_ADDRESS, TRANSFER_AMOUNT, "");\Let’s make sure that the signer of a transaction is actually the delegating EOA account. If you want to restrict access to just that key, you can check that msg.sender == address(this).But what if you want more flexible access control — like multisig or passkeys? The SET_CODE_TX_TYPE doesn’t allow you to deploy a new contract with init code. You can only point to an existing one. That means the contract must already be initialized before it’s delegated to.The most straightforward — but incorrect — approach is to set the storage in the implementation contract.The problem is that EIP-7702 delegates code execution, not storage. Like delegatecall, execution happens in the context of the EOA, not the contract you're delegating to.Take the OpenZeppelin Ownable contract as an example. If you initialize it before delegation, the onlyOwner check will look for the owner in the EOA's storage — not in the contract’s own storage — and it won’t find it. The value you set in the contract itself will be ignored.To solve this, you need to move the owner initialization into a separate function and call it after the delegation:\// Remove constructor initialization// constructor() {// owner = msg.sender;// }function initialize() external { require(owner == address(0), "Already initialized"); owner = msg.sender;}\Now, after invoking the initialize function, the owner will be stored in the EOA storage and, as a result, recognized correctly. However, we've introduced another vulnerability.2. Front-runningIf delegation and initialization are two separate transactions, someone could initialize the wallet faster and gain access to the account's assets. There are two ways to prevent this:Delegate and initialize in the same transaction — by including the initialization call in the same transaction that performs the delegation.Use the ecrecover precompile to ensure that the initialization is performed by the EOA's key.Let’s start with the first option. You can use the to and data fields of the EIP-7702 transaction to include an initialization call. Suppose our implementation contract has the following function:\function initialize(address _owner) external { require(owner == address(0), "Already initialized"); owner = _owner;}\Then you can call this function in the same transaction, immediately after setting the delegation. Here's how you can do it using viem:\...const authorization = await walletClient.signAuthorization({ contractAddress: implementationAddress, account: delegator, //