From b52606bed9ee0b2d8928198c3499907869319d26 Mon Sep 17 00:00:00 2001 From: Alexander Soong Date: Thu, 14 Jun 2018 15:01:34 -0700 Subject: [PATCH] Core issuance flow --- contracts/core/Core.sol | 135 +++++++++- contracts/core/Vault.sol | 1 + contracts/core/interfaces/ISetToken.sol | 45 ++++ contracts/core/interfaces/ITransferProxy.sol | 1 - contracts/core/interfaces/IVault.sol | 14 +- package.json | 2 +- test/authorizable.spec.ts | 2 +- test/constants/constants.ts | 4 +- test/core.spec.ts | 258 +++++++++++++++++-- test/logs/log_utils.ts | 14 +- test/setToken.spec.ts | 2 +- test/setTokenFactory.spec.ts | 2 +- test/transferProxy.spec.ts | 4 +- test/utils/coreWrapper.ts | 63 ++++- test/vault.spec.ts | 22 +- 15 files changed, 506 insertions(+), 63 deletions(-) create mode 100644 contracts/core/interfaces/ISetToken.sol diff --git a/contracts/core/Core.sol b/contracts/core/Core.sol index 06fb82b8b..dd1af7330 100644 --- a/contracts/core/Core.sol +++ b/contracts/core/Core.sol @@ -18,9 +18,11 @@ pragma solidity 0.4.24; import { ISetFactory } from "./interfaces/ISetFactory.sol"; +import { ISetToken } from "./interfaces/ISetToken.sol"; import { ITransferProxy } from "./interfaces/ITransferProxy.sol"; import { IVault } from "./interfaces/IVault.sol"; import { Ownable } from "zeppelin-solidity/contracts/ownership/Ownable.sol"; +import { SafeMath } from "zeppelin-solidity/contracts/math/SafeMath.sol"; /** @@ -34,13 +36,18 @@ import { Ownable } from "zeppelin-solidity/contracts/ownership/Ownable.sol"; contract Core is Ownable { + // Use SafeMath library for all uint256 arithmetic + using SafeMath for uint256; + /* ============ Constants ============ */ string constant ADDRESSES_MISSING = "Addresses must not be empty."; string constant BATCH_INPUT_MISMATCH = "Addresses and quantities must be the same length."; + string constant INVALID_FACTORY = "Factory is disabled or does not exist."; + string constant INVALID_QUANTITY = "Quantity must be multiple of the natural unit of the set."; + string constant INVALID_SET = "Set token is disabled or does not exist."; string constant QUANTITES_MISSING = "Quantities must not be empty."; string constant ZERO_QUANTITY = "Quantity must be greater than zero."; - string constant INVALID_FACTORY = "Factory must be tracked by Core."; /* ============ State Variables ============ */ @@ -51,12 +58,13 @@ contract Core is address public vaultAddress; // Mapping of tracked SetToken factories - mapping(address => bool) public isValidFactory; + mapping(address => bool) public validFactories; // Mapping of tracked SetTokens - mapping(address => bool) public isValidSet; + mapping(address => bool) public validSets; /* ============ Events ============ */ + event LogCreate( address indexed _setTokenAddress, address _factoryAddress, @@ -69,6 +77,15 @@ contract Core is /* ============ Modifiers ============ */ + // Validate quantity is multiple of natural unit + modifier isNaturalUnitMultiple(uint _quantity, address _setToken) { + require( + _quantity % ISetToken(_setToken).naturalUnit() == 0, + INVALID_QUANTITY + ); + _; + } + modifier isNonZero(uint _quantity) { require( _quantity > 0, @@ -77,14 +94,23 @@ contract Core is _; } - modifier isValidFactoryCheck(address _factoryAddress) { + modifier isValidFactory(address _factoryAddress) { require( - isValidFactory[_factoryAddress], + validFactories[_factoryAddress], INVALID_FACTORY ); _; } + // Verify set was created by core and is enabled + modifier isValidSet(address _setAddress) { + require( + validSets[_setAddress], + INVALID_SET + ); + _; + } + // Confirm that all inputs are valid for batch transactions modifier isValidBatchTransaction(address[] _tokenAddresses, uint[] _quantities) { // Confirm an empty _addresses array is not passed @@ -152,7 +178,7 @@ contract Core is external onlyOwner { - isValidFactory[_factoryAddress] = true; + validFactories[_factoryAddress] = true; } /** @@ -166,11 +192,81 @@ contract Core is external onlyOwner { - isValidFactory[_factoryAddress] = false; + validFactories[_factoryAddress] = false; } /* ============ Public Functions ============ */ + /** + * Issue + * + * @param _setAddress Address of set to issue + * @param _quantity Quantity of set to issue + */ + function issue( + address _setAddress, + uint _quantity + ) + public + isValidSet(_setAddress) + isNaturalUnitMultiple(_quantity, _setAddress) + { + // Fetch set token components + address[] memory components = ISetToken(_setAddress).getComponents(); + // Fetch set token component units + uint[] memory units = ISetToken(_setAddress).getUnits(); + + // Inspect vault for required component quantity + for (uint16 i = 0; i < components.length; i++) { + address component = components[i]; + uint unit = units[i]; + + // Calculate required component quantity + uint requiredComponentQuantity = calculateTransferValue( + unit, + ISetToken(_setAddress).naturalUnit(), + _quantity + ); + + // Fetch component quantity in vault + uint vaultBalance = IVault(vaultAddress).getOwnerBalance(msg.sender, component); + if (vaultBalance >= requiredComponentQuantity) { + // Decrement vault balance by the required component quantity + IVault(vaultAddress).decrementTokenOwner( + msg.sender, + component, + requiredComponentQuantity + ); + } else { + // User has less than required amount, decrement the vault by full balance + if (vaultBalance > 0) { + IVault(vaultAddress).decrementTokenOwner( + msg.sender, + component, + vaultBalance + ); + } + + // Transfer the remainder component quantity required to vault + ITransferProxy(transferProxyAddress).transferToVault( + msg.sender, + component, + requiredComponentQuantity.sub(vaultBalance) + ); + } + + // Increment the vault balance of the set token for the component + IVault(vaultAddress).incrementTokenOwner( + _setAddress, + component, + requiredComponentQuantity + ); + } + + // Issue set token + ISetToken(_setAddress).mint(msg.sender, _quantity); + } + /** * Deposit multiple tokens to the vault. Quantities should be in the * order of the addresses of the tokens being deposited. @@ -292,7 +388,7 @@ contract Core is string _symbol ) public - isValidFactoryCheck(_factoryAddress) + isValidFactory(_factoryAddress) returns (address) { // Create the Set @@ -305,7 +401,7 @@ contract Core is ); // Add Set to the list of tracked Sets - isValidSet[newSetTokenAddress] = true; + validSets[newSetTokenAddress] = true; emit LogCreate( newSetTokenAddress, @@ -319,4 +415,25 @@ contract Core is return newSetTokenAddress; } + + /* ============ Private Functions ============ */ + + /** + * Function to calculate the transfer value of a component given quantity of Set + * + * @param _componentUnits The units of the component token + * @param _naturalUnit The natural unit of the Set token + * @param _quantity The number of tokens being redeem + */ + function calculateTransferValue( + uint _componentUnits, + uint _naturalUnit, + uint _quantity + ) + pure + internal + returns(uint) + { + return _quantity.div(_naturalUnit).mul(_componentUnits); + } } diff --git a/contracts/core/Vault.sol b/contracts/core/Vault.sol index 86f95fca4..429019a28 100644 --- a/contracts/core/Vault.sol +++ b/contracts/core/Vault.sol @@ -154,6 +154,7 @@ contract Vault is } /* ============ Getter Functions ============ */ + /* * Get balance of particular contract for owner. * diff --git a/contracts/core/interfaces/ISetToken.sol b/contracts/core/interfaces/ISetToken.sol new file mode 100644 index 000000000..40269f3c9 --- /dev/null +++ b/contracts/core/interfaces/ISetToken.sol @@ -0,0 +1,45 @@ +/* + 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.24; + +/** + * @title ISetToken + * @author Set Protocol + * + * The ISetToken interface provides a light-weight, structured way to interact with the + * SetToken contract from another contract. + */ + +interface ISetToken { + function naturalUnit() + public + returns (uint); + + function getComponents() + public + returns(address[]); + + function getUnits() + public + returns(uint[]); + + function mint( + address _issuer, + uint _quantity + ) + external; +} diff --git a/contracts/core/interfaces/ITransferProxy.sol b/contracts/core/interfaces/ITransferProxy.sol index bd173f27a..bec516319 100644 --- a/contracts/core/interfaces/ITransferProxy.sol +++ b/contracts/core/interfaces/ITransferProxy.sol @@ -15,7 +15,6 @@ */ pragma solidity 0.4.24; -pragma experimental "ABIEncoderV2"; /** * @title ITransferProxy diff --git a/contracts/core/interfaces/IVault.sol b/contracts/core/interfaces/IVault.sol index ffaf33c5f..1badc38c1 100644 --- a/contracts/core/interfaces/IVault.sol +++ b/contracts/core/interfaces/IVault.sol @@ -15,7 +15,6 @@ */ pragma solidity 0.4.24; -pragma experimental "ABIEncoderV2"; /** * @title IVault @@ -71,4 +70,17 @@ interface IVault { uint _quantity ) external; + + /* + * Get balance of particular contract for owner. + * + * @param _owner The address of the token owner + * @param _tokenAddress The address of the ERC20 token + */ + function getOwnerBalance( + address _owner, + address _tokenAddress + ) + external + returns (uint256); } diff --git a/package.json b/package.json index 406cce88c..c426204d3 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "lint-ts": "tslint --fix test/*.ts", "lint-sol": "solium -d contracts/", "lint": "yarn run lint-sol && yarn run lint-ts", - "chain": "ganache-cli --networkId 70 --accounts 20 -l 7000000 -e 1000000", + "chain": "ganache-cli --networkId 70 --accounts 20 -l 10000000 -e 1000000", "coverage-setup": "cp -r transpiled/test/* test/. && cp -r transpiled/types/* types/.", "coverage-cleanup": "find test -name \\*.js* -type f -delete && find types -name \\*.js* -type f -delete", "coverage": "yarn prepare-test && yarn coverage-setup && ./node_modules/.bin/solidity-coverage && yarn coverage-cleanup" diff --git a/test/authorizable.spec.ts b/test/authorizable.spec.ts index 68031d569..664fe0090 100644 --- a/test/authorizable.spec.ts +++ b/test/authorizable.spec.ts @@ -38,7 +38,7 @@ import { expectRevertError, } from "./utils/tokenAssertions"; import { - STANDARD_INITIAL_TOKENS, + DEPLOYED_TOKEN_QUANTITY, UNLIMITED_ALLOWANCE_IN_BASE_UNITS, } from "./constants/constants"; diff --git a/test/constants/constants.ts b/test/constants/constants.ts index 82f9a6ba4..eb040d109 100644 --- a/test/constants/constants.ts +++ b/test/constants/constants.ts @@ -1,11 +1,11 @@ import { BigNumber } from "bignumber.js"; import { ether, gWei } from "../utils/units"; -export const DEFAULT_GAS = 7000000; +export const DEFAULT_GAS = 10000000; export const INVALID_OPCODE = "invalid opcode"; export const NULL_ADDRESS = "0x0000000000000000000000000000000000000000"; export const REVERT_ERROR = "revert"; -export const STANDARD_INITIAL_TOKENS: BigNumber = ether(100000000000); +export const DEPLOYED_TOKEN_QUANTITY: BigNumber = ether(100000000000); export const STANDARD_QUANTITY_ISSUED: BigNumber = ether(10); export const STANDARD_NATURAL_UNIT = ether(1); export const UNLIMITED_ALLOWANCE_IN_BASE_UNITS = new BigNumber(2).pow(256).minus(1); diff --git a/test/core.spec.ts b/test/core.spec.ts index 11148f4c7..1c309887d 100644 --- a/test/core.spec.ts +++ b/test/core.spec.ts @@ -3,6 +3,7 @@ import * as _ from "lodash"; import * as ABIDecoder from "abi-decoder"; import { BigNumber } from "bignumber.js"; +import { ether } from "./utils/units"; // Types import { Address } from "../types/common.js"; @@ -10,10 +11,11 @@ import { Address } from "../types/common.js"; // Contract types import { AuthorizableContract } from "../types/generated/authorizable"; import { CoreContract } from "../types/generated/core"; +import { SetTokenContract } from "../types/generated/set_token"; +import { SetTokenFactoryContract } from "../types/generated/set_token_factory"; import { StandardTokenMockContract } from "../types/generated/standard_token_mock"; import { TransferProxyContract } from "../types/generated/transfer_proxy"; import { VaultContract } from "../types/generated/vault"; -import { SetTokenFactoryContract } from "../types/generated/set_token_factory"; // Artifacts const Core = artifacts.require("Core"); @@ -36,10 +38,12 @@ import { expectRevertError, } from "./utils/tokenAssertions"; import { - STANDARD_INITIAL_TOKENS, - UNLIMITED_ALLOWANCE_IN_BASE_UNITS, + DEFAULT_GAS, + DEPLOYED_TOKEN_QUANTITY, NULL_ADDRESS, ONE, + STANDARD_NATURAL_UNIT, + UNLIMITED_ALLOWANCE_IN_BASE_UNITS, } from "./constants/constants"; import { getExpectedCreateLogs, @@ -94,6 +98,7 @@ contract("Core", (accounts) => { setTokenFactory = await coreWrapper.deploySetTokenFactoryAsync(); await coreWrapper.addAuthorizationAsync(setTokenFactory, core.address); + await coreWrapper.setCoreAddress(setTokenFactory, core.address); await setCoreDependencies(); }; @@ -191,7 +196,7 @@ contract("Core", (accounts) => { it("adds setTokenFactory address correctly", async () => { await subject(); - const isFactoryValid = await core.isValidFactory.callAsync(setTokenFactory.address); + const isFactoryValid = await core.validFactories.callAsync(setTokenFactory.address); expect(isFactoryValid).to.be.true; }); @@ -227,7 +232,7 @@ contract("Core", (accounts) => { it("removes setTokenFactory address correctly", async () => { await subject(); - const isFactoryValid = await core.isValidFactory.callAsync(setTokenFactory.address); + const isFactoryValid = await core.validFactories.callAsync(setTokenFactory.address); expect(isFactoryValid).to.be.false; }); @@ -253,7 +258,7 @@ contract("Core", (accounts) => { await coreWrapper.approveTransferAsync(mockToken, transferProxy.address, approver); }); - let amountToDeposit = STANDARD_INITIAL_TOKENS; + let amountToDeposit = DEPLOYED_TOKEN_QUANTITY; let depositor: Address = ownerAccount; async function subject(): Promise { @@ -303,7 +308,7 @@ contract("Core", (accounts) => { describe("when the amount is not the full balance of the token for the owner", async () => { beforeEach(async () => { - amountToDeposit = STANDARD_INITIAL_TOKENS.div(2); + amountToDeposit = DEPLOYED_TOKEN_QUANTITY.div(2); }); it("should transfer the correct amount from the vault to the withdrawer", async () => { @@ -338,7 +343,7 @@ contract("Core", (accounts) => { describe("#withdraw", async () => { const tokenOwner: Address = ownerAccount; const approver: Address = ownerAccount; - const ownerBalanceInVault: BigNumber = STANDARD_INITIAL_TOKENS; + const ownerBalanceInVault: BigNumber = DEPLOYED_TOKEN_QUANTITY; beforeEach(async () => { await deployCoreAndInitializeDependencies(); @@ -348,7 +353,7 @@ contract("Core", (accounts) => { await coreWrapper.depositFromUser(core, mockToken.address, ownerBalanceInVault); }); - let amountToWithdraw: BigNumber = STANDARD_INITIAL_TOKENS; + let amountToWithdraw: BigNumber = DEPLOYED_TOKEN_QUANTITY; let withdrawer: Address = ownerAccount; async function subject(): Promise { @@ -388,7 +393,7 @@ contract("Core", (accounts) => { describe("when the amount is not the full balance of the token for the owner", async () => { beforeEach(async () => { - amountToWithdraw = STANDARD_INITIAL_TOKENS.div(2); + amountToWithdraw = DEPLOYED_TOKEN_QUANTITY.div(2); }); it("should transfer the correct amount from the vault to the withdrawer", async () => { @@ -450,7 +455,7 @@ contract("Core", (accounts) => { // Initialize addresses to deployed tokens' addresses unless tokenAddresses is overwritten in test cases const addresses = tokenAddresses || _.map(mockTokens, (token) => token.address); // Initialize quantities to deployed tokens' quantities unless amountsToDeposit is overwritten in test cases - const quantities = amountsToDeposit || _.map(mockTokens, () => STANDARD_INITIAL_TOKENS); + const quantities = amountsToDeposit || _.map(mockTokens, () => DEPLOYED_TOKEN_QUANTITY); return core.batchDeposit.sendTransactionAsync( addresses, @@ -462,7 +467,7 @@ contract("Core", (accounts) => { it("transfers the correct amount of each token from the caller", async () => { const existingTokenBalances = await coreWrapper.getTokenBalances(mockTokens, ownerAccount); const expectedNewBalances = _.map(existingTokenBalances, (balance) => - balance.sub(STANDARD_INITIAL_TOKENS), + balance.sub(DEPLOYED_TOKEN_QUANTITY), ); await subject(); @@ -474,7 +479,7 @@ contract("Core", (accounts) => { it("transfers the correct amount of each token to the vault", async () => { const existingTokenBalances = await coreWrapper.getTokenBalances(mockTokens, vault.address); const expectedNewBalances = _.map(existingTokenBalances, (balance) => - balance.add(STANDARD_INITIAL_TOKENS), + balance.add(DEPLOYED_TOKEN_QUANTITY), ); await subject(); @@ -490,7 +495,7 @@ contract("Core", (accounts) => { ownerAccount, ); const expectedNewOwnerVaultBalances = _.map(existingOwnerVaultBalances, (balance) => - balance.add(STANDARD_INITIAL_TOKENS), + balance.add(DEPLOYED_TOKEN_QUANTITY), ); await subject(); @@ -526,7 +531,7 @@ contract("Core", (accounts) => { describe("when the token addresses input length does not match the deposit quantities input length", async () => { beforeEach(async () => { tokenAddresses = [_.first(mockTokens).address]; - amountsToDeposit = [STANDARD_INITIAL_TOKENS, STANDARD_INITIAL_TOKENS]; + amountsToDeposit = [DEPLOYED_TOKEN_QUANTITY, DEPLOYED_TOKEN_QUANTITY]; }); it("should revert", async () => { @@ -546,7 +551,7 @@ contract("Core", (accounts) => { await subject(); const newOwnerBalance = await vault.balances.callAsync(token.address, ownerAccount); - expect(newOwnerBalance).to.be.bignumber.equal(existingOwnerVaultBalance.add(STANDARD_INITIAL_TOKENS)); + expect(newOwnerBalance).to.be.bignumber.equal(existingOwnerVaultBalance.add(DEPLOYED_TOKEN_QUANTITY)); }); }); }); @@ -571,7 +576,7 @@ contract("Core", (accounts) => { // Deposit tokens first so they can be withdrawn await core.batchDeposit.sendTransactionAsync( _.map(mockTokens, (token) => token.address), - _.map(mockTokens, () => STANDARD_INITIAL_TOKENS), + _.map(mockTokens, () => DEPLOYED_TOKEN_QUANTITY), { from: ownerAccount }, ); }); @@ -588,7 +593,7 @@ contract("Core", (accounts) => { // Initialize addresses to deployed tokens' addresses unless tokenAddresses is overwritten in test cases const addresses = tokenAddresses || _.map(mockTokens, (token) => token.address); // Initialize quantites to deployed tokens' quantities unless amountsToWithdraw is overwritten in test cases - const quantities = amountsToWithdraw || _.map(mockTokens, () => STANDARD_INITIAL_TOKENS); + const quantities = amountsToWithdraw || _.map(mockTokens, () => DEPLOYED_TOKEN_QUANTITY); return core.batchWithdraw.sendTransactionAsync( addresses, @@ -600,7 +605,7 @@ contract("Core", (accounts) => { it("transfers the correct amount of each token from the caller", async () => { const existingTokenBalances = await coreWrapper.getTokenBalances(mockTokens, ownerAccount); const expectedNewBalances = _.map(existingTokenBalances, (balance) => - balance.add(STANDARD_INITIAL_TOKENS), + balance.add(DEPLOYED_TOKEN_QUANTITY), ); await subject(); @@ -612,7 +617,7 @@ contract("Core", (accounts) => { it("transfers the correct amount of each token to the vault", async () => { const existingTokenBalances = await await coreWrapper.getTokenBalances(mockTokens, vault.address); const expectedNewBalances = _.map(existingTokenBalances, (balance) => - balance.sub(STANDARD_INITIAL_TOKENS), + balance.sub(DEPLOYED_TOKEN_QUANTITY), ); await subject(); @@ -628,7 +633,7 @@ contract("Core", (accounts) => { ownerAccount, ); const expectedNewOwnerVaultBalances = _.map(existingOwnerVaultBalances, (balance) => - balance.sub(STANDARD_INITIAL_TOKENS), + balance.sub(DEPLOYED_TOKEN_QUANTITY), ); await subject(); @@ -664,7 +669,7 @@ contract("Core", (accounts) => { describe("when the token addresses input length does not match the withdraw quantities input length", async () => { beforeEach(async () => { tokenAddresses = [_.first(mockTokens).address]; - amountsToWithdraw = [STANDARD_INITIAL_TOKENS, STANDARD_INITIAL_TOKENS]; + amountsToWithdraw = [DEPLOYED_TOKEN_QUANTITY, DEPLOYED_TOKEN_QUANTITY]; }); it("should revert", async () => { @@ -684,7 +689,209 @@ contract("Core", (accounts) => { await subject(); const newOwnerBalance = await vault.balances.callAsync(token.address, ownerAccount); - expect(newOwnerBalance).to.be.bignumber.equal(existingOwnerVaultBalance.sub(STANDARD_INITIAL_TOKENS)); + expect(newOwnerBalance).to.be.bignumber.equal(existingOwnerVaultBalance.sub(DEPLOYED_TOKEN_QUANTITY)); + }); + }); + }); + + describe("#issue", async () => { + let subjectCaller: Address; + let subjectQuantityToIssue: BigNumber; + let subjectSetToIssue: Address; + + const naturalUnit: BigNumber = ether(2); + let components: StandardTokenMockContract[] = []; + let componentUnits: BigNumber[]; + let setToken: SetTokenContract; + + beforeEach(async () => { + await deployCoreAndInitializeDependencies(); + + components = await coreWrapper.deployTokensAsync(2, ownerAccount); + await coreWrapper.approveTransfersAsync(components, transferProxy.address); + + const componentAddresses = _.map(components, (token) => token.address); + componentUnits = _.map(components, () => ether(4)); // Multiple of naturalUnit + setToken = await coreWrapper.createSetTokenAsync( + core, + setTokenFactory.address, + componentAddresses, + componentUnits, + naturalUnit, + "Set Token", + "SET", + ); + + subjectCaller = ownerAccount; + subjectQuantityToIssue = ether(2); + subjectSetToIssue = setToken.address; + }); + + async function subject(): Promise { + return core.issue.sendTransactionAsync( + subjectSetToIssue, + subjectQuantityToIssue, + { from: ownerAccount }, + ); + } + + it("transfers the required tokens from the user", async () => { + const component: StandardTokenMockContract = _.first(components); + const unit: BigNumber = _.first(componentUnits); + + const existingBalance = await component.balanceOf.callAsync(ownerAccount); + assertTokenBalance(component, DEPLOYED_TOKEN_QUANTITY, ownerAccount); + + await subject(); + + const newBalance = await component.balanceOf.callAsync(ownerAccount); + const expectedNewBalance = existingBalance.sub(subjectQuantityToIssue.div(naturalUnit).mul(unit)); + expect(newBalance).to.be.bignumber.equal(expectedNewBalance); + }); + + it("updates the balances of the components in the vault to belong to the set token", async () => { + const existingBalances = await coreWrapper.getVaultBalancesForTokensForOwner( + components, + vault, + setToken.address, + ); + + await subject(); + + const expectedNewBalances = _.map(existingBalances, (balance, idx) => { + const units = componentUnits[idx]; + return balance.add(subjectQuantityToIssue.div(naturalUnit).mul(units)); + }); + const newBalances = await coreWrapper.getVaultBalancesForTokensForOwner(components, vault, setToken.address); + expect(newBalances).to.be.bignumber.eql(expectedNewBalances); + }); + + it("does not change balances of the components in the vault for the user", async () => { + const existingBalances = await coreWrapper.getVaultBalancesForTokensForOwner(components, vault, ownerAccount); + + await subject(); + + const newBalances = await coreWrapper.getVaultBalancesForTokensForOwner(components, vault, ownerAccount); + expect(newBalances).to.be.bignumber.eql(existingBalances); + }); + + it("mints the correct quantity of the set for the user", async () => { + const existingBalance = await setToken.balanceOf.callAsync(ownerAccount); + + await subject(); + + assertTokenBalance(setToken, existingBalance.add(subjectQuantityToIssue), ownerAccount); + }); + + describe("when the set was not created through core", async () => { + beforeEach(async () => { + subjectSetToIssue = NULL_ADDRESS; + }); + + it("should revert", async () => { + await expectRevertError(subject()); + }); + }); + + describe("when the user does not have enough of a component", async () => { + beforeEach(async () => { + await _.first(components).transfer.sendTransactionAsync( + otherAccount, + DEPLOYED_TOKEN_QUANTITY, + { from: ownerAccount, gas: DEFAULT_GAS }, + ); + }); + + 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 () => { + subjectQuantityToIssue = ether(3); + }); + + it("should revert", async () => { + await expectRevertError(subject()); + }); + }); + + describe("when a required component quantity is in the vault for the user", async () => { + let alreadyDepositedComponent: StandardTokenMockContract; + const alreadyDepositedQuantity: BigNumber = DEPLOYED_TOKEN_QUANTITY; + let componentUnit: BigNumber; + + beforeEach(async () => { + alreadyDepositedComponent = _.first(components); + componentUnit = _.first(componentUnits); + await coreWrapper.depositFromUser(core, alreadyDepositedComponent.address, alreadyDepositedQuantity); + }); + + it("updates the vault balance of the component for the user by the correct amount", async () => { + const existingVaultBalance = await vault.balances.callAsync(alreadyDepositedComponent.address, ownerAccount); + + await subject(); + + const requiredQuantityToIssue = subjectQuantityToIssue.div(naturalUnit).mul(componentUnit); + const expectedNewBalance = alreadyDepositedQuantity.sub(requiredQuantityToIssue); + const newVaultBalance = await vault.balances.callAsync(alreadyDepositedComponent.address, ownerAccount); + expect(newVaultBalance).to.be.bignumber.equal(expectedNewBalance); + }); + + it("mints the correct quantity of the set for the user", async () => { + const existingBalance = await setToken.balanceOf.callAsync(ownerAccount); + + await subject(); + + assertTokenBalance(setToken, existingBalance.add(subjectQuantityToIssue), ownerAccount); + }); + }); + + describe("when half of a required component quantity is in the vault for the user", async () => { + let alreadyDepositedComponent: StandardTokenMockContract; + let alreadyDepositedQuantity: BigNumber; + let componentUnit: BigNumber; + let quantityToTransfer: BigNumber; + + beforeEach(async () => { + alreadyDepositedComponent = _.first(components); + componentUnit = _.first(componentUnits); + + alreadyDepositedQuantity = subjectQuantityToIssue.div(naturalUnit).mul(componentUnit).div(2); + await coreWrapper.depositFromUser(core, alreadyDepositedComponent.address, alreadyDepositedQuantity); + + quantityToTransfer = subjectQuantityToIssue.div(naturalUnit).mul(componentUnit).sub(alreadyDepositedQuantity); + }); + + it("transfers the correct amount from the user", async () => { + const existingBalance = await alreadyDepositedComponent.balanceOf.callAsync(ownerAccount); + const expectedExistingBalance = DEPLOYED_TOKEN_QUANTITY.sub(alreadyDepositedQuantity); + assertTokenBalance(alreadyDepositedComponent, expectedExistingBalance, ownerAccount); + + await subject(); + + const expectedNewBalance = existingBalance.sub(quantityToTransfer); + const newBalance = await alreadyDepositedComponent.balanceOf.callAsync(ownerAccount); + expect(newBalance).to.be.bignumber.equal(expectedNewBalance); + }); + + it("updates the vault balance of the component for the user by the correct amount", async () => { + const existingVaultBalance = await vault.balances.callAsync(alreadyDepositedComponent.address, ownerAccount); + + await subject(); + + const expectedNewBalance = await existingVaultBalance.sub(alreadyDepositedQuantity); + const newVaultBalance = await vault.balances.callAsync(alreadyDepositedComponent.address, ownerAccount); + expect(newVaultBalance).to.be.bignumber.eql(expectedNewBalance); + }); + + it("mints the correct quantity of the set for the user", async () => { + const existingBalance = await setToken.balanceOf.callAsync(ownerAccount); + + await subject(); + + assertTokenBalance(setToken, existingBalance.add(subjectQuantityToIssue), ownerAccount); }); }); }); @@ -724,8 +931,8 @@ contract("Core", (accounts) => { const logs = await getFormattedLogsFromTxHash(txHash); const newSetTokenAddress = extractNewSetTokenAddressFromLogs(logs); - const setTokenIsValid = await core.isValidSet.callAsync(newSetTokenAddress); - expect(setTokenIsValid).to.be.true; + const isSetTokenValid = await core.validSets.callAsync(newSetTokenAddress); + expect(isSetTokenValid).to.be.true; }); it("should have the correct logs", async () => { @@ -756,6 +963,5 @@ contract("Core", (accounts) => { await expectRevertError(subject()); }); }); - }); }); diff --git a/test/logs/log_utils.ts b/test/logs/log_utils.ts index 826bff95e..5048cd3ac 100644 --- a/test/logs/log_utils.ts +++ b/test/logs/log_utils.ts @@ -45,7 +45,19 @@ export function formatLogEntry(logs: ABIDecoder.DecodedLog): Log { // Loop through each event and add to args _.each(events, (event) => { const { name, type, value } = event; - args[name] = (/^(uint)\d*/.test(type)) ? new BigNumber(value.toString()) : value; + + var argValue: any = value; + switch (true) { + case (/^(uint)\d*\[\]/.test(type)): { + break; + } + case (/^(uint)\d*/.test(type)): { + argValue = new BigNumber(value.toString()); + break; + } + } + + args[name] = argValue; }); return { diff --git a/test/setToken.spec.ts b/test/setToken.spec.ts index 3a8ecba58..fe3406e1b 100644 --- a/test/setToken.spec.ts +++ b/test/setToken.spec.ts @@ -45,10 +45,10 @@ import { expectRevertError, } from "./utils/tokenAssertions"; import { + DEPLOYED_TOKEN_QUANTITY, INVALID_OPCODE, NULL_ADDRESS, REVERT_ERROR, - STANDARD_INITIAL_TOKENS, STANDARD_NATURAL_UNIT, STANDARD_QUANTITY_ISSUED, UNLIMITED_ALLOWANCE_IN_BASE_UNITS, diff --git a/test/setTokenFactory.spec.ts b/test/setTokenFactory.spec.ts index 44691049b..0a7e5a0db 100644 --- a/test/setTokenFactory.spec.ts +++ b/test/setTokenFactory.spec.ts @@ -26,7 +26,7 @@ ChaiSetup.configure(); const { expect, assert } = chai; import { assertTokenBalance, expectRevertError } from "./utils/tokenAssertions"; -import { STANDARD_INITIAL_TOKENS, ZERO } from "./constants/constants"; +import { DEPLOYED_TOKEN_QUANTITY, ZERO } from "./constants/constants"; contract("SetTokenFactory", (accounts) => { const [ diff --git a/test/transferProxy.spec.ts b/test/transferProxy.spec.ts index 3707d04eb..e7635c47d 100644 --- a/test/transferProxy.spec.ts +++ b/test/transferProxy.spec.ts @@ -27,7 +27,7 @@ ChaiSetup.configure(); const { expect, assert } = chai; import { assertTokenBalance, expectRevertError } from "./utils/tokenAssertions"; -import { STANDARD_INITIAL_TOKENS } from "./constants/constants"; +import { DEPLOYED_TOKEN_QUANTITY } from "./constants/constants"; contract("TransferProxy", (accounts) => { const [ @@ -100,7 +100,7 @@ contract("TransferProxy", (accounts) => { }); // Subject - const amountToTransfer: BigNumber = STANDARD_INITIAL_TOKENS; + const amountToTransfer: BigNumber = DEPLOYED_TOKEN_QUANTITY; let tokenAddress: Address; async function subject(): Promise { diff --git a/test/utils/coreWrapper.ts b/test/utils/coreWrapper.ts index bc1ba98c6..832b348cd 100644 --- a/test/utils/coreWrapper.ts +++ b/test/utils/coreWrapper.ts @@ -17,7 +17,7 @@ import { Address } from "../../types/common.js"; import { DEFAULT_GAS, - STANDARD_INITIAL_TOKENS, + DEPLOYED_TOKEN_QUANTITY, UNLIMITED_ALLOWANCE_IN_BASE_UNITS, } from "../constants/constants"; @@ -32,6 +32,9 @@ const StandardTokenWithFeeMock = artifacts.require("StandardTokenWithFeeMock"); const Vault = artifacts.require("Vault"); const SetToken = artifacts.require("SetToken"); +import { getFormattedLogsFromTxHash } from "../logs/log_utils"; +import { extractNewSetTokenAddressFromLogs } from "../logs/Core"; + export class CoreWrapper { private _tokenOwnerAddress: Address; @@ -50,7 +53,7 @@ export class CoreWrapper { ): Promise { const truffleMockToken = await StandardTokenMock.new( initialAccount, - STANDARD_INITIAL_TOKENS, + DEPLOYED_TOKEN_QUANTITY, "Mock Token", "MOCK", { from, gas: DEFAULT_GAS }, @@ -73,7 +76,7 @@ export class CoreWrapper { ): Promise { const truffleMockTokenWithFee = await StandardTokenWithFeeMock.new( initialAccount, - STANDARD_INITIAL_TOKENS, + DEPLOYED_TOKEN_QUANTITY, `Mock Token With Fee`, `FEE`, fee, @@ -100,7 +103,7 @@ export class CoreWrapper { const tokenMocks = _.times(tokenCount, (index) => { return StandardTokenMock.new( initialAccount, - STANDARD_INITIAL_TOKENS, + DEPLOYED_TOKEN_QUANTITY, `Component ${index}`, index, { from, gas: DEFAULT_GAS }, @@ -113,7 +116,10 @@ export class CoreWrapper { .contract(standardToken.abi) .at(standardToken.address); - mockTokens.push(new StandardTokenMockContract(tokenWeb3Contract, { from })); + mockTokens.push(new StandardTokenMockContract( + tokenWeb3Contract, + { from } + )); }); }); @@ -126,7 +132,7 @@ export class CoreWrapper { ): Promise { const truffleMockToken = await BadTokenMock.new( initialAccount, - STANDARD_INITIAL_TOKENS, + DEPLOYED_TOKEN_QUANTITY, "Mock Token Bad Balances", "BAD", { from, gas: DEFAULT_GAS }, @@ -281,6 +287,21 @@ export class CoreWrapper { ); } + public async approveTransfersAsync( + tokens: StandardTokenMockContract[], + to: Address, + from: Address = this._tokenOwnerAddress, + ) { + const approvePromises = _.map(tokens, (token) => + token.approve.sendTransactionAsync( + to, + UNLIMITED_ALLOWANCE_IN_BASE_UNITS, + { from: from }, + ), + ); + await Promise.all(approvePromises); + } + public async getTokenBalances( tokens: StandardTokenContract[], owner: Address, @@ -355,6 +376,36 @@ export class CoreWrapper { ); } + public async createSetTokenAsync( + core: CoreContract, + factory: Address, + componentAddresses: Address[], + units: BigNumber[], + naturalUnit: BigNumber, + name: string, + symbol: string, + from: Address = this._tokenOwnerAddress, + ): Promise { + const txHash = await core.create.sendTransactionAsync( + factory, + componentAddresses, + units, + naturalUnit, + name, + symbol, + { from }, // TODO: investigate how to set limit when not run with coveralls + ); + + const logs = await getFormattedLogsFromTxHash(txHash); + const setAddress = extractNewSetTokenAddressFromLogs(logs); + + return await SetTokenContract.at( + setAddress, + web3, + { from } + ); + } + // SetTokenFactory public async setCoreAddress( diff --git a/test/vault.spec.ts b/test/vault.spec.ts index e71bf491a..03802ff2f 100644 --- a/test/vault.spec.ts +++ b/test/vault.spec.ts @@ -27,7 +27,7 @@ ChaiSetup.configure(); const { expect, assert } = chai; import { assertTokenBalance, expectRevertError } from "./utils/tokenAssertions"; -import { NULL_ADDRESS, STANDARD_INITIAL_TOKENS, ZERO } from "./constants/constants"; +import { DEPLOYED_TOKEN_QUANTITY, NULL_ADDRESS, ZERO } from "./constants/constants"; contract("Vault", (accounts) => { const [ @@ -51,11 +51,11 @@ contract("Vault", (accounts) => { }); describe("#withdrawTo", async () => { - let subjectAmountToWithdraw: BigNumber = STANDARD_INITIAL_TOKENS; + let subjectAmountToWithdraw: BigNumber = DEPLOYED_TOKEN_QUANTITY; let subjectCaller: Address = authorizedAccount; let subjectTokenAddress: Address; let subjectReceiver: Address = ownerAccount; - const ownerExistingBalanceInVault: BigNumber = STANDARD_INITIAL_TOKENS; + const ownerExistingBalanceInVault: BigNumber = DEPLOYED_TOKEN_QUANTITY; beforeEach(async () => { vault = await coreWrapper.deployVaultAsync(); @@ -72,7 +72,7 @@ contract("Vault", (accounts) => { }); afterEach(async () => { - subjectAmountToWithdraw = STANDARD_INITIAL_TOKENS; + subjectAmountToWithdraw = DEPLOYED_TOKEN_QUANTITY; subjectCaller = authorizedAccount; subjectReceiver = ownerAccount; subjectTokenAddress = null; @@ -180,7 +180,7 @@ contract("Vault", (accounts) => { const tokenAddress: Address = NULL_ADDRESS; const authorized: Address = authorizedAccount; let subjectCaller: Address = authorizedAccount; - let subjectAmountToIncrement: BigNumber = STANDARD_INITIAL_TOKENS; + let subjectAmountToIncrement: BigNumber = DEPLOYED_TOKEN_QUANTITY; beforeEach(async () => { vault = await coreWrapper.deployVaultAsync(); @@ -189,7 +189,7 @@ contract("Vault", (accounts) => { afterEach(async () => { subjectCaller = authorizedAccount; - subjectAmountToIncrement = STANDARD_INITIAL_TOKENS; + subjectAmountToIncrement = DEPLOYED_TOKEN_QUANTITY; }); async function subject(): Promise { @@ -230,9 +230,9 @@ contract("Vault", (accounts) => { }); describe("#decrementTokenOwner", async () => { - const amountToIncrement: BigNumber = STANDARD_INITIAL_TOKENS; + const amountToIncrement: BigNumber = DEPLOYED_TOKEN_QUANTITY; const tokenAddress: Address = NULL_ADDRESS; - let subjectAmountToDecrement: BigNumber = STANDARD_INITIAL_TOKENS; + let subjectAmountToDecrement: BigNumber = DEPLOYED_TOKEN_QUANTITY; let subjectCaller: Address = authorizedAccount; beforeEach(async () => { @@ -248,7 +248,7 @@ contract("Vault", (accounts) => { }); afterEach(async () => { - subjectAmountToDecrement = STANDARD_INITIAL_TOKENS; + subjectAmountToDecrement = DEPLOYED_TOKEN_QUANTITY; subjectCaller = authorizedAccount; }); @@ -280,7 +280,7 @@ contract("Vault", (accounts) => { describe("when the decrementAmount is larger than balance", async () => { beforeEach(async () => { - subjectAmountToDecrement = STANDARD_INITIAL_TOKENS.add(1); + subjectAmountToDecrement = DEPLOYED_TOKEN_QUANTITY.add(1); }); it("should revert", async () => { @@ -300,7 +300,7 @@ contract("Vault", (accounts) => { }); describe("#getOwnerBalance", async () => { - const balance: BigNumber = STANDARD_INITIAL_TOKENS; + const balance: BigNumber = DEPLOYED_TOKEN_QUANTITY; let subjectCaller: Address = ownerAccount; let subjectTokenAddress: Address;