Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,8 @@ USDT_OWNER_ETH_ADDRESS=0x6C96dE32CEa08842dcc4058c14d3aaAD7Fa41dee
USDT0_OWNER_ARBITRUM_ADDRESS=0xF977814e90dA44bFA03b6295A0616a897441aceC
USDT0_OWNER_UNICHAIN_ADDRESS=0x1F98400000000000000000000000000000000004
USDT0_OWNER_POLYGON_ADDRESS=0x67366782805870060151383F4BbFF9daB53e5cD6

TENDERLY_NODE_BASE=
TENDERLY_ACCESS_KEY=
TENDERLY_PROJECT=
TENDERLY_ACCOUNT=
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ ignition/deployments/chain-31337

echidna-corpus
crytic-export

scripts/results/
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,38 @@ You can optionally set VERIFY to `true` in order to publish the source code to E
[Base](deployments/deploy-base.log), [Optimism Mainnet](deployments/deploy-opmainnet.log), [Arbitrum One](deployments/deploy-arbitrumone.log),
[Ethereum](deployments/deploy-ethereum.log), [Polygon Mainnet](deployments/deploy-polygon.log), [Unichain](deployments/deploy-unichain.log)

### Deployment simulation
To ensure that the deployment machine is not corrupted and the deployment is performed without any side effects (i.e. the storage is not tampered with), the simulation could be performed on a separate machine and the logs could be compared with the actual deployment logs.

#### Steps to perform:
On two machines (the deployment machine and the validation machine):
1. Fill in the `TENDERLY_NODE_BASE`, `TENDERLY_ACCESS_KEY`, `TENDERLY_PROJECT`, and `TENDERLY_ACCOUNT` variables in the `.env` file.

2. Run the `simulate` command for the inteded deployment script, for example:
```
npm run dry:deploy-repayer-base-simulate
```

3. Compare the output logs in the `scripts/results/simulation` folder from both machines. There will be two files: `sim-<timestamp>.json` and `statediff-<timestamp>.json`. The first one contains the logs of the deployment simulation, and the second one contains the storage diff of the simulated contracts. Make sure they match on both machines (except timestamps or order of entries).

4. On the deployment machine, run the actual deployment command, for example:
```
npm run deploy-repayer-base
```

The deployment script will save the deployment transaction hashes in the `scripts/results/transactions` folder in a file named `txs-<timestamp>.json`.

The deployment script will also generate the state diff files for the actual deployment (`scripts/results/simulation/statediff-<timestamp>.json`). Compare them with the state diff logs from the simulation step.

5. On the validation machine, collect the state diff files from Tenderly for the actual deployment by running the Hardhat task:
```
npx ts-node scripts/collect-tx-state-changes.ts --file scripts/transactions/txs-<timestamp>.json
```

The resulting state changes will be written to the `scripts/results/tx-state-changes` directory in the form of `tx-state-changes-<timestamp>.json`.

6. Compare the actual state diff files from both machines to ensure consistency.

### Hardhat tasks

In order to update onchain rebalancer or repayer configurations to reflect what is put into configuration, execute the following tasks:
Expand Down
9 changes: 9 additions & 0 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,15 @@ task("push-native-token", "Push native currency through a selfdestruct")
console.log("Done.");
});

task("get-proof", "Get proof of a contract")
.addParam("contract", "Contract address")
.setAction(async ({contract}: {contract: string}, hre) => {
console.log(contract);
const proof = await hre.ethers.provider.send("eth_getProof", [contract, [], "latest"]);
console.log(`Proof for the address ${contract}:`);
console.log(proof);
});

const accounts: string[] = isSet(process.env.PRIVATE_KEY) ? [process.env.PRIVATE_KEY || ""] : [];

