avatar
原创2021/12/18

ERC 代币标准详解

AI 总结

全面讲解以太坊 ERC 代币标准体系,深入解析 ERC-20 同质化代币、ERC-721 非同质化代币(NFT)、ERC-1155 多代币标准的接口定义与实现细节,对比各标准的适用场景与设计差异,帮助开发者快速掌握以太坊代币开发的核心规范。

ERC 全称是 Ethereum Request for Comment,即以太坊征求意见协议,在以太坊网络中一方面为开发者提供了技术指导,另一方面也对以太坊网络的发展提供了建议。

ERC 标准是由以太坊开发人员为以太坊社区编写的。为了给以太坊平台创建标准,提交了以太坊改进建议( EIP )——包括协议规范和合同标准。如果 EIP 被委员会批准并最终确定,它就成为了一个 ERC。

EIP 完整列表可以查看

ERC 中常用的代币标准包括了

  • ERC20: 同质化代币的标准
  • ERC721:非同质化代币的标准
  • ERC1155:多代币标准

ERC20

ERC20 提供了一个同质化代币(Fungible Token, FT)的标准,换句话说,即每一枚代币(在类型和价值上)完全相同。例如,一个 ERC-20 代币就像以太币一样,意味着一个代币会并永远会与其他代币一样。

主要功能包括了

  • 转账:将代币从一个帐户转到另一个帐户
  • 查询余额:获取帐户的当前代币余额
  • 查询总量:获取网络上可用代币的总供应量
  • 代币授权:批准一个帐户中一定的代币金额由第三方帐户使用

标准实现的方法有

以下代码节选自 @openzeppelin/contracts: 4.7.0

name

function name() public view virtual override returns (string memory) {
  return _name;
}

获取代币名称

symbol

function symbol() public view virtual override returns (string memory) {
  return _symbol;
}

获取代币符号

decimals

function decimals() public view virtual override returns (uint8) {
  return 18;
}

获取代币的精度。通常为 18。例如数量为 10001000, 则 10001000 除以 101810^{18}得到的值才是真正的代币数量

totalSupply

function totalSupply() public view virtual override returns (uint256) {
  return __totalSupply;
}

获取代币的总供应量

balanceOf

function balanceOf(address account) public view virtual override returns (uint256) {
  return _balances[account];
}

返回 account 账户的代币余额

transfer

function transfer(address to, uint256 amount) public virtual override returns (bool) {
    address owner = _msgSender();
    _transfer(owner, to, amount);
    return true;
}
// 获取调用的地址
function _msgSender() internal view virtual returns (address) {
    return msg.sender;
}
 
function _transfer(
    address from,
    address to,
    uint256 amount
) internal virtual {
    require(from != address(0), "ERC20: transfer from the zero address");
    require(to != address(0), "ERC20: transfer to the zero address");
 
    _beforeTokenTransfer(from, to, amount);
 
    // 获取 from 地址的余额
    uint256 fromBalance = _balances[from];
    // 检查余额数量不小于转账的数量
    require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
 
    // 将 from 的余额减少
    unchecked {
        _balances[from] = fromBalance - amount;
    }
    // 将 to 的余额增加
    _balances[to] += amount;
 
    emit Transfer(from, to, amount);
 
    _afterTokenTransfer(from, to, amount);
}

转账,将调用者指定数量的 token 转到 _to 地址并必须触发 Transfer 事件。如果调用者的账户余额没有足够的代币可以支付 amount 数量的代币时,该函数应该抛出异常并回滚。

transferFrom

function transferFrom(
    address from,
    address to,
    uint256 amount
) public virtual override returns (bool) {
    // 获取消息的发送者
    address spender = _msgSender();
    // 获取并更新 spender 可操作 from 的代币数量
    _spendAllowance(from, spender, amount);
    // 转账
    _transfer(from, to, amount);
    return true;
}
 
function _spendAllowance(
    address owner,
    address spender,
    uint256 amount
) internal virtual {
    // 获取 spender 可以从 owner 中操作的代币数量
    uint256 currentAllowance = allowance(owner, spender);
    if (currentAllowance != type(uint256).max) {
        // 要求可操作的数量大于 amount
        require(currentAllowance >= amount, "ERC20: insufficient allowance");
        // 更新可操作的数量
        unchecked {
            _approve(owner, spender, currentAllowance - amount);
        }
    }
}

