Skip to content
This repository was archived by the owner on Jan 18, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 70 additions & 3 deletions contracts/core/extensions/CoreIssuance.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
Expand All @@ -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 ============ */

/**
Expand All @@ -131,7 +198,7 @@ contract CoreIssuance is
)
pure
internal
returns(uint)
returns (uint)
{
return _quantity.div(_naturalUnit).mul(_componentUnits);
}
Expand Down
2 changes: 1 addition & 1 deletion test/core/extensions/coreAccounting.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down
163 changes: 163 additions & 0 deletions test/core/extensions/coreIssuance.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
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 () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth it to also include a check to make doubly sure that no component tokens have been transferred to the user that weren't supposed to be transferred, purely to prevent any potential regressions. I don't feel overly strongly...could be over testing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The next test in the same block shows that they are still in the vault

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());
});
});
});
});
54 changes: 39 additions & 15 deletions test/utils/coreWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export class CoreWrapper {
this._contractOwnerAddress = contractOwnerAddress;
}

/* ============ Deployment ============ */

public async deployTransferProxyAsync(
vaultAddress: Address,
from: Address = this._tokenOwnerAddress
Expand Down Expand Up @@ -147,7 +149,7 @@ export class CoreWrapper {
);
}

// Internal
/* ============ CoreInternal Extension ============ */

public async enableFactoryAsync(
core: CoreContract,
Expand All @@ -160,7 +162,7 @@ export class CoreWrapper {
);
}

// Authorizable
/* ============ Authorizable ============ */

public async setDefaultStateAndAuthorizationsAsync(
core: CoreContract,
Expand Down Expand Up @@ -200,7 +202,7 @@ export class CoreWrapper {
);
}

// Vault
/* ============ Vault ============ */

public async incrementAccountBalanceAsync(
vault: VaultContract,
Expand Down Expand Up @@ -232,7 +234,7 @@ export class CoreWrapper {
return balances;
}

// Core
/* ============ CoreFactory Extension ============ */

public async createSetTokenAsync(
core: CoreContract,
Expand Down Expand Up @@ -264,6 +266,8 @@ export class CoreWrapper {
);
}

/* ============ CoreAccounting Extension ============ */

public async depositFromUser(
core: CoreContract,
token: Address,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions truffle.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ console.log("key", infura_apikey);
console.log("mnemonic", mnemonic);

module.exports = {
solc: {
optimizer: {
enabled: true
}
},
networks: {
development: {
host: "localhost",
Expand Down