Advanced Solidity Concepts

nn·2022년 2월 16일
0

크립토좀비

목록 보기
4/8

외부 의존성

솔리디티는 자바스크립트와 같은 다른언어와 꽤 비슷하다.
하지만 이더리움 DApp은 컨트랙트를 배포하고나면 절대 수정하거나 업데이트를 할 수 없다.

배포한 코드는 블록체인에 영구적으로 저장되기 때문에 컨트랙트에 결함이있다면 고칠 수 있는 방법이 없다.

예를 들어 이전 포스팅에서 사용한 크립토키티 컨트랙트 주소에 버그가 있거나, 누군가 고양이들을 모두 파괴했다고 상상해보자.
이 이후 크립토키티 컨트랙트를 사용하는 모든 컨트랙트는 더이상 제대로 사용할 수 없을 것이다.

그렇기 때문에 DApp에 크립토키티의 컨트렉트 주소를 직접 써 넣는 방법대신 크립토키티의 주소가 문제가 생기면 주소를 바꿀 수 있도록 바꿔야한다.

...
contract ZombieFeeding is ZombieFactory {

// 주소를 제거
  address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;

  KittyInterface kittyContract = KittyInterface(ckAddress);


  function feedAndMultiply(uint _zombieId, uint _targetDna, string _species) public {
  ...

위 코드 일부를 다음과 같이 수정했다.

contract ZombieFeeding is ZombieFactory {
	
    KittyInterface kittyContract;

  	function setKittyContractAddress(address _address) external {
    	kittyContract = KittyInterface(_address);
  	}
    
  	function feedAndMultiply(uint _zombieId, uint _targetDna, string 	_species) public {
    ...

주소를 직접적으로 사용하지 않고 함수를 새로 만들어서 KittyInterface 에 대입해주었다.


OpenZeppelin의 Ownable 컨트랙트

위에서 새로 추가한 setKittyContractAddressexternal 이므로 누구나 함수를 호출할 수 있다.
즉, 보안 취약점이 될 수 있다.

크립토키티의 주소가 변경되었을 때 컨트렉트는 크립토키티의 주소를 바꿀 순 있지만, 아무나 주소를 바꾸게 해서는 안된다.

다음은 OpenZeppelin 라이브러리에서 가져온 Ownable 컨트랙트이다.

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

위 컨트랙트에서 봐야할 몇가지 요소들이있다.

1. 생성자

  • function Ownable() 생성자는 컨트랙트가 실행될 때 딱 한번만 실행된다.

2. 함수제어자

  • modifier onlyOwner() 제어자는 다른 함수에 대한 접근을 제어하기위해 사용된다. 즉, 함수를 실행하기전 요구사항 충족 여부를 확인한다.
  • onlyOwner 는 오직 컨트랙트의 소유자만 해당 함수를 실행할 수 있도록 접근을 제어한다.

3. indexed 키워드

  • 출력된 이벤트를 블록안에 넣을 때 원하는 이벤트만 필터링하고 싶을 때 사용한다.
    다음 포스팅에 예제와 함께 설명

즉 위 설명에 Ownable 컨트랙트는 다음과 같은 것들을 할 수 있다.

  • 컨트랙트가 생성되면 컨트랙트의 생성자가 owner에 msg.sender(컨트랙트를 배포한 사람)를 대입한다.
  • 특정한 함수들에 대해서 오직 소유자만 접근할 수 있도록 제한 가능한 onlyOwner 제어자를 추가한다.
  • 새로운 소유자에게 해당 컨트랙트의 소유권을 옮길 수 있도록 한다.

onlyOwner는 컨트랙트에서 흔히 쓰는 것 중 하나라, 대부분의 솔리디티 DApp들은 Ownable 컨트랙트를 복사/붙여넣기 하면서 시작한다.
그리고 첫 컨트랙트는 이 컨트랙트를 상속해서 만든다.


함수 제어자

이전 포스팅에서도 설명했듯이 상속을 받으면 상속한 컨트랙트의 함수/이벤트/제어자에 접근할 수 있다.

그렇기 때문에 Ownable 컨트랙트를 상속받으면 아래의 modifier 키워드를 사용한 함수제어자에 접근할 수 있다.

  modifier onlyOwner() {
    require(msg.sender == owner);
    _;
  }

onlyOwner() 에 접근하는 방법은 다음과 같다.

contract MyContract is Ownable {
event LaughManiacally(string laughter);

// `onlyOwner`의 사용 방법
function likeABoss() external onlyOwner {
  	LaughManiacally("Muahahahaha");
	}
}

likeABoss함수를 호출하면, onlyOwner의 코드가 먼저 실행된다.
그리고 onlyOwner_; 부분을 likeABoss 함수로 되돌아가 해당 코드를 실행하게 된다.

이 내용을 기억하고 코드를 수정하면 다음과 같다.

zombiefactory.sol

pragma solidity ^0.4.19;

//ownable.sol 상속 받기위해 임포트.
import "./ownable.sol";

// 상속
contract ZombieFactory is Ownable {

    event NewZombie(uint zombieId, string name, uint dna);

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    struct Zombie {
        string name;
        uint dna;
 
    }

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

}

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;

// onlyOwner 제어자 추가
  function setKittyContractAddress(address _address) external onlyOwner {
    kittyContract = KittyInterface(_address);
  }

  function feedAndMultiply(uint _zombieId, uint _targetDna, string _species) public {
    require(msg.sender == zombieToOwner[_zombieId]);
    Zombie storage myZombie = zombies[_zombieId];
    _targetDna = _targetDna % dnaModulus;
    uint newDna = (myZombie.dna + _targetDna) / 2;
    if (keccak256(_species) == keccak256("kitty")) {
      newDna = newDna - newDna % 100 + 99;
    }
    _createZombie("NoName", newDna);
  }

  function feedOnKitty(uint _zombieId, uint _kittyId) public {
    uint kittyDna;
    (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
    feedAndMultiply(_zombieId, kittyDna, "kitty");
  }

}

이 제어자를 사용하는 법은 다양한 방법이 있지만 일반적으로는 함수 실행 전에 require 체크를 넣는다.

참고 : 이렇게 소유자가 컨트랙트에 특별한 권한을 갖도록 하는 것은 자주 필요하지만, 이게 악용될 수도 있음을 기억해야한다. 예를 들어, 소유자가 다른 사람의 좀비를 뺏어올 수 있도록 하는 백도어 함수를 추가하는 경우가 있을 것이다.

그러므로 이더리움에서 돌아가는 DApp이라고 해서 그것만으로 분산화되어 있다고 할 수는 없다. 반드시 전체 소스 코드를 읽어보고, 잠재적으로 걱정할 만한, 소유자에 의한 특별한 제어가 불가능한 상태인지 확인해야한다.
개발자로서는 잠재적인 버그를 수정하고 DApp을 안정적으로 유지하도록 하는 것과, 사용자들이 그들의 데이터를 믿고 저장할 수 있는 소유자가 없는 플랫폼을 만드는 것 사이에서 균형을 잘 잡는 것이 중요하다.

profile
내가 될 거라고 했잖아

0개의 댓글

관련 채용 정보