diff --git a/contracts/interfaces/IClaimV2Adapter.sol b/contracts/interfaces/IClaimV2Adapter.sol new file mode 100644 index 000000000..0ee7b32ae --- /dev/null +++ b/contracts/interfaces/IClaimV2Adapter.sol @@ -0,0 +1,70 @@ +/* + Copyright 2020 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache License, Version 2.0 +*/ + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { ISetToken } from "../interfaces/ISetToken.sol"; + +pragma solidity 0.6.10; + +/** + * @title IClaimV2Adapter + * @author Set Protocol (modified by Index Cooperative) + * + * Interface for claim adapters that support arbitrary claim data parameters. + * Extends the base IClaimAdapter functionality by allowing additional bytes data + * to be passed to claim calls. + */ +interface IClaimV2Adapter { + + /** + * Generates the calldata for claiming tokens from the rewards pool + * + * @param _setToken the set token that is owed the tokens + * @param _rewardPool the rewards pool to claim from + * @param _claimData the claim data to use + * + * @return _subject the rewards pool to call + * @return _value the amount of ether to send in the call + * @return _calldata the calldata to use + */ + function getClaimCallData( + ISetToken _setToken, + address _rewardPool, + bytes calldata _claimData + ) external view returns(address _subject, uint256 _value, bytes memory _calldata); + + /** + * Gets the amount of unclaimed rewards + * + * @param _setToken the set token that is owed the tokens + * @param _rewardPool the rewards pool to check + * + * @return uint256 the amount of unclaimed rewards + */ + function getRewardsAmount(ISetToken _setToken, address _rewardPool) external view returns(uint256); + + /** + * Gets the rewards token + * + * @param _rewardPool the rewards pool to check + * + * @return IERC20 the reward token + */ + function getTokenAddress(address _rewardPool) external view returns(IERC20); +} diff --git a/contracts/mocks/integrations/ClaimAdapterMockV2.sol b/contracts/mocks/integrations/ClaimAdapterMockV2.sol new file mode 100644 index 000000000..1f762ec64 --- /dev/null +++ b/contracts/mocks/integrations/ClaimAdapterMockV2.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache License, Version 2.0 +pragma solidity 0.6.10; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ISetToken } from "../../interfaces/ISetToken.sol"; +import { IClaimV2Adapter } from "../../interfaces/IClaimV2Adapter.sol"; + +contract ClaimAdapterMockV2 is ERC20, IClaimV2Adapter { + /* ============ State Variables ============ */ + uint256 public rewards; + bytes public lastClaimData; + + /* ============ Constructor ============ */ + constructor() public ERC20("ClaimAdapterV2", "CLAIMV2") {} + + /* ============ External Functions ============ */ + function setRewards(uint256 _rewards) external { + rewards = _rewards; + } + + function mint(bytes memory _claimData) external { + lastClaimData = _claimData; + _mint(msg.sender, rewards); + } + + function getClaimCallData( + ISetToken _setToken, + address _rewardPool, + bytes memory _claimData + ) + external + view + override + returns (address _subject, uint256 _value, bytes memory _callData) + { + // Quell compiler warnings about unused vars + _setToken; + _rewardPool; + + bytes memory callData = abi.encodeWithSignature("mint(bytes)", _claimData); + return (address(this), 0, callData); + } + + function getRewardsAmount( + ISetToken _setToken, + address _rewardPool + ) + external + view + override + returns (uint256) + { + // Quell compiler warnings about unused vars + _setToken; + _rewardPool; + + return rewards; + } + + function getTokenAddress(address _rewardPool) + external + view + override + returns (IERC20) + { + // Quell compiler warnings about unused vars + _rewardPool; + + return this; + } +} \ No newline at end of file diff --git a/contracts/protocol/modules/v1/ClaimModuleV2.sol b/contracts/protocol/modules/v1/ClaimModuleV2.sol new file mode 100644 index 000000000..0cae6be13 --- /dev/null +++ b/contracts/protocol/modules/v1/ClaimModuleV2.sol @@ -0,0 +1,509 @@ +/* + Copyright 2020 Set Labs Inc. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + SPDX-License-Identifier: Apache License, Version 2.0 +*/ + +pragma solidity 0.6.10; +pragma experimental "ABIEncoderV2"; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { AddressArrayUtils } from "../../../lib/AddressArrayUtils.sol"; +import { IClaimV2Adapter } from "../../../interfaces/IClaimV2Adapter.sol"; +import { IController } from "../../../interfaces/IController.sol"; +import { ISetToken } from "../../../interfaces/ISetToken.sol"; +import { ModuleBase } from "../../lib/ModuleBase.sol"; + +/** + * @title ClaimModuleV2 + * @author Set Protocol (modified by Index Cooperative) + * + * Module that enables managers to claim tokens from external protocols that require additional claim data parameters. + * Extends ClaimModule functionality by allowing arbitrary bytes data to be passed to claim calls. + */ +contract ClaimModuleV2 is ModuleBase { + using AddressArrayUtils for address[]; + + /* ============ Events ============ */ + + event RewardClaimed( + ISetToken indexed _setToken, + address indexed _rewardPool, + IClaimV2Adapter indexed _adapter, + uint256 _amount, + bytes _claimData + ); + + event AnyoneClaimUpdated( + ISetToken indexed _setToken, + bool _anyoneClaim + ); + + /* ============ Modifiers ============ */ + + /** + * Throws if claim is confined to the manager and caller is not the manager + */ + modifier onlyValidCaller(ISetToken _setToken) { + require(_isValidCaller(_setToken), "Must be valid caller"); + _; + } + + /* ============ State Variables ============ */ + + // Indicates if any address can call claim or just the manager of the SetToken + mapping(ISetToken => bool) public anyoneClaim; + + // Map and array of rewardPool addresses to claim rewards for the SetToken + mapping(ISetToken => address[]) public rewardPoolList; + // Map from set tokens to rewards pool address to isAdded boolean. Used to check if a reward pool has been added in O(1) time + mapping(ISetToken => mapping(address => bool)) public rewardPoolStatus; + + // Map and array of adapters associated to the rewardPool for the SetToken + mapping(ISetToken => mapping(address => address[])) public claimSettings; + // Map from set tokens to rewards pool address to claim adapters to isAdded boolean. Used to check if an adapter has been added in O(1) time + mapping(ISetToken => mapping(address => mapping(address => bool))) public claimSettingsStatus; + + /* ============ Constructor ============ */ + + constructor(IController _controller) public ModuleBase(_controller) {} + + /* ============ External Functions ============ */ + + /** + * Claim the rewards available on the rewardPool for the specified claim integration. + * Callable only by manager unless manager has set anyoneClaim to true. + * + * @param _setToken Address of SetToken + * @param _rewardPool Address of the rewardPool that identifies the contract governing claims + * @param _integrationName ID of claim module integration (mapping on integration registry) + * @param _claimData Bytes data passed to claim function + */ + function claim( + ISetToken _setToken, + address _rewardPool, + string calldata _integrationName, + bytes calldata _claimData + ) + external + onlyValidAndInitializedSet(_setToken) + onlyValidCaller(_setToken) + { + _claim(_setToken, _rewardPool, _integrationName, _claimData); + } + + /** + * Claims rewards on all the passed rewardPool/claim integration pairs. Callable only by manager unless manager has + * set anyoneClaim to true. + * + * @param _setToken Address of SetToken + * @param _rewardPools Addresses of rewardPools that identifies the contract governing claims. Maps to same + * index integrationNames + * @param _integrationNames Human-readable names matching adapter used to collect claim on pool. Maps to same index + * in rewardPools + */ + function batchClaim( + ISetToken _setToken, + address[] calldata _rewardPools, + string[] calldata _integrationNames, + bytes[] calldata _claimData + ) + external + onlyValidAndInitializedSet(_setToken) + onlyValidCaller(_setToken) + { + uint256 poolArrayLength = _validateBatchArrays(_rewardPools, _integrationNames); + require(poolArrayLength == _claimData.length, "Array length mismatch"); + + for (uint256 i = 0; i < poolArrayLength; i++) { + _claim(_setToken, _rewardPools[i], _integrationNames[i], _claimData[i]); + } + } + + /** + * SET MANAGER ONLY. Update whether manager allows other addresses to call claim. + * + * @param _setToken Address of SetToken + * @param _anyoneClaim Boolean indicating if anyone can claim + */ + function updateAnyoneClaim(ISetToken _setToken, bool _anyoneClaim) external onlyManagerAndValidSet(_setToken) { + anyoneClaim[_setToken] = _anyoneClaim; + emit AnyoneClaimUpdated(_setToken, _anyoneClaim); + } + + /** + * SET MANAGER ONLY. Adds a new claim integration for an existent rewardPool. If rewardPool doesn't have existing + * claims then rewardPool is added to rewardPoolLiost. The claim integration is associated to an adapter that + * provides the functionality to claim the rewards for a specific token. + * + * @param _setToken Address of SetToken + * @param _rewardPool Address of the rewardPool that identifies the contract governing claims + * @param _integrationName ID of claim module integration (mapping on integration registry) + */ + function addClaim( + ISetToken _setToken, + address _rewardPool, + string calldata _integrationName + ) + external + onlyManagerAndValidSet(_setToken) + { + _addClaim(_setToken, _rewardPool, _integrationName); + } + + /** + * SET MANAGER ONLY. Adds a new rewardPool to the list to perform claims for the SetToken indicating the list of + * claim integrations. Each claim integration is associated to an adapter that provides the functionality to claim + * the rewards for a specific token. + * + * @param _setToken Address of SetToken + * @param _rewardPools Addresses of rewardPools that identifies the contract governing claims. Maps to same + * index integrationNames + * @param _integrationNames Human-readable names matching adapter used to collect claim on pool. Maps to same index + * in rewardPools + */ + function batchAddClaim( + ISetToken _setToken, + address[] calldata _rewardPools, + string[] calldata _integrationNames + ) + external + onlyManagerAndValidSet(_setToken) + { + _batchAddClaim(_setToken, _rewardPools, _integrationNames); + } + + /** + * SET MANAGER ONLY. Removes a claim integration from an existent rewardPool. If no claim remains for reward pool then + * reward pool is removed from rewardPoolList. + * + * @param _setToken Address of SetToken + * @param _rewardPool Address of the rewardPool that identifies the contract governing claims + * @param _integrationName ID of claim module integration (mapping on integration registry) + */ + function removeClaim( + ISetToken _setToken, + address _rewardPool, + string calldata _integrationName + ) + external + onlyManagerAndValidSet(_setToken) + { + _removeClaim(_setToken, _rewardPool, _integrationName); + } + + /** + * SET MANAGER ONLY. Batch removes claims from SetToken's settings. + * + * @param _setToken Address of SetToken + * @param _rewardPools Addresses of rewardPools that identifies the contract governing claims. Maps to same index + * integrationNames + * @param _integrationNames Human-readable names matching adapter used to collect claim on pool. Maps to same index in + * rewardPools + */ + function batchRemoveClaim( + ISetToken _setToken, + address[] calldata _rewardPools, + string[] calldata _integrationNames + ) + external + onlyManagerAndValidSet(_setToken) + { + uint256 poolArrayLength = _validateBatchArrays(_rewardPools, _integrationNames); + for (uint256 i = 0; i < poolArrayLength; i++) { + _removeClaim(_setToken, _rewardPools[i], _integrationNames[i]); + } + } + + /** + * SET MANAGER ONLY. Initializes this module to the SetToken. + * + * @param _setToken Instance of the SetToken to issue + * @param _anyoneClaim Boolean indicating if anyone can claim or just manager + * @param _rewardPools Addresses of rewardPools that identifies the contract governing claims. Maps to same index + * integrationNames + * @param _integrationNames Human-readable names matching adapter used to collect claim on pool. Maps to same index in + * rewardPools + */ + function initialize( + ISetToken _setToken, + bool _anyoneClaim, + address[] calldata _rewardPools, + string[] calldata _integrationNames + ) + external + onlySetManager(_setToken, msg.sender) + onlyValidAndPendingSet(_setToken) + { + _batchAddClaim(_setToken, _rewardPools, _integrationNames); + anyoneClaim[_setToken] = _anyoneClaim; + _setToken.initializeModule(); + } + + /** + * Removes this module from the SetToken, via call by the SetToken. + */ + function removeModule() external override { + ISetToken setToken = ISetToken(msg.sender); + delete anyoneClaim[setToken]; + + // Explicitly delete all elements for gas refund + address[] memory setTokenPoolList = rewardPoolList[setToken]; + for (uint256 i = 0; i < setTokenPoolList.length; i++) { + address[] storage adapterList = claimSettings[setToken][setTokenPoolList[i]]; + for (uint256 j = 0; j < adapterList.length; j++) { + address toRemove = adapterList[j]; + claimSettingsStatus[setToken][setTokenPoolList[i]][toRemove] = false; + delete adapterList[j]; + } + delete claimSettings[setToken][setTokenPoolList[i]]; + } + + for (uint256 i = 0; i < rewardPoolList[setToken].length; i++) { + address toRemove = rewardPoolList[setToken][i]; + rewardPoolStatus[setToken][toRemove] = false; + delete rewardPoolList[setToken][i]; + } + delete rewardPoolList[setToken]; + } + + /* ============ External View Functions ============ */ + + /** + * Get list of rewardPools to perform claims for the SetToken. + * + * @param _setToken Address of SetToken + * @return Array of rewardPool addresses to claim rewards for the SetToken + */ + function getRewardPools(ISetToken _setToken) external view returns (address[] memory) { + return rewardPoolList[_setToken]; + } + + /** + * Get boolean indicating if the rewardPool is in the list to perform claims for the SetToken. + * + * @param _setToken Address of SetToken + * @param _rewardPool Address of rewardPool + * @return Boolean indicating if the rewardPool is in the list for claims. + */ + function isRewardPool(ISetToken _setToken, address _rewardPool) public view returns (bool) { + return rewardPoolStatus[_setToken][_rewardPool]; + } + + /** + * Get list of claim integration of the rewardPool for the SetToken. + * + * @param _setToken Address of SetToken + * @param _rewardPool Address of rewardPool + * @return Array of adapter addresses associated to the rewardPool for the SetToken + */ + function getRewardPoolClaims(ISetToken _setToken, address _rewardPool) external view returns (address[] memory) { + return claimSettings[_setToken][_rewardPool]; + } + + /** + * Get boolean indicating if the adapter address of the claim integration is associated to the rewardPool. + * + * @param _setToken Address of SetToken + * @param _rewardPool Address of rewardPool + * @param _integrationName ID of claim module integration (mapping on integration registry) + * @return Boolean indicating if the claim integration is associated to the rewardPool. + */ + function isRewardPoolClaim( + ISetToken _setToken, + address _rewardPool, + string calldata _integrationName + ) + external + view + returns (bool) + { + address adapter = getAndValidateAdapter(_integrationName); + return claimSettingsStatus[_setToken][_rewardPool][adapter]; + } + + /** + * Get the rewards available to be claimed by the claim integration on the rewardPool. + * + * @param _setToken Address of SetToken + * @param _rewardPool Address of the rewardPool that identifies the contract governing claims + * @param _integrationName ID of claim module integration (mapping on integration registry) + * @return rewards Amount of units available to be claimed + */ + function getRewards( + ISetToken _setToken, + address _rewardPool, + string calldata _integrationName + ) + external + view + returns (uint256) + { + IClaimV2Adapter adapter = _getAndValidateIntegrationAdapter(_setToken, _rewardPool, _integrationName); + return adapter.getRewardsAmount(_setToken, _rewardPool); + } + + /* ============ Internal Functions ============ */ + + /** + * Claim the rewards, if available, on the rewardPool using the specified adapter. Interact with the adapter to get + * the rewards available, the calldata for the SetToken to invoke the claim and the token associated to the claim. + * + * @param _setToken Address of SetToken + * @param _rewardPool Address of the rewardPool that identifies the contract governing claims + * @param _integrationName Human readable name of claim integration + * @param _claimData Bytes data passed to claim function + */ + function _claim(ISetToken _setToken, address _rewardPool, string calldata _integrationName, bytes calldata _claimData) internal { + require(isRewardPool(_setToken, _rewardPool), "RewardPool not present"); + IClaimV2Adapter adapter = _getAndValidateIntegrationAdapter(_setToken, _rewardPool, _integrationName); + + IERC20 rewardsToken = IERC20(adapter.getTokenAddress(_rewardPool)); + uint256 initRewardsBalance = rewardsToken.balanceOf(address(_setToken)); + + ( + address callTarget, + uint256 callValue, + bytes memory callByteData + ) = adapter.getClaimCallData( + _setToken, + _rewardPool, + _claimData + ); + + _setToken.invoke(callTarget, callValue, callByteData); + + uint256 finalRewardsBalance = rewardsToken.balanceOf(address(_setToken)); + + emit RewardClaimed(_setToken, _rewardPool, adapter, finalRewardsBalance.sub(initRewardsBalance), _claimData); + } + + /** + * Gets the adapter and validate it is associated to the list of claim integration of a rewardPool. + * + * @param _setToken Address of SetToken + * @param _rewardsPool Address of rewards pool + * @param _integrationName ID of claim module integration (mapping on integration registry) + * @return IClaimV2Adapter The claim adapter + */ + function _getAndValidateIntegrationAdapter( + ISetToken _setToken, + address _rewardsPool, + string calldata _integrationName + ) + internal + view + returns (IClaimV2Adapter) + { + address adapter = getAndValidateAdapter(_integrationName); + require(claimSettingsStatus[_setToken][_rewardsPool][adapter], "Adapter integration not present"); + return IClaimV2Adapter(adapter); + } + + /** + * Validates and store the adapter address used to claim rewards for the passed rewardPool. If after adding + * adapter to pool length of adapters is 1 then add to rewardPoolList as well. + * + * @param _setToken Address of SetToken + * @param _rewardPool Address of the rewardPool that identifies the contract governing claims + * @param _integrationName ID of claim module integration (mapping on integration registry) + */ + function _addClaim(ISetToken _setToken, address _rewardPool, string calldata _integrationName) internal { + address adapter = getAndValidateAdapter(_integrationName); + address[] storage _rewardPoolClaimSettings = claimSettings[_setToken][_rewardPool]; + + require(!claimSettingsStatus[_setToken][_rewardPool][adapter], "Integration names must be unique"); + _rewardPoolClaimSettings.push(adapter); + claimSettingsStatus[_setToken][_rewardPool][adapter] = true; + + if (!rewardPoolStatus[_setToken][_rewardPool]) { + rewardPoolList[_setToken].push(_rewardPool); + rewardPoolStatus[_setToken][_rewardPool] = true; + } + } + + /** + * Internal version. Adds a new rewardPool to the list to perform claims for the SetToken indicating the list of claim + * integrations. Each claim integration is associated to an adapter that provides the functionality to claim the rewards + * for a specific token. + * + * @param _setToken Address of SetToken + * @param _rewardPools Addresses of rewardPools that identifies the contract governing claims. Maps to same + * index integrationNames + * @param _integrationNames Human-readable names matching adapter used to collect claim on pool. Maps to same index + * in rewardPools + */ + function _batchAddClaim( + ISetToken _setToken, + address[] calldata _rewardPools, + string[] calldata _integrationNames + ) + internal + { + uint256 poolArrayLength = _validateBatchArrays(_rewardPools, _integrationNames); + for (uint256 i = 0; i < poolArrayLength; i++) { + _addClaim(_setToken, _rewardPools[i], _integrationNames[i]); + } + } + + /** + * Validates and stores the adapter address used to claim rewards for the passed rewardPool. If no adapters + * left after removal then remove rewardPool from rewardPoolList and delete entry in claimSettings. + * + * @param _setToken Address of SetToken + * @param _rewardPool Address of the rewardPool that identifies the contract governing claims + * @param _integrationName ID of claim module integration (mapping on integration registry) + */ + function _removeClaim(ISetToken _setToken, address _rewardPool, string calldata _integrationName) internal { + address adapter = getAndValidateAdapter(_integrationName); + + require(claimSettingsStatus[_setToken][_rewardPool][adapter], "Integration must be added"); + claimSettings[_setToken][_rewardPool].removeStorage(adapter); + claimSettingsStatus[_setToken][_rewardPool][adapter] = false; + + if (claimSettings[_setToken][_rewardPool].length == 0) { + rewardPoolList[_setToken].removeStorage(_rewardPool); + rewardPoolStatus[_setToken][_rewardPool] = false; + } + } + + /** + * For batch functions validate arrays are of equal length and not empty. Return length of array for iteration. + * + * @param _rewardPools Addresses of the rewardPool that identifies the contract governing claims + * @param _integrationNames IDs of claim module integration (mapping on integration registry) + * @return Length of arrays + */ + function _validateBatchArrays( + address[] memory _rewardPools, + string[] calldata _integrationNames + ) + internal + pure + returns(uint256) + { + uint256 poolArrayLength = _rewardPools.length; + require(poolArrayLength == _integrationNames.length, "Array length mismatch"); + require(poolArrayLength > 0, "Arrays must not be empty"); + return poolArrayLength; + } + + /** + * If claim is confined to the manager, manager must be caller + * + * @param _setToken Address of SetToken + * @return bool Whether or not the caller is valid + */ + function _isValidCaller(ISetToken _setToken) internal view returns(bool) { + return anyoneClaim[_setToken] || isSetManager(_setToken, msg.sender); + } +} diff --git a/test/protocol/modules/v1/claimModuleV2.spec.ts b/test/protocol/modules/v1/claimModuleV2.spec.ts new file mode 100644 index 000000000..972a7c642 --- /dev/null +++ b/test/protocol/modules/v1/claimModuleV2.spec.ts @@ -0,0 +1,1438 @@ +import "module-alias/register"; + +import { Address, Bytes } from "@utils/types"; +import { Account } from "@utils/test/types"; +import { ClaimModuleV2, ClaimAdapterMockV2, SetToken } from "@utils/contracts"; +import DeployHelper from "@utils/deploys"; +import { + ether, +} from "@utils/index"; +import { + addSnapshotBeforeRestoreAfterEach, + getAccounts, + getWaffleExpect, + getSystemFixture, + getRandomAccount, + getRandomAddress, +} from "@utils/test/index"; +import { SystemFixture } from "@utils/fixtures"; +import { BigNumber } from "ethers"; +import { EMPTY_BYTES, ZERO } from "@utils/constants"; + +const expect = getWaffleExpect(); + +describe("ClaimModuleV2", () => { + let owner: Account; + let deployer: DeployHelper; + let setup: SystemFixture; + + let claimModuleV2: ClaimModuleV2; + let claimAdapterV2Mock: ClaimAdapterMockV2; + let claimAdapterV2Mock2: ClaimAdapterMockV2; + + const claimAdapterMockIntegrationName: string = "MOCK_CLAIM"; + const claimAdapterMockIntegrationName2: string = "MOCK2_CLAIM"; + + before(async () => { + [ + owner, + ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + setup = getSystemFixture(owner.address); + await setup.initialize(); + + claimModuleV2 = await deployer.modules.deployClaimModuleV2(setup.controller.address); + await setup.controller.addModule(claimModuleV2.address); + + claimAdapterV2Mock = await deployer.mocks.deployClaimAdapterMockV2(); + await setup.integrationRegistry.addIntegration(claimModuleV2.address, claimAdapterMockIntegrationName, claimAdapterV2Mock.address); + claimAdapterV2Mock2 = await deployer.mocks.deployClaimAdapterMockV2(); + await setup.integrationRegistry.addIntegration(claimModuleV2.address, claimAdapterMockIntegrationName2, claimAdapterV2Mock2.address); + }); + + addSnapshotBeforeRestoreAfterEach(); + + describe("#constructor", async () => { + let subjectController: Address; + + beforeEach(async () => { + subjectController = setup.controller.address; + }); + + async function subject(): Promise { + return deployer.modules.deployClaimModuleV2(subjectController); + } + + it("should set the correct controller", async () => { + const claimModule = await subject(); + + const controller = await claimModule.controller(); + expect(controller).to.eq(subjectController); + }); + }); + + describe("#initialize", async () => { + let setToken: SetToken; + let subjectSetToken: Address; + let subjectRewardPools: Address[]; + let subjectIntegrations: string[]; + let subjectAnyoneClaim: boolean; + let subjectCaller: Account; + + beforeEach(async () => { + setToken = await setup.createSetToken( + [setup.weth.address], + [ether(1)], + [claimModuleV2.address] + ); + subjectSetToken = setToken.address; + subjectRewardPools = [await getRandomAddress(), await getRandomAddress()]; + subjectIntegrations = [claimAdapterMockIntegrationName, claimAdapterMockIntegrationName2]; + subjectCaller = owner; + subjectAnyoneClaim = true; + }); + + async function subject(): Promise { + return claimModuleV2.connect(subjectCaller.wallet).initialize(subjectSetToken, subjectAnyoneClaim, subjectRewardPools, subjectIntegrations); + } + + it("should enable the Module on the SetToken", async () => { + await subject(); + const isModuleEnabled = await setToken.isInitializedModule(claimModuleV2.address); + expect(isModuleEnabled).to.eq(true); + }); + + it("should set the anyoneClaim field", async () => { + const anyoneClaimBefore = await claimModuleV2.anyoneClaim(subjectSetToken); + expect(anyoneClaimBefore).to.eq(false); + + await subject(); + + const anyoneClaim = await claimModuleV2.anyoneClaim(subjectSetToken); + expect(anyoneClaim).to.eq(true); + }); + + it("should add the rewardPools to the rewardPoolList", async () => { + expect((await claimModuleV2.getRewardPools(subjectSetToken)).length).to.eq(0); + + await subject(); + + const rewardPools = await claimModuleV2.getRewardPools(subjectSetToken); + expect(rewardPools[0]).to.eq(subjectRewardPools[0]); + expect(rewardPools[1]).to.eq(subjectRewardPools[1]); + }); + + it("should add all new integrations for the rewardPools", async () => { + await subject(); + + const rewardPoolOneClaims = await claimModuleV2.getRewardPoolClaims( + setToken.address, + subjectRewardPools[0] + ); + const rewardPoolTwoClaims = await claimModuleV2.getRewardPoolClaims( + setToken.address, + subjectRewardPools[1] + ); + expect(rewardPoolOneClaims[0]).to.eq(claimAdapterV2Mock.address); + expect(rewardPoolTwoClaims[0]).to.eq(claimAdapterV2Mock2.address); + }); + + describe("when the caller is not the SetToken manager", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be the SetToken manager"); + }); + }); + + describe("when module is in NONE state", async () => { + beforeEach(async () => { + await subject(); + await setToken.removeModule(claimModuleV2.address); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be pending initialization"); + }); + }); + + describe("when module is in INITIALIZED state", async () => { + beforeEach(async () => { + await subject(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be pending initialization"); + }); + }); + + describe("when the SetToken is not enabled on the controller", async () => { + beforeEach(async () => { + const nonEnabledSetToken = await setup.createNonControllerEnabledSetToken( + [setup.weth.address], + [ether(1)], + [claimModuleV2.address] + ); + + subjectSetToken = nonEnabledSetToken.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be controller-enabled SetToken"); + }); + }); + }); + + describe("#removeModule", async () => { + let setToken: SetToken; + let subjectModule: Address; + let subjectCaller: Account; + let anyoneClaim: boolean; + let rewardPool: Address; + + beforeEach(async () => { + setToken = await setup.createSetToken( + [setup.weth.address], + [ether(1)], + [claimModuleV2.address] + ); + anyoneClaim = true; + + subjectModule = claimModuleV2.address; + subjectCaller = owner; + + rewardPool = await getRandomAddress(); + await claimModuleV2.initialize(setToken.address, anyoneClaim, [rewardPool], [claimAdapterMockIntegrationName]); + }); + + async function subject(): Promise { + return setToken.connect(subjectCaller.wallet).removeModule(subjectModule); + } + + it("should properly remove the module and settings", async () => { + const rewardPoolsBefore = await claimModuleV2.getRewardPools(setToken.address); + const rewardPoolClaimsBefore = await claimModuleV2.getRewardPoolClaims(setToken.address, rewardPool); + const isPoolAddedBefore = await claimModuleV2.rewardPoolStatus(setToken.address, rewardPool); + const isAdapterAddedBefore = await claimModuleV2.claimSettingsStatus(setToken.address, rewardPool, claimAdapterV2Mock.address); + expect(rewardPoolsBefore.length).to.eq(1); + expect(rewardPoolClaimsBefore.length).to.eq(1); + expect(isPoolAddedBefore).to.be.true; + expect(isAdapterAddedBefore).to.be.true; + + await subject(); + + const rewardPools = await claimModuleV2.getRewardPools(setToken.address); + const rewardPoolClaims = await claimModuleV2.getRewardPoolClaims(setToken.address, rewardPool); + const isPoolAdded = await claimModuleV2.rewardPoolStatus(setToken.address, rewardPool); + const isAdapterAdded = await claimModuleV2.claimSettingsStatus(setToken.address, rewardPool, claimAdapterV2Mock.address); + expect(rewardPools.length).to.eq(0); + expect(rewardPoolClaims.length).to.eq(0); + expect(isPoolAdded).to.be.false; + expect(isAdapterAdded).to.be.false; + const isModuleEnabled = await setToken.isInitializedModule(subjectModule); + expect(isModuleEnabled).to.eq(false); + }); + }); + + describe("#updateAnyoneClaim", async () => { + let setToken: SetToken; + let isInitialized: boolean; + + let subjectSetToken: Address; + let subjectCaller: Account; + let subjectAnyoneClaim: boolean; + + before(async () => { + isInitialized = true; + }); + + beforeEach(async () => { + setToken = await setup.createSetToken( + [setup.weth.address], + [ether(1)], + [claimModuleV2.address] + ); + + if (isInitialized) { + await claimModuleV2.initialize(setToken.address, true, [await getRandomAddress()], [claimAdapterMockIntegrationName2]); + } + + subjectSetToken = setToken.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return claimModuleV2.connect(subjectCaller.wallet).updateAnyoneClaim(subjectSetToken, subjectAnyoneClaim); + } + + it("should change the anyoneClaim indicator", async () => { + const anyoneClaimBefore = await claimModuleV2.anyoneClaim(subjectSetToken); + expect(anyoneClaimBefore).to.eq(true); + + subjectAnyoneClaim = false; + await subject(); + + const anyoneClaim = await claimModuleV2.anyoneClaim(subjectSetToken); + expect(anyoneClaim).to.eq(false); + + subjectAnyoneClaim = true; + await subject(); + + const anyoneClaimAfter = await claimModuleV2.anyoneClaim(subjectSetToken); + expect(anyoneClaimAfter).to.eq(true); + }); + + describe("when caller is not SetToken manager", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be the SetToken manager"); + }); + }); + + describe("when module is not initialized", async () => { + before(async () => { + isInitialized = false; + }); + + after(async () => { + isInitialized = true; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be a valid and initialized SetToken"); + }); + }); + + describe("when the SetToken is not valid", async () => { + beforeEach(async () => { + const nonEnabledSetToken = await setup.createNonControllerEnabledSetToken( + [setup.weth.address], + [ether(1)], + [claimModuleV2.address] + ); + + subjectSetToken = nonEnabledSetToken.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be a valid and initialized SetToken"); + }); + }); + }); + + describe("#batchAddClaim", async () => { + let setToken: SetToken; + let isInitialized: boolean; + + let subjectSetToken: Address; + let subjectRewardPools: Address[]; + let subjectIntegrations: string[]; + let subjectCaller: Account; + + before(async () => { + isInitialized = true; + }); + + beforeEach(async () => { + setToken = await setup.createSetToken( + [setup.weth.address], + [ether(1)], + [claimModuleV2.address] + ); + + subjectCaller = owner; + const [rewardPoolOne, rewardPoolTwo] = [await getRandomAddress(), await getRandomAddress()]; + subjectRewardPools = [rewardPoolOne, rewardPoolOne, rewardPoolTwo]; + subjectIntegrations = [claimAdapterMockIntegrationName, claimAdapterMockIntegrationName2, claimAdapterMockIntegrationName]; + subjectSetToken = setToken.address; + + if (isInitialized) { + await claimModuleV2.initialize(setToken.address, true, [await getRandomAddress()], [claimAdapterMockIntegrationName2]); + } + }); + + async function subject(): Promise { + return claimModuleV2.connect(subjectCaller.wallet).batchAddClaim(subjectSetToken, subjectRewardPools, subjectIntegrations); + } + + it("should add the rewardPools to the rewardPoolList", async () => { + const isFirstAddedBefore = await claimModuleV2.rewardPoolStatus(subjectSetToken, subjectRewardPools[0]); + const isSecondAddedBefore = await claimModuleV2.rewardPoolStatus(subjectSetToken, subjectRewardPools[2]); + expect((await claimModuleV2.getRewardPools(subjectSetToken)).length).to.eq(1); + expect(isFirstAddedBefore).to.be.false; + expect(isSecondAddedBefore).to.be.false; + + await subject(); + + const rewardPools = await claimModuleV2.getRewardPools(subjectSetToken); + const isFirstAdded = await claimModuleV2.rewardPoolStatus(subjectSetToken, subjectRewardPools[0]); + const isSecondAdded = await claimModuleV2.rewardPoolStatus(subjectSetToken, subjectRewardPools[2]); + expect(rewardPools[1]).to.eq(subjectRewardPools[0]); + expect(rewardPools[2]).to.eq(subjectRewardPools[2]); + expect(isFirstAdded).to.be.true; + expect(isSecondAdded).to.be.true; + }); + + it("should add all new integrations for the rewardPools", async () => { + await subject(); + + const rewardPoolOneClaims = await claimModuleV2.getRewardPoolClaims( + setToken.address, + subjectRewardPools[0] + ); + const rewardPoolTwoClaims = await claimModuleV2.getRewardPoolClaims( + setToken.address, + subjectRewardPools[2] + ); + const isFirstIntegrationAddedPool1 = await claimModuleV2.claimSettingsStatus( + setToken.address, + subjectRewardPools[0], + claimAdapterV2Mock.address + ); + const isSecondIntegrationAddedPool1 = await claimModuleV2.claimSettingsStatus( + setToken.address, + subjectRewardPools[1], + claimAdapterV2Mock2.address + ); + const isIntegrationAddedPool2 = await claimModuleV2.claimSettingsStatus( + setToken.address, + subjectRewardPools[0], + claimAdapterV2Mock.address + ); + expect(rewardPoolOneClaims[0]).to.eq(claimAdapterV2Mock.address); + expect(rewardPoolOneClaims[1]).to.eq(claimAdapterV2Mock2.address); + expect(rewardPoolTwoClaims[0]).to.eq(claimAdapterV2Mock.address); + expect(isFirstIntegrationAddedPool1).to.be.true; + expect(isSecondIntegrationAddedPool1).to.be.true; + expect(isIntegrationAddedPool2).to.be.true; + }); + + describe("when passed arrays are different length", async () => { + beforeEach(async () => { + subjectIntegrations = [claimAdapterMockIntegrationName, claimAdapterMockIntegrationName]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Array length mismatch"); + }); + }); + + describe("when passed arrays are empty", async () => { + beforeEach(async () => { + subjectRewardPools = []; + subjectIntegrations = []; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Arrays must not be empty"); + }); + }); + + describe("when claim already added", async () => { + beforeEach(async () => { + subjectIntegrations = [claimAdapterMockIntegrationName, claimAdapterMockIntegrationName, claimAdapterMockIntegrationName]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Integration names must be unique"); + }); + }); + + describe("when caller is not SetToken manager", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be the SetToken manager"); + }); + }); + + describe("when the SetToken is not valid", async () => { + beforeEach(async () => { + const nonEnabledSetToken = await setup.createNonControllerEnabledSetToken( + [setup.weth.address], + [ether(1)], + [claimModuleV2.address] + ); + + subjectSetToken = nonEnabledSetToken.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be a valid and initialized SetToken"); + }); + }); + + describe("when module is not initialized", async () => { + before(async () => { + isInitialized = false; + }); + + after(async () => { + isInitialized = true; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be a valid and initialized SetToken"); + }); + }); + }); + + describe("#addClaim", async () => { + let setToken: SetToken; + let isInitialized: boolean; + + let subjectSetToken: Address; + let subjectRewardPool: Address; + let subjectIntegration: string; + let subjectCaller: Account; + + before(async () => { + isInitialized = true; + }); + + beforeEach(async () => { + setToken = await setup.createSetToken( + [setup.weth.address], + [ether(1)], + [claimModuleV2.address] + ); + + subjectCaller = owner; + + subjectRewardPool = await getRandomAddress(); + subjectIntegration = claimAdapterMockIntegrationName2; + subjectSetToken = setToken.address; + + if (isInitialized) { + await claimModuleV2.initialize(setToken.address, true, [await getRandomAddress()], [claimAdapterMockIntegrationName2]); + } + }); + + async function subject(): Promise { + return claimModuleV2.connect(subjectCaller.wallet).addClaim(subjectSetToken, subjectRewardPool, subjectIntegration); + } + + it("should add the rewardPool to the rewardPoolList and rewardPoolStatus", async () => { + expect(await claimModuleV2.isRewardPool(subjectSetToken, subjectRewardPool)).to.be.false; + + await subject(); + + expect(await claimModuleV2.isRewardPool(subjectSetToken, subjectRewardPool)).to.be.true; + expect(await claimModuleV2.rewardPoolList(subjectSetToken, 1)).to.eq(subjectRewardPool); + }); + + it("should add new integration for the rewardPool", async () => { + const rewardPoolClaimsBefore = await claimModuleV2.getRewardPoolClaims(setToken.address, subjectRewardPool); + const isIntegrationAddedBefore = await claimModuleV2.claimSettingsStatus(setToken.address, subjectRewardPool, claimAdapterV2Mock2.address); + expect(rewardPoolClaimsBefore.length).to.eq(0); + expect(isIntegrationAddedBefore).to.be.false; + + await subject(); + + const rewardPoolClaims = await claimModuleV2.getRewardPoolClaims(setToken.address, subjectRewardPool); + const isIntegrationAdded = await claimModuleV2.claimSettingsStatus(setToken.address, subjectRewardPool, claimAdapterV2Mock2.address); + expect(rewardPoolClaims.length).to.eq(1); + expect(rewardPoolClaims[0]).to.eq(claimAdapterV2Mock2.address); + expect(isIntegrationAdded).to.be.true; + }); + + describe("when new claim is being added to existing rewardPool", async () => { + beforeEach(async () => { + await claimModuleV2.addClaim(subjectSetToken, subjectRewardPool, claimAdapterMockIntegrationName); + }); + + it("should add new integration for the rewardPool", async () => { + const rewardPoolClaimsBefore = await claimModuleV2.getRewardPoolClaims(setToken.address, subjectRewardPool); + const isIntegrationAddedBefore = await claimModuleV2.claimSettingsStatus(setToken.address, subjectRewardPool, claimAdapterV2Mock2.address); + expect(rewardPoolClaimsBefore.length).to.eq(1); + expect(isIntegrationAddedBefore).to.be.false; + + await subject(); + + const isIntegrationAdded = await claimModuleV2.claimSettingsStatus(setToken.address, subjectRewardPool, claimAdapterV2Mock2.address); + const rewardPoolClaims = await claimModuleV2.getRewardPoolClaims(setToken.address, subjectRewardPool); + expect(rewardPoolClaims.length).to.eq(2); + expect(rewardPoolClaims[1]).to.eq(claimAdapterV2Mock2.address); + expect(isIntegrationAdded).to.be.true; + }); + + it("should not add the rewardPool again", async () => { + expect(await claimModuleV2.isRewardPool(subjectSetToken, subjectRewardPool)).to.be.true; + + await subject(); + + const rewardPools = await claimModuleV2.getRewardPools(subjectSetToken); + expect(rewardPools.length).to.eq(2); + expect(rewardPools[1]).to.eq(subjectRewardPool); + }); + }); + + describe("when claim already added", async () => { + beforeEach(async () => { + await claimModuleV2.addClaim(subjectSetToken, subjectRewardPool, subjectIntegration); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Integration names must be unique"); + }); + }); + + describe("when caller is not SetToken manager", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be the SetToken manager"); + }); + }); + + describe("when the SetToken is not valid", async () => { + beforeEach(async () => { + const nonEnabledSetToken = await setup.createNonControllerEnabledSetToken( + [setup.weth.address], + [ether(1)], + [claimModuleV2.address] + ); + + subjectSetToken = nonEnabledSetToken.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be a valid and initialized SetToken"); + }); + }); + + describe("when module is not initialized", async () => { + before(async () => { + isInitialized = false; + }); + + after(async () => { + isInitialized = true; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be a valid and initialized SetToken"); + }); + }); + }); + + describe("#batchRemoveClaim", async () => { + let setToken: SetToken; + let subjectCaller: Account; + let subjectRewardPools: Address[]; + let subjectIntegrations: string[]; + let subjectSetToken: Address; + let isInitialized: boolean; + + before(async () => { + isInitialized = true; + }); + + beforeEach(async () => { + setToken = await setup.createSetToken( + [setup.weth.address], + [ether(1)], + [claimModuleV2.address] + ); + + subjectCaller = owner; + + subjectRewardPools = [await getRandomAddress(), await getRandomAddress()]; + subjectSetToken = setToken.address; + subjectIntegrations = [claimAdapterMockIntegrationName, claimAdapterMockIntegrationName]; + + if (isInitialized) { + await claimModuleV2.initialize(setToken.address, true, [await getRandomAddress()], [claimAdapterMockIntegrationName2]); + await claimModuleV2.batchAddClaim(subjectSetToken, subjectRewardPools, subjectIntegrations); + } + }); + + async function subject(): Promise { + return claimModuleV2.connect(subjectCaller.wallet).batchRemoveClaim(subjectSetToken, subjectRewardPools, subjectIntegrations); + } + + it("should remove the adapter associated to the reward pool", async () => { + const rewardPoolOneClaimsBefore = await claimModuleV2.getRewardPoolClaims(setToken.address, subjectRewardPools[0]); + const rewardPoolTwoClaimsBefore = await claimModuleV2.getRewardPoolClaims(setToken.address, subjectRewardPools[1]); + const isRewardPoolOneAdapterOneBefore = await claimModuleV2.claimSettingsStatus( + setToken.address, + subjectRewardPools[0], + claimAdapterV2Mock.address + ); + const isRewardPoolTwoAdapterOneBefore = await claimModuleV2.claimSettingsStatus( + setToken.address, + subjectRewardPools[0], + claimAdapterV2Mock.address + ); + expect(rewardPoolOneClaimsBefore.length).to.eq(1); + expect(rewardPoolTwoClaimsBefore.length).to.eq(1); + expect(isRewardPoolOneAdapterOneBefore).to.be.true; + expect(isRewardPoolTwoAdapterOneBefore).to.be.true; + + await subject(); + + const rewardPoolOneClaims = await claimModuleV2.getRewardPoolClaims(setToken.address, subjectRewardPools[0]); + const rewardPoolTwoClaims = await claimModuleV2.getRewardPoolClaims(setToken.address, subjectRewardPools[1]); + const isRewardPoolOneAdapterOne = await claimModuleV2.claimSettingsStatus(setToken.address, subjectRewardPools[0], claimAdapterV2Mock.address); + const isRewardPoolTwoAdapterOne = await claimModuleV2.claimSettingsStatus(setToken.address, subjectRewardPools[0], claimAdapterV2Mock.address); + expect(rewardPoolOneClaims.length).to.eq(0); + expect(rewardPoolTwoClaims.length).to.eq(0); + expect(isRewardPoolOneAdapterOne).to.be.false; + expect(isRewardPoolTwoAdapterOne).to.be.false; + + }); + + it("should remove the rewardPool from the rewardPoolStatus", async () => { + expect(await claimModuleV2.isRewardPool(subjectSetToken, subjectRewardPools[0])).to.be.true; + expect(await claimModuleV2.isRewardPool(subjectSetToken, subjectRewardPools[1])).to.be.true; + + await subject(); + + expect(await claimModuleV2.isRewardPool(subjectSetToken, subjectRewardPools[0])).to.be.false; + expect(await claimModuleV2.isRewardPool(subjectSetToken, subjectRewardPools[1])).to.be.false; + }); + + describe("when the claim integration is not present", async () => { + beforeEach(async () => { + subjectRewardPools = [owner.address, owner.address]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Integration must be added"); + }); + }); + + describe("when passed arrays are different length", async () => { + beforeEach(async () => { + subjectIntegrations = [claimAdapterMockIntegrationName]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Array length mismatch"); + }); + }); + + describe("when passed arrays are empty", async () => { + beforeEach(async () => { + subjectRewardPools = []; + subjectIntegrations = []; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Arrays must not be empty"); + }); + }); + + describe("when caller is not SetToken manager", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be the SetToken manager"); + }); + }); + + describe("when the SetToken is not valid", async () => { + beforeEach(async () => { + const nonEnabledSetToken = await setup.createNonControllerEnabledSetToken( + [setup.weth.address], + [ether(1)], + [claimModuleV2.address] + ); + + subjectSetToken = nonEnabledSetToken.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be a valid and initialized SetToken"); + }); + }); + + describe("when module is not initialized", async () => { + before(async () => { + isInitialized = false; + }); + + after(async () => { + isInitialized = true; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be a valid and initialized SetToken"); + }); + }); + }); + + describe("#removeClaim", async () => { + let setToken: SetToken; + let subjectCaller: Account; + let subjectRewardPool: Address; + let subjectIntegration: string; + let subjectSetToken: Address; + let isInitialized: boolean; + + before(async () => { + isInitialized = true; + }); + + beforeEach(async () => { + setToken = await setup.createSetToken( + [setup.weth.address], + [ether(1)], + [claimModuleV2.address] + ); + + subjectCaller = owner; + + subjectRewardPool = await getRandomAddress(); + subjectSetToken = setToken.address; + subjectIntegration = claimAdapterMockIntegrationName; + + if (isInitialized) { + await claimModuleV2.initialize(setToken.address, true, [await getRandomAddress()], [claimAdapterMockIntegrationName2]); + await claimModuleV2.addClaim(subjectSetToken, subjectRewardPool, subjectIntegration); + } + }); + + async function subject(): Promise { + return claimModuleV2.connect(subjectCaller.wallet).removeClaim(subjectSetToken, subjectRewardPool, subjectIntegration); + } + + it("should remove the adapter associated to the reward pool", async () => { + const rewardPoolClaimsBefore = await claimModuleV2.getRewardPoolClaims(setToken.address, subjectRewardPool); + const isAdapterAddedBefore = await claimModuleV2.claimSettingsStatus(setToken.address, subjectRewardPool, claimAdapterV2Mock.address); + expect(rewardPoolClaimsBefore.length).to.eq(1); + expect(isAdapterAddedBefore).to.be.true; + + await subject(); + + const rewardPoolClaims = await claimModuleV2.getRewardPoolClaims(setToken.address, subjectRewardPool); + const isAdapterAdded = await claimModuleV2.claimSettingsStatus(setToken.address, subjectRewardPool, claimAdapterV2Mock.address); + expect(rewardPoolClaims.length).to.eq(0); + expect(isAdapterAdded).to.be.false; + }); + + it("should remove the rewardPool from the rewardPoolStatus", async () => { + expect(await claimModuleV2.isRewardPool(subjectSetToken, subjectRewardPool)).to.be.true; + + await subject(); + + expect(await claimModuleV2.isRewardPool(subjectSetToken, subjectRewardPool)).to.be.false; + }); + + describe("when the rewardPool still has integrations left after removal", async () => { + beforeEach(async () => { + await claimModuleV2.addClaim(subjectSetToken, subjectRewardPool, claimAdapterMockIntegrationName2); + }); + + it("should remove the adapter associated to the reward pool", async () => { + const rewardPoolClaimsBefore = await claimModuleV2.getRewardPoolClaims(setToken.address, subjectRewardPool); + const isAdapterAddedBefore = await claimModuleV2.claimSettingsStatus(setToken.address, subjectRewardPool, claimAdapterV2Mock.address); + expect(rewardPoolClaimsBefore.length).to.eq(2); + expect(isAdapterAddedBefore).to.be.true; + + await subject(); + + const rewardPoolClaims = await claimModuleV2.getRewardPoolClaims(setToken.address, subjectRewardPool); + const isAdapterAdded = await claimModuleV2.claimSettingsStatus(setToken.address, subjectRewardPool, claimAdapterV2Mock.address); + expect(rewardPoolClaims.length).to.eq(1); + expect(rewardPoolClaims[0]).to.eq(claimAdapterV2Mock2.address); + expect(isAdapterAdded).to.be.false; + }); + + it("should not remove the rewardPool from the rewardPoolStatus", async () => { + expect(await claimModuleV2.isRewardPool(subjectSetToken, subjectRewardPool)).to.be.true; + + await subject(); + + expect(await claimModuleV2.isRewardPool(subjectSetToken, subjectRewardPool)).to.be.true; + }); + }); + + describe("when the claim integration is not present", async () => { + beforeEach(async () => { + subjectIntegration = claimAdapterMockIntegrationName2; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Integration must be added"); + }); + }); + + describe("when caller is not SetToken manager", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be the SetToken manager"); + }); + }); + + describe("when the SetToken is not valid", async () => { + beforeEach(async () => { + const nonEnabledSetToken = await setup.createNonControllerEnabledSetToken( + [setup.weth.address], + [ether(1)], + [claimModuleV2.address] + ); + + subjectSetToken = nonEnabledSetToken.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be a valid and initialized SetToken"); + }); + }); + + describe("when module is not initialized", async () => { + before(async () => { + isInitialized = false; + }); + + after(async () => { + isInitialized = true; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be a valid and initialized SetToken"); + }); + }); + }); + + describe("#getRewards", async () => { + let setToken: SetToken; + let subjectRewardPool: Address; + let subjectIntegration: string; + let subjectSetToken: Address; + let subjectRewards: BigNumber; + + beforeEach(async () => { + setToken = await setup.createSetToken( + [setup.weth.address], + [ether(1)], + [claimModuleV2.address] + ); + + subjectRewardPool = await getRandomAddress(); + subjectSetToken = setToken.address; + subjectIntegration = claimAdapterMockIntegrationName; + subjectRewards = ether(1); + + await claimModuleV2.initialize(setToken.address, true, [await getRandomAddress()], [claimAdapterMockIntegrationName2]); + await claimModuleV2.addClaim(subjectSetToken, subjectRewardPool, subjectIntegration); + await claimAdapterV2Mock.setRewards(subjectRewards); + }); + + async function subject(): Promise { + return claimModuleV2.getRewards(subjectSetToken, subjectRewardPool, subjectIntegration); + } + + it("should return the rewards and tokens associated", async () => { + const rewards = await subject(); + expect(rewards).to.eq(subjectRewards); + }); + + describe("when the rewardPool is not present", async () => { + + beforeEach(async () => { + subjectRewardPool = await getRandomAddress(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Adapter integration not present"); + }); + }); + + describe("when the claim integration is not present", async () => { + + beforeEach(async () => { + subjectIntegration = claimAdapterMockIntegrationName2; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Adapter integration not present"); + }); + }); + }); + + describe("#isRewardPoolClaim", async () => { + let setToken: SetToken; + let subjectRewardPool: Address; + let subjectIntegration: string; + let subjectSetToken: Address; + + beforeEach(async () => { + setToken = await setup.createSetToken( + [setup.weth.address], + [ether(1)], + [claimModuleV2.address] + ); + + subjectRewardPool = await getRandomAddress(); + subjectSetToken = setToken.address; + subjectIntegration = claimAdapterMockIntegrationName; + + await claimModuleV2.initialize(setToken.address, true, [await getRandomAddress()], [claimAdapterMockIntegrationName2]); + await claimModuleV2.addClaim(subjectSetToken, subjectRewardPool, subjectIntegration); + }); + + async function subject(): Promise { + return claimModuleV2.isRewardPoolClaim(subjectSetToken, subjectRewardPool, subjectIntegration); + } + + it("should return true", async () => { + const isReward = await subject(); + expect(isReward).to.be.true; + }); + + describe("when the rewardPool is not present", async () => { + beforeEach(async () => { + subjectIntegration = claimAdapterMockIntegrationName2; + }); + + it("should return false", async () => { + const isReward = await subject(); + expect(isReward).to.be.false; + }); + }); + }); + + describe("#claim", async () => { + let setToken: SetToken; + let rewards: BigNumber; + let anyoneClaim: boolean; + let isInitialized: boolean; + + let subjectCaller: Account; + let subjectRewardPool: Address; + let subjectIntegration: string; + let subjectSetToken: Address; + + before(async () => { + rewards = ether(1); + anyoneClaim = true; + isInitialized = true; + }); + + beforeEach(async () => { + setToken = await setup.createSetToken( + [setup.weth.address], + [ether(1)], + [claimModuleV2.address] + ); + + subjectCaller = owner; + + subjectRewardPool = await getRandomAddress(); + subjectSetToken = setToken.address; + subjectIntegration = claimAdapterMockIntegrationName; + + if (isInitialized) { + await claimModuleV2.initialize(setToken.address, anyoneClaim, [subjectRewardPool], [subjectIntegration]); + } + + await claimAdapterV2Mock.setRewards(rewards); + }); + + async function subject(): Promise { + return claimModuleV2.connect(subjectCaller.wallet).claim(subjectSetToken, subjectRewardPool, subjectIntegration, EMPTY_BYTES); + } + + it("should claim the rewards on the rewardPool for the claim integration", async () => { + const balanceBefore = await claimAdapterV2Mock.balanceOf(subjectSetToken); + expect(balanceBefore).to.eq(ZERO); + + await subject(); + + const balance = await claimAdapterV2Mock.balanceOf(subjectSetToken); + expect(balance).to.eq(rewards); + }); + + it("emits the correct RewardClaimed event", async () => { + await expect(subject()).to.emit(claimModuleV2, "RewardClaimed").withArgs( + subjectSetToken, + subjectRewardPool, + claimAdapterV2Mock.address, + rewards, + EMPTY_BYTES + ); + }); + + describe("when anyoneClaim is true and caller is not manager", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should claim the rewards on the rewardPool for the claim integration", async () => { + const balanceBefore = await claimAdapterV2Mock.balanceOf(subjectSetToken); + expect(balanceBefore).to.eq(ZERO); + + await subject(); + + const balance = await claimAdapterV2Mock.balanceOf(subjectSetToken); + expect(balance).to.eq(rewards); + }); + + it("emits the correct RewardClaimed event", async () => { + await expect(subject()).to.emit(claimModuleV2, "RewardClaimed").withArgs( + subjectSetToken, + subjectRewardPool, + claimAdapterV2Mock.address, + rewards, + EMPTY_BYTES + ); + }); + }); + + describe("when anyoneClaim is false and caller is the manager", async () => { + before(async () => { + anyoneClaim = false; + }); + + after(async () => { + anyoneClaim = true; + }); + + it("should claim the rewards on the rewardPool for the claim integration", async () => { + const balanceBefore = await claimAdapterV2Mock.balanceOf(subjectSetToken); + expect(balanceBefore).to.eq(ZERO); + + await subject(); + + const balance = await claimAdapterV2Mock.balanceOf(subjectSetToken); + expect(balance).to.eq(rewards); + }); + + it("emits the correct RewardClaimed event", async () => { + await expect(subject()).to.emit(claimModuleV2, "RewardClaimed").withArgs( + subjectSetToken, + subjectRewardPool, + claimAdapterV2Mock.address, + rewards, + EMPTY_BYTES + ); + }); + }); + + describe("when the rewardPool is not present", async () => { + beforeEach(async () => { + subjectRewardPool = await getRandomAddress(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("RewardPool not present"); + }); + }); + + describe("when the claim integration is not present", async () => { + + beforeEach(async () => { + subjectIntegration = claimAdapterMockIntegrationName2; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Adapter integration not present"); + }); + }); + + describe("when anyoneClaim is false and caller is not manager", async () => { + before(async () => { + anyoneClaim = false; + }); + + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + after(async () => { + anyoneClaim = true; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be valid caller"); + }); + }); + + describe("when module is not initialized", async () => { + before(async () => { + isInitialized = false; + }); + + after(async () => { + isInitialized = true; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be a valid and initialized SetToken"); + }); + }); + + describe("when SetToken is not valid", async () => { + beforeEach(async () => { + const nonEnabledSetToken = await setup.createNonControllerEnabledSetToken( + [setup.weth.address], + [ether(1)], + [claimModuleV2.address] + ); + + subjectSetToken = nonEnabledSetToken.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be a valid and initialized SetToken"); + }); + }); + }); + + describe("#batchClaim", async () => { + let setToken: SetToken; + let rewards: BigNumber; + let anyoneClaim: boolean; + let isInitialized: boolean; + + let subjectCaller: Account; + let subjectRewardPools: Address[]; + let subjectIntegrations: string[]; + let subjectData: Bytes[]; + let subjectSetToken: Address; + + before(async () => { + rewards = ether(1); + anyoneClaim = true; + isInitialized = true; + }); + + beforeEach(async () => { + setToken = await setup.createSetToken( + [setup.weth.address], + [ether(1)], + [claimModuleV2.address] + ); + + subjectCaller = owner; + + subjectRewardPools = [await getRandomAddress(), await getRandomAddress()]; + subjectSetToken = setToken.address; + subjectIntegrations = [claimAdapterMockIntegrationName, claimAdapterMockIntegrationName2]; + subjectData = [EMPTY_BYTES, EMPTY_BYTES]; + + if (isInitialized) { + await claimModuleV2.initialize(setToken.address, anyoneClaim, subjectRewardPools, subjectIntegrations); + } + + await claimAdapterV2Mock.setRewards(rewards); + await claimAdapterV2Mock2.setRewards(rewards); + }); + + async function subject(): Promise { + return claimModuleV2.connect(subjectCaller.wallet).batchClaim(subjectSetToken, subjectRewardPools, subjectIntegrations, subjectData); + } + + it("should claim the rewards on the rewardPool for the claim integration", async () => { + const balanceBefore = await claimAdapterV2Mock.balanceOf(subjectSetToken); + expect(balanceBefore).to.eq(ZERO); + + await subject(); + + const balance = await claimAdapterV2Mock.balanceOf(subjectSetToken); + expect(balance).to.eq(rewards); + }); + + it("emits the correct RewardClaimed event", async () => { + await expect(subject()).to.emit(claimModuleV2, "RewardClaimed").withArgs( + subjectSetToken, + subjectRewardPools[0], + claimAdapterV2Mock.address, + rewards, + subjectData[0] + ); + }); + + it("emits the correct RewardClaimed event", async () => { + await expect(subject()).to.emit(claimModuleV2, "RewardClaimed").withArgs( + subjectSetToken, + subjectRewardPools[1], + claimAdapterV2Mock2.address, + rewards, + subjectData[1] + ); + }); + + describe("when anyoneClaim is true and caller is not manager", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should claim the rewards on the rewardPool for the claim integration", async () => { + const balanceBefore = await claimAdapterV2Mock.balanceOf(subjectSetToken); + expect(balanceBefore).to.eq(ZERO); + + await subject(); + + const balance = await claimAdapterV2Mock.balanceOf(subjectSetToken); + expect(balance).to.eq(rewards); + }); + + it("emits the correct RewardClaimed event", async () => { + await expect(subject()).to.emit(claimModuleV2, "RewardClaimed").withArgs( + subjectSetToken, + subjectRewardPools[0], + claimAdapterV2Mock.address, + rewards, + subjectData[0] + ); + }); + + it("emits the correct RewardClaimed event", async () => { + await expect(subject()).to.emit(claimModuleV2, "RewardClaimed").withArgs( + subjectSetToken, + subjectRewardPools[1], + claimAdapterV2Mock2.address, + rewards, + subjectData[1] + ); + }); + }); + + describe("when anyoneClaim is false and caller is the manager", async () => { + before(async () => { + anyoneClaim = false; + }); + + after(async () => { + anyoneClaim = true; + }); + + it("should claim the rewards on the rewardPool for the claim integration", async () => { + const balanceBefore = await claimAdapterV2Mock.balanceOf(subjectSetToken); + expect(balanceBefore).to.eq(ZERO); + + await subject(); + + const balance = await claimAdapterV2Mock.balanceOf(subjectSetToken); + expect(balance).to.eq(rewards); + }); + + it("emits the correct RewardClaimed event", async () => { + await expect(subject()).to.emit(claimModuleV2, "RewardClaimed").withArgs( + subjectSetToken, + subjectRewardPools[0], + claimAdapterV2Mock.address, + rewards, + subjectData[0] + ); + }); + + it("emits the correct RewardClaimed event", async () => { + await expect(subject()).to.emit(claimModuleV2, "RewardClaimed").withArgs( + subjectSetToken, + subjectRewardPools[1], + claimAdapterV2Mock2.address, + rewards, + subjectData[1] + ); + }); + }); + + describe("when the rewardPool is not present", async () => { + beforeEach(async () => { + subjectRewardPools = [await getRandomAddress(), await getRandomAddress()]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("RewardPool not present"); + }); + }); + + describe("when the claim integration is not present", async () => { + beforeEach(async () => { + subjectIntegrations = [claimAdapterMockIntegrationName2, claimAdapterMockIntegrationName]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Adapter integration not present"); + }); + }); + + describe("when passed arrays are different length", async () => { + beforeEach(async () => { + subjectIntegrations = [claimAdapterMockIntegrationName]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Array length mismatch"); + }); + }); + + describe("when passed arrays are empty", async () => { + beforeEach(async () => { + subjectRewardPools = []; + subjectIntegrations = []; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Arrays must not be empty"); + }); + }); + + describe("when anyoneClaim is false and caller is not manager", async () => { + before(async () => { + anyoneClaim = false; + }); + + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + after(async () => { + anyoneClaim = true; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be valid caller"); + }); + }); + + describe("when module is not initialized", async () => { + before(async () => { + isInitialized = false; + }); + + after(async () => { + isInitialized = true; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be a valid and initialized SetToken"); + }); + }); + + describe("when SetToken is not valid", async () => { + beforeEach(async () => { + const nonEnabledSetToken = await setup.createNonControllerEnabledSetToken( + [setup.weth.address], + [ether(1)], + [claimModuleV2.address] + ); + + subjectSetToken = nonEnabledSetToken.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be a valid and initialized SetToken"); + }); + }); + }); +}); diff --git a/utils/contracts/index.ts b/utils/contracts/index.ts index e664d4ba9..ce9128b8e 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -23,7 +23,9 @@ export { BytesArrayUtilsMock } from "../../typechain/BytesArrayUtilsMock"; export { CachedSetValuer } from "../../typechain/CachedSetValuer"; export { ChainlinkAggregatorMock } from "../../typechain/ChainlinkAggregatorMock"; export { ClaimAdapterMock } from "../../typechain/ClaimAdapterMock"; +export { ClaimAdapterMockV2 } from "../../typechain/ClaimAdapterMockV2"; export { ClaimModule } from "../../typechain/ClaimModule"; +export { ClaimModuleV2 } from "../../typechain/ClaimModuleV2"; export { Compound } from "../../typechain/Compound"; export { CompoundBravoGovernanceAdapter } from "../../typechain/CompoundBravoGovernanceAdapter"; export { CompoundLikeGovernanceAdapter } from "../../typechain/CompoundLikeGovernanceAdapter"; diff --git a/utils/deploys/deployMocks.ts b/utils/deploys/deployMocks.ts index 03a7fee2e..1a9369021 100644 --- a/utils/deploys/deployMocks.ts +++ b/utils/deploys/deployMocks.ts @@ -133,6 +133,8 @@ import { UnitConversionUtilsMock__factory } from "../../typechain/factories/Unit import { SetTokenAccessibleMock__factory } from "../../typechain/factories/SetTokenAccessibleMock__factory"; import { WrappedfCashMock__factory } from "../../typechain/factories/WrappedfCashMock__factory"; import { WrappedfCashFactoryMock__factory } from "../../typechain/factories/WrappedfCashFactoryMock__factory"; +import { ClaimAdapterMockV2 } from "../../typechain/ClaimAdapterMockV2"; +import { ClaimAdapterMockV2__factory } from "../../typechain/factories/ClaimAdapterMockV2__factory"; export default class DeployMocks { private _deployerSigner: Signer; @@ -439,6 +441,10 @@ export default class DeployMocks { return await new ClaimAdapterMock__factory(this._deployerSigner).deploy(); } + public async deployClaimAdapterMockV2(): Promise { + return await new ClaimAdapterMockV2__factory(this._deployerSigner).deploy(); + } + public async deployGaugeControllerMock(): Promise { return await new GaugeControllerMock__factory(this._deployerSigner).deploy(); } diff --git a/utils/deploys/deployModules.ts b/utils/deploys/deployModules.ts index bffa8d38e..15e2bfec9 100644 --- a/utils/deploys/deployModules.ts +++ b/utils/deploys/deployModules.ts @@ -56,6 +56,8 @@ import { StreamingFeeModule__factory } from "../../typechain/factories/Streaming import { TradeModule__factory } from "../../typechain/factories/TradeModule__factory"; import { WrapModule__factory } from "../../typechain/factories/WrapModule__factory"; import { WrapModuleV2__factory } from "../../typechain/factories/WrapModuleV2__factory"; +import { ClaimModuleV2 } from "../../typechain/ClaimModuleV2"; +import { ClaimModuleV2__factory } from "../../typechain/factories/ClaimModuleV2__factory"; export default class DeployModules { private _deployerSigner: Signer; @@ -299,4 +301,8 @@ export default class DeployModules { public async deployAuctionRebalanceModuleV1(controller: Address): Promise { return await new AuctionRebalanceModuleV1__factory(this._deployerSigner).deploy(controller); } + + public async deployClaimModuleV2(controller: Address): Promise { + return await new ClaimModuleV2__factory(this._deployerSigner).deploy(controller); + } }