Solidity

CryptoZombies 3 (고급 솔리디티 개념) 정리

dlwltn98 2023. 2. 8. 14:10

컨트랙트의 불변성

 - 이더리움 컨트랙트를 배포하고 나면 변하지 않음 (Immutable)

 - 컨트랙트로 배포한 최초의 코드는 블록체인에 영구적으로 존재

 - 수정하거나 업데이트 할 수 없음

 

외부 의존성

 - 대부분 DApp의 중요한 일부를 수정할 수 있도록 하는 함수를 만들어 두는 것이 합리적

 

소유 가능한 컨트랙트

컨트랙트를 소유 가능하게 함 → 컨트랙트를 대상으로 특별한 권리를 가지는 소유자가 있음 

 

Ownable Contract

 - OpenZeppelin 솔리디티 라이브러리에 있음

 

생성자( Constructor )

 - 컨트랙트와 동일한 이름을 가진 생략 가능한 함수

 - 컨트랙트가 실행될 때 딱 한번만 실행됨

/**
  * @dev The Ownable constructor sets the original `owner` of the contract to the sender
  * account.
  */
  function Ownable() public { 
    owner = msg.sender;
  }

 

onlyOwner 함수 제어자

 - 특정 함수에 소유자만 접근 가능하도록 해줌

 - 컨트랙트에서 흔히 쓰는 것 중 하나

 - 대부분의 솔리디티 DApp들은 Ownable 컨트랙트를 복사/붙여넣기 하면서 시작한다고 함

 - 첫 컨트랙트는 이 컨트랙트를 상속해서 만듦

/**
 * @dev Throws if called by any account other than the owner.
 */
modifier onlyOwner() {
    require(msg.sender == owner);
    _;
  }
contract MyContract is Ownable {
  event LaughManiacally(string laughter);

  function likeABoss() external onlyOwner {
    LaughManiacally("Muahahahaha");
  }
}

 - likeABoss()를 호출하면 onlyOwner()가 먼저 실행

 - onlyOwner() 실행 중  _; 를 만나면 likeABoss()로 돌아가 코드 실행

 - onlyOwner의 경우에는, 함수에 이 제어자를 추가하면 오직 컨트랙트의 소유자 (내가 배포했다면 나)만이 해당 함수를 호출가능

 

 

기본 컨트랙트인 ZombieFactory Ownable을 상속하고 있으니, 우리는 onlyOwner 함수 제어자를 ZombieFeeding에서도 사용할 수 있음

ZombieFeeding is ZombieFactory
ZombieFactory is Ownable

 

 

함수 제어자 (Function Modifier)

 - 다른 함수들에 대한 접근을 제어하기 위해 사용되는 유사 함수

 - 보통 함수 실행 전 요구사항 충족 여부를 확인하는데 사용

 - 함수 정의부 끝에 해당 함수의 작동 방식을 바꾸도록 제어자의 이름을 붙일 수 있음

 

gas

 - 이더리움 DApp이 사용하는 연료

 - 이더리움에서는 사용자들이 만들어진 DApp을 사용할 때마다 가스라고 불리는 화폐를 지불해야함

 

함수 로직에 따라 필요한 가스가 달라짐

⇒ 각 연산은 소모되는 가스 비용(gas cost)이 있고 그 연산을 수행하는데 소모되는 컴퓨팅 자원의 양이 비용을 결정함

⇒ 함수를 실행하는 것은 실제 돈을 쓰게 하는 것이라 코드 최적화 아주 중요

 

가스가 필요한 이유

이더리움을 만든 사람들은 누군가가 무한 반복문을 써 네트워크를 방해하거나 자원소모가 큰 연산을 써 네트워크 자원을 모두 사용하지 못하도록 만들길 원함

⇒ 그래서 연산처리에 비용이 들도록 만듦

⇒ 사용자들은 저장공간 뿐만 아니라 연산 사용시간에 따라서도 비용을 지불함

 

가스를 아끼기 위한 구조체 압축

 uint8 등의 하위 타입을 쓰는건 득이 없음

⇒ 솔리디티에선 uint의 크기에 상관없이 256비트의 저장공간을 미리 잡아기때문

⇒ uint8을 쓰는 건 가스 소모를 줄이는데 아무 영향 없음

 

그러나 Struct는 예외

구조체 안에서 여러개의 uint를 만든다면 가능한 작은 크기의 uint를 쓰는게 좋음

솔리디티에서 그 변수들을 더 적은 공간을 차지하도록 압축함

struct NormalStruct {
  uint a;
  uint b;
  uint c;
}

struct MiniMe {
  uint32 a;
  uint32 b;
  uint c;
}

// `mini`는 구조체 압축을 했기 때문에 `normal`보다 가스를 조금 사용할 것이네.
NormalStruct normal = NormalStruct(10, 20, 30);
MiniMe mini = MiniMe(10, 20, 30);

 - 구조체 안에서는 가능한 작은 크기의 정수 타입을 쓰는게 좋음

 - 동일한 데이터 타입은 하나로 묶어두는게 좋음 → 솔리디티에서 사용하는 저장공간 최소화

