diff --git a/contracts/managers/ETHTwentyDayMACOManager.sol b/contracts/managers/ETHTwentyDayMACOManager.sol index b5e263a..8f1ce1a 100644 --- a/contracts/managers/ETHTwentyDayMACOManager.sol +++ b/contracts/managers/ETHTwentyDayMACOManager.sol @@ -33,26 +33,22 @@ import { FlexibleTimingManagerLibrary } from "./lib/FlexibleTimingManagerLibrary * @author Set Protocol * * Rebalancing Manager contract for implementing the Moving Average (MA) Crossover - * Strategy between ETH 20-day MA and the spot price of ETH. When the spot price - * dips below the 20-day MA ETH is sold for USDC and vice versa when the spot price - * exceeds the 20-day MA. + * Strategy between ETH MA and the spot price of ETH. The time frame for the MA is + * defined on instantiation When the spot price dips below the MA ETH is sold for + * USDC and vice versa when the spot price exceeds the MA. */ contract ETHTwentyDayMACOManager { using SafeMath for uint256; /* ============ Constants ============ */ - uint256 constant MOVING_AVERAGE_DAYS = 20; uint256 constant AUCTION_LIB_PRICE_DIVISOR = 1000; - uint256 constant CALCULATION_PRECISION = 100; - - uint256 constant COLLATERAL_SET_PRICE_DIFF_LOWER_BOUND = 20; - uint256 constant COLLATERAL_SET_PRICE_DIFF_UPPER_BOUND = 500; + uint256 constant ALLOCATION_PRICE_RATIO_LIMIT = 4; uint256 constant TEN_MINUTES_IN_SECONDS = 600; uint256 constant SIX_HOURS_IN_SECONDS = 21600; uint256 constant TWELVE_HOURS_IN_SECONDS = 43200; - // Equal to $1 + // Equal to $1 since token prices are passed with 18 decimals uint256 constant USDC_PRICE = 10 ** 18; uint256 constant USDC_DECIMALS = 6; uint256 constant ETH_DECIMALS = 18; @@ -66,14 +62,13 @@ contract ETHTwentyDayMACOManager { address public auctionLibrary; address public usdcAddress; - address public ethAddress; + address public wethAddress; address public stableCollateralAddress; address public riskCollateralAddress; uint256 public auctionTimeToPivot; - bool public riskOn; - - uint256 public proposalTimestamp; + uint256 public movingAverageDays; + uint256 public lastProposalTimestamp; /* ============ Events ============ */ @@ -85,30 +80,30 @@ contract ETHTwentyDayMACOManager { /* * ETHTwentyDayMACOManager constructor. * - * @param _coreAddress The address of the Core contract - * @param _movingAveragePriceFeed The address of MA price feed - * @param _usdcAddress The address of the USDC contract - * @param _stableCollateralAddress The address stable collateral - * (made of USDC wrapped in a Set Token) - * @param _riskCollateralAddress The address risk collateral - * (made of ETH wrapped in a Set Token) - * @param _setTokenFactory The address of the SetTokenFactory - * @param _auctionLibrary The address of auction price curve to use in rebalance - * @param _auctionTimeToPivot The amount of time until pivot reached in rebalance - * @param _riskOn Indicate is initial allocation is collateralized by risky - * asset (true) or stable asset (false) + * @param _coreAddress The address of the Core contract + * @param _movingAveragePriceFeed The address of MA price feed + * @param _usdcAddress The address of the USDC contract + * @param _wethAddress The address of the WETH contract + * @param _initialStableCollateralAddress The address stable collateral + * (made of USDC wrapped in a Set Token) + * @param _initialRiskCollateralAddress The address risk collateral + * (made of ETH wrapped in a Set Token) + * @param _setTokenFactory The address of the SetTokenFactory + * @param _auctionLibrary The address of auction price curve to use in rebalance + * @param _movingAverageDays The amount of days to use in moving average calculation + * @param _auctionTimeToPivot The amount of time until pivot reached in rebalance */ constructor( address _coreAddress, address _movingAveragePriceFeed, address _usdcAddress, - address _ethAddress, - address _stableCollateralAddress, - address _riskCollateralAddress, + address _wethAddress, + address _initialStableCollateralAddress, + address _initialRiskCollateralAddress, address _setTokenFactory, address _auctionLibrary, - uint256 _auctionTimeToPivot, - bool _riskOn + uint256 _movingAverageDays, + uint256 _auctionTimeToPivot ) public { @@ -119,14 +114,17 @@ contract ETHTwentyDayMACOManager { auctionLibrary = _auctionLibrary; usdcAddress = _usdcAddress; - ethAddress = _ethAddress; - stableCollateralAddress = _stableCollateralAddress; - riskCollateralAddress = _riskCollateralAddress; + wethAddress = _wethAddress; + stableCollateralAddress = _initialStableCollateralAddress; + riskCollateralAddress = _initialRiskCollateralAddress; auctionTimeToPivot = _auctionTimeToPivot; - riskOn = _riskOn; + movingAverageDays = _movingAverageDays; + lastProposalTimestamp = 0; } + /* ============ External ============ */ + /* * This function sets the Rebalancing Set Token address that the manager is associated with. * Since, the rebalancing set token must first specify the address of the manager before deployment, @@ -140,30 +138,25 @@ contract ETHTwentyDayMACOManager { ) external { + // Check that contract deployer is calling function require( msg.sender == contractDeployer, "ETHTwentyDayMACOManager.initialize: Only the contract deployer can initialize" ); - require( - rebalancingSetTokenAddress == address(0), - "ETHTwentyDayMACOManager.initialize: Rebalancing SetToken Address must be empty" - ); - // Make sure the rebalancingSetToken is tracked by Core require( ICore(coreAddress).validSets(_rebalancingSetTokenAddress), - "ETHTwentyDayMACOManager.initialize: Invalid or disabled SetToken address" + "ETHTwentyDayMACOManager.initialize: Invalid or disabled RebalancingSetToken address" ); rebalancingSetTokenAddress = _rebalancingSetTokenAddress; + contractDeployer = address(0); } - /* ============ External ============ */ - /* - * When allowed on RebalancingSetToken, anyone can call for a new rebalance proposal. The Sets off a six - * hour period where the signal con be confirmed before moving ahead with rebalance. + * When allowed on RebalancingSetToken, anyone can call for a new rebalance proposal. This begins a six + * hour period where the signal can be confirmed before moving ahead with rebalance. * */ function initialPropose() @@ -171,13 +164,9 @@ contract ETHTwentyDayMACOManager { { // Make sure propose in manager hasn't already been initiated require( - block.timestamp > proposalTimestamp.add(TWELVE_HOURS_IN_SECONDS), + block.timestamp > lastProposalTimestamp.add(TWELVE_HOURS_IN_SECONDS), "ETHTwentyDayMACOManager.initialPropose: 12 hours must pass before new proposal initiated" ); - - // Checks to make sure that collateral used aligns with riskOn parameter in case a rebalance is aborted - // after proposal goes through. - confirmLastRebalance(); // Create interface to interact with RebalancingSetToken and check enough time has passed for proposal FlexibleTimingManagerLibrary.validateManagerPropose(IRebalancingSetToken(rebalancingSetTokenAddress)); @@ -191,7 +180,7 @@ contract ETHTwentyDayMACOManager { // Make sure price trigger has been reached checkPriceTriggerMet(ethPrice, movingAveragePrice); - proposalTimestamp = block.timestamp; + lastProposalTimestamp = block.timestamp; } /* @@ -203,11 +192,14 @@ contract ETHTwentyDayMACOManager { { // Make sure enough time has passed to initiate proposal on Rebalancing Set Token require( - block.timestamp >= proposalTimestamp.add(SIX_HOURS_IN_SECONDS) && - block.timestamp <= proposalTimestamp.add(TWELVE_HOURS_IN_SECONDS), + block.timestamp >= lastProposalTimestamp.add(SIX_HOURS_IN_SECONDS) && + block.timestamp <= lastProposalTimestamp.add(TWELVE_HOURS_IN_SECONDS), "ETHTwentyDayMACOManager.confirmPropose: Confirming signal must be 6-12 hours from initial propose" ); + // Create interface to interact with RebalancingSetToken and check not in Proposal state + FlexibleTimingManagerLibrary.validateManagerPropose(IRebalancingSetToken(rebalancingSetTokenAddress)); + // Get price data from oracles ( uint256 ethPrice, @@ -221,8 +213,8 @@ contract ETHTwentyDayMACOManager { // great to run good auction. Return nextSet address and dollar value of current and next set ( address nextSetAddress, - uint256 nextSetDollarValue, - uint256 currentSetDollarValue + uint256 currentSetDollarValue, + uint256 nextSetDollarValue ) = determineNewAllocation( ethPrice, movingAveragePrice @@ -249,9 +241,6 @@ contract ETHTwentyDayMACOManager { auctionPivotPrice ); - // Update riskOn parameter - riskOn = !riskOn; - emit LogManagerProposal( ethPrice, movingAveragePrice @@ -261,26 +250,29 @@ contract ETHTwentyDayMACOManager { /* ============ Internal ============ */ /* - * Make sure that riskOn parameter is aligned with the expected collateral underlying the rebalancing set. - * Done in case an auction fails and resets back to the original collateral, this failed rebalance would - * not show up on the manager and would indicate the wrong position. + * Determine if risk collateral is currently collateralizing the rebalancing set, if so return true, + * else return false. * + * @return boolean True if risk collateral in use, false otherwise */ - function confirmLastRebalance() + function usingRiskCollateral() internal + view + returns (bool) { // Get set currently collateralizing rebalancing set token address[] memory currentCollateralComponents = ISetToken(rebalancingSetTokenAddress).getComponents(); - // If collateralized by riskCollateral set riskOn to true, else to false - riskOn = (currentCollateralComponents[0] == riskCollateralAddress); + // If collateralized by riskCollateral set return true, else to false + return (currentCollateralComponents[0] == riskCollateralAddress); } /* - * Get the ETH and moving average prices from respective oracles + * Get the ETH and moving average prices from respective oracles. Price returned have 18 decimals so + * 10 ** 18 = $1. * * @return uint256 USD Price of ETH - * @return uint256 20 day moving average USD Price of ETH + * @return uint256 Moving average USD Price of ETH */ function getPriceData() internal @@ -292,7 +284,7 @@ contract ETHTwentyDayMACOManager { // Get current eth price and moving average data uint256 ethPrice = FlexibleTimingManagerLibrary.queryPriceData(ethPriceFeed); - uint256 movingAveragePrice = uint256(IMetaOracle(movingAveragePriceFeed).read(MOVING_AVERAGE_DAYS)); + uint256 movingAveragePrice = uint256(IMetaOracle(movingAveragePriceFeed).read(movingAverageDays)); return (ethPrice, movingAveragePrice); } @@ -301,26 +293,26 @@ contract ETHTwentyDayMACOManager { * Check to make sure that the necessary price changes have occured to allow a rebalance. * * @param _ethPrice Current Ethereum price as found on oracle - * @param _movingAveragePrice Current 20 day MA price from Meta Oracle - * @return boolean Boolean indicating if price conditions for rebalance met + * @param _movingAveragePrice Current MA price from Meta Oracle */ function checkPriceTriggerMet( uint256 _ethPrice, uint256 _movingAveragePrice ) internal + view { - if (riskOn) { - // If currently holding ETH (riskOn) check to see if price is below 20 day MA, otherwise revert. + if (usingRiskCollateral()) { + // If currently holding ETH (riskOn) check to see if price is below MA, otherwise revert. require( _movingAveragePrice > _ethPrice, - "ETHTwentyDayMACOManager.initialPropose: ETH Price must be below moving average price" + "ETHTwentyDayMACOManager.checkPriceTriggerMet: ETH Price must be below moving average price" ); } else { - // If currently holding USDC (!riskOn) check to see if price is above 20 day MA, otherwise revert. + // If currently holding USDC (not riskOn) check to see if price is above MA, otherwise revert. require( _movingAveragePrice < _ethPrice, - "ETHTwentyDayMACOManager.initialPropose: ETH Price must be above moving average price" + "ETHTwentyDayMACOManager.checkPriceTriggerMet: ETH Price must be above moving average price" ); } } @@ -332,10 +324,10 @@ contract ETHTwentyDayMACOManager { * stable collateral set is created, if !riskOn then a new risk collateral set is created. * * @param _ethPrice Current Ethereum price as found on oracle - * @param _movingAveragePrice Current 20 day MA price from Meta Oracle + * @param _movingAveragePrice Current MA price from Meta Oracle * @return address The address of the proposed nextSet - * @return uint256 The USD value of next Set * @return uint256 The USD value of current Set + * @return uint256 The USD value of next Set */ function determineNewAllocation( uint256 _ethPrice, @@ -351,11 +343,14 @@ contract ETHTwentyDayMACOManager { uint256 riskCollateralDollarValue ) = checkForNewAllocation(_ethPrice); - address nextSetAddress = riskOn ? stableCollateralAddress : riskCollateralAddress; - uint256 currentSetDollarValue = riskOn ? riskCollateralDollarValue : stableCollateralDollarValue; - uint256 nextSetDollarValue = riskOn ? stableCollateralDollarValue : riskCollateralDollarValue; + ( + address nextSetAddress, + uint256 currentSetDollarValue, + uint256 nextSetDollarValue + ) = usingRiskCollateral() ? (stableCollateralAddress, riskCollateralDollarValue, stableCollateralDollarValue) : + (riskCollateralAddress, stableCollateralDollarValue, riskCollateralDollarValue); - return (nextSetAddress, nextSetDollarValue, currentSetDollarValue); + return (nextSetAddress, currentSetDollarValue, nextSetDollarValue); } /* @@ -363,8 +358,8 @@ contract ETHTwentyDayMACOManager { * than 5x different from each other then create a new collateral set. * * @param _ethPrice Current Ethereum price as found on oracle - * @return uint256 The USD value of stable collateral - * @return uint256 The USD value of risk collateral + * @return uint256 The USD value of stable collateral + * @return uint256 The USD value of risk collateral */ function checkForNewAllocation( uint256 _ethPrice @@ -393,14 +388,10 @@ contract ETHTwentyDayMACOManager { riskCollateralDetails.units[0], ETH_DECIMALS ); - - // Determine fair value for the auction - uint256 fairValue = riskCollateralDollarValue - .mul(CALCULATION_PRECISION) - .div(stableCollateralDollarValue); // If value of one Set is 5 times greater than the other, create a new collateral Set - if (fairValue <= COLLATERAL_SET_PRICE_DIFF_LOWER_BOUND || fairValue >= COLLATERAL_SET_PRICE_DIFF_UPPER_BOUND) { + if (riskCollateralDollarValue.mul(ALLOCATION_PRICE_RATIO_LIMIT) <= stableCollateralDollarValue || + riskCollateralDollarValue >= stableCollateralDollarValue.mul(ALLOCATION_PRICE_RATIO_LIMIT)) { //Determine the new collateral parameters return determineNewCollateralParameters( _ethPrice, @@ -415,8 +406,9 @@ contract ETHTwentyDayMACOManager { } /* - * Calculate new collateral parameters for the occasion where the dollar value of the two collateral - * sets is more than 5x different from each other. + * Create new collateral Set for the occasion where the dollar value of the two collateral + * sets is more than 5x different from each other. The new collateral set address is then + * assigned to the correct state variable (risk or stable collateral) * * @param _ethPrice Current Ethereum price as found on oracle * @param _stableCollateralValue Value of current stable collateral set in USD @@ -439,7 +431,7 @@ contract ETHTwentyDayMACOManager { uint256 stableCollateralDollarValue; uint256 riskCollateralDollarValue; - if (riskOn) { + if (usingRiskCollateral()) { // Create static components and units array address[] memory nextSetComponents = new address[](1); nextSetComponents[0] = usdcAddress; @@ -450,8 +442,7 @@ contract ETHTwentyDayMACOManager { _stableCollateralDetails ); - // Create new stable collateral set with units as calculated above and naturalUnit - // equal to CALCULATION_PRECISION + // Create new stable collateral set with units and naturalUnit as calculated above stableCollateralAddress = ICore(coreAddress).createSet( setTokenFactory, nextSetComponents, @@ -472,7 +463,7 @@ contract ETHTwentyDayMACOManager { } else { // Create static components and units array address[] memory nextSetComponents = new address[](1); - nextSetComponents[0] = ethAddress; + nextSetComponents[0] = wethAddress; uint256[] memory nextSetUnits = getNewCollateralSetUnits( _stableCollateralValue, _ethPrice, @@ -480,8 +471,7 @@ contract ETHTwentyDayMACOManager { _riskCollateralDetails ); - // Create new risk collateral set with units as calculated above and naturalUnit - // equal to CALCULATION_PRECISION + // Create new risk collateral set with units and naturalUnit as calculated above riskCollateralAddress = ICore(coreAddress).createSet( setTokenFactory, nextSetComponents, @@ -532,8 +522,7 @@ contract ETHTwentyDayMACOManager { nextSetUnits[0] = _currentCollateralUSDValue .mul(10 ** _replacedCollateralDecimals) .mul(_replacedCollateralDetails.naturalUnit) - .div(SET_TOKEN_DECIMALS) - .div(_replacedCollateralPrice); + .div(SET_TOKEN_DECIMALS.mul(_replacedCollateralPrice)); return nextSetUnits; } } diff --git a/contracts/managers/lib/FlexibleTimingManagerLibrary.sol b/contracts/managers/lib/FlexibleTimingManagerLibrary.sol index 9bafb7d..1bf306e 100644 --- a/contracts/managers/lib/FlexibleTimingManagerLibrary.sol +++ b/contracts/managers/lib/FlexibleTimingManagerLibrary.sol @@ -49,14 +49,14 @@ library FlexibleTimingManagerLibrary { uint256 rebalanceInterval = _rebalancingSetInterface.rebalanceInterval(); require( block.timestamp >= lastRebalanceTimestamp.add(rebalanceInterval), - "ManagerLibrary.proposeNewRebalance: Rebalance interval not elapsed" + "FlexibleTimingManagerLibrary.proposeNewRebalance: Rebalance interval not elapsed" ); // Require that Rebalancing Set Token is in Default state, won't allow for re-proposals // because malicious actor could prevent token from ever rebalancing require( _rebalancingSetInterface.rebalanceState() == RebalancingLibrary.State.Default, - "ManagerLibrary.proposeNewRebalance: State must be in Default" + "FlexibleTimingManagerLibrary.proposeNewRebalance: State must be in Default" ); } diff --git a/test/contracts/managers/ethTwentyDayMACOManager.spec.ts b/test/contracts/managers/ethTwentyDayMACOManager.spec.ts index f9750ef..b85a57a 100644 --- a/test/contracts/managers/ethTwentyDayMACOManager.spec.ts +++ b/test/contracts/managers/ethTwentyDayMACOManager.spec.ts @@ -162,7 +162,7 @@ contract('ETHTwentyDayMACOManager', accounts => { core, factory.address, [wrappedETH.address], - [new BigNumber(100)], + [new BigNumber(10 ** 6)], RISK_COLLATERAL_NATURAL_UNIT, ); }); @@ -181,7 +181,7 @@ contract('ETHTwentyDayMACOManager', accounts => { let subjectSetTokenFactoryAddress: Address; let subjectAuctionLibraryAddress: Address; let subjectAuctionTimeToPivot: BigNumber; - let subjectRiskOn: boolean; + let subjectMovingAverageDays: BigNumber; beforeEach(async () => { subjectCoreAddress = core.address; @@ -192,8 +192,8 @@ contract('ETHTwentyDayMACOManager', accounts => { subjectRiskCollateralAddress = riskCollateral.address; subjectSetTokenFactoryAddress = factory.address; subjectAuctionLibraryAddress = linearAuctionPriceCurve.address; + subjectMovingAverageDays = new BigNumber(20); subjectAuctionTimeToPivot = ONE_DAY_IN_SECONDS.div(6); - subjectRiskOn = false; }); async function subject(): Promise { @@ -206,8 +206,8 @@ contract('ETHTwentyDayMACOManager', accounts => { subjectRiskCollateralAddress, subjectSetTokenFactoryAddress, subjectAuctionLibraryAddress, + subjectMovingAverageDays, subjectAuctionTimeToPivot, - subjectRiskOn ); } @@ -267,20 +267,20 @@ contract('ETHTwentyDayMACOManager', accounts => { expect(actualAuctionLibraryAddress).to.equal(subjectAuctionLibraryAddress); }); - it('sets the correct auction time to pivot', async () => { + it('sets the correct moving average days', async () => { ethTwentyDayMACOManager = await subject(); - const actualAuctionTimeToPivot = await ethTwentyDayMACOManager.auctionTimeToPivot.callAsync(); + const actualMovingAverageDays = await ethTwentyDayMACOManager.movingAverageDays.callAsync(); - expect(actualAuctionTimeToPivot).to.be.bignumber.equal(subjectAuctionTimeToPivot); + expect(actualMovingAverageDays).to.be.bignumber.equal(subjectMovingAverageDays); }); - it('sets the correct risk on parameter', async () => { + it('sets the correct auction time to pivot', async () => { ethTwentyDayMACOManager = await subject(); - const actualRiskOn = await ethTwentyDayMACOManager.riskOn.callAsync(); + const actualAuctionTimeToPivot = await ethTwentyDayMACOManager.auctionTimeToPivot.callAsync(); - expect(actualRiskOn).to.equal(subjectRiskOn); + expect(actualAuctionTimeToPivot).to.be.bignumber.equal(subjectAuctionTimeToPivot); }); }); @@ -306,7 +306,7 @@ contract('ETHTwentyDayMACOManager', accounts => { ); auctionTimeToPivot = ONE_DAY_IN_SECONDS.div(4); - const [riskOn, initialAllocationAddress] = await managerWrapper.getMACOInitialAllocationAsync( + const initialAllocationAddress = await managerWrapper.getMACOInitialAllocationAsync( stableCollateral, riskCollateral, ethMedianizer, @@ -314,6 +314,7 @@ contract('ETHTwentyDayMACOManager', accounts => { new BigNumber(20) ); + const movingAverageDays = new BigNumber(20); ethTwentyDayMACOManager = await managerWrapper.deployETHTwentyDayMACOManagerAsync( core.address, movingAverageOracle.address, @@ -323,8 +324,8 @@ contract('ETHTwentyDayMACOManager', accounts => { riskCollateral.address, factory.address, linearAuctionPriceCurve.address, + movingAverageDays, auctionTimeToPivot, - riskOn, ); proposalPeriod = ONE_DAY_IN_SECONDS; @@ -355,19 +356,6 @@ contract('ETHTwentyDayMACOManager', accounts => { expect(rebalancingSetTokenAddress).to.equal(subjectRebalancingSetToken); }); - describe('when the rebalancing set address has already been set', async () => { - beforeEach(async () => { - await ethTwentyDayMACOManager.initialize.sendTransactionAsync( - subjectRebalancingSetToken, - { from: subjectCaller, gas: DEFAULT_GAS} - ); - }); - - it('should revert', async () => { - await expectRevertError(subject()); - }); - }); - describe('but caller is not the contract deployer', async () => { beforeEach(async () => { subjectCaller = notDeployerAccount; @@ -410,7 +398,6 @@ contract('ETHTwentyDayMACOManager', accounts => { let lastPrice: BigNumber; let proposalPeriod: BigNumber; let auctionTimeToPivot: BigNumber; - let misalignedRiskOn: boolean = undefined; before(async () => { updatedValues = _.map(new Array(19), function(el, i) {return ether(150 + i); }); @@ -427,7 +414,7 @@ contract('ETHTwentyDayMACOManager', accounts => { ); auctionTimeToPivot = ONE_DAY_IN_SECONDS.div(4); - const [riskOn, initialAllocationAddress] = await managerWrapper.getMACOInitialAllocationAsync( + const initialAllocationAddress = await managerWrapper.getMACOInitialAllocationAsync( stableCollateral, riskCollateral, ethMedianizer, @@ -435,13 +422,7 @@ contract('ETHTwentyDayMACOManager', accounts => { new BigNumber(20) ); - let passedRiskOn: boolean; - if (misalignedRiskOn != undefined) { - passedRiskOn = misalignedRiskOn; - } else { - passedRiskOn = riskOn; - } - + const movingAverageDays = new BigNumber(20); ethTwentyDayMACOManager = await managerWrapper.deployETHTwentyDayMACOManagerAsync( core.address, movingAverageOracle.address, @@ -451,8 +432,8 @@ contract('ETHTwentyDayMACOManager', accounts => { riskCollateral.address, factory.address, linearAuctionPriceCurve.address, + movingAverageDays, auctionTimeToPivot, - passedRiskOn, ); proposalPeriod = ONE_DAY_IN_SECONDS; @@ -495,40 +476,10 @@ contract('ETHTwentyDayMACOManager', accounts => { const block = await web3.eth.getBlock('latest'); const expectedTimestamp = new BigNumber(block.timestamp); - const actualTimestamp = await ethTwentyDayMACOManager.proposalTimestamp.callAsync(); + const actualTimestamp = await ethTwentyDayMACOManager.lastProposalTimestamp.callAsync(); expect(actualTimestamp).to.be.bignumber.equal(expectedTimestamp); }); - describe('but riskOn parameter has become misaligned due to failed rebalance', async () => { - before(async () => { - misalignedRiskOn = false; - }); - - after(async () => { - misalignedRiskOn = undefined; - }); - - it('riskOn parameter is flipped', async () => { - const preCallRiskOn = await ethTwentyDayMACOManager.riskOn.callAsync(); - expect(preCallRiskOn).to.equal(false); - - await subject(); - - const postCallRiskOn = await ethTwentyDayMACOManager.riskOn.callAsync(); - expect(postCallRiskOn).to.equal(true); - }); - - it('sets the proposalTimestamp correctly', async () => { - await subject(); - - const block = await web3.eth.getBlock('latest'); - const expectedTimestamp = new BigNumber(block.timestamp); - - const actualTimestamp = await ethTwentyDayMACOManager.proposalTimestamp.callAsync(); - expect(actualTimestamp).to.be.bignumber.equal(expectedTimestamp); - }); - }); - describe('but price has not dipped below MA', async () => { before(async () => { lastPrice = ether(170); @@ -579,40 +530,10 @@ contract('ETHTwentyDayMACOManager', accounts => { const block = await web3.eth.getBlock('latest'); const expectedTimestamp = new BigNumber(block.timestamp); - const actualTimestamp = await ethTwentyDayMACOManager.proposalTimestamp.callAsync(); + const actualTimestamp = await ethTwentyDayMACOManager.lastProposalTimestamp.callAsync(); expect(actualTimestamp).to.be.bignumber.equal(expectedTimestamp); }); - describe('but riskOn parameter has become misaligned due to failed rebalance', async () => { - before(async () => { - misalignedRiskOn = true; - }); - - after(async () => { - misalignedRiskOn = undefined; - }); - - it('riskOn parameter is flipped', async () => { - const preCallRiskOn = await ethTwentyDayMACOManager.riskOn.callAsync(); - expect(preCallRiskOn).to.equal(true); - - await subject(); - - const postCallRiskOn = await ethTwentyDayMACOManager.riskOn.callAsync(); - expect(postCallRiskOn).to.equal(false); - }); - - it('sets the proposalTimestamp correctly', async () => { - await subject(); - - const block = await web3.eth.getBlock('latest'); - const expectedTimestamp = new BigNumber(block.timestamp); - - const actualTimestamp = await ethTwentyDayMACOManager.proposalTimestamp.callAsync(); - expect(actualTimestamp).to.be.bignumber.equal(expectedTimestamp); - }); - }); - describe('but price has not dipped below MA', async () => { before(async () => { lastPrice = ether(150); @@ -632,7 +553,9 @@ contract('ETHTwentyDayMACOManager', accounts => { describe('when propose is called and rebalancing set token is in Proposal state', async () => { beforeEach(async () => { await blockchain.increaseTimeAsync(subjectTimeFastForward); - await ethTwentyDayMACOManager.initialPropose.sendTransactionAsync(); + await ethTwentyDayMACOManager.initialPropose.sendTransactionAsync( + { from: subjectCaller, gas: DEFAULT_GAS} + ); await blockchain.increaseTimeAsync(ONE_DAY_IN_SECONDS.div(4)); await ethTwentyDayMACOManager.confirmPropose.sendTransactionAsync(); @@ -642,6 +565,42 @@ contract('ETHTwentyDayMACOManager', accounts => { await expectRevertError(subject()); }); }); + + describe('when propose is called and rebalancing set token is in Rebalance state', async () => { + beforeEach(async () => { + // Issue currentSetToken + const initialAllocationTokenAddress = await rebalancingSetToken.currentSet.callAsync(); + const initialAllocationToken = await protocolWrapper.getSetTokenAsync(initialAllocationTokenAddress); + await core.issue.sendTransactionAsync( + initialAllocationToken.address, + ether(9), + {from: deployerAccount, gas: DEFAULT_GAS}, + ); + await erc20Wrapper.approveTransfersAsync([initialAllocationToken], transferProxy.address); + + // Use issued currentSetToken to issue rebalancingSetToken + await core.issue.sendTransactionAsync( + rebalancingSetToken.address, + ether(7), + { from: deployerAccount, gas: DEFAULT_GAS } + ); + + await blockchain.increaseTimeAsync(subjectTimeFastForward); + await ethTwentyDayMACOManager.initialPropose.sendTransactionAsync( + { from: subjectCaller, gas: DEFAULT_GAS} + ); + + await blockchain.increaseTimeAsync(ONE_DAY_IN_SECONDS.div(4)); + await ethTwentyDayMACOManager.confirmPropose.sendTransactionAsync(); + + await blockchain.increaseTimeAsync(ONE_DAY_IN_SECONDS); + await rebalancingSetToken.startRebalance.sendTransactionAsync(); + }); + + it('should revert', async () => { + await expectRevertError(subject()); + }); + }); }); describe('#confirmPropose', async () => { @@ -670,7 +629,7 @@ contract('ETHTwentyDayMACOManager', accounts => { ); auctionTimeToPivot = ONE_DAY_IN_SECONDS.div(4); - const [riskOn, initialAllocationAddress] = await managerWrapper.getMACOInitialAllocationAsync( + const initialAllocationAddress = await managerWrapper.getMACOInitialAllocationAsync( stableCollateral, riskCollateral, ethMedianizer, @@ -678,6 +637,7 @@ contract('ETHTwentyDayMACOManager', accounts => { new BigNumber(20) ); + const movingAverageDays = new BigNumber(20); ethTwentyDayMACOManager = await managerWrapper.deployETHTwentyDayMACOManagerAsync( core.address, movingAverageOracle.address, @@ -687,8 +647,8 @@ contract('ETHTwentyDayMACOManager', accounts => { riskCollateral.address, factory.address, linearAuctionPriceCurve.address, + movingAverageDays, auctionTimeToPivot, - riskOn, ); proposalPeriod = ONE_DAY_IN_SECONDS; @@ -795,14 +755,6 @@ contract('ETHTwentyDayMACOManager', accounts => { expect(newAuctionPivotPrice).to.be.bignumber.equal(auctionPriceParameters['auctionPivotPrice']); }); - it('updates riskOn to false', async () => { - await subject(); - - const actualRiskOn = await ethTwentyDayMACOManager.riskOn.callAsync(); - - expect(actualRiskOn).to.equal(false); - }); - it('emits correct LogProposal event', async () => { const txHash = await subject(); @@ -817,9 +769,9 @@ contract('ETHTwentyDayMACOManager', accounts => { await SetTestUtils.assertLogEquivalence(formattedLogs, expectedLogs); }); - describe('but stable collateral is 5x valuable than risk collateral', async () => { + describe('but stable collateral is 4x valuable than risk collateral', async () => { before(async () => { - triggerPrice = ether(20); + triggerPrice = ether(25); lastPrice = triggerPrice; }); @@ -925,14 +877,6 @@ contract('ETHTwentyDayMACOManager', accounts => { expect(newAuctionPivotPrice).to.be.bignumber.equal(auctionPriceParameters['auctionPivotPrice']); }); - - it('updates riskOn to false', async () => { - await subject(); - - const actualRiskOn = await ethTwentyDayMACOManager.riskOn.callAsync(); - - expect(actualRiskOn).to.equal(false); - }); }); describe('but price has not dipped below MA', async () => { @@ -1035,14 +979,6 @@ contract('ETHTwentyDayMACOManager', accounts => { expect(newAuctionPivotPrice).to.be.bignumber.equal(auctionPriceParameters['auctionPivotPrice']); }); - it('updates riskOn to true', async () => { - await subject(); - - const actualRiskOn = await ethTwentyDayMACOManager.riskOn.callAsync(); - - expect(actualRiskOn).to.equal(true); - }); - it('emits correct LogProposal event', async () => { const txHash = await subject(); @@ -1057,9 +993,9 @@ contract('ETHTwentyDayMACOManager', accounts => { await SetTestUtils.assertLogEquivalence(formattedLogs, expectedLogs); }); - describe('but risk collateral is 5x valuable than stable collateral', async () => { + describe('but risk collateral is 4x valuable than stable collateral', async () => { before(async () => { - triggerPrice = ether(500); + triggerPrice = ether(400); lastPrice = triggerPrice; }); @@ -1165,14 +1101,6 @@ contract('ETHTwentyDayMACOManager', accounts => { expect(newAuctionPivotPrice).to.be.bignumber.equal(auctionPriceParameters['auctionPivotPrice']); }); - - it('updates riskOn to true', async () => { - await subject(); - - const actualRiskOn = await ethTwentyDayMACOManager.riskOn.callAsync(); - - expect(actualRiskOn).to.equal(true); - }); }); describe('but price has not gone above MA', async () => { @@ -1181,6 +1109,11 @@ contract('ETHTwentyDayMACOManager', accounts => { lastPrice = ether(150); }); + after(async () => { + triggerPrice = ether(170); + lastPrice = triggerPrice; + }); + it('should revert', async () => { await expectRevertError(subject()); }); @@ -1207,5 +1140,49 @@ contract('ETHTwentyDayMACOManager', accounts => { }); }); }); + + describe('when propose is called and rebalancing set token is in Proposal state', async () => { + beforeEach(async () => { + await blockchain.increaseTimeAsync(ONE_DAY_IN_SECONDS.div(4)); + await ethTwentyDayMACOManager.confirmPropose.sendTransactionAsync(); + + subjectTimeFastForward = new BigNumber(1); + }); + + it('should revert', async () => { + await expectRevertError(subject()); + }); + }); + + describe('when propose is called and rebalancing set token is in Rebalance state', async () => { + beforeEach(async () => { + // Issue currentSetToken + const initialAllocationTokenAddress = await rebalancingSetToken.currentSet.callAsync(); + const initialAllocationToken = await protocolWrapper.getSetTokenAsync(initialAllocationTokenAddress); + await core.issue.sendTransactionAsync( + initialAllocationToken.address, + ether(9), + {from: deployerAccount, gas: DEFAULT_GAS}, + ); + await erc20Wrapper.approveTransfersAsync([initialAllocationToken], transferProxy.address); + + // Use issued currentSetToken to issue rebalancingSetToken + await core.issue.sendTransactionAsync( + rebalancingSetToken.address, + ether(7), + { from: deployerAccount, gas: DEFAULT_GAS } + ); + + await blockchain.increaseTimeAsync(ONE_DAY_IN_SECONDS.div(4)); + await ethTwentyDayMACOManager.confirmPropose.sendTransactionAsync(); + + await blockchain.increaseTimeAsync(ONE_DAY_IN_SECONDS); + await rebalancingSetToken.startRebalance.sendTransactionAsync(); + }); + + it('should revert', async () => { + await expectRevertError(subject()); + }); + }); }); }); \ No newline at end of file diff --git a/utils/constants.ts b/utils/constants.ts index 9ef7dd2..5574f5a 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -15,7 +15,7 @@ export const ETH_DECIMALS = new BigNumber(10 ** 18); export const KYBER_RESERVE_CONFIGURED_RATE: BigNumber = new BigNumber('321556325999999997'); export const ONE: BigNumber = new BigNumber(1); export const ONE_DAY_IN_SECONDS = new BigNumber(86400); -export const RISK_COLLATERAL_NATURAL_UNIT = new BigNumber(100); +export const RISK_COLLATERAL_NATURAL_UNIT = new BigNumber(10 ** 6); export const SNAPSHOT_TIME_LOCK = new BigNumber(1); export const STABLE_COLLATERAL_NATURAL_UNIT = new BigNumber(10 ** 12); export const STANDARD_COMPONENT_UNIT = ether(1); diff --git a/utils/wrappers/managerWrapper.ts b/utils/wrappers/managerWrapper.ts index d0a225b..2cfed43 100644 --- a/utils/wrappers/managerWrapper.ts +++ b/utils/wrappers/managerWrapper.ts @@ -152,8 +152,8 @@ export class ManagerWrapper { riskCollateralAddress: Address, setTokenFactoryAddress: Address, auctionLibrary: Address, + movingAverageDays: BigNumber, auctionTimeToPivot: BigNumber = new BigNumber(100000), - riskOn: boolean, from: Address = this._tokenOwnerAddress ): Promise { const truffleRebalacingTokenManager = await ETHTwentyDayMACOManager.new( @@ -165,8 +165,8 @@ export class ManagerWrapper { riskCollateralAddress, setTokenFactoryAddress, auctionLibrary, + movingAverageDays, auctionTimeToPivot, - riskOn, { from }, ); @@ -184,14 +184,14 @@ export class ManagerWrapper { spotPriceOracle: MedianContract, movingAverageOracle: MovingAverageOracleContract, dataDays: BigNumber, - ): Promise<[boolean, Address]> { + ): Promise
{ const spotPrice = parseInt(await spotPriceOracle.read.callAsync()); const maPrice = parseInt(await movingAverageOracle.read.callAsync(dataDays)); if (spotPrice > maPrice) { - return [true, riskCollateral.address]; + return riskCollateral.address; } else { - return [false, stableCollateral.address]; + return stableCollateral.address; } }