diff --git a/package.json b/package.json index 3753d8f14..33ff03cb9 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "moment": "^2.22.2", "set-protocol-contracts": "1.4.9-beta", "set-protocol-oracles": "^1.0.16", - "set-protocol-strategies": "^1.1.37", + "set-protocol-strategies": "^1.1.39", "set-protocol-viewers": "^1.0.12", "set-protocol-utils": "^1.1.2", "timekeeper": "^2.1.2", diff --git a/src/api/RebalancingManagerAPI.ts b/src/api/RebalancingManagerAPI.ts index 3602e1d6d..c4b236c26 100644 --- a/src/api/RebalancingManagerAPI.ts +++ b/src/api/RebalancingManagerAPI.ts @@ -17,11 +17,13 @@ 'use strict'; import * as _ from 'lodash'; +import * as setProtocolUtils from 'set-protocol-utils'; import Web3 from 'web3'; import { BigNumber } from '../util'; import { AssetPairManagerWrapper, + AssetPairManagerV2Wrapper, BTCDAIRebalancingManagerWrapper, BTCETHRebalancingManagerWrapper, ETHDAIRebalancingManagerWrapper, @@ -29,12 +31,21 @@ import { MACOStrategyManagerV2Wrapper, MovingAverageOracleWrapper, MedianizerWrapper, + PerformanceFeeCalculatorWrapper, ProtocolViewerWrapper, + RebalancingSetTokenV3Wrapper, SetTokenWrapper, RebalancingSetTokenWrapper, } from '../wrappers'; import { Assertions } from '../assertions'; -import { Address, ManagerType, SetProtocolConfig, Tx } from '../types/common'; +import { + Address, + Bytes, + FeeType, + ManagerType, + SetProtocolConfig, + Tx +} from '../types/common'; import { DAI_FULL_TOKEN_UNITS, DAI_PRICE, @@ -45,12 +56,15 @@ import { } from '../constants'; import { AssetPairManagerDetails, + AssetPairManagerV2Details, BTCDAIRebalancingManagerDetails, BTCETHRebalancingManagerDetails, ETHDAIRebalancingManagerDetails, MovingAverageManagerDetails, } from '../types/strategies'; +const { SetProtocolUtils: SetUtils } = setProtocolUtils; + /** * @title RebalancingManagerAPI * @author Set Protocol @@ -71,6 +85,9 @@ export class RebalancingManagerAPI { private macoStrategyManager: MACOStrategyManagerWrapper; private macoStrategyManagerV2: MACOStrategyManagerV2Wrapper; private assetPairManager: AssetPairManagerWrapper; + private assetPairManagerV2: AssetPairManagerV2Wrapper; + private performanceFeeCalculator: PerformanceFeeCalculatorWrapper; + private rebalancingSetV3: RebalancingSetTokenV3Wrapper; /** * Instantiates a new RebalancingManagerAPI instance that contains methods for issuing and redeeming Sets @@ -86,6 +103,9 @@ export class RebalancingManagerAPI { this.macoStrategyManager = new MACOStrategyManagerWrapper(web3); this.macoStrategyManagerV2 = new MACOStrategyManagerV2Wrapper(web3); this.assetPairManager = new AssetPairManagerWrapper(web3); + this.assetPairManagerV2 = new AssetPairManagerV2Wrapper(web3); + this.performanceFeeCalculator = new PerformanceFeeCalculatorWrapper(web3); + this.rebalancingSetV3 = new RebalancingSetTokenV3Wrapper(web3); this.assert = assertions; this.setToken = new SetTokenWrapper(web3); @@ -172,6 +192,121 @@ export class RebalancingManagerAPI { return await this.macoStrategyManager.confirmPropose(macoManager, txOpts); } + /** + * Calls manager to adjustFees. Only for asset pair manager v2 + * + * @param manager Address of manager + * @param newFeeType Type of fee being changed + * @param newFeePercentage New fee percentage + * @return The hash of the resulting transaction. + */ + public async adjustPerformanceFeesAsync( + manager: Address, + newFeeType: FeeType, + newFeePercentage: BigNumber, + txOpts: Tx, + ): Promise { + await this.assertAdjustFees( + manager, + newFeeType, + newFeePercentage, + txOpts + ); + + const newFeeCallData = SetUtils.generateAdjustFeeCallData( + new BigNumber(newFeeType), + newFeePercentage + ); + + return this.assetPairManagerV2.adjustFee( + manager, + newFeeCallData, + txOpts + ); + } + + /** + * Cancels previous fee adjustment (before enacted) + * + * @param manager Address of manager + * @param upgradeHash Hash of the inital fee adjustment call data + * @return The hash of the resulting transaction. + */ + public async removeFeeUpdateAsync( + manager: Address, + upgradeHash: string, + txOpts: Tx, + ): Promise { + return this.assetPairManagerV2.removeRegisteredUpgrade(manager, upgradeHash); + } + + /** + * Calls AssetPairManagerV2's setLiquidator function. Changes liquidator used in rebalances. + * + * @param manager Address of the asset pair manager contract + * @param newLiquidator New liquidator address + * @param txOpts Transaction options object conforming to `Tx` with signer, gas, and + * gasPrice data + * @return The hash of the resulting transaction. + */ + public async setLiquidatorAsync( + manager: Address, + newLiquidator: Address, + txOpts: Tx, + ): Promise { + await this.assertAddressSetters(manager, newLiquidator); + + return this.assetPairManagerV2.setLiquidator( + manager, + newLiquidator, + txOpts + ); + } + + /** + * Calls AssetPairManagerV2's setLiquidatorData function. Changes liquidatorData used in rebalances. + * + * @param manager Address of the asset pair manager contract + * @param newLiquidatorData New liquidator data + * @param txOpts Transaction options object conforming to `Tx` with signer, gas, and + * gasPrice data + * @return The hash of the resulting transaction. + */ + public async setLiquidatorDataAsync( + manager: Address, + newLiquidatorData: Bytes, + txOpts: Tx, + ): Promise { + return this.assetPairManagerV2.setLiquidatorData( + manager, + newLiquidatorData, + txOpts + ); + } + + /** + * Calls AssetPairManagerV2's setFeeRecipient function. Changes feeRecipient address. + * + * @param manager Address of the asset pair manager contract + * @param newFeeRecipient New feeRecipient address + * @param txOpts Transaction options object conforming to `Tx` with signer, gas, and + * gasPrice data + * @return The hash of the resulting transaction. + */ + public async setFeeRecipientAsync( + manager: Address, + newFeeRecipient: Address, + txOpts: Tx, + ): Promise { + await this.assertAddressSetters(manager, newFeeRecipient); + + return this.assetPairManagerV2.setFeeRecipient( + manager, + newFeeRecipient, + txOpts + ); + } + /** * Fetches if initialPropose can be called without revert on AssetPairManager * @@ -520,6 +655,66 @@ export class RebalancingManagerAPI { } as AssetPairManagerDetails; } + /** + * Fetches the state variables of the Asset Pair Manager V2 contract. + * + * @param manager Address of the AssetPairManagerV2 contract + * @return Object containing the state information related to the manager + */ + public async getAssetPairManagerV2DetailsAsync( + manager: Address + ): Promise { + const [ + allocationDenominator, + allocator, + baseAssetAllocation, + bullishBaseAssetAllocation, + bearishBaseAssetAllocation, + core, + recentInitialProposeTimestamp, + rebalancingSetToken, + ] = await Promise.all([ + this.assetPairManagerV2.allocationDenominator(manager), + this.assetPairManagerV2.allocator(manager), + this.assetPairManagerV2.baseAssetAllocation(manager), + this.assetPairManagerV2.bullishBaseAssetAllocation(manager), + this.assetPairManagerV2.bearishBaseAssetAllocation(manager), + this.assetPairManagerV2.core(manager), + this.assetPairManagerV2.recentInitialProposeTimestamp(manager), + this.assetPairManagerV2.rebalancingSetToken(manager), + ]); + + const [ + signalConfirmationMinTime, + signalConfirmationMaxTime, + trigger, + liquidatorData, + rebalanceFeeCalculator, + ] = await Promise.all([ + this.assetPairManagerV2.signalConfirmationMinTime(manager), + this.assetPairManagerV2.signalConfirmationMaxTime(manager), + this.assetPairManagerV2.trigger(manager), + this.assetPairManagerV2.liquidatorData(manager), + this.rebalancingSetV3.rebalanceFeeCalculator(rebalancingSetToken), + ]); + + return { + allocationDenominator, + allocator, + baseAssetAllocation, + bullishBaseAssetAllocation, + bearishBaseAssetAllocation, + core, + recentInitialProposeTimestamp, + rebalancingSetToken, + signalConfirmationMinTime, + signalConfirmationMaxTime, + trigger, + liquidatorData, + rebalanceFeeCalculator, + } as AssetPairManagerV2Details; + } + /** * Fetches the crossover confirmation time of AssetPairManager contracts. * @@ -789,6 +984,41 @@ export class RebalancingManagerAPI { ); } + private async assertAdjustFees( + manager: Address, + newFeeType: FeeType, + newFeePercentage: BigNumber, + txOpts: Tx + ): Promise { + const managerDetails = await this.getAssetPairManagerV2DetailsAsync(manager); + const feeCalculatorAddress = managerDetails.rebalanceFeeCalculator; + + let maxFee: BigNumber; + if (newFeeType == FeeType.StreamingFee) { + maxFee = await this.performanceFeeCalculator.maximumStreamingFeePercentage( + feeCalculatorAddress + ); + } else { + maxFee = await this.performanceFeeCalculator.maximumProfitFeePercentage( + feeCalculatorAddress + ); + } + + this.assert.common.isGreaterOrEqualThan( + maxFee, + newFeePercentage, + 'Passed fee exceeds allowed maximum.' + ); + } + + private async assertAddressSetters( + manager: Address, + newAddress: Address, + ): Promise { + this.assert.schema.isValidAddress('manager', manager); + this.assert.schema.isValidAddress('newAddress', newAddress); + } + // Helper functions private assertCrossoverTriggerMet( diff --git a/src/types/strategies.ts b/src/types/strategies.ts index 36075e8f3..e8c3ff3e9 100644 --- a/src/types/strategies.ts +++ b/src/types/strategies.ts @@ -51,6 +51,22 @@ export interface AssetPairManagerDetails { trigger: Address; } +export interface AssetPairManagerV2Details { + allocationDenominator: BigNumber; + allocator: Address; + baseAssetAllocation: BigNumber; + bullishBaseAssetAllocation: BigNumber; + bearishBaseAssetAllocation: BigNumber; + core: Address; + recentInitialProposeTimestamp: BigNumber; + rebalancingSetToken: Address; + signalConfirmationMinTime: BigNumber; + signalConfirmationMaxTime: BigNumber; + trigger: Address; + liquidatorData: string; + rebalanceFeeCalculator: Address; +} + export interface BTCETHRebalancingManagerDetails { core: Address; btcPriceFeed: Address; diff --git a/src/wrappers/index.ts b/src/wrappers/index.ts index 105639b63..a52955e90 100644 --- a/src/wrappers/index.ts +++ b/src/wrappers/index.ts @@ -43,6 +43,7 @@ export { WhitelistWrapper } from './set_protocol/WhitelistWrapper'; export { VaultWrapper } from './set_protocol/VaultWrapper'; export { AssetPairManagerWrapper } from './strategies/AssetPairManagerWrapper'; +export { AssetPairManagerV2Wrapper } from './strategies/AssetPairManagerV2Wrapper'; export { BTCDAIRebalancingManagerWrapper } from './strategies/BTCDAIRebalancingManagerWrapper'; export { BTCETHRebalancingManagerWrapper } from './strategies/BTCETHRebalancingManagerWrapper'; export { ETHDAIRebalancingManagerWrapper } from './strategies/ETHDAIRebalancingManagerWrapper'; diff --git a/src/wrappers/set_protocol/RebalancingSetTokenV3Wrapper.ts b/src/wrappers/set_protocol/RebalancingSetTokenV3Wrapper.ts index 6ef8eabe6..6090bce4a 100644 --- a/src/wrappers/set_protocol/RebalancingSetTokenV3Wrapper.ts +++ b/src/wrappers/set_protocol/RebalancingSetTokenV3Wrapper.ts @@ -53,4 +53,18 @@ export class RebalancingSetTokenV3Wrapper { txOpts ); } + + /** + * Get rebalanceFeeCalculator + * + * @param rebalancingSetAddress Address of the Set + * @return Transaction hash + */ + public async rebalanceFeeCalculator( + rebalancingSetAddress: Address, + ): Promise { + const rebalancingSetTokenInstance = await this.contracts.loadRebalancingSetTokenV3Async(rebalancingSetAddress); + + return await rebalancingSetTokenInstance.rebalanceFeeCalculator.callAsync(); + } } \ No newline at end of file diff --git a/src/wrappers/strategies/AssetPairManagerV2Wrapper.ts b/src/wrappers/strategies/AssetPairManagerV2Wrapper.ts new file mode 100644 index 000000000..b884d0e9c --- /dev/null +++ b/src/wrappers/strategies/AssetPairManagerV2Wrapper.ts @@ -0,0 +1,208 @@ +/* + Copyright 2018 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. +*/ + +'use strict'; + +import Web3 from 'web3'; + +import { StrategyContractWrapper } from './StrategyContractWrapper'; +import { BigNumber, generateTxOpts } from '../../util'; +import { Address, Tx } from '../../types/common'; + +/** + * @title AssetPairManagerV2Wrapper + * @author Set Protocol + * + * The AssetPairManagerV2Wrapper handles all functions on the AssetPairManagerV2 contract + * + */ +export class AssetPairManagerV2Wrapper { + protected web3: Web3; + protected contracts: StrategyContractWrapper; + + public constructor(web3: Web3) { + this.web3 = web3; + this.contracts = new StrategyContractWrapper(this.web3); + } + + /** + * Update liquidator used by Rebalancing Set. + * + * @param managerAddress Address of the rebalancing manager contract + * @param newLiquidatorAddress Address of new Liquidator + * @return The hash of the resulting transaction. + */ + public async setLiquidator( + managerAddress: Address, + newLiquidatorAddress: Address, + txOpts?: Tx, + ): Promise { + const assetPairManagerInstance = await this.contracts.loadAssetPairManagerV2ContractAsync(managerAddress); + const txOptions = await generateTxOpts(this.web3, txOpts); + + return await assetPairManagerInstance.setLiquidator.sendTransactionAsync(newLiquidatorAddress, txOptions); + } + + /** + * Update liquidatorData used by Rebalancing Set. + * + * @param managerAddress Address of the rebalancing manager contract + * @param newLiquidatorData New liquidator data in bytes + * @return The hash of the resulting transaction. + */ + public async setLiquidatorData( + managerAddress: Address, + newLiquidatorData: string, + txOpts?: Tx, + ): Promise { + const assetPairManagerInstance = await this.contracts.loadAssetPairManagerV2ContractAsync(managerAddress); + const txOptions = await generateTxOpts(this.web3, txOpts); + + return await assetPairManagerInstance.setLiquidatorData.sendTransactionAsync(newLiquidatorData, txOptions); + } + + /** + * Update fee recipient on the Set. + * + * @param managerAddress Address of the rebalancing manager contract + * @param newFeeRecipient Address of new fee recipient + * @return The hash of the resulting transaction. + */ + public async setFeeRecipient( + managerAddress: Address, + newFeeRecipient: Address, + txOpts?: Tx, + ): Promise { + const assetPairManagerInstance = await this.contracts.loadAssetPairManagerV2ContractAsync(managerAddress); + const txOptions = await generateTxOpts(this.web3, txOpts); + + return await assetPairManagerInstance.setFeeRecipient.sendTransactionAsync(newFeeRecipient, txOptions); + } + + /** + * Calls AssetPairManagerV2's adjustFee function. Allows manager to change performance fees. + * + * @param managerAddress Address of the rebalancing manager contract + * @param newFeeCallData New fee call data + * @return The hash of the resulting transaction. + */ + public async adjustFee( + managerAddress: Address, + newFeeCallData: string, + txOpts?: Tx, + ): Promise { + const assetPairManagerInstance = await this.contracts.loadAssetPairManagerV2ContractAsync(managerAddress); + const txOptions = await generateTxOpts(this.web3, txOpts); + + return await assetPairManagerInstance.adjustFee.sendTransactionAsync( + newFeeCallData, + txOptions + ); + } + + /** + * Calls AssetPairManagerV2's adjustFee function. Allows trader to change performance fees. + * + * @param managerAddress Address of the rebalancing manager contract + * @param upgradeHash Hash of upgrade to be removed + * @return The hash of the resulting transaction. + */ + public async removeRegisteredUpgrade( + managerAddress: Address, + upgradeHash: string, + txOpts?: Tx, + ): Promise { + const assetPairManagerInstance = await this.contracts.loadAssetPairManagerV2ContractAsync(managerAddress); + const txOptions = await generateTxOpts(this.web3, txOpts); + + return await assetPairManagerInstance.removeRegisteredUpgrade.sendTransactionAsync( + upgradeHash, + txOptions + ); + } + + public async core(managerAddress: Address): Promise
{ + const assetPairManagerInstance = await this.contracts.loadAssetPairManagerV2ContractAsync(managerAddress); + + return await assetPairManagerInstance.core.callAsync(); + } + + public async allocator(managerAddress: Address): Promise
{ + const assetPairManagerInstance = await this.contracts.loadAssetPairManagerV2ContractAsync(managerAddress); + + return await assetPairManagerInstance.allocator.callAsync(); + } + + public async trigger(managerAddress: Address): Promise
{ + const assetPairManagerInstance = await this.contracts.loadAssetPairManagerV2ContractAsync(managerAddress); + + return await assetPairManagerInstance.trigger.callAsync(); + } + + public async rebalancingSetToken(managerAddress: Address): Promise
{ + const assetPairManagerInstance = await this.contracts.loadAssetPairManagerV2ContractAsync(managerAddress); + + return await assetPairManagerInstance.rebalancingSetToken.callAsync(); + } + + public async baseAssetAllocation(managerAddress: Address): Promise { + const assetPairManagerInstance = await this.contracts.loadAssetPairManagerV2ContractAsync(managerAddress); + + return await assetPairManagerInstance.baseAssetAllocation.callAsync(); + } + + public async allocationDenominator(managerAddress: Address): Promise { + const assetPairManagerInstance = await this.contracts.loadAssetPairManagerV2ContractAsync(managerAddress); + + return await assetPairManagerInstance.allocationDenominator.callAsync(); + } + + public async bullishBaseAssetAllocation(managerAddress: Address): Promise { + const assetPairManagerInstance = await this.contracts.loadAssetPairManagerV2ContractAsync(managerAddress); + + return await assetPairManagerInstance.bullishBaseAssetAllocation.callAsync(); + } + + public async bearishBaseAssetAllocation(managerAddress: Address): Promise { + const assetPairManagerInstance = await this.contracts.loadAssetPairManagerV2ContractAsync(managerAddress); + + return await assetPairManagerInstance.bearishBaseAssetAllocation.callAsync(); + } + + public async signalConfirmationMinTime(managerAddress: Address): Promise { + const assetPairManagerInstance = await this.contracts.loadAssetPairManagerV2ContractAsync(managerAddress); + + return await assetPairManagerInstance.signalConfirmationMinTime.callAsync(); + } + + public async signalConfirmationMaxTime(managerAddress: Address): Promise { + const assetPairManagerInstance = await this.contracts.loadAssetPairManagerV2ContractAsync(managerAddress); + + return await assetPairManagerInstance.signalConfirmationMaxTime.callAsync(); + } + + public async recentInitialProposeTimestamp(managerAddress: Address): Promise { + const assetPairManagerInstance = await this.contracts.loadAssetPairManagerV2ContractAsync(managerAddress); + + return await assetPairManagerInstance.recentInitialProposeTimestamp.callAsync(); + } + + public async liquidatorData(managerAddress: Address): Promise { + const assetPairManagerInstance = await this.contracts.loadAssetPairManagerV2ContractAsync(managerAddress); + + return await assetPairManagerInstance.liquidatorData.callAsync(); + } +} diff --git a/src/wrappers/strategies/StrategyContractWrapper.ts b/src/wrappers/strategies/StrategyContractWrapper.ts index 0c0b24498..99ad555f2 100644 --- a/src/wrappers/strategies/StrategyContractWrapper.ts +++ b/src/wrappers/strategies/StrategyContractWrapper.ts @@ -28,6 +28,7 @@ import { import { AssetPairManagerContract, + AssetPairManagerV2Contract, BaseContract as StrategyBaseContract, BTCDaiRebalancingManagerContract, BTCETHRebalancingManagerContract, @@ -316,6 +317,32 @@ export class StrategyContractWrapper { } } + /** + * Load a AssetPairManagerV2 contract + * + * @param assetPairManagerV2 Address of the AssetPairManagerV2 contract + * @param transactionOptions Options sent into the contract deployed method + * @return The AssetPairManagerV2 Contract + */ + public async loadAssetPairManagerV2ContractAsync( + assetPairManagerV2: Address, + transactionOptions: object = {}, + ): Promise { + const cacheKey = `assetPairManagerV2_${assetPairManagerV2}`; + + if (cacheKey in this.cache) { + return this.cache[cacheKey] as AssetPairManagerV2Contract; + } else { + const assetPairManagerV2Contract = await AssetPairManagerV2Contract.at( + assetPairManagerV2, + this.web3, + transactionOptions, + ); + this.cache[cacheKey] = assetPairManagerV2Contract; + return assetPairManagerV2Contract; + } + } + /** * Load a SocialTradingManager contract * diff --git a/test/helpers/index.ts b/test/helpers/index.ts index dc6af9c8a..d7eec139c 100644 --- a/test/helpers/index.ts +++ b/test/helpers/index.ts @@ -88,6 +88,7 @@ export { export { deployAssetPairManagerAsync, + deployAssetPairManagerV2Async, deployBinaryAllocatorAsync, deployBtcDaiManagerContractAsync, deployBtcEthManagerContractAsync, diff --git a/test/helpers/strategyHelpers.ts b/test/helpers/strategyHelpers.ts index 983c994f7..a5ed8614c 100644 --- a/test/helpers/strategyHelpers.ts +++ b/test/helpers/strategyHelpers.ts @@ -4,6 +4,8 @@ import { Address, Bytes } from 'set-protocol-utils'; import { AssetPairManager, AssetPairManagerContract, + AssetPairManagerV2, + AssetPairManagerV2Contract, BinaryAllocator, BinaryAllocatorContract, BTCDaiRebalancingManager, @@ -258,6 +260,38 @@ export const deployAssetPairManagerAsync = async( ); }; +export const deployAssetPairManagerV2Async = async( + web3: Web3, + coreInstance: Address, + allocatorInstance: Address, + triggerInstance: Address, + useBullishAssetAllocation: boolean, + allocationPrecision: BigNumber, + bullishBaseAssetAllocation: BigNumber, + signalConfirmationMinTime: BigNumber, + signalConfirmationMaxTime: BigNumber, + liquidatorData: Bytes, +): Promise => { + const truffleAssetPairManager = setDefaultTruffleContract(web3, AssetPairManagerV2); + + // Deploy MACO Strategy Manager V2 + const deployedAssetPairManagerInstance = await truffleAssetPairManager.new( + coreInstance, + allocatorInstance, + triggerInstance, + useBullishAssetAllocation, + allocationPrecision, + bullishBaseAssetAllocation, + [signalConfirmationMinTime, signalConfirmationMaxTime], + liquidatorData, + ); + return await AssetPairManagerV2Contract.at( + deployedAssetPairManagerInstance.address, + web3, + TX_DEFAULTS, + ); +}; + export const deploySocialTradingManagerAsync = async( web3: Web3, core: Address, @@ -406,7 +440,8 @@ export const deployRSITrendingTriggerAsync = async( }; export const initializeManagerAsync = async( - macoManager: MACOStrategyManagerContract | MACOStrategyManagerV2Contract | AssetPairManagerContract, + macoManager: + MACOStrategyManagerContract | MACOStrategyManagerV2Contract | AssetPairManagerContract | AssetPairManagerV2Contract, rebalancingSetTokenAddress: Address, ): Promise => { await macoManager.initialize.sendTransactionAsync(rebalancingSetTokenAddress); diff --git a/test/integration/api/RebalancingManagerAPI.spec.ts b/test/integration/api/RebalancingManagerAPI.spec.ts index 45cb4caf9..5a8dc69f4 100644 --- a/test/integration/api/RebalancingManagerAPI.spec.ts +++ b/test/integration/api/RebalancingManagerAPI.spec.ts @@ -32,9 +32,12 @@ import Web3 from 'web3'; import { CoreContract, ConstantAuctionPriceCurveContract, + FeeCalculatorHelper, SetTokenContract, RebalancingSetTokenContract, + RebalancingSetTokenV3Contract, RebalancingSetTokenFactoryContract, + RebalancingSetTokenV3FactoryContract, SetTokenFactoryContract, StandardTokenMockContract, TransferProxyContract, @@ -43,6 +46,7 @@ import { import { AssetPairManagerContract, + AssetPairManagerV2Contract, BinaryAllocatorContract, BTCDaiRebalancingManagerContract, BTCETHRebalancingManagerContract, @@ -63,6 +67,10 @@ import { TimeSeriesFeedContract, } from 'set-protocol-oracles'; +import { + ProtocolViewerContract, +} from 'set-protocol-viewers'; + import ChaiSetup from '@test/helpers/chaiSetup'; import { DEFAULT_ACCOUNT } from '@src/constants/accounts'; import { Assertions } from '@src/assertions'; @@ -74,8 +82,10 @@ import { NULL_ADDRESS, ONE_DAY_IN_SECONDS, ONE_HOUR_IN_SECONDS, + ONE_WEEK_IN_SECONDS, TX_DEFAULTS, - ZERO + ZERO, + ZERO_BYTES } from '@src/constants'; import { ACCOUNTS } from '@src/constants/accounts'; import { @@ -85,7 +95,9 @@ import { approveContractToOracleProxy, approveForTransferAsync, createDefaultRebalancingSetTokenAsync, + createDefaultRebalancingSetTokenV3Async, deployAssetPairManagerAsync, + deployAssetPairManagerV2Async, deployBaseContracts, deployBinaryAllocatorAsync, deployBtcDaiManagerContractAsync, @@ -95,6 +107,7 @@ import { deployEthDaiManagerContractAsync, deployHistoricalPriceFeedAsync, deployLegacyMakerOracleAdapterAsync, + deployLinearAuctionLiquidatorContractAsync, deployLinearizedPriceDataSourceAsync, deployMedianizerAsync, deployMovingAverageOracleAsync, @@ -102,18 +115,27 @@ import { deployMovingAverageStrategyManagerAsync, deployMovingAverageStrategyManagerV2Async, deployOracleProxyAsync, + deployOracleWhiteListAsync, deployProtocolViewerAsync, + deployRebalancingSetTokenV3FactoryContractAsync, deployRSIOracleAsync, deployRSITrendingTriggerAsync, deploySetTokenAsync, deployTimeSeriesFeedAsync, deployTokensSpecifyingDecimals, + deployWhiteListContract, increaseChainTimeAsync, initializeManagerAsync, updateMedianizerPriceAsync, } from '@test/helpers'; -import { BigNumber } from '@src/util'; -import { Address, ManagerType, SetProtocolConfig } from '@src/types/common'; +import { BigNumber, ether } from '@src/util'; +import { + Address, + Bytes, + FeeType, + ManagerType, + SetProtocolConfig +} from '@src/types/common'; import { AssetPairManagerWrapper, BTCDAIRebalancingManagerWrapper, @@ -123,6 +145,7 @@ import { } from '@src/wrappers'; import { AssetPairManagerDetails, + AssetPairManagerV2Details, BTCDAIRebalancingManagerDetails, BTCETHRebalancingManagerDetails, ETHDAIRebalancingManagerDetails, @@ -160,6 +183,7 @@ describe('RebalancingManagerAPI', () => { let dai: StandardTokenMockContract; let usdc: StandardTokenMockContract; let whitelist: WhiteListContract; + let protocolViewer: ProtocolViewerContract; let rebalancingManagerAPI: RebalancingManagerAPI; @@ -219,7 +243,7 @@ describe('RebalancingManagerAPI', () => { constantAuctionPriceCurve.address, ); - const protocolViewer = await deployProtocolViewerAsync(web3); + protocolViewer = await deployProtocolViewerAsync(web3); const setProtocolConfig: SetProtocolConfig = { coreAddress: NULL_ADDRESS, @@ -2578,6 +2602,494 @@ describe('RebalancingManagerAPI', () => { }); }); + describe('AssetPairManagerV2', async () => { + let assetPairManagerV2: AssetPairManagerV2Contract; + let allocator: BinaryAllocatorContract; + let trigger: RSITrendingTriggerContract; + let ethOracleProxy: ConstantPriceOracleContract; + let usdcOracle: ConstantPriceOracleContract; + let rsiOracle: RSIOracleContract; + let initialQuoteCollateral: SetTokenContract; + let initialBaseCollateral: SetTokenContract; + let rebalancingSetToken: RebalancingSetTokenV3Contract; + let rebalancingFactory: RebalancingSetTokenV3FactoryContract; + let seededPriceFeedPrices: BigNumber[]; + + let liquidatorData: Bytes; + let rebalanceFeeCalculator: Address; + let newLiquidatorAddress: Address; + + const priceFeedDataDescription: string = 'ETHRSIValue'; + + const useBullishBaseAssetAllocation = true; + + const stableCollateralUnit = new BigNumber(250); + const stableCollateralNaturalUnit = new BigNumber(10 ** 12); + + const riskCollateralUnit = new BigNumber(10 ** 6); + const riskCollateralNaturalUnit = new BigNumber(10 ** 6); + const initializedProposalTimestamp = new BigNumber(0); + + const baseAssetAllocation = new BigNumber(100); + const allocationDenominator = new BigNumber(100); + const bullishBaseAssetAllocation = new BigNumber(100); + const bearishBaseAssetAllocation = allocationDenominator.sub(bullishBaseAssetAllocation); + + const signalConfirmationMinTime = ONE_HOUR_IN_SECONDS.mul(6); + const signalConfirmationMaxTime = ONE_HOUR_IN_SECONDS.mul(12); + + const feeCalculatorHelper = new FeeCalculatorHelper(DEFAULT_ACCOUNT); + + beforeAll(async () => { + seededPriceFeedPrices = _.map(new Array(15), function(el, i) {return new BigNumber((170 - i) * 10 ** 18); }); + }); + + beforeEach(async () => { + const initialMedianizerEthPrice: BigNumber = E18.mul(70); + ethOracleProxy = await deployConstantPriceOracleAsync( + web3, + initialMedianizerEthPrice + ); + + usdcOracle = await deployConstantPriceOracleAsync( + web3, + new BigNumber(10 ** 18) + ); + + const dataSource = await deployLinearizedPriceDataSourceAsync( + web3, + ethOracleProxy.address, + ONE_HOUR_IN_SECONDS, + '' + ); + + const timeSeriesFeed = await deployTimeSeriesFeedAsync( + web3, + dataSource.address, + seededPriceFeedPrices + ); + + rsiOracle = await deployRSIOracleAsync( + web3, + timeSeriesFeed.address, + priceFeedDataDescription + ); + + const oracleWhiteList = await deployOracleWhiteListAsync( + web3, + [wrappedETH.address, usdc.address], + [ethOracleProxy.address, usdcOracle.address], + ); + + const liquidator = await deployLinearAuctionLiquidatorContractAsync( + web3, + core, + oracleWhiteList + ); + const newLiquidator = await deployLinearAuctionLiquidatorContractAsync( + web3, + core, + oracleWhiteList + ); + + newLiquidatorAddress = newLiquidator.address; + + const liquidatorWhiteList = await deployWhiteListContract(web3, [liquidator.address, newLiquidator.address]); + + const maxProfitFeePercentage = ether(.4); + const maxStreamingFeePercentage = ether(.07); + const performanceFeeCalculator = await feeCalculatorHelper.deployPerformanceFeeCalculatorAsync( + core.address, + oracleWhiteList.address, + maxProfitFeePercentage, + maxStreamingFeePercentage, + ); + + const feeCalculatorWhiteList = await deployWhiteListContract(web3, [performanceFeeCalculator.address]); + + rebalancingFactory = await deployRebalancingSetTokenV3FactoryContractAsync( + web3, + core, + whitelist, + liquidatorWhiteList, + feeCalculatorWhiteList + ); + + // Create Stable Collateral Set + initialQuoteCollateral = await deploySetTokenAsync( + web3, + core, + factory.address, + [usdc.address], + [stableCollateralUnit], + stableCollateralNaturalUnit, + ); + + // Create Risk Collateral Set + initialBaseCollateral = await deploySetTokenAsync( + web3, + core, + factory.address, + [wrappedETH.address], + [riskCollateralUnit], + riskCollateralNaturalUnit, + ); + + allocator = await deployBinaryAllocatorAsync( + web3, + wrappedETH.address, + usdc.address, + ethOracleProxy.address, + usdcOracle.address, + initialBaseCollateral.address, + initialQuoteCollateral.address, + core.address, + factory.address + ); + + const lowerBound = new BigNumber(40); + const upperBound = new BigNumber(60); + const rsiTimePeriod = new BigNumber(14); + trigger = await deployRSITrendingTriggerAsync( + web3, + rsiOracle.address, + lowerBound, + upperBound, + rsiTimePeriod + ); + + liquidatorData = ZERO_BYTES; + assetPairManagerV2 = await deployAssetPairManagerV2Async( + web3, + core.address, + allocator.address, + trigger.address, + useBullishBaseAssetAllocation, + allocationDenominator, + bullishBaseAssetAllocation, + signalConfirmationMinTime, + signalConfirmationMaxTime, + liquidatorData + ); + + await assetPairManagerV2.setTimeLockPeriod.sendTransactionAsync(ONE_DAY_IN_SECONDS, { from: DEFAULT_ACCOUNT }); + + // Deploy a RB Set + const rbSetFeeRecipient = ACCOUNTS[2].address; + rebalanceFeeCalculator = performanceFeeCalculator.address; + const failRebalancePeriod = ONE_DAY_IN_SECONDS; + const { timestamp } = await web3.eth.getBlock('latest'); + const lastRebalanceTimestamp = new BigNumber(timestamp); + const rbSetEntryFee = ether(.01); + const rbSetProfitFee = ether(.2); + const rbSetStreamingFee = ether(.02); + rebalancingSetToken = await createDefaultRebalancingSetTokenV3Async( + web3, + core, + rebalancingFactory.address, + assetPairManagerV2.address, + liquidator.address, + rbSetFeeRecipient, + rebalanceFeeCalculator, + initialBaseCollateral.address, + failRebalancePeriod, + lastRebalanceTimestamp, + rbSetEntryFee, + rbSetProfitFee, + rbSetStreamingFee + ); + + await initializeManagerAsync( + assetPairManagerV2, + rebalancingSetToken.address + ); + }); + + describe('getAssetPairManagerV2DetailsAsync', async () => { + let subjectManagerAddress: Address; + + beforeEach(async () => { + subjectManagerAddress = assetPairManagerV2.address; + }); + + async function subject(): Promise { + return await rebalancingManagerAPI.getAssetPairManagerV2DetailsAsync( + subjectManagerAddress, + ); + } + + test('gets the correct core address', async () => { + const details = await subject(); + expect(details.core).to.equal(core.address); + }); + + test('gets the correct allocation precision', async () => { + const details = await subject(); + expect(details.allocationDenominator).to.be.bignumber.equal(allocationDenominator); + }); + + test('gets the correct allocator address', async () => { + const details = await subject(); + expect(details.allocator).to.equal(allocator.address); + }); + + test('gets the correct baseAssetAllocation', async () => { + const details = await subject(); + expect(details.baseAssetAllocation).to.bignumber.equal(baseAssetAllocation); + }); + + test('gets the correct bullishBaseAssetAllocation', async () => { + const details = await subject(); + expect(details.bullishBaseAssetAllocation).to.bignumber.equal(bullishBaseAssetAllocation); + }); + + test('gets the correct bearishBaseAssetAllocation', async () => { + const details = await subject(); + expect(details.bearishBaseAssetAllocation).to.bignumber.equal(bearishBaseAssetAllocation); + }); + + test('gets the correct recentInitialProposeTimestamp', async () => { + const details = await subject(); + expect(details.recentInitialProposeTimestamp).to.bignumber.equal(initializedProposalTimestamp); + }); + + test('gets the correct rebalancingSetToken address', async () => { + const details = await subject(); + expect(details.rebalancingSetToken).to.equal(rebalancingSetToken.address); + }); + + test('gets the correct signalConfirmationMinTime', async () => { + const details = await subject(); + expect(details.signalConfirmationMinTime).to.bignumber.equal(signalConfirmationMinTime); + }); + + test('gets the correct signalConfirmationMaxTime', async () => { + const details = await subject(); + expect(details.signalConfirmationMaxTime).to.bignumber.equal(signalConfirmationMaxTime); + }); + + test('gets the correct trigger address', async () => { + const details = await subject(); + expect(details.trigger).to.equal(trigger.address); + }); + + test('gets the correct liquidatorData', async () => { + const details = await subject(); + expect(details.liquidatorData).to.equal(liquidatorData); + }); + + test('gets the correct rebalance fee calculator address', async () => { + const details = await subject(); + expect(details.rebalanceFeeCalculator).to.equal(rebalanceFeeCalculator); + }); + }); + + describe('adjustPerformanceFeesAsync', async () => { + let subjectManager: Address; + let subjectFeeType: FeeType; + let subjectFeePercentage: BigNumber; + let subjectCaller: Address; + + beforeEach(async () => { + subjectManager = assetPairManagerV2.address; + subjectFeeType = FeeType.StreamingFee; + subjectFeePercentage = ether(.03); + subjectCaller = DEFAULT_ACCOUNT; + + // Issue currentSetToken + await core.issue.sendTransactionAsync( + initialBaseCollateral.address, + ether(9), + {from: DEFAULT_ACCOUNT }, + ); + await approveForTransferAsync([initialBaseCollateral], transferProxy.address); + + // Use issued currentSetToken to issue rebalancingSetToken + await core.issue.sendTransactionAsync( + rebalancingSetToken.address, + ether(7), + {from: DEFAULT_ACCOUNT }, + ); + }); + + async function subject(): Promise { + return await rebalancingManagerAPI.adjustPerformanceFeesAsync( + subjectManager, + subjectFeeType, + subjectFeePercentage, + { from: subjectCaller } + ); + } + + test('successfully initiates adjustFee process', async () => { + const txHash = await subject(); + const { blockHash, input } = await web3.eth.getTransaction(txHash); + const { timestamp } = await web3.eth.getBlock(blockHash as any); + + const upgradeHash = web3.utils.soliditySha3(input); + const actualTimestamp = await assetPairManagerV2.timeLockedUpgrades.callAsync(upgradeHash); + expect(actualTimestamp).to.bignumber.equal(timestamp); + }); + + describe('the confirmation transaction goes through', async () => { + beforeEach(async () => { + await subject(); + + await increaseChainTimeAsync(web3, ONE_WEEK_IN_SECONDS); + }); + + test('successfully initiates adjustFee process', async () => { + await subject(); + const [feeState] = + await protocolViewer.batchFetchTradingPoolFeeState.callAsync([rebalancingSetToken.address]); + + expect(feeState.streamingFeePercentage).to.be.bignumber.equal(subjectFeePercentage); + }); + }); + + describe('when the passed profitFee is greater than maximum', async () => { + beforeEach(async () => { + subjectFeePercentage = ether(.5); + subjectFeeType = FeeType.ProfitFee; + }); + + test('throws', async () => { + return expect(subject()).to.be.rejectedWith( + `Passed fee exceeds allowed maximum.` + ); + }); + }); + + describe('when the passed streamingFee is greater than maximum', async () => { + beforeEach(async () => { + subjectFeePercentage = ether(.1); + }); + + test('throws', async () => { + return expect(subject()).to.be.rejectedWith( + `Passed fee exceeds allowed maximum.` + ); + }); + }); + }); + + describe('removeFeeUpdateAsync', async () => { + let subjectManager: Address; + let subjectUpgradeHash: string; + let subjectCaller: Address; + + beforeEach(async () => { + const feeType = FeeType.StreamingFee; + const feePercentage = ether(.03); + + const adjustTxHash = await rebalancingManagerAPI.adjustPerformanceFeesAsync( + assetPairManagerV2.address, + feeType, + feePercentage, + { from: DEFAULT_ACCOUNT } + ); + + const { input } = await web3.eth.getTransaction(adjustTxHash); + subjectUpgradeHash = web3.utils.soliditySha3(input); + subjectManager = assetPairManagerV2.address; + subjectCaller = DEFAULT_ACCOUNT; + }); + + async function subject(): Promise { + return await rebalancingManagerAPI.removeFeeUpdateAsync( + subjectManager, + subjectUpgradeHash, + { from: subjectCaller } + ); + } + + test('successfully removes upgradeHash', async () => { + await subject(); + + const actualTimestamp = await assetPairManagerV2.timeLockedUpgrades.callAsync(subjectUpgradeHash); + expect(actualTimestamp).to.bignumber.equal(ZERO); + }); + }); + + describe('setLiquidatorAsync', async () => { + let subjectNewLiquidator: Address; + let subjectManagerAddress: Address; + + beforeEach(async () => { + subjectManagerAddress = assetPairManagerV2.address; + subjectNewLiquidator = newLiquidatorAddress; + }); + + async function subject(): Promise { + return await rebalancingManagerAPI.setLiquidatorAsync( + subjectManagerAddress, + subjectNewLiquidator, + { from: DEFAULT_ACCOUNT } + ); + } + + test('sets the new liquidator correctly', async () => { + await subject(); + + const actualLiquidator = await rebalancingSetToken.liquidator.callAsync(); + + expect(actualLiquidator).to.equal(subjectNewLiquidator); + }); + }); + + describe('setLiquidatorDataAsync', async () => { + let subjectLiquidatorData: Bytes; + let subjectManagerAddress: Address; + + beforeEach(async () => { + subjectManagerAddress = assetPairManagerV2.address; + subjectLiquidatorData = '0x0000000000000000000000000000000000000000000000000000000000000005'; + }); + + async function subject(): Promise { + return await rebalancingManagerAPI.setLiquidatorDataAsync( + subjectManagerAddress, + subjectLiquidatorData, + { from: DEFAULT_ACCOUNT } + ); + } + + test('sets the new liquidatorData correctly', async () => { + await subject(); + + const actualLiquidatorData = await assetPairManagerV2.liquidatorData.callAsync(); + + expect(actualLiquidatorData).to.equal(subjectLiquidatorData); + }); + }); + + describe('setFeeRecipientAsync', async () => { + let subjectFeeRecipient: string; + let subjectManagerAddress: Address; + + beforeEach(async () => { + subjectManagerAddress = assetPairManagerV2.address; + subjectFeeRecipient = ACCOUNTS[3].address; + }); + + async function subject(): Promise { + return await rebalancingManagerAPI.setFeeRecipientAsync( + subjectManagerAddress, + subjectFeeRecipient, + { from: DEFAULT_ACCOUNT } + ); + } + + test('sets the new fee recipient correctly', async () => { + await subject(); + + const actualNewFeeRecipient = await rebalancingSetToken.feeRecipient.callAsync(); + + expect(actualNewFeeRecipient).to.equal(subjectFeeRecipient); + }); + }); + }); + describe('BTCDAIRebalancingManager', async () => { let btcDaiRebalancingManager: BTCDaiRebalancingManagerContract; let btcMultiplier: BigNumber; diff --git a/test/integration/wrappers/set_protocol/RebalancingSetTokenV3Wrapper.spec.ts b/test/integration/wrappers/set_protocol/RebalancingSetTokenV3Wrapper.spec.ts index ce06b6e60..b632729c4 100644 --- a/test/integration/wrappers/set_protocol/RebalancingSetTokenV3Wrapper.spec.ts +++ b/test/integration/wrappers/set_protocol/RebalancingSetTokenV3Wrapper.spec.ts @@ -284,4 +284,24 @@ describe('RebalancingSetTokenV3Wrapper', () => { expect(feeState.lastProfitFeeTimestamp).to.be.bignumber.equal(timestamp); }); }); + + describe('rebalanceFeeCalculator', async () => { + let subjectRebalancingSetTokenV3: Address; + + beforeEach(async () => { + subjectRebalancingSetTokenV3 = rebalancingSetToken.address; + }); + + async function subject(): Promise
{ + return await rebalancingSetTokenV3Wrapper.rebalanceFeeCalculator( + subjectRebalancingSetTokenV3, + ); + } + + test('successfully returns the rebalanceFeeCalculator address', async () => { + const returnedRebalanceFeeCalculator = await subject(); + + expect(returnedRebalanceFeeCalculator).to.be.bignumber.equal(performanceFeeCalculator.address); + }); + }); }); diff --git a/test/integration/wrappers/strategies/AssetPairManagerV2Wrapper.spec.ts b/test/integration/wrappers/strategies/AssetPairManagerV2Wrapper.spec.ts new file mode 100644 index 000000000..bd88ab861 --- /dev/null +++ b/test/integration/wrappers/strategies/AssetPairManagerV2Wrapper.spec.ts @@ -0,0 +1,726 @@ +/* + 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. +*/ + +'use strict'; + +// Given that this is an integration test, we unmock the Set Protocol +// smart contracts artifacts package to pull the most recently +// deployed contracts on the current network. +jest.unmock('set-protocol-contracts'); +jest.setTimeout(30000); + +import * as _ from 'lodash'; +import * as ABIDecoder from 'abi-decoder'; +import * as chai from 'chai'; +import * as setProtocolUtils from 'set-protocol-utils'; +import Web3 from 'web3'; +import { + CoreContract, + FeeCalculatorHelper, + SetTokenContract, + RebalancingSetTokenV3Contract, + RebalancingSetTokenV3FactoryContract, + SetTokenFactoryContract, + StandardTokenMockContract, + TransferProxyContract, + WhiteListContract, +} from 'set-protocol-contracts'; + +import { + AssetPairManagerV2Contract, + BinaryAllocatorContract, + RSITrendingTriggerContract +} from 'set-protocol-strategies'; + +import { + ConstantPriceOracleContract, + RSIOracleContract, +} from 'set-protocol-oracles'; + +import { ACCOUNTS, DEFAULT_ACCOUNT } from '@src/constants/accounts'; +import { AssetPairManagerV2Wrapper } from '@src/wrappers'; +import { + E18, + TX_DEFAULTS, + ONE_DAY_IN_SECONDS, + ONE_HOUR_IN_SECONDS, + ZERO, + ZERO_BYTES +} from '@src/constants'; +import { + addWhiteListedTokenAsync, + approveForTransferAsync, + createDefaultRebalancingSetTokenV3Async, + deployAssetPairManagerV2Async, + deployBaseContracts, + deployBinaryAllocatorAsync, + deployConstantPriceOracleAsync, + deployLinearAuctionLiquidatorContractAsync, + deployLinearizedPriceDataSourceAsync, + deployOracleWhiteListAsync, + deployRebalancingSetTokenV3FactoryContractAsync, + deployRSIOracleAsync, + deployRSITrendingTriggerAsync, + deploySetTokenAsync, + deployTimeSeriesFeedAsync, + deployTokensSpecifyingDecimals, + deployWhiteListContract, + initializeManagerAsync, +} from '@test/helpers'; +import { + BigNumber, + ether +} from '@src/util'; +import { Address, Bytes } from '@src/types/common'; + +const Core = require('set-protocol-contracts/dist/artifacts/ts/Core').Core; + +const chaiBigNumber = require('chai-bignumber'); +chai.use(chaiBigNumber(BigNumber)); +const { expect } = chai; +const contract = require('truffle-contract'); +const web3 = new Web3('http://localhost:8545'); +const { Web3Utils } = setProtocolUtils; +const web3Utils = new Web3Utils(web3); + +const coreContract = contract(Core); +coreContract.setProvider(web3.currentProvider); +coreContract.defaults(TX_DEFAULTS); + +let currentSnapshotId: number; + +describe('AssetPairManagerV2Wrapper', () => { + let core: CoreContract; + let transferProxy: TransferProxyContract; + let factory: SetTokenFactoryContract; + let rebalancingFactory: RebalancingSetTokenV3FactoryContract; + let rebalancingComponentWhiteList: WhiteListContract; + let assetPairManagerV2: AssetPairManagerV2Contract; + let allocator: BinaryAllocatorContract; + let trigger: RSITrendingTriggerContract; + + let rsiOracle: RSIOracleContract; + let ethOracleProxy: ConstantPriceOracleContract; + let usdcOracle: ConstantPriceOracleContract; + + let usdc: StandardTokenMockContract; + let wrappedETH: StandardTokenMockContract; + let initialQuoteCollateral: SetTokenContract; + let initialBaseCollateral: SetTokenContract; + let rebalancingSetToken: RebalancingSetTokenV3Contract; + + let seededPriceFeedPrices: BigNumber[]; + + let newLiquidatorAddress: Address; + + let assetPairManagerV2Wrapper: AssetPairManagerV2Wrapper; + + const useBullishBaseAssetAllocation = true; + const allocationDenominator = new BigNumber(100); + const bullishBaseAssetAllocation = new BigNumber(100); + const bearishBaseAssetAllocation = allocationDenominator.sub(bullishBaseAssetAllocation); + const signalConfirmationMinTime = ONE_HOUR_IN_SECONDS.mul(6); + const signalConfirmationMaxTime = ONE_HOUR_IN_SECONDS.mul(12); + + const initialMedianizerEthPrice: BigNumber = E18; + const priceFeedDataDescription: string = '200DailyETHPrice'; + + const rsiTimePeriod = new BigNumber(14); + const lowerBound = new BigNumber(40); + const upperBound = new BigNumber(60); + + const quoteCollateralUnit = new BigNumber(250); + const quoteCollateralNaturalUnit = new BigNumber(10 ** 12); + + const baseCollateralUnit = new BigNumber(10 ** 6); + const baseCollateralNaturalUnit = new BigNumber(10 ** 6); + + const feeCalculatorHelper = new FeeCalculatorHelper(DEFAULT_ACCOUNT); + + beforeAll(() => { + ABIDecoder.addABI(coreContract.abi); + seededPriceFeedPrices = _.map(new Array(15), function(el, i) {return new BigNumber((170 - i) * 10 ** 18); }); + }); + + afterAll(() => { + ABIDecoder.removeABI(coreContract.abi); + }); + + beforeEach(async () => { + currentSnapshotId = await web3Utils.saveTestSnapshot(); + + [ + core, + transferProxy, , + factory, + , + , + rebalancingComponentWhiteList, + ] = await deployBaseContracts(web3); + + ethOracleProxy = await deployConstantPriceOracleAsync( + web3, + initialMedianizerEthPrice + ); + + usdcOracle = await deployConstantPriceOracleAsync( + web3, + new BigNumber(10 ** 18) + ); + + const dataSource = await deployLinearizedPriceDataSourceAsync( + web3, + ethOracleProxy.address, + ONE_HOUR_IN_SECONDS, + '' + ); + + const timeSeriesFeed = await deployTimeSeriesFeedAsync( + web3, + dataSource.address, + seededPriceFeedPrices + ); + + rsiOracle = await deployRSIOracleAsync( + web3, + timeSeriesFeed.address, + priceFeedDataDescription + ); + + [usdc, wrappedETH] = await deployTokensSpecifyingDecimals(2, [6, 18], web3, DEFAULT_ACCOUNT); + await approveForTransferAsync( + [usdc, wrappedETH], + transferProxy.address + ); + await addWhiteListedTokenAsync( + rebalancingComponentWhiteList, + usdc.address, + ); + await addWhiteListedTokenAsync( + rebalancingComponentWhiteList, + wrappedETH.address, + ); + + const oracleWhiteList = await deployOracleWhiteListAsync( + web3, + [wrappedETH.address, usdc.address], + [ethOracleProxy.address, usdcOracle.address], + ); + + const liquidator = await deployLinearAuctionLiquidatorContractAsync( + web3, + core, + oracleWhiteList + ); + const newLiquidator = await deployLinearAuctionLiquidatorContractAsync( + web3, + core, + oracleWhiteList + ); + + newLiquidatorAddress = newLiquidator.address; + + const liquidatorWhiteList = await deployWhiteListContract(web3, [liquidator.address, newLiquidator.address]); + + const maxProfitFeePercentage = ether(.4); + const maxStreamingFeePercentage = ether(.07); + const performanceFeeCalculator = await feeCalculatorHelper.deployPerformanceFeeCalculatorAsync( + core.address, + oracleWhiteList.address, + maxProfitFeePercentage, + maxStreamingFeePercentage, + ); + + const feeCalculatorWhiteList = await deployWhiteListContract(web3, [performanceFeeCalculator.address]); + + rebalancingFactory = await deployRebalancingSetTokenV3FactoryContractAsync( + web3, + core, + rebalancingComponentWhiteList, + liquidatorWhiteList, + feeCalculatorWhiteList + ); + + // Create Risk Collateral Set + initialBaseCollateral = await deploySetTokenAsync( + web3, + core, + factory.address, + [wrappedETH.address], + [baseCollateralUnit], + baseCollateralNaturalUnit, + ); + + // Create Stable Collateral Set + initialQuoteCollateral = await deploySetTokenAsync( + web3, + core, + factory.address, + [usdc.address], + [quoteCollateralUnit], + quoteCollateralNaturalUnit, + ); + + allocator = await deployBinaryAllocatorAsync( + web3, + wrappedETH.address, + usdc.address, + ethOracleProxy.address, + usdcOracle.address, + initialBaseCollateral.address, + initialQuoteCollateral.address, + core.address, + factory.address + ); + + trigger = await deployRSITrendingTriggerAsync( + web3, + rsiOracle.address, + lowerBound, + upperBound, + rsiTimePeriod + ); + + const liquidatorData = ZERO_BYTES; + assetPairManagerV2 = await deployAssetPairManagerV2Async( + web3, + core.address, + allocator.address, + trigger.address, + useBullishBaseAssetAllocation, + allocationDenominator, + bullishBaseAssetAllocation, + signalConfirmationMinTime, + signalConfirmationMaxTime, + liquidatorData + ); + + await assetPairManagerV2.setTimeLockPeriod.sendTransactionAsync(ONE_DAY_IN_SECONDS, { from: DEFAULT_ACCOUNT }); + + // Deploy a RB Set + const rbSetFeeRecipient = ACCOUNTS[2].address; + const rebalanceFeeCalculator = performanceFeeCalculator.address; + const failRebalancePeriod = ONE_DAY_IN_SECONDS; + const { timestamp } = await web3.eth.getBlock('latest'); + const lastRebalanceTimestamp = new BigNumber(timestamp); + const rbSetEntryFee = ether(.01); + const rbSetProfitFee = ether(.2); + const rbSetStreamingFee = ether(.02); + rebalancingSetToken = await createDefaultRebalancingSetTokenV3Async( + web3, + core, + rebalancingFactory.address, + assetPairManagerV2.address, + liquidator.address, + rbSetFeeRecipient, + rebalanceFeeCalculator, + initialBaseCollateral.address, + failRebalancePeriod, + lastRebalanceTimestamp, + rbSetEntryFee, + rbSetProfitFee, + rbSetStreamingFee + ); + + await initializeManagerAsync( + assetPairManagerV2, + rebalancingSetToken.address + ); + + assetPairManagerV2Wrapper = new AssetPairManagerV2Wrapper(web3); + }); + + afterEach(async () => { + await web3Utils.revertToSnapshot(currentSnapshotId); + }); + + describe('core', async () => { + let subjectManagerAddress: Address; + + beforeEach(async () => { + subjectManagerAddress = assetPairManagerV2.address; + }); + + async function subject(): Promise
{ + return await assetPairManagerV2Wrapper.core( + subjectManagerAddress, + ); + } + + test('gets the correct core', async () => { + const address = await subject(); + expect(address).to.equal(core.address); + }); + }); + + describe('allocator', async () => { + let subjectManagerAddress: Address; + + beforeEach(async () => { + subjectManagerAddress = assetPairManagerV2.address; + }); + + async function subject(): Promise
{ + return await assetPairManagerV2Wrapper.allocator( + subjectManagerAddress, + ); + } + + test('gets the correct core', async () => { + const address = await subject(); + expect(address).to.equal(allocator.address); + }); + }); + + describe('trigger', async () => { + let subjectManagerAddress: Address; + + beforeEach(async () => { + subjectManagerAddress = assetPairManagerV2.address; + }); + + async function subject(): Promise
{ + return await assetPairManagerV2Wrapper.trigger( + subjectManagerAddress, + ); + } + + test('gets the correct core', async () => { + const address = await subject(); + expect(address).to.equal(trigger.address); + }); + }); + + describe('rebalancingSetToken', async () => { + let subjectManagerAddress: Address; + + beforeEach(async () => { + subjectManagerAddress = assetPairManagerV2.address; + }); + + async function subject(): Promise
{ + return await assetPairManagerV2Wrapper.rebalancingSetToken( + subjectManagerAddress, + ); + } + + test('gets the correct rebalancingSetToken', async () => { + const address = await subject(); + expect(address).to.equal(rebalancingSetToken.address); + }); + }); + + describe('baseAssetAllocation', async () => { + let subjectManagerAddress: Address; + + beforeEach(async () => { + subjectManagerAddress = assetPairManagerV2.address; + }); + + async function subject(): Promise { + return await assetPairManagerV2Wrapper.baseAssetAllocation( + subjectManagerAddress, + ); + } + + test('gets the correct baseAssetAllocation', async () => { + const address = await subject(); + const baseAssetAllocation = useBullishBaseAssetAllocation ? new BigNumber(100) : ZERO; + + expect(address).to.be.bignumber.equal(baseAssetAllocation); + }); + }); + + describe('allocationDenominator', async () => { + let subjectManagerAddress: Address; + + beforeEach(async () => { + subjectManagerAddress = assetPairManagerV2.address; + }); + + async function subject(): Promise { + return await assetPairManagerV2Wrapper.allocationDenominator( + subjectManagerAddress, + ); + } + + test('gets the correct allocationDenominator', async () => { + const address = await subject(); + expect(address).to.be.bignumber.equal(allocationDenominator); + }); + }); + + describe('bullishBaseAssetAllocation', async () => { + let subjectManagerAddress: Address; + + beforeEach(async () => { + subjectManagerAddress = assetPairManagerV2.address; + }); + + async function subject(): Promise { + return await assetPairManagerV2Wrapper.bullishBaseAssetAllocation( + subjectManagerAddress, + ); + } + + test('gets the correct bullishBaseAssetAllocation', async () => { + const address = await subject(); + expect(address).to.be.bignumber.equal(bullishBaseAssetAllocation); + }); + }); + + describe('bearishBaseAssetAllocation', async () => { + let subjectManagerAddress: Address; + + beforeEach(async () => { + subjectManagerAddress = assetPairManagerV2.address; + }); + + async function subject(): Promise { + return await assetPairManagerV2Wrapper.bearishBaseAssetAllocation( + subjectManagerAddress, + ); + } + + test('gets the correct bearishBaseAssetAllocation', async () => { + const address = await subject(); + expect(address).to.be.bignumber.equal(bearishBaseAssetAllocation); + }); + }); + + describe('signalConfirmationMinTime', async () => { + let subjectManagerAddress: Address; + + beforeEach(async () => { + subjectManagerAddress = assetPairManagerV2.address; + }); + + async function subject(): Promise { + return await assetPairManagerV2Wrapper.signalConfirmationMinTime( + subjectManagerAddress, + ); + } + + test('gets the correct signalConfirmationMinTime', async () => { + const address = await subject(); + expect(address).to.be.bignumber.equal(signalConfirmationMinTime); + }); + }); + + describe('signalConfirmationMaxTime', async () => { + let subjectManagerAddress: Address; + + beforeEach(async () => { + subjectManagerAddress = assetPairManagerV2.address; + }); + + async function subject(): Promise { + return await assetPairManagerV2Wrapper.signalConfirmationMaxTime( + subjectManagerAddress, + ); + } + + test('gets the correct signalConfirmationMaxTime', async () => { + const address = await subject(); + expect(address).to.be.bignumber.equal(signalConfirmationMaxTime); + }); + }); + + describe('recentInitialProposeTimestamp', async () => { + let subjectManagerAddress: Address; + + beforeEach(async () => { + subjectManagerAddress = assetPairManagerV2.address; + }); + + async function subject(): Promise { + return await assetPairManagerV2Wrapper.recentInitialProposeTimestamp( + subjectManagerAddress, + ); + } + + test('gets the correct recentInitialProposeTimestamp', async () => { + const address = await subject(); + expect(address).to.be.bignumber.equal(ZERO); + }); + }); + + describe('liquidatorData', async () => { + let subjectManagerAddress: Address; + + beforeEach(async () => { + subjectManagerAddress = assetPairManagerV2.address; + }); + + async function subject(): Promise { + return await assetPairManagerV2Wrapper.liquidatorData( + subjectManagerAddress, + ); + } + + test('gets the correct liquidatorData', async () => { + const liquidatorData = await subject(); + expect(liquidatorData).to.be.bignumber.equal(ZERO_BYTES); + }); + }); + + describe('setLiquidator', async () => { + let subjectNewLiquidator: Address; + let subjectManagerAddress: Address; + + beforeEach(async () => { + subjectManagerAddress = assetPairManagerV2.address; + subjectNewLiquidator = newLiquidatorAddress; + }); + + async function subject(): Promise { + return await assetPairManagerV2Wrapper.setLiquidator( + subjectManagerAddress, + subjectNewLiquidator + ); + } + + test('sets the new liquidator correctly', async () => { + await subject(); + + const actualLiquidator = await rebalancingSetToken.liquidator.callAsync(); + + expect(actualLiquidator).to.equal(subjectNewLiquidator); + }); + }); + + describe('setLiquidatorData', async () => { + let subjectLiquidatorData: Bytes; + let subjectManagerAddress: Address; + + beforeEach(async () => { + subjectManagerAddress = assetPairManagerV2.address; + subjectLiquidatorData = '0x0000000000000000000000000000000000000000000000000000000000000005'; + }); + + async function subject(): Promise { + return await assetPairManagerV2Wrapper.setLiquidatorData( + subjectManagerAddress, + subjectLiquidatorData + ); + } + + test('sets the new liquidatorData correctly', async () => { + await subject(); + + const actualLiquidatorData = await assetPairManagerV2.liquidatorData.callAsync(); + + expect(actualLiquidatorData).to.equal(subjectLiquidatorData); + }); + }); + + describe('setFeeRecipient', async () => { + let subjectFeeRecipient: string; + let subjectManagerAddress: Address; + + beforeEach(async () => { + subjectManagerAddress = assetPairManagerV2.address; + subjectFeeRecipient = ACCOUNTS[3].address; + }); + + async function subject(): Promise { + return await assetPairManagerV2Wrapper.setFeeRecipient( + subjectManagerAddress, + subjectFeeRecipient + ); + } + + test('sets the new fee recipient correctly', async () => { + await subject(); + + const actualNewFeeRecipient = await rebalancingSetToken.feeRecipient.callAsync(); + + expect(actualNewFeeRecipient).to.equal(subjectFeeRecipient); + }); + }); + + describe('adjustFee', async () => { + let subjectNewFeeCallData: string; + let subjectManagerAddress: Address; + + beforeEach(async () => { + const feeType = ZERO; + const newFeePercentage = ether(.03); + + subjectNewFeeCallData = feeCalculatorHelper.generateAdjustFeeCallData(feeType, newFeePercentage); + subjectManagerAddress = assetPairManagerV2.address; + }); + + async function subject(): Promise { + return await assetPairManagerV2Wrapper.adjustFee( + subjectManagerAddress, + subjectNewFeeCallData + ); + } + + test('sets the correct upgrade hash', async () => { + const txHash = await subject(); + const { blockHash, input } = await web3.eth.getTransaction(txHash); + const { timestamp } = await web3.eth.getBlock(blockHash as any); + + const upgradeHash = web3.utils.soliditySha3(input); + const actualTimestamp = await assetPairManagerV2.timeLockedUpgrades.callAsync(upgradeHash); + expect(actualTimestamp).to.bignumber.equal(timestamp); + }); + }); + + describe('removeRegisteredUpgrade', async () => { + let subjectManager: Address; + let subjectUpgradeHash: string; + + let feeType: BigNumber; + let feePercentage: BigNumber; + + beforeEach(async () => { + feeType = ZERO; + feePercentage = ether(.03); + + subjectManager = assetPairManagerV2.address; + + const newFeeCallData = feeCalculatorHelper.generateAdjustFeeCallData( + feeType, + feePercentage + ); + + const txHash = await assetPairManagerV2Wrapper.adjustFee( + subjectManager, + newFeeCallData, + ); + + const { input } = await web3.eth.getTransaction(txHash); + subjectUpgradeHash = web3.utils.soliditySha3(input); + }); + + async function subject(): Promise { + return await assetPairManagerV2Wrapper.removeRegisteredUpgrade( + subjectManager, + subjectUpgradeHash, + ); + } + + test('successfully removes upgradeHash', async () => { + await subject(); + + const actualTimestamp = await assetPairManagerV2.timeLockedUpgrades.callAsync(subjectUpgradeHash); + expect(actualTimestamp).to.bignumber.equal(ZERO); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 2687cb7cd..4b3f4247e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6708,6 +6708,35 @@ set-protocol-strategies@^1.1.37: web3 "1.0.0-beta.36" web3-utils "1.0.0-beta.36" +set-protocol-strategies@^1.1.39: + version "1.1.39" + resolved "https://registry.yarnpkg.com/set-protocol-strategies/-/set-protocol-strategies-1.1.39.tgz#69e0f607bd3da9b5c7d52863adb80f0aeca9d525" + integrity sha512-4PilqEEDFFkvaG8ErzuTWCgWr6rewtDvQH5c97iwzVqPj9d2BW5jaziBXO5s/2f7miMYjDcqjTsCzM4DOSSFCQ== + dependencies: + bn-chai "^1.0.1" + canonical-weth "^1.3.1" + dotenv "^6.2.0" + eth-gas-reporter "^0.1.10" + ethlint "^1.2.3" + expect "^24.1.0" + fs-extra "^5.0.0" + husky "^0.14.3" + lint-staged "^7.2.0" + minify-all "^1.2.2" + module-alias "^2.1.0" + openzeppelin-solidity "^2.2" + set-protocol-contract-utils "^1.0.3" + set-protocol-contracts "^1.4.5-beta" + set-protocol-oracles "^1.0.16" + set-protocol-utils "^1.1.2" + tiny-promisify "^1.0.0" + truffle-flattener "^1.4.2" + ts-mocha "^6.0.0" + ts-node "^8.0.2" + tslint-eslint-rules "^5.3.1" + web3 "1.0.0-beta.36" + web3-utils "1.0.0-beta.36" + set-protocol-utils@^1.0.0-beta.45, set-protocol-utils@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/set-protocol-utils/-/set-protocol-utils-1.1.2.tgz#02d2fabb30bb1be741bad9aede3a058f56a7301d"