Skip to main content
EOA Code Delegation lets an Externally Owned Account (EOA) point at a contract whose code will execute in the EOA’s own context whenever the EOA is called, similar to how DELEGATECALL runs Contract B’s code inside Contract A’s storage frame. It is Hedera’s adoption of Ethereum’s EIP-7702, defined for Hedera by HIP-1340 and rolled out as part of the Pectra hard fork. Delegation unlocks smart-account use cases (transaction batching, sponsored gas, session keys, privilege de-escalation) without migrating funds to a new contract wallet; the EOA’s address, balance, NFTs, and tokens stay put.

How delegation is stored

The Account entity gains a new optional 20-byte field, delegation_address. When set, calls to the EOA’s address resolve to the code at delegation_address. For Ethereum-tool compatibility, the Smart Contract Service exposes the delegation as a Delegation Indicator in the EOA’s code field:
delegation_indicator = 0xef0100 ‖ <20-byte delegation_address>
  • Before Pectra, eth_getCode(eoa) always returned 0x.
  • After Pectra, an EOA with delegation set returns the 23-byte 0xef0100... indicator. An EOA with no delegation still returns 0x.
  • Regular contract bytecode cannot start with 0xef (per EIP-3541), so the prefix is unambiguous.
A delegation_address of 0x0000000000000000000000000000000000000000 clears the delegation (the field becomes empty).

Two paths to configure delegation

You can set, change, or clear an EOA’s delegation through either an Ethereum-compatible Type 4 transaction or a native HAPI CryptoCreate / CryptoUpdate. Both routes converge on the same delegation_address field on the account.
                 ┌──────────────────────────┐
                 │       User / Wallet      │
                 └────────────┬─────────────┘

                   ┌──────────┴──────────┐
                   │                     │
                   ▼                     ▼
         ┌───────────────┐     ┌────────────────────┐
         │    EVM path   │     │    HAPI path       │
         │    EIP-7702   │     │   CryptoCreate /   │
         │   Type 4 tx   │     │   CryptoUpdate     │
         └───────┬───────┘     └──────────┬─────────┘
                 │                        │
                 └───────────┬────────────┘

                 Account.delegation_address

Path A: EVM (Type 4 EthereumTransaction)

A Type 4 transaction extends the standard RLP-encoded fields with an authorization_list:
transaction_payload = 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],
  ...
]
Each entry is an independent intent, signed by the EOA that wants delegation set. The signature is recovered with ecrecover, and the recovered account’s delegation_address is set to the entry’s address. A single Type 4 transaction can authorize delegations for many unrelated EOAs in one shot, and the transaction sender may be a third party (i.e., sponsored delegation setup, useful for onboarding flows). Validity rules (per EIP-7702, enforced by Hedera):
  • Signature must be valid ecrecover over (MAGIC ‖ rlp([chain_id, address, nonce])).
  • chain_id must be 0 (any chain) or match the network’s chain ID (296 testnet, 295 mainnet).
  • nonce must match the authorizing account’s current nonce.
  • If the authorization list is empty, the transaction is invalid and rejected.
  • Invalid entries are silently skipped (no revert).
  • If multiple valid entries target the same EOA, only the last one is applied.
Each authorization spawns a child CryptoCreate (for a brand-new hollow account) or CryptoUpdate (for an existing account). These child records appear in the transaction’s record stream regardless of whether the parent EVM call reverts; the delegation persists even if the subsequent code execution fails.

JavaScript example (viem)

There are two common flows: self-submitted (the EOA pays for and sends its own delegation transaction) and sponsored (a third party pays; the EOA only signs the authorization). Self-submitted (the EOA owns the gas):
JavaScript
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { hederaTestnet } from "viem/chains";

const eoa       = privateKeyToAccount(process.env.EOA_PRIVATE_KEY);
const eoaClient = createWalletClient({ chain: hederaTestnet, transport: http(), account: eoa });

// `executor: 'self'` tells viem the EOA will also submit the transaction,
// so the authorization must be signed with the next nonce (current + 1).
const authorization = await eoaClient.signAuthorization({
  contractAddress: "0x<smart-account-implementation>",
  executor: "self",
});

