Skip to content

Commit

Permalink
feat(world-modules): add SystemSwitch util
Browse files Browse the repository at this point in the history
  • Loading branch information
alvrs committed Oct 3, 2023
1 parent f99e889 commit c0fadbb
Show file tree
Hide file tree
Showing 3 changed files with 309 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;

import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol";
import { IBaseWorld } from "@latticexyz/world/src/codegen/interfaces/IBaseWorld.sol";

import { IUniqueEntitySystem } from "../../interfaces/IUniqueEntitySystem.sol";

import { SystemSwitch } from "../utils/SystemSwitch.sol";
import { SYSTEM_ID } from "./constants.sol";

/**
* Increment and get an entity nonce.
*
* Note: this util can only be called within the context of a World (e.g. from a System or Module).
* For usage outside of a World, use the overload that takes an explicit store argument.
*/
function getUniqueEntity() returns (bytes32 uniqueEntity) {
address world = StoreSwitch.getStoreAddress();
return IUniqueEntitySystem(world).uniqueEntity_system_getUniqueEntity();
return abi.decode(SystemSwitch.call(SYSTEM_ID, new bytes(0)), (bytes32));
}

/**
Expand Down
80 changes: 80 additions & 0 deletions packages/world-modules/src/modules/utils/SystemSwitch.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;

import { Hook } from "@latticexyz/store/src/Hook.sol";
import { Bytes } from "@latticexyz/store/src/Bytes.sol";

import { IWorldKernel } from "@latticexyz/world/src/IWorldKernel.sol";
import { ResourceId, WorldResourceIdInstance } from "@latticexyz/world/src/WorldResourceId.sol";
import { WorldContextProviderLib, WorldContextConsumerLib } from "@latticexyz/world/src/WorldContext.sol";
import { AccessControl } from "@latticexyz/world/src/AccessControl.sol";
import { ROOT_NAMESPACE } from "@latticexyz/world/src/constants.sol";
import { revertWithBytes } from "@latticexyz/world/src/revertWithBytes.sol";
import { BEFORE_CALL_SYSTEM, AFTER_CALL_SYSTEM } from "@latticexyz/world/src/systemHookTypes.sol";

import { IWorldErrors } from "@latticexyz/world/src/IWorldErrors.sol";
import { ISystemHook } from "@latticexyz/world/src/ISystemHook.sol";

import { FunctionSelectors } from "@latticexyz/world/src/codegen/tables/FunctionSelectors.sol";
import { Systems } from "@latticexyz/world/src/codegen/tables/Systems.sol";
import { SystemHooks } from "@latticexyz/world/src/codegen/tables/SystemHooks.sol";
import { Balances } from "@latticexyz/world/src/codegen/tables/Balances.sol";

/**
* @title SystemSwitch
* @dev The SystemSwitch library provides functions for interacting with systems from other systems.
*/
library SystemSwitch {
using WorldResourceIdInstance for ResourceId;

/**
* @notice Calls a system identified by its Resource ID.
* @dev Reverts if the system is not found, or if the system call reverts.
* If the call is executed from the root context, the system is called directly via delegatecall.
* Otherwise, the call is executed via an external call to the World contract.
* @param systemId The unique Resource ID of the system being called.
* @param callData The calldata to be executed in the system.
* @return data The return data from the system call.
*/
function call(ResourceId systemId, bytes memory callData) internal returns (bytes memory data) {
address worldAddress = WorldContextConsumerLib._world();

// If we're in the World context, call the system directly via delegatecall
if (address(this) == worldAddress) {
(address systemAddress, ) = Systems.get(systemId);
// Check if the system exists
if (systemAddress == address(0)) revert IWorldErrors.World_ResourceNotFound(systemId, systemId.toString());

(bool success, bytes memory returnData) = WorldContextProviderLib.delegatecallWithContext({
msgSender: WorldContextConsumerLib._msgSender(),
msgValue: WorldContextConsumerLib._msgValue(),
target: systemAddress,
callData: callData
});

if (!success) revertWithBytes(returnData);
return returnData;
}

// Otherwise, call the system via world.call
data = IWorldKernel(worldAddress).call(systemId, callData);
}

/**
* @notice Calls a system via the function selector registered for it in the World contract.
* @dev Reverts if the system is not found, or if the system call reverts.
* If the call is executed from the root context, the system is called directly via delegatecall.
* Otherwise, the call is executed via an external call to the World contract.
* @param callData The world function selector, and call data to be forwarded to the system.
*/
function call(bytes memory callData) internal returns (bytes memory data) {
// Get the systemAddress and systemFunctionSelector from the worldFunctionSelector encoded in the calldata
(ResourceId systemId, bytes4 systemFunctionSelector) = FunctionSelectors.get(bytes4(callData));

// Revert if the function selector is not found
if (ResourceId.unwrap(systemId) == 0) revert IWorldErrors.World_FunctionSelectorNotFound(msg.sig);

// Replace function selector in the calldata with the system function selector, and call the system
return call({ systemId: systemId, callData: Bytes.setBytes4(callData, 0, systemFunctionSelector) });
}
}
225 changes: 225 additions & 0 deletions packages/world-modules/test/SystemSwitch.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;

