Skip to main content
This tutorial builds a modular cheatcode playground in Foundry. You deploy a single contract to a Virtual Environment, then call its functions with 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: a forge script deploy.

Function map

CategoryFunctionWraps
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 (generic, 0–4 topics)
DemodemoScenario(account)Combines balance, nonce, storage, USDC Transfer

Prerequisites

  • A Tenderly account with a project.
  • Foundry installed (curl -L https://foundry.paradigm.xyz | bash && foundryup).
1
Create a Virtual Environment
2
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.
3
Initialize the Foundry project
4
forge init tenderly-cheatcodes
cd tenderly-cheatcodes
5
Add the precompile interface
6
Save as 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
Add the playground contract
9
Save as 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
Add the deploy script
12
Save as 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
Configure environment variables
15
Save as .env:
16
VNET_RPC=https://virtual.mainnet.eu.rpc.tenderly.co/<your-vnet-uuid>
PRIVATE_KEY=0x...
DEPLOYER=0x...
17
Source it: source .env.
18
Fund the deployer
19
cast rpc tenderly_setBalance "[\"$DEPLOYER\"]" "0x56BC75E2D63100000" --rpc-url "$VNET_RPC"
20
Deploy and verify
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
Export the deployed address from the script output:
23
export TEST_CONTRACT=0x...
24
Exercise the cheatcodes
25
Each call is a separate transaction. Re-run with different arguments at any time, no redeployment needed.
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
Verify the state changes
28
cast balance 0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA --rpc-url "$VNET_RPC"
cast nonce   0xd8da6bf26964af9d7eed9e03e53415d37aa96045 --rpc-url "$VNET_RPC"
cast storage "$TARGET" 0                                --rpc-url "$VNET_RPC"
29
Confirm the spoofed log
30
Pull the receipt and check logs[] for an entry whose address matches the contract you spoofed:
31
cast receipt <TX_HASH> --rpc-url "$VNET_RPC" --json | jq '.logs'
32
For the BAYC example, the receipt should contain:
33
{
  "address": "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d",
  "topics": [
    "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
    "0x000000000000000000000000cccccccccccccccccccccccccccccccccccccccc",
    "0x000000000000000000000000dddddddddddddddddddddddddddddddddddddddd",
    "0x00000000000000000000000000000000000000000000000000000000000004d2"
  ],
  "data": "0x"
}
34
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.
35
The precompile also emits diagnostic logs with address = 0xc100000000000000000000000000000000000000, recording each state mutation. Filter logs[] by the target address to isolate the spoofed log from the precompile’s own entries.

See also