diff --git a/packages/world-modules/gas-report.json b/packages/world-modules/gas-report.json index e5cf4c39a0..b036a94012 100644 --- a/packages/world-modules/gas-report.json +++ b/packages/world-modules/gas-report.json @@ -225,7 +225,7 @@ "file": "test/StandardDelegationsModule.t.sol", "test": "testCallFromCallboundDelegation", "name": "register a callbound delegation", - "gasUsed": 117420 + "gasUsed": 117426 }, { "file": "test/StandardDelegationsModule.t.sol", @@ -233,11 +233,23 @@ "name": "call a system via a callbound delegation", "gasUsed": 36688 }, + { + "file": "test/StandardDelegationsModule.t.sol", + "test": "testCallFromSystemDelegation", + "name": "register a systembound delegation", + "gasUsed": 114982 + }, + { + "file": "test/StandardDelegationsModule.t.sol", + "test": "testCallFromSystemDelegation", + "name": "call a system via a systembound delegation", + "gasUsed": 33831 + }, { "file": "test/StandardDelegationsModule.t.sol", "test": "testCallFromTimeboundDelegation", "name": "register a timebound delegation", - "gasUsed": 111914 + "gasUsed": 111920 }, { "file": "test/StandardDelegationsModule.t.sol", diff --git a/packages/world-modules/mud.config.ts b/packages/world-modules/mud.config.ts index a830eecd1f..aa8099f3fa 100644 --- a/packages/world-modules/mud.config.ts +++ b/packages/world-modules/mud.config.ts @@ -80,6 +80,17 @@ export default mudConfig({ availableCalls: "uint256", }, }, + SystemboundDelegations: { + directory: "modules/std-delegations/tables", + keySchema: { + delegator: "address", + delegatee: "address", + systemId: "ResourceId", + }, + valueSchema: { + availableCalls: "uint256", + }, + }, TimeboundDelegations: { directory: "modules/std-delegations/tables", keySchema: { diff --git a/packages/world-modules/src/index.sol b/packages/world-modules/src/index.sol index b1d73d8c0d..6a983a0ac7 100644 --- a/packages/world-modules/src/index.sol +++ b/packages/world-modules/src/index.sol @@ -8,6 +8,7 @@ import { KeysInTable, KeysInTableData, KeysInTableTableId } from "./modules/keys import { UsedKeysIndex, UsedKeysIndexTableId } from "./modules/keysintable/tables/UsedKeysIndex.sol"; import { UniqueEntity } from "./modules/uniqueentity/tables/UniqueEntity.sol"; import { CallboundDelegations, CallboundDelegationsTableId } from "./modules/std-delegations/tables/CallboundDelegations.sol"; +import { SystemboundDelegations, SystemboundDelegationsTableId } from "./modules/std-delegations/tables/SystemboundDelegations.sol"; import { TimeboundDelegations, TimeboundDelegationsTableId } from "./modules/std-delegations/tables/TimeboundDelegations.sol"; import { PuppetRegistry } from "./modules/puppet/tables/PuppetRegistry.sol"; import { Balances } from "./modules/tokens/tables/Balances.sol"; diff --git a/packages/world-modules/src/modules/std-delegations/StandardDelegationsModule.sol b/packages/world-modules/src/modules/std-delegations/StandardDelegationsModule.sol index fe5ead7929..ceee1895e7 100644 --- a/packages/world-modules/src/modules/std-delegations/StandardDelegationsModule.sol +++ b/packages/world-modules/src/modules/std-delegations/StandardDelegationsModule.sol @@ -8,10 +8,12 @@ import { WorldContextConsumer } from "@latticexyz/world/src/WorldContext.sol"; import { revertWithBytes } from "@latticexyz/world/src/revertWithBytes.sol"; import { CallboundDelegationControl } from "./CallboundDelegationControl.sol"; +import { SystemboundDelegationControl } from "./SystemboundDelegationControl.sol"; import { TimeboundDelegationControl } from "./TimeboundDelegationControl.sol"; -import { MODULE_NAME, CALLBOUND_DELEGATION, TIMEBOUND_DELEGATION } from "./constants.sol"; +import { MODULE_NAME, CALLBOUND_DELEGATION, SYSTEMBOUND_DELEGATION, TIMEBOUND_DELEGATION } from "./constants.sol"; import { CallboundDelegations } from "./tables/CallboundDelegations.sol"; +import { SystemboundDelegations } from "./tables/SystemboundDelegations.sol"; import { TimeboundDelegations } from "./tables/TimeboundDelegations.sol"; /** @@ -19,6 +21,7 @@ import { TimeboundDelegations } from "./tables/TimeboundDelegations.sol"; */ contract StandardDelegationsModule is Module { CallboundDelegationControl private immutable callboundDelegationControl = new CallboundDelegationControl(); + SystemboundDelegationControl private immutable systemboundDelegationControl = new SystemboundDelegationControl(); TimeboundDelegationControl private immutable timeboundDelegationControl = new TimeboundDelegationControl(); function getName() public pure returns (bytes16) { @@ -30,6 +33,7 @@ contract StandardDelegationsModule is Module { // Register tables CallboundDelegations.register(); + SystemboundDelegations.register(); TimeboundDelegations.register(); // Register systems @@ -38,6 +42,11 @@ contract StandardDelegationsModule is Module { ); if (!success) revertWithBytes(returnData); + (success, returnData) = address(world).delegatecall( + abi.encodeCall(world.registerSystem, (SYSTEMBOUND_DELEGATION, systemboundDelegationControl, true)) + ); + if (!success) revertWithBytes(returnData); + (success, returnData) = address(world).delegatecall( abi.encodeCall(world.registerSystem, (TIMEBOUND_DELEGATION, timeboundDelegationControl, true)) ); diff --git a/packages/world-modules/src/modules/std-delegations/SystemboundDelegationControl.sol b/packages/world-modules/src/modules/std-delegations/SystemboundDelegationControl.sol new file mode 100644 index 0000000000..ea61274a2b --- /dev/null +++ b/packages/world-modules/src/modules/std-delegations/SystemboundDelegationControl.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.21; + +import { DelegationControl } from "@latticexyz/world/src/DelegationControl.sol"; +import { ResourceId } from "@latticexyz/world/src/WorldResourceId.sol"; +import { SystemboundDelegations } from "./tables/SystemboundDelegations.sol"; + +contract SystemboundDelegationControl is DelegationControl { + /** + * Verify a delegation by checking if the caller (delegatee) has any available calls left for the given delegator in the SystemboundDelegations table and decrementing the available calls if so. + */ + function verify(address delegator, ResourceId systemId, bytes memory) public returns (bool) { + // Get the number of available calls for the given delegator, systemId and callData + uint256 availableCalls = SystemboundDelegations.get({ + delegator: delegator, + delegatee: _msgSender(), + systemId: systemId + }); + + if (availableCalls == 1) { + // Remove the delegation from the SystemboundDelegations table + SystemboundDelegations.deleteRecord({ delegator: delegator, delegatee: _msgSender(), systemId: systemId }); + return true; + } + + if (availableCalls > 0) { + // Decrement the number of available calls + unchecked { + availableCalls--; + } + SystemboundDelegations.set({ + delegator: delegator, + delegatee: _msgSender(), + systemId: systemId, + availableCalls: availableCalls + }); + return true; + } + + return false; + } + + /** + * Initialize a delegation by setting the number of available calls in the SystemboundDelegations table + */ + function initDelegation(address delegatee, ResourceId systemId, uint256 numCalls) public { + SystemboundDelegations.set({ + delegator: _msgSender(), + delegatee: delegatee, + systemId: systemId, + availableCalls: numCalls + }); + } +} diff --git a/packages/world-modules/src/modules/std-delegations/constants.sol b/packages/world-modules/src/modules/std-delegations/constants.sol index bc6b4096b9..1e788a0bfa 100644 --- a/packages/world-modules/src/modules/std-delegations/constants.sol +++ b/packages/world-modules/src/modules/std-delegations/constants.sol @@ -12,6 +12,11 @@ ResourceId constant CALLBOUND_DELEGATION = ResourceId.wrap( bytes32(abi.encodePacked(RESOURCE_SYSTEM, ROOT_NAMESPACE, bytes16("callbound"))) ); +// Systembound delegation +ResourceId constant SYSTEMBOUND_DELEGATION = ResourceId.wrap( + bytes32(abi.encodePacked(RESOURCE_SYSTEM, ROOT_NAMESPACE, bytes16("systembound"))) +); + // Timebound delegation ResourceId constant TIMEBOUND_DELEGATION = ResourceId.wrap( bytes32(abi.encodePacked(RESOURCE_SYSTEM, ROOT_NAMESPACE, bytes16("timebound"))) diff --git a/packages/world-modules/src/modules/std-delegations/tables/SystemboundDelegations.sol b/packages/world-modules/src/modules/std-delegations/tables/SystemboundDelegations.sol new file mode 100644 index 0000000000..49fd116cc1 --- /dev/null +++ b/packages/world-modules/src/modules/std-delegations/tables/SystemboundDelegations.sol @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.21; + +/* Autogenerated file. Do not edit manually. */ + +// Import schema type +import { SchemaType } from "@latticexyz/schema-type/src/solidity/SchemaType.sol"; + +// Import store internals +import { IStore } from "@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, FieldLayoutLib } from "@latticexyz/store/src/FieldLayout.sol"; +import { Schema, SchemaLib } from "@latticexyz/store/src/Schema.sol"; +import { PackedCounter, PackedCounterLib } from "@latticexyz/store/src/PackedCounter.sol"; +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; +import { RESOURCE_TABLE, RESOURCE_OFFCHAIN_TABLE } from "@latticexyz/store/src/storeResourceTypes.sol"; + +// Import user types +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; + +ResourceId constant _tableId = ResourceId.wrap( + bytes32(abi.encodePacked(RESOURCE_TABLE, bytes14(""), bytes16("SystemboundDeleg"))) +); +ResourceId constant SystemboundDelegationsTableId = _tableId; + +FieldLayout constant _fieldLayout = FieldLayout.wrap( + 0x0020010020000000000000000000000000000000000000000000000000000000 +); + +library SystemboundDelegations { + /** + * @notice Get the table values' field layout. + * @return _fieldLayout The field layout for the table. + */ + function getFieldLayout() internal pure returns (FieldLayout) { + return _fieldLayout; + } + + /** + * @notice Get the table's key schema. + * @return _keySchema The key schema for the table. + */ + function getKeySchema() internal pure returns (Schema) { + SchemaType[] memory _keySchema = new SchemaType[](3); + _keySchema[0] = SchemaType.ADDRESS; + _keySchema[1] = SchemaType.ADDRESS; + _keySchema[2] = SchemaType.BYTES32; + + return SchemaLib.encode(_keySchema); + } + + /** + * @notice Get the table's value schema. + * @return _valueSchema The value schema for the table. + */ + function getValueSchema() internal pure returns (Schema) { + SchemaType[] memory _valueSchema = new SchemaType[](1); + _valueSchema[0] = SchemaType.UINT256; + + return SchemaLib.encode(_valueSchema); + } + + /** + * @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[](3); + keyNames[0] = "delegator"; + keyNames[1] = "delegatee"; + keyNames[2] = "systemId"; + } + + /** + * @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] = "availableCalls"; + } + + /** + * @notice Register the table with its config. + */ + function register() internal { + StoreSwitch.registerTable(_tableId, _fieldLayout, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames()); + } + + /** + * @notice Register the table with its config. + */ + function _register() internal { + StoreCore.registerTable(_tableId, _fieldLayout, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames()); + } + + /** + * @notice Get availableCalls. + */ + function getAvailableCalls( + address delegator, + address delegatee, + ResourceId systemId + ) internal view returns (uint256 availableCalls) { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + _keyTuple[2] = ResourceId.unwrap(systemId); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get availableCalls. + */ + function _getAvailableCalls( + address delegator, + address delegatee, + ResourceId systemId + ) internal view returns (uint256 availableCalls) { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + _keyTuple[2] = ResourceId.unwrap(systemId); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get availableCalls. + */ + function get( + address delegator, + address delegatee, + ResourceId systemId + ) internal view returns (uint256 availableCalls) { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + _keyTuple[2] = ResourceId.unwrap(systemId); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get availableCalls. + */ + function _get( + address delegator, + address delegatee, + ResourceId systemId + ) internal view returns (uint256 availableCalls) { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + _keyTuple[2] = ResourceId.unwrap(systemId); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Set availableCalls. + */ + function setAvailableCalls( + address delegator, + address delegatee, + ResourceId systemId, + uint256 availableCalls + ) internal { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + _keyTuple[2] = ResourceId.unwrap(systemId); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((availableCalls)), _fieldLayout); + } + + /** + * @notice Set availableCalls. + */ + function _setAvailableCalls( + address delegator, + address delegatee, + ResourceId systemId, + uint256 availableCalls + ) internal { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + _keyTuple[2] = ResourceId.unwrap(systemId); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((availableCalls)), _fieldLayout); + } + + /** + * @notice Set availableCalls. + */ + function set(address delegator, address delegatee, ResourceId systemId, uint256 availableCalls) internal { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + _keyTuple[2] = ResourceId.unwrap(systemId); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((availableCalls)), _fieldLayout); + } + + /** + * @notice Set availableCalls. + */ + function _set(address delegator, address delegatee, ResourceId systemId, uint256 availableCalls) internal { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + _keyTuple[2] = ResourceId.unwrap(systemId); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((availableCalls)), _fieldLayout); + } + + /** + * @notice Delete all data for given keys. + */ + function deleteRecord(address delegator, address delegatee, ResourceId systemId) internal { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + _keyTuple[2] = ResourceId.unwrap(systemId); + + StoreSwitch.deleteRecord(_tableId, _keyTuple); + } + + /** + * @notice Delete all data for given keys. + */ + function _deleteRecord(address delegator, address delegatee, ResourceId systemId) internal { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + _keyTuple[2] = ResourceId.unwrap(systemId); + + 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 availableCalls) internal pure returns (bytes memory) { + return abi.encodePacked(availableCalls); + } + + /** + * @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 dyanmic (variable length) data, encoded into a sequence of bytes. + */ + function encode(uint256 availableCalls) internal pure returns (bytes memory, PackedCounter, bytes memory) { + bytes memory _staticData = encodeStatic(availableCalls); + + PackedCounter _encodedLengths; + bytes memory _dynamicData; + + return (_staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Encode keys as a bytes32 array using this table's field layout. + */ + function encodeKeyTuple( + address delegator, + address delegatee, + ResourceId systemId + ) internal pure returns (bytes32[] memory) { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + _keyTuple[2] = ResourceId.unwrap(systemId); + + return _keyTuple; + } +} diff --git a/packages/world-modules/test/StandardDelegationsModule.t.sol b/packages/world-modules/test/StandardDelegationsModule.t.sol index 4734a7751d..fd7b4a65de 100644 --- a/packages/world-modules/test/StandardDelegationsModule.t.sol +++ b/packages/world-modules/test/StandardDelegationsModule.t.sol @@ -18,10 +18,11 @@ import { Systems } from "@latticexyz/world/src/codegen/tables/Systems.sol"; import { StandardDelegationsModule } from "../src/modules/std-delegations/StandardDelegationsModule.sol"; import { CallboundDelegationControl } from "../src/modules/std-delegations/CallboundDelegationControl.sol"; +import { SystemboundDelegationControl } from "../src/modules/std-delegations/SystemboundDelegationControl.sol"; import { TimeboundDelegationControl } from "../src/modules/std-delegations/TimeboundDelegationControl.sol"; -import { CALLBOUND_DELEGATION, TIMEBOUND_DELEGATION } from "../src/modules/std-delegations/StandardDelegationsModule.sol"; +import { CALLBOUND_DELEGATION, SYSTEMBOUND_DELEGATION, TIMEBOUND_DELEGATION } from "../src/modules/std-delegations/StandardDelegationsModule.sol"; -import { WorldTestSystem } from "@latticexyz/world/test/World.t.sol"; +import { WorldTestSystem, WorldTestSystemReturn } from "@latticexyz/world/test/World.t.sol"; contract StandardDelegationsModuleTest is Test, GasReporter { IBaseWorld private world; @@ -70,6 +71,41 @@ contract StandardDelegationsModuleTest is Test, GasReporter { world.callFrom(delegator, systemId, abi.encodeCall(WorldTestSystem.msgSender, ())); } + function testCallFromSystemDelegation() public { + // Register the systembound delegation for one call to the system's msgSender function + vm.prank(delegator); + startGasReport("register a systembound delegation"); + world.registerDelegation( + delegatee, + SYSTEMBOUND_DELEGATION, + abi.encodeCall(SystemboundDelegationControl.initDelegation, (delegatee, systemId, 2)) + ); + endGasReport(); + + // Call a system from the delegatee on behalf of the delegator + vm.prank(delegatee); + startGasReport("call a system via a systembound delegation"); + bytes memory returnData = world.callFrom(delegator, systemId, abi.encodeCall(WorldTestSystem.msgSender, ())); + endGasReport(); + address returnedAddress = abi.decode(returnData, (address)); + + // Expect the system to have received the delegator's address + assertEq(returnedAddress, delegator); + + // Call a different function from the delegatee on behalf of the delegator + vm.prank(delegatee); + returnData = world.callFrom(delegator, systemId, abi.encodeCall(WorldTestSystem.echo, (bytes32(0)))); + WorldTestSystemReturn memory returnedStruct = abi.decode(returnData, (WorldTestSystemReturn)); + + // Expect the system to have received the delegator's address + assertEq(returnedStruct.sender, delegator); + + // Expect the delegation to have been used up + vm.prank(delegatee); + vm.expectRevert(abi.encodeWithSelector(IWorldErrors.World_DelegationNotFound.selector, delegator, delegatee)); + world.callFrom(delegator, systemId, abi.encodeCall(WorldTestSystem.msgSender, ())); + } + function testCallFromTimeboundDelegation() public { uint256 maxTimestamp = 4242;