카테고리 없음

Cryptozombies_Lesson2. 좀비가 희생물을 공격하다

IT공부방 2023. 11. 9. 16:05

Lesson2 목표

좀비를 랜덤으로만 생성하지 않고, 좀비가 다른 생명체를 "먹도록"해서 새로운 좀비를 생성해보자!

새로운 좀비의 DNA는 이전 좀비의 DNA와 먹이의 DNA를 활용하여 계산될 것.

 

 

매핑과 주소

주소

  • 이더리움 블록체인은 은행 계좌와 같은 계정들로 이루어져 있다.
  • 계정은 이더리움 블록체인 상의 통화인 "_이더_"의 잔액을 가진다.
  • 계정을 통해 다른 계정과 이더를 주고 받을 수 있다.
  • 각 계정은 은행 계좌번호와 같은 "주소"를 가지고 있다.
  • 주소는 특정 계정을 가리키는 "고유 식별자"이다.
  • 주소는 "특정 유저(또는 스마트 컨트랙트)"가 소유한다.
  • 여기서 주소는 좀비들에 대한 소유권을 나타내는 고유ID로 활용할 수 있다. 즉, 유저가 새로운 좀비를 생성하면, 좀비를 생성하는 함수를 호출한 이더리움 주소에 그 좀비에 대한 소유권을 부여한다.

매핑

  • 솔리디티에서 구조화된 데이터를 저장하는 방법이다.
  • 기본적으로 키-값저장소로, 데이터를 저장하고 검색하는 데 이용된다.
  • 예1) mapping (address => uint) public accountBalance;   ----> 키 : address, 값 : uint  // 계좌 잔액을 보유하는 uint를 저장
  • 예2) mapping (uiunt => string) userIdToName; //userID로 유저 이름을 저장

*실습*

  • 좀비 소유자를 추적하는 매핑 만들기
mapping (uint => address) public zombieToOwner; // 좀비 id로 좀비를 저장 및 검색
mapping (address => uint) ownerZombieCount; // 좀비 주소로 ownerZombie의 수를 셀 것.

 

 

Msg.sender

전역 변수

  • Msg.sender는 현재 함수를 호출한 사람(또는 스마트컨트랙트)의 "주소"를 가리킨다.
  • 참고로, 솔리디티에서 함수 실행은 항상 외부 호출자가 실행한다. 따라서 컨트랙트는 누군가 컨트랙트의 함수를 호출할 때까지 블록체인에서 아무 것도 안 하고 있어야 한다. 그래서 항상 msg.sender가 있어야 한다.

*실습*

1) 새로운 좀비의 id가 반환된 후, zombieToOwner 매핑을 업데이트해서 id에 대하여 msg.sender가 저장되도록 만들기.

2) 저장된 msg.sender를 고려하여 ownerZombieCount를 증가시키기

mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;

function _createZombie(string _name, uint _dna) private {
uint id = zombies.push(Zombie(_name, _dna)) - 1;
zombieToOwner[id] = msg.sender; // 1)
ownerZombieCount[msg.sender]++; // 2)
NewZombie(id, _name, _dna);
}

 

 

Require

  • require함수를 활용하면, 특정 조건이 참이 아닐 때 함수가 에러 메시지를 발생하고 실행을 멈춘다.
  • require 함수를 실행하기 전에, 참이어야 하는 특정 조건을 확인하는 데 유용하다.
  • cryptozombie에서는 require 함수를 활용해서 각 플레이어가 한 번만 createRandomZombie 함수를 호출하도록 만들 것이다. 즉, 새로운 플레이어들이 게임을 처음 시작할 때 좀비 군대를 위한 첫 좀비를 생성하기 위해 createRandomZombie 함수를 호출하게 된다.

 

*실습*

  • require를 활용해 유저들이 첫 좀비를 만들 때 이 함수가 유저 당 한 번만 호출되도록 해보기.
function createRandomZombie(string _name) public {
   require(ownerZombieCount[msg.sender] == 0 ); //ownerZombieCount[msg.sender]가 0일 때, createRandomZombie 실행 가능.
   uint randDna = _generateRandomDna(_name);
   _createZombie(_name, randDna);
}

 

 

상속

  • 여러 컨트랙트에 코드 로직을 관리하기 쉽게 나눌 수 있다.
  • 상속을 받는 컨트랙트는 상위 컨트랙트의 함수에 접근 가능하다.
  • 그러나 상위 컨트랙트에서 어떤 함수를 private로 설정하면, 해당 컨트랙트를 상속하는 어떤 컨트랙트도 해당 함수에 접근할 수 없다.

*실습*

  • ZombieFeeding 컨트랙트는 ZombieFactory 컨트랙트를 상속받음.
pragma solidity ^0.4.19;

contract ZombieFactory {
   ....
}

contract ZombieFeeding is ZombieFactory {

}

 

 

Import

  • 다수의 파일이 있고, 어떤 파일을 다른 파일로 불러오고 싶을 때 import라는 키워드를 사용한다.

*실습*

  • zombiefeeding.sol에 zombiefactory.sol 불러오기
//zombiefeeding.sol
 
pragma solidity ^0.4.19;

import "./zombiefactory.sol"; // import문 사

contract ZombieFeeding is ZombieFactory {

}

 

 

