Skip to content

Commit

Permalink
feat(cli): verify command (#2662)
Browse files Browse the repository at this point in the history
  • Loading branch information
yonadaa authored Apr 25, 2024
1 parent 375d902 commit fce741b
Show file tree
Hide file tree
Showing 8 changed files with 314 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/many-bulldogs-pay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@latticexyz/cli": patch
---

Added a new `mud verify` command which verifies all contracts in a project. This includes systems, modules, the WorldFactory and World.
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"glob": "^8.0.3",
"nice-grpc-web": "^2.0.1",
"openurl": "^1.1.1",
"p-queue": "^7.4.1",
"p-retry": "^5.1.2",
"path": "^0.12.7",
"rxjs": "7.5.5",
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import setVersion from "./set-version";
import test from "./test";
import trace from "./trace";
import devContracts from "./dev-contracts";
import verify from "./verify";

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Each command has different options
export const commands: CommandModule<any, any>[] = [
Expand All @@ -30,4 +31,5 @@ export const commands: CommandModule<any, any>[] = [
trace,
devContracts,
abiTs,
verify,
];
111 changes: 111 additions & 0 deletions packages/cli/src/commands/verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import type { CommandModule, InferredOptionTypes } from "yargs";
import { verify } from "../verify";
import { loadConfig } from "@latticexyz/config/node";
import { World as WorldConfig } from "@latticexyz/world";
import { resolveWorldConfig } from "@latticexyz/world/internal";
import { worldToV1 } from "@latticexyz/world/config/v2";
import { getOutDirectory, getRpcUrl, getSrcDirectory } from "@latticexyz/common/foundry";
import { getExistingContracts } from "../utils/getExistingContracts";
import { getContractData } from "../utils/getContractData";
import { defaultModuleContracts } from "../utils/defaultModuleContracts";
import { Hex, createWalletClient, http } from "viem";
import chalk from "chalk";

const verifyOptions = {
deployerAddress: {
type: "string",
desc: "Deploy using an existing deterministic deployer (https://github.com/Arachnid/deterministic-deployment-proxy)",
},
worldAddress: { type: "string", required: true, desc: "Verify an existing World at the given address" },
configPath: { type: "string", desc: "Path to the MUD config file" },
profile: { type: "string", desc: "The foundry profile to use" },
rpc: { type: "string", desc: "The RPC URL to use. Defaults to the RPC url from the local foundry.toml" },
rpcBatch: {
type: "boolean",
desc: "Enable batch processing of RPC requests in viem client (defaults to batch size of 100 and wait of 1s)",
},
srcDir: { type: "string", desc: "Source directory. Defaults to foundry src directory." },
verifier: { type: "string", desc: "The verifier to use" },
verifierUrl: {
type: "string",
desc: "The verification provider.",
},
} as const;

type Options = InferredOptionTypes<typeof verifyOptions>;

const commandModule: CommandModule<Options, Options> = {
command: "verify",

describe: "Verify contracts",

builder(yargs) {
return yargs.options(verifyOptions);
},

async handler(opts) {
const profile = opts.profile ?? process.env.FOUNDRY_PROFILE;

const configV2 = (await loadConfig(opts.configPath)) as WorldConfig;
const config = worldToV1(configV2);

const srcDir = opts.srcDir ?? (await getSrcDirectory(profile));
const outDir = await getOutDirectory(profile);

const rpc = opts.rpc ?? (await getRpcUrl(profile));
console.log(
chalk.bgBlue(
chalk.whiteBright(`\n Verifying MUD contracts${profile ? " with profile " + profile : ""} to RPC ${rpc} \n`),
),
);

const client = createWalletClient({
transport: http(rpc, {
batch: opts.rpcBatch
? {
batchSize: 100,
wait: 1000,
}
: undefined,
}),
});

const contractNames = getExistingContracts(srcDir).map(({ basename }) => basename);
const resolvedWorldConfig = resolveWorldConfig(config, contractNames);

const systems = Object.keys(resolvedWorldConfig.systems).map((name) => {
const contractData = getContractData(`${name}.sol`, name, outDir);

return {
name,
bytecode: contractData.bytecode,
};
});

// Get modules
const modules = config.modules.map((mod) => {
const contractData =
defaultModuleContracts.find((defaultMod) => defaultMod.name === mod.name) ??
getContractData(`${mod.name}.sol`, mod.name, outDir);

return {
name: mod.name,
bytecode: contractData.bytecode,
};
});

await verify({
client,
rpc,
foundryProfile: profile,
systems,
modules,
deployerAddress: opts.deployerAddress as Hex | undefined,
worldAddress: opts.worldAddress as Hex,
verifier: opts.verifier,
verifierUrl: opts.verifierUrl,
});
},
};

export default commandModule;
166 changes: 166 additions & 0 deletions packages/cli/src/verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { Chain, Client, Hex, Transport, getCreate2Address, sliceHex, zeroHash } from "viem";
import { getWorldFactoryContracts } from "./deploy/getWorldFactoryContracts";
import { verifyContract } from "./verify/verifyContract";
import PQueue from "p-queue";
import { getWorldProxyFactoryContracts } from "./deploy/getWorldProxyFactoryContracts";
import { getDeployer } from "./deploy/getDeployer";
import { MUDError } from "@latticexyz/common/errors";
import { salt } from "./deploy/common";
import { getStorageAt } from "viem/actions";

type VerifyOptions = {
client: Client<Transport, Chain | undefined>;
rpc: string;
foundryProfile?: string;
verifier?: string;
verifierUrl?: string;
systems: { name: string; bytecode: Hex }[];
modules: { name: string; bytecode: Hex }[];
worldAddress: Hex;
/**
* Address of determinstic deployment proxy: https://github.com/Arachnid/deterministic-deployment-proxy
* By default, we look for a deployment at 0x4e59b44847b379578588920ca78fbf26c0b4956c.
* If it is not deployed or the target chain does not support legacy transactions, the user must set the deployer manually.
*/
deployerAddress?: Hex;
};

const ERC1967_IMPLEMENTATION_SLOT = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc";

export async function verify({
client,
rpc,
foundryProfile = process.env.FOUNDRY_PROFILE,
systems,
modules,
worldAddress,
deployerAddress: initialDeployerAddress,
verifier,
verifierUrl,
}: VerifyOptions): Promise<void> {
const deployerAddress = initialDeployerAddress ?? (await getDeployer(client));
if (!deployerAddress) {
throw new MUDError(`No deployer`);
}

// If the proxy implementation storage slot is set on the World, the World was deployed as a proxy.
const implementationStorage = await getStorageAt(client, {
address: worldAddress,
slot: ERC1967_IMPLEMENTATION_SLOT,
});
const usesProxy = implementationStorage && implementationStorage !== zeroHash;

const verifyQueue = new PQueue({ concurrency: 1 });

systems.map(({ name, bytecode }) =>
verifyQueue.add(() =>
verifyContract(
{
name,
rpc,
verifier,
verifierUrl,
address: getCreate2Address({
from: deployerAddress,
bytecode: bytecode,
salt,
}),
},
{ profile: foundryProfile },
).catch((error) => {
console.error(`Error verifying system contract ${name}:`, error);
}),
),
);

Object.entries(
usesProxy ? getWorldProxyFactoryContracts(deployerAddress) : getWorldFactoryContracts(deployerAddress),
).map(([name, { bytecode }]) =>
verifyQueue.add(() =>
verifyContract(
{
name,
rpc,
verifier,
verifierUrl,
address: getCreate2Address({
from: deployerAddress,
bytecode: bytecode,
salt,
}),
},
{
profile: foundryProfile,
cwd: "node_modules/@latticexyz/world",
},
).catch((error) => {
console.error(`Error verifying world factory contract ${name}:`, error);
}),
),
);

modules.map(({ name, bytecode }) =>
verifyQueue.add(() =>
verifyContract(
{
name: name,
rpc,
verifier,
verifierUrl,
address: getCreate2Address({
from: deployerAddress,
bytecode: bytecode,
salt,
}),
},
{
profile: foundryProfile,
cwd: "node_modules/@latticexyz/world-modules",
},
).catch((error) => {
console.error(`Error verifying module contract ${name}:`, error);
}),
),
);

// If the world was deployed as a Proxy, verify the proxy and implementation.
if (usesProxy) {
const implementationAddress = sliceHex(implementationStorage, -20);

verifyQueue.add(() =>
verifyContract(
{ name: "WorldProxy", rpc, verifier, verifierUrl, address: worldAddress },
{
profile: foundryProfile,
cwd: "node_modules/@latticexyz/world",
},
).catch((error) => {
console.error(`Error verifying WorldProxy contract:`, error);
}),
);

verifyQueue.add(() =>
verifyContract(
{ name: "World", rpc, verifier, verifierUrl, address: implementationAddress },
{
profile: foundryProfile,
cwd: "node_modules/@latticexyz/world",
},
).catch((error) => {
console.error(`Error verifying World contract:`, error);
}),
);
} else {
verifyQueue.add(() =>
verifyContract(
{ name: "World", rpc, verifier, verifierUrl, address: worldAddress },
{
profile: foundryProfile,
cwd: "node_modules/@latticexyz/world",
},
).catch((error) => {
console.error(`Error verifying World contract:`, error);
}),
);
}
}
24 changes: 24 additions & 0 deletions packages/cli/src/verify/verifyContract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { forge } from "@latticexyz/common/foundry";
import { Address } from "viem";

type VerifyContractOptions = {
name: string;
rpc: string;
verifier?: string;
verifierUrl?: string;
address: Address;
};

type ForgeOptions = { profile?: string; silent?: boolean; env?: NodeJS.ProcessEnv; cwd?: string };

export async function verifyContract(options: VerifyContractOptions, forgeOptions?: ForgeOptions) {
const args = ["verify-contract", options.address, options.name, "--rpc-url", options.rpc];

if (options.verifier) {
args.push("--verifier", options.verifier);
}
if (options.verifierUrl) {
args.push("--verifier-url", options.verifierUrl);
}
await forge(args, forgeOptions);
}
3 changes: 2 additions & 1 deletion packages/common/src/foundry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,13 @@ export async function getRemappings(profile?: string): Promise<[string, string][
*/
export async function forge(
args: string[],
options?: { profile?: string; silent?: boolean; env?: NodeJS.ProcessEnv },
options?: { profile?: string; silent?: boolean; env?: NodeJS.ProcessEnv; cwd?: string },
): Promise<void> {
const execOptions: Options<string> = {
env: { FOUNDRY_PROFILE: options?.profile, ...options?.env },
stdout: "inherit",
stderr: "pipe",
cwd: options?.cwd,
};

await (options?.silent ? execa("forge", args, execOptions) : execLog("forge", args, execOptions));
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

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

0 comments on commit fce741b

Please sign in to comment.