# ERC20 & ERC721 & 1155代码简析

By [Raver|sudden](https://paragraph.com/@suden) · 2022-03-08

---

简单记录下这三种ERC协议的要点。源码是openzepplin的实现。

ERC20
-----

IErc20 接口定义的方法共有以下几个：

    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
    
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
    

通过方法我们可以看出，ERC20涉及：获取总量、余额、授权额度 这几个变量。

以下是ERC20 openzeppelin的变量，不出所料。

        mapping (address => uint256) private _balances;
    
        mapping (address => mapping (address => uint256)) private _allowances;
    
        uint256 private _totalSupply;
    

而 name，symbol，decimal来自ERC20Detailed的实现。同时构造函数也定义在这个合约里。组合使用时，我们一般这样定义：

    contract ERC20XXXX is ERC20, ERC20Detailed {...}
    

可以看到solidity是支持多继承的，这与Java，python等语言不同。其实Solidity的基础，是把父合约的代码完全拷贝过来的，变量slot也是按从父到子合约排布。

使用这种方式应该是为了使用者可以灵活组合。

transfer等方法都比较简单，就是使用SafeMath包装的uint256做一下sub和add，然后触发一下event。不过其实 SafeMath 在 solidity0.8.0 之后可以省略了，我这边源码比较老。

还有一点值得注意的是，transfer，approve等方法中，都使用了下划线+方法名的 internal 方法来实现具体逻辑。这样的好处应该是提取出业务逻辑，方便继承 ERC20 的子方法重写时，调用父合约的下划线方法，复用相同逻辑。

另外说一点就是，transfer 和 transferFrom 的返回值都是bool，这点是我们写合约时，通过call调用，经常用来判断是否转账成功的。如果我们修改实现，需要注意返回值。

ERC721
------

Erc721是NFT协议，官方实现中，IERC721继承了IERC165接口，用来实现对to地址为合约地址时，是否支持接受NFT的校验，从而防止误转到黑洞。

    interface IERC721 is IERC165 {
        event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
        event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
        event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);
    
        function balanceOf(address _owner) external view returns (uint256);
        function ownerOf(uint256 _tokenId) external view returns (address);
        function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;
        function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
        function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
        function approve(address _approved, uint256 _tokenId) external payable;
        function setApprovalForAll(address _operator, bool _approved) external;
        function getApproved(uint256 _tokenId) external view returns (address);
        function isApprovedForAll(address _owner, address _operator) external view returns (bool);
    }
    

可以看到主要支持 获取地址余额，获取id对应owner，转账，授权，全部授权等操作。涉及的变量如下：

        string private _name;
        string private _symbol;
        mapping (uint256 => address) private _owners;
        mapping (uint256 => address) private _tokenApprovals;
        mapping (address => Counters.Counter) private _balances;
        mapping (address => mapping (address => bool)) private _operatorApprovals;
    

这里的\_tokenApprovals存储单个tokenId 的 approve ，而 \_operatorApprovals 存储的是某个 owner 对另一个地址的全权授权。这意味着，被授权者（operator）可以操作owner名下的所有 tokenId 的 NFT。

其实说到这里我想到，虽然没看过opensea的代码，但opensea应该就是使用setApprovalForAll 来获取我对NFT的控制权的，因为对一个 NFT合约，只需要授权一次，下次再卖就无需授权了。

### transfer方法

transfer方法代码：

    function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes calldata _data) external payable {
        address owner = ownerOf(_tokenId);
        address approvedAddress = getApproved(_tokenId);
        require(msg.sender == owner || msg.sender == approvedAddress || isApprovedForAll(owner,msg.sender),"EIP721/safeTransferFrom msg.sender not correct");
        require(from == owner, "EIP721/safeTransferFrom from not correct");
        require(to != address(0), "EIP721/safeTransferFrom to not correct");
        require(_exists(_tokenId), "EIP721/safeTransferFrom tokenId not exists");
        _transfer(_from,_to,_tokenId);
        require(_checkOnERC721Received(_from,_to,_tokenId,_data));   
    }
    

前面进行了大量校验逻辑。校验了是否为owner，此tokenid是否被授权，是否被授权整个系列，参数是否正确等。

