Skip to content
Open
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
33 changes: 30 additions & 3 deletions contracts/modules/grouping/EvenSplitGroupPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ contract EvenSplitGroupPool is IGroupRewardPool, ProtocolPausableUpgradeable, UU
uint128 pendingBalance; // Pending balance to be added to accRewardPerIp
uint128 accRewardPerIp; // Accumulated rewards per IP, times MAX_GROUP_SIZE.
uint256 averageRewardShare; // The avg reward share per IP, only increases as new IPs join with higher min share
bool isAverageRewardShareStale; // Whether the average reward share is stale
}

/// @dev Storage structure for the EvenSplitGroupPool
Expand Down Expand Up @@ -98,22 +99,30 @@ contract EvenSplitGroupPool is IGroupRewardPool, ProtocolPausableUpgradeable, UU
EvenSplitGroupPoolStorage storage $ = _getEvenSplitGroupPoolStorage();
GroupInfo storage groupInfo = $.groupInfo[groupId];
// ignore if IP is already added to pool
if (_isIpAdded(groupId, ipId)) return groupInfo.averageRewardShare * $.groupInfo[groupId].totalMembers;
if (_isIpAdded(groupId, ipId)) return groupInfo.averageRewardShare * groupInfo.totalMembers;
$.ipAddedTime[groupId][ipId] = block.timestamp;
groupInfo.totalMembers += 1;
if (groupInfo.totalMembers > MAX_GROUP_SIZE) {
revert Errors.EvenSplitGroupPool__MaxGroupSizeReached(groupId, groupInfo.totalMembers, MAX_GROUP_SIZE);
}
if (minimumGroupRewardShare > 0) {
$.minimumRewardShare[groupId][ipId] = minimumGroupRewardShare;
}
// Update the average reward share if it is currently stale and could prevent adding a new IP
if (
groupInfo.isAverageRewardShareStale && groupInfo.averageRewardShare * groupInfo.totalMembers > 100 * 10 ** 6
) {
_updateGroupAverageRewardShare(groupInfo, groupId);
} else if (minimumGroupRewardShare > 0) {
groupInfo.averageRewardShare = Math.max(groupInfo.averageRewardShare, minimumGroupRewardShare);
}
$.ipRewardDebt[groupId][ipId] = groupInfo.accRewardPerIp / MAX_GROUP_SIZE;
totalGroupRewardShare = groupInfo.averageRewardShare * groupInfo.totalMembers;
}

/// @notice Removes an IP from the group pool
/// @dev Only the GroupingModule can call this function
/// @dev Only the GroupingModule can call this function. The average reward share is marked as stale
/// if it may now be larger than the largest expectedGroupRewardShare among the group's remaining IPs.
/// @param groupId The group ID
/// @param ipId The IP ID
function removeIp(address groupId, address ipId) external onlyGroupingModule {
Expand All @@ -123,7 +132,11 @@ contract EvenSplitGroupPool is IGroupRewardPool, ProtocolPausableUpgradeable, UU
$.ipAddedTime[groupId][ipId] = 0;
GroupInfo storage groupInfo = $.groupInfo[groupId];
groupInfo.totalMembers -= 1;
if ($.minimumRewardShare[groupId][ipId] > 0) {
uint256 ipMinimumRewardShare = $.minimumRewardShare[groupId][ipId];
if (ipMinimumRewardShare == groupInfo.averageRewardShare) {
groupInfo.isAverageRewardShareStale = true;
}
if (ipMinimumRewardShare > 0) {
$.minimumRewardShare[groupId][ipId] = 0;
}
$.ipRewardDebt[groupId][ipId] = 0;
Expand Down Expand Up @@ -234,6 +247,20 @@ contract EvenSplitGroupPool is IGroupRewardPool, ProtocolPausableUpgradeable, UU
groupInfo.pendingBalance = 0;
}

function _updateGroupAverageRewardShare(GroupInfo storage groupInfo, address groupId) internal {
EvenSplitGroupPoolStorage storage $ = _getEvenSplitGroupPoolStorage();
uint32 totalIps = groupInfo.totalMembers;
address[] memory groupMembers = GROUP_IP_ASSET_REGISTRY.getGroupMembers(groupId, 0, totalIps);
uint256 updatedAverageRewardShare = 0;
for (uint256 i = 0; i < totalIps; i++) {
address ipId = groupMembers[i];
uint256 ipMinimumRewardShare = $.minimumRewardShare[groupId][ipId];
updatedAverageRewardShare = Math.max(updatedAverageRewardShare, ipMinimumRewardShare);
}
groupInfo.averageRewardShare = updatedAverageRewardShare;
groupInfo.isAverageRewardShareStale = false;
}

/// @dev Returns the available reward for each IP in the group of given token
/// @param groupId The group ID
/// @param token The reward token
Expand Down
94 changes: 94 additions & 0 deletions test/foundry/modules/grouping/EvenSplitGroupPool.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ERC721Holder } from "@openzeppelin/contracts/token/ERC721/utils/ERC721H

