CryptoKitties智能合约剖析

本文为Nazar Ilamanov对CryptoKitties的智能合约代码的深入分析,主要内容包括: 加密猫简介、合约代码解读、发展时间线以及Nazar对代码的看法。

cryptokitties smart contract

CryptoKitties 项目对我来说一直很有趣。这是有史以来第一个流行的 NFT 游戏。但我不明白它的吸引力,为什么它会爆炸。 我也从来没有完全理解游戏的动态。所以我决定最终了解这款游戏的内容以及它是如何在幕后实现的。

我很清楚 NFT 部分是作为 ERC-721 实现的。但我想了解育种是如何实施的。什么是链上与链下的?当我开始探索这款游戏时, 我了解到它有一个拍卖机制。这是如何实施的?

人们实际上从这件事上赚了多少钱?人家还在玩吗?还是被更成功的 Axie Infinity 完全取代?

在本文中,我们将回答所有这些问题并分解 CryptoKitties 背后的智能合约。以下是这篇文章的大纲:

  • 1、CryptoKitties简介

起初,我想对 Axie Infinity(AI) 进行分解。但AI并没有开源他们的大部分智能合约 (SC)。 所以我转向 CryptoKitties(CK),因为它的合约是公开的。这是具有类似的游戏动态的Axie Infinity的简化版本:

  • 基于区块链的Play-to-Earn游戏
  • 人们收集和交易小猫
  • 可以配对两只小猫以获得新的小猫
  • 通过出售你的小猫或出租它们进行繁殖来赚取真正的 ETH
  • 基于“基因”的育种工作。小猫混合了父母的基因
  • 小猫没有性别
  • CK 通过在其市场中收取佣金(拍卖师费用)和铸造新的小猫来赚钱
  • 要进入游戏,你需要购买一些小猫

下面是 CK 游戏的视频:

cryptokitties smart contract

Axie Infinity 的灵感来自 CryptoKitties、Pokemon(用于战斗)和后来的 Clash of Clans(用于土地)。 更多关于Axie Infinity的起源和商业方面的信息可以访问这里

2、加密猫的代码结构

CryptoKitties 有 3 个智能合约:Core、Breeding 和 Auction。

cryptokitties smart contract

源代码链接如下:

3、加密猫的核心合约

核心合约被分解为许多子合约:KittyBase合约继承/扩展KittyAccessControl合约、KittyOwnership扩展KittyBase等, 将KittyCore所有内容组合在一起。

cryptokitties smart contract

  • KittyAccessControl:创建 3 个角色:CEO、CFO、COO,并将某些功能的访问权限限制为这些角色。CEO 可以重新分配角色, 更改指向同级合约的指针。CFO 可以提取资金。首席运营官可以铸造新的小猫。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
contract KittyAccessControl {
address public ceoAddress;
address public cfoAddress;
address public cooAddress;

modifier onlyCEO() {
require(msg.sender == ceoAddress);
_;
}
// ...same functions for CFO and COO

function setCFO(address _newCFO) external onlyCEO {
require(_newCFO != address(0));
cfoAddress = _newCFO;
}
// ...same functions for CEO and COO
}
  • KittyBase: kitties 的数据结构;存储所有小猫和所有权信息;所有权转让。
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
contract KittyBase is KittyAccessControl {
struct Kitty {
uint256 genes; // genetic code packed into 256 bits
uint64 birthTime;
uint64 cooldownEndBlock; // cooldown after breeding
uint32 matronId; // mom's ID
uint32 sireId; // dad's ID
uint32 siringWithId; // if set then pregnant
uint16 cooldownIndex; // like age. bigger it is, longer is the cooldown
uint16 generation;
}

Kitty[] kitties; // all kitties in existence. Index in the array is the ID
mapping (uint256 => address) public kittyIndexToOwner; // owners of kitties
mapping (address => uint256) ownershipTokenCount; // number of kitties an address owns

function _transfer(address _from, address _to, uint256 _tokenId) internal {
ownershipTokenCount[_to]++;
kittyIndexToOwner[_tokenId] = _to;
if (_from != address(0)) { // when creating new kittens _from is 0x0
ownershipTokenCount[_from]--;
}
}

function _createKitty(uint256 _matronId, uint256 _sireId, uint256 _generation,
uint256 _genes, address _owner) internal returns (uint) {
// add a new Kitty object to kitties array
}
}
  • KittyOwnership:ERC-721接口的实现。我在BAYC 智能合约分解 中解释了 ERC-721 的实现。如果你有兴趣,请查看它。

