SlideShare a Scribd company logo
1 of 42
ERC721 Token & CryptoKitties
분석
Editor: SooBokJin
Email: thdnthdn24@gmail.com
목차
1. ERC721 definition
2. ERC721 standard
3. ERC721 Implementation - openzeppelin ERC721Token
3.1. contract 상속 관계
3.2. 변수 분석
1) ERC721BasicToken
2) ERC721Token
3.3. 주요 동작에 따른 메서드 분석
1) 토큰 생성 (create token)
2) 토큰 전달 (transfer token)
3) 권한 부여 (approve token)
4) 토큰 삭제 (burn token)
목차
4. ERC721 example – cryptokitties
4.1 cryptokitties
1) intro
2) Kitty property
4.2 크립토키티에서 ERC721이 어떻게 쓰였나?
1) 시작에 앞서
2) 주요 변수
3) flow
4.3 크립토키티 주요 메서드 분석
1) breed
2) birth
3) transfer
4) approve
ERC721 Token
1. ERC721 definition
ERC-721 is a free, open standard that describes how to build non-fungible or unique tokens on the
Ethereum blockchain. While most tokens are fungible (every token is the same as every other token),
ERC-721 tokens are all unique.
interface ERC721 /* is ERC165 */ {
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);
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);
}
interface ERC165 {
function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
eip-ERC721 standard
https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
2. ERC721 standard
3. ERC721 Implementation - openzeppelin ERC721Token
https://github.com/OpenZeppelin/openzeppelin-solidity/tree/master/contracts/token/ERC721
ERC721 코드를 분석하기 앞서 토큰의 핵심 동작을 이해할 필요가 있다.
1. Ownership
How is token ownership handled?
2. Creation
How are tokens created?
3. Transfer & Allowance
How are tokens transferred, and how do we allow other addresses (contracts or externally owned accounts)
transfer capability?
4. Burn
How do we burn or destroy a token?
3. ERC721 Implementation - intro
3.1. contract 상속 관계
ERC721BasicToken
SupportsInterfaceWithLookupERC721Token
ERC721
ERC721Basic ERC165
ERC721Basic
ERC721Enumerable
ERC721Metadata
check
check
3.2. 변수 분석 - ERC721BasicToken
mapping (uint256 => address) internal tokenOwner
각 토큰(tokenId)의 소유주(address)를 기록하는 변수
mapping (address => uint256) internal ownedTokensCount
각 address 별 총 token 보유 개수를 기록하는 변수
mapping (uint256 => address) internal tokenApprovals
토큰(tokenId)에 대한 사용 권한을 얻은(approved) address를 기록하는 변수
mapping (address => mapping (address => bool)) internal operatorApprovals
특정 address에게 자신이 보유한 모든 token(ownedTokens)에 대한 사용 권한을 부여(approve)했는지에 대한 여부를 기록하는 변수. default는 0(false)이다.
tokenId 1
tokenId 2
tokenId 3
…
addr 1(0x24da..)
addr 2(0x32fe.. )
addr 3(0x52de..)
…
addr 1(0x24da..)
addr 2(0x32fe.. )
addr 3(0x52de..)
…
6
2
3
…
tokenId 1
tokenId 2
tokenId 3
…
approved addr 1(0x24da..)
approved addr 2(0x32fe.. )
approved addr 3(0x52de..)
…
각 address가 보유한 token 개수
addr 1(0x24da..)
addr 2(0x32fe.. )
addr 3(0x52de..)
…
approvalForAll addr 1(0x39qe.. )
approvalForAll addr 2(0x10yv.. )
approvalForAll addr 3(0x92dw..)
…
true
false
true
…
true: approvalForAll addr x는 addr1 이 보유한 모든 토큰(ownedTokens)에 대한 사용 권한을 지님
false: default 값, approvalForAll addr x는addr1이 보유한 토큰에 대한 사용 권한이 없음
3.2. 변수 분석 - ERC721Token
string internal name_
token name(e.g. CryptoKitties)
string internal symbol_
token symbol(e.g. CK)
mapping(uint256 => uint256) internal ownedTokensIndex
토큰(tokenId)과 ownedTokens의 value값의 index를 mapping
mapping(address => uint256[]) internal ownedTokens
각 address가 소유한 모든 token을 배열 형태로 기록하는 변수
uint256[] internal allTokens
모든 토큰(tokenId)을 배열의 형태로 기록한 변수
mapping(uint256 => uint256) internal allTokensIndex
tokenId와 allTokens 배열의 index를 mapping
mapping(uint256 => string) internal tokenURIs
Optional mapping for token URIs(분석 필요)
addr 1(0x24da..)
addr 2(0x32fe.. )
addr 3(0x52de..)
…
0
1
2
tokenId 1
tokenId 2
tokenId 3
…
tokenId 1
tokenId 2
tokenId 3
…
0
7
3
…
ownedTokens의 index(값 중복 존재)
0
1
2
tokenId 1
tokenId 2
tokenId 3
…
0
1
2
tokenId 1
tokenId 2
tokenId 3
…
0
1
2
…
allTokens의 index
3.2. 핵심 변수
mapping (uint256 => address) internal tokenOwner
각 토큰(tokenId)의 소유주(address)를 기록하는 변수
tokenId 1
tokenId 2
tokenId 3
…
addr 1(0x24da..)
addr 2(0x32fe.. )
addr 3(0x52de..)
…
mapping(address => uint256[]) internal ownedTokens
각 address가 소유한 모든 token을 배열 형태로 기록하는 변수
addr 1(0x24da..)
addr 2(0x32fe.. )
addr 3(0x52de..)
…
0
1
2
tokenId 1
tokenId 2
tokenId 3
…
uint256[] internal allTokens
모든 토큰(tokenId)을 배열의 형태로 기록한 변수
0
1
2
tokenId 1
tokenId 2
tokenId 3
…
mapping (uint256 => address) internal tokenApprovals
토큰(tokenId)에 대한 사용 권한을 얻은(approved) address를 기록하는 변수
tokenId 1
tokenId 2
tokenId 3
…
approved addr 1(0x24da..)
approved addr 2(0x32fe.. )
approved addr 3(0x52de..)
…
토큰별 소유주 address 정보
각 address가 소유한 모든 토큰 정보
현재까지 생성된 모든 토큰 정보
토큰의 사용 권한을 얻은 address 정보
3.3. 주요 동작에 따른 메서드 분석 - view function
function balanceOf(address _owner) public view returns (uint256) {
require(_owner != address(0));
return ownedTokensCount[_owner];
}
function ownerOf(uint256 _tokenId) public view returns (address) {
address owner = tokenOwner[_tokenId];
require(owner != address(0));
return owner;
}
function exists(uint256 _tokenId) public view returns (bool) {
address owner = tokenOwner[_tokenId];
return owner != address(0);
}
function name() external view returns (string) {
return name_;
}
function symbol() external view returns (string) {
return symbol_;
}
function tokenURI(uint256 _tokenId) public view returns (string) {
require(exists(_tokenId));
return tokenURIs[_tokenId];
}
function tokenOfOwnerByIndex(address _owner, uint256 _index) public view returns (uint256)
{
require(_index < balanceOf(_owner));
return ownedTokens[_owner][_index];
}
function totalSupply() public view returns (uint256) {
return allTokens.length;
}
function tokenByIndex(uint256 _index) public view returns (uint256) {
require(_index < totalSupply());
return allTokens[_index];
}
ERC721Token.solERC721BasicToken.sol
3.3. 주요 동작에 따른 메서드 분석 - create token
function _mint(address _to, uint256 _tokenId) internal {
super._mint(_to, _tokenId);
allTokensIndex[_tokenId] = allTokens.length;
allTokens.push(_tokenId);
}
function _mint(address _to, uint256 _tokenId) internal {
require(_to != address(0));
addTokenTo(_to, _tokenId);
emit Transfer(address(0), _to, _tokenId);
}
function addTokenTo(address _to, uint256 _tokenId) internal {
super.addTokenTo(_to, _tokenId);
uint256 length = ownedTokens[_to].length;
ownedTokens[_to].push(_tokenId);
ownedTokensIndex[_tokenId] = length;
}
function addTokenTo(address _to, uint256 _tokenId) internal {
require(tokenOwner[_tokenId] == address(0));
tokenOwner[_tokenId] = _to;
ownedTokensCount[_to] = ownedTokensCount[_to].add(1);
}
ERC721Token.sol ERC721BasicToken.sol
ERC721Token.sol
ERC721BasicToken.sol
1 2
3
4
internal이므로 contract 외부에서 메서드 호출 불가
(public으로 설정할 경우 누구든지 특정 tokenId를 생성할 수 있게 됨)
_mint는 ERC721 standard method가 아니다
(openzeppelin에서 구현)
3.3. 주요 동작에 따른 메서드 분석 - create token
메서드(method)
_mint(address _to, uint256 _tokenId)
매개변수(parameter)
_to: 새로 생성될 토큰의 소유주(address)
_tokenId: 새로 생성될 토큰의 고유값(uint256)
동작 흐름 설명
토큰(tokenId)의 소유주(address)를 기록하는 변수인 tokenOwner에 값을 기록하고(tokenOwner[_tokenId] = _to) 동시에 소유주의 토큰 보유량을 기록하는 변수인
ownedTokensCount의 값을 1 증가 시킨다(ownedTokensCount[_to] = ownedTokensCount[_to].add(1)).
※ 새로 생성된 토큰에 대한 소유주가 이미 존재하면(이미 존재하는 tokenId이면) revert된다(require(tokenOwner[_tokenId] == address(0)).
이후 토큰 소유주가 보유한 모든 토큰을 배열의 형태로 기록하는 ownedTokens에 새로 생성된 토큰(_tokenId)을 추가한다(ownedTokens[_to].push(_tokenId)).
이 때 ownedTokens의 value인 tokenId 배열의 index값을 기록하는 ownedTokenIndex 변수에 새로 생성된 토큰의 index값을추가한다
(ownedTokensIndex[_tokenId] = length).
마지막으로 allTokens에 새로 생성된 토큰을 등록한다(allTokens.push(_tokenId)). 동시에 allTokensIndex도 등록한다(allTokensIndex[_tokenId] = allTokens.length).
※ _mint 메서드는 internal이므로 외부에서 직접 호출이 불가하며, 해당 함수를 호출하는 메서드를 추가 구현해야한다. 이때 _mint를 호출하는 메서드(호출자)에서
_mint 메서드 호출 권한을 유동적으로 설정할 수 있을 것으로 예상된다(체크 필요).
실행 후 변경되는 변수
tokenOwner: tokenOwner[_tokenId] = _to
ownedTokensCount: ownedTokensCount[_to]++
ownedTokens: ownedTokens[_to]에 mapping되는 token array에 _tokenId 기록
ownedTokensIndex: ownedTokens[_to] mapping되는 token array에 추가된 _tokenId의 index 기록
allTokens: 새로 생성된 _tokenId 기록
allTokensIndex: allTokens에 추가된 _tokenId의 index 기록
address(0)의 의미
address(0)은 0x000..을 의미한다.
function transferFrom(address _from, address _to, uint256 _tokenId) public canTransfer(_tokenId)
{
require(_from != address(0));
require(_to != address(0));
clearApproval(_from, _tokenId);
removeTokenFrom(_from, _tokenId);
addTokenTo(_to, _tokenId);
emit Transfer(_from, _to, _tokenId);
}
modifier canTransfer(uint256 _tokenId) {
require(isApprovedOrOwner(msg.sender, _tokenId));
_;
}
function clearApproval(address _owner, uint256 _tokenId) internal {
require(ownerOf(_tokenId) == _owner);
if (tokenApprovals[_tokenId] != address(0)) {
tokenApprovals[_tokenId] = address(0);
}
}
function removeTokenFrom(address _from, uint256 _tokenId) internal {
super.removeTokenFrom(_from, _tokenId);
uint256 tokenIndex = ownedTokensIndex[_tokenId];
uint256 lastTokenIndex = ownedTokens[_from].length.sub(1);
uint256 lastToken = ownedTokens[_from][lastTokenIndex];
ownedTokens[_from][tokenIndex] = lastToken;
ownedTokens[_from][lastTokenIndex] = 0;
ownedTokens[_from].length--;
ownedTokensIndex[_tokenId] = 0;
ownedTokensIndex[lastToken] = tokenIndex;
}
function removeTokenFrom(address _from, uint256 _tokenId) internal {
require(ownerOf(_tokenId) == _from);
ownedTokensCount[_from] = ownedTokensCount[_from].sub(1);
tokenOwner[_tokenId] = address(0);
}
3.3. 주요 동작에 따른 메서드 분석
- transfer token
ERC721BasicToken.sol ERC721BasicToken.sol
function addTokenTo(address _to, uint256 _tokenId) internal {
super.addTokenTo(_to, _tokenId);
uint256 length = ownedTokens[_to].length;
ownedTokens[_to].push(_tokenId);
ownedTokensIndex[_tokenId] = length;
}
function addTokenTo(address _to, uint256 _tokenId) internal {
require(tokenOwner[_tokenId] == address(0));
tokenOwner[_tokenId] = _to;
ownedTokensCount[_to] = ownedTokensCount[_to].add(1);
}
ERC721Token.sol
ERC721BasicToken.sol
ERC721BasicToken.sol
ERC721Token.sol
ERC721BasicToken.sol
1
3
4
5
6
7
2
function isApprovedOrOwner(address _spender, uint256 _tokenId) internal view returns (bool) {
address owner = ownerOf(_tokenId);
return (_spender == owner || getApproved(_tokenId) == _spender || isApprovedForAll(owner, _spender));
}
ERC721BasicToken.sol
2
ownedTokens
array reorg
3.3. 주요 동작에 따른 메서드 분석 - transfer token
메서드(method)
transferFrom(address _from, address _to, uint256 _tokenId)
매개변수(parameter)
_from: 현재 토큰(_tokenId)의 소유주 주소(address),
_to: 토큰을 받는 주소(address)
_tokenId: 전송할 토큰Id(uint256)
동작 흐름 설명
우선 메서드를 실행시킨 주체(호출자, msg.sender)가 해당 토큰에 대한 사용권한을 가지고 있는 지 체크한다(canTransfer). 1) 토큰 소유주이거나(tokenOwner), 2) 소
유주로부터 권한을 받았거나(getApproved), 3) 해당 토큰 소유주의 모든 토큰에 대한 사용 권한을 가지고 있는 경우(isApprovedForAll) 다음 처리가 진행된다.
권한 확인한 후 해당 토큰(_tokenId)에 대한 사용 권한을 가진 address를 기록하는 tokenApprovals의 값을 0x000.. 으로 초기화한다
(clearApproval(_from, _tokenId)).
이후 토큰의 소유주(_to)를 기록하는 변수인 tokenOwner의 값을 0x00... 으로 초기화한다(tokenOwner[_tokenId] = address(0)). 토큰의 소유주가 변경되었다는 말은
이전 소유주(_from)가 보유한 토큰 개수가 한 개 줄어들었음을 의미하므로 ownedTokensCount의 값에서 1을 뺸다
(ownedTokensCount[_from] = ownedTokensCount[_from].sub(1)).
ownedTokens[_from]에 mapping되는 token array에서 거래되는 토큰(_tokenId)을 제거한다. 이때 배열의 길이를 효율적으로 관리하기 위한 reorg 작업이 진행된다.
※ reorg의 핵심 원리는 token array의 마지막 요소를 제거되는 토큰의 위치에 넣어주고 token array의 길이를 -1 하는 방식이다.
※ 이더리움의 경우 배열의 특정 요소를 삭제한다는 것은 요소 배열에서 빠지는 것이 아니라 해당 요소의 값이 0으로 초기화됨을 의미한다(e.g. [1,2,3]에서 0번째 값(1)
을 삭제하면 [0,2,3]이 된다.).
solidity의 length: Arrays have a length member to hold their number of elements. Dynamic arrays can be resized in storage (not in memory) by changing
the .length member.
참고: http://solidity.readthedocs.io/en/v0.4.24/types.html
마지막으로 토큰을 전달받는 address의 정보를 업데이트한다. 해당 작업은 token creation에서 호출된 addTokenTo 메서드를 이용한다.
3.3. 주요 동작에 따른 메서드 분석 - transfer token
실행 후 변경되는 변수
tokenOwner: tokenOwner[_tokenId] = _to
ownedTokensCount: ownedTokensCount[_from]-- , ownedTokensCount[_to]++
ownedTokens:
- ownedTokens[_from]: 배열에서 _tokenId 요소 제거(해당 자리에 배열의 마지막 요소 tokenId가 들어가고 length--)
- ownedTokens[_to]: 배열에 _tokenId 추가, length++
ownedTokensIndex:
- ownedTokensIndex[_tokenId]: ownedTokens[_to]에 mapping되는 array의 마지막 요소 index
- ownedTokensIndex[lastToken]: ownedTokens[_from]에 mapping되는 array의 이전에 _tokenId가 위치했던 index
safeTransferFrom
safeTransferFrom()
token을 잘못된 주소 또는 전달받은 토큰을 사용할 수 있는 function이 구현되지 않은 contract에 전달되었을 경우
해당 토큰은 영원히 사용될 수 없다는 문제가 존재한다. 이 문제를 해결하기 위해 구현된 메서드이다.
function approve(address _to, uint256 _tokenId) public {
address owner = ownerOf(_tokenId);
require(_to != owner);
require(msg.sender == owner || isApprovedForAll(owner, msg.sender));
tokenApprovals[_tokenId] = _to;
emit Approval(owner, _to, _tokenId);
}
function getApproved(uint256 _tokenId) public view returns (address) {
return tokenApprovals[_tokenId];
}
function isApprovedForAll(address _owner, address _operator) public view returns (bool)
{
return operatorApprovals[_owner][_operator];
}
function setApprovalForAll(address _to, bool _approved) public {
require(_to != msg.sender);
operatorApprovals[msg.sender][_to] = _approved;
emit ApprovalForAll(msg.sender, _to, _approved);
}
3.3. 주요 동작에 따른 메서드 분석 - approve token
ERC721BasicToken.sol ERC721BasicToken.sol
ERC721BasicToken.sol
ERC721BasicToken.sol
setApprovalForAll() function을 통해 setting
getApproved() function을 통해 approve 확인
1 2
3.3. 주요 동작에 따른 메서드 분석 - approve token
메서드(method)
approve(address _to, uint256 _tokenId)
매개변수(parameter)
_to: 새로 생성될 토큰의 소유주(address)
_tokenId:
동작 흐름 설명
아래의 조건 중 하나를 만족하는 경우에만 tokenId에 대한 사용 권한을 특정 address에게 부여할 수 있다.
1) tokenOwner
address가 토큰의 실 소유주인 경우(tokenOwner[tokenId] == address)
2) operatorApprovals
address가 실 소유주(owner)로부터 모든 토큰에 대한 권한을 부여받은 경우
(operatorApproval[owner][address] == true)
approve 메서드에서는 인수로 받은 address(_to)가 위 조건을 만족하는 지 확인한 후, 인수로 받은 토큰(_tokenId)에 대한 권한을 _to에게 부여한다
(tokenApprovals[_tokenId] = _to).
실행 후 변경되는 변수
tokenApprovals: tokenApprovals[_tokenId] = _to
3.3. 주요 동작에 따른 메서드 분석 - burn token
function _burn(address _owner, uint256 _tokenId) internal {
super._burn(_owner, _tokenId);
// Clear metadata (if any)
if (bytes(tokenURIs[_tokenId]).length != 0) {
delete tokenURIs[_tokenId];
}
uint256 tokenIndex = allTokensIndex[_tokenId];
uint256 lastTokenIndex = allTokens.length.sub(1);
uint256 lastToken = allTokens[lastTokenIndex];
allTokens[tokenIndex] = lastToken;
allTokens[lastTokenIndex] = 0;
allTokens.length--;
allTokensIndex[_tokenId] = 0;
allTokensIndex[lastToken] = tokenIndex;
}
function _burn(address _owner, uint256 _tokenId) internal {
clearApproval(_owner, _tokenId);
removeTokenFrom(_owner, _tokenId);
emit Transfer(_owner, address(0), _tokenId);
}
function clearApproval(address _owner, uint256 _tokenId) internal {
require(ownerOf(_tokenId) == _owner);
if (tokenApprovals[_tokenId] != address(0)) {
tokenApprovals[_tokenId] = address(0);
}
}
ERC721BasicToken.sol
3
function removeTokenFrom(address _from, uint256 _tokenId) internal {
super.removeTokenFrom(_from, _tokenId);
uint256 tokenIndex = ownedTokensIndex[_tokenId];
uint256 lastTokenIndex = ownedTokens[_from].length.sub(1);
uint256 lastToken = ownedTokens[_from][lastTokenIndex];
ownedTokens[_from][tokenIndex] = lastToken;
ownedTokens[_from][lastTokenIndex] = 0;
ownedTokens[_from].length--;
ownedTokensIndex[_tokenId] = 0;
ownedTokensIndex[lastToken] = tokenIndex;
}
function removeTokenFrom(address _from, uint256 _tokenId) internal {
require(ownerOf(_tokenId) == _from);
ownedTokensCount[_from] = ownedTokensCount[_from].sub(1);
tokenOwner[_tokenId] = address(0);
}
ERC721Token.sol
ERC721BasicToken.sol
4
5
1
2
ERC721Token.sol
ERC721BasicToken.sol
internal이므로 contract 외부에서 메서드 호출 불가
(public으로 설정할 경우 누구든지 특정 tokenId를 삭제할 수 있게 됨)
ownedTokens
array reorg
allTokens
array reorg
_burn은 ERC721 standard method가
아니다(openzeppelin에서 구현)
3.3. 주요 동작에 따른 메서드 분석 - burn token
메서드(method)
_burn(address _owner, uint256 _tokenId)
매개변수(parameter)
_owner: 제거될 토큰(_tokenId)의 소유주(address)
_tokenId: 제거되는 토큰
동작 흐름 설명
transferFrom메서드 내부에서 사용된 clearApproval, removeTokenFrom 메서드를 통해 tokenOwner 초기화(0x00..) 및 ownedTokens에 기록된 토큰 정보를 제거한다.
이후 모든 토큰에 대한 정보를 기록하는 allTokens에 해당 토큰(_tokenId)에 대한 기록을 제거한다.
※ burn 메서드 또한 internal이므로 외부에서 직접 호출이 불가하며, 해당 함수를 호출하는 메서드를 추가 구현해야한다. 이때 _burn()을 호출하는 메서드에서 _burn()
메서드 호출 권한을 유동적으로 설정할 수 있을 것으로 예상된다(체크 필요).
실행 후 변경되는 변수
tokenOwner: tokenOwner[_tokenId] = address(0)
ownedTokensCount: ownedTokensCount[_owner]--
ownedTokens: ownedTokens[_owner]에 mapping되는 token array에 _tokenId 제거
ownedTokensIndex: ownedTokens[_owner] mapping되는 token array에 추가된 _tokenId의 인덱스 값 기록 제거
allTokens: 배열에서 토큰(_tokenId) 제거
allTokensIndex: allTokens에 기록된 _tokenId의 index 초기화
CryptoKitties
4.1 cryptokitties - intro
• 교배 가능한 키티를 사고 파는 게임
• 키티는 각자 고유한 ERC721 토큰에 대응
• 약 40억 개(2^32-1)의 표현형과 유전 형질을 지님
참조: 이더리움 연구회-크립토키티분석
https://cryptokittydex.com/kitties/471433
breedWithAuto(uint256 _matronId, uint256 _sireId)
참조: 이더리움 연구회-크립토키티분석
giveBirth(uint256 _matronId)
Cooldowns
새로운 키티를 생성하는 함수(giveBirth)를 직접 호출해야 한다.
giveBirth는 누구나 호출 가능하며, breeding시 지불된 birthFee를
보상으로 받을 수 있다.
* gas fee가 높아지면 운영진이 직접 호출
breedWithAuto를 실행 후 일정 시간이 지나면 새로운 키티를 생성(giveBirth)할 수 있다.
Birth가 일어나는 시점은 breeding Tx가 담기는 블록으로부터 Cooldown / 15sec 번째 블록이다.
교배(breeding)
생성
키티 간(matron, sire) 교배(breeding)를 통해 새로운 키티를 생성한다. 교배를 신청(breedWithAuto)한 후 일정 기간(cooldowns)이 지
나면 새로운 키티를 생성(giveBirth)할 수 있다.
교배 시 성별(sex)을 정할 수 있으며(암컷: matron 수컷: sire), 교배 후 생성된 키티는 matron kitty 보유자가 소유하게 된다.
matron
sire
matron kitty의 소유주가 소유
4.1 cryptokitties - intro
새로이 태어난 키티는 고유한 성질을 지니며(외형, 유전자 등), 또 다른 키티와 교배(Breed)를 하거나, 판매(Sell) 또는 선물(Gift)할 수 있다.
교배(Breed)가 핵심!
4.1 cryptokitties - intro
4.1 Kitty property
참조: https://steemkr.com/kr/@kimyd/3-kittybase
• genes:
키티의 유전자 정보를 표현, 256bit의 genes 값을 토대로 키티의 외형이 결정된다.
• birthtime:
키티가 태어난 날짜(block.timestamp)
• coolDownEndBlock:
키티가 임신했을 경우 새로운 키티가 생성될 수 있는 block number. 현재 블록 넘버(block.number)가 coolDownEndBlock보다 같거나
높아지는 경우 giveBirth 정상 실행된다(kittyBreeding.sol의 _isReadyToGiveBirth 참조).
• matronId:
임신 중인 키티의 ID
• sireId:
씨를 뿌린(아버지) 키티의 ID
• siringWithId:
키티가 임신 중일 경우 matronId를 설정(임신 중이지 않을 경우 0)
• coolDownIndex:
현재 cooldown의 index. 교배 후에 재교배하기까지 어느정도 기다리는지 index에 따라 시간이 결정됨
• generation:
키티의 제네레이션의 수. 부모의 제네레이션의 수에 1를 더한 수(최초의 키티는 0). 여기서 부모 제네레이션 중 높은 값을 토대로 정해진다
(e.g. 아버지 generation= 8, 어머니 generation= 2 인 경우 child generation은 9)
키티는 다양한 속성을 지니므로 구조체(struct)를 이용하여 표현한다. 타입명은 Kitty이다.
* KittyBase.sol에 정의
4.1 Kitty property
출처: https://cryptokittydex.com/kitties/471433
tokenId
property
4.2 크립토키티에서 ERC721이 어떻게 쓰였나? – 시작에 앞서
크립토키티에서 사용된 ERC721 token은 standard가 아닌 draft 버전이다. 크립토키티 게임이 나올 시점(약 2017년 11월)에 ERC721
standard가 정의되어 있지 않았기 때문이다.
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);
}
<cryptokitties에서 사용된 ERC721>
4.2 크립토키티에서 ERC721이 어떻게 쓰였나? – 주요 변수
/// @dev A mapping from cat IDs to the address that owns them. All cats have
/// some valid owner address, even gen0 cats are created with a non-zero owner.
mapping (uint256 => address) public kittyIndexToOwner;
// @dev A mapping from owner address to count of tokens that address owns.
// Used internally inside balanceOf() to resolve ownership count.
mapping (address => uint256) ownershipTokenCount;
/// @dev A mapping from KittyIDs to an address that has been approved to call
/// transferFrom(). Each Kitty can only have one approved address for transfer
/// at any time. A zero value means no approval is outstanding.
mapping (uint256 => address) public kittyIndexToApproved;
/// @dev A mapping from KittyIDs to an address that has been approved to use
/// this Kitty for siring via breedWith(). Each Kitty can only have one approved
/// address for siring at any time. A zero value means no approval is outstanding.
mapping (uint256 => address) public sireAllowedToAddress;
Kitty[] kitties;
struct Kitty {
uint256 genes;
uint64 birthTime;
uint64 cooldownEndBlock;
uint32 matronId;
uint32 sireId;
uint32 siringWithId;
uint16 cooldownIndex;
uint16 generation;
}
4.2 크립토키티에서 ERC721이 어떻게 쓰였나? – flow
교배(breed)를 통해 키티가 태어나면(birth) kitties 배열에 추가(push)된다. 이때 배열의 index가 ERC721 토큰에서의 고유한 값(tokenId)으로
활용된다.
Kitty[] kitties
32142
…
32143
32144
32145
32146
32147
index
tokenId
1. 교배(breed)
breedWithAuto(uint256 _matronId, uint256 _sireId)
matronId: 32147
sireId: 32146
Kitty[] kitties
32142
…
32143
32144
32145
32146
32147
index
tokenId
2. 키티 생성(birth)
giveBirth(uint256 _matronId)
Kitty memory _kitty = Kitty({
genes: _genes,
birthTime: uint64(now),
cooldownEndBlock: 0,
matronId: uint32(_matronId),
sireId: uint32(_sireId),
siringWithId: 0,
cooldownIndex: cooldownIndex,
generation: uint16(_generation)
});
4.2 크립토키티에서 ERC721이 어떻게 쓰였나? – flow
matronId: 32147
sireId: 32146
교배(breed)를 통해 키티가 태어나면(birth) kitties 배열에 추가(push)된다. 이때 배열의 index가 ERC721 토큰에서의 고유한 값(tokenId)으로
활용된다.
Kitty[] kitties
32142
…
32143
32144
32145
32146
32147
32148
index
tokenId
3. 배열에 추가(push 메서드는 배열의 길이를 return)
uint256 newKittenId = kitties.push(_kitty) - 1;
Kitty memory _kitty = Kitty({
genes: _genes,
birthTime: uint64(now),
cooldownEndBlock: 0,
matronId: uint32(_matronId),
sireId: uint32(_sireId),
siringWithId: 0,
cooldownIndex: cooldownIndex,
generation: uint16(_generation)
});
4.2 크립토키티에서 ERC721이 어떻게 쓰였나? – flow
matronId: 32147
sireId: 32146
교배(breed)를 통해 키티가 태어나면(birth) kitties 배열에 추가(push)된다. 이때 배열의 index가 ERC721 토큰에서의 고유한 값(tokenId)으로
활용된다.
Kitty[] kitties
32142
…
32143
32144
32145
32146
32147
32148
index
tokenId
uint256 newKittenId = kitties.push(_kitty) - 1;
4. 소유주 기록(matron 소유주가 소유하게 됨)
_transfer(0, _owner, newKittenId);_owner = kittyIndexToOwner[_matronId]
4.2 크립토키티에서 ERC721이 어떻게 쓰였나? – flow
matronId: 32147
sireId: 32146
0xD5bfCb58f900b31c8155057616482A9d4ad70170
owner
교배(breed)를 통해 키티가 태어나면(birth) kitties 배열에 추가(push)된다. 이때 배열의 index가 ERC721 토큰에서의 고유한 값(tokenId)으로
활용된다.
4.2 크립토키티에서 ERC721이 어떻게 쓰였나? - 정리
Kitty[] kitties
32142
…
32143
32144
32145
32146
32147
0xBb02904FB51b462D8E72a1532279dBb4AE53a365
0x14d591E70c1A63736970e9dEaa8ad1e7DC178B1b
0x14d591E70c1A63736970e9dEaa8ad1e7DC178B1b
0x9108e25f92B70F603Ec4F3a7A1A7AB18ccAe02F5
…
kitty owner(address)
생성되는 키티를 kitties[] 배열에 기록하고, 배열의 index를 tokenId로 활용
각 tokenId 마다 소유주 address가 mapping되며, 이를 통해 tokenId에 해당하는 kitty의 소유 증명(ERC721) 및 거래
4.3 크립토키티 주요 메서드 분석 - breed
function breedWithAuto(uint256 _matronId, uint256 _sireId) external payable whenNotPaused {
// Checks for payment.
require(msg.value >= autoBirthFee);
// Caller must own the matron.
require(_owns(msg.sender, _matronId));
require(_isSiringPermitted(_sireId, _matronId));
// Grab a reference to the potential matron
Kitty storage matron = kitties[_matronId];
// Make sure matron isn't pregnant, or in the middle of a siring cooldown
require(_isReadyToBreed(matron));
// Grab a reference to the potential sire
Kitty storage sire = kitties[_sireId];
// Make sure sire isn't pregnant, or in the middle of a siring cooldown
require(_isReadyToBreed(sire));
// Test that these cats are a valid mating pair.
require(_isValidMatingPair(
matron,
_matronId,
sire,
_sireId
));
// All checks passed, kitty gets pregnant!
_breedWith(_matronId, _sireId);
}
function _isSiringPermitted(uint256 _sireId, uint256 _matronId) internal view returns (bool) {
address matronOwner = kittyIndexToOwner[_matronId];
address sireOwner = kittyIndexToOwner[_sireId];
// Siring is okay if they have same owner, or if the matron's owner was given
// permission to breed with this sire.
return (matronOwner == sireOwner || sireAllowedToAddress[_sireId] == matronOwner);
}
KittyBreeding.sol
KittyBreeding.sol
function _isReadyToBreed(Kitty _kit) internal view returns (bool) {
// In addition to checking the cooldownEndBlock, we also need to check to see if
// the cat has a pending birth; there can be some period of time between the end
// of the pregnacy timer and the birth event.
return (_kit.siringWithId == 0) && (_kit.cooldownEndBlock <= uint64(block.number));
}
KittyBreeding.sol
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);
}
KittyBreeding.sol
4.3 크립토키티 주요 메서드 분석 - Birth
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;
}
function _createKitty
(uint256 _matronId, uint256 _sireId, uint256 _generation, uint256 _genes, address
_owner)
internal returns (uint){
~ 코드 생략
Kitty memory _kitty = Kitty({
genes: _genes,
birthTime: uint64(now),
cooldownEndBlock: 0,
matronId: uint32(_matronId),
sireId: uint32(_sireId),
siringWithId: 0,
cooldownIndex: cooldownIndex,
generation: uint16(_generation)
});
//The function returns the new length
//newKittenId = 새로 생성된 kitty가 기록된 배열의 index, 고유한 값이다.
uint256 newKittenId = kitties.push(_kitty) - 1;
require(newKittenId == uint256(uint32(newKittenId)));
// This will assign ownership, and also emit the Transfer event as
// per ERC721 draft
_transfer(0, _owner, newKittenId);
return newKittenId;
}
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];
}
Transfer(_from, _to, _tokenId);
}
KittyBreeding.sol
KittyBase.sol
KittyBase.sol
4.3 크립토키티 주요 메서드 분석 - transfer
function transfer(
address _to,
uint256 _tokenId
)
external
whenNotPaused
{
// Safety check to prevent against an unexpected 0x0 default.
require(_to != address(0));
// Disallow transfers to this contract to prevent accidental misuse.
// The contract should never own any kitties (except very briefly
// after a gen0 cat is created and before it goes on auction).
require(_to != address(this));
// Disallow transfers to the auction contracts to prevent accidental
// misuse. Auction contracts should only take ownership of kitties
// through the allow + transferFrom flow.
require(_to != address(saleAuction));
require(_to != address(siringAuction));
// You can only send your own cat.
require(_owns(msg.sender, _tokenId));
// Reassign ownership, clear pending approvals, emit Transfer event.
_transfer(msg.sender, _to, _tokenId);
}
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);
}
function _owns(address _claimant, uint256 _tokenId) internal view returns (bool) {
return kittyIndexToOwner[_tokenId] == _claimant;
}
KittyOwnership.sol
KittyOwnership.sol
KittyBase.sol
* transferFrom 생략
4.3 크립토키티 주요 메서드 분석 - approve
function approve(
address _to,
uint256 _tokenId
)
external
whenNotPaused
{
// Only an owner can grant transfer approval.
require(_owns(msg.sender, _tokenId));
// Register the approval (replacing any previous approval).
_approve(_tokenId, _to);
// Emit approval event.
Approval(msg.sender, _to, _tokenId);
}
function _owns(address _claimant, uint256 _tokenId) internal view returns (bool) {
return kittyIndexToOwner[_tokenId] == _claimant;
}
function _approve(uint256 _tokenId, address _approved) internal {
kittyIndexToApproved[_tokenId] = _approved;
}
KittyOwnership.sol
KittyOwnership.sol
KittyOwnership.sol
참조 자료
eip-ERC721 standard
https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
openzeppelin ERC721 implementation
https://github.com/OpenZeppelin/openzeppelin-solidity/tree/master/contracts/token/ERC721
The Anatomy of ERC721
https://medium.com/crypto-currently/the-anatomy-of-erc721-e9db77abfc24
Walking Through the ERC721 Full Implementation
https://medium.com/blockchannel/walking-through-the-erc721-full-implementation-72ad72735f3c
cryptokitties github
https://github.com/cryptocopycats/awesome-cryptokitties/tree/master/contracts
How to Code Your Own CryptoKitties-Style Game on Ethereum
https://medium.com/loom-network/how-to-code-your-own-cryptokitties-style-game-on-ethereum-7c8ac86a4eb3
CryptoKitties Clone In 20 minutes. Non-fungible Token Tutorial
https://maksimivanov.com/posts/gradient-coin-tutorial/#minting
크립토 키티 코드 분석
https://steemkr.com/@kimyd
CryptoKitties Breeding Guide
https://segmentnext.com/2017/12/08/cryptokitties-breeding-guide/

