Get $IP
Get $IP
Story Story
07 February 2025
© Story Foundation 2025

Learn

WhitepaperBlogFAQs

Build

Getting StartedDocsGitHubBrand Kit

Tools

Block ExplorerProtocol ExplorerFaucetStaking

Explore

EcosystemBridgeIP Portal

Community

CareersGovernanceForum

Legal

PrivacyTerms of UseEnd User TermsMiCA White Paper
External Royalty Policy Guide
back

External Royalty Policy Guide

Story

Story

27 August 2025

Tech

This technical guide explains how to set up custom royalty policies on Story.

Introduction

Story’s Royalty Module enables automated revenue sharing between IP Assets (IPAs) based on their derivative relationships and license terms.

The following technical guide explains how revenue flows through the protocol, as well as how IP owners are paid and can claim their share.

Each IPA has one royalty vault. This royalty vault has 100 Royalty Tokens associated with it, where each token represents the right to 1% of the total revenue generated by the IP Asset and thus deposited into the Royalty Vault.

Read more about Story’s Royalty module.

The way revenue is distributed from derivatives to their ancestors varies depending on the royalty policy relationship between them. Currently, in the protocol, there are two royalty policies that already exist:

  • LAP royalty policy: The Liquid Absolute Percentage (LAP) defines that each parent IP Asset can choose a minimum royalty percentage that all of its downstream IP Assets in a derivative chain will share from their monetary gains as defined in the license agreement. Further detail here.
  • LRP royalty policy: The Liquid Relative Percentage (LRP) royalty policy defines that each parent IP Asset can choose a minimum royalty percentage that only the direct derivative IP Assets in a derivative chain will share from their monetary gains as defined in the license agreement. Further detail here.

There can be as many flavors and variations of royalty distribution rules as we observe in the real world. The same can be expected on-chain. LAP or LRP may cover a significant amount of use cases, but many use cases have their own specificities for which neither would be a fit.

Whenever a use case requires unique and specific royalty rules, that set of rules can be registered as an External Royalty Policy.

What is an External Royalty Policy?

When developing your smart contract in order to register your new External Royalty Policy by calling registerExternalRoyaltyPolicy function in RoyaltyModule.sol make sure that your new external royalty policy smart contract:

  1. …is a smart contract that inherits a specific interface called IExternalRoyaltyPolicy.sol (Github link), which implements the view function below:
/// @notice Returns the amount of royalty tokens required to link a child to a given IP asset
/// @param ipId The ipId of the IP asset
/// @param licensePercent The percentage of the license
/// @return The amount of royalty tokens required to link a child to a given IP asset
function getPolicyRtsRequiredToLink(address ipId, uint32 licensePercent) external view returns (uint32);
  1. …is a smart contract that inherits ERC165 and implements the function below. For further details, you can check the smart contract example shown in section 2.
/// @notice IERC165 interface support.
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
    return interfaceId == this.getPolicyRtsRequiredToLink.selector || super.supportsInterface(interfaceId);
}

Creating an External Royalty Policy (Example)

Let’s imagine a situation where a scientist is raising 1M USD to build a new type of IP related to a new pharmaceutical product.

There is an investor who agrees to invest in the production of the new pharmaceutical but demands that the first 1M of profits from this venture be returned to them with priority. After the first 1M of profits, any additional profit is split 70% for the scientist and 30% for the investor.

royalty 1

These are profit/royalties distribution rules that are custom and more specific than the generic percentage split. In this case, there is an additional nuance in which up to a certain threshold, all profit goes to the investor until the investor breaks even on the investment.

In terms of IP registration, there would be a root IP with the Pharmaceutical Formula and a derivative IP with the Pharmaceutical Product. The license used to create IP2 has as its royalty policy the address of the external royalty policy smart contract.

royalty 2

Given that this use case is a custom case, it would require a new external royalty policy smart contract to be developed. As an example of the implementation of the external royalty policy above, let’s take a look at the smart contract below:

// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.26;

