Skip to content

Commit

Permalink
feat(world-modules): add puppet module (#1793)
Browse files Browse the repository at this point in the history
Co-authored-by: dk1a <dk1a@protonmail.com>
  • Loading branch information
alvrs and dk1a committed Nov 1, 2023
1 parent 52182f7 commit 35348f8
Show file tree
Hide file tree
Showing 15 changed files with 671 additions and 2 deletions.
22 changes: 22 additions & 0 deletions .changeset/happy-pants-try.md
@@ -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";
// Install the puppet module
world.installModule(new PuppetModule(), new bytes(0));
// 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>));
```
35 changes: 33 additions & 2 deletions packages/world-modules/mud.config.ts
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
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
@@ -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
@@ -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 {
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,
bytes memory eventData
) public onlyPuppetMaster {
assembly {
log4(add(eventData, 0x20), mload(eventData), eventSignature, topic1, topic2, topic3)
}
}
}
@@ -0,0 +1,17 @@
// 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 { 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;
}
}
25 changes: 25 additions & 0 deletions packages/world-modules/src/modules/puppet/PuppetFactorySystem.sol
@@ -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);
}
}
19 changes: 19 additions & 0 deletions packages/world-modules/src/modules/puppet/PuppetMaster.sol
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;

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) {
ResourceId systemId = SystemRegistry.getSystemId(address(this));
address puppetAddress = PuppetRegistry.get(PUPPET_TABLE_ID, systemId);
if (puppetAddress == address(0)) revert PuppetMaster_NoPuppet(address(this), systemId);
return Puppet(puppetAddress);
}
}
49 changes: 49 additions & 0 deletions packages/world-modules/src/modules/puppet/PuppetModule.sol
@@ -0,0 +1,49 @@
// 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 { 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
@@ -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")))
);
24 changes: 24 additions & 0 deletions packages/world-modules/src/modules/puppet/createPuppet.sol
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;

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))),
(address)
);
world.registerNamespaceDelegation(systemId.getNamespaceId(), PUPPET_DELEGATION, new bytes(0));
}

0 comments on commit 35348f8

Please sign in to comment.