projectawakening / world-chain-contracts

https://www.projectawakening.io/
MIT License
10 stars 0 forks source link

Library autogeneration for non-root modules #12

Open MerkleBoy opened 3 months ago

MerkleBoy commented 3 months ago

Below, a proposal for a steamlined way to call non-root Systems in a way that abstracts away the namespace it's deployed onto:

One of the main concern I have with registering some System methods as World-function selectors are the following:

Transforming a System interface into it's world.call(..) equivalent is pretty straightforward, right now it needs to be done by hand, which could lead to mismatching errors if said interfaces have to be updated; so following that logic we need something like mud worldgen or mud tablegen, so that libraries can be auto-generated in one command line.

mud modulegen perhaps ?

Let's study a dummy system DummySystem to see how it plays out :

export default mudConfig({
    namespace: "MySystem_v0",
    systems: {
        DummySystem: {
            name: "DummySystem,
            openAccess: true,
        },
    },
    tables: {
        MyTable: {
            keySchema: {
                a: "uint256",
            }
            valueSchema: {
                b: "uint256",
            },
            storeArgument: true,
            tableIdArgument: true,
        },
    },
});
// DummySystem.sol
contract DummySystem is System {
  function foo(uint256 a, uint256 b) public {
    MyTable.set(_namespace().myTableTableId(), a, b);
  }

  function bar(uint256 a) public returns (uint256 b) {
    return MyTable.get(_namespace().myTableId(), a);
  }
}
// Utils.sol
library Utils {
  using WorldResourceIdInstance for ResourceId;

  function dummySystemId(bytes14 namespace) internal pure returns (ResourceId) {
    return WorldResourceIdLib.encode({ typeId: RESOURCE_SYSTEM, namespace: namespace, name: DUMMY_SYSTEM_NAME });
  }

  function myTableId(bytes14 namespace) internal pure returns (ResourceId) {
    return WorldResourceIdLib.encode({ typeId: RESOURCE_TABLE, namespace: namespace, name: MY_TABLE_NAME });
  }
}
// DummyLib.sol
import { Utils } from "./Utils.sol";

interface IDummySystem {
  function foo(uint256 a, uint256 b) external;
  function bar(uint256 a) external returns (uint256 b);
}

library DummyLib {
  using Utils for bytes14;

  struct World {
    IBaseWorld iface;
    bytes14 namespace;
  }

  function foo(World memory world, uint256 a, uint256 b) internal {
    world.iface.call(world.namespace.dummySystemId(),
      abi.encodeCall(IDummySystem.foo,
        (a, b)
      )
    );
  }

  function bar(World memory world, uint256 a) internal returns (uint256 b) {
    bytes memory result = world.iface.call(world.namespace.dummySystemId(),
      abi.encodeCall(IDummySystem.bar,
        (a)
      )
    );
    return abi.decode(result, (uint256));
  }
}

From there, is is possible to import DummyLib anywhere, and call our DummySystem methods like this:

// DummyTest.t.sol
import { DummyLib } from "../src/DummyLib.sol";

contract DummyTest is Test {
  using Utils for bytes14;   
  using DummyLib for DummyLib.World;
  using WorldResourceIdInstance for ResourceId;

  IBaseWorld baseWorld;
  DummyLib.World dummy;
  DummyModule dummyModule;

  function setup() public {
    // Setting up a Base World and install Dummy System on it through a Module contract
    baseWorld = IBaseWorld(address(new World()));
    baseWorld.initialize(createCoreModule()); // doing some installation stuff, not getting into that it was meant to be sudo code
    DummyModule module = new DummyModule();
    baseWorld.installModule(module, abi.encode(DUMMY_NAMESPACE));
    StoreSwitch.setStoreAddress(address(baseWorld));

    // This is the interesting part
    dummy = DummyLib.World(baseWorld, DUMMY_NAMESPACE);
  }

  function testFoo() public {
    dummy.foo(123, 456); //just works !

    assertEq(MyTable.get(DUMMY_NAMESPACE.myTableId(), 123), 456); // true
  }

  function testBar() public {
    testFoo();
    uint2356 result = dummy.bar(123);
    assertEq(result, 456); // true
  }
}

Granted, it needs a little bit of setting things up, but it's fairly straightforward once that's done.

The issue boils down to this: come up with a code-generation script to make a library out of a given System contract ( or its interface), in the same way as shown above

The biggest advantage of doing this is that the interface for Dummy (or rather, the syntax to call Dummy's methods) becomes decoupled from the namespace it's deployed onto, which makes that system fully reusable. It's just a matter of deploying the module on your namespace, and initializing your DummyLib.World structure with the right IBaseWorld and namespace parameters.

Instead of having to use the worldgen interfaces, which are constructed with an appended namespace to each methods (which breaks it if you decide to re-deploy it on another namespace), this bypasses completely World-level function selectors, since we make a direct call to the related system instead

MerkleBoy commented 3 months ago

posted the issue on latticexyz/mud too since it can be generalized to a MUD feature => Library auto-generation for non-root Modules #2319