const config: HardhatUserConfig = {
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
"dry:deploy-opmainnet": "DRY_RUN=OP_MAINNET VERIFY=false ts-node --files ./scripts/deploy.ts",
"dry:deploy-unichain": "DRY_RUN=UNICHAIN VERIFY=false ts-node --files ./scripts/deploy.ts",
"dry:deploy-base-stage": "DRY_RUN=BASE DEPLOY_TYPE=STAGE VERIFY=false ts-node --files ./scripts/deploy.ts",
"dry:deploy-base-simulate": "DRY_RUN=BASE DEPLOY_TYPE=STAGE VERIFY=false SIMULATE=true ts-node --files ./scripts/deploy.ts",
"dry:deploy-arbitrumone-stage": "DRY_RUN=ARBITRUM_ONE DEPLOY_TYPE=STAGE VERIFY=false ts-node --files ./scripts/deploy.ts",
"dry:deploy-opmainnet-stage": "DRY_RUN=OP_MAINNET DEPLOY_TYPE=STAGE VERIFY=false ts-node --files ./scripts/deploy.ts",
"dry:deploy-ethereum-stage": "DRY_RUN=ETHEREUM DEPLOY_TYPE=STAGE VERIFY=false ts-node --files ./scripts/deploy.ts",
Expand All @@ -132,6 +133,7 @@
"dry:deploy-erc4626adapterusdc-arbitrumone": "DRY_RUN=ARBITRUM_ONE VERIFY=false ts-node --files ./scripts/deployERC4626Adapter.ts",
"dry:deploy-usdcpool-ethereum-stage": "DRY_RUN=ETHEREUM DEPLOY_TYPE=STAGE VERIFY=false ts-node --files ./scripts/deployUSDCPool.ts",
"dry:deploy-usdcpool-base-stage": "DRY_RUN=BASE DEPLOY_TYPE=STAGE VERIFY=false ts-node --files ./scripts/deployUSDCPool.ts",
"dry:deploy-usdcpool-base-simulate": "DRY_RUN=BASE DEPLOY_TYPE=STAGE VERIFY=false SIMULATE=true ts-node --files ./scripts/deployUSDCPool.ts",
"dry:deploy-usdcpoolaave-base-stage": "DRY_RUN=BASE DEPLOY_TYPE=STAGE VERIFY=false ts-node --files ./scripts/deployUSDCPoolAave.ts",
"dry:deploy-usdcstablecoinpool-base-stage": "DRY_RUN=BASE DEPLOY_TYPE=STAGE VERIFY=false ts-node --files ./scripts/deployUSDCStablecoinPool.ts",
"dry:deploy-usdcpool-opmainnet-stage": "DRY_RUN=OP_MAINNET DEPLOY_TYPE=STAGE VERIFY=false ts-node --files ./scripts/deployUSDCPool.ts",
Expand Down Expand Up @@ -187,6 +189,9 @@
"dry:upgrade-repayer-polygon-stage": "DRY_RUN=POLYGON_MAINNET DEPLOY_TYPE=STAGE VERIFY=false ts-node --files ./scripts/upgradeRepayer.ts",
"dry:upgrade-repayer-bsc-stage": "DRY_RUN=BSC DEPLOY_TYPE=STAGE VERIFY=false ts-node --files ./scripts/upgradeRepayer.ts",
"dry:upgrade-repayer-gnosis-stage": "DRY_RUN=GNOSIS_CHAIN DEPLOY_TYPE=STAGE VERIFY=false ts-node --files ./scripts/upgradeRepayer.ts",
"dry:deploy-repayer-base-simulate": "DRY_RUN=BASE SIMULATE=true VERIFY=false ts-node --files ./scripts/deployRepayer.ts",
"dry:upgrade-repayer-ethereum-simulate": "DRY_RUN=ETHEREUM SIMULATE=true VERIFY=false ts-node --files ./scripts/upgradeRepayer.ts",
"dry:upgrade-repayer-base-simulate": "DRY_RUN=BASE SIMULATE=true VERIFY=false ts-node --files ./scripts/upgradeRepayer.ts",
"lint": "npm run lint:solidity && npm run lint:ts",
"lint:solidity": "solhint 'contracts/**/*.sol'",
"lint:ts": "eslint --ignore-pattern 'coverage/'",
Expand Down
133 changes: 133 additions & 0 deletions scripts/collect-tx-state-changes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import dotenv from "dotenv";
dotenv.config();
import axios from "axios";
import {promises as fs} from "fs";
import * as path from "path";

interface StorageChange {
address: string;
slots: { slot: string; from: string; to: string }[];
}

interface TxResult {
txHash: string;
networkId: string;
status: boolean;
stateChanges: StorageChange[];
error?: string;
}

async function fetchTxStateChanges(txHash: string, networkId: string): Promise<TxResult> {
const {TENDERLY_ACCESS_KEY, TENDERLY_PROJECT, TENDERLY_ACCOUNT} = process.env;
if (!TENDERLY_ACCESS_KEY || !TENDERLY_PROJECT || !TENDERLY_ACCOUNT) {
throw new Error("Missing TENDERLY_ACCESS_KEY, TENDERLY_PROJECT, or TENDERLY_ACCOUNT");
}

const url = `https://api.tenderly.co/api/v1/account/${TENDERLY_ACCOUNT}/project/` +
`${TENDERLY_PROJECT}/network/${networkId}/trace/${txHash}`;

try {
const {data} = await axios.get(url, {
headers: {"X-Access-Key": TENDERLY_ACCESS_KEY},
});

const callTrace = data.call_trace;
const status = !(callTrace?.error || callTrace?.error_reason);

const stateChanges: StorageChange[] = (data.state_diff || [])
.filter((d: any) => d.raw?.length > 0)
.map((d: any) => ({
address: d.address,
slots: d.raw.map((r: any) => ({slot: r.key, from: r.original, to: r.dirty})),
}));

return {txHash, networkId, status, stateChanges};
} catch (error: any) {
return {txHash, networkId, status: false, stateChanges: [], error: error.message};
}
}

async function main() {
const args = process.argv.slice(2);

if (args.length === 0) {
console.log("Usage: npx ts-node scripts/collect-tx-state-changes.ts --file <path> | <txHash:networkId> ...");
process.exit(1);
}

let transactions: { txHash: string; networkId: string }[] = [];
let outputPath: string | undefined;

for (let i = 0; i < args.length; i++) {
if (args[i] === "--file") {
transactions = JSON.parse(await fs.readFile(args[++i], "utf8"));
} else if (args[i] === "--output") {
outputPath = args[++i];
} else if (args[i].includes(":")) {
const [txHash, networkId] = args[i].split(":");
transactions.push({txHash, networkId});
}
}

if (!transactions.length) {
console.error("No transactions provided");
process.exit(1);
}

console.log(`\nCollecting state changes for ${transactions.length} transaction(s)...\n`);

const results: TxResult[] = [];
for (const tx of transactions) {
console.log(`Fetching ${tx.txHash}...`);
const result = await fetchTxStateChanges(tx.txHash, tx.networkId);
results.push(result);
const statusMsg = result.status ? "Success" : "Failed";
const msg = result.error ? `Error: ${result.error}` : `${statusMsg}, ${result.stateChanges.length} addresses`;
console.log(` ${msg}`);
}

// Aggregate storage changes by address
const aggregated = new Map<string, StorageChange>();
for (const r of results) {
for (const c of r.stateChanges) {
const addr = c.address.toLowerCase();
if (!aggregated.has(addr)) {
aggregated.set(addr, {address: c.address, slots: []});
}
const existing = aggregated.get(addr)!;
for (const s of c.slots) {
const idx = existing.slots.findIndex((x) => x.slot === s.slot);
if (idx >= 0) existing.slots[idx].to = s.to;
else existing.slots.push({...s});
}
}
}

const output = {transactions: results, aggregatedStateChanges: Array.from(aggregated.values())};

if (!outputPath) {
const dir = path.join(process.cwd(), "./scripts/results/tx-state-changes");
await fs.mkdir(dir, {recursive: true});
outputPath = path.join(dir, `tx-${new Date().toISOString().replace(/[:.]/g, "-")}.json`);
}
await fs.writeFile(outputPath, JSON.stringify(output, null, 2));
console.log(`\nResults written to ${outputPath}`);

// Summary
console.log("\n=== Summary ===");
console.log(`Successful: ${results.filter((t) => t.status).length}/${results.length}`);
console.log(`Addresses with changes: ${aggregated.size}`);

for (const [, change] of aggregated) {
console.log(`\n${change.address}: ${change.slots.length} slot(s)`);
for (const s of change.slots.slice(0, 5)) {
console.log(` ${s.slot}: ${s.from} -> ${s.to}`);
}
if (change.slots.length > 5) console.log(` ... +${change.slots.length - 5} more`);
}
}

main().catch((e) => {
console.error(e);
process.exit(1);
});
Loading
Loading