Розробка NFT з роялті
У 2022-2023 роках маркетплейси почали роблять роялті опціональними — Blur запропонував нульові комісії, частина аудиторії OpenSea перейшла туди. Творці колекцій втратили мільйони. Це привело до двох таборів: ті, хто впроваджує on-chain enforcement роялті, та ті, хто покладаються на добру волю маркетплейсів. Вибір між ними — не технічний, а продуктовий. Ми реалізуємо обидва підходи.
ERC-2981 як базовий стандарт
ERC-2981 — це сигнальний стандарт. Контракт оголошує royaltyInfo(tokenId, salePrice), маркетплейс читає та (опціонально) платить. Blur може ігнорувати. OpenSea поважає. Magic Eden — залежить.
Реалізація через OpenZeppelin займає 10 рядків:
import "@openzeppelin/contracts/token/common/ERC2981.sol";
contract MyCollection is ERC721, ERC2981 {
constructor(address royaltyReceiver) ERC721("Collection", "COL") {
_setDefaultRoyalty(royaltyReceiver, 750); // 7.5%
}
function supportsInterface(bytes4 interfaceId)
public view override(ERC721, ERC2981) returns (bool) {
return super.supportsInterface(interfaceId);
}
}
Без переовайду supportsInterface маркетплейс не побачить підтримку ERC-2981 при перевірці ERC-165.
Operator Filter: On-chain Enforcement
Якщо роялті комерційно важливі — потрібен operator filter. Ідея: контракт перевіряє кожен transferFrom та safeTransferFrom, дозволяючи transfer лише через затверджені маркетплейси, які чесно платять роялті.
OpenSea запропонував OperatorFilterRegistry у 2022 році:
import {DefaultOperatorFilterer} from "operator-filter-registry/src/DefaultOperatorFilterer.sol";
contract MyCollection is ERC721, ERC2981, DefaultOperatorFilterer {
function transferFrom(address from, address to, uint256 tokenId)
public override onlyAllowedOperator(from) {
super.transferFrom(from, to, tokenId);
}
function safeTransferFrom(address from, address to, uint256 tokenId)
public override onlyAllowedOperatorApproval(from) {
super.safeTransferFrom(from, to, tokenId);
}
}
onlyAllowedOperator перевіряє адресу оператора в OperatorFilterRegistry. Blur спочатку був заблокований, потім доданий після переговорів.
Компромісс: operator filter захищає роялті, але обмежує ліквідність — користувачи не можуть торгувати на незатверджених платформах. Для деяких колекцій це неприйнятно.
Кастомна логіка Enforcement роялті
Незалежність від реєстру OpenSea — через кастомну логіку. Підхід: дозволити transfer лише якщо його ініціював через whitelist контрактів (маркетплейси, які явно інтегрували наш механізм роялті), або якщо це wallet-to-wallet transfer (не через маркетплейс).
mapping(address => bool) public approvedMarketplaces;
function _beforeTokenTransfer(address from, address to, uint256 tokenId)
internal override {
// Дозволяємо прямі трансфери (не через маркетплейс)
if (from == tx.origin || to == tx.origin) return;
// Перевіряємо, що маркетплейс затверджений
require(approvedMarketplaces[msg.sender], "Marketplace not approved");
}
Менш гнучко, але незалежно від зовнішніх реєстрів.
Splitter для команд
Якщо роялті ділиться між кількома адресами, встановіть receiver у ERC-2981 на PaymentSplitter:
address[] memory payees = [founder, artist, treasury];
uint256[] memory shares = [50, 30, 20];
PaymentSplitter splitter = new PaymentSplitter(payees, shares);
_setDefaultRoyalty(address(splitter), 500); // 5% роялті на сплиттер
Кожен отримувач викликає splitter.release(token) щоб вивести накопичені кошти. Pull паттерн — без ризику reentrancy при автоматичній дистрибуції.
Типові помилки
Забутий supportsInterface — маркетплейс не бачить ERC-2981. Роялті на нульову адресу при address(0) receiver — платежі йдуть в нікуди. Занадто високі роялті (>10%) зменшують обсяг торгів. Немає способу оновити receiver — творець не може змінити адресу гаманця.
Щоб receiver був оновлювальним, додайте updateDefaultRoyalty() з onlyOwner:
function updateDefaultRoyalty(address receiver, uint96 feeNumerator)
external onlyOwner {
_setDefaultRoyalty(receiver, feeNumerator);
}
Оцінка часу
NFT контракт з ERC-2981 роялті та PaymentSplitter — 2-3 дні. З operator filter та кастомною логікою enforcement — 3-4 дні.







