Skip to content

Commit

Permalink
feat(cli): link and deploy public libraries (#1910)
Browse files Browse the repository at this point in the history
Co-authored-by: Fraser Scott <[email protected]>
Co-authored-by: Kevin Ingersoll <[email protected]>
  • Loading branch information
3 people authored Mar 11, 2024
1 parent 3c0f11e commit 5554b19
Show file tree
Hide file tree
Showing 25 changed files with 386 additions and 51 deletions.
9 changes: 9 additions & 0 deletions .changeset/thin-boxes-sparkle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@latticexyz/cli": minor
---

`mud deploy` now supports public/linked libraries.

This helps with cases where system contracts would exceed the EVM bytecode size limit and logic would need to be split into many smaller systems.

Instead of the overhead and complexity of system-to-system calls, this logic can now be moved into public libraries that will be deployed alongside your systems and automatically `delegatecall`ed.
15 changes: 15 additions & 0 deletions e2e/packages/contracts/src/codegen/world/ILibWrapperSystem.sol

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

10 changes: 9 additions & 1 deletion e2e/packages/contracts/src/codegen/world/IWorld.sol

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

16 changes: 16 additions & 0 deletions e2e/packages/contracts/src/libraries/Lib2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;

import { Lib3 } from "../systems/LibWrapperSystem.sol";

/**
* @title Library 2
* @author MUD (https://mud.dev) by Lattice (https://lattice.xyz)
* @dev Testing that the deployer can handle nesting of 2 libraries
* Included in a separate file to test handling libraries in different files
*/
library Lib2 {
function call() public pure returns (string memory) {
return Lib3.call();
}
}
26 changes: 26 additions & 0 deletions e2e/packages/contracts/src/libraries/Lib4and5.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;

/**
* @title Library 4
* @author MUD (https://mud.dev) by Lattice (https://lattice.xyz)
* @dev Testing that the deployer can handle nesting of 4 libraries
* Included in a separate file to test handling libraries in different files
*/
library Lib4 {
function call() public pure returns (string memory) {
return Lib5.call();
}
}

/**
* @title Library 5
* @author MUD (https://mud.dev) by Lattice (https://lattice.xyz)
* @dev Testing that the deployer can handle nesting of 4 libraries
* Included in a separate file to test handling libraries in different files
*/
library Lib5 {
function call() public pure returns (string memory) {
return "success";
}
}
50 changes: 50 additions & 0 deletions e2e/packages/contracts/src/systems/LibWrapperSystem.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;

import { System } from "@latticexyz/world/src/System.sol";
import { Lib2 } from "../libraries/Lib2.sol";
import { Lib4 } from "../libraries/Lib4and5.sol";

/**
* @dev For calling a library using a free function.
*/
function freeLibWrapper() pure returns (string memory) {
return Lib1.call();
}

/**
* @title Library 1
* @author MUD (https://mud.dev) by Lattice (https://lattice.xyz)
* @dev Used for testing that the deployer can handle a single library call
*/
library Lib1 {
function call() public pure returns (string memory) {
return Lib2.call();
}
}

/**
* @title Library 3
* @author MUD (https://mud.dev) by Lattice (https://lattice.xyz)
* @dev Testing that the deployer can handle nesting of 3 libraries
*/
library Lib3 {
function call() public pure returns (string memory) {
return Lib4.call();
}
}

/**
* @title Library Wrapper System
* @author MUD (https://mud.dev) by Lattice (https://lattice.xyz)
* @dev This contract is used for testing that the deployer can handle deeply nested public libraries
*/
contract LibWrapperSystem is System {
function callLib() public pure returns (string memory) {
return Lib1.call();
}

function callFreeFunc() public pure returns (string memory) {
return freeLibWrapper();
}
}
16 changes: 16 additions & 0 deletions e2e/packages/contracts/test/PublicLibrary.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;

import { MudTest } from "@latticexyz/world/test/MudTest.t.sol";

import { IWorld } from "../src/codegen/world/IWorld.sol";

contract PublicLibraryTest is MudTest {
/**
* @dev Test that the deployer can handle deeply nested public libraries.
*/
function testNesting() public {
assertEq(IWorld(worldAddress).callLib(), "success");
assertEq(IWorld(worldAddress).callFreeFunc(), "success");
}
}
2 changes: 2 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"path": "^0.12.7",
"rxjs": "7.5.5",
"throttle-debounce": "^5.0.0",
"toposort": "^2.0.2",
"typescript": "5.1.6",
"viem": "2.7.12",
"yargs": "^17.7.1",
Expand All @@ -72,6 +73,7 @@
"@types/node": "^18.15.11",
"@types/openurl": "^1.0.0",
"@types/throttle-debounce": "^5.0.0",
"@types/toposort": "^2.0.6",
"@types/yargs": "^17.0.10",
"ds-test": "https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0",
"forge-std": "https://github.com/foundry-rs/forge-std.git#74cfb77e308dd188d2f58864aaf44963ae6b88b1",
Expand Down
45 changes: 39 additions & 6 deletions packages/cli/src/deploy/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,48 @@ export type WorldFunction = {
readonly systemFunctionSelector: Hex;
};

export type LibraryPlaceholder = {
/**
* Path to library source file, e.g. `src/libraries/SomeLib.sol`
*/
path: string;
/**
* Library name, e.g. `SomeLib`
*/
name: string;
/**
* Byte offset of placeholder in bytecode
*/
start: number;
/**
* Size of placeholder to replace in bytes
*/
length: number;
};

export type DeterministicContract = {
readonly getAddress: (deployer: Address) => Address;
readonly bytecode: Hex;
readonly prepareDeploy: (
deployer: Address,
libraries: readonly Library[],
) => {
readonly address: Address;
readonly bytecode: Hex;
};
readonly deployedBytecodeSize: number;
readonly abi: Abi;
};

export type Library = DeterministicContract & {
/**
* Path to library source file, e.g. `src/libraries/SomeLib.sol`
*/
path: string;
/**
* Library name, e.g. `SomeLib`
*/
name: string;
};

export type System = DeterministicContract & {
readonly namespace: string;
readonly name: string;
Expand All @@ -64,10 +99,7 @@ export type System = DeterministicContract & {
readonly functions: readonly WorldFunction[];
};

export type DeployedSystem = Omit<
System,
"getAddress" | "abi" | "bytecode" | "deployedBytecodeSize" | "allowedSystemIds"
> & {
export type DeployedSystem = Omit<System, "abi" | "prepareDeploy" | "deployedBytecodeSize" | "allowedSystemIds"> & {
address: Address;
};

Expand All @@ -82,4 +114,5 @@ export type Config<config extends ConfigInput> = {
readonly tables: Tables<config>;
readonly systems: readonly System[];
readonly modules: readonly Module[];
readonly libraries: readonly Library[];
};
28 changes: 28 additions & 0 deletions packages/cli/src/deploy/createPrepareDeploy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { DeterministicContract, Library, LibraryPlaceholder, salt } from "./common";
import { spliceHex } from "@latticexyz/common";
import { Hex, getCreate2Address, Address } from "viem";

export function createPrepareDeploy(
bytecodeWithPlaceholders: Hex,
placeholders: readonly LibraryPlaceholder[],
): DeterministicContract["prepareDeploy"] {
return function prepareDeploy(deployer: Address, libraries: readonly Library[]) {
let bytecode = bytecodeWithPlaceholders;
for (const placeholder of placeholders) {
const library = libraries.find((lib) => lib.path === placeholder.path && lib.name === placeholder.name);
if (!library) {
throw new Error(`Could not find library for bytecode placeholder ${placeholder.path}:${placeholder.name}`);
}
bytecode = spliceHex(
bytecode,
placeholder.start,
placeholder.length,
library.prepareDeploy(deployer, libraries).address,
);
}
return {
bytecode,
address: getCreate2Address({ from: deployer, bytecode, salt }),
};
};
}
18 changes: 12 additions & 6 deletions packages/cli/src/deploy/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Account, Address, Chain, Client, Hex, Transport, getAddress } from "viem";
import { Account, Address, Chain, Client, Hex, Transport } from "viem";
import { ensureDeployer } from "./ensureDeployer";
import { deployWorld } from "./deployWorld";
import { ensureTables } from "./ensureTables";
Expand All @@ -12,7 +12,6 @@ import { Table } from "./configToTables";
import { ensureNamespaceOwner } from "./ensureNamespaceOwner";
import { debug } from "./debug";
import { resourceToLabel } from "@latticexyz/common";
import { uniqueBy } from "@latticexyz/common/utils";
import { ensureContractsDeployed } from "./ensureContractsDeployed";
import { randomBytes } from "crypto";
import { ensureWorldFactory } from "./ensureWorldFactory";
Expand Down Expand Up @@ -55,13 +54,18 @@ export async function deploy<configInput extends ConfigInput>({
client,
deployerAddress,
contracts: [
...uniqueBy(config.systems, (system) => getAddress(system.getAddress(deployerAddress))).map((system) => ({
bytecode: system.bytecode,
...config.libraries.map((library) => ({
bytecode: library.prepareDeploy(deployerAddress, config.libraries).bytecode,
deployedBytecodeSize: library.deployedBytecodeSize,
label: `${library.path}:${library.name} library`,
})),
...config.systems.map((system) => ({
bytecode: system.prepareDeploy(deployerAddress, config.libraries).bytecode,
deployedBytecodeSize: system.deployedBytecodeSize,
label: `${resourceToLabel(system)} system`,
})),
...uniqueBy(config.modules, (mod) => getAddress(mod.getAddress(deployerAddress))).map((mod) => ({
bytecode: mod.bytecode,
...config.modules.map((mod) => ({
bytecode: mod.prepareDeploy(deployerAddress, config.libraries).bytecode,
deployedBytecodeSize: mod.deployedBytecodeSize,
label: `${mod.name} module`,
})),
Expand Down Expand Up @@ -98,6 +102,7 @@ export async function deploy<configInput extends ConfigInput>({
const systemTxs = await ensureSystems({
client,
deployerAddress,
libraries: config.libraries,
worldDeploy,
systems: config.systems,
});
Expand All @@ -109,6 +114,7 @@ export async function deploy<configInput extends ConfigInput>({
const moduleTxs = await ensureModules({
client,
deployerAddress,
libraries: config.libraries,
worldDeploy,
modules: config.modules,
});
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/deploy/ensureContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export async function ensureContract({
readonly client: Client<Transport, Chain | undefined, Account>;
readonly deployerAddress: Hex;
} & Contract): Promise<readonly Hex[]> {
if (bytecode.includes("__$")) {
throw new Error(`Found unlinked public library in ${label} bytecode`);
}

const address = getCreate2Address({ from: deployerAddress, salt, bytecode });

const contractCode = await getBytecode(client, { address, blockTag: "pending" });
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/deploy/ensureContractsDeployed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Client, Transport, Chain, Account, Hex } from "viem";
import { waitForTransactionReceipt } from "viem/actions";
import { debug } from "./debug";
import { Contract, ensureContract } from "./ensureContract";
import { uniqueBy } from "@latticexyz/common/utils";

export async function ensureContractsDeployed({
client,
Expand All @@ -12,8 +13,11 @@ export async function ensureContractsDeployed({
readonly deployerAddress: Hex;
readonly contracts: readonly Contract[];
}): Promise<readonly Hex[]> {
// Deployments assume a deterministic deployer, so we only need to deploy the unique bytecode
const uniqueContracts = uniqueBy(contracts, (contract) => contract.bytecode);

const txs = (
await Promise.all(contracts.map((contract) => ensureContract({ client, deployerAddress, ...contract })))
await Promise.all(uniqueContracts.map((contract) => ensureContract({ client, deployerAddress, ...contract })))
).flat();

if (txs.length) {
Expand Down
Loading

0 comments on commit 5554b19

Please sign in to comment.