Solidity Token Standards Compared
Token standards differ in whether units are identical, unique, or grouped by type. ERC20, ERC721, and ERC1155 each fit a different kind of thing.
ERC20 counts identical amounts
An ERC20 transfer moves 10 identical units. The receiver balance grows by 10 and the units are interchangeable.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract SimpleCoin {
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
event Transfer(address indexed from, address indexed to, uint256 value);
constructor() {
totalSupply = 1000;
balanceOf[msg.sender] = 1000;
}
function transfer(address to, uint256 amount) public returns (bool) {
require(to != address(0), "zero receiver");
require(balanceOf[msg.sender] >= amount, "not enough balance");
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
}ERC721 tracks one owner per id
An ERC721 mint gives id 1 exactly one owner. No amount is counted, because the id itself is the item.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract SimpleItem {
address public owner;
mapping(uint256 => address) private owners;
mapping(address => uint256) public balanceOf;
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
constructor() {
owner = msg.sender;
}
function mint(address to, uint256 tokenId) public {
require(msg.sender == owner, "only owner mints");
require(to != address(0), "zero receiver");
require(owners[tokenId] == address(0), "already minted");
owners[tokenId] = to;
balanceOf[to] += 1;
emit Transfer(address(0), to, tokenId);
}
function ownerOf(uint256 tokenId) public view returns (address) {
require(owners[tokenId] != address(0), "no such token");
return owners[tokenId];
}
function transferFrom(address from, address to, uint256 tokenId) public {
require(owners[tokenId] == msg.sender, "only owner moves it");
require(from == owners[tokenId], "wrong sender");
require(to != address(0), "zero receiver");
owners[tokenId] = to;
balanceOf[from] -= 1;
balanceOf[to] += 1;
emit Transfer(from, to, tokenId);
}
}ERC1155 holds many types in one contract
One ERC1155 contract mints 100 of id 1 and 5 of id 2. One holder carries both types at once.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract MultiToken {
address public owner;
mapping(uint256 => mapping(address => uint256)) private balances;
event TransferBatch(address operator, address from, address to, uint256[] ids, uint256[] amounts);
constructor() {
owner = msg.sender;
}
function mint(address to, uint256 id, uint256 amount) public {
require(msg.sender == owner, "only owner mints");
require(to != address(0), "zero receiver");
balances[id][to] += amount;
}
function balanceOf(address account, uint256 id) public view returns (uint256) {
return balances[id][account];
}
function balanceOfBatch(address[] memory accounts, uint256[] memory ids) public view returns (uint256[] memory) {
require(accounts.length == ids.length, "length mismatch");
uint256[] memory result = new uint256[](accounts.length);
for (uint256 i = 0; i < accounts.length; i++) {
result[i] = balances[ids[i]][accounts[i]];
}
return result;
}
function transferBatch(address to, uint256[] memory ids, uint256[] memory amounts) public {
require(to != address(0), "zero receiver");
require(ids.length == amounts.length, "length mismatch");
for (uint256 i = 0; i < ids.length; i++) {
require(balances[ids[i]][msg.sender] >= amounts[i], "not enough balance");
balances[ids[i]][msg.sender] -= amounts[i];
balances[ids[i]][to] += amounts[i];
}
emit TransferBatch(msg.sender, msg.sender, to, ids, amounts);
}
}Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract MiniCoin {
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
constructor() {
totalSupply = 1000;
balanceOf[msg.sender] = 1000;
}
function transfer(address to, uint256 amount) public returns (bool) {
require(balanceOf[msg.sender] >= amount, "not enough balance");
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
return true;
}
}
contract MiniItem {
mapping(uint256 => address) private owners;
function mint(address to, uint256 tokenId) public {
require(owners[tokenId] == address(0), "already minted");
owners[tokenId] = to;
}
function ownerOf(uint256 tokenId) public view returns (address) {
require(owners[tokenId] != address(0), "no such token");
return owners[tokenId];
}
}
contract MiniPack {
mapping(uint256 => mapping(address => uint256)) private balances;
function mint(address to, uint256 id, uint256 amount) public {
balances[id][to] += amount;
}
function balanceOf(address account, uint256 id) public view returns (uint256) {
return balances[id][account];
}
}Key points
- ERC20: identical units counted as amounts in a balance.
- ERC721: unique ids with exactly one owner per id.
- ERC1155: many token types in one contract, counted as amounts per id.
- Fungible: interchangeable, so one unit can replace another without any difference.
- Choosing: identical units fit a currency, unique items fit a collectible, and grouped types fit a game inventory.
In this example, three small contracts sit side by side. The coin counts 10 identical units, the item gives id 1 one owner, and the pack holds 100 of one type and 5 of another.
Note
the three standards share the transfer idea, but they answer a different question about what a unit is.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Remix IDE
Exercise
Which standard fits a game with swords and shields in one contract?