文章目录

想了想,还是决定开这个新坑吧。上一次玩合约,好像已经是19年的事情了。18年跟几位大神入了区块链这个神坑,受益匪浅。时隔数年,有好多东西都忘了。借着Axie Infinity这些NFTs概念游戏大火,来温故一番、也知新一下。

这里声明一下我的观点,区块链的前景还是可以说很大的,但是不是在炒币炒NFT炒概念上。必须承认这个目前能赚钱,但是除非能确保赚钱的是自己。我们国家的趋势是支持区块链应用,但是绝对反对用来做金融投机的。

我的另一个观点是,了解区块链,应用区块链,对个人绝对是有好处的。但是不要太乐观,从18年到现在,这个行业的实际应用到目前为止几乎没有发生实质性的实用性进展。不过就是从炒币变成了炒NFTs。这一堆数据在会操作的人手里是工具,但是在韭菜那里就是悬在头上的镰刀而已。

好了,胡扯了这么多,说回正题,来看Axie Infinity的合约吧。从白皮书的《智能合约与GitHub仓库》 Smart contracts and GitHub Repo 上可以看到,有一些完全公开的合约,也有一些并没有公开源代码但提供了查询地址的合约。我们先看公开的部分,初步进行了解,也帮助我自己确定自己都理解了。

Axie Infinity的核心资产是Axie,我们从 GitHub仓库公开合约 里可以找到,Core contract部分就是我们要找的东西。

目录结构是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
│  AxieAccessControl.sol
│ AxieCore.sol

├─dependency
│ AxieDependency.sol
│ AxieManager.sol
│ AxieManagerCustomizable.sol

├─erc721
│ AxieERC721.sol
│ AxieERC721BaseEnumerable.sol
│ AxieERC721Metadata.sol

└─lifecycle
AxiePausable.sol
AxieUpgradeable.sol

AxieCore.sol就是我们目前最需要的东西。现在我们顺着依赖往回理,看它是如何从最基础的协议一步步做过来的。这个文件头部写明:

1
2
3
4
5
6
7
8

pragma solidity ^0.4.19;


import "./erc721/AxieERC721.sol";


// solium-disable-next-line no-empty-blocks

所以我们再找到AxieERC721.sol

1
2
3
4
5
6
7
8
9
10
pragma solidity ^0.4.19;


import "./AxieERC721BaseEnumerable.sol";
import "./AxieERC721Metadata.sol";


// solium-disable-next-line no-empty-blocks
contract AxieERC721 is AxieERC721BaseEnumerable, AxieERC721Metadata {
}

然后我们又可以发现,这个作为核心依赖的合约,主要由两部分构成,AxieERC721BaseEnumerable.solAxieERC721Metadata.sol,从名称上我们可以推测,这两部分分别管理了基础数据(即直译过来的基础可枚举)和扩展属性(即直译过来的元数据)。通常在实现一个ERC721资产的时候,链上只会存储最基础的数据,例如作为先锋的CryptoKitties,链上只存储了基因数据和生育相关数据(因为要用于链上计算)。而更多的信息,例如名字、描述、图片信息、其他属性等等,则通过Metadata的方式存储在其他地方,例如IPFS或者自己指定的传统互联网地址等。

先来看AxieERC721BaseEnumerable.sol文件。

1
2
3
4
5
6
7
8
9
10
11
pragma solidity ^0.4.19;


import "zeppelin/contracts/math/SafeMath.sol";

import "../../erc/erc165/ERC165.sol";
import "../../erc/erc721/IERC721Base.sol";
import "../../erc/erc721/IERC721Enumerable.sol";
import "../../erc/erc721/IERC721TokenReceiver.sol";
import "../dependency/AxieDependency.sol";
import "../lifecycle/AxiePausable.sol";

好吧,我终于还是打算先说一句,虽然我相信来看文章的新人肯定很少,但以防万一嘛。pragma solidity ^0.4.19这一句是指定了solidity的版本,这个与js很相近的语言,一直在迭代更新,现在已经到了0.8.x的版本,虽然谈不上大相径庭,但是已经产生了很多不兼容的情况,甚至无法通过编译。所以熟悉和指定语言版本在这里很重要。

如果没记错,Axie说过在他们开发完成合约之前,EIP721还在草案阶段,所以在引入的这些IERC721xxx可以看到,跟Zeppelin这一类的合约库相比,有一些不同。但是这也不重要,因为他们功能其实基本一致。然后我们来看,他引用的这些类(erc165和erc721文件夹里的这些合约就不详细说了,现在已经成了ERC721标准,可以直接在Zeppelin引入):

SafeMath - 没有专门去弄清楚这个用于避免程序结果产生溢出的类到底是不是出自Zeppelin,但是他们的这个类库很出名也很有用的。这么有名的库为什么不重点说说呢?因为在最新的Solidity 0.8.x中,这个类库已经不需要了。

ERC165 - 一种发布并能检测到一个智能合约实现了什么接口的标准。我自己组织了很久语言,都没找到合适的描述方式,最后觉得这句听上去特别废话的话最能解释他的作用。可以看看这位的解析erc721-165学习,这句话就出自他。事实上我认为,能弄懂这个看起来很简单的标准,也就代表区块链基础知识已经完全过关了。

IERC721Base - 直接翻译过来,就是符合ERC721的合约基本需求的接口。包括了用户余额、Token持有者、转让、授权等等。

IERC721Enumerable - 这一个也有点不好归纳,通过内容和EIP721的描述,大概可以理解为作为查看整个合约的资产状况的接口。但似乎这里包含了更多内容,例如资产总量、根据索引查资产、根据索引查持有者、Token有效性验证、持有者有效性验证、以及一些修饰器。

IERC721TokenReceiver - 这个对应了ERC721的safeTransfer,用于在接收到safeTransferFrom完成的消息后,触发onERC721Received方法。

基本的合约和接口分析完了,接下来又是2个Axie自己的特有依赖AxieDependencyAxiePausable。为了防止无限套娃,这里就此打住,目前只需要知道,前者主要在规范整个合约的权限,后者可暂停代币的一切行为。这些以后再详说。只是这里再补一句,大家认为的完全去中心化,只是一厢情愿,所有区块链的应用,只可能在信任的基础上,一定程度上去中心化。

接下来看合约部分:

