From e9c75945b39759db51c6d3da69e6779280effde8 Mon Sep 17 00:00:00 2001 From: Ori Pomerantz Date: Wed, 11 Sep 2024 03:15:30 -0500 Subject: [PATCH] docs(world/modules/erc20): first version (#3166) --- docs/pages/world/modules/_meta.js | 1 + docs/pages/world/modules/erc20.mdx | 314 +++++++++++++++++++++++++++++ 2 files changed, 315 insertions(+) create mode 100644 docs/pages/world/modules/erc20.mdx diff --git a/docs/pages/world/modules/_meta.js b/docs/pages/world/modules/_meta.js index 0c5379cd01..de7612a3b3 100644 --- a/docs/pages/world/modules/_meta.js +++ b/docs/pages/world/modules/_meta.js @@ -1,5 +1,6 @@ export default { "keyswithvalue": "Keys with Value", "keysintable": "Keys in Table", + "erc20": "ERC-20 tokens", "erc721": "ERC-721 (NFT)" }; diff --git a/docs/pages/world/modules/erc20.mdx b/docs/pages/world/modules/erc20.mdx new file mode 100644 index 0000000000..1bd906b91c --- /dev/null +++ b/docs/pages/world/modules/erc20.mdx @@ -0,0 +1,314 @@ +import { CollapseCode } from "../../../components/CollapseCode"; +import { Callout } from "nextra/components"; + +# ERC 20 (fungible tokens) module + + + +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/) 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 + +The easiest way to deploy this module is to edit `mud.config.ts`. +This is a modified version of the [vanilla](/templates/typescript/contracts) template. + +Note that before you use this file you need to run `pnpm add viem` (see explanation below). + + + +```typescript filename="mud.config.ts" showLineNumbers copy {2-13,25-41} +import { defineWorld } from "@latticexyz/world"; +import { encodeAbiParameters, stringToHex } from "viem"; + +const erc20ModuleArgs = encodeAbiParameters( + [ + { type: "bytes14" }, + { + type: "tuple", + components: [{ type: "uint8" }, { type: "string" }, { type: "string" }], + }, + ], + [stringToHex("MyToken", { size: 14 }), [18, "Worthless Token", "WT"]], +); + +export default defineWorld({ + namespace: "app", + tables: { + Counter: { + schema: { + value: "uint32", + }, + key: [], + }, + }, + modules: [ + { + artifactPath: "@latticexyz/world-modules/out/PuppetModule.sol/PuppetModule.json", + root: false, + args: [], + }, + { + artifactPath: "@latticexyz/world-modules/out/ERC20Module.sol/ERC20Module.json", + root: false, + args: [ + { + type: "bytes", + value: erc20ModuleArgs, + }, + ], + }, + ], +}); +``` + + + +
+ +Explanation + +```typescript +import { encodeAbiParameters, stringToHex } from "viem"; +``` + +In simple cases it is enough to use the config parser to specify the module arguments. +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 erc20ModuleArgs = encodeAbiParameters( +``` + +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 `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. +The first parameter of this function is a list of argument types. + +```typescript + [ + { type: "bytes14" }, +``` + +The first parameter is simple, a 14 byte value for the namespace. + +```typescript + { + type: "tuple", + 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. +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("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-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 + [18, "Worthless Token", "WT"]], + ], +); +``` + +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: [ + { + artifactPath: "@latticexyz/world-modules/out/PuppetModule.sol/PuppetModule.json", + root: false, + args: [], + }, +``` + +A module declaration requires three parameters: + +- `artifactPath`, a link to the compiled JSON file for the module. +- `root`, whether to install the module with [root namespace permissions](/world/systems#root-systems) or not. +- `args` the module arguments. + +Here we install [the `puppet` module](https://github.com/latticexyz/mud/tree/main/packages/world-modules/src/modules/puppet). +We need this module because a `System` is supposed to be stateless, and easily upgradeable to a contract in a different address. +However, both the [ERC-20 standard](https://ethereum.org/en/developers/docs/standards/tokens/erc-20/) and the [ERC-721 standard](https://ethereum.org/en/developers/docs/standards/tokens/erc-721/) require the token contract to emit events. +The solution is to put the `System` in one contract and have another contract, the puppet, which receives requests and emits events according to the ERC. + +```typescript + { + artifactPath: "@latticexyz/world-modules/out/ERC20Module.sol/ERC20Module.json", + root: false, + args: [ + { + type: "bytes", +``` + +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: erc20ModuleArgs, + }, + ], + }, +``` + +The module arguments, stored in `erc20ModuleArgs`. + +
+ +## Usage + +You can use the token the same way you use any other ERC20 contract. +For example, run this script. + + + +```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 { 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 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); + + // Specify a store so that you can use tables directly in PostDeploy + StoreSwitch.setStoreAddress(worldAddress); + + // Load the private key from the `PRIVATE_KEY` environment variable (in .env) + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address myAddress = vm.addr(deployerPrivateKey); + + // Start broadcasting transactions from the deployer account + vm.startBroadcast(deployerPrivateKey); + + // 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); + + address goodGuy = address(0x600D); + address badGuy = address(0x0BAD); + + // Use the token + IERC20Mintable erc20 = IERC20Mintable(tokenAddress); + + console.log("Initial state"); + reportBalances(erc20, myAddress); + + // Mint some tokens + console.log("Minting for myself and Badguy"); + erc20.mint(myAddress, 1000); + erc20.mint(badGuy, 500); + reportBalances(erc20, myAddress); + + // Transfer tokens + console.log("Transfering to Goodguy"); + erc20.transfer(goodGuy, 750); + reportBalances(erc20, myAddress); + + // Burn tokens + console.log("Burning badGuy's tokens"); + erc20.burn(badGuy, 500); + reportBalances(erc20, myAddress); + + vm.stopBroadcast(); + } +} +``` + + + +
+ +Explanation + +```solidity + 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 `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 + IERC20Mintable erc20 = IERC20Mintable(tokenAddress); +``` + +Create an [`IERC20Mintable`](https://github.com/latticexyz/mud/blob/main/packages/world-modules/src/modules/erc20-puppet/IERC20Mintable.sol) for the token. + +```solidity + console.log("Minting for myself and Badguy"); + erc20.mint(myAddress, 1000); + erc20.mint(badGuy, 500); + reportBalances(erc20, myAddress); +``` + +Mint tokens for two addresses. +Note that only the owner of the name space is authorized to mint tokens. + +```solidity + 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 + console.log("Burning badGuy's tokens"); + erc20.burn(badGuy, 500); + reportBalances(erc20, myAddress); +``` + +Destroy some tokens. + +