Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(world-modules): add ERC20 module #1789

Merged
merged 17 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .changeset/silent-buttons-peel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"@latticexyz/world-modules": minor
---

Added the `ERC20Module` to `@latticexyz/world-modules`.
This module allows the registration of `ERC20` tokens in an existing World.

Important note: this module has not been audited yet, so any production use is discouraged for now.

```solidity
import { PuppetModule } from "@latticexyz/world-modules/src/modules/puppet/PuppetModule.sol";
import { IERC20Mintable } from "@latticexyz/world-modules/src/modules/erc20-puppet/IERC20Mintable.sol";
import { registerERC20 } from "@latticexyz/world-modules/src/modules/erc20-puppet/registerERC20.sol";

// The ERC20 module requires the Puppet module to be installed first
world.installModule(new PuppetModule(), new bytes(0));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we also need to install the ERC20Module here?

I now see below that registerERC20 installs the module


// After the Puppet module is installed, new ERC20 tokens can be registered
IERC20Mintable token = registerERC20(world, "myERC20", ERC20MetadataData({ decimals: 18, name: "Token", symbol: "TKN" }));
```
30 changes: 30 additions & 0 deletions packages/world-modules/gas-report.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,34 @@
[
{
"file": "test/ERC20.t.sol",
"test": "testApprove",
"name": "approve",
"gasUsed": 114329
},
{
"file": "test/ERC20.t.sol",
"test": "testBurn",
"name": "burn",
"gasUsed": 75866
},
{
"file": "test/ERC20.t.sol",
"test": "testMint",
"name": "mint",
"gasUsed": 161705
},
{
"file": "test/ERC20.t.sol",
"test": "testTransfer",
"name": "transfer",
"gasUsed": 92948
},
{
"file": "test/ERC20.t.sol",
"test": "testTransferFrom",
"name": "transferFrom",
"gasUsed": 130250
},
{
"file": "test/KeysInTableModule.t.sol",
"test": "testInstallComposite",
Expand Down
56 changes: 55 additions & 1 deletion packages/world-modules/mud.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,61 @@ export default mudConfig({
},
tableIdArgument: true,
},
/************************************************************************
*
* ERC20 MODULE
*
************************************************************************/
Balances: {
directory: "modules/erc20-puppet/tables",
keySchema: {
account: "address",
},
valueSchema: {
value: "uint256",
},
tableIdArgument: true,
},
Allowances: {
directory: "modules/erc20-puppet/tables",
keySchema: {
account: "address",
spender: "address",
},
valueSchema: {
value: "uint256",
},
tableIdArgument: true,
},
TotalSupply: {
directory: "modules/erc20-puppet/tables",
keySchema: {},
valueSchema: {
totalSupply: "uint256",
},
tableIdArgument: true,
},
Metadata: {
directory: "modules/erc20-puppet/tables",
keySchema: {},
valueSchema: {
decimals: "uint8",
name: "string",
symbol: "string",
},
tableIdArgument: true,
},
ERC20Registry: {
directory: "modules/erc20-puppet/tables",
keySchema: {
namespaceId: "ResourceId",
},
valueSchema: {
erc20Address: "address",
},
tableIdArgument: true,
},
},

excludeSystems: ["UniqueEntitySystem", "PuppetFactorySystem"],
excludeSystems: ["UniqueEntitySystem", "PuppetFactorySystem", "ERC20System"],
});
2 changes: 1 addition & 1 deletion packages/world-modules/remappings.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
ds-test/=node_modules/ds-test/src/
forge-std/=node_modules/forge-std/src/
@latticexyz/=node_modules/@latticexyz/
@latticexyz/=node_modules/@latticexyz/
5 changes: 5 additions & 0 deletions packages/world-modules/src/index.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,8 @@ 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 { PuppetRegistry } from "./modules/puppet/tables/PuppetRegistry.sol";
import { Balances } from "./modules/erc20-puppet/tables/Balances.sol";
import { Allowances } from "./modules/erc20-puppet/tables/Allowances.sol";
import { TotalSupply } from "./modules/erc20-puppet/tables/TotalSupply.sol";
import { Metadata, MetadataData } from "./modules/erc20-puppet/tables/Metadata.sol";
import { ERC20Registry } from "./modules/erc20-puppet/tables/ERC20Registry.sol";
32 changes: 32 additions & 0 deletions packages/world-modules/src/interfaces/IERC20System.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;

/* Autogenerated file. Do not edit manually. */

/**
* @title IERC20System
* @dev This interface is automatically generated from the corresponding system contract. Do not edit manually.
*/
interface IERC20System {
function name() external view returns (string memory);

function symbol() external view returns (string memory);

function decimals() external view returns (uint8);

function totalSupply() external view returns (uint256);

function balanceOf(address account) external view returns (uint256);

function allowance(address owner, address spender) external view returns (uint256);

function transfer(address to, uint256 value) external returns (bool);

function approve(address spender, uint256 value) external returns (bool);

function transferFrom(address from, address to, uint256 value) external returns (bool);

function mint(address account, uint256 value) external;

function burn(address account, uint256 value) external;
}
93 changes: 93 additions & 0 deletions packages/world-modules/src/modules/erc20-puppet/ERC20Module.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;