你知道 CryptoKitties开创了ERC-721 标准并创造了 NFT 这个术语吗?

  • KittyBreeding:将在“育种”部分进行说明。
  • KittyAuction:将在“拍卖”部分进行说明。
  • KittyMinting:只能铸造 50K 小猫——5K 是促销小猫,其余的是普通的 gen0 小猫。(区别:promo 可以在铸币时转移到特定地址, 普通 gen0 只能拍卖)。在铸造过程中可以指定任何基因。
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
contract KittyMinting is KittyAuction {
uint256 public constant PROMO_CREATION_LIMIT = 5000;
uint256 public constant GEN0_CREATION_LIMIT = 45000;

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;

function createPromoKitty(uint256 _genes, address _owner) external onlyCOO {
address kittyOwner = _owner;
if (kittyOwner == address(0)) {
kittyOwner = cooAddress; // default to COO address
}
require(promoCreatedCount < PROMO_CREATION_LIMIT);
promoCreatedCount++;
_createKitty(0, 0, 0, _genes, kittyOwner);
}

// Creates a new gen0 kitty and an auction for it.
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++;
}
}
  • KittyCore:将所有内容联系在一起,添加付款/取款,并处理可升级性 - 将在本文后面介绍。
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
contract KittyCore is KittyMinting {
function KittyCore() public {
paused = true;

// the creator of the contract is the initial CEO and COO
ceoAddress = msg.sender;
cooAddress = msg.sender;

// start with the mythical kitten 0 - so we don't have generation-0 parent issues
_createKitty(0, 0, 0, uint256(-1), address(0));
}

/// @notice No tipping!
/// @dev Reject all Ether from being sent here, unless it's from one of the
/// two auction contracts. (Hopefully, we can prevent user accidents.)
function() external payable {
require(
msg.sender == address(saleAuction) ||
msg.sender == address(siringAuction)
);
}

function withdrawBalance() external onlyCFO {
uint256 balance = this.balance;
// Subtract all the currently pregnant kittens we have, plus 1 of margin.
uint256 subtractFees = (pregnantKitties + 1) * autoBirthFee;

if (balance > subtractFees) {
cfoAddress.send(balance - subtractFees);
}
}
}

以太坊 101 中的支付: - 接受付款,只需将你的函数声明为payable。msg.value变量包含已发送的金额 - 要将付款发送到地址,只需使用address.send.

4、加密猫的育种合约

育种逻辑KittyBreeding在核心合约的子合约中实现。

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
contract KittyBreeding is KittyOwnership {
uint256 public autoBirthFee = 2 finney; // autoBirth is explained in the article
uint256 public pregnantKitties; // number of pregnant kitties

function _isReadyToBreed(Kitty _kit) internal view returns (bool) {
return (_kit.siringWithId == 0) && (_kit.cooldownEndBlock <= uint64(block.number));
}
function _isReadyToGiveBirth(Kitty _matron) private view returns (bool) {
return (_matron.siringWithId != 0) && (_matron.cooldownEndBlock <= uint64(block.number));
}

function _triggerCooldown(Kitty storage _kitten) internal {
// Compute an estimation of the cooldown time in blocks (based on current cooldownIndex).
_kitten.cooldownEndBlock = uint64((cooldowns[_kitten.cooldownIndex]/secondsPerBlock) + block.number);
if (_kitten.cooldownIndex < 13) {
_kitten.cooldownIndex += 1;
}
}

function approveSiring(address _addr, uint256 _sireId) external whenNotPaused {
require(_owns(msg.sender, _sireId));
sireAllowedToAddress[_sireId] = _addr;
}

function _isSiringPermitted(uint256 _sireId, uint256 _matronId) internal view returns (bool) {
address matronOwner = kittyIndexToOwner[_matronId];
address sireOwner = kittyIndexToOwner[_sireId];
return (matronOwner == sireOwner || sireAllowedToAddress[_sireId] == matronOwner);
}

function _isValidMatingPair(Kitty storage _matron, uint256 _matronId, Kitty storage _sire, uint256 _sireId)
private view returns(bool) {
// A Kitty can't breed with itself!
if (_matronId == _sireId) {
return false;
}
// Kitties can't breed with their parents.
if (_matron.matronId == _sireId || _matron.sireId == _sireId) {
return false;
}
if (_sire.matronId == _matronId || _sire.sireId == _matronId) {
return false;
}
// We can short circuit the sibling check (below) if either cat is
// gen zero (has a matron ID of zero).
if (_sire.matronId == 0 || _matron.matronId == 0) {
return true;
}
// Kitties can't breed with full or half siblings.
if (_sire.matronId == _matron.matronId || _sire.matronId == _matron.sireId) {
return false;
}
if (_sire.sireId == _matron.matronId || _sire.sireId == _matron.sireId) {
return false;
}
// Everything seems cool! Let's get DTF.
return true;
}

function _breedWith(uint256 _matronId, uint256 _sireId) internal {
Kitty storage sire = kitties[_sireId];
Kitty storage matron = kitties[_matronId];

matron.siringWithId = uint32(_sireId);

_triggerCooldown(sire);
_triggerCooldown(matron);

pregnantKitties++;
}

// Anyone can call this function (if they are willing to pay the gas!),
// but the new kitten always goes to the mother's owner.
function giveBirth(uint256 _matronId) external whenNotPaused returns(uint256) {
Kitty storage matron = kitties[_matronId];
require(_isReadyToGiveBirth(matron));
uint256 sireId = matron.siringWithId;
Kitty storage sire = kitties[sireId];

// Determine the higher generation number of the two parents
uint16 parentGen = matron.generation;
if (sire.generation > matron.generation) {
parentGen = sire.generation;
}

// Call the sooper-sekret gene mixing operation.
uint256 childGenes = geneScience.mixGenes(matron.genes, sire.genes, matron.cooldownEndBlock - 1);

// Make the new kitten!
address owner = kittyIndexToOwner[_matronId];
uint256 kittenId = _createKitty(_matronId, matron.siringWithId, parentGen + 1, childGenes, owner);

// Clear the reference to sire from the matron (REQUIRED! Having siringWithId
// set is what marks a matron as being pregnant.)
delete matron.siringWithId;

pregnantKitties--;

// Send the balance fee to the person who made birth happen.
msg.sender.send(autoBirthFee);

return kittenId;
}
}
  • 首先,有一堆辅助函数,如isReadyToBreed, isSiringPermitted,isValidMatingPair等。
  • 然后有 2 个函数实际上是在进行育种。breedWith开始繁殖过程并giveBirth结束它。giveBirth只有在妊娠期完成后,调用才会成功。

