MUD modules 101 (1/2) Installation

TL: DR

  • The module is a script to install reusable Systems and Store Hooks for the World.

  • The advantages of the module are that an off-chain indexer can be used, and loosely coupled stores can be maintained by separating by Namespace.

  • Conversely, the disadvantages include the need to write separate modules to customize official samples and the lack of upgradability at present.

  • The installation of erc20-puppet and erc721-puppet can be confusing so I will explain with examples.

Note: MUD is currently under development and the content of this article may be out of date. This article is supported up to version 2.0.0-next.17

Abstract

To quote from the Modules description in the official documentation

Modules are onchain installation scripts that create resources and their associated configuration when called by a World. This is somewhat similar to one of the use cases for foundry scripts, except that modules are deployed onchain and can be used by any World on the same chain.

The resources mentioned here include not only System but also Store hooks, etc. The official Module files are structured as follows

  • Module definition with register script functionality

  • System or Store Hooks definition

  • Table definitions (mud.config.ts)

  • Other constant definitions such as errors, events, table IDs, etc.

Use cases

Possible use cases are

  • When using and distributing the ERC standard Contract as a System

  • When using and distributing the versatile Store hooks

According to the official documentation

The common use for a module is to add functionality to a World. In most cases we expect that a module would:

  1. Create a namespace for the new functionality.

  2. Create the tables and [Systems](https://file+.vscode-resource.vscode-cdn.net/Users/ibuki.nakajima/Workspace/Private/mud/docs/pages/world/systems) for the new functionality.

  3. Create any access permissions required (beyond the default, which is that a System has access to its own namespace).

  4. Either assign the ownership of the new namespace to an entity that would administer it (a user, a multisig, etc.) or burn it by assigning the namespace ownership to address(0).

The official sample contains seven modules, including the following

— Store Hook —-

  • [KeysInTable](https://github.com/latticexyz/mud/tree/main/packages/world-modules/src/modules/keysintable) - automatically tracks the keys in a table to make them enumerable.

  • [KeysWithValue](https://github.com/latticexyz/mud/tree/main/packages/world-modules/src/modules/keyswithvalue) - automatically tracks a reverse mapping for a table that maps a value hash to a list of keys with this value.

— System —

  • [Puppet](https://github.com/latticexyz/mud/tree/main/packages/world-modules/src/modules/puppet) - installs the PuppetDelegationControl to allow creating Puppet contracts, eg used by the ERC20 and ERC721 modules.

  • [ERC20](https://github.com/latticexyz/mud/tree/main/packages/world-modules/src/modules/erc20-puppet) - installs an ERC20 token into a namespace in a World.

  • [ERC721](https://github.com/latticexyz/mud/tree/main/packages/world-modules/src/modules/erc721-puppet) - installs an ERC721 token into a namespace in a World.

  • [UniqueEntity](https://github.com/latticexyz/mud/tree/main/packages/world-modules/src/modules/uniqueentity) - add methods to get a unique entity ID.

  • [Standard Delegations](https://github.com/latticexyz/mud/tree/main/packages/world-modules/src/modules/std-delegations) - add delegations limited by time or number of calls.

Pros. Cons.

Pros. to install as a module are as follows

  • If installed as a module in World, the following functions are available

    • Indexing with an off-chain indexer if the table is included in the module.

    • Namespace Access Control allows writing only from authorized Namespace.

  • When multiple Systems with the same functionality are installed, the Store is loosely coupled due to Namespace separation.

The disadvantages at present are as follows

  • For example, if you want to create an ERC721ExpandedSystem by inheriting from ERC721System, you cannot use the code of ERC721Module, so you need to define a separate Module if you want to customize the System.

  • Installed modules cannot be upgraded, so it is necessary to reinstall them in a different namespace and avoid using older versions.

Module installation

The official documentation states

Modules can be installed using [World.installModule(address moduleAddress, bytes memory initData)](https://github.com/latticexyz/mud/blob/ main/packages/world/src/modules/init/implementations/ModuleInstallationSystem.sol#L17-L37)

but additional work is required for erc20-puppet and erc721-puppet, which is easily confused and explained. Here, we will use erc721-puppet as an example.

For a more practical example, Sky Strife's PostDeploy script may be helpful.

This is an example of an install script for erc721-puppet.

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;

import { Script } from "forge-std/Script.sol";
import { console } from "forge-std/console.sol";
import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol";
import { IERC721Mintable } from "@latticexyz/world-modules/src/modules/erc721-puppet/IERC721Mintable.sol";
import { PuppetModule } from "@latticexyz/world-modules/src/modules/puppet/PuppetModule.sol";
import { ERC721MetadataData } from "@latticexyz/world-modules/src/modules/erc721-puppet/tables/ERC721Metadata.sol";
import { registerERC721 } from "@latticexyz/world-modules/src/modules/erc721-puppet/registerERC721.sol";
import { ResourceId, WorldResourceIdLib } from "@latticexyz/world/src/WorldResourceId.sol";

bytes14 constant NAMESPACE = "TestNS";

contract InstallERC20Example is Script {
  function run(address worldAddress) external {
    IWorld world = IWorld(worldAddress);
    StoreSwitch.setStoreAddress(worldAddress);
    uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");

    vm.startBroadcast(deployerPrivateKey);

    world.installModule(new PuppetModule(), new bytes(0));

    IERC721Mintable token = registerERC721(
      world,
      NAMESPACE,
      ERC721MetadataData({ name: "Test", symbol: "TEST", baseURI: "http://test" })
    );

    // Transfer namespace to World
    ResourceId namespaceId = WorldResourceIdLib.encodeNamespace(NAMESPACE);
    world.transferOwnership(namespaceId, worldAddress);

    // Store token address

    vm.stopBroadcast();
  }
}

I'll also include an example of a shell that runs this script.

$ forge script InstallERC20Example --sig 'run(address)' --rpc-url=$RPC_HTTP_URL $YOUR_WORLD_ADDRESS --broadcast

The meaning of each row is explained below.

Since there is a possibility that more than one of these ERC standards may be installed in the world, each of them should have a unique Namespace to avoid duplication of Tables. For example, when using a utility token and a governance token such as ERC20.

bytes14 constant NAMESPACE = "TestNS";

To use erc721-puppet, you need to install the Puppet module in advance, so do the following.

world.installModule(new PuppetModule(), new bytes(0));

The puppet module provides more detailed access control and logging capabilities and is described in the official documentation as follows

Puppet - installs the PuppetDelegationControl to allow creating Puppet contracts, eg used by the ERC20 and ERC721 modules.

Then call registerERC721()

IERC721Mintable token = registerERC721(
  world,
  NAMESPACE,
  ERC721MetadataData({ name: "Test", symbol: "TEST", baseURI: "http://test" })
);

Inside registerERC721(), world.installModule() is called to install the module in the world.

function registerERC721(
  IBaseWorld world,
  bytes14 namespace,
  ERC721MetadataData memory metadata
) returns (IERC721Mintable token) {
  // Get the ERC721 module
  ERC721Module erc721Module = ERC721Module(NamespaceOwner.get(MODULE_NAMESPACE_ID));
  if (address(erc721Module) == address(0)) {
    erc721Module = new ERC721Module();
  }

  // Install the ERC721 module with the provided args
  world.installModule(erc721Module, abi.encode(namespace, metadata));

  // Return the newly created ERC721 token
  token = IERC721Mintable(ERC721Registry.get(ERC721_REGISTRY_TABLE_ID, WorldResourceIdLib.encodeNamespace(namespace)));
}

Next, transfer the ownership rights of Namespace to the world.

ResourceId namespaceId = WorldResourceIdLib.encodeNamespace(NAMESPACE);
world.transferOwnership(namespaceId, worldAddress);

The following is an excerpt from the ERC721System to be Installed by this script to illustrate why the transfer of permissions is necessary.

For example, mint() calls _requireOwner(), which checks whether _msgSender() has access privileges to ERC721System. transferOwnership() in the installation script passes access privileges to worldAddress, so that access from the world is allowed and all others are reverted.

contract ERC721System is IERC721Mintable, System, PuppetMaster {
  // ...
	
  function mint(address to, uint256 tokenId) public virtual {
    _requireOwner();
    _mint(to, tokenId);
  }

  function _requireOwner() internal view {
    AccessControlLib.requireOwner(SystemRegistry.get(address(this)), _msgSender());
  }

  // ...
}

Finally, although not included in the script, save the address of the installed token so that it can be called from the system, etc.

Finally

In this article, I have given an overview of modules and some additional installation information. In the next article, I will write about how to customize modules.

Subscribe to 0xshanks
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.