Basic - 좀비 전투 시스템

EllaDev·2022년 4월 1일
0

CrytoZombie

목록 보기
4/6

해당 글은 CrytoZombie를 공부하면서 정리한 내용입니다.

CrytoZombie

Ch1. Payable

제어자

  • 접근 제어자(visibility modifier) : 함수가 언제, 어디서 호출될 수 있는지 제어하는 제어자
    • private : 컨트랙트 내부의 다른 함수들에서만 호출될 수 있음
    • internal: private과 비슷하지만, 해당 컨트랙트를 상속하는 컨트랙트에서도 호출될 수 있다.
    • external: 오직 컨트랙트 외부에서만 호출될 수 있다.
    • public: 내외부 모두에서, 어디서든 호출될 수 있다.
  • 상태 제어자(state modifier) : 블록체인과 상호작용 하는 방법에 대해 알려준다
    • view: 해당 함수를 실행해도 어떤 데이터도 저장/변경되지 않음을 알려준다.
    • pure: 해당 함수가 어떤 데이터도 블록체인에 저장하지 않을 뿐만 아니라, 블록체인으로부터 어떤 데이터도 읽지 않음을 알려준다.
      이들 모두는 컨트랙트 외부에서 불렸을 때 가스를 전혀 소모하지 않는다.하지만 다른 함수에 의해 내부적으로 호출됐을 경우에는 가스를 소모한다.
  • 사용자 정의 제어자: onlyOwner와 aboveLevel 같은 것이다. 이런 제어자를 사용해서 우린 함수에 이 제어자들이 어떻게 영향을 줄지를 결정하는 우리만의 논리를 구성할 수 있다.

payable 제어자

일반적인 웹 서버에서 API 함수를 실행할 때에는, 함수 호출을 통해서 US 달러를 보낼 수 없다.(비트코인도 보낼 수 없다.) 하지만 이더리움에서는, 돈(이더), 데이터(transaction payload), 그리고 컨트랙트 코드 자체 모두 이더리움 위에 존재하기 때문에, 자네가 함수를 실행하는 동시에 컨트랙트에 돈을 지불하는 것이 가능하다.

contract OnlineStore {
  function buySomething() external payable {
    // 함수 실행에 0.001이더가 보내졌는지 확실히 하기 위해 확인:
    require(msg.value == 0.001 ether);
    // 보내졌다면, 함수를 호출한 자에게 디지털 아이템을 전달하기 위한 내용 구성:
    transferThing(msg.sender);
  }
}

// web3.js
// `OnlineStore`는 자네의 이더리움 상의 컨트랙트를 가리킨다고 가정하네:
OnlineStore.buySomething({from: web3.eth.defaultAccount, value: web3.utils.toWei(0.001)})
  • msg.value
    • 컨트랙트로 이더가 얼마나 보내졌는지 확인하는 방법이고, ether는 기본적으로 포함된 단위이다.
    • 자바스크립트 함수 호출에서 이 필드를 통해 ether를 얼마나 보낼지 결정한다.
    • 트랜잭션을 봉투로 생각하고, 함수 호출에 전달하는 매개 변수를 자네가 써넣은 편지의 내용이라 생각한다면, value는 봉투 안에 현금을 넣는 것과 같다
  • 참고: 만약 함수가 payable로 표시되지 않았는데 자네가 위에서 본 것처럼 이더를 보내려 한다면, 함수에서 자네의 트랜잭션을 거부할 것이네.

Ch2. 출금

컨트랙트에 이더를 보냈으면 출금을 해야한다.

contract GetPaid is Ownable {
  function withdraw() external onlyOwner {
    owner.transfer(this.balance);
  }
  
  // 되돌려주는 함수도 만들 수 있다. 
  uint itemFee = 0.001 ether;
  msg.sender.transfer(msg.value - itemFee);
}
  • transfer함수를 사용해서 이더를 특정 주소로 전달한다.
  • this.balance는 컨트랙트에 저장돼있는 전체 잔액을 반환한다.

Ch4. 난수(Random Numbers)

keccak256을 통한 난수 생성

// Generate a random number between 1 and 100:
uint randNonce = 0;
uint random = uint(keccak256(now, msg.sender, randNonce)) % 100;
randNonce++;
uint random2 = uint(keccak256(now, msg.sender, randNonce)) % 100;
  • now의 타임스탬프 값, msg.sender, 증가하는 nonce(딱 한 번만 사용되는 숫자, 즉 똑같은 입력으로 두 번 이상 동일한 해시 함수를 실행할 수 없게 함)를 받고 있다.
  • keccak을 사용하여 이 입력들을 임의의 해시 값으로 변환하고, 변환한 해시 값을 uint로 바꾼 후, % 100을 써서 마지막 2자리 숫자만 받도록 했다.
  • 이를 통해 0과 99 사이의 완전한 난수를 얻을 수 있다.

