TL;DR : We’ve designed and implemented a token contract which allows us to transfer bundles of different types of tokens for 80% to 90% less gas than ERC-20 and ERC-721. This is accomplished by “packing” different token balances together, saving a substantial amount of storage space on the blockchain. One trade-off is that the maximum user balance for each token type is lower, however this can be adjusted to be made acceptable for many projects. Multi-class token contracts are particularly interesting for the gaming world, where transferring bundles of items (sets, decks, etc.) is common. — PhABCD @ Twitter
Ethereum Token Standards
In the last six months, we have seen an explosion of new token standards in the Ethereum ecosystem, which is a great thing. In the end, “adoption is key” as Matt Lockyer said in his blog post on token standards. Just as with websites or cryptocurrencies, over time, the ones that are truly important will be adopted while insignificant ones will be ignored. This acceleration in Ethereum token design proposals can be observed in the exhaustive list of Ethereum Token Standard proposals I recently created. It was therefore not surprising for us to realize the token interface we were working on for our first game at Horizon Games, SkyWeaver, was being contemplated by others as well. Before going into the details that lead to our more efficient token contract design, let’s review the two most popular token standards today and what they are good for: ERC-20 and ERC-721. For those familiar with these, feel free to skip the next two sections.
ERC-20 Standard: Fungible Tokens
When we think of money, we usually think of all dollars as identical. You don’t open your wallet (if you still have one) and choose which $20 bill you want to give the merchant. To both of you, all valid $20 bills are the same. This is generally speaking what we mean by “fungible”, where each object or element is interchangeable with others of the same “type”. In the realm of cryptocurrencies, Ether and Bitcoin are generally said to be fungible, where 1 Ether is equal to any other 1 Ether. When people create tokens on the Ethereum network, via token sales for example, they usually create fungible tokens and usually these tokens are said to comply with the ERC-20 Token Standard.
Standardization is important for developers as it makes our life much easier. Imagine if each laptop had a different keyboard layout and you couldn’t change it. It would be quite painful indeed and hence why standardizing some interface (e.g. keyboard) is important for user experience. Currently, with ERC-20 token standard, if Bob wants to transfer 100 ZRX or ANT tokens to Alice (both ERC-20 compliant), he simply needs to execute the
transfer(Alice, 100) function in both cases. If these tokens did not use the same standard, then perhaps Bob would need to call
transfer(Alice, 100) in the case of transferring ZRXs, but
sendTokens(Bob, Alice, 100) in the case of transferring ANTs. This would be kind of confusing, since Bob and all the other developers would always need to check how each token works and write code for each different implementation. Standards are important as they simplify life for everyone and allow people to easily integrate in their own system tools developed by others. On the other hand, a given standard might not satisfy the needs of all applications, hence these several new token standards being proposed.
ERC-721 Standard: Non-Fungible Tokens
Fungibility is not always desired, however. Each house and car is unique and therefore should be considered “non-fungible”. Similarly in the digital world, it is possible to have some digital objects that are unique, hence the recently proposed ERC-721 Non Fungible Tokens (NFT) Standard. NFTs were popularized by CryptoKitties, where each cat is unique and therefore non-fungible. Although most projects currently using or intending to use non-fungible tokens are video games, it is important to note that gaming is not the only area where NFTs can be useful. You can read more about the possible applications of NFTs in this non-technical blog post by William Entriken.
NFT functionality is slightly different than fungible tokens, necessitating a different standard: ERC-721. Indeed, instead of sending X number of tokens, you need to specify which token you want to send. In the case of ERC-721 tokens, when Bob calls the function
transfer(Alice, 100), Bob is not sending 100 tokens, but sending the token
#100. The actual implementation of the transfer function is slightly different in the new ERC-721 standard, but the essence is the same.
Contracts following the ERC-20 standard keep track of a single class of divisible token and contracts following the ERC-721 standard keep track of many indivisible tokens. However, it can be useful to keep track of many different types of fungible and non-fungible tokens within the same contract, both for simplicity and efficiency, as explained in the next section.
Multi-Class Token Contracts
At horizongames.net, we make games for players who want to have fun without spending much (or at all), and so minimizing on-chain costs is critical. We therefore wanted to have a single contract that keeps track of all our tokens so that transferring many different tokens simultaneously would be efficient. In the case of our flagship trading card game SkyWeaver, users might want to sell their decks on a market, buy bundles of cards or claim a many rewards at once. All of these scenarios involve multiple token types (e.g. cards) being transferred. This means that whether we choose our virtual goods to be fungible or not, we would still need a slightly different interface than ERC-20 and ERC-721. Currently, we found five proposed token standards that had similar intentions
- ERC-998: Composable Non-Fungible Token Standard
- ERC-888: MultiDimensional Token Standard
- ERC-1155: Multi Token Standard
- ERC-1178: Multi-Class Token Standard
- ERC-1203: Multi-Class Token Standard
The latter four standards (888, 1155, 1178, 1203) are practically identical in their intentions and reflect most our implementation. For simplicity, we will refer to these proposed token standards as Multi-Class Token (MCT) contracts. The four MCT proposals can be summarized as having a mapping variable such that
balances[Bob][tokenA] is the number of
tokenA Bob currently owns. With a standard ERC-20 token contract A,
balances[BobAddress] returns Bob’s
tokenA balance since there is only one token class tracked on this contract. With a standard ERC-721 contract B,
balances[BobAddress] would return a list of all NFT tokens Bob currently owns on contract B. MCTs are obviously incompatible with both ERC-20 and ERC-721 standards giving the additional balance mapping. Yet, despite this lack of compatibility, we argue that this new token interface can lead to significant efficiency gains under the right circumstances. Indeed, we will show that we can transfer a large amount of tokens from different classes more efficiently by “packing” the balance of various tokens together.
For the sake of simplicity, in the rest of this post, we will assume that all token classes/types within an MCT contract are fungible tokens. MCTs could implement various token standards within the same contract, but this beyond the scope of this post.
Balance Packing Transfer Efficiency
One of the advantages of having multiple fungible token classes within the same contract is that we can more efficiently transfer different token classes in a single transaction. Most of the efficiency gains can be attributed to the ability to “pack” balances together within a share “storage slot” (256 bits), saving some storage cost. Storing data on the blockchain is one of the most expensive operations. Occupying less storage space can significantly reduce costs, and this might be even truer in the near future.
In our MCT implementation, we store 16 token balances within a single
uint256 (a 256 bits unsigned integer) where each token balance occupies 16 bits. You can then keep track of as many token balances as you want by having many
uint256 where balances are packed in. However, because of this packing, a given token balance can’t exceed 2¹⁶, or
65,546. This can be a reasonable limit for many applications. For instance, under what circumstances would a single player need to be able to own more than 2¹⁶ swords? The one instance where this limit could become a problem is for a custodial exchange or proxy contract holding the funds on the behalf of many users, however, it is unclear whether or not this will be a problem in practice. Nevertheless, the amount of packing can easily be modified. For instance, packing 8 token balances in an
uint256 instead of 16 increases the balance limit from
4,294,967,296. One can also do the opposite and increase the amount of token balances packed per
uint256, reducing the balance limit but increasing storage efficiency.
The efficiency gains come from the fact that you can transfer 16 token classes with a single storage update, which is much less costly. However, the balance of these token classes needs to be packed within the same
uint256 in order to benefit from this efficiency gain. Indeed, if you transfer 16 token classes where their balance is stored in different
uint256, then you will have 16 storage updates, like a regular ERC-20 transfer. This means that token classes that are more likely to be transferred together should have IDs that are close to each others (e.g. 23, 24, 25, etc.) as well, increasing the likelihood of having many token classes within the same
These compromises and assumptions might be inappropriate for some applications, but we believe others will find this useful.
Comparing Transfer Costs Between Standards
We transferred a single token from 100 different classes for ERC-20, ERC-721 and MCTs. In the case of ERC-721, each token type is a different NFT, within the same contract. In the case of ERC-20, each token type is a different ERC-20 token, stored in different contracts. For both ERC-721 and ERC-20, we also wrote a wrapper contract that transfers on behalf of users, saving on the base transaction cost. The cost here does not include the
approval call cost that such wrapping contracts would necessitate.
Transferring 100 ERC-721 tokens in different transaction calls :
- Total gas cost : 5,113,036
- Gas Cost Per Transfer : 51,130
Transferring 100 ERC-721 tokens with a wrapper contract :
- Total gas cost : 2,463,700
- Gas Cost Per Transfer : 24,637
Transferring 100 ERC-20 tokens in different transaction calls :
- Total gas cost : 5,153,300
- Gas Cost Per Transfer : 51,533
Transferring 100 ERC-20 tokens with wrapper contract :
- Total gas cost : 3,373,822
- Gas Cost Per Transfer : 33,738
Transferring 100 fungible tokens from MCT contract without balance packing :
- Total gas cost : 2,788,039
- Gas Cost Per Transfer : 27,880
Transferring 100 fungible tokens from MCT contract with balance packing
- Total gas cost : 467,173
- Gas Cost Per Transfer : 4,671
Note that the balance packing calculation assumes tokens have close by IDs, hence the result above is a cost lower bound. The balance packing can offer significant efficiency gains under the right circumstances, up to 10x savings compared to regular transfers and 5x–7x when using wrapper contracts for batch transfers. In addition, I am fairly convinced additional significant optimization are possible without adding much complexity.
Computational efficiency is not the only efficiency that matters and better price discovery efficiency is an advantage fungible tokens have over non-fungible ones. Stay tuned for a post on this topic.
In this post, we discussed the concept of Multi-Class Token contracts and how they are distinct from now popular ERC-20 and ERC-721 token standards. Via balance packing, we also showed how having many fungible token classes within the same contract can lead to significant efficiency gains when transferring multiple token classes at once. We hope that this post will lead to more research in MCTs and token transfer efficiency in general.
You can find our MCT implementation (an ERC1155 contract) with balance packing here: https://github.com/arcadeum/multi-token-standard