ERC 721 (NFT) module
This module is unaudited and may change in the future.
The erc721-puppet
(opens in a new tab) module lets you create ERC-721 (opens in a new tab) NFTs as part of a MUD World
.
The advantage of doing this, rather than creating a separate NFT contract (opens in a new tab) 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
.
import { defineWorld } from "@latticexyz/world";
import { encodeAbiParameters } from "viem";
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/ERC721Module.sol/ERC721Module.json",
root: false,
args: [
{
type: "bytes",
value: encodeAbiParameters(
[
{ type: "bytes14" },
{
type: "tuple",
components: [{ type: "string" }, { type: "string" }, { type: "string" }],
},
], // end of list of types
[
"0x44444444".padEnd(30, "0"),
["No Valuable Token", "NVT", "http://www.example.com/base/uri/goes/here"], // end of the ERC-721 metadata tuple
], // end of parameter list
), // end of encodeAbiParameters call
}, // end of the argument
], // end of list of args for the module
}, // end of module definition for the ERC-721 module
], // end of module list
}); // end of defineWorld call
Explanation
import { encodeAbiParameters } 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 (opens in a new tab).
We use encodeAbiParameters
(opens in a new tab) to encode the struct
data.
Note that this means we need to issue pnpm install viem
in packages/contracts
to be able to use the library here.
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 or not.args
the module arguments.
Here we install the puppet
module (opens in a new tab).
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 (opens in a new tab) and the ERC-721 standard (opens in a new tab) 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.
{
artifactPath: "@latticexyz/world-modules/out/ERC721Module.sol/ERC721Module.json",
root: false,
args: [
You can see the arguments for the ERC-721 module here (opens in a new tab). There are two arguments:
- A 14-byte identifier for the namespace.
- An
ERC721MetadataData
for the ERC-721 parameters, defined here (opens in a new tab).
However, the arguments for a module are ABI encoded (opens in a new tab) to a single value of type bytes
.
{
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.
value: encodeAbiParameters(
[
Use encodeAbiParameters
from the viem library to create the argument.
The first parameter of this function is a list of argument types.
[
{ type: 'bytes14' },
The first parameter is simple, a 14 byte value for the namespace.
{
type: 'tuple',
components: [
{ "type": "string" },
{ "type": "string" },
{ "type": "string" },
]
}
], // end of list of types
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 (opens in a new tab)).
[
"0x44444444".padEnd(30, "0"),
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
, which is expected to be a hexadecimal value with twenty-eight hexadecimal digits, for a total length of thirty.
Here we use 0x4...40....0
with eight 4's followed by twenty 0's.
This gives us the namespace DDDD
, which is easy to recognize both as hex and as text.
["No Valuable Token", "NVT", "http://www.example.com/base/uri/goes/here"]; // end of the ERC-721 metadata tuple
The second parameter for the module is a structure of three strings, so here we provide the three strings.
] // end of parameter list
) // end of encodeAbiParameters call
} // end of the argument
], // end of list of args for the module
} // end of module definition for the ERC-721 module
] // end of module list
}); // end of defineWorld call
And then we need to finish all the definitions.
Usage
You can use the token the same way you use any other ERC721 contract. For example, run this script.
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;
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 { ERC721Registry } from "@latticexyz/world-modules/src/codegen/index.sol";
import { IERC721Mintable } from "@latticexyz/world-modules/src/modules/erc721-puppet/IERC721Mintable.sol";
import { IWorld } from "../src/codegen/world/IWorld.sol";
contract ManageERC721 is Script {
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-721 token address
ResourceId namespaceResource = WorldResourceIdLib.encodeNamespace(bytes14("DDDD"));
ResourceId erc721RegistryResource = WorldResourceIdLib.encode(RESOURCE_TABLE, "erc721-puppet", "ERC721Registry");
address tokenAddress = ERC721Registry.getTokenAddress(erc721RegistryResource, 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);
// 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));
// 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));
// Burn the tokens
erc721.burn(badGoatToken);
erc721.burn(beefToken);
console.log("Done");
vm.stopBroadcast();
}
}
Explanation
// Get the ERC-721 token address
ResourceId namespaceResource = WorldResourceIdLib.encodeNamespace(bytes14("DDDD"));
ResourceId erc721RegistryResource =
WorldResourceIdLib.encode(RESOURCE_TABLE, "erc721-puppet", "ERC721Registry");
address tokenAddress = ERC721Registry.getTokenAddress(erc721RegistryResource, 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 for the erc721-puppet__ERC721Registry
table and the namespace we are interested in (each namespace can only have one ERC721 token).
Then we use ERC721Registry
to get the token address.
// Use the token
IERC721Mintable erc721 = IERC721Mintable(tokenAddress);
Create an IERC721Mintable
(opens in a new tab) for the token.
// 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));
Mint a couple of tokens, and show who owns them. Note that only the owner of the name space is authorized to mint tokens.
// 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));
Transfer a token. We can only transfer tokens we own, or that we have approval to transfer from the current owner.
// Burn the tokens
erc721.burn(badGoatToken);
erc721.burn(beefToken);
Destroy the tokens.