Crypto Zombie로 Solidity 처음부터 공부하기 [Lesson 5]

Sungmin Oh·2021년 3월 16일

토큰

이더리움에서 토큰은 기본적으로 몇 개의 공통 규약을 따르는 스마트 컨트랙트이다. 즉 다른 모든 토큰 컨트랙트가 사용하는 표준 함수 집합을 구현하는 것이다.

● transfer(adress _to, uint256 _value)
● balanceOf(address _owner)

예시로는 위와 같은 함수들이 있다.

또 각각의 토큰 컨트랙트는 보통 mapping(address => uint256) balances 와 같은 매핑을 가지고 있다. 각 주소에 얼만큼의 잔액이 있는지 기록하는 용도이다.

즉 기본적으로 토큰은 하나의 컨트랙트라고 할 수 있다. 그 안에서 누가 얼만큼의 토큰을 가지고 있는지 기록하고, 내부의 함수를 이용해서 사용자들 간에 토큰을 전송할 수 있게 해주는 것이다.

ERC20 이라는 가장 잘 알려진 토큰의 규격이 있다. ERC20 토큰들은 모두 동일한 함수 집합을 공유하기 때문에 각 토큰들에 똑같은 방식으로 상호작용 할 수 있다. 즉, 내가 어떤 한 토큰과 상호작용 할 수 있는 코드를 만들었을 때, 다른 토큰과도 상호작용 할 수 있도록 만들고 싶다면 따로 코드를 추가할 필요 없이 해당 토큰의 컨트랙트 주소만 추가하면 된다는 것이다. (두 토큰이 모두 ERC20 규격을 따를 때 말이다.)

ERC20 외에도 ERC721 등 여러가지 토큰들이 있다고 한다.

다중 상속

어떤 컨트랙트가 두 개 이상의 컨트랙트를 사용해야 할 때, 다중 상속을 사용할 수 있다. 다중 상속은 아래와 같이 사용한다.

contract 컨트랙트명 is 상속할 컨트랙트1, 상속할 컨트랙트2, ... {

}

이렇게 쉼표로 구분해서 여러 컨트랙트를 상속할 수 있다.

오버플로우/ 언더플로우

uint8 자료형에는 8비트의 데이터(정수)를 저장할 수 있다. 2^8 = 256 이니까 0에서 255까지의 정수를 저장할 수 있다는 것이다. 이 때, 아래와 같은 연산을 하면 어떻게 될까?

uint8 a = 255;
a++;

저장할 수 있는 최대 값인 255를 저장하고, 1을 더하니까 우리의 일반적인 상식과 다르게 a의 값은 제일 작은 값인 0이 된다. 이것이 오버플로우다. 언더플로우도 마찬가지로 이해할 수 있다. 이러한 오버플로우/ 언더플로우 때문에 우리의 DApp에서 여러가지 예상치 못한 문제가 생길 수 있다. (간디가 다른 나라에 핵을 쏟아붓는다던가 하는 버그가 생길 수 있다는 말이다.)

라이브러리를 이용해 오버플로우 막기

이러한 버그를 막기 위해 SafeMath라는 라이브러리를 사용한다. 라이브러리란 특별한 종류의 컨트랙트를 말한다. 라이브러리는 솔리디티에서 기본 데이터 타입(자료형)에 함수를 붙일 때 유용하게 사용된다. 예를 들어서 SafeMath 에서는 "using SafeMath for uint256;" 이라는 구문을 사용하게 될텐데, 이를 통해 아래와 같이 uint256에서 함수에 접근할 수 있게 된다.

using SafeMath for uint256;

uint256 a = 5;
uint256 b = a.add(3);
uint256 c = a.mul(2);

여기에서 add나 mul과 같은 함수는 SafeMath 라이브러리에서 정의하고 있는 함수들이다. SafeMath 라이브러리는 다음과 같이 구성되어있다.

