From 85e105cc43067c8217c809d4e4618a1ed03a2a67 Mon Sep 17 00:00:00 2001 From: dk1a Date: Thu, 16 May 2024 16:20:57 +0300 Subject: [PATCH 1/2] LibOrderTimelock --- packages/contracts/mud.config.ts | 1 + packages/contracts/src/codegen/index.sol | 1 + .../src/codegen/tables/LastFulfilled.sol | 199 ++++++++++++++++++ packages/contracts/src/constants.sol | 2 + .../src/libraries/LibOrderTimelock.sol | 15 ++ .../contracts/src/libraries/Libraries.sol | 1 + .../contracts/src/systems/tank/TankSystem.sol | 6 +- .../test/systems/orders/OrderSystem.t.sol | 47 ++++- 8 files changed, 270 insertions(+), 2 deletions(-) create mode 100644 packages/contracts/src/codegen/tables/LastFulfilled.sol create mode 100644 packages/contracts/src/libraries/LibOrderTimelock.sol diff --git a/packages/contracts/mud.config.ts b/packages/contracts/mud.config.ts index 9ae644c8..03e137ce 100644 --- a/packages/contracts/mud.config.ts +++ b/packages/contracts/mud.config.ts @@ -96,6 +96,7 @@ export default defineWorld({ inlets: "bytes32[]" } }, + LastFulfilled: "uint256", // Used to track time since last order fulfillment CurrentOrder: "bytes32", // ID of the order the player has accepted Recipe: { key: ["machineType", "input"], diff --git a/packages/contracts/src/codegen/index.sol b/packages/contracts/src/codegen/index.sol index 685526dd..3afdc710 100644 --- a/packages/contracts/src/codegen/index.sol +++ b/packages/contracts/src/codegen/index.sol @@ -29,5 +29,6 @@ import { TankConnection } from "./tables/TankConnection.sol"; import { MachinesInPod } from "./tables/MachinesInPod.sol"; import { TanksInPod } from "./tables/TanksInPod.sol"; import { FixedEntities, FixedEntitiesData } from "./tables/FixedEntities.sol"; +import { LastFulfilled } from "./tables/LastFulfilled.sol"; import { CurrentOrder } from "./tables/CurrentOrder.sol"; import { Recipe } from "./tables/Recipe.sol"; diff --git a/packages/contracts/src/codegen/tables/LastFulfilled.sol b/packages/contracts/src/codegen/tables/LastFulfilled.sol new file mode 100644 index 00000000..04c72555 --- /dev/null +++ b/packages/contracts/src/codegen/tables/LastFulfilled.sol @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +// Import store internals +import { IStore } from "@latticexyz/store/src/IStore.sol"; +import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; +import { StoreCore } from "@latticexyz/store/src/StoreCore.sol"; +import { Bytes } from "@latticexyz/store/src/Bytes.sol"; +import { Memory } from "@latticexyz/store/src/Memory.sol"; +import { SliceLib } from "@latticexyz/store/src/Slice.sol"; +import { EncodeArray } from "@latticexyz/store/src/tightcoder/EncodeArray.sol"; +import { FieldLayout } from "@latticexyz/store/src/FieldLayout.sol"; +import { Schema } from "@latticexyz/store/src/Schema.sol"; +import { EncodedLengths, EncodedLengthsLib } from "@latticexyz/store/src/EncodedLengths.sol"; +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; + +library LastFulfilled { + // Hex below is the result of `WorldResourceIdLib.encode({ namespace: "", name: "LastFulfilled", typeId: RESOURCE_TABLE });` + ResourceId constant _tableId = ResourceId.wrap(0x746200000000000000000000000000004c61737446756c66696c6c6564000000); + + FieldLayout constant _fieldLayout = + FieldLayout.wrap(0x0020010020000000000000000000000000000000000000000000000000000000); + + // Hex-encoded key schema of (bytes32) + Schema constant _keySchema = Schema.wrap(0x002001005f000000000000000000000000000000000000000000000000000000); + // Hex-encoded value schema of (uint256) + Schema constant _valueSchema = Schema.wrap(0x002001001f000000000000000000000000000000000000000000000000000000); + + /** + * @notice Get the table's key field names. + * @return keyNames An array of strings with the names of key fields. + */ + function getKeyNames() internal pure returns (string[] memory keyNames) { + keyNames = new string[](1); + keyNames[0] = "id"; + } + + /** + * @notice Get the table's value field names. + * @return fieldNames An array of strings with the names of value fields. + */ + function getFieldNames() internal pure returns (string[] memory fieldNames) { + fieldNames = new string[](1); + fieldNames[0] = "value"; + } + + /** + * @notice Register the table with its config. + */ + function register() internal { + StoreSwitch.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); + } + + /** + * @notice Register the table with its config. + */ + function _register() internal { + StoreCore.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); + } + + /** + * @notice Get value. + */ + function getValue(bytes32 id) internal view returns (uint256 value) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = id; + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get value. + */ + function _getValue(bytes32 id) internal view returns (uint256 value) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = id; + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get value. + */ + function get(bytes32 id) internal view returns (uint256 value) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = id; + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get value. + */ + function _get(bytes32 id) internal view returns (uint256 value) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = id; + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Set value. + */ + function setValue(bytes32 id, uint256 value) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = id; + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); + } + + /** + * @notice Set value. + */ + function _setValue(bytes32 id, uint256 value) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = id; + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); + } + + /** + * @notice Set value. + */ + function set(bytes32 id, uint256 value) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = id; + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); + } + + /** + * @notice Set value. + */ + function _set(bytes32 id, uint256 value) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = id; + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); + } + + /** + * @notice Delete all data for given keys. + */ + function deleteRecord(bytes32 id) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = id; + + StoreSwitch.deleteRecord(_tableId, _keyTuple); + } + + /** + * @notice Delete all data for given keys. + */ + function _deleteRecord(bytes32 id) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = id; + + StoreCore.deleteRecord(_tableId, _keyTuple, _fieldLayout); + } + + /** + * @notice Tightly pack static (fixed length) data using this table's schema. + * @return The static data, encoded into a sequence of bytes. + */ + function encodeStatic(uint256 value) internal pure returns (bytes memory) { + return abi.encodePacked(value); + } + + /** + * @notice Encode all of a record's fields. + * @return The static (fixed length) data, encoded into a sequence of bytes. + * @return The lengths of the dynamic fields (packed into a single bytes32 value). + * @return The dynamic (variable length) data, encoded into a sequence of bytes. + */ + function encode(uint256 value) internal pure returns (bytes memory, EncodedLengths, bytes memory) { + bytes memory _staticData = encodeStatic(value); + + EncodedLengths _encodedLengths; + bytes memory _dynamicData; + + return (_staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Encode keys as a bytes32 array using this table's field layout. + */ + function encodeKeyTuple(bytes32 id) internal pure returns (bytes32[] memory) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = id; + + return _keyTuple; + } +} diff --git a/packages/contracts/src/constants.sol b/packages/contracts/src/constants.sol index b7bf07ed..63132a09 100644 --- a/packages/contracts/src/constants.sol +++ b/packages/contracts/src/constants.sol @@ -16,3 +16,5 @@ uint32 constant NUMBER_OF_TANKS = 3; /// @dev The base rate of material pushed trough the network per block uint256 constant FLOW_RATE = 10 * ONE_UNIT; uint256 constant TANK_CAPACITY = 500 * ONE_UNIT; + +uint256 constant ORDER_TIMELOCK = 10 minutes; diff --git a/packages/contracts/src/libraries/LibOrderTimelock.sol b/packages/contracts/src/libraries/LibOrderTimelock.sol new file mode 100644 index 00000000..74b0fe46 --- /dev/null +++ b/packages/contracts/src/libraries/LibOrderTimelock.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; +import { LastFulfilled } from "../codegen/index.sol"; +import { ORDER_TIMELOCK } from "../constants.sol"; + +library LibOrderTimelock { + function checkLock(bytes32 _playerEntity) internal view { + uint256 lastFulfilled = LastFulfilled.get(_playerEntity); + require(lastFulfilled == 0 || block.timestamp >= lastFulfilled + ORDER_TIMELOCK, "order fulfillment timelocked"); + } + + function updateLock(bytes32 _playerEntity) internal { + LastFulfilled.set(_playerEntity, block.timestamp); + } +} diff --git a/packages/contracts/src/libraries/Libraries.sol b/packages/contracts/src/libraries/Libraries.sol index 4f445c30..57d7b783 100644 --- a/packages/contracts/src/libraries/Libraries.sol +++ b/packages/contracts/src/libraries/Libraries.sol @@ -8,6 +8,7 @@ import { LibNetwork } from "./LibNetwork.sol"; import { LibMachine } from "./LibMachine.sol"; import { LibTank } from "./LibTank.sol"; import { LibOrder } from "./LibOrder.sol"; +import { LibOrderTimelock } from "./LibOrderTimelock.sol"; import { LibWipe } from "./LibWipe.sol"; import { LibOffer } from "./LibOffer.sol"; import { LibMaterial, MaterialId } from "./LibMaterial.sol"; diff --git a/packages/contracts/src/systems/tank/TankSystem.sol b/packages/contracts/src/systems/tank/TankSystem.sol index 0bda21da..b649577f 100644 --- a/packages/contracts/src/systems/tank/TankSystem.sol +++ b/packages/contracts/src/systems/tank/TankSystem.sol @@ -3,7 +3,7 @@ pragma solidity >=0.8.24; import { System } from "@latticexyz/world/src/System.sol"; import { EntityType, CarriedBy, ContainedMaterial, MachineType, Amount, TankConnection, CurrentOrder, Order, OrderData, CompletedOrders, CompletedPlayers, Tutorial, TutorialLevel, NonTransferableBalance, TanksInPod, ProducedMaterials, GameConfig } from "../../codegen/index.sol"; import { ENTITY_TYPE, MACHINE_TYPE } from "../../codegen/common.sol"; -import { LibUtils, LibNetwork, LibMaterial, PublicMaterials } from "../../libraries/Libraries.sol"; +import { LibUtils, LibOrderTimelock, LibNetwork, LibMaterial, PublicMaterials } from "../../libraries/Libraries.sol"; import { NUMBER_OF_TUTORIAL_LEVELS, TANK_CAPACITY } from "../../constants.sol"; contract TankSystem is System { @@ -166,6 +166,10 @@ contract TankSystem is System { IN MAIN GAME //////////////////////////////////////////////////////////////*/ + // Prevent batching order shipments + LibOrderTimelock.checkLock(playerEntity); + LibOrderTimelock.updateLock(playerEntity); + // Reward player in real tokens PublicMaterials.BUGS.mint(_msgSender(), currentOrder.reward); diff --git a/packages/contracts/test/systems/orders/OrderSystem.t.sol b/packages/contracts/test/systems/orders/OrderSystem.t.sol index d5535bc2..3e101430 100644 --- a/packages/contracts/test/systems/orders/OrderSystem.t.sol +++ b/packages/contracts/test/systems/orders/OrderSystem.t.sol @@ -5,7 +5,7 @@ import { BaseTest } from "../../BaseTest.sol"; import "../../../src/codegen/index.sol"; import "../../../src/libraries/Libraries.sol"; import { MACHINE_TYPE, PORT_INDEX } from "../../../src/codegen/common.sol"; -import { FLOW_RATE, ONE_MINUTE, ONE_HOUR, ONE_UNIT } from "../../../src/constants.sol"; +import { FLOW_RATE, ONE_MINUTE, ONE_HOUR, ONE_UNIT, ORDER_TIMELOCK } from "../../../src/constants.sol"; import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; import { WorldResourceIdInstance } from "@latticexyz/world/src/WorldResourceId.sol"; @@ -334,6 +334,51 @@ contract OrderSystemTest is BaseTest { vm.stopPrank(); } + function testShipAfterTimelock() public { + prankAdmin(); + bytes32 order1Entity = world.createOrder(PublicMaterials.BUGS, 100, 100, ONE_MINUTE, 10); + bytes32 order2Entity = world.createOrder(PublicMaterials.BUGS, 100, 100, ONE_MINUTE, 10); + world.devGraduate(alice); + world.devFillTank(tanksInPod[0], 200, PublicMaterials.BUGS); + vm.stopPrank(); + + vm.startPrank(alice); + + world.acceptOrder(order1Entity); + + world.shipTank(tanksInPod[0]); + + world.acceptOrder(order2Entity); + + vm.warp(block.timestamp + ORDER_TIMELOCK); + + world.shipTank(tanksInPod[0]); + + vm.stopPrank(); + } + + function testRevertOrderTimelocked() public { + prankAdmin(); + bytes32 order1Entity = world.createOrder(PublicMaterials.BUGS, 100, 100, ONE_MINUTE, 10); + bytes32 order2Entity = world.createOrder(PublicMaterials.BUGS, 100, 100, ONE_MINUTE, 10); + world.devGraduate(alice); + world.devFillTank(tanksInPod[0], 200, PublicMaterials.BUGS); + vm.stopPrank(); + + vm.startPrank(alice); + + world.acceptOrder(order1Entity); + + world.shipTank(tanksInPod[0]); + + world.acceptOrder(order2Entity); + + vm.expectRevert("order fulfillment timelocked"); + world.shipTank(tanksInPod[0]); + + vm.stopPrank(); + } + function testRevertAcceptExhaustedOrder() public { prankAdmin(); bytes32 orderEntity = world.createOrder(PublicMaterials.BUGS, 100, 100, ONE_MINUTE, 1); From 9f84189da089707675494fa7cc473cfa3b09c23f Mon Sep 17 00:00:00 2001 From: dk1a Date: Thu, 16 May 2024 16:21:07 +0300 Subject: [PATCH 2/2] client types --- packages/client/src/svelte/modules/state/base/types.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/client/src/svelte/modules/state/base/types.ts b/packages/client/src/svelte/modules/state/base/types.ts index 27897f8b..1883b8f0 100644 --- a/packages/client/src/svelte/modules/state/base/types.ts +++ b/packages/client/src/svelte/modules/state/base/types.ts @@ -39,6 +39,7 @@ declare global { machinesInPod?: string[] tanksInPod?: string[] fixedEntities?: FixedEntities + lastFulfilled?: bigint currentOrder?: string tokenBalances?: bigint recipe?: { @@ -100,6 +101,7 @@ declare global { spawnIndex: number name?: string carriedBy: string + lastFulfilled: bigint currentOrder: string incomingConnections: string[] outgoingConnections: string[]