Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs(world-module-erc20): replace erc20 docs #3383

Merged
merged 3 commits into from
Nov 28, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 42 additions & 183 deletions docs/pages/world/modules/erc20.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,22 @@ This module is unaudited and may change in the future.

</Callout>

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 [`erc20` module](https://github.com/latticexyz/mud/tree/main/packages/world-module-erc20/) 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 ERC20Module receives the namespace, name and symbol of the token as parameters, and deploys the new token. Currently it installs a [default ERC20](https://github.com/latticexyz/mud/tree/main/packages/world-module-erc20/src/examples/ERC20WithWorld.sol) with the following features:

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.
- ERC20Burnable: Allows users to burn their tokens (or the ones approved to them) using the `burn` and `burnFrom` function.
- ERC20Pausable: Supports pausing and unpausing token operations. This is combined with the `pause` and `unpause` public functions that can be called by addresses with access to the token's namespace.
- Minting: Addresses with namespace access can call the `mint` function to mint tokens to any address.

Note that before you use this file you need to run `pnpm add viem` (see explanation below).
## Installation

<CollapseCode>
The simplest way to install this module and register a new ERC20 token in your world is to import the `defineERC20Module` helper and use it to add the module's declaration to your MUD config:

```typescript filename="mud.config.ts" showLineNumbers copy {2-13,25-41}
```typescript filename="mud.config.ts"
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"]],
);
import { defineERC20Module } from "@latticexyz/world-module-erc20/internal";

export default defineWorld({
namespace: "app",
Expand All @@ -47,133 +37,19 @@ export default defineWorld({
},
},
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,
},
],
},
defineERC20Module({
// The new namespace the module will register
namespace: "erc20Namespace",
// The metadata of the ERC20 token that will be deployed by the module
name: "MyToken",
symbol: "MTK",
}),
],
});
```

</CollapseCode>

<details>

<summary>Explanation</summary>

```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`.

</details>
This will deploy the token and register it under the provided namespace. Note that the namespace must not exist beforehand, as the module will create it upon installation.
The ownership of the new namespace will be transferred to the deployer after installation.

## Usage

Expand All @@ -182,25 +58,24 @@ For example, run this script.

<CollapseCode>

```solidity filename="ManageERC20.s.sol" copy showLineNumbers {16,35-39,45,50-64}
```solidity filename="ManageERC20.s.sol" copy showLineNumbers {16,34-38,43,48-56}
import { Script } from "forge-std/Script.sol";
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 { ERC20Registry } from "@latticexyz/world-module-erc20/src/codegen/index.sol";
import { ERC20WithWorld as ERC20 } from "@latticexyz/world-module-erc20/src/examples/ERC20WithWorld.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);
function reportBalances(ERC20 erc20, address myAddress) internal view {
address alice = address(0x600D);

console.log(" My balance:", erc20.balanceOf(myAddress));
console.log("Goodguy balance:", erc20.balanceOf(goodGuy));
console.log(" Badguy balance:", erc20.balanceOf(badGuy));
console.log("Alice's balance:", erc20.balanceOf(alice));
console.log("--------------");
}

Expand All @@ -218,34 +93,27 @@ contract ManageERC20 is Script {
vm.startBroadcast(deployerPrivateKey);

// Get the ERC-20 token address
ResourceId namespaceResource = WorldResourceIdLib.encodeNamespace(bytes14("MyToken"));
ResourceId erc20RegistryResource = WorldResourceIdLib.encode(RESOURCE_TABLE, "erc20-puppet", "ERC20Registry");
ResourceId namespaceResource = WorldResourceIdLib.encodeNamespace(bytes14("erc20Namespace"));
ResourceId erc20RegistryResource = WorldResourceIdLib.encode(RESOURCE_TABLE, "erc20-module", "ERC20_REGISTRY");
address tokenAddress = ERC20Registry.getTokenAddress(erc20RegistryResource, namespaceResource);
console.log("Token address", tokenAddress);

address goodGuy = address(0x600D);
address badGuy = address(0x0BAD);
address alice = address(0x600D);

// Use the token
IERC20Mintable erc20 = IERC20Mintable(tokenAddress);
ERC20 erc20 = ERC20(tokenAddress);

console.log("Initial state");
reportBalances(erc20, myAddress);

// Mint some tokens
console.log("Minting for myself and Badguy");
console.log("Minting for myself");
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);
console.log("Transfering to Alice");
erc20.transfer(alice, 750);
reportBalances(erc20, myAddress);

vm.stopBroadcast();
Expand All @@ -267,48 +135,39 @@ contract ManageERC20 is Script {

```solidity
// Get the ERC-20 token address
ResourceId namespaceResource = WorldResourceIdLib.encodeNamespace(bytes14("MyToken"));
ResourceId erc20RegistryResource = WorldResourceIdLib.encode(RESOURCE_TABLE, "erc20-puppet", "ERC20Registry");
ResourceId namespaceResource = WorldResourceIdLib.encodeNamespace(bytes14("erc20Namespace"));
ResourceId erc20RegistryResource = WorldResourceIdLib.encode(RESOURCE_TABLE, "erc20-module", "ERC20_REGISTRY");
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).
This is the process to get the address of our token contract.
First, we get the [`resourceId` values](/world/resource-ids) for the `erc20-module__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);
ERC20 erc20 = ERC20(tokenAddress);
```

Create an [`IERC20Mintable`](https://github.com/latticexyz/mud/blob/main/packages/world-modules/src/modules/erc20-puppet/IERC20Mintable.sol) for the token.
Cast the token address to an `ERC20` contract so we can call its methods.

```solidity
console.log("Minting for myself and Badguy");
console.log("Minting for myself");
erc20.mint(myAddress, 1000);
erc20.mint(badGuy, 500);
reportBalances(erc20, myAddress);
```

Mint tokens for two addresses.
Mint tokens for your address.
Note that only the owner of the name space is authorized to mint tokens.

```solidity
console.log("Transfering to Goodguy");
erc20.transfer(goodGuy, 750);
console.log("Transfering to Alice");
erc20.transfer(alice, 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.

</details>