diff --git a/contracts/core/lib/Rebalance.sol b/contracts/core/lib/Rebalance.sol index 2d9d7d77b..30acc4d64 100644 --- a/contracts/core/lib/Rebalance.sol +++ b/contracts/core/lib/Rebalance.sol @@ -25,28 +25,12 @@ pragma solidity 0.5.7; */ library Rebalance { - struct Price { - uint256 numerator; - uint256 denominator; - } - struct TokenFlow { address[] addresses; uint256[] inflow; uint256[] outflow; } - function composePrice( - uint256 _numerator, - uint256 _denominator - ) - internal - pure - returns(Price memory) - { - return Price({ numerator: _numerator, denominator: _denominator }); - } - function composeTokenFlow( address[] memory _addresses, uint256[] memory _inflow, diff --git a/contracts/core/liquidators/LinearAuctionLiquidator.sol b/contracts/core/liquidators/LinearAuctionLiquidator.sol index 4e45b9190..de8560fcb 100644 --- a/contracts/core/liquidators/LinearAuctionLiquidator.sol +++ b/contracts/core/liquidators/LinearAuctionLiquidator.sol @@ -27,6 +27,7 @@ import { Auction } from "./impl/Auction.sol"; import { LinearAuction } from "./impl/LinearAuction.sol"; import { Rebalance } from "../lib/Rebalance.sol"; import { RebalancingLibrary } from "../lib/RebalancingLibrary.sol"; +import { TwoAssetPriceBoundedLinearAuction } from "./impl/TwoAssetPriceBoundedLinearAuction.sol"; /** @@ -36,7 +37,7 @@ import { RebalancingLibrary } from "../lib/RebalancingLibrary.sol"; * Contract that holds all the state and functionality required for setting up, returning prices, and tearing * down linear auction rebalances for RebalancingSetTokens. */ -contract LinearAuctionLiquidator is LinearAuction, ILiquidator { +contract LinearAuctionLiquidator is TwoAssetPriceBoundedLinearAuction, ILiquidator { using SafeMath for uint256; ICore public core; @@ -68,7 +69,7 @@ contract LinearAuctionLiquidator is LinearAuction, ILiquidator { string memory _name ) public - LinearAuction( + TwoAssetPriceBoundedLinearAuction( _oracleWhiteList, _auctionPeriod, _rangeStart, @@ -100,7 +101,7 @@ contract LinearAuctionLiquidator is LinearAuction, ILiquidator { { _liquidatorData; // Pass linting - LinearAuction.validateRebalanceComponents( + TwoAssetPriceBoundedLinearAuction.validateTwoAssetPriceBoundedAuction( _currentSet, _nextSet ); @@ -209,8 +210,8 @@ contract LinearAuctionLiquidator is LinearAuction, ILiquidator { return RebalancingLibrary.AuctionPriceParameters({ auctionStartTime: auction(_set).startTime, auctionTimeToPivot: auctionPeriod, - auctionStartPrice: linearAuction(_set).startNumerator, - auctionPivotPrice: linearAuction(_set).endNumerator + auctionStartPrice: linearAuction(_set).startPrice, + auctionPivotPrice: linearAuction(_set).endPrice }); } diff --git a/contracts/core/liquidators/impl/Auction.sol b/contracts/core/liquidators/impl/Auction.sol index adf52c839..459666b2f 100644 --- a/contracts/core/liquidators/impl/Auction.sol +++ b/contracts/core/liquidators/impl/Auction.sol @@ -17,19 +17,15 @@ pragma solidity 0.5.7; pragma experimental "ABIEncoderV2"; -import { ERC20Detailed } from "openzeppelin-solidity/contracts/token/ERC20/ERC20Detailed.sol"; import { Math } from "openzeppelin-solidity/contracts/math/Math.sol"; import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; import { IOracle } from "set-protocol-strategies/contracts/meta-oracles/interfaces/IOracle.sol"; import { AddressArrayUtils } from "../../../lib/AddressArrayUtils.sol"; -import { CommonMath } from "../../../lib/CommonMath.sol"; import { ICore } from "../../interfaces/ICore.sol"; -import { IOracleWhiteList } from "../../interfaces/IOracleWhiteList.sol"; import { ISetToken } from "../../interfaces/ISetToken.sol"; import { Rebalance } from "../../lib/Rebalance.sol"; import { SetMath } from "../../lib/SetMath.sol"; -import { SetUSDValuation } from "../impl/SetUSDValuation.sol"; /** @@ -46,7 +42,7 @@ contract Auction { /* ============ Structs ============ */ struct Setup { - uint256 pricePrecision; + uint256 maxNaturalUnit; uint256 minimumBid; uint256 startTime; uint256 startingCurrentSets; @@ -56,20 +52,8 @@ contract Auction { uint256[] combinedNextSetUnits; } - /* ============ Constants ============ */ - uint256 constant public MINIMUM_PRICE_PRECISION = 1000; - - /* ============ State Variables ============ */ - IOracleWhiteList public oracleWhiteList; - - /** - * Auction constructor - * - * @param _oracleWhiteList Oracle WhiteList instance - */ - constructor(IOracleWhiteList _oracleWhiteList) public { - oracleWhiteList = _oracleWhiteList; - } + /* ============ Structs ============ */ + uint256 constant private CURVE_DENOMINATOR = 10 ** 18; /* ============ Auction Struct Methods ============ */ @@ -89,17 +73,11 @@ contract Auction { ) internal { - _auction.pricePrecision = calculatePricePrecision(_currentSet, _nextSet); - - uint256 minimumBid = calculateMinimumBid(_currentSet, _nextSet, _auction.pricePrecision); - - // remainingCurrentSets must be greater than minimumBid or no bidding would be allowed - require( - _startingCurrentSetQuantity >= minimumBid, - "Auction.initializeAuction: Not enough collateral to rebalance" + _auction.maxNaturalUnit = Math.max( + _currentSet.naturalUnit(), + _nextSet.naturalUnit() ); - _auction.minimumBid = minimumBid; _auction.startingCurrentSets = _startingCurrentSetQuantity; _auction.remainingCurrentSets = _startingCurrentSetQuantity; _auction.startTime = block.timestamp; @@ -157,19 +135,19 @@ contract Auction { * * @param _auction Auction Setup object * @param _quantity Amount of currentSets bidder is seeking to rebalance - * @param _price Struct of auction price numerator and denominator + * @param _price Value representing the auction numeartor */ function calculateTokenFlow( Setup storage _auction, uint256 _quantity, - Rebalance.Price memory _price + uint256 _price ) internal view returns (Rebalance.TokenFlow memory) { // Normalized quantity amount - uint256 unitsMultiplier = _quantity.div(_auction.minimumBid).mul(_auction.pricePrecision); + uint256 unitsMultiplier = _quantity.div(_auction.maxNaturalUnit); address[] memory memCombinedTokenArray = _auction.combinedTokenArray; @@ -194,57 +172,6 @@ contract Auction { return Rebalance.composeTokenFlow(memCombinedTokenArray, inflowUnitArray, outflowUnitArray); } - /** - * Calculates the price precision based on the USD values of the next and current Sets. - */ - function calculatePricePrecision( - ISetToken _currentSet, - ISetToken _nextSet - ) - internal - view - returns (uint256) - { - // Value the sets - uint256 currentSetUSDValue = calculateUSDValueOfSet(_currentSet); - uint256 nextSetUSDValue = calculateUSDValueOfSet(_nextSet); - - // If currentSetValue is 10x greater than nextSetValue calculate required bump in pricePrecision - if (currentSetUSDValue > nextSetUSDValue.mul(10)) { - // Round up valuation to nearest order of magnitude - uint256 orderOfMagnitude = CommonMath.ceilLog10(currentSetUSDValue.div(nextSetUSDValue)); - - // Apply order of magnitude to pricePrecision, since Log10 is rounded up subtract 1 order of - // magnitude - return MINIMUM_PRICE_PRECISION.mul(10 ** orderOfMagnitude).div(10); - } - - return MINIMUM_PRICE_PRECISION; - } - - /** - * Calculate the minimumBid allowed for the rebalance - * - * @param _currentSet The Set to rebalance from - * @param _nextSet The Set to rebalance to - * @param _pricePrecision Price precision used in auction - * @return Minimum bid amount - */ - function calculateMinimumBid( - ISetToken _currentSet, - ISetToken _nextSet, - uint256 _pricePrecision - ) - internal - view - returns (uint256) - { - uint256 currentSetNaturalUnit = _currentSet.naturalUnit(); - uint256 nextNaturalUnit = _nextSet.naturalUnit(); - return Math.max(currentSetNaturalUnit, nextNaturalUnit) - .mul(_pricePrecision); - } - /** * Computes the union of the currentSet and nextSet components * @@ -271,7 +198,7 @@ contract Auction { * @param _currentUnit Amount of token i in currentSet per minimum bid amount * @param _nextSetUnit Amount of token i in nextSet per minimum bid amount * @param _unitsMultiplier Bid amount normalized to number of minimum bid amounts - * @param _price Struct of auction price numerator and denominator + * @param _price Auction price numerator with 10 ** 18 as denominator * @return inflowUnit Amount of token i transferred into the system * @return outflowUnit Amount of token i transferred to the bidder */ @@ -279,7 +206,7 @@ contract Auction { uint256 _currentUnit, uint256 _nextSetUnit, uint256 _unitsMultiplier, - Rebalance.Price memory _price + uint256 _price ) internal pure @@ -316,19 +243,19 @@ contract Auction { uint256 outflowUnit; // Use if statement to check if token inflow or outflow - if (_nextSetUnit.mul(_price.denominator) > _currentUnit.mul(_price.numerator)) { + if (_nextSetUnit.mul(CURVE_DENOMINATOR) > _currentUnit.mul(_price)) { // Calculate inflow amount inflowUnit = _unitsMultiplier.mul( - _nextSetUnit.mul(_price.denominator).sub(_currentUnit.mul(_price.numerator)) - ).div(_price.numerator); + _nextSetUnit.mul(CURVE_DENOMINATOR).sub(_currentUnit.mul(_price)) + ).div(_price); // Set outflow amount to 0 for component i, since tokens need to be injected in rebalance outflowUnit = 0; } else { // Calculate outflow amount outflowUnit = _unitsMultiplier.mul( - _currentUnit.mul(_price.numerator).sub(_nextSetUnit.mul(_price.denominator)) - ).div(_price.numerator); + _currentUnit.mul(_price).sub(_nextSetUnit.mul(CURVE_DENOMINATOR)) + ).div(_price); // Set inflow amount to 0 for component i, since tokens need to be returned in rebalance inflowUnit = 0; @@ -358,12 +285,10 @@ contract Auction { { address[] memory combinedTokenArray = _auction.combinedTokenArray; uint256[] memory combinedUnits = new uint256[](combinedTokenArray.length); - uint256 pricePrecisionMem = _auction.pricePrecision; for (uint256 i = 0; i < combinedTokenArray.length; i++) { combinedUnits[i] = calculateCombinedUnit( _set, - _auction.minimumBid, - pricePrecisionMem, + _auction.maxNaturalUnit, combinedTokenArray[i] ); } @@ -375,15 +300,13 @@ contract Auction { * Calculations the unit amount of Token to include in the the combined Set units. * * @param _setToken Information on the SetToken - * @param _minimumBid Minimum bid amount - * @param _pricePrecision Price precision used in auction + * @param _maxNaturalUnit Max natural unit of two sets in rebalance * @param _component Current component in iteration * @return Unit inflow/outflow */ function calculateCombinedUnit( ISetToken _setToken, - uint256 _minimumBid, - uint256 _pricePrecision, + uint256 _maxNaturalUnit, address _component ) private @@ -401,8 +324,7 @@ contract Auction { return calculateTransferValue( _setToken.getUnits()[indexCurrent], _setToken.naturalUnit(), - _minimumBid, - _pricePrecision + _maxNaturalUnit ); } @@ -415,31 +337,18 @@ contract Auction { * * @param _unit Units of the component token * @param _naturalUnit Natural unit of the Set token - * @param _minimumBid Minimum bid amount - * @param _pricePrecision Price precision used in auction + * @param _maxNaturalUnit Max natural unit of two sets in rebalance * @return uint256 Amount of tokens per standard bid amount (minimumBid/priceDivisor) */ function calculateTransferValue( uint256 _unit, uint256 _naturalUnit, - uint256 _minimumBid, - uint256 _pricePrecision + uint256 _maxNaturalUnit ) private pure returns (uint256) { - return SetMath.setToComponent(_minimumBid, _unit, _naturalUnit) - .div(_pricePrecision); - } - - /** - * Calculate USD value of passed Set - * - * @param _set Instance of SetToken - * @return USDValue USD Value of the Set Token - */ - function calculateUSDValueOfSet(ISetToken _set) internal view returns(uint256) { - return SetUSDValuation.calculateSetTokenDollarValue(_set, oracleWhiteList); + return SetMath.setToComponent(_maxNaturalUnit, _unit, _naturalUnit); } } \ No newline at end of file diff --git a/contracts/core/liquidators/impl/LinearAuction.sol b/contracts/core/liquidators/impl/LinearAuction.sol index 28d7cf80f..b096ffb96 100644 --- a/contracts/core/liquidators/impl/LinearAuction.sol +++ b/contracts/core/liquidators/impl/LinearAuction.sol @@ -20,7 +20,6 @@ pragma experimental "ABIEncoderV2"; import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; import { Auction } from "./Auction.sol"; -import { IOracleWhiteList } from "../../interfaces/IOracleWhiteList.sol"; import { ISetToken } from "../../interfaces/ISetToken.sol"; import { Rebalance } from "../../lib/Rebalance.sol"; @@ -38,35 +37,24 @@ contract LinearAuction is Auction { struct State { Auction.Setup auction; uint256 endTime; - uint256 startNumerator; - uint256 endNumerator; + uint256 startPrice; + uint256 endPrice; } /* ============ State Variables ============ */ uint256 public auctionPeriod; // Length in seconds of auction - uint256 public rangeStart; // Percentage below FairValue to begin auction at - uint256 public rangeEnd; // Percentage above FairValue to end auction at /** * LinearAuction constructor * - * @param _oracleWhiteList Oracle WhiteList instance * @param _auctionPeriod Length of auction - * @param _rangeStart Percentage below FairValue to begin auction at - * @param _rangeEnd Percentage above FairValue to end auction at */ constructor( - IOracleWhiteList _oracleWhiteList, - uint256 _auctionPeriod, - uint256 _rangeStart, - uint256 _rangeEnd + uint256 _auctionPeriod ) public - Auction(_oracleWhiteList) { auctionPeriod = _auctionPeriod; - rangeStart = _rangeStart; - rangeEnd = _rangeEnd; } /* ============ Internal Functions ============ */ @@ -94,28 +82,24 @@ contract LinearAuction is Auction { _startingCurrentSetQuantity ); - uint256 fairValue = calculateFairValue(_currentSet, _nextSet, _linearAuction.auction.pricePrecision); - _linearAuction.startNumerator = calculateStartNumerator(fairValue); - _linearAuction.endNumerator = calculateEndNumerator(fairValue); + uint256 minimumBid = calculateMinimumBid(_linearAuction.auction, _currentSet, _nextSet); + + // remainingCurrentSets must be greater than minimumBid or no bidding would be allowed + require( + _startingCurrentSetQuantity >= minimumBid, + "Auction.initializeAuction: Not enough collateral to rebalance" + ); + + _linearAuction.auction.minimumBid = minimumBid; + + _linearAuction.startPrice = calculateStartPrice(_linearAuction.auction, _currentSet, _nextSet); + _linearAuction.endPrice = calculateEndPrice(_linearAuction.auction, _currentSet, _nextSet); + _linearAuction.endTime = block.timestamp.add(auctionPeriod); } /* ============ Internal View Functions ============ */ - function validateRebalanceComponents( - ISetToken _currentSet, - ISetToken _nextSet - ) - internal - view - { - address[] memory combinedTokenArray = Auction.getCombinedTokenArray(_currentSet, _nextSet); - require( - oracleWhiteList.areValidAddresses(combinedTokenArray), - "LinearAuction.validateRebalanceComponents: Passed token does not have matching oracle." - ); - } - /** * Returns the TokenFlow based on the current price */ @@ -134,13 +118,6 @@ contract LinearAuction is Auction { ); } - /** - * Returns the linear price based on the current timestamp - */ - function getPrice(State storage _linearAuction) internal view returns (Rebalance.Price memory) { - return Rebalance.composePrice(getNumerator(_linearAuction), _linearAuction.auction.pricePrecision); - } - /** * Auction failed is defined the timestamp breacnhing the auction end time and * the auction not being complete @@ -153,57 +130,67 @@ contract LinearAuction is Auction { } /** - * Returns the linear price based on the current timestamp. Returns the endNumerator - * if time has exceeded the auciton period + * Returns the price based on the current timestamp. Returns the endPrice + * if time has exceeded the auction period * * @param _linearAuction Linear Auction State object * @return price uint representing the current price */ - function getNumerator(State storage _linearAuction) internal view returns (uint256) { + function getPrice(State storage _linearAuction) internal view returns (uint256) { uint256 elapsed = block.timestamp.sub(_linearAuction.auction.startTime); // If current time has elapsed if (elapsed >= auctionPeriod) { - return _linearAuction.endNumerator; + return _linearAuction.endPrice; } else { - uint256 range = _linearAuction.endNumerator.sub(_linearAuction.startNumerator); + uint256 range = _linearAuction.endPrice.sub(_linearAuction.startPrice); uint256 elapsedPrice = elapsed.mul(range).div(auctionPeriod); - return _linearAuction.startNumerator.add(elapsedPrice); + return _linearAuction.startPrice.add(elapsedPrice); } } /** - * Calculates the fair value based on the USD values of the next and current Sets. + * Abstract function that must be implemented. + * Calculate the minimumBid allowed for the rebalance. + * + * @param _auction Auction object + * @param _currentSet The Set to rebalance from + * @param _nextSet The Set to rebalance to + * @return Minimum bid amount */ - function calculateFairValue( + function calculateMinimumBid( + Setup storage _auction, ISetToken _currentSet, - ISetToken _nextSet, - uint256 _pricePrecision + ISetToken _nextSet ) internal view - returns (uint256) - { - uint256 currentSetUSDValue = Auction.calculateUSDValueOfSet(_currentSet); - uint256 nextSetUSDValue = Auction.calculateUSDValueOfSet(_nextSet); - - return nextSetUSDValue.mul(_pricePrecision).div(currentSetUSDValue); - } + returns (uint256); /** + * Abstract function that must be implemented. * Calculates the linear auction start price */ - function calculateStartNumerator(uint256 _fairValue) internal view returns(uint256) { - uint256 startRange = _fairValue.mul(rangeStart).div(100); - return _fairValue.sub(startRange); - } + function calculateStartPrice( + Auction.Setup storage _auction, + ISetToken _currentSet, + ISetToken _nextSet + ) + internal + view + returns(uint256); /** + * Abstract function that must be implemented. * Calculates the linear auction end price */ - function calculateEndNumerator(uint256 _fairValue) internal view returns(uint256) { - uint256 endRange = _fairValue.mul(rangeEnd).div(100); - return _fairValue.add(endRange); - } + function calculateEndPrice( + Auction.Setup storage _auction, + ISetToken _currentSet, + ISetToken _nextSet + ) + internal + view + returns(uint256); } \ No newline at end of file diff --git a/contracts/core/liquidators/impl/TwoAssetPriceBoundedLinearAuction.sol b/contracts/core/liquidators/impl/TwoAssetPriceBoundedLinearAuction.sol new file mode 100644 index 000000000..fdd616920 --- /dev/null +++ b/contracts/core/liquidators/impl/TwoAssetPriceBoundedLinearAuction.sol @@ -0,0 +1,392 @@ +/* + Copyright 2019 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. +*/ + +pragma solidity 0.5.7; +pragma experimental "ABIEncoderV2"; + +import { ERC20Detailed } from "openzeppelin-solidity/contracts/token/ERC20/ERC20Detailed.sol"; +import { Math } from "openzeppelin-solidity/contracts/math/Math.sol"; +import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import { IOracle } from "set-protocol-strategies/contracts/meta-oracles/interfaces/IOracle.sol"; + +import { Auction } from "./Auction.sol"; +import { CommonMath } from "../../../lib/CommonMath.sol"; +import { IOracleWhiteList } from "../../interfaces/IOracleWhiteList.sol"; +import { ISetToken } from "../../interfaces/ISetToken.sol"; +import { LinearAuction } from "./LinearAuction.sol"; + + +/** + * @title TwoAssetPriceBoundedLinearAuction + * @author Set Protocol + * + * Contract to calculate minimumBid and auction start bounds for auctions containing only + * an asset pair. + */ +contract TwoAssetPriceBoundedLinearAuction is LinearAuction { + using SafeMath for uint256; + using CommonMath for uint256; + + /* ============ Struct ============ */ + struct AssetInfo { + uint256 price; + uint256 fullUnit; + } + + /* ============ Constants ============ */ + uint256 constant private CURVE_DENOMINATOR = 10 ** 18; + uint256 constant private ONE = 1; + // Minimum token flow allowed at spot price in auction + uint256 constant private MIN_SPOT_TOKEN_FLOW_SCALED = 10 ** 21; + uint256 constant private ONE_HUNDRED = 100; + + /* ============ State Variables ============ */ + IOracleWhiteList public oracleWhiteList; + uint256 public rangeStart; // Percentage below FairValue to begin auction at + uint256 public rangeEnd; // Percentage above FairValue to end auction at + + /** + * TwoAssetPriceBoundedLinearAuction constructor + * + * @param _auctionPeriod Length of auction + * @param _rangeStart Percentage below FairValue to begin auction at + * @param _rangeEnd Percentage above FairValue to end auction at + */ + constructor( + IOracleWhiteList _oracleWhiteList, + uint256 _auctionPeriod, + uint256 _rangeStart, + uint256 _rangeEnd + ) + public + LinearAuction(_auctionPeriod) + { + oracleWhiteList = _oracleWhiteList; + rangeStart = _rangeStart; + rangeEnd = _rangeEnd; + } + + /* ============ Internal Functions ============ */ + + /** + * Validates that the auction only includes two components and the components are valid. + */ + function validateTwoAssetPriceBoundedAuction( + ISetToken _currentSet, + ISetToken _nextSet + ) + internal + view + { + address[] memory combinedTokenArray = Auction.getCombinedTokenArray(_currentSet, _nextSet); + require( + combinedTokenArray.length == 2, + "TwoAssetPriceBoundedLinearAuction: Only two components are allowed." + ); + + require( + oracleWhiteList.areValidAddresses(combinedTokenArray), + "TwoAssetPriceBoundedLinearAuction: Passed token does not have matching oracle." + ); + } + + /** + * Calculates the minimumBid. First calculates the minimum token flow for the pair at fair value using + * maximum natural unit of two Sets. If that token flow is below 1000 units then calculate minimumBid + * as such: + * + * minimumBid = maxNaturalUnit*1000/min(tokenFlow) + * + * Else, set minimumBid equal to maxNaturalUnit. This is to ensure that around fair value there is ample + * granualarity in asset pair price changes and not large discontinuities. + * + * @param _auction Auction object + * @param _currentSet CurrentSet, unused in this implementation + * @param _nextSet NextSet, unused in this implementation + */ + function calculateMinimumBid( + Auction.Setup storage _auction, + ISetToken _currentSet, + ISetToken _nextSet + ) + internal + view + returns (uint256) + { + // Get full Unit amount and price for each asset + AssetInfo memory assetOne = getAssetInfo(_auction.combinedTokenArray[0]); + AssetInfo memory assetTwo = getAssetInfo(_auction.combinedTokenArray[1]); + + // Calculate current spot price as assetOne/assetTwo + uint256 spotPrice = calculateSpotPrice(assetOne.price, assetTwo.price); + + // Calculate auction price at current asset pair spot price + uint256 auctionFairValue = convertAssetPairPriceToAuctionPrice( + _auction, + spotPrice, + assetOne.fullUnit, + assetTwo.fullUnit + ); + + uint256 minimumBidMultiplier = 0; + for (uint8 i = 0; i < _auction.combinedTokenArray.length; i++) { + // Get token flow at fair value for asset i, using an amount equal to ONE maxNaturalUnit + // Hence the ONE.scale() + ( + uint256 tokenInflowScaled, + uint256 tokenOutflowScaled + ) = Auction.calculateInflowOutflow( + _auction.combinedCurrentSetUnits[i], + _auction.combinedNextSetUnits[i], + ONE.scale(), + auctionFairValue + ); + + // One returned number from previous function will be zero so use max to get tokenFlow + uint256 tokenFlowScaled = Math.max(tokenInflowScaled, tokenOutflowScaled); + + // Divide minimum spot token flow (1000 units) by token flow if more than minimumBidMultiplier + // update minimumBidMultiplier + uint256 currentMinBidMultiplier = MIN_SPOT_TOKEN_FLOW_SCALED.divCeil(tokenFlowScaled); + minimumBidMultiplier = currentMinBidMultiplier > minimumBidMultiplier ? + currentMinBidMultiplier : + minimumBidMultiplier; + } + + // Multiply the minimumBidMultiplier by maxNaturalUnit to get minimumBid + return _auction.maxNaturalUnit.mul(minimumBidMultiplier); + } + + /** + * Calculates the linear auction start price. A target asset pair (i.e. ETH/DAI) price is calculated + * to start the auction at, that asset pair price is then translated into the equivalent auction price. + * + * @param _auction Auction object + * @param _currentSet CurrentSet, unused in this implementation + * @param _nextSet NextSet, unused in this implementation + */ + function calculateStartPrice( + Auction.Setup storage _auction, + ISetToken _currentSet, + ISetToken _nextSet + ) + internal + view + returns(uint256) + { + // Get full Unit amount and price for each asset + AssetInfo memory assetOne = getAssetInfo(_auction.combinedTokenArray[0]); + AssetInfo memory assetTwo = getAssetInfo(_auction.combinedTokenArray[1]); + + // Calculate current asset pair spot price as assetOne/assetTwo + uint256 spotPrice = calculateSpotPrice(assetOne.price, assetTwo.price); + + // Check to see if asset pair price is increasing or decreasing as time passes + bool isTokenFlowIncreasing = isTokenFlowIncreasing( + _auction, + spotPrice, + assetOne.fullUnit, + assetTwo.fullUnit + ); + + // If price implied by token flows is increasing then target price we are using for lower bound + // is below current spot price, if flows decreasing set target price above spotPrice + uint256 startPairPrice; + if (isTokenFlowIncreasing) { + startPairPrice = spotPrice.mul(ONE_HUNDRED.sub(rangeStart)).div(ONE_HUNDRED); + } else { + startPairPrice = spotPrice.mul(ONE_HUNDRED.add(rangeStart)).div(ONE_HUNDRED); + } + + // Convert start asset pair price to equivalent auction price + return convertAssetPairPriceToAuctionPrice( + _auction, + startPairPrice, + assetOne.fullUnit, + assetTwo.fullUnit + ); + } + + /** + * Calculates the linear auction end price. A target asset pair (i.e. ETH/DAI) price is calculated + * to end the auction at, that asset pair price is then translated into the equivalent auction price. + * + * @param _auction Auction object + * @param _currentSet CurrentSet, unused in this implementation + * @param _nextSet NextSet, unused in this implementation + */ + function calculateEndPrice( + Auction.Setup storage _auction, + ISetToken _currentSet, + ISetToken _nextSet + ) + internal + view + returns(uint256) + { + // Get full Unit amount and price for each asset + AssetInfo memory assetOne = getAssetInfo(_auction.combinedTokenArray[0]); + AssetInfo memory assetTwo = getAssetInfo(_auction.combinedTokenArray[1]); + + // Calculate current spot price as assetOne/assetTwo + uint256 spotPrice = calculateSpotPrice(assetOne.price, assetTwo.price); + + // Check to see if asset pair price is increasing or decreasing as time passes + bool isTokenFlowIncreasing = isTokenFlowIncreasing( + _auction, + spotPrice, + assetOne.fullUnit, + assetTwo.fullUnit + ); + + // If price implied by token flows is increasing then target price we are using for upper bound + // is above current spot price, if flows decreasing set target price below spotPrice + uint256 endPairPrice; + if (isTokenFlowIncreasing) { + endPairPrice = spotPrice.mul(ONE_HUNDRED.add(rangeEnd)).div(ONE_HUNDRED); + } else { + endPairPrice = spotPrice.mul(ONE_HUNDRED.sub(rangeEnd)).div(ONE_HUNDRED); + } + + // Convert end asset pair price to equivalent auction price + return convertAssetPairPriceToAuctionPrice( + _auction, + endPairPrice, + assetOne.fullUnit, + assetTwo.fullUnit + ); + } + + /* ============ Private Functions ============ */ + + /** + * Determines if asset pair price is increasing or decreasing as time passed in auction. Used to set the + * auction price bounds. Below a refers to any asset and subscripts c, n, d mean currentSetUnit, nextSetUnit + * and fullUnit amount, respectively. pP and pD refer to auction price and auction denominator. Asset pair + * price is defined as such: + * + * assetPrice = abs(assetTwoOutflow/assetOneOutflow) + * + * The equation for an outflow is given by (a_c/a_d)*pP - (a_n/a_d)*pD). It can be proven that the derivative + * of this equation is always increasing. Thus by determining the sign of the assetOneOutflow (where a negative + * amount signifies an inflow) it can be determined whether the asset pair price is increasing or decreasing. + * + * For example, if assetOneOutflow is negative it means that the denominator is getting smaller as time passes + * and thus the assetPrice is increasing during the auction. + * + * @param _auction Auction object + * @param _spotPrice Current spot price provided by asset oracles + * @param _assetOneFullUnit Units in one full unit of assetOne + * @param _assetTwoFullUnit Units in one full unit of assetTwo + */ + function isTokenFlowIncreasing( + Auction.Setup storage _auction, + uint256 _spotPrice, + uint256 _assetOneFullUnit, + uint256 _assetTwoFullUnit + ) + private + view + returns (bool) + { + // Calculate auction price at current asset pair spot price + uint256 auctionFairValue = convertAssetPairPriceToAuctionPrice( + _auction, + _spotPrice, + _assetOneFullUnit, + _assetTwoFullUnit + ); + + // Determine whether outflow for assetOne is positive or negative, if positive then asset pair price is + // increasing, else decreasing. + return _auction.combinedNextSetUnits[0].mul(CURVE_DENOMINATOR) > + _auction.combinedCurrentSetUnits[0].mul(auctionFairValue); + } + + /** + * Convert an asset pair price to the equivalent auction price where a1 refers to assetOne and a2 refers to assetTwo + * and subscripts c, n, d mean currentSetUnit, nextSetUnit and fullUnit amount, respectively. pP and pD refer to auction + * price and auction denominator: + * + * assetPrice = abs(assetTwoOutflow/assetOneOutflow) + * + * assetPrice = ((a2_c/a2_d)*pP - (a2_n/a2_d)*pD) / ((a1_c/a1_d)*pP - (a1_n/a1_d)*pD) + * + * We know assetPrice so we isolate for pP: + * + * pP = pD((a2_n/a2_d)+assetPrice*(a1_n/a1_d)) / (a2_c/a2_d)+assetPrice*(a1_c/a1_d) + * + * This gives us the auction price that matches with the passed asset pair price. + * + * @param _auction Auction object + * @param _targetPrice Target asset pair price + * @param _assetOneFullUnit Units in one full unit of assetOne + * @param _assetTwoFullUnit Units in one full unit of assetTwo + */ + function convertAssetPairPriceToAuctionPrice( + Auction.Setup storage _auction, + uint256 _targetPrice, + uint256 _assetOneFullUnit, + uint256 _assetTwoFullUnit + ) + private + view + returns (uint256) + { + // Calculate the numerator for the above equation. In order to ensure no rounding down errors we distribute the auction + // denominator. Additionally, since the price is passed as an 18 decimal number in order to maintain consistency we + // have to scale the first term up accordingly + uint256 calcNumerator = _auction.combinedNextSetUnits[1].mul(CURVE_DENOMINATOR).scale().div(_assetTwoFullUnit).add( + _targetPrice.mul(_auction.combinedNextSetUnits[0]).mul(CURVE_DENOMINATOR).div(_assetOneFullUnit) + ); + + // Calculate the denominator for the above equation. As above we we have to scale the first term match the 18 decimal + // price. Furthermore since we are not guaranteed that targetPrice * a1_c > a1_d we have to scale the second term and + // thus also the first term in order to match (hence the two scale() in the first term) + uint256 calcDenominator = _auction.combinedCurrentSetUnits[1].scale().scale().div(_assetTwoFullUnit).add( + _targetPrice.mul(_auction.combinedCurrentSetUnits[0]).scale().div(_assetOneFullUnit) + ); + + // Here the scale required to account for the 18 decimal price cancels out since it was applied to both the numerator + // and denominator. However, there was an extra scale applied to the denominator that we need to remove, in order to + // do so we'll just apply another scale to the numerator before dividing since 1/(1/10 ** 18) = 10 ** 18! + return calcNumerator.scale().div(calcDenominator); + } + + /** + * Get fullUnit amount and price of given asset. + * + * @param _asset Address of auction to get information from + */ + function getAssetInfo(address _asset) private view returns(AssetInfo memory) { + address assetOracle = oracleWhiteList.getOracleAddressByToken(_asset); + uint256 assetPrice = IOracle(assetOracle).read(); + + uint256 decimals = ERC20Detailed(_asset).decimals(); + + return AssetInfo({ + price: assetPrice, + fullUnit: CommonMath.safePower(10, decimals) + }); + } + + /** + * Calculate asset pair price given two prices. + */ + function calculateSpotPrice(uint256 _assetOnePrice, uint256 _assetTwoPrice) private view returns(uint256) { + return _assetOnePrice.scale().div(_assetTwoPrice); + } +} \ No newline at end of file diff --git a/contracts/lib/CommonMath.sol b/contracts/lib/CommonMath.sol index e7b9eac1a..c9a7dd274 100644 --- a/contracts/lib/CommonMath.sol +++ b/contracts/lib/CommonMath.sol @@ -103,6 +103,17 @@ library CommonMath { return result; } + /** + * @dev Performs division where if there is a modulo, the value is rounded up + */ + function divCeil(uint256 a, uint256 b) + internal + pure + returns(uint256) + { + return a.mod(b) > 0 ? a.div(b).add(1) : a.div(b); + } + /** * Checks for rounding errors and returns value of potential partial amounts of a principal * diff --git a/contracts/mocks/core/liquidators/impl/AuctionMock.sol b/contracts/mocks/core/liquidators/impl/AuctionMock.sol index 705a8146f..472a9af42 100644 --- a/contracts/mocks/core/liquidators/impl/AuctionMock.sol +++ b/contracts/mocks/core/liquidators/impl/AuctionMock.sol @@ -1,5 +1,7 @@ pragma solidity 0.5.7; +import { Math } from "openzeppelin-solidity/contracts/math/Math.sol"; + import { Auction } from "../../../../core/liquidators/impl/Auction.sol"; import { ISetToken } from "../../../../core/interfaces/ISetToken.sol"; import { IOracleWhiteList } from "../../../../core/interfaces/IOracleWhiteList.sol"; @@ -8,11 +10,6 @@ import { IOracleWhiteList } from "../../../../core/interfaces/IOracleWhiteList.s contract AuctionMock is Auction { Auction.Setup public auction; - constructor(IOracleWhiteList _oracleWhiteList) - public - Auction(_oracleWhiteList) - {} - function initializeAuction( ISetToken _currentSet, ISetToken _nextSet, @@ -21,6 +18,8 @@ contract AuctionMock is Auction { external { super.initializeAuction(auction, _currentSet, _nextSet, _startingCurrentSetQuantity); + + auction.minimumBid = calculateMinimumBid(auction, _currentSet, _nextSet); } function reduceRemainingCurrentSets( @@ -58,5 +57,20 @@ contract AuctionMock is Auction { function combinedNextSetUnits() external view returns(uint256[] memory) { return auction.combinedNextSetUnits; } + + function calculateMinimumBid( + Setup storage _auction, + ISetToken _currentSet, + ISetToken _nextSet + ) + internal + view + returns (uint256) + { + return Math.max( + _currentSet.naturalUnit(), + _nextSet.naturalUnit() + ); + } } diff --git a/contracts/mocks/core/liquidators/impl/LinearAuctionMock.sol b/contracts/mocks/core/liquidators/impl/LinearAuctionMock.sol index 7dfebb1e1..34f105132 100644 --- a/contracts/mocks/core/liquidators/impl/LinearAuctionMock.sol +++ b/contracts/mocks/core/liquidators/impl/LinearAuctionMock.sol @@ -1,7 +1,10 @@ pragma solidity 0.5.7; pragma experimental "ABIEncoderV2"; +import { Math } from "openzeppelin-solidity/contracts/math/Math.sol"; + import { Auction } from "../../../../core/liquidators/impl/Auction.sol"; +import { CommonMath } from "../../../../lib/CommonMath.sol"; import { LinearAuction } from "../../../../core/liquidators/impl/LinearAuction.sol"; import { IOracleWhiteList } from "../../../../core/interfaces/IOracleWhiteList.sol"; import { ISetToken } from "../../../../core/interfaces/ISetToken.sol"; @@ -9,7 +12,13 @@ import { Rebalance } from "../../../../core/lib/Rebalance.sol"; import { SetUSDValuation } from "../../../../core/liquidators/impl/SetUSDValuation.sol"; contract LinearAuctionMock is LinearAuction { + using CommonMath for uint256; + LinearAuction.State public auction; + IOracleWhiteList public oracleWhiteList; + + uint256 public rangeStart; // Percentage below FairValue to begin auction at + uint256 public rangeEnd; // Percentage above FairValue to end auction at constructor( IOracleWhiteList _oracleWhiteList, @@ -19,12 +28,59 @@ contract LinearAuctionMock is LinearAuction { ) public LinearAuction( - _oracleWhiteList, - _auctionPeriod, - _rangeStart, - _rangeEnd + _auctionPeriod ) - {} + { + oracleWhiteList = _oracleWhiteList; + rangeStart = _rangeStart; + rangeEnd = _rangeEnd; + } + + function calculateStartPrice( + Auction.Setup storage _auction, + ISetToken _currentSet, + ISetToken _nextSet + ) + internal + view + returns(uint256) + { + uint256 fairValue = calculateFairValue(_currentSet, _nextSet); + uint256 startRange = fairValue.mul(rangeStart).div(100); + return fairValue.sub(startRange); + } + + function calculateEndPrice( + Auction.Setup storage _auction, + ISetToken _currentSet, + ISetToken _nextSet + ) + internal + view + returns(uint256) + { + uint256 fairValue = calculateFairValue(_currentSet, _nextSet); + uint256 endRange = fairValue.mul(rangeEnd).div(100); + return fairValue.add(endRange); + } + + /** + * Calculates the fair value based on the USD values of the next and current Sets. + * Returns a scaled value + */ + function calculateFairValue( + ISetToken _currentSet, + ISetToken _nextSet + ) + internal + view + returns (uint256) + { + uint256 currentSetUSDValue = calculateUSDValueOfSet(_currentSet); + uint256 nextSetUSDValue = calculateUSDValueOfSet(_nextSet); + + return nextSetUSDValue.scale().div(currentSetUSDValue); + } function initializeLinearAuction( ISetToken _currentSet, @@ -48,7 +104,7 @@ contract LinearAuctionMock is LinearAuction { return super.hasAuctionFailed(auction); } - function getPrice() external view returns(Rebalance.Price memory) { + function getPrice() external view returns(uint256) { return super.getPrice(auction); } @@ -58,12 +114,29 @@ contract LinearAuctionMock is LinearAuction { return super.getTokenFlow(auction, _quantity); } - function getNumerator() external view returns(uint256) { - return super.getNumerator(auction); + /** + * Calculate USD value of passed Set + * + * @param _set Instance of SetToken + * @return USDValue USD Value of the Set Token + */ + function calculateUSDValueOfSet(ISetToken _set) internal view returns(uint256) { + return SetUSDValuation.calculateSetTokenDollarValue(_set, oracleWhiteList); } - function calculateUSDValueOfSet(ISetToken _set) internal view returns(uint256) { - return super.calculateUSDValueOfSet(_set); + function calculateMinimumBid( + Setup storage _auction, + ISetToken _currentSet, + ISetToken _nextSet + ) + internal + view + returns (uint256) + { + return Math.max( + _currentSet.naturalUnit(), + _nextSet.naturalUnit() + ); } } diff --git a/contracts/mocks/core/liquidators/impl/TwoAssetPriceBoundedLinearAuctionMock.sol b/contracts/mocks/core/liquidators/impl/TwoAssetPriceBoundedLinearAuctionMock.sol new file mode 100644 index 000000000..0f1ebe46a --- /dev/null +++ b/contracts/mocks/core/liquidators/impl/TwoAssetPriceBoundedLinearAuctionMock.sol @@ -0,0 +1,98 @@ +/* + Copyright 2019 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. +*/ + +pragma solidity 0.5.7; +pragma experimental "ABIEncoderV2"; + +import { ERC20Detailed } from "openzeppelin-solidity/contracts/token/ERC20/ERC20Detailed.sol"; +import { Math } from "openzeppelin-solidity/contracts/math/Math.sol"; +import { IOracle } from "set-protocol-strategies/contracts/meta-oracles/interfaces/IOracle.sol"; + +import { LinearAuction } from "../../../../core/liquidators/impl/LinearAuction.sol"; +import { ISetToken } from "../../../../core/interfaces/ISetToken.sol"; +import { IOracleWhiteList } from "../../../../core/interfaces/IOracleWhiteList.sol"; +import { TwoAssetPriceBoundedLinearAuction } from "../../../../core/liquidators/impl/TwoAssetPriceBoundedLinearAuction.sol"; + +/** + * @title TwoAssetPriceBoundedLinearAuction + * @author Set Protocol + * + */ +contract TwoAssetPriceBoundedLinearAuctionMock is TwoAssetPriceBoundedLinearAuction { + + LinearAuction.State public auctionInfo; + + constructor( + IOracleWhiteList _oracleWhiteList, + uint256 _auctionPeriod, + uint256 _rangeStart, + uint256 _rangeEnd + ) + public + TwoAssetPriceBoundedLinearAuction( + _oracleWhiteList, + _auctionPeriod, + _rangeStart, + _rangeEnd + ) + {} + + // To test + function validateTwoAssetPriceBoundedAuctionMock(ISetToken _currentSet,ISetToken _nextSet) external view { + validateTwoAssetPriceBoundedAuction(_currentSet, _nextSet); + } + + function calculateMinimumBid(ISetToken _currentSet, ISetToken _nextSet) external returns(uint256) { + auctionInfo.auction.maxNaturalUnit = Math.max( + _currentSet.naturalUnit(), + _nextSet.naturalUnit() + ); + + return super.calculateMinimumBid(auctionInfo.auction, _currentSet, _nextSet); + } + + function calculateStartPriceMock() external view returns(uint256) { + ISetToken currentSet = ISetToken(address(0)); + ISetToken nextSet = ISetToken(address(0)); + return super.calculateStartPrice(auctionInfo.auction, currentSet, nextSet); + } + + function calculateEndPriceMock() external view returns(uint256) { + ISetToken currentSet = ISetToken(address(0)); + ISetToken nextSet = ISetToken(address(0)); + return super.calculateEndPrice(auctionInfo.auction, currentSet, nextSet); + } + + function parameterizeAuction( + address[] calldata _combinedTokenArray, + uint256[] calldata _combinedCurrentSetUnits, + uint256[] calldata _combinedNextSetUnits + ) + external + { + auctionInfo.auction.combinedTokenArray = _combinedTokenArray; + auctionInfo.auction.combinedCurrentSetUnits = _combinedCurrentSetUnits; + auctionInfo.auction.combinedNextSetUnits = _combinedNextSetUnits; + } + + function getCombinedTokenArray() + external + view + returns(address[] memory) + { + return auctionInfo.auction.combinedTokenArray; + } +} \ No newline at end of file diff --git a/contracts/mocks/lib/CommonMathMock.sol b/contracts/mocks/lib/CommonMathMock.sol index e7252be67..aef5ddbf3 100644 --- a/contracts/mocks/lib/CommonMathMock.sol +++ b/contracts/mocks/lib/CommonMathMock.sol @@ -52,6 +52,17 @@ contract CommonMathMock { return CommonMath.deScale(a); } + function testDivCeil( + uint256 a, + uint256 b + ) + external + pure + returns(uint256) + { + return CommonMath.divCeil(a, b); + } + function testGetPartialAmount( uint256 _principal, uint256 _numerator, diff --git a/package.json b/package.json index c43ee4187..6ca64ec5a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "set-protocol-contracts", - "version": "1.3.15-beta", + "version": "1.3.16-beta", "description": "Smart contracts for {Set} Protocol", "main": "dist/artifacts/index.js", "typings": "dist/typings/artifacts/index.d.ts", diff --git a/test/contracts/core/integration/rebalancingLinearLiquidator.spec.ts b/test/contracts/core/integration/rebalancingLinearLiquidator.spec.ts index 04ada9ec9..2c7d1d386 100644 --- a/test/contracts/core/integration/rebalancingLinearLiquidator.spec.ts +++ b/test/contracts/core/integration/rebalancingLinearLiquidator.spec.ts @@ -173,8 +173,8 @@ contract('RebalancingSetV2 - LinearAuctionLiquidator', accounts => { set1NaturalUnit, ); - set2Components = [component2.address, component3.address]; - set2Units = [gWei(1), gWei(1)]; + set2Components = [component1.address, component2.address]; + set2Units = [gWei(1), gWei(2)]; set2NaturalUnit = customSet2NaturalUnit || gWei(2); set2 = await coreHelper.createSetTokenAsync( coreMock, @@ -355,6 +355,27 @@ contract('RebalancingSetV2 - LinearAuctionLiquidator', accounts => { }); }); + describe('when the union of currentSet and nextSet is not 2 components', async () => { + beforeEach(async () => { + const set3Components = [component1.address, component3.address]; + const set3Units = [gWei(1), gWei(1)]; + const set3NaturalUnit = customSet1NaturalUnit || gWei(1); + const set3 = await coreHelper.createSetTokenAsync( + coreMock, + setTokenFactory.address, + set3Components, + set3Units, + set3NaturalUnit, + ); + + subjectNextSet = set3.address; + }); + + it('should revert', async () => { + await expectRevertError(subject()); + }); + }); + describe('when the rebalance interval has not elapsed', async () => { beforeEach(async () => { subjectTimeFastForward = ONE_DAY_IN_SECONDS.sub(10); diff --git a/test/contracts/core/liquidators/impl/auction.spec.ts b/test/contracts/core/liquidators/impl/auction.spec.ts index f93709760..d05b8fc80 100644 --- a/test/contracts/core/liquidators/impl/auction.spec.ts +++ b/test/contracts/core/liquidators/impl/auction.spec.ts @@ -14,9 +14,7 @@ import { SetTokenFactoryContract, StandardTokenMockContract, AuctionMockContract, - OracleWhiteListContract, TransferProxyContract, - UpdatableOracleMockContract, VaultContract, } from '@utils/contracts'; import { expectRevertError } from '@utils/tokenAssertions'; @@ -29,7 +27,6 @@ import { ether, gWei } from '@utils/units'; import { CoreHelper } from '@utils/helpers/coreHelper'; import { ERC20Helper } from '@utils/helpers/erc20Helper'; -import { LibraryMockHelper } from '@utils/helpers/libraryMockHelper'; import { LiquidatorHelper } from '@utils/helpers/liquidatorHelper'; BigNumberSetup.configure(); @@ -51,25 +48,15 @@ contract('Auction', accounts => { let vault: VaultContract; let setTokenFactory: SetTokenFactoryContract; let auctionMock: AuctionMockContract; - let oracleWhiteList: OracleWhiteListContract; const coreHelper = new CoreHelper(ownerAccount, ownerAccount); const erc20Helper = new ERC20Helper(ownerAccount); const liquidatorHelper = new LiquidatorHelper(ownerAccount, erc20Helper); - const libraryMockHelper = new LibraryMockHelper(ownerAccount); let component1: StandardTokenMockContract; let component2: StandardTokenMockContract; let component3: StandardTokenMockContract; - let component1Price: BigNumber; - let component2Price: BigNumber; - let component3Price: BigNumber; - - let component1Oracle: UpdatableOracleMockContract; - let component2Oracle: UpdatableOracleMockContract; - let component3Oracle: UpdatableOracleMockContract; - let set1: SetTokenContract; let set2: SetTokenContract; @@ -98,19 +85,6 @@ contract('Auction', accounts => { component2 = await erc20Helper.deployTokenAsync(ownerAccount); component3 = await erc20Helper.deployTokenAsync(ownerAccount); - component1Price = ether(1); - component2Price = ether(2); - component3Price = ether(1); - - component1Oracle = await libraryMockHelper.deployUpdatableOracleMockAsync(component1Price); - component2Oracle = await libraryMockHelper.deployUpdatableOracleMockAsync(component2Price); - component3Oracle = await libraryMockHelper.deployUpdatableOracleMockAsync(component3Price); - - oracleWhiteList = await coreHelper.deployOracleWhiteListAsync( - [component1.address, component2.address, component3.address], - [component1Oracle.address, component2Oracle.address, component3Oracle.address], - ); - set1Components = [component1.address, component2.address]; set1Units = [gWei(1), gWei(1)]; set1NaturalUnit = gWei(2); @@ -147,25 +121,6 @@ contract('Auction', accounts => { await blockchain.revertAsync(); }); - describe('#constructor', async () => { - let subjectWhiteList: Address; - - beforeEach(async () => { - subjectWhiteList = oracleWhiteList.address; - }); - - async function subject(): Promise { - return liquidatorHelper.deployAuctionMockAsync(subjectWhiteList); - } - - it('sets the correct oracleWhiteList', async () => { - auctionMock = await subject(); - - const actualOracleWhiteList = await auctionMock.oracleWhiteList.callAsync(); - expect(actualOracleWhiteList).to.bignumber.equal(oracleWhiteList.address); - }); - }); - describe('#initializeAuction', async () => { let subjectCaller: Address; let subjectCurrentSet: Address; @@ -173,7 +128,7 @@ contract('Auction', accounts => { let subjectStartingCurrentSetQuantity: BigNumber; beforeEach(async () => { - auctionMock = await liquidatorHelper.deployAuctionMockAsync(oracleWhiteList.address); + auctionMock = await liquidatorHelper.deployAuctionMockAsync(); subjectCaller = functionCaller; subjectCurrentSet = set1.address; @@ -193,31 +148,6 @@ contract('Auction', accounts => { ); } - it('sets the correct pricePrecision', async () => { - await subject(); - - const auctionSetup: any = await auctionMock.auction.callAsync(); - - const expectedPricePrecision = await liquidatorHelper.calculatePricePrecisionAsync( - set1, - set2, - oracleWhiteList - ); - - expect(auctionSetup.pricePrecision).to.bignumber.equal(expectedPricePrecision); - }); - - it('sets the correct minimumBid', async () => { - await subject(); - - const auctionSetup: any = await auctionMock.auction.callAsync(); - - const pricePrecision = auctionSetup.pricePrecision; - const expectedMinimumBid = BigNumber.max(set1NaturalUnit, set2NaturalUnit) - .mul(pricePrecision); - expect(auctionSetup.minimumBid).to.bignumber.equal(expectedMinimumBid); - }); - it('sets the correct startTime', async () => { await subject(); @@ -261,8 +191,7 @@ contract('Auction', accounts => { const expectedResult = await liquidatorHelper.constructCombinedUnitArrayAsync( set1, combinedTokenArray, - new BigNumber(auctionSetup.minimumBid), - auctionSetup.pricePrecision + new BigNumber(auctionSetup.maxNaturalUnit), ); expect(JSON.stringify(combinedCurrentSetUnits)).to.equal(JSON.stringify(expectedResult)); @@ -278,84 +207,11 @@ contract('Auction', accounts => { const expectedResult = await liquidatorHelper.constructCombinedUnitArrayAsync( set2, combinedTokenArray, - new BigNumber(auctionSetup.minimumBid), - auctionSetup.pricePrecision + new BigNumber(auctionSetup.maxNaturalUnit), ); expect(JSON.stringify(combinedNextSetUnits)).to.equal(JSON.stringify(expectedResult)); }); - - describe('when currentSet is greater than 10x the nextSet', async () => { - beforeEach(async () => { - const setComponents = [component1.address, component2.address]; - const setUnits = [gWei(1), gWei(1)]; - const setNaturalUnit = gWei(300); - const set3 = await coreHelper.createSetTokenAsync( - core, - setTokenFactory.address, - setComponents, - setUnits, - setNaturalUnit, - ); - - subjectNextSet = set3.address; - }); - - it('sets the correct pricePrecision', async () => { - await subject(); - - const auctionSetup: any = await auctionMock.auction.callAsync(); - - const expectedPricePrecision = await liquidatorHelper.calculatePricePrecisionAsync( - await coreHelper.getSetInstance(subjectCurrentSet), - await coreHelper.getSetInstance(subjectNextSet), - oracleWhiteList - ); - console.log(auctionSetup.pricePrecision); - expect(auctionSetup.pricePrecision).to.bignumber.equal(expectedPricePrecision); - }); - }); - - describe('when currentSet is 1-2x greater than nextSet', async () => { - beforeEach(async () => { - const set3Components = [component1.address, component3.address]; - const set3Units = [gWei(1), gWei(1)]; - const set3NaturalUnit = gWei(2); - const set3 = await coreHelper.createSetTokenAsync( - core, - setTokenFactory.address, - set3Components, - set3Units, - set3NaturalUnit, - ); - - subjectNextSet = set3.address; - }); - - it('sets the correct pricePrecision', async () => { - await subject(); - - const auctionSetup: any = await auctionMock.auction.callAsync(); - - const expectedPricePrecision = await liquidatorHelper.calculatePricePrecisionAsync( - await coreHelper.getSetInstance(subjectCurrentSet), - await coreHelper.getSetInstance(subjectNextSet), - oracleWhiteList - ); - console.log(auctionSetup.pricePrecision); - expect(auctionSetup.pricePrecision).to.bignumber.equal(expectedPricePrecision); - }); - }); - - describe('when there is insufficient collateral to rebalance', async () => { - beforeEach(async () => { - subjectStartingCurrentSetQuantity = gWei(10); - }); - - it('should revert', async () => { - await expectRevertError(subject()); - }); - }); }); describe('#reduceRemainingCurrentSets', async () => { @@ -366,7 +222,7 @@ contract('Auction', accounts => { let subjectReductionQuantity: BigNumber; beforeEach(async () => { - auctionMock = await liquidatorHelper.deployAuctionMockAsync(oracleWhiteList.address); + auctionMock = await liquidatorHelper.deployAuctionMockAsync(); subjectCaller = functionCaller; startingCurrentSetQuantity = ether(10); @@ -405,7 +261,7 @@ contract('Auction', accounts => { let subjectQuantity: BigNumber; beforeEach(async () => { - auctionMock = await liquidatorHelper.deployAuctionMockAsync(oracleWhiteList.address); + auctionMock = await liquidatorHelper.deployAuctionMockAsync(); subjectCaller = functionCaller; startingCurrentSetQuantity = ether(10); @@ -433,10 +289,7 @@ contract('Auction', accounts => { describe('when the quantity is not a multiple of the minimumBid', async () => { beforeEach(async () => { - const auctionSetup: any = await auctionMock.auction.callAsync(); - const halfMinimumBid = BigNumber.max(set1NaturalUnit, set2NaturalUnit) - .mul(auctionSetup.pricePrecision) - .div(2); + const halfMinimumBid = BigNumber.max(set1NaturalUnit, set2NaturalUnit).div(2); subjectQuantity = gWei(10).plus(halfMinimumBid); }); @@ -464,7 +317,7 @@ contract('Auction', accounts => { let customReductionQuantity: BigNumber; beforeEach(async () => { - auctionMock = await liquidatorHelper.deployAuctionMockAsync(oracleWhiteList.address); + auctionMock = await liquidatorHelper.deployAuctionMockAsync(); subjectCaller = functionCaller; startingCurrentSetQuantity = ether(10); @@ -513,7 +366,7 @@ contract('Auction', accounts => { let startingCurrentSetQuantity: BigNumber; beforeEach(async () => { - auctionMock = await liquidatorHelper.deployAuctionMockAsync(oracleWhiteList.address); + auctionMock = await liquidatorHelper.deployAuctionMockAsync(); subjectCaller = functionCaller; }); @@ -549,4 +402,4 @@ contract('Auction', accounts => { }); }); }); -}); \ No newline at end of file +}); diff --git a/test/contracts/core/liquidators/impl/linearAuction.spec.ts b/test/contracts/core/liquidators/impl/linearAuction.spec.ts index 92c46a158..b6392c1de 100644 --- a/test/contracts/core/liquidators/impl/linearAuction.spec.ts +++ b/test/contracts/core/liquidators/impl/linearAuction.spec.ts @@ -19,11 +19,12 @@ import { UpdatableOracleMockContract, VaultContract, } from '@utils/contracts'; +import { expectRevertError } from '@utils/tokenAssertions'; import { Blockchain } from '@utils/blockchain'; import { getWeb3 } from '@utils/web3Helper'; import { DEFAULT_GAS, - ONE_DAY_IN_SECONDS + ONE_DAY_IN_SECONDS, } from '@utils/constants'; import { ether, gWei } from '@utils/units'; import { getLinearAuction, TokenFlow } from '@utils/auction'; @@ -210,9 +211,7 @@ contract('LinearAuction', accounts => { const auction: any = await auctionMock.auction.callAsync(); - const pricePrecision = auction.auction.pricePrecision; - const expectedMinimumBid = BigNumber.max(set1NaturalUnit, set2NaturalUnit) - .mul(pricePrecision); + const expectedMinimumBid = BigNumber.max(set1NaturalUnit, set2NaturalUnit); expect(auction.auction.minimumBid).to.bignumber.equal(expectedMinimumBid); }); @@ -227,21 +226,7 @@ contract('LinearAuction', accounts => { expect(auction.endTime).to.bignumber.equal(expectedEndTime); }); - it('sets the correct pricePrecision', async () => { - await subject(); - - const auction: any = await auctionMock.auction.callAsync(); - - const expectedPricePrecision = await liquidatorHelper.calculatePricePrecisionAsync( - await coreHelper.getSetInstance(subjectCurrentSet), - await coreHelper.getSetInstance(subjectNextSet), - oracleWhiteList - ); - - expect(auction.auction.pricePrecision).to.bignumber.equal(expectedPricePrecision); - }); - - it('sets the correct startNumerator', async () => { + it('sets the correct startPrice', async () => { await subject(); const auction: any = await auctionMock.auction.callAsync(); @@ -250,14 +235,15 @@ contract('LinearAuction', accounts => { set1, set2, oracleWhiteList, - auction.auction.pricePrecision, ); const rangeStart = await auctionMock.rangeStart.callAsync(); - const expectedStartPrice = liquidatorHelper.calculateStartPrice(fairValue, rangeStart); - expect(auction.startNumerator).to.bignumber.equal(expectedStartPrice); + + const negativeRange = fairValue.mul(rangeStart).div(100).round(0, 3); + const expectedStartPrice = fairValue.sub(negativeRange); + expect(auction.startPrice).to.bignumber.equal(expectedStartPrice); }); - it('sets the correct endNumerator', async () => { + it('sets the correct endPrice', async () => { await subject(); const auction: any = await auctionMock.auction.callAsync(); @@ -266,11 +252,11 @@ contract('LinearAuction', accounts => { set1, set2, oracleWhiteList, - auction.auction.pricePrecision, ); const rangeEnd = await auctionMock.rangeEnd.callAsync(); - const expectedEndPrice = liquidatorHelper.calculateEndPrice(fairValue, rangeEnd); - expect(auction.endNumerator).to.bignumber.equal(expectedEndPrice); + const positiveRange = fairValue.mul(rangeEnd).div(100).round(0, 3); + const expectedEndPrice = fairValue.add(positiveRange); + expect(auction.endPrice).to.bignumber.equal(expectedEndPrice); }); describe('when currentSet is greater than 10x the nextSet', async () => { @@ -289,21 +275,7 @@ contract('LinearAuction', accounts => { subjectNextSet = set3.address; }); - it('sets the correct pricePrecision', async () => { - await subject(); - - const auction: any = await auctionMock.auction.callAsync(); - - const expectedPricePrecision = await liquidatorHelper.calculatePricePrecisionAsync( - await coreHelper.getSetInstance(subjectCurrentSet), - await coreHelper.getSetInstance(subjectNextSet), - oracleWhiteList - ); - - expect(auction.auction.pricePrecision).to.bignumber.equal(expectedPricePrecision); - }); - - it('sets the correct startNumerator', async () => { + it('sets the correct startPrice', async () => { await subject(); const auction: any = await auctionMock.auction.callAsync(); @@ -312,15 +284,15 @@ contract('LinearAuction', accounts => { await coreHelper.getSetInstance(subjectCurrentSet), await coreHelper.getSetInstance(subjectNextSet), oracleWhiteList, - auction.auction.pricePrecision, ); const rangeStart = await auctionMock.rangeStart.callAsync(); - const expectedStartPrice = liquidatorHelper.calculateStartPrice(fairValue, rangeStart); + const negativeRange = fairValue.mul(rangeStart).div(100).round(0, 3); + const expectedStartPrice = fairValue.sub(negativeRange); - expect(auction.startNumerator).to.bignumber.equal(expectedStartPrice); + expect(auction.startPrice).to.bignumber.equal(expectedStartPrice); }); - it('sets the correct endNumerator', async () => { + it('sets the correct endPrice', async () => { await subject(); const auction: any = await auctionMock.auction.callAsync(); @@ -329,12 +301,22 @@ contract('LinearAuction', accounts => { await coreHelper.getSetInstance(subjectCurrentSet), await coreHelper.getSetInstance(subjectNextSet), oracleWhiteList, - auction.auction.pricePrecision, ); const rangeEnd = await auctionMock.rangeEnd.callAsync(); - const expectedEndPrice = liquidatorHelper.calculateEndPrice(fairValue, rangeEnd); + const positiveRange = fairValue.mul(rangeEnd).div(100).round(0, 3); + const expectedEndPrice = fairValue.add(positiveRange); - expect(auction.endNumerator).to.bignumber.equal(expectedEndPrice); + expect(auction.endPrice).to.bignumber.equal(expectedEndPrice); + }); + }); + + describe('when there is insufficient collateral to rebalance', async () => { + beforeEach(async () => { + subjectStartingCurrentSetQuantity = gWei(0.5); + }); + + it('should revert', async () => { + await expectRevertError(subject()); }); }); }); @@ -356,9 +338,9 @@ contract('LinearAuction', accounts => { ); }); - describe('#getNumerator', async () => { + describe('#getPrice', async () => { async function subject(): Promise { - return auctionMock.getNumerator.callAsync(); + return auctionMock.getPrice.callAsync(); } it('returns the correct result', async () => { @@ -408,11 +390,11 @@ contract('LinearAuction', accounts => { ); }); - it('returns the correct price / endNumerator', async () => { + it('returns the correct price / endPrice', async () => { const result = await subject(); const linearAuction = getLinearAuction(await auctionMock.auction.callAsync()); - expect(result).to.bignumber.equal(linearAuction.endNumerator); + expect(result).to.bignumber.equal(linearAuction.endPrice); }); }); }); @@ -423,7 +405,7 @@ contract('LinearAuction', accounts => { } it('returns the correct numerator', async () => { - const { numerator } = await subject(); + const numerator = await subject(); const { timestamp } = await web3.eth.getBlock('latest'); const linearAuction = getLinearAuction(await auctionMock.auction.callAsync()); const currentPrice = await liquidatorHelper.calculateCurrentPrice( @@ -433,13 +415,6 @@ contract('LinearAuction', accounts => { ); expect(numerator).to.bignumber.equal(currentPrice); }); - - it('returns the correct denominator', async () => { - const { denominator } = await subject(); - - const linearAuction = getLinearAuction(await auctionMock.auction.callAsync()); - expect(denominator).to.bignumber.equal(linearAuction.auction.pricePrecision); - }); }); describe('#getTokenFlow', async () => { @@ -461,10 +436,8 @@ contract('LinearAuction', accounts => { tokenFlows = liquidatorHelper.constructTokenFlow( linearAuction, - linearAuction.auction.pricePrecision, subjectQuantity, currentPrice, - linearAuction.auction.pricePrecision, ); }); @@ -509,10 +482,8 @@ contract('LinearAuction', accounts => { tokenFlows = liquidatorHelper.constructTokenFlow( linearAuction, - linearAuction.auction.pricePrecision, subjectQuantity, currentPrice, - linearAuction.auction.pricePrecision, ); }); @@ -587,4 +558,4 @@ contract('LinearAuction', accounts => { }); }); }); -}); \ No newline at end of file +}); diff --git a/test/contracts/core/liquidators/impl/twoAssetPriceBoundedLinearAuction.spec.ts b/test/contracts/core/liquidators/impl/twoAssetPriceBoundedLinearAuction.spec.ts new file mode 100644 index 000000000..237a5215e --- /dev/null +++ b/test/contracts/core/liquidators/impl/twoAssetPriceBoundedLinearAuction.spec.ts @@ -0,0 +1,589 @@ +require('module-alias/register'); + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as ABIDecoder from 'abi-decoder'; +import { BigNumber } from 'bignumber.js'; +import { Address } from 'set-protocol-utils'; + +import ChaiSetup from '@utils/chaiSetup'; +import { BigNumberSetup } from '@utils/bigNumberSetup'; +import { + CoreMockContract, + OracleWhiteListContract, + SetTokenContract, + SetTokenFactoryContract, + StandardTokenMockContract, + TwoAssetPriceBoundedLinearAuctionMockContract, + TransferProxyContract, + UpdatableOracleMockContract, + VaultContract, +} from '@utils/contracts'; +import { ether, gWei } from '@utils/units'; +import { LinearAuction } from '@utils/auction'; + +import { CoreHelper } from '@utils/helpers/coreHelper'; +import { ERC20Helper } from '@utils/helpers/erc20Helper'; +import { LibraryMockHelper } from '@utils/helpers/libraryMockHelper'; +import { LiquidatorHelper } from '@utils/helpers/liquidatorHelper'; +import { expectRevertError } from '@utils/tokenAssertions'; + +BigNumberSetup.configure(); +ChaiSetup.configure(); +const { expect } = chai; +const Core = artifacts.require('Core'); +const TwoAssetPriceBoundedLinearAuction = artifacts.require('TwoAssetPriceBoundedLinearAuction'); + +contract('TwoAssetPriceBoundedLinearAuction', accounts => { + const [ + deployerAccount, + ] = accounts; + + let coreMock: CoreMockContract; + let transferProxy: TransferProxyContract; + let vault: VaultContract; + let setTokenFactory: SetTokenFactoryContract; + + let boundsCalculator: TwoAssetPriceBoundedLinearAuctionMockContract; + let oracleWhiteList: OracleWhiteListContract; + + const coreHelper = new CoreHelper(deployerAccount, deployerAccount); + const erc20Helper = new ERC20Helper(deployerAccount); + const liquidatorHelper = new LiquidatorHelper(deployerAccount, erc20Helper); + const libraryMockHelper = new LibraryMockHelper(deployerAccount); + + let wrappedETH: StandardTokenMockContract; + let wrappedBTC: StandardTokenMockContract; + let usdc: StandardTokenMockContract; + let dai: StandardTokenMockContract; + + let wrappedETHPrice: BigNumber; + let wrappedBTCPrice: BigNumber; + let usdcPrice: BigNumber; + let daiPrice: BigNumber; + + let wrappedETHOracle: UpdatableOracleMockContract; + let wrappedBTCOracle: UpdatableOracleMockContract; + let usdcOracle: UpdatableOracleMockContract; + let daiOracle: UpdatableOracleMockContract; + + let auctionPeriod: BigNumber; + let rangeStart: BigNumber; + let rangeEnd: BigNumber; + + before(async () => { + ABIDecoder.addABI(Core.abi); + ABIDecoder.addABI(TwoAssetPriceBoundedLinearAuction.abi); + + transferProxy = await coreHelper.deployTransferProxyAsync(); + vault = await coreHelper.deployVaultAsync(); + coreMock = await coreHelper.deployCoreMockAsync(transferProxy, vault); + + setTokenFactory = await coreHelper.deploySetTokenFactoryAsync(coreMock.address); + await coreHelper.setDefaultStateAndAuthorizationsAsync(coreMock, vault, transferProxy, setTokenFactory); + + wrappedETH = await erc20Helper.deployTokenAsync(deployerAccount, 18); + wrappedBTC = await erc20Helper.deployTokenAsync(deployerAccount, 8); + usdc = await erc20Helper.deployTokenAsync(deployerAccount, 6); + dai = await erc20Helper.deployTokenAsync(deployerAccount, 18); + + wrappedETHPrice = ether(128); + wrappedBTCPrice = ether(7500); + usdcPrice = ether(1); + daiPrice = ether(1); + + wrappedETHOracle = await libraryMockHelper.deployUpdatableOracleMockAsync(wrappedETHPrice); + wrappedBTCOracle = await libraryMockHelper.deployUpdatableOracleMockAsync(wrappedBTCPrice); + usdcOracle = await libraryMockHelper.deployUpdatableOracleMockAsync(usdcPrice); + daiOracle = await libraryMockHelper.deployUpdatableOracleMockAsync(daiPrice); + + oracleWhiteList = await coreHelper.deployOracleWhiteListAsync( + [wrappedETH.address, wrappedBTC.address, usdc.address, dai.address], + [wrappedETHOracle.address, wrappedBTCOracle.address, usdcOracle.address, daiOracle.address], + ); + + auctionPeriod = new BigNumber(14400); // 4 hours + rangeStart = new BigNumber(3); // 3% + rangeEnd = new BigNumber(21); // 21% + + boundsCalculator = await liquidatorHelper.deployTwoAssetPriceBoundedLinearAuctionMock( + oracleWhiteList.address, + auctionPeriod, + rangeStart, + rangeEnd, + ); + }); + + after(async () => { + ABIDecoder.removeABI(Core.abi); + ABIDecoder.removeABI(TwoAssetPriceBoundedLinearAuction.abi); + }); + + describe('#constructor', async () => { + it('sets the correct auctionPeriod', async () => { + const result = await boundsCalculator.auctionPeriod.callAsync(); + expect(result).to.bignumber.equal(auctionPeriod); + }); + + it('sets the correct rangeStart', async () => { + const result = await boundsCalculator.rangeStart.callAsync(); + expect(result).to.bignumber.equal(rangeStart); + }); + + it('sets the correct rangeEnd', async () => { + const result = await boundsCalculator.rangeEnd.callAsync(); + expect(result).to.bignumber.equal(rangeEnd); + }); + + it('sets the correct oracleWhiteList', async () => { + const result = await boundsCalculator.oracleWhiteList.callAsync(); + expect(result).to.equal(oracleWhiteList.address); + }); + }); + + describe('#validateTwoAssetPriceBoundedAuction', async () => { + let set1: SetTokenContract; + let set2: SetTokenContract; + + let set1Components: Address[]; + let set2Components: Address[]; + + let set1Units: BigNumber[]; + let set2Units: BigNumber[]; + + let set1NaturalUnit: BigNumber; + let set2NaturalUnit: BigNumber; + + let customComponents1: Address[]; + let customComponents2: Address[]; + + let customUnits1: BigNumber[]; + let customUnits2: BigNumber[]; + + let subjectCurrentSet: Address; + let subjectNextSet: Address; + + beforeEach(async () => { + set1Components = customComponents1 || [wrappedETH.address, wrappedBTC.address]; + set1Units = customUnits1 || [gWei(1), gWei(1)]; + set1NaturalUnit = new BigNumber(10 ** 12); + set1 = await coreHelper.createSetTokenAsync( + coreMock, + setTokenFactory.address, + set1Components, + set1Units, + set1NaturalUnit, + ); + + set2Components = customComponents2 || [wrappedETH.address, wrappedBTC.address]; + set2Units = customUnits2 || [gWei(1), gWei(2)]; + set2NaturalUnit = new BigNumber(10 ** 12); + set2 = await coreHelper.createSetTokenAsync( + coreMock, + setTokenFactory.address, + set2Components, + set2Units, + set2NaturalUnit, + ); + + subjectCurrentSet = set1.address; + subjectNextSet = set2.address; + }); + + async function subject(): Promise { + return boundsCalculator.validateTwoAssetPriceBoundedAuctionMock.callAsync( + subjectCurrentSet, + subjectNextSet + ); + } + + it('does not revert', async () => { + await subject(); + }); + + describe('when the union is 3 components', async () => { + before(async () => { + customComponents2 = [wrappedETH.address, usdc.address]; + }); + + after(async () => { + customComponents2 = undefined; + }); + + it('should revert', async () => { + await expectRevertError(subject()); + }); + }); + + describe('when the union is 1 components', async () => { + before(async () => { + customComponents1 = [wrappedETH.address]; + customComponents2 = [wrappedETH.address]; + + customUnits1 = [gWei(1)]; + customUnits2 = [gWei(2)]; + }); + + after(async () => { + customComponents1 = undefined; + customComponents2 = undefined; + + customUnits1 = undefined; + customUnits2 = undefined; + }); + + it('should revert', async () => { + await expectRevertError(subject()); + }); + }); + + describe('when a passed token does not have matching oracle', async () => { + before(async () => { + const nonOracleComponent = await erc20Helper.deployTokenAsync(deployerAccount, 6); + + customComponents2 = [wrappedETH.address, nonOracleComponent.address]; + }); + + after(async () => { + customComponents2 = undefined; + }); + + it('should revert', async () => { + await expectRevertError(subject()); + }); + }); + }); + + describe('#calculateMinimumBid', async () => { + let set1: SetTokenContract; + let set2: SetTokenContract; + + let set1Components: Address[]; + let set2Components: Address[]; + + let set1Units: BigNumber[]; + let set2Units: BigNumber[]; + + let set1NaturalUnit: BigNumber; + let set2NaturalUnit: BigNumber; + let combinedTokenArray: Address[]; + let combinedCurrentSetUnits: BigNumber[]; + let combinedNextSetUnits: BigNumber[]; + + let linearAuction: LinearAuction; + + let subjectCurrentSet: Address; + let subjectNextSet: Address; + + before(async () => { + set1Components = [wrappedBTC.address, usdc.address]; + set1Units = [new BigNumber(100), new BigNumber(7500)]; + set1NaturalUnit = new BigNumber(10 ** 12); + + set2Components = [wrappedBTC.address, usdc.address]; + set2Units = [new BigNumber(100), new BigNumber(7806)]; + set2NaturalUnit = new BigNumber(10 ** 12); + }); + + beforeEach(async () => { + set1 = await coreHelper.createSetTokenAsync( + coreMock, + setTokenFactory.address, + set1Components, + set1Units, + set1NaturalUnit, + ); + + set2 = await coreHelper.createSetTokenAsync( + coreMock, + setTokenFactory.address, + set2Components, + set2Units, + set2NaturalUnit, + ); + + combinedTokenArray = set1Components; + combinedCurrentSetUnits = set1Units; + combinedNextSetUnits = set2Units; + + await boundsCalculator.parameterizeAuction.sendTransactionAsync( + combinedTokenArray, + combinedCurrentSetUnits, + combinedNextSetUnits + ); + + linearAuction = { + auction: { + maxNaturalUnit: BigNumber.max(set1NaturalUnit, set2NaturalUnit), + minimumBid: new BigNumber(0), + startTime: new BigNumber(0), + startingCurrentSets: new BigNumber(0), + remainingCurrentSets: new BigNumber(0), + combinedTokenArray, + combinedCurrentSetUnits, + combinedNextSetUnits, + }, + endTime: new BigNumber(0), + startPrice: new BigNumber(0), + endPrice: new BigNumber(0), + }; + + subjectCurrentSet = set1.address; + subjectNextSet = set2.address; + }); + + async function subject(): Promise { + return boundsCalculator.calculateMinimumBid.callAsync( + subjectCurrentSet, + subjectNextSet + ); + } + + it('calculates the correct minimumBid', async () => { + const result = await subject(); + + const expectedResult = await liquidatorHelper.calculateMinimumBidAsync( + linearAuction, + set1, + set2, + wrappedBTCPrice.div(usdcPrice), + ); + + expect(result).to.bignumber.equal(expectedResult); + }); + + describe('when using assets that do not require a bump in minimumBid', async () => { + before(async () => { + set1Components = [wrappedETH.address, dai.address]; + set1Units = [new BigNumber(10 ** 6), new BigNumber(128 * 10 ** 6)]; + set1NaturalUnit = new BigNumber(10 ** 6); + + set2Components = [wrappedETH.address, dai.address]; + set2Units = [new BigNumber(10 ** 6), new BigNumber(133224489)]; + set2NaturalUnit = new BigNumber(10 ** 6); + }); + + it('calculates the correct minimumBid', async () => { + const result = await subject(); + + const expectedResult = await liquidatorHelper.calculateMinimumBidAsync( + linearAuction, + set1, + set2, + wrappedETHPrice.div(usdcPrice) + ); + + expect(result).to.bignumber.equal(expectedResult); + }); + }); + }); + + describe('#calculateStartPrice and calculateEndPrice', async () => { + let combinedTokenArray: Address[]; + let combinedCurrentSetUnits: BigNumber[]; + let combinedNextSetUnits: BigNumber[]; + + let linearAuction: LinearAuction; + + beforeEach(async () => { + combinedTokenArray = [wrappedBTC.address, usdc.address]; + combinedCurrentSetUnits = [new BigNumber(100), new BigNumber(7500)]; + combinedNextSetUnits = [new BigNumber(100), new BigNumber(7806)]; + + await boundsCalculator.parameterizeAuction.sendTransactionAsync( + combinedTokenArray, + combinedCurrentSetUnits, + combinedNextSetUnits + ); + + linearAuction = { + auction: { + maxNaturalUnit: new BigNumber(10 ** 12), + minimumBid: new BigNumber(0), + startTime: new BigNumber(0), + startingCurrentSets: new BigNumber(0), + remainingCurrentSets: new BigNumber(0), + combinedTokenArray, + combinedCurrentSetUnits, + combinedNextSetUnits, + }, + endTime: new BigNumber(0), + startPrice: new BigNumber(0), + endPrice: new BigNumber(0), + }; + }); + + async function startPriceSubject(): Promise { + return boundsCalculator.calculateStartPriceMock.callAsync(); + } + + async function endPriceSubject(): Promise { + return boundsCalculator.calculateEndPriceMock.callAsync(); + } + + it('calculates the correct start price value', async () => { + const result = await startPriceSubject(); + + const [expectedResult, ] = await liquidatorHelper.calculateAuctionBoundsAsync( + linearAuction, + rangeStart, + rangeEnd, + oracleWhiteList, + ); + + expect(result).to.bignumber.equal(expectedResult); + }); + + it('calculates the correct end price value', async () => { + const result = await endPriceSubject(); + + const [, expectedResult] = await liquidatorHelper.calculateAuctionBoundsAsync( + linearAuction, + rangeStart, + rangeEnd, + oracleWhiteList, + ); + + expect(result).to.bignumber.equal(expectedResult); + }); + }); + + describe('#calculateAuctionBounds', async () => { + let linearAuction: LinearAuction; + + let combinedTokenArray: Address[]; + let combinedCurrentSetUnits: BigNumber[]; + let combinedNextSetUnits: BigNumber[]; + + before(async () => { + combinedTokenArray = [wrappedETH.address, usdc.address]; + combinedCurrentSetUnits = [new BigNumber(10 ** 12), new BigNumber(128)]; + combinedNextSetUnits = [new BigNumber(10 ** 12), new BigNumber(1152)]; + }); + + beforeEach(async () => { + await boundsCalculator.parameterizeAuction.sendTransactionAsync( + combinedTokenArray, + combinedCurrentSetUnits, + combinedNextSetUnits + ); + + linearAuction = { + auction: { + maxNaturalUnit: new BigNumber(10 ** 12), + minimumBid: new BigNumber(0), + startTime: new BigNumber(0), + startingCurrentSets: new BigNumber(0), + remainingCurrentSets: new BigNumber(0), + combinedTokenArray, + combinedCurrentSetUnits, + combinedNextSetUnits, + }, + endTime: new BigNumber(0), + startPrice: new BigNumber(0), + endPrice: new BigNumber(0), + }; + }); + + async function subject(): Promise { + return boundsCalculator.calculateStartPriceMock.callAsync(); + } + + it('gets the correct start bound', async () => { + const actualStartBound = await subject(); + + const [expectedStartBound, ] = await liquidatorHelper.calculateAuctionBoundsAsync( + linearAuction, + rangeStart, + rangeEnd, + oracleWhiteList + ); + + expect(actualStartBound).to.bignumber.equal(expectedStartBound); + }); + + describe('when asset order is flipped', async () => { + before(async () => { + combinedTokenArray = [usdc.address, wrappedETH.address]; + combinedCurrentSetUnits = [new BigNumber(128), new BigNumber(10 ** 12)]; + combinedNextSetUnits = [new BigNumber(1152), new BigNumber(10 ** 12)]; + }); + + it('gets the correct start bound', async () => { + const actualStartBound = await subject(); + + const [expectedStartBound, ] = await liquidatorHelper.calculateAuctionBoundsAsync( + linearAuction, + rangeStart, + rangeEnd, + oracleWhiteList + ); + + expect(actualStartBound).to.bignumber.equal(expectedStartBound); + }); + }); + + describe('when other asset is higher allocation', async () => { + before(async () => { + combinedTokenArray = [wrappedETH.address, usdc.address]; + combinedCurrentSetUnits = [new BigNumber(10 ** 12), new BigNumber(128)]; + combinedNextSetUnits = [new BigNumber(9 * 10 ** 12), new BigNumber(128)]; + }); + + it('gets the correct start bound', async () => { + const actualStartBound = await subject(); + + const [expectedStartBound, ] = await liquidatorHelper.calculateAuctionBoundsAsync( + linearAuction, + rangeStart, + rangeEnd, + oracleWhiteList + ); + + expect(actualStartBound).to.bignumber.equal(expectedStartBound); + }); + }); + + describe('when other asset is highest allocation and assets are flipped', async () => { + before(async () => { + combinedTokenArray = [usdc.address, wrappedETH.address]; + combinedCurrentSetUnits = [new BigNumber(128), new BigNumber(10 ** 12)]; + combinedNextSetUnits = [new BigNumber(128), new BigNumber(9 * 10 ** 12)]; + }); + + it('gets the correct start bound', async () => { + const actualStartBound = await subject(); + + const [expectedStartBound, ] = await liquidatorHelper.calculateAuctionBoundsAsync( + linearAuction, + rangeStart, + rangeEnd, + oracleWhiteList + ); + + expect(actualStartBound).to.bignumber.equal(expectedStartBound); + }); + }); + + describe('different allocation', async () => { + before(async () => { + combinedTokenArray = [wrappedETH.address, usdc.address]; + combinedCurrentSetUnits = [new BigNumber(10 ** 14), new BigNumber(12800)]; + combinedNextSetUnits = [new BigNumber(10 ** 14), new BigNumber(1267200)]; + }); + + it('gets the correct start bound', async () => { + const actualStartBound = await subject(); + + const [expectedStartBound, ] = await liquidatorHelper.calculateAuctionBoundsAsync( + linearAuction, + rangeStart, + rangeEnd, + oracleWhiteList + ); + + expect(actualStartBound).to.bignumber.equal(expectedStartBound); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/contracts/core/liquidators/linearAuctionLiquidator.spec.ts b/test/contracts/core/liquidators/linearAuctionLiquidator.spec.ts index 948a66e71..42045c75e 100644 --- a/test/contracts/core/liquidators/linearAuctionLiquidator.spec.ts +++ b/test/contracts/core/liquidators/linearAuctionLiquidator.spec.ts @@ -30,7 +30,7 @@ import { ONE_DAY_IN_SECONDS, } from '@utils/constants'; import { ether, gWei } from '@utils/units'; -import { getLinearAuction, TokenFlow } from '@utils/auction'; +import { getLinearAuction, LinearAuction, TokenFlow } from '@utils/auction'; import { CoreHelper } from '@utils/helpers/coreHelper'; import { ERC20Helper } from '@utils/helpers/erc20Helper'; @@ -120,8 +120,8 @@ contract('LinearAuctionLiquidator', accounts => { set1NaturalUnit, ); - set2Components = [component2.address, component3.address]; - set2Units = [gWei(1), gWei(1)]; + set2Components = [component1.address, component2.address]; + set2Units = [gWei(1), gWei(0.5)]; set2NaturalUnit = gWei(2); set2 = await coreHelper.createSetTokenAsync( core, @@ -217,6 +217,8 @@ contract('LinearAuctionLiquidator', accounts => { }); describe('#startRebalance', async () => { + let linearAuction: LinearAuction; + let subjectCaller: Address; let subjectCurrentSet: Address; let subjectNextSet: Address; @@ -224,6 +226,39 @@ contract('LinearAuctionLiquidator', accounts => { let subjectLiquidatorData: string; beforeEach(async () => { + const maxNaturalUnit = BigNumber.max( + await set1.naturalUnit.callAsync(), + await set2.naturalUnit.callAsync() + ); + + const combinedTokenArray = _.union(set1Components, set2Components); + const combinedCurrentSetUnits = await liquidatorHelper.constructCombinedUnitArrayAsync( + set1, + combinedTokenArray, + maxNaturalUnit, + ); + const combinedNextSetUnits = await liquidatorHelper.constructCombinedUnitArrayAsync( + set2, + combinedTokenArray, + maxNaturalUnit, + ); + + linearAuction = { + auction: { + maxNaturalUnit, + minimumBid: new BigNumber(0), + startTime: new BigNumber(0), + startingCurrentSets: new BigNumber(0), + remainingCurrentSets: new BigNumber(0), + combinedTokenArray, + combinedCurrentSetUnits, + combinedNextSetUnits, + }, + endTime: new BigNumber(0), + startPrice: new BigNumber(0), + endPrice: new BigNumber(0), + }; + subjectCaller = functionCaller; subjectCurrentSet = set1.address; subjectNextSet = set2.address; @@ -249,9 +284,13 @@ contract('LinearAuctionLiquidator', accounts => { const auction: any = await liquidator.auctions.callAsync(subjectCaller); - const pricePrecision = auction.auction.pricePrecision; - const expectedMinimumBid = BigNumber.max(set1NaturalUnit, set2NaturalUnit) - .mul(pricePrecision); + const expectedMinimumBid = await liquidatorHelper.calculateMinimumBidAsync( + linearAuction, + set1, + set2, + component1Price.div(component2Price) + ); + expect(auction.auction.minimumBid).to.bignumber.equal(expectedMinimumBid); }); @@ -266,56 +305,40 @@ contract('LinearAuctionLiquidator', accounts => { expect(auction.endTime).to.bignumber.equal(expectedEndTime); }); - it('sets the correct pricePrecision', async () => { - await subject(); - - const auction: any = await liquidator.auctions.callAsync(subjectCaller); - - const expectedPricePrecision = await liquidatorHelper.calculatePricePrecisionAsync( - set1, - set2, - oracleWhiteList, - ); - - expect(auction.auction.pricePrecision).to.bignumber.equal(expectedPricePrecision); - }); - - it('sets the correct startNumerator', async () => { + it('sets the correct startPrice', async () => { await subject(); const auction: any = await liquidator.auctions.callAsync(subjectCaller); - const fairValue = await liquidatorHelper.calculateFairValueAsync( - set1, - set2, + const rangeStart = await liquidator.rangeStart.callAsync(); + const [expectedStartPrice, ] = await liquidatorHelper.calculateAuctionBoundsAsync( + getLinearAuction(auction), + rangeStart, + rangeEnd, oracleWhiteList, - auction.auction.pricePrecision, ); - const rangeStart = await liquidator.rangeStart.callAsync(); - const expectedStartPrice = liquidatorHelper.calculateStartPrice(fairValue, rangeStart); - expect(auction.startNumerator).to.bignumber.equal(expectedStartPrice); + expect(auction.startPrice).to.bignumber.equal(expectedStartPrice); }); - it('sets the correct endNumerator', async () => { + it('sets the correct endPrice', async () => { await subject(); const auction: any = await liquidator.auctions.callAsync(subjectCaller); - const fairValue = await liquidatorHelper.calculateFairValueAsync( - set1, - set2, + const rangeEnd = await liquidator.rangeEnd.callAsync(); + const [, expectedEndPrice] = await liquidatorHelper.calculateAuctionBoundsAsync( + getLinearAuction(auction), + rangeStart, + rangeEnd, oracleWhiteList, - auction.auction.pricePrecision, ); - const rangeEnd = await liquidator.rangeEnd.callAsync(); - const expectedEndPrice = liquidatorHelper.calculateEndPrice(fairValue, rangeEnd); - expect(auction.endNumerator).to.bignumber.equal(expectedEndPrice); + expect(auction.endPrice).to.bignumber.equal(expectedEndPrice); }); describe('when the currentSet is > 10x nextSet', async () => { beforeEach(async () => { const setComponents = [component1.address, component2.address]; - const setUnits = [gWei(1), gWei(1)]; + const setUnits = [gWei(1), gWei(2)]; const setNaturalUnit = gWei(100); const set3 = await coreHelper.createSetTokenAsync( core, @@ -328,50 +351,34 @@ contract('LinearAuctionLiquidator', accounts => { subjectNextSet = set3.address; }); - it('sets the correct pricePrecision', async () => { + it('sets the correct startPrice', async () => { await subject(); const auction: any = await liquidator.auctions.callAsync(subjectCaller); - const expectedPricePrecision = await liquidatorHelper.calculatePricePrecisionAsync( - await coreHelper.getSetInstance(subjectCurrentSet), - await coreHelper.getSetInstance(subjectNextSet), - oracleWhiteList - ); - - expect(auction.auction.pricePrecision).to.bignumber.equal(expectedPricePrecision); - }); - - it('sets the correct startNumerator', async () => { - await subject(); - - const auction: any = await liquidator.auctions.callAsync(subjectCaller); - - const fairValue = await liquidatorHelper.calculateFairValueAsync( - await coreHelper.getSetInstance(subjectCurrentSet), - await coreHelper.getSetInstance(subjectNextSet), + const rangeStart = await liquidator.rangeStart.callAsync(); + const [expectedStartPrice, ] = await liquidatorHelper.calculateAuctionBoundsAsync( + getLinearAuction(auction), + rangeStart, + rangeEnd, oracleWhiteList, - auction.auction.pricePrecision, ); - const rangeStart = await liquidator.rangeStart.callAsync(); - const expectedStartPrice = liquidatorHelper.calculateStartPrice(fairValue, rangeStart); - expect(auction.startNumerator).to.bignumber.equal(expectedStartPrice); + expect(auction.startPrice).to.bignumber.equal(expectedStartPrice); }); - it('sets the correct endNumerator', async () => { + it('sets the correct endPrice', async () => { await subject(); const auction: any = await liquidator.auctions.callAsync(subjectCaller); - const fairValue = await liquidatorHelper.calculateFairValueAsync( - await coreHelper.getSetInstance(subjectCurrentSet), - await coreHelper.getSetInstance(subjectNextSet), + const rangeEnd = await liquidator.rangeEnd.callAsync(); + const [, expectedEndPrice] = await liquidatorHelper.calculateAuctionBoundsAsync( + getLinearAuction(auction), + rangeStart, + rangeEnd, oracleWhiteList, - auction.auction.pricePrecision, ); - const rangeEnd = await liquidator.rangeEnd.callAsync(); - const expectedEndPrice = liquidatorHelper.calculateEndPrice(fairValue, rangeEnd); - expect(auction.endNumerator).to.bignumber.equal(expectedEndPrice); + expect(auction.endPrice).to.bignumber.equal(expectedEndPrice); }); }); @@ -388,7 +395,7 @@ contract('LinearAuctionLiquidator', accounts => { describe('when a token does not have a supported oracle', async () => { beforeEach(async () => { await oracleWhiteList.removeTokenOraclePair.sendTransactionAsync( - component3.address, + component1.address, { from: ownerAccount, gas: DEFAULT_GAS }, ); }); @@ -397,6 +404,27 @@ contract('LinearAuctionLiquidator', accounts => { await expectRevertError(subject()); }); }); + + describe('when the union of the current and next Set is not 2 components', async () => { + beforeEach(async () => { + const set3Components = [component1.address, component3.address]; + const set3Units = [gWei(1), gWei(2)]; + const set3NaturalUnit = gWei(2); + const set3 = await coreHelper.createSetTokenAsync( + core, + setTokenFactory.address, + set3Components, + set3Units, + set3NaturalUnit, + ); + + subjectNextSet = set3.address; + }); + + it('should revert', async () => { + await expectRevertError(subject()); + }); + }); }); describe('[CONTEXT] Initialized auction', async () => { @@ -426,6 +454,20 @@ contract('LinearAuctionLiquidator', accounts => { }); async function subject(): Promise { + return liquidatorProxy.placeBid.sendTransactionAsync( + subjectQuantity, + { from: subjectCaller, gas: DEFAULT_GAS }, + ); + } + + async function directCallSubject(): Promise { + return liquidator.placeBid.sendTransactionAsync( + subjectQuantity, + { from: subjectCaller, gas: DEFAULT_GAS }, + ); + } + + async function setTokenFlows(): Promise { const linearAuction = getLinearAuction(await liquidator.auctions.callAsync(liquidatorProxy.address)); const { timestamp } = await web3.eth.getBlock('latest'); @@ -437,22 +479,8 @@ contract('LinearAuctionLiquidator', accounts => { tokenFlows = liquidatorHelper.constructTokenFlow( linearAuction, - linearAuction.auction.pricePrecision, subjectQuantity, currentPrice, - linearAuction.auction.pricePrecision, - ); - - return liquidatorProxy.placeBid.sendTransactionAsync( - subjectQuantity, - { from: subjectCaller, gas: DEFAULT_GAS }, - ); - } - - async function directCallSubject(): Promise { - return liquidator.placeBid.sendTransactionAsync( - subjectQuantity, - { from: subjectCaller, gas: DEFAULT_GAS }, ); } @@ -466,6 +494,7 @@ contract('LinearAuctionLiquidator', accounts => { it('returns the correct combinedTokenArray', async () => { await subject(); + await setTokenFlows(); const combinedTokenArray = await liquidatorProxy.getCombinedTokenArray.callAsync(); expect(JSON.stringify(combinedTokenArray)).to.equal(JSON.stringify(tokenFlows.addresses)); @@ -473,6 +502,7 @@ contract('LinearAuctionLiquidator', accounts => { it('returns the correct inflow', async () => { await subject(); + await setTokenFlows(); const inflow = await liquidatorProxy.getInflow.callAsync(); expect(JSON.stringify(inflow)).to.equal(JSON.stringify(tokenFlows.inflow)); @@ -480,6 +510,7 @@ contract('LinearAuctionLiquidator', accounts => { it('returns the correct outflow', async () => { await subject(); + await setTokenFlows(); const outflow = await liquidatorProxy.getOutflow.callAsync(); expect(JSON.stringify(outflow)).to.equal(JSON.stringify(tokenFlows.outflow)); @@ -487,12 +518,7 @@ contract('LinearAuctionLiquidator', accounts => { describe('when the quantity is not a multiple of the minimumBid', async () => { beforeEach(async () => { - const auction: any = await liquidator.auctions.callAsync(liquidatorProxy.address); - - const pricePrecision = auction.auction.pricePrecision; - const halfMinimumBid = BigNumber.max(set1NaturalUnit, set2NaturalUnit) - .mul(pricePrecision) - .div(2); + const halfMinimumBid = BigNumber.max(set1NaturalUnit, set2NaturalUnit).div(2); subjectQuantity = gWei(10).plus(halfMinimumBid); }); @@ -551,10 +577,8 @@ contract('LinearAuctionLiquidator', accounts => { tokenFlows = liquidatorHelper.constructTokenFlow( linearAuction, - linearAuction.auction.pricePrecision, subjectQuantity, currentPrice, - linearAuction.auction.pricePrecision, ); }); @@ -616,8 +640,8 @@ contract('LinearAuctionLiquidator', accounts => { expect(JSON.stringify(auction.auction.combinedCurrentSetUnits)).to.equal(JSON.stringify([])); expect(JSON.stringify(auction.auction.combinedNextSetUnits)).to.equal(JSON.stringify([])); expect(auction.endTime).to.bignumber.equal(ZERO); - expect(auction.startNumerator).to.bignumber.equal(ZERO); - expect(auction.endNumerator).to.bignumber.equal(ZERO); + expect(auction.startPrice).to.bignumber.equal(ZERO); + expect(auction.endPrice).to.bignumber.equal(ZERO); }); describe('when there is a biddable quantity', async () => { @@ -666,8 +690,8 @@ contract('LinearAuctionLiquidator', accounts => { expect(JSON.stringify(auction.auction.combinedCurrentSetUnits)).to.equal(JSON.stringify([])); expect(JSON.stringify(auction.auction.combinedNextSetUnits)).to.equal(JSON.stringify([])); expect(auction.endTime).to.bignumber.equal(ZERO); - expect(auction.startNumerator).to.bignumber.equal(ZERO); - expect(auction.endNumerator).to.bignumber.equal(ZERO); + expect(auction.startPrice).to.bignumber.equal(ZERO); + expect(auction.endPrice).to.bignumber.equal(ZERO); }); describe('when the caller is not a valid Set', async () => { @@ -777,8 +801,8 @@ contract('LinearAuctionLiquidator', accounts => { const linearAuction = getLinearAuction(await liquidator.auctions.callAsync(subjectSet)); expect(auctionStartTime).to.bignumber.equal(linearAuction.auction.startTime); expect(auctionTimeToPivot).to.bignumber.equal(auctionPeriod); - expect(auctionStartPrice).to.bignumber.equal(linearAuction.startNumerator); - expect(auctionPivotPrice).to.bignumber.equal(linearAuction.endNumerator); + expect(auctionStartPrice).to.bignumber.equal(linearAuction.startPrice); + expect(auctionPivotPrice).to.bignumber.equal(linearAuction.endPrice); }); }); }); diff --git a/test/contracts/lib/commonMath.spec.ts b/test/contracts/lib/commonMath.spec.ts index 7464766ca..14988dbf3 100644 --- a/test/contracts/lib/commonMath.spec.ts +++ b/test/contracts/lib/commonMath.spec.ts @@ -170,6 +170,46 @@ contract('CommonMathMock', accounts => { }); }); + describe('#testDivCeil', async () => { + let subjectA: BigNumber; + let subjectB: BigNumber; + const caller: Address = ownerAccount; + + beforeEach(async () => { + subjectA = new BigNumber(26); + subjectB = new BigNumber(11); + }); + + async function subject(): Promise { + return commonMathLibrary.testDivCeil.callAsync( + subjectA, + subjectB, + { from: caller }, + ); + } + + it('returns the correct value', async () => { + const result = await subject(); + + const expectedResult = new BigNumber(3); + expect(result).to.be.bignumber.equal(expectedResult); + }); + + describe('when there is no rounding', async () => { + beforeEach(async () => { + subjectA = new BigNumber(6); + subjectB = new BigNumber(2); + }); + + it('returns the correct value', async () => { + const result = await subject(); + + const expectedResult = new BigNumber(subjectA).div(subjectB); + expect(result).to.be.bignumber.equal(expectedResult); + }); + }); + }); + describe('getPartialAmount', async () => { let subjectPrincipal: BigNumber; let subjectNumerator: BigNumber; diff --git a/utils/auction.ts b/utils/auction.ts index c1ac29d0a..3da4a6c12 100644 --- a/utils/auction.ts +++ b/utils/auction.ts @@ -7,8 +7,8 @@ import { export interface LinearAuction { auction: Auction; endTime: BigNumber; - startNumerator: BigNumber; - endNumerator: BigNumber; + startPrice: BigNumber; + endPrice: BigNumber; } export interface Price { @@ -23,7 +23,7 @@ export interface TokenFlow { } export interface Auction { - pricePrecision: BigNumber; + maxNaturalUnit: BigNumber; minimumBid: BigNumber; startTime: BigNumber; startingCurrentSets: BigNumber; @@ -35,18 +35,18 @@ export interface Auction { export function getLinearAuction(input: any): LinearAuction { const { - pricePrecision, minimumBid, startTime, startingCurrentSets, remainingCurrentSets, combinedCurrentSetUnits, combinedNextSetUnits, + maxNaturalUnit, } = input.auction; return { auction: { - pricePrecision: new BigNumber(pricePrecision), + maxNaturalUnit: new BigNumber(maxNaturalUnit), minimumBid: new BigNumber(minimumBid), startTime: new BigNumber(startTime), startingCurrentSets: new BigNumber(startingCurrentSets), @@ -56,7 +56,7 @@ export function getLinearAuction(input: any): LinearAuction { combinedNextSetUnits: combinedNextSetUnits.map(v => new BigNumber(v)), }, endTime: new BigNumber(input.endTime), - startNumerator: new BigNumber(input.startNumerator), - endNumerator: new BigNumber(input.endNumerator), + startPrice: new BigNumber(input.startPrice), + endPrice: new BigNumber(input.endPrice), }; } \ No newline at end of file diff --git a/utils/constants.ts b/utils/constants.ts index c572e316d..22035263c 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -2,6 +2,7 @@ import { BigNumber } from 'bignumber.js'; import { ether } from '../utils/units'; export const AUCTION_TIME_INCREMENT = new BigNumber(30); // Unix seconds +export const AUCTION_CURVE_DENOMINATOR = ether(1); export const DEFAULT_AUCTION_PRICE_NUMERATOR = new BigNumber(1374); export const DEFAULT_AUCTION_PRICE_DIVISOR = new BigNumber(1000); export const DEFAULT_GAS = 19000000; @@ -17,6 +18,7 @@ export const EMPTY_BYTESTRING: string = '0x00'; export const KYBER_RESERVE_CONFIGURED_RATE: BigNumber = new BigNumber('321556325999999997'); export const NULL_ADDRESS: string = '0x0000000000000000000000000000000000000000'; export const ONE: BigNumber = new BigNumber(1); +export const ONE_HUNDRED = new BigNumber(100); export const ONE_DAY_IN_SECONDS = new BigNumber(86400); export const ONE_HOUR_IN_SECONDS = new BigNumber(3600); export const SCALE_FACTOR = ether(1); diff --git a/utils/contracts.ts b/utils/contracts.ts index ee4e418f1..dd43296bb 100644 --- a/utils/contracts.ts +++ b/utils/contracts.ts @@ -70,6 +70,9 @@ export { TimeLockUpgradeV2Contract } from '../types/generated/time_lock_upgrade_ export { TimeLockUpgradeV2MockContract } from '../types/generated/time_lock_upgrade_v2_mock'; export { TransferProxyContract } from '../types/generated/transfer_proxy'; export { TradingPoolViewerContract } from '../types/generated/trading_pool_viewer'; +export { + TwoAssetPriceBoundedLinearAuctionMockContract +} from '../types/generated/two_asset_price_bounded_linear_auction_mock'; export { UpdatableConstantAuctionPriceCurveContract } from '../types/generated/updatable_constant_auction_price_curve'; export { UpdatableOracleMockContract } from '../types/generated/updatable_oracle_mock'; export { VaultContract } from '../types/generated/vault'; diff --git a/utils/helpers/liquidatorHelper.ts b/utils/helpers/liquidatorHelper.ts index af5ff22d4..20665f1d3 100644 --- a/utils/helpers/liquidatorHelper.ts +++ b/utils/helpers/liquidatorHelper.ts @@ -11,21 +11,27 @@ import { LiquidatorProxyContract, OracleWhiteListContract, SetTokenContract, + TwoAssetPriceBoundedLinearAuctionMockContract, } from '../contracts'; import { getContractInstance, txnFrom } from '../web3Helper'; import { - ZERO, + AUCTION_CURVE_DENOMINATOR, + ONE_HUNDRED, + SCALE_FACTOR, + ZERO } from '../constants'; import { LinearAuction, TokenFlow } from '../auction'; +import { ether } from '@utils/units'; const AuctionMock = artifacts.require('AuctionMock'); const LinearAuctionLiquidator = artifacts.require('LinearAuctionLiquidator'); const LinearAuctionMock = artifacts.require('LinearAuctionMock'); const LiquidatorMock = artifacts.require('LiquidatorMock'); const LiquidatorProxy = artifacts.require('LiquidatorProxy'); +const TwoAssetPriceBoundedLinearAuctionMock = artifacts.require('TwoAssetPriceBoundedLinearAuctionMock'); import { ERC20Helper } from './erc20Helper'; import { LibraryMockHelper } from './libraryMockHelper'; @@ -49,10 +55,9 @@ export class LiquidatorHelper { /* ============ Deployment ============ */ public async deployAuctionMockAsync( - oracleWhiteList: Address, from: Address = this._contractOwnerAddress ): Promise { - const auctionMock = await AuctionMock.new(oracleWhiteList, txnFrom(from)); + const auctionMock = await AuctionMock.new(txnFrom(from)); return new AuctionMockContract(getContractInstance(auctionMock), txnFrom(from)); } @@ -109,6 +114,27 @@ export class LiquidatorHelper { ); } + public async deployTwoAssetPriceBoundedLinearAuctionMock( + oracleWhiteList: Address, + auctionPeriod: BigNumber, + rangeStart: BigNumber, + rangeEnd: BigNumber, + from: Address = this._contractOwnerAddress + ): Promise { + const mockContract = await TwoAssetPriceBoundedLinearAuctionMock.new( + oracleWhiteList, + auctionPeriod, + rangeStart, + rangeEnd, + txnFrom(from) + ); + + return new TwoAssetPriceBoundedLinearAuctionMockContract( + getContractInstance(mockContract), + txnFrom(from) + ); + } + public async deployLiquidatorMockAsync( from: Address = this._contractOwnerAddress ): Promise { @@ -129,43 +155,21 @@ export class LiquidatorHelper { return combinedUnits.map(unit => unit.mul(quantity).div(naturalUnit)); } - public async calculatePricePrecisionAsync( - currentSet: SetTokenContract, - nextSet: SetTokenContract, - oracleWhiteList: OracleWhiteListContract - ): Promise { - const currentSetValue = await this.calculateSetTokenValueAsync(currentSet, oracleWhiteList); - const nextSetValue = await this.calculateSetTokenValueAsync(nextSet, oracleWhiteList); - const minimumPricePrecision = new BigNumber(1000); - - if (currentSetValue.greaterThan(nextSetValue)) { - const orderOfMag = this._libraryMockHelper.ceilLog10(currentSetValue.div(nextSetValue)); - - return minimumPricePrecision.mul(10 ** (orderOfMag.toNumber() - 1)); - } - - return minimumPricePrecision; - } - public async constructCombinedUnitArrayAsync( setToken: SetTokenContract, combinedTokenArray: Address[], minimumBid: BigNumber, - priceDivisor: BigNumber, ): Promise { const setTokenComponents = await setToken.getComponents.callAsync(); const setTokenUnits = await setToken.getUnits.callAsync(); const setTokenNaturalUnit = await setToken.naturalUnit.callAsync(); - // Calculate minimumBidAmount - const maxNaturalUnit = minimumBid.div(priceDivisor); - // Create combined unit array for target Set const combinedSetTokenUnits: BigNumber[] = []; combinedTokenArray.forEach(address => { const index = setTokenComponents.indexOf(address); if (index != -1) { - const totalTokenAmount = setTokenUnits[index].mul(maxNaturalUnit).div(setTokenNaturalUnit); + const totalTokenAmount = setTokenUnits[index].mul(minimumBid).div(setTokenNaturalUnit); combinedSetTokenUnits.push(totalTokenAmount); } else { combinedSetTokenUnits.push(new BigNumber(0)); @@ -174,45 +178,213 @@ export class LiquidatorHelper { return combinedSetTokenUnits; } - public calculateCurrentPrice( + public async calculateMinimumBidAsync( linearAuction: LinearAuction, - timestamp: BigNumber, - auctionPeriod: BigNumber + currentSet: SetTokenContract, + nextSet: SetTokenContract, + assetPairPrice: BigNumber, + ): Promise { + const maxNaturalUnit = BigNumber.max( + await currentSet.naturalUnit.callAsync(), + await nextSet.naturalUnit.callAsync() + ); + + const [assetOneDecimals, assetTwoDecimals] = await this.getTokensDecimalsAsync( + linearAuction.auction.combinedTokenArray + ); + + const assetOneFullUnit = new BigNumber(10 ** assetOneDecimals.toNumber()); + const assetTwoFullUnit = new BigNumber(10 ** assetTwoDecimals.toNumber()); + + const auctionFairValue = this.calculateAuctionBound( + linearAuction, + assetOneFullUnit, + assetTwoFullUnit, + assetPairPrice + ); + + const tokenFlow = this.constructTokenFlow( + linearAuction, + maxNaturalUnit.mul(ether(1)), + auctionFairValue + ); + + const tokenFlowList = [ + BigNumber.max(tokenFlow.inflow[0], tokenFlow.outflow[0]), + BigNumber.max(tokenFlow.inflow[1], tokenFlow.outflow[1]), + ]; + + let minimumBidMultiplier: BigNumber = ZERO; + for (let i = 0; i < linearAuction.auction.combinedTokenArray.length; i++) { + const currentMinBidMultiplier = ether(1000).div(tokenFlowList[i]).round(0, 2); + minimumBidMultiplier = currentMinBidMultiplier.greaterThan(minimumBidMultiplier) ? + currentMinBidMultiplier : + minimumBidMultiplier; + } + + return maxNaturalUnit.mul(minimumBidMultiplier); + } + + public async calculateAuctionBoundsAsync( + linearAuction: LinearAuction, + startBound: BigNumber, + endBound: BigNumber, + oracleWhiteList: OracleWhiteListContract + ): Promise<[BigNumber, BigNumber]> { + const [assetOneDecimals, assetTwoDecimals] = await this.getTokensDecimalsAsync( + linearAuction.auction.combinedTokenArray + ); + + const assetOneFullUnit = new BigNumber(10 ** assetOneDecimals.toNumber()); + const assetTwoFullUnit = new BigNumber(10 ** assetTwoDecimals.toNumber()); + + const [assetOnePrice, assetTwoPrice] = await this.getComponentPricesAsync( + linearAuction.auction.combinedTokenArray, + oracleWhiteList + ); + + const startValue = this.calculateTwoAssetStartPrice( + linearAuction, + assetOneFullUnit, + assetTwoFullUnit, + assetOnePrice.div(assetTwoPrice), + startBound + ); + + const endValue = this.calculateTwoAssetEndPrice( + linearAuction, + assetOneFullUnit, + assetTwoFullUnit, + assetOnePrice.div(assetTwoPrice), + endBound + ); + + return [startValue, endValue]; + } + + public calculateTwoAssetStartPrice( + linearAuction: LinearAuction, + assetOneFullUnit: BigNumber, + assetTwoFullUnit: BigNumber, + assetPairPrice: BigNumber, + startBound: BigNumber ): BigNumber { - const elapsed = timestamp.sub(linearAuction.auction.startTime); - const priceRange = new BigNumber(linearAuction.endNumerator).sub(linearAuction.startNumerator); - const elapsedPrice = elapsed.mul(priceRange).div(auctionPeriod).round(0, 3); + const auctionFairValue = this.calculateAuctionBound( + linearAuction, + assetOneFullUnit, + assetTwoFullUnit, + assetPairPrice + ); + + const tokenFlowIncreasing = this.isTokenFlowIncreasing( + linearAuction.auction.combinedCurrentSetUnits[0], + linearAuction.auction.combinedNextSetUnits[0], + auctionFairValue + ); + + let startPairPrice: BigNumber; + if (tokenFlowIncreasing) { + startPairPrice = assetPairPrice.mul(ONE_HUNDRED.sub(startBound)).div(ONE_HUNDRED); + } else { + startPairPrice = assetPairPrice.mul(ONE_HUNDRED.add(startBound)).div(ONE_HUNDRED); + } - return new BigNumber(linearAuction.startNumerator).add(elapsedPrice); + const startValue = this.calculateAuctionBound( + linearAuction, + assetOneFullUnit, + assetTwoFullUnit, + startPairPrice + ); + return startValue; } - public calculateStartPrice( - fairValue: BigNumber, - rangeStart: BigNumber, + public calculateTwoAssetEndPrice( + linearAuction: LinearAuction, + assetOneFullUnit: BigNumber, + assetTwoFullUnit: BigNumber, + assetPairPrice: BigNumber, + endBound: BigNumber ): BigNumber { - const negativeRange = fairValue.mul(rangeStart).div(100).round(0, 3); - return fairValue.sub(negativeRange); + const auctionFairValue = this.calculateAuctionBound( + linearAuction, + assetOneFullUnit, + assetTwoFullUnit, + assetPairPrice + ); + + const tokenFlowIncreasing = this.isTokenFlowIncreasing( + linearAuction.auction.combinedCurrentSetUnits[0], + linearAuction.auction.combinedNextSetUnits[0], + auctionFairValue + ); + + let endPairPrice: BigNumber; + if (tokenFlowIncreasing) { + endPairPrice = assetPairPrice.mul(ONE_HUNDRED.add(endBound)).div(ONE_HUNDRED); + } else { + endPairPrice = assetPairPrice.mul(ONE_HUNDRED.sub(endBound)).div(ONE_HUNDRED); + } + + const endValue = this.calculateAuctionBound( + linearAuction, + assetOneFullUnit, + assetTwoFullUnit, + endPairPrice + ); + return endValue; } - public calculateEndPrice( - fairValue: BigNumber, - rangeEnd: BigNumber, + public calculateAuctionBound( + linearAuction: LinearAuction, + assetOneFullUnit: BigNumber, + assetTwoFullUnit: BigNumber, + targetPrice: BigNumber + ): BigNumber { + + const combinedNextUnitArray = linearAuction.auction.combinedNextSetUnits; + const combinedCurrentUnitArray = linearAuction.auction.combinedCurrentSetUnits; + + const calcNumerator = combinedNextUnitArray[1].mul(AUCTION_CURVE_DENOMINATOR).div(assetTwoFullUnit).add( + targetPrice.mul(combinedNextUnitArray[0]).mul(AUCTION_CURVE_DENOMINATOR).div(assetOneFullUnit) + ); + + const calcDenominator = combinedCurrentUnitArray[1].div(assetTwoFullUnit).add( + targetPrice.mul(combinedCurrentUnitArray[0]).div(assetOneFullUnit) + ); + + return calcNumerator.div(calcDenominator).round(0, 3); + } + + public isTokenFlowIncreasing( + assetOneCurrentUnit: BigNumber, + assetOneNextUnit: BigNumber, + fairValue: BigNumber + ): boolean { + return assetOneNextUnit.mul(AUCTION_CURVE_DENOMINATOR).greaterThan(assetOneCurrentUnit.mul(fairValue)); + } + + public calculateCurrentPrice( + linearAuction: LinearAuction, + timestamp: BigNumber, + auctionPeriod: BigNumber ): BigNumber { - const positiveRange = fairValue.mul(rangeEnd).div(100).round(0, 3); - return fairValue.add(positiveRange); + const elapsed = timestamp.sub(linearAuction.auction.startTime); + const priceRange = new BigNumber(linearAuction.endPrice).sub(linearAuction.startPrice); + const elapsedPrice = elapsed.mul(priceRange).div(auctionPeriod).round(0, 3); + + return new BigNumber(linearAuction.startPrice).add(elapsedPrice); } public async calculateFairValueAsync( currentSetToken: SetTokenContract, nextSetToken: SetTokenContract, oracleWhiteList: OracleWhiteListContract, - pricePrecision: BigNumber, from: Address = this._contractOwnerAddress, ): Promise { const currentSetUSDValue = await this.calculateSetTokenValueAsync(currentSetToken, oracleWhiteList); const nextSetUSDValue = await this.calculateSetTokenValueAsync(nextSetToken, oracleWhiteList); - return nextSetUSDValue.mul(pricePrecision).div(currentSetUSDValue).round(0, 3); + return nextSetUSDValue.mul(SCALE_FACTOR).div(currentSetUSDValue).round(0, 3); } public async calculateSetTokenValueAsync( @@ -290,10 +462,8 @@ export class LiquidatorHelper { public constructTokenFlow( linearAuction: LinearAuction, - pricePrecision: BigNumber, quantity: BigNumber, - priceNumerator: BigNumber, - priceDenominator: BigNumber, + priceScaled: BigNumber, ): TokenFlow { const inflow: BigNumber[] = []; const outflow: BigNumber[] = []; @@ -303,23 +473,23 @@ export class LiquidatorHelper { combinedTokenArray, combinedCurrentSetUnits, combinedNextSetUnits, - minimumBid, + maxNaturalUnit, } = linearAuction.auction; - const unitsMultiplier = quantity.div(minimumBid).round(0, 3).mul(pricePrecision); + const unitsMultiplier = quantity.div(maxNaturalUnit).round(0, 3); for (let i = 0; i < combinedCurrentSetUnits.length; i++) { - const flow = combinedNextSetUnits[i].mul(priceDenominator).sub(combinedCurrentSetUnits[i].mul(priceNumerator)); + const flow = combinedNextSetUnits[i].mul(SCALE_FACTOR).sub(combinedCurrentSetUnits[i].mul(priceScaled)); if (flow.greaterThan(0)) { const inflowUnit = unitsMultiplier.mul( - combinedNextSetUnits[i].mul(priceDenominator).sub(combinedCurrentSetUnits[i].mul(priceNumerator)) - ).div(priceNumerator).round(0, 3); + combinedNextSetUnits[i].mul(SCALE_FACTOR).sub(combinedCurrentSetUnits[i].mul(priceScaled)) + ).div(priceScaled).round(0, 3); inflow.push(inflowUnit); outflow.push(ZERO); } else { const outflowUnit = unitsMultiplier.mul( - combinedCurrentSetUnits[i].mul(priceNumerator).sub(combinedNextSetUnits[i].mul(priceDenominator)) - ).div(priceNumerator).round(0, 3); + combinedCurrentSetUnits[i].mul(priceScaled).sub(combinedNextSetUnits[i].mul(SCALE_FACTOR)) + ).div(priceScaled).round(0, 3); outflow.push(outflowUnit); inflow.push(ZERO); }