[Lesson2] Zombie Feeding

Seokhun Yoon·2022년 2월 11일
0

[Solidity] CryptoZombie

목록 보기
2/5
post-thumbnail
post-custom-banner

Zombie Feeding 만들기

Crypto Zombie lesson2 링크

이전 ZombieFactory에서는 랜덤한 DNA를 갖는 좀비를 만들어보았다.
이번엔 우리가 만든 좀비가 사람을 물게 되면, 그 사람도 좀비로 변하게 해보자.
물려서 생성된 좀비의 DNA는 이전 좀비의 DNA와 사람의 DNA를 계산한 값을 입력할 것이다.

1. Addresses & Mappings

Addersses

이더리움 블록체인은 여러 account들로 이루어져있다.
은행계좌와 마찬가지로 각 account에는 잔고가 들어있는데, 이더리움 내 화폐 단위인 Ether로 기록된다.
계좌를 송금하듯이 이 Ether를 다른 계좌로 보내거나 혹은 내 계좌로 받을 수도 있다.
이더를 주고받기 위해 모든 account에는 고유한 address을 가지는데, 아래와 같이 해쉬값으로 표현된다.

0x05a56e2d52c817161883f50c441c3228cfe54d9f // 이더리움 첫 miner 주소

Mappings

좀비를 소유하기 위해선 address와 내 좀비를 연결해주어야한다.
솔리디티에서 데이터를 저장하는 방식 중 하나인 mapping을 이용하면 쉽게 연결할 수 있다.
mappingkey-value 방식으로 저장을 하는데, 자바스크립트의 object와 비슷하다.
정의 방식은 아래와 같다.

// zombieToOwner : 좀비 아이디(uint) => 소유자 지갑 주소(address) 
mapping (uint => address) public zombieToOwner;

// ownerZombieCount : 사용자 지갑 주소(address) => 소유한 좀비 수(uint) 
mapping (address => uint) ownerZombieCount;

2. Msg.sender

솔리디티에는 모든 함수에서 사용 가능한 특정 전역변수를 가지고 있는데, msg.sender가 그 중 하나다.

  • msg.sender : 현재 함수 혹은 스마트 컨트랙트를 호출한 사람의 address

이를 이용해서 생성된 좀비가 현재 사용자의 소유가 되게 만들어보자.

contract ZombieFactory {

 ...
 
  // zombie id => user's address : zombie를 소유한 사용자 매핑
  mapping (uint => address) public zombieToOwner;
  // user's address => number of owned zombies : 사용자가 소유한 좀비 수 매핑
  mapping (address => uint) ownerZombieCount;

  // 좀비의 name과 dna를 이용해서 좀비 생성하는 함수
  function _createZombie(string memory _name, uint _dna) private {
    uint id = zombies.push(Zombie(_name, _dna)); // 생성한 좀비는 zombis 배열에 추가, 배열의 인덱스 : 좀비의 id
    zombieToOwner[id] = msg.sender; // 생성된 좀비와 현재 사용자를 매핑함
    ownerZombieCount[msg.sender]++; // 현재 사용자의 좀비 보유 수를 1 증가 시킴
    emit NewZombie(id, _name, _dna); // 새로운 좀비가 생성됐다는 이벤트 발생
  }
  
  ...
}

3. Require

_createZombie()를 사용한 만큼 한 사용자 당 너무 많은 좀비가 생성된다.
한 사용자에게 하나의 좀비만 생성될 수 있게하려면 어떻게 해야할까?
require를 사용하면 된다!
require은 특정 조건이 참이 아닐 때, 에러 메시지를 발생시키고 함수 실행을 멈출 수 있다.

contract ZombieFactory {

 ...
 
  // 랜덤한 새로운 좀비 생성하는 함수
  function createRandomZombie(string memory _name) public {
  	require(ownerZombieCount[msg.sender] == 0); // 보유한 좀비 수가 0인 경우에만 함수 실행
    uint randDna = _generateRandomDna(_name);
    _createZombie(_name, randDna);
  } 
}

