From d7325e517ce18597d55e8bce41036e78e00c3a78 Mon Sep 17 00:00:00 2001 From: alvarius Date: Wed, 1 Nov 2023 16:38:27 +0100 Subject: [PATCH] feat(world-modules): add ERC721 module (#1844) Co-authored-by: 0xhank --- .changeset/gorgeous-swans-hide.md | 21 + packages/world-modules/mud.config.ts | 86 +- packages/world-modules/src/index.sol | 10 +- .../src/modules/erc20-puppet/ERC20Module.sol | 10 +- .../src/modules/erc20-puppet/ERC20System.sol | 11 +- .../src/modules/erc20-puppet/IERC20.sol | 3 +- .../modules/erc20-puppet/IERC20Mintable.sol | 3 +- .../modules/erc20-puppet/registerERC20.sol | 4 +- .../{Metadata.sol => ERC20Metadata.sol} | 14 +- .../modules/erc721-puppet/ERC721Module.sol | 101 +++ .../modules/erc721-puppet/ERC721System.sol | 529 +++++++++++++ .../src/modules/erc721-puppet/IERC721.sol | 120 +++ .../modules/erc721-puppet/IERC721Errors.sol | 61 ++ .../modules/erc721-puppet/IERC721Events.sol | 23 + .../modules/erc721-puppet/IERC721Metadata.sol | 26 + .../modules/erc721-puppet/IERC721Mintable.sol | 53 ++ .../modules/erc721-puppet/IERC721Receiver.sol | 27 + .../src/modules/erc721-puppet/constants.sol | 24 + .../modules/erc721-puppet/registerERC721.sol | 37 + .../erc721-puppet/tables/ERC721Metadata.sol | 734 ++++++++++++++++++ .../erc721-puppet/tables/ERC721Registry.sol | 229 ++++++ .../erc721-puppet/tables/OperatorApproval.sol | 251 ++++++ .../modules/erc721-puppet/tables/Owners.sol | 226 ++++++ .../erc721-puppet/tables/TokenApproval.sol | 226 ++++++ .../modules/erc721-puppet/tables/TokenURI.sol | 480 ++++++++++++ .../src/modules/erc721-puppet/utils.sol | 38 + .../tables/Balances.sol | 0 packages/world-modules/test/ERC20.t.sol | 6 +- packages/world-modules/test/ERC721.t.sol | 581 ++++++++++++++ 29 files changed, 3898 insertions(+), 36 deletions(-) create mode 100644 .changeset/gorgeous-swans-hide.md rename packages/world-modules/src/modules/erc20-puppet/tables/{Metadata.sol => ERC20Metadata.sol} (97%) create mode 100644 packages/world-modules/src/modules/erc721-puppet/ERC721Module.sol create mode 100644 packages/world-modules/src/modules/erc721-puppet/ERC721System.sol create mode 100644 packages/world-modules/src/modules/erc721-puppet/IERC721.sol create mode 100644 packages/world-modules/src/modules/erc721-puppet/IERC721Errors.sol create mode 100644 packages/world-modules/src/modules/erc721-puppet/IERC721Events.sol create mode 100644 packages/world-modules/src/modules/erc721-puppet/IERC721Metadata.sol create mode 100644 packages/world-modules/src/modules/erc721-puppet/IERC721Mintable.sol create mode 100644 packages/world-modules/src/modules/erc721-puppet/IERC721Receiver.sol create mode 100644 packages/world-modules/src/modules/erc721-puppet/constants.sol create mode 100644 packages/world-modules/src/modules/erc721-puppet/registerERC721.sol create mode 100644 packages/world-modules/src/modules/erc721-puppet/tables/ERC721Metadata.sol create mode 100644 packages/world-modules/src/modules/erc721-puppet/tables/ERC721Registry.sol create mode 100644 packages/world-modules/src/modules/erc721-puppet/tables/OperatorApproval.sol create mode 100644 packages/world-modules/src/modules/erc721-puppet/tables/Owners.sol create mode 100644 packages/world-modules/src/modules/erc721-puppet/tables/TokenApproval.sol create mode 100644 packages/world-modules/src/modules/erc721-puppet/tables/TokenURI.sol create mode 100644 packages/world-modules/src/modules/erc721-puppet/utils.sol rename packages/world-modules/src/modules/{erc20-puppet => tokens}/tables/Balances.sol (100%) create mode 100644 packages/world-modules/test/ERC721.t.sol 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/packages/world-modules/mud.config.ts b/packages/world-modules/mud.config.ts index b18913bee2..a830eecd1f 100644 --- a/packages/world-modules/mud.config.ts +++ b/packages/world-modules/mud.config.ts @@ -107,11 +107,11 @@ export default mudConfig({ }, /************************************************************************ * - * ERC20 MODULE + * TOKEN TABLES (SHARED BY ERC20, ERC721) * ************************************************************************/ Balances: { - directory: "modules/erc20-puppet/tables", + directory: "modules/tokens/tables", keySchema: { account: "address", }, @@ -120,6 +120,21 @@ export default mudConfig({ }, 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: { @@ -139,18 +154,74 @@ export default mudConfig({ }, tableIdArgument: true, }, - Metadata: { + ERC20Registry: { directory: "modules/erc20-puppet/tables", + keySchema: { + namespaceId: "ResourceId", + }, + valueSchema: { + erc20Address: "address", + }, + tableIdArgument: true, + }, + /************************************************************************ + * + * ERC721 MODULE + * + ************************************************************************/ + ERC721Metadata: { + directory: "modules/erc721-puppet/tables", keySchema: {}, valueSchema: { - decimals: "uint8", name: "string", symbol: "string", + baseURI: "string", }, tableIdArgument: true, }, - ERC20Registry: { - directory: "modules/erc20-puppet/tables", + 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", }, @@ -160,6 +231,5 @@ export default mudConfig({ tableIdArgument: true, }, }, - - excludeSystems: ["UniqueEntitySystem", "PuppetFactorySystem", "ERC20System"], + excludeSystems: ["UniqueEntitySystem", "PuppetFactorySystem", "ERC20System", "ERC721System"], }); diff --git a/packages/world-modules/src/index.sol b/packages/world-modules/src/index.sol index 4805e545b5..b1d73d8c0d 100644 --- a/packages/world-modules/src/index.sol +++ b/packages/world-modules/src/index.sol @@ -10,8 +10,14 @@ import { UniqueEntity } from "./modules/uniqueentity/tables/UniqueEntity.sol"; import { CallboundDelegations, CallboundDelegationsTableId } from "./modules/std-delegations/tables/CallboundDelegations.sol"; import { TimeboundDelegations, TimeboundDelegationsTableId } from "./modules/std-delegations/tables/TimeboundDelegations.sol"; import { PuppetRegistry } from "./modules/puppet/tables/PuppetRegistry.sol"; -import { Balances } from "./modules/erc20-puppet/tables/Balances.sol"; +import { 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 { Metadata, MetadataData } from "./modules/erc20-puppet/tables/Metadata.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/modules/erc20-puppet/ERC20Module.sol b/packages/world-modules/src/modules/erc20-puppet/ERC20Module.sol index e69f02d084..e51df95407 100644 --- a/packages/world-modules/src/modules/erc20-puppet/ERC20Module.sol +++ b/packages/world-modules/src/modules/erc20-puppet/ERC20Module.sol @@ -11,15 +11,15 @@ import { InstalledModules } from "@latticexyz/world/src/codegen/tables/Installed 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 { Balances } from "./tables/Balances.sol"; import { Allowances } from "./tables/Allowances.sol"; -import { Metadata, MetadataData } from "./tables/Metadata.sol"; +import { ERC20Metadata, ERC20MetadataData } from "./tables/ERC20Metadata.sol"; contract ERC20Module is Module { error ERC20Module_InvalidNamespace(bytes14 namespace); @@ -35,7 +35,7 @@ contract ERC20Module is Module { // Register the tables Allowances.register(_allowancesTableId(namespace)); Balances.register(_balancesTableId(namespace)); - Metadata.register(_metadataTableId(namespace)); + ERC20Metadata.register(_metadataTableId(namespace)); // Register a new ERC20System IBaseWorld(_world()).registerSystem(_erc20SystemId(namespace), new ERC20System(), true); @@ -55,7 +55,7 @@ contract ERC20Module is Module { } // Extract args - (bytes14 namespace, MetadataData memory metadata) = abi.decode(args, (bytes14, MetadataData)); + (bytes14 namespace, ERC20MetadataData memory metadata) = abi.decode(args, (bytes14, ERC20MetadataData)); // Require the namespace to not be the module's namespace if (namespace == MODULE_NAMESPACE) { @@ -69,7 +69,7 @@ contract ERC20Module is Module { _registerERC20(namespace); // Initialize the Metadata - Metadata.set(_metadataTableId(namespace), metadata); + ERC20Metadata.set(_metadataTableId(namespace), metadata); // Deploy and register the ERC20 puppet. IBaseWorld world = IBaseWorld(_world()); diff --git a/packages/world-modules/src/modules/erc20-puppet/ERC20System.sol b/packages/world-modules/src/modules/erc20-puppet/ERC20System.sol index 878f51f39b..8c5a5474e4 100644 --- a/packages/world-modules/src/modules/erc20-puppet/ERC20System.sol +++ b/packages/world-modules/src/modules/erc20-puppet/ERC20System.sol @@ -6,18 +6,17 @@ import { System } from "@latticexyz/world/src/System.sol"; import { WorldResourceIdInstance } from "@latticexyz/world/src/WorldResourceId.sol"; import { NamespaceOwner } from "@latticexyz/world/src/codegen/tables/NamespaceOwner.sol"; import { SystemRegistry } from "@latticexyz/world/src/codegen/tables/SystemRegistry.sol"; -import { ALLOWANCES_NAME, BALANCES_NAME, METADATA_NAME } from "./constants.sol"; import { AccessControlLib } from "../../utils/AccessControlLib.sol"; import { PuppetMaster } from "../puppet/PuppetMaster.sol"; import { toTopic } from "../puppet/utils.sol"; +import { Balances } from "../tokens/tables/Balances.sol"; import { IERC20Mintable } from "./IERC20Mintable.sol"; import { Allowances } from "./tables/Allowances.sol"; -import { Balances } from "./tables/Balances.sol"; import { TotalSupply } from "./tables/TotalSupply.sol"; -import { Metadata } from "./tables/Metadata.sol"; +import { ERC20Metadata } from "./tables/ERC20Metadata.sol"; import { _allowancesTableId, _balancesTableId, _totalSupplyTableId, _metadataTableId } from "./utils.sol"; @@ -28,7 +27,7 @@ contract ERC20System is System, IERC20Mintable, PuppetMaster { * @dev Returns the name of the token. */ function name() public view virtual returns (string memory) { - return Metadata.getName(_metadataTableId(_namespace())); + return ERC20Metadata.getName(_metadataTableId(_namespace())); } /** @@ -36,7 +35,7 @@ contract ERC20System is System, IERC20Mintable, PuppetMaster { * name. */ function symbol() public view virtual returns (string memory) { - return Metadata.getSymbol(_metadataTableId(_namespace())); + return ERC20Metadata.getSymbol(_metadataTableId(_namespace())); } /** @@ -53,7 +52,7 @@ contract ERC20System is System, IERC20Mintable, PuppetMaster { * {IERC20-balanceOf} and {IERC20-transfer}. */ function decimals() public view virtual returns (uint8) { - return Metadata.getDecimals(_metadataTableId(_namespace())); + return ERC20Metadata.getDecimals(_metadataTableId(_namespace())); } /** diff --git a/packages/world-modules/src/modules/erc20-puppet/IERC20.sol b/packages/world-modules/src/modules/erc20-puppet/IERC20.sol index 8d10c323da..2129a3dc77 100644 --- a/packages/world-modules/src/modules/erc20-puppet/IERC20.sol +++ b/packages/world-modules/src/modules/erc20-puppet/IERC20.sol @@ -4,11 +4,12 @@ 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 { +interface IERC20 is IERC20Events, IERC20Errors { /** * @dev Returns the name of the token. */ diff --git a/packages/world-modules/src/modules/erc20-puppet/IERC20Mintable.sol b/packages/world-modules/src/modules/erc20-puppet/IERC20Mintable.sol index 014069ee08..3e5ece4d28 100644 --- a/packages/world-modules/src/modules/erc20-puppet/IERC20Mintable.sol +++ b/packages/world-modules/src/modules/erc20-puppet/IERC20Mintable.sol @@ -4,12 +4,11 @@ pragma solidity >=0.8.21; import { IERC20 } from "./IERC20.sol"; -import { IERC20Errors } from "./IERC20Errors.sol"; /** * @dev Extending the ERC20 standard with permissioned mint and burn functions. */ -interface IERC20Mintable is IERC20, IERC20Errors { +interface IERC20Mintable is IERC20 { /** * @dev Creates a `value` amount of tokens and assigns them to `account`, by transferring it from address(0). * diff --git a/packages/world-modules/src/modules/erc20-puppet/registerERC20.sol b/packages/world-modules/src/modules/erc20-puppet/registerERC20.sol index 7b14f89568..fb9d780a95 100644 --- a/packages/world-modules/src/modules/erc20-puppet/registerERC20.sol +++ b/packages/world-modules/src/modules/erc20-puppet/registerERC20.sol @@ -9,7 +9,7 @@ import { ERC20Module } from "./ERC20Module.sol"; import { MODULE_NAMESPACE_ID, ERC20_REGISTRY_TABLE_ID } from "./constants.sol"; import { IERC20Mintable } from "./IERC20Mintable.sol"; -import { MetadataData } from "./tables/Metadata.sol"; +import { ERC20MetadataData } from "./tables/ERC20Metadata.sol"; import { ERC20Registry } from "./tables/ERC20Registry.sol"; /** @@ -19,7 +19,7 @@ import { ERC20Registry } from "./tables/ERC20Registry.sol"; function registerERC20( IBaseWorld world, bytes14 namespace, - MetadataData memory metadata + ERC20MetadataData memory metadata ) returns (IERC20Mintable token) { // Get the ERC20 module ERC20Module erc20Module = ERC20Module(NamespaceOwner.get(MODULE_NAMESPACE_ID)); diff --git a/packages/world-modules/src/modules/erc20-puppet/tables/Metadata.sol b/packages/world-modules/src/modules/erc20-puppet/tables/ERC20Metadata.sol similarity index 97% rename from packages/world-modules/src/modules/erc20-puppet/tables/Metadata.sol rename to packages/world-modules/src/modules/erc20-puppet/tables/ERC20Metadata.sol index d24ef388df..c62353a8e4 100644 --- a/packages/world-modules/src/modules/erc20-puppet/tables/Metadata.sol +++ b/packages/world-modules/src/modules/erc20-puppet/tables/ERC20Metadata.sol @@ -24,13 +24,13 @@ FieldLayout constant _fieldLayout = FieldLayout.wrap( 0x0001010201000000000000000000000000000000000000000000000000000000 ); -struct MetadataData { +struct ERC20MetadataData { uint8 decimals; string name; string symbol; } -library Metadata { +library ERC20Metadata { /** * @notice Get the table values' field layout. * @return _fieldLayout The field layout for the table. @@ -432,7 +432,7 @@ library Metadata { /** * @notice Get the full data. */ - function get(ResourceId _tableId) internal view returns (MetadataData memory _table) { + 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( @@ -446,7 +446,7 @@ library Metadata { /** * @notice Get the full data. */ - function _get(ResourceId _tableId) internal view returns (MetadataData memory _table) { + 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( @@ -488,7 +488,7 @@ library Metadata { /** * @notice Set the full data using the data struct. */ - function set(ResourceId _tableId, MetadataData memory _table) internal { + function set(ResourceId _tableId, ERC20MetadataData memory _table) internal { bytes memory _staticData = encodeStatic(_table.decimals); PackedCounter _encodedLengths = encodeLengths(_table.name, _table.symbol); @@ -502,7 +502,7 @@ library Metadata { /** * @notice Set the full data using the data struct. */ - function _set(ResourceId _tableId, MetadataData memory _table) internal { + function _set(ResourceId _tableId, ERC20MetadataData memory _table) internal { bytes memory _staticData = encodeStatic(_table.decimals); PackedCounter _encodedLengths = encodeLengths(_table.name, _table.symbol); @@ -551,7 +551,7 @@ library Metadata { bytes memory _staticData, PackedCounter _encodedLengths, bytes memory _dynamicData - ) internal pure returns (MetadataData memory _table) { + ) internal pure returns (ERC20MetadataData memory _table) { (_table.decimals) = decodeStatic(_staticData); (_table.name, _table.symbol) = decodeDynamic(_encodedLengths, _dynamicData); 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/erc20-puppet/tables/Balances.sol b/packages/world-modules/src/modules/tokens/tables/Balances.sol similarity index 100% rename from packages/world-modules/src/modules/erc20-puppet/tables/Balances.sol rename to packages/world-modules/src/modules/tokens/tables/Balances.sol diff --git a/packages/world-modules/test/ERC20.t.sol b/packages/world-modules/test/ERC20.t.sol index 8b62a34675..9416e8aacd 100644 --- a/packages/world-modules/test/ERC20.t.sol +++ b/packages/world-modules/test/ERC20.t.sol @@ -16,7 +16,7 @@ import { GasReporter } from "@latticexyz/gas-report/src/GasReporter.sol"; import { PuppetModule } from "../src/modules/puppet/PuppetModule.sol"; import { ERC20Module } from "../src/modules/erc20-puppet/ERC20Module.sol"; -import { MetadataData } from "../src/modules/erc20-puppet/tables/Metadata.sol"; +import { 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"; @@ -36,7 +36,7 @@ contract ERC20Test is Test, GasReporter, IERC20Events, IERC20Errors { StoreSwitch.setStoreAddress(address(world)); // Register a new ERC20 token - token = registerERC20(world, "myERC20", MetadataData({ decimals: 18, name: "Token", symbol: "TKN" })); + token = registerERC20(world, "myERC20", ERC20MetadataData({ decimals: 18, name: "Token", symbol: "TKN" })); } function testSetUp() public { @@ -49,7 +49,7 @@ contract ERC20Test is Test, GasReporter, IERC20Events, IERC20Errors { IERC20Mintable anotherToken = registerERC20( world, "anotherERC20", - MetadataData({ decimals: 18, name: "Token", symbol: "TKN" }) + ERC20MetadataData({ decimals: 18, name: "Token", symbol: "TKN" }) ); assertTrue(address(anotherToken) != address(0)); assertTrue(address(anotherToken) != address(token)); 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); + } +}