Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): link and deploy public libraries #1910

Merged
merged 32 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
06222fc
feat(cli): link and deploy public libraries
dk1a Nov 11, 2023
9b5446e
Merge branch 'main' into dk1a/public-libraries
dk1a Nov 11, 2023
fca4367
Merge branch 'main' into dk1a/public-libraries
dk1a Nov 12, 2023
f1265bd
test, fixes
dk1a Nov 12, 2023
88049cd
test free func
dk1a Nov 12, 2023
c6ee27a
Create thin-boxes-sparkle.md
dk1a Nov 12, 2023
ad187a2
prettier
dk1a Nov 12, 2023
90da9c0
refactor: remove unused imports
yonadaa Mar 6, 2024
5f5a0c1
fix: do not try to get contractFullPath from AST
yonadaa Mar 6, 2024
dfaffa7
Merge remote-tracking branch 'origin/main' into dk1a/public-libraries
yonadaa Mar 6, 2024
cfa3a50
chore: fix after merge
yonadaa Mar 6, 2024
c6563ad
fix: resolveConfig argument
yonadaa Mar 6, 2024
cb5cdfe
chore: unused import
yonadaa Mar 7, 2024
85bb8eb
test: document tests
yonadaa Mar 7, 2024
80a4b7a
move out dep order
holic Mar 9, 2024
630eab1
rework
holic Mar 9, 2024
1566c4b
fix labels for root namespaces
holic Mar 9, 2024
d0ffbbf
rename file to match method name
holic Mar 9, 2024
f2e331c
update changeset
holic Mar 9, 2024
463fc88
not sure how that got here
holic Mar 9, 2024
9d387a4
small error message tweak
holic Mar 9, 2024
13d6740
remove console logs
holic Mar 9, 2024
48333c4
moved out this change to another PR
holic Mar 9, 2024
8f3514d
return this to how it was
holic Mar 9, 2024
3e958e4
doesn't need to be async
holic Mar 9, 2024
efa1299
clean up
holic Mar 9, 2024
49eb1aa
more clean up
holic Mar 9, 2024
54767dd
Merge remote-tracking branch 'origin/main' into dk1a/public-libraries
holic Mar 11, 2024
32fe6fe
deploy public libs with systems and modules
holic Mar 11, 2024
8817a96
typo
holic Mar 11, 2024
0e5730c
fill in placeholders for default modules
holic Mar 11, 2024
bcc2f28
fix import
holic Mar 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading