일요일, 12월 16

[SGBL] 크립토키티 살펴보기 (4)

0

태어난 Kitty를 경매에 올리기 – giveBirth(), createKitty(), createSaleAuction()

앞 부분의 _breedWith() 함수에서 내가 선택한 두 고양이들을 교배하는 과정을 거쳤는데 이와 같이 교배를 마친 고양이 중 암컷 고양이는 임신을 하게 된다. 이 때 새끼 고양이가 탄생하도록 해주는 역할을 하는게 function giveBirth(uint256 _matronId) 이며 이 함수는 암컷 고양이의 Id를 함수 값으로 받고 kittenId를 uint 256 bit 형태의 값으로 return해준다.

<function giveBirth() – part 1>

matron = kities[_matronId];를 통해서 matron의 reference를 받아오고, matron이 valid한 고양이인지를 확인하기 위해 이 고양이의 태어난 시간인 birthTime을 확인해준다.

 <function _isReadyToGiveBirt()>

그리고 _isReadyToGiveBirth(matron)을 통해서 matron고양이가 임신 상태가 아님을 확인하고, cooldownEndBlock 을 통해서 cooldowntime 이 끝났는지를 한 번 더 확인해주는 과정을 거친다.
반면에, sire의 경우에는 확인하는 방법이 조금 다른데 이 때, matron.siringWithId는 sireId를 변환한 것이므로 siringWithId에 저장되어 있는 값인 sireId와 kitties배열에 저장되어 있는 sireId가 같은지 비교하여 유효한 sire임을 확인해준다.

두 고양이가 교배한 경우에는 두 고양이의 generation number 값 중에서 더 큰 값을 골라 그 숫자에 +1을 해준 값을 kitten의 generation값으로 정해주는데, if문을 통해서 parentGen = Max(sire.generation, matron.generation) 와 같은 방법으로 둘의 크기를 비교해서 큰 값을 parentGen에 넣어준다.

<function giveBirt() – part 2>

childGenes = geneScience.mixGenes(matron.genes, sire.genes, matron.cooldownEndBlock – 1); 에서는 mixGenes라는 function을 통해 유전자를 조합해내는 함수에 matron.genes와 sire.genes값을 넣어서 나온 결과값을 childGenes라는 변수에 넣어주는데, 이 때의 mixGenes()는 black box 로, 자식의 유전자를 계산하는 함수는 일반인들에게 공개되어있지 않다. 실제로 bytecode를 분석해서 함수를 crack해보는 시도도 있지만 아직 이를 완벽하게 알아낸 경우는 없다고 한다.

이제 뒷부분의 코드는 만들어진 childGenes 로 kitty instance를 생성하는 부분이다. owner = kittyIndexToOwner[_matronId]; 에서 kittyIndexToOwner는 _matronId와 owner의 Id를 mapping해주는 배열이므로 owner에 kitten의 주인 Id가 담긴다. 다음 라인에는 실질적으로 kitty instance를 create하여 kittenId를 받아오는 _createKitty() 함수가 호출된다.

kittenId = _createKitty(_matronId, matron.siringWithId, parentGen + 1, childGenes, owner);

 <function _createKitty() – part 1>

이 때의 function _createKitty를 자세히 보면, 인자로 _matronId, _sireId, _generation, _genes, _owner가 전달되고 new kitty를 create하여 kitteyId를 리턴 해주는 역할을 한다.

_matronId, _sireId, _generation은 256bit로 변환해서 각각 변수에 다시 저장해주고 cooldownIndex는_generation를 2로 나눈 값을 가지며, 이는 Kitty 의 _generation 숫자가 커질수록 cooldowntime 이 길어져서 다음 교배를 할 때까지 기다려야하는 시간이 길어진다는 것을 의미한다.

<_createKitty – part 2>

다음으로는 Kitty의 instance를 만들어서 _kitty라는 메모리 변수에 넣어주고 이를 kitties 배열에 push 해준다. 그리고 이 때의 kitties index를 newKittenId 변수에 저장해준다.

Event Birth는 new kitten이 만들어질 때마다 발생되는 event로, gen0 cat이 create되는 경우, 그리고 giveBirth를 통해서 새로운 고양이가 탄생하게 되는 경우에 사용된다.

<function _transfer()>

