이전 ZombieFactory
에서는 랜덤한 DNA를 갖는 좀비를 만들어보았다.
이번엔 우리가 만든 좀비가 사람을 물게 되면, 그 사람도 좀비로 변하게 해보자.
물려서 생성된 좀비의 DNA는 이전 좀비의 DNA와 사람의 DNA를 계산한 값을 입력할 것이다.
이더리움 블록체인은 여러
account
들로 이루어져있다.
은행계좌와 마찬가지로 각account
에는 잔고가 들어있는데, 이더리움 내 화폐 단위인Ether
로 기록된다.
계좌를 송금하듯이 이Ether
를 다른 계좌로 보내거나 혹은 내 계좌로 받을 수도 있다.
이더를 주고받기 위해 모든account
에는 고유한address
을 가지는데, 아래와 같이 해쉬값으로 표현된다.0x05a56e2d52c817161883f50c441c3228cfe54d9f // 이더리움 첫 miner 주소
좀비를 소유하기 위해선
address
와 내 좀비를 연결해주어야한다.
솔리디티에서 데이터를 저장하는 방식 중 하나인mapping
을 이용하면 쉽게 연결할 수 있다.
mapping
은key-value
방식으로 저장을 하는데, 자바스크립트의object
와 비슷하다.
정의 방식은 아래와 같다.
// zombieToOwner : 좀비 아이디(uint) => 소유자 지갑 주소(address)
mapping (uint => address) public zombieToOwner;
// ownerZombieCount : 사용자 지갑 주소(address) => 소유한 좀비 수(uint)
mapping (address => uint) ownerZombieCount;
솔리디티에는 모든 함수에서 사용 가능한 특정 전역변수를 가지고 있는데,
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); // 새로운 좀비가 생성됐다는 이벤트 발생
}
...
}
_createZombie()
를 사용한 만큼 한 사용자 당 너무 많은 좀비가 생성된다.
한 사용자에게 하나의 좀비만 생성될 수 있게하려면 어떻게 해야할까?
require
를 사용하면 된다!
require
은 특정 조건이 참이 아닐 때, 에러 메시지를 발생시키고 함수 실행을 멈출 수 있다.
contract ZombieFactory {
...
// 랜덤한 새로운 좀비 생성하는 함수
function createRandomZombie(string memory _name) public {
require(ownerZombieCount[msg.sender] == 0); // 보유한 좀비 수가 0인 경우에만 함수 실행
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}
자바스크립트에서 클래스가 다른 클래스를 상속할 때
extends
를 쓰듯이, 솔리디티에서도contract
를 상속할 수 있다.
ZombieFeeding
이라는 컨트랙트를 만들어서ZombieFactory
를 상속해보자.
contract ZombieFactory {
...
}
// ZombieFeeding이 ZombieFactory를 상속함
contract ZombieFeeding is ZombieFactory {
}
한 파일에서 여러개의 컨트랙트를 사용하면 가독성이 떨어지므로,
ZombieFeeding
컨트랙트를 새로운 파일zombiefeeding.sol
에서 정의해보자.
이렇게 하려면 상속하려는ZombieFactory
를import
해야한다.
import
방법은 자바스크립트와 동일하게,import "경로"
형식으로 작성하면 된다.
/*********************/
/* zombiefeeding.sol */
/*********************/
pragma solidity >=0.5.0 <0.6.0;
import "./zombiefactory.sol";
// ZombieFeeding이 ZombieFactory를 상속함
contract ZombieFeeding is ZombieFactory {
}
솔리디티에는 변수를 저장하는 두 가지 공간이 존재한다.
storage
: 변수를 블록체인에 영원히 저장 (state 변수
)memory
: 변수를 임시저장하는 곳 (함수 안의지역 변수
)
일반적으로 default로state 변수
는storage
에,지역변수
는memory
에 저장되기 때문에 따로 지정하지 않아도 된다.
하지만,struct
나array
를 함수 안에서 사용할 때는 용도에 따라 지정해주는 것이 좋다.그리고
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
함수라 외부 컨트랙트에서 사용할 수 없기 떄문이다. (상속하는 컨트랙트에서도 불가능)
따라서 제대로 사용하기 위해서는 함수 접근 제어자를 수정해야한다.
솔리디티에서는
public
과private
뿐만 아니라,internal
과external
라는 함수 제어자가 존재한다.
internal
:private
과 동일하지만, 상속하는 컨트랙트에서도 접근이 가능하다는 점이 다르다.external
:public
과 동일하지만, 오직 컨트랙트 밖에서만 사용이 가능하다는 점이 다르다.
_createZombie()
함수를 상속하는 컨트랙트에서는 사용할 수 있도록,private
을internal
로 수정해주자.
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);
}
...
}
이제는 좀비가 어떤 먹이를 먹게 할지 정해보자.
여기서는 다른 스마트 컨트랙트인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 함수를 호출
...
}
솔리디티에서는 하나의 함수가 여러개의 반환값을 가질 수 있다.
그렇다면 다수의 반환값을 어떻게 다뤄야 할까?
아래 예시 코드를 보자.
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);
}
}
솔리디티에서
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" 넣어줌
}
}
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);
}
}
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");
}
}