Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introduce RoyaltyTokenDistributionWorkflows to main for royalty token distribution upon IP registration #136

Merged
merged 2 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions contracts/SPGNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -41,24 +41,33 @@ contract SPGNFT is ISPGNFT, ERC721URIStorageUpgradeable, AccessControlUpgradeabl
bytes32 private constant SPGNFTStorageLocation = 0x66c08f80d8d0ae818983b725b864514cf274647be6eb06de58ff94d1defb6d00;

/// @dev The address of the DerivativeWorkflows contract.
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
address public immutable DERIVATIVE_WORKFLOWS_ADDRESS;

/// @dev The address of the GroupingWorkflows contract.
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
address public immutable GROUPING_WORKFLOWS_ADDRESS;

/// @dev The address of the LicenseAttachmentWorkflows contract.
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
address public immutable LICENSE_ATTACHMENT_WORKFLOWS_ADDRESS;

/// @dev The address of the RegistrationWorkflows contract.
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
address public immutable REGISTRATION_WORKFLOWS_ADDRESS;

/// @dev The address of the RoyaltyTokenDistributionWorkflows contract.
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
address public immutable ROYALTY_TOKEN_DISTRIBUTION_WORKFLOWS_ADDRESS;

/// @notice Modifier to restrict access to workflow contracts.
modifier onlyPeriphery() {
if (
msg.sender != DERIVATIVE_WORKFLOWS_ADDRESS &&
msg.sender != GROUPING_WORKFLOWS_ADDRESS &&
msg.sender != LICENSE_ATTACHMENT_WORKFLOWS_ADDRESS &&
msg.sender != REGISTRATION_WORKFLOWS_ADDRESS
msg.sender != REGISTRATION_WORKFLOWS_ADDRESS &&
msg.sender != ROYALTY_TOKEN_DISTRIBUTION_WORKFLOWS_ADDRESS
) revert Errors.SPGNFT__CallerNotPeripheryContract();
_;
}
Expand All @@ -68,20 +77,22 @@ contract SPGNFT is ISPGNFT, ERC721URIStorageUpgradeable, AccessControlUpgradeabl
address derivativeWorkflows,
address groupingWorkflows,
address licenseAttachmentWorkflows,
address registrationWorkflows
address registrationWorkflows,
address royaltyTokenDistributionWorkflows
) {
if (
derivativeWorkflows == address(0) ||
groupingWorkflows == address(0) ||
licenseAttachmentWorkflows == address(0) ||
registrationWorkflows == address(0)
registrationWorkflows == address(0) ||
royaltyTokenDistributionWorkflows == address(0)
) revert Errors.SPGNFT__ZeroAddressParam();

DERIVATIVE_WORKFLOWS_ADDRESS = derivativeWorkflows;
GROUPING_WORKFLOWS_ADDRESS = groupingWorkflows;
LICENSE_ATTACHMENT_WORKFLOWS_ADDRESS = licenseAttachmentWorkflows;
REGISTRATION_WORKFLOWS_ADDRESS = registrationWorkflows;

ROYALTY_TOKEN_DISTRIBUTION_WORKFLOWS_ADDRESS = royaltyTokenDistributionWorkflows;
_disableInitializers();
}

Expand Down Expand Up @@ -344,6 +355,8 @@ contract SPGNFT is ISPGNFT, ERC721URIStorageUpgradeable, AccessControlUpgradeabl
_grantRole(SPGNFTLib.MINTER_ROLE, LICENSE_ATTACHMENT_WORKFLOWS_ADDRESS);
_grantRole(SPGNFTLib.ADMIN_ROLE, REGISTRATION_WORKFLOWS_ADDRESS);
_grantRole(SPGNFTLib.MINTER_ROLE, REGISTRATION_WORKFLOWS_ADDRESS);
_grantRole(SPGNFTLib.ADMIN_ROLE, ROYALTY_TOKEN_DISTRIBUTION_WORKFLOWS_ADDRESS);
_grantRole(SPGNFTLib.MINTER_ROLE, ROYALTY_TOKEN_DISTRIBUTION_WORKFLOWS_ADDRESS);
}

//
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import { WorkflowStructs } from "../../lib/WorkflowStructs.sol";

