迷恋猫CryptoKitties源码分析

CryptoKitties源码可以在这里查看:
https://etherscan.io/address/0x06012c8cf97bead5deae237070f9587f8e7a266d#code

源码一共有2000多行,合约共16个。如果是做应用开发的,看了CryptoKitties简介之后应该就能预估有哪些合约了,如果了解一些DApp,那看代码之前就能有个大概的了解
我们可以先想下,如果自己要开发一个这样的DApp,会有哪些模块:

  • ERC721及接口实现
  • 权限管理
  • 猫咪相关:基本属性、饲养、出售等行为

具体对应到合约源码就是如下的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# ERC721及接口实现
contract ERC721
contract ERC721Metadata

# 权限管理
contract Ownable
contract Pausable is Ownable
contract KittyAccessControl
contract KittyOwnership is KittyBase, ERC721

+ 猫咪相关:基本属性、饲养、出售等行为
contract GeneScienceInterface
contract ClockAuctionBase
contract KittyBase is KittyAccessControl
contract ClockAuction is Pausable, ClockAuctionBase
contract SiringClockAuction is ClockAuction
contract SaleClockAuction is ClockAuction
contract KittyAuction is KittyBreeding
contract KittyMinting is KittyAuction
contract KittyBreeding is KittyOwnership
contract KittyCore is KittyMinting

下面我们来分别介绍下各个合约的作用,合约代码较长,完整合约可以查看源码。对于一些简单的合约我们只介绍下功能即可,其余合约也只截取了重要部分内容进行讲解

contract Ownable

经常写合约的都知道,合约一般都会有一个Ownable,提供给其它合约继承,以实现对合约的访问限制

contract ERC721

请移步 以太坊标准ERC20和ERC721

GeneScienceInterface

根据父母的基因计算子代的基因,具体计算方法未公布

contract KittyAccessControl

权限管理,该合约定义了三种角色,分别拥有不同的操作权限
CEO:可以重新分配角色,改变我们依赖的智能合约,也是唯一一个可以激活合约的角色
CFO:可以提取合约中的余额
COO:可以释放0代猫咪,0代猫咪的数量随着智能合约部署就被限定,并且该角色可以修改。
三个角色中任意一个都可以暂停合约
如果合约出现Bug,或不可抗拒的因素需要暂停,以上三个角色都可以执行暂停合约的操作

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
contract KittyAccessControl {

address public ceoAddress;
address public cfoAddress;
address public cooAddress;


modifier onlyCLevel() {
require(
msg.sender == cooAddress ||
msg.sender == ceoAddress ||
msg.sender == cfoAddress
);
_;
}

// 三个角色中任意一个都可以暂停合约
function pause() external onlyCLevel whenNotPaused {
paused = true;
}

// 只有COO
function unpause() public onlyCEO whenPaused {
// can't unpause if contract was upgraded
paused = false;
}
}

contract KittyBase is KittyAccessControl

