Royalty Enforcement

Our Protocol allows NFT Creators to decide which trading smart contracts they trust which, therefore, allows their NFT collections to be traded in them, and also allows for their NFTs to interoperate with non-trading smart contracts.

Nevertheless, proactively monitoring trading and P2P transfer modules is going to be a collective effort from the ecosystem. Our Protocol allows NFT creators to tap into any trusted list curated by the community. While anyone can create their own list, it is likely that the Creator’s community will coalesce in a select few of them.

In order to guarantee royalty enforcement and prevent royalty leaking, OriginByte ownership model revolves around NFTs living inside the OriginByte Kiosk, which prevents users from calling sui::transfer functions. By guaranteeing that NFTs always live in the Owner's Kiosk instead of being directly owned by the Owner address, we can indirectly constrain sui::transfer functions by enforcing transferability only via trusted smart contracts.

OB Kiosk

Kiosk is an abstraction meant to hold NFTs in it. We have a dedicated page about OriginByte Kiosk, which you can ready to get all the details.

The TLDR is that a user that transfers its NFTs to its OB Kiosk is able to delegate the power of transferability. One typical issue with on-chain trading is that by sending one’s assets to a shared object (the trading primitive), one loses the ability to see them in their wallet, even though one has still technical ownership of such assets, until a trade is effectively executed.

To solve for this, we use Kiosk to hold the user’s assets. Then instead of transferring the assets to the shared object (trading primitive), the user provides a transfer authorization to the trading smart contract, that delegates the ability to transfer a given NFT out of the seller’s Kiosk at a later stage.

The OB Kiosk's ownership is not transferrable, and it relies on the static field owner. The owner of an OB Kiosk receives an honorary OwnerToken object, which helps in the discoverability process (it makes it easier for Wallets and clients to fetch the Kiosk objects).

The OB Kiosk abstraction also guarantees that traders cannot bypass royalties by tapping into the global transfer function.

Sui Kiosk & OB Kiosk

The Sui Kiosk implementation is in the Sui Framework, and OB Kiosk an extension implemented by OriginByte.

While the base Kiosk implementation provides the core functionality for holding and listing NFTs, OB Kiosk extends the functionality with a multitude of features, which you can find here. It's important to note that in order to guarantee Royalty Enforcement, NFT collections should use the OB Kiosk extension.

Allowlists

Allowlists are used to authorize which contracts are allowed to transfer NFTs of a particular collection, and its module, nft_protocol::transfer_allowlist is a set of functions for implementing and managing an allowlist for NFT transfers.

The module includes functions for creating and managing the allowlist, adding and removing collections from the allowlist, and checking whether a contract is authorized to transfer a particular NFT. The module uses generics and reflection to allow for flexibility in implementing and managing the allowlist.

struct Allowlist has key, store {
    id: UID,
    /// We don't store it as generic because then it has to be propagated
    /// around and it's very unergonomic.
    admin_witness: TypeName,
    /// Which collections does this allowlist apply to?
    ///
    /// We use reflection to avoid generics.
    collections: VecSet<TypeName>,
    /// If None, then there's no allowlist and everyone is allowed.
    ///
    /// Otherwise we use a witness pattern but store the witness object as
    /// the output of `type_name::get`.
    authorities: Option<VecSet<TypeName>>,
}

There are three generics at play:

  1. Admin (allowlist witness) enables any organization to start their own allowlist and manage it according to their own rules;

  2. CW (collection witness) empowers creators to add or remove their collections to allowlists;

  3. Auth (3rd party witness) is used to authorize contracts via their witness types. If e.g. an Orderbook trading contract wants to be included in a allowlist, the allowlist admin adds the stringified version of their witness type. The Orderbook then uses this witness type to authorize transfers.

To confirm that a trading contract is authorized, a witness of said contract is attached, as a TypeName, to the TransferRequest. This is done by the trading contract effectively calling ob_kiosk::set_transfer_request_auth.

The Transfer Receipt abstraction

TransferReceipt is an intermediate Hot Potato object that is issued from the OB Kiosk when an NFT gets withdrawn from the seller's Kiosk. To return this Hot Potato, the client will have to go through a list of tasks, and by resolving those task, we guarantee that Royalty is enforced.

This pattern of rolling tasks into a hot potato is called rolling hot potato, or hot potato pipelining, and it was designed by OriginByte in conjunction with Mysten team. The task required to be performed are defined by an object called TransferPolicy.

