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;}
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.
Impersonating a contract address requires special permissions on the BuildBear Sandbox. Ensure you have the necessary permissions to impersonate the account.
Reach out to us at [email protected] to request this feature.
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, parseEther("1000000")); await fundSandbox(FUNDER_ADDRESS, parseEther("100")); // fund extra to pay for gas // Delay is done to ensure the faucet tx is confirmed console.log("===================================="); console.log( `🟠 Deploying Self Destruct Contract from wallet : ${wallet.address}` ); console.log("===================================="); // Bytecode for a contract that can self-destruct 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.