System Best Practices
Security considerations
Avoid the root namespace if possible
A System
in the root namespace costs slightly less gas because it uses the World
context, including storage, and does not need to call the World
back to read or write information.
However, because it runs in the World
context, there are security risks in writing such a System
.
- A root namespace
System
can overwrite information in tables. This means, for example, it can create any delegation that it wants by modifyingworld__UserDelegationControl
. - Any ETH owned by a namespace in the
World
is actually stored in theWorld
and therefore can be sent anywhere by such aSystem
. - A root namespace
System
can call any otherSystem
with arbitrary values for_msgSender()
and_msgValue()
. This means it can perform any action while pretending to be any user.
This violates the security principle of least privilege (opens in a new tab). Ideally, you should avoid running in the root namespace.
If you have to use a System
in the root namespace, avoid using
delegatecall
(opens in a new tab),
except for libraries whose security you verified. Any contract you call with delegatecall
is going to inherit the
World
context, and have unlimited permissions on your application.
Design consideraions
One call, one action
If you want a transaction to do multiple actions (for example: register, start a game, and then perform the first move in the game), you don't need a DoThis_DoThat_AndThen_DoTheOtherThing
function.
Just use batch calls, which preserve the message sender (in `_msgSender()).
Use delegation for user agents
If a System
is going to act on the users' behalf, it will need to call other System
s, probably in other namespaces.
The way to do this is to request users to delegate their permissions.
This is necessary because:
- If you call another
System
through theWorld
, the message sender (_msgSender()
) is the callingSystem
. The calledSystem
has no way of knowing if it can trust the callerSystem
to represent the wishes of the ultimate user. See the Solidity documentation (opens in a new tab) for an in-depth explanation. - If you
delegatecall
anotherSystem
directly you keep_msgSender()
's value, but the calledSystem
is only allowed to perform actions allowed to the callingSystem
.
Don't send ETH with your calls
The way ETH balances work in MUD, all ETH is stored in the World
contract and internally accounted as belonging to namespaces.
If your System
is in the root namespace it has access to this ETH, but if you transfer out except through BalanceTransferSystem
(opens in a new tab), you need to update the accounting table.
If your System
is not in the root namespace it does not have direct access to the ETH anyway.
You have to use BalanceTransferSystem
.
Implementation considerations
Use libraries to bypass the contract size limit
If you have a large System
and you come up against the contract size limit (opens in a new tab), don't split it in two.
Instead, use public libraries (opens in a new tab).
Libraries are called using delegatecall
(opens in a new tab), so they have the same permissions as the calling contract.
This includes the ability to write to MUD tables when the calling System
is permitted to do so.