[크립토 좀비] 2일차

재호·2022년 7월 8일
0

크립토 좀비

목록 보기
2/3

레슨 3

외부 의존성

컨트랙트를 이더리움에 배포를 하고나면 컨트랙트는 수정하거나 업데이트할 수 없는 Immutable상태가 된다. 하지만 만약 우리가 외부에서 컨트랙트를 가져와 사용하는데 그 컨트랙트에 알지못했던 버그가 발견되었다면 이를 막을 방법이 없어진다. 이를 위해 DApp의 중요한 일부를 수정할 수 있도록 하는 함수를 만들어 놓는 것이 합리적일 것이다. 실습에서는 레슨2에서 받아왔던 크립토키티 컨트랙트에 문제가 생기면 해당 주소를 바꿀 수 있도록 해주는 setKittyContractAddress 함수를 만들었다.

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

}
소유 가능한 컨트랙트

컨트랙트에 대한 권한을 부여하기 위해 Ownable 컨트랙트를 사용하였다. Ownable 컨트랙트는 OpenZepplein 솔리디티 라이브러리를 통해 사용할 수 있다. 많은 컨트랙트들이 흔히 사용하는 기능이며 Ownable 컨트랙트를 복사/붙여놓기 하면서 구현을 시작한다고 볼 수 있다.

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;
  }
}
함수 제어자(Function Modifier)

제어자는 다른 함수들에 대한 접근을 제어하기 위해 사용되는 일종의 유사 함수이다. 함수를 선언할 때 제어자를 같이 선언하여 사용하며 함수를 호출하면 제어가자 먼저 실행되고 제어자의 _;부분에서 기존 함수로 되돌아가 코드들을 실행한다.

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

  event LaughManiacally(string laughter);

  function likeABoss() external onlyOwner {
    LaughManiacally("Muahahahaha");
  }

함수 제어자는 또한 함수처럼 인수를 가질 수 있다. 함수를 선언할 때 함수의 인수를 제어자에게 똑같이 전달할 수 있다.

// 사용자의 나이를 저장하기 위한 매핑
mapping (uint => uint) public age;

// 사용자가 특정 나이 이상인지 확인하는 제어자
modifier olderThan(uint _age, uint _userId) {
  require (age[_userId] >= _age);
  _;
}

// 차를 운전하기 위햐서는 16살 이상이어야 하네(적어도 미국에서는).
// `olderThan` 제어자를 인수와 함께 호출하려면 이렇게 하면 되네:
function driveCar(uint _userId) public olderThan(16, _userId) {
  // 필요한 함수 내용들
}
가스

솔리디티에서는 사용자들이 우리가 만든 DApp의 함수를 실행할 때마다 가스라고 불리는 화폐를 지불해야 한다. 함수를 실행하는 데에 얼마나 많은 가스가 필요한지는 그 함수의 로직이 얼마나 복잡한지에 따라 달라진다. 가스는 이더를 이용해서 사기 때문에 사용자들은 실제 돈을 쓰며 사용하는 것이다. 그렇기에 솔리디티에서 코드 최적화는 다른 프로그래밍 언어들에 비해 훨씬 더 중요한 것이다.
가스를 아끼기 위해 uint를 uint8, uint16 등등으로 바꾸는 것은 의미가 없다. 기본적으로 솔리디티에서 uint는 크기에 상관없이 256비트의 저장 공간을 미리 잡아놓기 때문이다. 하지만 struct안에서는 uint의 크기를 최적화해야 한다. 구조체는 선언하는 uint의 크기로 데이터를 잡으며 같은 uint를 나란히 쓰는 것이 가스를 절약할 수 있는 방법이다.

구조체를 인수로 전달하기

솔리디티에서는 private 또는 internal 함수에 인수로서 구조체의 storage 포인터를 전달할 수 있다. 이런 방식은 여러 단계를 거쳐 데이터를 찾는 대신 쉽게 참조를 전달할 수 있다.

View를 통한 가스 절약

view 함수는 사용자에 의해 외부에서 호출되었을 때 가스를 전혀 소모하지 않는다. 블록체인 상에서 실제로 트랙잭션이 발생하지 않기 때문이다. 하지만 만약 view 함수가 동일 컨트랙트 내에 있는, view 함수가 아닌 다른 함수에서 내부적으로 호출될 경우, 여전히 가스를 소모할 것이다.

메모리에 배열 선언하기

솔리디티에서 비싼 연산 중 하나가 storage를 쓰는 것이다. storage 연산은 메모리가 아니라 블록체인에 영구적으로 기록되기 때문이다. 그렇기 때문에 다른 프로그래밍 언어에서는 비효율적으로 보일 수도 있는 배열을 생성하는 것이 솔리디티에서는 저렴한 방식이 될 수 있다.

function getArray() external pure returns(uint[]) {
  // 메모리에 길이 3의 새로운 배열을 생성한다.
  uint[] memory values = new uint[](3);
  // 여기에 특정한 값들을 넣는다.
  values.push(1);
  values.push(2);
  values.push(3);
  // 해당 배열을 반환한다.
  return values;
}

위와 같이 메모리에 배열을 선언할 때는 memory라는 명령어가 필요하고 반드시 배열의 길이 인수가 함께 생성되어야 한다.

레슨 4

payable

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

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

transfer함수를 통해 이더를 특정 주소로 전달할 수 있다.

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

여기서 this.balance는 컨트랙트에 저장되어있는 전체 잔액을 반환한다.

난수

솔리디티에서 난수를 만들기에 가장 좋은 방법은 keccak256 해시 함수를 사용하는 것이다.

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

위 예시는 동일한 값을 사용할 수 없게 하며 %100 을 통해 0~99 의 난수를 발생시킨다.
하지만 이더리움에서 안전한 난수를 만들어내는 것이 어려운 문제이다. 내가 만약 노드를 실행하고 있다면 나는 오직 나의 노드에만 트랜잭션 발생을 알리고 이것을 공유하지 않을 수 있다. 그 후 발생한 난수가 나에게 불리하다면 다음 블록에 해당 트랜잭션을 포함하지 않는 것을 선택하고 이득이 발생하고 다음 블록을 풀 때까지 무한대로 반복할 수 있기 때문이다.

블록체인의 전체 내용은 모든 참여자에게 공개되므로 이 문제는 굉장히 풀기 어려운 문제이고, 그 해결법 중 하나는 이더리움 블록체인 외부의 난수 함수에 접근할 수 있도록 오라클을 사용하는 것이다.

하지만 본 실습에서는 난수 공격이 돈이 되지 않기에 타협하며 진행할 것이다.

profile
Java, Spring, SpringMVC, JPA, MyBatis

0개의 댓글