跳转到主要内容
本指南构建了一个 3 步的模拟捆绑包,用于演练经典的 ERC-20 授权流程:向持有人铸造代币、授权 spender,然后让 spender 调用 transferFrom。每一步都依赖前一步创建的状态,这也是为什么捆绑(bundle)是在此正确工具的原因。

为什么使用捆绑?

单独模拟每笔交易会失败:
  • 仅铸造:您可以验证铸造逻辑,但持有人地址在主网上没有代币。
  • 仅授权:模拟需要持有人实际持有一定余额才能授权。
  • 仅 TransferFrom:既需要余额也需要授权都已就绪。
Sim 捆绑包会依次运行这三步,共享生成的状态。第 2 步能看到第 1 步铸造的余额。第 3 步能看到第 2 步的授权。

三笔交易

这三笔交易都以 Ethereum 主网上的 DAI 合约(0x6b175474e89094c44da98b954eedeac495271d0f)为目标。

第 1 步:铸造 2 DAI

From:  0xe2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2  (fake ward)
To:    0x6b175474e89094c44da98b954eedeac495271d0f  (DAI)
Input: 0x40c10f19
       000000000000000000000000e58b9ee93700a616b50509c8292977fa7a0f8ce1  (recipient)
       0000000000000000000000000000000000000000000000001bc16d674ec80000  (2 DAI)
DAI 的 mint 函数由 wards 映射守卫:只有注册为 ward 的地址才能调用它。fake ward(0xe2e2...e2)在主网上并不是 ward。要让这一步成功,请添加一个状态覆盖,将该地址的 ward 存储槽设置为 1
字段
合约0x6b175474e89094c44da98b954eedeac495271d0f
Storage key0xedd7d04419e9c48ceb6055956cbb4e2091ae310313a4d1fa7cbcfe7561616e03
Value0x0000000000000000000000000000000000000000000000000000000000000001
storage key 为 keccak256(abi.encode(wardAddress, 0)),即 wards[fakeWardAddress] 在存储位置 0 处的标准 Solidity mapping 槽位。

第 2 步:授权 1 DAI

From:  0xe58b9ee93700a616b50509c8292977fa7a0f8ce1  (token holder from step 1)
To:    0x6b175474e89094c44da98b954eedeac495271d0f  (DAI)
Input: 0x095ea7b3
       000000000000000000000000f7ddedc66b1d482e5c38e4730b3357d32411e5dd  (spender)
       0000000000000000000000000000000000000000000000000de0b6b3a7640000  (1 DAI)
持有人授权一名 spender 拉取最多 1 DAI。此步骤能够成功,是因为第 1 步已在共享状态流中向该持有人的余额存入了 2 DAI。

第 3 步:TransferFrom 0.03 DAI

From:  0xf7ddedc66b1d482e5c38e4730b3357d32411e5dd  (approved spender)
To:    0x6b175474e89094c44da98b954eedeac495271d0f  (DAI)
Input: 0x23b872dd
       000000000000000000000000e58b9ee93700a616b50509c8292977fa7a0f8ce1  (sender / token holder)
       000000000000000000000000bd8daa414fda8a8a129f7035e7496759c5af8570  (recipient)
       000000000000000000000000000000000000000000000000006a94d74f430000  (0.03 DAI)
spender 从持有人处向接收方转移 0.03 DAI。之所以能够成功,是因为第 2 步已授予额度,而该额度仅在捆绑包的状态流中可见,链上并不存在。

通过 Simulator UI 运行

1

打开 Sim Bundle Builder

在左侧导航中前往 Simulator,然后点击 New Simulation。捆绑包构建器将随一个空步骤打开。
2

配置会话条

会话条横跨顶部。将 Network 设置为 Mainnet,并将 Block 保持为 Pending
3

填写第 1 步:Mint

将该步骤切换到 Raw 模式。设置:
  • From0xe2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2
  • To0x6b175474e89094c44da98b954eedeac495271d0f
  • Calldata0x40c10f19000000000000000000000000e58b9ee93700a616b50509c8292977fa7a0f8ce10000000000000000000000000000000000000000000000001bc16d674ec80000
然后展开 State overrides,添加 DAI 合约,并输入上表中的 ward 存储槽和值。点击 Add storage override 提交该槽位。
Step 1 mint transaction configured with state override
4

添加第 2 步:Approve

