diff --git a/.changeset/gorgeous-swans-hide.md b/.changeset/gorgeous-swans-hide.md new file mode 100644 index 0000000000..152184065c --- /dev/null +++ b/.changeset/gorgeous-swans-hide.md @@ -0,0 +1,21 @@ +--- +"@latticexyz/world-modules": minor +--- + +Added the `ERC721Module` to `@latticexyz/world-modules`. +This module allows the registration of `ERC721` 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 { ERC721MetadataData } from "@latticexyz/world-modules/src/modules/erc721-puppet/tables/ERC721Metadata.sol"; +import { IERC721Mintable } from "@latticexyz/world-modules/src/modules/erc721-puppet/IERC721Mintable.sol"; +import { registerERC721 } from "@latticexyz/world-modules/src/modules/erc721-puppet/registerERC721.sol"; + +// The ERC721 module requires the Puppet module to be installed first +world.installModule(new PuppetModule(), new bytes(0)); + +// After the Puppet module is installed, new ERC721 tokens can be registered +IERC721Mintable token = registerERC721(world, "myERC721", ERC721MetadataData({ name: "Token", symbol: "TKN", baseURI: "" }));``` +```` diff --git a/.changeset/happy-pants-try.md b/.changeset/happy-pants-try.md new file mode 100644 index 0000000000..3f2a70fc4d --- /dev/null +++ b/.changeset/happy-pants-try.md @@ -0,0 +1,22 @@ +--- +"@latticexyz/world-modules": minor +--- + +Added the `PuppetModule` to `@latticexyz/world-modules`. The puppet pattern allows an external contract to be registered as an external interface for a MUD system. +This allows standards like `ERC20` (that require a specific interface and events to be emitted by a unique contract) to be implemented inside a MUD World. + +The puppet serves as a proxy, forwarding all calls to the implementation system (also called the "puppet master"). +The "puppet master" system can emit events from the puppet contract. + +```solidity +import { PuppetModule } from "@latticexyz/world-modules/src/modules/puppet/PuppetModule.sol"; +import { createPuppet } from "@latticexyz/world-modules/src/modules/puppet/createPuppet.sol"; + +// Install the puppet module +world.installModule(new PuppetModule(), new bytes(0)); + +// Register a new puppet for any system +// The system must implement the `CustomInterface`, +// and the caller must own the system's namespace +CustomInterface puppet = CustomInterface(createPuppet(world, )); +``` 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 bf8e4bc6e7..a830eecd1f 100644 --- a/packages/world-modules/mud.config.ts +++ b/packages/world-modules/mud.config.ts @@ -10,7 +10,7 @@ export default mudConfig({ tables: { /************************************************************************ * - * MODULE TABLES + * KEYS WITH VALUE MODULE * ************************************************************************/ KeysWithValue: { @@ -24,6 +24,11 @@ export default mudConfig({ tableIdArgument: true, storeArgument: true, }, + /************************************************************************ + * + * KEYS IN TABLE MODULE + * + ************************************************************************/ KeysInTable: { directory: "modules/keysintable/tables", keySchema: { sourceTableId: "ResourceId" }, @@ -46,6 +51,11 @@ export default mudConfig({ dataStruct: false, storeArgument: true, }, + /************************************************************************ + * + * UNIQUE ENTITY MODULE + * + ************************************************************************/ UniqueEntity: { directory: "modules/uniqueentity/tables", keySchema: {}, @@ -53,6 +63,11 @@ export default mudConfig({ tableIdArgument: true, storeArgument: true, }, + /************************************************************************ + * + * STD DELEGATIONS MODULE + * + ************************************************************************/ CallboundDelegations: { directory: "modules/std-delegations/tables", keySchema: { @@ -75,6 +90,146 @@ export default mudConfig({ maxTimestamp: "uint256", }, }, + /************************************************************************ + * + * PUPPET MODULE + * + ************************************************************************/ + PuppetRegistry: { + directory: "modules/puppet/tables", + keySchema: { + systemId: "ResourceId", + }, + valueSchema: { + puppet: "address", + }, + tableIdArgument: true, + }, + /************************************************************************ + * + * TOKEN TABLES (SHARED BY ERC20, ERC721) + * + ************************************************************************/ + Balances: { + directory: "modules/tokens/tables", + keySchema: { + account: "address", + }, + valueSchema: { + value: "uint256", + }, + tableIdArgument: true, + }, + /************************************************************************ + * + * ERC20 MODULE + * + ************************************************************************/ + ERC20Metadata: { + directory: "modules/erc20-puppet/tables", + keySchema: {}, + valueSchema: { + decimals: "uint8", + name: "string", + symbol: "string", + }, + 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, + }, + ERC20Registry: { + directory: "modules/erc20-puppet/tables", + keySchema: { + namespaceId: "ResourceId", + }, + valueSchema: { + erc20Address: "address", + }, + tableIdArgument: true, + }, + /************************************************************************ + * + * ERC721 MODULE + * + ************************************************************************/ + ERC721Metadata: { + directory: "modules/erc721-puppet/tables", + keySchema: {}, + valueSchema: { + name: "string", + symbol: "string", + baseURI: "string", + }, + tableIdArgument: true, + }, + TokenURI: { + directory: "modules/erc721-puppet/tables", + keySchema: { + tokenId: "uint256", + }, + valueSchema: { + tokenURI: "string", + }, + tableIdArgument: true, + }, + Owners: { + directory: "modules/erc721-puppet/tables", + keySchema: { + tokenId: "uint256", + }, + valueSchema: { + owner: "address", + }, + tableIdArgument: true, + }, + TokenApproval: { + directory: "modules/erc721-puppet/tables", + keySchema: { + tokenId: "uint256", + }, + valueSchema: { + account: "address", + }, + tableIdArgument: true, + }, + OperatorApproval: { + directory: "modules/erc721-puppet/tables", + keySchema: { + owner: "address", + operator: "address", + }, + valueSchema: { + approved: "bool", + }, + tableIdArgument: true, + }, + ERC721Registry: { + directory: "modules/erc721-puppet/tables", + keySchema: { + namespaceId: "ResourceId", + }, + valueSchema: { + erc20Address: "address", + }, + tableIdArgument: true, + }, }, - excludeSystems: ["UniqueEntitySystem"], + excludeSystems: ["UniqueEntitySystem", "PuppetFactorySystem", "ERC20System", "ERC721System"], }); 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 e88b495bf3..b1d73d8c0d 100644 --- a/packages/world-modules/src/index.sol +++ b/packages/world-modules/src/index.sol @@ -9,3 +9,15 @@ import { UsedKeysIndex, UsedKeysIndexTableId } from "./modules/keysintable/table 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/tokens/tables/Balances.sol"; +import { ERC20Metadata, ERC20MetadataData } from "./modules/erc20-puppet/tables/ERC20Metadata.sol"; +import { Allowances } from "./modules/erc20-puppet/tables/Allowances.sol"; +import { TotalSupply } from "./modules/erc20-puppet/tables/TotalSupply.sol"; +import { ERC20Registry } from "./modules/erc20-puppet/tables/ERC20Registry.sol"; +import { ERC721Metadata, ERC721MetadataData } from "./modules/erc721-puppet/tables/ERC721Metadata.sol"; +import { TokenURI } from "./modules/erc721-puppet/tables/TokenURI.sol"; +import { Owners } from "./modules/erc721-puppet/tables/Owners.sol"; +import { TokenApproval } from "./modules/erc721-puppet/tables/TokenApproval.sol"; +import { OperatorApproval } from "./modules/erc721-puppet/tables/OperatorApproval.sol"; +import { ERC721Registry } from "./modules/erc721-puppet/tables/ERC721Registry.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/interfaces/IPuppetFactorySystem.sol b/packages/world-modules/src/interfaces/IPuppetFactorySystem.sol new file mode 100644 index 0000000000..6166baa98b --- /dev/null +++ b/packages/world-modules/src/interfaces/IPuppetFactorySystem.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.21; + +/* Autogenerated file. Do not edit manually. */ + +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; + +/** + * @title IPuppetFactorySystem + * @dev This interface is automatically generated from the corresponding system contract. Do not edit manually. + */ +interface IPuppetFactorySystem { + function createPuppet(ResourceId systemId) external returns (address puppet); +} 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..e51df95407 --- /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 { Balances } from "../tokens/tables/Balances.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 { Allowances } from "./tables/Allowances.sol"; +import { ERC20Metadata, ERC20MetadataData } from "./tables/ERC20Metadata.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)); + ERC20Metadata.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, ERC20MetadataData memory metadata) = abi.decode(args, (bytes14, ERC20MetadataData)); + + // 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 + ERC20Metadata.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..8c5a5474e4 --- /dev/null +++ b/packages/world-modules/src/modules/erc20-puppet/ERC20System.sol @@ -0,0 +1,286 @@ +// 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 { AccessControlLib } from "../../utils/AccessControlLib.sol"; +import { PuppetMaster } from "../puppet/PuppetMaster.sol"; +import { toTopic } from "../puppet/utils.sol"; +import { Balances } from "../tokens/tables/Balances.sol"; + +import { IERC20Mintable } from "./IERC20Mintable.sol"; + +import { Allowances } from "./tables/Allowances.sol"; +import { TotalSupply } from "./tables/TotalSupply.sol"; +import { ERC20Metadata } from "./tables/ERC20Metadata.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 ERC20Metadata.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 ERC20Metadata.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 ERC20Metadata.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..2129a3dc77 --- /dev/null +++ b/packages/world-modules/src/modules/erc20-puppet/IERC20.sol @@ -0,0 +1,94 @@ +// 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"; +import { IERC20Errors } from "./IERC20Errors.sol"; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 is IERC20Events, IERC20Errors { + /** + * @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..3e5ece4d28 --- /dev/null +++ b/packages/world-modules/src/modules/erc20-puppet/IERC20Mintable.sol @@ -0,0 +1,25 @@ +// 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"; + +/** + * @dev Extending the ERC20 standard with permissioned mint and burn functions. + */ +interface IERC20Mintable is IERC20 { + /** + * @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..fb9d780a95 --- /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 { ERC20MetadataData } from "./tables/ERC20Metadata.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, + ERC20MetadataData 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/ERC20Metadata.sol b/packages/world-modules/src/modules/erc20-puppet/tables/ERC20Metadata.sol new file mode 100644 index 0000000000..c62353a8e4 --- /dev/null +++ b/packages/world-modules/src/modules/erc20-puppet/tables/ERC20Metadata.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 ERC20MetadataData { + uint8 decimals; + string name; + string symbol; +} + +library ERC20Metadata { + /** + * @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 (ERC20MetadataData 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 (ERC20MetadataData 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, ERC20MetadataData 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, ERC20MetadataData 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 (ERC20MetadataData 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/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/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/src/modules/erc721-puppet/ERC721Module.sol b/packages/world-modules/src/modules/erc721-puppet/ERC721Module.sol new file mode 100644 index 0000000000..579529e98f --- /dev/null +++ b/packages/world-modules/src/modules/erc721-puppet/ERC721Module.sol @@ -0,0 +1,101 @@ +// 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 { PuppetModule } from "../puppet/PuppetModule.sol"; +import { Balances } from "../tokens/tables/Balances.sol"; + +import { MODULE_NAME, MODULE_NAMESPACE, MODULE_NAMESPACE_ID, ERC721_REGISTRY_TABLE_ID } from "./constants.sol"; +import { _erc721SystemId, _balancesTableId, _metadataTableId, _tokenUriTableId, _operatorApprovalTableId, _ownersTableId, _tokenApprovalTableId } from "./utils.sol"; +import { ERC721System } from "./ERC721System.sol"; + +import { OperatorApproval } from "./tables/OperatorApproval.sol"; +import { Owners } from "./tables/Owners.sol"; +import { TokenApproval } from "./tables/TokenApproval.sol"; +import { TokenURI } from "./tables/TokenURI.sol"; +import { ERC721Registry } from "./tables/ERC721Registry.sol"; +import { ERC721Metadata, ERC721MetadataData } from "./tables/ERC721Metadata.sol"; + +contract ERC721Module is Module { + error ERC721Module_InvalidNamespace(bytes14 namespace); + + function getName() public pure override returns (bytes16) { + return MODULE_NAME; + } + + /** + * Register systems and tables for a new ERC721 token in a given namespace + */ + function _registerERC721(bytes14 namespace) internal { + // Register the tables + + OperatorApproval.register(_operatorApprovalTableId(namespace)); + Owners.register(_ownersTableId(namespace)); + TokenApproval.register(_tokenApprovalTableId(namespace)); + TokenURI.register(_tokenUriTableId(namespace)); + Balances.register(_balancesTableId(namespace)); + ERC721Metadata.register(_metadataTableId(namespace)); + + // Register a new ERC20System + IBaseWorld(_world()).registerSystem(_erc721SystemId(namespace), new ERC721System(), true); + } + + function _requireDependencies() internal { + // If the PuppetModule is not installed yet, install it + if (InstalledModules.get(PUPPET_MODULE_NAME, keccak256(new bytes(0))) == address(0)) { + IBaseWorld(_world()).installModule(new PuppetModule(), new bytes(0)); + } + } + + 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, ERC721MetadataData memory metadata) = abi.decode(args, (bytes14, ERC721MetadataData)); + + // Require the namespace to not be the module's namespace + if (namespace == MODULE_NAMESPACE) { + revert ERC721Module_InvalidNamespace(namespace); + } + + // Require dependencies + _requireDependencies(); + + // Register the ERC721 tables and system + _registerERC721(namespace); + + // Initialize the Metadata + ERC721Metadata.set(_metadataTableId(namespace), metadata); + + // Deploy and register the ERC721 puppet. + IBaseWorld world = IBaseWorld(_world()); + ResourceId erc721SystemId = _erc721SystemId(namespace); + address puppet = createPuppet(world, erc721SystemId); + + // Transfer ownership of the namespace to the caller + ResourceId namespaceId = WorldResourceIdLib.encodeNamespace(namespace); + world.transferOwnership(namespaceId, _msgSender()); + + // Register the ERC721 in the ERC20Registry + if (!ResourceIds.getExists(ERC721_REGISTRY_TABLE_ID)) { + ERC721Registry.register(ERC721_REGISTRY_TABLE_ID); + } + ERC721Registry.set(ERC721_REGISTRY_TABLE_ID, namespaceId, puppet); + } + + function installRoot(bytes memory) public pure { + revert Module_RootInstallNotSupported(); + } +} diff --git a/packages/world-modules/src/modules/erc721-puppet/ERC721System.sol b/packages/world-modules/src/modules/erc721-puppet/ERC721System.sol new file mode 100644 index 0000000000..b4ce186ef8 --- /dev/null +++ b/packages/world-modules/src/modules/erc721-puppet/ERC721System.sol @@ -0,0 +1,529 @@ +// 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 { SystemRegistry } from "@latticexyz/world/src/codegen/tables/SystemRegistry.sol"; + +import { AccessControlLib } from "../../utils/AccessControlLib.sol"; +import { PuppetMaster } from "../puppet/PuppetMaster.sol"; +import { toTopic } from "../puppet/utils.sol"; +import { Balances } from "../tokens/tables/Balances.sol"; + +import { IERC721Mintable } from "./IERC721Mintable.sol"; +import { IERC721Receiver } from "./IERC721Receiver.sol"; + +import { ERC721Metadata } from "./tables/ERC721Metadata.sol"; +import { OperatorApproval } from "./tables/OperatorApproval.sol"; +import { Owners } from "./tables/Owners.sol"; +import { TokenApproval } from "./tables/TokenApproval.sol"; +import { TokenURI } from "./tables/TokenURI.sol"; + +import { _balancesTableId, _metadataTableId, _tokenUriTableId, _operatorApprovalTableId, _ownersTableId, _tokenApprovalTableId } from "./utils.sol"; + +contract ERC721System is IERC721Mintable, System, PuppetMaster { + using WorldResourceIdInstance for ResourceId; + + /** + * @dev See {IERC721-balanceOf}. + */ + function balanceOf(address owner) public view virtual returns (uint256) { + if (owner == address(0)) { + revert ERC721InvalidOwner(address(0)); + } + return Balances.get(_balancesTableId(_namespace()), owner); + } + + /** + * @dev See {IERC721-ownerOf}. + */ + function ownerOf(uint256 tokenId) public view virtual returns (address) { + return _requireOwned(tokenId); + } + + /** + * @dev See {IERC721Metadata-name}. + */ + function name() public view virtual returns (string memory) { + return ERC721Metadata.getName(_metadataTableId(_namespace())); + } + + /** + * @dev See {IERC721Metadata-symbol}. + */ + function symbol() public view virtual returns (string memory) { + return ERC721Metadata.getSymbol(_metadataTableId(_namespace())); + } + + /** + * @dev See {IERC721Metadata-tokenURI}. + */ + function tokenURI(uint256 tokenId) public view virtual returns (string memory) { + _requireOwned(tokenId); + + string memory baseURI = _baseURI(); + string memory _tokenURI = TokenURI.get(_tokenUriTableId(_namespace()), tokenId); + _tokenURI = bytes(_tokenURI).length > 0 ? _tokenURI : string(abi.encodePacked(tokenId)); + return bytes(baseURI).length > 0 ? string.concat(baseURI, _tokenURI) : _tokenURI; + } + + /** + * @dev Base URI for computing {tokenURI}. If set, the resulting URI for each + * token will be the concatenation of the `baseURI` and the `tokenId`. + */ + function _baseURI() internal view virtual returns (string memory) { + return ERC721Metadata.getBaseURI(_metadataTableId(_namespace())); + } + + /** + * @dev See {IERC721-approve}. + */ + function approve(address to, uint256 tokenId) public virtual { + _approve(to, tokenId, _msgSender()); + } + + /** + * @dev See {IERC721-getApproved}. + */ + function getApproved(uint256 tokenId) public view virtual returns (address) { + _requireOwned(tokenId); + + return _getApproved(tokenId); + } + + /** + * @dev See {IERC721-setApprovalForAll}. + */ + function setApprovalForAll(address operator, bool approved) public virtual { + _setApprovalForAll(_msgSender(), operator, approved); + } + + /** + * @dev See {IERC721-isApprovedForAll}. + */ + function isApprovedForAll(address owner, address operator) public view virtual returns (bool) { + return OperatorApproval.get(_operatorApprovalTableId(_namespace()), owner, operator); + } + + /** + * @dev See {IERC721-transferFrom}. + */ + function transferFrom(address from, address to, uint256 tokenId) public virtual { + if (to == address(0)) { + revert ERC721InvalidReceiver(address(0)); + } + // Setting an "auth" arguments enables the `_isAuthorized` check which verifies that the token exists + // (from != 0). Therefore, it is not needed to verify that the return value is not 0 here. + address previousOwner = _update(to, tokenId, _msgSender()); + if (previousOwner != from) { + revert ERC721IncorrectOwner(from, tokenId, previousOwner); + } + } + + /** + * @dev See {IERC721-safeTransferFrom}. + */ + function safeTransferFrom(address from, address to, uint256 tokenId) public { + safeTransferFrom(from, to, tokenId, ""); + } + + /** + * @dev See {IERC721-safeTransferFrom}. + */ + function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual { + transferFrom(from, to, tokenId); + _checkOnERC721Received(from, to, tokenId, data); + } + + /** + * @dev Mints `tokenId` and transfers it to `to`. + * + * Requirements: + * + * - caller must own the namespace + * - `tokenId` must not exist. + * - `to` cannot be the zero address. + * + * Emits a {Transfer} event. + */ + function mint(address to, uint256 tokenId) public virtual { + _requireOwner(); + _mint(to, tokenId); + } + + /** + * @dev Mints `tokenId`, transfers it to `to` and checks for `to` acceptance. + * + * Requirements: + * + * - caller must own the namespace + * - `tokenId` must not exist. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + function safeMint(address to, uint256 tokenId) public { + _requireOwner(); + _safeMint(to, tokenId, ""); + } + + /** + * @dev Same as {xref-ERC721-safeMint-address-uint256-}[`safeMint`], with an additional `data` parameter which is + * forwarded in {IERC721Receiver-onERC721Received} to contract recipients. + */ + function safeMint(address to, uint256 tokenId, bytes memory data) public virtual { + _requireOwner(); + _safeMint(to, tokenId, data); + } + + /** + * @dev Destroys `tokenId`. + * The approval is cleared when the token is burned. + * + * Requirements: + * - caller must own the namespace + * - `tokenId` must exist. + * + * Emits a {Transfer} event. + */ + function burn(uint256 tokenId) public { + _requireOwner(); + _burn(tokenId); + } + + /** + * @dev Returns the owner of the `tokenId`. Does NOT revert if token doesn't exist + * + * IMPORTANT: Any overrides to this function that add ownership of tokens not tracked by the + * core ERC721 logic MUST be matched with the use of {_increaseBalance} to keep balances + * consistent with ownership. The invariant to preserve is that for any address `a` the value returned by + * `balanceOf(a)` must be equal to the number of tokens such that `_ownerOf(tokenId)` is `a`. + */ + function _ownerOf(uint256 tokenId) internal view virtual returns (address) { + return Owners.get(_ownersTableId(_namespace()), tokenId); + } + + /** + * @dev Returns the approved address for `tokenId`. Returns 0 if `tokenId` is not minted. + */ + function _getApproved(uint256 tokenId) internal view virtual returns (address) { + return TokenApproval.get(_tokenApprovalTableId(_namespace()), tokenId); + } + + /** + * @dev Returns whether `spender` is allowed to manage `owner`'s tokens, or `tokenId` in + * particular (ignoring whether it is owned by `owner`). + * + * WARNING: This function assumes that `owner` is the actual owner of `tokenId` and does not verify this + * assumption. + */ + function _isAuthorized(address owner, address spender, uint256 tokenId) internal view virtual returns (bool) { + return + spender != address(0) && + (owner == spender || isApprovedForAll(owner, spender) || _getApproved(tokenId) == spender); + } + + /** + * @dev Checks if `spender` can operate on `tokenId`, assuming the provided `owner` is the actual owner. + * Reverts if `spender` does not have approval from the provided `owner` for the given token or for all its assets + * the `spender` for the specific `tokenId`. + * + * WARNING: This function assumes that `owner` is the actual owner of `tokenId` and does not verify this + * assumption. + */ + function _checkAuthorized(address owner, address spender, uint256 tokenId) internal view virtual { + if (!_isAuthorized(owner, spender, tokenId)) { + if (owner == address(0)) { + revert ERC721NonexistentToken(tokenId); + } else { + revert ERC721InsufficientApproval(spender, tokenId); + } + } + } + + /** + * @dev Unsafe write access to the balances, used by extensions that "mint" tokens using an {ownerOf} override. + * + * NOTE: the value is limited to type(uint128).max. This protect against _balance overflow. It is unrealistic that + * a uint256 would ever overflow from increments when these increments are bounded to uint128 values. + * + * WARNING: Increasing an account's balance using this function tends to be paired with an override of the + * {_ownerOf} function to resolve the ownership of the corresponding tokens so that balances and ownership + * remain consistent with one another. + */ + function _increaseBalance(address account, uint128 value) internal virtual { + ResourceId balanceTableId = _balancesTableId(_namespace()); + unchecked { + Balances.set(balanceTableId, account, Balances.get(balanceTableId, account) + value); + } + } + + /** + * @dev Transfers `tokenId` from its current owner to `to`, or alternatively mints (or burns) if the current owner + * (or `to`) is the zero address. Returns the owner of the `tokenId` before the update. + * + * The `auth` argument is optional. If the value passed is non 0, then this function will check that + * `auth` is either the owner of the token, or approved to operate on the token (by the owner). + * + * Emits a {Transfer} event. + * + * NOTE: If overriding this function in a way that tracks balances, see also {_increaseBalance}. + */ + function _update(address to, uint256 tokenId, address auth) internal virtual returns (address) { + ResourceId balanceTableId = _balancesTableId(_namespace()); + address from = _ownerOf(tokenId); + + // Perform (optional) operator check + if (auth != address(0)) { + _checkAuthorized(from, auth, tokenId); + } + + // Execute the update + if (from != address(0)) { + // Clear approval. No need to re-authorize or emit the Approval event + _approve(address(0), tokenId, address(0), false); + + unchecked { + Balances.set(balanceTableId, from, Balances.get(balanceTableId, from) - 1); + } + } + + if (to != address(0)) { + unchecked { + Balances.set(balanceTableId, to, Balances.get(balanceTableId, to) + 1); + } + } + + Owners.set(_ownersTableId(_namespace()), tokenId, to); + + // Emit Transfer event on puppet + puppet().log(Transfer.selector, toTopic(from), toTopic(to), toTopic(tokenId), new bytes(0)); + + return from; + } + + /** + * @dev Mints `tokenId` and transfers it to `to`. + * + * WARNING: Usage of this method is discouraged, use {_safeMint} whenever possible + * + * Requirements: + * + * - `tokenId` must not exist. + * - `to` cannot be the zero address. + * + * Emits a {Transfer} event. + */ + function _mint(address to, uint256 tokenId) internal { + if (to == address(0)) { + revert ERC721InvalidReceiver(address(0)); + } + address previousOwner = _update(to, tokenId, address(0)); + if (previousOwner != address(0)) { + revert ERC721InvalidSender(address(0)); + } + } + + /** + * @dev Mints `tokenId`, transfers it to `to` and checks for `to` acceptance. + * + * Requirements: + * + * - `tokenId` must not exist. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + function _safeMint(address to, uint256 tokenId) internal { + _safeMint(to, tokenId, ""); + } + + /** + * @dev Same as {xref-ERC721-_safeMint-address-uint256-}[`_safeMint`], with an additional `data` parameter which is + * forwarded in {IERC721Receiver-onERC721Received} to contract recipients. + */ + function _safeMint(address to, uint256 tokenId, bytes memory data) internal virtual { + _mint(to, tokenId); + _checkOnERC721Received(address(0), to, tokenId, data); + } + + /** + * @dev Destroys `tokenId`. + * The approval is cleared when the token is burned. + * This is an internal function that does not check if the sender is authorized to operate on the token. + * + * Requirements: + * + * - `tokenId` must exist. + * + * Emits a {Transfer} event. + */ + function _burn(uint256 tokenId) internal { + address previousOwner = _update(address(0), tokenId, address(0)); + if (previousOwner == address(0)) { + revert ERC721NonexistentToken(tokenId); + } + } + + /** + * @dev Transfers `tokenId` from `from` to `to`. + * As opposed to {transferFrom}, this imposes no restrictions on msg.sender. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * + * Emits a {Transfer} event. + */ + function _transfer(address from, address to, uint256 tokenId) internal { + if (to == address(0)) { + revert ERC721InvalidReceiver(address(0)); + } + address previousOwner = _update(to, tokenId, address(0)); + if (previousOwner == address(0)) { + revert ERC721NonexistentToken(tokenId); + } else if (previousOwner != from) { + revert ERC721IncorrectOwner(from, tokenId, previousOwner); + } + } + + /** + * @dev Safely transfers `tokenId` token from `from` to `to`, checking that contract recipients + * are aware of the ERC721 standard to prevent tokens from being forever locked. + * + * `data` is additional data, it has no specified format and it is sent in call to `to`. + * + * This internal function is like {safeTransferFrom} in the sense that it invokes + * {IERC721Receiver-onERC721Received} on the receiver, and can be used to e.g. + * implement alternative mechanisms to perform token transfer, such as signature-based. + * + * Requirements: + * + * - `tokenId` token must exist and be owned by `from`. + * - `to` cannot be the zero address. + * - `from` cannot be the zero address. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + function _safeTransfer(address from, address to, uint256 tokenId) internal { + _safeTransfer(from, to, tokenId, ""); + } + + /** + * @dev Same as {xref-ERC721-_safeTransfer-address-address-uint256-}[`_safeTransfer`], with an additional `data` parameter which is + * forwarded in {IERC721Receiver-onERC721Received} to contract recipients. + */ + function _safeTransfer(address from, address to, uint256 tokenId, bytes memory data) internal virtual { + _transfer(from, to, tokenId); + _checkOnERC721Received(from, to, tokenId, data); + } + + /** + * @dev Approve `to` to operate on `tokenId` + * + * The `auth` argument is optional. If the value passed is non 0, then this function will check that `auth` is + * either the owner of the token, or approved to operate on all tokens held by this owner. + * + * Emits an {Approval} event. + * + * Overrides to this logic should be done to the variant with an additional `bool emitEvent` argument. + */ + function _approve(address to, uint256 tokenId, address auth) internal { + _approve(to, tokenId, auth, true); + } + + /** + * @dev Variant of `_approve` with an optional flag to enable or disable the {Approval} event. The event is not + * emitted in the context of transfers. + */ + function _approve(address to, uint256 tokenId, address auth, bool emitEvent) internal virtual { + // Avoid reading the owner unless necessary + if (emitEvent || auth != address(0)) { + address owner = _requireOwned(tokenId); + + // We do not use _isAuthorized because single-token approvals should not be able to call approve + if (auth != address(0) && owner != auth && !isApprovedForAll(owner, auth)) { + revert ERC721InvalidApprover(auth); + } + + if (emitEvent) { + // Emit Approval event on puppet + puppet().log(Approval.selector, toTopic(owner), toTopic(to), toTopic(tokenId), new bytes(0)); + } + } + + TokenApproval.set(_tokenApprovalTableId(_namespace()), tokenId, to); + } + + /** + * @dev Approve `operator` to operate on all of `owner` tokens + * + * Requirements: + * - operator can't be the address zero. + * + * Emits an {ApprovalForAll} event. + */ + function _setApprovalForAll(address owner, address operator, bool approved) internal virtual { + if (operator == address(0)) { + revert ERC721InvalidOperator(operator); + } + OperatorApproval.set(_operatorApprovalTableId(_namespace()), owner, operator, approved); + + // Emit ApprovalForAll event on puppet + puppet().log(ApprovalForAll.selector, toTopic(owner), toTopic(operator), abi.encode(approved)); + } + + /** + * @dev Reverts if the `tokenId` doesn't have a current owner (it hasn't been minted, or it has been burned). + * Returns the owner. + * + * Overrides to ownership logic should be done to {_ownerOf}. + */ + function _requireOwned(uint256 tokenId) internal view returns (address) { + address owner = _ownerOf(tokenId); + if (owner == address(0)) { + revert ERC721NonexistentToken(tokenId); + } + return owner; + } + + /** + * @dev Private function to invoke {IERC721Receiver-onERC721Received} on a target address. This will revert if the + * recipient doesn't accept the token transfer. The call is not executed if the target address is not a contract. + * + * @param from address representing the previous owner of the given token ID + * @param to target address that will receive the tokens + * @param tokenId uint256 ID of the token to be transferred + * @param data bytes optional data to send along with the call + */ + function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory data) private { + if (to.code.length > 0) { + try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, data) returns (bytes4 retval) { + if (retval != IERC721Receiver.onERC721Received.selector) { + revert ERC721InvalidReceiver(to); + } + } catch (bytes memory reason) { + if (reason.length == 0) { + revert ERC721InvalidReceiver(to); + } else { + /// @solidity memory-safe-assembly + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + } + } + + 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/erc721-puppet/IERC721.sol b/packages/world-modules/src/modules/erc721-puppet/IERC721.sol new file mode 100644 index 0000000000..9d42965d5a --- /dev/null +++ b/packages/world-modules/src/modules/erc721-puppet/IERC721.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC721/IERC721.sol) +pragma solidity >=0.8.21; + +import { IERC721Events } from "./IERC721Events.sol"; +import { IERC721Errors } from "./IERC721Errors.sol"; + +/** + * @dev Required interface of an ERC721 compliant contract. + */ +interface IERC721 is IERC721Events, IERC721Errors { + /** + * @dev Returns the number of tokens in ``owner``'s account. + */ + function balanceOf(address owner) external view returns (uint256 balance); + + /** + * @dev Returns the owner of the `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function ownerOf(uint256 tokenId) external view returns (address owner); + + /** + * @dev Safely transfers `tokenId` token from `from` to `to`. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon + * a safe transfer. + * + * Emits a {Transfer} event. + */ + function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external; + + /** + * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients + * are aware of the ERC721 protocol to prevent tokens from being forever locked. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If the caller is not `from`, it must have been allowed to move this token by either {approve} or + * {setApprovalForAll}. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon + * a safe transfer. + * + * Emits a {Transfer} event. + */ + function safeTransferFrom(address from, address to, uint256 tokenId) external; + + /** + * @dev Transfers `tokenId` token from `from` to `to`. + * + * WARNING: Note that the caller is responsible to confirm that the recipient is capable of receiving ERC721 + * or else they may be permanently lost. Usage of {safeTransferFrom} prevents loss, though the caller must + * understand this adds an external call which potentially creates a reentrancy vulnerability. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. + * + * Emits a {Transfer} event. + */ + function transferFrom(address from, address to, uint256 tokenId) external; + + /** + * @dev Gives permission to `to` to transfer `tokenId` token to another account. + * The approval is cleared when the token is transferred. + * + * Only a single account can be approved at a time, so approving the zero address clears previous approvals. + * + * Requirements: + * + * - The caller must own the token or be an approved operator. + * - `tokenId` must exist. + * + * Emits an {Approval} event. + */ + function approve(address to, uint256 tokenId) external; + + /** + * @dev Approve or remove `operator` as an operator for the caller. + * Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller. + * + * Requirements: + * + * - The `operator` cannot be the address zero. + * + * Emits an {ApprovalForAll} event. + */ + function setApprovalForAll(address operator, bool approved) external; + + /** + * @dev Returns the account approved for `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function getApproved(uint256 tokenId) external view returns (address operator); + + /** + * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`. + * + * See {setApprovalForAll} + */ + function isApprovedForAll(address owner, address operator) external view returns (bool); +} diff --git a/packages/world-modules/src/modules/erc721-puppet/IERC721Errors.sol b/packages/world-modules/src/modules/erc721-puppet/IERC721Errors.sol new file mode 100644 index 0000000000..5e287daec0 --- /dev/null +++ b/packages/world-modules/src/modules/erc721-puppet/IERC721Errors.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/draft-IERC6093.sol) +pragma solidity >=0.8.21; + +/** + * @dev Standard ERC721 Errors + * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC721 tokens. + */ +interface IERC721Errors { + /** + * @dev Indicates that an address can't be an owner. For example, `address(0)` is a forbidden owner in EIP-20. + * Used in balance queries. + * @param owner Address of the current owner of a token. + */ + error ERC721InvalidOwner(address owner); + + /** + * @dev Indicates a `tokenId` whose `owner` is the zero address. + * @param tokenId Identifier number of a token. + */ + error ERC721NonexistentToken(uint256 tokenId); + + /** + * @dev Indicates an error related to the ownership over a particular token. Used in transfers. + * @param sender Address whose tokens are being transferred. + * @param tokenId Identifier number of a token. + * @param owner Address of the current owner of a token. + */ + error ERC721IncorrectOwner(address sender, uint256 tokenId, address owner); + + /** + * @dev Indicates a failure with the token `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + */ + error ERC721InvalidSender(address sender); + + /** + * @dev Indicates a failure with the token `receiver`. Used in transfers. + * @param receiver Address to which tokens are being transferred. + */ + error ERC721InvalidReceiver(address receiver); + + /** + * @dev Indicates a failure with the `operator`’s approval. Used in transfers. + * @param operator Address that may be allowed to operate on tokens without being their owner. + * @param tokenId Identifier number of a token. + */ + error ERC721InsufficientApproval(address operator, uint256 tokenId); + + /** + * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals. + * @param approver Address initiating an approval operation. + */ + error ERC721InvalidApprover(address approver); + + /** + * @dev Indicates a failure with the `operator` to be approved. Used in approvals. + * @param operator Address that may be allowed to operate on tokens without being their owner. + */ + error ERC721InvalidOperator(address operator); +} diff --git a/packages/world-modules/src/modules/erc721-puppet/IERC721Events.sol b/packages/world-modules/src/modules/erc721-puppet/IERC721Events.sol new file mode 100644 index 0000000000..fde5124465 --- /dev/null +++ b/packages/world-modules/src/modules/erc721-puppet/IERC721Events.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC721/IERC721.sol) +pragma solidity >=0.8.21; + +/** + * @dev Events emitted by an ERC721 compliant contract. + */ +interface IERC721Events { + /** + * @dev Emitted when `tokenId` token is transferred from `from` to `to`. + */ + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + + /** + * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token. + */ + event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); + + /** + * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets. + */ + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); +} diff --git a/packages/world-modules/src/modules/erc721-puppet/IERC721Metadata.sol b/packages/world-modules/src/modules/erc721-puppet/IERC721Metadata.sol new file mode 100644 index 0000000000..fd6621cf14 --- /dev/null +++ b/packages/world-modules/src/modules/erc721-puppet/IERC721Metadata.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC721/extensions/IERC721Metadata.sol) +pragma solidity >=0.8.21; + +import { IERC721 } from "./IERC721.sol"; + +/** + * @title ERC-721 Non-Fungible Token Standard, optional metadata extension + * @dev See https://eips.ethereum.org/EIPS/eip-721 + */ +interface IERC721Metadata is IERC721 { + /** + * @dev Returns the token collection name. + */ + function name() external view returns (string memory); + + /** + * @dev Returns the token collection symbol. + */ + function symbol() external view returns (string memory); + + /** + * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token. + */ + function tokenURI(uint256 tokenId) external view returns (string memory); +} diff --git a/packages/world-modules/src/modules/erc721-puppet/IERC721Mintable.sol b/packages/world-modules/src/modules/erc721-puppet/IERC721Mintable.sol new file mode 100644 index 0000000000..6a7e570e7f --- /dev/null +++ b/packages/world-modules/src/modules/erc721-puppet/IERC721Mintable.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/IERC20.sol) +pragma solidity >=0.8.21; + +import { IERC721 } from "./IERC721.sol"; + +/** + * @dev Extending the ERC721 standard with permissioned mint and burn functions. + */ +interface IERC721Mintable is IERC721 { + /** + * @dev Mints `tokenId` and transfers it to `to`. + * + * Requirements: + * + * - `tokenId` must not exist. + * - `to` cannot be the zero address. + * + * Emits a {Transfer} event. + */ + function mint(address to, uint256 tokenId) external; + + /** + * @dev Mints `tokenId`, transfers it to `to` and checks for `to` acceptance. + * + * Requirements: + * + * - caller must own the namespace + * - `tokenId` must not exist. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + function safeMint(address to, uint256 tokenId) external; + + /** + * @dev Same as {xref-ERC721-safeMint-address-uint256-}[`safeMint`], with an additional `data` parameter which is + * forwarded in {IERC721Receiver-onERC721Received} to contract recipients. + */ + function safeMint(address to, uint256 tokenId, bytes memory data) external; + + /** + * @dev Destroys `tokenId`. + * The approval is cleared when the token is burned. + * + * Requirements: + * + * - `tokenId` must exist. + * + * Emits a {Transfer} event. + */ + function burn(uint256 tokenId) external; +} diff --git a/packages/world-modules/src/modules/erc721-puppet/IERC721Receiver.sol b/packages/world-modules/src/modules/erc721-puppet/IERC721Receiver.sol new file mode 100644 index 0000000000..f974fab71d --- /dev/null +++ b/packages/world-modules/src/modules/erc721-puppet/IERC721Receiver.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC721/IERC721Receiver.sol) +pragma solidity >=0.8.21; + +/** + * @title ERC721 token receiver interface + * @dev Interface for any contract that wants to support safeTransfers + * from ERC721 asset contracts. + */ +interface IERC721Receiver { + /** + * @dev Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom} + * by `operator` from `from`, this function is called. + * + * It must return its Solidity selector to confirm the token transfer. + * If any other value is returned or the interface is not implemented by the recipient, the transfer will be + * reverted. + * + * The selector can be obtained in Solidity with `IERC721Receiver.onERC721Received.selector`. + */ + function onERC721Received( + address operator, + address from, + uint256 tokenId, + bytes calldata data + ) external returns (bytes4); +} diff --git a/packages/world-modules/src/modules/erc721-puppet/constants.sol b/packages/world-modules/src/modules/erc721-puppet/constants.sol new file mode 100644 index 0000000000..8967ed7e09 --- /dev/null +++ b/packages/world-modules/src/modules/erc721-puppet/constants.sol @@ -0,0 +1,24 @@ +// 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 = "erc721-puppet"; +bytes14 constant MODULE_NAMESPACE = "erc721-puppet"; +ResourceId constant MODULE_NAMESPACE_ID = ResourceId.wrap( + bytes32(abi.encodePacked(RESOURCE_NAMESPACE, MODULE_NAMESPACE)) +); + +bytes16 constant TOKEN_URI_NAME = "TokenURI"; +bytes16 constant BALANCES_NAME = "Balances"; +bytes16 constant METADATA_NAME = "Metadata"; +bytes16 constant OPERATOR_APPROVAL_NAME = "OperatorApproval"; +bytes16 constant TOKEN_APPROVAL_NAME = "TokenApproval"; +bytes16 constant OWNERS_NAME = "Owners"; + +bytes16 constant ERC721_SYSTEM_NAME = "ERC721System"; + +ResourceId constant ERC721_REGISTRY_TABLE_ID = ResourceId.wrap( + bytes32(abi.encodePacked(RESOURCE_TABLE, MODULE_NAMESPACE, bytes16("ERC721Registry"))) +); diff --git a/packages/world-modules/src/modules/erc721-puppet/registerERC721.sol b/packages/world-modules/src/modules/erc721-puppet/registerERC721.sol new file mode 100644 index 0000000000..b02415e779 --- /dev/null +++ b/packages/world-modules/src/modules/erc721-puppet/registerERC721.sol @@ -0,0 +1,37 @@ +// 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 { SystemSwitch } from "../../utils/SystemSwitch.sol"; + +import { ERC721Module } from "./ERC721Module.sol"; +import { MODULE_NAMESPACE_ID, ERC721_REGISTRY_TABLE_ID } from "./constants.sol"; +import { IERC721Mintable } from "./IERC721Mintable.sol"; + +import { ERC721MetadataData } from "./tables/ERC721Metadata.sol"; +import { ERC721Registry } from "./tables/ERC721Registry.sol"; + +/** + * @notice Register a new ERC721 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 registerERC721( + IBaseWorld world, + bytes14 namespace, + ERC721MetadataData memory metadata +) returns (IERC721Mintable token) { + // Get the ERC721 module + ERC721Module erc721Module = ERC721Module(NamespaceOwner.get(MODULE_NAMESPACE_ID)); + if (address(erc721Module) == address(0)) { + erc721Module = new ERC721Module(); + } + + // Install the ERC721 module with the provided args + world.installModule(erc721Module, abi.encode(namespace, metadata)); + + // Return the newly created ERC721 token + token = IERC721Mintable(ERC721Registry.get(ERC721_REGISTRY_TABLE_ID, WorldResourceIdLib.encodeNamespace(namespace))); +} diff --git a/packages/world-modules/src/modules/erc721-puppet/tables/ERC721Metadata.sol b/packages/world-modules/src/modules/erc721-puppet/tables/ERC721Metadata.sol new file mode 100644 index 0000000000..3586fa252f --- /dev/null +++ b/packages/world-modules/src/modules/erc721-puppet/tables/ERC721Metadata.sol @@ -0,0 +1,734 @@ +// 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( + 0x0000000300000000000000000000000000000000000000000000000000000000 +); + +struct ERC721MetadataData { + string name; + string symbol; + string baseURI; +} + +library ERC721Metadata { + /** + * @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.STRING; + _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] = "name"; + fieldNames[1] = "symbol"; + fieldNames[2] = "baseURI"; + } + + /** + * @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 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 baseURI. + */ + function getBaseURI(ResourceId _tableId) internal view returns (string memory baseURI) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes memory _blob = StoreSwitch.getDynamicField(_tableId, _keyTuple, 2); + return (string(_blob)); + } + + /** + * @notice Get baseURI. + */ + function _getBaseURI(ResourceId _tableId) internal view returns (string memory baseURI) { + bytes32[] memory _keyTuple = new bytes32[](0); + + bytes memory _blob = StoreCore.getDynamicField(_tableId, _keyTuple, 2); + return (string(_blob)); + } + + /** + * @notice Set baseURI. + */ + function setBaseURI(ResourceId _tableId, string memory baseURI) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.setDynamicField(_tableId, _keyTuple, 2, bytes((baseURI))); + } + + /** + * @notice Set baseURI. + */ + function _setBaseURI(ResourceId _tableId, string memory baseURI) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.setDynamicField(_tableId, _keyTuple, 2, bytes((baseURI))); + } + + /** + * @notice Get the length of baseURI. + */ + function lengthBaseURI(ResourceId _tableId) internal view returns (uint256) { + bytes32[] memory _keyTuple = new bytes32[](0); + + uint256 _byteLength = StoreSwitch.getDynamicFieldLength(_tableId, _keyTuple, 2); + unchecked { + return _byteLength / 1; + } + } + + /** + * @notice Get the length of baseURI. + */ + function _lengthBaseURI(ResourceId _tableId) internal view returns (uint256) { + bytes32[] memory _keyTuple = new bytes32[](0); + + uint256 _byteLength = StoreCore.getDynamicFieldLength(_tableId, _keyTuple, 2); + unchecked { + return _byteLength / 1; + } + } + + /** + * @notice Get an item of baseURI. + * @dev Reverts with Store_IndexOutOfBounds if `_index` is out of bounds for the array. + */ + function getItemBaseURI(ResourceId _tableId, uint256 _index) internal view returns (string memory) { + bytes32[] memory _keyTuple = new bytes32[](0); + + unchecked { + bytes memory _blob = StoreSwitch.getDynamicFieldSlice(_tableId, _keyTuple, 2, _index * 1, (_index + 1) * 1); + return (string(_blob)); + } + } + + /** + * @notice Get an item of baseURI. + * @dev Reverts with Store_IndexOutOfBounds if `_index` is out of bounds for the array. + */ + function _getItemBaseURI(ResourceId _tableId, uint256 _index) internal view returns (string memory) { + bytes32[] memory _keyTuple = new bytes32[](0); + + unchecked { + bytes memory _blob = StoreCore.getDynamicFieldSlice(_tableId, _keyTuple, 2, _index * 1, (_index + 1) * 1); + return (string(_blob)); + } + } + + /** + * @notice Push a slice to baseURI. + */ + function pushBaseURI(ResourceId _tableId, string memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.pushToDynamicField(_tableId, _keyTuple, 2, bytes((_slice))); + } + + /** + * @notice Push a slice to baseURI. + */ + function _pushBaseURI(ResourceId _tableId, string memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.pushToDynamicField(_tableId, _keyTuple, 2, bytes((_slice))); + } + + /** + * @notice Pop a slice from baseURI. + */ + function popBaseURI(ResourceId _tableId) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreSwitch.popFromDynamicField(_tableId, _keyTuple, 2, 1); + } + + /** + * @notice Pop a slice from baseURI. + */ + function _popBaseURI(ResourceId _tableId) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.popFromDynamicField(_tableId, _keyTuple, 2, 1); + } + + /** + * @notice Update a slice of baseURI at `_index`. + */ + function updateBaseURI(ResourceId _tableId, uint256 _index, string memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + unchecked { + bytes memory _encoded = bytes((_slice)); + StoreSwitch.spliceDynamicData(_tableId, _keyTuple, 2, uint40(_index * 1), uint40(_encoded.length), _encoded); + } + } + + /** + * @notice Update a slice of baseURI at `_index`. + */ + function _updateBaseURI(ResourceId _tableId, uint256 _index, string memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](0); + + unchecked { + bytes memory _encoded = bytes((_slice)); + StoreCore.spliceDynamicData(_tableId, _keyTuple, 2, uint40(_index * 1), uint40(_encoded.length), _encoded); + } + } + + /** + * @notice Get the full data. + */ + function get(ResourceId _tableId) internal view returns (ERC721MetadataData 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 (ERC721MetadataData 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, string memory name, string memory symbol, string memory baseURI) internal { + bytes memory _staticData; + PackedCounter _encodedLengths = encodeLengths(name, symbol, baseURI); + bytes memory _dynamicData = encodeDynamic(name, symbol, baseURI); + + 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, string memory name, string memory symbol, string memory baseURI) internal { + bytes memory _staticData; + PackedCounter _encodedLengths = encodeLengths(name, symbol, baseURI); + bytes memory _dynamicData = encodeDynamic(name, symbol, baseURI); + + 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, ERC721MetadataData memory _table) internal { + bytes memory _staticData; + PackedCounter _encodedLengths = encodeLengths(_table.name, _table.symbol, _table.baseURI); + bytes memory _dynamicData = encodeDynamic(_table.name, _table.symbol, _table.baseURI); + + 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, ERC721MetadataData memory _table) internal { + bytes memory _staticData; + PackedCounter _encodedLengths = encodeLengths(_table.name, _table.symbol, _table.baseURI); + bytes memory _dynamicData = encodeDynamic(_table.name, _table.symbol, _table.baseURI); + + bytes32[] memory _keyTuple = new bytes32[](0); + + StoreCore.setRecord(_tableId, _keyTuple, _staticData, _encodedLengths, _dynamicData, _fieldLayout); + } + + /** + * @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, string memory baseURI) { + 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())); + + _start = _end; + unchecked { + _end += _encodedLengths.atIndex(2); + } + baseURI = (string(SliceLib.getSubslice(_blob, _start, _end).toBytes())); + } + + /** + * @notice Decode the tightly packed blobs using this table's field layout. + * + * @param _encodedLengths Encoded lengths of dynamic fields. + * @param _dynamicData Tightly packed dynamic fields. + */ + function decode( + bytes memory, + PackedCounter _encodedLengths, + bytes memory _dynamicData + ) internal pure returns (ERC721MetadataData memory _table) { + (_table.name, _table.symbol, _table.baseURI) = 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 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, + string memory baseURI + ) 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, bytes(baseURI).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, + string memory baseURI + ) internal pure returns (bytes memory) { + return abi.encodePacked(bytes((name)), bytes((symbol)), bytes((baseURI))); + } + + /** + * @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( + string memory name, + string memory symbol, + string memory baseURI + ) internal pure returns (bytes memory, PackedCounter, bytes memory) { + bytes memory _staticData; + PackedCounter _encodedLengths = encodeLengths(name, symbol, baseURI); + bytes memory _dynamicData = encodeDynamic(name, symbol, baseURI); + + 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/erc721-puppet/tables/ERC721Registry.sol b/packages/world-modules/src/modules/erc721-puppet/tables/ERC721Registry.sol new file mode 100644 index 0000000000..61afb8017a --- /dev/null +++ b/packages/world-modules/src/modules/erc721-puppet/tables/ERC721Registry.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 ERC721Registry { + /** + * @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/erc721-puppet/tables/OperatorApproval.sol b/packages/world-modules/src/modules/erc721-puppet/tables/OperatorApproval.sol new file mode 100644 index 0000000000..0cf1a0d775 --- /dev/null +++ b/packages/world-modules/src/modules/erc721-puppet/tables/OperatorApproval.sol @@ -0,0 +1,251 @@ +// 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( + 0x0001010001000000000000000000000000000000000000000000000000000000 +); + +library OperatorApproval { + /** + * @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.BOOL; + + 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] = "owner"; + keyNames[1] = "operator"; + } + + /** + * @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] = "approved"; + } + + /** + * @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 approved. + */ + function getApproved(ResourceId _tableId, address owner, address operator) internal view returns (bool approved) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(owner))); + _keyTuple[1] = bytes32(uint256(uint160(operator))); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (_toBool(uint8(bytes1(_blob)))); + } + + /** + * @notice Get approved. + */ + function _getApproved(ResourceId _tableId, address owner, address operator) internal view returns (bool approved) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(owner))); + _keyTuple[1] = bytes32(uint256(uint160(operator))); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (_toBool(uint8(bytes1(_blob)))); + } + + /** + * @notice Get approved. + */ + function get(ResourceId _tableId, address owner, address operator) internal view returns (bool approved) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(owner))); + _keyTuple[1] = bytes32(uint256(uint160(operator))); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (_toBool(uint8(bytes1(_blob)))); + } + + /** + * @notice Get approved. + */ + function _get(ResourceId _tableId, address owner, address operator) internal view returns (bool approved) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(owner))); + _keyTuple[1] = bytes32(uint256(uint160(operator))); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (_toBool(uint8(bytes1(_blob)))); + } + + /** + * @notice Set approved. + */ + function setApproved(ResourceId _tableId, address owner, address operator, bool approved) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(owner))); + _keyTuple[1] = bytes32(uint256(uint160(operator))); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((approved)), _fieldLayout); + } + + /** + * @notice Set approved. + */ + function _setApproved(ResourceId _tableId, address owner, address operator, bool approved) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(owner))); + _keyTuple[1] = bytes32(uint256(uint160(operator))); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((approved)), _fieldLayout); + } + + /** + * @notice Set approved. + */ + function set(ResourceId _tableId, address owner, address operator, bool approved) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(owner))); + _keyTuple[1] = bytes32(uint256(uint160(operator))); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((approved)), _fieldLayout); + } + + /** + * @notice Set approved. + */ + function _set(ResourceId _tableId, address owner, address operator, bool approved) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(owner))); + _keyTuple[1] = bytes32(uint256(uint160(operator))); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((approved)), _fieldLayout); + } + + /** + * @notice Delete all data for given keys. + */ + function deleteRecord(ResourceId _tableId, address owner, address operator) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(owner))); + _keyTuple[1] = bytes32(uint256(uint160(operator))); + + StoreSwitch.deleteRecord(_tableId, _keyTuple); + } + + /** + * @notice Delete all data for given keys. + */ + function _deleteRecord(ResourceId _tableId, address owner, address operator) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(owner))); + _keyTuple[1] = bytes32(uint256(uint160(operator))); + + 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(bool approved) internal pure returns (bytes memory) { + return abi.encodePacked(approved); + } + + /** + * @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(bool approved) internal pure returns (bytes memory, PackedCounter, bytes memory) { + bytes memory _staticData = encodeStatic(approved); + + 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 owner, address operator) internal pure returns (bytes32[] memory) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = bytes32(uint256(uint160(owner))); + _keyTuple[1] = bytes32(uint256(uint160(operator))); + + return _keyTuple; + } +} + +/** + * @notice Cast a value to a bool. + * @dev Boolean values are encoded as uint8 (1 = true, 0 = false), but Solidity doesn't allow casting between uint8 and bool. + * @param value The uint8 value to convert. + * @return result The boolean value. + */ +function _toBool(uint8 value) pure returns (bool result) { + assembly { + result := value + } +} diff --git a/packages/world-modules/src/modules/erc721-puppet/tables/Owners.sol b/packages/world-modules/src/modules/erc721-puppet/tables/Owners.sol new file mode 100644 index 0000000000..139d8e092a --- /dev/null +++ b/packages/world-modules/src/modules/erc721-puppet/tables/Owners.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( + 0x0014010014000000000000000000000000000000000000000000000000000000 +); + +library Owners { + /** + * @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.UINT256; + + 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] = "tokenId"; + } + + /** + * @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] = "owner"; + } + + /** + * @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 owner. + */ + function getOwner(ResourceId _tableId, uint256 tokenId) internal view returns (address owner) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (address(bytes20(_blob))); + } + + /** + * @notice Get owner. + */ + function _getOwner(ResourceId _tableId, uint256 tokenId) internal view returns (address owner) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (address(bytes20(_blob))); + } + + /** + * @notice Get owner. + */ + function get(ResourceId _tableId, uint256 tokenId) internal view returns (address owner) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (address(bytes20(_blob))); + } + + /** + * @notice Get owner. + */ + function _get(ResourceId _tableId, uint256 tokenId) internal view returns (address owner) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (address(bytes20(_blob))); + } + + /** + * @notice Set owner. + */ + function setOwner(ResourceId _tableId, uint256 tokenId, address owner) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((owner)), _fieldLayout); + } + + /** + * @notice Set owner. + */ + function _setOwner(ResourceId _tableId, uint256 tokenId, address owner) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((owner)), _fieldLayout); + } + + /** + * @notice Set owner. + */ + function set(ResourceId _tableId, uint256 tokenId, address owner) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((owner)), _fieldLayout); + } + + /** + * @notice Set owner. + */ + function _set(ResourceId _tableId, uint256 tokenId, address owner) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((owner)), _fieldLayout); + } + + /** + * @notice Delete all data for given keys. + */ + function deleteRecord(ResourceId _tableId, uint256 tokenId) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + StoreSwitch.deleteRecord(_tableId, _keyTuple); + } + + /** + * @notice Delete all data for given keys. + */ + function _deleteRecord(ResourceId _tableId, uint256 tokenId) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + 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 owner) internal pure returns (bytes memory) { + return abi.encodePacked(owner); + } + + /** + * @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 owner) internal pure returns (bytes memory, PackedCounter, bytes memory) { + bytes memory _staticData = encodeStatic(owner); + + PackedCounter _encodedLengths; + bytes memory _dynamicData; + + return (_staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Encode keys as a bytes32 array using this table's field layout. + */ + function encodeKeyTuple(uint256 tokenId) internal pure returns (bytes32[] memory) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + return _keyTuple; + } +} diff --git a/packages/world-modules/src/modules/erc721-puppet/tables/TokenApproval.sol b/packages/world-modules/src/modules/erc721-puppet/tables/TokenApproval.sol new file mode 100644 index 0000000000..908ade8d32 --- /dev/null +++ b/packages/world-modules/src/modules/erc721-puppet/tables/TokenApproval.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( + 0x0014010014000000000000000000000000000000000000000000000000000000 +); + +library TokenApproval { + /** + * @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.UINT256; + + 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] = "tokenId"; + } + + /** + * @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] = "account"; + } + + /** + * @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 account. + */ + function getAccount(ResourceId _tableId, uint256 tokenId) internal view returns (address account) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (address(bytes20(_blob))); + } + + /** + * @notice Get account. + */ + function _getAccount(ResourceId _tableId, uint256 tokenId) internal view returns (address account) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (address(bytes20(_blob))); + } + + /** + * @notice Get account. + */ + function get(ResourceId _tableId, uint256 tokenId) internal view returns (address account) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (address(bytes20(_blob))); + } + + /** + * @notice Get account. + */ + function _get(ResourceId _tableId, uint256 tokenId) internal view returns (address account) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (address(bytes20(_blob))); + } + + /** + * @notice Set account. + */ + function setAccount(ResourceId _tableId, uint256 tokenId, address account) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((account)), _fieldLayout); + } + + /** + * @notice Set account. + */ + function _setAccount(ResourceId _tableId, uint256 tokenId, address account) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((account)), _fieldLayout); + } + + /** + * @notice Set account. + */ + function set(ResourceId _tableId, uint256 tokenId, address account) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((account)), _fieldLayout); + } + + /** + * @notice Set account. + */ + function _set(ResourceId _tableId, uint256 tokenId, address account) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((account)), _fieldLayout); + } + + /** + * @notice Delete all data for given keys. + */ + function deleteRecord(ResourceId _tableId, uint256 tokenId) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + StoreSwitch.deleteRecord(_tableId, _keyTuple); + } + + /** + * @notice Delete all data for given keys. + */ + function _deleteRecord(ResourceId _tableId, uint256 tokenId) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + 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 account) internal pure returns (bytes memory) { + return abi.encodePacked(account); + } + + /** + * @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 account) internal pure returns (bytes memory, PackedCounter, bytes memory) { + bytes memory _staticData = encodeStatic(account); + + PackedCounter _encodedLengths; + bytes memory _dynamicData; + + return (_staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Encode keys as a bytes32 array using this table's field layout. + */ + function encodeKeyTuple(uint256 tokenId) internal pure returns (bytes32[] memory) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + return _keyTuple; + } +} diff --git a/packages/world-modules/src/modules/erc721-puppet/tables/TokenURI.sol b/packages/world-modules/src/modules/erc721-puppet/tables/TokenURI.sol new file mode 100644 index 0000000000..15ef83a978 --- /dev/null +++ b/packages/world-modules/src/modules/erc721-puppet/tables/TokenURI.sol @@ -0,0 +1,480 @@ +// 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( + 0x0000000100000000000000000000000000000000000000000000000000000000 +); + +library TokenURI { + /** + * @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.UINT256; + + 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.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[](1); + keyNames[0] = "tokenId"; + } + + /** + * @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] = "tokenURI"; + } + + /** + * @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 tokenURI. + */ + function getTokenURI(ResourceId _tableId, uint256 tokenId) internal view returns (string memory tokenURI) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + bytes memory _blob = StoreSwitch.getDynamicField(_tableId, _keyTuple, 0); + return (string(_blob)); + } + + /** + * @notice Get tokenURI. + */ + function _getTokenURI(ResourceId _tableId, uint256 tokenId) internal view returns (string memory tokenURI) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + bytes memory _blob = StoreCore.getDynamicField(_tableId, _keyTuple, 0); + return (string(_blob)); + } + + /** + * @notice Get tokenURI. + */ + function get(ResourceId _tableId, uint256 tokenId) internal view returns (string memory tokenURI) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + bytes memory _blob = StoreSwitch.getDynamicField(_tableId, _keyTuple, 0); + return (string(_blob)); + } + + /** + * @notice Get tokenURI. + */ + function _get(ResourceId _tableId, uint256 tokenId) internal view returns (string memory tokenURI) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + bytes memory _blob = StoreCore.getDynamicField(_tableId, _keyTuple, 0); + return (string(_blob)); + } + + /** + * @notice Set tokenURI. + */ + function setTokenURI(ResourceId _tableId, uint256 tokenId, string memory tokenURI) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + StoreSwitch.setDynamicField(_tableId, _keyTuple, 0, bytes((tokenURI))); + } + + /** + * @notice Set tokenURI. + */ + function _setTokenURI(ResourceId _tableId, uint256 tokenId, string memory tokenURI) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + StoreCore.setDynamicField(_tableId, _keyTuple, 0, bytes((tokenURI))); + } + + /** + * @notice Set tokenURI. + */ + function set(ResourceId _tableId, uint256 tokenId, string memory tokenURI) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + StoreSwitch.setDynamicField(_tableId, _keyTuple, 0, bytes((tokenURI))); + } + + /** + * @notice Set tokenURI. + */ + function _set(ResourceId _tableId, uint256 tokenId, string memory tokenURI) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + StoreCore.setDynamicField(_tableId, _keyTuple, 0, bytes((tokenURI))); + } + + /** + * @notice Get the length of tokenURI. + */ + function lengthTokenURI(ResourceId _tableId, uint256 tokenId) internal view returns (uint256) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + uint256 _byteLength = StoreSwitch.getDynamicFieldLength(_tableId, _keyTuple, 0); + unchecked { + return _byteLength / 1; + } + } + + /** + * @notice Get the length of tokenURI. + */ + function _lengthTokenURI(ResourceId _tableId, uint256 tokenId) internal view returns (uint256) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + uint256 _byteLength = StoreCore.getDynamicFieldLength(_tableId, _keyTuple, 0); + unchecked { + return _byteLength / 1; + } + } + + /** + * @notice Get the length of tokenURI. + */ + function length(ResourceId _tableId, uint256 tokenId) internal view returns (uint256) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + uint256 _byteLength = StoreSwitch.getDynamicFieldLength(_tableId, _keyTuple, 0); + unchecked { + return _byteLength / 1; + } + } + + /** + * @notice Get the length of tokenURI. + */ + function _length(ResourceId _tableId, uint256 tokenId) internal view returns (uint256) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + uint256 _byteLength = StoreCore.getDynamicFieldLength(_tableId, _keyTuple, 0); + unchecked { + return _byteLength / 1; + } + } + + /** + * @notice Get an item of tokenURI. + * @dev Reverts with Store_IndexOutOfBounds if `_index` is out of bounds for the array. + */ + function getItemTokenURI(ResourceId _tableId, uint256 tokenId, uint256 _index) internal view returns (string memory) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + unchecked { + bytes memory _blob = StoreSwitch.getDynamicFieldSlice(_tableId, _keyTuple, 0, _index * 1, (_index + 1) * 1); + return (string(_blob)); + } + } + + /** + * @notice Get an item of tokenURI. + * @dev Reverts with Store_IndexOutOfBounds if `_index` is out of bounds for the array. + */ + function _getItemTokenURI( + ResourceId _tableId, + uint256 tokenId, + uint256 _index + ) internal view returns (string memory) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + unchecked { + bytes memory _blob = StoreCore.getDynamicFieldSlice(_tableId, _keyTuple, 0, _index * 1, (_index + 1) * 1); + return (string(_blob)); + } + } + + /** + * @notice Get an item of tokenURI. + * @dev Reverts with Store_IndexOutOfBounds if `_index` is out of bounds for the array. + */ + function getItem(ResourceId _tableId, uint256 tokenId, uint256 _index) internal view returns (string memory) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + unchecked { + bytes memory _blob = StoreSwitch.getDynamicFieldSlice(_tableId, _keyTuple, 0, _index * 1, (_index + 1) * 1); + return (string(_blob)); + } + } + + /** + * @notice Get an item of tokenURI. + * @dev Reverts with Store_IndexOutOfBounds if `_index` is out of bounds for the array. + */ + function _getItem(ResourceId _tableId, uint256 tokenId, uint256 _index) internal view returns (string memory) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + unchecked { + bytes memory _blob = StoreCore.getDynamicFieldSlice(_tableId, _keyTuple, 0, _index * 1, (_index + 1) * 1); + return (string(_blob)); + } + } + + /** + * @notice Push a slice to tokenURI. + */ + function pushTokenURI(ResourceId _tableId, uint256 tokenId, string memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + StoreSwitch.pushToDynamicField(_tableId, _keyTuple, 0, bytes((_slice))); + } + + /** + * @notice Push a slice to tokenURI. + */ + function _pushTokenURI(ResourceId _tableId, uint256 tokenId, string memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + StoreCore.pushToDynamicField(_tableId, _keyTuple, 0, bytes((_slice))); + } + + /** + * @notice Push a slice to tokenURI. + */ + function push(ResourceId _tableId, uint256 tokenId, string memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + StoreSwitch.pushToDynamicField(_tableId, _keyTuple, 0, bytes((_slice))); + } + + /** + * @notice Push a slice to tokenURI. + */ + function _push(ResourceId _tableId, uint256 tokenId, string memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + StoreCore.pushToDynamicField(_tableId, _keyTuple, 0, bytes((_slice))); + } + + /** + * @notice Pop a slice from tokenURI. + */ + function popTokenURI(ResourceId _tableId, uint256 tokenId) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + StoreSwitch.popFromDynamicField(_tableId, _keyTuple, 0, 1); + } + + /** + * @notice Pop a slice from tokenURI. + */ + function _popTokenURI(ResourceId _tableId, uint256 tokenId) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + StoreCore.popFromDynamicField(_tableId, _keyTuple, 0, 1); + } + + /** + * @notice Pop a slice from tokenURI. + */ + function pop(ResourceId _tableId, uint256 tokenId) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + StoreSwitch.popFromDynamicField(_tableId, _keyTuple, 0, 1); + } + + /** + * @notice Pop a slice from tokenURI. + */ + function _pop(ResourceId _tableId, uint256 tokenId) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + StoreCore.popFromDynamicField(_tableId, _keyTuple, 0, 1); + } + + /** + * @notice Update a slice of tokenURI at `_index`. + */ + function updateTokenURI(ResourceId _tableId, uint256 tokenId, uint256 _index, string memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + unchecked { + bytes memory _encoded = bytes((_slice)); + StoreSwitch.spliceDynamicData(_tableId, _keyTuple, 0, uint40(_index * 1), uint40(_encoded.length), _encoded); + } + } + + /** + * @notice Update a slice of tokenURI at `_index`. + */ + function _updateTokenURI(ResourceId _tableId, uint256 tokenId, uint256 _index, string memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + unchecked { + bytes memory _encoded = bytes((_slice)); + StoreCore.spliceDynamicData(_tableId, _keyTuple, 0, uint40(_index * 1), uint40(_encoded.length), _encoded); + } + } + + /** + * @notice Update a slice of tokenURI at `_index`. + */ + function update(ResourceId _tableId, uint256 tokenId, uint256 _index, string memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + unchecked { + bytes memory _encoded = bytes((_slice)); + StoreSwitch.spliceDynamicData(_tableId, _keyTuple, 0, uint40(_index * 1), uint40(_encoded.length), _encoded); + } + } + + /** + * @notice Update a slice of tokenURI at `_index`. + */ + function _update(ResourceId _tableId, uint256 tokenId, uint256 _index, string memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + unchecked { + bytes memory _encoded = bytes((_slice)); + StoreCore.spliceDynamicData(_tableId, _keyTuple, 0, uint40(_index * 1), uint40(_encoded.length), _encoded); + } + } + + /** + * @notice Delete all data for given keys. + */ + function deleteRecord(ResourceId _tableId, uint256 tokenId) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + StoreSwitch.deleteRecord(_tableId, _keyTuple); + } + + /** + * @notice Delete all data for given keys. + */ + function _deleteRecord(ResourceId _tableId, uint256 tokenId) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + StoreCore.deleteRecord(_tableId, _keyTuple, _fieldLayout); + } + + /** + * @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 tokenURI) internal pure returns (PackedCounter _encodedLengths) { + // Lengths are effectively checked during copy by 2**40 bytes exceeding gas limits + unchecked { + _encodedLengths = PackedCounterLib.pack(bytes(tokenURI).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 tokenURI) internal pure returns (bytes memory) { + return abi.encodePacked(bytes((tokenURI))); + } + + /** + * @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(string memory tokenURI) internal pure returns (bytes memory, PackedCounter, bytes memory) { + bytes memory _staticData; + PackedCounter _encodedLengths = encodeLengths(tokenURI); + bytes memory _dynamicData = encodeDynamic(tokenURI); + + return (_staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Encode keys as a bytes32 array using this table's field layout. + */ + function encodeKeyTuple(uint256 tokenId) internal pure returns (bytes32[] memory) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = bytes32(uint256(tokenId)); + + return _keyTuple; + } +} diff --git a/packages/world-modules/src/modules/erc721-puppet/utils.sol b/packages/world-modules/src/modules/erc721-puppet/utils.sol new file mode 100644 index 0000000000..d7cdd6156a --- /dev/null +++ b/packages/world-modules/src/modules/erc721-puppet/utils.sol @@ -0,0 +1,38 @@ +// 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 { ERC721_SYSTEM_NAME, BALANCES_NAME, METADATA_NAME, OPERATOR_APPROVAL_NAME, OWNERS_NAME, TOKEN_APPROVAL_NAME, TOKEN_URI_NAME } from "./constants.sol"; + +function _balancesTableId(bytes14 namespace) pure returns (ResourceId) { + return WorldResourceIdLib.encode({ typeId: RESOURCE_TABLE, namespace: namespace, name: BALANCES_NAME }); +} + +function _metadataTableId(bytes14 namespace) pure returns (ResourceId) { + return WorldResourceIdLib.encode({ typeId: RESOURCE_TABLE, namespace: namespace, name: METADATA_NAME }); +} + +function _operatorApprovalTableId(bytes14 namespace) pure returns (ResourceId) { + return WorldResourceIdLib.encode({ typeId: RESOURCE_TABLE, namespace: namespace, name: OPERATOR_APPROVAL_NAME }); +} + +function _ownersTableId(bytes14 namespace) pure returns (ResourceId) { + return WorldResourceIdLib.encode({ typeId: RESOURCE_TABLE, namespace: namespace, name: OWNERS_NAME }); +} + +function _tokenApprovalTableId(bytes14 namespace) pure returns (ResourceId) { + return WorldResourceIdLib.encode({ typeId: RESOURCE_TABLE, namespace: namespace, name: TOKEN_APPROVAL_NAME }); +} + +function _tokenUriTableId(bytes14 namespace) pure returns (ResourceId) { + return WorldResourceIdLib.encode({ typeId: RESOURCE_TABLE, namespace: namespace, name: TOKEN_URI_NAME }); +} + +function _erc721SystemId(bytes14 namespace) pure returns (ResourceId) { + return WorldResourceIdLib.encode({ typeId: RESOURCE_SYSTEM, namespace: namespace, name: ERC721_SYSTEM_NAME }); +} diff --git a/packages/world-modules/src/modules/puppet/Puppet.sol b/packages/world-modules/src/modules/puppet/Puppet.sol new file mode 100644 index 0000000000..0b688b2ca0 --- /dev/null +++ b/packages/world-modules/src/modules/puppet/Puppet.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.21; + +import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; +import { IBaseWorld } from "@latticexyz/world/src/codegen/interfaces/IBaseWorld.sol"; +import { Systems } from "@latticexyz/world/src/codegen/tables/Systems.sol"; + +contract Puppet { + error Puppet_AccessDenied(address caller); + + IBaseWorld public immutable world; + ResourceId public immutable systemId; + + constructor(IBaseWorld _world, ResourceId _systemId) { + world = _world; + systemId = _systemId; + StoreSwitch.setStoreAddress(address(_world)); + } + + modifier onlyPuppetMaster() { + (address systemAddress, ) = Systems.get(systemId); + if (msg.sender != systemAddress) { + revert Puppet_AccessDenied(msg.sender); + } + _; + } + + fallback() external { + // Forward all calls to the system in the world + bytes memory returnData = world.callFrom(msg.sender, systemId, msg.data); + + // If the call was successful, return the return data + assembly { + return(add(returnData, 0x20), mload(returnData)) + } + } + + /** + * @dev Log an event with a signature and no additional topic + */ + function log(bytes32 eventSignature, bytes memory eventData) public onlyPuppetMaster { + assembly { + log1(add(eventData, 0x20), mload(eventData), eventSignature) + } + } + + /** + * @dev Log an event with a signature and one additional topics + */ + function log(bytes32 eventSignature, bytes32 topic1, bytes memory eventData) public onlyPuppetMaster { + assembly { + log2(add(eventData, 0x20), mload(eventData), eventSignature, topic1) + } + } + + /** + * @dev Log an event with a signature and two additional topics + */ + function log(bytes32 eventSignature, bytes32 topic1, bytes32 topic2, bytes memory eventData) public onlyPuppetMaster { + assembly { + log3(add(eventData, 0x20), mload(eventData), eventSignature, topic1, topic2) + } + } + + /** + * @dev Log an event with a signature and three additional topics + */ + function log( + bytes32 eventSignature, + bytes32 topic1, + bytes32 topic2, + bytes32 topic3, + bytes memory eventData + ) public onlyPuppetMaster { + assembly { + log4(add(eventData, 0x20), mload(eventData), eventSignature, topic1, topic2, topic3) + } + } +} diff --git a/packages/world-modules/src/modules/puppet/PuppetDelegationControl.sol b/packages/world-modules/src/modules/puppet/PuppetDelegationControl.sol new file mode 100644 index 0000000000..c60d5afa6d --- /dev/null +++ b/packages/world-modules/src/modules/puppet/PuppetDelegationControl.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.21; + +import { DelegationControl } from "@latticexyz/world/src/DelegationControl.sol"; +import { ResourceId } from "@latticexyz/world/src/WorldResourceId.sol"; +import { PuppetRegistry } from "./tables/PuppetRegistry.sol"; +import { PUPPET_TABLE_ID } from "./constants.sol"; + +contract PuppetDelegationControl is DelegationControl { + /** + * Verify a delegation by checking if the resourceId maps to the caller as puppet + */ + function verify(address, ResourceId systemId, bytes memory) public view returns (bool) { + address puppet = _msgSender(); + return PuppetRegistry.get(PUPPET_TABLE_ID, systemId) == puppet; + } +} diff --git a/packages/world-modules/src/modules/puppet/PuppetFactorySystem.sol b/packages/world-modules/src/modules/puppet/PuppetFactorySystem.sol new file mode 100644 index 0000000000..70ead9e57b --- /dev/null +++ b/packages/world-modules/src/modules/puppet/PuppetFactorySystem.sol @@ -0,0 +1,25 @@ +// 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 { IBaseWorld } from "@latticexyz/world/src/codegen/interfaces/IBaseWorld.sol"; + +import { AccessControlLib } from "../../utils/AccessControlLib.sol"; + +import { PuppetRegistry } from "./tables/PuppetRegistry.sol"; +import { Puppet } from "./Puppet.sol"; +import { PUPPET_TABLE_ID } from "./constants.sol"; + +contract PuppetFactorySystem is System { + function createPuppet(ResourceId systemId) public returns (address puppet) { + // Only the owner of a system can create a puppet for it + AccessControlLib.requireOwner(systemId, _msgSender()); + + // Deploy a new puppet contract + puppet = address(new Puppet(IBaseWorld(_world()), systemId)); + + // Register the puppet + PuppetRegistry.set(PUPPET_TABLE_ID, systemId, puppet); + } +} diff --git a/packages/world-modules/src/modules/puppet/PuppetMaster.sol b/packages/world-modules/src/modules/puppet/PuppetMaster.sol new file mode 100644 index 0000000000..45e587bb0a --- /dev/null +++ b/packages/world-modules/src/modules/puppet/PuppetMaster.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.21; + +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; +import { SystemRegistry } from "@latticexyz/world/src/codegen/tables/SystemRegistry.sol"; +import { PuppetRegistry } from "./tables/PuppetRegistry.sol"; +import { PUPPET_TABLE_ID } from "./constants.sol"; +import { Puppet } from "./Puppet.sol"; + +contract PuppetMaster { + error PuppetMaster_NoPuppet(address systemAddress, ResourceId systemId); + + function puppet() internal view returns (Puppet) { + ResourceId systemId = SystemRegistry.getSystemId(address(this)); + address puppetAddress = PuppetRegistry.get(PUPPET_TABLE_ID, systemId); + if (puppetAddress == address(0)) revert PuppetMaster_NoPuppet(address(this), systemId); + return Puppet(puppetAddress); + } +} diff --git a/packages/world-modules/src/modules/puppet/PuppetModule.sol b/packages/world-modules/src/modules/puppet/PuppetModule.sol new file mode 100644 index 0000000000..ecf4fcfad4 --- /dev/null +++ b/packages/world-modules/src/modules/puppet/PuppetModule.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.21; + +import { IBaseWorld } from "@latticexyz/world/src/codegen/interfaces/IBaseWorld.sol"; + +import { Module } from "@latticexyz/world/src/Module.sol"; +import { revertWithBytes } from "@latticexyz/world/src/revertWithBytes.sol"; + +import { PuppetFactorySystem } from "./PuppetFactorySystem.sol"; +import { PuppetDelegationControl } from "./PuppetDelegationControl.sol"; +import { MODULE_NAME, PUPPET_DELEGATION, PUPPET_FACTORY, PUPPET_TABLE_ID } from "./constants.sol"; + +import { PuppetRegistry } from "./tables/PuppetRegistry.sol"; + +/** + * This module registers tables and delegation control systems required for puppet delegations + */ +contract PuppetModule is Module { + PuppetDelegationControl private immutable puppetDelegationControl = new PuppetDelegationControl(); + PuppetFactorySystem private immutable puppetFactorySystem = new PuppetFactorySystem(); + + function getName() public pure returns (bytes16) { + return MODULE_NAME; + } + + function installRoot(bytes memory) public { + IBaseWorld world = IBaseWorld(_world()); + + // Register table + PuppetRegistry.register(PUPPET_TABLE_ID); + + // Register system + (bool success, bytes memory returnData) = address(world).delegatecall( + abi.encodeCall(world.registerSystem, (PUPPET_DELEGATION, puppetDelegationControl, true)) + ); + if (!success) revertWithBytes(returnData); + } + + function install(bytes memory) public { + IBaseWorld world = IBaseWorld(_world()); + + // Register table + PuppetRegistry.register(PUPPET_TABLE_ID); + + // Register puppet factory and delegation control + world.registerSystem(PUPPET_FACTORY, puppetFactorySystem, true); + world.registerSystem(PUPPET_DELEGATION, puppetDelegationControl, true); + } +} diff --git a/packages/world-modules/src/modules/puppet/constants.sol b/packages/world-modules/src/modules/puppet/constants.sol new file mode 100644 index 0000000000..c441dd03b9 --- /dev/null +++ b/packages/world-modules/src/modules/puppet/constants.sol @@ -0,0 +1,22 @@ +// 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 } from "@latticexyz/world/src/worldResourceTypes.sol"; +import { ROOT_NAMESPACE } from "@latticexyz/world/src/constants.sol"; + +bytes16 constant MODULE_NAME = bytes16("puppet"); +bytes14 constant NAMESPACE = bytes14("puppet"); + +ResourceId constant PUPPET_DELEGATION = ResourceId.wrap( + bytes32(abi.encodePacked(RESOURCE_SYSTEM, NAMESPACE, bytes16("Delegation"))) +); + +ResourceId constant PUPPET_FACTORY = ResourceId.wrap( + bytes32(abi.encodePacked(RESOURCE_SYSTEM, NAMESPACE, bytes16("Factory"))) +); + +ResourceId constant PUPPET_TABLE_ID = ResourceId.wrap( + bytes32(abi.encodePacked(RESOURCE_TABLE, NAMESPACE, bytes16("PuppetRegistry"))) +); diff --git a/packages/world-modules/src/modules/puppet/createPuppet.sol b/packages/world-modules/src/modules/puppet/createPuppet.sol new file mode 100644 index 0000000000..b00641a5b8 --- /dev/null +++ b/packages/world-modules/src/modules/puppet/createPuppet.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.21; + +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; +import { IBaseWorld } from "@latticexyz/world/src/codegen/interfaces/IBaseWorld.sol"; +import { WorldResourceIdInstance } from "@latticexyz/world/src/WorldResourceId.sol"; +import { PUPPET_DELEGATION, PUPPET_FACTORY } from "./constants.sol"; +import { PuppetDelegationControl } from "./PuppetDelegationControl.sol"; +import { Puppet } from "./Puppet.sol"; +import { PuppetFactorySystem } from "./PuppetFactorySystem.sol"; + +using WorldResourceIdInstance for ResourceId; + +/** + * This free function can be used to create a puppet and register it with the puppet delegation control. + * Since it is inlined in the caller's context, the calls originate from the caller's address. + */ +function createPuppet(IBaseWorld world, ResourceId systemId) returns (address puppet) { + puppet = abi.decode( + world.call(PUPPET_FACTORY, abi.encodeCall(PuppetFactorySystem.createPuppet, (systemId))), + (address) + ); + world.registerNamespaceDelegation(systemId.getNamespaceId(), PUPPET_DELEGATION, new bytes(0)); +} diff --git a/packages/world-modules/src/modules/puppet/tables/PuppetRegistry.sol b/packages/world-modules/src/modules/puppet/tables/PuppetRegistry.sol new file mode 100644 index 0000000000..054b093cbd --- /dev/null +++ b/packages/world-modules/src/modules/puppet/tables/PuppetRegistry.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 PuppetRegistry { + /** + * @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] = "systemId"; + } + + /** + * @notice Get the table's value field names. + * @return fieldNames An array of strings with the names of value fields. + */ + function getFieldNames() internal pure returns (string[] memory fieldNames) { + fieldNames = new string[](1); + fieldNames[0] = "puppet"; + } + + /** + * @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 puppet. + */ + function getPuppet(ResourceId _tableId, ResourceId systemId) internal view returns (address puppet) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(systemId); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (address(bytes20(_blob))); + } + + /** + * @notice Get puppet. + */ + function _getPuppet(ResourceId _tableId, ResourceId systemId) internal view returns (address puppet) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(systemId); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (address(bytes20(_blob))); + } + + /** + * @notice Get puppet. + */ + function get(ResourceId _tableId, ResourceId systemId) internal view returns (address puppet) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(systemId); + + bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (address(bytes20(_blob))); + } + + /** + * @notice Get puppet. + */ + function _get(ResourceId _tableId, ResourceId systemId) internal view returns (address puppet) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(systemId); + + bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout); + return (address(bytes20(_blob))); + } + + /** + * @notice Set puppet. + */ + function setPuppet(ResourceId _tableId, ResourceId systemId, address puppet) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(systemId); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((puppet)), _fieldLayout); + } + + /** + * @notice Set puppet. + */ + function _setPuppet(ResourceId _tableId, ResourceId systemId, address puppet) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(systemId); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((puppet)), _fieldLayout); + } + + /** + * @notice Set puppet. + */ + function set(ResourceId _tableId, ResourceId systemId, address puppet) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(systemId); + + StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((puppet)), _fieldLayout); + } + + /** + * @notice Set puppet. + */ + function _set(ResourceId _tableId, ResourceId systemId, address puppet) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(systemId); + + StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((puppet)), _fieldLayout); + } + + /** + * @notice Delete all data for given keys. + */ + function deleteRecord(ResourceId _tableId, ResourceId systemId) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(systemId); + + StoreSwitch.deleteRecord(_tableId, _keyTuple); + } + + /** + * @notice Delete all data for given keys. + */ + function _deleteRecord(ResourceId _tableId, ResourceId systemId) internal { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(systemId); + + StoreCore.deleteRecord(_tableId, _keyTuple, _fieldLayout); + } + + /** + * @notice Tightly pack static (fixed length) data using this table's schema. + * @return The static data, encoded into a sequence of bytes. + */ + function encodeStatic(address puppet) internal pure returns (bytes memory) { + return abi.encodePacked(puppet); + } + + /** + * @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 puppet) internal pure returns (bytes memory, PackedCounter, bytes memory) { + bytes memory _staticData = encodeStatic(puppet); + + 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 systemId) internal pure returns (bytes32[] memory) { + bytes32[] memory _keyTuple = new bytes32[](1); + _keyTuple[0] = ResourceId.unwrap(systemId); + + return _keyTuple; + } +} diff --git a/packages/world-modules/src/modules/puppet/utils.sol b/packages/world-modules/src/modules/puppet/utils.sol new file mode 100644 index 0000000000..03028b4a99 --- /dev/null +++ b/packages/world-modules/src/modules/puppet/utils.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.21; + +function toTopic(address value) pure returns (bytes32) { + return bytes32(uint256(uint160(value))); +} + +function toTopic(uint256 value) pure returns (bytes32) { + return bytes32(value); +} diff --git a/packages/world-modules/src/modules/tokens/tables/Balances.sol b/packages/world-modules/src/modules/tokens/tables/Balances.sol new file mode 100644 index 0000000000..bad1af5c6d --- /dev/null +++ b/packages/world-modules/src/modules/tokens/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/utils/AccessControlLib.sol b/packages/world-modules/src/utils/AccessControlLib.sol new file mode 100644 index 0000000000..462d683d67 --- /dev/null +++ b/packages/world-modules/src/utils/AccessControlLib.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.21; + +import { ResourceId, WorldResourceIdInstance } from "@latticexyz/world/src/WorldResourceId.sol"; +import { IWorldErrors } from "@latticexyz/world/src/IWorldErrors.sol"; +import { ResourceAccess } from "@latticexyz/world/src/codegen/tables/ResourceAccess.sol"; +import { NamespaceOwner } from "@latticexyz/world/src/codegen/tables/NamespaceOwner.sol"; + +/** + * @title AccessControlLib + * @dev Provides access control functions for checking permissions and ownership within a namespace. + * This library is functionally equivalent with the AccessControl library from world, + * but uses StoreSwitch instead of always reading from own storage. + */ +library AccessControlLib { + using WorldResourceIdInstance for ResourceId; + + /** + * @notice Checks if the caller has access to the given resource ID or its namespace. + * @param resourceId The resource ID to check access for. + * @param caller The address of the caller. + * @return true if the caller has access, false otherwise. + */ + function hasAccess(ResourceId resourceId, address caller) internal view returns (bool) { + return + // First check access based on the namespace. If caller has no namespace access, check access on the resource. + ResourceAccess.get(resourceId.getNamespaceId(), caller) || ResourceAccess.get(resourceId, caller); + } + + /** + * @notice Check for access at the given namespace or resource. + * @param resourceId The resource ID to check access for. + * @param caller The address of the caller. + * @dev Reverts with IWorldErrors.World_AccessDenied if access is denied. + */ + function requireAccess(ResourceId resourceId, address caller) internal view { + // Check if the given caller has access to the given namespace or name + if (!hasAccess(resourceId, caller)) { + revert IWorldErrors.World_AccessDenied(resourceId.toString(), caller); + } + } + + /** + * @notice Check for ownership of the namespace of the given resource ID. + * @dev Reverts with IWorldErrors.World_AccessDenied if caller is not owner of the namespace of the resource. + * @param resourceId The resource ID to check ownership for. + * @param caller The address of the caller. + */ + function requireOwner(ResourceId resourceId, address caller) internal view { + if (NamespaceOwner.get(resourceId.getNamespaceId()) != caller) { + revert IWorldErrors.World_AccessDenied(resourceId.toString(), caller); + } + } +} diff --git a/packages/world-modules/test/ERC20.t.sol b/packages/world-modules/test/ERC20.t.sol new file mode 100644 index 0000000000..9416e8aacd --- /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 { ERC20MetadataData } from "../src/modules/erc20-puppet/tables/ERC20Metadata.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", ERC20MetadataData({ 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", + ERC20MetadataData({ 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-modules/test/ERC721.t.sol b/packages/world-modules/test/ERC721.t.sol new file mode 100644 index 0000000000..17bf5595af --- /dev/null +++ b/packages/world-modules/test/ERC721.t.sol @@ -0,0 +1,581 @@ +// 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, WorldResourceIdInstance } 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 { IWorldErrors } from "@latticexyz/world/src/IWorldErrors.sol"; +import { GasReporter } from "@latticexyz/gas-report/src/GasReporter.sol"; + +import { PuppetModule } from "../src/modules/puppet/PuppetModule.sol"; +import { ERC721Module } from "../src/modules/erc721-puppet/ERC721Module.sol"; +import { ERC721MetadataData } from "../src/modules/erc721-puppet/tables/ERC721Metadata.sol"; +import { IERC721Mintable } from "../src/modules/erc721-puppet/IERC721Mintable.sol"; +import { registerERC721 } from "../src/modules/erc721-puppet/registerERC721.sol"; +import { IERC721Errors } from "../src/modules/erc721-puppet/IERC721Errors.sol"; +import { IERC721Events } from "../src/modules/erc721-puppet/IERC721Events.sol"; +import { _erc721SystemId } from "../src/modules/erc721-puppet/utils.sol"; + +abstract contract ERC721TokenReceiver { + function onERC721Received(address, address, uint256, bytes calldata) external virtual returns (bytes4) { + return ERC721TokenReceiver.onERC721Received.selector; + } +} + +contract ERC721Recipient is ERC721TokenReceiver { + address public operator; + address public from; + uint256 public id; + bytes public data; + + function onERC721Received( + address _operator, + address _from, + uint256 _id, + bytes calldata _data + ) public virtual override returns (bytes4) { + operator = _operator; + from = _from; + id = _id; + data = _data; + + return ERC721TokenReceiver.onERC721Received.selector; + } +} + +contract RevertingERC721Recipient is ERC721TokenReceiver { + function onERC721Received(address, address, uint256, bytes calldata) public virtual override returns (bytes4) { + revert(string(abi.encodeWithSelector(ERC721TokenReceiver.onERC721Received.selector))); + } +} + +contract WrongReturnDataERC721Recipient is ERC721TokenReceiver { + function onERC721Received(address, address, uint256, bytes calldata) public virtual override returns (bytes4) { + return 0xCAFEBEEF; + } +} + +contract NonERC721Recipient {} + +contract ERC721Test is Test, GasReporter, IERC721Events, IERC721Errors { + using WorldResourceIdInstance for ResourceId; + + IBaseWorld world; + ERC721Module erc721Module; + IERC721Mintable 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 ERC721 token + token = registerERC721(world, "myERC721", ERC721MetadataData({ name: "Token", symbol: "TKN", baseURI: "" })); + } + + function _expectAccessDenied(address caller) internal { + ResourceId tokenSystemId = _erc721SystemId("myERC721"); + vm.expectRevert(abi.encodeWithSelector(IWorldErrors.World_AccessDenied.selector, tokenSystemId.toString(), caller)); + } + + function _expectMintEvent(address to, uint256 id) internal { + _expectTransferEvent(address(0), to, id); + } + + function _expectBurnEvent(address from, uint256 id) internal { + _expectTransferEvent(from, address(0), id); + } + + function _expectTransferEvent(address from, address to, uint256 id) internal { + vm.expectEmit(true, true, true, true); + emit Transfer(from, to, id); + } + + function _expectApprovalEvent(address owner, address approved, uint256 id) internal { + vm.expectEmit(true, true, true, true); + emit Approval(owner, approved, id); + } + + function _expectApprovalForAllEvent(address owner, address operator, bool approved) internal { + vm.expectEmit(true, true, true, true); + emit ApprovalForAll(owner, operator, approved); + } + + function _assumeDifferentNonZero(address address1, address address2) internal pure { + vm.assume(address1 != address(0)); + vm.assume(address2 != address(0)); + vm.assume(address1 != address2); + } + + function _assumeEOA(address address1) internal view { + uint256 toCodeSize; + assembly { + toCodeSize := extcodesize(address1) + } + vm.assume(toCodeSize == 0); + } + + function _assumeDifferentNonZero(address address1, address address2, address address3) internal pure { + vm.assume(address1 != address(0)); + vm.assume(address2 != address(0)); + vm.assume(address3 != address(0)); + vm.assume(address1 != address2); + vm.assume(address2 != address3); + vm.assume(address3 != address1); + } + + function testSetUp() public { + assertTrue(address(token) != address(0)); + assertEq(NamespaceOwner.get(WorldResourceIdLib.encodeNamespace("myERC721")), address(this)); + } + + function testInstallTwice() public { + // Install the ERC721 module again + IERC721Mintable anotherToken = registerERC721( + world, + "anotherERC721", + ERC721MetadataData({ name: "Token", symbol: "TKN", baseURI: "" }) + ); + assertTrue(address(anotherToken) != address(0)); + assertTrue(address(anotherToken) != address(token)); + } + + ///////////////////////////////////////////////// + // SOLADY ERC721 TEST CAES + // (https://github.com/Vectorized/solady/blob/main/test/ERC721.t.sol) + ///////////////////////////////////////////////// + + function testMint(uint256 id, address owner) public { + vm.assume(owner != address(0)); + + _expectMintEvent(owner, id); + token.mint(owner, id); + + assertEq(token.balanceOf(owner), 1); + assertEq(token.ownerOf(id), owner); + } + + function testMintRevertAccessDenied(uint256 id, address owner, address operator) public { + _assumeDifferentNonZero(owner, operator); + + _expectAccessDenied(operator); + vm.prank(operator); + token.mint(owner, id); + } + + function testBurn(uint256 id, address owner) public { + vm.assume(owner != address(0)); + + assertEq(token.balanceOf(owner), 0, "before"); + + _expectMintEvent(owner, id); + token.mint(owner, id); + + assertEq(token.balanceOf(owner), 1, "after mint"); + + _expectBurnEvent(owner, id); + token.burn(id); + + assertEq(token.balanceOf(owner), 0, "after burn"); + + vm.expectRevert(abi.encodeWithSelector(ERC721NonexistentToken.selector, id)); + token.ownerOf(id); + } + + function testBurnRevertAccessDenined(uint256 id, address owner, address operator) public { + _assumeDifferentNonZero(owner, operator); + + _expectMintEvent(owner, id); + token.mint(owner, id); + + _expectAccessDenied(operator); + vm.prank(operator); + token.burn(id); + } + + function testTransferFrom(address owner, address to, uint256 tokenId) public { + _assumeDifferentNonZero(owner, to); + + token.mint(owner, tokenId); + + vm.prank(owner); + token.transferFrom(owner, to, tokenId); + + assertEq(token.balanceOf(owner), 0); + assertEq(token.balanceOf(to), 1); + assertEq(token.ownerOf(tokenId), to); + } + + function testApprove(address owner, uint256 id, address spender) public { + _assumeDifferentNonZero(owner, spender); + + token.mint(owner, id); + + vm.prank(owner); + _expectApprovalEvent(owner, spender, id); + token.approve(spender, id); + assertEq(token.getApproved(id), spender); + } + + function testApproveAll(address owner, address operator, bool approved) public { + _assumeDifferentNonZero(owner, operator); + + vm.prank(owner); + _expectApprovalForAllEvent(owner, operator, approved); + token.setApprovalForAll(operator, approved); + assertEq(token.isApprovedForAll(owner, operator), approved); + } + + function testTransferFromSelf(uint256 id, address from, address to) public { + _assumeDifferentNonZero(from, to); + + token.mint(from, id); + + vm.prank(from); + token.transferFrom(from, to, id); + + assertEq(token.getApproved(id), address(0)); + assertEq(token.ownerOf(id), to); + assertEq(token.balanceOf(to), 1); + assertEq(token.balanceOf(from), 0); + } + + function testTransferFromApproveAll(uint256 id, address from, address to, address operator) public { + _assumeDifferentNonZero(from, to, operator); + + token.mint(from, id); + + vm.prank(from); + token.setApprovalForAll(operator, true); + + vm.prank(operator); + token.transferFrom(from, to, id); + + assertEq(token.getApproved(id), address(0)); + assertEq(token.ownerOf(id), to); + assertEq(token.balanceOf(to), 1); + assertEq(token.balanceOf(from), 0); + } + + function testSafeTransferFromToEOA(uint256 id, address from, address to, address operator) public { + _assumeEOA(from); + _assumeEOA(to); + _assumeDifferentNonZero(from, to, operator); + + token.mint(from, id); + + vm.prank(from); + token.setApprovalForAll(operator, true); + + vm.prank(operator); + token.safeTransferFrom(from, to, id); + + assertEq(token.getApproved(id), address(0)); + assertEq(token.ownerOf(id), to); + assertEq(token.balanceOf(to), 1); + assertEq(token.balanceOf(from), 0); + } + + function testSafeTransferFromToERC721Recipient(uint256 id, address from, address operator) public { + _assumeDifferentNonZero(from, operator); + + ERC721Recipient recipient = new ERC721Recipient(); + + token.mint(from, id); + + vm.prank(from); + token.setApprovalForAll(operator, true); + + vm.prank(operator); + token.safeTransferFrom(from, address(recipient), id); + + assertEq(token.getApproved(id), address(0)); + assertEq(token.ownerOf(id), address(recipient)); + assertEq(token.balanceOf(address(recipient)), 1); + assertEq(token.balanceOf(from), 0); + + assertEq(recipient.operator(), operator); + assertEq(recipient.from(), from); + assertEq(recipient.id(), id); + assertEq(recipient.data(), ""); + } + + function testSafeTransferFromToERC721RecipientWithData( + uint256 id, + address from, + address operator, + bytes memory data + ) public { + _assumeDifferentNonZero(from, operator); + + ERC721Recipient recipient = new ERC721Recipient(); + + token.mint(from, id); + + vm.prank(from); + token.setApprovalForAll(operator, true); + + vm.prank(operator); + token.safeTransferFrom(from, address(recipient), id, data); + + assertEq(recipient.data(), data); + assertEq(recipient.id(), id); + assertEq(recipient.operator(), operator); + assertEq(recipient.from(), from); + + assertEq(token.getApproved(id), address(0)); + assertEq(token.ownerOf(id), address(recipient)); + assertEq(token.balanceOf(address(recipient)), 1); + assertEq(token.balanceOf(from), 0); + } + + function testSafeMintToEOA(uint256 id, address to) public { + _assumeEOA(to); + vm.assume(to != address(0)); + + token.safeMint(to, id); + + assertEq(token.ownerOf(id), to); + assertEq(token.balanceOf(to), 1); + } + + function testSafeMintToERC721Recipient(uint256 id) public { + ERC721Recipient to = new ERC721Recipient(); + + token.safeMint(address(to), id); + + assertEq(token.ownerOf(id), address(to)); + assertEq(token.balanceOf(address(to)), 1); + + assertEq(to.operator(), address(this)); + assertEq(to.from(), address(0)); + assertEq(to.id(), id); + assertEq(to.data(), ""); + } + + function testSafeMintToERC721RecipientWithData(uint256 id, bytes memory data) public { + ERC721Recipient to = new ERC721Recipient(); + + token.safeMint(address(to), id, data); + + assertEq(token.ownerOf(id), address(to)); + assertEq(token.balanceOf(address(to)), 1); + + assertEq(to.operator(), address(this)); + assertEq(to.from(), address(0)); + assertEq(to.id(), id); + assertEq(to.data(), data); + } + + function testMintToZeroReverts(uint256 id) public { + vm.expectRevert(abi.encodeWithSelector(ERC721InvalidReceiver.selector, address(0))); + token.mint(address(0), id); + } + + function testDoubleMintReverts(uint256 id, address to) public { + vm.assume(to != address(0)); + token.mint(to, id); + + vm.expectRevert(abi.encodeWithSelector(ERC721InvalidSender.selector, address(0))); + token.mint(to, id); + } + + function testBurnNonExistentReverts(uint256 id) public { + vm.expectRevert(abi.encodeWithSelector(ERC721NonexistentToken.selector, id)); + token.burn(id); + } + + function testDoubleBurnReverts(uint256 id, address to) public { + vm.assume(to != address(0)); + + token.mint(to, id); + + token.burn(id); + + vm.expectRevert(abi.encodeWithSelector(ERC721NonexistentToken.selector, id)); + token.burn(id); + } + + function testApproveNonExistentReverts(uint256 id, address to) public { + vm.expectRevert(abi.encodeWithSelector(ERC721NonexistentToken.selector, id)); + token.approve(to, id); + } + + function testApproveUnauthorizedReverts(uint256 id, address owner, address operator, address to) public { + _assumeDifferentNonZero(owner, operator, to); + + token.mint(owner, id); + + vm.expectRevert(abi.encodeWithSelector(ERC721InvalidApprover.selector, operator)); + vm.prank(operator); + token.approve(to, id); + } + + function testTransferFromNotExistentReverts(address from, address to, uint256 id) public { + _assumeDifferentNonZero(from, to); + + vm.expectRevert(abi.encodeWithSelector(ERC721NonexistentToken.selector, id)); + token.transferFrom(from, to, id); + } + + function testTransferFromWrongFromReverts(address to, uint256 id, address owner, address from) public { + _assumeDifferentNonZero(owner, from, to); + token.mint(owner, id); + + vm.prank(owner); + vm.expectRevert(abi.encodeWithSelector(ERC721IncorrectOwner.selector, from, id, owner)); + token.transferFrom(from, to, id); + } + + function testTransferFromToZeroReverts(uint256 id) public { + token.mint(address(this), id); + + vm.expectRevert(abi.encodeWithSelector(ERC721InvalidReceiver.selector, address(0))); + token.transferFrom(address(this), address(0), id); + } + + function testTransferFromNotOwner(uint256 id, address from, address to, address operator) public { + _assumeDifferentNonZero(from, to, operator); + + token.mint(from, id); + + vm.prank(operator); + vm.expectRevert(abi.encodeWithSelector(ERC721InsufficientApproval.selector, operator, id)); + token.transferFrom(from, to, id); + } + + function testSafeTransferFromToNonERC721RecipientReverts(uint256 id, address from) public { + vm.assume(from != address(0)); + + token.mint(from, id); + + address to = address(new NonERC721Recipient()); + + vm.prank(from); + vm.expectRevert(abi.encodeWithSelector(ERC721InvalidReceiver.selector, to)); + token.safeTransferFrom(from, to, id); + } + + function testSafeTransferFromToNonERC721RecipientWithDataReverts(uint256 id, address from, bytes memory data) public { + vm.assume(from != address(0)); + + token.mint(from, id); + + address to = address(new NonERC721Recipient()); + + vm.prank(from); + vm.expectRevert(abi.encodeWithSelector(ERC721InvalidReceiver.selector, to)); + token.safeTransferFrom(from, to, id, data); + } + + function testSafeTransferFromToRevertingERC721RecipientReverts(uint256 id, address from) public { + vm.assume(from != address(0)); + + token.mint(from, id); + + address to = address(new RevertingERC721Recipient()); + + vm.prank(from); + vm.expectRevert(abi.encodeWithSelector(ERC721TokenReceiver.onERC721Received.selector)); + token.safeTransferFrom(from, to, id); + } + + function testSafeTransferFromToRevertingERC721RecipientWithDataReverts( + uint256 id, + address from, + bytes memory data + ) public { + vm.assume(from != address(0)); + + token.mint(from, id); + + address to = address(new RevertingERC721Recipient()); + + vm.prank(from); + vm.expectRevert(abi.encodeWithSelector(ERC721TokenReceiver.onERC721Received.selector)); + token.safeTransferFrom(from, to, id, data); + } + + function testSafeTransferFromToERC721RecipientWithWrongReturnDataReverts(uint256 id, address from) public { + vm.assume(from != address(0)); + + token.mint(from, id); + + address to = address(new WrongReturnDataERC721Recipient()); + + vm.prank(from); + vm.expectRevert(abi.encodeWithSelector(ERC721InvalidReceiver.selector, to)); + token.safeTransferFrom(from, to, id); + } + + function testSafeTransferFromToERC721RecipientWithWrongReturnDataWithDataReverts( + uint256 id, + address from, + bytes memory data + ) public { + vm.assume(from != address(0)); + + token.mint(from, id); + + address to = address(new WrongReturnDataERC721Recipient()); + + vm.prank(from); + vm.expectRevert(abi.encodeWithSelector(ERC721InvalidReceiver.selector, to)); + token.safeTransferFrom(from, to, id, data); + } + + function testSafeMintToNonERC721RecipientReverts(uint256 id) public { + address to = address(new NonERC721Recipient()); + + vm.expectRevert(abi.encodeWithSelector(ERC721InvalidReceiver.selector, to)); + token.safeMint(to, id); + } + + function testSafeMintToNonERC721RecipientWithDataReverts(uint256 id, bytes memory data) public { + address to = address(new NonERC721Recipient()); + + vm.expectRevert(abi.encodeWithSelector(ERC721InvalidReceiver.selector, to)); + token.safeMint(to, id, data); + } + + function testSafeMintToRevertingERC721RecipientReverts(uint256 id) public { + address to = address(new RevertingERC721Recipient()); + + vm.expectRevert(abi.encodeWithSelector(ERC721TokenReceiver.onERC721Received.selector)); + token.safeMint(to, id); + } + + function testSafeMintToRevertingERC721RecipientWithDataReverts(uint256 id, bytes memory data) public { + address to = address(new RevertingERC721Recipient()); + + vm.expectRevert(abi.encodeWithSelector(ERC721TokenReceiver.onERC721Received.selector)); + token.safeMint(to, id, data); + } + + function testSafeMintToERC721RecipientWithWrongReturnData(uint256 id) public { + address to = address(new WrongReturnDataERC721Recipient()); + + vm.expectRevert(abi.encodeWithSelector(ERC721InvalidReceiver.selector, to)); + token.safeMint(to, id); + } + + function testSafeMintToERC721RecipientWithWrongReturnDataWithData(uint256 id, bytes memory data) public { + address to = address(new WrongReturnDataERC721Recipient()); + + vm.expectRevert(abi.encodeWithSelector(ERC721InvalidReceiver.selector, to)); + token.safeMint(to, id, data); + } + + function testOwnerOfNonExistent(uint256 id) public { + vm.expectRevert(abi.encodeWithSelector(ERC721NonexistentToken.selector, id)); + token.ownerOf(id); + } +} diff --git a/packages/world-modules/test/PuppetModule.t.sol b/packages/world-modules/test/PuppetModule.t.sol new file mode 100644 index 0000000000..7e29308620 --- /dev/null +++ b/packages/world-modules/test/PuppetModule.t.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.21; + +import { Test } from "forge-std/Test.sol"; +import { GasReporter } from "@latticexyz/gas-report/src/GasReporter.sol"; + +import { World } from "@latticexyz/world/src/World.sol"; +import { ResourceId, WorldResourceIdLib, WorldResourceIdInstance } from "@latticexyz/world/src/WorldResourceId.sol"; +import { System } from "@latticexyz/world/src/System.sol"; +import { RESOURCE_SYSTEM } from "@latticexyz/world/src/worldResourceTypes.sol"; + +import { IBaseWorld } from "@latticexyz/world/src/codegen/interfaces/IBaseWorld.sol"; +import { IWorldErrors } from "@latticexyz/world/src/IWorldErrors.sol"; +import { DELEGATION_CONTROL_INTERFACE_ID } from "@latticexyz/world/src/IDelegationControl.sol"; + +import { CoreModule } from "@latticexyz/world/src/modules/core/CoreModule.sol"; +import { Systems } from "@latticexyz/world/src/codegen/tables/Systems.sol"; + +import { PuppetModule } from "../src/modules/puppet/PuppetModule.sol"; +import { PuppetDelegationControl } from "../src/modules/puppet/PuppetDelegationControl.sol"; +import { Puppet } from "../src/modules/puppet/Puppet.sol"; +import { PuppetMaster } from "../src/modules/puppet/PuppetMaster.sol"; +import { PUPPET_DELEGATION } from "../src/modules/puppet/constants.sol"; +import { createPuppet } from "../src/modules/puppet/createPuppet.sol"; + +contract PuppetTestSystem is System, PuppetMaster { + event Hello(string message); + + function echoAndEmit(string memory message) public returns (string memory) { + puppet().log(Hello.selector, abi.encode(message)); + return message; + } + + function msgSender() public view returns (address) { + return _msgSender(); + } +} + +contract PuppetModuleTest is Test, GasReporter { + using WorldResourceIdInstance for ResourceId; + + event Hello(string msg); + + IBaseWorld private world; + ResourceId private systemId = + WorldResourceIdLib.encode({ typeId: RESOURCE_SYSTEM, namespace: "namespace", name: "testSystem" }); + PuppetTestSystem private puppet; + + function setUp() public { + world = IBaseWorld(address(new World())); + world.initialize(new CoreModule()); + world.installModule(new PuppetModule(), new bytes(0)); + + // Register a new system + PuppetTestSystem system = new PuppetTestSystem(); + world.registerSystem(systemId, system, true); + + // Connect the puppet + puppet = PuppetTestSystem(createPuppet(world, systemId)); + } + + function testEmitOnPuppet() public { + vm.expectEmit(true, true, true, true); + emit Hello("hello world"); + string memory result = puppet.echoAndEmit("hello world"); + assertEq(result, "hello world"); + } + + function testMsgSender() public { + assertEq(puppet.msgSender(), address(this)); + } +} 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.