World
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.

Delegation types

Delegation can either be unlimited or limited by various factors (time, calldata, etc.). To create a limited delegation you need a contract that implements the DelegationControl (opens in a new tab) interface to decide whether or not to approve a specific call. The StandardDelegationModule (opens in a new tab) includes three limited delegation control contracts. To use them, the owner of the root namespace needs to install this module.

Alternatively, you can write your own DelegationControl contract with custom logic.

DeployDelegation.s.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;
 
import { Script } from "forge-std/Script.sol";
import { console } from "forge-std/console.sol";
 
import { IWorld } from "../src/codegen/world/IWorld.sol";
 
import { StandardDelegationsModule } from "@latticexyz/world-modules/src/modules/std-delegations/StandardDelegationsModule.sol";
 
contract DeployDelegation is Script {
  function run() external {
    uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
    address worldAddress = vm.envAddress("WORLD_ADDRESS");
 
    vm.startBroadcast(deployerPrivateKey);
    IWorld world = IWorld(worldAddress);
 
    StandardDelegationsModule standardDelegationsModule = new StandardDelegationsModule();
    world.installRootModule(standardDelegationsModule, new bytes(0));
 
    vm.stopBroadcast();
  }
}
Explanation

Other than boilerplate, this script does two things.

StandardDelegationsModule standardDelegationsModule =
  new StandardDelegationsModule();

Deploy a new StandardDelegationsModule (opens in a new tab) contract. MUD modules are onchain installation scripts, which can be used by multiple Worlds.

world.installRootModule(standardDelegationsModule, new bytes(0));

Install the module. StandardDelegationsModule can only be installed as a root module (using installRootModule, not installModule), which can only be done by the owner of the root namespace.

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).

TestDelegation.s.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;
 
import { Script } from "forge-std/Script.sol";
import { console } from "forge-std/console.sol";
 
import { SystemboundDelegationControl } from "@latticexyz/world-modules/src/modules/std-delegations/SystemboundDelegationControl.sol";
import { SYSTEMBOUND_DELEGATION } from "@latticexyz/world-modules/src/modules/std-delegations/StandardDelegationsModule.sol";
import { ResourceId, WorldResourceIdLib, WorldResourceIdInstance } from "@latticexyz/world/src/WorldResourceId.sol";
import { RESOURCE_SYSTEM } from "@latticexyz/world/src/worldResourceTypes.sol";
 
import { IWorld } from "../src/codegen/world/IWorld.sol";
 
contract TestDelegation is Script {
  using WorldResourceIdInstance for ResourceId;
 
  function run() external {
    // Load the configuration
    uint256 userPrivateKey = vm.envUint("PRIVATE_KEY");
    uint256 userPrivateKey2 = vm.envUint("PRIVATE_KEY_2");
    address userAddress = vm.envAddress("USER_ADDRESS");
    address userAddress2 = vm.envAddress("USER_ADDRESS_2");
    address worldAddress = vm.envAddress("WORLD_ADDRESS");
 
    IWorld world = IWorld(worldAddress);
    ResourceId systemId = WorldResourceIdLib.encode({
      typeId: RESOURCE_SYSTEM,
      namespace: "",
      name: "IncrementSystem"
    });
 
    // Run as the first address
    vm.startBroadcast(userPrivateKey);
 
    world.registerDelegation(
      userAddress2,
      SYSTEMBOUND_DELEGATION,
      abi.encodeCall(SystemboundDelegationControl.initDelegation, (userAddress2, systemId, 2))
    );
 
    vm.stopBroadcast();
 
    // Run as the second address
    vm.startBroadcast(userPrivateKey2);
 
    bytes memory returnData = world.callFrom(userAddress, systemId, abi.encodeWithSignature("increment()"));
    console.logBytes(returnData);
 
    vm.stopBroadcast();
  }
}
Explanation
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;
 
import { Script } from "forge-std/Script.sol";
import { console } from "forge-std/console.sol";
 
import { SystemboundDelegationControl } from "@latticexyz/world-modules/src/modules/std-delegations/SystemboundDelegationControl.sol";
import { SYSTEMBOUND_DELEGATION } from "@latticexyz/world-modules/src/modules/std-delegations/StandardDelegationsModule.sol";

Import the contract definitions for the delegation system we use, SystemboundDelegationControl (opens in a new tab), and the System identifier for that delegation type.

import { ResourceId, WorldResourceIdLib, WorldResourceIdInstance } from "@latticexyz/world/src/WorldResourceId.sol";
import { RESOURCE_SYSTEM } from "@latticexyz/world/src/worldResourceTypes.sol";

We need to create the resource identifier for the target System, the one where USER_ADDRESS allows USER_ADDRESS_2 to perform actions on its behalf.

import { IWorld } from "../src/codegen/world/IWorld.sol";
 
