ERC-4910: Royalty Bearing NFTs
ERC-4910 goes beyond ERC-2981 (which only declares royalty info) by enforcing royalty distribution on-chain. The NFT contract escrows royalty payments at sale time, lets multiple recipients (artist, label, charity) claim their share, and supports royalty-recipient changes through a managed flow.
Used by:
- Music NFTs with multi-party splits (artist + producer + label + sample owners).
- Collaborative art with co-creator royalty splits.
- Charity NFTs automatically routing a fraction to a charitable address.
- DAO-owned NFTs distributing royalty across DAO members.
Required Interface (delta from ERC-2981)
solidity
interface IERC4910 {
event RoyaltiesPaid(uint256 indexed tokenId, uint256 totalAmount);
event RoyaltyClaimed(address indexed recipient, uint256 amount);
struct RoyaltyRecipient { address recipient; uint256 share; } // share in basis points
function setRoyaltyRecipients(uint256 tokenId, RoyaltyRecipient[] calldata recipients) external;
function distributeRoyalties(uint256 tokenId, uint256 amount) external;
function claimRoyalties() external;
function pendingRoyaltiesOf(address recipient) external view returns (uint256);
}The marketplace calls distributeRoyalties(tokenId, totalRoyalty) on every sale; the contract escrows and tracks per-recipient balances. Recipients call claimRoyalties() to withdraw their accumulated share.
Neo Equivalent: NEP-11 + Escrow + Claim Flow
csharp
public static void DistributeRoyalties(ByteString tokenId, BigInteger totalAmount)
{
var recipients = GetRecipients(tokenId);
BigInteger sumShares = 0;
for (var i = 0; i < recipients.Length; i++)
sumShares += (BigInteger)((Map<string, object>)recipients[i])["share"];
if (sumShares != 10000) throw new Exception("NEP24:SharesNot100Percent");
for (var i = 0; i < recipients.Length; i++)
{
var entry = (Map<string, object>)recipients[i];
var addr = (UInt160)entry["recipient"];
var share = (BigInteger)entry["share"];
var amount = (totalAmount * share) / 10000;
var key = PendingKey(addr);
var prev = (BigInteger)(Storage.Get(Storage.CurrentContext, key) ?? ByteString.Empty);
Storage.Put(Storage.CurrentContext, key, prev + amount);
}
OnRoyaltiesPaid(tokenId, totalAmount);
}
public static void ClaimRoyalties()
{
var caller = Runtime.CallingScriptHash;
if (!Runtime.CheckWitness(caller)) throw new Exception("NEP24:NoAuth");
var key = PendingKey(caller);
var pending = (BigInteger)(Storage.Get(Storage.CurrentContext, key) ?? ByteString.Empty);
if (pending == 0) return;
Storage.Put(Storage.CurrentContext, key, 0);
Contract.Call(GAS.Hash, "transfer", CallFlags.All,
new object[] { Runtime.ExecutingScriptHash, caller, pending, null });
OnRoyaltyClaimed(caller, pending);
}| ERC-4910 (Ethereum) | Neo Equivalent | Notes |
|---|---|---|
distributeRoyalties(tokenId, amount) | DistributeRoyalties(...) marketplace-callable | Marketplace escrows royalty into NFT contract |
claimRoyalties() | ClaimRoyalties() recipient-witness-checked | Pull pattern — recipients withdraw |
pendingRoyaltiesOf(recipient) | PendingRoyaltiesOf(recipient) view | |
| Recipients + shares per tokenId | (tokenId → [{ recipient, share_bps }]) storage | |
setRoyaltyRecipients(...) | SetRoyaltyRecipients(...) issuer-witness-checked | Update splits |
