cast send to exercise every Solidity precompile cheatcode from inside transactions.
What you build
Three files:src/IVnetPrecompile.sol: the precompile interface.src/MyTestSetup.sol: a playground contract. Each function wraps one precompile call, so you deploy once and call individual functions to manipulate one piece of state at a time.script/Deploy.s.sol: aforge scriptdeploy.
Function map
| Category | Function | Wraps |
|---|---|---|
| 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 (generic, 0–4 topics) | |
| Demo | demoScenario(account) | Combines balance, nonce, storage, USDC Transfer |
Prerequisites
- A Tenderly account with a project.
- Foundry installed (
curl -L https://foundry.paradigm.xyz | bash && foundryup).
In the Tenderly Dashboard, go to Virtual Environments and create a Virtual Environment. Copy the Admin RPC URL, the public RPC returns 401 on funding and verification calls, so use the Admin RPC throughout this tutorial.
// 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-vnet-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"
Each call is a separate transaction. Re-run with different arguments at any time, no redeployment needed.
# 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 matches the BAYC contract, topics[0] is the Transfer event signature hash, and topics[1–3] carry the indexed from, to, and tokenId. An indexer listening for BAYC transfers will process this log identically to a real emission.See also
- Solidity Cheatcodes reference, full interface definition and function table.
- Admin RPC, state manipulation from outside a transaction via JSON-RPC.