diff --git a/packages/world/gas-report.json b/packages/world/gas-report.json index 8e46e1c304..e669ed59d6 100644 --- a/packages/world/gas-report.json +++ b/packages/world/gas-report.json @@ -35,71 +35,95 @@ "name": "AccessControl: requireAccess (this address)", "gasUsed": 153 }, + { + "file": "test/DelegationsModule.t.sol", + "test": "testCallFromDisposableDelegation", + "name": "register a disposable delegation with a non-root module", + "gasUsed": 112653 + }, + { + "file": "test/DelegationsModule.t.sol", + "test": "testCallFromDisposableDelegation", + "name": "call a system via a disposable delegation with a non-root module", + "gasUsed": 54285 + }, + { + "file": "test/DelegationsModule.t.sol", + "test": "testCallFromDisposableDelegationRoot", + "name": "register a disposable delegation with a root module", + "gasUsed": 104604 + }, + { + "file": "test/DelegationsModule.t.sol", + "test": "testCallFromDisposableDelegationRoot", + "name": "call a system via a disposable delegation with a root module", + "gasUsed": 45638 + }, { "file": "test/KeysInTableModule.t.sol", "test": "testInstallComposite", "name": "install keys in table module", - "gasUsed": 1411953 + "gasUsed": 1411658 }, { "file": "test/KeysInTableModule.t.sol", "test": "testInstallGas", "name": "install keys in table module", - "gasUsed": 1411953 + "gasUsed": 1411658 }, { "file": "test/KeysInTableModule.t.sol", "test": "testInstallGas", "name": "set a record on a table with keysInTableModule installed", - "gasUsed": 181989 + "gasUsed": 182055 }, { "file": "test/KeysInTableModule.t.sol", "test": "testInstallSingleton", "name": "install keys in table module", - "gasUsed": 1411953 + "gasUsed": 1411658 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookCompositeGas", "name": "install keys in table module", - "gasUsed": 1411953 + "gasUsed": 1411658 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookCompositeGas", "name": "change a composite record on a table with keysInTableModule installed", - "gasUsed": 25645 + "gasUsed": 25667 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookCompositeGas", "name": "delete a composite record on a table with keysInTableModule installed", - "gasUsed": 250522 + "gasUsed": 250544 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookGas", "name": "install keys in table module", - "gasUsed": 1411953 + "gasUsed": 1411658 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookGas", "name": "change a record on a table with keysInTableModule installed", - "gasUsed": 24365 + "gasUsed": 24387 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookGas", "name": "delete a record on a table with keysInTableModule installed", - "gasUsed": 128811 + "gasUsed": 128833 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testGetKeysWithValueGas", "name": "install keys with value module", - "gasUsed": 650480 + "gasUsed": 650464 }, { "file": "test/KeysWithValueModule.t.sol", @@ -117,25 +141,25 @@ "file": "test/KeysWithValueModule.t.sol", "test": "testInstall", "name": "install keys with value module", - "gasUsed": 650480 + "gasUsed": 650464 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testInstall", "name": "set a record on a table with KeysWithValueModule installed", - "gasUsed": 151489 + "gasUsed": 151511 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetAndDeleteRecordHook", "name": "install keys with value module", - "gasUsed": 650480 + "gasUsed": 650464 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetAndDeleteRecordHook", "name": "change a record on a table with KeysWithValueModule installed", - "gasUsed": 117961 + "gasUsed": 117983 }, { "file": "test/KeysWithValueModule.t.sol", @@ -147,7 +171,7 @@ "file": "test/KeysWithValueModule.t.sol", "test": "testSetField", "name": "install keys with value module", - "gasUsed": 650480 + "gasUsed": 650464 }, { "file": "test/KeysWithValueModule.t.sol", @@ -165,31 +189,31 @@ "file": "test/query.t.sol", "test": "testCombinedHasHasValueNotQuery", "name": "CombinedHasHasValueNotQuery", - "gasUsed": 165699 + "gasUsed": 165765 }, { "file": "test/query.t.sol", "test": "testCombinedHasHasValueQuery", "name": "CombinedHasHasValueQuery", - "gasUsed": 75996 + "gasUsed": 76018 }, { "file": "test/query.t.sol", "test": "testCombinedHasNotQuery", "name": "CombinedHasNotQuery", - "gasUsed": 229855 + "gasUsed": 229965 }, { "file": "test/query.t.sol", "test": "testCombinedHasQuery", "name": "CombinedHasQuery", - "gasUsed": 151588 + "gasUsed": 151676 }, { "file": "test/query.t.sol", "test": "testCombinedHasValueNotQuery", "name": "CombinedHasValueNotQuery", - "gasUsed": 143470 + "gasUsed": 143536 }, { "file": "test/query.t.sol", @@ -201,19 +225,19 @@ "file": "test/query.t.sol", "test": "testHasQuery", "name": "HasQuery", - "gasUsed": 34883 + "gasUsed": 34905 }, { "file": "test/query.t.sol", "test": "testHasQuery1000Keys", "name": "HasQuery with 1000 keys", - "gasUsed": 9272645 + "gasUsed": 9272667 }, { "file": "test/query.t.sol", "test": "testHasQuery100Keys", "name": "HasQuery with 100 keys", - "gasUsed": 861378 + "gasUsed": 861400 }, { "file": "test/query.t.sol", @@ -225,13 +249,13 @@ "file": "test/query.t.sol", "test": "testNotValueQuery", "name": "NotValueQuery", - "gasUsed": 69590 + "gasUsed": 69612 }, { "file": "test/UniqueEntityModule.t.sol", "test": "testInstall", "name": "install unique entity module", - "gasUsed": 722077 + "gasUsed": 722053 }, { "file": "test/UniqueEntityModule.t.sol", @@ -243,7 +267,7 @@ "file": "test/UniqueEntityModule.t.sol", "test": "testInstallRoot", "name": "installRoot unique entity module", - "gasUsed": 700972 + "gasUsed": 700987 }, { "file": "test/UniqueEntityModule.t.sol", @@ -257,6 +281,18 @@ "name": "call a system via the World", "gasUsed": 17531 }, + { + "file": "test/World.t.sol", + "test": "testCallFromUnlimitedDelegation", + "name": "register an unlimited delegation", + "gasUsed": 55457 + }, + { + "file": "test/World.t.sol", + "test": "testCallFromUnlimitedDelegation", + "name": "call a system via an unlimited delegation", + "gasUsed": 17865 + }, { "file": "test/World.t.sol", "test": "testDeleteRecord", @@ -273,37 +309,37 @@ "file": "test/World.t.sol", "test": "testRegisterFallbackSystem", "name": "Register a fallback system", - "gasUsed": 70183 + "gasUsed": 70137 }, { "file": "test/World.t.sol", "test": "testRegisterFallbackSystem", "name": "Register a root fallback system", - "gasUsed": 63676 + "gasUsed": 63700 }, { "file": "test/World.t.sol", "test": "testRegisterFunctionSelector", "name": "Register a function selector", - "gasUsed": 90777 + "gasUsed": 90731 }, { "file": "test/World.t.sol", "test": "testRegisterNamespace", "name": "Register a new namespace", - "gasUsed": 140015 + "gasUsed": 140037 }, { "file": "test/World.t.sol", "test": "testRegisterRootFunctionSelector", "name": "Register a root function selector", - "gasUsed": 79587 + "gasUsed": 79611 }, { "file": "test/World.t.sol", "test": "testRegisterTable", "name": "Register a new table in the namespace", - "gasUsed": 650174 + "gasUsed": 650005 }, { "file": "test/World.t.sol", diff --git a/packages/world/mud.config.ts b/packages/world/mud.config.ts index df45eb446b..9003aa683e 100644 --- a/packages/world/mud.config.ts +++ b/packages/world/mud.config.ts @@ -42,6 +42,15 @@ export default mudConfig({ // schema in `getField` too. (See https://github.com/latticexyz/mud/issues/444) dataStruct: true, }, + Delegations: { + keySchema: { + delegator: "address", + delegatee: "address", + }, + schema: { + delegationControl: "bytes32", + }, + }, /************************************************************************ * * MODULE TABLES @@ -131,6 +140,20 @@ export default mudConfig({ tableIdArgument: true, storeArgument: true, }, + DisposableDelegations: { + directory: "modules/delegations/tables", + keySchema: { + delegator: "address", + delegatee: "address", + resourceSelector: "bytes32", + funcSelectorAndArgsHash: "bytes32", + }, + schema: { + availableCalls: "uint256", + }, + tableIdArgument: true, + storeArgument: true, + }, /************************************************************************ * * TEST TABLES diff --git a/packages/world/src/Delegation.sol b/packages/world/src/Delegation.sol new file mode 100644 index 0000000000..c4140ea0d7 --- /dev/null +++ b/packages/world/src/Delegation.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import { UNLIMITED_DELEGATION } from "./constants.sol"; +import { IDelegationControl } from "./interfaces/IDelegationControl.sol"; +import { SystemCall } from "./SystemCall.sol"; + +type Delegation is bytes32; + +using DelegationInstance for Delegation global; + +library DelegationInstance { + function exists(Delegation self) internal pure returns (bool) { + return Delegation.unwrap(self) != bytes32(""); + } + + function isUnlimited(Delegation self) internal pure returns (bool) { + return Delegation.unwrap(self) == UNLIMITED_DELEGATION; + } + + function isLimited(Delegation self) internal pure returns (bool) { + return exists(self) && !isUnlimited(self); + } + + /** + * Verify a delegation. + * Returns true if the delegation exists and is valid, false otherwise. + * Note: verifying the delegation might have side effects in the delegation control contract. + */ + function verify( + Delegation self, + address delegator, + address delegatee, + bytes32 systemId, + bytes memory funcSelectorAndArgs + ) internal returns (bool) { + // Early return if there is an unlimited delegation + if (isUnlimited(self)) return true; + + // Early return if there is no valid delegation + if (!exists(self)) return false; + + // Call the delegation control contract to check if the delegator has granted access to the delegatee + (bool success, bytes memory data) = SystemCall.call({ + caller: delegatee, + resourceSelector: Delegation.unwrap(self), + funcSelectorAndArgs: abi.encodeWithSelector( + IDelegationControl.verify.selector, + delegator, + systemId, + funcSelectorAndArgs + ), + value: 0 + }); + + if (!success) return false; + return abi.decode(data, (bool)); + } +} diff --git a/packages/world/src/DelegationControl.sol b/packages/world/src/DelegationControl.sol new file mode 100644 index 0000000000..29c432e2ad --- /dev/null +++ b/packages/world/src/DelegationControl.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import { WorldContextConsumer } from "./WorldContext.sol"; +import { IDelegationControl } from "./interfaces/IDelegationControl.sol"; + +abstract contract DelegationControl is WorldContextConsumer, IDelegationControl {} diff --git a/packages/world/src/Tables.sol b/packages/world/src/Tables.sol index b6b1f4bd1a..ec5ba90fc1 100644 --- a/packages/world/src/Tables.sol +++ b/packages/world/src/Tables.sol @@ -6,6 +6,7 @@ pragma solidity >=0.8.0; import { NamespaceOwner, NamespaceOwnerTableId } from "./tables/NamespaceOwner.sol"; import { ResourceAccess, ResourceAccessTableId } from "./tables/ResourceAccess.sol"; import { InstalledModules, InstalledModulesData, InstalledModulesTableId } from "./tables/InstalledModules.sol"; +import { Delegations, DelegationsTableId } from "./tables/Delegations.sol"; import { Systems, SystemsTableId } from "./modules/core/tables/Systems.sol"; import { SystemRegistry, SystemRegistryTableId } from "./modules/core/tables/SystemRegistry.sol"; import { SystemHooks, SystemHooksTableId } from "./modules/core/tables/SystemHooks.sol"; @@ -15,5 +16,6 @@ import { KeysWithValue } from "./modules/keyswithvalue/tables/KeysWithValue.sol" import { KeysInTable, KeysInTableData, KeysInTableTableId } from "./modules/keysintable/tables/KeysInTable.sol"; import { UsedKeysIndex, UsedKeysIndexTableId } from "./modules/keysintable/tables/UsedKeysIndex.sol"; import { UniqueEntity } from "./modules/uniqueentity/tables/UniqueEntity.sol"; +import { DisposableDelegations } from "./modules/delegations/tables/DisposableDelegations.sol"; import { Bool } from "./../test/tables/Bool.sol"; import { AddressArray } from "./../test/tables/AddressArray.sol"; diff --git a/packages/world/src/Utils.sol b/packages/world/src/Utils.sol index dd5a4138dc..7e231fcefe 100644 --- a/packages/world/src/Utils.sol +++ b/packages/world/src/Utils.sol @@ -21,4 +21,4 @@ library Utils { return ResourceSelector.getNamespace(resourceSelector); } } -} +} \ No newline at end of file diff --git a/packages/world/src/World.sol b/packages/world/src/World.sol index 9870fd82bc..fce2f6ad72 100644 --- a/packages/world/src/World.sol +++ b/packages/world/src/World.sol @@ -14,13 +14,16 @@ import { AccessControl } from "./AccessControl.sol"; import { SystemCall } from "./SystemCall.sol"; import { WorldContextProvider } from "./WorldContext.sol"; import { revertWithBytes } from "./revertWithBytes.sol"; +import { Delegation } from "./Delegation.sol"; import { NamespaceOwner } from "./tables/NamespaceOwner.sol"; import { InstalledModules } from "./tables/InstalledModules.sol"; +import { Delegations } from "./tables/Delegations.sol"; import { ISystemHook } from "./interfaces/ISystemHook.sol"; import { IModule } from "./interfaces/IModule.sol"; import { IWorldKernel } from "./interfaces/IWorldKernel.sol"; +import { IDelegationControl } from "./interfaces/IDelegationControl.sol"; import { Systems } from "./modules/core/tables/Systems.sol"; import { SystemHooks } from "./modules/core/tables/SystemHooks.sol"; @@ -179,6 +182,33 @@ contract World is StoreRead, IStoreData, IWorldKernel { return SystemCall.callWithHooksOrRevert(msg.sender, resourceSelector, funcSelectorAndArgs, msg.value); } + /** + * Call the system at the given resourceSelector on behalf of the given delegator. + * If the system is not public, the delegator must have access to the namespace or name (encoded in the resourceSelector). + */ + function callFrom( + address delegator, + bytes32 resourceSelector, + bytes memory funcSelectorAndArgs + ) external payable virtual returns (bytes memory) { + // Check if there is an explicit authorization for this caller to perform actions on behalf of the delegator + Delegation explicitDelegation = Delegation.wrap(Delegations.get({ delegator: delegator, delegatee: msg.sender })); + + if (explicitDelegation.verify(delegator, msg.sender, resourceSelector, funcSelectorAndArgs)) { + // forward the call as `delegator` + return SystemCall.callWithHooksOrRevert(delegator, resourceSelector, funcSelectorAndArgs, msg.value); + } + + // Check if the delegator has a fallback delegation control set + Delegation fallbackDelegation = Delegation.wrap(Delegations.get({ delegator: delegator, delegatee: address(0) })); + if (fallbackDelegation.verify(delegator, msg.sender, resourceSelector, funcSelectorAndArgs)) { + // forward the call with `from` as `msgSender` + return SystemCall.callWithHooksOrRevert(delegator, resourceSelector, funcSelectorAndArgs, msg.value); + } + + revert DelegationNotFound(delegator, msg.sender); + } + /************************************************************************ * * DYNAMIC FUNCTION SELECTORS diff --git a/packages/world/src/constants.sol b/packages/world/src/constants.sol index 98de758522..a4a2538803 100644 --- a/packages/world/src/constants.sol +++ b/packages/world/src/constants.sol @@ -3,3 +3,4 @@ pragma solidity >=0.8.0; bytes16 constant ROOT_NAMESPACE = 0; bytes16 constant ROOT_NAME = 0; +bytes32 constant UNLIMITED_DELEGATION = bytes32(abi.encodePacked(ROOT_NAMESPACE, bytes16("unlimited.d"))); diff --git a/packages/world/src/interfaces/IDelegationControl.sol b/packages/world/src/interfaces/IDelegationControl.sol new file mode 100644 index 0000000000..239c48f7d7 --- /dev/null +++ b/packages/world/src/interfaces/IDelegationControl.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +interface IDelegationControl { + function verify(address grantor, bytes32 systemId, bytes calldata funcSelectorAndArgs) external returns (bool); +} diff --git a/packages/world/src/interfaces/IWorldErrors.sol b/packages/world/src/interfaces/IWorldErrors.sol index 2ed17b66bd..984ee282bb 100644 --- a/packages/world/src/interfaces/IWorldErrors.sol +++ b/packages/world/src/interfaces/IWorldErrors.sol @@ -10,4 +10,5 @@ interface IWorldErrors { error FunctionSelectorExists(bytes4 functionSelector); error FunctionSelectorNotFound(bytes4 functionSelector); error ModuleAlreadyInstalled(string module); + error DelegationNotFound(address delegator, address delegatee); } diff --git a/packages/world/src/interfaces/IWorldKernel.sol b/packages/world/src/interfaces/IWorldKernel.sol index d4ac2e6f13..4016a56772 100644 --- a/packages/world/src/interfaces/IWorldKernel.sol +++ b/packages/world/src/interfaces/IWorldKernel.sol @@ -20,6 +20,16 @@ interface IWorldCall { * If the system is not public, the caller must have access to the namespace or name (encoded in the resourceSelector). */ function call(bytes32 resourceSelector, bytes memory funcSelectorAndArgs) external payable returns (bytes memory); + + /** + * Call the system at the given resourceSelector on behalf of the given delegator. + * If the system is not public, the delegator must have access to the namespace or name (encoded in the resourceSelector). + */ + function callFrom( + address delegator, + bytes32 resourceSelector, + bytes memory funcSelectorAndArgs + ) external payable returns (bytes memory); } /** diff --git a/packages/world/src/interfaces/IWorldRegistrationSystem.sol b/packages/world/src/interfaces/IWorldRegistrationSystem.sol index 8409aadd6f..d4479d1ba1 100644 --- a/packages/world/src/interfaces/IWorldRegistrationSystem.sol +++ b/packages/world/src/interfaces/IWorldRegistrationSystem.sol @@ -24,4 +24,10 @@ interface IWorldRegistrationSystem { bytes4 worldFunctionSelector, bytes4 systemFunctionSelector ) external returns (bytes4); + + function registerDelegation( + address delegatee, + bytes32 delegationControl, + bytes memory initFuncSelectorAndArgs + ) external; } diff --git a/packages/world/src/modules/core/CoreModule.sol b/packages/world/src/modules/core/CoreModule.sol index e1b820aef2..99d2509f04 100644 --- a/packages/world/src/modules/core/CoreModule.sol +++ b/packages/world/src/modules/core/CoreModule.sol @@ -90,13 +90,14 @@ contract CoreModule is IModule, WorldContextConsumer { * Register function selectors for all CoreSystem functions in the World */ function _registerFunctionSelectors() internal { - bytes4[11] memory functionSelectors = [ + bytes4[12] memory functionSelectors = [ // --- WorldRegistrationSystem --- WorldRegistrationSystem.registerNamespace.selector, WorldRegistrationSystem.registerSystemHook.selector, WorldRegistrationSystem.registerSystem.selector, WorldRegistrationSystem.registerFunctionSelector.selector, WorldRegistrationSystem.registerRootFunctionSelector.selector, + WorldRegistrationSystem.registerDelegation.selector, // --- StoreRegistrationSystem --- StoreRegistrationSystem.registerTable.selector, StoreRegistrationSystem.registerStoreHook.selector, diff --git a/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol b/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol index fe0251b54c..97c3c415d1 100644 --- a/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol +++ b/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol @@ -6,10 +6,11 @@ import { WorldContextConsumer } from "../../../WorldContext.sol"; import { ResourceSelector } from "../../../ResourceSelector.sol"; import { Resource } from "../../../Types.sol"; import { SystemCall } from "../../../SystemCall.sol"; -import { ROOT_NAMESPACE, ROOT_NAME } from "../../../constants.sol"; +import { ROOT_NAMESPACE, ROOT_NAME, UNLIMITED_DELEGATION } from "../../../constants.sol"; import { AccessControl } from "../../../AccessControl.sol"; import { NamespaceOwner } from "../../../tables/NamespaceOwner.sol"; import { ResourceAccess } from "../../../tables/ResourceAccess.sol"; +import { Delegations } from "../../../tables/Delegations.sol"; import { ISystemHook } from "../../../interfaces/ISystemHook.sol"; import { IWorldErrors } from "../../../interfaces/IWorldErrors.sol"; @@ -152,4 +153,26 @@ contract WorldRegistrationSystem is System, IWorldErrors { return worldFunctionSelector; } + + /** + * Register a delegation from the caller to the given delegatee. + */ + function registerDelegation( + address delegatee, + bytes32 delegationControl, + bytes memory initFuncSelectorAndArgs + ) public { + // Store the delegation control contract address + Delegations.set({ delegator: _msgSender(), delegatee: delegatee, delegationControl: delegationControl }); + + // If the delegation is not unlimited, call the delegation control contract's init function + if (delegationControl != UNLIMITED_DELEGATION && initFuncSelectorAndArgs.length > 0) { + SystemCall.call({ + caller: _msgSender(), + resourceSelector: delegationControl, + funcSelectorAndArgs: initFuncSelectorAndArgs, + value: 0 + }); + } + } } diff --git a/packages/world/src/modules/delegations/DelegationsModule.sol b/packages/world/src/modules/delegations/DelegationsModule.sol new file mode 100644 index 0000000000..3215638889 --- /dev/null +++ b/packages/world/src/modules/delegations/DelegationsModule.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import { IBaseWorld } from "../../interfaces/IBaseWorld.sol"; +import { IModule } from "../../interfaces/IModule.sol"; + +import { WorldContextConsumer } from "../../WorldContext.sol"; +import { ResourceSelector } from "../../ResourceSelector.sol"; + +import { DisposableDelegations } from "./tables/DisposableDelegations.sol"; +import { DisposableDelegationControl } from "./DisposableDelegationControl.sol"; + +import { MODULE_NAME, NAMESPACE, DISPOSABLE_DELEGATION, DISPOSABLE_DELEGATION_ROOT, DISPOSABLE_DELEGATION_TABLE, DISPOSABLE_DELEGATION_TABLE_ROOT } from "./constants.sol"; + +/** + * This module registers tables and delegation control systems required for standard delegations + */ +contract DelegationsModule is IModule, WorldContextConsumer { + DisposableDelegationControl immutable disposableDelegationControl = new DisposableDelegationControl(); + + function getName() public pure returns (bytes16) { + return MODULE_NAME; + } + + function install(bytes memory) public { + IBaseWorld world = IBaseWorld(_world()); + + // If this module is installed as a root module, register it in the root namespace + if (address(world) == address(this)) { + DisposableDelegations.register(world, DISPOSABLE_DELEGATION_TABLE_ROOT); + world.registerSystem(DISPOSABLE_DELEGATION_ROOT, disposableDelegationControl, true); + } else { + DisposableDelegations.register(world, DISPOSABLE_DELEGATION_TABLE); + world.registerSystem(DISPOSABLE_DELEGATION, disposableDelegationControl, true); + } + } +} diff --git a/packages/world/src/modules/delegations/DisposableDelegationControl.sol b/packages/world/src/modules/delegations/DisposableDelegationControl.sol new file mode 100644 index 0000000000..6659932411 --- /dev/null +++ b/packages/world/src/modules/delegations/DisposableDelegationControl.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import { DISPOSABLE_DELEGATION_TABLE } from "./constants.sol"; +import { DelegationControl } from "../../DelegationControl.sol"; +import { DisposableDelegations } from "./tables/DisposableDelegations.sol"; + +contract DisposableDelegationControl is DelegationControl { + /** + * Verify a delegation by checking if the delegator has any available calls left in the DisposableDelegations table and decrementing the available calls if so. + */ + function verify(address delegator, bytes32 resourceSelector, bytes memory funcSelectorAndArgs) public returns (bool) { + bytes32 funcSelectorAndArgsHash = keccak256(funcSelectorAndArgs); + + // Get the number of available calls for the given delegator, resourceSelector and funcSelectorAndArgs + uint256 availableCalls = DisposableDelegations.get({ + _tableId: DISPOSABLE_DELEGATION_TABLE, + delegator: delegator, + delegatee: _msgSender(), + resourceSelector: resourceSelector, + funcSelectorAndArgsHash: funcSelectorAndArgsHash + }); + + if (availableCalls > 0) { + // Decrement the number of available calls + DisposableDelegations.set({ + _tableId: DISPOSABLE_DELEGATION_TABLE, + delegator: delegator, + delegatee: _msgSender(), + resourceSelector: resourceSelector, + funcSelectorAndArgsHash: funcSelectorAndArgsHash, + availableCalls: availableCalls - 1 + }); + + return true; + } + + return false; + } + + /** + * Initialize a delegation by setting the number of available calls in the DisposableDelegations table + */ + function initDelegation( + address delegatee, + bytes32 resourceSelector, + bytes memory funcSelectorAndArgs, + uint256 numCalls + ) public { + DisposableDelegations.set({ + _tableId: DISPOSABLE_DELEGATION_TABLE, + delegator: _msgSender(), + delegatee: delegatee, + resourceSelector: resourceSelector, + funcSelectorAndArgsHash: keccak256(funcSelectorAndArgs), + availableCalls: numCalls + }); + } +} diff --git a/packages/world/src/modules/delegations/constants.sol b/packages/world/src/modules/delegations/constants.sol new file mode 100644 index 0000000000..94eb147800 --- /dev/null +++ b/packages/world/src/modules/delegations/constants.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; +import { ResourceSelector } from "../../ResourceSelector.sol"; +import { ROOT_NAMESPACE } from "../../constants.sol"; + +bytes16 constant MODULE_NAME = bytes16("delegations.m"); +bytes16 constant NAMESPACE = bytes16("delegations"); +bytes32 constant DISPOSABLE_DELEGATION = bytes32(abi.encodePacked(NAMESPACE, bytes16("disposable.d"))); +bytes32 constant DISPOSABLE_DELEGATION_ROOT = bytes32(abi.encodePacked(ROOT_NAMESPACE, bytes16("disposable.d"))); +bytes32 constant DISPOSABLE_DELEGATION_TABLE = bytes32(abi.encodePacked(NAMESPACE, bytes16("disposable.t"))); +bytes32 constant DISPOSABLE_DELEGATION_TABLE_ROOT = bytes32(abi.encodePacked(ROOT_NAMESPACE, bytes16("disposable.t"))); diff --git a/packages/world/src/modules/delegations/tables/DisposableDelegations.sol b/packages/world/src/modules/delegations/tables/DisposableDelegations.sol new file mode 100644 index 0000000000..239ed6a3ad --- /dev/null +++ b/packages/world/src/modules/delegations/tables/DisposableDelegations.sol @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +/* 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 { Schema, SchemaLib } from "@latticexyz/store/src/Schema.sol"; +import { PackedCounter, PackedCounterLib } from "@latticexyz/store/src/PackedCounter.sol"; + +library DisposableDelegations { + /** Get the table's key schema */ + function getKeySchema() internal pure returns (Schema) { + SchemaType[] memory _schema = new SchemaType[](4); + _schema[0] = SchemaType.ADDRESS; + _schema[1] = SchemaType.ADDRESS; + _schema[2] = SchemaType.BYTES32; + _schema[3] = SchemaType.BYTES32; + + return SchemaLib.encode(_schema); + } + + /** Get the table's value schema */ + function getValueSchema() internal pure returns (Schema) { + SchemaType[] memory _schema = new SchemaType[](1); + _schema[0] = SchemaType.UINT256; + + return SchemaLib.encode(_schema); + } + + /** Get the table's key names */ + function getKeyNames() internal pure returns (string[] memory keyNames) { + keyNames = new string[](4); + keyNames[0] = "delegator"; + keyNames[1] = "delegatee"; + keyNames[2] = "resourceSelector"; + keyNames[3] = "funcSelectorAndArgsHash"; + } + + /** Get the table's field names */ + function getFieldNames() internal pure returns (string[] memory fieldNames) { + fieldNames = new string[](1); + fieldNames[0] = "availableCalls"; + } + + /** Register the table's key schema, value schema, key names and value names */ + function register(bytes32 _tableId) internal { + StoreSwitch.registerTable(_tableId, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames()); + } + + /** Register the table's key schema, value schema, key names and value names (using the specified store) */ + function register(IStore _store, bytes32 _tableId) internal { + _store.registerTable(_tableId, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames()); + } + + /** Get availableCalls */ + function get( + bytes32 _tableId, + address delegator, + address delegatee, + bytes32 resourceSelector, + bytes32 funcSelectorAndArgsHash + ) internal view returns (uint256 availableCalls) { + bytes32[] memory _keyTuple = new bytes32[](4); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + _keyTuple[2] = resourceSelector; + _keyTuple[3] = funcSelectorAndArgsHash; + + bytes memory _blob = StoreSwitch.getField(_tableId, _keyTuple, 0, getValueSchema()); + return (uint256(Bytes.slice32(_blob, 0))); + } + + /** Get availableCalls (using the specified store) */ + function get( + IStore _store, + bytes32 _tableId, + address delegator, + address delegatee, + bytes32 resourceSelector, + bytes32 funcSelectorAndArgsHash + ) internal view returns (uint256 availableCalls) { + bytes32[] memory _keyTuple = new bytes32[](4); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + _keyTuple[2] = resourceSelector; + _keyTuple[3] = funcSelectorAndArgsHash; + + bytes memory _blob = _store.getField(_tableId, _keyTuple, 0, getValueSchema()); + return (uint256(Bytes.slice32(_blob, 0))); + } + + /** Set availableCalls */ + function set( + bytes32 _tableId, + address delegator, + address delegatee, + bytes32 resourceSelector, + bytes32 funcSelectorAndArgsHash, + uint256 availableCalls + ) internal { + bytes32[] memory _keyTuple = new bytes32[](4); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + _keyTuple[2] = resourceSelector; + _keyTuple[3] = funcSelectorAndArgsHash; + + StoreSwitch.setField(_tableId, _keyTuple, 0, abi.encodePacked((availableCalls)), getValueSchema()); + } + + /** Set availableCalls (using the specified store) */ + function set( + IStore _store, + bytes32 _tableId, + address delegator, + address delegatee, + bytes32 resourceSelector, + bytes32 funcSelectorAndArgsHash, + uint256 availableCalls + ) internal { + bytes32[] memory _keyTuple = new bytes32[](4); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + _keyTuple[2] = resourceSelector; + _keyTuple[3] = funcSelectorAndArgsHash; + + _store.setField(_tableId, _keyTuple, 0, abi.encodePacked((availableCalls)), getValueSchema()); + } + + /** Tightly pack full data using this table's schema */ + function encode(uint256 availableCalls) internal pure returns (bytes memory) { + return abi.encodePacked(availableCalls); + } + + /** Encode keys as a bytes32 array using this table's schema */ + function encodeKeyTuple( + address delegator, + address delegatee, + bytes32 resourceSelector, + bytes32 funcSelectorAndArgsHash + ) internal pure returns (bytes32[] memory) { + bytes32[] memory _keyTuple = new bytes32[](4); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + _keyTuple[2] = resourceSelector; + _keyTuple[3] = funcSelectorAndArgsHash; + + return _keyTuple; + } + + /* Delete all data for given keys */ + function deleteRecord( + bytes32 _tableId, + address delegator, + address delegatee, + bytes32 resourceSelector, + bytes32 funcSelectorAndArgsHash + ) internal { + bytes32[] memory _keyTuple = new bytes32[](4); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + _keyTuple[2] = resourceSelector; + _keyTuple[3] = funcSelectorAndArgsHash; + + StoreSwitch.deleteRecord(_tableId, _keyTuple, getValueSchema()); + } + + /* Delete all data for given keys (using the specified store) */ + function deleteRecord( + IStore _store, + bytes32 _tableId, + address delegator, + address delegatee, + bytes32 resourceSelector, + bytes32 funcSelectorAndArgsHash + ) internal { + bytes32[] memory _keyTuple = new bytes32[](4); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + _keyTuple[2] = resourceSelector; + _keyTuple[3] = funcSelectorAndArgsHash; + + _store.deleteRecord(_tableId, _keyTuple, getValueSchema()); + } +} diff --git a/packages/world/src/tables/Delegations.sol b/packages/world/src/tables/Delegations.sol new file mode 100644 index 0000000000..1a444ff748 --- /dev/null +++ b/packages/world/src/tables/Delegations.sol @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +/* 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 { Schema, SchemaLib } from "@latticexyz/store/src/Schema.sol"; +import { PackedCounter, PackedCounterLib } from "@latticexyz/store/src/PackedCounter.sol"; + +bytes32 constant _tableId = bytes32(abi.encodePacked(bytes16(""), bytes16("Delegations"))); +bytes32 constant DelegationsTableId = _tableId; + +library Delegations { + /** Get the table's key schema */ + function getKeySchema() internal pure returns (Schema) { + SchemaType[] memory _schema = new SchemaType[](2); + _schema[0] = SchemaType.ADDRESS; + _schema[1] = SchemaType.ADDRESS; + + return SchemaLib.encode(_schema); + } + + /** Get the table's value schema */ + function getValueSchema() internal pure returns (Schema) { + SchemaType[] memory _schema = new SchemaType[](1); + _schema[0] = SchemaType.BYTES32; + + return SchemaLib.encode(_schema); + } + + /** Get the table's key names */ + function getKeyNames() internal pure returns (string[] memory keyNames) { + keyNames = new string[](2); + keyNames[0] = "delegator"; + keyNames[1] = "delegatee"; + } + + /** Get the table's field names */ + function getFieldNames() internal pure returns (string[] memory fieldNames) { + fieldNames = new string[](1); + fieldNames[0] = "delegationControl"; + } + + /** Register the table's key schema, value schema, key names and value names */ + function register() internal { + StoreSwitch.registerTable(_tableId, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames()); + } + + /** Register the table's key schema, value schema, key names and value names (using the specified store) */ + function register(IStore _store) internal { + _store.registerTable(_tableId, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames()); + } + + /** Get delegationControl */ + function get(address delegator, address delegatee) internal view returns (bytes32 delegationControl) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + + bytes memory _blob = StoreSwitch.getField(_tableId, _keyTuple, 0, getValueSchema()); + return (Bytes.slice32(_blob, 0)); + } + + /** Get delegationControl (using the specified store) */ + function get(IStore _store, address delegator, address delegatee) internal view returns (bytes32 delegationControl) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + + bytes memory _blob = _store.getField(_tableId, _keyTuple, 0, getValueSchema()); + return (Bytes.slice32(_blob, 0)); + } + + /** Set delegationControl */ + function set(address delegator, address delegatee, bytes32 delegationControl) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + + StoreSwitch.setField(_tableId, _keyTuple, 0, abi.encodePacked((delegationControl)), getValueSchema()); + } + + /** Set delegationControl (using the specified store) */ + function set(IStore _store, address delegator, address delegatee, bytes32 delegationControl) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + + _store.setField(_tableId, _keyTuple, 0, abi.encodePacked((delegationControl)), getValueSchema()); + } + + /** Tightly pack full data using this table's schema */ + function encode(bytes32 delegationControl) internal pure returns (bytes memory) { + return abi.encodePacked(delegationControl); + } + + /** Encode keys as a bytes32 array using this table's schema */ + function encodeKeyTuple(address delegator, address delegatee) internal pure returns (bytes32[] memory) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + + return _keyTuple; + } + + /* Delete all data for given keys */ + function deleteRecord(address delegator, address delegatee) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + + StoreSwitch.deleteRecord(_tableId, _keyTuple, getValueSchema()); + } + + /* Delete all data for given keys (using the specified store) */ + function deleteRecord(IStore _store, address delegator, address delegatee) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + + _store.deleteRecord(_tableId, _keyTuple, getValueSchema()); + } +} diff --git a/packages/world/test/DelegationsModule.t.sol b/packages/world/test/DelegationsModule.t.sol new file mode 100644 index 0000000000..47382af0fa --- /dev/null +++ b/packages/world/test/DelegationsModule.t.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import { Test } from "forge-std/Test.sol"; +import { GasReporter } from "@latticexyz/gas-report/src/GasReporter.sol"; + +import { World } from "../src/World.sol"; +import { ResourceSelector } from "../src/ResourceSelector.sol"; +import { IBaseWorld } from "../src/interfaces/IBaseWorld.sol"; +import { IWorldErrors } from "../src/interfaces/IWorldErrors.sol"; +import { CoreModule } from "../src/modules/core/CoreModule.sol"; +import { Systems } from "../src/modules/core/tables/Systems.sol"; +import { DelegationsModule } from "../src/modules/delegations/DelegationsModule.sol"; +import { DisposableDelegationControl } from "../src/modules/delegations/DisposableDelegationControl.sol"; +import { DISPOSABLE_DELEGATION, DISPOSABLE_DELEGATION_ROOT } from "../src/modules/delegations/DelegationsModule.sol"; + +import { WorldTestSystem } from "./World.t.sol"; + +contract DelegationsModuleTest is Test, GasReporter { + IBaseWorld world; + bytes32 systemResourceSelector = ResourceSelector.from("namespace", "testSystem"); + address delegator = address(1); + address delegatee = address(2); + + function setUp() public { + world = IBaseWorld(address(new World())); + world.installRootModule(new CoreModule(), new bytes(0)); + + // Register a new system + WorldTestSystem system = new WorldTestSystem(); + world.registerSystem(systemResourceSelector, system, true); + } + + function testCallFromDisposableDelegationRoot() public { + // Install Delegations module as root module + world.installRootModule(new DelegationsModule(), new bytes(0)); + + // Register the disposable delegation for one call to the system's msgSender function + vm.prank(delegator); + startGasReport("register a disposable delegation with a root module"); + world.registerDelegation( + delegatee, + DISPOSABLE_DELEGATION_ROOT, + abi.encodeWithSelector( + DisposableDelegationControl.initDelegation.selector, + delegatee, + systemResourceSelector, + abi.encodeWithSelector(WorldTestSystem.msgSender.selector), + 1 + ) + ); + endGasReport(); + + // Call a system from the delegatee on behalf of the delegator + vm.prank(delegatee); + startGasReport("call a system via a disposable delegation with a root module"); + bytes memory returnData = world.callFrom( + delegator, + systemResourceSelector, + abi.encodeWithSelector(WorldTestSystem.msgSender.selector) + ); + endGasReport(); + address returnedAddress = abi.decode(returnData, (address)); + + // Expect the system to have received the delegator's address + assertEq(returnedAddress, delegator); + + // Expect the delegation to have been used up + vm.expectRevert(abi.encodeWithSelector(IWorldErrors.DelegationNotFound.selector, delegator, delegatee)); + vm.prank(delegatee); + world.callFrom(delegator, systemResourceSelector, abi.encodeWithSelector(WorldTestSystem.msgSender.selector)); + } + + function testCallFromDisposableDelegation() public { + // Install Delegations module as root module + world.installModule(new DelegationsModule(), new bytes(0)); + + // Register the disposable delegation for one call to the system's msgSender function + vm.prank(delegator); + startGasReport("register a disposable delegation with a non-root module"); + world.registerDelegation( + delegatee, + DISPOSABLE_DELEGATION, + abi.encodeWithSelector( + DisposableDelegationControl.initDelegation.selector, + delegatee, + systemResourceSelector, + abi.encodeWithSelector(WorldTestSystem.msgSender.selector), + 1 + ) + ); + endGasReport(); + + // Call a system from the delegatee on behalf of the delegator + vm.prank(delegatee); + startGasReport("call a system via a disposable delegation with a non-root module"); + bytes memory returnData = world.callFrom( + delegator, + systemResourceSelector, + abi.encodeWithSelector(WorldTestSystem.msgSender.selector) + ); + endGasReport(); + address returnedAddress = abi.decode(returnData, (address)); + + // Expect the system to have received the delegator's address + assertEq(returnedAddress, delegator); + + // Expect the delegation to have been used up + vm.expectRevert(abi.encodeWithSelector(IWorldErrors.DelegationNotFound.selector, delegator, delegatee)); + vm.prank(delegatee); + world.callFrom(delegator, systemResourceSelector, abi.encodeWithSelector(WorldTestSystem.msgSender.selector)); + } +} diff --git a/packages/world/test/World.t.sol b/packages/world/test/World.t.sol index 8141b093b9..58ba3aeef1 100644 --- a/packages/world/test/World.t.sol +++ b/packages/world/test/World.t.sol @@ -18,7 +18,7 @@ import { EncodeArray } from "@latticexyz/store/src/tightcoder/EncodeArray.sol"; import { World } from "../src/World.sol"; import { System } from "../src/System.sol"; import { ResourceSelector } from "../src/ResourceSelector.sol"; -import { ROOT_NAMESPACE, ROOT_NAME } from "../src/constants.sol"; +import { ROOT_NAMESPACE, ROOT_NAME, UNLIMITED_DELEGATION } from "../src/constants.sol"; import { NamespaceOwner, NamespaceOwnerTableId } from "../src/tables/NamespaceOwner.sol"; import { ResourceAccess } from "../src/tables/ResourceAccess.sol"; @@ -580,6 +580,66 @@ contract WorldTest is Test, GasReporter { assertEq(returnedAddress, address(this), "subsystem returned wrong address"); } + function testCallFromUnlimitedDelegation() public { + // Register a new system + WorldTestSystem system = new WorldTestSystem(); + bytes32 resourceSelector = ResourceSelector.from("namespace", "testSystem"); + world.registerSystem(resourceSelector, system, true); + + // Register an unlimited delegation + address delegator = address(1); + address delegatee = address(2); + vm.prank(delegator); + startGasReport("register an unlimited delegation"); + world.registerDelegation(delegatee, UNLIMITED_DELEGATION, new bytes(0)); + endGasReport(); + + // Call a system from the delegatee on behalf of the delegator + vm.prank(delegatee); + startGasReport("call a system via an unlimited delegation"); + bytes memory returnData = world.callFrom( + delegator, + resourceSelector, + abi.encodeWithSelector(WorldTestSystem.msgSender.selector) + ); + endGasReport(); + address returnedAddress = abi.decode(returnData, (address)); + + // Expect the system to have received the delegator's address + assertEq(returnedAddress, delegator); + } + + function testCallFromFailDelegationNotFound() public { + // Register a new system + WorldTestSystem system = new WorldTestSystem(); + bytes32 resourceSelector = ResourceSelector.from("namespace", "testSystem"); + world.registerSystem(resourceSelector, system, true); + + // 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.DelegationNotFound.selector, + address(2), // Delegator + address(1) // Delegatee + ) + ); + vm.prank(address(1)); + world.callFrom(address(2), resourceSelector, abi.encodeWithSelector(WorldTestSystem.msgSender.selector)); + } + + function testCallFromLimitedDelegation() public { + // Register a new system + WorldTestSystem system = new WorldTestSystem(); + bytes32 resourceSelector = ResourceSelector.from("namespace", "testSystem"); + world.registerSystem(resourceSelector, system, true); + + // Register a limited delegation + address delegator = address(1); + address delegatee = address(2); + vm.prank(delegator); + world.registerDelegation(delegatee, UNLIMITED_DELEGATION, new bytes(0)); + } + function testRegisterTableHook() public { Schema valueSchema = Bool.getValueSchema(); bytes32 tableId = ResourceSelector.from("", "testTable");