Systems
The functionality of autonomous worlds is primarily expressed through systems. Systems are on-chain public libraries deployed as contracts. Normally, they don’t hold any assets, have no state storage, and can be used by anyone.
Developing a System
System contracts need to inherit from
@latticexyz/world/src/System.sol. This base contract provides the
essential functionalities a system needs and guides us in correctly
developing a system.
System.sol provides three crucial functions:
_world(): Returns the associatedWorldcontract address when the system contract is called._msgSender(): Returns the address of the caller of the associatedWorldcontract._msgValue(): Returns the amount of ETH sent by the caller when calling the associatedWorldcontract.
Suppose the buy function of the same TokenSystem is called by
users in two autonomous worlds:
--> call, ==> delegatecall
User1 (with 0.5 eth) --> WorldAbc --> TokenSystem.buy // TokenSystem in non-root namespace
User2 (with 1 eth) --> WorldXyz ==> TokenSystem.buy // TokenSystem in root namespace
Then in the first call, for TokenSystem:
_world() == address(WorldAbc)
_msgSender() == address(User1)
msg.sender == address(WorldAbc)
_msgValue() == 0.5 ether
msg.value == 0
Note
World, as the interaction entry point, also uniformly manages all
incoming ETH. Any routing call from World to System will not
carry ETH.
In the second call:
_world() == address(WorldXyz)
_msgSender() == address(User2)
msg.sender == address(User2)
_msgValue() == 1 ether
msg.value == 1 ether
We can see that in the first case, after routing through World,
msg.sender is changed to the World contract address, and
msg.value is changed to 0. However, in both cases, _msgSender() and
_msgValue() return the expected values. Therefore, when developing
systems, we should use _msgSender() and _msgValue() to get the logical
caller and carried ETH.
Tip
Don’t use this. Especially, don’t use address(this) for any
logical judgments or as a parameter.
Moreover, we must note that the same system contract can be used by
multiple autonomous worlds. This means that in different call scenarios,
the World address returned by _world() for the same system
contract is different. The World contract defines and stores the
tables that the system needs to operate on, so in different World
contexts, the actual contracts where the tables reside that the system
operates on are also different.
Calling Other Systems
In the Differences of writing smart contracts section, we implemented the
SimpleStorageCallerSystem calling the setUint function of
SimpleStorageSystem using IWorld(_world()).muddoc__setUint(x).
This is the most common and frequently used approach, but it’s only applicable when two systems meet specific conditions.
Fundamentally, the calling system must have access to the called system, which applies to all system calls.
Secondly, the calling system must belong to a custom namespace.
Lastly, the called system must have registered the corresponding function selector on the
Worldcontract.Note
IWorldis an interface automatically generated byMud CLI: worldgen, which includes all the function interfaces corresponding to the function selectors registered on theWorldcontract by the systems.
Assuming no access issues, the namespace of the calling system, the namespace of the called system, and the registration status of the called system’s function all affect how we implement inter-system calls.
If the calling system belongs to the root namespace, it’s recommended
to use SystemSwitch.
Note
SystemSwitch is suitable for inter-system calls in any situation. However,
manually encoding calldata is extremely inconvenient. If you explicitly know
that the calling system belongs to a custom namespace and the called system
has registered the corresponding function selector on the World contract,
it is recommended to directly use the automatically generated functions in
the IWorld interface.
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;
import { WorldResourceIdLib } from "@latticexyz/world/src/WorldResourceId.sol";
import { System } from "@latticexyz/world/src/System.sol";
import { ResourceId } from "@latticexyz/store/src/ResourceId.sol";
import { IWorld } from "../codegen/world/IWorld.sol";
import { SystemSwitch } from "@latticexyz/world-modules/src/utils/SystemSwitch.sol";
import { SimpleStorageSystem } from "./SimpleStorageSystem.sol";
contract SimpleStorageCallerSystem is System {
function getUintFromSimpleStorageSystem() public view returns (uint) {
ResourceId simpleStorageSystemId = WorldResourceIdLib.encode("sy", "muddoc", "SimpleStorage");
return abi.decode(
SystemSwitch.call(simpleStorageSystemId, abi.encodeWithSelector(SimpleStorageSystem.getUint.selector)),
(uint256)
);
}
}
If the calling system belongs to a custom namespace and the called system
has not registered its functions, it is recommended to use IWorld.call.
Note
Compared to SystemSwitch, directly using IWorld.call can save one
if...else... condition check.
function getUintFromSimpleStorageSystem() public view returns (uint) {
ResourceId simpleStorageSystemId = WorldResourceIdLib.encode("sy", "muddoc", "SimpleStorage");
return abi.decode(
IWorld(_world()).call(simpleStorageSystemId, abi.encodeWithSelector(SimpleStorageSystem.getUint.selector)),
(uint256)
);
}
If the calling system belongs to a custom namespace and the called system
has registered its functions, it is recommended to directly use the
corresponding function interface in IWorld, as shown in
Message Call.
To more clearly demonstrate the implementation of inter-system calls, the complete call chains for different scenarios are as follows:
--> call, ==> delegatecall
// root system calling root system, regardless of whether the called system has registered functions
User --> World ==> SystemFrom ==> SystemTo.foo()
// root system calling root system, regardless of whether the called system has registered functions
User --> World ==> SystemFrom --> SystemTo
// non-root system calling root system, called system has not registered functions
User --> World --> SystemFrom --> World.call() ==> SystemTo.foo()
// non-root system calling non-root system, called system has not registered functions
User --> World --> SystemFrom --> World.call() --> SystemTo.foo()
// non-root system calling root system, called system has registered functions
User --> World --> SystemFrom --> World.fallback() ==> SystemTo.foo()
// non-root system calling non-root system, called system has registered functions
User --> World --> SystemFrom --> World.fallback() --> SystemTo.foo()
Note
When the calling system belongs to the root namespace, it cannot use
call to route the call through World. Although delegatecall
can be used, the extra call wastes gas.
User --> World ==> SystemFrom -❌-> World ==> SystemTo.foo()
User --> World ==> SystemFrom (==> World) ==> SystemTo.foo()
Calling External Contracts
Be cautious when using call to interact with contracts that are not
Systems, including other World contracts. This is especially important
when the called contract uses msg.sender as a parameter.
Important
If the system SystemX initiating the external contract call belongs to a
custom namespace, the caller for this contract call will be
SystemX, not World, and not tx.origin.
If the called external contract uses msg.sender as a parameter, which is
actually address(SystemX), it could potentially lead to financial losses.
This is because Systems are typically considered public, reusable
library resources.
Suppose SystemX can deposit some USDT into an on-chain DeFi mining pool
that relies on msg.sender as the source of funds, and implements a
corresponding method to withdraw the deposited USDT from the pool. Then anyone
could reuse this system to withdraw these deposited USDT. Even if access
control is added to the asset withdrawal method implementation, it cannot
prevent this behavior. This is because, by default, the data storage that
system contracts rely on for access control is stored in World, and
who is using SystemX determines which contract is the World. When your
autonomous world is using this system contract, it reads data from your
World contract. When an attacker’s autonomous world is using SystemX,
it reads data from their World contract, at which point they can provide
any data as needed.
Note
If SystemX is a system in the root namespace, the situation improves
considerably. In this case, for the called external contract,
msg.sender == address(World). Although anyone can register any namespace
and system in your World contract, only systems under root can
initiate external calls in the context of World. And only you can register
systems under the root namespace, as long as you haven’t transferred the
owner of root namespace to someone else.
System Registration
Systems need to be registered in any World contract before they can be
used. System registration consists of two parts: registering the system
contract and registering system functions.
Through registration, the system contract is recorded as a resource in the
specified namespace of the autonomous world and can be called using
IWorld.call().
The purpose of registering system functions is to add a specified function
selector as a fallback function in the World contract.
Subsequently, the registered function selector can be used to call the
World contract, and the World contract will automatically forward the
call to the corresponding system contract.
Note
Function selectors registered on the World contract must be globally
unique. There are differences in how non-root systems and root systems
register system functions.
When registering system functions for non-root systems, the global function
selector uses the namespace name as a prefix, connected with __ to the
system function name.
When registering system functions for root systems, the function selector can be arbitrarily specified.
Note
Although a system contract only needs to be registered to be used, each call requires carrying the resource ID of the system being called. For more convenient system calls, system functions can be registered with globally unique function selectors.
Registration through Configuration Files
import { defineWorld } from "@latticexyz/world";
export default defineWorld({
namespace: "muddoc",
systems: {
SimpleStorageSystem: {
name: "SimpleStorage",
openAccess: false,
accessList: ["SimpleStorageCallerSystem", "0x0123456789012345678901234567890123456789"],
deploy: {
disabled: false,
registerWorldFunctions: true,
},
},
// SimpleStorageCallerSystem: {
// name: "SimpleStorageCal",
// openAccess: true,
// accessList: [],
// deploy: {
// disabled: false,
// registerWorldFunctions: true,
// },
// },
},
// excludeSystems: ["SimpleStorageSystem"],
tables: {...},
});
This is a configuration file for systems applicable to
SimpleStorageCallerSystem and SimpleStorageSystem in
Differences of writing smart contracts. They are both in the muddoc namespace.
Let’s look at the meaning of each system configuration item:
name:string, default: first 16 characters of the system name withSystemsuffix. Used to determine the system’sResourceId. The system’sResourceIdis used to register the system in theWorld.openAccess:bool, default:true. Whether to allow open access. Iftrue, any address can call this system contract through theWorldcontract. Iffalse, it can be configured throughaccessList.Note
When
openAccessisfalseandaccessListis empty, the system contract can only be called by systems within the same namespace or the namespace owner.accessList:string[], default: empty array. Access list, can be either full names of systems in the project or addresses.deploy:object. Deployment configuration.disabled:bool, default:false. Whether to deploy and register this system contract.registerWorldFunctions:bool, default:true. Whether to register corresponding function selectors inWorldfor all external system contract functions.Note
When the system is in the
rootnamespace, the registered function selectors are consistent with the system contract’s function selectors.When the system is in a custom namespace, the registered function selector’s function name will be prefixed with the namespace name. For example,
IWorld(_world()).muddoc__getUint().
excludeSystems:string[], default: empty array. Disabled systems. Disabled systems are treated as if they don’t exist at all.
Mud CLI automatically completes the deployment of all systems in the
project and registers them to the newly deployed World contract based on
the configuration file during deployment/testing. If a system doesn’t need
special configuration, it doesn’t require any configuration in the file.
Default configuration items and values will be automatically applied to
system contracts that exist in the project directory but don’t appear in the
configuration file.
Note
Automated default system configuration requires the system contract file to
be named *System.sol, placed in the src folder, typically in
src/systems. The system contract name should match the file name
(excluding format suffix .sol).
Now, looking at the configuration file above, we renamed
SimpleStorageSystem, affecting its ResourceId:
0x73796d7564646f63000000000000000053696d706c6553746f72616765000000.
Here, 7379 is the hexadecimal encoding of sy, 6d7564646f63 is for
muddoc, and 53696d706c6553746f72616765 is for SimpleStorage.
We disabled public access for SimpleStorageSystem, only allowing
SimpleStorageCallerSystem and
0x0123456789012345678901234567890123456789 to call it through World.
We enabled normal deployment for SimpleStorageSystem and registered
corresponding function selectors in World for all external system
functions. This allows authorized addresses to use
IWorld(worldAddress).muddoc__getUint
and IWorld(worldAddress).muddoc__setUint.
Note
Because SimpleStorageCallerSystem and SimpleStorageSystem are in the
same namespace muddoc, SimpleStorageCallerSystem can call
SimpleStorageSystem even without configuring the accessList.
For SimpleStorageCallerSystem, we didn’t configure it in the configuration
file, which means it will use the default configuration items. The default
configuration items are the same as the commented-out configuration items in
the file. The system’s name is taken from the first 16 characters of
SimpleStorageCallerSystem. Its ResourceId is
0x73796d7564646f63000000000000000053696d706c6553746f7261676543616c,
where the last 16 characters differ, 53696d706c6553746f7261676543616c
represents SimpleStorageCal. The default configuration enables public
access, doesn’t require an additional access list, enables deployment, and
registers all external system functions.
Manual Registration
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;
import { Script } from "forge-std/Script.sol";
import { WorldResourceIdLib } from "@latticexyz/world/src/WorldResourceId.sol";
import { System } from "@latticexyz/world/src/System.sol";
import { ResourceId } from "@latticexyz/store/src/ResourceId.sol";
import { IWorld } from "../src/codegen/world/IWorld.sol";
contract ManuallyRegisterSystem is Script {
// Load the private key from the `PRIVATE_KEY` environment variable (in .env)
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
// Start broadcasting transactions from the deployer account
vm.startBroadcast(deployerPrivateKey);
// register the namespace if it not exists
IWorld(worldAddress).registerNamespace({namespaceId: WorldResourceIdLib.encodeNamespace("muddoc")});
// deploy SimpleStorageSystem
SimpleStorageSystem simpleStorageSystem = new SimpleStorageSystem();
// get SimpleStorageSystem ResourceId
ResourceId simpleStorageSystemId = WorldResourceIdLib.encode("sy", "muddoc", "SimpleStorage");
// register SimpleStorageSystem in provided worldAddress with open access
IWorld(worldAddress).registerSystem({
systemId: simpleStorageSystemId,
system: simpleStorageSystem,
publicAccess: false
});
// register function selector for `setUint`. registered function signature is `muddoc__setUint(uint256)`
IWorld(worldAddress).registerFunctionSelector({
systemId: simpleStorageSystemId,
systemFunctionSignature: "setUint(uint256)"
});
// register function selector for `getUint`. registered function signature is `muddoc__getUint`
IWorld(worldAddress).registerFunctionSelector({
systemId: simpleStorageSystemId,
systemFunctionSignature: "getUint()"
});
}
This is a script for manually deploying and registering
SimpleStorageSystem, which belongs to the muddoc namespace.
If we want to register SimpleStorageSystem in the root namespace, we
can refer to the following example. The difference is that systems within the
root namespace can customize function signatures when registering system
functions.
SimpleStorageSystem simpleStorageRootSystem = new SimpleStorageSystem();
// root namepsace name is empty string
ResourceId simpleStorageRootSystemId = WorldResourceIdLib.encode("sy", "", "SimpleStorage");
IWorld(worldAddress).registerSystem({
systemId: simpleStorageRootSystemId,
system: simpleStorageRootSystem,
publicAccess: false
});
// you can customize function signature when registering root system functions
IWorld(worldAddress).registerRootFunctionSelector({
systemId: simpleStorageRootSystemId,
worldFunctionSignature: "myRootSetUint(uint256)",
systemFunctionSignature: "setUint(uint256)"
});
IWorld(worldAddress).registerRootFunctionSelector({
systemId: simpleStorageRootSystemId,
worldFunctionSignature: "myRootGetUint()",
systemFunctionSignature: "getUint()"
});
Important
The code above is just an example of registering a system in the root
namespace. It doesn’t mean we can manually change a system’s namespace this
way.
We recommend using configuration files to change namespaces. This approach can simultaneously update both tables and systems. When manually registering systems and changing namespaces, it’s easy to forget updating the auto-generated table code libraries, potentially causing data inconsistency.
System Usage
Here, system usage refers to how EOAs or contracts outside the World
contract use functions of registered systems.
Note
The implementation process is the same as non-root namespace systems
calling other systems.
We must reiterate that World is the unified entry point for the autonomous
world. Externally, any system function call must go through the World
contract.
There are two methods of usage. One is to use the system’s SystemId
(i.e., ResourceId) to forward the calldata through the World
contract to the system contract.
ResourceId simpleStorageSystemId = WorldResourceIdLib.encode("sy", "muddoc", "SimpleStorage");
uint256 res = abi.decode(
IWorld(worldAddress).call(simpleStorageSystemId, abi.encodeWithSelector(SimpleStorageSystem.getUint.selector)),
(uint256)
);
The other method is to directly call the World contract using the function
selectors registered by the system.
uint256 res = IWorld(worldAddress).muddoc__getUint();
Core Systems
More details about core systems can be found in Core Systems.