将指定数量的代币从地址 from 转移到地址 to,并且必须触发Transfer事件。进行此项操作的前提是消息的发送者具有操作 from 代币的权利,才能将代币从 from 转移到 to。即转账操作前需要进行 approve 授权操作,将 from 地址指定数量的代币的操作权交给 消息的发送者。并且 approve 操作需要由 from 发出。具体看下方代码

approve

function approve(address spender, uint256 amount) public virtual override returns (bool) {
    address owner = _msgSender();
    _approve(owner, spender, amount);
    return true;
}
function _approve(
    address owner,
    address spender,
    uint256 amount
) internal virtual {
    // 地址校验
    require(owner != address(0), "ERC20: approve from the zero address");
    require(spender != address(0), "ERC20: approve to the zero address");
 
    // 存储 spender 账户可以对 owner 账户的 amount 数量的代币进行操作
    _allowances[owner][spender] = amount;
    emit Approval(owner, spender, amount);
}

该函数的调用者账户地址将 amount 数量的代币授权给 spender,或者更新授权的代币数量,即 spender 可以多次提取调用者账户地址的代币,总数量不超过 amount,每次提取的代币数量也不能超过调用者账户地址的代币余额,同时提取后会更新 amount 的值。如果在提取过程中,提取的代币数量超过了代币余额不足,该函数会抛出异常并回滚。

allowance

function allowance(address owner, address spender) public view virtual override returns (uint256) {
    return _allowances[owner][spender];
}

返回账户地址 spender 还可以从账户地址 owner 中提取的代币数量。

ERC721

不同于 ERC20 中所有的代币都具有相同的价值和属性。ERC721 为非同质化代币(NFT) 标准,即每一枚代币都是独一无二的。例如收藏品、音乐等。

为了实现这种独一无二。会有一个uint256类型的变量 tokenId 来表示每一枚 代币, 通常 tokenId 是自增的。并随着 mint 的数量增加

以下代码节选自 @openzeppelin/contracts 4.7.0

变量:

// 代币名称
string private _name;
 
// 代币符号
string private _symbol;
 
// tokenID 对应的 拥有者地址
mapping(uint256 => address) private _owners;
 
// tokenID 对应的 metadata json 文件的地址
mapping(uint256 => string) private _tokenURIs;
 
// 拥有者地址所有的 nft 数量
mapping(address => uint256) private _balances;
 
// tokenID 对应授权的地址 - 单个授权
mapping(uint256 => address) private _tokenApprovals;
 
// _operatorApprovals[owner][operator] = true or false
// 存储是否将 owner 所持有的全部 nft 的操作权限交给 operator - 全部授权
mapping(address => mapping(address => bool)) private _operatorApprovals;

方法:

balanceOf

function balanceOf(address owner) public view virtual override returns (uint256) {
    require(owner != address(0), "ERC721: address zero is not a valid owner");
    return _balances[owner];
}

查询余额,返回地址 owner 拥有的 nft 的数量,若没有,则返回 0。

ownerOf

function ownerOf(uint256 tokenId) public view virtual override returns (address) {
    address owner = _owners[tokenId];
    require(owner != address(0), "ERC721: invalid token ID");
    return owner;
}

获取 tokenId 对应的 nft 的持有者地址

tokenURI

function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
    _requireMinted(tokenId);
 
    string memory baseURI = _baseURI();
    return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
}
function _baseURI() internal view virtual returns (string memory) {
    return "";
}
// 要求 tokenId 对应的 nft 存在拥有者 即该 nft 已经被铸造了
function _requireMinted(uint256 tokenId) internal view virtual {
    require(_exists(tokenId), "ERC721: invalid token ID");
}
// 查询 tokenId 是否存在拥有者
function _exists(uint256 tokenId) internal view virtual returns (bool) {
    return _owners[tokenId] != address(0);
}
 

