-
Notifications
You must be signed in to change notification settings - Fork 196
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs(world/modules/erc20): first version (#3166)
- Loading branch information
Showing
2 changed files
with
315 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)" | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |