[Solidity] 크립토 좀비 레슨 3 학습 리뷰

드림보이즈·2023년 2월 20일
0

크립토좀비

목록 보기
3/6

이 포스팅의 목표

  • 솔리디티에 익숙해지고 실력을 늘린다.
  • 내가 이더리움 DAPP 개발자라고 생각하고 능동적으로 사고하는 법을 배운다.

레슨 3 목표

  • onlyOwner로 핵심 함수 보호

  • 가스 사용 최적화 배우기

  • 레벨, 대기시간 개념 적용

  • 사용자 좀비 군대 반환 함수

  • 특정 레벨이 되면 이름, dna 재설적 함수 만들기

  • 외부 컨트랙트를 사용시 하드코딩 대신


챕터 1 : 컨트랙트 불변성

Immutable : 일단 이더리움에 컨트랙트를 배포하면, 수정, 업데이트 불가

더 큰 책임감과 실력이 따른다

외부 의존성

크립토 키티 컨트랙트 주소를 하드코딩 했는데, 만약 저기 망하면 우리도 망하는거야

하드코딩보다 언젠가 주소를 바꿀 수 있도록 하면 좋겠지

Q. 하드코딩말고 주소를 받는 함수를 만들자

KittyInterface kittyContract;

function setKittyContractAddress(address _address) external {
    kittyContract = KittyInterface(_address);
  }


챕터 2 : 소유 가능한 컨트랙트

위의 코드는 external이라 누구든 함수를 호출할 수 있고, 주소를 멋대로 바꾸면 우리 앱 망함

컨트랙트를 소유하게 만들어 소유자만 컨트롤 가능하게 하자

OpenZeppelin의 Ownable 컨트랙트

안전하다고 인증받은 라이브버리

/**
 * @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 : 다른 함수들에 대한 접근을 제어하기 위해 사용되는 유사 함수
(보통 함수 실행 전 요구 사항 충족 여부 체크)


챕터 3 : onlyOwner 함수 제어자

A is B
B is Ownable

이라면, A도 Ownable 함수들 사용 가능하다

함수 제어자

함수처럼 보이지만, modifier를 사용하고,
직접 얘만 호출하는 것은 불가능

함수 정의부 끝에 이름을 넣어줘

Q. setKittyContractAddress를 우리만 사용할 수 있도록 함수 제어자를 추가하라

function setKittyContractAddress(address _address) external onlyOwner{
    kittyContract = KittyInterface(_address);
  }

챕터 4 : 가스

가스 : 이더리움 dapp이 사용하는 연료

사용자

들이 DAPP 함수 실행마다 수수료를 내는 것임

저장공간 뿐 아니라 연산 사용에도 지불

가스 아끼기

uint, uint8, uint32 아무 도움 안됨, 어차피 uint256으로 256비트 저장공간 만들어 놓음

그러나 struct안에서는 다름, 더 작은 크기의 uint를 쓰고,동일한 데이터 타입은 끼리끼리 모아놓으면 좋음

Q. Zombie 구조체에 level과, 먹이 먹는 걸 제한할 readyTime 추가하라

    struct Zombie {
        string name;
        uint dna;
        uint32 level;
        uint32 readyTime;
    }

챕터 5 : 시간 단위

now : 현재 유닉스 타임스탬프(1970.1.1부터 지금까지 초 단위 합)
(32비트)

seconds, minutes, hours, days, weeks, years 지원
(다 초로 바뀜)

now + 5 minutes;
// 이런식으로 사용가능!

Q. 먹이 먹는 쿨타임을 설정하고, createZombie시 level과 readyTime도 고려하게 하라

    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 : 하루 뒤 지금부터 먹을 수 있다


챕터 6 : 좀비 재사용 대기 시간

대기 시간을 둬서 먹이 멸종을 막자

구조체를 인수로 전달하기

function _do(Zombie storage _zombie) internal {}

이렇게 좀비 id대신 참조를 전달 가능

Q. _triggerCooldown, _isReady 함수를 만들라

function _triggerCooldown(Zombie storage _zombie) internal {
	_zombie.readyTime = uint32(now + cooldownTime);
    
function _isReady(Zombie storage _zombie) internal view returns (bool) {
	return (_zombie.readyTime <= now)

챕터 7 : Public & 보안

보안을 점검하는 가장 좋은 방법은 public, external 함수를 검사하고 남용 될 가능성을 생각해보기!

feedAndMultiply(먹이 먹으면 새로운 좀비 만드는 함수)는 feedOnKitty()에 의해서만 호출되면 되므로(지금은 종류가 고양이 밖에 없음, 인간, 개 이런게 없으니) internal이 더 좋겠따

Q. 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);
  }

챕터 8 : 함수 제어자 또 다른 특징

코드가 늘어나니, 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) {
  // 필요한 함수 내용들
}

Q. aboveLevel이란 제어자를 만들자

modifier aboveLevel(uint _level, uint _zombieId) {
	require(zombies[_zombieId].level >= level);
    _;
    }
}    

챕터 9 : 좀비 제어자

Q.위의 aboveLevel 이용해

  • 렙 2 넘으면 이름을 바꿀 수 있게
  • 렙 20 넘으면 dna를 바꿀 수 있게
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;
    }
}    

챕터 10 : View 함수로 가스 절약

사용자 전체 좀비 군대를 볼 수 있는 메소드 getZombiesByOwner
를 만들어보자

View 함수는 가스를 소모하지 않는다

'사용자에 의해 외부에서 호출되었을 때'

동일 컨트랙트의 view 함수 아닌 함수에서 내부적으로 호출될 경우에는 가스 소모

Q. getZombiesByOwner 틀 만들자

function getZombiesByOwner(address _owner) external view returns (uint[]) {}

챕터 11 : storage는 비싸다

영원히 블록체인에 기록되니 비싸지

진짜 필요한 경우 아니면 쓰지마

대부분 언어는 큰 데이터 집합의 개별 데이터에 접근하는 것은 비싸지만

솔리디티는 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;
}

메모리 배열은 반드시 '길이' 인수와 함께 생성되어야, 동적 X

Q. getZombiesByOwner 완성해

function getZombiesByOwner(address _owner) external view returns(uint[]) {

	uint[] memory result = new uint[](ownerZombieCount[_owner]);
    return result;

챕터 12 : for 반복문

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;
       }
}       
profile
10년 후 세계 최고 블록체인 개발자

0개의 댓글