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 associated ``World`` contract address when the system contract is called. - ``_msgSender()``: Returns the address of the caller of the associated ``World`` contract. - ``_msgValue()``: Returns the amount of ETH sent by the caller when calling the associated ``World`` contract. Suppose the ``buy`` function of the same ``TokenSystem`` is called by users in two autonomous worlds: .. code-block:: ts --> 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``: .. code-block:: solidity _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: .. code-block:: solidity _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 :ref:`dev-differences` 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 ``World`` contract. .. note:: ``IWorld`` is an interface automatically generated by ``Mud CLI: worldgen``, which includes all the function interfaces corresponding to the function selectors registered on the ``World`` contract 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. .. code-block:: solidity // 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. .. code-block:: solidity 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 :ref:`dev-differences_contract_interaction`. To more clearly demonstrate the implementation of inter-system calls, the complete call chains for different scenarios are as follows: .. code-block:: ts --> 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``. .. code-block:: 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 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: ts 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 :ref:`dev-differences`. 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 with ``System`` suffix. Used to determine the system's ``ResourceId``. The system's ``ResourceId`` is used to register the system in the ``World``. - ``openAccess``: ``bool``, default: ``true``. Whether to allow open access. If ``true``, any address can call this system contract through the ``World`` contract. If ``false``, it can be configured through ``accessList``. .. note:: When ``openAccess`` is ``false`` and ``accessList`` is 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 in ``World`` for all external system contract functions. .. note:: When the system is in the ``root`` namespace, 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 ^^^^^^^^^^^^^^^^^^^ .. code-block:: solidity // 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. .. code-block:: solidity 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. .. code-block:: solidity 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. .. code-block:: solidity uint256 res = IWorld(worldAddress).muddoc__getUint(); Core Systems -------------- More details about core systems can be found in :ref:`internals_core_systems`.