World 101
The World is a standard smart contract that can be deployed by anyone. Creating a new World is akin to creating a new community computer or installing a new Operating System: it’s a brand new space for state and logic to be deployed by anyone on-chain — although you will probably be the first one to create resources in your new World!
When building with the MUD World framework, the first decision you need to make is whether your project requires a new World, or if you can build on an existing one. Here are some examples of situations you might find yourself in, and recommendations for which route to follow:
- I am building a standalone proof of concept: start from a fresh World.
- I am building a project on a new chain that has no World yet: start from a fresh World.
- I am building features on top of an existing project, like a marketplace for an on-chain game or an aggregator for two AMMs deployed on the same world: build on the World with the application you would like to extend.
- I want features that can only be installed by the root user / DAO of a World, and no World out there includes them: start from a fresh World.
- I want to add new features to an application I have built before: build on the World where you initially deployed your application.
World Concepts
Resources and namespaces
A World contains resources. Currently, there exists three types of resources. More of them can be added by the root user of the World (if there is one), and future versions of MUD might include new default resources.
- Namespace: a namespace is like a folder in a file system. They are used to group resources together for the purpose of making access-control less verbose. Currently, nested namespaces are not available in World framework. The filesystem is thus flat.
- Table: a Store table. Used to store and retrieve data.
- System: a piece of logic, stored as EVM bytecode. Systems have no state, and instead read and write to Tables.
Each resource is contained within a namespace. You can think of the resources within a World as a filesystem:
root |
(--mudswap < -Namespace) |
(Balance < -Table) |
(Pool < -Table) |
(Transfer < -System) |
(--tetris < -Namespace) |
(Board < -Table) |
(Move < -System) |
(Drop < -System) |
(Score < -Table) |
(Win < -System);
The organization of resources within namespaces is used for two different features of MUD:
- Access control: resources in a namespace have “write” access to the other resources within their namespace. Currently, having write access only matters for systems interacting with tables: it means these systems can create and edit records within those tables.
- Synchronization of state: MUD clients can decide which namespaces they synchronize. Synchronization means different things depending on the resource type:
- Synchronizing a Table means downloading and keeping track of all changes to records found within the Table. As an example, synchronizing a
BalanceTable
would mean keeping track of the balances of all addresses within that table. - Synchronizing a System means downloading its EVM bytecode from the chain, and in a future version of MUD, being able to execute these systems optimistically client side. As an example, this would allow clients to immediately predict the likely outcome of an on-chain action without relying on external nodes or services like Tenderly to simulate the outcome.
- Synchronizing a Table means downloading and keeping track of all changes to records found within the Table. As an example, synchronizing a
A note on managing namespaces and resources:
In most basic cases, you don’t need to worry about namespaces and access control while building your application with World (regardless of whether you are deploying a new World or building on an existing one). If your project was generated from the MUD templates using npm create mud
/yarn create mud
/mud create
, it will use the tablegen
tool from the MUD CLI to generate libraries for tables, and the deploy
tool to deploy the resources into the World. Namespace access will be done for you: systems will be able to write to all your tables out-of-the-box. You just need to decide which namespace you will build your application in!
Systems
This is WIP
- Piece of code added to the world with logic
- no state! use tables to read and write data
- _msgSender
- root systems
- systems and namespaces
- can be delegatacall from the world
- can be called from the world
- both handled transparently by the StoreSwitch in the generated table
Getting started with World
In order to use World, you just need your project to have the right folder structure and have a mud.config.mts
file at the root of your contract folder. It is recommended starting from one of the MUD template to get familiar with the structure, but it is also possible to roll out your own folder and file organization.
- Start from the minimal template
Run yarn create mud my-first-mud-project
; and cd
into it. Open your favorite code editor.
- Let’s look at the MUD config
The MUD config for the vanilla template looks like this:
import { mudConfig } from "@latticexyz/config";
export default mudConfig({
namespace: "mud",
systems: {
IncrementSystem: {
name: "increment",
openAccess: true,
},
tables: {
CounterTable: {
name: "counter",
schema: {
value: "uint32",
},
},
},
},
});
Let’s break it down:
namespace: "mud";
namespace: "mud"
: we are deploying our systems and tables in the namespace called “mud”. This namespace will be registered on the World deployed in development and will be owned by the deployer address.
systems: {
IncrementSystem: {
name: "increment",
openAccess: true,
},
},
We are deploying one system named “IncrementSystem”. The deployer will find its code by looking for a file named IncrementSystem.sol
. Its name is increment
, so its full path within the world will be /mud/increment
(remember our namespace is mud
). Its access is open, which means the World will let any address call this system.
tables: {
CounterTable: {
name: "counter",
schema: {
value: "uint32",
},
},
},
We are creating one table named “CounterTable”, with a single column named value
with type uint32
. To learn more about the format for defining tables, head to the Store documentation.
The file system on the World looks like this now when deployed:
root
|-- mud
| counter <- Table
| increment <- System
Because increment
is in the same namespace as counter
, it can write records on that table using the libraries generated by tablegen
.
- Adding another table
Let’s add a new table. It’s as simple as extending the config. For reference on how to create new tables and different options available, refer to the Store documentation (opens in a new tab).
import { mudConfig } from "@latticexyz/config";
export default mudConfig({
namespace: "mud",
systems: {
IncrementSystem: {
name: "increment",
openAccess: true,
},
tables: {
CounterTable: {
name: "counter",
schema: {
value: "uint32",
},
},
DogTable: {
name: "dog",
schema: {
owner: "address",
name: "string",
color: "string",
},
},
},
},
});
We can run yarn mud tablegen
in the contract folder to recreate the libraries.
> yarn mud tablegen
Generated table: src/codegen/tables/CounterTable.sol
Generated table: src/codegen/tables/DogTable.sol
We now have a library in src/codegen/tables/DogTable.sol
that can be used to interact with the new table we created!
The file system on the World looks like this at this stage:
root
|-- mud
| counter <- Table
| increment <- System
| mytable <- Table
- Adding another system
Let’s add a system that writes to our new table.
We create a file in src/systems
named MySystem.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
import { System } from "@latticexyz/world/src/System.sol";
contract MySystem is System {
function doStuff() public returns () {}
}
And we edit our config
import { mudConfig } from "@latticexyz/config";
export default mudConfig({
namespace: "mud",
systems: {
IncrementSystem: {
name: "increment",
openAccess: true,
},
// let's add a new system
MySystem: {
name: "mysystem",
openAccess: true
}
tables: {
CounterTable: {
name: "counter",
schema: {
value: "uint32",
},
},
DogTable: {
name: "dog",
schema: {
owner: "address",
name: "string",
color: "string"
}
}
},
}
});
Now we can import our new table, and write something to it. Let’s write a function that adds a new record to DogTable, and takes the color and the name as an argument. It will assign the owner
column to the sender of the transaction:
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
import { System } from "@latticexyz/world/src/System.sol";
import { MyNewTable } from "../codegen/tables/DogTable.sol"; // import table we created
contract MySystem is System {
function addEntry(string memory name, string memory color) public returns () {
bytes32 key = bytes32(abi.encodePacked(block.number, msg.sender, gasleft())) // creating a random key for the record
address owner = _msgSender() // IMPORTANT: always refer to the msg.sender using the _msgSender() function
DogTable.set(key, {owner: owner, name: name, color: color}) // creating our record!
}
}
That’s it! MySystem, just like IncrementSystem, will have access to DogTable given they are in the same namespace.
After this step, the filesystem of the World is like this:
root
|-- mud
| counter <- Table
| increment <- System
| dog <- Table
| mysystem <- System
- Writing a test
WIP