const hash = await eoaClient.sendTransaction({
  authorizationList: [authorization],
  to: eoa.address,    // can be the EOA itself, the zero address, or any target
  data: "0x",         // optional follow-up call data (executed after the auth is processed)
});
Sponsored (a third party pays; the EOA only signs the authorization):
JavaScript
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { hederaTestnet } from "viem/chains";

const eoa     = privateKeyToAccount(process.env.EOA_PRIVATE_KEY);
const sponsor = privateKeyToAccount(process.env.SPONSOR_PRIVATE_KEY);

const eoaClient     = createWalletClient({ chain: hederaTestnet, transport: http(), account: eoa });
const sponsorClient = createWalletClient({ chain: hederaTestnet, transport: http(), account: sponsor });

// Omit `executor`; the sponsor submits the transaction, so the EOA's nonce
// is only consumed once (by the authorization itself).
const authorization = await eoaClient.signAuthorization({
  contractAddress: "0x<smart-account-implementation>",
});

const hash = await sponsorClient.sendTransaction({
  authorizationList: [authorization],
  to: eoa.address,
  data: "0x",
});
After the transaction is mined, calls to eoa.address will execute the smart-account implementation’s code in the EOA’s storage context. To revoke, the EOA signs a new authorization with contractAddress: "0x0000000000000000000000000000000000000000".

Submitting via EthereumTransaction (Hedera SDK)

If you have a fully-signed Type 4 RLP payload, you submit it the same way as any other Ethereum transaction:
JavaScript
import { EthereumTransaction, Hbar } from "@hashgraph/sdk";

const tx = await new EthereumTransaction()
  .setEthereumData(rawType4RlpBytes)          // raw 0x04 || rlp(...)
  .setMaxGasAllowanceHbar(new Hbar(2))
  .execute(client);

const receipt = await tx.getReceipt(client);
console.log(receipt.status.toString());
The SDK also models the Type 4 payload directly via EthereumTransactionDataEip7702, which carries the standard EIP-1559-style fields plus accessList and an authorizationList of Authorization entries (chainId, address, nonce, yParity, r, s). Calling toBytes() on it produces the raw payload to pass to setEthereumData().

Path B: Native HAPI (CryptoCreate / CryptoUpdate)

HIP-1340 extends both CryptoCreateTransactionBody and CryptoUpdateTransactionBody with a delegation_address field (bytes, 20 raw EVM address bytes, not the 0xef0100-prefixed indicator). The SDK lifts this onto AccountCreateTransaction and AccountUpdateTransaction as setDelegationAddress(), which accepts raw bytes, a hex string (with or without the 0x prefix), or an EvmAddress.
JavaScript
import { AccountUpdateTransaction, AccountId } from "@hashgraph/sdk";

// Point an existing EOA at a smart-account implementation
const tx = await new AccountUpdateTransaction()
  .setAccountId(AccountId.fromString("0.0.4567"))
  .setDelegationAddress("0x019b8ee526333d9cbbbc35fff0309794dfa73451")
  .freezeWith(client)
  .sign(eoaKey);                              // EOA must sign

await (await tx.execute(client)).getReceipt(client);

// Read it back via property access
console.log(`Delegation address: ${tx.delegationAddress?.toString()}`);
To clear the delegation, set it to the zero address (0x0000000000000000000000000000000000000000) or pass null on an AccountUpdateTransaction.
Native HAPI is the only path available to ED25519 accounts and ECDSA accounts with a long-zero alias, since those account types cannot sign EIP-7702 authorizations. See Account-type applicability.

What happens when a delegated EOA is called

When the Smart Contract Service processes a call whose target is an EOA address:
1. Look up the target Account.
2. Inspect the call's 4-byte selector.
   ├─ Matches a HAS facade selector? → route to Hedera Account Service. (HAS precedence)
   ├─ Account has delegation_address set? → execute target's code in EOA's storage context.
   └─ Otherwise → no-op (value transfer only, if any).
3. If the delegated code reverts, any value transfer also reverts.
The call applies uniformly whether it originated from:
  • A top-level transaction (transaction.to == eoa).
  • An inner CALL / STATICCALL from another contract (address(eoa).call(...)).
  • Any of the legacy transaction types (0, 1, 2), not just Type 4.

