EOA Delegation

This guide walks you through the process of delegating an Externally Owned Account (EOA) to a contract following ERC-7702. This allows you to use your EOA’s private key to sign and execute operations with custom execution logic. Combined with ERC-4337 infrastructure, users can achieve gas sponsoring through paymasters.

Delegating execution

ERC-7702 introduces a new transaction type (0x4) that grants externally owned accounts (EOAs) the ability to execute smart contract operations by setting a delegate contract. This functionality bridges the gap between EOAs and smart account, unlocking capabilities including batch transaction processing, gas fee sponsorship, and managed delegation of access rights.

Signing Authorization

To authorize delegation, the EOA owner signs a message containing the chain ID, nonce, delegation address, and signature components (i.e. [chain_id, address, nonce, y_parity, r, s]). This signed authorization serves two purposes: it restricts execution to only the delegate contract and prevents replay attacks.

The EOA maintains a delegation designator for each authorized address on each chain, which points to the contract whose code will be executed in the EOA’s context to handle delegated operations.

Here’s how to construct an authorization with viem:

const walletClient = createWalletClient({
  account, // See Viem's `privateKeyToAccount`
  chain, // import { ... } from 'viem/chains';
  transport: http(),
})

const authorization = await eoaClient.signAuthorization({
  account: walletClient.account.address,
  contractAddress: '0x<YOUR_DELEGATE_CONTRACT_ADDRESS>',
  // Use instead of `account` if your
  // walletClient's account is the one sending the transaction
  // executor: "self",
});

Send a Set Code Transaction

After signing the authorization, you need to send a SET_CODE_TX_TYPE (0x04) transaction to write the delegation designator (i.e. 0xef0100 || address) to your EOA’s code, which tells the EVM to load and execute code from the specified address when operations are performed on your EOA.

// Send the `authorization` along with `data`
const receipt = await walletClient
  .sendTransaction({
    authorizationList: [authorization],
    data: '0x<CALLDATA_TO_EXECUTE_IN_THE_ACCOUNT>',
    to: eoa.address,
  })
  .then((txHash) =>
    publicClient.waitForTransactionReceipt({
      hash: txHash,
    })
  );

// Print receipt
console.log(userOpReceipt);

To remove the delegation and restore your EOA to its original state, you can send a SET_CODE_TX_TYPE transaction with an authorization tuple that points to the zero address (0x0000000000000000000000000000000000000000). This will clear the account’s code and reset its code hash to the empty hash, however, be aware that it will not automatically clean the EOA storage.

When changing an account’s delegation, ensure the newly delegated code is purposely designed and tested as an upgrade to the old one. To ensure safe migration between delegate contracts, namespaced storage that avoid accidental collisions following ERC-7201.

Updating the delegation designator may render your EOA unusable due to potential storage collisions. We recommend following similar practices to those of writing upgradeable smart contracts.

Using with ERC-4337

The ability to set code to execute logic on an EOA allows users to leverage ERC-4337 infrastructure to process user operations. Developers only need to combine an Account contract with an SignerERC7702 to accomplish ERC-4337 compliance out of the box.

// contracts/MyAccountERC7702.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Account} from "@openzeppelin/community-contracts/account/Account.sol";
import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
import {ERC7821} from "@openzeppelin/community-contracts/account/extensions/ERC7821.sol";
import {SignerERC7702} from "@openzeppelin/community-contracts/utils/cryptography/SignerERC7702.sol";

contract MyAccountERC7702 is Account, SignerERC7702, ERC7821, ERC721Holder, ERC1155Holder {
    /// @dev Allows the entry point as an authorized executor.
    function _erc7821AuthorizedExecutor(
        address caller,
        bytes32 mode,
        bytes calldata executionData
    ) internal view virtual override returns (bool) {
        return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
    }
}

When implementing delegate contracts, ensure they require signatures over:

  • a nonce value for replay protection

  • value to prevent unexpected effects in the callee

  • gas to prevent griefing via out-of-gas failures

  • target and calldata to prevent arbitrary function calls

A poorly implemented delegate can allow a malicious actor to take near complete control over a signer’s EOA.

ERC7821 is included to bring batching capabilities to the account, a convenient feature.

Sending a UserOp

Once your EOA is delegated to an ERC-4337 compatible account, you can send user operations through the entry point contract. The user operation includes all the necessary fields for execution, including gas limits, fees, and the actual call data to execute. The entry point will validate the operation and execute it in the context of your delegated account.

Similar to how sending a UserOp is achieved for factory accounts, here’s how you can construct a UserOp for an EOA who’s delegated to an Account contract.

const userOp = {
  sender: eoa.address,
  nonce: await entrypoint.read.getNonce([eoa.address, 0n]),
  initCode: "0x" as Hex,
  callData: '0x<CALLDATA_TO_EXECUTE_IN_THE_ACCOUNT>',
  accountGasLimits: encodePacked(
    ["uint128", "uint128"],
    [
      100_000n, // verificationGas
      300_000n, // callGas
    ]
  ),
  preVerificationGas: 50_000n,
  gasFees: encodePacked(
    ["uint128", "uint128"],
    [
      0n, // maxPriorityFeePerGas
      0n, // maxFeePerGas
    ]
  ),
  paymasterAndData: "0x" as Hex,
  signature: "0x" as Hex,
};

const userOpHash = await entrypoint.read.getUserOpHash([userOp]);
userOp.signature = await eoa.sign({ hash: userOpHash });

const userOpReceipt = await eoaClient
  .writeContract({
    abi: EntrypointV08Abi,
    address: entrypoint.address,
    authorizationList: [authorization],
    functionName: "handleOps",
    args: [[userOp], eoa.address],
  })
  .then((txHash) =>
    publicClient.waitForTransactionReceipt({
      hash: txHash,
    })
  );

console.log(userOpReceipt);
When using sponsored transaction relayers, be aware that the authorized account can cause relayers to spend gas without being reimbursed by either invalidating the authorization (increasing the account’s nonce) or by sweeping the relevant assets out of the account. Relayers may implement safeguards like requiring a bond or using a reputation system.