在捆绑包侧栏中点击 + Add function call。将新步骤切换到 Raw 模式。设置:
  • From0xe58b9ee93700a616b50509c8292977fa7a0f8ce1
  • To0x6b175474e89094c44da98b954eedeac495271d0f
  • Calldata0x095ea7b3000000000000000000000000f7ddedc66b1d482e5c38e4730b3357d32411e5dd0000000000000000000000000000000000000000000000000de0b6b3a7640000
5

添加第 3 步:TransferFrom

再添加一个步骤。切换到 Raw 模式。设置:
  • From0xf7ddedc66b1d482e5c38e4730b3357d32411e5dd
  • To0x6b175474e89094c44da98b954eedeac495271d0f
  • Calldata0x23b872dd000000000000000000000000e58b9ee93700a616b50509c8292977fa7a0f8ce1000000000000000000000000bd8daa414fda8a8a129f7035e7496759c5af8570000000000000000000000000000000000000000000000000006a94d74f430000
6

运行捆绑包

点击 Simulate(或按 ⌘↵)。三个步骤会依次执行。点击侧栏中的每个步骤即可查看其解码后的输出、消耗的 gas、事件和代币转账。
Step 2 approve result showing the Approval event
Step 3 transferFrom result showing the DAI transfer

通过 Simulation API 运行

使用 simulate-bundle 端点以编程方式运行相同的序列。将 {accountSlug}{projectSlug} 替换为您自己的值。
curl --request POST \
  --url https://api.tenderly.co/api/v1/account/{accountSlug}/project/{projectSlug}/simulate-bundle \
  --header 'Content-Type: application/json' \
  --header 'X-Access-Key: <api-key>' \
  --data '{
  "simulations": [
    {
      "network_id": "1",
      "save": true,
      "save_if_fails": true,
      "simulation_type": "full",
      "from": "0xe2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2",
      "to": "0x6b175474e89094c44da98b954eedeac495271d0f",
      "input": "0x40c10f19000000000000000000000000e58b9ee93700a616b50509c8292977fa7a0f8ce10000000000000000000000000000000000000000000000001bc16d674ec80000",
      "state_objects": {
        "0x6b175474e89094c44da98b954eedeac495271d0f": {
          "storage": {
            "0xedd7d04419e9c48ceb6055956cbb4e2091ae310313a4d1fa7cbcfe7561616e03": "0x0000000000000000000000000000000000000000000000000000000000000001"
          }
        }
      }
    },
    {
      "network_id": "1",
      "save": true,
      "save_if_fails": true,
      "simulation_type": "full",
      "from": "0xe58b9ee93700a616b50509c8292977fa7a0f8ce1",
      "to": "0x6b175474e89094c44da98b954eedeac495271d0f",
      "input": "0x095ea7b3000000000000000000000000f7ddedc66b1d482e5c38e4730b3357d32411e5dd0000000000000000000000000000000000000000000000000de0b6b3a7640000"
    },
    {
      "network_id": "1",
      "save": true,
      "save_if_fails": true,
      "simulation_type": "full",
      "from": "0xf7ddedc66b1d482e5c38e4730b3357d32411e5dd",
      "to": "0x6b175474e89094c44da98b954eedeac495271d0f",
      "input": "0x23b872dd000000000000000000000000e58b9ee93700a616b50509c8292977fa7a0f8ce1000000000000000000000000bd8daa414fda8a8a129f7035e7496759c5af8570000000000000000000000000000000000000000000000000006a94d74f430000"
    }
  ]
}'
响应是一个包含三条模拟结果的数组,每个步骤对应一条。每一项都包含 statusgasUsed、解码后的 logstraceassetChangesbalanceChanges 一次成功的运行会显示所有三个步骤的 "status": true。如果某一步回滚(例如第 1 步缺少了状态覆盖),API 会返回直至并包括失败步骤的所有结果,其后的步骤不会再运行。

应重点关注的结果

步骤关键信号
MintassetChanges 条目中出现 "type": "Mint",向持有人存入 2 DAI。来自零地址的 Transfer 事件。
Approvelogs 中出现 Approval 事件。没有资产变化(授权不会转移代币)。
TransferFromassetChanges 条目中出现 "type": "Transfer",0.03 DAI 从持有人转移到接收方。logs 中出现 Transfer 事件。balanceChanges 显示持有人有负增量,接收方有正增量。

后续步骤