Skip to content

Commit

Permalink
Introduce License Configurations for Group Restrictions (#313)
Browse files Browse the repository at this point in the history
  • Loading branch information
kingster-will authored Nov 21, 2024
1 parent 0c681fd commit 6aa8573
Show file tree
Hide file tree
Showing 13 changed files with 585 additions and 72 deletions.
8 changes: 7 additions & 1 deletion contracts/interfaces/modules/grouping/IGroupRewardPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ interface IGroupRewardPool {
/// @notice Adds an IP to the group pool
/// @param groupId The group ID
/// @param ipId The IP ID
function addIp(address groupId, address ipId) external;
/// @param minimumGroupRewardShare The minimum group reward share the IP expects to be added to the group
/// @return totalGroupRewardShare The total group reward share after adding the IP
function addIp(
address groupId,
address ipId,
uint256 minimumGroupRewardShare
) external returns (uint256 totalGroupRewardShare);

/// @notice Removes an IP from the group pool
/// @param groupId The group ID
Expand Down
17 changes: 17 additions & 0 deletions contracts/interfaces/registries/ILicenseRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,23 @@ interface ILicenseRegistry {
bool isMintedByIpOwner
) external view returns (Licensing.LicensingConfig memory);

/// @notice Verifies the group can add given IP.
/// @param groupId The address of the group.
/// @param groupRewardPool The address of the reward pool of the group.
/// @param ipId The address of the IP to be added to the group.
/// @param groupLicenseTemplate the address of the license template attached to the group.
/// the IP must have this license template.
/// @param groupLicenseTermsId The ID of the license terms attached to the group.
/// the IP must have this license terms.
/// @return ipLicensingConfig The configuration for license attached to the IP.
function verifyGroupAddIp(
address groupId,
address groupRewardPool,
address ipId,
address groupLicenseTemplate,
uint256 groupLicenseTermsId
) external view returns (Licensing.LicensingConfig memory ipLicensingConfig);

/// @notice Attaches license terms to an IP.
/// @param ipId The address of the IP to which the license terms are attached.
/// @param licenseTemplate The address of the license template.
Expand Down
37 changes: 31 additions & 6 deletions contracts/lib/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,6 @@ library Errors {
/// @notice The group ip has no attached any license terms.
error GroupingModule__GroupIPHasNoLicenseTerms(address groupId);

/// @notice The IP has no attached the same license terms of Group IPA.
error GroupingModule__IpHasNoGroupLicenseTerms(address groupId, address licenseTemplate, uint256 licenseTermsId);

/// @notice The Royalty Vault has not been created.
error GroupingModule__GroupRoyaltyVaultNotCreated(address groupId);

Expand All @@ -136,12 +133,18 @@ library Errors {
/// @notice The Group IP has been frozen due to already mint license tokens.
error GroupingModule__GroupFrozenDueToAlreadyMintLicenseTokens(address groupId);

/// @notice Cannot add IP which has expiration to group.
error GroupingModule__CannotAddIpWithExpirationToGroup(address ipId);

/// @notice Group IP should attach non default license terms.
error GroupingModule__GroupIPShouldHasNonDefaultLicenseTerms(address groupId);

/// @notice The total group reward share exceeds 100% when adding IP to the group.
/// means the IP is not allowed to be added to the group.
error GroupingModule__TotalGroupRewardShareExceeds100Percent(
address groupId,
uint256 totalGroupRewardShare,
address ipId,
uint256 expectGroupRewardShare
);

////////////////////////////////////////////////////////////////////////////
// IP Asset Registry //
////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -258,6 +261,28 @@ library Errors {
/// @notice Zero address provided for IP Graph ACL.
error LicenseRegistry__ZeroIPGraphACL();

/// @notice The license of IP to be added to a group is disabled
error LicenseRegistry__IpLicenseDisabled(address ipId, address licenseTemplate, uint256 licenseTermsId);

/// @notice The IP does not set expected group reward pool to be added,
/// means the IP is not allowed to be added to any group.
error LicenseRegistry__IpExpectGroupRewardPoolNotSet(address ipId);

/// @notice The expected group reward pool of IP does not match the group reward pool of the group.
/// Means the IP is not allowed to be added to the group.
error LicenseRegistry__IpExpectGroupRewardPoolNotMatch(
address ipId,
address expectGroupRewardPool,
address groupId,
address groupRewardPool
);

/// @notice Cannot add IP which has expiration to group.
error LicenseRegistry__CannotAddIpWithExpirationToGroup(address ipId);

/// @notice The IP has no attached the same license terms of Group IPA.
error LicenseRegistry__IpHasNoGroupLicenseTerms(address groupId, address licenseTemplate, uint256 licenseTermsId);

/// @notice When Set LicenseConfig the license template cannot be Zero address if royalty percentage is not Zero.
error LicensingModule__LicenseTemplateCannotBeZeroAddressToOverrideRoyaltyPercent();

Expand Down
9 changes: 9 additions & 0 deletions contracts/lib/Licensing.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,21 @@ library Licensing {
/// @param hookData The data to be used by the licensing hook.
/// @param commercialRevShare The commercial revenue share percentage.
/// @param disabled Whether the license is disabled or not.
/// @param expectMinimumGroupRewardShare The minimum percentage of the group’s reward share
/// (from 0 to 100%, represented as 100 * 10 ** 6) that can be allocated to the IP when it is added to the group.
/// If the remaining reward share in the group is less than the minimumGroupRewardShare,
/// the IP cannot be added to the group.
/// @param expectGroupRewardPool The address of the expected group reward pool.
/// The IP can only be added to a group with this specified reward pool address,
/// or address(0) if the IP does not want to be added to any group.
struct LicensingConfig {
bool isSet;
uint256 mintingFee;
address licensingHook;
bytes hookData;
uint32 commercialRevShare;
bool disabled;
uint32 expectMinimumGroupRewardShare;
address expectGroupRewardPool;
}
}
31 changes: 28 additions & 3 deletions contracts/modules/grouping/EvenSplitGroupPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ contract EvenSplitGroupPool is IGroupRewardPool, ProtocolPausableUpgradeable, UU
mapping(address groupId => uint256 totalIps) totalMemberIps;
mapping(address groupId => mapping(address token => uint256 balance)) poolBalance;
mapping(address groupId => mapping(address tokenId => mapping(address ipId => uint256))) ipRewardDebt;
mapping(address groupId => uint256 totalMinimumRewardShare) totalMinimumRewardShare;
mapping(address groupId => mapping(address ipId => uint256 minimumRewardShare)) minimumRewardShare;
}

// keccak256(abi.encode(uint256(keccak256("story-protocol.EvenSplitGroupPool")) - 1)) & ~bytes32(uint256(0xff));
Expand Down Expand Up @@ -74,12 +76,23 @@ contract EvenSplitGroupPool is IGroupRewardPool, ProtocolPausableUpgradeable, UU
/// @dev Only the GroupingModule can call this function
/// @param groupId The group ID
/// @param ipId The IP ID
function addIp(address groupId, address ipId) external onlyGroupingModule {
// ignore if IP is already added to pool
if (_isIpAdded(groupId, ipId)) return;
/// @param minimumGroupRewardShare The minimum group reward share the IP expects to be added to the group
/// @return totalGroupRewardShare The total group reward share after adding the IP
function addIp(
address groupId,
address ipId,
uint256 minimumGroupRewardShare
) external onlyGroupingModule returns (uint256 totalGroupRewardShare) {
EvenSplitGroupPoolStorage storage $ = _getEvenSplitGroupPoolStorage();
// ignore if IP is already added to pool
if (_isIpAdded(groupId, ipId)) return $.totalMinimumRewardShare[groupId];
$.ipAddedTime[groupId][ipId] = block.timestamp;
$.totalMemberIps[groupId] += 1;
if (minimumGroupRewardShare > 0) {
$.minimumRewardShare[groupId][ipId] = minimumGroupRewardShare;
$.totalMinimumRewardShare[groupId] += minimumGroupRewardShare;
}
totalGroupRewardShare = $.totalMinimumRewardShare[groupId];
}

/// @notice Removes an IP from the group pool
Expand All @@ -92,6 +105,10 @@ contract EvenSplitGroupPool is IGroupRewardPool, ProtocolPausableUpgradeable, UU
EvenSplitGroupPoolStorage storage $ = _getEvenSplitGroupPoolStorage();
$.ipAddedTime[groupId][ipId] = 0;
$.totalMemberIps[groupId] -= 1;
if ($.minimumRewardShare[groupId][ipId] > 0) {
$.totalMinimumRewardShare[groupId] -= $.minimumRewardShare[groupId][ipId];
$.minimumRewardShare[groupId][ipId] = 0;
}
}

/// @notice Deposits reward to the group pool directly
Expand Down Expand Up @@ -158,6 +175,14 @@ contract EvenSplitGroupPool is IGroupRewardPool, ProtocolPausableUpgradeable, UU
return _isIpAdded(groupId, ipId);
}

function getMinimumRewardShare(address groupId, address ipId) external view returns (uint256) {
return _getEvenSplitGroupPoolStorage().minimumRewardShare[groupId][ipId];
}

function getTotalMinimumRewardShare(address groupId) external view returns (uint256) {
return _getEvenSplitGroupPoolStorage().totalMinimumRewardShare[groupId];
}

function _getRewardPerIp(address groupId, address token) internal view returns (uint256) {
EvenSplitGroupPoolStorage storage $ = _getEvenSplitGroupPoolStorage();
uint256 totalIps = $.totalMemberIps[groupId];
Expand Down
25 changes: 15 additions & 10 deletions contracts/modules/grouping/GroupingModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { IPILicenseTemplate, PILTerms } from "../../interfaces/modules/licensing
import { ILicenseToken } from "../../interfaces/ILicenseToken.sol";
import { IRoyaltyModule } from "../../interfaces/modules/royalty/IRoyaltyModule.sol";
import { IIpRoyaltyVault } from "../../interfaces/modules/royalty/policies/IIpRoyaltyVault.sol";
import { Licensing } from "../../lib/Licensing.sol";

/// @title Grouping Module
/// @notice Grouping module is the main entry point for the IPA grouping. It is responsible for:
Expand Down Expand Up @@ -166,19 +167,23 @@ contract GroupingModule is
if (GROUP_IP_ASSET_REGISTRY.isRegisteredGroup(ipIds[i])) {
revert Errors.GroupingModule__CannotAddGroupToGroup(groupIpId, ipIds[i]);
}
// check if the IP has the same license terms as the group
if (!LICENSE_REGISTRY.hasIpAttachedLicenseTerms(ipIds[i], groupLicenseTemplate, groupLicenseTermsId)) {
revert Errors.GroupingModule__IpHasNoGroupLicenseTerms(

Licensing.LicensingConfig memory lc = LICENSE_REGISTRY.verifyGroupAddIp(
groupIpId,
address(pool),
ipIds[i],
groupLicenseTemplate,
groupLicenseTermsId
);
uint256 totalGroupRewardShare = pool.addIp(groupIpId, ipIds[i], lc.expectMinimumGroupRewardShare);
if (totalGroupRewardShare > 100 * 10 ** 6) {
revert Errors.GroupingModule__TotalGroupRewardShareExceeds100Percent(
groupIpId,
totalGroupRewardShare,
ipIds[i],
groupLicenseTemplate,
groupLicenseTermsId
lc.expectMinimumGroupRewardShare
);
}
// IP must not have expiration time to be added to group
if (LICENSE_REGISTRY.getExpireTime(ipIds[i]) != 0) {
revert Errors.GroupingModule__CannotAddIpWithExpirationToGroup(ipIds[i]);
}
pool.addIp(groupIpId, ipIds[i]);
}

emit AddedIpToGroup(groupIpId, ipIds);
Expand Down
50 changes: 48 additions & 2 deletions contracts/registries/LicenseRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,9 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr
licensingHook: licensingConfig.licensingHook,
hookData: licensingConfig.hookData,
commercialRevShare: licensingConfig.commercialRevShare,
disabled: licensingConfig.disabled
disabled: licensingConfig.disabled,
expectMinimumGroupRewardShare: licensingConfig.expectMinimumGroupRewardShare,
expectGroupRewardPool: licensingConfig.expectGroupRewardPool
});

emit LicensingConfigSetForLicense(ipId, licenseTemplate, licenseTermsId, licensingConfig);
Expand All @@ -171,7 +173,9 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr
licensingHook: licensingConfig.licensingHook,
hookData: licensingConfig.hookData,
commercialRevShare: licensingConfig.commercialRevShare,
disabled: licensingConfig.disabled
disabled: licensingConfig.disabled,
expectMinimumGroupRewardShare: licensingConfig.expectMinimumGroupRewardShare,
expectGroupRewardPool: licensingConfig.expectGroupRewardPool
});
emit LicensingConfigSetForIP(ipId, licensingConfig);
}
Expand Down Expand Up @@ -297,6 +301,48 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr
return _getLicensingConfig(licensorIpId, licenseTemplate, licenseTermsId);
}