1
2
3
4
contract AxieERC721BaseEnumerable is ERC165, IERC721Base, IERC721Enumerable, AxieDependency, AxiePausable {
using SafeMath for uint256;

...

整个合约因为其实都是ERC721标准的实现,没有太多可讲的。重点说一下using ... for ...,通常是指将前者的函数用于后者。在这里,则是说把SafeMath的方法绑定到uint256类型上。需要注意的是,using之前,必须要先import

好了,来看另一个合约,AxieERC721Metadata,它引入了两个类库

1
2
3
4
5
6
...

import "../../erc/erc721/IERC721Metadata.sol";
import "./AxieERC721BaseEnumerable.sol";

...

很熟悉的身影是不是,没错,就是上面的AxieERC721BaseEnumerableAxieERC721Metadata合约通过继承前者,来实现了一些定义。这个文件内容不多,但是有很多特别有意思,也是对新人特别有用的地方。

先来看第一个:

1
2
3
4
5
6
7
...

function AxieERC721Metadata() internal {
supportedInterfaces[0x5b5e139f] = true; // ERC-721 Metadata
}

...

来看网上的这句说明: Note: the ERC-165 identifier for this interface is 0x5b5e139f. 是不是有点意思?还记得前面我提到的那篇文章和那句话吗?结合起来,看懂了,就明白ERC165在做什么了。

来看下一个点,

1
2
3
4
5
6
7
8
9
10
11
...

function name() external pure returns (string) {
return "Axie";
}

function symbol() external pure returns (string) {
return "AXIE";
}

...

这里的作用很简单,其实就是定义了Token的名字叫Axie, 代币符号为AXIE。但这不重要,因为重点是出现了一个很难在其他语言里看到的词,pure。我一开始接触到这个修饰器的时候,第一个想到了惜字如金的电报时代。因为每一个操作都是白花花的银子,所以能不操作的东西,就不要操作。于是在solidity中出现了这两个东西viewpure。其中view表示,对storage变量可读不可写,而pure则对storage既不可读也不可写。因为不写就代表不需要矿工验证,不需要gas,也就是不需要花钱。那说到storage,是不是还有个对应的东西?没错,0.4.x版本里模糊了这个概念,很多时候开发者不需要关心是什么(只是很多时候,因为后面我们就能看到声明)。但是0.8.x版本已经把这个权力交回到开发者手里,到底是storage还是memory,需要开发者自己来规划。有什么区别呢?是不是记得区块链最贵的操作是存储💴?但是结果需要大家自己去实践,不要省这点时间。

下一部分可以有两个地方注意:

1
2
3
4
5
6
7
8
...

function setTokenURIAffixes(string _prefix, string _suffix) external onlyCEO {
tokenURIPrefix = _prefix;
tokenURISuffix = _suffix;
}

...

第一个,我们一直没说的external,以及之前出现了的internal,实际上还有publicprivate。来个清晰的说明:

private - 只能在当前合约内访问,在继承合约中都不可访问。

internal - 函数只能通过内部访问(当前合约或者继承的合约),可在当前合约或继承合约中调用。

external - external标识的函数是合约接口的一部分。函数只能通过外部的方式调用。外部函数在接收大的数组时更有效。

public - public标识的函数是合约接口的一部分。可以通过内部,或者消息来进行调用。

除了以上解释以外,作为合约开发,还有一个必须扎根心底的概念,就是gas消耗。那么,如果你的函数仅仅需要外部调用,那么你应该用external,如果你的函数需要内部和外部同时调用,那么使用public。这个涉及到内存分配管理,我觉得也没必要深入了解,仅仅上面几句话足以判断4个可见性花费的递进关系了。

第二个,是onlyCEO,这个看起来很特别的修饰器,实际上是在AxieDependency所引入的另一个文件AxieAccessControl中定义的,它判断了消息发送者地址是否跟我们定义的ceo地址一致。有时候我们用的很少的判断,会直接在需要的地方require(msg.sender == ceoAddress),但是这种随时可能用到的判断,直接定义为修饰器会更方便。

到了最后一个方法了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
...

function tokenURI(
uint256 _tokenId
)
external
view
mustBeValidToken(_tokenId)
returns (string)
{
bytes memory _tokenURIPrefixBytes = bytes(tokenURIPrefix);
bytes memory _tokenURISuffixBytes = bytes(tokenURISuffix);
uint256 _tmpTokenId = _tokenId;
uint256 _length;

do {
_length++;
_tmpTokenId /= 10;
} while (_tmpTokenId > 0);

bytes memory _tokenURIBytes = new bytes(_tokenURIPrefixBytes.length + _length + 5);
uint256 _i = _tokenURIBytes.length - 6;

_tmpTokenId = _tokenId;

do {e
_tokenURIBytes[_i--] = byte(48 + _tmpTokenId % 10);
_tmpTokenId /= 10;
} while (_tmpTokenId > 0);

for (_i = 0; _i < _tokenURIPrefixBytes.length; _i++) {
_tokenURIBytes[_i] = _tokenURIPrefixBytes[_i];
}

for (_i = 0; _i < _tokenURISuffixBytes.length; _i++) {
_tokenURIBytes[_tokenURIBytes.length + _i - 5] = _tokenURISuffixBytes[_i];
}

return string(_tokenURIBytes);
}

...

这部分是这个合约的核心部分。它定义了扩展数据的网络地址,以便可以在访问ERC721资产的同时,得到一些诸如图片之类的额外数据。头部的mustBeValidToken(_tokenId)比较明显,是在基础类里定义的修饰器。

这里主要打算说的是另一个问题,一开始我想当然的觉得,把_tokenURIPrefixtokenURISuffix_tokenId组合起来的方式肯定是tokenURIPrefix + _tokenId + tokenURISuffix。当然,现在看起来这很蠢。因为作为一个处处花钱的语言,是不会轻易自动去做任何转换的。所以正确的方式,是把所有内容全部转换为字节(其实多看点代码还会发现,string并不是经常被使用的类型,相信原因都懂的),然后拼接起来。

劈里啪啦写了一堆,再一看时间,都凌晨5点了。下次继续吧。

♦ 本文固定连接:http://gsgundam.com/2021-10-31-axie-contract-how-to-1/

♦ 转载请注明:GSGundam 2021年10月31日发布于 GSGUNDAM砍柴工

♦ 本文版权归作者,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

♦ 原创不易,如果页面上有适合你的广告,不妨点击一下看看,支持作者。(广告来源:Google Adsense)