이더리움에서 게임을 만든다? 크립토 좀비(5. ERC721 & 크립토 수집품)

민식킴·2021년 3월 12일
0

크립토 좀비

목록 보기
8/9
사족

11일차, 12일차.
의도치않게 그라운드x강의와 크립토좀비 모두 NFT에 대해 다루었다.
NFT는 대체불가능한 토큰이다. 이를 활용한 재미난 것들이 많다.

CH.1,2 이더리움 상의 토큰 & ERC721 표준, 다중 상속

토큰에 대해서 얘기해보지.

..._ERC20 토큰에 대해서 말이네.
...모든 ERC20 토큰들이 똑같은 이름의 동일한 함수 집합을 공유하기 때문에, 이 토큰들에 똑같은 방식으로 상호작용이 가능하네.

즉 자네가 하나의 ERC20 토큰과 상호작용할 수 있는 애플리케이션 하나를 만들면, 이 앱이 다른 어떤 ERC20 토큰과도 상호작용이 가능한 것이지...

여기서 말하는 ERC20토큰은 그저 우리가 흔히 아는 그 코인과 같은 성질을 띈다. 하지만 화폐로 사용하는것 외에 크립토 좀비에서 유용하게 쓸수 있는건 없다. 그대신, 대체불가능한, 고유한 토큰인 NFT(Non-Fungible token)을 사용한다면, 단순히 크립토좀비 컨트랙트에서 NFT함수, ERC721함수를 실행한다면 좀비하나하나를 ERC721토큰과 매칭시킬수 있고 좀비를 물건같이 거래도 손쉽게 가능해진다!
*참고로 다중 상속방법 : contract SatoshiNakamoto is NickSzabo, HalFinney{} // 콤마~~?

앞으로 구현할 함수들은 이렇다.

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

CH.3 balanceOf & ownerOf

(ERC721을 구현할 함수 중 2개)
ERC721을 구현할 것이다. import "./erc721.sol";를 잊지말자.(파일도)

balanceOf
function balanceOf(address _owner) public view returns (uint256 _balance);
이 함수는 단순히 address를 받아, 해당 address가 토큰을 얼마나 가지고 있는지 반환하는데 여기에선 내 주소에 좀비가 얼마나 있는지 반환하는 역할을 하겠다.

ownerOf
function ownerOf(uint256 _tokenId) public view returns (address _owner);
이 함수에서는 토큰 ID(좀비 ID)를 받아, 이를 소유하고 있는 사람의 address를 반환한다. 컨트랙트를 실행시키는 사용자가 올바른지 확인하기위한 역할을 할것이다.

CH.4 리팩토링

그런데 문제가 생겼다. 컨트랙트를 짜면서 나역시 ownerOf함수를 짰기때문에 이 둘이 충돌을 일으킬 것이다. 둘중 무엇을 바꾸어야 할까?
만약 ERC721의 함수이름을 바꾼다면 ERC721은 제대로 작동하지 않을것이다. 즉 내가 만들었던 함수의 이름을 바꿔주어야 한다.

CH.5,6 ERC721: 전송 로직

(한 사람이 다른 사람에게 소유권을 넘기는 것을 구현할 함수)

ERC721 스펙에서는 토큰을 전송할 때 2개의 다른 방식이 있음을 기억하게:

  1. 첫 번째 방법은 토큰의 소유자가 전송 상대의 address, 전송하고자 하는 _tokenId와 함께 transfer 함수를 호출하는 것이네.

  2. 두 번째 방법은 토큰의 소유자가 먼저 위에서 본 정보들을 가지고 approve를 호출하는 것이네. 그리고서 컨트랙트에 누가 해당 토큰을 가질 수 있도록 허가를 받았는지 저장하지. 보통 mapping (uint256 => address)를 써서 말이지. 이후 누군가 takeOwnership을 호출하면, 해당 컨트랙트는 이 msg.sender가 소유자로부터 토큰을 받을 수 있게 허가를 받았는지 확인하네. 그리고 허가를 받았다면 해당 토큰을 그에게 전송하지.

말이 길었는데 1번은 토큰을 보내는 함수로 해당 주소에 전송하는것이고,
2번은 보내는사람이 받는사람의 주소를 허용하고 받는사람이 해당함수를 호출하여 허용된 주소로 토큰을 받아가는 것이다.(아니왜 복잡하게 2번도 있을까?)

CH.7 ERC721: Approve (: 승인하다)

approve / takeOwnership을 사용하는 전송은 2단계로 나뉜다는 것을 기억하게: // 둘로 나뉜다는 뜻이 아니다

  1. 소유자인 자네가 새로운 소유자의 address와 그에게 보내고 싶은 _tokenId를 사용하여 approve를 호출하네.

  2. 새로운 소유자가 _tokenId를 사용하여 takeOwnership 함수를 호출하면, 컨트랙트는 그가 승인된 자인지 확인하고 그에게 토큰을 전송하네.

내용을 보아하니 위의 2번 전송 로직에서 Approve함수를 크게 둘로 나눈것이다.

CH.8 ERC721: takeOwnership

우린 이제 이 함수가 승인된 자인지 확인하는 함수임을 안다.

function takeOwnership(uint256 _tokenId) public { // 왜 제어자 함수를 쓰지 않은걸까..?
    require(zombieApprovals[_tokenId] == msg.sender); // 본인확인
    address owner = ownerOf(_tokenId); // 오너의 주소가 바뀜
    _transfer(owner, msg.sender, _tokenId); // 보내는 함수 실행
}

CH.9~ 오버플로우 막기 (SafeMath)

만약 uint8에서 255의 값을 가진 변수가 ++된다면 어떻게 될까,
우리의 상식과는 다르게 0이 되어버린다. 이러면 우리가 의도한대로 함수가 작동하지 않을 수도 있다. (사용자가 공격을 시도한다던가..) 물론 반대로 0에서 1을 뺀다면 255가 된다.
이를 오버플로우라고 하는데, 42서울에서도 이것때문에 애많이 먹었다..
이것을 막아주는 편리한(?) 함수가 있는데 SafeMath라고 한다. 사용법이 신기한데,

using SafeMath for uint256; // 라이브러리 함수 추가하는 느낌?

uint256 a = 5;
uint256 b = a.add(3); // 5 + 3 = 8
uint256 c = a.mul(2); // 5 * 2 = 10
(uint256) pp = pp.add(1); // == pp++;

따라서 여태 ++, --해준 것들을 모두 SafeMath를 사용하여 바꾸어주었다ㅠㅠ
SafeMath함수의 로직은 단순하다.

library SafeMath { // 이것이 a.add 를 쓸수 있게 해준 친구이다.
	function add(uint256 a, uint256 b) internal pure returns 	(uint256) {
    		uint256 c = a + b;
    		assert(c >= a); // require과 비슷하지만 다른점은 오류가 났을 때 가스를 되돌리지 않지만(!) 확실한 에러를 뜨게 할 수 있다.
    		return c;
	}
}

CH.13~ 주석(Comment)

///*, */ 이 있다는 것은 알것이다. 그리고 서로 약속한 주석이 또 있는데 바로 /// ~이다.

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

귀찮게 주석을 일일이 달 필요는 없지않을까 했지만

자네가 모든 함수에 대해 꼭 이 모든 태그들을 항상 써야만 하는 것은 아니라는 점을 명심하게 - 모든 태그는 필수가 아니네. 하지만 적어도, 각각의 함수가 어떤 것을 하는지 설명하도록 @dev는 남기도록 하게.

@dev는 그래도 남기는 것이 미덕인 듯 하다.

profile
우리의 꿈, 우리의 희망

0개의 댓글