TransferPolicy is a type that allows the creator to set custom transfer rules for deals performed in a Kiosk. When a TransferPolicy is created and shared, the type T becomes tradable in Kiosks. On every purchase operation, a TransferRequest is created and needs to be confirmed by the TransferPolicy, or the transaction will fail. The creator can set any rules for the TransferPolicy, and once the required actions are performed, the TransferRequest can be confirmed. TransferPolicy serves as the main interface for creators to control their royalty policies are respected. Custom policies can be removed at any time and will affect all instances of the type at once.

The base TransferPolicy and TransferRequest are implemented in the Sui-Framework. Whilst OriginByte uses the base TransferPolicy from the Sui-Framework, we have implemented a custom version of TransferRequest that gives more fine-grained control to Creators, such as:

  • OriginByte transfer request offers generics over fungible token. Game Studios and Creators are no longer limited to receiving royalties in SUI token, but can also use their own Fungible Tokens for that purpose. Royalty policies can decide whether they charge a fee in other tokens.

  • Our transfer request associates paid balance with the request. This enables us to do permissionless transfers of NFTs. That's because we store the beneficiary address (e.g. NFT seller) with the paid balance. Then when confirming a transfer, we transfer this balance to the seller. With special capability, the policies which act as a middleware can access the balance and charge royalty from it. Therefore, a 3rd party to a trade can send a transaction to finish it. This is helpful for reducing # of transactions users have to send for trading logic, which requires multiple steps. With OriginByte protocol, automation can be set up by marketplaces.

To lower the barrier to entry, we mimic those TransferReceipt APIs where relevant. We interoperate with the base TransferRequest by allowing our TransferRequest to be converted into the Sui version. This is only possible for the cases where the payment is done in SUI token.

Trading-Royalty Flow

This section is intended to describe the E2E royalty-trading flow.

It all starts with the creator initiating the TransferPolicy and registering the Allowlist as a rule in the TransferPolicy. This step occurs in the init function of the contract deployed by the Creator:

// Creates a new policy and registers an allowlist rule to it.
// Therefore now to finish a transfer, the allowlist must be included
// in the chain.
let (transfer_policy, transfer_policy_cap) =
    sui::transfer_policy::new<SUIMARINES>(&publisher, ctx);
nft_protocol::transfer_allowlist::add_policy_rule(
    &mut transfer_policy,
    &transfer_policy_cap,
);

Still in the same init function, the Creator defines the Royalty Strategy, in this case it's the plain-vanilla BPS strategy:

royalty_strategy_bps::create_domain_and_add_strategy<Submarine>(
    witness::from_witness(Witness {}), &mut collection, 100, ctx,
);

This function call essentially creates a Royalty domain in the Collection<T> whic is as follows:

/// A shared object which can be used to add receipts of type
/// `BpsRoyaltyStrategyRule` to `TransferRequest`.
struct BpsRoyaltyStrategy<phantom T> has key {
    id: UID,
    /// Royalty charged on trades in basis points
    royalty_fee_bps: u64,
    /// Allows this middleware to touch the balance paid.
    /// The balance is deducted from the transfer request.
    /// See the docs for `BalanceAccessCap` for more info.
    access_cap: Option<BalanceAccessCap<T>>,
    /// Contains balances of various currencies.
    aggregator: Balances,
}

Where the BalanceAccessCap<T> is an object that allows the royalty domain to gain access to balance inside the TransferReceipt.

When a trade occurs in the Liquidity Layer or in an authorized trading contract, the NFT sold gets withdrawn from the Seller's Kiosk, along with the corresponding TransferReceipt. This TransferReceipt gets returned to the client, to be resolved in a Programmable Transaction Batch.

To resolve the TransferReceipt, the client must call transfer_allowlist::confirm_transfer which will confirm if the trading contract used is authorized. If it's authorized, it will stamp the TransferReceipt with an AllowlistRule receipt and return it. Then the client must call royalty_strategy_bps::confirm_transfer in order for royalties to be deducted from the trade price and added to the Royalty domain in the Collection<T> object. As a final step, the client must call ob_transfer_request::confirm, which will consume the hot potato and resolve the transaction.

Royalties can then be distributed to creators by calling:

royalty_strategy_bps::distribute_royalties

Last updated