5、助产士和autoBirth

我们可以假设当有人开始繁殖时,breedWith被从CK的前端调用。但是giveBirth是如何被调用呢?Solidity中没有回调或cron作业。 所以将来需要有人调用giveBirth。

这就是助产士的用武之地。CK 有一个autoBirth守护程序网络,它们在正确的时间调用giveBirth。任何人都可以设置守护进程。

但是调用giveBirth要花gas。为什么守护进程会为其他人支付gas?这就是autoBirthFee进来的地方。当玩家开始繁殖时, 他需要支付autoBirthFee给CK(目前0.04 ETH)。CK 稍后将在他调用giveBirth时补偿守护进程。

6、超密遗传组合算法

你可能注意到该giveBirth函数调用mixGenes函数来获取子基因。该mixGenes函数实际上是名为GeneScience(源代码:v1和v2)的 同级合约的一部分。KittyBreeding只存储一个指向GeneScience合约的指针。

1
2
3
4
5
6
7
8
9
10
11
contract KittyBreeding is KittyOwnership {
// The address of the sibling contract that is used to implement the sooper-sekret
// genetic combination algorithm.
GeneScienceInterface public geneScience;

function setGeneScienceAddress(address _address) external onlyCEO {
GeneScienceInterface candidateContract = GeneScienceInterface(_address);
require(candidateContract.isGeneScience());
geneScience = candidateContract;
}
}

cryptokitties smart contract

最初,GeneScience不是为了“故意围绕 CryptoKitties 基因组培养神秘感和发现” 而开源的。但是社区构建了对基因科学算法进行逆向工程的工具。CK 随后将其开源,甚至发布了稍有改进的第二个版本(CK_blog_post)。

GeneScience合约时间线: v1 2017 年 11 月发布,2019 年 1 月开源,v2 2019 年 2 月发布(已经开源)。

我不会介绍GeneScience代码,因为它太底层了。但我会强调几点。

GeneScience涉及大量的位操作来混合父母小猫的基因。请记住,基因以 256 位数字的形式存储在Kitty struct. 这些位被映射到 决定小猫外观的特征(或CK喜欢说的属性)。

随机性是如何产生的? Solidity 没有随机数生成器,因此 CK 使用后代出生时的块号作为随机性的种子。这个块号很难操纵,所以它应该提供足够的随机性

7、加密猫的拍卖合约

