ERC-5773 맥락 의존 멀티에셋 토큰 (Context-Dependent Multi-Asset Tokens)
Final · NFT·멀티토큰 · 2022-10-10 작성
NFT 하나에 여러 에셋(파일·데이터)을 붙이고 전자책 리더·게임·마켓플레이스 등 여는 환경에 따라 알맞은 에셋을 보여 줄 수 있게 하는 표준입니다.
왜 필요한가
기존 ERC-721 NFT는 보통 메타데이터 주소(tokenURI) 하나만 가리키므로, 같은 토큰이라도 환경에 맞는 다른 표현을 담기 어렵습니다. 내용을 바꾸려면 발행자가 URI를 통째로 교체해야 하는데, 이렇게 하면 이전 기록의 추적이 어려워지고 소유자 동의 없이 내용이 바뀔 수 있어 신뢰 문제가 생깁니다. 메타데이터가 서버 한 곳이나 특정 IPFS 게이트웨이에만 올라가 있으면 그곳이 멈출 때 정보가 사라질 위험도 있습니다. 게임 아이템 NFT는 다른 게임과 틀이 달라 쓸 수 없고 게임 서버가 닫히면 소유자에게는 쓸모없는 토큰만 남습니다. 레벨업 같은 변화를 반영하려면 중앙에서 메타데이터를 바꾸거나 새 NFT를 지갑에 넣어 줘야 했습니다. 이 표준은 하나의 NFT에 여러 에셋을 담고 추가·교체는 발행자가 제안하고 소유자가 수락하는 방식으로 풀어 이런 문제를 해결합니다.
구현해야 하는 것
event AssetSet(uint64 assetId);: assetId 위치에 에셋 객체가 초기화되었음을 알립니다.event AssetRejected(uint256 indexed tokenId, uint64 indexed assetId);: 펜딩 에셋이 거절되어 펜딩 배열에서 제거되었음을 알립니다.event AssetPrioritySet(uint256 indexed tokenId);: 토큰의 에셋 우선순위 배열이 다시 정렬되었음을 알립니다.function rejectAllAssets(uint256 tokenId, uint256 maxRejections) external;: 펜딩 배열 전체를 비웁니다. maxRejections로 직전에 도착한 에셋까지 거절되는 일을 막습니다.function setPriority(uint256 tokenId, uint64[] calldata priorities) external;: 액티브 에셋들의 우선순위 배열을 새로 지정합니다. 값이 낮을수록 우선순위가 높습니다.function getActiveAssets(uint256 tokenId) external view returns (uint64[] memory);: 토큰의 액티브 에셋 ID 배열을 돌려줍니다.function getPendingAssets(uint256 tokenId) external view returns (uint64[] memory);: 토큰의 펜딩 에셋 ID 배열을 돌려줍니다.function getActiveAssetPriorities(uint256 tokenId) external view returns (uint64[] memory);: 액티브 에셋들의 우선순위 배열을 돌려줍니다.function getAssetReplacements(uint256 tokenId, uint64 newAssetId) external view returns (uint64);: 펜딩 에셋 newAssetId가 수락될 때 교체될 기존 에셋의 ID를 돌려줍니다.function getAssetMetadata(uint256 tokenId, uint64 assetId) external view returns (string memory);: 지정한 액티브 에셋의 메타데이터 URI를 돌려줍니다.function approveForAssets(address to, uint256 tokenId) external;: 특정 토큰의 에셋 관리 권한을 한 계정에 줍니다. 0x0 주소를 승인하면 기존 승인이 지워집니다.function getApprovedForAssets(uint256 tokenId) external view returns (address);: 해당 토큰의 에셋 관리 권한을 승인받은 계정 주소를 돌려줍니다.function setApprovalForAllForAssets(address operator, bool approved) external;: 호출자의 모든 토큰에 대해 에셋 관리 오퍼레이터를 지정하거나 해제합니다.function isApprovedForAllForAssets(address owner, address operator) external view returns (bool);: operator가 owner의 모든 토큰에 대한 에셋 관리 권한을 가졌는지 확인합니다.
심층 설명
액티브·펜딩 두 배열 구조
토큰마다 에셋은 두 배열로 관리됩니다. 액티브 배열은 소유자가 수락해 실제로 쓰이는 에셋 목록이고 펜딩 배열은 제안되었지만 아직 수락되지 않은 에셋 목록입니다. 액티브 에셋은 누구도 삭제할 수 없고 새 에셋으로 교체만 할 수 있습니다. 펜딩 배열은 스팸과 괴롭힘(griefing)을 막기 위해 128칸으로 제한하는 것이 권장(SHOULD)됩니다. 소유자는 액티브와 펜딩 양쪽에서 에셋을 내릴 수 있고(MAY), 펜딩 배열 전체를 비우는 비상 함수는 반드시(MUST) 포함되어야 합니다.
제안-수락(propose-commit) 패턴
이미 존재하는 토큰에 에셋을 추가할 때는 반드시(MUST) 제안-수락 패턴을 따릅니다. 에셋은 먼저 펜딩 배열에 들어가고 소유자가 acceptAsset으로 수락해야 액티브 배열로 옮겨집니다. 교체 대상이 지정된 제안이 수락되면 옛 에셋이 새 에셋으로 바뀌는데, 옛 에셋은 여전히 도달·검증 가능해 추적성이 유지됩니다. 발행자가 임의로 URI를 바꾸는 방식과 달리 소유자 동의 없이는 에셋이 바뀌지 않으므로 소유자가 더 신뢰할 수 있습니다. 레벨업이나 IoT 기기 펌웨어 갱신을 이 교체 방식으로 표현할 수 있습니다.
인덱스와 assetId의 이중 확인
acceptAsset과 rejectAsset은 에셋 ID 대신 배열 인덱스를 받습니다. ID로 찾으면 배열을 순회해야 해서 비용이 배열 크기에 비례하지만 인덱스를 쓰면 비용이 고정되기 때문입니다. 다만 그 사이 인덱스가 바뀌는 경합을 막기 위해 기대하는 assetId를 함께 받아 해당 위치의 에셋이 맞는지 검증합니다. 매핑으로 인덱스를 내부 추적하는 구현도 시도했지만 에셋 추가 비용이 25% 넘게, 수락과 거절 비용이 각각 4.6%, 7.1% 늘어나 채택하지 않았고 필요하면 훅을 이용해 확장으로 구현할 수 있습니다.
우선순위 배열
setPriority는 액티브 에셋마다 우선순위를 지정합니다. 우선순위 배열은 비순차적인 값의 목록이며 가장 낮은 값이 가장 높은 우선순위입니다. 값 0은 초기화되지 않음과 같은 특수한 경우입니다. priorities 배열의 길이는 액티브 에셋 배열의 길이와 같아야 하고 배열의 순서는 액티브 배열의 순서와 짝을 이룹니다. 호출자는 토큰 소유자이거나 에셋 관리 승인을 받은 계정이어야 하며 호출 시 AssetPrioritySet 이벤트가 발생합니다. 소비처는 이 우선순위를 참고해 어떤 에셋을 먼저 보여 줄지 정합니다.
전송 승인과 분리된 에셋 승인
approveForAssets는 ERC-721의 전송 승인과 별개인 에셋 관리 전용 승인입니다. 승인받은 계정이 에셋을 수락·거절하거나 우선순위를 정해도 승인이 지워지지 않고 토큰이 전송될 때 지워집니다. 한 번에 한 계정만 승인할 수 있어 0x0 주소를 승인하면 기존 승인이 지워집니다. setApprovalForAllForAssets로 지정된 오퍼레이터는 호출자가 소유한 모든 토큰에 대해 acceptAsset, rejectAsset, rejectAllAssets, setPriority를 호출할 수 있으며 오퍼레이터는 호출자 자신일 수 없습니다.
참조 기반 저장 구조
에셋은 토큰 안에 uint64 식별자 배열로 저장됩니다. 중복되는 문자열을 체인에 거듭 저장하지 않도록 에셋 데이터는 내부 저장소에 한 번만 두고 uint64 매핑으로 참조합니다. 같은 범용 에셋을 여러 토큰에 붙여도 토큰마다 에셋 배열에 16바이트만 더 쓰면 됩니다. 구현자는 에셋의 기본 주소와 토큰 ID를 문자열로 이어 붙여 tokenId에 따라 달라지는 URI를 프로그램적으로 만들 수 있습니다. 에셋 ID가 uint64라 가스 한도 안에서 약 30.000개까지 읽을 수 있음이 확인되어 페이지네이션은 인터페이스에 넣지 않았습니다.
에셋 메타데이터 형식
getAssetMetadata는 에셋의 메타데이터 URI를 돌려줍니다. 그 URI가 가리키는 메타데이터는 name, description, mediaUri, thumbnailUri, externalUri, license, licenseUri, tags, preferThumb, attributes 필드를 담은 JSON일 수 있습니다(MAY). preferThumb는 UI가 mediaUri 대신 thumbnailUri를 우선 쓰라는 플래그이고, attributes의 각 항목은 하위 호환을 위해 trait_type 필드를 함께 둘 수 있습니다. 이 스키마는 제안일 뿐 강제되지 않으며 구현자가 완전히 다르게 구성해도 됩니다.
ERC-721 호환과 인터페이스 식별자
이 표준은 ERC-721의 풍부한 도구와 기존 인프라를 그대로 쓸 수 있도록 ERC-721과 호환되게 설계되었습니다. ERC-165를 요구하며 이 인터페이스의 ERC-165 식별자는 0x06b4329a입니다. 승인에 EIP-712 서명 방식을 쓰지 않은 이유는 일관성 때문으로, ERC-721이 이미 트랜잭션 1회로 승인을 처리하므로 에셋 작업만 서명 방식을 섞는 것은 일관되지 않다고 보았습니다. 이 표준은 상태가 Final로 확정된 제안입니다.
쓸 때 주의할 점
- 숨은 로직 주의: ERC-721과 같은 보안 고려 사항이 적용됩니다. 소각(burn), 에셋 추가, 에셋 수락을 비롯한 어떤 함수에도 숨은 로직이 들어 있을 수 있습니다.
- 감사받지 않은 컨트랙트 주의: 감사(audit)를 받지 않은 컨트랙트를 다룰 때는 주의가 필요합니다.