ERC-5496 NFT 다중 특권 관리 확장 (Multi-privilege Management NFT Extension)
Last Call · NFT·멀티토큰 · 2022-07-30 작성
NFT 하나에 투표권, 쿠폰, 에어드롭 청구권 같은 여러 특권을 붙이고 NFT 소유권은 그대로 둔 채 특권만 따로 넘기거나 복제해 공유할 수 있게 하는 EIP-721 확장입니다.
왜 필요한가
EIP-721은 NFT가 누구 소유인지와 소유권 이전만 기록합니다. NFT에 딸린 할인 쿠폰, 투표권, 에어드롭 청구권 같은 혜택은 체인에 기록되지 않습니다. 그래서 혜택을 주려면 별도 토큰을 새로 발행해야 하는데, 이 토큰은 원래 NFT와 묶여 있지 않습니다. 예를 들어 항공사가 크립토펑크 보유자를 자기 클럽으로 끌어들이려고 혜택용 토큰을 나눠 준 뒤 원래 NFT가 다른 사람에게 넘어가면, 혜택은 이전 보유자 손에 남고 새 보유자는 자동으로 누리지 못합니다. 반대로 NFT 보유자가 혜택을 쓸 생각이 없어 남에게 넘기고 싶어도, 소유권과 혜택이 분리되지 않아 NFT 자체를 넘기는 수밖에 없습니다. 이 표준은 특권을 NFT에 묶어 두면서도 보유자가 각 특권을 독립적으로 관리하고 양도할 수 있는 인터페이스를 제안합니다.
구현해야 하는 것
event PrivilegeAssigned(uint256 tokenId, uint256 privilegeId, address user, uint256 expires);: NFT 소유자가 특권 보유자를 바꿀 때 발생하는 이벤트입니다. setPrivilege가 호출되면 반드시 발생해야 합니다.event PrivilegeTotalChanged(uint256 newTotal, uint256 oldTotal);: 컨트랙트 소유자가 컬렉션의 전체 특권 수를 바꿀 때 발생하는 이벤트입니다.function setPrivilege(uint256 tokenId, uint256 privilegeId, address user, uint256 expires) external;: NFT의 특정 특권 보유자를 user로 지정하고 보유 기한을 expires로 정합니다. 호출자가 해당 토큰의 소유자나 승인받은 주소가 아니면 실패합니다.function privilegeExpires(uint256 tokenId, uint256 privilegeId) external view returns(uint256);: 해당 NFT의 특정 특권이 언제 만료되는지 타임스탬프를 돌려줍니다.function hasPrivilege(uint256 tokenId, uint256 privilegeId, address user) external view returns(bool);: user가 해당 NFT의 특정 특권을 갖고 있는지 여부를 돌려줍니다.event PrivilegeCloned(uint tokenId, uint privId, address from, address to);: 특권이 복제될 때 발생하는 이벤트입니다. clonePrivilege가 호출되면 반드시 발생해야 합니다.function clonePrivilege(uint tokenId, uint privId, address referrer) external returns (bool);: 공유 가능으로 설정된 특권을 복제합니다. referrer에 추천인 주소를 적고 성공 여부를 돌려줍니다.
심층 설명
필수 인터페이스 IERC5496
이 표준을 따르는 컨트랙트는 IERC5496을 반드시 구현해야 하며 EIP-721 컨트랙트 입장에서 이 확장 자체는 선택 사항입니다. setPrivilege는 tokenId의 privilegeId 특권 보유자를 user로 지정하고 expires까지 유효하게 합니다. msg.sender가 해당 토큰의 소유자나 승인받은 주소가 아니면 실패해야 하고 dev 주석은 expires가 30일보다 짧아야 한다고 적습니다. privilegeExpires는 만료 타임스탬프를, hasPrivilege는 보유 여부를 돌려줍니다. setPrivilege가 호출되면 PrivilegeAssigned 이벤트가 반드시 발생해야 하고 supportsInterface는 0x076e1bbb에 대해 true를 돌려줘야 합니다. 만료 시점은 uint256 타입의 타임스탬프로 저장합니다.
특권 수 상한과 PrivilegeTotalChanged
구현 컨트랙트는 특권을 설정하기 전에 최대 특권 수를 정해 두는 것이 권장되며 privilegeId는 이 최대값을 넘으면 안 됩니다. 컬렉션의 전체 특권 수가 바뀔 때는 PrivilegeTotalChanged 이벤트가 반드시 발생해야 합니다. 참조 구현에서는 privilegeTotal 변수와 내부 함수 _setPrivilegeTotal로 이를 관리하고 setPrivilege에서 privId < privilegeTotal을 검사합니다.
공유 가능한 특권과 복제 확장 IERC721Cloneable
특권이 공유 불가능하면 특권 보유자 수가 NFT 수에 묶입니다. 공유 가능한 특권은 원 보유자가 자기 특권을 넘기는 것이 아니라 복사해서 남에게 주는 방식이라, 특권과 NFT의 확산을 크게 넓힙니다. 이를 위한 선택적 확장 IERC721Cloneable은 clonePrivilege(tokenId, privId, referrer) 함수를 정의하며 호출되면 PrivilegeCloned 이벤트가 반드시 발생해야 합니다. 또한 규격을 따르는 컨트랙트는 서명 검증에 EIP-1271을 쓰는 것이 권장됩니다.
추천인 설계
clonePrivilege의 referrer 인자는 특권이 퍼진 경로를 기록합니다. 원문 예시로, 피자 가게가 30% 할인 쿠폰을 내걸고 손님이 친구와 쿠폰을 나누도록 권장하는 경우, 톰이 가게에서 쿠폰을 받아 앨리스에게 공유하면 앨리스의 추천인은 톰이 됩니다. 어떤 경우에는 톰이 가게에서 추가 보상을 받을 수 있어 상인이 소비자 사이에서 판촉을 퍼뜨리는 데 도움이 됩니다.
NFT 양도와 unwrap 보호
NFT 소유권이 다른 사용자에게 넘어가도 특권에는 영향이 없습니다. 다만 랩(wrap)된 NFT에서 원래 EIP-721 토큰을 unwrap()으로 꺼내려 할 때 아직 유효한 특권이 남아 있으면 오류가 날 수 있습니다. 그래서 원문 제안 코드는 특권 보유자의 권리를 지키기 위해 특권의 마지막 만료 시점을 검사합니다. unwrap은 현재 블록 타임스탬프가 privilegeBook[tokenId].lastExpiresAt 이상이어야 하고 호출자가 소유자여야 합니다. 통과하면 토큰을 소각하고 원래 NFT를 돌려준 뒤 Unwrap 이벤트를 냅니다.
참조 구현의 동작
참조 구현의 hasPrivilege는 특권의 expiresAt이 현재 시각 이후면 기록된 보유자인지 확인하고 만료됐으면 NFT 소유자를 특권 보유자로 봅니다. setPrivilege는 소유자나 승인받은 주소, 또는 특권 보유자 본인이나 그가 지정한 위임자(delegator)만 호출할 수 있고 expires는 block.timestamp + 30 days보다 작아야 합니다. 소유자 측 호출일 때만 expiresAt을 갱신하고 lastExpiresAt도 함께 올립니다. setDelegator로 위임자를 켜고 끌 수 있습니다. 이 확장은 EIP-721에 함수와 자료구조를 더할 뿐 원래 표준을 건드리지 않아 EIP-721을 따르는 어떤 NFT와도 호환됩니다.
쓸 때 주의할 점
- 특권 설정·복제 권한 관리: 누가 특권을 설정하거나 복제할 권한을 갖는지 구현에서 철저히 따져야 합니다.