
좀더 사용자 입장에서 익숙한 용어는 바로 NFT이다.
이더리움의 개선 제안의 721번쨰에서 하나하나 구분 가능한 토큰에 대한 토론이 이루어 졌다.
ERC-20같이 완전히 대체 가능한 토큰이 아니라 오로지 단 하나의 토큰으로만 존재하는 토큰을 만드는 제안이였고
이를 NFT라고 부르게 되었다.
NFT가 존재하는 이유에 대해서 생각을 해보자.
나의 사견으로는 일단 현 시점에서 NFT는 너무 비싸다고 생각을 한다.
일단 NFT가 존재하는 이유는 간단하다. 유일한 자산이 필요하기 때문이다.
현실 사회에서 집이라는 존재를 생각해보자.
물론 임대라는 역할로써의 집도 있지만
임대라는 역할의 집을 수행하려면 누군가가 그 집의 주인이 되어야 한다.
NFT가 이 주인 역할을 수행하게 된다.
현실 사회에서는 이 주인이라는 역할을 증명하려면 많은 서류와 과정이 필요하게 된다.
하지만 NFT를 활용하면 누가 주인인지 생태계에 입증이 되고 매우 간단한 과정이라고 할 수가 있다.
이외에도 게임에도 정말 다양한 방식으로 사용가능하다.
NFT가 큰 가치를 지니는 이유는 유일무이하다는 특징 떄문이다.
둘의 가장 큰 차이는 중복이 가능하냐 이다.
ERC-20토큰은 누구나 발행이 가능하며 주인이 없는 토큰이라고 할 수가 있다.
하지만 ERC-721토큰은 주인이 명확하게 주어지는 토큰이며 누구나 발행 할 수 있지만 그 데이터에 대한 값은 오로지 하나이다.
ERC721토큰은 일단 ERC20토큰보다 복잡하다..
일단 기본적으로 OpenZeppelin을 이용하여 고유한 NFT를 만드는 코드를 살펴보자.
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract GameItem is ERC721 {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
constructor() public ERC721("GameItem", "ITM") {}
function awardItem(address player, string memory tokenURI) public returns (uint256){
_tokenIds.increment();
uint256 newItemId = _tokenIds.current();
_mint(player, newItemId);
_setTokenURI(newItemId, tokenURI);
return newItemId;
}
}
모든 플레이어 계정에서 awardItem을 통해서 고유한 아이템을 생성 가능하다.
https://docs.openzeppelin.com/contracts/3.x/api/token/erc721#IERC721-balanceOf-address-
🔨 ApprovalForAll
이벤트 함수이다.
ApprovalForAll(address owner, address operator, bool approved)
컨트랙트 오너(owner)가 오퍼레이터에게 모든 자산을 관리할 수 있는 권한을 부여하거나 없애는 역할이다.
setApprovalForAll함수를 실행하면 이 이벤트가 실행이 된다.
setApprovalForAll(operator, _approved)🔨 ownerOf(uint256 tokenId)
tokenId를 매개변수로 받으며 해당 값에 맞는 NFT소유자를 알려준다.
function ownerOf(uint256 tokenId) public view virtual override returns (address) {
address owner = _owners[tokenId];
require(owner != address(0), "ERC721: owner query for nonexistent token");
return owner;
}
public으로 누구나 소유자가 누구인지 확인이 가능하며
만약 토큰의 수유자가 없다면 에러를 발생시킨다.
_owners에는 토큰의 소유자에 대한 주소값들이 저장되어 있다.🔨 setApprovalForAll(address operator, bool _approved)
앞서 말했듯이 ApprovalForAll 이벤트를 실행시키는 함수이다.
관리자만이 함수를 실행 가능하며 오퍼레이터의 모든 자산을 관리할 수 있는 권한을 부여하거나 없애는 역할이다.
function setApprovalForAll(address operator, bool approved) public virtual override
{
_setApprovalForAll(_msgSender(), operator, approved);
}
function _setApprovalForAll(address owner, address operator, bool approved) internal virtual {
require(owner != operator, "ERC721: approve to caller");
_operatorApprovals[owner][operator] = approved;
emit ApprovalForAll(owner, operator, approved);
}
기본적인 setApprovalForAll함수는 실행 가능하다.
하지만 그다음 바로 실행되는 _setApprovalForAll함수는 조건이 적혀 있기 떄문에 제약에 걸리게 된다.
이 함수는 관리자가 다른 주소에 권한을 주는 함수이며 검증후에 다음 코드로 넘어가게 된다.
🔨 getApproved(uint256 tokenId)
function getApproved(uint256 tokenId) public view virtual override returns (address) {
require(_exists(tokenId), "ERC721: approved query for nonexistent token");
return _tokenApprovals[tokenId];
}
변수로 주어지는 tokenId에 매칭된 주소를 반환하는 함수이다.
_tokenApprovals에는 승인된 주소들 목록이 담겨 있다.그러기 떄문에 승인된 주소들을 확인하는 함수라고 할 수가 있다.
🔨 isApprovedForAll(address owner, address operator)
function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) {
return _operatorApprovals[owner][operator];
}
오퍼레이터가 관리자의 모든 자산을 관리 할 수 있는 권한을 가지고 있는지 확인하는 함수이다.
ERC165는 ERC-721 스마트 컨트랙트를 생성할 떄 반드시 구현해야 하는 인터페이스를 검사하고, 언제 사용하는지 감지하는 역할이다.
함수 supportsInterface(bytes4 interfaceID) 만을 가지고 있으며 변수로 ERC-721의 인터페이스 ID를 입력해야만 ERC-721스마트 컨트랙트가 정상적으로 작동하게 된다.
pragma solidity ^0.4.20;
interface ERC165 {
function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
다음은 ERC165 인터페이스 이다.
ERC-721에 작성되어 있는 인터페이스 id는 0x80ac58cd이다.
이 id는 기본적인 인터페이스를 몇번의 암호화 과정을 거쳐 축약되는 것이다.
쉽게 말해 keccak256을 통해서 모든 함수를 각각 암호화 하고 그 값을 축약한 id값이다.
자산의 컨트랙트에서 safeTransfer를 지원하려는 모든 컨트랙트에 대한 인터페이스이다.
🔨 onERC721Received(address operator, address from, uint256 tokenId, bytes data)
NFT의 수신을 처리하는 이 컨트랙트는 safeTransfer후, 이 함수를 호출하게 된다.
function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory _data) private returns (bool) {
if (to.isContract()) {
try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, _data) returns (bytes4 retval) {
return retval == IERC721Receiver.onERC721Received.selector;
} catch (bytes memory reason) {
if (reason.length == 0) {
revert("ERC721: transfer to non ERC721Receiver implementer");
} else {
assembly {
revert(add(32, reason), mload(reason))
}
}
}
} else {
return true;
}
}
function onERC721Received(address operator, address from, uint256 tokenId, bytes memory data) public returns (bytes4);
인터페이스 이다.
🔨 tokenURI(uint256 tokenId)
이 함수는 tokenId를 입력받아 URI를 리턴한다.
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
string memory baseURI = _baseURI();
return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
}
일단 tokenId가 있는지를 검사를 하고 함수 _baseURI를 실행하여 baseURI가 있는지를 검사한다.
있다면 tokenId를 합친 문자열을 리턴, 없다면 빈 문자열을 리턴한다.
🔨 baseURI()
기본 URI를 반환한다.
_setBaseURI으로 설정한 URI를 반환한다.인터페이스 이다.
🔨 tokenByIndex(uint256 index)
컨트랙트에 의해 저장된 모든 토큰의 index를 기반으로 tokenId를 리턴한다.
totalSupply와 함께 사용하여 모든 토큰을 열거할 수 있다.🔨 tokenOfOwnerByIndex(address owner, uint256 index)
토큰 목록의 인덱스를 기반으로 관리자가 소유한 토큰 ID를 리턴한다.
balanceOf와 함께 사용하여 관리자의 모든 토큰을 열거할 수 있다.