완성된 코드

완성된 코드를 끝까지 따라가보세요.

이 섹션에서는 IP Asset에 대한 지불 방법을 보여줍니다. 이를 수행하는 몇 가지 이유가 있습니다:

  1. 단순히 IP에 “팁”을 주고 싶을 때
  2. 조상 IP와의 라이선스 조건에 따라 특정 비율의 지불금을 전달해야 할 때

두 시나리오 모두에서 아래의 payRoyaltyOnBehalf 함수를 사용하게 됩니다. 이 경우, Royalty Module이 자동으로 다양한 지불 흐름을 처리하여 지불 받는 IPA와 특정 commercialRevShare를 협상한 상위 IP Asset이 자신의 몫을 청구할 수 있도록 합니다.

전제 조건

튜토리얼을 시작하기 전에 완료해야 할 몇 가지 단계가 있습니다.

  1. 완료하세요 자신의 프로젝트 설정하기
  2. 에 대한 기본적인 이해가 필요합니다 Royalty Module
  3. 이 예제를 작동시키기 위해 상업적 수익 공유에 대한 라이선스 조건이 있는 자식 IPA와 부모 IPA

시작하기 전에

함수를 사용하여 IP Asset에 지불할 수 있습니다 payRoyaltyOnBehalfRoyalty Module.

IP Asset에 를 사용하여 지불할 것입니다 MockERC20. 일반적으로 $WIP로 지불하겠지만, 테스트를 위해 일부 토큰을 발행해야 하므로 MockERC20을 사용할 것입니다.

다음 시나리오를 돕기 위해, 자식 IP Asset과 50%의 commercialRevShare를 협상한 부모 IP Asset이 있다고 가정해 봅시다.

화이트리스트에 등록된 수익 토큰

우리 프로토콜에 의해 화이트리스트에 등록된 토큰만 지불(“수익”) 토큰으로 사용될 수 있습니다. MockERC20은 그 토큰 중 하나입니다. 그 목록을 보려면 여기를 참조하세요.

IP Asset에 지불하기

다음과 같이 IP Asset에 지불할 수 있습니다:

Solidity
ROYALTY_MODULE.payRoyaltyOnBehalf(childIpId, address(0), address(MERC20), 10);

이는 10 $MERC20를 의 childIpIdIP 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 계약의 주소를 여러분을 위해 미리 채워 넣었습니다. 하지만 여기에서도 그 주소들을 찾을 수 있습니다: 배포된 스마트 계약

test/5_Royalty.t.sol
// 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에 대해 분쟁을 제기할 수 있습니다. 이는 다음 페이지에서 다룰 것입니다.

곧 출시됩니다!