以太坊开发入门(二)-深度解析ERC721标准

  • EimJacky
  • 更新于 2024-08-27 15:56
  • 阅读 587

ERC721标准是以太坊上的非同质化代币(NFT)的核心协议。与ERC20不同,ERC721中的每个代币都是独特的,不可互换。这使得ERC721非常适合表示如收藏品、艺术品、游戏道具等具有独特属性的数字资产。本文将探讨ERC721的开发流程、技术细节。

1、ERC721标准规范

1.1 IERC721

ERC20一样,ERC721同样是一个代币标准,官方解释NFTNon-Fungible Token,译作非同质化代币。

如何理解NFT呢?非同质化代表独一无二,其和ERC20的区别,在于资产是否可以分割与独一无二,而NFT标准都有唯一的标识符和元数据,就像是世界上没有两片完全相同的树叶,每个NFT代币彼此不可替代,这些独特性使得ERC721标准具有广泛的应用场景,包括艺术品、收藏品、域名、数字证书、数字音乐等等领域。

ERC721基本标准为

  • balanceOf():返回owner账户的代币数量。

  • ownerOf():返回tokenId的所有者。

  • safeTransferFrom():安全转账NFT

    函数需要做以下校验

    • msg.senfer应该是当前tokenIdowner或是spender
    • _from必须是_tokenId的所有者;
    • _tokenId必须存在并且属于_from
    • _to如果是CA(合约地址),它必须实现IERC721Receiver-onERC721Received接口,检查其返回值。这么做的目的是,为了避免将tokenId转移到一个无法控制的合约地址,导致token被永久转进黑洞。因为CA账户无法主动触发交易,只能由EOA账户来调用合约触发交易。
  • transferFrom():非安全转账NFT

  • approve():授权地址_to具有tokenId的支配权

  • setApprovalForAll():批准或取消_openratertoken操作权限,用于批量授权

  • getApproved():获取_tokenId授权

  • isApprovedForAll():获取_tokenId的支配情况

1.2 IERC165

ERC721要求必须符合ERC165标准,什么是ERC165

ERC20ERC721一样,它也是以太坊系统的一种标准规范,其主要用于:

  • 一种接口检查查询和发布标准
  • 检测智能合约实现了哪些接口

IERC165官方定义为

    /// @dev 查询一个合约时候实现了一个接口
    /// param interfaceID  参数:接口ID
    /// return true 如果函数实现了 interfaceID (interfaceID 不为 0xffffffff )返回true, 否则为 false
interface ERC165 {
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}

这里仅作补充,ERC165提供了一种确定性的互操作支持,方便了合约之间的检查交互。

1.3 IERC721与IERC165

ERC721当中,依赖ERC165接口,重写supportsInterface(bytes4 interfaceId)覆盖父级合约,在调用方法之前查询目标合约是否实现相应接口,具体实现如下:

    /**
     * @dev 查询目标合约是否实现ERC721接口
     */
    function supportsInterface(bytes4 interfaceId) public view override(ERC165, IERC165) returns (bool) {
        return
            interfaceId == type(IERC721).interfaceId ||
            interfaceId == type(IERC721Metadata).interfaceId ||
            super.supportsInterface(interfaceId);
    }

当查询的是IERC721IERC721MetadataIERC165的接口id时,返回true;反之返回false,参考:EIP165官方提案

2、编写ERC721函数接口

IERC721ERC721标准的接口合约,规定了ERC721要实现的基本函数。它利用tokenId来表示特定的非同质化代币,授权或转账都要明确tokenId,它通过一组标准化的函数接口来管理资产的所有权和交易;而ERC20只需要明确转账的数额即可。

2.1 IERC165接口

IERC721必须符合IERC165接口,便于合约交互做接口查询

interface IERC165 {
    /**
     * @dev 查询一个合约时候实现了一个接口
     *  param interfaceID  参数:接口ID
     *  return bool
     */
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

2.2 ERC721事件

IERC721定义了3个事件:TransferApprovalApprovalForAll,分别在转账、授权和批量授权时候释放;

    /**
     * @dev 释放条件:发生`tokenId`代币转移,从`from`转移至`to`.
     * param( address , address , uint256 )
     */
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