4. Inheritance

자바스크립트에서 클래스가 다른 클래스를 상속할 때 extends를 쓰듯이, 솔리디티에서도 contract를 상속할 수 있다.
ZombieFeeding이라는 컨트랙트를 만들어서 ZombieFactory를 상속해보자.

contract ZombieFactory {
	...
} 

// ZombieFeeding이 ZombieFactory를 상속함
contract ZombieFeeding is ZombieFactory {

}   

5. Import

한 파일에서 여러개의 컨트랙트를 사용하면 가독성이 떨어지므로, ZombieFeeding 컨트랙트를 새로운 파일 zombiefeeding.sol에서 정의해보자.
이렇게 하려면 상속하려는 ZombieFactoryimport해야한다.
import 방법은 자바스크립트와 동일하게, import "경로" 형식으로 작성하면 된다.

/*********************/
/* zombiefeeding.sol */
/*********************/

pragma solidity >=0.5.0 <0.6.0;

import "./zombiefactory.sol";

// ZombieFeeding이 ZombieFactory를 상속함
contract ZombieFeeding is ZombieFactory {

}

6. Storage vs Memory

솔리디티에는 변수를 저장하는 두 가지 공간이 존재한다.

  • storage : 변수를 블록체인에 영원히 저장 (state 변수)
  • memory : 변수를 임시저장하는 곳 (함수 안의 지역 변수)
    일반적으로 default로 state 변수storage에, 지역변수memory에 저장되기 때문에 따로 지정하지 않아도 된다.
    하지만, structarray를 함수 안에서 사용할 때는 용도에 따라 지정해주는 것이 좋다.

그리고 storage를 활용해서 내 좀비의 DNA를 불러오고 변해버린 좀비의 DNA는 아래와 같이 계산한다.

  • 물려서 변해버린 좀비의 DNA = (내 좀비의 DNA + 물린 생명체의 DNA) /2
contract ZombieFeeding is ZombieFactory {
  function feedAndMultiply(uint _zombieId, uint _targetDna) public {
    require(msg.sender == zombieToOwner[_zombieId]);	// 소유한 좀비만 feeding 가능하게 함
    Zombie storage myZombie = zombies[_zombieId];		// 내 좀비를 zombies 배열에서 가져옴(storage 사용!)
    _targetDna = _targetDna % dnaModulus; 				// target DNA를 16자리 수로 만듦
    uint newDna = (myZombie.dna + _targetDna) / 2; 		// 물려서 변한 좀비의 DNA 계산
    _createZombie("NoName", newDna);  					// 임시로 "NoName"이라는 이름을 가진 좀비 생성
  }
}

하지만, 이대로 컴파일을 하면 에러가 발생한다.
_createZombie()private 함수라 외부 컨트랙트에서 사용할 수 없기 떄문이다. (상속하는 컨트랙트에서도 불가능)
따라서 제대로 사용하기 위해서는 함수 접근 제어자를 수정해야한다.

7. Funcion Visibility

Internal vs External

솔리디티에서는 publicprivate 뿐만 아니라, internalexternal라는 함수 제어자가 존재한다.

  • internal : private과 동일하지만, 상속하는 컨트랙트에서도 접근이 가능하다는 점이 다르다.
  • external : public과 동일하지만, 오직 컨트랙트 밖에서만 사용이 가능하다는 점이 다르다.

_createZombie() 함수를 상속하는 컨트랙트에서는 사용할 수 있도록, privateinternal로 수정해주자.

contract ZombieFactory {

    ...
    
    // edit function definition below
    function _createZombie(string memory _name, uint _dna) internal {
        uint id = zombies.push(Zombie(_name, _dna)) - 1;
        zombieToOwner[id] = msg.sender;
        ownerZombieCount[msg.sender]++;
        emit NewZombie(id, _name, _dna);
    }
    ...
}

