-
Notifications
You must be signed in to change notification settings - Fork 196
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
Changes from all commits
f146cfe
803b83a
a6bdbbd
2ecfa3d
3124be1
276b1f0
5afee8b
2bd978b
d6cc57e
6efde97
d46722a
235d571
4af069d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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"; | ||
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
|
||
``` |
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); | ||
} |
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. are these essentially the indexed args? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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,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; | ||
} | ||
} |
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); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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)
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
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"))) | ||
); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
// 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))), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ooc why don't we use SystemSwitch here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree should probably use system switch here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. update: turns out using system switch here pushes There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)); | ||
} |
There was a problem hiding this comment.
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 🙈
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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