Skip to content

Commit

Permalink
add upfront minting fee (storyprotocol#113)
Browse files Browse the repository at this point in the history
* royalty module adjustments

* license related adjustments

* uml related adjustments

* script adjustments

* integration and mocks adjustments

* prevent NC minting fee

---------

Co-authored-by: Raul <[email protected]>
  • Loading branch information
2 people authored and kingster-will committed Mar 16, 2024
1 parent 9df71c5 commit 3daf86c
Show file tree
Hide file tree
Showing 19 changed files with 245 additions and 120 deletions.
24 changes: 11 additions & 13 deletions contracts/interfaces/modules/licensing/ILicensingModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,16 @@ interface ILicensingModule is IModule {
/// @param frameworkData The policy framework specific encoded data
/// @param royaltyPolicy The address of the royalty policy
/// @param royaltyData The royalty policy specific encoded data
/// @param mintingFee The fee to be paid when minting a license
/// @param mintingFeeToken The token to be used to pay the minting fee
event PolicyRegistered(
uint256 indexed policyId,
address indexed policyFrameworkManager,
bytes frameworkData,
address royaltyPolicy,
bytes royaltyData
bytes royaltyData,
uint256 mintingFee,
address mintingFeeToken
);

/// @notice Emitted when a policy is added to an IP
Expand Down Expand Up @@ -73,18 +77,12 @@ interface ILicensingModule is IModule {
/// @param manager the address of the manager. Will be ERC165 checked for IPolicyFrameworkManager
function registerPolicyFrameworkManager(address manager) external;

/// @notice Registers a policy into the contract. MUST be called by a registered framework or it will revert.
/// The policy data and its integrity must be verified by the policy framework manager.
/// @param isLicenseTransferable True if the license is transferable
/// @param royaltyPolicy The address of the royalty policy
/// @param royaltyData The royalty policy specific encoded data
/// @param frameworkData The policy framework specific encoded data
function registerPolicy(
bool isLicenseTransferable,
address royaltyPolicy,
bytes memory royaltyData,
bytes memory frameworkData
) external returns (uint256 policyId);
/// @notice Registers a policy into the contract. MUST be called by a registered
/// framework or it will revert. The policy data and its integrity must be
/// verified by the policy framework manager.
/// @param pol The Licensing policy data. MUST have same policy framework as the caller address
/// @return policyId The id of the newly registered policy
function registerPolicy(Licensing.Policy memory pol) external returns (uint256 policyId);

/// @notice Adds a policy to the set of policies of an IP
/// @param ipId The id of the IP
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,14 @@ struct UMLPolicy {

/// @param transferable Whether or not the license is transferable
/// @param royaltyPolicy Address of a royalty policy contract (e.g. RoyaltyPolicyLS) that will handle royalty payments
/// @param mintingFee Fee to be paid when minting a license
/// @param mintingFeeToken Token to be used to pay the minting fee
/// @param umlPolicy UMLPolicy compliant licensing term values
struct RegisterUMLPolicyParams {
bool transferable;
address royaltyPolicy;
uint256 mintingFee;
address mintingFeeToken;
UMLPolicy policy;
}

Expand Down
21 changes: 21 additions & 0 deletions contracts/interfaces/modules/royalty/IRoyaltyModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ interface IRoyaltyModule is IModule {
/// @param amount The amount that is paid
event RoyaltyPaid(address receiverIpId, address payerIpId, address sender, address token, uint256 amount);

/// @notice Event emitted when the license minting fee is paid
/// @param receiverIpId The ipId that receives the royalties
/// @param payerAddress The address that pays the royalties
/// @param token The token that is used to pay the royalties
/// @param amount The amount paid
event LicenseMintingFeePaid(address receiverIpId, address payerAddress, address token, uint256 amount);

/// @notice Returns the licensing module address
function LICENSING_MODULE() external view returns (address);

Expand Down Expand Up @@ -93,4 +100,18 @@ interface IRoyaltyModule is IModule {
/// @param token The token to use to pay the royalties
/// @param amount The amount to pay
function payRoyaltyOnBehalf(address receiverIpId, address payerIpId, address token, uint256 amount) external;

/// @notice Allows to pay the minting fee for a license
/// @param receiverIpId The ipId that receives the royalties
/// @param payerAddress The address that pays the royalties
/// @param licenseRoyaltyPolicy The royalty policy of the license being minted
/// @param token The token to use to pay the royalties
/// @param amount The amount to pay
function payLicenseMintingFee(
address receiverIpId,
address payerAddress,
address licenseRoyaltyPolicy,
address token,
uint256 amount
) external;
}
3 changes: 3 additions & 0 deletions contracts/lib/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ library Errors {
error LicensingModule__IncompatibleLicensorRoyaltyDerivativeRevShare();
error LicensingModule__DerivativeRevShareSumExceedsMaxRNFTSupply();
error LicensingModule__MismatchBetweenRoyaltyPolicy();
error LicensingModule__RegisterPolicyFrameworkMismatch();
error LicensingModule__RoyaltyPolicyNotWhitelisted();
error LicensingModule__MintingFeeTokenNotWhitelisted();
/// @notice emitted when trying to interact with an IP that has been disputed in the DisputeModule
error LicensingModule__DisputedIpId();
/// @notice emitted when linking a license from a licensor that has been disputed in the DisputeModule
Expand Down
5 changes: 5 additions & 0 deletions contracts/lib/Licensing.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,18 @@ library Licensing {
/// @param frameworkData Data to be used by the policy framework to verify minting and linking
/// @param royaltyPolicy address of the royalty policy to be used by the policy framework, if any
/// @param royaltyData Data to be used by the royalty policy (for example, encoding of the royalty percentage)
/// @param mintingFee Fee to be paid when minting a license
/// @param mintingFeeToken Token to be used to pay the minting fee
struct Policy {
bool isLicenseTransferable;
address policyFramework;
bytes frameworkData;
address royaltyPolicy;
bytes royaltyData;
uint256 mintingFee;
address mintingFeeToken;
}

/// @notice Data that define a License Agreement NFT
/// @param policyId Id of the policy this license is based on, which will be set in the derivative IP when the
/// license is burnt for linking
Expand Down
2 changes: 2 additions & 0 deletions contracts/lib/UMLFrameworkErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ library UMLFrameworkErrors {
error UMLPolicyFrameworkManager__ReciprocalValueMismatch();
error UMLPolicyFrameworkManager__CommercialValueMismatch();
error UMLPolicyFrameworkManager__StringArrayMismatch();
error UMLPolicyFrameworkManager__CommecialDisabled_CantAddMintingFee();
error UMLPolicyFrameworkManager__CommecialDisabled_CantAddMintingFeeToken();
}
46 changes: 25 additions & 21 deletions contracts/modules/licensing/LicensingModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -113,35 +113,36 @@ contract LicensingModule is AccessControlled, ILicensingModule, BaseModule, Reen

/// @notice Registers a policy into the contract. MUST be called by a registered framework or it will revert.
/// The policy data and its integrity must be verified by the policy framework manager.
/// @param isLicenseTransferable True if the license is transferable
/// @param royaltyPolicy The address of the royalty policy
/// @param royaltyData The royalty policy specific encoded data
/// @param frameworkData The policy framework specific encoded data
function registerPolicy(
bool isLicenseTransferable,
address royaltyPolicy,
bytes memory royaltyData,
bytes memory frameworkData
) external returns (uint256 policyId) {
/// @param pol The Licensing policy data. MUST have same policy framework as the caller address
/// @return policyId The id of the newly registered policy
function registerPolicy(Licensing.Policy memory pol) external returns (uint256 policyId) {
_verifyRegisteredFramework(address(msg.sender));
Licensing.Policy memory pol = Licensing.Policy({
isLicenseTransferable: isLicenseTransferable,
policyFramework: msg.sender,
frameworkData: frameworkData,
royaltyPolicy: royaltyPolicy,
royaltyData: royaltyData
});

if (pol.policyFramework != address(msg.sender)) {
revert Errors.LicensingModule__RegisterPolicyFrameworkMismatch();
}
if (pol.royaltyPolicy != address(0) && !ROYALTY_MODULE.isWhitelistedRoyaltyPolicy(pol.royaltyPolicy)) {
revert Errors.LicensingModule__RoyaltyPolicyNotWhitelisted();
}
if (pol.mintingFee > 0 && !ROYALTY_MODULE.isWhitelistedRoyaltyToken(pol.mintingFeeToken)) {
revert Errors.LicensingModule__MintingFeeTokenNotWhitelisted();
}
(uint256 polId, bool newPol) = DataUniqueness.addIdOrGetExisting(
abi.encode(pol),
_hashedPolicies,
_totalPolicies
);

if (newPol) {
_totalPolicies = polId;
_policies[polId] = pol;
emit PolicyRegistered(polId, msg.sender, frameworkData, royaltyPolicy, royaltyData);
emit PolicyRegistered(
polId,
pol.policyFramework,
pol.frameworkData,
pol.royaltyPolicy,
pol.royaltyData,
pol.mintingFee,
pol.mintingFeeToken
);
}
return polId;
}
Expand Down Expand Up @@ -204,7 +205,10 @@ contract LicensingModule is AccessControlled, ILicensingModule, BaseModule, Reen
// If the policy has a royalty policy, we need to call the royalty module to process the minting
// Otherwise, it's non commercial and we can skip the call.
if (pol.royaltyPolicy != address(0)) {
ROYALTY_MODULE.onLicenseMinting(licensorIpId, pol.royaltyPolicy, pol.royaltyData, royaltyContext);
// If there's a minting fee, sender must pay it
if (pol.mintingFee > 0) {
ROYALTY_MODULE.payLicenseMintingFee(licensorIpId, msg.sender, pol.royaltyPolicy, pol.mintingFeeToken, pol.mintingFee);
}
}

// If a policy is set, then is only up to the policy params.
Expand Down
32 changes: 22 additions & 10 deletions contracts/modules/licensing/UMLPolicyFrameworkManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.s
// contracts
import { IHookModule } from "../../interfaces/modules/base/IHookModule.sol";
import { ILicensingModule } from "../../interfaces/modules/licensing/ILicensingModule.sol";
import { IRoyaltyModule } from "../../interfaces/modules/royalty/IRoyaltyModule.sol";
import { Licensing } from "../../lib/Licensing.sol";
import { Errors } from "../../lib/Errors.sol";
import { UMLFrameworkErrors } from "../../lib/UMLFrameworkErrors.sol";
Expand Down Expand Up @@ -54,17 +55,23 @@ contract UMLPolicyFrameworkManager is
/// @param params parameters needed to register a UMLPolicy
/// @return policyId The ID of the newly registered policy
function registerPolicy(RegisterUMLPolicyParams calldata params) external nonReentrant returns (uint256 policyId) {
_verifyComercialUse(params.policy, params.royaltyPolicy);
/// Minting fee amount & address checked in LicensingModule, no need to check here.
/// We don't limit charging for minting to commercial use, you could sell a NC license in theory.
_verifyComercialUse(params.policy, params.royaltyPolicy, params.mintingFee, params.mintingFeeToken);
_verifyDerivatives(params.policy);
/// TODO: DO NOT deploy on production networks without hashing string[] values instead of storing them

Licensing.Policy memory pol = Licensing.Policy({
isLicenseTransferable: params.transferable,
policyFramework: address(this),
frameworkData: abi.encode(params.policy),
royaltyPolicy: params.royaltyPolicy,
royaltyData: abi.encode(params.policy.commercialRevShare),
mintingFee: params.mintingFee,
mintingFeeToken: params.mintingFeeToken
});
// No need to emit here, as the LicensingModule will emit the event
return
LICENSING_MODULE.registerPolicy(
params.transferable,
params.royaltyPolicy,
abi.encode(params.policy.commercialRevShare), // TODO: this should be encoded by the royalty policy
abi.encode(params.policy)
);
return LICENSING_MODULE.registerPolicy(pol);
}

/// @notice Verify policy parameters for linking a child IP to a parent IP (licensor) by burning a license NFT.
Expand Down Expand Up @@ -347,7 +354,7 @@ contract UMLPolicyFrameworkManager is
/// @param policy The policy to verify
/// @param royaltyPolicy The address of the royalty policy
// solhint-disable-next-line code-complexity
function _verifyComercialUse(UMLPolicy calldata policy, address royaltyPolicy) internal view {
function _verifyComercialUse(UMLPolicy calldata policy, address royaltyPolicy, uint256 mintingFee, address mintingFeeToken) internal view {
if (!policy.commercialUse) {
if (policy.commercialAttribution) {
revert UMLFrameworkErrors.UMLPolicyFrameworkManager__CommecialDisabled_CantAddAttribution();
Expand All @@ -361,8 +368,13 @@ contract UMLPolicyFrameworkManager is
if (royaltyPolicy != address(0)) {
revert UMLFrameworkErrors.UMLPolicyFrameworkManager__CommercialDisabled_CantAddRoyaltyPolicy();
}
if (mintingFee > 0) {
revert UMLFrameworkErrors.UMLPolicyFrameworkManager__CommecialDisabled_CantAddMintingFee();
}
if (mintingFeeToken != address(0)) {
revert UMLFrameworkErrors.UMLPolicyFrameworkManager__CommecialDisabled_CantAddMintingFeeToken();
}
} else {
// TODO: check for supportInterface instead
if (royaltyPolicy == address(0)) {
revert UMLFrameworkErrors.UMLPolicyFrameworkManager__CommecialEnabled_RoyaltyPolicyRequired();
}
Expand Down
25 changes: 25 additions & 0 deletions contracts/modules/royalty-module/RoyaltyModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,33 @@ contract RoyaltyModule is IRoyaltyModule, Governable, ReentrancyGuard, BaseModul
emit RoyaltyPaid(receiverIpId, payerIpId, msg.sender, token, amount);
}

/// @notice Allows to pay the minting fee for a license
/// @param receiverIpId The ipId that receives the royalties
/// @param payerAddress The address that pays the royalties
/// @param licenseRoyaltyPolicy The royalty policy of the license being minted
/// @param token The token to use to pay the royalties
/// @param amount The amount to pay
function payLicenseMintingFee(
address receiverIpId,
address payerAddress,
address licenseRoyaltyPolicy,
address token,
uint256 amount
) external onlyLicensingModule {
if (!isWhitelistedRoyaltyToken[token]) revert Errors.RoyaltyModule__NotWhitelistedRoyaltyToken();

if (licenseRoyaltyPolicy == address(0)) revert Errors.RoyaltyModule__NoRoyaltyPolicySet();
if (!isWhitelistedRoyaltyPolicy[licenseRoyaltyPolicy])
revert Errors.RoyaltyModule__NotWhitelistedRoyaltyPolicy();

IRoyaltyPolicy(licenseRoyaltyPolicy).onRoyaltyPayment(payerAddress, receiverIpId, token, amount);

emit LicenseMintingFeePaid(receiverIpId, payerAddress, token, amount);
}

/// @notice IERC165 interface support.
function supportsInterface(bytes4 interfaceId) public view virtual override(BaseModule, IERC165) returns (bool) {
return interfaceId == type(IRoyaltyModule).interfaceId || super.supportsInterface(interfaceId);
}

}
4 changes: 4 additions & 0 deletions script/foundry/deployment/Main.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,8 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler {
RegisterUMLPolicyParams({
transferable: true,
royaltyPolicy: address(royaltyPolicyLAP),
mintingFee: 0,
mintingFeeToken: address(0),
policy: UMLPolicy({
attribution: true,
commercialUse: true,
Expand All @@ -423,6 +425,8 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler {
RegisterUMLPolicyParams({
transferable: false,
royaltyPolicy: address(0), // no royalty, non-commercial
mintingFee: 0,
mintingFeeToken: address(0),
policy: UMLPolicy({
attribution: true,
commercialUse: false,
Expand Down
3 changes: 0 additions & 3 deletions test/foundry/integration/BaseIntegration.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,6 @@ contract BaseIntegration is BaseTest {

dealMockAssets();
approveRegistration();

// Also deploy mock royalty policy LS
// mockRoyaltyPolicyLS = new MockRoyaltyPolicyLS(address(royaltyModule));
}

function approveRegistration() internal {
Expand Down
16 changes: 8 additions & 8 deletions test/foundry/integration/big-bang/SingleNftCollection.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ contract BigBang_Integration_SingleNftCollection is BaseIntegration {
REGISTER IP ACCOUNTS
///////////////////////////////////////////////////////////////*/

// ipAcct[tokenId] => ipAccount address
/* // ipAcct[tokenId] => ipAccount address
// owner is the vm.pranker
vm.startPrank(u.alice);
Expand All @@ -109,12 +109,12 @@ contract BigBang_Integration_SingleNftCollection is BaseIntegration {
vm.startPrank(u.carl);
mockNFT.mintId(u.carl, 5);
ipAcct[5] = registerIpAccount(mockNFT, 5, u.carl);

*/
/*//////////////////////////////////////////////////////////////
ADD POLICIES TO IP ACCOUNTS
///////////////////////////////////////////////////////////////*/

vm.startPrank(u.alice);
/* vm.startPrank(u.alice);
licensingModule.addPolicyToIp(ipAcct[1], policyIds["uml_com_deriv_cheap_flexible"]);
licensingModule.addPolicyToIp(ipAcct[100], policyIds["uml_noncom_deriv_reciprocal_derivative"]);
Expand All @@ -133,13 +133,13 @@ contract BigBang_Integration_SingleNftCollection is BaseIntegration {
ipAcct[3],
policyIds["uml_noncom_deriv_reciprocal_derivative"]
)
);
); */

/*///////////////////////////////////////////////////////////////
MINT & USE LICENSES
///////////////////////////////////////////////////////////////*/

// Carl mints 1 license for policy "com_deriv_all_true" on Alice's NFT 1 IPAccount
/* // Carl mints 1 license for policy "com_deriv_all_true" on Alice's NFT 1 IPAccount
// Carl creates NFT 6 IPAccount
// Carl activates the license on his NFT 6 IPAccount, linking as child to Alice's NFT 1 IPAccount
{
Expand Down Expand Up @@ -340,7 +340,7 @@ contract BigBang_Integration_SingleNftCollection is BaseIntegration {
metadata,
u.carl, // caller
abi.encode(params)
);
}
);
}*/
}
}
}
12 changes: 11 additions & 1 deletion test/foundry/mocks/licensing/MockPolicyFrameworkManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity ^0.8.23;

// contracts
import { BasePolicyFrameworkManager } from "contracts/modules/licensing/BasePolicyFrameworkManager.sol";
import { Licensing } from "contracts/lib/Licensing.sol";

struct MockPolicyFrameworkConfig {
address licensingModule;
Expand Down Expand Up @@ -32,7 +33,16 @@ contract MockPolicyFrameworkManager is BasePolicyFrameworkManager {

function registerPolicy(MockPolicy calldata mockPolicy) external returns (uint256 policyId) {
emit MockPolicyAdded(policyId, mockPolicy);
return LICENSING_MODULE.registerPolicy(true, royaltyPolicy, "", abi.encode(mockPolicy));
Licensing.Policy memory pol = Licensing.Policy({
isLicenseTransferable: true,
policyFramework: address(this),
frameworkData: "",
royaltyPolicy: royaltyPolicy,
royaltyData: abi.encode(mockPolicy),
mintingFee: 0,
mintingFeeToken: address(0)
});
return LICENSING_MODULE.registerPolicy(pol);
}

function verifyMint(address, bool, address, address, uint256, bytes memory data) external pure returns (bool) {
Expand Down
Loading

0 comments on commit 3daf86c

Please sign in to comment.