From 6fea3ad9cf3ca0358cd7493a71c29891ad4ce53d Mon Sep 17 00:00:00 2001 From: Ori Pomerantz Date: Tue, 10 Sep 2024 13:53:47 -0500 Subject: [PATCH] first version --- docs/pages/world/modules/erc20.mdx | 143 +++++++++++++++-------------- 1 file changed, 75 insertions(+), 68 deletions(-) diff --git a/docs/pages/world/modules/erc20.mdx b/docs/pages/world/modules/erc20.mdx index 6621aed35f..1bd906b91c 100644 --- a/docs/pages/world/modules/erc20.mdx +++ b/docs/pages/world/modules/erc20.mdx @@ -9,7 +9,7 @@ This module is unaudited and may change in the future. -The [`erc20-puppet`](https://github.com/latticexyz/mud/tree/main/packages/world-modules/src/modules/erc20-puppet) module lets you create [ERC-20](https://ethereum.org/en/developers/docs/standards/tokens/erc-20/) NFTs as part of a MUD `World`. +The [`erc20-puppet`](https://github.com/latticexyz/mud/tree/main/packages/world-modules/src/modules/erc20-puppet) module lets you create [ERC-20](https://ethereum.org/en/developers/docs/standards/tokens/erc-20/) tokens as part of a MUD `World`. The advantage of doing this, rather than creating a separate [ERC-20 contract](https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/token/ERC20) and merely controlling it from MUD, is that all the information is in MUD tables and is immediately available in the client. ## Deployment @@ -77,21 +77,21 @@ import { encodeAbiParameters, stringToHex } from "viem"; ``` In simple cases it is enough to use the config parser to specify the module arguments. -However, the NFT module requires a `struct` as [one of the arguments](https://github.com/latticexyz/mud/blob/main/packages/world-modules/src/modules/erc721-puppet/ERC721Module.sol#L37). +However, the ERC-20 module requires a `struct` as [one of the arguments](https://github.com/latticexyz/mud/blob/main/packages/world-modules/src/modules/erc20-puppet/ERC20Module.sol#L34). We use [`encodeAbiParameters`](https://viem.sh/docs/abi/encodeAbiParameters.html) to encode the `struct` data. The [`stringToHex`](https://viem.sh/docs/utilities/toHex.html#stringtohex) function is used to specify the namespace the token uses. This is the reason we need to issue `pnpm install viem` in `packages/contracts` to be able to use the library here. ```typescript -const erc721ModuleArgs = encodeAbiParameters( +const erc20ModuleArgs = encodeAbiParameters( ``` -You can see the arguments for the ERC-721 module [here](https://github.com/latticexyz/mud/blob/main/packages/world-modules/src/modules/erc721-puppet/ERC721Module.sol#L37). +You can see the arguments for the ERC-20 module [here](https://github.com/latticexyz/mud/blob/main/packages/world-modules/src/modules/erc20-puppet/ERC20Module.sol#L34). There are two arguments: - A 14-byte identifier for the namespace. -- An `ERC721MetadataData` for the ERC-721 parameters, [defined here](https://github.com/latticexyz/mud/blob/main/packages/world-modules/src/modules/erc721-puppet/tables/ERC721Metadata.sol#L19-L23). +- An `ERC20MetadataData` for the ERC-20 parameters, [defined here](https://github.com/latticexyz/mud/blob/main/packages/world-modules/src/modules/erc20-puppet/tables/ERC20Metadata.sol#L19-L23). However, the arguments for a module are [ABI encoded](https://docs.soliditylang.org/en/develop/abi-spec.html) to a single value of type `bytes`. So we use `encodeAbiParameters` from the viem library to create this argument. @@ -107,32 +107,31 @@ The first parameter is simple, a 14 byte value for the namespace. ```typescript { type: "tuple", - components: [{ type: "string" }, { type: "string" }, { type: "string" }], + components: [{ type: "uint8" }, { type: "string" }, { type: "string" }], }, - ], ``` The second value is more complicated, it's a struct, or as it is called in ABI, a tuple. -It consists of three strings (the token name, symbol, and [base URI](https://docs.openzeppelin.com/contracts/3.x/api/token/erc721#ERC721-baseURI--)). +The first field is the number of digits after the decimal point when displaying the token. +The second field is the token's full name, and the third a short symbol for it. ```typescript [ - stringToHex("MyNFT", { size: 14 }), + stringToHex("MyToken", { size: 14 }), ``` The second `encodeAbiParameters` parameter is a list of the values, of the types declared in the first list. -The first parameter for the module is `bytes14`, the namespace of the ERC-721 token. +The first parameter for the module is `bytes14`, the namespace of the ERC-20 token. We use [`stringToHex`](https://viem.sh/docs/utilities/toHex.html#stringtohex) to convert it from the text form that is easy for us to use, to the hexadecimal number that Viem expects for `bytes14` parameter. ```typescript - ["No Valuable Token", "NVT", "http://www.example.com/base/uri/goes/here"], + [18, "Worthless Token", "WT"]], ], ); ``` -The second parameter for the module is a structure of three strings, so here we provide the three strings. -Then we close all the definitions. +The second parameter for the module is the [`ERC20MetadataData`](https://github.com/latticexyz/mud/blob/main/packages/world-modules/src/modules/erc20-puppet/tables/ERC20Metadata.sol#L19-L23) structure. ```typesceript modules: [ @@ -156,7 +155,7 @@ The solution is to put the `System` in one contract and have another contract, t ```typescript { - artifactPath: "@latticexyz/world-modules/out/ERC721Module.sol/ERC721Module.json", + artifactPath: "@latticexyz/world-modules/out/ERC20Module.sol/ERC20Module.json", root: false, args: [ { @@ -166,39 +165,45 @@ The solution is to put the `System` in one contract and have another contract, t The data type for this parameter is `bytes`, because it is treated as opaque bytes by the `World` and only gets parsed by the module after it is transferred. ```typescript - value: erc721ModuleArgs, + value: erc20ModuleArgs, }, ], }, ``` -The module arguments, stored in `erc721ModuleArgs`. +The module arguments, stored in `erc20ModuleArgs`. ## Usage -You can use the token the same way you use any other ERC721 contract. +You can use the token the same way you use any other ERC20 contract. For example, run this script. -```solidity filename="ManageERC721.s.sol" copy {29-33, 41-57} -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.24; - -import { Script } from "forge-std/Script.sol"; +```solidity filename="ManageERC20.s.sol" copy showLineNumbers {16,35-39,45,50-64} import { console } from "forge-std/console.sol"; import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; 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 { ERC721Registry } from "@latticexyz/world-modules/src/codegen/index.sol"; -import { IERC721Mintable } from "@latticexyz/world-modules/src/modules/erc721-puppet/IERC721Mintable.sol"; +import { ERC20Registry } from "@latticexyz/world-modules/src/codegen/index.sol"; +import { IERC20Mintable } from "@latticexyz/world-modules/src/modules/erc20-puppet/IERC20Mintable.sol"; import { IWorld } from "../src/codegen/world/IWorld.sol"; -contract ManageERC721 is Script { +contract ManageERC20 is Script { + function reportBalances(IERC20Mintable erc20, address myAddress) internal view { + address goodGuy = address(0x600D); + address badGuy = address(0x0BAD); + + console.log(" My balance:", erc20.balanceOf(myAddress)); + console.log("Goodguy balance:", erc20.balanceOf(goodGuy)); + console.log(" Badguy balance:", erc20.balanceOf(badGuy)); + console.log("--------------"); + } + function run() external { address worldAddress = address(0x8D8b6b8414E1e3DcfD4168561b9be6bD3bF6eC4B); @@ -212,37 +217,36 @@ contract ManageERC721 is Script { // Start broadcasting transactions from the deployer account vm.startBroadcast(deployerPrivateKey); - // Get the ERC-721 token address - ResourceId namespaceResource = WorldResourceIdLib.encodeNamespace(bytes14("MyNFT")); - ResourceId erc721RegistryResource = WorldResourceIdLib.encode(RESOURCE_TABLE, "erc721-puppet", "ERC721Registry"); - address tokenAddress = ERC721Registry.getTokenAddress(erc721RegistryResource, namespaceResource); + // Get the ERC-20 token address + ResourceId namespaceResource = WorldResourceIdLib.encodeNamespace(bytes14("MyToken")); + ResourceId erc20RegistryResource = WorldResourceIdLib.encode(RESOURCE_TABLE, "erc20-puppet", "ERC20Registry"); + address tokenAddress = ERC20Registry.getTokenAddress(erc20RegistryResource, namespaceResource); console.log("Token address", tokenAddress); - // Settings to test with - uint256 badGoatToken = uint256(0xBAD060A7); - uint256 beefToken = uint256(0xBEEF); address goodGuy = address(0x600D); address badGuy = address(0x0BAD); // Use the token - IERC721Mintable erc721 = IERC721Mintable(tokenAddress); + IERC20Mintable erc20 = IERC20Mintable(tokenAddress); - // Mint two tokens - erc721.mint(goodGuy, badGoatToken); - erc721.mint(myAddress, beefToken); - console.log("Owner of bad goat:", erc721.ownerOf(badGoatToken)); - console.log("Owner of beef:", erc721.ownerOf(beefToken)); + console.log("Initial state"); + reportBalances(erc20, myAddress); - // Transfer a token - erc721.transferFrom(myAddress, badGuy, beefToken); - console.log("Owner of bad goat:", erc721.ownerOf(badGoatToken)); - console.log("Owner of beef:", erc721.ownerOf(beefToken)); + // Mint some tokens + console.log("Minting for myself and Badguy"); + erc20.mint(myAddress, 1000); + erc20.mint(badGuy, 500); + reportBalances(erc20, myAddress); - // Burn the tokens - erc721.burn(badGoatToken); - erc721.burn(beefToken); + // Transfer tokens + console.log("Transfering to Goodguy"); + erc20.transfer(goodGuy, 750); + reportBalances(erc20, myAddress); - console.log("Done"); + // Burn tokens + console.log("Burning badGuy's tokens"); + erc20.burn(badGuy, 500); + reportBalances(erc20, myAddress); vm.stopBroadcast(); } @@ -256,52 +260,55 @@ contract ManageERC721 is Script { Explanation ```solidity - // Get the ERC-721 token address - ResourceId namespaceResource = WorldResourceIdLib.encodeNamespace(bytes14("MyNFT")); - ResourceId erc721RegistryResource = - WorldResourceIdLib.encode(RESOURCE_TABLE, "erc721-puppet", "ERC721Registry"); - address tokenAddress = ERC721Registry.getTokenAddress(erc721RegistryResource, namespaceResource); + console.log(" My balance:", erc20.balanceOf(myAddress)); +``` + +[The `balanceOf` function](https://eips.ethereum.org/EIPS/eip-20#balanceof) is the way ERC-20 specifies to get an address's balance. + +```solidity + // Get the ERC-20 token address + ResourceId namespaceResource = WorldResourceIdLib.encodeNamespace(bytes14("MyToken")); + ResourceId erc20RegistryResource = WorldResourceIdLib.encode(RESOURCE_TABLE, "erc20-puppet", "ERC20Registry"); + address tokenAddress = ERC20Registry.getTokenAddress(erc20RegistryResource, namespaceResource); console.log("Token address", tokenAddress); ``` This is the process to get the address of our token contract (the puppet). -First, we get the [`resourceId` values](/world/resource-ids) for the `erc721-puppet__ERC721Registry` table and the namespace we are interested in (each namespace can only have one ERC721 token). +First, we get the [`resourceId` values](/world/resource-ids) for the `erc20-puppet__ERC20Registry` table and the namespace we are interested in (each namespace can only have one ERC-20 token). Then we use that table to get the token address. ```solidity // Use the token - IERC721Mintable erc721 = IERC721Mintable(tokenAddress); + IERC20Mintable erc20 = IERC20Mintable(tokenAddress); ``` -Create an [`IERC721Mintable`](https://github.com/latticexyz/mud/blob/main/packages/world-modules/src/modules/erc721-puppet/IERC721Mintable.sol) for the token. +Create an [`IERC20Mintable`](https://github.com/latticexyz/mud/blob/main/packages/world-modules/src/modules/erc20-puppet/IERC20Mintable.sol) for the token. ```solidity - // Mint two tokens - erc721.mint(goodGuy, badGoatToken); - erc721.mint(myAddress, beefToken); - console.log("Owner of bad goat:", erc721.ownerOf(badGoatToken)); - console.log("Owner of beef:", erc721.ownerOf(beefToken)); + console.log("Minting for myself and Badguy"); + erc20.mint(myAddress, 1000); + erc20.mint(badGuy, 500); + reportBalances(erc20, myAddress); ``` -Mint a couple of tokens, and show who owns them. +Mint tokens for two addresses. Note that only the owner of the name space is authorized to mint tokens. ```solidity - // Transfer a token - erc721.transferFrom(myAddress, badGuy, beefToken); - console.log("Owner of bad goat:", erc721.ownerOf(badGoatToken)); - console.log("Owner of beef:", erc721.ownerOf(beefToken)); + console.log("Transfering to Goodguy"); + erc20.transfer(goodGuy, 750); + reportBalances(erc20, myAddress); ``` Transfer a token. We can only transfer tokens we own, or that we have approval to transfer from the current owner. ```solidity - // Burn the tokens - erc721.burn(badGoatToken); - erc721.burn(beefToken); + console.log("Burning badGuy's tokens"); + erc20.burn(badGuy, 500); + reportBalances(erc20, myAddress); ``` -Destroy the tokens. +Destroy some tokens.