Solidity

SolidityCryptoZombies 4 (좀비 전투 시스템) 정리

dlwltn98 2023. 2. 9. 12:05

접근 제어자 (visibility modifier)

 - 함수가 언제, 어디서 호출 될 수 있는지 제어

  내부 컨트랙트 외부 컨트랙트 상속 컨트랙트
Private O X X
Internal O X O
Public O O O
External X O X

 

상태 제어자 (state modifier)

 - 블록체인과 상호작용 하는 방법을 알려줌

view - 해당 함수를 실행해도 어떤 데이터도 저장/변경되지 않음
- 그저 읽기만 함
pure - 해당 함수를 실행해도 어떤 데이터도 저장/변경되지 않음
- 어떤 데이터도 읽지 않음
payable - 이더(ether)를 받을 수 있음

 

사용자 정의 제어자

 - 함수에 이 제어자들이 어떻게 영향을 줄지를 결정하는 논리구성 가능

 

위의 제어자들은 아래와 같이 사용 가능

function test() external view onlyOwner anotherModifier { /* ... */ }

 

Payable 

 - 함수 제어자

 - 이더를 받을 수 있는 특별한 함수 유형

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

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

 * ether : 기본적으로 포함된 단위

 

// web3.js 에서 아래 함수를 실행하면 위 코드 실행
OnlineStore.buySomething({from: web3.eth.defaultAccount, value: web3.utils.toWei(0.001)})

 * web3.js : Dapp의 JS 프론트엔드

 * value : 이더(ether)를 얼마나 보낼지 결정 (함수에선 0.001)

 * 솔리디티 함수에 payable로 표시되지 않았는데 이더를 보내려고 하면 함수에서 트랜잭션 거부함

 

출금

 - 이더를 보내면 해당 컨트랙트의 이더리움 계좌에 저장됨

 - 컨트랙트로부터 이더를 인출하는 함수를 만들어야 인출 가능

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

 * transfer() : 이더를 특정 주소로 전달 가능

 * this.balance : 컨트랙트에 저장되어 있는 전체 잔액 반환

 

난수 (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;

 1. keccak를 사용해서 값들을 임의의 해쉬 값으로 변환

    - now(타임 스탬프 값)

    - nonce(딱 한번만 사용되는 숫자)

    똑같은 입력으로 두 번 이상 동일한 해시 함수 실행할 수 없게하는 역할

 2. 변환 값을 uint 값으로 변환

 3. %100을 써서 마지막 두자리만 받음

    ⇒ 0 ~ 99 의 값 얻을 수 있음

 

위 메소드의 문제점 : 정직하지 않은 노드 공격에 취약

 - 컨트랙트 실행 → 트랜잭션으로서 네트워크 노드 하나 혹은 여러 노드에 실행 알림

 - 네트워크 노드 : 여러개의 트랜잭션 모으고 "작업증명"으로 알려진 문제 풀려고 시도

 - 해당 트랜잭션 그룹을 그들의 작업증명(PoW)과 함께 블록으로 네트워크에 배포

 - 한 노드 : PoW를 풀기 성공

 - 다른 노드들 : PoW 풀려는 시도 멈춤, 해당 노드가 보낸 트랜잭션이 유효한 것인지 검증

 - 유효하다면 해당 블록을 받아들이고 다음 블록 풀려고 시도

    ⇒ 이것이 난수 함수 발생을 취약하게 만듦

 

 

수정 코드

 - 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;  // 재사용 대기시간
        uint16 winCount;   // 전투 승리 횟수
        uint16 lossCount;  // 전투 패배 횟수
    }

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

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


    // 함수 접근 제어자를 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), 0, 0)) - 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;

    // 좀비 소유자 확인하는 사용자 정의 제어자
    modifier ownerOf(uint _zombieId) {
        require(msg.sender == zombieToOwner[_zombieId]);
    }
    
    // 소유자만 수정할 수 있도록 수정
    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 ownerOf(_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  {

    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 levelUpFee(uint _fee) external onlyOwner {
        levelUpFee = _fee; // 레벨업 이더 값 변경
    }

    // 이더를 받으면 좀비의 레벨을 올려주는 함수
    function levelUp(uint _zombieId) external payable {
        require(msg.value == levelUpFee); // 함수 실행에 이더가 보내졌는지 확인
        zombies[_zombieId].level++; // 확인되면 좀비 레벨 증가
    }

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

    // 좀비의 레벨이 20이상이면 좀비에게 임의의 dna 줄 수 있음
    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.lengthl; i++) {
            if(zombieToOwner[i] == _owner) {
                result[counter] = i;
                counter++;
            }
        }
        return result;
    }

}

 - zombieattack.sol

pragma solidity ^0.4.19;

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

 

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

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