这里最后就是检查to地址是否注册为 onERC721Received 方法。

    function _transfer(address _from, address _to, uint256 _tokenId) internal {
            require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer of token that is not own");
            require(to != address(0), "ERC721: transfer to the zero address");
            _beforeTokenTransfer(from, to, tokenId);  //hock
            _approve(address(0), tokenId);  // Clear approvals from the previous owner
            _balances[from] -= 1;
            _balances[to] += 1;
            _owners[tokenId] = to;
            emit Transfer(from, to, tokenId);
    }
    

可以看到修改了 from 和 to 的 \_balances，修改了 \_owners，清理了 \_tokenApprovals 授权。 这里看到，transfer 并没有修改 \_operatorApprovals。通过这里我想到了前段时间频繁出现的opeasea漏洞，有人的 NFT被低价买走。

这个漏洞的原因是，拥有者曾在opensea低价挂单，后面没有进行撤单，直接转到了其他地址。某一天他再次转到旧地址，当时的挂单直接被成交了。从这里就可以解释了，因为 opensea 没有处理旧挂单，transfer 也没有清理opensea合约的授权，导致了这个悲剧。

### checkOnERC721Received

        function _checkOnERC721Received(
            address from,
            address to,
            uint256 tokenId,
            bytes memory _data
        ) private returns (bool) {
            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 {
                        assembly {
                            revert(add(32, reason), mload(reason))
                        }
                    }
                }
            } else {
                return true;
            }
        }
    

这部分逻辑主要是，如果to是合约地址，调用onERC721Received，判断返回值是否为selector常量。并且如果调用出错时，处理并抛出错误原因

### 扩展合约

ERC721Enumerable，实现了两个功能：

1.  获取某个owner拥有的第index个 token 的 tokenId
    
2.  获取第index个 token 的 tokenId
    

代码具体实现比较复杂，定义了几个存储。通过实现\_beforeTokenTransfer 这个hock，修改这些存储。

IERC721Metadata，定义了获取 name，symbol，tokenURI几个方法。其中tokenURI是很重要的，存储了我们token 的 json元数据，例如opensea等广泛采用的 name，description，image 三个字段的格式。其中image字段甚至可以包含 base64编码的 html。这样就能实现纯链上存储 NFT。

ERC1155
-------

Erc1155可以说是结合了Erc20和 Erc721，是一种更通用的表达，更灵活，也比较符合现实世界的一些编程逻辑，广泛应用在游戏中。

    interface IERC1155 is IERC165 {
        function balanceOf(address account, uint256 id) external view returns (uint256);
        function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids)
            external
            view
            returns (uint256[] memory);
        function setApprovalForAll(address operator, bool approved) external;
        function isApprovedForAll(address account, address operator) external view returns (bool);
        function safeTransferFrom(
            address from,
            address to,
            uint256 id,
            uint256 amount,
            bytes calldata data
        ) external;
        function safeBatchTransferFrom(
            address from,
            address to,
            uint256[] calldata ids,
            uint256[] calldata amounts,
            bytes calldata data
        ) external;
    

可以看到和 Erc721有很大相似之处。只是在转账的时候，支持对某个 tokenid 转一定的amount。并且添加了批量操作的接口。

合约内变量：

        mapping(uint256 => mapping(address => uint256)) private _balances;
    
        mapping(address => mapping(address => bool)) private _operatorApprovals;
    

对比Erc721省略了\_tokenApprovals，这一点很棒，简化了模型。

transfer中的逻辑也类似，只是除了before的hock，还增加了after的hook。

[https://embed.0xecho.com.ipns.page/?color-theme=light&desc=&has-h-padding=true&has-v-padding=true&modules=comment&receiver=suden.eth&target\_uri=https%3A%2F%2Fmirror.xyz%2Fsuden.eth%2Fys8GollEBdLwENX0CMCvkHbXR-Ep8arNPf4NhdoY7zI&height=800&display=iframe](https://embed.0xecho.com.ipns.page/?color-theme=light&desc=&has-h-padding=true&has-v-padding=true&modules=comment&receiver=suden.eth&target_uri=https%3A%2F%2Fmirror.xyz%2Fsuden.eth%2Fys8GollEBdLwENX0CMCvkHbXR-Ep8arNPf4NhdoY7zI&height=800&display=iframe)

---

*Originally published on [Raver|sudden](https://paragraph.com/@suden/erc20-erc721-1155)*
