cast send 调用其函数,从交易内部演练每一个 Solidity 预编译作弊码。
您将构建的内容
三个文件:src/IVnetPrecompile.sol:预编译接口。src/MyTestSetup.sol:一个演练合约。每个函数封装一个预编译调用,因此您只需部署一次,然后调用各个函数来一次操纵一部分状态。script/Deploy.s.sol:一个forge script部署脚本。
函数映射
| 类别 | 函数 | 封装 |
|---|---|---|
| Balance | forceBalance(account, newBalance) | setBalance |
topUp(account, amount) | addBalance | |
| Nonce | forceNonce(account, newNonce) | setNonce |
| Storage | writeStorage(target, slot, value) | setStorageAt |
readStorage(target, slot) → bytes32 | getStorageAt | |
| Events | fireTransfer(token, from, to, amount) | emitEvent(ERC-20 Transfer) |
fireEvent(target, topics[], data) | emitEvent(通用,0–4 个 topics) | |
| Demo | demoScenario(account) | 组合 balance、nonce、storage、USDC Transfer |
前提条件
- 一个包含项目的 Tenderly 账户。
- 已安装 Foundry(
curl -L https://foundry.paradigm.xyz | bash && foundryup)。
在 Tenderly Dashboard 中,前往 Virtual Environments 并创建一个 Virtual Environment。复制 Admin RPC URL,公共 RPC 在充值和验证调用上会返回 401,因此本教程始终使用 Admin RPC。
// 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);
}
// 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))
);
}
}
// 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();
}
}
VNET_RPC=https://virtual.mainnet.eu.rpc.tenderly.co/<your-virtual-env-uuid>
PRIVATE_KEY=0x...
DEPLOYER=0x...
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"
# 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"
cast balance 0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA --rpc-url "$VNET_RPC"
cast nonce 0xd8da6bf26964af9d7eed9e03e53415d37aa96045 --rpc-url "$VNET_RPC"
cast storage "$TARGET" 0 --rpc-url "$VNET_RPC"
{
"address": "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x000000000000000000000000cccccccccccccccccccccccccccccccccccccccc",
"0x000000000000000000000000dddddddddddddddddddddddddddddddddddddddd",
"0x00000000000000000000000000000000000000000000000000000000000004d2"
],
"data": "0x"
}
address 与 BAYC 合约匹配,topics[0] 是 Transfer 事件签名哈希,topics[1–3] 携带索引的 from、to 和 tokenId。监听 BAYC 转账的索引器将以与真实发出相同的方式处理此日志。另请参阅
- Solidity Cheatcodes 参考,完整的接口定义和函数表。
- Admin RPC,通过 JSON-RPC 从交易外部操纵状态。