library SafeMath {

  function mul(uint256 a, uint256 b) internal pure returns (uint256) {
    if (a == 0) {
      return 0;
    }
    uint256 c = a * b;
    assert(c / a == b);
    return c;
  }

  function div(uint256 a, uint256 b) internal pure returns (uint256) {
    // assert(b > 0); // Solidity automatically throws when dividing by 0
    uint256 c = a / b;
    // assert(a == b * c + a % b); // There is no case in which this doesn't hold
    return c;
  }

  function sub(uint256 a, uint256 b) internal pure returns (uint256) {
    assert(b <= a);
    return a - b;
  }

  function add(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 c = a + b;
    assert(c >= a);
    return c;
  }
}

이 라이브러리에서는 uint256 자료형에 대한 add, sub, mul, div 함수를 사용할 수 있게 한다. 라이브러리는 컨트랙트와 비슷하지만 우리가 using 키워드를 통해 다른 컨트랙트에서 라이브러리의 메소드들을 다른 데이터 타입에 적용할 수 있게 해준다.

using SafeMath for uint256;

uint256 a = 5;
uint256 b = a.add(3);
uint256 c = a.mul(2);

위에서 본 이 예시에서 add 함수를 사용했는데 라이브러리에서 add 함수를 살펴보면 인수가 2개 필요하다는 것을 알 수 있다. 한 인수는 add 함수를 호출할 때 괄호 안에 넣은 값(3)이고 나머지 하나의 인수는 add 함수가 붙어있는 a이다.

SafeMath 라이브러리에서 제공하는 add 함수 (또는 그 밖의 함수도)에서는 오버플로우가 발생했는지 확인하는 assert문이 포함되어있다. 따라서 Solidity의 기본 수학 연산자를 사용하는 것보다 SafeMath 라이브러리를 활용하는 것이 보안성에 더 좋다고 할 수 있다.

  • 라이브러리 사용 방법
import "라이브러리 파일 주소";

contract 컨트랙트 {
    using 라이브러리명 for 자료형;
    ...
    ...
}

assert

assert는 require와 거의 비슷하다고 보면 된다.

assert(조건);

assert 내의 조건이 참이라면 그대로 진행되고, 거짓이라면 에러를 발생시킨다. 다만 require에서는 에러가 발생하면 남은 가스를 사용자에게 돌려주지만 assert는 남은 가스를 사용자에게 돌려주지 않는다.

주석

주석은 코드에서 기계가 신경쓰지 않는 부분으로, 협업할 때 동료 프로그래머에게, 혹은 미래의 나 자신에게 코드에 대한 이해를 돕기 위해 사용하는 것이다.
주석은 아래와 같이 두 가지 방법으로 사용한다.

// 한 줄 주석, 컴퓨터는 이 부분을 전혀 신경쓰지 않는다.

/* 여러 줄 주석
   이렇게 하면
   이 안에 있는
   모든 코드를
   컴퓨터는 신경쓰지
   않는다.
*/

주석도 막 달아놓는 것이 아니라, 솔리디티 커뮤니티에서 표준으로 사용되는 형식인 natspec이 있다. natspec은 아래와 같은 형식으로 쓴다.

/// @title 컨트랙트/함수의 이름
/// @author 작성자의 이름
/// @notice 사용자에게 컨트랙트/함수가 무엇을 하는지 설명
/// @dev 개발자에게 추가적인 상세 정보를 설명
/// @param 함수에서 어떤 매개변수를 가지는지 설명
/// @return 함수에서 어떤 반환값을 가지는지 설명

필수는 아니지만 코드의 가독성을 위해 표준에 맞춰 주석을 작성하는 연습을 하면 좋을 것 같다.

이 내용은 정보를 공유하기 위함이 아닌 저의 개인적인 공부 기록을 남기기 위함입니다. 따라서 틀린 정보가 있을 수 있으니 유의해서 봐주시면 감사하겠습니다.

profile
ambitious person

0개의 댓글