Skip to content

Commit

Permalink
docs(world/modules/erc20): first version (#3166)
Browse files Browse the repository at this point in the history
  • Loading branch information
qbzzt authored Sep 11, 2024
1 parent b252618 commit e9c7594
Show file tree
Hide file tree
Showing 2 changed files with 315 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/pages/world/modules/_meta.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export default {
"keyswithvalue": "Keys with Value",
"keysintable": "Keys in Table",
"erc20": "ERC-20 tokens",
"erc721": "ERC-721 (NFT)"
};
314 changes: 314 additions & 0 deletions docs/pages/world/modules/erc20.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,314 @@
import { CollapseCode } from "../../../components/CollapseCode";
import { Callout } from "nextra/components";

# ERC 20 (fungible tokens) module

<Callout type="warning" emoji="⚠️">

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

<CollapseCode>

```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,
},
],
},
],
});
```

</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>

## Usage

You can use the token the same way you use any other ERC20 contract.
For example, run this script.

<CollapseCode>

```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();
}
}
```

</CollapseCode>

<details>

<summary>Explanation</summary>

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

</details>

0 comments on commit e9c7594

Please sign in to comment.