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(world): add support for modules, move logic to RegistrationModule #482

Merged
merged 13 commits into from
Mar 14, 2023
9 changes: 5 additions & 4 deletions packages/cli/src/commands/deploy-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import type { CommandModule } from "yargs";
import { loadWorldConfig } from "../config/loadWorldConfig.js";
import { deploy } from "../utils/deploy-v2.js";
import { logError, MUDError } from "../utils/errors.js";
import { forge, getRpcUrl } from "../utils/foundry.js";
import { getOutDirectory } from "../utils/foundry.js";
import { forge, getRpcUrl, getSrcDirectory } from "../utils/foundry.js";
import { mkdirSync, writeFileSync } from "fs";
import { loadStoreConfig } from "../config/loadStoreConfig.js";
import { deploymentInfoFilenamePrefix } from "../constants.js";
Expand All @@ -18,6 +17,7 @@ type Options = {
privateKey: string;
priorityFeeMultiplier: number;
clean?: boolean;
debug?: boolean;
};

const commandModule: CommandModule<Options, Options> = {
Expand All @@ -31,6 +31,7 @@ const commandModule: CommandModule<Options, Options> = {
clean: { type: "boolean", desc: "Remove the build forge artifacts and cache directories before building" },
printConfig: { type: "boolean", desc: "Print the resolved config" },
profile: { type: "string", desc: "The foundry profile to use" },
debug: { type: "boolean", desc: "Print debug logs, like full error messages" },
priorityFeeMultiplier: {
type: "number",
desc: "Multiply the estimated priority fee by the provided factor",
Expand All @@ -56,9 +57,9 @@ const commandModule: CommandModule<Options, Options> = {
await forge(["build"], { profile });

// Get a list of all contract names
const outDir = await getOutDirectory();
const srcDir = await getSrcDirectory();
const existingContracts = glob
.sync(`${outDir}/*.sol`)
.sync(`${srcDir}/**/*.sol`)
// Get the basename of the file
.map((path) => basename(path, ".sol"));

Expand Down
24 changes: 22 additions & 2 deletions packages/cli/src/utils/deploy-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import { MUDError } from "./errors.js";
import { getOutDirectory, getScriptDirectory, cast, forge } from "./foundry.js";
import { BigNumber, ContractInterface, ethers } from "ethers";
import { IWorld } from "@latticexyz/world/types/ethers-contracts/IWorld.js";
import { abi as WorldABI, bytecode as WorldBytecode } from "@latticexyz/world/abi/World.json";
import { bytecode as WorldBytecode } from "@latticexyz/world/abi/World.json";
import { abi as WorldABI } from "@latticexyz/world/abi/IWorld.json";
import CoreModuleData from "@latticexyz/world/abi/CoreModule.json";
import RegistrationModuleData from "@latticexyz/world/abi/RegistrationModule.json";
import { ArgumentsType } from "vitest";
import chalk from "chalk";
import { encodeSchema } from "@latticexyz/schema-type";
Expand All @@ -16,6 +19,7 @@ export interface DeployConfig {
rpc: string;
privateKey: string;
priorityFeeMultiplier: number;
debug?: boolean;
}

export interface DeploymentInfo {
Expand All @@ -27,7 +31,7 @@ export interface DeploymentInfo {
export async function deploy(mudConfig: MUDConfig, deployConfig: DeployConfig): Promise<DeploymentInfo> {
const startTime = Date.now();
const { worldContractName, namespace, postDeployScript } = mudConfig;
const { profile, rpc, privateKey, priorityFeeMultiplier } = deployConfig;
const { profile, rpc, privateKey, priorityFeeMultiplier, debug } = deployConfig;
const forgeOutDirectory = await getOutDirectory(profile);

// Set up signer for deployment
Expand Down Expand Up @@ -60,12 +64,24 @@ export async function deploy(mudConfig: MUDConfig, deployConfig: DeployConfig):
World: worldContractName
? deployContractByName(worldContractName)
: deployContract(WorldABI, WorldBytecode, "World"),
CoreModule: deployContract(CoreModuleData.abi, CoreModuleData.bytecode, "CoreModule"),
RegistrationModule: deployContract(
RegistrationModuleData.abi,
RegistrationModuleData.bytecode,
"RegistrationModule"
),
}
);

// Create World contract instance from deployed address
const WorldContract = new ethers.Contract(await contractPromises.World, WorldABI, signer) as IWorld;

// Install core Modules
console.log(chalk.blue("Installing modules"));
await fastTxExecute(WorldContract, "installRootModule", [await contractPromises.CoreModule]);
await fastTxExecute(WorldContract, "installRootModule", [await contractPromises.RegistrationModule]);
console.log(chalk.green("Installed modules"));

// Register namespace
if (namespace) await fastTxExecute(WorldContract, "registerNamespace", [toBytes16(namespace)]);

Expand Down Expand Up @@ -228,6 +244,7 @@ export async function deploy(mudConfig: MUDConfig, deployConfig: DeployConfig):
console.log(chalk.green("Deployed", contractName, "to", address));
return address;
} catch (error: any) {
if (debug) console.error(error);
if (retryCount === 0 && error?.message.includes("transaction already imported")) {
// If the deployment failed because the transaction was already imported,
// retry with a higher priority fee
Expand All @@ -237,6 +254,8 @@ export async function deploy(mudConfig: MUDConfig, deployConfig: DeployConfig):
throw new MUDError(
`Error deploying ${contractName}: invalid bytecode. Note that linking of public libraries is not supported yet, make sure none of your libraries use "external" functions.`
);
} else if (error?.message.includes("CreateContractLimit")) {
throw new MUDError(`Error deploying ${contractName}: CreateContractLimit exceeded.`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no changes needed here, but we should be sure to not throw away the original error in case its useful (today or in the future) for debugging

viem has a good example of wrapping errors with nicer messages: https://github.com/wagmi-dev/viem/blob/main/src/errors/contract.ts

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed! In this case the original error message includes the entire bytecode, so it makes your terminal explode and buries the actual error message, but we could add a CLI flag (sth like --debug) to print the full error message too

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(will leave that to a follow PR to not bloat this one too much)

} else throw error;
}
}
Expand Down Expand Up @@ -284,6 +303,7 @@ export async function deploy(mudConfig: MUDConfig, deployConfig: DeployConfig):
promises.push(txPromise);
return txPromise;
} catch (error: any) {
if (debug) console.error(error);
if (retryCount === 0 && error?.message.includes("transaction already imported")) {
// If the deployment failed because the transaction was already imported,
// retry with a higher priority fee
Expand Down
2 changes: 1 addition & 1 deletion packages/store/src/StoreSwitch.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ library StoreSwitch {
* (The isStore method doesn't return a value to save gas, but it if exists, the call will succeed.)
*/
function isDelegateCall() internal view returns (bool success) {
// Detect calls from within a constructor and revert to avoid unexpected behavior
// Detect calls from within a constructor
uint256 codeSize;
assembly {
codeSize := extcodesize(address())
Expand Down
22 changes: 11 additions & 11 deletions packages/world/gas-report.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
(test/World.t.sol) | Delete record [world.deleteRecord(namespace, file, singletonKey)]: 16038
(test/World.t.sol) | Push data to the table [world.pushToField(namespace, file, keyTuple, 0, encodedData)]: 96365
(test/World.t.sol) | Register a fallback system [bytes4 funcSelector1 = world.registerFunctionSelector(namespace, file, "", "")]: 80896
(test/World.t.sol) | Register a root fallback system [bytes4 funcSelector2 = world.registerRootFunctionSelector(namespace, file, worldFunc, 0)]: 72123
(test/World.t.sol) | Register a function selector [bytes4 functionSelector = world.registerFunctionSelector(namespace, file, "msgSender", "()")]: 101493
(test/World.t.sol) | Register a new namespace [world.registerNamespace("test")]: 151546
(test/World.t.sol) | Register a root function selector [bytes4 functionSelector = world.registerRootFunctionSelector(namespace, file, worldFunc, sysFunc)]: 96029
(test/World.t.sol) | Register a new table in the namespace [bytes32 tableSelector = world.registerTable(namespace, table, schema, defaultKeySchema)]: 252073
(test/World.t.sol) | Write data to a table field [world.setField(namespace, file, singletonKey, 0, abi.encodePacked(true))]: 44704
(test/World.t.sol) | Set metadata [world.setMetadata(namespace, file, tableName, fieldNames)]: 277121
(test/World.t.sol) | Write data to the table [Bool.set(tableId, world, true)]: 42576
(test/World.t.sol) | Delete record [world.deleteRecord(namespace, file, singletonKey)]: 16026
(test/World.t.sol) | Push data to the table [world.pushToField(namespace, file, keyTuple, 0, encodedData)]: 96397
(test/World.t.sol) | Register a fallback system [bytes4 funcSelector1 = world.registerFunctionSelector(namespace, file, "", "")]: 80940
(test/World.t.sol) | Register a root fallback system [bytes4 funcSelector2 = world.registerRootFunctionSelector(namespace, file, worldFunc, 0)]: 72166
(test/World.t.sol) | Register a function selector [bytes4 functionSelector = world.registerFunctionSelector(namespace, file, "msgSender", "()")]: 101537
(test/World.t.sol) | Register a new namespace [world.registerNamespace("test")]: 151631
(test/World.t.sol) | Register a root function selector [bytes4 functionSelector = world.registerRootFunctionSelector(namespace, file, worldFunc, sysFunc)]: 96072
(test/World.t.sol) | Register a new table in the namespace [bytes32 tableSelector = world.registerTable(namespace, table, schema, defaultKeySchema)]: 252158
(test/World.t.sol) | Write data to a table field [world.setField(namespace, file, singletonKey, 0, abi.encodePacked(true))]: 44714
(test/World.t.sol) | Set metadata [world.setMetadata(namespace, file, tableName, fieldNames)]: 277165
(test/World.t.sol) | Write data to the table [Bool.set(tableId, world, true)]: 42598
32 changes: 24 additions & 8 deletions packages/world/mud.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,6 @@ const config: StoreUserConfig = {
},
storeArgument: true,
},
SystemRegistry: {
primaryKeys: {
system: SchemaType.ADDRESS,
},
schema: {
resourceSelector: SchemaType.BYTES32,
},
},
Systems: {
primaryKeys: {
resourceSelector: SchemaType.BYTES32,
Expand All @@ -41,7 +33,17 @@ const config: StoreUserConfig = {
storeArgument: true,
dataStruct: false,
},
SystemRegistry: {
directory: "/modules/registration/tables",
primaryKeys: {
system: SchemaType.ADDRESS,
},
schema: {
resourceSelector: SchemaType.BYTES32,
},
},
ResourceType: {
directory: "/modules/registration/tables",
primaryKeys: {
resourceSelector: SchemaType.BYTES32,
},
Expand Down Expand Up @@ -76,6 +78,20 @@ const config: StoreUserConfig = {
storeArgument: true,
tableIdArgument: true,
},
InstalledModules: {
primaryKeys: {
namespace: SchemaType.BYTES16,
mdouleName: SchemaType.BYTES16,
},
schema: {
moduleAddress: SchemaType.ADDRESS,
},
// TODO: this is a workaround to use `getRecord` instead of `getField` in the autogen library,
// to allow using the table before it is registered. This is because `getRecord` passes the schema
// to store, while `getField` loads it from storage. Remove this once we have support for passing the
// schema in `getField` too. (See https://github.com/latticexyz/mud/issues/444)
dataStruct: true,
},
},
userTypes: {
enums: {
Expand Down
33 changes: 33 additions & 0 deletions packages/world/src/Call.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;

library Call {
/**
* Call a contract with delegatecall/call and append the given msgSender to the calldata.
* If the call is successfall, return the returndata as bytes memory.
* Else, forward the error (with a revert)
*/
function withSender(
address msgSender,
address target,
bytes memory funcSelectorAndArgs,
bool delegate
) internal returns (bytes memory) {
// Append msg.sender to the calldata
bytes memory callData = abi.encodePacked(funcSelectorAndArgs, msgSender);

// Call the target using `delegatecall` or `call`
(bool success, bytes memory data) = delegate
? target.delegatecall(callData) // root system
: target.call(callData); // non-root system

// Forward returned data if the call succeeded
if (success) return data;

// Forward error if the call failed
assembly {
// data+32 is a pointer to the error message, mload(data) is the length of the error message
revert(add(data, 0x20), mload(data))
}
}
}
14 changes: 6 additions & 8 deletions packages/world/src/System.sol
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;

contract System {
// Extract the trusted msg.sender value appended to the calldata
function _msgSender() internal pure returns (address sender) {
assembly {
// 96 = 256 - 20 * 8
sender := shr(96, calldataload(sub(calldatasize(), 20)))
}
}
import { WorldContext } from "./WorldContext.sol";

// For now System is just an alias for `WorldContext`,
// but we might add more default functionality in the future.
contract System is WorldContext {

}
Loading