Documentation

Synchronizing Uniswap V2 Reserves in BuildBear Sandbox with Mainnet

Learn to implement a script that continuously synchronizes Uniswap V2 token reserves in a BuildBear Sandbox environment based on the live mainnet reserves data.

Introduction

BuildBear enables developers to effortlessly fork any mainnet or testnet, providing a persistent, stateful sandbox environment ideal for deterministic testing and robust application development. A common requirement in such sandbox environments is the synchronization of specific state data with live networks. One critical use case involves the reserves of token pairs on Uniswap V2.

This guide offers detailed instructions on developing a script to fetch live reserve data from Uniswap V2 on mainnet and adjust the BuildBear Sandbox reserves accordingly.

What You'll Learn

  • How to fetch reserve data for token pairs from Ethereum mainnet.
  • Setting up a BuildBear Sandbox to mirror mainnet state.
  • Using helper scripts to mint and burn tokens dynamically.
  • Deploying a temporary contract to fund Uniswap pairs.
  • Automating reserve synchronization between mainnet and BuildBear Sandbox.

Step 1: Setup & Installation

Install the necessary dependencies, for more info, refer to the package.json

npm i @types/node axios dotenv ethers typescript viem 

Environment Configuration

  • Obtain Mainnet RPC from chainlist.org
  • Obtain BuildBear RPC from BuildBear Dashboard
  • Obtain Uniswap V2 Pair Address by invoking getPair() on the Uniswap V2 Factory contract.
  • Tokens involved can be identified from the Pair Address.
  • A Funder Address is required, typically a wallet used for funding the pair.
  • Sandbox Chain ID mirrors the forked network's ID.
  • Sandbox ID appears in RPC URL format: https://rpc.buildbear.io/{SANDBOX-ID}
MAINNET_RPC=
BUILDBEAR_RPC=
PAIR_ADDRESS=
TOKEN0=
TOKEN1=
FUNDER_ADDRESS=
SANDBOX_CHAIN_ID=
SANDBOX_ID=
PRIVATE_KEY=

This script supports all networks available on BuildBear. See BuildBear Supported Networks for details.

Directory & Utilities

Your directory structure should look like this:

ERC20Abi.json
SelfDestruct.json
network.ts
index.ts
.env
.env.example

Abstract Binary Interfaces

The ABI directory contains the files ERC20Abi.json, SelfDestruct.json, containing the Abstract Binary Interfaces for both.

But more on them later.

Network Setup

The network.ts file configures your BuildBear Sandbox connection using viem.sh:

import { createPublicClient, defineChain, http } from "viem";
import dotenv from "dotenv";
dotenv.config(); // Load environment variables from .env file
 
const buildbearSandboxUrl = process.env.BUILDBEAR_RPC;
const buildbearSandboxId = process.env.SANDBOX_ID;
if (!buildbearSandboxUrl || !buildbearSandboxId) {
  throw new Error("BUILDBEAR_RPC environment variable is missing");
}
 
const BBSandboxNetwork = /*#__PURE__*/ defineChain({
  id: process.env.SANDBOX_CHAIN_ID, // IMPORTANT : replace this with your sandbox's chain id
  name: "BuildBear x Mainnet Sandbox", // name your network
  nativeCurrency: { name: "BBETH", symbol: "BBETH", decimals: 18 }, // native currency of forked network
  rpcUrls: {
    default: {
      http: [buildbearSandboxUrl],
    },
  },
  blockExplorers: {
    default: {
      name: "BuildBear x Mainnet Scan", // block explorer for network
      url: `https://explorer.buildbear.io/${buildbearSandboxId}`,
    },
  },
});
 
export const publicClient = createPublicClient({
  chain: BBSandboxNetwork,
  transport: http(buildbearSandboxUrl), //@>>> Put in buildbear rpc
});

This will setup the Network Endpoint for sandbox where we will submit the request to.

Step 2: Implementing the Balancer Script

Set up environment variables and network connections clearly defined in previous steps. The detailed implementation covers ABI definitions, network setups, and utility functions for fetching and comparing token balances, minting, and burning excess tokens.

Load the Environment variables

