Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update cast-on-tenderly script #388

Merged
merged 10 commits into from
Feb 12, 2024
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,15 @@ export ETH_GAS_PRICE=$(seth --to-wei 420 "gwei")

1. Create Tenderly account (no trial period needed atm) https://dashboard.tenderly.co/register
- Note down `TENDERLY_USER` and `TENDERLY_PROJECT` values
2. Create Tenderly access token https://dashboard.tenderly.co/account/authorization
2. Create Tenderly access token (on the account level!) https://dashboard.tenderly.co/account/authorization
- Note down `TENDERLY_ACCESS_KEY` values
3. Export env vars specified above or create `scripts/cast-on-tenderly/.env` file with them
3. Export required env vars via `export` or create `scripts/cast-on-tenderly/.env` file with them:
```env
ETH_RPC_URL=""
TENDERLY_USER=""
TENDERLY_PROJECT=""
TENDERLY_ACCESS_KEY=""
```
4. Execute `make cast-on-tenderly spell=0x...`, with the address of the spell that hasn't been casted yet
- The execution should finish with `successfully casted`
5. Open the `publicly sharable transaction url` printed into the console (it should require no credentials)
5. Open the `public explorer url` printed into the console (it should require no credentials)
157 changes: 113 additions & 44 deletions scripts/cast-on-tenderly/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import 'dotenv/config';
import axios from 'axios';
import { Contract, ethers } from 'ethers';
import { Contract, ethers, utils } from 'ethers';
import { randomUUID } from 'crypto';

const NETWORK_ID = 1;
const NETWORK_ID = '1';
const CHAINLOG_ADDRESS = '0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F';
const CHIEF_HAT_SLOT = 12;
const DEFAULT_TRANSACTION_PARAMETERS = { gasLimit: 1_000_000_000 };

// check env vars
const REQUIRED_ENV_VARS = ['TENDERLY_USER', 'TENDERLY_PROJECT', 'TENDERLY_ACCESS_KEY'];
const REQUIRED_ENV_VARS = ['ETH_RPC_URL', 'TENDERLY_USER', 'TENDERLY_PROJECT', 'TENDERLY_ACCESS_KEY'];
if (REQUIRED_ENV_VARS.some(varName => !process.env[varName])) {
throw new Error(`Please provide all required env variables: ${REQUIRED_ENV_VARS.join(', ')}`);
}
Expand All @@ -19,63 +20,123 @@ if (!SPELL_ADDRESS) {
throw new Error('Please provide address of the spell, e.g.: `node index.js 0x...`');
}