Code queries

EXTCODESIZE, EXTCODECOPY, EXTCODEHASH, eth_getCode, and ContractGetBytecodeQuery against a delegated EOA all return the Delegation Indicator (0xef0100 ‖ delegation_address). For an undelegated EOA they return empty bytecode, as before.

Storage & logs

A delegated EOA can now hold mutable storage slots and emit EVM logs, because the delegated code runs in its context. Tooling implications:
  • eth_getStorageAt(eoa, slot) returns the EOA’s storage.
  • eth_getLogs returns logs emitted from the EOA’s address.
  • Mirror Node /api/v1/contracts/{eoa}/state, /results/logs, /call, and friends now work on EOA addresses too.

Chains & loops

If delegation_address points to another EOA that itself has a delegation, only the first hop is resolved. The bytecode at the second hop is interpreted literally, and since 0xef is a banned opcode (EIP-3541), the call reverts. In practice this means don’t chain delegations: always delegate directly to a real contract.

No-op targets

Setting delegation_address to any of the following is allowed, but the resulting call to the EOA is a no-op (value transfer only, no code executes):
  • A non-existent account.
  • A precompile address (0x010x11).
  • A system contract (HTS at 0x167, HAS at 0x16a, HSS at 0x16b, Exchange Rate at 0x168, PRNG at 0x169).
  • A Hedera system account.
  • Another EOA whose own code is empty.
Direct delegation from a regular EOA to a system contract is therefore intentionally inert; the token-proxy / schedule-proxy accounts are the only entities permitted to “delegate” to a system contract address.

HAS facade precedence

Calls that match a Hedera Account Service facade selector are always routed to HAS, even if the EOA has a delegation set. This preserves the user-facing behavior of pre-Pectra HAS calls.
HAS functionSelector
hbarAllowance(address spender)0xbbee989e
hbarApprove(address spender, int256 amount)0x86aff07c
setUnlimitedAutomaticAssociations(bool enabled)0xf5677e99
Processing order (per HIP-1340):
  1. If the call selector matches a HAS facade function → HAS handles it. The user’s delegation is ignored for this call.
  2. Else if the EOA has a delegation → run the delegate contract’s code.
  3. Else → no-op (with any attached value transfer).
⚠️ Selector-collision warning. If your smart-account delegate defines a function whose selector happens to equal one of the HAS selectors above (e.g. you happen to write a hbarApprove(address,int256) method), that function will be unreachable; HAS will intercept the call first. Pick distinct signatures.

Account-type applicability

Whether you can use the EVM path depends on the account’s key type and alias scheme.
Account typeEVM path (Type 4)HAPI path (CryptoCreate/Update)
ED25519 account
ECDSA account, long-zero alias (0x0000…<accountNum>)
ECDSA account, public-key-derived alias
This isn’t new behavior; it reflects the existing rule that only ECDSA-with-public-key-derived-alias accounts can submit Ethereum transactions on Hedera. Once delegation is configured, execution semantics are uniform across account types: a call to an ED25519 account with delegation_address set runs the delegate code just like for any other account. CryptoTransfer signature validation is unchanged. Hedera-native authorization (e.g., multi-sig threshold keys) continues to govern transfers initiated through HAPI.

Gas costs

Per EIP-7702, every entry in an authorization_list costs gas, whether valid, invalid, or duplicate:
Authorization outcomeGas charged
Authorizes an existing account12,500
Authorizes a new account (hollow account creation)25,000 + Hedera’s hollow-account creation cost
Invalid signature / wrong chain ID / wrong nonce12,500 (consumed, then silently skipped)
Duplicate entry for an EOA already authorized in the list12,500 (consumed, then skipped)
The cap on hollow accounts created in one transaction is governed by the transaction’s gas_limit and the network’s operations-per-second throttle. For the calldata and execution sides of the gas calculation, see Gas and Fees.
Gas shortage on a valid authorization. If an otherwise-valid authorization can’t be processed because the transaction ran out of gas (for example, it would require creating a hollow account but the gas_limit is exhausted), Hedera still emits a child CryptoCreate / CryptoUpdate record with a failed status so the error surfaces in the record stream rather than being silently dropped.

