diff --git a/.node-xmlhttprequest-sync-8770 b/.node-xmlhttprequest-sync-8770 deleted file mode 100644 index e69de29bb..000000000 diff --git a/contracts/core/RebalancingSetToken.sol b/contracts/core/RebalancingSetToken.sol index bfb826aab..af393fe1d 100644 --- a/contracts/core/RebalancingSetToken.sol +++ b/contracts/core/RebalancingSetToken.sol @@ -19,8 +19,11 @@ pragma solidity 0.4.24; import { DetailedERC20 } from "zeppelin-solidity/contracts/token/ERC20/DetailedERC20.sol"; import { SafeMath } from "zeppelin-solidity/contracts/math/SafeMath.sol"; import { StandardToken } from "zeppelin-solidity/contracts/token/ERC20/StandardToken.sol"; -import { Bytes32 } from "../lib/Bytes32.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 { AddressArrayUtils } from "../external/cryptofin/AddressArrayUtils.sol"; /** @@ -35,6 +38,7 @@ contract RebalancingSetToken is { using SafeMath for uint256; using Bytes32 for bytes32; + using AddressArrayUtils for address[]; /* ============ Enums ============ */ @@ -162,6 +166,7 @@ contract RebalancingSetToken is ) external { + // Make sure it is manager that is proposing the rebalance require(msg.sender == manager); @@ -178,6 +183,9 @@ contract RebalancingSetToken is auctionStartPrice = _auctionStartPrice; auctionPriceDivisor = _auctionPriceDivisor; + // Create token arrays needed for auction + parseUnitArrays(); + // Update state parameters proposalStartTime = block.timestamp; rebalanceState = State.Proposal; @@ -202,6 +210,12 @@ contract RebalancingSetToken is // Be sure the full proposal period has elapsed require(block.timestamp >= proposalStartTime.add(proposalPeriod)); + // Get core address + address core = ISetFactory(factory).core(); + + // Redeem current set held by rebalancing token in vault + ICore(core).redeemInVault(currentSet, unitShares.mul(totalSupply_)); + // Update state parameters auctionStartTime = block.timestamp; rebalanceState = State.Rebalance; @@ -331,6 +345,45 @@ contract RebalancingSetToken is return [unitShares]; } + /* + * Get combinedTokenArray of Rebalancing Set + * + * @return combinedTokenArray + */ + function getCombinedTokenArray() + external + view + returns(address[]) + { + return combinedTokenArray; + } + + /* + * Get combinedCurrentUnits of Rebalancing Set + * + * @return combinedCurrentUnits + */ + function getCombinedCurrentUnits() + external + view + returns(uint256[]) + { + return combinedCurrentUnits; + } + + /* + * Get combinedRebalanceUnits of Rebalancing Set + * + * @return combinedRebalanceUnits + */ + function getCombinedRebalanceUnits() + external + view + returns(uint256[]) + { + return combinedRebalanceUnits; + } + /* ============ Transfer Overrides ============ */ /* @@ -376,4 +429,67 @@ contract RebalancingSetToken is // Use inherited transferFrom function return super.transferFrom(_from, _to, _value); } + + /* ============ Internal Functions ============ */ + function parseUnitArrays() + internal + { + // Create interfaces for interacting with sets + ISetToken currentSetInterface = ISetToken(currentSet); + ISetToken rebalancingSetInterface = ISetToken(rebalancingSet); + + // Create combined token Array + address[] memory oldComponents = currentSetInterface.getComponents(); + address[] memory newComponents = rebalancingSetInterface.getComponents(); + combinedTokenArray = oldComponents.union(newComponents); + + // Get naturalUnit of both sets + uint256 currentSetNaturalUnit = currentSetInterface.naturalUnit(); + uint256 rebalancingSetNaturalUnit = rebalancingSetInterface.naturalUnit(); + + // Get units arrays for both sets + uint256[] memory currentSetUnits = currentSetInterface.getUnits(); + uint256[] memory rebalancingSetUnits = rebalancingSetInterface.getUnits(); + + 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]); + + // Compute and push unit amounts of token in currentSet, push 0 if not in set + if (isInCurrent) { + combinedCurrentUnits.push( + computeUnits(currentSetUnits[indexCurrent], currentSetNaturalUnit) + ); + } else { + combinedCurrentUnits.push(uint256(0)); + } + + // Compute and push unit amounts of token in rebalancingSet, push 0 if not in set + if (isInRebalance) { + combinedRebalanceUnits.push( + computeUnits(rebalancingSetUnits[indexRebalance], rebalancingSetNaturalUnit) + ); + } else { + combinedRebalanceUnits.push(uint256(0)); + } + } + } + + /** + * 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 + */ + function computeUnits( + uint256 _unit, + uint256 _naturalUnit + ) + internal + returns (uint256) + { + uint256 coefficient = uint256(10) ** uint256(18); + return coefficient.mul(_unit).div(_naturalUnit); + } } diff --git a/contracts/core/extensions/CoreIssuance.sol b/contracts/core/extensions/CoreIssuance.sol index 936bed03b..c78dc1495 100644 --- a/contracts/core/extensions/CoreIssuance.sol +++ b/contracts/core/extensions/CoreIssuance.sol @@ -100,7 +100,7 @@ contract CoreIssuance is * * Normally, you should expect to be able to withdraw all of the tokens. * However, some have central abilities to freeze transfers (e.g. EOS). _toExclude - * allows you to optionally specify which component tokens to exclude when + * allows you to optionally specify which component tokens to exclude when * redeeming. They will remain in the vault under the users' addresses. * * @param _set The address of the Set token @@ -365,6 +365,6 @@ contract CoreIssuance is internal returns (uint256) { - return _quantity.div(_naturalUnit).mul(_componentUnits); + return _quantity.mul(_componentUnits).div(_naturalUnit); } } diff --git a/contracts/core/interfaces/ICore.sol b/contracts/core/interfaces/ICore.sol index e25b4f1db..fb877ead3 100644 --- a/contracts/core/interfaces/ICore.sol +++ b/contracts/core/interfaces/ICore.sol @@ -109,6 +109,18 @@ interface ICore { ) external; + /** + * Function to convert Set Tokens held in vault into underlying components + * + * @param _set The address of the Set token + * @param _quantity The number of tokens to redeem. Should be multiple of natural unit. + */ + function redeemInVault( + address _set, + uint256 _quantity + ) + external; + /** * Deposit multiple tokens to the vault. Quantities should be in the * order of the addresses of the tokens being deposited. diff --git a/contracts/mocks/core/CoreMock.sol b/contracts/mocks/core/CoreMock.sol new file mode 100644 index 000000000..360b05064 --- /dev/null +++ b/contracts/mocks/core/CoreMock.sol @@ -0,0 +1,64 @@ +pragma solidity 0.4.24; + +import { Core } from "../../core/Core.sol"; +import { ISetToken } from "../../core/interfaces/ISetToken.sol"; + +// Mock contract implementation of Core with extra functions for testing +contract CoreMock is Core { + constructor( + address _transferProxy, + address _vault + ) + public + Core(_transferProxy, _vault) + {} + + /* + * Mint set token for given address. + * Can only be called by authorized contracts. + * + * @param _set The address of the Set to mint + * @param _issuer The address of the issuing account + * @param _quantity The number of sets to attribute to issuer + */ + function mint( + address _set, + address _issuer, + uint256 _quantity + ) + external + { + ISetToken setToken = ISetToken(_set); + + // Issue set token + setToken.mint( + _issuer, + _quantity + ); + } + + /* + * Burn set token for given address. + * Can only be called by authorized contracts. + * + * @param _set The address of the Set to burn + * @param _from The address of the redeeming account + * @param _quantity The number of sets to burn from redeemer + */ + function burn( + address _set, + address _from, + uint256 _quantity + ) + external + { + ISetToken setToken = ISetToken(_set); + + // Issue set token + setToken.burn( + _from, + _quantity + ); + } +} + diff --git a/test/core/rebalancingSetToken.spec.ts b/test/core/rebalancingSetToken.spec.ts index 18af6700c..d1625f903 100644 --- a/test/core/rebalancingSetToken.spec.ts +++ b/test/core/rebalancingSetToken.spec.ts @@ -1,3 +1,4 @@ +import * as _ from 'lodash'; import * as ABIDecoder from 'abi-decoder'; import * as chai from 'chai'; import { BigNumber } from 'bignumber.js'; @@ -6,13 +7,24 @@ import { Address } from 'set-protocol-utils'; import ChaiSetup from '../../utils/chaiSetup'; import { BigNumberSetup } from '../../utils/bigNumberSetup'; import { + CoreMockContract, + SetTokenContract, RebalancingSetTokenContract, - StandardTokenMockContract, + RebalancingSetTokenFactoryContract, SetTokenFactoryContract, + StandardTokenMockContract, + TransferProxyContract, + VaultContract, } from '../../utils/contracts'; import { Blockchain } from '../../utils/blockchain'; import { ether } from '../../utils/units'; -import { DEFAULT_GAS, NULL_ADDRESS, REBALANCING_STATE } from '../../utils/constants'; +import { + DEFAULT_GAS, + NULL_ADDRESS, + REBALANCING_STATE, + ONE_DAY_IN_SECONDS, + DEFAULT_UNIT_SHARES, +} from '../../utils/constants'; import { assertLogEquivalence, getFormattedLogsFromTxHash } from '../../utils/logs'; import { getExpectedTransferLog, @@ -23,11 +35,14 @@ import { import { expectRevertError, assertTokenBalance } from '../../utils/tokenAssertions'; import { CoreWrapper } from '../../utils/coreWrapper'; import { ERC20Wrapper } from '../../utils/erc20Wrapper'; +import { RebalancingTokenWrapper } from '../../utils/RebalancingTokenWrapper'; BigNumberSetup.configure(); ChaiSetup.configure(); const { expect } = chai; const RebalancingSetToken = artifacts.require('RebalancingSetToken'); +const CoreMock = artifacts.require('CoreMock'); + contract('RebalancingSetToken', accounts => { const [ @@ -41,22 +56,46 @@ contract('RebalancingSetToken', accounts => { let rebalancingSetToken: RebalancingSetTokenContract; let components: StandardTokenMockContract[] = []; + + let coreMock: CoreMockContract; + let transferProxy: TransferProxyContract; + let vault: VaultContract; let factory: SetTokenFactoryContract; + let rebalancingFactory: RebalancingSetTokenFactoryContract; const coreWrapper = new CoreWrapper(deployerAccount, deployerAccount); const erc20Wrapper = new ERC20Wrapper(deployerAccount); const blockchain = new Blockchain(web3); + const rebalancingTokenWrapper = new RebalancingTokenWrapper( + deployerAccount, + coreWrapper, + erc20Wrapper, + blockchain + ); before(async () => { await blockchain.saveSnapshotAsync(); + ABIDecoder.addABI(CoreMock.abi); ABIDecoder.addABI(RebalancingSetToken.abi); }); after(async () => { + ABIDecoder.removeABI(CoreMock.abi); ABIDecoder.removeABI(RebalancingSetToken.abi); await blockchain.revertAsync(); }); + beforeEach(async () => { + transferProxy = await coreWrapper.deployTransferProxyAsync(); + vault = await coreWrapper.deployVaultAsync(); + coreMock = await coreWrapper.deployCoreMockAsync(transferProxy, vault); + factory = await coreWrapper.deploySetTokenFactoryAsync(coreMock.address); + rebalancingFactory = await coreWrapper.deployRebalancingSetTokenFactoryAsync(coreMock.address); + + await coreWrapper.setDefaultStateAndAuthorizationsAsync(coreMock, vault, transferProxy, factory); + await coreWrapper.enableFactoryAsync(coreMock, rebalancingFactory); + }); + describe('#constructor', async () => { let subjectFactory: Address; let subjectManager: Address; @@ -73,9 +112,9 @@ contract('RebalancingSetToken', accounts => { subjectFactory = factoryAccount; subjectManager = managerAccount; subjectInitialSet = components[0].address, - subjectInitialUnitShares = ether(1); - subjectProposalPeriod = new BigNumber(100000); - subjectRebalanceInterval = new BigNumber(100000); + subjectInitialUnitShares = DEFAULT_UNIT_SHARES; + subjectProposalPeriod = ONE_DAY_IN_SECONDS; + subjectRebalanceInterval = ONE_DAY_IN_SECONDS; }); async function subject(): Promise { @@ -183,9 +222,9 @@ contract('RebalancingSetToken', accounts => { initialSet = components[0].address; const manager = managerAccount; - const initialUnitShares = ether(1); - const proposalPeriod = new BigNumber(100000); - const rebalanceInterval = new BigNumber(100000); + const initialUnitShares = DEFAULT_UNIT_SHARES; + const proposalPeriod = ONE_DAY_IN_SECONDS; + const rebalanceInterval = ONE_DAY_IN_SECONDS; rebalancingSetToken = await coreWrapper.deployRebalancingSetTokenAsync( factoryAccount, @@ -196,7 +235,7 @@ contract('RebalancingSetToken', accounts => { rebalanceInterval, ); - subjectCaller = manager; + subjectCaller = managerAccount; }); async function subject(): Promise { @@ -219,11 +258,11 @@ contract('RebalancingSetToken', accounts => { beforeEach(async () => { components = await erc20Wrapper.deployTokensAsync(1, deployerAccount); - initialUnitShares = ether(1); const initialSet = components[0].address; const manager = managerAccount; - const proposalPeriod = new BigNumber(100000); - const rebalanceInterval = new BigNumber(100000); + initialUnitShares = DEFAULT_UNIT_SHARES; + const proposalPeriod = ONE_DAY_IN_SECONDS; + const rebalanceInterval = ONE_DAY_IN_SECONDS; rebalancingSetToken = await coreWrapper.deployRebalancingSetTokenAsync( factoryAccount, @@ -234,7 +273,7 @@ contract('RebalancingSetToken', accounts => { rebalanceInterval, ); - subjectCaller = manager; + subjectCaller = managerAccount; }); async function subject(): Promise { @@ -252,25 +291,37 @@ contract('RebalancingSetToken', accounts => { }); }); - describe('#mint', async () => { + describe('#mint: Called on Rebalancing Token', async () => { let subjectIssuer: Address; let subjectQuantity: BigNumber; let subjectCaller: Address; - let newRebalancingToken: Address; - beforeEach(async () => { + const naturalUnit: BigNumber = ether(2); components = await erc20Wrapper.deployTokensAsync(2, deployerAccount); - factory = await coreWrapper.deploySetTokenFactoryAsync(coreAccount); + 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( + coreMock, + factory.address, + currentComponentAddresses, + currentComponentUnits, + naturalUnit, + ); const manager = managerAccount; - const initialSet = components[0].address; - const initialUnitShares = ether(1); - const proposalPeriod = new BigNumber(100000); - const rebalanceInterval = new BigNumber(100000); + const initialSet = currentSetToken.address; + const initialUnitShares = DEFAULT_UNIT_SHARES; + const proposalPeriod = ONE_DAY_IN_SECONDS; + const rebalanceInterval = ONE_DAY_IN_SECONDS; + + const rebalancingFactory = await coreWrapper.deployRebalancingSetTokenFactoryAsync(coreAccount); + await coreWrapper.enableFactoryAsync(coreMock, rebalancingFactory); rebalancingSetToken = await coreWrapper.deployRebalancingSetTokenAsync( - factory.address, + rebalancingFactory.address, manager, initialSet, initialUnitShares, @@ -278,8 +329,6 @@ contract('RebalancingSetToken', accounts => { rebalanceInterval, ); - newRebalancingToken = components[1].address; - subjectIssuer = deployerAccount, subjectQuantity = ether(5); subjectCaller = coreAccount; @@ -302,6 +351,66 @@ contract('RebalancingSetToken', accounts => { assertTokenBalance(rebalancingSetToken, expectedNewBalance, subjectIssuer); }); + describe('when the caller is not Core', async () => { + beforeEach(async () => { + subjectCaller = otherAccount; + }); + + it('should revert', async () => { + await expectRevertError(subject()); + }); + }); + }); + + describe('#mint: Called from CoreMock', async () => { + let rebalancingSetToken: RebalancingSetTokenContract; + let subjectIssuer: Address; + let subjectQuantity: BigNumber; + let subjectCaller: Address; + + let newRebalancingSetToken: SetTokenContract; + + beforeEach(async () => { + const setTokens = await rebalancingTokenWrapper.createSetTokensAsync( + coreMock, + factory.address, + transferProxy.address + ); + const currentSetToken = setTokens[0]; + newRebalancingSetToken = setTokens[1]; + + const proposalPeriod = ONE_DAY_IN_SECONDS; + rebalancingSetToken = await rebalancingTokenWrapper.createDefaultRebalancingSetTokenAsync( + coreMock, + rebalancingFactory.address, + managerAccount, + currentSetToken.address, + proposalPeriod + ); + + subjectIssuer = deployerAccount, + subjectQuantity = ether(5); + subjectCaller = managerAccount; + }); + + async function subject(): Promise { + return coreMock.mint.sendTransactionAsync( + rebalancingSetToken.address, + subjectIssuer, + subjectQuantity, + { from: subjectCaller, gas: DEFAULT_GAS} + ); + } + + it('updates the balances of the user correctly', async () => { + const existingBalance = await rebalancingSetToken.balanceOf.callAsync(subjectIssuer); + + await subject(); + + const expectedNewBalance = existingBalance.add(subjectQuantity); + assertTokenBalance(rebalancingSetToken, expectedNewBalance, subjectIssuer); + }); + it('updates the totalSupply_ correctly', async () => { const existingTokenSupply = await rebalancingSetToken.totalSupply.callAsync(); @@ -326,38 +435,13 @@ contract('RebalancingSetToken', accounts => { await assertLogEquivalence(formattedLogs, expectedLogs); }); - describe('when the caller is not Core', async () => { - beforeEach(async () => { - subjectCaller = otherAccount; - }); - - it('should revert', async () => { - await expectRevertError(subject()); - }); - }); - describe('when mint is called from Rebalance state', async () => { beforeEach(async () => { - const auctionLibrary = libraryAccount; - const curveCoefficient = ether(1); - const auctionStartPrice = ether(5); - const auctionPriceDivisor = ether(10); - const caller = managerAccount; - const timeFastForward = 100000; - - blockchain.increaseTimeAsync(timeFastForward); - await rebalancingSetToken.propose.sendTransactionAsync( - newRebalancingToken, - auctionLibrary, - curveCoefficient, - auctionStartPrice, - auctionPriceDivisor, - { from: caller, gas: DEFAULT_GAS} - ); - - blockchain.increaseTimeAsync(timeFastForward); - await rebalancingSetToken.rebalance.sendTransactionAsync( - { from: caller, gas: DEFAULT_GAS } + await rebalancingTokenWrapper.defaultTransitionToRebalanceAsync( + rebalancingSetToken, + newRebalancingSetToken.address, + libraryAccount, + managerAccount ); }); @@ -367,25 +451,37 @@ contract('RebalancingSetToken', accounts => { }); }); - describe('#burn', async () => { - let subjectBurner: Address; + describe('#burn: Called on Rebalancing Token', async () => { + let subjectIssuer: Address; let subjectQuantity: BigNumber; let subjectCaller: Address; - let newRebalancingToken: Address; - beforeEach(async () => { + const naturalUnit: BigNumber = ether(2); components = await erc20Wrapper.deployTokensAsync(2, deployerAccount); - factory = await coreWrapper.deploySetTokenFactoryAsync(coreAccount); + 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( + coreMock, + factory.address, + currentComponentAddresses, + currentComponentUnits, + naturalUnit, + ); const manager = managerAccount; - const initialSet = components[0].address; - const initialUnitShares = ether(1); - const proposalPeriod = new BigNumber(100000); - const rebalanceInterval = new BigNumber(100000); + const initialSet = currentSetToken.address; + const initialUnitShares = DEFAULT_UNIT_SHARES; + const proposalPeriod = ONE_DAY_IN_SECONDS; + const rebalanceInterval = ONE_DAY_IN_SECONDS; + + const rebalancingFactory = await coreWrapper.deployRebalancingSetTokenFactoryAsync(coreAccount); + await coreWrapper.enableFactoryAsync(coreMock, rebalancingFactory); rebalancingSetToken = await coreWrapper.deployRebalancingSetTokenAsync( - factory.address, + rebalancingFactory.address, manager, initialSet, initialUnitShares, @@ -393,14 +489,78 @@ contract('RebalancingSetToken', accounts => { rebalanceInterval, ); - const mintedQuantity = ether(5); - subjectBurner = deployerAccount, + subjectIssuer = deployerAccount, subjectQuantity = ether(5); subjectCaller = coreAccount; - newRebalancingToken = components[1].address; + return rebalancingSetToken.mint.sendTransactionAsync( + subjectIssuer, + subjectQuantity, + { from: subjectCaller, gas: DEFAULT_GAS} + ); + }); - await rebalancingSetToken.mint.sendTransactionAsync( + async function subject(): Promise { + return rebalancingSetToken.burn.sendTransactionAsync( + subjectIssuer, + subjectQuantity, + { from: subjectCaller, gas: DEFAULT_GAS} + ); + } + + it('updates the balances of the user correctly', async () => { + const existingBalance = await rebalancingSetToken.balanceOf.callAsync(subjectIssuer); + + await subject(); + + const expectedNewBalance = existingBalance.sub(subjectQuantity); + assertTokenBalance(rebalancingSetToken, expectedNewBalance, subjectIssuer); + }); + + describe('when the caller is not Core', async () => { + beforeEach(async () => { + subjectCaller = otherAccount; + }); + + it('should revert', async () => { + await expectRevertError(subject()); + }); + }); + }); + + describe('#burn: Called from CoreMock', async () => { + let rebalancingSetToken: RebalancingSetTokenContract; + let subjectBurner: Address; + let subjectQuantity: BigNumber; + let subjectCaller: Address; + + let newRebalancingSetToken: SetTokenContract; + + beforeEach(async () => { + const setTokens = await rebalancingTokenWrapper.createSetTokensAsync( + coreMock, + factory.address, + transferProxy.address + ); + const currentSetToken = setTokens[0]; + newRebalancingSetToken = setTokens[1]; + + const proposalPeriod = ONE_DAY_IN_SECONDS; + rebalancingSetToken = await rebalancingTokenWrapper.createDefaultRebalancingSetTokenAsync( + coreMock, + rebalancingFactory.address, + managerAccount, + currentSetToken.address, + proposalPeriod + ); + + const mintedQuantity = ether(5); + subjectBurner = deployerAccount, + subjectQuantity = ether(5); + subjectCaller = managerAccount; + + await coreMock.mint.sendTransactionAsync( + rebalancingSetToken.address, subjectBurner, mintedQuantity, { from: subjectCaller, gas: DEFAULT_GAS} @@ -408,7 +568,8 @@ contract('RebalancingSetToken', accounts => { }); async function subject(): Promise { - return rebalancingSetToken.burn.sendTransactionAsync( + return coreMock.burn.sendTransactionAsync( + rebalancingSetToken.address, subjectBurner, subjectQuantity, { from: subjectCaller, gas: DEFAULT_GAS} @@ -448,16 +609,6 @@ contract('RebalancingSetToken', accounts => { await assertLogEquivalence(formattedLogs, expectedLogs); }); - describe('when the caller is not Core', async () => { - beforeEach(async () => { - subjectCaller = otherAccount; - }); - - it('should revert', async () => { - await expectRevertError(subject()); - }); - }); - describe('when the user does not have enough shares to burn', async () => { beforeEach(async () => { subjectQuantity = ether(10); @@ -470,26 +621,20 @@ contract('RebalancingSetToken', accounts => { describe('when burn is called from Rebalance state', async () => { beforeEach(async () => { - const auctionLibrary = libraryAccount; - const curveCoefficient = ether(1); - const auctionStartPrice = ether(5); - const auctionPriceDivisor = ether(10); - const caller = managerAccount; - const timeFastForward = 100000; - - blockchain.increaseTimeAsync(timeFastForward); - await rebalancingSetToken.propose.sendTransactionAsync( - newRebalancingToken, - auctionLibrary, - curveCoefficient, - auctionStartPrice, - auctionPriceDivisor, - { from: caller, gas: DEFAULT_GAS} + // 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} ); - blockchain.increaseTimeAsync(timeFastForward); - await rebalancingSetToken.rebalance.sendTransactionAsync( - { from: caller, gas: DEFAULT_GAS } + await rebalancingTokenWrapper.defaultTransitionToRebalanceAsync( + rebalancingSetToken, + newRebalancingSetToken.address, + libraryAccount, + managerAccount ); }); @@ -507,11 +652,11 @@ contract('RebalancingSetToken', accounts => { beforeEach(async () => { components = await erc20Wrapper.deployTokensAsync(1, deployerAccount); - const manager = managerAccount; const initialSet = components[0].address; - const initialUnitShares = ether(1); - const proposalPeriod = new BigNumber(100000); - const rebalanceInterval = new BigNumber(100000); + const manager = managerAccount; + const initialUnitShares = DEFAULT_UNIT_SHARES; + const proposalPeriod = ONE_DAY_IN_SECONDS; + const rebalanceInterval = ONE_DAY_IN_SECONDS; rebalancingSetToken = await coreWrapper.deployRebalancingSetTokenAsync( factoryAccount, @@ -523,7 +668,7 @@ contract('RebalancingSetToken', accounts => { ); subjectNewManager = otherAccount, - subjectCaller = manager; + subjectCaller = managerAccount; }); async function subject(): Promise { @@ -575,9 +720,9 @@ contract('RebalancingSetToken', accounts => { const manager = managerAccount; const initialSet = components[0].address; - const initialUnitShares = ether(1); - const proposalPeriod = new BigNumber(100000); - const rebalanceInterval = new BigNumber(100000); + const initialUnitShares = DEFAULT_UNIT_SHARES; + const proposalPeriod = ONE_DAY_IN_SECONDS; + const rebalanceInterval = ONE_DAY_IN_SECONDS; rebalancingSetToken = await coreWrapper.deployRebalancingSetTokenAsync( factory.address, @@ -651,9 +796,9 @@ contract('RebalancingSetToken', accounts => { const manager = managerAccount; const initialSet = components[0].address; - const initialUnitShares = ether(1); - const proposalPeriod = new BigNumber(100000); - const rebalanceInterval = new BigNumber(100000); + const initialUnitShares = DEFAULT_UNIT_SHARES; + const proposalPeriod = ONE_DAY_IN_SECONDS; + const rebalanceInterval = ONE_DAY_IN_SECONDS; rebalancingSetToken = await coreWrapper.deployRebalancingSetTokenAsync( factory.address, @@ -726,39 +871,37 @@ contract('RebalancingSetToken', accounts => { let subjectAuctionStartPrice: BigNumber; let subjectAuctionPriceDivisor: BigNumber; let subjectCaller: Address; - let subjectTimeFastForward: number; + let subjectTimeFastForward: BigNumber; let proposalPeriod: BigNumber; - let newRebalancingToken: Address; + let currentSetToken: SetTokenContract; + let newRebalancingSetToken: SetTokenContract; beforeEach(async () => { - components = await erc20Wrapper.deployTokensAsync(2, deployerAccount); - factory = await coreWrapper.deploySetTokenFactoryAsync(coreAccount); - - const manager = managerAccount; - const initialSet = components[0].address; - const initialUnitShares = ether(1); - const rebalanceInterval = new BigNumber(90000); - proposalPeriod = new BigNumber(90000); - - rebalancingSetToken = await coreWrapper.deployRebalancingSetTokenAsync( + const setTokens = await rebalancingTokenWrapper.createSetTokensAsync( + coreMock, factory.address, - manager, - initialSet, - initialUnitShares, - proposalPeriod, - rebalanceInterval, + transferProxy.address + ); + currentSetToken = setTokens[0]; + newRebalancingSetToken = setTokens[1]; + + proposalPeriod = ONE_DAY_IN_SECONDS; + rebalancingSetToken = await rebalancingTokenWrapper.createDefaultRebalancingSetTokenAsync( + coreMock, + rebalancingFactory.address, + managerAccount, + currentSetToken.address, + proposalPeriod ); - newRebalancingToken = components[1].address; - - subjectRebalancingToken = components[1].address; + subjectRebalancingToken = newRebalancingSetToken.address; subjectAuctionLibrary = libraryAccount; subjectCurveCoefficient = ether(1); subjectAuctionStartPrice = ether(5); subjectAuctionPriceDivisor = ether(10); subjectCaller = managerAccount; - subjectTimeFastForward = 100000; + subjectTimeFastForward = ONE_DAY_IN_SECONDS.add(1); }); async function subject(): Promise { @@ -816,6 +959,41 @@ contract('RebalancingSetToken', accounts => { expect(newRebalanceState).to.be.bignumber.equal(REBALANCING_STATE.PROPOSAL); }); + it('creates the correct combinedTokenArray', async () => { + const oldSet = await currentSetToken.getComponents.callAsync(); + const newSet = await newRebalancingSetToken.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); + }); + }); + + it('creates the correct combinedCurrentUnits', async () => { + await subject(); + + const expectedCombinedCurrentUnits = await rebalancingTokenWrapper.constructCombinedUnitArrayAsync( + rebalancingSetToken, + currentSetToken + ); + const actualCombinedCurrentUnits = await rebalancingSetToken.getCombinedCurrentUnits.callAsync(); + expect(JSON.stringify(actualCombinedCurrentUnits)).to.eql(JSON.stringify(expectedCombinedCurrentUnits)); + }); + + it('creates the correct combinedRebalanceUnits', async () => { + await subject(); + + const expectedCombinedRebalanceUnits = await rebalancingTokenWrapper.constructCombinedUnitArrayAsync( + rebalancingSetToken, + newRebalancingSetToken + ); + const actualCombinedRebalanceUnits = await rebalancingSetToken.getCombinedRebalanceUnits.callAsync(); + expect(JSON.stringify(actualCombinedRebalanceUnits)).to.eql(JSON.stringify(expectedCombinedRebalanceUnits)); + }); + it('emits the correct RebalanceProposed event', async () => { const txHash = await subject(); @@ -830,12 +1008,12 @@ contract('RebalancingSetToken', accounts => { rebalancingSetToken.address, ); - await assertLogEquivalence(formattedLogs, expectedLogs); + await assertLogEquivalence(formattedLogs, expectedLogs); }); describe('but the rebalance interval has not elapsed', async () => { beforeEach(async () => { - subjectTimeFastForward = 1000; + subjectTimeFastForward = ONE_DAY_IN_SECONDS.sub(10); }); it('should revert', async () => { @@ -856,21 +1034,11 @@ contract('RebalancingSetToken', accounts => { describe('when propose is called from Proposal state', async () => { beforeEach(async () => { - const auctionLibrary = libraryAccount; - const curveCoefficient = ether(1); - const auctionStartPrice = ether(5); - const auctionPriceDivisor = ether(10); - const caller = managerAccount; - const timeFastForward = 100000; - - blockchain.increaseTimeAsync(timeFastForward); - await rebalancingSetToken.propose.sendTransactionAsync( - newRebalancingToken, - auctionLibrary, - curveCoefficient, - auctionStartPrice, - auctionPriceDivisor, - { from: caller, gas: DEFAULT_GAS} + await rebalancingTokenWrapper.defaultTransitionToProposeAsync( + rebalancingSetToken, + newRebalancingSetToken.address, + libraryAccount, + managerAccount ); }); @@ -881,26 +1049,11 @@ contract('RebalancingSetToken', accounts => { describe('when propose is called from Rebalance state', async () => { beforeEach(async () => { - const auctionLibrary = libraryAccount; - const curveCoefficient = ether(1); - const auctionStartPrice = ether(5); - const auctionPriceDivisor = ether(10); - const caller = managerAccount; - const timeFastForward = 100000; - - blockchain.increaseTimeAsync(timeFastForward); - await rebalancingSetToken.propose.sendTransactionAsync( - newRebalancingToken, - auctionLibrary, - curveCoefficient, - auctionStartPrice, - auctionPriceDivisor, - { from: caller, gas: DEFAULT_GAS} - ); - - blockchain.increaseTimeAsync(timeFastForward); - await rebalancingSetToken.rebalance.sendTransactionAsync( - { from: caller, gas: DEFAULT_GAS } + await rebalancingTokenWrapper.defaultTransitionToRebalanceAsync( + rebalancingSetToken, + newRebalancingSetToken.address, + libraryAccount, + managerAccount ); }); @@ -912,37 +1065,36 @@ contract('RebalancingSetToken', accounts => { describe('#rebalance', async () => { let subjectCaller: Address; - let subjectTimeFastForward: number; + let subjectTimeFastForward: BigNumber; let proposalPeriod: BigNumber; - let initialSet: Address; - let newRebalancingToken: Address; + let currentSetToken: SetTokenContract; + let newRebalancingSetToken: SetTokenContract; beforeEach(async () => { - components = await erc20Wrapper.deployTokensAsync(2, deployerAccount); - - const manager = managerAccount; - const initialUnitShares = ether(1); - const rebalanceInterval = new BigNumber(90000); - proposalPeriod = new BigNumber(90000); - initialSet = components[0].address; - - rebalancingSetToken = await coreWrapper.deployRebalancingSetTokenAsync( - factoryAccount, - manager, - initialSet, - initialUnitShares, - proposalPeriod, - rebalanceInterval, + const setTokens = await rebalancingTokenWrapper.createSetTokensAsync( + coreMock, + factory.address, + transferProxy.address + ); + currentSetToken = setTokens[0]; + newRebalancingSetToken = setTokens[1]; + + proposalPeriod = ONE_DAY_IN_SECONDS; + rebalancingSetToken = await rebalancingTokenWrapper.createDefaultRebalancingSetTokenAsync( + coreMock, + rebalancingFactory.address, + managerAccount, + currentSetToken.address, + proposalPeriod ); - newRebalancingToken = components[1].address; subjectCaller = managerAccount; - subjectTimeFastForward = 100000; + subjectTimeFastForward = ONE_DAY_IN_SECONDS.add(1); }); async function subject(): Promise { - blockchain.increaseTimeAsync(subjectTimeFastForward); + await blockchain.increaseTimeAsync(subjectTimeFastForward); return rebalancingSetToken.rebalance.sendTransactionAsync( { from: subjectCaller, gas: DEFAULT_GAS} ); @@ -956,21 +1108,11 @@ contract('RebalancingSetToken', accounts => { describe('when rebalance is called from Propose State', async () => { beforeEach(async () => { - const auctionLibrary = libraryAccount; - const curveCoefficient = ether(1); - const auctionStartPrice = ether(5); - const auctionPriceDivisor = ether(10); - const caller = managerAccount; - const timeFastForward = 100000; - - blockchain.increaseTimeAsync(timeFastForward); - await rebalancingSetToken.propose.sendTransactionAsync( - newRebalancingToken, - auctionLibrary, - curveCoefficient, - auctionStartPrice, - auctionPriceDivisor, - { from: caller, gas: DEFAULT_GAS} + await rebalancingTokenWrapper.defaultTransitionToProposeAsync( + rebalancingSetToken, + newRebalancingSetToken.address, + libraryAccount, + managerAccount ); }); @@ -986,8 +1128,8 @@ contract('RebalancingSetToken', accounts => { const formattedLogs = await getFormattedLogsFromTxHash(txHash); const expectedLogs = getExpectedRebalanceStartedLog( - initialSet, - newRebalancingToken, + currentSetToken.address, + newRebalancingSetToken.address, rebalancingSetToken.address, ); @@ -996,7 +1138,7 @@ contract('RebalancingSetToken', accounts => { describe('but not enough time has passed before proposal period has elapsed', async () => { beforeEach(async () => { - subjectTimeFastForward = 1000; + subjectTimeFastForward = ONE_DAY_IN_SECONDS.sub(10); }); it('should revert', async () => { @@ -1007,26 +1149,11 @@ contract('RebalancingSetToken', accounts => { describe('when rebalance is called from Rebalance State', async () => { beforeEach(async () => { - const auctionLibrary = libraryAccount; - const curveCoefficient = ether(1); - const auctionStartPrice = ether(5); - const auctionPriceDivisor = ether(10); - const caller = managerAccount; - const timeFastForward = 100000; - - blockchain.increaseTimeAsync(timeFastForward); - await rebalancingSetToken.propose.sendTransactionAsync( - newRebalancingToken, - auctionLibrary, - curveCoefficient, - auctionStartPrice, - auctionPriceDivisor, - { from: caller, gas: DEFAULT_GAS} - ); - - blockchain.increaseTimeAsync(timeFastForward); - await rebalancingSetToken.rebalance.sendTransactionAsync( - { from: caller, gas: DEFAULT_GAS } + await rebalancingTokenWrapper.defaultTransitionToRebalanceAsync( + rebalancingSetToken, + newRebalancingSetToken.address, + libraryAccount, + managerAccount ); }); @@ -1040,29 +1167,27 @@ contract('RebalancingSetToken', accounts => { let subjectCaller: Address; let proposalPeriod: BigNumber; - let initialSet: Address; - let newRebalancingToken: Address; + let newRebalancingSetToken: SetTokenContract; beforeEach(async () => { - components = await erc20Wrapper.deployTokensAsync(2, deployerAccount); - - const manager = managerAccount; - const initialUnitShares = ether(1); - const rebalanceInterval = new BigNumber(90000); - proposalPeriod = new BigNumber(90000); - initialSet = components[0].address; + const setTokens = await rebalancingTokenWrapper.createSetTokensAsync( + coreMock, + factory.address, + transferProxy.address + ); + const currentSetToken = setTokens[0]; + newRebalancingSetToken = setTokens[1]; + + proposalPeriod = ONE_DAY_IN_SECONDS; + rebalancingSetToken = await rebalancingTokenWrapper.createDefaultRebalancingSetTokenAsync( + coreMock, + rebalancingFactory.address, + managerAccount, + currentSetToken.address, + proposalPeriod + ); - newRebalancingToken = components[1].address; subjectCaller = managerAccount; - - rebalancingSetToken = await coreWrapper.deployRebalancingSetTokenAsync( - factoryAccount, - manager, - initialSet, - initialUnitShares, - proposalPeriod, - rebalanceInterval, - ); }); async function subject(): Promise { @@ -1079,21 +1204,11 @@ contract('RebalancingSetToken', accounts => { describe('when settlement is called from Proposal State', async () => { beforeEach(async () => { - const auctionLibrary = libraryAccount; - const curveCoefficient = ether(1); - const auctionStartPrice = ether(5); - const auctionPriceDivisor = ether(10); - const caller = managerAccount; - const timeFastForward = 100000; - - blockchain.increaseTimeAsync(timeFastForward); - await rebalancingSetToken.propose.sendTransactionAsync( - newRebalancingToken, - auctionLibrary, - curveCoefficient, - auctionStartPrice, - auctionPriceDivisor, - { from: caller, gas: DEFAULT_GAS} + await rebalancingTokenWrapper.defaultTransitionToProposeAsync( + rebalancingSetToken, + newRebalancingSetToken.address, + libraryAccount, + managerAccount ); }); @@ -1104,26 +1219,11 @@ contract('RebalancingSetToken', accounts => { describe('when settlement is called from Rebalance State', async () => { beforeEach(async () => { - const auctionLibrary = libraryAccount; - const curveCoefficient = ether(1); - const auctionStartPrice = ether(5); - const auctionPriceDivisor = ether(10); - const caller = managerAccount; - const timeFastForward = 100000; - - blockchain.increaseTimeAsync(timeFastForward); - await rebalancingSetToken.propose.sendTransactionAsync( - newRebalancingToken, - auctionLibrary, - curveCoefficient, - auctionStartPrice, - auctionPriceDivisor, - { from: caller, gas: DEFAULT_GAS} - ); - - blockchain.increaseTimeAsync(timeFastForward); - await rebalancingSetToken.rebalance.sendTransactionAsync( - { from: caller, gas: DEFAULT_GAS } + await rebalancingTokenWrapper.defaultTransitionToRebalanceAsync( + rebalancingSetToken, + newRebalancingSetToken.address, + libraryAccount, + managerAccount ); }); @@ -1138,7 +1238,7 @@ contract('RebalancingSetToken', accounts => { await subject(); const newCurrentSet = await rebalancingSetToken.currentSet.callAsync(); - expect(newCurrentSet).to.equal(newRebalancingToken); + expect(newCurrentSet).to.equal(newRebalancingSetToken.address); }); }); }); diff --git a/test/lib/authorizable.spec.ts b/test/lib/authorizable.spec.ts index 28cb0ce8a..919c20594 100644 --- a/test/lib/authorizable.spec.ts +++ b/test/lib/authorizable.spec.ts @@ -114,7 +114,7 @@ contract('Authorizable', accounts => { }); describe('when the timestamp is beyond the grace period of 4 weeks', async () => { - const timeToIncrease = gracePeriod.plus(new BigNumber(1000)).toNumber(); + const timeToIncrease = gracePeriod.plus(new BigNumber(1000)); beforeEach(async () => { await blockchain.saveSnapshotAsync(); @@ -207,7 +207,7 @@ contract('Authorizable', accounts => { }); describe('when the timestamp is beyond the grace period of 4 weeks', async () => { - const timeToIncrease = gracePeriod.plus(new BigNumber(1000)).toNumber(); + const timeToIncrease = gracePeriod.plus(new BigNumber(1000)); beforeEach(async () => { await blockchain.saveSnapshotAsync(); @@ -314,7 +314,7 @@ contract('Authorizable', accounts => { }); describe('when the timestamp is beyond the grace period of 4 weeks', async () => { - const timeToIncrease = gracePeriod.plus(new BigNumber(1000)).toNumber(); + const timeToIncrease = gracePeriod.plus(new BigNumber(1000)); beforeEach(async () => { await blockchain.saveSnapshotAsync(); diff --git a/utils/RebalancingTokenWrapper.ts b/utils/RebalancingTokenWrapper.ts new file mode 100644 index 000000000..80f2cab37 --- /dev/null +++ b/utils/RebalancingTokenWrapper.ts @@ -0,0 +1,166 @@ +import * as _ from 'lodash'; +import { SetProtocolUtils, Address } from 'set-protocol-utils'; + +import { + CoreContract, + CoreMockContract, + SetTokenContract, + RebalancingSetTokenContract +} from './contracts'; +import { BigNumber } from 'bignumber.js'; + +import { ether } from './units'; +import { + DEFAULT_GAS, + ONE_DAY_IN_SECONDS, + DEFAULT_UNIT_SHARES, + DEFAULT_REBALANCING_NATURAL_UNIT, +} from './constants'; + +import { CoreWrapper } from './coreWrapper'; +import { ERC20Wrapper } from './erc20Wrapper'; +import { Blockchain } from './blockchain'; + +declare type CoreLikeContract = CoreMockContract | CoreContract; + +export class RebalancingTokenWrapper { + private _tokenOwnerAddress: Address; + private _coreWrapper: CoreWrapper; + private _erc20Wrapper: ERC20Wrapper; + private _blockchain: Blockchain; + + constructor( + tokenOwnerAddress: Address, + coreWrapper: CoreWrapper, + erc20Wrapper: ERC20Wrapper, + blockchain: Blockchain, + ) { + this._tokenOwnerAddress = tokenOwnerAddress; + + this._coreWrapper = coreWrapper; + this._erc20Wrapper = erc20Wrapper; + this._blockchain = blockchain; + } + + public async createSetTokensAsync( + core: CoreLikeContract, + factory: Address, + transferProxy: Address, + from: Address = this._tokenOwnerAddress, + ): Promise { + const naturalUnit = ether(2); + const components = await this._erc20Wrapper.deployTokensAsync(3, this._tokenOwnerAddress); + await this._erc20Wrapper.approveTransfersAsync(components, transferProxy); + + const firstSetComponents = components.slice(0, 2); + const firstSetComponentAddresses = _.map(firstSetComponents, token => token.address); + const firstSetComponentUnits = _.map(firstSetComponents, () => naturalUnit.mul(2)); // Multiple of naturalUnit + const firstSetToken = await this._coreWrapper.createSetTokenAsync( + core, + factory, + firstSetComponentAddresses, + firstSetComponentUnits, + naturalUnit, + ); + + const secondSetComponents = components.slice(1, 3); + const secondSetComponentAddresses = _.map(secondSetComponents, token => token.address); + const secondSetComponentUnits = _.map(secondSetComponents, () => naturalUnit.mul(1)); // Multiple of naturalUnit + const secondSetToken = await this._coreWrapper.createSetTokenAsync( + core, + factory, + secondSetComponentAddresses, + secondSetComponentUnits, + naturalUnit, + ); + return [firstSetToken, secondSetToken]; + } + + public async createDefaultRebalancingSetTokenAsync( + core: CoreLikeContract, + factory: Address, + manager: Address, + initialSet: Address, + proposalPeriod: BigNumber, + ): Promise { + const initialUnitShares = DEFAULT_UNIT_SHARES; + const rebalanceInterval = ONE_DAY_IN_SECONDS; + const callData = SetProtocolUtils.bufferArrayToHex([ + SetProtocolUtils.paddedBufferForPrimitive(manager), + SetProtocolUtils.paddedBufferForBigNumber(proposalPeriod), + SetProtocolUtils.paddedBufferForBigNumber(rebalanceInterval), + ]); + + return await this._coreWrapper.createRebalancingTokenAsync( + core, + factory, + [initialSet], + [initialUnitShares], + DEFAULT_REBALANCING_NATURAL_UNIT, + callData, + ); + } + + public async defaultTransitionToProposeAsync( + rebalancingSetToken: RebalancingSetTokenContract, + newRebalancingSetToken: Address, + auctionLibrary: Address, + caller: Address + ): Promise { + const curveCoefficient = ether(1); + const auctionStartPrice = ether(5); + const auctionPriceDivisor = ether(10); + + await this._blockchain.increaseTimeAsync(ONE_DAY_IN_SECONDS.add(1)); + await rebalancingSetToken.propose.sendTransactionAsync( + newRebalancingSetToken, + auctionLibrary, + curveCoefficient, + auctionStartPrice, + auctionPriceDivisor, + { from: caller, gas: DEFAULT_GAS} + ); + } + + public async defaultTransitionToRebalanceAsync( + rebalancingSetToken: RebalancingSetTokenContract, + newRebalancingSetToken: Address, + auctionLibrary: Address, + caller: Address + ): Promise { + await this.defaultTransitionToProposeAsync( + rebalancingSetToken, + newRebalancingSetToken, + auctionLibrary, + caller + ); + + 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 constructCombinedUnitArrayAsync( + rebalancingSetToken: RebalancingSetTokenContract, + setToken: SetTokenContract, + ): Promise { + 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 combinedSetTokenUnits: BigNumber[] = []; + combinedTokenArray.forEach(address => { + const index = setTokenComponents.indexOf(address); + if (index != -1) { + const totalTokenAmount = setTokenUnits[index].mul(new BigNumber(10 ** 18)).div(setNaturalUnit); + combinedSetTokenUnits.push(totalTokenAmount); + } else { + combinedSetTokenUnits.push(new BigNumber(0)); + } + }); + return combinedSetTokenUnits; + } +} diff --git a/utils/blockchain.ts b/utils/blockchain.ts index f063062cc..671027dca 100644 --- a/utils/blockchain.ts +++ b/utils/blockchain.ts @@ -1,6 +1,8 @@ import * as promisify from 'tiny-promisify'; import * as Web3 from 'web3'; +import { BigNumber } from 'bignumber.js'; + export class Blockchain { private _web3: Web3; @@ -22,9 +24,9 @@ export class Blockchain { } public async increaseTimeAsync( - duration: number, + duration: BigNumber, ): Promise { - await this.sendJSONRpcRequestAsync('evm_increaseTime', [duration]); + await this.sendJSONRpcRequestAsync('evm_increaseTime', [duration.toNumber()]); } private async sendJSONRpcRequestAsync( diff --git a/utils/constants.ts b/utils/constants.ts index fea508bf7..405adf0c6 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -14,6 +14,11 @@ export const UNLIMITED_ALLOWANCE_IN_BASE_UNITS = new BigNumber(2).pow(256).minus export const MAX_DIGITS_IN_UNSIGNED_256_INT = 78; export const ZRX_TOKEN_ADDRESS = '0x871dd7c2b4b25e1aa18728e9d5f2af4c4e431f5c'; +// Rebalancing Constants +export const ONE_DAY_IN_SECONDS = new BigNumber(86400); +export const DEFAULT_UNIT_SHARES = new BigNumber(1); +export const DEFAULT_REBALANCING_NATURAL_UNIT = new BigNumber(1); + // TODO: Move this into set-protocol-utils export const REBALANCING_STATE = { DEFAULT: 0, diff --git a/utils/contracts.ts b/utils/contracts.ts index f69ea0b0a..0cc830d0f 100644 --- a/utils/contracts.ts +++ b/utils/contracts.ts @@ -2,6 +2,7 @@ import { AuthorizableContract } from '../types/generated/authorizable'; import { Bytes32MockContract } from '../types/generated/bytes32_mock'; import { CommonMathMockContract } from '../types/generated/common_math_mock'; import { CoreContract } from '../types/generated/core'; +import { CoreMockContract } from '../types/generated/core_mock'; import { ERC20WrapperMockContract } from '../types/generated/erc20_wrapper_mock'; import { InvalidReturnTokenMockContract } from '../types/generated/invalid_return_token_mock'; import { NoXferReturnTokenMockContract } from '../types/generated/no_xfer_return_token_mock'; @@ -24,6 +25,7 @@ export { Bytes32MockContract, CommonMathMockContract, CoreContract, + CoreMockContract, ERC20WrapperMockContract, InvalidReturnTokenMockContract, NoXferReturnTokenMockContract, diff --git a/utils/coreWrapper.ts b/utils/coreWrapper.ts index 971c4ca0b..cbfb567f0 100644 --- a/utils/coreWrapper.ts +++ b/utils/coreWrapper.ts @@ -4,6 +4,7 @@ import { SetProtocolUtils, Address } from 'set-protocol-utils'; import { AuthorizableContract, CoreContract, + CoreMockContract, OrderLibraryMockContract, SetTokenContract, RebalancingSetTokenContract, @@ -21,6 +22,7 @@ import { stringToBytes32 } from './encoding'; const Authorizable = artifacts.require('Authorizable'); const Core = artifacts.require('Core'); +const CoreMock = artifacts.require('CoreMock'); const ERC20Wrapper = artifacts.require('ERC20Wrapper'); const OrderLibrary = artifacts.require('OrderLibrary'); const OrderLibraryMock = artifacts.require('OrderLibraryMock'); @@ -31,6 +33,7 @@ const SetTokenFactory = artifacts.require('SetTokenFactory'); const TransferProxy = artifacts.require('TransferProxy'); const Vault = artifacts.require('Vault'); +declare type CoreLikeContract = CoreMockContract | CoreContract; export class CoreWrapper { private _tokenOwnerAddress: Address; @@ -266,10 +269,34 @@ export class CoreWrapper { ); } + public async deployCoreMockAsync( + transferProxy: TransferProxyContract, + vault: VaultContract, + from: Address = this._tokenOwnerAddress + ): Promise { + if (!this._truffleOrderLibrary) { + this._truffleOrderLibrary = await OrderLibrary.new( + { from: this._tokenOwnerAddress }, + ); + } + + await Core.link('OrderLibrary', this._truffleOrderLibrary.address); + const truffleCore = await CoreMock.new( + transferProxy.address, + vault.address, + { from }, + ); + + return new CoreMockContract( + web3.eth.contract(truffleCore.abi).at(truffleCore.address), + { from, gas: DEFAULT_GAS }, + ); + } + /* ============ CoreInternal Extension ============ */ public async enableFactoryAsync( - core: CoreContract, + core: CoreLikeContract, setTokenFactory: SetTokenFactoryContract | RebalancingSetTokenFactoryContract, from: Address = this._contractOwnerAddress, ) { @@ -282,7 +309,7 @@ export class CoreWrapper { /* ============ Authorizable ============ */ public async setDefaultStateAndAuthorizationsAsync( - core: CoreContract, + core: CoreLikeContract, vault: VaultContract, transferProxy: TransferProxyContract, setTokenFactory: SetTokenFactoryContract, @@ -343,7 +370,7 @@ export class CoreWrapper { /* ============ CoreFactory Extension ============ */ public async createSetTokenAsync( - core: CoreContract, + core: CoreLikeContract, factory: Address, componentAddresses: Address[], units: BigNumber[], @@ -377,10 +404,45 @@ export class CoreWrapper { ); } + public async createRebalancingTokenAsync( + core: CoreLikeContract, + factory: Address, + componentAddresses: Address[], + units: BigNumber[], + naturalUnit: BigNumber, + callData: string = '', + name: string = 'Rebalancing Set Token', + symbol: string = 'RBSET', + from: Address = this._tokenOwnerAddress, + ): Promise { + const encodedName = stringToBytes32(name); + const encodedSymbol = stringToBytes32(symbol); + + const txHash = await core.create.sendTransactionAsync( + factory, + componentAddresses, + units, + naturalUnit, + encodedName, + encodedSymbol, + callData, + { from }, + ); + + const logs = await getFormattedLogsFromTxHash(txHash); + const setAddress = extractNewSetTokenAddressFromLogs(logs); + + return await RebalancingSetTokenContract.at( + setAddress, + web3, + { from } + ); + } + /* ============ CoreAccounting Extension ============ */ public async depositFromUser( - core: CoreContract, + core: CoreLikeContract, token: Address, quantity: BigNumber, from: Address = this._contractOwnerAddress, @@ -408,7 +470,7 @@ export class CoreWrapper { /* ============ CoreIssuance Extension ============ */ public async issueSetTokenAsync( - core: CoreContract, + core: CoreLikeContract, token: Address, quantity: BigNumber, from: Address = this._tokenOwnerAddress, @@ -441,7 +503,7 @@ export class CoreWrapper { /* ============ CoreExchangeDispatcher Extension ============ */ public async registerDefaultExchanges( - core: CoreContract, + core: CoreLikeContract, from: Address = this._contractOwnerAddress, ) { const approvePromises = _.map(_.values(SetProtocolUtils.EXCHANGES), exchangeId => @@ -451,7 +513,7 @@ export class CoreWrapper { } public async registerExchange( - core: CoreContract, + core: CoreLikeContract, exchangeId: number, exchangeAddress: Address, from: Address = this._contractOwnerAddress,