Skip to content

Commit

Permalink
first version
Browse files Browse the repository at this point in the history
  • Loading branch information
qbzzt committed Sep 10, 2024
1 parent 558b263 commit 6fea3ad
Showing 1 changed file with 75 additions and 68 deletions.
143 changes: 75 additions & 68 deletions docs/pages/world/modules/erc20.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ 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/) 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
Expand Down Expand Up @@ -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.
Expand All @@ -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: [
Expand All @@ -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: [
{
Expand All @@ -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`.

</details>

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

<CollapseCode>

```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);
Expand All @@ -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();
}
Expand All @@ -256,52 +260,55 @@ contract ManageERC721 is Script {
<summary>Explanation</summary>

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

</details>

0 comments on commit 6fea3ad

Please sign in to comment.