Simulation API With State Overrides
State overrides allow you to customize the network state for the purpose of your simulation. This feature is useful when you need to test scenarios under conditions that aren’t currently present on the live network.
To illustrate how state overrides work, let’s use the DAI stablecoin contract. In the DAI system, certain actions, like minting new DAI tokens, can only be performed by addresses designated as wards
. If your address isn’t a ward, you can’t test minting operations.
Setting up as a ward through state overrides
To simulate a transaction where we mint DAI, we need to override the state to make our address appear as a ward. We need to adjust the wards
map in the DAI contract to include our address with the necessary permissions.
To be able to mint, the sender of the transaction must be a ward. In other words, the following condition must hold: wards['0xe2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2'] == 1
.
In the API request, assign a map of overrides to the state_objects
object. This map points to specific storage locations in the smart contract and assigns new values for the simulation.
- DAI contract address:
0x6b175474e89094c44da98b954eedeac495271d0f
- Storage slot location:
0xedd7d04419e9c48ceb6055956cbb4e2091ae310313a4d1fa7cbcfe7561616e03
- Override value:
0x0000000000000000000000000000000000000000000000000000000000000001
{
"state_objects": {
"0x6b175474e89094c44da98b954eedeac495271d0f": {
"storage": {
"0xedd7d04419e9c48ceb6055956cbb4e2091ae310313a4d1fa7cbcfe7561616e03": "0x0000000000000000000000000000000000000000000000000000000000000001"
}
}
}
}
The script below shows how to set the overrides and send a request to Simulation API.
Make sure to have TENDERLY_ACCOUNT_SLUG
and TENDERLY_ACCESS_KEY
set as environment variables. Learn how to obtain the slug and access key.
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 mintDai = async () => {
console.time('Simulation');
const fakeWardAddress = '0xe2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2';
// calculate the storage location of `wards[fakeWardAddress]`
// yields 0xedd7d04419e9c48ceb6055956cbb4e2091ae310313a4d1fa7cbcfe7561616e03
const overrideStorageLocation = ethers.utils.keccak256(
ethers.utils.concat([
ethers.utils.hexZeroPad(fakeWardAddress, 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
]),
);
console.log('Storage Override Location', overrideStorageLocation);
const mint = (
await axios.post(
`https://api.tenderly.co/api/v1/account/${TENDERLY_ACCOUNT_SLUG}/project/${TENDERLY_PROJECT_SLUG}/simulate`,
// the transaction
{
save: true,
save_if_fails: true,
simulation_type: 'full',
network_id: '1',
from: '0xe2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2',
to: '0x6b175474e89094c44da98b954eedeac495271d0f',
input:
'0x40c10f19000000000000000000000000e58b9ee93700a616b50509c8292977fa7a0f8ce10000000000000000000000000000000000000000000000001bc16d674ec80000',
gas: 8000000,
state_objects: {
'0x6b175474e89094c44da98b954eedeac495271d0f': {
storage: {
'0xedd7d04419e9c48ceb6055956cbb4e2091ae310313a4d1fa7cbcfe7561616e03':
'0x0000000000000000000000000000000000000000000000000000000000000001',
},
},
},
},
{
headers: {
'X-Access-Key': TENDERLY_ACCESS_KEY as string,
},
},
)
).data;
console.timeEnd('Simulation');
console.log(JSON.stringify(mint, null, 2));
// TODO: extract token transferred
};
mintDai();
Storage Slot Calculation
The key part of setting an override is calculating the correct storage location. This depends on the structure of the contract’s storage and the specific variable you want to override.
For the DAI contract, the storage location for the wards
mapping for our address is calculated based on the Solidity storage layout rules, and is given by this expression:
const fakeWardAddress = '0xe2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2';
const overrideStorageLocation = keccak256(
concatenate(
hexZeroPad(fakeWardAddress), hexZeroPad(0x0)
)
)
In our example, the storage location is: 0xedd7d04419e9c48ceb6055956cbb4e2091ae310313a4d1fa7cbcfe7561616e03
.