diff --git a/.changeset/soft-fans-wink.md b/.changeset/soft-fans-wink.md new file mode 100644 index 0000000000..add3beb520 --- /dev/null +++ b/.changeset/soft-fans-wink.md @@ -0,0 +1,16 @@ +--- +"@latticexyz/store": minor +"@latticexyz/world": minor +--- + +Add protocol version with corresponding getter and event on deploy + +```solidity +world.worldVersion(); +world.storeVersion(); // a World is also a Store +``` + +```solidity +event HelloWorld(bytes32 indexed worldVersion); +event HelloStore(bytes32 indexed storeVersion); +``` diff --git a/packages/store/gas-report.json b/packages/store/gas-report.json index 2aa0f2ab67..3a701079a4 100644 --- a/packages/store/gas-report.json +++ b/packages/store/gas-report.json @@ -699,7 +699,7 @@ "file": "test/StoreCoreGas.t.sol", "test": "testHooks", "name": "delete record on table with subscriber", - "gasUsed": 16415 + "gasUsed": 16438 }, { "file": "test/StoreCoreGas.t.sol", @@ -723,7 +723,7 @@ "file": "test/StoreCoreGas.t.sol", "test": "testHooksDynamicData", "name": "delete (dynamic) record on table with subscriber", - "gasUsed": 17400 + "gasUsed": 17423 }, { "file": "test/StoreCoreGas.t.sol", diff --git a/packages/store/src/IStore.sol b/packages/store/src/IStore.sol index b13097f3a4..767ec27b7b 100644 --- a/packages/store/src/IStore.sol +++ b/packages/store/src/IStore.sol @@ -8,6 +8,10 @@ import { Schema } from "./Schema.sol"; import { IStoreHook } from "./IStoreHook.sol"; interface IStoreRead { + event HelloStore(bytes32 indexed storeVersion); + + function storeVersion() external view returns (bytes32); + function getFieldLayout(bytes32 tableId) external view returns (FieldLayout fieldLayout); function getValueSchema(bytes32 tableId) external view returns (Schema valueSchema); diff --git a/packages/store/src/StoreCore.sol b/packages/store/src/StoreCore.sol index 8b95cebd92..5c2b974148 100644 --- a/packages/store/src/StoreCore.sol +++ b/packages/store/src/StoreCore.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; +import { STORE_VERSION } from "./version.sol"; import { Bytes } from "./Bytes.sol"; import { Storage } from "./Storage.sol"; import { Memory } from "./Memory.sol"; @@ -16,7 +17,7 @@ import { Hook, HookLib } 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 + event HelloStore(bytes32 indexed version); event StoreSetRecord( bytes32 indexed tableId, bytes32[] keyTuple, @@ -53,6 +54,8 @@ library StoreCore { * Consumers must call this function in their constructor. */ function initialize() internal { + emit HelloStore(STORE_VERSION); + // StoreSwitch uses the storeAddress to decide where to write data to. // If StoreSwitch is called in the context of a Store contract (storeAddress == address(this)), // StoreSwitch uses internal methods to write data instead of external calls. diff --git a/packages/store/src/StoreRead.sol b/packages/store/src/StoreRead.sol index 3017f1510f..82ea6e30fe 100644 --- a/packages/store/src/StoreRead.sol +++ b/packages/store/src/StoreRead.sol @@ -1,12 +1,17 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; +import { STORE_VERSION } from "./version.sol"; import { IStoreRead } from "./IStore.sol"; import { StoreCore } from "./StoreCore.sol"; import { FieldLayout } from "./FieldLayout.sol"; import { Schema } from "./Schema.sol"; contract StoreRead is IStoreRead { + function storeVersion() public pure returns (bytes32) { + return STORE_VERSION; + } + function getFieldLayout(bytes32 tableId) public view virtual returns (FieldLayout fieldLayout) { fieldLayout = StoreCore.getFieldLayout(tableId); } diff --git a/packages/store/src/version.sol b/packages/store/src/version.sol new file mode 100644 index 0000000000..9f024319ad --- /dev/null +++ b/packages/store/src/version.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +bytes32 constant STORE_VERSION = "0.0.0"; diff --git a/packages/store/test/StoreMock.t.sol b/packages/store/test/StoreMock.t.sol new file mode 100644 index 0000000000..641ff8f528 --- /dev/null +++ b/packages/store/test/StoreMock.t.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import { Test } from "forge-std/Test.sol"; +import { GasReporter } from "@latticexyz/gas-report/src/GasReporter.sol"; +import { STORE_VERSION } from "../src/version.sol"; +import { StoreCore } from "../src/StoreCore.sol"; +import { StoreMock } from "../test/StoreMock.sol"; +import { StoreSwitch } from "../src/StoreSwitch.sol"; + +contract StoreMockTest is Test { + event HelloStore(bytes32 indexed storeVersion); + + function testStoreMockConstrctor() public { + vm.expectEmit(true, true, true, true); + emit HelloStore(STORE_VERSION); + new StoreMock(); + } +} diff --git a/packages/store/ts/storeEventsAbi.test.ts b/packages/store/ts/storeEventsAbi.test.ts index be25f2cd8a..17b704d341 100644 --- a/packages/store/ts/storeEventsAbi.test.ts +++ b/packages/store/ts/storeEventsAbi.test.ts @@ -7,7 +7,10 @@ import { AbiEvent } from "abitype"; describe("storeEventsAbi", () => { it("should match the store ABI", () => { - const expectedEvents = IStoreAbi.filter((item) => item.type === "event") as readonly AbiEvent[]; + const expectedEvents = IStoreAbi.filter((item) => item.type === "event").filter( + (item) => item.name !== "HelloStore" + ) as readonly AbiEvent[]; + const expectedAbi = expectedEvents .map((item) => ({ // return data in a shape that matches abitype's parseAbi diff --git a/packages/world/gas-report.json b/packages/world/gas-report.json index 35515f840a..cb6c335b40 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": 1415306 + "gasUsed": 1415308 }, { "file": "test/KeysInTableModule.t.sol", "test": "testInstallGas", "name": "install keys in table module", - "gasUsed": 1415306 + "gasUsed": 1415308 }, { "file": "test/KeysInTableModule.t.sol", "test": "testInstallGas", "name": "set a record on a table with keysInTableModule installed", - "gasUsed": 158959 + "gasUsed": 158915 }, { "file": "test/KeysInTableModule.t.sol", "test": "testInstallSingleton", "name": "install keys in table module", - "gasUsed": 1415306 + "gasUsed": 1415308 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookCompositeGas", "name": "install keys in table module", - "gasUsed": 1415306 + "gasUsed": 1415308 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookCompositeGas", "name": "change a composite record on a table with keysInTableModule installed", - "gasUsed": 22016 + "gasUsed": 21994 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookCompositeGas", "name": "delete a composite record on a table with keysInTableModule installed", - "gasUsed": 171520 + "gasUsed": 171498 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookGas", "name": "install keys in table module", - "gasUsed": 1415306 + "gasUsed": 1415308 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookGas", "name": "change a record on a table with keysInTableModule installed", - "gasUsed": 20738 + "gasUsed": 20716 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookGas", "name": "delete a record on a table with keysInTableModule installed", - "gasUsed": 88171 + "gasUsed": 88149 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testGetKeysWithValueGas", "name": "install keys with value module", - "gasUsed": 654597 + "gasUsed": 654598 }, { "file": "test/KeysWithValueModule.t.sol", @@ -117,49 +117,49 @@ "file": "test/KeysWithValueModule.t.sol", "test": "testInstall", "name": "install keys with value module", - "gasUsed": 654597 + "gasUsed": 654598 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testInstall", "name": "set a record on a table with KeysWithValueModule installed", - "gasUsed": 134495 + "gasUsed": 134451 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetAndDeleteRecordHook", "name": "install keys with value module", - "gasUsed": 654597 + "gasUsed": 654598 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetAndDeleteRecordHook", "name": "change a record on a table with KeysWithValueModule installed", - "gasUsed": 104070 + "gasUsed": 104026 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetAndDeleteRecordHook", "name": "delete a record on a table with KeysWithValueModule installed", - "gasUsed": 32927 + "gasUsed": 32905 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetField", "name": "install keys with value module", - "gasUsed": 654597 + "gasUsed": 654598 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetField", "name": "set a field on a table with KeysWithValueModule installed", - "gasUsed": 139109 + "gasUsed": 139065 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetField", "name": "change a field on a table with KeysWithValueModule installed", - "gasUsed": 103868 + "gasUsed": 103824 }, { "file": "test/query.t.sol", @@ -255,7 +255,7 @@ "file": "test/UniqueEntityModule.t.sol", "test": "testInstall", "name": "install unique entity module", - "gasUsed": 678986 + "gasUsed": 678987 }, { "file": "test/UniqueEntityModule.t.sol", @@ -267,7 +267,7 @@ "file": "test/UniqueEntityModule.t.sol", "test": "testInstallRoot", "name": "installRoot unique entity module", - "gasUsed": 669045 + "gasUsed": 669046 }, { "file": "test/UniqueEntityModule.t.sol", @@ -279,7 +279,7 @@ "file": "test/World.t.sol", "test": "testCall", "name": "call a system via the World", - "gasUsed": 12288 + "gasUsed": 12333 }, { "file": "test/World.t.sol", @@ -309,7 +309,7 @@ "file": "test/World.t.sol", "test": "testRegisterFallbackSystem", "name": "Register a fallback system", - "gasUsed": 58887 + "gasUsed": 58888 }, { "file": "test/World.t.sol", @@ -321,7 +321,7 @@ "file": "test/World.t.sol", "test": "testRegisterFunctionSelector", "name": "Register a function selector", - "gasUsed": 79481 + "gasUsed": 79482 }, { "file": "test/World.t.sol", diff --git a/packages/world/src/World.sol b/packages/world/src/World.sol index bc3895dd1e..88265fa063 100644 --- a/packages/world/src/World.sol +++ b/packages/world/src/World.sol @@ -10,6 +10,7 @@ import { Schema } from "@latticexyz/store/src/Schema.sol"; import { PackedCounter } from "@latticexyz/store/src/PackedCounter.sol"; import { FieldLayout } from "@latticexyz/store/src/FieldLayout.sol"; +import { WORLD_VERSION } from "./version.sol"; import { System } from "./System.sol"; import { ResourceSelector } from "./ResourceSelector.sol"; import { ROOT_NAMESPACE, ROOT_NAME } from "./constants.sol"; @@ -38,10 +39,14 @@ contract World is StoreRead, IStoreData, IWorldKernel { using ResourceSelector for bytes32; address public immutable creator; + function worldVersion() public pure returns (bytes32) { + return WORLD_VERSION; + } + constructor() { creator = msg.sender; StoreCore.initialize(); - emit HelloWorld(); + emit HelloWorld(WORLD_VERSION); } /** diff --git a/packages/world/src/interfaces/IWorldKernel.sol b/packages/world/src/interfaces/IWorldKernel.sol index 1dc9f602ad..d5b1c5cec4 100644 --- a/packages/world/src/interfaces/IWorldKernel.sol +++ b/packages/world/src/interfaces/IWorldKernel.sol @@ -39,7 +39,12 @@ interface IWorldCall { * registered functions selectors from the `CoreModule`. */ interface IWorldKernel is IWorldModuleInstallation, IWorldCall, IWorldErrors { - event HelloWorld(); + event HelloWorld(bytes32 indexed worldVersion); + + /** + * The version of the World. + */ + function worldVersion() external view returns (bytes32); /** * The immutable original deployer of the World. diff --git a/packages/world/src/version.sol b/packages/world/src/version.sol new file mode 100644 index 0000000000..bd7a91f4d6 --- /dev/null +++ b/packages/world/src/version.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +bytes32 constant WORLD_VERSION = "0.0.0"; diff --git a/packages/world/test/Factories.t.sol b/packages/world/test/Factories.t.sol index 9cc72f6a3d..5887d42ddb 100644 --- a/packages/world/test/Factories.t.sol +++ b/packages/world/test/Factories.t.sol @@ -4,6 +4,7 @@ pragma solidity >=0.8.0; import { Test, console } from "forge-std/Test.sol"; import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; +import { WORLD_VERSION } from "../src/version.sol"; import { World } from "../src/World.sol"; import { CoreModule } from "../src/modules/core/CoreModule.sol"; import { Create2Factory } from "../src/factories/Create2Factory.sol"; @@ -16,7 +17,7 @@ import { ROOT_NAMESPACE } from "../src/constants.sol"; contract FactoriesTest is Test { event ContractDeployed(address addr, uint256 salt); event WorldDeployed(address indexed newContract); - event HelloWorld(); + event HelloWorld(bytes32 indexed version); function calculateAddress( address deployingAddress, @@ -58,8 +59,8 @@ contract FactoriesTest is Test { address calculatedAddress = calculateAddress(worldFactoryAddress, bytes32(0), type(World).creationCode); // Check for HelloWorld event from World - vm.expectEmit(true, false, false, false); - emit HelloWorld(); + vm.expectEmit(true, true, true, true); + emit HelloWorld(WORLD_VERSION); // Check for WorldDeployed event from Factory vm.expectEmit(true, false, false, false); diff --git a/packages/world/test/World.t.sol b/packages/world/test/World.t.sol index 9d651d3ef2..6a3406fe63 100644 --- a/packages/world/test/World.t.sol +++ b/packages/world/test/World.t.sol @@ -20,6 +20,7 @@ import { StoreHookLib } from "@latticexyz/store/src/StoreHook.sol"; import { RevertSubscriber } from "@latticexyz/store/test/RevertSubscriber.sol"; import { EchoSubscriber } from "@latticexyz/store/test/EchoSubscriber.sol"; +import { WORLD_VERSION } from "../src/version.sol"; import { World } from "../src/World.sol"; import { System } from "../src/System.sol"; import { ResourceSelector } from "../src/ResourceSelector.sol"; @@ -158,7 +159,7 @@ contract RevertSystemHook is SystemHook { contract WorldTest is Test, GasReporter { using ResourceSelector for bytes32; - event HelloWorld(); + event HelloWorld(bytes32 indexed worldVersion); event HookCalled(bytes data); event SystemHookCalled(bytes data); event WorldTestSystemLog(string log); @@ -196,7 +197,7 @@ contract WorldTest is Test, GasReporter { CoreModule coreModule = new CoreModule(); vm.expectEmit(true, true, true, true); - emit HelloWorld(); + emit HelloWorld(WORLD_VERSION); IBaseWorld newWorld = IBaseWorld(address(new World())); // Expect the creator to be the original deployer