_transfer(0, _owner, newKittenId); 에서 쓰이는 function _transfer(address _from, address _ to, uint256 _tokenId) 은 kitty를 transfer 하는 경우 실행되는 함수로, kittyBase에 있는 constant들을 정리하고 kitty의 ownership을 지정해주는 역할을 하는 함수이다.

이 때 ownershipTokenCount 는 owner address와 그 address가 가지는 token의 개수를 mapping해주는 역할을 하며 이는 ownership을 count하는데 쓰이므로 owner의 token수를 하나 늘려주는 과정이다. kittyIndexToOwner의 경우에는 cat ID와 cat의 주인 address를 mapping 해주는데 valid한 owner address의 경우, 0이 아닌 값을 가진다. 따라서 kittyIndexToOwner[newkittenId] = _owner;의 경우에는 newkittenId의 ownership을 가지는 사람에 _owner을 mapping해준 것이다.

이 때 sireAllowedToAddress는 이미 언급되었지만, SiringAuction을 통해서 교배권을 넘겨준 사용자의 address를 표시해주는 배열이고, kittyIndexToApproved는 Kitty 가 한번에 한 transfer만 할 수 있게 설정해주는 배열로 두 배열에서 해당 KittyID에 해당하는 원소를 delete 해준다. 이 모든 작업을 마치고 최종적으로 transfer event를 호출하고 _transfer()는 끝나게 된다.

정리하자면, _createKitty() 함수는 새로운 kitty instance를 생성하기 위해 그 안에 들어가는 정보들을 구하고 이들을 이용해서 matron, sire과 owner에 kitty를 연결해주는 과정을 거치고 마지막으로 이를 transfer해주는 것으로 kitten을 만들어 낸다.

giveBirth() 함수의 마지막 부분은 setautoBirthFee를 사용자에게 send하는 코드가 있는데, 이 때의 autoBirthFee는 giveBirth()를 수행하는데 최소한의 금액으로, kitten을 많이 생산할 수 있도록 owner들에게 지급하여 kitten생산을 장려해주는 인센티브 역할을 한다. 이와 같이 giveBirth() 함수를 통해 sire 과 matron으로부터 kitten을 만들어 내면 이를 경매에 올릴 수 있다. 이렇게 경매에 kitten을 올리는 것은 function createSaleAuction()을 통해서 살펴볼 수 있다.

<function createSaleAuction()>

function createSaleAuction()에서는 _kittyId(경매할 kitty)와 _startingPrice(경매 시작 최소 가격), _endingPrice(경매 낙찰 가격), 그리고 _duration(경매 기간)을 지정해서 auction instance를 만드는 역할을 한다. 이 때 require(_owns(msg.sender, _kittyId)) 에서는 소유권을 체크해주는 역할을 하고 require(!isPregnant(_kittyId)) 의 경우에는 임신한 고양이의 경우 새끼 고양이의 소유권까지 넘어갈 수 있는 문제가 생기기 때문에 제한한다. 그리고 _approve(_kittyId, saleAuction) 은 특정 kittyId 대해서 saleAuction 의 교배권을 허가해주는 역할을 한다.

<function createAuction()의 인자 목록>

마지막으로 saleAuction.createAuction() 에서는 uint256으로 overflow가 일어나지 않게 몇 개의 원소들을 변환해주고 require(_owns(msg.sender, _tokenId)); 와 _escrow(msg.sender, _tokenId) 를 통해서 이 kitty의 소유권이 나에게 있음을 증명해주는 과정을 필요로 한다. 마지막으로 auction instance를 생성해서 function _addAuction 을 통해서 정해진 _tokenId에 auction을 부여해주는 역할을 한다.

따라서 createSaleAuction 함수는 정해진 owner의 _kittyId를 넘기고 msg.sender를 seller로 설정해 validation과정을 거친 뒤 function createAuction을 통해서 sale auction instance를 만들어주는 역할을 한다.

 

modifier 및 그 외 함수 –contract plausible, contract KittyAccessControl, setAddress()

Solidity에는 modifier라는 함수 실행 전에 수행 조건을 만족하는지 자동으로 검사하는 contract inheritable property(Document 참조)가 존재한다. 지금까지 언급된 함수에 사용된 modifier로는 whenNotPaused와 onlyCEO가 존재하는데, 각각 컨트랙트에 버그가 발견되었을 때 Cryptokitty 운영자 측에서 함수 호출을 막기 위한 역할과 Cryptokitty 운영자만 호출할 수 있도록 제한하는 역할을 하는데, 코드는 다음과 같다.

