Let's say you're trying to execute a complex all-in-one atomic transaction that requires swapping tokens, adding liquidity, and staking rewards. But then, your regular Ethereum wallet EOA can't do batch transactions. So, you'll need to sign three separate transactions, pay gas three times, which is a no no, and then pray that MEV bots don't sandwich you between steps.
This is one of the heated conversation in the EVM
ecosystem lately, how exactly do we give regular wallets superpowers without breaking everything all at the same time?
Two EIPs
came out of this challenge, EIP-3074
and subsequently EIP-7702
. Both looking to close the gap between simple EOA
and smart contract accounts
, but they take different approaches to delegating authority. One offers persistent power in which things can go very wrong and your wallets gets drained. The other can give you both persistent delegation and temporary delegation that could vanish the moment your transaction is executed on-chain.
Let's talk about why this difference matters and how Vitalik's proposal might have just saved Ethereum from a potential security nightmare.
Before we get into the delegation topic, there's an important piece of infra that makes all of this possible which is EIP-2718
. Think of it as Ethereum's shipping container system for transactions.
// before eip-2718 all transactions looked the same
const legacyTx = rlp([nonce, gasPrice, gasLimit, to, value, data, v, r, s]);
// after eip-2718 transactions can now be typed
const typedTx = TransactionType || TransactionPayload;
EIP-2718
introduces Typed Transaction Envelopes
where TransactionType || TransactionPayload
is a valid transaction. TransactionType
identifies the format of the transaction while Payload
contains the transaction contents. Now, this wrapper system means we can introduce entirely new transaction behaviors without breaking the existing functionality.
By bring in an envelope transaction type, we only need to ensure backward compatibility with existing transactions and from then on we just need to solve the much simpler problem of ensuring there is no numbering conflict between TransactionTypes
. So when EIP-7702
needed to introduce its temporary delegation mechanism, it could simply define a new transaction type for e.g, 0x04
without requiring any changes to the EVM
itself.
EIP-3074 took a bold approach to delegation, giving EOAs
the ability to permanently delegate authority to smart contracts using two new EVM
opcodes
- AUTH
and AUTHCALL
.
The mechanism is straight forward
// user signs an auth message
bytes32 commit = keccak256(abi.encode(invokerAddress, batchActions));
bytes memory signature = signMessage(userPrivateKey, commit);
// invoker contract calls AUTH opcode
AUTH(commit, signature.v, signature.r, signature.s);
// This sets the 'authorized' context variable to user's address
// now invoker can make calls as the user
AUTHCALL(gasLimit, targetContract, 0, 0, calldata, calldataLength, 0, 0);
AUTH
takes four parameters commit
and the signature's yParity
, r
, and s
and if the signature is valid and the signer address equals the authority, the context variable authorized is set to that authority. AUTHCALL
then allows the authorized invoker contract to send transactions, with CALLER
being set to the authorized address rather than the invoker.
Now, imagine an invoker contract that could handle batched transactions
contract BatchInvoker {
struct BatchCall {
address target;
bytes data;
uint256 value;
}
function executeBatch(
BatchCall[] calldata calls,
bytes32 commit,
uint8 v, bytes32 r, bytes32 s
) external {
// authorize the user
AUTH(commit, v, r, s);
// Execute all calls as the user
for (uint i = 0; i < calls.length; i++) {
AUTHCALL(
gasleft(),
calls[i].target,
calls[i].value,
0, // valueExt (must be 0 for now)
// ... calldata parameters
);
}
}
}
In as much as this is exciting, these two opcodes
brought up a number of security concerns with the EVM
community. The context variable authorized
persists throughout a single frame of execution of the contract, but is not passed through any calls including DELEGATECALL
. This basically means the authorization is active for the entire transaction scope.
The real problem isn't the temporary nature within a transaction, it's what happens when you have widely used invoker contracts. If everyone will be using smart accounts eventually, the AUTH
and AUTHCALL
opcodes
can be a huge technical burden and add complexity to the EVM
itself.
What happens when a popular invoker contract like the one above has a bug? Suddenly, every user who's ever authorized that contract would possible be at a rist. One malicious update, one missed edge case, one integer overflow and that's all, a mass drainage starts.
The EVM
ecosystem realized that this created a weird dynamic. The security model of EIP-3074
relies heavily on the invoker contract. Since multiple users might rely on the same invoker logic, a vulnerability or exploit in one invoker could compromise assets or permissions of many users all at once, which is why EIP-3074
was withdrawn.
Now, EIP-7702 proposed by Vitalik in May 2024 went mainnet
via the pectra upgrade
in May 2025 and introduces a new streamlined approach, allowing EOAs
to temporarily function like smart contracts for the duration of a single transaction, which is a big deal!
Instead of new opcodes
, EIP-7702
leverages EIP-2718's
transaction envelope system to introduce a new transaction type
interface EIP7702Transaction {
chainId: number;
nonce: number;
maxPriorityFeePerGas: bigint;
maxFeePerGas: bigint;
gasLimit: bigint;
to: string;
value: bigint;
data: Uint8Array;
accessList: AccessList;
authorizationList: Authorization[];
}
interface Authorization {
chainId: number;
address: string; // the address of the contract code to delegate to
nonce: number;
yParity: 0 | 1;
r: Uint8Array;
s: Uint8Array;
}
For each authorization object, 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.
This is where many people get confused. EIP-7702
has evolved since its original draft. Initially, the proposal was designed to make EOAs
temporarily smart for just one transaction. But the final version gives you control over how long the delegation lasts.
function processEIP7702Transaction(tx) {
// verify all auth signatures
for (const auth of tx.authorizationList) {
const signer = recoverSigner(auth);
if (signer !== getAddressFromAuth(auth)) {
throw new Error("Invalid authorization");
}
}
// set delegation pointers (these persist after transaction)
for (const auth of tx.authorizationList) {
const eoaAddress = getAddressFromAuth(auth);
// set delegation pointer - 0xef0100 || contract_address
const delegationPointer = concat(['0xef0100', auth.address]);
setCode(eoaAddress, delegationPointer);
// nowm this delegation PERSISTS unless explicitly cleared
}
// execute the transaction normally
executeTransaction(tx);
// no auto cleanup, delegation stays active
}
NOTE: If a 7702
transaction execution fails, the processed delegation indicators are not rolled back and persists unless explicitly cleared by setting the delegation address to 0x0
in a subsequent 7702
transaction.
This means you have three delegation patterns:
Batching Contract
for a day, the delegation itself is persistent until they clear it.The good thing about EIP-7702
is that it gives you the option to choose. Want temporary delegation for a one-off complex transaction? You can do that. Want to persistently upgrade your EOA
to act like a smart contract wallet? You can do that too.
For temporary delegation, here's an example
// one off delegation
const tempDelegationTx = {
type: 0x04,
authorizationList: [{
address: batchExecutorContract,
// ... auth details
}],
to: userAddress,
data: encodeBatchCall([
// execute your complex operation
swapTokens(),
addLiquidity(),
stakeRewards(),
])
};
And for Persistent Delegation, here's an example
// upgrade EOA to smart wallet
const persistentDelegationTx = {
type: 0x04,
authorizationList: [{
address: smartWalletImplementation,
// ... auth details
}],
to: userAddress,
data: encodeCall("initializeSmartFeatures", [])
// this delegation persists for future txns
};
// later txns can now use smart wallet features
const futureTransaction = {
to: userAddress, // your EOA will now execute smart wallet code
data: encodeCall("batchExecute", [multipleOperations])
// no new auth needed
};
The key difference between EIP-3074
and EIP-7702
isn't just temporary vs. persistent, it's about risk distribution and control. EIP-3074
creates a shared point of failure where one popular contract's problem could affect thousands of users. EIP-7702
isolates each transaction, so bugs could only affect that specific transaction. EIP-7702
is fully compatible with existing ERC-4337
, requiring no new opcodes
. This basically means, wallets can adopt it gradually without breaking any existing systems.
The critical insight is that wallets
, not dApps
, manage delegations. So the general agreement is to only allow wallets to manage delegations which means:
If you're building an application that will support EIP-7702
, few technical things to note
// old: simple EOA transaction
const tx = {
to: contractAddress,
data: encodedCall,
// ... standard fields
};
// new way with eip-7702 delegation
const tx = {
type: 0x04,
to: userAddress, // calling user's own address
data: encodeBatchCall([
contract.interface.encodeFunctionData("swap", [tokenA, tokenB, amount]),
contract.interface.encodeFunctionData("addLiquidity", [lpTokens]),
contract.interface.encodeFunctionData("stake", [lpAmount])
]),
authorizationList: [{
address: batchExecutorImplementation,
// ... authorization signature
}],
// ... standard fields
};
7702
transaction must be sent to set delegation back to 0x0
. This means your gas estimation needs to account forEIP-7702
enables EIP-1271
signature verification for EOAs
. This means your upgraded EOA
can sign messages using alternative methods like passkeys
, while still maintaining backward compatibility.// your EOA can now implement smart signature verification
function isValidSignature(bytes32 hash, bytes signature) external view returns (bytes4) {
// can verify passkey signatures, multisig, etc.
if (verifyPasskeySignature(hash, signature)) {
return 0x1626ba7e; // EIP-1271 magic value
}
return 0xffffffff;
}