Zombie Battle System

Payable

payable 함수는 이더를 받을 수 있는 함수유형이다.

이더리움에서는 돈(이더), 데이터(transaction payload), 컨트랙트 코드 자체가 모두 이더리움 위에 존재하므로 함수를 실행하는 동시에 컨트랙트에 돈을 지불하는 것이 가능하다.

다음 예시를 보자.

contract OnlineStore {
  function buySomething() external payable {

    // 함수 실행에 0.001이더가 보내졌는지 확실히 하기 위해 확인
    require(msg.value == 0.001 ether);

    // 보내졌다면, 함수를 호출한 자에게 디지털 아이템을 전달하기 위한 내용 구성:
    transferThing(msg.sender);
  }
}

msg.value 는 컨트랙트로 이더가 얼마나 보내졌는지 확인하는 방법이다.

이 함수는 web3.js(DApp의 자바스크립트 프론트엔드) 에서 다음과 같이 함수를 실행할 때 발생한다.

// `OnlineStore`는 이더리움 상의 컨트랙트라고 가정.
OnlineStore.buySomething({from: web3.eth.defaultAccount, value: web3.utils.toWei(0.001)})

value 필드는 위 자바스크립트 함수에서 얼마의 이더를 보낼 지 결정한다.


출금

컨트랙트에서 이더를 인출하는 함수는 다음과 같다.

contract GetPaid is Ownable {
  function withdraw() external onlyOwner {
    owner.transfer(this.balance);
  }
}

Ownable 컨트랙트를 임포트해서, owneronlyOwner를 사용하고 있다.

그리고 transfer 함수를 사용해서 이더를 특정 주소로 전달한다.
this.balance 는 컨트랙트에 저장된 잔액을 반환한다.

즉, 위 코드는 계정 소유자에게 잔액을 전달하고 있음을 알 수 있다.

만약 초과 지불을 한 경우에는 어떻게 할까?
다음과 같이 이더를 msg.sender로 되돌려 주는 방법을 사용할 수 있다.

uint itemFee = 0.001 ether;
msg.sender.transfer(msg.value - itemFee);

0.001 이더를 메세지를 전송한 사람에게 transfer 한다.


위 두가지 내용을 기억하고, 좀비 레벨이 상승할 때 이더를 지불하고, 이더를 인출 하는 코드를 작성해보자.

zombieHelper.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 levelUp(uint _zombieId) external payable {
    require(msg.value == levelUpFee);
    zombies[_zombieId].level++;
  }
  ...

난수 생성

이제 생성된 좀비들끼리 전투를 하도록 만들어 보겠다.

좀비가 전투를 하기위해서는 난수를 사용해야한다.

솔리디티에서 난수를 발생시키는 방법은 keccak256 해시 함수를 사용하는 것이다.

uint randNonce = 0;
uint random = uint(keccak256(now, msg.sender, randNonce)) % 100;
randNonce++;
uint random2 = uint(keccak256(now, msg.sender, randNonce)) % 100;

위 예시는 현재시간 (now), sender 와 Nonce 를 증가시키면서 해시 값을 반환하고, 그것을 % 100 연산으로 통해 마지막 두자리만 출력하게 될 것이다.

난수 취약점

이더리움에서는 컨트랙트 함수를 실행하면 트랜젝션으로서 네트워크의 노드에게 실행을 알린다.
그 후 이 노드들이 여러 트랜젝션으로 모아서 "작업 증명" 이라는 수학적 문제를 시도한다.
그리고 이 트랜젝션 그룹이 작업 증명(PoW) 와 함께 블록으로 네트워크에 배포된다.

한 노드가 PoW를 풀면 다른 노드들은 이 PoW를 푸려는 시도를 중지하고, 그 노드가 보낸 트랜젝션 목록이 유효한지 검증한다. 유효한 경우 해당 블록을 받아들이고 다음 블록을 풀기 시도한다.

이것이 난수를 취약하게 만든다.

예를들어 동전 던지기라는 컨트렉트는 앞면이 나오면 이기고, 뒷면이 나오면 지는 규칙이 있다고 가정해보자.

노드가 실행이 되었지만 트랜젝션을 다른 노드와 공유하지 않을 수 있다.
즉 내 노드가 졌지만, 이 트렌젝션을 다음 블록에 포함하지 않고 이긴 경우에만 트렌젝션이 알리게 되면 무한대로 이기는 경우가 생길 것이다.

