Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(world-modules): add puppet module #1793

Merged
merged 13 commits into from
Nov 1, 2023
22 changes: 22 additions & 0 deletions .changeset/happy-pants-try.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
"@latticexyz/world-modules": minor
---

Added the `PuppetModule` to `@latticexyz/world-modules`. The puppet pattern allows an external contract to be registered as an external interface for a MUD system.
This allows standards like `ERC20` (that require a specific interface and events to be emitted by a unique contract) to be implemented inside a MUD World.

The puppet serves as a proxy, forwarding all calls to the implementation system (also called the "puppet master").
The "puppet master" system can emit events from the puppet contract.

```solidity
import { PuppetModule } from "@latticexyz/world-modules/src/modules/puppet/PuppetModule.sol";
import { createPuppet } from "@latticexyz/world-modules/src/modules/puppet/createPuppet.sol";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although the puppet terminology is fun, I wonder if we should just call this a proxy 🙈

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought about that too but I'm afraid proxy has too many preconceptions around the proxy being the authority (ie proxy holds the state, proxy switches its implementation, etc), whereas here the "proxy" is just the "puppet" of the World

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true! proxy is quite overloaded

alvrs marked this conversation as resolved.
Show resolved Hide resolved

// Install the puppet module
world.installModule(new PuppetModule(), new bytes(0));

alvrs marked this conversation as resolved.
Show resolved Hide resolved
// Register a new puppet for any system
// The system must implement the `CustomInterface`,
// and the caller must own the system's namespace
CustomInterface puppet = CustomInterface(createPuppet(world, <systemId>));
alvrs marked this conversation as resolved.
Show resolved Hide resolved
```
35 changes: 33 additions & 2 deletions packages/world-modules/mud.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default mudConfig({
tables: {
/************************************************************************
*
* MODULE TABLES
* KEYS WITH VALUE MODULE
*
************************************************************************/
KeysWithValue: {
Expand All @@ -24,6 +24,11 @@ export default mudConfig({
tableIdArgument: true,
storeArgument: true,
},
/************************************************************************
*
* KEYS IN TABLE MODULE
*
************************************************************************/
KeysInTable: {
directory: "modules/keysintable/tables",
keySchema: { sourceTableId: "ResourceId" },
Expand All @@ -46,13 +51,23 @@ export default mudConfig({
dataStruct: false,
storeArgument: true,
},
/************************************************************************
*
* UNIQUE ENTITY MODULE
*
************************************************************************/
UniqueEntity: {
directory: "modules/uniqueentity/tables",
keySchema: {},
valueSchema: "uint256",
tableIdArgument: true,
storeArgument: true,
},
/************************************************************************
*
* STD DELEGATIONS MODULE
*
************************************************************************/
CallboundDelegations: {
directory: "modules/std-delegations/tables",
keySchema: {
Expand All @@ -75,6 +90,22 @@ export default mudConfig({
maxTimestamp: "uint256",
},
},
/************************************************************************
*
* PUPPET MODULE
*
************************************************************************/
PuppetRegistry: {
directory: "modules/puppet/tables",
keySchema: {
systemId: "ResourceId",
},
valueSchema: {
puppet: "address",
},
tableIdArgument: true,
},
},
excludeSystems: ["UniqueEntitySystem"],

excludeSystems: ["UniqueEntitySystem", "PuppetFactorySystem"],
});
1 change: 1 addition & 0 deletions packages/world-modules/src/index.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ import { UsedKeysIndex, UsedKeysIndexTableId } from "./modules/keysintable/table
import { UniqueEntity } from "./modules/uniqueentity/tables/UniqueEntity.sol";
import { CallboundDelegations, CallboundDelegationsTableId } from "./modules/std-delegations/tables/CallboundDelegations.sol";
import { TimeboundDelegations, TimeboundDelegationsTableId } from "./modules/std-delegations/tables/TimeboundDelegations.sol";
import { PuppetRegistry } from "./modules/puppet/tables/PuppetRegistry.sol";
14 changes: 14 additions & 0 deletions packages/world-modules/src/interfaces/IPuppetFactorySystem.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;

/* Autogenerated file. Do not edit manually. */

import { ResourceId } from "@latticexyz/store/src/ResourceId.sol";

/**
* @title IPuppetFactorySystem
* @dev This interface is automatically generated from the corresponding system contract. Do not edit manually.
*/
interface IPuppetFactorySystem {
function createPuppet(ResourceId systemId) external returns (address puppet);
}
80 changes: 80 additions & 0 deletions packages/world-modules/src/modules/puppet/Puppet.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;

import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol";
import { ResourceId } from "@latticexyz/store/src/ResourceId.sol";
import { IBaseWorld } from "@latticexyz/world/src/codegen/interfaces/IBaseWorld.sol";
import { Systems } from "@latticexyz/world/src/codegen/tables/Systems.sol";

contract Puppet {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pretty great to see how lean this is!

error Puppet_AccessDenied(address caller);

IBaseWorld public immutable world;
ResourceId public immutable systemId;

constructor(IBaseWorld _world, ResourceId _systemId) {
world = _world;
systemId = _systemId;
StoreSwitch.setStoreAddress(address(_world));
}

modifier onlyPuppetMaster() {
(address systemAddress, ) = Systems.get(systemId);
if (msg.sender != systemAddress) {
revert Puppet_AccessDenied(msg.sender);
}
_;
}

fallback() external {
// Forward all calls to the system in the world
bytes memory returnData = world.callFrom(msg.sender, systemId, msg.data);

// If the call was successful, return the return data
assembly {
return(add(returnData, 0x20), mload(returnData))
}
}

/**
* @dev Log an event with a signature and no additional topic
*/
function log(bytes32 eventSignature, bytes memory eventData) public onlyPuppetMaster {
assembly {
log1(add(eventData, 0x20), mload(eventData), eventSignature)
}
}

/**
* @dev Log an event with a signature and one additional topics
*/
function log(bytes32 eventSignature, bytes32 topic1, bytes memory eventData) public onlyPuppetMaster {
assembly {
log2(add(eventData, 0x20), mload(eventData), eventSignature, topic1)
}
}

/**
* @dev Log an event with a signature and two additional topics
*/
function log(bytes32 eventSignature, bytes32 topic1, bytes32 topic2, bytes memory eventData) public onlyPuppetMaster {
assembly {
log3(add(eventData, 0x20), mload(eventData), eventSignature, topic1, topic2)
}
}

/**
* @dev Log an event with a signature and three additional topics
*/
function log(
bytes32 eventSignature,
bytes32 topic1,
bytes32 topic2,
bytes32 topic3,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these essentially the indexed args?

Copy link
Member Author

@alvrs alvrs Oct 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes exactly

bytes memory eventData
) public onlyPuppetMaster {
assembly {
log4(add(eventData, 0x20), mload(eventData), eventSignature, topic1, topic2, topic3)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;

import { DelegationControl } from "@latticexyz/world/src/DelegationControl.sol";
import { ResourceId } from "@latticexyz/world/src/WorldResourceId.sol";
import { AccessControlLib } from "../../utils/AccessControlLib.sol";
alvrs marked this conversation as resolved.
Show resolved Hide resolved
import { PuppetRegistry } from "./tables/PuppetRegistry.sol";
import { PUPPET_TABLE_ID } from "./constants.sol";

contract PuppetDelegationControl is DelegationControl {
/**
* Verify a delegation by checking if the resourceId maps to the caller as puppet
*/
function verify(address, ResourceId systemId, bytes memory) public view returns (bool) {
address puppet = _msgSender();
return PuppetRegistry.get(PUPPET_TABLE_ID, systemId) == puppet;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;

import { ResourceId } from "@latticexyz/store/src/ResourceId.sol";
import { System } from "@latticexyz/world/src/System.sol";
import { IBaseWorld } from "@latticexyz/world/src/codegen/interfaces/IBaseWorld.sol";

import { AccessControlLib } from "../../utils/AccessControlLib.sol";

import { PuppetRegistry } from "./tables/PuppetRegistry.sol";
import { Puppet } from "./Puppet.sol";
import { PUPPET_TABLE_ID } from "./constants.sol";

contract PuppetFactorySystem is System {
function createPuppet(ResourceId systemId) public returns (address puppet) {
// Only the owner of a system can create a puppet for it
AccessControlLib.requireOwner(systemId, _msgSender());

// Deploy a new puppet contract
puppet = address(new Puppet(IBaseWorld(_world()), systemId));

// Register the puppet
PuppetRegistry.set(PUPPET_TABLE_ID, systemId, puppet);
}
}
18 changes: 18 additions & 0 deletions packages/world-modules/src/modules/puppet/PuppetMaster.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;
alvrs marked this conversation as resolved.
Show resolved Hide resolved
import { ResourceId } from "@latticexyz/store/src/ResourceId.sol";
import { SystemRegistry } from "@latticexyz/world/src/codegen/tables/SystemRegistry.sol";
import { PuppetRegistry } from "./tables/PuppetRegistry.sol";
import { PUPPET_TABLE_ID } from "./constants.sol";
import { Puppet } from "./Puppet.sol";

contract PuppetMaster {
error PuppetMaster_NoPuppet(address systemAddress, ResourceId systemId);

function puppet() internal view returns (Puppet) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this mean there's only a 1:1 mapping between a system and a puppet?

if I want multiple tokens (e.g. ERC721) on my world, how would I go about that?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this mean there's only a 1:1 mapping between a system and a puppet

For now yes. Agree 1:N would be nicer but makes it much more complex, so I'd think about that later (either as a change to the puppet module, or with a new module)

if I want multiple tokens (e.g. ERC721) on my world, how would I go about that?

You'll have to register different systems (see #1844). Thought a lot about this too, but I feel like this approach is the simplest (one namespace per token, with a separate system and tables in that namespace). Otherwise everything gets more complicated (if it's just one system you'd need to pass a param to the system, which means the interface doesn't match anymore with the puppet, access management becomes more complex, etc)

ResourceId systemId = SystemRegistry.getSystemId(address(this));
address puppetAddress = PuppetRegistry.get(PUPPET_TABLE_ID, systemId);
if (puppetAddress == address(0)) revert PuppetMaster_NoPuppet(address(this), systemId);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does address(this) still work in the context of a delegatecall?

Copy link
Member Author

@alvrs alvrs Nov 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There could only be a single puppet for root systems (because they all have the world address). There would also have to be a namespace delegation in the root namespace, which feels like a bad idea. So all in all i'd say you shouldn't use puppets with root systems.

return Puppet(puppetAddress);
}
}
50 changes: 50 additions & 0 deletions packages/world-modules/src/modules/puppet/PuppetModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;

import { IBaseWorld } from "@latticexyz/world/src/codegen/interfaces/IBaseWorld.sol";

import { Module } from "@latticexyz/world/src/Module.sol";
import { WorldContextConsumer } from "@latticexyz/world/src/WorldContext.sol";
alvrs marked this conversation as resolved.
Show resolved Hide resolved
import { revertWithBytes } from "@latticexyz/world/src/revertWithBytes.sol";

import { PuppetFactorySystem } from "./PuppetFactorySystem.sol";
import { PuppetDelegationControl } from "./PuppetDelegationControl.sol";
import { MODULE_NAME, PUPPET_DELEGATION, PUPPET_FACTORY, PUPPET_TABLE_ID } from "./constants.sol";

import { PuppetRegistry } from "./tables/PuppetRegistry.sol";

/**
* This module registers tables and delegation control systems required for puppet delegations
*/
contract PuppetModule is Module {
PuppetDelegationControl private immutable puppetDelegationControl = new PuppetDelegationControl();
PuppetFactorySystem private immutable puppetFactorySystem = new PuppetFactorySystem();

function getName() public pure returns (bytes16) {
return MODULE_NAME;
}

function installRoot(bytes memory) public {
IBaseWorld world = IBaseWorld(_world());

// Register table
PuppetRegistry.register(PUPPET_TABLE_ID);

// Register system
(bool success, bytes memory returnData) = address(world).delegatecall(
abi.encodeCall(world.registerSystem, (PUPPET_DELEGATION, puppetDelegationControl, true))
);
if (!success) revertWithBytes(returnData);
}

function install(bytes memory) public {
IBaseWorld world = IBaseWorld(_world());

// Register table
PuppetRegistry.register(PUPPET_TABLE_ID);

// Register puppet factory and delegation control
world.registerSystem(PUPPET_FACTORY, puppetFactorySystem, true);
world.registerSystem(PUPPET_DELEGATION, puppetDelegationControl, true);
}
}
22 changes: 22 additions & 0 deletions packages/world-modules/src/modules/puppet/constants.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;

import { ResourceId } from "@latticexyz/store/src/ResourceId.sol";
import { RESOURCE_TABLE } from "@latticexyz/store/src/storeResourceTypes.sol";
import { RESOURCE_SYSTEM } from "@latticexyz/world/src/worldResourceTypes.sol";
import { ROOT_NAMESPACE } from "@latticexyz/world/src/constants.sol";

bytes16 constant MODULE_NAME = bytes16("puppet");
bytes14 constant NAMESPACE = bytes14("puppet");

ResourceId constant PUPPET_DELEGATION = ResourceId.wrap(
bytes32(abi.encodePacked(RESOURCE_SYSTEM, NAMESPACE, bytes16("Delegation")))
);

ResourceId constant PUPPET_FACTORY = ResourceId.wrap(
bytes32(abi.encodePacked(RESOURCE_SYSTEM, NAMESPACE, bytes16("Factory")))
);

ResourceId constant PUPPET_TABLE_ID = ResourceId.wrap(
bytes32(abi.encodePacked(RESOURCE_TABLE, NAMESPACE, bytes16("PuppetRegistry")))
);
23 changes: 23 additions & 0 deletions packages/world-modules/src/modules/puppet/createPuppet.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;
alvrs marked this conversation as resolved.
Show resolved Hide resolved
import { ResourceId } from "@latticexyz/store/src/ResourceId.sol";
import { IBaseWorld } from "@latticexyz/world/src/codegen/interfaces/IBaseWorld.sol";
import { WorldResourceIdInstance } from "@latticexyz/world/src/WorldResourceId.sol";
import { PUPPET_DELEGATION, PUPPET_FACTORY } from "./constants.sol";
import { PuppetDelegationControl } from "./PuppetDelegationControl.sol";
import { Puppet } from "./Puppet.sol";
import { PuppetFactorySystem } from "./PuppetFactorySystem.sol";

using WorldResourceIdInstance for ResourceId;

/**
* This free function can be used to create a puppet and register it with the puppet delegation control.
* Since it is inlined in the caller's context, the calls originate from the caller's address.
*/
function createPuppet(IBaseWorld world, ResourceId systemId) returns (address puppet) {
puppet = abi.decode(
world.call(PUPPET_FACTORY, abi.encodeCall(PuppetFactorySystem.createPuppet, (systemId))),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ooc why don't we use SystemSwitch here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree should probably use system switch here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update: turns out using system switch here pushes ERC20Module over the bytecode limit, so reverting for now (meaning you can't register a puppet from a root system for now). We should find a better implementation for SystemSwitch that doesn't require inlining the libraries, maybe by deploying StoreCore as a public library.

Copy link
Member

@holic holic Nov 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should prob add a follow up issue to come back to 1) this specific instance and 2) the problem overall

StoreCore as a public lib seems bad for gas because it'd be a delegatecall to an external contract right? And iirc foundry doesn't have good support for linked libs

or do you mean public as in not-internal?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah i mean public as in not-internal, which means delegatecall to an external contract

(address)
);
world.registerNamespaceDelegation(systemId.getNamespaceId(), PUPPET_DELEGATION, new bytes(0));
}
Loading
Loading