Skip to content

Commit

Permalink
feat(cli): warn when contract is over or close to the size limit (#1894)
Browse files Browse the repository at this point in the history
  • Loading branch information
holic committed Nov 10, 2023
1 parent 1feecf4 commit bdb46fe
Show file tree
Hide file tree
Showing 11 changed files with 71 additions and 28 deletions.
5 changes: 5 additions & 0 deletions .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.
4 changes: 4 additions & 0 deletions packages/cli/src/deploy/common.ts
Expand Up @@ -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);
Expand Down Expand Up @@ -46,6 +49,7 @@ export type WorldFunction = {
export type DeterministicContract = {
readonly address: Address;
readonly bytecode: Hex;
readonly deployedBytecodeSize: number;
readonly abi: Abi;
};

Expand Down
8 changes: 4 additions & 4 deletions packages/cli/src/deploy/deploy.ts
Expand Up @@ -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<configInput extends ConfigInput> = {
client: Client<Transport, Chain | undefined, Account>;
Expand Down Expand Up @@ -43,14 +42,15 @@ export async function deploy<configInput extends ConfigInput>({
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`,
})),
],
Expand Down
16 changes: 14 additions & 2 deletions packages/cli/src/deploy/ensureContract.ts
@@ -1,20 +1,22 @@
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";
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<Transport, Chain | undefined, Account>;
Expand All @@ -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(
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/deploy/ensureModules.ts
Expand Up @@ -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`,
})),
});
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/deploy/ensureSystems.ts
Expand Up @@ -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`,
})),
});
Expand Down
23 changes: 18 additions & 5 deletions packages/cli/src/deploy/ensureWorldFactory.ts
@@ -1,17 +1,20 @@
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: [],
});

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)"]),
Expand All @@ -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<Transport, Chain | undefined, Account>
): Promise<readonly Hex[]> {
// 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,
});
}
2 changes: 1 addition & 1 deletion packages/cli/src/deploy/getSystems.ts
Expand Up @@ -14,7 +14,7 @@ export async function getSystems({
}: {
readonly client: Client;
readonly worldDeploy: WorldDeploy;
}): Promise<readonly Omit<System, "abi" | "bytecode">[]> {
}): Promise<readonly Omit<System, "abi" | "bytecode" | "deployedBytecodeSize">[]> {
const [resourceIds, functions, resourceAccess] = await Promise.all([
getResourceIds({ client, worldDeploy }),
getFunctions({ client, worldDeploy }),
Expand Down
11 changes: 4 additions & 7 deletions packages/cli/src/deploy/resolveConfig.ts
Expand Up @@ -75,6 +75,7 @@ export function resolveConfig<config extends ConfigInput>({
),
address: getCreate2Address({ from: deployer, bytecode: contractData.bytecode, salt }),
bytecode: contractData.bytecode,
deployedBytecodeSize: contractData.deployedBytecodeSize,
abi: contractData.abi,
functions: systemFunctions,
};
Expand Down Expand Up @@ -118,15 +119,10 @@ export function resolveConfig<config extends ConfigInput>({
),
};

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) => {
Expand All @@ -142,6 +138,7 @@ export function resolveConfig<config extends ConfigInput>({
installData: installArgs.length === 0 ? "0x" : installArgs[0],
address: getCreate2Address({ from: deployer, bytecode: contractData.bytecode, salt }),
bytecode: contractData.bytecode,
deployedBytecodeSize: contractData.deployedBytecodeSize,
abi: contractData.abi,
};
});
Expand Down
16 changes: 10 additions & 6 deletions 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),
},
];
12 changes: 9 additions & 3 deletions 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 {
Expand All @@ -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) };
}

0 comments on commit bdb46fe

Please sign in to comment.