From 5253dbaa1aeec1d616d389bd58d4fe24f997dd90 Mon Sep 17 00:00:00 2001 From: bweick Date: Fri, 30 Nov 2018 16:31:09 -0800 Subject: [PATCH] Use structs to pass auction parameters to price curves, and store params as struct on rebalancingSetToken. (#309) --- contracts/core/RebalancingSetToken.sol | 30 +++---- .../core/lib/RebalancingHelperLibrary.sol | 40 +++++++++ .../IAuctionPriceCurve.sol | 25 +++--- .../LinearAuctionPriceCurve.sol | 42 +++++----- .../core/lib/ConstantAuctionPriceCurve.sol | 30 +++---- .../linearAuctionPriceCurve.spec.ts | 81 +++++++++++-------- .../core/rebalancingSetToken.spec.ts | 14 ++-- 7 files changed, 150 insertions(+), 112 deletions(-) create mode 100644 contracts/core/lib/RebalancingHelperLibrary.sol diff --git a/contracts/core/RebalancingSetToken.sol b/contracts/core/RebalancingSetToken.sol index d0666a604..2930d21cf 100644 --- a/contracts/core/RebalancingSetToken.sol +++ b/contracts/core/RebalancingSetToken.sol @@ -31,6 +31,7 @@ import { ICore } from "./interfaces/ICore.sol"; import { IRebalancingSetFactory } from "./interfaces/IRebalancingSetFactory.sol"; import { ISetToken } from "./interfaces/ISetToken.sol"; import { IVault } from "./interfaces/IVault.sol"; +import { RebalancingHelperLibrary } from "./lib/RebalancingHelperLibrary.sol"; /** @@ -78,12 +79,9 @@ contract RebalancingSetToken is uint256 public proposalStartTime; // State needed for auction/rebalance - uint256 public auctionStartTime; address public nextSet; address public auctionLibrary; - uint256 public auctionStartPrice; - uint256 public auctionTimeToPivot; - uint256 public auctionPivotPrice; + RebalancingHelperLibrary.AuctionPriceParameters public auctionParameters; uint256 public minimumBid; address[] public combinedTokenArray; uint256[] public combinedCurrentUnits; @@ -245,11 +243,19 @@ contract RebalancingSetToken is "RebalancingSetToken.propose: Invalid time to pivot, must be less than 3 days" ); + // Set auction parameters + nextSet = _nextSet; + auctionLibrary = _auctionLibrary; + auctionParameters = RebalancingHelperLibrary.AuctionPriceParameters({ + auctionTimeToPivot: _auctionTimeToPivot, + auctionStartPrice: _auctionStartPrice, + auctionPivotPrice: _auctionPivotPrice, + auctionStartTime: 0 + }); + // Check that pivot price is compliant with library restrictions IAuctionPriceCurve(_auctionLibrary).validateAuctionPriceParameters( - _auctionTimeToPivot, - _auctionStartPrice, - _auctionPivotPrice + auctionParameters ); // Check that the propoosed set natural unit is a multiple of current set natural unit, or vice versa. @@ -265,9 +271,6 @@ contract RebalancingSetToken is // Set auction parameters nextSet = _nextSet; auctionLibrary = _auctionLibrary; - auctionTimeToPivot = _auctionTimeToPivot; - auctionStartPrice = _auctionStartPrice; - auctionPivotPrice = _auctionPivotPrice; // Update state parameters proposalStartTime = block.timestamp; @@ -306,7 +309,7 @@ contract RebalancingSetToken is redeemCurrentSet(); // Update state parameters - auctionStartTime = block.timestamp; + auctionParameters.auctionStartTime = block.timestamp; rebalanceState = State.Rebalance; emit RebalanceStarted(currentSet, nextSet); @@ -447,10 +450,7 @@ contract RebalancingSetToken is // Get bid conversion price, currently static placeholder for calling auctionlibrary (uint256 priceNumerator, uint256 priceDivisor) = IAuctionPriceCurve(auctionLibrary).getCurrentPrice( - auctionStartTime, - auctionTimeToPivot, - auctionStartPrice, - auctionPivotPrice + auctionParameters ); // Normalized quantity amount diff --git a/contracts/core/lib/RebalancingHelperLibrary.sol b/contracts/core/lib/RebalancingHelperLibrary.sol new file mode 100644 index 000000000..265def789 --- /dev/null +++ b/contracts/core/lib/RebalancingHelperLibrary.sol @@ -0,0 +1,40 @@ +/* + Copyright 2018 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +pragma solidity 0.4.25; +pragma experimental "ABIEncoderV2"; + + +/** + * @title RebalancingHelperLibrary + * @author Set Protocol + * + * The Rebalancing Helper Library contains functions for facilitating the rebalancing process for + * Rebalancing Set Tokens. + * + */ + +library RebalancingHelperLibrary { + + /* ============ Structs ============ */ + + struct AuctionPriceParameters { + uint256 auctionStartTime; + uint256 auctionTimeToPivot; + uint256 auctionStartPrice; + uint256 auctionPivotPrice; + } +} diff --git a/contracts/core/lib/auction-price-libraries/IAuctionPriceCurve.sol b/contracts/core/lib/auction-price-libraries/IAuctionPriceCurve.sol index f89d7ac23..57fdbe4b6 100644 --- a/contracts/core/lib/auction-price-libraries/IAuctionPriceCurve.sol +++ b/contracts/core/lib/auction-price-libraries/IAuctionPriceCurve.sol @@ -15,6 +15,9 @@ */ pragma solidity 0.4.25; +pragma experimental "ABIEncoderV2"; + +import { RebalancingHelperLibrary } from "../RebalancingHelperLibrary.sol"; /** @@ -36,35 +39,25 @@ interface IAuctionPriceCurve { /* * Validate any auction parameters that have library-specific restrictions * - * @param _auctionTimeToPivot Time until auction reaches pivot point - * @param _auctionStartPrice The price to start the auction at - * @param _auctionPivotPrice The price at which auction curve changes from linear to exponential + * @param _auctionPriceParameters Struct containing relevant auction price parameters */ function validateAuctionPriceParameters( - uint256 _auctionTimeToPivot, - uint256 _auctionStartPrice, - uint256 _auctionPivotPrice + RebalancingHelperLibrary.AuctionPriceParameters _auctionParameters ) - external + public view; /* * Calculate the current priceRatio for an auction given defined price and time parameters * - * @param _auctionStartTime Time of auction start - * @param _auctionTimeToPivot Time until auction reaches pivot point - * @param _auctionStartPrice The price to start the auction at - * @param _auctionPivotPrice The price at which auction curve changes from linear to exponential + * @param _auctionPriceParameters Struct containing relevant auction price parameters * @return uint256 The auction price numerator * @return uint256 The auction price denominator */ function getCurrentPrice( - uint256 _auctionStartTime, - uint256 _auctionTimeToPivot, - uint256 _auctionStartPrice, - uint256 _auctionPivotPrice + RebalancingHelperLibrary.AuctionPriceParameters _auctionParameters ) - external + public view returns (uint256, uint256); } diff --git a/contracts/core/lib/auction-price-libraries/LinearAuctionPriceCurve.sol b/contracts/core/lib/auction-price-libraries/LinearAuctionPriceCurve.sol index 056bee5fb..4d4f995d3 100644 --- a/contracts/core/lib/auction-price-libraries/LinearAuctionPriceCurve.sol +++ b/contracts/core/lib/auction-price-libraries/LinearAuctionPriceCurve.sol @@ -15,8 +15,10 @@ */ pragma solidity 0.4.25; +pragma experimental "ABIEncoderV2"; import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import { RebalancingHelperLibrary } from "../RebalancingHelperLibrary.sol"; /** @@ -47,28 +49,24 @@ contract LinearAuctionPriceCurve { /* * Validate any auction parameters that have library-specific restrictions * - * @param -- Unused auction time to pivot param to conform to IAuctionPriceCurve -- - * @param -- Unused auction start price to conform to IAuctionPriceCurve -- - * @param _auctionPivotPrice The price at which auction curve changes from linear to exponential + * @param _auctionPriceParameters Struct containing relevant auction price parameters */ function validateAuctionPriceParameters( - uint256, - uint256, - uint256 _auctionPivotPrice + RebalancingHelperLibrary.AuctionPriceParameters _auctionParameters ) - external + public view { // Require pivot price to be greater than 0.5 * price denominator // Equivalent to oldSet/newSet = 0.5 require( - _auctionPivotPrice > priceDenominator.div(2), + _auctionParameters.auctionPivotPrice > priceDenominator.div(2), "LinearAuctionPriceCurve.validateAuctionPriceParameters: Pivot price too low" ); // Require pivot price to be less than 5 * price denominator // Equivalent to oldSet/newSet = 5 require( - _auctionPivotPrice < priceDenominator.mul(5), + _auctionParameters.auctionPivotPrice < priceDenominator.mul(5), "LinearAuctionPriceCurve.validateAuctionPriceParameters: Pivot price too high" ); } @@ -76,28 +74,22 @@ contract LinearAuctionPriceCurve { /* * Calculate the current priceRatio for an auction given defined price and time parameters * - * @param _auctionStartTime Time of auction start - * @param _auctionTimeToPivot Time until auction reaches pivot point - * @param -- Unused auction start price to conform to IAuctionPriceCurve -- - * @param _auctionPivotPrice The price at which auction curve changes from linear to exponential + * @param _auctionPriceParameters Struct containing relevant auction price parameters * @return uint256 The auction price numerator * @return uint256 The auction price denominator */ function getCurrentPrice( - uint256 _auctionStartTime, - uint256 _auctionTimeToPivot, - uint256, - uint256 _auctionPivotPrice + RebalancingHelperLibrary.AuctionPriceParameters _auctionParameters ) - external + public view returns (uint256, uint256) { // Calculate how much time has elapsed since start of auction - uint256 elapsed = block.timestamp.sub(_auctionStartTime); + uint256 elapsed = block.timestamp.sub(_auctionParameters.auctionStartTime); // Initialize numerator and denominator - uint256 priceNumerator = _auctionPivotPrice; + uint256 priceNumerator = _auctionParameters.auctionPivotPrice; uint256 currentPriceDenominator = priceDenominator; /* @@ -136,12 +128,12 @@ contract LinearAuctionPriceCurve { */ // If time hasn't passed to pivot use the user-defined curve - if (elapsed <= _auctionTimeToPivot) { + if (elapsed <= _auctionParameters.auctionTimeToPivot) { // Calculate the priceNumerator as a linear function of time between 0 and _auctionPivotPrice - priceNumerator = elapsed.mul(_auctionPivotPrice).div(_auctionTimeToPivot); + priceNumerator = elapsed.mul(_auctionParameters.auctionPivotPrice).div(_auctionParameters.auctionTimeToPivot); } else { // Calculate how many 30 second increments have passed since pivot was reached - uint256 thirtySecondPeriods = elapsed.sub(_auctionTimeToPivot).div(30); + uint256 thirtySecondPeriods = elapsed.sub(_auctionParameters.auctionTimeToPivot).div(30); // Because after 1000 thirtySecondPeriods the priceDenominator would be 0 (causes revert) if (thirtySecondPeriods < 1000) { @@ -153,7 +145,9 @@ contract LinearAuctionPriceCurve { currentPriceDenominator = 1; // Now priceNumerator just changes linearly, but with slope equal to the pivot price - priceNumerator = _auctionPivotPrice.add(_auctionPivotPrice.mul(thirtySecondPeriods.sub(1000))); + priceNumerator = _auctionParameters.auctionPivotPrice.add( + _auctionParameters.auctionPivotPrice.mul(thirtySecondPeriods.sub(1000)) + ); } } diff --git a/contracts/mocks/core/lib/ConstantAuctionPriceCurve.sol b/contracts/mocks/core/lib/ConstantAuctionPriceCurve.sol index 50247f195..65dc29389 100644 --- a/contracts/mocks/core/lib/ConstantAuctionPriceCurve.sol +++ b/contracts/mocks/core/lib/ConstantAuctionPriceCurve.sol @@ -15,8 +15,10 @@ */ pragma solidity 0.4.25; +pragma experimental "ABIEncoderV2"; import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import { RebalancingHelperLibrary } from "../../../core/lib/RebalancingHelperLibrary.sol"; /** * @title ConstantAuctionPriceCurve @@ -52,49 +54,39 @@ contract ConstantAuctionPriceCurve { /* * Validate any auction parameters that have library-specific restrictions * - * @param -- Unused auction time to pivot param to conform to IAuctionPriceCurve -- - * @param -- Unused auction start price to conform to IAuctionPriceCurve -- - * @param _auctionPivotPrice The price at which auction curve changes from linear to exponential + * @param _auctionPriceParameters Struct containing relevant auction price parameters */ function validateAuctionPriceParameters( - uint256, - uint256, - uint256 _auctionPivotPrice + RebalancingHelperLibrary.AuctionPriceParameters _auctionParameters ) - external + public view { // Require pivot price to be greater than 0.5 * price denominator // Equivalent to oldSet/newSet = 0.5 require( - _auctionPivotPrice > priceDenominator.div(2), + _auctionParameters.auctionPivotPrice > priceDenominator.div(2), "LinearAuctionPriceCurve.validateAuctionPriceParameters: Pivot price too low" ); // Require pivot price to be less than 5 * price denominator // Equivalent to oldSet/newSet = 5 require( - _auctionPivotPrice < priceDenominator.mul(5), + _auctionParameters.auctionPivotPrice < priceDenominator.mul(5), "LinearAuctionPriceCurve.validateAuctionPriceParameters: Pivot price too high" ); } /* - * Return constant price amount + * Calculate the current priceRatio for an auction given defined price and time parameters * - * @param -- Unused auction start time to conform to IAuctionPriceCurve -- - * @param -- Unused auction time to pivot param to conform to IAuctionPriceCurve -- - * @param -- Unused auction start price to conform to IAuctionPriceCurve -- - * @param -- Unused auction pivot price to conform to IAuctionPriceCurve -- + * @param _auctionPriceParameters Struct containing relevant auction price parameters * @return uint256 The auction price numerator * @return uint256 The auction price denominator */ function getCurrentPrice( - uint256, - uint256, - uint256, - uint256 + RebalancingHelperLibrary.AuctionPriceParameters _auctionParameters ) - external + public view returns (uint256, uint256) { 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 1d0ba0a4d..4619c868e 100644 --- a/test/contracts/core/lib/auction-price-libraries/linearAuctionPriceCurve.spec.ts +++ b/test/contracts/core/lib/auction-price-libraries/linearAuctionPriceCurve.spec.ts @@ -54,23 +54,32 @@ contract('LinearAuctionPriceCurve', accounts => { }); describe('#validateAuctionPriceParameters', async () => { - let subjectAuctionTimeToPivot: BigNumber; - let subjectAuctionStartPrice: BigNumber; - let subjectAuctionPivotPrice: BigNumber; + let auctionStartTime: BigNumber; + let auctionTimeToPivot: BigNumber; + let auctionStartPrice: BigNumber; + let auctionPivotPrice: BigNumber; + + let subjectAuctionPriceParameters: any; let subjectCaller: Address; beforeEach(async () => { - subjectAuctionStartPrice = new BigNumber(500); - subjectAuctionTimeToPivot = new BigNumber(100000); - subjectAuctionPivotPrice = DEFAULT_AUCTION_PRICE_DENOMINATOR.mul(2); + auctionStartPrice = new BigNumber(500); + auctionTimeToPivot = new BigNumber(100000); + auctionStartTime = ZERO; + auctionPivotPrice = DEFAULT_AUCTION_PRICE_DENOMINATOR.mul(2); + + subjectAuctionPriceParameters = { + auctionStartTime, + auctionTimeToPivot, + auctionStartPrice, + auctionPivotPrice, + }; subjectCaller = ownerAccount; }); async function subject(): Promise { return auctionCurve.validateAuctionPriceParameters.callAsync( - subjectAuctionTimeToPivot, - subjectAuctionStartPrice, - subjectAuctionPivotPrice, + subjectAuctionPriceParameters, { from: subjectCaller, gas: DEFAULT_GAS} ); } @@ -83,8 +92,8 @@ contract('LinearAuctionPriceCurve', accounts => { describe('when the pivot price is lower than .5', async () => { beforeEach(async () => { - const auctionPivotPrice = new BigNumber(0.4); - subjectAuctionPivotPrice = DEFAULT_AUCTION_PRICE_DENOMINATOR.mul(auctionPivotPrice); + const auctionPivotRatio = new BigNumber(0.4); + subjectAuctionPriceParameters.auctionPivotPrice = DEFAULT_AUCTION_PRICE_DENOMINATOR.mul(auctionPivotRatio); }); it('should revert', async () => { @@ -94,8 +103,8 @@ contract('LinearAuctionPriceCurve', accounts => { describe('when the pivot price is higher than 5', async () => { beforeEach(async () => { - const auctionPivotPrice = new BigNumber(6); - subjectAuctionPivotPrice = DEFAULT_AUCTION_PRICE_DENOMINATOR.mul(auctionPivotPrice); + const auctionPivotRatio = new BigNumber(6); + subjectAuctionPriceParameters.auctionPivotPrice = DEFAULT_AUCTION_PRICE_DENOMINATOR.mul(auctionPivotRatio); }); it('should revert', async () => { @@ -105,26 +114,32 @@ contract('LinearAuctionPriceCurve', accounts => { }); describe('#getCurrentPrice', async () => { - let subjectAuctionStartTime: BigNumber; - let subjectAuctionTimeToPivot: BigNumber; - let subjectAuctionStartPrice: BigNumber; - let subjectAuctionPivotPrice: BigNumber; + let auctionStartTime: BigNumber; + let auctionTimeToPivot: BigNumber; + let auctionStartPrice: BigNumber; + let auctionPivotPrice: BigNumber; + + let subjectAuctionPriceParameters: any; let subjectCaller: Address; beforeEach(async () => { - subjectAuctionStartPrice = new BigNumber(500); - subjectAuctionTimeToPivot = new BigNumber(100000); - subjectAuctionStartTime = SetTestUtils.generateTimestamp(0); - subjectAuctionPivotPrice = DEFAULT_AUCTION_PRICE_DENOMINATOR.mul(2); + auctionStartPrice = new BigNumber(500); + auctionTimeToPivot = new BigNumber(100000); + auctionStartTime = SetTestUtils.generateTimestamp(0); + auctionPivotPrice = DEFAULT_AUCTION_PRICE_DENOMINATOR.mul(2); + + subjectAuctionPriceParameters = { + auctionStartPrice, + auctionTimeToPivot, + auctionStartTime, + auctionPivotPrice, + }; subjectCaller = ownerAccount; }); async function subject(): Promise { return auctionCurve.getCurrentPrice.callAsync( - subjectAuctionStartTime, - subjectAuctionTimeToPivot, - subjectAuctionStartPrice, - subjectAuctionPivotPrice, + subjectAuctionPriceParameters, { from: subjectCaller, gas: DEFAULT_GAS} ); } @@ -144,8 +159,8 @@ contract('LinearAuctionPriceCurve', accounts => { const expectedPrice = rebalancingWrapper.getExpectedLinearAuctionPrice( timeJump, - subjectAuctionTimeToPivot, - subjectAuctionPivotPrice, + subjectAuctionPriceParameters.auctionTimeToPivot, + subjectAuctionPriceParameters.auctionPivotPrice, DEFAULT_AUCTION_PRICE_DENOMINATOR, ); @@ -154,12 +169,12 @@ contract('LinearAuctionPriceCurve', accounts => { }); it('returns the correct price at the pivot', async () => { - const timeJump = subjectAuctionTimeToPivot; + const timeJump = subjectAuctionPriceParameters.auctionTimeToPivot; await blockchain.increaseTimeAsync(timeJump); const returnedPrice = await subject(); - expect(returnedPrice[0]).to.be.bignumber.equal(subjectAuctionPivotPrice); + expect(returnedPrice[0]).to.be.bignumber.equal(subjectAuctionPriceParameters.auctionPivotPrice); expect(returnedPrice[1]).to.be.bignumber.equal(DEFAULT_AUCTION_PRICE_DENOMINATOR); }); @@ -171,8 +186,8 @@ contract('LinearAuctionPriceCurve', accounts => { const expectedPrice = rebalancingWrapper.getExpectedLinearAuctionPrice( timeJump, - subjectAuctionTimeToPivot, - subjectAuctionPivotPrice, + subjectAuctionPriceParameters.auctionTimeToPivot, + subjectAuctionPriceParameters.auctionPivotPrice, DEFAULT_AUCTION_PRICE_DENOMINATOR, ); @@ -188,8 +203,8 @@ contract('LinearAuctionPriceCurve', accounts => { const expectedPrice = rebalancingWrapper.getExpectedLinearAuctionPrice( timeJump, - subjectAuctionTimeToPivot, - subjectAuctionPivotPrice, + subjectAuctionPriceParameters.auctionTimeToPivot, + subjectAuctionPriceParameters.auctionPivotPrice, DEFAULT_AUCTION_PRICE_DENOMINATOR, ); diff --git a/test/contracts/core/rebalancingSetToken.spec.ts b/test/contracts/core/rebalancingSetToken.spec.ts index ece5e5008..fe62403e7 100644 --- a/test/contracts/core/rebalancingSetToken.spec.ts +++ b/test/contracts/core/rebalancingSetToken.spec.ts @@ -1028,22 +1028,26 @@ contract('RebalancingSetToken', accounts => { it('updates the time to pivot correctly', async () => { await subject(); - const newCurveCoefficient = await rebalancingSetToken.auctionTimeToPivot.callAsync(); - expect(newCurveCoefficient).to.be.bignumber.equal(subjectAuctionTimeToPivot); + // const newCurveCoefficient = await rebalancingSetToken.auctionTimeToPivot.callAsync(); + const auctionParameters = await rebalancingSetToken.auctionParameters.callAsync(); + const newAuctionTimeToPivot = auctionParameters[1]; + expect(newAuctionTimeToPivot).to.be.bignumber.equal(subjectAuctionTimeToPivot); }); it('updates the auction start price correctly', async () => { await subject(); - const newAuctionStartPrice = await rebalancingSetToken.auctionStartPrice.callAsync(); + const auctionParameters = await rebalancingSetToken.auctionParameters.callAsync(); + const newAuctionStartPrice = auctionParameters[2]; expect(newAuctionStartPrice).to.be.bignumber.equal(subjectAuctionStartPrice); }); it('updates the auction pivot price correctly', async () => { await subject(); - const newAuctionPriceDivisor = await rebalancingSetToken.auctionPivotPrice.callAsync(); - expect(newAuctionPriceDivisor).to.be.bignumber.equal(subjectAuctionPivotPrice); + const auctionParameters = await rebalancingSetToken.auctionParameters.callAsync(); + const newAuctionPivotPrice = auctionParameters[3]; + expect(newAuctionPivotPrice).to.be.bignumber.equal(subjectAuctionPivotPrice); }); it('updates the rebalanceState to Proposal', async () => {