You've successfully subscribed to ZKF
Great! Next, complete checkout for full access to ZKF
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.
Success! Your billing info is updated.
Billing info update failed.

Tokens of Concord Smart Contract Overview

My overview (and review) of the Tokens of Concord ERC1155 smart contract.

0xZakk
0xZakk
. 4 min read
Tokens of Concord Smart Contract Overview

WAGDIE is a story-based NFT game where players can collect characters and in-game items to go on quests and participate in tournaments. It looks and feels reminiscent of Dungeons and Dragons, but the team has put in a lot of effort to come up with novel quests and to build a unique universe of game play.

The characters are all stored in an ERC721A contract while the in-game items are stored in an ERC1155 contract. In this article, I want to provide an overview of the Tokens of Concord contract, the ERC1155 for in-game items, because it has some interesting and unique features in its implementation. Specifically, the things I'll discuss are:

  1. The contract isn't deployed with any preset tokens, as you might expect. Instead, tokens are created after deployment.
  2. It does not have any logic for managing token supplies (like limiting a token to a total supply of 10).
  3. It does not include logic for selling tokens from the contract.
  4. It has four different methods for minting tokens once they're created on the contract.

These deviations from common ERC1155 contracts make for a very simple and flexible contract.

Creating Tokens of Concord

The Tokens of Concord contract is impressive because it's very simple. The authors have managed to cut out a lot of code while still giving themselves a lot of flexibility. One way they've done this is in how the contract manages tokens and how new tokens can be created.

Tokens are stored in one, single-level mapping called tokenURIs:

mapping(uint256 => string) private tokenURIs;

When the contract was deployed, it didn't include any pre-defined tokens created in the constructor. Instead, the team is able to create new tokens using the setURI() method. I'm assuming they do this as part of game play, adding new in-game items as necessary:

function setURI(uint256 _token, string memory _tokenURI)
    external
    onlyCreator
{
    tokenURIs[_token] = _tokenURI;
    emit URI(uri(_token), _token);
}

The nice thing is this token can actually be used for making entirely new tokens or updating the URI of an existing token.

The contract is not tracking token IDs (uint256 _token in the above snippet), so the creators must be doing that manually. That sounds annoying and it wouldn't be too much to start tracking the tokens with a nextTokenId counter variable:

// ... license, pragma, imports, etc

contract TokensOfConcord is ERC1155, IERC2981, Ordainable {
	// ... header/token info
	
	uint8 public nextTokenID = 1;
	mapping(uint8 => string) private tokenURIs;
	
	// .. rest of the contract logic
}

Notice that I changed the IDs from a uint256 to a uint8. Based on how they're using the contract and the tokens they've created so far, I don't think they need a full uint256 for their tokenIDs.

We're also setting the starting value for nextTokenID to 1 because tokens in an ERC1155 can't have an ID of 0. So the first token needs to have an ID of 1.

What I would do next is overload the setURI method with one implementation for editing an existing URI and one for creating a new token:

/*
 * This implementation of `setURI` can be used for creating
 * new tokens without needing to manually track the next
 * token ID, now that the contract is doing that.
 */
function setURI(string memory _tokenURI) external onlyCreator {
    uint256 tokenId = nextTokenID;
    tokenURIs[tokenId] = _tokenURI;

    ++nextTokenID;

    emit URI(uri(tokenId), tokenId);
}

/*
 * This implementation of `setURI` is almost identical to
 * the one in the deployed contract, except it adds a
 * `require` statement to ensure the token URI you're changing
 * already exists. This prevents you from setting a tokenURI
 * for a token with an ID at or above the `nextTokenID`. 
 */
function setURI(uint256 _token, string memory _tokenURI)
    external
    onlyCreator
{
    require(bytes(tokenURIs[_token]).length > 0);

    tokenURIs[_token] = _tokenURI;
    emit URI(uri(_token), _token);
}

In the above snippet, I've added a second implementation of setURI that can be used for creating entirely new tokens while keeping the existing implementation for changing the URI of an existing token.

Token Supplies

The Tokens of Concord contract doesn't have anything in it that manages the supply of the tokens created. This means that all tokens effectively have an uncapped supply and function like ERC20-like tokens. From looking at the collection on OpenSea, it's hard to tell if this is what they intended.

So there's no way for a supply limit to be enforced by the contract. If the creators say they'll only ever mint 10 of a particular token, there's nothing stopping them from changing their mind and minting more.

Selling and Withdrawing

The creators have also left out any logic in the contract that would allow them to sell or distribute tokens directly from the contract. There also isn't a way to withdraw any Eth stored in the contract. They have implemented royalties on sales though.

The community has a marketplace on OpenSea. So I'm assuming what they do is create a new token, mint it to the creator's address, then list it on this marketplace. This is a clever way to avoid having to write any sales logic into the contract. It also avoids having their contract be a honeypot, because it should (in theory) never hold any Eth.

Minting Tokens

The Tokens of Concord contract has four (4) different methods for minting tokens. It's common for ERC1155 contracts to include multiple minting methods (two, at least). Based on how they're using these, I don't see a way to have fewer methods.

The four minting methods (what they call bestowing) are:

  1. bestowTokensUponCreator(uint256 _token, uint256 _quantity) external onlyCreator;
  2. bestowTokensUponCreatorMany(uint256[] memory _tokens, uint256[] memory _amounts) external onlyCreator;
  3. bestowTokens(address[] memory _to, uint256 _token, uint256 _quantity) external onlyOrdainedOrCreator;
  4. bestowTokensMany(address[] memory _to, uint256[] memory _tokens, uint256[] memory _amounts) external onlyOrdainedOrCreator;

If you look at these methods closely, there are really two methods, each with two versions. They just aren't actually overloading them. They could get away with two versions each of the bestowTokens() and bestowTokensMany() methods where one version accepts a list of addresses, while the other does not and mints the tokens to the contract's creator. The signatures for the bestowTokens() method would then become:

  1. bestowTokens(uint256 _token, uint256 _quantity) external onlyCreator;
  2. bestowTokens(address[] memory _to, uint256 _token, uint256 _quantity) external onlyOrdainedOrCreator;

All I've basically done here is change the name of bestowTokensCreator(), the original method, to bestowTokens().

Conclusion

The impressive thing about the Tokens of Concord contract is just how simple it is. As stated, there's no logic for selling and withdrawing, nor is there logic to enforce a tokens supply. This is a lot of logic to leave out of the contract. But it works for how they're using the tokens.

The way new tokens are created gives the creators a lot of flexibility and is an implementation I haven't seen in other contracts before.

SolidityProgramming

0xZakk

Zakk is a software engineer and writer. In 2021, he co-founded CabinDAO.