World
ERC-20 tokens

ERC 20 (fungible tokens) module

⚠️

This module is unaudited and may change in the future.

The erc20 module (opens in a new tab) lets you create ERC-20 (opens in a new tab) tokens as part of a MUD World. The advantage of doing this, rather than creating a separate ERC-20 contract (opens in a new tab) and merely controlling it from MUD, is that all the information is in MUD tables and is immediately available in the client.

The ERC20Module receives the namespace, name and symbol of the token as parameters, and deploys the new token. Currently it installs a default ERC20 (opens in a new tab) with the following features:

  • ERC20Burnable: Allows users to burn their tokens (or the ones approved to them) using the burn and burnFrom function.
  • ERC20Pausable: Supports pausing and unpausing token operations. This is combined with the pause and unpause public functions that can be called by addresses with access to the token's namespace.
  • Minting: Addresses with namespace access can call the mint function to mint tokens to any address.

Installation

The simplest way to install this module and register a new ERC20 token in your world is to import the defineERC20Module helper and use it to add the module's declaration to your MUD config:

mud.config.ts
import { defineWorld } from "@latticexyz/world";
import { defineERC20Module } from "@latticexyz/world-module-erc20/internal";
 
export default defineWorld({
  namespace: "app",
  tables: {
    Counter: {
      schema: {
        value: "uint32",
      },
      key: [],
    },
  },
  modules: [
    defineERC20Module({
      // The new namespace the module will register
      namespace: "erc20Namespace",
      // The metadata of the ERC20 token that will be deployed by the module
      name: "MyToken",
      symbol: "MTK",
    }),
  ],
});

This will deploy the token and register it under the provided namespace. Note that the namespace must not exist beforehand, as the module will create it upon installation. The ownership of the new namespace will be transferred to the deployer after installation.

Usage

You can use the token the same way you use any other ERC20 contract. For example, run this script.

ManageERC20.s.sol
import { Script } from "forge-std/Script.sol";
import { console } from "forge-std/console.sol";
import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol";
import { ResourceId } from "@latticexyz/store/src/ResourceId.sol";
import { RESOURCE_TABLE } from "@latticexyz/store/src/storeResourceTypes.sol";
import { WorldResourceIdLib } from "@latticexyz/world/src/WorldResourceId.sol";
import { ERC20Registry } from "@latticexyz/world-module-erc20/src/codegen/index.sol";
import { ERC20WithWorld as ERC20 } from "@latticexyz/world-module-erc20/src/examples/ERC20WithWorld.sol";
 
import { IWorld } from "../src/codegen/world/IWorld.sol";
 
contract ManageERC20 is Script {
  function reportBalances(ERC20 erc20, address myAddress) internal view {
    address alice = address(0x600D);
 
    console.log("     My balance:", erc20.balanceOf(myAddress));
    console.log("Alice's balance:", erc20.balanceOf(alice));
    console.log("--------------");
  }
 
  function run() external {
    address worldAddress = address(0x8D8b6b8414E1e3DcfD4168561b9be6bD3bF6eC4B);
 
    // Specify a store so that you can use tables directly in PostDeploy
    StoreSwitch.setStoreAddress(worldAddress);
 
    // Load the private key from the `PRIVATE_KEY` environment variable (in .env)
    uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
    address myAddress = vm.addr(deployerPrivateKey);
 
    // Start broadcasting transactions from the deployer account
    vm.startBroadcast(deployerPrivateKey);
 
    // Get the ERC-20 token address
    ResourceId namespaceResource = WorldResourceIdLib.encodeNamespace(bytes14("erc20Namespace"));
    ResourceId erc20RegistryResource = WorldResourceIdLib.encode(RESOURCE_TABLE, "erc20-module", "ERC20_REGISTRY");
    address tokenAddress = ERC20Registry.getTokenAddress(erc20RegistryResource, namespaceResource);
    console.log("Token address", tokenAddress);
 
    address alice = address(0x600D);
 
    // Use the token
    ERC20 erc20 = ERC20(tokenAddress);
 
    console.log("Initial state");
    reportBalances(erc20, myAddress);
 
    // Mint some tokens
    console.log("Minting for myself");
    erc20.mint(myAddress, 1000);
    reportBalances(erc20, myAddress);
 
    // Transfer tokens
    console.log("Transfering to Alice");
    erc20.transfer(alice, 750);
    reportBalances(erc20, myAddress);
 
    vm.stopBroadcast();
  }
}
Explanation
    console.log("     My balance:", erc20.balanceOf(myAddress));

The balanceOf function (opens in a new tab) is the way ERC-20 specifies to get an address's balance.

    // Get the ERC-20 token address
    ResourceId namespaceResource = WorldResourceIdLib.encodeNamespace(bytes14("erc20Namespace"));
    ResourceId erc20RegistryResource = WorldResourceIdLib.encode(RESOURCE_TABLE, "erc20-module", "ERC20_REGISTRY");
    address tokenAddress = ERC20Registry.getTokenAddress(erc20RegistryResource, namespaceResource);
    console.log("Token address", tokenAddress);

This is the process to get the address of our token contract. First, we get the resourceId values for the erc20-module__ERC20Registry table and the namespace we are interested in (each namespace can only have one ERC-20 token). Then we use that table to get the token address.

    // Use the token
   ERC20 erc20 = ERC20(tokenAddress);

Cast the token address to an ERC20 contract so we can call its methods.

    console.log("Minting for myself");
    erc20.mint(myAddress, 1000);
    reportBalances(erc20, myAddress);

Mint tokens for your address. Note that only the owner of the name space is authorized to mint tokens.

    console.log("Transfering to Alice");
    erc20.transfer(alice, 750);
    reportBalances(erc20, myAddress);

Transfer a token. We can only transfer tokens we own, or that we have approval to transfer from the current owner.