ERC-4907 NFT 대여 확장(사용자·만료) (Rental NFT, an Extension of EIP-721)

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

NFT의 소유자와 별도로 '사용자(user)' 역할과 만료 시각(expires)을 둡니다. 소유권을 넘기지 않고 정해진 기간만 NFT를 빌려 쓰게 할 수 있습니다.

왜 필요한가

가상 토지로 장면을 짓거나 게임 아이템을 게임 안에서 쓰는 것처럼, 쓰임새가 있는 NFT가 있습니다. 이런 NFT는 소유자와 실제로 쓰는 사람이 다를 수 있습니다. 소유자가 다른 사람에게 빌려주는 경우가 대표적입니다. 그런데 빌려 쓰는 사람이 소유자와 같은 권한을 가지면 안 됩니다. 보통 빌린 사람은 NFT를 팔 수 없어야 합니다. 기존 NFT 표준(EIP-721)에는 소유자와 사용자를 구분하는 역할이 없어서 프로젝트마다 'operator'나 'controller' 같은 이름으로 따로 만들어 썼습니다. 이름과 동작이 제각각이라 앱끼리 맞춰 쓰기 어렵습니다. 또 대여처럼 기간이 정해진 사용은 끝나는 시점에 권한을 거둬야 합니다. 만료 기능이 없으면 소유자가 시작할 때 사용자를 등록하는 거래와 끝날 때 거두는 거래, 두 번을 체인에 보내야 합니다. 손도 많이 가고 가스비도 두 배로 듭니다.

구현해야 하는 것

심층 설명

소유자와 사용자, 두 역할의 구분

이 표준은 EIP-721에 user 역할과 expires 값을 더하는 확장입니다. owner는 기존 EIP-721의 소유자이고 user는 NFT를 '쓸' 권한만 가진 주소입니다. user는 NFT를 전송하거나 다른 user를 정할 수 없습니다. 원문은 이 구분으로 빌려주는 쪽과 빌리는 쪽이 할 수 있는 일, 즉 각자의 권리를 관리하기 쉬워진다고 설명합니다. 소유자는 user를 정할 수 있고 다른 프로젝트는 owner와 user 각각에 자기 서비스의 권한을 붙일 수 있습니다. 기존 EIP-721 함수는 그대로 두고 함수 묶음만 추가하므로 EIP-721과 완전히 호환됩니다.

setUser: 사용자와 만료 시각 설정

setUser(uint256 tokenId, address user, uint64 expires)는 NFT의 user와 expires를 한 번에 정합니다. user에 0 주소를 넣으면 사용자가 없는 상태가 됩니다. tokenId가 유효한 NFT가 아니면 실패(throw)해야 합니다. expires는 UNIX 타임스탬프이고 새 user는 이 시각 전까지 NFT를 쓸 수 있습니다. 함수는 public이나 external 중 어느 쪽으로 구현해도 됩니다(MAY). 누가 호출할 수 있는지는 인터페이스에 명시되어 있지 않고 참조 구현은 _isApprovedOrOwner(msg.sender, tokenId)로 소유자나 승인받은 주소만 허용합니다. 참조 구현은 _users[tokenId]에 UserInfo{user, expires}를 저장한 뒤 UpdateUser를 내보냅니다.

userOf와 userExpires: 조회 규칙

userOf(uint256 tokenId)는 NFT의 사용자 주소를 돌려줍니다. 사용자가 없거나 만료됐으면 0 주소를 돌려줍니다. 호출자가 만료 여부를 따로 계산하지 않아도 userOf 결과만 보면 됩니다. userExpires(uint256 tokenId)는 만료 시각을 돌려줍니다. 0이면 사용자가 없습니다. 두 함수 모두 pure나 view로 구현할 수 있습니다(MAY). 참조 구현의 userOf는 uint256(_users[tokenId].expires) >= block.timestamp이면 저장된 user를, 아니면 address(0)을 돌려줍니다. 만료 시각과 현재 블록 시각이 같으면 아직 유효한 것으로 봅니다. 참조 구현의 userExpires는 저장된 expires를 그대로 돌려주므로, 만료가 지난 뒤에도 0이 아닌 값이 나올 수 있습니다.

UpdateUser 이벤트

event UpdateUser(uint256 indexed tokenId, address indexed user, uint64 expires)는 user나 expires가 바뀔 때마다 반드시(MUST) 내보내야 합니다. user가 0 주소이면 사용자가 없다는 뜻입니다. tokenId와 user에 indexed가 붙어 있어 특정 NFT나 특정 사용자 기준으로 로그를 걸러 볼 수 있습니다. 참조 구현은 setUser가 호출될 때와, NFT가 다른 주소로 전송되어 사용자 정보가 지워질 때 이 이벤트를 내보냅니다.

ERC-165 인터페이스 식별자

supportsInterface를 0xad092b5c로 호출하면 반드시(MUST) true를 돌려줘야 합니다. 이 값이 IERC4907 인터페이스의 식별자입니다. 대여 플랫폼 같은 외부 컨트랙트는 이 호출로 해당 NFT가 user와 expires를 지원하는지 먼저 확인할 수 있습니다. 참조 구현은 interfaceId == type(IERC4907).interfaceId || super.supportsInterface(interfaceId)로 EIP-721 쪽 식별자도 함께 지원합니다. 이 표준은 EIP-165와 EIP-721을 전제로 합니다(requires: 165, 721).

참조 구현의 전송 처리와 테스트

참조 구현 ERC4907.sol은 OpenZeppelin v4 ERC721을 상속하고 _beforeTokenTransfer를 재정의합니다. from != to이고 _users[tokenId].user가 0 주소가 아니면 delete _users[tokenId]로 사용자 정보를 지우고 UpdateUser(tokenId, address(0), 0)을 내보냅니다. 즉 NFT가 다른 주소로 넘어가면 기존 대여 관계는 사라집니다. 이 동작은 인터페이스 명세가 아니라 참조 구현에 있으므로, 다른 구현이 반드시 같은 방식일 필요는 없습니다. 테스트 코드는 Alice에게 토큰 1을 민팅한 뒤 Bob을 현재 시각 + 1000초 만료로 setUser하고 userOf(1)이 Bob, ownerOf(1)이 Alice인지 확인합니다.

허락 없는 제3자 연동

원문은 이 표준이 허가 없는 상호운용성을 목표로 한다고 설명합니다. NFT 프로젝트가 user 역할과 expires를 채택하면, 다른 프로젝트는 NFT 발행자나 원래 앱의 허락 없이 이 기능에 바로 접근해 자기 방식의 거래를 만들 수 있습니다. 원문이 드는 예시는 같은 PFP NFT를 30일 동안 빌려주는 대여 플랫폼과, 할부로 값을 치르면서 NFT를 쓰다가 결국 소유권을 얻는 담보 플랫폼에 동시에 연결하는 경우입니다. 두 경우 모두 원래 PFP 프로젝트의 허락이 필요하지 않습니다. 또 새 함수가 EIP-721의 기존 함수와 비슷한 모양이라 개발자가 빨리 채택할 수 있다고 설명합니다.

쓸 때 주의할 점

ERC 원문 · ← ERCs