以太坊应用层提案之ERC-721

一 NFT概述

1.1 什么是NFT

ERC-20是同质化代币标准,同质化的意思就是说,代币在单位内表示的价值和功能是一样, 代币之间是可以互换的。比如你现在有100个BNB, 那么每一个BNB价值和功能都是一样的。

但是,有时候我们希望给区块链中的虚拟资产赋予独特的、唯一的等特性的时候,比如对数字资产的所有权、纪念章、数字艺术品、各种唯一的虚拟物品,比如游戏道具等等,这时候同质化代币是做不到这一点,因此有了非同质化代币NFT,即Non-Fungible Tokens

NFT: 就是Non-Fungible Token,即非同质化代币,他也是以太坊区块链上一种虚拟数字资产,但是和同质化代币不同,它是唯一的,独特的,不可互换的代币,每一个NFT都具有独特的属性和价值。

1.2 Non-Fungible Token(NFT) 和 Fungible  Token(Token)比较

1.2.1 可互换性

Token是可以互换的,每一个Token的价值和功能是一样的

NFT: 不可互换,每一个NFT都是独一无二的,都可能具有不同价值

1.2.2 不可分割性

Token是可以分割的,通常可以分割为更小的单位进行交易。比如你可以购买0.1个或者0.001个代币

NFT通常不可分割,每个NFT都是一个完整的单位,你不能购买0.5个NFT

1.2.3 使用场景不一样

Token主要用于支付、交易和价值存储,适合需要标准化价值的场景

NFT主要用于表示独特的资产和所有权,适合需要强调独特性和所有权的场景。比如游戏道具、数字艺术品、数字收藏品、所有权、数字票据等等

二  ERC-721标准

2.1 什么是ERC-721

ERC-721是以太坊非同质化代币(NFT)的一个标准,它定义了一套规范或者标准,如果NFT实现了ERC-721标准,外部的钱包、交易所或者其他DApp应用程序就可以很方便的和NFT进行交互。

2.2 ERC-721特点

非同质化

与同质化代币(如比特币和以太币)不同,ERC-721 代币是独特的,每个代币有其独特的属性和标识符。

这种独特性使得 ERC-721 代币特别适合表示独一无二的资产,如数字艺术品、收藏品、游戏物品等。

不可分割

大多数 ERC-721 代币是不可分割的,即不能像同质化代币那样分割成更小的单位。一个完整的 ERC-721 代币必须作为一个整体进行交易。

所有权和转移

ERC-721 标准定义了一组接口,使得NFT的所有权和转移过程可以在区块链上透明和安全地进行。代币的所有权信息被永久记录在区块链上,任何人都可以验证。

2.3 ERC-721标准内容

2.3.1 字段

和ERC-20不同,ERC-721因为要表示代币独一无二。因此,给NFT添加了一个tokenId的字段,表示该NFT独一无二。

另外,每一个NFT都有一个tokenURI,通常指向一个包含该代币详细信息的 JSON 文件,如名称、描述、图像链接等。这使得每个 NFT 可以有丰富的元数据,与其独特性相对应。比如:

{
    "title": "Louis Vena",
    "description": "Cneter Left",
    "image": "ipfs://QmX6pUBZDRt2uzw79iatA9zH9sbbDrrT1wThs8cjamqAkF",
    "attributes": [
        {
        "trait_type": "Singer",
        "value": "Belly"
        },
        {
        "trait_type": "Area",
        "value": "America"
        }
    ],
    "version": "1"
}

2.3.2 函数

2.3.2.1 balanceOf

function balanceOf(address _owner) external view returns (uint256);

返回指定地址的NFT余额,一个人可能会拥有多个

2.3.2.2 ownerOf

function ownerOf(uint256 _tokenId) external view returns (address);

根据指定的tokenId返回该NFT的拥有者地址

2.3.2.3 safeTransferFrom

function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;

安全的将指定的tokenId的NFT从_from地址转移到_to地址,并且还可以携带一些额外数据。

在复杂的交易场景中,可能需要附加的上下文信息。例如,某个 NFT 可能代表一个游戏中的物品,附加的数据可以包含关于游戏场景的信息。

在目标地址是合约时,附加的数据可以用来传递特定指令给合约。

function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;

安全的将指定的tokenId的NFT从_from地址转移到_to地址,不能携带一些额外数据

2.3.2.4 transferFrom

function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

将指定的tokenId的NFT从_from地址转移到_to地址

2.3.2.5 approve

function approve(address _approved, uint256 _tokenId) external payable;

