ERC-5516 소울바운드 다중 소유자 토큰 (Soulbound Multi-owner Tokens)
Final · NFT·멀티토큰 · 2022-08-19 작성
양도할 수 없는 증명용 토큰(소울바운드 토큰)을 한 번의 거래로 여러 계정에 동시에 발행하고 같은 컨트랙트 안에 여러 토큰 종류를 담을 수 있게 하는 표준입니다.
왜 필요한가
졸업장이나 참석 증명처럼 남에게 넘기면 안 되는 토큰을 소울바운드 토큰(SBT)이라고 부릅니다. 기존의 계정 귀속 토큰 표준에서는 토큰이 계정 하나에만 묶이기 때문에, 사용자가 계정 열쇠(키)를 잃어버리거나 키를 교체하면 토큰도 함께 잃게 됩니다. 또 같은 증명서를 여러 사람에게 주려면 받는 사람마다 발행 거래를 따로 보내야 해서 거래 비용이 늘어나고 토큰 종류마다 별도 컨트랙트를 배포하면 같은 코드가 블록체인에 중복으로 올라갑니다. 이 표준은 하나의 토큰을 여러 계정에 동시에 묶고 한 번의 거래로 여러 수령자에게 발행하며 여러 토큰 종류를 한 컨트랙트에 담을 수 있게 해서 이런 문제를 풉니다. 받은 사람이 원하지 않는 토큰은 스스로 포기할 수도 있습니다.
구현해야 하는 것
event Renounced(uint256 indexed tokenId, address indexed who);: 보유자가 자신의 소울바운드 토큰을 자발적으로 포기할 때 발생합니다.function renounce(uint256 tokenId) external;: 보유자가 자신의 토큰을 자발적으로 포기합니다. 포기는 최종적이어서 같은 tokenId를 같은 주소로 다시 발행할 수 없습니다.function has(address who, uint256 tokenId) external view returns (bool);: 주어진 주소가 해당 tokenId의 토큰을 보유하고 있는지 확인합니다.function issuerOf(uint256 tokenId) external view returns (address);: 해당 토큰 ID의 원래 발행자 주소를 반환합니다.function uri(uint256 tokenId) external view returns (string memory);: 토큰 메타데이터의 URI를 반환합니다. 이 값은 처음 설정된 뒤 바꿀 수 없습니다.
심층 설명
인터페이스와 ERC-165
이 표준을 구현하는 컨트랙트는 ERC-5516 인터페이스의 모든 함수를 구현해야 하고(MUST), ERC-165의 supportsInterface에 interfaceID로 0x85a5f87c가 들어오면 true를 반환해야 합니다. 인터페이스는 issue, renounce, has, hasRenounced, issuerOf, uri, deriveTokenId 일곱 함수와 Issued, Renounced 두 이벤트로 구성됩니다. ERC-1155의 멀티 토큰 구조에서 영감을 받았지만 부분 호환을 버리고 증명 발행이라는 목적에 맞는 최소 인터페이스로 설계했습니다.
토큰 ID 유도 규칙
tokenId는 (issuer, metadataURI) 쌍에서 결정적으로 유도되어야 하고 유도 함수는 이 쌍에 대해 충돌 저항성이 있어야 합니다. 권장 구현은 uint256(keccak256(abi.encodePacked(issuer, metadataURI)))입니다. issue(recipients, metadataURI) 호출은 deriveTokenId(msg.sender, metadataURI)와 같은 값을 반환해야 하며 deriveTokenId는 인자와 불변 배포 파라미터만의 함수여야 하고 가변 컨트랙트 상태에 의존하면 안 됩니다. tokenId에 호출자 주소가 섞이기 때문에, 대기 중인 발행 거래를 멤풀에서 본 공격자가 같은 tokenId를 선점할 수 없습니다. 공격자가 보내는 복사 거래는 자기 주소 기준의 다른 식별자가 나오기 때문입니다.
재발행과 원자성
같은 msg.sender가 같은 metadataURI로 issue를 다시 호출하면 같은 tokenId로 귀결되고 새 토큰을 만드는 대신 그 tokenId의 보유자 집합을 넓혀야 합니다. 기존 tokenId를 원래 발행자가 아닌 주소가 노리는 issue 호출은 거부해야 합니다. 요구 조건으로 recipients는 비어 있으면 안 되고 0 주소가 없어야 하며 이미 그 tokenId를 보유했거나 과거에 포기한 주소가 하나라도 있으면 전체 호출이 되돌려집니다. 부분 성공을 허용하면 Issued 이벤트의 recipients를 실제로 자격이 부여된 주소 집합으로 믿을 수 없게 되기 때문입니다. 따라서 큰 집단에 재발행하는 발행자는 이미 받았거나 포기한 주소를 체인 밖에서 걸러낸 뒤 호출해야 합니다.
포기(renounce)의 최종성
renounce(tokenId)가 성공하면 호출자의 보유가 지워지고 그 주소에 대해 영구적입니다. 이후 같은 tokenId로 그 주소를 recipients에 포함한 issue 호출은 모두 거부되어야 합니다. 발행자가 협조하거나 강요받더라도 다시 붙일 수 없게 해서 어떤 증명을 자기 계정에 남길지에 대한 보유자의 결정권을 지키는 의도된 설계입니다. 마음이 바뀐 포기자에게 증명을 복구하려면 발행자가 다른 metadataURI로 새 tokenId를 발행해야 합니다.
메타데이터와 URI 불변성
uri(tokenId)가 반환하는 값은 첫 발행 때 고정되며 재발행이나 다른 어떤 작업으로도 바꿀 수 없습니다. tokenId가 URI에서 유도되므로 체인 위에서 URI를 바꾸면 식별자와 메타데이터의 결합이 깨지기 때문입니다. URI 문자열에 {id}가 있으면 클라이언트는 이를 토큰 ID의 16진수 표현으로 치환해야 하는데, 0x 접두사 없는 소문자 [0-9a-f]로 쓰고 필요하면 앞을 0으로 채워 64자로 만듭니다. 구현은 고정 베이스 URI에 발행 시 받은 metadataURI를 이어 붙여 uri 값을 구성할 수 있으며 이 경우 tokenId를 다시 계산하는 검증자는 uri(tokenId) 반환값이 아니라 issue에 원래 넣은 metadataURI를 써야 합니다. 베이스 URI는 배포 후 불변이어야 하고 이를 수정하는 어떤 기능도 노출하면 안 됩니다.
이벤트 규칙과 인덱싱
issue 호출마다(재발행 포함) Issued 이벤트를 정확히 한 번 내야 하고 이벤트의 recipients에는 그 호출에서 추가된 주소만 담아야 합니다. 누적 보유자 집합이 아닙니다. renounce도 호출마다 Renounced 이벤트를 정확히 한 번 냅니다. 이 규칙 덕분에 컨트랙트가 내는 이벤트 로그만으로 모든 현재 토큰 잔액의 정확한 기록을 만들 수 있고 데이터베이스나 탐색기가 이벤트를 수신해 컨트랙트 안의 모든 토큰을 색인하고 분류해 검색할 수 있습니다. 인덱서는 두 이벤트를 모두 처리해야 하며 가장 최근 Issued 이벤트를 그 tokenId의 전체 보유자 집합으로 가정하면 안 됩니다.
발행자 회수 기능의 의도적 제외
기본 인터페이스에는 발행자 측 회수(revocation)가 없습니다. 일단 발행되면 (주소, tokenId) 결합을 지우는 유일한 작업은 보유자의 자발적 renounce입니다. 회수 정책은 만료, 대체, 부정행위 등 용도마다 크게 달라서 한 가지 모델을 기본 인터페이스에 표준화하면 다른 모델이 필요한 구현을 제약하기 때문입니다. 회수가 필요한 발행자는 인터페이스를 자체 확장하거나, 대체하는 새 tokenId를 발행하고 검증자가 그 발행자의 최신 증명만 인정하게 만드는 방식으로 모델링할 수 있습니다. 회수를 뺀 덕분에 발행자 키가 나중에 유출되더라도 보유자 외에는 누구도 결합을 지울 수 없다는 보유자 측 보장이 강해집니다.
다중 소유와 키 분실 대응
양도 불가 토큰은 계정 키가 유출되거나 교체되면 보유자가 토큰과의 연결을 잃을 수 있습니다. 이 표준은 하나의 토큰을 여러 계정에 묶을 수 있게 해서 이 문제의 해법을 제시합니다. 멀티시그 기능을 구현한 컨트랙트 계정으로 발행하는 방법도 있는데, 이는 ERC-4973의 예외 처리에서 권장된 방식과 같습니다. 멀티 토큰 구조 덕분에 여러 토큰 종류가 한 컨트랙트에 저장되어 중복 바이트코드 배포를 막고 발행된 모든 토큰을 같은 컨트랙트 주소에서 접근할 수 있습니다. 이 표준은 Final 상태로 확정되었습니다.
쓸 때 주의할 점
- 발행자 사칭과 메타데이터 모방: 누구나 임의의 metadataURI로 issue를 호출할 수 있어 정당한 발행자의 메타데이터를 흉내 낼 수 있습니다. 다만 주소가 다르므로 tokenId가 달라지고 issuerOf는 사칭자 주소를 반환합니다. 검증자는 메타데이터 내용만으로 믿지 말고 deriveTokenId(기대 발행자, 기대 URI)로 tokenId를 계산한 뒤 issuerOf를 기대 발행자와 비교해야 합니다. 또한 컨트랙트 주소 자체를 검증 대상 증명과 함께 전달받으면 안 되고 알려진 배포 레지스트리나 서명된 발행자 성명 같은 별도 경로로 확보해야 합니다.
- 프런트러닝: tokenId 유도에 발행자 주소가 섞이므로, 대기 중인 issue 거래를 본 공격자가 그 tokenId를 선점할 수 없습니다. 다른 주소에서 보내면 다른 식별자가 나오기 때문입니다. 공격자가 같은 metadataURI로 자기 주소 아래 토큰을 만들 수는 있지만 이는 사칭 사례로 수렴하며 issuerOf 검증으로 걸러집니다.
- 포기 최종성의 범위: 포기는 (tokenId, 주소) 단위로만 최종적입니다. 발행자가 다른 metadataURI로 새 tokenId를 만들어 포기자를 수령자에 포함하는 것은 새 증명이므로 허용됩니다. 포기를 표시하는 UI가 같은 발행자의 새 증명까지 막는 것처럼 잘못 표현하면 안 됩니다.
- 발행자 키 유출: 발행자 키가 유출되면 공격자는 그 발행자가 원래 발행한 증명들을 재발행으로 확장할 수 있습니다. 다만 다른 주소가 발행한 토큰의 issuerOf를 바꾸거나 포기된 증명을 다시 붙일 수는 없습니다. 학위, 자격, 규제 관련처럼 중요한 증명을 다루는 발행자는 단일 외부 소유 계정 대신 멀티시그나 스마트 컨트랙트 지갑을 발행 주체로 쓰는 것이 권장됩니다.
- 보유자 키 분실: 다중 소유가 보유자 키 분실에 대한 이 표준의 1차 완화책입니다. 같은 사람이 관리하는 여러 주소나 키 교체를 지원하는 스마트 컨트랙트 지갑에 증명을 발행할 수 있습니다. has(who, tokenId)로 보유를 확인하는 검증자는 단일 주소가 그 증명에 대한 보유자 정체성의 전부라고 가정하면 안 됩니다.