토큰
은 기본적으로 몇몇 공통 규약을 따르는 스마트 컨트랙트
이다.컨트랙트 안에서 누가 얼마나 많은 토큰을 가지고 있는지 기록하고, 몇몇 함수를 가지고 사용자들이 그들의 토큰을 다른 주소로 전송할 수 있게 한다.토큰
예를 들면 transfer(address _to, uint256 _value)
나 balanceOf(address _owner)
같은 함수!내부적으로 스마트 컨트랙트는 보통 mapping(address => uint256) balances와 같은 매핑을 가지고 있다. 각각의 주소에 잔액이 얼마나 있는지 기록하는 것. 토큰도 스마트 컨트랙트의 일종이므로 저 매핑을 가지고 있다.
즉 기본적으로 토큰은 그냥 하나의 컨트랙트!
💵 ERC20 토큰은 화폐처럼 사용되는 토큰으로 적절하다.
모든 ERC20 토큰들이 똑같은 이름의 동일한 함수 집합을 공유하기 때문에, 이 토큰들에 똑같은 방식으로 상호작용이 가능하다.
따라서, 로직
은 한번 구현하고, 새로운 토큰을 추가하는 것은 DB
에 새 컨트랙트 주소
를 추가하기만 하면 된다. (토큰이 스마트 컨트랙트 그 자체이므로)
지금 만들고 있는 좀비 게임에서는 아래와 같은 이유로 ERC20을 못쓴다..!
Steve
는 내 레벨732 좀비 H4XF13LD MORRIS 💯💯😎💯💯
와는 완전히 다르다.☄️ 크립토좀비와 같은 크립토 수집품을 위해 더 적절한 토큰 표준은 ERC721 토큰!
ERC712 토큰을 사용하는 이유
contract ERC721 {
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId);
function balanceOf(address _owner) public view returns (uint256 _balance);
function ownerOf(uint256 _tokenId) public view returns (address _owner);
function transfer(address _to, uint256 _tokenId) public;
function approve(address _to, uint256 _tokenId) public;
function takeOwnership(uint256 _tokenId) public;
}
contract SatoshiNakamoto is NickSzabo, HalFinney {
// , 로 간단히 다중 상속 가능
}
,
로 구분해서 나란히 쓴다. function balanceOf(address _owner) public view returns (uint256 _balance);
address
를 받아, 해당 address
가 토큰
을 얼마나 가지고 있는지 반환한다.토큰
은 좀비
들이 된다. function ownerOf(uint256 _tokenId) public view returns (address _owner);
토큰 ID
를 받아, 이를 소유하고 있는 사람의 address
를 반환한다.function transfer(address _to, uint256 _tokenId) public;
address
, 전송하고자 하는 _tokenId
와 함께 transfer
함수를 호출하는 것이다.function approve(address _to, uint256 _tokenId) public;
function takeOwnership(uint256 _tokenId) public;
approve
를 호출하는 형식이다.mapping (uint256 => address)
를 써서 이를 확인한다.takeOwnership
을 호출하면, 해당 컨트랙트는 이 msg.sender
가 소유자로부터 토큰을 받을 수 있게 허가를 받았는지 확인한다. 그리고 허가를 받았다면 해당 토큰을 그에게 전송한다.transfer
와 takeOwnership
모두 동일한 전송 로직을 가지고 있다. 순서만 반대인 것!(전자는 토큰을 보내는 사람이 함수를 호출, 후자는 토큰을 받는 사람이 호출).
그러니 이 로직만의 프라이빗 함수, _transfer
를 만들어 추상화하면 두 함수에서 모두에서 효율적으로 사용할 수 있다.
function _transfer(address _from, address _to, uint256 _tokenId) private {
ownerZombieCount[_to]++;
ownerZombieCount[_from]--;
zombieToOwner[_tokenId] = _to;
Transfer(_from, _to, _tokenId);
}
작동 방식 요약
address
와 그에게 보내고 싶은 _tokenId
를 사용하여 approve
를 호출한다._tokenId
를 사용하여 takeOwnership
함수를 호출하면, 컨트랙트는 그가 승인된 자인지 확인하고 그에게 토큰을 전송한다.🌝 컨트랙트 보안 고려 요소: 오버플로우, 언더플로우
우리가 8비트 데이터를 저장할 수 있는 uint8
하나를 가지고 있다고 해보지. 이 말인즉 우리가 저장할 수 있는 가장 큰 수는 이진수로 11111111
(또는 십진수로 2^8 - 1 = 255)가 된다.
uint8 number = 255;
number++;
이 예시에서, 우리는 이 변수에 오버플로우를 만들었다.
number
는 직관과는 다르게 0
이 된다.이진수 11111111
에 1
을 더하면, 이 수는 00000000
으로 돌아간다.언더플로우는 오버플로우와 유사하게 0
값을 가진 uint8
에서 1
을 빼면, 255
와 같아지는 것을 말한다. (uint에 부호가 없어, 음수가 될 수 없기 때문에!)
따라서, 미래에 우리의 DApp에 예상치 못한 문제가 발생하지 않도록 컨트랙트에 보호 장치를 두어야 한다! (컨트랙트 한번 올리면 수정도 못함..)
오버플로우와 언더플로우를 막기 위해, OpenZeppelin
에서 기본적으로 이런 문제를 막아주는 SafeMath
라고 하는 라이브러리를 만들었다.
라이브러리(Library)는 솔리디티에서 특별한 종류의 컨트랙트이다. 이게 유용하게 사용되는 경우 중 하나는 기본(native) 데이터 타입에 함수를 붙일 때!
예를 들어, SafeMath
라이브러리를 쓸 때는 using SafeMath for uint256
이라는 구문을 사용한다. SafeMath
라이브러리는 4개의 함수를 가지고 있다. - add
, sub
, mul
,div
. 함수 접근은 아래와 같이 이루어진다.
using SafeMath for uint256;
uint256 a = 5;
uint256 b = a.add(3); // 5 + 3 = 8
uint256 c = a.mul(2); // 5 * 2 = 10
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;
}
}
라이브러리는 using
키워드를 사용할 수 있게 해준다. 이를 통해 라이브러리의 메소드들을 다른 데이터 타입에 적용할 수 있다.
using SafeMath for uint;
// 우리는 이제 이 메소드들을 아무 uint에서나 쓸 수 있다.
uint test = 2;
test = test.mul(3); // test는 이제 6이 된다
test = test.add(5); // test는 이제 11이 된다
mul
과 add
함수는 각각 2개의 인수를 필요로 한다.하지만 우리가 using SafeMath for uint
를 선언할 때, 우리가 함수를 적용하는 uint(test)
는 첫 번째 인수로 자동으로 전달된다.function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
//여기가 오버플로우를 막는 부분
assert(c >= a);
return c;
}
add
는 그저 2개의 uint
를 +처럼 더한다.assert
구문을 써서 그 합이 a보다 크도록 보장하고, 이것이 오버플로우를 막아준다.assert
는 조건을 만족하지 않으면 에러를 발생시킨다는 점에서 require
와 비슷하다.assert
와 require
의 차이점은, require
는 함수 실행이 실패하면 남은 가스를 사용자에게 되돌려 주지만, assert
는 그렇지 않다는 것이다.require
를 쓰고, assert
는 일반적으로 코드가 심각하게 잘못 실행될 때 사용한다.오버플로우나 언더플로우를 막기 위해, 우리의 코드에서 +
, -
, *
또는 /
을 쓰는 곳을 찾아 add
, sub
, mul
, div
로 교체한다.
예를 들어, 아래처럼 하는 대신
myUint++;
이렇게 사용해 오버플로우와 언더플로우를 방지한다.
myUint = myUint.add(1);
/// @title 기본적인 산수를 위한 컨트랙트
/// @author H4XF13LD MORRIS 💯💯😎💯💯
/// @notice 지금은 곱하기 함수만 추가한다.
contract Math {
/// @notice 2개의 숫자를 곱한다.
/// @param x 첫 번쨰 uint.
/// @param y 두 번째 uint.
/// @return z (x * y) 곱의 값
/// @dev 이 함수는 현재 오버플로우를 확인하지 않는다.
function multiply(uint x, uint y) returns (uint z) {
// 이것은 일반적인 주석으로, natspec에 포함되지 않는다.
z = x * y;
}
}
@title
과 @author
: 말 그대로 제목, 쓴사람@notice
: 사용자에게 컨트랙트/함수가 무엇을 하는지 설명한다.@dev
: 개발자에게 추가적인 상세 정보를 설명한다.@param
과 @return
: 함수에서 어떤 매개 변수와 반환값을 가지는지 설명한다.@dev
는 대부분 남겨야 함!