// Load environment variables
const MAINNET_RPC = process.env.MAINNET_RPC as string;
const BUILDBEAR_RPC = process.env.BUILDBEAR_RPC as string;
const PAIR_ADDRESS = process.env.PAIR_ADDRESS as `0x${string}`;
const TOKEN0 = process.env.TOKEN0 as `0x${string}`;
const TOKEN1 = process.env.TOKEN1 as `0x${string}`;
const FUNDER_ADDRESS = process.env.FUNDER_ADDRESS as `0x${string}`;
const PRIVATE_KEY = process.env.PRIVATE_KEY as `0x${string}`;

Define the ABI for Uniswap V2 Pair Contract. We can do this in the json file inside of utils/ABI folder as before, but we only need specific set of functions for the purpose of this tutorial.

const UNISWAP_V2_PAIR_ABI = [
  "function getReserves() view returns (uint112, uint112, uint32)",
  "function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data)",
  "function transfer(address to, uint amount) returns (bool)",
  "function sync()",
  "function skim(address to)",
];

Setting Up Mainnet Network and BuildBear network with ethers

// Connect to networks
const mainnetProvider: JsonRpcProvider = new ethers.JsonRpcProvider(
  MAINNET_RPC
);
const sandboxProvider: JsonRpcProvider = new ethers.JsonRpcProvider(
  BUILDBEAR_RPC
);
 
const wallet = new ethers.Wallet(PRIVATE_KEY, sandboxProvider);

Helper functions

For this tutorial, we need to use a variety of helper functions, like functions to, fetch number of decimals for a token address, getting mainnet reserves for pair address, get token name, and so on.

Function to get reserves for token pair on given provider, i.e. same function can be used to get token reserves on Mainnet & Sandbox.

// Fetch reserves from Uniswap V2 pair
const getReserves = async (
  provider: JsonRpcProvider,
  address: `0x${string}`
) => {
  const pair = new ethers.Contract(address, UNISWAP_V2_PAIR_ABI, provider);
  const [reserve0, reserve1] = await pair.getReserves();
  return { reserve0, reserve1 };
};

Function to collectively get sandbox and mainnet reserves, although this can be done by calling the above getReserves functions with correct parameters, but defining a separate function call to get both can reduce redundant code

async function getMainnetAndSandboxStates() {
  const mainnetReserves = await getReserves(mainnetProvider, PAIR_ADDRESS);
  const sandboxReserves = await getReserves(sandboxProvider, PAIR_ADDRESS);
 
  return { mainnetReserves, sandboxReserves };
}

Functions related to token & account balances, getTokenBalanceForAccount, getNativeBalanceForAccount, getTokenName, getTokenDecimals, are helper functions for getting the token balances for a given account, getting the native balance of an account, getting the token name, and getting the token decimals respectively.

async function getTokenBalanceForAccount(
  tokenAddress: `0x${string}`,
  account: `0x${string}`
): Promise<string> {
  let res = await publicClient.readContract({
    address: tokenAddress,
    abi: ERC20Abi,
    functionName: "balanceOf",
    args: [account],
  });
  return formatUnits(
    res as bigint,
    await getTokenDecimals(tokenAddress)
  ).toString();
}
 
async function getNativeBalanceForAccount(account: `0x${string}`) {
  let res = await publicClient.getBalance({
    address: account,
  });
  return res.toString();
}
 
async function getTokenName(tokenAddress: `0x${string}`): Promise<string> {
  let res = await publicClient.readContract({
    address: tokenAddress,
    abi: ERC20Abi,
    functionName: "name",
  });
  return res as string;
}
 
async function getTokenDecimals(tokenAddress: `0x${string}`): Promise<number> {
  let res = await publicClient.readContract({
    address: tokenAddress,
    abi: ERC20Abi,
    functionName: "decimals",
  });
  return res as number;
}

Helper functions to mint & burn token reserves

Function to fund sandbox with excess tokens, when the reserve balance on mainnet is greater when compared to sandbox reserves