그래서 이더리움 블록체인이 외부의 난수 함수에 접근 할 수 있도록 오라클을 사용하는 방법이 있다.

다만 크립토 좀비에서는 실제로 돈을 거래하지 않으므로 keccak256 해시 함수를 사용해 진행하겠다.


좀비 배틀은 다음과 같은 규칙으로 진행될 것이다.

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

좀비 배틀을 위한 컨트랙트를 새로 만들고 이 컨트랙트는 zombieHelper 를 상속하도록 했다.

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 _target) external {
    
  } 
}

위 함수는 보이다시피 완성된 코드가 아니다.

attack 함수는 좀비의 소유자만이 실행 할 수 있어야한다.
함수제어자를 사용해 다음과 같이 zombiefeeding 컨트렉트를 수정해보자.

zombiefeeding.sol

...
contract ZombieFeeding is ZombieFactory {

  KittyInterface kittyContract;

// 함수를 호출하는 사람이 좀비의 소유자인지 구별
  modifier ownerOf(uint _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    _;
  }
...
  
  // 좀비가 먹이를 먹는 행동도 호출한 사람이 소유자인 경우에만 실행
  // ownerOf(_zombieId) 를 함수 정의에 추가
  function feedAndMultiply(uint _zombieId, uint _targetDna, string _species) internal ownerOf(_zombieId) {
   
   // 제어자를 함수 정의에 추가했으니 다음 줄은 삭제
    ~~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);
  }

...
}

zombiehelper.sol

zombiefeeding.sol와 동일하게 수정한다.

  // ownerOf(_zombieId) 를 함수 정의에 추가
  function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) ownerOf(_zombieId) {
  
  // 제어자를 함수 정의에 추가했으니 다음 줄은 삭제
  ~~  require(msg.sender == zombieToOwner[_zombieId]);~~
    zombies[_zombieId].name = _newName;
  }

  // ownerOf(_zombieId) 를 함수 정의에 추가
  function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) ownerOf(_zombieId) {
  
  // 제어자를 함수 정의에 추가했으니 다음 줄은 삭제
    ~~require(msg.sender == zombieToOwner[_zombieId]);~~
    zombies[_zombieId].dna = _newDna;
  }

이제 attack 함수를 마저 작성해보자.

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

 // ownerOf(_zombieId) 를 함수 정의에 추가
 
  function attack(uint _zombieId, uint _targetId) external ownerOf(_zombieId) {
  
  // 두 좀비의 포인터를 얻어 옴
    Zombie storage myZombie = zombies[_zombieId];
    Zombie storage enemyZombie = zombies[_targetId];
  
  // 전투 결과 (0-99사이의 난수)
    uint rand = randMod(100);
  }
}

좀비의 승패 유무를 가리고, 기록하기위해서
Zombie 구조체에 winCountlossCount를 정의해 상태를 저장하겠다.

zombiefactory.sol

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 {
  
 																				 // 0승 0패 초기화
        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);
    }

구조체를 변경했으므로, _createZombie 함수 정의도 0승 0패로 생성되게 수정하였다.

winCountlossCount 에 값을 넣으려면 좀비가 이겼는지 졌는지 판단해야한다.

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

 // ownerOf(_zombieId) 를 함수 정의에 추가
 
  function attack(uint _zombieId, uint _targetId) external ownerOf(_zombieId) {
  
  // 두 좀비의 포인터를 얻어 옴
    Zombie storage myZombie = zombies[_zombieId];
    Zombie storage enemyZombie = zombies[_targetId];
  
  // 전투 결과 (0-99사이의 난수)
    uint rand = randMod(100);
    
    // 전투 결과가 70보다 작으면 승
    // 전투 결과가 70보다 크면 패
    if (rand <= attackVictoryProbability) {
    
      // 승리 홧수 증가
      myZombie.winCount++;
      
      // 레벨 증가
      myZombie.level++;
      
      // 상대방 좀비의 패배 횟수 증가
      enemyZombie.lossCount++;
      
      // 좀비 먹기 
      // 세번째 인자는 "zombie" 로 지정
      // 크립토키티를 먹었을 때와 구별됨
      feedAndMultiply(_zombieId, enemyZombie.dna, "zombie");
      
    } else {
    
      myZombie.lossCount++;
      enemyZombie.winCount++;
    }
    
    // 대기 시간 호출
    _triggerCooldown(myZombie);
  }
}

좋은 웹페이지 즐겨찾기