From 023872c5afe2515d56f24e0bcd125a59909a246e Mon Sep 17 00:00:00 2001 From: Alexander Soong Date: Tue, 3 Jul 2018 16:53:07 -0700 Subject: [PATCH] Redeem and withdraw --- contracts/core/extensions/CoreIssuance.sol | 73 ++++++++- test/core/extensions/coreAccounting.spec.ts | 2 +- test/core/extensions/coreIssuance.spec.ts | 163 ++++++++++++++++++++ test/utils/coreWrapper.ts | 54 +++++-- truffle.js | 5 + 5 files changed, 278 insertions(+), 19 deletions(-) diff --git a/contracts/core/extensions/CoreIssuance.sol b/contracts/core/extensions/CoreIssuance.sol index eb587465a..45bb0ae26 100644 --- a/contracts/core/extensions/CoreIssuance.sol +++ b/contracts/core/extensions/CoreIssuance.sol @@ -91,10 +91,9 @@ contract CoreIssuance is uint[] memory units = ISetToken(_setAddress).getUnits(); for (uint16 i = 0; i < components.length; i++) { address currentComponent = components[i]; - uint currentUnit = units[i]; uint tokenValue = calculateTransferValue( - currentUnit, + units[i], naturalUnit, _quantity ); @@ -115,6 +114,74 @@ contract CoreIssuance is } } + /** + * Composite method to redeem and withdraw with a single transaction + * + * Normally, you should expect to be able to withdraw all of the tokens. + * However, some have central abilities to freeze transfers (e.g. EOS). _toWithdraw + * allows you to optionally specify which component tokens to transfer + * back to the user. The rest will remain in the vault under the users' addresses. + * + * @param _setAddress The address of the Set token + * @param _quantity The number of tokens to redeem + * @param _toWithdraw Mask of indexes of tokens to withdraw + */ + function redeemAndWithdraw( + address _setAddress, + uint _quantity, + uint _toWithdraw + ) + external + isValidSet(_setAddress) + isPositiveQuantity(_quantity) + isNaturalUnitMultiple(_quantity, _setAddress) + { + // Burn the Set token (thereby decrementing the SetToken balance) + ISetToken(_setAddress).burn(msg.sender, _quantity); + + // Fetch Set token properties + uint naturalUnit = ISetToken(_setAddress).naturalUnit(); + address[] memory components = ISetToken(_setAddress).getComponents(); + uint[] memory units = ISetToken(_setAddress).getUnits(); + + // Loop through and decrement vault balances for the set, withdrawing if requested + for (uint i = 0; i < components.length; i++) { + // Calculate quantity to transfer + uint componentQuantity = calculateTransferValue( + units[i], + naturalUnit, + _quantity + ); + + // Decrement the component amount owned by the Set + IVault(state.vaultAddress).decrementTokenOwner( + _setAddress, + components[i], + componentQuantity + ); + + // Calculate bit index of current component + uint componentBitIndex = 2 ** i; + + // Transfer to user if component is included in _toWithdraw + if ((_toWithdraw & componentBitIndex) != 0) { + // Call Vault to withdraw tokens from Vault to user + IVault(state.vaultAddress).withdrawTo( + components[i], + msg.sender, + componentQuantity + ); + } else { + // Otherwise, increment the component amount for the user + IVault(state.vaultAddress).incrementTokenOwner( + msg.sender, + components[i], + componentQuantity + ); + } + } + } + /* ============ Private Functions ============ */ /** @@ -131,7 +198,7 @@ contract CoreIssuance is ) pure internal - returns(uint) + returns (uint) { return _quantity.div(_naturalUnit).mul(_componentUnits); } diff --git a/test/core/extensions/coreAccounting.spec.ts b/test/core/extensions/coreAccounting.spec.ts index 7742027cb..d785d573c 100644 --- a/test/core/extensions/coreAccounting.spec.ts +++ b/test/core/extensions/coreAccounting.spec.ts @@ -417,7 +417,7 @@ contract("CoreAccounting", (accounts) => { await subject(); - const newTokenBalances = await await erc20Wrapper.getTokenBalances(mockTokens, ownerAccount); + const newTokenBalances = await erc20Wrapper.getTokenBalances(mockTokens, ownerAccount); expect(newTokenBalances).to.eql(expectedNewBalances); }); diff --git a/test/core/extensions/coreIssuance.spec.ts b/test/core/extensions/coreIssuance.spec.ts index 65ca39e73..f59b23004 100644 --- a/test/core/extensions/coreIssuance.spec.ts +++ b/test/core/extensions/coreIssuance.spec.ts @@ -475,4 +475,167 @@ contract("CoreIssuance", (accounts) => { }); }); }); + + describe("#redeemAndWithdraw", async () => { + let subjectCaller: Address; + let subjectQuantityToRedeem: BigNumber; + let subjectSetToRedeem: Address; + let subjectComponentsToWithdrawMask: BigNumber; + + const naturalUnit: BigNumber = ether(2); + const numComponents: number = 3; + let components: StandardTokenMockContract[] = []; + let componentUnits: BigNumber[]; + let setToken: SetTokenContract; + + beforeEach(async () => { + components = await erc20Wrapper.deployTokensAsync(numComponents, ownerAccount); + await erc20Wrapper.approveTransfersAsync(components, transferProxy.address); + + const componentAddresses = _.map(components, (token) => token.address); + componentUnits = _.map(components, () => naturalUnit.mul(2)); // Multiple of naturalUnit + setToken = await coreWrapper.createSetTokenAsync( + core, + setTokenFactory.address, + componentAddresses, + componentUnits, + naturalUnit, + ); + + await coreWrapper.issueSetTokenAsync(core, setToken.address, naturalUnit); + + subjectCaller = ownerAccount; + subjectQuantityToRedeem = naturalUnit; + subjectSetToRedeem = setToken.address; + subjectComponentsToWithdrawMask = coreWrapper.maskForAllComponents(numComponents); + }); + + async function subject(): Promise { + return core.redeemAndWithdraw.sendTransactionAsync( + subjectSetToRedeem, + subjectQuantityToRedeem, + subjectComponentsToWithdrawMask, + { from: ownerAccount }, + ); + } + + it("decrements the balance of the tokens owned by set in vault", async () => { + const existingVaultBalances = await coreWrapper.getVaultBalancesForTokensForOwner(components, vault, subjectSetToRedeem); + + await subject(); + + const expectedVaultBalances = _.map(components, (component, idx) => { + const requiredQuantityToRedeem = subjectQuantityToRedeem.div(naturalUnit).mul(componentUnits[idx]); + return existingVaultBalances[idx].sub(requiredQuantityToRedeem); + }); + const newVaultBalances = await coreWrapper.getVaultBalancesForTokensForOwner(components, vault, subjectSetToRedeem); + expect(newVaultBalances).to.eql(expectedVaultBalances); + }); + + it("decrements the balance of the set tokens owned by owner", async () => { + const existingSetBalance = await setToken.balanceOf.callAsync(ownerAccount); + + await subject(); + + const expectedSetBalance = existingSetBalance.sub(subjectQuantityToRedeem); + const newSetBalance = await setToken.balanceOf.callAsync(ownerAccount); + expect(newSetBalance).to.be.bignumber.equal(expectedSetBalance); + }); + + it("transfers all of the component tokens back to the user", async () => { + const existingTokenBalances = await erc20Wrapper.getTokenBalances(components, ownerAccount); + + await subject(); + + const expectedNewBalances = _.map(existingTokenBalances, (balance, idx) => { + const quantityToRedeem = subjectQuantityToRedeem.div(naturalUnit).mul(componentUnits[idx]); + return balance.add(quantityToRedeem); + }); + const newTokenBalances = await erc20Wrapper.getTokenBalances(components, ownerAccount); + expect(newTokenBalances).to.eql(expectedNewBalances); + }); + + describe("when the withdraw mask includes one component", async () => { + const componentIndicesToWithdraw: number[] = [0]; + + beforeEach(async () => { + subjectComponentsToWithdrawMask = coreWrapper.maskForComponentsAtIndexes(componentIndicesToWithdraw); + }); + + it("transfers the component back to the user", async () => { + const componentToWithdraw = _.first(components); + const existingComponentBalance = await componentToWithdraw.balanceOf.callAsync(ownerAccount); + + await subject(); + + const componentQuantityToRedeem = subjectQuantityToRedeem.div(naturalUnit).mul(_.first(componentUnits)); + const expectedComponentBalance = existingComponentBalance.add(componentQuantityToRedeem); + const newTokenBalances = await componentToWithdraw.balanceOf.callAsync(ownerAccount); + expect(newTokenBalances).to.eql(expectedComponentBalance); + }); + + it("increments the balances of the remaining tokens back to the user in vault", async () => { + const remainingComponents = _.tail(components); + const existingBalances = await coreWrapper.getVaultBalancesForTokensForOwner(remainingComponents, vault, subjectSetToRedeem); + + await subject(); + + const expectedVaultBalances = _.map(remainingComponents, (component, idx) => { + const requiredQuantityToRedeem = subjectQuantityToRedeem.div(naturalUnit).mul(componentUnits[idx]); + return existingBalances[idx].sub(requiredQuantityToRedeem); + }); + const newVaultBalances = await coreWrapper.getVaultBalancesForTokensForOwner(remainingComponents, vault, subjectSetToRedeem); + expect(newVaultBalances).to.eql(expectedVaultBalances); + }); + }); + + describe("when the withdraw mask does not include any of the components", async () => { + beforeEach(async () => { + subjectComponentsToWithdrawMask = ZERO; + }); + + it("increments the balances of the tokens back to the user in vault", async () => { + const existingVaultBalances = await coreWrapper.getVaultBalancesForTokensForOwner(components, vault, ownerAccount); + + await subject(); + + const expectedVaultBalances = _.map(components, (component, idx) => { + const requiredQuantityToRedeem = subjectQuantityToRedeem.div(naturalUnit).mul(componentUnits[idx]); + return existingVaultBalances[idx].add(requiredQuantityToRedeem); + }); + const newVaultBalances = await coreWrapper.getVaultBalancesForTokensForOwner(components, vault, ownerAccount); + expect(newVaultBalances).to.eql(expectedVaultBalances); + }); + }); + + describe("when the set was not created through core", async () => { + beforeEach(async () => { + subjectSetToRedeem = NULL_ADDRESS; + }); + + it("should revert", async () => { + await expectRevertError(subject()); + }); + }); + + describe("when the user does not have enough of a set", async () => { + beforeEach(async () => { + subjectQuantityToRedeem = ether(3); + }); + + it("should revert", async () => { + await expectRevertError(subject()); + }); + }); + + describe("when the quantity is not a multiple of the natural unit of the set", async () => { + beforeEach(async () => { + subjectQuantityToRedeem = ether(1.5); + }); + + it("should revert", async () => { + await expectRevertError(subject()); + }); + }); + }); }); diff --git a/test/utils/coreWrapper.ts b/test/utils/coreWrapper.ts index eb5fd51d2..ae57810ea 100644 --- a/test/utils/coreWrapper.ts +++ b/test/utils/coreWrapper.ts @@ -33,6 +33,8 @@ export class CoreWrapper { this._contractOwnerAddress = contractOwnerAddress; } + /* ============ Deployment ============ */ + public async deployTransferProxyAsync( vaultAddress: Address, from: Address = this._tokenOwnerAddress @@ -147,7 +149,7 @@ export class CoreWrapper { ); } - // Internal + /* ============ CoreInternal Extension ============ */ public async enableFactoryAsync( core: CoreContract, @@ -160,7 +162,7 @@ export class CoreWrapper { ); } - // Authorizable + /* ============ Authorizable ============ */ public async setDefaultStateAndAuthorizationsAsync( core: CoreContract, @@ -200,7 +202,7 @@ export class CoreWrapper { ); } - // Vault + /* ============ Vault ============ */ public async incrementAccountBalanceAsync( vault: VaultContract, @@ -232,7 +234,7 @@ export class CoreWrapper { return balances; } - // Core + /* ============ CoreFactory Extension ============ */ public async createSetTokenAsync( core: CoreContract, @@ -264,6 +266,8 @@ export class CoreWrapper { ); } + /* ============ CoreAccounting Extension ============ */ + public async depositFromUser( core: CoreContract, token: Address, @@ -277,6 +281,21 @@ export class CoreWrapper { ); } + /* ============ SetToken Factory ============ */ + + public async setCoreAddress( + factory: SetTokenFactoryContract, + coreAddress: Address, + from: Address = this._contractOwnerAddress, + ) { + await factory.setCoreAddress.sendTransactionAsync( + coreAddress, + { from }, + ); + } + + /* ============ CoreIssuance Extension ============ */ + public async issueSetTokenAsync( core: CoreContract, token: Address, @@ -290,20 +309,25 @@ export class CoreWrapper { ); } - // SetTokenFactory + public maskForAllComponents( + numComponents: number, + ): BigNumber { + const allIndices = _.range(numComponents); + return this.maskForComponentsAtIndexes(allIndices); + } - public async setCoreAddress( - factory: SetTokenFactoryContract, - coreAddress: Address, - from: Address = this._contractOwnerAddress, - ) { - await factory.setCoreAddress.sendTransactionAsync( - coreAddress, - { from }, - ); + public maskForComponentsAtIndexes( + indexes: number[], + ): BigNumber { + return new BigNumber( + _.sum( + _.map( + indexes, (_, idx) => Math.pow(2, idx)) + ) + ) } - // ExchangeDispatcher + /* ============ CoreExchangeDispatcher Extension ============ */ public async registerDefaultExchanges( core: CoreContract, diff --git a/truffle.js b/truffle.js index 812021e23..02572c423 100644 --- a/truffle.js +++ b/truffle.js @@ -7,6 +7,11 @@ console.log("key", infura_apikey); console.log("mnemonic", mnemonic); module.exports = { + solc: { + optimizer: { + enabled: true + } + }, networks: { development: { host: "localhost",