이더리움에서 토큰은 기본적으로 몇 개의 공통 규약을 따르는 스마트 컨트랙트이다. 즉 다른 모든 토큰 컨트랙트가 사용하는 표준 함수 집합을 구현하는 것이다.
● 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는 require와 거의 비슷하다고 보면 된다.
assert(조건);
assert 내의 조건이 참이라면 그대로 진행되고, 거짓이라면 에러를 발생시킨다. 다만 require에서는 에러가 발생하면 남은 가스를 사용자에게 돌려주지만 assert는 남은 가스를 사용자에게 돌려주지 않는다.
주석은 코드에서 기계가 신경쓰지 않는 부분으로, 협업할 때 동료 프로그래머에게, 혹은 미래의 나 자신에게 코드에 대한 이해를 돕기 위해 사용하는 것이다.
주석은 아래와 같이 두 가지 방법으로 사용한다.
// 한 줄 주석, 컴퓨터는 이 부분을 전혀 신경쓰지 않는다.
/* 여러 줄 주석
이렇게 하면
이 안에 있는
모든 코드를
컴퓨터는 신경쓰지
않는다.
*/
주석도 막 달아놓는 것이 아니라, 솔리디티 커뮤니티에서 표준으로 사용되는 형식인 natspec이 있다. natspec은 아래와 같은 형식으로 쓴다.
/// @title 컨트랙트/함수의 이름
/// @author 작성자의 이름
/// @notice 사용자에게 컨트랙트/함수가 무엇을 하는지 설명
/// @dev 개발자에게 추가적인 상세 정보를 설명
/// @param 함수에서 어떤 매개변수를 가지는지 설명
/// @return 함수에서 어떤 반환값을 가지는지 설명
필수는 아니지만 코드의 가독성을 위해 표준에 맞춰 주석을 작성하는 연습을 하면 좋을 것 같다.
이 내용은 정보를 공유하기 위함이 아닌 저의 개인적인 공부 기록을 남기기 위함입니다. 따라서 틀린 정보가 있을 수 있으니 유의해서 봐주시면 감사하겠습니다.