Solidity极简入门 ERC721专题:3. ERC721主合约

我最近在重新学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 the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.