跳转到主要内容
本教程在 Foundry 中构建一个模块化的作弊码演练环境。您将向 Virtual Environment 部署一个合约,然后使用 cast send 调用其函数,从交易内部演练每一个 Solidity 预编译作弊码

您将构建的内容

三个文件:
  • src/IVnetPrecompile.sol:预编译接口。
  • src/MyTestSetup.sol:一个演练合约。每个函数封装一个预编译调用,因此您只需部署一次,然后调用各个函数来一次操纵一部分状态。
  • script/Deploy.s.sol:一个 forge script 部署脚本。

函数映射

类别函数封装
BalanceforceBalance(account, newBalance)setBalance
topUp(account, amount)addBalance
NonceforceNonce(account, newNonce)setNonce
StoragewriteStorage(target, slot, value)setStorageAt
readStorage(target, slot) → bytes32getStorageAt
EventsfireTransfer(token, from, to, amount)emitEvent(ERC-20 Transfer
fireEvent(target, topics[], data)emitEvent(通用,0–4 个 topics)
DemodemoScenario(account)组合 balance、nonce、storage、USDC Transfer

前提条件

  • 一个包含项目的 Tenderly 账户。
  • 已安装 Foundry(curl -L https://foundry.paradigm.xyz | bash && foundryup)。
1
创建 Virtual Environment
2
在 Tenderly Dashboard 中,前往 Virtual Environments 并创建一个 Virtual Environment。复制 Admin RPC URL,公共 RPC 在充值和验证调用上会返回 401,因此本教程始终使用 Admin RPC。
3
初始化 Foundry 项目
4
forge init tenderly-cheatcodes
cd tenderly-cheatcodes
5
添加预编译接口
6
保存为 src/IVnetPrecompile.sol
7
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IVnetPrecompile {
    function setStorageAt(address target, bytes32 slot, bytes32 value) external returns (bool);
    function getStorageAt(address target, bytes32 slot) external view returns (bytes32);
    function setBalance(address target, uint256 balance) external returns (bool);
    function addBalance(address target, uint256 amount) external returns (bool);
    function setNonce(address target, uint256 value) external returns (bool);

    function emitEvent(address target, bytes calldata data) external returns (bool);
    function emitEvent(address target, bytes32 topic1) external returns (bool);
    function emitEvent(address target, bytes32 topic1, bytes calldata data) external returns (bool);
    function emitEvent(address target, bytes32 topic1, bytes32 topic2) external returns (bool);
    function emitEvent(address target, bytes32 topic1, bytes32 topic2, bytes calldata data) external returns (bool);
    function emitEvent(address target, bytes32 topic1, bytes32 topic2, bytes32 topic3) external returns (bool);
    function emitEvent(address target, bytes32 topic1, bytes32 topic2, bytes32 topic3, bytes calldata data) external returns (bool);
    function emitEvent(address target, bytes32 topic1, bytes32 topic2, bytes32 topic3, bytes32 topic4) external returns (bool);
    function emitEvent(address target, bytes32 topic1, bytes32 topic2, bytes32 topic3, bytes32 topic4, bytes calldata data) external returns (bool);
}
8
添加演练合约
9
保存为 src/MyTestSetup.sol
10
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {IVnetPrecompile} from "./IVnetPrecompile.sol";

contract MyTestSetup {
    IVnetPrecompile constant VNET = IVnetPrecompile(0xc100000000000000000000000000000000000000);

    bytes32 constant TRANSFER_TOPIC = keccak256("Transfer(address,address,uint256)");

    function forceBalance(address account, uint256 amount) external {
        VNET.setBalance(account, amount);
    }

    function topUp(address account, uint256 amount) external {
        VNET.addBalance(account, amount);
    }

    function forceNonce(address account, uint256 n) external {
        VNET.setNonce(account, n);
    }

    function writeStorage(address target, bytes32 slot, bytes32 value) external {
        VNET.setStorageAt(target, slot, value);
    }

    function readStorage(address target, bytes32 slot) external view returns (bytes32) {
        return VNET.getStorageAt(target, slot);
    }

    function fireTransfer(address token, address from, address to, uint256 amount) external {
        VNET.emitEvent(
            token,
            TRANSFER_TOPIC,
            bytes32(uint256(uint160(from))),
            bytes32(uint256(uint160(to))),
            abi.encode(amount)
        );
    }

    function fireEvent(address target, bytes32[] calldata topics, bytes calldata data) external {
        if (topics.length == 0)      VNET.emitEvent(target, data);
        else if (topics.length == 1) VNET.emitEvent(target, topics[0], data);
        else if (topics.length == 2) VNET.emitEvent(target, topics[0], topics[1], data);
        else if (topics.length == 3) VNET.emitEvent(target, topics[0], topics[1], topics[2], data);
        else                         VNET.emitEvent(target, topics[0], topics[1], topics[2], topics[3], data);
    }

    function demoScenario(address account) external {
        VNET.setBalance(account, 10 ether);
        VNET.setNonce(account, 5);
        VNET.setStorageAt(account, bytes32(uint256(0)), bytes32(uint256(0x1234)));
        VNET.addBalance(account, 2 ether);
        VNET.emitEvent(
            0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48,
            TRANSFER_TOPIC,
            bytes32(uint256(0)),
            bytes32(uint256(uint160(account))),
            abi.encode(uint256(1000e6))
        );
    }
}
11
添加部署脚本
12
保存为 script/Deploy.s.sol
13
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {Script} from "forge-std/Script.sol";
import {MyTestSetup} from "../src/MyTestSetup.sol";

contract Deploy is Script {
    function run() external {
        vm.startBroadcast();
        new MyTestSetup();
        vm.stopBroadcast();
    }
}
14
配置环境变量
15
保存为 .env
16
VNET_RPC=https://virtual.mainnet.eu.rpc.tenderly.co/<your-virtual-env-uuid>
PRIVATE_KEY=0x...
DEPLOYER=0x...
17
加载它:source .env
18
为部署者注资
19
cast rpc tenderly_setBalance "[\"$DEPLOYER\"]" "0x56BC75E2D63100000" --rpc-url "$VNET_RPC"
20
部署和验证
21
forge script script/Deploy.s.sol:Deploy \
    --rpc-url "$VNET_RPC" \
    --private-key "$PRIVATE_KEY" \
    --broadcast --slow \
    --verify \
    --verifier custom \
    --verifier-url "$VNET_RPC/verify"
22
从脚本输出中导出已部署的地址:
23
export TEST_CONTRACT=0x...
24
演练作弊码
25
每次调用都是一笔单独的交易。您可以随时用不同参数重新运行,无需重新部署。
26
# Set 999 ETH on an arbitrary account
cast send "$TEST_CONTRACT" "forceBalance(address,uint256)" \
    0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 999000000000000000000 \
    --rpc-url "$VNET_RPC" --private-key "$PRIVATE_KEY"

# Set vitalik.eth's nonce to 4242
cast send "$TEST_CONTRACT" "forceNonce(address,uint256)" \
    0xd8da6bf26964af9d7eed9e03e53415d37aa96045 4242 \
    --rpc-url "$VNET_RPC" --private-key "$PRIVATE_KEY"

# Write storage slot 0 of a contract
cast send "$TEST_CONTRACT" "writeStorage(address,bytes32,bytes32)" \
    "$TARGET" \
    0x0000000000000000000000000000000000000000000000000000000000000000 \
    0x0000000000000000000000000000000000000000000000000000000000001234 \
    --rpc-url "$VNET_RPC" --private-key "$PRIVATE_KEY"

# Spoof a 1 DAI Transfer from the real DAI contract
cast send "$TEST_CONTRACT" "fireTransfer(address,address,address,uint256)" \
    0x6B175474E89094C44Da98b954EedeAC495271d0F \
    0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC \
    0xDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD \
    1000000000000000000 \
    --rpc-url "$VNET_RPC" --private-key "$PRIVATE_KEY"

# Spoof a BAYC ERC-721 Transfer of token #1234 (all args indexed, data is empty)
cast send "$TEST_CONTRACT" "fireEvent(address,bytes32[],bytes)" \
    0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D \
    '[0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef,0x000000000000000000000000CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC,0x000000000000000000000000DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD,0x00000000000000000000000000000000000000000000000000000000000004d2]' \
    0x \
    --rpc-url "$VNET_RPC" --private-key "$PRIVATE_KEY"
27
验证状态变化
28
cast balance 0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA --rpc-url "$VNET_RPC"
cast nonce   0xd8da6bf26964af9d7eed9e03e53415d37aa96045 --rpc-url "$VNET_RPC"
cast storage "$TARGET" 0                                --rpc-url "$VNET_RPC"
29
确认伪造的日志
30
拉取收据并检查 logs[]address 与您伪造的合约匹配的条目:
31
cast receipt <TX_HASH> --rpc-url "$VNET_RPC" --json | jq '.logs'
32
对于 BAYC 示例,收据应包含:
33
{
  "address": "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d",
  "topics": [
    "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
    "0x000000000000000000000000cccccccccccccccccccccccccccccccccccccccc",
    "0x000000000000000000000000dddddddddddddddddddddddddddddddddddddddd",
    "0x00000000000000000000000000000000000000000000000000000000000004d2"
  ],
  "data": "0x"
}
34
address 与 BAYC 合约匹配,topics[0]Transfer 事件签名哈希,topics[1–3] 携带索引的 fromtotokenId。监听 BAYC 转账的索引器将以与真实发出相同的方式处理此日志。
35
预编译还会发出诊断日志,其 address = 0xc100000000000000000000000000000000000000,记录每次状态修改。按目标地址过滤 logs[],可将伪造的日志与预编译自身的条目区分开来。

另请参阅