EIP-7702:SetCode 交易——EOA 临时智能账户能力解析

EIP-7702 引入了一种新的交易类型,允许 EOA 账户在交易执行期间临时地表现得像智能合约,无需部署或改变长期状态。它通过授权列表和短期的委托存根实现,从而提升用户体验,支持批量交易、交易赞助和权限委托等功能。

EIP-7702 提出了一种强大而简单的解决方案:通过一种新的交易类型,赋予 EOA 临时行为像智能合约的能力。在这种交易中,用户可以在交易执行期间为其账户附加自定义代码。无需部署,没有长期状态更改,也没有额外的 Gas 成本。

工作原理:核心委托模型

EIP-7702 的核心是一个轻量级的委托模型。交易包含一个授权元组列表,其格式如下:

[chain_id, address, nonce, y_parity, r, s]

对于每个元组,一个委托指示符 (0xef0100 || address) 被写入授权账户的代码中。所有执行代码操作都必须加载并执行该委托指向的代码。

  • 0xef0100 是一个特殊标记,表示“此账户委托其行为”。
  • 20 字节的地址指向包含实际可执行逻辑的合约。

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_idnoncemax_priority_fee_per_gasmax_fee_per_gasgas_limitdestinationvaluedataaccess_list 字段遵循 EIP-4844 的相同语义。这意味着空的目标地址是无效的。

新的潜在用例

EIP-7702 带来了多项关键的用户体验改进,这些改进此前需要完整的账户抽象或外部基础设施:

  • 批量处理:例如,在一个原子交易中批准和使用代币。
  • 赞助:让另一个账户支付你的 Gas 费用。
  • 权限委托:授予特定应用密钥或子账户有限的使用权限。

通过引入一种原生方式,让 EOA 能够临时将其行为委托给可重用的智能合约逻辑,EIP-7702 为更安全、更灵活的钱包开辟了一条道路,而无需进行协议大修。

详细机制

在执行交易之前,EIP-7702 会处理一个可选的授权元组列表,每个元组包含 chain_idaddressnonce 和 ECDSA 签名。每个元组都会被验证(包括签名恢复和 nonce 检查),如果有效,授权账户的代码将临时替换为特殊的委托指示符0xef0100 || address。这表明任何代码执行(例如通过 CALLDELEGATECALL 或直接交易执行)都应重定向到指定 address 处的逻辑。值得注意的是,这种委托会影响执行,但不会影响代码检查:CODESIZECODECOPY 操作码反映的是被委托合约的代码,而对委托账户的 EXTCODESIZE 只返回委托标记的大小(23 字节)。即使交易回滚,委托也会持续存在,并且来自同一账户的多个元组将使用最后一个有效的元组进行解析。

示例:批量交易

我们将部署两个合约 MultiDelegationInvokerInvoked,并指定两个不同的 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)
}

发生了什么:

连接与准备

  • 通过 RPC 连接到 Polygon Amoy 测试网。
  • 从本地钱包助手加载两个 EOA(acc1acc2)及其地址和私钥。

设置目标合约

  • to 是你要调用的智能合约地址。
  • moduleAddr 是你将委托到的实现合约。

构建 calldata

  • 定义 triggerPings(address[]) 的最小 ABI。
  • 将你的两个授权地址(froms = [acc1, acc2])打包到 data 中。

获取 Nonce 和 Gas 参数

  • 读取 acc2 的待处理 Nonce (baseNonce2) 用于交易,然后计算其委托条目的递增后 Nonce (nonce2 = baseNonce2 + 1)。
  • 读取 acc1 的待处理 Nonce (nonce1) 用于其委托。
  • 建议 EIP‑1559 小费和基础费用以计算 gasFeeCap

生成 EIP‑7702 签名

  • 每个授权方通过 signEIP7702Delegation() 签署元组 (chainID, moduleAddr, nonce)
    • acc1 使用 nonce1
    • acc2(交易发送者)使用 nonce2

组装 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:… },
  },
}

签名与广播

  • 使用 SignTx 通过 acc2Priv 签署完整交易。
  • 通过 client.SendTransaction 发送。
  • 轮询收据以确认挖矿。

输出应如下所示:

https://amoy.polygonscan.com/tx/0x5d9b3a8ebbfc26ee94024f5e1f608e4fd1c933d9dcd78f8b4b127379015854d1

资源

  • 原文链接: medium.com/@andrey_obruc...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Andrey Obruchkov
Andrey Obruchkov
江湖只有他的大名,没有他的介绍。