CK的最终合约是拍卖的。CK 使用“时钟拍卖”:你设置开始和结束的价格和持续时间。然后价格从开始价格线性变化到结束价格。 谁先出价,谁就赢了。

cryptokitties smart contract

以下是拍卖合约的结构以及它如何与其它合约相适应:

cryptokitties smart contract

  • ClockAuctionBase:跟踪现有拍卖和出价功能:
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
contract ClockAuctionBase {
struct Auction {
address seller;
uint128 startingPrice;
uint128 endingPrice;
uint64 duration;
uint64 startedAt; // 0 if this auction has been concluded
}

uint256 public ownerCut; // measured in basis points (1/100 of a percent)
// Map from token ID to their corresponding auction.
mapping (uint256 => Auction) tokenIdToAuction;

// Computes the price and transfers winnings.
// Does NOT transfer ownership of token.
function _bid(uint256 _tokenId, uint256 _bidAmount) internal returns (uint256) {
Auction storage auction = tokenIdToAuction[_tokenId];
require(_isOnAuction(auction));

// linearly interpolate between startingPrice and endingPrice based on duration
uint256 price = _currentPrice(auction);
require(_bidAmount >= price);

address seller = auction.seller;

_removeAuction(_tokenId);

// Transfer proceeds to seller (if there are any!)
if (price > 0) {
// calculate CK's cut based on ownerCut
uint256 auctioneerCut = _computeCut(price);
uint256 sellerProceeds = price - auctioneerCut;
seller.transfer(sellerProceeds);
}

// transfer back the excess to the bidder
uint256 bidExcess = _bidAmount - price;
msg.sender.transfer(bidExcess);

return price;
}
}
  • ClockAuction:只是ClockAuctionBase的一个包装
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
contract ClockAuction is Pausable, ClockAuctionBase {
// Remove all Ether from the contract, which is the owner's cuts
// as well as any Ether sent directly to the contract address.
// Always transfers to the NFT contract
function withdrawBalance() external {
address nftAddress = address(nonFungibleContract);
require(msg.sender == owner || msg.sender == nftAddress);
bool res = nftAddress.send(this.balance);
}

function createAuction(uint256 _tokenId, uint256 _startingPrice, uint256 _endingPrice,
uint256 _duration, address _seller) external whenNotPaused {
require(_owns(msg.sender, _tokenId));
_escrow(msg.sender, _tokenId); // transfer token from the seller to this contract
Auction memory auction = Auction(
_seller,
uint128(_startingPrice),
uint128(_endingPrice),
uint64(_duration),
uint64(now)
);
_addAuction(_tokenId, auction); // add a mapping from tokenId to auction
}

function bid(uint256 _tokenId) external payable whenNotPaused {
_bid(_tokenId, msg.value);
_transfer(msg.sender, _tokenId); // transfer token to the bidder
}
}
  • SiringClockAuction和SaleClockAuction: 出租你的小猫进行繁殖和出售你的小猫的拍卖,分别。 这些需要分开,因为在成功投标后采取的行动对于每种情况都是完全不同的。
  • SaleClockAuction除了跟踪用于为新铸造的小猫设置最佳拍卖起始价格的最后 5 个拍卖价格之外,并没有增加太多。
  • SiringClockAuction是合约的另一个包装ClockAuction,除了它将租来的小猫转移回所有者而不是投标人(投标人保留后代)。

它们与KittyAuction分包合约中的核心合约相关联:

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
contract KittyAuction is KittyBreeding {
SaleClockAuction public saleAuction;
SiringClockAuction public siringAuction;

function setSaleAuctionAddress(address _address) external onlyCEO {
SaleClockAuction candidateContract = SaleClockAuction(_address);
require(candidateContract.isSaleClockAuction());
saleAuction = candidateContract;
}

function setSiringAuctionAddress(address _address) external onlyCEO {
SiringClockAuction candidateContract = SiringClockAuction(_address);
require(candidateContract.isSiringClockAuction());
siringAuction = candidateContract;
}

function createSaleAuction(uint256 _kittyId, uint256 _startingPrice, uint256 _endingPrice,
uint256 _duration) external whenNotPaused {
require(_owns(msg.sender, _kittyId));
require(!isPregnant(_kittyId));
_approve(_kittyId, saleAuction);
saleAuction.createAuction(_kittyId, _startingPrice, _endingPrice, _duration, msg.sender);
}

function createSiringAuction(uint256 _kittyId, uint256 _startingPrice, uint256 _endingPrice,
uint256 _duration) external whenNotPaused {
require(_owns(msg.sender, _kittyId));
require(isReadyToBreed(_kittyId));
_approve(_kittyId, siringAuction);
siringAuction.createAuction(_kittyId, _startingPrice, _endingPrice, _duration, msg.sender);
}

function bidOnSiringAuction(uint256 _sireId, uint256 _matronId) external payable whenNotPaused {
require(_owns(msg.sender, _matronId));
require(isReadyToBreed(_matronId));
require(_canBreedWithViaAuction(_matronId, _sireId));

// Define the current price of the auction.
uint256 currentPrice = siringAuction.getCurrentPrice(_sireId);
require(msg.value >= currentPrice + autoBirthFee);

siringAuction.bid.value(msg.value - autoBirthFee)(_sireId);
_breedWith(uint32(_matronId), uint32(_sireId));
}
}

