Adding License Terms to an IP Asset

"License Terms" represent permissions and licensing configurations around how an IP Asset may be licensed. For example, Alice may want to register their digital art as an IP Asset and attach License Terms which stipulates that any derivatives must not impose any royalties.

By creating such a License Term, Alice's IP Asset becomes eligible for licensing creation. Users who then wish to create derivatives of Alice's IP may then mint License Tokens, which can be burned to enroll their IPs as derivative IP Assets of Alice's original artwork. We will cover how to create License Tokens on the next page.

Adding License Terms to an IP Asset is a two-step process:

  1. User must register a License Template into the protocol, such as the Programmable IP License (PIL💊) which generates PIL Terms. If you wish to use the PIL, you obviously don't have to do this step.
  2. User must attach a selected combination of License Terms (e.g. Commercial Remix PIL terms) to an existing IP Asset.


For v1.0, you can only add new License Terms to an IP Asset if it is not a derivative (ie. the IPA does not have a parent). Likewise if an IP Asset already has License Terms attached, it cannot be registered as a derivative.

Now, let's see how we can do this programmatically below.

Creating a contract to attach License Terms

This section assumes that you have SimpleNFT.sol and the development environment setup from previous pages.

Create a new file under ./src/IPALicenseTerms.sol and paste the following:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.23;

import { IPAssetRegistry } from "@storyprotocol/core/registries/IPAssetRegistry.sol";
import { LicensingModule } from "@storyprotocol/core/modules/licensing/LicensingModule.sol";
import { PILicenseTemplate } from "@storyprotocol/core/modules/licensing/PILicenseTemplate.sol";

import { SimpleNFT } from "./SimpleNFT.sol";

/// @notice Attach a Selected Programmable IP License Terms to an IP Account.
contract IPALicenseTerms {
    IPAssetRegistry public immutable IP_ASSET_REGISTRY;
    LicensingModule public immutable LICENSING_MODULE;
    PILicenseTemplate public immutable PIL_TEMPLATE;
    SimpleNFT public immutable SIMPLE_NFT;

    constructor(address ipAssetRegistry, address licensingModule, address pilTemplate) {
        IP_ASSET_REGISTRY = IPAssetRegistry(ipAssetRegistry);
        LICENSING_MODULE = LicensingModule(licensingModule);
        PIL_TEMPLATE = PILicenseTemplate(pilTemplate);
        // Create a new Simple NFT collection
        SIMPLE_NFT = new SimpleNFT("Simple IP NFT", "SIM");

    function attachLicenseTerms() external returns (address ipId, uint256 tokenId) {
        // First, mint an NFT and register it as an IP Account.
        // Note that first we mint the NFT to this contract for ease of attaching license terms.
        // We will transfer the NFT to the msg.sender at last.
        tokenId =;
        ipId = IP_ASSET_REGISTRY.register(block.chainid, address(SIMPLE_NFT), tokenId);

        // Then, attach a selection of license terms from the PILicenseTemplate, which is already registered.
        // Note that licenseTermsId = 1 is a random selection of license terms already registered by another user.
        LICENSING_MODULE.attachLicenseTerms(ipId, address(PIL_TEMPLATE), 1);

        // Finally, transfer the NFT to the msg.sender.
        SIMPLE_NFT.transferFrom(address(this), msg.sender, tokenId);

Now, run the following command:

forge build

If everything is successful, the command should successfully compile. We can now test the contract.

Testing our contract

Create another new file under ./test/IPALicenseTerms.t.sol and paste the following:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.23;

import { Test } from "forge-std/Test.sol";
import { IPAssetRegistry } from "@storyprotocol/core/registries/IPAssetRegistry.sol";
import { LicenseRegistry } from "@storyprotocol/core/registries/LicenseRegistry.sol";

import { IPALicenseTerms } from "../src/IPALicenseTerms.sol";
import { SimpleNFT } from "../src/SimpleNFT.sol";

contract IPALicenseTermsTest is Test {
    address internal alice = address(0xa11ce);

    // Protocol Core v1 addresses
    // (see
    address internal ipAssetRegistryAddr = 0xd43fE0d865cb5C26b1351d3eAf2E3064BE3276F6;
    address internal licensingModuleAddr = 0xe89b0EaA8a0949738efA80bB531a165FB3456CBe;
    address internal licenseRegistryAddr = 0x4f4b1bf7135C7ff1462826CCA81B048Ed19562ed;
    address internal pilTemplateAddr = 0x260B6CB6284c89dbE660c0004233f7bB99B5edE7;

    IPAssetRegistry public ipAssetRegistry;
    LicenseRegistry public licenseRegistry;

    IPALicenseTerms public ipaLicenseTerms;
    SimpleNFT public simpleNft;

    function setUp() public {
        ipAssetRegistry = IPAssetRegistry(ipAssetRegistryAddr);
        licenseRegistry = LicenseRegistry(licenseRegistryAddr);
        ipaLicenseTerms = new IPALicenseTerms(ipAssetRegistryAddr, licensingModuleAddr, pilTemplateAddr);
        simpleNft = SimpleNFT(ipaLicenseTerms.SIMPLE_NFT());

        vm.label(address(ipAssetRegistryAddr), "IPAssetRegistry");
        vm.label(address(licensingModuleAddr), "LicensingModule");
        vm.label(address(licenseRegistryAddr), "LicenseRegistry");
        vm.label(address(pilTemplateAddr), "PILicenseTemplate");
        vm.label(address(simpleNft), "SimpleNFT");
        vm.label(address(0x000000006551c19487814612e58FE06813775758), "ERC6551Registry");

    function test_attachLicenseTerms() public {
        uint256 expectedTokenId = simpleNft.nextTokenId();
        address expectedIpId = ipAssetRegistry.ipId(block.chainid, address(simpleNft), expectedTokenId);

        address expectedLicenseTemplate = pilTemplateAddr;
        uint256 expectedLicenseTermsId = 1;

        (address ipId, uint256 tokenId) = ipaLicenseTerms.attachLicenseTerms();

        assertEq(ipId, expectedIpId);
        assertEq(tokenId, expectedTokenId);
        assertEq(simpleNft.ownerOf(tokenId), alice);

        assertTrue(licenseRegistry.hasIpAttachedLicenseTerms(ipId, expectedLicenseTemplate, expectedLicenseTermsId));
        assertEq(licenseRegistry.getAttachedLicenseTermsCount(ipId), 1);

        (address licenseTemplate, uint256 licenseTermsId) = licenseRegistry.getAttachedLicenseTerms({
            ipId: ipId,
            index: 0
        assertEq(licenseTemplate, expectedLicenseTemplate);
        assertEq(licenseTermsId, expectedLicenseTermsId);

To test this out, simply run the following command:

forge test --fork-url $SEPOLIA_RPC_URL

If everything was set up properly, the test should pass!