diff --git a/.changeset/tough-dolphins-bathe.md b/.changeset/tough-dolphins-bathe.md new file mode 100644 index 0000000000..f212e587cc --- /dev/null +++ b/.changeset/tough-dolphins-bathe.md @@ -0,0 +1,7 @@ +--- +"@latticexyz/cli": patch +--- + +Added a non-deterministic fallback for deploying to chains that have replay protection on and do not support pre-EIP-155 transactions (no chain ID). + +If you're using `mud deploy` and there's already a [deterministic deployer](https://github.com/Arachnid/deterministic-deployment-proxy) on your target chain, you can provide the address with `--deployerAddress 0x...` to still get some determinism. diff --git a/packages/cli/src/commands/dev-contracts.ts b/packages/cli/src/commands/dev-contracts.ts index 9cd69fb3cd..084c271275 100644 --- a/packages/cli/src/commands/dev-contracts.ts +++ b/packages/cli/src/commands/dev-contracts.ts @@ -87,6 +87,7 @@ const commandModule: CommandModule Address; readonly bytecode: Hex; readonly deployedBytecodeSize: number; readonly abi: Abi; @@ -59,9 +60,17 @@ export type System = DeterministicContract & { readonly systemId: Hex; readonly allowAll: boolean; readonly allowedAddresses: readonly Hex[]; + readonly allowedSystemIds: readonly Hex[]; readonly functions: readonly WorldFunction[]; }; +export type DeployedSystem = Omit< + System, + "getAddress" | "abi" | "bytecode" | "deployedBytecodeSize" | "allowedSystemIds" +> & { + address: Address; +}; + export type Module = DeterministicContract & { readonly name: string; readonly installAsRoot: boolean; diff --git a/packages/cli/src/deploy/create2/README.md b/packages/cli/src/deploy/create2/README.md index 19d14f64ae..beb5c9f8bf 100644 --- a/packages/cli/src/deploy/create2/README.md +++ b/packages/cli/src/deploy/create2/README.md @@ -6,4 +6,8 @@ cd deterministic-deployment-proxy git checkout b3bb19c npm install npm run build +cd output +jq --arg bc "$(cat bytecode.txt)" '. + {bytecode: $bc}' deployment.json > deployment-with-bytecode.json +mv deployment-with-bytecode.json deployment.json +cp deployment.json ../path/to/this/dir ``` diff --git a/packages/cli/src/deploy/create2/deployment.json b/packages/cli/src/deploy/create2/deployment.json index 20e5d9532a..21d97ff780 100644 --- a/packages/cli/src/deploy/create2/deployment.json +++ b/packages/cli/src/deploy/create2/deployment.json @@ -3,5 +3,6 @@ "gasLimit": 100000, "signerAddress": "3fab184622dc19b6109349b94811493bf2a45362", "transaction": "f8a58085174876e800830186a08080b853604580600e600039806000f350fe7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf31ba02222222222222222222222222222222222222222222222222222222222222222a02222222222222222222222222222222222222222222222222222222222222222", - "address": "4e59b44847b379578588920ca78fbf26c0b4956c" + "address": "4e59b44847b379578588920ca78fbf26c0b4956c", + "bytecode": "604580600e600039806000f350fe7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3" } diff --git a/packages/cli/src/deploy/deploy.ts b/packages/cli/src/deploy/deploy.ts index 3dc6f8e210..febcca2963 100644 --- a/packages/cli/src/deploy/deploy.ts +++ b/packages/cli/src/deploy/deploy.ts @@ -14,14 +14,21 @@ import { debug } from "./debug"; import { resourceToLabel } from "@latticexyz/common"; import { uniqueBy } from "@latticexyz/common/utils"; import { ensureContractsDeployed } from "./ensureContractsDeployed"; -import { worldFactoryContracts } from "./ensureWorldFactory"; import { randomBytes } from "crypto"; +import { ensureWorldFactory } from "./ensureWorldFactory"; type DeployOptions = { client: Client; config: Config; salt?: Hex; worldAddress?: Address; + /** + * Address of determinstic deployment proxy: https://github.com/Arachnid/deterministic-deployment-proxy + * By default, we look for a deployment at 0x4e59b44847b379578588920ca78fbf26c0b4956c and, if not, deploy one. + * If the target chain does not support legacy transactions, we deploy the proxy bytecode anyway, but it will + * not have a deterministic address. + */ + deployerAddress?: Hex; }; /** @@ -35,23 +42,25 @@ export async function deploy({ config, salt, worldAddress: existingWorldAddress, + deployerAddress: initialDeployerAddress, }: DeployOptions): Promise { const tables = Object.values(config.tables) as Table[]; - const systems = Object.values(config.systems); - await ensureDeployer(client); + const deployerAddress = initialDeployerAddress ?? (await ensureDeployer(client)); + + await ensureWorldFactory(client, deployerAddress); // deploy all dependent contracts, because system registration, module install, etc. all expect these contracts to be callable. await ensureContractsDeployed({ client, + deployerAddress, contracts: [ - ...worldFactoryContracts, - ...uniqueBy(systems, (system) => getAddress(system.address)).map((system) => ({ + ...uniqueBy(config.systems, (system) => getAddress(system.getAddress(deployerAddress))).map((system) => ({ bytecode: system.bytecode, deployedBytecodeSize: system.deployedBytecodeSize, label: `${resourceToLabel(system)} system`, })), - ...uniqueBy(config.modules, (mod) => getAddress(mod.address)).map((mod) => ({ + ...uniqueBy(config.modules, (mod) => getAddress(mod.getAddress(deployerAddress))).map((mod) => ({ bytecode: mod.bytecode, deployedBytecodeSize: mod.deployedBytecodeSize, label: `${mod.name} module`, @@ -61,7 +70,7 @@ export async function deploy({ const worldDeploy = existingWorldAddress ? await getWorldDeploy(client, existingWorldAddress) - : await deployWorld(client, salt ? salt : `0x${randomBytes(32).toString("hex")}`); + : await deployWorld(client, deployerAddress, salt ?? `0x${randomBytes(32).toString("hex")}`); if (!supportedStoreVersions.includes(worldDeploy.storeVersion)) { throw new Error(`Unsupported Store version: ${worldDeploy.storeVersion}`); @@ -73,7 +82,7 @@ export async function deploy({ const namespaceTxs = await ensureNamespaceOwner({ client, worldDeploy, - resourceIds: [...tables.map((table) => table.tableId), ...systems.map((system) => system.systemId)], + resourceIds: [...tables.map((table) => table.tableId), ...config.systems.map((system) => system.systemId)], }); debug("waiting for all namespace registration transactions to confirm"); @@ -88,16 +97,18 @@ export async function deploy({ }); const systemTxs = await ensureSystems({ client, + deployerAddress, worldDeploy, - systems, + systems: config.systems, }); const functionTxs = await ensureFunctions({ client, worldDeploy, - functions: systems.flatMap((system) => system.functions), + functions: config.systems.flatMap((system) => system.functions), }); const moduleTxs = await ensureModules({ client, + deployerAddress, worldDeploy, modules: config.modules, }); diff --git a/packages/cli/src/deploy/deployWorld.ts b/packages/cli/src/deploy/deployWorld.ts index c038327c84..16c4c7bc75 100644 --- a/packages/cli/src/deploy/deployWorld.ts +++ b/packages/cli/src/deploy/deployWorld.ts @@ -1,6 +1,6 @@ import { Account, Chain, Client, Hex, Log, Transport } from "viem"; import { waitForTransactionReceipt } from "viem/actions"; -import { ensureWorldFactory, worldFactory } from "./ensureWorldFactory"; +import { ensureWorldFactory } from "./ensureWorldFactory"; import WorldFactoryAbi from "@latticexyz/world/out/WorldFactory.sol/WorldFactory.abi.json" assert { type: "json" }; import { writeContract } from "@latticexyz/common"; import { debug } from "./debug"; @@ -9,9 +9,10 @@ import { WorldDeploy } from "./common"; export async function deployWorld( client: Client, + deployerAddress: Hex, salt: Hex ): Promise { - await ensureWorldFactory(client); + const worldFactory = await ensureWorldFactory(client, deployerAddress); debug("deploying world"); const tx = await writeContract(client, { diff --git a/packages/cli/src/deploy/ensureContract.ts b/packages/cli/src/deploy/ensureContract.ts index 92553952f9..b7110bcb1d 100644 --- a/packages/cli/src/deploy/ensureContract.ts +++ b/packages/cli/src/deploy/ensureContract.ts @@ -1,6 +1,5 @@ -import { Client, Transport, Chain, Account, concatHex, getCreate2Address, Hex, size } from "viem"; +import { Client, Transport, Chain, Account, concatHex, getCreate2Address, Hex } from "viem"; import { getBytecode } from "viem/actions"; -import { deployer } from "./ensureDeployer"; import { contractSizeLimit, salt } from "./common"; import { sendTransaction } from "@latticexyz/common"; import { debug } from "./debug"; @@ -15,13 +14,15 @@ export type Contract = { export async function ensureContract({ client, + deployerAddress, bytecode, deployedBytecodeSize, label = "contract", }: { readonly client: Client; + readonly deployerAddress: Hex; } & Contract): Promise { - const address = getCreate2Address({ from: deployer, salt, bytecode }); + const address = getCreate2Address({ from: deployerAddress, salt, bytecode }); const contractCode = await getBytecode(client, { address, blockTag: "pending" }); if (contractCode) { @@ -45,7 +46,7 @@ export async function ensureContract({ () => sendTransaction(client, { chain: client.chain ?? null, - to: deployer, + to: deployerAddress, data: concatHex([salt, bytecode]), }), { diff --git a/packages/cli/src/deploy/ensureContractsDeployed.ts b/packages/cli/src/deploy/ensureContractsDeployed.ts index 14c3fcfccc..ca9d304850 100644 --- a/packages/cli/src/deploy/ensureContractsDeployed.ts +++ b/packages/cli/src/deploy/ensureContractsDeployed.ts @@ -5,12 +5,16 @@ import { Contract, ensureContract } from "./ensureContract"; export async function ensureContractsDeployed({ client, + deployerAddress, contracts, }: { readonly client: Client; + readonly deployerAddress: Hex; readonly contracts: readonly Contract[]; }): Promise { - const txs = (await Promise.all(contracts.map((contract) => ensureContract({ client, ...contract })))).flat(); + const txs = ( + await Promise.all(contracts.map((contract) => ensureContract({ client, deployerAddress, ...contract }))) + ).flat(); if (txs.length) { debug("waiting for contracts"); diff --git a/packages/cli/src/deploy/ensureDeployer.ts b/packages/cli/src/deploy/ensureDeployer.ts index c51d438970..709f27d6f3 100644 --- a/packages/cli/src/deploy/ensureDeployer.ts +++ b/packages/cli/src/deploy/ensureDeployer.ts @@ -1,36 +1,75 @@ -import { Account, Chain, Client, Transport } from "viem"; -import { getBytecode, sendRawTransaction, sendTransaction, waitForTransactionReceipt } from "viem/actions"; +import { Account, Address, Chain, Client, Transport } from "viem"; +import { getBalance, getBytecode, sendRawTransaction, sendTransaction, waitForTransactionReceipt } from "viem/actions"; import deployment from "./create2/deployment.json"; import { debug } from "./debug"; -export const deployer = `0x${deployment.address}` as const; +const deployer = `0x${deployment.address}` as const; +const deployerBytecode = `0x${deployment.bytecode}` as const; -export async function ensureDeployer(client: Client): Promise { +export async function ensureDeployer(client: Client): Promise
{ const bytecode = await getBytecode(client, { address: deployer }); if (bytecode) { - debug("found create2 deployer at", deployer); - return; + debug("found CREATE2 deployer at", deployer); + if (bytecode !== deployerBytecode) { + console.warn( + `\n ⚠️ Bytecode for deployer at ${deployer} did not match the expected CREATE2 bytecode. You may have unexpected results.\n` + ); + } + return deployer; } - // send gas to signer - debug("sending gas for create2 deployer to signer at", deployment.signerAddress); - const gasTx = await sendTransaction(client, { - chain: client.chain ?? null, - to: `0x${deployment.signerAddress}`, - value: BigInt(deployment.gasLimit) * BigInt(deployment.gasPrice), - }); - const gasReceipt = await waitForTransactionReceipt(client, { hash: gasTx }); - if (gasReceipt.status !== "success") { - console.error("failed to send gas to deployer signer", gasReceipt); - throw new Error("failed to send gas to deployer signer"); + // There's not really a way to simulate a pre-EIP-155 (no chain ID) transaction, + // so we have to attempt to create the deployer first and, if it fails, fall back + // to a regular deploy. + + // Send gas to deployment signer + const gasRequired = BigInt(deployment.gasLimit) * BigInt(deployment.gasPrice); + const currentBalance = await getBalance(client, { address: `0x${deployment.signerAddress}` }); + const gasNeeded = gasRequired - currentBalance; + if (gasNeeded > 0) { + debug("sending gas for CREATE2 deployer to signer at", deployment.signerAddress); + const gasTx = await sendTransaction(client, { + chain: client.chain ?? null, + to: `0x${deployment.signerAddress}`, + value: gasNeeded, + }); + const gasReceipt = await waitForTransactionReceipt(client, { hash: gasTx }); + if (gasReceipt.status !== "success") { + console.error("failed to send gas to deployer signer", gasReceipt); + throw new Error("failed to send gas to deployer signer"); + } } - // deploy the deployer - debug("deploying create2 deployer at", deployer); - const deployTx = await sendRawTransaction(client, { serializedTransaction: `0x${deployment.transaction}` }); + // Deploy the deployer + debug("deploying CREATE2 deployer at", deployer); + const deployTx = await sendRawTransaction(client, { serializedTransaction: `0x${deployment.transaction}` }).catch( + (error) => { + // Do a regular contract create if the presigned transaction doesn't work due to replay protection + if (String(error).includes("only replay-protected (EIP-155) transactions allowed over RPC")) { + console.warn( + // eslint-disable-next-line max-len + `\n ⚠️ Your chain or RPC does not allow for non EIP-155 signed transactions, so your deploys will not be determinstic and contract addresses may change between deploys.\n\n We recommend running your chain's node with \`--rpc.allow-unprotected-txs\` to enable determinstic deployments.\n` + ); + debug("deploying CREATE2 deployer"); + return sendTransaction(client, { + chain: client.chain ?? null, + data: deployerBytecode, + }); + } + throw error; + } + ); + const deployReceipt = await waitForTransactionReceipt(client, { hash: deployTx }); + if (!deployReceipt.contractAddress) { + throw new Error("Deploy receipt did not have contract address, was the deployer not deployed?"); + } + if (deployReceipt.contractAddress !== deployer) { - console.error("unexpected contract address for deployer", deployReceipt); - throw new Error("unexpected contract address for deployer"); + console.warn( + `\n ⚠️ CREATE2 deployer created at ${deployReceipt.contractAddress} does not match the CREATE2 determinstic deployer we expected (${deployer})` + ); } + + return deployReceipt.contractAddress; } diff --git a/packages/cli/src/deploy/ensureModules.ts b/packages/cli/src/deploy/ensureModules.ts index 276485dfc5..dddef29a09 100644 --- a/packages/cli/src/deploy/ensureModules.ts +++ b/packages/cli/src/deploy/ensureModules.ts @@ -8,10 +8,12 @@ import { ensureContractsDeployed } from "./ensureContractsDeployed"; export async function ensureModules({ client, + deployerAddress, worldDeploy, modules, }: { readonly client: Client; + readonly deployerAddress: Hex; // TODO: move this into WorldDeploy to reuse a world's deployer? readonly worldDeploy: WorldDeploy; readonly modules: readonly Module[]; }): Promise { @@ -19,7 +21,8 @@ export async function ensureModules({ await ensureContractsDeployed({ client, - contracts: uniqueBy(modules, (mod) => getAddress(mod.address)).map((mod) => ({ + deployerAddress, + contracts: uniqueBy(modules, (mod) => getAddress(mod.getAddress(deployerAddress))).map((mod) => ({ bytecode: mod.bytecode, deployedBytecodeSize: mod.deployedBytecodeSize, label: `${mod.name} module`, @@ -40,7 +43,7 @@ export async function ensureModules({ abi: worldAbi, // TODO: replace with batchCall (https://github.com/latticexyz/mud/issues/1645) functionName: "installRootModule", - args: [mod.address, mod.installData], + args: [mod.getAddress(deployerAddress), mod.installData], }) : await writeContract(client, { chain: client.chain ?? null, @@ -48,7 +51,7 @@ export async function ensureModules({ abi: worldAbi, // TODO: replace with batchCall (https://github.com/latticexyz/mud/issues/1645) functionName: "installModule", - args: [mod.address, mod.installData], + args: [mod.getAddress(deployerAddress), mod.installData], }); } catch (error) { if (error instanceof BaseError && error.message.includes("Module_AlreadyInstalled")) { diff --git a/packages/cli/src/deploy/ensureSystems.ts b/packages/cli/src/deploy/ensureSystems.ts index 3772843a5a..51bac4ae5b 100644 --- a/packages/cli/src/deploy/ensureSystems.ts +++ b/packages/cli/src/deploy/ensureSystems.ts @@ -1,5 +1,5 @@ -import { Client, Transport, Chain, Account, Hex, getAddress } from "viem"; -import { resourceToLabel, writeContract } from "@latticexyz/common"; +import { Client, Transport, Chain, Account, Hex, getAddress, Address } from "viem"; +import { writeContract, resourceToLabel } from "@latticexyz/common"; import { System, WorldDeploy, worldAbi } from "./common"; import { debug } from "./debug"; import { getSystems } from "./getSystems"; @@ -8,12 +8,16 @@ import { uniqueBy, wait } from "@latticexyz/common/utils"; import pRetry from "p-retry"; import { ensureContractsDeployed } from "./ensureContractsDeployed"; +// TODO: move each system registration+access to batch call to be atomic + export async function ensureSystems({ client, + deployerAddress, worldDeploy, systems, }: { readonly client: Client; + readonly deployerAddress: Hex; // TODO: move this into WorldDeploy to reuse a world's deployer? readonly worldDeploy: WorldDeploy; readonly systems: readonly System[]; }): Promise { @@ -21,11 +25,95 @@ export async function ensureSystems({ getSystems({ client, worldDeploy }), getResourceAccess({ client, worldDeploy }), ]); + + // Register or replace systems + + const existingSystems = systems.filter((system) => + worldSystems.some( + (worldSystem) => + worldSystem.systemId === system.systemId && + getAddress(worldSystem.address) === getAddress(system.getAddress(deployerAddress)) + ) + ); + if (existingSystems.length) { + debug("existing systems", existingSystems.map(resourceToLabel).join(", ")); + } + const existingSystemIds = existingSystems.map((system) => system.systemId); + + const missingSystems = systems.filter((system) => !existingSystemIds.includes(system.systemId)); + if (!missingSystems.length) return []; + + const systemsToUpgrade = missingSystems.filter((system) => + worldSystems.some( + (worldSystem) => + worldSystem.systemId === system.systemId && + getAddress(worldSystem.address) !== getAddress(system.getAddress(deployerAddress)) + ) + ); + if (systemsToUpgrade.length) { + debug("upgrading systems", systemsToUpgrade.map(resourceToLabel).join(", ")); + } + + const systemsToAdd = missingSystems.filter( + (system) => !worldSystems.some((worldSystem) => worldSystem.systemId === system.systemId) + ); + if (systemsToAdd.length) { + debug("registering new systems", systemsToAdd.map(resourceToLabel).join(", ")); + } + + await ensureContractsDeployed({ + client, + deployerAddress, + contracts: uniqueBy(missingSystems, (system) => getAddress(system.getAddress(deployerAddress))).map((system) => ({ + bytecode: system.bytecode, + deployedBytecodeSize: system.deployedBytecodeSize, + label: `${resourceToLabel(system)} system`, + })), + }); + + const registerTxs = await Promise.all( + missingSystems.map((system) => + pRetry( + () => + writeContract(client, { + chain: client.chain ?? null, + address: worldDeploy.address, + abi: worldAbi, + // TODO: replace with batchCall (https://github.com/latticexyz/mud/issues/1645) + functionName: "registerSystem", + args: [system.systemId, system.getAddress(deployerAddress), system.allowAll], + }), + { + retries: 3, + onFailedAttempt: async (error) => { + const delay = error.attemptNumber * 500; + debug(`failed to register system ${resourceToLabel(system)}, retrying in ${delay}ms...`); + await wait(delay); + }, + } + ) + ) + ); + + // Adjust system access + const systemIds = systems.map((system) => system.systemId); const currentAccess = worldAccess.filter(({ resourceId }) => systemIds.includes(resourceId)); - const desiredAccess = systems.flatMap((system) => - system.allowedAddresses.map((address) => ({ resourceId: system.systemId, address })) - ); + const desiredAccess = [ + ...systems.flatMap((system) => + system.allowedAddresses.map((address) => ({ resourceId: system.systemId, address })) + ), + ...systems.flatMap((system) => + system.allowedSystemIds + .map((systemId) => ({ + resourceId: system.systemId, + address: + worldSystems.find((s) => s.systemId === systemId)?.address ?? + systems.find((s) => s.systemId === systemId)?.getAddress(deployerAddress), + })) + .filter((access): access is typeof access & { address: Address } => access.address != null) + ), + ]; const accessToAdd = desiredAccess.filter( (access) => @@ -43,8 +131,6 @@ export async function ensureSystems({ ) ); - // TODO: move each system access+registration to batch call to be atomic - if (accessToRemove.length) { debug("revoking", accessToRemove.length, "access grants"); } @@ -52,7 +138,7 @@ export async function ensureSystems({ debug("adding", accessToAdd.length, "access grants"); } - const accessTxs = [ + const accessTxs = await Promise.all([ ...accessToRemove.map((access) => pRetry( () => @@ -93,69 +179,7 @@ export async function ensureSystems({ } ) ), - ]; - - const existingSystems = systems.filter((system) => - worldSystems.some( - (worldSystem) => - worldSystem.systemId === system.systemId && getAddress(worldSystem.address) === getAddress(system.address) - ) - ); - if (existingSystems.length) { - debug("existing systems", existingSystems.map(resourceToLabel).join(", ")); - } - const existingSystemIds = existingSystems.map((system) => system.systemId); - - const missingSystems = systems.filter((system) => !existingSystemIds.includes(system.systemId)); - if (!missingSystems.length) return []; - - const systemsToUpgrade = missingSystems.filter((system) => - worldSystems.some( - (worldSystem) => - worldSystem.systemId === system.systemId && getAddress(worldSystem.address) !== getAddress(system.address) - ) - ); - if (systemsToUpgrade.length) { - debug("upgrading systems", systemsToUpgrade.map(resourceToLabel).join(", ")); - } - - const systemsToAdd = missingSystems.filter( - (system) => !worldSystems.some((worldSystem) => worldSystem.systemId === system.systemId) - ); - if (systemsToAdd.length) { - debug("registering new systems", systemsToAdd.map(resourceToLabel).join(", ")); - } - - await ensureContractsDeployed({ - client, - contracts: uniqueBy(missingSystems, (system) => getAddress(system.address)).map((system) => ({ - bytecode: system.bytecode, - deployedBytecodeSize: system.deployedBytecodeSize, - label: `${resourceToLabel(system)} system`, - })), - }); - - const registerTxs = missingSystems.map((system) => - pRetry( - () => - writeContract(client, { - chain: client.chain ?? null, - address: worldDeploy.address, - abi: worldAbi, - // TODO: replace with batchCall (https://github.com/latticexyz/mud/issues/1645) - functionName: "registerSystem", - args: [system.systemId, system.address, system.allowAll], - }), - { - retries: 3, - onFailedAttempt: async (error) => { - const delay = error.attemptNumber * 500; - debug(`failed to register system ${resourceToLabel(system)}, retrying in ${delay}ms...`); - await wait(delay); - }, - } - ) - ); + ]); - return await Promise.all([...accessTxs, ...registerTxs]); + return [...registerTxs, ...accessTxs]; } diff --git a/packages/cli/src/deploy/ensureWorldFactory.ts b/packages/cli/src/deploy/ensureWorldFactory.ts index 1f2913a801..0df1c21508 100644 --- a/packages/cli/src/deploy/ensureWorldFactory.ts +++ b/packages/cli/src/deploy/ensureWorldFactory.ts @@ -6,113 +6,100 @@ import initModuleBuild from "@latticexyz/world/out/InitModule.sol/InitModule.jso import initModuleAbi from "@latticexyz/world/out/InitModule.sol/InitModule.abi.json" assert { type: "json" }; import worldFactoryBuild from "@latticexyz/world/out/WorldFactory.sol/WorldFactory.json" assert { type: "json" }; import worldFactoryAbi from "@latticexyz/world/out/WorldFactory.sol/WorldFactory.abi.json" assert { type: "json" }; -import { Client, Transport, Chain, Account, Hex, getCreate2Address, encodeDeployData, size, Abi } from "viem"; -import { deployer } from "./ensureDeployer"; +import { Client, Transport, Chain, Account, Hex, getCreate2Address, encodeDeployData, size, Address } from "viem"; import { salt } from "./common"; import { ensureContractsDeployed } from "./ensureContractsDeployed"; import { Contract } from "./ensureContract"; -export const accessManagementSystemDeployedBytecodeSize = size( - accessManagementSystemBuild.deployedBytecode.object as Hex -); -export const accessManagementSystemBytecode = encodeDeployData({ - bytecode: accessManagementSystemBuild.bytecode.object as Hex, - abi: [] as Abi, -}); -export const accessManagementSystem = getCreate2Address({ - from: deployer, - bytecode: accessManagementSystemBytecode, - salt, -}); +export async function ensureWorldFactory( + client: Client, + deployerAddress: Hex +): Promise
{ + const accessManagementSystemDeployedBytecodeSize = size(accessManagementSystemBuild.deployedBytecode.object as Hex); + const accessManagementSystemBytecode = accessManagementSystemBuild.bytecode.object as Hex; + const accessManagementSystem = getCreate2Address({ + from: deployerAddress, + bytecode: accessManagementSystemBytecode, + salt, + }); -export const balanceTransferSystemDeployedBytecodeSize = size( - balanceTransferSystemBuild.deployedBytecode.object as Hex -); -export const balanceTransferSystemBytecode = encodeDeployData({ - bytecode: balanceTransferSystemBuild.bytecode.object as Hex, - abi: [] as Abi, -}); -export const balanceTransferSystem = getCreate2Address({ - from: deployer, - bytecode: balanceTransferSystemBytecode, - salt, -}); + const balanceTransferSystemDeployedBytecodeSize = size(balanceTransferSystemBuild.deployedBytecode.object as Hex); + const balanceTransferSystemBytecode = balanceTransferSystemBuild.bytecode.object as Hex; + const balanceTransferSystem = getCreate2Address({ + from: deployerAddress, + bytecode: balanceTransferSystemBytecode, + salt, + }); -export const batchCallSystemDeployedBytecodeSize = size(batchCallSystemBuild.deployedBytecode.object as Hex); -export const batchCallSystemBytecode = encodeDeployData({ - bytecode: batchCallSystemBuild.bytecode.object as Hex, - abi: [] as Abi, -}); -export const batchCallSystem = getCreate2Address({ from: deployer, bytecode: batchCallSystemBytecode, salt }); + const batchCallSystemDeployedBytecodeSize = size(batchCallSystemBuild.deployedBytecode.object as Hex); + const batchCallSystemBytecode = batchCallSystemBuild.bytecode.object as Hex; + const batchCallSystem = getCreate2Address({ from: deployerAddress, bytecode: batchCallSystemBytecode, salt }); -export const registrationDeployedBytecodeSize = size(registrationSystemBuild.deployedBytecode.object as Hex); -export const registrationBytecode = encodeDeployData({ - bytecode: registrationSystemBuild.bytecode.object as Hex, - abi: [] as Abi, -}); -export const registration = getCreate2Address({ - from: deployer, - bytecode: registrationBytecode, - salt, -}); + const registrationDeployedBytecodeSize = size(registrationSystemBuild.deployedBytecode.object as Hex); + const registrationBytecode = registrationSystemBuild.bytecode.object as Hex; + const registration = getCreate2Address({ + from: deployerAddress, + bytecode: registrationBytecode, + salt, + }); -export const initModuleDeployedBytecodeSize = size(initModuleBuild.deployedBytecode.object as Hex); -export const initModuleBytecode = encodeDeployData({ - bytecode: initModuleBuild.bytecode.object as Hex, - abi: initModuleAbi, - args: [accessManagementSystem, balanceTransferSystem, batchCallSystem, registration], -}); + const initModuleDeployedBytecodeSize = size(initModuleBuild.deployedBytecode.object as Hex); + const initModuleBytecode = encodeDeployData({ + bytecode: initModuleBuild.bytecode.object as Hex, + abi: initModuleAbi, + args: [accessManagementSystem, balanceTransferSystem, batchCallSystem, registration], + }); -export const initModule = getCreate2Address({ from: deployer, bytecode: initModuleBytecode, salt }); + const initModule = getCreate2Address({ from: deployerAddress, bytecode: initModuleBytecode, salt }); -export const worldFactoryDeployedBytecodeSize = size(worldFactoryBuild.deployedBytecode.object as Hex); -export const worldFactoryBytecode = encodeDeployData({ - bytecode: worldFactoryBuild.bytecode.object as Hex, - abi: worldFactoryAbi, - args: [initModule], -}); + const worldFactoryDeployedBytecodeSize = size(worldFactoryBuild.deployedBytecode.object as Hex); + const worldFactoryBytecode = encodeDeployData({ + bytecode: worldFactoryBuild.bytecode.object as Hex, + abi: worldFactoryAbi, + args: [initModule], + }); -export const worldFactory = getCreate2Address({ from: deployer, bytecode: worldFactoryBytecode, salt }); + const worldFactory = getCreate2Address({ from: deployerAddress, bytecode: worldFactoryBytecode, salt }); -export const worldFactoryContracts: readonly Contract[] = [ - { - bytecode: accessManagementSystemBytecode, - deployedBytecodeSize: accessManagementSystemDeployedBytecodeSize, - label: "access management system", - }, - { - bytecode: balanceTransferSystemBytecode, - deployedBytecodeSize: balanceTransferSystemDeployedBytecodeSize, - label: "balance transfer system", - }, - { - bytecode: batchCallSystemBytecode, - deployedBytecodeSize: batchCallSystemDeployedBytecodeSize, - label: "batch call system", - }, - { - bytecode: registrationBytecode, - deployedBytecodeSize: registrationDeployedBytecodeSize, - label: "core registration system", - }, - { - bytecode: initModuleBytecode, - deployedBytecodeSize: initModuleDeployedBytecodeSize, - label: "core module", - }, - { - bytecode: worldFactoryBytecode, - deployedBytecodeSize: worldFactoryDeployedBytecodeSize, - label: "world factory", - }, -]; + const worldFactoryContracts: readonly Contract[] = [ + { + bytecode: accessManagementSystemBytecode, + deployedBytecodeSize: accessManagementSystemDeployedBytecodeSize, + label: "access management system", + }, + { + bytecode: balanceTransferSystemBytecode, + deployedBytecodeSize: balanceTransferSystemDeployedBytecodeSize, + label: "balance transfer system", + }, + { + bytecode: batchCallSystemBytecode, + deployedBytecodeSize: batchCallSystemDeployedBytecodeSize, + label: "batch call system", + }, + { + bytecode: registrationBytecode, + deployedBytecodeSize: registrationDeployedBytecodeSize, + label: "core registration system", + }, + { + bytecode: initModuleBytecode, + deployedBytecodeSize: initModuleDeployedBytecodeSize, + label: "core module", + }, + { + bytecode: worldFactoryBytecode, + deployedBytecodeSize: worldFactoryDeployedBytecodeSize, + label: "world factory", + }, + ]; -export async function ensureWorldFactory( - client: Client -): Promise { // WorldFactory constructor doesn't call InitModule, only sets its address, so we can do these in parallel since the address is deterministic - return await ensureContractsDeployed({ + await ensureContractsDeployed({ client, + deployerAddress, contracts: worldFactoryContracts, }); + + return worldFactory; } diff --git a/packages/cli/src/deploy/getFunctions.ts b/packages/cli/src/deploy/getFunctions.ts index d3ba822e48..39b7955486 100644 --- a/packages/cli/src/deploy/getFunctions.ts +++ b/packages/cli/src/deploy/getFunctions.ts @@ -1,4 +1,4 @@ -import { Client, getFunctionSelector, parseAbiItem } from "viem"; +import { Client, toFunctionSelector, parseAbiItem } from "viem"; import { WorldDeploy, WorldFunction, worldTables } from "./common"; import { debug } from "./debug"; import { storeSetRecordEvent } from "@latticexyz/store"; @@ -34,7 +34,7 @@ export async function getFunctions({ // TODO: parallelize with a bulk getRecords const functions = await Promise.all( signatures.map(async (signature) => { - const selector = getFunctionSelector(signature); + const selector = toFunctionSelector(signature); const { systemId, systemFunctionSelector } = await getTableValue({ client, worldDeploy, diff --git a/packages/cli/src/deploy/getSystems.ts b/packages/cli/src/deploy/getSystems.ts index 8c291bc8c3..c87145efc1 100644 --- a/packages/cli/src/deploy/getSystems.ts +++ b/packages/cli/src/deploy/getSystems.ts @@ -1,5 +1,5 @@ -import { System, WorldDeploy, worldTables } from "./common"; -import { Client } from "viem"; +import { DeployedSystem, System, WorldDeploy, worldTables } from "./common"; +import { Address, Client } from "viem"; import { getResourceIds } from "./getResourceIds"; import { hexToResource, resourceToLabel } from "@latticexyz/common"; import { getTableValue } from "./getTableValue"; @@ -13,7 +13,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 034af8bf41..0b8ea9f260 100644 --- a/packages/cli/src/deploy/resolveConfig.ts +++ b/packages/cli/src/deploy/resolveConfig.ts @@ -1,23 +1,14 @@ import { resolveWorldConfig } from "@latticexyz/world"; import { Config, ConfigInput, WorldFunction, salt } from "./common"; -import { resourceToLabel, resourceToHex, hexToResource } from "@latticexyz/common"; +import { resourceToHex } from "@latticexyz/common"; import { resolveWithContext } from "@latticexyz/config"; import { encodeField } from "@latticexyz/protocol-parser"; import { SchemaAbiType, SchemaAbiTypeToPrimitiveType } from "@latticexyz/schema-type"; -import { - getFunctionSelector, - Hex, - getCreate2Address, - getAddress, - hexToBytes, - bytesToHex, - getFunctionSignature, -} from "viem"; +import { Hex, getCreate2Address, hexToBytes, bytesToHex, Address, toFunctionSelector, toFunctionSignature } from "viem"; import { getExistingContracts } from "../utils/getExistingContracts"; import { defaultModuleContracts } from "../utils/modules/constants"; import { getContractData } from "../utils/utils/getContractData"; import { configToTables } from "./configToTables"; -import { deployer } from "./ensureDeployer"; // TODO: this should be replaced by https://github.com/latticexyz/mud/issues/1668 @@ -38,7 +29,7 @@ export function resolveConfig({ const baseSystemContractData = getContractData("System", forgeOutDir); const baseSystemFunctions = baseSystemContractData.abi .filter((item): item is typeof item & { type: "function" } => item.type === "function") - .map(getFunctionSignature); + .map(toFunctionSignature); const systems = Object.entries(resolvedConfig.systems).map(([systemName, system]) => { const namespace = config.namespace; @@ -48,17 +39,17 @@ export function resolveConfig({ const systemFunctions = contractData.abi .filter((item): item is typeof item & { type: "function" } => item.type === "function") - .map(getFunctionSignature) + .map(toFunctionSignature) .filter((sig) => !baseSystemFunctions.includes(sig)) .map((sig): WorldFunction => { // TODO: figure out how to not duplicate contract behavior (https://github.com/latticexyz/mud/issues/1708) const worldSignature = namespace === "" ? sig : `${namespace}__${sig}`; return { signature: worldSignature, - selector: getFunctionSelector(worldSignature), + selector: toFunctionSelector(worldSignature), systemId, systemFunctionSignature: sig, - systemFunctionSelector: getFunctionSelector(sig), + systemFunctionSelector: toFunctionSelector(sig), }; }); @@ -71,7 +62,7 @@ export function resolveConfig({ allowedSystemIds: system.accessListSystems.map((name) => resourceToHex({ type: "system", namespace, name: resolvedConfig.systems[name].name }) ), - address: getCreate2Address({ from: deployer, bytecode: contractData.bytecode, salt }), + getAddress: (deployer: Address) => getCreate2Address({ from: deployer, bytecode: contractData.bytecode, salt }), bytecode: contractData.bytecode, deployedBytecodeSize: contractData.deployedBytecodeSize, abi: contractData.abi, @@ -79,28 +70,6 @@ export function resolveConfig({ }; }); - // resolve allowedSystemIds - // TODO: resolve this at deploy time so we can allow for arbitrary system IDs registered in the world as the source-of-truth rather than config - const systemsWithAccess = systems.map(({ allowedAddresses, allowedSystemIds, ...system }) => { - const allowedSystemAddresses = allowedSystemIds.map((systemId) => { - const targetSystem = systems.find((s) => s.systemId === systemId); - if (!targetSystem) { - throw new Error( - `System ${resourceToLabel(system)} wanted access to ${resourceToLabel( - hexToResource(systemId) - )}, but it wasn't found in the config.` - ); - } - return targetSystem.address; - }); - return { - ...system, - allowedAddresses: Array.from( - new Set([...allowedAddresses, ...allowedSystemAddresses].map((addr) => getAddress(addr))) - ), - }; - }); - // ugh (https://github.com/latticexyz/mud/issues/1668) const resolveContext = { tableIds: Object.fromEntries( @@ -134,7 +103,7 @@ export function resolveConfig({ name: mod.name, installAsRoot: mod.root, installData: installArgs.length === 0 ? "0x" : installArgs[0], - address: getCreate2Address({ from: deployer, bytecode: contractData.bytecode, salt }), + getAddress: (deployer: Address) => getCreate2Address({ from: deployer, bytecode: contractData.bytecode, salt }), bytecode: contractData.bytecode, deployedBytecodeSize: contractData.deployedBytecodeSize, abi: contractData.abi, @@ -143,7 +112,7 @@ export function resolveConfig({ return { tables, - systems: systemsWithAccess, + systems, modules, }; } diff --git a/packages/cli/src/runDeploy.ts b/packages/cli/src/runDeploy.ts index 90e4ff556e..ba668668b8 100644 --- a/packages/cli/src/runDeploy.ts +++ b/packages/cli/src/runDeploy.ts @@ -22,6 +22,10 @@ export const deployOptions = { profile: { type: "string", desc: "The foundry profile to use" }, saveDeployment: { type: "boolean", desc: "Save the deployment info to a file", default: true }, rpc: { type: "string", desc: "The RPC URL to use. Defaults to the RPC url from the local foundry.toml" }, + deployerAddress: { + type: "string", + desc: "Deploy using an existing deterministic deployer (https://github.com/Arachnid/deterministic-deployment-proxy)", + }, worldAddress: { type: "string", desc: "Deploy to an existing World at the given address" }, srcDir: { type: "string", desc: "Source directory. Defaults to foundry src directory." }, skipBuild: { type: "boolean", desc: "Skip rebuilding the contracts before deploying" }, @@ -88,6 +92,7 @@ in your contracts directory to use the default anvil private key.` const startTime = Date.now(); const worldDeploy = await deploy({ + deployerAddress: opts.deployerAddress as Hex | undefined, salt, worldAddress: opts.worldAddress as Hex | undefined, client,