Skip to content

Commit 8eaad30

Browse files
authored
fix(cli): support public library methods in modules (latticexyz#3308)
1 parent 9dbc565 commit 8eaad30

22 files changed

+240
-89
lines changed

.changeset/gentle-carrots-kneel.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@latticexyz/cli": patch
3+
---
4+
5+
Added support for deploying public libraries used within modules.

.changeset/little-ways-change.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@latticexyz/world-module-erc20": patch
3+
"@latticexyz/world-modules": patch
4+
---
5+
6+
Changed ERC20 and ERC721 related modules to use public library methods instead of manual `delegatecall`s.

e2e/pnpm-lock.yaml

+3-35
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
},
88
"type": "module",
99
"scripts": {
10-
"all-build": "for dir in packages/store packages/world packages/world-modules packages/cli test/mock-game-contracts e2e/packages/contracts examples/*/packages/contracts examples/multiple-namespaces templates/*/packages/contracts; do (cd \"$dir\" && pwd && pnpm build); done",
10+
"all-build": "for dir in packages/store packages/world packages/world-modules packages/cli test/mock-game-contracts test/puppet-modules e2e/packages/contracts examples/*/packages/contracts examples/multiple-namespaces templates/*/packages/contracts; do (cd \"$dir\" && pwd && pnpm build); done",
1111
"all-install": "for dir in . docs e2e examples/* templates/*; do (cd \"$dir\" && pwd && pnpm install); done",
1212
"bench": "pnpm run --recursive bench",
1313
"build": "turbo run build --concurrency=100%",

packages/cli/src/deploy/resolveConfig.ts

+28-10
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { groupBy } from "@latticexyz/common/utils";
77
import { findLibraries } from "./findLibraries";
88
import { createPrepareDeploy } from "./createPrepareDeploy";
99
import { World } from "@latticexyz/world";
10+
import { findUp } from "find-up";
11+
import { createRequire } from "node:module";
1012

1113
// TODO: replace this with a manifest/combined config output
1214

@@ -22,18 +24,34 @@ export async function resolveConfig({
2224
readonly systems: readonly System[];
2325
readonly libraries: readonly Library[];
2426
}> {
25-
const libraries = findLibraries(forgeOutDir).map((library): Library => {
26-
// foundry/solc flattens artifacts, so we just use the path basename
27-
const contractData = getContractData(path.basename(library.path), library.name, forgeOutDir);
28-
return {
29-
path: library.path,
30-
name: library.name,
31-
abi: contractData.abi,
32-
prepareDeploy: createPrepareDeploy(contractData.bytecode, contractData.placeholders),
33-
deployedBytecodeSize: contractData.deployedBytecodeSize,
34-
};
27+
const requirePath = await findUp("package.json");
28+
if (!requirePath) throw new Error("Could not find package.json to import relative to.");
29+
const require = createRequire(requirePath);
30+
31+
const moduleOutDirs = config.modules.flatMap((mod) => {
32+
if (mod.artifactPath == undefined) {
33+
return [];
34+
}
35+
36+
// Navigate up two dirs to get the contract output directory
37+
const moduleOutDir = path.join(require.resolve(mod.artifactPath), "../../");
38+
return [moduleOutDir];
3539
});
3640

41+
const libraries = [forgeOutDir, ...moduleOutDirs].flatMap((outDir) =>
42+
findLibraries(outDir).map((library): Library => {
43+
// foundry/solc flattens artifacts, so we just use the path basename
44+
const contractData = getContractData(path.basename(library.path), library.name, outDir);
45+
return {
46+
path: library.path,
47+
name: library.name,
48+
abi: contractData.abi,
49+
prepareDeploy: createPrepareDeploy(contractData.bytecode, contractData.placeholders),
50+
deployedBytecodeSize: contractData.deployedBytecodeSize,
51+
};
52+
}),
53+
);
54+
3755
const baseSystemContractData = getContractData("System.sol", "System", forgeOutDir);
3856
const baseSystemFunctions = baseSystemContractData.abi
3957
.filter((item): item is typeof item & { type: "function" } => item.type === "function")

packages/cli/src/runDeploy.ts

+1
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export async function runDeploy(opts: DeployOptions): Promise<WorldDeploy> {
9191
config,
9292
forgeOutDir: outDir,
9393
});
94+
9495
const artifacts = await findContractArtifacts({ forgeOutDir: outDir });
9596
// TODO: pass artifacts into configToModules (https://github.com/latticexyz/mud/issues/3153)
9697
const modules = await configToModules(config, outDir);

packages/cli/src/utils/getContractArtifact.ts

+17-11
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,24 @@ export type GetContractArtifactResult = {
1111
deployedBytecodeSize: number;
1212
};
1313

14+
function isBytecode(value: string): value is Hex {
15+
return isHex(value, { strict: false });
16+
}
17+
1418
const bytecodeSchema = z.object({
15-
object: z.string().refine(isHex),
16-
linkReferences: z.record(
17-
z.record(
18-
z.array(
19-
z.object({
20-
start: z.number(),
21-
length: z.number(),
22-
}),
19+
object: z.string().refine(isBytecode),
20+
linkReferences: z
21+
.record(
22+
z.record(
23+
z.array(
24+
z.object({
25+
start: z.number(),
26+
length: z.number(),
27+
}),
28+
),
2329
),
24-
),
25-
),
30+
)
31+
.optional(),
2632
});
2733

2834
const artifactSchema = z.object({
@@ -34,7 +40,7 @@ const artifactSchema = z.object({
3440
export function getContractArtifact(artifactJson: unknown): GetContractArtifactResult {
3541
// TODO: improve errors or replace with arktype?
3642
const artifact = artifactSchema.parse(artifactJson);
37-
const placeholders = findPlaceholders(artifact.bytecode.linkReferences);
43+
const placeholders = findPlaceholders(artifact.bytecode.linkReferences ?? {});
3844

3945
return {
4046
abi: artifact.abi,

packages/world-module-erc20/gas-report.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@
135135
"file": "test/ERC20Module.t.sol:ERC20ModuleTest",
136136
"test": "testInstall",
137137
"name": "install erc20 module",
138-
"gasUsed": 4524262
138+
"gasUsed": 4523802
139139
},
140140
{
141141
"file": "test/ERC20Pausable.t.sol:ERC20PausableWithInternalStoreTest",

packages/world-module-erc20/src/ERC20Module.sol

+2-13
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ contract ERC20Module is Module {
1717
error ERC20Module_InvalidNamespace(bytes14 namespace);
1818
error ERC20Module_NamespaceAlreadyExists(bytes14 namespace);
1919

20-
ERC20RegistryLib public immutable registryLib = new ERC20RegistryLib();
21-
2220
function install(bytes memory encodedArgs) public {
2321
// TODO: we should probably check just for namespace, not for all args
2422
requireNotInstalled(__self, encodedArgs);
@@ -44,15 +42,15 @@ contract ERC20Module is Module {
4442
// The token should have transferred the namespace ownership to this module in its constructor
4543
world.transferOwnership(namespaceId, _msgSender());
4644

47-
registryLib.delegateRegister(world, namespaceId, address(token));
45+
ERC20RegistryLib.register(world, namespaceId, address(token));
4846
}
4947

5048
function installRoot(bytes memory) public pure {
5149
revert Module_RootInstallNotSupported();
5250
}
5351
}
5452

55-
contract ERC20RegistryLib {
53+
library ERC20RegistryLib {
5654
function register(IBaseWorld world, ResourceId namespaceId, address token) public {
5755
ResourceId erc20RegistryTableId = ModuleConstants.registryTableId();
5856
if (!ResourceIds.getExists(erc20RegistryTableId)) {
@@ -63,12 +61,3 @@ contract ERC20RegistryLib {
6361
ERC20Registry.set(erc20RegistryTableId, namespaceId, address(token));
6462
}
6563
}
66-
67-
function delegateRegister(ERC20RegistryLib lib, IBaseWorld world, ResourceId namespaceId, address token) {
68-
(bool success, bytes memory returnData) = address(lib).delegatecall(
69-
abi.encodeCall(ERC20RegistryLib.register, (world, namespaceId, token))
70-
);
71-
if (!success) revertWithBytes(returnData);
72-
}
73-
74-
using { delegateRegister } for ERC20RegistryLib;

packages/world-module-erc20/test/ERC20Module.t.sol

+1-4
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,7 @@ contract ERC20ModuleTest is Test, GasReporter {
4646
ResourceId erc20NamespaceId = WorldResourceIdLib.encodeNamespace(TestConstants.ERC20_NAMESPACE);
4747

4848
// Token should retain access to the namespace
49-
address token = ERC20Registry.get(
50-
erc20RegistryTableId,
51-
WorldResourceIdLib.encodeNamespace(TestConstants.ERC20_NAMESPACE)
52-
);
49+
address token = ERC20Registry.get(erc20RegistryTableId, erc20NamespaceId);
5350

5451
// Module should grant the token access to the token namespace
5552
assertTrue(ResourceAccess.get(erc20NamespaceId, token), "Token does not have access to its namespace");

packages/world-modules/src/modules/erc20-puppet/ERC20Module.sol

+2-7
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ import { ERC20Metadata, ERC20MetadataData } from "./tables/ERC20Metadata.sol";
2424
contract ERC20Module is Module {
2525
error ERC20Module_InvalidNamespace(bytes14 namespace);
2626

27-
address immutable registrationLibrary = address(new ERC20ModuleRegistrationLibrary());
28-
2927
function install(bytes memory encodedArgs) public {
3028
// Require the module to not be installed with these args yet
3129
requireNotInstalled(__self, encodedArgs);
@@ -40,10 +38,7 @@ contract ERC20Module is Module {
4038

4139
// Register the ERC20 tables and system
4240
IBaseWorld world = IBaseWorld(_world());
43-
(bool success, bytes memory returnData) = registrationLibrary.delegatecall(
44-
abi.encodeCall(ERC20ModuleRegistrationLibrary.register, (world, namespace))
45-
);
46-
if (!success) revertWithBytes(returnData);
41+
ERC20ModuleRegistrationLib.register(world, namespace);
4742

4843
// Initialize the Metadata
4944
ERC20Metadata.set(_metadataTableId(namespace), metadata);
@@ -69,7 +64,7 @@ contract ERC20Module is Module {
6964
}
7065
}
7166

72-
contract ERC20ModuleRegistrationLibrary {
67+
library ERC20ModuleRegistrationLib {
7368
/**
7469
* Register systems and tables for a new ERC20 token in a given namespace
7570
*/

packages/world-modules/src/modules/erc721-puppet/ERC721Module.sol

+2-7
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ import { ERC721Metadata, ERC721MetadataData } from "./tables/ERC721Metadata.sol"
2727
contract ERC721Module is Module {
2828
error ERC721Module_InvalidNamespace(bytes14 namespace);
2929

30-
address immutable registrationLibrary = address(new ERC721ModuleRegistrationLibrary());
31-
3230
function install(bytes memory encodedArgs) public {
3331
// Require the module to not be installed with these args yet
3432
requireNotInstalled(__self, encodedArgs);
@@ -43,10 +41,7 @@ contract ERC721Module is Module {
4341

4442
// Register the ERC721 tables and system
4543
IBaseWorld world = IBaseWorld(_world());
46-
(bool success, bytes memory returnData) = registrationLibrary.delegatecall(
47-
abi.encodeCall(ERC721ModuleRegistrationLibrary.register, (world, namespace))
48-
);
49-
if (!success) revertWithBytes(returnData);
44+
ERC721ModuleRegistrationLib.register(world, namespace);
5045

5146
// Initialize the Metadata
5247
ERC721Metadata.set(_metadataTableId(namespace), metadata);
@@ -72,7 +67,7 @@ contract ERC721Module is Module {
7267
}
7368
}
7469

75-
contract ERC721ModuleRegistrationLibrary {
70+
library ERC721ModuleRegistrationLib {
7671
/**
7772
* Register systems and tables for a new ERC721 token in a given namespace
7873
*/

0 commit comments

Comments
 (0)