uint c; uint32 a; uint32 b; // 아래의 필드보다 가스 덜 씀, uint32 필드가 묶여있어서
uint32 a; uint c; uint32 b;

 

시간 단위 (Time units)

 - 시간을 다룰 수 있는 단위계를 기본적으로 제공

 - now 변수를 쓰면 현재의 유닉스 타임 스탬프 값을 얻을 수 있음

   + 유닉스 타임 스탬프 : 1970년 1월 1일부터 지금까지의 초 단위 합

 - seconds, minutes, hours, days, weeks, years 같은 시간 단위를 포함하고 있음

    + 이들은 그에 해당하는 길이 만큼의 초 단위 uint 숫자로 변환됨

    + 1분 = 60 / 1시간 = 3600(60*60) / 1일 = 86400(24*60*60)

uint lastUpdated;

// `lastUpdated`를 `now`로 설정
function updateTimestamp() public {
  lastUpdated = now;
}

// 마지막으로 `updateTimestamp`가 호출된 뒤 5분이 지났으면 `true`를, 5분이 아직 지나지 않았으면 `false`를 반환
function fiveMinutesHavePassed() public view returns (bool) {
  return (now >= (lastUpdated + 5 minutes));
}

 

구조체로 인수 전달

 - private 또는 internal 함수에 인수로서 구조체의 storage 포인터를 전달할 수 있음

function _doStuff(Zombie storage _zombie) internal {
  // _zombie로 할 수 있는 것들을 처리
}

 

인수를 가지는 함수 제어자

 - 함수 제어자는 인수를 받을 수 있음

// 사용자의 나이를 저장하기 위한 매핑
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) {
  // 필요한 함수 내용들
}

 

View 함수를 사용해 가스 절약하기

View 함수는 블록체인 상에서 어떠한 것도 수정하지 않고 데이터를 읽기만 했을때 가스를 소모하지 않음

⇒ 외부에서 호출 되었을땐 무료

 

그러나 동일한 컨트랙트 내에 있는 View 함수가 아닌 다른 함수에서 내부적으로 호출할 경우 가스 소모함

⇒ 다른 함수가 이더리움에 트랜잭션을 생성하고 이는 모든 개별 노드에서 검증되어야 하기때문

 

비싼 Storage

 - 데이터의 일부를 쓰거나 바꿀때마다 블록체인에 영구적으로 기록되어 비쌈

   지구상의 수천개의 노드들이 그들의 하드 드라이브에 그 데이터를 저장해야 함

   블록체인이 커져가면ㄴ서 이 데이터의 양도 같이 커져 감

 - 비용을 최소화 하기 위해 진짜 필요한 경우가 아니면 storage에 데이터를 쓰지 않는게 좋음

  ⇒  이를 위해 때때로 비효율적으로 보이는 프로그래밍을 구성하기도 함

 

대부분의 프로그래밍 언어에서 큰 데이터 집합의 개별 데이터에 모두 접근하는 것은 비용이 비쌈 

그러나 솔리디티는 그 접근이 external view 함수라면 storage를 사용하는 것보다 저렴함

 

메모리에 배열 선언

 - 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;
}

 

for 반복문

 - JS 문법과 비슷함

// 짝수로 구성된 배열을 만드는 예시

function getEvens() pure external returns(uint[]) {
  uint[] memory evens = new uint[](5);
  // 새로운 배열의 인덱스를 추적하는 변수
  uint counter = 0;
  // for 반복문에서 1부터 10까지 반복함
  for (uint i = 1; i <= 10; i++) {
    // `i`가 짝수라면...
    if (i % 2 == 0) {
      // 배열에 i를 추가함
      evens[counter] = i;
      // `evens`의 다음 빈 인덱스 값으로 counter를 증가시킴
      counter++;
    }
  }
  return evens;
}

 

 

전체 코드

 - zombiefactory.sol

pragma solidity ^0.4.19;

import "./ownable.sol";

