EIP-7702 引入了一种新的交易类型,允许 EOA 账户在交易执行期间临时地表现得像智能合约,无需部署或改变长期状态。它通过授权列表和短期的委托存根实现,从而提升用户体验,支持批量交易、交易赞助和权限委托等功能。
EIP-7702 提出了一种强大而简单的解决方案:通过一种新的交易类型,赋予 EOA 临时行为像智能合约的能力。在这种交易中,用户可以在交易执行期间为其账户附加自定义代码。无需部署,没有长期状态更改,也没有额外的 Gas 成本。
EIP-7702 的核心是一个轻量级的委托模型。交易包含一个授权元组列表,其格式如下:
[chain_id, address, nonce, y_parity, r, s]
对于每个元组,一个委托指示符 (0xef0100 || address) 被写入授权账户的代码中。所有执行代码操作都必须加载并执行该委托指向的代码。
0xef0100 是一个特殊标记,表示“此账户委托其行为”。RLP 编码的交易结构如下:
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])
其中 authorization_list 为:
authorization_list = [[chain_id, address, nonce, y_parity, r, s], ...]
注意:外部交易的 chain_id、nonce、max_priority_fee_per_gas、max_fee_per_gas、gas_limit、destination、value、data 和 access_list 字段遵循 EIP-4844 的相同语义。这意味着空的目标地址是无效的。
EIP-7702 带来了多项关键的用户体验改进,这些改进此前需要完整的账户抽象或外部基础设施:
通过引入一种原生方式,让 EOA 能够临时将其行为委托给可重用的智能合约逻辑,EIP-7702 为更安全、更灵活的钱包开辟了一条道路,而无需进行协议大修。
在执行交易之前,EIP-7702 会处理一个可选的授权元组列表,每个元组包含 chain_id、address、nonce 和 ECDSA 签名。每个元组都会被验证(包括签名恢复和 nonce 检查),如果有效,授权账户的代码将临时替换为特殊的委托指示符:0xef0100 || address。这表明任何代码执行(例如通过 CALL、DELEGATECALL 或直接交易执行)都应重定向到指定 address 处的逻辑。值得注意的是,这种委托会影响执行,但不会影响代码检查:CODESIZE 和 CODECOPY 操作码反映的是被委托合约的代码,而对委托账户的 EXTCODESIZE 只返回委托标记的大小(23 字节)。即使交易回滚,委托也会持续存在,并且来自同一账户的多个元组将使用最后一个有效的元组进行解析。
我们将部署两个合约 MultiDelegationInvoker 和 Invoked,并指定两个不同的 EOA 作为授权方。每个授权方将签署一个委托,授予 Invoked 的字节码在其身份下执行的权限。最后,一个 EIP‑7702 交易将调用 MultiDelegationInvoker,它将验证这些签名,注入委托代码存根,并将执行路由到 Invoked 合约,所有这些都在一个原子调用中完成。
注意:我们将使用 Polygon Amoy 测试网。
Invoked 智能合约:
pragma solidity ^0.8.24;
contract Invoked {
event Pinged(address sender);
function ping() external {
emit Pinged(msg.sender);
}
}
用于验证和授权的目标智能合约:
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");
}
}
}
}
让我们理解发生了什么:
for (uint i = 0; i < froms.length; i++) {
address from = froms[i];
…
}
froms 包含你预授权的 EOA。你将检查每个 EOA 的“代码”是否存在 23 字节的标记。
bytes memory code = new bytes(23);
assembly {
extcodecopy(from, add(code, 0x20), 0, 23)
}
new bytes(23) 为长度 (0x17) 分配一个 32 字节的字,并为数据分配另一个 32 字节。extcodecopy(from, …, 0, 23) 将 from 代码的前 23 字节(你注入的存根!)拉入 code[0..22]。(请记住 (0xef0100 || address) 被写入授权地址的代码中)。
if (
code.length == 23 &&
uint8(code[0]) == 0xef &&
uint8(code[1]) == 0x01 &&
uint8(code[2]) == 0x00
) {
…
} else {
revert("Not delegated or invalid delegation format");
}
检查代码前缀是否为 0xef0100,如前所述。如果为真,则此地址已分配了代码。
address realModule;
assembly {
realModule := shr(96, mload(add(code, 0x23)))
}
extcodecopy 后的内存布局:
code[0..2] = 标记字节code[3..22] = 你签署的 20 字节 delegatee 地址add(code, 0x23) = 指向 code[3] 的指针。
mload(...) 加载 32 字节(你的 20 字节地址 + 12 字节零填充)。
shr(96, …) 右移去除填充,只留下 160 位地址。
(bool ok, ) = realModule.call(
abi.encodeWithSignature("ping()")
);
require(ok, "Ping failed");
emit PingSuccess(from);
在 realModule 处的你的模块合约上执行 ping()。在 ping() 内部,msg.sender 仍然是你委托的 EOA(而不是赞助者),这要归功于 EIP‑7702 的委托调用语义。
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)
}
发生了什么:
acc1、acc2)及其地址和私钥。to 是你要调用的智能合约地址。moduleAddr 是你将委托到的实现合约。calldatatriggerPings(address[]) 的最小 ABI。froms = [acc1, acc2])打包到 data 中。acc2 的待处理 Nonce (baseNonce2) 用于交易,然后计算其委托条目的递增后 Nonce (nonce2 = baseNonce2 + 1)。acc1 的待处理 Nonce (nonce1) 用于其委托。gasFeeCap。signEIP7702Delegation() 签署元组 (chainID, moduleAddr, nonce):
nonce1nonce2SetCodeTx&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:… },
},
}
SignTx 通过 acc2Priv 签署完整交易。client.SendTransaction 发送。输出应如下所示:

https://amoy.polygonscan.com/tx/0x5d9b3a8ebbfc26ee94024f5e1f608e4fd1c933d9dcd78f8b4b127379015854d1
- 原文链接: medium.com/@andrey_obruc...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!