/// @notice Verifies the group can add given IP.
/// @param groupId The address of the group.
/// @param groupRewardPool The address of the reward pool of the group.
/// @param ipId The address of the IP to be added to the group.
/// @param groupLicenseTemplate the address of the license template attached to the group.
/// the IP must have this license template.
/// @param groupLicenseTermsId The ID of the license terms attached to the group.
/// the IP must have this license terms.
/// @return ipLicensingConfig The configuration for license attached to the IP.
function verifyGroupAddIp(
address groupId,
address groupRewardPool,
address ipId,
address groupLicenseTemplate,
uint256 groupLicenseTermsId
) external view returns (Licensing.LicensingConfig memory ipLicensingConfig) {
// check if the IP has the same license terms as the group
if (!_hasIpAttachedLicenseTerms(ipId, groupLicenseTemplate, groupLicenseTermsId)) {
revert Errors.LicenseRegistry__IpHasNoGroupLicenseTerms(ipId, groupLicenseTemplate, groupLicenseTermsId);
}
Licensing.LicensingConfig memory lct = _getLicensingConfig(ipId, groupLicenseTemplate, groupLicenseTermsId);
if (lct.disabled) {
revert Errors.LicenseRegistry__IpLicenseDisabled(ipId, groupLicenseTemplate, groupLicenseTermsId);
}
if (lct.expectGroupRewardPool == address(0)) {
revert Errors.LicenseRegistry__IpExpectGroupRewardPoolNotSet(ipId);
}
if (lct.expectGroupRewardPool != address(groupRewardPool)) {
revert Errors.LicenseRegistry__IpExpectGroupRewardPoolNotMatch(
ipId,
lct.expectGroupRewardPool,
groupId,
address(groupRewardPool)
);
}
// IP must not have expiration time to be added to group
if (_getExpireTime(ipId) != 0) {
revert Errors.LicenseRegistry__CannotAddIpWithExpirationToGroup(ipId);
}
ipLicensingConfig = lct;
}

