ERC-721 대체 불가능 토큰(NFT) 표준 (Non-Fungible Token Standard)

Final · NFT·멀티토큰 · 2018-01-24 작성

하나하나 구별되는 자산을 스마트 컨트랙트 안에서 소유자별로 추적하고 옮기는 공통 방식을 정합니다. 지갑과 중개·경매 앱이 어떤 NFT든 같은 함수로 다룰 수 있게 합니다.

왜 필요한가

집, 그림, 게임 아이템처럼 세상의 많은 자산은 하나하나가 다릅니다. 기존 ERC-20 토큰 표준은 모든 토큰이 똑같다는 전제로 수량만 세기 때문에 이런 자산의 소유권을 하나씩 따로 기록할 수 없습니다. 그래서 프로젝트마다 자기만의 방식으로 컨트랙트를 만들었습니다. 원문은 그 예로 CryptoPunks를 드는데, 경매 기능이 컨트랙트 안에 들어 있고 함수 이름에 punks가 박혀 있어 일반화하기 어렵습니다. 이렇게 되면 지갑이나 중개·경매 앱은 컨트랙트마다 따로 연동해야 하고 사용자는 자산을 어디로 보내야 안전한지 알기 어렵습니다. 이 표준은 구별되는 자산을 세고, 소유자를 찾고, 옮기고, 남에게 관리 권한을 주는 함수와 이벤트 이름을 통일해 어떤 앱이든 어떤 NFT와도 바로 맞물리게 합니다.

구현해야 하는 것

심층 설명

필수 인터페이스와 ERC-165 식별자

ERC-721을 따르는 컨트랙트는 ERC721 인터페이스와 ERC165 인터페이스를 모두 구현해야 합니다. ERC721 인터페이스의 ERC-165 식별자는 0x80ac58cd입니다. ERC165의 supportsInterface(bytes4 interfaceID)는 해당 인터페이스를 구현하고 interfaceID가 0xffffffff가 아니면 true, 아니면 false를 돌려주며 30,000 가스 미만으로 실행됩니다. 선택 확장도 각각 식별자가 있습니다. 수신 인터페이스 ERC721TokenReceiver는 0x150b7a02, 메타데이터 확장 ERC721Metadata는 0x5b5e139f, 열거 확장 ERC721Enumerable은 0x780e9d63입니다. 원문은 컨트랙트가 지원하는 인터페이스를 밖에서 알 수 있게 하려고 ERC-165를 택했다고 설명하고 앞으로 전역 인터페이스 등록소가 생기면 확장 구현을 별도 컨트랙트에 위임할 수 있을 것이라고 적습니다.

토큰 식별자는 uint256 블랙박스

모든 NFT는 컨트랙트 안에서 고유한 uint256 ID로 식별되며 이 번호는 컨트랙트가 존재하는 동안 바뀌어서는 안 됩니다. (컨트랙트 주소, uint256 tokenId) 쌍이 이더리움 체인 위에서 특정 자산을 가리키는 전역 고유 식별자가 됩니다. 어떤 컨트랙트는 0부터 1씩 올리며 ID를 붙이는 편이 편하겠지만 호출자는 ID에 특정 규칙이 있다고 가정해서는 안 되고 블랙박스로 다뤄야 합니다. NFT는 파괴되어 무효가 될 수도 있습니다. uint256을 고른 이유는 UUID와 sha3 해시를 그대로 uint256으로 바꿀 수 있어 다양한 응용에 맞기 때문입니다. 0 주소에 배정된 NFT는 무효로 간주되며 balanceOf에 0 주소를 넣거나 무효 토큰에 ownerOf를 물으면 throw합니다.

전송 함수 세 가지와 throw 조건

전송 함수는 bytes data가 있는 safeTransferFrom, 없는 safeTransferFrom(data를 빈 값으로 둔 것과 동일), 그리고 안전 확인이 없는 transferFrom 세 가지입니다. 세 함수 모두 msg.sender가 현재 소유자·승인 주소·인가된 운영자가 아니면, _from이 현재 소유자가 아니면, _to가 0 주소이면, _tokenId가 유효한 NFT가 아니면 throw해야 합니다. safeTransferFrom은 전송이 끝난 뒤 _to의 코드 크기가 0보다 크면 onERC721Received를 호출하고 반환값이 bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))가 아니면 throw합니다. transferFrom은 이 확인을 하지 않으므로 _to가 NFT를 받을 수 있는지 호출자가 직접 확인해야 하며 아니면 영구히 잃을 수 있습니다. 명세는 반드시 throw해야 하는 조건만 정하고 구현은 다른 상황에서도 throw할 수 있습니다. 원문은 그 예로 컨트랙트 일시 정지 중 전송 금지, 특정 주소의 수신 차단, 안전하지 않은 전송 금지, 양쪽에 수수료 부과, 읽기 전용 등록소를 듭니다. 발행(minting)과 소각(burning)은 명세에 없어 구현이 정합니다.

승인 체계: 토큰별 승인과 운영자