import { ResourceIds } from "@latticexyz/store/src/codegen/tables/ResourceIds.sol";
import { ResourceId } from "@latticexyz/store/src/ResourceId.sol";
import { Module } from "@latticexyz/world/src/Module.sol";
import { WorldResourceIdLib } from "@latticexyz/world/src/WorldResourceId.sol";
import { IBaseWorld } from "@latticexyz/world/src/codegen/interfaces/IBaseWorld.sol";
import { InstalledModules } from "@latticexyz/world/src/codegen/tables/InstalledModules.sol";

import { Puppet } from "../puppet/Puppet.sol";
import { createPuppet } from "../puppet/createPuppet.sol";
import { MODULE_NAME as PUPPET_MODULE_NAME } from "../puppet/constants.sol";

import { MODULE_NAME, MODULE_NAMESPACE, MODULE_NAMESPACE_ID, ERC20_REGISTRY_TABLE_ID } from "./constants.sol";
import { _allowancesTableId, _balancesTableId, _metadataTableId, _erc20SystemId } from "./utils.sol";
import { ERC20System } from "./ERC20System.sol";

import { ERC20Registry } from "./tables/ERC20Registry.sol";
import { Balances } from "./tables/Balances.sol";
import { Allowances } from "./tables/Allowances.sol";
import { Metadata, MetadataData } from "./tables/Metadata.sol";

contract ERC20Module is Module {
error ERC20Module_InvalidNamespace(bytes14 namespace);

function getName() public pure override returns (bytes16) {
return MODULE_NAME;
}

/**
* Register systems and tables for a new ERC20 token in a given namespace
*/
function _registerERC20(bytes14 namespace) internal {
// Register the tables
Allowances.register(_allowancesTableId(namespace));
Balances.register(_balancesTableId(namespace));
Metadata.register(_metadataTableId(namespace));

// Register a new ERC20System
IBaseWorld(_world()).registerSystem(_erc20SystemId(namespace), new ERC20System(), true);
}

function _requireDependencies() internal view {
// If the PuppetModule is not installed yet, install it
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in a world where install becomes idempotent, can we detect+install the puppet module instead of reverting?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had that at some intermediate state in this PR, but it blew the module's bytecode size way over the limit (because the puppet module's bytecode had to be inlinted to the erc20 module)

Copy link
Member

@holic holic Oct 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh interesting! makes sense to go with this approach then

I wonder if we could eventually get to a place where the install just takes in the CREATE2 address of the module and we expect it to exist on chain (or deployed via our deterministic deployer ahead of world deploy)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree that would be nice

if (InstalledModules.get(PUPPET_MODULE_NAME, keccak256(new bytes(0))) == address(0)) {
Copy link
Member

@holic holic Nov 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

side note/not blocking: since we're starting to use this more, might be nice to lift this into Module as a helper function e.g. isInstalled() or isInstalled(module) and requireNotInstalled()

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revert Module_MissingDependency(string(bytes.concat(PUPPET_MODULE_NAME)));
}
}

function install(bytes memory args) public {
// Require the module to not be installed with these args yet
if (InstalledModules.get(MODULE_NAME, keccak256(args)) != address(0)) {
revert Module_AlreadyInstalled();
}

// Extract args
(bytes14 namespace, MetadataData memory metadata) = abi.decode(args, (bytes14, MetadataData));

// Require the namespace to not be the module's namespace
if (namespace == MODULE_NAMESPACE) {
revert ERC20Module_InvalidNamespace(namespace);
}

// Require dependencies
_requireDependencies();

// Register the ERC20 tables and system
_registerERC20(namespace);

// Initialize the Metadata
Metadata.set(_metadataTableId(namespace), metadata);

// Deploy and register the ERC20 puppet.
IBaseWorld world = IBaseWorld(_world());
ResourceId erc20SystemId = _erc20SystemId(namespace);
address puppet = createPuppet(world, erc20SystemId);

// Transfer ownership of the namespace to the caller
ResourceId namespaceId = WorldResourceIdLib.encodeNamespace(namespace);
world.transferOwnership(namespaceId, _msgSender());

// Register the ERC20 in the ERC20Registry
if (!ResourceIds.getExists(ERC20_REGISTRY_TABLE_ID)) {
ERC20Registry.register(ERC20_REGISTRY_TABLE_ID);
}
ERC20Registry.set(ERC20_REGISTRY_TABLE_ID, namespaceId, puppet);
}

function installRoot(bytes memory) public pure {
revert Module_RootInstallNotSupported();
}
}
Loading
Loading