From 35348f831b923aed6e9bdf8b38bf337f3e944a48 Mon Sep 17 00:00:00 2001 From: alvarius Date: Wed, 1 Nov 2023 15:08:25 +0100 Subject: [PATCH] feat(world-modules): add puppet module (#1793) Co-authored-by: dk1a --- .changeset/happy-pants-try.md | 22 ++ packages/world-modules/mud.config.ts | 35 ++- packages/world-modules/src/index.sol | 1 + .../src/interfaces/IPuppetFactorySystem.sol | 14 ++ .../src/modules/puppet/Puppet.sol | 80 ++++++ .../puppet/PuppetDelegationControl.sol | 17 ++ .../modules/puppet/PuppetFactorySystem.sol | 25 ++ .../src/modules/puppet/PuppetMaster.sol | 19 ++ .../src/modules/puppet/PuppetModule.sol | 49 ++++ .../src/modules/puppet/constants.sol | 22 ++ .../src/modules/puppet/createPuppet.sol | 24 ++ .../modules/puppet/tables/PuppetRegistry.sol | 229 ++++++++++++++++++ .../src/modules/puppet/utils.sol | 10 + .../src/utils/AccessControlLib.sol | 54 +++++ .../world-modules/test/PuppetModule.t.sol | 72 ++++++ 15 files changed, 671 insertions(+), 2 deletions(-) create mode 100644 .changeset/happy-pants-try.md create mode 100644 packages/world-modules/src/interfaces/IPuppetFactorySystem.sol create mode 100644 packages/world-modules/src/modules/puppet/Puppet.sol create mode 100644 packages/world-modules/src/modules/puppet/PuppetDelegationControl.sol create mode 100644 packages/world-modules/src/modules/puppet/PuppetFactorySystem.sol create mode 100644 packages/world-modules/src/modules/puppet/PuppetMaster.sol create mode 100644 packages/world-modules/src/modules/puppet/PuppetModule.sol create mode 100644 packages/world-modules/src/modules/puppet/constants.sol create mode 100644 packages/world-modules/src/modules/puppet/createPuppet.sol create mode 100644 packages/world-modules/src/modules/puppet/tables/PuppetRegistry.sol create mode 100644 packages/world-modules/src/modules/puppet/utils.sol create mode 100644 packages/world-modules/src/utils/AccessControlLib.sol create mode 100644 packages/world-modules/test/PuppetModule.t.sol diff --git a/.changeset/happy-pants-try.md b/.changeset/happy-pants-try.md new file mode 100644 index 0000000000..3f2a70fc4d --- /dev/null +++ b/.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, )); +``` diff --git a/packages/world-modules/mud.config.ts b/packages/world-modules/mud.config.ts index bf8e4bc6e7..9701c93ab2 100644 --- a/packages/world-modules/mud.config.ts +++ b/packages/world-modules/mud.config.ts @@ -10,7 +10,7 @@ export default mudConfig({ tables: { /************************************************************************ * - * MODULE TABLES + * KEYS WITH VALUE MODULE * ************************************************************************/ KeysWithValue: { @@ -24,6 +24,11 @@ export default mudConfig({ tableIdArgument: true, storeArgument: true, }, + /************************************************************************ + * + * KEYS IN TABLE MODULE + * + ************************************************************************/ KeysInTable: { directory: "modules/keysintable/tables", keySchema: { sourceTableId: "ResourceId" }, @@ -46,6 +51,11 @@ export default mudConfig({ dataStruct: false, storeArgument: true, }, + /************************************************************************ + * + * UNIQUE ENTITY MODULE + * + ************************************************************************/ UniqueEntity: { directory: "modules/uniqueentity/tables", keySchema: {}, @@ -53,6 +63,11 @@ export default mudConfig({ tableIdArgument: true, storeArgument: true, }, + /************************************************************************ + * + * STD DELEGATIONS MODULE + * + ************************************************************************/ CallboundDelegations: { directory: "modules/std-delegations/tables", keySchema: { @@ -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"], }); diff --git a/packages/world-modules/src/index.sol b/packages/world-modules/src/index.sol index e88b495bf3..64a418403f 100644 --- a/packages/world-modules/src/index.sol +++ b/packages/world-modules/src/index.sol @@ -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"; diff --git a/packages/world-modules/src/interfaces/IPuppetFactorySystem.sol b/packages/world-modules/src/interfaces/IPuppetFactorySystem.sol new file mode 100644 index 0000000000..6166baa98b --- /dev/null +++ b/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); +} diff --git a/packages/world-modules/src/modules/puppet/Puppet.sol b/packages/world-modules/src/modules/puppet/Puppet.sol new file mode 100644 index 0000000000..0b688b2ca0 --- /dev/null +++ b/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) + } + } +} diff --git a/packages/world-modules/src/modules/puppet/PuppetDelegationControl.sol b/packages/world-modules/src/modules/puppet/PuppetDelegationControl.sol new file mode 100644 index 0000000000..c60d5afa6d --- /dev/null +++ b/packages/world-modules/src/modules/puppet/PuppetDelegationControl.sol @@ -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; + } +} diff --git a/packages/world-modules/src/modules/puppet/PuppetFactorySystem.sol b/packages/world-modules/src/modules/puppet/PuppetFactorySystem.sol new file mode 100644 index 0000000000..70ead9e57b --- /dev/null +++ b/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); + } +} diff --git a/packages/world-modules/src/modules/puppet/PuppetMaster.sol b/packages/world-modules/src/modules/puppet/PuppetMaster.sol new file mode 100644 index 0000000000..45e587bb0a --- /dev/null +++ b/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); + } +} diff --git a/packages/world-modules/src/modules/puppet/PuppetModule.sol b/packages/world-modules/src/modules/puppet/PuppetModule.sol new file mode 100644 index 0000000000..ecf4fcfad4 --- /dev/null +++ b/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); + } +} diff --git a/packages/world-modules/src/modules/puppet/constants.sol b/packages/world-modules/src/modules/puppet/constants.sol new file mode 100644 index 0000000000..c441dd03b9 --- /dev/null +++ b/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"))) +); diff --git a/packages/world-modules/src/modules/puppet/createPuppet.sol b/packages/world-modules/src/modules/puppet/createPuppet.sol new file mode 100644 index 0000000000..b00641a5b8 --- /dev/null +++ b/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)); +} diff --git a/packages/world-modules/src/modules/puppet/tables/PuppetRegistry.sol b/packages/world-modules/src/modules/puppet/tables/PuppetRegistry.sol new file mode 100644 index 0000000000..054b093cbd --- /dev/null +++ b/packages/world-modules/src/modules/puppet/tables/PuppetRegistry.sol @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.21; + +/* Autogenerated file. Do not edit manually. */ + +// Import schema type +import { SchemaType } from "@latticexyz/schema-type/src/solidity/SchemaType.sol"; + +// Import store internals +import { IStore } from "@latticexyz/store/src/IStore.sol"; +import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; +import { StoreCore } from "@latticexyz/store/src/StoreCore.sol"; +import { Bytes } from "@latticexyz/store/src/Bytes.sol"; +import { Memory } from "@latticexyz/store/src/Memory.sol"; +import { SliceLib } from "@latticexyz/store/src/Slice.sol"; +import { EncodeArray } from "@latticexyz/store/src/tightcoder/EncodeArray.sol"; +import { FieldLayout, FieldLayoutLib } from "@latticexyz/store/src/FieldLayout.sol"; +import { Schema, SchemaLib } from "@latticexyz/store/src/Schema.sol"; +import { PackedCounter, PackedCounterLib } from "@latticexyz/store/src/PackedCounter.sol"; +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; +import { RESOURCE_TABLE, RESOURCE_OFFCHAIN_TABLE } from "@latticexyz/store/src/storeResourceTypes.sol"; + +// Import user types +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; + +FieldLayout constant _fieldLayout = FieldLayout.wrap( + 0x0014010014000000000000000000000000000000000000000000000000000000 +); + +library PuppetRegistry { + /** + * @notice Get the table values' field layout. + * @return _fieldLayout The field layout for the table. + */ + function getFieldLayout() internal pure returns (FieldLayout) { + return _fieldLayout; + } + + /** + * @notice Get the table's key schema. + * @return _keySchema The key schema for the table. + */ + function getKeySchema() internal pure returns (Schema) { + SchemaType[] memory _keySchema = new SchemaType[](1); + _keySchema[0] = SchemaType.BYTES32; + + return SchemaLib.encode(_keySchema); + } + + /** + * @notice Get the table's value schema. + * @return _valueSchema The value schema for the table. + */ + function getValueSchema() internal pure returns (Schema) { + SchemaType[] memory _valueSchema = new SchemaType[](1); + _valueSchema[0] = SchemaType.ADDRESS; + + return SchemaLib.encode(_valueSchema); + } + + /** + * @notice Get the table's key field names. + * @return keyNames An array of strings with the names of key fields. + */ + function getKeyNames() internal pure returns (string[] memory keyNames) { + keyNames = new string[](1); + keyNames[0] = "systemId"; + } + + /** + * @notice Get the table's value field names. + * @return fieldNames An array of strings with the names of value fields. + */ + function getFieldNames() internal pure returns (string[] memory fieldNames) { + fieldNames = new string[](1); + fieldNames[0] = "puppet"; + } + + /** + * @notice Register the table with its config. + */ + function register(ResourceId _tableId) internal { + StoreSwitch.registerTable(_tableId, _fieldLayout, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames()); + } + + /** + * @notice Register the table with its config. + */ + function _register(ResourceId _tableId) internal { + StoreCore.registerTable(_tableId, _fieldLayout, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames()); + } + + /** + * @notice Get puppet. + */ + function getPuppet(ResourceId _tableId, ResourceId systemId) internal view returns (address puppet) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(systemId); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (address(bytes20(_blob))); + } + + /** + * @notice Get puppet. + */ + function _getPuppet(ResourceId _tableId, ResourceId systemId) internal view returns (address puppet) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(systemId); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (address(bytes20(_blob))); + } + + /** + * @notice Get puppet. + */ + function get(ResourceId _tableId, ResourceId systemId) internal view returns (address puppet) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(systemId); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (address(bytes20(_blob))); + } + + /** + * @notice Get puppet. + */ + function _get(ResourceId _tableId, ResourceId systemId) internal view returns (address puppet) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(systemId); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (address(bytes20(_blob))); + } + + /** + * @notice Set puppet. + */ + function setPuppet(ResourceId _tableId, ResourceId systemId, address puppet) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(systemId); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((puppet)), _fieldLayout); + } + + /** + * @notice Set puppet. + */ + function _setPuppet(ResourceId _tableId, ResourceId systemId, address puppet) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(systemId); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((puppet)), _fieldLayout); + } + + /** + * @notice Set puppet. + */ + function set(ResourceId _tableId, ResourceId systemId, address puppet) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(systemId); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((puppet)), _fieldLayout); + } + + /** + * @notice Set puppet. + */ + function _set(ResourceId _tableId, ResourceId systemId, address puppet) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(systemId); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((puppet)), _fieldLayout); + } + + /** + * @notice Delete all data for given keys. + */ + function deleteRecord(ResourceId _tableId, ResourceId systemId) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(systemId); + + StoreSwitch.deleteRecord(_tableId, _keyTuple); + } + + /** + * @notice Delete all data for given keys. + */ + function _deleteRecord(ResourceId _tableId, ResourceId systemId) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(systemId); + + StoreCore.deleteRecord(_tableId, _keyTuple, _fieldLayout); + } + + /** + * @notice Tightly pack static (fixed length) data using this table's schema. + * @return The static data, encoded into a sequence of bytes. + */ + function encodeStatic(address puppet) internal pure returns (bytes memory) { + return abi.encodePacked(puppet); + } + + /** + * @notice Encode all of a record's fields. + * @return The static (fixed length) data, encoded into a sequence of bytes. + * @return The lengths of the dynamic fields (packed into a single bytes32 value). + * @return The dyanmic (variable length) data, encoded into a sequence of bytes. + */ + function encode(address puppet) internal pure returns (bytes memory, PackedCounter, bytes memory) { + bytes memory _staticData = encodeStatic(puppet); + + PackedCounter _encodedLengths; + bytes memory _dynamicData; + + return (_staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Encode keys as a bytes32 array using this table's field layout. + */ + function encodeKeyTuple(ResourceId systemId) internal pure returns (bytes32[] memory) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(systemId); + + return _keyTuple; + } +} diff --git a/packages/world-modules/src/modules/puppet/utils.sol b/packages/world-modules/src/modules/puppet/utils.sol new file mode 100644 index 0000000000..03028b4a99 --- /dev/null +++ b/packages/world-modules/src/modules/puppet/utils.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.21; + +function toTopic(address value) pure returns (bytes32) { + return bytes32(uint256(uint160(value))); +} + +function toTopic(uint256 value) pure returns (bytes32) { + return bytes32(value); +} diff --git a/packages/world-modules/src/utils/AccessControlLib.sol b/packages/world-modules/src/utils/AccessControlLib.sol new file mode 100644 index 0000000000..462d683d67 --- /dev/null +++ b/packages/world-modules/src/utils/AccessControlLib.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.21; + +import { ResourceId, WorldResourceIdInstance } from "@latticexyz/world/src/WorldResourceId.sol"; +import { IWorldErrors } from "@latticexyz/world/src/IWorldErrors.sol"; +import { ResourceAccess } from "@latticexyz/world/src/codegen/tables/ResourceAccess.sol"; +import { NamespaceOwner } from "@latticexyz/world/src/codegen/tables/NamespaceOwner.sol"; + +/** + * @title AccessControlLib + * @dev Provides access control functions for checking permissions and ownership within a namespace. + * This library is functionally equivalent with the AccessControl library from world, + * but uses StoreSwitch instead of always reading from own storage. + */ +library AccessControlLib { + using WorldResourceIdInstance for ResourceId; + + /** + * @notice Checks if the caller has access to the given resource ID or its namespace. + * @param resourceId The resource ID to check access for. + * @param caller The address of the caller. + * @return true if the caller has access, false otherwise. + */ + function hasAccess(ResourceId resourceId, address caller) internal view returns (bool) { + return + // First check access based on the namespace. If caller has no namespace access, check access on the resource. + ResourceAccess.get(resourceId.getNamespaceId(), caller) || ResourceAccess.get(resourceId, caller); + } + + /** + * @notice Check for access at the given namespace or resource. + * @param resourceId The resource ID to check access for. + * @param caller The address of the caller. + * @dev Reverts with IWorldErrors.World_AccessDenied if access is denied. + */ + function requireAccess(ResourceId resourceId, address caller) internal view { + // Check if the given caller has access to the given namespace or name + if (!hasAccess(resourceId, caller)) { + revert IWorldErrors.World_AccessDenied(resourceId.toString(), caller); + } + } + + /** + * @notice Check for ownership of the namespace of the given resource ID. + * @dev Reverts with IWorldErrors.World_AccessDenied if caller is not owner of the namespace of the resource. + * @param resourceId The resource ID to check ownership for. + * @param caller The address of the caller. + */ + function requireOwner(ResourceId resourceId, address caller) internal view { + if (NamespaceOwner.get(resourceId.getNamespaceId()) != caller) { + revert IWorldErrors.World_AccessDenied(resourceId.toString(), caller); + } + } +} diff --git a/packages/world-modules/test/PuppetModule.t.sol b/packages/world-modules/test/PuppetModule.t.sol new file mode 100644 index 0000000000..7e29308620 --- /dev/null +++ b/packages/world-modules/test/PuppetModule.t.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.21; + +import { Test } from "forge-std/Test.sol"; +import { GasReporter } from "@latticexyz/gas-report/src/GasReporter.sol"; + +import { World } from "@latticexyz/world/src/World.sol"; +import { ResourceId, WorldResourceIdLib, WorldResourceIdInstance } from "@latticexyz/world/src/WorldResourceId.sol"; +import { System } from "@latticexyz/world/src/System.sol"; +import { RESOURCE_SYSTEM } from "@latticexyz/world/src/worldResourceTypes.sol"; + +import { IBaseWorld } from "@latticexyz/world/src/codegen/interfaces/IBaseWorld.sol"; +import { IWorldErrors } from "@latticexyz/world/src/IWorldErrors.sol"; +import { DELEGATION_CONTROL_INTERFACE_ID } from "@latticexyz/world/src/IDelegationControl.sol"; + +import { CoreModule } from "@latticexyz/world/src/modules/core/CoreModule.sol"; +import { Systems } from "@latticexyz/world/src/codegen/tables/Systems.sol"; + +import { PuppetModule } from "../src/modules/puppet/PuppetModule.sol"; +import { PuppetDelegationControl } from "../src/modules/puppet/PuppetDelegationControl.sol"; +import { Puppet } from "../src/modules/puppet/Puppet.sol"; +import { PuppetMaster } from "../src/modules/puppet/PuppetMaster.sol"; +import { PUPPET_DELEGATION } from "../src/modules/puppet/constants.sol"; +import { createPuppet } from "../src/modules/puppet/createPuppet.sol"; + +contract PuppetTestSystem is System, PuppetMaster { + event Hello(string message); + + function echoAndEmit(string memory message) public returns (string memory) { + puppet().log(Hello.selector, abi.encode(message)); + return message; + } + + function msgSender() public view returns (address) { + return _msgSender(); + } +} + +contract PuppetModuleTest is Test, GasReporter { + using WorldResourceIdInstance for ResourceId; + + event Hello(string msg); + + IBaseWorld private world; + ResourceId private systemId = + WorldResourceIdLib.encode({ typeId: RESOURCE_SYSTEM, namespace: "namespace", name: "testSystem" }); + PuppetTestSystem private puppet; + + function setUp() public { + world = IBaseWorld(address(new World())); + world.initialize(new CoreModule()); + world.installModule(new PuppetModule(), new bytes(0)); + + // Register a new system + PuppetTestSystem system = new PuppetTestSystem(); + world.registerSystem(systemId, system, true); + + // Connect the puppet + puppet = PuppetTestSystem(createPuppet(world, systemId)); + } + + function testEmitOnPuppet() public { + vm.expectEmit(true, true, true, true); + emit Hello("hello world"); + string memory result = puppet.echoAndEmit("hello world"); + assertEq(result, "hello world"); + } + + function testMsgSender() public { + assertEq(puppet.msgSender(), address(this)); + } +}