Skip to content

Commit

Permalink
Introduce Workflows and Group IPA Features (storyprotocol#43)
Browse files Browse the repository at this point in the history
* chore: update changelog

* chore: version bump and package update

* chore: remove sepolia references

* chore: add missing comments for SPGNFTLib

* feat(spg): add licensing helper library

* feat(spg): add metadata helper library

* feat(spg): add permissions helper library

* chore: update ISPG comments

* refactor: add base workflow and refactor spg to use helper lib

* feat: add grouping workflows

* test: refactor & add grouping tests

* feat(script): update scripts to support grouping

* chore: lint, naming, comments & update package.json

* fix(baseWorkflow): mark as abstract
  • Loading branch information
sebsadface authored Aug 31, 2024
1 parent 48a9cf9 commit bae5bee
Show file tree
Hide file tree
Showing 28 changed files with 1,637 additions and 557 deletions.
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ MAINNET_RPC_URL = https://eth-mainnet.g.alchemy.com/v2/1234123412341234
# TODO: Remove private key in favor of forge cast wallet
MAINNET_PRIVATEKEY= 12341234123412341234123412341234

# SEPOLIA
SEPOLIA_RPC_URL = https://eth-mainnet.g.alchemy.com/v2/1234123412341234
# TESTNET
TESTNET_RPC_URL = https://eth-mainnet.g.alchemy.com/v2/1234123412341234
# TODO: Remove private key in favor of forge cast wallet

# ETHSCAN
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
# CHANGELOG

## v1.1.0
- Migrate periphery contracts from protocol core repo (#1)
- Revamped SPG with NFT collection and mint token logic. (#5, #6)
- Added support for batch transactions via `multicall` (#38)
- Added functionality for registering IP with metadata and supporting metadata for SPG NFT. (#8, #20, #37)
- Addressed ownership transfer issues in deployment script. (#18, #39)
- Fixed issues with derivative registration, including minting fees for commercial licenses, license token flow, and making register and attach PIL terms idempotent. (#23, #25, #30)
- Added SPG & SPG NFT upgrade scripts (#10)
- Added IP Graph, Solady's ERC6551 integration, and core protocol package bumps. (#30)
- Enhance CI/CD, repo, and misc.(#2, #3, #11, #32)

**Full Changelog**: [v1.1.0](https://github.com/storyprotocol/protocol-periphery-v1/commits/v1.1.0)

## v1.0.0-beta-rc1

This is the first release of the Story Protocol Gateway

- Adds the SPG, a convenient wrapper around the core contracts for registration
- Includes NFT minting management tooling for registering and minting in one-shot

60 changes: 60 additions & 0 deletions contracts/BaseWorkflow.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.23;

import { IAccessController } from "@storyprotocol/core/interfaces/access/IAccessController.sol";
import { IIPAssetRegistry } from "@storyprotocol/core/interfaces/registries/IIPAssetRegistry.sol";
import { ILicenseRegistry } from "@storyprotocol/core/interfaces/registries/ILicenseRegistry.sol";
import { ILicensingModule } from "@storyprotocol/core/interfaces/modules/licensing/ILicensingModule.sol";
import { ICoreMetadataModule } from "@storyprotocol/core/interfaces/modules/metadata/ICoreMetadataModule.sol";
import { IPILicenseTemplate } from "@storyprotocol/core/interfaces/modules/licensing/IPILicenseTemplate.sol";

import { ISPGNFT } from "./interfaces/ISPGNFT.sol";
import { SPGNFTLib } from "./lib/SPGNFTLib.sol";
import { Errors } from "./lib/Errors.sol";

/// @title Base Workflow
/// @notice The base contract for all Story Protocol Periphery workflows.
abstract contract BaseWorkflow {
/// @notice The address of the Access Controller.
IAccessController public immutable ACCESS_CONTROLLER;

/// @notice The address of the Core Metadata Module.
ICoreMetadataModule public immutable CORE_METADATA_MODULE;

/// @notice The address of the IP Asset Registry.
IIPAssetRegistry public immutable IP_ASSET_REGISTRY;

/// @notice The address of the Licensing Module.
ILicensingModule public immutable LICENSING_MODULE;

/// @notice The address of the License Registry.
ILicenseRegistry public immutable LICENSE_REGISTRY;

/// @notice The address of the PIL License Template.
IPILicenseTemplate public immutable PIL_TEMPLATE;

constructor(
address accessController,
address coreMetadataModule,
address ipAssetRegistry,
address licensingModule,
address licenseRegistry,
address pilTemplate
) {
// assumes 0 addresses are checked in the child contract
ACCESS_CONTROLLER = IAccessController(accessController);
CORE_METADATA_MODULE = ICoreMetadataModule(coreMetadataModule);
IP_ASSET_REGISTRY = IIPAssetRegistry(ipAssetRegistry);
LICENSING_MODULE = ILicensingModule(licensingModule);
LICENSE_REGISTRY = ILicenseRegistry(licenseRegistry);
PIL_TEMPLATE = IPILicenseTemplate(pilTemplate);
}

/// @notice Check that the caller has the minter role for the provided SPG NFT.
/// @param spgNftContract The address of the SPG NFT.
modifier onlyCallerWithMinterRole(address spgNftContract) {
if (!ISPGNFT(spgNftContract).hasRole(SPGNFTLib.MINTER_ROLE, msg.sender))
revert Errors.SPG__CallerNotMinterRole();
_;
}
}
248 changes: 248 additions & 0 deletions contracts/GroupingWorkflows.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.23;

import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { MulticallUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol";
// solhint-disable-next-line max-line-length
import { AccessManagedUpgradeable } from "@openzeppelin/contracts-upgradeable/access/manager/AccessManagedUpgradeable.sol";

import { IGroupingModule } from "@storyprotocol/core/interfaces/modules/grouping/IGroupingModule.sol";
import { ILicensingModule } from "@storyprotocol/core/interfaces/modules/licensing/ILicensingModule.sol";
import { ICoreMetadataModule } from "@storyprotocol/core/interfaces/modules/metadata/ICoreMetadataModule.sol";

import { GroupNFT } from "@storyprotocol/core/GroupNFT.sol";
import { PILTerms } from "@storyprotocol/core/interfaces/modules/licensing/IPILicenseTemplate.sol";

import { Errors } from "./lib/Errors.sol";
import { BaseWorkflow } from "./BaseWorkflow.sol";
import { ISPGNFT } from "./interfaces/ISPGNFT.sol";
import { MetadataHelper } from "./lib/MetadataHelper.sol";
import { LicensingHelper } from "./lib/LicensingHelper.sol";
import { PermissionHelper } from "./lib/PermissionHelper.sol";
import { IGroupingWorkflows } from "./interfaces/IGroupingWorkflows.sol";
import { IStoryProtocolGateway as ISPG } from "./interfaces/IStoryProtocolGateway.sol";

/// @title Grouping Workflows
/// @notice This contract provides key workflows for engaging with Group IPA features in
/// Story’s Proof of Creativity protocol.
contract GroupingWorkflows is
IGroupingWorkflows,
BaseWorkflow,
MulticallUpgradeable,
AccessManagedUpgradeable,
UUPSUpgradeable
{
using ERC165Checker for address;

/// @dev Storage structure for the Grouping Workflow.
/// @param nftContractBeacon The address of the NFT contract beacon.
/// @custom:storage-location erc7201:story-protocol-periphery.GroupingWorkflows
struct GroupingWorkflowsStorage {
address nftContractBeacon;
}

// solhint-disable-next-line max-line-length
// keccak256(abi.encode(uint256(keccak256("story-protocol-periphery.GroupingWorkflows")) - 1)) & ~bytes32(uint256(0xff));
bytes32 private constant GroupingWorkflowsStorageLocation =
0xa8ddbb5f662015e2b3d6b4c61921979ad3d3d1d19e338b1c4ba6a196b10c6400;

/// @notice The address of the Grouping Module.
IGroupingModule public immutable GROUPING_MODULE;

/// @notice The address of the Group NFT contract.
GroupNFT public immutable GROUP_NFT;

constructor(
address accessController,
address coreMetadataModule,
address groupingModule,
address groupNft,
address ipAssetRegistry,
address licensingModule,
address licenseRegistry,
address pilTemplate
)
BaseWorkflow(
accessController,
coreMetadataModule,
ipAssetRegistry,
licensingModule,
licenseRegistry,
pilTemplate
)
{
if (
accessController == address(0) ||
coreMetadataModule == address(0) ||
groupingModule == address(0) ||
groupNft == address(0) ||
ipAssetRegistry == address(0) ||
licensingModule == address(0) ||
licenseRegistry == address(0) ||
pilTemplate == address(0)
) revert Errors.GroupingWorkflows__ZeroAddressParam();

GROUPING_MODULE = IGroupingModule(groupingModule);
GROUP_NFT = GroupNFT(groupNft);

_disableInitializers();
}

/// @dev Initializes the contract.
/// @param accessManager The address of the protocol access manager.
function initialize(address accessManager) external initializer {
if (accessManager == address(0)) revert Errors.GroupingWorkflows__ZeroAddressParam();
__AccessManaged_init(accessManager);
__UUPSUpgradeable_init();
}

/// @dev Sets the NFT contract beacon address.
/// @param newNftContractBeacon The address of the new NFT contract beacon.
function setNftContractBeacon(address newNftContractBeacon) external restricted {
if (newNftContractBeacon == address(0)) revert Errors.GroupingWorkflows__ZeroAddressParam();
GroupingWorkflowsStorage storage $ = _getGroupingWorkflowsStorage();
$.nftContractBeacon = newNftContractBeacon;
}

/// @notice Mint an NFT from a SPGNFT collection, register it with metadata as an IP, attach
/// Programmable IP License Terms to the registered IP, and add it to a group IP.
/// @dev Caller must have the minter role for the provided SPG NFT.
/// @param spgNftContract The address of the SPGNFT collection.
/// @param groupId The ID of the group IP to add the newly registered IP.
/// @param recipient The address of the recipient of the minted NFT.
/// @param licenseTermsId The ID of the registered PIL terms that will be attached to the newly registered IP.
/// @param ipMetadata OPTIONAL. The desired metadata for the newly minted NFT and registered IP.
/// @param sigAddToGroup Signature data for addIp to the group IP via the Grouping Module.
/// @return ipId The ID of the newly registered IP.
/// @return tokenId The ID of the newly minted NFT.
function mintAndRegisterIpAndAttachPILTermsAndAddToGroup(
address spgNftContract,
address groupId,
address recipient,
uint256 licenseTermsId,
ISPG.IPMetadata calldata ipMetadata,
ISPG.SignatureData calldata sigAddToGroup
) external onlyCallerWithMinterRole(spgNftContract) returns (address ipId, uint256 tokenId) {
tokenId = ISPGNFT(spgNftContract).mintByPeriphery({
to: address(this),
payer: msg.sender,
nftMetadataURI: ipMetadata.nftMetadataURI
});
ipId = IP_ASSET_REGISTRY.register(block.chainid, spgNftContract, tokenId);
MetadataHelper.setMetadata(ipId, address(CORE_METADATA_MODULE), ipMetadata);

LICENSING_MODULE.attachLicenseTerms(ipId, address(PIL_TEMPLATE), licenseTermsId);

PermissionHelper.setPermissionForModule(
groupId,
address(GROUPING_MODULE),
address(ACCESS_CONTROLLER),
IGroupingModule.addIp.selector,
sigAddToGroup
);

address[] memory ipIds = new address[](1);
ipIds[0] = ipId;
GROUPING_MODULE.addIp(groupId, ipIds);

ISPGNFT(spgNftContract).safeTransferFrom(address(this), recipient, tokenId, "");
}

/// @notice Register an NFT as IP with metadata, attach Programmable IP License Terms to the registered IP,
/// and add it to a group IP.
/// @param nftContract The address of the NFT collection.
/// @param tokenId The ID of the NFT.
/// @param groupId The ID of the group IP to add the newly registered IP.
/// @param licenseTermsId The ID of the registered PIL terms that will be attached to the newly registered IP.
/// @param ipMetadata OPTIONAL. The desired metadata for the newly registered IP.
/// @param sigMetadataAndAttach Signature data for setAll (metadata) and attachLicenseTerms to the IP
/// via the Core Metadata Module and Licensing Module.
/// @param sigAddToGroup Signature data for addIp to the group IP via the Grouping Module.
/// @return ipId The ID of the newly registered IP.
function registerIpAndAttachPILTermsAndAddToGroup(
address nftContract,
uint256 tokenId,
address groupId,
uint256 licenseTermsId,
ISPG.IPMetadata calldata ipMetadata,
ISPG.SignatureData calldata sigMetadataAndAttach,
ISPG.SignatureData calldata sigAddToGroup
) external returns (address ipId) {
ipId = IP_ASSET_REGISTRY.register(block.chainid, nftContract, tokenId);

address[] memory modules = new address[](2);
bytes4[] memory selectors = new bytes4[](2);
modules[0] = address(CORE_METADATA_MODULE);
modules[1] = address(LICENSING_MODULE);
selectors[0] = ICoreMetadataModule.setAll.selector;
selectors[1] = ILicensingModule.attachLicenseTerms.selector;

PermissionHelper.setBatchPermissionForModules(
ipId,
address(ACCESS_CONTROLLER),
modules,
selectors,
sigMetadataAndAttach
);

MetadataHelper.setMetadata(ipId, address(CORE_METADATA_MODULE), ipMetadata);

LICENSING_MODULE.attachLicenseTerms(ipId, address(PIL_TEMPLATE), licenseTermsId);

PermissionHelper.setPermissionForModule(
groupId,
address(GROUPING_MODULE),
address(ACCESS_CONTROLLER),
IGroupingModule.addIp.selector,
sigAddToGroup
);

address[] memory ipIds = new address[](1);
ipIds[0] = ipId;
GROUPING_MODULE.addIp(groupId, ipIds);
}

/// @notice Register a group IP with a group reward pool, register Programmable IP License Terms,
/// attach it to the group IP, and add individual IPs to the group IP.
/// @dev ipIds must have the same PIL terms as the group IP.
/// @param groupPool The address of the group reward pool.
/// @param ipIds The IDs of the IPs to add to the newly registered group IP.
/// @param groupIpTerms The PIL terms to be registered and attached to the newly registered group IP.
/// @return groupId The ID of the newly registered group IP.
/// @return groupLicenseTermsId The ID of the newly registered PIL terms.
function registerGroupAndAttachPILTermsAndAddIps(
address groupPool,
address[] calldata ipIds,
PILTerms calldata groupIpTerms
) external returns (address groupId, uint256 groupLicenseTermsId) {
groupId = GROUPING_MODULE.registerGroup(groupPool);

groupLicenseTermsId = LicensingHelper.registerPILTermsAndAttach(
groupId,
address(PIL_TEMPLATE),
address(LICENSING_MODULE),
address(LICENSE_REGISTRY),
groupIpTerms
);

GROUPING_MODULE.addIp(groupId, ipIds);

GROUP_NFT.safeTransferFrom(address(this), msg.sender, GROUP_NFT.totalSupply() - 1);
}

//
// Upgrade
//

/// @dev Returns the storage struct of GroupingWorkflows.
function _getGroupingWorkflowsStorage() private pure returns (GroupingWorkflowsStorage storage $) {
assembly {
$.slot := GroupingWorkflowsStorageLocation
}
}

/// @dev Hook to authorize the upgrade according to UUPSUpgradeable
/// @param newImplementation The address of the new implementation
function _authorizeUpgrade(address newImplementation) internal override restricted {}
}
19 changes: 13 additions & 6 deletions contracts/SPGNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,22 @@ contract SPGNFT is ISPGNFT, ERC721URIStorageUpgradeable, AccessControlUpgradeabl
/// @dev The address of the SPG contract.
address public immutable SPG_ADDRESS;

///@dev The address of the GroupingWorkflows contract.
address public immutable GROUPING_ADDRESS;

/// @notice Modifier to restrict access to the SPG contract.
modifier onlySPG() {
if (msg.sender != SPG_ADDRESS) revert Errors.SPGNFT__CallerNotSPG();
modifier onlyPeriphery() {
if (msg.sender != SPG_ADDRESS && msg.sender != GROUPING_ADDRESS)
revert Errors.SPGNFT__CallerNotPeripheryContract();
_;
}

/// @custom:oz-upgrades-unsafe-allow constructor
constructor(address spg) {
constructor(address spg, address groupingWorkflows) {
if (spg == address(0) || groupingWorkflows == address(0)) revert Errors.SPGNFT__ZeroAddressParam();

SPG_ADDRESS = spg;
GROUPING_ADDRESS = groupingWorkflows;

_disableInitializers();
}
Expand Down Expand Up @@ -121,16 +128,16 @@ contract SPGNFT is ISPGNFT, ERC721URIStorageUpgradeable, AccessControlUpgradeabl
tokenId = _mintToken({ to: to, payer: msg.sender, nftMetadataURI: nftMetadataURI });
}

/// @notice Mints an NFT from the collection. Only callable by the SPG.
/// @notice Mints an NFT from the collection. Only callable by the Periphery contracts.
/// @param to The address of the recipient of the minted NFT.
/// @param payer The address of the payer for the mint fee.
/// @param nftMetadataURI OPTIONAL. The URI of the desired metadata for the newly minted NFT.
/// @return tokenId The ID of the minted NFT.
function mintBySPG(
function mintByPeriphery(
address to,
address payer,
string calldata nftMetadataURI
) public virtual onlySPG returns (uint256 tokenId) {
) public virtual onlyPeriphery returns (uint256 tokenId) {
tokenId = _mintToken({ to: to, payer: payer, nftMetadataURI: nftMetadataURI });
}

Expand Down
Loading

0 comments on commit bae5bee

Please sign in to comment.