const fundSandbox = async (
  address: string,
  amount: string,
  token?: `0x${string}`
) => {
  try {
    // Logging Balance before and after
    if (token) {
      console.log(
        `${token} Balance Before : ${await getTokenBalanceForAccount(
          token,
          address as `0x${string}`
        )}`
      );
    } else {
      console.log(
        `Native Balance Before : ${await getNativeBalanceForAccount(
          address as `0x${string}`
        )}`
      );
    }
    console.log(
      `🚰 Requesting BuildBear faucet for ${amount} wei of ${
        token || "Native Token"
      }...`
    );
 
    // rpc requests for buildbear sandbox to fund with native or erc20 tokens
    const method = token ? "buildbear_ERC20Faucet" : "buildbear_nativeFaucet";
    const params = token
      ? [
          {
            address,
            balance: amount.toString(),
            token,
            unit: "wei",
          },
        ]
      : [{ address, balance: amount.toString(), unit: "wei" }];
    // console.log("=========== 🐞Debugging =========== ");
    // console.log(`params\n`, params);
    // console.log(`method\n`, method);
 
    // console.log("===================================");
 
    const response = await axios.post(BUILDBEAR_RPC, {
      jsonrpc: "2.0",
      id: 1,
      method,
      params,
    });
 
    // Error Handling and Balance Logging 
    if (response.data.error) {
      console.error(`❌ Faucet Error: ${response.data}`);
      console.error("Faucet response:", response.data); // Add this for debugging
    } else {
      if (token) {
        console.log(
          `✅ Faucet Success: Funded ${formatUnits(
            BigInt(amount),
            await getTokenDecimals(token)
          )} ${token} to ${address}`
        );
        console.log(
          `${token} Balance After : ${await getTokenBalanceForAccount(
            token,
            address as `0x${string}`
          )}`
        );
      } else {
        console.log(
          `✅ Faucet Success: Funded ${formatUnits(
            BigInt(amount),
            18
          )} Native to ${address}`
        );
        console.log(
          `Native Balance After : ${await getNativeBalanceForAccount(
            address as `0x${string}`
          )}`
        );
      }
    }
  } catch (error) {
    console.error(error);
    console.error(`❌ Error funding sandbox pair: ${error.message}`);
  }
};

Function to burn sandbox of excess tokens, when the reserve balance on mainnet is lesser when compared to sandbox reserves

We need to impersonate the pair address to simulate as if the pair address were itself burning it's balances to avoid "authorization" reverts.

// Impersonation signer (for sandbox)
const getImpersonatedSigner = async (address: `0x${string}`) => {
  await sandboxProvider.send("hardhat_impersonateAccount", [address]);
  return sandboxProvider.getSigner(address);
};
// Burn excess tokens by sending to 0xdead
const burnExcessTokens = async (
  pairAddress: `0x${string}`, // token pair address
  tokenAddress: `0x${string}`, // token address to burn
  amount: string // amount of tokens to burn
) => {
  // Impersonate the Uniswap V2 Pair contract
  const signer = await getImpersonatedSigner(pairAddress);
 
  const currentBalance = await getTokenBalanceForAccount(
    tokenAddress,
    pairAddress as `0x${string}`
  );
 
 
  // ERC20 Token Contract Instance
  const tokenContract = new ethers.Contract(tokenAddress, ERC20Abi, signer);
 
  // Send tokens to dead address (burn)
  const approveTx = await tokenContract.approve(
    "0x000000000000000000000000000000000000dEaD",
    amount
  );
 
  const transferTx = await tokenContract.transfer(
    "0x000000000000000000000000000000000000dEaD",
    amount
  );
 
  console.log(`✅ Burned ${amount} ${tokenAddress} tokens`);
};

Step 3: Main Script and Mint/Burn Logic

Before we setup the main logic for the script we need a contract to deploy a temporary contract.

Why is a Temporary contract needed?

It's because the Uniswap V2 Pair Contract, does not have a fallback/receive function to be able to accept any ether, therefore when you try to impersonate as pair address and burn the tokens, you get revert due to insufficient funds to pay for gas.

Therefore this step is crucial to fund your FUNDER_ADDRESS and deploy a Temporary contract, funded with ether from FUNDER_ADDRESS wallet.

Why it works?

The Temporary Contract SelfDestruct is a fairly simple contract, that self destructs on initialization within the constructor, and sending the funds of itself to the PAIR_ADDRESS.