More Related Content

More from Soobok Jin

Sidechain Overview
Sidechain OverviewSidechain Overview
Sidechain OverviewSoobok Jin
 
Ethereum Basics Part1
Ethereum Basics Part1Ethereum Basics Part1
Ethereum Basics Part1Soobok Jin
 
Bitcoin Basics Part4
Bitcoin Basics Part4Bitcoin Basics Part4
Bitcoin Basics Part4Soobok Jin
 
Bitcoin Basics Part3
Bitcoin Basics Part3Bitcoin Basics Part3
Bitcoin Basics Part3Soobok Jin
 
Bitcoin Basics Part 2
Bitcoin Basics Part 2Bitcoin Basics Part 2
Bitcoin Basics Part 2Soobok Jin
 
Bitcoin Basics Part1
Bitcoin Basics Part1Bitcoin Basics Part1
Bitcoin Basics Part1Soobok Jin
 

More from Soobok Jin (6)

Sidechain Overview
Sidechain OverviewSidechain Overview
Sidechain Overview
 
Ethereum Basics Part1
Ethereum Basics Part1Ethereum Basics Part1
Ethereum Basics Part1
 
Bitcoin Basics Part4
Bitcoin Basics Part4Bitcoin Basics Part4
Bitcoin Basics Part4
 