contract TestDelegation is Script {
  using WorldResourceIdInstance for ResourceId;
 
  function run() external {
 
    // Load the configuration
    uint256 userPrivateKey = vm.envUint("PRIVATE_KEY");
    uint256 userPrivateKey2 = vm.envUint("PRIVATE_KEY_2");
    address userAddress = vm.envAddress("USER_ADDRESS");
    address userAddress2 = vm.envAddress("USER_ADDRESS_2");
    address worldAddress = vm.envAddress("WORLD_ADDRESS");
 
    IWorld world = IWorld(worldAddress);
    ResourceId systemId = WorldResourceIdLib.encode({
      typeId: RESOURCE_SYSTEM,
      namespace: "",
      name: "IncrementSystem"
    });

The system where increment, the function to which we are delegating access, is located in IncrementSystem at the root namespace.

 
    // Run as the first address
    vm.startBroadcast(userPrivateKey);
 
    world.registerDelegation(
      userAddress2,
      SYSTEMBOUND_DELEGATION,
      abi.encodeCall(SystemboundDelegationControl.initDelegation, (userAddress2, systemId, 2))
    );

This is how you register a delegation. The exact parameters depend on the delegation System we are using, so we call registerDelegation with some parameters, and then provide the exact call to the System, in this case initDelegation(userAddress2, systemId, 2).

The remaining code uses a user delegation, so you can see it in the next section.

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.

TestDelegation.s.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;
 
import { Script } from "forge-std/Script.sol";
import { console } from "forge-std/console.sol";
 
import { SystemboundDelegationControl } from "@latticexyz/world-modules/src/modules/std-delegations/SystemboundDelegationControl.sol";
import { SYSTEMBOUND_DELEGATION } from "@latticexyz/world-modules/src/modules/std-delegations/StandardDelegationsModule.sol";
import { ResourceId, WorldResourceIdLib, WorldResourceIdInstance } from "@latticexyz/world/src/WorldResourceId.sol";
import { RESOURCE_SYSTEM } from "@latticexyz/world/src/worldResourceTypes.sol";
 
import { IWorld } from "../src/codegen/world/IWorld.sol";
 
contract TestDelegation is Script {
  using WorldResourceIdInstance for ResourceId;
 
  function run() external {
    // Load the configuration
    uint256 userPrivateKey = vm.envUint("PRIVATE_KEY");
    uint256 userPrivateKey2 = vm.envUint("PRIVATE_KEY_2");
    address userAddress = vm.envAddress("USER_ADDRESS");
    address userAddress2 = vm.envAddress("USER_ADDRESS_2");
    address worldAddress = vm.envAddress("WORLD_ADDRESS");
 
    IWorld world = IWorld(worldAddress);
    ResourceId systemId = WorldResourceIdLib.encode({
      typeId: RESOURCE_SYSTEM,
      namespace: "",
      name: "IncrementSystem"
    });
 
    // Run as the first address
    vm.startBroadcast(userPrivateKey);
 
    world.registerDelegation(
      userAddress2,
      SYSTEMBOUND_DELEGATION,
      abi.encodeCall(SystemboundDelegationControl.initDelegation, (userAddress2, systemId, 2))
    );
 
    vm.stopBroadcast();
 
    // Run as the second address
    vm.startBroadcast(userPrivateKey2);
 
    bytes memory returnData = world.callFrom(userAddress, systemId, abi.encodeWithSignature("increment()"));
    console.logBytes(returnData);
 
    vm.stopBroadcast();
  }
}
Explanation

This code explains how to use a user delegation. The beginning of the script, which registers a delegation, is in the previous section.

    vm.stopBroadcast();
 
    // Run as the second address
    vm.startBroadcast(userPrivateKey2);
 
    bytes memory returnData = world.callFrom(userAddress, systemId,
      abi.encodeWithSignature("increment()"));
    console.logBytes(returnData);

To actually use a delegation you use world.callFrom with the address of the user on whose behalf you are performing the action, the resource identifier of the System on which you perform the action, and the calldata to send that system (which includes the signature of the function name and parameters, followed by parameter values if any).

The output is returned as bytes memory.

    vm.stopBroadcast();
  }
}

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.

RemoveDelegation.s.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;
 
import { Script } from "forge-std/Script.sol";
import { console } from "forge-std/console.sol";
import { IWorld } from "../src/codegen/world/IWorld.sol";
 
contract RemoveDelegation is Script {
  function run() external {
    // Load the configuration
    uint256 userPrivateKey = vm.envUint("PRIVATE_KEY");
    address worldAddress = vm.envAddress("WORLD_ADDRESS");
    address userAddress2 = vm.envAddress("USER_ADDRESS_2");
 
    IWorld world = IWorld(worldAddress);
    vm.startBroadcast(userPrivateKey);
    world.unregisterDelegation(userAddress2);
    vm.stopBroadcast();
  }
}

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.