EVM Tx — SetCode Transactions EIP-7702 Temporary Smart-Account Power for EOAs Explained
I’m currently open to collaborations and development projects across blockchain, smart contracts, and full-stack systems, feel free to connect if you’re building something interesting.
Most wallets are still EOAs, which limits UX: no batching, no scoped permissions, no custom verification. EIP-7702 introduces a new transaction type that lets an EOA temporarily act like a smart contract, no deployment, no persistent state changes. The tx carries an authorization list and installs a short delegation stub (
0xef0100 || address) for the duration of execution, redirecting calls to reusable module code.In this post, you’ll see how 7702’s lightweight delegation model works, what the authorization tuples contain, and how to batch app actions atomically while keeping the user’s EOA as the effective
msg.sender. We’ll finish with a Go example that builds and submits a SetCode transaction.
PreReq
Wallets setup from this blog post
EIP-7702 Set Code Transaction
Despite all the innovation around smart contract wallets, most users still rely on EOAs (Externally Owned Accounts) the classic Ethereum keypair. But EOAs are limited: they can’t batch actions, can’t delegate permissions, and can’t verify signatures beyond the standard ECDSA curve. This limits the kinds of UX improvements developers can deliver.
EIP-7702 proposes a powerful but simple fix: give EOAs the ability to temporarily behave like smart contracts, using a new transaction type. In this transaction, the user can attach custom code to their account just for the duration of the transaction. No deployment, no long-term state change, no extra gas costs.
How it works
At the core of EIP-7702 is a lightweight delegation model. The transaction includes a list of authorization tuples — formatted as:
[chain_id, address, nonce, y_parity, r, s]For each tuple, a delegation indicator (0xef0100 || address) is written to the authorizing account’s code. All code executing operations must load and execute the code pointed to by the delegation.
0xef0100is a special marker indicating “this account delegates its behavior”The 20-byte address points to the contract containing actual executable logic
New possible use cases
EIP-7702 unlocks several key UX improvements that have previously required full account abstraction or external infrastructure:
Batching: For example approve and use tokens in a single atomic tx
Sponsorship: Let another account pay your gas
Privilege Delegation: Grant limited-use permissions to app-specific keys or sub-accounts
By introducing a native way for EOAs to temporarily delegate behavior to reusable smart contract logic, EIP-7702 creates a path for safer, more flexible wallets without requiring protocol overhaul.
Transaction with RLP encoding looks like this:
0x04 || 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])where authorization_list is:
authorization_list = [[chain_id, address, nonce, y_parity, r, s], ...]Note: The fields
chain_id,nonce,max_priority_fee_per_gas,max_fee_per_gas,gas_limit,destination,value,data, andaccess_listof the outer transaction follow the same semantics as EIP-4844. Note, this implies a null destination is not valid.
How it works
Before executing the transaction, EIP-7702 processes an optional list of authorization tuples, each containing a chain_id, address, nonce, and ECDSA signature. Each tuple is verified (including signature recovery and nonce checks), and if valid, the authorized account’s code is temporarily replaced with a special delegation indicator: 0xef0100 || address. This signals that any code execution (e.g. via CALL, DELEGATECALL, or direct transaction execution) should redirect to the logic at the specified address. Notably, this delegation affects execution but not code inspection: CODESIZE and CODECOPY opcodes reflect the delegated contract’s code, while EXTCODESIZE on the delegating account returns only the size of the delegation marker (23 bytes). Delegations persist even if the transaction reverts, and multiple tuples from the same account are resolved using the last valid one.
Example of batch transaction
We’ll deploy two contracts MultiDelegationInvoker and Invoked (attached both addresses) and designate two distinct EOAs as authorities. Each authority will sign a delegation granting Invoked’s bytecode permission to execute under their identity. Finally, a single EIP‑7702 transaction will invoke MultiDelegationInvoker, which verifies those signatures, injects the delegated code stubs, and routes execution to the Invoked contract all in one atomic call.
Note: We will use Polygon Amoy testnet
smart-contracts perspective
Invoked smart contract:
pragma solidity ^0.8.24;
contract Invoked {
event Pinged(address sender);
function ping() external {
emit Pinged(msg.sender);
}
}Target smart-contract for validation and authorization:
pragma solidity ^0.8.24;
contract MultiDelegationInvoker {
event PingSuccess(address from);
event PingStart(address from);
function triggerPings(address[] calldata froms) external {
emit PingStart(msg.sender);
for (uint i = 0; i < froms.length; i++) {
address from = froms[i];
bytes memory code = new bytes(23);
// Load the first 23 bytes of the code at `from`
assembly {
extcodecopy(from, add(code, 0x20), 0, 23)
}
// Check if it starts with 0xef0100
if (
code.length == 23 &&
uint8(code[0]) == 0xef &&
uint8(code[1]) == 0x01 &&
uint8(code[2]) == 0x00
) {
// After verifying code starts with 0xef0100
address someModule;
assembly {
someModule := shr(96, mload(add(code, 0x23)))
}
(bool ok, ) = someModule.call(abi.encodeWithSignature(”ping()”));
require(ok, “Ping failed”);
emit PingSuccess(from);
} else {
revert(”Not delegated or invalid delegation format”);
}
}
}
}Lets understand whats going on:
for (uint i = 0; i < froms.length; i++) {
address from = froms[i];
…
}froms holds the EOAs you pre‑authorized. You’re going to check each one’s “code” for the 23‑byte marker.
bytes memory code = new bytes(23);
assembly {
extcodecopy(from, add(code, 0x20), 0, 23)
}new bytes(23) allocates a 32‑byte word for length (0x17) and another 32 bytes for data. extcodecopy(from, …, 0, 23) pulls the first 23 bytes of from’s code (the stub you injected!) into code[0..22]. (remember (0xef0100 || address) is written to the authorized address code)
if (
code.length == 23 &&
uint8(code[0]) == 0xef &&
uint8(code[1]) == 0x01 &&
uint8(code[2]) == 0x00
) {
…
} else {
revert(”Not delegated or invalid delegation format”);
}check the code’s prefix to be0xef0100 as explained before. If true this address got an assigned code to it.
address realModule;
assembly {
realModule := shr(96, mload(add(code, 0x23)))
}Memory layout after extcodecopy:
code[0..2]= marker bytescode[3..22]= the 20‑bytedelegateeaddress you signed over
add(code, 0x23) = pointer to code[3].
mload(...) loads 32 bytes (your 20-byte address + 12 bytes zero padding).
shr(96, …) right‑shifts out the padding, leaving exactly the 160‑bit address.
(bool ok, ) = realModule.call(
abi.encodeWithSignature(”ping()”)
);
require(ok, “Ping failed”);
emit PingSuccess(from);Executes ping() on your module contract at realModule. Inside that ping(), msg.sender is still the EOA you delegated (not the sponsor), thanks to EIP‑7702’s delegate‑call semantics.
wallet perspective
package main
import (
“context”
“crypto/ecdsa”
“fmt”
“log”
“math/big”
“strings”
“time”
“transactiontypes/account”
“github.com/ethereum/go-ethereum/accounts/abi”
“github.com/ethereum/go-ethereum/common”
“github.com/ethereum/go-ethereum/core/types”
“github.com/ethereum/go-ethereum/crypto”
“github.com/ethereum/go-ethereum/ethclient”
“github.com/ethereum/go-ethereum/rlp”
“github.com/holiman/uint256”
)
const (
// Public RPC URL for Polygon Amoy Testnet
NodeRPCURL = “https://polygon-amoy.drpc.org”
AmoyChainID = 80002 // Polygon Amoy Testnet Chain ID
)
func main() {
ctx := context.Background()
client, err := ethclient.Dial(NodeRPCURL)
if err != nil {
log.Fatal(”RPC connection failed:”, err)
}
// Load account
acc2Addr, acc2Priv := account.GetAccount(2)
acc1Addr, acc1Priv := account.GetAccount(1)
to := common.HexToAddress(”0x87581c71b3693062f4d3e34617c3919ec1abf39b”)
// Define contract and parameters
moduleAddr := common.HexToAddress(”0x4f9c96915a9ce8cd5eb11a2c35ab587fc97d5126”)
froms := []common.Address{
*acc1Addr,
*acc2Addr,
}
// Build calldata
contractAbiJson := `[{”anonymous”: false,”inputs”: [{”indexed”: false,”internalType”: “address”,”name”: “from”,”type”: “address”}],”name”: “PingStart”,”type”: “event”},{”anonymous”: false,”inputs”: [{”indexed”: false,”internalType”: “address”,”name”: “from”,”type”: “address”}],”name”: “PingSuccess”,”type”: “event”},{”inputs”: [{”internalType”: “address[]”,”name”: “froms”,”type”: “address[]”}],”name”: “triggerPings”,”outputs”: [],”stateMutability”: “nonpayable”,”type”: “function”}]`
parsedAbi, _ := abi.JSON(strings.NewReader(contractAbiJson))
data, err := parsedAbi.Pack(”triggerPings”, froms)
if err != nil {
log.Fatal(”ABI pack error:”, err)
}
// Nonce and gas
baseNonce2, err := client.PendingNonceAt(ctx, *acc2Addr)
if err != nil {
log.Fatal(”Nonce fetch failed:”, err)
}
nonce2 := baseNonce2 + 1
nonce1, err := client.PendingNonceAt(ctx, *acc1Addr)
if err != nil {
log.Fatal(”Nonce fetch failed:”, err)
}
gasTipCap, err := client.SuggestGasTipCap(ctx)
if err != nil {
log.Fatal(”Failed to fetch gas tip cap:”, err)
}
baseFee, err := client.SuggestGasPrice(ctx)
if err != nil {
log.Fatal(”Failed to fetch base fee:”, err)
}
gasFeeCap := new(big.Int).Add(baseFee, gasTipCap)
// Create EIP-712-style signature for delegation
sig2, err := signEIP7702Delegation(acc2Priv, AmoyChainID, moduleAddr, nonce2)
if err != nil {
log.Fatal(”Signature failed:”, err)
}
sig1, err := signEIP7702Delegation(acc1Priv, AmoyChainID, moduleAddr, nonce1)
if err != nil {
log.Fatal(”Signature failed:”, err)
}
r2 := new(big.Int).SetBytes(sig2[:32])
s2 := new(big.Int).SetBytes(sig2[32:64])
v2 := uint8(sig2[64])
r1 := new(big.Int).SetBytes(sig1[:32])
s1 := new(big.Int).SetBytes(sig1[32:64])
v1 := uint8(sig1[64])
// Build EIP-7702 TxWithDelegation
delegation := types.SetCodeTx{
ChainID: uint256.NewInt(AmoyChainID),
Nonce: baseNonce2,
GasTipCap: uint256.MustFromBig(gasTipCap),
GasFeeCap: uint256.MustFromBig(gasFeeCap),
Gas: 120000,
To: to,
Data: data,
AuthList: []types.SetCodeAuthorization{
{
ChainID: *uint256.NewInt(AmoyChainID),
Address: moduleAddr,
Nonce: nonce1,
R: *uint256.MustFromBig(r1),
S: *uint256.MustFromBig(s1),
V: v1,
},
{
ChainID: *uint256.NewInt(AmoyChainID),
Address: moduleAddr,
Nonce: nonce2,
R: *uint256.MustFromBig(r2),
S: *uint256.MustFromBig(s2),
V: v2,
},
},
}
fullTx := types.NewTx(&delegation)
signedTx, err := types.SignTx(fullTx, types.LatestSignerForChainID(big.NewInt(AmoyChainID)), acc2Priv)
if err != nil {
log.Fatal(”Signing failed:”, err)
}
err = client.SendTransaction(ctx, signedTx)
if err != nil {
log.Fatal(”Tx failed:”, err)
}
fmt.Println(”EIP-7702 Tx sent:”, signedTx.Hash().Hex())
time.Sleep(10 * time.Second)
receipt, err := client.TransactionReceipt(ctx, signedTx.Hash())
if err != nil {
fmt.Println(”Waiting...”)
} else {
fmt.Println(”Tx mined in block”, receipt.BlockNumber)
}
}
// signEIP7702Delegation creates a hash of (chainID, from, nonce) and signs it
func signEIP7702Delegation(priv *ecdsa.PrivateKey, chainID int64, from common.Address, nonce uint64) ([]byte, error) {
// Encode [chain_id, address, nonce] in RLP
msgPayload, err := rlp.EncodeToBytes([]interface{}{
big.NewInt(chainID),
from,
big.NewInt(int64(nonce)),
})
if err != nil {
return nil, err
}
// Prepend MAGIC 0x05
prefixed := append([]byte{0x05}, msgPayload...)
// Hash
msgHash := crypto.Keccak256Hash(prefixed)
return crypto.Sign(msgHash.Bytes(), priv)
}Whats going on:
Connect & prepare
Dial into the Polygon Amoy testnet via RPC.
Load two EOAs (
acc1,acc2) (their addresses & private keys) from your local wallet helper.
Set target contracts
tois the smart-contract address you’re calling.moduleAddris the implementation contract you’ll delegate into.
Build the calldata
Define the minimal ABI for
triggerPings(address[]).Pack your two authority addresses (
froms = [acc1, acc2]) intodata.
Fetch nonces & gas parameters
Read
acc2’s pending nonce (baseNonce2) for the transaction, then compute its post‐increment nonce (nonce2 = baseNonce2 + 1) for the delegation entry.Read
acc1’s pending nonce (nonce1) for its delegation.Suggest EIP‑1559 tip and base fee to compute
gasFeeCap.
Generate EIP‑7702 signatures
Each authority signs the tuple
(chainID, moduleAddr, nonce)viasignEIP7702Delegation():acc1 uses
nonce1acc2 (the tx sender) uses
nonce2
Assemble the SetCodeTx
&types.SetCodeTx{
ChainID: AmoyChainID,
Nonce: baseNonce2,
To: to,
Data: data,
Gas: 120_000, // some hardcoded fee to not write too much code
AuthList: []SetCodeAuthorization{
{ Address: moduleAddr, Nonce: nonce1, R:…, S:…, V:… },
{ Address: moduleAddr, Nonce: nonce2, R:…, S:…, V:… },
},
}Sign & broadcast
Sign the full transaction with acc2Priv using
SignTx.Send it via
client.SendTransaction.Poll for the receipt to confirm mining.
The output should look like:
Press enter or click to view image in full size


