Register an NFT as an IP Asset

Let's see how we can perform a barebones registration using the IP Asset registry directly. Before you dive into the code, make sure you understand what an IP Asset is and how they get registered using the IP Asset Registry.

Creating a simple registration contract

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

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

import { IPAssetRegistry } from "@storyprotocol/core/registries/IPAssetRegistry.sol";
import { StoryProtocolGateway } from "@storyprotocol/periphery/StoryProtocolGateway.sol";
import { IStoryProtocolGateway as ISPG } from "@storyprotocol/periphery/interfaces/IStoryProtocolGateway.sol";
import { SPGNFT } from "@storyprotocol/periphery/SPGNFT.sol";

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

/// @notice Register an NFT as an IP Account.
contract IPARegistrar {
    IPAssetRegistry public immutable IP_ASSET_REGISTRY;
    StoryProtocolGateway public immutable SPG;
    SimpleNFT public immutable SIMPLE_NFT;
    SPGNFT public immutable SPG_NFT;

    constructor(address ipAssetRegistry, address storyProtocolGateway) {
        IP_ASSET_REGISTRY = IPAssetRegistry(ipAssetRegistry);
        SPG = StoryProtocolGateway(storyProtocolGateway);
        // Create a new Simple NFT collection
        SIMPLE_NFT = new SimpleNFT("Simple IP NFT", "SIM");
        // Create a new NFT collection via SPG
        SPG_NFT = SPGNFT(
            SPG.createCollection({
                name: "SPG IP NFT",
                symbol: "SPIN",
                maxSupply: 10000,
                mintFee: 0,
                mintFeeToken: address(0),
                owner: address(this)
            })
        );
    }

    /// @notice Mint an IP NFT and register it as an IP Account via Story Protocol core.
    /// @return ipId The address of the IP Account.
    /// @return tokenId The token ID of the IP NFT.
    function mintIp() external returns (address ipId, uint256 tokenId) {
        tokenId = SIMPLE_NFT.mint(msg.sender);
        ipId = IP_ASSET_REGISTRY.register(block.chainid, address(SIMPLE_NFT), tokenId);
    }

    /// @notice Mint an IP NFT and register it as an IP Account via Story Protocol Gateway (periphery).
    /// @dev Requires the collection to be created via SPG (createCollection).
    function spgMintIp() external returns (address ipId, uint256 tokenId) {
        (ipId, tokenId) = SPG.mintAndRegisterIp(
            address(SPG_NFT),
            msg.sender,
            ISPG.IPMetadata({
                metadataURI: "ip-metadata-uri",
                metadataHash: keccak256("ip-metadata-uri-content"),
                nftMetadataHash: keccak256("nft-metadata-uri-content")
            })
        );
    }
}

Create another new file under ./src/SimpleNFT.sol and paste the following:

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

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol";

contract SimpleNFT is ERC721, Ownable {
    uint256 public nextTokenId;

    constructor(string memory name, string memory symbol) ERC721(name, symbol) Ownable(msg.sender) {}

    function mint(address to) public onlyOwner returns (uint256) {
        uint256 tokenId = nextTokenId++;
        _mint(to, tokenId);
        return 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 file in ./test/IPARegistrar.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 { ISPGNFT } from "@storyprotocol/periphery/interfaces/ISPGNFT.sol";

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

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

    // Protocol Core v1 addresses
    // (see https://docs.storyprotocol.xyz/docs/deployed-smart-contracts)
    address internal ipAssetRegistryAddr = 0xd43fE0d865cb5C26b1351d3eAf2E3064BE3276F6;
    // Protocol Periphery v1 addresses
    // (see https://github.com/storyprotocol/protocol-periphery-v1/blob/main/deploy-out/deployment-11155111.json)
    address internal storyProtocolGatewayAddr = 0x69415CE984A79a3Cfbe3F51024C63b6C107331e3;

    IPAssetRegistry public ipAssetRegistry;
    ISPGNFT public spgNft;

    IPARegistrar public ipaRegistrar;
    SimpleNFT public simpleNft;

    function setUp() public {
        ipAssetRegistry = IPAssetRegistry(ipAssetRegistryAddr);
        ipaRegistrar = new IPARegistrar(ipAssetRegistryAddr, storyProtocolGatewayAddr);
        simpleNft = SimpleNFT(ipaRegistrar.SIMPLE_NFT());
        spgNft = ISPGNFT(ipaRegistrar.SPG_NFT());

        vm.label(address(ipAssetRegistry), "IPAssetRegistry");
        vm.label(address(simpleNft), "SimpleNFT");
        vm.label(address(spgNft), "SPGNFT");
        vm.label(address(0x000000006551c19487814612e58FE06813775758), "ERC6551Registry");
    }

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

        vm.prank(alice);
        (address ipId, uint256 tokenId) = ipaRegistrar.mintIp();

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

    function test_spgMintIp() public {
        uint256 expectedTokenId = spgNft.totalSupply() + 1;
        address expectedIpId = ipAssetRegistry.ipId(block.chainid, address(spgNft), expectedTokenId);

        vm.prank(alice);
        (address ipId, uint256 tokenId) = ipaRegistrar.spgMintIp();

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

Note that we have hardcoded the addresses of Story Protocol contracts deployed on Sepolia in order to show you a more useful example where you can fork test contracts directly.

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!