    /**
     * @dev 释放条件:发生`tokenId`代币授权,`owner`授权给`approved`支配token.
     * param( address , address , uint256 )
     */
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);

    /**
     * @dev 释放条件:当`owner`管理`operator`的所有资产管理权限,即批量授权
     * param(address,address,bool)
     */
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

2.3 函数接口

IERC721定义了9个函数,实现代币的交易、授权和查询功能。

  • balanceOf()

返回目标账户owner的代币数量,区分ERC20代币数量,这里可理解为tokenId数量

    /**
     * @dev 返回代币数量.
     * param address 账户地址
     * return uint256 代币数量
     */
    function balanceOf(address owner) external view returns (uint256 balance);
  • ownerOf()

返回tokenIdowner

    /**
     * @dev 查询`tokenId`的拥有者
     * 
     *  param uint256 tokenId
     *  return address 代币拥有者
     * 查询条件:
     * - `tokenId` 必须存在.
     */
    function ownerOf(uint256 tokenId) external view returns (address owner);
  • safeTransferFrom()

安全转账,将tokenIdfrom转移至to,携带data参数,data的作用可以是附加额外的参数(没有指定格式),传递给接收者。

    /**
     * @dev 安全转账,将NFT的所有权从`from`转移至`to`.
     *
     * 转移条件:
     *
     * - `from` 不能是address(0).
     * - `to` 不能是address(0).
     * - `tokenId` 必须存在且属于`from`.
     * - 如果调用者不是`from`,则必须通过授权校验,拥有该`tokenId`的支配权.
     * - 如果`to`为合约地址,则必须实现{IERC721Receiver-onERC721Received}接口.
     *
     *释放 {Transfer} 事件.
     */
    function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;
  • safeTransferFrom()

安全转账, 将tokenIdfrom转移至to,功能同上,不带data参数。

    /**
     * @dev 功能参考 ``safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data)``
     *
     * 释放 {Transfer} 事件.
     */
    function safeTransferFrom(address from, address to, uint256 tokenId) external;
  • transferFrom()

普通转账, 将tokenIdfrom转移至to

    /**
     * @dev 转移 `tokenId` 从 `from` 到 `to`.
     *
     * @notice: 调用此方法需注意接收者有能力调配`ERC721`,否则可能会永久丢失,推荐使用`safeTransferFrom`,但这会增加一次外部调用,可能会导致重入,注意防范.
     *
     * 条件:参考`safeTransferFrom`
     *
     * 释放 {Transfer} 事件.
     */
    function transferFrom(address from, address to, uint256 tokenId) external;
  • approve()

代币授权,和ERC20一致,授权其他用户支配自己的代币,这里体现为NFT

    /**
     * @dev 授权`to`账户支配调用者`msg.sender`的`tokenId`-`NFT`权限.
     * 当`token`发生转账时会清除授权.
     *
     * NFT只能授权给一个账户,当发生新的授权时候会更新授权账户.
     *
     * 条件:
     *
     * - 调用者必须为拥有该`NFT`或者被授权能够支配该`NFT`
     * - `tokenId` 必须存在.
     *
     * 释放 {Approval} 事件.
     */
    function approve(address to, uint256 tokenId) external;
  • setApprovalForAll()

批量授权其他账户支配自己NFT的权限

    /**
     * @dev 批准或者移除`operator`账户对`msg.sender`账户所有NFT操作的权限
     * operator可以调用{transferFrom}或者{safeTransferFrom}转移token
     *
     * 条件:
     *
     * - `operator` 不能是address(0).
     *
     * 释放 {ApprovalForAll} 事件.
     */
    function setApprovalForAll(address operator, bool approved) external;
  • getApproved()

查询某tokenId被授权给哪个账户

    /**
     * @dev 返回`tokenId`批准支配的账户.
     *
     * 条件:
     *
     * - `tokenId` 必须存在.
     */
    function getApproved(uint256 tokenId) external view returns (address operator);
  • isApprovedForAll()

