diff --git a/.changeset/brave-islands-wash.md b/.changeset/brave-islands-wash.md new file mode 100644 index 0000000000..b5ed025eb9 --- /dev/null +++ b/.changeset/brave-islands-wash.md @@ -0,0 +1,5 @@ +--- +"@latticexyz/cli": minor +--- + +Deploys now validate contract size before deploying and warns when a contract is over or close to the size limit (24kb). This should help identify the most common cause of "evm revert" errors during system and module contract deploys. diff --git a/packages/cli/src/deploy/common.ts b/packages/cli/src/deploy/common.ts index 59f352fd7e..26a81638dd 100644 --- a/packages/cli/src/deploy/common.ts +++ b/packages/cli/src/deploy/common.ts @@ -9,6 +9,9 @@ import { WorldConfig, helloWorldEvent } from "@latticexyz/world"; export const salt = padHex("0x", { size: 32 }); +// https://eips.ethereum.org/EIPS/eip-170 +export const contractSizeLimit = parseInt("6000", 16); + // TODO: add `as const` to mud config so these get more strongly typed (blocked by current config parsing not using readonly) export const storeTables = configToTables(storeConfig); export const worldTables = configToTables(worldConfig); @@ -46,6 +49,7 @@ export type WorldFunction = { export type DeterministicContract = { readonly address: Address; readonly bytecode: Hex; + readonly deployedBytecodeSize: number; readonly abi: Abi; }; diff --git a/packages/cli/src/deploy/deploy.ts b/packages/cli/src/deploy/deploy.ts index 1be0ed3580..b75c189ed7 100644 --- a/packages/cli/src/deploy/deploy.ts +++ b/packages/cli/src/deploy/deploy.ts @@ -12,10 +12,9 @@ import { Table } from "./configToTables"; import { assertNamespaceOwner } from "./assertNamespaceOwner"; import { debug } from "./debug"; import { resourceLabel } from "./resourceLabel"; -import { ensureContract } from "./ensureContract"; import { uniqueBy } from "@latticexyz/common/utils"; import { ensureContractsDeployed } from "./ensureContractsDeployed"; -import { coreModuleBytecode, worldFactoryBytecode } from "./ensureWorldFactory"; +import { coreModuleBytecode, worldFactoryBytecode, worldFactoryContracts } from "./ensureWorldFactory"; type DeployOptions = { client: Client; @@ -43,14 +42,15 @@ export async function deploy({ await ensureContractsDeployed({ client, contracts: [ - { bytecode: coreModuleBytecode, label: "core module" }, - { bytecode: worldFactoryBytecode, label: "world factory" }, + ...worldFactoryContracts, ...uniqueBy(systems, (system) => getAddress(system.address)).map((system) => ({ bytecode: system.bytecode, + deployedBytecodeSize: system.deployedBytecodeSize, label: `${resourceLabel(system)} system`, })), ...uniqueBy(config.modules, (mod) => getAddress(mod.address)).map((mod) => ({ bytecode: mod.bytecode, + deployedBytecodeSize: mod.deployedBytecodeSize, label: `${mod.name} module`, })), ], diff --git a/packages/cli/src/deploy/ensureContract.ts b/packages/cli/src/deploy/ensureContract.ts index 9d5786a1c5..92553952f9 100644 --- a/packages/cli/src/deploy/ensureContract.ts +++ b/packages/cli/src/deploy/ensureContract.ts @@ -1,7 +1,7 @@ -import { Client, Transport, Chain, Account, concatHex, getCreate2Address, Hex } from "viem"; +import { Client, Transport, Chain, Account, concatHex, getCreate2Address, Hex, size } from "viem"; import { getBytecode } from "viem/actions"; import { deployer } from "./ensureDeployer"; -import { salt } from "./common"; +import { contractSizeLimit, salt } from "./common"; import { sendTransaction } from "@latticexyz/common"; import { debug } from "./debug"; import pRetry from "p-retry"; @@ -9,12 +9,14 @@ import { wait } from "@latticexyz/common/utils"; export type Contract = { bytecode: Hex; + deployedBytecodeSize: number; label?: string; }; export async function ensureContract({ client, bytecode, + deployedBytecodeSize, label = "contract", }: { readonly client: Client; @@ -27,6 +29,16 @@ export async function ensureContract({ return []; } + if (deployedBytecodeSize > contractSizeLimit) { + console.warn( + `\nBytecode for ${label} (${deployedBytecodeSize} bytes) is over the contract size limit (${contractSizeLimit} bytes). Run \`forge build --sizes\` for more info.\n` + ); + } else if (deployedBytecodeSize > contractSizeLimit * 0.95) { + console.warn( + `\nBytecode for ${label} (${deployedBytecodeSize} bytes) is almost over the contract size limit (${contractSizeLimit} bytes). Run \`forge build --sizes\` for more info.\n` + ); + } + debug("deploying", label, "at", address); return [ await pRetry( diff --git a/packages/cli/src/deploy/ensureModules.ts b/packages/cli/src/deploy/ensureModules.ts index 2b5b68e626..276485dfc5 100644 --- a/packages/cli/src/deploy/ensureModules.ts +++ b/packages/cli/src/deploy/ensureModules.ts @@ -21,6 +21,7 @@ export async function ensureModules({ client, contracts: uniqueBy(modules, (mod) => getAddress(mod.address)).map((mod) => ({ bytecode: mod.bytecode, + deployedBytecodeSize: mod.deployedBytecodeSize, label: `${mod.name} module`, })), }); diff --git a/packages/cli/src/deploy/ensureSystems.ts b/packages/cli/src/deploy/ensureSystems.ts index 03c4c59253..4ebaf24e3b 100644 --- a/packages/cli/src/deploy/ensureSystems.ts +++ b/packages/cli/src/deploy/ensureSystems.ts @@ -131,6 +131,7 @@ export async function ensureSystems({ client, contracts: uniqueBy(missingSystems, (system) => getAddress(system.address)).map((system) => ({ bytecode: system.bytecode, + deployedBytecodeSize: system.deployedBytecodeSize, label: `${resourceLabel(system)} system`, })), }); diff --git a/packages/cli/src/deploy/ensureWorldFactory.ts b/packages/cli/src/deploy/ensureWorldFactory.ts index cd70777482..2f321260d2 100644 --- a/packages/cli/src/deploy/ensureWorldFactory.ts +++ b/packages/cli/src/deploy/ensureWorldFactory.ts @@ -1,10 +1,12 @@ import coreModuleBuild from "@latticexyz/world/out/CoreModule.sol/CoreModule.json" assert { type: "json" }; import worldFactoryBuild from "@latticexyz/world/out/WorldFactory.sol/WorldFactory.json" assert { type: "json" }; -import { Client, Transport, Chain, Account, Hex, parseAbi, getCreate2Address, encodeDeployData } from "viem"; +import { Client, Transport, Chain, Account, Hex, parseAbi, getCreate2Address, encodeDeployData, size } from "viem"; import { deployer } from "./ensureDeployer"; import { salt } from "./common"; import { ensureContractsDeployed } from "./ensureContractsDeployed"; +import { Contract } from "./ensureContract"; +export const coreModuleDeployedBytecodeSize = size(coreModuleBuild.deployedBytecode.object as Hex); export const coreModuleBytecode = encodeDeployData({ bytecode: coreModuleBuild.bytecode.object as Hex, abi: [], @@ -12,6 +14,7 @@ export const coreModuleBytecode = encodeDeployData({ export const coreModule = getCreate2Address({ from: deployer, bytecode: coreModuleBytecode, salt }); +export const worldFactoryDeployedBytecodeSize = size(worldFactoryBuild.deployedBytecode.object as Hex); export const worldFactoryBytecode = encodeDeployData({ bytecode: worldFactoryBuild.bytecode.object as Hex, abi: parseAbi(["constructor(address)"]), @@ -20,15 +23,25 @@ export const worldFactoryBytecode = encodeDeployData({ export const worldFactory = getCreate2Address({ from: deployer, bytecode: worldFactoryBytecode, salt }); +export const worldFactoryContracts: readonly Contract[] = [ + { + bytecode: coreModuleBytecode, + deployedBytecodeSize: coreModuleDeployedBytecodeSize, + label: "core module", + }, + { + bytecode: worldFactoryBytecode, + deployedBytecodeSize: worldFactoryDeployedBytecodeSize, + label: "world factory", + }, +]; + export async function ensureWorldFactory( client: Client ): Promise { // WorldFactory constructor doesn't call CoreModule, only sets its address, so we can do these in parallel since the address is deterministic return await ensureContractsDeployed({ client, - contracts: [ - { bytecode: coreModuleBytecode, label: "core module" }, - { bytecode: worldFactoryBytecode, label: "world factory" }, - ], + contracts: worldFactoryContracts, }); } diff --git a/packages/cli/src/deploy/getSystems.ts b/packages/cli/src/deploy/getSystems.ts index 1641aff8eb..f3619fed63 100644 --- a/packages/cli/src/deploy/getSystems.ts +++ b/packages/cli/src/deploy/getSystems.ts @@ -14,7 +14,7 @@ export async function getSystems({ }: { readonly client: Client; readonly worldDeploy: WorldDeploy; -}): Promise[]> { +}): Promise[]> { const [resourceIds, functions, resourceAccess] = await Promise.all([ getResourceIds({ client, worldDeploy }), getFunctions({ client, worldDeploy }), diff --git a/packages/cli/src/deploy/resolveConfig.ts b/packages/cli/src/deploy/resolveConfig.ts index 1267f7d66e..c074b273a2 100644 --- a/packages/cli/src/deploy/resolveConfig.ts +++ b/packages/cli/src/deploy/resolveConfig.ts @@ -75,6 +75,7 @@ export function resolveConfig({ ), address: getCreate2Address({ from: deployer, bytecode: contractData.bytecode, salt }), bytecode: contractData.bytecode, + deployedBytecodeSize: contractData.deployedBytecodeSize, abi: contractData.abi, functions: systemFunctions, }; @@ -118,15 +119,10 @@ export function resolveConfig({ ), }; - const defaultModules = defaultModuleContracts.map((mod) => ({ - name: mod.name, - bytecode: (typeof mod.bytecode === "string" ? mod.bytecode : mod.bytecode.object) as Hex, - abi: mod.abi as Abi, - })); - const modules = config.modules.map((mod) => { const contractData = - defaultModules.find((defaultMod) => defaultMod.name === mod.name) ?? getContractData(mod.name, forgeOutDir); + defaultModuleContracts.find((defaultMod) => defaultMod.name === mod.name) ?? + getContractData(mod.name, forgeOutDir); const installArgs = mod.args .map((arg) => resolveWithContext(arg, resolveContext)) .map((arg) => { @@ -142,6 +138,7 @@ export function resolveConfig({ installData: installArgs.length === 0 ? "0x" : installArgs[0], address: getCreate2Address({ from: deployer, bytecode: contractData.bytecode, salt }), bytecode: contractData.bytecode, + deployedBytecodeSize: contractData.deployedBytecodeSize, abi: contractData.abi, }; }); diff --git a/packages/cli/src/utils/modules/constants.ts b/packages/cli/src/utils/modules/constants.ts index 4888967880..239860ba6f 100644 --- a/packages/cli/src/utils/modules/constants.ts +++ b/packages/cli/src/utils/modules/constants.ts @@ -1,22 +1,26 @@ import KeysWithValueModuleData from "@latticexyz/world-modules/out/KeysWithValueModule.sol/KeysWithValueModule.json" assert { type: "json" }; import KeysInTableModuleData from "@latticexyz/world-modules/out/KeysInTableModule.sol/KeysInTableModule.json" assert { type: "json" }; import UniqueEntityModuleData from "@latticexyz/world-modules/out/UniqueEntityModule.sol/UniqueEntityModule.json" assert { type: "json" }; +import { Abi, Hex, size } from "viem"; // These modules are always deployed export const defaultModuleContracts = [ { name: "KeysWithValueModule", - abi: KeysWithValueModuleData.abi, - bytecode: KeysWithValueModuleData.bytecode, + abi: KeysWithValueModuleData.abi as Abi, + bytecode: KeysWithValueModuleData.bytecode.object as Hex, + deployedBytecodeSize: size(KeysWithValueModuleData.deployedBytecode.object as Hex), }, { name: "KeysInTableModule", - abi: KeysInTableModuleData.abi, - bytecode: KeysInTableModuleData.bytecode, + abi: KeysInTableModuleData.abi as Abi, + bytecode: KeysInTableModuleData.bytecode.object as Hex, + deployedBytecodeSize: size(KeysInTableModuleData.deployedBytecode.object as Hex), }, { name: "UniqueEntityModule", - abi: UniqueEntityModuleData.abi, - bytecode: UniqueEntityModuleData.bytecode, + abi: UniqueEntityModuleData.abi as Abi, + bytecode: UniqueEntityModuleData.bytecode.object as Hex, + deployedBytecodeSize: size(UniqueEntityModuleData.deployedBytecode.object as Hex), }, ]; diff --git a/packages/cli/src/utils/utils/getContractData.ts b/packages/cli/src/utils/utils/getContractData.ts index 16695b5735..a289b0443d 100644 --- a/packages/cli/src/utils/utils/getContractData.ts +++ b/packages/cli/src/utils/utils/getContractData.ts @@ -1,13 +1,16 @@ import { readFileSync } from "fs"; import path from "path"; import { MUDError } from "@latticexyz/common/errors"; -import { Abi, Hex } from "viem"; +import { Abi, Hex, size } from "viem"; /** * Load the contract's abi and bytecode from the file system * @param contractName: Name of the contract to load */ -export function getContractData(contractName: string, forgeOutDirectory: string): { bytecode: Hex; abi: Abi } { +export function getContractData( + contractName: string, + forgeOutDirectory: string +): { bytecode: Hex; abi: Abi; deployedBytecodeSize: number } { let data: any; const contractDataPath = path.join(forgeOutDirectory, contractName + ".sol", contractName + ".json"); try { @@ -19,8 +22,11 @@ export function getContractData(contractName: string, forgeOutDirectory: string) const bytecode = data?.bytecode?.object; if (!bytecode) throw new MUDError(`No bytecode found in ${contractDataPath}`); + const deployedBytecode = data?.deployedBytecode?.object; + if (!deployedBytecode) throw new MUDError(`No deployed bytecode found in ${contractDataPath}`); + const abi = data?.abi; if (!abi) throw new MUDError(`No ABI found in ${contractDataPath}`); - return { abi, bytecode }; + return { abi, bytecode, deployedBytecodeSize: size(deployedBytecode as Hex) }; }