Pay & Claim Revenue
Learn how to pay an IP Asset and claim revenue in Solidity.
This section demonstrates how to pay an IP Asset. There are a few reasons you would do this:
- you simply want to "tip" an IP
- you have to because your license terms with an ancestor IP require you to forward a certain % of payment
In either scenario, you would use the below payRoyaltyOnBehalf
function. When this happens, the 💸 Royalty Module automatically handles the different payment flows such that parent IP Assets who have negotiated a certain commercialRevShare
with the IPA being paid can claim their due share.
⚠️ Prerequisites
There are a few steps you have to complete before you can start the tutorial.
- Complete the Setup Your Own Project
- Have a basic understanding of the 💸 Royalty Module
- A child IPA and a parent IPA, for which their license terms have a commercial revenue share to make this example work
0. Before We Start
You can pay an IP Asset using the payRoyaltyOnBehalf
function from the 💸 Royalty Module.
You will be paying the IP Asset with MockERC20. Usually you would pay with $WIP, but because we need to mint some tokens to test, we will use MockERC20.
To help with the following scenarios, let's say we have a parent IP Asset that has negotiated a 50% commercialRevShare
with its child IP Asset.
0a. 🪙 Whitelisted Revenue Tokens
Only tokens that are whitelisted by our protocol can be used as payment ("revenue") tokens. MockERC20 is one of those tokens. To see that list, go here.
1. Paying an IP Asset
We can pay an IP Asset like so:
ROYALTY_MODULE.payRoyaltyOnBehalf(childIpId, address(0), address(MERC20), 10);
This will send 10 $MERC20 to the childIpId
's IP Royalty Vault. From there, the child can claim revenue. In the next section, you'll see a working version of this.
2. Claim Revenue
When payments are made, they eventually end up in an IP Asset's IP Royalty Vault. From here, they are claimed/transferred to whoever owns the Royalty Tokens associated with it, which represent a % of revenue share for a given IP Asset's IP Royalty Vault.
The IP Account (the smart contract that represents the 🧩 IP Asset) is what holds 100% of the Royalty Tokens when it's first registered. So usually, it indeed holds most of the Royalty Tokens.
Let's create a test file under test/5_Royalty.t.sol
to see it work and verify the results:
Contract Addresses
We have filled in the addresses from the Story contracts for you. However you can also find the addresses for them here: Deployed Smart Contracts
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.26;
import { Test } from "forge-std/Test.sol";
// for testing purposes only
import { MockIPGraph } from "@storyprotocol/test/mocks/MockIPGraph.sol";
import { IPAssetRegistry } from "@storyprotocol/core/registries/IPAssetRegistry.sol";
import { LicenseRegistry } from "@storyprotocol/core/registries/LicenseRegistry.sol";
import { PILicenseTemplate } from "@storyprotocol/core/modules/licensing/PILicenseTemplate.sol";
import { RoyaltyPolicyLAP } from "@storyprotocol/core/modules/royalty/policies/LAP/RoyaltyPolicyLAP.sol";
import { PILFlavors } from "@storyprotocol/core/lib/PILFlavors.sol";
import { PILTerms } from "@storyprotocol/core/interfaces/modules/licensing/IPILicenseTemplate.sol";
import { LicensingModule } from "@storyprotocol/core/modules/licensing/LicensingModule.sol";
import { LicenseToken } from "@storyprotocol/core/LicenseToken.sol";
import { RoyaltyWorkflows } from "@storyprotocol/periphery/workflows/RoyaltyWorkflows.sol";
import { RoyaltyModule } from "@storyprotocol/core/modules/royalty/RoyaltyModule.sol";
import { MockERC20 } from "@storyprotocol/test/mocks/token/MockERC20.sol";
import { SimpleNFT } from "../src/mocks/SimpleNFT.sol";
// Run this test:
// forge test --fork-url https://aeneid.storyrpc.io/ --match-path test/5_Royalty.t.sol
contract RoyaltyTest is Test {
address internal alice = address(0xa11ce);
address internal bob = address(0xb0b);
// For addresses, see https://docs.story.foundation/docs/deployed-smart-contracts
// Protocol Core - IPAssetRegistry
IPAssetRegistry internal IP_ASSET_REGISTRY = IPAssetRegistry(0x77319B4031e6eF1250907aa00018B8B1c67a244b);
// Protocol Core - LicenseRegistry
LicenseRegistry internal LICENSE_REGISTRY = LicenseRegistry(0x529a750E02d8E2f15649c13D69a465286a780e24);
// Protocol Core - LicensingModule
LicensingModule internal LICENSING_MODULE = LicensingModule(0x04fbd8a2e56dd85CFD5500A4A4DfA955B9f1dE6f);
// Protocol Core - PILicenseTemplate
PILicenseTemplate internal PIL_TEMPLATE = PILicenseTemplate(0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316);
// Protocol Core - RoyaltyPolicyLAP
RoyaltyPolicyLAP internal ROYALTY_POLICY_LAP = RoyaltyPolicyLAP(0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E);
// Protocol Core - LicenseToken
LicenseToken internal LICENSE_TOKEN = LicenseToken(0xFe3838BFb30B34170F00030B52eA4893d8aAC6bC);
// Protocol Core - RoyaltyModule
RoyaltyModule internal ROYALTY_MODULE = RoyaltyModule(0xD2f60c40fEbccf6311f8B47c4f2Ec6b040400086);
// Protocol Periphery - RoyaltyWorkflows
RoyaltyWorkflows internal ROYALTY_WORKFLOWS = RoyaltyWorkflows(0x9515faE61E0c0447C6AC6dEe5628A2097aFE1890);
// Mock - MERC20
MockERC20 internal MERC20 = MockERC20(0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E);
SimpleNFT public SIMPLE_NFT;
uint256 public tokenId;
address public ipId;
uint256 public licenseTermsId;
uint256 public startLicenseTokenId;
address public childIpId;
function setUp() public {
// this is only for testing purposes
// due to our IPGraph precompile not being
// deployed on the fork
vm.etch(address(0x0101), address(new MockIPGraph()).code);
SIMPLE_NFT = new SimpleNFT("Simple IP NFT", "SIM");
tokenId = SIMPLE_NFT.mint(alice);
ipId = IP_ASSET_REGISTRY.register(block.chainid, address(SIMPLE_NFT), tokenId);
licenseTermsId = PIL_TEMPLATE.registerLicenseTerms(
PILFlavors.commercialRemix({
mintingFee: 0,
commercialRevShare: 10 * 10 ** 6, // 10%
royaltyPolicy: address(ROYALTY_POLICY_LAP),
currencyToken: address(MERC20)
})
);
vm.prank(alice);
LICENSING_MODULE.attachLicenseTerms(ipId, address(PIL_TEMPLATE), licenseTermsId);
startLicenseTokenId = LICENSING_MODULE.mintLicenseTokens({
licensorIpId: ipId,
licenseTemplate: address(PIL_TEMPLATE),
licenseTermsId: licenseTermsId,
amount: 2,
receiver: bob,
royaltyContext: "", // for PIL, royaltyContext is empty string
maxMintingFee: 0,
maxRevenueShare: 0
});
// Registers a child IP (owned by Bob) as a derivative of Alice's IP.
uint256 childTokenId = SIMPLE_NFT.mint(bob);
childIpId = IP_ASSET_REGISTRY.register(block.chainid, address(SIMPLE_NFT), childTokenId);
uint256[] memory licenseTokenIds = new uint256[](1);
licenseTokenIds[0] = startLicenseTokenId;
vm.prank(bob);
LICENSING_MODULE.registerDerivativeWithLicenseTokens({
childIpId: childIpId,
licenseTokenIds: licenseTokenIds,
royaltyContext: "", // empty for PIL
maxRts: 0
});
}
/// @notice Pays MERC20 to Bob's IP. Some of this MERC20 is then claimable
/// by Alice's IP.
/// @dev In this case, this contract will act as the 3rd party paying MERC20
/// to Bob (the child IP).
function test_claimAllRevenue() public {
// ADMIN SETUP
// We mint 100 MERC20 to this contract so it has some money to pay.
MERC20.mint(address(this), 100);
// We approve the Royalty Module to spend MERC20 on our behalf, which
// it will do using `payRoyaltyOnBehalf`.
MERC20.approve(address(ROYALTY_MODULE), 10);
// This contract pays 10 MERC20 to Bob's IP.
ROYALTY_MODULE.payRoyaltyOnBehalf(childIpId, address(0), address(MERC20), 10);
// Now that Bob's IP has been paid, Alice can claim her share (1 MERC20, which
// is 10% as specified in the license terms)
address[] memory childIpIds = new address[](1);
address[] memory royaltyPolicies = new address[](1);
address[] memory currencyTokens = new address[](1);
childIpIds[0] = childIpId;
royaltyPolicies[0] = address(ROYALTY_POLICY_LAP);
currencyTokens[0] = address(MERC20);
uint256[] memory amountsClaimed = ROYALTY_WORKFLOWS.claimAllRevenue({
ancestorIpId: ipId,
claimer: ipId,
childIpIds: childIpIds,
royaltyPolicies: royaltyPolicies,
currencyTokens: currencyTokens
});
// Check that 1 MERC20 was claimed by Alice's IP Account
assertEq(amountsClaimed[0], 1);
// Check that Alice's IP Account now has 1 MERC20 in its balance.
assertEq(MERC20.balanceOf(ipId), 1);
// Check that Bob's IP now has 9 MERC20 in its Royalty Vault, which it
// can claim to its IP Account at a later point if he wants.
assertEq(MERC20.balanceOf(ROYALTY_MODULE.ipRoyaltyVaults(childIpId)), 9);
}
}
3. Test Your Code!
Run forge build
. If everything is successful, the command should successfully compile.
Now run the test by executing the following command:
forge test --fork-url https://aeneid.storyrpc.io/ --match-path test/5_Royalty.t.sol
4. Dispute an IP
Congratulations, you claimed revenue using the 💸 Royalty Module!
Now what happens if an IP Asset doesn't pay their due share? We can dispute the IP on-chain, which we will cover on the next page.
Coming soon!
Updated 6 days ago