import { Test } from "forge-std/Test.sol";
import { GasReporter } from "@latticexyz/gas-report/src/GasReporter.sol";

import { System } from "@latticexyz/world/src/System.sol";
import { IBaseWorld } from "@latticexyz/world/src/codegen/interfaces/IBaseWorld.sol";
import { World } from "@latticexyz/world/src/World.sol";
import { CoreModule } from "@latticexyz/world/src/modules/core/CoreModule.sol";
import { ResourceId, WorldResourceIdLib } from "@latticexyz/world/src/WorldResourceId.sol";
import { RESOURCE_SYSTEM } from "@latticexyz/world/src/worldResourceTypes.sol";
import { ROOT_NAMESPACE } from "@latticexyz/world/src/constants.sol";
import { SystemSwitch } from "../src/modules/utils/SystemSwitch.sol";

contract EchoSystem is System {
function msgSender() public view returns (address) {
return _msgSender();
}

function world() public view returns (address) {
return _world();
}

function echo(string memory message) public view returns (string memory) {
return message;
}

function call(ResourceId systemId, bytes memory callData) public returns (bytes memory) {
return SystemSwitch.call(systemId, callData);
}

function callViaSelector(bytes memory callData) public returns (bytes memory) {
return SystemSwitch.call(callData);
}
}

address constant caller = address(4232);

