diff --git a/.changeset/fuzzy-cars-stare.md b/.changeset/fuzzy-cars-stare.md new file mode 100644 index 0000000000..6ea8e90cf7 --- /dev/null +++ b/.changeset/fuzzy-cars-stare.md @@ -0,0 +1,70 @@ +--- +"@latticexyz/store": major +"@latticexyz/world": major +--- + +- The `onSetRecord` hook is split into `onBeforeSetRecord` and `onAfterSetRecord` and the `onDeleteRecord` hook is split into `onBeforeDeleteRecord` and `onAfterDeleteRecord`. + The purpose of this change is to allow more fine-grained control over the point in the lifecycle at which hooks are executed. + + The previous hooks were executed before modifying data, so they can be replaced with the respective `onBefore` hooks. + + ```diff + - function onSetRecord( + + function onBeforeSetRecord( + bytes32 table, + bytes32[] memory key, + bytes memory data, + Schema valueSchema + ) public; + + - function onDeleteRecord( + + function onBeforeDeleteRecord( + bytes32 table, + bytes32[] memory key, + Schema valueSchema + ) public; + ``` + +- It is now possible to specify which methods of a hook contract should be called when registering a hook. The purpose of this change is to save gas by avoiding to call no-op hook methods. + + ```diff + function registerStoreHook( + bytes32 tableId, + - IStoreHook hookAddress + + IStoreHook hookAddress, + + uint8 enabledHooksBitmap + ) public; + + function registerSystemHook( + bytes32 systemId, + - ISystemHook hookAddress + + ISystemHook hookAddress, + + uint8 enabledHooksBitmap + ) public; + ``` + + There are `StoreHookLib` and `SystemHookLib` with helper functions to encode the bitmap of enabled hooks. + + ```solidity + import { StoreHookLib } from "@latticexyz/store/src/StoreHook.sol"; + + uint8 storeHookBitmap = StoreBookLib.encodeBitmap({ + onBeforeSetRecord: true, + onAfterSetRecord: true, + onBeforeSetField: true, + onAfterSetField: true, + onBeforeDeleteRecord: true, + onAfterDeleteRecord: true + }); + ``` + + ```solidity + import { SystemHookLib } from "@latticexyz/world/src/SystemHook.sol"; + + uint8 systemHookBitmap = SystemHookLib.encodeBitmap({ + onBeforeCallSystem: true, + onAfterCallSystem: true + }); + ``` + +- The `onSetRecord` hook call for `emitEphemeralRecord` has been removed to save gas and to more clearly distinguish ephemeral tables as offchain tables. diff --git a/packages/store/abi/EchoSubscriber.sol/EchoSubscriber.abi.json b/packages/store/abi/EchoSubscriber.sol/EchoSubscriber.abi.json new file mode 100644 index 0000000000..d2ea7262eb --- /dev/null +++ b/packages/store/abi/EchoSubscriber.sol/EchoSubscriber.abi.json @@ -0,0 +1,183 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "HookCalled", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "table", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "key", + "type": "bytes32[]" + }, + { + "internalType": "Schema", + "name": "valueSchema", + "type": "bytes32" + } + ], + "name": "onAfterDeleteRecord", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "table", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "key", + "type": "bytes32[]" + }, + { + "internalType": "uint8", + "name": "schemaIndex", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "Schema", + "name": "valueSchem", + "type": "bytes32" + } + ], + "name": "onAfterSetField", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "table", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "key", + "type": "bytes32[]" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "Schema", + "name": "valueSchema", + "type": "bytes32" + } + ], + "name": "onAfterSetRecord", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "table", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "key", + "type": "bytes32[]" + }, + { + "internalType": "Schema", + "name": "valueSchema", + "type": "bytes32" + } + ], + "name": "onBeforeDeleteRecord", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "table", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "key", + "type": "bytes32[]" + }, + { + "internalType": "uint8", + "name": "schemaIndex", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "Schema", + "name": "valueSchema", + "type": "bytes32" + } + ], + "name": "onBeforeSetField", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "table", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "key", + "type": "bytes32[]" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "Schema", + "name": "valueSchema", + "type": "bytes32" + } + ], + "name": "onBeforeSetRecord", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/packages/store/abi/EchoSubscriber.sol/EchoSubscriber.abi.json.d.ts b/packages/store/abi/EchoSubscriber.sol/EchoSubscriber.abi.json.d.ts new file mode 100644 index 0000000000..b2650bcda6 --- /dev/null +++ b/packages/store/abi/EchoSubscriber.sol/EchoSubscriber.abi.json.d.ts @@ -0,0 +1,184 @@ +declare const abi: [ + { + anonymous: false; + inputs: [ + { + indexed: false; + internalType: "bytes"; + name: ""; + type: "bytes"; + } + ]; + name: "HookCalled"; + type: "event"; + }, + { + inputs: [ + { + internalType: "bytes32"; + name: "table"; + type: "bytes32"; + }, + { + internalType: "bytes32[]"; + name: "key"; + type: "bytes32[]"; + }, + { + internalType: "Schema"; + name: "valueSchema"; + type: "bytes32"; + } + ]; + name: "onAfterDeleteRecord"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; + }, + { + inputs: [ + { + internalType: "bytes32"; + name: "table"; + type: "bytes32"; + }, + { + internalType: "bytes32[]"; + name: "key"; + type: "bytes32[]"; + }, + { + internalType: "uint8"; + name: "schemaIndex"; + type: "uint8"; + }, + { + internalType: "bytes"; + name: "data"; + type: "bytes"; + }, + { + internalType: "Schema"; + name: "valueSchem"; + type: "bytes32"; + } + ]; + name: "onAfterSetField"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; + }, + { + inputs: [ + { + internalType: "bytes32"; + name: "table"; + type: "bytes32"; + }, + { + internalType: "bytes32[]"; + name: "key"; + type: "bytes32[]"; + }, + { + internalType: "bytes"; + name: "data"; + type: "bytes"; + }, + { + internalType: "Schema"; + name: "valueSchema"; + type: "bytes32"; + } + ]; + name: "onAfterSetRecord"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; + }, + { + inputs: [ + { + internalType: "bytes32"; + name: "table"; + type: "bytes32"; + }, + { + internalType: "bytes32[]"; + name: "key"; + type: "bytes32[]"; + }, + { + internalType: "Schema"; + name: "valueSchema"; + type: "bytes32"; + } + ]; + name: "onBeforeDeleteRecord"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; + }, + { + inputs: [ + { + internalType: "bytes32"; + name: "table"; + type: "bytes32"; + }, + { + internalType: "bytes32[]"; + name: "key"; + type: "bytes32[]"; + }, + { + internalType: "uint8"; + name: "schemaIndex"; + type: "uint8"; + }, + { + internalType: "bytes"; + name: "data"; + type: "bytes"; + }, + { + internalType: "Schema"; + name: "valueSchema"; + type: "bytes32"; + } + ]; + name: "onBeforeSetField"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; + }, + { + inputs: [ + { + internalType: "bytes32"; + name: "table"; + type: "bytes32"; + }, + { + internalType: "bytes32[]"; + name: "key"; + type: "bytes32[]"; + }, + { + internalType: "bytes"; + name: "data"; + type: "bytes"; + }, + { + internalType: "Schema"; + name: "valueSchema"; + type: "bytes32"; + } + ]; + name: "onBeforeSetRecord"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; + } +]; +export default abi; diff --git a/packages/store/abi/Hook.sol/HookInstance.abi.json b/packages/store/abi/Hook.sol/HookInstance.abi.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/packages/store/abi/Hook.sol/HookInstance.abi.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/packages/store/abi/Hook.sol/HookLib.abi.json b/packages/store/abi/Hook.sol/HookLib.abi.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/packages/store/abi/Hook.sol/HookLib.abi.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/packages/store/abi/IStore.sol/IStore.abi.json b/packages/store/abi/IStore.sol/IStore.abi.json index d21e6f935f..5b408efd0e 100644 --- a/packages/store/abi/IStore.sol/IStore.abi.json +++ b/packages/store/abi/IStore.sol/IStore.abi.json @@ -510,8 +510,13 @@ }, { "internalType": "contract IStoreHook", - "name": "hook", + "name": "hookAddress", "type": "address" + }, + { + "internalType": "uint8", + "name": "enabledHooksBitmap", + "type": "uint8" } ], "name": "registerStoreHook", 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 dbaf46f7db..7d43da2fd0 100644 --- a/packages/store/abi/IStore.sol/IStore.abi.json.d.ts +++ b/packages/store/abi/IStore.sol/IStore.abi.json.d.ts @@ -510,8 +510,13 @@ declare const abi: [ }, { internalType: "contract IStoreHook"; - name: "hook"; + name: "hookAddress"; type: "address"; + }, + { + internalType: "uint8"; + name: "enabledHooksBitmap"; + type: "uint8"; } ]; name: "registerStoreHook"; diff --git a/packages/store/abi/IStore.sol/IStoreHook.abi.json b/packages/store/abi/IStore.sol/IStoreHook.abi.json index 171784fcb2..316af6422c 100644 --- a/packages/store/abi/IStore.sol/IStoreHook.abi.json +++ b/packages/store/abi/IStore.sol/IStoreHook.abi.json @@ -1,4 +1,27 @@ [ + { + "inputs": [ + { + "internalType": "bytes32", + "name": "table", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "key", + "type": "bytes32[]" + }, + { + "internalType": "Schema", + "name": "valueSchema", + "type": "bytes32" + } + ], + "name": "onAfterDeleteRecord", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -44,11 +67,6 @@ "name": "key", "type": "bytes32[]" }, - { - "internalType": "uint8", - "name": "schemaIndex", - "type": "uint8" - }, { "internalType": "bytes", "name": "data", @@ -60,7 +78,7 @@ "type": "bytes32" } ], - "name": "onBeforeSetField", + "name": "onAfterSetRecord", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -83,7 +101,40 @@ "type": "bytes32" } ], - "name": "onDeleteRecord", + "name": "onBeforeDeleteRecord", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "table", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "key", + "type": "bytes32[]" + }, + { + "internalType": "uint8", + "name": "schemaIndex", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "Schema", + "name": "valueSchema", + "type": "bytes32" + } + ], + "name": "onBeforeSetField", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -111,7 +162,7 @@ "type": "bytes32" } ], - "name": "onSetRecord", + "name": "onBeforeSetRecord", "outputs": [], "stateMutability": "nonpayable", "type": "function" diff --git a/packages/store/abi/IStore.sol/IStoreHook.abi.json.d.ts b/packages/store/abi/IStore.sol/IStoreHook.abi.json.d.ts index 233d4d36da..2e39c851eb 100644 --- a/packages/store/abi/IStore.sol/IStoreHook.abi.json.d.ts +++ b/packages/store/abi/IStore.sol/IStoreHook.abi.json.d.ts @@ -1,4 +1,27 @@ declare const abi: [ + { + inputs: [ + { + internalType: "bytes32"; + name: "table"; + type: "bytes32"; + }, + { + internalType: "bytes32[]"; + name: "key"; + type: "bytes32[]"; + }, + { + internalType: "Schema"; + name: "valueSchema"; + type: "bytes32"; + } + ]; + name: "onAfterDeleteRecord"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; + }, { inputs: [ { @@ -44,11 +67,6 @@ declare const abi: [ name: "key"; type: "bytes32[]"; }, - { - internalType: "uint8"; - name: "schemaIndex"; - type: "uint8"; - }, { internalType: "bytes"; name: "data"; @@ -60,7 +78,7 @@ declare const abi: [ type: "bytes32"; } ]; - name: "onBeforeSetField"; + name: "onAfterSetRecord"; outputs: []; stateMutability: "nonpayable"; type: "function"; @@ -83,7 +101,40 @@ declare const abi: [ type: "bytes32"; } ]; - name: "onDeleteRecord"; + name: "onBeforeDeleteRecord"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; + }, + { + inputs: [ + { + internalType: "bytes32"; + name: "table"; + type: "bytes32"; + }, + { + internalType: "bytes32[]"; + name: "key"; + type: "bytes32[]"; + }, + { + internalType: "uint8"; + name: "schemaIndex"; + type: "uint8"; + }, + { + internalType: "bytes"; + name: "data"; + type: "bytes"; + }, + { + internalType: "Schema"; + name: "valueSchema"; + type: "bytes32"; + } + ]; + name: "onBeforeSetField"; outputs: []; stateMutability: "nonpayable"; type: "function"; @@ -111,7 +162,7 @@ declare const abi: [ type: "bytes32"; } ]; - name: "onSetRecord"; + name: "onBeforeSetRecord"; outputs: []; stateMutability: "nonpayable"; type: "function"; diff --git a/packages/store/abi/IStore.sol/IStoreRegistration.abi.json b/packages/store/abi/IStore.sol/IStoreRegistration.abi.json index 0a30f85789..feac4849a3 100644 --- a/packages/store/abi/IStore.sol/IStoreRegistration.abi.json +++ b/packages/store/abi/IStore.sol/IStoreRegistration.abi.json @@ -8,8 +8,13 @@ }, { "internalType": "contract IStoreHook", - "name": "hook", + "name": "hookAddress", "type": "address" + }, + { + "internalType": "uint8", + "name": "enabledHooksBitmap", + "type": "uint8" } ], "name": "registerStoreHook", 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 95262775f6..c03b1d5863 100644 --- a/packages/store/abi/IStore.sol/IStoreRegistration.abi.json.d.ts +++ b/packages/store/abi/IStore.sol/IStoreRegistration.abi.json.d.ts @@ -8,8 +8,13 @@ declare const abi: [ }, { internalType: "contract IStoreHook"; - name: "hook"; + name: "hookAddress"; type: "address"; + }, + { + internalType: "uint8"; + name: "enabledHooksBitmap"; + type: "uint8"; } ]; name: "registerStoreHook"; diff --git a/packages/store/abi/MirrorSubscriber.sol/MirrorSubscriber.abi.json b/packages/store/abi/MirrorSubscriber.sol/MirrorSubscriber.abi.json index c71b2f21d8..7d3335b604 100644 --- a/packages/store/abi/MirrorSubscriber.sol/MirrorSubscriber.abi.json +++ b/packages/store/abi/MirrorSubscriber.sol/MirrorSubscriber.abi.json @@ -94,6 +94,29 @@ "name": "StoreCore_InvalidDataLength", "type": "error" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "table", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "key", + "type": "bytes32[]" + }, + { + "internalType": "Schema", + "name": "valueSchema", + "type": "bytes32" + } + ], + "name": "onAfterDeleteRecord", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -139,11 +162,6 @@ "name": "key", "type": "bytes32[]" }, - { - "internalType": "uint8", - "name": "schemaIndex", - "type": "uint8" - }, { "internalType": "bytes", "name": "data", @@ -155,7 +173,7 @@ "type": "bytes32" } ], - "name": "onBeforeSetField", + "name": "onAfterSetRecord", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -178,7 +196,40 @@ "type": "bytes32" } ], - "name": "onDeleteRecord", + "name": "onBeforeDeleteRecord", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "table", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "key", + "type": "bytes32[]" + }, + { + "internalType": "uint8", + "name": "schemaIndex", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "Schema", + "name": "valueSchema", + "type": "bytes32" + } + ], + "name": "onBeforeSetField", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -206,7 +257,7 @@ "type": "bytes32" } ], - "name": "onSetRecord", + "name": "onBeforeSetRecord", "outputs": [], "stateMutability": "nonpayable", "type": "function" diff --git a/packages/store/abi/MirrorSubscriber.sol/MirrorSubscriber.abi.json.d.ts b/packages/store/abi/MirrorSubscriber.sol/MirrorSubscriber.abi.json.d.ts index 02cf3933f5..14369e1ee9 100644 --- a/packages/store/abi/MirrorSubscriber.sol/MirrorSubscriber.abi.json.d.ts +++ b/packages/store/abi/MirrorSubscriber.sol/MirrorSubscriber.abi.json.d.ts @@ -94,6 +94,29 @@ declare const abi: [ name: "StoreCore_InvalidDataLength"; type: "error"; }, + { + inputs: [ + { + internalType: "bytes32"; + name: "table"; + type: "bytes32"; + }, + { + internalType: "bytes32[]"; + name: "key"; + type: "bytes32[]"; + }, + { + internalType: "Schema"; + name: "valueSchema"; + type: "bytes32"; + } + ]; + name: "onAfterDeleteRecord"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; + }, { inputs: [ { @@ -139,11 +162,6 @@ declare const abi: [ name: "key"; type: "bytes32[]"; }, - { - internalType: "uint8"; - name: "schemaIndex"; - type: "uint8"; - }, { internalType: "bytes"; name: "data"; @@ -155,7 +173,7 @@ declare const abi: [ type: "bytes32"; } ]; - name: "onBeforeSetField"; + name: "onAfterSetRecord"; outputs: []; stateMutability: "nonpayable"; type: "function"; @@ -178,7 +196,40 @@ declare const abi: [ type: "bytes32"; } ]; - name: "onDeleteRecord"; + name: "onBeforeDeleteRecord"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; + }, + { + inputs: [ + { + internalType: "bytes32"; + name: "table"; + type: "bytes32"; + }, + { + internalType: "bytes32[]"; + name: "key"; + type: "bytes32[]"; + }, + { + internalType: "uint8"; + name: "schemaIndex"; + type: "uint8"; + }, + { + internalType: "bytes"; + name: "data"; + type: "bytes"; + }, + { + internalType: "Schema"; + name: "valueSchema"; + type: "bytes32"; + } + ]; + name: "onBeforeSetField"; outputs: []; stateMutability: "nonpayable"; type: "function"; @@ -206,7 +257,7 @@ declare const abi: [ type: "bytes32"; } ]; - name: "onSetRecord"; + name: "onBeforeSetRecord"; outputs: []; stateMutability: "nonpayable"; type: "function"; diff --git a/packages/store/abi/RevertSubscriber.sol/RevertSubscriber.abi.json b/packages/store/abi/RevertSubscriber.sol/RevertSubscriber.abi.json new file mode 100644 index 0000000000..b81dff7d47 --- /dev/null +++ b/packages/store/abi/RevertSubscriber.sol/RevertSubscriber.abi.json @@ -0,0 +1,170 @@ +[ + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "", + "type": "bytes32[]" + }, + { + "internalType": "Schema", + "name": "", + "type": "bytes32" + } + ], + "name": "onAfterDeleteRecord", + "outputs": [], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "", + "type": "bytes32[]" + }, + { + "internalType": "uint8", + "name": "", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + }, + { + "internalType": "Schema", + "name": "", + "type": "bytes32" + } + ], + "name": "onAfterSetField", + "outputs": [], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "", + "type": "bytes32[]" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + }, + { + "internalType": "Schema", + "name": "", + "type": "bytes32" + } + ], + "name": "onAfterSetRecord", + "outputs": [], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "", + "type": "bytes32[]" + }, + { + "internalType": "Schema", + "name": "", + "type": "bytes32" + } + ], + "name": "onBeforeDeleteRecord", + "outputs": [], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "", + "type": "bytes32[]" + }, + { + "internalType": "uint8", + "name": "", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + }, + { + "internalType": "Schema", + "name": "", + "type": "bytes32" + } + ], + "name": "onBeforeSetField", + "outputs": [], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "", + "type": "bytes32[]" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + }, + { + "internalType": "Schema", + "name": "", + "type": "bytes32" + } + ], + "name": "onBeforeSetRecord", + "outputs": [], + "stateMutability": "pure", + "type": "function" + } +] \ No newline at end of file diff --git a/packages/store/abi/RevertSubscriber.sol/RevertSubscriber.abi.json.d.ts b/packages/store/abi/RevertSubscriber.sol/RevertSubscriber.abi.json.d.ts new file mode 100644 index 0000000000..b7eb63707e --- /dev/null +++ b/packages/store/abi/RevertSubscriber.sol/RevertSubscriber.abi.json.d.ts @@ -0,0 +1,171 @@ +declare const abi: [ + { + inputs: [ + { + internalType: "bytes32"; + name: ""; + type: "bytes32"; + }, + { + internalType: "bytes32[]"; + name: ""; + type: "bytes32[]"; + }, + { + internalType: "Schema"; + name: ""; + type: "bytes32"; + } + ]; + name: "onAfterDeleteRecord"; + outputs: []; + stateMutability: "pure"; + type: "function"; + }, + { + inputs: [ + { + internalType: "bytes32"; + name: ""; + type: "bytes32"; + }, + { + internalType: "bytes32[]"; + name: ""; + type: "bytes32[]"; + }, + { + internalType: "uint8"; + name: ""; + type: "uint8"; + }, + { + internalType: "bytes"; + name: ""; + type: "bytes"; + }, + { + internalType: "Schema"; + name: ""; + type: "bytes32"; + } + ]; + name: "onAfterSetField"; + outputs: []; + stateMutability: "pure"; + type: "function"; + }, + { + inputs: [ + { + internalType: "bytes32"; + name: ""; + type: "bytes32"; + }, + { + internalType: "bytes32[]"; + name: ""; + type: "bytes32[]"; + }, + { + internalType: "bytes"; + name: ""; + type: "bytes"; + }, + { + internalType: "Schema"; + name: ""; + type: "bytes32"; + } + ]; + name: "onAfterSetRecord"; + outputs: []; + stateMutability: "pure"; + type: "function"; + }, + { + inputs: [ + { + internalType: "bytes32"; + name: ""; + type: "bytes32"; + }, + { + internalType: "bytes32[]"; + name: ""; + type: "bytes32[]"; + }, + { + internalType: "Schema"; + name: ""; + type: "bytes32"; + } + ]; + name: "onBeforeDeleteRecord"; + outputs: []; + stateMutability: "pure"; + type: "function"; + }, + { + inputs: [ + { + internalType: "bytes32"; + name: ""; + type: "bytes32"; + }, + { + internalType: "bytes32[]"; + name: ""; + type: "bytes32[]"; + }, + { + internalType: "uint8"; + name: ""; + type: "uint8"; + }, + { + internalType: "bytes"; + name: ""; + type: "bytes"; + }, + { + internalType: "Schema"; + name: ""; + type: "bytes32"; + } + ]; + name: "onBeforeSetField"; + outputs: []; + stateMutability: "pure"; + type: "function"; + }, + { + inputs: [ + { + internalType: "bytes32"; + name: ""; + type: "bytes32"; + }, + { + internalType: "bytes32[]"; + name: ""; + type: "bytes32[]"; + }, + { + internalType: "bytes"; + name: ""; + type: "bytes"; + }, + { + internalType: "Schema"; + name: ""; + type: "bytes32"; + } + ]; + name: "onBeforeSetRecord"; + outputs: []; + stateMutability: "pure"; + type: "function"; + } +]; +export default abi; diff --git a/packages/store/abi/StoreHook.sol/StoreHookLib.abi.json b/packages/store/abi/StoreHook.sol/StoreHookLib.abi.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/packages/store/abi/StoreHook.sol/StoreHookLib.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 cb871d1238..d124b7950d 100644 --- a/packages/store/abi/StoreMock.sol/StoreMock.abi.json +++ b/packages/store/abi/StoreMock.sol/StoreMock.abi.json @@ -558,8 +558,13 @@ }, { "internalType": "contract IStoreHook", - "name": "hook", + "name": "hookAddress", "type": "address" + }, + { + "internalType": "uint8", + "name": "enabledHooksBitmap", + "type": "uint8" } ], "name": "registerStoreHook", 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 ebfb7508d1..59c9e4b6bc 100644 --- a/packages/store/abi/StoreMock.sol/StoreMock.abi.json.d.ts +++ b/packages/store/abi/StoreMock.sol/StoreMock.abi.json.d.ts @@ -558,8 +558,13 @@ declare const abi: [ }, { internalType: "contract IStoreHook"; - name: "hook"; + name: "hookAddress"; type: "address"; + }, + { + internalType: "uint8"; + name: "enabledHooksBitmap"; + type: "uint8"; } ]; name: "registerStoreHook"; diff --git a/packages/store/abi/StoreReadWithStubs.sol/StoreReadWithStubs.abi.json b/packages/store/abi/StoreReadWithStubs.sol/StoreReadWithStubs.abi.json index 46ca3e084b..fce41a3237 100644 --- a/packages/store/abi/StoreReadWithStubs.sol/StoreReadWithStubs.abi.json +++ b/packages/store/abi/StoreReadWithStubs.sol/StoreReadWithStubs.abi.json @@ -554,6 +554,11 @@ "internalType": "contract IStoreHook", "name": "", "type": "address" + }, + { + "internalType": "uint8", + "name": "", + "type": "uint8" } ], "name": "registerStoreHook", 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 858431114b..582bb42f9f 100644 --- a/packages/store/abi/StoreReadWithStubs.sol/StoreReadWithStubs.abi.json.d.ts +++ b/packages/store/abi/StoreReadWithStubs.sol/StoreReadWithStubs.abi.json.d.ts @@ -554,6 +554,11 @@ declare const abi: [ internalType: "contract IStoreHook"; name: ""; type: "address"; + }, + { + internalType: "uint8"; + name: ""; + type: "uint8"; } ]; name: "registerStoreHook"; diff --git a/packages/store/gas-report.json b/packages/store/gas-report.json index eb66f1e40b..d5d29f495e 100644 --- a/packages/store/gas-report.json +++ b/packages/store/gas-report.json @@ -239,11 +239,17 @@ "name": "solidity storage load (warm, 10 words)", "gasUsed": 1897 }, + { + "file": "test/Hook.t.sol", + "test": "testIsEnabled", + "name": "check if hook is enabled", + "gasUsed": 114 + }, { "file": "test/KeyEncoding.t.sol", "test": "testRegisterAndGetSchema", "name": "register KeyEncoding schema", - "gasUsed": 669538 + "gasUsed": 669576 }, { "file": "test/Mixed.t.sol", @@ -255,13 +261,13 @@ "file": "test/Mixed.t.sol", "test": "testRegisterAndGetSchema", "name": "register Mixed schema", - "gasUsed": 531229 + "gasUsed": 531309 }, { "file": "test/Mixed.t.sol", "test": "testSetAndGet", "name": "set record in Mixed", - "gasUsed": 107190 + "gasUsed": 107229 }, { "file": "test/Mixed.t.sol", @@ -555,7 +561,7 @@ "file": "test/StoreCoreGas.t.sol", "test": "testDeleteData", "name": "delete record (complex data, 3 slots)", - "gasUsed": 8400 + "gasUsed": 8421 }, { "file": "test/StoreCoreGas.t.sol", @@ -573,49 +579,49 @@ "file": "test/StoreCoreGas.t.sol", "test": "testHooks", "name": "register subscriber", - "gasUsed": 60289 + "gasUsed": 60581 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooks", "name": "set record on table with subscriber", - "gasUsed": 70432 + "gasUsed": 70977 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooks", "name": "set static field on table with subscriber", - "gasUsed": 26264 + "gasUsed": 24327 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooks", "name": "delete record on table with subscriber", - "gasUsed": 18826 + "gasUsed": 19373 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooksDynamicData", "name": "register subscriber", - "gasUsed": 60289 + "gasUsed": 60581 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooksDynamicData", "name": "set (dynamic) record on table with subscriber", - "gasUsed": 163278 + "gasUsed": 163845 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooksDynamicData", "name": "set (dynamic) field on table with subscriber", - "gasUsed": 28200 + "gasUsed": 26263 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooksDynamicData", "name": "delete (dynamic) record on table with subscriber", - "gasUsed": 20432 + "gasUsed": 20846 }, { "file": "test/StoreCoreGas.t.sol", @@ -633,7 +639,7 @@ "file": "test/StoreCoreGas.t.sol", "test": "testRegisterAndGetSchema", "name": "StoreCore: register schema", - "gasUsed": 594571 + "gasUsed": 594625 }, { "file": "test/StoreCoreGas.t.sol", @@ -651,7 +657,7 @@ "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetDynamicData", "name": "set complex record with dynamic data (4 slots)", - "gasUsed": 102577 + "gasUsed": 102615 }, { "file": "test/StoreCoreGas.t.sol", @@ -741,7 +747,7 @@ "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetStaticData", "name": "set static record (1 slot)", - "gasUsed": 32806 + "gasUsed": 32833 }, { "file": "test/StoreCoreGas.t.sol", @@ -753,7 +759,7 @@ "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetStaticDataSpanningWords", "name": "set static record (2 slots)", - "gasUsed": 55309 + "gasUsed": 55336 }, { "file": "test/StoreCoreGas.t.sol", @@ -773,6 +779,36 @@ "name": "push to field (2 slots, 6 uint64 items)", "gasUsed": 14653 }, + { + "file": "test/StoreHook.t.sol", + "test": "testCallHook", + "name": "call an enabled hook", + "gasUsed": 10135 + }, + { + "file": "test/StoreHook.t.sol", + "test": "testCallHook", + "name": "call a disabled hook", + "gasUsed": 144 + }, + { + "file": "test/StoreHook.t.sol", + "test": "testGetAddress", + "name": "get store hook address", + "gasUsed": 1 + }, + { + "file": "test/StoreHook.t.sol", + "test": "testGetBitmap", + "name": "get store hook bitmap", + "gasUsed": 1 + }, + { + "file": "test/StoreHook.t.sol", + "test": "testIsEnabled", + "name": "check if store hook is enabled", + "gasUsed": 129 + }, { "file": "test/StoreSwitch.t.sol", "test": "testDelegatecall", @@ -789,7 +825,7 @@ "file": "test/tables/Callbacks.t.sol", "test": "testSetAndGet", "name": "Callbacks: set field", - "gasUsed": 58202 + "gasUsed": 58198 }, { "file": "test/tables/Callbacks.t.sol", @@ -801,7 +837,13 @@ "file": "test/tables/Callbacks.t.sol", "test": "testSetAndGet", "name": "Callbacks: push 1 element", - "gasUsed": 37654 + "gasUsed": 37650 + }, + { + "file": "test/tables/Hooks.t.sol", + "test": "testOneSlot", + "name": "Hooks: set field with one elements (cold)", + "gasUsed": 60196 }, { "file": "test/tables/Hooks.t.sol", @@ -819,7 +861,7 @@ "file": "test/tables/Hooks.t.sol", "test": "testTable", "name": "Hooks: push 1 element (cold)", - "gasUsed": 37641 + "gasUsed": 17735 }, { "file": "test/tables/Hooks.t.sol", @@ -831,19 +873,19 @@ "file": "test/tables/Hooks.t.sol", "test": "testTable", "name": "Hooks: push 1 element (warm)", - "gasUsed": 15799 + "gasUsed": 15793 }, { "file": "test/tables/Hooks.t.sol", "test": "testTable", "name": "Hooks: update 1 element (warm)", - "gasUsed": 16248 + "gasUsed": 36142 }, { "file": "test/tables/Hooks.t.sol", "test": "testTable", "name": "Hooks: delete record (warm)", - "gasUsed": 9797 + "gasUsed": 9818 }, { "file": "test/tables/Hooks.t.sol", @@ -851,11 +893,23 @@ "name": "Hooks: set field (warm)", "gasUsed": 32418 }, + { + "file": "test/tables/Hooks.t.sol", + "test": "testThreeSlots", + "name": "Hooks: set field with three elements (cold)", + "gasUsed": 82887 + }, + { + "file": "test/tables/Hooks.t.sol", + "test": "testTwoSlots", + "name": "Hooks: set field with two elements (cold)", + "gasUsed": 82795 + }, { "file": "test/tables/HooksColdLoad.t.sol", "test": "testDelete", "name": "Hooks: delete record (cold)", - "gasUsed": 18592 + "gasUsed": 18613 }, { "file": "test/tables/HooksColdLoad.t.sol", @@ -867,7 +921,7 @@ "file": "test/tables/HooksColdLoad.t.sol", "test": "testGetItem", "name": "Hooks: get 1 element (cold)", - "gasUsed": 7063 + "gasUsed": 7080 }, { "file": "test/tables/HooksColdLoad.t.sol", @@ -879,13 +933,13 @@ "file": "test/tables/HooksColdLoad.t.sol", "test": "testPop", "name": "Hooks: pop 1 element (cold)", - "gasUsed": 24231 + "gasUsed": 24235 }, { "file": "test/tables/HooksColdLoad.t.sol", "test": "testUpdate", "name": "Hooks: update 1 element (cold)", - "gasUsed": 25821 + "gasUsed": 25811 }, { "file": "test/tightcoder/DecodeSlice.t.sol", @@ -939,13 +993,13 @@ "file": "test/Vector2.t.sol", "test": "testRegisterAndGetSchema", "name": "register Vector2 schema", - "gasUsed": 392653 + "gasUsed": 392691 }, { "file": "test/Vector2.t.sol", "test": "testSetAndGet", "name": "set Vector2 record", - "gasUsed": 35057 + "gasUsed": 35081 }, { "file": "test/Vector2.t.sol", diff --git a/packages/store/mud.config.ts b/packages/store/mud.config.ts index 6957d88632..bfaa6a5552 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: "address[]", + Hooks: "bytes21[]", Callbacks: "bytes24[]", Tables: { keySchema: { diff --git a/packages/store/src/Hook.sol b/packages/store/src/Hook.sol new file mode 100644 index 0000000000..2e7c28f011 --- /dev/null +++ b/packages/store/src/Hook.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +// 20 bytes address, 1 byte bitmap of enabled hooks +type Hook is bytes21; + +using HookInstance for Hook global; + +library HookLib { + /** + * Encode enabled hooks into a bitmap with 1 bit per hook, and pack the bitmap with the store hook address into a bytes21 value + */ + function encode(address hookAddress, uint8 encodedHooks) internal pure returns (Hook) { + // Move the address to the leftmost 20 bytes and the bitmap to the rightmost byte + return Hook.wrap(bytes21(bytes20(hookAddress)) | bytes21(uint168(encodedHooks))); + } +} + +library HookInstance { + /** + * Check if the given hook type is enabled in the hook + */ + function isEnabled(Hook self, uint8 hookType) internal pure returns (bool) { + // Pick the bitmap encoded in the rightmost byte from the hook and check if the bit at the given hook type is set + return (getBitmap(self) & (1 << uint8(hookType))) != 0; + } + + /** + * Get the hook's address + */ + function getAddress(Hook self) internal pure returns (address) { + // Extract the address from the leftmost 20 bytes + return address(bytes20(Hook.unwrap(self))); + } + + /** + * Get the store hook's bitmap + */ + function getBitmap(Hook self) internal pure returns (uint8) { + // Extract the bitmap from the rightmost bytes + return uint8(uint168(Hook.unwrap(self))); + } +} diff --git a/packages/store/src/IStore.sol b/packages/store/src/IStore.sol index cc38319efb..147aeab0f8 100644 --- a/packages/store/src/IStore.sol +++ b/packages/store/src/IStore.sol @@ -123,15 +123,16 @@ interface IStoreRegistration { ) external; // Register hook to be called when a record or field is set or deleted - function registerStoreHook(bytes32 table, IStoreHook hook) external; + function registerStoreHook(bytes32 table, IStoreHook hookAddress, uint8 enabledHooksBitmap) external; } interface IStore is IStoreData, IStoreRegistration, IStoreEphemeral, IStoreErrors {} interface IStoreHook { - function onSetRecord(bytes32 table, bytes32[] memory key, bytes memory data, Schema valueSchema) external; + function onBeforeSetRecord(bytes32 table, bytes32[] memory key, bytes memory data, Schema valueSchema) external; + + function onAfterSetRecord(bytes32 table, bytes32[] memory key, bytes memory data, Schema valueSchema) external; - // Split onSetField into pre and post to simplify the implementation of hooks function onBeforeSetField( bytes32 table, bytes32[] memory key, @@ -148,5 +149,7 @@ interface IStoreHook { Schema valueSchema ) external; - function onDeleteRecord(bytes32 table, bytes32[] memory key, Schema valueSchema) external; + function onBeforeDeleteRecord(bytes32 table, bytes32[] memory key, Schema valueSchema) external; + + function onAfterDeleteRecord(bytes32 table, bytes32[] memory key, Schema valueSchema) external; } diff --git a/packages/store/src/StoreCore.sol b/packages/store/src/StoreCore.sol index f3b5776476..8c7754580d 100644 --- a/packages/store/src/StoreCore.sol +++ b/packages/store/src/StoreCore.sol @@ -12,6 +12,8 @@ import { Hooks, Tables, HooksTableId } 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 { StoreHookLib, StoreHookType } from "./StoreHook.sol"; library StoreCore { // note: the preimage of the tuple of keys used to index is part of the event, so it can be used by indexers @@ -119,8 +121,8 @@ library StoreCore { /* * Register hooks to be called when a record or field is set or deleted */ - function registerStoreHook(bytes32 tableId, IStoreHook hook) internal { - Hooks.push(tableId, address(hook)); + function registerStoreHook(bytes32 tableId, IStoreHook hookAddress, uint8 enabledHooksBitmap) internal { + Hooks.push(tableId, Hook.unwrap(StoreHookLib.encode(hookAddress, enabledHooksBitmap))); } /************************************************************************ @@ -142,11 +144,13 @@ library StoreCore { // Emit event to notify indexers emit StoreSetRecord(tableId, key, data); - // Call onSetRecord hooks (before actually modifying the state, so observers have access to the previous state if needed) - address[] memory hooks = Hooks.get(tableId); + // Call onBeforeSetRecord hooks (before actually modifying the state, so observers have access to the previous state if needed) + bytes21[] memory hooks = Hooks.get(tableId); for (uint256 i; i < hooks.length; i++) { - IStoreHook hook = IStoreHook(hooks[i]); - hook.onSetRecord(tableId, key, data, valueSchema); + Hook hook = Hook.wrap(hooks[i]); + if (hook.isEnabled(uint8(StoreHookType.BEFORE_SET_RECORD))) { + IStoreHook(hook.getAddress()).onBeforeSetRecord(tableId, key, data, valueSchema); + } } // Store the static data at the static data location @@ -160,28 +164,36 @@ library StoreCore { }); memoryPointer += staticLength + 32; // move the memory pointer to the start of the dynamic data (skip the encoded dynamic length) - // If there is no dynamic data, we're done - if (valueSchema.numDynamicFields() == 0) return; - - // Store the dynamic data length at the dynamic data length location - uint256 dynamicDataLengthLocation = StoreCoreInternal._getDynamicDataLengthLocation(tableId, key); - Storage.store({ storagePointer: dynamicDataLengthLocation, data: dynamicLength.unwrap() }); - - // For every dynamic element, slice off the dynamic data and store it at the dynamic location - uint256 dynamicDataLocation; - uint256 dynamicDataLength; - for (uint8 i; i < valueSchema.numDynamicFields(); ) { - dynamicDataLocation = StoreCoreInternal._getDynamicDataLocation(tableId, key, i); - dynamicDataLength = dynamicLength.atIndex(i); - Storage.store({ - storagePointer: dynamicDataLocation, - offset: 0, - memoryPointer: memoryPointer, - length: dynamicDataLength - }); - memoryPointer += dynamicDataLength; // move the memory pointer to the start of the next dynamic data - unchecked { - i++; + // Set the dynamic data if there are dynamic fields + if (valueSchema.numDynamicFields() > 0) { + // Store the dynamic data length at the dynamic data length location + uint256 dynamicDataLengthLocation = StoreCoreInternal._getDynamicDataLengthLocation(tableId, key); + Storage.store({ storagePointer: dynamicDataLengthLocation, data: dynamicLength.unwrap() }); + + // For every dynamic element, slice off the dynamic data and store it at the dynamic location + uint256 dynamicDataLocation; + uint256 dynamicDataLength; + for (uint8 i; i < valueSchema.numDynamicFields(); ) { + dynamicDataLocation = StoreCoreInternal._getDynamicDataLocation(tableId, key, i); + dynamicDataLength = dynamicLength.atIndex(i); + Storage.store({ + storagePointer: dynamicDataLocation, + offset: 0, + memoryPointer: memoryPointer, + length: dynamicDataLength + }); + memoryPointer += dynamicDataLength; // move the memory pointer to the start of the next dynamic data + unchecked { + i++; + } + } + } + + // Call onAfterSetRecord hooks (after modifying the state) + for (uint256 i; i < hooks.length; i++) { + Hook hook = Hook.wrap(hooks[i]); + if (hook.isEnabled(uint8(StoreHookType.AFTER_SET_RECORD))) { + IStoreHook(hook.getAddress()).onAfterSetRecord(tableId, key, data, valueSchema); } } } @@ -200,11 +212,12 @@ library StoreCore { emit StoreSetField(tableId, key, schemaIndex, data); // Call onBeforeSetField hooks (before modifying the state) - address[] memory hooks = Hooks.get(tableId); - + bytes21[] memory hooks = Hooks.get(tableId); for (uint256 i; i < hooks.length; i++) { - IStoreHook hook = IStoreHook(hooks[i]); - hook.onBeforeSetField(tableId, key, schemaIndex, data, valueSchema); + Hook hook = Hook.wrap(hooks[i]); + if (hook.isEnabled(uint8(StoreHookType.BEFORE_SET_FIELD))) { + IStoreHook(hook.getAddress()).onBeforeSetField(tableId, key, schemaIndex, data, valueSchema); + } } if (schemaIndex < valueSchema.numStaticFields()) { @@ -215,8 +228,10 @@ library StoreCore { // Call onAfterSetField hooks (after modifying the state) for (uint256 i; i < hooks.length; i++) { - IStoreHook hook = IStoreHook(hooks[i]); - hook.onAfterSetField(tableId, key, schemaIndex, data, valueSchema); + Hook hook = Hook.wrap(hooks[i]); + if (hook.isEnabled(uint8(StoreHookType.AFTER_SET_FIELD))) { + IStoreHook(hook.getAddress()).onAfterSetField(tableId, key, schemaIndex, data, valueSchema); + } } } @@ -227,23 +242,32 @@ library StoreCore { // Emit event to notify indexers emit StoreDeleteRecord(tableId, key); - // Call onDeleteRecord hooks (before actually modifying the state, so observers have access to the previous state if needed) - address[] memory hooks = Hooks.get(tableId); + // Call onBeforeDeleteRecord hooks (before actually modifying the state, so observers have access to the previous state if needed) + bytes21[] memory hooks = Hooks.get(tableId); for (uint256 i; i < hooks.length; i++) { - IStoreHook hook = IStoreHook(hooks[i]); - hook.onDeleteRecord(tableId, key, valueSchema); + Hook hook = Hook.wrap(hooks[i]); + if (hook.isEnabled(uint8(StoreHookType.BEFORE_DELETE_RECORD))) { + IStoreHook(hook.getAddress()).onBeforeDeleteRecord(tableId, key, valueSchema); + } } // Delete static data uint256 staticDataLocation = StoreCoreInternal._getStaticDataLocation(tableId, key); Storage.store({ storagePointer: staticDataLocation, offset: 0, data: new bytes(valueSchema.staticDataLength()) }); - // If there are no dynamic fields, we're done - if (valueSchema.numDynamicFields() == 0) return; + // If there are dynamic fields, delete the dynamic data length + if (valueSchema.numDynamicFields() > 0) { + uint256 dynamicDataLengthLocation = StoreCoreInternal._getDynamicDataLengthLocation(tableId, key); + Storage.store({ storagePointer: dynamicDataLengthLocation, data: bytes32(0) }); + } - // Delete dynamic data length - uint256 dynamicDataLengthLocation = StoreCoreInternal._getDynamicDataLengthLocation(tableId, key); - Storage.store({ storagePointer: dynamicDataLengthLocation, data: bytes32(0) }); + // Call onAfterDeleteRecord hooks + for (uint256 i; i < hooks.length; i++) { + Hook hook = Hook.wrap(hooks[i]); + if (hook.isEnabled(uint8(StoreHookType.AFTER_DELETE_RECORD))) { + IStoreHook(hook.getAddress()).onAfterDeleteRecord(tableId, key, valueSchema); + } + } } /** @@ -270,18 +294,22 @@ library StoreCore { emit StoreSetField(tableId, key, schemaIndex, fullData); // Call onBeforeSetField hooks (before modifying the state) - address[] memory hooks = Hooks.get(tableId); + bytes21[] memory hooks = Hooks.get(tableId); for (uint256 i; i < hooks.length; i++) { - IStoreHook hook = IStoreHook(hooks[i]); - hook.onBeforeSetField(tableId, key, schemaIndex, fullData, valueSchema); + Hook hook = Hook.wrap(hooks[i]); + if (hook.isEnabled(uint8(StoreHookType.BEFORE_SET_FIELD))) { + IStoreHook(hook.getAddress()).onBeforeSetField(tableId, key, schemaIndex, fullData, valueSchema); + } } StoreCoreInternal._pushToDynamicField(tableId, key, valueSchema, schemaIndex, dataToPush); // Call onAfterSetField hooks (after modifying the state) for (uint256 i; i < hooks.length; i++) { - IStoreHook hook = IStoreHook(hooks[i]); - hook.onAfterSetField(tableId, key, schemaIndex, fullData, valueSchema); + Hook hook = Hook.wrap(hooks[i]); + if (hook.isEnabled(uint8(StoreHookType.AFTER_SET_FIELD))) { + IStoreHook(hook.getAddress()).onAfterSetField(tableId, key, schemaIndex, fullData, valueSchema); + } } } @@ -310,18 +338,22 @@ library StoreCore { emit StoreSetField(tableId, key, schemaIndex, fullData); // Call onBeforeSetField hooks (before modifying the state) - address[] memory hooks = Hooks.get(tableId); + bytes21[] memory hooks = Hooks.get(tableId); for (uint256 i; i < hooks.length; i++) { - IStoreHook hook = IStoreHook(hooks[i]); - hook.onBeforeSetField(tableId, key, schemaIndex, fullData, valueSchema); + Hook hook = Hook.wrap(hooks[i]); + if (hook.isEnabled(uint8(StoreHookType.BEFORE_SET_FIELD))) { + IStoreHook(hook.getAddress()).onBeforeSetField(tableId, key, schemaIndex, fullData, valueSchema); + } } StoreCoreInternal._popFromDynamicField(tableId, key, valueSchema, schemaIndex, byteLengthToPop); // Call onAfterSetField hooks (after modifying the state) for (uint256 i; i < hooks.length; i++) { - IStoreHook hook = IStoreHook(hooks[i]); - hook.onAfterSetField(tableId, key, schemaIndex, fullData, valueSchema); + Hook hook = Hook.wrap(hooks[i]); + if (hook.isEnabled(uint8(StoreHookType.AFTER_SET_FIELD))) { + IStoreHook(hook.getAddress()).onAfterSetField(tableId, key, schemaIndex, fullData, valueSchema); + } } } @@ -360,18 +392,22 @@ library StoreCore { emit StoreSetField(tableId, key, schemaIndex, fullData); // Call onBeforeSetField hooks (before modifying the state) - address[] memory hooks = Hooks.get(tableId); + bytes21[] memory hooks = Hooks.get(tableId); for (uint256 i; i < hooks.length; i++) { - IStoreHook hook = IStoreHook(hooks[i]); - hook.onBeforeSetField(tableId, key, schemaIndex, fullData, valueSchema); + Hook hook = Hook.wrap(hooks[i]); + if (hook.isEnabled(uint8(StoreHookType.BEFORE_SET_FIELD))) { + IStoreHook(hook.getAddress()).onBeforeSetField(tableId, key, schemaIndex, fullData, valueSchema); + } } StoreCoreInternal._setDynamicFieldItem(tableId, key, valueSchema, schemaIndex, startByteIndex, dataToSet); // Call onAfterSetField hooks (after modifying the state) for (uint256 i; i < hooks.length; i++) { - IStoreHook hook = IStoreHook(hooks[i]); - hook.onAfterSetField(tableId, key, schemaIndex, fullData, valueSchema); + Hook hook = Hook.wrap(hooks[i]); + if (hook.isEnabled(uint8(StoreHookType.AFTER_SET_FIELD))) { + IStoreHook(hook.getAddress()).onAfterSetField(tableId, key, schemaIndex, fullData, valueSchema); + } } } @@ -390,13 +426,6 @@ library StoreCore { // Emit event to notify indexers emit StoreEphemeralRecord(tableId, key, data); - - // Call onSetRecord hooks - address[] memory hooks = Hooks.get(tableId); - for (uint256 i; i < hooks.length; i++) { - IStoreHook hook = IStoreHook(hooks[i]); - hook.onSetRecord(tableId, key, data, valueSchema); - } } /************************************************************************ diff --git a/packages/store/src/StoreHook.sol b/packages/store/src/StoreHook.sol new file mode 100644 index 0000000000..82cacb7cc2 --- /dev/null +++ b/packages/store/src/StoreHook.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import { Hook, HookLib } from "./Hook.sol"; +import { IStoreHook } from "./IStore.sol"; + +enum StoreHookType { + BEFORE_SET_RECORD, + AFTER_SET_RECORD, + BEFORE_SET_FIELD, + AFTER_SET_FIELD, + BEFORE_DELETE_RECORD, + AFTER_DELETE_RECORD +} + +library StoreHookLib { + /** + * Encode the bitmap into a single byte + */ + function encodeBitmap( + bool onBeforeSetRecord, + bool onAfterSetRecord, + bool onBeforeSetField, + bool onAfterSetField, + bool onBeforeDeleteRecord, + bool onAfterDeleteRecord + ) internal pure returns (uint8) { + uint256 bitmap = 0; + if (onBeforeSetRecord) bitmap |= 1 << uint8(StoreHookType.BEFORE_SET_RECORD); + if (onAfterSetRecord) bitmap |= 1 << uint8(StoreHookType.AFTER_SET_RECORD); + if (onBeforeSetField) bitmap |= 1 << uint8(StoreHookType.BEFORE_SET_FIELD); + if (onAfterSetField) bitmap |= 1 << uint8(StoreHookType.AFTER_SET_FIELD); + if (onBeforeDeleteRecord) bitmap |= 1 << uint8(StoreHookType.BEFORE_DELETE_RECORD); + if (onAfterDeleteRecord) bitmap |= 1 << uint8(StoreHookType.AFTER_DELETE_RECORD); + return uint8(bitmap); + } + + /** + * Encode enabled hooks into a bitmap with 1 bit per hook, and pack the bitmap with the store hook address into a bytes21 value + */ + function encode(IStoreHook storeHook, uint8 enabledHooksBitmap) internal pure returns (Hook) { + return HookLib.encode(address(storeHook), enabledHooksBitmap); + } +} diff --git a/packages/store/src/StoreReadWithStubs.sol b/packages/store/src/StoreReadWithStubs.sol index ee655284f2..761cef9231 100644 --- a/packages/store/src/StoreReadWithStubs.sol +++ b/packages/store/src/StoreReadWithStubs.sol @@ -59,7 +59,7 @@ contract StoreReadWithStubs is IStore, StoreRead { /** * Not implemented in StoreReadWithStubs */ - function registerStoreHook(bytes32, IStoreHook) public virtual { + function registerStoreHook(bytes32, IStoreHook, uint8) public virtual { revert StoreReadWithStubs_NotImplemented(); } diff --git a/packages/store/src/StoreSwitch.sol b/packages/store/src/StoreSwitch.sol index 9350a80635..2df6755858 100644 --- a/packages/store/src/StoreSwitch.sol +++ b/packages/store/src/StoreSwitch.sol @@ -44,12 +44,12 @@ library StoreSwitch { _layout().storeAddress = _storeAddress; } - function registerStoreHook(bytes32 table, IStoreHook hook) internal { + function registerStoreHook(bytes32 table, IStoreHook hookAddress, uint8 enabledHooksBitmap) internal { address _storeAddress = getStoreAddress(); if (_storeAddress == address(this)) { - StoreCore.registerStoreHook(table, hook); + StoreCore.registerStoreHook(table, hookAddress, enabledHooksBitmap); } else { - IStore(_storeAddress).registerStoreHook(table, hook); + IStore(_storeAddress).registerStoreHook(table, hookAddress, enabledHooksBitmap); } } diff --git a/packages/store/src/codegen/tables/Hooks.sol b/packages/store/src/codegen/tables/Hooks.sol index ba3545cdbc..37d268c143 100644 --- a/packages/store/src/codegen/tables/Hooks.sol +++ b/packages/store/src/codegen/tables/Hooks.sol @@ -32,7 +32,7 @@ library Hooks { /** Get the table's value schema */ function getValueSchema() internal pure returns (Schema) { SchemaType[] memory _schema = new SchemaType[](1); - _schema[0] = SchemaType.ADDRESS_ARRAY; + _schema[0] = SchemaType.BYTES21_ARRAY; return SchemaLib.encode(_schema); } @@ -60,25 +60,25 @@ library Hooks { } /** Get value */ - function get(bytes32 key) internal view returns (address[] memory 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_address()); + return (SliceLib.getSubslice(_blob, 0, _blob.length).decodeArray_bytes21()); } /** Get value (using the specified store) */ - function get(IStore _store, bytes32 key) internal view returns (address[] memory value) { + 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_address()); + return (SliceLib.getSubslice(_blob, 0, _blob.length).decodeArray_bytes21()); } /** Set value */ - function set(bytes32 key, address[] memory value) internal { + function set(bytes32 key, bytes21[] memory value) internal { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = key; @@ -86,7 +86,7 @@ library Hooks { } /** Set value (using the specified store) */ - function set(IStore _store, bytes32 key, address[] memory value) internal { + function set(IStore _store, bytes32 key, bytes21[] memory value) internal { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = key; @@ -100,7 +100,7 @@ library Hooks { uint256 _byteLength = StoreSwitch.getFieldLength(_tableId, _keyTuple, 0, getValueSchema()); unchecked { - return _byteLength / 20; + return _byteLength / 21; } } @@ -111,7 +111,7 @@ library Hooks { uint256 _byteLength = _store.getFieldLength(_tableId, _keyTuple, 0, getValueSchema()); unchecked { - return _byteLength / 20; + return _byteLength / 21; } } @@ -119,7 +119,7 @@ library Hooks { * Get an item of value * (unchecked, returns invalid data if index overflows) */ - function getItem(bytes32 key, uint256 _index) internal view returns (address) { + function getItem(bytes32 key, uint256 _index) internal view returns (bytes21) { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = key; @@ -129,10 +129,10 @@ library Hooks { _keyTuple, 0, getValueSchema(), - _index * 20, - (_index + 1) * 20 + _index * 21, + (_index + 1) * 21 ); - return (address(Bytes.slice20(_blob, 0))); + return (Bytes.slice21(_blob, 0)); } } @@ -140,7 +140,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 (address) { + function getItem(IStore _store, bytes32 key, uint256 _index) internal view returns (bytes21) { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = key; @@ -150,15 +150,15 @@ library Hooks { _keyTuple, 0, getValueSchema(), - _index * 20, - (_index + 1) * 20 + _index * 21, + (_index + 1) * 21 ); - return (address(Bytes.slice20(_blob, 0))); + return (Bytes.slice21(_blob, 0)); } } /** Push an element to value */ - function push(bytes32 key, address _element) internal { + function push(bytes32 key, bytes21 _element) internal { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = key; @@ -166,7 +166,7 @@ library Hooks { } /** Push an element to value (using the specified store) */ - function push(IStore _store, bytes32 key, address _element) internal { + function push(IStore _store, bytes32 key, bytes21 _element) internal { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = key; @@ -178,7 +178,7 @@ library Hooks { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = key; - StoreSwitch.popFromField(_tableId, _keyTuple, 0, 20, getValueSchema()); + StoreSwitch.popFromField(_tableId, _keyTuple, 0, 21, getValueSchema()); } /** Pop an element from value (using the specified store) */ @@ -186,19 +186,19 @@ library Hooks { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = key; - _store.popFromField(_tableId, _keyTuple, 0, 20, getValueSchema()); + _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, address _element) internal { + 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 * 20, abi.encodePacked((_element)), getValueSchema()); + StoreSwitch.updateInField(_tableId, _keyTuple, 0, _index * 21, abi.encodePacked((_element)), getValueSchema()); } } @@ -206,21 +206,21 @@ 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, address _element) internal { + 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 * 20, abi.encodePacked((_element)), getValueSchema()); + _store.updateInField(_tableId, _keyTuple, 0, _index * 21, abi.encodePacked((_element)), getValueSchema()); } } /** Tightly pack full data using this table's schema */ - function encode(address[] memory value) internal pure returns (bytes memory) { + 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 * 20); + _encodedLengths = PackedCounterLib.pack(value.length * 21); } return abi.encodePacked(_encodedLengths.unwrap(), EncodeArray.encode((value))); diff --git a/packages/store/test/EchoSubscriber.sol b/packages/store/test/EchoSubscriber.sol new file mode 100644 index 0000000000..6c7682b4bc --- /dev/null +++ b/packages/store/test/EchoSubscriber.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import { IStoreHook } from "../src/IStore.sol"; +import { Schema } from "../src/Schema.sol"; + +contract EchoSubscriber is IStoreHook { + event HookCalled(bytes); + + 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 valueSchem + ) public { + emit HookCalled(abi.encode(table, key, schemaIndex, data, valueSchem)); + } + + function onBeforeDeleteRecord(bytes32 table, bytes32[] memory key, Schema valueSchema) public { + emit HookCalled(abi.encode(table, key, valueSchema)); + } + + function onAfterDeleteRecord(bytes32 table, bytes32[] memory key, Schema valueSchema) public { + emit HookCalled(abi.encode(table, key, valueSchema)); + } +} diff --git a/packages/store/test/Hook.t.sol b/packages/store/test/Hook.t.sol new file mode 100644 index 0000000000..c328d701d4 --- /dev/null +++ b/packages/store/test/Hook.t.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import "forge-std/Test.sol"; +import { GasReporter } from "@latticexyz/gas-report/src/GasReporter.sol"; +import { Hook, HookLib } from "../src/Hook.sol"; + +contract HookTest is Test, GasReporter { + uint8 public NONE = 0x00; // 0b00000000 + uint8 public FIRST = 0x01; // 0b00000001 + uint8 public SECOND = 0x02; // 0b00000010 + uint8 public THIRD = 0x04; // 0b00000100 + uint8 public FOURTH = 0x08; // 0b00001000 + uint8 public FIFTH = 0x10; // 0b00010000 + uint8 public SIXTH = 0x20; // 0b00100000 + uint8 public SEVENTH = 0x40; // 0b01000000 + uint8 public EIGHTH = 0x80; // 0b10000000 + + function testFuzzEncode(address hookAddress, uint8 encodedHooks) public { + assertEq( + Hook.unwrap(HookLib.encode(hookAddress, encodedHooks)), + bytes21(abi.encodePacked(hookAddress, encodedHooks)) + ); + } + + function testIsEnabled() public { + Hook hook = HookLib.encode(address(0), THIRD); + + startGasReport("check if hook is enabled"); + hook.isEnabled(0); + endGasReport(); + + assertFalse(hook.isEnabled(0)); + assertFalse(hook.isEnabled(1)); + assertTrue(hook.isEnabled(2)); + assertFalse(hook.isEnabled(3)); + assertFalse(hook.isEnabled(4)); + assertFalse(hook.isEnabled(5)); + assertFalse(hook.isEnabled(6)); + assertFalse(hook.isEnabled(7)); + } + + function testFuzzIsEnabled( + address hookAddress, + bool first, + bool second, + bool third, + bool fourth, + bool fifth, + bool sixth, + bool seventh, + bool eighth + ) public { + uint8 encodedHooks = 0x00; + if (first) encodedHooks |= FIRST; + if (second) encodedHooks |= SECOND; + if (third) encodedHooks |= THIRD; + if (fourth) encodedHooks |= FOURTH; + if (fifth) encodedHooks |= FIFTH; + if (sixth) encodedHooks |= SIXTH; + if (seventh) encodedHooks |= SEVENTH; + if (eighth) encodedHooks |= EIGHTH; + + Hook hook = HookLib.encode(hookAddress, encodedHooks); + + assertEq(hook.isEnabled(0), first); + assertEq(hook.isEnabled(1), second); + assertEq(hook.isEnabled(2), third); + assertEq(hook.isEnabled(3), fourth); + assertEq(hook.isEnabled(4), fifth); + assertEq(hook.isEnabled(5), sixth); + assertEq(hook.isEnabled(6), seventh); + assertEq(hook.isEnabled(7), eighth); + } + + function testFuzzGetAddressAndBitmap(address hookAddress, uint8 encodedHooks) public { + Hook hook = HookLib.encode(hookAddress, encodedHooks); + + assertEq(hook.getAddress(), hookAddress); + assertEq(hook.getBitmap(), encodedHooks); + } +} diff --git a/packages/store/test/MirrorSubscriber.sol b/packages/store/test/MirrorSubscriber.sol index 9276111340..b5adf0f5a2 100644 --- a/packages/store/test/MirrorSubscriber.sol +++ b/packages/store/test/MirrorSubscriber.sol @@ -21,11 +21,15 @@ contract MirrorSubscriber is IStoreHook { _table = table; } - function onSetRecord(bytes32 table, bytes32[] memory key, bytes memory data, Schema valueSchema) public { + function onBeforeSetRecord(bytes32 table, bytes32[] memory key, bytes memory data, Schema valueSchema) public { if (table != table) revert("invalid table"); StoreSwitch.setRecord(indexerTableId, key, data, valueSchema); } + function onAfterSetRecord(bytes32 table, bytes32[] memory key, bytes memory data, Schema valueSchema) public { + // NOOP + } + function onBeforeSetField( bytes32 table, bytes32[] memory key, @@ -37,10 +41,16 @@ contract MirrorSubscriber is IStoreHook { StoreSwitch.setField(indexerTableId, key, schemaIndex, data, valueSchema); } - function onAfterSetField(bytes32, bytes32[] memory, uint8, bytes memory, Schema) public {} + function onAfterSetField(bytes32, bytes32[] memory, uint8, bytes memory, Schema) public { + // NOOP + } - function onDeleteRecord(bytes32 table, bytes32[] memory key, Schema valueSchema) public { + function onBeforeDeleteRecord(bytes32 table, bytes32[] memory key, Schema valueSchema) public { if (table != table) revert("invalid table"); StoreSwitch.deleteRecord(indexerTableId, key, valueSchema); } + + function onAfterDeleteRecord(bytes32 table, bytes32[] memory key, Schema valueSchema) public { + // NOOP + } } diff --git a/packages/store/test/RevertSubscriber.sol b/packages/store/test/RevertSubscriber.sol new file mode 100644 index 0000000000..38d48a86a0 --- /dev/null +++ b/packages/store/test/RevertSubscriber.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import { IStoreHook } from "../src/IStore.sol"; +import { Schema } from "../src/Schema.sol"; + +contract RevertSubscriber is IStoreHook { + function onBeforeSetRecord(bytes32, bytes32[] memory, bytes memory, Schema) public pure { + revert("onBeforeSetRecord"); + } + + function onAfterSetRecord(bytes32, bytes32[] memory, bytes memory, Schema) public pure { + revert("onAfterSetRecord"); + } + + function onBeforeSetField(bytes32, bytes32[] memory, uint8, bytes memory, Schema) public pure { + revert("onBeforeSetField"); + } + + function onAfterSetField(bytes32, bytes32[] memory, uint8, bytes memory, Schema) public pure { + revert("onAfterSetField"); + } + + function onBeforeDeleteRecord(bytes32, bytes32[] memory, Schema) public pure { + revert("onBeforeDeleteRecord"); + } + + function onAfterDeleteRecord(bytes32, bytes32[] memory, Schema) public pure { + revert("onAfterDeleteRecord"); + } +} diff --git a/packages/store/test/StoreCore.t.sol b/packages/store/test/StoreCore.t.sol index 23fd259c5d..2f6f4240b1 100644 --- a/packages/store/test/StoreCore.t.sol +++ b/packages/store/test/StoreCore.t.sol @@ -14,6 +14,7 @@ import { IStoreErrors } from "../src/IStoreErrors.sol"; import { IStore } from "../src/IStore.sol"; import { StoreSwitch } from "../src/StoreSwitch.sol"; import { Tables, TablesTableId } from "../src/codegen/Tables.sol"; +import { StoreHookLib } from "../src/StoreHook.sol"; import { SchemaEncodeHelper } from "./SchemaEncodeHelper.sol"; import { StoreMock } from "./StoreMock.sol"; import { MirrorSubscriber, indexerTableId } from "./MirrorSubscriber.sol"; @@ -838,7 +839,18 @@ contract StoreCoreTest is Test, StoreMock { new string[](1) ); - IStore(this).registerStoreHook(table, subscriber); + IStore(this).registerStoreHook( + table, + subscriber, + StoreHookLib.encodeBitmap({ + onBeforeSetRecord: true, + onAfterSetRecord: false, + onBeforeSetField: true, + onAfterSetField: false, + onBeforeDeleteRecord: true, + onAfterDeleteRecord: false + }) + ); bytes memory data = abi.encodePacked(bytes16(0x0102030405060708090a0b0c0d0e0f10)); @@ -881,7 +893,18 @@ contract StoreCoreTest is Test, StoreMock { new string[](2) ); - IStore(this).registerStoreHook(table, subscriber); + IStore(this).registerStoreHook( + table, + subscriber, + StoreHookLib.encodeBitmap({ + onBeforeSetRecord: true, + onAfterSetRecord: false, + onBeforeSetField: true, + onAfterSetField: false, + onBeforeDeleteRecord: true, + onAfterDeleteRecord: false + }) + ); uint32[] memory arrayData = new uint32[](1); arrayData[0] = 0x01020304; diff --git a/packages/store/test/StoreCoreGas.t.sol b/packages/store/test/StoreCoreGas.t.sol index 9c0f7fa722..ace0c8141f 100644 --- a/packages/store/test/StoreCoreGas.t.sol +++ b/packages/store/test/StoreCoreGas.t.sol @@ -13,6 +13,7 @@ import { PackedCounter, PackedCounterLib } from "../src/PackedCounter.sol"; import { StoreReadWithStubs } from "../src/StoreReadWithStubs.sol"; import { IStoreErrors } from "../src/IStoreErrors.sol"; import { IStore } from "../src/IStore.sol"; +import { StoreHookLib } from "../src/StoreHook.sol"; import { SchemaEncodeHelper } from "./SchemaEncodeHelper.sol"; import { StoreMock } from "./StoreMock.sol"; import { MirrorSubscriber } from "./MirrorSubscriber.sol"; @@ -597,7 +598,18 @@ contract StoreCoreGasTest is Test, GasReporter, StoreMock { ); startGasReport("register subscriber"); - StoreCore.registerStoreHook(table, subscriber); + StoreCore.registerStoreHook( + table, + subscriber, + StoreHookLib.encodeBitmap({ + onBeforeSetRecord: true, + onAfterSetRecord: false, + onBeforeSetField: true, + onAfterSetField: false, + onBeforeDeleteRecord: true, + onAfterDeleteRecord: false + }) + ); endGasReport(); bytes memory data = abi.encodePacked(bytes16(0x0102030405060708090a0b0c0d0e0f10)); @@ -636,7 +648,18 @@ contract StoreCoreGasTest is Test, GasReporter, StoreMock { ); startGasReport("register subscriber"); - StoreCore.registerStoreHook(table, subscriber); + StoreCore.registerStoreHook( + table, + subscriber, + StoreHookLib.encodeBitmap({ + onBeforeSetRecord: true, + onAfterSetRecord: false, + onBeforeSetField: true, + onAfterSetField: false, + onBeforeDeleteRecord: true, + onAfterDeleteRecord: false + }) + ); endGasReport(); uint32[] memory arrayData = new uint32[](1); diff --git a/packages/store/test/StoreHook.t.sol b/packages/store/test/StoreHook.t.sol new file mode 100644 index 0000000000..fe35a244b2 --- /dev/null +++ b/packages/store/test/StoreHook.t.sol @@ -0,0 +1,309 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import "forge-std/Test.sol"; +import { GasReporter } from "@latticexyz/gas-report/src/GasReporter.sol"; + +import { EchoSubscriber } from "./EchoSubscriber.sol"; +import { RevertSubscriber } from "./RevertSubscriber.sol"; + +import { Hook } from "../src/Hook.sol"; +import { StoreHookType } from "../src/StoreHook.sol"; +import { StoreHookLib } from "../src/StoreHook.sol"; +import { IStoreHook } from "../src/IStore.sol"; +import { Schema } from "../src/Schema.sol"; + +contract StoreHookTest is Test, GasReporter { + event HookCalled(bytes); + + // Testdata + EchoSubscriber private echoSubscriber = new EchoSubscriber(); + RevertSubscriber private revertSubscriber = new RevertSubscriber(); + bytes32 private tableId = "table"; + bytes32[] private key = new bytes32[](1); + bytes private data = "data"; + uint8 private schemaIndex = 1; + Schema private valueSchema = Schema.wrap(0); + + function testEncodeBitmap() public { + assertEq( + StoreHookLib.encodeBitmap({ + onBeforeSetRecord: false, + onAfterSetRecord: false, + onBeforeSetField: false, + onAfterSetField: false, + onBeforeDeleteRecord: false, + onAfterDeleteRecord: false + }), + uint8(0x00), + "0b00000000" + ); + + assertEq( + StoreHookLib.encodeBitmap({ + onBeforeSetRecord: true, + onAfterSetRecord: false, + onBeforeSetField: false, + onAfterSetField: false, + onBeforeDeleteRecord: false, + onAfterDeleteRecord: false + }), + uint8(0x01), + "0b00000001" + ); + + assertEq( + StoreHookLib.encodeBitmap({ + onBeforeSetRecord: false, + onAfterSetRecord: true, + onBeforeSetField: false, + onAfterSetField: false, + onBeforeDeleteRecord: false, + onAfterDeleteRecord: false + }), + uint8(0x02), + "0b00000010" + ); + + assertEq( + StoreHookLib.encodeBitmap({ + onBeforeSetRecord: false, + onAfterSetRecord: false, + onBeforeSetField: true, + onAfterSetField: false, + onBeforeDeleteRecord: false, + onAfterDeleteRecord: false + }), + uint8(0x04), + "0b00000100" + ); + + assertEq( + StoreHookLib.encodeBitmap({ + onBeforeSetRecord: false, + onAfterSetRecord: false, + onBeforeSetField: false, + onAfterSetField: true, + onBeforeDeleteRecord: false, + onAfterDeleteRecord: false + }), + uint8(0x08), + "0b00001000" + ); + + assertEq( + StoreHookLib.encodeBitmap({ + onBeforeSetRecord: false, + onAfterSetRecord: false, + onBeforeSetField: false, + onAfterSetField: false, + onBeforeDeleteRecord: true, + onAfterDeleteRecord: false + }), + uint8(0x10), + "0b00010000" + ); + + assertEq( + StoreHookLib.encodeBitmap({ + onBeforeSetRecord: false, + onAfterSetRecord: false, + onBeforeSetField: false, + onAfterSetField: false, + onBeforeDeleteRecord: false, + onAfterDeleteRecord: true + }), + uint8(0x20), + "0b00100000" + ); + + assertEq( + StoreHookLib.encodeBitmap({ + onBeforeSetRecord: true, + onAfterSetRecord: true, + onBeforeSetField: true, + onAfterSetField: true, + onBeforeDeleteRecord: true, + onAfterDeleteRecord: true + }), + uint8(0x3f), + "0b00111111" + ); + } + + function testEncode() public { + assertEq( + Hook.unwrap( + StoreHookLib.encode( + echoSubscriber, + StoreHookLib.encodeBitmap({ + onBeforeSetRecord: true, + onAfterSetRecord: true, + onBeforeSetField: true, + onAfterSetField: true, + onBeforeDeleteRecord: true, + onAfterDeleteRecord: true + }) + ) + ), + bytes21(abi.encodePacked(echoSubscriber, uint8(0x3f))) + ); + } + + function testFuzzEncode( + address hookAddress, + bool enableBeforeSetRecord, + bool enableAfterSetRecord, + bool enableBeforeSetField, + bool enableAfterSetField, + bool enableBeforeDeleteRecord, + bool enableAfterDeleteRecord + ) public { + uint8 encodedBitmap = StoreHookLib.encodeBitmap({ + onBeforeSetRecord: enableBeforeSetRecord, + onAfterSetRecord: enableAfterSetRecord, + onBeforeSetField: enableBeforeSetField, + onAfterSetField: enableAfterSetField, + onBeforeDeleteRecord: enableBeforeDeleteRecord, + onAfterDeleteRecord: enableAfterDeleteRecord + }); + assertEq( + Hook.unwrap(StoreHookLib.encode(IStoreHook(hookAddress), encodedBitmap)), + bytes21(abi.encodePacked(hookAddress, encodedBitmap)) + ); + } + + function testIsEnabled() public { + Hook storeHook = StoreHookLib.encode( + echoSubscriber, + StoreHookLib.encodeBitmap({ + onBeforeSetRecord: false, + onAfterSetRecord: false, + onBeforeSetField: true, + onAfterSetField: false, + onBeforeDeleteRecord: false, + onAfterDeleteRecord: false + }) + ); + + startGasReport("check if store hook is enabled"); + storeHook.isEnabled(uint8(StoreHookType.BEFORE_SET_RECORD)); + endGasReport(); + + assertEq(storeHook.isEnabled(uint8(StoreHookType.BEFORE_SET_RECORD)), false); + assertEq(storeHook.isEnabled(uint8(StoreHookType.AFTER_SET_RECORD)), false); + assertEq(storeHook.isEnabled(uint8(StoreHookType.BEFORE_SET_FIELD)), true); + assertEq(storeHook.isEnabled(uint8(StoreHookType.AFTER_SET_FIELD)), false); + assertEq(storeHook.isEnabled(uint8(StoreHookType.BEFORE_DELETE_RECORD)), false); + assertEq(storeHook.isEnabled(uint8(StoreHookType.AFTER_DELETE_RECORD)), false); + } + + function testFuzzIsEnabled( + address hookAddress, + bool enableBeforeSetRecord, + bool enableAfterSetRecord, + bool enableBeforeSetField, + bool enableAfterSetField, + bool enableBeforeDeleteRecord, + bool enableAfterDeleteRecord + ) public { + Hook storeHook = StoreHookLib.encode( + IStoreHook(hookAddress), + StoreHookLib.encodeBitmap({ + onBeforeSetRecord: enableBeforeSetRecord, + onAfterSetRecord: enableAfterSetRecord, + onBeforeSetField: enableBeforeSetField, + onAfterSetField: enableAfterSetField, + onBeforeDeleteRecord: enableBeforeDeleteRecord, + onAfterDeleteRecord: enableAfterDeleteRecord + }) + ); + + assertEq(storeHook.isEnabled(uint8(StoreHookType.BEFORE_SET_RECORD)), enableBeforeSetRecord); + assertEq(storeHook.isEnabled(uint8(StoreHookType.AFTER_SET_RECORD)), enableAfterSetRecord); + assertEq(storeHook.isEnabled(uint8(StoreHookType.BEFORE_SET_FIELD)), enableBeforeSetField); + assertEq(storeHook.isEnabled(uint8(StoreHookType.AFTER_SET_FIELD)), enableAfterSetField); + assertEq(storeHook.isEnabled(uint8(StoreHookType.BEFORE_DELETE_RECORD)), enableBeforeDeleteRecord); + assertEq(storeHook.isEnabled(uint8(StoreHookType.AFTER_DELETE_RECORD)), enableAfterDeleteRecord); + } + + function testGetAddress() public { + Hook storeHook = StoreHookLib.encode( + echoSubscriber, + StoreHookLib.encodeBitmap({ + onBeforeSetRecord: false, + onAfterSetRecord: false, + onBeforeSetField: true, + onAfterSetField: false, + onBeforeDeleteRecord: false, + onAfterDeleteRecord: false + }) + ); + + startGasReport("get store hook address"); + storeHook.getAddress(); + endGasReport(); + + assertEq(storeHook.getAddress(), address(echoSubscriber)); + } + + function testGetBitmap() public { + uint8 encodedBitmap = StoreHookLib.encodeBitmap({ + onBeforeSetRecord: false, + onAfterSetRecord: false, + onBeforeSetField: true, + onAfterSetField: false, + onBeforeDeleteRecord: false, + onAfterDeleteRecord: false + }); + + Hook storeHook = StoreHookLib.encode(echoSubscriber, encodedBitmap); + + startGasReport("get store hook bitmap"); + storeHook.getBitmap(); + endGasReport(); + + assertEq(storeHook.getBitmap(), encodedBitmap); + } + + function testCallHook() public { + Hook storeHook = StoreHookLib.encode( + echoSubscriber, + StoreHookLib.encodeBitmap({ + onBeforeSetRecord: true, + onAfterSetRecord: false, + onBeforeSetField: false, + onAfterSetField: false, + onBeforeDeleteRecord: false, + onAfterDeleteRecord: false + }) + ); + + vm.expectEmit(true, true, true, true); + emit HookCalled(abi.encode(tableId, key, data, valueSchema)); + startGasReport("call an enabled hook"); + if (storeHook.isEnabled(uint8(StoreHookType.BEFORE_SET_RECORD))) { + IStoreHook(storeHook.getAddress()).onBeforeSetRecord(tableId, key, data, valueSchema); + } + endGasReport(); + + Hook revertHook = StoreHookLib.encode( + revertSubscriber, + StoreHookLib.encodeBitmap({ + onBeforeSetRecord: false, + onAfterSetRecord: false, + onBeforeSetField: false, + onAfterSetField: false, + onBeforeDeleteRecord: false, + onAfterDeleteRecord: false + }) + ); + + // Expect the to not be called - otherwise the test will fail with a revert + startGasReport("call a disabled hook"); + if (revertHook.isEnabled(uint8(StoreHookType.BEFORE_SET_RECORD))) { + IStoreHook(revertHook.getAddress()).onBeforeSetRecord(tableId, key, data, valueSchema); + } + endGasReport(); + } +} diff --git a/packages/store/test/StoreMock.sol b/packages/store/test/StoreMock.sol index d58e7299c9..6425642b65 100644 --- a/packages/store/test/StoreMock.sol +++ b/packages/store/test/StoreMock.sol @@ -81,7 +81,7 @@ contract StoreMock is IStore, StoreRead { } // Register hook to be called when a record or field is set or deleted - function registerStoreHook(bytes32 table, IStoreHook hook) public { - StoreCore.registerStoreHook(table, hook); + function registerStoreHook(bytes32 table, IStoreHook hookAddress, uint8 enabledHooksBitmap) public { + StoreCore.registerStoreHook(table, hookAddress, enabledHooksBitmap); } } diff --git a/packages/store/test/tables/Hooks.t.sol b/packages/store/test/tables/Hooks.t.sol index 2c7a1e2f8d..1c035e44c9 100644 --- a/packages/store/test/tables/Hooks.t.sol +++ b/packages/store/test/tables/Hooks.t.sol @@ -11,66 +11,99 @@ contract HooksTest is Test, GasReporter, StoreReadWithStubs { // Hooks schema is already registered by StoreCore bytes32 key = keccak256("somekey"); - address[] memory addresses = new address[](1); - addresses[0] = address(this); + bytes21[] memory hooks = new bytes21[](1); + hooks[0] = bytes21("some data"); startGasReport("Hooks: set field (cold)"); - Hooks.set(key, addresses); + Hooks.set(key, hooks); endGasReport(); startGasReport("Hooks: get field (warm)"); - address[] memory returnedAddresses = Hooks.get(key); + bytes21[] memory returnedHooks = Hooks.get(key); endGasReport(); - assertEq(returnedAddresses.length, addresses.length); - assertEq(returnedAddresses[0], addresses[0]); + assertEq(returnedHooks.length, hooks.length); + assertEq(returnedHooks[0], hooks[0]); startGasReport("Hooks: push 1 element (cold)"); - Hooks.push(key, addresses[0]); + Hooks.push(key, hooks[0]); endGasReport(); - returnedAddresses = Hooks.get(key); + returnedHooks = Hooks.get(key); - assertEq(returnedAddresses.length, 2); - assertEq(returnedAddresses[1], addresses[0]); + assertEq(returnedHooks.length, 2); + assertEq(returnedHooks[1], hooks[0]); startGasReport("Hooks: pop 1 element (warm)"); Hooks.pop(key); endGasReport(); - returnedAddresses = Hooks.get(key); + returnedHooks = Hooks.get(key); - assertEq(returnedAddresses.length, 1); - assertEq(returnedAddresses[0], addresses[0]); + assertEq(returnedHooks.length, 1); + assertEq(returnedHooks[0], hooks[0]); startGasReport("Hooks: push 1 element (warm)"); - Hooks.push(key, addresses[0]); + Hooks.push(key, hooks[0]); endGasReport(); - returnedAddresses = Hooks.get(key); + returnedHooks = Hooks.get(key); - assertEq(returnedAddresses.length, 2); - assertEq(returnedAddresses[1], addresses[0]); + assertEq(returnedHooks.length, 2); + assertEq(returnedHooks[1], hooks[0]); - address newAddress = address(bytes20(keccak256("alice"))); + bytes21 newHook = bytes21(keccak256("alice")); startGasReport("Hooks: update 1 element (warm)"); - Hooks.update(key, 1, newAddress); + Hooks.update(key, 1, newHook); endGasReport(); - returnedAddresses = Hooks.get(key); - assertEq(returnedAddresses.length, 2); - assertEq(returnedAddresses[0], addresses[0]); - assertEq(returnedAddresses[1], newAddress); + returnedHooks = Hooks.get(key); + assertEq(returnedHooks.length, 2); + assertEq(returnedHooks[0], hooks[0]); + assertEq(returnedHooks[1], newHook); startGasReport("Hooks: delete record (warm)"); Hooks.deleteRecord(key); endGasReport(); - returnedAddresses = Hooks.get(key); - assertEq(returnedAddresses.length, 0); + returnedHooks = Hooks.get(key); + assertEq(returnedHooks.length, 0); startGasReport("Hooks: set field (warm)"); - Hooks.set(key, addresses); + Hooks.set(key, hooks); + endGasReport(); + } + + function testOneSlot() public { + bytes32 key1 = keccak256("somekey"); + bytes21[] memory hooks = new bytes21[](1); + hooks[0] = bytes21("some data"); + + startGasReport("Hooks: set field with one elements (cold)"); + Hooks.set(key1, hooks); + endGasReport(); + } + + function testTwoSlots() public { + bytes32 key2 = keccak256("somekey"); + bytes21[] memory hooks = new bytes21[](2); + hooks[0] = bytes21("some data"); + hooks[1] = bytes21("some other data"); + + startGasReport("Hooks: set field with two elements (cold)"); + Hooks.set(key2, hooks); + endGasReport(); + } + + function testThreeSlots() public { + bytes32 key3 = keccak256("somekey"); + bytes21[] memory hooks = new bytes21[](3); + hooks[0] = bytes21("some data"); + 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); endGasReport(); } } diff --git a/packages/store/test/tables/HooksColdLoad.t.sol b/packages/store/test/tables/HooksColdLoad.t.sol index 30b2c6c644..2e0ff5b0e9 100644 --- a/packages/store/test/tables/HooksColdLoad.t.sol +++ b/packages/store/test/tables/HooksColdLoad.t.sol @@ -7,27 +7,27 @@ import { StoreReadWithStubs } from "../../src/StoreReadWithStubs.sol"; import { Hooks } from "../../src/codegen/Tables.sol"; contract HooksColdLoadTest is Test, GasReporter, StoreReadWithStubs { - address[] addresses; + bytes21[] hooks; function setUp() public { // Hooks schema is already registered by StoreCore bytes32 key = keccak256("somekey"); - addresses = new address[](1); - addresses[0] = address(this); + hooks = new bytes21[](1); + hooks[0] = bytes21("some data"); - Hooks.set(key, addresses); + Hooks.set(key, hooks); } function testGet() public { bytes32 key = keccak256("somekey"); startGasReport("Hooks: get field (cold)"); - address[] memory returnedAddresses = Hooks.get(key); + bytes21[] memory returnedAddresses = Hooks.get(key); endGasReport(); - assertEq(returnedAddresses.length, addresses.length); - assertEq(returnedAddresses[0], addresses[0]); + assertEq(returnedAddresses.length, hooks.length); + assertEq(returnedAddresses[0], hooks[0]); } function testLength() public { @@ -37,17 +37,17 @@ contract HooksColdLoadTest is Test, GasReporter, StoreReadWithStubs { uint256 length = Hooks.length(key); endGasReport(); - assertEq(length, addresses.length); + assertEq(length, hooks.length); } function testGetItem() public { bytes32 key = keccak256("somekey"); startGasReport("Hooks: get 1 element (cold)"); - address returnedAddress = Hooks.getItem(key, 0); + bytes21 returnedAddress = Hooks.getItem(key, 0); endGasReport(); - assertEq(returnedAddress, addresses[0]); + assertEq(returnedAddress, hooks[0]); } function testPop() public { @@ -59,18 +59,18 @@ contract HooksColdLoadTest is Test, GasReporter, StoreReadWithStubs { uint256 length = Hooks.length(key); - assertEq(length, addresses.length - 1); + assertEq(length, hooks.length - 1); } function testUpdate() public { bytes32 key = keccak256("somekey"); - address newAddress = address(bytes20(keccak256("alice"))); + bytes21 newAddress = bytes21(bytes20(keccak256("alice"))); startGasReport("Hooks: update 1 element (cold)"); Hooks.update(key, 0, newAddress); endGasReport(); - address[] memory returnedAddresses = Hooks.get(key); + bytes21[] memory returnedAddresses = Hooks.get(key); assertEq(returnedAddresses.length, 1); assertEq(returnedAddresses[0], newAddress); } @@ -82,7 +82,7 @@ contract HooksColdLoadTest is Test, GasReporter, StoreReadWithStubs { Hooks.deleteRecord(key); endGasReport(); - address[] memory returnedAddresses = Hooks.get(key); + bytes21[] memory returnedAddresses = Hooks.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 decf383ad6..bf331e49e6 100644 --- a/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json +++ b/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json @@ -417,8 +417,13 @@ }, { "internalType": "contract IStoreHook", - "name": "hook", + "name": "hookAddress", "type": "address" + }, + { + "internalType": "uint8", + "name": "enabledHooksBitmap", + "type": "uint8" } ], "name": "registerStoreHook", @@ -458,8 +463,13 @@ }, { "internalType": "contract ISystemHook", - "name": "hook", + "name": "hookAddress", "type": "address" + }, + { + "internalType": "uint8", + "name": "enabledHooksBitmap", + "type": "uint8" } ], "name": "registerSystemHook", 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 4d919071af..44a9fef39a 100644 --- a/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json.d.ts +++ b/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json.d.ts @@ -417,8 +417,13 @@ declare const abi: [ }, { internalType: "contract IStoreHook"; - name: "hook"; + name: "hookAddress"; type: "address"; + }, + { + internalType: "uint8"; + name: "enabledHooksBitmap"; + type: "uint8"; } ]; name: "registerStoreHook"; @@ -458,8 +463,13 @@ declare const abi: [ }, { internalType: "contract ISystemHook"; - name: "hook"; + name: "hookAddress"; type: "address"; + }, + { + internalType: "uint8"; + name: "enabledHooksBitmap"; + type: "uint8"; } ]; name: "registerSystemHook"; diff --git a/packages/world/abi/EphemeralRecordSystem.sol/EphemeralRecordSystem.abi.json b/packages/world/abi/EphemeralRecordSystem.sol/EphemeralRecordSystem.abi.json index 3b31063c2b..bd15689e98 100644 --- a/packages/world/abi/EphemeralRecordSystem.sol/EphemeralRecordSystem.abi.json +++ b/packages/world/abi/EphemeralRecordSystem.sol/EphemeralRecordSystem.abi.json @@ -31,27 +31,6 @@ "name": "SchemaLib_StaticTypeAfterDynamicType", "type": "error" }, - { - "inputs": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint256", - "name": "start", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "end", - "type": "uint256" - } - ], - "name": "Slice_OutOfBounds", - "type": "error" - }, { "inputs": [ { diff --git a/packages/world/abi/EphemeralRecordSystem.sol/EphemeralRecordSystem.abi.json.d.ts b/packages/world/abi/EphemeralRecordSystem.sol/EphemeralRecordSystem.abi.json.d.ts index 1998c46ee9..69fc2e4772 100644 --- a/packages/world/abi/EphemeralRecordSystem.sol/EphemeralRecordSystem.abi.json.d.ts +++ b/packages/world/abi/EphemeralRecordSystem.sol/EphemeralRecordSystem.abi.json.d.ts @@ -31,27 +31,6 @@ declare const abi: [ name: "SchemaLib_StaticTypeAfterDynamicType"; type: "error"; }, - { - inputs: [ - { - internalType: "bytes"; - name: "data"; - type: "bytes"; - }, - { - internalType: "uint256"; - name: "start"; - type: "uint256"; - }, - { - internalType: "uint256"; - name: "end"; - type: "uint256"; - } - ]; - name: "Slice_OutOfBounds"; - type: "error"; - }, { inputs: [ { diff --git a/packages/world/abi/Hook.sol/HookInstance.abi.json b/packages/world/abi/Hook.sol/HookInstance.abi.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/packages/world/abi/Hook.sol/HookInstance.abi.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/packages/world/abi/Hook.sol/HookLib.abi.json b/packages/world/abi/Hook.sol/HookLib.abi.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/packages/world/abi/Hook.sol/HookLib.abi.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/packages/world/abi/IBaseWorld.sol/IBaseWorld.abi.json b/packages/world/abi/IBaseWorld.sol/IBaseWorld.abi.json index 0396d362ca..e9d08b90b4 100644 --- a/packages/world/abi/IBaseWorld.sol/IBaseWorld.abi.json +++ b/packages/world/abi/IBaseWorld.sol/IBaseWorld.abi.json @@ -826,8 +826,13 @@ }, { "internalType": "contract IStoreHook", - "name": "hook", + "name": "hookAddress", "type": "address" + }, + { + "internalType": "uint8", + "name": "enabledHooksBitmap", + "type": "uint8" } ], "name": "registerStoreHook", @@ -867,8 +872,13 @@ }, { "internalType": "contract ISystemHook", - "name": "hook", + "name": "hookAddress", "type": "address" + }, + { + "internalType": "uint8", + "name": "enabledHooksBitmap", + "type": "uint8" } ], "name": "registerSystemHook", 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 df6d9dccba..b72d53aaac 100644 --- a/packages/world/abi/IBaseWorld.sol/IBaseWorld.abi.json.d.ts +++ b/packages/world/abi/IBaseWorld.sol/IBaseWorld.abi.json.d.ts @@ -826,8 +826,13 @@ declare const abi: [ }, { internalType: "contract IStoreHook"; - name: "hook"; + name: "hookAddress"; type: "address"; + }, + { + internalType: "uint8"; + name: "enabledHooksBitmap"; + type: "uint8"; } ]; name: "registerStoreHook"; @@ -867,8 +872,13 @@ declare const abi: [ }, { internalType: "contract ISystemHook"; - name: "hook"; + name: "hookAddress"; type: "address"; + }, + { + internalType: "uint8"; + name: "enabledHooksBitmap"; + type: "uint8"; } ]; name: "registerSystemHook"; diff --git a/packages/world/abi/IStore.sol/IStore.abi.json b/packages/world/abi/IStore.sol/IStore.abi.json index d21e6f935f..5b408efd0e 100644 --- a/packages/world/abi/IStore.sol/IStore.abi.json +++ b/packages/world/abi/IStore.sol/IStore.abi.json @@ -510,8 +510,13 @@ }, { "internalType": "contract IStoreHook", - "name": "hook", + "name": "hookAddress", "type": "address" + }, + { + "internalType": "uint8", + "name": "enabledHooksBitmap", + "type": "uint8" } ], "name": "registerStoreHook", 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 dbaf46f7db..7d43da2fd0 100644 --- a/packages/world/abi/IStore.sol/IStore.abi.json.d.ts +++ b/packages/world/abi/IStore.sol/IStore.abi.json.d.ts @@ -510,8 +510,13 @@ declare const abi: [ }, { internalType: "contract IStoreHook"; - name: "hook"; + name: "hookAddress"; type: "address"; + }, + { + internalType: "uint8"; + name: "enabledHooksBitmap"; + type: "uint8"; } ]; name: "registerStoreHook"; diff --git a/packages/world/abi/IStore.sol/IStoreHook.abi.json b/packages/world/abi/IStore.sol/IStoreHook.abi.json index 171784fcb2..316af6422c 100644 --- a/packages/world/abi/IStore.sol/IStoreHook.abi.json +++ b/packages/world/abi/IStore.sol/IStoreHook.abi.json @@ -1,4 +1,27 @@ [ + { + "inputs": [ + { + "internalType": "bytes32", + "name": "table", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "key", + "type": "bytes32[]" + }, + { + "internalType": "Schema", + "name": "valueSchema", + "type": "bytes32" + } + ], + "name": "onAfterDeleteRecord", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -44,11 +67,6 @@ "name": "key", "type": "bytes32[]" }, - { - "internalType": "uint8", - "name": "schemaIndex", - "type": "uint8" - }, { "internalType": "bytes", "name": "data", @@ -60,7 +78,7 @@ "type": "bytes32" } ], - "name": "onBeforeSetField", + "name": "onAfterSetRecord", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -83,7 +101,40 @@ "type": "bytes32" } ], - "name": "onDeleteRecord", + "name": "onBeforeDeleteRecord", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "table", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "key", + "type": "bytes32[]" + }, + { + "internalType": "uint8", + "name": "schemaIndex", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "Schema", + "name": "valueSchema", + "type": "bytes32" + } + ], + "name": "onBeforeSetField", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -111,7 +162,7 @@ "type": "bytes32" } ], - "name": "onSetRecord", + "name": "onBeforeSetRecord", "outputs": [], "stateMutability": "nonpayable", "type": "function" diff --git a/packages/world/abi/IStore.sol/IStoreHook.abi.json.d.ts b/packages/world/abi/IStore.sol/IStoreHook.abi.json.d.ts index 233d4d36da..2e39c851eb 100644 --- a/packages/world/abi/IStore.sol/IStoreHook.abi.json.d.ts +++ b/packages/world/abi/IStore.sol/IStoreHook.abi.json.d.ts @@ -1,4 +1,27 @@ declare const abi: [ + { + inputs: [ + { + internalType: "bytes32"; + name: "table"; + type: "bytes32"; + }, + { + internalType: "bytes32[]"; + name: "key"; + type: "bytes32[]"; + }, + { + internalType: "Schema"; + name: "valueSchema"; + type: "bytes32"; + } + ]; + name: "onAfterDeleteRecord"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; + }, { inputs: [ { @@ -44,11 +67,6 @@ declare const abi: [ name: "key"; type: "bytes32[]"; }, - { - internalType: "uint8"; - name: "schemaIndex"; - type: "uint8"; - }, { internalType: "bytes"; name: "data"; @@ -60,7 +78,7 @@ declare const abi: [ type: "bytes32"; } ]; - name: "onBeforeSetField"; + name: "onAfterSetRecord"; outputs: []; stateMutability: "nonpayable"; type: "function"; @@ -83,7 +101,40 @@ declare const abi: [ type: "bytes32"; } ]; - name: "onDeleteRecord"; + name: "onBeforeDeleteRecord"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; + }, + { + inputs: [ + { + internalType: "bytes32"; + name: "table"; + type: "bytes32"; + }, + { + internalType: "bytes32[]"; + name: "key"; + type: "bytes32[]"; + }, + { + internalType: "uint8"; + name: "schemaIndex"; + type: "uint8"; + }, + { + internalType: "bytes"; + name: "data"; + type: "bytes"; + }, + { + internalType: "Schema"; + name: "valueSchema"; + type: "bytes32"; + } + ]; + name: "onBeforeSetField"; outputs: []; stateMutability: "nonpayable"; type: "function"; @@ -111,7 +162,7 @@ declare const abi: [ type: "bytes32"; } ]; - name: "onSetRecord"; + name: "onBeforeSetRecord"; outputs: []; stateMutability: "nonpayable"; type: "function"; diff --git a/packages/world/abi/IStore.sol/IStoreRegistration.abi.json b/packages/world/abi/IStore.sol/IStoreRegistration.abi.json index 0a30f85789..feac4849a3 100644 --- a/packages/world/abi/IStore.sol/IStoreRegistration.abi.json +++ b/packages/world/abi/IStore.sol/IStoreRegistration.abi.json @@ -8,8 +8,13 @@ }, { "internalType": "contract IStoreHook", - "name": "hook", + "name": "hookAddress", "type": "address" + }, + { + "internalType": "uint8", + "name": "enabledHooksBitmap", + "type": "uint8" } ], "name": "registerStoreHook", 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 95262775f6..c03b1d5863 100644 --- a/packages/world/abi/IStore.sol/IStoreRegistration.abi.json.d.ts +++ b/packages/world/abi/IStore.sol/IStoreRegistration.abi.json.d.ts @@ -8,8 +8,13 @@ declare const abi: [ }, { internalType: "contract IStoreHook"; - name: "hook"; + name: "hookAddress"; type: "address"; + }, + { + internalType: "uint8"; + name: "enabledHooksBitmap"; + type: "uint8"; } ]; name: "registerStoreHook"; diff --git a/packages/world/abi/IWorldRegistrationSystem.sol/IWorldRegistrationSystem.abi.json b/packages/world/abi/IWorldRegistrationSystem.sol/IWorldRegistrationSystem.abi.json index 27592788d5..a2b26c0d1a 100644 --- a/packages/world/abi/IWorldRegistrationSystem.sol/IWorldRegistrationSystem.abi.json +++ b/packages/world/abi/IWorldRegistrationSystem.sol/IWorldRegistrationSystem.abi.json @@ -125,8 +125,13 @@ }, { "internalType": "contract ISystemHook", - "name": "hook", + "name": "hookAddress", "type": "address" + }, + { + "internalType": "uint8", + "name": "enabledHooksBitmap", + "type": "uint8" } ], "name": "registerSystemHook", 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 42e2db59d2..91e71a7fb6 100644 --- a/packages/world/abi/IWorldRegistrationSystem.sol/IWorldRegistrationSystem.abi.json.d.ts +++ b/packages/world/abi/IWorldRegistrationSystem.sol/IWorldRegistrationSystem.abi.json.d.ts @@ -125,8 +125,13 @@ declare const abi: [ }, { internalType: "contract ISystemHook"; - name: "hook"; + name: "hookAddress"; type: "address"; + }, + { + internalType: "uint8"; + name: "enabledHooksBitmap"; + type: "uint8"; } ]; name: "registerSystemHook"; diff --git a/packages/world/abi/KeysInTableHook.sol/KeysInTableHook.abi.json b/packages/world/abi/KeysInTableHook.sol/KeysInTableHook.abi.json index 3b28134403..d4a8fde8ea 100644 --- a/packages/world/abi/KeysInTableHook.sol/KeysInTableHook.abi.json +++ b/packages/world/abi/KeysInTableHook.sol/KeysInTableHook.abi.json @@ -84,6 +84,29 @@ "name": "StoreCore_NotDynamicField", "type": "error" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "table", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "key", + "type": "bytes32[]" + }, + { + "internalType": "Schema", + "name": "valueSchema", + "type": "bytes32" + } + ], + "name": "onAfterDeleteRecord", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -129,11 +152,6 @@ "name": "key", "type": "bytes32[]" }, - { - "internalType": "uint8", - "name": "", - "type": "uint8" - }, { "internalType": "bytes", "name": "", @@ -145,7 +163,7 @@ "type": "bytes32" } ], - "name": "onBeforeSetField", + "name": "onAfterSetRecord", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -168,7 +186,40 @@ "type": "bytes32" } ], - "name": "onDeleteRecord", + "name": "onBeforeDeleteRecord", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "table", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "key", + "type": "bytes32[]" + }, + { + "internalType": "uint8", + "name": "", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + }, + { + "internalType": "Schema", + "name": "", + "type": "bytes32" + } + ], + "name": "onBeforeSetField", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -196,7 +247,7 @@ "type": "bytes32" } ], - "name": "onSetRecord", + "name": "onBeforeSetRecord", "outputs": [], "stateMutability": "nonpayable", "type": "function" diff --git a/packages/world/abi/KeysInTableHook.sol/KeysInTableHook.abi.json.d.ts b/packages/world/abi/KeysInTableHook.sol/KeysInTableHook.abi.json.d.ts index 423b95c859..ac7b1c5e5b 100644 --- a/packages/world/abi/KeysInTableHook.sol/KeysInTableHook.abi.json.d.ts +++ b/packages/world/abi/KeysInTableHook.sol/KeysInTableHook.abi.json.d.ts @@ -84,6 +84,29 @@ declare const abi: [ name: "StoreCore_NotDynamicField"; type: "error"; }, + { + inputs: [ + { + internalType: "bytes32"; + name: "table"; + type: "bytes32"; + }, + { + internalType: "bytes32[]"; + name: "key"; + type: "bytes32[]"; + }, + { + internalType: "Schema"; + name: "valueSchema"; + type: "bytes32"; + } + ]; + name: "onAfterDeleteRecord"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; + }, { inputs: [ { @@ -129,11 +152,6 @@ declare const abi: [ name: "key"; type: "bytes32[]"; }, - { - internalType: "uint8"; - name: ""; - type: "uint8"; - }, { internalType: "bytes"; name: ""; @@ -145,7 +163,7 @@ declare const abi: [ type: "bytes32"; } ]; - name: "onBeforeSetField"; + name: "onAfterSetRecord"; outputs: []; stateMutability: "nonpayable"; type: "function"; @@ -168,7 +186,40 @@ declare const abi: [ type: "bytes32"; } ]; - name: "onDeleteRecord"; + name: "onBeforeDeleteRecord"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; + }, + { + inputs: [ + { + internalType: "bytes32"; + name: "table"; + type: "bytes32"; + }, + { + internalType: "bytes32[]"; + name: "key"; + type: "bytes32[]"; + }, + { + internalType: "uint8"; + name: ""; + type: "uint8"; + }, + { + internalType: "bytes"; + name: ""; + type: "bytes"; + }, + { + internalType: "Schema"; + name: ""; + type: "bytes32"; + } + ]; + name: "onBeforeSetField"; outputs: []; stateMutability: "nonpayable"; type: "function"; @@ -196,7 +247,7 @@ declare const abi: [ type: "bytes32"; } ]; - name: "onSetRecord"; + name: "onBeforeSetRecord"; outputs: []; stateMutability: "nonpayable"; type: "function"; diff --git a/packages/world/abi/KeysWithValueHook.sol/KeysWithValueHook.abi.json b/packages/world/abi/KeysWithValueHook.sol/KeysWithValueHook.abi.json index cba5bb5b68..0df0c4e92f 100644 --- a/packages/world/abi/KeysWithValueHook.sol/KeysWithValueHook.abi.json +++ b/packages/world/abi/KeysWithValueHook.sol/KeysWithValueHook.abi.json @@ -68,6 +68,29 @@ "name": "StoreCore_NotDynamicField", "type": "error" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "sourceTableId", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "key", + "type": "bytes32[]" + }, + { + "internalType": "Schema", + "name": "valueSchema", + "type": "bytes32" + } + ], + "name": "onAfterDeleteRecord", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -113,14 +136,9 @@ "name": "key", "type": "bytes32[]" }, - { - "internalType": "uint8", - "name": "", - "type": "uint8" - }, { "internalType": "bytes", - "name": "", + "name": "data", "type": "bytes" }, { @@ -129,7 +147,7 @@ "type": "bytes32" } ], - "name": "onBeforeSetField", + "name": "onAfterSetRecord", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -152,7 +170,40 @@ "type": "bytes32" } ], - "name": "onDeleteRecord", + "name": "onBeforeDeleteRecord", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "sourceTableId", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "key", + "type": "bytes32[]" + }, + { + "internalType": "uint8", + "name": "", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + }, + { + "internalType": "Schema", + "name": "valueSchema", + "type": "bytes32" + } + ], + "name": "onBeforeSetField", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -180,7 +231,7 @@ "type": "bytes32" } ], - "name": "onSetRecord", + "name": "onBeforeSetRecord", "outputs": [], "stateMutability": "nonpayable", "type": "function" diff --git a/packages/world/abi/KeysWithValueHook.sol/KeysWithValueHook.abi.json.d.ts b/packages/world/abi/KeysWithValueHook.sol/KeysWithValueHook.abi.json.d.ts index a54ec782a8..799f8fd0b2 100644 --- a/packages/world/abi/KeysWithValueHook.sol/KeysWithValueHook.abi.json.d.ts +++ b/packages/world/abi/KeysWithValueHook.sol/KeysWithValueHook.abi.json.d.ts @@ -68,6 +68,29 @@ declare const abi: [ name: "StoreCore_NotDynamicField"; type: "error"; }, + { + inputs: [ + { + internalType: "bytes32"; + name: "sourceTableId"; + type: "bytes32"; + }, + { + internalType: "bytes32[]"; + name: "key"; + type: "bytes32[]"; + }, + { + internalType: "Schema"; + name: "valueSchema"; + type: "bytes32"; + } + ]; + name: "onAfterDeleteRecord"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; + }, { inputs: [ { @@ -113,14 +136,9 @@ declare const abi: [ name: "key"; type: "bytes32[]"; }, - { - internalType: "uint8"; - name: ""; - type: "uint8"; - }, { internalType: "bytes"; - name: ""; + name: "data"; type: "bytes"; }, { @@ -129,7 +147,7 @@ declare const abi: [ type: "bytes32"; } ]; - name: "onBeforeSetField"; + name: "onAfterSetRecord"; outputs: []; stateMutability: "nonpayable"; type: "function"; @@ -152,7 +170,40 @@ declare const abi: [ type: "bytes32"; } ]; - name: "onDeleteRecord"; + name: "onBeforeDeleteRecord"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; + }, + { + inputs: [ + { + internalType: "bytes32"; + name: "sourceTableId"; + type: "bytes32"; + }, + { + internalType: "bytes32[]"; + name: "key"; + type: "bytes32[]"; + }, + { + internalType: "uint8"; + name: ""; + type: "uint8"; + }, + { + internalType: "bytes"; + name: ""; + type: "bytes"; + }, + { + internalType: "Schema"; + name: "valueSchema"; + type: "bytes32"; + } + ]; + name: "onBeforeSetField"; outputs: []; stateMutability: "nonpayable"; type: "function"; @@ -180,7 +231,7 @@ declare const abi: [ type: "bytes32"; } ]; - name: "onSetRecord"; + name: "onBeforeSetRecord"; outputs: []; stateMutability: "nonpayable"; type: "function"; diff --git a/packages/world/abi/StoreHook.sol/StoreHookLib.abi.json b/packages/world/abi/StoreHook.sol/StoreHookLib.abi.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/packages/world/abi/StoreHook.sol/StoreHookLib.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 8e3dd8de1f..cdb4bb8101 100644 --- a/packages/world/abi/StoreRegistrationSystem.sol/StoreRegistrationSystem.abi.json +++ b/packages/world/abi/StoreRegistrationSystem.sol/StoreRegistrationSystem.abi.json @@ -234,8 +234,13 @@ }, { "internalType": "contract IStoreHook", - "name": "hook", + "name": "hookAddress", "type": "address" + }, + { + "internalType": "uint8", + "name": "enabledHooksBitmap", + "type": "uint8" } ], "name": "registerStoreHook", 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 34f598a939..60a453b564 100644 --- a/packages/world/abi/StoreRegistrationSystem.sol/StoreRegistrationSystem.abi.json.d.ts +++ b/packages/world/abi/StoreRegistrationSystem.sol/StoreRegistrationSystem.abi.json.d.ts @@ -234,8 +234,13 @@ declare const abi: [ }, { internalType: "contract IStoreHook"; - name: "hook"; + name: "hookAddress"; type: "address"; + }, + { + internalType: "uint8"; + name: "enabledHooksBitmap"; + type: "uint8"; } ]; name: "registerStoreHook"; diff --git a/packages/world/abi/SystemHook.sol/SystemHookLib.abi.json b/packages/world/abi/SystemHook.sol/SystemHookLib.abi.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/packages/world/abi/SystemHook.sol/SystemHookLib.abi.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json b/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json index f1b9ad18e7..64d8d6b37c 100644 --- a/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json +++ b/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json @@ -303,8 +303,13 @@ }, { "internalType": "contract ISystemHook", - "name": "hook", + "name": "hookAddress", "type": "address" + }, + { + "internalType": "uint8", + "name": "enabledHooksBitmap", + "type": "uint8" } ], "name": "registerSystemHook", 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 5c6fd80885..12fe662f81 100644 --- a/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json.d.ts +++ b/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json.d.ts @@ -303,8 +303,13 @@ declare const abi: [ }, { internalType: "contract ISystemHook"; - name: "hook"; + name: "hookAddress"; type: "address"; + }, + { + internalType: "uint8"; + name: "enabledHooksBitmap"; + type: "uint8"; } ]; name: "registerSystemHook"; diff --git a/packages/world/abi/src/Hook.sol/HookInstance.abi.json b/packages/world/abi/src/Hook.sol/HookInstance.abi.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/packages/world/abi/src/Hook.sol/HookInstance.abi.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/packages/world/abi/src/Hook.sol/HookLib.abi.json b/packages/world/abi/src/Hook.sol/HookLib.abi.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/packages/world/abi/src/Hook.sol/HookLib.abi.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/packages/world/abi/src/IStore.sol/IStore.abi.json b/packages/world/abi/src/IStore.sol/IStore.abi.json index d21e6f935f..5b408efd0e 100644 --- a/packages/world/abi/src/IStore.sol/IStore.abi.json +++ b/packages/world/abi/src/IStore.sol/IStore.abi.json @@ -510,8 +510,13 @@ }, { "internalType": "contract IStoreHook", - "name": "hook", + "name": "hookAddress", "type": "address" + }, + { + "internalType": "uint8", + "name": "enabledHooksBitmap", + "type": "uint8" } ], "name": "registerStoreHook", 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 dbaf46f7db..7d43da2fd0 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 @@ -510,8 +510,13 @@ declare const abi: [ }, { internalType: "contract IStoreHook"; - name: "hook"; + name: "hookAddress"; type: "address"; + }, + { + internalType: "uint8"; + name: "enabledHooksBitmap"; + type: "uint8"; } ]; name: "registerStoreHook"; diff --git a/packages/world/abi/src/IStore.sol/IStoreHook.abi.json b/packages/world/abi/src/IStore.sol/IStoreHook.abi.json index 171784fcb2..316af6422c 100644 --- a/packages/world/abi/src/IStore.sol/IStoreHook.abi.json +++ b/packages/world/abi/src/IStore.sol/IStoreHook.abi.json @@ -1,4 +1,27 @@ [ + { + "inputs": [ + { + "internalType": "bytes32", + "name": "table", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "key", + "type": "bytes32[]" + }, + { + "internalType": "Schema", + "name": "valueSchema", + "type": "bytes32" + } + ], + "name": "onAfterDeleteRecord", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -44,11 +67,6 @@ "name": "key", "type": "bytes32[]" }, - { - "internalType": "uint8", - "name": "schemaIndex", - "type": "uint8" - }, { "internalType": "bytes", "name": "data", @@ -60,7 +78,7 @@ "type": "bytes32" } ], - "name": "onBeforeSetField", + "name": "onAfterSetRecord", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -83,7 +101,40 @@ "type": "bytes32" } ], - "name": "onDeleteRecord", + "name": "onBeforeDeleteRecord", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "table", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "key", + "type": "bytes32[]" + }, + { + "internalType": "uint8", + "name": "schemaIndex", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "Schema", + "name": "valueSchema", + "type": "bytes32" + } + ], + "name": "onBeforeSetField", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -111,7 +162,7 @@ "type": "bytes32" } ], - "name": "onSetRecord", + "name": "onBeforeSetRecord", "outputs": [], "stateMutability": "nonpayable", "type": "function" diff --git a/packages/world/abi/src/IStore.sol/IStoreHook.abi.json.d.ts b/packages/world/abi/src/IStore.sol/IStoreHook.abi.json.d.ts index 233d4d36da..2e39c851eb 100644 --- a/packages/world/abi/src/IStore.sol/IStoreHook.abi.json.d.ts +++ b/packages/world/abi/src/IStore.sol/IStoreHook.abi.json.d.ts @@ -1,4 +1,27 @@ declare const abi: [ + { + inputs: [ + { + internalType: "bytes32"; + name: "table"; + type: "bytes32"; + }, + { + internalType: "bytes32[]"; + name: "key"; + type: "bytes32[]"; + }, + { + internalType: "Schema"; + name: "valueSchema"; + type: "bytes32"; + } + ]; + name: "onAfterDeleteRecord"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; + }, { inputs: [ { @@ -44,11 +67,6 @@ declare const abi: [ name: "key"; type: "bytes32[]"; }, - { - internalType: "uint8"; - name: "schemaIndex"; - type: "uint8"; - }, { internalType: "bytes"; name: "data"; @@ -60,7 +78,7 @@ declare const abi: [ type: "bytes32"; } ]; - name: "onBeforeSetField"; + name: "onAfterSetRecord"; outputs: []; stateMutability: "nonpayable"; type: "function"; @@ -83,7 +101,40 @@ declare const abi: [ type: "bytes32"; } ]; - name: "onDeleteRecord"; + name: "onBeforeDeleteRecord"; + outputs: []; + stateMutability: "nonpayable"; + type: "function"; + }, + { + inputs: [ + { + internalType: "bytes32"; + name: "table"; + type: "bytes32"; + }, + { + internalType: "bytes32[]"; + name: "key"; + type: "bytes32[]"; + }, + { + internalType: "uint8"; + name: "schemaIndex"; + type: "uint8"; + }, + { + internalType: "bytes"; + name: "data"; + type: "bytes"; + }, + { + internalType: "Schema"; + name: "valueSchema"; + type: "bytes32"; + } + ]; + name: "onBeforeSetField"; outputs: []; stateMutability: "nonpayable"; type: "function"; @@ -111,7 +162,7 @@ declare const abi: [ type: "bytes32"; } ]; - name: "onSetRecord"; + name: "onBeforeSetRecord"; outputs: []; stateMutability: "nonpayable"; type: "function"; diff --git a/packages/world/abi/src/IStore.sol/IStoreRegistration.abi.json b/packages/world/abi/src/IStore.sol/IStoreRegistration.abi.json index 0a30f85789..feac4849a3 100644 --- a/packages/world/abi/src/IStore.sol/IStoreRegistration.abi.json +++ b/packages/world/abi/src/IStore.sol/IStoreRegistration.abi.json @@ -8,8 +8,13 @@ }, { "internalType": "contract IStoreHook", - "name": "hook", + "name": "hookAddress", "type": "address" + }, + { + "internalType": "uint8", + "name": "enabledHooksBitmap", + "type": "uint8" } ], "name": "registerStoreHook", 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 95262775f6..c03b1d5863 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 @@ -8,8 +8,13 @@ declare const abi: [ }, { internalType: "contract IStoreHook"; - name: "hook"; + name: "hookAddress"; type: "address"; + }, + { + internalType: "uint8"; + name: "enabledHooksBitmap"; + type: "uint8"; } ]; name: "registerStoreHook"; diff --git a/packages/world/abi/src/StoreHook.sol/StoreHookLib.abi.json b/packages/world/abi/src/StoreHook.sol/StoreHookLib.abi.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/packages/world/abi/src/StoreHook.sol/StoreHookLib.abi.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/packages/world/gas-report.json b/packages/world/gas-report.json index b142eb66ee..b3e0438426 100644 --- a/packages/world/gas-report.json +++ b/packages/world/gas-report.json @@ -39,67 +39,67 @@ "file": "test/KeysInTableModule.t.sol", "test": "testInstallComposite", "name": "install keys in table module", - "gasUsed": 1411774 + "gasUsed": 1412483 }, { "file": "test/KeysInTableModule.t.sol", "test": "testInstallGas", "name": "install keys in table module", - "gasUsed": 1411774 + "gasUsed": 1412483 }, { "file": "test/KeysInTableModule.t.sol", "test": "testInstallGas", "name": "set a record on a table with keysInTableModule installed", - "gasUsed": 182055 + "gasUsed": 182589 }, { "file": "test/KeysInTableModule.t.sol", "test": "testInstallSingleton", "name": "install keys in table module", - "gasUsed": 1411774 + "gasUsed": 1412483 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookCompositeGas", "name": "install keys in table module", - "gasUsed": 1411774 + "gasUsed": 1412483 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookCompositeGas", "name": "change a composite record on a table with keysInTableModule installed", - "gasUsed": 25667 + "gasUsed": 26174 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookCompositeGas", "name": "delete a composite record on a table with keysInTableModule installed", - "gasUsed": 250544 + "gasUsed": 251091 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookGas", "name": "install keys in table module", - "gasUsed": 1411774 + "gasUsed": 1412483 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookGas", "name": "change a record on a table with keysInTableModule installed", - "gasUsed": 24387 + "gasUsed": 24894 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookGas", "name": "delete a record on a table with keysInTableModule installed", - "gasUsed": 128833 + "gasUsed": 129380 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testGetKeysWithValueGas", "name": "install keys with value module", - "gasUsed": 650602 + "gasUsed": 650950 }, { "file": "test/KeysWithValueModule.t.sol", @@ -117,49 +117,49 @@ "file": "test/KeysWithValueModule.t.sol", "test": "testInstall", "name": "install keys with value module", - "gasUsed": 650602 + "gasUsed": 650950 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testInstall", "name": "set a record on a table with KeysWithValueModule installed", - "gasUsed": 151511 + "gasUsed": 151906 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetAndDeleteRecordHook", "name": "install keys with value module", - "gasUsed": 650602 + "gasUsed": 650950 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetAndDeleteRecordHook", "name": "change a record on a table with KeysWithValueModule installed", - "gasUsed": 117983 + "gasUsed": 118369 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetAndDeleteRecordHook", "name": "delete a record on a table with KeysWithValueModule installed", - "gasUsed": 43561 + "gasUsed": 43982 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetField", "name": "install keys with value module", - "gasUsed": 650602 + "gasUsed": 650950 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetField", "name": "set a field on a table with KeysWithValueModule installed", - "gasUsed": 158433 + "gasUsed": 158593 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetField", "name": "change a field on a table with KeysWithValueModule installed", - "gasUsed": 120691 + "gasUsed": 120851 }, { "file": "test/query.t.sol", @@ -237,7 +237,7 @@ "file": "test/StandardDelegationsModule.t.sol", "test": "testCallFromCallboundDelegation", "name": "call a system via a callbound delegation", - "gasUsed": 44047 + "gasUsed": 44074 }, { "file": "test/StandardDelegationsModule.t.sol", @@ -249,13 +249,13 @@ "file": "test/StandardDelegationsModule.t.sol", "test": "testCallFromTimeboundDelegation", "name": "call a system via a timebound delegation", - "gasUsed": 34791 + "gasUsed": 34779 }, { "file": "test/UniqueEntityModule.t.sol", "test": "testInstall", "name": "install unique entity module", - "gasUsed": 726281 + "gasUsed": 726423 }, { "file": "test/UniqueEntityModule.t.sol", @@ -267,7 +267,7 @@ "file": "test/UniqueEntityModule.t.sol", "test": "testInstallRoot", "name": "installRoot unique entity module", - "gasUsed": 705270 + "gasUsed": 705367 }, { "file": "test/UniqueEntityModule.t.sol", @@ -279,7 +279,7 @@ "file": "test/World.t.sol", "test": "testCall", "name": "call a system via the World", - "gasUsed": 17531 + "gasUsed": 17519 }, { "file": "test/World.t.sol", @@ -291,13 +291,13 @@ "file": "test/World.t.sol", "test": "testCallFromUnlimitedDelegation", "name": "call a system via an unlimited delegation", - "gasUsed": 17865 + "gasUsed": 17853 }, { "file": "test/World.t.sol", "test": "testDeleteRecord", "name": "Delete record", - "gasUsed": 12190 + "gasUsed": 12229 }, { "file": "test/World.t.sol", @@ -309,37 +309,37 @@ "file": "test/World.t.sol", "test": "testRegisterFallbackSystem", "name": "Register a fallback system", - "gasUsed": 70405 + "gasUsed": 70432 }, { "file": "test/World.t.sol", "test": "testRegisterFallbackSystem", "name": "Register a root fallback system", - "gasUsed": 63711 + "gasUsed": 63716 }, { "file": "test/World.t.sol", "test": "testRegisterFunctionSelector", "name": "Register a function selector", - "gasUsed": 90999 + "gasUsed": 91026 }, { "file": "test/World.t.sol", "test": "testRegisterNamespace", "name": "Register a new namespace", - "gasUsed": 140095 + "gasUsed": 140073 }, { "file": "test/World.t.sol", "test": "testRegisterRootFunctionSelector", "name": "Register a root function selector", - "gasUsed": 79622 + "gasUsed": 79627 }, { "file": "test/World.t.sol", "test": "testRegisterTable", "name": "Register a new table in the namespace", - "gasUsed": 650153 + "gasUsed": 650137 }, { "file": "test/World.t.sol", diff --git a/packages/world/mud.config.ts b/packages/world/mud.config.ts index 407dfe56e5..56dc6e701e 100644 --- a/packages/world/mud.config.ts +++ b/packages/world/mud.config.ts @@ -81,7 +81,7 @@ export default mudConfig({ keySchema: { resourceSelector: "bytes32", }, - schema: "address[]", + schema: "bytes21[]", }, ResourceType: { directory: "modules/core/tables", diff --git a/packages/world/src/SystemCall.sol b/packages/world/src/SystemCall.sol index a573c6947c..4750b20f59 100644 --- a/packages/world/src/SystemCall.sol +++ b/packages/world/src/SystemCall.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; +import { Hook } from "@latticexyz/store/src/Hook.sol"; + import { ResourceSelector } from "./ResourceSelector.sol"; import { WorldContextProvider } from "./WorldContext.sol"; import { AccessControl } from "./AccessControl.sol"; @@ -8,6 +10,7 @@ import { ResourceSelector } from "./ResourceSelector.sol"; import { ROOT_NAMESPACE } from "./constants.sol"; import { WorldContextProvider } from "./WorldContext.sol"; import { revertWithBytes } from "./revertWithBytes.sol"; +import { SystemHookType } from "./SystemHook.sol"; import { IWorldErrors } from "./interfaces/IWorldErrors.sol"; import { ISystemHook } from "./interfaces/ISystemHook.sol"; @@ -64,12 +67,14 @@ library SystemCall { uint256 value ) internal returns (bool success, bytes memory data) { // Get system hooks - address[] memory hooks = SystemHooks.get(resourceSelector); + bytes21[] memory hooks = SystemHooks.get(resourceSelector); // Call onBeforeCallSystem hooks (before calling the system) for (uint256 i; i < hooks.length; i++) { - ISystemHook hook = ISystemHook(hooks[i]); - hook.onBeforeCallSystem(caller, resourceSelector, funcSelectorAndArgs); + Hook hook = Hook.wrap(hooks[i]); + if (hook.isEnabled(uint8(SystemHookType.BEFORE_CALL_SYSTEM))) { + ISystemHook(hook.getAddress()).onBeforeCallSystem(caller, resourceSelector, funcSelectorAndArgs); + } } // Call the system and forward any return data @@ -77,8 +82,10 @@ library SystemCall { // Call onAfterCallSystem hooks (after calling the system) for (uint256 i; i < hooks.length; i++) { - ISystemHook hook = ISystemHook(hooks[i]); - hook.onAfterCallSystem(caller, resourceSelector, funcSelectorAndArgs); + Hook hook = Hook.wrap(hooks[i]); + if (hook.isEnabled(uint8(SystemHookType.AFTER_CALL_SYSTEM))) { + ISystemHook(hook.getAddress()).onAfterCallSystem(caller, resourceSelector, funcSelectorAndArgs); + } } } diff --git a/packages/world/src/SystemHook.sol b/packages/world/src/SystemHook.sol new file mode 100644 index 0000000000..60b9408392 --- /dev/null +++ b/packages/world/src/SystemHook.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import { Hook, HookLib } from "@latticexyz/store/src/Hook.sol"; +import { ISystemHook } from "./interfaces/ISystemHook.sol"; + +enum SystemHookType { + BEFORE_CALL_SYSTEM, + AFTER_CALL_SYSTEM +} + +library SystemHookLib { + /** + * Encode the bitmap into a single byte + */ + function encodeBitmap(bool onBeforeCallSystem, bool onAfterCallSystem) internal pure returns (uint8) { + uint256 bitmap = 0; + if (onBeforeCallSystem) bitmap |= 1 << uint8(SystemHookType.BEFORE_CALL_SYSTEM); + if (onAfterCallSystem) bitmap |= 1 << uint8(SystemHookType.AFTER_CALL_SYSTEM); + return uint8(bitmap); + } + + /** + * Encode enabled hooks into a bitmap with 1 bit per hook, and pack the bitmap with the system hook address into a bytes21 value + */ + function encode(ISystemHook systemHook, uint8 enabledHooksBitmap) internal pure returns (Hook) { + return HookLib.encode(address(systemHook), enabledHooksBitmap); + } +} diff --git a/packages/world/src/interfaces/IWorldRegistrationSystem.sol b/packages/world/src/interfaces/IWorldRegistrationSystem.sol index b684602e53..184886400e 100644 --- a/packages/world/src/interfaces/IWorldRegistrationSystem.sol +++ b/packages/world/src/interfaces/IWorldRegistrationSystem.sol @@ -9,7 +9,7 @@ import { WorldContextConsumer } from "./../WorldContext.sol"; interface IWorldRegistrationSystem { function registerNamespace(bytes16 namespace) external; - function registerSystemHook(bytes32 resourceSelector, ISystemHook hook) external; + function registerSystemHook(bytes32 resourceSelector, ISystemHook hookAddress, uint8 enabledHooksBitmap) external; function registerSystem(bytes32 resourceSelector, WorldContextConsumer system, bool publicAccess) external; diff --git a/packages/world/src/modules/core/implementations/StoreRegistrationSystem.sol b/packages/world/src/modules/core/implementations/StoreRegistrationSystem.sol index c832ace089..8457ec45c7 100644 --- a/packages/world/src/modules/core/implementations/StoreRegistrationSystem.sol +++ b/packages/world/src/modules/core/implementations/StoreRegistrationSystem.sol @@ -77,11 +77,11 @@ contract StoreRegistrationSystem is System, IWorldErrors { * Register a hook for the table at the given namepace and name. * Requires the caller to own the namespace. */ - function registerStoreHook(bytes32 tableId, IStoreHook hook) public virtual { + function registerStoreHook(bytes32 tableId, IStoreHook hookAddress, uint8 enabledHooksBitmap) public virtual { // Require caller to own the namespace AccessControl.requireOwnerOrSelf(tableId, _msgSender()); // Register the hook - StoreCore.registerStoreHook(tableId, hook); + StoreCore.registerStoreHook(tableId, hookAddress, enabledHooksBitmap); } } diff --git a/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol b/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol index 9f38aa0c33..e22b7ebf06 100644 --- a/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol +++ b/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol @@ -1,11 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; +import { Hook } from "@latticexyz/store/src/Hook.sol"; + import { System } from "../../../System.sol"; import { WorldContextConsumer } from "../../../WorldContext.sol"; import { ResourceSelector } from "../../../ResourceSelector.sol"; import { Resource } from "../../../Types.sol"; import { SystemCall } from "../../../SystemCall.sol"; +import { SystemHookLib } from "../../../SystemHook.sol"; import { ROOT_NAMESPACE, ROOT_NAME, UNLIMITED_DELEGATION } from "../../../constants.sol"; import { AccessControl } from "../../../AccessControl.sol"; import { NamespaceOwner } from "../../../tables/NamespaceOwner.sol"; @@ -49,12 +52,16 @@ contract WorldRegistrationSystem is System, IWorldErrors { /** * Register a hook for the system at the given namespace and name */ - function registerSystemHook(bytes32 resourceSelector, ISystemHook hook) public virtual { + function registerSystemHook( + bytes32 resourceSelector, + ISystemHook hookAddress, + uint8 enabledHooksBitmap + ) public virtual { // Require caller to own the namespace AccessControl.requireOwnerOrSelf(resourceSelector, _msgSender()); // Register the hook - SystemHooks.push(resourceSelector, address(hook)); + SystemHooks.push(resourceSelector, Hook.unwrap(SystemHookLib.encode(hookAddress, enabledHooksBitmap))); } /** diff --git a/packages/world/src/modules/core/tables/SystemHooks.sol b/packages/world/src/modules/core/tables/SystemHooks.sol index 0489ccbb7a..c0699e5a4e 100644 --- a/packages/world/src/modules/core/tables/SystemHooks.sol +++ b/packages/world/src/modules/core/tables/SystemHooks.sol @@ -32,7 +32,7 @@ library SystemHooks { /** Get the table's value schema */ function getValueSchema() internal pure returns (Schema) { SchemaType[] memory _schema = new SchemaType[](1); - _schema[0] = SchemaType.ADDRESS_ARRAY; + _schema[0] = SchemaType.BYTES21_ARRAY; return SchemaLib.encode(_schema); } @@ -60,25 +60,25 @@ library SystemHooks { } /** Get value */ - function get(bytes32 resourceSelector) internal view returns (address[] memory value) { + function get(bytes32 resourceSelector) internal view returns (bytes21[] memory value) { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = resourceSelector; bytes memory _blob = StoreSwitch.getField(_tableId, _keyTuple, 0, getValueSchema()); - return (SliceLib.getSubslice(_blob, 0, _blob.length).decodeArray_address()); + return (SliceLib.getSubslice(_blob, 0, _blob.length).decodeArray_bytes21()); } /** Get value (using the specified store) */ - function get(IStore _store, bytes32 resourceSelector) internal view returns (address[] memory value) { + function get(IStore _store, bytes32 resourceSelector) internal view returns (bytes21[] memory value) { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = resourceSelector; bytes memory _blob = _store.getField(_tableId, _keyTuple, 0, getValueSchema()); - return (SliceLib.getSubslice(_blob, 0, _blob.length).decodeArray_address()); + return (SliceLib.getSubslice(_blob, 0, _blob.length).decodeArray_bytes21()); } /** Set value */ - function set(bytes32 resourceSelector, address[] memory value) internal { + function set(bytes32 resourceSelector, bytes21[] memory value) internal { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = resourceSelector; @@ -86,7 +86,7 @@ library SystemHooks { } /** Set value (using the specified store) */ - function set(IStore _store, bytes32 resourceSelector, address[] memory value) internal { + function set(IStore _store, bytes32 resourceSelector, bytes21[] memory value) internal { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = resourceSelector; @@ -100,7 +100,7 @@ library SystemHooks { uint256 _byteLength = StoreSwitch.getFieldLength(_tableId, _keyTuple, 0, getValueSchema()); unchecked { - return _byteLength / 20; + return _byteLength / 21; } } @@ -111,7 +111,7 @@ library SystemHooks { uint256 _byteLength = _store.getFieldLength(_tableId, _keyTuple, 0, getValueSchema()); unchecked { - return _byteLength / 20; + return _byteLength / 21; } } @@ -119,7 +119,7 @@ library SystemHooks { * Get an item of value * (unchecked, returns invalid data if index overflows) */ - function getItem(bytes32 resourceSelector, uint256 _index) internal view returns (address) { + function getItem(bytes32 resourceSelector, uint256 _index) internal view returns (bytes21) { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = resourceSelector; @@ -129,10 +129,10 @@ library SystemHooks { _keyTuple, 0, getValueSchema(), - _index * 20, - (_index + 1) * 20 + _index * 21, + (_index + 1) * 21 ); - return (address(Bytes.slice20(_blob, 0))); + return (Bytes.slice21(_blob, 0)); } } @@ -140,7 +140,7 @@ library SystemHooks { * Get an item of value (using the specified store) * (unchecked, returns invalid data if index overflows) */ - function getItem(IStore _store, bytes32 resourceSelector, uint256 _index) internal view returns (address) { + function getItem(IStore _store, bytes32 resourceSelector, uint256 _index) internal view returns (bytes21) { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = resourceSelector; @@ -150,15 +150,15 @@ library SystemHooks { _keyTuple, 0, getValueSchema(), - _index * 20, - (_index + 1) * 20 + _index * 21, + (_index + 1) * 21 ); - return (address(Bytes.slice20(_blob, 0))); + return (Bytes.slice21(_blob, 0)); } } /** Push an element to value */ - function push(bytes32 resourceSelector, address _element) internal { + function push(bytes32 resourceSelector, bytes21 _element) internal { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = resourceSelector; @@ -166,7 +166,7 @@ library SystemHooks { } /** Push an element to value (using the specified store) */ - function push(IStore _store, bytes32 resourceSelector, address _element) internal { + function push(IStore _store, bytes32 resourceSelector, bytes21 _element) internal { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = resourceSelector; @@ -178,7 +178,7 @@ library SystemHooks { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = resourceSelector; - StoreSwitch.popFromField(_tableId, _keyTuple, 0, 20, getValueSchema()); + StoreSwitch.popFromField(_tableId, _keyTuple, 0, 21, getValueSchema()); } /** Pop an element from value (using the specified store) */ @@ -186,19 +186,19 @@ library SystemHooks { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = resourceSelector; - _store.popFromField(_tableId, _keyTuple, 0, 20, getValueSchema()); + _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 resourceSelector, uint256 _index, address _element) internal { + function update(bytes32 resourceSelector, uint256 _index, bytes21 _element) internal { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = resourceSelector; unchecked { - StoreSwitch.updateInField(_tableId, _keyTuple, 0, _index * 20, abi.encodePacked((_element)), getValueSchema()); + StoreSwitch.updateInField(_tableId, _keyTuple, 0, _index * 21, abi.encodePacked((_element)), getValueSchema()); } } @@ -206,21 +206,21 @@ library SystemHooks { * 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 resourceSelector, uint256 _index, address _element) internal { + function update(IStore _store, bytes32 resourceSelector, uint256 _index, bytes21 _element) internal { bytes32[] memory _keyTuple = new bytes32[](1); _keyTuple[0] = resourceSelector; unchecked { - _store.updateInField(_tableId, _keyTuple, 0, _index * 20, abi.encodePacked((_element)), getValueSchema()); + _store.updateInField(_tableId, _keyTuple, 0, _index * 21, abi.encodePacked((_element)), getValueSchema()); } } /** Tightly pack full data using this table's schema */ - function encode(address[] memory value) internal pure returns (bytes memory) { + 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 * 20); + _encodedLengths = PackedCounterLib.pack(value.length * 21); } return abi.encodePacked(_encodedLengths.unwrap(), EncodeArray.encode((value))); diff --git a/packages/world/src/modules/keysintable/KeysInTableHook.sol b/packages/world/src/modules/keysintable/KeysInTableHook.sol index f2c4522de5..7d55c25b75 100644 --- a/packages/world/src/modules/keysintable/KeysInTableHook.sol +++ b/packages/world/src/modules/keysintable/KeysInTableHook.sol @@ -40,17 +40,23 @@ contract KeysInTableHook is IStoreHook { } } - function onSetRecord(bytes32 table, bytes32[] memory key, bytes memory, Schema) public { + function onBeforeSetRecord(bytes32 table, bytes32[] memory key, bytes memory, Schema) public { handleSet(table, key); } - function onBeforeSetField(bytes32 table, bytes32[] memory key, uint8, bytes memory, Schema) public {} + function onAfterSetRecord(bytes32 table, bytes32[] memory key, bytes memory, Schema) public { + // NOOP + } + + function onBeforeSetField(bytes32 table, bytes32[] memory key, uint8, bytes memory, Schema) public { + // NOOP + } function onAfterSetField(bytes32 table, bytes32[] memory key, uint8, bytes memory, Schema) public { handleSet(table, key); } - function onDeleteRecord(bytes32 tableId, bytes32[] memory key, Schema) public { + function onBeforeDeleteRecord(bytes32 tableId, bytes32[] memory key, Schema) public { bytes32 keysHash = keccak256(abi.encode(key)); (bool has, uint40 index) = UsedKeysIndex.get(tableId, keysHash); @@ -118,4 +124,8 @@ contract KeysInTableHook is IStoreHook { } } } + + function onAfterDeleteRecord(bytes32 table, bytes32[] memory key, Schema valueSchema) public { + // NOOP + } } diff --git a/packages/world/src/modules/keysintable/KeysInTableModule.sol b/packages/world/src/modules/keysintable/KeysInTableModule.sol index 8c8506eabf..253d9d0f9f 100644 --- a/packages/world/src/modules/keysintable/KeysInTableModule.sol +++ b/packages/world/src/modules/keysintable/KeysInTableModule.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; +import { StoreHookLib } from "@latticexyz/store/src/StoreHook.sol"; + import { ResourceType } from "../core/tables/ResourceType.sol"; import { Resource } from "../../Types.sol"; @@ -52,6 +54,17 @@ contract KeysInTableModule is IModule, WorldContextConsumer { } // Register a hook that is called when a value is set in the source table - world.registerStoreHook(sourceTableId, hook); + world.registerStoreHook( + sourceTableId, + hook, + StoreHookLib.encodeBitmap({ + onBeforeSetRecord: true, + onAfterSetRecord: false, + onBeforeSetField: false, + onAfterSetField: true, + onBeforeDeleteRecord: true, + onAfterDeleteRecord: false + }) + ); } } diff --git a/packages/world/src/modules/keyswithvalue/KeysWithValueHook.sol b/packages/world/src/modules/keyswithvalue/KeysWithValueHook.sol index d16f90b2a2..da2f6e8940 100644 --- a/packages/world/src/modules/keyswithvalue/KeysWithValueHook.sol +++ b/packages/world/src/modules/keyswithvalue/KeysWithValueHook.sol @@ -29,7 +29,12 @@ contract KeysWithValueHook is IStoreHook { return IBaseWorld(StoreSwitch.getStoreAddress()); } - function onSetRecord(bytes32 sourceTableId, bytes32[] memory key, bytes memory data, Schema valueSchema) public { + function onBeforeSetRecord( + bytes32 sourceTableId, + bytes32[] memory key, + bytes memory data, + Schema valueSchema + ) public { bytes32 targetTableId = getTargetTableSelector(MODULE_NAMESPACE, sourceTableId); // Get the previous value @@ -42,6 +47,10 @@ contract KeysWithValueHook is IStoreHook { KeysWithValue.push(targetTableId, keccak256(data), key[0]); } + function onAfterSetRecord(bytes32 sourceTableId, bytes32[] memory key, bytes memory data, Schema valueSchema) public { + // NOOP + } + function onBeforeSetField( bytes32 sourceTableId, bytes32[] memory key, @@ -68,13 +77,17 @@ contract KeysWithValueHook is IStoreHook { KeysWithValue.push(targetTableId, newValue, key[0]); } - function onDeleteRecord(bytes32 sourceTableId, bytes32[] memory key, Schema valueSchema) public { + function onBeforeDeleteRecord(bytes32 sourceTableId, bytes32[] memory key, Schema valueSchema) public { // Remove the key from the list of keys with the previous value bytes32 previousValue = keccak256(_world().getRecord(sourceTableId, key, valueSchema)); bytes32 targetTableId = getTargetTableSelector(MODULE_NAMESPACE, sourceTableId); _removeKeyFromList(targetTableId, key[0], previousValue); } + function onAfterDeleteRecord(bytes32 sourceTableId, bytes32[] memory key, Schema valueSchema) public { + // NOOP + } + function _removeKeyFromList(bytes32 targetTableId, bytes32 key, bytes32 valueHash) internal { // Get the keys with the previous value excluding the current key bytes32[] memory keysWithPreviousValue = KeysWithValue.get(targetTableId, valueHash).filter(key); diff --git a/packages/world/src/modules/keyswithvalue/KeysWithValueModule.sol b/packages/world/src/modules/keyswithvalue/KeysWithValueModule.sol index 9bfb076d2a..1cd17800d2 100644 --- a/packages/world/src/modules/keyswithvalue/KeysWithValueModule.sol +++ b/packages/world/src/modules/keyswithvalue/KeysWithValueModule.sol @@ -2,6 +2,7 @@ pragma solidity >=0.8.0; import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; +import { StoreHookLib } from "@latticexyz/store/src/StoreHook.sol"; import { IBaseWorld } from "../../interfaces/IBaseWorld.sol"; import { IModule } from "../../interfaces/IModule.sol"; @@ -48,6 +49,17 @@ contract KeysWithValueModule is IModule, WorldContextConsumer { IBaseWorld(_world()).grantAccess(targetTableSelector, address(hook)); // Register a hook that is called when a value is set in the source table - StoreSwitch.registerStoreHook(sourceTableId, hook); + StoreSwitch.registerStoreHook( + sourceTableId, + hook, + StoreHookLib.encodeBitmap({ + onBeforeSetRecord: true, + onAfterSetRecord: false, + onBeforeSetField: true, + onAfterSetField: true, + onBeforeDeleteRecord: true, + onAfterDeleteRecord: false + }) + ); } } diff --git a/packages/world/test/SystemHook.t.sol b/packages/world/test/SystemHook.t.sol new file mode 100644 index 0000000000..b1d423be0f --- /dev/null +++ b/packages/world/test/SystemHook.t.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import "forge-std/Test.sol"; +import { GasReporter } from "@latticexyz/gas-report/src/GasReporter.sol"; + +import { Hook } from "@latticexyz/store/src/Hook.sol"; +import { SystemHookType } from "../src/SystemHook.sol"; +import { SystemHookLib } from "../src/SystemHook.sol"; +import { ISystemHook } from "../src/interfaces/ISystemHook.sol"; + +contract SystemHookTest is Test, GasReporter { + function testEncodeBitmap() public { + assertEq( + SystemHookLib.encodeBitmap({ onBeforeCallSystem: false, onAfterCallSystem: false }), + uint8(0x00), + "0b00000000" + ); + + assertEq( + SystemHookLib.encodeBitmap({ onBeforeCallSystem: true, onAfterCallSystem: false }), + uint8(0x01), + "0b00000001" + ); + + assertEq( + SystemHookLib.encodeBitmap({ onBeforeCallSystem: false, onAfterCallSystem: true }), + uint8(0x02), + "0b00000010" + ); + + assertEq( + SystemHookLib.encodeBitmap({ onBeforeCallSystem: true, onAfterCallSystem: true }), + uint8(0x03), + "0b00000011" + ); + } + + function testFuzzEncode(address hookAddress, bool enableBeforeCallSystem, bool enableAfterCallSystem) public { + uint8 enabledHooksBitmap = SystemHookLib.encodeBitmap({ + onBeforeCallSystem: enableBeforeCallSystem, + onAfterCallSystem: enableAfterCallSystem + }); + + assertEq( + Hook.unwrap(SystemHookLib.encode(ISystemHook(hookAddress), enabledHooksBitmap)), + bytes21(abi.encodePacked(hookAddress, enabledHooksBitmap)) + ); + } + + function testFuzzIsEnabled(address hookAddress, bool enableBeforeCallSystem, bool enableAfterCallSystem) public { + uint8 enabledHooksBitmap = SystemHookLib.encodeBitmap({ + onBeforeCallSystem: enableBeforeCallSystem, + onAfterCallSystem: enableAfterCallSystem + }); + + Hook systemHook = SystemHookLib.encode(ISystemHook(hookAddress), enabledHooksBitmap); + + assertEq(systemHook.isEnabled(uint8(SystemHookType.BEFORE_CALL_SYSTEM)), enableBeforeCallSystem); + assertEq(systemHook.isEnabled(uint8(SystemHookType.AFTER_CALL_SYSTEM)), enableAfterCallSystem); + } + + function testFuzzGetAddressAndBitmap( + address hookAddress, + bool enableBeforeCallSystem, + bool enableAfterCallSystem + ) public { + uint8 enabledHooksBitmap = SystemHookLib.encodeBitmap({ + onBeforeCallSystem: enableBeforeCallSystem, + onAfterCallSystem: enableAfterCallSystem + }); + + Hook systemHook = SystemHookLib.encode(ISystemHook(hookAddress), enabledHooksBitmap); + + assertEq(systemHook.getAddress(), hookAddress); + assertEq(systemHook.getBitmap(), enabledHooksBitmap); + } +} diff --git a/packages/world/test/World.t.sol b/packages/world/test/World.t.sol index f9f47e2756..7efd9a9eb8 100644 --- a/packages/world/test/World.t.sol +++ b/packages/world/test/World.t.sol @@ -14,12 +14,14 @@ import { SchemaEncodeHelper } from "@latticexyz/store/test/SchemaEncodeHelper.so 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 { World } from "../src/World.sol"; import { System } from "../src/System.sol"; import { ResourceSelector } from "../src/ResourceSelector.sol"; import { ROOT_NAMESPACE, ROOT_NAME, UNLIMITED_DELEGATION } from "../src/constants.sol"; import { Resource } from "../src/Types.sol"; +import { SystemHookLib } from "../src/SystemHook.sol"; import { NamespaceOwner, NamespaceOwnerTableId } from "../src/tables/NamespaceOwner.sol"; import { ResourceAccess } from "../src/tables/ResourceAccess.sol"; @@ -112,7 +114,11 @@ contract PayableFallbackSystem is System { contract WorldTestTableHook is IStoreHook { event HookCalled(bytes data); - function onSetRecord(bytes32 table, bytes32[] memory key, bytes memory data, Schema valueSchema) public { + 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)); } @@ -136,7 +142,11 @@ contract WorldTestTableHook is IStoreHook { emit HookCalled(abi.encode(table, key, schemaIndex, data, valueSchema)); } - function onDeleteRecord(bytes32 table, bytes32[] memory key, Schema valueSchema) public { + function onBeforeDeleteRecord(bytes32 table, bytes32[] memory key, Schema valueSchema) public { + emit HookCalled(abi.encode(table, key, valueSchema)); + } + + function onAfterDeleteRecord(bytes32 table, bytes32[] memory key, Schema valueSchema) public { emit HookCalled(abi.encode(table, key, valueSchema)); } } @@ -714,7 +724,7 @@ contract WorldTest is Test, GasReporter { world.registerDelegation(delegatee, UNLIMITED_DELEGATION, new bytes(0)); } - function testRegisterTableHook() public { + function testRegisterStoreHook() public { Schema valueSchema = Bool.getValueSchema(); bytes32 tableId = ResourceSelector.from("", "testTable"); @@ -723,18 +733,48 @@ contract WorldTest is Test, GasReporter { // Register a new hook IStoreHook tableHook = new WorldTestTableHook(); - world.registerStoreHook(tableId, tableHook); + world.registerStoreHook( + tableId, + tableHook, + 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 the hook to be notified when a record is written + // 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); - // TODO: add tests for other hook methods (onBeforeSetField, onAfterSetField, onDeleteRecord) - // (See https://github.com/latticexyz/mud/issues/444) + // 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 { @@ -746,7 +786,11 @@ contract WorldTest is Test, GasReporter { // Register a new hook ISystemHook systemHook = new WorldTestSystemHook(); - world.registerSystemHook(systemId, systemHook); + world.registerSystemHook( + systemId, + systemHook, + SystemHookLib.encodeBitmap({ onBeforeCallSystem: true, onAfterCallSystem: true }) + ); bytes memory funcSelectorAndArgs = abi.encodeWithSelector(bytes4(keccak256("fallbackselector")));