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, .
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:
- TX1 – mint: We’ll use an arbitrary address ($minter) to mint 2 DAI for $owner.
- TX2 – approve: The address $owner will approve $spender to spend 0.5 DAI.
- 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,
},
},
);
}