<100 subscribers
Share Dialog
Ethereum is on the verge of the Pectra upgrade, an update of significant importance that will introduce several key Ethereum Improvement Proposals (EIPs). Among them, EIP-7702 revolutionizes Ethereum External Accounts (EOAs) by blurring the lines between EOAs and Contract Accounts (CAs). Following EIP-4337, this is another crucial step toward native account abstraction, bringing a new interaction model to the Ethereum ecosystem.
Currently, Pectra has been deployed on the test network and is expected to go live on the mainnet soon. This article will provide an in-depth analysis of the implementation mechanism of EIP-7702, explore its potential opportunities and challenges, and offer practical guides for different participants.
Protocol Analysis
Overview
EIP-7702 introduces a new type of transaction that allows EOAs to specify a smart contract address and set code for it. This enables EOAs to execute code like smart contracts while retaining the ability to initiate transactions. This feature endows EOAs with programmability and composability, allowing users to implement functions such as social recovery, permission control, multisignature management, zk verification, subscription payments, transaction sponsorship, and batch processing within EOAs. Notably, EIP-7702 is perfectly compatible with the smart contract wallets implemented by EIP-4337, and their seamless integration greatly simplifies the development and application of new features.
The specific implementation of EIP-7702 introduces a transaction type with the identifier SET_CODE_TX_TYPE (0x04), with the following data structure:
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])
The authorization_list field is defined as:
authorization_list = [[chain_id, address, nonce, y_parity, r, s], ...]
In the new transaction structure, all fields except authorization_list follow the same semantics as in EIP-4844. This field is a list type, and the list can contain multiple authorization entries. In each authorization entry:
The chain_id field indicates the chain on which the authorization takes effect.
The address field indicates the target address of the delegation.
The nonce field must match the current nonce of the authorizing account.
The y_parity, r, and s fields are the signature data of the authorizing account.
The authorization_list field in a transaction can contain multiple authorization entries signed by different authorizing accounts (EOAs), meaning that the transaction initiator can be different from the authorizer to enable gas payment on behalf of the authorizer.
Implementation
When an authorizer signs authorization data, they first RLP-encode the chain_id, address, and nonce. They then hash the encoded data together with the MAGIC number using keccak256 to obtain the data to be signed [1]. Finally, the authorizer signs the hashed data with their private key to obtain the y_parity, r, and s data. The MAGIC (0x05) is used as a domain separator to ensure that the results of different types of signatures do not conflict.
// Go-ethereum/core/types/tx_setcode.go#L109-L113
func (a *SetCodeAuthorization) sigHash() common.Hash {
return prefixedRlpHash(0x05, []any{
a.ChainID,
a.Address,
a.Nonce,
})
}
It is important to note that if the chain_id specified by the authorizer is 0, it means that the authorizer allows [2] the authorization to be replayed on all EVM-compatible chains that support EIP-7702 (provided the nonce also matches).
// Go-ethereum/core/state_transition.go#L562
if !auth.ChainID.IsZero() && auth.ChainID.CmpBig(st.evm.ChainConfig().ChainID) != 0 {
return authority, ErrAuthorizationWrongChainID
}
After the authorizer signs the authorization data, the transaction initiator aggregates it in the authorization_list field, signs it, and broadcasts the transaction via RPC. Before a transaction is included in a block and executed, the Proposer first performs a pre-check [3] on the transaction, including a mandatory check on the to address to ensure that the transaction is not a contract creation transaction. In other words, when sending an EIP-7702 type transaction, the to address of the transaction cannot be empty [4].
// Go-ethereum/core/state_transition.go#L388-L390
if msg.To == nil {
return fmt.Errorf("%w (sender %v)", ErrSetCodeTxCreate, msg.From)
}
Additionally, this type of transaction enforces that the authorization_list field must contain at least one authorization entry. If multiple authorization entries are signed by the same authorizer, only the last authorization entry will take effect.
// Go-ethereum/core/state_transition.go#L391-L393
if len(msg.SetCodeAuthorizations) == 0 {
return fmt.Errorf("%w (sender %v)", ErrEmptyAuthList, msg.From)
}
Subsequently, during transaction execution, the node first increments the nonce of the transaction initiator and then applies the applyAuthorization operation to each authorization entry in the authorization_list. In the applyAuthorization operation, the node first checks the nonce of the authorizer and then increments the nonce of the authorizer. This means that if the transaction initiator and the authorizer are the same user (EOA), the nonce value should be incremented by 1 when signing the authorization transaction.
// Go-ethereum/core/state_transition.go#L489-L497
func (st *stateTransition) execute() (*ExecutionResult, error) {
...
st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeEoACall)
// Apply EIP-7702 authorizations.
if msg.SetCodeAuthorizations != nil {
for _, auth := range msg.SetCodeAuthorizations {
// Note errors are ignored, we simply skip invalid authorizations here.
st.applyAuthorization(&auth)
}
}
...
}
// Go-ethereum/core/state_transition.go#L604
func (st *stateTransition) applyAuthorization(auth *types.SetCodeAuthorization) error {
authority, err := st.validateAuthorization(auth)
...
st.state.SetNonce(authority, auth.Nonce+1, tracing.NonceChangeAuthorization)
...
}
// Go-ethereum/core/state_transition.go#L566
func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorization) (authority common.Address, err error) {
...
if auth.Nonce+1 < auth.Nonce {
return authority, ErrAuthorizationNonceOverflow
}
...
}
When a node applies an authorization entry, if any error occurs, that entry will be skipped, and the transaction will not fail. Other authorization entries will continue to be applied to ensure that there is no DoS risk in batch authorization scenarios.
// Go-ethereum/core/state_transition.go#L494
for _, auth := range msg.SetCodeAuthorizations {
// Note errors are ignored, we simply skip invalid authorizations here.
st.applyAuthorization(&auth)
}
After the authorization application is complete, the code field of the authorizer's address will be set to 0xef0100 || address, where 0xef0100 is a fixed identifier, and address is the target address of the delegation. Due to the restriction of EIP-3541, users cannot deploy contract code starting with the 0xef byte through conventional means, ensuring that this identifier can only be deployed by transactions of type SET_CODE_TX_TYPE (0x04).
// Go-ethereum/core/state_transition.go#L612
st.state.SetCode(authority, types.AddressToDelegation(auth.Address))
// Go-ethereum/core/types/tx_setcode.go#L45
var DelegationPrefix = []byte{0xef, 0x01, 0x00}
func AddressToDelegation(addr common.Address) []byte {
return append(DelegationPrefix, addr.Bytes()...)
}
After authorization, if the authorizer wishes to remove the authorization, they simply need to set the target address of the delegation to the zero address.
The new transaction type introduced by EIP-7702 allows authorizers (EOAs) to execute code like smart contracts while retaining the ability to initiate transactions. Compared to EIP-4337, this provides users with an experience closer to native account

Richard.M.Lu
No comments yet