Skip to content

Commit

Permalink
Introduce Story NFT (#87)
Browse files Browse the repository at this point in the history
* feat: introduce Story NFT

* fix(deploy-script): remove test deployment logic

* chore: lint & rm extra imports
  • Loading branch information
sebsadface authored Oct 9, 2024
1 parent 6acb0db commit ef4b000
Show file tree
Hide file tree
Showing 16 changed files with 2,057 additions and 27 deletions.
22 changes: 22 additions & 0 deletions contracts/interfaces/story-nft/IERC5192.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

/// @title Minimal Soulbound NFT Interface
/// @notice Minimal interface for soulbinding EIP-721 NFTs
interface IERC5192 {
/// @notice Emitted when the locking status is changed to locked.
/// @dev If a token is minted and the status is locked, this event should be emitted.
/// @param tokenId The identifier for a token.
event Locked(uint256 tokenId);

/// @notice Emitted when the locking status is changed to unlocked.
/// @dev If a token is minted and the status is unlocked, this event should be emitted.
/// @param tokenId The identifier for a token.
event Unlocked(uint256 tokenId);

/// @notice Returns the locking status of an Soulbound Token
/// @dev SBTs assigned to zero address are considered invalid, and queries
/// about them do throw.
/// @param tokenId The identifier for an SBT.
function locked(uint256 tokenId) external view returns (bool);
}
67 changes: 67 additions & 0 deletions contracts/interfaces/story-nft/IOrgNFT.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol";

/// @title Organization NFT Interface
/// @notice Each organization token represents a Story ecosystem project.
/// The root organization token represents Story.
/// Each organization token register as a IP on Story and is a derivative of the root organization IP.
interface IOrgNFT is IERC721Metadata {
////////////////////////////////////////////////////////////////////////////
// Errors //
////////////////////////////////////////////////////////////////////////////
/// @notice Caller is not the StoryNFTFactory contract.
/// @param caller The address of the caller.
/// @param storyNftFactory The address of the `StoryNFTFactory` contract.
error OrgNFT__CallerNotStoryNFTFactory(address caller, address storyNftFactory);

/// @notice Root organization NFT has already been minted.
error OrgNFT__RootOrgNftAlreadyMinted();

/// @notice Root organization NFT has not been minted yet (`mintRootOrgNft` has not been called).
error OrgNFT__RootOrgNftNotMinted();

/// @notice Zero address provided as a param to OrgNFT functions.
error OrgNFT__ZeroAddressParam();

////////////////////////////////////////////////////////////////////////////
// Events //
////////////////////////////////////////////////////////////////////////////
/// @notice Emitted when a organization token minted.
/// @param recipient The address of the recipient of the organization token.
/// @param orgNft The address of the organization NFT.
/// @param tokenId The ID of the minted organization token.
/// @param orgIpId The ID of the organization IP.
event OrgNFTMinted(address recipient, address orgNft, uint256 tokenId, address orgIpId);

////////////////////////////////////////////////////////////////////////////
// Functions //
////////////////////////////////////////////////////////////////////////////
/// @notice Mints the root organization token and register it as an IP.
/// @param recipient The address of the recipient of the root organization token.
/// @param tokenURI The URI of the root organization token.
/// @return rootOrgTokenId The ID of the root organization token.
/// @return rootOrgIpId The ID of the root organization IP.
function mintRootOrgNft(
address recipient,
string memory tokenURI
) external returns (uint256 rootOrgTokenId, address rootOrgIpId);

/// @notice Mints a organization token, register it as an IP,
/// and makes the IP as a derivative of the root organization IP.
/// @param recipient The address of the recipient of the minted organization token.
/// @param tokenURI The URI of the minted organization token.
/// @return orgTokenId The ID of the minted organization token.
/// @return orgIpId The ID of the organization IP.
function mintOrgNft(
address recipient,
string memory tokenURI
) external returns (uint256 orgTokenId, address orgIpId);

/// @notice Returns the ID of the root organization IP.
function getRootOrgIpId() external view returns (address);

/// @notice Returns the total supply of OrgNFT.
function totalSupply() external view returns (uint256);
}
70 changes: 70 additions & 0 deletions contracts/interfaces/story-nft/IStoryBadgeNFT.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol";

import { IERC5192 } from "./IERC5192.sol";
import { IStoryNFT } from "./IStoryNFT.sol";

/// @title Story Badge NFT Interface
/// @notice A Story Badge NFT is a soulbound NFT that has an unified token URI for all tokens.
interface IStoryBadgeNFT is IStoryNFT, IERC5192, IERC721Metadata {
////////////////////////////////////////////////////////////////////////////
// Errors //
////////////////////////////////////////////////////////////////////////////
/// @notice Invalid whitelist signature.
error StoryBadgeNFT__InvalidSignature();

/// @notice The provided whitelist signature is already used.
error StoryBadgeNFT__SignatureAlreadyUsed();

/// @notice Badges are soulbound, cannot be transferred.
error StoryBadgeNFT__TransferLocked();

/// @notice Zero address provided as a param to StoryBadgeNFT functions.
error StoryBadgeNFT__ZeroAddressParam();

////////////////////////////////////////////////////////////////////////////
// Structs //
////////////////////////////////////////////////////////////////////////////
/// @notice Struct for custom data for initializing the StoryBadgeNFT contract.
/// @param tokenURI The token URI for all the badges (follows OpenSea metadata standard).
/// @param signer The signer of the whitelist signatures.
struct CustomInitParams {
string tokenURI;
address signer;
}

////////////////////////////////////////////////////////////////////////////
// Events //
////////////////////////////////////////////////////////////////////////////
/// @notice Emitted when a badge NFT is minted.
/// @param recipient The address of the recipient of the badge NFT.
/// @param tokenId The token ID of the minted badge NFT.
/// @param ipId The ID of the badge NFT IP.
event StoryBadgeNFTMinted(address recipient, uint256 tokenId, address ipId);

/// @notice Emitted when the signer is updated.
/// @param signer The new signer address.
event StoryBadgeNFTSignerUpdated(address signer);

////////////////////////////////////////////////////////////////////////////
// Functions //
////////////////////////////////////////////////////////////////////////////
/// @notice Mints a badge for the given recipient, registers it as an IP,
/// and makes it a derivative of the organization IP.
/// @param recipient The address of the recipient of the badge NFT.
/// @param signature The signature from the whitelist signer. This signautre is genreated by having the whitelist
/// signer sign the caller's address (msg.sender) for this `mint` function.
/// @return tokenId The token ID of the minted badge NFT.
/// @return ipId The ID of the badge NFT IP.
function mint(address recipient, bytes calldata signature) external returns (uint256 tokenId, address ipId);

/// @notice Updates the whitelist signer.
/// @param signer_ The new whitelist signer address.
function setSigner(address signer_) external;

/// @notice Updates the unified token URI for all badges.
/// @param tokenURI_ The new token URI.
function setTokenURI(string memory tokenURI_) external;
}
48 changes: 48 additions & 0 deletions contracts/interfaces/story-nft/IStoryNFT.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

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

/// @title IStoryNFT
/// @notice Interface for StoryNFT contracts.
interface IStoryNFT is IERC721 {
////////////////////////////////////////////////////////////////////////////
// Errors //
////////////////////////////////////////////////////////////////////////////
/// @notice Zero address provided as a param to BaseStoryNFT constructor.
error BaseStoryNFT__ZeroAddressParam();

////////////////////////////////////////////////////////////////////////////
// Structs //
////////////////////////////////////////////////////////////////////////////
/// @notice Struct for initializing StoryNFT contracts.
/// @param owner The address of the owner of this collection.
/// @param name The name of the collection.
/// @param symbol The symbol of the collection.
/// @param contractURI The contract URI of the collection (follows OpenSea contract-level metadata standard).
/// @param baseURI The base URI of the collection (see {ERC721URIStorage-tokenURI} for how it is used).
/// @param customInitData Custom data to initialize the StoryNFT.
struct StoryNftInitParams {
address owner;
string name;
string symbol;
string contractURI;
string baseURI;
bytes customInitData;
}

////////////////////////////////////////////////////////////////////////////
// Functions //
////////////////////////////////////////////////////////////////////////////
/// @notice Initializes the StoryNFT.
/// @param orgTokenId_ The token ID of the organization NFT.
/// @param orgIpId_ The ID of the organization IP.
/// @param initParams The initialization parameters for StoryNFT {see {StoryNftInitParams}}.
function initialize(uint256 orgTokenId_, address orgIpId_, StoryNftInitParams calldata initParams) external;

/// @notice Returns the current total supply of the collection.
function totalSupply() external view returns (uint256);

/// @notice Returns the contract URI of the collection (follows OpenSea contract-level metadata standard).
function contractURI() external view returns (string memory);
}
148 changes: 148 additions & 0 deletions contracts/interfaces/story-nft/IStoryNFTFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

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

/// @title Story NFT Factory Interface
/// @notice Story NFT Factory is the entrypoint for creating new Story NFT collections.
interface IStoryNFTFactory {
////////////////////////////////////////////////////////////////////////////
// Errors //
////////////////////////////////////////////////////////////////////////////
/// @notice Invalid signature provided to StoryNFTFactory functions.
/// @param signature The signature that is invalid.
error StoryNFTFactory__InvalidSignature(bytes signature);

/// @notice NftTemplate is not whitelisted to be used as a StoryNFT.
/// @param nftTemplate The NFT template that is not whitelisted.
error StoryNFTFactory__NftTemplateNotWhitelisted(address nftTemplate);

/// @notice Organization is already deployed by the StoryNFTFactory.
/// @param orgName The name of the organization that is already deployed.
/// @param deployedStoryNft The address of the already deployed StoryNFT for the organization.
error StoryNFTFactory__OrgAlreadyDeployed(string orgName, address deployedStoryNft);

/// @notice Organization is not found in the StoryNFTFactory.
/// @param orgName The name of the organization that is not found.
error StoryNFTFactory__OrgNotFoundByOrgName(string orgName);

/// @notice Organization is not found in the StoryNFTFactory.
/// @param orgTokenId The token ID of the organization that is not found.
error StoryNFTFactory__OrgNotFoundByOrgTokenId(uint256 orgTokenId);

/// @notice Organization is not found in the StoryNFTFactory.
/// @param orgIpId The ID of the organization IP that is not found.
error StoryNFTFactory__OrgNotFoundByOrgIpId(address orgIpId);

/// @notice Signature is already used to deploy a StoryNFT.
/// @param signature The signature that is already used.
error StoryNFTFactory__SignatureAlreadyUsed(bytes signature);

/// @notice BaseStoryNFT is not supported by the StoryNFTFactory.
/// @param tokenContract The address of the token contract that does not implement IStoryNFT.
error StoryNFTFactory__UnsupportedIStoryNFT(address tokenContract);

/// @notice Zero address provided as a param to StoryNFTFactory functions.
error StoryNFTFactory__ZeroAddressParam();

////////////////////////////////////////////////////////////////////////////
// Events //
////////////////////////////////////////////////////////////////////////////
/// @notice Emitted when the default StoryNFT template is updated.
/// @param defaultStoryNftTemplate The new default StoryNFT template.
event StoryNFTFactoryDefaultStoryNftTemplateUpdated(address defaultStoryNftTemplate);

/// @notice Emitted when a new orgnization NFT is minted and a new StoryNFT associated with it is deployed.
/// @param orgName The name of the organization.
/// @param orgNft The address of the organization NFT.
/// @param orgTokenId The token ID of the organization NFT.
/// @param orgIpId The ID of the organization IP.
/// @param storyNft The address of the deployed StoryNFT.
event StoryNftDeployed(string orgName, address orgNft, uint256 orgTokenId, address orgIpId, address storyNft);

/// @notice Emitted when the signer of the StoryNFTFactory is updated.
/// @param signer The new signer of the StoryNFTFactory.
event StoryNFTFactorySignerUpdated(address signer);

/// @notice Emitted when a new Story NFT template is whitelisted.
/// @param nftTemplate The new Story NFT template that is whitelisted to be used in StoryNFTFactory.
event StoryNFTFactoryNftTemplateWhitelisted(address nftTemplate);

////////////////////////////////////////////////////////////////////////////
// Functions //
////////////////////////////////////////////////////////////////////////////
/// @notice Mints a new organization NFT and deploys (creates a clone of) `storyNftTemplate` as the StoryNFT
/// associated with the new organization NFT.
/// @param storyNftTemplate The address of a whitelisted StoryNFT template to be cloned.
/// @param orgNftRecipient The address of the recipient of the organization NFT.
/// @param orgName The name of the organization.
/// @param orgTokenURI The token URI of the organization NFT.
/// @param signature The signature from the StoryNFTFactory's whitelist signer. This signautre is genreated by
/// having the whitelist signer sign the caller's address (msg.sender) for this `deployStoryNft` function.
/// @param storyNftInitParams The initialization parameters for StoryNFT {see {IStoryNFT-StoryNftInitParams}}.
/// @return orgNft The address of the organization NFT.
/// @return orgTokenId The token ID of the organization NFT.
/// @return orgIpId The ID of the organization IP.
/// @return storyNft The address of the dployed StoryNFT
function deployStoryNft(
address storyNftTemplate,
address orgNftRecipient,
string calldata orgName,
string calldata orgTokenURI,
bytes calldata signature,
IStoryNFT.StoryNftInitParams calldata storyNftInitParams
) external returns (address orgNft, uint256 orgTokenId, address orgIpId, address storyNft);

/// @notice Mints a new organization NFT and deploys (creates a clone of) `storyNftTemplate` as the StoryNFT
/// associated with the new organization NFT.
/// @dev Enforced to be only callable by the protocol admin in governance.
/// @param storyNftTemplate The address of a whitelisted StoryNFT template to be cloned.
/// @param orgNftRecipient The address of the recipient of the organization NFT.
/// @param orgName The name of the organization.
/// @param orgTokenURI The token URI of the organization NFT.
/// @param storyNftInitParams The initialization parameters for StoryNFT {see {IStoryNFT-StoryNftInitParams}}.
/// @param isRootOrg Whether the organization is the root organization.
/// @return orgNft The address of the organization NFT.
/// @return orgTokenId The token ID of the organization NFT.
/// @return orgIpId The ID of the organization IP.
/// @return storyNft The address of the dployed StoryNFT
function deployStoryNftByAdmin(
address storyNftTemplate,
address orgNftRecipient,
string calldata orgName,
string calldata orgTokenURI,
IStoryNFT.StoryNftInitParams calldata storyNftInitParams,
bool isRootOrg
) external returns (address orgNft, uint256 orgTokenId, address orgIpId, address storyNft);

/// @notice Sets the default StoryNFT template of the StoryNFTFactory.
/// @param defaultStoryNftTemplate The new default StoryNFT template.
function setDefaultStoryNftTemplate(address defaultStoryNftTemplate) external;

/// @notice Sets the signer of the StoryNFTFactory.
/// @param signer The new signer of the StoryNFTFactory.
function setSigner(address signer) external;

/// @notice Whitelists a new StoryNFT template.
/// @param storyNftTemplate The new StoryNFT template to be whitelisted.
function whitelistNftTemplate(address storyNftTemplate) external;

/// @notice Returns the default StoryNFT template address.
function getDefaultStoryNftTemplate() external view returns (address);

/// @notice Returns the address of the StoryNFT for a given organization name.
/// @param orgName The name of the organization.
function getStoryNftAddressByOrgName(string calldata orgName) external view returns (address);

/// @notice Returns the address of the StoryNFT for a given organization token ID.
/// @param orgTokenId The token ID of the organization.
function getStoryNftAddressByOrgTokenId(uint256 orgTokenId) external view returns (address);

/// @notice Returns the address of the StoryNFT for a given organization IP ID.
/// @param orgIpId The ID of the organization IP.
function getStoryNftAddressByOrgIpId(address orgIpId) external view returns (address);

/// @notice Returns whether a given StoryNFT template is whitelisted.
/// @param storyNftTemplate The address of the StoryNFT template.
function isNftTemplateWhitelisted(address storyNftTemplate) external view returns (bool);
}
Loading

0 comments on commit ef4b000

Please sign in to comment.