Account Delegation

Account delegation

Account delegation allows a delegator address to permit a delegatee address to call a System on its behalf. Alternatively, the namespace owner can register a delegation that applies to all calls to all Systems in the namespace.

This feature enables multiple use cases.

  • Session wallets. Normally a wallet requires the user to authorize each transaction separately. In the case of a blockchain game, this means having to authorize every move, which is excessive. Using account delegation players can authorize a different wallet, one whose private key is stored on the client, to act on their behalf. Because this wallet's private key is stored in the client, rather than a browser extension, the client can decide when asking for authorization is warranted and when it isn't. By making sure this is a separate wallet, we protect the player's main account in the case of vulnerable or malicious game clients.

  • Approvals. Users can approve contracts to perform actions in a MUD world on their behalf. This is conceptually similar to how ERC20's approve/transferFrom (opens in a new tab) enables atomic swaps by allowing contracts to withdraw from a user's balance and then deposit a different asset in a single transaction. For example, an agent could exchange two in-game assets atomically if the two sides of the trade give it the necessary permission.

Delegation and Account Abstraction

While there are some overlaps in use cases between ERC-4337 (opens in a new tab) and MUD's account delegation, they are different things. In ERC-4337, a smart contract wallet becomes your main onchain identity. With MUD delegation, you keep your existing account, but (temporarily) approve other accounts to act on its behalf.

User delegation

The most common type of delegation is when a delegator address allows a specific delegatee address to act on its behalf.

Creating a user delegation

First, the delegator has to call registerDelegation (opens in a new tab). This function takes three parameters:

  • delegatee, the address given the privileges. This can be address(0) to let this delegation apply to all callers. Note that combining address(0) with an unlimited delegation is discouraged, as it would allow anyone to perform any action on behalf of the delegator.

  • delegationControlId, this is usually the ResourceId for a System that decides whether an attempt to do something by the delegatee on behalf of the delegator is authorized or not. Alternatively, this value can be UNLIMITED_DELEGATION (opens in a new tab), in which case the delegatee has unlimited authority.

    We have an example of a delegation control System in the std-delegations module (opens in a new tab).

  • initCallData, call data for a function that is called on the delegationControlId to inform it of the new delegation. This call data (opens in a new tab) includes both the function selector of the function to call and arguments to pass to the function (the result of abi.encodeCall).

Using a user delegation

The delegatee can use callFrom (opens in a new tab). This function takes three parameters:

  • delegator, the address on whose behalf the call is happening.
  • systemId, the System to call.
  • callData, the call data (opens in a new tab) to send the System, which includes both the function selector and arguments (the result of abi.encodeCall).

Note that between a specific delegator and a specific delegatee there can only be one user delegation at a time. This means that if you need in a World to implement multiple delegation algorithms you might need to create a dispatcher delegation that calls different verification functions based on the systemId and callData it receives.

Removing a user delegation

You can use unregisterDelegation (opens in a new tab) to remove a delegation.

Because of unregisterDelegation delegations cannot be used for a delegator to commit to allow something to be done in the future. If you need a commitment, create a table with a System that lets the address that commits write the commitment and have an action that other addresses can call only if the proper commitment is there - without giving the committed address an option to delete the entry.

Namespace delegation

The owner of a namespace can use registerNamespaceDelegation (opens in a new tab) to register a delegation that applies to all callers of systems in this namespace. This functionality is useful, for example, to implement a trusted forwarder for the namespace. This is not a security concern because the namespace owner already has full control over any table or System in the namespace.

Order of delegation checks

As you can see in callFrom (opens in a new tab), the order of delegation checks is:

  1. If there is a world:UserDelegationControl entry with the delegator and the delegatee (a.k.a. msg.sender), check it. If it results in an approval, perform the call.
  2. If the user provided a fallback delegation (one where the delegatee is address(0)), check it. If it results in an approval, perform the call.
  3. If there is an applicable namespace delegation, check that one. If it results in an approval, perform the call.

This means that if there's a namespace delegation that modifies information (for example, writes to a MUD table to gather statistics) users will be able to bypass it by creating their own delegation with the same delegetee and a System that would approve.