获取 tokenId 对应的 tokenURI, tokenURI 是一个描述了该 NFT metadata的地址。

setTokenURI

function _setTokenURI(uint256 tokenId, string memory _tokenURI) internal virtual {
    require(_exists(tokenId), "ERC721URIStorage: URI set of nonexistent token");
    _tokenURIs[tokenId] = _tokenURI;
}

设置 tokenId 对应的 tokenURI

approve

function approve(address to, uint256 tokenId) public virtual override {
    // 获取 tokenId 对应的持有者
    address owner = ERC721.ownerOf(tokenId);
    require(to != owner, "ERC721: approval to current owner");
 
    require(
        _msgSender() == owner || isApprovedForAll(owner, _msgSender()),
        "ERC721: approve caller is not token owner nor approved for all"
    );
 
    _approve(to, tokenId);
}
 
function _approve(address to, uint256 tokenId) internal virtual {
    // 存储授权信息
    _tokenApprovals[tokenId] = to;
    emit Approval(ERC721.ownerOf(tokenId), to, tokenId);
}

授权 tokenId 的 token 给 to 地址, 并且触发 Approve事件

getApproved

function getApproved(uint256 tokenId) public view virtual override returns (address) {
    _requireMinted(tokenId);
 
    return _tokenApprovals[tokenId];
}

查询单个 NFT 的授权地址

setApprovalForAll

function setApprovalForAll(address operator, bool approved) public virtual override {
    _setApprovalForAll(_msgSender(), operator, approved);
}
 
function _setApprovalForAll(
    address owner,
    address operator,
    bool approved
) internal virtual {
    require(owner != operator, "ERC721: approve to caller");
    _operatorApprovals[owner][operator] = approved;
    emit ApprovalForAll(owner, operator, approved);
}

将调用者的所有 NFT 资产授权给 operator 地址来管理,或者取消授权,并且触发 ApprovalForAll 事件

isApprovedForAll

function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) {
    return _operatorApprovals[owner][operator];
}

查询 operator 是否具有 owner 所持有的所有 NFT 的操作权限

transferFrom

function transferFrom(
    address from,
    address to,
    uint256 tokenId
) public virtual override {
    //solhint-disable-next-line max-line-length
    require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner nor approved");
 
    _transfer(from, to, tokenId);
}
 
// 判断是否具有操作权限
function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) {
    address owner = ERC721.ownerOf(tokenId);
    return (spender == owner || isApprovedForAll(owner, spender) || getApproved(tokenId) == spender);
}
 

将 tokenId 的 NFT 的所有权由地址 from 转移给地址 to,并触发Transfer事件

safeTransferFrom

function safeTransferFrom(
    address from,
    address to,
    uint256 tokenId
) public virtual override {
    safeTransferFrom(from, to, tokenId, "");
}
 
function safeTransferFrom(
    address from,
    address to,
    uint256 tokenId,
    bytes memory data
) public virtual override {
    // 确保消息发送者 具有 tokenId 的操作权限 - 拥有者 或者 已授权。
    require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner nor approved");
    // 调用转账方法
    _safeTransfer(from, to, tokenId, data);
}
 
function _safeTransfer(
    address from,
    address to,
    uint256 tokenId,
    bytes memory data
) internal virtual {
    _transfer(from, to, tokenId);
    require(_checkOnERC721Received(from, to, tokenId, data), "ERC721: transfer to non ERC721Receiver implementer");
}
 
function _checkOnERC721Received(
    address from,
    address to,
    uint256 tokenId,
    bytes memory data
) private returns (bool) {
    // to 地址是合约地址
    if (to.isContract()) {
        try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, data) returns (bytes4 retval) {
            return retval == IERC721Receiver.onERC721Received.selector;
        } catch (bytes memory reason) {
            if (reason.length == 0) {
                revert("ERC721: transfer to non ERC721Receiver implementer");
            } else {
                /// @solidity memory-safe-assembly
                assembly {
                    revert(add(32, reason), mload(reason))
                }
            }
        }
    } else {
        return true;
    }
}
 
