diff --git a/.changeset/quiet-squids-share.md b/.changeset/quiet-squids-share.md new file mode 100644 index 0000000000..34aed7b320 --- /dev/null +++ b/.changeset/quiet-squids-share.md @@ -0,0 +1,116 @@ +--- +"@latticexyz/world": major +--- + +- The previous `Call.withSender` util is replaced with `WorldContextProvider`, since the usecase of appending the `msg.sender` to the calldata is tightly coupled with `WorldContextConsumer` (which extracts the appended context from the calldata). + + The previous `Call.withSender` utility reverted if the call failed and only returned the returndata on success. This is replaced with `callWithContextOrRevert`/`delegatecallWithContextOrRevert` + + ```diff + -import { Call } from "@latticexyz/world/src/Call.sol"; + +import { WorldContextProvider } from "@latticexyz/world/src/WorldContext.sol"; + + -Call.withSender({ + - delegate: false, + - value: 0, + - ... + -}); + +WorldContextProvider.callWithContextOrRevert({ + + value: 0, + + ... + +}); + + -Call.withSender({ + - delegate: true, + - value: 0, + - ... + -}); + +WorldContextProvider.delegatecallWithContextOrRevert({ + + ... + +}); + ``` + + In addition there are utils that return a `bool success` flag instead of reverting on errors. This mirrors the behavior of Solidity's low level `call`/`delegatecall` functions and is useful in situations where additional logic should be executed in case of a reverting external call. + + ```solidity + library WorldContextProvider { + function callWithContext( + address target, // Address to call + bytes memory funcSelectorAndArgs, // Abi encoded function selector and arguments to pass to pass to the contract + address msgSender, // Address to append to the calldata as context for msgSender + uint256 value // Value to pass with the call + ) internal returns (bool success, bytes memory data); + + function delegatecallWithContext( + address target, // Address to call + bytes memory funcSelectorAndArgs, // Abi encoded function selector and arguments to pass to pass to the contract + address msgSender // Address to append to the calldata as context for msgSender + ) internal returns (bool success, bytes memory data); + } + ``` + +- `WorldContext` is renamed to `WorldContextConsumer` to clarify the relationship between `WorldContextProvider` (appending context to the calldata) and `WorldContextConsumer` (extracting context from the calldata) + + ```diff + -import { WorldContext } from "@latticexyz/world/src/WorldContext.sol"; + -import { WorldContextConsumer } from "@latticexyz/world/src/WorldContext.sol"; + ``` + +- The `World` contract previously had a `_call` method to handle calling systems via their resource selector, performing accesss control checks and call hooks registered for the system. + + ```solidity + library SystemCall { + /** + * Calls a system via its resource selector and perform access control checks. + * Does not revert if the call fails, but returns a `success` flag along with the returndata. + */ + function call( + address caller, + bytes32 resourceSelector, + bytes memory funcSelectorAndArgs, + uint256 value + ) internal returns (bool success, bytes memory data); + + /** + * Calls a system via its resource selector, perform access control checks and trigger hooks registered for the system. + * Does not revert if the call fails, but returns a `success` flag along with the returndata. + */ + function callWithHooks( + address caller, + bytes32 resourceSelector, + bytes memory funcSelectorAndArgs, + uint256 value + ) internal returns (bool success, bytes memory data); + + /** + * Calls a system via its resource selector, perform access control checks and trigger hooks registered for the system. + * Reverts if the call fails. + */ + function callWithHooksOrRevert( + address caller, + bytes32 resourceSelector, + bytes memory funcSelectorAndArgs, + uint256 value + ) internal returns (bytes memory data); + } + ``` + +- System hooks now are called with the system's resource selector instead of its address. The system's address can still easily obtained within the hook via `Systems.get(resourceSelector)` if necessary. + + ```diff + interface ISystemHook { + function onBeforeCallSystem( + address msgSender, + - address systemAddress, + + bytes32 resourceSelector, + bytes memory funcSelectorAndArgs + ) external; + + function onAfterCallSystem( + address msgSender, + - address systemAddress, + + bytes32 resourceSelector, + bytes memory funcSelectorAndArgs + ) external; + } + ``` 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 9fd58269ee..1b3f766848 100644 --- a/e2e/packages/contracts/types/ethers-contracts/factories/IWorld__factory.ts +++ b/e2e/packages/contracts/types/ethers-contracts/factories/IWorld__factory.ts @@ -864,7 +864,7 @@ const _abi = [ type: "bytes32", }, { - internalType: "contract System", + internalType: "contract WorldContextConsumer", name: "system", type: "address", }, 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 c9ac6862ea..860136eab7 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 @@ -859,7 +859,7 @@ const _abi = [ type: "bytes32", }, { - internalType: "contract System", + internalType: "contract WorldContextConsumer", name: "system", type: "address", }, diff --git a/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json b/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json index fa1cac5fff..353b346bdb 100644 --- a/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json +++ b/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json @@ -395,7 +395,7 @@ "type": "bytes32" }, { - "internalType": "contract System", + "internalType": "contract WorldContextConsumer", "name": "system", "type": "address" }, diff --git a/packages/world/abi/IBaseWorld.sol/IBaseWorld.abi.json b/packages/world/abi/IBaseWorld.sol/IBaseWorld.abi.json index a71695ddf8..59c2ddcb7c 100644 --- a/packages/world/abi/IBaseWorld.sol/IBaseWorld.abi.json +++ b/packages/world/abi/IBaseWorld.sol/IBaseWorld.abi.json @@ -775,7 +775,7 @@ "type": "bytes32" }, { - "internalType": "contract System", + "internalType": "contract WorldContextConsumer", "name": "system", "type": "address" }, diff --git a/packages/world/abi/ISystemHook.sol/ISystemHook.abi.json b/packages/world/abi/ISystemHook.sol/ISystemHook.abi.json index cbab824056..9ff64abcc5 100644 --- a/packages/world/abi/ISystemHook.sol/ISystemHook.abi.json +++ b/packages/world/abi/ISystemHook.sol/ISystemHook.abi.json @@ -7,9 +7,9 @@ "type": "address" }, { - "internalType": "address", - "name": "systemAddress", - "type": "address" + "internalType": "bytes32", + "name": "resourceSelector", + "type": "bytes32" }, { "internalType": "bytes", @@ -30,9 +30,9 @@ "type": "address" }, { - "internalType": "address", - "name": "systemAddress", - "type": "address" + "internalType": "bytes32", + "name": "resourceSelector", + "type": "bytes32" }, { "internalType": "bytes", diff --git a/packages/world/abi/IWorldRegistrationSystem.sol/IWorldRegistrationSystem.abi.json b/packages/world/abi/IWorldRegistrationSystem.sol/IWorldRegistrationSystem.abi.json index 79efd8e7e0..87fb69f260 100644 --- a/packages/world/abi/IWorldRegistrationSystem.sol/IWorldRegistrationSystem.abi.json +++ b/packages/world/abi/IWorldRegistrationSystem.sol/IWorldRegistrationSystem.abi.json @@ -78,7 +78,7 @@ "type": "bytes32" }, { - "internalType": "contract System", + "internalType": "contract WorldContextConsumer", "name": "system", "type": "address" }, diff --git a/packages/world/abi/Call.sol/Call.abi.json b/packages/world/abi/SystemCall.sol/SystemCall.abi.json similarity index 100% rename from packages/world/abi/Call.sol/Call.abi.json rename to packages/world/abi/SystemCall.sol/SystemCall.abi.json diff --git a/packages/world/abi/WorldContext.sol/WorldContext.abi.json b/packages/world/abi/WorldContext.sol/WorldContextConsumer.abi.json similarity index 100% rename from packages/world/abi/WorldContext.sol/WorldContext.abi.json rename to packages/world/abi/WorldContext.sol/WorldContextConsumer.abi.json diff --git a/packages/world/abi/WorldContext.sol/WorldContextProvider.abi.json b/packages/world/abi/WorldContext.sol/WorldContextProvider.abi.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/packages/world/abi/WorldContext.sol/WorldContextProvider.abi.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json b/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json index 6d19d7475e..776ca9e340 100644 --- a/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json +++ b/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json @@ -240,7 +240,7 @@ "type": "bytes32" }, { - "internalType": "contract System", + "internalType": "contract WorldContextConsumer", "name": "system", "type": "address" }, diff --git a/packages/world/gas-report.json b/packages/world/gas-report.json index 00f5ec4ed6..8e46e1c304 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": 1411022 + "gasUsed": 1411953 }, { "file": "test/KeysInTableModule.t.sol", "test": "testInstallGas", "name": "install keys in table module", - "gasUsed": 1411022 + "gasUsed": 1411953 }, { "file": "test/KeysInTableModule.t.sol", "test": "testInstallGas", "name": "set a record on a table with keysInTableModule installed", - "gasUsed": 182044 + "gasUsed": 181989 }, { "file": "test/KeysInTableModule.t.sol", "test": "testInstallSingleton", "name": "install keys in table module", - "gasUsed": 1411022 + "gasUsed": 1411953 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookCompositeGas", "name": "install keys in table module", - "gasUsed": 1411022 + "gasUsed": 1411953 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookCompositeGas", "name": "change a composite record on a table with keysInTableModule installed", - "gasUsed": 25656 + "gasUsed": 25645 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookCompositeGas", "name": "delete a composite record on a table with keysInTableModule installed", - "gasUsed": 250709 + "gasUsed": 250522 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookGas", "name": "install keys in table module", - "gasUsed": 1411022 + "gasUsed": 1411953 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookGas", "name": "change a record on a table with keysInTableModule installed", - "gasUsed": 24376 + "gasUsed": 24365 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookGas", "name": "delete a record on a table with keysInTableModule installed", - "gasUsed": 128910 + "gasUsed": 128811 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testGetKeysWithValueGas", "name": "install keys with value module", - "gasUsed": 650020 + "gasUsed": 650480 }, { "file": "test/KeysWithValueModule.t.sol", @@ -117,49 +117,49 @@ "file": "test/KeysWithValueModule.t.sol", "test": "testInstall", "name": "install keys with value module", - "gasUsed": 650020 + "gasUsed": 650480 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testInstall", "name": "set a record on a table with KeysWithValueModule installed", - "gasUsed": 151544 + "gasUsed": 151489 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetAndDeleteRecordHook", "name": "install keys with value module", - "gasUsed": 650020 + "gasUsed": 650480 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetAndDeleteRecordHook", "name": "change a record on a table with KeysWithValueModule installed", - "gasUsed": 118016 + "gasUsed": 117961 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetAndDeleteRecordHook", "name": "delete a record on a table with KeysWithValueModule installed", - "gasUsed": 43594 + "gasUsed": 43561 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetField", "name": "install keys with value module", - "gasUsed": 650020 + "gasUsed": 650480 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetField", "name": "set a field on a table with KeysWithValueModule installed", - "gasUsed": 158488 + "gasUsed": 158433 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetField", "name": "change a field on a table with KeysWithValueModule installed", - "gasUsed": 120746 + "gasUsed": 120691 }, { "file": "test/query.t.sol", @@ -231,108 +231,114 @@ "file": "test/UniqueEntityModule.t.sol", "test": "testInstall", "name": "install unique entity module", - "gasUsed": 721265 + "gasUsed": 722077 }, { "file": "test/UniqueEntityModule.t.sol", "test": "testInstall", "name": "get a unique entity nonce (non-root module)", - "gasUsed": 65023 + "gasUsed": 65194 }, { "file": "test/UniqueEntityModule.t.sol", "test": "testInstallRoot", "name": "installRoot unique entity module", - "gasUsed": 700336 + "gasUsed": 700972 }, { "file": "test/UniqueEntityModule.t.sol", "test": "testInstallRoot", "name": "get a unique entity nonce (root module)", - "gasUsed": 65023 + "gasUsed": 65194 + }, + { + "file": "test/World.t.sol", + "test": "testCall", + "name": "call a system via the World", + "gasUsed": 17531 }, { "file": "test/World.t.sol", "test": "testDeleteRecord", "name": "Delete record", - "gasUsed": 12201 + "gasUsed": 12190 }, { "file": "test/World.t.sol", "test": "testPushToField", "name": "Push data to the table", - "gasUsed": 91408 + "gasUsed": 91397 }, { "file": "test/World.t.sol", "test": "testRegisterFallbackSystem", "name": "Register a fallback system", - "gasUsed": 70007 + "gasUsed": 70183 }, { "file": "test/World.t.sol", "test": "testRegisterFallbackSystem", "name": "Register a root fallback system", - "gasUsed": 63500 + "gasUsed": 63676 }, { "file": "test/World.t.sol", "test": "testRegisterFunctionSelector", "name": "Register a function selector", - "gasUsed": 90601 + "gasUsed": 90777 }, { "file": "test/World.t.sol", "test": "testRegisterNamespace", "name": "Register a new namespace", - "gasUsed": 139839 + "gasUsed": 140015 }, { "file": "test/World.t.sol", "test": "testRegisterRootFunctionSelector", "name": "Register a root function selector", - "gasUsed": 79411 + "gasUsed": 79587 }, { "file": "test/World.t.sol", "test": "testRegisterTable", "name": "Register a new table in the namespace", - "gasUsed": 649941 + "gasUsed": 650174 }, { "file": "test/World.t.sol", "test": "testSetField", "name": "Write data to a table field", - "gasUsed": 40733 + "gasUsed": 40722 }, { "file": "test/World.t.sol", "test": "testSetRecord", "name": "Write data to the table", - "gasUsed": 39596 + "gasUsed": 39585 }, { "file": "test/WorldDynamicUpdate.t.sol", "test": "testPopFromField", "name": "pop 1 address (cold)", - "gasUsed": 31108 + "gasUsed": 31097 }, { "file": "test/WorldDynamicUpdate.t.sol", "test": "testPopFromField", "name": "pop 1 address (warm)", - "gasUsed": 17898 + "gasUsed": 17887 }, { "file": "test/WorldDynamicUpdate.t.sol", "test": "testUpdateInField", "name": "updateInField 1 item (cold)", - "gasUsed": 33520 + "gasUsed": 33509 }, { "file": "test/WorldDynamicUpdate.t.sol", "test": "testUpdateInField", "name": "updateInField 1 item (warm)", - "gasUsed": 20724 + "gasUsed": 20713 } ] diff --git a/packages/world/src/Call.sol b/packages/world/src/Call.sol deleted file mode 100644 index 659129072c..0000000000 --- a/packages/world/src/Call.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.0; - -import { ResourceSelector } from "./ResourceSelector.sol"; - -import { FunctionSelectors } from "./modules/core/tables/FunctionSelectors.sol"; -import { Systems } from "./modules/core/tables/Systems.sol"; - -library Call { - /** - * Call a contract with delegatecall/call and append the given msgSender to the calldata. - * If the call is successfall, return the returndata as bytes memory. - * Else, forward the error (with a revert) - */ - function withSender( - address msgSender, - address target, - bytes memory funcSelectorAndArgs, - bool delegate, - uint256 value - ) internal returns (bytes memory) { - // Append msg.sender to the calldata - bytes memory callData = abi.encodePacked(funcSelectorAndArgs, msgSender); - - // Call the target using `delegatecall` or `call` - (bool success, bytes memory data) = delegate - ? target.delegatecall(callData) // root system - : target.call{ value: value }(callData); // non-root system - - // Forward returned data if the call succeeded - if (success) return data; - - // Forward error if the call failed - assembly { - // data+32 is a pointer to the error message, mload(data) is the length of the error message - revert(add(data, 0x20), mload(data)) - } - } -} diff --git a/packages/world/src/System.sol b/packages/world/src/System.sol index aa1bfd0ebf..ff3769fa67 100644 --- a/packages/world/src/System.sol +++ b/packages/world/src/System.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; -import { WorldContext } from "./WorldContext.sol"; +import { WorldContextConsumer } from "./WorldContext.sol"; -// For now System is just an alias for `WorldContext`, +// For now System is just an alias for `WorldContextConsumer`, // but we might add more default functionality in the future. -contract System is WorldContext { +contract System is WorldContextConsumer { } diff --git a/packages/world/src/SystemCall.sol b/packages/world/src/SystemCall.sol new file mode 100644 index 0000000000..a573c6947c --- /dev/null +++ b/packages/world/src/SystemCall.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import { ResourceSelector } from "./ResourceSelector.sol"; +import { WorldContextProvider } from "./WorldContext.sol"; +import { AccessControl } from "./AccessControl.sol"; +import { ResourceSelector } from "./ResourceSelector.sol"; +import { ROOT_NAMESPACE } from "./constants.sol"; +import { WorldContextProvider } from "./WorldContext.sol"; +import { revertWithBytes } from "./revertWithBytes.sol"; + +import { IWorldErrors } from "./interfaces/IWorldErrors.sol"; +import { ISystemHook } from "./interfaces/ISystemHook.sol"; + +import { FunctionSelectors } from "./modules/core/tables/FunctionSelectors.sol"; +import { Systems } from "./modules/core/tables/Systems.sol"; +import { SystemHooks } from "./modules/core/tables/SystemHooks.sol"; + +library SystemCall { + using ResourceSelector for bytes32; + + /** + * Calls a system via its resource selector and perform access control checks. + * Does not revert if the call fails, but returns a `success` flag along with the returndata. + */ + function call( + address caller, + bytes32 resourceSelector, + bytes memory funcSelectorAndArgs, + uint256 value + ) internal returns (bool success, bytes memory data) { + // Load the system data + (address systemAddress, bool publicAccess) = Systems.get(resourceSelector); + + // Check if the system exists + if (systemAddress == address(0)) revert IWorldErrors.ResourceNotFound(resourceSelector.toString()); + + // Allow access if the system is public or the caller has access to the namespace or name + if (!publicAccess) AccessControl.requireAccess(resourceSelector, caller); + + // Call the system and forward any return data + (success, data) = resourceSelector.getNamespace() == ROOT_NAMESPACE // Use delegatecall for root systems (= registered in the root namespace) + ? WorldContextProvider.delegatecallWithContext({ + msgSender: caller, + target: systemAddress, + funcSelectorAndArgs: funcSelectorAndArgs + }) + : WorldContextProvider.callWithContext({ + msgSender: caller, + target: systemAddress, + funcSelectorAndArgs: funcSelectorAndArgs, + value: value + }); + } + + /** + * Calls a system via its resource selector, perform access control checks and trigger hooks registered for the system. + * Does not revert if the call fails, but returns a `success` flag along with the returndata. + */ + function callWithHooks( + address caller, + bytes32 resourceSelector, + bytes memory funcSelectorAndArgs, + uint256 value + ) internal returns (bool success, bytes memory data) { + // Get system hooks + address[] memory hooks = SystemHooks.get(resourceSelector); + + // Call onBeforeCallSystem hooks (before calling the system) + for (uint256 i; i < hooks.length; i++) { + ISystemHook hook = ISystemHook(hooks[i]); + hook.onBeforeCallSystem(caller, resourceSelector, funcSelectorAndArgs); + } + + // Call the system and forward any return data + (success, data) = call(caller, resourceSelector, funcSelectorAndArgs, value); + + // Call onAfterCallSystem hooks (after calling the system) + for (uint256 i; i < hooks.length; i++) { + ISystemHook hook = ISystemHook(hooks[i]); + hook.onAfterCallSystem(caller, resourceSelector, funcSelectorAndArgs); + } + } + + /** + * Calls a system via its resource selector, perform access control checks and trigger hooks registered for the system. + * Reverts if the call fails. + */ + function callWithHooksOrRevert( + address caller, + bytes32 resourceSelector, + bytes memory funcSelectorAndArgs, + uint256 value + ) internal returns (bytes memory data) { + (bool success, bytes memory returnData) = callWithHooks(caller, resourceSelector, funcSelectorAndArgs, value); + if (!success) revertWithBytes(returnData); + return returnData; + } +} diff --git a/packages/world/src/World.sol b/packages/world/src/World.sol index da03421008..9870fd82bc 100644 --- a/packages/world/src/World.sol +++ b/packages/world/src/World.sol @@ -11,7 +11,9 @@ import { System } from "./System.sol"; import { ResourceSelector } from "./ResourceSelector.sol"; import { ROOT_NAMESPACE, ROOT_NAME } from "./constants.sol"; import { AccessControl } from "./AccessControl.sol"; -import { Call } from "./Call.sol"; +import { SystemCall } from "./SystemCall.sol"; +import { WorldContextProvider } from "./WorldContext.sol"; +import { revertWithBytes } from "./revertWithBytes.sol"; import { NamespaceOwner } from "./tables/NamespaceOwner.sol"; import { InstalledModules } from "./tables/InstalledModules.sol"; @@ -47,12 +49,10 @@ contract World is StoreRead, IStoreData, IWorldKernel { function installRootModule(IModule module, bytes memory args) public { AccessControl.requireOwnerOrSelf(ROOT_NAMESPACE, msg.sender); - Call.withSender({ + WorldContextProvider.delegatecallWithContextOrRevert({ msgSender: msg.sender, target: address(module), - funcSelectorAndArgs: abi.encodeWithSelector(IModule.install.selector, args), - delegate: true, // The module is delegate called so it can edit any table - value: 0 + funcSelectorAndArgs: abi.encodeWithSelector(IModule.install.selector, args) }); // Register the module in the InstalledModules table @@ -176,50 +176,7 @@ contract World is StoreRead, IStoreData, IWorldKernel { bytes32 resourceSelector, bytes memory funcSelectorAndArgs ) external payable virtual returns (bytes memory) { - return _call(resourceSelector, funcSelectorAndArgs, msg.value); - } - - /** - * Call the system at the given namespace and name and pass the given value. - * If the system is not public, the caller must have access to the namespace or name. - */ - function _call( - bytes32 resourceSelector, - bytes memory funcSelectorAndArgs, - uint256 value - ) internal virtual returns (bytes memory data) { - // Load the system data - (address systemAddress, bool publicAccess) = Systems.get(resourceSelector); - - // Check if the system exists - if (systemAddress == address(0)) revert ResourceNotFound(resourceSelector.toString()); - - // Allow access if the system is public or the caller has access to the namespace or name - if (!publicAccess) AccessControl.requireAccess(resourceSelector, msg.sender); - - // Get system hooks - address[] memory hooks = SystemHooks.get(resourceSelector); - - // Call onBeforeCallSystem hooks (before calling the system) - for (uint256 i; i < hooks.length; i++) { - ISystemHook hook = ISystemHook(hooks[i]); - hook.onBeforeCallSystem(msg.sender, systemAddress, funcSelectorAndArgs); - } - - // Call the system and forward any return data - data = Call.withSender({ - msgSender: msg.sender, - target: systemAddress, - funcSelectorAndArgs: funcSelectorAndArgs, - delegate: resourceSelector.getNamespace() == ROOT_NAMESPACE, // Use delegatecall for root systems (= registered in the root namespace) - value: value - }); - - // Call onAfterCallSystem hooks (after calling the system) - for (uint256 i; i < hooks.length; i++) { - ISystemHook hook = ISystemHook(hooks[i]); - hook.onAfterCallSystem(msg.sender, systemAddress, funcSelectorAndArgs); - } + return SystemCall.callWithHooksOrRevert(msg.sender, resourceSelector, funcSelectorAndArgs, msg.value); } /************************************************************************ @@ -244,8 +201,10 @@ contract World is StoreRead, IStoreData, IWorldKernel { // Replace function selector in the calldata with the system function selector bytes memory callData = Bytes.setBytes4(msg.data, 0, systemFunctionSelector); - // Call the function and forward the calldata and returndata - bytes memory returnData = _call(resourceSelector, callData, msg.value); + // Call the function and forward the call data + bytes memory returnData = SystemCall.callWithHooksOrRevert(msg.sender, resourceSelector, callData, msg.value); + + // If the call was successful, return the return data assembly { return(add(returnData, 0x20), mload(returnData)) } diff --git a/packages/world/src/WorldContext.sol b/packages/world/src/WorldContext.sol index 1d5f6c00ab..ff5bd964c5 100644 --- a/packages/world/src/WorldContext.sol +++ b/packages/world/src/WorldContext.sol @@ -2,10 +2,11 @@ pragma solidity >=0.8.0; import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; +import { revertWithBytes } from "./revertWithBytes.sol"; // Similar to https://eips.ethereum.org/EIPS/eip-2771, but any contract can be the trusted forwarder. // This should only be used for contracts without own storage, like Systems. -abstract contract WorldContext { +abstract contract WorldContextConsumer { // Extract the trusted msg.sender value appended to the calldata function _msgSender() internal view returns (address sender) { assembly { @@ -19,3 +20,50 @@ abstract contract WorldContext { return StoreSwitch.getStoreAddress(); } } + +/** + * Simple utility function to call a contract and append the msg.sender to the calldata (to be consumed by WorldContextConsumer) + */ +library WorldContextProvider { + function appendContext(bytes memory funcSelectorAndArgs, address msgSender) internal pure returns (bytes memory) { + return abi.encodePacked(funcSelectorAndArgs, msgSender); + } + + function callWithContext( + address target, + bytes memory funcSelectorAndArgs, + address msgSender, + uint256 value + ) internal returns (bool success, bytes memory data) { + (success, data) = target.call{ value: value }(appendContext(funcSelectorAndArgs, msgSender)); + } + + function delegatecallWithContext( + address target, + bytes memory funcSelectorAndArgs, + address msgSender + ) internal returns (bool success, bytes memory data) { + (success, data) = target.delegatecall(appendContext(funcSelectorAndArgs, msgSender)); + } + + function callWithContextOrRevert( + address target, + bytes memory funcSelectorAndArgs, + address msgSender, + uint256 value + ) internal returns (bytes memory data) { + (bool success, bytes memory _data) = callWithContext(target, funcSelectorAndArgs, msgSender, value); + if (!success) revertWithBytes(_data); + return _data; + } + + function delegatecallWithContextOrRevert( + address target, + bytes memory funcSelectorAndArgs, + address msgSender + ) internal returns (bytes memory data) { + (bool success, bytes memory _data) = delegatecallWithContext(target, funcSelectorAndArgs, msgSender); + if (!success) revertWithBytes(_data); + return _data; + } +} diff --git a/packages/world/src/interfaces/ISystemHook.sol b/packages/world/src/interfaces/ISystemHook.sol index 6d7b1b8147..3810f571a6 100644 --- a/packages/world/src/interfaces/ISystemHook.sol +++ b/packages/world/src/interfaces/ISystemHook.sol @@ -2,7 +2,7 @@ pragma solidity >=0.8.0; interface ISystemHook { - function onBeforeCallSystem(address msgSender, address systemAddress, bytes memory funcSelectorAndArgs) external; + function onBeforeCallSystem(address msgSender, bytes32 resourceSelector, bytes memory funcSelectorAndArgs) external; - function onAfterCallSystem(address msgSender, address systemAddress, bytes memory funcSelectorAndArgs) external; + function onAfterCallSystem(address msgSender, bytes32 resourceSelector, bytes memory funcSelectorAndArgs) external; } diff --git a/packages/world/src/interfaces/IWorldKernel.sol b/packages/world/src/interfaces/IWorldKernel.sol index 49763c2170..d4ac2e6f13 100644 --- a/packages/world/src/interfaces/IWorldKernel.sol +++ b/packages/world/src/interfaces/IWorldKernel.sol @@ -16,8 +16,8 @@ interface IWorldModuleInstallation { interface IWorldCall { /** - * Call the system at the given resource selector (namespace + name) - * If the system is not public, the caller must have access to the namespace or name. + * Call the system at the given resourceSelector. + * 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); } diff --git a/packages/world/src/interfaces/IWorldRegistrationSystem.sol b/packages/world/src/interfaces/IWorldRegistrationSystem.sol index bed728265a..8409aadd6f 100644 --- a/packages/world/src/interfaces/IWorldRegistrationSystem.sol +++ b/packages/world/src/interfaces/IWorldRegistrationSystem.sol @@ -4,14 +4,14 @@ pragma solidity >=0.8.0; /* Autogenerated file. Do not edit manually. */ import { ISystemHook } from "./ISystemHook.sol"; -import { System } from "./../System.sol"; +import { WorldContextConsumer } from "./../WorldContext.sol"; interface IWorldRegistrationSystem { function registerNamespace(bytes16 namespace) external; function registerSystemHook(bytes32 resourceSelector, ISystemHook hook) external; - function registerSystem(bytes32 resourceSelector, System system, bool publicAccess) external; + function registerSystem(bytes32 resourceSelector, WorldContextConsumer system, bool publicAccess) external; function registerFunctionSelector( bytes32 resourceSelector, diff --git a/packages/world/src/modules/core/CoreModule.sol b/packages/world/src/modules/core/CoreModule.sol index 88ebc319b9..e1b820aef2 100644 --- a/packages/world/src/modules/core/CoreModule.sol +++ b/packages/world/src/modules/core/CoreModule.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; -import { Call } from "../../Call.sol"; +import { WorldContextProvider } from "../../WorldContext.sol"; import { ROOT_NAMESPACE } from "../../constants.sol"; -import { WorldContext } from "../../WorldContext.sol"; +import { WorldContextConsumer } from "../../WorldContext.sol"; import { Resource } from "../../Types.sol"; import { IBaseWorld } from "../../interfaces/IBaseWorld.sol"; @@ -38,7 +38,7 @@ import { EphemeralRecordSystem } from "./implementations/EphemeralRecordSystem.s * This module is required to be delegatecalled (via `World.registerRootSystem`), * because it needs to install root tables, systems and function selectors. */ -contract CoreModule is IModule, WorldContext { +contract CoreModule is IModule, WorldContextConsumer { // Since the CoreSystem only exists once per World and writes to // known tables, we can deploy it once and register it in multiple Worlds. address immutable coreSystem = address(new CoreSystem()); @@ -74,11 +74,9 @@ contract CoreModule is IModule, WorldContext { */ function _registerCoreSystem() internal { // Use the CoreSystem's `registerSystem` implementation to register itself on the World. - Call.withSender({ + WorldContextProvider.delegatecallWithContextOrRevert({ msgSender: _msgSender(), target: coreSystem, - delegate: true, - value: 0, funcSelectorAndArgs: abi.encodeWithSelector( WorldRegistrationSystem.registerSystem.selector, ResourceSelector.from(ROOT_NAMESPACE, CORE_SYSTEM_NAME), @@ -114,11 +112,9 @@ contract CoreModule is IModule, WorldContext { for (uint256 i = 0; i < functionSelectors.length; i++) { // Use the CoreSystem's `registerRootFunctionSelector` to register the // root function selectors in the World. - Call.withSender({ + WorldContextProvider.delegatecallWithContextOrRevert({ msgSender: _msgSender(), target: coreSystem, - delegate: true, - value: 0, funcSelectorAndArgs: abi.encodeWithSelector( WorldRegistrationSystem.registerRootFunctionSelector.selector, ResourceSelector.from(ROOT_NAMESPACE, CORE_SYSTEM_NAME), diff --git a/packages/world/src/modules/core/implementations/AccessManagementSystem.sol b/packages/world/src/modules/core/implementations/AccessManagementSystem.sol index df10f158e0..3fd2c695d8 100644 --- a/packages/world/src/modules/core/implementations/AccessManagementSystem.sol +++ b/packages/world/src/modules/core/implementations/AccessManagementSystem.sol @@ -5,7 +5,6 @@ import { IModule } from "../../../interfaces/IModule.sol"; import { System } from "../../../System.sol"; import { AccessControl } from "../../../AccessControl.sol"; import { ResourceSelector } from "../../../ResourceSelector.sol"; -import { Call } from "../../../Call.sol"; import { ResourceAccess } from "../../../tables/ResourceAccess.sol"; import { InstalledModules } from "../../../tables/InstalledModules.sol"; diff --git a/packages/world/src/modules/core/implementations/EphemeralRecordSystem.sol b/packages/world/src/modules/core/implementations/EphemeralRecordSystem.sol index f49bb8996b..d52278520c 100644 --- a/packages/world/src/modules/core/implementations/EphemeralRecordSystem.sol +++ b/packages/world/src/modules/core/implementations/EphemeralRecordSystem.sol @@ -3,13 +3,9 @@ pragma solidity >=0.8.0; import { IStoreEphemeral } from "@latticexyz/store/src/IStore.sol"; import { Schema } from "@latticexyz/store/src/Schema.sol"; -import { IModule } from "../../../interfaces/IModule.sol"; import { System } from "../../../System.sol"; import { ResourceSelector } from "../../../ResourceSelector.sol"; import { AccessControl } from "../../../AccessControl.sol"; -import { Call } from "../../../Call.sol"; -import { ResourceAccess } from "../../../tables/ResourceAccess.sol"; -import { InstalledModules } from "../../../tables/InstalledModules.sol"; import { StoreCore } from "@latticexyz/store/src/StoreCore.sol"; contract EphemeralRecordSystem is IStoreEphemeral, System { diff --git a/packages/world/src/modules/core/implementations/ModuleInstallationSystem.sol b/packages/world/src/modules/core/implementations/ModuleInstallationSystem.sol index 8639ece1a5..2e55da8b57 100644 --- a/packages/world/src/modules/core/implementations/ModuleInstallationSystem.sol +++ b/packages/world/src/modules/core/implementations/ModuleInstallationSystem.sol @@ -4,7 +4,7 @@ pragma solidity >=0.8.0; import { IModule } from "../../../interfaces/IModule.sol"; import { System } from "../../../System.sol"; import { AccessControl } from "../../../AccessControl.sol"; -import { Call } from "../../../Call.sol"; +import { WorldContextProvider } from "../../../WorldContext.sol"; import { ResourceAccess } from "../../../tables/ResourceAccess.sol"; import { InstalledModules } from "../../../tables/InstalledModules.sol"; @@ -16,11 +16,10 @@ contract ModuleInstallationSystem is System { * Install the given module at the given namespace in the World. */ function installModule(IModule module, bytes memory args) public { - Call.withSender({ + WorldContextProvider.callWithContextOrRevert({ msgSender: _msgSender(), target: address(module), funcSelectorAndArgs: abi.encodeWithSelector(IModule.install.selector, args), - delegate: false, value: 0 }); diff --git a/packages/world/src/modules/core/implementations/StoreRegistrationSystem.sol b/packages/world/src/modules/core/implementations/StoreRegistrationSystem.sol index 7486549f6a..c832ace089 100644 --- a/packages/world/src/modules/core/implementations/StoreRegistrationSystem.sol +++ b/packages/world/src/modules/core/implementations/StoreRegistrationSystem.sol @@ -10,7 +10,7 @@ import { ResourceSelector } from "../../../ResourceSelector.sol"; import { Resource } from "../../../Types.sol"; import { ROOT_NAMESPACE, ROOT_NAME } from "../../../constants.sol"; import { AccessControl } from "../../../AccessControl.sol"; -import { Call } from "../../../Call.sol"; +import { WorldContextProvider } from "../../../WorldContext.sol"; import { NamespaceOwner } from "../../../tables/NamespaceOwner.sol"; import { ResourceAccess } from "../../../tables/ResourceAccess.sol"; import { ISystemHook } from "../../../interfaces/ISystemHook.sol"; @@ -51,12 +51,10 @@ contract StoreRegistrationSystem is System, IWorldErrors { // We can't call IBaseWorld(this).registerSchema directly because it would be handled like // an external call, so msg.sender would be the address of the World contract (address systemAddress, ) = Systems.get(ResourceSelector.from(ROOT_NAMESPACE, CORE_SYSTEM_NAME)); - Call.withSender({ + WorldContextProvider.delegatecallWithContextOrRevert({ msgSender: _msgSender(), target: systemAddress, - funcSelectorAndArgs: abi.encodeWithSelector(WorldRegistrationSystem.registerNamespace.selector, namespace), - delegate: true, - value: 0 + funcSelectorAndArgs: abi.encodeWithSelector(WorldRegistrationSystem.registerNamespace.selector, namespace) }); } else { // otherwise require caller to own the namespace diff --git a/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol b/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol index af57701497..fe0251b54c 100644 --- a/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol +++ b/packages/world/src/modules/core/implementations/WorldRegistrationSystem.sol @@ -2,8 +2,10 @@ pragma solidity >=0.8.0; import { System } from "../../../System.sol"; +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 { AccessControl } from "../../../AccessControl.sol"; import { NamespaceOwner } from "../../../tables/NamespaceOwner.sol"; @@ -60,7 +62,7 @@ contract WorldRegistrationSystem is System, IWorldErrors { * The system is granted access to its namespace, so it can write to any table in the same namespace. * If publicAccess is true, no access control check is performed for calling the system. */ - function registerSystem(bytes32 resourceSelector, System system, bool publicAccess) public virtual { + function registerSystem(bytes32 resourceSelector, WorldContextConsumer system, bool publicAccess) public virtual { // Require the name to not be the namespace's root name if (resourceSelector.getName() == ROOT_NAME) revert InvalidSelector(resourceSelector.toString()); diff --git a/packages/world/src/modules/keysintable/KeysInTableModule.sol b/packages/world/src/modules/keysintable/KeysInTableModule.sol index a80e7de6c2..8c8506eabf 100644 --- a/packages/world/src/modules/keysintable/KeysInTableModule.sol +++ b/packages/world/src/modules/keysintable/KeysInTableModule.sol @@ -7,7 +7,7 @@ import { Resource } from "../../Types.sol"; import { IBaseWorld } from "../../interfaces/IBaseWorld.sol"; import { IModule } from "../../interfaces/IModule.sol"; -import { WorldContext } from "../../WorldContext.sol"; +import { WorldContextConsumer } from "../../WorldContext.sol"; import { ResourceSelector } from "../../ResourceSelector.sol"; import { KeysInTableHook } from "./KeysInTableHook.sol"; @@ -24,7 +24,7 @@ import { UsedKeysIndex, UsedKeysIndexTableId } from "./tables/UsedKeysIndex.sol" * Note: this module currently expects to be `delegatecalled` via World.installRootModule. * Support for installing it via `World.installModule` depends on `World.callFrom` being implemented. */ -contract KeysInTableModule is IModule, WorldContext { +contract KeysInTableModule is IModule, WorldContextConsumer { using ResourceSelector for bytes32; // The KeysInTableHook is deployed once and infers the target table id diff --git a/packages/world/src/modules/keyswithvalue/KeysWithValueModule.sol b/packages/world/src/modules/keyswithvalue/KeysWithValueModule.sol index 575ae3238f..9bfb076d2a 100644 --- a/packages/world/src/modules/keyswithvalue/KeysWithValueModule.sol +++ b/packages/world/src/modules/keyswithvalue/KeysWithValueModule.sol @@ -6,7 +6,7 @@ import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; import { IBaseWorld } from "../../interfaces/IBaseWorld.sol"; import { IModule } from "../../interfaces/IModule.sol"; -import { WorldContext } from "../../WorldContext.sol"; +import { WorldContextConsumer } from "../../WorldContext.sol"; import { ResourceSelector } from "../../ResourceSelector.sol"; import { MODULE_NAMESPACE } from "./constants.sol"; @@ -25,7 +25,7 @@ import { getTargetTableSelector } from "../utils/getTargetTableSelector.sol"; * Note: this module currently expects to be `delegatecalled` via World.installRootModule. * Support for installing it via `World.installModule` depends on `World.callFrom` being implemented. */ -contract KeysWithValueModule is IModule, WorldContext { +contract KeysWithValueModule is IModule, WorldContextConsumer { using ResourceSelector for bytes32; // The KeysWithValueHook is deployed once and infers the target table id diff --git a/packages/world/src/modules/uniqueentity/UniqueEntityModule.sol b/packages/world/src/modules/uniqueentity/UniqueEntityModule.sol index b817e4a531..cfff61f6bc 100644 --- a/packages/world/src/modules/uniqueentity/UniqueEntityModule.sol +++ b/packages/world/src/modules/uniqueentity/UniqueEntityModule.sol @@ -4,7 +4,7 @@ pragma solidity >=0.8.0; import { IBaseWorld } from "../../interfaces/IBaseWorld.sol"; import { IModule } from "../../interfaces/IModule.sol"; -import { WorldContext } from "../../WorldContext.sol"; +import { WorldContextConsumer } from "../../WorldContext.sol"; import { ResourceSelector } from "../../ResourceSelector.sol"; import { UniqueEntity } from "./tables/UniqueEntity.sol"; @@ -16,7 +16,7 @@ import { NAMESPACE, MODULE_NAME, SYSTEM_NAME, TABLE_NAME } from "./constants.sol * This module creates a table that stores a nonce, and * a public system that returns an incremented nonce each time. */ -contract UniqueEntityModule is IModule, WorldContext { +contract UniqueEntityModule is IModule, WorldContextConsumer { // Since the UniqueEntitySystem only exists once per World and writes to // known tables, we can deploy it once and register it in multiple Worlds. UniqueEntitySystem immutable uniqueEntitySystem = new UniqueEntitySystem(); diff --git a/packages/world/src/revertWithBytes.sol b/packages/world/src/revertWithBytes.sol new file mode 100644 index 0000000000..318ef91a8e --- /dev/null +++ b/packages/world/src/revertWithBytes.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +/** + * Utility function to revert with raw bytes (eg. coming from a low level call or from a previously encoded error) + */ +function revertWithBytes(bytes memory reason) pure { + assembly { + // reason+32 is a pointer to the error message, mload(reason) is the length of the error message + revert(add(reason, 0x20), mload(reason)) + } +} diff --git a/packages/world/test/RevertWithBytes.t.sol b/packages/world/test/RevertWithBytes.t.sol new file mode 100644 index 0000000000..e9bda37e30 --- /dev/null +++ b/packages/world/test/RevertWithBytes.t.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import { Test } from "forge-std/Test.sol"; +import { revertWithBytes } from "../src/revertWithBytes.sol"; + +contract RevertWithBytesTest is Test { + error SomeError(uint256 someValue, string someString); + + function testRegularRevert() public { + vm.expectRevert(abi.encodeWithSelector(SomeError.selector, 1, "test")); + revert SomeError(1, "test"); + } + + function testRevertWithBytes() public { + vm.expectRevert(abi.encodeWithSelector(SomeError.selector, 1, "test")); + revertWithBytes(abi.encodeWithSelector(SomeError.selector, 1, "test")); + } +} diff --git a/packages/world/test/Utils.t.sol b/packages/world/test/Utils.t.sol index 55d066d32c..6a9aa2895d 100644 --- a/packages/world/test/Utils.t.sol +++ b/packages/world/test/Utils.t.sol @@ -21,6 +21,8 @@ contract UtilsTest is Test { using ResourceSelector for bytes32; IBaseWorld internal world; + error SomeError(uint256 someValue, string someString); + function setUp() public { world = IBaseWorld(address(new World())); world.installRootModule(new CoreModule(), new bytes(0)); diff --git a/packages/world/test/World.t.sol b/packages/world/test/World.t.sol index 04b36e064e..8141b093b9 100644 --- a/packages/world/test/World.t.sol +++ b/packages/world/test/World.t.sol @@ -141,12 +141,12 @@ contract WorldTestTableHook is IStoreHook { contract WorldTestSystemHook is ISystemHook { event SystemHookCalled(bytes data); - function onBeforeCallSystem(address msgSender, address systemAddress, bytes memory funcSelectorAndArgs) public { - emit SystemHookCalled(abi.encode("before", msgSender, systemAddress, funcSelectorAndArgs)); + function onBeforeCallSystem(address msgSender, bytes32 resourceSelector, bytes memory funcSelectorAndArgs) public { + emit SystemHookCalled(abi.encode("before", msgSender, resourceSelector, funcSelectorAndArgs)); } - function onAfterCallSystem(address msgSender, address systemAddress, bytes memory funcSelectorAndArgs) public { - emit SystemHookCalled(abi.encode("after", msgSender, systemAddress, funcSelectorAndArgs)); + function onAfterCallSystem(address msgSender, bytes32 resourceSelector, bytes memory funcSelectorAndArgs) public { + emit SystemHookCalled(abi.encode("after", msgSender, resourceSelector, funcSelectorAndArgs)); } } @@ -518,7 +518,9 @@ contract WorldTest is Test, GasReporter { world.registerSystem(resourceSelector, system, false); // Call a system function without arguments via the World + startGasReport("call a system via the World"); bytes memory result = world.call(resourceSelector, abi.encodeWithSelector(WorldTestSystem.msgSender.selector)); + endGasReport(); // Expect the system to have received the caller's address assertEq(address(uint160(uint256(bytes32(result)))), address(this)); @@ -602,30 +604,30 @@ contract WorldTest is Test, GasReporter { } function testRegisterSystemHook() public { - bytes32 tableId = ResourceSelector.from("namespace", "testTable"); + bytes32 systemId = ResourceSelector.from("namespace", "testTable"); // Register a new system WorldTestSystem system = new WorldTestSystem(); - world.registerSystem(tableId, system, false); + world.registerSystem(systemId, system, false); // Register a new hook ISystemHook systemHook = new WorldTestSystemHook(); - world.registerSystemHook(tableId, systemHook); + world.registerSystemHook(systemId, systemHook); bytes memory funcSelectorAndArgs = abi.encodeWithSelector(bytes4(keccak256("fallbackselector"))); // Expect the hooks to be called in correct order vm.expectEmit(true, true, true, true); - emit SystemHookCalled(abi.encode("before", address(this), address(system), funcSelectorAndArgs)); + emit SystemHookCalled(abi.encode("before", address(this), systemId, funcSelectorAndArgs)); vm.expectEmit(true, true, true, true); emit WorldTestSystemLog("fallback"); vm.expectEmit(true, true, true, true); - emit SystemHookCalled(abi.encode("after", address(this), address(system), funcSelectorAndArgs)); + emit SystemHookCalled(abi.encode("after", address(this), systemId, funcSelectorAndArgs)); // Call a system fallback function without arguments via the World - world.call(tableId, funcSelectorAndArgs); + world.call(systemId, funcSelectorAndArgs); } function testWriteRootSystem() public {