Best Practices

Gas Efficiency

Choose Smaller Data Types

Although Mud can help us save overall storage space through compact encoding when storing multiple data points, when a table field is defined as uint256, it still requires at least one slot of space to store. So, while preserving future expandability, we can try to use smaller data types to save storage space.

Users: {
  schema: {
    id: "uint256",
    age: "uint256",
    weight: "uint256",
    height: "uint256",
  },
  key: ["id"],
},

In the Users table above, we defined three fields of type uint256, which means each user occupies 3 slots of space. If we believe that the age, weight, and height fields, in most cases, will not exceed the range of uint32:

Users: {
  schema: {
    id: "uint256",
    age: "uint32",
    weight: "uint32",
    height: "uint32",
  },
  key: ["id"],
},

We can define their types as uint32, so each user only needs to occupy 1 slot of space. And this one slot of space is not fully filled, so we can still add more fields to the Users table in the future without significantly affecting the read and write costs of the original fields.

Read and write all fields of a table at once whenever possible

If a table contains at least two fields, the automatically generated code library will not only generate a get() method, but also get<Fieldname>() methods. Similarly, in addition to the set() method, set<Fieldname>() methods will also be generated.

In a single system call, if multiple fields of the same table need to be read or written, try to use the get() and set() methods as much as possible. Compared to using get<Fieldname>() and set<Fieldname>() methods consecutively, this approach saves more gas.

T: {
  schema: {
    id: "uint256",
    F1: "uint32",
    F2: "uint32",
  },
  key: ["id"],
},

// gasCostOf(T.getF1() + T.getF2()) > gasCostOf(T.get())
// gasCostOf(T.setF1() + T.setF2()) > gasCostOf(T.set())

Note

The more fields that need to be read or written in the same table, the more gas is saved by using get() and set() to read and write all fields at once.

Important

If only one field of the table needs to be read or written, using the get<Fieldname>() and set<Fieldname>() methods is more gas-efficient.

Design table structure based on field usage frequency

If you have an object with many attributes, for example 10, deciding whether to define each attribute as a separate table or merge them into a single table becomes a trade-off issue.

Obviously, defining each attribute as a separate table makes the table structure clearer and easier to understand. However, each table occupies at least one slot of space, which significantly increases the cost of reading and writing.

On the other hand, combining all fields into one table indiscriminately, while saving storage space, may not necessarily save on read and write costs. It can make the table structure bloated, difficult to understand and maintain.

From a gas-saving perspective, we can distribute the fields into different tables based on their usage frequency, and reduce the cost of table read and write operations in each system call by using the get() and set() methods as much as possible. Moreover, this classification method often aligns with categorization based on the inherent meaning of the fields, without adding extra complexity to understanding the tables. For example, the length, width, and height attributes of a house are all inherent properties of the house and are usually used simultaneously in most cases.

Note

How to summarize and organize fields and design table structures needs to be considered in the context of specific business scenarios. There is no uniform standard. Categorizing fields based on their usage frequency is a design approach that aligns more with saving gas.

Important

If a field’s type is a reference type, it is more suitable to define it as a separate table.

If there are other fields, whether numeric or reference types, that are always used together with it, they are suitable to be defined in the same table.

Using IWorld.call() to invoke system contracts

We are typically accustomed to using the generated world interface from the CLI to call system contracts. For example:

// SpawnSystem is a system contract under the root namespace, providing a spawn() method
//   Call from outside the world, or from a non-root system contract
IWorld(worldAddress).spawn();
//   Call from within the world, e.g., from a root system contract
worldAddress.delegatecall(abi.encodeCall(IWorld(worldAddress).spawn, ()));

// ListSystem is a system contract under the muddoc namespace, providing a list() method
IWorld(worldAddress).mudddoc_list();

This calling method is simple and clear.

If you wish to save more gas, you can also use the IWorld.call() method to invoke system contracts. This approach saves gas by explicitly specifying the system resource ID and method call parameters, thus avoiding the lookup of corresponding system resources and system function selectors through registered autonomous world function selectors. For example:

// SpawnSystem is a system contract under the root namespace, providing a spawn() method
//   Call from outside the world, or from a non-root system contract
IWorld(worldAddress).call(
  WorldResourceIdLib.encode("sy", "", "SpawnSystem"),
  abi.encodeCall(SpawnSystem.spawn, ())
);
//   Call from within the world, e.g., from a root system contract
worldAddress.delegatecall(
  abi.encodeCall(
    IWorld(worldAddress).call,
    (
      WorldResourceIdLib.encode("sy", "", "SpawnSystem"),
      abi.encodeCall(SpawnSystem.spawn, ())
    )
  )
);

// ListSystem is a system contract under the muddoc namespace, providing a list() method
IWorld(worldAddress).call(
  WorldResourceIdLib.encode("sy", "muddoc", "ListSystem"),
  abi.encodeCall(ListSystem.list, ())
);