By definition of selfdestruct in solidity, when a function destroys and sends it funds to a receiver address, there won't be any reverts even if the receiving contract doesn't have a fallback/receive function.

async function deploySelfDestructContract(initialFunds: string) {
  await fundSandbox(FUNDER_ADDRESS, initialFunds); // fund FUNDER_ADDRESS with native token
 
  console.log("====================================");
  console.log(
    `🟠 Deploying Self Destruct Contract from wallet : ${wallet.address}`
  );
  console.log("====================================");
 
  // Bytecode for a contract that can self-destruct
  // NOTE: We directly deploy the Temporary contract with bytecode, it's a relatively simple contract that self destructs in constructor and sends the funds to the pair address, without reverting
  const bytecode =
    "60806040526040516100ba3803806100ba8339818101604052810190602391906093565b8073ffffffffffffffffffffffffffffffffffffffff16ff5b5f80fd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6067826040565b9050919050565b607581605f565b8114607e575f80fd5b50565b5f81519050608d81606e565b92915050565b5f6020828403121560a55760a4603c565b5b5f60b0848285016081565b9150509291505056fe";
 
  console.log(
    `🚀 Deploying contract with ${ethers.formatEther(initialFunds)} ETH...`
  );
 
  const factory = new ContractFactory(SelfDestructAbi, bytecode, wallet);
 
  // If your contract requires constructor args, you can specify them here
  const contract = await factory.deploy(PAIR_ADDRESS, {
    value: initialFunds,
  });
 
  await contract.waitForDeployment();
  console.log(await contract.deploymentTransaction());
 
  console.log(`✅ Contract deployed at: ${await contract.getAddress()}`);
}

Now that we have a way to fund the PAIR_ADDRESS, we can continue with the main logic that will handle the reserves of sandbox.

Syncing the Reserves

Here is how we would sync the mainnet reserves:

  1. Sanity check: It verifies that the required pair address is available before proceeding.

  2. Data fetching: It pulls the current reserve data for a token pair from both the Ethereum mainnet and the BuildBear sandbox environment.

  3. Reserve comparison: It compares the token reserves across the two environments to identify any imbalance.

  4. Token shortfall detection: If the sandbox has fewer tokens than mainnet, it calculates how much needs to be minted.

  5. Excess token detection: If the sandbox has more tokens than mainnet, it calculates how much needs to be burned.

  6. No-op escape hatch: If the reserves are already in sync, it exits early without making changes.

  7. Token adjustments: It funds the sandbox with missing tokens and burns any excess to match mainnet's reserves.

  8. State synchronization: After adjusting balances, it calls the sync() function on the Uniswap V2 pair contract to lock in the new state.

  9. Fallback recovery: If sync() fails (due to violation of constant product AMM formula), it falls back to using skim() to clean up the surplus.

  10. Post-adjustment validation: It fetches the updated reserves again and validates whether the sync operation was successful.

Here are the above points put into a function:

