ERC-4910 로열티 내장 NFT (Royalty Bearing NFTs)

Final · NFT·멀티토큰 · 2022-03-14 작성

NFT 표준(ERC-721)을 확장해 로열티의 정의·분배·지급을 모두 온체인에서 처리합니다. 원작과 파생작을 잇는 계층형 로열티 구조를 만들어 중앙 기관 없이 창작자에게 수익이 가게 합니다.

왜 필요한가

로열티 관리는 복잡한 계약과 불투명한 운영, 속임수와 사기가 많았던 오래된 문제입니다. 특히 원화에서 만든 판화, 다른 곡을 샘플링한 노래처럼 원작에서 파생작이 나오는 계층형 로열티에서는 문제가 더 심합니다. 원작자는 파생작이 팔리고 되팔릴 때마다 몫을 받아야 하지만 이를 보장할 장치가 없으면 중간 사업자가 지급을 조작하거나 아예 건너뛸 수 있습니다. 기존 NFT 거래에서도 구매자와 판매자가 컨트랙트 밖에서 돈을 주고받으면 로열티를 피해 갈 수 있습니다. 이 표준은 NFT 컨트랙트 자체가 판매와 결제에 직접 개입하게 만들어 법적으로 받을 권리가 있는 사람에게 로열티가 돌아가도록 설계합니다.

구현해야 하는 것

심층 설명

로열티 계정과 하위 계정 구조

각 NFT는 tokenId를 통해 로열티 계정(Royalty Account, RA) 하나와 연결되고 RA에는 하위 계정이 하나 이상 있어야 합니다(R1). RA 식별자 raAccountId는 유일해야 하고(R2), tokenId와 연결되어야 합니다(R3). 하위 계정은 개인 계정이거나 다른 RA일 수 있으며 개인 계정만 인출 가능한 잔액을 가질 수 있습니다. 하위 계정이 RA인 경우 그 몫은 다시 그 RA의 하위 계정들로 재배분되어야 하므로 최종 잔액은 0이어야 합니다. 부모와 자식 NFT 사이에는 직접 연결이 있어야 하고(R4), 지원 토큰 종류 목록(R5)과 토큰별 마지막 검증 잔액(R6), 판매 목록(R7, R8), 등록된 결제 목록(R9, R10)도 유지해야 합니다.

민팅 규칙과 컨트랙트 소유 구조

NFT를 민팅할 때 RA와 하위 계정이 함께 생성되어 NFT, 소유자, 그리고 조상이 있으면 조상의 RA에 연결되어야 합니다(R18). 특이하게도 새 NFT의 owner는 NFT 컨트랙트 자신이어야 하고(R19), 실제 소유자는 isApproved로 지정되어 owner처럼 행동합니다(R20). 컨트랙트가 결제와 로열티의 에스크로 역할을 하기 위한 설계입니다. mint 입력에는 parent, canBeParent, maxChildren, royaltySplitForItsChildren, uri가 담기며(R21), 발행 수가 0이 아닐 것, msg.sender가 MINTER_ROLE 또는 CREATOR_Role일 것, 자식을 가질 수 없는 NFT는 royaltySplitForItsChildren이 0일 것, 부모의 세대 수준이 허용 최대 세대 수 미만일 것 등의 규칙이 적용됩니다(R23).

두 가지 판매 방식과 결제 처리

판매는 직접 판매(trxnType = 0)와 거래소 중개 판매(trxnType = 1)로 나뉩니다. 직접 판매는 listNFT와 removeNFTListing으로 컨트랙트를 통해 등록·해제해야 합니다(R24). 구매자는 항상 판매자가 아닌 NFT 컨트랙트에 지불합니다. ERC-20이면 approve 후 executePayment를 호출하고(R31), ETH 같은 프로토콜 토큰이면 msg.data에 seller, tokenId 배열, receiver, trxnType을 인코딩해 payable fallback 함수로 보냅니다(R39~R41). receive()는 msg.data를 받을 수 없어 허용되지 않습니다. 직접 판매는 결제 검증 후 내부 _safeTransferFrom으로 즉시 전송하고 리스팅을 제거하며(R37, R38), 중개 판매는 결제를 registeredPayment에 등록해 두었다가 별도의 safeTransferFrom에서 검증합니다(R36). 완결 전에는 reversePayment로 구매자가 결제를 되돌릴 수 있습니다(R44, R45).