import { IERC165, ERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

import { IIpRoyaltyVault } from "../../../../contracts/interfaces/modules/royalty/policies/IIpRoyaltyVault.sol";
import { IRoyaltyModule } from "../../../../contracts/interfaces/modules/royalty/IRoyaltyModule.sol";

import { IExternalRoyaltyPolicy } from "../../../../contracts/interfaces/modules/royalty/policies/IExternalRoyaltyPolicy.sol";

contract ExternalRoyaltyPolicy is ERC165, IExternalRoyaltyPolicy, ReentrancyGuard {
    using SafeERC20 for IERC20;

    /// @notice Returns the percentage scale - represents 100%
    uint32 public constant MAX_PERCENT = 100_000_000;

    /// @notice The address of the IP asset
    address public constant WIP = 0x1514000000000000000000000000000000000000;

    /// @notice The address of the royalty module
    address public immutable ROYALTY_MODULE;    

    struct Product {
        address scientist;
        address investor;
        uint32 thresholdPercent;
        uint256 thresholdValue;
    }

    mapping(address ipId => Product) public products;
    mapping(address ipId => uint256) public totalClaimed;

    error NotAuthorized();
    error ProductAlreadyAssigned();
    error ProductNotAssigned();
    error InvalidScientist();
    error InvalidInvestor();
    error InvalidThresholdPercent();
    error InvalidThresholdValue();
    error InsufficientBalanceToAssign();
    error InvalidRoyaltyModule();
    error InsufficientBalanceToClaim();

    /// @notice Constructor
    /// @param royaltyModule The address of the royalty module
    constructor(address royaltyModule) {
        if (royaltyModule == address(0)) revert InvalidRoyaltyModule();
        ROYALTY_MODULE = royaltyModule;
    }

    /// @notice Assigns a product to an IP asset
    /// @param ipId The address of the IP asset
    /// @param scientist The address of the scientist
    /// @param investor The address of the investor
    /// @param thresholdPercent The threshold percentage
    /// @param thresholdValue The threshold value
    function assign(
        address ipId, 
        address scientist, 
        address investor, 
        uint32 thresholdPercent, 
        uint256 thresholdValue) 
    external {
        if (msg.sender != ipId) revert NotAuthorized(); // It is assumed for this example contract that the IP owner is trusted by all parties for the setup
        if (products[ipId].scientist != address(0)) revert ProductAlreadyAssigned();
        address royaltyToken = IRoyaltyModule(ROYALTY_MODULE).ipRoyaltyVaults(ipId);
        if (IERC20(royaltyToken).balanceOf(address(this)) == 0) revert InsufficientBalanceToAssign();

        if (scientist == address(0)) revert InvalidScientist();
        if (investor == address(0)) revert InvalidInvestor();
        if (thresholdPercent > MAX_PERCENT) revert InvalidThresholdPercent();
        if (thresholdValue == 0) revert InvalidThresholdValue();

        products[ipId] = Product(scientist, investor, thresholdPercent, thresholdValue);
    }

    /// @notice Claims revenue for an IP asset
    /// @param ipId The address of the IP asset
    function claim(address ipId) external nonReentrant {
        address scientist = products[ipId].scientist;
        if (scientist == address(0)) revert ProductNotAssigned();

        address royaltyToken = IRoyaltyModule(ROYALTY_MODULE).ipRoyaltyVaults(ipId);
        uint256 amountClaimed = IIpRoyaltyVault(royaltyToken).claimRevenueOnBehalf(address(this), WIP);
        if (amountClaimed == 0) revert InsufficientBalanceToClaim();

        uint256 amountToInvestor;
        uint256 amountToScientist;
        uint256 totalClaimedAmount = totalClaimed[ipId];
        uint256 thresholdValue = products[ipId].thresholdValue;
        uint32 thresholdPercent = products[ipId].thresholdPercent;
        if (totalClaimedAmount > thresholdValue) {
            amountToInvestor = amountClaimed * thresholdPercent / MAX_PERCENT;
            amountToScientist = amountClaimed - amountToInvestor;
        } else {
            if (totalClaimedAmount + amountClaimed > thresholdValue) {
                uint256 amountAboveThreshold = totalClaimedAmount + amountClaimed - thresholdValue;
                uint256 amountAboveThresholdForInvestor = amountAboveThreshold * thresholdPercent / MAX_PERCENT;
                amountToInvestor = thresholdValue - totalClaimedAmount + amountAboveThresholdForInvestor;
                amountToScientist = amountAboveThreshold - amountAboveThresholdForInvestor;
            } else {
                amountToInvestor = amountClaimed;
                amountToScientist = 0;
            }
        }

        totalClaimed[ipId] += amountClaimed;

        IERC20(WIP).safeTransfer(scientist, amountToScientist);
        IERC20(WIP).safeTransfer(products[ipId].investor, amountToInvestor);
    }

    /// @notice Returns the amount of royalty tokens required to link a child to a given IP asset
    /// @param ipId The ipId of the IP asset
    /// @param licensePercent The percentage of the license
    /// @return The amount of royalty tokens required to link a child to a given IP asset
    function getPolicyRtsRequiredToLink(address ipId, uint32 licensePercent) external view returns (uint32) {
        return MAX_PERCENT;
    }

    /// @notice IERC165 interface support
    function supportsInterface(bytes4 interfaceId) public view override(ERC165, IERC165) returns (bool) {
        return interfaceId == this.getPolicyRtsRequiredToLink.selector || super.supportsInterface(interfaceId);
    }
}

You can observe the implementation getPolicyRtsRequiredToLink, which in this case has been set to always return 100% (MAX_PERCENT). What this means is that every derivative IP that was created with this external royalty policy has to give 100% of its royalty tokens to the “ExternalRoyaltyPolicy” smart contract.

Once the royalty tokens are in a smart contract, they can be used to claim royalties via the claim function. The final flow of capital, assuming 2M yearly revenue, would look like the diagram below:

royalty 3

Custom Royalty Policies on Story

As it’s possible to imagine by now, there is an entire universe of different royalty policies yet to be created. The External Royalty Policy allows for high expressiveness of breadth of possible royalty rules while keeping the value of having access/benefiting from the licensing and dispute modules, among other benefits of being part of the Story Ecosystem.

If you have any feedback, comments, or ideas, feel free to reach out on Discord or DM the Story Engineers account on X → https://x.com/StoryEngs.

We're excited to see what projects you'll build!

You might also like

Story Network Update

Story Network Update

From Launch to Chapter 2
Tech
19 Aug 2025
Introducing Story’s Security Council

Introducing Story’s Security Council

Story's path to progressive decentralization
Tech
08 Aug 2025
From Prototype to Possibility

From Prototype to Possibility

What we built in Story's internal hackathon
Tech
30 Jul 2025

Subscribe to our newsletter

Thanks for subscribing!

Sign Up