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.
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.
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 environment variablesconst 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.
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.
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
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;}
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.
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.
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.