Bitcoin Basics Part3
Bitcoin Basics Part3Bitcoin Basics Part3
Bitcoin Basics Part3
 
Bitcoin Basics Part 2
Bitcoin Basics Part 2Bitcoin Basics Part 2
Bitcoin Basics Part 2
 
Bitcoin Basics Part1
Bitcoin Basics Part1Bitcoin Basics Part1
Bitcoin Basics Part1
 

Erc721 token & crypto kitties analysis

  • 1. ERC721 Token & CryptoKitties 분석 Editor: SooBokJin Email: thdnthdn24@gmail.com
  • 2. 목차 1. ERC721 definition 2. ERC721 standard 3. ERC721 Implementation - openzeppelin ERC721Token 3.1. contract 상속 관계 3.2. 변수 분석 1) ERC721BasicToken 2) ERC721Token 3.3. 주요 동작에 따른 메서드 분석 1) 토큰 생성 (create token) 2) 토큰 전달 (transfer token) 3) 권한 부여 (approve token) 4) 토큰 삭제 (burn token)
  • 3. 목차 4. ERC721 example – cryptokitties 4.1 cryptokitties 1) intro 2) Kitty property 4.2 크립토키티에서 ERC721이 어떻게 쓰였나? 1) 시작에 앞서 2) 주요 변수 3) flow 4.3 크립토키티 주요 메서드 분석 1) breed 2) birth 3) transfer 4) approve
  • 5. 1. ERC721 definition ERC-721 is a free, open standard that describes how to build non-fungible or unique tokens on the Ethereum blockchain. While most tokens are fungible (every token is the same as every other token), ERC-721 tokens are all unique.
  • 6. interface ERC721 /* is ERC165 */ { 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); 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); } interface ERC165 { function supportsInterface(bytes4 interfaceID) external view returns (bool); } eip-ERC721 standard https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md 2. ERC721 standard
  • 7. 3. ERC721 Implementation - openzeppelin ERC721Token https://github.com/OpenZeppelin/openzeppelin-solidity/tree/master/contracts/token/ERC721
  • 8. ERC721 코드를 분석하기 앞서 토큰의 핵심 동작을 이해할 필요가 있다. 1. Ownership How is token ownership handled? 2. Creation How are tokens created? 3. Transfer & Allowance How are tokens transferred, and how do we allow other addresses (contracts or externally owned accounts) transfer capability? 4. Burn How do we burn or destroy a token? 3. ERC721 Implementation - intro
  • 9. 3.1. contract 상속 관계 ERC721BasicToken SupportsInterfaceWithLookupERC721Token ERC721 ERC721Basic ERC165 ERC721Basic ERC721Enumerable ERC721Metadata check check
  • 10. 3.2. 변수 분석 - ERC721BasicToken mapping (uint256 => address) internal tokenOwner 각 토큰(tokenId)의 소유주(address)를 기록하는 변수 mapping (address => uint256) internal ownedTokensCount 각 address 별 총 token 보유 개수를 기록하는 변수 mapping (uint256 => address) internal tokenApprovals 토큰(tokenId)에 대한 사용 권한을 얻은(approved) address를 기록하는 변수 mapping (address => mapping (address => bool)) internal operatorApprovals 특정 address에게 자신이 보유한 모든 token(ownedTokens)에 대한 사용 권한을 부여(approve)했는지에 대한 여부를 기록하는 변수. default는 0(false)이다. tokenId 1 tokenId 2 tokenId 3 … addr 1(0x24da..) addr 2(0x32fe.. ) addr 3(0x52de..) … addr 1(0x24da..) addr 2(0x32fe.. ) addr 3(0x52de..) … 6 2 3 … tokenId 1 tokenId 2 tokenId 3 … approved addr 1(0x24da..) approved addr 2(0x32fe.. ) approved addr 3(0x52de..) … 각 address가 보유한 token 개수 addr 1(0x24da..) addr 2(0x32fe.. ) addr 3(0x52de..) … approvalForAll addr 1(0x39qe.. ) approvalForAll addr 2(0x10yv.. ) approvalForAll addr 3(0x92dw..) … true false true … true: approvalForAll addr x는 addr1 이 보유한 모든 토큰(ownedTokens)에 대한 사용 권한을 지님 false: default 값, approvalForAll addr x는addr1이 보유한 토큰에 대한 사용 권한이 없음
  • 11. 3.2. 변수 분석 - ERC721Token string internal name_ token name(e.g. CryptoKitties) string internal symbol_ token symbol(e.g. CK) mapping(uint256 => uint256) internal ownedTokensIndex 토큰(tokenId)과 ownedTokens의 value값의 index를 mapping mapping(address => uint256[]) internal ownedTokens 각 address가 소유한 모든 token을 배열 형태로 기록하는 변수 uint256[] internal allTokens 모든 토큰(tokenId)을 배열의 형태로 기록한 변수 mapping(uint256 => uint256) internal allTokensIndex tokenId와 allTokens 배열의 index를 mapping mapping(uint256 => string) internal tokenURIs Optional mapping for token URIs(분석 필요) addr 1(0x24da..) addr 2(0x32fe.. ) addr 3(0x52de..) … 0 1 2 tokenId 1 tokenId 2 tokenId 3 … tokenId 1 tokenId 2 tokenId 3 … 0 7 3 … ownedTokens의 index(값 중복 존재) 0 1 2 tokenId 1 tokenId 2 tokenId 3 … 0 1 2 tokenId 1 tokenId 2 tokenId 3 … 0 1 2 … allTokens의 index
  • 12. 3.2. 핵심 변수 mapping (uint256 => address) internal tokenOwner 각 토큰(tokenId)의 소유주(address)를 기록하는 변수 tokenId 1 tokenId 2 tokenId 3 … addr 1(0x24da..) addr 2(0x32fe.. ) addr 3(0x52de..) … mapping(address => uint256[]) internal ownedTokens 각 address가 소유한 모든 token을 배열 형태로 기록하는 변수 addr 1(0x24da..) addr 2(0x32fe.. ) addr 3(0x52de..) … 0 1 2 tokenId 1 tokenId 2 tokenId 3 … uint256[] internal allTokens 모든 토큰(tokenId)을 배열의 형태로 기록한 변수 0 1 2 tokenId 1 tokenId 2 tokenId 3 … mapping (uint256 => address) internal tokenApprovals 토큰(tokenId)에 대한 사용 권한을 얻은(approved) address를 기록하는 변수 tokenId 1 tokenId 2 tokenId 3 … approved addr 1(0x24da..) approved addr 2(0x32fe.. ) approved addr 3(0x52de..) … 토큰별 소유주 address 정보 각 address가 소유한 모든 토큰 정보 현재까지 생성된 모든 토큰 정보 토큰의 사용 권한을 얻은 address 정보
  • 13. 3.3. 주요 동작에 따른 메서드 분석 - view function function balanceOf(address _owner) public view returns (uint256) { require(_owner != address(0)); return ownedTokensCount[_owner]; } function ownerOf(uint256 _tokenId) public view returns (address) { address owner = tokenOwner[_tokenId]; require(owner != address(0)); return owner; } function exists(uint256 _tokenId) public view returns (bool) { address owner = tokenOwner[_tokenId]; return owner != address(0); } function name() external view returns (string) { return name_; } function symbol() external view returns (string) { return symbol_; } function tokenURI(uint256 _tokenId) public view returns (string) { require(exists(_tokenId)); return tokenURIs[_tokenId]; } function tokenOfOwnerByIndex(address _owner, uint256 _index) public view returns (uint256) { require(_index < balanceOf(_owner)); return ownedTokens[_owner][_index]; } function totalSupply() public view returns (uint256) { return allTokens.length; } function tokenByIndex(uint256 _index) public view returns (uint256) { require(_index < totalSupply()); return allTokens[_index]; } ERC721Token.solERC721BasicToken.sol
  • 14. 3.3. 주요 동작에 따른 메서드 분석 - create token function _mint(address _to, uint256 _tokenId) internal { super._mint(_to, _tokenId); allTokensIndex[_tokenId] = allTokens.length; allTokens.push(_tokenId); } function _mint(address _to, uint256 _tokenId) internal { require(_to != address(0)); addTokenTo(_to, _tokenId); emit Transfer(address(0), _to, _tokenId); } function addTokenTo(address _to, uint256 _tokenId) internal { super.addTokenTo(_to, _tokenId); uint256 length = ownedTokens[_to].length; ownedTokens[_to].push(_tokenId); ownedTokensIndex[_tokenId] = length; } function addTokenTo(address _to, uint256 _tokenId) internal { require(tokenOwner[_tokenId] == address(0)); tokenOwner[_tokenId] = _to; ownedTokensCount[_to] = ownedTokensCount[_to].add(1); } ERC721Token.sol ERC721BasicToken.sol ERC721Token.sol ERC721BasicToken.sol 1 2 3 4 internal이므로 contract 외부에서 메서드 호출 불가 (public으로 설정할 경우 누구든지 특정 tokenId를 생성할 수 있게 됨) _mint는 ERC721 standard method가 아니다 (openzeppelin에서 구현)
  • 15. 3.3. 주요 동작에 따른 메서드 분석 - create token 메서드(method) _mint(address _to, uint256 _tokenId) 매개변수(parameter) _to: 새로 생성될 토큰의 소유주(address) _tokenId: 새로 생성될 토큰의 고유값(uint256) 동작 흐름 설명 토큰(tokenId)의 소유주(address)를 기록하는 변수인 tokenOwner에 값을 기록하고(tokenOwner[_tokenId] = _to) 동시에 소유주의 토큰 보유량을 기록하는 변수인 ownedTokensCount의 값을 1 증가 시킨다(ownedTokensCount[_to] = ownedTokensCount[_to].add(1)). ※ 새로 생성된 토큰에 대한 소유주가 이미 존재하면(이미 존재하는 tokenId이면) revert된다(require(tokenOwner[_tokenId] == address(0)). 이후 토큰 소유주가 보유한 모든 토큰을 배열의 형태로 기록하는 ownedTokens에 새로 생성된 토큰(_tokenId)을 추가한다(ownedTokens[_to].push(_tokenId)). 이 때 ownedTokens의 value인 tokenId 배열의 index값을 기록하는 ownedTokenIndex 변수에 새로 생성된 토큰의 index값을추가한다 (ownedTokensIndex[_tokenId] = length). 마지막으로 allTokens에 새로 생성된 토큰을 등록한다(allTokens.push(_tokenId)). 동시에 allTokensIndex도 등록한다(allTokensIndex[_tokenId] = allTokens.length). ※ _mint 메서드는 internal이므로 외부에서 직접 호출이 불가하며, 해당 함수를 호출하는 메서드를 추가 구현해야한다. 이때 _mint를 호출하는 메서드(호출자)에서 _mint 메서드 호출 권한을 유동적으로 설정할 수 있을 것으로 예상된다(체크 필요). 실행 후 변경되는 변수 tokenOwner: tokenOwner[_tokenId] = _to ownedTokensCount: ownedTokensCount[_to]++ ownedTokens: ownedTokens[_to]에 mapping되는 token array에 _tokenId 기록 ownedTokensIndex: ownedTokens[_to] mapping되는 token array에 추가된 _tokenId의 index 기록 allTokens: 새로 생성된 _tokenId 기록 allTokensIndex: allTokens에 추가된 _tokenId의 index 기록
  • 17. function transferFrom(address _from, address _to, uint256 _tokenId) public canTransfer(_tokenId) { require(_from != address(0)); require(_to != address(0)); clearApproval(_from, _tokenId); removeTokenFrom(_from, _tokenId); addTokenTo(_to, _tokenId); emit Transfer(_from, _to, _tokenId); } modifier canTransfer(uint256 _tokenId) { require(isApprovedOrOwner(msg.sender, _tokenId)); _; } function clearApproval(address _owner, uint256 _tokenId) internal { require(ownerOf(_tokenId) == _owner); if (tokenApprovals[_tokenId] != address(0)) { tokenApprovals[_tokenId] = address(0); } } function removeTokenFrom(address _from, uint256 _tokenId) internal { super.removeTokenFrom(_from, _tokenId); uint256 tokenIndex = ownedTokensIndex[_tokenId]; uint256 lastTokenIndex = ownedTokens[_from].length.sub(1); uint256 lastToken = ownedTokens[_from][lastTokenIndex]; ownedTokens[_from][tokenIndex] = lastToken; ownedTokens[_from][lastTokenIndex] = 0; ownedTokens[_from].length--; ownedTokensIndex[_tokenId] = 0; ownedTokensIndex[lastToken] = tokenIndex; } function removeTokenFrom(address _from, uint256 _tokenId) internal { require(ownerOf(_tokenId) == _from); ownedTokensCount[_from] = ownedTokensCount[_from].sub(1); tokenOwner[_tokenId] = address(0); } 3.3. 주요 동작에 따른 메서드 분석 - transfer token ERC721BasicToken.sol ERC721BasicToken.sol function addTokenTo(address _to, uint256 _tokenId) internal { super.addTokenTo(_to, _tokenId); uint256 length = ownedTokens[_to].length; ownedTokens[_to].push(_tokenId); ownedTokensIndex[_tokenId] = length; } function addTokenTo(address _to, uint256 _tokenId) internal { require(tokenOwner[_tokenId] == address(0)); tokenOwner[_tokenId] = _to; ownedTokensCount[_to] = ownedTokensCount[_to].add(1); } ERC721Token.sol ERC721BasicToken.sol ERC721BasicToken.sol ERC721Token.sol ERC721BasicToken.sol 1 3 4 5 6 7 2 function isApprovedOrOwner(address _spender, uint256 _tokenId) internal view returns (bool) { address owner = ownerOf(_tokenId); return (_spender == owner || getApproved(_tokenId) == _spender || isApprovedForAll(owner, _spender)); } ERC721BasicToken.sol 2 ownedTokens array reorg
  • 18. 3.3. 주요 동작에 따른 메서드 분석 - transfer token 메서드(method) transferFrom(address _from, address _to, uint256 _tokenId) 매개변수(parameter) _from: 현재 토큰(_tokenId)의 소유주 주소(address), _to: 토큰을 받는 주소(address) _tokenId: 전송할 토큰Id(uint256) 동작 흐름 설명 우선 메서드를 실행시킨 주체(호출자, msg.sender)가 해당 토큰에 대한 사용권한을 가지고 있는 지 체크한다(canTransfer). 1) 토큰 소유주이거나(tokenOwner), 2) 소 유주로부터 권한을 받았거나(getApproved), 3) 해당 토큰 소유주의 모든 토큰에 대한 사용 권한을 가지고 있는 경우(isApprovedForAll) 다음 처리가 진행된다. 권한 확인한 후 해당 토큰(_tokenId)에 대한 사용 권한을 가진 address를 기록하는 tokenApprovals의 값을 0x000.. 으로 초기화한다 (clearApproval(_from, _tokenId)). 이후 토큰의 소유주(_to)를 기록하는 변수인 tokenOwner의 값을 0x00... 으로 초기화한다(tokenOwner[_tokenId] = address(0)). 토큰의 소유주가 변경되었다는 말은 이전 소유주(_from)가 보유한 토큰 개수가 한 개 줄어들었음을 의미하므로 ownedTokensCount의 값에서 1을 뺸다 (ownedTokensCount[_from] = ownedTokensCount[_from].sub(1)). ownedTokens[_from]에 mapping되는 token array에서 거래되는 토큰(_tokenId)을 제거한다. 이때 배열의 길이를 효율적으로 관리하기 위한 reorg 작업이 진행된다. ※ reorg의 핵심 원리는 token array의 마지막 요소를 제거되는 토큰의 위치에 넣어주고 token array의 길이를 -1 하는 방식이다. ※ 이더리움의 경우 배열의 특정 요소를 삭제한다는 것은 요소 배열에서 빠지는 것이 아니라 해당 요소의 값이 0으로 초기화됨을 의미한다(e.g. [1,2,3]에서 0번째 값(1) 을 삭제하면 [0,2,3]이 된다.). solidity의 length: Arrays have a length member to hold their number of elements. Dynamic arrays can be resized in storage (not in memory) by changing the .length member. 참고: http://solidity.readthedocs.io/en/v0.4.24/types.html 마지막으로 토큰을 전달받는 address의 정보를 업데이트한다. 해당 작업은 token creation에서 호출된 addTokenTo 메서드를 이용한다.
  • 19. 3.3. 주요 동작에 따른 메서드 분석 - transfer token 실행 후 변경되는 변수 tokenOwner: tokenOwner[_tokenId] = _to ownedTokensCount: ownedTokensCount[_from]-- , ownedTokensCount[_to]++ ownedTokens: - ownedTokens[_from]: 배열에서 _tokenId 요소 제거(해당 자리에 배열의 마지막 요소 tokenId가 들어가고 length--) - ownedTokens[_to]: 배열에 _tokenId 추가, length++ ownedTokensIndex: - ownedTokensIndex[_tokenId]: ownedTokens[_to]에 mapping되는 array의 마지막 요소 index - ownedTokensIndex[lastToken]: ownedTokens[_from]에 mapping되는 array의 이전에 _tokenId가 위치했던 index
  • 20. safeTransferFrom safeTransferFrom() token을 잘못된 주소 또는 전달받은 토큰을 사용할 수 있는 function이 구현되지 않은 contract에 전달되었을 경우 해당 토큰은 영원히 사용될 수 없다는 문제가 존재한다. 이 문제를 해결하기 위해 구현된 메서드이다.
  • 21. function approve(address _to, uint256 _tokenId) public { address owner = ownerOf(_tokenId); require(_to != owner); require(msg.sender == owner || isApprovedForAll(owner, msg.sender)); tokenApprovals[_tokenId] = _to; emit Approval(owner, _to, _tokenId); } function getApproved(uint256 _tokenId) public view returns (address) { return tokenApprovals[_tokenId]; } function isApprovedForAll(address _owner, address _operator) public view returns (bool) { return operatorApprovals[_owner][_operator]; } function setApprovalForAll(address _to, bool _approved) public { require(_to != msg.sender); operatorApprovals[msg.sender][_to] = _approved; emit ApprovalForAll(msg.sender, _to, _approved); } 3.3. 주요 동작에 따른 메서드 분석 - approve token ERC721BasicToken.sol ERC721BasicToken.sol ERC721BasicToken.sol ERC721BasicToken.sol setApprovalForAll() function을 통해 setting getApproved() function을 통해 approve 확인 1 2
  • 22. 3.3. 주요 동작에 따른 메서드 분석 - approve token 메서드(method) approve(address _to, uint256 _tokenId) 매개변수(parameter) _to: 새로 생성될 토큰의 소유주(address) _tokenId: 동작 흐름 설명 아래의 조건 중 하나를 만족하는 경우에만 tokenId에 대한 사용 권한을 특정 address에게 부여할 수 있다. 1) tokenOwner address가 토큰의 실 소유주인 경우(tokenOwner[tokenId] == address) 2) operatorApprovals address가 실 소유주(owner)로부터 모든 토큰에 대한 권한을 부여받은 경우 (operatorApproval[owner][address] == true) approve 메서드에서는 인수로 받은 address(_to)가 위 조건을 만족하는 지 확인한 후, 인수로 받은 토큰(_tokenId)에 대한 권한을 _to에게 부여한다 (tokenApprovals[_tokenId] = _to). 실행 후 변경되는 변수 tokenApprovals: tokenApprovals[_tokenId] = _to
  • 23. 3.3. 주요 동작에 따른 메서드 분석 - burn token function _burn(address _owner, uint256 _tokenId) internal { super._burn(_owner, _tokenId); // Clear metadata (if any) if (bytes(tokenURIs[_tokenId]).length != 0) { delete tokenURIs[_tokenId]; } uint256 tokenIndex = allTokensIndex[_tokenId]; uint256 lastTokenIndex = allTokens.length.sub(1); uint256 lastToken = allTokens[lastTokenIndex]; allTokens[tokenIndex] = lastToken; allTokens[lastTokenIndex] = 0; allTokens.length--; allTokensIndex[_tokenId] = 0; allTokensIndex[lastToken] = tokenIndex; } function _burn(address _owner, uint256 _tokenId) internal { clearApproval(_owner, _tokenId); removeTokenFrom(_owner, _tokenId); emit Transfer(_owner, address(0), _tokenId); } function clearApproval(address _owner, uint256 _tokenId) internal { require(ownerOf(_tokenId) == _owner); if (tokenApprovals[_tokenId] != address(0)) { tokenApprovals[_tokenId] = address(0); } } ERC721BasicToken.sol 3 function removeTokenFrom(address _from, uint256 _tokenId) internal { super.removeTokenFrom(_from, _tokenId); uint256 tokenIndex = ownedTokensIndex[_tokenId]; uint256 lastTokenIndex = ownedTokens[_from].length.sub(1); uint256 lastToken = ownedTokens[_from][lastTokenIndex]; ownedTokens[_from][tokenIndex] = lastToken; ownedTokens[_from][lastTokenIndex] = 0; ownedTokens[_from].length--; ownedTokensIndex[_tokenId] = 0; ownedTokensIndex[lastToken] = tokenIndex; } function removeTokenFrom(address _from, uint256 _tokenId) internal { require(ownerOf(_tokenId) == _from); ownedTokensCount[_from] = ownedTokensCount[_from].sub(1); tokenOwner[_tokenId] = address(0); } ERC721Token.sol ERC721BasicToken.sol 4 5 1 2 ERC721Token.sol ERC721BasicToken.sol internal이므로 contract 외부에서 메서드 호출 불가 (public으로 설정할 경우 누구든지 특정 tokenId를 삭제할 수 있게 됨) ownedTokens array reorg allTokens array reorg _burn은 ERC721 standard method가 아니다(openzeppelin에서 구현)
  • 24. 3.3. 주요 동작에 따른 메서드 분석 - burn token 메서드(method) _burn(address _owner, uint256 _tokenId) 매개변수(parameter) _owner: 제거될 토큰(_tokenId)의 소유주(address) _tokenId: 제거되는 토큰 동작 흐름 설명 transferFrom메서드 내부에서 사용된 clearApproval, removeTokenFrom 메서드를 통해 tokenOwner 초기화(0x00..) 및 ownedTokens에 기록된 토큰 정보를 제거한다. 이후 모든 토큰에 대한 정보를 기록하는 allTokens에 해당 토큰(_tokenId)에 대한 기록을 제거한다. ※ burn 메서드 또한 internal이므로 외부에서 직접 호출이 불가하며, 해당 함수를 호출하는 메서드를 추가 구현해야한다. 이때 _burn()을 호출하는 메서드에서 _burn() 메서드 호출 권한을 유동적으로 설정할 수 있을 것으로 예상된다(체크 필요). 실행 후 변경되는 변수 tokenOwner: tokenOwner[_tokenId] = address(0) ownedTokensCount: ownedTokensCount[_owner]-- ownedTokens: ownedTokens[_owner]에 mapping되는 token array에 _tokenId 제거 ownedTokensIndex: ownedTokens[_owner] mapping되는 token array에 추가된 _tokenId의 인덱스 값 기록 제거 allTokens: 배열에서 토큰(_tokenId) 제거 allTokensIndex: allTokens에 기록된 _tokenId의 index 초기화
  • 26. 4.1 cryptokitties - intro • 교배 가능한 키티를 사고 파는 게임 • 키티는 각자 고유한 ERC721 토큰에 대응 • 약 40억 개(2^32-1)의 표현형과 유전 형질을 지님 참조: 이더리움 연구회-크립토키티분석
  • 27. https://cryptokittydex.com/kitties/471433 breedWithAuto(uint256 _matronId, uint256 _sireId) 참조: 이더리움 연구회-크립토키티분석 giveBirth(uint256 _matronId) Cooldowns 새로운 키티를 생성하는 함수(giveBirth)를 직접 호출해야 한다. giveBirth는 누구나 호출 가능하며, breeding시 지불된 birthFee를 보상으로 받을 수 있다. * gas fee가 높아지면 운영진이 직접 호출 breedWithAuto를 실행 후 일정 시간이 지나면 새로운 키티를 생성(giveBirth)할 수 있다. Birth가 일어나는 시점은 breeding Tx가 담기는 블록으로부터 Cooldown / 15sec 번째 블록이다. 교배(breeding) 생성 키티 간(matron, sire) 교배(breeding)를 통해 새로운 키티를 생성한다. 교배를 신청(breedWithAuto)한 후 일정 기간(cooldowns)이 지 나면 새로운 키티를 생성(giveBirth)할 수 있다. 교배 시 성별(sex)을 정할 수 있으며(암컷: matron 수컷: sire), 교배 후 생성된 키티는 matron kitty 보유자가 소유하게 된다. matron sire matron kitty의 소유주가 소유 4.1 cryptokitties - intro
  • 28. 새로이 태어난 키티는 고유한 성질을 지니며(외형, 유전자 등), 또 다른 키티와 교배(Breed)를 하거나, 판매(Sell) 또는 선물(Gift)할 수 있다. 교배(Breed)가 핵심! 4.1 cryptokitties - intro
  • 29. 4.1 Kitty property 참조: https://steemkr.com/kr/@kimyd/3-kittybase • genes: 키티의 유전자 정보를 표현, 256bit의 genes 값을 토대로 키티의 외형이 결정된다. • birthtime: 키티가 태어난 날짜(block.timestamp) • coolDownEndBlock: 키티가 임신했을 경우 새로운 키티가 생성될 수 있는 block number. 현재 블록 넘버(block.number)가 coolDownEndBlock보다 같거나 높아지는 경우 giveBirth 정상 실행된다(kittyBreeding.sol의 _isReadyToGiveBirth 참조). • matronId: 임신 중인 키티의 ID • sireId: 씨를 뿌린(아버지) 키티의 ID • siringWithId: 키티가 임신 중일 경우 matronId를 설정(임신 중이지 않을 경우 0) • coolDownIndex: 현재 cooldown의 index. 교배 후에 재교배하기까지 어느정도 기다리는지 index에 따라 시간이 결정됨 • generation: 키티의 제네레이션의 수. 부모의 제네레이션의 수에 1를 더한 수(최초의 키티는 0). 여기서 부모 제네레이션 중 높은 값을 토대로 정해진다 (e.g. 아버지 generation= 8, 어머니 generation= 2 인 경우 child generation은 9) 키티는 다양한 속성을 지니므로 구조체(struct)를 이용하여 표현한다. 타입명은 Kitty이다. * KittyBase.sol에 정의
  • 30. 4.1 Kitty property 출처: https://cryptokittydex.com/kitties/471433 tokenId property
  • 31. 4.2 크립토키티에서 ERC721이 어떻게 쓰였나? – 시작에 앞서 크립토키티에서 사용된 ERC721 token은 standard가 아닌 draft 버전이다. 크립토키티 게임이 나올 시점(약 2017년 11월)에 ERC721 standard가 정의되어 있지 않았기 때문이다. 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); } <cryptokitties에서 사용된 ERC721>
  • 32. 4.2 크립토키티에서 ERC721이 어떻게 쓰였나? – 주요 변수 /// @dev A mapping from cat IDs to the address that owns them. All cats have /// some valid owner address, even gen0 cats are created with a non-zero owner. mapping (uint256 => address) public kittyIndexToOwner; // @dev A mapping from owner address to count of tokens that address owns. // Used internally inside balanceOf() to resolve ownership count. mapping (address => uint256) ownershipTokenCount; /// @dev A mapping from KittyIDs to an address that has been approved to call /// transferFrom(). Each Kitty can only have one approved address for transfer /// at any time. A zero value means no approval is outstanding. mapping (uint256 => address) public kittyIndexToApproved; /// @dev A mapping from KittyIDs to an address that has been approved to use /// this Kitty for siring via breedWith(). Each Kitty can only have one approved /// address for siring at any time. A zero value means no approval is outstanding. mapping (uint256 => address) public sireAllowedToAddress; Kitty[] kitties; struct Kitty { uint256 genes; uint64 birthTime; uint64 cooldownEndBlock; uint32 matronId; uint32 sireId; uint32 siringWithId; uint16 cooldownIndex; uint16 generation; }
  • 33. 4.2 크립토키티에서 ERC721이 어떻게 쓰였나? – flow 교배(breed)를 통해 키티가 태어나면(birth) kitties 배열에 추가(push)된다. 이때 배열의 index가 ERC721 토큰에서의 고유한 값(tokenId)으로 활용된다. Kitty[] kitties 32142 … 32143 32144 32145 32146 32147 index tokenId 1. 교배(breed) breedWithAuto(uint256 _matronId, uint256 _sireId) matronId: 32147 sireId: 32146
  • 34. Kitty[] kitties 32142 … 32143 32144 32145 32146 32147 index tokenId 2. 키티 생성(birth) giveBirth(uint256 _matronId) Kitty memory _kitty = Kitty({ genes: _genes, birthTime: uint64(now), cooldownEndBlock: 0, matronId: uint32(_matronId), sireId: uint32(_sireId), siringWithId: 0, cooldownIndex: cooldownIndex, generation: uint16(_generation) }); 4.2 크립토키티에서 ERC721이 어떻게 쓰였나? – flow matronId: 32147 sireId: 32146 교배(breed)를 통해 키티가 태어나면(birth) kitties 배열에 추가(push)된다. 이때 배열의 index가 ERC721 토큰에서의 고유한 값(tokenId)으로 활용된다.
  • 35. Kitty[] kitties 32142 … 32143 32144 32145 32146 32147 32148 index tokenId 3. 배열에 추가(push 메서드는 배열의 길이를 return) uint256 newKittenId = kitties.push(_kitty) - 1; Kitty memory _kitty = Kitty({ genes: _genes, birthTime: uint64(now), cooldownEndBlock: 0, matronId: uint32(_matronId), sireId: uint32(_sireId), siringWithId: 0, cooldownIndex: cooldownIndex, generation: uint16(_generation) }); 4.2 크립토키티에서 ERC721이 어떻게 쓰였나? – flow matronId: 32147 sireId: 32146 교배(breed)를 통해 키티가 태어나면(birth) kitties 배열에 추가(push)된다. 이때 배열의 index가 ERC721 토큰에서의 고유한 값(tokenId)으로 활용된다.
  • 36. Kitty[] kitties 32142 … 32143 32144 32145 32146 32147 32148 index tokenId uint256 newKittenId = kitties.push(_kitty) - 1; 4. 소유주 기록(matron 소유주가 소유하게 됨) _transfer(0, _owner, newKittenId);_owner = kittyIndexToOwner[_matronId] 4.2 크립토키티에서 ERC721이 어떻게 쓰였나? – flow matronId: 32147 sireId: 32146 0xD5bfCb58f900b31c8155057616482A9d4ad70170 owner 교배(breed)를 통해 키티가 태어나면(birth) kitties 배열에 추가(push)된다. 이때 배열의 index가 ERC721 토큰에서의 고유한 값(tokenId)으로 활용된다.
  • 37. 4.2 크립토키티에서 ERC721이 어떻게 쓰였나? - 정리 Kitty[] kitties 32142 … 32143 32144 32145 32146 32147 0xBb02904FB51b462D8E72a1532279dBb4AE53a365 0x14d591E70c1A63736970e9dEaa8ad1e7DC178B1b 0x14d591E70c1A63736970e9dEaa8ad1e7DC178B1b 0x9108e25f92B70F603Ec4F3a7A1A7AB18ccAe02F5 … kitty owner(address) 생성되는 키티를 kitties[] 배열에 기록하고, 배열의 index를 tokenId로 활용 각 tokenId 마다 소유주 address가 mapping되며, 이를 통해 tokenId에 해당하는 kitty의 소유 증명(ERC721) 및 거래
  • 38. 4.3 크립토키티 주요 메서드 분석 - breed function breedWithAuto(uint256 _matronId, uint256 _sireId) external payable whenNotPaused { // Checks for payment. require(msg.value >= autoBirthFee); // Caller must own the matron. require(_owns(msg.sender, _matronId)); require(_isSiringPermitted(_sireId, _matronId)); // Grab a reference to the potential matron Kitty storage matron = kitties[_matronId]; // Make sure matron isn't pregnant, or in the middle of a siring cooldown require(_isReadyToBreed(matron)); // Grab a reference to the potential sire Kitty storage sire = kitties[_sireId]; // Make sure sire isn't pregnant, or in the middle of a siring cooldown require(_isReadyToBreed(sire)); // Test that these cats are a valid mating pair. require(_isValidMatingPair( matron, _matronId, sire, _sireId )); // All checks passed, kitty gets pregnant! _breedWith(_matronId, _sireId); } function _isSiringPermitted(uint256 _sireId, uint256 _matronId) internal view returns (bool) { address matronOwner = kittyIndexToOwner[_matronId]; address sireOwner = kittyIndexToOwner[_sireId]; // Siring is okay if they have same owner, or if the matron's owner was given // permission to breed with this sire. return (matronOwner == sireOwner || sireAllowedToAddress[_sireId] == matronOwner); } KittyBreeding.sol KittyBreeding.sol function _isReadyToBreed(Kitty _kit) internal view returns (bool) { // In addition to checking the cooldownEndBlock, we also need to check to see if // the cat has a pending birth; there can be some period of time between the end // of the pregnacy timer and the birth event. return (_kit.siringWithId == 0) && (_kit.cooldownEndBlock <= uint64(block.number)); } KittyBreeding.sol 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); } KittyBreeding.sol
  • 39. 4.3 크립토키티 주요 메서드 분석 - Birth 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; } function _createKitty (uint256 _matronId, uint256 _sireId, uint256 _generation, uint256 _genes, address _owner) internal returns (uint){ ~ 코드 생략 Kitty memory _kitty = Kitty({ genes: _genes, birthTime: uint64(now), cooldownEndBlock: 0, matronId: uint32(_matronId), sireId: uint32(_sireId), siringWithId: 0, cooldownIndex: cooldownIndex, generation: uint16(_generation) }); //The function returns the new length //newKittenId = 새로 생성된 kitty가 기록된 배열의 index, 고유한 값이다. uint256 newKittenId = kitties.push(_kitty) - 1; require(newKittenId == uint256(uint32(newKittenId))); // This will assign ownership, and also emit the Transfer event as // per ERC721 draft _transfer(0, _owner, newKittenId); return newKittenId; } 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]; } Transfer(_from, _to, _tokenId); } KittyBreeding.sol KittyBase.sol KittyBase.sol
  • 40. 4.3 크립토키티 주요 메서드 분석 - transfer function transfer( address _to, uint256 _tokenId ) external whenNotPaused { // Safety check to prevent against an unexpected 0x0 default. require(_to != address(0)); // Disallow transfers to this contract to prevent accidental misuse. // The contract should never own any kitties (except very briefly // after a gen0 cat is created and before it goes on auction). require(_to != address(this)); // Disallow transfers to the auction contracts to prevent accidental // misuse. Auction contracts should only take ownership of kitties // through the allow + transferFrom flow. require(_to != address(saleAuction)); require(_to != address(siringAuction)); // You can only send your own cat. require(_owns(msg.sender, _tokenId)); // Reassign ownership, clear pending approvals, emit Transfer event. _transfer(msg.sender, _to, _tokenId); } 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); } function _owns(address _claimant, uint256 _tokenId) internal view returns (bool) { return kittyIndexToOwner[_tokenId] == _claimant; } KittyOwnership.sol KittyOwnership.sol KittyBase.sol * transferFrom 생략
  • 41. 4.3 크립토키티 주요 메서드 분석 - approve function approve( address _to, uint256 _tokenId ) external whenNotPaused { // Only an owner can grant transfer approval. require(_owns(msg.sender, _tokenId)); // Register the approval (replacing any previous approval). _approve(_tokenId, _to); // Emit approval event. Approval(msg.sender, _to, _tokenId); } function _owns(address _claimant, uint256 _tokenId) internal view returns (bool) { return kittyIndexToOwner[_tokenId] == _claimant; } function _approve(uint256 _tokenId, address _approved) internal { kittyIndexToApproved[_tokenId] = _approved; } KittyOwnership.sol KittyOwnership.sol KittyOwnership.sol
  • 42. 참조 자료 eip-ERC721 standard https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md openzeppelin ERC721 implementation https://github.com/OpenZeppelin/openzeppelin-solidity/tree/master/contracts/token/ERC721 The Anatomy of ERC721 https://medium.com/crypto-currently/the-anatomy-of-erc721-e9db77abfc24 Walking Through the ERC721 Full Implementation https://medium.com/blockchannel/walking-through-the-erc721-full-implementation-72ad72735f3c cryptokitties github https://github.com/cryptocopycats/awesome-cryptokitties/tree/master/contracts How to Code Your Own CryptoKitties-Style Game on Ethereum https://medium.com/loom-network/how-to-code-your-own-cryptokitties-style-game-on-ethereum-7c8ac86a4eb3 CryptoKitties Clone In 20 minutes. Non-fungible Token Tutorial https://maksimivanov.com/posts/gradient-coin-tutorial/#minting 크립토 키티 코드 분석 https://steemkr.com/@kimyd CryptoKitties Breeding Guide https://segmentnext.com/2017/12/08/cryptokitties-breeding-guide/