From f052eb1bc36f23513a4942d2be20772a47a5426c Mon Sep 17 00:00:00 2001 From: Pranav Bhardwaj Date: Fri, 25 Aug 2023 10:00:06 -0400 Subject: [PATCH 01/10] add delegated manager system contracts --- contracts/ManagerCore.sol | 236 ++++++ .../factories/DelegatedManagerFactory.sol | 469 +++++++++++ .../GlobalBatchTradeExtension.sol | 292 +++++++ .../GlobalClaimExtension.sol | 742 ++++++++++++++++++ .../GlobalIssuanceExtension.sol | 284 +++++++ .../GlobalStreamingFeeSplitExtension.sol | 230 ++++++ .../GlobalTradeExtension.sol | 166 ++++ .../global-extensions/GlobalWrapExtension.sol | 264 +++++++ contracts/interfaces/IClaimAdapter.sol | 65 ++ contracts/interfaces/IClaimModule.sol | 46 ++ contracts/interfaces/IDelegatedManager.sol | 53 ++ contracts/interfaces/IGlobalExtension.sol | 24 + contracts/interfaces/IManagerCore.sol | 26 + contracts/interfaces/IModule.sol | 33 + contracts/interfaces/IPriceOracle.sol | 32 + contracts/interfaces/ISetTokenCreator.sol | 32 + contracts/interfaces/ISetValuer.sol | 24 + contracts/interfaces/IWrapModuleV2.sol | 11 +- contracts/lib/BaseGlobalExtension.sol | 157 ++++ contracts/lib/ExplicitERC20.sol | 71 ++ contracts/lib/Invoke.sol | 141 ++++ contracts/lib/ModuleBase.sol | 237 ++++++ contracts/lib/MutualUpgradeV2.sol | 82 ++ contracts/lib/Position.sol | 259 ++++++ contracts/lib/ResourceIdentifier.sol | 64 ++ contracts/manager/DelegatedManager.sol | 478 +++++++++++ contracts/mocks/BaseGlobalExtensionMock.sol | 110 +++ contracts/mocks/ManagerMock.sol | 42 + contracts/mocks/ModuleMock.sol | 48 ++ contracts/mocks/MutualUpgradeV2Mock.sol | 28 + 30 files changed, 4742 insertions(+), 4 deletions(-) create mode 100644 contracts/ManagerCore.sol create mode 100644 contracts/factories/DelegatedManagerFactory.sol create mode 100644 contracts/global-extensions/GlobalBatchTradeExtension.sol create mode 100644 contracts/global-extensions/GlobalClaimExtension.sol create mode 100644 contracts/global-extensions/GlobalIssuanceExtension.sol create mode 100644 contracts/global-extensions/GlobalStreamingFeeSplitExtension.sol create mode 100644 contracts/global-extensions/GlobalTradeExtension.sol create mode 100644 contracts/global-extensions/GlobalWrapExtension.sol create mode 100644 contracts/interfaces/IClaimAdapter.sol create mode 100644 contracts/interfaces/IClaimModule.sol create mode 100644 contracts/interfaces/IDelegatedManager.sol create mode 100644 contracts/interfaces/IGlobalExtension.sol create mode 100644 contracts/interfaces/IManagerCore.sol create mode 100644 contracts/interfaces/IModule.sol create mode 100644 contracts/interfaces/IPriceOracle.sol create mode 100644 contracts/interfaces/ISetTokenCreator.sol create mode 100644 contracts/interfaces/ISetValuer.sol create mode 100644 contracts/lib/BaseGlobalExtension.sol create mode 100644 contracts/lib/ExplicitERC20.sol create mode 100644 contracts/lib/Invoke.sol create mode 100644 contracts/lib/ModuleBase.sol create mode 100644 contracts/lib/MutualUpgradeV2.sol create mode 100644 contracts/lib/Position.sol create mode 100644 contracts/lib/ResourceIdentifier.sol create mode 100644 contracts/manager/DelegatedManager.sol create mode 100644 contracts/mocks/BaseGlobalExtensionMock.sol create mode 100644 contracts/mocks/ManagerMock.sol create mode 100644 contracts/mocks/ModuleMock.sol create mode 100644 contracts/mocks/MutualUpgradeV2Mock.sol diff --git a/contracts/ManagerCore.sol b/contracts/ManagerCore.sol new file mode 100644 index 00000000..8af7ce0c --- /dev/null +++ b/contracts/ManagerCore.sol @@ -0,0 +1,236 @@ +/* + 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 GlobalExtensions, DelegatedManagerFactories, and DelegatedManagers. + */ +contract ManagerCore is Ownable { + using AddressArrayUtils for address[]; + + /* ============ Events ============ */ + + event ExtensionAdded(address indexed _extension); + event ExtensionRemoved(address indexed _extension); + 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 extensions + address[] public extensions; + // List of enabled factories of managers + address[] public factories; + // List of enabled managers + address[] public managers; + + // Mapping to check whether address is valid Extension, Factory, or Manager + mapping(address => bool) public isExtension; + mapping(address => bool) public isFactory; + mapping(address => bool) public isManager; + + + // 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 _extensions List of extensions to add + * @param _factories List of factories to add + */ + function initialize( + address[] memory _extensions, + address[] memory _factories + ) + external + onlyOwner + { + require(!isInitialized, "ManagerCore is already initialized"); + + extensions = _extensions; + factories = _factories; + + // Loop through and initialize isExtension and isFactory mapping + for (uint256 i = 0; i < _extensions.length; i++) { + _addExtension(_extensions[i]); + } + for (uint256 i = 0; i < _factories.length; i++) { + _addFactory(_factories[i]); + } + + // Set to true to only allow initialization once + isInitialized = true; + } + + /** + * PRIVILEGED GOVERNANCE FUNCTION. Allows governance to add an extension + * + * @param _extension Address of the extension contract to add + */ + function addExtension(address _extension) external onlyInitialized onlyOwner { + require(!isExtension[_extension], "Extension already exists"); + + _addExtension(_extension); + + extensions.push(_extension); + } + + /** + * PRIVILEGED GOVERNANCE FUNCTION. Allows governance to remove an extension + * + * @param _extension Address of the extension contract to remove + */ + function removeExtension(address _extension) external onlyInitialized onlyOwner { + require(isExtension[_extension], "Extension does not exist"); + + extensions.removeStorage(_extension); + + isExtension[_extension] = false; + + emit ExtensionRemoved(_extension); + } + + /** + * 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"); + + _addFactory(_factory); + + factories.push(_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); + } + + /** + * 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); + } + + /* ============ External Getter Functions ============ */ + + function getExtensions() external view returns (address[] memory) { + return extensions; + } + + function getFactories() external view returns (address[] memory) { + return factories; + } + + function getManagers() external view returns (address[] memory) { + return managers; + } + + /* ============ Internal Functions ============ */ + + /** + * Add an extension tracked on the ManagerCore + * + * @param _extension Address of the extension contract to add + */ + function _addExtension(address _extension) internal { + require(_extension != address(0), "Zero address submitted."); + + isExtension[_extension] = true; + + emit ExtensionAdded(_extension); + } + + /** + * Add a factory tracked on the ManagerCore + * + * @param _factory Address of the factory contract to add + */ + function _addFactory(address _factory) internal { + require(_factory != address(0), "Zero address submitted."); + + isFactory[_factory] = true; + + emit FactoryAdded(_factory); + } +} diff --git a/contracts/factories/DelegatedManagerFactory.sol b/contracts/factories/DelegatedManagerFactory.sol new file mode 100644 index 00000000..71df8b1d --- /dev/null +++ b/contracts/factories/DelegatedManagerFactory.sol @@ -0,0 +1,469 @@ +/* + 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 { AddressArrayUtils } from "../lib/AddressArrayUtils.sol"; +import { IController } from "../interfaces/IController.sol"; +import { ISetToken } from "../interfaces/ISetToken.sol"; +import { ISetTokenCreator } from "../interfaces/ISetTokenCreator.sol"; + +import { DelegatedManager } from "../manager/DelegatedManager.sol"; +import { IDelegatedManager } from "../interfaces/IDelegatedManager.sol"; +import { IManagerCore } from "../interfaces/IManagerCore.sol"; + +/** + * @title DelegatedManagerFactory + * @author Set Protocol + * + * Factory smart contract which gives asset managers the ability to: + * > create a Set Token managed with a DelegatedManager contract + * > create a DelegatedManager contract for an existing Set Token to migrate to + * > initialize extensions and modules for SetTokens using the DelegatedManager system + */ +contract DelegatedManagerFactory { + using AddressArrayUtils for address[]; + using Address for address; + + /* ============ Structs ============ */ + + struct InitializeParams{ + address deployer; + address owner; + address methodologist; + IDelegatedManager manager; + bool isPending; + } + + /* ============ Events ============ */ + + /** + * @dev Emitted on DelegatedManager creation + * @param _setToken Instance of the SetToken being created + * @param _manager Address of the DelegatedManager + * @param _deployer Address of the deployer + */ + event DelegatedManagerCreated( + ISetToken indexed _setToken, + DelegatedManager indexed _manager, + address _deployer + ); + + /** + * @dev Emitted on DelegatedManager initialization + * @param _setToken Instance of the SetToken being initialized + * @param _manager Address of the DelegatedManager owner + */ + event DelegatedManagerInitialized( + ISetToken indexed _setToken, + IDelegatedManager indexed _manager + ); + + /* ============ State Variables ============ */ + + // ManagerCore address + IManagerCore public immutable managerCore; + + // Controller address + IController public immutable controller; + + // SetTokenFactory address + ISetTokenCreator public immutable setTokenFactory; + + // Mapping which stores manager creation metadata between creation and initialization steps + mapping(ISetToken=>InitializeParams) public initializeState; + + /* ============ Constructor ============ */ + + /** + * @dev Sets managerCore and setTokenFactory address. + * @param _managerCore Address of ManagerCore protocol contract + * @param _controller Address of Controller protocol contract + * @param _setTokenFactory Address of SetTokenFactory protocol contract + */ + constructor( + IManagerCore _managerCore, + IController _controller, + ISetTokenCreator _setTokenFactory + ) + public + { + managerCore = _managerCore; + controller = _controller; + setTokenFactory = _setTokenFactory; + } + + /* ============ External Functions ============ */ + + /** + * ANYONE CAN CALL: Deploys a new SetToken and DelegatedManager. Sets some temporary metadata about + * the deployment which will be read during a subsequent intialization step which wires everything + * together. + * + * @param _components List of addresses of components for initial Positions + * @param _units List of units. Each unit is the # of components per 10^18 of a SetToken + * @param _name Name of the SetToken + * @param _symbol Symbol of the SetToken + * @param _owner Address to set as the DelegateManager's `owner` role + * @param _methodologist Address to set as the DelegateManager's methodologist role + * @param _modules List of modules to enable. All modules must be approved by the Controller + * @param _operators List of operators authorized for the DelegateManager + * @param _assets List of assets DelegateManager can trade. When empty, asset allow list is not enforced + * @param _extensions List of extensions authorized for the DelegateManager + * + * @return (ISetToken, address) The created SetToken and DelegatedManager addresses, respectively + */ + function createSetAndManager( + address[] memory _components, + int256[] memory _units, + string memory _name, + string memory _symbol, + address _owner, + address _methodologist, + address[] memory _modules, + address[] memory _operators, + address[] memory _assets, + address[] memory _extensions + ) + external + returns (ISetToken, address) + { + _validateManagerParameters(_components, _extensions, _assets); + + ISetToken setToken = _deploySet( + _components, + _units, + _modules, + _name, + _symbol + ); + + DelegatedManager manager = _deployManager( + setToken, + _extensions, + _operators, + _assets + ); + + _setInitializationState(setToken, address(manager), _owner, _methodologist); + + return (setToken, address(manager)); + } + + /** + * ONLY SETTOKEN MANAGER: Deploys a DelegatedManager and sets some temporary metadata about the + * deployment which will be read during a subsequent intialization step which wires everything together. + * This method is used when migrating an existing SetToken to the DelegatedManager system. + * + * (Note: This flow should work well for SetTokens managed by an EOA. However, existing + * contract-managed Sets may need to have their ownership temporarily transferred to an EOA when + * migrating. We don't anticipate high demand for this migration case though.) + * + * @param _setToken Instance of SetToken to migrate to the DelegatedManager system + * @param _owner Address to set as the DelegateManager's `owner` role + * @param _methodologist Address to set as the DelegateManager's methodologist role + * @param _operators List of operators authorized for the DelegateManager + * @param _assets List of assets DelegateManager can trade. When empty, asset allow list is not enforced + * @param _extensions List of extensions authorized for the DelegateManager + * + * @return (address) Address of the created DelegatedManager + */ + function createManager( + ISetToken _setToken, + address _owner, + address _methodologist, + address[] memory _operators, + address[] memory _assets, + address[] memory _extensions + ) + external + returns (address) + { + require(controller.isSet(address(_setToken)), "Must be controller-enabled SetToken"); + require(msg.sender == _setToken.manager(), "Must be manager"); + + _validateManagerParameters(_setToken.getComponents(), _extensions, _assets); + + DelegatedManager manager = _deployManager( + _setToken, + _extensions, + _operators, + _assets + ); + + _setInitializationState(_setToken, address(manager), _owner, _methodologist); + + return address(manager); + } + + /** + * ONLY DEPLOYER: Wires SetToken, DelegatedManager, global manager extensions, and modules together + * into a functioning package. + * + * 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. + * + * @param _setToken Instance of the SetToken + * @param _ownerFeeSplit Percent of fees in precise units (10^16 = 1%) sent to owner, rest to methodologist + * @param _ownerFeeRecipient Address which receives owner's share of fees when they're distributed + * @param _extensions List of addresses of extensions which need to be initialized + * @param _initializeBytecode List of bytecode encoded calls to relevant target's initialize function + */ + function initialize( + ISetToken _setToken, + uint256 _ownerFeeSplit, + address _ownerFeeRecipient, + address[] memory _extensions, + bytes[] memory _initializeBytecode + ) + external + { + require(initializeState[_setToken].isPending, "Manager must be awaiting initialization"); + require(msg.sender == initializeState[_setToken].deployer, "Only deployer can initialize manager"); + _extensions.validatePairsWithArray(_initializeBytecode); + + IDelegatedManager manager = initializeState[_setToken].manager; + + // If the SetToken was factory-deployed & factory is its current `manager`, transfer + // managership to the new DelegatedManager + if (_setToken.manager() == address(this)) { + _setToken.setManager(address(manager)); + } + + _initializeExtensions(manager, _extensions, _initializeBytecode); + + _setManagerState( + manager, + initializeState[_setToken].owner, + initializeState[_setToken].methodologist, + _ownerFeeSplit, + _ownerFeeRecipient + ); + + delete initializeState[_setToken]; + + emit DelegatedManagerInitialized(_setToken, manager); + } + + /* ============ Internal Functions ============ */ + + /** + * Deploys a SetToken, setting this factory as its manager temporarily, pending initialization. + * Managership is transferred to a newly created DelegatedManager during `initialize` + * + * @param _components List of addresses of components for initial Positions + * @param _units List of units. Each unit is the # of components per 10^18 of a SetToken + * @param _modules List of modules to enable. All modules must be approved by the Controller + * @param _name Name of the SetToken + * @param _symbol Symbol of the SetToken + * + * @return Address of created SetToken; + */ + function _deploySet( + address[] memory _components, + int256[] memory _units, + address[] memory _modules, + string memory _name, + string memory _symbol + ) + internal + returns (ISetToken) + { + address setToken = setTokenFactory.create( + _components, + _units, + _modules, + address(this), + _name, + _symbol + ); + + return ISetToken(setToken); + } + + /** + * Deploys a DelegatedManager. Sets owner and methodologist roles to address(this) and the resulting manager address is + * saved to the ManagerCore. + * + * @param _setToken Instance of SetToken to migrate to the DelegatedManager system + * @param _extensions List of extensions authorized for the DelegateManager + * @param _operators List of operators authorized for the DelegateManager + * @param _assets List of assets DelegateManager can trade. When empty, asset allow list is not enforced + * + * @return Address of created DelegatedManager + */ + function _deployManager( + ISetToken _setToken, + address[] memory _extensions, + address[] memory _operators, + address[] memory _assets + ) + internal + returns (DelegatedManager) + { + // If asset array is empty, manager's useAssetAllowList will be set to false + // and the asset allow list is not enforced + bool useAssetAllowlist = _assets.length > 0; + + DelegatedManager newManager = new DelegatedManager( + _setToken, + address(this), + address(this), + _extensions, + _operators, + _assets, + useAssetAllowlist + ); + + // Registers manager with ManagerCore + managerCore.addManager(address(newManager)); + + emit DelegatedManagerCreated( + _setToken, + newManager, + msg.sender + ); + + return newManager; + } + + /** + * Initialize extensions on the DelegatedManager. Checks that extensions are tracked on the ManagerCore and that the + * provided bytecode targets the input manager. + * + * @param _manager Instance of DelegatedManager + * @param _extensions List of addresses of extensions to initialize + * @param _initializeBytecode List of bytecode encoded calls to relevant extensions's initialize function + */ + function _initializeExtensions( + IDelegatedManager _manager, + address[] memory _extensions, + bytes[] memory _initializeBytecode + ) internal { + for (uint256 i = 0; i < _extensions.length; i++) { + address extension = _extensions[i]; + require(managerCore.isExtension(extension), "Target must be ManagerCore-enabled Extension"); + + bytes memory initializeBytecode = _initializeBytecode[i]; + + // Each input initializeBytecode is a varible length bytes array which consists of a 32 byte prefix for the + // length parameter, a 4 byte function selector, a 32 byte DelegatedManager address, and any additional parameters + // as shown below: + // [32 bytes - length parameter, 4 bytes - function selector, 32 bytes - DelegatedManager address, additional parameters] + // It is required that the input DelegatedManager address is the DelegatedManager address corresponding to the caller + address inputManager; + assembly { + inputManager := mload(add(initializeBytecode, 36)) + } + require(inputManager == address(_manager), "Must target correct DelegatedManager"); + + // Because we validate uniqueness of _extensions only one transaction can be sent to each extension during this + // transaction. Due to this no extension can be used for any SetToken transactions other than initializing these contracts + extension.functionCallWithValue(initializeBytecode, 0); + } + } + + /** + * Stores temporary creation metadata during the contract creation step. Data is retrieved, read and + * finally deleted during `initialize`. + * + * @param _setToken Instance of SetToken + * @param _manager Address of DelegatedManager created for SetToken + * @param _owner Address that will be given the `owner` DelegatedManager's role on initialization + * @param _methodologist Address that will be given the `methodologist` DelegatedManager's role on initialization + */ + function _setInitializationState( + ISetToken _setToken, + address _manager, + address _owner, + address _methodologist + ) internal { + initializeState[_setToken] = InitializeParams({ + deployer: msg.sender, + owner: _owner, + methodologist: _methodologist, + manager: IDelegatedManager(_manager), + isPending: true + }); + } + + /** + * Initialize fee settings on DelegatedManager and transfer `owner` and `methodologist` roles. + * + * @param _manager Instance of DelegatedManager + * @param _owner Address that will be given the `owner` DelegatedManager's role + * @param _methodologist Address that will be given the `methodologist` DelegatedManager's role + * @param _ownerFeeSplit Percent of fees in precise units (10^16 = 1%) sent to owner, rest to methodologist + * @param _ownerFeeRecipient Address which receives owner's share of fees when they're distributed + */ + function _setManagerState( + IDelegatedManager _manager, + address _owner, + address _methodologist, + uint256 _ownerFeeSplit, + address _ownerFeeRecipient + ) internal { + _manager.updateOwnerFeeSplit(_ownerFeeSplit); + _manager.updateOwnerFeeRecipient(_ownerFeeRecipient); + + _manager.transferOwnership(_owner); + _manager.setMethodologist(_methodologist); + } + + /** + * Validates that all components currently held by the Set are on the asset allow list. Validate that the manager is + * deployed with at least one extension in the PENDING state. + * + * @param _components List of addresses of components for initial/current Set positions + * @param _extensions List of extensions authorized for the DelegateManager + * @param _assets List of assets DelegateManager can trade. When empty, asset allow list is not enforced + */ + function _validateManagerParameters( + address[] memory _components, + address[] memory _extensions, + address[] memory _assets + ) + internal + pure + { + require(_extensions.length > 0, "Must have at least 1 extension"); + + if (_assets.length != 0) { + _validateComponentsIncludedInAssetsList(_components, _assets); + } + } + + /** + * Validates that all SetToken components are included in the assets whitelist. This prevents the + * DelegatedManager from being initialized with some components in an untrade-able state. + * + * @param _components List of addresses of components for initial Positions + * @param _assets List of assets DelegateManager can trade. + */ + function _validateComponentsIncludedInAssetsList( + address[] memory _components, + address[] memory _assets + ) internal pure { + for (uint256 i = 0; i < _components.length; i++) { + require(_assets.contains(_components[i]), "Asset list must include all components"); + } + } +} diff --git a/contracts/global-extensions/GlobalBatchTradeExtension.sol b/contracts/global-extensions/GlobalBatchTradeExtension.sol new file mode 100644 index 00000000..8b93a743 --- /dev/null +++ b/contracts/global-extensions/GlobalBatchTradeExtension.sol @@ -0,0 +1,292 @@ +/* + 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 { ISetToken } from "../interfaces/ISetToken.sol"; +import { ITradeModule } from "../interfaces/ITradeModule.sol"; +import { StringArrayUtils } from "../lib/StringArrayUtils.sol"; + +import { BaseGlobalExtension } from "../lib/BaseGlobalExtension.sol"; +import { IDelegatedManager } from "../interfaces/IDelegatedManager.sol"; +import { IManagerCore } from "../interfaces/IManagerCore.sol"; + +/** + * @title GlobalBatchTradeExtension + * @author Set Protocol + * + * Smart contract global extension which provides DelegatedManager operator(s) the ability to execute a batch of trades + * on a DEX and the owner the ability to restrict operator(s) permissions with an asset whitelist. + */ +contract GlobalBatchTradeExtension is BaseGlobalExtension { + using StringArrayUtils for string[]; + + /* ============ Structs ============ */ + + struct TradeInfo { + string exchangeName; // Human readable name of the exchange in the integrations registry + address sendToken; // Address of the token to be sent to the exchange + uint256 sendQuantity; // Max units of `sendToken` sent to the exchange + address receiveToken; // Address of the token that will be received from the exchange + uint256 receiveQuantity; // Min units of `receiveToken` to be received from the exchange + bytes data; // Arbitrary bytes to be used to construct trade call data + } + + /* ============ Events ============ */ + + event IntegrationAdded( + string _integrationName // String name of TradeModule exchange integration to allow + ); + + event IntegrationRemoved( + string _integrationName // String name of TradeModule exchange integration to disallow + ); + + event BatchTradeExtensionInitialized( + address indexed _setToken, // Address of the SetToken which had BatchTradeExtension initialized on their manager + address indexed _delegatedManager // Address of the DelegatedManager which initialized the BatchTradeExtension + ); + + event StringTradeFailed( + address indexed _setToken, // Address of the SetToken which the failed trade targeted + uint256 indexed _index, // Index of trade that failed in _trades parameter of batchTrade call + string _reason, // String reason for the trade failure + TradeInfo _tradeInfo // Input TradeInfo of the failed trade + ); + + event BytesTradeFailed( + address indexed _setToken, // Address of the SetToken which the failed trade targeted + uint256 indexed _index, // Index of trade that failed in _trades parameter of batchTrade call + bytes _lowLevelData, // Bytes low level data reason for the trade failure + TradeInfo _tradeInfo // Input TradeInfo of the failed trade + ); + + /* ============ State Variables ============ */ + + // Instance of TradeModule + ITradeModule public immutable tradeModule; + + // List of allowed TradeModule exchange integrations + string[] public integrations; + + // Mapping to check whether string is allowed TradeModule exchange integration + mapping(string => bool) public isIntegration; + + /* ============ Modifiers ============ */ + + /** + * Throws if the sender is not the ManagerCore contract owner + */ + modifier onlyManagerCoreOwner() { + require(msg.sender == managerCore.owner(), "Caller must be ManagerCore owner"); + _; + } + + /* ============ Constructor ============ */ + + /** + * Instantiate with ManagerCore address, TradeModule address, and allowed TradeModule integration strings. + * + * @param _managerCore Address of ManagerCore contract + * @param _tradeModule Address of TradeModule contract + * @param _integrations List of TradeModule exchange integrations to allow + */ + constructor( + IManagerCore _managerCore, + ITradeModule _tradeModule, + string[] memory _integrations + ) + public + BaseGlobalExtension(_managerCore) + { + tradeModule = _tradeModule; + + integrations = _integrations; + uint256 integrationsLength = _integrations.length; + for (uint256 i = 0; i < integrationsLength; i++) { + _addIntegration(_integrations[i]); + } + } + + /* ============ External Functions ============ */ + + /** + * MANAGER OWNER ONLY. Allows manager owner to add allowed TradeModule exchange integrations + * + * @param _integrations List of TradeModule exchange integrations to allow + */ + function addIntegrations(string[] memory _integrations) external onlyManagerCoreOwner { + uint256 integrationsLength = _integrations.length; + for (uint256 i = 0; i < integrationsLength; i++) { + require(!isIntegration[_integrations[i]], "Integration already exists"); + + integrations.push(_integrations[i]); + + _addIntegration(_integrations[i]); + } + } + + /** + * MANAGER OWNER ONLY. Allows manager owner to remove allowed TradeModule exchange integrations + * + * @param _integrations List of TradeModule exchange integrations to disallow + */ + function removeIntegrations(string[] memory _integrations) external onlyManagerCoreOwner { + uint256 integrationsLength = _integrations.length; + for (uint256 i = 0; i < integrationsLength; i++) { + require(isIntegration[_integrations[i]], "Integration does not exist"); + + integrations.removeStorage(_integrations[i]); + + isIntegration[_integrations[i]] = false; + + IntegrationRemoved(_integrations[i]); + } + } + + /** + * 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) { + _initializeModule(_delegatedManager.setToken(), _delegatedManager); + } + + /** + * ONLY OWNER: Initializes BatchTradeExtension to the DelegatedManager. + * + * @param _delegatedManager Instance of the DelegatedManager to initialize + */ + function initializeExtension(IDelegatedManager _delegatedManager) external onlyOwnerAndValidManager(_delegatedManager) { + ISetToken setToken = _delegatedManager.setToken(); + + _initializeExtension(setToken, _delegatedManager); + + emit BatchTradeExtensionInitialized(address(setToken), address(_delegatedManager)); + } + + /** + * ONLY OWNER: Initializes BatchTradeExtension to the DelegatedManager and TradeModule to the SetToken + * + * @param _delegatedManager Instance of the DelegatedManager to initialize + */ + function initializeModuleAndExtension(IDelegatedManager _delegatedManager) external onlyOwnerAndValidManager(_delegatedManager){ + ISetToken setToken = _delegatedManager.setToken(); + + _initializeExtension(setToken, _delegatedManager); + _initializeModule(setToken, _delegatedManager); + + emit BatchTradeExtensionInitialized(address(setToken), address(_delegatedManager)); + } + + /** + * ONLY MANAGER: Remove an existing SetToken and DelegatedManager tracked by the BatchTradeExtension + */ + function removeExtension() external override { + IDelegatedManager delegatedManager = IDelegatedManager(msg.sender); + ISetToken setToken = delegatedManager.setToken(); + + _removeExtension(setToken, delegatedManager); + } + + /** + * ONLY OPERATOR: Executes a batch of trades on a supported DEX. If any individual trades fail, events are emitted. + * @dev Although the SetToken units are passed in for the send and receive quantities, the total quantity + * sent and received is the quantity of component units multiplied by the SetToken totalSupply. + * + * @param _setToken Instance of the SetToken to trade + * @param _trades Array of TradeInfo structs containing information about trades + */ + function batchTrade( + ISetToken _setToken, + TradeInfo[] memory _trades + ) + external + onlyOperator(_setToken) + { + uint256 tradesLength = _trades.length; + IDelegatedManager manager = _manager(_setToken); + for(uint256 i = 0; i < tradesLength; i++) { + require(isIntegration[_trades[i].exchangeName], "Must be allowed integration"); + require(manager.isAllowedAsset(_trades[i].receiveToken), "Must be allowed asset"); + + bytes memory callData = abi.encodeWithSelector( + ITradeModule.trade.selector, + _setToken, + _trades[i].exchangeName, + _trades[i].sendToken, + _trades[i].sendQuantity, + _trades[i].receiveToken, + _trades[i].receiveQuantity, + _trades[i].data + ); + + // ZeroEx (for example) throws custom errors which slip through OpenZeppelin's + // functionCallWithValue error management and surface here as `bytes`. These should be + // decode-able off-chain given enough context about protocol targeted by the adapter. + try manager.interactManager(address(tradeModule), callData) {} + catch Error(string memory reason) { + emit StringTradeFailed( + address(_setToken), + i, + reason, + _trades[i] + ); + } catch (bytes memory lowLevelData) { + emit BytesTradeFailed( + address(_setToken), + i, + lowLevelData, + _trades[i] + ); + } + } + } + + /* ============ External Getter Functions ============ */ + + function getIntegrations() external view returns (string[] memory) { + return integrations; + } + + /* ============ Internal Functions ============ */ + + /** + * Add an allowed TradeModule exchange integration to the BatchTradeExtension + * + * @param _integrationName Name of TradeModule exchange integration to allow + */ + function _addIntegration(string memory _integrationName) internal { + isIntegration[_integrationName] = true; + + emit IntegrationAdded(_integrationName); + } + + /** + * 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.encodeWithSelector(ITradeModule.initialize.selector, _setToken); + _invokeManager(_delegatedManager, address(tradeModule), callData); + } +} diff --git a/contracts/global-extensions/GlobalClaimExtension.sol b/contracts/global-extensions/GlobalClaimExtension.sol new file mode 100644 index 00000000..9110a120 --- /dev/null +++ b/contracts/global-extensions/GlobalClaimExtension.sol @@ -0,0 +1,742 @@ +/* + 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 { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; + +import { IAirdropModule } from "../interfaces/IAirdropModule.sol"; +import { IClaimAdapter } from "../interfaces/IClaimAdapter.sol"; +import { IClaimModule } from "../interfaces/IClaimModule.sol"; +import { IIntegrationRegistry } from "../interfaces/IIntegrationRegistry.sol"; +import { ISetToken } from "../interfaces/ISetToken.sol"; +import { PreciseUnitMath } from "../lib/PreciseUnitMath.sol"; + +import { BaseGlobalExtension } from "../lib/BaseGlobalExtension.sol"; +import { IDelegatedManager } from "../interfaces/IDelegatedManager.sol"; +import { IManagerCore } from "../interfaces/IManagerCore.sol"; + +/** + * @title GlobalClaimExtension + * @author Set Protocol + * + * Smart contract global extension which provides DelegatedManager owner the ability to perform administrative tasks on the AirdropModule + * and the ClaimModule and the DelegatedManager operator(s) the ability to + * - absorb tokens sent to the SetToken into the token's positions + * - claim tokens from external protocols given to a Set as part of participating in incentivized activities of other protocols + * and absorb them into the SetToken's positions in a single transaction + */ +contract GlobalClaimExtension is BaseGlobalExtension { + using PreciseUnitMath for uint256; + using SafeMath for uint256; + + /* ============ Events ============ */ + + event ClaimExtensionInitialized( + address indexed _setToken, + address indexed _delegatedManager + ); + + event FeesDistributed( + address _setToken, // Address of SetToken which generated the airdrop fees + address _token, // Address of the token to distribute + address indexed _ownerFeeRecipient, // Address which receives the owner's take of the fees + address indexed _methodologist, // Address of methodologist + uint256 _ownerTake, // Amount of _token distributed to owner + uint256 _methodologistTake // Amount of _token distributed to methodologist + ); + + /* ============ Modifiers ============ */ + + /** + * Throws if useAssetAllowList is true and one of the assets is not on the asset allow list + */ + modifier onlyAllowedAssets(ISetToken _setToken, address[] memory _assets) { + _validateAllowedAssets(_setToken, _assets); + _; + } + + /** + * Throws if anyoneAbsorb on the AirdropModule is false and caller is not the operator + */ + modifier onlyValidAbsorbCaller(ISetToken _setToken) { + require(_isValidAbsorbCaller(_setToken), "Must be valid AirdropModule absorb caller"); + _; + } + + /** + * Throws if caller is not the operator and either anyoneAbsorb on the AirdropModule or anyoneClaim on the ClaimModule is false + */ + modifier onlyValidClaimAndAbsorbCaller(ISetToken _setToken) { + require(_isValidClaimAndAbsorbCaller(_setToken), "Must be valid AirdropModule absorb and ClaimModule claim caller"); + _; + } + + /* ============ State Variables ============ */ + + // Instance of AirdropModule + IAirdropModule public immutable airdropModule; + + // Instance of ClaimModule + IClaimModule public immutable claimModule; + + // Instance of IntegrationRegistry + IIntegrationRegistry public immutable integrationRegistry; + + /* ============ Constructor ============ */ + + /** + * Instantiate with ManagerCore, AirdropModule, ClaimModule, and Controller addresses. + * + * @param _managerCore Address of ManagerCore contract + * @param _airdropModule Address of AirdropModule contract + * @param _claimModule Address of ClaimModule contract + * @param _integrationRegistry Address of IntegrationRegistry contract + */ + constructor( + IManagerCore _managerCore, + IAirdropModule _airdropModule, + IClaimModule _claimModule, + IIntegrationRegistry _integrationRegistry + ) + public + BaseGlobalExtension(_managerCore) + { + airdropModule = _airdropModule; + claimModule = _claimModule; + integrationRegistry = _integrationRegistry; + } + + /* ============ External Functions ============ */ + + /** + * ANYONE CALLABLE: Distributes airdrop fees accrued to the DelegatedManager. Calculates fees for + * owner and methodologist, and sends to owner fee recipient and methodologist respectively. + * + * @param _setToken Address of SetToken + * @param _token Address of token to distribute + */ + function distributeFees( + ISetToken _setToken, + IERC20 _token + ) + public + { + IDelegatedManager delegatedManager = _manager(_setToken); + + uint256 totalFees = _token.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(_token), ownerFeeRecipient, ownerTake); + } + + if (methodologistTake > 0) { + delegatedManager.transferTokens(address(_token), methodologist, methodologistTake); + } + + emit FeesDistributed( + address(_setToken), + address(_token), + ownerFeeRecipient, + methodologist, + ownerTake, + methodologistTake + ); + } + + /** + * ONLY OWNER: Initializes AirdropModule on the SetToken associated with the DelegatedManager. + * + * @param _delegatedManager Instance of the DelegatedManager to initialize the AirdropModule for + * @param _airdropSettings Struct of airdrop setting for Set including accepted airdrops, feeRecipient, + * airdropFee, and indicating if anyone can call an absorb + */ + function initializeAirdropModule( + IDelegatedManager _delegatedManager, + IAirdropModule.AirdropSettings memory _airdropSettings + ) + external + onlyOwnerAndValidManager(_delegatedManager) + { + _initializeAirdropModule(_delegatedManager.setToken(), _delegatedManager, _airdropSettings); + } + + /** + * ONLY OWNER: Initializes ClaimModule on the SetToken associated with the DelegatedManager. + * + * @param _delegatedManager Instance of the DelegatedManager to initialize the ClaimModule for + * @param _anyoneClaim Boolean indicating if anyone can claim or just manager + * @param _rewardPools Addresses of rewardPools that identifies the contract governing claims. Maps to same index integrationNames + * @param _integrationNames Human-readable names matching adapter used to collect claim on pool. Maps to same index in rewardPools + */ + function initializeClaimModule( + IDelegatedManager _delegatedManager, + bool _anyoneClaim, + address[] calldata _rewardPools, + string[] calldata _integrationNames + ) + external + onlyOwnerAndValidManager(_delegatedManager) + { + _initializeClaimModule(_delegatedManager.setToken(), _delegatedManager, _anyoneClaim, _rewardPools, _integrationNames); + } + + /** + * ONLY OWNER: Initializes ClaimExtension to the DelegatedManager. + * + * @param _delegatedManager Instance of the DelegatedManager to initialize + */ + function initializeExtension(IDelegatedManager _delegatedManager) external onlyOwnerAndValidManager(_delegatedManager) { + ISetToken setToken = _delegatedManager.setToken(); + + _initializeExtension(setToken, _delegatedManager); + + emit ClaimExtensionInitialized(address(setToken), address(_delegatedManager)); + } + + /** + * ONLY OWNER: Initializes ClaimExtension to the DelegatedManager and AirdropModule and ClaimModule to the SetToken + * + * @param _delegatedManager Instance of the DelegatedManager to initialize + * @param _airdropSettings Struct of airdrop setting for Set including accepted airdrops, feeRecipient, + * airdropFee, and indicating if anyone can call an absorb + * @param _anyoneClaim Boolean indicating if anyone can claim or just manager + * @param _rewardPools Addresses of rewardPools that identifies the contract governing claims. Maps to same index integrationNames + * @param _integrationNames Human-readable names matching adapter used to collect claim on pool. Maps to same index in rewardPools + */ + function initializeModulesAndExtension( + IDelegatedManager _delegatedManager, + IAirdropModule.AirdropSettings memory _airdropSettings, + bool _anyoneClaim, + address[] calldata _rewardPools, + string[] calldata _integrationNames + ) + external + onlyOwnerAndValidManager(_delegatedManager) + { + ISetToken setToken = _delegatedManager.setToken(); + + _initializeExtension(setToken, _delegatedManager); + _initializeAirdropModule(_delegatedManager.setToken(), _delegatedManager, _airdropSettings); + _initializeClaimModule(_delegatedManager.setToken(), _delegatedManager, _anyoneClaim, _rewardPools, _integrationNames); + + emit ClaimExtensionInitialized(address(setToken), address(_delegatedManager)); + } + + /** + * ONLY MANAGER: Remove an existing SetToken and DelegatedManager tracked by the ClaimExtension + */ + function removeExtension() external override { + IDelegatedManager delegatedManager = IDelegatedManager(msg.sender); + ISetToken setToken = delegatedManager.setToken(); + + _removeExtension(setToken, delegatedManager); + } + + /** + * ONLY VALID ABSORB CALLER: Absorb passed tokens into respective positions. If airdropFee defined, send portion to feeRecipient + * and portion to protocol feeRecipient address. Callable only by operator unless set anyoneAbsorb is true on the AirdropModule. + * + * @param _setToken Address of SetToken + * @param _tokens Array of tokens to absorb + */ + function batchAbsorb( + ISetToken _setToken, + address[] memory _tokens + ) + external + onlyValidAbsorbCaller(_setToken) + onlyAllowedAssets(_setToken, _tokens) + { + _batchAbsorb(_setToken, _tokens); + } + + /** + * ONLY VALID ABSORB CALLER: Absorb specified token into position. If airdropFee defined, send portion to feeRecipient and portion to + * protocol feeRecipient address. Callable only by operator unless anyoneAbsorb is true on the AirdropModule. + * + * @param _setToken Address of SetToken + * @param _token Address of token to absorb + */ + function absorb( + ISetToken _setToken, + IERC20 _token + ) + external + onlyValidAbsorbCaller(_setToken) + onlyAllowedAsset(_setToken, address(_token)) + { + _absorb(_setToken, _token); + } + + /** + * ONLY OWNER: Adds new tokens to be added to positions when absorb is called. + * + * @param _setToken Address of SetToken + * @param _airdrop Component to add to airdrop list + */ + function addAirdrop( + ISetToken _setToken, + IERC20 _airdrop + ) + external + onlyOwner(_setToken) + { + bytes memory callData = abi.encodeWithSelector( + IAirdropModule.addAirdrop.selector, + _setToken, + _airdrop + ); + _invokeManager(_manager(_setToken), address(airdropModule), callData); + } + + /** + * ONLY OWNER: Removes tokens from list to be absorbed. + * + * @param _setToken Address of SetToken + * @param _airdrop Component to remove from airdrop list + */ + function removeAirdrop( + ISetToken _setToken, + IERC20 _airdrop + ) + external + onlyOwner(_setToken) + { + bytes memory callData = abi.encodeWithSelector( + IAirdropModule.removeAirdrop.selector, + _setToken, + _airdrop + ); + _invokeManager(_manager(_setToken), address(airdropModule), callData); + } + + /** + * ONLY OWNER: Update whether manager allows other addresses to call absorb. + * + * @param _setToken Address of SetToken + */ + function updateAnyoneAbsorb( + ISetToken _setToken, + bool _anyoneAbsorb + ) + external + onlyOwner(_setToken) + { + bytes memory callData = abi.encodeWithSelector( + IAirdropModule.updateAnyoneAbsorb.selector, + _setToken, + _anyoneAbsorb + ); + _invokeManager(_manager(_setToken), address(airdropModule), callData); + } + + /** + * ONLY OWNER: Update address AirdropModule manager fees are sent to. + * + * @param _setToken Address of SetToken + * @param _newFeeRecipient Address of new fee recipient + */ + function updateAirdropFeeRecipient( + ISetToken _setToken, + address _newFeeRecipient + ) + external + onlyOwner(_setToken) + { + bytes memory callData = abi.encodeWithSelector( + IAirdropModule.updateFeeRecipient.selector, + _setToken, + _newFeeRecipient + ); + _invokeManager(_manager(_setToken), address(airdropModule), callData); + } + + /** + * ONLY OWNER: Update airdrop fee percentage. + * + * @param _setToken Address of SetToken + * @param _newFee Percentage, in preciseUnits, of new airdrop fee (1e16 = 1%) + */ + function updateAirdropFee( + ISetToken _setToken, + uint256 _newFee + ) + external + onlyOwner(_setToken) + { + bytes memory callData = abi.encodeWithSelector( + IAirdropModule.updateAirdropFee.selector, + _setToken, + _newFee + ); + _invokeManager(_manager(_setToken), address(airdropModule), callData); + } + + /** + * ONLY VALID CLAIM AND ABSORB CALLER: Claim the rewards available on the rewardPool for the specified claim integration and absorb + * the reward token into position. If airdropFee defined, send portion to feeRecipient and portion to protocol feeRecipient address. + * Callable only by operator unless anyoneAbsorb on the AirdropModule and anyoneClaim on the ClaimModule are true. + * + * @param _setToken Address of SetToken + * @param _rewardPool Address of the rewardPool that identifies the contract governing claims + * @param _integrationName ID of claim module integration (mapping on integration registry) + */ + function claimAndAbsorb( + ISetToken _setToken, + address _rewardPool, + string calldata _integrationName + ) + external + onlyValidClaimAndAbsorbCaller(_setToken) + { + IERC20 rewardsToken = _getAndValidateRewardsToken(_setToken, _rewardPool, _integrationName); + + _claim(_setToken, _rewardPool, _integrationName); + + _absorb(_setToken, rewardsToken); + } + + /** + * ONLY VALID CLAIM AND ABSORB CALLER: Claims rewards on all the passed rewardPool/claim integration pairs and absorb the reward tokens + * into positions. If airdropFee defined, send portion of each reward token to feeRecipient and a portion to protocol feeRecipient address. + * Callable only by operator unless anyoneAbsorb on the AirdropModule and anyoneClaim on the ClaimModule are true. + * + * @param _setToken Address of SetToken + * @param _rewardPools Addresses of rewardPools that identifies the contract governing claims. Maps to same index integrationNames + * @param _integrationNames Human-readable names matching adapter used to collect claim on pool. Maps to same index in rewardPools + */ + function batchClaimAndAbsorb( + ISetToken _setToken, + address[] calldata _rewardPools, + string[] calldata _integrationNames + ) + external + onlyValidClaimAndAbsorbCaller(_setToken) + { + address[] storage rewardsTokens; + uint256 numPools = _rewardPools.length; + for (uint256 i = 0; i < numPools; i++) { + IERC20 token = _getAndValidateRewardsToken(_setToken, _rewardPools[i], _integrationNames[i]); + rewardsTokens.push(address(token)); + } + + _batchClaim(_setToken, _rewardPools, _integrationNames); + + _batchAbsorb(_setToken, rewardsTokens); + } + + /** + * ONLY OWNER: Update whether manager allows other addresses to call claim. + * + * @param _setToken Address of SetToken + */ + function updateAnyoneClaim( + ISetToken _setToken, + bool _anyoneClaim + ) + external + onlyOwner(_setToken) + { + bytes memory callData = abi.encodeWithSelector( + IClaimModule.updateAnyoneClaim.selector, + _setToken, + _anyoneClaim + ); + _invokeManager(_manager(_setToken), address(claimModule), callData); + } + + /** + * ONLY OWNER: Adds a new claim integration for an existent rewardPool. If rewardPool doesn't have existing + * claims then rewardPool is added to rewardPoolList. The claim integration is associated to an adapter that + * provides the functionality to claim the rewards for a specific token. + * + * @param _setToken Address of SetToken + * @param _rewardPool Address of the rewardPool that identifies the contract governing claims + * @param _integrationName ID of claim module integration (mapping on integration registry) + */ + function addClaim( + ISetToken _setToken, + address _rewardPool, + string calldata _integrationName + ) + external + onlyOwner(_setToken) + { + bytes memory callData = abi.encodeWithSelector( + IClaimModule.addClaim.selector, + _setToken, + _rewardPool, + _integrationName + ); + _invokeManager(_manager(_setToken), address(claimModule), callData); + } + + /** + * ONLY OWNER: Adds a new rewardPool to the list to perform claims for the SetToken indicating the list of + * claim integrations. Each claim integration is associated to an adapter that provides the functionality to claim + * the rewards for a specific token. + * + * @param _setToken Address of SetToken + * @param _rewardPools Addresses of rewardPools that identifies the contract governing claims. Maps to same + * index integrationNames + * @param _integrationNames Human-readable names matching adapter used to collect claim on pool. Maps to same index + * in rewardPools + */ + function batchAddClaim( + ISetToken _setToken, + address[] calldata _rewardPools, + string[] calldata _integrationNames + ) + external + onlyOwner(_setToken) + { + bytes memory callData = abi.encodeWithSelector( + IClaimModule.batchAddClaim.selector, + _setToken, + _rewardPools, + _integrationNames + ); + _invokeManager(_manager(_setToken), address(claimModule), callData); + } + + /** + * ONLY OWNER: Removes a claim integration from an existent rewardPool. If no claim remains for reward pool then + * reward pool is removed from rewardPoolList. + * + * @param _setToken Address of SetToken + * @param _rewardPool Address of the rewardPool that identifies the contract governing claims + * @param _integrationName ID of claim module integration (mapping on integration registry) + */ + function removeClaim( + ISetToken _setToken, + address _rewardPool, + string calldata _integrationName + ) + external + onlyOwner(_setToken) + { + bytes memory callData = abi.encodeWithSelector( + IClaimModule.removeClaim.selector, + _setToken, + _rewardPool, + _integrationName + ); + _invokeManager(_manager(_setToken), address(claimModule), callData); + } + + /** + * ONLY OWNER: Batch removes claims from SetToken's settings. + * + * @param _setToken Address of SetToken + * @param _rewardPools Addresses of rewardPools that identifies the contract governing claims. Maps to same index + * integrationNames + * @param _integrationNames Human-readable names matching adapter used to collect claim on pool. Maps to same index in + * rewardPools + */ + function batchRemoveClaim( + ISetToken _setToken, + address[] calldata _rewardPools, + string[] calldata _integrationNames + ) + external + onlyOwner(_setToken) + { + bytes memory callData = abi.encodeWithSelector( + IClaimModule.batchRemoveClaim.selector, + _setToken, + _rewardPools, + _integrationNames + ); + _invokeManager(_manager(_setToken), address(claimModule), callData); + } + + + /* ============ Internal Functions ============ */ + + /** + * Internal function to initialize AirdropModule 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 AirdropModule for + * @param _airdropSettings Struct of airdrop setting for Set including accepted airdrops, feeRecipient, + * airdropFee, and indicating if anyone can call an absorb + */ + function _initializeAirdropModule( + ISetToken _setToken, + IDelegatedManager _delegatedManager, + IAirdropModule.AirdropSettings memory _airdropSettings + ) + internal + { + bytes memory callData = abi.encodeWithSelector( + IAirdropModule.initialize.selector, + _setToken, + _airdropSettings + ); + _invokeManager(_delegatedManager, address(airdropModule), callData); + } + + /** + * Internal function to initialize ClaimModule 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 ClaimModule for + * @param _anyoneClaim Boolean indicating if anyone can claim or just manager + * @param _rewardPools Addresses of rewardPools that identifies the contract governing claims. Maps to same index integrationNames + * @param _integrationNames Human-readable names matching adapter used to collect claim on pool. Maps to same index in rewardPools + */ + function _initializeClaimModule( + ISetToken _setToken, + IDelegatedManager _delegatedManager, + bool _anyoneClaim, + address[] calldata _rewardPools, + string[] calldata _integrationNames + ) + internal + { + bytes memory callData = abi.encodeWithSelector( + IClaimModule.initialize.selector, + _setToken, + _anyoneClaim, + _rewardPools, + _integrationNames + ); + _invokeManager(_delegatedManager, address(claimModule), callData); + } + + /** + * Must have all assets on asset allow list or useAssetAllowlist to be false + */ + function _validateAllowedAssets(ISetToken _setToken, address[] memory _assets) internal view { + IDelegatedManager manager = _manager(_setToken); + if (manager.useAssetAllowlist()) { + uint256 assetsLength = _assets.length; + for (uint i = 0; i < assetsLength; i++) { + require(manager.assetAllowlist(_assets[i]), "Must be allowed asset"); + } + } + } + + /** + * AirdropModule anyoneAbsorb setting must be true or must be operator + * + * @param _setToken Instance of the SetToken corresponding to the DelegatedManager + */ + function _isValidAbsorbCaller(ISetToken _setToken) internal view returns(bool) { + return airdropModule.airdropSettings(_setToken).anyoneAbsorb || _manager(_setToken).operatorAllowlist(msg.sender); + } + + /** + * Must be operator or must have both AirdropModule anyoneAbsorb and ClaimModule anyoneClaim + * + * @param _setToken Instance of the SetToken corresponding to the DelegatedManager + */ + function _isValidClaimAndAbsorbCaller(ISetToken _setToken) internal view returns(bool) { + return ( + (claimModule.anyoneClaim(_setToken) && airdropModule.airdropSettings(_setToken).anyoneAbsorb) + || _manager(_setToken).operatorAllowlist(msg.sender) + ); + } + + /** + * Absorb specified token into position. If airdropFee defined, send portion to feeRecipient and portion to protocol feeRecipient address. + * + * @param _setToken Address of SetToken + * @param _token Address of token to absorb + */ + function _absorb(ISetToken _setToken, IERC20 _token) internal { + bytes memory callData = abi.encodeWithSelector( + IAirdropModule.absorb.selector, + _setToken, + _token + ); + _invokeManager(_manager(_setToken), address(airdropModule), callData); + } + + /** + * Absorb passed tokens into respective positions. If airdropFee defined, send portion to feeRecipient and portion to protocol feeRecipient address. + * + * @param _setToken Address of SetToken + * @param _tokens Array of tokens to absorb + */ + function _batchAbsorb(ISetToken _setToken, address[] memory _tokens) internal { + bytes memory callData = abi.encodeWithSelector( + IAirdropModule.batchAbsorb.selector, + _setToken, + _tokens + ); + _invokeManager(_manager(_setToken), address(airdropModule), callData); + } + + /** + * Claim the rewards available on the rewardPool for the specified claim integration and absorb the reward token into position. + * + * @param _setToken Address of SetToken + * @param _rewardPool Address of the rewardPool that identifies the contract governing claims + * @param _integrationName ID of claim module integration (mapping on integration registry) + */ + function _claim(ISetToken _setToken, address _rewardPool, string calldata _integrationName) internal { + bytes memory callData = abi.encodeWithSelector( + IClaimModule.claim.selector, + _setToken, + _rewardPool, + _integrationName + ); + _invokeManager(_manager(_setToken), address(claimModule), callData); + } + + /** + * Claims rewards on all the passed rewardPool/claim integration pairs and absorb the reward tokens into positions. + * + * @param _setToken Address of SetToken + * @param _rewardPools Addresses of rewardPools that identifies the contract governing claims. Maps to same index integrationNames + * @param _integrationNames Human-readable names matching adapter used to collect claim on pool. Maps to same index in rewardPools + */ + function _batchClaim(ISetToken _setToken, address[] calldata _rewardPools, string[] calldata _integrationNames) internal { + bytes memory callData = abi.encodeWithSelector( + IClaimModule.batchClaim.selector, + _setToken, + _rewardPools, + _integrationNames + ); + _invokeManager(_manager(_setToken), address(claimModule), callData); + } + + /** + * Get the rewards token from the rewardPool and integrationName and check if it is an allowed asset on the DelegatedManager + * + * @param _setToken Address of SetToken + * @param _rewardPool Address of the rewardPool that identifies the contract governing claims + * @param _integrationName ID of claim module integration (mapping on integration registry) + */ + function _getAndValidateRewardsToken(ISetToken _setToken, address _rewardPool, string calldata _integrationName) internal view returns(IERC20) { + IClaimAdapter adapter = IClaimAdapter(integrationRegistry.getIntegrationAdapter(address(claimModule), _integrationName)); + IERC20 rewardsToken = adapter.getTokenAddress(_rewardPool); + require(_manager(_setToken).isAllowedAsset(address(rewardsToken)), "Must be allowed asset"); + return rewardsToken; + } +} diff --git a/contracts/global-extensions/GlobalIssuanceExtension.sol b/contracts/global-extensions/GlobalIssuanceExtension.sol new file mode 100644 index 00000000..cdef5364 --- /dev/null +++ b/contracts/global-extensions/GlobalIssuanceExtension.sol @@ -0,0 +1,284 @@ +/* + 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 "../interfaces/ISetToken.sol"; +import { IIssuanceModule } from "../interfaces/IIssuanceModule.sol"; +import { PreciseUnitMath } from "../lib/PreciseUnitMath.sol"; + +import { BaseGlobalExtension } from "../lib/BaseGlobalExtension.sol"; +import { IDelegatedManager } from "../interfaces/IDelegatedManager.sol"; +import { IManagerCore } from "../interfaces/IManagerCore.sol"; + +/** + * @title GlobalIssuanceExtension + * @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. + * + * Notes + * - the fee split is set on the Delegated Manager contract + * - when fees distributed via this contract will be inclusive of all fee types that have already been accrued + */ +contract GlobalIssuanceExtension 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); + + emit 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 + ); + + emit IssuanceExtensionInitialized(address(setToken), address(_delegatedManager)); + } + + /** + * ONLY MANAGER: Remove an existing SetToken and DelegatedManager tracked by the IssuanceExtension + */ + function removeExtension() external override { + IDelegatedManager delegatedManager = IDelegatedManager(msg.sender); + ISetToken setToken = delegatedManager.setToken(); + + _removeExtension(setToken, delegatedManager); + } + + /** + * 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); + } +} diff --git a/contracts/global-extensions/GlobalStreamingFeeSplitExtension.sol b/contracts/global-extensions/GlobalStreamingFeeSplitExtension.sol new file mode 100644 index 00000000..4263b725 --- /dev/null +++ b/contracts/global-extensions/GlobalStreamingFeeSplitExtension.sol @@ -0,0 +1,230 @@ +/* + 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 "../interfaces/ISetToken.sol"; +import { IStreamingFeeModule } from "../interfaces/IStreamingFeeModule.sol"; +import { PreciseUnitMath } from "../lib/PreciseUnitMath.sol"; + +import { BaseGlobalExtension } from "../lib/BaseGlobalExtension.sol"; +import { IDelegatedManager } from "../interfaces/IDelegatedManager.sol"; +import { IManagerCore } from "../interfaces/IManagerCore.sol"; + +/** + * @title GlobalStreamingFeeSplitExtension + * @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. + * + * Notes + * - the fee split is set on the Delegated Manager contract + * - when fees distributed via this contract will be inclusive of all fee types + */ +contract GlobalStreamingFeeSplitExtension 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); + + emit 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); + + emit StreamingFeeSplitExtensionInitialized(address(setToken), address(_delegatedManager)); + } + + /** + * ONLY MANAGER: Remove an existing SetToken and DelegatedManager tracked by the StreamingFeeSplitExtension + */ + function removeExtension() external override { + IDelegatedManager delegatedManager = IDelegatedManager(msg.sender); + ISetToken setToken = delegatedManager.setToken(); + + _removeExtension(setToken, delegatedManager); + } + + /** + * 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); + } +} diff --git a/contracts/global-extensions/GlobalTradeExtension.sol b/contracts/global-extensions/GlobalTradeExtension.sol new file mode 100644 index 00000000..a4f01458 --- /dev/null +++ b/contracts/global-extensions/GlobalTradeExtension.sol @@ -0,0 +1,166 @@ +/* + 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 "../interfaces/ISetToken.sol"; +import { ITradeModule } from "../interfaces/ITradeModule.sol"; + +import { BaseGlobalExtension } from "../lib/BaseGlobalExtension.sol"; +import { IDelegatedManager } from "../interfaces/IDelegatedManager.sol"; +import { IManagerCore } from "../interfaces/IManagerCore.sol"; + +/** + * @title GlobalTradeExtension + * @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 GlobalTradeExtension 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); + + emit 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); + + emit TradeExtensionInitialized(address(setToken), address(_delegatedManager)); + } + + /** + * ONLY MANAGER: Remove an existing SetToken and DelegatedManager tracked by the TradeExtension + */ + function removeExtension() external override { + IDelegatedManager delegatedManager = IDelegatedManager(msg.sender); + ISetToken setToken = delegatedManager.setToken(); + + _removeExtension(setToken, delegatedManager); + } + + /** + * 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); + } +} diff --git a/contracts/global-extensions/GlobalWrapExtension.sol b/contracts/global-extensions/GlobalWrapExtension.sol new file mode 100644 index 00000000..67508f4f --- /dev/null +++ b/contracts/global-extensions/GlobalWrapExtension.sol @@ -0,0 +1,264 @@ +/* + 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 { ISetToken } from "../interfaces/ISetToken.sol"; +import { IWETH } from "../interfaces/IWETH.sol"; +import { IWrapModuleV2 } from "../interfaces/IWrapModuleV2.sol"; + +import { BaseGlobalExtension } from "../lib/BaseGlobalExtension.sol"; +import { IDelegatedManager } from "../interfaces/IDelegatedManager.sol"; +import { IManagerCore } from "../interfaces/IManagerCore.sol"; + +/** + * @title GlobalWrapExtension + * @author Set Protocol + * + * Smart contract global extension which provides DelegatedManager operator(s) the ability to wrap ERC20 and Ether positions + * via third party protocols. + * + * Some examples of wrap actions include wrapping, DAI to cDAI (Compound) or Dai to aDai (AAVE). + */ +contract GlobalWrapExtension is BaseGlobalExtension { + + /* ============ Events ============ */ + + event WrapExtensionInitialized( + address indexed _setToken, + address indexed _delegatedManager + ); + + /* ============ State Variables ============ */ + + // Instance of WrapModuleV2 + IWrapModuleV2 public immutable wrapModule; + + /* ============ Constructor ============ */ + + /** + * Instantiate with ManagerCore address and WrapModuleV2 address. + * + * @param _managerCore Address of ManagerCore contract + * @param _wrapModule Address of WrapModuleV2 contract + */ + constructor( + IManagerCore _managerCore, + IWrapModuleV2 _wrapModule + ) + public + BaseGlobalExtension(_managerCore) + { + wrapModule = _wrapModule; + } + + /* ============ External Functions ============ */ + + /** + * ONLY OWNER: Initializes WrapModuleV2 on the SetToken associated with the DelegatedManager. + * + * @param _delegatedManager Instance of the DelegatedManager to initialize the WrapModuleV2 for + */ + function initializeModule(IDelegatedManager _delegatedManager) external onlyOwnerAndValidManager(_delegatedManager) { + _initializeModule(_delegatedManager.setToken(), _delegatedManager); + } + + /** + * ONLY OWNER: Initializes WrapExtension to the DelegatedManager. + * + * @param _delegatedManager Instance of the DelegatedManager to initialize + */ + function initializeExtension(IDelegatedManager _delegatedManager) external onlyOwnerAndValidManager(_delegatedManager) { + ISetToken setToken = _delegatedManager.setToken(); + + _initializeExtension(setToken, _delegatedManager); + + emit WrapExtensionInitialized(address(setToken), address(_delegatedManager)); + } + + /** + * ONLY OWNER: Initializes WrapExtension to the DelegatedManager and TradeModule to the SetToken + * + * @param _delegatedManager Instance of the DelegatedManager to initialize + */ + function initializeModuleAndExtension(IDelegatedManager _delegatedManager) external onlyOwnerAndValidManager(_delegatedManager){ + ISetToken setToken = _delegatedManager.setToken(); + + _initializeExtension(setToken, _delegatedManager); + _initializeModule(setToken, _delegatedManager); + + emit WrapExtensionInitialized(address(setToken), address(_delegatedManager)); + } + + /** + * ONLY MANAGER: Remove an existing SetToken and DelegatedManager tracked by the WrapExtension + */ + function removeExtension() external override { + IDelegatedManager delegatedManager = IDelegatedManager(msg.sender); + ISetToken setToken = delegatedManager.setToken(); + + _removeExtension(setToken, delegatedManager); + } + + /** + * ONLY OPERATOR: Instructs the SetToken to wrap an underlying asset into a wrappedToken via a specified adapter. + * + * @param _setToken Instance of the SetToken + * @param _underlyingToken Address of the component to be wrapped + * @param _wrappedToken Address of the desired wrapped token + * @param _underlyingUnits Quantity of underlying units in Position units + * @param _integrationName Name of wrap module integration (mapping on integration registry) + * @param _wrapData Arbitrary bytes to pass into the WrapV2Adapter + */ + function wrap( + ISetToken _setToken, + address _underlyingToken, + address _wrappedToken, + uint256 _underlyingUnits, + string calldata _integrationName, + bytes memory _wrapData + ) + external + onlyOperator(_setToken) + onlyAllowedAsset(_setToken, _wrappedToken) + { + bytes memory callData = abi.encodeWithSelector( + IWrapModuleV2.wrap.selector, + _setToken, + _underlyingToken, + _wrappedToken, + _underlyingUnits, + _integrationName, + _wrapData + ); + _invokeManager(_manager(_setToken), address(wrapModule), callData); + } + + /** + * ONLY OPERATOR: Instructs the SetToken to wrap Ether into a wrappedToken via a specified adapter. Since SetTokens + * only hold WETH, in order to support protocols that collateralize with Ether the SetToken's WETH must be unwrapped + * first before sending to the external protocol. + * + * @param _setToken Instance of the SetToken + * @param _wrappedToken Address of the desired wrapped token + * @param _underlyingUnits Quantity of underlying units in Position units + * @param _integrationName Name of wrap module integration (mapping on integration registry) + * @param _wrapData Arbitrary bytes to pass into the WrapV2Adapter + */ + function wrapWithEther( + ISetToken _setToken, + address _wrappedToken, + uint256 _underlyingUnits, + string calldata _integrationName, + bytes memory _wrapData + ) + external + onlyOperator(_setToken) + onlyAllowedAsset(_setToken, _wrappedToken) + { + bytes memory callData = abi.encodeWithSelector( + IWrapModuleV2.wrapWithEther.selector, + _setToken, + _wrappedToken, + _underlyingUnits, + _integrationName, + _wrapData + ); + _invokeManager(_manager(_setToken), address(wrapModule), callData); + } + + /** + * ONLY OPERATOR: Instructs the SetToken to unwrap a wrapped asset into its underlying via a specified adapter. + * + * @param _setToken Instance of the SetToken + * @param _underlyingToken Address of the underlying asset + * @param _wrappedToken Address of the component to be unwrapped + * @param _wrappedUnits Quantity of wrapped tokens in Position units + * @param _integrationName ID of wrap module integration (mapping on integration registry) + * @param _unwrapData Arbitrary bytes to pass into the WrapV2Adapter + */ + function unwrap( + ISetToken _setToken, + address _underlyingToken, + address _wrappedToken, + uint256 _wrappedUnits, + string calldata _integrationName, + bytes memory _unwrapData + ) + external + onlyOperator(_setToken) + onlyAllowedAsset(_setToken, _underlyingToken) + { + bytes memory callData = abi.encodeWithSelector( + IWrapModuleV2.unwrap.selector, + _setToken, + _underlyingToken, + _wrappedToken, + _wrappedUnits, + _integrationName, + _unwrapData + ); + _invokeManager(_manager(_setToken), address(wrapModule), callData); + } + + /** + * ONLY OPERATOR: Instructs the SetToken to unwrap a wrapped asset collateralized by Ether into Wrapped Ether. Since + * external protocol will send back Ether that Ether must be Wrapped into WETH in order to be accounted for by SetToken. + * + * @param _setToken Instance of the SetToken + * @param _wrappedToken Address of the component to be unwrapped + * @param _wrappedUnits Quantity of wrapped tokens in Position units + * @param _integrationName ID of wrap module integration (mapping on integration registry) + * @param _unwrapData Arbitrary bytes to pass into the WrapV2Adapter + */ + function unwrapWithEther( + ISetToken _setToken, + address _wrappedToken, + uint256 _wrappedUnits, + string calldata _integrationName, + bytes memory _unwrapData + ) + external + onlyOperator(_setToken) + onlyAllowedAsset(_setToken, address(wrapModule.weth())) + { + bytes memory callData = abi.encodeWithSelector( + IWrapModuleV2.unwrapWithEther.selector, + _setToken, + _wrappedToken, + _wrappedUnits, + _integrationName, + _unwrapData + ); + _invokeManager(_manager(_setToken), address(wrapModule), callData); + } + + /* ============ Internal Functions ============ */ + + /** + * Internal function to initialize WrapModuleV2 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 WrapModuleV2 for + */ + function _initializeModule(ISetToken _setToken, IDelegatedManager _delegatedManager) internal { + bytes memory callData = abi.encodeWithSelector(IWrapModuleV2.initialize.selector, _setToken); + _invokeManager(_delegatedManager, address(wrapModule), callData); + } +} diff --git a/contracts/interfaces/IClaimAdapter.sol b/contracts/interfaces/IClaimAdapter.sol new file mode 100644 index 00000000..14438ec8 --- /dev/null +++ b/contracts/interfaces/IClaimAdapter.sol @@ -0,0 +1,65 @@ +/* + Copyright 2020 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache License, Version 2.0 +*/ + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { ISetToken } from "./ISetToken.sol"; + +pragma solidity 0.6.10; + +/** + * @title IClaimAdapter + * @author Set Protocol + * + */ +interface IClaimAdapter { + + /** + * Generates the calldata for claiming tokens from the rewars pool + * + * @param _setToken the set token that is owed the tokens + * @param _rewardPool the rewards pool to claim from + * + * @return _subject the rewards pool to call + * @return _value the amount of ether to send in the call + * @return _calldata the calldata to use + */ + function getClaimCallData( + ISetToken _setToken, + address _rewardPool + ) external view returns(address _subject, uint256 _value, bytes memory _calldata); + + /** + * Gets the amount of unclaimed rewards + * + * @param _setToken the set token that is owed the tokens + * @param _rewardPool the rewards pool to check + * + * @return uint256 the amount of unclaimed rewards + */ + function getRewardsAmount(ISetToken _setToken, address _rewardPool) external view returns(uint256); + + /** + * Gets the rewards token + * + * @param _rewardPool the rewards pool to check + * + * @return IERC20 the reward token + */ + function getTokenAddress(address _rewardPool) external view returns(IERC20); +} diff --git a/contracts/interfaces/IClaimModule.sol b/contracts/interfaces/IClaimModule.sol new file mode 100644 index 00000000..f317d749 --- /dev/null +++ b/contracts/interfaces/IClaimModule.sol @@ -0,0 +1,46 @@ +/* + 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 { ISetToken } from "./ISetToken.sol"; + +interface IClaimModule { + function initialize( + ISetToken _setToken, + bool _anyoneClaim, + address[] calldata _rewardPools, + string[] calldata _integrationNames + ) external; + + function anyoneClaim(ISetToken _setToken) external view returns(bool); + function claim(ISetToken _setToken, address _rewardPool, string calldata _integrationName) external; + function batchClaim(ISetToken _setToken, address[] calldata _rewardPools, string[] calldata _integrationNames) external; + function updateAnyoneClaim(ISetToken _setToken, bool _anyoneClaim) external; + function addClaim(ISetToken _setToken, address _rewardPool, string calldata _integrationName) external; + function batchAddClaim(ISetToken _setToken, address[] calldata _rewardPools, string[] calldata _integrationNames) external; + function removeClaim(ISetToken _setToken, address _rewardPool, string calldata _integrationName) external; + function batchRemoveClaim(ISetToken _setToken, address[] calldata _rewardPools, string[] calldata _integrationNames) external; + function removeModule() external; + function getRewardPools(ISetToken _setToken) external returns(address[] memory); + function isRewardPool(ISetToken _setToken, address _rewardPool) external returns(bool); + function getRewardPoolClaims(ISetToken _setToken, address _rewardPool) external returns(address[] memory); + function isRewardPoolClaim(ISetToken _setToken, address _rewardPool, string calldata _integrationName) external returns (bool); + function getRewards(ISetToken _setToken, address _rewardPool, string calldata _integrationName) external returns (uint256); +} diff --git a/contracts/interfaces/IDelegatedManager.sol b/contracts/interfaces/IDelegatedManager.sol new file mode 100644 index 00000000..ff2add5e --- /dev/null +++ b/contracts/interfaces/IDelegatedManager.sol @@ -0,0 +1,53 @@ +/* + 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; +pragma experimental "ABIEncoderV2"; + +import { ISetToken } from "./ISetToken.sol"; + +interface IDelegatedManager { + function interactManager(address _module, bytes calldata _encoded) external; + + function initializeExtension() external; + + function transferTokens(address _token, address _destination, uint256 _amount) external; + + function updateOwnerFeeSplit(uint256 _newFeeSplit) external; + + function updateOwnerFeeRecipient(address _newFeeRecipient) external; + + function setMethodologist(address _newMethodologist) external; + + function transferOwnership(address _owner) external; + + function setToken() external view returns(ISetToken); + function owner() external view returns(address); + function methodologist() external view returns(address); + function operatorAllowlist(address _operator) external view returns(bool); + function assetAllowlist(address _asset) external view returns(bool); + function useAssetAllowlist() external view returns(bool); + function isAllowedAsset(address _asset) external view returns(bool); + function isPendingExtension(address _extension) external view returns(bool); + function isInitializedExtension(address _extension) external view returns(bool); + function getExtensions() external view returns(address[] memory); + function getOperators() external view returns(address[] memory); + function getAllowedAssets() external view returns(address[] memory); + function ownerFeeRecipient() external view returns(address); + function ownerFeeSplit() external view returns(uint256); +} diff --git a/contracts/interfaces/IGlobalExtension.sol b/contracts/interfaces/IGlobalExtension.sol new file mode 100644 index 00000000..4e40bcb2 --- /dev/null +++ b/contracts/interfaces/IGlobalExtension.sol @@ -0,0 +1,24 @@ +/* + 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; +pragma experimental "ABIEncoderV2"; + +interface IGlobalExtension { + function removeExtension() external; +} diff --git a/contracts/interfaces/IManagerCore.sol b/contracts/interfaces/IManagerCore.sol new file mode 100644 index 00000000..606a0443 --- /dev/null +++ b/contracts/interfaces/IManagerCore.sol @@ -0,0 +1,26 @@ +/* + 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 isExtension(address _extension) external view returns(bool); + function isFactory(address _factory) external view returns(bool); + function isManager(address _manager) external view returns(bool); + function owner() external view returns(address); +} diff --git a/contracts/interfaces/IModule.sol b/contracts/interfaces/IModule.sol new file mode 100644 index 00000000..0dd22115 --- /dev/null +++ b/contracts/interfaces/IModule.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; + + +/** + * @title IModule + * @author Set Protocol + * + * Interface for interacting with Modules. + */ +interface IModule { + /** + * Called by a SetToken to notify that this module was removed from the Set token. Any logic can be included + * in case checks need to be made or state needs to be cleared. + */ + function removeModule() external; +} diff --git a/contracts/interfaces/IPriceOracle.sol b/contracts/interfaces/IPriceOracle.sol new file mode 100644 index 00000000..614d6313 --- /dev/null +++ b/contracts/interfaces/IPriceOracle.sol @@ -0,0 +1,32 @@ +/* + 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; + +/** + * @title IPriceOracle + * @author Set Protocol + * + * Interface for interacting with PriceOracle + */ +interface IPriceOracle { + + /* ============ Functions ============ */ + + function getPrice(address _assetOne, address _assetTwo) external view returns (uint256); + function masterQuoteAsset() external view returns (address); +} diff --git a/contracts/interfaces/ISetTokenCreator.sol b/contracts/interfaces/ISetTokenCreator.sol new file mode 100644 index 00000000..9465d1e7 --- /dev/null +++ b/contracts/interfaces/ISetTokenCreator.sol @@ -0,0 +1,32 @@ +/* + 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 ISetTokenCreator { + function create( + address[] memory _components, + int256[] memory _units, + address[] memory _modules, + address _manager, + string memory _name, + string memory _symbol + ) + external + returns (address); +} diff --git a/contracts/interfaces/ISetValuer.sol b/contracts/interfaces/ISetValuer.sol new file mode 100644 index 00000000..3d11140d --- /dev/null +++ b/contracts/interfaces/ISetValuer.sol @@ -0,0 +1,24 @@ +/* + 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; + +import { ISetToken } from "../interfaces/ISetToken.sol"; + +interface ISetValuer { + function calculateSetTokenValuation(ISetToken _setToken, address _quoteAsset) external view returns (uint256); +} diff --git a/contracts/interfaces/IWrapModuleV2.sol b/contracts/interfaces/IWrapModuleV2.sol index bb69a38e..a50b70ea 100644 --- a/contracts/interfaces/IWrapModuleV2.sol +++ b/contracts/interfaces/IWrapModuleV2.sol @@ -1,5 +1,5 @@ /* - Copyright 2021 Index Coop. + 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. @@ -16,11 +16,14 @@ SPDX-License-Identifier: Apache License, Version 2.0 */ -import { ISetToken } from "./ISetToken.sol"; - pragma solidity 0.6.10; +pragma experimental "ABIEncoderV2"; + +import { ISetToken } from "./ISetToken.sol"; +import { IWETH } from "./IWETH.sol"; interface IWrapModuleV2 { + function weth() external view returns(IWETH); function initialize(ISetToken _setToken) external; @@ -57,4 +60,4 @@ interface IWrapModuleV2 { string calldata _integrationName, bytes memory _unwrapData ) external; -} \ No newline at end of file +} diff --git a/contracts/lib/BaseGlobalExtension.sol b/contracts/lib/BaseGlobalExtension.sol new file mode 100644 index 00000000..33786d9a --- /dev/null +++ b/contracts/lib/BaseGlobalExtension.sol @@ -0,0 +1,157 @@ +/* + 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 { AddressArrayUtils } from "../lib/AddressArrayUtils.sol"; +import { ISetToken } from "../interfaces/ISetToken.sol"; + +import { IDelegatedManager } from "../interfaces/IDelegatedManager.sol"; +import { IManagerCore } from "../interfaces/IManagerCore.sol"; + +/** + * @title BaseGlobalExtension + * @author Set Protocol + * + * Abstract class that houses common global extension-related functions. Global extensions must + * also have their own initializeExtension function (not included here because interfaces will vary). + */ +abstract contract BaseGlobalExtension { + using AddressArrayUtils for address[]; + + /* ============ Events ============ */ + + event ExtensionRemoved( + address indexed _setToken, + address indexed _delegatedManager + ); + + /* ============ State Variables ============ */ + + // Address of the ManagerCore + IManagerCore public immutable managerCore; + + // Mapping from Set Token to DelegatedManager + mapping(ISetToken => IDelegatedManager) public setManagers; + + /* ============ Modifiers ============ */ + + /** + * Throws if the sender is not the SetToken manager contract owner + */ + modifier onlyOwner(ISetToken _setToken) { + require(msg.sender == _manager(_setToken).owner(), "Must be owner"); + _; + } + + /** + * Throws if the sender is not the SetToken methodologist + */ + modifier onlyMethodologist(ISetToken _setToken) { + require(msg.sender == _manager(_setToken).methodologist(), "Must be methodologist"); + _; + } + + /** + * Throws if the sender is not a SetToken operator + */ + modifier onlyOperator(ISetToken _setToken) { + require(_manager(_setToken).operatorAllowlist(msg.sender), "Must be approved operator"); + _; + } + + /** + * Throws if the sender is not the SetToken manager contract owner or if the manager is not enabled on the ManagerCore + */ + modifier onlyOwnerAndValidManager(IDelegatedManager _delegatedManager) { + require(msg.sender == _delegatedManager.owner(), "Must be owner"); + require(managerCore.isManager(address(_delegatedManager)), "Must be ManagerCore-enabled manager"); + _; + } + + /** + * Throws if asset is not allowed to be held by the Set + */ + modifier onlyAllowedAsset(ISetToken _setToken, address _asset) { + require(_manager(_setToken).isAllowedAsset(_asset), "Must be allowed asset"); + _; + } + + /* ============ 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() external virtual; + + /* ============ Internal Functions ============ */ + + /** + * Invoke call from manager + * + * @param _delegatedManager Manager to interact with + * @param _module Module to interact with + * @param _encoded Encoded byte data + */ + function _invokeManager(IDelegatedManager _delegatedManager, address _module, bytes memory _encoded) internal { + _delegatedManager.interactManager(_module, _encoded); + } + + /** + * Internal function to grab manager of passed SetToken from extensions data structure. + * + * @param _setToken SetToken who's manager is needed + */ + 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(ISetToken _setToken, IDelegatedManager _delegatedManager) internal { + require(msg.sender == address(_manager(_setToken)), "Must be Manager"); + + delete setManagers[_setToken]; + + emit ExtensionRemoved(address(_setToken), address(_delegatedManager)); + } +} diff --git a/contracts/lib/ExplicitERC20.sol b/contracts/lib/ExplicitERC20.sol new file mode 100644 index 00000000..7684e1ae --- /dev/null +++ b/contracts/lib/ExplicitERC20.sol @@ -0,0 +1,71 @@ +/* + 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; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; + +/** + * @title ExplicitERC20 + * @author Set Protocol + * + * Utility functions for ERC20 transfers that require the explicit amount to be transferred. + */ +library ExplicitERC20 { + using SafeMath for uint256; + + /** + * When given allowance, transfers a token from the "_from" to the "_to" of quantity "_quantity". + * Ensures that the recipient has received the correct quantity (ie no fees taken on transfer) + * + * @param _token ERC20 token to approve + * @param _from The account to transfer tokens from + * @param _to The account to transfer tokens to + * @param _quantity The quantity to transfer + */ + function transferFrom( + IERC20 _token, + address _from, + address _to, + uint256 _quantity + ) + internal + { + // Call specified ERC20 contract to transfer tokens (via proxy). + if (_quantity > 0) { + uint256 existingBalance = _token.balanceOf(_to); + + SafeERC20.safeTransferFrom( + _token, + _from, + _to, + _quantity + ); + + uint256 newBalance = _token.balanceOf(_to); + + // Verify transfer quantity is reflected in balance + require( + newBalance == existingBalance.add(_quantity), + "Invalid post transfer balance" + ); + } + } +} diff --git a/contracts/lib/Invoke.sol b/contracts/lib/Invoke.sol new file mode 100644 index 00000000..1e9b7f3e --- /dev/null +++ b/contracts/lib/Invoke.sol @@ -0,0 +1,141 @@ +/* + 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; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; + +import { ISetToken } from "../interfaces/ISetToken.sol"; + + +/** + * @title Invoke + * @author Set Protocol + * + * A collection of common utility functions for interacting with the SetToken's invoke function + */ +library Invoke { + using SafeMath for uint256; + + /* ============ Internal ============ */ + + /** + * Instructs the SetToken to set approvals of the ERC20 token to a spender. + * + * @param _setToken SetToken instance to invoke + * @param _token ERC20 token to approve + * @param _spender The account allowed to spend the SetToken's balance + * @param _quantity The quantity of allowance to allow + */ + function invokeApprove( + ISetToken _setToken, + address _token, + address _spender, + uint256 _quantity + ) + internal + { + bytes memory callData = abi.encodeWithSignature("approve(address,uint256)", _spender, _quantity); + _setToken.invoke(_token, 0, callData); + } + + /** + * Instructs the SetToken to transfer the ERC20 token to a recipient. + * + * @param _setToken SetToken instance to invoke + * @param _token ERC20 token to transfer + * @param _to The recipient account + * @param _quantity The quantity to transfer + */ + function invokeTransfer( + ISetToken _setToken, + address _token, + address _to, + uint256 _quantity + ) + internal + { + if (_quantity > 0) { + bytes memory callData = abi.encodeWithSignature("transfer(address,uint256)", _to, _quantity); + + bytes memory returnData = _setToken.invoke(_token, 0, callData); + if (returnData.length > 0) { + require(abi.decode(returnData, (bool)), "ERC20 transfer failed"); + } + } + } + + /** + * Instructs the SetToken to transfer the ERC20 token to a recipient. + * The new SetToken balance must equal the existing balance less the quantity transferred + * + * @param _setToken SetToken instance to invoke + * @param _token ERC20 token to transfer + * @param _to The recipient account + * @param _quantity The quantity to transfer + */ + function strictInvokeTransfer( + ISetToken _setToken, + address _token, + address _to, + uint256 _quantity + ) + internal + { + if (_quantity > 0) { + // Retrieve current balance of token for the SetToken + uint256 existingBalance = IERC20(_token).balanceOf(address(_setToken)); + + Invoke.invokeTransfer(_setToken, _token, _to, _quantity); + + // Get new balance of transferred token for SetToken + uint256 newBalance = IERC20(_token).balanceOf(address(_setToken)); + + // Verify only the transfer quantity is subtracted + require( + newBalance == existingBalance.sub(_quantity), + "Invalid post transfer balance" + ); + } + } + + /** + * Instructs the SetToken to unwrap the passed quantity of WETH + * + * @param _setToken SetToken instance to invoke + * @param _weth WETH address + * @param _quantity The quantity to unwrap + */ + function invokeUnwrapWETH(ISetToken _setToken, address _weth, uint256 _quantity) internal { + bytes memory callData = abi.encodeWithSignature("withdraw(uint256)", _quantity); + _setToken.invoke(_weth, 0, callData); + } + + /** + * Instructs the SetToken to wrap the passed quantity of ETH + * + * @param _setToken SetToken instance to invoke + * @param _weth WETH address + * @param _quantity The quantity to unwrap + */ + function invokeWrapWETH(ISetToken _setToken, address _weth, uint256 _quantity) internal { + bytes memory callData = abi.encodeWithSignature("deposit()"); + _setToken.invoke(_weth, _quantity, callData); + } +} diff --git a/contracts/lib/ModuleBase.sol b/contracts/lib/ModuleBase.sol new file mode 100644 index 00000000..df5d4c68 --- /dev/null +++ b/contracts/lib/ModuleBase.sol @@ -0,0 +1,237 @@ +/* + 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; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { AddressArrayUtils } from "./AddressArrayUtils.sol"; +import { ExplicitERC20 } from "./ExplicitERC20.sol"; +import { IController } from "../interfaces/IController.sol"; +import { IModule } from "../interfaces/IModule.sol"; +import { ISetToken } from "../interfaces/ISetToken.sol"; +import { Invoke } from "./Invoke.sol"; +import { Position } from "./Position.sol"; +import { PreciseUnitMath } from "./PreciseUnitMath.sol"; +import { ResourceIdentifier } from "./ResourceIdentifier.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/SafeCast.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; +import { SignedSafeMath } from "@openzeppelin/contracts/math/SignedSafeMath.sol"; + +/** + * @title ModuleBase + * @author Set Protocol + * + * Abstract class that houses common Module-related state and functions. + * + * CHANGELOG: + * - 4/21/21: Delegated modifier logic to internal helpers to reduce contract size + * + */ +abstract contract ModuleBase is IModule { + using AddressArrayUtils for address[]; + using Invoke for ISetToken; + using Position for ISetToken; + using PreciseUnitMath for uint256; + using ResourceIdentifier for IController; + using SafeCast for int256; + using SafeCast for uint256; + using SafeMath for uint256; + using SignedSafeMath for int256; + + /* ============ State Variables ============ */ + + // Address of the controller + IController public controller; + + /* ============ Modifiers ============ */ + + modifier onlyManagerAndValidSet(ISetToken _setToken) { + _validateOnlyManagerAndValidSet(_setToken); + _; + } + + modifier onlySetManager(ISetToken _setToken, address _caller) { + _validateOnlySetManager(_setToken, _caller); + _; + } + + modifier onlyValidAndInitializedSet(ISetToken _setToken) { + _validateOnlyValidAndInitializedSet(_setToken); + _; + } + + /** + * Throws if the sender is not a SetToken's module or module not enabled + */ + modifier onlyModule(ISetToken _setToken) { + _validateOnlyModule(_setToken); + _; + } + + /** + * Utilized during module initializations to check that the module is in pending state + * and that the SetToken is valid + */ + modifier onlyValidAndPendingSet(ISetToken _setToken) { + _validateOnlyValidAndPendingSet(_setToken); + _; + } + + /* ============ Constructor ============ */ + + /** + * Set state variables and map asset pairs to their oracles + * + * @param _controller Address of controller contract + */ + constructor(IController _controller) public { + controller = _controller; + } + + /* ============ Internal Functions ============ */ + + /** + * Transfers tokens from an address (that has set allowance on the module). + * + * @param _token The address of the ERC20 token + * @param _from The address to transfer from + * @param _to The address to transfer to + * @param _quantity The number of tokens to transfer + */ + function transferFrom(IERC20 _token, address _from, address _to, uint256 _quantity) internal { + ExplicitERC20.transferFrom(_token, _from, _to, _quantity); + } + + /** + * Gets the integration for the module with the passed in name. Validates that the address is not empty + */ + function getAndValidateAdapter(string memory _integrationName) internal view returns(address) { + bytes32 integrationHash = getNameHash(_integrationName); + return getAndValidateAdapterWithHash(integrationHash); + } + + /** + * Gets the integration for the module with the passed in hash. Validates that the address is not empty + */ + function getAndValidateAdapterWithHash(bytes32 _integrationHash) internal view returns(address) { + address adapter = controller.getIntegrationRegistry().getIntegrationAdapterWithHash( + address(this), + _integrationHash + ); + + require(adapter != address(0), "Must be valid adapter"); + return adapter; + } + + /** + * Gets the total fee for this module of the passed in index (fee % * quantity) + */ + function getModuleFee(uint256 _feeIndex, uint256 _quantity) internal view returns(uint256) { + uint256 feePercentage = controller.getModuleFee(address(this), _feeIndex); + return _quantity.preciseMul(feePercentage); + } + + /** + * Pays the _feeQuantity from the _setToken denominated in _token to the protocol fee recipient + */ + function payProtocolFeeFromSetToken(ISetToken _setToken, address _token, uint256 _feeQuantity) internal { + if (_feeQuantity > 0) { + _setToken.strictInvokeTransfer(_token, controller.feeRecipient(), _feeQuantity); + } + } + + /** + * Returns true if the module is in process of initialization on the SetToken + */ + function isSetPendingInitialization(ISetToken _setToken) internal view returns(bool) { + return _setToken.isPendingModule(address(this)); + } + + /** + * Returns true if the address is the SetToken's manager + */ + function isSetManager(ISetToken _setToken, address _toCheck) internal view returns(bool) { + return _setToken.manager() == _toCheck; + } + + /** + * Returns true if SetToken must be enabled on the controller + * and module is registered on the SetToken + */ + function isSetValidAndInitialized(ISetToken _setToken) internal view returns(bool) { + return controller.isSet(address(_setToken)) && + _setToken.isInitializedModule(address(this)); + } + + /** + * Hashes the string and returns a bytes32 value + */ + function getNameHash(string memory _name) internal pure returns(bytes32) { + return keccak256(bytes(_name)); + } + + /* ============== Modifier Helpers =============== + * Internal functions used to reduce bytecode size + */ + + /** + * Caller must SetToken manager and SetToken must be valid and initialized + */ + function _validateOnlyManagerAndValidSet(ISetToken _setToken) internal view { + require(isSetManager(_setToken, msg.sender), "Must be the SetToken manager"); + require(isSetValidAndInitialized(_setToken), "Must be a valid and initialized SetToken"); + } + + /** + * Caller must SetToken manager + */ + function _validateOnlySetManager(ISetToken _setToken, address _caller) internal view { + require(isSetManager(_setToken, _caller), "Must be the SetToken manager"); + } + + /** + * SetToken must be valid and initialized + */ + function _validateOnlyValidAndInitializedSet(ISetToken _setToken) internal view { + require(isSetValidAndInitialized(_setToken), "Must be a valid and initialized SetToken"); + } + + /** + * Caller must be initialized module and module must be enabled on the controller + */ + function _validateOnlyModule(ISetToken _setToken) internal view { + require( + _setToken.moduleStates(msg.sender) == ISetToken.ModuleState.INITIALIZED, + "Only the module can call" + ); + + require( + controller.isModule(msg.sender), + "Module must be enabled on controller" + ); + } + + /** + * SetToken must be in a pending state and module must be in pending state + */ + function _validateOnlyValidAndPendingSet(ISetToken _setToken) internal view { + require(controller.isSet(address(_setToken)), "Must be controller-enabled SetToken"); + require(isSetPendingInitialization(_setToken), "Must be pending initialization"); + } +} diff --git a/contracts/lib/MutualUpgradeV2.sol b/contracts/lib/MutualUpgradeV2.sol new file mode 100644 index 00000000..9a7ec527 --- /dev/null +++ b/contracts/lib/MutualUpgradeV2.sol @@ -0,0 +1,82 @@ +/* + 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; + +/** + * @title MutualUpgradeV2 + * @author Set Protocol + * + * The MutualUpgradeV2 contract contains a modifier for handling mutual upgrades between two parties + * + * CHANGELOG: + * - Update mutualUpgrade to allow single transaction execution if the two signing addresses are the same + */ +contract MutualUpgradeV2 { + /* ============ State Variables ============ */ + + // Mapping of upgradable units and if upgrade has been initialized by other party + mapping(bytes32 => bool) public mutualUpgrades; + + /* ============ Events ============ */ + + event MutualUpgradeRegistered( + bytes32 _upgradeHash + ); + + /* ============ Modifiers ============ */ + + modifier mutualUpgrade(address _signerOne, address _signerTwo) { + require( + msg.sender == _signerOne || msg.sender == _signerTwo, + "Must be authorized address" + ); + + // If the two signing addresses are the same, skip upgrade hash step + if (_signerOne == _signerTwo) { + _; + } + + address nonCaller = _getNonCaller(_signerOne, _signerTwo); + + // The upgrade hash is defined by the hash of the transaction call data and sender of msg, + // which uniquely identifies the function, arguments, and sender. + bytes32 expectedHash = keccak256(abi.encodePacked(msg.data, nonCaller)); + + if (!mutualUpgrades[expectedHash]) { + bytes32 newHash = keccak256(abi.encodePacked(msg.data, msg.sender)); + + mutualUpgrades[newHash] = true; + + emit MutualUpgradeRegistered(newHash); + + return; + } + + delete mutualUpgrades[expectedHash]; + + // Run the rest of the upgrades + _; + } + + /* ============ Internal Functions ============ */ + + function _getNonCaller(address _signerOne, address _signerTwo) internal view returns(address) { + return msg.sender == _signerOne ? _signerTwo : _signerOne; + } +} diff --git a/contracts/lib/Position.sol b/contracts/lib/Position.sol new file mode 100644 index 00000000..d71c5dfd --- /dev/null +++ b/contracts/lib/Position.sol @@ -0,0 +1,259 @@ +/* + Copyright 2020 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache License, Version 2.0 +*/ + +pragma solidity 0.6.10; +pragma experimental "ABIEncoderV2"; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/SafeCast.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; +import { SignedSafeMath } from "@openzeppelin/contracts/math/SignedSafeMath.sol"; + +import { ISetToken } from "../interfaces/ISetToken.sol"; +import { PreciseUnitMath } from "./PreciseUnitMath.sol"; + + +/** + * @title Position + * @author Set Protocol + * + * Collection of helper functions for handling and updating SetToken Positions + * + * CHANGELOG: + * - Updated editExternalPosition to work when no external position is associated with module + */ +library Position { + using SafeCast for uint256; + using SafeMath for uint256; + using SafeCast for int256; + using SignedSafeMath for int256; + using PreciseUnitMath for uint256; + + /* ============ Helper ============ */ + + /** + * Returns whether the SetToken has a default position for a given component (if the real unit is > 0) + */ + function hasDefaultPosition(ISetToken _setToken, address _component) internal view returns(bool) { + return _setToken.getDefaultPositionRealUnit(_component) > 0; + } + + /** + * Returns whether the SetToken has an external position for a given component (if # of position modules is > 0) + */ + function hasExternalPosition(ISetToken _setToken, address _component) internal view returns(bool) { + return _setToken.getExternalPositionModules(_component).length > 0; + } + + /** + * Returns whether the SetToken component default position real unit is greater than or equal to units passed in. + */ + function hasSufficientDefaultUnits(ISetToken _setToken, address _component, uint256 _unit) internal view returns(bool) { + return _setToken.getDefaultPositionRealUnit(_component) >= _unit.toInt256(); + } + + /** + * Returns whether the SetToken component external position is greater than or equal to the real units passed in. + */ + function hasSufficientExternalUnits( + ISetToken _setToken, + address _component, + address _positionModule, + uint256 _unit + ) + internal + view + returns(bool) + { + return _setToken.getExternalPositionRealUnit(_component, _positionModule) >= _unit.toInt256(); + } + + /** + * If the position does not exist, create a new Position and add to the SetToken. If it already exists, + * then set the position units. If the new units is 0, remove the position. Handles adding/removing of + * components where needed (in light of potential external positions). + * + * @param _setToken Address of SetToken being modified + * @param _component Address of the component + * @param _newUnit Quantity of Position units - must be >= 0 + */ + function editDefaultPosition(ISetToken _setToken, address _component, uint256 _newUnit) internal { + bool isPositionFound = hasDefaultPosition(_setToken, _component); + if (!isPositionFound && _newUnit > 0) { + // If there is no Default Position and no External Modules, then component does not exist + if (!hasExternalPosition(_setToken, _component)) { + _setToken.addComponent(_component); + } + } else if (isPositionFound && _newUnit == 0) { + // If there is a Default Position and no external positions, remove the component + if (!hasExternalPosition(_setToken, _component)) { + _setToken.removeComponent(_component); + } + } + + _setToken.editDefaultPositionUnit(_component, _newUnit.toInt256()); + } + + /** + * Update an external position and remove and external positions or components if necessary. The logic flows as follows: + * 1) If component is not already added then add component and external position. + * 2) If component is added but no existing external position using the passed module exists then add the external position. + * 3) If the existing position is being added to then just update the unit and data + * 4) If the position is being closed and no other external positions or default positions are associated with the component + * then untrack the component and remove external position. + * 5) If the position is being closed and other existing positions still exist for the component then just remove the + * external position. + * + * @param _setToken SetToken being updated + * @param _component Component position being updated + * @param _module Module external position is associated with + * @param _newUnit Position units of new external position + * @param _data Arbitrary data associated with the position + */ + function editExternalPosition( + ISetToken _setToken, + address _component, + address _module, + int256 _newUnit, + bytes memory _data + ) + internal + { + if (_newUnit != 0) { + if (!_setToken.isComponent(_component)) { + _setToken.addComponent(_component); + _setToken.addExternalPositionModule(_component, _module); + } else if (!_setToken.isExternalPositionModule(_component, _module)) { + _setToken.addExternalPositionModule(_component, _module); + } + _setToken.editExternalPositionUnit(_component, _module, _newUnit); + _setToken.editExternalPositionData(_component, _module, _data); + } else { + require(_data.length == 0, "Passed data must be null"); + // If no default or external position remaining then remove component from components array + if (_setToken.getExternalPositionRealUnit(_component, _module) != 0) { + address[] memory positionModules = _setToken.getExternalPositionModules(_component); + if (_setToken.getDefaultPositionRealUnit(_component) == 0 && positionModules.length == 1) { + require(positionModules[0] == _module, "External positions must be 0 to remove component"); + _setToken.removeComponent(_component); + } + _setToken.removeExternalPositionModule(_component, _module); + } + } + } + + /** + * Get total notional amount of Default position + * + * @param _setTokenSupply Supply of SetToken in precise units (10^18) + * @param _positionUnit Quantity of Position units + * + * @return Total notional amount of units + */ + function getDefaultTotalNotional(uint256 _setTokenSupply, uint256 _positionUnit) internal pure returns (uint256) { + return _setTokenSupply.preciseMul(_positionUnit); + } + + /** + * Get position unit from total notional amount + * + * @param _setTokenSupply Supply of SetToken in precise units (10^18) + * @param _totalNotional Total notional amount of component prior to + * @return Default position unit + */ + function getDefaultPositionUnit(uint256 _setTokenSupply, uint256 _totalNotional) internal pure returns (uint256) { + return _totalNotional.preciseDiv(_setTokenSupply); + } + + /** + * Get the total tracked balance - total supply * position unit + * + * @param _setToken Address of the SetToken + * @param _component Address of the component + * @return Notional tracked balance + */ + function getDefaultTrackedBalance(ISetToken _setToken, address _component) internal view returns(uint256) { + int256 positionUnit = _setToken.getDefaultPositionRealUnit(_component); + return _setToken.totalSupply().preciseMul(positionUnit.toUint256()); + } + + /** + * Calculates the new default position unit and performs the edit with the new unit + * + * @param _setToken Address of the SetToken + * @param _component Address of the component + * @param _setTotalSupply Current SetToken supply + * @param _componentPreviousBalance Pre-action component balance + * @return Current component balance + * @return Previous position unit + * @return New position unit + */ + function calculateAndEditDefaultPosition( + ISetToken _setToken, + address _component, + uint256 _setTotalSupply, + uint256 _componentPreviousBalance + ) + internal + returns(uint256, uint256, uint256) + { + uint256 currentBalance = IERC20(_component).balanceOf(address(_setToken)); + uint256 positionUnit = _setToken.getDefaultPositionRealUnit(_component).toUint256(); + + uint256 newTokenUnit; + if (currentBalance > 0) { + newTokenUnit = calculateDefaultEditPositionUnit( + _setTotalSupply, + _componentPreviousBalance, + currentBalance, + positionUnit + ); + } else { + newTokenUnit = 0; + } + + editDefaultPosition(_setToken, _component, newTokenUnit); + + return (currentBalance, positionUnit, newTokenUnit); + } + + /** + * Calculate the new position unit given total notional values pre and post executing an action that changes SetToken state + * The intention is to make updates to the units without accidentally picking up airdropped assets as well. + * + * @param _setTokenSupply Supply of SetToken in precise units (10^18) + * @param _preTotalNotional Total notional amount of component prior to executing action + * @param _postTotalNotional Total notional amount of component after the executing action + * @param _prePositionUnit Position unit of SetToken prior to executing action + * @return New position unit + */ + function calculateDefaultEditPositionUnit( + uint256 _setTokenSupply, + uint256 _preTotalNotional, + uint256 _postTotalNotional, + uint256 _prePositionUnit + ) + internal + pure + returns (uint256) + { + // If pre action total notional amount is greater then subtract post action total notional and calculate new position units + uint256 airdroppedAmount = _preTotalNotional.sub(_prePositionUnit.preciseMul(_setTokenSupply)); + return _postTotalNotional.sub(airdroppedAmount).preciseDiv(_setTokenSupply); + } +} diff --git a/contracts/lib/ResourceIdentifier.sol b/contracts/lib/ResourceIdentifier.sol new file mode 100644 index 00000000..259f2179 --- /dev/null +++ b/contracts/lib/ResourceIdentifier.sol @@ -0,0 +1,64 @@ +/* + 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; + +import { IController } from "../interfaces/IController.sol"; +import { IIntegrationRegistry } from "../interfaces/IIntegrationRegistry.sol"; +import { IPriceOracle } from "../interfaces/IPriceOracle.sol"; +import { ISetValuer } from "../interfaces/ISetValuer.sol"; + +/** + * @title ResourceIdentifier + * @author Set Protocol + * + * A collection of utility functions to fetch information related to Resource contracts in the system + */ +library ResourceIdentifier { + + // IntegrationRegistry will always be resource ID 0 in the system + uint256 constant internal INTEGRATION_REGISTRY_RESOURCE_ID = 0; + // PriceOracle will always be resource ID 1 in the system + uint256 constant internal PRICE_ORACLE_RESOURCE_ID = 1; + // SetValuer resource will always be resource ID 2 in the system + uint256 constant internal SET_VALUER_RESOURCE_ID = 2; + + /* ============ Internal ============ */ + + /** + * Gets the instance of integration registry stored on Controller. Note: IntegrationRegistry is stored as index 0 on + * the Controller + */ + function getIntegrationRegistry(IController _controller) internal view returns (IIntegrationRegistry) { + return IIntegrationRegistry(_controller.resourceId(INTEGRATION_REGISTRY_RESOURCE_ID)); + } + + /** + * Gets instance of price oracle on Controller. Note: PriceOracle is stored as index 1 on the Controller + */ + function getPriceOracle(IController _controller) internal view returns (IPriceOracle) { + return IPriceOracle(_controller.resourceId(PRICE_ORACLE_RESOURCE_ID)); + } + + /** + * Gets the instance of Set valuer on Controller. Note: SetValuer is stored as index 2 on the Controller + */ + function getSetValuer(IController _controller) internal view returns (ISetValuer) { + return ISetValuer(_controller.resourceId(SET_VALUER_RESOURCE_ID)); + } +} diff --git a/contracts/manager/DelegatedManager.sol b/contracts/manager/DelegatedManager.sol new file mode 100644 index 00000000..b07b265e --- /dev/null +++ b/contracts/manager/DelegatedManager.sol @@ -0,0 +1,478 @@ +/* + 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 { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; + +import { AddressArrayUtils } from "../lib/AddressArrayUtils.sol"; +import { ISetToken } from "../interfaces/ISetToken.sol"; +import { PreciseUnitMath } from "../lib/PreciseUnitMath.sol"; + +import { IGlobalExtension } from "../interfaces/IGlobalExtension.sol"; +import { MutualUpgradeV2 } from "../lib/MutualUpgradeV2.sol"; + + +/** + * @title DelegatedManager + * @author Set Protocol + * + * Smart contract manager that maintains permissions and SetToken admin functionality via owner role. Owner + * works alongside methodologist to ensure business agreements are kept. Owner is able to delegate maintenance + * operations to operator(s). There can be more than one operator, however they have a global role so once + * delegated to they can perform any operator delegated roles. The owner is able to set restrictions on what + * operators can do in the form of asset whitelists. Operators cannot trade/wrap/claim/etc. an asset that is not + * a part of the asset whitelist, hence they are a semi-trusted party. It is recommended that the owner address + * be managed by a multi-sig or some form of permissioning system. + */ +contract DelegatedManager is Ownable, MutualUpgradeV2 { + using Address for address; + using AddressArrayUtils for address[]; + using SafeERC20 for IERC20; + + /* ============ Enums ============ */ + + enum ExtensionState { + NONE, + PENDING, + INITIALIZED + } + + /* ============ Events ============ */ + + event MethodologistChanged( + address indexed _newMethodologist + ); + + event ExtensionAdded( + address indexed _extension + ); + + event ExtensionRemoved( + address indexed _extension + ); + + event ExtensionInitialized( + address indexed _extension + ); + + event OperatorAdded( + address indexed _operator + ); + + event OperatorRemoved( + address indexed _operator + ); + + event AllowedAssetAdded( + address indexed _asset + ); + + event AllowedAssetRemoved( + address indexed _asset + ); + + event UseAssetAllowlistUpdated( + bool _status + ); + + event OwnerFeeSplitUpdated( + uint256 _newFeeSplit + ); + + event OwnerFeeRecipientUpdated( + address indexed _newFeeRecipient + ); + + /* ============ Modifiers ============ */ + + /** + * Throws if the sender is not the SetToken methodologist + */ + modifier onlyMethodologist() { + require(msg.sender == methodologist, "Must be methodologist"); + _; + } + + /** + * Throws if the sender is not an initialized extension + */ + modifier onlyExtension() { + require(extensionAllowlist[msg.sender] == ExtensionState.INITIALIZED, "Must be initialized extension"); + _; + } + + /* ============ State Variables ============ */ + + // Instance of SetToken + ISetToken public immutable setToken; + + // Address of factory contract used to deploy contract + address public immutable factory; + + // Mapping to check which ExtensionState a given extension is in + mapping(address => ExtensionState) public extensionAllowlist; + + // Array of initialized extensions + address[] internal extensions; + + // Mapping indicating if address is an approved operator + mapping(address=>bool) public operatorAllowlist; + + // List of approved operators + address[] internal operators; + + // Mapping indicating if asset is approved to be traded for, wrapped into, claimed, etc. + mapping(address=>bool) public assetAllowlist; + + // List of allowed assets + address[] internal allowedAssets; + + // Toggle if asset allow list is being enforced + bool public useAssetAllowlist; + + // Global owner fee split that can be referenced by Extensions + uint256 public ownerFeeSplit; + + // Address owners portions of fees get sent to + address public ownerFeeRecipient; + + // Address of methodologist which serves as providing methodology for the index and receives fee splits + address public methodologist; + + /* ============ Constructor ============ */ + + constructor( + ISetToken _setToken, + address _factory, + address _methodologist, + address[] memory _extensions, + address[] memory _operators, + address[] memory _allowedAssets, + bool _useAssetAllowlist + ) + public + { + setToken = _setToken; + factory = _factory; + methodologist = _methodologist; + useAssetAllowlist = _useAssetAllowlist; + emit UseAssetAllowlistUpdated(_useAssetAllowlist); + + _addExtensions(_extensions); + _addOperators(_operators); + _addAllowedAssets(_allowedAssets); + } + + /* ============ External Functions ============ */ + + /** + * ONLY EXTENSION: Interact with a module registered on the SetToken. In order to ensure SetToken admin + * functions can only be changed from this contract no calls to the SetToken can originate from Extensions. + * To transfer SetTokens use the `transferTokens` function. + * + * @param _module Module to interact with + * @param _data Byte data of function to call in module + */ + function interactManager(address _module, bytes calldata _data) external onlyExtension { + require(_module != address(setToken), "Extensions cannot call SetToken"); + // Invoke call to module, assume value will always be 0 + _module.functionCallWithValue(_data, 0); + } + + /** + * EXTENSION ONLY: Transfers _tokens held by the manager to _destination. Can be used to + * distribute fees or recover anything sent here accidentally. + * + * @param _token ERC20 token to send + * @param _destination Address receiving the tokens + * @param _amount Quantity of tokens to send + */ + function transferTokens(address _token, address _destination, uint256 _amount) external onlyExtension { + IERC20(_token).safeTransfer(_destination, _amount); + } + + /** + * Initializes an added extension from PENDING to INITIALIZED state and adds to extension array. An + * address can only enter a PENDING state if it is an enabled extension added by the manager. Only + * callable by the extension itself, hence msg.sender is the subject of update. + */ + function initializeExtension() external { + require(extensionAllowlist[msg.sender] == ExtensionState.PENDING, "Extension must be pending"); + + extensionAllowlist[msg.sender] = ExtensionState.INITIALIZED; + extensions.push(msg.sender); + + emit ExtensionInitialized(msg.sender); + } + + /** + * ONLY OWNER: Add new extension(s) that the DelegatedManager can call. Puts extensions into PENDING + * state, each must be initialized in order to be used. + * + * @param _extensions New extension(s) to add + */ + function addExtensions(address[] memory _extensions) external onlyOwner { + _addExtensions(_extensions); + } + + /** + * ONLY OWNER: Remove existing extension(s) tracked by the DelegatedManager. Removed extensions are + * placed in NONE state. + * + * @param _extensions Old extension to remove + */ + function removeExtensions(address[] memory _extensions) external onlyOwner { + for (uint256 i = 0; i < _extensions.length; i++) { + address extension = _extensions[i]; + + require(extensionAllowlist[extension] == ExtensionState.INITIALIZED, "Extension not initialized"); + + extensions.removeStorage(extension); + + extensionAllowlist[extension] = ExtensionState.NONE; + + IGlobalExtension(extension).removeExtension(); + + emit ExtensionRemoved(extension); + } + } + + /** + * ONLY OWNER: Add new operator(s) address(es) + * + * @param _operators New operator(s) to add + */ + function addOperators(address[] memory _operators) external onlyOwner { + _addOperators(_operators); + } + + /** + * ONLY OWNER: Remove operator(s) from the allowlist + * + * @param _operators New operator(s) to remove + */ + function removeOperators(address[] memory _operators) external onlyOwner { + for (uint256 i = 0; i < _operators.length; i++) { + address operator = _operators[i]; + + require(operatorAllowlist[operator], "Operator not already added"); + + operators.removeStorage(operator); + + operatorAllowlist[operator] = false; + + emit OperatorRemoved(operator); + } + } + + /** + * ONLY OWNER: Add new asset(s) that can be traded to, wrapped to, or claimed + * + * @param _assets New asset(s) to add + */ + function addAllowedAssets(address[] memory _assets) external onlyOwner { + _addAllowedAssets(_assets); + } + + /** + * ONLY OWNER: Remove asset(s) so that it/they can't be traded to, wrapped to, or claimed + * + * @param _assets Asset(s) to remove + */ + function removeAllowedAssets(address[] memory _assets) external onlyOwner { + for (uint256 i = 0; i < _assets.length; i++) { + address asset = _assets[i]; + + require(assetAllowlist[asset], "Asset not already added"); + + allowedAssets.removeStorage(asset); + + assetAllowlist[asset] = false; + + emit AllowedAssetRemoved(asset); + } + } + + /** + * ONLY OWNER: Toggle useAssetAllowlist on and off. When false asset allowlist is ignored + * when true it is enforced. + * + * @param _useAssetAllowlist Bool indicating whether to use asset allow list + */ + function updateUseAssetAllowlist(bool _useAssetAllowlist) external onlyOwner { + useAssetAllowlist = _useAssetAllowlist; + + emit UseAssetAllowlistUpdated(_useAssetAllowlist); + } + + /** + * MUTUAL UPGRADE: Update percent of fees that are sent to owner. Owner and Methodologist must each call this function to execute + * the update. If Owner and Methodologist point to the same address, the update can be executed in a single call. + * + * @param _newFeeSplit Percent in precise units (100% = 10**18) of fees that accrue to owner + */ + function updateOwnerFeeSplit(uint256 _newFeeSplit) external mutualUpgrade(owner(), methodologist) { + require(_newFeeSplit <= PreciseUnitMath.preciseUnit(), "Invalid fee split"); + + ownerFeeSplit = _newFeeSplit; + + emit OwnerFeeSplitUpdated(_newFeeSplit); + } + + /** + * ONLY OWNER: Update address owner receives fees at + * + * @param _newFeeRecipient Address to send owner fees to + */ + function updateOwnerFeeRecipient(address _newFeeRecipient) external onlyOwner { + require(_newFeeRecipient != address(0), "Null address passed"); + + ownerFeeRecipient = _newFeeRecipient; + + emit OwnerFeeRecipientUpdated(_newFeeRecipient); + } + + /** + * ONLY METHODOLOGIST: Update the methodologist address + * + * @param _newMethodologist New methodologist address + */ + function setMethodologist(address _newMethodologist) external onlyMethodologist { + require(_newMethodologist != address(0), "Null address passed"); + + methodologist = _newMethodologist; + + emit MethodologistChanged(_newMethodologist); + } + + /** + * ONLY OWNER: Update the SetToken manager address. + * + * @param _newManager New manager address + */ + function setManager(address _newManager) external onlyOwner { + require(_newManager != address(0), "Zero address not valid"); + require(extensions.length == 0, "Must remove all extensions"); + setToken.setManager(_newManager); + } + + /** + * ONLY OWNER: Add a new module to the SetToken. + * + * @param _module New module to add + */ + function addModule(address _module) external onlyOwner { + setToken.addModule(_module); + } + + /** + * ONLY OWNER: Remove a module from the SetToken. + * + * @param _module Module to remove + */ + function removeModule(address _module) external onlyOwner { + setToken.removeModule(_module); + } + + /* ============ External View Functions ============ */ + + function isAllowedAsset(address _asset) external view returns(bool) { + return !useAssetAllowlist || assetAllowlist[_asset]; + } + + function isPendingExtension(address _extension) external view returns(bool) { + return extensionAllowlist[_extension] == ExtensionState.PENDING; + } + + function isInitializedExtension(address _extension) external view returns(bool) { + return extensionAllowlist[_extension] == ExtensionState.INITIALIZED; + } + + function getExtensions() external view returns(address[] memory) { + return extensions; + } + + function getOperators() external view returns(address[] memory) { + return operators; + } + + function getAllowedAssets() external view returns(address[] memory) { + return allowedAssets; + } + + /* ============ Internal Functions ============ */ + + /** + * Add extensions that the DelegatedManager can call. + * + * @param _extensions New extension to add + */ + function _addExtensions(address[] memory _extensions) internal { + for (uint256 i = 0; i < _extensions.length; i++) { + address extension = _extensions[i]; + + require(extensionAllowlist[extension] == ExtensionState.NONE , "Extension already exists"); + + extensionAllowlist[extension] = ExtensionState.PENDING; + + emit ExtensionAdded(extension); + } + } + + /** + * Add new operator(s) address(es) + * + * @param _operators New operator to add + */ + function _addOperators(address[] memory _operators) internal { + for (uint256 i = 0; i < _operators.length; i++) { + address operator = _operators[i]; + + require(!operatorAllowlist[operator], "Operator already added"); + + operators.push(operator); + + operatorAllowlist[operator] = true; + + emit OperatorAdded(operator); + } + } + + /** + * Add new assets that can be traded to, wrapped to, or claimed + * + * @param _assets New asset to add + */ + function _addAllowedAssets(address[] memory _assets) internal { + for (uint256 i = 0; i < _assets.length; i++) { + address asset = _assets[i]; + + require(!assetAllowlist[asset], "Asset already added"); + + allowedAssets.push(asset); + + assetAllowlist[asset] = true; + + emit AllowedAssetAdded(asset); + } + } +} diff --git a/contracts/mocks/BaseGlobalExtensionMock.sol b/contracts/mocks/BaseGlobalExtensionMock.sol new file mode 100644 index 00000000..72ae023d --- /dev/null +++ b/contracts/mocks/BaseGlobalExtensionMock.sol @@ -0,0 +1,110 @@ +/* + 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 "../interfaces/ISetToken.sol"; + +import { BaseGlobalExtension } from "../lib/BaseGlobalExtension.sol"; +import { IDelegatedManager } from "../interfaces/IDelegatedManager.sol"; +import { IManagerCore } from "../interfaces/IManagerCore.sol"; +import { ModuleMock } from "./ModuleMock.sol"; + +contract BaseGlobalExtensionMock is BaseGlobalExtension { + + /* ============ State Variables ============ */ + + ModuleMock public immutable module; + + /* ============ Constructor ============ */ + + constructor( + IManagerCore _managerCore, + ModuleMock _module + ) + public + BaseGlobalExtension(_managerCore) + { + module = _module; + } + + /* ============ External Functions ============ */ + + function initializeExtension( + IDelegatedManager _delegatedManager + ) + external + onlyOwnerAndValidManager(_delegatedManager) + { + require(_delegatedManager.isPendingExtension(address(this)), "Extension must be pending"); + + _initializeExtension(_delegatedManager.setToken(), _delegatedManager); + } + + function initializeModuleAndExtension( + IDelegatedManager _delegatedManager + ) + external + onlyOwnerAndValidManager(_delegatedManager) + { + require(_delegatedManager.isPendingExtension(address(this)), "Extension must be pending"); + + ISetToken setToken = _delegatedManager.setToken(); + + _initializeExtension(setToken, _delegatedManager); + + bytes memory callData = abi.encodeWithSignature("initialize(address)", setToken); + _invokeManager(_delegatedManager, address(module), callData); + } + + function testInvokeManager(ISetToken _setToken, address _module, bytes calldata _encoded) external { + _invokeManager(_manager(_setToken), _module, _encoded); + } + + function testOnlyOwner(ISetToken _setToken) + external + onlyOwner(_setToken) + {} + + function testOnlyMethodologist(ISetToken _setToken) + external + onlyMethodologist(_setToken) + {} + + function testOnlyOperator(ISetToken _setToken) + external + onlyOperator(_setToken) + {} + + function testOnlyOwnerAndValidManager(IDelegatedManager _delegatedManager) + external + onlyOwnerAndValidManager(_delegatedManager) + {} + + function testOnlyAllowedAsset(ISetToken _setToken, address _asset) + external + onlyAllowedAsset(_setToken, _asset) + {} + + function removeExtension() external override { + IDelegatedManager delegatedManager = IDelegatedManager(msg.sender); + ISetToken setToken = delegatedManager.setToken(); + + _removeExtension(setToken, delegatedManager); + } +} diff --git a/contracts/mocks/ManagerMock.sol b/contracts/mocks/ManagerMock.sol new file mode 100644 index 00000000..60d7b211 --- /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 "../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(); + } + } +} diff --git a/contracts/mocks/ModuleMock.sol b/contracts/mocks/ModuleMock.sol new file mode 100644 index 00000000..b690c8ad --- /dev/null +++ b/contracts/mocks/ModuleMock.sol @@ -0,0 +1,48 @@ +/* + 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 { IController } from "../interfaces/IController.sol"; +import { ISetToken } from "../interfaces/ISetToken.sol"; +import { ModuleBase } from "../lib/ModuleBase.sol"; + +contract ModuleMock is ModuleBase { + + bool public removed; + + /* ============ Constructor ============ */ + + constructor(IController _controller) public ModuleBase(_controller) {} + + /* ============ External Functions ============ */ + + function initialize( + ISetToken _setToken + ) + external + onlyValidAndPendingSet(_setToken) + onlySetManager(_setToken, msg.sender) + { + _setToken.initializeModule(); + } + + function removeModule() external override { + removed = true; + } +} diff --git a/contracts/mocks/MutualUpgradeV2Mock.sol b/contracts/mocks/MutualUpgradeV2Mock.sol new file mode 100644 index 00000000..a5f326f1 --- /dev/null +++ b/contracts/mocks/MutualUpgradeV2Mock.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache License, Version 2.0 +pragma solidity 0.6.10; + +import { MutualUpgradeV2 } from "../lib/MutualUpgradeV2.sol"; + + +// Mock contract implementation of MutualUpgradeV2 functions +contract MutualUpgradeV2Mock is + MutualUpgradeV2 +{ + uint256 public testUint; + address public owner; + address public methodologist; + + constructor(address _owner, address _methodologist) public { + owner = _owner; + methodologist = _methodologist; + } + + function testMutualUpgrade( + uint256 _testUint + ) + external + mutualUpgrade(owner, methodologist) + { + testUint = _testUint; + } +} From 50ac613befb7d49087a5075513ff9a9a251767ca Mon Sep 17 00:00:00 2001 From: Pranav Bhardwaj Date: Fri, 25 Aug 2023 11:56:54 -0400 Subject: [PATCH 02/10] add core contract tests --- .../factories/delegatedManagerFactory.spec.ts | 871 +++++++++++++ test/manager/delegatedManager.spec.ts | 1155 +++++++++++++++++ test/managerCore.spec.ts | 602 +++++++++ utils/constants.ts | 12 + utils/contracts/index.ts | 13 + utils/deploys/deployFactories.ts | 27 + utils/deploys/deployGlobalExtensions.ts | 91 ++ utils/deploys/deployManager.ts | 31 +- utils/deploys/deployManagerCore.ts | 16 + utils/deploys/deployMocks.ts | 12 + utils/deploys/deploySetV2.ts | 4 + utils/deploys/index.ts | 9 + utils/index.ts | 3 +- utils/test/testingUtils.ts | 4 + utils/types.ts | 22 + 15 files changed, 2869 insertions(+), 3 deletions(-) create mode 100644 test/factories/delegatedManagerFactory.spec.ts create mode 100644 test/manager/delegatedManager.spec.ts create mode 100644 test/managerCore.spec.ts create mode 100644 utils/deploys/deployFactories.ts create mode 100644 utils/deploys/deployGlobalExtensions.ts create mode 100644 utils/deploys/deployManagerCore.ts diff --git a/test/factories/delegatedManagerFactory.spec.ts b/test/factories/delegatedManagerFactory.spec.ts new file mode 100644 index 00000000..111e2760 --- /dev/null +++ b/test/factories/delegatedManagerFactory.spec.ts @@ -0,0 +1,871 @@ +import "module-alias/register"; + +import { BigNumber, ContractTransaction } from "ethers"; +import { Address, Account } from "@utils/types"; +import { ADDRESS_ZERO, ZERO, MODULE_STATE } from "@utils/constants"; +import { ProtocolUtils } from "@utils/common"; +import { + DelegatedManagerFactory, + DelegatedManager, + BaseGlobalExtensionMock, + ManagerCore, + ModuleMock, +} from "@utils/contracts/index"; +import DeployHelper from "@utils/deploys"; +import { + cacheBeforeEach, + ether, + getAccounts, + getSetFixture, + getWaffleExpect, + getRandomAccount, + getProtocolUtils, +} from "@utils/index"; +import { SetFixture } from "@utils/fixtures"; +import { SetToken } from "@utils/contracts/setV2"; + + +const expect = getWaffleExpect(); + +describe("DelegatedManagerFactory", () => { + let owner: Account; + let methodologist: Account; + let otherAccount: Account; + let operatorOne: Account; + let operatorTwo: Account; + let EOAManagedSetToken: SetToken; + + let setV2Setup: SetFixture; + + let deployer: DeployHelper; + let protocolUtils: ProtocolUtils; + + let managerCore: ManagerCore; + let delegatedManagerFactory: DelegatedManagerFactory; + let mockFeeExtension: BaseGlobalExtensionMock; + let mockIssuanceExtension: BaseGlobalExtensionMock; + let mockFeeModule: ModuleMock; + let mockIssuanceModule: ModuleMock; + + cacheBeforeEach(async () => { + [ + owner, + otherAccount, + methodologist, + operatorOne, + operatorTwo, + ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + protocolUtils = getProtocolUtils(); + + setV2Setup = getSetFixture(owner.address); + await setV2Setup.initialize(); + + mockFeeModule = await deployer.mocks.deployModuleMock(setV2Setup.controller.address); + mockIssuanceModule = await deployer.mocks.deployModuleMock(setV2Setup.controller.address); + await setV2Setup.controller.addModule(mockFeeModule.address); + await setV2Setup.controller.addModule(mockIssuanceModule.address); + + managerCore = await deployer.managerCore.deployManagerCore(); + + mockFeeExtension = await deployer.mocks.deployBaseGlobalExtensionMock(managerCore.address, mockFeeModule.address); + mockIssuanceExtension = await deployer.mocks.deployBaseGlobalExtensionMock(managerCore.address, mockIssuanceModule.address); + + delegatedManagerFactory = await deployer.factories.deployDelegatedManagerFactory( + managerCore.address, + setV2Setup.controller.address, + setV2Setup.factory.address + ); + + await managerCore.initialize( + [mockFeeExtension.address, mockIssuanceExtension.address], + [delegatedManagerFactory.address] + ); + }); + + // Helper function to run a setup execution of either `createSetAndManager` or `createManager` + async function create(existingSetToken?: Address): Promise { + const tokens = [setV2Setup.dai.address, setV2Setup.wbtc.address]; + const operators = [operatorOne.address, operatorTwo.address]; + const otherAccountAddress = otherAccount.address; + const methodologistAddress = methodologist.address; + const modules = [mockFeeModule.address, mockIssuanceModule.address]; + const extensions = [mockFeeExtension.address, mockIssuanceExtension.address]; + + if (existingSetToken === undefined) { + return await delegatedManagerFactory.createSetAndManager( + tokens, + [ether(1), ether(.1)], + "TestToken", + "TT", + otherAccountAddress, + methodologistAddress, + modules, + operators, + tokens, + extensions + ); + } + + return await delegatedManagerFactory.createManager( + existingSetToken as string, + otherAccountAddress, + methodologistAddress, + operators, + tokens, + extensions + ); + } + + // Helper function to generate bytecode packets for factory initialization call + async function generateBytecode(manager: Address, modulesInitialized: Boolean): Promise { + if (modulesInitialized) { + const feeExtensionBytecode = mockFeeExtension.interface.encodeFunctionData("initializeExtension", [ + manager, + ]); + + const issuanceExtensionBytecode = mockIssuanceExtension.interface.encodeFunctionData("initializeExtension", [ + manager, + ]); + + return [feeExtensionBytecode, issuanceExtensionBytecode]; + } else { + const feeExtensionBytecode = mockFeeExtension.interface.encodeFunctionData("initializeModuleAndExtension", [ + manager, + ]); + + const issuanceExtensionBytecode = mockIssuanceExtension.interface.encodeFunctionData("initializeModuleAndExtension", [ + manager, + ]); + + return [feeExtensionBytecode, issuanceExtensionBytecode]; + } + } + + describe("#constructor", async () => { + let subjectManagerCore: Address; + let subjectController: Address; + let subjectSetTokenFactory: Address; + + beforeEach(async () => { + subjectManagerCore = managerCore.address; + subjectController = setV2Setup.controller.address; + subjectSetTokenFactory = setV2Setup.factory.address; + }); + + async function subject(): Promise { + return await deployer.factories.deployDelegatedManagerFactory( + subjectManagerCore, + subjectController, + 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 Controller address", async () => { + const delegatedManager = await subject(); + + const actualController = await delegatedManager.controller(); + expect (actualController).to.eq(subjectController); + }); + + it("should set the correct SetToken factory address", async () => { + const delegatedManager = await subject(); + + const actualFactory = await delegatedManager.setTokenFactory(); + expect (actualFactory).to.eq(subjectSetTokenFactory); + }); + }); + + describe("#createSetAndManager", () => { + let subjectComponents: Address[]; + let subjectUnits: BigNumber[]; + let subjectName: string; + let subjectSymbol: string; + let subjectOwner: Address; + let subjectMethodologist: Address; + let subjectModules: Address[]; + let subjectOperators: Address[]; + let subjectAssets: Address[]; + let subjectExtensions: Address[]; + + beforeEach(() => { + subjectComponents = [setV2Setup.dai.address, setV2Setup.wbtc.address], + subjectUnits = [ether(1), ether(.1)]; + subjectName = "TestToken"; + subjectSymbol = "TT"; + subjectOwner = otherAccount.address; + subjectMethodologist = methodologist.address; + subjectModules = [setV2Setup.issuanceModule.address, setV2Setup.streamingFeeModule.address]; + subjectOperators = [operatorOne.address, operatorTwo.address]; + subjectAssets = [setV2Setup.dai.address, setV2Setup.wbtc.address]; + subjectExtensions = [mockIssuanceExtension.address, mockFeeExtension.address]; + }); + + async function subject(): Promise { + return await delegatedManagerFactory.createSetAndManager( + subjectComponents, + subjectUnits, + subjectName, + subjectSymbol, + subjectOwner, + subjectMethodologist, + subjectModules, + subjectOperators, + subjectAssets, + subjectExtensions + ); + } + + it("should configure the SetToken correctly", async() => { + const tx = await subject(); + + const setTokenAddress = await protocolUtils.getCreatedSetTokenAddress(tx.hash); + const setToken = await deployer.setV2.getSetToken(setTokenAddress); + + expect(await setToken.getComponents()).deep.eq(subjectComponents); + expect(await setToken.name()).eq(subjectName); + expect(await setToken.symbol()).eq(subjectSymbol); + }); + + it("should set the manager factory as the SetToken manager", async() => { + const tx = await subject(); + + const setTokenAddress = await protocolUtils.getCreatedSetTokenAddress(tx.hash); + const setToken = await deployer.setV2.getSetToken(setTokenAddress); + + expect(await setToken.manager()).eq(delegatedManagerFactory.address); + }); + + it("should configure the DelegatedManager correctly", 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); + + expect(await delegatedManager.setToken()).eq(setTokenAddress); + expect(await delegatedManager.factory()).eq(delegatedManagerFactory.address); + expect(await delegatedManager.methodologist()).eq(delegatedManagerFactory.address); + 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, + subjectUnits, + subjectName, + subjectSymbol, + subjectOwner, + subjectMethodologist, + subjectModules, + subjectOperators, + subjectAssets, + subjectExtensions + ); + + const tx = await subject(); + + const setTokenAddress = await protocolUtils.getCreatedSetTokenAddress(tx.hash); + const initializeParams = await delegatedManagerFactory.initializeState(setTokenAddress); + + expect(initializeParams.deployer).eq(owner.address); + expect(initializeParams.owner).eq(subjectOwner); + expect(initializeParams.methodologist).eq(subjectMethodologist); + expect(initializeParams.isPending).eq(true); + expect(initializeParams.manager).eq(createdContracts[1]); + }); + + it("should emit a DelegatedManagerDeployed event", async() => { + const createdContracts = await delegatedManagerFactory.callStatic.createSetAndManager( + subjectComponents, + subjectUnits, + subjectName, + subjectSymbol, + subjectOwner, + subjectMethodologist, + subjectModules, + subjectOperators, + subjectAssets, + subjectExtensions + ); + + await expect(subject()).to.emit(delegatedManagerFactory, "DelegatedManagerCreated").withArgs( + createdContracts[0], // SetToken + createdContracts[1], // DelegatedManager + owner.address + ); + }); + + describe("when the assets array is non-empty but missing some component elements", async() => { + beforeEach(async() => { + subjectAssets = [setV2Setup.dai.address]; + }); + + it("should revert", async() => { + await expect(subject()).to.be.revertedWith("Asset list must include all components"); + }); + }); + + describe("when the assets array is empty", async() => { + beforeEach(() => { + subjectAssets = []; + }); + + it("should set the intialization state correctly", async() => { + const tx = await subject(); + + const setTokenAddress = await protocolUtils.getCreatedSetTokenAddress(tx.hash); + const initializeParams = await delegatedManagerFactory.initializeState(setTokenAddress); + + expect(initializeParams.isPending).eq(true); + }); + + it("should set the DelegatedManager's useAssetAllowlist to false", 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); + + expect(await delegatedManager.useAssetAllowlist()).eq(false); + }); + }); + + describe("when the extensions array is empty", async() => { + beforeEach(async() => { + subjectExtensions = []; + }); + + it("should revert", async() => { + 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", () => { + let subjectCaller: Account; + let subjectSetToken: Address; + let subjectOwner: Address; + let subjectMethodologist: Address; + let subjectOperators: Address[]; + let subjectAssets: Address[]; + let subjectExtensions: Address[]; + + let components: Address[]; + let units: BigNumber[]; + let modules: Address[]; + + cacheBeforeEach(async() => { + components = [setV2Setup.dai.address]; + units = [ether(1)]; + modules = [setV2Setup.issuanceModule.address, setV2Setup.streamingFeeModule.address]; + + // Deploy EOA managed SetToken + EOAManagedSetToken = await setV2Setup.createSetToken( + components, + units, + modules + ); + + // Initialize modules + await setV2Setup.issuanceModule.initialize(EOAManagedSetToken.address, ADDRESS_ZERO); + + const streamingFeeSettings = { + feeRecipient: owner.address, + maxStreamingFeePercentage: ether(.05), + streamingFeePercentage: ether(.02), + lastStreamingFeeTimestamp: ZERO, + }; + + await setV2Setup.streamingFeeModule.initialize( + EOAManagedSetToken.address, + streamingFeeSettings + ); + + // Set subject variables + subjectSetToken = EOAManagedSetToken.address; + subjectOwner = otherAccount.address; + subjectMethodologist = methodologist.address; + subjectOperators = [operatorOne.address, operatorTwo.address]; + subjectAssets = [setV2Setup.dai.address, setV2Setup.wbtc.address]; + subjectExtensions = [mockIssuanceExtension.address, mockFeeExtension.address]; + }); + + beforeEach(() => { + subjectCaller = owner; + }); + + async function subject(): Promise { + return delegatedManagerFactory.connect(subjectCaller.wallet).createManager( + subjectSetToken, + subjectOwner, + subjectMethodologist, + subjectOperators, + subjectAssets, + subjectExtensions + ); + } + + it("should configure the DelegatedManager correctly", async () => { + await subject(); + + const initializeParams = await delegatedManagerFactory.initializeState(subjectSetToken); + const delegatedManager = await deployer.manager.getDelegatedManager(initializeParams.manager); + + expect(await delegatedManager.setToken()).eq(subjectSetToken); + expect(await delegatedManager.factory()).eq(delegatedManagerFactory.address); + expect(await delegatedManager.methodologist()).eq(delegatedManagerFactory.address); + expect(await delegatedManager.useAssetAllowlist()).eq(true); + }); + + it("should set the intialization state correctly", async() => { + const newManagerAddress = await delegatedManagerFactory.callStatic.createManager( + subjectSetToken, + subjectOwner, + subjectMethodologist, + subjectOperators, + subjectAssets, + subjectExtensions + ); + + await subject(); + + const initializeParams = await delegatedManagerFactory.initializeState(subjectSetToken); + + expect(initializeParams.deployer).eq(owner.address); + expect(initializeParams.owner).eq(subjectOwner); + expect(initializeParams.methodologist).eq(subjectMethodologist); + expect(initializeParams.isPending).eq(true); + 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, + subjectOwner, + subjectMethodologist, + subjectOperators, + subjectAssets, + subjectExtensions + ); + + await expect(subject()).to.emit(delegatedManagerFactory, "DelegatedManagerCreated").withArgs( + subjectSetToken, + managerAddress, + owner.address + ); + }); + + describe("when the caller is not the SetToken manager", async () => { + beforeEach(() => { + subjectCaller = otherAccount; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be manager"); + }); + }); + + describe("when the assets array is non-empty but missing some component elements", async() => { + beforeEach(async() => { + subjectAssets = [setV2Setup.wbtc.address]; + }); + + it("should revert", async() => { + await expect(subject()).to.be.revertedWith("Asset list must include all components"); + }); + }); + + describe("when the assets array is empty", async() => { + beforeEach(() => { + subjectAssets = []; + }); + + it("should set the intialization state correctly", async() => { + await subject(); + + const initializeParams = await delegatedManagerFactory.initializeState(subjectSetToken); + + expect(initializeParams.isPending).eq(true); + }); + + it("should set the DelegatedManager's useAssetAllowlist to false", async () => { + await subject(); + + const initializeParams = await delegatedManagerFactory.initializeState(subjectSetToken); + const delegatedManager = await deployer.manager.getDelegatedManager(initializeParams.manager); + + expect(await delegatedManager.useAssetAllowlist()).eq(false); + }); + }); + + 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 = []; + }); + + it("should revert", async() => { + await expect(subject()).to.be.revertedWith("Must have at least 1 extension"); + }); + }); + + describe("when the SetToken is not controller-enabled", async () => { + beforeEach(() => { + subjectSetToken = otherAccount.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be controller-enabled SetToken"); + }); + }); + }); + + describe("#initialize", () => { + let manager: DelegatedManager; + let initializeParams: any; + let setToken: SetToken; + let setTokenAddress: Address; + + let subjectCaller: Account; + let subjectSetToken: Address; + let subjectOwnerFeeSplit: BigNumber; + let subjectOwnerFeeRecipient: Address; + let subjectExtensions: Address[]; + let subjectInitializeBytecode: string[]; + + beforeEach(() => { + subjectCaller = owner; + subjectOwnerFeeSplit = ether(.5); + subjectOwnerFeeRecipient = otherAccount.address; + subjectExtensions = [mockFeeExtension.address, mockIssuanceExtension.address]; + }); + + async function subject(): Promise { + return await delegatedManagerFactory.connect(subjectCaller.wallet).initialize( + subjectSetToken, + subjectOwnerFeeSplit, + subjectOwnerFeeRecipient, + subjectExtensions, + subjectInitializeBytecode + ); + } + + describe("when the SetToken was created by the factory", () => { + cacheBeforeEach(async () => { + const tx = await create(); + + setTokenAddress = await protocolUtils.getCreatedSetTokenAddress(tx.hash); + initializeParams = await delegatedManagerFactory.initializeState(setTokenAddress); + manager = await deployer.manager.getDelegatedManager(initializeParams.manager); + setToken = await deployer.setV2.getSetToken(setTokenAddress); + + subjectSetToken = setTokenAddress; + }); + + beforeEach(async () => { + subjectInitializeBytecode = await generateBytecode(initializeParams.manager, false); + }); + + it("should initialize the modules", async() => { + await subject(); + + expect(await setToken.moduleStates(mockFeeModule.address)).eq(MODULE_STATE.INITIALIZED); + expect(await setToken.moduleStates(mockIssuanceModule.address)).eq(MODULE_STATE.INITIALIZED); + }); + + it("should initialize the extensions", async() => { + await subject(); + + expect(await manager.isInitializedExtension(mockFeeExtension.address)).eq(true); + expect(await manager.isInitializedExtension(mockIssuanceExtension.address)).eq(true); + }); + + it("should set the ownerFeeSplit on the DelegatedManager", async() => { + await subject(); + + expect(await manager.ownerFeeSplit()).eq(subjectOwnerFeeSplit); + }); + + it("should set the ownerFeeRecipient on the DelegatedManager", async() => { + await subject(); + + expect(await manager.ownerFeeRecipient()).eq(subjectOwnerFeeRecipient); + }); + + it("should set the SetToken's manager to the `manager` specified initializeParams", async () => { + const oldManager = await setToken.manager(); + + await subject(); + + const newManager = await setToken.manager(); + + expect(newManager).not.eq(oldManager); + expect(newManager).eq(initializeParams.manager); + }); + + it("should transfer ownership of DelegatedManager to the `owner` specified initializeState", async () => { + const oldOwner = await manager.owner(); + + await subject(); + + const newOwner = await manager.owner(); + + expect(oldOwner).not.eq(newOwner); + expect(newOwner).eq(initializeParams.owner); + }); + + it("should transfer the methodologist role of DelegatedManager to the `methodologist` specified initializeState", async () => { + const oldMethodologist = await manager.methodologist(); + + await subject(); + + const newMethodologist = await manager.methodologist(); + + expect(oldMethodologist).not.eq(newMethodologist); + expect(newMethodologist).eq(initializeParams.methodologist); + }); + + it("should delete the initializeState for the SetToken", async () => { + await subject(); + + const finalInitializeParams = await delegatedManagerFactory.initializeState(setTokenAddress); + + expect(finalInitializeParams.deployer).eq(ADDRESS_ZERO); + expect(finalInitializeParams.owner).eq(ADDRESS_ZERO); + expect(finalInitializeParams.methodologist).eq(ADDRESS_ZERO); + expect(finalInitializeParams.manager).eq(ADDRESS_ZERO); + expect(finalInitializeParams.isPending).eq(false); + }); + + it("should emit a DelegatedManagerInitialized event", async() => { + await expect(subject()).to.emit(delegatedManagerFactory, "DelegatedManagerInitialized").withArgs( + subjectSetToken, + initializeParams.manager + ); + }); + }); + + describe("when a SetToken is being migrated to a DelegatedManager", async () => { + cacheBeforeEach(async () => { + setToken = await setV2Setup.createSetToken( + [setV2Setup.dai.address], + [ether(1)], + [mockFeeModule.address, mockIssuanceModule.address] + ); + + await create(setToken.address); + + initializeParams = await delegatedManagerFactory.initializeState(setToken.address); + manager = await deployer.manager.getDelegatedManager(initializeParams.manager); + + subjectSetToken = setToken.address; + }); + + beforeEach(async () => { + subjectInitializeBytecode = await generateBytecode(initializeParams.manager, true); + }); + + it("should initialize the extensions", async() => { + await subject(); + + expect(await manager.isInitializedExtension(mockFeeExtension.address)).eq(true); + expect(await manager.isInitializedExtension(mockIssuanceExtension.address)).eq(true); + }); + + it("should set the ownerFeeSplit on the DelegatedManager", async() => { + await subject(); + + expect(await manager.ownerFeeSplit()).eq(subjectOwnerFeeSplit); + }); + + it("should set the ownerFeeRecipient on the DelegatedManager", async() => { + await subject(); + + expect(await manager.ownerFeeRecipient()).eq(subjectOwnerFeeRecipient); + }); + + it("should NOT set the SetToken's manager", async () => { + const oldManager = await setToken.manager(); + + await subject(); + + const newManager = await setToken.manager(); + + expect(newManager).eq(oldManager); + }); + + it("should transfer ownership of DelegateManager to the `owner` specified initializeState", async () => { + const oldOwner = await manager.owner(); + + await subject(); + + const newOwner = await manager.owner(); + + expect(oldOwner).not.eq(newOwner); + expect(newOwner).eq(initializeParams.owner); + }); + + it("should transfer the methodologist role of DelegatedManager to the `methodologist` specified initializeState", async () => { + const oldMethodologist = await manager.methodologist(); + + await subject(); + + const newMethodologist = await manager.methodologist(); + + expect(oldMethodologist).not.eq(newMethodologist); + expect(newMethodologist).eq(initializeParams.methodologist); + }); + + it("should delete the initializeState for the SetToken", async () => { + await subject(); + + const finalInitializeParams = await delegatedManagerFactory.initializeState(setToken.address); + + expect(finalInitializeParams.deployer).eq(ADDRESS_ZERO); + expect(finalInitializeParams.owner).eq(ADDRESS_ZERO); + expect(finalInitializeParams.methodologist).eq(ADDRESS_ZERO); + expect(finalInitializeParams.manager).eq(ADDRESS_ZERO); + expect(finalInitializeParams.isPending).eq(false); + }); + + describe("when the caller tries to initializeModuleAndExtension", async() => { + beforeEach(async () => { + subjectInitializeBytecode = await generateBytecode(initializeParams.manager, false); + }); + + it("should revert", async() => { + await expect(subject()).to.be.revertedWith("Must be the SetToken manager"); + }); + }); + }); + + describe("when the initialization state is not pending", async() => { + it("should revert", async() => { + await expect(subject()).to.be.revertedWith("Manager must be awaiting initialization"); + }); + }); + + describe("when the factory is not approved by the ManagerCore", async() => { + beforeEach(async () => { + await create(); + + 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 an input Extension is not approved by the ManagerCore", async() => { + let mockUnapprovedExtension: BaseGlobalExtensionMock; + + beforeEach(async () => { + await create(); + + mockUnapprovedExtension = await deployer.mocks.deployBaseGlobalExtensionMock(managerCore.address, mockFeeModule.address); + subjectExtensions = [mockUnapprovedExtension.address]; + + subjectInitializeBytecode = [mockUnapprovedExtension.interface.encodeFunctionData( + "initializeExtension", + [manager.address] + )]; + }); + + it("should revert", async() => { + await expect(subject()).to.be.revertedWith("Target must be ManagerCore-enabled Extension"); + }); + }); + + describe("when an initializeBytecode targets the wrong DelegatedManager", async() => { + let otherDelegatedManager: Account; + + beforeEach(async () => { + await create(); + otherDelegatedManager = await getRandomAccount(); + + subjectExtensions = [mockFeeExtension.address]; + subjectInitializeBytecode = [mockFeeExtension.interface.encodeFunctionData( + "initializeExtension", + [otherDelegatedManager.address] + )]; + }); + + it("should revert", async() => { + await expect(subject()).to.be.revertedWith("Must target correct DelegatedManager"); + }); + }); + + describe("when the caller is not the deployer", async() => { + beforeEach(async() => { + await create(); + subjectCaller = otherAccount; + }); + + it("should revert", async() => { + await expect(subject()).to.be.revertedWith("Only deployer can initialize manager"); + }); + }); + + describe("when extensions and initializeBytecodes do not have the same length", async() => { + beforeEach(async () => { + await create(); + subjectInitializeBytecode = []; + }); + + it("should revert", async() => { + await expect(subject()).to.be.revertedWith("Array length mismatch"); + }); + }); + }); +}); diff --git a/test/manager/delegatedManager.spec.ts b/test/manager/delegatedManager.spec.ts new file mode 100644 index 00000000..2b279ff5 --- /dev/null +++ b/test/manager/delegatedManager.spec.ts @@ -0,0 +1,1155 @@ +import "module-alias/register"; + +import { solidityKeccak256 } from "ethers/lib/utils"; +import { BigNumber } from "ethers"; +import { Address, Account, Bytes } from "@utils/types"; +import { ADDRESS_ZERO, EXTENSION_STATE, ZERO } from "@utils/constants"; +import { DelegatedManager, BaseGlobalExtensionMock, ManagerCore } from "@utils/contracts/index"; +import { SetToken } from "@utils/contracts/setV2"; +import DeployHelper from "@utils/deploys"; +import { + addSnapshotBeforeRestoreAfterEach, + ether, + getAccounts, + getWaffleExpect, + getRandomAddress, + getSetFixture, + getRandomAccount, +} from "@utils/index"; +import { ContractTransaction } from "ethers"; +import { getLastBlockTransaction } from "@utils/test/testingUtils"; +import { SetFixture } from "@utils/fixtures"; + +const expect = getWaffleExpect(); + +describe("DelegatedManager", () => { + let owner: Account; + let methodologist: Account; + let otherAccount: Account; + let factory: Account; + let operatorOne: Account; + let operatorTwo: Account; + let fakeExtension: Account; + let newManager: Account; + + let setV2Setup: SetFixture; + + let deployer: DeployHelper; + let setToken: SetToken; + + let managerCore: ManagerCore; + let delegatedManager: DelegatedManager; + let baseExtension: BaseGlobalExtensionMock; + let mockModule: Account; + + before(async () => { + [ + owner, + otherAccount, + methodologist, + factory, + operatorOne, + operatorTwo, + fakeExtension, + newManager, + mockModule, + ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + + setV2Setup = getSetFixture(owner.address); + await setV2Setup.initialize(); + + setToken = await setV2Setup.createSetToken( + [setV2Setup.dai.address], + [ether(1)], + [setV2Setup.issuanceModule.address, setV2Setup.streamingFeeModule.address] + ); + + // Initialize modules + await setV2Setup.issuanceModule.initialize(setToken.address, ADDRESS_ZERO); + const feeRecipient = owner.address; + const maxStreamingFeePercentage = ether(.1); + const streamingFeePercentage = ether(.02); + const streamingFeeSettings = { + feeRecipient, + maxStreamingFeePercentage, + streamingFeePercentage, + lastStreamingFeeTimestamp: ZERO, + }; + await setV2Setup.streamingFeeModule.initialize(setToken.address, streamingFeeSettings); + + managerCore = await deployer.managerCore.deployManagerCore(); + + baseExtension = await deployer.mocks.deployBaseGlobalExtensionMock(managerCore.address, mockModule.address); + + // Deploy DelegatedManager + delegatedManager = await deployer.manager.deployDelegatedManager( + setToken.address, + factory.address, + methodologist.address, + [baseExtension.address], + [operatorOne.address, operatorTwo.address], + [setV2Setup.usdc.address, setV2Setup.weth.address], + true + ); + + // Transfer ownership to DelegatedManager + await setToken.setManager(delegatedManager.address); + + await managerCore.initialize([baseExtension.address], [factory.address]); + await managerCore.connect(factory.wallet).addManager(delegatedManager.address); + }); + + addSnapshotBeforeRestoreAfterEach(); + + describe("#constructor", async () => { + let subjectSetToken: Address; + let subjectFactory: Address; + let subjectMethodologist: Address; + let subjectExtensions: Address[]; + let subjectOperators: Address[]; + let subjectAllowedAssets: Address[]; + let subjectUseAssetAllowlist: boolean; + + beforeEach(async () => { + subjectSetToken = setToken.address; + subjectFactory = factory.address; + subjectMethodologist = methodologist.address; + subjectExtensions = [baseExtension.address]; + subjectOperators = [operatorOne.address, operatorTwo.address]; + subjectAllowedAssets = [setV2Setup.usdc.address, setV2Setup.weth.address]; + subjectUseAssetAllowlist = true; + }); + + async function subject(): Promise { + return await deployer.manager.deployDelegatedManager( + subjectSetToken, + subjectFactory, + subjectMethodologist, + subjectExtensions, + subjectOperators, + subjectAllowedAssets, + subjectUseAssetAllowlist + ); + } + + it("should set the correct SetToken address", async () => { + const delegatedManager = await subject(); + + const actualToken = await delegatedManager.setToken(); + expect (actualToken).to.eq(subjectSetToken); + }); + + it("should set the correct factory address", async () => { + const delegatedManager = await subject(); + + const actualFactory = await delegatedManager.factory(); + expect (actualFactory).to.eq(subjectFactory); + }); + + it("should set the correct Methodologist address", async () => { + const delegatedManager = await subject(); + + const actualMethodologist = await delegatedManager.methodologist(); + expect (actualMethodologist).to.eq(subjectMethodologist); + }); + + it("should set Extension to pending and NOT add to array", async () => { + const delegatedManager = await subject(); + + const actualExtensionArray = await delegatedManager.getExtensions(); + const isApprovedExtension = await delegatedManager.extensionAllowlist(subjectExtensions[0]); + + expect(actualExtensionArray).to.be.empty; + expect(isApprovedExtension).to.eq(EXTENSION_STATE["PENDING"]); + }); + + it("should emit the correct ExtensionAdded events", async () => { + const delegatedManager = await subject(); + + await expect(getLastBlockTransaction()).to.emit(delegatedManager, "ExtensionAdded").withArgs(baseExtension.address); + }); + + it("should set the correct Operators approvals and arrays", async () => { + const delegatedManager = await subject(); + + const actualOperatorsArray = await delegatedManager.getOperators(); + const isApprovedOperatorOne = await delegatedManager.operatorAllowlist(operatorOne.address); + const isApprovedOperatorTwo = await delegatedManager.operatorAllowlist(operatorTwo.address); + + expect(JSON.stringify(actualOperatorsArray)).to.eq(JSON.stringify(subjectOperators)); + expect(isApprovedOperatorOne).to.be.true; + expect(isApprovedOperatorTwo).to.be.true; + }); + + it("should emit the correct OperatorAdded events", async () => { + const delegatedManager = await subject(); + + await expect(getLastBlockTransaction()).to.emit(delegatedManager, "OperatorAdded").withArgs(operatorOne.address); + await expect(getLastBlockTransaction()).to.emit(delegatedManager, "OperatorAdded").withArgs(operatorTwo.address); + }); + + it("should set the correct Allowed assets approvals and arrays", async () => { + const delegatedManager = await subject(); + + const actualAssetsArray = await delegatedManager.getAllowedAssets(); + const isApprovedUSDC = await delegatedManager.assetAllowlist(setV2Setup.usdc.address); + const isApprovedWETH = await delegatedManager.assetAllowlist(setV2Setup.weth.address); + + expect(JSON.stringify(actualAssetsArray)).to.eq(JSON.stringify(subjectAllowedAssets)); + expect(isApprovedUSDC).to.be.true; + expect(isApprovedWETH).to.be.true; + }); + + it("should emit the correct AllowedAssetAdded events", async () => { + const delegatedManager = await subject(); + + await expect(getLastBlockTransaction()).to.emit(delegatedManager, "AllowedAssetAdded").withArgs(setV2Setup.usdc.address); + await expect(getLastBlockTransaction()).to.emit(delegatedManager, "AllowedAssetAdded").withArgs(setV2Setup.weth.address); + }); + + it("should indicate whether to use the asset allow list", async () => { + const delegatedManager = await subject(); + + const useAllowList = await delegatedManager.useAssetAllowlist(); + + expect(useAllowList).to.be.true; + }); + + it("should emit the correct UseAssetAllowlistUpdated event", async () => { + const delegatedManager = await subject(); + + await expect(getLastBlockTransaction()).to.emit(delegatedManager, "UseAssetAllowlistUpdated").withArgs(true); + }); + }); + + describe("#initializeExtension", async () => { + let subjectCaller: Account; + + beforeEach(async () => { + await delegatedManager.addExtensions([otherAccount.address]); + + subjectCaller = otherAccount; + }); + + async function subject(): Promise { + return delegatedManager.connect(subjectCaller.wallet).initializeExtension(); + } + + it("should mark the extension as initialized", async () => { + await subject(); + + const isInitializedExternsion = await delegatedManager.extensionAllowlist(otherAccount.address); + expect(isInitializedExternsion).to.eq(EXTENSION_STATE["INITIALIZED"]); + }); + + it("should emit the correct ExtensionInitialized event for the first address", async () => { + await expect(subject()).to.emit(delegatedManager, "ExtensionInitialized").withArgs(otherAccount.address); + }); + + describe("when the caller is not a pending extension", async () => { + beforeEach(async () => { + subjectCaller = fakeExtension; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Extension must be pending"); + }); + }); + }); + + describe("#interactManager", async () => { + let subjectModule: Address; + let subjectCallData: Bytes; + let subjectCaller: Account; + + beforeEach(async () => { + await delegatedManager.addExtensions([otherAccount.address]); + await delegatedManager.connect(otherAccount.wallet).initializeExtension(); + + subjectModule = setV2Setup.streamingFeeModule.address; + + // Invoke update fee recipient + subjectCallData = setV2Setup.streamingFeeModule.interface.encodeFunctionData("updateFeeRecipient", [ + setToken.address, + otherAccount.address, + ]); + + subjectCaller = otherAccount; + }); + + async function subject(): Promise { + return delegatedManager.connect(subjectCaller.wallet).interactManager( + subjectModule, + subjectCallData + ); + } + + it("should call updateFeeRecipient on the streaming fee module from the SetToken", async () => { + await subject(); + const feeStates = await setV2Setup.streamingFeeModule.feeStates(setToken.address); + expect(feeStates.feeRecipient).to.eq(otherAccount.address); + }); + + describe("when target address is the SetToken", async () => { + beforeEach(async () => { + subjectModule = setToken.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Extensions cannot call SetToken"); + }); + }); + + describe("when the caller is not an initialized extension", async () => { + beforeEach(async () => { + subjectCaller = fakeExtension; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be initialized extension"); + }); + }); + }); + + describe("#transferTokens", async () => { + let subjectCaller: Account; + let subjectToken: Address; + let subjectDestination: Address; + let subjectAmount: BigNumber; + + beforeEach(async () => { + await delegatedManager.connect(owner.wallet).addExtensions([otherAccount.address]); + await delegatedManager.connect(otherAccount.wallet).initializeExtension(); + + subjectCaller = otherAccount; + subjectToken = setV2Setup.weth.address; + subjectDestination = otherAccount.address; + subjectAmount = ether(1); + + await setV2Setup.weth.transfer(delegatedManager.address, subjectAmount); + }); + + async function subject(): Promise { + return delegatedManager.connect(subjectCaller.wallet).transferTokens( + subjectToken, + subjectDestination, + subjectAmount + ); + } + + it("should send the given amount from the manager to the address", async () => { + const preManagerAmount = await setV2Setup.weth.balanceOf(delegatedManager.address); + const preDestinationAmount = await setV2Setup.weth.balanceOf(subjectDestination); + + await subject(); + + const postManagerAmount = await setV2Setup.weth.balanceOf(delegatedManager.address); + const postDestinationAmount = await setV2Setup.weth.balanceOf(subjectDestination); + + expect(preManagerAmount.sub(postManagerAmount)).to.eq(subjectAmount); + expect(postDestinationAmount.sub(preDestinationAmount)).to.eq(subjectAmount); + }); + + describe("when the caller is not an extension", async () => { + beforeEach(async () => { + subjectCaller = operatorOne; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be initialized extension"); + }); + }); + }); + + describe("#addExtensions", async () => { + let subjectExtensions: Address[]; + let subjectCaller: Account; + + beforeEach(async () => { + subjectExtensions = [otherAccount.address, fakeExtension.address]; + subjectCaller = owner; + }); + + async function subject(): Promise { + return delegatedManager.connect(subjectCaller.wallet).addExtensions(subjectExtensions); + } + + it("should NOT add the extensions address", async () => { + const preExtensions = await delegatedManager.getExtensions(); + + await subject(); + + const postExtensions = await delegatedManager.getExtensions(); + + expect(JSON.stringify(preExtensions)).to.eq(JSON.stringify(postExtensions)); + }); + + it("should set the extension mapping", async () => { + await subject(); + const isExtensionOne = await delegatedManager.extensionAllowlist(otherAccount.address); + const isExtensionTwo = await delegatedManager.extensionAllowlist(fakeExtension.address); + + expect(isExtensionOne).to.eq(EXTENSION_STATE["PENDING"]); + expect(isExtensionTwo).to.eq(EXTENSION_STATE["PENDING"]); + }); + + it("should emit the correct ExtensionAdded event for the first address", async () => { + await expect(subject()).to.emit(delegatedManager, "ExtensionAdded").withArgs(otherAccount.address); + }); + + it("should emit the correct ExtensionAdded event for the second address", async () => { + await expect(subject()).to.emit(delegatedManager, "ExtensionAdded").withArgs(fakeExtension.address); + }); + + describe("when the extension already exists", async () => { + beforeEach(async () => { + subjectExtensions = [baseExtension.address]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Extension already exists"); + }); + }); + + describe("when the caller is not the owner", async () => { + beforeEach(async () => { + subjectCaller = methodologist; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Ownable: caller is not the owner"); + }); + }); + }); + + describe("#removeExtensions", async () => { + let subjectExtensions: Address[]; + let subjectCaller: Account; + + beforeEach(async () => { + await baseExtension.connect(owner.wallet).initializeExtension( + delegatedManager.address + ); + + subjectExtensions = [baseExtension.address]; + subjectCaller = owner; + }); + + async function subject(): Promise { + return delegatedManager.connect(subjectCaller.wallet).removeExtensions(subjectExtensions); + } + + it("should remove the extension address", async () => { + await subject(); + const extensions = await delegatedManager.getExtensions(); + + expect(extensions.length).to.eq(0); + }); + + it("should set the extension mapping", async () => { + const preIsExtensionOne = await delegatedManager.extensionAllowlist(baseExtension.address); + + expect(preIsExtensionOne).to.eq(EXTENSION_STATE["INITIALIZED"]); + + await subject(); + + const postIsExtensionOne = await delegatedManager.extensionAllowlist(baseExtension.address); + + expect(postIsExtensionOne).to.eq(EXTENSION_STATE["NONE"]); + }); + + it("should emit the correct ExtensionRemoved event for the first address", async () => { + await expect(subject()).to.emit(delegatedManager, "ExtensionRemoved").withArgs(baseExtension.address); + }); + + describe("when the extension does not exist", async () => { + beforeEach(async () => { + subjectExtensions = [await getRandomAddress()]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Extension not initialized"); + }); + }); + + describe("when the caller is not the owner", async () => { + beforeEach(async () => { + subjectCaller = methodologist; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Ownable: caller is not the owner"); + }); + }); + }); + + describe("#addOperators", async () => { + let subjectOperators: Address[]; + let subjectCaller: Account; + + beforeEach(async () => { + subjectOperators = [otherAccount.address]; + subjectCaller = owner; + }); + + async function subject(): Promise { + return delegatedManager.connect(subjectCaller.wallet).addOperators(subjectOperators); + } + + it("should add the operator address", async () => { + await subject(); + const operators = await delegatedManager.getOperators(); + + expect(operators[2]).to.eq(otherAccount.address); + }); + + it("should set the operator mapping", async () => { + await subject(); + const isOperatorOne = await delegatedManager.operatorAllowlist(otherAccount.address); + + expect(isOperatorOne).to.be.true; + }); + + it("should emit the correct OperatorAdded event", async () => { + await expect(subject()).to.emit(delegatedManager, "OperatorAdded").withArgs(otherAccount.address); + }); + + describe("when the operator already exists", async () => { + beforeEach(async () => { + subjectOperators = [operatorOne.address]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Operator already added"); + }); + }); + + describe("when the caller is not the owner", async () => { + beforeEach(async () => { + subjectCaller = methodologist; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Ownable: caller is not the owner"); + }); + }); + }); + + describe("#removeOperators", async () => { + let subjectOperators: Address[]; + let subjectCaller: Account; + + beforeEach(async () => { + subjectOperators = [operatorOne.address, operatorTwo.address]; + subjectCaller = owner; + }); + + async function subject(): Promise { + return delegatedManager.connect(subjectCaller.wallet).removeOperators(subjectOperators); + } + + it("should remove the operator addresses", async () => { + await subject(); + const operators = await delegatedManager.getOperators(); + + expect(operators).to.be.empty; + }); + + it("should set the operator mapping", async () => { + await subject(); + const isOperatorOne = await delegatedManager.operatorAllowlist(operatorOne.address); + const isOperatorTwo = await delegatedManager.operatorAllowlist(operatorTwo.address); + + expect(isOperatorOne).to.be.false; + expect(isOperatorTwo).to.be.false; + }); + + it("should emit the correct OperatorRemoved event for the first address", async () => { + await expect(subject()).to.emit(delegatedManager, "OperatorRemoved").withArgs(operatorOne.address); + }); + + it("should emit the correct OperatorRemoved event for the second address", async () => { + await expect(subject()).to.emit(delegatedManager, "OperatorRemoved").withArgs(operatorTwo.address); + }); + + describe("when the operator hasn't been added", async () => { + beforeEach(async () => { + subjectOperators = [otherAccount.address]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Operator not already added"); + }); + }); + + describe("when the caller is not the owner", async () => { + beforeEach(async () => { + subjectCaller = methodologist; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Ownable: caller is not the owner"); + }); + }); + }); + + describe("#addAllowedAssets", async () => { + let subjectAssets: Address[]; + let subjectCaller: Account; + + beforeEach(async () => { + subjectAssets = [setV2Setup.wbtc.address]; + subjectCaller = owner; + }); + + async function subject(): Promise { + return delegatedManager.connect(subjectCaller.wallet).addAllowedAssets(subjectAssets); + } + + it("should add the asset address", async () => { + await subject(); + const assets = await delegatedManager.getAllowedAssets(); + + expect(assets[2]).to.eq(setV2Setup.wbtc.address); + }); + + it("should set the allowed asset mapping", async () => { + await subject(); + + const isApprovedWBTC = await delegatedManager.assetAllowlist(setV2Setup.wbtc.address); + + expect(isApprovedWBTC).to.be.true; + }); + + it("should emit the correct AllowedAssetAdded event", async () => { + await expect(subject()).to.emit(delegatedManager, "AllowedAssetAdded").withArgs(setV2Setup.wbtc.address); + }); + + describe("when the asset already exists", async () => { + beforeEach(async () => { + subjectAssets = [setV2Setup.weth.address]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Asset already added"); + }); + }); + + describe("when the caller is not the owner", async () => { + beforeEach(async () => { + subjectCaller = methodologist; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Ownable: caller is not the owner"); + }); + }); + }); + + describe("#removeAllowedAssets", async () => { + let subjectAssets: Address[]; + let subjectCaller: Account; + + beforeEach(async () => { + subjectAssets = [setV2Setup.weth.address, setV2Setup.usdc.address]; + subjectCaller = owner; + }); + + async function subject(): Promise { + return delegatedManager.connect(subjectCaller.wallet).removeAllowedAssets(subjectAssets); + } + + it("should remove the asset addresses", async () => { + await subject(); + const assets = await delegatedManager.getAllowedAssets(); + + expect(assets).to.be.empty; + }); + + it("should set the asset mapping", async () => { + await subject(); + const isApprovedWETH = await delegatedManager.assetAllowlist(setV2Setup.weth.address); + const isApprovedUSDC = await delegatedManager.assetAllowlist(setV2Setup.usdc.address); + + expect(isApprovedWETH).to.be.false; + expect(isApprovedUSDC).to.be.false; + }); + + it("should emit the correct AllowedAssetRemoved event for the first address", async () => { + await expect(subject()).to.emit(delegatedManager, "AllowedAssetRemoved").withArgs(setV2Setup.weth.address); + }); + + it("should emit the correct AllowedAssetRemoved event for the second address", async () => { + await expect(subject()).to.emit(delegatedManager, "AllowedAssetRemoved").withArgs(setV2Setup.usdc.address); + }); + + describe("when the asset hasn't been added", async () => { + beforeEach(async () => { + subjectAssets = [otherAccount.address]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Asset not already added"); + }); + }); + + describe("when the caller is not the owner", async () => { + beforeEach(async () => { + subjectCaller = methodologist; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Ownable: caller is not the owner"); + }); + }); + }); + + describe("#updateUseAssetAllowlist", async () => { + let subjectUseAssetAllowlist: boolean; + let subjectCaller: Account; + + beforeEach(async () => { + subjectUseAssetAllowlist = false; + subjectCaller = owner; + }); + + async function subject(): Promise { + return delegatedManager.connect(subjectCaller.wallet).updateUseAssetAllowlist(subjectUseAssetAllowlist); + } + + it("should update the callAllowList", async () => { + await subject(); + const useAssetAllowlist = await delegatedManager.useAssetAllowlist(); + expect(useAssetAllowlist).to.be.false; + }); + + it("should emit UseAssetAllowlistUpdated event", async () => { + await expect(subject()).to.emit(delegatedManager, "UseAssetAllowlistUpdated").withArgs( + subjectUseAssetAllowlist + ); + }); + + describe("when the sender is not operator", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Ownable: caller is not the owner"); + }); + }); + }); + + describe("#setMethodologist", async () => { + let subjectNewMethodologist: Address; + let subjectCaller: Account; + + beforeEach(async () => { + subjectNewMethodologist = await getRandomAddress(); + subjectCaller = methodologist; + }); + + async function subject(): Promise { + return delegatedManager.connect(subjectCaller.wallet).setMethodologist(subjectNewMethodologist); + } + + it("should set the new methodologist", async () => { + await subject(); + const actualIndexModule = await delegatedManager.methodologist(); + expect(actualIndexModule).to.eq(subjectNewMethodologist); + }); + + it("should emit the correct MethodologistChanged event", async () => { + await expect(subject()).to.emit(delegatedManager, "MethodologistChanged").withArgs(subjectNewMethodologist); + }); + + describe("when the caller is not the methodologist", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be methodologist"); + }); + }); + + describe("when passed methodologist is the zero address", async () => { + beforeEach(async () => { + subjectNewMethodologist = ADDRESS_ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Null address passed"); + }); + }); + }); + + describe("#updateOwnerFeeSplit", async () => { + let subjectNewFeeSplit: BigNumber; + let subjectOwnerCaller: Account; + let subjectMethodologistCaller: Account; + + beforeEach(async () => { + subjectNewFeeSplit = ether(.1); + subjectOwnerCaller = owner; + subjectMethodologistCaller = methodologist; + }); + + async function subject(caller: Account): Promise { + return await delegatedManager.connect(caller.wallet).updateOwnerFeeSplit(subjectNewFeeSplit); + } + + it("should set the new owner fee split", async () => { + await subject(subjectOwnerCaller); + await subject(subjectMethodologistCaller); + + const newFeeSplit = await delegatedManager.ownerFeeSplit(); + + expect(newFeeSplit).to.eq(subjectNewFeeSplit); + }); + + it("should emit the correct OwnerFeeSplitUpdated event", async () => { + await subject(subjectOwnerCaller); + await expect(subject(subjectMethodologistCaller)).to.emit(delegatedManager, "OwnerFeeSplitUpdated").withArgs(subjectNewFeeSplit); + }); + + describe("when a fee split greater than 100% is passed", async () => { + beforeEach(async () => { + subjectNewFeeSplit = ether(1.1); + }); + + it("should revert", async () => { + await subject(subjectOwnerCaller); + await expect(subject(subjectMethodologistCaller)).to.be.revertedWith("Invalid fee split"); + }); + }); + + context("when a single mutual upgrade party has called the method", async () => { + it("should log the proposed streaming fee hash in the mutualUpgrades mapping", async () => { + const txHash = await subject(subjectOwnerCaller); + + const expectedHash = solidityKeccak256( + ["bytes", "address"], + [txHash.data, subjectOwnerCaller.address] + ); + + const isLogged = await delegatedManager.mutualUpgrades(expectedHash); + + expect(isLogged).to.be.true; + }); + + it("should not update fee split", async () => { + await subject(subjectOwnerCaller); + + const feeSplit = await delegatedManager.ownerFeeSplit(); + + expect(feeSplit).to.eq(ZERO); + }); + }); + + describe("when the caller is not the owner or methodologist", async () => { + beforeEach(async () => { + subjectOwnerCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject(subjectOwnerCaller)).to.be.revertedWith("Must be authorized address"); + }); + }); + }); + + describe("#updateOwnerFeeRecipient", async () => { + let subjectNewFeeRecipient: Address; + let subjectCaller: Account; + + beforeEach(async () => { + subjectNewFeeRecipient = owner.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return delegatedManager.connect(subjectCaller.wallet).updateOwnerFeeRecipient(subjectNewFeeRecipient); + } + + it("should set the new owner fee recipient", async () => { + const currentFeeRecipient = await delegatedManager.ownerFeeRecipient(); + + expect(currentFeeRecipient).to.eq(ADDRESS_ZERO); + + await subject(); + + const newFeeRecipient = await delegatedManager.ownerFeeRecipient(); + + expect(newFeeRecipient).to.eq(subjectNewFeeRecipient); + }); + + it("should emit the correct OwnerFeeRecipientUpdated event", async () => { + await expect(subject()).to.emit(delegatedManager, "OwnerFeeRecipientUpdated").withArgs(subjectNewFeeRecipient); + }); + + describe("when the fee recipient is the zero address", async () => { + beforeEach(async () => { + subjectNewFeeRecipient = ADDRESS_ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Null address passed"); + }); + }); + + describe("when the caller 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("#addModule", async () => { + let subjectModule: Address; + let subjectCaller: Account; + + beforeEach(async () => { + await setV2Setup.controller.addModule(otherAccount.address); + + subjectModule = otherAccount.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return delegatedManager.connect(subjectCaller.wallet).addModule(subjectModule); + } + + it("should add the module to the SetToken", async () => { + await subject(); + const isModule = await setToken.isPendingModule(subjectModule); + expect(isModule).to.eq(true); + }); + + describe("when the caller 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("#removeModule", async () => { + let subjectModule: Address; + let subjectCaller: Account; + + beforeEach(async () => { + subjectModule = setV2Setup.streamingFeeModule.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return delegatedManager.connect(subjectCaller.wallet).removeModule(subjectModule); + } + + it("should remove the module from the SetToken", async () => { + await subject(); + const isModule = await setToken.isInitializedModule(subjectModule); + expect(isModule).to.eq(false); + }); + + describe("when the caller 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("#setManager", async () => { + let subjectNewManager: Address; + let subjectCaller: Account; + + beforeEach(async () => { + subjectNewManager = newManager.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return delegatedManager.connect(subjectCaller.wallet).setManager(subjectNewManager); + } + + it("should change the manager address", async () => { + await subject(); + const manager = await setToken.manager(); + + expect(manager).to.eq(newManager.address); + }); + + describe("when manager still has extension initialized", async () => { + beforeEach(async () => { + await baseExtension.initializeExtension(delegatedManager.address); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must remove all extensions"); + }); + }); + + describe("when passed manager is the zero address", async () => { + beforeEach(async () => { + subjectNewManager = ADDRESS_ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Zero address not valid"); + }); + }); + + describe("when the caller is not the owner", async () => { + beforeEach(async () => { + subjectCaller = methodologist; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Ownable: caller is not the owner"); + }); + }); + }); + + describe("#isAllowedAsset", async () => { + let subjectAsset: Address; + + beforeEach(async () => { + subjectAsset = setV2Setup.usdc.address; + }); + + async function subject(): Promise { + return delegatedManager.isAllowedAsset(subjectAsset); + } + + it("should return true", async () => { + const isAllowAsset = await subject(); + + expect(isAllowAsset).to.be.true; + }); + + describe("when useAssetAllowlist is flipped to false", async () => { + beforeEach(async () => { + await delegatedManager.connect(owner.wallet).updateUseAssetAllowlist(false); + + subjectAsset = setV2Setup.wbtc.address; + }); + + it("should return true", async () => { + const isAllowAsset = await subject(); + + expect(isAllowAsset).to.be.true; + }); + }); + + describe("when the asset is not on allowlist", async () => { + beforeEach(async () => { + subjectAsset = setV2Setup.wbtc.address; + }); + + it("should return false", async () => { + const isAllowAsset = await subject(); + + expect(isAllowAsset).to.be.false; + }); + }); + }); + + describe("#isPendingExtension", async () => { + let subjectExtension: Address; + + beforeEach(async () => { + subjectExtension = baseExtension.address; + }); + + async function subject(): Promise { + return delegatedManager.isPendingExtension(subjectExtension); + } + + it("should return true", async () => { + const isPendingExtension = await subject(); + + expect(isPendingExtension).to.be.true; + }); + + describe("when extension is initialized", async () => { + beforeEach(async () => { + await baseExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + }); + + it("should return false", async () => { + const isPendingExtension = await subject(); + + expect(isPendingExtension).to.be.false; + }); + }); + + describe("when the extension is not tracked in allowlist", async () => { + beforeEach(async () => { + await baseExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + await delegatedManager.connect(owner.wallet).removeExtensions([baseExtension.address]); + }); + + it("should return false", async () => { + const isPendingExtension = await subject(); + + expect(isPendingExtension).to.be.false; + }); + }); + }); + + describe("#isInitializedExtension", async () => { + let subjectExtension: Address; + + beforeEach(async () => { + subjectExtension = baseExtension.address; + }); + + async function subject(): Promise { + return delegatedManager.isInitializedExtension(subjectExtension); + } + + it("should return true", async () => { + const isInitializedExtension = await subject(); + + expect(isInitializedExtension).to.be.false; + }); + + describe("when extension is initialized", async () => { + beforeEach(async () => { + await baseExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + }); + + it("should return false", async () => { + const isInitializedExtension = await subject(); + + expect(isInitializedExtension).to.be.true; + }); + }); + + describe("when the extension is not tracked in allowlist", async () => { + beforeEach(async () => { + await baseExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + await delegatedManager.connect(owner.wallet).removeExtensions([baseExtension.address]); + }); + + it("should return false", async () => { + const isInitializedExtension = await subject(); + + expect(isInitializedExtension).to.be.false; + }); + }); + }); +}); diff --git a/test/managerCore.spec.ts b/test/managerCore.spec.ts new file mode 100644 index 00000000..7c2b4fec --- /dev/null +++ b/test/managerCore.spec.ts @@ -0,0 +1,602 @@ +import "module-alias/register"; + +import { Account, Address } from "@utils/types"; +import { ADDRESS_ZERO } from "@utils/constants"; +import { + DelegatedManagerFactory, + ManagerCore, + BaseGlobalExtensionMock +} from "@utils/contracts/index"; +import DeployHelper from "@utils/deploys"; +import { + addSnapshotBeforeRestoreAfterEach, + getAccounts, + getWaffleExpect, + getRandomAccount, + getSetFixture, +} from "@utils/index"; +import { SetFixture } from "@utils/fixtures"; + + +const expect = getWaffleExpect(); + +describe("ManagerCore", () => { + let owner: Account; + let mockDelegatedManagerFactory: Account; + let mockManager: Account; + let mockModule: Account; + + let deployer: DeployHelper; + let setV2Setup: SetFixture; + + let managerCore: ManagerCore; + let delegatedManagerFactory: DelegatedManagerFactory; + let mockExtension: BaseGlobalExtensionMock; + + before(async () => { + [ + owner, + mockDelegatedManagerFactory, + mockManager, + mockModule, + ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + + setV2Setup = getSetFixture(owner.address); + await setV2Setup.initialize(); + + managerCore = await deployer.managerCore.deployManagerCore(); + + mockExtension = await deployer.mocks.deployBaseGlobalExtensionMock(managerCore.address, mockModule.address); + + delegatedManagerFactory = await deployer.factories.deployDelegatedManagerFactory( + managerCore.address, + setV2Setup.controller.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 subjectExtensions: Address[]; + let subjectFactories: Address[]; + + beforeEach(async () => { + subjectCaller = owner; + subjectExtensions = [mockExtension.address]; + subjectFactories = [delegatedManagerFactory.address]; + }); + + async function subject(): Promise { + return await managerCore.connect(subjectCaller.wallet).initialize( + subjectExtensions, + subjectFactories + ); + } + + it("should have set the correct extensions length of 1", async () => { + await subject(); + + const extensions = await managerCore.getExtensions(); + expect(extensions.length).to.eq(1); + }); + + it("should have a valid extension", async () => { + await subject(); + + const validExtension = await managerCore.isExtension(mockExtension.address); + expect(validExtension).to.eq(true); + }); + + it("should emit the ExtensionAdded event", async () => { + await expect(subject()).to.emit(managerCore, "ExtensionAdded").withArgs(mockExtension.address); + }); + + 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 emit the FactoryAdded event", async () => { + await expect(subject()).to.emit(managerCore, "FactoryAdded").withArgs(delegatedManagerFactory.address); + }); + + 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 extension", async () => { + beforeEach(async () => { + subjectExtensions = [ADDRESS_ZERO]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Zero address submitted."); + }); + }); + + 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 zero address passed for a factory", async () => { + beforeEach(async () => { + subjectFactory = ADDRESS_ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Zero address submitted."); + }); + }); + + 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"); + }); + }); + }); + + describe("#addExtension", async () => { + let subjectExtension: Address; + let subjectCaller: Account; + let subjectManagerCore: ManagerCore; + + beforeEach(async () => { + await managerCore.initialize([], []); + + subjectExtension = mockExtension.address; + subjectCaller = owner; + subjectManagerCore = managerCore; + }); + + async function subject(): Promise { + return await subjectManagerCore.connect(subjectCaller.wallet).addExtension(subjectExtension); + } + + it("should be stored in the extensions array", async () => { + await subject(); + + const extensions = await managerCore.getExtensions(); + expect(extensions.length).to.eq(1); + }); + + it("should be returned as a valid extension", async () => { + await subject(); + + const validExtension = await managerCore.isExtension(mockExtension.address); + expect(validExtension).to.eq(true); + }); + + it("should emit the ExtensionAdded event", async () => { + await expect(subject()).to.emit(managerCore, "ExtensionAdded").withArgs(subjectExtension); + }); + + 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 zero address passed for an extension", async () => { + beforeEach(async () => { + subjectExtension = ADDRESS_ZERO; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Zero address submitted."); + }); + }); + + describe("when the extension already exists", async () => { + beforeEach(async () => { + await subject(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Extension already exists"); + }); + }); + }); + + describe("#removeExtension", async () => { + let subjectExtension: Address; + let subjectCaller: Account; + let subjectManagerCore: ManagerCore; + + beforeEach(async () => { + await managerCore.initialize([mockExtension.address], []); + + subjectExtension = mockExtension.address; + subjectCaller = owner; + subjectManagerCore = managerCore; + }); + + async function subject(): Promise { + return await subjectManagerCore.connect(subjectCaller.wallet).removeExtension(subjectExtension); + } + + it("should remove extension from extensions array", async () => { + await subject(); + + const extensions = await managerCore.getExtensions(); + expect(extensions.length).to.eq(0); + }); + + it("should return false as a valid extension", async () => { + await subject(); + + const validExtension = await managerCore.isExtension(mockExtension.address); + expect(validExtension).to.eq(false); + }); + + it("should emit the ExtensionRemoved event", async () => { + await expect(subject()).to.emit(managerCore, "ExtensionRemoved").withArgs(subjectExtension); + }); + + 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 extension does not exist", async () => { + beforeEach(async () => { + await subject(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Extension does not exist"); + }); + }); + }); +}); diff --git a/utils/constants.ts b/utils/constants.ts index ad6027ca..f20b8fac 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -3,6 +3,18 @@ import { BigNumber } from "@ethersproject/bignumber"; const { AddressZero, MaxUint256, One, Two, Zero } = constants; +export const MODULE_STATE = { + "NONE": 0, + "PENDING": 1, + "INITIALIZED": 2, +}; + +export const EXTENSION_STATE = { + "NONE": 0, + "PENDING": 1, + "INITIALIZED": 2, +}; + export const ADDRESS_ZERO = AddressZero; export const ETH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; export const EMPTY_BYTES = "0x"; diff --git a/utils/contracts/index.ts b/utils/contracts/index.ts index 655840ec..19e012b1 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -48,3 +48,16 @@ export { WrapExtension } from "../../typechain/WrapExtension"; export { WrappedfCashMock } from "../../typechain/WrappedfCashMock"; export { WrappedfCashFactoryMock } from "../../typechain/WrappedfCashFactoryMock"; export { ZeroExExchangeProxyMock } from "../../typechain/ZeroExExchangeProxyMock"; +export { BaseGlobalExtensionMock } from "../../typechain/BaseGlobalExtensionMock"; +export { DelegatedManager } from "../../typechain/DelegatedManager"; +export { DelegatedManagerFactory } from "../../typechain/DelegatedManagerFactory"; +export { ManagerCore } from "../../typechain/ManagerCore"; +export { ManagerMock } from "../../typechain/ManagerMock"; +export { ModuleMock } from "../../typechain/ModuleMock"; +export { MutualUpgradeV2Mock } from "../../typechain/MutualUpgradeV2Mock"; +export { GlobalTradeExtension } from "../../typechain/GlobalTradeExtension"; +export { GlobalIssuanceExtension } from "../../typechain/GlobalIssuanceExtension"; +export { GlobalStreamingFeeSplitExtension } from "../../typechain/GlobalStreamingFeeSplitExtension"; +export { GlobalBatchTradeExtension } from "../../typechain/GlobalBatchTradeExtension"; +export { GlobalWrapExtension } from "../../typechain/GlobalWrapExtension"; +export { GlobalClaimExtension } from "../../typechain/GlobalClaimExtension"; diff --git a/utils/deploys/deployFactories.ts b/utils/deploys/deployFactories.ts new file mode 100644 index 00000000..7587b29d --- /dev/null +++ b/utils/deploys/deployFactories.ts @@ -0,0 +1,27 @@ +import { Signer } from "ethers"; +import { + Address +} from "../types"; + +import { DelegatedManagerFactory } from "../contracts/index"; +import { DelegatedManagerFactory__factory } from "../../typechain/factories/DelegatedManagerFactory__factory"; + +export default class DeployFactories { + private _deployerSigner: Signer; + + constructor(deployerSigner: Signer) { + this._deployerSigner = deployerSigner; + } + + public async deployDelegatedManagerFactory( + managerCore: Address, + controller: Address, + setTokenFactory: Address + ): Promise { + return await new DelegatedManagerFactory__factory(this._deployerSigner).deploy( + managerCore, + controller, + setTokenFactory + ); + } +} diff --git a/utils/deploys/deployGlobalExtensions.ts b/utils/deploys/deployGlobalExtensions.ts new file mode 100644 index 00000000..b148e42e --- /dev/null +++ b/utils/deploys/deployGlobalExtensions.ts @@ -0,0 +1,91 @@ +import { Signer } from "ethers"; +import { Address } from "../types"; +import { + GlobalBatchTradeExtension, + GlobalClaimExtension, + GlobalIssuanceExtension, + GlobalStreamingFeeSplitExtension, + GlobalTradeExtension, + GlobalWrapExtension +} from "../contracts/index"; + +import { GlobalBatchTradeExtension__factory } from "../../typechain/factories/GlobalBatchTradeExtension__factory"; +import { GlobalClaimExtension__factory } from "../../typechain/factories/GlobalClaimExtension__factory"; +import { GlobalIssuanceExtension__factory } from "../../typechain/factories/GlobalIssuanceExtension__factory"; +import { GlobalStreamingFeeSplitExtension__factory } from "../../typechain/factories/GlobalStreamingFeeSplitExtension__factory"; +import { GlobalTradeExtension__factory } from "../../typechain/factories/GlobalTradeExtension__factory"; +import { GlobalWrapExtension__factory } from "../../typechain/factories/GlobalWrapExtension__factory"; + +export default class DeployGlobalExtensions { + private _deployerSigner: Signer; + + constructor(deployerSigner: Signer) { + this._deployerSigner = deployerSigner; + } + + public async deployGlobalBatchTradeExtension( + managerCore: Address, + tradeModule: Address, + integrations: string[] + ): Promise { + return await new GlobalBatchTradeExtension__factory(this._deployerSigner).deploy( + managerCore, + tradeModule, + integrations + ); + } + + public async deployGlobalClaimExtension( + managerCore: Address, + airdropModule: Address, + claimModule: Address, + integrationRegistry: Address + ): Promise { + return await new GlobalClaimExtension__factory(this._deployerSigner).deploy( + managerCore, + airdropModule, + claimModule, + integrationRegistry + ); + } + + public async deployGlobalIssuanceExtension( + managerCore: Address, + basicIssuanceModule: Address + ): Promise { + return await new GlobalIssuanceExtension__factory(this._deployerSigner).deploy( + managerCore, + basicIssuanceModule, + ); + } + + public async deployGlobalStreamingFeeSplitExtension( + managerCore: Address, + streamingFeeModule: Address + ): Promise { + return await new GlobalStreamingFeeSplitExtension__factory(this._deployerSigner).deploy( + managerCore, + streamingFeeModule, + ); + } + + public async deployGlobalTradeExtension( + managerCore: Address, + tradeModule: Address + ): Promise { + return await new GlobalTradeExtension__factory(this._deployerSigner).deploy( + managerCore, + tradeModule, + ); + } + + public async deployGlobalWrapExtension( + managerCore: Address, + wrapModule: Address + ): Promise { + return await new GlobalWrapExtension__factory(this._deployerSigner).deploy( + managerCore, + wrapModule, + ); + } +} diff --git a/utils/deploys/deployManager.ts b/utils/deploys/deployManager.ts index 60550867..1f996fee 100644 --- a/utils/deploys/deployManager.ts +++ b/utils/deploys/deployManager.ts @@ -1,11 +1,12 @@ import { Signer } from "ethers"; import { BigNumber } from "@ethersproject/bignumber"; import { Address } from "../types"; -import { ICManager, BaseManager, BaseManagerV2 } from "../contracts/index"; +import { ICManager, BaseManager, BaseManagerV2, DelegatedManager } from "../contracts/index"; import { ICManager__factory } from "../../typechain/factories/ICManager__factory"; import { BaseManager__factory } from "../../typechain/factories/BaseManager__factory"; import { BaseManagerV2__factory } from "../../typechain/factories/BaseManagerV2__factory"; +import { DelegatedManager__factory } from "../../typechain/factories/DelegatedManager__factory"; export default class DeployToken { private _deployerSigner: Signer; @@ -55,4 +56,30 @@ export default class DeployToken { methodologist ); } -} \ No newline at end of file + + public async deployDelegatedManager( + setToken: Address, + factory: Address, + methodologist: Address, + extensions: Address[], + operators: Address[], + allowedAssets: Address[], + useAssetAllowlist: boolean + ): Promise { + return await new DelegatedManager__factory(this._deployerSigner).deploy( + setToken, + factory, + methodologist, + extensions, + operators, + allowedAssets, + useAssetAllowlist + ); + } + + /* GETTERS */ + + public async getDelegatedManager(managerAddress: Address): Promise { + return await new DelegatedManager__factory(this._deployerSigner).attach(managerAddress); + } +} diff --git a/utils/deploys/deployManagerCore.ts b/utils/deploys/deployManagerCore.ts new file mode 100644 index 00000000..3c40406f --- /dev/null +++ b/utils/deploys/deployManagerCore.ts @@ -0,0 +1,16 @@ +import { Signer } from "ethers"; + +import { ManagerCore } from "../contracts/index"; +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(); + } +} diff --git a/utils/deploys/deployMocks.ts b/utils/deploys/deployMocks.ts index ea3c94b4..59f290ab 100644 --- a/utils/deploys/deployMocks.ts +++ b/utils/deploys/deployMocks.ts @@ -15,10 +15,14 @@ import { WrappedfCashFactoryMock, ZeroExExchangeProxyMock, DEXAdapter, + ModuleMock, + BaseGlobalExtensionMock, } from "../contracts/index"; import { BaseExtensionMock__factory } from "../../typechain/factories/BaseExtensionMock__factory"; import { DEXAdapter__factory } from "../../typechain/factories/DEXAdapter__factory"; +import { ModuleMock__factory } from "../../typechain/factories/ModuleMock__factory"; +import { BaseGlobalExtensionMock__factory } from "../../typechain/factories/BaseGlobalExtensionMock__factory"; import { convertLibraryNameToLinkId } from "../common"; import { ChainlinkAggregatorV3Mock__factory } from "../../typechain/factories/ChainlinkAggregatorV3Mock__factory"; import { FLIStrategyExtensionMock__factory } from "../../typechain/factories/FLIStrategyExtensionMock__factory"; @@ -50,6 +54,10 @@ export default class DeployMocks { return await new BaseExtensionMock__factory(this._deployerSigner).deploy(manager); } + public async deployBaseGlobalExtensionMock(managerCore: Address, module: Address): Promise { + return await new BaseGlobalExtensionMock__factory(this._deployerSigner).deploy(managerCore, module); + } + public async deployTradeAdapterMock(): Promise { return await new TradeAdapterMock__factory(this._deployerSigner).deploy(); } @@ -149,6 +157,10 @@ export default class DeployMocks { return await new DEXAdapter__factory(this._deployerSigner).deploy(); } + public async deployModuleMock(controller: Address): Promise { + return await new ModuleMock__factory(this._deployerSigner).deploy(controller); + } + public async deployFlashMintLeveragedCompMock( wethAddress: Address, quickRouterAddress: Address, diff --git a/utils/deploys/deploySetV2.ts b/utils/deploys/deploySetV2.ts index 648ce2e3..fc7c583e 100644 --- a/utils/deploys/deploySetV2.ts +++ b/utils/deploys/deploySetV2.ts @@ -79,6 +79,10 @@ export default class DeploySetV2 { return await new Compound__factory(this._deployerSigner).deploy(); } + public async getSetToken(setTokenAddress: Address): Promise { + return await new SetToken__factory(this._deployerSigner).attach(setTokenAddress); + } + public async deploySetToken( _components: Address[], _units: BigNumberish[], diff --git a/utils/deploys/index.ts b/utils/deploys/index.ts index 37a1348c..83632b46 100644 --- a/utils/deploys/index.ts +++ b/utils/deploys/index.ts @@ -1,6 +1,9 @@ import { Signer } from "ethers"; import DeployManager from "./deployManager"; +import DeployManagerCore from "./deployManagerCore"; +import DeployGlobalExtensions from "./deployGlobalExtensions"; +import DeployFactories from "./deployFactories"; import DeployMocks from "./deployMocks"; import DeployToken from "./deployToken"; import DeploySetV2 from "./deploySetV2"; @@ -15,6 +18,9 @@ export default class DeployHelper { public token: DeployToken; public setV2: DeploySetV2; public manager: DeployManager; + public managerCore: DeployManagerCore; + public globalExtensions: DeployGlobalExtensions; + public factories: DeployFactories; public mocks: DeployMocks; public extensions: DeployExtensions; public external: DeployExternalContracts; @@ -34,5 +40,8 @@ export default class DeployHelper { this.staking = new DeployStaking(deployerSigner); this.viewers = new DeployViewers(deployerSigner); this.keepers = new DeployKeepers(deployerSigner); + this.managerCore = new DeployManagerCore(deployerSigner); + this.globalExtensions = new DeployGlobalExtensions(deployerSigner); + this.factories = new DeployFactories(deployerSigner); } } diff --git a/utils/index.ts b/utils/index.ts index 04230127..6eee9cc4 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -1,5 +1,5 @@ import { ethers } from "hardhat"; -import { Blockchain } from "./common"; +import { Blockchain, ProtocolUtils } from "./common"; import { Address } from "./types"; const provider = ethers.provider; @@ -13,6 +13,7 @@ import { UniswapV3Fixture } from "./fixtures"; +export const getProtocolUtils = () => new ProtocolUtils(provider); export const getSetFixture = (ownerAddress: Address) => new SetFixture(provider, ownerAddress); export const getAaveV2Fixture = (ownerAddress: Address) => new AaveV2Fixture(provider, ownerAddress); export const getCompoundFixture = (ownerAddress: Address) => new CompoundFixture(provider, ownerAddress); diff --git a/utils/test/testingUtils.ts b/utils/test/testingUtils.ts index 4c8e3585..23d26117 100644 --- a/utils/test/testingUtils.ts +++ b/utils/test/testingUtils.ts @@ -143,3 +143,7 @@ export function setBlockNumber(blockNumber: number) { }); }); } + +export async function getLastBlockTransaction(): Promise { + return (await provider.getBlockWithTransactions("latest")).transactions[0]; +} diff --git a/utils/types.ts b/utils/types.ts index 2f53dc79..a82b357d 100644 --- a/utils/types.ts +++ b/utils/types.ts @@ -98,3 +98,25 @@ export interface AirdropSettings { airdropFee: BigNumber; anyoneAbsorb: boolean; } + +export interface StreamingFeeState { + feeRecipient: Address; + streamingFeePercentage: BigNumber; + maxStreamingFeePercentage: BigNumber; + lastStreamingFeeTimestamp: BigNumber; +} + +export interface TradeInfo { + exchangeName: string; + sendToken: Address; + sendQuantity: BigNumber; + receiveToken: Address; + receiveQuantity: BigNumber; + data: Bytes; +} + +export interface BatchTradeResult { + success: boolean; + tradeInfo: TradeInfo; + revertReason?: string | undefined; +} From 91fbd28f3bfe8eb4f000d2b7ee0d0bea01f7aabe Mon Sep 17 00:00:00 2001 From: Pranav Bhardwaj Date: Sat, 26 Aug 2023 13:56:43 -0400 Subject: [PATCH 03/10] add mutualUpgradeV2 tests --- test/lib/mutualUpgradeV2.spec.ts | 148 +++++++++++++++++++++++++++++++ utils/deploys/deployMocks.ts | 6 ++ 2 files changed, 154 insertions(+) create mode 100644 test/lib/mutualUpgradeV2.spec.ts diff --git a/test/lib/mutualUpgradeV2.spec.ts b/test/lib/mutualUpgradeV2.spec.ts new file mode 100644 index 00000000..717a6cc3 --- /dev/null +++ b/test/lib/mutualUpgradeV2.spec.ts @@ -0,0 +1,148 @@ +import "module-alias/register"; +import { solidityKeccak256 } from "ethers/lib/utils"; +import { BigNumber } from "ethers"; + +import { Account } from "@utils/types"; +import { ONE, ZERO } from "@utils/constants"; +import { MutualUpgradeV2Mock } from "@utils/contracts/index"; +import DeployHelper from "@utils/deploys"; +import { + addSnapshotBeforeRestoreAfterEach, + getAccounts, + getWaffleExpect, + getRandomAccount, +} from "@utils/index"; +import { ContractTransaction } from "ethers"; + +const expect = getWaffleExpect(); + +describe("MutualUpgradeV2", () => { + let owner: Account; + let methodologist: Account; + let deployer: DeployHelper; + + let mutualUpgradeV2Mock: MutualUpgradeV2Mock; + + before(async () => { + [ + owner, + methodologist, + ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + + mutualUpgradeV2Mock = await deployer.mocks.deployMutualUpgradeV2Mock(owner.address, methodologist.address); + }); + + addSnapshotBeforeRestoreAfterEach(); + + describe("#testMutualUpgradeV2", async () => { + let subjectTestUint: BigNumber; + let subjectCaller: Account; + let subjectMutualUpgradeV2Mock: MutualUpgradeV2Mock; + + beforeEach(async () => { + subjectTestUint = ONE; + subjectCaller = owner; + subjectMutualUpgradeV2Mock = mutualUpgradeV2Mock; + }); + + async function subject(): Promise { + return subjectMutualUpgradeV2Mock.connect(subjectCaller.wallet).testMutualUpgrade(subjectTestUint); + } + + describe("when the two mutual upgrade parties are the same", async () => { + let trivialMutualUpgradeV2Mock: MutualUpgradeV2Mock; + + beforeEach(async () => { + trivialMutualUpgradeV2Mock = await deployer.mocks.deployMutualUpgradeV2Mock(owner.address, owner.address); + + subjectMutualUpgradeV2Mock = trivialMutualUpgradeV2Mock; + }); + + it("should update the testUint", async () => { + await subject(); + + const currentTestUint = await trivialMutualUpgradeV2Mock.testUint(); + expect(currentTestUint).to.eq(subjectTestUint); + }); + }); + + describe("when the mutualUpgrade hash is not set", async () => { + it("should register the initial mutual upgrade", async () => { + const txHash = await subject(); + + const expectedHash = solidityKeccak256(["bytes", "address"], [txHash.data, subjectCaller.address]); + const isLogged = await mutualUpgradeV2Mock.mutualUpgrades(expectedHash); + + expect(isLogged).to.be.true; + }); + + it("should not update the testUint", async () => { + await subject(); + + const currentInt = await mutualUpgradeV2Mock.testUint(); + expect(currentInt).to.eq(ZERO); + }); + + it("emits a MutualUpgradeRegistered event", async () => { + await expect(subject()).to.emit(mutualUpgradeV2Mock, "MutualUpgradeRegistered"); + }); + }); + + describe("when the mutualUpgrade hash is set", async () => { + beforeEach(async () => { + await subject(); + subjectCaller = methodologist; + }); + + it("should clear the mutualUpgrade hash", async () => { + const txHash = await subject(); + + const expectedHash = solidityKeccak256(["bytes", "address"], [txHash.data, subjectCaller.address]); + const isLogged = await mutualUpgradeV2Mock.mutualUpgrades(expectedHash); + + expect(isLogged).to.be.false; + }); + + it("should update the testUint", async () => { + await subject(); + + const currentTestUint = await mutualUpgradeV2Mock.testUint(); + expect(currentTestUint).to.eq(subjectTestUint); + }); + + describe("when the same address calls it twice", async () => { + beforeEach(async () => { + subjectCaller = owner; + }); + + it("should stay logged", async () => { + const txHash = await subject(); + + const expectedHash = solidityKeccak256(["bytes", "address"], [txHash.data, subjectCaller.address]); + const isLogged = await mutualUpgradeV2Mock.mutualUpgrades(expectedHash); + + expect(isLogged).to.be.true; + }); + + it("should not change the integer value", async () => { + await subject(); + + const currentInt = await mutualUpgradeV2Mock.testUint(); + expect(currentInt).to.eq(ZERO); + }); + }); + }); + + describe("when the sender is not one of the allowed addresses", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be authorized address"); + }); + }); + }); +}); diff --git a/utils/deploys/deployMocks.ts b/utils/deploys/deployMocks.ts index 59f290ab..15dfa983 100644 --- a/utils/deploys/deployMocks.ts +++ b/utils/deploys/deployMocks.ts @@ -17,11 +17,13 @@ import { DEXAdapter, ModuleMock, BaseGlobalExtensionMock, + MutualUpgradeV2Mock, } from "../contracts/index"; import { BaseExtensionMock__factory } from "../../typechain/factories/BaseExtensionMock__factory"; import { DEXAdapter__factory } from "../../typechain/factories/DEXAdapter__factory"; import { ModuleMock__factory } from "../../typechain/factories/ModuleMock__factory"; +import { MutualUpgradeV2Mock__factory } from "../../typechain/factories/MutualUpgradeV2Mock__factory"; import { BaseGlobalExtensionMock__factory } from "../../typechain/factories/BaseGlobalExtensionMock__factory"; import { convertLibraryNameToLinkId } from "../common"; import { ChainlinkAggregatorV3Mock__factory } from "../../typechain/factories/ChainlinkAggregatorV3Mock__factory"; @@ -161,6 +163,10 @@ export default class DeployMocks { return await new ModuleMock__factory(this._deployerSigner).deploy(controller); } + public async deployMutualUpgradeV2Mock(owner: Address, methodologist: string): Promise { + return await new MutualUpgradeV2Mock__factory(this._deployerSigner).deploy(owner, methodologist); + } + public async deployFlashMintLeveragedCompMock( wethAddress: Address, quickRouterAddress: Address, From ec4df4ae70cb874097a350714e4a21d96cf8b3c6 Mon Sep 17 00:00:00 2001 From: Pranav Bhardwaj Date: Sat, 26 Aug 2023 15:59:19 -0400 Subject: [PATCH 04/10] add claim extension tests --- external/abi/set/ClaimAdapterMock.json | 413 ++++ external/abi/set/ClaimModule.json | 546 +++++ external/contracts/set/ClaimAdapterMock.sol | 71 + external/contracts/set/ClaimModule.sol | 516 ++++ .../globalClaimExtension.spec.ts | 2156 +++++++++++++++++ utils/contracts/setV2.ts | 2 + utils/deploys/deployMocks.ts | 6 + utils/deploys/deploySetV2.ts | 12 + utils/types.ts | 7 + 9 files changed, 3729 insertions(+) create mode 100644 external/abi/set/ClaimAdapterMock.json create mode 100644 external/abi/set/ClaimModule.json create mode 100644 external/contracts/set/ClaimAdapterMock.sol create mode 100644 external/contracts/set/ClaimModule.sol create mode 100644 test/global-extensions/globalClaimExtension.spec.ts diff --git a/external/abi/set/ClaimAdapterMock.json b/external/abi/set/ClaimAdapterMock.json new file mode 100644 index 00000000..118179d9 --- /dev/null +++ b/external/abi/set/ClaimAdapterMock.json @@ -0,0 +1,413 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "ClaimAdapterMock", + "sourceName": "contracts/mocks/integrations/ClaimAdapterMock.sol", + "abi": [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_holder", + "type": "address" + }, + { + "internalType": "address", + "name": "_rewardPool", + "type": "address" + } + ], + "name": "getClaimCallData", + "outputs": [ + { + "internalType": "address", + "name": "_subject", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_calldata", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_holder", + "type": "address" + }, + { + "internalType": "address", + "name": "_rewardPool", + "type": "address" + } + ], + "name": "getRewardsAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_rewardPool", + "type": "address" + } + ], + "name": "getTokenAddress", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [], + "name": "rewards", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_rewards", + "type": "uint256" + } + ], + "name": "setRewards", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + } + ], + "bytecode": "0x608060405234801561001057600080fd5b50604080518082018252600c81526b21b630b4b6a0b230b83a32b960a11b602080830191825283518085019094526005845264434c41494d60d81b90840152815191929161006091600391610089565b508051610074906004906020840190610089565b50506005805460ff1916601217905550610124565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106100ca57805160ff19168380011785556100f7565b828001600101855582156100f7579182015b828111156100f75782518255916020019190600101906100dc565b50610103929150610107565b5090565b61012191905b80821115610103576000815560010161010d565b90565b610d2e806101336000396000f3fe608060405234801561001057600080fd5b506004361061010b5760003560e01c806370a08231116100a2578063a457c2d711610071578063a457c2d71461039b578063a9059cbb146103c7578063b8d7b669146103f3578063c7a29c6f14610435578063dd62ed3e146104525761010b565b806370a0823114610337578063825409e21461035d57806395d89b411461038b5780639ec5a894146103935761010b565b806318160ddd116100de57806318160ddd1461029d57806323b872dd146102b7578063313ce567146102ed578063395093511461030b5761010b565b806306fdde0314610110578063095ea7b31461018d5780630f1ff4ba146101cd5780631249c58b14610293575b600080fd5b610118610480565b6040805160208082528351818301528351919283929083019185019080838360005b8381101561015257818101518382015260200161013a565b50505050905090810190601f16801561017f5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6101b9600480360360408110156101a357600080fd5b506001600160a01b038135169060200135610516565b604080519115158252519081900360200190f35b6101fb600480360360408110156101e357600080fd5b506001600160a01b0381358116916020013516610533565b60405180846001600160a01b03166001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b8381101561025657818101518382015260200161023e565b50505050905090810190601f1680156102835780820380516001836020036101000a031916815260200191505b5094505050505060405180910390f35b61029b610568565b005b6102a5610576565b60408051918252519081900360200190f35b6101b9600480360360608110156102cd57600080fd5b506001600160a01b0381358116916020810135909116906040013561057c565b6102f5610609565b6040805160ff9092168252519081900360200190f35b6101b96004803603604081101561032157600080fd5b506001600160a01b038135169060200135610612565b6102a56004803603602081101561034d57600080fd5b50356001600160a01b0316610666565b6102a56004803603604081101561037357600080fd5b506001600160a01b0381358116916020013516610681565b610118610689565b6102a56106ea565b6101b9600480360360408110156103b157600080fd5b506001600160a01b0381351690602001356106f0565b6101b9600480360360408110156103dd57600080fd5b506001600160a01b03813516906020013561075e565b6104196004803603602081101561040957600080fd5b50356001600160a01b0316610772565b604080516001600160a01b039092168252519081900360200190f35b61029b6004803603602081101561044b57600080fd5b5035610777565b6102a56004803603604081101561046857600080fd5b506001600160a01b038135811691602001351661077c565b60038054604080516020601f600260001961010060018816150201909516949094049384018190048102820181019092528281526060939092909183018282801561050c5780601f106104e15761010080835404028352916020019161050c565b820191906000526020600020905b8154815290600101906020018083116104ef57829003601f168201915b5050505050905090565b600061052a6105236107a7565b84846107ab565b50600192915050565b6040805160048152602481019091526020810180516001600160e01b0316631249c58b60e01b17905230906000909250925092565b61057433600654610897565b565b60025490565b6000610589848484610993565b6105ff846105956107a7565b6105fa85604051806060016040528060288152602001610c63602891396001600160a01b038a166000908152600160205260408120906105d36107a7565b6001600160a01b03168152602081019190915260400160002054919063ffffffff610afa16565b6107ab565b5060019392505050565b60055460ff1690565b600061052a61061f6107a7565b846105fa85600160006106306107a7565b6001600160a01b03908116825260208083019390935260409182016000908120918c16815292529020549063ffffffff610b9116565b6001600160a01b031660009081526020819052604090205490565b505060065490565b60048054604080516020601f600260001961010060018816150201909516949094049384018190048102820181019092528281526060939092909183018282801561050c5780601f106104e15761010080835404028352916020019161050c565b60065481565b600061052a6106fd6107a7565b846105fa85604051806060016040528060258152602001610cd460259139600160006107276107a7565b6001600160a01b03908116825260208083019390935260409182016000908120918d1681529252902054919063ffffffff610afa16565b600061052a61076b6107a7565b8484610993565b503090565b600655565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b3390565b6001600160a01b0383166107f05760405162461bcd60e51b8152600401808060200182810382526024815260200180610cb06024913960400191505060405180910390fd5b6001600160a01b0382166108355760405162461bcd60e51b8152600401808060200182810382526022815260200180610c1b6022913960400191505060405180910390fd5b6001600160a01b03808416600081815260016020908152604080832094871680845294825291829020859055815185815291517f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9259281900390910190a3505050565b6001600160a01b0382166108f2576040805162461bcd60e51b815260206004820152601f60248201527f45524332303a206d696e7420746f20746865207a65726f206164647265737300604482015290519081900360640190fd5b6108fe60008383610bf2565b600254610911908263ffffffff610b9116565b6002556001600160a01b03821660009081526020819052604090205461093d908263ffffffff610b9116565b6001600160a01b0383166000818152602081815260408083209490945583518581529351929391927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9281900390910190a35050565b6001600160a01b0383166109d85760405162461bcd60e51b8152600401808060200182810382526025815260200180610c8b6025913960400191505060405180910390fd5b6001600160a01b038216610a1d5760405162461bcd60e51b8152600401808060200182810382526023815260200180610bf86023913960400191505060405180910390fd5b610a28838383610bf2565b610a6b81604051806060016040528060268152602001610c3d602691396001600160a01b038616600090815260208190526040902054919063ffffffff610afa16565b6001600160a01b038085166000908152602081905260408082209390935590841681522054610aa0908263ffffffff610b9116565b6001600160a01b038084166000818152602081815260409182902094909455805185815290519193928716927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef92918290030190a3505050565b60008184841115610b895760405162461bcd60e51b81526004018080602001828103825283818151815260200191508051906020019080838360005b83811015610b4e578181015183820152602001610b36565b50505050905090810190601f168015610b7b5780820380516001836020036101000a031916815260200191505b509250505060405180910390fd5b505050900390565b600082820183811015610beb576040805162461bcd60e51b815260206004820152601b60248201527f536166654d6174683a206164646974696f6e206f766572666c6f770000000000604482015290519081900360640190fd5b9392505050565b50505056fe45524332303a207472616e7366657220746f20746865207a65726f206164647265737345524332303a20617070726f766520746f20746865207a65726f206164647265737345524332303a207472616e7366657220616d6f756e7420657863656564732062616c616e636545524332303a207472616e7366657220616d6f756e74206578636565647320616c6c6f77616e636545524332303a207472616e736665722066726f6d20746865207a65726f206164647265737345524332303a20617070726f76652066726f6d20746865207a65726f206164647265737345524332303a2064656372656173656420616c6c6f77616e63652062656c6f77207a65726fa26469706673582212201b865811cefcbb39d3a444ee18118604e3981816bd2617fa7f14e0e50e4fa1c264736f6c634300060a0033", + "deployedBytecode": "0x608060405234801561001057600080fd5b506004361061010b5760003560e01c806370a08231116100a2578063a457c2d711610071578063a457c2d71461039b578063a9059cbb146103c7578063b8d7b669146103f3578063c7a29c6f14610435578063dd62ed3e146104525761010b565b806370a0823114610337578063825409e21461035d57806395d89b411461038b5780639ec5a894146103935761010b565b806318160ddd116100de57806318160ddd1461029d57806323b872dd146102b7578063313ce567146102ed578063395093511461030b5761010b565b806306fdde0314610110578063095ea7b31461018d5780630f1ff4ba146101cd5780631249c58b14610293575b600080fd5b610118610480565b6040805160208082528351818301528351919283929083019185019080838360005b8381101561015257818101518382015260200161013a565b50505050905090810190601f16801561017f5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6101b9600480360360408110156101a357600080fd5b506001600160a01b038135169060200135610516565b604080519115158252519081900360200190f35b6101fb600480360360408110156101e357600080fd5b506001600160a01b0381358116916020013516610533565b60405180846001600160a01b03166001600160a01b0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b8381101561025657818101518382015260200161023e565b50505050905090810190601f1680156102835780820380516001836020036101000a031916815260200191505b5094505050505060405180910390f35b61029b610568565b005b6102a5610576565b60408051918252519081900360200190f35b6101b9600480360360608110156102cd57600080fd5b506001600160a01b0381358116916020810135909116906040013561057c565b6102f5610609565b6040805160ff9092168252519081900360200190f35b6101b96004803603604081101561032157600080fd5b506001600160a01b038135169060200135610612565b6102a56004803603602081101561034d57600080fd5b50356001600160a01b0316610666565b6102a56004803603604081101561037357600080fd5b506001600160a01b0381358116916020013516610681565b610118610689565b6102a56106ea565b6101b9600480360360408110156103b157600080fd5b506001600160a01b0381351690602001356106f0565b6101b9600480360360408110156103dd57600080fd5b506001600160a01b03813516906020013561075e565b6104196004803603602081101561040957600080fd5b50356001600160a01b0316610772565b604080516001600160a01b039092168252519081900360200190f35b61029b6004803603602081101561044b57600080fd5b5035610777565b6102a56004803603604081101561046857600080fd5b506001600160a01b038135811691602001351661077c565b60038054604080516020601f600260001961010060018816150201909516949094049384018190048102820181019092528281526060939092909183018282801561050c5780601f106104e15761010080835404028352916020019161050c565b820191906000526020600020905b8154815290600101906020018083116104ef57829003601f168201915b5050505050905090565b600061052a6105236107a7565b84846107ab565b50600192915050565b6040805160048152602481019091526020810180516001600160e01b0316631249c58b60e01b17905230906000909250925092565b61057433600654610897565b565b60025490565b6000610589848484610993565b6105ff846105956107a7565b6105fa85604051806060016040528060288152602001610c63602891396001600160a01b038a166000908152600160205260408120906105d36107a7565b6001600160a01b03168152602081019190915260400160002054919063ffffffff610afa16565b6107ab565b5060019392505050565b60055460ff1690565b600061052a61061f6107a7565b846105fa85600160006106306107a7565b6001600160a01b03908116825260208083019390935260409182016000908120918c16815292529020549063ffffffff610b9116565b6001600160a01b031660009081526020819052604090205490565b505060065490565b60048054604080516020601f600260001961010060018816150201909516949094049384018190048102820181019092528281526060939092909183018282801561050c5780601f106104e15761010080835404028352916020019161050c565b60065481565b600061052a6106fd6107a7565b846105fa85604051806060016040528060258152602001610cd460259139600160006107276107a7565b6001600160a01b03908116825260208083019390935260409182016000908120918d1681529252902054919063ffffffff610afa16565b600061052a61076b6107a7565b8484610993565b503090565b600655565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b3390565b6001600160a01b0383166107f05760405162461bcd60e51b8152600401808060200182810382526024815260200180610cb06024913960400191505060405180910390fd5b6001600160a01b0382166108355760405162461bcd60e51b8152600401808060200182810382526022815260200180610c1b6022913960400191505060405180910390fd5b6001600160a01b03808416600081815260016020908152604080832094871680845294825291829020859055815185815291517f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9259281900390910190a3505050565b6001600160a01b0382166108f2576040805162461bcd60e51b815260206004820152601f60248201527f45524332303a206d696e7420746f20746865207a65726f206164647265737300604482015290519081900360640190fd5b6108fe60008383610bf2565b600254610911908263ffffffff610b9116565b6002556001600160a01b03821660009081526020819052604090205461093d908263ffffffff610b9116565b6001600160a01b0383166000818152602081815260408083209490945583518581529351929391927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9281900390910190a35050565b6001600160a01b0383166109d85760405162461bcd60e51b8152600401808060200182810382526025815260200180610c8b6025913960400191505060405180910390fd5b6001600160a01b038216610a1d5760405162461bcd60e51b8152600401808060200182810382526023815260200180610bf86023913960400191505060405180910390fd5b610a28838383610bf2565b610a6b81604051806060016040528060268152602001610c3d602691396001600160a01b038616600090815260208190526040902054919063ffffffff610afa16565b6001600160a01b038085166000908152602081905260408082209390935590841681522054610aa0908263ffffffff610b9116565b6001600160a01b038084166000818152602081815260409182902094909455805185815290519193928716927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef92918290030190a3505050565b60008184841115610b895760405162461bcd60e51b81526004018080602001828103825283818151815260200191508051906020019080838360005b83811015610b4e578181015183820152602001610b36565b50505050905090810190601f168015610b7b5780820380516001836020036101000a031916815260200191505b509250505060405180910390fd5b505050900390565b600082820183811015610beb576040805162461bcd60e51b815260206004820152601b60248201527f536166654d6174683a206164646974696f6e206f766572666c6f770000000000604482015290519081900360640190fd5b9392505050565b50505056fe45524332303a207472616e7366657220746f20746865207a65726f206164647265737345524332303a20617070726f766520746f20746865207a65726f206164647265737345524332303a207472616e7366657220616d6f756e7420657863656564732062616c616e636545524332303a207472616e7366657220616d6f756e74206578636565647320616c6c6f77616e636545524332303a207472616e736665722066726f6d20746865207a65726f206164647265737345524332303a20617070726f76652066726f6d20746865207a65726f206164647265737345524332303a2064656372656173656420616c6c6f77616e63652062656c6f77207a65726fa26469706673582212201b865811cefcbb39d3a444ee18118604e3981816bd2617fa7f14e0e50e4fa1c264736f6c634300060a0033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/external/abi/set/ClaimModule.json b/external/abi/set/ClaimModule.json new file mode 100644 index 00000000..cc28da34 --- /dev/null +++ b/external/abi/set/ClaimModule.json @@ -0,0 +1,546 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "ClaimModule", + "sourceName": "contracts/protocol/modules/v1/ClaimModule.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "contract IController", + "name": "_controller", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "_anyoneClaim", + "type": "bool" + } + ], + "name": "AnyoneClaimUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "_rewardPool", + "type": "address" + }, + { + "indexed": true, + "internalType": "contract IClaimAdapter", + "name": "_adapter", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "RewardClaimed", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_rewardPool", + "type": "address" + }, + { + "internalType": "string", + "name": "_integrationName", + "type": "string" + } + ], + "name": "addClaim", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "", + "type": "address" + } + ], + "name": "anyoneClaim", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "internalType": "address[]", + "name": "_rewardPools", + "type": "address[]" + }, + { + "internalType": "string[]", + "name": "_integrationNames", + "type": "string[]" + } + ], + "name": "batchAddClaim", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "internalType": "address[]", + "name": "_rewardPools", + "type": "address[]" + }, + { + "internalType": "string[]", + "name": "_integrationNames", + "type": "string[]" + } + ], + "name": "batchClaim", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "internalType": "address[]", + "name": "_rewardPools", + "type": "address[]" + }, + { + "internalType": "string[]", + "name": "_integrationNames", + "type": "string[]" + } + ], + "name": "batchRemoveClaim", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_rewardPool", + "type": "address" + }, + { + "internalType": "string", + "name": "_integrationName", + "type": "string" + } + ], + "name": "claim", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "claimSettings", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "claimSettingsStatus", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [], + "name": "controller", + "outputs": [ + { + "internalType": "contract IController", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_rewardPool", + "type": "address" + } + ], + "name": "getRewardPoolClaims", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + } + ], + "name": "getRewardPools", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_rewardPool", + "type": "address" + }, + { + "internalType": "string", + "name": "_integrationName", + "type": "string" + } + ], + "name": "getRewards", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "internalType": "bool", + "name": "_anyoneClaim", + "type": "bool" + }, + { + "internalType": "address[]", + "name": "_rewardPools", + "type": "address[]" + }, + { + "internalType": "string[]", + "name": "_integrationNames", + "type": "string[]" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_rewardPool", + "type": "address" + } + ], + "name": "isRewardPool", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_rewardPool", + "type": "address" + }, + { + "internalType": "string", + "name": "_integrationName", + "type": "string" + } + ], + "name": "isRewardPoolClaim", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_rewardPool", + "type": "address" + }, + { + "internalType": "string", + "name": "_integrationName", + "type": "string" + } + ], + "name": "removeClaim", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [], + "name": "removeModule", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "rewardPoolList", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "rewardPoolStatus", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "internalType": "bool", + "name": "_anyoneClaim", + "type": "bool" + } + ], + "name": "updateAnyoneClaim", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + } + ], + "bytecode": "0x608060405234801561001057600080fd5b50604051620024253803806200242583398101604081905261003191610056565b600080546001600160a01b0319166001600160a01b0392909216919091179055610084565b600060208284031215610067578081fd5b81516001600160a01b038116811461007d578182fd5b9392505050565b61239180620000946000396000f3fe608060405234801561001057600080fd5b506004361061012c5760003560e01c8063a98627cb116100ad578063d051796f11610071578063d051796f14610282578063e981828414610295578063ecf274da146102a8578063f4c872f8146102bb578063f77c4791146102ce5761012c565b8063a98627cb14610223578063af91baf514610236578063b2bfb0ad14610249578063bca39c201461025c578063bf614b131461026f5761012c565b8063847ef08d116100f4578063847ef08d146101c25780638e0f3418146101ca5780639bf0889d146101ea578063a4a56926146101fd578063a8045a8e146102105761012c565b80634100e3cb1461013157806352a4037b1461015a5780635d9026b21461017a578063620814e91461018f57806367462d91146101af575b600080fd5b61014461013f366004611ca8565b6102d6565b6040516101519190611fc6565b60405180910390f35b61016d610168366004611b47565b610355565b6040516101519190611f79565b61018d610188366004611ca8565b6103cb565b005b6101a261019d366004611eb6565b6103e8565b6040516101519190611f25565b61018d6101bd366004611d77565b61041d565b61018d610500565b6101dd6101d8366004611ca8565b6107d6565b60405161015191906122bc565b61018d6101f8366004611df7565b610870565b61018d61020b366004611d77565b6108db565b61016d61021e366004611c26565b61098e565b6101a2610231366004611d37565b610a11565b61018d610244366004611ca8565b610a53565b610144610257366004611c26565b610a69565b61018d61026a366004611d77565b610a89565b61014461027d366004611c26565b610aa8565b610144610290366004611b47565b610ad6565b6101446102a3366004611c5e565b610aeb565b61018d6102b6366004611e24565b610b11565b61018d6102c9366004611ca8565b610bae565b6101a2610bea565b60008061031884848080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250610bf992505050565b6001600160a01b039687166000908152600560209081526040808320988a168352978152878220929098168152965250505091205460ff16919050565b6001600160a01b0381166000908152600260209081526040918290208054835181840281018401909452808452606093928301828280156103bf57602002820191906000526020600020905b81546001600160a01b031681526001909101906020018083116103a1575b50505050509050919050565b836103d581610c17565b6103e185858585610c62565b5050505050565b6002602052816000526040600020818154811061040157fe5b6000918252602090912001546001600160a01b03169150829050565b8461042781610c3d565b8561043181610dd9565b6104565760405162461bcd60e51b815260040161044d9061202d565b60405180910390fd5b6000610498878780806020026020016040519081016040528093929190818152602001838360200280828437600092019190915250899250889150610e0b9050565b905060005b818110156104f5576104ed898989848181106104b557fe5b90506020020160208101906104ca9190611b47565b8888858181106104d657fe5b90506020028101906104e891906122c5565b610e57565b60010161049d565b505050505050505050565b336000908152600160209081526040808320805460ff19169055600282529182902080548351818402810184019094528084526060939283018282801561057057602002820191906000526020600020905b81546001600160a01b03168152600190910190602001808311610552575b50939450600093505050505b815181101561071157336000908152600460205260408120835182908590859081106105a457fe5b60200260200101516001600160a01b03166001600160a01b03168152602001908152602001600020905060008090505b81548110156106b35760008282815481106105eb57fe5b600091825260208083209091015433835260059091526040822087516001600160a01b03909216935090829088908890811061062357fe5b60200260200101516001600160a01b03166001600160a01b031681526020019081526020016000206000836001600160a01b03166001600160a01b0316815260200190815260200160002060006101000a81548160ff02191690831515021790555082828154811061069157fe5b600091825260209091200180546001600160a01b0319169055506001016105d4565b5033600090815260046020526040812084519091908590859081106106d457fe5b60200260200101516001600160a01b03166001600160a01b0316815260200190815260200160002060006107089190611a4f565b5060010161057c565b5060005b336000908152600260205260409020548110156107ba5733600090815260026020526040812080548390811061074757fe5b6000918252602080832090910154338084526003835260408085206001600160a01b03909316808652928452808520805460ff19169055908452600290925291208054919250908390811061079857fe5b600091825260209091200180546001600160a01b031916905550600101610715565b503360009081526002602052604081206107d391611a4f565b50565b6000806107e586868686611187565b60405163412a04f160e11b81529091506001600160a01b0382169063825409e2906108169089908990600401611fd1565b60206040518083038186803b15801561082e57600080fd5b505afa158015610842573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906108669190611ee1565b9695505050505050565b8161087a81610c17565b6001600160a01b03831660008181526001602052604090819020805460ff1916851515179055517ffc5fc0cfabd873d47a6133c597c0e71ed6d2662ddaf66bec70ac075043ec9a5d906108ce908590611fc6565b60405180910390a2505050565b846108e581610c17565b6000610927868680806020026020016040519081016040528093929190818152602001838360200280828437600092019190915250889250879150610e0b9050565b905060005b818110156109845761097c8888888481811061094457fe5b90506020020160208101906109599190611b47565b87878581811061096557fe5b905060200281019061097791906122c5565b610c62565b60010161092c565b5050505050505050565b6001600160a01b038083166000908152600460209081526040808320938516835292815290829020805483518184028101840190945280845260609392830182828015610a0457602002820191906000526020600020905b81546001600160a01b031681526001909101906020018083116109e6575b5050505050905092915050565b60046020528260005260406000206020528160005260406000208181548110610a3657fe5b6000918252602090912001546001600160a01b0316925083915050565b83610a5d81610c17565b6103e185858585611224565b600360209081526000928352604080842090915290825290205460ff1681565b84610a9381610c17565b610aa086868686866113b3565b505050505050565b6001600160a01b03918216600090815260036020908152604080832093909416825291909152205460ff1690565b60016020526000908152604090205460ff1681565b600560209081526000938452604080852082529284528284209052825290205460ff1681565b8533610b1d828261145b565b87610b2781611485565b610b3489888888886113b3565b6001600160a01b038916600081815260016020526040808220805460ff19168c151517905580516307ff078f60e11b81529051630ffe0f1e9260048084019391929182900301818387803b158015610b8b57600080fd5b505af1158015610b9f573d6000803e3d6000fd5b50505050505050505050505050565b83610bb881610c3d565b84610bc281610dd9565b610bde5760405162461bcd60e51b815260040161044d9061202d565b610aa086868686610e57565b6000546001600160a01b031681565b600080610c0583611546565b9050610c1081611551565b9392505050565b610c21813361160e565b610c3d5760405162461bcd60e51b815260040161044d90612217565b610c468161169c565b6107d35760405162461bcd60e51b815260040161044d9061208a565b6000610ca383838080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250610bf992505050565b6001600160a01b038087166000908152600560209081526040808320898516845282528083209385168352929052205490915060ff16610cf55760405162461bcd60e51b815260040161044d9061219d565b6001600160a01b038086166000908152600460209081526040808320938816835292905220610d2a908263ffffffff6117a016565b6001600160a01b0380861660008181526005602090815260408083208986168085529083528184209587168452948252808320805460ff1916905592825260048152828220938252929092529020546103e1576001600160a01b0385166000908152600260205260409020610da5908563ffffffff6117a016565b6001600160a01b038086166000908152600360209081526040808320938816835292905220805460ff191690555050505050565b6001600160a01b03811660009081526001602052604081205460ff1680610e055750610e05823361160e565b92915050565b8251600090828114610e2f5760405162461bcd60e51b815260040161044d9061205b565b60008111610e4f5760405162461bcd60e51b815260040161044d90612285565b949350505050565b610e618484610aa8565b610e7d5760405162461bcd60e51b815260040161044d9061216d565b6000610e8b85858585611187565b90506000816001600160a01b031663b8d7b669866040518263ffffffff1660e01b8152600401610ebb9190611f25565b60206040518083038186803b158015610ed357600080fd5b505afa158015610ee7573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610f0b9190611b63565b90506000816001600160a01b03166370a08231886040518263ffffffff1660e01b8152600401610f3b9190611f25565b60206040518083038186803b158015610f5357600080fd5b505afa158015610f67573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610f8b9190611ee1565b90506000806060856001600160a01b0316630f1ff4ba8b8b6040518363ffffffff1660e01b8152600401610fc0929190611fd1565b60006040518083038186803b158015610fd857600080fd5b505afa158015610fec573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526110149190810190611b7f565b925092509250896001600160a01b0316638f6f03328484846040518463ffffffff1660e01b815260040161104a93929190611f52565b600060405180830381600087803b15801561106457600080fd5b505af1158015611078573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526110a09190810190611bf3565b506040516370a0823160e01b81526000906001600160a01b038716906370a08231906110d0908e90600401611f25565b60206040518083038186803b1580156110e857600080fd5b505afa1580156110fc573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906111209190611ee1565b90506001600160a01b03808816908b8116908d167f2422cac5e23c46c890fdcf42d0c64757409df6832174df639337558f09d99c68611165858a63ffffffff6118cd16565b60405161117291906122bc565b60405180910390a45050505050505050505050565b6000806111c984848080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250610bf992505050565b6001600160a01b0380881660009081526005602090815260408083208a8516845282528083209385168352929052205490915060ff1661121b5760405162461bcd60e51b815260040161044d9061224e565b95945050505050565b600061126583838080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250610bf992505050565b6001600160a01b0380871660008181526004602090815260408083208a8616808552908352818420948452600583528184209084528252808320948616835293905291909120549192509060ff16156112d05760405162461bcd60e51b815260040161044d90612138565b8054600180820183556000838152602080822090930180546001600160a01b0319166001600160a01b038781169182179092558a8216808452600586526040808520938c16808652938752808520928552918652818420805460ff19169095179094559282526003845282822090825290925290205460ff16610aa057505050506001600160a01b0391821660008181526002602090815260408083208054600180820183559185528385200180546001600160a01b0319169690971695861790965592825260038152828220938252929092529020805460ff19169091179055565b60006113f5858580806020026020016040519081016040528093929190818152602001838360200280828437600092019190915250879250869150610e0b9050565b905060005b818110156114525761144a8787878481811061141257fe5b90506020020160208101906114279190611b47565b86868581811061143357fe5b905060200281019061144591906122c5565b611224565b6001016113fa565b50505050505050565b611465828261160e565b6114815760405162461bcd60e51b815260040161044d90612217565b5050565b600054604051631d3af8fb60e21b81526001600160a01b03909116906374ebe3ec906114b5908490600401611f25565b60206040518083038186803b1580156114cd57600080fd5b505afa1580156114e1573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906115059190611bd7565b6115215760405162461bcd60e51b815260040161044d906121d4565b61152a8161190f565b6107d35760405162461bcd60e51b815260040161044d906120d2565b805160209091012090565b600080548190611569906001600160a01b031661193e565b6001600160a01b031663e6d642c530856040518363ffffffff1660e01b8152600401611596929190611f39565b60206040518083038186803b1580156115ae57600080fd5b505afa1580156115c2573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906115e69190611b63565b90506001600160a01b038116610e055760405162461bcd60e51b815260040161044d90612109565b6000816001600160a01b0316836001600160a01b031663481c6a756040518163ffffffff1660e01b815260040160206040518083038186803b15801561165357600080fd5b505afa158015611667573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061168b9190611b63565b6001600160a01b0316149392505050565b60008054604051631d3af8fb60e21b81526001600160a01b03909116906374ebe3ec906116cd908590600401611f25565b60206040518083038186803b1580156116e557600080fd5b505afa1580156116f9573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061171d9190611bd7565b8015610e0557506040516335fc6c9f60e21b81526001600160a01b0383169063d7f1b27c90611750903090600401611f25565b60206040518083038186803b15801561176857600080fd5b505afa15801561177c573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610e059190611bd7565b600080611806848054806020026020016040519081016040528092919081815260200182805480156117fb57602002820191906000526020600020905b81546001600160a01b031681526001909101906020018083116117dd575b5050505050846119bd565b91509150806118275760405162461bcd60e51b815260040161044d90611ffe565b8354600019018281146118995784818154811061184057fe5b9060005260206000200160009054906101000a90046001600160a01b031685848154811061186a57fe5b9060005260206000200160006101000a8154816001600160a01b0302191690836001600160a01b031602179055505b848054806118a357fe5b600082815260209020810160001990810180546001600160a01b0319169055019055505b50505050565b6000610c1083836040518060400160405280601e81526020017f536166654d6174683a207375627472616374696f6e206f766572666c6f770000815250611a23565b6040516353bae5f760e01b81526000906001600160a01b038316906353bae5f790611750903090600401611f25565b6040516373b2e76b60e11b81526000906001600160a01b0383169063e765ced69061196d9084906004016122bc565b60206040518083038186803b15801561198557600080fd5b505afa158015611999573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610e059190611b63565b81516000908190815b81811015611a1057846001600160a01b03168682815181106119e457fe5b60200260200101516001600160a01b03161415611a0857925060019150611a1c9050565b6001016119c6565b50600019600092509250505b9250929050565b60008184841115611a475760405162461bcd60e51b815260040161044d9190611feb565b505050900390565b50805460008255906000526020600020908101906107d39190611a8691905b80821115611a825760008155600101611a6e565b5090565b90565b60008083601f840112611a9a578182fd5b50813567ffffffffffffffff811115611ab1578182fd5b6020830191508360208083028501011115611a1c57600080fd5b600082601f830112611adb578081fd5b815167ffffffffffffffff80821115611af2578283fd5b604051601f8301601f191681016020018281118282101715611b12578485fd5b604052828152925082848301602001861015611b2d57600080fd5b611b3e83602083016020880161230c565b50505092915050565b600060208284031215611b58578081fd5b8135610c1081612338565b600060208284031215611b74578081fd5b8151610c1081612338565b600080600060608486031215611b93578182fd5b8351611b9e81612338565b60208501516040860151919450925067ffffffffffffffff811115611bc1578182fd5b611bcd86828701611acb565b9150509250925092565b600060208284031215611be8578081fd5b8151610c108161234d565b600060208284031215611c04578081fd5b815167ffffffffffffffff811115611c1a578182fd5b610e4f84828501611acb565b60008060408385031215611c38578182fd5b8235611c4381612338565b91506020830135611c5381612338565b809150509250929050565b600080600060608486031215611c72578283fd5b8335611c7d81612338565b92506020840135611c8d81612338565b91506040840135611c9d81612338565b809150509250925092565b60008060008060608587031215611cbd578081fd5b8435611cc881612338565b93506020850135611cd881612338565b9250604085013567ffffffffffffffff80821115611cf4578283fd5b81870188601f820112611d05578384fd5b8035925081831115611d15578384fd5b886020848301011115611d26578384fd5b959894975050602090940194505050565b600080600060608486031215611d4b578283fd5b8335611d5681612338565b92506020840135611d6681612338565b929592945050506040919091013590565b600080600080600060608688031215611d8e578081fd5b8535611d9981612338565b9450602086013567ffffffffffffffff80821115611db5578283fd5b611dc189838a01611a89565b90965094506040880135915080821115611dd9578283fd5b50611de688828901611a89565b969995985093965092949392505050565b60008060408385031215611e09578182fd5b8235611e1481612338565b91506020830135611c538161234d565b60008060008060008060808789031215611e3c578384fd5b8635611e4781612338565b95506020870135611e578161234d565b9450604087013567ffffffffffffffff80821115611e73578586fd5b611e7f8a838b01611a89565b90965094506060890135915080821115611e97578283fd5b50611ea489828a01611a89565b979a9699509497509295939492505050565b60008060408385031215611ec8578182fd5b8235611ed381612338565b946020939093013593505050565b600060208284031215611ef2578081fd5b5051919050565b60008151808452611f1181602086016020860161230c565b601f01601f19169290920160200192915050565b6001600160a01b0391909116815260200190565b6001600160a01b03929092168252602082015260400190565b600060018060a01b03851682528360208301526060604083015261121b6060830184611ef9565b6020808252825182820181905260009190848201906040850190845b81811015611fba5783516001600160a01b031683529284019291840191600101611f95565b50909695505050505050565b901515815260200190565b6001600160a01b0392831681529116602082015260400190565b600060208252610c106020830184611ef9565b60208082526015908201527420b2323932b9b9903737ba1034b71030b93930bc9760591b604082015260600190565b60208082526014908201527326bab9ba103132903b30b634b21031b0b63632b960611b604082015260600190565b602080825260159082015274082e4e4c2f240d8cadccee8d040dad2e6dac2e8c6d605b1b604082015260600190565b60208082526028908201527f4d75737420626520612076616c696420616e6420696e697469616c697a65642060408201526729b2ba2a37b5b2b760c11b606082015260800190565b6020808252601e908201527f4d7573742062652070656e64696e6720696e697469616c697a6174696f6e0000604082015260600190565b60208082526015908201527426bab9ba103132903b30b634b21030b230b83a32b960591b604082015260600190565b6020808252818101527f496e746567726174696f6e206e616d6573206d75737420626520756e69717565604082015260600190565b60208082526016908201527514995dd85c99141bdbdb081b9bdd081c1c995cd95b9d60521b604082015260600190565b60208082526019908201527f496e746567726174696f6e206d75737420626520616464656400000000000000604082015260600190565b60208082526023908201527f4d75737420626520636f6e74726f6c6c65722d656e61626c656420536574546f60408201526235b2b760e91b606082015260800190565b6020808252601c908201527f4d7573742062652074686520536574546f6b656e206d616e6167657200000000604082015260600190565b6020808252601f908201527f4164617074657220696e746567726174696f6e206e6f742070726573656e7400604082015260600190565b60208082526018908201527f417272617973206d757374206e6f7420626520656d7074790000000000000000604082015260600190565b90815260200190565b6000808335601e198436030181126122db578283fd5b8084018035925067ffffffffffffffff8311156122f6578384fd5b60200192505036819003821315611a1c57600080fd5b60005b8381101561232757818101518382015260200161230f565b838111156118c75750506000910152565b6001600160a01b03811681146107d357600080fd5b80151581146107d357600080fdfea26469706673582212203bc3d3b381b7657f1ef961c2b1ee9a5a9fa96b0cb8cbbbe25235511e17678b9a64736f6c634300060a0033", + "deployedBytecode": "0x608060405234801561001057600080fd5b506004361061012c5760003560e01c8063a98627cb116100ad578063d051796f11610071578063d051796f14610282578063e981828414610295578063ecf274da146102a8578063f4c872f8146102bb578063f77c4791146102ce5761012c565b8063a98627cb14610223578063af91baf514610236578063b2bfb0ad14610249578063bca39c201461025c578063bf614b131461026f5761012c565b8063847ef08d116100f4578063847ef08d146101c25780638e0f3418146101ca5780639bf0889d146101ea578063a4a56926146101fd578063a8045a8e146102105761012c565b80634100e3cb1461013157806352a4037b1461015a5780635d9026b21461017a578063620814e91461018f57806367462d91146101af575b600080fd5b61014461013f366004611ca8565b6102d6565b6040516101519190611fc6565b60405180910390f35b61016d610168366004611b47565b610355565b6040516101519190611f79565b61018d610188366004611ca8565b6103cb565b005b6101a261019d366004611eb6565b6103e8565b6040516101519190611f25565b61018d6101bd366004611d77565b61041d565b61018d610500565b6101dd6101d8366004611ca8565b6107d6565b60405161015191906122bc565b61018d6101f8366004611df7565b610870565b61018d61020b366004611d77565b6108db565b61016d61021e366004611c26565b61098e565b6101a2610231366004611d37565b610a11565b61018d610244366004611ca8565b610a53565b610144610257366004611c26565b610a69565b61018d61026a366004611d77565b610a89565b61014461027d366004611c26565b610aa8565b610144610290366004611b47565b610ad6565b6101446102a3366004611c5e565b610aeb565b61018d6102b6366004611e24565b610b11565b61018d6102c9366004611ca8565b610bae565b6101a2610bea565b60008061031884848080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250610bf992505050565b6001600160a01b039687166000908152600560209081526040808320988a168352978152878220929098168152965250505091205460ff16919050565b6001600160a01b0381166000908152600260209081526040918290208054835181840281018401909452808452606093928301828280156103bf57602002820191906000526020600020905b81546001600160a01b031681526001909101906020018083116103a1575b50505050509050919050565b836103d581610c17565b6103e185858585610c62565b5050505050565b6002602052816000526040600020818154811061040157fe5b6000918252602090912001546001600160a01b03169150829050565b8461042781610c3d565b8561043181610dd9565b6104565760405162461bcd60e51b815260040161044d9061202d565b60405180910390fd5b6000610498878780806020026020016040519081016040528093929190818152602001838360200280828437600092019190915250899250889150610e0b9050565b905060005b818110156104f5576104ed898989848181106104b557fe5b90506020020160208101906104ca9190611b47565b8888858181106104d657fe5b90506020028101906104e891906122c5565b610e57565b60010161049d565b505050505050505050565b336000908152600160209081526040808320805460ff19169055600282529182902080548351818402810184019094528084526060939283018282801561057057602002820191906000526020600020905b81546001600160a01b03168152600190910190602001808311610552575b50939450600093505050505b815181101561071157336000908152600460205260408120835182908590859081106105a457fe5b60200260200101516001600160a01b03166001600160a01b03168152602001908152602001600020905060008090505b81548110156106b35760008282815481106105eb57fe5b600091825260208083209091015433835260059091526040822087516001600160a01b03909216935090829088908890811061062357fe5b60200260200101516001600160a01b03166001600160a01b031681526020019081526020016000206000836001600160a01b03166001600160a01b0316815260200190815260200160002060006101000a81548160ff02191690831515021790555082828154811061069157fe5b600091825260209091200180546001600160a01b0319169055506001016105d4565b5033600090815260046020526040812084519091908590859081106106d457fe5b60200260200101516001600160a01b03166001600160a01b0316815260200190815260200160002060006107089190611a4f565b5060010161057c565b5060005b336000908152600260205260409020548110156107ba5733600090815260026020526040812080548390811061074757fe5b6000918252602080832090910154338084526003835260408085206001600160a01b03909316808652928452808520805460ff19169055908452600290925291208054919250908390811061079857fe5b600091825260209091200180546001600160a01b031916905550600101610715565b503360009081526002602052604081206107d391611a4f565b50565b6000806107e586868686611187565b60405163412a04f160e11b81529091506001600160a01b0382169063825409e2906108169089908990600401611fd1565b60206040518083038186803b15801561082e57600080fd5b505afa158015610842573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906108669190611ee1565b9695505050505050565b8161087a81610c17565b6001600160a01b03831660008181526001602052604090819020805460ff1916851515179055517ffc5fc0cfabd873d47a6133c597c0e71ed6d2662ddaf66bec70ac075043ec9a5d906108ce908590611fc6565b60405180910390a2505050565b846108e581610c17565b6000610927868680806020026020016040519081016040528093929190818152602001838360200280828437600092019190915250889250879150610e0b9050565b905060005b818110156109845761097c8888888481811061094457fe5b90506020020160208101906109599190611b47565b87878581811061096557fe5b905060200281019061097791906122c5565b610c62565b60010161092c565b5050505050505050565b6001600160a01b038083166000908152600460209081526040808320938516835292815290829020805483518184028101840190945280845260609392830182828015610a0457602002820191906000526020600020905b81546001600160a01b031681526001909101906020018083116109e6575b5050505050905092915050565b60046020528260005260406000206020528160005260406000208181548110610a3657fe5b6000918252602090912001546001600160a01b0316925083915050565b83610a5d81610c17565b6103e185858585611224565b600360209081526000928352604080842090915290825290205460ff1681565b84610a9381610c17565b610aa086868686866113b3565b505050505050565b6001600160a01b03918216600090815260036020908152604080832093909416825291909152205460ff1690565b60016020526000908152604090205460ff1681565b600560209081526000938452604080852082529284528284209052825290205460ff1681565b8533610b1d828261145b565b87610b2781611485565b610b3489888888886113b3565b6001600160a01b038916600081815260016020526040808220805460ff19168c151517905580516307ff078f60e11b81529051630ffe0f1e9260048084019391929182900301818387803b158015610b8b57600080fd5b505af1158015610b9f573d6000803e3d6000fd5b50505050505050505050505050565b83610bb881610c3d565b84610bc281610dd9565b610bde5760405162461bcd60e51b815260040161044d9061202d565b610aa086868686610e57565b6000546001600160a01b031681565b600080610c0583611546565b9050610c1081611551565b9392505050565b610c21813361160e565b610c3d5760405162461bcd60e51b815260040161044d90612217565b610c468161169c565b6107d35760405162461bcd60e51b815260040161044d9061208a565b6000610ca383838080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250610bf992505050565b6001600160a01b038087166000908152600560209081526040808320898516845282528083209385168352929052205490915060ff16610cf55760405162461bcd60e51b815260040161044d9061219d565b6001600160a01b038086166000908152600460209081526040808320938816835292905220610d2a908263ffffffff6117a016565b6001600160a01b0380861660008181526005602090815260408083208986168085529083528184209587168452948252808320805460ff1916905592825260048152828220938252929092529020546103e1576001600160a01b0385166000908152600260205260409020610da5908563ffffffff6117a016565b6001600160a01b038086166000908152600360209081526040808320938816835292905220805460ff191690555050505050565b6001600160a01b03811660009081526001602052604081205460ff1680610e055750610e05823361160e565b92915050565b8251600090828114610e2f5760405162461bcd60e51b815260040161044d9061205b565b60008111610e4f5760405162461bcd60e51b815260040161044d90612285565b949350505050565b610e618484610aa8565b610e7d5760405162461bcd60e51b815260040161044d9061216d565b6000610e8b85858585611187565b90506000816001600160a01b031663b8d7b669866040518263ffffffff1660e01b8152600401610ebb9190611f25565b60206040518083038186803b158015610ed357600080fd5b505afa158015610ee7573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610f0b9190611b63565b90506000816001600160a01b03166370a08231886040518263ffffffff1660e01b8152600401610f3b9190611f25565b60206040518083038186803b158015610f5357600080fd5b505afa158015610f67573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610f8b9190611ee1565b90506000806060856001600160a01b0316630f1ff4ba8b8b6040518363ffffffff1660e01b8152600401610fc0929190611fd1565b60006040518083038186803b158015610fd857600080fd5b505afa158015610fec573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526110149190810190611b7f565b925092509250896001600160a01b0316638f6f03328484846040518463ffffffff1660e01b815260040161104a93929190611f52565b600060405180830381600087803b15801561106457600080fd5b505af1158015611078573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526110a09190810190611bf3565b506040516370a0823160e01b81526000906001600160a01b038716906370a08231906110d0908e90600401611f25565b60206040518083038186803b1580156110e857600080fd5b505afa1580156110fc573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906111209190611ee1565b90506001600160a01b03808816908b8116908d167f2422cac5e23c46c890fdcf42d0c64757409df6832174df639337558f09d99c68611165858a63ffffffff6118cd16565b60405161117291906122bc565b60405180910390a45050505050505050505050565b6000806111c984848080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250610bf992505050565b6001600160a01b0380881660009081526005602090815260408083208a8516845282528083209385168352929052205490915060ff1661121b5760405162461bcd60e51b815260040161044d9061224e565b95945050505050565b600061126583838080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250610bf992505050565b6001600160a01b0380871660008181526004602090815260408083208a8616808552908352818420948452600583528184209084528252808320948616835293905291909120549192509060ff16156112d05760405162461bcd60e51b815260040161044d90612138565b8054600180820183556000838152602080822090930180546001600160a01b0319166001600160a01b038781169182179092558a8216808452600586526040808520938c16808652938752808520928552918652818420805460ff19169095179094559282526003845282822090825290925290205460ff16610aa057505050506001600160a01b0391821660008181526002602090815260408083208054600180820183559185528385200180546001600160a01b0319169690971695861790965592825260038152828220938252929092529020805460ff19169091179055565b60006113f5858580806020026020016040519081016040528093929190818152602001838360200280828437600092019190915250879250869150610e0b9050565b905060005b818110156114525761144a8787878481811061141257fe5b90506020020160208101906114279190611b47565b86868581811061143357fe5b905060200281019061144591906122c5565b611224565b6001016113fa565b50505050505050565b611465828261160e565b6114815760405162461bcd60e51b815260040161044d90612217565b5050565b600054604051631d3af8fb60e21b81526001600160a01b03909116906374ebe3ec906114b5908490600401611f25565b60206040518083038186803b1580156114cd57600080fd5b505afa1580156114e1573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906115059190611bd7565b6115215760405162461bcd60e51b815260040161044d906121d4565b61152a8161190f565b6107d35760405162461bcd60e51b815260040161044d906120d2565b805160209091012090565b600080548190611569906001600160a01b031661193e565b6001600160a01b031663e6d642c530856040518363ffffffff1660e01b8152600401611596929190611f39565b60206040518083038186803b1580156115ae57600080fd5b505afa1580156115c2573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906115e69190611b63565b90506001600160a01b038116610e055760405162461bcd60e51b815260040161044d90612109565b6000816001600160a01b0316836001600160a01b031663481c6a756040518163ffffffff1660e01b815260040160206040518083038186803b15801561165357600080fd5b505afa158015611667573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061168b9190611b63565b6001600160a01b0316149392505050565b60008054604051631d3af8fb60e21b81526001600160a01b03909116906374ebe3ec906116cd908590600401611f25565b60206040518083038186803b1580156116e557600080fd5b505afa1580156116f9573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061171d9190611bd7565b8015610e0557506040516335fc6c9f60e21b81526001600160a01b0383169063d7f1b27c90611750903090600401611f25565b60206040518083038186803b15801561176857600080fd5b505afa15801561177c573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610e059190611bd7565b600080611806848054806020026020016040519081016040528092919081815260200182805480156117fb57602002820191906000526020600020905b81546001600160a01b031681526001909101906020018083116117dd575b5050505050846119bd565b91509150806118275760405162461bcd60e51b815260040161044d90611ffe565b8354600019018281146118995784818154811061184057fe5b9060005260206000200160009054906101000a90046001600160a01b031685848154811061186a57fe5b9060005260206000200160006101000a8154816001600160a01b0302191690836001600160a01b031602179055505b848054806118a357fe5b600082815260209020810160001990810180546001600160a01b0319169055019055505b50505050565b6000610c1083836040518060400160405280601e81526020017f536166654d6174683a207375627472616374696f6e206f766572666c6f770000815250611a23565b6040516353bae5f760e01b81526000906001600160a01b038316906353bae5f790611750903090600401611f25565b6040516373b2e76b60e11b81526000906001600160a01b0383169063e765ced69061196d9084906004016122bc565b60206040518083038186803b15801561198557600080fd5b505afa158015611999573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610e059190611b63565b81516000908190815b81811015611a1057846001600160a01b03168682815181106119e457fe5b60200260200101516001600160a01b03161415611a0857925060019150611a1c9050565b6001016119c6565b50600019600092509250505b9250929050565b60008184841115611a475760405162461bcd60e51b815260040161044d9190611feb565b505050900390565b50805460008255906000526020600020908101906107d39190611a8691905b80821115611a825760008155600101611a6e565b5090565b90565b60008083601f840112611a9a578182fd5b50813567ffffffffffffffff811115611ab1578182fd5b6020830191508360208083028501011115611a1c57600080fd5b600082601f830112611adb578081fd5b815167ffffffffffffffff80821115611af2578283fd5b604051601f8301601f191681016020018281118282101715611b12578485fd5b604052828152925082848301602001861015611b2d57600080fd5b611b3e83602083016020880161230c565b50505092915050565b600060208284031215611b58578081fd5b8135610c1081612338565b600060208284031215611b74578081fd5b8151610c1081612338565b600080600060608486031215611b93578182fd5b8351611b9e81612338565b60208501516040860151919450925067ffffffffffffffff811115611bc1578182fd5b611bcd86828701611acb565b9150509250925092565b600060208284031215611be8578081fd5b8151610c108161234d565b600060208284031215611c04578081fd5b815167ffffffffffffffff811115611c1a578182fd5b610e4f84828501611acb565b60008060408385031215611c38578182fd5b8235611c4381612338565b91506020830135611c5381612338565b809150509250929050565b600080600060608486031215611c72578283fd5b8335611c7d81612338565b92506020840135611c8d81612338565b91506040840135611c9d81612338565b809150509250925092565b60008060008060608587031215611cbd578081fd5b8435611cc881612338565b93506020850135611cd881612338565b9250604085013567ffffffffffffffff80821115611cf4578283fd5b81870188601f820112611d05578384fd5b8035925081831115611d15578384fd5b886020848301011115611d26578384fd5b959894975050602090940194505050565b600080600060608486031215611d4b578283fd5b8335611d5681612338565b92506020840135611d6681612338565b929592945050506040919091013590565b600080600080600060608688031215611d8e578081fd5b8535611d9981612338565b9450602086013567ffffffffffffffff80821115611db5578283fd5b611dc189838a01611a89565b90965094506040880135915080821115611dd9578283fd5b50611de688828901611a89565b969995985093965092949392505050565b60008060408385031215611e09578182fd5b8235611e1481612338565b91506020830135611c538161234d565b60008060008060008060808789031215611e3c578384fd5b8635611e4781612338565b95506020870135611e578161234d565b9450604087013567ffffffffffffffff80821115611e73578586fd5b611e7f8a838b01611a89565b90965094506060890135915080821115611e97578283fd5b50611ea489828a01611a89565b979a9699509497509295939492505050565b60008060408385031215611ec8578182fd5b8235611ed381612338565b946020939093013593505050565b600060208284031215611ef2578081fd5b5051919050565b60008151808452611f1181602086016020860161230c565b601f01601f19169290920160200192915050565b6001600160a01b0391909116815260200190565b6001600160a01b03929092168252602082015260400190565b600060018060a01b03851682528360208301526060604083015261121b6060830184611ef9565b6020808252825182820181905260009190848201906040850190845b81811015611fba5783516001600160a01b031683529284019291840191600101611f95565b50909695505050505050565b901515815260200190565b6001600160a01b0392831681529116602082015260400190565b600060208252610c106020830184611ef9565b60208082526015908201527420b2323932b9b9903737ba1034b71030b93930bc9760591b604082015260600190565b60208082526014908201527326bab9ba103132903b30b634b21031b0b63632b960611b604082015260600190565b602080825260159082015274082e4e4c2f240d8cadccee8d040dad2e6dac2e8c6d605b1b604082015260600190565b60208082526028908201527f4d75737420626520612076616c696420616e6420696e697469616c697a65642060408201526729b2ba2a37b5b2b760c11b606082015260800190565b6020808252601e908201527f4d7573742062652070656e64696e6720696e697469616c697a6174696f6e0000604082015260600190565b60208082526015908201527426bab9ba103132903b30b634b21030b230b83a32b960591b604082015260600190565b6020808252818101527f496e746567726174696f6e206e616d6573206d75737420626520756e69717565604082015260600190565b60208082526016908201527514995dd85c99141bdbdb081b9bdd081c1c995cd95b9d60521b604082015260600190565b60208082526019908201527f496e746567726174696f6e206d75737420626520616464656400000000000000604082015260600190565b60208082526023908201527f4d75737420626520636f6e74726f6c6c65722d656e61626c656420536574546f60408201526235b2b760e91b606082015260800190565b6020808252601c908201527f4d7573742062652074686520536574546f6b656e206d616e6167657200000000604082015260600190565b6020808252601f908201527f4164617074657220696e746567726174696f6e206e6f742070726573656e7400604082015260600190565b60208082526018908201527f417272617973206d757374206e6f7420626520656d7074790000000000000000604082015260600190565b90815260200190565b6000808335601e198436030181126122db578283fd5b8084018035925067ffffffffffffffff8311156122f6578384fd5b60200192505036819003821315611a1c57600080fd5b60005b8381101561232757818101518382015260200161230f565b838111156118c75750506000910152565b6001600160a01b03811681146107d357600080fd5b80151581146107d357600080fdfea26469706673582212203bc3d3b381b7657f1ef961c2b1ee9a5a9fa96b0cb8cbbbe25235511e17678b9a64736f6c634300060a0033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/external/contracts/set/ClaimAdapterMock.sol b/external/contracts/set/ClaimAdapterMock.sol new file mode 100644 index 00000000..ff0383a5 --- /dev/null +++ b/external/contracts/set/ClaimAdapterMock.sol @@ -0,0 +1,71 @@ +/* + 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; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { ISetToken } from "../../interfaces/ISetToken.sol"; + +contract ClaimAdapterMock is ERC20 { + + /* ============ State Variables ============ */ + + uint256 public rewards; + + /* ============ Constructor ============ */ + constructor() public ERC20("ClaimAdapter", "CLAIM") {} + + /* ============ External Functions ============ */ + + function setRewards(uint256 _rewards) external { + rewards = _rewards; + } + + function mint() external { + _mint(msg.sender, rewards); + } + + function getClaimCallData( + ISetToken _holder, + address _rewardPool + ) external view returns(address _subject, uint256 _value, bytes memory _calldata) { + // Quell compiler warnings about unused vars + _holder; + _rewardPool; + + bytes memory callData = abi.encodeWithSignature("mint()"); + return (address(this), 0, callData); + } + + function getRewardsAmount(ISetToken _holder, address _rewardPool) external view returns(uint256) { + // Quell compiler warnings about unused vars + _holder; + _rewardPool; + + return rewards; + } + + function getTokenAddress(address _rewardPool) external view returns(IERC20) { + // Quell compiler warnings about unused vars + _rewardPool; + + return this; + } +} diff --git a/external/contracts/set/ClaimModule.sol b/external/contracts/set/ClaimModule.sol new file mode 100644 index 00000000..51e8fc5e --- /dev/null +++ b/external/contracts/set/ClaimModule.sol @@ -0,0 +1,516 @@ +/* + Copyright 2020 Set Labs Inc. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + SPDX-License-Identifier: Apache License, Version 2.0 +*/ + +pragma solidity 0.6.10; +pragma experimental "ABIEncoderV2"; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { AddressArrayUtils } from "../../../lib/AddressArrayUtils.sol"; +import { IClaimAdapter } from "../../../interfaces/IClaimAdapter.sol"; +import { IController } from "../../../interfaces/IController.sol"; +import { ISetToken } from "../../../interfaces/ISetToken.sol"; +import { ModuleBase } from "../../lib/ModuleBase.sol"; + + +/** + * @title ClaimModule + * @author Set Protocol + * + * Module that enables managers to claim tokens from external protocols given to the Set as part of participating in + * incentivized activities of other protocols. The ClaimModule works in conjunction with ClaimAdapters, in which the + * claimAdapterID / integrationNames are stored on the integration registry. + * + * Design: + * The ecosystem is coalescing around a few standards of how reward programs are created, using forks of popular + * contracts such as Synthetix's Mintr. Thus, the Claim architecture reflects a more functional vs external-protocol + * approach where an adapter with common functionality can be used across protocols. + * + * Definitions: + * Reward Pool: A reward pool is a contract associated with an external protocol's reward. Examples of reward pools + * include the Curve sUSDV2 Gauge or the Synthetix iBTC StakingReward contract. + * Adapter: An adapter contains the logic and context for how a reward pool should be claimed - returning the requisite + * function signature. Examples of adapters include StakingRewardAdapter (for getting rewards from Synthetix-like + * reward contracts) and CurveClaimAdapter (for calling Curve Minter contract's mint function) + * ClaimSettings: A reward pool can be associated with multiple awards. For example, a Curve liquidity gauge can be + * associated with the CURVE_CLAIM adapter to claim CRV and CURVE_DIRECT adapter to claim BPT. + */ +contract ClaimModule is ModuleBase { + using AddressArrayUtils for address[]; + + /* ============ Events ============ */ + + event RewardClaimed( + ISetToken indexed _setToken, + address indexed _rewardPool, + IClaimAdapter indexed _adapter, + uint256 _amount + ); + + event AnyoneClaimUpdated( + ISetToken indexed _setToken, + bool _anyoneClaim + ); + + /* ============ Modifiers ============ */ + + /** + * Throws if claim is confined to the manager and caller is not the manager + */ + modifier onlyValidCaller(ISetToken _setToken) { + require(_isValidCaller(_setToken), "Must be valid caller"); + _; + } + + /* ============ State Variables ============ */ + + // Indicates if any address can call claim or just the manager of the SetToken + mapping(ISetToken => bool) public anyoneClaim; + + // Map and array of rewardPool addresses to claim rewards for the SetToken + mapping(ISetToken => address[]) public rewardPoolList; + // Map from set tokens to rewards pool address to isAdded boolean. Used to check if a reward pool has been added in O(1) time + mapping(ISetToken => mapping(address => bool)) public rewardPoolStatus; + + // Map and array of adapters associated to the rewardPool for the SetToken + mapping(ISetToken => mapping(address => address[])) public claimSettings; + // Map from set tokens to rewards pool address to claim adapters to isAdded boolean. Used to check if an adapter has been added in O(1) time + mapping(ISetToken => mapping(address => mapping(address => bool))) public claimSettingsStatus; + + + /* ============ Constructor ============ */ + + constructor(IController _controller) public ModuleBase(_controller) {} + + /* ============ External Functions ============ */ + + /** + * Claim the rewards available on the rewardPool for the specified claim integration. + * Callable only by manager unless manager has set anyoneClaim to true. + * + * @param _setToken Address of SetToken + * @param _rewardPool Address of the rewardPool that identifies the contract governing claims + * @param _integrationName ID of claim module integration (mapping on integration registry) + */ + function claim( + ISetToken _setToken, + address _rewardPool, + string calldata _integrationName + ) + external + onlyValidAndInitializedSet(_setToken) + onlyValidCaller(_setToken) + { + _claim(_setToken, _rewardPool, _integrationName); + } + + /** + * Claims rewards on all the passed rewardPool/claim integration pairs. Callable only by manager unless manager has + * set anyoneClaim to true. + * + * @param _setToken Address of SetToken + * @param _rewardPools Addresses of rewardPools that identifies the contract governing claims. Maps to same + * index integrationNames + * @param _integrationNames Human-readable names matching adapter used to collect claim on pool. Maps to same index + * in rewardPools + */ + function batchClaim( + ISetToken _setToken, + address[] calldata _rewardPools, + string[] calldata _integrationNames + ) + external + onlyValidAndInitializedSet(_setToken) + onlyValidCaller(_setToken) + { + uint256 poolArrayLength = _validateBatchArrays(_rewardPools, _integrationNames); + for (uint256 i = 0; i < poolArrayLength; i++) { + _claim(_setToken, _rewardPools[i], _integrationNames[i]); + } + } + + /** + * SET MANAGER ONLY. Update whether manager allows other addresses to call claim. + * + * @param _setToken Address of SetToken + */ + function updateAnyoneClaim(ISetToken _setToken, bool _anyoneClaim) external onlyManagerAndValidSet(_setToken) { + anyoneClaim[_setToken] = _anyoneClaim; + emit AnyoneClaimUpdated(_setToken, _anyoneClaim); + } + /** + * SET MANAGER ONLY. Adds a new claim integration for an existent rewardPool. If rewardPool doesn't have existing + * claims then rewardPool is added to rewardPoolLiost. The claim integration is associated to an adapter that + * provides the functionality to claim the rewards for a specific token. + * + * @param _setToken Address of SetToken + * @param _rewardPool Address of the rewardPool that identifies the contract governing claims + * @param _integrationName ID of claim module integration (mapping on integration registry) + */ + function addClaim( + ISetToken _setToken, + address _rewardPool, + string calldata _integrationName + ) + external + onlyManagerAndValidSet(_setToken) + { + _addClaim(_setToken, _rewardPool, _integrationName); + } + + /** + * SET MANAGER ONLY. Adds a new rewardPool to the list to perform claims for the SetToken indicating the list of + * claim integrations. Each claim integration is associated to an adapter that provides the functionality to claim + * the rewards for a specific token. + * + * @param _setToken Address of SetToken + * @param _rewardPools Addresses of rewardPools that identifies the contract governing claims. Maps to same + * index integrationNames + * @param _integrationNames Human-readable names matching adapter used to collect claim on pool. Maps to same index + * in rewardPools + */ + function batchAddClaim( + ISetToken _setToken, + address[] calldata _rewardPools, + string[] calldata _integrationNames + ) + external + onlyManagerAndValidSet(_setToken) + { + _batchAddClaim(_setToken, _rewardPools, _integrationNames); + } + + /** + * SET MANAGER ONLY. Removes a claim integration from an existent rewardPool. If no claim remains for reward pool then + * reward pool is removed from rewardPoolList. + * + * @param _setToken Address of SetToken + * @param _rewardPool Address of the rewardPool that identifies the contract governing claims + * @param _integrationName ID of claim module integration (mapping on integration registry) + */ + function removeClaim( + ISetToken _setToken, + address _rewardPool, + string calldata _integrationName + ) + external + onlyManagerAndValidSet(_setToken) + { + _removeClaim(_setToken, _rewardPool, _integrationName); + } + + /** + * SET MANAGER ONLY. Batch removes claims from SetToken's settings. + * + * @param _setToken Address of SetToken + * @param _rewardPools Addresses of rewardPools that identifies the contract governing claims. Maps to same index + * integrationNames + * @param _integrationNames Human-readable names matching adapter used to collect claim on pool. Maps to same index in + * rewardPools + */ + function batchRemoveClaim( + ISetToken _setToken, + address[] calldata _rewardPools, + string[] calldata _integrationNames + ) + external + onlyManagerAndValidSet(_setToken) + { + uint256 poolArrayLength = _validateBatchArrays(_rewardPools, _integrationNames); + for (uint256 i = 0; i < poolArrayLength; i++) { + _removeClaim(_setToken, _rewardPools[i], _integrationNames[i]); + } + } + + /** + * SET MANAGER ONLY. Initializes this module to the SetToken. + * + * @param _setToken Instance of the SetToken to issue + * @param _anyoneClaim Boolean indicating if anyone can claim or just manager + * @param _rewardPools Addresses of rewardPools that identifies the contract governing claims. Maps to same index + * integrationNames + * @param _integrationNames Human-readable names matching adapter used to collect claim on pool. Maps to same index in + * rewardPools + */ + function initialize( + ISetToken _setToken, + bool _anyoneClaim, + address[] calldata _rewardPools, + string[] calldata _integrationNames + ) + external + onlySetManager(_setToken, msg.sender) + onlyValidAndPendingSet(_setToken) + { + _batchAddClaim(_setToken, _rewardPools, _integrationNames); + anyoneClaim[_setToken] = _anyoneClaim; + _setToken.initializeModule(); + } + + /** + * Removes this module from the SetToken, via call by the SetToken. + */ + function removeModule() external override { + delete anyoneClaim[ISetToken(msg.sender)]; + + // explicitly delete all elements for gas refund + address[] memory setTokenPoolList = rewardPoolList[ISetToken(msg.sender)]; + for (uint256 i = 0; i < setTokenPoolList.length; i++) { + + address[] storage adapterList = claimSettings[ISetToken(msg.sender)][setTokenPoolList[i]]; + for (uint256 j = 0; j < adapterList.length; j++) { + + address toRemove = adapterList[j]; + claimSettingsStatus[ISetToken(msg.sender)][setTokenPoolList[i]][toRemove] = false; + + delete adapterList[j]; + } + delete claimSettings[ISetToken(msg.sender)][setTokenPoolList[i]]; + } + + for (uint256 i = 0; i < rewardPoolList[ISetToken(msg.sender)].length; i++) { + address toRemove = rewardPoolList[ISetToken(msg.sender)][i]; + rewardPoolStatus[ISetToken(msg.sender)][toRemove] = false; + + delete rewardPoolList[ISetToken(msg.sender)][i]; + } + delete rewardPoolList[ISetToken(msg.sender)]; + } + + /** + * Get list of rewardPools to perform claims for the SetToken. + * + * @param _setToken Address of SetToken + * @return Array of rewardPool addresses to claim rewards for the SetToken + */ + function getRewardPools(ISetToken _setToken) external view returns (address[] memory) { + return rewardPoolList[_setToken]; + } + + /** + * Get boolean indicating if the rewardPool is in the list to perform claims for the SetToken. + * + * @param _setToken Address of SetToken + * @param _rewardPool Address of rewardPool + * @return Boolean indicating if the rewardPool is in the list for claims. + */ + function isRewardPool(ISetToken _setToken, address _rewardPool) public view returns (bool) { + return rewardPoolStatus[_setToken][_rewardPool]; + } + + /** + * Get list of claim integration of the rewardPool for the SetToken. + * + * @param _setToken Address of SetToken + * @param _rewardPool Address of rewardPool + * @return Array of adapter addresses associated to the rewardPool for the SetToken + */ + function getRewardPoolClaims(ISetToken _setToken, address _rewardPool) external view returns (address[] memory) { + return claimSettings[_setToken][_rewardPool]; + } + + /** + * Get boolean indicating if the adapter address of the claim integration is associated to the rewardPool. + * + * @param _setToken Address of SetToken + * @param _rewardPool Address of rewardPool + * @param _integrationName ID of claim module integration (mapping on integration registry) + * @return Boolean indicating if the claim integration is associated to the rewardPool. + */ + function isRewardPoolClaim( + ISetToken _setToken, + address _rewardPool, + string calldata _integrationName + ) + external + view + returns (bool) + { + address adapter = getAndValidateAdapter(_integrationName); + return claimSettingsStatus[_setToken][_rewardPool][adapter]; + } + + /** + * Get the rewards available to be claimed by the claim integration on the rewardPool. + * + * @param _setToken Address of SetToken + * @param _rewardPool Address of the rewardPool that identifies the contract governing claims + * @param _integrationName ID of claim module integration (mapping on integration registry) + * @return rewards Amount of units available to be claimed + */ + function getRewards( + ISetToken _setToken, + address _rewardPool, + string calldata _integrationName + ) + external + view + returns (uint256) + { + IClaimAdapter adapter = _getAndValidateIntegrationAdapter(_setToken, _rewardPool, _integrationName); + return adapter.getRewardsAmount(_setToken, _rewardPool); + } + + /* ============ Internal Functions ============ */ + + /** + * Claim the rewards, if available, on the rewardPool using the specified adapter. Interact with the adapter to get + * the rewards available, the calldata for the SetToken to invoke the claim and the token associated to the claim. + * + * @param _setToken Address of SetToken + * @param _rewardPool Address of the rewardPool that identifies the contract governing claims + * @param _integrationName Human readable name of claim integration + */ + function _claim(ISetToken _setToken, address _rewardPool, string calldata _integrationName) internal { + require(isRewardPool(_setToken, _rewardPool), "RewardPool not present"); + IClaimAdapter adapter = _getAndValidateIntegrationAdapter(_setToken, _rewardPool, _integrationName); + + IERC20 rewardsToken = IERC20(adapter.getTokenAddress(_rewardPool)); + uint256 initRewardsBalance = rewardsToken.balanceOf(address(_setToken)); + + ( + address callTarget, + uint256 callValue, + bytes memory callByteData + ) = adapter.getClaimCallData( + _setToken, + _rewardPool + ); + + _setToken.invoke(callTarget, callValue, callByteData); + + uint256 finalRewardsBalance = rewardsToken.balanceOf(address(_setToken)); + + emit RewardClaimed(_setToken, _rewardPool, adapter, finalRewardsBalance.sub(initRewardsBalance)); + } + + /** + * Gets the adapter and validate it is associated to the list of claim integration of a rewardPool. + * + * @param _setToken Address of SetToken + * @param _rewardsPool Sddress of rewards pool + * @param _integrationName ID of claim module integration (mapping on integration registry) + */ + function _getAndValidateIntegrationAdapter( + ISetToken _setToken, + address _rewardsPool, + string calldata _integrationName + ) + internal + view + returns (IClaimAdapter) + { + address adapter = getAndValidateAdapter(_integrationName); + require(claimSettingsStatus[_setToken][_rewardsPool][adapter], "Adapter integration not present"); + return IClaimAdapter(adapter); + } + + /** + * Validates and store the adapter address used to claim rewards for the passed rewardPool. If after adding + * adapter to pool length of adapters is 1 then add to rewardPoolList as well. + * + * @param _setToken Address of SetToken + * @param _rewardPool Address of the rewardPool that identifies the contract governing claims + * @param _integrationName ID of claim module integration (mapping on integration registry) + */ + function _addClaim(ISetToken _setToken, address _rewardPool, string calldata _integrationName) internal { + address adapter = getAndValidateAdapter(_integrationName); + address[] storage _rewardPoolClaimSettings = claimSettings[_setToken][_rewardPool]; + + require(!claimSettingsStatus[_setToken][_rewardPool][adapter], "Integration names must be unique"); + _rewardPoolClaimSettings.push(adapter); + claimSettingsStatus[_setToken][_rewardPool][adapter] = true; + + if (!rewardPoolStatus[_setToken][_rewardPool]) { + rewardPoolList[_setToken].push(_rewardPool); + rewardPoolStatus[_setToken][_rewardPool] = true; + } + } + + /** + * Internal version. Adds a new rewardPool to the list to perform claims for the SetToken indicating the list of claim + * integrations. Each claim integration is associated to an adapter that provides the functionality to claim the rewards + * for a specific token. + * + * @param _setToken Address of SetToken + * @param _rewardPools Addresses of rewardPools that identifies the contract governing claims. Maps to same + * index integrationNames + * @param _integrationNames Human-readable names matching adapter used to collect claim on pool. Maps to same index + * in rewardPools + */ + function _batchAddClaim( + ISetToken _setToken, + address[] calldata _rewardPools, + string[] calldata _integrationNames + ) + internal + { + uint256 poolArrayLength = _validateBatchArrays(_rewardPools, _integrationNames); + for (uint256 i = 0; i < poolArrayLength; i++) { + _addClaim(_setToken, _rewardPools[i], _integrationNames[i]); + } + } + + /** + * Validates and stores the adapter address used to claim rewards for the passed rewardPool. If no adapters + * left after removal then remove rewardPool from rewardPoolList and delete entry in claimSettings. + * + * @param _setToken Address of SetToken + * @param _rewardPool Address of the rewardPool that identifies the contract governing claims + * @param _integrationName ID of claim module integration (mapping on integration registry) + */ + function _removeClaim(ISetToken _setToken, address _rewardPool, string calldata _integrationName) internal { + address adapter = getAndValidateAdapter(_integrationName); + + require(claimSettingsStatus[_setToken][_rewardPool][adapter], "Integration must be added"); + claimSettings[_setToken][_rewardPool].removeStorage(adapter); + claimSettingsStatus[_setToken][_rewardPool][adapter] = false; + + if (claimSettings[_setToken][_rewardPool].length == 0) { + rewardPoolList[_setToken].removeStorage(_rewardPool); + rewardPoolStatus[_setToken][_rewardPool] = false; + } + } + + /** + * For batch functions validate arrays are of equal length and not empty. Return length of array for iteration. + * + * @param _rewardPools Addresses of the rewardPool that identifies the contract governing claims + * @param _integrationNames IDs of claim module integration (mapping on integration registry) + * @return Length of arrays + */ + function _validateBatchArrays( + address[] memory _rewardPools, + string[] calldata _integrationNames + ) + internal + pure + returns(uint256) + { + uint256 poolArrayLength = _rewardPools.length; + require(poolArrayLength == _integrationNames.length, "Array length mismatch"); + require(poolArrayLength > 0, "Arrays must not be empty"); + return poolArrayLength; + } + + /** + * If claim is confined to the manager, manager must be caller + * + * @param _setToken Address of SetToken + * @return bool Whether or not the caller is valid + */ + function _isValidCaller(ISetToken _setToken) internal view returns(bool) { + return anyoneClaim[_setToken] || isSetManager(_setToken, msg.sender); + } +} \ No newline at end of file diff --git a/test/global-extensions/globalClaimExtension.spec.ts b/test/global-extensions/globalClaimExtension.spec.ts new file mode 100644 index 00000000..2e2b461a --- /dev/null +++ b/test/global-extensions/globalClaimExtension.spec.ts @@ -0,0 +1,2156 @@ +import "module-alias/register"; + +import { BigNumber, Contract } from "ethers"; +import { Address, Account } from "@utils/types"; +import { ADDRESS_ZERO, ZERO, PRECISE_UNIT } from "@utils/constants"; +import { + DelegatedManager, + GlobalClaimExtension, + ManagerCore, +} from "@utils/contracts/index"; +import { + SetToken, + AirdropModule, + ClaimModule, + ClaimAdapterMock +} from "@utils/contracts/setV2"; +import DeployHelper from "@utils/deploys"; +import { + addSnapshotBeforeRestoreAfterEach, + ether, + getAccounts, + getWaffleExpect, + getRandomAddress, + preciseMul, + preciseDiv, + getSetFixture, + getRandomAccount, +} from "@utils/index"; +import { ContractTransaction } from "ethers"; +import { AirdropSettings } from "@utils/types"; +import { SetFixture } from "@utils/fixtures"; + +const expect = getWaffleExpect(); + +describe("ClaimExtension", () => { + let owner: Account; + let methodologist: Account; + let operator: Account; + let ownerFeeRecipient: Account; + let factory: Account; + + let deployer: DeployHelper; + let setToken: SetToken; + let setV2Setup: SetFixture; + + let managerCore: ManagerCore; + let delegatedManager: DelegatedManager; + let claimExtension: GlobalClaimExtension; + let ownerFeeSplit: BigNumber; + + let airdropModule: AirdropModule; + let claimModule: ClaimModule; + let claimAdapterMockOne: ClaimAdapterMock; + let claimAdapterMockTwo: ClaimAdapterMock; + const claimAdapterMockIntegrationNameOne: string = "MOCK_CLAIM_ONE"; + const claimAdapterMockIntegrationNameTwo: string = "MOCK_CLAIM_TWO"; + + before(async () => { + [ + owner, + methodologist, + operator, + ownerFeeRecipient, + factory, + ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + + setV2Setup = getSetFixture(owner.address); + await setV2Setup.initialize(); + + airdropModule = await deployer.setV2.deployAirdropModule(setV2Setup.controller.address); + await setV2Setup.controller.addModule(airdropModule.address); + + claimModule = await deployer.setV2.deployClaimModule(setV2Setup.controller.address); + await setV2Setup.controller.addModule(claimModule.address); + claimAdapterMockOne = await deployer.setV2.deployClaimAdapterMock(); + await setV2Setup.integrationRegistry.addIntegration( + claimModule.address, + claimAdapterMockIntegrationNameOne, + claimAdapterMockOne.address + ); + claimAdapterMockTwo = await deployer.setV2.deployClaimAdapterMock(); + await setV2Setup.integrationRegistry.addIntegration( + claimModule.address, + claimAdapterMockIntegrationNameTwo, + claimAdapterMockTwo.address + ); + + managerCore = await deployer.managerCore.deployManagerCore(); + + claimExtension = await deployer.globalExtensions.deployGlobalClaimExtension( + managerCore.address, + airdropModule.address, + claimModule.address, + setV2Setup.integrationRegistry.address + ); + + setToken = await setV2Setup.createSetToken( + [setV2Setup.weth.address], + [ether(1)], + [setV2Setup.issuanceModule.address, airdropModule.address, claimModule.address] + ); + + await setV2Setup.issuanceModule.initialize(setToken.address, ADDRESS_ZERO); + + delegatedManager = await deployer.manager.deployDelegatedManager( + setToken.address, + factory.address, + methodologist.address, + [claimExtension.address], + [operator.address], + [setV2Setup.usdc.address, setV2Setup.weth.address], + true + ); + + ownerFeeSplit = ether(0.6); + await delegatedManager.connect(owner.wallet).updateOwnerFeeSplit(ownerFeeSplit); + await delegatedManager.connect(methodologist.wallet).updateOwnerFeeSplit(ownerFeeSplit); + await delegatedManager.connect(owner.wallet).updateOwnerFeeRecipient(ownerFeeRecipient.address); + + await setToken.setManager(delegatedManager.address); + + await managerCore.initialize([claimExtension.address], [factory.address]); + await managerCore.connect(factory.wallet).addManager(delegatedManager.address); + }); + + addSnapshotBeforeRestoreAfterEach(); + + describe("#constructor", async () => { + let subjectManagerCore: Address; + let subjectAirdropModule: Address; + let subjectClaimModule: Address; + let subjectIntegrationRegistry: Address; + + beforeEach(async () => { + subjectManagerCore = managerCore.address; + subjectAirdropModule = airdropModule.address; + subjectClaimModule = claimModule.address; + subjectIntegrationRegistry = setV2Setup.integrationRegistry.address; + }); + + async function subject(): Promise { + return await deployer.globalExtensions.deployGlobalClaimExtension( + subjectManagerCore, + subjectAirdropModule, + subjectClaimModule, + subjectIntegrationRegistry + ); + } + + it("should set the correct AirdropModule address", async () => { + const claimExtension = await subject(); + + const storedModule = await claimExtension.airdropModule(); + expect(storedModule).to.eq(subjectAirdropModule); + }); + + it("should set the correct ClaimModule address", async () => { + const claimExtension = await subject(); + + const storedModule = await claimExtension.claimModule(); + expect(storedModule).to.eq(subjectClaimModule); + }); + + it("should set the correct IntegrationRegistry address", async () => { + const claimExtension = await subject(); + + const storedIntegrationRegistry = await claimExtension.integrationRegistry(); + expect(storedIntegrationRegistry).to.eq(subjectIntegrationRegistry); + }); + }); + + describe("#initializeAirdropModule", async () => { + let airdrops: Address[]; + let airdropFee: BigNumber; + let anyoneAbsorb: boolean; + let airdropFeeRecipient: Address; + + let subjectDelegatedManager: Address; + let subjectAirdropSettings: AirdropSettings; + let subjectCaller: Account; + + before(async () => { + airdrops = [setV2Setup.usdc.address, setV2Setup.weth.address]; + airdropFee = ether(.2); + anyoneAbsorb = true; + airdropFeeRecipient = delegatedManager.address; + }); + + beforeEach(async () => { + await claimExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + + subjectDelegatedManager = delegatedManager.address; + subjectAirdropSettings = { + airdrops, + feeRecipient: airdropFeeRecipient, + airdropFee, + anyoneAbsorb, + } as AirdropSettings; + subjectCaller = owner; + }); + + async function subject(): Promise { + return claimExtension.connect(subjectCaller.wallet).initializeAirdropModule( + subjectDelegatedManager, + subjectAirdropSettings + ); + } + + it("should initialize the AirdropModule on the SetToken", async () => { + await subject(); + + const isModuleInitialized: Boolean = await setToken.isInitializedModule(airdropModule.address); + expect(isModuleInitialized).to.eq(true); + }); + + it("should set the correct airdrops and anyoneAbsorb fields", async () => { + await subject(); + + const airdropSettings: any = await airdropModule.airdropSettings(setToken.address); + const airdrops = await airdropModule.getAirdrops(setToken.address); + + expect(JSON.stringify(airdrops)).to.eq(JSON.stringify(airdrops)); + expect(airdropSettings.airdropFee).to.eq(airdropFee); + expect(airdropSettings.anyoneAbsorb).to.eq(anyoneAbsorb); + }); + + it("should set the correct isAirdrop state", async () => { + await subject(); + + const wethIsAirdrop = await airdropModule.isAirdrop(setToken.address, setV2Setup.weth.address); + const usdcIsAirdrop = await airdropModule.isAirdrop(setToken.address, setV2Setup.usdc.address); + + expect(wethIsAirdrop).to.be.true; + expect(usdcIsAirdrop).to.be.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([claimExtension.address]); + await delegatedManager.connect(owner.wallet).setManager(owner.address); + await setToken.connect(owner.wallet).removeModule(airdropModule.address); + await setToken.connect(owner.wallet).setManager(delegatedManager.address); + await delegatedManager.connect(owner.wallet).addExtensions([claimExtension.address]); + await claimExtension.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([claimExtension.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be initialized extension"); + }); + }); + + describe("when the extension is pending", async () => { + beforeEach(async () => { + await delegatedManager.connect(owner.wallet).removeExtensions([claimExtension.address]); + await delegatedManager.connect(owner.wallet).addExtensions([claimExtension.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be initialized extension"); + }); + }); + + 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("#initializeClaimModule", async () => { + let subjectDelegatedManager: Address; + let subjectRewardPools: Address[]; + let subjectIntegrations: string[]; + let subjectAnyoneClaim: boolean; + let subjectCaller: Account; + + beforeEach(async () => { + await claimExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + + subjectDelegatedManager = delegatedManager.address; + subjectRewardPools = [await getRandomAddress(), await getRandomAddress()]; + subjectIntegrations = [claimAdapterMockIntegrationNameOne, claimAdapterMockIntegrationNameTwo]; + subjectAnyoneClaim = true; + subjectCaller = owner; + }); + + async function subject(): Promise { + return claimExtension.connect(subjectCaller.wallet).initializeClaimModule( + subjectDelegatedManager, + subjectAnyoneClaim, + subjectRewardPools, + subjectIntegrations + ); + } + + it("should initialize the ClaimModule on the SetToken", async () => { + await subject(); + + const isModuleInitialized: Boolean = await setToken.isInitializedModule(claimModule.address); + expect(isModuleInitialized).to.eq(true); + }); + + it("should set the anyoneClaim field", async () => { + const anyoneClaimBefore = await claimModule.anyoneClaim(setToken.address); + expect(anyoneClaimBefore).to.eq(false); + + await subject(); + + const anyoneClaim = await claimModule.anyoneClaim(setToken.address); + expect(anyoneClaim).to.eq(true); + }); + + it("should add the rewardPools to the rewardPoolList", async () => { + expect((await claimModule.getRewardPools(setToken.address)).length).to.eq(0); + + await subject(); + + const rewardPools = await claimModule.getRewardPools(setToken.address); + expect(rewardPools[0]).to.eq(subjectRewardPools[0]); + expect(rewardPools[1]).to.eq(subjectRewardPools[1]); + }); + + it("should add all new integrations for the rewardPools", async () => { + await subject(); + + const rewardPoolOneClaims = await claimModule.getRewardPoolClaims(setToken.address, subjectRewardPools[0]); + const rewardPoolTwoClaims = await claimModule.getRewardPoolClaims(setToken.address, subjectRewardPools[1]); + expect(rewardPoolOneClaims[0]).to.eq(claimAdapterMockOne.address); + expect(rewardPoolTwoClaims[0]).to.eq(claimAdapterMockTwo.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 subject(); + await delegatedManager.connect(owner.wallet).removeExtensions([claimExtension.address]); + await delegatedManager.connect(owner.wallet).setManager(owner.address); + await setToken.connect(owner.wallet).removeModule(claimModule.address); + await setToken.connect(owner.wallet).setManager(delegatedManager.address); + await delegatedManager.connect(owner.wallet).addExtensions([claimExtension.address]); + await claimExtension.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([claimExtension.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be initialized extension"); + }); + }); + + describe("when the extension is pending", async () => { + beforeEach(async () => { + await delegatedManager.connect(owner.wallet).removeExtensions([claimExtension.address]); + await delegatedManager.connect(owner.wallet).addExtensions([claimExtension.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be initialized extension"); + }); + }); + + 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 claimExtension.connect(subjectCaller.wallet).initializeExtension(subjectDelegatedManager); + } + + it("should store the correct SetToken and DelegatedManager on the ClaimExtension", async () => { + await subject(); + + const storedDelegatedManager: Address = await claimExtension.setManagers(setToken.address); + expect(storedDelegatedManager).to.eq(delegatedManager.address); + }); + + it("should initialize the ClaimExtension on the DelegatedManager", async () => { + await subject(); + + const isExtensionInitialized: Boolean = await delegatedManager.isInitializedExtension(claimExtension.address); + expect(isExtensionInitialized).to.eq(true); + }); + + it("should emit the correct ClaimExtensionInitialized event", async () => { + await expect(subject()).to.emit(claimExtension, "ClaimExtensionInitialized").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 claimExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + await delegatedManager.connect(owner.wallet).removeExtensions([claimExtension.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 claimExtension.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("#initializeModulesAndExtension", async () => { + let airdrops: Address[]; + let airdropFee: BigNumber; + let anyoneAbsorb: boolean; + let airdropFeeRecipient: Address; + + let subjectDelegatedManager: Address; + let subjectAirdropSettings: AirdropSettings; + let subjectRewardPools: Address[]; + let subjectIntegrations: string[]; + let subjectAnyoneClaim: boolean; + let subjectCaller: Account; + + before(async () => { + airdrops = [setV2Setup.usdc.address, setV2Setup.weth.address]; + airdropFee = ether(.2); + anyoneAbsorb = true; + airdropFeeRecipient = delegatedManager.address; + }); + + beforeEach(async () => { + subjectDelegatedManager = delegatedManager.address; + subjectAirdropSettings = { + airdrops, + feeRecipient: airdropFeeRecipient, + airdropFee, + anyoneAbsorb, + } as AirdropSettings; + subjectRewardPools = [await getRandomAddress(), await getRandomAddress()]; + subjectIntegrations = [claimAdapterMockIntegrationNameOne, claimAdapterMockIntegrationNameTwo]; + subjectAnyoneClaim = true; + subjectCaller = owner; + }); + + async function subject(): Promise { + return claimExtension.connect(subjectCaller.wallet).initializeModulesAndExtension( + subjectDelegatedManager, + subjectAirdropSettings, + subjectAnyoneClaim, + subjectRewardPools, + subjectIntegrations + ); + } + + it("should initialize the AirdropModule and ClaimModule on the SetToken", async () => { + await subject(); + + const isAirdropModuleInitialized: Boolean = await setToken.isInitializedModule(airdropModule.address); + const isClaimModuleInitialized: Boolean = await setToken.isInitializedModule(claimModule.address); + expect(isAirdropModuleInitialized).to.eq(true); + expect(isClaimModuleInitialized).to.eq(true); + }); + + it("should set the correct airdrops and anyoneAbsorb fields", async () => { + await subject(); + + const airdropSettings: any = await airdropModule.airdropSettings(setToken.address); + const airdrops = await airdropModule.getAirdrops(setToken.address); + + expect(JSON.stringify(airdrops)).to.eq(JSON.stringify(airdrops)); + expect(airdropSettings.airdropFee).to.eq(airdropFee); + expect(airdropSettings.anyoneAbsorb).to.eq(anyoneAbsorb); + }); + + it("should set the correct isAirdrop state", async () => { + await subject(); + + const wethIsAirdrop = await airdropModule.isAirdrop(setToken.address, setV2Setup.weth.address); + const usdcIsAirdrop = await airdropModule.isAirdrop(setToken.address, setV2Setup.usdc.address); + + expect(wethIsAirdrop).to.be.true; + expect(usdcIsAirdrop).to.be.true; + }); + + it("should set the anyoneClaim field", async () => { + const anyoneClaimBefore = await claimModule.anyoneClaim(setToken.address); + expect(anyoneClaimBefore).to.eq(false); + + await subject(); + + const anyoneClaim = await claimModule.anyoneClaim(setToken.address); + expect(anyoneClaim).to.eq(true); + }); + + it("should add the rewardPools to the rewardPoolList", async () => { + expect((await claimModule.getRewardPools(setToken.address)).length).to.eq(0); + + await subject(); + + const rewardPools = await claimModule.getRewardPools(setToken.address); + expect(rewardPools[0]).to.eq(subjectRewardPools[0]); + expect(rewardPools[1]).to.eq(subjectRewardPools[1]); + }); + + it("should add all new integrations for the rewardPools", async () => { + await subject(); + + const rewardPoolOneClaims = await claimModule.getRewardPoolClaims(setToken.address, subjectRewardPools[0]); + const rewardPoolTwoClaims = await claimModule.getRewardPoolClaims(setToken.address, subjectRewardPools[1]); + expect(rewardPoolOneClaims[0]).to.eq(claimAdapterMockOne.address); + expect(rewardPoolTwoClaims[0]).to.eq(claimAdapterMockTwo.address); + }); + + it("should store the correct SetToken and DelegatedManager on the ClaimExtension", async () => { + await subject(); + + const storedDelegatedManager: Address = await claimExtension.setManagers(setToken.address); + expect(storedDelegatedManager).to.eq(delegatedManager.address); + }); + + it("should initialize the ClaimExtension on the DelegatedManager", async () => { + await subject(); + + const isExtensionInitialized: Boolean = await delegatedManager.isInitializedExtension(claimExtension.address); + expect(isExtensionInitialized).to.eq(true); + }); + + it("should emit the correct ClaimExtensionInitialized event", async () => { + await expect(subject()).to.emit(claimExtension, "ClaimExtensionInitialized").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 AirdropModule is not pending or initialized", async () => { + beforeEach(async () => { + await claimExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + await claimExtension.connect(owner.wallet).initializeAirdropModule( + delegatedManager.address, + { + airdrops, + feeRecipient: airdropFeeRecipient, + airdropFee, + anyoneAbsorb, + } as AirdropSettings + ); + await delegatedManager.connect(owner.wallet).removeExtensions([claimExtension.address]); + await delegatedManager.connect(owner.wallet).setManager(owner.address); + await setToken.connect(owner.wallet).removeModule(airdropModule.address); + await setToken.connect(owner.wallet).setManager(delegatedManager.address); + await delegatedManager.connect(owner.wallet).addExtensions([claimExtension.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be pending initialization"); + }); + }); + + describe("when the AirdropModule is already initialized", async () => { + beforeEach(async () => { + await claimExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + await claimExtension.connect(owner.wallet).initializeAirdropModule( + delegatedManager.address, + { + airdrops, + feeRecipient: airdropFeeRecipient, + airdropFee, + anyoneAbsorb, + } as AirdropSettings + ); + await delegatedManager.connect(owner.wallet).removeExtensions([claimExtension.address]); + await delegatedManager.connect(owner.wallet).addExtensions([claimExtension.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be pending initialization"); + }); + }); + + describe("when the ClaimModule is not pending or initialized", async () => { + beforeEach(async () => { + await claimExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + await claimExtension.connect(owner.wallet).initializeClaimModule( + delegatedManager.address, + true, + [await getRandomAddress(), await getRandomAddress()], + [claimAdapterMockIntegrationNameOne, claimAdapterMockIntegrationNameTwo] + ); + await delegatedManager.connect(owner.wallet).removeExtensions([claimExtension.address]); + await delegatedManager.connect(owner.wallet).setManager(owner.address); + await setToken.connect(owner.wallet).removeModule(claimModule.address); + await setToken.connect(owner.wallet).setManager(delegatedManager.address); + await delegatedManager.connect(owner.wallet).addExtensions([claimExtension.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be pending initialization"); + }); + }); + + describe("when the ClaimModule is already initialized", async () => { + beforeEach(async () => { + await claimExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + await claimExtension.connect(owner.wallet).initializeClaimModule( + delegatedManager.address, + true, + [await getRandomAddress(), await getRandomAddress()], + [claimAdapterMockIntegrationNameOne, claimAdapterMockIntegrationNameTwo] + ); + await delegatedManager.connect(owner.wallet).removeExtensions([claimExtension.address]); + await delegatedManager.connect(owner.wallet).addExtensions([claimExtension.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 claimExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + await delegatedManager.connect(owner.wallet).removeExtensions([claimExtension.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 claimExtension.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 subjectClaimExtension: Address[]; + let subjectCaller: Account; + + beforeEach(async () => { + await claimExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + + subjectManager = delegatedManager; + subjectClaimExtension = [claimExtension.address]; + subjectCaller = owner; + }); + + async function subject(): Promise { + return subjectManager.connect(subjectCaller.wallet).removeExtensions(subjectClaimExtension); + } + + it("should clear SetToken and DelegatedManager from ClaimExtension state", async () => { + await subject(); + + const storedDelegatedManager: Address = await claimExtension.setManagers(setToken.address); + expect(storedDelegatedManager).to.eq(ADDRESS_ZERO); + }); + + it("should emit the correct ExtensionRemoved event", async () => { + await expect(subject()).to.emit(claimExtension, "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"); + }); + }); + }); + + context("when the ClaimExtension, AirdropModule, and ClaimModule are initialized", async () => { + let airdrops: Address[]; + let airdropFee: BigNumber; + let anyoneAbsorb: boolean; + let airdropFeeRecipient: Address; + + let rewardPools: Address[]; + let integrations: string[]; + let anyoneClaim: boolean; + + let protocolFee: BigNumber; + + before(async () => { + airdrops = [setV2Setup.usdc.address, setV2Setup.weth.address]; + airdropFee = ether(.2); + anyoneAbsorb = false; + airdropFeeRecipient = delegatedManager.address; + + rewardPools = [await getRandomAddress(), await getRandomAddress()]; + integrations = [claimAdapterMockIntegrationNameOne, claimAdapterMockIntegrationNameTwo]; + anyoneClaim = false; + + await claimExtension.connect(owner.wallet).initializeModulesAndExtension( + delegatedManager.address, + { + airdrops, + feeRecipient: airdropFeeRecipient, + airdropFee, + anyoneAbsorb, + }, + anyoneClaim, + rewardPools, + integrations, + ); + + protocolFee = ether(.15); + await setV2Setup.controller.addFee(airdropModule.address, ZERO, protocolFee); + + await setV2Setup.issuanceModule.issue(setToken.address, ether(1), owner.address); + }); + + describe("#distributeFees", async () => { + let numTokens: BigNumber; + let subjectToken: Address; + let subjectSetToken: Address; + + beforeEach(async () => { + numTokens = ether(1); + await setV2Setup.dai.transfer(delegatedManager.address, numTokens); + + subjectToken = setV2Setup.dai.address; + subjectSetToken = setToken.address; + }); + + async function subject(): Promise { + return await claimExtension.distributeFees(subjectSetToken, subjectToken); + } + + it("should send correct amount of fees to owner fee recipient and methodologist", async () => { + const ownerFeeRecipientBalanceBefore = await setV2Setup.dai.balanceOf(ownerFeeRecipient.address); + const methodologistBalanceBefore = await setV2Setup.dai.balanceOf(methodologist.address); + + await subject(); + + const expectedOwnerTake = preciseMul(numTokens, ownerFeeSplit); + const expectedMethodologistTake = numTokens.sub(expectedOwnerTake); + + const ownerFeeRecipientBalanceAfter = await setV2Setup.dai.balanceOf(ownerFeeRecipient.address); + const methodologistBalanceAfter = await setV2Setup.dai.balanceOf(methodologist.address); + + const ownerFeeRecipientBalanceIncrease = ownerFeeRecipientBalanceAfter.sub(ownerFeeRecipientBalanceBefore); + const methodologistBalanceIncrease = methodologistBalanceAfter.sub(methodologistBalanceBefore); + + expect(ownerFeeRecipientBalanceIncrease).to.eq(expectedOwnerTake); + expect(methodologistBalanceIncrease).to.eq(expectedMethodologistTake); + }); + + it("should emit the correct FeesDistributed event", async () => { + const expectedOwnerTake = preciseMul(numTokens, ownerFeeSplit); + const expectedMethodologistTake = numTokens.sub(expectedOwnerTake); + + await expect(subject()).to.emit(claimExtension, "FeesDistributed").withArgs( + setToken.address, + setV2Setup.dai.address, + ownerFeeRecipient.address, + methodologist.address, + expectedOwnerTake, + expectedMethodologistTake + ); + }); + + describe("when methodologist fees are 0", async () => { + beforeEach(async () => { + await delegatedManager.connect(owner.wallet).updateOwnerFeeSplit(ether(1)); + await delegatedManager.connect(methodologist.wallet).updateOwnerFeeSplit(ether(1)); + }); + + it("should not send fees to methodologist", async () => { + const preMethodologistBalance = await setV2Setup.dai.balanceOf(methodologist.address); + + await subject(); + + const postMethodologistBalance = await setV2Setup.dai.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); + await delegatedManager.connect(methodologist.wallet).updateOwnerFeeSplit(ZERO); + }); + + it("should not send fees to owner fee recipient", async () => { + const preOwnerFeeRecipientBalance = await setV2Setup.dai.balanceOf(owner.address); + + await subject(); + + const postOwnerFeeRecipientBalance = await setV2Setup.dai.balanceOf(owner.address); + expect(postOwnerFeeRecipientBalance.sub(preOwnerFeeRecipientBalance)).to.eq(ZERO); + }); + }); + }); + + describe("#batchAbsorb", async () => { + let airdropOne: BigNumber; + let airdropTwo: BigNumber; + + let subjectSetToken: Address; + let subjectTokens: Address[]; + let subjectCaller: Account; + + beforeEach(async () => { + airdropOne = ether(100); + airdropTwo = ether(1); + + await setV2Setup.usdc.transfer(setToken.address, airdropOne); + await setV2Setup.weth.transfer(setToken.address, airdropTwo); + + subjectSetToken = setToken.address; + subjectTokens = [setV2Setup.usdc.address, setV2Setup.weth.address]; + subjectCaller = operator; + }); + + async function subject(): Promise { + return claimExtension.connect(subjectCaller.wallet).batchAbsorb( + subjectSetToken, + subjectTokens + ); + } + + it("should create the correct new usdc position", async () => { + const balanceBefore = await setV2Setup.usdc.balanceOf(setToken.address); + expect(balanceBefore).to.eq(airdropOne); + + await subject(); + + const totalSupply = await setToken.totalSupply(); + const actualBalanceAfter = await setV2Setup.usdc.balanceOf(setToken.address); + + const expectedBalanceAfter = airdropOne.sub(preciseMul(airdropOne, airdropFee)); + expect(actualBalanceAfter).to.eq(expectedBalanceAfter); + + const positions = await setToken.getPositions(); + const expectedUnitAfter = preciseDiv(expectedBalanceAfter, totalSupply); + expect(positions[1].component).to.eq(setV2Setup.usdc.address); + expect(positions[1].unit).to.eq(expectedUnitAfter); + }); + + it("should transfer the correct usdc amount to the setToken feeRecipient", async () => { + const balanceBefore = await setV2Setup.usdc.balanceOf(setToken.address); + expect(balanceBefore).to.eq(airdropOne); + + await subject(); + + const actualManagerTake = await setV2Setup.usdc.balanceOf(delegatedManager.address); + const expectedManagerTake = preciseMul(preciseMul(airdropOne, airdropFee), PRECISE_UNIT.sub(protocolFee)); + expect(actualManagerTake).to.eq(expectedManagerTake); + }); + + it("should transfer the correct usdc amount to the protocol feeRecipient", async () => { + const balanceBefore = await setV2Setup.usdc.balanceOf(setToken.address); + expect(balanceBefore).to.eq(airdropOne); + + await subject(); + + const actualProtocolTake = await setV2Setup.usdc.balanceOf(setV2Setup.feeRecipient); + const expectedProtocolTake = preciseMul(preciseMul(airdropOne, airdropFee), protocolFee); + expect(actualProtocolTake).to.eq(expectedProtocolTake); + }); + + it("should emit the correct ComponentAbsorbed event for USDC", async () => { + const expectedManagerTake = preciseMul(preciseMul(airdropOne, airdropFee), PRECISE_UNIT.sub(protocolFee)); + const expectedProtocolTake = preciseMul(preciseMul(airdropOne, airdropFee), protocolFee); + await expect(subject()).to.emit(airdropModule, "ComponentAbsorbed").withArgs( + setToken.address, + setV2Setup.usdc.address, + airdropOne, + expectedManagerTake, + expectedProtocolTake + ); + }); + + it("should add the correct amount to the existing weth position", async () => { + const totalSupply = await setToken.totalSupply(); + const prePositions = await setToken.getPositions(); + const knownBalance = preciseMul(prePositions[0].unit, totalSupply); + const balanceBefore = await setV2Setup.weth.balanceOf(setToken.address); + expect(airdropTwo).to.eq(balanceBefore.sub(knownBalance)); + + await subject(); + + const expectedAirdropAmount = airdropTwo.sub(preciseMul(airdropTwo, airdropFee)); + const expectedBalanceAfter = knownBalance.add(expectedAirdropAmount); + + const actualBalanceAfter = await setV2Setup.weth.balanceOf(setToken.address); + expect(actualBalanceAfter).to.eq(expectedBalanceAfter); + + const postPositions = await setToken.getPositions(); + expect(postPositions[0].unit).to.eq(preciseDiv(expectedBalanceAfter, totalSupply)); + }); + + it("should transfer the correct weth amount to the setToken feeRecipient", async () => { + const totalSupply = await setToken.totalSupply(); + const prePositions = await setToken.getPositions(); + const preDropBalance = preciseMul(prePositions[0].unit, totalSupply); + const balance = await setV2Setup.weth.balanceOf(setToken.address); + + await subject(); + + const airdroppedTokens = balance.sub(preDropBalance); + const expectedManagerTake = preciseMul(preciseMul(airdroppedTokens, airdropFee), PRECISE_UNIT.sub(protocolFee)); + + const actualManagerTake = await setV2Setup.weth.balanceOf(delegatedManager.address); + expect(actualManagerTake).to.eq(expectedManagerTake); + }); + + it("should transfer the correct weth amount to the protocol feeRecipient", async () => { + const totalSupply = await setToken.totalSupply(); + const prePositions = await setToken.getPositions(); + const preDropBalance = preciseMul(prePositions[0].unit, totalSupply); + const balance = await setV2Setup.weth.balanceOf(setToken.address); + + await subject(); + + const airdroppedTokens = balance.sub(preDropBalance); + const expectedProtocolTake = preciseMul(preciseMul(airdroppedTokens, airdropFee), protocolFee); + + const actualProtocolTake = await setV2Setup.weth.balanceOf(setV2Setup.feeRecipient); + expect(actualProtocolTake).to.eq(expectedProtocolTake); + }); + + it("should emit the correct ComponentAbsorbed event for WETH", async () => { + const totalSupply = await setToken.totalSupply(); + const prePositions = await setToken.getPositions(); + const preDropBalance = preciseMul(prePositions[0].unit, totalSupply); + const balance = await setV2Setup.weth.balanceOf(setToken.address); + + const airdroppedTokens = balance.sub(preDropBalance); + const expectedManagerTake = preciseMul(preciseMul(airdroppedTokens, airdropFee), PRECISE_UNIT.sub(protocolFee)); + const expectedProtocolTake = preciseMul(preciseMul(airdroppedTokens, airdropFee), protocolFee); + await expect(subject()).to.emit(airdropModule, "ComponentAbsorbed").withArgs( + setToken.address, + setV2Setup.weth.address, + airdroppedTokens, + expectedManagerTake, + expectedProtocolTake + ); + }); + + describe("when anyoneAbsorb is false and the caller is not the DelegatedManager operator", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be valid AirdropModule absorb caller"); + }); + }); + + describe("when anyoneAbsorb is true and the caller is not the DelegatedManager operator", async () => { + beforeEach(async () => { + await claimExtension.connect(owner.wallet).updateAnyoneAbsorb(setToken.address, true); + + subjectCaller = await getRandomAccount(); + }); + + it("should create the correct new usdc position", async () => { + const balanceBefore = await setV2Setup.usdc.balanceOf(setToken.address); + expect(balanceBefore).to.eq(airdropOne); + + await subject(); + + const totalSupply = await setToken.totalSupply(); + const actualBalanceAfter = await setV2Setup.usdc.balanceOf(setToken.address); + + const expectedBalanceAfter = airdropOne.sub(preciseMul(airdropOne, airdropFee)); + expect(actualBalanceAfter).to.eq(expectedBalanceAfter); + + const positions = await setToken.getPositions(); + const expectedUnitAfter = preciseDiv(expectedBalanceAfter, totalSupply); + expect(positions[1].component).to.eq(setV2Setup.usdc.address); + expect(positions[1].unit).to.eq(expectedUnitAfter); + }); + + it("should add the correct amount to the existing weth position", async () => { + const totalSupply = await setToken.totalSupply(); + const prePositions = await setToken.getPositions(); + const knownBalance = preciseMul(prePositions[0].unit, totalSupply); + const balanceBefore = await setV2Setup.weth.balanceOf(setToken.address); + expect(airdropTwo).to.eq(balanceBefore.sub(knownBalance)); + + await subject(); + + const expectedAirdropAmount = airdropTwo.sub(preciseMul(airdropTwo, airdropFee)); + const expectedBalanceAfter = knownBalance.add(expectedAirdropAmount); + + const actualBalanceAfter = await setV2Setup.weth.balanceOf(setToken.address); + expect(actualBalanceAfter).to.eq(expectedBalanceAfter); + + const postPositions = await setToken.getPositions(); + expect(postPositions[0].unit).to.eq(preciseDiv(expectedBalanceAfter, totalSupply)); + }); + }); + + describe("when a passed token is not an allowed asset", async () => { + beforeEach(async () => { + subjectTokens = [setV2Setup.usdc.address, setV2Setup.wbtc.address]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be allowed asset"); + }); + }); + + describe("when useAssetAllowlist is false and a passed token is not on allowed asset list", async () => { + beforeEach(async () => { + await delegatedManager.connect(owner.wallet).removeAllowedAssets([setV2Setup.usdc.address]); + await delegatedManager.connect(owner.wallet).updateUseAssetAllowlist(false); + + await setV2Setup.wbtc.transfer(setToken.address, ether(1)); + + await claimExtension.connect(owner.wallet).addAirdrop(setToken.address, setV2Setup.wbtc.address); + + subjectTokens = [setV2Setup.usdc.address, setV2Setup.wbtc.address]; + }); + + it("should create the correct new usdc position", async () => { + const totalSupply = await setToken.totalSupply(); + const preDropBalance = ZERO; + const balance = await setV2Setup.usdc.balanceOf(setToken.address); + + await subject(); + + const airdroppedTokens = balance.sub(preDropBalance); + const netBalance = balance.sub(preciseMul(airdroppedTokens, airdropFee)); + + const positions = await setToken.getPositions(); + expect(positions[1].unit).to.eq(preciseDiv(netBalance, totalSupply)); + }); + }); + }); + + describe("#absorb", async () => { + let airdropOne: BigNumber; + + let subjectSetToken: Address; + let subjectToken: Address; + let subjectCaller: Account; + + beforeEach(async () => { + airdropOne = ether(100); + await setV2Setup.usdc.transfer(setToken.address, airdropOne); + + subjectSetToken = setToken.address; + subjectToken = setV2Setup.usdc.address; + subjectCaller = operator; + }); + + async function subject(): Promise { + return claimExtension.connect(subjectCaller.wallet).absorb( + subjectSetToken, + subjectToken + ); + } + + it("should create the correct new usdc position", async () => { + const balanceBefore = await setV2Setup.usdc.balanceOf(setToken.address); + expect(balanceBefore).to.eq(airdropOne); + + await subject(); + + const totalSupply = await setToken.totalSupply(); + const actualBalanceAfter = await setV2Setup.usdc.balanceOf(setToken.address); + + const expectedBalanceAfter = airdropOne.sub(preciseMul(airdropOne, airdropFee)); + expect(actualBalanceAfter).to.eq(expectedBalanceAfter); + + const positions = await setToken.getPositions(); + const expectedUnitAfter = preciseDiv(expectedBalanceAfter, totalSupply); + expect(positions[1].component).to.eq(setV2Setup.usdc.address); + expect(positions[1].unit).to.eq(expectedUnitAfter); + }); + + it("should transfer the correct usdc amount to the setToken feeRecipient", async () => { + const balanceBefore = await setV2Setup.usdc.balanceOf(setToken.address); + expect(balanceBefore).to.eq(airdropOne); + + await subject(); + + const actualManagerTake = await setV2Setup.usdc.balanceOf(delegatedManager.address); + const expectedManagerTake = preciseMul(preciseMul(airdropOne, airdropFee), PRECISE_UNIT.sub(protocolFee)); + expect(actualManagerTake).to.eq(expectedManagerTake); + }); + + it("should transfer the correct usdc amount to the protocol feeRecipient", async () => { + const balanceBefore = await setV2Setup.usdc.balanceOf(setToken.address); + expect(balanceBefore).to.eq(airdropOne); + + await subject(); + + const actualProtocolTake = await setV2Setup.usdc.balanceOf(setV2Setup.feeRecipient); + const expectedProtocolTake = preciseMul(preciseMul(airdropOne, airdropFee), protocolFee); + expect(actualProtocolTake).to.eq(expectedProtocolTake); + }); + + it("should emit the correct ComponentAbsorbed event for USDC", async () => { + const expectedManagerTake = preciseMul(preciseMul(airdropOne, airdropFee), PRECISE_UNIT.sub(protocolFee)); + const expectedProtocolTake = preciseMul(preciseMul(airdropOne, airdropFee), protocolFee); + await expect(subject()).to.emit(airdropModule, "ComponentAbsorbed").withArgs( + setToken.address, + setV2Setup.usdc.address, + airdropOne, + expectedManagerTake, + expectedProtocolTake + ); + }); + + describe("when anyoneAbsorb is false and the caller is not the DelegatedManager operator", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be valid AirdropModule absorb caller"); + }); + }); + + describe("when anyoneAbsorb is true and the caller is not the DelegatedManager operator", async () => { + beforeEach(async () => { + await claimExtension.connect(owner.wallet).updateAnyoneAbsorb(setToken.address, true); + + subjectCaller = await getRandomAccount(); + }); + + it("should create the correct new usdc position", async () => { + const balanceBefore = await setV2Setup.usdc.balanceOf(setToken.address); + expect(balanceBefore).to.eq(airdropOne); + + await subject(); + + const totalSupply = await setToken.totalSupply(); + const actualBalanceAfter = await setV2Setup.usdc.balanceOf(setToken.address); + + const expectedBalanceAfter = airdropOne.sub(preciseMul(airdropOne, airdropFee)); + expect(actualBalanceAfter).to.eq(expectedBalanceAfter); + + const positions = await setToken.getPositions(); + const expectedUnitAfter = preciseDiv(expectedBalanceAfter, totalSupply); + expect(positions[1].component).to.eq(setV2Setup.usdc.address); + expect(positions[1].unit).to.eq(expectedUnitAfter); + }); + }); + + describe("when passed token is not an allowed asset", async () => { + beforeEach(async () => { + subjectToken = setV2Setup.wbtc.address; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be allowed asset"); + }); + }); + }); + + describe("#addAirdrop", async () => { + let subjectSetToken: Address; + let subjectAirdrop: Address; + let subjectCaller: Account; + + beforeEach(async () => { + subjectSetToken = setToken.address; + subjectAirdrop = setV2Setup.wbtc.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return claimExtension.connect(subjectCaller.wallet).addAirdrop( + subjectSetToken, + subjectAirdrop + ); + } + + it("should add the new token", async () => { + await subject(); + + const airdrops = await airdropModule.getAirdrops(setToken.address); + const isAirdrop = await airdropModule.isAirdrop(setToken.address, setV2Setup.wbtc.address); + expect(airdrops[2]).to.eq(setV2Setup.wbtc.address); + expect(isAirdrop).to.be.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("#removeAirdrop", async () => { + let subjectSetToken: Address; + let subjectAirdrop: Address; + let subjectCaller: Account; + + beforeEach(async () => { + subjectSetToken = setToken.address; + subjectAirdrop = setV2Setup.usdc.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return claimExtension.connect(subjectCaller.wallet).removeAirdrop( + subjectSetToken, + subjectAirdrop + ); + } + + it("should remove the token", async () => { + await subject(); + + const airdrops = await airdropModule.getAirdrops(setToken.address); + const isAirdrop = await airdropModule.isAirdrop(subjectSetToken, subjectAirdrop); + expect(airdrops).to.not.contain(subjectAirdrop); + expect(isAirdrop).to.be.false; + }); + + 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("#updateAnyoneAbsorb", async () => { + let subjectSetToken: Address; + let subjectAnyoneAbsorb: boolean; + let subjectCaller: Account; + + beforeEach(async () => { + subjectSetToken = setToken.address; + subjectAnyoneAbsorb = true; + subjectCaller = owner; + }); + + async function subject(): Promise { + return claimExtension.connect(subjectCaller.wallet).updateAnyoneAbsorb( + subjectSetToken, + subjectAnyoneAbsorb + ); + } + + it("should flip the anyoneAbsorb indicator", async () => { + await subject(); + + const airdropSettings = await airdropModule.airdropSettings(setToken.address); + expect(airdropSettings.anyoneAbsorb).to.be.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("#updateAirdropFeeRecipient", async () => { + let subjectSetToken: Address; + let subjectNewFeeRecipient: Address; + let subjectCaller: Account; + + beforeEach(async () => { + subjectSetToken = setToken.address; + subjectNewFeeRecipient = await getRandomAddress(); + subjectCaller = owner; + }); + + async function subject(): Promise { + return claimExtension.connect(subjectCaller.wallet).updateAirdropFeeRecipient( + subjectSetToken, + subjectNewFeeRecipient + ); + } + + it("should change the fee recipient to the new address", async () => { + await subject(); + + const airdropSettings = await airdropModule.airdropSettings(setToken.address); + expect(airdropSettings.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("#updateAirdropFee", async () => { + let subjectSetToken: Address; + let subjectNewFee: BigNumber; + let subjectCaller: Account; + + beforeEach(async () => { + await setV2Setup.usdc.transfer(setToken.address, ether(1)); + + subjectSetToken = setToken.address; + subjectNewFee = ether(.5); + subjectCaller = owner; + }); + + async function subject(): Promise { + return claimExtension.connect(subjectCaller.wallet).updateAirdropFee( + subjectSetToken, + subjectNewFee + ); + } + + it("should create the correct new usdc position", async () => { + const totalSupply = await setToken.totalSupply(); + const preDropBalance = ZERO; + const balance = await setV2Setup.usdc.balanceOf(setToken.address); + + await subject(); + + const airdroppedTokens = balance.sub(preDropBalance); + const netBalance = balance.sub(preciseMul(airdroppedTokens, airdropFee)); + + const positions = await setToken.getPositions(); + expect(positions[1].unit).to.eq(preciseDiv(netBalance, totalSupply)); + }); + + it("should set the new fee", async () => { + await subject(); + + const airdropSettings = await airdropModule.airdropSettings(setToken.address); + expect(airdropSettings.airdropFee).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("#claimAndAbsorb", async () => { + let rewards: BigNumber; + + let subjectSetToken: Address; + let subjectRewardPool: Address; + let subjectIntegration: string; + let subjectCaller: Account; + + beforeEach(async () => { + rewards = ether(1); + await claimAdapterMockOne.setRewards(rewards); + + await claimExtension.connect(owner.wallet).addAirdrop( + setToken.address, + claimAdapterMockOne.address + ); + + await delegatedManager.connect(owner.wallet).addAllowedAssets([claimAdapterMockOne.address]); + + subjectSetToken = setToken.address; + subjectRewardPool = rewardPools[0]; + subjectIntegration = claimAdapterMockIntegrationNameOne; + subjectCaller = operator; + }); + + async function subject(): Promise { + return claimExtension.connect(subjectCaller.wallet).claimAndAbsorb( + subjectSetToken, + subjectRewardPool, + subjectIntegration + ); + } + + it("emits the correct RewardClaimed event", async () => { + await expect(subject()).to.emit(claimModule, "RewardClaimed").withArgs( + subjectSetToken, + subjectRewardPool, + claimAdapterMockOne.address, + rewards + ); + }); + + it("should claim the rewards and create the correct new reward token position", async () => { + const balanceBefore = await claimAdapterMockOne.balanceOf(setToken.address); + expect(balanceBefore).to.eq(ZERO); + + await subject(); + + const totalSupply = await setToken.totalSupply(); + const actualBalanceAfter = await claimAdapterMockOne.balanceOf(setToken.address); + + const expectedBalanceAfter = rewards.sub(preciseMul(rewards, airdropFee)); + expect(actualBalanceAfter).to.eq(expectedBalanceAfter); + + const positions = await setToken.getPositions(); + const expectedUnitAfter = preciseDiv(expectedBalanceAfter, totalSupply); + expect(positions[1].component).to.eq(claimAdapterMockOne.address); + expect(positions[1].unit).to.eq(expectedUnitAfter); + }); + + it("should transfer the correct rewards amount to the setToken feeRecipient", async () => { + const balanceBefore = await claimAdapterMockOne.balanceOf(delegatedManager.address); + expect(balanceBefore).to.eq(ZERO); + + await subject(); + + const actualManagerTake = await claimAdapterMockOne.balanceOf(delegatedManager.address); + const expectedManagerTake = preciseMul(preciseMul(rewards, airdropFee), PRECISE_UNIT.sub(protocolFee)); + + expect(actualManagerTake).to.eq(expectedManagerTake); + }); + + it("should transfer the correct rewards amount to the protocol feeRecipient", async () => { + const balanceBefore = await claimAdapterMockOne.balanceOf(setV2Setup.feeRecipient); + expect(balanceBefore).to.eq(ZERO); + + await subject(); + + const actualProtocolTake = await claimAdapterMockOne.balanceOf(setV2Setup.feeRecipient); + const expectedProtocolTake = preciseMul(preciseMul(rewards, airdropFee), protocolFee); + expect(actualProtocolTake).to.eq(expectedProtocolTake); + }); + + it("should emit the correct ComponentAbsorbed event for rewards", async () => { + const expectedManagerTake = preciseMul(preciseMul(rewards, airdropFee), PRECISE_UNIT.sub(protocolFee)); + const expectedProtocolTake = preciseMul(preciseMul(rewards, airdropFee), protocolFee); + await expect(subject()).to.emit(airdropModule, "ComponentAbsorbed").withArgs( + setToken.address, + claimAdapterMockOne.address, + rewards, + expectedManagerTake, + expectedProtocolTake + ); + }); + + describe("when anyoneClaim and anyoneAbsorb are false and the caller is not the DelegatedManager operator", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be valid AirdropModule absorb and ClaimModule claim caller"); + }); + }); + + describe("when anyoneClaim and anyoneAbsorb is true and the caller is not the DelegatedManager operator", async () => { + beforeEach(async () => { + await claimExtension.connect(owner.wallet).updateAnyoneClaim(setToken.address, true); + await claimExtension.connect(owner.wallet).updateAnyoneAbsorb(setToken.address, true); + + subjectCaller = await getRandomAccount(); + }); + + it("should claim the rewards and create the correct new reward token position", async () => { + const balanceBefore = await claimAdapterMockOne.balanceOf(setToken.address); + expect(balanceBefore).to.eq(ZERO); + + await subject(); + + const totalSupply = await setToken.totalSupply(); + const actualBalanceAfter = await claimAdapterMockOne.balanceOf(setToken.address); + + const expectedBalanceAfter = rewards.sub(preciseMul(rewards, airdropFee)); + expect(actualBalanceAfter).to.eq(expectedBalanceAfter); + + const positions = await setToken.getPositions(); + const expectedUnitAfter = preciseDiv(expectedBalanceAfter, totalSupply); + expect(positions[1].component).to.eq(claimAdapterMockOne.address); + expect(positions[1].unit).to.eq(expectedUnitAfter); + }); + }); + + describe("when the rewards token is not an allowed asset", async () => { + beforeEach(async () => { + await delegatedManager.connect(owner.wallet).removeAllowedAssets([claimAdapterMockOne.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be allowed asset"); + }); + }); + }); + + describe("#batchClaimAndAbsorb", async () => { + let rewardsOne: BigNumber; + let rewardsTwo: BigNumber; + + let subjectSetToken: Address; + let subjectRewardPools: Address[]; + let subjectIntegrations: string[]; + let subjectCaller: Account; + + beforeEach(async () => { + rewardsOne = ether(1); + rewardsTwo = ether(2); + await claimAdapterMockOne.setRewards(rewardsOne); + await claimAdapterMockTwo.setRewards(rewardsTwo); + + await claimExtension.connect(owner.wallet).addAirdrop( + setToken.address, + claimAdapterMockOne.address + ); + + await claimExtension.connect(owner.wallet).addAirdrop( + setToken.address, + claimAdapterMockTwo.address + ); + + await delegatedManager.connect(owner.wallet).addAllowedAssets( + [claimAdapterMockOne.address, claimAdapterMockTwo.address] + ); + + subjectSetToken = setToken.address; + subjectRewardPools = rewardPools; + subjectIntegrations = [claimAdapterMockIntegrationNameOne, claimAdapterMockIntegrationNameTwo]; + subjectCaller = operator; + }); + + async function subject(): Promise { + return claimExtension.connect(subjectCaller.wallet).batchClaimAndAbsorb( + subjectSetToken, + subjectRewardPools, + subjectIntegrations + ); + } + + it("emits the correct first RewardClaimed events", async () => { + await expect(subject()).to.emit(claimModule, "RewardClaimed").withArgs( + subjectSetToken, + subjectRewardPools[0], + claimAdapterMockOne.address, + rewardsOne + ); + }); + + it("emits the correct second RewardClaimed events", async () => { + await expect(subject()).to.emit(claimModule, "RewardClaimed").withArgs( + subjectSetToken, + subjectRewardPools[1], + claimAdapterMockTwo.address, + rewardsTwo + ); + }); + + it("should claim the rewards and create the correct new reward token positions", async () => { + const balanceOneBefore = await claimAdapterMockOne.balanceOf(setToken.address); + const balanceTwoBefore = await claimAdapterMockTwo.balanceOf(setToken.address); + expect(balanceOneBefore).to.eq(ZERO); + expect(balanceTwoBefore).to.eq(ZERO); + + await subject(); + + const totalSupply = await setToken.totalSupply(); + const actualBalanceOneAfter = await claimAdapterMockOne.balanceOf(setToken.address); + const actualBalanceTwoAfter = await claimAdapterMockTwo.balanceOf(setToken.address); + + const expectedBalanceOneAfter = rewardsOne.sub(preciseMul(rewardsOne, airdropFee)); + const expectedBalanceTwoAfter = rewardsTwo.sub(preciseMul(rewardsTwo, airdropFee)); + expect(actualBalanceOneAfter).to.eq(expectedBalanceOneAfter); + expect(actualBalanceTwoAfter).to.eq(expectedBalanceTwoAfter); + + const positions = await setToken.getPositions(); + const expectedUnitOneAfter = preciseDiv(expectedBalanceOneAfter, totalSupply); + const expectedUnitTwoAfter = preciseDiv(expectedBalanceTwoAfter, totalSupply); + expect(positions[1].component).to.eq(claimAdapterMockOne.address); + expect(positions[2].component).to.eq(claimAdapterMockTwo.address); + expect(positions[1].unit).to.eq(expectedUnitOneAfter); + expect(positions[2].unit).to.eq(expectedUnitTwoAfter); + }); + + it("should transfer the correct rewards amounts to the setToken feeRecipient", async () => { + const balanceOneBefore = await claimAdapterMockOne.balanceOf(delegatedManager.address); + const balanceTwoBefore = await claimAdapterMockTwo.balanceOf(delegatedManager.address); + expect(balanceOneBefore).to.eq(ZERO); + expect(balanceTwoBefore).to.eq(ZERO); + + await subject(); + + const actualManagerTakeOne = await claimAdapterMockOne.balanceOf(delegatedManager.address); + const expectedManagerTakeOne = preciseMul(preciseMul(rewardsOne, airdropFee), PRECISE_UNIT.sub(protocolFee)); + + const actualManagerTakeTwo = await claimAdapterMockTwo.balanceOf(delegatedManager.address); + const expectedManagerTakeTwo = preciseMul(preciseMul(rewardsTwo, airdropFee), PRECISE_UNIT.sub(protocolFee)); + + expect(actualManagerTakeOne).to.eq(expectedManagerTakeOne); + expect(actualManagerTakeTwo).to.eq(expectedManagerTakeTwo); + }); + + it("should transfer the correct rewards amounts to the protocol feeRecipient", async () => { + const balanceOneBefore = await claimAdapterMockOne.balanceOf(setV2Setup.feeRecipient); + const balanceTwoBefore = await claimAdapterMockTwo.balanceOf(setV2Setup.feeRecipient); + expect(balanceOneBefore).to.eq(ZERO); + expect(balanceTwoBefore).to.eq(ZERO); + + await subject(); + + const actualProtocolTakeOne = await claimAdapterMockOne.balanceOf(setV2Setup.feeRecipient); + const expectedProtocolTakeOne = preciseMul(preciseMul(rewardsOne, airdropFee), protocolFee); + + const actualProtocolTakeTwo = await claimAdapterMockTwo.balanceOf(setV2Setup.feeRecipient); + const expectedProtocolTakeTwo = preciseMul(preciseMul(rewardsTwo, airdropFee), protocolFee); + + expect(actualProtocolTakeOne).to.eq(expectedProtocolTakeOne); + expect(actualProtocolTakeTwo).to.eq(expectedProtocolTakeTwo); + }); + + it("should emit the correct ComponentAbsorbed event for the first rewards", async () => { + const expectedManagerTakeOne = preciseMul(preciseMul(rewardsOne, airdropFee), PRECISE_UNIT.sub(protocolFee)); + const expectedProtocolTakeOne = preciseMul(preciseMul(rewardsOne, airdropFee), protocolFee); + await expect(subject()).to.emit(airdropModule, "ComponentAbsorbed").withArgs( + setToken.address, + claimAdapterMockOne.address, + rewardsOne, + expectedManagerTakeOne, + expectedProtocolTakeOne + ); + }); + + it("should emit the correct ComponentAbsorbed event for the second rewards", async () => { + const expectedManagerTakeTwo = preciseMul(preciseMul(rewardsTwo, airdropFee), PRECISE_UNIT.sub(protocolFee)); + const expectedProtocolTakeTwo = preciseMul(preciseMul(rewardsTwo, airdropFee), protocolFee); + await expect(subject()).to.emit(airdropModule, "ComponentAbsorbed").withArgs( + setToken.address, + claimAdapterMockTwo.address, + rewardsTwo, + expectedManagerTakeTwo, + expectedProtocolTakeTwo + ); + }); + + describe("when anyoneClaim and anyoneAbsorb are false and the caller is not the DelegatedManager operator", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be valid AirdropModule absorb and ClaimModule claim caller"); + }); + }); + + describe("when anyoneClaim and anyoneAbsorb is true and the caller is not the DelegatedManager operator", async () => { + beforeEach(async () => { + await claimExtension.connect(owner.wallet).updateAnyoneClaim(setToken.address, true); + await claimExtension.connect(owner.wallet).updateAnyoneAbsorb(setToken.address, true); + + subjectCaller = await getRandomAccount(); + }); + + it("should claim the rewards and create the correct new reward token positions", async () => { + const balanceOneBefore = await claimAdapterMockOne.balanceOf(setToken.address); + const balanceTwoBefore = await claimAdapterMockTwo.balanceOf(setToken.address); + expect(balanceOneBefore).to.eq(ZERO); + expect(balanceTwoBefore).to.eq(ZERO); + + await subject(); + + const totalSupply = await setToken.totalSupply(); + const actualBalanceOneAfter = await claimAdapterMockOne.balanceOf(setToken.address); + const actualBalanceTwoAfter = await claimAdapterMockTwo.balanceOf(setToken.address); + + const expectedBalanceOneAfter = rewardsOne.sub(preciseMul(rewardsOne, airdropFee)); + const expectedBalanceTwoAfter = rewardsTwo.sub(preciseMul(rewardsTwo, airdropFee)); + expect(actualBalanceOneAfter).to.eq(expectedBalanceOneAfter); + expect(actualBalanceTwoAfter).to.eq(expectedBalanceTwoAfter); + + const positions = await setToken.getPositions(); + const expectedUnitOneAfter = preciseDiv(expectedBalanceOneAfter, totalSupply); + const expectedUnitTwoAfter = preciseDiv(expectedBalanceTwoAfter, totalSupply); + expect(positions[1].component).to.eq(claimAdapterMockOne.address); + expect(positions[2].component).to.eq(claimAdapterMockTwo.address); + expect(positions[1].unit).to.eq(expectedUnitOneAfter); + expect(positions[2].unit).to.eq(expectedUnitTwoAfter); + }); + }); + + describe("when the rewards token is not an allowed asset", async () => { + beforeEach(async () => { + await delegatedManager.connect(owner.wallet).removeAllowedAssets([claimAdapterMockTwo.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be allowed asset"); + }); + }); + }); + + describe("#updateAnyoneClaim", async () => { + let subjectSetToken: Address; + let subjectAnyoneClaim: boolean; + let subjectCaller: Account; + + beforeEach(async () => { + subjectSetToken = setToken.address; + subjectAnyoneClaim = true; + subjectCaller = owner; + }); + + async function subject(): Promise { + return claimExtension.connect(subjectCaller.wallet).updateAnyoneClaim( + subjectSetToken, + subjectAnyoneClaim + ); + } + + it("should change the anyoneClaim indicator", async () => { + const anyoneClaimBefore = await claimModule.anyoneClaim(subjectSetToken); + expect(anyoneClaimBefore).to.eq(false); + + await subject(); + + const anyoneClaim = await claimModule.anyoneClaim(subjectSetToken); + expect(anyoneClaim).to.eq(true); + + subjectAnyoneClaim = false; + await subject(); + + const anyoneClaimAfter = await claimModule.anyoneClaim(subjectSetToken); + expect(anyoneClaimAfter).to.eq(false); + }); + + 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("#addClaim", async () => { + let subjectSetToken: Address; + let subjectRewardPool: Address; + let subjectIntegration: string; + let subjectCaller: Account; + + beforeEach(async () => { + subjectSetToken = setToken.address; + subjectRewardPool = await getRandomAddress(); + subjectIntegration = claimAdapterMockIntegrationNameTwo; + subjectCaller = owner; + }); + + async function subject(): Promise { + return claimExtension.connect(subjectCaller.wallet).addClaim( + subjectSetToken, + subjectRewardPool, + subjectIntegration + ); + } + + it("should add the rewardPool to the rewardPoolList and rewardPoolStatus", async () => { + expect(await claimModule.isRewardPool(subjectSetToken, subjectRewardPool)).to.be.false; + + await subject(); + + expect(await claimModule.isRewardPool(subjectSetToken, subjectRewardPool)).to.be.true; + expect(await claimModule.rewardPoolList(subjectSetToken, 2)).to.eq(subjectRewardPool); + }); + + it("should add new integration for the rewardPool", async () => { + const rewardPoolClaimsBefore = await claimModule.getRewardPoolClaims(setToken.address, subjectRewardPool); + const isIntegrationAddedBefore = await claimModule.claimSettingsStatus(setToken.address, subjectRewardPool, claimAdapterMockTwo.address); + expect(rewardPoolClaimsBefore.length).to.eq(0); + expect(isIntegrationAddedBefore).to.be.false; + + await subject(); + + const rewardPoolClaims = await claimModule.getRewardPoolClaims(setToken.address, subjectRewardPool); + const isIntegrationAdded = await claimModule.claimSettingsStatus(setToken.address, subjectRewardPool, claimAdapterMockTwo.address); + expect(rewardPoolClaims.length).to.eq(1); + expect(rewardPoolClaims[0]).to.eq(claimAdapterMockTwo.address); + expect(isIntegrationAdded).to.be.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("#batchAddClaim", async () => { + let subjectSetToken: Address; + let subjectRewardPools: Address[]; + let subjectIntegrations: string[]; + let subjectCaller: Account; + + beforeEach(async () => { + subjectSetToken = setToken.address; + const [rewardPoolOne, rewardPoolTwo] = [await getRandomAddress(), await getRandomAddress()]; + subjectRewardPools = [rewardPoolOne, rewardPoolOne, rewardPoolTwo]; + subjectIntegrations = [claimAdapterMockIntegrationNameOne, claimAdapterMockIntegrationNameTwo, claimAdapterMockIntegrationNameOne]; + subjectCaller = owner; + }); + + async function subject(): Promise { + return claimExtension.connect(subjectCaller.wallet).batchAddClaim( + subjectSetToken, + subjectRewardPools, + subjectIntegrations + ); + } + + it("should add the rewardPools to the rewardPoolList", async () => { + const isFirstAddedBefore = await claimModule.rewardPoolStatus(subjectSetToken, subjectRewardPools[0]); + const isSecondAddedBefore = await claimModule.rewardPoolStatus(subjectSetToken, subjectRewardPools[2]); + expect((await claimModule.getRewardPools(subjectSetToken)).length).to.eq(2); + expect(isFirstAddedBefore).to.be.false; + expect(isSecondAddedBefore).to.be.false; + + await subject(); + + const rewardPools = await claimModule.getRewardPools(subjectSetToken); + const isFirstAdded = await claimModule.rewardPoolStatus(subjectSetToken, subjectRewardPools[0]); + const isSecondAdded = await claimModule.rewardPoolStatus(subjectSetToken, subjectRewardPools[2]); + expect(rewardPools.length).to.eq(4); + expect(rewardPools[2]).to.eq(subjectRewardPools[0]); + expect(rewardPools[3]).to.eq(subjectRewardPools[2]); + expect(isFirstAdded).to.be.true; + expect(isSecondAdded).to.be.true; + }); + + it("should add all new integrations for the rewardPools", async () => { + await subject(); + + const rewardPoolOneClaims = await claimModule.getRewardPoolClaims(setToken.address, subjectRewardPools[0]); + const rewardPoolTwoClaims = await claimModule.getRewardPoolClaims(setToken.address, subjectRewardPools[2]); + const isFirstIntegrationAddedPool1 = await claimModule.claimSettingsStatus( + setToken.address, + subjectRewardPools[0], + claimAdapterMockOne.address + ); + const isSecondIntegrationAddedPool1 = await claimModule.claimSettingsStatus( + setToken.address, + subjectRewardPools[1], + claimAdapterMockTwo.address + ); + const isIntegrationAddedPool2 = await claimModule.claimSettingsStatus( + setToken.address, + subjectRewardPools[0], + claimAdapterMockOne.address + ); + expect(rewardPoolOneClaims[0]).to.eq(claimAdapterMockOne.address); + expect(rewardPoolOneClaims[1]).to.eq(claimAdapterMockTwo.address); + expect(rewardPoolTwoClaims[0]).to.eq(claimAdapterMockOne.address); + expect(isFirstIntegrationAddedPool1).to.be.true; + expect(isSecondIntegrationAddedPool1).to.be.true; + expect(isIntegrationAddedPool2).to.be.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("#removeClaim", async () => { + let subjectSetToken: Address; + let subjectRewardPool: Address; + let subjectIntegration: string; + let subjectCaller: Account; + + beforeEach(async () => { + subjectSetToken = setToken.address; + subjectRewardPool = await getRandomAddress(); + subjectIntegration = claimAdapterMockIntegrationNameOne; + subjectCaller = owner; + + await claimExtension.connect(subjectCaller.wallet).addClaim( + subjectSetToken, + subjectRewardPool, + subjectIntegration + ); + }); + + async function subject(): Promise { + return claimExtension.connect(subjectCaller.wallet).removeClaim( + subjectSetToken, + subjectRewardPool, + subjectIntegration + ); + } + + it("should remove the adapter associated to the reward pool", async () => { + const rewardPoolClaimsBefore = await claimModule.getRewardPoolClaims(setToken.address, subjectRewardPool); + const isAdapterAddedBefore = await claimModule.claimSettingsStatus(setToken.address, subjectRewardPool, claimAdapterMockOne.address); + expect(rewardPoolClaimsBefore.length).to.eq(1); + expect(isAdapterAddedBefore).to.be.true; + + await subject(); + + const rewardPoolClaims = await claimModule.getRewardPoolClaims(setToken.address, subjectRewardPool); + const isAdapterAdded = await claimModule.claimSettingsStatus(setToken.address, subjectRewardPool, claimAdapterMockOne.address); + expect(rewardPoolClaims.length).to.eq(0); + expect(isAdapterAdded).to.be.false; + }); + + it("should remove the rewardPool from the rewardPoolStatus", async () => { + expect(await claimModule.isRewardPool(setToken.address, subjectRewardPool)).to.be.true; + + await subject(); + + expect(await claimModule.isRewardPool(setToken.address, subjectRewardPool)).to.be.false; + }); + + 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("#batchRemoveClaim", async () => { + let subjectSetToken: Address; + let subjectRewardPools: Address[]; + let subjectIntegrations: string[]; + let subjectCaller: Account; + + beforeEach(async () => { + subjectSetToken = setToken.address; + subjectRewardPools = [await getRandomAddress(), await getRandomAddress()]; + subjectIntegrations = [claimAdapterMockIntegrationNameOne, claimAdapterMockIntegrationNameTwo]; + subjectCaller = owner; + + await claimExtension.connect(subjectCaller.wallet).batchAddClaim( + subjectSetToken, + subjectRewardPools, + subjectIntegrations + ); + }); + + async function subject(): Promise { + return claimExtension.connect(subjectCaller.wallet).batchRemoveClaim( + subjectSetToken, + subjectRewardPools, + subjectIntegrations + ); + } + + it("should remove the adapter associated to the reward pool", async () => { + const rewardPoolOneClaimsBefore = await claimModule.getRewardPoolClaims(setToken.address, subjectRewardPools[0]); + const rewardPoolTwoClaimsBefore = await claimModule.getRewardPoolClaims(setToken.address, subjectRewardPools[1]); + const isRewardPoolOneAdapterOneBefore = await claimModule.claimSettingsStatus( + setToken.address, + subjectRewardPools[0], + claimAdapterMockOne.address + ); + const isRewardPoolTwoAdapterTwoBefore = await claimModule.claimSettingsStatus( + setToken.address, + subjectRewardPools[1], + claimAdapterMockTwo.address + ); + expect(rewardPoolOneClaimsBefore.length).to.eq(1); + expect(rewardPoolTwoClaimsBefore.length).to.eq(1); + expect(isRewardPoolOneAdapterOneBefore).to.be.true; + expect(isRewardPoolTwoAdapterTwoBefore).to.be.true; + + await subject(); + + const rewardPoolOneClaims = await claimModule.getRewardPoolClaims(setToken.address, subjectRewardPools[0]); + const rewardPoolTwoClaims = await claimModule.getRewardPoolClaims(setToken.address, subjectRewardPools[1]); + const isRewardPoolOneAdapterOne = await claimModule.claimSettingsStatus(setToken.address, subjectRewardPools[0], claimAdapterMockOne.address); + const isRewardPoolTwoAdapterTwo = await claimModule.claimSettingsStatus(setToken.address, subjectRewardPools[1], claimAdapterMockTwo.address); + expect(rewardPoolOneClaims.length).to.eq(0); + expect(rewardPoolTwoClaims.length).to.eq(0); + expect(isRewardPoolOneAdapterOne).to.be.false; + expect(isRewardPoolTwoAdapterTwo).to.be.false; + + }); + + it("should remove the rewardPool from the rewardPoolStatus", async () => { + expect(await claimModule.isRewardPool(subjectSetToken, subjectRewardPools[0])).to.be.true; + expect(await claimModule.isRewardPool(subjectSetToken, subjectRewardPools[1])).to.be.true; + + await subject(); + + expect(await claimModule.isRewardPool(subjectSetToken, subjectRewardPools[0])).to.be.false; + expect(await claimModule.isRewardPool(subjectSetToken, subjectRewardPools[1])).to.be.false; + }); + + 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"); + }); + }); + }); + }); +}); diff --git a/utils/contracts/setV2.ts b/utils/contracts/setV2.ts index 11272fe4..0380dbc0 100644 --- a/utils/contracts/setV2.ts +++ b/utils/contracts/setV2.ts @@ -21,3 +21,5 @@ export { SingleIndexModule } from "../../typechain/SingleIndexModule"; export { StreamingFeeModule } from "../../typechain/StreamingFeeModule"; export { UniswapV2ExchangeAdapter } from "../../typechain/UniswapV2ExchangeAdapter"; export { WrapModule } from "../../typechain/WrapModule"; +export { ClaimModule } from "../../typechain/ClaimModule"; +export { ClaimAdapterMock } from "../../typechain/ClaimAdapterMock"; diff --git a/utils/deploys/deployMocks.ts b/utils/deploys/deployMocks.ts index 15dfa983..39873223 100644 --- a/utils/deploys/deployMocks.ts +++ b/utils/deploys/deployMocks.ts @@ -9,6 +9,7 @@ import { NotionalTradeModuleMock, StandardTokenMock, StringArrayUtilsMock, + ManagerMock, TradeAdapterMock, WrapAdapterMock, WrappedfCashMock, @@ -23,6 +24,7 @@ import { import { BaseExtensionMock__factory } from "../../typechain/factories/BaseExtensionMock__factory"; import { DEXAdapter__factory } from "../../typechain/factories/DEXAdapter__factory"; import { ModuleMock__factory } from "../../typechain/factories/ModuleMock__factory"; +import { ManagerMock__factory } from "../../typechain/factories/ManagerMock__factory"; import { MutualUpgradeV2Mock__factory } from "../../typechain/factories/MutualUpgradeV2Mock__factory"; import { BaseGlobalExtensionMock__factory } from "../../typechain/factories/BaseGlobalExtensionMock__factory"; import { convertLibraryNameToLinkId } from "../common"; @@ -211,4 +213,8 @@ export default class DeployMocks { cEtherAddress, ); } + + public async deployManagerMock(setToken: Address): Promise { + return await new ManagerMock__factory(this._deployerSigner).deploy(setToken); + } } diff --git a/utils/deploys/deploySetV2.ts b/utils/deploys/deploySetV2.ts index fc7c583e..87a88b70 100644 --- a/utils/deploys/deploySetV2.ts +++ b/utils/deploys/deploySetV2.ts @@ -14,6 +14,8 @@ import { ConstantPriceAdapter, ComptrollerMock, ContractCallerMock, + ClaimAdapterMock, + ClaimModule, DebtIssuanceModule, GeneralIndexModule, GovernanceModule, @@ -45,6 +47,8 @@ import { Compound__factory } from "../../typechain/factories/Compound__factory"; import { CompoundLeverageModule__factory } from "../../typechain/factories/CompoundLeverageModule__factory"; import { ComptrollerMock__factory } from "../../typechain/factories/ComptrollerMock__factory"; import { ContractCallerMock__factory } from "../../typechain/factories/ContractCallerMock__factory"; +import { ClaimAdapterMock__factory } from "../../typechain/factories/ClaimAdapterMock__factory"; +import { ClaimModule__factory } from "../../typechain/factories/ClaimModule__factory"; import { DebtIssuanceModule__factory } from "../../typechain/factories/DebtIssuanceModule__factory"; import { GeneralIndexModule__factory } from "../../typechain/factories/GeneralIndexModule__factory"; import { GovernanceModule__factory } from "../../typechain/factories/GovernanceModule__factory"; @@ -291,4 +295,12 @@ export default class DeploySetV2 { public async deployConstantPriceAdapter(): Promise { return await new ConstantPriceAdapter__factory(this._deployerSigner).deploy(); } + + public async deployClaimAdapterMock(): Promise { + return await new ClaimAdapterMock__factory(this._deployerSigner).deploy(); + } + + public async deployClaimModule(controller: Address): Promise { + return await new ClaimModule__factory(this._deployerSigner).deploy(controller); + } } diff --git a/utils/types.ts b/utils/types.ts index a82b357d..295f0724 100644 --- a/utils/types.ts +++ b/utils/types.ts @@ -120,3 +120,10 @@ export interface BatchTradeResult { tradeInfo: TradeInfo; revertReason?: string | undefined; } + +export interface AirdropSettings { + airdrops: Address[]; + feeRecipient: Address; + airdropFee: BigNumber; + anyoneAbsorb: boolean; +} From 2ef76b748c61199db7d4fb12c097d664c8900d62 Mon Sep 17 00:00:00 2001 From: Pranav Bhardwaj Date: Sat, 26 Aug 2023 20:42:08 -0400 Subject: [PATCH 05/10] add batch trade extension tests --- contracts/mocks/BatchTradeAdapterMock.sol | 86 ++ external/abi/index-protocol/TradeModule.json | 10 - external/abi/set/TradeModule.json | 151 ++++ external/contracts/set/TradeModule.sol | 323 +++++++ .../globalBatchTradeExtension.spec.ts | 808 ++++++++++++++++++ utils/contracts/index.ts | 1 + utils/contracts/setV2.ts | 1 + utils/deploys/deployMocks.ts | 6 + utils/deploys/deploySetV2.ts | 6 + 9 files changed, 1382 insertions(+), 10 deletions(-) create mode 100644 contracts/mocks/BatchTradeAdapterMock.sol delete mode 100644 external/abi/index-protocol/TradeModule.json create mode 100644 external/abi/set/TradeModule.json create mode 100644 external/contracts/set/TradeModule.sol create mode 100644 test/global-extensions/globalBatchTradeExtension.spec.ts diff --git a/contracts/mocks/BatchTradeAdapterMock.sol b/contracts/mocks/BatchTradeAdapterMock.sol new file mode 100644 index 00000000..88e9356a --- /dev/null +++ b/contracts/mocks/BatchTradeAdapterMock.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache License, Version 2.0 +pragma solidity 0.6.10; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +/** + * Trade Adapter that doubles as a mock exchange + */ +contract BatchTradeAdapterMock { + + /* ============ Helper Functions ============ */ + + function withdraw(address _token) + external + { + uint256 balance = ERC20(_token).balanceOf(address(this)); + require(ERC20(_token).transfer(msg.sender, balance), "ERC20 transfer failed"); + } + + /* ============ Trade Functions ============ */ + + function trade( + address _sourceToken, + address _destinationToken, + address _destinationAddress, + uint256 _sourceQuantity, + uint256 _minDestinationQuantity + ) + external + { + uint256 destinationBalance = ERC20(_destinationToken).balanceOf(address(this)); + require(ERC20(_sourceToken).transferFrom(_destinationAddress, address(this), _sourceQuantity), "ERC20 TransferFrom failed"); + if (_minDestinationQuantity == 1) { // byte revert case, min nonzero uint256 minimum receive quantity + bytes memory data = abi.encodeWithSelector( + bytes4(keccak256("trade(address,address,address,uint256,uint256)")), + _sourceToken, + _destinationToken, + _destinationAddress, + _sourceQuantity, + _minDestinationQuantity + ); + assembly { revert(add(data, 32), mload(data)) } + } + if (destinationBalance >= _minDestinationQuantity) { // normal case + require(ERC20(_destinationToken).transfer(_destinationAddress, destinationBalance), "ERC20 transfer failed"); + } + else { // string revert case, minimum destination quantity not in exchange + revert("Insufficient funds in exchange"); + } + } + + /* ============ Adapter Functions ============ */ + + function getSpender() + external + view + returns (address) + { + return address(this); + } + + function getTradeCalldata( + address _sourceToken, + address _destinationToken, + address _destinationAddress, + uint256 _sourceQuantity, + uint256 _minDestinationQuantity, + bytes memory /* _data */ + ) + external + view + returns (address, uint256, bytes memory) + { + // Encode method data for SetToken to invoke + bytes memory methodData = abi.encodeWithSignature( + "trade(address,address,address,uint256,uint256)", + _sourceToken, + _destinationToken, + _destinationAddress, + _sourceQuantity, + _minDestinationQuantity + ); + + return (address(this), 0, methodData); + } +} diff --git a/external/abi/index-protocol/TradeModule.json b/external/abi/index-protocol/TradeModule.json deleted file mode 100644 index a51f95d0..00000000 --- a/external/abi/index-protocol/TradeModule.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "_format": "hh-sol-artifact-1", - "contractName": "TradeModule", - "sourceName": "contracts/protocol/modules/TradeModule.sol", - "abi": [{"inputs":[{"internalType":"contract IController","name":"_controller","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"contract ISetToken","name":"_setToken","type":"address"},{"indexed":true,"internalType":"address","name":"_sendToken","type":"address"},{"indexed":true,"internalType":"address","name":"_receiveToken","type":"address"},{"indexed":false,"internalType":"contract IExchangeAdapter","name":"_exchangeAdapter","type":"address"},{"indexed":false,"internalType":"uint256","name":"_totalSendAmount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"_totalReceiveAmount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"_protocolFee","type":"uint256"}],"name":"ComponentExchanged","type":"event"},{"inputs":[],"name":"controller","outputs":[{"internalType":"contract IController","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"contract ISetToken","name":"_setToken","type":"address"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"removeModule","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract ISetToken","name":"_setToken","type":"address"},{"internalType":"string","name":"_exchangeName","type":"string"},{"internalType":"address","name":"_sendToken","type":"address"},{"internalType":"uint256","name":"_sendQuantity","type":"uint256"},{"internalType":"address","name":"_receiveToken","type":"address"},{"internalType":"uint256","name":"_minReceiveQuantity","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"trade","outputs":[],"stateMutability":"nonpayable","type":"function"}], - "bytecode": "608060405234801561001057600080fd5b50604051611ed0380380611ed083398101604081905261002f91610058565b600080546001600160a01b0319166001600160a01b039290921691909117905560018055610086565b600060208284031215610069578081fd5b81516001600160a01b038116811461007f578182fd5b9392505050565b611e3b806100956000396000f3fe608060405234801561001057600080fd5b506004361061004c5760003560e01c80630a5cb52914610051578063847ef08d14610066578063c4d66de81461006e578063f77c479114610081575b600080fd5b61006461005f366004611891565b61009f565b005b6100646101e5565b61006461007c366004611875565b6101e7565b610089610329565b6040516100969190611985565b60405180910390f35b600260015414156100cb5760405162461bcd60e51b81526004016100c290611cff565b60405180910390fd5b6002600155866100db8133610338565b6100f75760405162461bcd60e51b81526004016100c290611cc8565b610100816103c8565b61011c5760405162461bcd60e51b81526004016100c290611aa0565b6101246115e8565b610132898989888a896104cc565b905061013e81876106be565b6101488184610725565b6000610153826108fe565b9050600061016183836109c0565b905060008061016f856109ea565b91509150886001600160a01b03168b6001600160a01b03168e6001600160a01b03167ff26ad8d17d1f980b62e857e137d0a000ce14bcf3b2aa54e1a0c7d57cf907e1a488602001518686896040516101ca9493929190611a30565b60405180910390a45050600180555050505050505050505050565b565b600054604051631d3af8fb60e21b815282916001600160a01b0316906374ebe3ec90610217908490600401611985565b60206040518083038186803b15801561022f57600080fd5b505afa158015610243573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906102679190611822565b6102835760405162461bcd60e51b81526004016100c290611c85565b61028c81610a92565b6102a85760405162461bcd60e51b81526004016100c290611ae8565b81336102b48282610338565b6102d05760405162461bcd60e51b81526004016100c290611cc8565b836001600160a01b0316630ffe0f1e6040518163ffffffff1660e01b8152600401600060405180830381600087803b15801561030b57600080fd5b505af115801561031f573d6000803e3d6000fd5b5050505050505050565b6000546001600160a01b031681565b6000816001600160a01b0316836001600160a01b031663481c6a756040518163ffffffff1660e01b815260040160206040518083038186803b15801561037d57600080fd5b505afa158015610391573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103b59190611703565b6001600160a01b03161490505b92915050565b60008054604051631d3af8fb60e21b81526001600160a01b03909116906374ebe3ec906103f9908590600401611985565b60206040518083038186803b15801561041157600080fd5b505afa158015610425573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906104499190611822565b80156103c257506040516335fc6c9f60e21b81526001600160a01b0383169063d7f1b27c9061047c903090600401611985565b60206040518083038186803b15801561049457600080fd5b505afa1580156104a8573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103c29190611822565b6104d46115e8565b6104dc6115e8565b6001600160a01b03881681526104f187610ac1565b6001600160a01b03908116602080840191909152878216604080850191909152878316606085015280516318160ddd60e01b81529051928b16926318160ddd92600480840193919291829003018186803b15801561054e57600080fd5b505afa158015610562573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906105869190611941565b608082018190526105979085610ad8565b60a082015260808101516105ab9084610ad8565b60c08201526040516370a0823160e01b81526001600160a01b038716906370a08231906105dc908b90600401611985565b60206040518083038186803b1580156105f457600080fd5b505afa158015610608573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061062c9190611941565b60e08201526040516370a0823160e01b81526001600160a01b038616906370a082319061065d908b90600401611985565b60206040518083038186803b15801561067557600080fd5b505afa158015610689573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906106ad9190611941565b610100820152979650505050505050565b60008260a00151116106e25760405162461bcd60e51b81526004016100c290611d36565b60408201518251610705916001600160a01b03909116908363ffffffff610aea16565b6107215760405162461bcd60e51b81526004016100c290611bfb565b5050565b6107c0826040015183602001516001600160a01b031663334fc2896040518163ffffffff1660e01b815260040160206040518083038186803b15801561076a57600080fd5b505afa15801561077e573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906107a29190611703565b60a085015185516001600160a01b031692919063ffffffff610b7b16565b600080606084602001516001600160a01b031663e171fcab8660400151876060015188600001518960a001518a60c001518a6040518763ffffffff1660e01b815260040161081396959493929190611999565b60006040518083038186803b15801561082b57600080fd5b505afa15801561083f573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052610867919081019061171f565b87516040516347b7819960e11b815293965091945092506001600160a01b031690638f6f0332906108a090869086908690600401611a00565b600060405180830381600087803b1580156108ba57600080fd5b505af11580156108ce573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526108f69190810190611842565b505050505050565b60008061099a83610100015184606001516001600160a01b03166370a0823186600001516040518263ffffffff1660e01b815260040161093e9190611985565b60206040518083038186803b15801561095657600080fd5b505afa15801561096a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061098e9190611941565b9063ffffffff610bec16565b90508260c001518110156103c25760405162461bcd60e51b81526004016100c290611a69565b6000806109ce600084610c2e565b90506109e38460000151856060015183610ccb565b9392505050565b6000806000610a1f846040015185608001518660e0015187600001516001600160a01b0316610d77909392919063ffffffff16565b505090506000610a568560600151866080015187610100015188600001516001600160a01b0316610d77909392919063ffffffff16565b505060e0860151909150610a70908363ffffffff610bec16565b610100860151610a8790839063ffffffff610bec16565b935093505050915091565b6040516353bae5f760e01b81526000906001600160a01b038316906353bae5f79061047c903090600401611985565b600080610acd83610eae565b90506109e381610eb9565b60006109e3838363ffffffff610f7616565b6000610af582610fa0565b6040516366cb8d2f60e01b81526001600160a01b038616906366cb8d2f90610b21908790600401611985565b60206040518083038186803b158015610b3957600080fd5b505afa158015610b4d573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610b719190611941565b1215949350505050565b60608282604051602401610b909291906119e7565b60408051601f198184030181529181526020820180516001600160e01b031663095ea7b360e01b179052516347b7819960e11b81529091506001600160a01b03861690638f6f0332906108a09087906000908690600401611a00565b60006109e383836040518060400160405280601e81526020017f536166654d6174683a207375627472616374696f6e206f766572666c6f770000815250610fc9565b6000805460405163792aa04f60e01b815282916001600160a01b03169063792aa04f90610c6190309088906004016119e7565b60206040518083038186803b158015610c7957600080fd5b505afa158015610c8d573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610cb19190611941565b9050610cc3838263ffffffff610f7616565b949350505050565b8015610d7257610d72826000809054906101000a90046001600160a01b03166001600160a01b031663469048406040518163ffffffff1660e01b815260040160206040518083038186803b158015610d2257600080fd5b505afa158015610d36573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610d5a9190611703565b6001600160a01b03861691908463ffffffff610ff516565b505050565b600080600080866001600160a01b03166370a08231896040518263ffffffff1660e01b8152600401610da99190611985565b60206040518083038186803b158015610dc157600080fd5b505afa158015610dd5573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610df99190611941565b90506000610e81896001600160a01b03166366cb8d2f8a6040518263ffffffff1660e01b8152600401610e2c9190611985565b60206040518083038186803b158015610e4457600080fd5b505afa158015610e58573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610e7c9190611941565b61113d565b90506000610e918888858561115f565b9050610e9e8a8a836111ae565b9199909850909650945050505050565b805160209091012090565b600080548190610ed1906001600160a01b031661130e565b6001600160a01b031663e6d642c530856040518363ffffffff1660e01b8152600401610efe9291906119e7565b60206040518083038186803b158015610f1657600080fd5b505afa158015610f2a573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610f4e9190611703565b90506001600160a01b0381166103c25760405162461bcd60e51b81526004016100c290611b1f565b60006109e3670de0b6b3a7640000610f94858563ffffffff61138d16565b9063ffffffff6113c716565b6000600160ff1b8210610fc55760405162461bcd60e51b81526004016100c290611c3d565b5090565b60008184841115610fed5760405162461bcd60e51b81526004016100c29190611a56565b505050900390565b8015611137576040516370a0823160e01b81526000906001600160a01b038516906370a082319061102a908890600401611985565b60206040518083038186803b15801561104257600080fd5b505afa158015611056573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061107a9190611941565b905061108885858585611409565b6040516370a0823160e01b81526000906001600160a01b038616906370a08231906110b7908990600401611985565b60206040518083038186803b1580156110cf57600080fd5b505afa1580156110e3573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906111079190611941565b9050611119828463ffffffff610bec16565b81146108f65760405162461bcd60e51b81526004016100c290611bc4565b50505050565b600080821215610fc55760405162461bcd60e51b81526004016100c290611b4e565b600080611182611175848863ffffffff610f7616565b869063ffffffff610bec16565b90506111a486611198868463ffffffff610bec16565b9063ffffffff61148016565b9695505050505050565b60006111ba848461149e565b9050801580156111ca5750600082115b15611241576111d98484611525565b61123c576040516304e3532760e41b81526001600160a01b03851690634e35327090611209908690600401611985565b600060405180830381600087803b15801561122357600080fd5b505af1158015611237573d6000803e3d6000fd5b505050505b6112be565b80801561124c575081155b156112be5761125b8484611525565b6112be57604051636f86c89760e01b81526001600160a01b03851690636f86c8979061128b908690600401611985565b600060405180830381600087803b1580156112a557600080fd5b505af11580156112b9573d6000803e3d6000fd5b505050505b836001600160a01b0316632ba57d17846112d785610fa0565b6040518363ffffffff1660e01b81526004016112f49291906119e7565b600060405180830381600087803b15801561030b57600080fd5b6040516373b2e76b60e11b81526000906001600160a01b0383169063e765ced69061133d908490600401611d6d565b60206040518083038186803b15801561135557600080fd5b505afa158015611369573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103c29190611703565b60008261139c575060006103c2565b828202828482816113a957fe5b04146109e35760405162461bcd60e51b81526004016100c290611b83565b60006109e383836040518060400160405280601a81526020017f536166654d6174683a206469766973696f6e206279207a65726f0000000000008152506115b1565b801561113757606082826040516024016114249291906119e7565b60408051601f198184030181529181526020820180516001600160e01b031663a9059cbb60e01b179052516347b7819960e11b81529091506001600160a01b03861690638f6f0332906108a09087906000908690600401611a00565b60006109e382610f9485670de0b6b3a764000063ffffffff61138d16565b600080836001600160a01b03166366cb8d2f846040518263ffffffff1660e01b81526004016114cd9190611985565b60206040518083038186803b1580156114e557600080fd5b505afa1580156114f9573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061151d9190611941565b139392505050565b600080836001600160a01b031663a7bdad03846040518263ffffffff1660e01b81526004016115549190611985565b60006040518083038186803b15801561156c57600080fd5b505afa158015611580573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526115a89190810190611777565b51119392505050565b600081836115d25760405162461bcd60e51b81526004016100c29190611a56565b5060008385816115de57fe5b0495945050505050565b60405180610120016040528060006001600160a01b0316815260200160006001600160a01b0316815260200160006001600160a01b0316815260200160006001600160a01b0316815260200160008152602001600081526020016000815260200160008152602001600081525090565b80516103c281611ded565b600082601f830112611673578081fd5b813561168661168182611d9d565b611d76565b915080825283602082850101111561169d57600080fd5b8060208401602084013760009082016020015292915050565b600082601f8301126116c6578081fd5b81516116d461168182611d9d565b91508082528360208285010111156116eb57600080fd5b6116fc816020840160208601611dc1565b5092915050565b600060208284031215611714578081fd5b81516109e381611ded565b600080600060608486031215611733578182fd5b835161173e81611ded565b60208501516040860151919450925067ffffffffffffffff811115611761578182fd5b61176d868287016116b6565b9150509250925092565b60006020808385031215611789578182fd5b825167ffffffffffffffff808211156117a0578384fd5b81850186601f8201126117b1578485fd5b80519250818311156117c1578485fd5b83830291506117d1848301611d76565b8381528481019082860184840187018a10156117eb578788fd5b8794505b85851015611815576118018a82611658565b8352600194909401939186019186016117ef565b5098975050505050505050565b600060208284031215611833578081fd5b815180151581146109e3578182fd5b600060208284031215611853578081fd5b815167ffffffffffffffff811115611869578182fd5b610cc3848285016116b6565b600060208284031215611886578081fd5b81356109e381611ded565b600080600080600080600060e0888a0312156118ab578283fd5b87356118b681611ded565b9650602088013567ffffffffffffffff808211156118d2578485fd5b6118de8b838c01611663565b975060408a013591506118f082611ded565b9095506060890135945060808901359061190982611ded565b90935060a0890135925060c08901359080821115611925578283fd5b506119328a828b01611663565b91505092959891949750929550565b600060208284031215611952578081fd5b5051919050565b60008151808452611971816020860160208601611dc1565b601f01601f19169290920160200192915050565b6001600160a01b0391909116815260200190565b6001600160a01b038781168252868116602083015285166040820152606081018490526080810183905260c060a082018190526000906119db90830184611959565b98975050505050505050565b6001600160a01b03929092168252602082015260400190565b600060018060a01b038516825283602083015260606040830152611a276060830184611959565b95945050505050565b6001600160a01b0394909416845260208401929092526040830152606082015260800190565b6000602082526109e36020830184611959565b6020808252601d908201527f536c6970706167652067726561746572207468616e20616c6c6f776564000000604082015260600190565b60208082526028908201527f4d75737420626520612076616c696420616e6420696e697469616c697a65642060408201526729b2ba2a37b5b2b760c11b606082015260800190565b6020808252601e908201527f4d7573742062652070656e64696e6720696e697469616c697a6174696f6e0000604082015260600190565b60208082526015908201527426bab9ba103132903b30b634b21030b230b83a32b960591b604082015260600190565b6020808252818101527f53616665436173743a2076616c7565206d75737420626520706f736974697665604082015260600190565b60208082526021908201527f536166654d6174683a206d756c7469706c69636174696f6e206f766572666c6f6040820152607760f81b606082015260800190565b6020808252601d908201527f496e76616c696420706f7374207472616e736665722062616c616e6365000000604082015260600190565b60208082526022908201527f556e69742063616e742062652067726561746572207468616e206578697374696040820152616e6760f01b606082015260800190565b60208082526028908201527f53616665436173743a2076616c756520646f65736e27742066697420696e2061604082015267371034b73a191a9b60c11b606082015260800190565b60208082526023908201527f4d75737420626520636f6e74726f6c6c65722d656e61626c656420536574546f60408201526235b2b760e91b606082015260800190565b6020808252601c908201527f4d7573742062652074686520536574546f6b656e206d616e6167657200000000604082015260600190565b6020808252601f908201527f5265656e7472616e637947756172643a207265656e7472616e742063616c6c00604082015260600190565b6020808252601d908201527f546f6b656e20746f2073656c6c206d757374206265206e6f6e7a65726f000000604082015260600190565b90815260200190565b60405181810167ffffffffffffffff81118282101715611d9557600080fd5b604052919050565b600067ffffffffffffffff821115611db3578081fd5b50601f01601f191660200190565b60005b83811015611ddc578181015183820152602001611dc4565b838111156111375750506000910152565b6001600160a01b0381168114611e0257600080fd5b5056fea2646970667358221220d538ba594473aed3476dd7da641db17a48d347f7f8e88079d51e30ef3a103dce64736f6c634300060a0033", - "deployedBytecode": "608060405234801561001057600080fd5b50604051611ed0380380611ed083398101604081905261002f91610058565b600080546001600160a01b0319166001600160a01b039290921691909117905560018055610086565b600060208284031215610069578081fd5b81516001600160a01b038116811461007f578182fd5b9392505050565b611e3b806100956000396000f3fe608060405234801561001057600080fd5b506004361061004c5760003560e01c80630a5cb52914610051578063847ef08d14610066578063c4d66de81461006e578063f77c479114610081575b600080fd5b61006461005f366004611891565b61009f565b005b6100646101e5565b61006461007c366004611875565b6101e7565b610089610329565b6040516100969190611985565b60405180910390f35b600260015414156100cb5760405162461bcd60e51b81526004016100c290611cff565b60405180910390fd5b6002600155866100db8133610338565b6100f75760405162461bcd60e51b81526004016100c290611cc8565b610100816103c8565b61011c5760405162461bcd60e51b81526004016100c290611aa0565b6101246115e8565b610132898989888a896104cc565b905061013e81876106be565b6101488184610725565b6000610153826108fe565b9050600061016183836109c0565b905060008061016f856109ea565b91509150886001600160a01b03168b6001600160a01b03168e6001600160a01b03167ff26ad8d17d1f980b62e857e137d0a000ce14bcf3b2aa54e1a0c7d57cf907e1a488602001518686896040516101ca9493929190611a30565b60405180910390a45050600180555050505050505050505050565b565b600054604051631d3af8fb60e21b815282916001600160a01b0316906374ebe3ec90610217908490600401611985565b60206040518083038186803b15801561022f57600080fd5b505afa158015610243573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906102679190611822565b6102835760405162461bcd60e51b81526004016100c290611c85565b61028c81610a92565b6102a85760405162461bcd60e51b81526004016100c290611ae8565b81336102b48282610338565b6102d05760405162461bcd60e51b81526004016100c290611cc8565b836001600160a01b0316630ffe0f1e6040518163ffffffff1660e01b8152600401600060405180830381600087803b15801561030b57600080fd5b505af115801561031f573d6000803e3d6000fd5b5050505050505050565b6000546001600160a01b031681565b6000816001600160a01b0316836001600160a01b031663481c6a756040518163ffffffff1660e01b815260040160206040518083038186803b15801561037d57600080fd5b505afa158015610391573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103b59190611703565b6001600160a01b03161490505b92915050565b60008054604051631d3af8fb60e21b81526001600160a01b03909116906374ebe3ec906103f9908590600401611985565b60206040518083038186803b15801561041157600080fd5b505afa158015610425573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906104499190611822565b80156103c257506040516335fc6c9f60e21b81526001600160a01b0383169063d7f1b27c9061047c903090600401611985565b60206040518083038186803b15801561049457600080fd5b505afa1580156104a8573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103c29190611822565b6104d46115e8565b6104dc6115e8565b6001600160a01b03881681526104f187610ac1565b6001600160a01b03908116602080840191909152878216604080850191909152878316606085015280516318160ddd60e01b81529051928b16926318160ddd92600480840193919291829003018186803b15801561054e57600080fd5b505afa158015610562573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906105869190611941565b608082018190526105979085610ad8565b60a082015260808101516105ab9084610ad8565b60c08201526040516370a0823160e01b81526001600160a01b038716906370a08231906105dc908b90600401611985565b60206040518083038186803b1580156105f457600080fd5b505afa158015610608573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061062c9190611941565b60e08201526040516370a0823160e01b81526001600160a01b038616906370a082319061065d908b90600401611985565b60206040518083038186803b15801561067557600080fd5b505afa158015610689573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906106ad9190611941565b610100820152979650505050505050565b60008260a00151116106e25760405162461bcd60e51b81526004016100c290611d36565b60408201518251610705916001600160a01b03909116908363ffffffff610aea16565b6107215760405162461bcd60e51b81526004016100c290611bfb565b5050565b6107c0826040015183602001516001600160a01b031663334fc2896040518163ffffffff1660e01b815260040160206040518083038186803b15801561076a57600080fd5b505afa15801561077e573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906107a29190611703565b60a085015185516001600160a01b031692919063ffffffff610b7b16565b600080606084602001516001600160a01b031663e171fcab8660400151876060015188600001518960a001518a60c001518a6040518763ffffffff1660e01b815260040161081396959493929190611999565b60006040518083038186803b15801561082b57600080fd5b505afa15801561083f573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052610867919081019061171f565b87516040516347b7819960e11b815293965091945092506001600160a01b031690638f6f0332906108a090869086908690600401611a00565b600060405180830381600087803b1580156108ba57600080fd5b505af11580156108ce573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526108f69190810190611842565b505050505050565b60008061099a83610100015184606001516001600160a01b03166370a0823186600001516040518263ffffffff1660e01b815260040161093e9190611985565b60206040518083038186803b15801561095657600080fd5b505afa15801561096a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061098e9190611941565b9063ffffffff610bec16565b90508260c001518110156103c25760405162461bcd60e51b81526004016100c290611a69565b6000806109ce600084610c2e565b90506109e38460000151856060015183610ccb565b9392505050565b6000806000610a1f846040015185608001518660e0015187600001516001600160a01b0316610d77909392919063ffffffff16565b505090506000610a568560600151866080015187610100015188600001516001600160a01b0316610d77909392919063ffffffff16565b505060e0860151909150610a70908363ffffffff610bec16565b610100860151610a8790839063ffffffff610bec16565b935093505050915091565b6040516353bae5f760e01b81526000906001600160a01b038316906353bae5f79061047c903090600401611985565b600080610acd83610eae565b90506109e381610eb9565b60006109e3838363ffffffff610f7616565b6000610af582610fa0565b6040516366cb8d2f60e01b81526001600160a01b038616906366cb8d2f90610b21908790600401611985565b60206040518083038186803b158015610b3957600080fd5b505afa158015610b4d573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610b719190611941565b1215949350505050565b60608282604051602401610b909291906119e7565b60408051601f198184030181529181526020820180516001600160e01b031663095ea7b360e01b179052516347b7819960e11b81529091506001600160a01b03861690638f6f0332906108a09087906000908690600401611a00565b60006109e383836040518060400160405280601e81526020017f536166654d6174683a207375627472616374696f6e206f766572666c6f770000815250610fc9565b6000805460405163792aa04f60e01b815282916001600160a01b03169063792aa04f90610c6190309088906004016119e7565b60206040518083038186803b158015610c7957600080fd5b505afa158015610c8d573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610cb19190611941565b9050610cc3838263ffffffff610f7616565b949350505050565b8015610d7257610d72826000809054906101000a90046001600160a01b03166001600160a01b031663469048406040518163ffffffff1660e01b815260040160206040518083038186803b158015610d2257600080fd5b505afa158015610d36573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610d5a9190611703565b6001600160a01b03861691908463ffffffff610ff516565b505050565b600080600080866001600160a01b03166370a08231896040518263ffffffff1660e01b8152600401610da99190611985565b60206040518083038186803b158015610dc157600080fd5b505afa158015610dd5573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610df99190611941565b90506000610e81896001600160a01b03166366cb8d2f8a6040518263ffffffff1660e01b8152600401610e2c9190611985565b60206040518083038186803b158015610e4457600080fd5b505afa158015610e58573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610e7c9190611941565b61113d565b90506000610e918888858561115f565b9050610e9e8a8a836111ae565b9199909850909650945050505050565b805160209091012090565b600080548190610ed1906001600160a01b031661130e565b6001600160a01b031663e6d642c530856040518363ffffffff1660e01b8152600401610efe9291906119e7565b60206040518083038186803b158015610f1657600080fd5b505afa158015610f2a573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610f4e9190611703565b90506001600160a01b0381166103c25760405162461bcd60e51b81526004016100c290611b1f565b60006109e3670de0b6b3a7640000610f94858563ffffffff61138d16565b9063ffffffff6113c716565b6000600160ff1b8210610fc55760405162461bcd60e51b81526004016100c290611c3d565b5090565b60008184841115610fed5760405162461bcd60e51b81526004016100c29190611a56565b505050900390565b8015611137576040516370a0823160e01b81526000906001600160a01b038516906370a082319061102a908890600401611985565b60206040518083038186803b15801561104257600080fd5b505afa158015611056573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061107a9190611941565b905061108885858585611409565b6040516370a0823160e01b81526000906001600160a01b038616906370a08231906110b7908990600401611985565b60206040518083038186803b1580156110cf57600080fd5b505afa1580156110e3573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906111079190611941565b9050611119828463ffffffff610bec16565b81146108f65760405162461bcd60e51b81526004016100c290611bc4565b50505050565b600080821215610fc55760405162461bcd60e51b81526004016100c290611b4e565b600080611182611175848863ffffffff610f7616565b869063ffffffff610bec16565b90506111a486611198868463ffffffff610bec16565b9063ffffffff61148016565b9695505050505050565b60006111ba848461149e565b9050801580156111ca5750600082115b15611241576111d98484611525565b61123c576040516304e3532760e41b81526001600160a01b03851690634e35327090611209908690600401611985565b600060405180830381600087803b15801561122357600080fd5b505af1158015611237573d6000803e3d6000fd5b505050505b6112be565b80801561124c575081155b156112be5761125b8484611525565b6112be57604051636f86c89760e01b81526001600160a01b03851690636f86c8979061128b908690600401611985565b600060405180830381600087803b1580156112a557600080fd5b505af11580156112b9573d6000803e3d6000fd5b505050505b836001600160a01b0316632ba57d17846112d785610fa0565b6040518363ffffffff1660e01b81526004016112f49291906119e7565b600060405180830381600087803b15801561030b57600080fd5b6040516373b2e76b60e11b81526000906001600160a01b0383169063e765ced69061133d908490600401611d6d565b60206040518083038186803b15801561135557600080fd5b505afa158015611369573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103c29190611703565b60008261139c575060006103c2565b828202828482816113a957fe5b04146109e35760405162461bcd60e51b81526004016100c290611b83565b60006109e383836040518060400160405280601a81526020017f536166654d6174683a206469766973696f6e206279207a65726f0000000000008152506115b1565b801561113757606082826040516024016114249291906119e7565b60408051601f198184030181529181526020820180516001600160e01b031663a9059cbb60e01b179052516347b7819960e11b81529091506001600160a01b03861690638f6f0332906108a09087906000908690600401611a00565b60006109e382610f9485670de0b6b3a764000063ffffffff61138d16565b600080836001600160a01b03166366cb8d2f846040518263ffffffff1660e01b81526004016114cd9190611985565b60206040518083038186803b1580156114e557600080fd5b505afa1580156114f9573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061151d9190611941565b139392505050565b600080836001600160a01b031663a7bdad03846040518263ffffffff1660e01b81526004016115549190611985565b60006040518083038186803b15801561156c57600080fd5b505afa158015611580573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526115a89190810190611777565b51119392505050565b600081836115d25760405162461bcd60e51b81526004016100c29190611a56565b5060008385816115de57fe5b0495945050505050565b60405180610120016040528060006001600160a01b0316815260200160006001600160a01b0316815260200160006001600160a01b0316815260200160006001600160a01b0316815260200160008152602001600081526020016000815260200160008152602001600081525090565b80516103c281611ded565b600082601f830112611673578081fd5b813561168661168182611d9d565b611d76565b915080825283602082850101111561169d57600080fd5b8060208401602084013760009082016020015292915050565b600082601f8301126116c6578081fd5b81516116d461168182611d9d565b91508082528360208285010111156116eb57600080fd5b6116fc816020840160208601611dc1565b5092915050565b600060208284031215611714578081fd5b81516109e381611ded565b600080600060608486031215611733578182fd5b835161173e81611ded565b60208501516040860151919450925067ffffffffffffffff811115611761578182fd5b61176d868287016116b6565b9150509250925092565b60006020808385031215611789578182fd5b825167ffffffffffffffff808211156117a0578384fd5b81850186601f8201126117b1578485fd5b80519250818311156117c1578485fd5b83830291506117d1848301611d76565b8381528481019082860184840187018a10156117eb578788fd5b8794505b85851015611815576118018a82611658565b8352600194909401939186019186016117ef565b5098975050505050505050565b600060208284031215611833578081fd5b815180151581146109e3578182fd5b600060208284031215611853578081fd5b815167ffffffffffffffff811115611869578182fd5b610cc3848285016116b6565b600060208284031215611886578081fd5b81356109e381611ded565b600080600080600080600060e0888a0312156118ab578283fd5b87356118b681611ded565b9650602088013567ffffffffffffffff808211156118d2578485fd5b6118de8b838c01611663565b975060408a013591506118f082611ded565b9095506060890135945060808901359061190982611ded565b90935060a0890135925060c08901359080821115611925578283fd5b506119328a828b01611663565b91505092959891949750929550565b600060208284031215611952578081fd5b5051919050565b60008151808452611971816020860160208601611dc1565b601f01601f19169290920160200192915050565b6001600160a01b0391909116815260200190565b6001600160a01b038781168252868116602083015285166040820152606081018490526080810183905260c060a082018190526000906119db90830184611959565b98975050505050505050565b6001600160a01b03929092168252602082015260400190565b600060018060a01b038516825283602083015260606040830152611a276060830184611959565b95945050505050565b6001600160a01b0394909416845260208401929092526040830152606082015260800190565b6000602082526109e36020830184611959565b6020808252601d908201527f536c6970706167652067726561746572207468616e20616c6c6f776564000000604082015260600190565b60208082526028908201527f4d75737420626520612076616c696420616e6420696e697469616c697a65642060408201526729b2ba2a37b5b2b760c11b606082015260800190565b6020808252601e908201527f4d7573742062652070656e64696e6720696e697469616c697a6174696f6e0000604082015260600190565b60208082526015908201527426bab9ba103132903b30b634b21030b230b83a32b960591b604082015260600190565b6020808252818101527f53616665436173743a2076616c7565206d75737420626520706f736974697665604082015260600190565b60208082526021908201527f536166654d6174683a206d756c7469706c69636174696f6e206f766572666c6f6040820152607760f81b606082015260800190565b6020808252601d908201527f496e76616c696420706f7374207472616e736665722062616c616e6365000000604082015260600190565b60208082526022908201527f556e69742063616e742062652067726561746572207468616e206578697374696040820152616e6760f01b606082015260800190565b60208082526028908201527f53616665436173743a2076616c756520646f65736e27742066697420696e2061604082015267371034b73a191a9b60c11b606082015260800190565b60208082526023908201527f4d75737420626520636f6e74726f6c6c65722d656e61626c656420536574546f60408201526235b2b760e91b606082015260800190565b6020808252601c908201527f4d7573742062652074686520536574546f6b656e206d616e6167657200000000604082015260600190565b6020808252601f908201527f5265656e7472616e637947756172643a207265656e7472616e742063616c6c00604082015260600190565b6020808252601d908201527f546f6b656e20746f2073656c6c206d757374206265206e6f6e7a65726f000000604082015260600190565b90815260200190565b60405181810167ffffffffffffffff81118282101715611d9557600080fd5b604052919050565b600067ffffffffffffffff821115611db3578081fd5b50601f01601f191660200190565b60005b83811015611ddc578181015183820152602001611dc4565b838111156111375750506000910152565b6001600160a01b0381168114611e0257600080fd5b5056fea2646970667358221220d538ba594473aed3476dd7da641db17a48d347f7f8e88079d51e30ef3a103dce64736f6c634300060a0033000000000000000000000000a4c8d221d8bb851f83aadd0223a8900a6921a349", - "linkReferences": {}, - "deployedLinkReferences": {} -} diff --git a/external/abi/set/TradeModule.json b/external/abi/set/TradeModule.json new file mode 100644 index 00000000..8a1ab03c --- /dev/null +++ b/external/abi/set/TradeModule.json @@ -0,0 +1,151 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "TradeModule", + "sourceName": "contracts/protocol/modules/v1/TradeModule.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "contract IController", + "name": "_controller", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "_sendToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "_receiveToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "contract IExchangeAdapter", + "name": "_exchangeAdapter", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_totalSendAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_totalReceiveAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_protocolFee", + "type": "uint256" + } + ], + "name": "ComponentExchanged", + "type": "event" + }, + { + "inputs": [], + "name": "controller", + "outputs": [ + { + "internalType": "contract IController", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [], + "name": "removeModule", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "internalType": "string", + "name": "_exchangeName", + "type": "string" + }, + { + "internalType": "address", + "name": "_sendToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_sendQuantity", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_receiveToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_minReceiveQuantity", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "trade", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + } + ], + "bytecode": "0x608060405234801561001057600080fd5b50604051611fc2380380611fc283398101604081905261002f91610058565b600080546001600160a01b0319166001600160a01b039290921691909117905560018055610086565b600060208284031215610069578081fd5b81516001600160a01b038116811461007f578182fd5b9392505050565b611f2d806100956000396000f3fe608060405234801561001057600080fd5b506004361061004c5760003560e01c80630a5cb52914610051578063847ef08d14610066578063c4d66de81461006e578063f77c479114610081575b600080fd5b61006461005f366004611957565b61009f565b005b6100646101a3565b61006461007c36600461193b565b6101a5565b610089610214565b6040516100969190611a4b565b60405180910390f35b600260015414156100cb5760405162461bcd60e51b81526004016100c290611df4565b60405180910390fd5b6002600155866100da81610223565b6100e26116ae565b6100f0898989888a89610271565b90506100fc8187610463565b61010681846104ca565b6000610111826106a3565b9050600061011f838361076b565b905060008061012d85610795565b91509150886001600160a01b03168b6001600160a01b03168e6001600160a01b03167ff26ad8d17d1f980b62e857e137d0a000ce14bcf3b2aa54e1a0c7d57cf907e1a488602001518686896040516101889493929190611af6565b60405180910390a45050600180555050505050505050505050565b565b806101af8161083d565b81336101bb82826108fe565b836001600160a01b0316630ffe0f1e6040518163ffffffff1660e01b8152600401600060405180830381600087803b1580156101f657600080fd5b505af115801561020a573d6000803e3d6000fd5b5050505050505050565b6000546001600160a01b031681565b61022d8133610924565b6102495760405162461bcd60e51b81526004016100c290611dbd565b610252816109b2565b61026e5760405162461bcd60e51b81526004016100c290611b95565b50565b6102796116ae565b6102816116ae565b6001600160a01b038816815261029687610ab6565b6001600160a01b03908116602080840191909152878216604080850191909152878316606085015280516318160ddd60e01b81529051928b16926318160ddd92600480840193919291829003018186803b1580156102f357600080fd5b505afa158015610307573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061032b9190611a07565b6080820181905261033c9085610acd565b60a082015260808101516103509084610acd565b60c08201526040516370a0823160e01b81526001600160a01b038716906370a0823190610381908b90600401611a4b565b60206040518083038186803b15801561039957600080fd5b505afa1580156103ad573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103d19190611a07565b60e08201526040516370a0823160e01b81526001600160a01b038616906370a0823190610402908b90600401611a4b565b60206040518083038186803b15801561041a57600080fd5b505afa15801561042e573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906104529190611a07565b610100820152979650505050505050565b60008260a00151116104875760405162461bcd60e51b81526004016100c290611e2b565b604082015182516104aa916001600160a01b03909116908363ffffffff610adf16565b6104c65760405162461bcd60e51b81526004016100c290611cf0565b5050565b610565826040015183602001516001600160a01b031663334fc2896040518163ffffffff1660e01b815260040160206040518083038186803b15801561050f57600080fd5b505afa158015610523573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061054791906117c9565b60a085015185516001600160a01b031692919063ffffffff610b7016565b600080606084602001516001600160a01b031663e171fcab8660400151876060015188600001518960a001518a60c001518a6040518763ffffffff1660e01b81526004016105b896959493929190611a5f565b60006040518083038186803b1580156105d057600080fd5b505afa1580156105e4573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261060c91908101906117e5565b87516040516347b7819960e11b815293965091945092506001600160a01b031690638f6f03329061064590869086908690600401611ac6565b600060405180830381600087803b15801561065f57600080fd5b505af1158015610673573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261069b9190810190611908565b505050505050565b60008061073f83610100015184606001516001600160a01b03166370a0823186600001516040518263ffffffff1660e01b81526004016106e39190611a4b565b60206040518083038186803b1580156106fb57600080fd5b505afa15801561070f573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906107339190611a07565b9063ffffffff610be116565b90508260c001518110156107655760405162461bcd60e51b81526004016100c290611b5e565b92915050565b600080610779600084610c23565b905061078e8460000151856060015183610cc0565b9392505050565b60008060006107ca846040015185608001518660e0015187600001516001600160a01b0316610d6c909392919063ffffffff16565b5050905060006108018560600151866080015187610100015188600001516001600160a01b0316610d6c909392919063ffffffff16565b505060e086015190915061081b908363ffffffff610be116565b61010086015161083290839063ffffffff610be116565b935093505050915091565b600054604051631d3af8fb60e21b81526001600160a01b03909116906374ebe3ec9061086d908490600401611a4b565b60206040518083038186803b15801561088557600080fd5b505afa158015610899573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906108bd91906118e8565b6108d95760405162461bcd60e51b81526004016100c290611d7a565b6108e281610eb2565b61026e5760405162461bcd60e51b81526004016100c290611bdd565b6109088282610924565b6104c65760405162461bcd60e51b81526004016100c290611dbd565b6000816001600160a01b0316836001600160a01b031663481c6a756040518163ffffffff1660e01b815260040160206040518083038186803b15801561096957600080fd5b505afa15801561097d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109a191906117c9565b6001600160a01b0316149392505050565b60008054604051631d3af8fb60e21b81526001600160a01b03909116906374ebe3ec906109e3908590600401611a4b565b60206040518083038186803b1580156109fb57600080fd5b505afa158015610a0f573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610a3391906118e8565b801561076557506040516335fc6c9f60e21b81526001600160a01b0383169063d7f1b27c90610a66903090600401611a4b565b60206040518083038186803b158015610a7e57600080fd5b505afa158015610a92573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061076591906118e8565b600080610ac283610ee1565b905061078e81610eec565b600061078e838363ffffffff610fa916565b6000610aea82610fd3565b6040516366cb8d2f60e01b81526001600160a01b038616906366cb8d2f90610b16908790600401611a4b565b60206040518083038186803b158015610b2e57600080fd5b505afa158015610b42573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610b669190611a07565b1215949350505050565b60608282604051602401610b85929190611aad565b60408051601f198184030181529181526020820180516001600160e01b031663095ea7b360e01b179052516347b7819960e11b81529091506001600160a01b03861690638f6f0332906106459087906000908690600401611ac6565b600061078e83836040518060400160405280601e81526020017f536166654d6174683a207375627472616374696f6e206f766572666c6f770000815250610ffc565b6000805460405163792aa04f60e01b815282916001600160a01b03169063792aa04f90610c569030908890600401611aad565b60206040518083038186803b158015610c6e57600080fd5b505afa158015610c82573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610ca69190611a07565b9050610cb8838263ffffffff610fa916565b949350505050565b8015610d6757610d67826000809054906101000a90046001600160a01b03166001600160a01b031663469048406040518163ffffffff1660e01b815260040160206040518083038186803b158015610d1757600080fd5b505afa158015610d2b573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610d4f91906117c9565b6001600160a01b03861691908463ffffffff61102816565b505050565b600080600080866001600160a01b03166370a08231896040518263ffffffff1660e01b8152600401610d9e9190611a4b565b60206040518083038186803b158015610db657600080fd5b505afa158015610dca573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610dee9190611a07565b90506000610e76896001600160a01b03166366cb8d2f8a6040518263ffffffff1660e01b8152600401610e219190611a4b565b60206040518083038186803b158015610e3957600080fd5b505afa158015610e4d573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610e719190611a07565b611170565b905060008215610e9357610e8c88888585611192565b9050610e97565b5060005b610ea28a8a836111e1565b9199909850909650945050505050565b6040516353bae5f760e01b81526000906001600160a01b038316906353bae5f790610a66903090600401611a4b565b805160209091012090565b600080548190610f04906001600160a01b0316611341565b6001600160a01b031663e6d642c530856040518363ffffffff1660e01b8152600401610f31929190611aad565b60206040518083038186803b158015610f4957600080fd5b505afa158015610f5d573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610f8191906117c9565b90506001600160a01b0381166107655760405162461bcd60e51b81526004016100c290611c14565b600061078e670de0b6b3a7640000610fc7858563ffffffff6113c016565b9063ffffffff6113fa16565b6000600160ff1b8210610ff85760405162461bcd60e51b81526004016100c290611d32565b5090565b600081848411156110205760405162461bcd60e51b81526004016100c29190611b1c565b505050900390565b801561116a576040516370a0823160e01b81526000906001600160a01b038516906370a082319061105d908890600401611a4b565b60206040518083038186803b15801561107557600080fd5b505afa158015611089573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906110ad9190611a07565b90506110bb8585858561143c565b6040516370a0823160e01b81526000906001600160a01b038616906370a08231906110ea908990600401611a4b565b60206040518083038186803b15801561110257600080fd5b505afa158015611116573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061113a9190611a07565b905061114c828463ffffffff610be116565b811461069b5760405162461bcd60e51b81526004016100c290611cb9565b50505050565b600080821215610ff85760405162461bcd60e51b81526004016100c290611c43565b6000806111b56111a8848863ffffffff610fa916565b869063ffffffff610be116565b90506111d7866111cb868463ffffffff610be116565b9063ffffffff61154616565b9695505050505050565b60006111ed8484611564565b9050801580156111fd5750600082115b156112745761120c84846115eb565b61126f576040516304e3532760e41b81526001600160a01b03851690634e3532709061123c908690600401611a4b565b600060405180830381600087803b15801561125657600080fd5b505af115801561126a573d6000803e3d6000fd5b505050505b6112f1565b80801561127f575081155b156112f15761128e84846115eb565b6112f157604051636f86c89760e01b81526001600160a01b03851690636f86c897906112be908690600401611a4b565b600060405180830381600087803b1580156112d857600080fd5b505af11580156112ec573d6000803e3d6000fd5b505050505b836001600160a01b0316632ba57d178461130a85610fd3565b6040518363ffffffff1660e01b8152600401611327929190611aad565b600060405180830381600087803b1580156101f657600080fd5b6040516373b2e76b60e11b81526000906001600160a01b0383169063e765ced690611370908490600401611e62565b60206040518083038186803b15801561138857600080fd5b505afa15801561139c573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061076591906117c9565b6000826113cf57506000610765565b828202828482816113dc57fe5b041461078e5760405162461bcd60e51b81526004016100c290611c78565b600061078e83836040518060400160405280601a81526020017f536166654d6174683a206469766973696f6e206279207a65726f000000000000815250611677565b801561116a5760608282604051602401611457929190611aad565b60408051601f198184030181529181526020820180516001600160e01b031663a9059cbb60e01b179052516347b7819960e11b81529091506060906001600160a01b03871690638f6f0332906114b69088906000908790600401611ac6565b600060405180830381600087803b1580156114d057600080fd5b505af11580156114e4573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261150c9190810190611908565b80519091501561069b578080602001905181019061152a91906118e8565b61069b5760405162461bcd60e51b81526004016100c290611b2f565b600061078e82610fc785670de0b6b3a764000063ffffffff6113c016565b600080836001600160a01b03166366cb8d2f846040518263ffffffff1660e01b81526004016115939190611a4b565b60206040518083038186803b1580156115ab57600080fd5b505afa1580156115bf573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906115e39190611a07565b139392505050565b600080836001600160a01b031663a7bdad03846040518263ffffffff1660e01b815260040161161a9190611a4b565b60006040518083038186803b15801561163257600080fd5b505afa158015611646573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261166e919081019061183d565b51119392505050565b600081836116985760405162461bcd60e51b81526004016100c29190611b1c565b5060008385816116a457fe5b0495945050505050565b60405180610120016040528060006001600160a01b0316815260200160006001600160a01b0316815260200160006001600160a01b0316815260200160006001600160a01b0316815260200160008152602001600081526020016000815260200160008152602001600081525090565b805161076581611ee2565b600082601f830112611739578081fd5b813561174c61174782611e92565b611e6b565b915080825283602082850101111561176357600080fd5b8060208401602084013760009082016020015292915050565b600082601f83011261178c578081fd5b815161179a61174782611e92565b91508082528360208285010111156117b157600080fd5b6117c2816020840160208601611eb6565b5092915050565b6000602082840312156117da578081fd5b815161078e81611ee2565b6000806000606084860312156117f9578182fd5b835161180481611ee2565b60208501516040860151919450925067ffffffffffffffff811115611827578182fd5b6118338682870161177c565b9150509250925092565b6000602080838503121561184f578182fd5b825167ffffffffffffffff80821115611866578384fd5b81850186601f820112611877578485fd5b8051925081831115611887578485fd5b8383029150611897848301611e6b565b8381528481019082860184840187018a10156118b1578788fd5b8794505b858510156118db576118c78a8261171e565b8352600194909401939186019186016118b5565b5098975050505050505050565b6000602082840312156118f9578081fd5b8151801515811461078e578182fd5b600060208284031215611919578081fd5b815167ffffffffffffffff81111561192f578182fd5b610cb88482850161177c565b60006020828403121561194c578081fd5b813561078e81611ee2565b600080600080600080600060e0888a031215611971578283fd5b873561197c81611ee2565b9650602088013567ffffffffffffffff80821115611998578485fd5b6119a48b838c01611729565b975060408a013591506119b682611ee2565b909550606089013594506080890135906119cf82611ee2565b90935060a0890135925060c089013590808211156119eb578283fd5b506119f88a828b01611729565b91505092959891949750929550565b600060208284031215611a18578081fd5b5051919050565b60008151808452611a37816020860160208601611eb6565b601f01601f19169290920160200192915050565b6001600160a01b0391909116815260200190565b6001600160a01b038781168252868116602083015285166040820152606081018490526080810183905260c060a08201819052600090611aa190830184611a1f565b98975050505050505050565b6001600160a01b03929092168252602082015260400190565b600060018060a01b038516825283602083015260606040830152611aed6060830184611a1f565b95945050505050565b6001600160a01b0394909416845260208401929092526040830152606082015260800190565b60006020825261078e6020830184611a1f565b602080825260159082015274115490cc8c081d1c985b9cd9995c8819985a5b1959605a1b604082015260600190565b6020808252601d908201527f536c6970706167652067726561746572207468616e20616c6c6f776564000000604082015260600190565b60208082526028908201527f4d75737420626520612076616c696420616e6420696e697469616c697a65642060408201526729b2ba2a37b5b2b760c11b606082015260800190565b6020808252601e908201527f4d7573742062652070656e64696e6720696e697469616c697a6174696f6e0000604082015260600190565b60208082526015908201527426bab9ba103132903b30b634b21030b230b83a32b960591b604082015260600190565b6020808252818101527f53616665436173743a2076616c7565206d75737420626520706f736974697665604082015260600190565b60208082526021908201527f536166654d6174683a206d756c7469706c69636174696f6e206f766572666c6f6040820152607760f81b606082015260800190565b6020808252601d908201527f496e76616c696420706f7374207472616e736665722062616c616e6365000000604082015260600190565b60208082526022908201527f556e69742063616e742062652067726561746572207468616e206578697374696040820152616e6760f01b606082015260800190565b60208082526028908201527f53616665436173743a2076616c756520646f65736e27742066697420696e2061604082015267371034b73a191a9b60c11b606082015260800190565b60208082526023908201527f4d75737420626520636f6e74726f6c6c65722d656e61626c656420536574546f60408201526235b2b760e91b606082015260800190565b6020808252601c908201527f4d7573742062652074686520536574546f6b656e206d616e6167657200000000604082015260600190565b6020808252601f908201527f5265656e7472616e637947756172643a207265656e7472616e742063616c6c00604082015260600190565b6020808252601d908201527f546f6b656e20746f2073656c6c206d757374206265206e6f6e7a65726f000000604082015260600190565b90815260200190565b60405181810167ffffffffffffffff81118282101715611e8a57600080fd5b604052919050565b600067ffffffffffffffff821115611ea8578081fd5b50601f01601f191660200190565b60005b83811015611ed1578181015183820152602001611eb9565b8381111561116a5750506000910152565b6001600160a01b038116811461026e57600080fdfea2646970667358221220245402a4e3852af9f6b6995ba518e585c6b34345f493b78612912dab1a0f0bfd64736f6c634300060a0033", + "deployedBytecode": "0x608060405234801561001057600080fd5b506004361061004c5760003560e01c80630a5cb52914610051578063847ef08d14610066578063c4d66de81461006e578063f77c479114610081575b600080fd5b61006461005f366004611957565b61009f565b005b6100646101a3565b61006461007c36600461193b565b6101a5565b610089610214565b6040516100969190611a4b565b60405180910390f35b600260015414156100cb5760405162461bcd60e51b81526004016100c290611df4565b60405180910390fd5b6002600155866100da81610223565b6100e26116ae565b6100f0898989888a89610271565b90506100fc8187610463565b61010681846104ca565b6000610111826106a3565b9050600061011f838361076b565b905060008061012d85610795565b91509150886001600160a01b03168b6001600160a01b03168e6001600160a01b03167ff26ad8d17d1f980b62e857e137d0a000ce14bcf3b2aa54e1a0c7d57cf907e1a488602001518686896040516101889493929190611af6565b60405180910390a45050600180555050505050505050505050565b565b806101af8161083d565b81336101bb82826108fe565b836001600160a01b0316630ffe0f1e6040518163ffffffff1660e01b8152600401600060405180830381600087803b1580156101f657600080fd5b505af115801561020a573d6000803e3d6000fd5b5050505050505050565b6000546001600160a01b031681565b61022d8133610924565b6102495760405162461bcd60e51b81526004016100c290611dbd565b610252816109b2565b61026e5760405162461bcd60e51b81526004016100c290611b95565b50565b6102796116ae565b6102816116ae565b6001600160a01b038816815261029687610ab6565b6001600160a01b03908116602080840191909152878216604080850191909152878316606085015280516318160ddd60e01b81529051928b16926318160ddd92600480840193919291829003018186803b1580156102f357600080fd5b505afa158015610307573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061032b9190611a07565b6080820181905261033c9085610acd565b60a082015260808101516103509084610acd565b60c08201526040516370a0823160e01b81526001600160a01b038716906370a0823190610381908b90600401611a4b565b60206040518083038186803b15801561039957600080fd5b505afa1580156103ad573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103d19190611a07565b60e08201526040516370a0823160e01b81526001600160a01b038616906370a0823190610402908b90600401611a4b565b60206040518083038186803b15801561041a57600080fd5b505afa15801561042e573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906104529190611a07565b610100820152979650505050505050565b60008260a00151116104875760405162461bcd60e51b81526004016100c290611e2b565b604082015182516104aa916001600160a01b03909116908363ffffffff610adf16565b6104c65760405162461bcd60e51b81526004016100c290611cf0565b5050565b610565826040015183602001516001600160a01b031663334fc2896040518163ffffffff1660e01b815260040160206040518083038186803b15801561050f57600080fd5b505afa158015610523573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061054791906117c9565b60a085015185516001600160a01b031692919063ffffffff610b7016565b600080606084602001516001600160a01b031663e171fcab8660400151876060015188600001518960a001518a60c001518a6040518763ffffffff1660e01b81526004016105b896959493929190611a5f565b60006040518083038186803b1580156105d057600080fd5b505afa1580156105e4573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261060c91908101906117e5565b87516040516347b7819960e11b815293965091945092506001600160a01b031690638f6f03329061064590869086908690600401611ac6565b600060405180830381600087803b15801561065f57600080fd5b505af1158015610673573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261069b9190810190611908565b505050505050565b60008061073f83610100015184606001516001600160a01b03166370a0823186600001516040518263ffffffff1660e01b81526004016106e39190611a4b565b60206040518083038186803b1580156106fb57600080fd5b505afa15801561070f573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906107339190611a07565b9063ffffffff610be116565b90508260c001518110156107655760405162461bcd60e51b81526004016100c290611b5e565b92915050565b600080610779600084610c23565b905061078e8460000151856060015183610cc0565b9392505050565b60008060006107ca846040015185608001518660e0015187600001516001600160a01b0316610d6c909392919063ffffffff16565b5050905060006108018560600151866080015187610100015188600001516001600160a01b0316610d6c909392919063ffffffff16565b505060e086015190915061081b908363ffffffff610be116565b61010086015161083290839063ffffffff610be116565b935093505050915091565b600054604051631d3af8fb60e21b81526001600160a01b03909116906374ebe3ec9061086d908490600401611a4b565b60206040518083038186803b15801561088557600080fd5b505afa158015610899573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906108bd91906118e8565b6108d95760405162461bcd60e51b81526004016100c290611d7a565b6108e281610eb2565b61026e5760405162461bcd60e51b81526004016100c290611bdd565b6109088282610924565b6104c65760405162461bcd60e51b81526004016100c290611dbd565b6000816001600160a01b0316836001600160a01b031663481c6a756040518163ffffffff1660e01b815260040160206040518083038186803b15801561096957600080fd5b505afa15801561097d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109a191906117c9565b6001600160a01b0316149392505050565b60008054604051631d3af8fb60e21b81526001600160a01b03909116906374ebe3ec906109e3908590600401611a4b565b60206040518083038186803b1580156109fb57600080fd5b505afa158015610a0f573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610a3391906118e8565b801561076557506040516335fc6c9f60e21b81526001600160a01b0383169063d7f1b27c90610a66903090600401611a4b565b60206040518083038186803b158015610a7e57600080fd5b505afa158015610a92573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061076591906118e8565b600080610ac283610ee1565b905061078e81610eec565b600061078e838363ffffffff610fa916565b6000610aea82610fd3565b6040516366cb8d2f60e01b81526001600160a01b038616906366cb8d2f90610b16908790600401611a4b565b60206040518083038186803b158015610b2e57600080fd5b505afa158015610b42573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610b669190611a07565b1215949350505050565b60608282604051602401610b85929190611aad565b60408051601f198184030181529181526020820180516001600160e01b031663095ea7b360e01b179052516347b7819960e11b81529091506001600160a01b03861690638f6f0332906106459087906000908690600401611ac6565b600061078e83836040518060400160405280601e81526020017f536166654d6174683a207375627472616374696f6e206f766572666c6f770000815250610ffc565b6000805460405163792aa04f60e01b815282916001600160a01b03169063792aa04f90610c569030908890600401611aad565b60206040518083038186803b158015610c6e57600080fd5b505afa158015610c82573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610ca69190611a07565b9050610cb8838263ffffffff610fa916565b949350505050565b8015610d6757610d67826000809054906101000a90046001600160a01b03166001600160a01b031663469048406040518163ffffffff1660e01b815260040160206040518083038186803b158015610d1757600080fd5b505afa158015610d2b573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610d4f91906117c9565b6001600160a01b03861691908463ffffffff61102816565b505050565b600080600080866001600160a01b03166370a08231896040518263ffffffff1660e01b8152600401610d9e9190611a4b565b60206040518083038186803b158015610db657600080fd5b505afa158015610dca573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610dee9190611a07565b90506000610e76896001600160a01b03166366cb8d2f8a6040518263ffffffff1660e01b8152600401610e219190611a4b565b60206040518083038186803b158015610e3957600080fd5b505afa158015610e4d573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610e719190611a07565b611170565b905060008215610e9357610e8c88888585611192565b9050610e97565b5060005b610ea28a8a836111e1565b9199909850909650945050505050565b6040516353bae5f760e01b81526000906001600160a01b038316906353bae5f790610a66903090600401611a4b565b805160209091012090565b600080548190610f04906001600160a01b0316611341565b6001600160a01b031663e6d642c530856040518363ffffffff1660e01b8152600401610f31929190611aad565b60206040518083038186803b158015610f4957600080fd5b505afa158015610f5d573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610f8191906117c9565b90506001600160a01b0381166107655760405162461bcd60e51b81526004016100c290611c14565b600061078e670de0b6b3a7640000610fc7858563ffffffff6113c016565b9063ffffffff6113fa16565b6000600160ff1b8210610ff85760405162461bcd60e51b81526004016100c290611d32565b5090565b600081848411156110205760405162461bcd60e51b81526004016100c29190611b1c565b505050900390565b801561116a576040516370a0823160e01b81526000906001600160a01b038516906370a082319061105d908890600401611a4b565b60206040518083038186803b15801561107557600080fd5b505afa158015611089573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906110ad9190611a07565b90506110bb8585858561143c565b6040516370a0823160e01b81526000906001600160a01b038616906370a08231906110ea908990600401611a4b565b60206040518083038186803b15801561110257600080fd5b505afa158015611116573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061113a9190611a07565b905061114c828463ffffffff610be116565b811461069b5760405162461bcd60e51b81526004016100c290611cb9565b50505050565b600080821215610ff85760405162461bcd60e51b81526004016100c290611c43565b6000806111b56111a8848863ffffffff610fa916565b869063ffffffff610be116565b90506111d7866111cb868463ffffffff610be116565b9063ffffffff61154616565b9695505050505050565b60006111ed8484611564565b9050801580156111fd5750600082115b156112745761120c84846115eb565b61126f576040516304e3532760e41b81526001600160a01b03851690634e3532709061123c908690600401611a4b565b600060405180830381600087803b15801561125657600080fd5b505af115801561126a573d6000803e3d6000fd5b505050505b6112f1565b80801561127f575081155b156112f15761128e84846115eb565b6112f157604051636f86c89760e01b81526001600160a01b03851690636f86c897906112be908690600401611a4b565b600060405180830381600087803b1580156112d857600080fd5b505af11580156112ec573d6000803e3d6000fd5b505050505b836001600160a01b0316632ba57d178461130a85610fd3565b6040518363ffffffff1660e01b8152600401611327929190611aad565b600060405180830381600087803b1580156101f657600080fd5b6040516373b2e76b60e11b81526000906001600160a01b0383169063e765ced690611370908490600401611e62565b60206040518083038186803b15801561138857600080fd5b505afa15801561139c573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061076591906117c9565b6000826113cf57506000610765565b828202828482816113dc57fe5b041461078e5760405162461bcd60e51b81526004016100c290611c78565b600061078e83836040518060400160405280601a81526020017f536166654d6174683a206469766973696f6e206279207a65726f000000000000815250611677565b801561116a5760608282604051602401611457929190611aad565b60408051601f198184030181529181526020820180516001600160e01b031663a9059cbb60e01b179052516347b7819960e11b81529091506060906001600160a01b03871690638f6f0332906114b69088906000908790600401611ac6565b600060405180830381600087803b1580156114d057600080fd5b505af11580156114e4573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261150c9190810190611908565b80519091501561069b578080602001905181019061152a91906118e8565b61069b5760405162461bcd60e51b81526004016100c290611b2f565b600061078e82610fc785670de0b6b3a764000063ffffffff6113c016565b600080836001600160a01b03166366cb8d2f846040518263ffffffff1660e01b81526004016115939190611a4b565b60206040518083038186803b1580156115ab57600080fd5b505afa1580156115bf573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906115e39190611a07565b139392505050565b600080836001600160a01b031663a7bdad03846040518263ffffffff1660e01b815260040161161a9190611a4b565b60006040518083038186803b15801561163257600080fd5b505afa158015611646573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261166e919081019061183d565b51119392505050565b600081836116985760405162461bcd60e51b81526004016100c29190611b1c565b5060008385816116a457fe5b0495945050505050565b60405180610120016040528060006001600160a01b0316815260200160006001600160a01b0316815260200160006001600160a01b0316815260200160006001600160a01b0316815260200160008152602001600081526020016000815260200160008152602001600081525090565b805161076581611ee2565b600082601f830112611739578081fd5b813561174c61174782611e92565b611e6b565b915080825283602082850101111561176357600080fd5b8060208401602084013760009082016020015292915050565b600082601f83011261178c578081fd5b815161179a61174782611e92565b91508082528360208285010111156117b157600080fd5b6117c2816020840160208601611eb6565b5092915050565b6000602082840312156117da578081fd5b815161078e81611ee2565b6000806000606084860312156117f9578182fd5b835161180481611ee2565b60208501516040860151919450925067ffffffffffffffff811115611827578182fd5b6118338682870161177c565b9150509250925092565b6000602080838503121561184f578182fd5b825167ffffffffffffffff80821115611866578384fd5b81850186601f820112611877578485fd5b8051925081831115611887578485fd5b8383029150611897848301611e6b565b8381528481019082860184840187018a10156118b1578788fd5b8794505b858510156118db576118c78a8261171e565b8352600194909401939186019186016118b5565b5098975050505050505050565b6000602082840312156118f9578081fd5b8151801515811461078e578182fd5b600060208284031215611919578081fd5b815167ffffffffffffffff81111561192f578182fd5b610cb88482850161177c565b60006020828403121561194c578081fd5b813561078e81611ee2565b600080600080600080600060e0888a031215611971578283fd5b873561197c81611ee2565b9650602088013567ffffffffffffffff80821115611998578485fd5b6119a48b838c01611729565b975060408a013591506119b682611ee2565b909550606089013594506080890135906119cf82611ee2565b90935060a0890135925060c089013590808211156119eb578283fd5b506119f88a828b01611729565b91505092959891949750929550565b600060208284031215611a18578081fd5b5051919050565b60008151808452611a37816020860160208601611eb6565b601f01601f19169290920160200192915050565b6001600160a01b0391909116815260200190565b6001600160a01b038781168252868116602083015285166040820152606081018490526080810183905260c060a08201819052600090611aa190830184611a1f565b98975050505050505050565b6001600160a01b03929092168252602082015260400190565b600060018060a01b038516825283602083015260606040830152611aed6060830184611a1f565b95945050505050565b6001600160a01b0394909416845260208401929092526040830152606082015260800190565b60006020825261078e6020830184611a1f565b602080825260159082015274115490cc8c081d1c985b9cd9995c8819985a5b1959605a1b604082015260600190565b6020808252601d908201527f536c6970706167652067726561746572207468616e20616c6c6f776564000000604082015260600190565b60208082526028908201527f4d75737420626520612076616c696420616e6420696e697469616c697a65642060408201526729b2ba2a37b5b2b760c11b606082015260800190565b6020808252601e908201527f4d7573742062652070656e64696e6720696e697469616c697a6174696f6e0000604082015260600190565b60208082526015908201527426bab9ba103132903b30b634b21030b230b83a32b960591b604082015260600190565b6020808252818101527f53616665436173743a2076616c7565206d75737420626520706f736974697665604082015260600190565b60208082526021908201527f536166654d6174683a206d756c7469706c69636174696f6e206f766572666c6f6040820152607760f81b606082015260800190565b6020808252601d908201527f496e76616c696420706f7374207472616e736665722062616c616e6365000000604082015260600190565b60208082526022908201527f556e69742063616e742062652067726561746572207468616e206578697374696040820152616e6760f01b606082015260800190565b60208082526028908201527f53616665436173743a2076616c756520646f65736e27742066697420696e2061604082015267371034b73a191a9b60c11b606082015260800190565b60208082526023908201527f4d75737420626520636f6e74726f6c6c65722d656e61626c656420536574546f60408201526235b2b760e91b606082015260800190565b6020808252601c908201527f4d7573742062652074686520536574546f6b656e206d616e6167657200000000604082015260600190565b6020808252601f908201527f5265656e7472616e637947756172643a207265656e7472616e742063616c6c00604082015260600190565b6020808252601d908201527f546f6b656e20746f2073656c6c206d757374206265206e6f6e7a65726f000000604082015260600190565b90815260200190565b60405181810167ffffffffffffffff81118282101715611e8a57600080fd5b604052919050565b600067ffffffffffffffff821115611ea8578081fd5b50601f01601f191660200190565b60005b83811015611ed1578181015183820152602001611eb9565b8381111561116a5750506000910152565b6001600160a01b038116811461026e57600080fdfea2646970667358221220245402a4e3852af9f6b6995ba518e585c6b34345f493b78612912dab1a0f0bfd64736f6c634300060a0033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/external/contracts/set/TradeModule.sol b/external/contracts/set/TradeModule.sol new file mode 100644 index 00000000..c98919d8 --- /dev/null +++ b/external/contracts/set/TradeModule.sol @@ -0,0 +1,323 @@ +/* + 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 { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/SafeCast.sol"; + +import { IController } from "../../../interfaces/IController.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IExchangeAdapter } from "../../../interfaces/IExchangeAdapter.sol"; +import { IIntegrationRegistry } from "../../../interfaces/IIntegrationRegistry.sol"; +import { Invoke } from "../../lib/Invoke.sol"; +import { ISetToken } from "../../../interfaces/ISetToken.sol"; +import { ModuleBase } from "../../lib/ModuleBase.sol"; +import { Position } from "../../lib/Position.sol"; +import { PreciseUnitMath } from "../../../lib/PreciseUnitMath.sol"; + +/** + * @title TradeModule + * @author Set Protocol + * + * Module that enables SetTokens to perform atomic trades using Decentralized Exchanges + * such as 1inch or Kyber. Integrations mappings are stored on the IntegrationRegistry contract. + */ +contract TradeModule is ModuleBase, ReentrancyGuard { + using SafeCast for int256; + using SafeMath for uint256; + + using Invoke for ISetToken; + using Position for ISetToken; + using PreciseUnitMath for uint256; + + /* ============ Struct ============ */ + + struct TradeInfo { + ISetToken setToken; // Instance of SetToken + IExchangeAdapter exchangeAdapter; // Instance of exchange adapter contract + address sendToken; // Address of token being sold + address receiveToken; // Address of token being bought + uint256 setTotalSupply; // Total supply of SetToken in Precise Units (10^18) + uint256 totalSendQuantity; // Total quantity of sold token (position unit x total supply) + uint256 totalMinReceiveQuantity; // Total minimum quantity of token to receive back + uint256 preTradeSendTokenBalance; // Total initial balance of token being sold + uint256 preTradeReceiveTokenBalance; // Total initial balance of token being bought + } + + /* ============ Events ============ */ + + event ComponentExchanged( + ISetToken indexed _setToken, + address indexed _sendToken, + address indexed _receiveToken, + IExchangeAdapter _exchangeAdapter, + uint256 _totalSendAmount, + uint256 _totalReceiveAmount, + uint256 _protocolFee + ); + + /* ============ Constants ============ */ + + // 0 index stores the fee % charged in the trade function + uint256 constant internal TRADE_MODULE_PROTOCOL_FEE_INDEX = 0; + + /* ============ Constructor ============ */ + + constructor(IController _controller) public ModuleBase(_controller) {} + + /* ============ External Functions ============ */ + + /** + * Initializes this module to the SetToken. Only callable by the SetToken's manager. + * + * @param _setToken Instance of the SetToken to initialize + */ + function initialize( + ISetToken _setToken + ) + external + onlyValidAndPendingSet(_setToken) + onlySetManager(_setToken, msg.sender) + { + _setToken.initializeModule(); + } + + /** + * Executes a trade on a supported DEX. Only callable by the SetToken's manager. + * @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 + nonReentrant + onlyManagerAndValidSet(_setToken) + { + TradeInfo memory tradeInfo = _createTradeInfo( + _setToken, + _exchangeName, + _sendToken, + _receiveToken, + _sendQuantity, + _minReceiveQuantity + ); + + _validatePreTradeData(tradeInfo, _sendQuantity); + + _executeTrade(tradeInfo, _data); + + uint256 exchangedQuantity = _validatePostTrade(tradeInfo); + + uint256 protocolFee = _accrueProtocolFee(tradeInfo, exchangedQuantity); + + ( + uint256 netSendAmount, + uint256 netReceiveAmount + ) = _updateSetTokenPositions(tradeInfo); + + emit ComponentExchanged( + _setToken, + _sendToken, + _receiveToken, + tradeInfo.exchangeAdapter, + netSendAmount, + netReceiveAmount, + protocolFee + ); + } + + /** + * Removes this module from the SetToken, via call by the SetToken. Left with empty logic + * here because there are no check needed to verify removal. + */ + function removeModule() external override {} + + /* ============ Internal Functions ============ */ + + /** + * Create and return TradeInfo struct + * + * @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 _receiveToken Address of the token that will be received from the exchange + * @param _sendQuantity Units of token in SetToken sent to the exchange + * @param _minReceiveQuantity Min units of token in SetToken to be received from the exchange + * + * return TradeInfo Struct containing data for trade + */ + function _createTradeInfo( + ISetToken _setToken, + string memory _exchangeName, + address _sendToken, + address _receiveToken, + uint256 _sendQuantity, + uint256 _minReceiveQuantity + ) + internal + view + returns (TradeInfo memory) + { + TradeInfo memory tradeInfo; + + tradeInfo.setToken = _setToken; + + tradeInfo.exchangeAdapter = IExchangeAdapter(getAndValidateAdapter(_exchangeName)); + + tradeInfo.sendToken = _sendToken; + tradeInfo.receiveToken = _receiveToken; + + tradeInfo.setTotalSupply = _setToken.totalSupply(); + + tradeInfo.totalSendQuantity = Position.getDefaultTotalNotional(tradeInfo.setTotalSupply, _sendQuantity); + + tradeInfo.totalMinReceiveQuantity = Position.getDefaultTotalNotional(tradeInfo.setTotalSupply, _minReceiveQuantity); + + tradeInfo.preTradeSendTokenBalance = IERC20(_sendToken).balanceOf(address(_setToken)); + tradeInfo.preTradeReceiveTokenBalance = IERC20(_receiveToken).balanceOf(address(_setToken)); + + return tradeInfo; + } + + /** + * Validate pre trade data. Check exchange is valid, token quantity is valid. + * + * @param _tradeInfo Struct containing trade information used in internal functions + * @param _sendQuantity Units of token in SetToken sent to the exchange + */ + function _validatePreTradeData(TradeInfo memory _tradeInfo, uint256 _sendQuantity) internal view { + require(_tradeInfo.totalSendQuantity > 0, "Token to sell must be nonzero"); + + require( + _tradeInfo.setToken.hasSufficientDefaultUnits(_tradeInfo.sendToken, _sendQuantity), + "Unit cant be greater than existing" + ); + } + + /** + * Invoke approve for send token, get method data and invoke trade in the context of the SetToken. + * + * @param _tradeInfo Struct containing trade information used in internal functions + * @param _data Arbitrary bytes to be used to construct trade call data + */ + function _executeTrade( + TradeInfo memory _tradeInfo, + bytes memory _data + ) + internal + { + // Get spender address from exchange adapter and invoke approve for exact amount on SetToken + _tradeInfo.setToken.invokeApprove( + _tradeInfo.sendToken, + _tradeInfo.exchangeAdapter.getSpender(), + _tradeInfo.totalSendQuantity + ); + + ( + address targetExchange, + uint256 callValue, + bytes memory methodData + ) = _tradeInfo.exchangeAdapter.getTradeCalldata( + _tradeInfo.sendToken, + _tradeInfo.receiveToken, + address(_tradeInfo.setToken), + _tradeInfo.totalSendQuantity, + _tradeInfo.totalMinReceiveQuantity, + _data + ); + + _tradeInfo.setToken.invoke(targetExchange, callValue, methodData); + } + + /** + * Validate post trade data. + * + * @param _tradeInfo Struct containing trade information used in internal functions + * @return uint256 Total quantity of receive token that was exchanged + */ + function _validatePostTrade(TradeInfo memory _tradeInfo) internal view returns (uint256) { + uint256 exchangedQuantity = IERC20(_tradeInfo.receiveToken) + .balanceOf(address(_tradeInfo.setToken)) + .sub(_tradeInfo.preTradeReceiveTokenBalance); + + require( + exchangedQuantity >= _tradeInfo.totalMinReceiveQuantity, + "Slippage greater than allowed" + ); + + return exchangedQuantity; + } + + /** + * Retrieve fee from controller and calculate total protocol fee and send from SetToken to protocol recipient + * + * @param _tradeInfo Struct containing trade information used in internal functions + * @return uint256 Amount of receive token taken as protocol fee + */ + function _accrueProtocolFee(TradeInfo memory _tradeInfo, uint256 _exchangedQuantity) internal returns (uint256) { + uint256 protocolFeeTotal = getModuleFee(TRADE_MODULE_PROTOCOL_FEE_INDEX, _exchangedQuantity); + + payProtocolFeeFromSetToken(_tradeInfo.setToken, _tradeInfo.receiveToken, protocolFeeTotal); + + return protocolFeeTotal; + } + + /** + * Update SetToken positions + * + * @param _tradeInfo Struct containing trade information used in internal functions + * @return uint256 Amount of sendTokens used in the trade + * @return uint256 Amount of receiveTokens received in the trade (net of fees) + */ + function _updateSetTokenPositions(TradeInfo memory _tradeInfo) internal returns (uint256, uint256) { + (uint256 currentSendTokenBalance,,) = _tradeInfo.setToken.calculateAndEditDefaultPosition( + _tradeInfo.sendToken, + _tradeInfo.setTotalSupply, + _tradeInfo.preTradeSendTokenBalance + ); + + (uint256 currentReceiveTokenBalance,,) = _tradeInfo.setToken.calculateAndEditDefaultPosition( + _tradeInfo.receiveToken, + _tradeInfo.setTotalSupply, + _tradeInfo.preTradeReceiveTokenBalance + ); + + return ( + _tradeInfo.preTradeSendTokenBalance.sub(currentSendTokenBalance), + currentReceiveTokenBalance.sub(_tradeInfo.preTradeReceiveTokenBalance) + ); + } +} \ No newline at end of file diff --git a/test/global-extensions/globalBatchTradeExtension.spec.ts b/test/global-extensions/globalBatchTradeExtension.spec.ts new file mode 100644 index 00000000..08425fd8 --- /dev/null +++ b/test/global-extensions/globalBatchTradeExtension.spec.ts @@ -0,0 +1,808 @@ +import "module-alias/register"; + +import { BigNumber, Contract } from "ethers"; +import { Address, Account, TradeInfo } from "@utils/types"; +import { ADDRESS_ZERO, EMPTY_BYTES } from "@utils/constants"; +import { + DelegatedManager, + GlobalBatchTradeExtension, + ManagerCore, + BatchTradeAdapterMock +} from "@utils/contracts/index"; +import { + SetToken, + TradeModule +} from "@utils/contracts/setV2"; +import DeployHelper from "@utils/deploys"; +import { + addSnapshotBeforeRestoreAfterEach, + ether, + getAccounts, + getWaffleExpect, + getSetFixture, + getRandomAccount, +} from "@utils/index"; +import { ContractTransaction } from "ethers"; +import { getLastBlockTransaction } from "@utils/test/testingUtils"; +import { SetFixture } from "@utils/fixtures"; + +const expect = getWaffleExpect(); + +describe("BatchTradeExtension", () => { + let owner: Account; + let methodologist: Account; + let operator: Account; + let factory: Account; + + let deployer: DeployHelper; + let setToken: SetToken; + let setV2Setup: SetFixture; + + let tradeModule: TradeModule; + + let managerCore: ManagerCore; + let delegatedManager: DelegatedManager; + let batchTradeExtension: GlobalBatchTradeExtension; + + const tradeAdapterName = "TRADEMOCK"; + let tradeMock: BatchTradeAdapterMock; + + before(async () => { + [ + owner, + methodologist, + operator, + factory, + ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + + setV2Setup = getSetFixture(owner.address); + await setV2Setup.initialize(); + + tradeModule = await deployer.setV2.deployTradeModule(setV2Setup.controller.address); + await setV2Setup.controller.addModule(tradeModule.address); + + tradeMock = await deployer.mocks.deployBatchTradeAdapterMock(); + + await setV2Setup.integrationRegistry.addIntegration( + tradeModule.address, + tradeAdapterName, + tradeMock.address + ); + + managerCore = await deployer.managerCore.deployManagerCore(); + + batchTradeExtension = await deployer.globalExtensions.deployGlobalBatchTradeExtension( + managerCore.address, + tradeModule.address, + [tradeAdapterName] + ); + + 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, + [batchTradeExtension.address], + [operator.address], + [setV2Setup.dai.address, setV2Setup.weth.address, setV2Setup.wbtc.address], + true + ); + + await setToken.setManager(delegatedManager.address); + + await managerCore.initialize([batchTradeExtension.address], [factory.address]); + await managerCore.connect(factory.wallet).addManager(delegatedManager.address); + }); + + addSnapshotBeforeRestoreAfterEach(); + + describe("#constructor", async () => { + let subjectManagerCore: Address; + let subjectTradeModule: Address; + let subjectIntegrations: string[]; + + beforeEach(async () => { + subjectManagerCore = managerCore.address; + subjectTradeModule = tradeModule.address; + subjectIntegrations = [tradeAdapterName]; + }); + + async function subject(): Promise { + return await deployer.globalExtensions.deployGlobalBatchTradeExtension( + subjectManagerCore, + subjectTradeModule, + subjectIntegrations + ); + } + + it("should set the correct TradeModule address", async () => { + const batchTradeExtension = await subject(); + + const storedModule = await batchTradeExtension.tradeModule(); + expect(storedModule).to.eq(subjectTradeModule); + }); + + it("should have set the correct integrations length of 1", async () => { + const batchTradeExtension = await subject(); + + const integrations = await batchTradeExtension.getIntegrations(); + expect(integrations.length).to.eq(1); + }); + + it("should have a valid integration", async () => { + const batchTradeExtension = await subject(); + + const validIntegration = await batchTradeExtension.isIntegration(tradeAdapterName); + expect(validIntegration).to.eq(true); + }); + + it("should emit the IntegrationAdded event", async () => { + const batchTradeExtension = await subject(); + + await expect(getLastBlockTransaction()).to.emit(batchTradeExtension, "IntegrationAdded").withArgs(tradeAdapterName); + }); + }); + + describe("#addIntegrations", async () => { + let subjectIntegrations: string[]; + let subjectCaller: Account; + + beforeEach(async () => { + subjectIntegrations = ["Test1", "Test2"]; + subjectCaller = owner; + }); + + async function subject(): Promise { + return batchTradeExtension.connect(subjectCaller.wallet).addIntegrations(subjectIntegrations); + } + + it("should be stored in the integrations array", async () => { + await subject(); + + const integrations = await batchTradeExtension.getIntegrations(); + expect(integrations.length).to.eq(3); + }); + + it("should be returned as valid integrations", async () => { + await subject(); + + const validIntegrationOne = await batchTradeExtension.isIntegration("Test1"); + const validIntegrationTwo = await batchTradeExtension.isIntegration("Test2"); + expect(validIntegrationOne).to.eq(true); + expect(validIntegrationTwo).to.eq(true); + }); + + it("should emit the first IntegrationAdded event", async () => { + await expect(subject()).to.emit(batchTradeExtension, "IntegrationAdded").withArgs("Test1"); + }); + + it("should emit the second IntegrationAdded event", async () => { + await expect(subject()).to.emit(batchTradeExtension, "IntegrationAdded").withArgs("Test2"); + }); + + describe("when the sender is not the ManagerCore owner", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Caller must be ManagerCore owner"); + }); + }); + + describe("when the integration already exists", async () => { + beforeEach(async () => { + await subject(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Integration already exists"); + }); + }); + }); + + describe("#removeIntegrations", async () => { + let subjectIntegrations: string[]; + let subjectCaller: Account; + + beforeEach(async () => { + await batchTradeExtension.connect(owner.wallet).addIntegrations(["Test1", "Test2"]); + + subjectIntegrations = ["Test1", "Test2"]; + subjectCaller = owner; + }); + + async function subject(): Promise { + return batchTradeExtension.connect(subjectCaller.wallet).removeIntegrations(subjectIntegrations); + } + + it("should be remove integrations from the integrations array", async () => { + await subject(); + + const integrations = await batchTradeExtension.getIntegrations(); + expect(integrations.length).to.eq(1); + }); + + it("should return false as valid integrations", async () => { + await subject(); + + const validIntegrationOne = await batchTradeExtension.isIntegration("Test1"); + const validIntegrationTwo = await batchTradeExtension.isIntegration("Test2"); + expect(validIntegrationOne).to.eq(false); + expect(validIntegrationTwo).to.eq(false); + }); + + it("should emit the first IntegrationRemoved event", async () => { + await expect(subject()).to.emit(batchTradeExtension, "IntegrationRemoved").withArgs("Test1"); + }); + + it("should emit the second IntegrationRemoved event", async () => { + await expect(subject()).to.emit(batchTradeExtension, "IntegrationRemoved").withArgs("Test2"); + }); + + describe("when the sender is not the ManagerCore owner", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Caller must be ManagerCore owner"); + }); + }); + + describe("when the integration does not exist", async () => { + beforeEach(async () => { + await subject(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Integration does not exist"); + }); + }); + }); + + describe("#initializeModule", async () => { + let subjectDelegatedManager: Address; + let subjectCaller: Account; + + beforeEach(async () => { + await batchTradeExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + + subjectDelegatedManager = delegatedManager.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return batchTradeExtension.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([batchTradeExtension.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([batchTradeExtension.address]); + await batchTradeExtension.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([batchTradeExtension.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be initialized extension"); + }); + }); + + describe("when the extension is pending", async () => { + beforeEach(async () => { + await delegatedManager.connect(owner.wallet).removeExtensions([batchTradeExtension.address]); + await delegatedManager.connect(owner.wallet).addExtensions([batchTradeExtension.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be initialized extension"); + }); + }); + + 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 batchTradeExtension.connect(subjectCaller.wallet).initializeExtension(subjectDelegatedManager); + } + + it("should store the correct SetToken and DelegatedManager on the BatchTradeExtension", async () => { + await subject(); + + const storedDelegatedManager: Address = await batchTradeExtension.setManagers(setToken.address); + expect(storedDelegatedManager).to.eq(delegatedManager.address); + }); + + it("should initialize the BatchTradeExtension on the DelegatedManager", async () => { + await subject(); + + const isExtensionInitialized: Boolean = await delegatedManager.isInitializedExtension(batchTradeExtension.address); + expect(isExtensionInitialized).to.eq(true); + }); + + it("should emit the correct BatchTradeExtensionInitialized event", async () => { + await expect(subject()).to.emit(batchTradeExtension, "BatchTradeExtensionInitialized").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 batchTradeExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + await delegatedManager.connect(owner.wallet).removeExtensions([batchTradeExtension.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 batchTradeExtension.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 batchTradeExtension.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 BatchTradeExtension", async () => { + await subject(); + + const storedDelegatedManager: Address = await batchTradeExtension.setManagers(setToken.address); + expect(storedDelegatedManager).to.eq(delegatedManager.address); + }); + + it("should initialize the BatchTradeExtension on the DelegatedManager", async () => { + await subject(); + + const isExtensionInitialized: Boolean = await delegatedManager.isInitializedExtension(batchTradeExtension.address); + expect(isExtensionInitialized).to.eq(true); + }); + + it("should emit the correct BatchTradeExtensionInitialized event", async () => { + await expect(subject()).to.emit(batchTradeExtension, "BatchTradeExtensionInitialized").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 batchTradeExtension.connect(owner.wallet).initializeModuleAndExtension(delegatedManager.address); + await delegatedManager.connect(owner.wallet).removeExtensions([batchTradeExtension.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([batchTradeExtension.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 batchTradeExtension.connect(owner.wallet).initializeModuleAndExtension(delegatedManager.address); + await delegatedManager.connect(owner.wallet).removeExtensions([batchTradeExtension.address]); + await delegatedManager.connect(owner.wallet).addExtensions([batchTradeExtension.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 batchTradeExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + await delegatedManager.connect(owner.wallet).removeExtensions([batchTradeExtension.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 batchTradeExtension.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 subjectBatchTradeExtension: Address[]; + let subjectCaller: Account; + + beforeEach(async () => { + await batchTradeExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + + subjectManager = delegatedManager; + subjectBatchTradeExtension = [batchTradeExtension.address]; + subjectCaller = owner; + }); + + async function subject(): Promise { + return subjectManager.connect(subjectCaller.wallet).removeExtensions(subjectBatchTradeExtension); + } + + it("should clear SetToken and DelegatedManager from BatchTradeExtension state", async () => { + await subject(); + + const storedDelegatedManager: Address = await batchTradeExtension.setManagers(setToken.address); + expect(storedDelegatedManager).to.eq(ADDRESS_ZERO); + }); + + it("should emit the correct ExtensionRemoved event", async () => { + await expect(subject()).to.emit(batchTradeExtension, "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("#batchTrade", async () => { + let mintedTokens: BigNumber; + let subjectSetToken: Address; + let subjectTradeOne: TradeInfo; + let subjectTradeTwo: TradeInfo; + let subjectTrades: TradeInfo[]; + let subjectCaller: Account; + + beforeEach(async () => { + await batchTradeExtension.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 destinationTokens WBTC, WETH and DAI + await setV2Setup.wbtc.transfer(tradeMock.address, ether(1)); + await setV2Setup.weth.transfer(tradeMock.address, ether(10)); + await setV2Setup.dai.transfer(tradeMock.address, ether(10)); + + subjectCaller = operator; + subjectSetToken = setToken.address; + subjectTradeOne = { + exchangeName: tradeAdapterName, + sendToken: setV2Setup.dai.address, + sendQuantity: ether(0.6), + receiveToken: setV2Setup.weth.address, + receiveQuantity: ether(0), + data: EMPTY_BYTES, + } as TradeInfo; + subjectTradeTwo = { + exchangeName: tradeAdapterName, + sendToken: setV2Setup.dai.address, + sendQuantity: ether(0.4), + receiveToken: setV2Setup.wbtc.address, + receiveQuantity: ether(0), + data: EMPTY_BYTES, + } as TradeInfo; + subjectTrades = [subjectTradeOne, subjectTradeTwo]; + }); + + async function subject(): Promise { + return batchTradeExtension.connect(subjectCaller.wallet).batchTrade( + subjectSetToken, + subjectTrades + ); + } + + it("should successfully execute the trades", async () => { + const oldSendTokenBalance = await setV2Setup.dai.balanceOf(setToken.address); + const oldReceiveTokenOneBalance = await setV2Setup.weth.balanceOf(setToken.address); + const oldReceiveTokenTwoBalance = await setV2Setup.wbtc.balanceOf(setToken.address); + + await subject(); + + const expectedNewSendTokenBalance = oldSendTokenBalance.sub(ether(1.0)); + const actualNewSendTokenBalance = await setV2Setup.dai.balanceOf(setToken.address); + const expectedNewReceiveTokenOneBalance = oldReceiveTokenOneBalance.add(ether(10)); + const actualNewReceiveTokenOneBalance = await setV2Setup.weth.balanceOf(setToken.address); + const expectedNewReceiveTokenTwoBalance = oldReceiveTokenTwoBalance.add(ether(1)); + const actualNewReceiveTokenTwoBalance = await setV2Setup.wbtc.balanceOf(setToken.address); + + expect(expectedNewSendTokenBalance).to.eq(actualNewSendTokenBalance); + expect(expectedNewReceiveTokenOneBalance).to.eq(actualNewReceiveTokenOneBalance); + expect(expectedNewReceiveTokenTwoBalance).to.eq(actualNewReceiveTokenTwoBalance); + }); + + 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 a trade fails with a string error", async () => { + beforeEach(async () => { + subjectTradeTwo = { + exchangeName: tradeAdapterName, + sendToken: setV2Setup.dai.address, + sendQuantity: ether(0.4), + receiveToken: setV2Setup.wbtc.address, + receiveQuantity: ether(2), + data: EMPTY_BYTES, + } as TradeInfo; + subjectTrades = [subjectTradeOne, subjectTradeTwo]; + }); + + it("should perform the other trades", async () => { + const oldSendTokenBalance = await setV2Setup.dai.balanceOf(setToken.address); + const oldReceiveTokenOneBalance = await setV2Setup.weth.balanceOf(setToken.address); + const oldReceiveTokenTwoBalance = await setV2Setup.wbtc.balanceOf(setToken.address); + + await subject(); + + const expectedNewSendTokenBalance = oldSendTokenBalance.sub(ether(0.6)); + const actualNewSendTokenBalance = await setV2Setup.dai.balanceOf(setToken.address); + const expectedNewReceiveTokenOneBalance = oldReceiveTokenOneBalance.add(ether(10)); + const actualNewReceiveTokenOneBalance = await setV2Setup.weth.balanceOf(setToken.address); + const actualNewReceiveTokenTwoBalance = await setV2Setup.wbtc.balanceOf(setToken.address); + + expect(expectedNewSendTokenBalance).to.eq(actualNewSendTokenBalance); + expect(expectedNewReceiveTokenOneBalance).to.eq(actualNewReceiveTokenOneBalance); + expect(oldReceiveTokenTwoBalance).to.eq(actualNewReceiveTokenTwoBalance); + }); + + it("should emit the correct StringTradeFailed event", async () => { + await expect(subject()).to.emit(batchTradeExtension, "StringTradeFailed").withArgs( + setToken.address, + 1, + "Insufficient funds in exchange", + [ + tradeAdapterName, + setV2Setup.dai.address, + ether(0.4), + setV2Setup.wbtc.address, + ether(2), + EMPTY_BYTES, + ] + ); + }); + }); + + describe("when a trade fails with a byte error", async () => { + beforeEach(async () => { + subjectTradeTwo = { + exchangeName: tradeAdapterName, + sendToken: setV2Setup.dai.address, + sendQuantity: ether(0.4), + receiveToken: setV2Setup.wbtc.address, + receiveQuantity: BigNumber.from(1), + data: EMPTY_BYTES, + } as TradeInfo; + subjectTrades = [subjectTradeOne, subjectTradeTwo]; + }); + + it("should perform the other trades", async () => { + const oldSendTokenBalance = await setV2Setup.dai.balanceOf(setToken.address); + const oldReceiveTokenOneBalance = await setV2Setup.weth.balanceOf(setToken.address); + const oldReceiveTokenTwoBalance = await setV2Setup.wbtc.balanceOf(setToken.address); + + await subject(); + + const expectedNewSendTokenBalance = oldSendTokenBalance.sub(ether(0.6)); + const actualNewSendTokenBalance = await setV2Setup.dai.balanceOf(setToken.address); + const expectedNewReceiveTokenOneBalance = oldReceiveTokenOneBalance.add(ether(10)); + const actualNewReceiveTokenOneBalance = await setV2Setup.weth.balanceOf(setToken.address); + const actualNewReceiveTokenTwoBalance = await setV2Setup.wbtc.balanceOf(setToken.address); + + expect(expectedNewSendTokenBalance).to.eq(actualNewSendTokenBalance); + expect(expectedNewReceiveTokenOneBalance).to.eq(actualNewReceiveTokenOneBalance); + expect(oldReceiveTokenTwoBalance).to.eq(actualNewReceiveTokenTwoBalance); + }); + + it("should emit the correct BytesTradeFailed event", async () => { + const expectedByteError = tradeMock.interface.encodeFunctionData("trade", [ + setV2Setup.dai.address, + setV2Setup.wbtc.address, + setToken.address, + ether(0.4), + BigNumber.from(1), + ]); + + await expect(subject()).to.emit(batchTradeExtension, "BytesTradeFailed").withArgs( + setToken.address, + 1, + expectedByteError, + [ + tradeAdapterName, + setV2Setup.dai.address, + ether(0.4), + setV2Setup.wbtc.address, + BigNumber.from(1), + EMPTY_BYTES, + ] + ); + }); + }); + + describe("when a exchangeName is not an allowed integration", async () => { + beforeEach(async () => { + subjectTradeTwo = { + exchangeName: "ZeroExApiAdapter", + sendToken: setV2Setup.dai.address, + sendQuantity: ether(0.4), + receiveToken: setV2Setup.wbtc.address, + receiveQuantity: ether(0), + data: EMPTY_BYTES, + } as TradeInfo; + subjectTrades = [subjectTradeOne, subjectTradeTwo]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be allowed integration"); + }); + }); + + describe("when a receiveToken is not an allowed asset", async () => { + beforeEach(async () => { + subjectTradeTwo = { + exchangeName: tradeAdapterName, + sendToken: setV2Setup.dai.address, + sendQuantity: ether(0.4), + receiveToken: setV2Setup.usdc.address, + receiveQuantity: ether(0), + data: EMPTY_BYTES, + } as TradeInfo; + subjectTrades = [subjectTradeOne, subjectTradeTwo]; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be allowed asset"); + }); + }); + }); +}); diff --git a/utils/contracts/index.ts b/utils/contracts/index.ts index 19e012b1..ad74c8f1 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -41,6 +41,7 @@ export { StringArrayUtilsMock } from "../../typechain/StringArrayUtilsMock"; export { SupplyCapAllowedCallerIssuanceHook } from "../../typechain/SupplyCapAllowedCallerIssuanceHook"; export { SupplyCapIssuanceHook } from "../../typechain/SupplyCapIssuanceHook"; export { TradeAdapterMock } from "../../typechain/TradeAdapterMock"; +export { BatchTradeAdapterMock } from "../../typechain/BatchTradeAdapterMock"; export { Vesting } from "../../typechain/Vesting"; export { WETH9 } from "../../typechain/WETH9"; export { WrapAdapterMock } from "../../typechain/WrapAdapterMock"; diff --git a/utils/contracts/setV2.ts b/utils/contracts/setV2.ts index 0380dbc0..58079195 100644 --- a/utils/contracts/setV2.ts +++ b/utils/contracts/setV2.ts @@ -23,3 +23,4 @@ export { UniswapV2ExchangeAdapter } from "../../typechain/UniswapV2ExchangeAdapt export { WrapModule } from "../../typechain/WrapModule"; export { ClaimModule } from "../../typechain/ClaimModule"; export { ClaimAdapterMock } from "../../typechain/ClaimAdapterMock"; +export { TradeModule } from "../../typechain/TradeModule"; diff --git a/utils/deploys/deployMocks.ts b/utils/deploys/deployMocks.ts index 39873223..966c8bb7 100644 --- a/utils/deploys/deployMocks.ts +++ b/utils/deploys/deployMocks.ts @@ -10,6 +10,7 @@ import { StandardTokenMock, StringArrayUtilsMock, ManagerMock, + BatchTradeAdapterMock, TradeAdapterMock, WrapAdapterMock, WrappedfCashMock, @@ -35,6 +36,7 @@ import { GovernanceAdapterMock__factory } from "../../typechain/factories/Govern import { MasterChefMock__factory } from "../../typechain/factories/MasterChefMock__factory"; import { MutualUpgradeMock__factory } from "../../typechain/factories/MutualUpgradeMock__factory"; import { NotionalTradeModuleMock__factory } from "../../typechain/factories/NotionalTradeModuleMock__factory"; +import { BatchTradeAdapterMock__factory } from "../../typechain/factories/BatchTradeAdapterMock__factory"; import { TradeAdapterMock__factory } from "../../typechain/factories/TradeAdapterMock__factory"; import { StandardTokenMock__factory } from "../../typechain/factories/StandardTokenMock__factory"; import { StringArrayUtilsMock__factory } from "../../typechain/factories/StringArrayUtilsMock__factory"; @@ -66,6 +68,10 @@ export default class DeployMocks { return await new TradeAdapterMock__factory(this._deployerSigner).deploy(); } + public async deployBatchTradeAdapterMock(): Promise { + return await new BatchTradeAdapterMock__factory(this._deployerSigner).deploy(); + } + public async deployGovernanceAdapterMock( initialProposal: BigNumber, ): Promise { diff --git a/utils/deploys/deploySetV2.ts b/utils/deploys/deploySetV2.ts index 87a88b70..617ac3aa 100644 --- a/utils/deploys/deploySetV2.ts +++ b/utils/deploys/deploySetV2.ts @@ -22,6 +22,7 @@ import { IntegrationRegistry, StreamingFeeModule, SetToken, + TradeModule, SetTokenCreator, SingleIndexModule, UniswapV2ExchangeAdapter, @@ -41,6 +42,7 @@ import { AaveV2__factory } from "../../typechain/factories/AaveV2__factory"; import { AirdropModule__factory } from "../../typechain/factories/AirdropModule__factory"; import { AuctionRebalanceModuleV1__factory } from "../../typechain/factories/AuctionRebalanceModuleV1__factory"; import { BasicIssuanceModule__factory } from "../../typechain/factories/BasicIssuanceModule__factory"; +import { TradeModule__factory } from "../../typechain/factories/TradeModule__factory"; import { Controller__factory } from "../../typechain/factories/Controller__factory"; import { ConstantPriceAdapter__factory } from "../../typechain/factories/ConstantPriceAdapter__factory"; import { Compound__factory } from "../../typechain/factories/Compound__factory"; @@ -303,4 +305,8 @@ export default class DeploySetV2 { public async deployClaimModule(controller: Address): Promise { return await new ClaimModule__factory(this._deployerSigner).deploy(controller); } + + public async deployTradeModule(controller: Address): Promise { + return await new TradeModule__factory(this._deployerSigner).deploy(controller); + } } From 2234c7bbd936ba5e151fad4507303c285cd0f8a4 Mon Sep 17 00:00:00 2001 From: Pranav Bhardwaj Date: Sat, 26 Aug 2023 20:54:32 -0400 Subject: [PATCH 06/10] add issuance extension tests --- .../globalIssuanceExtension.spec.ts | 729 ++++++++++++++++++ utils/contracts/index.ts | 1 + utils/contracts/setV2.ts | 1 + utils/deploys/deploySetV2.ts | 6 + 4 files changed, 737 insertions(+) create mode 100644 test/global-extensions/globalIssuanceExtension.spec.ts diff --git a/test/global-extensions/globalIssuanceExtension.spec.ts b/test/global-extensions/globalIssuanceExtension.spec.ts new file mode 100644 index 00000000..ecca337e --- /dev/null +++ b/test/global-extensions/globalIssuanceExtension.spec.ts @@ -0,0 +1,729 @@ +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, + GlobalIssuanceExtension, + ManagerCore +} from "@utils/contracts/index"; +import { SetToken, DebtIssuanceModuleV2 } from "@utils/contracts/setV2"; +import DeployHelper from "@utils/deploys"; +import { + addSnapshotBeforeRestoreAfterEach, + ether, + getAccounts, + getWaffleExpect, + preciseMul, + getSetFixture, + getRandomAccount, +} from "@utils/index"; +import { SetFixture } from "@utils/fixtures"; + +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: SetFixture; + + let issuanceModule: DebtIssuanceModuleV2; + + let managerCore: ManagerCore; + let delegatedManager: DelegatedManager; + let issuanceExtension: GlobalIssuanceExtension; + + 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 = getSetFixture(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.deployGlobalIssuanceExtension( + 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.connect(owner.wallet).updateOwnerFeeSplit(ownerFeeSplit); + await delegatedManager.connect(methodologist.wallet).updateOwnerFeeSplit(ownerFeeSplit); + ownerFeeRecipient = owner.address; + await delegatedManager.connect(owner.wallet).updateOwnerFeeRecipient(ownerFeeRecipient); + + await setToken.setManager(delegatedManager.address); + + await managerCore.initialize([issuanceExtension.address], [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.deployGlobalIssuanceExtension( + 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)); + await delegatedManager.connect(methodologist.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); + await delegatedManager.connect(methodologist.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); + }); + }); + }); +}); diff --git a/utils/contracts/index.ts b/utils/contracts/index.ts index ad74c8f1..94082263 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -8,6 +8,7 @@ export { BaseManagerV2 } from "../../typechain/BaseManagerV2"; export { ChainlinkAggregatorV3Mock } from "../../typechain/ChainlinkAggregatorV3Mock"; export { ConstantPriceAdapter } from "../../typechain/ConstantPriceAdapter"; export { DebtIssuanceModule } from "../../typechain/DebtIssuanceModule"; +export { DebtIssuanceModuleV2 } from "../../typechain/DebtIssuanceModuleV2"; export { BasicIssuanceModule } from "../../typechain/BasicIssuanceModule"; export { DEXAdapter } from "../../typechain/DEXAdapter"; export { ExchangeIssuance } from "../../typechain/ExchangeIssuance"; diff --git a/utils/contracts/setV2.ts b/utils/contracts/setV2.ts index 58079195..f736132f 100644 --- a/utils/contracts/setV2.ts +++ b/utils/contracts/setV2.ts @@ -11,6 +11,7 @@ export { ConstantPriceAdapter } from "../../typechain/ConstantPriceAdapter"; export { ComptrollerMock } from "../../typechain/ComptrollerMock"; export { CompoundLeverageModule } from "../../typechain/CompoundLeverageModule"; export { DebtIssuanceModule } from "../../typechain/DebtIssuanceModule"; +export { DebtIssuanceModuleV2 } from "../../typechain/DebtIssuanceModuleV2"; export { GeneralIndexModule } from "../../typechain/GeneralIndexModule"; export { SlippageIssuanceModule } from "../../typechain/SlippageIssuanceModule"; export { GovernanceModule } from "../../typechain/GovernanceModule"; diff --git a/utils/deploys/deploySetV2.ts b/utils/deploys/deploySetV2.ts index 617ac3aa..7cc41183 100644 --- a/utils/deploys/deploySetV2.ts +++ b/utils/deploys/deploySetV2.ts @@ -17,6 +17,7 @@ import { ClaimAdapterMock, ClaimModule, DebtIssuanceModule, + DebtIssuanceModuleV2, GeneralIndexModule, GovernanceModule, IntegrationRegistry, @@ -52,6 +53,7 @@ import { ContractCallerMock__factory } from "../../typechain/factories/ContractC import { ClaimAdapterMock__factory } from "../../typechain/factories/ClaimAdapterMock__factory"; import { ClaimModule__factory } from "../../typechain/factories/ClaimModule__factory"; import { DebtIssuanceModule__factory } from "../../typechain/factories/DebtIssuanceModule__factory"; +import { DebtIssuanceModuleV2__factory } from "../../typechain/factories/DebtIssuanceModuleV2__factory"; import { GeneralIndexModule__factory } from "../../typechain/factories/GeneralIndexModule__factory"; import { GovernanceModule__factory } from "../../typechain/factories/GovernanceModule__factory"; import { IntegrationRegistry__factory } from "../../typechain/factories/IntegrationRegistry__factory"; @@ -133,6 +135,10 @@ export default class DeploySetV2 { return await new DebtIssuanceModule__factory(this._deployerSigner).deploy(controller); } + public async deployDebtIssuanceModuleV2(controller: Address): Promise { + return await new DebtIssuanceModuleV2__factory(this._deployerSigner).deploy(controller); + } + public async deployStreamingFeeModule(controller: Address): Promise { return await new StreamingFeeModule__factory(this._deployerSigner).deploy(controller); } From 6acb3a314963a02a9624301a940b4d868e195399 Mon Sep 17 00:00:00 2001 From: Pranav Bhardwaj Date: Sat, 26 Aug 2023 21:02:39 -0400 Subject: [PATCH 07/10] add streaming fee split extension tests --- .../globalStreamingFeeSplitExtension.spec.ts | 663 ++++++++++++++++++ 1 file changed, 663 insertions(+) create mode 100644 test/global-extensions/globalStreamingFeeSplitExtension.spec.ts diff --git a/test/global-extensions/globalStreamingFeeSplitExtension.spec.ts b/test/global-extensions/globalStreamingFeeSplitExtension.spec.ts new file mode 100644 index 00000000..e6a69faa --- /dev/null +++ b/test/global-extensions/globalStreamingFeeSplitExtension.spec.ts @@ -0,0 +1,663 @@ +import "module-alias/register"; + +import { + BigNumber, + Contract, + ContractTransaction +} from "ethers"; +import { + Address, + Account, + StreamingFeeState +} from "@utils/types"; +import { ADDRESS_ZERO, ONE_YEAR_IN_SECONDS, ZERO } from "@utils/constants"; +import { + DelegatedManager, + GlobalStreamingFeeSplitExtension, + ManagerCore +} from "@utils/contracts/index"; +import { SetToken } from "@utils/contracts/setV2"; +import DeployHelper from "@utils/deploys"; +import { + addSnapshotBeforeRestoreAfterEach, + ether, + getAccounts, + getWaffleExpect, + increaseTimeAsync, + preciseMul, + getTransactionTimestamp, + getSetFixture, + getRandomAccount +} from "@utils/index"; +import { getStreamingFee, getStreamingFeeInflationAmount } from "@utils/common"; +import { SetFixture } from "@utils/fixtures"; + +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: SetFixture; + + let managerCore: ManagerCore; + let delegatedManager: DelegatedManager; + let streamingFeeSplitExtension: GlobalStreamingFeeSplitExtension; + + 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 = getSetFixture(owner.address); + await setV2Setup.initialize(); + + managerCore = await deployer.managerCore.deployManagerCore(); + + streamingFeeSplitExtension = await deployer.globalExtensions.deployGlobalStreamingFeeSplitExtension( + 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.connect(owner.wallet).updateOwnerFeeSplit(ownerFeeSplit); + await delegatedManager.connect(methodologist.wallet).updateOwnerFeeSplit(ownerFeeSplit); + ownerFeeRecipient = owner.address; + await delegatedManager.connect(owner.wallet).updateOwnerFeeRecipient(ownerFeeRecipient); + + await setToken.setManager(delegatedManager.address); + + await managerCore.initialize([streamingFeeSplitExtension.address], [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.deployGlobalStreamingFeeSplitExtension( + 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)); + await delegatedManager.connect(methodologist.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); + await delegatedManager.connect(methodologist.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); + }); + }); + }); +}); From 0737ba9d5016ddd996ff0efeea53f667383ceac0 Mon Sep 17 00:00:00 2001 From: Pranav Bhardwaj Date: Fri, 8 Sep 2023 14:48:14 -0400 Subject: [PATCH 08/10] add wrap extension tests --- contracts/mocks/WrapV2AdapterMock.sol | 121 +++ external/abi/set/WrapModuleV2.json | 309 +++++++ external/contracts/set/WrapModuleV2.sol | 528 ++++++++++++ .../globalWrapExtension.spec.ts | 809 ++++++++++++++++++ utils/contracts/setV2.ts | 2 + utils/deploys/deploySetV2.ts | 12 + 6 files changed, 1781 insertions(+) create mode 100644 contracts/mocks/WrapV2AdapterMock.sol create mode 100644 external/abi/set/WrapModuleV2.json create mode 100644 external/contracts/set/WrapModuleV2.sol create mode 100644 test/global-extensions/globalWrapExtension.spec.ts diff --git a/contracts/mocks/WrapV2AdapterMock.sol b/contracts/mocks/WrapV2AdapterMock.sol new file mode 100644 index 00000000..5b5eb87b --- /dev/null +++ b/contracts/mocks/WrapV2AdapterMock.sol @@ -0,0 +1,121 @@ +/* + 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 { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + + +/** + * @title WrapV2AdapterMock + * @author Set Protocol + * + * ERC20 contract that doubles as a wrap token. The wrapToken accepts any underlying token and + * mints/burns the WrapAdapter Token. + */ +contract WrapV2AdapterMock is ERC20 { + + address public constant ETH_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + /* ============ Constructor ============ */ + constructor() public ERC20("WrapV2Adapter", "WRAPV2") {} + + /* ============ External Functions ============ */ + + /** + * Mints tokens to the sender of the underlying quantity + */ + function deposit(address _underlyingToken, uint256 _underlyingQuantity) payable external { + // Do a transferFrom of the underlyingToken + if (_underlyingToken != ETH_TOKEN_ADDRESS) { + IERC20(_underlyingToken).transferFrom(msg.sender, address(this), _underlyingQuantity); + } + + _mint(msg.sender, _underlyingQuantity); + } + + /** + * Burns tokens from the sender of the wrapped asset and returns the underlying + */ + function withdraw(address _underlyingToken, uint256 _underlyingQuantity) external { + // Transfer the underlying to the sender + if (_underlyingToken == ETH_TOKEN_ADDRESS) { + msg.sender.transfer(_underlyingQuantity); + } else { + IERC20(_underlyingToken).transfer(msg.sender, _underlyingQuantity); + } + + _burn(msg.sender, _underlyingQuantity); + } + + /** + * Generates the calldata to wrap an underlying asset into a wrappedToken. + * + * @param _underlyingToken Address of the component to be wrapped + * @param _underlyingUnits Total quantity of underlying units to wrap + * + * @return _subject Target contract address + * @return _value Total quantity of underlying units (if underlying is ETH) + * @return _calldata Wrap calldata + */ + function getWrapCallData( + address _underlyingToken, + address /* _wrappedToken */, + uint256 _underlyingUnits, + address /* _to */, + bytes memory /* _wrapData */ + ) external view returns (address _subject, uint256 _value, bytes memory _calldata) { + uint256 value = _underlyingToken == ETH_TOKEN_ADDRESS ? _underlyingUnits : 0; + bytes memory callData = abi.encodeWithSignature("deposit(address,uint256)", _underlyingToken, _underlyingUnits); + return (address(this), value, callData); + } + + /** + * Generates the calldata to unwrap a wrapped asset into its underlying. + * + * @param _underlyingToken Address of the underlying of the component to be unwrapped + * @param _wrappedTokenUnits Total quantity of wrapped token units to unwrap + * + * @return _subject Target contract address + * @return _value Total quantity of wrapped token units to unwrap. This will always be 0 for unwrapping + * @return _calldata Unwrap calldata + */ + function getUnwrapCallData( + address _underlyingToken, + address /* _wrappedToken */, + uint256 _wrappedTokenUnits, + address /* _to */, + bytes memory /* _wrapData */ + ) external view returns (address _subject, uint256 _value, bytes memory _calldata) { + bytes memory callData = abi.encodeWithSignature("withdraw(address,uint256)", _underlyingToken, _wrappedTokenUnits); + return (address(this), 0, callData); + } + + /** + * Returns the address to approve source tokens for wrapping. + * + * @return address Address of the contract to approve tokens to + */ + function getSpenderAddress( + address /* _underlyingToken */, + address /* _wrappedToken */ + ) external view returns(address) { + return address(this); + } +} diff --git a/external/abi/set/WrapModuleV2.json b/external/abi/set/WrapModuleV2.json new file mode 100644 index 00000000..50edff5b --- /dev/null +++ b/external/abi/set/WrapModuleV2.json @@ -0,0 +1,309 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "WrapModuleV2", + "sourceName": "contracts/protocol/modules/v1/WrapModuleV2.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "contract IController", + "name": "_controller", + "type": "address" + }, + { + "internalType": "contract IWETH", + "name": "_weth", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "_underlyingToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "_wrappedToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_underlyingQuantity", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_wrappedQuantity", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "_integrationName", + "type": "string" + } + ], + "name": "ComponentUnwrapped", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "_underlyingToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "_wrappedToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_underlyingQuantity", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_wrappedQuantity", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "_integrationName", + "type": "string" + } + ], + "name": "ComponentWrapped", + "type": "event" + }, + { + "inputs": [], + "name": "controller", + "outputs": [ + { + "internalType": "contract IController", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [], + "name": "removeModule", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_underlyingToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_wrappedToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_wrappedUnits", + "type": "uint256" + }, + { + "internalType": "string", + "name": "_integrationName", + "type": "string" + }, + { + "internalType": "bytes", + "name": "_unwrapData", + "type": "bytes" + } + ], + "name": "unwrap", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_wrappedToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_wrappedUnits", + "type": "uint256" + }, + { + "internalType": "string", + "name": "_integrationName", + "type": "string" + }, + { + "internalType": "bytes", + "name": "_unwrapData", + "type": "bytes" + } + ], + "name": "unwrapWithEther", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [], + "name": "weth", + "outputs": [ + { + "internalType": "contract IWETH", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_underlyingToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_wrappedToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_underlyingUnits", + "type": "uint256" + }, + { + "internalType": "string", + "name": "_integrationName", + "type": "string" + }, + { + "internalType": "bytes", + "name": "_wrapData", + "type": "bytes" + } + ], + "name": "wrap", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "contract ISetToken", + "name": "_setToken", + "type": "address" + }, + { + "internalType": "address", + "name": "_wrappedToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_underlyingUnits", + "type": "uint256" + }, + { + "internalType": "string", + "name": "_integrationName", + "type": "string" + }, + { + "internalType": "bytes", + "name": "_wrapData", + "type": "bytes" + } + ], + "name": "wrapWithEther", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + "gas": "0xa7d8c0" + } + ], + "bytecode": "0x60806040523480156200001157600080fd5b506040516200221d3803806200221d83398101604081905262000034916200006a565b600080546001600160a01b039384166001600160a01b0319918216179091556001805560028054929093169116179055620000c1565b600080604083850312156200007d578182fd5b82516200008a81620000a8565b60208401519092506200009d81620000a8565b809150509250929050565b6001600160a01b0381168114620000be57600080fd5b50565b61214c80620000d16000396000f3fe608060405234801561001057600080fd5b50600436106100885760003560e01c80638c72ef231161005b5780638c72ef23146100db578063aa0770bc146100ee578063c4d66de814610101578063f77c47911461011457610088565b806310ac16d71461008d5780633231b2d7146100a25780633fc8cef3146100b5578063847ef08d146100d3575b600080fd5b6100a061009b366004611baf565b61011c565b005b6100a06100b0366004611b07565b6101e6565b6100bd61029b565b6040516100ca9190611c87565b60405180910390f35b6100a06102aa565b6100a06100e9366004611baf565b6102ac565b6100a06100fc366004611b07565b610356565b6100a061010f366004611aeb565b6103f3565b6100bd610518565b600260015414156101485760405162461bcd60e51b815260040161013f90612009565b60405180910390fd5b60026001558561015781610527565b600254600090819061017c90879087908c906001600160a01b03168c8c8a6001610575565b6002546040519294509092506001600160a01b03808b169291811691908c16907f266efe8e5e4e2e7e407c4814a2818ef8e990768c61e67315ac34a8d3555b438e906101cf90879087908d908d90612049565b60405180910390a450506001805550505050505050565b600260015414156102095760405162461bcd60e51b815260040161013f90612009565b60026001558661021881610527565b60008061022c86868c8c8c8c8a6000610575565b91509150876001600160a01b0316896001600160a01b03168b6001600160a01b03167f266efe8e5e4e2e7e407c4814a2818ef8e990768c61e67315ac34a8d3555b438e85858b8b6040516102839493929190612049565b60405180910390a45050600180555050505050505050565b6002546001600160a01b031681565b565b600260015414156102cf5760405162461bcd60e51b815260040161013f90612009565b6002600155856102de81610527565b600254600090819061030390879087908c906001600160a01b03168c8c8a600161080c565b6002546040519294509092506001600160a01b03808b169291811691908c16907f0e631fe8e26e2b6c2ce8c4c55eca1d769c98bb4b5539068aec9ada0a3b429afe906101cf90879087908d908d90612049565b600260015414156103795760405162461bcd60e51b815260040161013f90612009565b60026001558661038881610527565b60008061039c86868c8c8c8c8a600061080c565b91509150876001600160a01b0316896001600160a01b03168b6001600160a01b03167f0e631fe8e26e2b6c2ce8c4c55eca1d769c98bb4b5539068aec9ada0a3b429afe85858b8b6040516102839493929190612049565b80336103ff82826109dc565b600054604051631d3af8fb60e21b81526001600160a01b03909116906374ebe3ec9061042f908690600401611c87565b60206040518083038186803b15801561044757600080fd5b505afa15801561045b573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061047f9190611a90565b61049b5760405162461bcd60e51b815260040161013f90611f8f565b6104a483610a06565b6104c05760405162461bcd60e51b815260040161013f90611de8565b826001600160a01b0316630ffe0f1e6040518163ffffffff1660e01b8152600401600060405180830381600087803b1580156104fb57600080fd5b505af115801561050f573d6000803e3d6000fd5b50505050505050565b6000546001600160a01b031681565b6105318133610a8b565b61054d5760405162461bcd60e51b815260040161013f90611fd2565b61055681610b19565b6105725760405162461bcd60e51b815260040161013f90611da0565b50565b600080610583888887610bcd565b6000806105918a8a8a610c5d565b915091506000610618888c6001600160a01b03166318160ddd6040518163ffffffff1660e01b815260040160206040518083038186803b1580156105d457600080fd5b505afa1580156105e8573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061060c9190611c43565b9063ffffffff610d6c16565b9050600061065b8e8e8080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250610d8592505050565b9050861561068857600254610683906001600160a01b038e811691168463ffffffff610d9c16565b610722565b6107228b826001600160a01b031663de68a3da8e8e6040518363ffffffff1660e01b81526004016106ba929190611c9b565b60206040518083038186803b1580156106d257600080fd5b505afa1580156106e6573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061070a9190611971565b6001600160a01b038f1691908563ffffffff610e6816565b6107ab8c8289610732578d6107a3565b836001600160a01b0316631878d1f16040518163ffffffff1660e01b815260040160206040518083038186803b15801561076b57600080fd5b505afa15801561077f573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906107a39190611971565b8d868d610f37565b6000806107b98e8e8e610c5d565b915091506107c98e8e888561105c565b6107d58e8d878461105c565b6107e5868363ffffffff61118216565b6107f5828763ffffffff61118216565b975097505050505050509850989650505050505050565b60008061081a888787610bcd565b6000806108288a8a8a610c5d565b91509150600061086b888c6001600160a01b03166318160ddd6040518163ffffffff1660e01b815260040160206040518083038186803b1580156105d457600080fd5b905060006108ae8e8e8080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250610d8592505050565b90506108e28a826001600160a01b031663de68a3da8e8e6040518363ffffffff1660e01b81526004016106ba929190611c9b565b61096b8c82896108f2578d610963565b836001600160a01b0316631878d1f16040518163ffffffff1660e01b815260040160206040518083038186803b15801561092b57600080fd5b505afa15801561093f573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109639190611971565b8d868d6111c4565b861561099257600254610992906001600160a01b038e81169116813163ffffffff6111fd16565b6000806109a08e8e8e610c5d565b915091506109b08e8e888561105c565b6109bc8e8d878461105c565b6109cc828763ffffffff61118216565b6107f5868363ffffffff61118216565b6109e68282610a8b565b610a025760405162461bcd60e51b815260040161013f90611fd2565b5050565b6040516353bae5f760e01b81526000906001600160a01b038316906353bae5f790610a35903090600401611c87565b60206040518083038186803b158015610a4d57600080fd5b505afa158015610a61573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610a859190611a90565b92915050565b6000816001600160a01b0316836001600160a01b031663481c6a756040518163ffffffff1660e01b815260040160206040518083038186803b158015610ad057600080fd5b505afa158015610ae4573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610b089190611971565b6001600160a01b0316149392505050565b60008054604051631d3af8fb60e21b81526001600160a01b03909116906374ebe3ec90610b4a908590600401611c87565b60206040518083038186803b158015610b6257600080fd5b505afa158015610b76573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610b9a9190611a90565b8015610a8557506040516335fc6c9f60e21b81526001600160a01b0383169063d7f1b27c90610a35903090600401611c87565b60008111610bed5760405162461bcd60e51b815260040161013f90611e4e565b610c066001600160a01b0384168363ffffffff61125516565b610c225760405162461bcd60e51b815260040161013f90611d57565b610c3c6001600160a01b038416838363ffffffff6112dc16565b610c585760405162461bcd60e51b815260040161013f90611f05565b505050565b6000806000846001600160a01b03166370a08231876040518263ffffffff1660e01b8152600401610c8e9190611c87565b60206040518083038186803b158015610ca657600080fd5b505afa158015610cba573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610cde9190611c43565b90506000846001600160a01b03166370a08231886040518263ffffffff1660e01b8152600401610d0e9190611c87565b60206040518083038186803b158015610d2657600080fd5b505afa158015610d3a573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610d5e9190611c43565b919791965090945050505050565b6000610d7e838363ffffffff61136d16565b9392505050565b600080610d9183611397565b9050610d7e816113a2565b606081604051602401610daf9190612040565b60408051601f198184030181529181526020820180516001600160e01b0316632e1a7d4d60e01b179052516347b7819960e11b81529091506001600160a01b03851690638f6f033290610e0b9086906000908690600401611d14565b600060405180830381600087803b158015610e2557600080fd5b505af1158015610e39573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052610e619190810190611ab0565b5050505050565b60608282604051602401610e7d929190611cfb565b60408051601f198184030181529181526020820180516001600160e01b031663095ea7b360e01b179052516347b7819960e11b81529091506001600160a01b03861690638f6f033290610ed99087906000908690600401611d14565b600060405180830381600087803b158015610ef357600080fd5b505af1158015610f07573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052610f2f9190810190611ab0565b505050505050565b6000806060876001600160a01b031663d91462ca8888888d896040518663ffffffff1660e01b8152600401610f70959493929190611cb5565b60006040518083038186803b158015610f8857600080fd5b505afa158015610f9c573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052610fc4919081019061198d565b925092509250886001600160a01b0316638f6f03328484846040518463ffffffff1660e01b8152600401610ffa93929190611d14565b600060405180830381600087803b15801561101457600080fd5b505af1158015611028573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526110509190810190611ab0565b50505050505050505050565b600061116683836110e7886001600160a01b03166366cb8d2f896040518263ffffffff1660e01b81526004016110929190611c87565b60206040518083038186803b1580156110aa57600080fd5b505afa1580156110be573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906110e29190611c43565b61145f565b886001600160a01b03166318160ddd6040518163ffffffff1660e01b815260040160206040518083038186803b15801561112057600080fd5b505afa158015611134573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906111589190611c43565b92919063ffffffff61148516565b9050610e616001600160a01b038616858363ffffffff6114d416565b6000610d7e83836040518060400160405280601e81526020017f536166654d6174683a207375627472616374696f6e206f766572666c6f770000815250611652565b6000806060876001600160a01b03166390f0f9388888888d896040518663ffffffff1660e01b8152600401610f70959493929190611cb5565b6040805160048082526024820183526020820180516001600160e01b0316630d0e30db60e41b17905291516347b7819960e11b815290916001600160a01b03861691638f6f033291610e0b9187918791879101611d14565b600080836001600160a01b03166366cb8d2f846040518263ffffffff1660e01b81526004016112849190611c87565b60206040518083038186803b15801561129c57600080fd5b505afa1580156112b0573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906112d49190611c43565b139392505050565b60006112e78261167e565b6040516366cb8d2f60e01b81526001600160a01b038616906366cb8d2f90611313908790600401611c87565b60206040518083038186803b15801561132b57600080fd5b505afa15801561133f573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906113639190611c43565b1215949350505050565b6000610d7e670de0b6b3a764000061138b858563ffffffff6116a316565b9063ffffffff6116dd16565b805160209091012090565b6000805481906113ba906001600160a01b031661171f565b6001600160a01b031663e6d642c530856040518363ffffffff1660e01b81526004016113e7929190611cfb565b60206040518083038186803b1580156113ff57600080fd5b505afa158015611413573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906114379190611971565b90506001600160a01b038116610a855760405162461bcd60e51b815260040161013f90611e1f565b6000808212156114815760405162461bcd60e51b815260040161013f90611e8f565b5090565b6000806114a861149b848863ffffffff61136d16565b869063ffffffff61118216565b90506114ca866114be868463ffffffff61118216565b9063ffffffff61179e16565b9695505050505050565b60006114e08484611255565b9050801580156114f05750600082115b15611567576114ff84846117bc565b611562576040516304e3532760e41b81526001600160a01b03851690634e3532709061152f908690600401611c87565b600060405180830381600087803b15801561154957600080fd5b505af115801561155d573d6000803e3d6000fd5b505050505b6115e4565b808015611572575081155b156115e45761158184846117bc565b6115e457604051636f86c89760e01b81526001600160a01b03851690636f86c897906115b1908690600401611c87565b600060405180830381600087803b1580156115cb57600080fd5b505af11580156115df573d6000803e3d6000fd5b505050505b836001600160a01b0316632ba57d17846115fd8561167e565b6040518363ffffffff1660e01b815260040161161a929190611cfb565b600060405180830381600087803b15801561163457600080fd5b505af1158015611648573d6000803e3d6000fd5b5050505050505050565b600081848411156116765760405162461bcd60e51b815260040161013f9190611d44565b505050900390565b6000600160ff1b82106114815760405162461bcd60e51b815260040161013f90611f47565b6000826116b257506000610a85565b828202828482816116bf57fe5b0414610d7e5760405162461bcd60e51b815260040161013f90611ec4565b6000610d7e83836040518060400160405280601a81526020017f536166654d6174683a206469766973696f6e206279207a65726f000000000000815250611848565b6040516373b2e76b60e11b81526000906001600160a01b0383169063e765ced69061174e908490600401612040565b60206040518083038186803b15801561176657600080fd5b505afa15801561177a573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610a859190611971565b6000610d7e8261138b85670de0b6b3a764000063ffffffff6116a316565b600080836001600160a01b031663a7bdad03846040518263ffffffff1660e01b81526004016117eb9190611c87565b60006040518083038186803b15801561180357600080fd5b505afa158015611817573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261183f91908101906119e5565b51119392505050565b600081836118695760405162461bcd60e51b815260040161013f9190611d44565b50600083858161187557fe5b0495945050505050565b8051610a8581612101565b600082601f83011261189a578081fd5b81356118ad6118a8826120ad565b612086565b91508082528360208285010111156118c457600080fd5b8060208401602084013760009082016020015292915050565b600082601f8301126118ed578081fd5b81516118fb6118a8826120ad565b915080825283602082850101111561191257600080fd5b6119238160208401602086016120d1565b5092915050565b60008083601f84011261193b578182fd5b50813567ffffffffffffffff811115611952578182fd5b60208301915083602082850101111561196a57600080fd5b9250929050565b600060208284031215611982578081fd5b8151610d7e81612101565b6000806000606084860312156119a1578182fd5b83516119ac81612101565b60208501516040860151919450925067ffffffffffffffff8111156119cf578182fd5b6119db868287016118dd565b9150509250925092565b600060208083850312156119f7578182fd5b825167ffffffffffffffff80821115611a0e578384fd5b81850186601f820112611a1f578485fd5b8051925081831115611a2f578485fd5b8383029150611a3f848301612086565b8381528481019082860184840187018a1015611a59578788fd5b8794505b85851015611a8357611a6f8a8261187f565b835260019490940193918601918601611a5d565b5098975050505050505050565b600060208284031215611aa1578081fd5b81518015158114610d7e578182fd5b600060208284031215611ac1578081fd5b815167ffffffffffffffff811115611ad7578182fd5b611ae3848285016118dd565b949350505050565b600060208284031215611afc578081fd5b8135610d7e81612101565b600080600080600080600060c0888a031215611b21578283fd5b8735611b2c81612101565b96506020880135611b3c81612101565b95506040880135611b4c81612101565b945060608801359350608088013567ffffffffffffffff80821115611b6f578485fd5b611b7b8b838c0161192a565b909550935060a08a0135915080821115611b93578283fd5b50611ba08a828b0161188a565b91505092959891949750929550565b60008060008060008060a08789031215611bc7578182fd5b8635611bd281612101565b95506020870135611be281612101565b945060408701359350606087013567ffffffffffffffff80821115611c05578384fd5b611c118a838b0161192a565b90955093506080890135915080821115611c29578283fd5b50611c3689828a0161188a565b9150509295509295509295565b600060208284031215611c54578081fd5b5051919050565b60008151808452611c738160208601602086016120d1565b601f01601f19169290920160200192915050565b6001600160a01b0391909116815260200190565b6001600160a01b0392831681529116602082015260400190565b6001600160a01b0386811682528581166020830152604082018590528316606082015260a060808201819052600090611cf090830184611c5b565b979650505050505050565b6001600160a01b03929092168252602082015260400190565b600060018060a01b038516825283602083015260606040830152611d3b6060830184611c5b565b95945050505050565b600060208252610d7e6020830184611c5b565b60208082526029908201527f5461726765742064656661756c7420706f736974696f6e206d7573742062652060408201526818dbdb5c1bdb995b9d60ba1b606082015260800190565b60208082526028908201527f4d75737420626520612076616c696420616e6420696e697469616c697a65642060408201526729b2ba2a37b5b2b760c11b606082015260800190565b6020808252601e908201527f4d7573742062652070656e64696e6720696e697469616c697a6174696f6e0000604082015260600190565b60208082526015908201527426bab9ba103132903b30b634b21030b230b83a32b960591b604082015260600190565b60208082526021908201527f54617267657420706f736974696f6e20756e697473206d757374206265203e206040820152600360fc1b606082015260800190565b6020808252818101527f53616665436173743a2076616c7565206d75737420626520706f736974697665604082015260600190565b60208082526021908201527f536166654d6174683a206d756c7469706c69636174696f6e206f766572666c6f6040820152607760f81b606082015260800190565b60208082526022908201527f556e69742063616e742062652067726561746572207468616e206578697374696040820152616e6760f01b606082015260800190565b60208082526028908201527f53616665436173743a2076616c756520646f65736e27742066697420696e2061604082015267371034b73a191a9b60c11b606082015260800190565b60208082526023908201527f4d75737420626520636f6e74726f6c6c65722d656e61626c656420536574546f60408201526235b2b760e91b606082015260800190565b6020808252601c908201527f4d7573742062652074686520536574546f6b656e206d616e6167657200000000604082015260600190565b6020808252601f908201527f5265656e7472616e637947756172643a207265656e7472616e742063616c6c00604082015260600190565b90815260200190565b60008582528460208301526060604083015282606083015282846080840137818301608090810191909152601f909201601f191601019392505050565b60405181810167ffffffffffffffff811182821017156120a557600080fd5b604052919050565b600067ffffffffffffffff8211156120c3578081fd5b50601f01601f191660200190565b60005b838110156120ec5781810151838201526020016120d4565b838111156120fb576000848401525b50505050565b6001600160a01b038116811461057257600080fdfea2646970667358221220d85cc0cf8b88b97f2a1ce864f4abc8a211e9548f1c8c149931a51d9d6894aa2664736f6c634300060a0033", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100885760003560e01c80638c72ef231161005b5780638c72ef23146100db578063aa0770bc146100ee578063c4d66de814610101578063f77c47911461011457610088565b806310ac16d71461008d5780633231b2d7146100a25780633fc8cef3146100b5578063847ef08d146100d3575b600080fd5b6100a061009b366004611baf565b61011c565b005b6100a06100b0366004611b07565b6101e6565b6100bd61029b565b6040516100ca9190611c87565b60405180910390f35b6100a06102aa565b6100a06100e9366004611baf565b6102ac565b6100a06100fc366004611b07565b610356565b6100a061010f366004611aeb565b6103f3565b6100bd610518565b600260015414156101485760405162461bcd60e51b815260040161013f90612009565b60405180910390fd5b60026001558561015781610527565b600254600090819061017c90879087908c906001600160a01b03168c8c8a6001610575565b6002546040519294509092506001600160a01b03808b169291811691908c16907f266efe8e5e4e2e7e407c4814a2818ef8e990768c61e67315ac34a8d3555b438e906101cf90879087908d908d90612049565b60405180910390a450506001805550505050505050565b600260015414156102095760405162461bcd60e51b815260040161013f90612009565b60026001558661021881610527565b60008061022c86868c8c8c8c8a6000610575565b91509150876001600160a01b0316896001600160a01b03168b6001600160a01b03167f266efe8e5e4e2e7e407c4814a2818ef8e990768c61e67315ac34a8d3555b438e85858b8b6040516102839493929190612049565b60405180910390a45050600180555050505050505050565b6002546001600160a01b031681565b565b600260015414156102cf5760405162461bcd60e51b815260040161013f90612009565b6002600155856102de81610527565b600254600090819061030390879087908c906001600160a01b03168c8c8a600161080c565b6002546040519294509092506001600160a01b03808b169291811691908c16907f0e631fe8e26e2b6c2ce8c4c55eca1d769c98bb4b5539068aec9ada0a3b429afe906101cf90879087908d908d90612049565b600260015414156103795760405162461bcd60e51b815260040161013f90612009565b60026001558661038881610527565b60008061039c86868c8c8c8c8a600061080c565b91509150876001600160a01b0316896001600160a01b03168b6001600160a01b03167f0e631fe8e26e2b6c2ce8c4c55eca1d769c98bb4b5539068aec9ada0a3b429afe85858b8b6040516102839493929190612049565b80336103ff82826109dc565b600054604051631d3af8fb60e21b81526001600160a01b03909116906374ebe3ec9061042f908690600401611c87565b60206040518083038186803b15801561044757600080fd5b505afa15801561045b573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061047f9190611a90565b61049b5760405162461bcd60e51b815260040161013f90611f8f565b6104a483610a06565b6104c05760405162461bcd60e51b815260040161013f90611de8565b826001600160a01b0316630ffe0f1e6040518163ffffffff1660e01b8152600401600060405180830381600087803b1580156104fb57600080fd5b505af115801561050f573d6000803e3d6000fd5b50505050505050565b6000546001600160a01b031681565b6105318133610a8b565b61054d5760405162461bcd60e51b815260040161013f90611fd2565b61055681610b19565b6105725760405162461bcd60e51b815260040161013f90611da0565b50565b600080610583888887610bcd565b6000806105918a8a8a610c5d565b915091506000610618888c6001600160a01b03166318160ddd6040518163ffffffff1660e01b815260040160206040518083038186803b1580156105d457600080fd5b505afa1580156105e8573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061060c9190611c43565b9063ffffffff610d6c16565b9050600061065b8e8e8080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250610d8592505050565b9050861561068857600254610683906001600160a01b038e811691168463ffffffff610d9c16565b610722565b6107228b826001600160a01b031663de68a3da8e8e6040518363ffffffff1660e01b81526004016106ba929190611c9b565b60206040518083038186803b1580156106d257600080fd5b505afa1580156106e6573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061070a9190611971565b6001600160a01b038f1691908563ffffffff610e6816565b6107ab8c8289610732578d6107a3565b836001600160a01b0316631878d1f16040518163ffffffff1660e01b815260040160206040518083038186803b15801561076b57600080fd5b505afa15801561077f573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906107a39190611971565b8d868d610f37565b6000806107b98e8e8e610c5d565b915091506107c98e8e888561105c565b6107d58e8d878461105c565b6107e5868363ffffffff61118216565b6107f5828763ffffffff61118216565b975097505050505050509850989650505050505050565b60008061081a888787610bcd565b6000806108288a8a8a610c5d565b91509150600061086b888c6001600160a01b03166318160ddd6040518163ffffffff1660e01b815260040160206040518083038186803b1580156105d457600080fd5b905060006108ae8e8e8080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250610d8592505050565b90506108e28a826001600160a01b031663de68a3da8e8e6040518363ffffffff1660e01b81526004016106ba929190611c9b565b61096b8c82896108f2578d610963565b836001600160a01b0316631878d1f16040518163ffffffff1660e01b815260040160206040518083038186803b15801561092b57600080fd5b505afa15801561093f573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109639190611971565b8d868d6111c4565b861561099257600254610992906001600160a01b038e81169116813163ffffffff6111fd16565b6000806109a08e8e8e610c5d565b915091506109b08e8e888561105c565b6109bc8e8d878461105c565b6109cc828763ffffffff61118216565b6107f5868363ffffffff61118216565b6109e68282610a8b565b610a025760405162461bcd60e51b815260040161013f90611fd2565b5050565b6040516353bae5f760e01b81526000906001600160a01b038316906353bae5f790610a35903090600401611c87565b60206040518083038186803b158015610a4d57600080fd5b505afa158015610a61573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610a859190611a90565b92915050565b6000816001600160a01b0316836001600160a01b031663481c6a756040518163ffffffff1660e01b815260040160206040518083038186803b158015610ad057600080fd5b505afa158015610ae4573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610b089190611971565b6001600160a01b0316149392505050565b60008054604051631d3af8fb60e21b81526001600160a01b03909116906374ebe3ec90610b4a908590600401611c87565b60206040518083038186803b158015610b6257600080fd5b505afa158015610b76573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610b9a9190611a90565b8015610a8557506040516335fc6c9f60e21b81526001600160a01b0383169063d7f1b27c90610a35903090600401611c87565b60008111610bed5760405162461bcd60e51b815260040161013f90611e4e565b610c066001600160a01b0384168363ffffffff61125516565b610c225760405162461bcd60e51b815260040161013f90611d57565b610c3c6001600160a01b038416838363ffffffff6112dc16565b610c585760405162461bcd60e51b815260040161013f90611f05565b505050565b6000806000846001600160a01b03166370a08231876040518263ffffffff1660e01b8152600401610c8e9190611c87565b60206040518083038186803b158015610ca657600080fd5b505afa158015610cba573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610cde9190611c43565b90506000846001600160a01b03166370a08231886040518263ffffffff1660e01b8152600401610d0e9190611c87565b60206040518083038186803b158015610d2657600080fd5b505afa158015610d3a573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610d5e9190611c43565b919791965090945050505050565b6000610d7e838363ffffffff61136d16565b9392505050565b600080610d9183611397565b9050610d7e816113a2565b606081604051602401610daf9190612040565b60408051601f198184030181529181526020820180516001600160e01b0316632e1a7d4d60e01b179052516347b7819960e11b81529091506001600160a01b03851690638f6f033290610e0b9086906000908690600401611d14565b600060405180830381600087803b158015610e2557600080fd5b505af1158015610e39573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052610e619190810190611ab0565b5050505050565b60608282604051602401610e7d929190611cfb565b60408051601f198184030181529181526020820180516001600160e01b031663095ea7b360e01b179052516347b7819960e11b81529091506001600160a01b03861690638f6f033290610ed99087906000908690600401611d14565b600060405180830381600087803b158015610ef357600080fd5b505af1158015610f07573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052610f2f9190810190611ab0565b505050505050565b6000806060876001600160a01b031663d91462ca8888888d896040518663ffffffff1660e01b8152600401610f70959493929190611cb5565b60006040518083038186803b158015610f8857600080fd5b505afa158015610f9c573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052610fc4919081019061198d565b925092509250886001600160a01b0316638f6f03328484846040518463ffffffff1660e01b8152600401610ffa93929190611d14565b600060405180830381600087803b15801561101457600080fd5b505af1158015611028573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526110509190810190611ab0565b50505050505050505050565b600061116683836110e7886001600160a01b03166366cb8d2f896040518263ffffffff1660e01b81526004016110929190611c87565b60206040518083038186803b1580156110aa57600080fd5b505afa1580156110be573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906110e29190611c43565b61145f565b886001600160a01b03166318160ddd6040518163ffffffff1660e01b815260040160206040518083038186803b15801561112057600080fd5b505afa158015611134573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906111589190611c43565b92919063ffffffff61148516565b9050610e616001600160a01b038616858363ffffffff6114d416565b6000610d7e83836040518060400160405280601e81526020017f536166654d6174683a207375627472616374696f6e206f766572666c6f770000815250611652565b6000806060876001600160a01b03166390f0f9388888888d896040518663ffffffff1660e01b8152600401610f70959493929190611cb5565b6040805160048082526024820183526020820180516001600160e01b0316630d0e30db60e41b17905291516347b7819960e11b815290916001600160a01b03861691638f6f033291610e0b9187918791879101611d14565b600080836001600160a01b03166366cb8d2f846040518263ffffffff1660e01b81526004016112849190611c87565b60206040518083038186803b15801561129c57600080fd5b505afa1580156112b0573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906112d49190611c43565b139392505050565b60006112e78261167e565b6040516366cb8d2f60e01b81526001600160a01b038616906366cb8d2f90611313908790600401611c87565b60206040518083038186803b15801561132b57600080fd5b505afa15801561133f573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906113639190611c43565b1215949350505050565b6000610d7e670de0b6b3a764000061138b858563ffffffff6116a316565b9063ffffffff6116dd16565b805160209091012090565b6000805481906113ba906001600160a01b031661171f565b6001600160a01b031663e6d642c530856040518363ffffffff1660e01b81526004016113e7929190611cfb565b60206040518083038186803b1580156113ff57600080fd5b505afa158015611413573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906114379190611971565b90506001600160a01b038116610a855760405162461bcd60e51b815260040161013f90611e1f565b6000808212156114815760405162461bcd60e51b815260040161013f90611e8f565b5090565b6000806114a861149b848863ffffffff61136d16565b869063ffffffff61118216565b90506114ca866114be868463ffffffff61118216565b9063ffffffff61179e16565b9695505050505050565b60006114e08484611255565b9050801580156114f05750600082115b15611567576114ff84846117bc565b611562576040516304e3532760e41b81526001600160a01b03851690634e3532709061152f908690600401611c87565b600060405180830381600087803b15801561154957600080fd5b505af115801561155d573d6000803e3d6000fd5b505050505b6115e4565b808015611572575081155b156115e45761158184846117bc565b6115e457604051636f86c89760e01b81526001600160a01b03851690636f86c897906115b1908690600401611c87565b600060405180830381600087803b1580156115cb57600080fd5b505af11580156115df573d6000803e3d6000fd5b505050505b836001600160a01b0316632ba57d17846115fd8561167e565b6040518363ffffffff1660e01b815260040161161a929190611cfb565b600060405180830381600087803b15801561163457600080fd5b505af1158015611648573d6000803e3d6000fd5b5050505050505050565b600081848411156116765760405162461bcd60e51b815260040161013f9190611d44565b505050900390565b6000600160ff1b82106114815760405162461bcd60e51b815260040161013f90611f47565b6000826116b257506000610a85565b828202828482816116bf57fe5b0414610d7e5760405162461bcd60e51b815260040161013f90611ec4565b6000610d7e83836040518060400160405280601a81526020017f536166654d6174683a206469766973696f6e206279207a65726f000000000000815250611848565b6040516373b2e76b60e11b81526000906001600160a01b0383169063e765ced69061174e908490600401612040565b60206040518083038186803b15801561176657600080fd5b505afa15801561177a573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610a859190611971565b6000610d7e8261138b85670de0b6b3a764000063ffffffff6116a316565b600080836001600160a01b031663a7bdad03846040518263ffffffff1660e01b81526004016117eb9190611c87565b60006040518083038186803b15801561180357600080fd5b505afa158015611817573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261183f91908101906119e5565b51119392505050565b600081836118695760405162461bcd60e51b815260040161013f9190611d44565b50600083858161187557fe5b0495945050505050565b8051610a8581612101565b600082601f83011261189a578081fd5b81356118ad6118a8826120ad565b612086565b91508082528360208285010111156118c457600080fd5b8060208401602084013760009082016020015292915050565b600082601f8301126118ed578081fd5b81516118fb6118a8826120ad565b915080825283602082850101111561191257600080fd5b6119238160208401602086016120d1565b5092915050565b60008083601f84011261193b578182fd5b50813567ffffffffffffffff811115611952578182fd5b60208301915083602082850101111561196a57600080fd5b9250929050565b600060208284031215611982578081fd5b8151610d7e81612101565b6000806000606084860312156119a1578182fd5b83516119ac81612101565b60208501516040860151919450925067ffffffffffffffff8111156119cf578182fd5b6119db868287016118dd565b9150509250925092565b600060208083850312156119f7578182fd5b825167ffffffffffffffff80821115611a0e578384fd5b81850186601f820112611a1f578485fd5b8051925081831115611a2f578485fd5b8383029150611a3f848301612086565b8381528481019082860184840187018a1015611a59578788fd5b8794505b85851015611a8357611a6f8a8261187f565b835260019490940193918601918601611a5d565b5098975050505050505050565b600060208284031215611aa1578081fd5b81518015158114610d7e578182fd5b600060208284031215611ac1578081fd5b815167ffffffffffffffff811115611ad7578182fd5b611ae3848285016118dd565b949350505050565b600060208284031215611afc578081fd5b8135610d7e81612101565b600080600080600080600060c0888a031215611b21578283fd5b8735611b2c81612101565b96506020880135611b3c81612101565b95506040880135611b4c81612101565b945060608801359350608088013567ffffffffffffffff80821115611b6f578485fd5b611b7b8b838c0161192a565b909550935060a08a0135915080821115611b93578283fd5b50611ba08a828b0161188a565b91505092959891949750929550565b60008060008060008060a08789031215611bc7578182fd5b8635611bd281612101565b95506020870135611be281612101565b945060408701359350606087013567ffffffffffffffff80821115611c05578384fd5b611c118a838b0161192a565b90955093506080890135915080821115611c29578283fd5b50611c3689828a0161188a565b9150509295509295509295565b600060208284031215611c54578081fd5b5051919050565b60008151808452611c738160208601602086016120d1565b601f01601f19169290920160200192915050565b6001600160a01b0391909116815260200190565b6001600160a01b0392831681529116602082015260400190565b6001600160a01b0386811682528581166020830152604082018590528316606082015260a060808201819052600090611cf090830184611c5b565b979650505050505050565b6001600160a01b03929092168252602082015260400190565b600060018060a01b038516825283602083015260606040830152611d3b6060830184611c5b565b95945050505050565b600060208252610d7e6020830184611c5b565b60208082526029908201527f5461726765742064656661756c7420706f736974696f6e206d7573742062652060408201526818dbdb5c1bdb995b9d60ba1b606082015260800190565b60208082526028908201527f4d75737420626520612076616c696420616e6420696e697469616c697a65642060408201526729b2ba2a37b5b2b760c11b606082015260800190565b6020808252601e908201527f4d7573742062652070656e64696e6720696e697469616c697a6174696f6e0000604082015260600190565b60208082526015908201527426bab9ba103132903b30b634b21030b230b83a32b960591b604082015260600190565b60208082526021908201527f54617267657420706f736974696f6e20756e697473206d757374206265203e206040820152600360fc1b606082015260800190565b6020808252818101527f53616665436173743a2076616c7565206d75737420626520706f736974697665604082015260600190565b60208082526021908201527f536166654d6174683a206d756c7469706c69636174696f6e206f766572666c6f6040820152607760f81b606082015260800190565b60208082526022908201527f556e69742063616e742062652067726561746572207468616e206578697374696040820152616e6760f01b606082015260800190565b60208082526028908201527f53616665436173743a2076616c756520646f65736e27742066697420696e2061604082015267371034b73a191a9b60c11b606082015260800190565b60208082526023908201527f4d75737420626520636f6e74726f6c6c65722d656e61626c656420536574546f60408201526235b2b760e91b606082015260800190565b6020808252601c908201527f4d7573742062652074686520536574546f6b656e206d616e6167657200000000604082015260600190565b6020808252601f908201527f5265656e7472616e637947756172643a207265656e7472616e742063616c6c00604082015260600190565b90815260200190565b60008582528460208301526060604083015282606083015282846080840137818301608090810191909152601f909201601f191601019392505050565b60405181810167ffffffffffffffff811182821017156120a557600080fd5b604052919050565b600067ffffffffffffffff8211156120c3578081fd5b50601f01601f191660200190565b60005b838110156120ec5781810151838201526020016120d4565b838111156120fb576000848401525b50505050565b6001600160a01b038116811461057257600080fdfea2646970667358221220d85cc0cf8b88b97f2a1ce864f4abc8a211e9548f1c8c149931a51d9d6894aa2664736f6c634300060a0033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/external/contracts/set/WrapModuleV2.sol b/external/contracts/set/WrapModuleV2.sol new file mode 100644 index 00000000..c1c37108 --- /dev/null +++ b/external/contracts/set/WrapModuleV2.sol @@ -0,0 +1,528 @@ +/* + 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; +pragma experimental "ABIEncoderV2"; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/SafeCast.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; + +import { IController } from "../../../interfaces/IController.sol"; +import { IIntegrationRegistry } from "../../../interfaces/IIntegrationRegistry.sol"; +import { Invoke } from "../../lib/Invoke.sol"; +import { ISetToken } from "../../../interfaces/ISetToken.sol"; +import { IWETH } from "../../../interfaces/external/IWETH.sol"; +import { IWrapV2Adapter } from "../../../interfaces/IWrapV2Adapter.sol"; +import { ModuleBase } from "../../lib/ModuleBase.sol"; +import { Position } from "../../lib/Position.sol"; +import { PreciseUnitMath } from "../../../lib/PreciseUnitMath.sol"; + +/** + * @title WrapModuleV2 + * @author Set Protocol + * + * Module that enables the wrapping of ERC20 and Ether positions via third party protocols. The WrapModuleV2 + * works in conjunction with WrapV2Adapters, in which the wrapAdapterID / integrationNames are stored on the + * integration registry. + * + * Some examples of wrap actions include wrapping, DAI to cDAI (Compound) or Dai to aDai (AAVE). + */ +contract WrapModuleV2 is ModuleBase, ReentrancyGuard { + using SafeCast for int256; + using PreciseUnitMath for uint256; + using Position for uint256; + using SafeMath for uint256; + + using Invoke for ISetToken; + using Position for ISetToken.Position; + using Position for ISetToken; + + /* ============ Events ============ */ + + event ComponentWrapped( + ISetToken indexed _setToken, + address indexed _underlyingToken, + address indexed _wrappedToken, + uint256 _underlyingQuantity, + uint256 _wrappedQuantity, + string _integrationName + ); + + event ComponentUnwrapped( + ISetToken indexed _setToken, + address indexed _underlyingToken, + address indexed _wrappedToken, + uint256 _underlyingQuantity, + uint256 _wrappedQuantity, + string _integrationName + ); + + /* ============ State Variables ============ */ + + // Wrapped ETH address + IWETH public weth; + + /* ============ Constructor ============ */ + + /** + * @param _controller Address of controller contract + * @param _weth Address of wrapped eth + */ + constructor(IController _controller, IWETH _weth) public ModuleBase(_controller) { + weth = _weth; + } + + /* ============ External Functions ============ */ + + /** + * MANAGER-ONLY: Instructs the SetToken to wrap an underlying asset into a wrappedToken via a specified adapter. + * + * @param _setToken Instance of the SetToken + * @param _underlyingToken Address of the component to be wrapped + * @param _wrappedToken Address of the desired wrapped token + * @param _underlyingUnits Quantity of underlying units in Position units + * @param _integrationName Name of wrap module integration (mapping on integration registry) + * @param _wrapData Arbitrary bytes to pass into the WrapV2Adapter + */ + function wrap( + ISetToken _setToken, + address _underlyingToken, + address _wrappedToken, + uint256 _underlyingUnits, + string calldata _integrationName, + bytes memory _wrapData + ) + external + nonReentrant + onlyManagerAndValidSet(_setToken) + { + ( + uint256 notionalUnderlyingWrapped, + uint256 notionalWrapped + ) = _validateWrapAndUpdate( + _integrationName, + _setToken, + _underlyingToken, + _wrappedToken, + _underlyingUnits, + _wrapData, + false // does not use Ether + ); + + emit ComponentWrapped( + _setToken, + _underlyingToken, + _wrappedToken, + notionalUnderlyingWrapped, + notionalWrapped, + _integrationName + ); + } + + /** + * MANAGER-ONLY: Instructs the SetToken to wrap Ether into a wrappedToken via a specified adapter. Since SetTokens + * only hold WETH, in order to support protocols that collateralize with Ether the SetToken's WETH must be unwrapped + * first before sending to the external protocol. + * + * @param _setToken Instance of the SetToken + * @param _wrappedToken Address of the desired wrapped token + * @param _underlyingUnits Quantity of underlying units in Position units + * @param _integrationName Name of wrap module integration (mapping on integration registry) + * @param _wrapData Arbitrary bytes to pass into the WrapV2Adapter + */ + function wrapWithEther( + ISetToken _setToken, + address _wrappedToken, + uint256 _underlyingUnits, + string calldata _integrationName, + bytes memory _wrapData + ) + external + nonReentrant + onlyManagerAndValidSet(_setToken) + { + ( + uint256 notionalUnderlyingWrapped, + uint256 notionalWrapped + ) = _validateWrapAndUpdate( + _integrationName, + _setToken, + address(weth), + _wrappedToken, + _underlyingUnits, + _wrapData, + true // uses Ether + ); + + emit ComponentWrapped( + _setToken, + address(weth), + _wrappedToken, + notionalUnderlyingWrapped, + notionalWrapped, + _integrationName + ); + } + + /** + * MANAGER-ONLY: Instructs the SetToken to unwrap a wrapped asset into its underlying via a specified adapter. + * + * @param _setToken Instance of the SetToken + * @param _underlyingToken Address of the underlying asset + * @param _wrappedToken Address of the component to be unwrapped + * @param _wrappedUnits Quantity of wrapped tokens in Position units + * @param _integrationName ID of wrap module integration (mapping on integration registry) + * @param _unwrapData Arbitrary bytes to pass into the WrapV2Adapter + */ + function unwrap( + ISetToken _setToken, + address _underlyingToken, + address _wrappedToken, + uint256 _wrappedUnits, + string calldata _integrationName, + bytes memory _unwrapData + ) + external + nonReentrant + onlyManagerAndValidSet(_setToken) + { + ( + uint256 notionalUnderlyingUnwrapped, + uint256 notionalUnwrapped + ) = _validateUnwrapAndUpdate( + _integrationName, + _setToken, + _underlyingToken, + _wrappedToken, + _wrappedUnits, + _unwrapData, + false // uses Ether + ); + + emit ComponentUnwrapped( + _setToken, + _underlyingToken, + _wrappedToken, + notionalUnderlyingUnwrapped, + notionalUnwrapped, + _integrationName + ); + } + + /** + * MANAGER-ONLY: Instructs the SetToken to unwrap a wrapped asset collateralized by Ether into Wrapped Ether. Since + * external protocol will send back Ether that Ether must be Wrapped into WETH in order to be accounted for by SetToken. + * + * @param _setToken Instance of the SetToken + * @param _wrappedToken Address of the component to be unwrapped + * @param _wrappedUnits Quantity of wrapped tokens in Position units + * @param _integrationName ID of wrap module integration (mapping on integration registry) + * @param _unwrapData Arbitrary bytes to pass into the WrapV2Adapter + */ + function unwrapWithEther( + ISetToken _setToken, + address _wrappedToken, + uint256 _wrappedUnits, + string calldata _integrationName, + bytes memory _unwrapData + ) + external + nonReentrant + onlyManagerAndValidSet(_setToken) + { + ( + uint256 notionalUnderlyingUnwrapped, + uint256 notionalUnwrapped + ) = _validateUnwrapAndUpdate( + _integrationName, + _setToken, + address(weth), + _wrappedToken, + _wrappedUnits, + _unwrapData, + true // uses Ether + ); + + emit ComponentUnwrapped( + _setToken, + address(weth), + _wrappedToken, + notionalUnderlyingUnwrapped, + notionalUnwrapped, + _integrationName + ); + } + + /** + * Initializes this module to the SetToken. Only callable by the SetToken's manager. + * + * @param _setToken Instance of the SetToken to issue + */ + function initialize(ISetToken _setToken) external onlySetManager(_setToken, msg.sender) { + require(controller.isSet(address(_setToken)), "Must be controller-enabled SetToken"); + require(isSetPendingInitialization(_setToken), "Must be pending initialization"); + _setToken.initializeModule(); + } + + /** + * Removes this module from the SetToken, via call by the SetToken. + */ + function removeModule() external override {} + + + /* ============ Internal Functions ============ */ + + /** + * Validates the wrap operation is valid. In particular, the following checks are made: + * - The position is Default + * - The position has sufficient units given the transact quantity + * - The transact quantity > 0 + * + * It is expected that the adapter will check if wrappedToken/underlyingToken are a valid pair for the given + * integration. + */ + function _validateInputs( + ISetToken _setToken, + address _transactPosition, + uint256 _transactPositionUnits + ) + internal + view + { + require(_transactPositionUnits > 0, "Target position units must be > 0"); + require(_setToken.hasDefaultPosition(_transactPosition), "Target default position must be component"); + require( + _setToken.hasSufficientDefaultUnits(_transactPosition, _transactPositionUnits), + "Unit cant be greater than existing" + ); + } + + /** + * The WrapModule calculates the total notional underlying to wrap, approves the underlying to the 3rd party + * integration contract, then invokes the SetToken to call wrap by passing its calldata along. When raw ETH + * is being used (_usesEther = true) WETH position must first be unwrapped and underlyingAddress sent to + * adapter must be external protocol's ETH representative address. + * + * Returns notional amount of underlying tokens and wrapped tokens that were wrapped. + */ + function _validateWrapAndUpdate( + string calldata _integrationName, + ISetToken _setToken, + address _underlyingToken, + address _wrappedToken, + uint256 _underlyingUnits, + bytes memory _wrapData, + bool _usesEther + ) + internal + returns (uint256, uint256) + { + _validateInputs(_setToken, _underlyingToken, _underlyingUnits); + + // Snapshot pre wrap balances + ( + uint256 preActionUnderlyingNotional, + uint256 preActionWrapNotional + ) = _snapshotTargetAssetsBalance(_setToken, _underlyingToken, _wrappedToken); + + uint256 notionalUnderlying = _setToken.totalSupply().getDefaultTotalNotional(_underlyingUnits); + IWrapV2Adapter wrapAdapter = IWrapV2Adapter(getAndValidateAdapter(_integrationName)); + + // Execute any pre-wrap actions depending on if using raw ETH or not + if (_usesEther) { + _setToken.invokeUnwrapWETH(address(weth), notionalUnderlying); + } else { + _setToken.invokeApprove(_underlyingToken, wrapAdapter.getSpenderAddress(_underlyingToken, _wrappedToken), notionalUnderlying); + } + + // Get function call data and invoke on SetToken + _createWrapDataAndInvoke( + _setToken, + wrapAdapter, + _usesEther ? wrapAdapter.ETH_TOKEN_ADDRESS() : _underlyingToken, + _wrappedToken, + notionalUnderlying, + _wrapData + ); + + // Snapshot post wrap balances + ( + uint256 postActionUnderlyingNotional, + uint256 postActionWrapNotional + ) = _snapshotTargetAssetsBalance(_setToken, _underlyingToken, _wrappedToken); + + _updatePosition(_setToken, _underlyingToken, preActionUnderlyingNotional, postActionUnderlyingNotional); + _updatePosition(_setToken, _wrappedToken, preActionWrapNotional, postActionWrapNotional); + + return ( + preActionUnderlyingNotional.sub(postActionUnderlyingNotional), + postActionWrapNotional.sub(preActionWrapNotional) + ); + } + + /** + * The WrapModule calculates the total notional wrap token to unwrap, then invokes the SetToken to call + * unwrap by passing its calldata along. When raw ETH is being used (_usesEther = true) underlyingAddress + * sent to adapter must be set to external protocol's ETH representative address and ETH returned from + * external protocol is wrapped. + * + * Returns notional amount of underlying tokens and wrapped tokens unwrapped. + */ + function _validateUnwrapAndUpdate( + string calldata _integrationName, + ISetToken _setToken, + address _underlyingToken, + address _wrappedToken, + uint256 _wrappedTokenUnits, + bytes memory _unwrapData, + bool _usesEther + ) + internal + returns (uint256, uint256) + { + _validateInputs(_setToken, _wrappedToken, _wrappedTokenUnits); + + ( + uint256 preActionUnderlyingNotional, + uint256 preActionWrapNotional + ) = _snapshotTargetAssetsBalance(_setToken, _underlyingToken, _wrappedToken); + + uint256 notionalWrappedToken = _setToken.totalSupply().getDefaultTotalNotional(_wrappedTokenUnits); + IWrapV2Adapter wrapAdapter = IWrapV2Adapter(getAndValidateAdapter(_integrationName)); + + // Approve wrapped token for spending in case protocols require approvals to transfer wrapped tokens + _setToken.invokeApprove(_wrappedToken, wrapAdapter.getSpenderAddress(_underlyingToken, _wrappedToken), notionalWrappedToken); + + // Get function call data and invoke on SetToken + _createUnwrapDataAndInvoke( + _setToken, + wrapAdapter, + _usesEther ? wrapAdapter.ETH_TOKEN_ADDRESS() : _underlyingToken, + _wrappedToken, + notionalWrappedToken, + _unwrapData + ); + + if (_usesEther) { + _setToken.invokeWrapWETH(address(weth), address(_setToken).balance); + } + + ( + uint256 postActionUnderlyingNotional, + uint256 postActionWrapNotional + ) = _snapshotTargetAssetsBalance(_setToken, _underlyingToken, _wrappedToken); + + _updatePosition(_setToken, _underlyingToken, preActionUnderlyingNotional, postActionUnderlyingNotional); + _updatePosition(_setToken, _wrappedToken, preActionWrapNotional, postActionWrapNotional); + + return ( + postActionUnderlyingNotional.sub(preActionUnderlyingNotional), + preActionWrapNotional.sub(postActionWrapNotional) + ); + } + + /** + * Create the calldata for wrap and then invoke the call on the SetToken. + */ + function _createWrapDataAndInvoke( + ISetToken _setToken, + IWrapV2Adapter _wrapAdapter, + address _underlyingToken, + address _wrappedToken, + uint256 _notionalUnderlying, + bytes memory _wrapData + ) internal { + ( + address callTarget, + uint256 callValue, + bytes memory callByteData + ) = _wrapAdapter.getWrapCallData( + _underlyingToken, + _wrappedToken, + _notionalUnderlying, + address(_setToken), + _wrapData + ); + + _setToken.invoke(callTarget, callValue, callByteData); + } + + /** + * Create the calldata for unwrap and then invoke the call on the SetToken. + */ + function _createUnwrapDataAndInvoke( + ISetToken _setToken, + IWrapV2Adapter _wrapAdapter, + address _underlyingToken, + address _wrappedToken, + uint256 _notionalUnderlying, + bytes memory _unwrapData + ) internal { + ( + address callTarget, + uint256 callValue, + bytes memory callByteData + ) = _wrapAdapter.getUnwrapCallData( + _underlyingToken, + _wrappedToken, + _notionalUnderlying, + address(_setToken), + _unwrapData + ); + + _setToken.invoke(callTarget, callValue, callByteData); + } + + /** + * After a wrap/unwrap operation, check the underlying and wrap token quantities and recalculate + * the units ((total tokens - airdrop)/ total supply). Then update the position on the SetToken. + */ + function _updatePosition( + ISetToken _setToken, + address _token, + uint256 _preActionTokenBalance, + uint256 _postActionTokenBalance + ) internal { + uint256 newUnit = _setToken.totalSupply().calculateDefaultEditPositionUnit( + _preActionTokenBalance, + _postActionTokenBalance, + _setToken.getDefaultPositionRealUnit(_token).toUint256() + ); + + _setToken.editDefaultPosition(_token, newUnit); + } + + /** + * Take snapshot of SetToken's balance of underlying and wrapped tokens. + */ + function _snapshotTargetAssetsBalance( + ISetToken _setToken, + address _underlyingToken, + address _wrappedToken + ) internal view returns(uint256, uint256) { + uint256 underlyingTokenBalance = IERC20(_underlyingToken).balanceOf(address(_setToken)); + uint256 wrapTokenBalance = IERC20(_wrappedToken).balanceOf(address(_setToken)); + + return ( + underlyingTokenBalance, + wrapTokenBalance + ); + } +} \ No newline at end of file diff --git a/test/global-extensions/globalWrapExtension.spec.ts b/test/global-extensions/globalWrapExtension.spec.ts new file mode 100644 index 00000000..245c70e7 --- /dev/null +++ b/test/global-extensions/globalWrapExtension.spec.ts @@ -0,0 +1,809 @@ +import "module-alias/register"; + +import { BigNumber, Contract } from "ethers"; +import { Address, Account } from "@utils/types"; +import { ADDRESS_ZERO, ZERO_BYTES } from "@utils/constants"; +import { + DelegatedManager, + GlobalWrapExtension, + ManagerCore, +} from "@utils/contracts/index"; +import { + SetToken, + WrapModuleV2, + WrapV2AdapterMock +} from "@utils/contracts/setV2"; +import DeployHelper from "@utils/deploys"; +import { + addSnapshotBeforeRestoreAfterEach, + ether, + getAccounts, + getWaffleExpect, + preciseMul, + getProvider, + getRandomAccount, + getSetFixture, +} from "@utils/index"; +import { SetFixture } from "@utils/fixtures"; +import { ContractTransaction } from "ethers"; + +const expect = getWaffleExpect(); + +describe("GlobalWrapExtension", () => { + let owner: Account; + let methodologist: Account; + let operator: Account; + let factory: Account; + + let deployer: DeployHelper; + let setToken: SetToken; + let setV2Setup: SetFixture; + + let managerCore: ManagerCore; + let delegatedManager: DelegatedManager; + let wrapExtension: GlobalWrapExtension; + + let wrapModule: WrapModuleV2; + let wrapAdapterMock: WrapV2AdapterMock; + const wrapAdapterMockIntegrationName: string = "MOCK_WRAPPER_V2"; + + before(async () => { + [ + owner, + methodologist, + operator, + factory, + ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + + setV2Setup = getSetFixture(owner.address); + await setV2Setup.initialize(); + + wrapModule = await deployer.setV2.deployWrapModuleV2(setV2Setup.controller.address, setV2Setup.weth.address); + await setV2Setup.controller.addModule(wrapModule.address); + + wrapAdapterMock = await deployer.setV2.deployWrapV2AdapterMock(); + + await setV2Setup.integrationRegistry.addIntegration( + wrapModule.address, + wrapAdapterMockIntegrationName, + wrapAdapterMock.address + ); + + managerCore = await deployer.managerCore.deployManagerCore(); + + wrapExtension = await deployer.globalExtensions.deployGlobalWrapExtension( + managerCore.address, + wrapModule.address + ); + + setToken = await setV2Setup.createSetToken( + [setV2Setup.weth.address], + [ether(1)], + [setV2Setup.issuanceModule.address, wrapModule.address] + ); + + await setV2Setup.issuanceModule.initialize(setToken.address, ADDRESS_ZERO); + + delegatedManager = await deployer.manager.deployDelegatedManager( + setToken.address, + factory.address, + methodologist.address, + [wrapExtension.address], + [operator.address], + [setV2Setup.dai.address, setV2Setup.weth.address, setV2Setup.wbtc.address], + true + ); + + await setToken.setManager(delegatedManager.address); + + await managerCore.initialize([wrapExtension.address], [factory.address]); + await managerCore.connect(factory.wallet).addManager(delegatedManager.address); + }); + + addSnapshotBeforeRestoreAfterEach(); + + describe("#constructor", async () => { + let subjectManagerCore: Address; + let subjectWrapModule: Address; + + beforeEach(async () => { + subjectManagerCore = managerCore.address; + subjectWrapModule = wrapModule.address; + }); + + async function subject(): Promise { + return await deployer.globalExtensions.deployGlobalWrapExtension( + subjectManagerCore, + subjectWrapModule + ); + } + + it("should set the correct WrapModuleV2 address", async () => { + const wrapExtension = await subject(); + + const storedModule = await wrapExtension.wrapModule(); + expect(storedModule).to.eq(subjectWrapModule); + }); + }); + + describe("#initializeModule", async () => { + let subjectDelegatedManager: Address; + let subjectCaller: Account; + + beforeEach(async () => { + await wrapExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + + subjectDelegatedManager = delegatedManager.address; + subjectCaller = owner; + }); + + async function subject(): Promise { + return wrapExtension.connect(subjectCaller.wallet).initializeModule(subjectDelegatedManager); + } + + it("should initialize the module on the SetToken", async () => { + await subject(); + + const isModuleInitialized: Boolean = await setToken.isInitializedModule(wrapModule.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([wrapExtension.address]); + await delegatedManager.connect(owner.wallet).setManager(owner.address); + await setToken.connect(owner.wallet).removeModule(wrapModule.address); + await setToken.connect(owner.wallet).setManager(delegatedManager.address); + await delegatedManager.connect(owner.wallet).addExtensions([wrapExtension.address]); + await wrapExtension.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([wrapExtension.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be initialized extension"); + }); + }); + + describe("when the extension is pending", async () => { + beforeEach(async () => { + await delegatedManager.connect(owner.wallet).removeExtensions([wrapExtension.address]); + await delegatedManager.connect(owner.wallet).addExtensions([wrapExtension.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be initialized extension"); + }); + }); + + 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 wrapExtension.connect(subjectCaller.wallet).initializeExtension(subjectDelegatedManager); + } + + it("should store the correct SetToken and DelegatedManager on the WrapExtension", async () => { + await subject(); + + const storedDelegatedManager: Address = await wrapExtension.setManagers(setToken.address); + expect(storedDelegatedManager).to.eq(delegatedManager.address); + }); + + it("should initialize the WrapExtension on the DelegatedManager", async () => { + await subject(); + + const isExtensionInitialized: Boolean = await delegatedManager.isInitializedExtension(wrapExtension.address); + expect(isExtensionInitialized).to.eq(true); + }); + + it("should emit the correct WrapExtensionInitialized event", async () => { + await expect(subject()).to.emit(wrapExtension, "WrapExtensionInitialized").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 wrapExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + await delegatedManager.connect(owner.wallet).removeExtensions([wrapExtension.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 wrapExtension.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 wrapExtension.connect(subjectCaller.wallet).initializeModuleAndExtension(subjectDelegatedManager); + } + + it("should initialize the module on the SetToken", async () => { + await subject(); + + const isModuleInitialized: Boolean = await setToken.isInitializedModule(wrapModule.address); + expect(isModuleInitialized).to.eq(true); + }); + + it("should store the correct SetToken and DelegatedManager on the WrapExtension", async () => { + await subject(); + + const storedDelegatedManager: Address = await wrapExtension.setManagers(setToken.address); + expect(storedDelegatedManager).to.eq(delegatedManager.address); + }); + + it("should initialize the WrapExtension on the DelegatedManager", async () => { + await subject(); + + const isExtensionInitialized: Boolean = await delegatedManager.isInitializedExtension(wrapExtension.address); + expect(isExtensionInitialized).to.eq(true); + }); + + it("should emit the correct WrapExtensionInitialized event", async () => { + await expect(subject()).to.emit(wrapExtension, "WrapExtensionInitialized").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 wrapExtension.connect(owner.wallet).initializeModuleAndExtension(delegatedManager.address); + await delegatedManager.connect(owner.wallet).removeExtensions([wrapExtension.address]); + await delegatedManager.connect(owner.wallet).setManager(owner.address); + await setToken.connect(owner.wallet).removeModule(wrapModule.address); + await setToken.connect(owner.wallet).setManager(delegatedManager.address); + await delegatedManager.connect(owner.wallet).addExtensions([wrapExtension.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 wrapExtension.connect(owner.wallet).initializeModuleAndExtension(delegatedManager.address); + await delegatedManager.connect(owner.wallet).removeExtensions([wrapExtension.address]); + await delegatedManager.connect(owner.wallet).addExtensions([wrapExtension.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 wrapExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + await delegatedManager.connect(owner.wallet).removeExtensions([wrapExtension.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 wrapExtension.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 subjectWrapExtension: Address[]; + let subjectCaller: Account; + + beforeEach(async () => { + await wrapExtension.connect(owner.wallet).initializeExtension(delegatedManager.address); + + subjectManager = delegatedManager; + subjectWrapExtension = [wrapExtension.address]; + subjectCaller = owner; + }); + + async function subject(): Promise { + return subjectManager.connect(subjectCaller.wallet).removeExtensions(subjectWrapExtension); + } + + it("should clear SetToken and DelegatedManager from WrapExtension state", async () => { + await subject(); + + const storedDelegatedManager: Address = await wrapExtension.setManagers(setToken.address); + expect(storedDelegatedManager).to.eq(ADDRESS_ZERO); + }); + + it("should emit the correct ExtensionRemoved event", async () => { + await expect(subject()).to.emit(wrapExtension, "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"); + }); + }); + }); + + context("when the WrapExtension is initialized and SetToken has been issued", async () => { + let setTokensIssued: BigNumber; + + before(async () => { + wrapExtension.connect(owner.wallet).initializeModuleAndExtension(delegatedManager.address); + + // Issue some Sets + setTokensIssued = ether(10); + const underlyingRequired = setTokensIssued; + await setV2Setup.weth.approve(setV2Setup.issuanceModule.address, underlyingRequired); + await setV2Setup.issuanceModule.issue(setToken.address, setTokensIssued, owner.address); + }); + + describe("#wrap", async () => { + let subjectSetToken: Address; + let subjectUnderlyingToken: Address; + let subjectWrappedToken: Address; + let subjectUnderlyingUnits: BigNumber; + let subjectIntegrationName: string; + let subjectWrapData: string; + let subjectCaller: Account; + + beforeEach(async () => { + await delegatedManager.connect(owner.wallet).addAllowedAssets([wrapAdapterMock.address]); + + subjectSetToken = setToken.address; + subjectUnderlyingToken = setV2Setup.weth.address; + subjectWrappedToken = wrapAdapterMock.address; + subjectUnderlyingUnits = ether(1); + subjectIntegrationName = wrapAdapterMockIntegrationName; + subjectWrapData = ZERO_BYTES; + subjectCaller = operator; + }); + + async function subject(): Promise { + return wrapExtension.connect(subjectCaller.wallet).wrap( + subjectSetToken, + subjectUnderlyingToken, + subjectWrappedToken, + subjectUnderlyingUnits, + subjectIntegrationName, + subjectWrapData + ); + } + + it("should mint the correct wrapped asset to the SetToken", async () => { + await subject(); + const wrappedBalance = await wrapAdapterMock.balanceOf(setToken.address); + const expectedTokenBalance = setTokensIssued; + expect(wrappedBalance).to.eq(expectedTokenBalance); + }); + + it("should reduce the correct quantity of the underlying quantity", async () => { + const previousUnderlyingBalance = await setV2Setup.weth.balanceOf(setToken.address); + + await subject(); + const underlyingTokenBalance = await setV2Setup.weth.balanceOf(setToken.address); + const expectedUnderlyingBalance = previousUnderlyingBalance.sub(setTokensIssued); + expect(underlyingTokenBalance).to.eq(expectedUnderlyingBalance); + }); + + it("remove the underlying position and replace with the wrapped token position", async () => { + await subject(); + + const positions = await setToken.getPositions(); + const receivedWrappedTokenPosition = positions[0]; + + expect(positions.length).to.eq(1); + expect(receivedWrappedTokenPosition.component).to.eq(subjectWrappedToken); + expect(receivedWrappedTokenPosition.unit).to.eq(subjectUnderlyingUnits); + }); + + describe("when the caller is not the operator", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be approved operator"); + }); + }); + + describe("when the wrapped token is not an allowed asset", async () => { + beforeEach(async () => { + await delegatedManager.connect(owner.wallet).removeAllowedAssets([wrapAdapterMock.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be allowed asset"); + }); + }); + }); + + describe("#wrapWithEther", async () => { + let subjectSetToken: Address; + let subjectWrappedToken: Address; + let subjectUnderlyingUnits: BigNumber; + let subjectIntegrationName: string; + let subjectWrapData: string; + let subjectCaller: Account; + + beforeEach(async () => { + await delegatedManager.connect(owner.wallet).addAllowedAssets([wrapAdapterMock.address]); + + subjectSetToken = setToken.address; + subjectWrappedToken = wrapAdapterMock.address; + subjectUnderlyingUnits = ether(1); + subjectIntegrationName = wrapAdapterMockIntegrationName; + subjectWrapData = ZERO_BYTES; + subjectCaller = operator; + }); + + async function subject(): Promise { + return wrapExtension.connect(subjectCaller.wallet).wrapWithEther( + subjectSetToken, + subjectWrappedToken, + subjectUnderlyingUnits, + subjectIntegrationName, + subjectWrapData + ); + } + + it("should mint the correct wrapped asset to the SetToken", async () => { + await subject(); + const wrappedBalance = await wrapAdapterMock.balanceOf(setToken.address); + const expectedTokenBalance = setTokensIssued; + expect(wrappedBalance).to.eq(expectedTokenBalance); + }); + + it("should reduce the correct quantity of WETH", async () => { + const previousUnderlyingBalance = await setV2Setup.weth.balanceOf(setToken.address); + + await subject(); + const underlyingTokenBalance = await setV2Setup.weth.balanceOf(setToken.address); + const expectedUnderlyingBalance = previousUnderlyingBalance.sub(setTokensIssued); + expect(underlyingTokenBalance).to.eq(expectedUnderlyingBalance); + }); + + it("should send the correct quantity of ETH to the external protocol", async () => { + const provider = getProvider(); + const preEthBalance = await provider.getBalance(wrapAdapterMock.address); + + await subject(); + + const postEthBalance = await provider.getBalance(wrapAdapterMock.address); + expect(postEthBalance).to.eq(preEthBalance.add(preciseMul(subjectUnderlyingUnits, setTokensIssued))); + }); + + it("removes the underlying position and replace with the wrapped token position", async () => { + await subject(); + + const positions = await setToken.getPositions(); + const receivedWrappedTokenPosition = positions[0]; + + expect(positions.length).to.eq(1); + expect(receivedWrappedTokenPosition.component).to.eq(subjectWrappedToken); + expect(receivedWrappedTokenPosition.unit).to.eq(subjectUnderlyingUnits); + }); + + describe("when the caller is not the operator", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be approved operator"); + }); + }); + + describe("when the wrapped token is not an allowed asset", async () => { + beforeEach(async () => { + await delegatedManager.connect(owner.wallet).removeAllowedAssets([wrapAdapterMock.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be allowed asset"); + }); + }); + }); + + describe("#unwrap", async () => { + let subjectSetToken: Address; + let subjectUnderlyingToken: Address; + let subjectWrappedToken: Address; + let subjectWrappedTokenUnits: BigNumber; + let subjectIntegrationName: string; + let subjectUnwrapData: string; + let subjectCaller: Account; + + let wrappedQuantity: BigNumber; + + beforeEach(async () => { + subjectSetToken = setToken.address; + subjectUnderlyingToken = setV2Setup.weth.address; + subjectWrappedToken = wrapAdapterMock.address; + subjectWrappedTokenUnits = ether(0.5); + subjectIntegrationName = wrapAdapterMockIntegrationName; + subjectUnwrapData = ZERO_BYTES; + subjectCaller = operator; + + wrappedQuantity = ether(1); + + await delegatedManager.connect(owner.wallet).addAllowedAssets([wrapAdapterMock.address]); + + await wrapExtension.connect(operator.wallet).wrap( + subjectSetToken, + subjectUnderlyingToken, + subjectWrappedToken, + wrappedQuantity, + subjectIntegrationName, + ZERO_BYTES + ); + }); + + async function subject(): Promise { + return wrapExtension.connect(subjectCaller.wallet).unwrap( + subjectSetToken, + subjectUnderlyingToken, + subjectWrappedToken, + subjectWrappedTokenUnits, + subjectIntegrationName, + subjectUnwrapData + ); + } + + it("should burn the correct wrapped asset to the SetToken", async () => { + await subject(); + const newWrappedBalance = await wrapAdapterMock.balanceOf(setToken.address); + const expectedTokenBalance = preciseMul(setTokensIssued, wrappedQuantity.sub(subjectWrappedTokenUnits)); + expect(newWrappedBalance).to.eq(expectedTokenBalance); + }); + + it("should properly update the underlying and wrapped token units", async () => { + await subject(); + + const positions = await setToken.getPositions(); + const [receivedWrappedPosition, receivedUnderlyingPosition] = positions; + + expect(positions.length).to.eq(2); + expect(receivedWrappedPosition.component).to.eq(subjectWrappedToken); + expect(receivedWrappedPosition.unit).to.eq(ether(0.5)); + + expect(receivedUnderlyingPosition.component).to.eq(subjectUnderlyingToken); + expect(receivedUnderlyingPosition.unit).to.eq(ether(0.5)); + }); + + describe("when the caller is not the operator", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be approved operator"); + }); + }); + + describe("when the underlying token is not an allowed asset", async () => { + beforeEach(async () => { + await delegatedManager.connect(owner.wallet).removeAllowedAssets([setV2Setup.weth.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be allowed asset"); + }); + }); + }); + + describe("#unwrapWithEther", async () => { + let subjectSetToken: Address; + let subjectWrappedToken: Address; + let subjectWrappedTokenUnits: BigNumber; + let subjectIntegrationName: string; + let subjectUnwrapData: string; + let subjectCaller: Account; + + let wrappedQuantity: BigNumber; + + beforeEach(async () => { + subjectSetToken = setToken.address; + subjectWrappedToken = wrapAdapterMock.address; + subjectWrappedTokenUnits = ether(0.5); + subjectIntegrationName = wrapAdapterMockIntegrationName; + subjectUnwrapData = ZERO_BYTES; + subjectCaller = operator; + + wrappedQuantity = ether(1); + + await delegatedManager.connect(owner.wallet).addAllowedAssets([wrapAdapterMock.address]); + + await wrapExtension.connect(operator.wallet).wrapWithEther( + subjectSetToken, + subjectWrappedToken, + wrappedQuantity, + subjectIntegrationName, + ZERO_BYTES + ); + }); + + async function subject(): Promise { + return wrapExtension.connect(subjectCaller.wallet).unwrapWithEther( + subjectSetToken, + subjectWrappedToken, + subjectWrappedTokenUnits, + subjectIntegrationName, + subjectUnwrapData + ); + } + + it("should burn the correct wrapped asset to the SetToken", async () => { + await subject(); + const newWrappedBalance = await wrapAdapterMock.balanceOf(setToken.address); + const expectedTokenBalance = preciseMul(setTokensIssued, wrappedQuantity.sub(subjectWrappedTokenUnits)); + expect(newWrappedBalance).to.eq(expectedTokenBalance); + }); + + it("should properly update the underlying and wrapped token units", async () => { + await subject(); + + const positions = await setToken.getPositions(); + const [receivedWrappedPosition, receivedUnderlyingPosition] = positions; + + expect(positions.length).to.eq(2); + expect(receivedWrappedPosition.component).to.eq(subjectWrappedToken); + expect(receivedWrappedPosition.unit).to.eq(ether(0.5)); + + expect(receivedUnderlyingPosition.component).to.eq(setV2Setup.weth.address); + expect(receivedUnderlyingPosition.unit).to.eq(ether(0.5)); + }); + + it("should have sent the correct quantity of ETH to the SetToken", async () => { + const provider = getProvider(); + const preEthBalance = await provider.getBalance(wrapAdapterMock.address); + + await subject(); + + const postEthBalance = await provider.getBalance(wrapAdapterMock.address); + expect(postEthBalance).to.eq(preEthBalance.sub(preciseMul(subjectWrappedTokenUnits, setTokensIssued))); + }); + + describe("when the caller is not the operator", async () => { + beforeEach(async () => { + subjectCaller = await getRandomAccount(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be approved operator"); + }); + }); + + describe("when the underlying token is not an allowed asset", async () => { + beforeEach(async () => { + await delegatedManager.connect(owner.wallet).removeAllowedAssets([setV2Setup.weth.address]); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Must be allowed asset"); + }); + }); + }); + }); +}); diff --git a/utils/contracts/setV2.ts b/utils/contracts/setV2.ts index f736132f..295237ad 100644 --- a/utils/contracts/setV2.ts +++ b/utils/contracts/setV2.ts @@ -22,6 +22,8 @@ export { SingleIndexModule } from "../../typechain/SingleIndexModule"; export { StreamingFeeModule } from "../../typechain/StreamingFeeModule"; export { UniswapV2ExchangeAdapter } from "../../typechain/UniswapV2ExchangeAdapter"; export { WrapModule } from "../../typechain/WrapModule"; +export { WrapModuleV2 } from "../../typechain/WrapModuleV2"; +export { WrapV2AdapterMock } from "../../typechain/WrapV2AdapterMock"; export { ClaimModule } from "../../typechain/ClaimModule"; export { ClaimAdapterMock } from "../../typechain/ClaimAdapterMock"; export { TradeModule } from "../../typechain/TradeModule"; diff --git a/utils/deploys/deploySetV2.ts b/utils/deploys/deploySetV2.ts index 7cc41183..ed6374ac 100644 --- a/utils/deploys/deploySetV2.ts +++ b/utils/deploys/deploySetV2.ts @@ -28,6 +28,8 @@ import { SingleIndexModule, UniswapV2ExchangeAdapter, WrapModule, + WrapModuleV2, + WrapV2AdapterMock, SlippageIssuanceModule, } from "../contracts/setV2"; import { @@ -65,6 +67,8 @@ import { StandardTokenMock__factory } from "../../typechain/factories/StandardTo import { UniswapV2ExchangeAdapter__factory } from "../../typechain/factories/UniswapV2ExchangeAdapter__factory"; import { WETH9__factory } from "../../typechain/factories/WETH9__factory"; import { WrapModule__factory } from "../../typechain/factories/WrapModule__factory"; +import { WrapModuleV2__factory } from "../../typechain/factories/WrapModuleV2__factory"; +import { WrapV2AdapterMock__factory } from "../../typechain/factories/WrapV2AdapterMock__factory"; import { SlippageIssuanceModule__factory } from "../../typechain/factories/SlippageIssuanceModule__factory"; import { CompoundWrapV2Adapter__factory } from "@typechain/factories/CompoundWrapV2Adapter__factory"; @@ -286,6 +290,10 @@ export default class DeploySetV2 { return await new WrapModule__factory(this._deployerSigner).deploy(controller, weth); } + public async deployWrapModuleV2(controller: Address, weth: Address): Promise { + return await new WrapModuleV2__factory(this._deployerSigner).deploy(controller, weth); + } + public async deploySlippageIssuanceModule(controller: Address): Promise { return await new SlippageIssuanceModule__factory(this._deployerSigner).deploy(controller); } @@ -308,6 +316,10 @@ export default class DeploySetV2 { return await new ClaimAdapterMock__factory(this._deployerSigner).deploy(); } + public async deployWrapV2AdapterMock(): Promise { + return await new WrapV2AdapterMock__factory(this._deployerSigner).deploy(); + } + public async deployClaimModule(controller: Address): Promise { return await new ClaimModule__factory(this._deployerSigner).deploy(controller); } From 5071eb118413b5015ed6070ed27922a693bc0151 Mon Sep 17 00:00:00 2001 From: Pranav Bhardwaj Date: Fri, 8 Sep 2023 14:54:33 -0400 Subject: [PATCH 09/10] add tradeExtension tests --- .../globalTradeExtension.spec.ts | 527 ++++++++++++++++++ 1 file changed, 527 insertions(+) create mode 100644 test/global-extensions/globalTradeExtension.spec.ts diff --git a/test/global-extensions/globalTradeExtension.spec.ts b/test/global-extensions/globalTradeExtension.spec.ts new file mode 100644 index 00000000..82cd9dd2 --- /dev/null +++ b/test/global-extensions/globalTradeExtension.spec.ts @@ -0,0 +1,527 @@ +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, + GlobalTradeExtension, + ManagerCore, + BatchTradeAdapterMock, +} from "@utils/contracts/index"; +import { + SetToken, + TradeModule, +} from "@utils/contracts/setV2"; +import DeployHelper from "@utils/deploys"; +import { + addSnapshotBeforeRestoreAfterEach, + ether, + getAccounts, + getWaffleExpect, + getSetFixture, + getRandomAccount, +} from "@utils/index"; +import { SetFixture } from "@utils/fixtures"; +import { ContractTransaction } from "ethers"; + +const expect = getWaffleExpect(); + +describe("GlobalTradeExtension", () => { + let owner: Account; + let methodologist: Account; + let operator: Account; + let factory: Account; + + let deployer: DeployHelper; + let setToken: SetToken; + let setV2Setup: SetFixture; + + let tradeModule: TradeModule; + + let managerCore: ManagerCore; + let delegatedManager: DelegatedManager; + let tradeExtension: GlobalTradeExtension; + + const tradeAdapterName = "TRADEMOCK"; + let tradeMock: BatchTradeAdapterMock; + + before(async () => { + [ + owner, + methodologist, + operator, + factory, + ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + + setV2Setup = getSetFixture(owner.address); + await setV2Setup.initialize(); + + tradeModule = await deployer.setV2.deployTradeModule(setV2Setup.controller.address); + await setV2Setup.controller.addModule(tradeModule.address); + + tradeMock = await deployer.mocks.deployBatchTradeAdapterMock(); + + await setV2Setup.integrationRegistry.addIntegration( + tradeModule.address, + tradeAdapterName, + tradeMock.address + ); + + managerCore = await deployer.managerCore.deployManagerCore(); + + tradeExtension = await deployer.globalExtensions.deployGlobalTradeExtension( + 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([tradeExtension.address], [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.deployGlobalTradeExtension( + 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"); + }); + }); + }); +}); From 81e86c9d86cb033a7f7e2b31e3ccdd5c26212334 Mon Sep 17 00:00:00 2001 From: Pranav Bhardwaj Date: Fri, 8 Sep 2023 14:56:43 -0400 Subject: [PATCH 10/10] fix test names --- test/global-extensions/globalBatchTradeExtension.spec.ts | 2 +- test/global-extensions/globalClaimExtension.spec.ts | 2 +- test/global-extensions/globalIssuanceExtension.spec.ts | 2 +- test/global-extensions/globalStreamingFeeSplitExtension.spec.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/global-extensions/globalBatchTradeExtension.spec.ts b/test/global-extensions/globalBatchTradeExtension.spec.ts index 08425fd8..293d5a81 100644 --- a/test/global-extensions/globalBatchTradeExtension.spec.ts +++ b/test/global-extensions/globalBatchTradeExtension.spec.ts @@ -28,7 +28,7 @@ import { SetFixture } from "@utils/fixtures"; const expect = getWaffleExpect(); -describe("BatchTradeExtension", () => { +describe("GlobalBatchTradeExtension", () => { let owner: Account; let methodologist: Account; let operator: Account; diff --git a/test/global-extensions/globalClaimExtension.spec.ts b/test/global-extensions/globalClaimExtension.spec.ts index 2e2b461a..b25a9d05 100644 --- a/test/global-extensions/globalClaimExtension.spec.ts +++ b/test/global-extensions/globalClaimExtension.spec.ts @@ -32,7 +32,7 @@ import { SetFixture } from "@utils/fixtures"; const expect = getWaffleExpect(); -describe("ClaimExtension", () => { +describe("GlobalClaimExtension", () => { let owner: Account; let methodologist: Account; let operator: Account; diff --git a/test/global-extensions/globalIssuanceExtension.spec.ts b/test/global-extensions/globalIssuanceExtension.spec.ts index ecca337e..48f2fecb 100644 --- a/test/global-extensions/globalIssuanceExtension.spec.ts +++ b/test/global-extensions/globalIssuanceExtension.spec.ts @@ -27,7 +27,7 @@ import { SetFixture } from "@utils/fixtures"; const expect = getWaffleExpect(); -describe("IssuanceExtension", () => { +describe("GlobalIssuanceExtension", () => { let owner: Account; let methodologist: Account; let operator: Account; diff --git a/test/global-extensions/globalStreamingFeeSplitExtension.spec.ts b/test/global-extensions/globalStreamingFeeSplitExtension.spec.ts index e6a69faa..359bc818 100644 --- a/test/global-extensions/globalStreamingFeeSplitExtension.spec.ts +++ b/test/global-extensions/globalStreamingFeeSplitExtension.spec.ts @@ -34,7 +34,7 @@ import { SetFixture } from "@utils/fixtures"; const expect = getWaffleExpect(); -describe("StreamingFeeSplitExtension", () => { +describe("GlobalStreamingFeeSplitExtension", () => { let owner: Account; let methodologist: Account; let operator: Account;