understand what is EIP-7702 and why is EIP-7702 in Pectra :)
Special thanks to NIC Lin and Chang-Wu Chen for the feedback and review.
This article aims to help people understand the basic concept behind this proposal.
Some example code would be included in this article, mainly focusing on the exp-0001, an experiment project that ithaca did in EIP-7702.
So first, what is EIP-7702?? And what kind of problem does it want to solve?
One of the goal of Ethereum is to abstract current two kind of accounts:
EIP-7702 act as a bridge between EOA and smart contract, which enable an EOA to act like a smart contract.
For example, when an EOA interact with a smart contract looks like:
After EIP-7702 set up, an interaction would be like:
Okay, but what is the problem with EOA?
Single point of failure: EOAs are controlled by only one private key and lack of mechanism for recovery or multi-sigature protection.
Limited Functionality: EOAs can only send transactions but not execute code. This is bad since it cannot:
Gas Fee Payment: EOAs can only pay the gas in ETH.
All in all, current EOA has a worse UX design. And thus several account abstraction ideas came out.
Account Abstractions had been discussed for years. And there a quite a few Account Abstraction solution appeared from 2021–2024.
One of the motivations of EIP-7702 is that current (before Pectra Hardfork) account design. This EIP blurred the line between an EOA or a smart contract, and this is also the end goal that the Ethereum want to achieve — RIP-7560 (aka the native account abstraction that supports from the consensus layer).
What and How does it do?
My imagination of the delegation designator is Sutando in Jojo.
While executing transactions through delegation designator be like:
Currently we have 4 slots in an EOA in Ethereum:
The code in the EOA account is empty; that is, there's no code value storing in an EOA account. EIP-7702 introduces a new way to set code field by sending a new type of EIP-2178 transaction, "Set Code Transaction".
The transaction code is different from a normal transaction from EIP-1559 (tx type = 0x02), it has to send more data in the transaction payload.
In, this tx, an EOA has to sign the message with these additional four infomation:
Now the transaction payload looks like:
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 the authorization_list looks like:
authorization_list = [
[chain_id_1, address_1, nonce_1, y_parity_1, r_1, s_1],
[chain_id_2, address_2, nonce_2, y_parity_2, r_2, s_2],
...
]
After signing, the transaction is generated with the authorization List included. What this actually means is: “As a EOA, you authorized/allows some code to live in your account.”
However, at the moment, EIP-3607 stands out and say: "No, I'm not gonna allow you to do so", since if you have some code in your account, it is impossible for you to originate a transaction.
Here EIP-7702 introduce another method to make it work, which is “Delegation Designator”. What is done in the authorization is that the code is actually set into a target address like this:
0xef + 0100 + target address
By doing so, EIP-3607 is satisfied, and what's even better, the EOA doesn't have to store the whole bytecode of its contract; we can just use a deployed contract to convert your EOA into a Smart Contract Account!
* EIP-3541 prohibit contract address to have “0xef” prefix. The “0xef” is set to be a EOF(=Ethereum Object Format) in Etheruem.
* The original design is to store contract bytecode into the code field; This is refined to only store a short address pointing to the contract.
* authorization_list is a list of tuples that store the address to code which the signer desires to execute in the context of their EOA.
* with chain_id = 0, you can set up delegation designator to all EIP-7702 chains.
It would be hard to understand WHY do we need EIP-7702 without some examples. With EIP-7702, we could do:
more examples like:
All of these sounds pretty good and would give the users a better UX! But how should we actually leverage EIP-7702?
It is also hard to understand it without implementation :D. So I also read some code in ithaca odyssey, a L2 that already included EIP-7702 in their execution layer. They also have a well-written blog and you can give it a look.
In this section, we’ll walk through a simplified version of the EIP-7702 implementation. We’ll explain how an EOA can delegate code execution to a smart contract, how authorization works, and how batch transactions are executed.
In exp-0001, ExperimentDelegation.sol is used as the Delegation Designator. And P256 and WebAuthenP256 publicKey (curve: secp256r1) are stored in the contract.
This means: You can use a passkey (i.e., FaceId, TouchId) to sign a transaction without your private key.
Here’s a simplified version of it.
contract ExperimentDelegation is MultiSendCallOnly {
////////////////////////////////////////////////////////////////////////
// Data Structures
////////////////////////////////////////////////////////////////////////
/// @notice A Key that can be used to authorize calls.
/// @custom:property authorized - Whether the key is authorized.
/// @custom:property publicKey - ECDSA public key.
/// @custom:property expiry - Unix timestamp at which the key expires.
struct Key {
bool authorized;
uint256 expiry;
ECDSA.PublicKey publicKey;
}
// ...
////////////////////////////////////////////////////////////////////////
// Functions
////////////////////////////////////////////////////////////////////////
/// @notice List of keys associated with the Authority.
Key[] public keys;
/// @notice Authorizes a new public key on behalf of the Authority, provided the Authority's signature.
/// @param publicKey - The public key to authorize.
/// @param expiry - The Unix timestamp at which the key expires.
/// @param signature - EOA secp256k1 signature over the public key.
function authorize(
ECDSA.PublicKey calldata publicKey,
uint256 expiry,
ECDSA.RecoveredSignature calldata signature
) public returns (uint32 keyIndex) {
bytes32 digest = keccak256(
abi.encodePacked(nonce++, publicKey.x, publicKey.y, expiry)
);
address signer = ecrecover(
digest,
signature.yParity == 0 ? 27 : 28,
bytes32(signature.r),
bytes32(signature.s)
);
if (signer != address(this)) revert InvalidSignature();
Key memory key = Key({
authorized: true,
expiry: expiry,
publicKey: publicKey
});
keys.push(key);
return uint32(keys.length - 1);
}
// ...
/// @notice Executes a set of calls on behalf of the Authority, provided a WebAuthn-wrapped P256 signature over the calls, the WebAuthn metadata, and an invoker index.
/// @param calls - The calls to execute.
/// @param signature - The WebAuthn-wrapped P256 signature over the calls: `p256.sign(keccak256(nonce ‖ calls))`.
/// @param metadata - The WebAuthn metadata.
/// @param keyIndex - The index of the authorized public key to use.
function execute(
bytes memory calls,
ECDSA.Signature memory signature,
WebAuthnP256.Metadata memory metadata,
uint32 keyIndex
) public {
bytes32 challenge = keccak256(abi.encodePacked(nonce++, calls));
Key memory key = keys[keyIndex];
if (!key.authorized) revert KeyNotAuthorized();
if (key.expiry > 0 && key.expiry < block.timestamp) revert KeyExpired();
if (!WebAuthnP256.verify(challenge, metadata, signature, key.publicKey))
revert InvalidSignature();
multiSend(calls);
}
}
Two primary two functions are used in the contract:
authorize :
execute :
So first, we need to sign the message of this SetCodeTransaction. With viem::signAuthorization function, we can simply send a walletClient and sign the transaction. This message indicates that your EOA approves the delegation of code execution to the designated contract:
import { walletClient } from './client'
const authorization = await walletClient.signAuthorization({
contractAddress: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', // the delegation designator
})
// where the authorization_list looks like:
// [{
// chainId: 1,
// contractAddress: "0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2",
// nonce: 1,
// r: "0xf507fb8fa33ffd05a7f26c980bbb8271aa113affc8f192feba87abe26549bda1",
// s: "0x1b2687608968ecb67230bbf7944199560fa2b3cffe9cc2b1c024e1c8f86a9e08",
// yParity: 0,
// }]
const hash = await walletClient.sendTransaction({
authorizationList: [authorization],
data: '0xdeadbeef',
to: walletClient.account.address,
})
However, signing the message is not enough. You still have to send a transaction on chain to set up the code for your EOA.
Once the authorization is signed and the delegation code should be injected into your EOA.
// Sign an EIP-7702 authorization to inject the ExperimentDelegation contract
// onto the EOA.
const authorization = await signAuthorization(client, {
account,
contractAddress: ExperimentDelegation.address,
delegate: true,
})
// Send an EIP-7702 contract write to authorize the WebAuthn key on the EOA.
const hash = await writeContract(client, {
abi: ExperimentDelegation.abi,
address: account.address,
functionName: 'authorize',
args: [
{
x: publicKey.x,
y: publicKey.y,
},
expiry,
{
r: BigInt(signature.r),
s: BigInt(signature.s),
yParity: signature.yParity,
},
],
authorizationList: [authorization],
account: null, // defer to sequencer to fill
})
After authorize(), the contract field on Odyssey blockchain explorer would have this address:
0xef010035202a6e6317f3cc3a177eeee562d3bcda4a6fcc
As you can see, the delegation designator is set to this address, and the delegation contract is deployed with address: 35202a6e6317f3cc3a177eeee562d3bcda4a6fcc (see the implemenation)
After authorization, you can execute EOA through the delegation designator. And because the delegation contract already implement multiSend. So you can execute multiple calls within the same transaction.
// Fetch the next available nonce from the delegated EOA's contract.
const nonce = await readContract(client, {
abi: ExperimentDelegation.abi,
address: account.address,
functionName: 'nonce',
})
// Encode calls into format required by the contract.
const calls_encoded = concat(
calls.map((call) =>
encodePacked(
['uint8', 'address', 'uint256', 'uint256', 'bytes'],
[
0,
call.to,
call.value ?? 0n,
BigInt(size(call.data ?? '0x')),
call.data ?? '0x',
],
),
),
)
// Compute digest to sign for the execute function.
const digest = keccak256(
encodePacked(['uint256', 'bytes'], [nonce, calls_encoded]),
)
// Sign the digest with the authorized WebAuthn key.
const { signature, webauthn } = await sign({
hash: digest,
credentialId: account.key.id,
})
// Extract r and s values from signature.
const r = BigInt(slice(signature, 0, 32))
const s = BigInt(slice(signature, 32, 64))
// Execute calls.
return await writeContract(client, {
abi: ExperimentDelegation.abi,
address: account.address,
functionName: 'execute',
args: [calls_encoded, { r, s }, webauthn, 0],
account: null, // defer to sequencer to fill
})
This is what it looks like while sending the tokens to Alice and Bob in exp-0001 (see this)
Yes they are compatible.
Yes, see those Implementations below.
Yes and no. If you can authorize your wallet with another way like passkey, then you can use your passkey to sign the message instead of taking out your private key and sign the tx. or you can just throw your private key away and make it vanish forever.
The reason for “NO” is because the account should be “self-custodied”, which means if you are not able to keep your secret key, you can turn to use other service instead :).
We can use the same EOA secret key to sign the authorization transaction again, which will overwrite the previous delegation.
No, since in the proposal it has: It will in the case of multiple tuples for the same authority, set the code using the address in the last valid occurrence. So, no, there will be only one active delegation designator at one time.
Delegating to a wallet proxy, you can set chain Id to 0 for validity on all EIP-7702 chains. And Wallet maintainers can also hard code a single EIP-7702 authorization message into their wallet so that cross-chain code compatibility is never a problem.
EIP-7702 marks a significant step toward bridging the gap between EOAs and smart contract accounts on Ethereum. By enabling an EOA “adopt” smart contract code via a delegation designator, this proposal makes current EOA overcome the limitations, namely, their reliance on single private key (no multi-sig recovery or other authentication method), lack of native programmability or gas fee sponsorship. With EIP-7702, users can now perform advanced operations such as batch transactions, custom authentications (like passkeys), and even multi-sig recoveries, all while keeping control of their private keys.
As we move closer to the Pectra hardfork in 2025, the evolution of account abstraction through EIP-7702 is one of the most promising developments in Ethereum’s roadmap. Although the code shows a great example to design in EIP-7702, I believe there will be more EIP-7702 related account and wallet coming out :).
Linktree:
:) Donate to me: 0xF9B1810cFde1045d36eA198a07045a9F2bb47F32
EIP-7702 Introduction was originally published in Taipei Ethereum Meetup on Medium, where people are continuing the conversation by highlighting and responding to this story.
【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。