该合约定义了猫咪的各种属性、出生日期、父代ID等信息,以及猫咪相关的行为事件

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
contract KittyBase is KittyAccessControl {

event Transfer(address from, address to, uint256 tokenId);

uint256 public secondsPerBlock = 15;

struct Kitty {
uint256 genes; // 猫咪的基因,决定了猫咪的各种特征
uint64 birthTime; // 猫咪出生的日期

// 猫咪可以再次进行繁殖的最小区块。一只刚出生的猫咪cooldownEndBlock=0,当猫咪需要生产时,需要先判断cooldownEndBlock,只有cooldownEndBlock小于等于当前区块编号时才能繁殖
uint64 cooldownEndBlock;

uint32 matronId; // 猫妈妈的ID
uint32 sireId; // 猫爸爸的ID

//未繁殖的猫咪siringWithId=0,繁殖后的猫咪siringWithId=父代ID
uint32 siringWithId;

// 繁殖冷却时间,随着繁殖次数的增加,冷却时间也会增加,初始冷却时间为:_generation/2,对应的具体冷却时间可以查看cooldowns变量
uint16 cooldownIndex; // 繁殖冷却时间

uint16 generation; // 猫咪的代数
}

// 保存所有的猫咪
Kitty[] kitties;

//每只猫咪对应的主人,当猫咪的主人发生变化时,对应的映射关系也会变化
mapping (uint256 => address) public kittyIndexToOwner;

// 拥有猫咪的数量
mapping (address => uint256) ownershipTokenCount;

// 准备出售的猫咪 <--> 拥有者
mapping (uint256 => address) public kittyIndexToApproved;

// 交配的猫咪 <--> 拥有者
mapping (uint256 => address) public sireAllowedToAddress;

// 猫咪对应拍卖的合约
SaleClockAuction public saleAuction;

// 猫咪对应繁殖的合约
SiringClockAuction public siringAuction;

// 从_from拥有者,将id为_tokenId的猫猫转移到_to的新拥有者
// _from为0时,表明初代猫生成
function _transfer(address _from, address _to, uint256 _tokenId) internal {
// 增加新拥有者猫猫的数量
ownershipTokenCount[_to]++;
// 变更猫猫的新主人为_to
kittyIndexToOwner[_tokenId] = _to;
// 判断_from地址是否为空
if (_from != address(0)) {
// 如果不为空,_from原拥有者的猫猫数量减一
ownershipTokenCount[_from]--;
// 删除这个猫猫的出售信息
delete sireAllowedToAddress[_tokenId];
// 删除这个猫猫的交配信息
delete kittyIndexToApproved[_tokenId];
}
// 事件记录
Transfer(_from, _to, _tokenId);
}

// 生成一个新的猫猫
// _matronId、_sireId父母id
// _generation 代数
// _genes 基因
// _owner 猫猫拥有者
// 返回新猫猫id
function _createKitty(
uint256 _matronId,
uint256 _sireId,
uint256 _generation,
uint256 _genes,
address _owner
)
internal
returns (uint)
{
// 新的猫猫必须包含父母id和代数信息
require(_matronId == uint256(uint32(_matronId)));
require(_sireId == uint256(uint32(_sireId)));
require(_generation == uint256(uint16(_generation)));

// 更换_generation代数信息确定猫猫初始冷却交配时间,最大值为13(13对应7天)
uint16 cooldownIndex = uint16(_generation / 2);
if (cooldownIndex > 13) {
cooldownIndex = 13;
}

// 生成一个猫猫的基本属性
Kitty memory _kitty = Kitty({
genes: _genes,
birthTime: uint64(now),
cooldownEndBlock: 0,
matronId: uint32(_matronId),
sireId: uint32(_sireId),
siringWithId: 0,
cooldownIndex: cooldownIndex,
generation: uint16(_generation)
});
// 将新的猫咪放入kitties中
// 猫咪的id等于kitties数组中的顺序编号
uint256 newKittenId = kitties.push(_kitty) - 1;

require(newKittenId == uint256(uint32(newKittenId)));

// 事件记录
Birth(
_owner,
newKittenId,
uint256(_kitty.matronId),
uint256(_kitty.sireId),
_kitty.genes
);

// 给_owner分配新猫猫newKittenId
_transfer(0, _owner, newKittenId);

return newKittenId;
}

// CEO COO CFO 可以修改区块生成时间
function setSecondsPerBlock(uint256 secs) external onlyCLevel {
require(secs < cooldowns[0]);
secondsPerBlock = secs;
}
}

contract ERC721Metadata

该合约中只有一个方法,根据不同的tokenId返回特定的字符串和长度

contract KittyOwnership is KittyBase, ERC721

