diff --git a/contracts/core/interfaces/IRebalancingSetFactory.sol b/contracts/core/interfaces/IRebalancingSetFactory.sol index 910329483..4599eb6f4 100644 --- a/contracts/core/interfaces/IRebalancingSetFactory.sol +++ b/contracts/core/interfaces/IRebalancingSetFactory.sol @@ -17,6 +17,7 @@ pragma solidity 0.4.25; import { ISetFactory } from "./ISetFactory.sol"; +import { IWhiteList } from "./IWhiteList.sol"; /** diff --git a/contracts/core/interfaces/IWhiteList.sol b/contracts/core/interfaces/IWhiteList.sol new file mode 100644 index 000000000..fbf2cdab3 --- /dev/null +++ b/contracts/core/interfaces/IWhiteList.sol @@ -0,0 +1,54 @@ +/* + 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; + +/** + * @title IWhiteList + * @author Set Protocol + * + * The IWhiteList interface exposes the whitelist mapping to check components + */ +interface IWhiteList { + + /* ============ External Functions ============ */ + + /** + * Validates address against white list + * + * @param _address Address to check + * @return bool Whether passed in address is whitelisted + */ + function whiteList( + address _address + ) + external + view + returns(bool); + + /** + * Verifies an array of addresses against the whitelist + * + * @param _addresses Array of addresses to verify + * @return bool Whether all addresses in the list are whitelsited + */ + function areValidAddresses( + address[] _addresses + ) + external + view + returns(bool); +} diff --git a/contracts/core/tokens/RebalancingSetToken.sol b/contracts/core/tokens/RebalancingSetToken.sol index 82ac34a45..f122614ee 100644 --- a/contracts/core/tokens/RebalancingSetToken.sol +++ b/contracts/core/tokens/RebalancingSetToken.sol @@ -30,6 +30,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 { IWhiteList } from "../interfaces/IWhiteList.sol"; import { RebalancingHelperLibrary } from "../lib/RebalancingHelperLibrary.sol"; import { StandardFailAuctionLibrary } from "./rebalancing-libraries/StandardFailAuctionLibrary.sol"; import { StandardPlaceBidLibrary } from "./rebalancing-libraries/StandardPlaceBidLibrary.sol"; @@ -58,7 +59,6 @@ contract RebalancingSetToken is uint256 constant MIN_AUCTION_TIME_TO_PIVOT = 21600; uint256 constant MAX_AUCTION_TIME_TO_PIVOT = 259200; - /* ============ Enums ============ */ enum State { Default, Proposal, Rebalance } @@ -73,6 +73,7 @@ contract RebalancingSetToken is // Core and Vault instances ICore private coreInstance; IVault private vaultInstance; + IWhiteList private componentWhiteListInstance; // All rebalancingSetTokens have same natural unit, still allows for // small amounts to be issued and attempts to reduce slippage as much @@ -129,6 +130,7 @@ contract RebalancingSetToken is * @param _initialUnitShares Units of currentSet that equals one share * @param _proposalPeriod Amount of time for users to inspect a rebalance proposal * @param _rebalanceInterval Minimum amount of time between rebalances + * @param _componentWhiteList Address of component WhiteList contract * @param _name The name of the new RebalancingSetToken * @param _symbol The symbol of the new RebalancingSetToken */ @@ -140,6 +142,7 @@ contract RebalancingSetToken is uint256 _initialUnitShares, uint256 _proposalPeriod, uint256 _rebalanceInterval, + address _componentWhiteList, string _name, string _symbol ) @@ -177,6 +180,7 @@ contract RebalancingSetToken is coreInstance = ICore(core); vault = coreInstance.vault(); vaultInstance = IVault(vault); + componentWhiteListInstance = IWhiteList(_componentWhiteList); factory = _factory; manager = _manager; currentSet = _initialSet; @@ -226,6 +230,7 @@ contract RebalancingSetToken is _auctionTimeToPivot, _auctionStartPrice, _auctionPivotPrice, + componentWhiteListInstance, proposeParameters ); diff --git a/contracts/core/tokens/RebalancingSetTokenFactory.sol b/contracts/core/tokens/RebalancingSetTokenFactory.sol index 45d2f4e66..80f1265be 100644 --- a/contracts/core/tokens/RebalancingSetTokenFactory.sol +++ b/contracts/core/tokens/RebalancingSetTokenFactory.sol @@ -22,6 +22,7 @@ import { Bytes32 } from "../../lib/Bytes32.sol"; import { ICore } from "../interfaces/ICore.sol"; import { LibBytes } from "../../external/0x/LibBytes.sol"; import { RebalancingSetToken } from "./RebalancingSetToken.sol"; +import { IWhiteList } from "../interfaces/IWhiteList.sol"; /** @@ -41,6 +42,9 @@ contract RebalancingSetTokenFactory { // Address of the Core contract used to verify factory when creating a Set address public core; + // Address of the WhiteList contract used to verify the tokens in a rebalance proposal + address public rebalanceComponentWhitelist; + // Minimum amount of time between rebalances in seconds uint256 public minimumRebalanceInterval; @@ -62,17 +66,20 @@ contract RebalancingSetTokenFactory { * on RebalancingSetToken * * @param _core Address of deployed core contract + * @param _componentWhitelist Address of deployed whitelist contract * @param _minimumRebalanceInterval Minimum amount of time between rebalances in seconds * @param _minimumProposalPeriod Minimum amount of time users can review proposals in seconds */ constructor( address _core, + address _componentWhitelist, uint256 _minimumRebalanceInterval, uint256 _minimumProposalPeriod ) public { core = _core; + rebalanceComponentWhitelist = IWhiteList(_componentWhitelist); minimumRebalanceInterval = _minimumRebalanceInterval; minimumProposalPeriod = _minimumProposalPeriod; } @@ -151,6 +158,7 @@ contract RebalancingSetTokenFactory { _units[0], parameters.proposalPeriod, parameters.rebalanceInterval, + rebalanceComponentWhitelist, _name.bytes32ToString(), _symbol.bytes32ToString() ); diff --git a/contracts/core/tokens/rebalancing-libraries/StandardProposeLibrary.sol b/contracts/core/tokens/rebalancing-libraries/StandardProposeLibrary.sol index 7ea26e742..1e1a98565 100644 --- a/contracts/core/tokens/rebalancing-libraries/StandardProposeLibrary.sol +++ b/contracts/core/tokens/rebalancing-libraries/StandardProposeLibrary.sol @@ -22,6 +22,7 @@ import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; import { IAuctionPriceCurve } from "../../lib/auction-price-libraries/IAuctionPriceCurve.sol"; import { ICore } from "../../interfaces/ICore.sol"; import { ISetToken } from "../../interfaces/ISetToken.sol"; +import { IWhiteList } from "../../interfaces/IWhiteList.sol"; import { RebalancingHelperLibrary } from "../../lib/RebalancingHelperLibrary.sol"; @@ -35,10 +36,12 @@ library StandardProposeLibrary { using SafeMath for uint256; /* ============ Constants ============ */ + uint256 constant MIN_AUCTION_TIME_TO_PIVOT = 21600; uint256 constant MAX_AUCTION_TIME_TO_PIVOT = 259200; /* ============ Structs ============ */ + struct ProposeAuctionParameters { address manager; address currentSet; @@ -58,6 +61,7 @@ library StandardProposeLibrary { * @param _auctionTimeToPivot The amount of time for the auction to go ffrom start to pivot price * @param _auctionStartPrice The price to start the auction at * @param _auctionPivotPrice The price at which the price curve switches from linear to exponential + * @param _componentWhiteList Instance of component WhiteList to verify * @param _proposeParameters Rebalancing Set Token state parameters needed to execute logic * @return Struct containing auction price curve parameters */ @@ -67,6 +71,7 @@ library StandardProposeLibrary { uint256 _auctionTimeToPivot, uint256 _auctionStartPrice, uint256 _auctionPivotPrice, + IWhiteList _componentWhiteList, ProposeAuctionParameters memory _proposeParameters ) internal @@ -99,6 +104,13 @@ library StandardProposeLibrary { "RebalancingSetToken.propose: Invalid or disabled proposed SetToken address" ); + // Check proposed components on whitelist. This is to ensure managers are unable to add contract addresses + // to a propose that prohibit the set from carrying out an auction i.e. a token that only the manager possesses + require( + _componentWhiteList.areValidAddresses(ISetToken(_nextSet).getComponents()), + "RebalancingSetToken.propose: Proposed set contains invalid component token" + ); + // Check that the auction library is a valid priceLibrary tracked by Core require( _proposeParameters.coreInstance.validPriceLibraries(_auctionLibrary), diff --git a/contracts/lib/WhiteList.sol b/contracts/lib/WhiteList.sol index 9ccf20cd5..d431a7615 100644 --- a/contracts/lib/WhiteList.sol +++ b/contracts/lib/WhiteList.sol @@ -139,4 +139,26 @@ contract WhiteList is { return addresses; } -} \ No newline at end of file + + /** + * Verifies an array of addresses against the whitelist + * + * @param _addresses Array of addresses to verify + * @return bool Whether all addresses in the list are whitelsited + */ + function areValidAddresses( + address[] _addresses + ) + external + view + returns(bool) + { + for (uint256 i = 0; i < _addresses.length; i++) { + if (!whiteList[_addresses[i]]) { + return false; + } + } + + return true; + } +} diff --git a/test/contracts/core/extensions/coreIssuance.spec.ts b/test/contracts/core/extensions/coreIssuance.spec.ts index 0a4ecc50b..f93a3b475 100644 --- a/test/contracts/core/extensions/coreIssuance.spec.ts +++ b/test/contracts/core/extensions/coreIssuance.spec.ts @@ -18,7 +18,7 @@ import { SignatureValidatorContract, StandardTokenMockContract, TransferProxyContract, - VaultContract + VaultContract, } from '@utils/contracts'; import { ether } from '@utils/units'; import { assertTokenBalanceAsync, expectRevertError } from '@utils/tokenAssertions'; @@ -59,7 +59,6 @@ contract('CoreIssuance', accounts => { let transferProxy: TransferProxyContract; let vault: VaultContract; let setTokenFactory: SetTokenFactoryContract; - let rebalancingTokenFactory: RebalancingSetTokenFactoryContract; let signatureValidator: SignatureValidatorContract; const coreWrapper = new CoreWrapper(ownerAccount, ownerAccount); @@ -87,11 +86,7 @@ contract('CoreIssuance', accounts => { signatureValidator = await coreWrapper.deploySignatureValidatorAsync(); core = await coreWrapper.deployCoreAsync(transferProxy, vault, signatureValidator); setTokenFactory = await coreWrapper.deploySetTokenFactoryAsync(core.address); - rebalancingTokenFactory = await coreWrapper.deployRebalancingSetTokenFactoryAsync( - core.address, - ); await coreWrapper.setDefaultStateAndAuthorizationsAsync(core, vault, transferProxy, setTokenFactory); - await coreWrapper.addFactoryAsync(core, rebalancingTokenFactory); }); afterEach(async () => { @@ -363,6 +358,8 @@ contract('CoreIssuance', accounts => { let subjectQuantityToIssue: BigNumber; let subjectSetToIssue: Address; + let rebalancingTokenFactory: RebalancingSetTokenFactoryContract; + let vanillaQuantityToIssue: BigNumber; let vanillaSetToIssue: Address; @@ -373,6 +370,13 @@ contract('CoreIssuance', accounts => { let rebalancingSetToken: RebalancingSetTokenContract; beforeEach(async () => { + const rebalancingComponentWhiteList = await coreWrapper.deployWhiteListAsync(); + rebalancingTokenFactory = await coreWrapper.deployRebalancingSetTokenFactoryAsync( + core.address, + rebalancingComponentWhiteList.address + ); + await coreWrapper.addFactoryAsync(core, rebalancingTokenFactory); + const setTokens = await rebalancingTokenWrapper.createSetTokensAsync( core, setTokenFactory.address, @@ -865,6 +869,8 @@ contract('CoreIssuance', accounts => { let subjectQuantityToRedeem: BigNumber; let subjectSetToRedeem: Address; + let rebalancingTokenFactory: RebalancingSetTokenFactoryContract; + let vanillaQuantityToIssue: BigNumber; let vanillaSetToIssue: Address; @@ -879,6 +885,13 @@ contract('CoreIssuance', accounts => { let rebalancingToken: RebalancingSetTokenContract; beforeEach(async () => { + const rebalancingComponentWhiteList = await coreWrapper.deployWhiteListAsync(); + rebalancingTokenFactory = await coreWrapper.deployRebalancingSetTokenFactoryAsync( + core.address, + rebalancingComponentWhiteList.address + ); + await coreWrapper.addFactoryAsync(core, rebalancingTokenFactory); + const setTokens = await rebalancingTokenWrapper.createSetTokensAsync( core, setTokenFactory.address, diff --git a/test/contracts/core/modules/rebalanceAuctionModule.spec.ts b/test/contracts/core/modules/rebalanceAuctionModule.spec.ts index a74688480..bb0fcc85a 100644 --- a/test/contracts/core/modules/rebalanceAuctionModule.spec.ts +++ b/test/contracts/core/modules/rebalanceAuctionModule.spec.ts @@ -20,6 +20,7 @@ import { SignatureValidatorContract, TransferProxyContract, VaultContract, + WhiteListContract, } from '@utils/contracts'; import { ether } from '@utils/units'; import { @@ -65,6 +66,7 @@ contract('RebalanceAuctionModule', accounts => { let signatureValidator: SignatureValidatorContract; let rebalanceAuctionModuleMock: RebalanceAuctionModuleMockContract; let factory: SetTokenFactoryContract; + let rebalancingComponentWhiteList: WhiteListContract; let rebalancingFactory: RebalancingSetTokenFactoryContract; let constantAuctionPriceCurve: ConstantAuctionPriceCurveContract; @@ -98,8 +100,10 @@ contract('RebalanceAuctionModule', accounts => { await coreWrapper.addModuleAsync(coreMock, rebalanceAuctionModuleMock.address); factory = await coreWrapper.deploySetTokenFactoryAsync(coreMock.address); + rebalancingComponentWhiteList = await coreWrapper.deployWhiteListAsync(); rebalancingFactory = await coreWrapper.deployRebalancingSetTokenFactoryAsync( coreMock.address, + rebalancingComponentWhiteList.address, ); constantAuctionPriceCurve = await rebalancingWrapper.deployConstantAuctionPriceCurveAsync( DEFAULT_AUCTION_PRICE_NUMERATOR, @@ -194,8 +198,9 @@ contract('RebalanceAuctionModule', accounts => { beforeEach(async () => { await rebalancingWrapper.defaultTransitionToProposeAsync( coreMock, + rebalancingComponentWhiteList, rebalancingSetToken, - nextSetToken.address, + nextSetToken, constantAuctionPriceCurve.address, managerAccount ); @@ -210,8 +215,9 @@ contract('RebalanceAuctionModule', accounts => { beforeEach(async () => { await rebalancingWrapper.defaultTransitionToRebalanceAsync( coreMock, + rebalancingComponentWhiteList, rebalancingSetToken, - nextSetToken.address, + nextSetToken, constantAuctionPriceCurve.address, managerAccount ); @@ -347,8 +353,9 @@ contract('RebalanceAuctionModule', accounts => { beforeEach(async () => { await rebalancingWrapper.defaultTransitionToProposeAsync( coreMock, + rebalancingComponentWhiteList, rebalancingSetToken, - nextSetToken.address, + nextSetToken, constantAuctionPriceCurve.address, managerAccount ); @@ -363,8 +370,9 @@ contract('RebalanceAuctionModule', accounts => { beforeEach(async () => { await rebalancingWrapper.defaultTransitionToRebalanceAsync( coreMock, + rebalancingComponentWhiteList, rebalancingSetToken, - nextSetToken.address, + nextSetToken, constantAuctionPriceCurve.address, managerAccount ); @@ -440,8 +448,9 @@ contract('RebalanceAuctionModule', accounts => { beforeEach(async () => { await rebalancingWrapper.defaultTransitionToProposeAsync( coreMock, + rebalancingComponentWhiteList, rebalancingSetToken, - nextSetToken.address, + nextSetToken, constantAuctionPriceCurve.address, managerAccount ); @@ -456,8 +465,9 @@ contract('RebalanceAuctionModule', accounts => { beforeEach(async () => { await rebalancingWrapper.defaultTransitionToRebalanceAsync( coreMock, + rebalancingComponentWhiteList, rebalancingSetToken, - nextSetToken.address, + nextSetToken, constantAuctionPriceCurve.address, managerAccount ); @@ -504,8 +514,9 @@ contract('RebalanceAuctionModule', accounts => { beforeEach(async () => { await rebalancingWrapper.defaultTransitionToRebalanceAsync( coreMock, + rebalancingComponentWhiteList, rebalancingSetToken, - nextSetToken.address, + nextSetToken, constantAuctionPriceCurve.address, managerAccount ); @@ -570,8 +581,9 @@ contract('RebalanceAuctionModule', accounts => { beforeEach(async () => { await rebalancingWrapper.defaultTransitionToRebalanceAsync( coreMock, + rebalancingComponentWhiteList, rebalancingSetToken, - nextSetToken.address, + nextSetToken, constantAuctionPriceCurve.address, managerAccount ); @@ -654,8 +666,9 @@ contract('RebalanceAuctionModule', accounts => { beforeEach(async () => { await rebalancingWrapper.defaultTransitionToProposeAsync( coreMock, + rebalancingComponentWhiteList, rebalancingSetToken, - nextSetToken.address, + nextSetToken, constantAuctionPriceCurve.address, managerAccount ); @@ -670,8 +683,9 @@ contract('RebalanceAuctionModule', accounts => { beforeEach(async () => { await rebalancingWrapper.defaultTransitionToRebalanceAsync( coreMock, + rebalancingComponentWhiteList, rebalancingSetToken, - nextSetToken.address, + nextSetToken, constantAuctionPriceCurve.address, managerAccount ); @@ -688,8 +702,9 @@ contract('RebalanceAuctionModule', accounts => { beforeEach(async () => { await rebalancingWrapper.defaultTransitionToRebalanceAsync( coreMock, + rebalancingComponentWhiteList, rebalancingSetToken, - nextSetToken.address, + nextSetToken, constantAuctionPriceCurve.address, managerAccount ); diff --git a/test/contracts/core/tokens/rebalancingSetToken.spec.ts b/test/contracts/core/tokens/rebalancingSetToken.spec.ts index 765468378..53a6078ee 100644 --- a/test/contracts/core/tokens/rebalancingSetToken.spec.ts +++ b/test/contracts/core/tokens/rebalancingSetToken.spec.ts @@ -21,6 +21,7 @@ import { StandardTokenMockContract, TransferProxyContract, VaultContract, + WhiteListContract, } from '@utils/contracts'; import { Blockchain } from '@utils/blockchain'; import { ether } from '@utils/units'; @@ -77,6 +78,7 @@ contract('RebalancingSetToken', accounts => { let signatureValidator: SignatureValidatorContract; let factory: SetTokenFactoryContract; let rebalancingFactory: RebalancingSetTokenFactoryContract; + let rebalancingComponentWhiteList: WhiteListContract; let constantAuctionPriceCurve: ConstantAuctionPriceCurveContract; const coreWrapper = new CoreWrapper(deployerAccount, deployerAccount); @@ -109,8 +111,10 @@ contract('RebalancingSetToken', accounts => { await coreWrapper.addModuleAsync(coreMock, rebalanceAuctionModule.address); factory = await coreWrapper.deploySetTokenFactoryAsync(coreMock.address); + rebalancingComponentWhiteList = await coreWrapper.deployWhiteListAsync(); rebalancingFactory = await coreWrapper.deployRebalancingSetTokenFactoryAsync( coreMock.address, + rebalancingComponentWhiteList.address, ); constantAuctionPriceCurve = await rebalancingWrapper.deployConstantAuctionPriceCurveAsync( DEFAULT_AUCTION_PRICE_NUMERATOR, @@ -133,6 +137,7 @@ contract('RebalancingSetToken', accounts => { let subjectInitialUnitShares: BigNumber; let subjectProposalPeriod: BigNumber; let subjectRebalanceInterval: BigNumber; + let subjectComponentWhiteList: Address; const subjectName: string = 'Rebalancing Set'; const subjectSymbol: string = 'RBSET'; @@ -145,6 +150,7 @@ contract('RebalancingSetToken', accounts => { subjectInitialUnitShares = DEFAULT_UNIT_SHARES; subjectProposalPeriod = ONE_DAY_IN_SECONDS; subjectRebalanceInterval = ONE_DAY_IN_SECONDS; + subjectComponentWhiteList = rebalancingComponentWhiteList.address; }); async function subject(): Promise { @@ -155,6 +161,7 @@ contract('RebalancingSetToken', accounts => { subjectInitialUnitShares, subjectProposalPeriod, subjectRebalanceInterval, + subjectComponentWhiteList, subjectName, subjectSymbol, ); @@ -284,6 +291,7 @@ contract('RebalancingSetToken', accounts => { initialUnitShares, proposalPeriod, rebalanceInterval, + rebalancingComponentWhiteList.address, ); subjectCaller = managerAccount; @@ -322,6 +330,7 @@ contract('RebalancingSetToken', accounts => { initialUnitShares, proposalPeriod, rebalanceInterval, + rebalancingComponentWhiteList.address, ); subjectCaller = managerAccount; @@ -362,6 +371,7 @@ contract('RebalancingSetToken', accounts => { initialUnitShares, proposalPeriod, rebalanceInterval, + rebalancingComponentWhiteList.address, ); subjectCaller = managerAccount; @@ -419,8 +429,10 @@ contract('RebalancingSetToken', accounts => { const proposalPeriod = ONE_DAY_IN_SECONDS; const rebalanceInterval = ONE_DAY_IN_SECONDS; + const rebalancingComponentWhiteList = await coreWrapper.deployWhiteListAsync(); const rebalancingFactory = await coreWrapper.deployRebalancingSetTokenFactoryAsync( coreMock.address, + rebalancingComponentWhiteList.address, ); await coreWrapper.addFactoryAsync(coreMock, rebalancingFactory); @@ -431,6 +443,7 @@ contract('RebalancingSetToken', accounts => { initialUnitShares, proposalPeriod, rebalanceInterval, + rebalancingComponentWhiteList.address, ); subjectIssuer = deployerAccount, @@ -532,8 +545,9 @@ contract('RebalancingSetToken', accounts => { beforeEach(async () => { await rebalancingWrapper.defaultTransitionToRebalanceAsync( coreMock, + rebalancingComponentWhiteList, rebalancingSetToken, - nextSetToken.address, + nextSetToken, constantAuctionPriceCurve.address, managerAccount ); @@ -556,8 +570,9 @@ contract('RebalancingSetToken', accounts => { await rebalancingWrapper.defaultTransitionToRebalanceAsync( coreMock, + rebalancingComponentWhiteList, rebalancingSetToken, - nextSetToken.address, + nextSetToken, constantAuctionPriceCurve.address, managerAccount ); @@ -690,8 +705,9 @@ contract('RebalancingSetToken', accounts => { beforeEach(async () => { await rebalancingWrapper.defaultTransitionToRebalanceAsync( coreMock, + rebalancingComponentWhiteList, rebalancingSetToken, - nextSetToken.address, + nextSetToken, constantAuctionPriceCurve.address, managerAccount ); @@ -818,8 +834,9 @@ contract('RebalancingSetToken', accounts => { beforeEach(async () => { await rebalancingWrapper.defaultTransitionToRebalanceAsync( coreMock, + rebalancingComponentWhiteList, rebalancingSetToken, - nextSetToken.address, + nextSetToken, constantAuctionPriceCurve.address, managerAccount ); @@ -842,8 +859,9 @@ contract('RebalancingSetToken', accounts => { await rebalancingWrapper.defaultTransitionToRebalanceAsync( coreMock, + rebalancingComponentWhiteList, rebalancingSetToken, - nextSetToken.address, + nextSetToken, constantAuctionPriceCurve.address, managerAccount ); @@ -888,6 +906,7 @@ contract('RebalancingSetToken', accounts => { initialUnitShares, proposalPeriod, rebalanceInterval, + rebalancingComponentWhiteList.address ); subjectNewManager = otherAccount, @@ -962,6 +981,13 @@ contract('RebalancingSetToken', accounts => { nextSetToken = setTokens[1]; reproposeRebalancingSetToken = setTokens[2]; + const nextSetTokenComponentAddresses = await nextSetToken.getComponents.callAsync(); + const reproposeRebalancingSetComponentAddresses = await reproposeRebalancingSetToken.getComponents.callAsync(); + const componentsToWhiteList = _.uniq( + nextSetTokenComponentAddresses.concat(reproposeRebalancingSetComponentAddresses) + ); + await coreWrapper.addTokensToWhiteList(componentsToWhiteList, rebalancingComponentWhiteList); + proposalPeriod = ONE_DAY_IN_SECONDS; rebalancingSetToken = await rebalancingWrapper.createDefaultRebalancingSetTokenAsync( coreMock, @@ -1060,7 +1086,21 @@ contract('RebalancingSetToken', accounts => { await SetTestUtils.assertLogEquivalence(formattedLogs, expectedLogs); }); - describe('but the rebalance interval has not elapsed', async () => { + describe('when one of the components in the next set is not on the whitelist', async () => { + beforeEach(async () => { + const nextSetComponents = await nextSetToken.getComponents.callAsync(); + await rebalancingComponentWhiteList.removeAddress.sendTransactionAsync( + nextSetComponents[0], + { from: deployerAccount } + ); + }); + + it('should revert', async () => { + await expectRevertError(subject()); + }); + }); + + describe('when the rebalance interval has not elapsed', async () => { beforeEach(async () => { subjectTimeFastForward = ONE_DAY_IN_SECONDS.sub(10); }); @@ -1070,7 +1110,7 @@ contract('RebalancingSetToken', accounts => { }); }); - describe('but not by the token manager', async () => { + describe('when not by the token manager', async () => { beforeEach(async () => { subjectCaller = otherAccount; }); @@ -1080,7 +1120,7 @@ contract('RebalancingSetToken', accounts => { }); }); - describe('but the auction library is not approved by Core', async () => { + describe('when the auction library is not approved by Core', async () => { beforeEach(async () => { subjectAuctionLibrary = invalidAccount; }); @@ -1090,7 +1130,7 @@ contract('RebalancingSetToken', accounts => { }); }); - describe('but the time to pivot is less than 21600', async () => { + describe('when the time to pivot is less than 21600', async () => { beforeEach(async () => { subjectAuctionTimeToPivot = ZERO; }); @@ -1100,7 +1140,7 @@ contract('RebalancingSetToken', accounts => { }); }); - describe('but the time to pivot is greater than 259200', async () => { + describe('when the time to pivot is greater than 259200', async () => { beforeEach(async () => { subjectAuctionTimeToPivot = new BigNumber(300000); }); @@ -1110,7 +1150,7 @@ contract('RebalancingSetToken', accounts => { }); }); - describe('but the pivot price is less than .5', async () => { + describe('when the pivot price is less than .5', async () => { beforeEach(async () => { const pivotPrice = new BigNumber(.4); subjectAuctionPivotPrice = DEFAULT_AUCTION_PRICE_DENOMINATOR.mul(pivotPrice); @@ -1121,7 +1161,7 @@ contract('RebalancingSetToken', accounts => { }); }); - describe('but the pivot price is greater than 5', async () => { + describe('when the pivot price is greater than 5', async () => { beforeEach(async () => { const pivotPrice = new BigNumber(6); subjectAuctionPivotPrice = DEFAULT_AUCTION_PRICE_DENOMINATOR.mul(pivotPrice); @@ -1132,7 +1172,7 @@ contract('RebalancingSetToken', accounts => { }); }); - describe('but the proposed nextSet is not approved by Core', async () => { + describe('when the proposed nextSet is not approved by Core', async () => { beforeEach(async () => { subjectRebalancingToken = fakeTokenAccount; }); @@ -1142,7 +1182,7 @@ contract('RebalancingSetToken', accounts => { }); }); - describe("but the new proposed set's natural unit is not a multiple of the current set", async () => { + describe("when 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)]; @@ -1162,11 +1202,18 @@ contract('RebalancingSetToken', accounts => { let timeJump: BigNumber; beforeEach(async () => { - await rebalancingWrapper.defaultTransitionToProposeAsync( + const auctionTimeToPivot = new BigNumber(100000); + const auctionStartPrice = new BigNumber(500); + const auctionPivotPrice = DEFAULT_AUCTION_PRICE_NUMERATOR; + + await rebalancingWrapper.transitionToProposeAsync( coreMock, rebalancingSetToken, - nextSetToken.address, + nextSetToken, constantAuctionPriceCurve.address, + auctionTimeToPivot, + auctionStartPrice, + auctionPivotPrice, managerAccount ); @@ -1194,11 +1241,18 @@ contract('RebalancingSetToken', accounts => { describe('when propose is called from Rebalance state', async () => { beforeEach(async () => { - await rebalancingWrapper.defaultTransitionToRebalanceAsync( + const auctionTimeToPivot = new BigNumber(100000); + const auctionStartPrice = new BigNumber(500); + const auctionPivotPrice = DEFAULT_AUCTION_PRICE_NUMERATOR; + + await rebalancingWrapper.transitionToRebalanceAsync( coreMock, rebalancingSetToken, - nextSetToken.address, + nextSetToken, constantAuctionPriceCurve.address, + auctionTimeToPivot, + auctionStartPrice, + auctionPivotPrice, managerAccount ); }); @@ -1210,19 +1264,26 @@ contract('RebalancingSetToken', accounts => { describe('when propose is called from Drawdown State', async () => { beforeEach(async () => { - // Issue currentSetToken - await coreMock.issue.sendTransactionAsync(currentSetToken.address, ether(9), {from: deployerAccount}); - await erc20Wrapper.approveTransfersAsync([currentSetToken], transferProxy.address); + // Issue currentSetToken + await coreMock.issue.sendTransactionAsync(currentSetToken.address, ether(9), {from: deployerAccount}); + await erc20Wrapper.approveTransfersAsync([currentSetToken], transferProxy.address); - // Use issued currentSetToken to issue rebalancingSetToken - const rebalancingSetQuantityToIssue = ether(7); - await coreMock.issue.sendTransactionAsync(rebalancingSetToken.address, rebalancingSetQuantityToIssue); + // Use issued currentSetToken to issue rebalancingSetToken + const rebalancingSetQuantityToIssue = ether(7); + await coreMock.issue.sendTransactionAsync(rebalancingSetToken.address, rebalancingSetQuantityToIssue); - await rebalancingWrapper.defaultTransitionToRebalanceAsync( + const auctionTimeToPivot = new BigNumber(100000); + const auctionStartPrice = new BigNumber(500); + const auctionPivotPrice = DEFAULT_AUCTION_PRICE_NUMERATOR; + + await rebalancingWrapper.transitionToRebalanceAsync( coreMock, rebalancingSetToken, - nextSetToken.address, + nextSetToken, constantAuctionPriceCurve.address, + auctionTimeToPivot, + auctionStartPrice, + auctionPivotPrice, managerAccount ); @@ -1304,8 +1365,9 @@ contract('RebalancingSetToken', accounts => { beforeEach(async () => { await rebalancingWrapper.defaultTransitionToProposeAsync( coreMock, + rebalancingComponentWhiteList, rebalancingSetToken, - nextSetToken.address, + nextSetToken, constantAuctionPriceCurve.address, managerAccount ); @@ -1474,7 +1536,7 @@ contract('RebalancingSetToken', accounts => { }); }); - describe('but not enough time has passed before proposal period has elapsed', async () => { + describe('when not enough time has passed before proposal period has elapsed', async () => { beforeEach(async () => { subjectTimeFastForward = ONE_DAY_IN_SECONDS.sub(10); }); @@ -1489,8 +1551,9 @@ contract('RebalancingSetToken', accounts => { beforeEach(async () => { await rebalancingWrapper.defaultTransitionToRebalanceAsync( coreMock, + rebalancingComponentWhiteList, rebalancingSetToken, - nextSetToken.address, + nextSetToken, constantAuctionPriceCurve.address, managerAccount ); @@ -1505,8 +1568,9 @@ contract('RebalancingSetToken', accounts => { beforeEach(async () => { await rebalancingWrapper.defaultTransitionToRebalanceAsync( coreMock, + rebalancingComponentWhiteList, rebalancingSetToken, - nextSetToken.address, + nextSetToken, constantAuctionPriceCurve.address, managerAccount ); @@ -1585,8 +1649,9 @@ contract('RebalancingSetToken', accounts => { beforeEach(async () => { await rebalancingWrapper.defaultTransitionToProposeAsync( coreMock, + rebalancingComponentWhiteList, rebalancingSetToken, - nextSetToken.address, + nextSetToken, constantAuctionPriceCurve.address, managerAccount ); @@ -1601,8 +1666,9 @@ contract('RebalancingSetToken', accounts => { beforeEach(async () => { await rebalancingWrapper.defaultTransitionToRebalanceAsync( coreMock, + rebalancingComponentWhiteList, rebalancingSetToken, - nextSetToken.address, + nextSetToken, constantAuctionPriceCurve.address, managerAccount ); @@ -1700,8 +1766,9 @@ contract('RebalancingSetToken', accounts => { beforeEach(async () => { await rebalancingWrapper.defaultTransitionToRebalanceAsync( coreMock, + rebalancingComponentWhiteList, rebalancingSetToken, - nextSetToken.address, + nextSetToken, constantAuctionPriceCurve.address, managerAccount ); @@ -1716,8 +1783,9 @@ contract('RebalancingSetToken', accounts => { beforeEach(async () => { await rebalancingWrapper.defaultTransitionToRebalanceAsync( coreMock, + rebalancingComponentWhiteList, rebalancingSetToken, - nextSetToken.address, + nextSetToken, constantAuctionPriceCurve.address, managerAccount ); @@ -1797,8 +1865,9 @@ contract('RebalancingSetToken', accounts => { beforeEach(async () => { await rebalancingWrapper.defaultTransitionToProposeAsync( coreMock, + rebalancingComponentWhiteList, rebalancingSetToken, - nextSetToken.address, + nextSetToken, constantAuctionPriceCurve.address, managerAccount ); @@ -1813,8 +1882,9 @@ contract('RebalancingSetToken', accounts => { beforeEach(async () => { await rebalancingWrapper.defaultTransitionToRebalanceAsync( coreMock, + rebalancingComponentWhiteList, rebalancingSetToken, - nextSetToken.address, + nextSetToken, constantAuctionPriceCurve.address, managerAccount ); @@ -1878,13 +1948,13 @@ contract('RebalancingSetToken', accounts => { }); }); - describe('but pivot point has not been reached', async () => { + describe('when pivot point has not been reached', async () => { it('should revert', async () => { await expectRevertError(subject()); }); }); - describe('but auction could be settled', async () => { + describe('when auction could be settled', async () => { beforeEach(async () => { const defaultTimeToPivot = new BigNumber(100000); await blockchain.increaseTimeAsync(defaultTimeToPivot.add(1)); diff --git a/test/contracts/core/tokens/rebalancingSetTokenFactory.spec.ts b/test/contracts/core/tokens/rebalancingSetTokenFactory.spec.ts index 2d8c481a8..807c291c8 100644 --- a/test/contracts/core/tokens/rebalancingSetTokenFactory.spec.ts +++ b/test/contracts/core/tokens/rebalancingSetTokenFactory.spec.ts @@ -93,8 +93,10 @@ contract('RebalancingSetTokenFactory', accounts => { naturalUnit, ); + const rebalancingComponentWhiteList = await coreWrapper.deployWhiteListAsync(); rebalancingSetTokenFactory = await coreWrapper.deployRebalancingSetTokenFactoryAsync( core.address, + rebalancingComponentWhiteList.address ); await coreWrapper.addFactoryAsync(core, rebalancingSetTokenFactory); }); diff --git a/test/contracts/lib/whiteList.spec.ts b/test/contracts/lib/whiteList.spec.ts index 0597f3026..440272deb 100644 --- a/test/contracts/lib/whiteList.spec.ts +++ b/test/contracts/lib/whiteList.spec.ts @@ -31,6 +31,7 @@ contract('WhiteList', accounts => { firstTokenAddress, secondTokenAddress, thirdTokenAddress, + fourthTokenAddress, ] = accounts; let whiteList: WhiteListContract; @@ -221,4 +222,36 @@ contract('WhiteList', accounts => { }); }); }); + + describe('#areValidAddresses', async () => { + let subjectAddressesToVerify: Address[]; + + beforeEach(async () => { + whiteList = await coreWrapper.deployWhiteListAsync([firstTokenAddress, secondTokenAddress, thirdTokenAddress]); + + subjectAddressesToVerify = [firstTokenAddress, secondTokenAddress, thirdTokenAddress]; + }); + + async function subject(): Promise { + return await whiteList.areValidAddresses.callAsync(subjectAddressesToVerify); + } + + it('returns true', async () => { + const validity = await subject(); + + expect(validity).to.be.true; + }); + + describe('when one of the tokens is not whitelisted', async () => { + beforeEach(async () => { + subjectAddressesToVerify = [firstTokenAddress, secondTokenAddress, thirdTokenAddress, fourthTokenAddress]; + }); + + it('returns false', async () => { + const validity = await subject(); + + expect(validity).to.be.false; + }); + }); + }); }); diff --git a/utils/wrappers/coreWrapper.ts b/utils/wrappers/coreWrapper.ts index c2925e985..602b11996 100644 --- a/utils/wrappers/coreWrapper.ts +++ b/utils/wrappers/coreWrapper.ts @@ -136,12 +136,14 @@ export class CoreWrapper { public async deployRebalancingSetTokenFactoryAsync( coreAddress: Address, + componentWhitelistAddress: Address, minimumRebalanceInterval: BigNumber = ONE_DAY_IN_SECONDS, minimumProposalPeriod: BigNumber = ONE_DAY_IN_SECONDS, from: Address = this._tokenOwnerAddress ): Promise { const truffleTokenFactory = await RebalancingSetTokenFactory.new( coreAddress, + componentWhitelistAddress, minimumRebalanceInterval, minimumProposalPeriod, { from }, @@ -494,6 +496,31 @@ export class CoreWrapper { return balances; } + /* ============ WhiteList ============ */ + + public async addTokensToWhiteList( + tokenAddresses: Address[], + whiteList: WhiteListContract, + from: Address = this._contractOwnerAddress, + ): Promise { + const addAddressPromises = _.map(tokenAddresses, address => { + this.addTokenToWhiteList(address, whiteList); + }); + + await Promise.all(addAddressPromises); + } + + public async addTokenToWhiteList( + address: Address, + whiteList: WhiteListContract, + from: Address = this._contractOwnerAddress, + ): Promise { + await whiteList.addAddress.sendTransactionAsync( + address, + { from }, + ); + } + /* ============ CoreFactory Extension ============ */ public async createSetTokenAsync( diff --git a/utils/wrappers/rebalancingWrapper.ts b/utils/wrappers/rebalancingWrapper.ts index c34fe8d48..f7e2c3213 100644 --- a/utils/wrappers/rebalancingWrapper.ts +++ b/utils/wrappers/rebalancingWrapper.ts @@ -9,7 +9,8 @@ import { LinearAuctionPriceCurveContract, SetTokenContract, RebalancingSetTokenContract, - VaultContract + VaultContract, + WhiteListContract, } from '../contracts'; import { BigNumber } from 'bignumber.js'; @@ -65,6 +66,7 @@ export class RebalancingWrapper { initialShareRatio: BigNumber, proposalPeriod: BigNumber, rebalanceCoolOffPeriod: BigNumber, + rebalancingComponentWhiteListAddress: Address, name: string = 'Rebalancing Set', symbol: string = 'RBSET', from: Address = this._tokenOwnerAddress @@ -76,6 +78,7 @@ export class RebalancingWrapper { initialShareRatio, proposalPeriod, rebalanceCoolOffPeriod, + rebalancingComponentWhiteListAddress, name, symbol, { from, gas: DEFAULT_GAS }, @@ -251,8 +254,9 @@ export class RebalancingWrapper { public async defaultTransitionToProposeAsync( core: CoreLikeContract, + rebalancingComponentWhiteList: WhiteListContract, rebalancingSetToken: RebalancingSetTokenContract, - newRebalancingSetToken: Address, + nextSetToken: SetTokenContract, auctionLibrary: Address, caller: Address ): Promise { @@ -261,6 +265,35 @@ export class RebalancingWrapper { const auctionStartPrice = new BigNumber(500); const auctionPivotPrice = DEFAULT_AUCTION_PRICE_NUMERATOR; + const nextSetTokenComponentAddresses = await nextSetToken.getComponents.callAsync(); + await this._coreWrapper.addTokensToWhiteList( + nextSetTokenComponentAddresses, + rebalancingComponentWhiteList + ); + + // Transition to propose + await this.transitionToProposeAsync( + core, + rebalancingSetToken, + nextSetToken, + auctionLibrary, + auctionTimeToPivot, + auctionStartPrice, + auctionPivotPrice, + caller, + ); + } + + public async transitionToProposeAsync( + core: CoreLikeContract, + rebalancingSetToken: RebalancingSetTokenContract, + nextSetToken: SetTokenContract, + auctionLibrary: Address, + auctionTimeToPivot: BigNumber, + auctionStartPrice: BigNumber, + auctionPivotPrice: BigNumber, + caller: Address + ): Promise { // Approve price library await core.addPriceLibrary.sendTransactionAsync( auctionLibrary, @@ -270,7 +303,7 @@ export class RebalancingWrapper { // Transition to propose await this._blockchain.increaseTimeAsync(ONE_DAY_IN_SECONDS.add(1)); await rebalancingSetToken.propose.sendTransactionAsync( - newRebalancingSetToken, + nextSetToken.address, auctionLibrary, auctionTimeToPivot, auctionStartPrice, @@ -281,17 +314,47 @@ export class RebalancingWrapper { public async defaultTransitionToRebalanceAsync( core: CoreLikeContract, + rebalancingComponentWhiteList: WhiteListContract, rebalancingSetToken: RebalancingSetTokenContract, - newRebalancingSetToken: Address, + nextSetToken: SetTokenContract, auctionLibrary: Address, caller: Address ): Promise { // Transition to propose await this.defaultTransitionToProposeAsync( + core, + rebalancingComponentWhiteList, + rebalancingSetToken, + nextSetToken, + auctionLibrary, + caller + ); + + // Transition to rebalance + await this._blockchain.increaseTimeAsync(ONE_DAY_IN_SECONDS.add(1)); + await rebalancingSetToken.startRebalance.sendTransactionAsync( + { from: caller, gas: DEFAULT_GAS } + ); + } + + public async transitionToRebalanceAsync( + core: CoreLikeContract, + rebalancingSetToken: RebalancingSetTokenContract, + nextSetToken: SetTokenContract, + auctionLibrary: Address, + auctionTimeToPivot: BigNumber, + auctionStartPrice: BigNumber, + auctionPivotPrice: BigNumber, + caller: Address + ): Promise { + await this.transitionToProposeAsync( core, rebalancingSetToken, - newRebalancingSetToken, + nextSetToken, auctionLibrary, + auctionTimeToPivot, + auctionStartPrice, + auctionPivotPrice, caller );