2017-12-04
CryptoKitties has done a great job of demonstrating what blockchains can be used for beyond just simple financial transactions.
I’m hoping we’ll start to see a lot more innovative uses of the blockchain for games like this in the future, so I wanted to write a quick walkthrough of the code behind CryptoKitties to show how it was implemented beneath the surface.
This article is written for developers, and while it’s not an absolute beginner’s introduction to Solidity, I tried to include links to documentation to make it as accessible as possible to developers of all levels.
Let’s dive in…
Almost all of the CryptoKitties code is open source, so the best way to figure out how it works is to read through the source code.
It’s around 2,000 lines in total, so in this article, I’m going to be walking through only the sections I think are most important. But here’s a copy of the full contract code on EthFiddle, if you want to read through it on your own:
CryptoKitties Source Code
https://ethfiddle.com/09YbyJRfiI
If you’re not familiar with what CryptoKitties is, it’s basically a game for buying, selling, and breeding digital cats. Each cat has a unique appearance that is defined by its genes, and when you breed 2 cats together, their genes combine in a unique way to produce an offspring, which you can then breed or sell.
The code for CryptoKitties is split into a number of smaller contracts in order to keep related code bundled together without having one single giant file with everything in it.
The subcontract inheritance for the main kitty contract looks like this
contract KittyAccessControl
contract KittyBase is KittyAccessControl
contract KittyOwnership is KittyBase, ERC721
contract KittyBreeding is KittyOwnership
contract KittyAuction is KittyBreeding
contract KittyMinting is KittyAuction
contract KittyCore is KittyMinting
So KittyCore
is ultimately the contract address the app points to, and it inherits all the data and methods from the previous contracts.
Let’s walk through these contracts one by one.
1.KittyAccessControl
: Who Controls the Contract?This contract manages the various addresses and constraints for operations that can be executed only by specific roles. Namely CEO, CFO, and COO.
This contract is for management of the contract, and isn’t related to the game mechanics at all. It basically has ‘setter’ methods for the “CEO”, “COO”, and “CFO”, which are Ethereum addresses that have special ownership and control over particular functions of the contract.
KittyAccessControl defines some function modifiers like onlyCEO
(which restricts a function so that only the CEO can perform it), and adds methods to do things like pause / unpause the contract or withdraw funds:
modifier onlyCLevel() {
require(
msg.sender == cooAddress ||
msg.sender == ceoAddress ||
msg.sender == cfoAddress
);
_;
}//...some other stuff// Only the CEO, COO, and CFO can execute this function:
function pause() external onlyCLevel whenNotPaused {
paused = true;
}
The pause()
function was probably added so that the developers could update it with a newer version in case there were any unforeseen bugs… But as my colleague Luke pointed out, this would actually allow the developers to freeze the contract completely, making it so no one can transfer, sell, or breed their kitties! Not that they would ever want to do so — but it’s interesting to point out, since most people assume that a DApp is totally decentralized just because it’s on Ethereum.
Moving on…
This is where we define the most fundamental code shared throughout the core functionality. This includes our main data storage, constants and data types, plus internal functions for managing these items.
KittyBase defines a lot of the core data of the app. First, it defines a Kitty as a struct:
struct Kitty {
uint256 genes;
uint64 birthTime;
uint64 cooldownEndBlock;
uint32 matronId;
uint32 sireId;
uint32 siringWithId;
uint16 cooldownIndex;
uint16 generation;
}
So a kitty is really just a bunch of unsigned integers… I guess.
Breaking down each piece of this:
genes
— a 256 bit integer representing the cat’s genetic code, the core piece of data that determines what a cat looks likebirthTime
— a timestamp of the block when the cat was borncooldownEndBlock
— the minimum timestamp after which this cat can engage in breeding againmatronId
& sireId
— the ID of the cat’s mother and father, respectivelysiringWithId
— this is set to the ID of the father if the cat is currently pregnant, zero otherwisecooldownIndex
— the current cool down duration for this cat (how long the cat has to wait after breeding before it can breed again)generation
— the “generation number” of this cat. The first cats minted by the contract have generation 0; the generation for new cats is the larger of their parents generations, plus 1.Note that in CryptoKitties, cats are asexual and any 2 cats are allowed to reproduce together — thus a cat does not have a gender.
The KittyBase contract then goes on to declare an array of theseKitty
structs:
Kitty[] kitties;
This array holds the data of all the Kitties in existence, so it’s kind of like a master Kitty DB. Whenever a new cat is created, it is added to this array, with it’s index in the array becoming the cat’s ID, like Genesis here having an ID of ‘1’:
This contract also contains a mapping from the cat’s ID to the address of its owner to keep track of who a kitty is owned by:
mapping (uint256 => address) public kittyIndexToOwner;
Some other mappings are also defined, but for the sake of keeping this article a reasonable length, I’m not going to go over every detail.
Whenever a kitty is transferred from one person to the next, this kittyIndexToOwner
mapping is updated to reflect the new owner:
/// @dev Assigns ownership of a specific Kitty to an address.
function _transfer(address _from, address _to, uint256 _tokenId) internal {
// Since the number of kittens is capped to 2^32 we can't overflow this
ownershipTokenCount[_to]++;
// transfer ownership
kittyIndexToOwner[_tokenId] = _to;
// When creating new kittens _from is 0x0, but we can't account that address.
if (_from != address(0)) {
ownershipTokenCount[_from]--;
// once the kitten is transferred also clear sire allowances
delete sireAllowedToAddress[_tokenId];
// clear any previously approved ownership exchange
delete kittyIndexToApproved[_tokenId];
}
// Emit the transfer event.
Transfer(_from, _to, _tokenId);
}
Transfering ownership sets kittyIndexToOwner
of the Kitty’s ID to the recipient’s _to
address.
Now let’s look at what happens when a new kitty is created:
function _createKitty(
uint256 _matronId,
uint256 _sireId,
uint256 _generation,
uint256 _genes,
address _owner
)
internal
returns (uint)
{
// These requires are not strictly necessary, our calling code should make
// sure that these conditions are never broken. However! _createKitty() is already
// an expensive call (for storage), and it doesn't hurt to be especially careful
// to ensure our data structures are always valid.
require(_matronId == uint256(uint32(_matronId)));
require(_sireId == uint256(uint32(_sireId)));
require(_generation == uint256(uint16(_generation)));
// New kitty starts with the same cooldown as parent gen/2
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)
});
uint256 newKittenId = kitties.push(_kitty) - 1;
// It's probably never going to happen, 4 billion cats is A LOT, but
// let's just be 100% sure we never let this happen.
require(newKittenId == uint256(uint32(newKittenId)));
// emit the birth event
Birth(
_owner,
newKittenId,
uint256(_kitty.matronId),
uint256(_kitty.sireId),
_kitty.genes
);
// This will assign ownership, and also emit the Transfer event as
// per ERC721 draft
_transfer(0, _owner, newKittenId);
return newKittenId;
}
So this function is passed the IDs of the mother and father, the kitty’s generation number, the 256-bit genetic code, and the address of the owner. It then creates the kitty, pushes it to the master Kitty
array, and then calls _transfer()
to assign it to its new owner.
Cool — so now we can see how CryptoKitties defines a kitty as a data type, how it stores all the kitties on the blockchain, and how it keeps track of who owns which kitties.
This provides the methods required for basic non-fungible token transactions, following the draft ERC721 spec.
CryptoKitties conforms to the the ERC721 token spec, a non-fungible token type that lends itself really well to tracking ownership of digital collectables like digital playing cards or rare items in an MMORPG.
Note on Fungibility: Ether is fungible, because any 5 ETH is just as good as any other 5 ETH. But with non-fungible tokens like CryptoKitties, not every cat is created equal, so they’re not interchangeable with each other.
You can see from its contract definition that KittyOwnership inherits from an ERC721 contract:
contract KittyOwnership is KittyBase, ERC721 {
And all ERC721 tokens follow the same standard, so the KittyOwnership contract fills in the implementation of the following functions:
/// @title Interface for contracts conforming to ERC-721: Non-Fungible Tokens
/// @author Dieter Shirley <[email protected]> (https://github.com/dete)
contract ERC721 {
// Required methods
function totalSupply() public view returns (uint256 total);
function balanceOf(address _owner) public view returns (uint256 balance);
function ownerOf(uint256 _tokenId) external view returns (address owner);
function approve(address _to, uint256 _tokenId) external;
function transfer(address _to, uint256 _tokenId) external;
function transferFrom(address _from, address _to, uint256 _tokenId) external;
// Events
event Transfer(address from, address to, uint256 tokenId);
event Approval(address owner, address approved, uint256 tokenId);
// Optional
// function name() public view returns (string name);
// function symbol() public view returns (string symbol);
// function tokensOfOwner(address _owner) external view returns (uint256[] tokenIds);
// function tokenMetadata(uint256 _tokenId, string _preferredTransport) public view returns (string infoUrl);
// ERC-165 Compatibility (https://github.com/ethereum/EIPs/issues/165)
function supportsInterface(bytes4 _interfaceID) external view returns (bool);
}
Since these methods are public, this provides a standard way for users to interact with CryptoKitties tokens in the same way they’d interact with any other ERC721 token. You can transfer your tokens to someone else by interacting directly with the CryptoKitties contract on the Ethereum blockchain without having to go through their web interface, so in this sense you really own your kitties. (Unless the CEO pauses the contract 😉).
I won’t go over the implementation of all these methods, but you can check them out on EthFiddle (search for “KittyOwnership”).
This article received a ton of positive feedback when it was first published, so we built CryptoZombies: an interactive tutorial for building your own games on Ethereum. It will walk you step by step through learning to code Solidity, and building your own Ethereum game. If you’re enjoying the article, check it out!
This file contains the methods necessary to breed cats together, including keeping track of siring offers, and relies on an external genetic combination contract.
The “external genetic combination contract” (geneScience
) is stored in a separate contract, which is not open source.
The KittyBreeding contract contains a method for the CEO to set the address of this external contract:
/// @dev Update the address of the genetic contract, can only be called by the CEO.
/// @param _address An address of a GeneScience contract instance to be used from this point forward.
function setGeneScienceAddress(address _address) external onlyCEO {
GeneScienceInterface candidateContract = GeneScienceInterface(_address);
// NOTE: verify that a contract is what we expect - https://github.com/Lunyr/crowdsale-contracts/blob/cfadd15986c30521d8ba7d5b6f57b4fefcc7ac38/contracts/LunyrToken.sol#L117
require(candidateContract.isGeneScience());
// Set the new contract address
geneScience = candidateContract;
}
They did it this way so the game would not be too easy — if you could just read how a kitty’s DNA was determined, it would be a lot easier to know which cats to breed in order to get a “fancy cat”.
This external geneScience
contract is later used in the giveBirth()
function (which we’ll see in a bit) to determine the new cat’s DNA.
Now let’s see what happens when two cats are bred together:
/// @dev Internal utility function to initiate breeding, assumes that all breeding
/// requirements have been checked.
function _breedWith(uint256 _matronId, uint256 _sireId) internal {
// Grab a reference to the Kitties from storage.
Kitty storage sire = kitties[_sireId];
Kitty storage matron = kitties[_matronId];
// Mark the matron as pregnant, keeping track of who the sire is.
matron.siringWithId = uint32(_sireId);
// Trigger the cooldown for both parents.
_triggerCooldown(sire);
_triggerCooldown(matron);
// Clear siring permission for both parents. This may not be strictly necessary
// but it's likely to avoid confusion!
delete sireAllowedToAddress[_matronId];
delete sireAllowedToAddress[_sireId];
// Every time a kitty gets pregnant, counter is incremented.
pregnantKitties++;
// Emit the pregnancy event.
Pregnant(kittyIndexToOwner[_matronId], _matronId, _sireId, matron.cooldownEndBlock);
}
So this function takes the ID of the mother and father, looks them up in the master kitties
array, and sets the siringWithId
on the mother to the father’s ID. (When siringWithId
is non-zero, it indicates that the mother is pregnant).
It also executes triggerCooldown
on both parents, which makes it so they can’t breed again for a set amount of time.
Next, we have a public giveBirth()
function which creates a new cat:
/// @notice Have a pregnant Kitty give birth!
/// @param _matronId A Kitty ready to give birth.
/// @return The Kitty ID of the new kitten.
/// @dev Looks at a given Kitty and, if pregnant and if the gestation period has passed,
/// combines the genes of the two parents to create a new kitten. The new Kitty is assigned
/// to the current owner of the matron. Upon successful completion, both the matron and the
/// new kitten will be ready to breed again. Note that 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)
{
// Grab a reference to the matron in storage.
Kitty storage matron = kitties[_matronId];
// Check that the matron is a valid cat.
require(matron.birthTime != 0);
// Check that the matron is pregnant, and that its time has come!
require(_isReadyToGiveBirth(matron));
// Grab a reference to the sire in storage.
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;
// Every time a kitty gives birth counter is decremented.
pregnantKitties--;
// Send the balance fee to the person who made birth happen.
msg.sender.send(autoBirthFee);
// return the new kitten's ID
return kittenId;
}
The in-line commenting on the code is pretty self-explanatory. Basically the code first performs some checks to see if the mother is ready to give birth. It then determines the child’s genes using geneScience.mixGenes()
, assigns ownership of the new kitty to the mother’s owner, then calls the _createKitty()
function we looked at in KittyBase.
Note that the geneScience.mixGenes()
function is a black box, since that contract is closed-source. So we don’t actually know how the child’s genes are determined, but we know it’s some function of the mother’s genes, the father’s genes, and the mother’s cooldownEndBlock
.
Here we have the public methods for auctioning or bidding on cats or siring services. The actual auction functionality is handled in two sibling contracts (one for sales and one for siring), while auction creation and bidding is mostly mediated through this facet of the core contract.
According to the devs, they split this auction functionality into “sibling” contracts because “their logic is somewhat complex and there’s always a risk of subtle bugs. By keeping them in their own contracts, we can upgrade them without disrupting the main contract that tracks kitty ownership.”
So this KittyAuctions contract contains the functions setSaleAuctionAddress()
and setSiringAuctionAddress()
, which like setGeneScienceAddress()
can only be called by the CEO, and sets the address of an external contract that handles these functions.
Note: “Siring” is referring to pimping out your cat — putting it up for auction, where another user can pay you Ether for your cat to breed with theirs. Lols.
This means that even if the CryptoKitties contract itself were immutable, the CEO has the flexibility to change the address of these auction contracts down the line, which would change the rules of the auctions. Again, not necessarily a bad thing since sometimes developers need to fix bugs, but something to be aware of.
I’m not going to go in depth into how exactly the auction & bidding logic is handled to keep this article from getting too long (it’s already long enough!), but you can check out the code in EthFiddle (search for KittyAuctions).
This final facet contains the functionality we use for creating new gen0 cats. We can make up to 5000 “promo” cats that can be given away (especially important when the community is new), and all others can only be created and then immediately put up for auction via an algorithmically determined starting price. Regardless of how they are created, there is a hard limit of 50k gen0 cats. After that, it’s all up to the community to breed, breed, breed!
The number of promo cats and gen0 cats the contract is able to create is hard-coded here:
uint256 public constant PROMO_CREATION_LIMIT = 5000;
uint256 public constant GEN0_CREATION_LIMIT = 45000;
And here’s the code where the “COO” can create promo kitties and gen0 kitties:
/// @dev we can create promo kittens, up to a limit. Only callable by COO
/// @param _genes the encoded genes of the kitten to be created, any value is accepted
/// @param _owner the future owner of the created kittens. Default to contract 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);
}
/// @dev Creates a new gen0 kitty with the given genes and
/// creates 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++;
}
So with createPromoKitty()
, it looks like the COO can create a new kitty with whatever genes he wants, and send it to whoever he wants (up to a max of 5000 kitties). I’m guessing they use this for early beta testers, friends and family, to give away free kitties for promotion purposes, etc.
But this also means that your cat may not be as unique as you think, since he can potentially print 5000 identical copies of it!
For createGen0Auction()
, the COO also provides the genetic code for the new kitty. But instead of assigning it to a specific person’s address, it creates an auction where users would bid Ether to buy the kitty.
This is the main CryptoKitties contract, which is what is compiled and running on the Ethereum blockchain. This contract ties everything together.
Because of the inheritance structure, it inherits from all the contracts we looked at prior to this, and adds a few more final methods, like this function for getting all a Kitty’s data using its ID:
/// @notice Returns all the relevant information about a specific kitty.
/// @param _id The ID of the kitty of interest.
function getKitty(uint256 _id)
external
view
returns (
bool isGestating,
bool isReady,
uint256 cooldownIndex,
uint256 nextActionAt,
uint256 siringWithId,
uint256 birthTime,
uint256 matronId,
uint256 sireId,
uint256 generation,
uint256 genes
) {
Kitty storage kit = kitties[_id];
// if this variable is 0 then it's not gestating
isGestating = (kit.siringWithId != 0);
isReady = (kit.cooldownEndBlock <= block.number);
cooldownIndex = uint256(kit.cooldownIndex);
nextActionAt = uint256(kit.cooldownEndBlock);
siringWithId = uint256(kit.siringWithId);
birthTime = uint256(kit.birthTime);
matronId = uint256(kit.matronId);
sireId = uint256(kit.sireId);
generation = uint256(kit.generation);
genes = kit.genes;
}
This is a public method that will return all the data for a specific kitty from the blockchain. I imagine this is what is queried by their web server for displaying the cats on the web site.
As we can see from the code above, a “kitty” basically comes down to a 256-bit unsigned integer that represents its genetic code.
There’s nothing in the Solidity contract code that stores a cat’s image, or its description, or that determines what that 256-bit integer actually means. The interpretation of that genetic code happens on CryptoKitty’s web server.
So while this is a really clever demonstration of a game on the blockchain, it’s not actually 100% blockchain-based. If their website was taken offline in the future, unless someone had backed up all the images, you would be left with only owning a meaningless 256-bit integer.
In the contract code, I did find a contract called ERC721Metadata
, but it never ends up getting used for anything. So my guess is they originally planned on storing everything in the blockchain, but later decided against it (too cost prohibitive to store a lot of data in Ethereum?) and so they ended up needing to store it on their web server instead.
Ok… So to sum everything up, here we’ve looked at:
This was just a top-level overview. If you’re looking for a more in-depth tutorial on how to build your own game, you may be interested in an interactive code school we’ll be releasing within the next couple weeks.
UPDATE: We released the interactive code school. Check it out at CryptoZombies.io!