// Ownable 상속
contract ZombieFactory is Ownable {

    // event 선언
    event NewZombie(uint zombieId , string name, uint dna);

    // dnaModulus라는 uint형 변수를 생성하고 10의 dnaDigits승을 배정
    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    // 재사용 대기시간 : 1일이 지나야 다시 사용 가능
    uint cooldownTime = 1 days;

    // Zombie 라는 구조체 생성
    struct Zombie {
        string name;
        uint dna;
        uint32 level;
        uint32 readyTime;  // 재사용 대기시간
    }

    // Zombie 구조체의 public 배열 생성
    Zombie[] public zombies;

    // 매핑 생성
    // 좀비 id로 좀비를 저장하고 검색
    mapping (uint => address) public zombieToOwner;
    mapping (address => uint) ownerZombieCount;

    // 좀비 생성 private 함수 선언
    // 함수 접근 제어자를 private에서 internal로 변경
    function _createZombie(string _name, uint _dna) internal {

        // 새로운 좀비를 zombies 배열에 추가
        // 배열의 첫 원소가 0이라는 인덱스를 갖기 때문에, array.push() - 1은 막 추가된 좀비의 인덱스가 됨
        uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime))) - 1;

        zombieToOwner[id] = msg.sender;
        ownerZombieCount[msg.sender]++;

        // 새로운 좀비가 배열에 추가되면 이벤트 실행
        NewZombie(id, _name, _dna);
    }

    // uint를 반환하는 view 함수 선언
    function _generateRandomDna(string _str) private view returns (uint) {

        uint rand = uint(keccak256(_str));

        // 좀비 DNA가 16자리 숫자이기만을 원하므로 연산 후 값 반환
        return rand % dnaModulus;
    }

    // public 함수 선언
    function createRandomZombie(string _name) public {

        require(ownerZombieCount[msg.sender] == 0);
        // 좀비 dna 생성
        uint randDna = _generateRandomDna(_name);
        // 새로운 좀비 배열에 추가 
        _createZombie(_name, randDna);
    }

}

 - zombiefeeding.sol

pragma solidity ^0.4.19;

// zombiefactory.sol 불러오기
import "./zombiefactory.sol";

// KittyInterface라는 인터페이스를 정의
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
    );
}

// ZombieFactory 상속
contract ZombieFeeding is ZombieFactory {

    // 크립토키티 컨트랙트 주소의 업데이트가 가능하도록 수정
    KittyInterface kittyContract;
    // 소유자만 수정할 수 있도록 수정
    function setKittyContractAddress(address _address) external onlyOwner {
        kittyContract = KittyInterface(_address);
    }

    // 1일을 초단위로 바꾼것의 합과 같음
    function _triggerCooldown(Zombie storage _zombie) internal {
        _zombie.readyTime = uint32(now + cooldownTime);
    }

    // 좀비가 먹이를 먹은 후 충분한 시간이 지났는지 판단해서 알려줌
    function _isReady(Zombie storage _zombie) internal view returns(bool) {
        return (_zombie.readyTime <= now);
    }

    // 좀비 dna가 생명체의 dna와 혼합되어 새로운 좀비 생성
    function feedAndMultiply(uint _zombieId, uint _targetDna, string _species) internal {
        
        // 주인만 좀비에게 먹이를 줄 수 있음
        require(msg.sender == zombieToOwner[_zombieId]);

        // 먹이를 먹는 좀비의 dna 얻음
        Zombie storage myZombie = zombies[_zombieId];

       // 좀비가 먹이 먹은 시간을 고려하도록 수정
       require(_isReady(myZombie));

        _targetDna = _targetDna % dnaModulus; // 16자리보다 크면 안됨
        uint newDna = (myZombie + _targetDna) / 2;  // 새로운 dna 

        // 키티 유전자를 가진 좀비는 dna 마지막 2자리를 99로 변경
        if(keccak256(_species) == keccak256("kitty")) {
            newDna = newDna - newDna % 100 + 99;
        }

       // 좀비 생성 함수 호출
       // 인자로 좀비의 이름과 dna 값을 받음
        _createZombie("NoName", newDna);

        // 좀비의 재사용 대기시간 만듦
        _triggerCooldown(myZombie);
    }

    // 크립토키티 컨트랙트와 상호작용
    // 크립토키티 컨트랙트에서 고양이 유전자를 얻어내는 함수를 생성
    function feedOnKitty (uint _zombieId, uint _kittyId) public {

        uint kittyDna;
        (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);

        // dna가 혼합된 새로운 좀비를 생성하는 함수
        feedAndMultiply(_zombieId, kittyDna, "kitty");
    }

}

 - zombiehelper.sol

pragma solidity ^0.4.19;

import "./zombiefeeding.sol";

contract ZombieHelper is ZombieFeeding  {

    modifier aboveLevel(uint _level, uint _zombieId) {

        // 좀비의 레벨이 입력 받은 레벨보다 높은지 확인
        require(zombies[_zombieId].level >= _level);
        _;
    }

    // 좀비의 레벨이 2 이상이면 이름 수정 가능
    function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) {
        require(zombieToOwner[_zombieId] == msg.sender);
        zombies[_zombieId].name = _newName;
    }

    // 좀비의 레벨이 20이상이면 좀비에게 임의의 dna 줄 수 있음
    function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
        require(zombieToOwner[_zombieId] == msg.sender);
        zombies[_zombieId].dna = _newDna;
    }

    // 사용자의 전체 좀비 군대를 반환
    function getZombiesByOwner(address _owner) external view returns(uint[]) {
        uint[] memory result = new uint[](ownerZombieCount[_owner]);

        uint counter = 0;
        for (uint i = 0; i < zombies.lengthl; i++) {
            if(zombieToOwner[i] == _owner) {
                result[counter] = i;
                counter++;
            }
        }
        return result;
    }

}

 

 

github : https://github.com/dlwltn98/studyCryptoZombies

cryptoZombies: https://cryptozombies.io/ko/course