approve(_approved, _tokenId)는 NFT 하나의 승인 주소를 바꾸거나 다시 확인합니다. 0 주소는 승인 주소가 없다는 뜻이며 msg.sender가 현재 소유자이거나 소유자의 인가된 운영자가 아니면 throw합니다. setApprovalForAll(_operator, _approved)는 msg.sender의 모든 자산을 관리할 제3자(운영자)를 켜거나 끄며 ApprovalForAll 이벤트를 냅니다. 컨트랙트는 소유자 한 명당 여러 운영자를 허용해야 합니다. getApproved는 유효하지 않은 _tokenId에 throw하고 isApprovedForAll은 운영자 여부를 bool로 돌려줍니다. 인가된 운영자는 NFT의 승인 주소를 지정할 수도 있습니다. 전송이 일어나면 그 NFT의 승인 주소는 자동으로 없어집니다. ERC-20의 allowance 같은 수량 허용치는 없습니다. NFT는 수량이 0 아니면 1이므로, 허용치를 나중에 바꿀 때 생기던 문제(OpenZeppelin 이슈 #438) 없이 ERC-20 원래 설계의 이점만 취했다고 원문은 설명합니다.

세 이벤트와 발행·소각 책임

Transfer 이벤트는 어떤 방식으로든 NFT 소유권이 바뀔 때 발생합니다. 생성될 때는 _from이 0, 파괴될 때는 _to가 0입니다. 예외로 컨트랙트 생성 중에는 Transfer를 내지 않고 임의 개수의 NFT를 만들어 배정할 수 있습니다. Transfer가 발생하면 그 NFT의 승인 주소가 초기화됐다는 뜻이기도 합니다. Approval 이벤트는 NFT의 승인 주소가 바뀌거나 다시 확인될 때 발생하며 0 주소는 승인 없음을 뜻합니다. ApprovalForAll 이벤트는 운영자가 켜지거나 꺼질 때 발생합니다. 세 이벤트의 인자는 모두 indexed입니다. 발행과 소각 자체는 명세 밖이지만 NFT를 만들거나 없앨 때 위 이벤트 규칙은 지켜야 합니다.

수신 컨트랙트 인터페이스와 매직 값

안전 전송을 받으려는 지갑·중개·경매 컨트랙트는 ERC721TokenReceiver 인터페이스를 구현해야 합니다. NFT 컨트랙트는 전송 뒤 수신자의 onERC721Received(_operator, _from, _tokenId, _data)를 호출합니다. _operator는 safeTransferFrom을 호출한 주소, _from은 이전 소유자입니다. 이 함수는 throw로 전송을 거절할 수 있고 매직 값이 아닌 값을 돌려주면 거래가 되돌려져야 합니다. 메시지 발신자는 항상 NFT 컨트랙트 주소입니다. 단순 true 대신 매직 값을 쓰는 이유는 Solidity DelegateCallReturnValue 버그 때문입니다. 예전에 배포된 컨트랙트가 함수를 구현하지 않았는데도 특정 상황에서 1(true)을 돌려주는 버그입니다. 매직 값을 확인하면 진짜 긍정 응답과 빈 true를 구별할 수 있습니다. _operator 인자를 둔 이유는 운영자가 토큰을 보낼 때는 소유자를 대신하는 것이 아니라 자기 의사로 보내는 것이어서 수신자에게 운영자와 이전 소유자 둘 다 의미가 있기 때문입니다.

선택 확장 1: 메타데이터

ERC721Metadata는 선택 확장으로 name(), symbol(), tokenURI(uint256 _tokenId)를 둡니다. name은 컬렉션을 설명하는 이름, symbol은 줄임 이름이며 둘 다 빈 문자열도 유효한 응답입니다. tokenURI는 자산별 고유 URI(RFC 3986)를 돌려주고 무효한 _tokenId에는 throw합니다. URI는 ERC721 Metadata JSON Schema를 따르는 JSON 파일을 가리킬 수 있으며 그 스키마는 name(자산 식별), description(자산 설명), image(image/* 자원을 가리키는 URI) 세 속성을 정합니다. 이미지는 너비 320~1080픽셀, 가로세로 비율 1.91:1에서 4:5 사이를 권장하며 이 권장치는 Instagram에서 가져왔습니다. URI는 바뀔 수 있습니다. 집을 나타내는 NFT라면 사진이나 거주자 정보가 자연히 바뀔 수 있기 때문입니다. 메타데이터는 string으로 반환되므로 현재는 web3에서 호출하는 용도이지 다른 컨트랙트가 읽는 용도는 아닙니다. 어떤 컨트랙트든 같은 name과 symbol을 쓸 수 있으므로 어느 컨트랙트가 정식인지 판단하는 방법은 표준 범위 밖입니다.

선택 확장 2: 열거, 그리고 Caveats

ERC721Enumerable은 totalSupply(), tokenByIndex(_index), tokenOfOwnerByIndex(_owner, _index)를 둡니다. totalSupply는 0 주소가 아닌 소유자가 조회 가능한 유효 NFT 수입니다. tokenByIndex는 _index가 totalSupply() 이상이면, tokenOfOwnerByIndex는 _index가 balanceOf(_owner) 이상이거나 _owner가 0 주소이면 throw하며 정렬 순서는 정하지 않습니다. 원문은 규모가 커질 수 있는 구현에서 for/while 루프를 피하라고 권하고 2^128개를 추적하는 XXXXERC721을 테스트넷에 배포해 모든 함수가 ENS 조회보다 적은 가스로 동작함을 보였습니다. Solidity 0.4.20 문법 한계로 붙은 Caveats도 지켜야 합니다. 함수 가변성 보장은 payable, nonpayable, view, pure 순으로 강해지며 인터페이스와 같거나 더 강하게 구현해야 합니다. external로 표시된 함수는 public으로 구현해도 됩니다. ERC721Metadata나 ERC721Enumerable을 구현하면 ERC721도 구현해야 합니다. ERC-20 호환을 더 원하면 uint8(0)을 돌려주는 decimals를 둘 수 있지만 필수는 아닙니다. 열거를 막아 프라이버시를 지키려 해도 공격자가 모든 tokenId에 ownerOf를 호출하면 되므로 프라이버시는 얻을 수 없다고 원문은 적습니다.

ERC 원문 · ← ERCs