정직하지 않은 노드의 공격에 취약한 이유

이더리움에서는 자네가 컨트랙트의 함수를 실행하면 트랜잭션(transaction)으로서 네트워크의 노드 하나 혹은 여러 노드에 실행을 알리게 된다. 그 후 네트워크의 노드들은 여러 개의 트랜잭션을 모으고, "작업 증명"으로 알려진 계산이 매우 복잡한 수학적 문제를 먼저 풀기 위한 시도를 하게 된다. 그리고서 해당 트랜잭션 그룹을 그들의 작업 증명(PoW)과 함께 블록으로 네트워크에 배포하게 된다.

한 노드가 어떤 PoW를 풀면, 다른 노드들은 그 PoW를 풀려는 시도를 멈추고 해당 노드가 보낸 트랜잭션 목록이 유효한 것인지 검증한다. 유효하다면 해당 블록을 받아들이고 다음 블록을 풀기 시작한다.

이것이 우리의 난수 함수를 취약하게 만든다.

예를 들어, 동전 던지기 컨트랙트를 사용할때 앞면이 나오면 돈이 두 배가 되고, 뒷면이 나오면 모두 다 잃는 것이다. 앞뒷면을 결정할 때 위에서 본 난수 함수를 사용한다고 가정한다. (random >= 50은 앞면, random < 50은 뒷면).

만약 노드를 실행하고 있다면, 오직 나의 노드에만 트랜잭션을 알리고 이것을 공유하지 않을 수 있다. 그 후 내가 이기는지 확인하기 위해 동전 던지기 함수를 실행할 수 있다. 그리고 만약 내가 진다면, 내가 풀고 있는 다음 블록에 해당 트랜잭션을 포함하지 않는 것을 선택한다. 이것을 내가 결국 동전 던지기에서 이기고 다음 블록을 풀 때까지 무한대로 반복할 수 있고, 이득을 볼 수 있다.


Ch5. 개요

  • 좀비 중 하나를 고르고, 상대방의 좀비를 공격 대상으로 선택한다.
  • 공격하는 쪽의 좀비라면, 70%의 승리 확률을 가진다. 방어하는 쪽의 좀비는 30%의 승리 확률을 가질 것이다.
  • 모든 좀비들(공격, 방어 모두)은 전투 결과에 따라 증가하는 winCount와 lossCount를 가진다.
  • 공격하는 쪽의 좀비가 이기면, 좀비의 레벨이 오르고 새로운 좀비가 생긴다.
  • 좀비가 지면, 아무것도 일어나지 않는다.(좀비의 lossCount가 증가하는 것 빼고).
  • 좀비가 이기든 지든, 공격하는 쪽 좀비의 재사용 대기시간이 활성화될 것이다.


= zombieattack.sol

import "./zombiehelper.sol";
contract ZombieBattle is ZombieHelper {
  uint randNonce = 0;
  uint attackVictoryProbability = 70;

  function randMod(uint _modulus) internal returns(uint) {
    randNonce++;
    return uint(keccak256(now, msg.sender, randNonce)) % _modulus;
  }

  function attack(uint _zombieId, uint _targetId) external ownerOf(_zombieId) {
    Zombie storage myZombie = zombies[_zombieId];
    Zombie storage enemyZombie = zombies[_targetId];
    uint rand = randMod(100);
    if (rand <= attackVictoryProbability) {
      myZombie.winCount++;
      myZombie.level++;
      enemyZombie.lossCount++;
      feedAndMultiply(_zombieId, enemyZombie.dna, "zombie");
    } else {
      myZombie.lossCount++;
      enemyZombie.winCount++;
    }
    _triggerCooldown(myZombie);
  }
}

= zombiehelper.sol

pragma solidity ^0.4.19;

import "./zombiefeeding.sol";

contract ZombieHelper is ZombieFeeding {

  uint levelUpFee = 0.001 ether;

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

  function withdraw() external onlyOwner {
    owner.transfer(this.balance);
  }

  function setLevelUpFee(uint _fee) external onlyOwner {
    levelUpFee = _fee;
  }

  function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) ownerOf(_zombieId) {
    zombies[_zombieId].name = _newName;
  }

  function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) ownerOf(_zombieId) {
    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.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;

  modifier ownerOf(uint _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    _;
  }

  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 ownerOf(_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;
    uint cooldownTime = 1 days;

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

    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), 0, 0)) - 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
Frontend Developer

0개의 댓글