// contracts
import { PILFlavors } from "../../../../contracts/lib/PILFlavors.sol";
import { Licensing } from "../../../../contracts/modules/licensing/LicensingModule.sol";
// test
import { EvenSplitGroupPool } from "../../../../contracts/modules/grouping/EvenSplitGroupPool.sol";
import { MockERC721 } from "../../mocks/token/MockERC721.sol";
Expand Down Expand Up @@ -406,4 +407,97 @@ contract EvenSplitGroupPoolTest is BaseTest, ERC721Holder {
assertEq(rewards[2], 0);
vm.stopPrank();
}

function test_EvenSplitGroupPool_addIp_after_removeIp_updateGroupRewardShare() public {
uint256 termsId = pilTemplate.registerLicenseTerms(
PILFlavors.commercialRemix({
mintingFee: 0,
commercialRevShare: 10,
currencyToken: address(erc20),
royaltyPolicy: address(royaltyPolicyLRP)
})
);

Licensing.LicensingConfig memory licensingConfig = Licensing.LicensingConfig({
isSet: true,
mintingFee: 0,
licensingHook: address(0),
hookData: "",
commercialRevShare: 10 * 10 ** 6,
disabled: false,
expectMinimumGroupRewardShare: 50 * 10 ** 6,
expectGroupRewardPool: address(rewardPool)
});

Licensing.LicensingConfig memory licensingConfigZeroExpectedRewardShare = Licensing.LicensingConfig({
isSet: true,
mintingFee: 0,
licensingHook: address(0),
hookData: "",
commercialRevShare: 10 * 10 ** 6,
disabled: false,
expectMinimumGroupRewardShare: 0,
expectGroupRewardPool: address(rewardPool)
});

vm.startPrank(ipOwner1);
licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId);
licensingModule.setLicensingConfig(ipId1, address(pilTemplate), termsId, licensingConfig);
vm.stopPrank();

vm.startPrank(ipOwner2);
licensingModule.attachLicenseTerms(ipId2, address(pilTemplate), termsId);
licensingModule.setLicensingConfig(
ipId2,
address(pilTemplate),
termsId,
licensingConfigZeroExpectedRewardShare
);

vm.startPrank(ipOwner3);
licensingModule.attachLicenseTerms(ipId3, address(pilTemplate), termsId);
licensingModule.setLicensingConfig(
ipId3,
address(pilTemplate),
termsId,
licensingConfigZeroExpectedRewardShare
);
vm.stopPrank();

vm.startPrank(ipOwner5);
licensingModule.attachLicenseTerms(ipId5, address(pilTemplate), termsId);
licensingModule.setLicensingConfig(
ipId5,
address(pilTemplate),
termsId,
licensingConfigZeroExpectedRewardShare
);
vm.stopPrank();

licensingConfig.expectGroupRewardPool = address(0);
address group1 = groupingModule.registerGroup(address(rewardPool));
licensingModule.attachLicenseTerms(group1, address(pilTemplate), termsId);
licensingModule.setLicensingConfig(group1, address(pilTemplate), termsId, licensingConfig);

address[] memory ipIds = new address[](2);
ipIds[0] = ipId1;
ipIds[1] = ipId2;
groupingModule.addIp(group1, ipIds, 100e6);

assertEq(rewardPool.getTotalAllocatedRewardShare(group1), 100 * 10 ** 6);

ipIds = new address[](1);
ipIds[0] = ipId1;
groupingModule.removeIp(group1, ipIds);

assertEq(rewardPool.getTotalAllocatedRewardShare(group1), 50 * 10 ** 6);

ipIds = new address[](2);
ipIds[0] = ipId3;
ipIds[1] = ipId5;
groupingModule.addIp(group1, ipIds, 100e6);

// Group total reward share is updated to reflect the removal of the IP with the largest expectedRewardShare
assertEq(rewardPool.getTotalAllocatedRewardShare(group1), 0);
}
}