8. Interface

이제는 좀비가 어떤 먹이를 먹게 할지 정해보자.
여기서는 다른 스마트 컨트랙트인 CryptoKitties를 먹이로 사용할 것이다.
(크립토키티의 데이터가 블록체인에 있고, 블록체인은 데이터를 투명하게 저장하기 때문에 우리가 이 데이터를 직접 읽어올 수 있다!)
이렇게 다른 컨트랙트를 불러올 때는 Interface를 사용하면 된다.
interface는 기존의 컨트랙트를 정의하는 방식과 비슷하게 적어주면 된다.
다만, 함수는 중괄호를 사용한 { 본문 내용 }을 생략한다.
(인터페이스를 이용하면 함수의 내부 동작 내용을 알 수는 없고 사용만 가능하다!)
또한 이더리움 블록체인 내의 컨트랙트를 가져오려면 그 컨트랙트의 주소를 이용해야한다.

아래 코드와 같이 크립토키티 컨트랙트의getKitty()함수를 불러보자.

contract KittyInterface {
  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
  );
}

contract ZombieFeeding is ZombieFactory {

  address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;	// CryptoKitties 컨트랙트 주소
  KittyInterface kittyContract = KittyInterface(ckAddress);			// kittyContract로 CryptoKitties의 getKitty 함수를 호출

  ...

}

9. Handling Multiple Return Values

솔리디티에서는 하나의 함수가 여러개의 반환값을 가질 수 있다.
그렇다면 다수의 반환값을 어떻게 다뤄야 할까?
아래 예시 코드를 보자.

function multipleReturns() internal returns(uint a, uint b, uint c) {
  return (1, 2, 3);
}

function processMultipleReturns() external {
  uint a;
  uint b;
  uint c;
  // 3개의 반환값을 각각 a, b, c에 할당한다.  
  (a, b, c) = multipleReturns(); // a=1, b=2, c=3
}

// 만약 하나의 리턴값만 할당하려면
function getLastReturnValue() external {
  uint c;
  // 다른 필드는 빈칸으로 입력한다.
  (,,c) = multipleReturns();
}

이를 이용해서 getKitty()의 리턴값을 처리해보자.

...

