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 for distributing royalty tokens among IP co-creators #126

Merged
merged 1 commit into from
Nov 22, 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
20 changes: 17 additions & 3 deletions contracts/SPGNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -39,24 +39,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 @@ -66,19 +75,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 @@ -286,6 +298,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,100 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

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

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.
/// @dev In order to successfully distribute royalty tokens, the license terms attached to the IP must be
/// a commercial license.
/// @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 terms The PIL terms to attach to the IP (must be a commercial license).
/// @param royaltyShares Authors of the IP and their shares of the royalty tokens, see {WorkflowStructs.RoyaltyShare}.
/// @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,
PILTerms calldata terms,
WorkflowStructs.RoyaltyShare[] calldata royaltyShares
) external returns (address ipId, uint256 tokenId, uint256 licenseTermsId);

/// @notice Mint an NFT and register the IP, make a derivative, and distribute royalty tokens.
/// @dev In order to successfully distribute royalty tokens, the license terms attached to the IP must be
/// a commercial license.
/// @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}.
/// @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
) external returns (address ipId, uint256 tokenId);

/// @notice Register an IP, attach PIL terms, and deploy a royalty vault.
/// @dev In order to successfully deploy a royalty vault, the license terms attached to the IP must be
/// a commercial license.
/// @param nftContract The address of the NFT contract.
/// @param tokenId The ID of the NFT.
/// @param ipMetadata The metadata for the IP.
/// @param terms The PIL terms to attach to the IP (must be a commercial license).
/// @param sigMetadata The signature data for the IP metadata.
/// @param sigAttach The signature data for attaching the PIL terms.
/// @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,
PILTerms calldata terms,
WorkflowStructs.SignatureData calldata sigMetadata,
WorkflowStructs.SignatureData calldata sigAttach
) external returns (address ipId, uint256 licenseTermsId, address ipRoyaltyVault);

/// @notice Register an IP, make a derivative, and deploy a royalty vault.
/// @dev In order to successfully deploy a royalty vault, the license terms attached to the IP must be
/// a commercial license.
/// @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 sigMetadata The signature data for the IP metadata.
/// @param sigRegister The signature data for registering the derivative.
/// @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 sigMetadata,
WorkflowStructs.SignatureData calldata sigRegister
) external returns (address ipId, address ipRoyaltyVault);

/// @notice Distribute royalty tokens to the authors of the IP.
/// @param ipId The ID of the IP.
/// @param ipRoyaltyVault The address of the royalty vault.
/// @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,
address ipRoyaltyVault,
WorkflowStructs.RoyaltyShare[] calldata royaltyShares,
WorkflowStructs.SignatureData calldata sigApproveRoyaltyTokens
) external;
}
14 changes: 13 additions & 1 deletion contracts/lib/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,21 @@ 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 percentages exceed 100%.
error RoyaltyTokenDistributionWorkflows__TotalPercentagesExceeds100Percent();

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

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

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

/// @title Periphery Licensing Helper Library
/// @notice Library for all licensing related helper functions for Periphery contracts.
library LicensingHelper {
/// @dev Registers PIL License Terms and attaches them to the given IP.
using SafeERC20 for IERC20;

/// @dev Registers multiple 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.
Expand All @@ -24,11 +29,34 @@ library LicensingHelper {
) internal returns (uint256[] memory licenseTermsIds) {
licenseTermsIds = new uint256[](terms.length);
for (uint256 i = 0; i < terms.length; i++) {
licenseTermsIds[i] = IPILicenseTemplate(pilTemplate).registerLicenseTerms(terms[i]);
attachLicenseTerms(ipId, licensingModule, licenseRegistry, pilTemplate, licenseTermsIds[i]);
licenseTermsIds[i] = registerPILTermsAndAttach(
ipId,
pilTemplate,
licensingModule,
licenseRegistry,
terms[i]
);
}
}

/// @dev Registers a single PIL License and attaches it 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 licenseRegistry The address of the License Registry.
/// @param terms The PIL terms to be registered.
/// @return licenseTermsId The ID of the registered PIL terms.
function registerPILTermsAndAttach(
address ipId,
address pilTemplate,
address licensingModule,
address licenseRegistry,
PILTerms calldata terms
) internal returns (uint256 licenseTermsId) {
licenseTermsId = IPILicenseTemplate(pilTemplate).registerLicenseTerms(terms);
attachLicenseTerms(ipId, licensingModule, licenseRegistry, pilTemplate, licenseTermsId);
}

/// @dev Attaches license terms to the given IP.
/// @param ipId The ID of the IP.
/// @param licensingModule The address of the Licensing Module.
Expand All @@ -53,4 +81,71 @@ library LicensingHelper {
}
}
}

/// @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[] memory parentIpIds,
uint256[] memory licenseTermsIds
) internal {
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[] memory parentIpIds,
uint256[] memory licenseTermsIds
) internal 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 @@ -38,4 +38,12 @@ library WorkflowStructs {
uint256[] licenseTermsIds;
bytes royaltyContext;
}

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