Virtual TestNets are live! ⚡️ Test and stage dapps on real-time production data.
Get started →

All Products

Virtual TestNets
State Sync

State Sync on Virtual TestNets

State Sync keeps your environment in real-time sync with the state of the parent network. This helps ensure you can access all blocks produced after the TestNet has been created.

You can enable State Sync during the creation process or thereafter by going to Settings > State Sync.


Behavior of mainnet contracts that cache block.number is unpredictable when running TestNet transactions, since the block number grows at different rates on mainnet and TestNet.

When to activate State Sync

This option is useful in several scenarios:

  • TestNet contracts and transactions can access current Oracle values that are present on the original chain.
  • Access Uniswap pools from the TestNets when testing contracts and building proof-of-concept transactions.
  • Build a subgraph without stale data.
  • Test the behavior and performance of your dapps and protocols in one-to-one correspondence with real network conditions

How State Sync works

State sync functions in an intuitive way.

  • The initial block number matches the latest block of the original chain at the time of creation.
  • Each time you send a transaction to a Virtual Testnet via eth_sendRawTransaction and eth_sendRawTransaction, a block is minded and block number increases by 1, independently of the original network.
  • Until writing to a variable on TestNet, the TestNet tracks the current value from the original network.
  • After writing to a variable on TestNet of a contract (e.g., C.X=100), any subsequent read will yield that value (100).
  • All unmodified state variables of a contract (C.Y) will reflect the value from the original network.
  • Before a new block is mined on a TestNet, the latest virtual block is synchronized with the latest block on the parent chain.
  • TestNet mines a block after every state modification (sendTransaction, setBalance, etc.).
  • Block numbers on TestNet and the original network will change with different rates.

For better understanding, let’s explore an example:


The example demonstrates the following:

  • TestNet (Virtual Sepolia) accesses the latest state from original network (Sepolia).
  • After a TestNet write, the modified variable detaches from its Sepolia counterpart.

You can find the example on Github:

Th example uses a modified Counter contract, that contains a map of counters. It’s deployed on Sepolia at 0xd01dF6d2354c5A869265dC9a9561E3544ac53262. The test script will interact with both Sepolia and a Virtual TestNet based on Sepolia - Virtual Sepolia.

The script takes a random entry in the counter map, and then:

  • Sets the value on Sepolia, then reads values on both Sepolia and Virtual Sepolia, showing that the same value is read back.
  • Sets the value on Virtual Sepolia, then reads values on both Sepolia and Virtual Sepolia, showing that different values are read.
  • Again sets the value on Sepolia, then reads values on both Sepolia and Virtual Sepolia, showing that the modification on Sepolia is invisible on its virtual counterpart.

Create a TestNet

Create a new TestNet based on Sepolia, using 735711155111 as the custom Chain ID.

Environment setup

Copy .example.env template and replace the following:

  • TENDERLY_VIRTUAL_TESTNET_CHAIN_ID the chain ID of the testnet. Use 735711155111
  • PRIVATE_KEY for signing test transactions
cp .example.env .env
vi .env # modify environment variables
cd virtual-testnets-state-sync
source .env
yarn install
npx ts-node src/index.ts


Key points to notice about write operations:

  1. After writing to sepolia (1), both sepolia and virtualSepolia read value 1 - the latest state of the original network.
  2. After writing to Virtual Sepolia (10), reading from Sepolia gives the previously set value (1), and reading from virtualSepolia gives the recently set value (10).
  3. After writing to Sepolia again (100), reading from Sepolia gives the latest set value (100), and reading from virtualSepolia gives the previously set value (10).
Playing with Counter(0xd01dF6d2354c5A869265dC9a9561E3544ac53262).numbers[3788720642447205]
  sepolia: 0
  virtualSepolia: 0
Write: Counter(0xd01dF6d2354c5A869265dC9a9561E3544ac53262)@Sepolia.numbers[3788720642447205] = 1
Tx hash: 0xe57c5da70c68d55f86957f7a2fc2644670b032fe5f972b9e9ccc2774cd2ba352
  sepolia: 1
  virtualSepolia: 1
Write: Counter(0xd01dF6d2354c5A869265dC9a9561E3544ac53262)@Virtual Sepolia.numbers[3788720642447205] = 10
Tx hash: 0x56099d89c1d6fd001a1e9a0cecca13e13c04013b473acb6a48cd61a4b988696c
  sepolia: 1
  virtualSepolia: 10
Write: Counter(0xd01dF6d2354c5A869265dC9a9561E3544ac53262)@Sepolia.numbers[3788720642447205] = 100
Tx hash: 0xc3c37e912bfa4fca9b980339fa2c95dbef75d6c605f0df831ca985b0ef3b61ec
  sepolia: 100
  virtualSepolia: 10