总结拍卖:你为小猫创建拍卖,它被转移到拍卖合约并创建新的拍卖。每当有人成功出价时,要么将小猫转移给投标人(在拍卖时), 要么将小猫转回给你,但投标人保留后代(在拍卖时)。

8、CryptoKitties的时间线

我们看到了代码。但是代码是静态的,它没有告诉我们动态的画面。部署合约后发生了什么?

你可以在 Etherscan 上查看该合约的整个交易历史。如果我们回到开头:

cryptokitties smart contract

我们可以看到该合约是在 2017 年 11 月 23 日部署的。然后他们设置了兄弟合约的地址并设置了 CEO/CFO 角色。 然后创造了一堆促销小猫(准确地说是3K)。

然后他们取消暂停合约(这意味着现在大多数功能都可用)并开始操作:

cryptokitties smart contract

这是随时间变化的合约余额(来自Etherscan 分析):

cryptokitties smart contract

大幅下跌是CK团队的撤资。我们看到大部分动作发生在 2017 年 11 月到 2018 年 11 月。从CK 时间线网站, 看起来 CK 在 2018 年 1 月达到峰值时拥有 25 万用户。同样在 2017 年 12 月,CK 占以太坊流量的 25%。

这是交易频率的图表:

cryptokitties smart contract

虽然从 2018 年 11 月到 2019 年 7 月仍有大量交易,但它们都是低价值的,因为该地区的余额保持相对平稳。

9、我对加密猫合约代码的看法

-首先,艺术作品并不存在于链上。基因保持在链上,但理想情况下,小猫的图像也将在链上生成(如 Art Blocks)。 - CK 甚至没有将艺术品的链接放在链上。他们本可以使用该getMetadata函数返回指向不可变图像的链接。相反, 他们只是返回“Hello World”🤷‍♂️。从图像到图像的映射tokenIds发生在前端。因此,如果 CK 关闭他们的网站, 你将只剩下一个毫无意义的 256 位数字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
contract ERC721Metadata {
function getMetadata(uint256 _tokenId, string) public view returns (bytes32[4] buffer, uint256 count) {
if (_tokenId == 1) {
buffer[0] = "Hello World! :D";
count = 15;
} else if (_tokenId == 2) {
buffer[0] = "I would definitely choose a medi";
buffer[1] = "um length string.";
count = 49;
} else if (_tokenId == 3) {
buffer[0] = "Lorem ipsum dolor sit amet, mi e";
buffer[1] = "st accumsan dapibus augue lorem,";
buffer[2] = " tristique vestibulum id, libero";
buffer[3] = " suscipit varius sapien aliquam.";
count = 128;
}
}
}
  • 可升级性并不理想。可以通过在 KittyCore 合约中设置新地址来更新核心合约。在这种情况下,将发出一个事件,由客户端来监听它并切换到新合约。旧的将永远暂停。
1
2
3
4
5
6
7
8
contract KittyCore is KittyMinting {
address public newContractAddress;

function setNewAddress(address _v2Address) external onlyCEO whenPaused {
newContractAddress = _v2Address;
ContractUpgrade(_v2Address);
}
}

现在转向积极的

  • 良好的关注点分离:育种和拍卖与核心合约分开,以最大限度地减少错误。你可以在不中断核心的情况下插入同级合约的更新版本。
  • 干净的代码和对 Solidity 的深刻理解。整个合约中有很多有用的注释。防止重入攻击和最大化gas效率:

cryptokitties smart contract

cryptokitties smart contract

  • 良好的幽默感:

cryptokitties smart contract

10、结束语

以上就是 CryptoKittes 智能合约分解的内容!希望这可以帮到你!


原文链接:CryptoKitties: Smart Contract Breakdown

汇智网翻译整理,转载请标明出处