Crypto Zombie - 3

Hong·2022년 12월 18일
0

🧟‍♂️

🏭 zombiefactory.sol
 - 🍞 zombiefeeding.sol
    - 👼 zombieHelper.sol



Chapter 1 : 스마트 컨트랙트의 불변성을 외부의존성을 통해 해결

🍞 zombiefeeding.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로 수정할 수 있도록 해준다




Chapter 2 : 다른 sol파일 import하고 is로 상속하기

👼 zombieHelper.sol

import "./zombiefeeding.sol";

contract ZombieHelper is ZombieFeeding {
 ... 
}

import를 통해 zombiefeeding.sol 파일을 들고오고 ZombieHelper에 상속했다




Chapter 3 : onlyOwner modifier를 적용

🍞 zombiefeeding.sol

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

chapter 1에서 크립토 키티의 주소를 유동저긍로 변경하기 위해 함수를 external로 설정했지만 게임참여자들 모두가 크립토 키티의 주소를 원하는 대로 바꿔서 zombie에게 먹이로 준다면 게임이 지루해져 버릴 것이다
때문에 openZepplin 라이브러이의 ownable contract의 onlyOwner modifier를 통해 컨트렉트 소유자만 해당 함수를 실행할 수 있도록 해준다




Chapter 4 : 가스비용 줄이는 법(구조체 안에서)

🏭 zombiefactory.sol

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

uint8 uint16 uint32... 처럼 정수크기type을 변경시키는 행위는 이더리움 gas fee 절약에 아무런 영향을 주지 않는다

하지만 struct(구조체) 안에서 여러 개의 uint를 만든다면, 가능한 더 작은 크기의 uint를 쓰도록 하는것이 좋다.

솔리디티에서 그 변수들을 더 적은 공간을 차지하도록 압축할 것이다.
또한 동일한 데이터 타입은 하나로 묶어놓는 것이 좋다.
uint32가 두개 사용되어야 한다면 위의 코드처럼 연속되는 줄에 uint32의 변수들을 적어주는 것이 좋다.




Chapter 5 : 시간단위(now, seconds, minutes, hours, weeks, years)

🏭 zombiefactory.sol

 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와 같은 시간단위를 사용하고 이것은 모두 초 단위로 표현될 수 있다




Chapter 6 : 구조체를 파라미터로 전달하기(storage vs memory)

🍞 zombiefeeding.sol

    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 구조체함수의 파라미터로 전달이 가능하다




Chapter 7 : 함수보안. 체크방법 = 모든 public, external 함수를 검사한다

🍞 zombiefeeding.sol

  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에게 전달할 수 있을 것이다(이렇게 되면 게임이 재미없겠다)




Chapter 8 : 파라미터를 받을 수 있는 함수제어자 modifier

contract ZombieHelper is ZombieFeeding {

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

함수제어자 modifier에도 파라미터를 부여할 수 있다




Chapter 9 : 8의 modifier를 함수에 적용하기

👼 zombieHelper.sol

  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)를 사용할 수 있게된다




Chapter 10 : view함수는 gas를 소모하지 않는다 -> 가스 절약 = external view

Chapter 11 : storage함수는 gas를 소모하고 비싸다 -> 가스 절약 = memory

Chapter 12 : 반복문 for -> view함수에서 for문을 사용해 가스비를 절약함

👼 zombieHelper.sol

  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하는 방법).




🧟‍♂️ 전체코드

👼 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);
    _;
  }

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

}


🍞 zombiefeeding.sol

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

}


🏭 zombiefactory.sol

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

}


🚨 ownable.sol

/**
 * @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;
  }

}



profile
Notorious

0개의 댓글