Storage vs Memory

  • 솔리디티에서는 변수를 저장하는 공간으로 storage와 memory가 있다.
  • storage는 블록체인 상에 영구적으로 저장되는 변수이다.
  • memory는 임시적으로 저장되는 변수로, 컨트랙트 함수에 대한 외부 호출들이 일어나는 사이에 지워진다.
  • 하지만 솔리디티가 알아서 처리해주기 때문에, 이런 키워드를 우리가 이용할 필요는 없다.
  • 상태 변수(함수 외부에 선언된 변수)는 초기 설정 상, storage로 선언된다.
  • 함수 내에 선언된 변수는 memory에 자동선언되어 함수 호출이 종료되면 사라진다.
  • 그러나, 함수 내의 "구조체"와 "배열"을 처리할 때 해당 키워드를 사용해야 한다.

*실습*

먹이를 먹고 번식하는 능력을 좀비들에게 부여하려고 한다.

좀비가 어떤 다른 생명체를 잡아 먹을 떄, 좀비 DNA가 생명체의 DNA와 혼합되어 새로운 좀비가 생성될 것이다.

그 함수를 feedAndMultiply라고 정의한다.

1) 주인만이 좀비에게 먹이를 줄 수 있도록 한다. 따라서 require문을 추가하여 msg.sender가 좀비 주인과 동일하도록 한다.

2) 먹이를 먹는 좀비 DNA를 얻어야 한다. 

*코딩을 위한 참고*
pragma solidity ^0.4.19;

contract ZombieFactory {

event NewZombie(uint zombieId, string name, uint dna);

uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;

struct Zombie {
string name;
uint dna;
}

Zombie[] public zombies;

mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;

function _createZombie(string _name, uint _dna) private {
uint id = zombies.push(Zombie(_name, _dna)) - 1;
zombieToOwner[id] = msg.sender;
ownerZombieCount[msg.sender]++;
NewZombie(id, _name, _dna);
}

function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(_str));
return rand % dnaModulus;
}

function createRandomZombie(string _name) public {
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}

}
pragma solidity ^0.4.19;

import "./zombiefactory.sol";

contract ZombieFeeding is ZombieFactory {

function feedAndMultiply (uint _zombieId, uint _targetDna) public {
  require(msg.sender == zombieToOwner[_zombieId]); //1)
  Zombie storage myZombie = zombies[_zombieId]; //2) 먹이를 먹는 좀비 DNA를 얻어야 한다
}

}

 

 

좀비 DNA

새로운 좀비의 DNA를 계산하는 공식은 다음과 같다.

먹이를 먹는 좀비의 DNA와 먹이의 DNA 평균을 계산한다.

*실습*

1) targetDna가 16자리보다 크지 않도록 설정한다.

2) 새로운 좀비의 DNA를 계산한다.

3) 새로운 DNA 값을 얻게 되면, _createZombie 함수를 호출한다.

function feedAndMultiply(uint _zombieId, uint _targetDna) public {
   require(msg.sender == zombieToOwner[_zombieId]);
   Zombie storage myZombie = zombies[_zombieId];
   _targetDna = _targetDna % dnaModulus;  //1)
   uint newDna = (myZombie.dna + _targetDna) / 2; //2)
   _createZombie("NoName", newDna); //3)
}

 

 

 

함수 접근 제어자 더 알아보기 (Internal과 External)

  • 함수를 internal로 설정할 경우, 해당 컨트랙트를 상속하는 컨트랙트에서도 접근이 가능하다. 이 점을 제외하곤 private와 동일하다. 참고로 private는 해당 컨트랙트를 상속하는 컨트랙트에서 접근이 불가능했다.
  • external은 함수가 컨트랙트 밖에서만 호출될 수 있고, 컨트랙트 내의 다른 함수에 호출될 수 없다. 이 점을 제외하곤 public과 동일하다. 참고로 public은 컨트랙트 밖이든 외부든 모두 접근 가능하다.

 

 

 interface 정의 : 다른 컨트랙트와 상호작용하기 (크립토 키티 )

  • 블록체인 상에 있으면서, 우리가 소유하지 않은 컨트랙트와 우리 컨트랙트가 상호작용을 하려면, 인터페이스를 정의해야 한다.
  • 인터페이스를 정의하는 것은, 컨트랙트를 정의하는 것과 유사하다.
  • 다른 컨트랙트와 상호작용하고자 하는 함수만을 선언할 뿐, 다른 함수나 상태 변수를 언급하진 않는다.
  • 함수 몸체를 정의하지 않는다. 즉, 중괄호를 쓰지 않고 함수 선언을 세미콜론으로 끝낸다.
  • 예) contract NumberInterface { function getNum(address _myAddress) public view returns (uint); }
  • 따라서 인터페이스는 컨트랙트 뼈대처럼 보인다고 할 수 있다.

*실습*

좀비의 먹이는 크립토키티이다.

좀비에게 크립토 키티를 먹이로 주려면, 크립토키티 스마트 컨트랙트에서 키티의 dna를 읽어와야 한다.

KittyInterface라는 인터페이스를 정의하여, 인터페이스 내에 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
 );
}

 

 

 

인터페이스 활용하기

*실습*

정의된 KittyInterface를 활용해보기.

ckAddress는 크립토키티의 컨트랙트 주소이다.

1) KittyContract라는 이름으로 KittyInterface를 생성한다. 그리고 ckAddress를 이용하여 초기화한다.

  address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;
  KittyInterface kittyContract = KittyInterface(ckAddress);