bpip | title | author | discussions-to | status | created |
---|---|---|---|---|---|
5 |
Flexible royalties |
Klemen Zajc |
Review |
2023-03-13 |
This proposal describes the change of royalty handling to support more granular control, which enables multiple recipients and setting royalties per individual offer instead of a collection.
Currently, a seller can only set royalties on a voucher contract level, which means that all offers in the same collection get the same recipient (seller's treasury) and the same royalty percentage. A seller might want to specify multiple recipients and/or set different royalty percentages per offer. A big motivation to do it is that sellers can sell items of multiple brands and they want to pay out per-brand royalties.
A seller could always do it manually, but that presents a bad user experience and requires an off-chain management process. Alternatively, they could use a splitting contract (such as https://www.0xsplits.xyz/) to have multiple recipients. But that requires them to set the splitting contract as the treasury, which means that all proceeds (not only royalties) will eventually end up in a splitter, which is not desired. It also does not allow setting royalties per offer.
An additional motivation to change royalties is compatibility with https://royaltyregistry.xyz/. Although Boson voucher contracts already implement the EIP2981 royalty standard, which is compatible with the registry, it imposes additional limitation which is currently not met by the voucher contract.
To achieve this we propose to move royalties management from the Boson voucher into the Boson protocol, which will enable both more flexible royalties and full compatibility with Royalty Registry.
bps: basis points. A basis point is a unit of measure for percentages and equals 0.01%. Since EVM does not have a float or decimal type, percentages are stored in bps, for example, 23.55% is stored as 2355. The bps range is between 0 and 10,000, representing 0 and 100% respectively.
The following structs are added.
struct RoyaltyRecipient {
address wallet;
uint256 minRoyaltyPercentage;
string externalId;
}
struct RoyaltyInfo {
address payable[] recipients;
uint256[] bps; // basis points
}
And Offer
is extended with an additional field
struct Offer {
uint256 id;
uint256 sellerId;
uint256 price;
uint256 sellerDeposit;
uint256 buyerCancelPenalty;
uint256 quantityAvailable;
address exchangeToken;
string metadataUri;
string metadataHash;
bool voided;
uint256 collectionIndex;
+ RoyaltyInfo royaltyInfo;
}
The following event is changed.
- event VoucherInitialized(uint256 indexed sellerId, uint256 indexed royaltyPercentage, string indexed contractURI);
+ event VoucherInitialized(uint256 indexed sellerId, string indexed contractURI);
The following methods are removed.
- function setRoyaltyPercentage(uint256 _newRoyaltyPercentage) external;
- function getRoyaltyPercentage() external view returns (uint256);
The following event is added.
event RoyaltyRecipientsChanged(
uint256 indexed sellerId,
BosonTypes.RoyaltyRecipient[] royaltyRecipients,
address indexed executedBy
);
The following methods are added.
/**
* @notice Adds royalty recipients to a seller.
*
* Emits a RoyalRecipientsUpdated event if successful.
*
* Reverts if:
* - The sellers region of protocol is paused
* - Seller does not exist
* - Caller is not the seller admin
* - Caller does not own auth token
* - Some recipient is not unique
* - some royalty percentage is above the limit
*
* @param _sellerId - seller id
* @param _royaltyRecipients - list of royalty recipients to add
*/
function addRoyaltyRecipients(uint256 _sellerId, BosonTypes.RoyaltyRecipient[] calldata _royaltyRecipients)
external;
/**
* @notice Updates seller's royalty recipients.
*
* Emits a RoyalRecipientsUpdated event if successful.
*
* Reverts if:
* - The sellers region of protocol is paused
* - Seller does not exist
* - Caller is not the seller admin
* - Caller does not own auth token
* - Length of ids to change does not match length of new values
* - Id to update does not exist
* - Seller tries to update the address of default recipient
* - Some recipient is not unique
* - Some royalty percentage is above the limit
*
* @param _sellerId - seller id
* @param _royaltyRecipientIds - list of royalty recipient ids to update
* @param _royaltyRecipients - list of new royalty recipients corresponding to ids
*/
function updateRoyaltyRecipients(
uint256 _sellerId,
uint256[] calldata _royaltyRecipientIds,
BosonTypes.RoyaltyRecipient[] calldata _royaltyRecipients
) external;
/**
* @notice Removes seller's royalty recipients.
*
* Emits a RoyalRecipientsUpdated event if successful.
*
* Reverts if:
* - The sellers region of protocol is paused
* - Seller does not exist
* - Caller is not the seller admin
* - Caller does not own auth token
* - List of ids to remove is not sorted in ascending order
* - Id to remove does not exist
* - Seller tries to remove the default recipient
*
* @param _sellerId - seller id
* @param _royaltyRecipientIds - list of royalty recipient ids to remove
*/
function removeRoyaltyRecipients(uint256 _sellerId, uint256[] calldata _royaltyRecipientIds) external;
/**
* @notice Gets EIP2981 style royalty information for a chosen offer or exchange.
*
* EIP2981 supports only 1 recipient, therefore this method defaults to treasury address.
* This method is not exactly compliant with EIP2981, since it does not accept `salePrice` and does not return `royaltyAmount,
* but it rather returns `royaltyPercentage` which is the sum of all bps (an exchange can have multiple royalty recipients).
*
* This function is meant to be primarily used by boson voucher client, which implements EIP2981.
*
* Reverts if exchange does not exist.
*
* @param _queryId - offer id or exchange id
* @param _isExchangeId - indicates if the query represents the exchange id
* @return receiver - the address of the royalty receiver (seller's treasury address)
* @return royaltyPercentage - the royalty percentage in bps
*/
function getEIP2981Royalties(
uint256 _queryId,
bool _isExchangeId
) external view returns (address receiver, uint256 royaltyPercentage);
/**
* @notice Gets royalty information for a chosen offer or exchange.
*
* Returns a list of royalty recipients and corresponding bps. Format is compatible with Manifold and Foundation royalties
* and can be directly used by royalty registry.
*
* Reverts if exchange does not exist.
*
* @param _tokenId - tokenId
* @return recipients - list of royalty recipients
* @return bps - list of corresponding bps
*/
function getRoyalties(
uint256 _tokenId
) external view returns (address payable[] memory recipients, uint256[] memory bps);
/**
* @notice Sets new valid royalty info.
*
* Emits an OfferRoyaltyInfoUpdated event if successful.
*
* Reverts if:
* - The offers region of protocol is paused
* - Offer does not exist
* - Caller is not the assistant of the offer
* - New royalty info is invalid
*
* @param _offerId - the id of the offer to be updated
* @param _royaltyInfo - new royalty info
*/
function updateOfferRoyaltyRecipients(uint256 _offerId, BosonTypes.RoyaltyInfo calldata _royaltyInfo) external;
/**
* @notice Sets new valid until date for a batch of offers.
*
* Emits an OfferExtended event for every offer if successful.
*
* Reverts if:
* - The offers region of protocol is paused
* - For any of the offers:
* - Offer does not exist
* - Caller is not the assistant of the offer
* - New royalty info is invalid
*
* @param _offerIds - list of ids of the offers to extend
* @param _royaltyInfo - new royalty info
*/
function updateOfferRoyaltyRecipientsBatch(
uint256[] calldata _offerIds,
BosonTypes.RoyaltyInfo calldata _royaltyInfo
) external;
Although definitions of methods that create an offer do not change, the type of input parameter Offer offer
changes. As a consequence, the signatures of all methods that accept Offer
also change. Affected methods are
- createOffer
- createOfferBatch
- createSellerAndPremintedOffer
- createOfferWithCondition and createPremintedOfferWithCondition
- createOfferAddToGroup and createPremintedOfferAddToGroup
- createOfferAndTwinWithBundle and createPremintedOfferAndTwinWithBundle
- createOfferWithConditionAndTwinAndBundle and createPremintedOfferWithConditionAndTwinAndBundle
- createSellerAndOfferWithCondition and createSellerAndPremintedOfferWithCondition
- createSellerAndOfferAndTwinWithBundle and createSellerAndPremintedOfferAndTwinWithBundle
- createSellerAndOfferWithConditionAndTwinAndBundle and createSellerAndPremintedOfferWithConditionAndTwinAndBundle
All these methods must revert if, for any Offer:
- Royalty recipient is not on seller's allow list
- Royalty percentage is less that the value decided by the admin
- Total royalty percentage is more than max royalty percentage
Moving royalty management from the Boson voucher contract to the protocol contract is sensible since all other offer-related actions happen in the protocol. This then allows royalties to be set at the same time that the offer is created and does not require an additional external or internal transaction. Royalties are set at offer creation time, but they can later be updated (both recipients and bps).
Offers are created by the assistant, which should not have the right to specify an arbitrary recipient and/or royalty percentage since that would allow them to set the recipients that should not be entitled to get the royalties. Admin must approve recipients beforehand and the assistant can then choose any subset of recipients (including an empty set). Admin also has the option to set a minimum royalty percentage that a recipient should receive. When the assistant creates an offer, they must set percentages at least equal to or higher than the minimum percentage set by the admin. The seller's treasury is the default royalty recipient and cannot be removed from the list of approved recipients. It is created automatically when a seller is created. It is represented by a zero address, so if the treasury address is updated at any time, the seller does not have to update individual offers where the treasury is one of the recipients.
New methods in IBosonExchangeHandler
(getExchangeEIP2981Royalties
and getExchangeRoyalties
) are helper functions that are intended to be mainly used by the Boson voucher itself. It is expected that end users or clients will interact with Boson vouchers directly and these ExchangeHandler
methods will help to efficiently get the royalty recipients. getRoyalties
returns the full list of recipients and royalty percentage, while getExchangeEIP2981Royalties
reduces it to be closer to the EIP2981
royalties format, which supports only one recipient. getExchangeEIP2981Royalties
sums up all individual royalty percentages and makes the first recipient in the list the recipient of all royalties.
Marketplaces normally transfer the royalties at the same time when the NFT is transferred. In the case of sequential commit, the royalties are handled by the Boson protocol and are distributed only at the very end when the final state of the exchange is known. The rationale for that is that the intermediate prices in sequential commits might not represent the fair price and therefore the royalties should be calculated only at the end. If royalty recipients for the offer change between sequential commits, the protocol should still distribute the royalties to the recipients that were set at the time of each sequential commit.
This specification has an impact on previous protocol versions.
- All methods that create an offer have a different input parameter, so old methods won't work anymore.
- Existing sellers do not have a default royalty recipient set, so they will have to be initialized during the upgrade
-
Internal storage
uint256 private _royaltyPercentage;
is not used anymore. But since it affects storage layout we must keep a placeholder in to not affect other data
-
Removed methods
setRoyaltyPercentage
andgetRoyaltyPercentage
are removed.- Previously royalty percentage was also set during the initialization. This step is now omitted.
-
Royalty info Check if the token exists or is preminted. Otherwise, return zero address and zero royalty amount.
- If is preminted, query the protocol for royalties with
offerId
as an identifier. - If it is a normal voucher, query the protocol for royalties with
tokenId
as an identifier. - Apply the returned percentage to input
_salePrice
.
- If is preminted, query the protocol for royalties with
-
Implemented here.
-
Royalty recipients management
- When a seller is created, a default royalty recipient is created (treasury address), which cannot be removed.
- If the treasury address is updated via
updateSeller
, and the new treasury is already in the allowed royalty recipients list, it is removed from that list, since it's now represented by the default zero address. - Other royalty recipients can be
- added with
addRoyaltyRecipients
, - updated with
updateRoyaltyRecipients
and - removed with
removeRoyaltyRecipients
.
- added with
- Royalty recipients should be unique within a seller but can duplicate across sellers.
-
Offer creation
Offer
has an additional field - structRoyaltyInfo
which includes a list of recipients with corresponding royalty percentages.- If some recipient is not approved by the admin or the specified royalty percentage is too low, offer creation fails.
-
Updating the offer recipients
- Can be done with
updateOfferRoyaltyRecipients
for a single offer or withupdateOfferRoyaltyRecipientsBatch
for multiple offers in a single transaction. - If some recipient is not approved by the admin or the specified royalty percentage is too low, offer creation fails.
- If the seller's treasury is updated, the seller does not have to update individual royalty recipients, since it's handled by the protocol.
- Can be done with
-
Retrieving royalty info
- Use
getRoyalties
to get a full list of royalty recipients. - Use
getExchangeEIP2981Royalties
to get a reduced list of royalty recipients. The returned recipient is the first address from the recipient list and the returned royalty percentage is a sum of all individual royalty percentages. - Both methods can be queried with
tokenId
for preminted offers andexchangeId
for actual vouchers.
- Use
-
Implemented here.
-
Part of v2.4.0 release.
- External marketplaces that honor the royalties from https://royaltyregistry.xyz/ will send royalties to all recipients correctly. Marketplaces that follow EIP2981 will send royalties only to the first recipient.
To support both options in a single offer, we suggest using a splitter contract (for example https://www.0xsplits.xyz/) and setting the RoyaltyInfo as follows:
RoyaltyInfo{
recipients: [splitterAddress, recipient1, recipient2, recipient3, ...]
bps: [0, bps1, bps2, bps3, ...]
}
Using with Royalty Registry
If all offers in a collection have only one royalty recipient (which can be different across offers), no further action is needed since it's automatically handled by the Royalty Registry. It handles it as EIP2981 royalty standard.
However, if some offer has multiple recipients, the Royalty Registry would produce incorrect data. It first assumes that EIP2981-style royalty info exists and because the voucher contract returns the data, it does not check if additional information is available. In this case, the assistant must override the Royalty Lookup Address on Royalty Registry.
To do it, they must follow the steps below:
- Obtain the Royalty Registry address on the same chain where the seller's collection is
- Call method
setRoyaltyLookupAddress
with parameters:- tokenAddress: Collection address
- royaltyLookupAddress: Boson Protocol Address (
0x59A4C19b55193D5a2EAD0065c54af4d516E18Cb5
on Ethereum and Polygon. For testnet address refer to the protocol repository)
This will then produce the correct results for all offers in that collection.
Copyright and related rights waived via CC0.