From 83638373450af5d8f703a183a74107ef7efb4152 Mon Sep 17 00:00:00 2001 From: alvarius Date: Wed, 1 Nov 2023 16:02:56 +0100 Subject: [PATCH] feat(world-modules): add ERC20 module (#1789) Co-authored-by: Fraser Scott Co-authored-by: 0xhank --- .changeset/silent-buttons-peel.md | 20 + packages/world-modules/gas-report.json | 30 + packages/world-modules/mud.config.ts | 56 +- packages/world-modules/remappings.txt | 2 +- packages/world-modules/src/index.sol | 5 + .../src/interfaces/IERC20System.sol | 32 + .../src/modules/erc20-puppet/ERC20Module.sol | 93 +++ .../src/modules/erc20-puppet/ERC20System.sol | 287 ++++++++ .../src/modules/erc20-puppet/IERC20.sol | 93 +++ .../src/modules/erc20-puppet/IERC20Errors.sol | 49 ++ .../src/modules/erc20-puppet/IERC20Events.sol | 18 + .../modules/erc20-puppet/IERC20Mintable.sol | 26 + .../src/modules/erc20-puppet/constants.sol | 21 + .../modules/erc20-puppet/registerERC20.sol | 35 + .../erc20-puppet/tables/Allowances.sol | 239 +++++++ .../modules/erc20-puppet/tables/Balances.sol | 226 +++++++ .../erc20-puppet/tables/ERC20Registry.sol | 229 +++++++ .../modules/erc20-puppet/tables/Metadata.sol | 635 ++++++++++++++++++ .../erc20-puppet/tables/TotalSupply.sol | 213 ++++++ .../src/modules/erc20-puppet/utils.sol | 30 + packages/world-modules/test/ERC20.t.sol | 328 +++++++++ packages/world/src/IModule.sol | 1 + 22 files changed, 2666 insertions(+), 2 deletions(-) create mode 100644 .changeset/silent-buttons-peel.md create mode 100644 packages/world-modules/src/interfaces/IERC20System.sol create mode 100644 packages/world-modules/src/modules/erc20-puppet/ERC20Module.sol create mode 100644 packages/world-modules/src/modules/erc20-puppet/ERC20System.sol create mode 100644 packages/world-modules/src/modules/erc20-puppet/IERC20.sol create mode 100644 packages/world-modules/src/modules/erc20-puppet/IERC20Errors.sol create mode 100644 packages/world-modules/src/modules/erc20-puppet/IERC20Events.sol create mode 100644 packages/world-modules/src/modules/erc20-puppet/IERC20Mintable.sol create mode 100644 packages/world-modules/src/modules/erc20-puppet/constants.sol create mode 100644 packages/world-modules/src/modules/erc20-puppet/registerERC20.sol create mode 100644 packages/world-modules/src/modules/erc20-puppet/tables/Allowances.sol create mode 100644 packages/world-modules/src/modules/erc20-puppet/tables/Balances.sol create mode 100644 packages/world-modules/src/modules/erc20-puppet/tables/ERC20Registry.sol create mode 100644 packages/world-modules/src/modules/erc20-puppet/tables/Metadata.sol create mode 100644 packages/world-modules/src/modules/erc20-puppet/tables/TotalSupply.sol create mode 100644 packages/world-modules/src/modules/erc20-puppet/utils.sol create mode 100644 packages/world-modules/test/ERC20.t.sol diff --git a/.changeset/silent-buttons-peel.md b/.changeset/silent-buttons-peel.md new file mode 100644 index 0000000000..2a3d20e384 --- /dev/null +++ b/.changeset/silent-buttons-peel.md @@ -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)); + +// After the Puppet module is installed, new ERC20 tokens can be registered +IERC20Mintable token = registerERC20(world, "myERC20", ERC20MetadataData({ decimals: 18, name: "Token", symbol: "TKN" })); +``` diff --git a/packages/world-modules/gas-report.json b/packages/world-modules/gas-report.json index bff2b59b8c..e5cf4c39a0 100644 --- a/packages/world-modules/gas-report.json +++ b/packages/world-modules/gas-report.json @@ -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", diff --git a/packages/world-modules/mud.config.ts b/packages/world-modules/mud.config.ts index 9701c93ab2..b18913bee2 100644 --- a/packages/world-modules/mud.config.ts +++ b/packages/world-modules/mud.config.ts @@ -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"], }); diff --git a/packages/world-modules/remappings.txt b/packages/world-modules/remappings.txt index c4d992480e..66be45ecbd 100644 --- a/packages/world-modules/remappings.txt +++ b/packages/world-modules/remappings.txt @@ -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/ \ No newline at end of file diff --git a/packages/world-modules/src/index.sol b/packages/world-modules/src/index.sol index 64a418403f..4805e545b5 100644 --- a/packages/world-modules/src/index.sol +++ b/packages/world-modules/src/index.sol @@ -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"; diff --git a/packages/world-modules/src/interfaces/IERC20System.sol b/packages/world-modules/src/interfaces/IERC20System.sol new file mode 100644 index 0000000000..9583c9c176 --- /dev/null +++ b/packages/world-modules/src/interfaces/IERC20System.sol @@ -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; +} diff --git a/packages/world-modules/src/modules/erc20-puppet/ERC20Module.sol b/packages/world-modules/src/modules/erc20-puppet/ERC20Module.sol new file mode 100644 index 0000000000..e69f02d084 --- /dev/null +++ b/packages/world-modules/src/modules/erc20-puppet/ERC20Module.sol @@ -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 + if (InstalledModules.get(PUPPET_MODULE_NAME, keccak256(new bytes(0))) == address(0)) { + 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(); + } +} diff --git a/packages/world-modules/src/modules/erc20-puppet/ERC20System.sol b/packages/world-modules/src/modules/erc20-puppet/ERC20System.sol new file mode 100644 index 0000000000..878f51f39b --- /dev/null +++ b/packages/world-modules/src/modules/erc20-puppet/ERC20System.sol @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.21; + +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; +import { System } from "@latticexyz/world/src/System.sol"; +import { WorldResourceIdInstance } from "@latticexyz/world/src/WorldResourceId.sol"; +import { NamespaceOwner } from "@latticexyz/world/src/codegen/tables/NamespaceOwner.sol"; +import { SystemRegistry } from "@latticexyz/world/src/codegen/tables/SystemRegistry.sol"; +import { ALLOWANCES_NAME, BALANCES_NAME, METADATA_NAME } from "./constants.sol"; + +import { AccessControlLib } from "../../utils/AccessControlLib.sol"; +import { PuppetMaster } from "../puppet/PuppetMaster.sol"; +import { toTopic } from "../puppet/utils.sol"; + +import { IERC20Mintable } from "./IERC20Mintable.sol"; + +import { Allowances } from "./tables/Allowances.sol"; +import { Balances } from "./tables/Balances.sol"; +import { TotalSupply } from "./tables/TotalSupply.sol"; +import { Metadata } from "./tables/Metadata.sol"; + +import { _allowancesTableId, _balancesTableId, _totalSupplyTableId, _metadataTableId } from "./utils.sol"; + +contract ERC20System is System, IERC20Mintable, PuppetMaster { + using WorldResourceIdInstance for ResourceId; + + /** + * @dev Returns the name of the token. + */ + function name() public view virtual returns (string memory) { + return Metadata.getName(_metadataTableId(_namespace())); + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view virtual returns (string memory) { + return Metadata.getSymbol(_metadataTableId(_namespace())); + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5.05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. This is the default value returned by this function, unless + * it's overridden. + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view virtual returns (uint8) { + return Metadata.getDecimals(_metadataTableId(_namespace())); + } + + /** + * @dev Returns the value of tokens in existence. + */ + function totalSupply() external view returns (uint256) { + return TotalSupply.get(_totalSupplyTableId(_namespace())); + } + + /** + * @dev Returns the value of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256) { + return Balances.get(_balancesTableId(_namespace()), account); + } + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256) { + return Allowances.get(_allowancesTableId(_namespace()), owner, spender); + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - the caller must have a balance of at least `value`. + */ + function transfer(address to, uint256 value) public virtual returns (bool) { + address owner = _msgSender(); + _transfer(owner, to, value); + + return true; + } + + /** + * @dev See {IERC20-approve}. + * + * NOTE: If `value` is the maximum `uint256`, the allowance is not updated on + * `transferFrom`. This is semantically equivalent to an infinite approval. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 value) public virtual returns (bool) { + address owner = _msgSender(); + _approve(owner, spender, value); + + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}. + * + * NOTE: Does not update the allowance if the current allowance + * is the maximum `uint256`. + * + * Requirements: + * + * - `from` and `to` cannot be the zero address. + * - `from` must have a balance of at least `value`. + * - the caller must have allowance for ``from``'s tokens of at least + * `value`. + */ + function transferFrom(address from, address to, uint256 value) public virtual returns (bool) { + address spender = _msgSender(); + _spendAllowance(from, spender, value); + _transfer(from, to, value); + + return true; + } + + /** + * @dev Creates a `value` amount of tokens and assigns them to `account`, by transferring it from address(0). + * Relies on the `_update` mechanism + * + * Emits a {Transfer} event with `from` set to the zero address. + */ + function mint(address account, uint256 value) public { + // Require the caller to own the namespace + _requireOwner(); + + if (account == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + + _update(address(0), account, value); + } + + /** + * @dev Destroys a `value` amount of tokens from `account`, lowering the total supply. + * Relies on the `_update` mechanism. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * NOTE: This function is not virtual, {_update} should be overridden instead + */ + function burn(address account, uint256 value) public { + // Require the caller to own the namespace + _requireOwner(); + + if (account == address(0)) { + revert ERC20InvalidSender(address(0)); + } + + _update(account, address(0), value); + } + + /** + * @dev Moves a `value` amount of tokens from `from` to `to`. + * + * This internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * NOTE: This function is not virtual, {_update} should be overridden instead. + */ + function _transfer(address from, address to, uint256 value) internal { + if (from == address(0)) { + revert ERC20InvalidSender(address(0)); + } + if (to == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + _update(from, to, value); + } + + /** + * @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from` + * (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding + * this function. + * + * Emits a {Transfer} event. + */ + function _update(address from, address to, uint256 value) internal virtual { + bytes14 namespace = _namespace(); + ResourceId totalSupplyTableId = _totalSupplyTableId(namespace); + ResourceId balanceTableId = _balancesTableId(namespace); + + if (from == address(0)) { + // Overflow check required: The rest of the code assumes that totalSupply never overflows + TotalSupply.set(totalSupplyTableId, TotalSupply.get(totalSupplyTableId) + value); + } else { + uint256 fromBalance = Balances.get(balanceTableId, from); + if (fromBalance < value) { + revert ERC20InsufficientBalance(from, fromBalance, value); + } + unchecked { + // Overflow not possible: value <= fromBalance <= totalSupply. + Balances.set(balanceTableId, from, fromBalance - value); + } + } + + if (to == address(0)) { + unchecked { + // Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply. + TotalSupply.set(totalSupplyTableId, TotalSupply.get(totalSupplyTableId) - value); + } + } else { + unchecked { + // Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256. + Balances.set(balanceTableId, to, Balances.get(balanceTableId, to) + value); + } + } + + // Emit Transfer event on puppet + puppet().log(Transfer.selector, toTopic(from), toTopic(to), abi.encode(value)); + } + + /** + * @dev Sets `value` as the allowance of `spender` over the `owner`s tokens. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + function _approve(address owner, address spender, uint256 value) internal virtual { + if (owner == address(0)) { + revert ERC20InvalidApprover(address(0)); + } + if (spender == address(0)) { + revert ERC20InvalidSpender(address(0)); + } + Allowances.set(_allowancesTableId(_namespace()), owner, spender, value); + + // Emit Approval event on puppet + puppet().log(Approval.selector, toTopic(owner), toTopic(spender), abi.encode(value)); + } + + /** + * @dev Updates `owner` s allowance for `spender` based on spent `value`. + * + * Does not update the allowance value in case of infinite allowance. + * Revert if not enough allowance is available. + * + * Does not emit an {Approval} event. + */ + function _spendAllowance(address owner, address spender, uint256 value) internal virtual { + uint256 currentAllowance = Allowances.get(_allowancesTableId(_namespace()), owner, spender); + if (currentAllowance != type(uint256).max) { + if (currentAllowance < value) { + revert ERC20InsufficientAllowance(spender, currentAllowance, value); + } + unchecked { + _approve(owner, spender, currentAllowance - value); + } + } + } + + function _namespace() internal view returns (bytes14 namespace) { + ResourceId systemId = SystemRegistry.get(address(this)); + return systemId.getNamespace(); + } + + function _requireOwner() internal view { + AccessControlLib.requireOwner(SystemRegistry.get(address(this)), _msgSender()); + } +} diff --git a/packages/world-modules/src/modules/erc20-puppet/IERC20.sol b/packages/world-modules/src/modules/erc20-puppet/IERC20.sol new file mode 100644 index 0000000000..8d10c323da --- /dev/null +++ b/packages/world-modules/src/modules/erc20-puppet/IERC20.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/IERC20.sol) + +pragma solidity >=0.8.21; + +import { IERC20Events } from "./IERC20Events.sol"; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 is IERC20Events { + /** + * @dev Returns the name of the token. + */ + function name() external view returns (string memory); + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() external view returns (string memory); + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5.05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. This is the default value returned by this function, unless + * it's overridden. + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() external view returns (uint8); + + /** + * @dev Returns the value of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the value of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves a `value` amount of tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 value) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets a `value` amount of tokens as the allowance of `spender` over the + * caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 value) external returns (bool); + + /** + * @dev Moves a `value` amount of tokens from `from` to `to` using the + * allowance mechanism. `value` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address from, address to, uint256 value) external returns (bool); +} diff --git a/packages/world-modules/src/modules/erc20-puppet/IERC20Errors.sol b/packages/world-modules/src/modules/erc20-puppet/IERC20Errors.sol new file mode 100644 index 0000000000..1126664f2f --- /dev/null +++ b/packages/world-modules/src/modules/erc20-puppet/IERC20Errors.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/draft-IERC6093.sol) +pragma solidity >=0.8.21; + +/** + * @dev Standard ERC20 Errors + * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC20 tokens. + */ +interface IERC20Errors { + /** + * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + * @param balance Current balance for the interacting account. + * @param needed Minimum amount required to perform a transfer. + */ + error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed); + + /** + * @dev Indicates a failure with the token `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + */ + error ERC20InvalidSender(address sender); + + /** + * @dev Indicates a failure with the token `receiver`. Used in transfers. + * @param receiver Address to which tokens are being transferred. + */ + error ERC20InvalidReceiver(address receiver); + + /** + * @dev Indicates a failure with the `spender`’s `allowance`. Used in transfers. + * @param spender Address that may be allowed to operate on tokens without being their owner. + * @param allowance Amount of tokens a `spender` is allowed to operate with. + * @param needed Minimum amount required to perform a transfer. + */ + error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed); + + /** + * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals. + * @param approver Address initiating an approval operation. + */ + error ERC20InvalidApprover(address approver); + + /** + * @dev Indicates a failure with the `spender` to be approved. Used in approvals. + * @param spender Address that may be allowed to operate on tokens without being their owner. + */ + error ERC20InvalidSpender(address spender); +} diff --git a/packages/world-modules/src/modules/erc20-puppet/IERC20Events.sol b/packages/world-modules/src/modules/erc20-puppet/IERC20Events.sol new file mode 100644 index 0000000000..9494b29fe2 --- /dev/null +++ b/packages/world-modules/src/modules/erc20-puppet/IERC20Events.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.21; + +interface IERC20Events { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); +} diff --git a/packages/world-modules/src/modules/erc20-puppet/IERC20Mintable.sol b/packages/world-modules/src/modules/erc20-puppet/IERC20Mintable.sol new file mode 100644 index 0000000000..014069ee08 --- /dev/null +++ b/packages/world-modules/src/modules/erc20-puppet/IERC20Mintable.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/IERC20.sol) + +pragma solidity >=0.8.21; + +import { IERC20 } from "./IERC20.sol"; +import { IERC20Errors } from "./IERC20Errors.sol"; + +/** + * @dev Extending the ERC20 standard with permissioned mint and burn functions. + */ +interface IERC20Mintable is IERC20, IERC20Errors { + /** + * @dev Creates a `value` amount of tokens and assigns them to `account`, by transferring it from address(0). + * + * Emits a {Transfer} event with `from` set to the zero address. + */ + function mint(address account, uint256 value) external; + + /** + * @dev Destroys a `value` amount of tokens from `account`, lowering the total supply. + * + * Emits a {Transfer} event with `to` set to the zero address. + */ + function burn(address account, uint256 value) external; +} diff --git a/packages/world-modules/src/modules/erc20-puppet/constants.sol b/packages/world-modules/src/modules/erc20-puppet/constants.sol new file mode 100644 index 0000000000..f65d8aa8f3 --- /dev/null +++ b/packages/world-modules/src/modules/erc20-puppet/constants.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.21; +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; +import { RESOURCE_TABLE } from "@latticexyz/store/src/storeResourceTypes.sol"; +import { RESOURCE_SYSTEM, RESOURCE_NAMESPACE } from "@latticexyz/world/src/worldResourceTypes.sol"; + +bytes16 constant MODULE_NAME = "erc20-puppet"; +bytes14 constant MODULE_NAMESPACE = "erc20-puppet"; +ResourceId constant MODULE_NAMESPACE_ID = ResourceId.wrap( + bytes32(abi.encodePacked(RESOURCE_NAMESPACE, MODULE_NAMESPACE)) +); + +bytes16 constant ALLOWANCES_NAME = "Allowances"; +bytes16 constant BALANCES_NAME = "Balances"; +bytes16 constant TOTAL_SUPPLY_NAME = "TotalSupply"; +bytes16 constant METADATA_NAME = "Metadata"; +bytes16 constant ERC20_SYSTEM_NAME = "ERC20System"; + +ResourceId constant ERC20_REGISTRY_TABLE_ID = ResourceId.wrap( + bytes32(abi.encodePacked(RESOURCE_TABLE, MODULE_NAMESPACE, bytes16("ERC20Registry"))) +); diff --git a/packages/world-modules/src/modules/erc20-puppet/registerERC20.sol b/packages/world-modules/src/modules/erc20-puppet/registerERC20.sol new file mode 100644 index 0000000000..7b14f89568 --- /dev/null +++ b/packages/world-modules/src/modules/erc20-puppet/registerERC20.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.21; + +import { IBaseWorld } from "@latticexyz/world/src/codegen/interfaces/IBaseWorld.sol"; +import { NamespaceOwner } from "@latticexyz/world/src/codegen/tables/NamespaceOwner.sol"; +import { WorldResourceIdLib } from "@latticexyz/world/src/WorldResourceId.sol"; + +import { ERC20Module } from "./ERC20Module.sol"; +import { MODULE_NAMESPACE_ID, ERC20_REGISTRY_TABLE_ID } from "./constants.sol"; +import { IERC20Mintable } from "./IERC20Mintable.sol"; + +import { MetadataData } from "./tables/Metadata.sol"; +import { ERC20Registry } from "./tables/ERC20Registry.sol"; + +/** + * @notice Register a new ERC20 token with the given metadata in a given namespace + * @dev This function must be called within a Store context (i.e. using StoreSwitch.setStoreAddress()) + */ +function registerERC20( + IBaseWorld world, + bytes14 namespace, + MetadataData memory metadata +) returns (IERC20Mintable token) { + // Get the ERC20 module + ERC20Module erc20Module = ERC20Module(NamespaceOwner.get(MODULE_NAMESPACE_ID)); + if (address(erc20Module) == address(0)) { + erc20Module = new ERC20Module(); + } + + // Install the ERC20 module with the provided args + world.installModule(erc20Module, abi.encode(namespace, metadata)); + + // Return the newly created ERC20 token + token = IERC20Mintable(ERC20Registry.get(ERC20_REGISTRY_TABLE_ID, WorldResourceIdLib.encodeNamespace(namespace))); +} diff --git a/packages/world-modules/src/modules/erc20-puppet/tables/Allowances.sol b/packages/world-modules/src/modules/erc20-puppet/tables/Allowances.sol new file mode 100644 index 0000000000..c1c3e083ed --- /dev/null +++ b/packages/world-modules/src/modules/erc20-puppet/tables/Allowances.sol @@ -0,0 +1,239 @@ +// 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"; + +FieldLayout constant _fieldLayout = FieldLayout.wrap( + 0x0020010020000000000000000000000000000000000000000000000000000000 +); + +library Allowances { + /** + * @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[](2); + _keySchema[0] = SchemaType.ADDRESS; + _keySchema[1] = SchemaType.ADDRESS; + + 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[](2); + keyNames[0] = "account"; + keyNames[1] = "spender"; + } + + /** + * @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] = "value"; + } + + /** + * @notice Register the table with its config. + */ + function register(ResourceId _tableId) internal { + StoreSwitch.registerTable(_tableId, _fieldLayout, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames()); + } + + /** + * @notice Register the table with its config. + */ + function _register(ResourceId _tableId) internal { + StoreCore.registerTable(_tableId, _fieldLayout, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames()); + } + + /** + * @notice Get value. + */ + function getValue(ResourceId _tableId, address account, address spender) internal view returns (uint256 value) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(account))); + _keyTuple[1] = bytes32(uint256(uint160(spender))); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get value. + */ + function _getValue(ResourceId _tableId, address account, address spender) internal view returns (uint256 value) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(account))); + _keyTuple[1] = bytes32(uint256(uint160(spender))); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get value. + */ + function get(ResourceId _tableId, address account, address spender) internal view returns (uint256 value) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(account))); + _keyTuple[1] = bytes32(uint256(uint160(spender))); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get value. + */ + function _get(ResourceId _tableId, address account, address spender) internal view returns (uint256 value) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(account))); + _keyTuple[1] = bytes32(uint256(uint160(spender))); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Set value. + */ + function setValue(ResourceId _tableId, address account, address spender, uint256 value) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(account))); + _keyTuple[1] = bytes32(uint256(uint160(spender))); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); + } + + /** + * @notice Set value. + */ + function _setValue(ResourceId _tableId, address account, address spender, uint256 value) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(account))); + _keyTuple[1] = bytes32(uint256(uint160(spender))); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); + } + + /** + * @notice Set value. + */ + function set(ResourceId _tableId, address account, address spender, uint256 value) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(account))); + _keyTuple[1] = bytes32(uint256(uint160(spender))); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); + } + + /** + * @notice Set value. + */ + function _set(ResourceId _tableId, address account, address spender, uint256 value) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(account))); + _keyTuple[1] = bytes32(uint256(uint160(spender))); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); + } + + /** + * @notice Delete all data for given keys. + */ + function deleteRecord(ResourceId _tableId, address account, address spender) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(account))); + _keyTuple[1] = bytes32(uint256(uint160(spender))); + + StoreSwitch.deleteRecord(_tableId, _keyTuple); + } + + /** + * @notice Delete all data for given keys. + */ + function _deleteRecord(ResourceId _tableId, address account, address spender) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(account))); + _keyTuple[1] = bytes32(uint256(uint160(spender))); + + 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 value) internal pure returns (bytes memory) { + return abi.encodePacked(value); + } + + /** + * @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 value) internal pure returns (bytes memory, PackedCounter, bytes memory) { + bytes memory _staticData = encodeStatic(value); + + 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 account, address spender) internal pure returns (bytes32[] memory) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(account))); + _keyTuple[1] = bytes32(uint256(uint160(spender))); + + return _keyTuple; + } +} diff --git a/packages/world-modules/src/modules/erc20-puppet/tables/Balances.sol b/packages/world-modules/src/modules/erc20-puppet/tables/Balances.sol new file mode 100644 index 0000000000..bad1af5c6d --- /dev/null +++ b/packages/world-modules/src/modules/erc20-puppet/tables/Balances.sol @@ -0,0 +1,226 @@ +// 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"; + +FieldLayout constant _fieldLayout = FieldLayout.wrap( + 0x0020010020000000000000000000000000000000000000000000000000000000 +); + +library Balances { + /** + * @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[](1); + _keySchema[0] = SchemaType.ADDRESS; + + 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[](1); + keyNames[0] = "account"; + } + + /** + * @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] = "value"; + } + + /** + * @notice Register the table with its config. + */ + function register(ResourceId _tableId) internal { + StoreSwitch.registerTable(_tableId, _fieldLayout, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames()); + } + + /** + * @notice Register the table with its config. + */ + function _register(ResourceId _tableId) internal { + StoreCore.registerTable(_tableId, _fieldLayout, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames()); + } + + /** + * @notice Get value. + */ + function getValue(ResourceId _tableId, address account) internal view returns (uint256 value) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(account))); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get value. + */ + function _getValue(ResourceId _tableId, address account) internal view returns (uint256 value) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(account))); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get value. + */ + function get(ResourceId _tableId, address account) internal view returns (uint256 value) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(account))); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get value. + */ + function _get(ResourceId _tableId, address account) internal view returns (uint256 value) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(account))); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Set value. + */ + function setValue(ResourceId _tableId, address account, uint256 value) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(account))); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); + } + + /** + * @notice Set value. + */ + function _setValue(ResourceId _tableId, address account, uint256 value) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(account))); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); + } + + /** + * @notice Set value. + */ + function set(ResourceId _tableId, address account, uint256 value) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(account))); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); + } + + /** + * @notice Set value. + */ + function _set(ResourceId _tableId, address account, uint256 value) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(account))); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout); + } + + /** + * @notice Delete all data for given keys. + */ + function deleteRecord(ResourceId _tableId, address account) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(account))); + + StoreSwitch.deleteRecord(_tableId, _keyTuple); + } + + /** + * @notice Delete all data for given keys. + */ + function _deleteRecord(ResourceId _tableId, address account) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(account))); + + 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 value) internal pure returns (bytes memory) { + return abi.encodePacked(value); + } + + /** + * @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 value) internal pure returns (bytes memory, PackedCounter, bytes memory) { + bytes memory _staticData = encodeStatic(value); + + 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 account) internal pure returns (bytes32[] memory) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(uint160(account))); + + return _keyTuple; + } +} diff --git a/packages/world-modules/src/modules/erc20-puppet/tables/ERC20Registry.sol b/packages/world-modules/src/modules/erc20-puppet/tables/ERC20Registry.sol new file mode 100644 index 0000000000..184def382b --- /dev/null +++ b/packages/world-modules/src/modules/erc20-puppet/tables/ERC20Registry.sol @@ -0,0 +1,229 @@ +// 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"; + +FieldLayout constant _fieldLayout = FieldLayout.wrap( + 0x0014010014000000000000000000000000000000000000000000000000000000 +); + +library ERC20Registry { + /** + * @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[](1); + _keySchema[0] = 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.ADDRESS; + + 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[](1); + keyNames[0] = "namespaceId"; + } + + /** + * @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] = "erc20Address"; + } + + /** + * @notice Register the table with its config. + */ + function register(ResourceId _tableId) internal { + StoreSwitch.registerTable(_tableId, _fieldLayout, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames()); + } + + /** + * @notice Register the table with its config. + */ + function _register(ResourceId _tableId) internal { + StoreCore.registerTable(_tableId, _fieldLayout, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames()); + } + + /** + * @notice Get erc20Address. + */ + function getErc20Address(ResourceId _tableId, ResourceId namespaceId) internal view returns (address erc20Address) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(namespaceId); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (address(bytes20(_blob))); + } + + /** + * @notice Get erc20Address. + */ + function _getErc20Address(ResourceId _tableId, ResourceId namespaceId) internal view returns (address erc20Address) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(namespaceId); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (address(bytes20(_blob))); + } + + /** + * @notice Get erc20Address. + */ + function get(ResourceId _tableId, ResourceId namespaceId) internal view returns (address erc20Address) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(namespaceId); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (address(bytes20(_blob))); + } + + /** + * @notice Get erc20Address. + */ + function _get(ResourceId _tableId, ResourceId namespaceId) internal view returns (address erc20Address) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(namespaceId); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (address(bytes20(_blob))); + } + + /** + * @notice Set erc20Address. + */ + function setErc20Address(ResourceId _tableId, ResourceId namespaceId, address erc20Address) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(namespaceId); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((erc20Address)), _fieldLayout); + } + + /** + * @notice Set erc20Address. + */ + function _setErc20Address(ResourceId _tableId, ResourceId namespaceId, address erc20Address) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(namespaceId); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((erc20Address)), _fieldLayout); + } + + /** + * @notice Set erc20Address. + */ + function set(ResourceId _tableId, ResourceId namespaceId, address erc20Address) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(namespaceId); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((erc20Address)), _fieldLayout); + } + + /** + * @notice Set erc20Address. + */ + function _set(ResourceId _tableId, ResourceId namespaceId, address erc20Address) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(namespaceId); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((erc20Address)), _fieldLayout); + } + + /** + * @notice Delete all data for given keys. + */ + function deleteRecord(ResourceId _tableId, ResourceId namespaceId) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(namespaceId); + + StoreSwitch.deleteRecord(_tableId, _keyTuple); + } + + /** + * @notice Delete all data for given keys. + */ + function _deleteRecord(ResourceId _tableId, ResourceId namespaceId) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(namespaceId); + + 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(address erc20Address) internal pure returns (bytes memory) { + return abi.encodePacked(erc20Address); + } + + /** + * @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(address erc20Address) internal pure returns (bytes memory, PackedCounter, bytes memory) { + bytes memory _staticData = encodeStatic(erc20Address); + + PackedCounter _encodedLengths; + bytes memory _dynamicData; + + return (_staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Encode keys as a bytes32 array using this table's field layout. + */ + function encodeKeyTuple(ResourceId namespaceId) internal pure returns (bytes32[] memory) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(namespaceId); + + return _keyTuple; + } +} diff --git a/packages/world-modules/src/modules/erc20-puppet/tables/Metadata.sol b/packages/world-modules/src/modules/erc20-puppet/tables/Metadata.sol new file mode 100644 index 0000000000..d24ef388df --- /dev/null +++ b/packages/world-modules/src/modules/erc20-puppet/tables/Metadata.sol @@ -0,0 +1,635 @@ +// 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"; + +FieldLayout constant _fieldLayout = FieldLayout.wrap( + 0x0001010201000000000000000000000000000000000000000000000000000000 +); + +struct MetadataData { + uint8 decimals; + string name; + string symbol; +} + +library Metadata { + /** + * @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[](0); + + 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[](3); + _valueSchema[0] = SchemaType.UINT8; + _valueSchema[1] = SchemaType.STRING; + _valueSchema[2] = SchemaType.STRING; + + 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[](0); + } + + /** + * @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[](3); + fieldNames[0] = "decimals"; + fieldNames[1] = "name"; + fieldNames[2] = "symbol"; + } + + /** + * @notice Register the table with its config. + */ + function register(ResourceId _tableId) internal { + StoreSwitch.registerTable(_tableId, _fieldLayout, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames()); + } + + /** + * @notice Register the table with its config. + */ + function _register(ResourceId _tableId) internal { + StoreCore.registerTable(_tableId, _fieldLayout, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames()); + } + + /** + * @notice Get decimals. + */ + function getDecimals(ResourceId _tableId) internal view returns (uint8 decimals) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint8(bytes1(_blob))); + } + + /** + * @notice Get decimals. + */ + function _getDecimals(ResourceId _tableId) internal view returns (uint8 decimals) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint8(bytes1(_blob))); + } + + /** + * @notice Set decimals. + */ + function setDecimals(ResourceId _tableId, uint8 decimals) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((decimals)), _fieldLayout); + } + + /** + * @notice Set decimals. + */ + function _setDecimals(ResourceId _tableId, uint8 decimals) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((decimals)), _fieldLayout); + } + + /** + * @notice Get name. + */ + function getName(ResourceId _tableId) internal view returns (string memory name) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes memory _blob = StoreSwitch.getDynamicField(_tableId, _keyTuple, 0); + return (string(_blob)); + } + + /** + * @notice Get name. + */ + function _getName(ResourceId _tableId) internal view returns (string memory name) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes memory _blob = StoreCore.getDynamicField(_tableId, _keyTuple, 0); + return (string(_blob)); + } + + /** + * @notice Set name. + */ + function setName(ResourceId _tableId, string memory name) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.setDynamicField(_tableId, _keyTuple, 0, bytes((name))); + } + + /** + * @notice Set name. + */ + function _setName(ResourceId _tableId, string memory name) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.setDynamicField(_tableId, _keyTuple, 0, bytes((name))); + } + + /** + * @notice Get the length of name. + */ + function lengthName(ResourceId _tableId) internal view returns (uint256) { + bytes32[] memory _keyTuple = new bytes32[](0); + + uint256 _byteLength = StoreSwitch.getDynamicFieldLength(_tableId, _keyTuple, 0); + unchecked { + return _byteLength / 1; + } + } + + /** + * @notice Get the length of name. + */ + function _lengthName(ResourceId _tableId) internal view returns (uint256) { + bytes32[] memory _keyTuple = new bytes32[](0); + + uint256 _byteLength = StoreCore.getDynamicFieldLength(_tableId, _keyTuple, 0); + unchecked { + return _byteLength / 1; + } + } + + /** + * @notice Get an item of name. + * @dev Reverts with Store_IndexOutOfBounds if `_index` is out of bounds for the array. + */ + function getItemName(ResourceId _tableId, uint256 _index) internal view returns (string memory) { + bytes32[] memory _keyTuple = new bytes32[](0); + + unchecked { + bytes memory _blob = StoreSwitch.getDynamicFieldSlice(_tableId, _keyTuple, 0, _index * 1, (_index + 1) * 1); + return (string(_blob)); + } + } + + /** + * @notice Get an item of name. + * @dev Reverts with Store_IndexOutOfBounds if `_index` is out of bounds for the array. + */ + function _getItemName(ResourceId _tableId, uint256 _index) internal view returns (string memory) { + bytes32[] memory _keyTuple = new bytes32[](0); + + unchecked { + bytes memory _blob = StoreCore.getDynamicFieldSlice(_tableId, _keyTuple, 0, _index * 1, (_index + 1) * 1); + return (string(_blob)); + } + } + + /** + * @notice Push a slice to name. + */ + function pushName(ResourceId _tableId, string memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.pushToDynamicField(_tableId, _keyTuple, 0, bytes((_slice))); + } + + /** + * @notice Push a slice to name. + */ + function _pushName(ResourceId _tableId, string memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.pushToDynamicField(_tableId, _keyTuple, 0, bytes((_slice))); + } + + /** + * @notice Pop a slice from name. + */ + function popName(ResourceId _tableId) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.popFromDynamicField(_tableId, _keyTuple, 0, 1); + } + + /** + * @notice Pop a slice from name. + */ + function _popName(ResourceId _tableId) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.popFromDynamicField(_tableId, _keyTuple, 0, 1); + } + + /** + * @notice Update a slice of name at `_index`. + */ + function updateName(ResourceId _tableId, uint256 _index, string memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + unchecked { + bytes memory _encoded = bytes((_slice)); + StoreSwitch.spliceDynamicData(_tableId, _keyTuple, 0, uint40(_index * 1), uint40(_encoded.length), _encoded); + } + } + + /** + * @notice Update a slice of name at `_index`. + */ + function _updateName(ResourceId _tableId, uint256 _index, string memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + unchecked { + bytes memory _encoded = bytes((_slice)); + StoreCore.spliceDynamicData(_tableId, _keyTuple, 0, uint40(_index * 1), uint40(_encoded.length), _encoded); + } + } + + /** + * @notice Get symbol. + */ + function getSymbol(ResourceId _tableId) internal view returns (string memory symbol) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes memory _blob = StoreSwitch.getDynamicField(_tableId, _keyTuple, 1); + return (string(_blob)); + } + + /** + * @notice Get symbol. + */ + function _getSymbol(ResourceId _tableId) internal view returns (string memory symbol) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes memory _blob = StoreCore.getDynamicField(_tableId, _keyTuple, 1); + return (string(_blob)); + } + + /** + * @notice Set symbol. + */ + function setSymbol(ResourceId _tableId, string memory symbol) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.setDynamicField(_tableId, _keyTuple, 1, bytes((symbol))); + } + + /** + * @notice Set symbol. + */ + function _setSymbol(ResourceId _tableId, string memory symbol) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.setDynamicField(_tableId, _keyTuple, 1, bytes((symbol))); + } + + /** + * @notice Get the length of symbol. + */ + function lengthSymbol(ResourceId _tableId) internal view returns (uint256) { + bytes32[] memory _keyTuple = new bytes32[](0); + + uint256 _byteLength = StoreSwitch.getDynamicFieldLength(_tableId, _keyTuple, 1); + unchecked { + return _byteLength / 1; + } + } + + /** + * @notice Get the length of symbol. + */ + function _lengthSymbol(ResourceId _tableId) internal view returns (uint256) { + bytes32[] memory _keyTuple = new bytes32[](0); + + uint256 _byteLength = StoreCore.getDynamicFieldLength(_tableId, _keyTuple, 1); + unchecked { + return _byteLength / 1; + } + } + + /** + * @notice Get an item of symbol. + * @dev Reverts with Store_IndexOutOfBounds if `_index` is out of bounds for the array. + */ + function getItemSymbol(ResourceId _tableId, uint256 _index) internal view returns (string memory) { + bytes32[] memory _keyTuple = new bytes32[](0); + + unchecked { + bytes memory _blob = StoreSwitch.getDynamicFieldSlice(_tableId, _keyTuple, 1, _index * 1, (_index + 1) * 1); + return (string(_blob)); + } + } + + /** + * @notice Get an item of symbol. + * @dev Reverts with Store_IndexOutOfBounds if `_index` is out of bounds for the array. + */ + function _getItemSymbol(ResourceId _tableId, uint256 _index) internal view returns (string memory) { + bytes32[] memory _keyTuple = new bytes32[](0); + + unchecked { + bytes memory _blob = StoreCore.getDynamicFieldSlice(_tableId, _keyTuple, 1, _index * 1, (_index + 1) * 1); + return (string(_blob)); + } + } + + /** + * @notice Push a slice to symbol. + */ + function pushSymbol(ResourceId _tableId, string memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.pushToDynamicField(_tableId, _keyTuple, 1, bytes((_slice))); + } + + /** + * @notice Push a slice to symbol. + */ + function _pushSymbol(ResourceId _tableId, string memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.pushToDynamicField(_tableId, _keyTuple, 1, bytes((_slice))); + } + + /** + * @notice Pop a slice from symbol. + */ + function popSymbol(ResourceId _tableId) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.popFromDynamicField(_tableId, _keyTuple, 1, 1); + } + + /** + * @notice Pop a slice from symbol. + */ + function _popSymbol(ResourceId _tableId) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.popFromDynamicField(_tableId, _keyTuple, 1, 1); + } + + /** + * @notice Update a slice of symbol at `_index`. + */ + function updateSymbol(ResourceId _tableId, uint256 _index, string memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + unchecked { + bytes memory _encoded = bytes((_slice)); + StoreSwitch.spliceDynamicData(_tableId, _keyTuple, 1, uint40(_index * 1), uint40(_encoded.length), _encoded); + } + } + + /** + * @notice Update a slice of symbol at `_index`. + */ + function _updateSymbol(ResourceId _tableId, uint256 _index, string memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + unchecked { + bytes memory _encoded = bytes((_slice)); + StoreCore.spliceDynamicData(_tableId, _keyTuple, 1, uint40(_index * 1), uint40(_encoded.length), _encoded); + } + } + + /** + * @notice Get the full data. + */ + function get(ResourceId _tableId) internal view returns (MetadataData memory _table) { + bytes32[] memory _keyTuple = new bytes32[](0); + + (bytes memory _staticData, PackedCounter _encodedLengths, bytes memory _dynamicData) = StoreSwitch.getRecord( + _tableId, + _keyTuple, + _fieldLayout + ); + return decode(_staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Get the full data. + */ + function _get(ResourceId _tableId) internal view returns (MetadataData memory _table) { + bytes32[] memory _keyTuple = new bytes32[](0); + + (bytes memory _staticData, PackedCounter _encodedLengths, bytes memory _dynamicData) = StoreCore.getRecord( + _tableId, + _keyTuple, + _fieldLayout + ); + return decode(_staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Set the full data using individual values. + */ + function set(ResourceId _tableId, uint8 decimals, string memory name, string memory symbol) internal { + bytes memory _staticData = encodeStatic(decimals); + + PackedCounter _encodedLengths = encodeLengths(name, symbol); + bytes memory _dynamicData = encodeDynamic(name, symbol); + + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.setRecord(_tableId, _keyTuple, _staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Set the full data using individual values. + */ + function _set(ResourceId _tableId, uint8 decimals, string memory name, string memory symbol) internal { + bytes memory _staticData = encodeStatic(decimals); + + PackedCounter _encodedLengths = encodeLengths(name, symbol); + bytes memory _dynamicData = encodeDynamic(name, symbol); + + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.setRecord(_tableId, _keyTuple, _staticData, _encodedLengths, _dynamicData, _fieldLayout); + } + + /** + * @notice Set the full data using the data struct. + */ + function set(ResourceId _tableId, MetadataData memory _table) internal { + bytes memory _staticData = encodeStatic(_table.decimals); + + PackedCounter _encodedLengths = encodeLengths(_table.name, _table.symbol); + bytes memory _dynamicData = encodeDynamic(_table.name, _table.symbol); + + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.setRecord(_tableId, _keyTuple, _staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Set the full data using the data struct. + */ + function _set(ResourceId _tableId, MetadataData memory _table) internal { + bytes memory _staticData = encodeStatic(_table.decimals); + + PackedCounter _encodedLengths = encodeLengths(_table.name, _table.symbol); + bytes memory _dynamicData = encodeDynamic(_table.name, _table.symbol); + + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.setRecord(_tableId, _keyTuple, _staticData, _encodedLengths, _dynamicData, _fieldLayout); + } + + /** + * @notice Decode the tightly packed blob of static data using this table's field layout. + */ + function decodeStatic(bytes memory _blob) internal pure returns (uint8 decimals) { + decimals = (uint8(Bytes.slice1(_blob, 0))); + } + + /** + * @notice Decode the tightly packed blob of dynamic data using the encoded lengths. + */ + function decodeDynamic( + PackedCounter _encodedLengths, + bytes memory _blob + ) internal pure returns (string memory name, string memory symbol) { + uint256 _start; + uint256 _end; + unchecked { + _end = _encodedLengths.atIndex(0); + } + name = (string(SliceLib.getSubslice(_blob, _start, _end).toBytes())); + + _start = _end; + unchecked { + _end += _encodedLengths.atIndex(1); + } + symbol = (string(SliceLib.getSubslice(_blob, _start, _end).toBytes())); + } + + /** + * @notice Decode the tightly packed blobs using this table's field layout. + * @param _staticData Tightly packed static fields. + * @param _encodedLengths Encoded lengths of dynamic fields. + * @param _dynamicData Tightly packed dynamic fields. + */ + function decode( + bytes memory _staticData, + PackedCounter _encodedLengths, + bytes memory _dynamicData + ) internal pure returns (MetadataData memory _table) { + (_table.decimals) = decodeStatic(_staticData); + + (_table.name, _table.symbol) = decodeDynamic(_encodedLengths, _dynamicData); + } + + /** + * @notice Delete all data for given keys. + */ + function deleteRecord(ResourceId _tableId) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.deleteRecord(_tableId, _keyTuple); + } + + /** + * @notice Delete all data for given keys. + */ + function _deleteRecord(ResourceId _tableId) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + 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(uint8 decimals) internal pure returns (bytes memory) { + return abi.encodePacked(decimals); + } + + /** + * @notice Tightly pack dynamic data lengths using this table's schema. + * @return _encodedLengths The lengths of the dynamic fields (packed into a single bytes32 value). + */ + function encodeLengths( + string memory name, + string memory symbol + ) internal pure returns (PackedCounter _encodedLengths) { + // Lengths are effectively checked during copy by 2**40 bytes exceeding gas limits + unchecked { + _encodedLengths = PackedCounterLib.pack(bytes(name).length, bytes(symbol).length); + } + } + + /** + * @notice Tightly pack dynamic (variable length) data using this table's schema. + * @return The dynamic data, encoded into a sequence of bytes. + */ + function encodeDynamic(string memory name, string memory symbol) internal pure returns (bytes memory) { + return abi.encodePacked(bytes((name)), bytes((symbol))); + } + + /** + * @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( + uint8 decimals, + string memory name, + string memory symbol + ) internal pure returns (bytes memory, PackedCounter, bytes memory) { + bytes memory _staticData = encodeStatic(decimals); + + PackedCounter _encodedLengths = encodeLengths(name, symbol); + bytes memory _dynamicData = encodeDynamic(name, symbol); + + return (_staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Encode keys as a bytes32 array using this table's field layout. + */ + function encodeKeyTuple() internal pure returns (bytes32[] memory) { + bytes32[] memory _keyTuple = new bytes32[](0); + + return _keyTuple; + } +} diff --git a/packages/world-modules/src/modules/erc20-puppet/tables/TotalSupply.sol b/packages/world-modules/src/modules/erc20-puppet/tables/TotalSupply.sol new file mode 100644 index 0000000000..876013c395 --- /dev/null +++ b/packages/world-modules/src/modules/erc20-puppet/tables/TotalSupply.sol @@ -0,0 +1,213 @@ +// 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"; + +FieldLayout constant _fieldLayout = FieldLayout.wrap( + 0x0020010020000000000000000000000000000000000000000000000000000000 +); + +library TotalSupply { + /** + * @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[](0); + + 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[](0); + } + + /** + * @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] = "totalSupply"; + } + + /** + * @notice Register the table with its config. + */ + function register(ResourceId _tableId) internal { + StoreSwitch.registerTable(_tableId, _fieldLayout, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames()); + } + + /** + * @notice Register the table with its config. + */ + function _register(ResourceId _tableId) internal { + StoreCore.registerTable(_tableId, _fieldLayout, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames()); + } + + /** + * @notice Get totalSupply. + */ + function getTotalSupply(ResourceId _tableId) internal view returns (uint256 totalSupply) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get totalSupply. + */ + function _getTotalSupply(ResourceId _tableId) internal view returns (uint256 totalSupply) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get totalSupply. + */ + function get(ResourceId _tableId) internal view returns (uint256 totalSupply) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Get totalSupply. + */ + function _get(ResourceId _tableId) internal view returns (uint256 totalSupply) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (uint256(bytes32(_blob))); + } + + /** + * @notice Set totalSupply. + */ + function setTotalSupply(ResourceId _tableId, uint256 totalSupply) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((totalSupply)), _fieldLayout); + } + + /** + * @notice Set totalSupply. + */ + function _setTotalSupply(ResourceId _tableId, uint256 totalSupply) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((totalSupply)), _fieldLayout); + } + + /** + * @notice Set totalSupply. + */ + function set(ResourceId _tableId, uint256 totalSupply) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((totalSupply)), _fieldLayout); + } + + /** + * @notice Set totalSupply. + */ + function _set(ResourceId _tableId, uint256 totalSupply) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((totalSupply)), _fieldLayout); + } + + /** + * @notice Delete all data for given keys. + */ + function deleteRecord(ResourceId _tableId) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.deleteRecord(_tableId, _keyTuple); + } + + /** + * @notice Delete all data for given keys. + */ + function _deleteRecord(ResourceId _tableId) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + 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 totalSupply) internal pure returns (bytes memory) { + return abi.encodePacked(totalSupply); + } + + /** + * @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 totalSupply) internal pure returns (bytes memory, PackedCounter, bytes memory) { + bytes memory _staticData = encodeStatic(totalSupply); + + PackedCounter _encodedLengths; + bytes memory _dynamicData; + + return (_staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Encode keys as a bytes32 array using this table's field layout. + */ + function encodeKeyTuple() internal pure returns (bytes32[] memory) { + bytes32[] memory _keyTuple = new bytes32[](0); + + return _keyTuple; + } +} diff --git a/packages/world-modules/src/modules/erc20-puppet/utils.sol b/packages/world-modules/src/modules/erc20-puppet/utils.sol new file mode 100644 index 0000000000..cc319de053 --- /dev/null +++ b/packages/world-modules/src/modules/erc20-puppet/utils.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.21; + +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; +import { RESOURCE_TABLE } from "@latticexyz/store/src/storeResourceTypes.sol"; + +import { WorldResourceIdLib } from "@latticexyz/world/src/WorldResourceId.sol"; +import { RESOURCE_SYSTEM } from "@latticexyz/world/src/worldResourceTypes.sol"; + +import { ALLOWANCES_NAME, BALANCES_NAME, TOTAL_SUPPLY_NAME, METADATA_NAME, ERC20_SYSTEM_NAME } from "./constants.sol"; + +function _allowancesTableId(bytes14 namespace) pure returns (ResourceId) { + return WorldResourceIdLib.encode({ typeId: RESOURCE_TABLE, namespace: namespace, name: ALLOWANCES_NAME }); +} + +function _balancesTableId(bytes14 namespace) pure returns (ResourceId) { + return WorldResourceIdLib.encode({ typeId: RESOURCE_TABLE, namespace: namespace, name: BALANCES_NAME }); +} + +function _totalSupplyTableId(bytes14 namespace) pure returns (ResourceId) { + return WorldResourceIdLib.encode({ typeId: RESOURCE_TABLE, namespace: namespace, name: TOTAL_SUPPLY_NAME }); +} + +function _metadataTableId(bytes14 namespace) pure returns (ResourceId) { + return WorldResourceIdLib.encode({ typeId: RESOURCE_TABLE, namespace: namespace, name: METADATA_NAME }); +} + +function _erc20SystemId(bytes14 namespace) pure returns (ResourceId) { + return WorldResourceIdLib.encode({ typeId: RESOURCE_SYSTEM, namespace: namespace, name: ERC20_SYSTEM_NAME }); +} diff --git a/packages/world-modules/test/ERC20.t.sol b/packages/world-modules/test/ERC20.t.sol new file mode 100644 index 0000000000..8b62a34675 --- /dev/null +++ b/packages/world-modules/test/ERC20.t.sol @@ -0,0 +1,328 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.21; + +import { Test } from "forge-std/Test.sol"; +import { console } from "forge-std/console.sol"; + +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; +import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; +import { World } from "@latticexyz/world/src/World.sol"; +import { WorldResourceIdLib } from "@latticexyz/world/src/WorldResourceId.sol"; +import { CoreModule } from "@latticexyz/world/src/modules/core/CoreModule.sol"; +import { IBaseWorld } from "@latticexyz/world/src/codegen/interfaces/IBaseWorld.sol"; +import { NamespaceOwner } from "@latticexyz/world/src/codegen/tables/NamespaceOwner.sol"; +import { GasReporter } from "@latticexyz/gas-report/src/GasReporter.sol"; + +import { PuppetModule } from "../src/modules/puppet/PuppetModule.sol"; + +import { ERC20Module } from "../src/modules/erc20-puppet/ERC20Module.sol"; +import { MetadataData } from "../src/modules/erc20-puppet/tables/Metadata.sol"; +import { ERC20Registry } from "../src/modules/erc20-puppet/tables/ERC20Registry.sol"; +import { ERC20_REGISTRY_TABLE_ID } from "../src/modules/erc20-puppet/constants.sol"; +import { IERC20Mintable } from "../src/modules/erc20-puppet/IERC20Mintable.sol"; +import { registerERC20 } from "../src/modules/erc20-puppet/registerERC20.sol"; +import { IERC20Errors } from "../src/modules/erc20-puppet/IERC20Errors.sol"; +import { IERC20Events } from "../src/modules/erc20-puppet/IERC20Events.sol"; + +contract ERC20Test is Test, GasReporter, IERC20Events, IERC20Errors { + IBaseWorld world; + ERC20Module erc20Module; + IERC20Mintable token; + + function setUp() public { + world = IBaseWorld(address(new World())); + world.initialize(new CoreModule()); + world.installModule(new PuppetModule(), new bytes(0)); + StoreSwitch.setStoreAddress(address(world)); + + // Register a new ERC20 token + token = registerERC20(world, "myERC20", MetadataData({ decimals: 18, name: "Token", symbol: "TKN" })); + } + + function testSetUp() public { + assertTrue(address(token) != address(0)); + assertEq(NamespaceOwner.get(WorldResourceIdLib.encodeNamespace("myERC20")), address(this)); + } + + function testInstallTwice() public { + // Install the ERC20 module + IERC20Mintable anotherToken = registerERC20( + world, + "anotherERC20", + MetadataData({ decimals: 18, name: "Token", symbol: "TKN" }) + ); + assertTrue(address(anotherToken) != address(0)); + assertTrue(address(anotherToken) != address(token)); + } + + ///////////////////////////////////////////////// + // SOLADY ERC20 TEST CAES + // (https://github.com/Vectorized/solady/blob/main/test/ERC20.t.sol) + ///////////////////////////////////////////////// + + function testMetadata() public { + assertEq(token.name(), "Token"); + assertEq(token.symbol(), "TKN"); + assertEq(token.decimals(), 18); + } + + function testMint() public { + vm.expectEmit(true, true, true, true); + emit Transfer(address(0), address(0xBEEF), 1e18); + startGasReport("mint"); + token.mint(address(0xBEEF), 1e18); + endGasReport(); + + assertEq(token.totalSupply(), 1e18); + assertEq(token.balanceOf(address(0xBEEF)), 1e18); + } + + function testBurn() public { + token.mint(address(0xBEEF), 1e18); + + vm.expectEmit(true, true, true, true); + emit Transfer(address(0xBEEF), address(0), 0.9e18); + startGasReport("burn"); + token.burn(address(0xBEEF), 0.9e18); + endGasReport(); + + assertEq(token.totalSupply(), 1e18 - 0.9e18); + assertEq(token.balanceOf(address(0xBEEF)), 0.1e18); + } + + function testApprove() public { + vm.expectEmit(true, true, true, true); + emit Approval(address(this), address(0xBEEF), 1e18); + startGasReport("approve"); + bool success = token.approve(address(0xBEEF), 1e18); + endGasReport(); + assertTrue(success); + + assertEq(token.allowance(address(this), address(0xBEEF)), 1e18); + } + + function testTransfer() public { + token.mint(address(this), 1e18); + + vm.expectEmit(true, true, true, true); + emit Transfer(address(this), address(0xBEEF), 1e18); + startGasReport("transfer"); + bool success = token.transfer(address(0xBEEF), 1e18); + endGasReport(); + assertTrue(success); + assertEq(token.totalSupply(), 1e18); + + assertEq(token.balanceOf(address(this)), 0); + assertEq(token.balanceOf(address(0xBEEF)), 1e18); + } + + function testTransferFrom() public { + address from = address(0xABCD); + + token.mint(from, 1e18); + + vm.prank(from); + token.approve(address(this), 1e18); + + vm.expectEmit(true, true, true, true); + emit Transfer(from, address(0xBEEF), 1e18); + startGasReport("transferFrom"); + bool success = token.transferFrom(from, address(0xBEEF), 1e18); + endGasReport(); + assertTrue(success); + assertEq(token.totalSupply(), 1e18); + + assertEq(token.allowance(from, address(this)), 0); + + assertEq(token.balanceOf(from), 0); + assertEq(token.balanceOf(address(0xBEEF)), 1e18); + } + + function testInfiniteApproveTransferFrom() public { + address from = address(0xABCD); + + token.mint(from, 1e18); + + vm.prank(from); + token.approve(address(this), type(uint256).max); + + assertTrue(token.transferFrom(from, address(0xBEEF), 1e18)); + assertEq(token.totalSupply(), 1e18); + + assertEq(token.allowance(from, address(this)), type(uint256).max); + + assertEq(token.balanceOf(from), 0); + assertEq(token.balanceOf(address(0xBEEF)), 1e18); + } + + function testMintOverMaxUintReverts() public { + token.mint(address(this), type(uint256).max); + vm.expectRevert(); + token.mint(address(this), 1); + } + + function testTransferInsufficientBalanceReverts() public { + token.mint(address(this), 0.9e18); + vm.expectRevert( + abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, address(this), 0.9e18, 1e18) + ); + token.transfer(address(0xBEEF), 1e18); + } + + function testTransferFromInsufficientAllowanceReverts() public { + address from = address(0xABCD); + + token.mint(from, 1e18); + + vm.prank(from); + token.approve(address(this), 0.9e18); + + vm.expectRevert(abi.encodeWithSelector(ERC20InsufficientAllowance.selector, address(this), 0.9e18, 1e18)); + token.transferFrom(from, address(0xBEEF), 1e18); + } + + function testTransferFromInsufficientBalanceReverts() public { + address from = address(0xABCD); + + token.mint(from, 0.9e18); + + vm.prank(from); + token.approve(address(this), 1e18); + + vm.expectRevert(abi.encodeWithSelector(ERC20InsufficientBalance.selector, from, 0.9e18, 1e18)); + token.transferFrom(from, address(0xBEEF), 1e18); + } + + function testMint(address to, uint256 amount) public { + vm.assume(to != address(0)); + + vm.expectEmit(true, true, true, true); + emit Transfer(address(0), to, amount); + token.mint(to, amount); + + assertEq(token.totalSupply(), amount); + assertEq(token.balanceOf(to), amount); + } + + function testBurn(address from, uint256 mintAmount, uint256 burnAmount) public { + vm.assume(from != address(0)); + vm.assume(burnAmount <= mintAmount); + + token.mint(from, mintAmount); + vm.expectEmit(true, true, true, true); + emit Transfer(from, address(0), burnAmount); + token.burn(from, burnAmount); + + assertEq(token.totalSupply(), mintAmount - burnAmount); + assertEq(token.balanceOf(from), mintAmount - burnAmount); + } + + function testApprove(address to, uint256 amount) public { + vm.assume(to != address(0)); + + assertTrue(token.approve(to, amount)); + + assertEq(token.allowance(address(this), to), amount); + } + + function testTransfer(address to, uint256 amount) public { + vm.assume(to != address(0)); + token.mint(address(this), amount); + + vm.expectEmit(true, true, true, true); + emit Transfer(address(this), to, amount); + assertTrue(token.transfer(to, amount)); + assertEq(token.totalSupply(), amount); + + if (address(this) == to) { + assertEq(token.balanceOf(address(this)), amount); + } else { + assertEq(token.balanceOf(address(this)), 0); + assertEq(token.balanceOf(to), amount); + } + } + + function testTransferFrom(address spender, address from, address to, uint256 approval, uint256 amount) public { + vm.assume(from != address(0)); + vm.assume(to != address(0)); + vm.assume(spender != address(0)); + vm.assume(amount <= approval); + + token.mint(from, amount); + assertEq(token.balanceOf(from), amount); + + vm.prank(from); + token.approve(spender, approval); + + vm.expectEmit(true, true, true, true); + emit Transfer(from, to, amount); + vm.prank(spender); + assertTrue(token.transferFrom(from, to, amount)); + assertEq(token.totalSupply(), amount); + + if (approval == type(uint256).max) { + assertEq(token.allowance(from, spender), approval); + } else { + assertEq(token.allowance(from, spender), approval - amount); + } + + if (from == to) { + assertEq(token.balanceOf(from), amount); + } else { + assertEq(token.balanceOf(from), 0); + assertEq(token.balanceOf(to), amount); + } + } + + function testBurnInsufficientBalanceReverts(address to, uint256 mintAmount, uint256 burnAmount) public { + vm.assume(to != address(0)); + vm.assume(mintAmount < type(uint256).max); + vm.assume(burnAmount > mintAmount); + + token.mint(to, mintAmount); + vm.expectRevert(abi.encodeWithSelector(ERC20InsufficientBalance.selector, to, mintAmount, burnAmount)); + token.burn(to, burnAmount); + } + + function testTransferInsufficientBalanceReverts(address to, uint256 mintAmount, uint256 sendAmount) public { + vm.assume(to != address(0)); + vm.assume(mintAmount < type(uint256).max); + vm.assume(sendAmount > mintAmount); + + token.mint(address(this), mintAmount); + vm.expectRevert(abi.encodeWithSelector(ERC20InsufficientBalance.selector, address(this), mintAmount, sendAmount)); + token.transfer(to, sendAmount); + } + + function testTransferFromInsufficientAllowanceReverts(address to, uint256 approval, uint256 amount) public { + vm.assume(to != address(0)); + vm.assume(approval < type(uint256).max); + vm.assume(amount > approval); + + address from = address(0xABCD); + + token.mint(from, amount); + + vm.prank(from); + token.approve(address(this), approval); + + vm.expectRevert(abi.encodeWithSelector(ERC20InsufficientAllowance.selector, address(this), approval, amount)); + token.transferFrom(from, to, amount); + } + + function testTransferFromInsufficientBalanceReverts(address to, uint256 mintAmount, uint256 sendAmount) public { + vm.assume(to != address(0)); + vm.assume(mintAmount < type(uint256).max); + vm.assume(sendAmount > mintAmount); + + address from = address(0xABCD); + + token.mint(from, mintAmount); + + vm.prank(from); + token.approve(address(this), sendAmount); + + vm.expectRevert(abi.encodeWithSelector(ERC20InsufficientBalance.selector, from, mintAmount, sendAmount)); + token.transferFrom(from, to, sendAmount); + } +} diff --git a/packages/world/src/IModule.sol b/packages/world/src/IModule.sol index a0bf4f8f6d..4a3dd5a264 100644 --- a/packages/world/src/IModule.sol +++ b/packages/world/src/IModule.sol @@ -24,6 +24,7 @@ interface IModule is IERC165 { error Module_RootInstallNotSupported(); error Module_NonRootInstallNotSupported(); error Module_AlreadyInstalled(); + error Module_MissingDependency(string dependency); /** * @notice Return the name of the module.