Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Royalties · Boson Protocol
Skip to content

Royalties

Boson moves royalty handling from the voucher contract into the Diamond. This unlocks:

  • Per-offer royalty splits — different offers in the same voucher collection can pay different recipients at different percentages.
  • Multiple recipients per offer — split a single sale across many parties without an off-chain splitter.
  • Royalty Registry compatibility — works with royaltyregistry.xyz so marketplaces that consult the registry honour the full recipient list.
  • Sequential-commit royalties — secondary-market resales through the protocol pay royalties at finalisation time using the recipients in force at each sale (see Sequential commit).

The voucher contract still exposes ERC-2981 royaltyInfo(tokenId, salePrice) for marketplaces that only support a single recipient; it returns the sum of all bps and the first recipient on the list. Marketplaces that consult the Royalty Registry get the full multi-recipient breakdown.

bps: basis points. 100 bps = 1 %. The protocol stores percentages in bps so they can be expressed without floating-point.

Two-step model

  1. Admin pre-approves recipients on the seller, optionally setting a per-recipient minimum royalty %.
  2. Assistant picks a subset of approved recipients per offer, with bps that meet or exceed the admin's minimums.

The seller's treasury is always the default recipient (zero-address placeholder) and cannot be removed. If you later update the treasury via updateSeller, every offer that uses the default automatically picks up the new address — no per-offer update needed.

Configure recipients on the seller

SDK
// Admin-only. Recipients can later be referenced by address from any offer.
await (await sdk.addRoyaltyRecipients(sellerId, [
  { wallet: brandA, minRoyaltyPercentage: 250,  externalId: "brand-a" }, // 2.5% minimum
  { wallet: brandB, minRoyaltyPercentage: 500,  externalId: "brand-b" }, // 5%   minimum
  { wallet: charityC, minRoyaltyPercentage: 100, externalId: "charity-c" }, // 1%  minimum
])).wait()
 
// Later: update or remove
await (await sdk.updateRoyaltyRecipients(sellerId, [1], [{ wallet: brandA2, minRoyaltyPercentage: 300, externalId: "brand-a-v2" }])).wait()
await (await sdk.removeRoyaltyRecipients(sellerId, [2])).wait() // remove charityC

Set the per-offer split at creation

Offers now carry a royaltyInfo field. The default recipient (treasury) is address(0).

await sdk.createOffer({
  // …offer fields
  royaltyInfo: {
    recipients: [
      "0x0000000000000000000000000000000000000000", // = seller treasury (default)
      brandA,
      charityC,
    ],
    bps: [200, 300, 100], // 2% to treasury, 3% to brandA, 1% to charityC6% total
  },
})

Constraints enforced by the Diamond:

  • Every non-default recipient must be on the seller's approved list.
  • Each bps[i] must be ≥ the admin-set minimum for that recipient.
  • The sum of bps must be ≤ the protocol's maxRoyaltyPercentage.

Update royalties on an existing offer

await sdk.updateOfferRoyaltyRecipients(offerId, {
  recipients: [ZeroAddress, brandA],
  bps: [300, 400],
})
// Or batch
await sdk.updateOfferRoyaltyRecipientsBatch([offerId1, offerId2], { /* same shape */ })

This only affects future secondary-market sales of vouchers from that offer; commits already paid out keep their old terms.

Reading royalty info

// Full multi-recipient breakdowncall the Diamond's ERC-2981 entry point and the
// Royalty Registry. There is no `sdk.getRoyalties` / `sdk.getEIP2981Royalties`
// at the current SDK version; either build the call yourself or read from the
// subgraph.
import IBosonOfferHandler from "@bosonprotocol/common/dist/cjs/abis/IBosonOfferHandler.json"
const diamond = new ethers.Contract(diamondAddress, IBosonOfferHandler, provider)
const { recipients, bps } = await diamond.getRoyalties(tokenId)
 
// ERC-2981 reduction (single recipient, summed bps) — call the voucher contract
// (not the Diamond), e.g. via the seller's collection address:
const voucher = new ethers.Contract(voucherCollectionAddress, ["function royaltyInfo(uint256,uint256) view returns (address,uint256)"], provider)
const [receiver, royaltyAmount] = await voucher.royaltyInfo(tokenId, salePrice)

The Boson voucher contract exposes royaltyInfo(tokenId, salePrice) — the standard ERC-2981 entry point — and the Diamond carries the per-offer multi-recipient breakdown.

Royalty Registry setup

If your offers ever have multiple recipients, point the registry at the Boson protocol so it returns the full breakdown:

  1. Find the Royalty Registry address on your chain.
  2. Call setRoyaltyLookupAddress(yourVoucherCollectionAddress, bosonDiamondAddress).

Marketplaces that honour the registry then enumerate every recipient on each secondary sale. Marketplaces that only respect ERC-2981 still pay the first recipient.

If you'd rather split one ERC-2981 royalty across many wallets, use a splitter contract (e.g. 0xSplits) as the first recipient with the full bps, and leave the others at 0 bps — they'll only be paid by registry-aware marketplaces.

Common gotchas

  • Zero-address = treasury. If you put a real address in for the default split, you'll have to update every offer when the seller's treasury changes.
  • Minimum bps is per-recipient. The minimum doesn't apply to the treasury (default) recipient.
  • No retroactive royalty changes on completed exchanges. Royalties paid out on an exchange use the recipients that were in force at each sale event.
  • Royalty caps are protocol-level. maxRoyaltyPercentage is set by ConfigHandlerFacet; check protocol.getMaxRoyaltyPercentage() before designing splits.

Related