该合约继承KittyBase和ERC721,实现了ERC721规范中的方法,并定义了一些合约的单位和名称,以及一些常用的校验方法

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
contract KittyOwnership is KittyBase, ERC721 {

// 整个智能合约的名字为CryptoKitties
string public constant name = "CryptoKitties";
// 猫猫代币的单位为CK
string public constant symbol = "CK";

// The contract that will return kitty metadata
// 合约的元数据
ERC721Metadata public erc721Metadata;

// ERC165接口的加密byte = 0x01ffc9a7
bytes4 constant InterfaceSignature_ERC165 =
bytes4(keccak256('supportsInterface(bytes4)'));

// ERC721接口的加密byte = 0x9a20483d
bytes4 constant InterfaceSignature_ERC721 =
bytes4(keccak256('name()')) ^
bytes4(keccak256('symbol()')) ^
bytes4(keccak256('totalSupply()')) ^
bytes4(keccak256('balanceOf(address)')) ^
bytes4(keccak256('ownerOf(uint256)')) ^
bytes4(keccak256('approve(address,uint256)')) ^
bytes4(keccak256('transfer(address,uint256)')) ^
bytes4(keccak256('transferFrom(address,address,uint256)')) ^
bytes4(keccak256('tokensOfOwner(address)')) ^
bytes4(keccak256('tokenMetadata(uint256,string)'));

// 验证是否实现了ERC721和ERC165
//
function supportsInterface(bytes4 _interfaceID) external view returns (bool)
{
return ((_interfaceID == InterfaceSignature_ERC165) || (_interfaceID == InterfaceSignature_ERC721));
}

/// @dev Set the address of the sibling contract that tracks metadata.
/// CEO only.
function setMetadataAddress(address _contractAddress) public onlyCEO {
erc721Metadata = ERC721Metadata(_contractAddress);
}

// 判断_tokenId的猫猫是否归_claimant地址用户所有
function _owns(address _claimant, uint256 _tokenId) internal view returns (bool) {
return kittyIndexToOwner[_tokenId] == _claimant;
}

// 判断_tokenId的猫猫是否可以被_claimant的地址用户进行转让
function _approvedFor(address _claimant, uint256 _tokenId) internal view returns (bool) {
return kittyIndexToApproved[_tokenId] == _claimant;
}

// 允许_tokenId的猫猫可以被_approved的地址用户执行transferFrom()方法
function _approve(uint256 _tokenId, address _approved) internal {
kittyIndexToApproved[_tokenId] = _approved;
}

// 返回_ownerd拥有的猫猫token个数
function balanceOf(address _owner) public view returns (uint256 count) {
return ownershipTokenCount[_owner];
}

// 将_tokenId的猫猫转移给_to地址拥有者
// 当系统没有处于暂停状态时
function transfer(
address _to,
uint256 _tokenId
)
external
whenNotPaused
{
// 检查一下_to的地址是否合法
require(_to != address(0));
// 不允许将猫猫转移给本合约地址
require(_to != address(this));
// 不予许将猫猫转移给拍卖合约地址
require(_to != address(saleAuction));
// 不允许将猫猫转移给交配合约地址
require(_to != address(siringAuction));

// 你只能发送_tokenId为你你自己拥有的猫猫
require(_owns(msg.sender, _tokenId));

// 事件记录
_transfer(msg.sender, _to, _tokenId);
}

// 授权其他人将_tokenId为自己拥有的猫猫调用transferFrom()转移给地址为_to的拥有者
// 当系统处于非暂停状态
function approve(
address _to,
uint256 _tokenId
)
external
whenNotPaused
{
// 只有猫猫的拥有者可以授权其他人
require(_owns(msg.sender, _tokenId));

// 修改_approve()方法修改kittyIndexToApproved[_tokenId]
_approve(_tokenId, _to);

// 事件记录
Approval(msg.sender, _to, _tokenId);
}

// 将_from用户的猫猫_tokenId转移给_to用户
// 当系统处于非暂停状态
function transferFrom(
address _from,
address _to,
uint256 _tokenId
)
external
whenNotPaused
{
// 检查一下_to的地址是否合法
require(_to != address(0));
// 不允许将猫猫转移给本合约地址
require(_to != address(this));
// 检查msg.sender是否获得了授权转移_tokenId的毛毛啊
require(_approvedFor(msg.sender, _tokenId));
// 检查_from是否拥有_tokenId猫猫
require(_owns(_from, _tokenId));

// 调用_transfer()进行转移
_transfer(_from, _to, _tokenId);
}

// 返回目前所有的猫猫个数
function totalSupply() public view returns (uint) {
return kitties.length - 1;
}

// 返回_tokenId猫猫的拥有者的地址
function ownerOf(uint256 _tokenId)
external
view
returns (address owner)
{
owner = kittyIndexToOwner[_tokenId];

require(owner != address(0));
}

// 返回_owner拥有的所有猫猫的id数组
function tokensOfOwner(address _owner) external view returns(uint256[] ownerTokens) {
// 获得_owner拥有的猫猫数量
uint256 tokenCount = balanceOf(_owner);

// 判断数量是否为0
if (tokenCount == 0) {
// 如果该_owner没有猫猫,返回空数组
return new uint256[](0);
} else {
// 如果该_owner有
// 声明并初始化一个返回值result,长度为tokenCount
uint256[] memory result = new uint256[](tokenCount);
// 当前所有的猫猫数量
uint256 totalCats = totalSupply();
// 循环的初始值
uint256 resultIndex = 0;

// 所有的猫都有ID从1增加到totalCats
uint256 catId;

// 从1开始循环遍历所有的totalCats
for (catId = 1; catId <= totalCats; catId++) {
// 判断当前catId的拥有者是否为_owner
if (kittyIndexToOwner[catId] == _owner) {
// 如果是,将catId放入result数组resultIndex位置
result[resultIndex] = catId;
// resultIndex加1
resultIndex++;
}
}

// 返回result
return result;
}
}

// 拷贝方法
function _memcpy(uint _dest, uint _src, uint _len) private view {
// Copy word-length chunks while possible
for(; _len >= 32; _len -= 32) {
assembly {
mstore(_dest, mload(_src))
}
_dest += 32;
_src += 32;
}

// Copy remaining bytes
uint256 mask = 256 ** (32 - _len) - 1;
assembly {
let srcpart := and(mload(_src), not(mask))
let destpart := and(mload(_dest), mask)
mstore(_dest, or(destpart, srcpart))
}
}

// 将_rawBytes中长度为_stringLength转成string并返回
function _toString(bytes32[4] _rawBytes, uint256 _stringLength) private view returns (string) {
var outputString = new string(_stringLength);
uint256 outputPtr;
uint256 bytesPtr;

assembly {
outputPtr := add(outputString, 32)
bytesPtr := _rawBytes
}

_memcpy(outputPtr, bytesPtr, _stringLength);

return outputString;
}

// 返回指向该元数据的元数据包的URI
function tokenMetadata(uint256 _tokenId, string _preferredTransport) external view returns (string infoUrl) {
require(erc721Metadata != address(0));
bytes32[4] memory buffer;
uint256 count;
(buffer, count) = erc721Metadata.getMetadata(_tokenId, _preferredTransport);

return _toString(buffer, count);
}
}