contract ZombieFeeding is ZombieFactory {
  ...
  
  function feedOnKitty(uint _zombieId, uint _kittyId) public {
    uint kittyDna;
    (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
    feedAndMultiply(_zombieId, kittyDna);
  }
}

10. If statements

솔리디티에서 if 조건문은 자바스크립트의 if 조건문과 똑같이 사용이 가능하다.
이를 사용해서 특수 좀비들을 만들어보자.
CryptoKitties를 먹은 경우, 새로운 좀비는 고양이 타입의 좀비가 되어야 한다.
이를 위해 좀비 DNA의 제일 마지막 2자리를 특수 특성으로 지정할 것이다.
마지막 2자리 숫자가 99이면 고양이 좀비가 되도록 코드를 작성해보자.

...
contract ZombieFeeding is ZombieFactory {
  address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;
  KittyInterface kittyContract = KittyInterface(ckAddress);
  
  // 인자로 _species 를 추가함
  function feedAndMultiply(uint _zombieId, uint _targetDna, string memory _species) public {
    ...
    
    if (keccak256(abi.encodePacked(_species)) == keccak256(abi.encodePacked("kitty"))) {
      newDna = newDna - newDna % 100 + 99;			// 만약 _species가 "kitty" 라면 DNA의 맨 뒷자리를 99로 변환
    }
    
    _createZombie("NoName", newDna);                // 임시로 "NoName"이라는 이름을 가진 좀비 생성
  }

  function feedOnKitty(uint _zombieId, uint _kittyId) public {
    uint kittyDna;
    (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
    feedAndMultiply(_zombieId, kittyDna, "kitty");	// 함수 마지막 인자에 "kitty" 넣어줌
  }
}

전체 코드

zombiefactoy.sol

pragma solidity >=0.5.0 <0.6.0;

contract ZombieFactory {
  
  event NewZombie(uint id, string name, uint dna); // 새로운 좀비가 생성됐을 때의 이벤트 정의

  uint dnaDigits = 16;  // DNA는 16자리 수
  uint dnaModulus = 10 ** dnaDigits;  // 16자리 수보다 많은 경우, 16자리 수보다 큰 수는 제외할 때 사용
  
  // Zombie : name, dnan 값을 가짐
  struct Zombie {
    string name;
    uint dna;
  }
  
  // zombies : Zombie로 이루어진 배열
  Zombie[] public zombies;

  // zombie id => user's address : zombie를 소유한 사용자 매핑
  mapping (uint => address) public zombieToOwner;
  // user's address => number of owned zombies : 사용자가 소유한 좀비 수 매핑
  mapping (address => uint) ownerZombieCount;

  // 좀비의 name과 dna를 이용해서 좀비 생성하는 함수
  function _createZombie(string memory _name, uint _dna) internal {
    uint id = zombies.push(Zombie(_name, _dna)); // 생성한 좀비는 zombis 배열에 추가, 배열의 인덱스 : 좀비의 id
    zombieToOwner[id] = msg.sender;   // 생성된 좀비와 현재 사용자를 매핑함
    ownerZombieCount[msg.sender]++;   // 현재 사용자의 좀비 보유 수를 1 증가 시킴
    emit NewZombie(id, _name, _dna);  // 새로운 좀비가 생성됐다는 이벤트 발생
  }
  
  // 좀비의 이름으로 랜덤한 dna 발생하는 함수
  function _generateRandomDna(string memory _name) private view returns (uint) {
    uint randomDna = uint(keccak256(abi.encodePacked(_name))); // keccak256으로 유사 난수를 발생시켜 dna 값으로 사용
    return randomDna % dnaModulus;    // 랜덤 DNA가 16자리가 되도록 dnaModulus를 나눈 나머지를 반환
  }

  // 랜덤한 새로운 좀비 생성하는 함수
  function createRandomZombie(string memory _name) public {
    require(ownerZombieCount[msg.sender] == 0);
    uint randDna = _generateRandomDna(_name);
    _createZombie(_name, randDna);
  } 
} 

zombiefeeding.sol

pragma solidity >=0.5.0 <0.6.0;

import "./zombiefactory.sol";

contract KittyInterface {
  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
  );
}

// ZombieFeeding이 ZombieFactory를 상속함
contract ZombieFeeding is ZombieFactory {
  address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;
  KittyInterface kittyContract = KittyInterface(ckAddress);

  function feedAndMultiply(uint _zombieId, uint _targetDna, string memory _species) public {
    require(msg.sender == zombieToOwner[_zombieId]);  // 소유한 좀비만 feeding 가능하게 함
    Zombie storage myZombie = zombies[_zombieId];     // 내 좀비를 zombies 배열에서 가져옴(storage 사용!)
    _targetDna = _targetDna % dnaModulus;             // target DNA를 16자리 수로 만듦
    uint newDna = (myZombie.dna + _targetDna) / 2;    // 물려서 변한 좀비의 DNA 계산
    if (keccak256(abi.encodePacked(_species)) == keccak256(abi.encodePacked("kitty"))) {
      newDna = newDna - newDna % 100 + 99;            // 만약 _species가 "kitty" 라면 DNA의 맨 뒷자리를 99로 변환
    }
    _createZombie("NoName", newDna);                  // 임시로 "NoName"이라는 이름을 가진 좀비 생성
  }

  function feedOnKitty(uint _zombieId, uint _kittyId) public {
    uint kittyDna;
    (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
    feedAndMultiply(_zombieId, kittyDna, "kitty");
  }
}
profile
블록체인 개발자를 꿈꾸다
post-custom-banner

0개의 댓글