From 5298ce3095a6c3942602a22d7225d38c7bab23af Mon Sep 17 00:00:00 2001 From: Brian Weickmann Date: Tue, 18 Sep 2018 19:41:30 +0200 Subject: [PATCH 1/4] Add natural unit restrictions for proposed Set. Create minimumBid amount. Redo token flow math to use max component Set Token natural unit as base amount instead of 10**18. Figure out how many Sets to issue in settlement and recalcualte unitShares. --- contracts/core/RebalancingSetToken.sol | 211 ++++++++++---- contracts/core/interfaces/ICore.sol | 24 +- test/core/extensions/coreIssuance.spec.ts | 59 ++-- .../core/extensions/coreIssuanceOrder.spec.ts | 24 +- .../extensions/coreRebalanceAuction.spec.ts | 160 ++++------- test/core/rebalancingSetToken.spec.ts | 260 ++++++++++++------ test/core/vault.spec.ts | 8 +- utils/RebalancingTokenWrapper.ts | 124 ++++++++- utils/constants.ts | 4 +- utils/contract_logs/rebalancingSetToken.ts | 4 +- 10 files changed, 567 insertions(+), 311 deletions(-) diff --git a/contracts/core/RebalancingSetToken.sol b/contracts/core/RebalancingSetToken.sol index a74160901..307f89abe 100644 --- a/contracts/core/RebalancingSetToken.sol +++ b/contracts/core/RebalancingSetToken.sol @@ -16,15 +16,18 @@ pragma solidity 0.4.24; +import { AddressArrayUtils } from "cryptofin-solidity/contracts/array-utils/AddressArrayUtils.sol"; import { DetailedERC20 } from "zeppelin-solidity/contracts/token/ERC20/DetailedERC20.sol"; +import { Math } from "zeppelin-solidity/contracts/math/Math.sol"; import { SafeMath } from "zeppelin-solidity/contracts/math/SafeMath.sol"; import { StandardToken } from "zeppelin-solidity/contracts/token/ERC20/StandardToken.sol"; -import { AddressArrayUtils } from "cryptofin-solidity/contracts/array-utils/AddressArrayUtils.sol"; +import { Bytes32 } from "../lib/Bytes32.sol"; +import { CommonMath } from "../lib/CommonMath.sol"; +import { ERC20Wrapper } from "../lib/ERC20Wrapper.sol"; import { ICore } from "./interfaces/ICore.sol"; import { ISetFactory } from "./interfaces/ISetFactory.sol"; -import { Bytes32 } from "../lib/Bytes32.sol"; import { ISetToken } from "./interfaces/ISetToken.sol"; - +import { IVault } from "./interfaces/IVault.sol"; /** @@ -48,7 +51,10 @@ contract RebalancingSetToken is /* ============ State Variables ============ */ address public factory; - uint256 public naturalUnit = 1; + // All rebalancingSetTokens have same natural unit, still allows for + // small amounts to be issued and attempts to reduce slippage as much + // as possible. + uint256 public naturalUnit = 10**10; address public manager; State public rebalanceState; @@ -66,14 +72,15 @@ contract RebalancingSetToken is // State needed for auction/rebalance uint256 public auctionStartTime; - address public rebalancingSet; + address public nextSet; address public auctionLibrary; uint256 public auctionPriceDivisor; uint256 public auctionStartPrice; + uint256 public minimumBid; uint256 public curveCoefficient; address[] public combinedTokenArray; uint256[] public combinedCurrentUnits; - uint256[] public combinedRebalanceUnits; + uint256[] public combinedNextSetUnits; uint256 public remainingCurrentSets; uint256 public rebalanceSetSupply; @@ -85,7 +92,7 @@ contract RebalancingSetToken is ); event RebalanceProposed( - address rebalancingSet, + address nextSet, address indexed auctionLibrary, uint256 indexed proposalPeriodEndTime ); @@ -152,14 +159,14 @@ contract RebalancingSetToken is * Function used to set the terms of the next rebalance and start the proposal period * * - * @param _rebalancingSet The Set to rebalance into + * @param _nextSet The Set to rebalance into * @param _auctionLibrary The library used to calculate the Dutch Auction price * @param _curveCoefficient The slope (or convexity) of the price curve * @param _auctionPriceDivisor The granularity with which the prices change * @param _auctionStartPrice The price to start the auction at */ function propose( - address _rebalancingSet, + address _nextSet, address _auctionLibrary, uint256 _curveCoefficient, uint256 _auctionStartPrice, @@ -178,10 +185,18 @@ contract RebalancingSetToken is require(block.timestamp >= lastRebalanceTimestamp.add(rebalanceInterval)); // Check that new proposed Set is valid Set created by Core - require(ICore(ISetFactory(factory).core()).validSets(_rebalancingSet)); + require(ICore(ISetFactory(factory).core()).validSets(_nextSet)); + + // Check that the propoosed set is a multiple of current set, or vice versa + uint256 currentNaturalUnit = ISetToken(currentSet).naturalUnit(); + uint256 nextSetNaturalUnit = ISetToken(_nextSet).naturalUnit(); + require( + Math.max256(currentNaturalUnit, nextSetNaturalUnit) % + Math.min256(currentNaturalUnit, nextSetNaturalUnit) == 0 + ); // Set auction parameters - rebalancingSet = _rebalancingSet; + nextSet = _nextSet; auctionLibrary = _auctionLibrary; curveCoefficient = _curveCoefficient; auctionStartPrice = _auctionStartPrice; @@ -192,7 +207,7 @@ contract RebalancingSetToken is rebalanceState = State.Proposal; emit RebalanceProposed( - _rebalancingSet, + _nextSet, _auctionLibrary, proposalStartTime.add(proposalPeriod) ); @@ -212,13 +227,13 @@ contract RebalancingSetToken is require(block.timestamp >= proposalStartTime.add(proposalPeriod)); // Create token arrays needed for auction - parseUnitArrays(); + auctionSetUp(); // Get core address address core = ISetFactory(factory).core(); // Calculate remainingCurrentSets - remainingCurrentSets = unitShares.mul(totalSupply_); + remainingCurrentSets = unitShares.mul(totalSupply_).div(naturalUnit); // Redeem current set held by rebalancing token in vault ICore(core).redeemInVault(currentSet, remainingCurrentSets); @@ -227,7 +242,7 @@ contract RebalancingSetToken is auctionStartTime = block.timestamp; rebalanceState = State.Rebalance; - emit RebalanceStarted(currentSet, rebalancingSet); + emit RebalanceStarted(currentSet, nextSet); } /* @@ -235,14 +250,42 @@ contract RebalancingSetToken is * set owners. * */ - function settlement() + function settleRebalance() external { // Must be in Rebalance state to call settlement require(rebalanceState == State.Rebalance); + // Make sure all currentSets have been rebalanced + require(remainingCurrentSets < minimumBid); + + // Create ICore object + ICore core = ICore(ISetFactory(factory).core()); + + // Issue nextSet + uint256 issueAmount; + (issueAmount, unitShares) = calculateNextSetIssueQuantity(); + core.issue( + nextSet, + issueAmount + ); + + // Ensure allowance to transfer sets to Vault + ERC20Wrapper.ensureAllowance( + nextSet, + this, + core.transferProxy(), + issueAmount + ); + + // Deposit newly created Sets in Vault + core.deposit( + nextSet, + issueAmount + ); + // Set current set to be rebalancing set - currentSet = rebalancingSet; + currentSet = nextSet; // Update state parameters lastRebalanceTimestamp = block.timestamp; @@ -269,22 +312,19 @@ contract RebalancingSetToken is // Confirm in Rebalance State require(rebalanceState == State.Rebalance); - // Make sure that quantity remaining is - uint256 filled_quantity; - if (_quantity < remainingCurrentSets) { - filled_quantity = _quantity; - } else { - filled_quantity = remainingCurrentSets; - } + // Make sure that bid amount is multiple of minimum bid amount + require(_quantity % minimumBid == 0); + // Make sure that bid Amount is less than remainingCurrentSets + require(_quantity <= remainingCurrentSets); + + // Calculate token inflow and outflow arrays uint256[] memory inflowUnitArray = new uint256[](combinedTokenArray.length); uint256[] memory outflowUnitArray = new uint256[](combinedTokenArray.length); - uint256 rebalanceSetsAdded; - (inflowUnitArray, outflowUnitArray, rebalanceSetsAdded) = getBidPrice(filled_quantity); + (inflowUnitArray, outflowUnitArray) = getBidPrice(_quantity); - remainingCurrentSets = remainingCurrentSets.sub(filled_quantity); - rebalanceSetSupply = rebalanceSetSupply.add(rebalanceSetsAdded); + remainingCurrentSets = remainingCurrentSets.sub(_quantity); return (combinedTokenArray, inflowUnitArray, outflowUnitArray); } @@ -295,14 +335,14 @@ contract RebalancingSetToken is * @param _quantity The amount of currentSet to be rebalanced * @return uint256[] Array of amount of tokens inserted into system in bid * @return uint256[] Array of amount of tokens taken out of system in bid - * @return uint256 Amount of rebalancingSets traded into + * @return uint256 Amount of nextSets traded into */ function getBidPrice( uint256 _quantity ) public view - returns (uint256[], uint256[], uint256) + returns (uint256[], uint256[]) { // Confirm in Rebalance State require(rebalanceState == State.Rebalance); @@ -312,29 +352,29 @@ contract RebalancingSetToken is uint256[] memory outflowUnitArray = new uint256[](combinedTokenArray.length); // Get bid conversion price - uint256 priceNumerator = 1; - uint256 priceDivisor = 1; + uint256 priceNumerator = 1374; + + // Normalized quantity amount + uint256 unitsMultiplier = _quantity.div(minimumBid).mul(auctionPriceDivisor); for (uint256 i=0; i < combinedTokenArray.length; i++) { - uint256 rebalanceUnit = combinedRebalanceUnits[i]; + uint256 nextUnit = combinedNextSetUnits[i]; uint256 currentUnit = combinedCurrentUnits[i]; // If rebalance greater than currentUnit*price token inflow, else token outflow - if (rebalanceUnit > currentUnit.mul(priceNumerator).div(priceDivisor)) { - inflowUnitArray[i] = _quantity.mul(rebalanceUnit.sub( - priceNumerator.mul(currentUnit).div(priceDivisor) - )).div(10**18); + if (nextUnit > currentUnit.mul(priceNumerator).div(auctionPriceDivisor)) { + inflowUnitArray[i] = unitsMultiplier.mul( + nextUnit.mul(auctionPriceDivisor).sub(currentUnit.mul(priceNumerator)) + ).div(priceNumerator); outflowUnitArray[i] = 0; } else { - outflowUnitArray[i] = _quantity.mul( - priceNumerator.mul(currentUnit).div(priceDivisor).sub(rebalanceUnit).div(10**18) - ); + outflowUnitArray[i] = unitsMultiplier.mul( + currentUnit.mul(priceNumerator).sub(nextUnit.mul(auctionPriceDivisor)) + ).div(priceNumerator); inflowUnitArray[i] = 0; } } - // Calculate amount of currentSets traded for rebalancingSets - uint256 rebalanceSetsAdded = _quantity.mul(priceDivisor).div(priceNumerator); - return (inflowUnitArray, outflowUnitArray, rebalanceSetsAdded); + return (inflowUnitArray, outflowUnitArray); } /* @@ -487,38 +527,48 @@ contract RebalancingSetToken is } /* - * Get combinedRebalanceUnits of Rebalancing Set + * Get combinedNextSetUnits of Rebalancing Set * - * @return combinedRebalanceUnits + * @return combinedNextSetUnits */ - function getCombinedRebalanceUnits() + function getCombinedNextSetUnits() external view returns(uint256[]) { - return combinedRebalanceUnits; + return combinedNextSetUnits; } /* ============ Internal Functions ============ */ - function parseUnitArrays() + /** + * Create array that represents all components in currentSet and nextSet. + * Calcualate unit difference between both sets relative to the largest natural + * unit of the two sets. + */ + function auctionSetUp() internal { // Create interfaces for interacting with sets ISetToken currentSetInterface = ISetToken(currentSet); - ISetToken rebalancingSetInterface = ISetToken(rebalancingSet); + ISetToken nextSetInterface = ISetToken(nextSet); // Create combined token Array address[] memory oldComponents = currentSetInterface.getComponents(); - address[] memory newComponents = rebalancingSetInterface.getComponents(); + address[] memory newComponents = nextSetInterface.getComponents(); combinedTokenArray = oldComponents.union(newComponents); // Get naturalUnit of both sets uint256 currentSetNaturalUnit = currentSetInterface.naturalUnit(); - uint256 rebalancingSetNaturalUnit = rebalancingSetInterface.naturalUnit(); + uint256 nextSetNaturalUnit = nextSetInterface.naturalUnit(); // Get units arrays for both sets uint256[] memory currentSetUnits = currentSetInterface.getUnits(); - uint256[] memory rebalancingSetUnits = rebalancingSetInterface.getUnits(); + uint256[] memory nextSetUnits = nextSetInterface.getUnits(); + + minimumBid = Math.max256( + currentSetNaturalUnit.mul(auctionPriceDivisor), + nextSetNaturalUnit.mul(auctionPriceDivisor) + ); for (uint256 i=0; i < combinedTokenArray.length; i++) { // Check if component in arrays and get index if it is @@ -534,15 +584,61 @@ contract RebalancingSetToken is combinedCurrentUnits.push(uint256(0)); } - // Compute and push unit amounts of token in rebalancingSet, push 0 if not in set + // Compute and push unit amounts of token in nextSet, push 0 if not in set if (isInRebalance) { - combinedRebalanceUnits.push( - computeUnits(rebalancingSetUnits[indexRebalance], rebalancingSetNaturalUnit) + combinedNextSetUnits.push( + computeUnits(nextSetUnits[indexRebalance], nextSetNaturalUnit) ); } else { - combinedRebalanceUnits.push(uint256(0)); + combinedNextSetUnits.push(uint256(0)); + } + } + } + + /** + * Calculate the amount of nextSets to issue by using the component amounts in the + * vault, unitShares following from this calculation. + * @return uint256 Amount of nextSets to issue + * @return uint256 New unitShares for the rebalancingSetToken + */ + function calculateNextSetIssueQuantity() + internal + returns (uint256, uint256) + { + // Collect data necessary to compute issueAmounts + uint256 nextNaturalUnit = ISetToken(nextSet).naturalUnit(); + address[] memory nextComponents = ISetToken(nextSet).getComponents(); + uint256[] memory nextUnits = ISetToken(nextSet).getUnits(); + uint256 maxIssueAmount = CommonMath.maxUInt256(); + + // Set up vault interface + address vaultAddress = ICore(ISetFactory(factory).core()).vault(); + IVault vault = IVault(vaultAddress); + + for (uint256 i = 0; i < nextComponents.length; i++) { + // Get amount of components in vault owned by rebalancingSetToken + uint256 componentAmount = vault.getOwnerBalance( + nextComponents[i], + this + ); + + // Calculate amount of Sets that can be issued from those components, if less than amount for other + // components then set that as maxIssueAmount + uint256 componentIssueAmount = componentAmount.div(nextUnits[i]).mul(nextNaturalUnit); + if (componentIssueAmount < maxIssueAmount) { + maxIssueAmount = componentIssueAmount; } } + + // Calculate the amount of naturalUnits worth of rebalancingSetToken outstanding + uint256 naturalUnitsOutstanding = totalSupply_.div(naturalUnit); + + // Issue amount of Sets that is closest multiple of nextNaturalUnit to the maxIssueAmount + uint256 issueAmount = maxIssueAmount.div(nextNaturalUnit).mul(nextNaturalUnit); + + // Divide final issueAmount by naturalUnitsOutstanding to get newUnitShares + uint256 newUnitShares = issueAmount.div(naturalUnitsOutstanding); + return (issueAmount, newUnitShares); } /** @@ -558,7 +654,6 @@ contract RebalancingSetToken is internal returns (uint256) { - uint256 coefficient = uint256(10) ** uint256(18); - return coefficient.mul(_unit).div(_naturalUnit); + return minimumBid.mul(_unit).div(_naturalUnit).div(auctionPriceDivisor); } } diff --git a/contracts/core/interfaces/ICore.sol b/contracts/core/interfaces/ICore.sol index 2a9c10df7..97ea87497 100644 --- a/contracts/core/interfaces/ICore.sol +++ b/contracts/core/interfaces/ICore.sol @@ -25,10 +25,30 @@ pragma solidity 0.4.24; * various extensions and is a light weight way to interact with the contract. */ interface ICore { + /** + * Return transferProxy address. + * + * @return address transferProxy address + */ + function transferProxy() + public + view + returns(address); + + /** + * Return vault address. + * + * @return address vault address + */ + function vault() + public + view + returns(address); + /* - * Get natural unit of Set + * Returns if valid set * - * @return uint256 Natural unit of Set + * @return bool If valid set */ function validSets(address) external diff --git a/test/core/extensions/coreIssuance.spec.ts b/test/core/extensions/coreIssuance.spec.ts index cb2c5df4f..f06ba91e8 100644 --- a/test/core/extensions/coreIssuance.spec.ts +++ b/test/core/extensions/coreIssuance.spec.ts @@ -366,29 +366,21 @@ contract('CoreIssuance', accounts => { let initialShareRatio: BigNumber; let rebalancingNaturalUnit: BigNumber; - const naturalUnit: BigNumber = ether(2); - let components: StandardTokenMockContract[] = []; - let componentUnits: BigNumber[]; let setToken: SetTokenContract; - let rebalancingToken: RebalancingSetTokenContract; + let rebalancingSetToken: RebalancingSetTokenContract; beforeEach(async () => { - components = await erc20Wrapper.deployTokensAsync(2, ownerAccount); - await erc20Wrapper.approveTransfersAsync(components, transferProxy.address); - - const componentAddresses = _.map(components, token => token.address); - componentUnits = _.map(components, () => ether(4)); // Multiple of naturalUnit - setToken = await coreWrapper.createSetTokenAsync( + const setTokens = await rebalancingTokenWrapper.createSetTokensAsync( core, setTokenFactory.address, - componentAddresses, - componentUnits, - naturalUnit, + transferProxy.address, + 1 ); + setToken = setTokens[0]; rebalancingNaturalUnit = DEFAULT_REBALANCING_NATURAL_UNIT; initialShareRatio = DEFAULT_UNIT_SHARES; - rebalancingToken = await rebalancingTokenWrapper.createDefaultRebalancingSetTokenAsync( + rebalancingSetToken = await rebalancingTokenWrapper.createDefaultRebalancingSetTokenAsync( core, rebalancingTokenFactory.address, managerAccount, @@ -403,7 +395,7 @@ contract('CoreIssuance', accounts => { subjectCaller = ownerAccount; subjectQuantityToIssue = ether(1); - subjectSetToIssue = rebalancingToken.address; + subjectSetToIssue = rebalancingSetToken.address; }); async function subject(): Promise { @@ -432,7 +424,7 @@ contract('CoreIssuance', accounts => { const expectedLogs: Log[] = _.map([setToken], (component, idx) => { return IssuanceComponentDeposited( core.address, - rebalancingToken.address, + rebalancingSetToken.address, setToken.address, subjectQuantityToIssue.mul(initialShareRatio).div(rebalancingNaturalUnit), ); @@ -442,14 +434,14 @@ contract('CoreIssuance', accounts => { }); it('updates the balances of the components in the vault to belong to the set token', async () => { - const existingBalance = await vault.balances.callAsync(setToken.address, rebalancingToken.address); + const existingBalance = await vault.balances.callAsync(setToken.address, rebalancingSetToken.address); await subject(); const expectedNewBalance = existingBalance.add( subjectQuantityToIssue.div(rebalancingNaturalUnit).mul(initialShareRatio) ); - const newBalance = await vault.balances.callAsync(setToken.address, rebalancingToken.address); + const newBalance = await vault.balances.callAsync(setToken.address, rebalancingSetToken.address); expect(newBalance).to.be.bignumber.eql(expectedNewBalance); }); @@ -463,11 +455,11 @@ contract('CoreIssuance', accounts => { }); it('mints the correct quantity of the set for the user', async () => { - const existingBalance = await rebalancingToken.balanceOf.callAsync(ownerAccount); + const existingBalance = await rebalancingSetToken.balanceOf.callAsync(ownerAccount); await subject(); - await assertTokenBalanceAsync(rebalancingToken, existingBalance.add(subjectQuantityToIssue), ownerAccount); + await assertTokenBalanceAsync(rebalancingSetToken, existingBalance.add(subjectQuantityToIssue), ownerAccount); }); describe('when the set was not created through core', async () => { @@ -496,7 +488,6 @@ contract('CoreIssuance', accounts => { describe('when the required set component quantity is in the vault for the user', async () => { let alreadyDepositedQuantity: BigNumber; - const componentUnit: BigNumber = new BigNumber(1); beforeEach(async () => { alreadyDepositedQuantity = vanillaQuantityToIssue; @@ -505,6 +496,7 @@ contract('CoreIssuance', accounts => { it('updates the vault balance of the component for the user by the correct amount', async () => { const existingVaultBalance = await vault.balances.callAsync(setToken.address, ownerAccount); + const componentUnit = await rebalancingSetToken.unitShares.callAsync(); await subject(); @@ -515,11 +507,11 @@ contract('CoreIssuance', accounts => { }); it('mints the correct quantity of the set for the user', async () => { - const existingBalance = await rebalancingToken.balanceOf.callAsync(ownerAccount); + const existingBalance = await rebalancingSetToken.balanceOf.callAsync(ownerAccount); await subject(); - await assertTokenBalanceAsync(rebalancingToken, existingBalance.add(subjectQuantityToIssue), ownerAccount); + await assertTokenBalanceAsync(rebalancingSetToken, existingBalance.add(subjectQuantityToIssue), ownerAccount); }); }); @@ -558,11 +550,11 @@ contract('CoreIssuance', accounts => { }); it('mints the correct quantity of the set for the user', async () => { - const existingBalance = await rebalancingToken.balanceOf.callAsync(ownerAccount); + const existingBalance = await rebalancingSetToken.balanceOf.callAsync(ownerAccount); await subject(); - await assertTokenBalanceAsync(rebalancingToken, existingBalance.add(subjectQuantityToIssue), ownerAccount); + await assertTokenBalanceAsync(rebalancingSetToken, existingBalance.add(subjectQuantityToIssue), ownerAccount); }); }); }); @@ -707,25 +699,18 @@ contract('CoreIssuance', accounts => { let rebalancingQuantityToIssue: BigNumber; let rebalancingTokenToIssue: Address; - const naturalUnit: BigNumber = ether(2); - let components: StandardTokenMockContract[] = []; - let componentUnits: BigNumber[]; let setToken: SetTokenContract; let rebalancingToken: RebalancingSetTokenContract; beforeEach(async () => { - components = await erc20Wrapper.deployTokensAsync(2, ownerAccount); - await erc20Wrapper.approveTransfersAsync(components, transferProxy.address); - - const componentAddresses = _.map(components, token => token.address); - componentUnits = _.map(components, () => ether(4)); // Multiple of naturalUnit - setToken = await coreWrapper.createSetTokenAsync( + const setTokens = await rebalancingTokenWrapper.createSetTokensAsync( core, setTokenFactory.address, - componentAddresses, - componentUnits, - naturalUnit, + transferProxy.address, + 1 ); + setToken = setTokens[0]; + rebalancingNaturalUnit = DEFAULT_REBALANCING_NATURAL_UNIT; initialShareRatio = DEFAULT_UNIT_SHARES; diff --git a/test/core/extensions/coreIssuanceOrder.spec.ts b/test/core/extensions/coreIssuanceOrder.spec.ts index d8f10dd3f..8db2c755e 100644 --- a/test/core/extensions/coreIssuanceOrder.spec.ts +++ b/test/core/extensions/coreIssuanceOrder.spec.ts @@ -264,23 +264,23 @@ contract('CoreIssuanceOrder', accounts => { }); it('transfers the remaining maker tokens to the taker', async () => { - const existingBalance = await makerToken.balanceOf.callAsync(issuanceOrderTaker); - await assertTokenBalanceAsync(makerToken, ZERO, issuanceOrderTaker); + const existingBalance = await makerToken.balanceOf.callAsync(subjectCaller); + await assertTokenBalanceAsync(makerToken, ZERO, subjectCaller); await subject(); const netMakerToTaker = order.makerTokenAmount.sub(zeroExOrder.fillAmount); const expectedNewBalance = existingBalance.plus(netMakerToTaker); - await assertTokenBalanceAsync(makerToken, expectedNewBalance, issuanceOrderTaker); + await assertTokenBalanceAsync(makerToken, expectedNewBalance, subjectCaller); }); it('transfers the fees to the relayer', async () => { - await assertTokenBalanceAsync(relayerToken, ZERO, order.relayerAddress); + await assertTokenBalanceAsync(relayerToken, ZERO, relayerAccount); await subject(); - const expectedNewBalance = order.makerRelayerFee.add(order.takerRelayerFee); - await assertTokenBalanceAsync(relayerToken, expectedNewBalance, order.relayerAddress); + const expectedNewBalance = ether(3); + await assertTokenBalanceAsync(relayerToken, expectedNewBalance, relayerAccount); }); it('mints the correct quantity of the set for the maker', async () => { @@ -341,25 +341,25 @@ contract('CoreIssuanceOrder', accounts => { await assertTokenBalanceAsync(makerToken, expectedNewBalance, issuanceOrderMaker); }); - it('transfers the partial maker token amount to the taker', async () => { - const existingBalance = await makerToken.balanceOf.callAsync(issuanceOrderTaker); - await assertTokenBalanceAsync(makerToken, ZERO, issuanceOrderTaker); + it('transfers the remaining maker tokens to the taker', async () => { + const existingBalance = await makerToken.balanceOf.callAsync(subjectCaller); + await assertTokenBalanceAsync(makerToken, ZERO, subjectCaller); await subject(); const makerTokenAmountAvailableForThisOrder = order.makerTokenAmount.div(2); const netMakerToTaker = makerTokenAmountAvailableForThisOrder.mul(subjectQuantityToFill).div(ether(4)); const expectedNewBalance = existingBalance.plus(netMakerToTaker); - await assertTokenBalanceAsync(makerToken, expectedNewBalance, issuanceOrderTaker); + await assertTokenBalanceAsync(makerToken, expectedNewBalance, subjectCaller); }); it('transfers the partial fees to the relayer', async () => { - await assertTokenBalanceAsync(relayerToken, ZERO, order.relayerAddress); + await assertTokenBalanceAsync(relayerToken, ZERO, relayerAccount); await subject(); const expectedNewBalance = ether(3).mul(subjectQuantityToFill).div(ether(4)); - await assertTokenBalanceAsync(relayerToken, expectedNewBalance, order.relayerAddress); + await assertTokenBalanceAsync(relayerToken, expectedNewBalance, relayerAccount); }); it('mints the correct partial quantity of the set for the user', async () => { diff --git a/test/core/extensions/coreRebalanceAuction.spec.ts b/test/core/extensions/coreRebalanceAuction.spec.ts index 9c9c4bc30..ef48019e8 100644 --- a/test/core/extensions/coreRebalanceAuction.spec.ts +++ b/test/core/extensions/coreRebalanceAuction.spec.ts @@ -36,11 +36,13 @@ const { expect } = chai; const blockchain = new Blockchain(web3); + contract('CoreRebalanceAuction', accounts => { const [ deployerAccount, managerAccount, libraryAccount, + bidderAccount, ] = accounts; let rebalancingSetToken: RebalancingSetTokenContract; @@ -94,18 +96,22 @@ contract('CoreRebalanceAuction', accounts => { let proposalPeriod: BigNumber; let currentSetToken: SetTokenContract; - let newRebalancingSetToken: SetTokenContract; - let rebalancingSetQuantityToIssue: BigNumber; + let nextSetToken: SetTokenContract; + let rebalancingSetTokenQuantityToIssue: BigNumber; beforeEach(async () => { + const naturalUnits = [ether(.001), ether(.0001)]; + const setTokens = await rebalancingTokenWrapper.createSetTokensAsync( coreMock, factory.address, transferProxy.address, - 2 + 2, + naturalUnits ); + currentSetToken = setTokens[0]; - newRebalancingSetToken = setTokens[1]; + nextSetToken = setTokens[1]; proposalPeriod = ONE_DAY_IN_SECONDS; rebalancingSetToken = await rebalancingTokenWrapper.createDefaultRebalancingSetTokenAsync( @@ -117,15 +123,20 @@ contract('CoreRebalanceAuction', accounts => { ); // Issue currentSetToken - await coreMock.issue.sendTransactionAsync(currentSetToken.address, ether(4), {from: deployerAccount}); + await coreMock.issue.sendTransactionAsync(currentSetToken.address, ether(8), {from: deployerAccount}); await erc20Wrapper.approveTransfersAsync([currentSetToken], transferProxy.address); // Use issued currentSetToken to issue rebalancingSetToken - rebalancingSetQuantityToIssue = ether(2); - await coreMock.issue.sendTransactionAsync(rebalancingSetToken.address, rebalancingSetQuantityToIssue); + rebalancingSetTokenQuantityToIssue = ether(8); + await coreMock.issue.sendTransactionAsync(rebalancingSetToken.address, rebalancingSetTokenQuantityToIssue); + + // Determine minimum bid + const decOne = await currentSetToken.naturalUnit.callAsync(); + const decTwo = await nextSetToken.naturalUnit.callAsync(); + const minBid = new BigNumber(Math.max(decOne.toNumber(), decTwo.toNumber()) * 1000); subjectCaller = deployerAccount; - subjectQuantity = ether(2); + subjectQuantity = minBid; subjectRebalancingSetToken = rebalancingSetToken.address; }); @@ -147,7 +158,7 @@ contract('CoreRebalanceAuction', accounts => { beforeEach(async () => { await rebalancingTokenWrapper.defaultTransitionToProposeAsync( rebalancingSetToken, - newRebalancingSetToken.address, + nextSetToken.address, libraryAccount, managerAccount ); @@ -162,19 +173,21 @@ contract('CoreRebalanceAuction', accounts => { beforeEach(async () => { await rebalancingTokenWrapper.defaultTransitionToRebalanceAsync( rebalancingSetToken, - newRebalancingSetToken.address, + nextSetToken.address, libraryAccount, managerAccount ); }); it('transfers the correct amount of tokens to the bidder in the Vault', async () => { - const expectedTokenFlows = await rebalancingSetToken.getBidPrice.callAsync(subjectQuantity); - const tokenArray = await rebalancingSetToken.getCombinedTokenArray.callAsync(); - const outflowArray = expectedTokenFlows[1]; + const expectedTokenFlows = await rebalancingTokenWrapper.constructInflowOutflowArraysAsync( + rebalancingSetToken, + subjectQuantity + ); + const combinedTokenArray = await rebalancingSetToken.getCombinedTokenArray.callAsync(); const oldReceiverBalances = await coreWrapper.getVaultBalancesForTokensForOwner( - tokenArray, + combinedTokenArray, vault, deployerAccount ); @@ -182,23 +195,26 @@ contract('CoreRebalanceAuction', accounts => { await subject(); const newReceiverBalances = await coreWrapper.getVaultBalancesForTokensForOwner( - tokenArray, + combinedTokenArray, vault, deployerAccount ); const expectedReceiverBalances = _.map(oldReceiverBalances, (balance, index) => - balance.add(outflowArray[index]) + balance.add(expectedTokenFlows['outflowArray'][index]) ); + expect(JSON.stringify(newReceiverBalances)).to.equal(JSON.stringify(expectedReceiverBalances)); }); it('transfers the correct amount of tokens from the bidder to the rebalancing token in Vault', async () => { - const expectedTokenFlows = await rebalancingSetToken.getBidPrice.callAsync(subjectQuantity); - const tokenArray = await rebalancingSetToken.getCombinedTokenArray.callAsync(); - const inflowArray = expectedTokenFlows[0]; + const expectedTokenFlows = await rebalancingTokenWrapper.constructInflowOutflowArraysAsync( + rebalancingSetToken, + subjectQuantity + ); + const combinedTokenArray = await rebalancingSetToken.getCombinedTokenArray.callAsync(); const oldSenderBalances = await coreWrapper.getVaultBalancesForTokensForOwner( - tokenArray, + combinedTokenArray, vault, rebalancingSetToken.address ); @@ -206,12 +222,12 @@ contract('CoreRebalanceAuction', accounts => { await subject(); const newSenderBalances = await coreWrapper.getVaultBalancesForTokensForOwner( - tokenArray, + combinedTokenArray, vault, rebalancingSetToken.address ); const expectedSenderBalances = _.map(oldSenderBalances, (balance, index) => - balance.add(inflowArray[index]).sub(expectedTokenFlows[1][index]) + balance.add(expectedTokenFlows['inflowArray'][index]).sub(expectedTokenFlows['outflowArray'][index]) ); expect(JSON.stringify(newSenderBalances)).to.equal(JSON.stringify(expectedSenderBalances)); }); @@ -225,17 +241,6 @@ contract('CoreRebalanceAuction', accounts => { const newRemainingSets = await rebalancingSetToken.remainingCurrentSets.callAsync(); expect(newRemainingSets).to.be.bignumber.equal(expectedRemainingSets); }); - - it('adds the correct amount to rebalanceSetSupply', async () => { - const currentRebalanceSets = await rebalancingSetToken.rebalanceSetSupply.callAsync(); - - await subject(); - - const price = new BigNumber(1); - const expectedRebalanceSets = currentRebalanceSets.add(subjectQuantity.mul(price)); - const newRebalanceSets = await rebalancingSetToken.rebalanceSetSupply.callAsync(); - expect(newRebalanceSets).to.be.bignumber.equal(expectedRebalanceSets); - }); }); }); @@ -244,7 +249,7 @@ contract('CoreRebalanceAuction', accounts => { let subjectQuantity: BigNumber; let proposalPeriod: BigNumber; - let newRebalancingSetToken: SetTokenContract; + let nextSetToken: SetTokenContract; let currentSetToken: SetTokenContract; beforeEach(async () => { @@ -255,7 +260,7 @@ contract('CoreRebalanceAuction', accounts => { 2 ); currentSetToken = setTokens[0]; - newRebalancingSetToken = setTokens[1]; + nextSetToken = setTokens[1]; proposalPeriod = ONE_DAY_IN_SECONDS; rebalancingSetToken = await rebalancingTokenWrapper.createDefaultRebalancingSetTokenAsync( @@ -287,7 +292,7 @@ contract('CoreRebalanceAuction', accounts => { beforeEach(async () => { await rebalancingTokenWrapper.defaultTransitionToProposeAsync( rebalancingSetToken, - newRebalancingSetToken.address, + nextSetToken.address, libraryAccount, managerAccount ); @@ -302,37 +307,22 @@ contract('CoreRebalanceAuction', accounts => { beforeEach(async () => { await rebalancingTokenWrapper.defaultTransitionToRebalanceAsync( rebalancingSetToken, - newRebalancingSetToken.address, + nextSetToken.address, libraryAccount, managerAccount ); }); - it('returns the correct UnitArrays; using price=1', async () => { - const combinedCurrentUnits = await rebalancingSetToken.getCombinedCurrentUnits.callAsync(); - const combinedRebalanceUnits = await rebalancingSetToken.getCombinedRebalanceUnits.callAsync(); - - const arrays = await subject(); - - const price = new BigNumber(1); - combinedCurrentUnits.forEach((amount, index) => { - const flow = combinedRebalanceUnits[index].sub(amount.mul(price)); - if (flow >= new BigNumber(0)) { - expect(arrays[0][index]).to.be.bignumber.equal(flow); - expect(arrays[1][index]).to.be.bignumber.equal(new BigNumber(0)); - } else { - expect(arrays[0][index]).to.be.bignumber.equal(new BigNumber(0)); - expect(arrays[1][index]).to.be.bignumber.equal(flow.mul(new BigNumber(-1))); - } - }); - }); + it('returns the correct UnitArrays; using price=1.374', async () => { + const expectedFlows = await rebalancingTokenWrapper.constructInflowOutflowArraysAsync( + rebalancingSetToken, + subjectQuantity + ); - it('returns the correct rebalanceSetsAdded; using price=1', async () => { const arrays = await subject(); - const price = new BigNumber(1); - const expectedRebalancingSets = price.mul(subjectQuantity); - expect(arrays[2]).to.be.bignumber.equal(expectedRebalancingSets); + expect(JSON.stringify(arrays[0])).to.equal(JSON.stringify(expectedFlows['inflowArray'])); + expect(JSON.stringify(arrays[1])).to.equal(JSON.stringify(expectedFlows['outflowArray'])); }); }); }); @@ -342,7 +332,7 @@ contract('CoreRebalanceAuction', accounts => { let subjectQuantity: BigNumber; let amountToIssue: BigNumber; - let newRebalancingSetToken: SetTokenContract; + let nextSetToken: SetTokenContract; let currentSetToken: SetTokenContract; beforeEach(async () => { @@ -353,7 +343,7 @@ contract('CoreRebalanceAuction', accounts => { 2 ); currentSetToken = setTokens[0]; - newRebalancingSetToken = setTokens[1]; + nextSetToken = setTokens[1]; const proposalPeriod = ONE_DAY_IN_SECONDS; rebalancingSetToken = await rebalancingTokenWrapper.createDefaultRebalancingSetTokenAsync( @@ -369,7 +359,7 @@ contract('CoreRebalanceAuction', accounts => { await erc20Wrapper.approveTransfersAsync([currentSetToken], transferProxy.address); await coreMock.issue.sendTransactionAsync(rebalancingSetToken.address, amountToIssue); - subjectCaller = managerAccount; + subjectCaller = bidderAccount; subjectQuantity = ether(1); }); @@ -391,7 +381,7 @@ contract('CoreRebalanceAuction', accounts => { beforeEach(async () => { await rebalancingTokenWrapper.defaultTransitionToProposeAsync( rebalancingSetToken, - newRebalancingSetToken.address, + nextSetToken.address, libraryAccount, managerAccount ); @@ -406,7 +396,7 @@ contract('CoreRebalanceAuction', accounts => { beforeEach(async () => { await rebalancingTokenWrapper.defaultTransitionToRebalanceAsync( rebalancingSetToken, - newRebalancingSetToken.address, + nextSetToken.address, libraryAccount, managerAccount ); @@ -422,43 +412,13 @@ contract('CoreRebalanceAuction', accounts => { expect(newRemainingSets).to.be.bignumber.equal(expectedRemainingSets); }); - it('adds the correct amount to rebalanceSetSupply', async () => { - const currentRebalanceSets = await rebalancingSetToken.rebalanceSetSupply.callAsync(); - - await subject(); - - const price = new BigNumber(1); - const expectedRebalanceSets = currentRebalanceSets.add(subjectQuantity.mul(price)); - const newRebalanceSets = await rebalancingSetToken.rebalanceSetSupply.callAsync(); - expect(newRebalanceSets).to.be.bignumber.equal(expectedRebalanceSets); - }); - describe('and quantity is greater than remaining sets', async () => { beforeEach(async () => { subjectQuantity = ether(4); }); - it('subtracts the correct amount from remainingCurrentSets', async () => { - const currentRemainingSets = await rebalancingSetToken.remainingCurrentSets.callAsync(); - - expect(currentRemainingSets).to.be.bignumber.equal(amountToIssue); - expect(subjectQuantity).to.be.bignumber.greaterThan(currentRemainingSets); - await subject(); - - const expectedRemainingSets = currentRemainingSets.sub(amountToIssue); - const newRemainingSets = await rebalancingSetToken.remainingCurrentSets.callAsync(); - expect(newRemainingSets).to.be.bignumber.equal(expectedRemainingSets); - }); - - it('adds the correct amount to rebalanceSetSupply', async () => { - const currentRebalanceSets = await rebalancingSetToken.rebalanceSetSupply.callAsync(); - - await subject(); - - const price = new BigNumber(1); - const expectedRebalanceSets = currentRebalanceSets.add(amountToIssue.mul(price)); - const newRebalanceSets = await rebalancingSetToken.rebalanceSetSupply.callAsync(); - expect(newRebalanceSets).to.be.bignumber.equal(expectedRebalanceSets); + it('should revert', async () => { + await expectRevertError(subject()); }); }); }); @@ -468,7 +428,7 @@ contract('CoreRebalanceAuction', accounts => { let subjectCaller: Address; let subjectQuantity: BigNumber; - let newRebalancingSetToken: SetTokenContract; + let nextSetToken: SetTokenContract; let currentSetToken: SetTokenContract; beforeEach(async () => { @@ -479,7 +439,7 @@ contract('CoreRebalanceAuction', accounts => { 2 ); currentSetToken = setTokens[0]; - newRebalancingSetToken = setTokens[1]; + nextSetToken = setTokens[1]; const proposalPeriod = ONE_DAY_IN_SECONDS; rebalancingSetToken = await rebalancingTokenWrapper.createDefaultRebalancingSetTokenAsync( @@ -490,7 +450,7 @@ contract('CoreRebalanceAuction', accounts => { proposalPeriod ); - subjectCaller = managerAccount; + subjectCaller = bidderAccount; subjectQuantity = ether(1); }); @@ -505,7 +465,7 @@ contract('CoreRebalanceAuction', accounts => { beforeEach(async () => { await rebalancingTokenWrapper.defaultTransitionToRebalanceAsync( rebalancingSetToken, - newRebalancingSetToken.address, + nextSetToken.address, libraryAccount, managerAccount ); diff --git a/test/core/rebalancingSetToken.spec.ts b/test/core/rebalancingSetToken.spec.ts index 7ef1c1b28..7d61aa921 100644 --- a/test/core/rebalancingSetToken.spec.ts +++ b/test/core/rebalancingSetToken.spec.ts @@ -90,8 +90,7 @@ contract('RebalancingSetToken', accounts => { }); beforeEach(async () => { - await blockchain.saveSnapshotAsync(); - + blockchain.saveSnapshotAsync(); transferProxy = await coreWrapper.deployTransferProxyAsync(); vault = await coreWrapper.deployVaultAsync(); coreMock = await coreWrapper.deployCoreMockAsync(transferProxy, vault); @@ -103,7 +102,7 @@ contract('RebalancingSetToken', accounts => { }); afterEach(async () => { - await blockchain.revertAsync(); + blockchain.revertAsync(); }); describe('#constructor', async () => { @@ -307,19 +306,13 @@ contract('RebalancingSetToken', accounts => { let subjectCaller: Address; beforeEach(async () => { - const naturalUnit: BigNumber = ether(2); - components = await erc20Wrapper.deployTokensAsync(2, deployerAccount); - await erc20Wrapper.approveTransfersAsync(components, transferProxy.address); - - const currentComponentAddresses = _.map(components, token => token.address); - const currentComponentUnits = _.map(components, () => naturalUnit.mul(2)); // Multiple of naturalUnit - const currentSetToken = await coreWrapper.createSetTokenAsync( + const setTokens = await rebalancingTokenWrapper.createSetTokensAsync( coreMock, factory.address, - currentComponentAddresses, - currentComponentUnits, - naturalUnit, + transferProxy.address, + 1 ); + const currentSetToken = setTokens[0]; const manager = managerAccount; const initialSet = currentSetToken.address; @@ -378,7 +371,7 @@ contract('RebalancingSetToken', accounts => { let subjectQuantity: BigNumber; let subjectCaller: Address; - let newRebalancingSetToken: SetTokenContract; + let nextSetToken: SetTokenContract; beforeEach(async () => { const setTokens = await rebalancingTokenWrapper.createSetTokensAsync( @@ -388,7 +381,7 @@ contract('RebalancingSetToken', accounts => { 2 ); const currentSetToken = setTokens[0]; - newRebalancingSetToken = setTokens[1]; + nextSetToken = setTokens[1]; const proposalPeriod = ONE_DAY_IN_SECONDS; rebalancingSetToken = await rebalancingTokenWrapper.createDefaultRebalancingSetTokenAsync( @@ -450,7 +443,7 @@ contract('RebalancingSetToken', accounts => { beforeEach(async () => { await rebalancingTokenWrapper.defaultTransitionToRebalanceAsync( rebalancingSetToken, - newRebalancingSetToken.address, + nextSetToken.address, libraryAccount, managerAccount ); @@ -468,19 +461,13 @@ contract('RebalancingSetToken', accounts => { let subjectCaller: Address; beforeEach(async () => { - const naturalUnit: BigNumber = ether(2); - components = await erc20Wrapper.deployTokensAsync(2, deployerAccount); - await erc20Wrapper.approveTransfersAsync(components, transferProxy.address); - - const currentComponentAddresses = _.map(components, token => token.address); - const currentComponentUnits = _.map(components, () => naturalUnit.mul(2)); // Multiple of naturalUnit - const currentSetToken = await coreWrapper.createSetTokenAsync( + const setTokens = await rebalancingTokenWrapper.createSetTokensAsync( coreMock, factory.address, - currentComponentAddresses, - currentComponentUnits, - naturalUnit, + transferProxy.address, + 1 ); + const currentSetToken = setTokens[0]; const manager = managerAccount; const initialSet = currentSetToken.address; @@ -545,7 +532,8 @@ contract('RebalancingSetToken', accounts => { let subjectQuantity: BigNumber; let subjectCaller: Address; - let newRebalancingSetToken: SetTokenContract; + let nextSetToken: SetTokenContract; + let currentSetToken: SetTokenContract; beforeEach(async () => { const setTokens = await rebalancingTokenWrapper.createSetTokensAsync( @@ -554,8 +542,8 @@ contract('RebalancingSetToken', accounts => { transferProxy.address, 2 ); - const currentSetToken = setTokens[0]; - newRebalancingSetToken = setTokens[1]; + currentSetToken = setTokens[0]; + nextSetToken = setTokens[1]; const proposalPeriod = ONE_DAY_IN_SECONDS; rebalancingSetToken = await rebalancingTokenWrapper.createDefaultRebalancingSetTokenAsync( @@ -571,12 +559,12 @@ contract('RebalancingSetToken', accounts => { subjectQuantity = ether(5); subjectCaller = managerAccount; - await coreMock.mint.sendTransactionAsync( - rebalancingSetToken.address, - subjectBurner, - mintedQuantity, - { from: subjectCaller, gas: DEFAULT_GAS} - ); + // Issue currentSetToken + await coreMock.issue.sendTransactionAsync(currentSetToken.address, ether(5), {from: deployerAccount}); + await erc20Wrapper.approveTransfersAsync([currentSetToken], transferProxy.address); + + // Use issued currentSetToken to issue rebalancingSetToken + await coreMock.issue.sendTransactionAsync(rebalancingSetToken.address, mintedQuantity); }); async function subject(): Promise { @@ -633,18 +621,9 @@ contract('RebalancingSetToken', accounts => { describe('when burn is called from Rebalance state', async () => { beforeEach(async () => { - // Must burn otherwise won't get through rebalance call - // TO DO: Instead of mint call issue in set up. - coreMock.burn.sendTransactionAsync( - rebalancingSetToken.address, - subjectBurner, - subjectQuantity, - { from: subjectCaller, gas: DEFAULT_GAS} - ); - await rebalancingTokenWrapper.defaultTransitionToRebalanceAsync( rebalancingSetToken, - newRebalancingSetToken.address, + nextSetToken.address, libraryAccount, managerAccount ); @@ -720,8 +699,6 @@ contract('RebalancingSetToken', accounts => { }); }); }); - - describe('#propose', async () => { let subjectRebalancingToken: Address; let subjectAuctionLibrary: Address; @@ -733,20 +710,22 @@ contract('RebalancingSetToken', accounts => { let proposalPeriod: BigNumber; let currentSetToken: SetTokenContract; - let newRebalancingSetToken: SetTokenContract; + let nextSetToken: SetTokenContract; let reproposeRebalancingSetToken: SetTokenContract; let setTokens: SetTokenContract[]; + let naturalUnits: BigNumber[]; beforeEach(async () => { setTokens = await rebalancingTokenWrapper.createSetTokensAsync( coreMock, factory.address, transferProxy.address, - 3 + 3, + naturalUnits || undefined ); currentSetToken = setTokens[0]; - newRebalancingSetToken = setTokens[1]; + nextSetToken = setTokens[1]; reproposeRebalancingSetToken = setTokens[2]; proposalPeriod = ONE_DAY_IN_SECONDS; @@ -758,7 +737,7 @@ contract('RebalancingSetToken', accounts => { proposalPeriod ); - subjectRebalancingToken = newRebalancingSetToken.address; + subjectRebalancingToken = nextSetToken.address; subjectAuctionLibrary = libraryAccount; subjectCurveCoefficient = ether(1); subjectAuctionStartPrice = ether(5); @@ -783,7 +762,7 @@ contract('RebalancingSetToken', accounts => { it('updates to the new rebalancing set correctly', async () => { await subject(); - const newRebalacingSet = await rebalancingSetToken.rebalancingSet.callAsync(); + const newRebalacingSet = await rebalancingSetToken.nextSet.callAsync(); expect(newRebalacingSet).to.equal(subjectRebalancingToken); }); @@ -859,7 +838,7 @@ contract('RebalancingSetToken', accounts => { }); }); - describe('but the proposed rebalancingSet is not approved by Core', async () => { + describe('but the proposed nextSet is not approved by Core', async () => { beforeEach(async () => { subjectRebalancingToken = fakeTokenAccount; }); @@ -868,6 +847,20 @@ contract('RebalancingSetToken', accounts => { await expectRevertError(subject()); }); }); + + describe("but the new proposed set's natural unit is not a multiple of the current set", async () => { + before(async () => { + naturalUnits = [ether(.003), ether(.002), ether(.001)]; + }); + + after(async () => { + naturalUnits = undefined; + }); + + it('should revert', async () => { + await expectRevertError(subject()); + }); + }); }); describe('when propose is called from Proposal state', async () => { @@ -876,7 +869,7 @@ contract('RebalancingSetToken', accounts => { beforeEach(async () => { await rebalancingTokenWrapper.defaultTransitionToProposeAsync( rebalancingSetToken, - newRebalancingSetToken.address, + nextSetToken.address, libraryAccount, managerAccount ); @@ -889,7 +882,7 @@ contract('RebalancingSetToken', accounts => { it('updates to the new rebalancing set correctly', async () => { await subject(); - const newRebalancingSet = await rebalancingSetToken.rebalancingSet.callAsync(); + const newRebalancingSet = await rebalancingSetToken.nextSet.callAsync(); expect(newRebalancingSet).to.equal(subjectRebalancingToken); }); @@ -907,7 +900,7 @@ contract('RebalancingSetToken', accounts => { beforeEach(async () => { await rebalancingTokenWrapper.defaultTransitionToRebalanceAsync( rebalancingSetToken, - newRebalancingSetToken.address, + nextSetToken.address, libraryAccount, managerAccount ); @@ -925,7 +918,7 @@ contract('RebalancingSetToken', accounts => { let proposalPeriod: BigNumber; let currentSetToken: SetTokenContract; - let newRebalancingSetToken: SetTokenContract; + let nextSetToken: SetTokenContract; let rebalancingSetQuantityToIssue: BigNumber; beforeEach(async () => { @@ -933,10 +926,12 @@ contract('RebalancingSetToken', accounts => { coreMock, factory.address, transferProxy.address, - 2 + 2, + [ether(.07), ether(.007)] ); + currentSetToken = setTokens[0]; - newRebalancingSetToken = setTokens[1]; + nextSetToken = setTokens[1]; proposalPeriod = ONE_DAY_IN_SECONDS; rebalancingSetToken = await rebalancingTokenWrapper.createDefaultRebalancingSetTokenAsync( @@ -948,11 +943,11 @@ contract('RebalancingSetToken', accounts => { ); // Issue currentSetToken - await coreMock.issue.sendTransactionAsync(currentSetToken.address, ether(4), {from: deployerAccount}); + await coreMock.issue.sendTransactionAsync(currentSetToken.address, ether(7), {from: deployerAccount}); await erc20Wrapper.approveTransfersAsync([currentSetToken], transferProxy.address); // Use issued currentSetToken to issue rebalancingSetToken - rebalancingSetQuantityToIssue = ether(2); + rebalancingSetQuantityToIssue = ether(.7); await coreMock.issue.sendTransactionAsync(rebalancingSetToken.address, rebalancingSetQuantityToIssue); subjectCaller = managerAccount; @@ -976,7 +971,7 @@ contract('RebalancingSetToken', accounts => { beforeEach(async () => { await rebalancingTokenWrapper.defaultTransitionToProposeAsync( rebalancingSetToken, - newRebalancingSetToken.address, + nextSetToken.address, libraryAccount, managerAccount ); @@ -995,7 +990,7 @@ contract('RebalancingSetToken', accounts => { const formattedLogs = await setTestUtils.getLogsFromTxHash(txHash); const expectedLogs = getExpectedRebalanceStartedLog( currentSetToken.address, - newRebalancingSetToken.address, + nextSetToken.address, rebalancingSetToken.address, ); @@ -1004,15 +999,17 @@ contract('RebalancingSetToken', accounts => { it('creates the correct combinedTokenArray', async () => { const oldSet = await currentSetToken.getComponents.callAsync(); - const newSet = await newRebalancingSetToken.getComponents.callAsync(); + const newSet = await nextSetToken.getComponents.callAsync(); await subject(); const expectedCombinedTokenArray = _.union(oldSet, newSet); - expectedCombinedTokenArray.forEach(async (expectAddress, index) => { - const actualAddress = await rebalancingSetToken.combinedTokenArray.callAsync(new BigNumber(index)); - expect(actualAddress).to.be.bignumber.equal(expectAddress); - }); + await Promise.all( + expectedCombinedTokenArray.map(async (expectAddress, index) => { + const actualAddress = await rebalancingSetToken.combinedTokenArray.callAsync(new BigNumber(index)); + expect(actualAddress).to.be.bignumber.equal(expectAddress); + }) + ); }); it('creates the correct combinedCurrentUnits', async () => { @@ -1020,7 +1017,8 @@ contract('RebalancingSetToken', accounts => { const expectedCombinedCurrentUnits = await rebalancingTokenWrapper.constructCombinedUnitArrayAsync( rebalancingSetToken, - currentSetToken + currentSetToken, + nextSetToken ); const actualCombinedCurrentUnits = await rebalancingSetToken.getCombinedCurrentUnits.callAsync(); expect(JSON.stringify(actualCombinedCurrentUnits)).to.eql(JSON.stringify(expectedCombinedCurrentUnits)); @@ -1031,19 +1029,21 @@ contract('RebalancingSetToken', accounts => { const expectedCombinedRebalanceUnits = await rebalancingTokenWrapper.constructCombinedUnitArrayAsync( rebalancingSetToken, - newRebalancingSetToken + nextSetToken, + currentSetToken ); - const actualCombinedRebalanceUnits = await rebalancingSetToken.getCombinedRebalanceUnits.callAsync(); + const actualCombinedRebalanceUnits = await rebalancingSetToken.getCombinedNextSetUnits.callAsync(); expect(JSON.stringify(actualCombinedRebalanceUnits)).to.eql(JSON.stringify(expectedCombinedRebalanceUnits)); }); it('calculates the correct remainingCurrentSets', async () => { const supply = await rebalancingSetToken.totalSupply.callAsync(); const unitShares = await rebalancingSetToken.unitShares.callAsync(); + const naturalUnit = await rebalancingSetToken.naturalUnit.callAsync(); await subject(); - const expectedRemainingCurrentSets = supply.mul(unitShares); + const expectedRemainingCurrentSets = supply.mul(unitShares).div(naturalUnit); const actualRemainingCurrentSets = await rebalancingSetToken.remainingCurrentSets.callAsync(); expect(actualRemainingCurrentSets).to.be.bignumber.equal(expectedRemainingCurrentSets); }); @@ -1101,7 +1101,7 @@ contract('RebalancingSetToken', accounts => { beforeEach(async () => { await rebalancingTokenWrapper.defaultTransitionToRebalanceAsync( rebalancingSetToken, - newRebalancingSetToken.address, + nextSetToken.address, libraryAccount, managerAccount ); @@ -1113,11 +1113,12 @@ contract('RebalancingSetToken', accounts => { }); }); - describe('#settlement', async () => { + describe('#settleRebalance', async () => { let subjectCaller: Address; let proposalPeriod: BigNumber; - let newRebalancingSetToken: SetTokenContract; + let nextSetToken: SetTokenContract; + let rebalancingSetQuantityToIssue: BigNumber; beforeEach(async () => { const setTokens = await rebalancingTokenWrapper.createSetTokensAsync( @@ -1127,7 +1128,7 @@ contract('RebalancingSetToken', accounts => { 2 ); const currentSetToken = setTokens[0]; - newRebalancingSetToken = setTokens[1]; + nextSetToken = setTokens[1]; proposalPeriod = ONE_DAY_IN_SECONDS; rebalancingSetToken = await rebalancingTokenWrapper.createDefaultRebalancingSetTokenAsync( @@ -1138,26 +1139,34 @@ contract('RebalancingSetToken', accounts => { proposalPeriod ); + // Issue currentSetToken + await coreMock.issue.sendTransactionAsync(currentSetToken.address, ether(9), {from: deployerAccount}); + await erc20Wrapper.approveTransfersAsync([currentSetToken], transferProxy.address); + + // Use issued currentSetToken to issue rebalancingSetToken + rebalancingSetQuantityToIssue = ether(7); + await coreMock.issue.sendTransactionAsync(rebalancingSetToken.address, rebalancingSetQuantityToIssue); + subjectCaller = managerAccount; }); async function subject(): Promise { - return rebalancingSetToken.settlement.sendTransactionAsync( + return rebalancingSetToken.settleRebalance.sendTransactionAsync( { from: subjectCaller, gas: DEFAULT_GAS} ); } - describe('when settlement is called from Default State', async () => { + describe('when settleRebalance is called from Default State', async () => { it('should revert', async () => { await expectRevertError(subject()); }); }); - describe('when settlement is called from Proposal State', async () => { + describe('when settleRebalance is called from Proposal State', async () => { beforeEach(async () => { await rebalancingTokenWrapper.defaultTransitionToProposeAsync( rebalancingSetToken, - newRebalancingSetToken.address, + nextSetToken.address, libraryAccount, managerAccount ); @@ -1168,14 +1177,19 @@ contract('RebalancingSetToken', accounts => { }); }); - describe('when settlement is called from Rebalance State', async () => { + describe('when settleRebalance is called from Rebalance State and all currentSets are rebalanced', async () => { beforeEach(async () => { await rebalancingTokenWrapper.defaultTransitionToRebalanceAsync( rebalancingSetToken, - newRebalancingSetToken.address, + nextSetToken.address, libraryAccount, managerAccount ); + + await coreMock.bid.sendTransactionAsync( + rebalancingSetToken.address, + rebalancingSetQuantityToIssue + ); }); it('updates the rebalanceState to Default', async () => { @@ -1189,7 +1203,87 @@ contract('RebalancingSetToken', accounts => { await subject(); const newCurrentSet = await rebalancingSetToken.currentSet.callAsync(); - expect(newCurrentSet).to.equal(newRebalancingSetToken.address); + expect(newCurrentSet).to.equal(nextSetToken.address); + }); + + it('issues the nextSet to the rebalancingSetToken', async () => { + const existingBalance = await vault.balances.callAsync( + nextSetToken.address, + rebalancingSetToken.address + ); + const settlementAmounts = await rebalancingTokenWrapper.getExpectedUnitSharesAndIssueAmount( + rebalancingSetToken, + nextSetToken, + vault + ); + + await subject(); + + const expectedBalance = existingBalance.add(settlementAmounts['issueAmount']); + const newBalance = await vault.balances.callAsync(nextSetToken.address, rebalancingSetToken.address); + expect(newBalance).to.be.bignumber.equal(expectedBalance); + }); + + it('decrements component balance for the rebalancingSetToken by the correct amount', async () => { + const componentAddresses = await nextSetToken.getComponents.callAsync(); + const setNaturalUnit = await nextSetToken.naturalUnit.callAsync(); + const setComponentUnits = await nextSetToken.getUnits.callAsync(); + + const existingVaultBalances = await coreWrapper.getVaultBalancesForTokensForOwner( + componentAddresses, + vault, + rebalancingSetToken.address + ); + + const settlementAmounts = await rebalancingTokenWrapper.getExpectedUnitSharesAndIssueAmount( + rebalancingSetToken, + nextSetToken, + vault + ); + + await subject(); + + const quantityToIssue = settlementAmounts['issueAmount']; + const expectedVaultBalances: BigNumber[] = []; + setComponentUnits.forEach((component, idx) => { + const requiredQuantityToIssue = quantityToIssue.div(setNaturalUnit).mul(component); + expectedVaultBalances.push(existingVaultBalances[idx].sub(requiredQuantityToIssue)); + }); + + const newVaultBalances = await coreWrapper.getVaultBalancesForTokensForOwner( + componentAddresses, + vault, + rebalancingSetToken.address + ); + expect(JSON.stringify(newVaultBalances)).to.equal(JSON.stringify(expectedVaultBalances)); + }); + + it('updates the unitShares amount correctly', async () => { + const settlementAmounts = await rebalancingTokenWrapper.getExpectedUnitSharesAndIssueAmount( + rebalancingSetToken, + nextSetToken, + vault + ); + + await subject(); + + const newUnitShares = await rebalancingSetToken.unitShares.callAsync(); + expect(newUnitShares).to.be.bignumber.equal(settlementAmounts['unitShares']); + }); + }); + + describe('when settleRebalance is called and there are more than minimumBid amount of sets left', async () => { + beforeEach(async () => { + await rebalancingTokenWrapper.defaultTransitionToRebalanceAsync( + rebalancingSetToken, + nextSetToken.address, + libraryAccount, + managerAccount + ); + }); + + it('should revert', async () => { + await expectRevertError(subject()); }); }); }); diff --git a/test/core/vault.spec.ts b/test/core/vault.spec.ts index 847b492d9..d10242b40 100644 --- a/test/core/vault.spec.ts +++ b/test/core/vault.spec.ts @@ -393,10 +393,10 @@ contract('Vault', accounts => { subjectCaller = authorizedAccount; }); - // afterEach(async () => { - // subjectAmountsToTransfer = [DEPLOYED_TOKEN_QUANTITY, DEPLOYED_TOKEN_QUANTITY]; - // subjectCaller = authorizedAccount; - // }); + afterEach(async () => { + subjectAmountsToTransfer = [DEPLOYED_TOKEN_QUANTITY, DEPLOYED_TOKEN_QUANTITY]; + subjectCaller = authorizedAccount; + }); async function subject(): Promise { return vault.batchTransferBalance.sendTransactionAsync( diff --git a/utils/RebalancingTokenWrapper.ts b/utils/RebalancingTokenWrapper.ts index 4fa24f5d4..0d51166e1 100644 --- a/utils/RebalancingTokenWrapper.ts +++ b/utils/RebalancingTokenWrapper.ts @@ -5,7 +5,8 @@ import { CoreContract, CoreMockContract, SetTokenContract, - RebalancingSetTokenContract + RebalancingSetTokenContract, + VaultContract } from './contracts'; import { BigNumber } from 'bignumber.js'; @@ -15,6 +16,7 @@ import { ONE_DAY_IN_SECONDS, DEFAULT_UNIT_SHARES, DEFAULT_REBALANCING_NATURAL_UNIT, + UNLIMITED_ALLOWANCE_IN_BASE_UNITS } from './constants'; import { CoreWrapper } from './coreWrapper'; @@ -47,9 +49,10 @@ export class RebalancingTokenWrapper { factory: Address, transferProxy: Address, tokenCount: number, + naturalUnits: BigNumber[] = undefined, from: Address = this._tokenOwnerAddress, ): Promise { - const naturalUnit = ether(2); + let naturalUnit: BigNumber; const setTokenArray: SetTokenContract[] = []; const components = await this._erc20Wrapper.deployTokensAsync(tokenCount + 1, this._tokenOwnerAddress); @@ -57,10 +60,28 @@ export class RebalancingTokenWrapper { const indexArray = _.times(tokenCount, Number); for (const index in indexArray) { + let minDec: number; const idx = Number(index); + const decOne = await components[idx].decimals.callAsync(); + const decTwo = await components[idx + 1].decimals.callAsync(); + + // Determine minimum natural unit if not passed in + if (naturalUnits) { + naturalUnit = naturalUnits[idx]; + minDec = 18 - naturalUnit.e; + } else { + minDec = Math.min(decOne.toNumber(), decTwo.toNumber()); + naturalUnit = new BigNumber(10 ** (18 - minDec)); + } + + // Get Set component and component units const setComponents = components.slice(idx, idx + 2); const setComponentAddresses = _.map(setComponents, token => token.address); - const setComponentUnits = _.map(setComponents, () => naturalUnit.mul(idx + 1)); // Multiple of naturalUnit + const setComponentUnits: BigNumber[] = + [new BigNumber(10 ** (decOne.toNumber() - minDec)).mul(new BigNumber(idx + 1)), + new BigNumber(10 ** (decTwo.toNumber() - minDec)).mul(new BigNumber(idx + 1))]; + + // Create Set token const setToken = await this._coreWrapper.createSetTokenAsync( core, factory, @@ -68,6 +89,7 @@ export class RebalancingTokenWrapper { setComponentUnits, naturalUnit, ); + setTokenArray.push(setToken); } @@ -81,6 +103,7 @@ export class RebalancingTokenWrapper { initialSet: Address, proposalPeriod: BigNumber, ): Promise { + // Generate defualt rebalancingSetToken params const initialUnitShares = DEFAULT_UNIT_SHARES; const rebalanceInterval = ONE_DAY_IN_SECONDS; const callData = SetProtocolUtils.bufferArrayToHex([ @@ -89,6 +112,7 @@ export class RebalancingTokenWrapper { SetProtocolUtils.paddedBufferForBigNumber(rebalanceInterval), ]); + // Create rebalancingSetToken return await this._coreWrapper.createRebalancingTokenAsync( core, factory, @@ -105,10 +129,12 @@ export class RebalancingTokenWrapper { auctionLibrary: Address, caller: Address ): Promise { + // Generate default propose params const curveCoefficient = ether(1); - const auctionStartPrice = ether(5); - const auctionPriceDivisor = ether(10); + const auctionStartPrice = new BigNumber(500); + const auctionPriceDivisor = new BigNumber(1000); + // Transition to propose await this._blockchain.increaseTimeAsync(ONE_DAY_IN_SECONDS.add(1)); await rebalancingSetToken.propose.sendTransactionAsync( newRebalancingSetToken, @@ -126,6 +152,7 @@ export class RebalancingTokenWrapper { auctionLibrary: Address, caller: Address ): Promise { + // Transition to propose await this.defaultTransitionToProposeAsync( rebalancingSetToken, newRebalancingSetToken, @@ -133,27 +160,70 @@ export class RebalancingTokenWrapper { caller ); + // Transition to rebalance await this._blockchain.increaseTimeAsync(ONE_DAY_IN_SECONDS.add(1)); await rebalancingSetToken.rebalance.sendTransactionAsync( { from: caller, gas: DEFAULT_GAS } ); } - // Used to construct expected comined unit arrays made during propose calls + public async constructInflowOutflowArraysAsync( + rebalancingSetToken: RebalancingSetTokenContract, + quantity: BigNumber + ): Promise { + const inflowArray: BigNumber[] = []; + const outflowArray: BigNumber[] = []; + + // Get unit arrays + const combinedCurrentUnits = await rebalancingSetToken.getCombinedCurrentUnits.callAsync(); + const combinedRebalanceUnits = await rebalancingSetToken.getCombinedNextSetUnits.callAsync(); + + // Define price + const priceNumerator = new BigNumber(1374); + const priceDivisor = new BigNumber(1000); + + // Calculate the inflows and outflow arrays + const minimumBid = await rebalancingSetToken.minimumBid.callAsync(); + const coefficient = minimumBid.div(priceDivisor); + const effectiveQuantity = quantity.mul(priceDivisor).div(priceNumerator); + + for (let i = 0; i < combinedCurrentUnits.length; i++) { + const flow = combinedRebalanceUnits[i].mul(priceDivisor).sub(combinedCurrentUnits[i].mul(priceNumerator)); + if (flow.greaterThan(0)) { + inflowArray.push(effectiveQuantity.mul(flow).div(coefficient).round(0, 3).div(priceDivisor).round(0, 3)); + outflowArray.push(new BigNumber(0)); + } else { + outflowArray.push( + flow.mul(effectiveQuantity).div(coefficient).round(0, 3).div(priceDivisor).round(0, 3).mul(new BigNumber(-1)) + ); + inflowArray.push(new BigNumber(0)); + } + } + return {inflowArray, outflowArray}; + } + + // Used to construct expected combined unit arrays made during propose calls public async constructCombinedUnitArrayAsync( rebalancingSetToken: RebalancingSetTokenContract, - setToken: SetTokenContract, + targetSetToken: SetTokenContract, + otherSetToken: SetTokenContract ): Promise { + // Get target set tokens units and natural units of both set tokens const combinedTokenArray = await rebalancingSetToken.getCombinedTokenArray.callAsync(); - const setTokenComponents = await setToken.getComponents.callAsync(); - const setTokenUnits = await setToken.getUnits.callAsync(); - const setNaturalUnit = await setToken.naturalUnit.callAsync(); + const setTokenComponents = await targetSetToken.getComponents.callAsync(); + const setTokenUnits = await targetSetToken.getUnits.callAsync(); + const targetSetNaturalUnit = await targetSetToken.naturalUnit.callAsync(); + const otherSetNaturalUnit = await otherSetToken.naturalUnit.callAsync(); + + // Calculate minimumBidAmount + const maxNaturalUnit = Math.max(targetSetNaturalUnit.toNumber(), otherSetNaturalUnit.toNumber()); + // 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(new BigNumber(10 ** 18)).div(setNaturalUnit); + const totalTokenAmount = setTokenUnits[index].mul(maxNaturalUnit).div(targetSetNaturalUnit); combinedSetTokenUnits.push(totalTokenAmount); } else { combinedSetTokenUnits.push(new BigNumber(0)); @@ -161,4 +231,36 @@ export class RebalancingTokenWrapper { }); return combinedSetTokenUnits; } + + public async getExpectedUnitSharesAndIssueAmount( + rebalancingSetToken: RebalancingSetTokenContract, + newSet: SetTokenContract, + vault: VaultContract + ): Promise { + // Gather data needed for calculations + const totalSupply = await rebalancingSetToken.totalSupply.callAsync(); + const rebalancingNaturalUnit = await rebalancingSetToken.naturalUnit.callAsync(); + const newSetNaturalUnit = await newSet.naturalUnit.callAsync(); + const components = await newSet.getComponents.callAsync(); + const units = await newSet.getUnits.callAsync(); + + // Figure out how many new Sets can be issued from balance in Vault, if less than previously calculated + // amount, then set that to maxIssueAmount + let maxIssueAmount: BigNumber = UNLIMITED_ALLOWANCE_IN_BASE_UNITS; + for (let i = 0; i < components.length; i++) { + const componentAmount = await vault.getOwnerBalance.callAsync(components[i], rebalancingSetToken.address); + const componentIssueAmount = componentAmount.div(units[i]).round(0, 3).mul(newSetNaturalUnit); + + if (componentIssueAmount.lessThan(maxIssueAmount)) { + maxIssueAmount = componentIssueAmount; + } + } + const naturalUnitsOutstanding = totalSupply.div(rebalancingNaturalUnit); + // Calculate unitShares by finding how many natural units worth of the rebalancingSetToken have been issued + // Divide maxIssueAmount by this to find unitShares, remultiply unitShares by issued amount of rebalancing- + // SetToken in natural units to get amount of new Sets to issue + const issueAmount = maxIssueAmount.div(newSetNaturalUnit).round(0, 3).mul(newSetNaturalUnit); + const unitShares = issueAmount.div(naturalUnitsOutstanding).round(0, 3); + return {unitShares, issueAmount}; + } } diff --git a/utils/constants.ts b/utils/constants.ts index 30804e31b..156b5ea3d 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -3,8 +3,8 @@ import { ether } from '../utils/units'; export const DEFAULT_GAS = 12000000; export const DEFAULT_MOCK_TOKEN_DECIMALS = 18; -export const DEFAULT_REBALANCING_NATURAL_UNIT = new BigNumber(1); -export const DEFAULT_UNIT_SHARES = new BigNumber(1); +export const DEFAULT_REBALANCING_NATURAL_UNIT = new BigNumber(10 ** 10); +export const DEFAULT_UNIT_SHARES = new BigNumber(10 ** 10); export const DEPLOYED_TOKEN_QUANTITY: BigNumber = ether(100000000000); export const ONE: BigNumber = new BigNumber(1); export const ONE_DAY_IN_SECONDS = new BigNumber(86400); diff --git a/utils/contract_logs/rebalancingSetToken.ts b/utils/contract_logs/rebalancingSetToken.ts index 4380b5bd2..3475c0d3e 100644 --- a/utils/contract_logs/rebalancingSetToken.ts +++ b/utils/contract_logs/rebalancingSetToken.ts @@ -35,7 +35,7 @@ export function getExpectedNewManagerAddedLog( } export function getExpectedRebalanceProposedLog( - rebalancingSet: Address, + nextSet: Address, auctionLibrary: Address, proposalPeriodEndTime: BigNumber, contractAddress: Address, @@ -44,7 +44,7 @@ export function getExpectedRebalanceProposedLog( event: 'RebalanceProposed', address: contractAddress, args: { - rebalancingSet, + nextSet, auctionLibrary, proposalPeriodEndTime, }, From 3e393503d376d8a131548549584d22f6c10fbd17 Mon Sep 17 00:00:00 2001 From: Alexander Soong Date: Wed, 26 Sep 2018 13:54:20 -0700 Subject: [PATCH 2/4] Update gas limits --- package.json | 6 +++--- utils/constants.ts | 2 +- yarn.lock | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 32d49e80a..1c92c4e1b 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,8 @@ "lint-sol": "solium -d contracts/", "lint": "yarn run lint-sol && yarn run lint-ts", "clean-chain": "rm -rf blockchain && cp -r snapshots/0x-Kyber blockchain", - "chain": "yarn clean-chain && ganache-cli --db blockchain --networkId 50 --accounts 20 -l 12000000 -e 10000000000 -m 'concert load couple harbor equip island argue ramp clarify fence smart topic'", - "coverage-setup": "cp -r transpiled/artifacts/ts/* artifacts/ts/. && cp -r transpiled/test/* test/. && cp -r transpiled/types/* types/. && cp -r transpiled/utils/* utils/.", + "chain": "yarn clean-chain && ganache-cli --db blockchain --networkId 50 --accounts 20 -l 16000000 -e 10000000000 -m 'concert load couple harbor equip island argue ramp clarify fence smart topic'", + "coverage-setup": "yarn transpile && cp -r transpiled/artifacts/ts/* artifacts/ts/. && cp -r transpiled/test/* test/. && cp -r transpiled/types/* types/. && cp -r transpiled/utils/* utils/.", "coverage-cleanup": "find artifacts/ts -name \\*.js* -type f -delete && find test -name \\*.js* -type f -delete && find types -name \\*.js* -type f -delete && find utils -name \\*.js* -type f -delete", "coverage": "yarn coverage-setup && ./node_modules/.bin/solidity-coverage && yarn coverage-cleanup", "setup": "yarn clean && yarn compile && yarn generate-typings && yarn deploy:development", @@ -65,7 +65,7 @@ "ethereumjs-abi": "^0.6.4", "ethereumjs-util": "^5.1.2", "ethjs-abi": "^0.2.1", - "ganache-cli": "^6.1.7", + "ganache-cli": "^6.1.2", "import-sort-cli": "^4.2.0", "import-sort-parser-babylon": "^4.2.0", "import-sort-style-eslint": "^4.2.0", diff --git a/utils/constants.ts b/utils/constants.ts index 156b5ea3d..7ae0b872e 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -1,7 +1,7 @@ import { BigNumber } from 'bignumber.js'; import { ether } from '../utils/units'; -export const DEFAULT_GAS = 12000000; +export const DEFAULT_GAS = 16000000; export const DEFAULT_MOCK_TOKEN_DECIMALS = 18; export const DEFAULT_REBALANCING_NATURAL_UNIT = new BigNumber(10 ** 10); export const DEFAULT_UNIT_SHARES = new BigNumber(10 ** 10); diff --git a/yarn.lock b/yarn.lock index 31edbd1b8..a223abd62 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2524,7 +2524,7 @@ functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" -ganache-cli@^6.1.7: +ganache-cli@^6.1.2: version "6.1.8" resolved "https://registry.yarnpkg.com/ganache-cli/-/ganache-cli-6.1.8.tgz#49a8a331683a9652183f82ef1378d17e1814fcd3" dependencies: From ec1c502f8cd5f50f2e6d8dc11104a677cdf3ae28 Mon Sep 17 00:00:00 2001 From: Brian Weickmann Date: Thu, 27 Sep 2018 12:51:24 -0700 Subject: [PATCH 3/4] Added comments. Changed combinedUnitArray formation to be done in memory then committed to storage. Added missing test for bid quantities that are not multiples of minimum bid. --- contracts/core/RebalancingSetToken.sol | 118 ++++++++++++------ contracts/core/interfaces/ICore.sol | 2 +- .../extensions/coreRebalanceAuction.spec.ts | 12 +- utils/RebalancingTokenWrapper.ts | 19 +-- 4 files changed, 101 insertions(+), 50 deletions(-) diff --git a/contracts/core/RebalancingSetToken.sol b/contracts/core/RebalancingSetToken.sol index 307f89abe..96518365f 100644 --- a/contracts/core/RebalancingSetToken.sol +++ b/contracts/core/RebalancingSetToken.sol @@ -187,7 +187,8 @@ contract RebalancingSetToken is // Check that new proposed Set is valid Set created by Core require(ICore(ISetFactory(factory).core()).validSets(_nextSet)); - // Check that the propoosed set is a multiple of current set, or vice versa + // Check that the propoosed set natural unit is a multiple of current set natural unit, or vice versa. + // Done to make sure that when calculating token units there will are no rounding errors. uint256 currentNaturalUnit = ISetToken(currentSet).naturalUnit(); uint256 nextSetNaturalUnit = ISetToken(_nextSet).naturalUnit(); require( @@ -229,7 +230,7 @@ contract RebalancingSetToken is // Create token arrays needed for auction auctionSetUp(); - // Get core address + // Get core address from factory address core = ISetFactory(factory).core(); // Calculate remainingCurrentSets @@ -259,10 +260,10 @@ contract RebalancingSetToken is // Make sure all currentSets have been rebalanced require(remainingCurrentSets < minimumBid); - // Create ICore object + // Creating pointer to Core to Issue next set and Deposit into vault ICore core = ICore(ISetFactory(factory).core()); - // Issue nextSet + // Issue nextSet to RebalancingSetToken uint256 issueAmount; (issueAmount, unitShares) = calculateNextSetIssueQuantity(); core.issue( @@ -270,7 +271,7 @@ contract RebalancingSetToken is issueAmount ); - // Ensure allowance to transfer sets to Vault + // Ensure transfer proxy has enough spender allowance to move issued nextSet to vault ERC20Wrapper.ensureAllowance( nextSet, this, @@ -278,7 +279,7 @@ contract RebalancingSetToken is issueAmount ); - // Deposit newly created Sets in Vault + // Deposit newly created nextSets in Vault core.deposit( nextSet, issueAmount @@ -295,10 +296,10 @@ contract RebalancingSetToken is /* * Place bid during rebalance auction. Can only be called by Core. * - * @param _quantity The amount of currentSet to be rebalanced - * @return address[] Array of token addresses invovled in rebalancing - * @return uint256[] Array of amount of tokens inserted into system in bid - * @return uint256[] Array of amount of tokens taken out of system in bid + * @param _quantity The amount of currentSet to be rebalanced + * @return combinedTokenArray Array of token addresses invovled in rebalancing + * @return inflowUnitArray Array of amount of tokens inserted into system in bid + * @return outflowUnitArray Array of amount of tokens taken out of system in bid */ function placeBid( uint256 _quantity @@ -333,9 +334,8 @@ contract RebalancingSetToken is * Sets that would be generated. * * @param _quantity The amount of currentSet to be rebalanced - * @return uint256[] Array of amount of tokens inserted into system in bid - * @return uint256[] Array of amount of tokens taken out of system in bid - * @return uint256 Amount of nextSets traded into + * @return inflowUnitArray Array of amount of tokens inserted into system in bid + * @return outflowUnitArray Array of amount of tokens taken out of system in bid */ function getBidPrice( uint256 _quantity @@ -351,26 +351,57 @@ contract RebalancingSetToken is uint256[] memory inflowUnitArray = new uint256[](combinedTokenArray.length); uint256[] memory outflowUnitArray = new uint256[](combinedTokenArray.length); - // Get bid conversion price + // Get bid conversion price, currently static placeholder for calling auctionlibrary uint256 priceNumerator = 1374; // Normalized quantity amount uint256 unitsMultiplier = _quantity.div(minimumBid).mul(auctionPriceDivisor); - for (uint256 i=0; i < combinedTokenArray.length; i++) { + for (uint256 i = 0; i < combinedTokenArray.length; i++) { uint256 nextUnit = combinedNextSetUnits[i]; uint256 currentUnit = combinedCurrentUnits[i]; - // If rebalance greater than currentUnit*price token inflow, else token outflow - if (nextUnit > currentUnit.mul(priceNumerator).div(auctionPriceDivisor)) { + /* + * Below is a mathematically simplified formula for calculating token inflows and + * outflows, the following is it's derivation: + * token_flow = (bidQuantity/price)*(nextUnit - price*currentUnit) + * + * Where, + * 1) price = (priceNumerator/auctionPriceDivisor), + * 2) nextUnit and currentUnit are the amount of component i needed for a + * standardAmount of sets to be rebalanced where one standardAmount = + * max(natural unit nextSet, natural unit currentSet), and + * 3) bidQuantity is a normalized amount in terms of the standardAmount used + * to calculate nextUnit and currentUnit. This is represented by the unitsMultiplier + * variable. + * + * Given these definitions we can derive the below formula as follows: + * token_flow = (unitsMultiplier/(priceNumerator/auctionPriceDivisor))* + * (nextUnit - (priceNumerator/auctionPriceDivisor)*currentUnit) + * + * We can then multiply this equation by (auctionPriceDivisor/auctionPriceDivisor) + * which simplifies the above equation to: + * + * (unitsMultiplier/priceNumerator)* (nextUnit*auctionPriceDivisor - currentUnit*priceNumerator) + * + * This is the equation seen below, but since unsigned integers are used we must check to see if + * nextUnit*auctionPriceDivisor > currentUnit*priceNumerator, otherwise those two terms must be + * flipped in the equation. + */ + if (nextUnit.mul(auctionPriceDivisor) > currentUnit.mul(priceNumerator)) { inflowUnitArray[i] = unitsMultiplier.mul( nextUnit.mul(auctionPriceDivisor).sub(currentUnit.mul(priceNumerator)) ).div(priceNumerator); + + // Set outflow amount to 0 for component i outflowUnitArray[i] = 0; } else { + // Calculate outflow amount outflowUnitArray[i] = unitsMultiplier.mul( currentUnit.mul(priceNumerator).sub(nextUnit.mul(auctionPriceDivisor)) ).div(priceNumerator); + + // Set inflow amount to 0 for component i inflowUnitArray[i] = 0; } } @@ -379,7 +410,7 @@ contract RebalancingSetToken is /* * Mint set token for given address. - * Can only be called by authorized contracts. + * Can only be called by Core contract. * * @param _issuer The address of the issuing account * @param _quantity The number of sets to attribute to issuer @@ -546,24 +577,29 @@ contract RebalancingSetToken is * unit of the two sets. */ function auctionSetUp() - internal + private { // Create interfaces for interacting with sets - ISetToken currentSetInterface = ISetToken(currentSet); - ISetToken nextSetInterface = ISetToken(nextSet); + ISetToken currentSetInstance = ISetToken(currentSet); + ISetToken nextSetInstance = ISetToken(nextSet); // Create combined token Array - address[] memory oldComponents = currentSetInterface.getComponents(); - address[] memory newComponents = nextSetInterface.getComponents(); + address[] memory oldComponents = currentSetInstance.getComponents(); + address[] memory newComponents = nextSetInstance.getComponents(); combinedTokenArray = oldComponents.union(newComponents); // Get naturalUnit of both sets - uint256 currentSetNaturalUnit = currentSetInterface.naturalUnit(); - uint256 nextSetNaturalUnit = nextSetInterface.naturalUnit(); + uint256 currentSetNaturalUnit = currentSetInstance.naturalUnit(); + uint256 nextSetNaturalUnit = nextSetInstance.naturalUnit(); // Get units arrays for both sets - uint256[] memory currentSetUnits = currentSetInterface.getUnits(); - uint256[] memory nextSetUnits = nextSetInterface.getUnits(); + uint256[] memory currentSetUnits = currentSetInstance.getUnits(); + uint256[] memory nextSetUnits = nextSetInstance.getUnits(); + + // Create memory version of combinedNextSetUnits and combinedCurrentUnits to only make one + // call to storage once arrays have been created + uint256[] memory memoryCombinedCurrentUnits = new uint256[](combinedTokenArray.length); + uint256[] memory memoryCombinedNextSetUnits = new uint256[](combinedTokenArray.length); minimumBid = Math.max256( currentSetNaturalUnit.mul(auctionPriceDivisor), @@ -573,26 +609,26 @@ contract RebalancingSetToken is for (uint256 i=0; i < combinedTokenArray.length; i++) { // Check if component in arrays and get index if it is (uint256 indexCurrent, bool isInCurrent) = oldComponents.indexOf(combinedTokenArray[i]); - (uint256 indexRebalance, bool isInRebalance) = newComponents.indexOf(combinedTokenArray[i]); + (uint256 indexRebalance, bool isInNext) = newComponents.indexOf(combinedTokenArray[i]); // Compute and push unit amounts of token in currentSet, push 0 if not in set if (isInCurrent) { - combinedCurrentUnits.push( - computeUnits(currentSetUnits[indexCurrent], currentSetNaturalUnit) - ); + memoryCombinedCurrentUnits[i] = computeUnits(currentSetUnits[indexCurrent], currentSetNaturalUnit); } else { - combinedCurrentUnits.push(uint256(0)); + memoryCombinedCurrentUnits[i] = uint256(0); } // Compute and push unit amounts of token in nextSet, push 0 if not in set - if (isInRebalance) { - combinedNextSetUnits.push( - computeUnits(nextSetUnits[indexRebalance], nextSetNaturalUnit) - ); + if (isInNext) { + memoryCombinedNextSetUnits[i] = computeUnits(nextSetUnits[indexRebalance], nextSetNaturalUnit); } else { - combinedNextSetUnits.push(uint256(0)); + memoryCombinedNextSetUnits[i] = uint256(0); } } + + // Set combinedCurrentUnits and combinedNextSetUnits to memory versions of arrays + combinedCurrentUnits = memoryCombinedCurrentUnits; + combinedNextSetUnits = memoryCombinedNextSetUnits; } /** @@ -602,7 +638,7 @@ contract RebalancingSetToken is * @return uint256 New unitShares for the rebalancingSetToken */ function calculateNextSetIssueQuantity() - internal + private returns (uint256, uint256) { // Collect data necessary to compute issueAmounts @@ -634,6 +670,8 @@ contract RebalancingSetToken is uint256 naturalUnitsOutstanding = totalSupply_.div(naturalUnit); // Issue amount of Sets that is closest multiple of nextNaturalUnit to the maxIssueAmount + // Since the initial division will round down to the nearest whole number when we multiply + // by that same number we will return the closest multiple less than the maxIssueAmount uint256 issueAmount = maxIssueAmount.div(nextNaturalUnit).mul(nextNaturalUnit); // Divide final issueAmount by naturalUnitsOutstanding to get newUnitShares @@ -641,17 +679,19 @@ contract RebalancingSetToken is return (issueAmount, newUnitShares); } + /** * Function to calculate the transfer value of a component given 1 Set * * @param _unit The units of the component token * @param _naturalUnit The natural unit of the Set token + * @return uint256 Amount of tokens per minimumBid/auctionPriceDivisor */ function computeUnits( uint256 _unit, uint256 _naturalUnit ) - internal + private returns (uint256) { return minimumBid.mul(_unit).div(_naturalUnit).div(auctionPriceDivisor); diff --git a/contracts/core/interfaces/ICore.sol b/contracts/core/interfaces/ICore.sol index 97ea87497..6e663d26a 100644 --- a/contracts/core/interfaces/ICore.sol +++ b/contracts/core/interfaces/ICore.sol @@ -48,7 +48,7 @@ interface ICore { /* * Returns if valid set * - * @return bool If valid set + * @return bool Returns true if Set created through Core and isn't disabled */ function validSets(address) external diff --git a/test/core/extensions/coreRebalanceAuction.spec.ts b/test/core/extensions/coreRebalanceAuction.spec.ts index ef48019e8..723b6faa3 100644 --- a/test/core/extensions/coreRebalanceAuction.spec.ts +++ b/test/core/extensions/coreRebalanceAuction.spec.ts @@ -36,7 +36,6 @@ const { expect } = chai; const blockchain = new Blockchain(web3); - contract('CoreRebalanceAuction', accounts => { const [ deployerAccount, @@ -421,6 +420,17 @@ contract('CoreRebalanceAuction', accounts => { await expectRevertError(subject()); }); }); + + describe('and quantity is not a multiple of minimumBid', async () => { + beforeEach(async () => { + const minimumBid = await rebalancingSetToken.minimumBid.callAsync(); + subjectQuantity = minimumBid.mul(new BigNumber(1.5)); + }); + + it('should revert', async () => { + await expectRevertError(subject()); + }); + }); }); }); diff --git a/utils/RebalancingTokenWrapper.ts b/utils/RebalancingTokenWrapper.ts index 0d51166e1..f85ea5329 100644 --- a/utils/RebalancingTokenWrapper.ts +++ b/utils/RebalancingTokenWrapper.ts @@ -60,26 +60,27 @@ export class RebalancingTokenWrapper { const indexArray = _.times(tokenCount, Number); for (const index in indexArray) { - let minDec: number; + let minimumDecimal: number; const idx = Number(index); - const decOne = await components[idx].decimals.callAsync(); - const decTwo = await components[idx + 1].decimals.callAsync(); + const componentOneDecimal = await components[idx].decimals.callAsync(); + const componentTwoDecimal = await components[idx + 1].decimals.callAsync(); // Determine minimum natural unit if not passed in if (naturalUnits) { naturalUnit = naturalUnits[idx]; - minDec = 18 - naturalUnit.e; + minimumDecimal = 18 - naturalUnit.e; } else { - minDec = Math.min(decOne.toNumber(), decTwo.toNumber()); - naturalUnit = new BigNumber(10 ** (18 - minDec)); + minimumDecimal = Math.min(componentOneDecimal.toNumber(), componentTwoDecimal.toNumber()); + naturalUnit = new BigNumber(10 ** (18 - minimumDecimal)); } // Get Set component and component units const setComponents = components.slice(idx, idx + 2); const setComponentAddresses = _.map(setComponents, token => token.address); - const setComponentUnits: BigNumber[] = - [new BigNumber(10 ** (decOne.toNumber() - minDec)).mul(new BigNumber(idx + 1)), - new BigNumber(10 ** (decTwo.toNumber() - minDec)).mul(new BigNumber(idx + 1))]; + const setComponentUnits: BigNumber[] = [ + new BigNumber(10 ** (componentOneDecimal.toNumber() - minimumDecimal)).mul(new BigNumber(idx + 1)), + new BigNumber(10 ** (componentTwoDecimal.toNumber() - minimumDecimal)).mul(new BigNumber(idx + 1)), + ]; // Create Set token const setToken = await this._coreWrapper.createSetTokenAsync( From 6f3e52f65c1be8ee3c52c3ca533c20031acfc5e5 Mon Sep 17 00:00:00 2001 From: Brian Weickmann Date: Thu, 27 Sep 2018 15:15:50 -0700 Subject: [PATCH 4/4] Updated some comments. --- contracts/core/RebalancingSetToken.sol | 4 ++-- test/core/rebalancingSetToken.spec.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/core/RebalancingSetToken.sol b/contracts/core/RebalancingSetToken.sol index 96518365f..cc7bc975f 100644 --- a/contracts/core/RebalancingSetToken.sol +++ b/contracts/core/RebalancingSetToken.sol @@ -393,7 +393,7 @@ contract RebalancingSetToken is nextUnit.mul(auctionPriceDivisor).sub(currentUnit.mul(priceNumerator)) ).div(priceNumerator); - // Set outflow amount to 0 for component i + // Set outflow amount to 0 for component i, since tokens need to be injected in rebalance outflowUnitArray[i] = 0; } else { // Calculate outflow amount @@ -401,7 +401,7 @@ contract RebalancingSetToken is currentUnit.mul(priceNumerator).sub(nextUnit.mul(auctionPriceDivisor)) ).div(priceNumerator); - // Set inflow amount to 0 for component i + // Set inflow amount to 0 for component i, since tokens need to be returned in rebalance inflowUnitArray[i] = 0; } } diff --git a/test/core/rebalancingSetToken.spec.ts b/test/core/rebalancingSetToken.spec.ts index 7d61aa921..143fbbde8 100644 --- a/test/core/rebalancingSetToken.spec.ts +++ b/test/core/rebalancingSetToken.spec.ts @@ -850,6 +850,7 @@ contract('RebalancingSetToken', accounts => { describe("but the new proposed set's natural unit is not a multiple of the current set", async () => { before(async () => { + // a setToken with natural unit ether(.003) and setToken with natural unit ether(.002) are being used naturalUnits = [ether(.003), ether(.002), ether(.001)]; });