🏭 zombiefactory.sol
- 🍞 zombiefeeding.sol
- 👼 zombieHelper.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
);
}
KittyInterface kittyContract;
function setKittyContractAddress(address _address) external onlyOwner {
kittyContract = KittyInterface(_address);
}
smart contract
는 블록체인 위에 올라가고confirm
되면 올라간 데이터는 영원히 변하지 않는다.
이것은 장점이자 치명적인 단점이 될 수 있다 만약 내가 올려놓은smart contract의 일부 데이터가 손상된다면
해당 데이터와 연관된 smart contract에 영향을 주어 smart constract가 제대로된 기능을 수행하지 못할 것이다.
예를 들어, 아래처럼 특정 크립토 키티의 주소를 직접 들고왔다고 생각해보자
address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;
근데, 만약 이 주소의
크립토 키티 데이터가 손상되었다면
우리는 이 문제를 해결할 방법이 없다 (스마트 컨트렉트는 수정이 불가능하기 때문이다)
때문에 손상될 가능성이 있는 데이터를 직접 적지 않고 address유형의 데이터를 전부 받을 수 있게하여 address가 손상되면 다른 address로 수정할 수 있도록 해준다
import "./zombiefeeding.sol";
contract ZombieHelper is ZombieFeeding {
...
}
import
를 통해 zombiefeeding.sol
파일을 들고오고 ZombieHelper
에 상속했다
function setKittyContractAddress(address _address) external onlyOwner {
kittyContract = KittyInterface(_address);
}
chapter 1에서 크립토 키티의 주소를 유동저긍로 변경하기 위해 함수를 external로 설정했지만 게임참여자들 모두가 크립토 키티의 주소를 원하는 대로 바꿔서 zombie에게 먹이로 준다면 게임이 지루해져 버릴 것이다
때문에 openZepplin 라이브러이의 ownable contract의 onlyOwner modifier를 통해 컨트렉트 소유자만 해당 함수를 실행할 수 있도록 해준다
struct Zombie {
string name;
uint dna;
uint32 level;
uint32 readyTime;
}
uint8 uint16 uint32...
처럼 정수크기type을 변경시키는 행위는 이더리움 gas fee 절약에 아무런 영향을 주지 않는다하지만
struct(구조체)
안에서 여러 개의uint
를 만든다면, 가능한 더 작은 크기의uint
를 쓰도록 하는것이 좋다.솔리디티에서 그 변수들을 더 적은 공간을 차지하도록 압축할 것이다.
또한동일한 데이터 타입은 하나로
묶어놓는 것이 좋다.
uint32
가 두개 사용되어야 한다면 위의 코드처럼 연속되는 줄에uint32
의 변수들을 적어주는 것이 좋다.
uint cooldownTime = 1 days;
...
function _createZombie(string _name, uint _dna) internal {
uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime))) - 1;
zombieToOwner[id] = msg.sender;
ownerZombieCount[msg.sender]++;
NewZombie(id, _name, _dna);
}
solidity
에서는 seconds, minutes, hours, days, weeks, years
와 같은 시간단위를 사용하고 이것은 모두 초 단위로 표현될 수 있다
function _triggerCooldown(Zombie storage _zombie) internal {
_zombie.readyTime = uint32(now + cooldownTime);
}
function _isReady(Zombie storage _zombie) internal view returns (bool) {
return (_zombie.readyTime <= now);
}
Zombie 구조체
도 함수의 파라미터
로 전달이 가능하다
function feedAndMultiply(uint _zombieId, uint _targetDna, string _species) internal { // public -> internal
require(msg.sender == zombieToOwner[_zombieId]);
Zombie storage myZombie = zombies[_zombieId];
require(_isReady(myZombie)); // zombie가 cooldowntime이 지났는지 확인
_targetDna = _targetDna % dnaModulus;
uint newDna = (myZombie.dna + _targetDna) / 2;
if (keccak256(_species) == keccak256("kitty")) {
newDna = newDna - newDna % 100 + 99;
}
_createZombie("NoName", newDna);
_triggerCooldown(myZombie); // zombie에게 먹이를 먹였음으로 cooldonwtime 부여(+ 1 days)
}
이 함수는 오직
feedOnKitty()
에 의해서만 호출이 될 필요가 있다.
때문에feedAndMultiply
함수를public
에서internal
로 바꿔줬다(feedOnKitty는 feedAndMultiply의 상속을 받음).
public
으로feedAndMultiply
함수를 만들면 게임 참여자들은 그들이 원하는 아무_targetDna
나_species
를 자신들의 zombie에게 전달할 수 있을 것이다(이렇게 되면 게임이 재미없겠다)
contract ZombieHelper is ZombieFeeding {
modifier aboveLevel(uint _level, uint _zombieId) {
require(zombies[_zombieId].level >= _level);
_;
}
...
}
함수제어자 modifier
에도 파라미터
를 부여할 수 있다
function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].name = _newName;
}
function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].dna = _newDna;
}
chapter 8에서 만든 modifier aboveLevel을 함수에 적용했다
이 작업을 통해 이제 zombie는 level 2가 되면 이름 변경을 할 수 있는 함수(changeName)를 사용할 수 있고
level 20이 되면 특정 dna를 zombie에게 부여할 수 있는 함수(changeDna)를 사용할 수 있게된다
function getZombiesByOwner(address _owner) external view returns(uint[]) {
//동적 배열 uint[]에 memory를 적용해 gas fee를 절약했다
//동적 배열 uint[]를 선언한 후 변수를 입력받아 배열의 길이가 달라지길 원한다면 new를 선언해줘야 한다
uint[] memory result = new uint[](ownerZombieCount[_owner]);
for(uint i = 0; i < zombies.length; i++) {
if(zombieToOwner[i] == _owner) {
result[counter] = i;
counter++;
}
}
return result;
}
view
함수는 사용자에 의해 외부에서 호출되었을 때 가스를 전혀 소모하지 않는다.
view
함수가 블록체인 상에서 실제로 어떤 것도 수정하지 않기 떄문이다(데이터를 읽기만 한다).하지만 만약
view
함수가 동일 컨트랙트 내에 있는, view 함수가 아닌 다른 함수에서 내부적으로 호출될 경우, 여전히 가스를 소모할 것이다.
동적 array(uint[])
는memory 배열
로 만들어질 수 없다 때문에new
를 통해 동적 배열의 길이가 얼마인지(ownerZombieCount[_owner]
) 알려줘야한다
view
함수는 외부에서 호출될 때 가스를 사용하지 않기 때문에,getZombiesByOwner
함수에서for 반복문
을 사용해서 좀비 배열의 모든 요소에 접근한 후 특정 사용자의 좀비들로 구성된 배열을 만들 수 있다
(storage
를 쓰지않고 특정 배열을 순회후 원하는 요소만 뽑아서 배열을 return하는 방법).
pragma solidity ^0.4.19;
import "./zombiefeeding.sol";
contract ZombieHelper is ZombieFeeding {
modifier aboveLevel(uint _level, uint _zombieId) {
require(zombies[_zombieId].level >= _level);
_;
}
function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].name = _newName;
}
function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].dna = _newDna;
}
function getZombiesByOwner(address _owner) external view returns(uint[]) {
uint[] memory result = new uint[](ownerZombieCount[_owner]);
for(uint i = 0; i < zombies.length; i++) {
if(zombieToOwner[i] == _owner) {
result[counter] = i;
counter++;
}
}
return result;
}
}
pragma solidity ^0.4.19;
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
);
}
contract ZombieFeeding is ZombieFactory {
KittyInterface kittyContract;
function setKittyContractAddress(address _address) external onlyOwner {
kittyContract = KittyInterface(_address);
}
function _triggerCooldown(Zombie storage _zombie) internal {
_zombie.readyTime = uint32(now + cooldownTime);
}
function _isReady(Zombie storage _zombie) internal view returns (bool) {
return (_zombie.readyTime <= now);
}
function feedAndMultiply(uint _zombieId, uint _targetDna, string _species) internal {
require(msg.sender == zombieToOwner[_zombieId]);
Zombie storage myZombie = zombies[_zombieId];
require(_isReady(myZombie));
_targetDna = _targetDna % dnaModulus;
uint newDna = (myZombie.dna + _targetDna) / 2;
if (keccak256(_species) == keccak256("kitty")) {
newDna = newDna - newDna % 100 + 99;
}
_createZombie("NoName", newDna);
_triggerCooldown(myZombie);
}
function feedOnKitty(uint _zombieId, uint _kittyId) public {
uint kittyDna;
(,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
feedAndMultiply(_zombieId, kittyDna, "kitty");
}
}
pragma solidity ^0.4.19;
import "./ownable.sol";
contract ZombieFactory is Ownable {
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
/*
<chapter 5>
solidity에서는 seconds, minutes, hours, days, weeks, years와 같은 시간단위를 사용하고 이것은 모두 초 단위로 표현될 수 있다
*/
uint cooldownTime = 1 days;
/*
<chapter 4>
*/
struct Zombie {
string name;
uint dna;
uint32 level;
uint32 readyTime;
}
Zombie[] public zombies;
mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;
function _createZombie(string _name, uint _dna) internal {
uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime))) - 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);
randDna = randDna - randDna % 100;
_createZombie(_name, randDna);
}
}
/**
* @title Ownable
* @dev The Ownable contract has an owner address, and provides basic authorization control
* functions, this simplifies the implementation of "user permissions".
*/
contract Ownable {
address public owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev The Ownable constructor sets the original `owner` of the contract to the sender
* account.
*/
function Ownable() public {
owner = msg.sender;
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
/**
* @dev Allows the current owner to transfer control of the contract to a newOwner.
* @param newOwner The address to transfer ownership to.
*/
function transferOwnership(address newOwner) public onlyOwner {
require(newOwner != address(0));
OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
}