/// @notice Checks if a license template is registered.
/// @param licenseTemplate The address of the license template to check.
/// @return Whether the license template is registered.
Expand Down
14 changes: 14 additions & 0 deletions test/foundry/integration/flows/grouping/Grouping.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
// contracts
// solhint-disable-next-line max-line-length
import { PILFlavors } from "../../../../../contracts/lib/PILFlavors.sol";
import { Licensing } from "../../../../../contracts/lib/Licensing.sol";
import { IGroupingModule } from "../../../../../contracts/interfaces/modules/grouping/IGroupingModule.sol";
import { IGroupIPAssetRegistry } from "../../../../../contracts/interfaces/registries/IGroupIPAssetRegistry.sol";

Expand Down Expand Up @@ -56,6 +57,17 @@ contract Flows_Integration_Grouping is BaseIntegration {

function test_Integration_Grouping() public {
// create a group
Licensing.LicensingConfig memory licensingConfig = Licensing.LicensingConfig({
isSet: true,
mintingFee: 0,
licensingHook: address(0),
hookData: "",
commercialRevShare: 10 * 10 ** 6,
disabled: false,
expectMinimumGroupRewardShare: 0,
expectGroupRewardPool: address(evenSplitGroupPool)
});

{
vm.startPrank(groupOwner);
groupId = groupingModule.registerGroup(address(evenSplitGroupPool));
Expand All @@ -68,6 +80,7 @@ contract Flows_Integration_Grouping is BaseIntegration {
ipAcct[1] = registerIpAccount(mockNFT, 1, u.alice);
vm.label(ipAcct[1], "IPAccount1");
licensingModule.attachLicenseTerms(ipAcct[1], address(pilTemplate), commRemixTermsId);
licensingModule.setLicensingConfig(ipAcct[1], address(pilTemplate), commRemixTermsId, licensingConfig);
vm.stopPrank();
}

Expand All @@ -76,6 +89,7 @@ contract Flows_Integration_Grouping is BaseIntegration {
ipAcct[2] = registerIpAccount(mockNFT, 2, u.bob);
vm.label(ipAcct[2], "IPAccount2");
licensingModule.attachLicenseTerms(ipAcct[2], address(pilTemplate), commRemixTermsId);
licensingModule.setLicensingConfig(ipAcct[2], address(pilTemplate), commRemixTermsId, licensingConfig);
vm.stopPrank();
}

Expand Down
19 changes: 17 additions & 2 deletions test/foundry/mocks/grouping/MockEvenSplitGroupPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,21 @@ contract MockEvenSplitGroupPool is IGroupRewardPool {
mapping(address groupId => mapping(address token => PoolInfo)) public poolInfo;
// Info of each user that stakes LP tokens. groupId => { token => { ipId => IpInfo}}
mapping(address groupId => mapping(address tokenId => mapping(address ipId => IpRewardInfo))) public ipRewardInfo;
mapping(address groupId => uint256 totalMinimumRewardShare) public totalMinimumRewardShare;
mapping(address groupId => mapping(address ipId => uint256 minimumRewardShare)) public minimumRewardShare;

constructor(address _royaltyModule) {
require(_royaltyModule != address(0), "RoyaltyModule address cannot be 0");
ROYALTY_MODULE = IRoyaltyModule(_royaltyModule);
}

function addIp(address groupId, address ipId) external {
function addIp(
address groupId,
address ipId,
uint256 minimumGroupRewardShare
) external returns (uint256 totalGroupRewardShare) {
// ignore if IP is already added to pool
if (ipAddedTime[groupId][ipId] != 0) return;
if (ipAddedTime[groupId][ipId] != 0) return totalMinimumRewardShare[groupId];
ipAddedTime[groupId][ipId] = block.timestamp;
// set rewardDebt of IP to current availableReward of the IP
totalMemberIPs[groupId] += 1;
Expand All @@ -52,7 +58,12 @@ contract MockEvenSplitGroupPool is IGroupRewardPool {
uint256 totalReward = poolInfo[groupId][token].accBalance;
ipRewardInfo[groupId][token][ipId].startPoolBalance = totalReward;
ipRewardInfo[groupId][token][ipId].rewardDebt = 0;
if (minimumGroupRewardShare > 0) {
minimumRewardShare[groupId][ipId] = minimumGroupRewardShare;
totalMinimumRewardShare[groupId] += minimumGroupRewardShare;
}
}
return totalMinimumRewardShare[groupId];
}

function removeIp(address groupId, address ipId) external {
Expand All @@ -67,6 +78,10 @@ contract MockEvenSplitGroupPool is IGroupRewardPool {
ipAddedTime[groupId][ipId] = 0;
}
totalMemberIPs[groupId] -= 1;
if (minimumRewardShare[groupId][ipId] > 0) {
totalMinimumRewardShare[groupId] -= minimumRewardShare[groupId][ipId];
minimumRewardShare[groupId][ipId] = 0;
}
}

/// @notice Returns the reward for each IP in the group
Expand Down
Loading

0 comments on commit 6aa8573

Please sign in to comment.