Smart-account example (Solidity)

A minimal smart-account delegate that supports batched calls and an owner-only check:
Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

/// @title MinimalSmartAccount
/// @notice Intended to be installed as an EOA delegation target via EIP-7702.
///         All state read/written here lives in the *EOA's* storage frame.
contract MinimalSmartAccount {
    // EIP-7201 namespaced storage slot for "hedera.smart-account.v1".
    // Derivation: keccak256(abi.encode(uint256(keccak256("hedera.smart-account.v1")) - 1)) & ~bytes32(uint256(0xff))
    // Reproduce with Foundry: `cast index-erc7201 "hedera.smart-account.v1"`
    // If you change the namespace string, recompute this constant.
    bytes32 private constant SLOT =
        0xf7bd8ee8b6b02e586c7a21c9ecc95fa2e26786f8e462f9f9103d541de5d4bf00;

    struct Layout {
        uint256 nonce;
    }

    function _s() private pure returns (Layout storage $) {
        assembly { $.slot := SLOT }
    }

    /// Only the EOA itself (i.e. address(this) == msg.sender after delegation) may call.
    modifier onlyOwner() {
        require(msg.sender == address(this), "not owner");
        _;
    }

    struct Call { address to; uint256 value; bytes data; }

    /// Execute a batch of calls atomically from the EOA's address.
    function executeBatch(Call[] calldata calls) external onlyOwner {
        _s().nonce++;
        for (uint256 i = 0; i < calls.length; i++) {
            (bool ok, bytes memory ret) = calls[i].to.call{value: calls[i].value}(calls[i].data);
            require(ok, _revertReason(ret));
        }
    }

    function nonce() external view returns (uint256) { return _s().nonce; }

    receive() external payable {}

    function _revertReason(bytes memory ret) private pure returns (string memory) {
        if (ret.length < 68) return "call reverted";
        assembly { ret := add(ret, 0x04) }
        return abi.decode(ret, (string));
    }
}
Deploy once with a regular ContractCreate. Then any number of EOAs can delegate to that deployed address, and they each get smart-account behavior without re-deploying.
Use namespaced storage (ERC-7201) in delegate contracts so the EOA can later switch delegates without colliding on storage slots written by the previous implementation.
This is the flagship EIP-7702 use case: a new user with zero HBAR gets smart-account behavior and executes a multi-step action in one transaction, while a sponsor pays the gas. The single Type 4 transaction does two things at once: it sets the EOA’s delegation and runs a batched call in the EOA’s context.
JavaScript
import { createWalletClient, http, encodeFunctionData, parseAbi } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { hederaTestnet } from "viem/chains";

const eoa     = privateKeyToAccount(process.env.EOA_PRIVATE_KEY);       // new user, no HBAR
const sponsor = privateKeyToAccount(process.env.SPONSOR_PRIVATE_KEY);   // pays the gas

const eoaClient     = createWalletClient({ chain: hederaTestnet, transport: http(), account: eoa });
const sponsorClient = createWalletClient({ chain: hederaTestnet, transport: http(), account: sponsor });

// 1. The EOA signs an authorization delegating to the deployed MinimalSmartAccount.
//    No `executor` field, because the sponsor (not the EOA) submits the transaction.
const authorization = await eoaClient.signAuthorization({
  contractAddress: "0x<deployed-MinimalSmartAccount>",
});

// 2. Build the batched call: approve a router, then swap (both run in the EOA's context).
const smartAccountAbi = parseAbi([
  "struct Call { address to; uint256 value; bytes data; }",
  "function executeBatch(Call[] calls)",
]);
const erc20Abi  = parseAbi(["function approve(address spender, uint256 amount)"]);
const routerAbi = parseAbi(["function swapExactTokensForHBAR(uint256 amountIn, address token)"]);

