From 1ca35e9a1630a51dfd1e082c26399f76f2cd06ed Mon Sep 17 00:00:00 2001 From: alvarius Date: Fri, 1 Sep 2023 15:20:37 +0200 Subject: [PATCH] feat(world): add callFrom entry point (#1364) --- .changeset/nervous-walls-knock.md | 50 +++++ .../types/ethers-contracts/IWorld.ts | 95 +++++++++ .../factories/IWorld__factory.ts | 68 +++++++ .../types/ethers-contracts/IWorld.ts | 95 +++++++++ .../factories/IWorld__factory.ts | 68 +++++++ .../CallboundDelegationControl.abi.json | 123 +++++++++++ .../CallboundDelegations.abi.json | 1 + .../abi/CoreSystem.sol/CoreSystem.abi.json | 39 ++++ .../DelegationInstance.abi.json | 1 + .../DelegationControl.abi.json | 31 +++ .../abi/Delegations.sol/Delegations.abi.json | 1 + .../abi/IBaseWorld.sol/IBaseWorld.abi.json | 68 +++++++ .../IDelegationControl.abi.json | 31 +++ .../IWorldErrors.sol/IWorldErrors.abi.json | 16 ++ .../abi/IWorldKernel.sol/IWorldCall.abi.json | 29 +++ .../IWorldKernel.sol/IWorldKernel.abi.json | 45 +++++ .../IWorldRegistrationSystem.abi.json | 23 +++ .../StandardDelegationsModule.abi.json | 55 +++++ .../StoreRegistrationSystem.abi.json | 16 ++ .../TimeboundDelegationControl.abi.json | 113 +++++++++++ .../TimeboundDelegations.abi.json | 1 + packages/world/abi/World.sol/World.abi.json | 45 +++++ .../WorldRegistrationSystem.abi.json | 39 ++++ packages/world/gas-report.json | 102 +++++++--- packages/world/mud.config.ts | 31 +++ packages/world/src/Delegation.sol | 59 ++++++ packages/world/src/DelegationControl.sol | 7 + packages/world/src/Tables.sol | 3 + packages/world/src/World.sol | 30 +++ packages/world/src/constants.sol | 1 + .../src/interfaces/IDelegationControl.sol | 6 + .../world/src/interfaces/IWorldErrors.sol | 1 + .../world/src/interfaces/IWorldKernel.sol | 10 + .../interfaces/IWorldRegistrationSystem.sol | 6 + .../world/src/modules/core/CoreModule.sol | 3 +- .../WorldRegistrationSystem.sol | 25 ++- .../CallboundDelegationControl.sol | 68 +++++++ .../StandardDelegationsModule.sol | 39 ++++ .../TimeboundDelegationControl.sol | 26 +++ .../src/modules/std-delegations/constants.sol | 12 ++ .../tables/CallboundDelegations.sol | 191 ++++++++++++++++++ .../tables/TimeboundDelegations.sol | 133 ++++++++++++ packages/world/src/tables/Delegations.sol | 137 +++++++++++++ .../test/StandardDelegationsModule.t.sol | 114 +++++++++++ packages/world/test/World.t.sol | 62 +++++- 45 files changed, 2083 insertions(+), 36 deletions(-) create mode 100644 .changeset/nervous-walls-knock.md create mode 100644 packages/world/abi/CallboundDelegationControl.sol/CallboundDelegationControl.abi.json create mode 100644 packages/world/abi/CallboundDelegations.sol/CallboundDelegations.abi.json create mode 100644 packages/world/abi/Delegation.sol/DelegationInstance.abi.json create mode 100644 packages/world/abi/DelegationControl.sol/DelegationControl.abi.json create mode 100644 packages/world/abi/Delegations.sol/Delegations.abi.json create mode 100644 packages/world/abi/IDelegationControl.sol/IDelegationControl.abi.json create mode 100644 packages/world/abi/StandardDelegationsModule.sol/StandardDelegationsModule.abi.json create mode 100644 packages/world/abi/TimeboundDelegationControl.sol/TimeboundDelegationControl.abi.json create mode 100644 packages/world/abi/TimeboundDelegations.sol/TimeboundDelegations.abi.json create mode 100644 packages/world/src/Delegation.sol create mode 100644 packages/world/src/DelegationControl.sol create mode 100644 packages/world/src/interfaces/IDelegationControl.sol create mode 100644 packages/world/src/modules/std-delegations/CallboundDelegationControl.sol create mode 100644 packages/world/src/modules/std-delegations/StandardDelegationsModule.sol create mode 100644 packages/world/src/modules/std-delegations/TimeboundDelegationControl.sol create mode 100644 packages/world/src/modules/std-delegations/constants.sol create mode 100644 packages/world/src/modules/std-delegations/tables/CallboundDelegations.sol create mode 100644 packages/world/src/modules/std-delegations/tables/TimeboundDelegations.sol create mode 100644 packages/world/src/tables/Delegations.sol create mode 100644 packages/world/test/StandardDelegationsModule.t.sol diff --git a/.changeset/nervous-walls-knock.md b/.changeset/nervous-walls-knock.md new file mode 100644 index 0000000000..22d52c545f --- /dev/null +++ b/.changeset/nervous-walls-knock.md @@ -0,0 +1,50 @@ +--- +"@latticexyz/world": minor +--- + +The `World` has a new `callFrom` entry point which allows systems to be called on behalf of other addresses if those addresses have registered a delegation. +If there is a delegation, the call is forwarded to the system with `delegator` as `msgSender`. + +```solidity +interface IBaseWorld { + function callFrom( + address delegator, + bytes32 resourceSelector, + bytes memory funcSelectorAndArgs + ) external payable virtual returns (bytes memory); +} +``` + +A delegation can be registered via the `World`'s `registerDelegation` function. +If `delegatee` is `address(0)`, the delegation is considered to be a "fallback" delegation and is used in `callFrom` if there is no delegation is found for the specific caller. +Otherwise the delegation is registered for the specific `delegatee`. + +```solidity +interface IBaseWorld { + function registerDelegation( + address delegatee, + bytes32 delegationControl, + bytes memory initFuncSelectorAndArgs + ) external; +} +``` + +The `delegationControl` refers to the resource selector of a `DelegationControl` system that must have been registered beforehand. +As part of registering the delegation, the `DelegationControl` system is called with the provided `initFuncSelectorAndArgs`. +This can be used to initialize data in the given `DelegationControl` system. + +The `DelegationControl` system must implement the `IDelegationControl` interface: + +```solidity +interface IDelegationControl { + function verify(address delegator, bytes32 systemId, bytes calldata funcSelectorAndArgs) external returns (bool); +} +``` + +When `callFrom` is called, the `World` checks if a delegation is registered for the given caller, and if so calls the delegation control's `verify` function with the same same arguments as `callFrom`. +If the call to `verify` is successful and returns `true`, the delegation is valid and the call is forwarded to the system with `delegator` as `msgSender`. + +Note: if `UNLIMITED_DELEGATION` (from `@latticexyz/world/src/constants.sol`) is passed as `delegationControl`, the external call to the delegation control contract is skipped and the delegation is considered valid. + +For examples of `DelegationControl` systems, check out the `CallboundDelegationControl` or `TimeboundDelegationControl` systems in the `std-delegations` module. +See `StandardDelegations.t.sol` for usage examples. diff --git a/e2e/packages/contracts/types/ethers-contracts/IWorld.ts b/e2e/packages/contracts/types/ethers-contracts/IWorld.ts index 34adbd46c8..acf69c605e 100644 --- a/e2e/packages/contracts/types/ethers-contracts/IWorld.ts +++ b/e2e/packages/contracts/types/ethers-contracts/IWorld.ts @@ -31,6 +31,7 @@ import type { export interface IWorldInterface extends utils.Interface { functions: { "call(bytes32,bytes)": FunctionFragment; + "callFrom(address,bytes32,bytes)": FunctionFragment; "deleteRecord(bytes32,bytes32[],bytes32)": FunctionFragment; "emitEphemeralRecord(bytes32,bytes32[],bytes,bytes32)": FunctionFragment; "getField(bytes32,bytes32[],uint8,bytes32)": FunctionFragment; @@ -47,6 +48,7 @@ export interface IWorldInterface extends utils.Interface { "push(uint32)": FunctionFragment; "pushRange(uint32,uint32)": FunctionFragment; "pushToField(bytes32,bytes32[],uint8,bytes,bytes32)": FunctionFragment; + "registerDelegation(address,bytes32,bytes)": FunctionFragment; "registerFunctionSelector(bytes32,string,string)": FunctionFragment; "registerNamespace(bytes16)": FunctionFragment; "registerRootFunctionSelector(bytes32,bytes4,bytes4)": FunctionFragment; @@ -66,6 +68,7 @@ export interface IWorldInterface extends utils.Interface { getFunction( nameOrSignatureOrTopic: | "call" + | "callFrom" | "deleteRecord" | "emitEphemeralRecord" | "getField" @@ -82,6 +85,7 @@ export interface IWorldInterface extends utils.Interface { | "push" | "pushRange" | "pushToField" + | "registerDelegation" | "registerFunctionSelector" | "registerNamespace" | "registerRootFunctionSelector" @@ -102,6 +106,14 @@ export interface IWorldInterface extends utils.Interface { functionFragment: "call", values: [PromiseOrValue, PromiseOrValue] ): string; + encodeFunctionData( + functionFragment: "callFrom", + values: [ + PromiseOrValue, + PromiseOrValue, + PromiseOrValue + ] + ): string; encodeFunctionData( functionFragment: "deleteRecord", values: [ @@ -205,6 +217,14 @@ export interface IWorldInterface extends utils.Interface { PromiseOrValue ] ): string; + encodeFunctionData( + functionFragment: "registerDelegation", + values: [ + PromiseOrValue, + PromiseOrValue, + PromiseOrValue + ] + ): string; encodeFunctionData( functionFragment: "registerFunctionSelector", values: [ @@ -299,6 +319,7 @@ export interface IWorldInterface extends utils.Interface { ): string; decodeFunctionResult(functionFragment: "call", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "callFrom", data: BytesLike): Result; decodeFunctionResult( functionFragment: "deleteRecord", data: BytesLike @@ -348,6 +369,10 @@ export interface IWorldInterface extends utils.Interface { functionFragment: "pushToField", data: BytesLike ): Result; + decodeFunctionResult( + functionFragment: "registerDelegation", + data: BytesLike + ): Result; decodeFunctionResult( functionFragment: "registerFunctionSelector", data: BytesLike @@ -496,6 +521,13 @@ export interface IWorld extends BaseContract { overrides?: PayableOverrides & { from?: PromiseOrValue } ): Promise; + callFrom( + delegator: PromiseOrValue, + resourceSelector: PromiseOrValue, + funcSelectorAndArgs: PromiseOrValue, + overrides?: PayableOverrides & { from?: PromiseOrValue } + ): Promise; + deleteRecord( table: PromiseOrValue, key: PromiseOrValue[], @@ -605,6 +637,13 @@ export interface IWorld extends BaseContract { overrides?: Overrides & { from?: PromiseOrValue } ): Promise; + registerDelegation( + delegatee: PromiseOrValue, + delegationControlId: PromiseOrValue, + initFuncSelectorAndArgs: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + registerFunctionSelector( resourceSelector: PromiseOrValue, systemFunctionName: PromiseOrValue, @@ -708,6 +747,13 @@ export interface IWorld extends BaseContract { overrides?: PayableOverrides & { from?: PromiseOrValue } ): Promise; + callFrom( + delegator: PromiseOrValue, + resourceSelector: PromiseOrValue, + funcSelectorAndArgs: PromiseOrValue, + overrides?: PayableOverrides & { from?: PromiseOrValue } + ): Promise; + deleteRecord( table: PromiseOrValue, key: PromiseOrValue[], @@ -817,6 +863,13 @@ export interface IWorld extends BaseContract { overrides?: Overrides & { from?: PromiseOrValue } ): Promise; + registerDelegation( + delegatee: PromiseOrValue, + delegationControlId: PromiseOrValue, + initFuncSelectorAndArgs: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + registerFunctionSelector( resourceSelector: PromiseOrValue, systemFunctionName: PromiseOrValue, @@ -920,6 +973,13 @@ export interface IWorld extends BaseContract { overrides?: CallOverrides ): Promise; + callFrom( + delegator: PromiseOrValue, + resourceSelector: PromiseOrValue, + funcSelectorAndArgs: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + deleteRecord( table: PromiseOrValue, key: PromiseOrValue[], @@ -1027,6 +1087,13 @@ export interface IWorld extends BaseContract { overrides?: CallOverrides ): Promise; + registerDelegation( + delegatee: PromiseOrValue, + delegationControlId: PromiseOrValue, + initFuncSelectorAndArgs: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + registerFunctionSelector( resourceSelector: PromiseOrValue, systemFunctionName: PromiseOrValue, @@ -1177,6 +1244,13 @@ export interface IWorld extends BaseContract { overrides?: PayableOverrides & { from?: PromiseOrValue } ): Promise; + callFrom( + delegator: PromiseOrValue, + resourceSelector: PromiseOrValue, + funcSelectorAndArgs: PromiseOrValue, + overrides?: PayableOverrides & { from?: PromiseOrValue } + ): Promise; + deleteRecord( table: PromiseOrValue, key: PromiseOrValue[], @@ -1286,6 +1360,13 @@ export interface IWorld extends BaseContract { overrides?: Overrides & { from?: PromiseOrValue } ): Promise; + registerDelegation( + delegatee: PromiseOrValue, + delegationControlId: PromiseOrValue, + initFuncSelectorAndArgs: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + registerFunctionSelector( resourceSelector: PromiseOrValue, systemFunctionName: PromiseOrValue, @@ -1390,6 +1471,13 @@ export interface IWorld extends BaseContract { overrides?: PayableOverrides & { from?: PromiseOrValue } ): Promise; + callFrom( + delegator: PromiseOrValue, + resourceSelector: PromiseOrValue, + funcSelectorAndArgs: PromiseOrValue, + overrides?: PayableOverrides & { from?: PromiseOrValue } + ): Promise; + deleteRecord( table: PromiseOrValue, key: PromiseOrValue[], @@ -1499,6 +1587,13 @@ export interface IWorld extends BaseContract { overrides?: Overrides & { from?: PromiseOrValue } ): Promise; + registerDelegation( + delegatee: PromiseOrValue, + delegationControlId: PromiseOrValue, + initFuncSelectorAndArgs: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + registerFunctionSelector( resourceSelector: PromiseOrValue, systemFunctionName: PromiseOrValue, diff --git a/e2e/packages/contracts/types/ethers-contracts/factories/IWorld__factory.ts b/e2e/packages/contracts/types/ethers-contracts/factories/IWorld__factory.ts index ab6dab8147..3a6afdb60e 100644 --- a/e2e/packages/contracts/types/ethers-contracts/factories/IWorld__factory.ts +++ b/e2e/packages/contracts/types/ethers-contracts/factories/IWorld__factory.ts @@ -23,6 +23,22 @@ const _abi = [ name: "AccessDenied", type: "error", }, + { + inputs: [ + { + internalType: "address", + name: "delegator", + type: "address", + }, + { + internalType: "address", + name: "delegatee", + type: "address", + }, + ], + name: "DelegationNotFound", + type: "error", + }, { inputs: [ { @@ -379,6 +395,35 @@ const _abi = [ stateMutability: "payable", type: "function", }, + { + inputs: [ + { + internalType: "address", + name: "delegator", + type: "address", + }, + { + internalType: "bytes32", + name: "resourceSelector", + type: "bytes32", + }, + { + internalType: "bytes", + name: "funcSelectorAndArgs", + type: "bytes", + }, + ], + name: "callFrom", + outputs: [ + { + internalType: "bytes", + name: "", + type: "bytes", + }, + ], + stateMutability: "payable", + type: "function", + }, { inputs: [ { @@ -767,6 +812,29 @@ const _abi = [ stateMutability: "nonpayable", type: "function", }, + { + inputs: [ + { + internalType: "address", + name: "delegatee", + type: "address", + }, + { + internalType: "bytes32", + name: "delegationControlId", + type: "bytes32", + }, + { + internalType: "bytes", + name: "initFuncSelectorAndArgs", + type: "bytes", + }, + ], + name: "registerDelegation", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, { inputs: [ { diff --git a/examples/minimal/packages/contracts/types/ethers-contracts/IWorld.ts b/examples/minimal/packages/contracts/types/ethers-contracts/IWorld.ts index bca3079737..3a8a6e6801 100644 --- a/examples/minimal/packages/contracts/types/ethers-contracts/IWorld.ts +++ b/examples/minimal/packages/contracts/types/ethers-contracts/IWorld.ts @@ -39,6 +39,7 @@ export type StringStructStructOutput = [string] & { value: string }; export interface IWorldInterface extends utils.Interface { functions: { "call(bytes32,bytes)": FunctionFragment; + "callFrom(address,bytes32,bytes)": FunctionFragment; "deleteRecord(bytes32,bytes32[],bytes32)": FunctionFragment; "dynamicArrayBytesStruct((bytes)[])": FunctionFragment; "dynamicArrayStringStruct((string)[])": FunctionFragment; @@ -56,6 +57,7 @@ export interface IWorldInterface extends utils.Interface { "pickUp(uint32,uint32)": FunctionFragment; "popFromField(bytes32,bytes32[],uint8,uint256,bytes32)": FunctionFragment; "pushToField(bytes32,bytes32[],uint8,bytes,bytes32)": FunctionFragment; + "registerDelegation(address,bytes32,bytes)": FunctionFragment; "registerFunctionSelector(bytes32,string,string)": FunctionFragment; "registerNamespace(bytes16)": FunctionFragment; "registerRootFunctionSelector(bytes32,bytes4,bytes4)": FunctionFragment; @@ -77,6 +79,7 @@ export interface IWorldInterface extends utils.Interface { getFunction( nameOrSignatureOrTopic: | "call" + | "callFrom" | "deleteRecord" | "dynamicArrayBytesStruct" | "dynamicArrayStringStruct" @@ -94,6 +97,7 @@ export interface IWorldInterface extends utils.Interface { | "pickUp" | "popFromField" | "pushToField" + | "registerDelegation" | "registerFunctionSelector" | "registerNamespace" | "registerRootFunctionSelector" @@ -116,6 +120,14 @@ export interface IWorldInterface extends utils.Interface { functionFragment: "call", values: [PromiseOrValue, PromiseOrValue] ): string; + encodeFunctionData( + functionFragment: "callFrom", + values: [ + PromiseOrValue, + PromiseOrValue, + PromiseOrValue + ] + ): string; encodeFunctionData( functionFragment: "deleteRecord", values: [ @@ -223,6 +235,14 @@ export interface IWorldInterface extends utils.Interface { PromiseOrValue ] ): string; + encodeFunctionData( + functionFragment: "registerDelegation", + values: [ + PromiseOrValue, + PromiseOrValue, + PromiseOrValue + ] + ): string; encodeFunctionData( functionFragment: "registerFunctionSelector", values: [ @@ -325,6 +345,7 @@ export interface IWorldInterface extends utils.Interface { ): string; decodeFunctionResult(functionFragment: "call", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "callFrom", data: BytesLike): Result; decodeFunctionResult( functionFragment: "deleteRecord", data: BytesLike @@ -381,6 +402,10 @@ export interface IWorldInterface extends utils.Interface { functionFragment: "pushToField", data: BytesLike ): Result; + decodeFunctionResult( + functionFragment: "registerDelegation", + data: BytesLike + ): Result; decodeFunctionResult( functionFragment: "registerFunctionSelector", data: BytesLike @@ -540,6 +565,13 @@ export interface IWorld extends BaseContract { overrides?: PayableOverrides & { from?: PromiseOrValue } ): Promise; + callFrom( + delegator: PromiseOrValue, + resourceSelector: PromiseOrValue, + funcSelectorAndArgs: PromiseOrValue, + overrides?: PayableOverrides & { from?: PromiseOrValue } + ): Promise; + deleteRecord( table: PromiseOrValue, key: PromiseOrValue[], @@ -654,6 +686,13 @@ export interface IWorld extends BaseContract { overrides?: Overrides & { from?: PromiseOrValue } ): Promise; + registerDelegation( + delegatee: PromiseOrValue, + delegationControlId: PromiseOrValue, + initFuncSelectorAndArgs: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + registerFunctionSelector( resourceSelector: PromiseOrValue, systemFunctionName: PromiseOrValue, @@ -766,6 +805,13 @@ export interface IWorld extends BaseContract { overrides?: PayableOverrides & { from?: PromiseOrValue } ): Promise; + callFrom( + delegator: PromiseOrValue, + resourceSelector: PromiseOrValue, + funcSelectorAndArgs: PromiseOrValue, + overrides?: PayableOverrides & { from?: PromiseOrValue } + ): Promise; + deleteRecord( table: PromiseOrValue, key: PromiseOrValue[], @@ -880,6 +926,13 @@ export interface IWorld extends BaseContract { overrides?: Overrides & { from?: PromiseOrValue } ): Promise; + registerDelegation( + delegatee: PromiseOrValue, + delegationControlId: PromiseOrValue, + initFuncSelectorAndArgs: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + registerFunctionSelector( resourceSelector: PromiseOrValue, systemFunctionName: PromiseOrValue, @@ -992,6 +1045,13 @@ export interface IWorld extends BaseContract { overrides?: CallOverrides ): Promise; + callFrom( + delegator: PromiseOrValue, + resourceSelector: PromiseOrValue, + funcSelectorAndArgs: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + deleteRecord( table: PromiseOrValue, key: PromiseOrValue[], @@ -1104,6 +1164,13 @@ export interface IWorld extends BaseContract { overrides?: CallOverrides ): Promise; + registerDelegation( + delegatee: PromiseOrValue, + delegationControlId: PromiseOrValue, + initFuncSelectorAndArgs: PromiseOrValue, + overrides?: CallOverrides + ): Promise; + registerFunctionSelector( resourceSelector: PromiseOrValue, systemFunctionName: PromiseOrValue, @@ -1261,6 +1328,13 @@ export interface IWorld extends BaseContract { overrides?: PayableOverrides & { from?: PromiseOrValue } ): Promise; + callFrom( + delegator: PromiseOrValue, + resourceSelector: PromiseOrValue, + funcSelectorAndArgs: PromiseOrValue, + overrides?: PayableOverrides & { from?: PromiseOrValue } + ): Promise; + deleteRecord( table: PromiseOrValue, key: PromiseOrValue[], @@ -1375,6 +1449,13 @@ export interface IWorld extends BaseContract { overrides?: Overrides & { from?: PromiseOrValue } ): Promise; + registerDelegation( + delegatee: PromiseOrValue, + delegationControlId: PromiseOrValue, + initFuncSelectorAndArgs: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + registerFunctionSelector( resourceSelector: PromiseOrValue, systemFunctionName: PromiseOrValue, @@ -1488,6 +1569,13 @@ export interface IWorld extends BaseContract { overrides?: PayableOverrides & { from?: PromiseOrValue } ): Promise; + callFrom( + delegator: PromiseOrValue, + resourceSelector: PromiseOrValue, + funcSelectorAndArgs: PromiseOrValue, + overrides?: PayableOverrides & { from?: PromiseOrValue } + ): Promise; + deleteRecord( table: PromiseOrValue, key: PromiseOrValue[], @@ -1602,6 +1690,13 @@ export interface IWorld extends BaseContract { overrides?: Overrides & { from?: PromiseOrValue } ): Promise; + registerDelegation( + delegatee: PromiseOrValue, + delegationControlId: PromiseOrValue, + initFuncSelectorAndArgs: PromiseOrValue, + overrides?: Overrides & { from?: PromiseOrValue } + ): Promise; + registerFunctionSelector( resourceSelector: PromiseOrValue, systemFunctionName: PromiseOrValue, diff --git a/examples/minimal/packages/contracts/types/ethers-contracts/factories/IWorld__factory.ts b/examples/minimal/packages/contracts/types/ethers-contracts/factories/IWorld__factory.ts index 64672701f8..0dd9348d78 100644 --- a/examples/minimal/packages/contracts/types/ethers-contracts/factories/IWorld__factory.ts +++ b/examples/minimal/packages/contracts/types/ethers-contracts/factories/IWorld__factory.ts @@ -23,6 +23,22 @@ const _abi = [ name: "AccessDenied", type: "error", }, + { + inputs: [ + { + internalType: "address", + name: "delegator", + type: "address", + }, + { + internalType: "address", + name: "delegatee", + type: "address", + }, + ], + name: "DelegationNotFound", + type: "error", + }, { inputs: [ { @@ -341,6 +357,35 @@ const _abi = [ stateMutability: "payable", type: "function", }, + { + inputs: [ + { + internalType: "address", + name: "delegator", + type: "address", + }, + { + internalType: "bytes32", + name: "resourceSelector", + type: "bytes32", + }, + { + internalType: "bytes", + name: "funcSelectorAndArgs", + type: "bytes", + }, + ], + name: "callFrom", + outputs: [ + { + internalType: "bytes", + name: "", + type: "bytes", + }, + ], + stateMutability: "payable", + type: "function", + }, { inputs: [ { @@ -762,6 +807,29 @@ const _abi = [ stateMutability: "nonpayable", type: "function", }, + { + inputs: [ + { + internalType: "address", + name: "delegatee", + type: "address", + }, + { + internalType: "bytes32", + name: "delegationControlId", + type: "bytes32", + }, + { + internalType: "bytes", + name: "initFuncSelectorAndArgs", + type: "bytes", + }, + ], + name: "registerDelegation", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, { inputs: [ { diff --git a/packages/world/abi/CallboundDelegationControl.sol/CallboundDelegationControl.abi.json b/packages/world/abi/CallboundDelegationControl.sol/CallboundDelegationControl.abi.json new file mode 100644 index 0000000000..27a04c7a72 --- /dev/null +++ b/packages/world/abi/CallboundDelegationControl.sol/CallboundDelegationControl.abi.json @@ -0,0 +1,123 @@ +[ + { + "inputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "PackedCounter_InvalidLength", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "SchemaLib_InvalidLength", + "type": "error" + }, + { + "inputs": [], + "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": [ + { + "internalType": "uint256", + "name": "expected", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "received", + "type": "uint256" + } + ], + "name": "StoreCore_InvalidDataLength", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegatee", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "resourceSelector", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "funcSelectorAndArgs", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "numCalls", + "type": "uint256" + } + ], + "name": "initDelegation", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "resourceSelector", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "funcSelectorAndArgs", + "type": "bytes" + } + ], + "name": "verify", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/packages/world/abi/CallboundDelegations.sol/CallboundDelegations.abi.json b/packages/world/abi/CallboundDelegations.sol/CallboundDelegations.abi.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/packages/world/abi/CallboundDelegations.sol/CallboundDelegations.abi.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json b/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json index 8026424550..decf383ad6 100644 --- a/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json +++ b/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json @@ -15,6 +15,22 @@ "name": "AccessDenied", "type": "error" }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + }, + { + "internalType": "address", + "name": "delegatee", + "type": "address" + } + ], + "name": "DelegationNotFound", + "type": "error" + }, { "inputs": [ { @@ -298,6 +314,29 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegatee", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "delegationControlId", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "initFuncSelectorAndArgs", + "type": "bytes" + } + ], + "name": "registerDelegation", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/packages/world/abi/Delegation.sol/DelegationInstance.abi.json b/packages/world/abi/Delegation.sol/DelegationInstance.abi.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/packages/world/abi/Delegation.sol/DelegationInstance.abi.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/packages/world/abi/DelegationControl.sol/DelegationControl.abi.json b/packages/world/abi/DelegationControl.sol/DelegationControl.abi.json new file mode 100644 index 0000000000..5b5a35e503 --- /dev/null +++ b/packages/world/abi/DelegationControl.sol/DelegationControl.abi.json @@ -0,0 +1,31 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "systemId", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "funcSelectorAndArgs", + "type": "bytes" + } + ], + "name": "verify", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/packages/world/abi/Delegations.sol/Delegations.abi.json b/packages/world/abi/Delegations.sol/Delegations.abi.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/packages/world/abi/Delegations.sol/Delegations.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 93f7460458..0396d362ca 100644 --- a/packages/world/abi/IBaseWorld.sol/IBaseWorld.abi.json +++ b/packages/world/abi/IBaseWorld.sol/IBaseWorld.abi.json @@ -15,6 +15,22 @@ "name": "AccessDenied", "type": "error" }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + }, + { + "internalType": "address", + "name": "delegatee", + "type": "address" + } + ], + "name": "DelegationNotFound", + "type": "error" + }, { "inputs": [ { @@ -328,6 +344,35 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "resourceSelector", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "funcSelectorAndArgs", + "type": "bytes" + } + ], + "name": "callFrom", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "payable", + "type": "function" + }, { "inputs": [ { @@ -678,6 +723,29 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegatee", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "delegationControlId", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "initFuncSelectorAndArgs", + "type": "bytes" + } + ], + "name": "registerDelegation", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/packages/world/abi/IDelegationControl.sol/IDelegationControl.abi.json b/packages/world/abi/IDelegationControl.sol/IDelegationControl.abi.json new file mode 100644 index 0000000000..5b5a35e503 --- /dev/null +++ b/packages/world/abi/IDelegationControl.sol/IDelegationControl.abi.json @@ -0,0 +1,31 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "systemId", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "funcSelectorAndArgs", + "type": "bytes" + } + ], + "name": "verify", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/packages/world/abi/IWorldErrors.sol/IWorldErrors.abi.json b/packages/world/abi/IWorldErrors.sol/IWorldErrors.abi.json index be9080afeb..e1b0321916 100644 --- a/packages/world/abi/IWorldErrors.sol/IWorldErrors.abi.json +++ b/packages/world/abi/IWorldErrors.sol/IWorldErrors.abi.json @@ -15,6 +15,22 @@ "name": "AccessDenied", "type": "error" }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + }, + { + "internalType": "address", + "name": "delegatee", + "type": "address" + } + ], + "name": "DelegationNotFound", + "type": "error" + }, { "inputs": [ { diff --git a/packages/world/abi/IWorldKernel.sol/IWorldCall.abi.json b/packages/world/abi/IWorldKernel.sol/IWorldCall.abi.json index 9cc8e76434..797e92b604 100644 --- a/packages/world/abi/IWorldKernel.sol/IWorldCall.abi.json +++ b/packages/world/abi/IWorldKernel.sol/IWorldCall.abi.json @@ -22,5 +22,34 @@ ], "stateMutability": "payable", "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "resourceSelector", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "funcSelectorAndArgs", + "type": "bytes" + } + ], + "name": "callFrom", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "payable", + "type": "function" } ] \ No newline at end of file diff --git a/packages/world/abi/IWorldKernel.sol/IWorldKernel.abi.json b/packages/world/abi/IWorldKernel.sol/IWorldKernel.abi.json index c8ebaa6cd7..ca66fdee6c 100644 --- a/packages/world/abi/IWorldKernel.sol/IWorldKernel.abi.json +++ b/packages/world/abi/IWorldKernel.sol/IWorldKernel.abi.json @@ -15,6 +15,22 @@ "name": "AccessDenied", "type": "error" }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + }, + { + "internalType": "address", + "name": "delegatee", + "type": "address" + } + ], + "name": "DelegationNotFound", + "type": "error" + }, { "inputs": [ { @@ -122,6 +138,35 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "resourceSelector", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "funcSelectorAndArgs", + "type": "bytes" + } + ], + "name": "callFrom", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "payable", + "type": "function" + }, { "inputs": [ { diff --git a/packages/world/abi/IWorldRegistrationSystem.sol/IWorldRegistrationSystem.abi.json b/packages/world/abi/IWorldRegistrationSystem.sol/IWorldRegistrationSystem.abi.json index 87fb69f260..27592788d5 100644 --- a/packages/world/abi/IWorldRegistrationSystem.sol/IWorldRegistrationSystem.abi.json +++ b/packages/world/abi/IWorldRegistrationSystem.sol/IWorldRegistrationSystem.abi.json @@ -1,4 +1,27 @@ [ + { + "inputs": [ + { + "internalType": "address", + "name": "delegatee", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "delegationControlId", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "initFuncSelectorAndArgs", + "type": "bytes" + } + ], + "name": "registerDelegation", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/packages/world/abi/StandardDelegationsModule.sol/StandardDelegationsModule.abi.json b/packages/world/abi/StandardDelegationsModule.sol/StandardDelegationsModule.abi.json new file mode 100644 index 0000000000..646de1964b --- /dev/null +++ b/packages/world/abi/StandardDelegationsModule.sol/StandardDelegationsModule.abi.json @@ -0,0 +1,55 @@ +[ + { + "inputs": [ + { + "internalType": "string", + "name": "resourceSelector", + "type": "string" + } + ], + "name": "RequiredModuleNotFound", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "SchemaLib_InvalidLength", + "type": "error" + }, + { + "inputs": [], + "name": "SchemaLib_StaticTypeAfterDynamicType", + "type": "error" + }, + { + "inputs": [], + "name": "getName", + "outputs": [ + { + "internalType": "bytes16", + "name": "", + "type": "bytes16" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "install", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ 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 1132390e96..8e3dd8de1f 100644 --- a/packages/world/abi/StoreRegistrationSystem.sol/StoreRegistrationSystem.abi.json +++ b/packages/world/abi/StoreRegistrationSystem.sol/StoreRegistrationSystem.abi.json @@ -15,6 +15,22 @@ "name": "AccessDenied", "type": "error" }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + }, + { + "internalType": "address", + "name": "delegatee", + "type": "address" + } + ], + "name": "DelegationNotFound", + "type": "error" + }, { "inputs": [ { diff --git a/packages/world/abi/TimeboundDelegationControl.sol/TimeboundDelegationControl.abi.json b/packages/world/abi/TimeboundDelegationControl.sol/TimeboundDelegationControl.abi.json new file mode 100644 index 0000000000..00b7f632e4 --- /dev/null +++ b/packages/world/abi/TimeboundDelegationControl.sol/TimeboundDelegationControl.abi.json @@ -0,0 +1,113 @@ +[ + { + "inputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "PackedCounter_InvalidLength", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "SchemaLib_InvalidLength", + "type": "error" + }, + { + "inputs": [], + "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": [ + { + "internalType": "uint256", + "name": "expected", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "received", + "type": "uint256" + } + ], + "name": "StoreCore_InvalidDataLength", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegatee", + "type": "address" + }, + { + "internalType": "uint256", + "name": "maxTimestamp", + "type": "uint256" + } + ], + "name": "initDelegation", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "verify", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/packages/world/abi/TimeboundDelegations.sol/TimeboundDelegations.abi.json b/packages/world/abi/TimeboundDelegations.sol/TimeboundDelegations.abi.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/packages/world/abi/TimeboundDelegations.sol/TimeboundDelegations.abi.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/packages/world/abi/World.sol/World.abi.json b/packages/world/abi/World.sol/World.abi.json index 852942f8fc..42c837564a 100644 --- a/packages/world/abi/World.sol/World.abi.json +++ b/packages/world/abi/World.sol/World.abi.json @@ -20,6 +20,22 @@ "name": "AccessDenied", "type": "error" }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + }, + { + "internalType": "address", + "name": "delegatee", + "type": "address" + } + ], + "name": "DelegationNotFound", + "type": "error" + }, { "inputs": [ { @@ -355,6 +371,35 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "resourceSelector", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "funcSelectorAndArgs", + "type": "bytes" + } + ], + "name": "callFrom", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "payable", + "type": "function" + }, { "inputs": [ { diff --git a/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json b/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json index 776ca9e340..f1b9ad18e7 100644 --- a/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json +++ b/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json @@ -15,6 +15,22 @@ "name": "AccessDenied", "type": "error" }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegator", + "type": "address" + }, + { + "internalType": "address", + "name": "delegatee", + "type": "address" + } + ], + "name": "DelegationNotFound", + "type": "error" + }, { "inputs": [ { @@ -161,6 +177,29 @@ "name": "SystemExists", "type": "error" }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegatee", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "delegationControlId", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "initFuncSelectorAndArgs", + "type": "bytes" + } + ], + "name": "registerDelegation", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/packages/world/gas-report.json b/packages/world/gas-report.json index 816a3f54d2..7a66c2d785 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": 1411953 + "gasUsed": 1411774 }, { "file": "test/KeysInTableModule.t.sol", "test": "testInstallGas", "name": "install keys in table module", - "gasUsed": 1411953 + "gasUsed": 1411774 }, { "file": "test/KeysInTableModule.t.sol", "test": "testInstallGas", "name": "set a record on a table with keysInTableModule installed", - "gasUsed": 181989 + "gasUsed": 182055 }, { "file": "test/KeysInTableModule.t.sol", "test": "testInstallSingleton", "name": "install keys in table module", - "gasUsed": 1411953 + "gasUsed": 1411774 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookCompositeGas", "name": "install keys in table module", - "gasUsed": 1411953 + "gasUsed": 1411774 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookCompositeGas", "name": "change a composite record on a table with keysInTableModule installed", - "gasUsed": 25645 + "gasUsed": 25667 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookCompositeGas", "name": "delete a composite record on a table with keysInTableModule installed", - "gasUsed": 250522 + "gasUsed": 250544 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookGas", "name": "install keys in table module", - "gasUsed": 1411953 + "gasUsed": 1411774 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookGas", "name": "change a record on a table with keysInTableModule installed", - "gasUsed": 24365 + "gasUsed": 24387 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookGas", "name": "delete a record on a table with keysInTableModule installed", - "gasUsed": 128811 + "gasUsed": 128833 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testGetKeysWithValueGas", "name": "install keys with value module", - "gasUsed": 650480 + "gasUsed": 650602 }, { "file": "test/KeysWithValueModule.t.sol", @@ -117,25 +117,25 @@ "file": "test/KeysWithValueModule.t.sol", "test": "testInstall", "name": "install keys with value module", - "gasUsed": 650480 + "gasUsed": 650602 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testInstall", "name": "set a record on a table with KeysWithValueModule installed", - "gasUsed": 151489 + "gasUsed": 151511 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetAndDeleteRecordHook", "name": "install keys with value module", - "gasUsed": 650480 + "gasUsed": 650602 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetAndDeleteRecordHook", "name": "change a record on a table with KeysWithValueModule installed", - "gasUsed": 117961 + "gasUsed": 117983 }, { "file": "test/KeysWithValueModule.t.sol", @@ -147,7 +147,7 @@ "file": "test/KeysWithValueModule.t.sol", "test": "testSetField", "name": "install keys with value module", - "gasUsed": 650480 + "gasUsed": 650602 }, { "file": "test/KeysWithValueModule.t.sol", @@ -165,31 +165,31 @@ "file": "test/query.t.sol", "test": "testCombinedHasHasValueNotQuery", "name": "CombinedHasHasValueNotQuery", - "gasUsed": 165699 + "gasUsed": 165765 }, { "file": "test/query.t.sol", "test": "testCombinedHasHasValueQuery", "name": "CombinedHasHasValueQuery", - "gasUsed": 75996 + "gasUsed": 76018 }, { "file": "test/query.t.sol", "test": "testCombinedHasNotQuery", "name": "CombinedHasNotQuery", - "gasUsed": 229855 + "gasUsed": 229965 }, { "file": "test/query.t.sol", "test": "testCombinedHasQuery", "name": "CombinedHasQuery", - "gasUsed": 151588 + "gasUsed": 151676 }, { "file": "test/query.t.sol", "test": "testCombinedHasValueNotQuery", "name": "CombinedHasValueNotQuery", - "gasUsed": 143470 + "gasUsed": 143536 }, { "file": "test/query.t.sol", @@ -201,19 +201,19 @@ "file": "test/query.t.sol", "test": "testHasQuery", "name": "HasQuery", - "gasUsed": 34883 + "gasUsed": 34905 }, { "file": "test/query.t.sol", "test": "testHasQuery1000Keys", "name": "HasQuery with 1000 keys", - "gasUsed": 9272645 + "gasUsed": 9272667 }, { "file": "test/query.t.sol", "test": "testHasQuery100Keys", "name": "HasQuery with 100 keys", - "gasUsed": 861378 + "gasUsed": 861400 }, { "file": "test/query.t.sol", @@ -225,13 +225,37 @@ "file": "test/query.t.sol", "test": "testNotValueQuery", "name": "NotValueQuery", - "gasUsed": 69590 + "gasUsed": 69612 + }, + { + "file": "test/StandardDelegationsModule.t.sol", + "test": "testCallFromCallboundDelegation", + "name": "register a callbound delegation", + "gasUsed": 122345 + }, + { + "file": "test/StandardDelegationsModule.t.sol", + "test": "testCallFromCallboundDelegation", + "name": "call a system via a callbound delegation", + "gasUsed": 44047 + }, + { + "file": "test/StandardDelegationsModule.t.sol", + "test": "testCallFromTimeboundDelegation", + "name": "register a timebound delegation", + "gasUsed": 116594 + }, + { + "file": "test/StandardDelegationsModule.t.sol", + "test": "testCallFromTimeboundDelegation", + "name": "call a system via a timebound delegation", + "gasUsed": 34791 }, { "file": "test/UniqueEntityModule.t.sol", "test": "testInstall", "name": "install unique entity module", - "gasUsed": 722121 + "gasUsed": 722425 }, { "file": "test/UniqueEntityModule.t.sol", @@ -243,7 +267,7 @@ "file": "test/UniqueEntityModule.t.sol", "test": "testInstallRoot", "name": "installRoot unique entity module", - "gasUsed": 701016 + "gasUsed": 701392 }, { "file": "test/UniqueEntityModule.t.sol", @@ -257,6 +281,18 @@ "name": "call a system via the World", "gasUsed": 17531 }, + { + "file": "test/World.t.sol", + "test": "testCallFromUnlimitedDelegation", + "name": "register an unlimited delegation", + "gasUsed": 55457 + }, + { + "file": "test/World.t.sol", + "test": "testCallFromUnlimitedDelegation", + "name": "call a system via an unlimited delegation", + "gasUsed": 17865 + }, { "file": "test/World.t.sol", "test": "testDeleteRecord", @@ -273,37 +309,37 @@ "file": "test/World.t.sol", "test": "testRegisterFallbackSystem", "name": "Register a fallback system", - "gasUsed": 70205 + "gasUsed": 70416 }, { "file": "test/World.t.sol", "test": "testRegisterFallbackSystem", "name": "Register a root fallback system", - "gasUsed": 63676 + "gasUsed": 63722 }, { "file": "test/World.t.sol", "test": "testRegisterFunctionSelector", "name": "Register a function selector", - "gasUsed": 90799 + "gasUsed": 91010 }, { "file": "test/World.t.sol", "test": "testRegisterNamespace", "name": "Register a new namespace", - "gasUsed": 140015 + "gasUsed": 140095 }, { "file": "test/World.t.sol", "test": "testRegisterRootFunctionSelector", "name": "Register a root function selector", - "gasUsed": 79587 + "gasUsed": 79633 }, { "file": "test/World.t.sol", "test": "testRegisterTable", "name": "Register a new table in the namespace", - "gasUsed": 650174 + "gasUsed": 650153 }, { "file": "test/World.t.sol", diff --git a/packages/world/mud.config.ts b/packages/world/mud.config.ts index df45eb446b..407dfe56e5 100644 --- a/packages/world/mud.config.ts +++ b/packages/world/mud.config.ts @@ -42,6 +42,15 @@ export default mudConfig({ // schema in `getField` too. (See https://github.com/latticexyz/mud/issues/444) dataStruct: true, }, + Delegations: { + keySchema: { + delegator: "address", + delegatee: "address", + }, + schema: { + delegationControlId: "bytes32", + }, + }, /************************************************************************ * * MODULE TABLES @@ -131,6 +140,28 @@ export default mudConfig({ tableIdArgument: true, storeArgument: true, }, + CallboundDelegations: { + directory: "modules/std-delegations/tables", + keySchema: { + delegator: "address", + delegatee: "address", + resourceSelector: "bytes32", + funcSelectorAndArgsHash: "bytes32", + }, + schema: { + availableCalls: "uint256", + }, + }, + TimeboundDelegations: { + directory: "modules/std-delegations/tables", + keySchema: { + delegator: "address", + delegatee: "address", + }, + schema: { + maxTimestamp: "uint256", + }, + }, /************************************************************************ * * TEST TABLES diff --git a/packages/world/src/Delegation.sol b/packages/world/src/Delegation.sol new file mode 100644 index 0000000000..c4140ea0d7 --- /dev/null +++ b/packages/world/src/Delegation.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import { UNLIMITED_DELEGATION } from "./constants.sol"; +import { IDelegationControl } from "./interfaces/IDelegationControl.sol"; +import { SystemCall } from "./SystemCall.sol"; + +type Delegation is bytes32; + +using DelegationInstance for Delegation global; + +library DelegationInstance { + function exists(Delegation self) internal pure returns (bool) { + return Delegation.unwrap(self) != bytes32(""); + } + + function isUnlimited(Delegation self) internal pure returns (bool) { + return Delegation.unwrap(self) == UNLIMITED_DELEGATION; + } + + function isLimited(Delegation self) internal pure returns (bool) { + return exists(self) && !isUnlimited(self); + } + + /** + * Verify a delegation. + * Returns true if the delegation exists and is valid, false otherwise. + * Note: verifying the delegation might have side effects in the delegation control contract. + */ + function verify( + Delegation self, + address delegator, + address delegatee, + bytes32 systemId, + bytes memory funcSelectorAndArgs + ) internal returns (bool) { + // Early return if there is an unlimited delegation + if (isUnlimited(self)) return true; + + // Early return if there is no valid delegation + if (!exists(self)) return false; + + // Call the delegation control contract to check if the delegator has granted access to the delegatee + (bool success, bytes memory data) = SystemCall.call({ + caller: delegatee, + resourceSelector: Delegation.unwrap(self), + funcSelectorAndArgs: abi.encodeWithSelector( + IDelegationControl.verify.selector, + delegator, + systemId, + funcSelectorAndArgs + ), + value: 0 + }); + + if (!success) return false; + return abi.decode(data, (bool)); + } +} diff --git a/packages/world/src/DelegationControl.sol b/packages/world/src/DelegationControl.sol new file mode 100644 index 0000000000..29c432e2ad --- /dev/null +++ b/packages/world/src/DelegationControl.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import { WorldContextConsumer } from "./WorldContext.sol"; +import { IDelegationControl } from "./interfaces/IDelegationControl.sol"; + +abstract contract DelegationControl is WorldContextConsumer, IDelegationControl {} diff --git a/packages/world/src/Tables.sol b/packages/world/src/Tables.sol index b6b1f4bd1a..40e2a3aa48 100644 --- a/packages/world/src/Tables.sol +++ b/packages/world/src/Tables.sol @@ -6,6 +6,7 @@ pragma solidity >=0.8.0; import { NamespaceOwner, NamespaceOwnerTableId } from "./tables/NamespaceOwner.sol"; import { ResourceAccess, ResourceAccessTableId } from "./tables/ResourceAccess.sol"; import { InstalledModules, InstalledModulesData, InstalledModulesTableId } from "./tables/InstalledModules.sol"; +import { Delegations, DelegationsTableId } from "./tables/Delegations.sol"; import { Systems, SystemsTableId } from "./modules/core/tables/Systems.sol"; import { SystemRegistry, SystemRegistryTableId } from "./modules/core/tables/SystemRegistry.sol"; import { SystemHooks, SystemHooksTableId } from "./modules/core/tables/SystemHooks.sol"; @@ -15,5 +16,7 @@ import { KeysWithValue } from "./modules/keyswithvalue/tables/KeysWithValue.sol" import { KeysInTable, KeysInTableData, KeysInTableTableId } from "./modules/keysintable/tables/KeysInTable.sol"; 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 { TimeboundDelegations, TimeboundDelegationsTableId } from "./modules/std-delegations/tables/TimeboundDelegations.sol"; import { Bool } from "./../test/tables/Bool.sol"; import { AddressArray } from "./../test/tables/AddressArray.sol"; diff --git a/packages/world/src/World.sol b/packages/world/src/World.sol index 9870fd82bc..fce2f6ad72 100644 --- a/packages/world/src/World.sol +++ b/packages/world/src/World.sol @@ -14,13 +14,16 @@ import { AccessControl } from "./AccessControl.sol"; import { SystemCall } from "./SystemCall.sol"; import { WorldContextProvider } from "./WorldContext.sol"; import { revertWithBytes } from "./revertWithBytes.sol"; +import { Delegation } from "./Delegation.sol"; import { NamespaceOwner } from "./tables/NamespaceOwner.sol"; import { InstalledModules } from "./tables/InstalledModules.sol"; +import { Delegations } from "./tables/Delegations.sol"; import { ISystemHook } from "./interfaces/ISystemHook.sol"; import { IModule } from "./interfaces/IModule.sol"; import { IWorldKernel } from "./interfaces/IWorldKernel.sol"; +import { IDelegationControl } from "./interfaces/IDelegationControl.sol"; import { Systems } from "./modules/core/tables/Systems.sol"; import { SystemHooks } from "./modules/core/tables/SystemHooks.sol"; @@ -179,6 +182,33 @@ contract World is StoreRead, IStoreData, IWorldKernel { return SystemCall.callWithHooksOrRevert(msg.sender, resourceSelector, funcSelectorAndArgs, msg.value); } + /** + * Call the system at the given resourceSelector on behalf of the given delegator. + * If the system is not public, the delegator must have access to the namespace or name (encoded in the resourceSelector). + */ + function callFrom( + address delegator, + bytes32 resourceSelector, + bytes memory funcSelectorAndArgs + ) external payable virtual returns (bytes memory) { + // Check if there is an explicit authorization for this caller to perform actions on behalf of the delegator + Delegation explicitDelegation = Delegation.wrap(Delegations.get({ delegator: delegator, delegatee: msg.sender })); + + if (explicitDelegation.verify(delegator, msg.sender, resourceSelector, funcSelectorAndArgs)) { + // forward the call as `delegator` + return SystemCall.callWithHooksOrRevert(delegator, resourceSelector, funcSelectorAndArgs, msg.value); + } + + // Check if the delegator has a fallback delegation control set + Delegation fallbackDelegation = Delegation.wrap(Delegations.get({ delegator: delegator, delegatee: address(0) })); + if (fallbackDelegation.verify(delegator, msg.sender, resourceSelector, funcSelectorAndArgs)) { + // forward the call with `from` as `msgSender` + return SystemCall.callWithHooksOrRevert(delegator, resourceSelector, funcSelectorAndArgs, msg.value); + } + + revert DelegationNotFound(delegator, msg.sender); + } + /************************************************************************ * * DYNAMIC FUNCTION SELECTORS diff --git a/packages/world/src/constants.sol b/packages/world/src/constants.sol index 98de758522..a4a2538803 100644 --- a/packages/world/src/constants.sol +++ b/packages/world/src/constants.sol @@ -3,3 +3,4 @@ pragma solidity >=0.8.0; bytes16 constant ROOT_NAMESPACE = 0; bytes16 constant ROOT_NAME = 0; +bytes32 constant UNLIMITED_DELEGATION = bytes32(abi.encodePacked(ROOT_NAMESPACE, bytes16("unlimited.d"))); diff --git a/packages/world/src/interfaces/IDelegationControl.sol b/packages/world/src/interfaces/IDelegationControl.sol new file mode 100644 index 0000000000..468994f05e --- /dev/null +++ b/packages/world/src/interfaces/IDelegationControl.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +interface IDelegationControl { + function verify(address delegator, bytes32 systemId, bytes calldata funcSelectorAndArgs) external returns (bool); +} diff --git a/packages/world/src/interfaces/IWorldErrors.sol b/packages/world/src/interfaces/IWorldErrors.sol index 2ed17b66bd..984ee282bb 100644 --- a/packages/world/src/interfaces/IWorldErrors.sol +++ b/packages/world/src/interfaces/IWorldErrors.sol @@ -10,4 +10,5 @@ interface IWorldErrors { error FunctionSelectorExists(bytes4 functionSelector); error FunctionSelectorNotFound(bytes4 functionSelector); error ModuleAlreadyInstalled(string module); + error DelegationNotFound(address delegator, address delegatee); } diff --git a/packages/world/src/interfaces/IWorldKernel.sol b/packages/world/src/interfaces/IWorldKernel.sol index d4ac2e6f13..4016a56772 100644 --- a/packages/world/src/interfaces/IWorldKernel.sol +++ b/packages/world/src/interfaces/IWorldKernel.sol @@ -20,6 +20,16 @@ interface IWorldCall { * If the system is not public, the caller must have access to the namespace or name (encoded in the resourceSelector). */ function call(bytes32 resourceSelector, bytes memory funcSelectorAndArgs) external payable returns (bytes memory); + + /** + * Call the system at the given resourceSelector on behalf of the given delegator. + * If the system is not public, the delegator must have access to the namespace or name (encoded in the resourceSelector). + */ + function callFrom( + address delegator, + bytes32 resourceSelector, + bytes memory funcSelectorAndArgs + ) external payable returns (bytes memory); } /** diff --git a/packages/world/src/interfaces/IWorldRegistrationSystem.sol b/packages/world/src/interfaces/IWorldRegistrationSystem.sol index 8409aadd6f..b684602e53 100644 --- a/packages/world/src/interfaces/IWorldRegistrationSystem.sol +++ b/packages/world/src/interfaces/IWorldRegistrationSystem.sol @@ -24,4 +24,10 @@ interface IWorldRegistrationSystem { bytes4 worldFunctionSelector, bytes4 systemFunctionSelector ) external returns (bytes4); + + function registerDelegation( + address delegatee, + bytes32 delegationControlId, + bytes memory initFuncSelectorAndArgs + ) external; } diff --git a/packages/world/src/modules/core/CoreModule.sol b/packages/world/src/modules/core/CoreModule.sol index 820bb893a6..fecd590879 100644 --- a/packages/world/src/modules/core/CoreModule.sol +++ b/packages/world/src/modules/core/CoreModule.sol @@ -90,13 +90,14 @@ contract CoreModule is IModule, WorldContextConsumer { * Register function selectors for all CoreSystem functions in the World */ function _registerFunctionSelectors() internal { - bytes4[12] memory functionSelectors = [ + bytes4[13] memory functionSelectors = [ // --- WorldRegistrationSystem --- WorldRegistrationSystem.registerNamespace.selector, WorldRegistrationSystem.registerSystemHook.selector, WorldRegistrationSystem.registerSystem.selector, WorldRegistrationSystem.registerFunctionSelector.selector, WorldRegistrationSystem.registerRootFunctionSelector.selector, + WorldRegistrationSystem.registerDelegation.selector, // --- StoreRegistrationSystem --- StoreRegistrationSystem.registerTable.selector, StoreRegistrationSystem.registerStoreHook.selector, diff --git a/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol b/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol index fe0251b54c..ceba247354 100644 --- a/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol +++ b/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol @@ -6,10 +6,11 @@ import { WorldContextConsumer } from "../../../WorldContext.sol"; import { ResourceSelector } from "../../../ResourceSelector.sol"; import { Resource } from "../../../Types.sol"; import { SystemCall } from "../../../SystemCall.sol"; -import { ROOT_NAMESPACE, ROOT_NAME } from "../../../constants.sol"; +import { ROOT_NAMESPACE, ROOT_NAME, UNLIMITED_DELEGATION } from "../../../constants.sol"; import { AccessControl } from "../../../AccessControl.sol"; import { NamespaceOwner } from "../../../tables/NamespaceOwner.sol"; import { ResourceAccess } from "../../../tables/ResourceAccess.sol"; +import { Delegations } from "../../../tables/Delegations.sol"; import { ISystemHook } from "../../../interfaces/ISystemHook.sol"; import { IWorldErrors } from "../../../interfaces/IWorldErrors.sol"; @@ -152,4 +153,26 @@ contract WorldRegistrationSystem is System, IWorldErrors { return worldFunctionSelector; } + + /** + * Register a delegation from the caller to the given delegatee. + */ + function registerDelegation( + address delegatee, + bytes32 delegationControlId, + bytes memory initFuncSelectorAndArgs + ) public { + // Store the delegation control contract address + Delegations.set({ delegator: _msgSender(), delegatee: delegatee, delegationControlId: delegationControlId }); + + // If the delegation is not unlimited, call the delegation control contract's init function + if (delegationControlId != UNLIMITED_DELEGATION && initFuncSelectorAndArgs.length > 0) { + SystemCall.call({ + caller: _msgSender(), + resourceSelector: delegationControlId, + funcSelectorAndArgs: initFuncSelectorAndArgs, + value: 0 + }); + } + } } diff --git a/packages/world/src/modules/std-delegations/CallboundDelegationControl.sol b/packages/world/src/modules/std-delegations/CallboundDelegationControl.sol new file mode 100644 index 0000000000..66785b0bfa --- /dev/null +++ b/packages/world/src/modules/std-delegations/CallboundDelegationControl.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import { DelegationControl } from "../../DelegationControl.sol"; +import { CallboundDelegations } from "./tables/CallboundDelegations.sol"; + +contract CallboundDelegationControl is DelegationControl { + /** + * Verify a delegation by checking if the delegator has any available calls left in the CallboundDelegations table and decrementing the available calls if so. + */ + function verify(address delegator, bytes32 resourceSelector, bytes memory funcSelectorAndArgs) public returns (bool) { + bytes32 funcSelectorAndArgsHash = keccak256(funcSelectorAndArgs); + + // Get the number of available calls for the given delegator, resourceSelector and funcSelectorAndArgs + uint256 availableCalls = CallboundDelegations.get({ + delegator: delegator, + delegatee: _msgSender(), + resourceSelector: resourceSelector, + funcSelectorAndArgsHash: funcSelectorAndArgsHash + }); + + if (availableCalls == 1) { + // Remove the delegation from the CallboundDelegations table + CallboundDelegations.deleteRecord({ + delegator: delegator, + delegatee: _msgSender(), + resourceSelector: resourceSelector, + funcSelectorAndArgsHash: funcSelectorAndArgsHash + }); + return true; + } + + if (availableCalls > 0) { + // Decrement the number of available calls + unchecked { + availableCalls--; + } + CallboundDelegations.set({ + delegator: delegator, + delegatee: _msgSender(), + resourceSelector: resourceSelector, + funcSelectorAndArgsHash: funcSelectorAndArgsHash, + availableCalls: availableCalls + }); + return true; + } + + return false; + } + + /** + * Initialize a delegation by setting the number of available calls in the CallboundDelegations table + */ + function initDelegation( + address delegatee, + bytes32 resourceSelector, + bytes memory funcSelectorAndArgs, + uint256 numCalls + ) public { + CallboundDelegations.set({ + delegator: _msgSender(), + delegatee: delegatee, + resourceSelector: resourceSelector, + funcSelectorAndArgsHash: keccak256(funcSelectorAndArgs), + availableCalls: numCalls + }); + } +} diff --git a/packages/world/src/modules/std-delegations/StandardDelegationsModule.sol b/packages/world/src/modules/std-delegations/StandardDelegationsModule.sol new file mode 100644 index 0000000000..97d31ef2ec --- /dev/null +++ b/packages/world/src/modules/std-delegations/StandardDelegationsModule.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import { IBaseWorld } from "../../interfaces/IBaseWorld.sol"; +import { IModule } from "../../interfaces/IModule.sol"; + +import { WorldContextConsumer } from "../../WorldContext.sol"; +import { ResourceSelector } from "../../ResourceSelector.sol"; + +import { CallboundDelegationControl } from "./CallboundDelegationControl.sol"; +import { TimeboundDelegationControl } from "./TimeboundDelegationControl.sol"; +import { MODULE_NAME, CALLBOUND_DELEGATION, TIMEBOUND_DELEGATION } from "./constants.sol"; + +import { CallboundDelegations } from "./tables/CallboundDelegations.sol"; +import { TimeboundDelegations } from "./tables/TimeboundDelegations.sol"; + +/** + * This module registers tables and delegation control systems required for standard delegations + */ +contract StandardDelegationsModule is IModule, WorldContextConsumer { + CallboundDelegationControl private immutable callboundDelegationControl = new CallboundDelegationControl(); + TimeboundDelegationControl private immutable timeboundDelegationControl = new TimeboundDelegationControl(); + + function getName() public pure returns (bytes16) { + return MODULE_NAME; + } + + function install(bytes memory) public { + IBaseWorld world = IBaseWorld(_world()); + + // Register tables + CallboundDelegations.register(world); + TimeboundDelegations.register(world); + + // Register systems + world.registerSystem(CALLBOUND_DELEGATION, callboundDelegationControl, true); + world.registerSystem(TIMEBOUND_DELEGATION, timeboundDelegationControl, true); + } +} diff --git a/packages/world/src/modules/std-delegations/TimeboundDelegationControl.sol b/packages/world/src/modules/std-delegations/TimeboundDelegationControl.sol new file mode 100644 index 0000000000..ca792336bf --- /dev/null +++ b/packages/world/src/modules/std-delegations/TimeboundDelegationControl.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import { DelegationControl } from "../../DelegationControl.sol"; +import { TimeboundDelegations } from "./tables/TimeboundDelegations.sol"; + +contract TimeboundDelegationControl is DelegationControl { + /** + * Verify a delegation by checking if the current block timestamp is not larger than the max valid timestamp for the delegation. + * Note: the delegation control check ignores the resourceSelector and funcSelectorAndArgs parameters. + */ + function verify(address delegator, bytes32, bytes memory) public view returns (bool) { + // Get the max valid timestamp for the given delegator + uint256 maxTimestamp = TimeboundDelegations.get({ delegator: delegator, delegatee: _msgSender() }); + + // Return true if the current timestamp is smaller or equal to the max valid timestamp + return block.timestamp <= maxTimestamp; + } + + /** + * Initialize a delegation by setting the max valid timestamp in the TimeboundDelegations table + */ + function initDelegation(address delegatee, uint256 maxTimestamp) public { + TimeboundDelegations.set({ delegator: _msgSender(), delegatee: delegatee, maxTimestamp: maxTimestamp }); + } +} diff --git a/packages/world/src/modules/std-delegations/constants.sol b/packages/world/src/modules/std-delegations/constants.sol new file mode 100644 index 0000000000..dfe282a288 --- /dev/null +++ b/packages/world/src/modules/std-delegations/constants.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; +import { ResourceSelector } from "../../ResourceSelector.sol"; +import { ROOT_NAMESPACE } from "../../constants.sol"; + +bytes16 constant MODULE_NAME = bytes16("stddelegations.m"); + +// Callbound delegation +bytes32 constant CALLBOUND_DELEGATION = bytes32(abi.encodePacked(ROOT_NAMESPACE, bytes16("callbound.d"))); + +// Timebound delegation +bytes32 constant TIMEBOUND_DELEGATION = bytes32(abi.encodePacked(ROOT_NAMESPACE, bytes16("timebound.d"))); diff --git a/packages/world/src/modules/std-delegations/tables/CallboundDelegations.sol b/packages/world/src/modules/std-delegations/tables/CallboundDelegations.sol new file mode 100644 index 0000000000..2a0282f97d --- /dev/null +++ b/packages/world/src/modules/std-delegations/tables/CallboundDelegations.sol @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +/* 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 { Schema, SchemaLib } from "@latticexyz/store/src/Schema.sol"; +import { PackedCounter, PackedCounterLib } from "@latticexyz/store/src/PackedCounter.sol"; + +bytes32 constant _tableId = bytes32(abi.encodePacked(bytes16(""), bytes16("CallboundDelegat"))); +bytes32 constant CallboundDelegationsTableId = _tableId; + +library CallboundDelegations { + /** Get the table's key schema */ + function getKeySchema() internal pure returns (Schema) { + SchemaType[] memory _schema = new SchemaType[](4); + _schema[0] = SchemaType.ADDRESS; + _schema[1] = SchemaType.ADDRESS; + _schema[2] = SchemaType.BYTES32; + _schema[3] = SchemaType.BYTES32; + + return SchemaLib.encode(_schema); + } + + /** Get the table's value schema */ + function getValueSchema() internal pure returns (Schema) { + SchemaType[] memory _schema = new SchemaType[](1); + _schema[0] = SchemaType.UINT256; + + return SchemaLib.encode(_schema); + } + + /** Get the table's key names */ + function getKeyNames() internal pure returns (string[] memory keyNames) { + keyNames = new string[](4); + keyNames[0] = "delegator"; + keyNames[1] = "delegatee"; + keyNames[2] = "resourceSelector"; + keyNames[3] = "funcSelectorAndArgsHash"; + } + + /** Get the table's field names */ + function getFieldNames() internal pure returns (string[] memory fieldNames) { + fieldNames = new string[](1); + fieldNames[0] = "availableCalls"; + } + + /** Register the table's key schema, value schema, key names and value names */ + function register() internal { + StoreSwitch.registerTable(_tableId, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames()); + } + + /** Register the table's key schema, value schema, key names and value names (using the specified store) */ + function register(IStore _store) internal { + _store.registerTable(_tableId, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames()); + } + + /** Get availableCalls */ + function get( + address delegator, + address delegatee, + bytes32 resourceSelector, + bytes32 funcSelectorAndArgsHash + ) internal view returns (uint256 availableCalls) { + bytes32[] memory _keyTuple = new bytes32[](4); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + _keyTuple[2] = resourceSelector; + _keyTuple[3] = funcSelectorAndArgsHash; + + bytes memory _blob = StoreSwitch.getField(_tableId, _keyTuple, 0, getValueSchema()); + return (uint256(Bytes.slice32(_blob, 0))); + } + + /** Get availableCalls (using the specified store) */ + function get( + IStore _store, + address delegator, + address delegatee, + bytes32 resourceSelector, + bytes32 funcSelectorAndArgsHash + ) internal view returns (uint256 availableCalls) { + bytes32[] memory _keyTuple = new bytes32[](4); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + _keyTuple[2] = resourceSelector; + _keyTuple[3] = funcSelectorAndArgsHash; + + bytes memory _blob = _store.getField(_tableId, _keyTuple, 0, getValueSchema()); + return (uint256(Bytes.slice32(_blob, 0))); + } + + /** Set availableCalls */ + function set( + address delegator, + address delegatee, + bytes32 resourceSelector, + bytes32 funcSelectorAndArgsHash, + uint256 availableCalls + ) internal { + bytes32[] memory _keyTuple = new bytes32[](4); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + _keyTuple[2] = resourceSelector; + _keyTuple[3] = funcSelectorAndArgsHash; + + StoreSwitch.setField(_tableId, _keyTuple, 0, abi.encodePacked((availableCalls)), getValueSchema()); + } + + /** Set availableCalls (using the specified store) */ + function set( + IStore _store, + address delegator, + address delegatee, + bytes32 resourceSelector, + bytes32 funcSelectorAndArgsHash, + uint256 availableCalls + ) internal { + bytes32[] memory _keyTuple = new bytes32[](4); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + _keyTuple[2] = resourceSelector; + _keyTuple[3] = funcSelectorAndArgsHash; + + _store.setField(_tableId, _keyTuple, 0, abi.encodePacked((availableCalls)), getValueSchema()); + } + + /** Tightly pack full data using this table's schema */ + function encode(uint256 availableCalls) internal pure returns (bytes memory) { + return abi.encodePacked(availableCalls); + } + + /** Encode keys as a bytes32 array using this table's schema */ + function encodeKeyTuple( + address delegator, + address delegatee, + bytes32 resourceSelector, + bytes32 funcSelectorAndArgsHash + ) internal pure returns (bytes32[] memory) { + bytes32[] memory _keyTuple = new bytes32[](4); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + _keyTuple[2] = resourceSelector; + _keyTuple[3] = funcSelectorAndArgsHash; + + return _keyTuple; + } + + /* Delete all data for given keys */ + function deleteRecord( + address delegator, + address delegatee, + bytes32 resourceSelector, + bytes32 funcSelectorAndArgsHash + ) internal { + bytes32[] memory _keyTuple = new bytes32[](4); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + _keyTuple[2] = resourceSelector; + _keyTuple[3] = funcSelectorAndArgsHash; + + StoreSwitch.deleteRecord(_tableId, _keyTuple, getValueSchema()); + } + + /* Delete all data for given keys (using the specified store) */ + function deleteRecord( + IStore _store, + address delegator, + address delegatee, + bytes32 resourceSelector, + bytes32 funcSelectorAndArgsHash + ) internal { + bytes32[] memory _keyTuple = new bytes32[](4); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + _keyTuple[2] = resourceSelector; + _keyTuple[3] = funcSelectorAndArgsHash; + + _store.deleteRecord(_tableId, _keyTuple, getValueSchema()); + } +} diff --git a/packages/world/src/modules/std-delegations/tables/TimeboundDelegations.sol b/packages/world/src/modules/std-delegations/tables/TimeboundDelegations.sol new file mode 100644 index 0000000000..57df78c17a --- /dev/null +++ b/packages/world/src/modules/std-delegations/tables/TimeboundDelegations.sol @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +/* 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 { Schema, SchemaLib } from "@latticexyz/store/src/Schema.sol"; +import { PackedCounter, PackedCounterLib } from "@latticexyz/store/src/PackedCounter.sol"; + +bytes32 constant _tableId = bytes32(abi.encodePacked(bytes16(""), bytes16("TimeboundDelegat"))); +bytes32 constant TimeboundDelegationsTableId = _tableId; + +library TimeboundDelegations { + /** Get the table's key schema */ + function getKeySchema() internal pure returns (Schema) { + SchemaType[] memory _schema = new SchemaType[](2); + _schema[0] = SchemaType.ADDRESS; + _schema[1] = SchemaType.ADDRESS; + + return SchemaLib.encode(_schema); + } + + /** Get the table's value schema */ + function getValueSchema() internal pure returns (Schema) { + SchemaType[] memory _schema = new SchemaType[](1); + _schema[0] = SchemaType.UINT256; + + return SchemaLib.encode(_schema); + } + + /** Get the table's key names */ + function getKeyNames() internal pure returns (string[] memory keyNames) { + keyNames = new string[](2); + keyNames[0] = "delegator"; + keyNames[1] = "delegatee"; + } + + /** Get the table's field names */ + function getFieldNames() internal pure returns (string[] memory fieldNames) { + fieldNames = new string[](1); + fieldNames[0] = "maxTimestamp"; + } + + /** Register the table's key schema, value schema, key names and value names */ + function register() internal { + StoreSwitch.registerTable(_tableId, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames()); + } + + /** Register the table's key schema, value schema, key names and value names (using the specified store) */ + function register(IStore _store) internal { + _store.registerTable(_tableId, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames()); + } + + /** Get maxTimestamp */ + function get(address delegator, address delegatee) internal view returns (uint256 maxTimestamp) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + + bytes memory _blob = StoreSwitch.getField(_tableId, _keyTuple, 0, getValueSchema()); + return (uint256(Bytes.slice32(_blob, 0))); + } + + /** Get maxTimestamp (using the specified store) */ + function get(IStore _store, address delegator, address delegatee) internal view returns (uint256 maxTimestamp) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + + bytes memory _blob = _store.getField(_tableId, _keyTuple, 0, getValueSchema()); + return (uint256(Bytes.slice32(_blob, 0))); + } + + /** Set maxTimestamp */ + function set(address delegator, address delegatee, uint256 maxTimestamp) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + + StoreSwitch.setField(_tableId, _keyTuple, 0, abi.encodePacked((maxTimestamp)), getValueSchema()); + } + + /** Set maxTimestamp (using the specified store) */ + function set(IStore _store, address delegator, address delegatee, uint256 maxTimestamp) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + + _store.setField(_tableId, _keyTuple, 0, abi.encodePacked((maxTimestamp)), getValueSchema()); + } + + /** Tightly pack full data using this table's schema */ + function encode(uint256 maxTimestamp) internal pure returns (bytes memory) { + return abi.encodePacked(maxTimestamp); + } + + /** Encode keys as a bytes32 array using this table's schema */ + function encodeKeyTuple(address delegator, address delegatee) internal pure returns (bytes32[] memory) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + + return _keyTuple; + } + + /* Delete all data for given keys */ + function deleteRecord(address delegator, address delegatee) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + + StoreSwitch.deleteRecord(_tableId, _keyTuple, getValueSchema()); + } + + /* Delete all data for given keys (using the specified store) */ + function deleteRecord(IStore _store, address delegator, address delegatee) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + + _store.deleteRecord(_tableId, _keyTuple, getValueSchema()); + } +} diff --git a/packages/world/src/tables/Delegations.sol b/packages/world/src/tables/Delegations.sol new file mode 100644 index 0000000000..85c695f526 --- /dev/null +++ b/packages/world/src/tables/Delegations.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +/* 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 { Schema, SchemaLib } from "@latticexyz/store/src/Schema.sol"; +import { PackedCounter, PackedCounterLib } from "@latticexyz/store/src/PackedCounter.sol"; + +bytes32 constant _tableId = bytes32(abi.encodePacked(bytes16(""), bytes16("Delegations"))); +bytes32 constant DelegationsTableId = _tableId; + +library Delegations { + /** Get the table's key schema */ + function getKeySchema() internal pure returns (Schema) { + SchemaType[] memory _schema = new SchemaType[](2); + _schema[0] = SchemaType.ADDRESS; + _schema[1] = SchemaType.ADDRESS; + + return SchemaLib.encode(_schema); + } + + /** Get the table's value schema */ + function getValueSchema() internal pure returns (Schema) { + SchemaType[] memory _schema = new SchemaType[](1); + _schema[0] = SchemaType.BYTES32; + + return SchemaLib.encode(_schema); + } + + /** Get the table's key names */ + function getKeyNames() internal pure returns (string[] memory keyNames) { + keyNames = new string[](2); + keyNames[0] = "delegator"; + keyNames[1] = "delegatee"; + } + + /** Get the table's field names */ + function getFieldNames() internal pure returns (string[] memory fieldNames) { + fieldNames = new string[](1); + fieldNames[0] = "delegationControlId"; + } + + /** Register the table's key schema, value schema, key names and value names */ + function register() internal { + StoreSwitch.registerTable(_tableId, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames()); + } + + /** Register the table's key schema, value schema, key names and value names (using the specified store) */ + function register(IStore _store) internal { + _store.registerTable(_tableId, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames()); + } + + /** Get delegationControlId */ + function get(address delegator, address delegatee) internal view returns (bytes32 delegationControlId) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + + bytes memory _blob = StoreSwitch.getField(_tableId, _keyTuple, 0, getValueSchema()); + return (Bytes.slice32(_blob, 0)); + } + + /** Get delegationControlId (using the specified store) */ + function get( + IStore _store, + address delegator, + address delegatee + ) internal view returns (bytes32 delegationControlId) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + + bytes memory _blob = _store.getField(_tableId, _keyTuple, 0, getValueSchema()); + return (Bytes.slice32(_blob, 0)); + } + + /** Set delegationControlId */ + function set(address delegator, address delegatee, bytes32 delegationControlId) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + + StoreSwitch.setField(_tableId, _keyTuple, 0, abi.encodePacked((delegationControlId)), getValueSchema()); + } + + /** Set delegationControlId (using the specified store) */ + function set(IStore _store, address delegator, address delegatee, bytes32 delegationControlId) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + + _store.setField(_tableId, _keyTuple, 0, abi.encodePacked((delegationControlId)), getValueSchema()); + } + + /** Tightly pack full data using this table's schema */ + function encode(bytes32 delegationControlId) internal pure returns (bytes memory) { + return abi.encodePacked(delegationControlId); + } + + /** Encode keys as a bytes32 array using this table's schema */ + function encodeKeyTuple(address delegator, address delegatee) internal pure returns (bytes32[] memory) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + + return _keyTuple; + } + + /* Delete all data for given keys */ + function deleteRecord(address delegator, address delegatee) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + + StoreSwitch.deleteRecord(_tableId, _keyTuple, getValueSchema()); + } + + /* Delete all data for given keys (using the specified store) */ + function deleteRecord(IStore _store, address delegator, address delegatee) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(delegator))); + _keyTuple[1] = bytes32(uint256(uint160(delegatee))); + + _store.deleteRecord(_tableId, _keyTuple, getValueSchema()); + } +} diff --git a/packages/world/test/StandardDelegationsModule.t.sol b/packages/world/test/StandardDelegationsModule.t.sol new file mode 100644 index 0000000000..60258246c2 --- /dev/null +++ b/packages/world/test/StandardDelegationsModule.t.sol @@ -0,0 +1,114 @@ +// 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 { World } from "../src/World.sol"; +import { ResourceSelector } from "../src/ResourceSelector.sol"; +import { IBaseWorld } from "../src/interfaces/IBaseWorld.sol"; +import { IWorldErrors } from "../src/interfaces/IWorldErrors.sol"; +import { CoreModule } from "../src/modules/core/CoreModule.sol"; +import { Systems } from "../src/modules/core/tables/Systems.sol"; +import { StandardDelegationsModule } from "../src/modules/std-delegations/StandardDelegationsModule.sol"; +import { CallboundDelegationControl } from "../src/modules/std-delegations/CallboundDelegationControl.sol"; +import { TimeboundDelegationControl } from "../src/modules/std-delegations/TimeboundDelegationControl.sol"; +import { CALLBOUND_DELEGATION, TIMEBOUND_DELEGATION } from "../src/modules/std-delegations/StandardDelegationsModule.sol"; + +import { WorldTestSystem } from "./World.t.sol"; + +contract StandardDelegationsModuleTest is Test, GasReporter { + IBaseWorld private world; + bytes32 private systemResourceSelector = ResourceSelector.from("namespace", "testSystem"); + address private delegator = address(1); + address private delegatee = address(2); + + function setUp() public { + world = IBaseWorld(address(new World())); + world.installRootModule(new CoreModule(), new bytes(0)); + world.installRootModule(new StandardDelegationsModule(), new bytes(0)); + + // Register a new system + WorldTestSystem system = new WorldTestSystem(); + world.registerSystem(systemResourceSelector, system, true); + } + + function testCallFromCallboundDelegation() public { + // Register the callbound delegation for one call to the system's msgSender function + vm.prank(delegator); + startGasReport("register a callbound delegation"); + world.registerDelegation( + delegatee, + CALLBOUND_DELEGATION, + abi.encodeWithSelector( + CallboundDelegationControl.initDelegation.selector, + delegatee, + systemResourceSelector, + abi.encodeWithSelector(WorldTestSystem.msgSender.selector), + 1 + ) + ); + endGasReport(); + + // Call a system from the delegatee on behalf of the delegator + vm.prank(delegatee); + startGasReport("call a system via a callbound delegation"); + bytes memory returnData = world.callFrom( + delegator, + systemResourceSelector, + abi.encodeWithSelector(WorldTestSystem.msgSender.selector) + ); + endGasReport(); + address returnedAddress = abi.decode(returnData, (address)); + + // Expect the system to have received the delegator's address + assertEq(returnedAddress, delegator); + + // Expect the delegation to have been used up + vm.prank(delegatee); + vm.expectRevert(abi.encodeWithSelector(IWorldErrors.DelegationNotFound.selector, delegator, delegatee)); + world.callFrom(delegator, systemResourceSelector, abi.encodeWithSelector(WorldTestSystem.msgSender.selector)); + } + + function testCallFromTimeboundDelegation() public { + uint256 maxTimestamp = 4242; + + // Set the current timestamp to 1 + vm.warp(1); + + // Register the timebound delegation + vm.prank(delegator); + startGasReport("register a timebound delegation"); + world.registerDelegation( + delegatee, + TIMEBOUND_DELEGATION, + abi.encodeWithSelector(TimeboundDelegationControl.initDelegation.selector, delegatee, maxTimestamp) + ); + endGasReport(); + + // Call a system from the delegatee on behalf of the delegator + vm.prank(delegatee); + startGasReport("call a system via a timebound delegation"); + bytes memory returnData = world.callFrom( + delegator, + systemResourceSelector, + abi.encodeWithSelector(WorldTestSystem.msgSender.selector) + ); + endGasReport(); + address returnedAddress = abi.decode(returnData, (address)); + + // Expect the system to have received the delegator's address + assertEq(returnedAddress, delegator); + + // Set the timestamp to maxTimestamp and expect the delegation to still be valid + vm.warp(maxTimestamp); + vm.prank(delegatee); + world.callFrom(delegator, systemResourceSelector, abi.encodeWithSelector(WorldTestSystem.msgSender.selector)); + + // Set the timestamp to maxTimestamp+1 and expect the delegation to be expired + vm.warp(maxTimestamp + 1); + vm.prank(delegatee); + vm.expectRevert(abi.encodeWithSelector(IWorldErrors.DelegationNotFound.selector, delegator, delegatee)); + world.callFrom(delegator, systemResourceSelector, abi.encodeWithSelector(WorldTestSystem.msgSender.selector)); + } +} diff --git a/packages/world/test/World.t.sol b/packages/world/test/World.t.sol index f6d97b9850..5044a3f4d5 100644 --- a/packages/world/test/World.t.sol +++ b/packages/world/test/World.t.sol @@ -18,7 +18,7 @@ import { EncodeArray } from "@latticexyz/store/src/tightcoder/EncodeArray.sol"; import { World } from "../src/World.sol"; import { System } from "../src/System.sol"; import { ResourceSelector } from "../src/ResourceSelector.sol"; -import { ROOT_NAMESPACE, ROOT_NAME } from "../src/constants.sol"; +import { ROOT_NAMESPACE, ROOT_NAME, UNLIMITED_DELEGATION } from "../src/constants.sol"; import { NamespaceOwner, NamespaceOwnerTableId } from "../src/tables/NamespaceOwner.sol"; import { ResourceAccess } from "../src/tables/ResourceAccess.sol"; @@ -612,6 +612,66 @@ contract WorldTest is Test, GasReporter { assertEq(returnedAddress, address(this), "subsystem returned wrong address"); } + function testCallFromUnlimitedDelegation() public { + // Register a new system + WorldTestSystem system = new WorldTestSystem(); + bytes32 resourceSelector = ResourceSelector.from("namespace", "testSystem"); + world.registerSystem(resourceSelector, system, true); + + // Register an unlimited delegation + address delegator = address(1); + address delegatee = address(2); + vm.prank(delegator); + startGasReport("register an unlimited delegation"); + world.registerDelegation(delegatee, UNLIMITED_DELEGATION, new bytes(0)); + endGasReport(); + + // Call a system from the delegatee on behalf of the delegator + vm.prank(delegatee); + startGasReport("call a system via an unlimited delegation"); + bytes memory returnData = world.callFrom( + delegator, + resourceSelector, + abi.encodeWithSelector(WorldTestSystem.msgSender.selector) + ); + endGasReport(); + address returnedAddress = abi.decode(returnData, (address)); + + // Expect the system to have received the delegator's address + assertEq(returnedAddress, delegator); + } + + function testCallFromFailDelegationNotFound() public { + // Register a new system + WorldTestSystem system = new WorldTestSystem(); + bytes32 resourceSelector = ResourceSelector.from("namespace", "testSystem"); + world.registerSystem(resourceSelector, system, true); + + // Expect a revert when attempting to perform a call on behalf of an address that doesn't have a delegation + vm.expectRevert( + abi.encodeWithSelector( + IWorldErrors.DelegationNotFound.selector, + address(2), // Delegator + address(1) // Delegatee + ) + ); + vm.prank(address(1)); + world.callFrom(address(2), resourceSelector, abi.encodeWithSelector(WorldTestSystem.msgSender.selector)); + } + + function testCallFromLimitedDelegation() public { + // Register a new system + WorldTestSystem system = new WorldTestSystem(); + bytes32 resourceSelector = ResourceSelector.from("namespace", "testSystem"); + world.registerSystem(resourceSelector, system, true); + + // Register a limited delegation + address delegator = address(1); + address delegatee = address(2); + vm.prank(delegator); + world.registerDelegation(delegatee, UNLIMITED_DELEGATION, new bytes(0)); + } + function testRegisterTableHook() public { Schema valueSchema = Bool.getValueSchema(); bytes32 tableId = ResourceSelector.from("", "testTable");