# 智能合约示例：运动挑战奖章 NFT

By [0xdoux.eth](https://paragraph.com/@0xdoux) · 2022-03-31

---

在上篇文章里，我们展示了如何发行 token，以及如何通过上传打卡数据来获得 token。在实际情况中，我们一般会设置一些打卡任务，比如每个月必须跑多少次，或者是爬几次山之类的，在本文里，此类情况称之为运动挑战。在完成了运动挑战后，可以给用户发放奖章，就像各类运动 App 里设计的那样。

这个奖章的需求就很适合用 NFT 来完成。NFT，全称非同质化代币，即每个代币都是唯一的。类比下现金，你手里的一块钱和别人手里的一块钱，即使不是同一个硬币，但是都是一块钱，是可以等价交换的，这个就是 token 的概念。但是你手里的一张毕业证，和你同学的毕业证，虽然都是毕业证，但是它们是不可同等交换的。所以即使对于一个项目，它一次发售了 1 个 NFT，但是因为每个都不一样，但是反映到价格上是不一样的。

所以 NFT 就很适合用来代替权益证明，证件啊、奖章啊之类的。只是现阶段大都玩成了小图片，\[摊手\]

需求分析
----

当用户完成一项挑战之后，要给 ta 发放一枚独特的奖章，每个人的都不一样。类似于 Apple Watch 的奖章体系，虽然奖章的证明是一样的，但是奖章的背面是有用户的名字的。

然后，还可以设计几个不同类别的奖项。如跑步的、登山的、游泳的，等等。根据完成的不同挑战给到不同的奖章。进一步还可以更个性化一些，比如对于登山，春天的和秋天的，是不是奖章的样式可以有所区分，等等。

合约设计
----

对于大部分的 NFT，就是一张图片，然后这张图片存储到 IPFS 之类的去中心化存储上。IPFS 可以理解为百度网盘，可以用来存储各类文件。除了图片本身，同时还会有一个 NFT 元数据的配置文件，里面会标记好如图片地址、分类属性等信息，通过解析这个配置文件，就可以解析出 NFT。这些文件是存储在 IPFS 上的，所以不用担心删除、丢失等问题。

然后，只需要在需要给用户发放奖章的时候，将用户的地址和奖章的 ID 做好绑定关系，同时将奖章 ID 和对应的配置文件绑定好关系，就可以了。这个关系就是通过合约存储在区块链上，所以，实现起来并不复杂。

稍微复杂一些的，应该就在于挖出什么样的图片了。有些项目是提前将图片全部按照程序生成好，然后挖的时候开盲盒；或者也可以在挖的时候再去随机生成。同时只要将属性细化，准备对应的模版，通过预言机来获取当前的天气、位置等信息，生成的结果可以更加的个性化。

合约实现
----

和 Token 类似，NFT 的规范是 ERC721，里面也定义了一系列需要实现的接口，只要实现了这些接口，就是 NFT。

它包括：

    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);
    
    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);
    

和 Token 的比较类似，关于 Token 的详细规范要求，可以看上篇文章。同时，这里面没有列出如 `name`、`symbol` 的属性要求，这两个属性的实现和 Token 一致，本文不做赘述。本文重点放在 NFT 和用户的关系绑定上

首先，定义好保存绑定关系的字段

    mapping(address => uint256) private balances;
    mapping(uint256 => address) private owners;
    mapping(uint256 => string) private metas;
    

NFT 的 ID 必须是唯一的，因为合约地址是唯一的，所以只需要通过合约地址和 NFT 地址，就能标记出全网唯一。这个 ID 可以是 UUID，可以是 Sha，也可以更简单的使用 1、2、3、4 这样的数字，在本文里，我们使用不断增长数字来标记 ID，每个用户挖出 NFT 后，就将这个 ID 加 1。

一个用户有多少个 NFT，在 `balances` 里保存。对于一个给定的 NFT ID，它属于谁，在 `owners` 里保存，它的属性数据保存在 `metas` 里。对应的数据获取接口如下：

    function balanceOf(address _owner) public view returns (uint256) {
        return balances[_owner];
    }
    
    function balanceOfMe() public view returns (uint256) {
        return balances[msg.sender];
    }
    
    function ownerOf(uint256 tokenId) public view returns (address) {
        return owners[tokenId];
    }
    
    function tokenUri(uint256 _tokenId) public view returns (string memory) {
        return metas[_tokenId];
    }
    

设置属性数据的接口如下：

    function _setTokenUri(uint256 _tokenId, string memory _tokenUri) public {
        metas[_tokenId] = _tokenUri;
    }
    

挖 NFT，简单来说其实就是生成一个新的奖章 ID，给它绑定好属性属性，然后将这个 ID 和用户的地址做好绑定

    function _mintNFT(string memory _tokenUri) public returns (uint256) {
        require(bytes(_tokenUri).length != 0);
        _curItemId += 1;
        owners[_curItemId] = msg.sender;
        metas[_curItemId] = _tokenUri;
        balances[msg.sender] += 1;
    
        return _curItemId;
    }
    

具体生成外部的属性信息的过程就不做展示了，可以在合约里完成，也可以在外部程序上完成。在用户完成了挑战后，来调用上面这个接口就可以了

接下来就是 NFT 的转移，转移的过程简单来说就是将卖家的 `balance` 减 1，将买家的 `balance` 加 1，将这个 NFT 的 ID 的绑定关系有卖家转绑为买家

    event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
    
    function transferFrom(address _from, address _to, uint256 _tokenId) public {
        require(owners[_tokenId] == _from, "no permission");
        require(_to != address(0));
        balances[_from] -= 1;
        balances[_to] += 1;
        owners[_tokenId] = _to;
        emit Transfer(_from, _to, _tokenId);
    }
    

总结
--

对于现在的 NFT 项目，一般都会设置一个总供给，只需要在挖的接口里做好校验即可，同时还可以使用一个字段来标记当前还有多少未挖出，用来在前端用户界面上进行展示。

上述的示例中，并未完全完成 ERC721 的接口要求，但参照上面的实现方法，也是可以很容易的实现。和 Token 的实现一样，一般项目中我们不会手动的来实现这些接口，可以通过使用 OpenZepplion 来快速实现。

对于 NFT 项目，编写合约并不是难事儿，完全自动化的完成都是可行的，更重要的还在于产品的运营能力，市面上这么多的 NFT，为什么价格差异巨大？有的可以一路上涨，而有的则是破发。合约上并没有什么差异，还在于运营的能力。

最后，希望后面能看到更多 NFT 不同的玩法，如果只是玩小图片，那从业者的想象力太有限了。

---

*Originally published on [0xdoux.eth](https://paragraph.com/@0xdoux/nft)*
