Skip to content

Commit

Permalink
Add Even Split Group Pool to Core Protocol (#260)
Browse files Browse the repository at this point in the history
  • Loading branch information
kingster-will authored Sep 27, 2024
1 parent 8ceef99 commit 7834ac5
Show file tree
Hide file tree
Showing 5 changed files with 482 additions and 24 deletions.
22 changes: 22 additions & 0 deletions contracts/lib/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -746,4 +746,26 @@ library Errors {

/// @notice Zero address provided for Access Manager.
error GroupNFT__ZeroAccessManager();

////////////////////////////////////////////////////////////////////////////
// EvenSplitGroup //
////////////////////////////////////////////////////////////////////////////

/// @notice Zero address provided for GroupingModule.
error EvenSplitGroupPool__ZeroGroupingModule();

/// @notice Zero address provided for RoyaltyModule.
error EvenSplitGroupPool__ZeroRoyaltyModule();

/// @notice Zero address provided for IPAssetRegistry.
error EvenSplitGroupPool__ZeroIPAssetRegistry();

/// @notice Caller is not the GroupingModule.
error EvenSplitGroupPool__CallerIsNotGroupingModule(address caller);

/// @notice Unregistered currency token.
error EvenSplitGroupPool__UnregisteredCurrencyToken(address currencyToken);

/// @notice Unregistered group IP.
error EvenSplitGroupPool__UnregisteredGroupIP(address groupId);
}
234 changes: 234 additions & 0 deletions contracts/modules/grouping/EvenSplitGroupPool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
// SPDX-License-Identifier: BUSL-1.1
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 { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

import { IGroupRewardPool } from "../../interfaces/modules/grouping/IGroupRewardPool.sol";
import { IRoyaltyModule } from "../../interfaces/modules/royalty/IRoyaltyModule.sol";
import { IIpRoyaltyVault } from "../../interfaces/modules/royalty/policies/IIpRoyaltyVault.sol";
import { IGroupingModule } from "../../interfaces/modules/grouping/IGroupingModule.sol";
import { IGroupIPAssetRegistry } from "../../interfaces/registries/IGroupIPAssetRegistry.sol";
import { ProtocolPausableUpgradeable } from "../../pause/ProtocolPausableUpgradeable.sol";
import { Errors } from "../../lib/Errors.sol";

contract EvenSplitGroupPool is IGroupRewardPool, ProtocolPausableUpgradeable, UUPSUpgradeable {
using SafeERC20 for IERC20;

/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
IRoyaltyModule public immutable ROYALTY_MODULE;

/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
IGroupingModule public immutable GROUPING_MODULE;

/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
IGroupIPAssetRegistry public immutable GROUP_IP_ASSET_REGISTRY;

/// @dev Storage structure for the EvenSplitGroupPool
/// @custom:storage-location erc7201:story-protocol.EvenSplitGroupPool
struct EvenSplitGroupPoolStorage {
mapping(address groupId => mapping(address ipId => uint256 addedTime)) ipAddedTime;
mapping(address groupId => uint256 totalIps) totalMemberIps;
mapping(address groupId => mapping(address token => uint256 balance)) poolBalance;
// pending reward = (PoolInfo.accBalance - startPoolBalance) / totalIp - ip.rewardDebt
mapping(address groupId => mapping(address tokenId => mapping(address ipId => uint256))) ipRewardDebt;
}

// keccak256(abi.encode(uint256(keccak256("story-protocol.EvenSplitGroupPool")) - 1)) & ~bytes32(uint256(0xff));
bytes32 private constant EvenSplitGroupPoolStorageLocation =
0xe17b84b8162358d82299c7eebd6a64b870d7aca42dea9a37e0604aeaf8f24700;

/// @dev Only allows the GroupingModule to call the function
modifier onlyGroupingModule() {
if (msg.sender != address(GROUPING_MODULE)) {
revert Errors.EvenSplitGroupPool__CallerIsNotGroupingModule(msg.sender);
}
_;
}

/// @notice Initializes the EvenSplitGroupPool contract
/// @param groupingModule The address of the grouping module
/// @param royaltyModule The address of the royalty module
/// @param ipAssetRegistry The address of the group IP asset registry
/// @custom:oz-upgrades-unsafe-allow constructor
constructor(address groupingModule, address royaltyModule, address ipAssetRegistry) {
if (groupingModule == address(0)) revert Errors.EvenSplitGroupPool__ZeroGroupingModule();
if (royaltyModule == address(0)) revert Errors.EvenSplitGroupPool__ZeroRoyaltyModule();
if (ipAssetRegistry == address(0)) revert Errors.EvenSplitGroupPool__ZeroIPAssetRegistry();
ROYALTY_MODULE = IRoyaltyModule(royaltyModule);
GROUPING_MODULE = IGroupingModule(groupingModule);
GROUP_IP_ASSET_REGISTRY = IGroupIPAssetRegistry(ipAssetRegistry);
_disableInitializers();
}

/// @notice initializer for this implementation contract
/// @param accessManager The address of the protocol admin roles contract
function initialize(address accessManager) public initializer {
if (accessManager == address(0)) {
revert Errors.GroupingModule__ZeroAccessManager();
}
__UUPSUpgradeable_init();
__ProtocolPausable_init(accessManager);
}

/// @notice Adds an IP to the group pool
/// @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;
EvenSplitGroupPoolStorage storage $ = _getEvenSplitGroupPoolStorage();
$.ipAddedTime[groupId][ipId] = block.timestamp;
$.totalMemberIps[groupId] += 1;
}

/// @notice Removes an IP from the group pool
/// @dev Only the GroupingModule can call this function
/// @param groupId The group ID
/// @param ipId The IP ID
function removeIp(address groupId, address ipId) external onlyGroupingModule {
// ignore if IP is not added to pool
if (!_isIpAdded(groupId, ipId)) return;
EvenSplitGroupPoolStorage storage $ = _getEvenSplitGroupPoolStorage();
$.ipAddedTime[groupId][ipId] = 0;
$.totalMemberIps[groupId] -= 1;
}

/// @notice Returns the reward for each IP in the group
/// @param groupId The group ID
/// @param token The reward token
/// @param ipIds The IP IDs
/// @return The rewards for each IP
function getAvailableReward(
address groupId,
address token,
address[] calldata ipIds
) external view returns (uint256[] memory) {
return _getAvailableReward(groupId, token, ipIds);
}

/// @notice Distributes rewards to the given IP accounts in pool
/// @param groupId The group ID
/// @param token The reward tokens
/// @param ipIds The IP IDs
function distributeRewards(
address groupId,
address token,
address[] calldata ipIds
) external whenNotPaused returns (uint256[] memory rewards) {
return _distributeRewards(groupId, token, ipIds);
}

/// @notice Collects royalty revenue to the group pool through royalty module
/// @param groupId The group ID
/// @param token The reward token
function collectRoyalties(address groupId, address token) external whenNotPaused {
_collectRoyalties(groupId, token);
}

/// @notice Deposits reward to the group pool directly
/// @param groupId The group ID
/// @param token The reward token
/// @param amount The amount of reward
function depositReward(address groupId, address token, uint256 amount) external whenNotPaused {
if (amount == 0) return;
if (!ROYALTY_MODULE.isWhitelistedRoyaltyToken(token))
revert Errors.EvenSplitGroupPool__UnregisteredCurrencyToken(token);
if (!GROUP_IP_ASSET_REGISTRY.isRegisteredGroup(groupId))
revert Errors.EvenSplitGroupPool__UnregisteredGroupIP(groupId);
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
_getEvenSplitGroupPoolStorage().poolBalance[groupId][token] += amount;
}

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

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

function getIpRewardDebt(address groupId, address token, address ipId) external view returns (uint256) {
return _getEvenSplitGroupPoolStorage().ipRewardDebt[groupId][token][ipId];
}

function isIPAdded(address groupId, address ipId) external view returns (bool) {
return _isIpAdded(groupId, ipId);
}

/// @dev Returns the available reward for each IP in the group of given token
/// @param groupId The group ID
/// @param token The reward token
/// @param ipIds The IP IDs
function _getAvailableReward(
address groupId,
address token,
address[] memory ipIds
) internal view returns (uint256[] memory) {
EvenSplitGroupPoolStorage storage $ = _getEvenSplitGroupPoolStorage();
uint256 totalIps = $.totalMemberIps[groupId];
if (totalIps == 0) return new uint256[](ipIds.length);

uint256 totalAccumulatePoolBalance = $.poolBalance[groupId][token];
uint256[] memory rewards = new uint256[](ipIds.length);
for (uint256 i = 0; i < ipIds.length; i++) {
// ignore if IP is not added to pool
if (!_isIpAdded(groupId, ipIds[i])) {
rewards[i] = 0;
continue;
}
uint256 rewardPerIP = totalAccumulatePoolBalance / totalIps;
rewards[i] = rewardPerIP - $.ipRewardDebt[groupId][token][ipIds[i]];
}
return rewards;
}

/// @dev Distributes rewards to the given IP accounts in pool
/// @param groupId The group ID
/// @param token The reward tokens
/// @param ipIds The IP IDs
function _distributeRewards(
address groupId,
address token,
address[] memory ipIds
) internal returns (uint256[] memory rewards) {
rewards = _getAvailableReward(groupId, token, ipIds);
EvenSplitGroupPoolStorage storage $ = _getEvenSplitGroupPoolStorage();
for (uint256 i = 0; i < ipIds.length; i++) {
if (rewards[i] == 0) continue;
// calculate pending reward for each IP
$.ipRewardDebt[groupId][token][ipIds[i]] += rewards[i];
// call royalty module to transfer reward to IP's vault as royalty
IERC20(token).safeTransfer(ROYALTY_MODULE.ipRoyaltyVaults(ipIds[i]), rewards[i]);
}
}

/// @dev Collects royalty revenue to the group pool through royalty module
/// @param groupId The group ID
/// @param token The reward token
function _collectRoyalties(address groupId, address token) internal {
IIpRoyaltyVault vault = IIpRoyaltyVault(ROYALTY_MODULE.ipRoyaltyVaults(groupId));
// ignore if group IP vault is not created
if (address(vault) == address(0)) return;
uint256[] memory snapshotsToClaim = new uint256[](1);
snapshotsToClaim[0] = vault.snapshot();
uint256 royalties = vault.claimRevenueOnBehalfBySnapshotBatch(snapshotsToClaim, token, address(this));
_getEvenSplitGroupPoolStorage().poolBalance[groupId][token] += royalties;
}

/// @dev checks if IP is added to group pool
function _isIpAdded(address groupId, address ipId) internal view returns (bool) {
return _getEvenSplitGroupPoolStorage().ipAddedTime[groupId][ipId] != 0;
}

function _authorizeUpgrade(address newImplementation) internal override restricted {}

/// @dev Returns the storage struct of EvenSplitGroupPool.
function _getEvenSplitGroupPoolStorage() private pure returns (EvenSplitGroupPoolStorage storage $) {
assembly {
$.slot := EvenSplitGroupPoolStorageLocation
}
}
}
6 changes: 4 additions & 2 deletions test/foundry/integration/flows/grouping/Grouping.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +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 { MockEvenSplitGroupPool } from "test/foundry/mocks/grouping/MockEvenSplitGroupPool.sol";
import { EvenSplitGroupPool } from "../../../../../contracts/modules/grouping/EvenSplitGroupPool.sol";
import { IGroupingModule } from "../../../../../contracts/interfaces/modules/grouping/IGroupingModule.sol";

// test
Expand Down Expand Up @@ -38,7 +38,9 @@ contract Flows_Integration_Grouping is BaseIntegration {

function setUp() public override {
super.setUp();
rewardPool = address(new MockEvenSplitGroupPool(address(royaltyModule)));
rewardPool = address(
new EvenSplitGroupPool(address(groupingModule), address(royaltyModule), address(ipAssetRegistry))
);
commRemixTermsId = registerSelectedPILicenseTerms(
"commercial_remix",
PILFlavors.commercialRemix({
Expand Down
Loading

0 comments on commit 7834ac5

Please sign in to comment.