이 섹션에서는 IP Asset에 대한 지불 방법을 보여줍니다. 이를 수행하는 몇 가지 이유가 있습니다:
- 단순히 IP에 “팁”을 주고 싶을 때
- 조상 IP와의 라이선스 조건에 따라 특정 비율의 지불금을 전달해야 할 때
두 시나리오 모두에서 아래의 payRoyaltyOnBehalf
함수를 사용하게 됩니다. 이 경우, Royalty Module이 자동으로 다양한 지불 흐름을 처리하여 지불 받는 IPA와 특정 commercialRevShare
를 협상한 상위 IP Asset이 자신의 몫을 청구할 수 있도록 합니다.
전제 조건
튜토리얼을 시작하기 전에 완료해야 할 몇 가지 단계가 있습니다.
- 완료하세요 자신의 프로젝트 설정하기
- 에 대한 기본적인 이해가 필요합니다 Royalty Module
- 이 예제를 작동시키기 위해 상업적 수익 공유에 대한 라이선스 조건이 있는 자식 IPA와 부모 IPA
시작하기 전에
함수를 사용하여 IP Asset에 지불할 수 있습니다 payRoyaltyOnBehalf
의 Royalty Module.
IP Asset에 를 사용하여 지불할 것입니다 MockERC20. 일반적으로 $WIP로 지불하겠지만, 테스트를 위해 일부 토큰을 발행해야 하므로 MockERC20을 사용할 것입니다.
다음 시나리오를 돕기 위해, 자식 IP Asset과 50%의 commercialRevShare
를 협상한 부모 IP Asset이 있다고 가정해 봅시다.
화이트리스트에 등록된 수익 토큰
우리 프로토콜에 의해 화이트리스트에 등록된 토큰만 지불(“수익”) 토큰으로 사용될 수 있습니다. MockERC20은 그 토큰 중 하나입니다. 그 목록을 보려면 여기를 참조하세요.
IP Asset에 지불하기
다음과 같이 IP Asset에 지불할 수 있습니다:
ROYALTY_MODULE.payRoyaltyOnBehalf(childIpId, address(0), address(MERC20), 10);
이는 10 $MERC20를 의 childIpId
의 IP Royalty Vault. 거기서 자식이 수익을 청구할 수 있습니다. 다음 섹션에서 이것의 작동 버전을 볼 수 있습니다.
Important: Approving the Royalty Module
를 호출하기 전에 payRoyaltyOnBehalf
, 로열티 모듈이 당신을 위해 토큰을 사용할 수 있도록 승인해야 합니다. 아래 섹션에서 우리가 MERC20.approve(address(ROYALTY_MODULE), 10);
를 호출하는 것을 볼 수 있습니다. 그렇지 않으면 작동하지 않을 것입니다.
수익 청구
지불이 이루어지면 결국 IP 자산의 IP Royalty Vault에 도달합니다. 여기서 그들은 해당 IP 자산의 IP Royalty Vault에 대한 수익 공유 %를 나타내는 관련 로열티 토큰을 소유한 사람에게 청구/이전됩니다.
IP 계정 (IP Asset을 나타내는 스마트 계약)은 처음 등록될 때 100%의 로열티 토큰을 보유합니다. 따라서 일반적으로 대부분의 로열티 토큰을 보유하고 있습니다.
에서 테스트 파일을 만들어 test/5_Royalty.t.sol
작동하는 것을 보고 결과를 확인해 봅시다:
계약 주소
Story 계약의 주소를 여러분을 위해 미리 채워 넣었습니다. 하지만 여기에서도 그 주소들을 찾을 수 있습니다: 배포된 스마트 계약
// 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/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);
}
}
코드를 테스트하세요!
를 실행하세요 forge build
. 모든 것이 성공적이라면, 명령이 성공적으로 컴파일될 것입니다.
이제 다음 명령을 실행하여 테스트를 실행하세요:
forge test --fork-url https://aeneid.storyrpc.io/ --match-path test/5_Royalty.t.sol
IP 분쟁
축하합니다, Royalty Module을 사용하여 수익을 청구했습니다!
이제 IP 자산이 그들의 몫을 지불하지 않으면 어떻게 될까요? 우리는 온체인에서 IP에 대해 분쟁을 제기할 수 있습니다. 이는 다음 페이지에서 다룰 것입니다.