From 1f80a0b52a5c2d051e3697d6e60aad7364b0a925 Mon Sep 17 00:00:00 2001 From: alvarius Date: Mon, 25 Sep 2023 00:33:05 +0100 Subject: [PATCH] feat(world): add `registerNamespaceDelegation` for namespace-bound fallback delegation controls (#1590) --- .changeset/khaki-houses-whisper.md | 16 ++ packages/world/gas-report.json | 46 +-- packages/world/mud.config.ts | 10 +- packages/world/src/World.sol | 22 +- packages/world/src/index.sol | 3 +- .../world/src/interfaces/IWorldErrors.sol | 1 + .../interfaces/IWorldRegistrationSystem.sol | 6 + .../world/src/modules/core/CoreModule.sol | 11 +- .../WorldRegistrationSystem.sol | 52 +++- .../src/tables/NamespaceDelegationControl.sol | 271 ++++++++++++++++++ ...egations.sol => UserDelegationControl.sol} | 6 +- packages/world/test/DelegationControlMock.sol | 19 ++ packages/world/test/World.t.sol | 73 ++++- 13 files changed, 495 insertions(+), 41 deletions(-) create mode 100644 .changeset/khaki-houses-whisper.md create mode 100644 packages/world/src/tables/NamespaceDelegationControl.sol rename packages/world/src/tables/{Delegations.sol => UserDelegationControl.sol} (98%) create mode 100644 packages/world/test/DelegationControlMock.sol diff --git a/.changeset/khaki-houses-whisper.md b/.changeset/khaki-houses-whisper.md new file mode 100644 index 0000000000..ff2aa39534 --- /dev/null +++ b/.changeset/khaki-houses-whisper.md @@ -0,0 +1,16 @@ +--- +"@latticexyz/world": minor +--- + +It is now possible for namespace owners to register a fallback delegation control system for the namespace. +This fallback delegation control system is used to verify a delegation in `IBaseWorld.callFrom`, after the user's individual and fallback delegations have been checked. + +```solidity +IBaseWorld { + function registerNamespaceDelegation( + ResourceId namespaceId, + ResourceId delegationControlId, + bytes memory initCallData + ) external; +} +``` diff --git a/packages/world/gas-report.json b/packages/world/gas-report.json index 8f64253a7e..6c69342159 100644 --- a/packages/world/gas-report.json +++ b/packages/world/gas-report.json @@ -51,13 +51,13 @@ "file": "test/KeysInTableModule.t.sol", "test": "testInstallComposite", "name": "install keys in table module", - "gasUsed": 1408340 + "gasUsed": 1408400 }, { "file": "test/KeysInTableModule.t.sol", "test": "testInstallGas", "name": "install keys in table module", - "gasUsed": 1408340 + "gasUsed": 1408400 }, { "file": "test/KeysInTableModule.t.sol", @@ -69,13 +69,13 @@ "file": "test/KeysInTableModule.t.sol", "test": "testInstallSingleton", "name": "install keys in table module", - "gasUsed": 1408340 + "gasUsed": 1408400 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookCompositeGas", "name": "install keys in table module", - "gasUsed": 1408340 + "gasUsed": 1408400 }, { "file": "test/KeysInTableModule.t.sol", @@ -93,7 +93,7 @@ "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookGas", "name": "install keys in table module", - "gasUsed": 1408340 + "gasUsed": 1408400 }, { "file": "test/KeysInTableModule.t.sol", @@ -111,7 +111,7 @@ "file": "test/KeysWithValueModule.t.sol", "test": "testGetKeysWithValueGas", "name": "install keys with value module", - "gasUsed": 649497 + "gasUsed": 649560 }, { "file": "test/KeysWithValueModule.t.sol", @@ -129,7 +129,7 @@ "file": "test/KeysWithValueModule.t.sol", "test": "testInstall", "name": "install keys with value module", - "gasUsed": 649497 + "gasUsed": 649560 }, { "file": "test/KeysWithValueModule.t.sol", @@ -141,7 +141,7 @@ "file": "test/KeysWithValueModule.t.sol", "test": "testSetAndDeleteRecordHook", "name": "install keys with value module", - "gasUsed": 649497 + "gasUsed": 649560 }, { "file": "test/KeysWithValueModule.t.sol", @@ -159,7 +159,7 @@ "file": "test/KeysWithValueModule.t.sol", "test": "testSetField", "name": "install keys with value module", - "gasUsed": 649497 + "gasUsed": 649560 }, { "file": "test/KeysWithValueModule.t.sol", @@ -243,7 +243,7 @@ "file": "test/StandardDelegationsModule.t.sol", "test": "testCallFromCallboundDelegation", "name": "register a callbound delegation", - "gasUsed": 112955 + "gasUsed": 117579 }, { "file": "test/StandardDelegationsModule.t.sol", @@ -255,7 +255,7 @@ "file": "test/StandardDelegationsModule.t.sol", "test": "testCallFromTimeboundDelegation", "name": "register a timebound delegation", - "gasUsed": 107450 + "gasUsed": 112073 }, { "file": "test/StandardDelegationsModule.t.sol", @@ -267,7 +267,7 @@ "file": "test/UniqueEntityModule.t.sol", "test": "testInstall", "name": "install unique entity module", - "gasUsed": 676006 + "gasUsed": 676208 }, { "file": "test/UniqueEntityModule.t.sol", @@ -279,7 +279,7 @@ "file": "test/UniqueEntityModule.t.sol", "test": "testInstallRoot", "name": "installRoot unique entity module", - "gasUsed": 643280 + "gasUsed": 643472 }, { "file": "test/UniqueEntityModule.t.sol", @@ -293,17 +293,23 @@ "name": "call a system via the World", "gasUsed": 12409 }, + { + "file": "test/World.t.sol", + "test": "testCallFromNamespaceDelegation", + "name": "call a system via a namespace fallback delegation", + "gasUsed": 26144 + }, { "file": "test/World.t.sol", "test": "testCallFromUnlimitedDelegation", "name": "register an unlimited delegation", - "gasUsed": 47676 + "gasUsed": 47695 }, { "file": "test/World.t.sol", "test": "testCallFromUnlimitedDelegation", "name": "call a system via an unlimited delegation", - "gasUsed": 12839 + "gasUsed": 12845 }, { "file": "test/World.t.sol", @@ -321,31 +327,31 @@ "file": "test/World.t.sol", "test": "testRegisterFunctionSelector", "name": "Register a function selector", - "gasUsed": 83271 + "gasUsed": 83287 }, { "file": "test/World.t.sol", "test": "testRegisterNamespace", "name": "Register a new namespace", - "gasUsed": 121037 + "gasUsed": 121072 }, { "file": "test/World.t.sol", "test": "testRegisterRootFunctionSelector", "name": "Register a root function selector", - "gasUsed": 80532 + "gasUsed": 80571 }, { "file": "test/World.t.sol", "test": "testRegisterSystem", "name": "register a system", - "gasUsed": 162835 + "gasUsed": 162954 }, { "file": "test/World.t.sol", "test": "testRegisterTable", "name": "Register a new table in the namespace", - "gasUsed": 637671 + "gasUsed": 637709 }, { "file": "test/World.t.sol", diff --git a/packages/world/mud.config.ts b/packages/world/mud.config.ts index b7468bff86..f6efdff18b 100644 --- a/packages/world/mud.config.ts +++ b/packages/world/mud.config.ts @@ -40,7 +40,7 @@ export default mudConfig({ moduleAddress: "address", }, }, - Delegations: { + UserDelegationControl: { keySchema: { delegator: "address", delegatee: "address", @@ -49,6 +49,14 @@ export default mudConfig({ delegationControlId: "ResourceId", }, }, + NamespaceDelegationControl: { + keySchema: { + namespaceId: "ResourceId", + }, + valueSchema: { + delegationControlId: "ResourceId", + }, + }, /************************************************************************ * * MODULE TABLES diff --git a/packages/world/src/World.sol b/packages/world/src/World.sol index d93d4357b6..5404b05886 100644 --- a/packages/world/src/World.sol +++ b/packages/world/src/World.sol @@ -23,7 +23,8 @@ import { requireInterface } from "./requireInterface.sol"; import { NamespaceOwner } from "./tables/NamespaceOwner.sol"; import { InstalledModules } from "./tables/InstalledModules.sol"; -import { Delegations } from "./tables/Delegations.sol"; +import { UserDelegationControl } from "./tables/UserDelegationControl.sol"; +import { NamespaceDelegationControl } from "./tables/NamespaceDelegationControl.sol"; import { IModule, MODULE_INTERFACE_ID } from "./interfaces/IModule.sol"; import { IWorldKernel } from "./interfaces/IWorldKernel.sol"; @@ -302,18 +303,25 @@ contract World is StoreRead, IStoreData, IWorldKernel { return SystemCall.callWithHooksOrRevert(msg.sender, systemId, callData, msg.value); } - // Check if there is an explicit authorization for this caller to perform actions on behalf of the delegator - ResourceId explicitDelegationId = Delegations._get({ delegator: delegator, delegatee: msg.sender }); + // Check if there is an individual authorization for this caller to perform actions on behalf of the delegator + ResourceId individualDelegationId = UserDelegationControl._get({ delegator: delegator, delegatee: msg.sender }); - if (Delegation.verify(explicitDelegationId, delegator, msg.sender, systemId, callData)) { + if (Delegation.verify(individualDelegationId, delegator, msg.sender, systemId, callData)) { // forward the call as `delegator` return SystemCall.callWithHooksOrRevert(delegator, systemId, callData, msg.value); } // Check if the delegator has a fallback delegation control set - ResourceId fallbackDelegationId = Delegations._get({ delegator: delegator, delegatee: address(0) }); - if (Delegation.verify(fallbackDelegationId, delegator, msg.sender, systemId, callData)) { - // forward the call with `from` as `msgSender` + ResourceId userFallbackDelegationId = UserDelegationControl._get({ delegator: delegator, delegatee: address(0) }); + if (Delegation.verify(userFallbackDelegationId, delegator, msg.sender, systemId, callData)) { + // forward the call as `delegator` + return SystemCall.callWithHooksOrRevert(delegator, systemId, callData, msg.value); + } + + // Check if the namespace has a fallback delegation control set + ResourceId namespaceFallbackDelegationId = NamespaceDelegationControl._get(systemId.getNamespaceId()); + if (Delegation.verify(namespaceFallbackDelegationId, delegator, msg.sender, systemId, callData)) { + // forward the call as `delegator` return SystemCall.callWithHooksOrRevert(delegator, systemId, callData, msg.value); } diff --git a/packages/world/src/index.sol b/packages/world/src/index.sol index d42101c41c..6dc7d28033 100644 --- a/packages/world/src/index.sol +++ b/packages/world/src/index.sol @@ -6,7 +6,8 @@ pragma solidity >=0.8.21; import { NamespaceOwner, NamespaceOwnerTableId } from "./tables/NamespaceOwner.sol"; import { ResourceAccess, ResourceAccessTableId } from "./tables/ResourceAccess.sol"; import { InstalledModules, InstalledModulesTableId } from "./tables/InstalledModules.sol"; -import { Delegations, DelegationsTableId } from "./tables/Delegations.sol"; +import { UserDelegationControl, UserDelegationControlTableId } from "./tables/UserDelegationControl.sol"; +import { NamespaceDelegationControl, NamespaceDelegationControlTableId } from "./tables/NamespaceDelegationControl.sol"; import { Balances, BalancesTableId } from "./modules/core/tables/Balances.sol"; import { Systems, SystemsTableId } from "./modules/core/tables/Systems.sol"; import { SystemRegistry, SystemRegistryTableId } from "./modules/core/tables/SystemRegistry.sol"; diff --git a/packages/world/src/interfaces/IWorldErrors.sol b/packages/world/src/interfaces/IWorldErrors.sol index 5cff530663..a7ea95c578 100644 --- a/packages/world/src/interfaces/IWorldErrors.sol +++ b/packages/world/src/interfaces/IWorldErrors.sol @@ -13,6 +13,7 @@ interface IWorldErrors { error World_FunctionSelectorAlreadyExists(bytes4 functionSelector); error World_FunctionSelectorNotFound(bytes4 functionSelector); error World_DelegationNotFound(address delegator, address delegatee); + error World_UnlimitedDelegationNotAllowed(); error World_InsufficientBalance(uint256 balance, uint256 amount); error World_InterfaceNotSupported(address contractAddress, bytes4 interfaceId); error World_InvalidResourceType(bytes2 expected, ResourceId resourceId, string resourceIdString); diff --git a/packages/world/src/interfaces/IWorldRegistrationSystem.sol b/packages/world/src/interfaces/IWorldRegistrationSystem.sol index 5880071b4b..93900ee784 100644 --- a/packages/world/src/interfaces/IWorldRegistrationSystem.sol +++ b/packages/world/src/interfaces/IWorldRegistrationSystem.sol @@ -28,4 +28,10 @@ interface IWorldRegistrationSystem { ) external returns (bytes4 worldFunctionSelector); function registerDelegation(address delegatee, ResourceId delegationControlId, bytes memory initCallData) external; + + function registerNamespaceDelegation( + ResourceId namespaceId, + ResourceId delegationControlId, + bytes memory initCallData + ) external; } diff --git a/packages/world/src/modules/core/CoreModule.sol b/packages/world/src/modules/core/CoreModule.sol index b167f416ac..e69ebcfd44 100644 --- a/packages/world/src/modules/core/CoreModule.sol +++ b/packages/world/src/modules/core/CoreModule.sol @@ -15,7 +15,8 @@ import { RESOURCE_SYSTEM } from "../../worldResourceTypes.sol"; import { NamespaceOwner } from "../../tables/NamespaceOwner.sol"; import { ResourceAccess } from "../../tables/ResourceAccess.sol"; import { InstalledModules } from "../../tables/InstalledModules.sol"; -import { Delegations } from "../../tables/Delegations.sol"; +import { UserDelegationControl } from "../../tables/UserDelegationControl.sol"; +import { NamespaceDelegationControl } from "../../tables/NamespaceDelegationControl.sol"; import { CoreSystem } from "./CoreSystem.sol"; import { CORE_MODULE_NAME, CORE_SYSTEM_ID } from "./constants.sol"; @@ -67,7 +68,8 @@ contract CoreModule is Module { NamespaceOwner.register(); Balances.register(); InstalledModules.register(); - Delegations.register(); + UserDelegationControl.register(); + NamespaceDelegationControl.register(); ResourceAccess.register(); Systems.register(); FunctionSelectors.register(); @@ -96,7 +98,7 @@ contract CoreModule is Module { * Register function selectors for all CoreSystem functions in the World */ function _registerFunctionSelectors() internal { - string[17] memory functionSignatures = [ + string[18] memory functionSignatures = [ // --- AccessManagementSystem --- "grantAccess(bytes32,address)", "revokeAccess(bytes32,address)", @@ -119,7 +121,8 @@ contract CoreModule is Module { "registerSystem(bytes32,address,bool)", "registerFunctionSelector(bytes32,string)", "registerRootFunctionSelector(bytes32,string,bytes4)", - "registerDelegation(address,bytes32,bytes)" + "registerDelegation(address,bytes32,bytes)", + "registerNamespaceDelegation(bytes32,bytes32,bytes)" ]; for (uint256 i = 0; i < functionSignatures.length; i++) { diff --git a/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol b/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol index 25fadbab85..6ab8598d61 100644 --- a/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol +++ b/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol @@ -16,7 +16,8 @@ import { Delegation } from "../../../Delegation.sol"; import { requireInterface } from "../../../requireInterface.sol"; import { NamespaceOwner } from "../../../tables/NamespaceOwner.sol"; import { ResourceAccess } from "../../../tables/ResourceAccess.sol"; -import { Delegations } from "../../../tables/Delegations.sol"; +import { UserDelegationControl } from "../../../tables/UserDelegationControl.sol"; +import { NamespaceDelegationControl } from "../../../tables/NamespaceDelegationControl.sol"; import { ISystemHook, SYSTEM_HOOK_INTERFACE_ID } from "../../../interfaces/ISystemHook.sol"; import { IWorldErrors } from "../../../interfaces/IWorldErrors.sol"; import { IDelegationControl, DELEGATION_CONTROL_INTERFACE_ID } from "../../../interfaces/IDelegationControl.sol"; @@ -214,7 +215,11 @@ contract WorldRegistrationSystem is System, IWorldErrors { */ function registerDelegation(address delegatee, ResourceId delegationControlId, bytes memory initCallData) public { // Store the delegation control contract address - Delegations._set({ delegator: _msgSender(), delegatee: delegatee, delegationControlId: delegationControlId }); + UserDelegationControl._set({ + delegator: _msgSender(), + delegatee: delegatee, + delegationControlId: delegationControlId + }); // If the delegation is limited... if (Delegation.isLimited(delegationControlId) && initCallData.length > 0) { @@ -223,7 +228,48 @@ contract WorldRegistrationSystem is System, IWorldErrors { requireInterface(delegationControl, DELEGATION_CONTROL_INTERFACE_ID); // Call the delegation control contract's init function - SystemCall.call({ caller: _msgSender(), systemId: delegationControlId, callData: initCallData, value: 0 }); + SystemCall.callWithHooksOrRevert({ + caller: _msgSender(), + systemId: delegationControlId, + callData: initCallData, + value: 0 + }); + } + } + + function registerNamespaceDelegation( + ResourceId namespaceId, + ResourceId delegationControlId, + bytes memory initCallData + ) public { + // Require the namespaceId to be a valid namespace ID + if (namespaceId.getType() != RESOURCE_NAMESPACE) { + revert World_InvalidResourceType(RESOURCE_NAMESPACE, namespaceId, namespaceId.toString()); + } + + // Require the delegation to not be unlimited + if (!Delegation.isLimited(delegationControlId)) { + revert World_UnlimitedDelegationNotAllowed(); + } + + // Require the caller to own the namespace + AccessControl.requireOwner(namespaceId, _msgSender()); + + // Require the delegationControl contract to implement the IDelegationControl interface + (address delegationControl, ) = Systems._get(delegationControlId); + requireInterface(delegationControl, DELEGATION_CONTROL_INTERFACE_ID); + + // Register the delegation control + NamespaceDelegationControl._set(namespaceId, delegationControlId); + + // Call the delegation control contract's init function + if (initCallData.length > 0) { + SystemCall.callWithHooksOrRevert({ + caller: _msgSender(), + systemId: delegationControlId, + callData: initCallData, + value: 0 + }); } } } diff --git a/packages/world/src/tables/NamespaceDelegationControl.sol b/packages/world/src/tables/NamespaceDelegationControl.sol new file mode 100644 index 0000000000..58dc584f72 --- /dev/null +++ b/packages/world/src/tables/NamespaceDelegationControl.sol @@ -0,0 +1,271 @@ +// 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"; + +ResourceId constant _tableId = ResourceId.wrap( + bytes32(abi.encodePacked(RESOURCE_TABLE, bytes14(""), bytes16("NamespaceDelegat"))) +); +ResourceId constant NamespaceDelegationControlTableId = _tableId; + +FieldLayout constant _fieldLayout = FieldLayout.wrap( + 0x0020010020000000000000000000000000000000000000000000000000000000 +); + +library NamespaceDelegationControl { + /** Get the table values' field layout */ + function getFieldLayout() internal pure returns (FieldLayout) { + return _fieldLayout; + } + + /** Get the table's key schema */ + function getKeySchema() internal pure returns (Schema) { + SchemaType[] memory _keySchema = new SchemaType[](1); + _keySchema[0] = SchemaType.BYTES32; + + return SchemaLib.encode(_keySchema); + } + + /** Get the table's value schema */ + function getValueSchema() internal pure returns (Schema) { + SchemaType[] memory _valueSchema = new SchemaType[](1); + _valueSchema[0] = SchemaType.BYTES32; + + return SchemaLib.encode(_valueSchema); + } + + /** Get the table's key names */ + function getKeyNames() internal pure returns (string[] memory keyNames) { + keyNames = new string[](1); + keyNames[0] = "namespaceId"; + } + + /** Get the table's field names */ + function getFieldNames() internal pure returns (string[] memory fieldNames) { + fieldNames = new string[](1); + fieldNames[0] = "delegationControlId"; + } + + /** Register the table with its config */ + function register() internal { + StoreSwitch.registerTable(_tableId, _fieldLayout, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames()); + } + + /** Register the table with its config */ + function _register() internal { + StoreCore.registerTable(_tableId, _fieldLayout, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames()); + } + + /** Register the table with its config (using the specified store) */ + function register(IStore _store) internal { + _store.registerTable(_tableId, _fieldLayout, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames()); + } + + /** Get delegationControlId */ + function getDelegationControlId(ResourceId namespaceId) internal view returns (ResourceId delegationControlId) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(namespaceId); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return ResourceId.wrap(bytes32(_blob)); + } + + /** Get delegationControlId */ + function _getDelegationControlId(ResourceId namespaceId) internal view returns (ResourceId delegationControlId) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(namespaceId); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return ResourceId.wrap(bytes32(_blob)); + } + + /** Get delegationControlId (using the specified store) */ + function getDelegationControlId( + IStore _store, + ResourceId namespaceId + ) internal view returns (ResourceId delegationControlId) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(namespaceId); + + bytes32 _blob = _store.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return ResourceId.wrap(bytes32(_blob)); + } + + /** Get delegationControlId */ + function get(ResourceId namespaceId) internal view returns (ResourceId delegationControlId) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(namespaceId); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return ResourceId.wrap(bytes32(_blob)); + } + + /** Get delegationControlId */ + function _get(ResourceId namespaceId) internal view returns (ResourceId delegationControlId) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(namespaceId); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return ResourceId.wrap(bytes32(_blob)); + } + + /** Get delegationControlId (using the specified store) */ + function get(IStore _store, ResourceId namespaceId) internal view returns (ResourceId delegationControlId) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(namespaceId); + + bytes32 _blob = _store.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return ResourceId.wrap(bytes32(_blob)); + } + + /** Set delegationControlId */ + function setDelegationControlId(ResourceId namespaceId, ResourceId delegationControlId) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(namespaceId); + + StoreSwitch.setStaticField( + _tableId, + _keyTuple, + 0, + abi.encodePacked(ResourceId.unwrap(delegationControlId)), + _fieldLayout + ); + } + + /** Set delegationControlId */ + function _setDelegationControlId(ResourceId namespaceId, ResourceId delegationControlId) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(namespaceId); + + StoreCore.setStaticField( + _tableId, + _keyTuple, + 0, + abi.encodePacked(ResourceId.unwrap(delegationControlId)), + _fieldLayout + ); + } + + /** Set delegationControlId (using the specified store) */ + function setDelegationControlId(IStore _store, ResourceId namespaceId, ResourceId delegationControlId) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(namespaceId); + + _store.setStaticField( + _tableId, + _keyTuple, + 0, + abi.encodePacked(ResourceId.unwrap(delegationControlId)), + _fieldLayout + ); + } + + /** Set delegationControlId */ + function set(ResourceId namespaceId, ResourceId delegationControlId) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(namespaceId); + + StoreSwitch.setStaticField( + _tableId, + _keyTuple, + 0, + abi.encodePacked(ResourceId.unwrap(delegationControlId)), + _fieldLayout + ); + } + + /** Set delegationControlId */ + function _set(ResourceId namespaceId, ResourceId delegationControlId) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(namespaceId); + + StoreCore.setStaticField( + _tableId, + _keyTuple, + 0, + abi.encodePacked(ResourceId.unwrap(delegationControlId)), + _fieldLayout + ); + } + + /** Set delegationControlId (using the specified store) */ + function set(IStore _store, ResourceId namespaceId, ResourceId delegationControlId) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(namespaceId); + + _store.setStaticField( + _tableId, + _keyTuple, + 0, + abi.encodePacked(ResourceId.unwrap(delegationControlId)), + _fieldLayout + ); + } + + /** Delete all data for given keys */ + function deleteRecord(ResourceId namespaceId) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(namespaceId); + + StoreSwitch.deleteRecord(_tableId, _keyTuple); + } + + /** Delete all data for given keys */ + function _deleteRecord(ResourceId namespaceId) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(namespaceId); + + StoreCore.deleteRecord(_tableId, _keyTuple, _fieldLayout); + } + + /** Delete all data for given keys (using the specified store) */ + function deleteRecord(IStore _store, ResourceId namespaceId) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(namespaceId); + + _store.deleteRecord(_tableId, _keyTuple); + } + + /** Tightly pack static data using this table's schema */ + function encodeStatic(ResourceId delegationControlId) internal pure returns (bytes memory) { + return abi.encodePacked(delegationControlId); + } + + /** Tightly pack full data using this table's field layout */ + function encode(ResourceId delegationControlId) internal pure returns (bytes memory, PackedCounter, bytes memory) { + bytes memory _staticData = encodeStatic(delegationControlId); + + PackedCounter _encodedLengths; + bytes memory _dynamicData; + + return (_staticData, _encodedLengths, _dynamicData); + } + + /** Encode keys as a bytes32 array using this table's field layout */ + function encodeKeyTuple(ResourceId namespaceId) internal pure returns (bytes32[] memory) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(namespaceId); + + return _keyTuple; + } +} diff --git a/packages/world/src/tables/Delegations.sol b/packages/world/src/tables/UserDelegationControl.sol similarity index 98% rename from packages/world/src/tables/Delegations.sol rename to packages/world/src/tables/UserDelegationControl.sol index 62506e3b05..b0bac990ba 100644 --- a/packages/world/src/tables/Delegations.sol +++ b/packages/world/src/tables/UserDelegationControl.sol @@ -24,15 +24,15 @@ import { RESOURCE_TABLE, RESOURCE_OFFCHAIN_TABLE } from "@latticexyz/store/src/s import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; ResourceId constant _tableId = ResourceId.wrap( - bytes32(abi.encodePacked(RESOURCE_TABLE, bytes14(""), bytes16("Delegations"))) + bytes32(abi.encodePacked(RESOURCE_TABLE, bytes14(""), bytes16("UserDelegationCo"))) ); -ResourceId constant DelegationsTableId = _tableId; +ResourceId constant UserDelegationControlTableId = _tableId; FieldLayout constant _fieldLayout = FieldLayout.wrap( 0x0020010020000000000000000000000000000000000000000000000000000000 ); -library Delegations { +library UserDelegationControl { /** Get the table values' field layout */ function getFieldLayout() internal pure returns (FieldLayout) { return _fieldLayout; diff --git a/packages/world/test/DelegationControlMock.sol b/packages/world/test/DelegationControlMock.sol new file mode 100644 index 0000000000..152e26d25c --- /dev/null +++ b/packages/world/test/DelegationControlMock.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.21; + +import { DelegationControl } from "../src/DelegationControl.sol"; +import { ResourceId, WorldResourceIdInstance } from "../src/WorldResourceId.sol"; + +contract DelegationControlMock is DelegationControl { + using WorldResourceIdInstance for ResourceId; + + mapping(ResourceId => address) public trustedForwarders; + + function verify(address delegator, ResourceId systemId, bytes memory callData) public returns (bool) { + return trustedForwarders[systemId.getNamespaceId()] == _msgSender(); + } + + function initDelegation(ResourceId namespaceId, address trustedForwarder) public { + trustedForwarders[namespaceId] = trustedForwarder; + } +} diff --git a/packages/world/test/World.t.sol b/packages/world/test/World.t.sol index 1bdc6a56b3..27aded7654 100644 --- a/packages/world/test/World.t.sol +++ b/packages/world/test/World.t.sol @@ -49,6 +49,7 @@ import { ISystemHook, SYSTEM_HOOK_INTERFACE_ID } from "../src/interfaces/ISystem import { Bool } from "./tables/Bool.sol"; import { TwoFields, TwoFieldsData } from "./tables/TwoFields.sol"; import { AddressArray } from "./tables/AddressArray.sol"; +import { DelegationControlMock } from "./DelegationControlMock.sol"; interface IWorldTestSystem { function testNamespace_testSystem_err(string memory input) external pure; @@ -215,7 +216,7 @@ contract WorldTest is Test, GasReporter { // Should have registered the core system function selectors CoreSystem coreSystem = CoreSystem(Systems.getSystem(world, CORE_SYSTEM_ID)); - bytes4[17] memory coreFunctionSignatures = [ + bytes4[18] memory coreFunctionSignatures = [ // --- AccessManagementSystem --- coreSystem.grantAccess.selector, coreSystem.revokeAccess.selector, @@ -238,7 +239,8 @@ contract WorldTest is Test, GasReporter { coreSystem.registerSystem.selector, coreSystem.registerFunctionSelector.selector, coreSystem.registerRootFunctionSelector.selector, - coreSystem.registerDelegation.selector + coreSystem.registerDelegation.selector, + coreSystem.registerNamespaceDelegation.selector ]; for (uint256 i; i < coreFunctionSignatures.length; i++) { @@ -999,6 +1001,73 @@ contract WorldTest is Test, GasReporter { world.registerDelegation(delegatee, UNLIMITED_DELEGATION, new bytes(0)); } + function testCallFromNamespaceDelegation() public { + // Register a new system + WorldTestSystem system = new WorldTestSystem(); + ResourceId systemId = WorldResourceIdLib.encode({ + typeId: RESOURCE_SYSTEM, + namespace: "namespace", + name: "testSystem" + }); + world.registerSystem(systemId, system, true); + + // Register a delegation control mock system + DelegationControlMock delegationControlMock = new DelegationControlMock(); + ResourceId delegationControlMockId = WorldResourceIdLib.encode({ + typeId: RESOURCE_SYSTEM, + namespace: "delegation", + name: "mock" + }); + world.registerSystem(delegationControlMockId, delegationControlMock, true); + + address delegator = address(1); + address delegatee = address(2); + ResourceId namespaceId = systemId.getNamespaceId(); + + // Expect a revert when attempting to perform a call via callFrom before a delegation was created + vm.expectRevert(abi.encodeWithSelector(IWorldErrors.World_DelegationNotFound.selector, delegator, delegatee)); + vm.prank(delegatee); + world.callFrom(delegator, systemId, abi.encodeCall(WorldTestSystem.msgSender, ())); + + // Register the delegation mock as the delegation control for the namespace + // with `delegatee` and `namespaceId` in the init call data + world.registerNamespaceDelegation( + namespaceId, + delegationControlMockId, + abi.encodeWithSelector(delegationControlMock.initDelegation.selector, namespaceId, delegatee) + ); + + // Call a system from the delegatee on behalf of the delegator + vm.prank(delegatee); + startGasReport("call a system via a namespace fallback delegation"); + bytes memory returnData = world.callFrom(delegator, systemId, abi.encodeCall(WorldTestSystem.msgSender, ())); + endGasReport(); + address returnedAddress = abi.decode(returnData, (address)); + + // Expect the system to have received the delegator's address + assertEq(returnedAddress, delegator); + + // Expect a revert when attempting to perform a call on behalf of an address that doesn't have a delegation + vm.expectRevert( + abi.encodeWithSelector( + IWorldErrors.World_DelegationNotFound.selector, + delegator, + address(3) // Invalid delegatee + ) + ); + vm.prank(address(3)); + world.callFrom(delegator, systemId, abi.encodeCall(WorldTestSystem.msgSender, ())); + } + + function testCallFromNamespaceDelegationRevertUnlimitedNotAllowed() public { + vm.expectRevert(abi.encodeWithSelector(IWorldErrors.World_UnlimitedDelegationNotAllowed.selector)); + world.registerNamespaceDelegation( + WorldResourceIdLib.encodeNamespace("namespace"), + UNLIMITED_DELEGATION, + new bytes(0) + ); + } + function testRegisterStoreHook() public { FieldLayout fieldLayout = Bool.getFieldLayout(); Schema valueSchema = Bool.getValueSchema();