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.
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.
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.
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.
Important: Approving the Royalty Module
Before you call payRoyaltyOnBehalf, you have to approve the royalty module to spend the tokens for you. In the section below, you will see that we call MERC20.approve(address(ROYALTY_MODULE), 10); or else it will not work.
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
test/5_Royalty.t.sol
Copy
// SPDX-License-Identifier: UNLICENSEDpragma solidity ^0.8.26;import { Test } from "forge-std/Test.sol";// for testing purposes onlyimport { 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.solcontract RoyaltyTest is Test { address internal alice = address(0xa11ce); address internal bob = address(0xb0b); // For addresses, see https://docs.story.foundation/developers/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 have to 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); }}