<가장 많이 쓰이는 modifier인 whenNotPaused, onlyCEO>

require() 안에 들어가는 조건문은 앞선 설명대로 각각 운영자만 호출 가능, 그리고 변수 paused가 false인 경우만 호출 가능이라는 의미다. 변수 paused는 onlyCLevel이라는 modifier(onlyCEO와 마찬가지로 운영진만 호출 가능함을 의미)를 갖는 함수로, 컨트랙트에서 버그를 발견했거나 업데이트가 필요한 경우에 컨트랙트의 호출을 임시로 막기 위해 함수 pause()를 통해서 변수paused를 true로 바꾼다. 이러한 modifier는 코드 자체로는 내용도 짧고 복잡한 logic이 사용되지는 않았지만, 탈중앙환경인 블록체인 상에서 실행되는 응용 프로그램에서 중앙화 요소인 운영자(CEO)라는 개념과 paused와 같은 중앙에서 프로그램을 관리, 단속하는 기능을 구현했다는 점에서 의미가 있다.

<function pause(), 주요 function들의 호출을 막는다>

Cryptokitty에서는 주요한 기능인 Kitty의 유전자 형질 결정과 경매 진행에 관련된 함수들을 하나의 컨트랙트 안에 모두 정의한 것이 아니라, 아래의 예시와 같이 외부 컨트랙트에 정의해놓고 호출하는 방식을 사용한다. 스마트 컨트랙트에서는 외부 컨트랙트의 주소를 지정하여 컨트랙트 내부에 정의된 함수가 아닌, 외부에 정의된 함수를 호출하는 기능이 있다.

<contract B에서 외부 contract A의 함수 foo()를 호출>

Cryptokitty에서 왜 굳이 이런 수고스러운 방법으로 함수를 호출하는 것일까? 여러 가지 이유가 존재하지만, 태어나는 Kitty의 유전자를 결정하는 geneScience의 경우에는 코드를 공개하지 않기 위함이 가장 큰 이유다. 유전자를 결정하는 코드가 공개되어있지 않기 때문에, 사용자들은 Kitty들을 교배했을 때 나오는 자식의 유전형질을 예측할 수 없다. 이런 임의성은 사용자들의 흥미를 유발하고 사용자들이 원하는 형질의 Kitty를 얻을 때까지 여러 번의 교배를 시도하도록 유도한다. 또 다른 주요한 이유는 코드의 수정이 간접적으로 가능하기 때문이다. 본래 스마트 컨트랙트는 일단 배포하면 코드를 수정할 수 없지만, 외부 컨트렉트 호출을 통해서 코드 수정과 비슷한 효과를 얻을 수 있다. 예를 들어 경매를 진행하는 함수에서 심각한 버그가 발견되었다고 가정하자. 만약 경매에 관련된 모든 함수가 외부 컨트랙트에 정의되어 있으면, 함수 pause()를 호출하여 사용자들의 이용을 잠시 막아두고 버그를 수정한 새로운 경매 함수들을 재배포한다. 그리고 경매 컨트랙트의 주소를 재배포한 컨트랙트의 주소로 바꿔준다(위의 예시의 callFoo()처럼 새로운 주소를 할당한다). 그리고 다시 변수 paused의 값을 false로 바꿔주고 사용자들은 버그가 해결된 함수를 호출할 수 있게 된다. CryptoKitty가 dapp 개발 입문자들에게 공부하기 좋은 이유는 잘 정리된 주석도 있지만, 이런 dapp에서만 사용되는 특별한 기법들에 대한 insight를 얻을 수 있다는 점이 가장 크다. 아래 코드들은 위에서 설명한 외부 컨트랙트 주소를 지정하는 함수다. 어떤 외부 컨트랙트의 함수를 호출할지 결정하는 함수인 만큼 운영진만 호출 가능하다는 의미의 onlyCEO라는 modifier가 붙는다.

<KittyAuction을 담당하는 contract를 지정하는 함수>

<SiringAuction을 담당하는 contract를 지정하는 함수>

<GeneScience을 담당하는 contract를 지정하는 함수>

 

  • 본 분석칼럼은 Tconomy 의 분석 분야 기여파트너인 서강대학교블록체인학회(SGBL)의 지적재산입니다. 이 분석은 분석파트너의 판단이며 Tconomy 의 편집 방향이나 의견과 일치하지 않습니다.
Share.

Comments are closed.