Store hooks
This page is about hooks before and after information is modified. You can also use System
hooks that execute before or after a System
call.
Store hooks allow you to react to state changes onchain.
There are many ways to interact with tables, but they all boil down to four types of state changes:
- A full record (static and dynamic length data) is set (
SetRecord
) - A part of the static-length data of a record is changed (
SpliceStaticData
) - A part of the dynamic-length data of a record is changed (
SpliceDynamicData
) - A full record is deleted (
DeleteRecord
)
By registering a store hook contract on a table, arbitrary logic can be triggered either before or after each of these state changes.
Store hook contract
A store hook contract is a contract that implements the IStoreHook
interface.
After the store hook contract is registered, Store
calls the enabled hook methods on each state change.
For example, here is a hook that freezes the table information to prevent modifications.
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;
import { Script } from "forge-std/Script.sol";
import { console } from "forge-std/console.sol";
import { StoreHook } from "@latticexyz/store/src/StoreHook.sol";
import { BEFORE_SET_RECORD, BEFORE_SPLICE_STATIC_DATA, BEFORE_SPLICE_DYNAMIC_DATA, BEFORE_DELETE_RECORD } from "@latticexyz/store/src/storeHookTypes.sol";
import { IWorld } from "../src/codegen/world/IWorld.sol";
import { ResourceId } from "@latticexyz/store/src/ResourceId.sol";
import { EncodedLengths } from "@latticexyz/store/src/EncodedLengths.sol";
import { FieldLayout } from "@latticexyz/store/src/FieldLayout.sol";
import { Counter } from "../src/codegen/tables/Counter.sol";
contract Freeze is StoreHook {
function frozen() internal pure {
revert("Value frozen for now");
}
function onBeforeSetRecord(
ResourceId tableId,
bytes32[] memory keyTuple,
bytes memory staticData,
EncodedLengths encodedLengths,
bytes memory dynamicData,
FieldLayout
) public override {
frozen();
}
function onBeforeSpliceStaticData(
ResourceId tableId,
bytes32[] memory keyTuple,
uint48 start,
bytes memory data
) public override {
frozen();
}
function onBeforeSpliceDynamicData(
ResourceId tableId,
bytes32[] memory keyTuple,
uint8 dynamicFieldIndex,
uint40 startWithinField,
uint40 deleteCount,
EncodedLengths encodedLengths,
bytes memory data
) public override {
frozen();
}
function onBeforeDeleteRecord(
ResourceId tableId,
bytes32[] memory keyTuple,
FieldLayout fieldLayout
) public override {
frozen();
}
}
contract FreezeCounter is Script {
function run() external {
address worldAddress = 0xC14fBdb7808D9e2a37c1a45b635C8C3fF64a1cc1;
// Load the private key from the `PRIVATE_KEY` environment variable (in .env)
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
// Start broadcasting transactions from the deployer account
vm.startBroadcast(deployerPrivateKey);
// Deploy Freeze
Freeze freeze = new Freeze();
// Hang Freeze on the hook
IWorld(worldAddress).registerStoreHook(
Counter._tableId,
freeze,
BEFORE_SET_RECORD | BEFORE_SPLICE_STATIC_DATA | BEFORE_SPLICE_DYNAMIC_DATA | BEFORE_DELETE_RECORD
);
vm.stopBroadcast();
}
}
Explanation
import { StoreHook } from "@latticexyz/store/src/StoreHook.sol";
The StoreHook
(opens in a new tab) contract includes all the functions a store hook contract needs to implement.
We inherit from it and override as needed.
import { ResourceId } from "@latticexyz/store/src/ResourceId.sol";
import { EncodedLengths } from "@latticexyz/store/src/EncodedLengths.sol";
import { FieldLayout } from "@latticexyz/store/src/FieldLayout.sol";
These data types are provided to the store hook contract, so we need to be able to process them.
contract Freeze is StoreHook {
function frozen() internal pure {
revert("Value frozen for now");
}
This function implements the freezing.
function onBeforeSetRecord(
ResourceId tableId,
bytes32[] memory keyTuple,
bytes memory staticData,
EncodedLengths encodedLengths,
bytes memory dynamicData,
FieldLayout
) public override {
frozen();
}
Override the implementation in StoreHook
.
We get all the data we need to know about the record in the parameters.
function onBeforeSpliceStaticData(
...
) public override {
frozen();
}
function onBeforeSpliceDynamicData(
...
) public override {
frozen();
}
function onBeforeDeleteRecord(
...
) public override {
frozen();
}
}
Hook functions have different parameters, as required to understand the data.
In addition to these functions we can also override onAfter...
if we need it (for example, to have access to the updated data instead of calculating it ourselves).
Hook registration
Hooks are registered via the StoreCore
library or IStore
interface.
It is possible to select the type of state change that should trigger a hook and the time (before or after) with a bitmap passed during the registration.
For example, here we register the Freeze
contract above to freeze the value of Counter
.
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;
import { Script } from "forge-std/Script.sol";
import { console } from "forge-std/console.sol";
import { StoreHook } from "@latticexyz/store/src/StoreHook.sol";
import { BEFORE_SET_RECORD, BEFORE_SPLICE_STATIC_DATA, BEFORE_SPLICE_DYNAMIC_DATA, BEFORE_DELETE_RECORD } from "@latticexyz/store/src/storeHookTypes.sol";
import { IWorld } from "../src/codegen/world/IWorld.sol";
import { ResourceId } from "@latticexyz/store/src/ResourceId.sol";
import { EncodedLengths } from "@latticexyz/store/src/EncodedLengths.sol";
import { FieldLayout } from "@latticexyz/store/src/FieldLayout.sol";
import { Counter } from "../src/codegen/tables/Counter.sol";
contract Freeze is StoreHook {
function frozen() internal pure {
revert("Value frozen for now");
}
function onBeforeSetRecord(
ResourceId tableId,
bytes32[] memory keyTuple,
bytes memory staticData,
EncodedLengths encodedLengths,
bytes memory dynamicData,
FieldLayout
) public override {
frozen();
}
function onBeforeSpliceStaticData(
ResourceId tableId,
bytes32[] memory keyTuple,
uint48 start,
bytes memory data
) public override {
frozen();
}
function onBeforeSpliceDynamicData(
ResourceId tableId,
bytes32[] memory keyTuple,
uint8 dynamicFieldIndex,
uint40 startWithinField,
uint40 deleteCount,
EncodedLengths encodedLengths,
bytes memory data
) public override {
frozen();
}
function onBeforeDeleteRecord(
ResourceId tableId,
bytes32[] memory keyTuple,
FieldLayout fieldLayout
) public override {
frozen();
}
}
contract FreezeCounter is Script {
function run() external {
address worldAddress = 0xC14fBdb7808D9e2a37c1a45b635C8C3fF64a1cc1;
// Load the private key from the `PRIVATE_KEY` environment variable (in .env)
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
// Start broadcasting transactions from the deployer account
vm.startBroadcast(deployerPrivateKey);
// Deploy Freeze
Freeze freeze = new Freeze();
// Hang Freeze on the hook
IWorld(worldAddress).registerStoreHook(
Counter._tableId,
freeze,
BEFORE_SET_RECORD | BEFORE_SPLICE_STATIC_DATA | BEFORE_SPLICE_DYNAMIC_DATA | BEFORE_DELETE_RECORD
);
vm.stopBroadcast();
}
}
Explanation
import { BEFORE_SET_RECORD, BEFORE_SPLICE_STATIC_DATA, BEFORE_SPLICE_DYNAMIC_DATA, BEFORE_DELETE_RECORD } from "@latticexyz/store/src/storeHookTypes.sol";
Bitmap values for the hooks we want to use.
import { IWorld } from "../src/codegen/world/IWorld.sol";
When we use a World
we need this definition to register hooks.
If we were using store without a World
, we'd use StoreSwitch
instead.
// Deploy Freeze
Freeze freeze = new Freeze();
Deploy the hook contract.
If this contract is stateless (as it should be), we can use the same contract from multiple World
s.
// Hang Freeze on the hook
IWorld(worldAddress).registerStoreHook(
Register the hook.
This is the syntax to use when Store
is part of a MUD World
.
When it isn't, we'd use StoreSwitch.registerStoreHook()
.
Counter._tableId,
The table whose hooks we use.
freeze,
The contract to be called when a hook is activated.
BEFORE_SET_RECORD | BEFORE_SPLICE_STATIC_DATA | BEFORE_SPLICE_DYNAMIC_DATA | BEFORE_DELETE_RECORD
);
The hook functions to use.
You can see the complete list in storeHookTypes.sol
(opens in a new tab).