> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tenderly.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Foundry Cheatcode Tutorial

> Deploy a cheatcode playground on a Virtual Environment and use cast send to manipulate balances, nonces, storage, and event logs from transactions.

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](/virtual-environments/precompiles) 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

| Category | Function                                | Wraps                                             |
| -------- | --------------------------------------- | ------------------------------------------------- |
| Balance  | `forceBalance(account, newBalance)`     | `setBalance`                                      |
|          | `topUp(account, amount)`                | `addBalance`                                      |
| ERC-20   | `fundToken(token, holders[], amount)`   | `setErc20Balance`                                 |
|          | `maxToken(token, account)`              | `setMaxErc20Balance`                              |
| 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`).

<Steps>
  ### Create a Virtual Environment

  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.

  ### Initialize the Foundry project

  ```bash theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  forge init tenderly-cheatcodes
  cd tenderly-cheatcodes
  ```

  ### Add the precompile interface

  Save as `src/IVnetPrecompile.sol`:

  ```solidity title="src/IVnetPrecompile.sol" theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  // SPDX-License-Identifier: MIT
  pragma solidity ^0.8.0;

  interface IVnetPrecompile {
      function setStorageAt(address target, bytes32 slot, bytes32 value) external returns (bool);
      function setBalance(address[] calldata addresses, uint256 balance) external returns (bool);
      function getStorageAt(address target, bytes32 slot) external view returns (bytes32);
      function addBalance(address[] calldata addresses, uint256 amount) external returns (bool);
      function setNonce(address target, uint256 value) external returns (bool);
      function setErc20Balance(address target, address[] calldata addresses, uint256 amount) external returns (bool);
      function addErc20Balance(address target, address[] calldata addresses, uint256 amount) external returns (bool);
      function setMaxErc20Balance(address target, address account) 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);

      event SetStorageAtEvent(address target, bytes32 slot, bytes32 value);
      event SetBalanceEvent(address[] addresses, uint256 balance);
      event AddBalanceEvent(address[] addresses, uint256 amount);
      event SetNonceEvent(address target, uint256 value);
      event SetErc20BalanceEvent(address target, address[] addresses, uint256 amount);
      event AddErc20BalanceEvent(address target, address[] addresses, uint256 amount);
      event SetMaxErc20BalanceEvent(address target, address account);
  }
  ```

  ### Add the playground contract

  Save as `src/MyTestSetup.sol`:

  ```solidity title="src/MyTestSetup.sol" theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  // 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(_single(account), amount);
      }

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

      function fundToken(address token, address[] calldata holders, uint256 amount) external {
          VNET.setErc20Balance(token, holders, amount);
      }

      function maxToken(address token, address account) external {
          VNET.setMaxErc20Balance(token, account);
      }

      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(_single(account), 10 ether);
          VNET.setNonce(account, 5);
          VNET.setStorageAt(account, bytes32(uint256(0)), bytes32(uint256(0x1234)));
          VNET.addBalance(_single(account), 2 ether);
          VNET.emitEvent(
              0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48,
              TRANSFER_TOPIC,
              bytes32(uint256(0)),
              bytes32(uint256(uint160(account))),
              abi.encode(uint256(1000e6))
          );
      }

      function _single(address account) private pure returns (address[] memory arr) {
          arr = new address[](1);
          arr[0] = account;
      }
  }
  ```

  ### Add the deploy script

  Save as `script/Deploy.s.sol`:

  ```solidity title="script/Deploy.s.sol" theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  // 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();
      }
  }
  ```

  ### Configure environment variables

  Save as `.env`:

  ```bash title=".env" theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  VNET_RPC=https://virtual.mainnet.eu.rpc.tenderly.co/<your-virtual-env-uuid>
  PRIVATE_KEY=0x...
  DEPLOYER=0x...
  ```

  Source it: `source .env`.

  ### Fund the deployer

  ```bash theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  cast rpc tenderly_setBalance "[\"$DEPLOYER\"]" "0x56BC75E2D63100000" --rpc-url "$VNET_RPC"
  ```

  ### Deploy and verify

  ```bash theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  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"
  ```

  Export the deployed address from the script output:

  ```bash theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  export TEST_CONTRACT=0x...
  ```

  ### Exercise the cheatcodes

  Each call is a separate transaction. Re-run with different arguments at any time, no redeployment needed.

  ```bash theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  # Set 999 ETH on an arbitrary account
  cast send "$TEST_CONTRACT" "forceBalance(address,uint256)" \
      0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 999000000000000000000 \
      --rpc-url "$VNET_RPC" --private-key "$PRIVATE_KEY"

  # Give two accounts 1,000,000 USDC each (6 decimals)
  cast send "$TEST_CONTRACT" "fundToken(address,address[],uint256)" \
      0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 \
      '[0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA,0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB]' \
      1000000000000 \
      --rpc-url "$VNET_RPC" --private-key "$PRIVATE_KEY"

  # Max out an account's DAI balance
  cast send "$TEST_CONTRACT" "maxToken(address,address)" \
      0x6B175474E89094C44Da98b954EedeAC495271d0F \
      0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA \
      --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"
  ```

  ### Verify the state changes

  ```bash theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  cast balance 0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA --rpc-url "$VNET_RPC"
  cast nonce   0xd8da6bf26964af9d7eed9e03e53415d37aa96045 --rpc-url "$VNET_RPC"
  cast storage "$TARGET" 0                                --rpc-url "$VNET_RPC"
  cast call 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 "balanceOf(address)(uint256)" \
      0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA --rpc-url "$VNET_RPC"
  ```

  ### Confirm the spoofed log

  Pull the receipt and check `logs[]` for an entry whose `address` matches the contract you spoofed:

  ```bash theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  cast receipt <TX_HASH> --rpc-url "$VNET_RPC" --json | jq '.logs'
  ```

  For the BAYC example, the receipt should contain:

  ```json theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  {
    "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.

  <Note>
    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.
  </Note>
</Steps>

## See also

* [Solidity Cheatcodes reference](/virtual-environments/precompiles), full interface definition and function table.
* [Admin RPC](/virtual-environments/admin-rpc), state manipulation from outside a transaction via JSON-RPC.
