onlyOwner로 핵심 함수 보호
가스 사용 최적화 배우기
레벨, 대기시간 개념 적용
사용자 좀비 군대 반환 함수
특정 레벨이 되면 이름, dna 재설적 함수 만들기
외부 컨트랙트를 사용시 하드코딩 대신
Immutable : 일단 이더리움에 컨트랙트를 배포하면, 수정, 업데이트 불가
더 큰 책임감과 실력이 따른다
크립토 키티 컨트랙트 주소를 하드코딩 했는데, 만약 저기 망하면 우리도 망하는거야
하드코딩보다 언젠가 주소를 바꿀 수 있도록 하면 좋겠지
KittyInterface kittyContract;
function setKittyContractAddress(address _address) external {
kittyContract = KittyInterface(_address);
}
위의 코드는 external이라 누구든 함수를 호출할 수 있고, 주소를 멋대로 바꾸면 우리 앱 망함
컨트랙트를 소유하게 만들어 소유자만 컨트롤 가능하게 하자
안전하다고 인증받은 라이브버리
/**
* @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;
}
}
Constructor(생성자 ) : 컨트랙트 이름과 함수 명이 똑같은, 컨트랙트가 생성될 때 딱 한 번 실행되는 함수
modifier : 다른 함수들에 대한 접근을 제어하기 위해 사용되는 유사 함수
(보통 함수 실행 전 요구 사항 충족 여부 체크)
A is B
B is Ownable
이라면, A도 Ownable 함수들 사용 가능하다
함수처럼 보이지만, modifier를 사용하고,
직접 얘만 호출하는 것은 불가능
함수 정의부 끝에 이름을 넣어줘
function setKittyContractAddress(address _address) external onlyOwner{
kittyContract = KittyInterface(_address);
}
가스 : 이더리움 dapp이 사용하는 연료
들이 DAPP 함수 실행마다 수수료를 내는 것임
저장공간 뿐 아니라 연산 사용에도 지불
uint, uint8, uint32 아무 도움 안됨, 어차피 uint256으로 256비트 저장공간 만들어 놓음
Q. Zombie 구조체에 level과, 먹이 먹는 걸 제한할 readyTime 추가하라
struct Zombie {
string name;
uint dna;
uint32 level;
uint32 readyTime;
}
now : 현재 유닉스 타임스탬프(1970.1.1부터 지금까지 초 단위 합)
(32비트)
seconds, minutes, hours, days, weeks, years 지원
(다 초로 바뀜)
now + 5 minutes;
// 이런식으로 사용가능!
uint cooldownTime = 1 days;
function _createZombie(string _name, uint _dna) internal {
// 2. 아래 줄을 업데이트하게:
uint id = zombies.push(Zombie(_name, _dna,1,uint32(now + cooldownTime))) - 1;
zombieToOwner[id] = msg.sender;
ownerZombieCount[msg.sender]++;
NewZombie(id, _name, _dna);
}
now가 uint256을 반환하기에, uint32()를 사용해 명시적으로 바꿔줘야 한다.
now + cooldownTime : 하루 뒤 지금부터 먹을 수 있다
대기 시간을 둬서 먹이 멸종을 막자
function _do(Zombie storage _zombie) internal {}
이렇게 좀비 id대신 참조를 전달 가능
function _triggerCooldown(Zombie storage _zombie) internal {
_zombie.readyTime = uint32(now + cooldownTime);
function _isReady(Zombie storage _zombie) internal view returns (bool) {
return (_zombie.readyTime <= now)
보안을 점검하는 가장 좋은 방법은 public, external 함수를 검사하고 남용 될 가능성을 생각해보기!
feedAndMultiply(먹이 먹으면 새로운 좀비 만드는 함수)는 feedOnKitty()에 의해서만 호출되면 되므로(지금은 종류가 고양이 밖에 없음, 인간, 개 이런게 없으니) internal이 더 좋겠따
function feedAndMultiply(uint _zombieId, uint _targetDna, string _species) internal {
require(msg.sender == zombieToOwner[_zombieId]);
Zombie storage myZombie = zombies[_zombieId];
// 2. 여기에 `_isReady`를 확인하는 부분을 추가하게
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);
// 3. `_triggerCooldown`을 호출하게
_triggerCooldown(myZombie);
}
코드가 늘어나니, zombiehelper.sol을 따로 만들자
좀비가 특정 레벨이 넘으면 스킬을 배울 수 있게!
// 사용자의 나이를 저장하기 위한 매핑
mapping (uint => uint) public age;
// 사용자가 특정 나이 이상인지 확인하는 제어자
modifier olderThan(uint _age, uint _userId) {
require (age[_userId] >= _age);
_;
}
// 차를 운전하기 위햐서는 16살 이상이어야 하네(적어도 미국에서는).
// `olderThan` 제어자를 인수와 함께 호출하려면 이렇게 하면 되네:
function driveCar(uint _userId) public olderThan(16, _userId) {
// 필요한 함수 내용들
}
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;
}
}
사용자 전체 좀비 군대를 볼 수 있는 메소드 getZombiesByOwner
를 만들어보자
'사용자에 의해 외부에서 호출되었을 때'
function getZombiesByOwner(address _owner) external view returns (uint[]) {}
영원히 블록체인에 기록되니 비싸지
진짜 필요한 경우 아니면 쓰지마
대부분 언어는 큰 데이터 집합의 개별 데이터에 접근하는 것은 비싸지만
솔리디티는 external view라면 storage보다 그게 싸게 먹힌다
function getArray() external pure returns(uint[]) {
// 메모리에 길이 3의 새로운 배열을 생성한다.
uint[] memory values = new uint[](3);
// 여기에 특정한 값들을 넣는다.
values.push(1);
values.push(2);
values.push(3);
// 해당 배열을 반환한다.
return values;
}
function getZombiesByOwner(address _owner) external view returns(uint[]) {
uint[] memory result = new uint[](ownerZombieCount[_owner]);
return result;
getZombiesByOwner를 구현시 가장 기초적인 방법은
소유자의 좀비 군대에 대한 mapping을 만들어 저장하는 것
mapping (address => uint[]) public ownerToZombies
그리고 새로운 좀비를 만들 때 마다
ownerToZombies[owner].push(zombieId)
를 사용해 새 좀비를 추가하겠지
만약 내 좀비를 다른 사람에 줘야 한다면?
내 좀비를 ownerToZombies 배열에서 지우고,
한 칸 씩 다 땡기고 (여기서 가스 ㅈㄴ 쓰겠지)
차라리 for문을 돌려서 모든 좀비 배열을 돌려서 특정 사용자 좀비들로 구성된 배열을 만들어 뱉자
function getZombiesByOwner(address _owner) external view returns(uint[]) {
uint[] memory result = new uint[](ownerZombieCount[_owner]);
//
uint counter = 0;
for(uint i = 0 ; i < zombies.length ; i++) {
if(zombieToOwner[i] == _owner) {
result[counter] = i;
counter++;
}
}
return result;
}
}