contract KittyBreeding is KittyOwnership

两只猫咪繁殖会调用breedWithAuto()方法,然后进行一些列条件检查,繁殖费用是否足够、猫咪和主人的关系是否正常、是否在冷却期,在调用内部函数_breedWith()进行繁殖,然后会触发_triggerCooldown()改变下次冷却时间,并从可繁殖队列中删除,最后调用Pregnant()事件,触发giveBirth()方法产生新的猫咪

contract ClockAuctionBase

拍卖相关的基础函数,创建一个拍卖,取消拍卖,竞价等操作

contract Pausable is Ownable

暂定、开始拍卖合约,仅拍卖合约发起者能操作

contract ClockAuction is Pausable, ClockAuctionBase

拍卖相关的函数,创建一个拍卖,取消拍卖,竞价等操作

contract SiringClockAuction is ClockAuction

繁殖拍卖

contract SaleClockAuction is ClockAuction

初代宠物的拍卖,每15分钟执行一次,以及交易竞拍

contract KittyAuction is KittyBreeding

交易拍卖/初代猫咪竞拍(SaleClockAuction)和繁殖竞拍(SiringClockAuction)功能,包含了修改拍卖合约地址(仅CEO),修改繁殖合约地址(仅CEO),创建竞拍合约,创建繁殖合约。至于为什么这么做,开发者解释说是因为拍卖和繁殖合约总会有出现bug的风险,这样做的好处就是可以通过升级来解决问题而不改变猫咪的所有权