授权某个地址_approved可以转移指定tokenId的NFT。

2.3.2.6 setApprovalForAll

function setApprovalForAll(address _operator, bool _approved) external;

授权或取消授权_operator地址可以管理调用者的所有NFT

2.3.2.7 getApproved

function getApproved(uint256 _tokenId) external view returns (address);

返回指定tokenId的NFT被授权哪个地址可以进行转移

2.3.2.8 isApprovedForAll

function isApprovedForAll(address _owner, address _operator) external view returns (bool);

是否_operator是否被授权管理_owner的所有NFT

2.3.3 事件

2.3.3.1 Transfer

event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);

在代币转移时需要触发

2.3.3.2 Approval

event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);

在代币授权时触发

2.3.3.3 ApprovalForAll

event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

在设置或取消所有授权时触发

三 safeTransferFrom和transferFrom比较

function safeTransferFrom(address from, address to, uint256 tokenId) external;

function transferFrom(address from, address to, uint256 tokenId) external;

3.1 相同点

都是将 指定tokenId的NFT 从 `from` 地址转移到` to` 地址

3.2 不同点

不同点主要是安全问题:

我们知道智能合约地址一般分为EOA地址和CA地址,那么钱包一般属于EOA地址,这类地址他们不会有代码的执行,仅用于持有和发送代币等;但是CA地址就是合约地址,合约地址是可以有代码执行逻辑的。

那这个和NFT转移安全有什么关系呢?  难道CA地址就无法转移成功,EOA地址就可以?

其实是这样的,无论是EOA地址还是CA地址,safeTransferFrom和transferFrom都是可以转移成功的。但是转移给CA地址,需要考虑到这个合约是否有接口可以把NFT转出来,如果转不出来,那么这个NFT就一直在这个合约里面了。因此,如果 to 地址是一个不支持 操作ERC721的合约,最后safeTransferFrom就会revert, 这笔交易不会成功。

那safeTransferFrom是怎么实现检查合约是否具备操作NFT的能力的呢?

在OpenZeppelin中,定义了一个ERC721Receiver接口,如果你的合约你觉得需要让调用者知晓你是具备操作ERC721的能力的,比如转账,那么你需要在你的合约中实现这个ERC721Receiver接口的onERC721Received函数;否则调用者就无法知晓你这个合约到底是不是支持ERC721接口的,那么safeTransferFrom就会认为不支持,则判定失败,回滚交易。

function onERC721Received(

        address operator,

        address from,

        uint256 tokenId,

        bytes calldata data

) external returns (bytes4);

那么实现了或者重载了,怎么认为他就具备处理ERC721的能力了?只需要这个函数返回值是`bytes4`类型,并且返回值为`this.onERC721Received.selector`,这样可以确保函数符合ERC721接口中定义的规范。比如:

function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) external override returns (bytes4) {

        // 这里可以添加处理逻辑,例如记录接收到的代币信息

        // 返回这个值表明成功接收

        return this.onERC721Received.selector;

}

四 手动实现ERC-721

// SPDX-License-Identifier: UNLICENSED

pragma solidity ^0.8.13;

import "./IERC721.sol";



