-
Notifications
You must be signed in to change notification settings - Fork 196
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
314 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}), | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.