/// @title Royalty Token Distribution Workflows Interface
/// @notice Interface for IP royalty token distribution workflows.
interface IRoyaltyTokenDistributionWorkflows {
/// @notice Mint an NFT and register the IP, attach PIL terms, and distribute royalty tokens.
/// @param spgNftContract The address of the SPG NFT contract.
/// @param recipient The address to receive the NFT.
/// @param ipMetadata The metadata for the IP.
/// @param licenseTermsData The PIL terms and licensing configuration data to attach to the IP.
/// @param royaltyShares Authors of the IP and their shares of the royalty tokens, see {WorkflowStructs.RoyaltyShare}.
/// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash.
/// @return ipId The ID of the registered IP.
/// @return tokenId The ID of the minted NFT.
/// @return licenseTermsId The ID of the attached PIL terms.
function mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens(
address spgNftContract,
address recipient,
WorkflowStructs.IPMetadata calldata ipMetadata,
WorkflowStructs.LicenseTermsData calldata licenseTermsData,
WorkflowStructs.RoyaltyShare[] calldata royaltyShares,
bool allowDuplicates
) external returns (address ipId, uint256 tokenId, uint256 licenseTermsId);

/// @notice Mint an NFT and register the IP, make a derivative, and distribute royalty tokens.
/// @param spgNftContract The address of the SPG NFT contract.
/// @param recipient The address to receive the NFT.
/// @param ipMetadata The metadata for the IP.
/// @param derivData The data for the derivative, see {WorkflowStructs.MakeDerivative}.
/// @param royaltyShares Authors of the IP and their shares of the royalty tokens, see {WorkflowStructs.RoyaltyShare}.
/// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash.
/// @return ipId The ID of the registered IP.
/// @return tokenId The ID of the minted NFT.
function mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens(
address spgNftContract,
address recipient,
WorkflowStructs.IPMetadata calldata ipMetadata,
WorkflowStructs.MakeDerivative calldata derivData,
WorkflowStructs.RoyaltyShare[] calldata royaltyShares,
bool allowDuplicates
) external returns (address ipId, uint256 tokenId);

/// @notice Register an IP, attach PIL terms, and deploy a royalty vault.
/// @param nftContract The address of the NFT contract.
/// @param tokenId The ID of the NFT.
/// @param ipMetadata The metadata for the IP.
/// @param licenseTermsData The PIL terms and licensing configuration data to attach to the IP.
/// @param sigMetadataAndAttachAndConfig Signature data for setAll (metadata), attachLicenseTerms, and
/// setLicensingConfig to the IP via the Core Metadata Module and Licensing Module.
/// @return ipId The ID of the registered IP.
/// @return licenseTermsId The ID of the attached PIL terms.
/// @return ipRoyaltyVault The address of the deployed royalty vault.
function registerIpAndAttachPILTermsAndDeployRoyaltyVault(
address nftContract,
uint256 tokenId,
WorkflowStructs.IPMetadata calldata ipMetadata,
WorkflowStructs.LicenseTermsData calldata licenseTermsData,
WorkflowStructs.SignatureData calldata sigMetadataAndAttachAndConfig
) external returns (address ipId, uint256 licenseTermsId, address ipRoyaltyVault);

/// @notice Register an IP, make a derivative, and deploy a royalty vault.
/// @param nftContract The address of the NFT contract.
/// @param tokenId The ID of the NFT.
/// @param ipMetadata The metadata for the IP.
/// @param derivData The data for the derivative, see {WorkflowStructs.MakeDerivative}.
/// @param sigMetadataAndRegister Signature data for setAll (metadata) and registerDerivative to the IP via
/// the Core Metadata Module and Licensing Module.
/// @return ipId The ID of the registered IP.
/// @return ipRoyaltyVault The address of the deployed royalty vault.
function registerIpAndMakeDerivativeAndDeployRoyaltyVault(
address nftContract,
uint256 tokenId,
WorkflowStructs.IPMetadata calldata ipMetadata,
WorkflowStructs.MakeDerivative calldata derivData,
WorkflowStructs.SignatureData calldata sigMetadataAndRegister
) external returns (address ipId, address ipRoyaltyVault);

/// @notice Distribute royalty tokens to the authors of the IP.
/// @param ipId The ID of the IP.
/// @param royaltyShares Authors of the IP and their shares of the royalty tokens, see {WorkflowStructs.RoyaltyShare}.
/// @param sigApproveRoyaltyTokens The signature data for approving the royalty tokens.
function distributeRoyaltyTokens(
address ipId,
WorkflowStructs.RoyaltyShare[] calldata royaltyShares,
WorkflowStructs.SignatureData calldata sigApproveRoyaltyTokens
) external;
}
17 changes: 16 additions & 1 deletion contracts/lib/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,24 @@ library Errors {
////////////////////////////////////////////////////////////////////////////
// Royalty Workflows //
////////////////////////////////////////////////////////////////////////////
/// @notice Zero address provided as a param to the GroupingWorkflows.
/// @notice Zero address provided as a param to the RoyaltyWorkflows.
error RoyaltyWorkflows__ZeroAddressParam();

////////////////////////////////////////////////////////////////////////////
// Royalty Token Distribution Workflows //
////////////////////////////////////////////////////////////////////////////
/// @notice Zero address provided as a param to the RoyaltyTokenDistributionWorkflows.
error RoyaltyTokenDistributionWorkflows__ZeroAddressParam();

/// @notice Total percentage exceed the current balance of the IP account.
error RoyaltyTokenDistributionWorkflows__TotalSharesExceedsIPAccountBalance(
uint32 totalShares,
uint32 ipAccountBalance
);

/// @notice Royalty vault not deployed.
error RoyaltyTokenDistributionWorkflows__RoyaltyVaultNotDeployed();

////////////////////////////////////////////////////////////////////////////
// SPGNFT //
////////////////////////////////////////////////////////////////////////////
Expand Down
158 changes: 144 additions & 14 deletions contracts/lib/LicensingHelper.sol
Original file line number Diff line number Diff line change
@@ -1,16 +1,62 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { Errors as CoreErrors } from "@storyprotocol/core/lib/Errors.sol";
import { ILicensingModule } from "@storyprotocol/core/interfaces/modules/licensing/ILicensingModule.sol";
import { ILicenseTemplate } from "@storyprotocol/core/interfaces/modules/licensing/ILicenseTemplate.sol";
import { IPILicenseTemplate } from "@storyprotocol/core/interfaces/modules/licensing/IPILicenseTemplate.sol";
import { Licensing } from "@storyprotocol/core/lib/Licensing.sol";
import { PILTerms } from "@storyprotocol/core/interfaces/modules/licensing/IPILicenseTemplate.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

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

/// @title Periphery Licensing Helper Library
/// @notice Library for all licensing related helper functions for Periphery contracts.
library LicensingHelper {
using SafeERC20 for IERC20;

/// @dev Registers PIL License Terms and attaches them to the given IP.
/// @param ipId The ID of the IP.
/// @param pilTemplate The address of the PIL License Template.
/// @param licensingModule The address of the Licensing Module.
/// @param terms The PIL terms to be attached to the IP.
/// @return licenseTermsId The ID of the registered PIL terms.
function registerPILTermsAndAttach(
address ipId,
address pilTemplate,
address licensingModule,
PILTerms memory terms
) internal returns (uint256 licenseTermsId) {
licenseTermsId = IPILicenseTemplate(pilTemplate).registerLicenseTerms(terms);
attachLicenseTerms(ipId, licensingModule, pilTemplate, licenseTermsId);
}

/// @dev Attaches license terms to the given IP, does nothing if the license terms are already attached.
/// @param ipId The ID of the IP.
/// @param licensingModule The address of the Licensing Module.
/// @param licenseTemplate The address of the license template.
/// @param licenseTermsId The ID of the license terms to be attached.
function attachLicenseTerms(
address ipId,
address licensingModule,
address licenseTemplate,
uint256 licenseTermsId
) internal {
try ILicensingModule(licensingModule).attachLicenseTerms(ipId, licenseTemplate, licenseTermsId) {
// license terms are attached successfully.
return;
} catch (bytes memory reason) {
// if the error is not that the license terms are already attached, revert with the original error
if (CoreErrors.LicenseRegistry__LicenseTermsAlreadyAttached.selector != bytes4(reason)) {
assembly {
revert(add(reason, 32), mload(reason))
}
}
}
}

/// @dev Registers PIL License Terms and attaches them to the given IP and sets their licensing configurations.
/// @param ipId The ID of the IP.
/// @param pilTemplate The address of the PIL License Template.
Expand Down Expand Up @@ -46,21 +92,105 @@ library LicensingHelper {
uint256 licenseTermsId,
Licensing.LicensingConfig memory licensingConfig
) internal {
try ILicensingModule(licensingModule).attachLicenseTerms(ipId, licenseTemplate, licenseTermsId) {
// license terms are attached successfully, now we set the licensing config
ILicensingModule(licensingModule).setLicensingConfig(
ipId,
licenseTemplate,
licenseTermsId,
licensingConfig
);
} catch (bytes memory reason) {
// if the error is not that the license terms are already attached, revert with the original error
if (CoreErrors.LicenseRegistry__LicenseTermsAlreadyAttached.selector != bytes4(reason)) {
assembly {
revert(add(reason, 32), mload(reason))
}
attachLicenseTerms(ipId, licensingModule, licenseTemplate, licenseTermsId);
ILicensingModule(licensingModule).setLicensingConfig(ipId, licenseTemplate, licenseTermsId, licensingConfig);
}

/// @dev Collects mint fees and registers a derivative.
/// @param childIpId The ID of the child IP.
/// @param royaltyModule The address of the Royalty Module.
/// @param licensingModule The address of the Licensing Module.
/// @param derivData The derivative data to be used for registerDerivative.
function collectMintFeesAndMakeDerivative(
address childIpId,
address royaltyModule,
address licensingModule,
WorkflowStructs.MakeDerivative calldata derivData
) internal {
collectMintFeesAndSetApproval(
msg.sender,
royaltyModule,
licensingModule,
derivData.licenseTemplate,
derivData.parentIpIds,
derivData.licenseTermsIds
);

ILicensingModule(licensingModule).registerDerivative({
childIpId: childIpId,
parentIpIds: derivData.parentIpIds,
licenseTermsIds: derivData.licenseTermsIds,
licenseTemplate: derivData.licenseTemplate,
royaltyContext: derivData.royaltyContext,
maxMintingFee: derivData.maxMintingFee,
maxRts: derivData.maxRts
});
}

/// @dev Collect mint fees for all parent IPs from the payer and set approval for Royalty Module to spend mint fees.
/// @param payerAddress The address of the payer for the license mint fees.
/// @param royaltyModule The address of the Royalty Module.
/// @param licensingModule The address of the Licensing Module.
/// @param licenseTemplate The address of the license template.
/// @param parentIpIds The IDs of all the parent IPs.
/// @param licenseTermsIds The IDs of the license terms for each corresponding parent IP.
function collectMintFeesAndSetApproval(
address payerAddress,
address royaltyModule,
address licensingModule,
address licenseTemplate,
address[] calldata parentIpIds,
uint256[] calldata licenseTermsIds
) private {
ILicenseTemplate lct = ILicenseTemplate(licenseTemplate);
(address royaltyPolicy, , , address mintFeeCurrencyToken) = lct.getRoyaltyPolicy(licenseTermsIds[0]);

if (royaltyPolicy != address(0)) {
// Get total mint fee for all parent IPs
uint256 totalMintFee = aggregateMintFees({
payerAddress: payerAddress,
licensingModule: licensingModule,
licenseTemplate: licenseTemplate,
parentIpIds: parentIpIds,
licenseTermsIds: licenseTermsIds
});

if (totalMintFee != 0) {
// Transfer mint fee from payer to this contract
IERC20(mintFeeCurrencyToken).safeTransferFrom(payerAddress, address(this), totalMintFee);

// Approve Royalty Policy to spend mint fee
IERC20(mintFeeCurrencyToken).forceApprove(royaltyModule, totalMintFee);
}
}
}

/// @dev Aggregate license mint fees for all parent IPs.
/// @param payerAddress The address of the payer for the license mint fees.
/// @param licensingModule The address of the Licensing Module.
/// @param licenseTemplate The address of the license template.
/// @param parentIpIds The IDs of all the parent IPs.
/// @param licenseTermsIds The IDs of the license terms for each corresponding parent IP.
/// @return totalMintFee The sum of license mint fees across all parent IPs.
function aggregateMintFees(
address payerAddress,
address licensingModule,
address licenseTemplate,
address[] calldata parentIpIds,
uint256[] calldata licenseTermsIds
) private view returns (uint256 totalMintFee) {
uint256 mintFee;

for (uint256 i = 0; i < parentIpIds.length; i++) {
(, mintFee) = ILicensingModule(licensingModule).predictMintingLicenseFee({
licensorIpId: parentIpIds[i],
licenseTemplate: licenseTemplate,
licenseTermsId: licenseTermsIds[i],
amount: 1,
receiver: payerAddress,
royaltyContext: ""
});
totalMintFee += mintFee;
}
}
}
8 changes: 8 additions & 0 deletions contracts/lib/WorkflowStructs.sol
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,12 @@ library WorkflowStructs {
PILTerms terms;
Licensing.LicensingConfig licensingConfig;
}

/// @notice Struct for royalty shares information for royalty token distribution.
/// @param recipient The address of the recipient of the royalty shares.
/// @param percentage The percentage of the royalty share, 100_000_000 represents 100%.
struct RoyaltyShare {
address recipient;
uint32 percentage;
}
}
Loading
Loading