전송 함수 제한과 _data 검증

로열티 우회를 막기 위해 전송 함수는 하나만 남깁니다. transferFrom과 data 없는 safeTransferFrom은 비활성화해야 합니다(R46). 유일한 safeTransferFrom은 ERC-721 인터페이스를 그대로 쓰되, 여러 토큰 전송 정보를 _data에 인코딩합니다. _data에는 판매자·구매자·수령인 주소, tokenId 배열, 결제 토큰 종류, 결제 금액, 등록된 결제 식별자, block.chainid가 최소한 담겨야 하고(R48), seller == from, 수령인이 판매자가 아닐 것, 각 토큰이 등록된 결제의 토큰과 일치할 것 등의 규칙을 통과해야 합니다(R49). 로열티가 모두 배분된 뒤에야 전송이 일어나고 전송은 _approve(to, tokenId[i])로 승인 주소를 바꾸는 방식이며 기술적 owner는 계속 컨트랙트입니다(R53). 승인 갱신 후 등록된 결제를 제거합니다(R54).

계층 분해 방식의 로열티 분배

distributePayment는 깊이 N인 로열티 트리의 재귀 문제를 층별 N개의 문제로 나눠 가장 아래층에서 뿌리 조상까지 거슬러 올라가며 처리합니다. 결제 금액은 전송에 포함된 모든 NFT에 균등하게 나뉩니다. 각 하위 계정에 대해 isIndividual이 true면 로열티 비율을 적용한 금액을 그 계정의 royaltybalance에 더하고 assetId, accountId, tokenType, royaltybalance를 담은 이벤트를 발생시킵니다. false면 금액을 임시 변수에 담아 두고 ancestor를 따라 다음 RA로 넘어가 같은 규칙을 반복하며 조상이 없고 모든 하위 계정이 개인인 RA에 도달하면 끝납니다(R51). 비율 계산은 100%를 10000 같은 정수로 표현해 곱한 뒤 나누므로 오버런·언더런을 주의해야 합니다.

로열티 인출과 전송 중 정산

인출 함수는 공개 버전과 내부 버전 두 가지입니다. 공개 royaltyPayOut은 msg.sender == RAsubaccount, 토큰이 존재하고 소각되지 않았을 것, 해당 하위 계정의 isIndividual == true, amount가 royaltybalance 이하일 것을 요구하며(R56), 자금이 나가므로 nonReentrant 보호가 붙습니다. 내부 _royaltyPayOut은 지급을 보내고 성공한 경우에만 잔액을 차감합니다(R58, R59). 전송 과정에서는 먼저 from 주소(승인된 실소유자)의 하위 계정 잔액 전부를 from에게 지급하고 royaltybalance가 0이 된 뒤에야 하위 계정 소유자를 to로 갱신합니다(R52).

계정 갱신·삭제의 제약

updateRoyaltyAccount에서는 자산 식별자, 조상, 결제 토큰 종류, 하위 계정 잔액, 부모에게서 상속된 분배 비율을 바꿀 수 없습니다. 모든 분배 비율의 합은 항상 1이어야 하고 msg.sender는 수정 대상 RA의 하위 계정 소유자여야 하며 자기 비율은 줄이는 것만 가능합니다. 줄인 만큼 기존 계정의 비율을 올리거나 잔액 0인 새 하위 계정을 추가해야 합니다(R15). deleteRoyaltyAccount는 가스 비용이 매우 큰 작업으로, 해당 tokenId가 소각되었고 혈통상 연결된 조상·후손 토큰이 모두 소각되었으며 하위 계정 잔액이 전부 0일 때만 허용됩니다(R17). 규칙을 지키지 않으면 트리 일부가 무너져 자금이 잠길 수 있습니다.

쓸 때 주의할 점

ERC 원문 · ← ERCs