Skip to content

Commit 46ca61f

Browse files
authored
Merge pull request #17 from Layr-Labs/jb/tenderly
[1.2.0] Support for `zeus deploy run --fork tenderly`
2 parents f08f7a7 + 7fee2bf commit 46ca61f

File tree

17 files changed

+297
-101
lines changed

17 files changed

+297
-101
lines changed

CHANGELOG.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11

22
**[Current]**
3+
1.2.0:
4+
- Supports `zeus deploy run [--fork anvil | tenderly]`.
5+
- See full documentation at `zeus deploy run --help`
6+
- Allows applying a single upgrade w/o human interaction onto an anvil or tenderly testnet for debugging purposes.
7+
8+
**[Historical]**
39
1.1.2:
410
- Bump to support custom derivation paths.
511

@@ -12,7 +18,6 @@
1218
1.0.6:
1319
- disables gnosis verification (cringe)
1420

15-
**[Historical]**
1621
1.0.1:
1722
- `zeus test` now accepts an optional rpcUrl. Note that the RPC is checked to match the chainId
1823
of the specified environment.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@layr-labs/zeus",
3-
"version": "1.1.2",
3+
"version": "1.2.0",
44
"description": "web3 deployer / metadata manager",
55
"main": "src/index.ts",
66
"scripts": {

src/commands/args.ts

+21-1
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,30 @@ export const nonInteractive = flag({
2626
short: 'n',
2727
});
2828

29+
const FORK_DESCRIPTION = `one of: 'anvil', 'tenderly'
30+
If --fork is specified, the upgrade will be applied against a forked copy of the environment in which the deploy was to be run.
31+
32+
Applies all upgrades onto the forked testnet.
33+
- Any EOA steps are executed using a random address, that is pre-funded with 420 ETH via cheatcodes.
34+
- Any multisig steps are executed using a "sendUnsignedTransaction" cheatcode, directly from the Multisig's perspective.
35+
- Any script phases are skipped.
36+
37+
'--fork anvil': spins up a local anvil node and applies the upgrade.
38+
(1) Starts up a local anvil node
39+
(2) Applies all upgrades onto the anvil node
40+
- Any EOA steps are executed using a random address, that is pre-funded with 420 ETH via anvil cheatcodes.
41+
- Any multisig steps are executed using a "sendUnsignedTransaction" anvil cheatcode, directly from the Multisig's perspective.
42+
43+
'--fork tenderly': spins up a tenderly testnet and applies the upgrade.
44+
requires the following env vars:
45+
TENDERLY_API_KEY - an API key, valid for the given account/project.
46+
TENDERLY_ACCOUNT_SLUG - the account name.
47+
TENDERLY_PROJECT_SLUG - the project name, in which to create the virtual testnet.
48+
`;
2949

3050
export const fork = option({
3151
long: 'fork',
3252
short: 'f',
33-
description: 'one of: anvil',
53+
description: FORK_DESCRIPTION,
3454
type: optional(string)
3555
});

src/commands/deploy/cmd/run.ts

+75-39
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
import { command } from "cmd-ts";
22
import * as allArgs from '../../args';
3-
import { TState, requires, loggedIn, isLoggedIn, TLoggedInState, inRepo } from "../../inject";
3+
import { TState, requires, isLoggedIn, inRepo, TInRepoState, isInRepo } from "../../inject";
44
import { configs, getRepoRoot } from '../../configs';
55
import { getActiveDeploy, phaseType, formatNow, blankDeploy } from "./utils";
66
import { join, normalize } from 'path';
77
import { existsSync, lstatSync } from "fs";
8-
import { HaltDeployError, PauseDeployError, TExecuteOptions } from "../../../signing/strategy";
8+
import { HaltDeployError, PauseDeployError, TStrategyOptions } from "../../../signing/strategy";
99
import chalk from "chalk";
1010
import { canonicalPaths } from "../../../metadata/paths";
11-
import { createTestClient, http, TestClient, toHex } from "viem";
11+
import { createTestClient, http, parseEther, toHex } from "viem";
1212
import * as AllChains from "viem/chains";
1313
import * as allChains from 'viem/chains';
1414
import semver from 'semver';
1515
import { SavebleDocument, Transaction } from "../../../metadata/metadataStore";
1616
import { chainIdName } from "../../prompts";
1717
import { AnvilOptions, AnvilService } from '@foundry-rs/hardhat-anvil/dist/src/anvil-service';
18-
import { mnemonicToAccount } from 'viem/accounts'
19-
import { TenderlyVirtualTestnetClient } from "./utils-tenderly";
18+
import { generatePrivateKey, mnemonicToAccount, privateKeyToAccount } from 'viem/accounts'
19+
import { TenderlyVirtualTestnetClient, VirtualTestNetResponse } from "./utils-tenderly";
20+
import { AnvilTestClient, TenderlyTestClient, TestClient } from "./utils-testnets";
2021
import { TDeploy, TEnvironmentManifest, TUpgrade } from "../../../metadata/schema";
2122
import { acquireDeployLock, releaseDeployLock } from "./utils-locks";
2223
import { executeSystemPhase } from "../../../deploy/handlers/system";
@@ -36,7 +37,7 @@ const DEFAULT_ANVIL_URI = `http://127.0.0.1:8546/`;
3637

3738
const isValidFork = (fork: string | undefined) => {
3839
// TODO(tenderly) - support tenderly.
39-
return [undefined, `anvil`].includes(fork);
40+
return [undefined, `anvil`, `tenderly`].includes(fork);
4041
}
4142

4243

@@ -48,22 +49,26 @@ const throwIfUnset = (envVar: string | undefined, msg: string) => {
4849
}
4950

5051
export async function handler(_user: TState, args: {env: string, resume: boolean, rpcUrl: string | undefined, json: boolean, upgrade: string | undefined, nonInteractive: boolean | undefined, fork: string | undefined}) {
51-
if (!isLoggedIn(_user)) {
52-
return;
53-
}
54-
5552
if (!isValidFork(args.fork)) {
5653
throw new Error(`Invalid value for 'fork' - expected one of (tenderly, anvil)`);
5754
}
5855

59-
const user: TLoggedInState = _user;
56+
if (!args.fork && !isLoggedIn(_user)) {
57+
throw new Error(`To deploy to an environment, you must run from within the contracts repo while logged in.`);
58+
} else if (!isInRepo(_user)) {
59+
throw new Error(`Must be run from within the contracts repo.`);
60+
}
61+
const user = _user;
62+
6063
const repoConfig = await configs.zeus.load();
6164
if (!repoConfig) {
6265
console.error("This repo is not setup. Try `zeus init` first.");
6366
return;
6467
}
6568

6669
let anvil: AnvilService | undefined;
70+
let tenderly: TenderlyVirtualTestnetClient | undefined;
71+
let tenderlyTestnet: VirtualTestNetResponse | undefined;
6772
let overrideEoaPk: `0x${string}` | undefined;
6873
let overrideRpcUrl: string | undefined;
6974
let testClient: TestClient | undefined;
@@ -79,33 +84,52 @@ export async function handler(_user: TState, args: {env: string, resume: boolean
7984

8085
switch (args.fork) {
8186
case `tenderly`: {
82-
const tenderly = new TenderlyVirtualTestnetClient(
87+
tenderly = new TenderlyVirtualTestnetClient(
8388
throwIfUnset(process.env.TENDERLY_API_KEY, "Expected TENDERLY_API_KEY to be set."),
8489
throwIfUnset(process.env.TENDERLY_ACCOUNT_SLUG, "Expected TENDERLY_ACCOUNT_SLUG to be set."),
8590
throwIfUnset(process.env.TENDERLY_PROJECT_SLUG, "Expected TENDERLY_PROJECT_SLUG to be set."),
8691
);
8792

88-
// TODO(tenderly): continue from here
89-
const _vnetId = await tenderly.createVirtualNetwork({
90-
slug: `z-${args.env}-${formatNow()}`,
91-
display_name: `zeus testnet`,
92-
description: `CLI-created zeus testnet`,
93+
const vnetSlug = `z-${args.env}-${args.upgrade}-${formatNow()}`
94+
tenderlyTestnet = await tenderly.createVirtualNetwork({
95+
slug: vnetSlug,
96+
display_name: `${args.env} - ${args.upgrade}`,
97+
description: `automatically deployed via Zeus`,
9398
fork_config: {
94-
network_id: envManifest._.chainId.toString(),
99+
network_id: Number(envManifest._.chainId),
95100
},
96101
virtual_network_config: {
97102
chain_config: {
98-
chain_id: envManifest._.chainId
103+
chain_id: Number(envManifest._.chainId)
99104
},
100105
sync_state_config: {
101106
enabled: false
102107
},
103108
explorer_page_config: {
104109
enabled: false
105-
}
110+
},
106111
}
107112
})
113+
const tenderlyRpcUrl = tenderlyTestnet.rpcs ? tenderlyTestnet.rpcs[0].url : undefined;
114+
if (!tenderlyRpcUrl) {
115+
throw new Error(`Failed to load tenderly RPC URL.`);
116+
}
108117

118+
console.log(chalk.green(`+ created tenderly devnet: https://dashboard.tenderly.co/${process.env.TENDERLY_ACCOUNT_SLUG}/${process.env.TENDERLY_PROJECT_SLUG}/testnet/${tenderlyTestnet.id}\n`));
119+
120+
testClient = new TenderlyTestClient(tenderlyRpcUrl);
121+
122+
const specialAccountPk = generatePrivateKey();
123+
const specialAccountAddress = privateKeyToAccount(specialAccountPk).address;
124+
125+
overrideRpcUrl = tenderlyRpcUrl;
126+
overrideEoaPk = specialAccountPk;
127+
128+
// fund the special account.
129+
console.log(chalk.gray(`+ using deployer address ${specialAccountAddress}`));
130+
131+
await testClient.setBalance(specialAccountAddress, parseEther('420'));
132+
109133
break;
110134
}
111135
case `anvil`: {
@@ -123,10 +147,11 @@ export async function handler(_user: TState, args: {env: string, resume: boolean
123147
}
124148
};
125149
anvil = await AnvilService.create(opts, false);
126-
testClient = createTestClient({
150+
const viemClient = createTestClient({
127151
mode: 'anvil',
128152
transport: http(DEFAULT_ANVIL_URI)
129153
});
154+
testClient = new AnvilTestClient(viemClient);
130155
const pkRaw = mnemonicToAccount(ANVIL_MNENOMIC, {accountIndex: 0}).getHdKey().privateKey;
131156
if (!pkRaw) {
132157
throw new Error(`Invalid private key for anvil test account.`);
@@ -151,12 +176,16 @@ export async function handler(_user: TState, args: {env: string, resume: boolean
151176

152177
console.log(`[${chainIdName(deploy._.chainId)}] Resuming existing deploy... (began at ${deploy._.startTime})`);
153178
return await executeOrContinueDeployWithLock(deploy._.name, deploy._.env, user, {
154-
rpcUrl: overrideRpcUrl,
155-
nonInteractive: !!args.nonInteractive,
156-
fork: args.fork,
157-
anvil,
158-
testClient,
159-
overrideEoaPk,
179+
defaultArgs: {
180+
rpcUrl: overrideRpcUrl,
181+
nonInteractive: !!args.nonInteractive,
182+
fork: args.fork,
183+
anvil,
184+
testClient,
185+
overrideEoaPk,
186+
etherscanApiKey: !args.fork,
187+
},
188+
nonInteractive: !!args.fork
160189
});
161190
} else if (args.resume) {
162191
console.error(`Nothing to resume.`);
@@ -196,12 +225,16 @@ export async function handler(_user: TState, args: {env: string, resume: boolean
196225
console.log(chalk.green(`+ started deploy (${envManifest?._.deployedVersion ?? '0.0.0'}) => (${upgradeManifest._.to}) (requires: ${upgradeManifest._.from})`));
197226
await metaTxn.commit(`started deploy: ${deployJson._.env}/${deployJson._.name}`);
198227
await executeOrContinueDeployWithLock(deployJson._.name, deployJson._.env, user, {
199-
rpcUrl: overrideRpcUrl ?? args.rpcUrl,
200-
nonInteractive: !!args.nonInteractive,
201-
fork: args.fork,
202-
anvil,
203-
testClient,
204-
overrideEoaPk,
228+
defaultArgs: {
229+
rpcUrl: overrideRpcUrl ?? args.rpcUrl,
230+
nonInteractive: !!args.nonInteractive,
231+
fork: args.fork,
232+
anvil,
233+
testClient,
234+
overrideEoaPk,
235+
etherscanApiKey: !args.fork,
236+
},
237+
nonInteractive: !!args.fork
205238
});
206239
} finally {
207240
// shut down anvil after running
@@ -210,12 +243,15 @@ export async function handler(_user: TState, args: {env: string, resume: boolean
210243
}
211244
}
212245

213-
const executeOrContinueDeployWithLock = async (name: string, env: string, user: TLoggedInState, options: TExecuteOptions) => {
214-
const shouldUseLock = !options.fork;
215-
if (options.fork) {
246+
const executeOrContinueDeployWithLock = async (name: string, env: string, user: TInRepoState, options: TStrategyOptions) => {
247+
const shouldUseLock = !options.defaultArgs.fork;
248+
if (options.defaultArgs.fork) {
216249
// explicitly choose the logged out metadata store, to avoid writing anything.
250+
chalk.bold(`Deploying to fork: ${options.defaultArgs.fork}`);
217251
user.metadataStore = user.loggedOutMetadataStore;
218252
options.nonInteractive = true;
253+
options.defaultArgs.nonInteractive = true;
254+
options.defaultArgs.etherscanApiKey = false;
219255
}
220256

221257
const txn = await user.metadataStore.begin();
@@ -249,7 +285,7 @@ const executeOrContinueDeployWithLock = async (name: string, env: string, user:
249285
}
250286
}
251287

252-
const executeOrContinueDeploy = async (deploy: SavebleDocument<TDeploy>, _user: TState, metatxn: Transaction, options: TExecuteOptions) => {
288+
const executeOrContinueDeploy = async (deploy: SavebleDocument<TDeploy>, _user: TState, metatxn: Transaction, options: TStrategyOptions) => {
253289
try {
254290
while (true) {
255291
console.log(chalk.green(`[${deploy._.segments[deploy._.segmentId]?.filename ?? '<none>'}] ${deploy._.phase}`))
@@ -266,7 +302,7 @@ const executeOrContinueDeploy = async (deploy: SavebleDocument<TDeploy>, _user:
266302
case 'eoa': {
267303
await executeEOAPhase(deploy, metatxn, options)
268304
break;
269-
}
305+
}
270306
case 'script': {
271307
await executeScriptPhase(deploy, metatxn, options);
272308
break;
@@ -317,5 +353,5 @@ export default command({
317353
nonInteractive: allArgs.nonInteractive,
318354
fork: allArgs.fork
319355
},
320-
handler: requires(handler, loggedIn, inRepo),
356+
handler: requires(handler, inRepo),
321357
})

0 commit comments

Comments
 (0)