const syncV2Reserves = async () => {
  if (!PAIR_ADDRESS) {
    console.error("❌ Pair address not provided.");
    process.exit(0);
  }
 
  console.log("🔄 Fetching reserves from mainnet and sandbox...");
  const { mainnetReserves, sandboxReserves } =
    await getMainnetAndSandboxStates();
  console.log("✅ Mainnet Reserves");
  console.log(
    `${await getTokenName(TOKEN0)} : `,
    formatUnits(mainnetReserves.reserve0, await getTokenDecimals(TOKEN0))
  );
  console.log(
    ` ${await getTokenName(TOKEN1)} : `,
    formatUnits(mainnetReserves.reserve1, await getTokenDecimals(TOKEN1))
  );
 
  console.log("✅ Sandbox Reserves");
  console.log(
    `${await getTokenName(TOKEN0)} : `,
    formatUnits(sandboxReserves.reserve0, await getTokenDecimals(TOKEN0))
  );
  console.log(
    ` ${await getTokenName(TOKEN1)} : `,
    formatUnits(sandboxReserves.reserve1, await getTokenDecimals(TOKEN1))
  );
  const sandboxSigner = await getImpersonatedSigner(PAIR_ADDRESS);
  const pairContract = new ethers.Contract(
    PAIR_ADDRESS,
    UNISWAP_V2_PAIR_ABI,
    sandboxSigner
  );
 
  // Calculate reserve differences
  let missingToken0 =
    mainnetReserves.reserve0 > sandboxReserves.reserve0
      ? mainnetReserves.reserve0 - sandboxReserves.reserve0
      : 0;
  let missingToken1 =
    mainnetReserves.reserve1 > sandboxReserves.reserve1
      ? mainnetReserves.reserve1 - sandboxReserves.reserve1
      : 0;
 
  let excessToken0 =
    sandboxReserves.reserve0 > mainnetReserves.reserve0
      ? sandboxReserves.reserve0 - mainnetReserves.reserve0
      : 0;
  let excessToken1 =
    sandboxReserves.reserve1 > mainnetReserves.reserve1
      ? sandboxReserves.reserve1 - mainnetReserves.reserve1
      : 0;
 
  // Add check to exit if no changes are needed
  if (
    missingToken0 === 0 &&
    missingToken1 === 0 &&
    excessToken0 === 0 &&
    excessToken1 === 0
  ) {
    console.log(
      "✅ No changes needed! Sandbox reserves match mainnet reserves."
    );
    return; // Exit the function
  }
 
  console.log(`Token0 : ${TOKEN0}`);
  console.log(`Token1 : ${TOKEN1}`);
 
  console.log(`RAW :: missingToken0 : ${missingToken0}`);
  console.log(`RAW :: missingToken1 : ${missingToken1}`);
  console.log(`RAW :: excessToken0 : ${excessToken0}`);
  console.log(`RAW :: excessToken1 : ${excessToken1}`);
 
  // 🔹 FUND SANDBOX IF RESERVES ARE LOWER THAN MAINNET
  if (missingToken1 > 0) {
    console.log(
      `⚠️ ${TOKEN1} Sandbox reserves are lower than mainnet! Minting additional tokens...`
    );
    console.log(
      `🚰 Funding Sandbox: Minting ${formatUnits(
        missingToken1,
        await getTokenDecimals(TOKEN1)
      )} of Token1 (${TOKEN1})`
    );
    await fundSandbox(PAIR_ADDRESS, missingToken1.toString(), TOKEN1);
  }
 
  if (missingToken0 > 0) {
    console.log(
      `⚠️ ${TOKEN0} Sandbox reserves are lower than mainnet! Minting additional tokens...`
    );
    console.log(
      `🚰 Funding Sandbox: Minting ${formatUnits(
        missingToken0,
        await getTokenDecimals(TOKEN0)
      )} of Token0 (${TOKEN0})`
    );
    await fundSandbox(PAIR_ADDRESS, missingToken0.toString(), TOKEN0);
  }
 
  // 🔹 BURN EXCESS TOKENS IF RESERVES ARE HIGHER THAN MAINNET
  if (excessToken1 > 0) {
    console.log(
      `⚠️ ${TOKEN1} Sandbox reserves are higher than mainnet! Burning excess tokens...`
    );
    console.log(
      `🔥 Burning ${formatUnits(
        excessToken1,
        await getTokenDecimals(TOKEN1)
      )} of Token1 (${TOKEN1})`
    );
    await burnExcessTokens(PAIR_ADDRESS, TOKEN1, excessToken1.toString());
  }
 
  if (excessToken0 > 0) {
    console.log(
      `⚠️ ${TOKEN0} Sandbox reserves are higher than mainnet! Burning excess tokens...`
    );
    console.log(
      `🔥 Burning ${formatUnits(
        excessToken0,
        await getTokenDecimals(TOKEN0)
      )} of Token0 (${TOKEN0})`
    );
    await burnExcessTokens(PAIR_ADDRESS, TOKEN0, excessToken0.toString());
  }
 
  // Perform sync to finalize state update
  console.log("🔄 Performing sync() update the state...");
  try {
    await pairContract.sync();
    console.log("✅ Pair state updated successfully with sync()");
  } catch (error) {
    console.error(`❌ Error calling sync(): ${error.message}`);
    console.log("⚠️ Attempting to use skim() as fallback...");
    try {
      await pairContract.skim(FUNDER_ADDRESS);
      console.log("✅ Pair state updated successfully with skim()");
    } catch (innerError) {
      console.error(`❌ Error calling skim(): ${innerError.message}`);
    }
  }
 
  console.log("✅ Reserves adjusted successfully.");
  console.log("🔄 Fetching new reserves from mainnet and sandbox...");
  const { sandboxReserves: sandboxReservesNew } =
    await getMainnetAndSandboxStates();
  console.log("✅ Mainnet Reserves");
  console.log(
    `${await getTokenName(TOKEN0)} : `,
    formatUnits(mainnetReserves.reserve0, await getTokenDecimals(TOKEN0))
  );
  console.log(
    `${await getTokenName(TOKEN1)} : `,
    formatUnits(mainnetReserves.reserve1, await getTokenDecimals(TOKEN1))
  );
 
  console.log("✅ Updated Sandbox Reserves");
  console.log(
    `${await getTokenName(TOKEN0)} : `,
    formatUnits(sandboxReservesNew.reserve0, await getTokenDecimals(TOKEN0))
  );
  console.log(
    `${await getTokenName(TOKEN1)} : `,
    formatUnits(sandboxReservesNew.reserve1, await getTokenDecimals(TOKEN1))
  );
 
  console.assert(
    sandboxReservesNew.reserve0 == mainnetReserves.reserve0 &&
      sandboxReservesNew.reserve1 == mainnetReserves.reserve1,
    "❌ Failed to sync mainnet and sandbox reserves"
  );
};

