From 1d60930d6d4c9a0bda262e5e23a5f719b9dd48c7 Mon Sep 17 00:00:00 2001 From: alvarius Date: Mon, 11 Sep 2023 12:00:55 +0200 Subject: [PATCH] feat(store,world): add ability to unregister hooks (#1422) --- .changeset/dry-chicken-love.md | 18 ++ packages/store/abi/IStore.sol/IStore.abi.json | 18 ++ .../store/abi/IStore.sol/IStore.abi.json.d.ts | 18 ++ .../IStore.sol/IStoreRegistration.abi.json | 18 ++ .../IStoreRegistration.abi.json.d.ts | 18 ++ .../abi/StoreHooks.sol/StoreHooks.abi.json | 1 + .../abi/StoreMock.sol/StoreMock.abi.json | 18 ++ .../abi/StoreMock.sol/StoreMock.abi.json.d.ts | 18 ++ .../StoreReadWithStubs.abi.json | 18 ++ .../StoreReadWithStubs.abi.json.d.ts | 18 ++ packages/store/gas-report.json | 104 ++++---- packages/store/mud.config.ts | 7 +- packages/store/src/Hook.sol | 32 +++ packages/store/src/IStore.sol | 3 + packages/store/src/StoreCore.sol | 27 +- packages/store/src/StoreReadWithStubs.sol | 7 + packages/store/src/StoreSwitch.sol | 9 + packages/store/src/codegen/Tables.sol | 3 +- packages/store/src/codegen/tables/Hooks.sol | 39 ++- .../store/src/codegen/tables/StoreHooks.sol | 252 ++++++++++++++++++ packages/store/test/StoreCore.t.sol | 93 ++++++- packages/store/test/StoreMock.sol | 5 + .../tables/{Hooks.t.sol => StoreHooks.t.sol} | 60 ++--- ...oldLoad.t.sol => StoreHooksColdLoad.t.sol} | 38 +-- .../abi/CoreSystem.sol/CoreSystem.abi.json | 36 +++ .../CoreSystem.sol/CoreSystem.abi.json.d.ts | 36 +++ .../abi/IBaseWorld.sol/IBaseWorld.abi.json | 36 +++ .../IBaseWorld.sol/IBaseWorld.abi.json.d.ts | 36 +++ packages/world/abi/IStore.sol/IStore.abi.json | 18 ++ .../world/abi/IStore.sol/IStore.abi.json.d.ts | 18 ++ .../IStore.sol/IStoreRegistration.abi.json | 18 ++ .../IStoreRegistration.abi.json.d.ts | 18 ++ .../IWorldRegistrationSystem.abi.json | 18 ++ .../IWorldRegistrationSystem.abi.json.d.ts | 18 ++ .../abi/StoreHooks.sol/StoreHooks.abi.json | 1 + .../StoreRegistrationSystem.abi.json | 18 ++ .../StoreRegistrationSystem.abi.json.d.ts | 18 ++ .../WorldRegistrationSystem.abi.json | 18 ++ .../WorldRegistrationSystem.abi.json.d.ts | 18 ++ .../world/abi/src/IStore.sol/IStore.abi.json | 18 ++ .../abi/src/IStore.sol/IStore.abi.json.d.ts | 18 ++ .../IStore.sol/IStoreRegistration.abi.json | 18 ++ .../IStoreRegistration.abi.json.d.ts | 18 ++ packages/world/gas-report.json | 40 +-- .../interfaces/IWorldRegistrationSystem.sol | 2 + .../world/src/modules/core/CoreModule.sol | 4 +- .../StoreRegistrationSystem.sol | 14 +- .../WorldRegistrationSystem.sol | 17 +- packages/world/test/World.t.sol | 184 ++++++++++--- 49 files changed, 1299 insertions(+), 201 deletions(-) create mode 100644 .changeset/dry-chicken-love.md create mode 100644 packages/store/abi/StoreHooks.sol/StoreHooks.abi.json create mode 100644 packages/store/src/codegen/tables/StoreHooks.sol rename packages/store/test/tables/{Hooks.t.sol => StoreHooks.t.sol} (56%) rename packages/store/test/tables/{HooksColdLoad.t.sol => StoreHooksColdLoad.t.sol} (58%) create mode 100644 packages/world/abi/StoreHooks.sol/StoreHooks.abi.json diff --git a/.changeset/dry-chicken-love.md b/.changeset/dry-chicken-love.md new file mode 100644 index 0000000000..5dd3fc13c2 --- /dev/null +++ b/.changeset/dry-chicken-love.md @@ -0,0 +1,18 @@ +--- +"@latticexyz/store": minor +"@latticexyz/world": minor +--- + +It is now possible to unregister Store hooks and System hooks. + +```solidity +interface IStore { + function unregisterStoreHook(bytes32 table, IStoreHook hookAddress) external; + // ... +} + +interface IWorld { + function unregisterSystemHook(bytes32 resourceSelector, ISystemHook hookAddress) external; + // ... +} +``` diff --git a/packages/store/abi/IStore.sol/IStore.abi.json b/packages/store/abi/IStore.sol/IStore.abi.json index 5b408efd0e..48e69022f3 100644 --- a/packages/store/abi/IStore.sol/IStore.abi.json +++ b/packages/store/abi/IStore.sol/IStore.abi.json @@ -618,6 +618,24 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "table", + "type": "bytes32" + }, + { + "internalType": "contract IStoreHook", + "name": "hookAddress", + "type": "address" + } + ], + "name": "unregisterStoreHook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/packages/store/abi/IStore.sol/IStore.abi.json.d.ts b/packages/store/abi/IStore.sol/IStore.abi.json.d.ts index 7d43da2fd0..9838b2e184 100644 --- a/packages/store/abi/IStore.sol/IStore.abi.json.d.ts +++ b/packages/store/abi/IStore.sol/IStore.abi.json.d.ts @@ -618,6 +618,24 @@ declare const abi: [ stateMutability: "nonpayable"; type: "function"; }, + { + inputs: [ + { + internalType: "bytes32"; + name: "table"; + type: "bytes32"; + }, + { + internalType: "contract IStoreHook"; + name: "hookAddress"; + type: "address"; + } + ]; + name: "unregisterStoreHook"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; + }, { inputs: [ { diff --git a/packages/store/abi/IStore.sol/IStoreRegistration.abi.json b/packages/store/abi/IStore.sol/IStoreRegistration.abi.json index feac4849a3..8199933edd 100644 --- a/packages/store/abi/IStore.sol/IStoreRegistration.abi.json +++ b/packages/store/abi/IStore.sol/IStoreRegistration.abi.json @@ -54,5 +54,23 @@ "outputs": [], "stateMutability": "nonpayable", "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "table", + "type": "bytes32" + }, + { + "internalType": "contract IStoreHook", + "name": "hookAddress", + "type": "address" + } + ], + "name": "unregisterStoreHook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" } ] \ No newline at end of file diff --git a/packages/store/abi/IStore.sol/IStoreRegistration.abi.json.d.ts b/packages/store/abi/IStore.sol/IStoreRegistration.abi.json.d.ts index c03b1d5863..02db08cbf2 100644 --- a/packages/store/abi/IStore.sol/IStoreRegistration.abi.json.d.ts +++ b/packages/store/abi/IStore.sol/IStoreRegistration.abi.json.d.ts @@ -54,6 +54,24 @@ declare const abi: [ outputs: []; stateMutability: "nonpayable"; type: "function"; + }, + { + inputs: [ + { + internalType: "bytes32"; + name: "table"; + type: "bytes32"; + }, + { + internalType: "contract IStoreHook"; + name: "hookAddress"; + type: "address"; + } + ]; + name: "unregisterStoreHook"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; } ]; export default abi; diff --git a/packages/store/abi/StoreHooks.sol/StoreHooks.abi.json b/packages/store/abi/StoreHooks.sol/StoreHooks.abi.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/packages/store/abi/StoreHooks.sol/StoreHooks.abi.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/packages/store/abi/StoreMock.sol/StoreMock.abi.json b/packages/store/abi/StoreMock.sol/StoreMock.abi.json index d124b7950d..276498c2ca 100644 --- a/packages/store/abi/StoreMock.sol/StoreMock.abi.json +++ b/packages/store/abi/StoreMock.sol/StoreMock.abi.json @@ -666,6 +666,24 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "table", + "type": "bytes32" + }, + { + "internalType": "contract IStoreHook", + "name": "hookAddress", + "type": "address" + } + ], + "name": "unregisterStoreHook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/packages/store/abi/StoreMock.sol/StoreMock.abi.json.d.ts b/packages/store/abi/StoreMock.sol/StoreMock.abi.json.d.ts index 59c9e4b6bc..c04f073704 100644 --- a/packages/store/abi/StoreMock.sol/StoreMock.abi.json.d.ts +++ b/packages/store/abi/StoreMock.sol/StoreMock.abi.json.d.ts @@ -666,6 +666,24 @@ declare const abi: [ stateMutability: "nonpayable"; type: "function"; }, + { + inputs: [ + { + internalType: "bytes32"; + name: "table"; + type: "bytes32"; + }, + { + internalType: "contract IStoreHook"; + name: "hookAddress"; + type: "address"; + } + ]; + name: "unregisterStoreHook"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; + }, { inputs: [ { diff --git a/packages/store/abi/StoreReadWithStubs.sol/StoreReadWithStubs.abi.json b/packages/store/abi/StoreReadWithStubs.sol/StoreReadWithStubs.abi.json index fce41a3237..58e66783e2 100644 --- a/packages/store/abi/StoreReadWithStubs.sol/StoreReadWithStubs.abi.json +++ b/packages/store/abi/StoreReadWithStubs.sol/StoreReadWithStubs.abi.json @@ -660,6 +660,24 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + }, + { + "internalType": "contract IStoreHook", + "name": "", + "type": "address" + } + ], + "name": "unregisterStoreHook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/packages/store/abi/StoreReadWithStubs.sol/StoreReadWithStubs.abi.json.d.ts b/packages/store/abi/StoreReadWithStubs.sol/StoreReadWithStubs.abi.json.d.ts index 582bb42f9f..53d8067ddb 100644 --- a/packages/store/abi/StoreReadWithStubs.sol/StoreReadWithStubs.abi.json.d.ts +++ b/packages/store/abi/StoreReadWithStubs.sol/StoreReadWithStubs.abi.json.d.ts @@ -660,6 +660,24 @@ declare const abi: [ stateMutability: "nonpayable"; type: "function"; }, + { + inputs: [ + { + internalType: "bytes32"; + name: ""; + type: "bytes32"; + }, + { + internalType: "contract IStoreHook"; + name: ""; + type: "address"; + } + ]; + name: "unregisterStoreHook"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; + }, { inputs: [ { diff --git a/packages/store/gas-report.json b/packages/store/gas-report.json index d5d29f495e..dcfe0a654a 100644 --- a/packages/store/gas-report.json +++ b/packages/store/gas-report.json @@ -249,7 +249,7 @@ "file": "test/KeyEncoding.t.sol", "test": "testRegisterAndGetSchema", "name": "register KeyEncoding schema", - "gasUsed": 669576 + "gasUsed": 669560 }, { "file": "test/Mixed.t.sol", @@ -261,7 +261,7 @@ "file": "test/Mixed.t.sol", "test": "testRegisterAndGetSchema", "name": "register Mixed schema", - "gasUsed": 531309 + "gasUsed": 531268 }, { "file": "test/Mixed.t.sol", @@ -579,49 +579,49 @@ "file": "test/StoreCoreGas.t.sol", "test": "testHooks", "name": "register subscriber", - "gasUsed": 60581 + "gasUsed": 60584 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooks", "name": "set record on table with subscriber", - "gasUsed": 70977 + "gasUsed": 70983 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooks", "name": "set static field on table with subscriber", - "gasUsed": 24327 + "gasUsed": 24290 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooks", "name": "delete record on table with subscriber", - "gasUsed": 19373 + "gasUsed": 19379 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooksDynamicData", "name": "register subscriber", - "gasUsed": 60581 + "gasUsed": 60584 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooksDynamicData", "name": "set (dynamic) record on table with subscriber", - "gasUsed": 163845 + "gasUsed": 163851 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooksDynamicData", "name": "set (dynamic) field on table with subscriber", - "gasUsed": 26263 + "gasUsed": 26226 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooksDynamicData", "name": "delete (dynamic) record on table with subscriber", - "gasUsed": 20846 + "gasUsed": 20852 }, { "file": "test/StoreCoreGas.t.sol", @@ -840,105 +840,105 @@ "gasUsed": 37650 }, { - "file": "test/tables/Hooks.t.sol", + "file": "test/tables/StoreHooks.t.sol", "test": "testOneSlot", - "name": "Hooks: set field with one elements (cold)", + "name": "StoreHooks: set field with one elements (cold)", "gasUsed": 60196 }, { - "file": "test/tables/Hooks.t.sol", + "file": "test/tables/StoreHooks.t.sol", "test": "testTable", - "name": "Hooks: set field (cold)", - "gasUsed": 60192 + "name": "StoreHooks: set field (cold)", + "gasUsed": 60196 }, { - "file": "test/tables/Hooks.t.sol", + "file": "test/tables/StoreHooks.t.sol", "test": "testTable", - "name": "Hooks: get field (warm)", + "name": "StoreHooks: get field (warm)", "gasUsed": 4573 }, { - "file": "test/tables/Hooks.t.sol", + "file": "test/tables/StoreHooks.t.sol", "test": "testTable", - "name": "Hooks: push 1 element (cold)", - "gasUsed": 17735 + "name": "StoreHooks: push 1 element (cold)", + "gasUsed": 17731 }, { - "file": "test/tables/Hooks.t.sol", + "file": "test/tables/StoreHooks.t.sol", "test": "testTable", - "name": "Hooks: pop 1 element (warm)", - "gasUsed": 14110 + "name": "StoreHooks: pop 1 element (warm)", + "gasUsed": 14111 }, { - "file": "test/tables/Hooks.t.sol", + "file": "test/tables/StoreHooks.t.sol", "test": "testTable", - "name": "Hooks: push 1 element (warm)", + "name": "StoreHooks: push 1 element (warm)", "gasUsed": 15793 }, { - "file": "test/tables/Hooks.t.sol", + "file": "test/tables/StoreHooks.t.sol", "test": "testTable", - "name": "Hooks: update 1 element (warm)", - "gasUsed": 36142 + "name": "StoreHooks: update 1 element (warm)", + "gasUsed": 36144 }, { - "file": "test/tables/Hooks.t.sol", + "file": "test/tables/StoreHooks.t.sol", "test": "testTable", - "name": "Hooks: delete record (warm)", - "gasUsed": 9818 + "name": "StoreHooks: delete record (warm)", + "gasUsed": 9820 }, { - "file": "test/tables/Hooks.t.sol", + "file": "test/tables/StoreHooks.t.sol", "test": "testTable", - "name": "Hooks: set field (warm)", - "gasUsed": 32418 + "name": "StoreHooks: set field (warm)", + "gasUsed": 32426 }, { - "file": "test/tables/Hooks.t.sol", + "file": "test/tables/StoreHooks.t.sol", "test": "testThreeSlots", - "name": "Hooks: set field with three elements (cold)", - "gasUsed": 82887 + "name": "StoreHooks: set field with three elements (cold)", + "gasUsed": 82884 }, { - "file": "test/tables/Hooks.t.sol", + "file": "test/tables/StoreHooks.t.sol", "test": "testTwoSlots", - "name": "Hooks: set field with two elements (cold)", + "name": "StoreHooks: set field with two elements (cold)", "gasUsed": 82795 }, { - "file": "test/tables/HooksColdLoad.t.sol", + "file": "test/tables/StoreHooksColdLoad.t.sol", "test": "testDelete", - "name": "Hooks: delete record (cold)", + "name": "StoreHooks: delete record (cold)", "gasUsed": 18613 }, { - "file": "test/tables/HooksColdLoad.t.sol", + "file": "test/tables/StoreHooksColdLoad.t.sol", "test": "testGet", - "name": "Hooks: get field (cold)", - "gasUsed": 10562 + "name": "StoreHooks: get field (cold)", + "gasUsed": 10566 }, { - "file": "test/tables/HooksColdLoad.t.sol", + "file": "test/tables/StoreHooksColdLoad.t.sol", "test": "testGetItem", - "name": "Hooks: get 1 element (cold)", + "name": "StoreHooks: get 1 element (cold)", "gasUsed": 7080 }, { - "file": "test/tables/HooksColdLoad.t.sol", + "file": "test/tables/StoreHooksColdLoad.t.sol", "test": "testLength", - "name": "Hooks: get length (cold)", + "name": "StoreHooks: get length (cold)", "gasUsed": 6780 }, { - "file": "test/tables/HooksColdLoad.t.sol", + "file": "test/tables/StoreHooksColdLoad.t.sol", "test": "testPop", - "name": "Hooks: pop 1 element (cold)", + "name": "StoreHooks: pop 1 element (cold)", "gasUsed": 24235 }, { - "file": "test/tables/HooksColdLoad.t.sol", + "file": "test/tables/StoreHooksColdLoad.t.sol", "test": "testUpdate", - "name": "Hooks: update 1 element (cold)", + "name": "StoreHooks: update 1 element (cold)", "gasUsed": 25811 }, { diff --git a/packages/store/mud.config.ts b/packages/store/mud.config.ts index bfaa6a5552..80d775bf29 100644 --- a/packages/store/mud.config.ts +++ b/packages/store/mud.config.ts @@ -7,7 +7,7 @@ export default mudConfig({ ExampleEnum: ["None", "First", "Second", "Third"], }, tables: { - Hooks: "bytes21[]", + StoreHooks: "bytes21[]", Callbacks: "bytes24[]", Tables: { keySchema: { @@ -20,6 +20,11 @@ export default mudConfig({ abiEncodedFieldNames: "bytes", }, }, + // The Hooks table is a generic table used by the `filterFromList` util in `Hook.sol` + Hooks: { + schema: "bytes21[]", + tableIdArgument: true, + }, // TODO: move these test tables to a separate mud config Mixed: { schema: { diff --git a/packages/store/src/Hook.sol b/packages/store/src/Hook.sol index 2e7c28f011..876f7e7e52 100644 --- a/packages/store/src/Hook.sol +++ b/packages/store/src/Hook.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; +import { Hooks } from "./codegen/tables/Hooks.sol"; + // 20 bytes address, 1 byte bitmap of enabled hooks type Hook is bytes21; @@ -14,6 +16,36 @@ library HookLib { // Move the address to the leftmost 20 bytes and the bitmap to the rightmost byte return Hook.wrap(bytes21(bytes20(hookAddress)) | bytes21(uint168(encodedHooks))); } + + /** + * Filter the given hook from the hook list at the given key in the given hook table + */ + function filterListByAddress(bytes32 hookTableId, bytes32 key, address hookAddressToRemove) internal { + bytes21[] memory currentHooks = Hooks.get(hookTableId, key); + + // Initialize the new hooks array with the same length because we don't know if the hook is registered yet + bytes21[] memory newHooks = new bytes21[](currentHooks.length); + + // Filter the array of current hooks + uint256 newHooksIndex; + unchecked { + for (uint256 currentHooksIndex; currentHooksIndex < currentHooks.length; currentHooksIndex++) { + if (Hook.wrap(currentHooks[currentHooksIndex]).getAddress() != address(hookAddressToRemove)) { + newHooks[newHooksIndex] = currentHooks[currentHooksIndex]; + newHooksIndex++; + } + } + } + + // Set the new hooks table length in place + // (Note: this does not update the free memory pointer) + assembly { + mstore(newHooks, newHooksIndex) + } + + // Set the new hooks table + Hooks.set(hookTableId, key, newHooks); + } } library HookInstance { diff --git a/packages/store/src/IStore.sol b/packages/store/src/IStore.sol index 147aeab0f8..988e9c5b25 100644 --- a/packages/store/src/IStore.sol +++ b/packages/store/src/IStore.sol @@ -124,6 +124,9 @@ interface IStoreRegistration { // Register hook to be called when a record or field is set or deleted function registerStoreHook(bytes32 table, IStoreHook hookAddress, uint8 enabledHooksBitmap) external; + + // Unregister a hook for the given tableId + function unregisterStoreHook(bytes32 table, IStoreHook hookAddress) external; } interface IStore is IStoreData, IStoreRegistration, IStoreEphemeral, IStoreErrors {} diff --git a/packages/store/src/StoreCore.sol b/packages/store/src/StoreCore.sol index b8b46ba11f..e95c2dc2c8 100644 --- a/packages/store/src/StoreCore.sol +++ b/packages/store/src/StoreCore.sol @@ -8,11 +8,11 @@ import { Memory } from "./Memory.sol"; import { Schema, SchemaLib } from "./Schema.sol"; import { PackedCounter } from "./PackedCounter.sol"; import { Slice, SliceLib } from "./Slice.sol"; -import { Hooks, Tables, HooksTableId } from "./codegen/Tables.sol"; +import { StoreHooks, Tables, StoreHooksTableId } from "./codegen/Tables.sol"; import { IStoreErrors } from "./IStoreErrors.sol"; import { IStoreHook } from "./IStore.sol"; import { StoreSwitch } from "./StoreSwitch.sol"; -import { Hook } from "./Hook.sol"; +import { Hook, HookLib } from "./Hook.sol"; import { StoreHookLib, StoreHookType } from "./StoreHook.sol"; library StoreCore { @@ -34,7 +34,7 @@ library StoreCore { // Register internal tables Tables.register(); - Hooks.register(); + StoreHooks.register(); } /************************************************************************ @@ -120,7 +120,14 @@ library StoreCore { * Register hooks to be called when a record or field is set or deleted */ function registerStoreHook(bytes32 tableId, IStoreHook hookAddress, uint8 enabledHooksBitmap) internal { - Hooks.push(tableId, Hook.unwrap(StoreHookLib.encode(hookAddress, enabledHooksBitmap))); + StoreHooks.push(tableId, Hook.unwrap(StoreHookLib.encode(hookAddress, enabledHooksBitmap))); + } + + /** + * Unregister a hook from the given tableId + */ + function unregisterStoreHook(bytes32 tableId, IStoreHook hookAddress) internal { + HookLib.filterListByAddress(StoreHooksTableId, tableId, address(hookAddress)); } /************************************************************************ @@ -143,7 +150,7 @@ library StoreCore { emit StoreSetRecord(tableId, key, data); // Call onBeforeSetRecord hooks (before actually modifying the state, so observers have access to the previous state if needed) - bytes21[] memory hooks = Hooks.get(tableId); + bytes21[] memory hooks = StoreHooks.get(tableId); for (uint256 i; i < hooks.length; i++) { Hook hook = Hook.wrap(hooks[i]); if (hook.isEnabled(uint8(StoreHookType.BEFORE_SET_RECORD))) { @@ -210,7 +217,7 @@ library StoreCore { emit StoreSetField(tableId, key, schemaIndex, data); // Call onBeforeSetField hooks (before modifying the state) - bytes21[] memory hooks = Hooks.get(tableId); + bytes21[] memory hooks = StoreHooks.get(tableId); for (uint256 i; i < hooks.length; i++) { Hook hook = Hook.wrap(hooks[i]); if (hook.isEnabled(uint8(StoreHookType.BEFORE_SET_FIELD))) { @@ -241,7 +248,7 @@ library StoreCore { emit StoreDeleteRecord(tableId, key); // Call onBeforeDeleteRecord hooks (before actually modifying the state, so observers have access to the previous state if needed) - bytes21[] memory hooks = Hooks.get(tableId); + bytes21[] memory hooks = StoreHooks.get(tableId); for (uint256 i; i < hooks.length; i++) { Hook hook = Hook.wrap(hooks[i]); if (hook.isEnabled(uint8(StoreHookType.BEFORE_DELETE_RECORD))) { @@ -292,7 +299,7 @@ library StoreCore { emit StoreSetField(tableId, key, schemaIndex, fullData); // Call onBeforeSetField hooks (before modifying the state) - bytes21[] memory hooks = Hooks.get(tableId); + bytes21[] memory hooks = StoreHooks.get(tableId); for (uint256 i; i < hooks.length; i++) { Hook hook = Hook.wrap(hooks[i]); if (hook.isEnabled(uint8(StoreHookType.BEFORE_SET_FIELD))) { @@ -336,7 +343,7 @@ library StoreCore { emit StoreSetField(tableId, key, schemaIndex, fullData); // Call onBeforeSetField hooks (before modifying the state) - bytes21[] memory hooks = Hooks.get(tableId); + bytes21[] memory hooks = StoreHooks.get(tableId); for (uint256 i; i < hooks.length; i++) { Hook hook = Hook.wrap(hooks[i]); if (hook.isEnabled(uint8(StoreHookType.BEFORE_SET_FIELD))) { @@ -390,7 +397,7 @@ library StoreCore { emit StoreSetField(tableId, key, schemaIndex, fullData); // Call onBeforeSetField hooks (before modifying the state) - bytes21[] memory hooks = Hooks.get(tableId); + bytes21[] memory hooks = StoreHooks.get(tableId); for (uint256 i; i < hooks.length; i++) { Hook hook = Hook.wrap(hooks[i]); if (hook.isEnabled(uint8(StoreHookType.BEFORE_SET_FIELD))) { diff --git a/packages/store/src/StoreReadWithStubs.sol b/packages/store/src/StoreReadWithStubs.sol index 761cef9231..32a03bbdea 100644 --- a/packages/store/src/StoreReadWithStubs.sol +++ b/packages/store/src/StoreReadWithStubs.sol @@ -63,6 +63,13 @@ contract StoreReadWithStubs is IStore, StoreRead { revert StoreReadWithStubs_NotImplemented(); } + /** + * Not implemented in StoreReadWithStubs + */ + function unregisterStoreHook(bytes32, IStoreHook) public virtual { + revert StoreReadWithStubs_NotImplemented(); + } + /** * Not implemented in StoreReadWithStubs */ diff --git a/packages/store/src/StoreSwitch.sol b/packages/store/src/StoreSwitch.sol index 2df6755858..e165e3a161 100644 --- a/packages/store/src/StoreSwitch.sol +++ b/packages/store/src/StoreSwitch.sol @@ -53,6 +53,15 @@ library StoreSwitch { } } + function unregisterStoreHook(bytes32 table, IStoreHook hookAddress) internal { + address _storeAddress = getStoreAddress(); + if (_storeAddress == address(this)) { + StoreCore.unregisterStoreHook(table, hookAddress); + } else { + IStore(_storeAddress).unregisterStoreHook(table, hookAddress); + } + } + function getValueSchema(bytes32 table) internal view returns (Schema valueSchema) { address _storeAddress = getStoreAddress(); if (_storeAddress == address(this)) { diff --git a/packages/store/src/codegen/Tables.sol b/packages/store/src/codegen/Tables.sol index 00c5e8a600..64381605a6 100644 --- a/packages/store/src/codegen/Tables.sol +++ b/packages/store/src/codegen/Tables.sol @@ -3,9 +3,10 @@ pragma solidity >=0.8.0; /* Autogenerated file. Do not edit manually. */ -import { Hooks, HooksTableId } from "./tables/Hooks.sol"; +import { StoreHooks, StoreHooksTableId } from "./tables/StoreHooks.sol"; import { Callbacks, CallbacksTableId } from "./tables/Callbacks.sol"; import { Tables, TablesData, TablesTableId } from "./tables/Tables.sol"; +import { Hooks } from "./tables/Hooks.sol"; import { Mixed, MixedData, MixedTableId } from "./tables/Mixed.sol"; import { Vector2, Vector2Data, Vector2TableId } from "./tables/Vector2.sol"; import { KeyEncoding, KeyEncodingTableId } from "./tables/KeyEncoding.sol"; diff --git a/packages/store/src/codegen/tables/Hooks.sol b/packages/store/src/codegen/tables/Hooks.sol index 37d268c143..ae4d488386 100644 --- a/packages/store/src/codegen/tables/Hooks.sol +++ b/packages/store/src/codegen/tables/Hooks.sol @@ -17,9 +17,6 @@ import { EncodeArray } from "../../tightcoder/EncodeArray.sol"; import { Schema, SchemaLib } from "../../Schema.sol"; import { PackedCounter, PackedCounterLib } from "../../PackedCounter.sol"; -bytes32 constant _tableId = bytes32(abi.encodePacked(bytes16("mudstore"), bytes16("Hooks"))); -bytes32 constant HooksTableId = _tableId; - library Hooks { /** Get the table's key schema */ function getKeySchema() internal pure returns (Schema) { @@ -50,17 +47,17 @@ library Hooks { } /** Register the table's key schema, value schema, key names and value names */ - function register() internal { + 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) internal { + function register(IStore _store, bytes32 _tableId) internal { _store.registerTable(_tableId, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames()); } /** Get value */ - function get(bytes32 key) internal view returns (bytes21[] memory value) { + function get(bytes32 _tableId, bytes32 key) internal view returns (bytes21[] memory value) { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = key; @@ -69,7 +66,7 @@ library Hooks { } /** Get value (using the specified store) */ - function get(IStore _store, bytes32 key) internal view returns (bytes21[] memory value) { + function get(IStore _store, bytes32 _tableId, bytes32 key) internal view returns (bytes21[] memory value) { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = key; @@ -78,7 +75,7 @@ library Hooks { } /** Set value */ - function set(bytes32 key, bytes21[] memory value) internal { + function set(bytes32 _tableId, bytes32 key, bytes21[] memory value) internal { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = key; @@ -86,7 +83,7 @@ library Hooks { } /** Set value (using the specified store) */ - function set(IStore _store, bytes32 key, bytes21[] memory value) internal { + function set(IStore _store, bytes32 _tableId, bytes32 key, bytes21[] memory value) internal { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = key; @@ -94,7 +91,7 @@ library Hooks { } /** Get the length of value */ - function length(bytes32 key) internal view returns (uint256) { + function length(bytes32 _tableId, bytes32 key) internal view returns (uint256) { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = key; @@ -105,7 +102,7 @@ library Hooks { } /** Get the length of value (using the specified store) */ - function length(IStore _store, bytes32 key) internal view returns (uint256) { + function length(IStore _store, bytes32 _tableId, bytes32 key) internal view returns (uint256) { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = key; @@ -119,7 +116,7 @@ library Hooks { * Get an item of value * (unchecked, returns invalid data if index overflows) */ - function getItem(bytes32 key, uint256 _index) internal view returns (bytes21) { + function getItem(bytes32 _tableId, bytes32 key, uint256 _index) internal view returns (bytes21) { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = key; @@ -140,7 +137,7 @@ library Hooks { * Get an item of value (using the specified store) * (unchecked, returns invalid data if index overflows) */ - function getItem(IStore _store, bytes32 key, uint256 _index) internal view returns (bytes21) { + function getItem(IStore _store, bytes32 _tableId, bytes32 key, uint256 _index) internal view returns (bytes21) { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = key; @@ -158,7 +155,7 @@ library Hooks { } /** Push an element to value */ - function push(bytes32 key, bytes21 _element) internal { + function push(bytes32 _tableId, bytes32 key, bytes21 _element) internal { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = key; @@ -166,7 +163,7 @@ library Hooks { } /** Push an element to value (using the specified store) */ - function push(IStore _store, bytes32 key, bytes21 _element) internal { + function push(IStore _store, bytes32 _tableId, bytes32 key, bytes21 _element) internal { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = key; @@ -174,7 +171,7 @@ library Hooks { } /** Pop an element from value */ - function pop(bytes32 key) internal { + function pop(bytes32 _tableId, bytes32 key) internal { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = key; @@ -182,7 +179,7 @@ library Hooks { } /** Pop an element from value (using the specified store) */ - function pop(IStore _store, bytes32 key) internal { + function pop(IStore _store, bytes32 _tableId, bytes32 key) internal { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = key; @@ -193,7 +190,7 @@ library Hooks { * Update an element of value at `_index` * (checked only to prevent modifying other tables; can corrupt own data if index overflows) */ - function update(bytes32 key, uint256 _index, bytes21 _element) internal { + function update(bytes32 _tableId, bytes32 key, uint256 _index, bytes21 _element) internal { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = key; @@ -206,7 +203,7 @@ library Hooks { * Update an element of value (using the specified store) at `_index` * (checked only to prevent modifying other tables; can corrupt own data if index overflows) */ - function update(IStore _store, bytes32 key, uint256 _index, bytes21 _element) internal { + function update(IStore _store, bytes32 _tableId, bytes32 key, uint256 _index, bytes21 _element) internal { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = key; @@ -235,7 +232,7 @@ library Hooks { } /* Delete all data for given keys */ - function deleteRecord(bytes32 key) internal { + function deleteRecord(bytes32 _tableId, bytes32 key) internal { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = key; @@ -243,7 +240,7 @@ library Hooks { } /* Delete all data for given keys (using the specified store) */ - function deleteRecord(IStore _store, bytes32 key) internal { + function deleteRecord(IStore _store, bytes32 _tableId, bytes32 key) internal { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = key; diff --git a/packages/store/src/codegen/tables/StoreHooks.sol b/packages/store/src/codegen/tables/StoreHooks.sol new file mode 100644 index 0000000000..9998edbfd4 --- /dev/null +++ b/packages/store/src/codegen/tables/StoreHooks.sol @@ -0,0 +1,252 @@ +// 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 "../../IStore.sol"; +import { StoreSwitch } from "../../StoreSwitch.sol"; +import { StoreCore } from "../../StoreCore.sol"; +import { Bytes } from "../../Bytes.sol"; +import { Memory } from "../../Memory.sol"; +import { SliceLib } from "../../Slice.sol"; +import { EncodeArray } from "../../tightcoder/EncodeArray.sol"; +import { Schema, SchemaLib } from "../../Schema.sol"; +import { PackedCounter, PackedCounterLib } from "../../PackedCounter.sol"; + +bytes32 constant _tableId = bytes32(abi.encodePacked(bytes16("mudstore"), bytes16("StoreHooks"))); +bytes32 constant StoreHooksTableId = _tableId; + +library StoreHooks { + /** Get the table's key schema */ + function getKeySchema() internal pure returns (Schema) { + SchemaType[] memory _schema = new SchemaType[](1); + _schema[0] = 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.BYTES21_ARRAY; + + return SchemaLib.encode(_schema); + } + + /** Get the table's key names */ + function getKeyNames() internal pure returns (string[] memory keyNames) { + keyNames = new string[](1); + keyNames[0] = "key"; + } + + /** Get the table's field names */ + function getFieldNames() internal pure returns (string[] memory fieldNames) { + fieldNames = new string[](1); + fieldNames[0] = "value"; + } + + /** 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 value */ + function get(bytes32 key) internal view returns (bytes21[] memory value) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = key; + + bytes memory _blob = StoreSwitch.getField(_tableId, _keyTuple, 0, getValueSchema()); + return (SliceLib.getSubslice(_blob, 0, _blob.length).decodeArray_bytes21()); + } + + /** Get value (using the specified store) */ + function get(IStore _store, bytes32 key) internal view returns (bytes21[] memory value) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = key; + + bytes memory _blob = _store.getField(_tableId, _keyTuple, 0, getValueSchema()); + return (SliceLib.getSubslice(_blob, 0, _blob.length).decodeArray_bytes21()); + } + + /** Set value */ + function set(bytes32 key, bytes21[] memory value) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = key; + + StoreSwitch.setField(_tableId, _keyTuple, 0, EncodeArray.encode((value)), getValueSchema()); + } + + /** Set value (using the specified store) */ + function set(IStore _store, bytes32 key, bytes21[] memory value) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = key; + + _store.setField(_tableId, _keyTuple, 0, EncodeArray.encode((value)), getValueSchema()); + } + + /** Get the length of value */ + function length(bytes32 key) internal view returns (uint256) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = key; + + uint256 _byteLength = StoreSwitch.getFieldLength(_tableId, _keyTuple, 0, getValueSchema()); + unchecked { + return _byteLength / 21; + } + } + + /** Get the length of value (using the specified store) */ + function length(IStore _store, bytes32 key) internal view returns (uint256) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = key; + + uint256 _byteLength = _store.getFieldLength(_tableId, _keyTuple, 0, getValueSchema()); + unchecked { + return _byteLength / 21; + } + } + + /** + * Get an item of value + * (unchecked, returns invalid data if index overflows) + */ + function getItem(bytes32 key, uint256 _index) internal view returns (bytes21) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = key; + + unchecked { + bytes memory _blob = StoreSwitch.getFieldSlice( + _tableId, + _keyTuple, + 0, + getValueSchema(), + _index * 21, + (_index + 1) * 21 + ); + return (Bytes.slice21(_blob, 0)); + } + } + + /** + * Get an item of value (using the specified store) + * (unchecked, returns invalid data if index overflows) + */ + function getItem(IStore _store, bytes32 key, uint256 _index) internal view returns (bytes21) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = key; + + unchecked { + bytes memory _blob = _store.getFieldSlice( + _tableId, + _keyTuple, + 0, + getValueSchema(), + _index * 21, + (_index + 1) * 21 + ); + return (Bytes.slice21(_blob, 0)); + } + } + + /** Push an element to value */ + function push(bytes32 key, bytes21 _element) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = key; + + StoreSwitch.pushToField(_tableId, _keyTuple, 0, abi.encodePacked((_element)), getValueSchema()); + } + + /** Push an element to value (using the specified store) */ + function push(IStore _store, bytes32 key, bytes21 _element) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = key; + + _store.pushToField(_tableId, _keyTuple, 0, abi.encodePacked((_element)), getValueSchema()); + } + + /** Pop an element from value */ + function pop(bytes32 key) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = key; + + StoreSwitch.popFromField(_tableId, _keyTuple, 0, 21, getValueSchema()); + } + + /** Pop an element from value (using the specified store) */ + function pop(IStore _store, bytes32 key) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = key; + + _store.popFromField(_tableId, _keyTuple, 0, 21, getValueSchema()); + } + + /** + * Update an element of value at `_index` + * (checked only to prevent modifying other tables; can corrupt own data if index overflows) + */ + function update(bytes32 key, uint256 _index, bytes21 _element) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = key; + + unchecked { + StoreSwitch.updateInField(_tableId, _keyTuple, 0, _index * 21, abi.encodePacked((_element)), getValueSchema()); + } + } + + /** + * Update an element of value (using the specified store) at `_index` + * (checked only to prevent modifying other tables; can corrupt own data if index overflows) + */ + function update(IStore _store, bytes32 key, uint256 _index, bytes21 _element) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = key; + + unchecked { + _store.updateInField(_tableId, _keyTuple, 0, _index * 21, abi.encodePacked((_element)), getValueSchema()); + } + } + + /** Tightly pack full data using this table's schema */ + function encode(bytes21[] memory value) internal pure returns (bytes memory) { + PackedCounter _encodedLengths; + // Lengths are effectively checked during copy by 2**40 bytes exceeding gas limits + unchecked { + _encodedLengths = PackedCounterLib.pack(value.length * 21); + } + + return abi.encodePacked(_encodedLengths.unwrap(), EncodeArray.encode((value))); + } + + /** Encode keys as a bytes32 array using this table's schema */ + function encodeKeyTuple(bytes32 key) internal pure returns (bytes32[] memory) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = key; + + return _keyTuple; + } + + /* Delete all data for given keys */ + function deleteRecord(bytes32 key) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = key; + + StoreSwitch.deleteRecord(_tableId, _keyTuple, getValueSchema()); + } + + /* Delete all data for given keys (using the specified store) */ + function deleteRecord(IStore _store, bytes32 key) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = key; + + _store.deleteRecord(_tableId, _keyTuple, getValueSchema()); + } +} diff --git a/packages/store/test/StoreCore.t.sol b/packages/store/test/StoreCore.t.sol index 2f6f4240b1..add7fa5f77 100644 --- a/packages/store/test/StoreCore.t.sol +++ b/packages/store/test/StoreCore.t.sol @@ -18,6 +18,8 @@ import { StoreHookLib } from "../src/StoreHook.sol"; import { SchemaEncodeHelper } from "./SchemaEncodeHelper.sol"; import { StoreMock } from "./StoreMock.sol"; import { MirrorSubscriber, indexerTableId } from "./MirrorSubscriber.sol"; +import { RevertSubscriber } from "./RevertSubscriber.sol"; +import { EchoSubscriber } from "./EchoSubscriber.sol"; struct TestStruct { uint128 firstData; @@ -27,6 +29,7 @@ struct TestStruct { contract StoreCoreTest is Test, StoreMock { TestStruct private testStruct; + event HookCalled(bytes); mapping(uint256 => bytes) private testMapping; Schema defaultKeySchema = SchemaEncodeHelper.encode(SchemaType.BYTES32); @@ -821,7 +824,7 @@ contract StoreCoreTest is Test, StoreMock { assertEq(data3Slice.length, 0); } - function testHooks() public { + function testRegisterHook() public { bytes32 table = keccak256("some.table"); bytes32[] memory key = new bytes32[](1); key[0] = keccak256("some key"); @@ -875,6 +878,94 @@ contract StoreCoreTest is Test, StoreMock { assertEq(keccak256(indexedData), keccak256(abi.encodePacked(bytes16(0)))); } + function testUnregisterHook() public { + bytes32 table = keccak256("some.table"); + bytes32[] memory key = new bytes32[](1); + key[0] = keccak256("some key"); + + // Register table's schema + Schema valueSchema = SchemaEncodeHelper.encode(SchemaType.UINT128); + IStore(this).registerTable(table, defaultKeySchema, valueSchema, new string[](1), new string[](1)); + + // Create a RevertSubscriber and an EchoSubscriber + RevertSubscriber revertSubscriber = new RevertSubscriber(); + EchoSubscriber echoSubscriber = new EchoSubscriber(); + + // Register both subscribers + IStore(this).registerStoreHook( + table, + revertSubscriber, + StoreHookLib.encodeBitmap({ + onBeforeSetRecord: true, + onAfterSetRecord: true, + onBeforeSetField: true, + onAfterSetField: true, + onBeforeDeleteRecord: true, + onAfterDeleteRecord: true + }) + ); + // Register both subscribers + IStore(this).registerStoreHook( + table, + echoSubscriber, + StoreHookLib.encodeBitmap({ + onBeforeSetRecord: true, + onAfterSetRecord: true, + onBeforeSetField: true, + onAfterSetField: true, + onBeforeDeleteRecord: true, + onAfterDeleteRecord: true + }) + ); + + bytes memory data = abi.encodePacked(bytes16(0x0102030405060708090a0b0c0d0e0f10)); + + // Expect a revert when the RevertSubscriber's onBeforeSetRecord hook is called + vm.expectRevert(bytes("onBeforeSetRecord")); + IStore(this).setRecord(table, key, data, valueSchema); + + // Expect a revert when the RevertSubscriber's onBeforeSetField hook is called + vm.expectRevert(bytes("onBeforeSetField")); + IStore(this).setField(table, key, 0, data, valueSchema); + + // Expect a revert when the RevertSubscriber's onBeforeDeleteRecord hook is called + vm.expectRevert(bytes("onBeforeDeleteRecord")); + IStore(this).deleteRecord(table, key, valueSchema); + + // Unregister the RevertSubscriber + IStore(this).unregisterStoreHook(table, revertSubscriber); + + // Expect a HookCalled event to be emitted when the EchoSubscriber's onBeforeSetRecord hook is called + vm.expectEmit(true, true, true, true); + emit HookCalled(abi.encode(table, key, data, valueSchema)); + + // Expect a HookCalled event to be emitted when the EchoSubscriber's onAfterSetRecord hook is called + vm.expectEmit(true, true, true, true); + emit HookCalled(abi.encode(table, key, data, valueSchema)); + + IStore(this).setRecord(table, key, data, valueSchema); + + // Expect a HookCalled event to be emitted when the EchoSubscriber's onBeforeSetField hook is called + vm.expectEmit(true, true, true, true); + emit HookCalled(abi.encode(table, key, uint8(0), data, valueSchema)); + + // Expect a HookCalled event to be emitted when the EchoSubscriber's onAfterSetField hook is called + vm.expectEmit(true, true, true, true); + emit HookCalled(abi.encode(table, key, uint8(0), data, valueSchema)); + + IStore(this).setField(table, key, 0, data, valueSchema); + + // Expect a HookCalled event to be emitted when the EchoSubscriber's onBeforeDeleteRecord hook is called + vm.expectEmit(true, true, true, true); + emit HookCalled(abi.encode(table, key, valueSchema)); + + // Expect a HookCalled event to be emitted when the EchoSubscriber's onAfterDeleteRecord hook is called + vm.expectEmit(true, true, true, true); + emit HookCalled(abi.encode(table, key, valueSchema)); + + IStore(this).deleteRecord(table, key, valueSchema); + } + function testHooksDynamicData() public { bytes32 table = keccak256("some.table"); bytes32[] memory key = new bytes32[](1); diff --git a/packages/store/test/StoreMock.sol b/packages/store/test/StoreMock.sol index 6425642b65..3974dc6ed0 100644 --- a/packages/store/test/StoreMock.sol +++ b/packages/store/test/StoreMock.sol @@ -84,4 +84,9 @@ contract StoreMock is IStore, StoreRead { function registerStoreHook(bytes32 table, IStoreHook hookAddress, uint8 enabledHooksBitmap) public { StoreCore.registerStoreHook(table, hookAddress, enabledHooksBitmap); } + + // Unregister hook to be called when a record or field is set or deleted + function unregisterStoreHook(bytes32 table, IStoreHook hookAddress) public { + StoreCore.unregisterStoreHook(table, hookAddress); + } } diff --git a/packages/store/test/tables/Hooks.t.sol b/packages/store/test/tables/StoreHooks.t.sol similarity index 56% rename from packages/store/test/tables/Hooks.t.sol rename to packages/store/test/tables/StoreHooks.t.sol index 1c035e44c9..05d6413351 100644 --- a/packages/store/test/tables/Hooks.t.sol +++ b/packages/store/test/tables/StoreHooks.t.sol @@ -4,73 +4,73 @@ pragma solidity >=0.8.0; import { Test } from "forge-std/Test.sol"; import { GasReporter } from "@latticexyz/gas-report/src/GasReporter.sol"; import { StoreReadWithStubs } from "../../src/StoreReadWithStubs.sol"; -import { Hooks } from "../../src/codegen/Tables.sol"; +import { StoreHooks } from "../../src/codegen/Tables.sol"; -contract HooksTest is Test, GasReporter, StoreReadWithStubs { +contract StoreHooksTest is Test, GasReporter, StoreReadWithStubs { function testTable() public { - // Hooks schema is already registered by StoreCore + // StoreHooks schema is already registered by StoreCore bytes32 key = keccak256("somekey"); bytes21[] memory hooks = new bytes21[](1); hooks[0] = bytes21("some data"); - startGasReport("Hooks: set field (cold)"); - Hooks.set(key, hooks); + startGasReport("StoreHooks: set field (cold)"); + StoreHooks.set(key, hooks); endGasReport(); - startGasReport("Hooks: get field (warm)"); - bytes21[] memory returnedHooks = Hooks.get(key); + startGasReport("StoreHooks: get field (warm)"); + bytes21[] memory returnedHooks = StoreHooks.get(key); endGasReport(); assertEq(returnedHooks.length, hooks.length); assertEq(returnedHooks[0], hooks[0]); - startGasReport("Hooks: push 1 element (cold)"); - Hooks.push(key, hooks[0]); + startGasReport("StoreHooks: push 1 element (cold)"); + StoreHooks.push(key, hooks[0]); endGasReport(); - returnedHooks = Hooks.get(key); + returnedHooks = StoreHooks.get(key); assertEq(returnedHooks.length, 2); assertEq(returnedHooks[1], hooks[0]); - startGasReport("Hooks: pop 1 element (warm)"); - Hooks.pop(key); + startGasReport("StoreHooks: pop 1 element (warm)"); + StoreHooks.pop(key); endGasReport(); - returnedHooks = Hooks.get(key); + returnedHooks = StoreHooks.get(key); assertEq(returnedHooks.length, 1); assertEq(returnedHooks[0], hooks[0]); - startGasReport("Hooks: push 1 element (warm)"); - Hooks.push(key, hooks[0]); + startGasReport("StoreHooks: push 1 element (warm)"); + StoreHooks.push(key, hooks[0]); endGasReport(); - returnedHooks = Hooks.get(key); + returnedHooks = StoreHooks.get(key); assertEq(returnedHooks.length, 2); assertEq(returnedHooks[1], hooks[0]); bytes21 newHook = bytes21(keccak256("alice")); - startGasReport("Hooks: update 1 element (warm)"); - Hooks.update(key, 1, newHook); + startGasReport("StoreHooks: update 1 element (warm)"); + StoreHooks.update(key, 1, newHook); endGasReport(); - returnedHooks = Hooks.get(key); + returnedHooks = StoreHooks.get(key); assertEq(returnedHooks.length, 2); assertEq(returnedHooks[0], hooks[0]); assertEq(returnedHooks[1], newHook); - startGasReport("Hooks: delete record (warm)"); - Hooks.deleteRecord(key); + startGasReport("StoreHooks: delete record (warm)"); + StoreHooks.deleteRecord(key); endGasReport(); - returnedHooks = Hooks.get(key); + returnedHooks = StoreHooks.get(key); assertEq(returnedHooks.length, 0); - startGasReport("Hooks: set field (warm)"); - Hooks.set(key, hooks); + startGasReport("StoreHooks: set field (warm)"); + StoreHooks.set(key, hooks); endGasReport(); } @@ -79,8 +79,8 @@ contract HooksTest is Test, GasReporter, StoreReadWithStubs { bytes21[] memory hooks = new bytes21[](1); hooks[0] = bytes21("some data"); - startGasReport("Hooks: set field with one elements (cold)"); - Hooks.set(key1, hooks); + startGasReport("StoreHooks: set field with one elements (cold)"); + StoreHooks.set(key1, hooks); endGasReport(); } @@ -90,8 +90,8 @@ contract HooksTest is Test, GasReporter, StoreReadWithStubs { hooks[0] = bytes21("some data"); hooks[1] = bytes21("some other data"); - startGasReport("Hooks: set field with two elements (cold)"); - Hooks.set(key2, hooks); + startGasReport("StoreHooks: set field with two elements (cold)"); + StoreHooks.set(key2, hooks); endGasReport(); } @@ -102,8 +102,8 @@ contract HooksTest is Test, GasReporter, StoreReadWithStubs { hooks[1] = bytes21("some other data"); hooks[2] = bytes21("some other other data"); - startGasReport("Hooks: set field with three elements (cold)"); - Hooks.set(key3, hooks); + startGasReport("StoreHooks: set field with three elements (cold)"); + StoreHooks.set(key3, hooks); endGasReport(); } } diff --git a/packages/store/test/tables/HooksColdLoad.t.sol b/packages/store/test/tables/StoreHooksColdLoad.t.sol similarity index 58% rename from packages/store/test/tables/HooksColdLoad.t.sol rename to packages/store/test/tables/StoreHooksColdLoad.t.sol index 2e0ff5b0e9..fc869bcaee 100644 --- a/packages/store/test/tables/HooksColdLoad.t.sol +++ b/packages/store/test/tables/StoreHooksColdLoad.t.sol @@ -4,26 +4,26 @@ pragma solidity >=0.8.0; import { Test } from "forge-std/Test.sol"; import { GasReporter } from "@latticexyz/gas-report/src/GasReporter.sol"; import { StoreReadWithStubs } from "../../src/StoreReadWithStubs.sol"; -import { Hooks } from "../../src/codegen/Tables.sol"; +import { StoreHooks } from "../../src/codegen/Tables.sol"; -contract HooksColdLoadTest is Test, GasReporter, StoreReadWithStubs { +contract StoreHooksColdLoadTest is Test, GasReporter, StoreReadWithStubs { bytes21[] hooks; function setUp() public { - // Hooks schema is already registered by StoreCore + // StoreHooks schema is already registered by StoreCore bytes32 key = keccak256("somekey"); hooks = new bytes21[](1); hooks[0] = bytes21("some data"); - Hooks.set(key, hooks); + StoreHooks.set(key, hooks); } function testGet() public { bytes32 key = keccak256("somekey"); - startGasReport("Hooks: get field (cold)"); - bytes21[] memory returnedAddresses = Hooks.get(key); + startGasReport("StoreHooks: get field (cold)"); + bytes21[] memory returnedAddresses = StoreHooks.get(key); endGasReport(); assertEq(returnedAddresses.length, hooks.length); @@ -33,8 +33,8 @@ contract HooksColdLoadTest is Test, GasReporter, StoreReadWithStubs { function testLength() public { bytes32 key = keccak256("somekey"); - startGasReport("Hooks: get length (cold)"); - uint256 length = Hooks.length(key); + startGasReport("StoreHooks: get length (cold)"); + uint256 length = StoreHooks.length(key); endGasReport(); assertEq(length, hooks.length); @@ -43,8 +43,8 @@ contract HooksColdLoadTest is Test, GasReporter, StoreReadWithStubs { function testGetItem() public { bytes32 key = keccak256("somekey"); - startGasReport("Hooks: get 1 element (cold)"); - bytes21 returnedAddress = Hooks.getItem(key, 0); + startGasReport("StoreHooks: get 1 element (cold)"); + bytes21 returnedAddress = StoreHooks.getItem(key, 0); endGasReport(); assertEq(returnedAddress, hooks[0]); @@ -53,11 +53,11 @@ contract HooksColdLoadTest is Test, GasReporter, StoreReadWithStubs { function testPop() public { bytes32 key = keccak256("somekey"); - startGasReport("Hooks: pop 1 element (cold)"); - Hooks.pop(key); + startGasReport("StoreHooks: pop 1 element (cold)"); + StoreHooks.pop(key); endGasReport(); - uint256 length = Hooks.length(key); + uint256 length = StoreHooks.length(key); assertEq(length, hooks.length - 1); } @@ -66,11 +66,11 @@ contract HooksColdLoadTest is Test, GasReporter, StoreReadWithStubs { bytes32 key = keccak256("somekey"); bytes21 newAddress = bytes21(bytes20(keccak256("alice"))); - startGasReport("Hooks: update 1 element (cold)"); - Hooks.update(key, 0, newAddress); + startGasReport("StoreHooks: update 1 element (cold)"); + StoreHooks.update(key, 0, newAddress); endGasReport(); - bytes21[] memory returnedAddresses = Hooks.get(key); + bytes21[] memory returnedAddresses = StoreHooks.get(key); assertEq(returnedAddresses.length, 1); assertEq(returnedAddresses[0], newAddress); } @@ -78,11 +78,11 @@ contract HooksColdLoadTest is Test, GasReporter, StoreReadWithStubs { function testDelete() public { bytes32 key = keccak256("somekey"); - startGasReport("Hooks: delete record (cold)"); - Hooks.deleteRecord(key); + startGasReport("StoreHooks: delete record (cold)"); + StoreHooks.deleteRecord(key); endGasReport(); - bytes21[] memory returnedAddresses = Hooks.get(key); + bytes21[] memory returnedAddresses = StoreHooks.get(key); assertEq(returnedAddresses.length, 0); } } diff --git a/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json b/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json index bf331e49e6..d1a34a46a6 100644 --- a/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json +++ b/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json @@ -545,5 +545,41 @@ "outputs": [], "stateMutability": "nonpayable", "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "tableId", + "type": "bytes32" + }, + { + "internalType": "contract IStoreHook", + "name": "hookAddress", + "type": "address" + } + ], + "name": "unregisterStoreHook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "resourceSelector", + "type": "bytes32" + }, + { + "internalType": "contract ISystemHook", + "name": "hookAddress", + "type": "address" + } + ], + "name": "unregisterSystemHook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" } ] \ No newline at end of file diff --git a/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json.d.ts b/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json.d.ts index 44a9fef39a..2f28fea48f 100644 --- a/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json.d.ts +++ b/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json.d.ts @@ -545,6 +545,42 @@ declare const abi: [ outputs: []; stateMutability: "nonpayable"; type: "function"; + }, + { + inputs: [ + { + internalType: "bytes32"; + name: "tableId"; + type: "bytes32"; + }, + { + internalType: "contract IStoreHook"; + name: "hookAddress"; + type: "address"; + } + ]; + name: "unregisterStoreHook"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; + }, + { + inputs: [ + { + internalType: "bytes32"; + name: "resourceSelector"; + type: "bytes32"; + }, + { + internalType: "contract ISystemHook"; + name: "hookAddress"; + type: "address"; + } + ]; + name: "unregisterSystemHook"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; } ]; export default abi; diff --git a/packages/world/abi/IBaseWorld.sol/IBaseWorld.abi.json b/packages/world/abi/IBaseWorld.sol/IBaseWorld.abi.json index e9d08b90b4..48cc763e25 100644 --- a/packages/world/abi/IBaseWorld.sol/IBaseWorld.abi.json +++ b/packages/world/abi/IBaseWorld.sol/IBaseWorld.abi.json @@ -1016,6 +1016,42 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "table", + "type": "bytes32" + }, + { + "internalType": "contract IStoreHook", + "name": "hookAddress", + "type": "address" + } + ], + "name": "unregisterStoreHook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "resourceSelector", + "type": "bytes32" + }, + { + "internalType": "contract ISystemHook", + "name": "hookAddress", + "type": "address" + } + ], + "name": "unregisterSystemHook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/packages/world/abi/IBaseWorld.sol/IBaseWorld.abi.json.d.ts b/packages/world/abi/IBaseWorld.sol/IBaseWorld.abi.json.d.ts index b72d53aaac..ee8ae36caa 100644 --- a/packages/world/abi/IBaseWorld.sol/IBaseWorld.abi.json.d.ts +++ b/packages/world/abi/IBaseWorld.sol/IBaseWorld.abi.json.d.ts @@ -1016,6 +1016,42 @@ declare const abi: [ stateMutability: "nonpayable"; type: "function"; }, + { + inputs: [ + { + internalType: "bytes32"; + name: "table"; + type: "bytes32"; + }, + { + internalType: "contract IStoreHook"; + name: "hookAddress"; + type: "address"; + } + ]; + name: "unregisterStoreHook"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; + }, + { + inputs: [ + { + internalType: "bytes32"; + name: "resourceSelector"; + type: "bytes32"; + }, + { + internalType: "contract ISystemHook"; + name: "hookAddress"; + type: "address"; + } + ]; + name: "unregisterSystemHook"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; + }, { inputs: [ { diff --git a/packages/world/abi/IStore.sol/IStore.abi.json b/packages/world/abi/IStore.sol/IStore.abi.json index 5b408efd0e..48e69022f3 100644 --- a/packages/world/abi/IStore.sol/IStore.abi.json +++ b/packages/world/abi/IStore.sol/IStore.abi.json @@ -618,6 +618,24 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "table", + "type": "bytes32" + }, + { + "internalType": "contract IStoreHook", + "name": "hookAddress", + "type": "address" + } + ], + "name": "unregisterStoreHook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/packages/world/abi/IStore.sol/IStore.abi.json.d.ts b/packages/world/abi/IStore.sol/IStore.abi.json.d.ts index 7d43da2fd0..9838b2e184 100644 --- a/packages/world/abi/IStore.sol/IStore.abi.json.d.ts +++ b/packages/world/abi/IStore.sol/IStore.abi.json.d.ts @@ -618,6 +618,24 @@ declare const abi: [ stateMutability: "nonpayable"; type: "function"; }, + { + inputs: [ + { + internalType: "bytes32"; + name: "table"; + type: "bytes32"; + }, + { + internalType: "contract IStoreHook"; + name: "hookAddress"; + type: "address"; + } + ]; + name: "unregisterStoreHook"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; + }, { inputs: [ { diff --git a/packages/world/abi/IStore.sol/IStoreRegistration.abi.json b/packages/world/abi/IStore.sol/IStoreRegistration.abi.json index feac4849a3..8199933edd 100644 --- a/packages/world/abi/IStore.sol/IStoreRegistration.abi.json +++ b/packages/world/abi/IStore.sol/IStoreRegistration.abi.json @@ -54,5 +54,23 @@ "outputs": [], "stateMutability": "nonpayable", "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "table", + "type": "bytes32" + }, + { + "internalType": "contract IStoreHook", + "name": "hookAddress", + "type": "address" + } + ], + "name": "unregisterStoreHook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" } ] \ No newline at end of file diff --git a/packages/world/abi/IStore.sol/IStoreRegistration.abi.json.d.ts b/packages/world/abi/IStore.sol/IStoreRegistration.abi.json.d.ts index c03b1d5863..02db08cbf2 100644 --- a/packages/world/abi/IStore.sol/IStoreRegistration.abi.json.d.ts +++ b/packages/world/abi/IStore.sol/IStoreRegistration.abi.json.d.ts @@ -54,6 +54,24 @@ declare const abi: [ outputs: []; stateMutability: "nonpayable"; type: "function"; + }, + { + inputs: [ + { + internalType: "bytes32"; + name: "table"; + type: "bytes32"; + }, + { + internalType: "contract IStoreHook"; + name: "hookAddress"; + type: "address"; + } + ]; + name: "unregisterStoreHook"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; } ]; export default abi; diff --git a/packages/world/abi/IWorldRegistrationSystem.sol/IWorldRegistrationSystem.abi.json b/packages/world/abi/IWorldRegistrationSystem.sol/IWorldRegistrationSystem.abi.json index a2b26c0d1a..dafdf4f8ac 100644 --- a/packages/world/abi/IWorldRegistrationSystem.sol/IWorldRegistrationSystem.abi.json +++ b/packages/world/abi/IWorldRegistrationSystem.sol/IWorldRegistrationSystem.abi.json @@ -138,5 +138,23 @@ "outputs": [], "stateMutability": "nonpayable", "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "resourceSelector", + "type": "bytes32" + }, + { + "internalType": "contract ISystemHook", + "name": "hookAddress", + "type": "address" + } + ], + "name": "unregisterSystemHook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" } ] \ No newline at end of file diff --git a/packages/world/abi/IWorldRegistrationSystem.sol/IWorldRegistrationSystem.abi.json.d.ts b/packages/world/abi/IWorldRegistrationSystem.sol/IWorldRegistrationSystem.abi.json.d.ts index 91e71a7fb6..3c8934621c 100644 --- a/packages/world/abi/IWorldRegistrationSystem.sol/IWorldRegistrationSystem.abi.json.d.ts +++ b/packages/world/abi/IWorldRegistrationSystem.sol/IWorldRegistrationSystem.abi.json.d.ts @@ -138,6 +138,24 @@ declare const abi: [ outputs: []; stateMutability: "nonpayable"; type: "function"; + }, + { + inputs: [ + { + internalType: "bytes32"; + name: "resourceSelector"; + type: "bytes32"; + }, + { + internalType: "contract ISystemHook"; + name: "hookAddress"; + type: "address"; + } + ]; + name: "unregisterSystemHook"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; } ]; export default abi; diff --git a/packages/world/abi/StoreHooks.sol/StoreHooks.abi.json b/packages/world/abi/StoreHooks.sol/StoreHooks.abi.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/packages/world/abi/StoreHooks.sol/StoreHooks.abi.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/packages/world/abi/StoreRegistrationSystem.sol/StoreRegistrationSystem.abi.json b/packages/world/abi/StoreRegistrationSystem.sol/StoreRegistrationSystem.abi.json index cdb4bb8101..c312a17e90 100644 --- a/packages/world/abi/StoreRegistrationSystem.sol/StoreRegistrationSystem.abi.json +++ b/packages/world/abi/StoreRegistrationSystem.sol/StoreRegistrationSystem.abi.json @@ -280,5 +280,23 @@ "outputs": [], "stateMutability": "nonpayable", "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "tableId", + "type": "bytes32" + }, + { + "internalType": "contract IStoreHook", + "name": "hookAddress", + "type": "address" + } + ], + "name": "unregisterStoreHook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" } ] \ No newline at end of file diff --git a/packages/world/abi/StoreRegistrationSystem.sol/StoreRegistrationSystem.abi.json.d.ts b/packages/world/abi/StoreRegistrationSystem.sol/StoreRegistrationSystem.abi.json.d.ts index 60a453b564..eec6fa9c6b 100644 --- a/packages/world/abi/StoreRegistrationSystem.sol/StoreRegistrationSystem.abi.json.d.ts +++ b/packages/world/abi/StoreRegistrationSystem.sol/StoreRegistrationSystem.abi.json.d.ts @@ -280,6 +280,24 @@ declare const abi: [ outputs: []; stateMutability: "nonpayable"; type: "function"; + }, + { + inputs: [ + { + internalType: "bytes32"; + name: "tableId"; + type: "bytes32"; + }, + { + internalType: "contract IStoreHook"; + name: "hookAddress"; + type: "address"; + } + ]; + name: "unregisterStoreHook"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; } ]; export default abi; diff --git a/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json b/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json index 64d8d6b37c..2f527ba15b 100644 --- a/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json +++ b/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json @@ -316,5 +316,23 @@ "outputs": [], "stateMutability": "nonpayable", "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "resourceSelector", + "type": "bytes32" + }, + { + "internalType": "contract ISystemHook", + "name": "hookAddress", + "type": "address" + } + ], + "name": "unregisterSystemHook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" } ] \ No newline at end of file diff --git a/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json.d.ts b/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json.d.ts index 12fe662f81..37c3dda709 100644 --- a/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json.d.ts +++ b/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json.d.ts @@ -316,6 +316,24 @@ declare const abi: [ outputs: []; stateMutability: "nonpayable"; type: "function"; + }, + { + inputs: [ + { + internalType: "bytes32"; + name: "resourceSelector"; + type: "bytes32"; + }, + { + internalType: "contract ISystemHook"; + name: "hookAddress"; + type: "address"; + } + ]; + name: "unregisterSystemHook"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; } ]; export default abi; diff --git a/packages/world/abi/src/IStore.sol/IStore.abi.json b/packages/world/abi/src/IStore.sol/IStore.abi.json index 5b408efd0e..48e69022f3 100644 --- a/packages/world/abi/src/IStore.sol/IStore.abi.json +++ b/packages/world/abi/src/IStore.sol/IStore.abi.json @@ -618,6 +618,24 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "table", + "type": "bytes32" + }, + { + "internalType": "contract IStoreHook", + "name": "hookAddress", + "type": "address" + } + ], + "name": "unregisterStoreHook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/packages/world/abi/src/IStore.sol/IStore.abi.json.d.ts b/packages/world/abi/src/IStore.sol/IStore.abi.json.d.ts index 7d43da2fd0..9838b2e184 100644 --- a/packages/world/abi/src/IStore.sol/IStore.abi.json.d.ts +++ b/packages/world/abi/src/IStore.sol/IStore.abi.json.d.ts @@ -618,6 +618,24 @@ declare const abi: [ stateMutability: "nonpayable"; type: "function"; }, + { + inputs: [ + { + internalType: "bytes32"; + name: "table"; + type: "bytes32"; + }, + { + internalType: "contract IStoreHook"; + name: "hookAddress"; + type: "address"; + } + ]; + name: "unregisterStoreHook"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; + }, { inputs: [ { diff --git a/packages/world/abi/src/IStore.sol/IStoreRegistration.abi.json b/packages/world/abi/src/IStore.sol/IStoreRegistration.abi.json index feac4849a3..8199933edd 100644 --- a/packages/world/abi/src/IStore.sol/IStoreRegistration.abi.json +++ b/packages/world/abi/src/IStore.sol/IStoreRegistration.abi.json @@ -54,5 +54,23 @@ "outputs": [], "stateMutability": "nonpayable", "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "table", + "type": "bytes32" + }, + { + "internalType": "contract IStoreHook", + "name": "hookAddress", + "type": "address" + } + ], + "name": "unregisterStoreHook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" } ] \ No newline at end of file diff --git a/packages/world/abi/src/IStore.sol/IStoreRegistration.abi.json.d.ts b/packages/world/abi/src/IStore.sol/IStoreRegistration.abi.json.d.ts index c03b1d5863..02db08cbf2 100644 --- a/packages/world/abi/src/IStore.sol/IStoreRegistration.abi.json.d.ts +++ b/packages/world/abi/src/IStore.sol/IStoreRegistration.abi.json.d.ts @@ -54,6 +54,24 @@ declare const abi: [ outputs: []; stateMutability: "nonpayable"; type: "function"; + }, + { + inputs: [ + { + internalType: "bytes32"; + name: "table"; + type: "bytes32"; + }, + { + internalType: "contract IStoreHook"; + name: "hookAddress"; + type: "address"; + } + ]; + name: "unregisterStoreHook"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; } ]; export default abi; diff --git a/packages/world/gas-report.json b/packages/world/gas-report.json index 24a0aacd2c..ec99ed9234 100644 --- a/packages/world/gas-report.json +++ b/packages/world/gas-report.json @@ -39,13 +39,13 @@ "file": "test/KeysInTableModule.t.sol", "test": "testInstallComposite", "name": "install keys in table module", - "gasUsed": 1412483 + "gasUsed": 1412624 }, { "file": "test/KeysInTableModule.t.sol", "test": "testInstallGas", "name": "install keys in table module", - "gasUsed": 1412483 + "gasUsed": 1412624 }, { "file": "test/KeysInTableModule.t.sol", @@ -57,13 +57,13 @@ "file": "test/KeysInTableModule.t.sol", "test": "testInstallSingleton", "name": "install keys in table module", - "gasUsed": 1412483 + "gasUsed": 1412624 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookCompositeGas", "name": "install keys in table module", - "gasUsed": 1412483 + "gasUsed": 1412624 }, { "file": "test/KeysInTableModule.t.sol", @@ -81,7 +81,7 @@ "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookGas", "name": "install keys in table module", - "gasUsed": 1412483 + "gasUsed": 1412624 }, { "file": "test/KeysInTableModule.t.sol", @@ -99,7 +99,7 @@ "file": "test/KeysWithValueModule.t.sol", "test": "testGetKeysWithValueGas", "name": "install keys with value module", - "gasUsed": 650950 + "gasUsed": 651087 }, { "file": "test/KeysWithValueModule.t.sol", @@ -117,7 +117,7 @@ "file": "test/KeysWithValueModule.t.sol", "test": "testInstall", "name": "install keys with value module", - "gasUsed": 650950 + "gasUsed": 651087 }, { "file": "test/KeysWithValueModule.t.sol", @@ -129,7 +129,7 @@ "file": "test/KeysWithValueModule.t.sol", "test": "testSetAndDeleteRecordHook", "name": "install keys with value module", - "gasUsed": 650950 + "gasUsed": 651087 }, { "file": "test/KeysWithValueModule.t.sol", @@ -147,7 +147,7 @@ "file": "test/KeysWithValueModule.t.sol", "test": "testSetField", "name": "install keys with value module", - "gasUsed": 650950 + "gasUsed": 651087 }, { "file": "test/KeysWithValueModule.t.sol", @@ -231,7 +231,7 @@ "file": "test/StandardDelegationsModule.t.sol", "test": "testCallFromCallboundDelegation", "name": "register a callbound delegation", - "gasUsed": 122345 + "gasUsed": 122373 }, { "file": "test/StandardDelegationsModule.t.sol", @@ -243,7 +243,7 @@ "file": "test/StandardDelegationsModule.t.sol", "test": "testCallFromTimeboundDelegation", "name": "register a timebound delegation", - "gasUsed": 116594 + "gasUsed": 116622 }, { "file": "test/StandardDelegationsModule.t.sol", @@ -255,7 +255,7 @@ "file": "test/UniqueEntityModule.t.sol", "test": "testInstall", "name": "install unique entity module", - "gasUsed": 726423 + "gasUsed": 726567 }, { "file": "test/UniqueEntityModule.t.sol", @@ -267,7 +267,7 @@ "file": "test/UniqueEntityModule.t.sol", "test": "testInstallRoot", "name": "installRoot unique entity module", - "gasUsed": 705367 + "gasUsed": 705483 }, { "file": "test/UniqueEntityModule.t.sol", @@ -285,7 +285,7 @@ "file": "test/World.t.sol", "test": "testCallFromUnlimitedDelegation", "name": "register an unlimited delegation", - "gasUsed": 55457 + "gasUsed": 55485 }, { "file": "test/World.t.sol", @@ -309,37 +309,37 @@ "file": "test/World.t.sol", "test": "testRegisterFallbackSystem", "name": "Register a fallback system", - "gasUsed": 70432 + "gasUsed": 70471 }, { "file": "test/World.t.sol", "test": "testRegisterFallbackSystem", "name": "Register a root fallback system", - "gasUsed": 63716 + "gasUsed": 63733 }, { "file": "test/World.t.sol", "test": "testRegisterFunctionSelector", "name": "Register a function selector", - "gasUsed": 91026 + "gasUsed": 91065 }, { "file": "test/World.t.sol", "test": "testRegisterNamespace", "name": "Register a new namespace", - "gasUsed": 140073 + "gasUsed": 140158 }, { "file": "test/World.t.sol", "test": "testRegisterRootFunctionSelector", "name": "Register a root function selector", - "gasUsed": 79627 + "gasUsed": 79644 }, { "file": "test/World.t.sol", "test": "testRegisterTable", "name": "Register a new table in the namespace", - "gasUsed": 650137 + "gasUsed": 650278 }, { "file": "test/World.t.sol", diff --git a/packages/world/src/interfaces/IWorldRegistrationSystem.sol b/packages/world/src/interfaces/IWorldRegistrationSystem.sol index 184886400e..8b319aef9a 100644 --- a/packages/world/src/interfaces/IWorldRegistrationSystem.sol +++ b/packages/world/src/interfaces/IWorldRegistrationSystem.sol @@ -11,6 +11,8 @@ interface IWorldRegistrationSystem { function registerSystemHook(bytes32 resourceSelector, ISystemHook hookAddress, uint8 enabledHooksBitmap) external; + function unregisterSystemHook(bytes32 resourceSelector, ISystemHook hookAddress) external; + function registerSystem(bytes32 resourceSelector, WorldContextConsumer system, bool publicAccess) external; function registerFunctionSelector( diff --git a/packages/world/src/modules/core/CoreModule.sol b/packages/world/src/modules/core/CoreModule.sol index fecd590879..74bb71585e 100644 --- a/packages/world/src/modules/core/CoreModule.sol +++ b/packages/world/src/modules/core/CoreModule.sol @@ -90,10 +90,11 @@ contract CoreModule is IModule, WorldContextConsumer { * Register function selectors for all CoreSystem functions in the World */ function _registerFunctionSelectors() internal { - bytes4[13] memory functionSelectors = [ + bytes4[15] memory functionSelectors = [ // --- WorldRegistrationSystem --- WorldRegistrationSystem.registerNamespace.selector, WorldRegistrationSystem.registerSystemHook.selector, + WorldRegistrationSystem.unregisterSystemHook.selector, WorldRegistrationSystem.registerSystem.selector, WorldRegistrationSystem.registerFunctionSelector.selector, WorldRegistrationSystem.registerRootFunctionSelector.selector, @@ -101,6 +102,7 @@ contract CoreModule is IModule, WorldContextConsumer { // --- StoreRegistrationSystem --- StoreRegistrationSystem.registerTable.selector, StoreRegistrationSystem.registerStoreHook.selector, + StoreRegistrationSystem.unregisterStoreHook.selector, // --- ModuleInstallationSystem --- ModuleInstallationSystem.installModule.selector, // --- AccessManagementSystem --- diff --git a/packages/world/src/modules/core/implementations/StoreRegistrationSystem.sol b/packages/world/src/modules/core/implementations/StoreRegistrationSystem.sol index 8457ec45c7..8d1fcfd6e1 100644 --- a/packages/world/src/modules/core/implementations/StoreRegistrationSystem.sol +++ b/packages/world/src/modules/core/implementations/StoreRegistrationSystem.sol @@ -74,7 +74,7 @@ contract StoreRegistrationSystem is System, IWorldErrors { } /** - * Register a hook for the table at the given namepace and name. + * Register a hook for the given tableId. * Requires the caller to own the namespace. */ function registerStoreHook(bytes32 tableId, IStoreHook hookAddress, uint8 enabledHooksBitmap) public virtual { @@ -84,4 +84,16 @@ contract StoreRegistrationSystem is System, IWorldErrors { // Register the hook StoreCore.registerStoreHook(tableId, hookAddress, enabledHooksBitmap); } + + /** + * Unregister a hook for the given tableId. + * Requires the caller to own the namespace. + */ + function unregisterStoreHook(bytes32 tableId, IStoreHook hookAddress) public virtual { + // Require caller to own the namespace + AccessControl.requireOwnerOrSelf(tableId, _msgSender()); + + // Unregister the hook + StoreCore.unregisterStoreHook(tableId, hookAddress); + } } diff --git a/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol b/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol index e22b7ebf06..a52090c42f 100644 --- a/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol +++ b/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; -import { Hook } from "@latticexyz/store/src/Hook.sol"; +import { Hook, HookLib } from "@latticexyz/store/src/Hook.sol"; import { System } from "../../../System.sol"; import { WorldContextConsumer } from "../../../WorldContext.sol"; @@ -18,7 +18,7 @@ import { ISystemHook } from "../../../interfaces/ISystemHook.sol"; import { IWorldErrors } from "../../../interfaces/IWorldErrors.sol"; import { ResourceType } from "../tables/ResourceType.sol"; -import { SystemHooks } from "../tables/SystemHooks.sol"; +import { SystemHooks, SystemHooksTableId } from "../tables/SystemHooks.sol"; import { SystemRegistry } from "../tables/SystemRegistry.sol"; import { Systems } from "../tables/Systems.sol"; import { FunctionSelectors } from "../tables/FunctionSelectors.sol"; @@ -50,7 +50,7 @@ contract WorldRegistrationSystem is System, IWorldErrors { } /** - * Register a hook for the system at the given namespace and name + * Register a hook for the system at the given resource selector */ function registerSystemHook( bytes32 resourceSelector, @@ -64,6 +64,17 @@ contract WorldRegistrationSystem is System, IWorldErrors { SystemHooks.push(resourceSelector, Hook.unwrap(SystemHookLib.encode(hookAddress, enabledHooksBitmap))); } + /** + * Unregister the given hook for the system at the given resource selector + */ + function unregisterSystemHook(bytes32 resourceSelector, ISystemHook hookAddress) public virtual { + // Require caller to own the namespace + AccessControl.requireOwnerOrSelf(resourceSelector, _msgSender()); + + // Remove the hook from the list of hooks for this resourceSelector in the system hooks table + HookLib.filterListByAddress(SystemHooksTableId, resourceSelector, address(hookAddress)); + } + /** * Register the given system in the given namespace. * If the namespace doesn't exist yet, it is registered. diff --git a/packages/world/test/World.t.sol b/packages/world/test/World.t.sol index e03cdb2dab..e82df66d02 100644 --- a/packages/world/test/World.t.sol +++ b/packages/world/test/World.t.sol @@ -15,6 +15,8 @@ import { Schema, SchemaLib } from "@latticexyz/store/src/Schema.sol"; import { Tables, TablesTableId } from "@latticexyz/store/src/codegen/Tables.sol"; import { EncodeArray } from "@latticexyz/store/src/tightcoder/EncodeArray.sol"; import { StoreHookLib } from "@latticexyz/store/src/StoreHook.sol"; +import { RevertSubscriber } from "@latticexyz/store/test/RevertSubscriber.sol"; +import { EchoSubscriber } from "@latticexyz/store/test/EchoSubscriber.sol"; import { World } from "../src/World.sol"; import { System } from "../src/System.sol"; @@ -111,55 +113,27 @@ contract PayableFallbackSystem is System { fallback() external payable {} } -contract WorldTestTableHook is IStoreHook { - event HookCalled(bytes data); - - function onBeforeSetRecord(bytes32 table, bytes32[] memory key, bytes memory data, Schema valueSchema) public { - emit HookCalled(abi.encode(table, key, data, valueSchema)); - } - - function onAfterSetRecord(bytes32 table, bytes32[] memory key, bytes memory data, Schema valueSchema) public { - emit HookCalled(abi.encode(table, key, data, valueSchema)); - } - - function onBeforeSetField( - bytes32 table, - bytes32[] memory key, - uint8 schemaIndex, - bytes memory data, - Schema valueSchema - ) public { - emit HookCalled(abi.encode(table, key, schemaIndex, data, valueSchema)); - } - - function onAfterSetField( - bytes32 table, - bytes32[] memory key, - uint8 schemaIndex, - bytes memory data, - Schema valueSchema - ) public { - emit HookCalled(abi.encode(table, key, schemaIndex, data, valueSchema)); - } +contract EchoSystemHook is ISystemHook { + event SystemHookCalled(bytes data); - function onBeforeDeleteRecord(bytes32 table, bytes32[] memory key, Schema valueSchema) public { - emit HookCalled(abi.encode(table, key, valueSchema)); + function onBeforeCallSystem(address msgSender, bytes32 resourceSelector, bytes memory funcSelectorAndArgs) public { + emit SystemHookCalled(abi.encode("before", msgSender, resourceSelector, funcSelectorAndArgs)); } - function onAfterDeleteRecord(bytes32 table, bytes32[] memory key, Schema valueSchema) public { - emit HookCalled(abi.encode(table, key, valueSchema)); + function onAfterCallSystem(address msgSender, bytes32 resourceSelector, bytes memory funcSelectorAndArgs) public { + emit SystemHookCalled(abi.encode("after", msgSender, resourceSelector, funcSelectorAndArgs)); } } -contract WorldTestSystemHook is ISystemHook { +contract RevertSystemHook is ISystemHook { event SystemHookCalled(bytes data); - function onBeforeCallSystem(address msgSender, bytes32 resourceSelector, bytes memory funcSelectorAndArgs) public { - emit SystemHookCalled(abi.encode("before", msgSender, resourceSelector, funcSelectorAndArgs)); + function onBeforeCallSystem(address, bytes32, bytes memory) public pure { + revert("onBeforeCallSystem"); } - function onAfterCallSystem(address msgSender, bytes32 resourceSelector, bytes memory funcSelectorAndArgs) public { - emit SystemHookCalled(abi.encode("after", msgSender, resourceSelector, funcSelectorAndArgs)); + function onAfterCallSystem(address, bytes32, bytes memory) public pure { + revert("onAfterCallSystem"); } } @@ -753,7 +727,7 @@ contract WorldTest is Test, GasReporter { world.registerTable(tableId, defaultKeySchema, valueSchema, new string[](1), new string[](1)); // Register a new hook - IStoreHook tableHook = new WorldTestTableHook(); + IStoreHook tableHook = new EchoSubscriber(); world.registerStoreHook( tableId, tableHook, @@ -798,6 +772,88 @@ contract WorldTest is Test, GasReporter { world.deleteRecord(tableId, singletonKey, valueSchema); } + function testUnregisterStoreHook() public { + Schema valueSchema = Bool.getValueSchema(); + bytes32 tableId = ResourceSelector.from("", "testTable"); + + // Register a new table + world.registerTable(tableId, defaultKeySchema, valueSchema, new string[](1), new string[](1)); + + // Register a new RevertSubscriber + IStoreHook revertSubscriber = new RevertSubscriber(); + world.registerStoreHook( + tableId, + revertSubscriber, + StoreHookLib.encodeBitmap({ + onBeforeSetRecord: true, + onAfterSetRecord: true, + onBeforeSetField: true, + onAfterSetField: true, + onBeforeDeleteRecord: true, + onAfterDeleteRecord: true + }) + ); + // Register a new EchoSubscriber + IStoreHook echoSubscriber = new EchoSubscriber(); + world.registerStoreHook( + tableId, + echoSubscriber, + StoreHookLib.encodeBitmap({ + onBeforeSetRecord: true, + onAfterSetRecord: true, + onBeforeSetField: true, + onAfterSetField: true, + onBeforeDeleteRecord: true, + onAfterDeleteRecord: true + }) + ); + + // Prepare data to write to the table + bytes memory value = abi.encodePacked(true); + + // Expect a revert when the RevertSubscriber's onBeforeSetRecord hook is called + vm.expectRevert(bytes("onBeforeSetRecord")); + world.setRecord(tableId, singletonKey, value, valueSchema); + + // Expect a revert when the RevertSubscriber's onBeforeSetField hook is called + vm.expectRevert(bytes("onBeforeSetField")); + world.setField(tableId, singletonKey, 0, value, valueSchema); + + // Expect a revert when the RevertSubscriber's onBeforeDeleteRecord hook is called + vm.expectRevert(bytes("onBeforeDeleteRecord")); + world.deleteRecord(tableId, singletonKey, valueSchema); + + // Unregister the RevertSubscriber + world.unregisterStoreHook(tableId, revertSubscriber); + + // Expect the hook to be notified when a record is written (once before and once after the record is written) + vm.expectEmit(true, true, true, true); + emit HookCalled(abi.encode(tableId, singletonKey, value, valueSchema)); + + vm.expectEmit(true, true, true, true); + emit HookCalled(abi.encode(tableId, singletonKey, value, valueSchema)); + + world.setRecord(tableId, singletonKey, value, valueSchema); + + // Expect the hook to be notified when a field is written (once before and once after the field is written) + vm.expectEmit(true, true, true, true); + emit HookCalled(abi.encode(tableId, singletonKey, uint8(0), value, valueSchema)); + + vm.expectEmit(true, true, true, true); + emit HookCalled(abi.encode(tableId, singletonKey, uint8(0), value, valueSchema)); + + world.setField(tableId, singletonKey, 0, value, valueSchema); + + // Expect the hook to be notified when a record is deleted (once before and once after the field is written) + vm.expectEmit(true, true, true, true); + emit HookCalled(abi.encode(tableId, singletonKey, valueSchema)); + + vm.expectEmit(true, true, true, true); + emit HookCalled(abi.encode(tableId, singletonKey, valueSchema)); + + world.deleteRecord(tableId, singletonKey, valueSchema); + } + function testRegisterSystemHook() public { bytes32 systemId = ResourceSelector.from("namespace", "testTable"); @@ -806,7 +862,7 @@ contract WorldTest is Test, GasReporter { world.registerSystem(systemId, system, false); // Register a new hook - ISystemHook systemHook = new WorldTestSystemHook(); + ISystemHook systemHook = new EchoSystemHook(); world.registerSystemHook( systemId, systemHook, @@ -829,6 +885,52 @@ contract WorldTest is Test, GasReporter { world.call(systemId, funcSelectorAndArgs); } + function testUnregisterSystemHook() public { + bytes32 systemId = ResourceSelector.from("namespace", "testTable"); + + // Register a new system + WorldTestSystem system = new WorldTestSystem(); + world.registerSystem(systemId, system, false); + + // Register a new RevertSystemHook + ISystemHook revertSystemHook = new RevertSystemHook(); + world.registerSystemHook( + systemId, + revertSystemHook, + SystemHookLib.encodeBitmap({ onBeforeCallSystem: true, onAfterCallSystem: true }) + ); + + // Register a new EchoSystemHook + ISystemHook echoSystemHook = new EchoSystemHook(); + world.registerSystemHook( + systemId, + echoSystemHook, + SystemHookLib.encodeBitmap({ onBeforeCallSystem: true, onAfterCallSystem: true }) + ); + + bytes memory funcSelectorAndArgs = abi.encodeWithSelector(bytes4(keccak256("fallbackselector"))); + + // Expect calls to fail while the RevertSystemHook is registered + vm.expectRevert(bytes("onBeforeCallSystem")); + world.call(systemId, funcSelectorAndArgs); + + // Unregister the RevertSystemHook + world.unregisterSystemHook(systemId, revertSystemHook); + + // Expect the echo hooks to be called in correct order + vm.expectEmit(true, true, true, true); + emit SystemHookCalled(abi.encode("before", address(this), systemId, funcSelectorAndArgs)); + + vm.expectEmit(true, true, true, true); + emit WorldTestSystemLog("fallback"); + + vm.expectEmit(true, true, true, true); + emit SystemHookCalled(abi.encode("after", address(this), systemId, funcSelectorAndArgs)); + + // Call a system fallback function without arguments via the World + world.call(systemId, funcSelectorAndArgs); + } + function testWriteRootSystem() public { bytes32 tableId = ResourceSelector.from("namespace", "testTable"); // Register a new table