abstract contract ERC720 is IERC721 {

    string private _name;

    string private _symbol;



    mapping(uint256 => address) private _owners;

    mapping(address => uint256) private _balances;

    mapping(uint256 => address) private _approvals;

    mapping(address owner => mapping(address operator => bool))

        private _operatorApprovals;



    constructor(string memory name, string memory symbol) {

        _name = name;

        _symbol = symbol;

    }



    function name() public view virtual returns (string memory) {

        return _name;

    }



    function symbol() public view virtual returns (string memory) {

        return _symbol;

    }



    function tokenURI(uint256 tokenId) public view virtual returns (string memory) {

        require(_owners[tokenId] != address(0), "Not exists owner");



        string memory baseURI = _baseURI();

        return bytes(baseURI).length > 0 ? string.concat(baseURI, tokenId.toString()) : "";

    }



    // Any contract extends the ERC721 contract could ovveride the function and set the baseURI value, default is empty string

    function _baseURI() internal view virtual returns (string memory) {

        return "";

    }



    function balanceOf(

        address owner

    ) external view override returns (uint256 balance) {

        return _balances[owner];

    }



    function ownerOf(

        uint256 tokenId

    ) external view override returns (address owner) {

        return _owners[tokenId];

    }



    function safeTransferFrom(

        address from,

        address to,

        uint256 tokenId,

        bytes calldata data

    ) external override {

        _checkOnERC721Received(from, to, tokenId, data);

        transferFrom(from, to, tokenId);

    }



    function safeTransferFrom(

        address from,

        address to,

        uint256 tokenId

    ) external override {

        safeTransferFrom(from, to, tokenId, "");

    }



    /**

     * Transfer ownership of an NFT

     * @param from

     * @param to

     * @param tokenId

     */

    function transferFrom(

        address from,

        address to,

        uint256 tokenId

    ) external override {

        // check if msg.sender has approved

        require(_checkPermission(from, msg.sender, tokenId), "msg.sender has no previleges to transfer");

        // update owner balance

        address prev = _update(to, tokenId, msg.sender);

        require(prev == from, "Incorrect owner");

    }



    // Approve permisson to `to` address for tokenId

    function approve(address to, uint256 tokenId) external override {

        _approve(to, tokenId, msg.sender);

    }



    function setApprovalForAll(

        address operator,

        bool approved

    ) external override {

        require(operator != address(0), "Invalid operator");

        _operatorApprovals[msg.sender][operator] = approved;

        emit ApprovalForAll(msg.sender, operator, approved);

    }



    function getApproved(

        uint256 tokenId

    ) external view override returns (address operator) {

        require(_owners[tokenId] != address(0), "Not exist owner");

        return _getApproved(tokenId);

    }



    function isApprovedForAll(

        address owner,

        address operator

    ) public view override returns (bool) {

        return _operatorApprovals[owner][operator];

    }



    function _update(address to, uint256 tokenId, address operator) internal virtual returns (address) {

        address from = _owners[tokenId];



        if (operator != address(0)) {

            _checkPermission(from, operator, tokenId);

        }



        if (from != address(0)) {

            unchecked {

                _balances[from] -= 1;

            }

        }



        if (to != address(0)) {

            unchecked {

                _balances[to] += 1;

            }

        }



        _owners[tokenId] = to;



        emit Transfer(from, to, tokenId);



        return from;

    }



    function _approve(address to, uint256 tokenId, address owner) internal virtual {

        require(_owners[tokenId] == owner, "Incorrect owner address")

        require(to != address(0), "Invalid to address");

        _approvals[tokenId] = to;

        emit Approval(owner, to, tokenId);

    }



    function _getApproved(uint256 tokenId) internal view virtual returns (address) {

        return approvals[tokenId];

    }



    /*

     * Check if owner approved for spender about the tokenId

     * @param owner

     * @param spender

     * @param tokenId

     */

    function _checkPermission(address owner, address operator, uint256 tokenId) internal returns (bool) {

        require(_owners[tokenId] == owner, "Incorrect owner");

        require(operator != address(0), "Illegal operator address");

        // If operator is owner, so operator must have the permission

        if (owner == operator) {

            return true;

        }



        // If operator is approved, return true

        if (_getApproved(tokenId) == operator) {

            return true;

        }



        return isApprovedForAll(owner, operator);

    }



    function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory data) internal {

        // check smart contract code length, EOA address length is zero, CA address length is greater than zero

        if (to.code.length > 0) {

            // if to address is contract address, see if implements the IERC721Receiver interface

            // if IERC721Receiver.onERC721Received return value is IERC721Receiver.onERC721Received.selector

            // it means the contract could operate NFT, such as transfer .etc

            try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, data) returns (bytes4 retval) {

                if (retval != IERC721Receiver.onERC721Received.selector) {

                    revert ERC721InvalidReceiver(to);

                }

            } catch (bytes memory reason) {

                if (reason.length == 0) {

                    revert ERC721InvalidReceiver(to);

                } else {

                    assembly {

                        revert(add(32, reason), mload(reason))

                    }

                }

            }

        }

    }



    /**

     * Get nft approver address by tokenId

     */

    function _getApproved(uint256 tokenId) internal view virtual returns (address) {

        return _approvals[tokenId];

    }



    /**

     * Mint nft to `to` address

     * @param to

     * @param tokenId

     */

    function _mint(address to, uint256 tokenId) internal {

        require(to != address(0), "Invalid mint address");

        unchecked {

            _balances[to] += 1;

        }

        _owners[tokenId] = to;



        emit Transfer(from, to, tokenId);

    }



    function _safeMint(address to, uint256 tokenId) internal {

        _safeMint(to, tokenId, "");

    }



    function _safeMint(address to, uint256 tokenId, bytes memory data) internal virtual {

        _mint(to, tokenId);

        _checkOnERC721Received(address(0), to, tokenId, data);

    }   

}

  • 24
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

莫言静好、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值