查询某地址的NFT是否批量授权给了operator`地址支配

    /**
     * @dev 返回是否允许`operator`能够支配`owner`的所有NFT
     *
     */
    function isApprovedForAll(address owner, address operator) external view returns (bool);

3、 编写IERC721Metadata接口

IERC721MetadataIERC721的扩展接口,实现了ERC721的元数据扩展,包括namesymbo、和tokenURINFT所对应的资源)。该接口用于存储额外数据,包括:

  • name():返回代币名称
  • symbol():返回代币代号符号
  • tokenURI():返回tokenId对应的元数据,URI通常存储图片的链接路径或者是IPFS存储链接

接口标准

/**
 * @title ERC-721 元数据扩展接口
 * @dev 见 https://eips.ethereum.org/EIPS/eip-721
 */
interface IERC721Metadata is IERC721 {
    /**
     * @dev 查询代币名称.
     */
    function name() external view returns (string memory);

    /**
     * @dev 查询代币代号符号.
     */
    function symbol() external view returns (string memory);

    /**
     * @dev 查询NFT的URI元数据
     */
    function tokenURI(uint256 tokenId) external view returns (string memory);
}

4、 编写IERC721Receiver接口

IERC721Receiver确保合约地址如果要接收NFT的安全转账,必须实现其接口。这里在于提醒合约开发者在编写接收NFT的合约时候,能够编写有效处理NFT的转账逻辑.

这里可以理解为当EOA转账ETHCA时,如果合约代码没有实现withdraw函数进行提现eth,那么这个eth便永久的储存在合约当中,因为合约代码不能自发调用其代码,必须通过EOA账户进行函数调用。对比NFT的转账,如果开发者在接收NFT的合约中没有提供转账NFT的功能,那么这个token便会永久留在这个合约当中,相当于发送进黑洞。

为了防止这种情况,IERC721Receiver接口中包含onERC721Received函数,只用接收合约中实现了这个接口才能接收NFT,意味着开发者意识到了这个问题,在自己的合约代码中防范了这种情况,当然,如果实现了这个接口,但是仍然没有针对合约转账NFT做出防范措施,那头铁的结果依然是NFT进了黑洞。

IERC721Receiver标注规范为:

interface IERC721Receiver {
    /**
     * @dev 当发送想合约转账NFT时,回调此函数
     *
     * @notice 返回其函数选择器,以确认token转账.
     * @notice 返回其他值,或者接收合约未实现该接口,转账将被revert.
     *
     * 函数选择器可通过`IERC721Receiver.onERC721Received.selector`获得.
     */
    function onERC721Received(
        address operator,
        address from,
        uint256 tokenId,
        bytes calldata data
    ) external returns (bytes4);
}

5、 编写IERC721Errors错误接口

IERC721Errors定义了8个错误,帮助我们在实现代码业务逻辑时捕获错误异常

  • ERC721InvalidOwner

转账错误时候触发,表明NFT的owner地址不合法

    /**
     * @dev 不合法的owner地址. 例如:address(0).
     * 用于查询balance时候调用.
     * param address -- owner.
     */
    error ERC721InvalidOwner(address owner);
  • ERC721NonexistentToken

tokenId不存在时候触发

    /**
     * @dev 表明 `tokenId`的`owner`为address(0).
     * param uint256 -- tokenId.
     */
    error ERC721NonexistentToken(uint256 tokenId);
  • ERC721IncorrectOwner

tokenId对应的token所有权错误,转账时候触发

    /**
     * @dev 表明 `tokenId`的`owner`为发生错误.
     * param (address,tokenId,address) -- (发送方,tokenId,NFTowner).
     */
    error ERC721IncorrectOwner(address sender, uint256 tokenId, address owner);
  • ERC721InvalidSender

token转账错误,不合法的sender,多用于address(0)转账NFT

    /**
     * @dev 表明`sender`发送token失败
     * param address 发生转账NFT的地址.
     */
    error ERC721InvalidSender(address sender);
  • ERC721InvalidReceiver

token转账错误,不合法的receiver,多用于向address(0)转账NFT

    /**
     * @dev 表明`receiver`接收token失败
     * param address 接收转账NFT的地址.
     */
    error ERC721InvalidReceiver(address receiver);
  • ERC721InsufficientApproval

operator操作账户获取授权失败,表明未被授权tokenId的操作权限

    /**
     * @dev `operater`未经授权`tokenId`,转账失败.
     * param (address uint256) -- (操作账户,`tokenId`)
     *
     */
    error ERC721InsufficientApproval(address operator, uint256 tokenId);
  • ERC721InvalidApprover

参考转账账户发生的错误,这里用于授权账户不合法,即address(0)发生授权。授权的时候触发

    /**
     * @dev 表明授权账户`approver`不合法,.
     * param address -- 授权账户.
     */
    error ERC721InvalidApprover(address approver);
  • ERC721InvalidOperator

操作账户不合法,即向address(0)地址授权。授权时候触发

    /**
     * @dev 表明操作账户`operator`不合法,.
     * param address -- 操作账户.
     */
    error ERC721InvalidOperator(address operator);

8error,涵盖了在NFT发生转账、授权的时候可能遇到的错误,帮助我们在编写代码的时候捕获错误异常

6、 实现ERC721

ERC721主合约实现了IERC721IERC165IERC721MetadataIERCErrors定义的所有功能,此外我们借助OpenzeppelinStrings.sol方法帮助我们处理uint256类型的字符串转换问题

接下来我们创建ERC721合约,导入以下接口文件

import {IERC721} from "./IERC721.sol";
import {IERC721Metadata} from "./IERC721Metadata.sol";
import {IERC721Receiver} from "./IERC721Receiver.sol";
import {IERC165} from "./IERC165.sol";
import {IERC721Errors} from "./IERC721Errors.sol";
import "@openzeppelin/contracts/utils/Strings.sol";

6.1 状态变量

对比ERC20标准,我们同样需要使用状态变量来记录账户NFT信息,授权以及token信息

    using  Strings for uint256;

    // 代币名称
    string private _name;
    // 代币符号
    string private _symbol;
    // NFT 的owner
    mapping (uint256  tokenId => address) private _owner;
    // 账户拥有的的NFT数量
    mapping (address owner => uint256) private  _balances;
    // NFT的授权账户
    mapping (uint256 tokenId => address) private _tokenApprovals;
    // 账户operator 是否被授权支出 owner 的NFT,即批量授权
    mapping (address owner => mapping (address operator => bool)) private  _operatorApprovals;

6.2 函数

  • 构造函数:初始化代币名称,符号。
    /**
     * @dev 合约部署时实例化name 和 symbol 状态变量.
     */
    constructor(string memory name_ , string memory symbol_){
        _name = name_;
        _symbol = symbol_;
    }
  • supportsInterface()

查询NFT合约支持的接口,在调用方法之前查询目标合约是否实现相应接口,详细描述见1.3

    /**
     * @dev 查询接口ID.
     */
    function supportsInterface(bytes4 interfaceId)public  pure returns (bool){
        return 
            interfaceId == type(IERC721).interfaceId ||
            interfaceId == type(IERC721Metadata).interfaceId ||
            interfaceId == type(IERC165).interfaceId;
    }
  • balanceOf()

查询owner持有的代币数量

    /**
     * @dev 查询用户持仓数量.
     */
    function balanceOf(address owner) public  view returns (uint256){
        //地址校验
        if (owner == address(0)){
            revert ERC721InvalidOwner(address(0));
        }
        return _balances[owner];
    }
  • ownerOf()和_requireOwned()

查询tokenId的所有者,校验NFT是否存在

    /**
     * @dev 查询NFT的所有者.
     */
    function _ownerOf(uint256 tokenId)internal  view returns (address){
        return _owner[tokenId];
    }
    /**
     * @dev 供外部调用,逻辑处理交给_ownerOf().
     */
    function ownerOf(uint256 tokenId)public  view  returns (address){
        return _requireOwned(tokenId);
    }
    /**
     * @dev 如果 `tokenId` 没有当前所有者(尚未铸币或已被烧毁) 交易回滚
     * 返回 `owner`.
     */
    function _requireOwned(uint256 tokenId)internal view returns(address){
        //判断NFT是否存在
        address owner = _ownerOf(tokenId);
        if (owner == address(0)){
            revert ERC721NonexistentToken(tokenId);
        }
        return owner;

    }
  • name()symbol()

查询NFT的名称和代号

    /**
     * @dev 查询名称.
     */
    function name()public  view returns (string memory){
        return _name;
    }
    /**
     * @dev 查询代号.
     */
    function symbol()public  view  returns (string memory){
        return  _symbol;
    }
  • tokenURI()_baseURI()

查询NFTURI元数据

    /**
     * @dev 查询NFT扩展对应外部资源.
     */
    function tokenURI(uint256 tokenId) public  view  returns (string memory){
        //这里做NFT校验
        _requireOwned(tokenId);
        //获取基础URI
        string memory baseURI = _baseURI();

        //拼接{baseURI} + {tokenId}
        return bytes(baseURI).length > 0 ? string.concat(baseURI,tokenId.toString()) : "";
    }

    /**
     * @dev 用作{tokenURI} 的基础 URI 
     * 如果设置:
     * 每个{token}的URI 由`baseURI` + `tokenId`拼接而成
     *   
     * 这里默认为 "" 支持后续继承重载
     */
    function _baseURI()internal pure virtual returns (string memory){
        return  "";
    } 
  • _getApproved()

查询NFT的授权地址

    /**
     * @dev 查询`tokenId` 的授权账户. 未被授权则返回address(0)
     */
    function _getApproved(uint256 tokenId) internal  view  returns (address){
        return _tokenApprovals[tokenId];
    }
  • _isAuthorized()

查询tokenId的NFT的账户操作权限

    /**
     * @dev 查询`spender`是否能操作`owner`的NFT
     * 三种情况:1.spender是NFT的owner 2. spender被owner批量授权管理其NFT 3.spender被owner授权管理`tokenId`的NFT
     */
    function _isAuthorized(address owner ,address spender , uint256 tokenId) internal view returns (bool){
        return 
            spender != address(0) &&
            (owner == spender || _operatorApprovals[owner][spender] || _getApproved(tokenId) == spender);
    }
  • _checkAuthorized()

检查NFT授权情况,捕获相应错误

    /**
     * @dev 检查`spender`是否能操作`owner`的NFT
     * 捕获相应错误{ERC721NonexistentToken} {ERC721InsufficientApproval}
     */
    function _checkAuthorized(address owner , address spender , uint256 tokenId)internal  view {
        if (!_isAuthorized(owner, spender, tokenId)){
            //这里的owner一般是后续外部调用: 通过`tokenId`查询得到的地址,即使用`_ownerOf()`函数得到的地址,所以非捕获的{ERC721InvalidOwner}错误
            if (owner  == address(0)){
                revert ERC721NonexistentToken(tokenId);
            }else {
                revert ERC721InsufficientApproval(spender,tokenId);
            }
        }
    }
  • _approve()

授权逻辑,参考ERC20的授权函数入参,这里同样引入emitEvent来区分是否需要释放授权事件

    /**
     * @dev 授权内部处理逻辑 emitEvent可选
     *
     * @param to 授权地址
     * @param tokenId NFT id
     * @param auth 支出账户 
     * @param emitEvent 事件释放信号
     */
    function _approve(address to , uint256 tokenId , address auth , bool emitEvent)internal {
        //地址校验
        if (emitEvent || auth != address(0)){
            address owner = _requireOwned(tokenId);

            //权限判断,授权账户非address(0)情况下:owner和auth不等且未获得批量授权;
            if (auth != address(0) && owner != auth && !_operatorApprovals[owner][auth]){
                 revert ERC721InvalidApprover(auth);
            }

            if ( emitEvent ){
                emit  Approval(owner, to, tokenId);
            }
        }
        //更新授权
        _tokenApprovals[tokenId] = to;
    }
  • _setApprovalForAll()

批量授权逻辑

    /**
     * @dev 批量授权owner的NFT
     *
     * 条件:
     * - operator 不能是address(0).
     *
     *  释放{ApprovalForAll} 事件.
     */
    function _setApprovalForAll(address owner , address operator , bool approved) internal {
        //地址校验
        if (operator == address(0)){
            revert ERC721InvalidOperator(operator);
        }

        _operatorApprovals[owner][operator] = approved;
        emit ApprovalForAll(owner, operator, approved);
    }
  • _checkOnERC721Received()

在安全转账NFT的时候调用,检查如果接收账户为CA则检查目标合约是否实现IERC721Receiver接口,提醒开发者注意合约是否能够正确处理转入合约的NFT,而_checkOnERC721Received内部检查目标合约是否返回指定的接口ID

    /**
     * @dev 在目标地址上调用 {IERC721Receiver-onERC721Received}数。如果
     * 接收方不接受token转账。如果目标地址不是合约,则不执行调用。
     *
     * @param from 地址,代表给定token ID 的上一个所有者
     * @param to 将接收代币的目标地址
     * @param tokenId uint256 要传输的NFT
     * @param data bytes 可选数据,与调用一起发送
     */
    function  _checkOnERC721Received(address from , address to ,uint256 tokenId ,bytes memory data)private {
        //在 {address} 的代码,EOA为空
        if (to.code.length > 0){
            try IERC721Receiver(to).onERC721Received(msg.sender , from , tokenId , data) returns (bytes4 retval){
                //校验返回值和指定ID是否一致
                if (retval != IERC721Receiver.onERC721Received.selector){
                    revert ERC721InvalidReceiver(to);
                }
            }catch (bytes memory reason ) {
                if (reason.length == 0){
                    revert ERC721InvalidReceiver(to);
                }else {
                    assembly {
                        //这里简单介绍:
                        //reason指向存储错误消息的指针位置
                        //add(32,reason)指针向前移动32个字节,因为Solidity动态数组在内存存储时候,前32个字节用于存储数组的长度
                        //这里加上32个字节,指针跳过数组长度信息,直接指向错误消息的实际内容

                        //mload指令: 从内存中加载数据
                        //从内存中reason + 32的位置开始,以mload(reason)指定的长度来返回错误消息,并终止交易
                        revert(add(32,reason),mload(reason))
                    }
                }
            }
        }
    }
  • _update()

转账的内部处理逻辑,包含用户转账、NFT铸造、NFT销毁等,都可视作NFT的转账,区别在于转账账户的不同。

    /**
     * @dev 将 `tokenId` 从其当前拥有者转移到 `to` 中,或者,如果当前拥有者或 `to` 是零地址,则进行铸币(或烧毁)
     *       
     * `auth` "参数是可选参数。如果传递的值非零地址,则此函数将检查
     * `auth` 是`token`的所有者,或已获准对`token`进行操作(由所有者批准)
     *
     * 释放 {Transfer} 事件。
     *
     */
    function _update(address to , uint256 tokenId , address auth)internal returns (address){
        //获取NFT的owner
        address from = _ownerOf(tokenId);   

        //地址校验 && 检查auth是否有支出权限 
        if (auth != address(0)) {
            _checkAuthorized(from, auth, tokenId);
        }

        //执行转账逻辑,首先判断NFT
        if (from != address(0)){
            //更新授权 授权账户清除
            _approve(address(0), tokenId, address(0), false);
            //更新from持仓数量
            unchecked {
                _balances[from] -= 1;
            }
        }
        //to 如果不是零地址 则代表不是销毁
        if (to != address(0)){
            //更新to持仓数量
            unchecked{
                _balances[to] += 1 ;
            }
        }
        //更新NFT所有者
        _owner[tokenId] = to;

        emit Transfer(from, to, tokenId);

        return  from;
    }
  • _mint()

铸造NFT

    /**
     * @dev 为 `tokenId` 造币并将其传输到 `to`。
     *
     * 建议使用 {_safeMint}
     *
     * 要求:
     *
     * `tokenId` 必须不存在。
     * `to` 不能是零地址。
     *
     * 释放 {Transfer} 事件。
     */
    function _mint(address to , uint256 tokenId) internal {
        if (to == address(0)){
            revert ERC721InvalidReceiver(address(0));
        }
        //判断前置NFT的Owner , 如果未铸造账户应是零地址
        address _previousOwner = _update(to, tokenId, address(0));

        if (_previousOwner != address(0) ){
            revert ERC721InvalidSender(address(0));
        }
    }
  • _safeMint()

安全铸造NFT,校验接收账户的NFT处理

    /**
     * @dev 安全铸造NFT,接收方若为合约地址则进行接口ID校验
     *
     *  详细解释参考{_checkOnERC721Received}
     */
    function _safeMint(address to, uint256 tokenId )internal {
        _mint(to, tokenId);
        _checkOnERC721Received(address(0), to, tokenId, "");
    }
  • _burn()

销毁NFT

    /**
     * @dev 为 `tokenId` 销毁,看作将其传输到 `address(0)`。
     *
     * 要求:
     *
     * `tokenId` 必须存在
     *
     * 释放 {Transfer} 事件。
     */
    function _burn(uint256 tokenId)internal {
        address previousOwner = _update(address(0), tokenId, address(0));
        if (previousOwner == address(0)){
            revert ERC721NonexistentToken(tokenId);
        }
    }
  • transferFrom()

转账NFT,用户转账自己的NFT

    /**
     * @dev 将 `tokenId` 从 `from` 传输到 `to`,与 {transferFrom} 相反,这对 msg.sender 没有任何限制。
     *
     * 要求:
     * -`to` 不能是零地址。
     * -`tokenId` 必须为 `from` 所有
     *
     * 释放 {Transfer} 事件
     */
    function transferFrom(address from , address to , uint256 tokenId)public {
        if (to == address(0)){
             revert ERC721InvalidReceiver(address(0));
        }
        //执行转账逻辑
        address previousOwner = _update(to, tokenId, msg.sender);
        //返回NFT的owner值校验
        //如果为address(0) 则NFT不存在
        if (previousOwner == address(0)){
             revert ERC721NonexistentToken(tokenId);
        //如果owner和from不等,则转账NFT错误
        }else if (previousOwner != from){
            revert ERC721IncorrectOwner(from, tokenId, previousOwner);
        }

    }
  • safeTransferFrom()

安全转账NFT

    /**
     * @dev 安全地将 `tokenId` 令牌从 `from` 传输到 `to`,检查合约接收方,防止代币被永久锁定。
     *
     * `data` 是附加数据,没有指定格式,在调用 `to` 时发送。
     *
     * 要求:
     *
     * -`tokenId` 令牌必须存在并为 `from` 所有。
     * - `to` 不能是零地址。
     * - `from`不能是零地址。
     * - 如果 `to` 指向一个智能合约,它必须实现 {IERC721Receiver-onERC721Received},在安全转移时调用。
     */
    function safeTransferFrom(address from , address to ,uint256 tokenId)public  {
        safeTransferFrom(from, to, tokenId, "");
    }

    /**
     * 与[`safeTransferFrom`]相同,但多了一个`data`参数。
     */
    function safeTransferFrom(address from, address to,uint256 tokenId , bytes memory data)public {
        transferFrom(from, to, tokenId);
        _checkOnERC721Received(from, to, tokenId, data);
    }
  • approve()

授权函数

    /**
     *  实现{IERC721}, 调用内部处理逻辑
     *  释放 {Approval} 事件
     */
    function approve(address to ,uint256 tokenId)public  {
        _approve(to, tokenId, msg.sender, true);
    }
  • getApproved()

查询授权账户

    /**
     *  实现{IERC721-getApproved}, 调用内部处理逻辑
     *  
     */
    function getApproved(uint256 tokenId)public view returns (address){
        //确保NFT存在
        _requireOwned(tokenId);

        return _getApproved(tokenId);

    }
  • setApprovalForAll()

进行批量授权

    /**
     *  实现{IERC721-setApprovalForAll}, 调用内部处理逻辑
     *  释放 {ApprovalForAll} 事件
     */
    function setApprovalForAll(address operator , bool approved)public {
        _setApprovalForAll(msg.sender, operator, approved);
    }
  • isApprovedForAll()

查询operator是否获得owner账户批量授权NFT

    /**
     *  实现{IERC721-isApprovedForAll}, 调用内部处理逻辑
     * 
     */
    function isApprovedForAll(address owner , address operator) public  view  returns (bool) {
        return  _operatorApprovals[owner][operator];
    }

7、 发行NFT

我们来利用ERC721来写一个免费铸造的NFT

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./ERC721.sol";

contract NFT is ERC721 {
    uint256 public counters = 1;

    constructor()ERC721("NFT","NFT") {

    }
    function mint(address to )public {
        _mint(to, counters);
        counters++;
    }
}

总结:ERC721的剖析我们就到这里,NFT还有很多优秀的设计模式,包括:

  • ERC721Enumerable:支持对ERC721持有的代币进行枚举;
  • ERC721A:实现批量铸造;
  • Merkle树实现铸造白名单;
  • ... 后面有时间再更新吧,更新不易,各位大大轻喷~~

参考:

EIP 721

Openzeppelin-contracts--ERC721

完整项目代码见:SolidityLongWayTODO/ERCTODO

点赞 2
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
EimJacky
EimJacky
0x1a8b...2e23
狂热的区块链爱好者 Long Way To Do in Blockchain Study