World
Systems

Systems

One of the design principles of MUD is to separate the state of the World from the business logic. The business logic is implemented in stateless System contracts. Systems are called through the World, and call back to the World to read and write state from tables.

Detailed illustration

Interaction between the World, a System, and a table

  1. An account calls a function called namespace__function via the World. This function was registered by the owner of the namespace namespace and points to the function function in one of the Systems in the namespace namespace.

  2. The World verifies that access is permitted (for example, because namespace:system is publicly accessible) and if so calls function on the namespace:system contract with the provided parameters.

  3. At some point in its execution function decides to update the data in the table namespace:table. As with all other tables, this table is stored in the World's storage. To modify it, function calls a function on the World contract.

  4. The World verifies that access is permitted (by default it would be, because namespace:system has access to the namespace namespace). If so, it modifies the data in the namespace:table table.

The World serves as a central entry point and forwards calls to systems, which allows it to provide access control.

Calling systems

To call a System, you call the World in one of these ways:

Writing systems

A System should not have any internal state, but store all of it in tables in the World. There are several reasons for this:

  • It allows a World to enforce access controls.
  • It allows the same System to be used by multiple World contracts.
  • Upgrades are a lot simpler when all the state is centralized outside of the System contract.

Because calls to systems are proxied through the World, some message fields don't reflect the original call. Use these substitutes:

Vanilla SoliditySystem replacement
msg.sender_msgSender()
msg.value_msgValue()

When calling other contracts from a System, be aware that if you use delegatecall the called contract inherits the System's permissions and can modify data in the World on behalf of the System.

Calling one System from another

There are two ways to call one System from another one.

Call typecall to the Worlddelegatecall directly to the System
Permissionsthose of the called Systemthose of the calling System
_msgSender()calling System (unless you can use callFrom, which is only available when the user delegates to your System)can use WorldContextProvider (opens in a new tab) to transfer the correct information
_msgValue()zerocan use WorldContextProvider (opens in a new tab) to transfer the correct information
Can be used by systems in the root namespaceNo (it's a security measure)Yes

Calling from a root System

If you need to call a System from a System in the root namespace you can use SystemSwitch (opens in a new tab).

  1. Import SystemSwitch.

    import { SystemSwitch } from "@latticexyz/world-modules/src/utils/SystemSwitch.sol";
  2. Import the interface for the system you wish to call.

    import { IIncrementSystem } from "../codegen/world/IIncrementSystem.sol";
  3. Call the function using SystemSwitch.call. For example, here is how you can call IncrementSystem.increment().

     uint32 returnValue = abi.decode(
       SystemSwitch.call(
         abi.encodeCall(IIncrementSystem.increment, ())
       ),
       (uint32)
     );
    Explanation
    abi.encodeCall(IIncrementSystem.increment, ())

    Use abi.encodeCall (opens in a new tab) to create the calldata. The first parameter is a pointer to the function. The second parameter is a tuple (opens in a new tab) with the function parameters. In this case, there aren't any.

    The advantage of abi.encodeCall is that it checks the types of the function parameters are correct.

    SystemSwitch.call(
         abi.encodeCall(...)
    )

    Using SystemSwitch.call with the calldata created by abi.encodeCall. SystemSwitch.call takes care of figuring out details, such as what type of call to use.

    uint32 retval = abi.decode(
       SystemSwitch.call(...),
       (uint32)
    );

    Use abi.decode (opens in a new tab) to decode the call's return value. The second parameter is the data type (or types if there are multiple return values).

Registering systems

For a System to be callable from a World it has to be registered (opens in a new tab). Only the namespace owner can register a System in a namespace.

Systems can be registered once per World, but the same system can be registered in multiple Worlds. If you need multiple instances of a System in the same world, you can deploy the System multiple times and register the individual deployments individually.

MessagingExtension.s.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;
 
import { Script } from "forge-std/Script.sol";
import { console } from "forge-std/console.sol";
import { IBaseWorld } from "@latticexyz/world-modules/src/interfaces/IBaseWorld.sol";
import { WorldRegistrationSystem } from "@latticexyz/world/src/modules/core/implementations/WorldRegistrationSystem.sol";
 
// Create resource identifiers (for the namespace and system)
import { ResourceId } from "@latticexyz/store/src/ResourceId.sol";
import { WorldResourceIdLib } from "@latticexyz/world/src/WorldResourceId.sol";
import { RESOURCE_SYSTEM } from "@latticexyz/world/src/worldResourceTypes.sol";
 
// For registering the table
import { Messages, MessagesTableId } from "../src/codegen/index.sol";
import { IStore } from "@latticexyz/store/src/IStore.sol";
import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol";
 
// For deploying MessageSystem
import { MessageSystem } from "../src/systems/MessageSystem.sol";
 
contract MessagingExtension is Script {
  function run() external {
    uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
    address worldAddress = vm.envAddress("WORLD_ADDRESS");
    WorldRegistrationSystem world = WorldRegistrationSystem(worldAddress);
    ResourceId namespaceResource = WorldResourceIdLib.encodeNamespace(bytes14("messaging"));
    ResourceId systemResource = WorldResourceIdLib.encode(RESOURCE_SYSTEM, "messaging", "MessageSystem");
 
    vm.startBroadcast(deployerPrivateKey);
 
    world.registerNamespace(namespaceResource);
    StoreSwitch.setStoreAddress(worldAddress);
    Messages.register();
    MessageSystem messageSystem = new MessageSystem();
    world.registerSystem(systemResource, messageSystem, true);
    world.registerFunctionSelector(systemResource, "incrementMessage(string)");
 
    vm.stopBroadcast();
  }
}

System registration requires several steps:

  1. Create the resource ID for the System.
  2. Deploy the System contract.
  3. Use WorldRegistrationSystem.registerSystem (opens in a new tab) to register the System. This function takes three parameters:
    • The ResourceId for the System.
    • The address of the System contract.
    • Access control - whether access to the System is public (true) or limited to entities with access either to the namespace or the System itself (false).
  4. Optionally, register function selectors for the System.

Upgrading systems

The namespace owner can upgrade a System. This is a two-step process: deploy the contract for the new System and then call registerSystem with the same ResourceId as the old one and the new contract address.

This upgrade process removes the old System contract's access to the namespace, and gives access to the new contract. Any access granted manually to the old System is not revoked, nor granted to the upgraded System.

Note: You should make sure to remove any such manually granted access. System access is based on the contract address, so somebody else could register a namespace they'd own, register the old System contract as a system in their namespace, and then abuse those permissions (if the System has code that can be used for that, of course).

Access control

When you register a System, you can specify whether it is going to be private or public.

  • A public System has no access control checks, it can be called by anybody. This is the main mechanism for user interaction with a MUD application.

  • A private System can only be called by accounts that have access. This access can be the result of:

    • Access permission to the namespace in which the System is registered.
    • Access permission specifically to the System.

Note that Systems have access to their own namespace by default, so public Systems can call private Systems in their namespace.

Root systems

The World uses call for systems in other namespaces, but delegatecall for those in the root namespace (bytes14(0)). As a result, root systems have access to the World contract's storage. Because of this access, root systems use the internal StoreCore methods (opens in a new tab), which are slightly cheaper than calling the external IStore methods (opens in a new tab) used by other systems. Note that the table libraries abstract this difference, so normally there is no reason to be concerned about it.

Another effect of having access to the storage of the World is that root systems could, in theory, overwrite any information in any table regardless of access control. Only the owner of the root namespace can register root systems. We recommend to only use the root namespace when strictly necessary.