contract SystemSwitchTest is Test, GasReporter {
IBaseWorld world;

EchoSystem systemA;
EchoSystem systemB;
EchoSystem rootSystemA;
EchoSystem rootSystemB;

ResourceId systemAId;
ResourceId systemBId;
ResourceId rootSystemAId;
ResourceId rootSystemBId;

function setUp() public {
// Deploy world
World _world = new World();
_world.initialize(new CoreModule());
world = IBaseWorld(address(_world));

// Deploy systems
systemA = new EchoSystem();
systemB = new EchoSystem();
rootSystemA = new EchoSystem();
rootSystemB = new EchoSystem();

// Encode system IDs
systemAId = WorldResourceIdLib.encode({ typeId: RESOURCE_SYSTEM, namespace: "namespaceA", name: "systemA" });
systemBId = WorldResourceIdLib.encode({ typeId: RESOURCE_SYSTEM, namespace: "namespaceB", name: "systemB" });
rootSystemAId = WorldResourceIdLib.encode({ typeId: RESOURCE_SYSTEM, namespace: ROOT_NAMESPACE, name: "systemA" });
rootSystemBId = WorldResourceIdLib.encode({ typeId: RESOURCE_SYSTEM, namespace: ROOT_NAMESPACE, name: "systemB" });

// Register systems
world.registerSystem(systemAId, systemA, true);
world.registerSystem(systemBId, systemB, true);
world.registerSystem(rootSystemAId, rootSystemA, true);
world.registerSystem(rootSystemBId, rootSystemB, true);
}

function _executeFromRootSystemA(ResourceId systemId, bytes memory callData) public returns (bytes memory) {
return abi.decode(world.call(rootSystemAId, abi.encodeCall(EchoSystem.call, (systemId, callData))), (bytes));
}

function _executeFromSystemA(ResourceId systemId, bytes memory callData) public returns (bytes memory) {
return abi.decode(world.call(systemAId, abi.encodeCall(EchoSystem.call, (systemId, callData))), (bytes));
}

function _executeFromRootSystemA(bytes memory callData) public returns (bytes memory) {
return abi.decode(world.call(rootSystemAId, abi.encodeCall(EchoSystem.callViaSelector, (callData))), (bytes));
}

function _executeFromSystemA(bytes memory callData) public returns (bytes memory) {
return abi.decode(world.call(systemAId, abi.encodeCall(EchoSystem.callViaSelector, (callData))), (bytes));
}

// - ROOT FROM ROOT ---------------------------------------------------------------------------- //

function testCallRootFromRootMsgSender() public {
vm.prank(caller);
bytes memory returnData = _executeFromRootSystemA(rootSystemBId, abi.encodeCall(EchoSystem.msgSender, ()));
assertEq(abi.decode(returnData, (address)), caller);
}

function testCallRootFromRootWorld() public {
vm.prank(caller);
bytes memory returnData = _executeFromRootSystemA(rootSystemBId, abi.encodeCall(EchoSystem.world, ()));
assertEq(abi.decode(returnData, (address)), address(world));
}

function testCallRootFromRootEcho() public {
vm.prank(caller);
bytes memory returnData = _executeFromRootSystemA(rootSystemBId, abi.encodeCall(EchoSystem.echo, ("hello")));
assertEq(abi.decode(returnData, (string)), "hello");
}

function testCallRootFromRootWorldSelector() public {
bytes4 worldFunctionSelector = world.registerRootFunctionSelector(
rootSystemBId,
"echo(string)",
EchoSystem.echo.selector
);
bytes memory callData = abi.encodeWithSelector(worldFunctionSelector, "hello");

vm.prank(caller);
bytes memory returnData = _executeFromRootSystemA(callData);
assertEq(abi.decode(returnData, (string)), "hello");
}

// - ROOT FROM NON ROOT ---------------------------------------------------------------------------- //

function testCallRootFromNonRootMsgSender() public {
vm.prank(caller);
bytes memory returnData = _executeFromSystemA(rootSystemBId, abi.encodeCall(EchoSystem.msgSender, ()));
assertEq(abi.decode(returnData, (address)), address(systemA));
}

function testCallRootFromNonRootWorld() public {
vm.prank(caller);
bytes memory returnData = _executeFromSystemA(rootSystemBId, abi.encodeCall(EchoSystem.world, ()));
assertEq(abi.decode(returnData, (address)), address(world));
}

function testCallRootFromNonRootEcho() public {
vm.prank(caller);
bytes memory returnData = _executeFromSystemA(rootSystemBId, abi.encodeCall(EchoSystem.echo, ("hello")));
assertEq(abi.decode(returnData, (string)), "hello");
}

function testCallRootFromNonRootWorldSelector() public {
bytes4 worldFunctionSelector = world.registerRootFunctionSelector(
rootSystemBId,
"echo(string)",
EchoSystem.echo.selector
);
bytes memory callData = abi.encodeWithSelector(worldFunctionSelector, "hello");

vm.prank(caller);
bytes memory returnData = _executeFromSystemA(callData);
assertEq(abi.decode(returnData, (string)), "hello");
}

// - NON ROOT FROM ROOT ---------------------------------------------------------------------------- //

function testCallNonRootFromRootMsgSender() public {
vm.prank(caller);
bytes memory returnData = _executeFromRootSystemA(systemBId, abi.encodeCall(EchoSystem.msgSender, ()));
assertEq(abi.decode(returnData, (address)), caller);
}

function testNonCallRootFromRootWorld() public {
vm.prank(caller);
bytes memory returnData = _executeFromRootSystemA(systemBId, abi.encodeCall(EchoSystem.world, ()));
assertEq(abi.decode(returnData, (address)), address(world));
}

function testNonCallRootFromRootEcho() public {
vm.prank(caller);
bytes memory returnData = _executeFromRootSystemA(systemBId, abi.encodeCall(EchoSystem.echo, ("hello")));
assertEq(abi.decode(returnData, (string)), "hello");
}

function testNonCallRootFromRootWorldSelector() public {
bytes4 worldFunctionSelector = world.registerRootFunctionSelector(
systemBId,
"echo(string)",
EchoSystem.echo.selector
);
bytes memory callData = abi.encodeWithSelector(worldFunctionSelector, "hello");

vm.prank(caller);
bytes memory returnData = _executeFromRootSystemA(callData);
assertEq(abi.decode(returnData, (string)), "hello");
}

// - NON ROOT FROM NON ROOT ---------------------------------------------------------------------------- //

function testCallNonRootFromNonRootMsgSender() public {
vm.prank(caller);
bytes memory returnData = _executeFromSystemA(systemBId, abi.encodeCall(EchoSystem.msgSender, ()));
assertEq(abi.decode(returnData, (address)), address(systemA));
}

function testNonCallRootFromNonRootWorld() public {
vm.prank(caller);
bytes memory returnData = _executeFromSystemA(systemBId, abi.encodeCall(EchoSystem.world, ()));
assertEq(abi.decode(returnData, (address)), address(world));
}

function testNonCallRootFromNonRootEcho() public {
vm.prank(caller);
bytes memory returnData = _executeFromSystemA(systemBId, abi.encodeCall(EchoSystem.echo, ("hello")));
assertEq(abi.decode(returnData, (string)), "hello");
}

function testNonCallRootFromNonRootWorldSelector() public {
bytes4 worldFunctionSelector = world.registerRootFunctionSelector(
systemBId,
"echo(string)",
EchoSystem.echo.selector
);
bytes memory callData = abi.encodeWithSelector(worldFunctionSelector, "hello");

vm.prank(caller);
bytes memory returnData = _executeFromSystemA(callData);
assertEq(abi.decode(returnData, (string)), "hello");
}
}

0 comments on commit c0fadbb

Please sign in to comment.