// 转账方法
function _transfer(
    address from,
    address to,
    uint256 tokenId
) internal virtual {
    require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner");
    require(to != address(0), "ERC721: transfer to the zero address");
 
    _beforeTokenTransfer(from, to, tokenId);
 
    // Clear approvals from the previous owner
    _approve(address(0), tokenId);
 
    _balances[from] -= 1;
    _balances[to] += 1;
    _owners[tokenId] = to;
 
    emit Transfer(from, to, tokenId);
 
    _afterTokenTransfer(from, to, tokenId);
}
 

安全地转移 NFT 所有权,即将 tokenId 对应的 NFT 的所有权由地址 from 转移给地址 to,并触发 Transfer事件

mint

function _mint(address to, uint256 tokenId) internal virtual {
    require(to != address(0), "ERC721: mint to the zero address");
    require(!_exists(tokenId), "ERC721: token already minted");
 
    _beforeTokenTransfer(address(0), to, tokenId);
 
    // 持有人 to 的持有数量 + 1
    _balances[to] += 1;
    // 添加 tokenId 对应的地址 to
    _owners[tokenId] = to;
 
    emit Transfer(address(0), to, tokenId);
 
    _afterTokenTransfer(address(0), to, tokenId);
}
 
 

铸造 NFT

ERC1155

上面说的 ERC20 和 ERC721标准,每个合约都对应着一个独立的代币。但 ERC1155 却不同,一个合约可以对应多种代币,包括同质化代币和非同质化代币。例如 阿迪达斯 NFT

为了实现单个合约中包含多种代币,会用 id 来标识每一种代币。并且每种代币都有一个 uri来存储其 metadata

以下代码节选自 @openzeppelin/contracts 4.7.0

变量

// _balances[id][owner]
// 存储 id 对应的 token, 其持有者拥有的数量
mapping(uint256 => mapping(address => uint256)) private _balances;
 
// _operatorApprovals[owner][operator] = true or false
// 存储 owner 将代币操作权限授权给 operator 或 取消授权
mapping(address => mapping(address => bool)) private _operatorApprovals;

mint

function _mint(
    address to,
    uint256 id,
    uint256 amount,
    bytes memory data
) internal virtual {
    require(to != address(0), "ERC1155: mint to the zero address");
 
    address operator = _msgSender();
    uint256[] memory ids = _asSingletonArray(id);
    uint256[] memory amounts = _asSingletonArray(amount);
 
    _beforeTokenTransfer(operator, address(0), to, ids, amounts, data);
 
    _balances[id][to] += amount;
    emit TransferSingle(operator, address(0), to, id, amount);
 
    _afterTokenTransfer(operator, address(0), to, ids, amounts, data);
 
    _doSafeTransferAcceptanceCheck(operator, address(0), to, id, amount, data);
}
 
// 铸造 id 为 1, 持有者为 消息发送者, 数量为 10**18 的代币
_mint(msg.sender, 1, 10**18, '');
 

单币种铸造

mintBatch

function _mintBatch(
    address to,
    uint256[] memory ids,
    uint256[] memory amounts,
    bytes memory data
) internal virtual {
    require(to != address(0), "ERC1155: mint to the zero address");
    require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch");
 
    address operator = _msgSender();
 
    _beforeTokenTransfer(operator, address(0), to, ids, amounts, data);
 
    for (uint256 i = 0; i < ids.length; i++) {
        _balances[ids[i]][to] += amounts[i];
    }
 
    emit TransferBatch(operator, address(0), to, ids, amounts);
 
    _afterTokenTransfer(operator, address(0), to, ids, amounts, data);
 
    _doSafeBatchTransferAcceptanceCheck(operator, address(0), to, ids, amounts, data);
}

多币种铸造

uri

function uri(uint256 id) public view virtual override returns (string memory) {
    string memory baseURI = _baseURI();
    return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, id.toString())) : "";
}

返回 id 对应的 token 存储的 uri,类似 ERC721的 tokenURI

balanceOf

function balanceOf(address account, uint256 id) public view virtual override returns (uint256) {
    require(account != address(0), "ERC1155: address zero is not a valid owner");
    return _balances[id][account];
}

查询余额

