Setup development environment

Setup the Story Protocol smart contract development environment in just a few minutes

🚧

This is a beta release of Story Protocol

This release of Story Protocol is beta and not yet production ready, which means that we could change the smart contracts or backend infrastructure that at any time without warning. When Story Protocol becomes production ready we will remove this warning and ensure regular updates to this document with formal notice.

In this guide, we will show you how to quickly get started with building on top of Story Protocol. In terms of tooling, we recommend using Foundry for all smart contract development workflows, and a package manager like yarn or npm for dependency management.

Prerequisites

Quickstart

The Story Protocol boilerplate repository provides an example of how to import both of the Story Protocol repositories, and how to create and test a contract for registering an NFT to the IP asset registry. To test this directly, simply do the following:

  • git clone https://github.com/storyprotocol/story-protocol-boilerplate
  • cd story-protocol-boilerplate
  • yarn

Once done, the repository will be setup with both Story Protocol core and periphery contracts installed. You can play around with the contract, and using a valid Sepolia RPC URL, perform a test directly forked off our contracts on Sepolia with the following command:

forge test --fork-url "<your preferred sepolia rpc url>"

And with that, you should have a simple starting codebase to work with for development. Now, if you are more of a power user, the next section will guide you through how to set things up from scratch.

Setup from scratch

To get started with setting a repository up from scratch, first initialize a project using a package manager. We recommend using yarn. Run the following in a new directory of your choice:

yarn init

Then, setup foundry using the following command:

forge init --force

We specify --force since we are initializing within a repository. Once done, do some cleaning up on the dependency management side - we recommend using yarn to manage dependencies directly, so remove those conflicting with forge by running the following:

rm -rf lib/ .gitmodules

Now, we are ready to start installing our dependencies. To incorporate the Story Protocol core and periphery modules, run the following to have them added to your package.json. We will also install openzeppelin and erc6551 as a dependency for the contract and test.

# note: you can run them one-by-one, or all at once
yarn add @story-protocol/core@https://github.com/storyprotocol/protocol-core#beta-rc5
yarn add @story-protocol/periphery@https://github.com/storyprotocol/protocol-periphery#main
yarn add @openzeppelin/contracts
yarn add erc6551

Additionally, for working with Foundry's testkit, we also recommend adding the following as devDependencies in your package.json:

...
"devDependencies": {
	"ds-test": "https://github.com/dapphub/ds-test",
	"forge-std": "github:foundry-rs/forge-std#v1.7.6"
},
...

Run yarn. Then, create a file in the root folder named remappings.txt and paste the following:

ds-test/=node_modules/ds-test/src/
forge-std/=node_modules/forge-std/src/
@story-protocol/core/=node_modules/@story-protocol/core/contracts/
@story-protocol/periphery/=node_modules/@story-protocol/periphery/contracts/
@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/
erc6551/=node_modules/erc6551/

we are ready to build a simple test registration contract.

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 { IP } from "@story-protocol/core/lib/IP.sol";
import { IPAssetRegistry } from "@story-protocol/core/registries/IPAssetRegistry.sol";
import { IPResolver } from "@story-protocol/core/resolvers/IPResolver.sol";

contract IPARegistrar {
    address public immutable NFT;
    address public immutable IP_RESOLVER;
    IPAssetRegistry public immutable IPA_REGISTRY;

    constructor(
        address ipAssetRegistry,
        address resolver,
        address nft
    ) {
        IPA_REGISTRY = IPAssetRegistry(ipAssetRegistry);
        IP_RESOLVER = resolver;
        NFT = nft;
    }

    function register(
				string memory ipName,
        uint256 tokenId 
    ) external returns (address) {
        bytes memory metadata = abi.encode(
            IP.MetadataV1({
                name: ipName,
                hash: "",
                registrationDate: uint64(block.timestamp),
                registrant: msg.sender,
                uri: ""
            })
        );
        return IPA_REGISTRY.register(block.chainid, NFT, tokenId, IP_RESOLVER, true, metadata);
    }
}

Now, run the following command:

forge build

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

Testing a 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 { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import { IPAssetRegistry } from "@story-protocol/core/registries/IPAssetRegistry.sol";
import { IPResolver } from "@story-protocol/core/resolvers/IPResolver.sol";

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

contract MockERC721 is ERC721 {
    uint256 public totalSupply = 0;

    constructor(string memory name, string memory symbol) ERC721(name, symbol) {}

    function mint() external returns (uint256 id) {
        id = totalSupply++;
        _mint(msg.sender, id);
    }
}

contract IPARegistrarTest is Test {
    address public constant IPA_REGISTRY_ADDR = address(0x7567ea73697De50591EEc317Fe2b924252c41608);
    address public constant IP_RESOLVER_ADDR = address(0xEF808885355B3c88648D39c9DB5A0c08D99C6B71);

    MockERC721 public NFT;
    IPARegistrar public ipaRegistrar;

    function setUp() public {
        NFT = new MockERC721("Story Mock NFT", "STORY");
        ipaRegistrar = new IPARegistrar(
            IPA_REGISTRY_ADDR,
            IP_RESOLVER_ADDR,
            address(NFT)
        );
    }

    function test_IPARegistration() public {
        uint256 tokenId = NFT.mint();
        address ipId = ipaRegistrar.register("test", tokenId);
        assertTrue(IPAssetRegistry(IPA_REGISTRY_ADDR).isRegistered(ipId), "not registered");
    }
}

Note that in this test, we have hardcoded addresses of Story Protocol contracts deployed on Sepolia, 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 "<your preferred sepolia rpc url>"

If everything was setup properly, the test should pass! Now that you have a good understanding of how to setup your environment to work with Story Protocol's contracts, the remainder of the guide will go over more concrete use-cases around IP and how to build them.