From ca226bb6bed123c6742e8cb70a8f2778b880fccf Mon Sep 17 00:00:00 2001
From: yonada <fraserdscott@gmail.com>
Date: Fri, 10 Nov 2023 18:12:13 +0300
Subject: [PATCH] feat(world-modules): system bound delegation control (#1885)

Co-authored-by: alvarius <alvarius@lattice.xyz>
Co-authored-by: Kevin Ingersoll <kingersoll@gmail.com>
---
 packages/world-modules/gas-report.json        |  16 +-
 packages/world-modules/mud.config.ts          |  11 +
 packages/world-modules/src/index.sol          |   1 +
 .../StandardDelegationsModule.sol             |  11 +-
 .../SystemboundDelegationControl.sol          |  54 ++++
 .../src/modules/std-delegations/constants.sol |   5 +
 .../tables/SystemboundDelegations.sol         | 290 ++++++++++++++++++
 .../test/StandardDelegationsModule.t.sol      |  40 ++-
 8 files changed, 423 insertions(+), 5 deletions(-)
 create mode 100644 packages/world-modules/src/modules/std-delegations/SystemboundDelegationControl.sol
 create mode 100644 packages/world-modules/src/modules/std-delegations/tables/SystemboundDelegations.sol

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