const makeTenderlyApiRequest = async function (path) {
const API_BASE = `https://api.tenderly.co/api/v1/account/${process.env.TENDERLY_USER}/project/${process.env.TENDERLY_PROJECT}`;
return await axios.post(
`${API_BASE}${path}`,
{ network_id: NETWORK_ID },
{ headers: { 'X-Access-Key': process.env.TENDERLY_ACCESS_KEY } }
const getSpellName = async function (spellAddress) {
const provider = new ethers.providers.JsonRpcProvider(process.env.ETH_RPC_URL);
const spell = new Contract(
spellAddress,
['function description() external view returns (string memory)'],
provider
);
const description = await spell.description();
return description.split('|')[0].trim();
};

const createTenderlyFork = async function () {
const response = await makeTenderlyApiRequest('/fork');
const forkId = response.data.simulation_fork.id;
const rpcUrl = `https://rpc.tenderly.co/fork/${forkId}`;
const forkUrl = `https://dashboard.tenderly.co/${process.env.TENDERLY_USER}/${process.env.TENDERLY_PROJECT}/fork/${forkId}`;
return { forkId, forkUrl, rpcUrl };
const makeTenderlyApiRequest = async function ({ method, path, body }) {
const API_BASE = `https://api.tenderly.co/api/v1/account/${process.env.TENDERLY_USER}/project/${process.env.TENDERLY_PROJECT}`;
try {
return await axios[method](`${API_BASE}${path}`, body, {
headers: {
'content-type': 'application/json',
accept: 'application/json, text/plain, */*',
'X-Access-Key': process.env.TENDERLY_ACCESS_KEY,
},
});
} catch (error) {
console.error('makeTenderlyApiRequest error', error.response);
throw new Error(`tenderly request failed with: ${error.code}`);
}
};

const publishTenderlyTransaction = async function (forkId, transactionId) {
await makeTenderlyApiRequest(`/fork/${forkId}/transaction/${transactionId}/share`);
return `https://dashboard.tenderly.co/shared/fork/simulation/${transactionId}`;
const createTenderlyTestnet = async function (spellName) {
const slug = `${spellName.replace(/\s+/g, '-').toLowerCase()}-${randomUUID()}`;
const response = await makeTenderlyApiRequest({
method: 'post',
path: '/testnet/container',
body: {
slug,
displayName: spellName,
description: spellName,
networkConfig: {
networkId: NETWORK_ID,
blockNumber: 'latest',
baseFeePerGas: '1',
chainConfig: {
chainId: NETWORK_ID,
},
},
explorerPage: 'DISABLED',
syncState: true,
},
});
const testnetId = response.data.container.id;
const rpcEndpointPrivate = response.data.container.connectivityConfig.endpoints.find(
endpoint => endpoint.displayName === 'unlocked'
);
console.info(`tenderly testnet "${testnetId}" is created`);
return {
testnetId,
rpcUrlPrivate: rpcEndpointPrivate.uri,
};
};

const runSpell = async function () {
const { forkId, forkUrl, rpcUrl } = await createTenderlyFork();
console.info('private tenderly fork is created', forkUrl);

const provider = new ethers.providers.JsonRpcProvider(rpcUrl);
const signer = provider.getSigner();
const publishTenderlyTestnet = async function (testnetId) {
console.info(`making tenderly testnet "${testnetId}" public...`);
const response = await makeTenderlyApiRequest({
method: 'put',
path: `/testnet/container/${testnetId}`,
body: {
explorerPage: 'ENABLED',
},
});
if (response.data?.container?.explorer_page !== 'ENABLED') {
throw new Error('failed to publish testnet');
}
console.info(`tenderly testnet is now public and discoverable`);
const rpcEndpointPublic = response.data.container.connectivityConfig.endpoints.find(
endpoint => endpoint.displayName === 'testnet'
);
const explorerUrlPublic = `https://dashboard.tenderly.co/explorer/vnet/${rpcEndpointPublic.id}`;
console.info(`public explorer url: ${explorerUrlPublic}`);
return { rpcUrlPrivate: rpcEndpointPublic.uri, explorerUrlPublic };
};

const giveTheHatToSpell = async function (spellAddress, provider) {
console.info('fetching the chief address from chainlog...');
const chainlog = new Contract(
CHAINLOG_ADDRESS,
['function getAddress(bytes32) external view returns (address)'],
signer
provider.getSigner()
);
const chiefAddress = await chainlog.getAddress(ethers.utils.formatBytes32String('MCD_ADM'));

console.info('overwriting the hat...');
await provider.send('tenderly_setStorageAt', [
chiefAddress,
ethers.utils.hexZeroPad(ethers.utils.hexValue(CHIEF_HAT_SLOT), 32),
ethers.utils.hexZeroPad(SPELL_ADDRESS, 32),
ethers.utils.hexZeroPad(spellAddress, 32),
]);

console.info('checking the hat...');
const chief = new Contract(chiefAddress, ['function hat() external view returns (address)'], signer);
const chief = new Contract(chiefAddress, ['function hat() external view returns (address)'], provider.getSigner());
const hatAddress = await chief.hat();
if (hatAddress !== SPELL_ADDRESS) {
if (hatAddress !== spellAddress) {
throw new Error('spell does not have the hat');
}
console.info('spell have the hat...');
};

const sheduleWarpAndCastSpell = async function (spellAddress, provider) {
amusingaxl marked this conversation as resolved.
Show resolved Hide resolved
const spell = new Contract(
SPELL_ADDRESS,
['function schedule() external', 'function cast() external', 'function eta() external view returns (uint256)'],
signer
spellAddress,
[
'function schedule() external',
'function cast() external',
'function nextCastTime() external view returns (uint256)',
],
provider.getSigner()
);
console.info('scheduling spell on a fork...');

console.info('scheduling the spell...');
try {
const scheduleTx = await spell.schedule(DEFAULT_TRANSACTION_PARAMETERS);
await scheduleTx.wait();
Expand All @@ -84,27 +145,35 @@ const runSpell = async function () {
}

console.info('fetching timestamp when the spell will be castable...');
const eta = await spell.eta();
const nextCastTime = await spell.nextCastTime();
console.info(`nextCastTime is "${nextCastTime}"`, new Date(nextCastTime.toNumber() * 1000));

console.info(`warping the time to "${eta}"...`);
const currentUnixTimestamp = Math.floor(Date.now() / 1000);
if (currentUnixTimestamp < eta) {
const timestampDifference = eta - currentUnixTimestamp + 1;
await provider.send('evm_increaseTime', [ethers.utils.hexValue(timestampDifference)]);
}
console.info(`warping the time to "${nextCastTime}"...`);
await provider.send('evm_setNextBlockTimestamp', [ethers.utils.hexValue(nextCastTime)]);

console.info('casting spell on a fork...');
console.info('casting spell...');
try {
const castTx = await spell.cast(DEFAULT_TRANSACTION_PARAMETERS);
await castTx.wait();
console.info('successfully casted');
} catch (error) {
console.error('casting failed', error);
}
};

const castOnTenderly = async function (spellAddress) {
const spellName = await getSpellName(spellAddress);
console.info(`preparing to cast spell "${spellAddress}" with name "${spellName}"...`);

const { testnetId, rpcUrlPrivate } = await createTenderlyTestnet(spellName);

const provider = new ethers.providers.JsonRpcProvider(rpcUrlPrivate);

await giveTheHatToSpell(spellAddress, provider);

await sheduleWarpAndCastSpell(spellAddress, provider);

const lastTransactionId = await provider.send('evm_getLatest', []);
const publicTransactionUrl = await publishTenderlyTransaction(forkId, lastTransactionId);
console.info('publicly sharable transaction url', publicTransactionUrl);
await publishTenderlyTestnet(testnetId);
};

runSpell();
castOnTenderly(utils.getAddress(SPELL_ADDRESS));
20 changes: 10 additions & 10 deletions scripts/cast-on-tenderly/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading