Solidity极简入门 ERC721专题:3. ERC721主合约
0x25df
April 25th, 2022

我最近在重新学solidity,巩固一下细节,也写一个“Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。

欢迎关注我的推特:@0xAA_Science

WTF技术社群discord,内有加微信群方法:链接

所有代码和教程开源在github(1024个star发课程认证,2048个star发社群NFT): github.com/AmazingAng/WTFSolidity

不知不觉我已经完成了Solidity极简教程的前13讲(基础),内容包括:Helloworld.sol,变量类型,存储位置,函数,控制流,构造函数,修饰器,事件,继承,抽象合约,接口,库,异常。在进阶内容之前,我决定做一个ERC721的专题,把之前的内容综合运用,帮助大家更好的复习基础知识,并且更深刻的理解ERC721合约。希望在学习完这个专题之后,每个人都能发行自己的NFT


ERC721主合约

我在ERC721前两讲介绍了它的相关库和接口,终于这讲可以介绍主合约了。ERC721主合约包含6个状态变量和28个函数,我们将会一一介绍。并且,我给ERC721代码增加了中文注释,方便大家使用。

状态变量

    // 代币名称
    string private _name;
 
    // 代币代号
    string private _symbol;

    // tokenId到owner地址Mapping
    mapping(uint256 => address) private _owners;

    // owner地址到持币数量Mapping
    mapping(address => uint256) private _balances;

    // tokenId到授权地址Mapping
    mapping(uint256 => address) private _tokenApprovals;

    // owner地址到是否批量批准Mapping
    mapping(address => mapping(address => bool)) private _operatorApprovals;
  • _name和_symbol是两个string,存储代币的名称和代号。
  • _owners是tokenId到owner地址的Mapping,存储每个代币的持有人。
  • _balances是owner地址到持币数量的Mapping,存储每个地址的持仓量。
  • _tokenApprovals是tokenId到授权地址的Mapping,存储每个token的授权信息。
  • _operatorApprovals是owner地址到是否批量批准的Mapping,存储每个owner的批量授权信息。注意,批量授权会把你钱包持有这个系列的所有nft都授权给另一个地址,别人可以随意支配。

函数

  • constructor:构造函数,设定ERC721代币的名字和代号(_name_symbol变量)。
    constructor(string memory name_, string memory symbol_) {
        _name = name_;
        _symbol = symbol_;
    }
    function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
        return
            interfaceId == type(IERC721).interfaceId ||
            interfaceId == type(IERC721Metadata).interfaceId ||
            super.supportsInterface(interfaceId);
    }
  • balanceOf:实现IERC721balanceOf,利用_balances变量查询owner地址的balance
    function balanceOf(address owner) public view virtual override returns (uint256) {
        require(owner != address(0), "ERC721: balance query for the zero address");
        return _balances[owner];
    }
  • ownerOf:实现IERC721ownerOf,利用_owners变量查询tokenIdowner
    function ownerOf(uint256 tokenId) public view virtual override returns (address) {
        address owner = _owners[tokenId];
        require(owner != address(0), "ERC721: owner query for nonexistent token");
        return owner;
    }
  • name:实现IERC721Metadataname,查询代币名称。
    function name() public view virtual override returns (string memory) {
        return _name;
    }
  • symbol:实现IERC721Metadatasymbol,查询代币代号。
    function symbol() public view virtual override returns (string memory) {
        return _symbol;
    }
  • tokenURI:实现IERC721MetadatatokenURI,查询代币metadata存放的网址。Opensea还有小狐狸钱包显示你NFT的图片,调用的就是这个函数。
    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");

        string memory baseURI = _baseURI();
        return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
    }
  • _baseURI:基URI,会被tokenURI()调用,跟tokenId拼成tokenURI,默认为空,需要子合约重写这个函数。
    function _baseURI() internal view virtual returns (string memory) {
        return "";
    }
  • approve:实现IERC721approve,将tokenId授权给 to 地址。条件:to不是owner,且msg.senderowner或授权地址。调用_approve函数。
    function approve(address to, uint256 tokenId) public virtual override {
        address owner = ERC721.ownerOf(tokenId);
        require(to != owner, "ERC721: approval to current owner");

        require(
            _msgSender() == owner || isApprovedForAll(owner, _msgSender()),
            "ERC721: approve caller is not owner nor approved for all"
        );

        _approve(to, tokenId);
    }
  • getApproved:实现IERC721getApproved,利用_tokenApprovals变量查询tokenId的授权地址。
    function getApproved(uint256 tokenId) public view virtual override returns (address) {
        require(_exists(tokenId), "ERC721: approved query for nonexistent token");

        return _tokenApprovals[tokenId];
    }
  • setApprovalForAll:实现IERC721setApprovalForAll,将持有代币全部授权给operator地址。调用_setApprovalForAll函数。
    function setApprovalForAll(address operator, bool approved) public virtual override {
        _setApprovalForAll(_msgSender(), operator, approved);
    }
  • isApprovedForAll:实现IERC721isApprovedForAll,利用_operatorApprovals变量查询owner地址是否将所持NFT批量授权给了operator地址。
    function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) {
        return _operatorApprovals[owner][operator];
    }
  • transferFrom:实现IERC721transferFrom,非安全转账,不建议使用。调用_transfer函数。
    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) public virtual override {
        //solhint-disable-next-line max-line-length
        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved");

        _transfer(from, to, tokenId);
    }
  • safeTransferFrom:实现IERC721safeTransferFrom,安全转账,调用了_safeTransfer函数。
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId,
        bytes memory _data
    ) public virtual override {
        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved");
        _safeTransfer(from, to, tokenId, _data);
    }
  • _safeTransfer:安全转账,安全地将 tokenId 代币从 from 转移到 to,会检查合约接收者是否了解 ERC721 协议,以防止代币被永久锁定。调用了_transfer函数和_checkOnERC721Received函数。条件:
    • from 不能是0地址.
    • to 不能是0地址.
    • tokenId 代币必须存在,并且被 from拥有.
    • 如果 to 是智能合约, 他必须支持 IERC721Receiver-onERC721Received.
    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");
    }
  • _exists:查询 tokenId是否存在(等价于查询他的owner是否为非0地址)。
    function _exists(uint256 tokenId) internal view virtual returns (bool) {
        return _owners[tokenId] != address(0);
    }
  • _isApprovedOrOwner:查询 spender地址是否被可以使用tokenId(他是owner或被授权地址)。
    function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) {
        require(_exists(tokenId), "ERC721: operator query for nonexistent token");
        address owner = ERC721.ownerOf(tokenId);
        return (spender == owner || getApproved(tokenId) == spender || isApprovedForAll(owner, spender));
    }
  • _safeMint:安全mint函数,铸造tokenId并转账给 to地址。 条件:
    • tokenId尚不存在,
    • to为智能合约,他要支持IERC721Receiver-onERC721Received接口。
    function _safeMint(address to, uint256 tokenId) internal virtual {
        _safeMint(to, tokenId, "");
    }
  • _safeMint的实现,调用了_checkOnERC721Received函数和_mint函数。
    function _safeMint(
        address to,
        uint256 tokenId,
        bytes memory _data
    ) internal virtual {
        _mint(to, tokenId);
        require(
            _checkOnERC721Received(address(0), to, tokenId, _data),
            "ERC721: transfer to non ERC721Receiver implementer"
        );
    }
  • _mintinternal铸造函数。通过调整_balances_owners变量来铸造tokenId并转账给 to,同时释放Tranfer事件。条件:
    • tokenId尚不存在。
    • to不是0地址.
    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);

        _balances[to] += 1;
        _owners[tokenId] = to;

        emit Transfer(address(0), to, tokenId);

        _afterTokenTransfer(address(0), to, tokenId);
    }
  • _burninternal销毁函数,通过调整_balances_owners变量来销毁tokenId,同时释放Tranfer事件。条件:tokenId存在。
    function _burn(uint256 tokenId) internal virtual {
        address owner = ERC721.ownerOf(tokenId);

        _beforeTokenTransfer(owner, address(0), tokenId);

        // 清空授权
        _approve(address(0), tokenId);

        _balances[owner] -= 1;
        delete _owners[tokenId];

        emit Transfer(owner, address(0), tokenId);

        _afterTokenTransfer(owner, address(0), tokenId);
    }
  • _transfer:转账函数。通过调整_balances_owner变量将 tokenIdfrom 转账给 to,同时释放Tranfer事件。条件:
    • tokenIdfrom 拥有
    • to 不是0地址
    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);

        // 清空授权
        _approve(address(0), tokenId);

        _balances[from] -= 1;
        _balances[to] += 1;
        _owners[tokenId] = to;

        emit Transfer(from, to, tokenId);

        _afterTokenTransfer(from, to, tokenId);
    }
  • _approve:授权函数。通过调整_tokenApprovals来,授权 to 地址操作 tokenId,同时释放Approval事件。
    function _approve(address to, uint256 tokenId) internal virtual {
        _tokenApprovals[tokenId] = to;
        emit Approval(ERC721.ownerOf(tokenId), to, tokenId);
    }
  • _setApprovalForAll:批量授权函数。通过调整_operatorApprovals变量,批量授权 to 来操作 owner全部代币,同时释放ApprovalForAll事件。
    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);
    }
  • _checkOnERC721Received:函数,用于在 to 为合约的时候调用IERC721Receiver-onERC721Received, 以防 tokenId 被不小心转入黑洞。
    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;
        }
    }
  • _beforeTokenTransfer:这个函数在转账之前会被调用(包括mintburn)。默认为空,子合约可以选择重写。
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal virtual {}
  • _afterTokenTransfer:这个函数在转账之后会被调用(包括mintburn)。默认为空,子合约可以选择重写。
    function _afterTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal virtual {}

总结

本文是ERC721专题的第三讲,我介绍了ERC721主合约的全部变量和函数,并给出了合约的中文注释。有了ERC721标准,NFT项目方只需要把mint函数包装一下,就可以发行NFT了。下一讲,我们会介绍无聊猿BAYC的合约,了解一下最火NFT在标准ERC721合约上做了什么改动。

Subscribe to 0xAA
Receive new entries directly to your inbox.
Collectors
View
#1
#2
#3
View collectors
This entry has been permanently stored on-chain and signed by its creator.