ERC-7743 다중 소유자 NFT(MO-NFT) (Multi-Owner Non-Fungible Tokens (MO-NFT))
Final · NFT·멀티토큰 · 2024-07-13 작성
하나의 NFT를 여러 사람이 동시에 소유할 수 있게 하는 표준입니다. 전송해도 기존 소유자는 그대로 남고 전송 때마다 정해진 수수료가 제공자에게 지급됩니다.
왜 필요한가
기존 NFT 표준(ERC-721)은 토큰 하나에 소유자가 한 명뿐인 단독 소유 모델입니다. 그런데 파일 같은 디지털 자산은 복제해도 품질이 떨어지지 않아 여러 사람이 함께 가질 수 있는데, 단독 소유 모델은 이런 성격을 담지 못합니다. 소유권을 넘기면 원래 주인은 권리를 잃고 여러 명이 같은 자산을 함께 소유하는 구조를 표준 방식으로 만들 수 없습니다. 또 자산을 처음 제공한 사람이 이후 유통 과정에서 계속 보상을 받을 장치도 표준에 없습니다. 이 표준은 전송할 때 기존 소유자를 지우지 않고 새 소유자를 목록에 추가하는 방식으로 공동 소유를 지원하고 전송이 일어날 때마다 정해진 수수료를 제공자에게 보내 창작자와 제공자가 보상을 받도록 합니다. 소유자가 스스로 전송 권한을 내려놓는 아카이브 장치도 함께 정의합니다.
구현해야 하는 것
function mintToken() public onlyOwner returns (uint256);: 새 MO-NFT를 발행합니다. 호출자가 첫 소유자이자 제공자로 기록되고 호출자의 잔고가 1 늘어납니다.function provide(string memory assetName, uint256 size, bytes32 fileHash, address provider, uint256 transferValue) external returns (uint256);: 자산 이름, 크기, 파일 해시, 제공자 주소, 전송 수수료를 지정해 디지털 자산 토큰을 등록합니다.function transferFrom(address from, address to, uint256 tokenId) public;: 새 소유자를 목록에 추가하는 방식으로 토큰을 전송합니다. 기존 소유자는 유지되고 수수료가 제공자에게 지급됩니다.function safeTransferFrom(address from, address to, uint256 tokenId) public;: ERC-721의 안전 전송 함수입니다. MO-NFT에서는 비활성화되거나 재정의됩니다.function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public;: 데이터를 함께 넘기는 안전 전송 함수입니다. 역시 비활성화되거나 재정의됩니다.function isOwner(uint256 tokenId, address account) public view returns (bool);: 해당 주소가 그 토큰의 소유자 목록에 있는지 확인합니다.function getOwnersCount(uint256 tokenId) public view returns (uint256);: 그 토큰의 소유자 수를 돌려줍니다.function balanceOf(address owner) external view returns (uint256 balance);: 해당 주소가 소유한 토큰 수를 돌려줍니다.function ownerOf(uint256 tokenId) external view returns (address owner);: 호환성을 위해 소유자 목록의 첫 번째 주소를 돌려줍니다.function setTransferValue(uint256 tokenId, uint256 newTransferValue) external;: 전송 수수료를 설정하거나 바꿉니다. 제공자만 호출할 수 있습니다.function archive(uint256 tokenId) external;: 호출한 소유자를 그 토큰에 대해 아카이브 상태로 표시합니다. 이후 그 토큰을 전송할 수 없고 되돌릴 수 없습니다.event TokenMinted(uint256 indexed tokenId, address indexed owner);: 새 토큰이 발행될 때 발생합니다.event TokenTransferred(uint256 indexed tokenId, address indexed from, address indexed to);: 토큰이 전송되어 새 소유자가 추가될 때 발생합니다.event TransferValueUpdated(uint256 indexed tokenId, uint256 oldTransferValue, uint256 newTransferValue);: 전송 수수료가 바뀔 때 이전 값과 새 값을 담아 발생합니다.event ArchivedStatusUpdated(uint256 indexed tokenId, address indexed owner, bool archived);: 소유자의 아카이브 상태가 바뀔 때 발생합니다.
심층 설명
추가형 소유권 모델
MO-NFT는 토큰마다 소유자 목록을 열거 가능한 집합(enumerable set)으로 유지해 중복을 막고 조회를 효율화합니다. transferFrom은 기존 소유자를 지우지 않고 to 주소를 집합에 추가하며 to의 balanceOf를 1 늘립니다. 참조 구현은 호출자가 소유자인지(isOwner), to가 0 주소가 아닌지, to가 이미 소유자가 아닌지 require로 검사하고 이미 소유자인 주소로 전송하면 "MO-NFT: Recipient is already an owner" 오류로 되돌립니다. 전송이 끝나면 TokenTransferred 이벤트가 발생합니다.
제공자 역할과 수수료 처리
mintToken()을 호출한 계정이 첫 소유자이자 제공자(provider)로 기록됩니다. 제공자만 setTransferValue로 전송 수수료를 설정·변경할 수 있고, 변경 시 TransferValueUpdated 이벤트가 발생합니다. 전송이 일어나면 transferValue만큼의 이더가 컨트랙트에서 제공자에게 지급되는데, 참조 구현은 address(this).balance >= transferValue를 확인한 뒤 call로 송금하고 실패하면 되돌립니다. 즉 컨트랙트에 수수료를 지급할 잔고가 있어야 전송이 성립합니다.
아카이브: 되돌릴 수 없는 전송 포기
소유자는 archive(tokenId)로 특정 토큰에 대해 자신을 아카이브 상태로 표시할 수 있습니다. 아카이브된 소유자는 그 토큰을 더는 전송할 수 없고 한 번 설정하면 되돌릴 수 없습니다. 디지털 자산 플랫폼에서는 이 상태가 실제 디지털 자산을 직접 내려받을 수 있다는 의미이며 아카이브되지 않은 소유자는 먼저 아카이브 상태로 바꿔야 자산을 내려받을 수 있습니다. Rationale은 이 장치가 기존의 소유권 소각(burning) 개념을 대체한다고 설명합니다. 상태 변경 시 ArchivedStatusUpdated 이벤트가 발생합니다.
ERC-721 호환과 의도적 차이
MO-NFT는 ERC-721의 balanceOf, ownerOf, transferFrom을 구현하고 supportsInterface에서 IERC721과 IERC165 인터페이스를 선언해 기존 도구·플랫폼과 연동되도록 합니다. 다만 approve, getApproved, setApprovalForAll, isApprovedForAll은 의도적으로 비활성화하거나 재정의합니다. 공동 소유 합의 장치 없이 승인을 허용하면 공동 소유자 한 명의 승인만으로 원치 않는 전송이 일어날 수 있기 때문이며 참조 구현은 승인 관련 호출을 되돌립니다. safeTransferFrom도 제한되는데, 원문은 ERC-721과 같은 방식으로 새 소유자 추가 후 수신 컨트랙트의 onERC721Received를 확인하는 구현 방식을 설명합니다. 이런 차이 때문에 approve에 의존하는 기존 마켓플레이스와의 호환이 제한될 수 있습니다.
ownerOf의 해석
ownerOf는 호환성을 위해 소유자 목록의 첫 번째 소유자를 돌려줍니다. 그러나 MO-NFT에서는 단일 소유자 개념이 온전히 적용되지 않으므로, ownerOf가 돌려주는 주소를 유일한 소유자로 해석하면 안 됩니다. 실제 소유 여부는 isOwner(tokenId, account)로, 소유자 수는 getOwnersCount(tokenId)로 확인합니다. 원문은 표준 ERC-721용으로 설계된 시스템에 MO-NFT를 연동할 때 개발자가 이런 차이를 인지해야 한다고 적습니다.
디지털 자산 등록 함수 provide
민팅 함수로 mintToken() 외에 provide(string memory assetName, uint256 size, bytes32 fileHash, address provider, uint256 transferValue)가 정의되어 있습니다. 자산 이름, 크기, 파일 해시, 제공자 주소, 전송 수수료를 받아 토큰 식별자를 돌려줍니다. 참조 구현에는 기본 구현인 MONFT.sol과 제공자 수수료를 갖춘 디지털 자산용 확장 구현 DigitalAsset.sol, 그리고 자산 관리 함수를 정의한 IDigitalAsset.sol 인터페이스가 포함됩니다.
쓸 때 주의할 점
- 재진입 공격: 전송 수수료를 제공자에게 이더로 보낼 때 재진입이 가능합니다. 검사-상태변경-상호작용(Checks-Effects-Interactions) 패턴을 지키고 OpenZeppelin의 ReentrancyGuard 사용을 권장합니다.
- 소유자 목록 순회로 인한 가스 부담(DoS): 소유자 목록을 순회하는 함수는 목록이 커지면 가스 비용이 크게 늘 수 있습니다. 그런 함수를 피하거나 소유자 수를 제한해야 합니다.
- 접근 제어: 전송은 소유자만, transferValue 설정은 제공자만 할 수 있어야 하며 require 문으로 이를 강제해야 합니다.
- 이더 수신 처리: 제공자에게 수수료를 지급하려면 컨트랙트가 이더를 받을 수 있어야 합니다. receive() 함수를 구현해 이더를 수신하도록 합니다.
- 정수 오버플로·언더플로: Solidity 0.8.x는 오버플로와 언더플로를 자동으로 검사해 발생 시 예외를 던집니다.