contract KittyMinting is KittyAuction

创世猫工厂,负责生产猫咪,一共包含5000只营销猫咪,用于项目上线后的赠送,4500只初代猫咪,生成后进入拍卖环节

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
contract KittyMinting is KittyAuction {

// 营销猫咪上线5000只,初代猫咪上线4500只
uint256 public constant PROMO_CREATION_LIMIT = 5000;
uint256 public constant GEN0_CREATION_LIMIT = 45000;

// 初代猫咪起始价格10 finney,拍卖时间1天
uint256 public constant GEN0_STARTING_PRICE = 10 finney;
uint256 public constant GEN0_AUCTION_DURATION = 1 days;

// Counts the number of cats the contract owner has created.
uint256 public promoCreatedCount;
uint256 public gen0CreatedCount;

// 赠送营销猫咪,仅限COO操作
function createPromoKitty(uint256 _genes, address _owner) external onlyCOO {
address kittyOwner = _owner;
if (kittyOwner == address(0)) {
kittyOwner = cooAddress;
}
require(promoCreatedCount < PROMO_CREATION_LIMIT);

promoCreatedCount++;
_createKitty(0, 0, 0, _genes, kittyOwner);
}

// 生成初代猫咪,并进行拍卖,仅限COO操作
function createGen0Auction(uint256 _genes) external onlyCOO {
require(gen0CreatedCount < GEN0_CREATION_LIMIT);

uint256 kittyId = _createKitty(0, 0, 0, _genes, address(this));
_approve(kittyId, saleAuction);

saleAuction.createAuction(
kittyId,
_computeNextGen0Price(),
0,
GEN0_AUCTION_DURATION,
address(this)
);

gen0CreatedCount++;
}

/// 计算猫咪的价格,最近5只的拍卖价格平均值+50%
function _computeNextGen0Price() internal view returns (uint256) {
uint256 avePrice = saleAuction.averageGen0SalePrice();

// Sanity check to ensure we don't overflow arithmetic
require(avePrice == uint256(uint128(avePrice)));

uint256 nextPrice = avePrice + (avePrice / 2);

// We never auction for less than starting price
if (nextPrice < GEN0_STARTING_PRICE) {
nextPrice = GEN0_STARTING_PRICE;
}

return nextPrice;
}
}

KittyCore is KittyMinting

CryptoKitties的核心合约,它继承了上述和猫咪相关的所有合约。
这种将主合约和业务逻辑的合约分开的做法是经常会用到的,因为一般涉及到业务逻辑的代码出现bug的几率会更高,而程序的合约地址是不可变更的,如果出现Bug,可以通过修改部分合约的合约地址来修复Bug及代码升级、甚至改变合约规则。

合约看完后,可能你会有个疑问:猫咪的图片保存在哪?是的,尽管合约是在去中心化的以太坊中运行,但是DApp的运行并不是完全去中心化的,猫咪的图片是和猫咪的基因绑定的,映射关系仍然保存在数据库中。

JouyPub wechat
欢迎订阅「K叔区块链」 - 专注于区块链技术学习