From 84a32f4a32d8f6759ea7a9ec1593ee2ba0988fd6 Mon Sep 17 00:00:00 2001 From: bweick Date: Thu, 29 Nov 2018 17:36:14 -0800 Subject: [PATCH] Brian/new pivot price curve (#306) New price curve with pivot pattern --- .../LinearAuctionPriceCurve.sol | 72 +++++++++++++++++-- .../linearAuctionPriceCurve.spec.ts | 54 +++++++++++++- utils/rebalancingWrapper.ts | 34 +++++++-- 3 files changed, 145 insertions(+), 15 deletions(-) diff --git a/contracts/core/lib/auction-price-libraries/LinearAuctionPriceCurve.sol b/contracts/core/lib/auction-price-libraries/LinearAuctionPriceCurve.sol index 33e669219..056bee5fb 100644 --- a/contracts/core/lib/auction-price-libraries/LinearAuctionPriceCurve.sol +++ b/contracts/core/lib/auction-price-libraries/LinearAuctionPriceCurve.sol @@ -78,7 +78,7 @@ contract LinearAuctionPriceCurve { * * @param _auctionStartTime Time of auction start * @param _auctionTimeToPivot Time until auction reaches pivot point - * @param _auctionStartPrice The price to start the auction at + * @param -- Unused auction start price to conform to IAuctionPriceCurve -- * @param _auctionPivotPrice The price at which auction curve changes from linear to exponential * @return uint256 The auction price numerator * @return uint256 The auction price denominator @@ -86,17 +86,77 @@ contract LinearAuctionPriceCurve { function getCurrentPrice( uint256 _auctionStartTime, uint256 _auctionTimeToPivot, - uint256 _auctionStartPrice, + uint256, uint256 _auctionPivotPrice ) external view returns (uint256, uint256) { - // Calculate how much time has elapsed since start of auction and divide by - // timeIncrement of 30 seconds, so price changes every 30 seconds - uint256 elapsed = block.timestamp.sub(_auctionStartTime).div(30); + // Calculate how much time has elapsed since start of auction + uint256 elapsed = block.timestamp.sub(_auctionStartTime); + + // Initialize numerator and denominator + uint256 priceNumerator = _auctionPivotPrice; + uint256 currentPriceDenominator = priceDenominator; + + /* + * This price curve can be broken down into three stages, 1) set up to allow a portion where managers + * have control over the cadence of the auction, and then two more stages that are used to enforce finality + * to the auction. The auction price, p(x), is defined by: + * + * p(x) = (priceNumerator/priceDenominator + * + * In each stage either the priceNumerator or priceDenominator is manipulated to change p(x).The curve shape + * in each stage is defined below. + * + * 1) Managers have the greatest control over stage 1. Here they define a linear curve that starts at zero + * and terminates at the passed pivot price. The length of time it takes for the auction to reach the pivot + * price is defined by the manager too, thus resulting in the following equation for the slope of the line: + * + * PriceNumerator(x) = auctionPivotPrice*(x/auctionTimeToPivot), where x is amount of time from auction start + * + * 2) Stage 2 the protocol takes over to attempt to hasten/guarantee finality, this unfortunately decreases + * the granularity of the auction price changes. In this stage the PriceNumerator remains fixed at the + * auctionPivotPrice. However, the priceDenominator decays at a rate equivalent to 0.1% of the ORIGINAL + * priceDenominator every 30 secs. This leads to the following function relative to time: + * + * PriceDenominator(x) = priceDenominator-(0.01*priceDeonimator*((x-auctionTimeToPivot)/30)), where x is amount + * of time from auction start. + * + * Since we are decaying the denominator the price curve takes on the shape of e^x. Because of the limitations + * of integer math the denominator can only be decayed to 1. Hence in order to maintain simplicity in calculations + * there is a third segment defined below. + * + * 3) The third segment is a simple linear calculation that changes the priceNumerator at the rate of the pivot + * price every 30 seconds and fixes the priceDenominator at 1: + * + * PriceNumerator(x) = auctionPivotPrice + auctionPivotPrice*(x-auctionTimeToPivot-30000), where x is amount of + * time from auction start and 30000 represents the amount of time spent in Stage 2 + */ + + // If time hasn't passed to pivot use the user-defined curve + if (elapsed <= _auctionTimeToPivot) { + // Calculate the priceNumerator as a linear function of time between 0 and _auctionPivotPrice + priceNumerator = elapsed.mul(_auctionPivotPrice).div(_auctionTimeToPivot); + } else { + // Calculate how many 30 second increments have passed since pivot was reached + uint256 thirtySecondPeriods = elapsed.sub(_auctionTimeToPivot).div(30); + + // Because after 1000 thirtySecondPeriods the priceDenominator would be 0 (causes revert) + if (thirtySecondPeriods < 1000) { + // Calculate new denominator where the denominator decays at a rate of 0.1% of the ORIGINAL + // priceDenominator per time increment (hence divide by 1000) + currentPriceDenominator = priceDenominator.sub(thirtySecondPeriods.mul(priceDenominator).div(1000)); + } else { + // Once denominator has fully decayed, fix it at 1 + currentPriceDenominator = 1; + + // Now priceNumerator just changes linearly, but with slope equal to the pivot price + priceNumerator = _auctionPivotPrice.add(_auctionPivotPrice.mul(thirtySecondPeriods.sub(1000))); + } + } - return (elapsed, priceDenominator); + return (priceNumerator, currentPriceDenominator); } } diff --git a/test/contracts/core/lib/auction-price-libraries/linearAuctionPriceCurve.spec.ts b/test/contracts/core/lib/auction-price-libraries/linearAuctionPriceCurve.spec.ts index 93961de99..1d0ba0a4d 100644 --- a/test/contracts/core/lib/auction-price-libraries/linearAuctionPriceCurve.spec.ts +++ b/test/contracts/core/lib/auction-price-libraries/linearAuctionPriceCurve.spec.ts @@ -142,9 +142,59 @@ contract('LinearAuctionPriceCurve', accounts => { const returnedPrice = await subject(); - const expectedPrice = timeJump.div(new BigNumber(30)); - expect(returnedPrice[0]).to.be.bignumber.equal(expectedPrice); + const expectedPrice = rebalancingWrapper.getExpectedLinearAuctionPrice( + timeJump, + subjectAuctionTimeToPivot, + subjectAuctionPivotPrice, + DEFAULT_AUCTION_PRICE_DENOMINATOR, + ); + + expect(returnedPrice[0]).to.be.bignumber.equal(expectedPrice.priceNumerator); + expect(returnedPrice[1]).to.be.bignumber.equal(expectedPrice.priceDenominator); + }); + + it('returns the correct price at the pivot', async () => { + const timeJump = subjectAuctionTimeToPivot; + await blockchain.increaseTimeAsync(timeJump); + + const returnedPrice = await subject(); + + expect(returnedPrice[0]).to.be.bignumber.equal(subjectAuctionPivotPrice); expect(returnedPrice[1]).to.be.bignumber.equal(DEFAULT_AUCTION_PRICE_DENOMINATOR); }); + + it('returns the correct price after the pivot', async () => { + const timeJump = new BigNumber(115000); + await blockchain.increaseTimeAsync(timeJump); + + const returnedPrice = await subject(); + + const expectedPrice = rebalancingWrapper.getExpectedLinearAuctionPrice( + timeJump, + subjectAuctionTimeToPivot, + subjectAuctionPivotPrice, + DEFAULT_AUCTION_PRICE_DENOMINATOR, + ); + + expect(returnedPrice[0]).to.be.bignumber.equal(expectedPrice.priceNumerator); + expect(returnedPrice[1]).to.be.bignumber.equal(expectedPrice.priceDenominator); + }); + + it('returns the correct price after denominator hits 1', async () => { + const timeJump = new BigNumber(150000); + await blockchain.increaseTimeAsync(timeJump); + + const returnedPrice = await subject(); + + const expectedPrice = rebalancingWrapper.getExpectedLinearAuctionPrice( + timeJump, + subjectAuctionTimeToPivot, + subjectAuctionPivotPrice, + DEFAULT_AUCTION_PRICE_DENOMINATOR, + ); + + expect(returnedPrice[0]).to.be.bignumber.equal(expectedPrice.priceNumerator); + expect(returnedPrice[1]).to.be.bignumber.equal(expectedPrice.priceDenominator); + }); }); }); diff --git a/utils/rebalancingWrapper.ts b/utils/rebalancingWrapper.ts index 05cddeb45..d89c72fbb 100644 --- a/utils/rebalancingWrapper.ts +++ b/utils/rebalancingWrapper.ts @@ -14,7 +14,6 @@ import { import { BigNumber } from 'bignumber.js'; import { - AUCTION_TIME_INCREMENT, DEFAULT_GAS, DEFAULT_REBALANCING_NATURAL_UNIT, DEFAULT_UNIT_SHARES, @@ -444,11 +443,32 @@ export class RebalancingWrapper { public getExpectedLinearAuctionPrice( elapsedTime: BigNumber, - curveCoefficient: BigNumber, - auctionStartPrice: BigNumber - ): BigNumber { - const elaspedTimeFromStart = elapsedTime.div(AUCTION_TIME_INCREMENT).round(0, 3); - const expectedPrice = curveCoefficient.mul(elaspedTimeFromStart).add(auctionStartPrice); - return expectedPrice; + auctionTimeToPivot: BigNumber, + auctionPivotPrice: BigNumber, + priceDivisor: BigNumber, + ): any { + let priceNumerator: BigNumber; + let priceDenominator: BigNumber; + const timeIncrementsToZero = new BigNumber(1000); + + if (elapsedTime.lessThanOrEqualTo(auctionTimeToPivot)) { + priceNumerator = elapsedTime.mul(auctionPivotPrice).div(auctionTimeToPivot).round(0, 3); + + priceDenominator = priceDivisor; + } else { + const timeIncrements = elapsedTime.sub(auctionTimeToPivot).div(30).round(0, 3); + + if (timeIncrements.lessThan(timeIncrementsToZero)) { + priceNumerator = auctionPivotPrice; + priceDenominator = priceDivisor.sub(timeIncrements.mul(priceDivisor).div(1000).round(0, 3)); + } else { + priceDenominator = new BigNumber(1); + priceNumerator = auctionPivotPrice.add(auctionPivotPrice.mul(timeIncrements.sub(1000))); + } + } + return { + priceNumerator, + priceDenominator, + }; } }