Trigger Synchronization

Run synchronization through the main script that checks and ensures pair address funding, executes mint/burn actions, and synchronizes states.

// -------------- MAIN SCRIPT --------------
try {
  let nativeFundsForPair = await getNativeBalanceForAccount(PAIR_ADDRESS);
  if (+nativeFundsForPair <= 0) {
    console.log("====================================");
    console.log("🟠 Getting Native Funds on Pair Address");
    console.log("====================================");
    await deploySelfDestructContract(parseEther("1000000").toString());
  }
  await syncV2Reserves();
} catch (error) {
  console.error(`❌ Error adjusting reserves:\n`);
  console.error(error);
}

Executing the Script

Run with npm start. An illustrative output demonstrates successful synchronization of sandbox reserves with mainnet.

The output below, is generated for the token pairs, USDC/WETH and are synced with Ethereum Mainnet

npm start
 
> [email protected] start
> npx tsx src/index.ts
 
🔄 Fetching reserves from mainnet and sandbox...
 Mainnet Reserves
USDC :  10123482.100533
WETH :  4880.497939626477228382
 Sandbox Reserves
USDC :  10121804.559038
WETH :  4881.450699071537339235
Token0 : 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
Token1 : 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
RAW :: missingToken0 : 1677541495
RAW :: missingToken1 : 0
RAW :: excessToken0 : 0
RAW :: excessToken1 : 952759445060110853
⚠️ 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 Sandbox reserves are lower than mainnet! Minting additional tokens...
🚰 Funding Sandbox: Minting 1677.541495 of Token0 (0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48)
0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 Balance Before : 10121804.559038
🚰 Requesting BuildBear faucet for 1677541495 wei of 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48...
 Faucet Success: Funded 1677.541495 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 to 0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc
0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 Balance After : 10123482.100533
⚠️ 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 Sandbox reserves are higher than mainnet! Burning excess tokens...
🔥 Burning 0.952759445060110853 of Token1 (0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2)
 Burned 952759445060110853 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 tokens
🔄 Performing sync() update the state...
 Pair state updated successfully with sync()
 Reserves adjusted successfully.
🔄 Fetching new reserves from mainnet and sandbox...
 Mainnet Reserves
USDC :  10123482.100533
WETH :  4880.497939626477228382
 Updated Sandbox Reserves
USDC :  10123482.100533
WETH :  4880.497939626477228382

Conclusion

You've now successfully learned how to synchronize Uniswap V2 token reserves in your BuildBear Sandbox environment with live mainnet data. Leveraging these techniques will enhance your ability to build accurate and deterministic testing environments, enabling precise development and validation of DeFi applications.

For the complete tutorial and codebase, refer to the GitHub repository.