diff --git a/contracts/ManagerCore.sol b/contracts/ManagerCore.sol new file mode 100644 index 0000000..8f2da81 --- /dev/null +++ b/contracts/ManagerCore.sol @@ -0,0 +1,168 @@ +/* + Copyright 2022 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; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; + +import { AddressArrayUtils } from "./lib/AddressArrayUtils.sol"; + +/** + * @title ManagerCore + * @author Set Protocol + * + * Registry for governance approved DelegatedManagerFactories and DelegatedManagers. + */ +contract ManagerCore is Ownable { + using AddressArrayUtils for address[]; + + /* ============ Events ============ */ + + event FactoryAdded(address indexed _factory); + event FactoryRemoved(address indexed _factory); + event ManagerAdded(address indexed _manager, address indexed _factory); + event ManagerRemoved(address indexed _manager); + + /* ============ Modifiers ============ */ + + /** + * Throws if function is called by any address other than a valid factory. + */ + modifier onlyFactory() { + require(isFactory[msg.sender], "Only valid factories can call"); + _; + } + + modifier onlyInitialized() { + require(isInitialized, "Contract must be initialized."); + _; + } + + /* ============ State Variables ============ */ + + // List of enabled managers + address[] public managers; + // List of enabled factories of managers + address[] public factories; + + // Mapping to check whether address is valid Manager or Factory + mapping(address => bool) public isManager; + mapping(address => bool) public isFactory; + + // Return true if the ManagerCore is initialized + bool public isInitialized; + + /* ============ External Functions ============ */ + + /** + * Initializes any predeployed factories. Note: This function can only be called by + * the owner once to batch initialize the initial system contracts. + * + * @param _factories List of factories to add + */ + function initialize( + address[] memory _factories + ) + external + onlyOwner + { + require(!isInitialized, "ManagerCore is already initialized"); + + factories = _factories; + + // Loop through and initialize isFactory mapping + for (uint256 i = 0; i < _factories.length; i++) { + address factory = _factories[i]; + require(factory != address(0), "Zero address submitted."); + isFactory[factory] = true; + } + + // Set to true to only allow initialization once + isInitialized = true; + } + + /** + * PRIVILEGED FACTORY FUNCTION. Adds a newly deployed manager as an enabled manager. + * + * @param _manager Address of the manager contract to add + */ + function addManager(address _manager) external onlyInitialized onlyFactory { + require(!isManager[_manager], "Manager already exists"); + + isManager[_manager] = true; + + managers.push(_manager); + + emit ManagerAdded(_manager, msg.sender); + } + + /** + * PRIVILEGED GOVERNANCE FUNCTION. Allows governance to remove a manager + * + * @param _manager Address of the manager contract to remove + */ + function removeManager(address _manager) external onlyInitialized onlyOwner { + require(isManager[_manager], "Manager does not exist"); + + managers.removeStorage(_manager); + + isManager[_manager] = false; + + emit ManagerRemoved(_manager); + } + + /** + * PRIVILEGED GOVERNANCE FUNCTION. Allows governance to add a factory + * + * @param _factory Address of the factory contract to add + */ + function addFactory(address _factory) external onlyInitialized onlyOwner { + require(!isFactory[_factory], "Factory already exists"); + + isFactory[_factory] = true; + + factories.push(_factory); + + emit FactoryAdded(_factory); + } + + /** + * PRIVILEGED GOVERNANCE FUNCTION. Allows governance to remove a factory + * + * @param _factory Address of the factory contract to remove + */ + function removeFactory(address _factory) external onlyInitialized onlyOwner { + require(isFactory[_factory], "Factory does not exist"); + + factories.removeStorage(_factory); + + isFactory[_factory] = false; + + emit FactoryRemoved(_factory); + } + + /* ============ External Getter Functions ============ */ + + function getManagers() external view returns (address[] memory) { + return managers; + } + + function getFactories() external view returns (address[] memory) { + return factories; + } +} \ No newline at end of file diff --git a/contracts/extensions/IssuanceExtension.sol b/contracts/extensions/IssuanceExtension.sol new file mode 100644 index 0000000..0405ba2 --- /dev/null +++ b/contracts/extensions/IssuanceExtension.sol @@ -0,0 +1,277 @@ +/* + Copyright 2022 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 { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; + +import { ISetToken } from "@setprotocol/set-protocol-v2/contracts/interfaces/ISetToken.sol"; +import { PreciseUnitMath } from "@setprotocol/set-protocol-v2/contracts/lib/PreciseUnitMath.sol"; + +import { BaseGlobalExtension } from "../lib/BaseGlobalExtension.sol"; +import { IDelegatedManager } from "../interfaces/IDelegatedManager.sol"; +import { IIssuanceModule } from "../interfaces/IIssuanceModule.sol"; +import { IManagerCore } from "../interfaces/IManagerCore.sol"; + +/** + * @title IssuanceExtension + * @author Set Protocol + * + * Smart contract global extension which provides DelegatedManager owner and methodologist the ability to accrue and split + * issuance and redemption fees. Owner may configure the fee split percentages. + */ +contract IssuanceExtension is BaseGlobalExtension { + using Address for address; + using PreciseUnitMath for uint256; + using SafeMath for uint256; + + /* ============ Events ============ */ + + event IssuanceExtensionInitialized( + address indexed _setToken, + address indexed _delegatedManager + ); + + event FeesDistributed( + address _setToken, + address indexed _ownerFeeRecipient, + address indexed _methodologist, + uint256 _ownerTake, + uint256 _methodologistTake + ); + + /* ============ State Variables ============ */ + + // Instance of IssuanceModule + IIssuanceModule public immutable issuanceModule; + + /* ============ Constructor ============ */ + + constructor( + IManagerCore _managerCore, + IIssuanceModule _issuanceModule + ) + public + BaseGlobalExtension(_managerCore) + { + issuanceModule = _issuanceModule; + } + + /* ============ External Functions ============ */ + + /** + * ANYONE CALLABLE: Distributes fees accrued to the DelegatedManager. Calculates fees for + * owner and methodologist, and sends to owner fee recipient and methodologist respectively. + */ + function distributeFees(ISetToken _setToken) public { + IDelegatedManager delegatedManager = _manager(_setToken); + + uint256 totalFees = _setToken.balanceOf(address(delegatedManager)); + + address methodologist = delegatedManager.methodologist(); + address ownerFeeRecipient = delegatedManager.ownerFeeRecipient(); + + uint256 ownerTake = totalFees.preciseMul(delegatedManager.ownerFeeSplit()); + uint256 methodologistTake = totalFees.sub(ownerTake); + + if (ownerTake > 0) { + delegatedManager.transferTokens(address(_setToken), ownerFeeRecipient, ownerTake); + } + + if (methodologistTake > 0) { + delegatedManager.transferTokens(address(_setToken), methodologist, methodologistTake); + } + + emit FeesDistributed(address(_setToken), ownerFeeRecipient, methodologist, ownerTake, methodologistTake); + } + + /** + * ONLY OWNER: Initializes IssuanceModule on the SetToken associated with the DelegatedManager. + * + * @param _delegatedManager Instance of the DelegatedManager to initialize the IssuanceModule for + * @param _maxManagerFee Maximum fee that can be charged on issue and redeem + * @param _managerIssueFee Fee to charge on issuance + * @param _managerRedeemFee Fee to charge on redemption + * @param _feeRecipient Address to send fees to + * @param _managerIssuanceHook Instance of the contract with the Pre-Issuance Hook function + */ + function initializeModule( + IDelegatedManager _delegatedManager, + uint256 _maxManagerFee, + uint256 _managerIssueFee, + uint256 _managerRedeemFee, + address _feeRecipient, + address _managerIssuanceHook + ) + external + onlyOwnerAndValidManager(_delegatedManager) + { + require(_delegatedManager.isInitializedExtension(address(this)), "Extension must be initialized"); + + _initializeModule( + _delegatedManager.setToken(), + _delegatedManager, + _maxManagerFee, + _managerIssueFee, + _managerRedeemFee, + _feeRecipient, + _managerIssuanceHook + ); + } + + /** + * ONLY OWNER: Initializes IssuanceExtension to the DelegatedManager. + * + * @param _delegatedManager Instance of the DelegatedManager to initialize + */ + function initializeExtension(IDelegatedManager _delegatedManager) external onlyOwnerAndValidManager(_delegatedManager) { + require(_delegatedManager.isPendingExtension(address(this)), "Extension must be pending"); + + ISetToken setToken = _delegatedManager.setToken(); + + _initializeExtension(setToken, _delegatedManager); + + IssuanceExtensionInitialized(address(setToken), address(_delegatedManager)); + } + + /** + * ONLY OWNER: Initializes IssuanceExtension to the DelegatedManager and IssuanceModule to the SetToken + * + * @param _delegatedManager Instance of the DelegatedManager to initialize + * @param _maxManagerFee Maximum fee that can be charged on issue and redeem + * @param _managerIssueFee Fee to charge on issuance + * @param _managerRedeemFee Fee to charge on redemption + * @param _feeRecipient Address to send fees to + * @param _managerIssuanceHook Instance of the contract with the Pre-Issuance Hook function + */ + function initializeModuleAndExtension( + IDelegatedManager _delegatedManager, + uint256 _maxManagerFee, + uint256 _managerIssueFee, + uint256 _managerRedeemFee, + address _feeRecipient, + address _managerIssuanceHook + ) + external + onlyOwnerAndValidManager(_delegatedManager) + { + require(_delegatedManager.isPendingExtension(address(this)), "Extension must be pending"); + + ISetToken setToken = _delegatedManager.setToken(); + + _initializeExtension(setToken, _delegatedManager); + _initializeModule( + setToken, + _delegatedManager, + _maxManagerFee, + _managerIssueFee, + _managerRedeemFee, + _feeRecipient, + _managerIssuanceHook + ); + + IssuanceExtensionInitialized(address(setToken), address(_delegatedManager)); + } + + /** + * ONLY MANAGER: Remove an existing SetToken and DelegatedManager tracked by the IssuanceExtension + */ + function removeExtension() external override { + _removeExtension(); + } + + /** + * ONLY OWNER: Updates issuance fee on IssuanceModule. + * + * @param _setToken Instance of the SetToken to update issue fee for + * @param _newFee New issue fee percentage in precise units (1% = 1e16, 100% = 1e18) + */ + function updateIssueFee(ISetToken _setToken, uint256 _newFee) + external + onlyOwner(_setToken) + { + bytes memory callData = abi.encodeWithSignature("updateIssueFee(address,uint256)", _setToken, _newFee); + _invokeManager(_manager(_setToken), address(issuanceModule), callData); + } + + /** + * ONLY OWNER: Updates redemption fee on IssuanceModule. + * + * @param _setToken Instance of the SetToken to update redeem fee for + * @param _newFee New redeem fee percentage in precise units (1% = 1e16, 100% = 1e18) + */ + function updateRedeemFee(ISetToken _setToken, uint256 _newFee) + external + onlyOwner(_setToken) + { + bytes memory callData = abi.encodeWithSignature("updateRedeemFee(address,uint256)", _setToken, _newFee); + _invokeManager(_manager(_setToken), address(issuanceModule), callData); + } + + /** + * ONLY OWNER: Updates fee recipient on IssuanceModule + * + * @param _setToken Instance of the SetToken to update fee recipient for + * @param _newFeeRecipient Address of new fee recipient. This should be the address of the DelegatedManager + */ + function updateFeeRecipient(ISetToken _setToken, address _newFeeRecipient) + external + onlyOwner(_setToken) + { + bytes memory callData = abi.encodeWithSignature("updateFeeRecipient(address,address)", _setToken, _newFeeRecipient); + _invokeManager(_manager(_setToken), address(issuanceModule), callData); + } + + /* ============ Internal Functions ============ */ + + /** + * Internal function to initialize IssuanceModule on the SetToken associated with the DelegatedManager. + * + * @param _setToken Instance of the SetToken corresponding to the DelegatedManager + * @param _delegatedManager Instance of the DelegatedManager to initialize the TradeModule for + * @param _maxManagerFee Maximum fee that can be charged on issue and redeem + * @param _managerIssueFee Fee to charge on issuance + * @param _managerRedeemFee Fee to charge on redemption + * @param _feeRecipient Address to send fees to + * @param _managerIssuanceHook Instance of the contract with the Pre-Issuance Hook function + */ + function _initializeModule( + ISetToken _setToken, + IDelegatedManager _delegatedManager, + uint256 _maxManagerFee, + uint256 _managerIssueFee, + uint256 _managerRedeemFee, + address _feeRecipient, + address _managerIssuanceHook + ) + internal + { + bytes memory callData = abi.encodeWithSignature( + "initialize(address,uint256,uint256,uint256,address,address)", + _setToken, + _maxManagerFee, + _managerIssueFee, + _managerRedeemFee, + _feeRecipient, + _managerIssuanceHook + ); + _invokeManager(_delegatedManager, address(issuanceModule), callData); + } +} \ No newline at end of file diff --git a/contracts/extensions/StreamingFeeSplitExtension.sol b/contracts/extensions/StreamingFeeSplitExtension.sol new file mode 100644 index 0000000..a1e455a --- /dev/null +++ b/contracts/extensions/StreamingFeeSplitExtension.sol @@ -0,0 +1,223 @@ +/* + Copyright 2022 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 { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; + +import { ISetToken } from "@setprotocol/set-protocol-v2/contracts/interfaces/ISetToken.sol"; +import { PreciseUnitMath } from "@setprotocol/set-protocol-v2/contracts/lib/PreciseUnitMath.sol"; + +import { BaseGlobalExtension } from "../lib/BaseGlobalExtension.sol"; +import { IDelegatedManager } from "../interfaces/IDelegatedManager.sol"; +import { IManagerCore } from "../interfaces/IManagerCore.sol"; +import { IStreamingFeeModule } from "../interfaces/IStreamingFeeModuleV2.sol"; + +/** + * @title StreamingFeeSplitExtension + * @author Set Protocol + * + * Smart contract global extension which provides DelegatedManager owner and methodologist the ability to accrue and split + * streaming fees. Owner may configure the fee split percentages. + */ +contract StreamingFeeSplitExtension is BaseGlobalExtension { + using Address for address; + using PreciseUnitMath for uint256; + using SafeMath for uint256; + + /* ============ Events ============ */ + + event StreamingFeeSplitExtensionInitialized( + address indexed _setToken, + address indexed _delegatedManager + ); + + event FeesDistributed( + address _setToken, + address indexed _ownerFeeRecipient, + address indexed _methodologist, + uint256 _ownerTake, + uint256 _methodologistTake + ); + + /* ============ State Variables ============ */ + + // Instance of StreamingFeeModule + IStreamingFeeModule public immutable streamingFeeModule; + + /* ============ Constructor ============ */ + + constructor( + IManagerCore _managerCore, + IStreamingFeeModule _streamingFeeModule + ) + public + BaseGlobalExtension(_managerCore) + { + streamingFeeModule = _streamingFeeModule; + } + + /* ============ External Functions ============ */ + + /** + * ANYONE CALLABLE: Accrues fees from streaming fee module. Gets resulting balance after fee accrual, calculates fees for + * owner and methodologist, and sends to owner fee recipient and methodologist respectively. + */ + function accrueFeesAndDistribute(ISetToken _setToken) public { + // Emits a FeeActualized event + streamingFeeModule.accrueFee(_setToken); + + IDelegatedManager delegatedManager = _manager(_setToken); + + uint256 totalFees = _setToken.balanceOf(address(delegatedManager)); + + address methodologist = delegatedManager.methodologist(); + address ownerFeeRecipient = delegatedManager.ownerFeeRecipient(); + + uint256 ownerTake = totalFees.preciseMul(delegatedManager.ownerFeeSplit()); + uint256 methodologistTake = totalFees.sub(ownerTake); + + if (ownerTake > 0) { + delegatedManager.transferTokens(address(_setToken), ownerFeeRecipient, ownerTake); + } + + if (methodologistTake > 0) { + delegatedManager.transferTokens(address(_setToken), methodologist, methodologistTake); + } + + emit FeesDistributed(address(_setToken), ownerFeeRecipient, methodologist, ownerTake, methodologistTake); + } + + /** + * ONLY OWNER: Initializes StreamingFeeModule on the SetToken associated with the DelegatedManager. + * + * @param _delegatedManager Instance of the DelegatedManager to initialize the StreamingFeeModule for + * @param _settings FeeState struct defining fee parameters for StreamingFeeModule initialization + */ + function initializeModule( + IDelegatedManager _delegatedManager, + IStreamingFeeModule.FeeState memory _settings + ) + external + onlyOwnerAndValidManager(_delegatedManager) + { + require(_delegatedManager.isInitializedExtension(address(this)), "Extension must be initialized"); + + _initializeModule(_delegatedManager.setToken(), _delegatedManager, _settings); + } + + /** + * ONLY OWNER: Initializes StreamingFeeSplitExtension to the DelegatedManager. + * + * @param _delegatedManager Instance of the DelegatedManager to initialize + */ + function initializeExtension(IDelegatedManager _delegatedManager) external onlyOwnerAndValidManager(_delegatedManager) { + require(_delegatedManager.isPendingExtension(address(this)), "Extension must be pending"); + + ISetToken setToken = _delegatedManager.setToken(); + + _initializeExtension(setToken, _delegatedManager); + + StreamingFeeSplitExtensionInitialized(address(setToken), address(_delegatedManager)); + } + + /** + * ONLY OWNER: Initializes StreamingFeeSplitExtension to the DelegatedManager and StreamingFeeModule to the SetToken + * + * @param _delegatedManager Instance of the DelegatedManager to initialize + * @param _settings FeeState struct defining fee parameters for StreamingFeeModule initialization + */ + function initializeModuleAndExtension( + IDelegatedManager _delegatedManager, + IStreamingFeeModule.FeeState memory _settings + ) + external + onlyOwnerAndValidManager(_delegatedManager) + { + require(_delegatedManager.isPendingExtension(address(this)), "Extension must be pending"); + + ISetToken setToken = _delegatedManager.setToken(); + + _initializeExtension(setToken, _delegatedManager); + _initializeModule(setToken, _delegatedManager, _settings); + + StreamingFeeSplitExtensionInitialized(address(setToken), address(_delegatedManager)); + } + + /** + * ONLY MANAGER: Remove an existing SetToken and DelegatedManager tracked by the StreamingFeeSplitExtension + */ + function removeExtension() external override { + _removeExtension(); + } + + /** + * ONLY OWNER: Updates streaming fee on StreamingFeeModule. + * + * NOTE: This will accrue streaming fees though not send to owner fee recipient and methodologist. + * + * @param _setToken Instance of the SetToken to update streaming fee for + * @param _newFee Percent of Set accruing to fee extension annually (1% = 1e16, 100% = 1e18) + */ + function updateStreamingFee(ISetToken _setToken, uint256 _newFee) + external + onlyOwner(_setToken) + { + bytes memory callData = abi.encodeWithSignature("updateStreamingFee(address,uint256)", _setToken, _newFee); + _invokeManager(_manager(_setToken), address(streamingFeeModule), callData); + } + + /** + * ONLY OWNER: Updates fee recipient on StreamingFeeModule + * + * @param _setToken Instance of the SetToken to update fee recipient for + * @param _newFeeRecipient Address of new fee recipient. This should be the address of the DelegatedManager + */ + function updateFeeRecipient(ISetToken _setToken, address _newFeeRecipient) + external + onlyOwner(_setToken) + { + bytes memory callData = abi.encodeWithSignature("updateFeeRecipient(address,address)", _setToken, _newFeeRecipient); + _invokeManager(_manager(_setToken), address(streamingFeeModule), callData); + } + + /* ============ Internal Functions ============ */ + + /** + * Internal function to initialize StreamingFeeModule on the SetToken associated with the DelegatedManager. + * + * @param _setToken Instance of the SetToken corresponding to the DelegatedManager + * @param _delegatedManager Instance of the DelegatedManager to initialize the TradeModule for + * @param _settings FeeState struct defining fee parameters for StreamingFeeModule initialization + */ + function _initializeModule( + ISetToken _setToken, + IDelegatedManager _delegatedManager, + IStreamingFeeModule.FeeState memory _settings + ) + internal + { + bytes memory callData = abi.encodeWithSignature( + "initialize(address,(address,uint256,uint256,uint256))", + _setToken, + _settings); + _invokeManager(_delegatedManager, address(streamingFeeModule), callData); + } +} \ No newline at end of file diff --git a/contracts/extensions/TradeExtension.sol b/contracts/extensions/TradeExtension.sol new file mode 100644 index 0000000..010f33b --- /dev/null +++ b/contracts/extensions/TradeExtension.sol @@ -0,0 +1,159 @@ +/* + Copyright 2022 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; + +import { ISetToken } from "@setprotocol/set-protocol-v2/contracts/interfaces/ISetToken.sol"; + +import { BaseGlobalExtension } from "../lib/BaseGlobalExtension.sol"; +import { IDelegatedManager } from "../interfaces/IDelegatedManager.sol"; +import { IManagerCore } from "../interfaces/IManagerCore.sol"; +import { ITradeModule } from "../interfaces/ITradeModule.sol"; + +/** + * @title TradeExtension + * @author Set Protocol + * + * Smart contract global extension which provides DelegatedManager privileged operator(s) the ability to trade on a DEX + * and the owner the ability to restrict operator(s) permissions with an asset whitelist. + */ +contract TradeExtension is BaseGlobalExtension { + + /* ============ Events ============ */ + + event TradeExtensionInitialized( + address indexed _setToken, + address indexed _delegatedManager + ); + + /* ============ State Variables ============ */ + + // Instance of TradeModule + ITradeModule public immutable tradeModule; + + /* ============ Constructor ============ */ + + constructor( + IManagerCore _managerCore, + ITradeModule _tradeModule + ) + public + BaseGlobalExtension(_managerCore) + { + tradeModule = _tradeModule; + } + + /* ============ External Functions ============ */ + + /** + * ONLY OWNER: Initializes TradeModule on the SetToken associated with the DelegatedManager. + * + * @param _delegatedManager Instance of the DelegatedManager to initialize the TradeModule for + */ + function initializeModule(IDelegatedManager _delegatedManager) external onlyOwnerAndValidManager(_delegatedManager) { + require(_delegatedManager.isInitializedExtension(address(this)), "Extension must be initialized"); + + _initializeModule(_delegatedManager.setToken(), _delegatedManager); + } + + /** + * ONLY OWNER: Initializes TradeExtension to the DelegatedManager. + * + * @param _delegatedManager Instance of the DelegatedManager to initialize + */ + function initializeExtension(IDelegatedManager _delegatedManager) external onlyOwnerAndValidManager(_delegatedManager) { + require(_delegatedManager.isPendingExtension(address(this)), "Extension must be pending"); + + ISetToken setToken = _delegatedManager.setToken(); + + _initializeExtension(setToken, _delegatedManager); + + TradeExtensionInitialized(address(setToken), address(_delegatedManager)); + } + + /** + * ONLY OWNER: Initializes TradeExtension to the DelegatedManager and TradeModule to the SetToken + * + * @param _delegatedManager Instance of the DelegatedManager to initialize + */ + function initializeModuleAndExtension(IDelegatedManager _delegatedManager) external onlyOwnerAndValidManager(_delegatedManager){ + require(_delegatedManager.isPendingExtension(address(this)), "Extension must be pending"); + + ISetToken setToken = _delegatedManager.setToken(); + + _initializeExtension(setToken, _delegatedManager); + _initializeModule(setToken, _delegatedManager); + + TradeExtensionInitialized(address(setToken), address(_delegatedManager)); + } + + /** + * ONLY MANAGER: Remove an existing SetToken and DelegatedManager tracked by the TradeExtension + */ + function removeExtension() external override { + _removeExtension(); + } + + /** + * ONLY OPERATOR: Executes a trade on a supported DEX. + * @dev Although the SetToken units are passed in for the send and receive quantities, the total quantity + * sent and received is the quantity of SetToken units multiplied by the SetToken totalSupply. + * + * @param _setToken Instance of the SetToken to trade + * @param _exchangeName Human readable name of the exchange in the integrations registry + * @param _sendToken Address of the token to be sent to the exchange + * @param _sendQuantity Units of token in SetToken sent to the exchange + * @param _receiveToken Address of the token that will be received from the exchange + * @param _minReceiveQuantity Min units of token in SetToken to be received from the exchange + * @param _data Arbitrary bytes to be used to construct trade call data + */ + function trade( + ISetToken _setToken, + string memory _exchangeName, + address _sendToken, + uint256 _sendQuantity, + address _receiveToken, + uint256 _minReceiveQuantity, + bytes memory _data + ) + external + onlyOperator(_setToken) + onlyAllowedAsset(_setToken, _receiveToken) + { + bytes memory callData = abi.encodeWithSignature( + "trade(address,string,address,uint256,address,uint256,bytes)", + _setToken, + _exchangeName, + _sendToken, + _sendQuantity, + _receiveToken, + _minReceiveQuantity, + _data + ); + _invokeManager(_manager(_setToken), address(tradeModule), callData); + } + + /* ============ Internal Functions ============ */ + + /** + * Internal function to initialize TradeModule on the SetToken associated with the DelegatedManager. + * + * @param _setToken Instance of the SetToken corresponding to the DelegatedManager + * @param _delegatedManager Instance of the DelegatedManager to initialize the TradeModule for + */ + function _initializeModule(ISetToken _setToken, IDelegatedManager _delegatedManager) internal { + bytes memory callData = abi.encodeWithSignature("initialize(address)", _setToken); + _invokeManager(_delegatedManager, address(tradeModule), callData); + } +} \ No newline at end of file diff --git a/contracts/factories/DelegatedManagerFactory.sol b/contracts/factories/DelegatedManagerFactory.sol index 984383a..5a58233 100644 --- a/contracts/factories/DelegatedManagerFactory.sol +++ b/contracts/factories/DelegatedManagerFactory.sol @@ -25,6 +25,7 @@ import { ISetToken } from "@setprotocol/set-protocol-v2/contracts/interfaces/ISe import { AddressArrayUtils } from "../lib/AddressArrayUtils.sol"; import { DelegatedManager } from "../manager/DelegatedManager.sol"; import { IDelegatedManager } from "../interfaces/IDelegatedManager.sol"; +import { IManagerCore } from "../interfaces/IManagerCore.sol"; import { ISetTokenCreator } from "../interfaces/ISetTokenCreator.sol"; /** @@ -75,6 +76,9 @@ contract DelegatedManagerFactory { /* ============ State Variables ============ */ + // ManagerCore address + IManagerCore public immutable managerCore; + // SetTokenFactory address ISetTokenCreator public setTokenFactory; @@ -84,10 +88,17 @@ contract DelegatedManagerFactory { /* ============ Constructor ============ */ /** - * @dev Sets setTokenFactory address. + * @dev Sets managerCore and setTokenFactory address. + * @param _managerCore Address of ManagerCore protocol contract * @param _setTokenFactory Address of SetTokenFactory protocol contract */ - constructor(ISetTokenCreator _setTokenFactory) public { + constructor( + IManagerCore _managerCore, + ISetTokenCreator _setTokenFactory + ) + public + { + managerCore = _managerCore; setTokenFactory = _setTokenFactory; } @@ -202,6 +213,7 @@ contract DelegatedManagerFactory { * NOTE: When migrating to this manager system from an existing SetToken, the SetToken's current manager address * must be reset to point at the newly deployed DelegatedManager contract in a separate, final transaction. * + * NOTE: Modules must be passed before corresponding extensions in _initializeTargets otherwise initializeExtension will revert. * * @param _setToken Instance of the SetToken * @param _ownerFeeSplit Percent of fees in precise units (10^16 = 1%) sent to operator, rest to methodologist @@ -316,6 +328,9 @@ contract DelegatedManagerFactory { useAssetAllowlist ); + // Registers manager with ManagerCore + managerCore.addManager(address(newManager)); + emit DelegatedManagerCreated( _setToken, newManager, diff --git a/contracts/interfaces/IGlobalExtension.sol b/contracts/interfaces/IGlobalExtension.sol index e90966f..bbd6900 100644 --- a/contracts/interfaces/IGlobalExtension.sol +++ b/contracts/interfaces/IGlobalExtension.sol @@ -22,5 +22,5 @@ pragma experimental "ABIEncoderV2"; import { ISetToken } from "@setprotocol/set-protocol-v2/contracts/interfaces/ISetToken.sol"; interface IGlobalExtension { - function removeExtension(ISetToken _setToken) external; + function removeExtension() external; } \ No newline at end of file diff --git a/contracts/interfaces/IManagerCore.sol b/contracts/interfaces/IManagerCore.sol new file mode 100644 index 0000000..d38dabc --- /dev/null +++ b/contracts/interfaces/IManagerCore.sol @@ -0,0 +1,24 @@ +/* + Copyright 2022 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; + +interface IManagerCore { + function addManager(address _manager) external; + function isManager(address _manager) external view returns(bool); + function isFactory(address _factory) external view returns(bool); +} \ No newline at end of file diff --git a/contracts/interfaces/IStreamingFeeModuleV2.sol b/contracts/interfaces/IStreamingFeeModuleV2.sol new file mode 100644 index 0000000..68fe15e --- /dev/null +++ b/contracts/interfaces/IStreamingFeeModuleV2.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache License, Version 2.0 +pragma solidity 0.6.10; +pragma experimental "ABIEncoderV2"; + +import { ISetToken } from "@setprotocol/set-protocol-v2/contracts/interfaces/ISetToken.sol"; + +interface IStreamingFeeModule { + struct FeeState { + address feeRecipient; + uint256 maxStreamingFeePercentage; + uint256 streamingFeePercentage; + uint256 lastStreamingFeeTimestamp; + } + + function getFee(ISetToken _setToken) external view returns (uint256); + function accrueFee(ISetToken _setToken) external; + function updateStreamingFee(ISetToken _setToken, uint256 _newFee) external; + function updateFeeRecipient(ISetToken _setToken, address _newFeeRecipient) external; + function initialize(ISetToken _setToken, FeeState memory _settings) external; +} diff --git a/contracts/interfaces/ITradeModule.sol b/contracts/interfaces/ITradeModule.sol new file mode 100644 index 0000000..95e67c2 --- /dev/null +++ b/contracts/interfaces/ITradeModule.sol @@ -0,0 +1,33 @@ +/* + 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 { ISetToken } from "@setprotocol/set-protocol-v2/contracts/interfaces/ISetToken.sol"; + +interface ITradeModule { + function initialize(ISetToken _setToken) external; + function trade(ISetToken _setToken, + string memory _exchangeName, + address _sendToken, + uint256 _sendQuantity, + address _receiveToken, + uint256 _minReceiveQuantity, + bytes memory _data + ) external; +} \ No newline at end of file diff --git a/contracts/lib/BaseGlobalExtension.sol b/contracts/lib/BaseGlobalExtension.sol index 5c6a3b4..e631700 100644 --- a/contracts/lib/BaseGlobalExtension.sol +++ b/contracts/lib/BaseGlobalExtension.sol @@ -18,9 +18,11 @@ pragma solidity 0.6.10; +import { ISetToken } from "@setprotocol/set-protocol-v2/contracts/interfaces/ISetToken.sol"; + import { AddressArrayUtils } from "../lib/AddressArrayUtils.sol"; import { IDelegatedManager } from "../interfaces/IDelegatedManager.sol"; -import { ISetToken } from "@setprotocol/set-protocol-v2/contracts/interfaces/ISetToken.sol"; +import { IManagerCore } from "../interfaces/IManagerCore.sol"; /** * @title BaseExtension @@ -32,6 +34,21 @@ import { ISetToken } from "@setprotocol/set-protocol-v2/contracts/interfaces/ISe abstract contract BaseGlobalExtension { using AddressArrayUtils for address[]; + /* ============ Events ============ */ + + event ExtensionRemoved( + address indexed _setToken, + address indexed _delegatedManager + ); + + /* ============ State Variables ============ */ + + // Address of the ManagerCore + IManagerCore public managerCore; + + // Mapping from Set Token to DelegatedManager + mapping(ISetToken => IDelegatedManager) public setManagers; + /* ============ Modifiers ============ */ /** @@ -59,10 +76,11 @@ abstract contract BaseGlobalExtension { } /** - * Throws if the sender is not the SetToken manager contract + * Throws if the sender is not the SetToken manager contract owner or if the manager is not enabled on the ManagerCore */ - modifier onlyManager(ISetToken _setToken) { - require(address(_manager(_setToken)) == msg.sender, "Must be manager"); + modifier onlyOwnerAndValidManager(IDelegatedManager _delegatedManager) { + require(msg.sender == _delegatedManager.owner(), "Must be owner"); + require(managerCore.isManager(address(_delegatedManager)), "Must be ManagerCore-enabled manager"); _; } @@ -74,21 +92,35 @@ abstract contract BaseGlobalExtension { _; } + /* ============ Constructor ============ */ + + /** + * Set state variables + * + * @param _managerCore Address of managerCore contract + */ + constructor(IManagerCore _managerCore) public { + managerCore = _managerCore; + } + + /* ============ External Functions ============ */ + /** * ONLY MANAGER: Deletes SetToken/Manager state from extension. Must only be callable by manager! */ - function removeExtension(ISetToken _setToken) external virtual; + function removeExtension() external virtual; /* ============ Internal Functions ============ */ /** * Invoke call from manager * - * @param _module Module to interact with - * @param _encoded Encoded byte data + * @param _delegatedManager Manager to interact with + * @param _module Module to interact with + * @param _encoded Encoded byte data */ - function _invokeManager(ISetToken _setToken, address _module, bytes memory _encoded) internal { - _manager(_setToken).interactManager(_module, _encoded); + function _invokeManager(IDelegatedManager _delegatedManager, address _module, bytes memory _encoded) internal { + _delegatedManager.interactManager(_module, _encoded); } /** @@ -96,5 +128,33 @@ abstract contract BaseGlobalExtension { * * @param _setToken SetToken who's manager is needed */ - function _manager(ISetToken _setToken) internal virtual view returns (IDelegatedManager); + function _manager(ISetToken _setToken) internal view returns (IDelegatedManager) { + return setManagers[_setToken]; + } + + /** + * Internal function to initialize extension to the DelegatedManager. + * + * @param _setToken Instance of the SetToken corresponding to the DelegatedManager + * @param _delegatedManager Instance of the DelegatedManager to initialize + */ + function _initializeExtension(ISetToken _setToken, IDelegatedManager _delegatedManager) internal { + setManagers[_setToken] = _delegatedManager; + + _delegatedManager.initializeExtension(); + } + + /** + * ONLY MANAGER: Internal function to delete SetToken/Manager state from extension + */ + function _removeExtension() internal { + IDelegatedManager delegatedManager = IDelegatedManager(msg.sender); + ISetToken setToken = delegatedManager.setToken(); + + require(msg.sender == address(_manager(setToken)), "Must be Manager"); + + delete setManagers[setToken]; + + ExtensionRemoved(address(setToken), address(delegatedManager)); + } } \ No newline at end of file diff --git a/contracts/manager/DelegatedManager.sol b/contracts/manager/DelegatedManager.sol index 62885d6..926e2a6 100644 --- a/contracts/manager/DelegatedManager.sol +++ b/contracts/manager/DelegatedManager.sol @@ -248,7 +248,7 @@ contract DelegatedManager is Ownable { extensionAllowlist[extension] = ExtensionState.NONE; - IGlobalExtension(extension).removeExtension(setToken); + IGlobalExtension(extension).removeExtension(); emit ExtensionRemoved(extension); } diff --git a/contracts/mocks/BaseGlobalExtensionMock.sol b/contracts/mocks/BaseGlobalExtensionMock.sol index 58432fc..aa32f4b 100644 --- a/contracts/mocks/BaseGlobalExtensionMock.sol +++ b/contracts/mocks/BaseGlobalExtensionMock.sol @@ -18,30 +18,33 @@ pragma solidity 0.6.10; +import { ISetToken } from "@setprotocol/set-protocol-v2/contracts/interfaces/ISetToken.sol"; + import { BaseGlobalExtension } from "../lib/BaseGlobalExtension.sol"; +import { IManagerCore } from "../interfaces/IManagerCore.sol"; import { IDelegatedManager } from "../interfaces/IDelegatedManager.sol"; -import { ISetToken } from "@setprotocol/set-protocol-v2/contracts/interfaces/ISetToken.sol"; contract BaseGlobalExtensionMock is BaseGlobalExtension { - mapping(ISetToken=>IDelegatedManager) public initializeInfo; + /* ============ Constructor ============ */ + + constructor(IManagerCore _managerCore) public BaseGlobalExtension(_managerCore) {} /* ============ External Functions ============ */ function initializeExtension( - ISetToken _setToken, - IDelegatedManager _manager + IDelegatedManager _delegatedManager ) external + onlyOwnerAndValidManager(_delegatedManager) { - require(msg.sender == _manager.owner(), "Must be owner"); - initializeInfo[_setToken] = _manager; + require(_delegatedManager.isPendingExtension(address(this)), "Extension must be pending"); - _manager.initializeExtension(); + _initializeExtension(_delegatedManager.setToken(), _delegatedManager); } function testInvokeManager(ISetToken _setToken, address _module, bytes calldata _encoded) external { - _invokeManager(_setToken, _module, _encoded); + _invokeManager(_manager(_setToken), _module, _encoded); } function testOnlyOwner(ISetToken _setToken) @@ -59,9 +62,9 @@ contract BaseGlobalExtensionMock is BaseGlobalExtension { onlyOperator(_setToken) {} - function testOnlyManager(ISetToken _setToken) + function testOnlyOwnerAndValidManager(IDelegatedManager _delegatedManager) external - onlyManager(_setToken) + onlyOwnerAndValidManager(_delegatedManager) {} function testOnlyAllowedAsset(ISetToken _setToken, address _asset) @@ -69,13 +72,7 @@ contract BaseGlobalExtensionMock is BaseGlobalExtension { onlyAllowedAsset(_setToken, _asset) {} - function removeExtension(ISetToken _setToken) external override onlyManager(_setToken) { - delete initializeInfo[_setToken]; - } - - /* ============ Internal Functions ============ */ - - function _manager(ISetToken _setToken) internal override view returns (IDelegatedManager) { - return initializeInfo[_setToken]; + function removeExtension() external override { + _removeExtension(); } } \ No newline at end of file diff --git a/contracts/mocks/ManagerMock.sol b/contracts/mocks/ManagerMock.sol new file mode 100644 index 0000000..c57e2af --- /dev/null +++ b/contracts/mocks/ManagerMock.sol @@ -0,0 +1,42 @@ +/* + Copyright 2021 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; + +import { ISetToken } from "@setprotocol/set-protocol-v2/contracts/interfaces/ISetToken.sol"; + +import { IGlobalExtension } from "../interfaces/IGlobalExtension.sol"; + +contract ManagerMock { + ISetToken public immutable setToken; + + constructor( + ISetToken _setToken + ) + public + { + setToken = _setToken; + } + + function removeExtensions(address[] memory _extensions) external { + for (uint256 i = 0; i < _extensions.length; i++) { + address extension = _extensions[i]; + IGlobalExtension(extension).removeExtension(); + } + } +} \ No newline at end of file diff --git a/test/extensions/issuanceExtension.spec.ts b/test/extensions/issuanceExtension.spec.ts new file mode 100644 index 0000000..b99fd01 --- /dev/null +++ b/test/extensions/issuanceExtension.spec.ts @@ -0,0 +1,725 @@ +import "module-alias/register"; + +import { + BigNumber, + Contract, + ContractTransaction +} from "ethers"; +import { Address, Account } from "@utils/types"; +import { ADDRESS_ZERO, ZERO } from "@utils/constants"; +import { + DelegatedManager, + IssuanceExtension, + ManagerCore +} from "@utils/contracts/index"; +import { SetToken, DebtIssuanceModuleV2 } from "@setprotocol/set-protocol-v2/utils/contracts"; +import DeployHelper from "@utils/deploys"; +import { + addSnapshotBeforeRestoreAfterEach, + ether, + getAccounts, + getWaffleExpect, + preciseMul +} from "@utils/index"; +import { SystemFixture } from "@setprotocol/set-protocol-v2/utils/fixtures"; +import { getSystemFixture, getRandomAccount } from "@setprotocol/set-protocol-v2/utils/test"; + +const expect = getWaffleExpect(); + +describe("IssuanceExtension", () => { + let owner: Account; + let methodologist: Account; + let operator: Account; + let factory: Account; + + let deployer: DeployHelper; + let setToken: SetToken; + let setV2Setup: SystemFixture; + + let issuanceModule: DebtIssuanceModuleV2; + + let managerCore: ManagerCore; + let delegatedManager: DelegatedManager; + let issuanceExtension: IssuanceExtension; + + let maxManagerFee: BigNumber; + let managerIssueFee: BigNumber; + let managerRedeemFee: BigNumber; + let feeRecipient: Address; + let managerIssuanceHook: Address; + + let ownerFeeSplit: BigNumber; + let ownerFeeRecipient: Address; + + before(async () => { + [ + owner, + methodologist, + operator, + factory + ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + + setV2Setup = getSystemFixture(owner.address); + await setV2Setup.initialize(); + + issuanceModule = await deployer.setV2.deployDebtIssuanceModuleV2(setV2Setup.controller.address); + await setV2Setup.controller.addModule(issuanceModule.address); + + managerCore = await deployer.managerCore.deployManagerCore(); + + issuanceExtension = await deployer.globalExtensions.deployIssuanceExtension( + managerCore.address, + issuanceModule.address + ); + + setToken = await setV2Setup.createSetToken( + [setV2Setup.dai.address], + [ether(1)], + [issuanceModule.address] + ); + + delegatedManager = await deployer.manager.deployDelegatedManager( + setToken.address, + factory.address, + methodologist.address, + [issuanceExtension.address], + [operator.address], + [setV2Setup.usdc.address, setV2Setup.weth.address], + true + ); + + ownerFeeSplit = ether(0.1); + await delegatedManager.updateOwnerFeeSplit(ownerFeeSplit); + ownerFeeRecipient = owner.address; + await delegatedManager.updateOwnerFeeRecipient(ownerFeeRecipient); + + await setToken.setManager(delegatedManager.address); + + await managerCore.initialize([factory.address]); + await managerCore.connect(factory.wallet).addManager(delegatedManager.address); + + maxManagerFee = ether(.1); + managerIssueFee = ether(.02); + managerRedeemFee = ether(.03); + feeRecipient = delegatedManager.address; + managerIssuanceHook = ADDRESS_ZERO; + }); + + addSnapshotBeforeRestoreAfterEach(); + + describe("#constructor", async () => { + let subjectManagerCore: Address; + let subjectIssuanceModule: Address; + + beforeEach(async () => { + subjectManagerCore = managerCore.address; + subjectIssuanceModule = issuanceModule.address; + }); + + async function subject(): Promise { + return await deployer.globalExtensions.deployIssuanceExtension( + subjectManagerCore, + subjectIssuanceModule + ); + } + + it("should set the correct IssuanceModule address", async () => { + const issuanceExtension = await subject(); + + const storedModule = await issuanceExtension.issuanceModule(); + expect(storedModule).to.eq(subjectIssuanceModule); + }); + }); + + describe("#initializeModule", async () => { + let subjectDelegatedManager: Address; + let subjectCaller: Account; + let subjectMaxManagerFee: BigNumber; + let subjectManagerIssueFee: BigNumber; + let subjectManagerRedeemFee: BigNumber; + let subjectFeeRecipient: Address; + let subjectManagerIssuanceHook: Address; + + beforeEach(async () => { + await issuanceExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + + subjectDelegatedManager = delegatedManager.address; + subjectCaller = owner; + subjectMaxManagerFee = maxManagerFee; + subjectManagerIssueFee = managerIssueFee; + subjectManagerRedeemFee = managerRedeemFee; + subjectFeeRecipient = feeRecipient; + subjectManagerIssuanceHook = managerIssuanceHook; + }); + + async function subject(): Promise { + return issuanceExtension.connect(subjectCaller.wallet).initializeModule( + subjectDelegatedManager, + subjectMaxManagerFee, + subjectManagerIssueFee, + subjectManagerRedeemFee, + subjectFeeRecipient, + subjectManagerIssuanceHook + ); + } + + it("should correctly initialize the IssuanceModule on the SetToken", async () => { + await subject(); + + const isModuleInitialized: Boolean = await setToken.isInitializedModule(issuanceModule.address); + expect(isModuleInitialized).to.eq(true); + + const storedSettings: any = await issuanceModule.issuanceSettings(setToken.address); + + expect(storedSettings.maxManagerFee).to.eq(maxManagerFee); + expect(storedSettings.managerIssueFee).to.eq(managerIssueFee); + expect(storedSettings.managerRedeemFee).to.eq(managerRedeemFee); + expect(storedSettings.feeRecipient).to.eq(feeRecipient); + expect(storedSettings.managerIssuanceHook).to.eq(managerIssuanceHook); + }); + + describe("when the sender is not the owner", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be owner"); + }); + }); + + describe("when the IssuanceModule is not pending or initialized", async () => { + beforeEach(async () => { + await subject(); + await delegatedManager.connect(owner.wallet).removeExtensions([issuanceExtension.address]); + await delegatedManager.connect(owner.wallet).setManager(owner.address); + await setToken.connect(owner.wallet).removeModule(issuanceModule.address); + await setToken.connect(owner.wallet).setManager(delegatedManager.address); + await delegatedManager.connect(owner.wallet).addExtensions([issuanceExtension.address]); + await issuanceExtension.connect(subjectCaller.wallet).initializeExtension(delegatedManager.address); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be pending initialization"); + }); + }); + + describe("when the IssuanceModule is already initialized", async () => { + beforeEach(async () => { + await subject(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be pending initialization"); + }); + }); + + describe("when the extension is not pending or initialized", async () => { + beforeEach(async () => { + await delegatedManager.connect(owner.wallet).removeExtensions([issuanceExtension.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Extension must be initialized"); + }); + }); + + describe("when the extension is pending", async () => { + beforeEach(async () => { + await delegatedManager.connect(owner.wallet).removeExtensions([issuanceExtension.address]); + await delegatedManager.connect(owner.wallet).addExtensions([issuanceExtension.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Extension must be initialized"); + }); + }); + + describe("when the manager is not a ManagerCore-enabled manager", async () => { + beforeEach(async () => { + await managerCore.connect(owner.wallet).removeManager(delegatedManager.address); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be ManagerCore-enabled manager"); + }); + }); + }); + + describe("#initializeExtension", async () => { + let subjectDelegatedManager: Address; + let subjectCaller: Account; + + beforeEach(async () => { + subjectDelegatedManager = delegatedManager.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return issuanceExtension.connect(subjectCaller.wallet).initializeExtension(subjectDelegatedManager); + } + + it("should store the correct SetToken and DelegatedManager on the IssuanceExtension", async () => { + await subject(); + + const storedDelegatedManager: Address = await issuanceExtension.setManagers(setToken.address); + expect(storedDelegatedManager).to.eq(delegatedManager.address); + }); + + it("should initialize the IssuanceExtension on the DelegatedManager", async () => { + await subject(); + + const isExtensionInitialized: Boolean = await delegatedManager.isInitializedExtension(issuanceExtension.address); + expect(isExtensionInitialized).to.eq(true); + }); + + it("should emit the correct IssuanceExtensionInitialized event", async () => { + await expect(subject()).to.emit( + issuanceExtension, + "IssuanceExtensionInitialized" + ).withArgs(setToken.address, delegatedManager.address); + }); + + describe("when the sender is not the owner", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be owner"); + }); + }); + + describe("when the extension is not pending or initialized", async () => { + beforeEach(async () => { + await issuanceExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + await delegatedManager.connect(owner.wallet).removeExtensions([issuanceExtension.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Extension must be pending"); + }); + }); + + describe("when the extension is already initialized", async () => { + beforeEach(async () => { + await issuanceExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Extension must be pending"); + }); + }); + + describe("when the manager is not a ManagerCore-enabled manager", async () => { + beforeEach(async () => { + await managerCore.connect(owner.wallet).removeManager(delegatedManager.address); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be ManagerCore-enabled manager"); + }); + }); + }); + + describe("#initializeModuleAndExtension", async () => { + let subjectDelegatedManager: Address; + let subjectCaller: Account; + let subjectMaxManagerFee: BigNumber; + let subjectManagerIssueFee: BigNumber; + let subjectManagerRedeemFee: BigNumber; + let subjectFeeRecipient: Address; + let subjectManagerIssuanceHook: Address; + + beforeEach(async () => { + subjectDelegatedManager = delegatedManager.address; + subjectCaller = owner; + subjectMaxManagerFee = maxManagerFee; + subjectManagerIssueFee = managerIssueFee; + subjectManagerRedeemFee = managerRedeemFee; + subjectFeeRecipient = feeRecipient; + subjectManagerIssuanceHook = managerIssuanceHook; + }); + + async function subject(): Promise { + return issuanceExtension.connect(subjectCaller.wallet).initializeModuleAndExtension( + subjectDelegatedManager, + subjectMaxManagerFee, + subjectManagerIssueFee, + subjectManagerRedeemFee, + subjectFeeRecipient, + subjectManagerIssuanceHook + ); + } + + it("should correctly initialize the IssuanceModule on the SetToken", async () => { + await subject(); + + const isModuleInitialized: Boolean = await setToken.isInitializedModule(issuanceModule.address); + expect(isModuleInitialized).to.eq(true); + + const storedSettings: any = await issuanceModule.issuanceSettings(setToken.address); + + expect(storedSettings.maxManagerFee).to.eq(maxManagerFee); + expect(storedSettings.managerIssueFee).to.eq(managerIssueFee); + expect(storedSettings.managerRedeemFee).to.eq(managerRedeemFee); + expect(storedSettings.feeRecipient).to.eq(feeRecipient); + expect(storedSettings.managerIssuanceHook).to.eq(managerIssuanceHook); + }); + + it("should store the correct SetToken and DelegatedManager on the IssuanceExtension", async () => { + await subject(); + + const storedDelegatedManager: Address = await issuanceExtension.setManagers(setToken.address); + expect(storedDelegatedManager).to.eq(delegatedManager.address); + }); + + it("should initialize the IssuanceExtension on the DelegatedManager", async () => { + await subject(); + + const isExtensionInitialized: Boolean = await delegatedManager.isInitializedExtension(issuanceExtension.address); + expect(isExtensionInitialized).to.eq(true); + }); + + it("should emit the correct IssuanceExtensionInitialized event", async () => { + await expect(subject()).to.emit( + issuanceExtension, + "IssuanceExtensionInitialized" + ).withArgs(setToken.address, delegatedManager.address); + }); + + describe("when the sender is not the owner", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be owner"); + }); + }); + + describe("when the IssuanceModule is not pending or initialized", async () => { + beforeEach(async () => { + await issuanceExtension.connect(owner.wallet).initializeModuleAndExtension( + subjectDelegatedManager, + maxManagerFee, + managerIssueFee, + managerRedeemFee, + feeRecipient, + managerIssuanceHook + ); + await delegatedManager.connect(owner.wallet).removeExtensions([issuanceExtension.address]); + await delegatedManager.connect(owner.wallet).setManager(owner.address); + await setToken.connect(owner.wallet).removeModule(issuanceModule.address); + await setToken.connect(owner.wallet).setManager(delegatedManager.address); + await delegatedManager.connect(owner.wallet).addExtensions([issuanceExtension.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be pending initialization"); + }); + }); + + describe("when the IssuanceModule is already initialized", async () => { + beforeEach(async () => { + await issuanceExtension.connect(owner.wallet).initializeModuleAndExtension( + subjectDelegatedManager, + maxManagerFee, + managerIssueFee, + managerRedeemFee, + feeRecipient, + managerIssuanceHook + ); + await delegatedManager.connect(owner.wallet).removeExtensions([issuanceExtension.address]); + await delegatedManager.connect(owner.wallet).addExtensions([issuanceExtension.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be pending initialization"); + }); + }); + + describe("when the extension is not pending or initialized", async () => { + beforeEach(async () => { + await issuanceExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + await delegatedManager.connect(owner.wallet).removeExtensions([issuanceExtension.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Extension must be pending"); + }); + }); + + describe("when the extension is already initialized", async () => { + beforeEach(async () => { + await issuanceExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Extension must be pending"); + }); + }); + + describe("when the manager is not a ManagerCore-enabled manager", async () => { + beforeEach(async () => { + await managerCore.connect(owner.wallet).removeManager(delegatedManager.address); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be ManagerCore-enabled manager"); + }); + }); + }); + + describe("#removeExtension", async () => { + let subjectManager: Contract; + let subjectIssuanceExtension: Address[]; + let subjectCaller: Account; + + beforeEach(async () => { + await issuanceExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + + subjectManager = delegatedManager; + subjectIssuanceExtension = [issuanceExtension.address]; + subjectCaller = owner; + }); + + async function subject(): Promise { + return subjectManager.connect(subjectCaller.wallet).removeExtensions(subjectIssuanceExtension); + } + + it("should clear SetToken and DelegatedManager from IssuanceExtension state", async () => { + await subject(); + + const storedDelegatedManager: Address = await issuanceExtension.setManagers(setToken.address); + expect(storedDelegatedManager).to.eq(ADDRESS_ZERO); + }); + + it("should emit the correct ExtensionRemoved event", async () => { + await expect(subject()).to.emit( + issuanceExtension, + "ExtensionRemoved" + ).withArgs(setToken.address, delegatedManager.address); + }); + + describe("when the caller is not the SetToken manager", async () => { + beforeEach(async () => { + subjectManager = await deployer.mocks.deployManagerMock(setToken.address); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be Manager"); + }); + }); + }); + + describe("#updateIssueFee", async () => { + let subjectNewFee: BigNumber; + let subjectSetToken: Address; + let subjectCaller: Account; + + beforeEach(async () => { + await issuanceExtension.connect(owner.wallet).initializeModuleAndExtension( + delegatedManager.address, + maxManagerFee, + managerIssueFee, + managerRedeemFee, + feeRecipient, + managerIssuanceHook + ); + + subjectNewFee = ether(.03); + subjectSetToken = setToken.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return await issuanceExtension.connect(subjectCaller.wallet).updateIssueFee(subjectSetToken, subjectNewFee); + } + + it("should update the issue fee on the IssuanceModule", async () => { + await subject(); + + const issueState: any = await issuanceModule.issuanceSettings(setToken.address); + expect(issueState.managerIssueFee).to.eq(subjectNewFee); + }); + + describe("when the sender is not the owner", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be owner"); + }); + }); + }); + + describe("#updateRedeemFee", async () => { + let subjectNewFee: BigNumber; + let subjectSetToken: Address; + let subjectCaller: Account; + + beforeEach(async () => { + await issuanceExtension.connect(owner.wallet).initializeModuleAndExtension( + delegatedManager.address, + maxManagerFee, + managerIssueFee, + managerRedeemFee, + feeRecipient, + managerIssuanceHook + ); + + subjectNewFee = ether(.02); + subjectSetToken = setToken.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return await issuanceExtension.connect(subjectCaller.wallet).updateRedeemFee(subjectSetToken, subjectNewFee); + } + + it("should update the redeem fee on the IssuanceModule", async () => { + await subject(); + + const issueState: any = await issuanceModule.issuanceSettings(setToken.address); + expect(issueState.managerRedeemFee).to.eq(subjectNewFee); + }); + + describe("when the sender is not the owner", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be owner"); + }); + }); + }); + + describe("#updateFeeRecipient", async () => { + let subjectNewFeeRecipient: Address; + let subjectSetToken: Address; + let subjectCaller: Account; + + beforeEach(async () => { + await issuanceExtension.connect(owner.wallet).initializeModuleAndExtension( + delegatedManager.address, + maxManagerFee, + managerIssueFee, + managerRedeemFee, + feeRecipient, + managerIssuanceHook + ); + + subjectNewFeeRecipient = factory.address; + subjectSetToken = setToken.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return await issuanceExtension.connect(subjectCaller.wallet).updateFeeRecipient(subjectSetToken, subjectNewFeeRecipient); + } + + it("should update the fee recipient on the IssuanceModule", async () => { + await subject(); + + const issueState: any = await issuanceModule.issuanceSettings(setToken.address); + expect(issueState.feeRecipient).to.eq(subjectNewFeeRecipient); + }); + + describe("when the sender is not the owner", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be owner"); + }); + }); + }); + + describe("#distributeFees", async () => { + let mintedTokens: BigNumber; + let redeemedTokens: BigNumber; + let subjectSetToken: Address; + + beforeEach(async () => { + await issuanceExtension.connect(owner.wallet).initializeModuleAndExtension( + delegatedManager.address, + maxManagerFee, + managerIssueFee, + managerRedeemFee, + feeRecipient, + managerIssuanceHook + ); + + mintedTokens = ether(2); + await setV2Setup.dai.approve(issuanceModule.address, ether(3)); + await issuanceModule.issue(setToken.address, mintedTokens, factory.address); + + redeemedTokens = ether(1); + await setToken.approve(issuanceModule.address, ether(2)); + await issuanceModule.connect(factory.wallet).redeem(setToken.address, redeemedTokens, factory.address); + + subjectSetToken = setToken.address; + }); + + async function subject(): Promise { + return await issuanceExtension.distributeFees(subjectSetToken); + } + + it("should send correct amount of fees to owner fee recipient and methodologist", async () => { + subject(); + + const expectedMintFees = preciseMul(mintedTokens, managerIssueFee); + const expectedRedeemFees = preciseMul(redeemedTokens, managerRedeemFee); + const expectedMintRedeemFees = expectedMintFees.add(expectedRedeemFees); + + const expectedOwnerTake = preciseMul(expectedMintRedeemFees, ownerFeeSplit); + const expectedMethodologistTake = expectedMintRedeemFees.sub(expectedOwnerTake); + + const ownerFeeRecipientBalance = await setToken.balanceOf(ownerFeeRecipient); + const methodologistBalance = await setToken.balanceOf(methodologist.address); + + expect(ownerFeeRecipientBalance).to.eq(expectedOwnerTake); + expect(methodologistBalance).to.eq(expectedMethodologistTake); + }); + + it("should emit a FeesDistributed event", async () => { + await expect(subject()).to.emit(issuanceExtension, "FeesDistributed"); + }); + + describe("when methodologist fees are 0", async () => { + beforeEach(async () => { + await delegatedManager.connect(owner.wallet).updateOwnerFeeSplit(ether(1)); + }); + + it("should not send fees to methodologist", async () => { + const preMethodologistBalance = await setToken.balanceOf(methodologist.address); + + await subject(); + + const postMethodologistBalance = await setToken.balanceOf(methodologist.address); + expect(postMethodologistBalance.sub(preMethodologistBalance)).to.eq(ZERO); + }); + }); + + describe("when owner fees are 0", async () => { + beforeEach(async () => { + await delegatedManager.connect(owner.wallet).updateOwnerFeeSplit(ZERO); + }); + + it("should not send fees to owner fee recipient", async () => { + const preOwnerFeeRecipientBalance = await setToken.balanceOf(ownerFeeRecipient); + + await subject(); + + const postOwnerFeeRecipientBalance = await setToken.balanceOf(ownerFeeRecipient); + expect(postOwnerFeeRecipientBalance.sub(preOwnerFeeRecipientBalance)).to.eq(ZERO); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/extensions/streamingFeeSplitExtension.spec.ts b/test/extensions/streamingFeeSplitExtension.spec.ts new file mode 100644 index 0000000..50cd602 --- /dev/null +++ b/test/extensions/streamingFeeSplitExtension.spec.ts @@ -0,0 +1,660 @@ +import "module-alias/register"; + +import { + BigNumber, + Contract, + ContractTransaction +} from "ethers"; +import { + Address, + Account, + StreamingFeeState +} from "@utils/types"; +import { ADDRESS_ZERO, ONE_YEAR_IN_SECONDS } from "@utils/constants"; +import { + DelegatedManager, + StreamingFeeSplitExtension, + ManagerCore +} from "@utils/contracts/index"; +import { SetToken } from "@setprotocol/set-protocol-v2/utils/contracts"; +import DeployHelper from "@utils/deploys"; +import { + addSnapshotBeforeRestoreAfterEach, + ether, + getAccounts, + getWaffleExpect, + increaseTimeAsync, + preciseMul, + getTransactionTimestamp +} from "@utils/index"; +import { getStreamingFee, getStreamingFeeInflationAmount } from "@utils/common"; +import { SystemFixture } from "@setprotocol/set-protocol-v2/utils/fixtures"; +import { getSystemFixture, getRandomAccount } from "@setprotocol/set-protocol-v2/utils/test"; +import { ZERO } from "@setprotocol/set-protocol-v2/utils/constants"; + +const expect = getWaffleExpect(); + +describe("StreamingFeeSplitExtension", () => { + let owner: Account; + let methodologist: Account; + let operator: Account; + let factory: Account; + + let deployer: DeployHelper; + let setToken: SetToken; + let setV2Setup: SystemFixture; + + let managerCore: ManagerCore; + let delegatedManager: DelegatedManager; + let streamingFeeSplitExtension: StreamingFeeSplitExtension; + + let feeRecipient: Address; + let maxStreamingFeePercentage: BigNumber; + let streamingFeePercentage: BigNumber; + let feeSettings: StreamingFeeState; + + let ownerFeeSplit: BigNumber; + let ownerFeeRecipient: Address; + + before(async () => { + [ + owner, + methodologist, + operator, + factory + ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + + setV2Setup = getSystemFixture(owner.address); + await setV2Setup.initialize(); + + managerCore = await deployer.managerCore.deployManagerCore(); + + streamingFeeSplitExtension = await deployer.globalExtensions.deployStreamingFeeSplitExtension( + managerCore.address, + setV2Setup.streamingFeeModule.address + ); + + setToken = await setV2Setup.createSetToken( + [setV2Setup.dai.address], + [ether(1)], + [setV2Setup.issuanceModule.address, setV2Setup.streamingFeeModule.address] + ); + + await setV2Setup.issuanceModule.initialize(setToken.address, ADDRESS_ZERO); + + delegatedManager = await deployer.manager.deployDelegatedManager( + setToken.address, + factory.address, + methodologist.address, + [streamingFeeSplitExtension.address], + [operator.address], + [setV2Setup.usdc.address, setV2Setup.weth.address], + true + ); + + ownerFeeSplit = ether(0.1); + await delegatedManager.updateOwnerFeeSplit(ownerFeeSplit); + ownerFeeRecipient = owner.address; + await delegatedManager.updateOwnerFeeRecipient(ownerFeeRecipient); + + await setToken.setManager(delegatedManager.address); + + await managerCore.initialize([factory.address]); + await managerCore.connect(factory.wallet).addManager(delegatedManager.address); + + feeRecipient = delegatedManager.address; + maxStreamingFeePercentage = ether(.1); + streamingFeePercentage = ether(.02); + + feeSettings = { + feeRecipient, + maxStreamingFeePercentage, + streamingFeePercentage, + lastStreamingFeeTimestamp: ZERO, + } as StreamingFeeState; + }); + + addSnapshotBeforeRestoreAfterEach(); + + describe("#constructor", async () => { + let subjectManagerCore: Address; + let subjectStreamingFeeModule: Address; + + beforeEach(async () => { + subjectManagerCore = managerCore.address; + subjectStreamingFeeModule = setV2Setup.streamingFeeModule.address; + }); + + async function subject(): Promise { + return await deployer.globalExtensions.deployStreamingFeeSplitExtension( + subjectManagerCore, + subjectStreamingFeeModule + ); + } + + it("should set the correct StreamingFeeModule address", async () => { + const streamingFeeSplitExtension = await subject(); + + const storedModule = await streamingFeeSplitExtension.streamingFeeModule(); + expect(storedModule).to.eq(subjectStreamingFeeModule); + }); + }); + + describe("#initializeModule", async () => { + let subjectFeeSettings: StreamingFeeState; + let subjectDelegatedManager: Address; + let subjectCaller: Account; + + beforeEach(async () => { + await streamingFeeSplitExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + + subjectFeeSettings = feeSettings; + subjectDelegatedManager = delegatedManager.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return streamingFeeSplitExtension.connect(subjectCaller.wallet).initializeModule(subjectDelegatedManager, subjectFeeSettings); + } + + it("should correctly initialize the StreamingFeeModule on the SetToken", async () => { + const txTimestamp = await getTransactionTimestamp(subject()); + + const isModuleInitialized: Boolean = await setToken.isInitializedModule(setV2Setup.streamingFeeModule.address); + expect(isModuleInitialized).to.eq(true); + + const storedFeeState: StreamingFeeState = await setV2Setup.streamingFeeModule.feeStates(setToken.address); + expect(storedFeeState.feeRecipient).to.eq(feeRecipient); + expect(storedFeeState.maxStreamingFeePercentage).to.eq(maxStreamingFeePercentage); + expect(storedFeeState.streamingFeePercentage).to.eq(streamingFeePercentage); + expect(storedFeeState.lastStreamingFeeTimestamp).to.eq(txTimestamp); + }); + + describe("when the sender is not the owner", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be owner"); + }); + }); + + describe("when the StreamingFeeModule is not pending or initialized", async () => { + beforeEach(async () => { + await subject(); + await delegatedManager.connect(owner.wallet).removeExtensions([streamingFeeSplitExtension.address]); + await delegatedManager.connect(owner.wallet).setManager(owner.address); + await setToken.connect(owner.wallet).removeModule(setV2Setup.streamingFeeModule.address); + await setToken.connect(owner.wallet).setManager(delegatedManager.address); + await delegatedManager.connect(owner.wallet).addExtensions([streamingFeeSplitExtension.address]); + await streamingFeeSplitExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be pending initialization"); + }); + }); + + describe("when the StreamingFeeModule is already initialized", async () => { + beforeEach(async () => { + await subject(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be pending initialization"); + }); + }); + + describe("when the extension is not pending or initialized", async () => { + beforeEach(async () => { + await delegatedManager.connect(owner.wallet).removeExtensions([streamingFeeSplitExtension.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Extension must be initialized"); + }); + }); + + describe("when the extension is pending", async () => { + beforeEach(async () => { + await delegatedManager.connect(owner.wallet).removeExtensions([streamingFeeSplitExtension.address]); + await delegatedManager.connect(owner.wallet).addExtensions([streamingFeeSplitExtension.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Extension must be initialized"); + }); + }); + + describe("when the manager is not a ManagerCore-enabled manager", async () => { + beforeEach(async () => { + await managerCore.connect(owner.wallet).removeManager(delegatedManager.address); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be ManagerCore-enabled manager"); + }); + }); + }); + + describe("#initializeExtension", async () => { + let subjectDelegatedManager: Address; + let subjectCaller: Account; + + beforeEach(async () => { + subjectDelegatedManager = delegatedManager.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return streamingFeeSplitExtension.connect(subjectCaller.wallet).initializeExtension(subjectDelegatedManager); + } + + it("should store the correct SetToken and DelegatedManager on the StreamingFeeSplitExtension", async () => { + await subject(); + + const storedDelegatedManager: Address = await streamingFeeSplitExtension.setManagers(setToken.address); + expect(storedDelegatedManager).to.eq(delegatedManager.address); + }); + + it("should initialize the StreamingFeeSplitExtension on the DelegatedManager", async () => { + await subject(); + + const isExtensionInitialized: Boolean = await delegatedManager.isInitializedExtension(streamingFeeSplitExtension.address); + expect(isExtensionInitialized).to.eq(true); + }); + + it("should emit the correct StreamingFeeSplitExtensionInitialized event", async () => { + await expect(subject()).to.emit( + streamingFeeSplitExtension, + "StreamingFeeSplitExtensionInitialized" + ).withArgs(setToken.address, delegatedManager.address); + }); + + describe("when the sender is not the owner", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be owner"); + }); + }); + + describe("when the extension is not pending or initialized", async () => { + beforeEach(async () => { + await streamingFeeSplitExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + await delegatedManager.connect(owner.wallet).removeExtensions([streamingFeeSplitExtension.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Extension must be pending"); + }); + }); + + describe("when the extension is already initialized", async () => { + beforeEach(async () => { + await streamingFeeSplitExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Extension must be pending"); + }); + }); + + describe("when the manager is not a ManagerCore-enabled manager", async () => { + beforeEach(async () => { + await managerCore.connect(owner.wallet).removeManager(delegatedManager.address); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be ManagerCore-enabled manager"); + }); + }); + }); + + describe("#initializeModuleAndExtension", async () => { + let subjectFeeSettings: StreamingFeeState; + let subjectDelegatedManager: Address; + let subjectCaller: Account; + + beforeEach(async () => { + subjectFeeSettings = feeSettings; + subjectDelegatedManager = delegatedManager.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return streamingFeeSplitExtension.connect(subjectCaller.wallet).initializeModuleAndExtension(subjectDelegatedManager, subjectFeeSettings); + } + + it("should correctly initialize the StreamingFeeModule on the SetToken", async () => { + const txTimestamp = await getTransactionTimestamp(subject()); + + const isModuleInitialized: Boolean = await setToken.isInitializedModule(setV2Setup.streamingFeeModule.address); + expect(isModuleInitialized).to.eq(true); + + const storedFeeState: StreamingFeeState = await setV2Setup.streamingFeeModule.feeStates(setToken.address); + expect(storedFeeState.feeRecipient).to.eq(feeRecipient); + expect(storedFeeState.maxStreamingFeePercentage).to.eq(maxStreamingFeePercentage); + expect(storedFeeState.streamingFeePercentage).to.eq(streamingFeePercentage); + expect(storedFeeState.lastStreamingFeeTimestamp).to.eq(txTimestamp); + }); + + it("should store the correct SetToken and DelegatedManager on the StreamingFeeSplitExtension", async () => { + await subject(); + + const storedDelegatedManager: Address = await streamingFeeSplitExtension.setManagers(setToken.address); + expect(storedDelegatedManager).to.eq(delegatedManager.address); + }); + + it("should initialize the StreamingFeeSplitExtension on the DelegatedManager", async () => { + await subject(); + + const isExtensionInitialized: Boolean = await delegatedManager.isInitializedExtension(streamingFeeSplitExtension.address); + expect(isExtensionInitialized).to.eq(true); + }); + + it("should emit the correct StreamingFeeSplitExtensionInitialized event", async () => { + await expect(subject()).to.emit( + streamingFeeSplitExtension, + "StreamingFeeSplitExtensionInitialized" + ).withArgs(setToken.address, delegatedManager.address); + }); + + describe("when the sender is not the owner", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be owner"); + }); + }); + + describe("when the StreamingFeeModule is not pending or initialized", async () => { + beforeEach(async () => { + await streamingFeeSplitExtension.connect(owner.wallet).initializeModuleAndExtension(subjectDelegatedManager, feeSettings); + await delegatedManager.connect(owner.wallet).removeExtensions([streamingFeeSplitExtension.address]); + await delegatedManager.connect(owner.wallet).setManager(owner.address); + await setToken.connect(owner.wallet).removeModule(setV2Setup.streamingFeeModule.address); + await setToken.connect(owner.wallet).setManager(delegatedManager.address); + await delegatedManager.connect(owner.wallet).addExtensions([streamingFeeSplitExtension.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be pending initialization"); + }); + }); + + describe("when the StreamingFeeModule is already initialized", async () => { + beforeEach(async () => { + await streamingFeeSplitExtension.connect(owner.wallet).initializeModuleAndExtension(subjectDelegatedManager, feeSettings); + await delegatedManager.connect(owner.wallet).removeExtensions([streamingFeeSplitExtension.address]); + await delegatedManager.connect(owner.wallet).addExtensions([streamingFeeSplitExtension.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be pending initialization"); + }); + }); + + describe("when the extension is not pending or initialized", async () => { + beforeEach(async () => { + await streamingFeeSplitExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + await delegatedManager.connect(owner.wallet).removeExtensions([streamingFeeSplitExtension.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Extension must be pending"); + }); + }); + + describe("when the extension is already initialized", async () => { + beforeEach(async () => { + await streamingFeeSplitExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Extension must be pending"); + }); + }); + + describe("when the manager is not a ManagerCore-enabled manager", async () => { + beforeEach(async () => { + await managerCore.connect(owner.wallet).removeManager(delegatedManager.address); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be ManagerCore-enabled manager"); + }); + }); + }); + + describe("#removeExtension", async () => { + let subjectManager: Contract; + let subjectStreamingFeeSplitExtension: Address[]; + let subjectCaller: Account; + + beforeEach(async () => { + await streamingFeeSplitExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + + subjectManager = delegatedManager; + subjectStreamingFeeSplitExtension = [streamingFeeSplitExtension.address]; + subjectCaller = owner; + }); + + async function subject(): Promise { + return subjectManager.connect(subjectCaller.wallet).removeExtensions(subjectStreamingFeeSplitExtension); + } + + it("should clear SetToken and DelegatedManager from StreamingFeeSplitExtension state", async () => { + await subject(); + + const storedDelegatedManager: Address = await streamingFeeSplitExtension.setManagers(setToken.address); + expect(storedDelegatedManager).to.eq(ADDRESS_ZERO); + }); + + it("should emit the correct ExtensionRemoved event", async () => { + await expect(subject()).to.emit( + streamingFeeSplitExtension, + "ExtensionRemoved" + ).withArgs(setToken.address, delegatedManager.address); + }); + + describe("when the caller is not the SetToken manager", async () => { + beforeEach(async () => { + subjectManager = await deployer.mocks.deployManagerMock(setToken.address); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be Manager"); + }); + }); + }); + + describe("#updateStreamingFee", async () => { + let mintedTokens: BigNumber; + const timeFastForward: BigNumber = ONE_YEAR_IN_SECONDS; + + let subjectNewFee: BigNumber; + + let subjectSetToken: Address; + let subjectCaller: Account; + + beforeEach(async () => { + mintedTokens = ether(2); + await setV2Setup.dai.approve(setV2Setup.issuanceModule.address, ether(3)); + await setV2Setup.issuanceModule.issue(setToken.address, mintedTokens, owner.address); + + await increaseTimeAsync(timeFastForward); + + await streamingFeeSplitExtension.connect(owner.wallet).initializeModuleAndExtension(delegatedManager.address, feeSettings); + + subjectNewFee = ether(.01); + subjectSetToken = setToken.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return await streamingFeeSplitExtension.connect(subjectCaller.wallet).updateStreamingFee(subjectSetToken, subjectNewFee); + } + + it("should update the streaming fee on the StreamingFeeModule", async () => { + await subject(); + + const storedFeeState: StreamingFeeState = await setV2Setup.streamingFeeModule.feeStates(setToken.address); + expect(storedFeeState.streamingFeePercentage).to.eq(subjectNewFee); + }); + + it("should send correct amount of fees to the DelegatedManager", async () => { + const preManagerBalance = await setToken.balanceOf(delegatedManager.address); + const feeState: any = await setV2Setup.streamingFeeModule.feeStates(setToken.address); + const totalSupply = await setToken.totalSupply(); + const txnTimestamp = await getTransactionTimestamp(subject()); + + const expectedFeeInflation = await getStreamingFee( + setV2Setup.streamingFeeModule, + setToken.address, + feeState.lastStreamingFeeTimestamp, + txnTimestamp, + ether(.02) + ); + + const feeInflation = getStreamingFeeInflationAmount(expectedFeeInflation, totalSupply); + + const postManagerBalance = await setToken.balanceOf(delegatedManager.address); + + expect(postManagerBalance.sub(preManagerBalance)).to.eq(feeInflation); + }); + + describe("when the sender is not the owner", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be owner"); + }); + }); + }); + + describe("#updateFeeRecipient", async () => { + let subjectNewFeeRecipient: Address; + let subjectSetToken: Address; + let subjectCaller: Account; + + beforeEach(async () => { + await streamingFeeSplitExtension.connect(owner.wallet).initializeModuleAndExtension(delegatedManager.address, feeSettings); + + subjectNewFeeRecipient = factory.address; + subjectSetToken = setToken.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return await streamingFeeSplitExtension.connect(subjectCaller.wallet).updateFeeRecipient(subjectSetToken, subjectNewFeeRecipient); + } + + it("should update the fee recipient on the StreamingFeeModule", async () => { + await subject(); + + const storedFeeState: StreamingFeeState = await setV2Setup.streamingFeeModule.feeStates(setToken.address); + expect(storedFeeState.feeRecipient).to.eq(subjectNewFeeRecipient); + }); + + describe("when the sender is not the owner", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be owner"); + }); + }); + }); + + describe("#accrueFeesAndDistribute", async () => { + let mintedTokens: BigNumber; + const timeFastForward: BigNumber = ONE_YEAR_IN_SECONDS; + let subjectSetToken: Address; + + beforeEach(async () => { + await streamingFeeSplitExtension.connect(owner.wallet).initializeModuleAndExtension(delegatedManager.address, feeSettings); + + mintedTokens = ether(2); + await setV2Setup.dai.approve(setV2Setup.issuanceModule.address, ether(3)); + await setV2Setup.issuanceModule.issue(setToken.address, mintedTokens, factory.address); + + await increaseTimeAsync(timeFastForward); + + subjectSetToken = setToken.address; + }); + + async function subject(): Promise { + return await streamingFeeSplitExtension.accrueFeesAndDistribute(subjectSetToken); + } + + it("should send correct amount of fees to owner fee recipient and methodologist", async () => { + const feeState: any = await setV2Setup.streamingFeeModule.feeStates(setToken.address); + const totalSupply = await setToken.totalSupply(); + + const txnTimestamp = await getTransactionTimestamp(subject()); + + const expectedFeeInflation = await getStreamingFee( + setV2Setup.streamingFeeModule, + setToken.address, + feeState.lastStreamingFeeTimestamp, + txnTimestamp + ); + + const feeInflation = getStreamingFeeInflationAmount(expectedFeeInflation, totalSupply); + + const expectedOwnerTake = preciseMul(feeInflation, ownerFeeSplit); + const expectedMethodologistTake = feeInflation.sub(expectedOwnerTake); + + const ownerFeeRecipientBalance = await setToken.balanceOf(ownerFeeRecipient); + const methodologistBalance = await setToken.balanceOf(methodologist.address); + + expect(ownerFeeRecipientBalance).to.eq(expectedOwnerTake); + expect(methodologistBalance).to.eq(expectedMethodologistTake); + }); + + it("should emit a FeesDistributed event", async () => { + await expect(subject()).to.emit(streamingFeeSplitExtension, "FeesDistributed"); + }); + + describe("when methodologist fees are 0", async () => { + beforeEach(async () => { + await delegatedManager.connect(owner.wallet).updateOwnerFeeSplit(ether(1)); + }); + + it("should not send fees to methodologist", async () => { + const preMethodologistBalance = await setToken.balanceOf(methodologist.address); + + await subject(); + + const postMethodologistBalance = await setToken.balanceOf(methodologist.address); + expect(postMethodologistBalance.sub(preMethodologistBalance)).to.eq(ZERO); + }); + }); + + describe("when owner fees are 0", async () => { + beforeEach(async () => { + await delegatedManager.connect(owner.wallet).updateOwnerFeeSplit(ZERO); + }); + + it("should not send fees to owner fee recipient", async () => { + const preOwnerFeeRecipientBalance = await setToken.balanceOf(ownerFeeRecipient); + + await subject(); + + const postOwnerFeeRecipientBalance = await setToken.balanceOf(ownerFeeRecipient); + expect(postOwnerFeeRecipientBalance.sub(preOwnerFeeRecipientBalance)).to.eq(ZERO); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/extensions/tradeExtension.spec.ts b/test/extensions/tradeExtension.spec.ts new file mode 100644 index 0000000..85be126 --- /dev/null +++ b/test/extensions/tradeExtension.spec.ts @@ -0,0 +1,526 @@ +import "module-alias/register"; + +import { BigNumber, Contract } from "ethers"; +import { Address, Account } from "@utils/types"; +import { ADDRESS_ZERO, EMPTY_BYTES } from "@utils/constants"; +import { + DelegatedManager, + TradeExtension, + ManagerCore +} from "@utils/contracts/index"; +import { + SetToken, + TradeModule, + TradeAdapterMock +} from "@setprotocol/set-protocol-v2/utils/contracts"; +import DeployHelper from "@utils/deploys"; +import { + addSnapshotBeforeRestoreAfterEach, + ether, + getAccounts, + getWaffleExpect +} from "@utils/index"; +import { SystemFixture } from "@setprotocol/set-protocol-v2/utils/fixtures"; +import { getSystemFixture, getRandomAccount } from "@setprotocol/set-protocol-v2/utils/test"; +import { ContractTransaction } from "ethers"; + +const expect = getWaffleExpect(); + +describe("TradeExtension", () => { + let owner: Account; + let methodologist: Account; + let operator: Account; + let factory: Account; + + let deployer: DeployHelper; + let setToken: SetToken; + let setV2Setup: SystemFixture; + + let tradeModule: TradeModule; + + let managerCore: ManagerCore; + let delegatedManager: DelegatedManager; + let tradeExtension: TradeExtension; + + const tradeAdapterName = "TRADEMOCK"; + let tradeMock: TradeAdapterMock; + + before(async () => { + [ + owner, + methodologist, + operator, + factory + ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + + setV2Setup = getSystemFixture(owner.address); + await setV2Setup.initialize(); + + tradeModule = await deployer.setDeployer.modules.deployTradeModule(setV2Setup.controller.address); + await setV2Setup.controller.addModule(tradeModule.address); + + tradeMock = await deployer.setDeployer.mocks.deployTradeAdapterMock(); + + await setV2Setup.integrationRegistry.addIntegration( + tradeModule.address, + tradeAdapterName, + tradeMock.address + ); + + managerCore = await deployer.managerCore.deployManagerCore(); + + tradeExtension = await deployer.globalExtensions.deployTradeExtension( + managerCore.address, + tradeModule.address + ); + + setToken = await setV2Setup.createSetToken( + [setV2Setup.dai.address], + [ether(1)], + [setV2Setup.issuanceModule.address, tradeModule.address] + ); + + await setV2Setup.issuanceModule.initialize(setToken.address, ADDRESS_ZERO); + + delegatedManager = await deployer.manager.deployDelegatedManager( + setToken.address, + factory.address, + methodologist.address, + [tradeExtension.address], + [operator.address], + [setV2Setup.dai.address, setV2Setup.weth.address], + true + ); + + await setToken.setManager(delegatedManager.address); + + await managerCore.initialize([factory.address]); + await managerCore.connect(factory.wallet).addManager(delegatedManager.address); + }); + + addSnapshotBeforeRestoreAfterEach(); + + describe("#constructor", async () => { + let subjectManagerCore: Address; + let subjectTradeModule: Address; + + beforeEach(async () => { + subjectManagerCore = managerCore.address; + subjectTradeModule = tradeModule.address; + }); + + async function subject(): Promise { + return await deployer.globalExtensions.deployTradeExtension( + subjectManagerCore, + subjectTradeModule + ); + } + + it("should set the correct TradeModule address", async () => { + const tradeExtension = await subject(); + + const storedModule = await tradeExtension.tradeModule(); + expect(storedModule).to.eq(subjectTradeModule); + }); + }); + + describe("#initializeModule", async () => { + let subjectDelegatedManager: Address; + let subjectCaller: Account; + + beforeEach(async () => { + await tradeExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + + subjectDelegatedManager = delegatedManager.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return tradeExtension.connect(subjectCaller.wallet).initializeModule(subjectDelegatedManager); + } + + it("should initialize the module on the SetToken", async () => { + await subject(); + + const isModuleInitialized: Boolean = await setToken.isInitializedModule(tradeModule.address); + expect(isModuleInitialized).to.eq(true); + }); + + describe("when the sender is not the owner", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be owner"); + }); + }); + + describe("when the module is not pending or initialized", async () => { + beforeEach(async () => { + await subject(); + await delegatedManager.connect(owner.wallet).removeExtensions([tradeExtension.address]); + await delegatedManager.connect(owner.wallet).setManager(owner.address); + await setToken.connect(owner.wallet).removeModule(tradeModule.address); + await setToken.connect(owner.wallet).setManager(delegatedManager.address); + await delegatedManager.connect(owner.wallet).addExtensions([tradeExtension.address]); + await tradeExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be pending initialization"); + }); + }); + + describe("when the module is already initialized", async () => { + beforeEach(async () => { + await subject(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be pending initialization"); + }); + }); + + describe("when the extension is not pending or initialized", async () => { + beforeEach(async () => { + await delegatedManager.connect(owner.wallet).removeExtensions([tradeExtension.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Extension must be initialized"); + }); + }); + + describe("when the extension is pending", async () => { + beforeEach(async () => { + await delegatedManager.connect(owner.wallet).removeExtensions([tradeExtension.address]); + await delegatedManager.connect(owner.wallet).addExtensions([tradeExtension.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Extension must be initialized"); + }); + }); + + describe("when the manager is not a ManagerCore-enabled manager", async () => { + beforeEach(async () => { + await managerCore.connect(owner.wallet).removeManager(delegatedManager.address); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be ManagerCore-enabled manager"); + }); + }); + }); + + describe("#initializeExtension", async () => { + let subjectDelegatedManager: Address; + let subjectCaller: Account; + + beforeEach(async () => { + subjectDelegatedManager = delegatedManager.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return tradeExtension.connect(subjectCaller.wallet).initializeExtension(subjectDelegatedManager); + } + + it("should store the correct SetToken and DelegatedManager on the TradeExtension", async () => { + await subject(); + + const storedDelegatedManager: Address = await tradeExtension.setManagers(setToken.address); + expect(storedDelegatedManager).to.eq(delegatedManager.address); + }); + + it("should initialize the TradeExtension on the DelegatedManager", async () => { + await subject(); + + const isExtensionInitialized: Boolean = await delegatedManager.isInitializedExtension(tradeExtension.address); + expect(isExtensionInitialized).to.eq(true); + }); + + it("should emit the correct TradeExtensionInitialized event", async () => { + await expect(subject()).to.emit(tradeExtension, "TradeExtensionInitialized").withArgs( + setToken.address, + delegatedManager.address + ); + }); + + describe("when the sender is not the owner", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be owner"); + }); + }); + + describe("when the extension is not pending or initialized", async () => { + beforeEach(async () => { + await tradeExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + await delegatedManager.connect(owner.wallet).removeExtensions([tradeExtension.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Extension must be pending"); + }); + }); + + describe("when the extension is already initialized", async () => { + beforeEach(async () => { + await tradeExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Extension must be pending"); + }); + }); + + describe("when the manager is not a ManagerCore-enabled manager", async () => { + beforeEach(async () => { + await managerCore.connect(owner.wallet).removeManager(delegatedManager.address); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be ManagerCore-enabled manager"); + }); + }); + }); + + describe("#initializeModuleAndExtension", async () => { + let subjectDelegatedManager: Address; + let subjectCaller: Account; + + beforeEach(async () => { + subjectDelegatedManager = delegatedManager.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return tradeExtension.connect(subjectCaller.wallet).initializeModuleAndExtension(subjectDelegatedManager); + } + + it("should initialize the module on the SetToken", async () => { + await subject(); + + const isModuleInitialized: Boolean = await setToken.isInitializedModule(tradeModule.address); + expect(isModuleInitialized).to.eq(true); + }); + + it("should store the correct SetToken and DelegatedManager on the TradeExtension", async () => { + await subject(); + + const storedDelegatedManager: Address = await tradeExtension.setManagers(setToken.address); + expect(storedDelegatedManager).to.eq(delegatedManager.address); + }); + + it("should initialize the TradeExtension on the DelegatedManager", async () => { + await subject(); + + const isExtensionInitialized: Boolean = await delegatedManager.isInitializedExtension(tradeExtension.address); + expect(isExtensionInitialized).to.eq(true); + }); + + it("should emit the correct TradeExtensionInitialized event", async () => { + await expect(subject()).to.emit(tradeExtension, "TradeExtensionInitialized").withArgs( + setToken.address, + delegatedManager.address + ); + }); + + describe("when the sender is not the owner", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be owner"); + }); + }); + + describe("when the module is not pending or initialized", async () => { + beforeEach(async () => { + await tradeExtension.connect(owner.wallet).initializeModuleAndExtension(delegatedManager.address); + await delegatedManager.connect(owner.wallet).removeExtensions([tradeExtension.address]); + await delegatedManager.connect(owner.wallet).setManager(owner.address); + await setToken.connect(owner.wallet).removeModule(tradeModule.address); + await setToken.connect(owner.wallet).setManager(delegatedManager.address); + await delegatedManager.connect(owner.wallet).addExtensions([tradeExtension.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be pending initialization"); + }); + }); + + describe("when the module is already initialized", async () => { + beforeEach(async () => { + await tradeExtension.connect(owner.wallet).initializeModuleAndExtension(delegatedManager.address); + await delegatedManager.connect(owner.wallet).removeExtensions([tradeExtension.address]); + await delegatedManager.connect(owner.wallet).addExtensions([tradeExtension.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be pending initialization"); + }); + }); + + describe("when the extension is not pending or initialized", async () => { + beforeEach(async () => { + await tradeExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + await delegatedManager.connect(owner.wallet).removeExtensions([tradeExtension.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Extension must be pending"); + }); + }); + + describe("when the extension is already initialized", async () => { + beforeEach(async () => { + await tradeExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Extension must be pending"); + }); + }); + + describe("when the manager is not a ManagerCore-enabled manager", async () => { + beforeEach(async () => { + await managerCore.connect(owner.wallet).removeManager(delegatedManager.address); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be ManagerCore-enabled manager"); + }); + }); + }); + + describe("#removeExtension", async () => { + let subjectManager: Contract; + let subjectTradeExtension: Address[]; + let subjectCaller: Account; + + beforeEach(async () => { + await tradeExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + + subjectManager = delegatedManager; + subjectTradeExtension = [tradeExtension.address]; + subjectCaller = owner; + }); + + async function subject(): Promise { + return subjectManager.connect(subjectCaller.wallet).removeExtensions(subjectTradeExtension); + } + + it("should clear SetToken and DelegatedManager from TradeExtension state", async () => { + await subject(); + + const storedDelegatedManager: Address = await tradeExtension.setManagers(setToken.address); + expect(storedDelegatedManager).to.eq(ADDRESS_ZERO); + }); + + it("should emit the correct ExtensionRemoved event", async () => { + await expect(subject()).to.emit(tradeExtension, "ExtensionRemoved").withArgs( + setToken.address, + delegatedManager.address + ); + }); + + describe("when the caller is not the SetToken manager", async () => { + beforeEach(async () => { + subjectManager = await deployer.mocks.deployManagerMock(setToken.address); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be Manager"); + }); + }); + }); + + describe("#trade", async () => { + let mintedTokens: BigNumber; + let subjectSetToken: Address; + let subjectAdapterName: string; + let subjectSendToken: Address; + let subjectSendAmount: BigNumber; + let subjectReceiveToken: Address; + let subjectMinReceiveAmount: BigNumber; + let subjectBytes: string; + let subjectCaller: Account; + + beforeEach(async () => { + await tradeExtension.connect(owner.wallet).initializeModuleAndExtension(delegatedManager.address); + + mintedTokens = ether(1); + await setV2Setup.dai.approve(setV2Setup.issuanceModule.address, ether(1)); + await setV2Setup.issuanceModule.issue(setToken.address, mintedTokens, owner.address); + + // Fund TradeAdapter with destinationToken WETH and DAI + await setV2Setup.weth.transfer(tradeMock.address, ether(10)); + await setV2Setup.dai.transfer(tradeMock.address, ether(10)); + + subjectSetToken = setToken.address; + subjectCaller = operator; + subjectAdapterName = tradeAdapterName; + subjectSendToken = setV2Setup.dai.address; + subjectSendAmount = ether(0.5); + subjectReceiveToken = setV2Setup.weth.address; + subjectMinReceiveAmount = ether(0); + subjectBytes = EMPTY_BYTES; + }); + + async function subject(): Promise { + return tradeExtension.connect(subjectCaller.wallet).trade( + subjectSetToken, + subjectAdapterName, + subjectSendToken, + subjectSendAmount, + subjectReceiveToken, + subjectMinReceiveAmount, + subjectBytes + ); + } + + it("should successfully execute the trade", async () => { + const oldSendTokenBalance = await setV2Setup.dai.balanceOf(setToken.address); + const oldReceiveTokenBalance = await setV2Setup.weth.balanceOf(setToken.address); + + await subject(); + + const expectedNewSendTokenBalance = oldSendTokenBalance.sub(ether(0.5)); + const actualNewSendTokenBalance = await setV2Setup.dai.balanceOf(setToken.address); + const expectedNewReceiveTokenBalance = oldReceiveTokenBalance.add(ether(10)); + const actualNewReceiveTokenBalance = await setV2Setup.weth.balanceOf(setToken.address); + + expect(expectedNewSendTokenBalance).to.eq(actualNewSendTokenBalance); + expect(expectedNewReceiveTokenBalance).to.eq(actualNewReceiveTokenBalance); + }); + + describe("when the sender is not an operator", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be approved operator"); + }); + }); + + describe("when the receiveToken is not an allowed asset", async () => { + beforeEach(async () => { + subjectReceiveToken = setV2Setup.wbtc.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be allowed asset"); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/factories/delegatedManagerFactory.spec.ts b/test/factories/delegatedManagerFactory.spec.ts index 1cb161a..69a52ce 100644 --- a/test/factories/delegatedManagerFactory.spec.ts +++ b/test/factories/delegatedManagerFactory.spec.ts @@ -6,7 +6,8 @@ import { ADDRESS_ZERO, ZERO } from "@utils/constants"; import { DelegatedManagerFactory, DelegatedManager, - BaseGlobalExtensionMock + BaseGlobalExtensionMock, + ManagerCore } from "@utils/contracts/index"; import DeployHelper from "@utils/deploys"; import { @@ -42,6 +43,7 @@ describe("DelegatedManagerFactory", () => { let deployer: DeployHelper; let protocolUtils: ProtocolUtils; + let managerCore: ManagerCore; let delegatedManagerFactory: DelegatedManagerFactory; let mockFeeExtension: BaseGlobalExtensionMock; let mockIssuanceExtension: BaseGlobalExtensionMock; @@ -61,12 +63,17 @@ describe("DelegatedManagerFactory", () => { setV2Setup = getSystemFixture(owner.address); await setV2Setup.initialize(); - mockFeeExtension = await deployer.mocks.deployBaseGlobalExtensionMock(); - mockIssuanceExtension = await deployer.mocks.deployBaseGlobalExtensionMock(); + managerCore = await deployer.managerCore.deployManagerCore(); + + mockFeeExtension = await deployer.mocks.deployBaseGlobalExtensionMock(managerCore.address); + mockIssuanceExtension = await deployer.mocks.deployBaseGlobalExtensionMock(managerCore.address); delegatedManagerFactory = await deployer.factories.deployDelegatedManagerFactory( + managerCore.address, setV2Setup.factory.address ); + + await managerCore.initialize([delegatedManagerFactory.address]); }); // Helper function to run a setup execution of either `createSetAndManager` or `createManager` @@ -109,7 +116,6 @@ describe("DelegatedManagerFactory", () => { ]); const extensionBytecode = mockIssuanceExtension.interface.encodeFunctionData("initializeExtension", [ - setToken, manager ]); @@ -117,18 +123,28 @@ describe("DelegatedManagerFactory", () => { } describe("#constructor", async () => { + let subjectManagerCore: Address; let subjectSetTokenFactory: Address; beforeEach(async () => { + subjectManagerCore = managerCore.address; subjectSetTokenFactory = setV2Setup.factory.address; }); async function subject(): Promise { return await deployer.factories.deployDelegatedManagerFactory( + subjectManagerCore, subjectSetTokenFactory ); } + it("should set the correct ManagerCore address", async () => { + const delegatedManager = await subject(); + + const actualManagerCore = await delegatedManager.managerCore(); + expect (actualManagerCore).to.eq(subjectManagerCore); + }); + it("should set the correct SetToken factory address", async () => { const delegatedManager = await subject(); @@ -197,7 +213,7 @@ describe("DelegatedManagerFactory", () => { expect(await setToken.manager()).eq(delegatedManagerFactory.address); }); - it("should configure the DelegatedBaseManager correctly", async () => { + it("should configure the DelegatedManager correctly", async () => { const tx = await subject(); const setTokenAddress = await protocolUtils.getCreatedSetTokenAddress(tx.hash); @@ -211,6 +227,17 @@ describe("DelegatedManagerFactory", () => { expect(await delegatedManager.useAssetAllowlist()).eq(true); }); + it("should enable the manager on the ManagerCore", async () => { + const tx = await subject(); + + const setTokenAddress = await protocolUtils.getCreatedSetTokenAddress(tx.hash); + const initializeParams = await delegatedManagerFactory.initializeState(setTokenAddress); + + const delegatedManager = await deployer.manager.getDelegatedManager(initializeParams.manager); + const isDelegatedManagerEnabled = await managerCore.isManager(delegatedManager.address); + expect(isDelegatedManagerEnabled).to.eq(true); + }); + it("should set the intialization state correctly", async() => { const createdContracts = await delegatedManagerFactory.callStatic.createSetAndManager( subjectComponents, @@ -301,6 +328,16 @@ describe("DelegatedManagerFactory", () => { await expect(subject()).to.be.revertedWith("Must have at least 1 extension"); }); }); + + describe("when the factory is not approved on the ManagerCore", async() => { + beforeEach(async() => { + await managerCore.removeFactory(delegatedManagerFactory.address); + }); + + it("should revert", async() => { + await expect(subject()).to.be.revertedWith("Only valid factories can call"); + }); + }); }); describe("#createManager", () => { @@ -367,7 +404,7 @@ describe("DelegatedManagerFactory", () => { ); } - it("should configure the DelegatedBaseManager correctly", async () => { + it("should configure the DelegatedManager correctly", async () => { await subject(); const initializeParams = await delegatedManagerFactory.initializeState(subjectSetToken); @@ -399,6 +436,16 @@ describe("DelegatedManagerFactory", () => { expect(initializeParams.manager).eq(newManagerAddress); }); + it("should enable the manager on the ManagerCore", async () => { + await subject(); + + const initializeParams = await delegatedManagerFactory.initializeState(subjectSetToken); + + const delegatedManager = await deployer.manager.getDelegatedManager(initializeParams.manager); + const isDelegatedManagerEnabled = await managerCore.isManager(delegatedManager.address); + expect(isDelegatedManagerEnabled).to.eq(true); + }); + it("should emit a DelegatedManagerDeployed event", async() => { const managerAddress = await delegatedManagerFactory.callStatic.createManager( subjectSetToken, @@ -459,6 +506,16 @@ describe("DelegatedManagerFactory", () => { }); }); + describe("when the factory is not approved on the ManagerCore", async() => { + beforeEach(async() => { + await managerCore.removeFactory(delegatedManagerFactory.address); + }); + + it("should revert", async() => { + await expect(subject()).to.be.revertedWith("Only valid factories can call"); + }); + }); + describe("when the extensions array is empty", async() => { beforeEach(async() => { subjectExtensions = []; @@ -586,6 +643,16 @@ describe("DelegatedManagerFactory", () => { initializeParams.manager ); }); + + describe("when the factory is not approved by the ManagerCore", async() => { + beforeEach(async () => { + await managerCore.connect(owner.wallet).removeManager(manager.address); + }); + + it("should revert", async() => { + await expect(subject()).to.be.revertedWith("Must be ManagerCore-enabled manager"); + }); + }); }); describe("when a SetToken is being migrated to a DelegatedManager", async () => { @@ -669,6 +736,16 @@ describe("DelegatedManagerFactory", () => { expect(finalInitializeParams.manager).eq(ADDRESS_ZERO); expect(finalInitializeParams.isPending).eq(false); }); + + describe("when the factory is not approved by the ManagerCore", async() => { + beforeEach(async () => { + await managerCore.connect(owner.wallet).removeManager(manager.address); + }); + + it("should revert", async() => { + await expect(subject()).to.be.revertedWith("Must be ManagerCore-enabled manager"); + }); + }); }); describe("when the initialization state is not pending", async() => { diff --git a/test/lib/baseGlobalExtension.spec.ts b/test/lib/baseGlobalExtension.spec.ts index 021313e..9fea631 100644 --- a/test/lib/baseGlobalExtension.spec.ts +++ b/test/lib/baseGlobalExtension.spec.ts @@ -2,7 +2,7 @@ import "module-alias/register"; import { Account, Address, Bytes } from "@utils/types"; import { ZERO, ADDRESS_ZERO } from "@utils/constants"; -import { BaseGlobalExtensionMock, DelegatedManager } from "@utils/contracts/index"; +import { BaseGlobalExtensionMock, DelegatedManager, ManagerCore } from "@utils/contracts/index"; import DeployHelper from "@utils/deploys"; @@ -38,6 +38,7 @@ describe("BaseGlobalExtension", () => { let setToken: SetToken; let setV2Setup: SystemFixture; + let managerCore: ManagerCore; let delegatedManager: DelegatedManager; let baseExtensionMock: BaseGlobalExtensionMock; @@ -74,7 +75,9 @@ describe("BaseGlobalExtension", () => { }; await setV2Setup.streamingFeeModule.initialize(setToken.address, streamingFeeSettings); - baseExtensionMock = await deployer.mocks.deployBaseGlobalExtensionMock(); + managerCore = await deployer.managerCore.deployManagerCore(); + + baseExtensionMock = await deployer.mocks.deployBaseGlobalExtensionMock(managerCore.address); // Deploy DelegatedManager delegatedManager = await deployer.manager.deployDelegatedManager( @@ -90,7 +93,10 @@ describe("BaseGlobalExtension", () => { // Transfer ownership to DelegatedManager await setToken.setManager(delegatedManager.address); - await baseExtensionMock.initializeExtension(setToken.address, delegatedManager.address); + await managerCore.initialize([factory.address]); + await managerCore.connect(factory.wallet).addManager(delegatedManager.address); + + await baseExtensionMock.initializeExtension(delegatedManager.address); }); addSnapshotBeforeRestoreAfterEach(); @@ -179,39 +185,43 @@ describe("BaseGlobalExtension", () => { }); }); - describe("#testOnlyManager", async () => { - let subjectRemoveExtensions: Address[]; + describe("#testOnlyOwnerAndValidManager", async () => { + let subjectDelegatedManager: Address; let subjectCaller: Account; beforeEach(async () => { - // Easiest way to test onlyManager is by calling removeExtensions on manager since that's the only - // fxn that calls back into extension - subjectRemoveExtensions = [baseExtensionMock.address]; + await delegatedManager.connect(owner.wallet).removeExtensions([baseExtensionMock.address]); + await delegatedManager.connect(owner.wallet).addExtensions([baseExtensionMock.address]); + + subjectDelegatedManager = delegatedManager.address; subjectCaller = owner; }); async function subject(): Promise { - return delegatedManager.connect(subjectCaller.wallet).removeExtensions(subjectRemoveExtensions); + return baseExtensionMock.connect(subjectCaller.wallet).initializeExtension(subjectDelegatedManager); } it("should succeed without revert", async () => { await subject(); }); - describe("when the sender is not the manager", async () => { - let subjectSetToken: Address; - + describe("when the sender is not the owner", async () => { beforeEach(async () => { - subjectSetToken = setToken.address; - subjectCaller = owner; + subjectCaller = operator; }); - async function subject(): Promise { - return baseExtensionMock.connect(subjectCaller.wallet).testOnlyManager(subjectSetToken); - } + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be owner"); + }); + }); + + describe("when the manager is not a ManagerCore-enabled manager", async () => { + beforeEach(async () => { + await managerCore.connect(owner.wallet).removeManager(delegatedManager.address); + }); it("should revert", async () => { - await expect(subject()).to.be.revertedWith("Must be manager"); + await expect(subject()).to.be.revertedWith("Must be ManagerCore-enabled manager"); }); }); }); diff --git a/test/manager/delegatedManager.spec.ts b/test/manager/delegatedManager.spec.ts index 6b581c0..3c06b20 100644 --- a/test/manager/delegatedManager.spec.ts +++ b/test/manager/delegatedManager.spec.ts @@ -3,7 +3,7 @@ import "module-alias/register"; import { BigNumber } from "ethers"; import { Address, Account, Bytes } from "@utils/types"; import { ADDRESS_ZERO, EXTENSION_STATE, ZERO } from "@utils/constants"; -import { DelegatedManager, BaseGlobalExtensionMock } from "@utils/contracts/index"; +import { DelegatedManager, BaseGlobalExtensionMock, ManagerCore } from "@utils/contracts/index"; import { SetToken } from "@setprotocol/set-protocol-v2/utils/contracts"; import DeployHelper from "@utils/deploys"; import { @@ -34,6 +34,7 @@ describe("DelegatedManager", () => { let deployer: DeployHelper; let setToken: SetToken; + let managerCore: ManagerCore; let delegatedManager: DelegatedManager; let baseExtension: BaseGlobalExtensionMock; @@ -73,7 +74,9 @@ describe("DelegatedManager", () => { }; await setV2Setup.streamingFeeModule.initialize(setToken.address, streamingFeeSettings); - baseExtension = await deployer.mocks.deployBaseGlobalExtensionMock(); + managerCore = await deployer.managerCore.deployManagerCore(); + + baseExtension = await deployer.mocks.deployBaseGlobalExtensionMock(managerCore.address); // Deploy DelegatedManager delegatedManager = await deployer.manager.deployDelegatedManager( @@ -88,6 +91,9 @@ describe("DelegatedManager", () => { // Transfer ownership to DelegatedManager await setToken.setManager(delegatedManager.address); + + await managerCore.initialize([factory.address]); + await managerCore.connect(factory.wallet).addManager(delegatedManager.address); }); addSnapshotBeforeRestoreAfterEach(); @@ -200,7 +206,7 @@ describe("DelegatedManager", () => { return delegatedManager.connect(subjectCaller.wallet).initializeExtension(); } - it("should mark the module as initialized", async () => { + it("should mark the extension as initialized", async () => { await subject(); const isInitializedExternsion = await delegatedManager.extensionAllowlist(otherAccount.address); @@ -393,7 +399,6 @@ describe("DelegatedManager", () => { beforeEach(async () => { await baseExtension.connect(owner.wallet).initializeExtension( - setToken.address, delegatedManager.address ); @@ -857,7 +862,7 @@ describe("DelegatedManager", () => { expect(isModule).to.eq(true); }); - describe("when the caller is not the operator", async () => { + describe("when the caller is not the owner", async () => { beforeEach(async () => { subjectCaller = await getRandomAccount(); }); @@ -887,7 +892,7 @@ describe("DelegatedManager", () => { expect(isModule).to.eq(false); }); - describe("when the caller is not the operator", async () => { + describe("when the caller is not the owner", async () => { beforeEach(async () => { subjectCaller = await getRandomAccount(); }); @@ -920,7 +925,7 @@ describe("DelegatedManager", () => { describe("when manager still has extension initialized", async () => { beforeEach(async () => { - await baseExtension.initializeExtension(setToken.address, delegatedManager.address); + await baseExtension.initializeExtension(delegatedManager.address); }); it("should revert", async () => { @@ -938,7 +943,7 @@ describe("DelegatedManager", () => { }); }); - describe("when the caller is not the operator", async () => { + describe("when the caller is not the owner", async () => { beforeEach(async () => { subjectCaller = methodologist; }); @@ -1012,7 +1017,7 @@ describe("DelegatedManager", () => { describe("when extension is initialized", async () => { beforeEach(async () => { - await baseExtension.connect(owner.wallet).initializeExtension(setToken.address, delegatedManager.address); + await baseExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); }); it("should return false", async () => { @@ -1024,7 +1029,7 @@ describe("DelegatedManager", () => { describe("when the extension is not tracked in allowlist", async () => { beforeEach(async () => { - await baseExtension.connect(owner.wallet).initializeExtension(setToken.address, delegatedManager.address); + await baseExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); await delegatedManager.connect(owner.wallet).removeExtensions([baseExtension.address]); }); @@ -1055,7 +1060,7 @@ describe("DelegatedManager", () => { describe("when extension is initialized", async () => { beforeEach(async () => { - await baseExtension.connect(owner.wallet).initializeExtension(setToken.address, delegatedManager.address); + await baseExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); }); it("should return false", async () => { @@ -1067,7 +1072,7 @@ describe("DelegatedManager", () => { describe("when the extension is not tracked in allowlist", async () => { beforeEach(async () => { - await baseExtension.connect(owner.wallet).initializeExtension(setToken.address, delegatedManager.address); + await baseExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); await delegatedManager.connect(owner.wallet).removeExtensions([baseExtension.address]); }); diff --git a/test/managerCore.spec.ts b/test/managerCore.spec.ts new file mode 100644 index 0000000..6e38ce6 --- /dev/null +++ b/test/managerCore.spec.ts @@ -0,0 +1,405 @@ +import "module-alias/register"; + +import { Account, Address } from "@utils/types"; +import { ADDRESS_ZERO } from "@utils/constants"; +import { + DelegatedManagerFactory, + ManagerCore +} from "@utils/contracts/index"; +import DeployHelper from "@utils/deploys"; +import { + addSnapshotBeforeRestoreAfterEach, + getAccounts, + getWaffleExpect, +} from "@utils/index"; +import { getSystemFixture, getRandomAccount } from "@setprotocol/set-protocol-v2/utils/test"; +import { SystemFixture } from "@setprotocol/set-protocol-v2/utils/fixtures"; + + +const expect = getWaffleExpect(); + +describe("ManagerCore", () => { + let owner: Account; + let mockDelegatedManagerFactory: Account; + let mockManager: Account; + + let deployer: DeployHelper; + let setV2Setup: SystemFixture; + + let managerCore: ManagerCore; + let delegatedManagerFactory: DelegatedManagerFactory; + + before(async () => { + [ + owner, + mockDelegatedManagerFactory, + mockManager + ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + + setV2Setup = getSystemFixture(owner.address); + await setV2Setup.initialize(); + + managerCore = await deployer.managerCore.deployManagerCore(); + + delegatedManagerFactory = await deployer.factories.deployDelegatedManagerFactory( + managerCore.address, + setV2Setup.factory.address + ); + }); + + addSnapshotBeforeRestoreAfterEach(); + + describe("#constructor", async () => { + let subjectDeployer: DeployHelper; + + beforeEach(async () => { + subjectDeployer = new DeployHelper(owner.wallet); + }); + + async function subject(): Promise { + return await subjectDeployer.managerCore.deployManagerCore(); + } + + it("should set the correct owner address", async () => { + const managerCore = await subject(); + + const storedOwner = await managerCore.owner(); + expect (storedOwner).to.eq(owner.address); + }); + }); + + describe("#initialize", async () => { + let subjectCaller: Account; + let subjectFactories: Address[]; + + beforeEach(async () => { + subjectCaller = owner; + subjectFactories = [delegatedManagerFactory.address]; + }); + + async function subject(): Promise { + return await managerCore.connect(subjectCaller.wallet).initialize(subjectFactories); + } + + it("should have set the correct factories length of 1", async () => { + await subject(); + + const factories = await managerCore.getFactories(); + expect(factories.length).to.eq(1); + }); + + it("should have a valid factory", async () => { + await subject(); + + const validFactory = await managerCore.isFactory(delegatedManagerFactory.address); + expect(validFactory).to.eq(true); + }); + + it("should initialize the ManagerCore", async () => { + await subject(); + + const storedIsInitialized = await managerCore.isInitialized(); + expect(storedIsInitialized).to.eq(true); + }); + + describe("when the sender is not the owner", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Ownable: caller is not the owner"); + }); + }); + + describe("when zero address passed for factory", async () => { + beforeEach(async () => { + subjectFactories = [ADDRESS_ZERO]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Zero address submitted."); + }); + }); + + describe("when the ManagerCore is already initialized", async () => { + beforeEach(async () => { + await subject(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("ManagerCore is already initialized"); + }); + }); + }); + + describe("#addManager", async () => { + let subjectManagerCore: ManagerCore; + let subjectManager: Address; + let subjectCaller: Account; + + beforeEach(async () => { + managerCore.initialize([]); + managerCore.addFactory(mockDelegatedManagerFactory.address); + + subjectManagerCore = managerCore; + subjectManager = mockManager.address; + subjectCaller = mockDelegatedManagerFactory; + }); + + async function subject(): Promise { + subjectManagerCore = subjectManagerCore.connect(subjectCaller.wallet); + return subjectManagerCore.addManager(subjectManager); + } + + it("should be stored in the manager array", async () => { + await subject(); + + const managers = await managerCore.getManagers(); + expect(managers.length).to.eq(1); + }); + + it("should be returned as a valid manager", async () => { + await subject(); + + const validManager = await managerCore.isManager(mockManager.address); + expect(validManager).to.eq(true); + }); + + it("should emit the ManagerAdded event", async () => { + await expect(subject()).to.emit(managerCore, "ManagerAdded").withArgs(subjectManager, subjectCaller.address); + }); + + describe("when the manager already exists", async () => { + beforeEach(async () => { + await subject(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Manager already exists"); + }); + }); + + describe("when the caller is not a factory", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Only valid factories can call"); + }); + }); + + describe("when the ManagerCore is not initialized", async () => { + beforeEach(async () => { + subjectManagerCore = await deployer.managerCore.deployManagerCore(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Contract must be initialized."); + }); + }); + }); + + describe("#removeManager", async () => { + let subjectManagerCore: ManagerCore; + let subjectManager: Address; + let subjectCaller: Account; + + beforeEach(async () => { + await managerCore.initialize([]); + await managerCore.addFactory(mockDelegatedManagerFactory.address); + await managerCore.connect(mockDelegatedManagerFactory.wallet).addManager(mockManager.address); + + subjectManagerCore = managerCore; + subjectManager = mockManager.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return subjectManagerCore.connect(subjectCaller.wallet).removeManager(subjectManager); + } + + it("should remove manager from manager array", async () => { + await subject(); + + const managers = await managerCore.getManagers(); + expect(managers.length).to.eq(0); + }); + + it("should return false as a valid manager", async () => { + await subject(); + + const isManager = await managerCore.isManager(mockManager.address); + expect(isManager).to.eq(false); + }); + + it("should emit the ManagerRemoved event", async () => { + await expect(subject()).to.emit(managerCore, "ManagerRemoved").withArgs(subjectManager); + }); + + describe("when the manager does not exist", async () => { + beforeEach(async () => { + await subject(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Manager does not exist"); + }); + }); + + describe("when the sender is not the owner", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Ownable: caller is not the owner"); + }); + }); + + describe("when the ManagerCore is not initialized", async () => { + beforeEach(async () => { + subjectManagerCore = await deployer.managerCore.deployManagerCore(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Contract must be initialized."); + }); + }); + }); + + describe("#addFactory", async () => { + let subjectFactory: Address; + let subjectCaller: Account; + let subjectManagerCore: ManagerCore; + + beforeEach(async () => { + await managerCore.initialize([]); + + subjectFactory = delegatedManagerFactory.address; + subjectCaller = owner; + subjectManagerCore = managerCore; + }); + + async function subject(): Promise { + return await subjectManagerCore.connect(subjectCaller.wallet).addFactory(subjectFactory); + } + + it("should be stored in the factories array", async () => { + await subject(); + + const factories = await managerCore.getFactories(); + expect(factories.length).to.eq(1); + }); + + it("should be returned as a valid factory", async () => { + await subject(); + + const validFactory = await managerCore.isFactory(delegatedManagerFactory.address); + expect(validFactory).to.eq(true); + }); + + it("should emit the FactoryAdded event", async () => { + await expect(subject()).to.emit(managerCore, "FactoryAdded").withArgs(subjectFactory); + }); + + describe("when the ManagerCore is not initialized", async () => { + beforeEach(async () => { + subjectManagerCore = await deployer.managerCore.deployManagerCore(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Contract must be initialized."); + }); + }); + + describe("when the sender is not the owner", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Ownable: caller is not the owner"); + }); + }); + + describe("when the factory already exists", async () => { + beforeEach(async () => { + await subject(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Factory already exists"); + }); + }); + }); + + describe("#removeFactory", async () => { + let subjectFactory: Address; + let subjectCaller: Account; + let subjectManagerCore: ManagerCore; + + beforeEach(async () => { + await managerCore.initialize([delegatedManagerFactory.address]); + + subjectFactory = delegatedManagerFactory.address; + subjectCaller = owner; + subjectManagerCore = managerCore; + }); + + async function subject(): Promise { + return await subjectManagerCore.connect(subjectCaller.wallet).removeFactory(subjectFactory); + } + + it("should remove factory from factories array", async () => { + await subject(); + + const factories = await managerCore.getFactories(); + expect(factories.length).to.eq(0); + }); + + it("should return false as a valid factory", async () => { + await subject(); + + const validFactory = await managerCore.isFactory(delegatedManagerFactory.address); + expect(validFactory).to.eq(false); + }); + + it("should emit the FactoryRemoved event", async () => { + await expect(subject()).to.emit(managerCore, "FactoryRemoved").withArgs(subjectFactory); + }); + + describe("when the ManagerCore is not initialized", async () => { + beforeEach(async () => { + subjectManagerCore = await deployer.managerCore.deployManagerCore(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Contract must be initialized."); + }); + }); + + describe("when the sender is not the owner", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Ownable: caller is not the owner"); + }); + }); + + describe("when the factory does not exist", async () => { + beforeEach(async () => { + await subject(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Factory does not exist"); + }); + }); + }); +}); \ No newline at end of file diff --git a/utils/contracts/index.ts b/utils/contracts/index.ts index 5a0990e..da093ee 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -5,6 +5,8 @@ export { BaseManager } from "../..//typechain/BaseManager"; export { ChainlinkAggregatorMock } from "../../typechain/ChainlinkAggregatorMock"; export { DelegatedManager } from "../../typechain/DelegatedManager"; export { DelegatedManagerFactory } from "../../typechain/DelegatedManagerFactory"; +export { ManagerCore } from "../../typechain/ManagerCore"; +export { ManagerMock } from "../../typechain/ManagerMock"; export { MutualUpgradeMock } from "../../typechain/MutualUpgradeMock"; export { PerpV2LeverageStrategyExtension } from "../../typechain/PerpV2LeverageStrategyExtension"; export { FeeSplitExtension } from "../../typechain/FeeSplitExtension"; @@ -12,3 +14,6 @@ export { PerpV2PriceFeedMock } from "../../typechain/PerpV2PriceFeedMock"; export { StandardTokenMock } from "../../typechain/StandardTokenMock"; export { StringArrayUtilsMock } from "../../typechain/StringArrayUtilsMock"; export { SupplyCapIssuanceHook } from "../../typechain/SupplyCapIssuanceHook"; +export { TradeExtension } from "../../typechain/TradeExtension"; +export { IssuanceExtension } from "../../typechain/IssuanceExtension"; +export { StreamingFeeSplitExtension } from "../../typechain/StreamingFeeSplitExtension"; diff --git a/utils/deploys/deployFactories.ts b/utils/deploys/deployFactories.ts index b66200e..3408e19 100644 --- a/utils/deploys/deployFactories.ts +++ b/utils/deploys/deployFactories.ts @@ -14,8 +14,9 @@ export default class DeployFactories { } public async deployDelegatedManagerFactory( - setTokenFactory: Address, + managerCore: Address, + setTokenFactory: Address ): Promise { - return await new DelegatedManagerFactory__factory(this._deployerSigner).deploy(setTokenFactory); + return await new DelegatedManagerFactory__factory(this._deployerSigner).deploy(managerCore, setTokenFactory); } } \ No newline at end of file diff --git a/utils/deploys/deployGlobalExtensions.ts b/utils/deploys/deployGlobalExtensions.ts new file mode 100644 index 0000000..fc07eb1 --- /dev/null +++ b/utils/deploys/deployGlobalExtensions.ts @@ -0,0 +1,49 @@ +import { Signer } from "ethers"; +import { Address } from "../types"; +import { + IssuanceExtension, + StreamingFeeSplitExtension, + TradeExtension +} from "../contracts/index"; + +import { IssuanceExtension__factory } from "../../typechain/factories/IssuanceExtension__factory"; +import { StreamingFeeSplitExtension__factory } from "../../typechain/factories/StreamingFeeSplitExtension__factory"; +import { TradeExtension__factory } from "../../typechain/factories/TradeExtension__factory"; + +export default class DeployGlobalExtensions { + private _deployerSigner: Signer; + + constructor(deployerSigner: Signer) { + this._deployerSigner = deployerSigner; + } + + public async deployIssuanceExtension( + managerCore: Address, + basicIssuanceModule: Address + ): Promise { + return await new IssuanceExtension__factory(this._deployerSigner).deploy( + managerCore, + basicIssuanceModule, + ); + } + + public async deployStreamingFeeSplitExtension( + managerCore: Address, + streamingFeeModule: Address + ): Promise { + return await new StreamingFeeSplitExtension__factory(this._deployerSigner).deploy( + managerCore, + streamingFeeModule, + ); + } + + public async deployTradeExtension( + managerCore: Address, + tradeModule: Address + ): Promise { + return await new TradeExtension__factory(this._deployerSigner).deploy( + managerCore, + tradeModule, + ); + } +} \ No newline at end of file diff --git a/utils/deploys/deployManagerCore.ts b/utils/deploys/deployManagerCore.ts new file mode 100644 index 0000000..dee5515 --- /dev/null +++ b/utils/deploys/deployManagerCore.ts @@ -0,0 +1,16 @@ +import { Signer } from "ethers"; + +import { ManagerCore } from "../contracts"; +import { ManagerCore__factory } from "../../typechain/factories/ManagerCore__factory"; + +export default class DeployFactories { + private _deployerSigner: Signer; + + constructor(deployerSigner: Signer) { + this._deployerSigner = deployerSigner; + } + + public async deployManagerCore(): Promise { + return await new ManagerCore__factory(this._deployerSigner).deploy(); + } +} \ No newline at end of file diff --git a/utils/deploys/deployMocks.ts b/utils/deploys/deployMocks.ts index 2695719..ac87916 100644 --- a/utils/deploys/deployMocks.ts +++ b/utils/deploys/deployMocks.ts @@ -4,6 +4,7 @@ import { AddressArrayUtilsMock, BaseExtensionMock, BaseGlobalExtensionMock, + ManagerMock, MutualUpgradeMock, StandardTokenMock, StringArrayUtilsMock, @@ -18,6 +19,7 @@ import { import { AddressArrayUtilsMock__factory } from "../../typechain/factories/AddressArrayUtilsMock__factory"; import { BaseExtensionMock__factory } from "../../typechain/factories/BaseExtensionMock__factory"; import { BaseGlobalExtensionMock__factory } from "../../typechain/factories/BaseGlobalExtensionMock__factory"; +import { ManagerMock__factory } from "../../typechain/factories/ManagerMock__factory"; import { ChainlinkAggregatorMock__factory } from "@setprotocol/set-protocol-v2/typechain"; import { ContractCallerMock__factory } from "@setprotocol/set-protocol-v2/typechain"; import { MutualUpgradeMock__factory } from "../../typechain/factories/MutualUpgradeMock__factory"; @@ -36,8 +38,12 @@ export default class DeployMocks { return await new BaseExtensionMock__factory(this._deployerSigner).deploy(manager); } - public async deployBaseGlobalExtensionMock(): Promise { - return await new BaseGlobalExtensionMock__factory(this._deployerSigner).deploy(); + public async deployBaseGlobalExtensionMock(managerCore: Address): Promise { + return await new BaseGlobalExtensionMock__factory(this._deployerSigner).deploy(managerCore); + } + + public async deployManagerMock(setToken: Address): Promise { + return await new ManagerMock__factory(this._deployerSigner).deploy(setToken); } public async deployAddressArrayUtilsMock(): Promise { diff --git a/utils/deploys/deploySetV2.ts b/utils/deploys/deploySetV2.ts index 27d0e95..9096087 100644 --- a/utils/deploys/deploySetV2.ts +++ b/utils/deploys/deploySetV2.ts @@ -7,6 +7,8 @@ import { SetToken } from "@setprotocol/set-protocol-v2/typechain/SetToken"; import { SetToken__factory } from "@setprotocol/set-protocol-v2/typechain/factories/SetToken__factory"; import { DebtIssuanceModule } from "@setprotocol/set-protocol-v2/typechain/DebtIssuanceModule"; import { DebtIssuanceModule__factory } from "@setprotocol/set-protocol-v2/typechain/factories/DebtIssuanceModule__factory"; +import { DebtIssuanceModuleV2 } from "@setprotocol/set-protocol-v2/typechain/DebtIssuanceModuleV2"; +import { DebtIssuanceModuleV2__factory } from "@setprotocol/set-protocol-v2/typechain/factories/DebtIssuanceModuleV2__factory"; export default class DeploySetV2 { private _deployerSigner: Signer; @@ -23,6 +25,14 @@ export default class DeploySetV2 { ); } + public async deployDebtIssuanceModuleV2( + controller: Address, + ): Promise { + return await new DebtIssuanceModuleV2__factory(this._deployerSigner).deploy( + controller, + ); + } + /* GETTERS */ public async getSetToken(setTokenAddress: Address): Promise { diff --git a/utils/deploys/index.ts b/utils/deploys/index.ts index e570233..9345857 100644 --- a/utils/deploys/index.ts +++ b/utils/deploys/index.ts @@ -5,12 +5,16 @@ import SetDeployHelper from "@setprotocol/set-protocol-v2/utils/deploys"; import DeployManager from "./deployManager"; import DeployMocks from "./deployMocks"; import DeployExtensions from "./deployExtensions"; +import DeployManagerCore from "./deployManagerCore"; +import DeployGlobalExtensions from "./deployGlobalExtensions"; import DeployFactories from "./deployFactories"; import DeployHooks from "./deployHooks"; import DeploySetV2 from "./deploySetV2"; export default class DeployHelper { public extensions: DeployExtensions; + public managerCore: DeployManagerCore; + public globalExtensions: DeployGlobalExtensions; public factories: DeployFactories; public manager: DeployManager; public mocks: DeployMocks; @@ -20,6 +24,8 @@ export default class DeployHelper { constructor(deployerSigner: Signer) { this.extensions = new DeployExtensions(deployerSigner); + this.managerCore = new DeployManagerCore(deployerSigner); + this.globalExtensions = new DeployGlobalExtensions(deployerSigner); this.factories = new DeployFactories(deployerSigner); this.manager = new DeployManager(deployerSigner); this.mocks = new DeployMocks(deployerSigner); diff --git a/utils/types.ts b/utils/types.ts index d3da4b8..b0cb365 100644 --- a/utils/types.ts +++ b/utils/types.ts @@ -67,4 +67,11 @@ export interface PerpV2IncentiveSettings { incentivizedSlippageTolerance: BigNumber; etherReward: BigNumber; incentivizedLeverageRatio: BigNumber; +} + +export interface StreamingFeeState { + feeRecipient: Address; + streamingFeePercentage: BigNumber; + maxStreamingFeePercentage: BigNumber; + lastStreamingFeeTimestamp: BigNumber; } \ No newline at end of file