const batch = encodeFunctionData({
  abi: smartAccountAbi,
  functionName: "executeBatch",
  args: [[
    { to: "0x<token>",  value: 0n, data: encodeFunctionData({ abi: erc20Abi,  functionName: "approve", args: ["0x<router>", 1_000_000n] }) },
    { to: "0x<router>", value: 0n, data: encodeFunctionData({ abi: routerAbi, functionName: "swapExactTokensForHBAR", args: [1_000_000n, "0x<token>"] }) },
  ]],
});

// 3. Sponsor submits one Type 4 transaction: it sets the delegation AND invokes the batch.
const hash = await sponsorClient.sendTransaction({
  authorizationList: [authorization],   // sets eoa.delegation_address
  to: eoa.address,                      // call resolves to the delegate code, in the EOA's context
  data: batch,                          // executeBatch([approve, swap])
});
The EOA never needed HBAR or a prior on-chain footprint. After this transaction, eth_getCode(eoa.address) returns the Delegation Indicator, and the approve + swap have executed from the EOA’s own address.
The delegation set by the authorization persists even if the batched call reverts (authorizations are committed before EVM execution). If you want all-or-nothing onboarding, have the delegate contract revert on any sub-call failure (as executeBatch does via its require), and treat the delegation itself as a separate, durable state change.

Mirror Node surface

HIP-1340 extends the Mirror Node REST API:
  • Accounts endpoints (GET /api/v1/accounts, GET /api/v1/accounts/{idOrAliasOrEvmAddress}) include a new delegation_address field per account. The value is 0x when no delegation is set.
  • Contract-result endpoints (GET /api/v1/contracts/{id}/results/{timestamp}, GET /api/v1/contracts/results, GET /api/v1/contracts/results/{txOrHash}) include a new authorization_list field for Type 4 transactions:
    {
      "authorization_list": [
        {
          "chain_id": "0x127",
          "address":  "0x1111111111111111111111111111111111111111",
          "nonce":    5,
          "y_parity": 1,
          "r": "0x2222...",
          "s": "0x3333..."
        }
      ]
    }
    
  • EOA contract APIs. /api/v1/contracts/{eoa}/state, /results/logs, /results, /results/{tx}/actions, /results/{tx}/opcodes, and /api/v1/contracts/call now accept EOA addresses and return contract-style data when the EOA has a delegation set.
JSON-RPC Relay updates: eth_getCode, eth_getStorageAt, and eth_getLogs work for delegated EOAs; the relay handles Type 4 transaction parsing and submission.

HTS / HSS proxy accounts

HIP-1340 also re-implements the long-standing HTS-token and HSS-schedule facade calls (HIP-218 / HIP-719 / HIP-755 / HIP-906) using the new Delegation Indicator format. Calls like IERC20(tokenAddress).transfer(...) continue to work exactly as before, but ContractGetBytecode(tokenAddress) now returns a stable 0xef0100 ‖ <HTS-system-contract-address> indicator instead of a runtime-synthesized DELEGATECALL stub. This is invisible to most users. Tools that introspect the bytecode of token-proxy or schedule-proxy accounts (e.g. block explorers, custom indexers) may need to update their parsing.

Caveats and edge cases

  • An empty authorization_list makes a Type 4 transaction invalid. Don’t construct one and then drop entries client-side; either include at least one authorization or send a different type.
  • Authorizations persist even if the EVM execution reverts. They are processed before EVM code runs and committed independently; this is a feature (so onboarding flows still succeed even if the follow-up call fails), but it does mean a “rolled back” transaction can still leave delegations in place.
  • Selector collisions with HAS make those functions unreachable on a delegated EOA. Pick non-colliding signatures.
  • Storage migration. Switching delegation targets does not migrate storage. Use ERC-7201 namespaces in your delegates to avoid collisions.
  • msg.sender == tx.origin invariant no longer holds in deeper frames. Code historically used msg.sender == tx.origin as a “no contracts” check. With delegation, the topmost frame on a Type 4 transaction can be a delegated EOA, meaning tx.origin and msg.sender align in places they previously wouldn’t. Treat this check as advisory, not a security boundary.
  • No impact on hooks. A call to a delegated EOA inside a hook behaves the same as outside one. Hooks use a dedicated storage namespace, so delegate code writing to the EOA’s storage slots cannot collide with hook storage.

References