cast send, чтобы использовать каждый Solidity precompile cheatcode изнутри транзакций.
Что вы соберёте
Три файла: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 топика) | |
| Demo | demoScenario(account) | Комбинирует balance, nonce, storage, USDC Transfer |
Предварительные условия
- Аккаунт Tenderly с проектом.
- Установленный Foundry (
curl -L https://foundry.paradigm.xyz | bash && foundryup).
В Tenderly Dashboard перейдите в Virtual Environments и создайте Virtual Environment. Скопируйте URL Admin RPC — публичный 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"
Получите чек и проверьте
logs[] на запись, у которой address совпадает с контрактом, чью эмиссию вы подделали:{
"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.