balanceOfBatch

function balanceOfBatch(address[] memory accounts, uint256[] memory ids)
    public
    view
    virtual
    override
    returns (uint256[] memory)
{
    require(accounts.length == ids.length, "ERC1155: accounts and ids length mismatch");
 
    uint256[] memory batchBalances = new uint256[](accounts.length);
 
    for (uint256 i = 0; i < accounts.length; ++i) {
        batchBalances[i] = balanceOf(accounts[i], ids[i]);
    }
 
    return batchBalances;
}

查询多账户持有的多 token 的余额

setApprovalForAll

function setApprovalForAll(address operator, bool approved) public virtual override {
    _setApprovalForAll(_msgSender(), operator, approved);
}
function _setApprovalForAll(
    address owner,
    address operator,
    bool approved
) internal virtual {
    require(owner != operator, "ERC1155: setting approval status for self");
    _operatorApprovals[owner][operator] = approved;
    emit ApprovalForAll(owner, operator, approved);
}

设置授权

isApprovedForAll

function isApprovedForAll(address account, address operator) public view virtual override returns (bool) {
    return _operatorApprovals[account][operator];
}

授权查询

safeTransferFrom

function safeTransferFrom(
    address from,
    address to,
    uint256 id,
    uint256 amount,
    bytes memory data
) public virtual override {
    // 要求 from 拥有代币所有权
    require(
        from == _msgSender() || isApprovedForAll(from, _msgSender()),
        "ERC1155: caller is not token owner nor approved"
    );
    _safeTransferFrom(from, to, id, amount, data);
}
 
function _safeTransferFrom(
    address from,
    address to,
    uint256 id,
    uint256 amount,
    bytes memory data
) internal virtual {
    require(to != address(0), "ERC1155: transfer to the zero address");
 
    address operator = _msgSender();
    uint256[] memory ids = _asSingletonArray(id);
    uint256[] memory amounts = _asSingletonArray(amount);
 
    _beforeTokenTransfer(operator, from, to, ids, amounts, data);
 
    // 获取 from 地址中 id 对应 token 的余额
    uint256 fromBalance = _balances[id][from];
    require(fromBalance >= amount, "ERC1155: insufficient balance for transfer");
    // 减少 from 地址中的余额
    unchecked {
        _balances[id][from] = fromBalance - amount;
    }
    // 增加 to 地址余额
    _balances[id][to] += amount;
 
    emit TransferSingle(operator, from, to, id, amount);
 
    _afterTokenTransfer(operator, from, to, ids, amounts, data);
 
    _doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data);
}

单币种安全转账,触发TransferSingle事件

safeBatchTransferFrom

function safeBatchTransferFrom(
    address from,
    address to,
    uint256[] memory ids,
    uint256[] memory amounts,
    bytes memory data
) public virtual override {
    require(
        from == _msgSender() || isApprovedForAll(from, _msgSender()),
        "ERC1155: caller is not token owner nor approved"
    );
    _safeBatchTransferFrom(from, to, ids, amounts, data);
}
 
function _safeBatchTransferFrom(
    address from,
    address to,
    uint256[] memory ids,
    uint256[] memory amounts,
    bytes memory data
) internal virtual {
    require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch");
    require(to != address(0), "ERC1155: transfer to the zero address");
 
    address operator = _msgSender();
 
    _beforeTokenTransfer(operator, from, to, ids, amounts, data);
 
    // 循环转账
    for (uint256 i = 0; i < ids.length; ++i) {
        uint256 id = ids[i];
        uint256 amount = amounts[i];
 
        uint256 fromBalance = _balances[id][from];
        require(fromBalance >= amount, "ERC1155: insufficient balance for transfer");
        unchecked {
            _balances[id][from] = fromBalance - amount;
        }
        _balances[id][to] += amount;
    }
 
    emit TransferBatch(operator, from, to, ids, amounts);
 
    _afterTokenTransfer(operator, from, to, ids, amounts, data);
 
    _doSafeBatchTransferAcceptanceCheck(operator, from, to, ids, amounts, data);
}

多币种安全转账,触发TransferBatch事件

点击播放