🤔 Got questions? Schedule an office hours session.
Forks
Using Forks With Ethers.js

Using Forks With Ethers.js

Migrate to Virtual TestNets

Virtual TestNets are publicly available!
For new projects, we recommend starting with TestNets.
For automatic migration of Forks to TestNets, .

The following code sample will guide you through:

  • Creating a Fork using the Fork API with custom chain config.
  • Connecting Ethers.js to the Fork.
  • Completing a Fork state override.
  • Sending several transactions.

In this example, we’ll mint, approve, and transferFrom some DAI. We’ll use the Mainnet Dai contract:

  1. TX1 – mint: We’ll use an arbitrary address ($minter) to mint 2 DAI for $owner.
  2. TX2 – approve: The address $owner will approve $spender to spend 0.5 DAI.
  3. TX3 – transferFrom: The address we just approved ($spender) will transfer 0.25 DAI to $receiver.

We have three participants in this scenario: $minter, $spender, and $receiver. Instead of dealing with real addresses, we’ll use accounts available on the Fork upon creation (lines 29..32).

example.ts
import axios from 'axios';
import * as dotenv from 'dotenv';
import { ethers } from 'ethers';
 
dotenv.config();
 
// assuming environment variables TENDERLY_ACCOUNT_SLUG, TENDERLY_PROJECT_SLUG and TENDERLY_ACCESS_KEY are set
// https://docs.tenderly.co/account/projects/account-project-slug
// https://docs.tenderly.co/account/projects/how-to-generate-api-access-token
const { TENDERLY_ACCOUNT_SLUG, TENDERLY_PROJECT_SLUG, TENDERLY_ACCESS_KEY } = process.env;
 
const daiOnFork = async () => {
  console.time('Fork Creation');
  const fork = await mainnetFork();
  console.timeEnd('Fork Creation');
 
  const forkId = fork.data.simulation_fork.id;
  const rpcUrl = `https://rpc.tenderly.co/fork/${forkId}`;
  // const rpcUrl = 'https://rpc.tenderly.co/fork/###-###-###-###';
  console.log('Fork URL\n\t' + rpcUrl);
 
  const forkProvider = new ethers.providers.JsonRpcProvider(rpcUrl);
 
  const [minterAddress, ownerAddress, spenderAddress, receiverAddress] =
    await forkProvider.listAccounts();
 
  const [minterSigner, ownerSigner, spenderSigner] = [
    forkProvider.getSigner(minterAddress),
    forkProvider.getSigner(ownerAddress),
    forkProvider.getSigner(spenderAddress),
    forkProvider.getSigner(receiverAddress),
  ];
 
  /*
  // alternatively, you could as well use an existing account you do have private key access to.
 
  const minterAddress = '0xdc6bdc37b2714ee601734cf55a05625c9e512461';
  const minterSigner = new ethers.Wallet(
    '0x94...5c'
  ).connect(forkProvider);
 
    await forkProvider.send('tenderly_setBalance', [
      [minterAddress],
      ethers.utils.hexValue(ethers.utils.parseUnits('10', 'ether').toHexString()),
    ]);
 
  */
 
  // override: make 0xdc6bdc37b2714ee601734cf55a05625c9e512461 a ward of DAI
  await forkProvider.send('tenderly_setStorageAt', [
    // the DAI contract address
    '0x6b175474e89094c44da98b954eedeac495271d0f',
    // storage location wards['0xdc6bdc37b2714ee601734cf55a05625c9e512461']
    ethers.utils.keccak256(
      ethers.utils.concat([
        ethers.utils.hexZeroPad(minterAddress, 32), // the ward address (address 0x000..0) - mapping key
        ethers.utils.hexZeroPad('0x0', 32), // the wards slot is 0th  in the DAI contract - the mapping variable
      ]),
    ),
    // flag 1 (at 32 bytes length)
    '0x0000000000000000000000000000000000000000000000000000000000000001',
  ]);
 
  // TX1: mint
  await minterSigner.sendTransaction({
    from: minterAddress,
    to: '0x6b175474e89094c44da98b954eedeac495271d0f',
    data: DaiAbi.encodeFunctionData('mint', [
      ethers.utils.hexZeroPad(ownerAddress.toLowerCase(), 20),
      ethers.utils.parseEther('1.0'),
    ]),
    gasLimit: 800000,
  });
 
  // TX2: approve
  await ownerSigner.sendTransaction({
    to: '0x6b175474e89094c44da98b954eedeac495271d0f',
    data: DaiAbi.encodeFunctionData('approve', [
      ethers.utils.hexZeroPad(spenderAddress.toLowerCase(), 20),
      ethers.utils.parseEther('1.0'),
    ]),
    gasLimit: 800000,
  });
 
  // TX3: transferFrom
  await spenderSigner.sendTransaction({
    to: '0x6b175474e89094c44da98b954eedeac495271d0f',
    data: DaiAbi.encodeFunctionData('transferFrom', [
      ethers.utils.hexZeroPad(ownerAddress.toLowerCase(), 20),
      ethers.utils.hexZeroPad(receiverAddress.toLowerCase(), 20),
      ethers.utils.parseEther('1.0'),
    ]),
    gasLimit: 800000,
  });
 
  // deleteFork(forkId);
};
 
const DaiAbi = new ethers.utils.Interface([
  {
    constant: false,
    inputs: [
      { internalType: 'address', name: 'usr', type: 'address' },
      { internalType: 'uint256', name: 'wad', type: 'uint256' },
    ],
    name: 'mint',
    outputs: [],
    payable: false,
    stateMutability: 'nonpayable',
    type: 'function',
  },
  {
    constant: false,
    inputs: [
      { internalType: 'address', name: 'usr', type: 'address' },
      { internalType: 'uint256', name: 'wad', type: 'uint256' },
    ],
    name: 'approve',
    outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
    payable: false,
    stateMutability: 'nonpayable',
    type: 'function',
  },
  {
    constant: false,
    inputs: [
      { internalType: 'address', name: 'src', type: 'address' },
      { internalType: 'address', name: 'dst', type: 'address' },
      { internalType: 'uint256', name: 'wad', type: 'uint256' },
    ],
    name: 'transferFrom',
    outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
    payable: false,
    stateMutability: 'nonpayable',
    type: 'function',
  },
]);
 
daiOnFork();
 
function deleteFork(forkId: any) {
  axios.delete(
    `https://api.tenderly.co/api/v1/account/${TENDERLY_ACCOUNT_SLUG}/project/${TENDERLY_PROJECT_SLUG}/fork/${forkId}`,
    {
      headers: {
        'X-Access-Key': TENDERLY_ACCESS_KEY as string,
      },
    },
  );
}
 
async function mainnetFork() {
  return await axios.post(
    `https://api.tenderly.co/api/v1/account/${TENDERLY_ACCOUNT_SLUG}/project/${TENDERLY_PROJECT_SLUG}/fork`,
    {
      network_id: '1',
      chain_config: {
        chain_id: 11,
        shanghai_time: 1677557088,
      },
    },
    {
      headers: {
        'X-Access-Key': TENDERLY_ACCESS_KEY as string,
      },
    },
  );
}