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 System
s 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.
CallboundDelegationControl
(opens in a new tab)SystemboundDelegationControl
(opens in a new tab)TimeboundDelegationControl
(opens in a new tab)
Alternatively, you can write your own DelegationControl
contract with custom logic.
// 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 World
s.
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 beaddress(0)
to let this delegation apply to all callers. Note that combiningaddress(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 theResourceId
for aSystem
that decides whether an attempt to do something by the delegatee on behalf of the delegator is authorized or not. Alternatively, this value can beUNLIMITED_DELEGATION
(opens in a new tab), in which case the delegatee has unlimited authority.We have an example of a delegation control
System
in thestd-delegations
module (opens in a new tab). -
initCallData
, call data for a function that is called on thedelegationControlId
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 ofabi.encodeCall
).
// 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
, theSystem
to call.callData
, the call data (opens in a new tab) to send theSystem
, which includes both the function selector and arguments (the result ofabi.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.
// 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.
// 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:
- 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. - 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. - 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.