diff --git a/contracts/core/extensions/CoreIssuanceOrder.sol b/contracts/core/extensions/CoreIssuanceOrder.sol index 40edbd7e7..b9642382e 100644 --- a/contracts/core/extensions/CoreIssuanceOrder.sol +++ b/contracts/core/extensions/CoreIssuanceOrder.sol @@ -24,6 +24,9 @@ import { CoreModifiers } from "../lib/CoreSharedModifiers.sol"; import { CoreState } from "../lib/CoreState.sol"; import { ExchangeHandler } from "../lib/ExchangeHandler.sol"; import { ICoreIssuance } from "../interfaces/ICoreIssuance.sol"; +import { IExchange } from "../interfaces/IExchange.sol"; +import { ITransferProxy } from "../interfaces/ITransferProxy.sol"; +import { IVault } from "../interfaces/IVault.sol"; import { ISetToken } from "../interfaces/ISetToken.sol"; import { LibBytes } from "../../external/0x/LibBytes.sol"; import { OrderLibrary } from "../lib/OrderLibrary.sol"; @@ -72,18 +75,20 @@ contract CoreIssuanceOrder is function fillOrder( address[5] _addresses, uint[5] _values, + address[] _requiredComponents, + uint[] _requiredComponentAmounts, uint _fillQuantity, uint8 _v, bytes32[] sigBytes, bytes _orderData ) external - isValidSet(_addresses[0]) - isPositiveQuantity(_fillQuantity) { OrderLibrary.IssuanceOrder memory order = OrderLibrary.IssuanceOrder({ setAddress: _addresses[0], quantity: _values[0], + requiredComponents: _requiredComponents, + requiredComponentAmounts: _requiredComponentAmounts, makerAddress: _addresses[1], makerToken: _addresses[2], makerTokenAmount: _values[1], @@ -94,7 +99,9 @@ contract CoreIssuanceOrder is salt: _values[4], orderHash: OrderLibrary.generateOrderHash( _addresses, - _values + _values, + _requiredComponents, + _requiredComponentAmounts ) }); @@ -116,26 +123,15 @@ contract CoreIssuanceOrder is _fillQuantity ); - // Execute exchange orders - executeExchangeOrders(_orderData); - - // Check to make sure open order amount equals _fillQuantity - uint closedOrderAmount = state.orderFills[order.orderHash].add(state.orderCancels[order.orderHash]); - uint openOrderAmount = order.quantity.sub(closedOrderAmount); - require( - openOrderAmount >= _fillQuantity, - INVALID_FILL_AMOUNT - ); - - // Tally fill in orderFills mapping - state.orderFills[order.orderHash] = state.orderFills[order.orderHash].add(_fillQuantity); + // Settle Order + settleOrder(order, _fillQuantity, _orderData); //Issue Set - issueInternal( - order.makerAddress, - order.setAddress, - _fillQuantity - ); + // issueInternal( + // order.makerAddress, + // order.setAddress, + // _fillQuantity + // ); } /** @@ -148,6 +144,8 @@ contract CoreIssuanceOrder is function cancelOrder( address[5] _addresses, uint[5] _values, + address[] _requiredComponents, + uint[] _requiredComponentAmounts, uint _cancelQuantity ) external @@ -156,6 +154,8 @@ contract CoreIssuanceOrder is OrderLibrary.IssuanceOrder memory order = OrderLibrary.IssuanceOrder({ setAddress: _addresses[0], quantity: _values[0], + requiredComponents: _requiredComponents, + requiredComponentAmounts: _requiredComponentAmounts, makerAddress: _addresses[1], makerToken: _addresses[2], makerTokenAmount: _values[1], @@ -166,7 +166,9 @@ contract CoreIssuanceOrder is salt: _values[4], orderHash: OrderLibrary.generateOrderHash( _addresses, - _values + _values, + _requiredComponents, + _requiredComponentAmounts ) }); @@ -198,11 +200,14 @@ contract CoreIssuanceOrder is * @param _orderData Bytes array containing the exchange orders to execute */ function executeExchangeOrders( - bytes _orderData + bytes _orderData, + address _makerAddress ) private + returns (uint256) { uint256 scannedBytes; + uint256 makerTokenUsed; while (scannedBytes < _orderData.length) { // Read the next exchange order header bytes memory headerData = LibBytes.slice( @@ -231,19 +236,28 @@ contract CoreIssuanceOrder is scannedBytes.add(exchangeDataLength) ); - // TODO: Transfer header.makerToken to Exchange + // Transfer header.makerTokenAmount to Exchange Wrapper + ITransferProxy(state.transferProxyAddress).transfer( + header.makerTokenAddress, + header.makerTokenAmount, + _makerAddress, + exchange + ); - // TODO: Call Exchange + //Call Exchange + //IExchange(header.exchange).exchange(orderBody); // Update scanned bytes with header and body lengths scannedBytes = scannedBytes.add(exchangeDataLength); + makerTokenUsed += header.makerTokenAmount; } + return makerTokenUsed; } /** * Validate order params are still valid * - * @param _order IssuanceOrder object containing order params + * @param _order IssuanceOrder object containing order params * @param _executeQuantity Quantity of Set to be filled */ function validateOrder( @@ -252,6 +266,8 @@ contract CoreIssuanceOrder is ) private view + isValidSet(_order.setAddress) + isPositiveQuantity(_executeQuantity) { // Make sure makerTokenAmount and Set Token to issue is greater than 0. require( @@ -276,4 +292,96 @@ contract CoreIssuanceOrder is INVALID_QUANTITY ); } + + function settleAccounts( + OrderLibrary.IssuanceOrder _order, + uint _fillQuantity, + uint _requiredMakerTokenAmount, + uint _makerTokenUsed + ) + private + { + // Calculate amount to send to taker + uint toTaker = _requiredMakerTokenAmount.sub(_makerTokenUsed); + + // Send left over maker token balance to taker + ITransferProxy(state.transferProxyAddress).transfer( + _order.makerToken, + toTaker, + _order.makerAddress, + msg.sender + ); + + // Calculate fees required + uint requiredFees = _order.relayerTokenAmount.mul(_fillQuantity).div(_order.quantity); + + //Send fees to relayer + ITransferProxy(state.transferProxyAddress).transfer( + _order.relayerToken, + requiredFees, + _order.makerAddress, + _order.relayerAddress + ); + ITransferProxy(state.transferProxyAddress).transfer( + _order.relayerToken, + requiredFees, + msg.sender, + _order.relayerAddress + ); + } + + function settleOrder( + OrderLibrary.IssuanceOrder _order, + uint _fillQuantity, + bytes _orderData + ) + private + { + // Check to make sure open order amount equals _fillQuantity + uint closedOrderAmount = state.orderFills[_order.orderHash].add(state.orderCancels[_order.orderHash]); + uint openOrderAmount = _order.quantity.sub(closedOrderAmount); + require( + openOrderAmount >= _fillQuantity, + INVALID_FILL_AMOUNT + ); + + uint[] memory requiredBalances = new uint[](_order.requiredComponents.length); + + // Calculate amount of maker token required + // Look into rounding errors + uint requiredMakerTokenAmount = _order.makerTokenAmount.mul(_fillQuantity).div(_order.quantity); + + // Calculate amount of component tokens required to issue + for (uint16 i = 0; i < _order.requiredComponents.length; i++) { + // Get current vault balances + uint tokenBalance = IVault(state.vaultAddress).getOwnerBalance( + _order.makerAddress, + _order.requiredComponents[i] + ); + + // Amount of component tokens to be added to Vault + uint requiredAddition = _order.requiredComponentAmounts[i].mul(_fillQuantity).div(_order.quantity); + + // Required vault balances after exchange order executed + requiredBalances[i] = tokenBalance.add(requiredAddition); + } + + // Execute exchange orders + uint makerTokenAmountUsed = executeExchangeOrders(_orderData, _order.makerAddress); + require(makerTokenAmountUsed <= requiredMakerTokenAmount); + + // Check that maker's component tokens in Vault have been incremented correctly + for (i = 0; i < _order.requiredComponents.length; i++) { + uint currentBal = IVault(state.vaultAddress).getOwnerBalance( + _order.makerAddress, + _order.requiredComponents[i] + ); + //require(currentBal >= requiredBalances[i]); + } + + settleAccounts(_order, _fillQuantity, requiredMakerTokenAmount, makerTokenAmountUsed); + + // Tally fill in orderFills mapping + state.orderFills[_order.orderHash] = state.orderFills[_order.orderHash].add(_fillQuantity); + } } diff --git a/contracts/core/lib/OrderLibrary.sol b/contracts/core/lib/OrderLibrary.sol index 3976d9b6a..aafb282ee 100644 --- a/contracts/core/lib/OrderLibrary.sol +++ b/contracts/core/lib/OrderLibrary.sol @@ -29,17 +29,36 @@ library OrderLibrary { /* ============ Structs ============ */ + /** + * Struct containing all parameters for the issuance order + * + * @param setAddress Set the maker wants to mint + * @param quantity Amount of Sets maker is looking to mint + * @param requiredComponents Components to be acquired by taker's exchange orders + * @param requiredComponentAmounts Amounts of each component to be acquired by exchange order + * @param makerAddress Address of maker of the Issuance Order + * @param makerToken Address of token maker wants to exchange for filling issuance order + * @param makerTokenAmount Amount of makerToken to be used to fill the order + * @param expiration Timestamp marking when the order expires + * @param relayerAddress Address of relayer + * @param relayerTokenAmount Token relayer wants to be compensated in + * @param relayerTokenAmount Amount of tokens relayer wants to be compensated + * @param salt Random number used to create unique orderHash + * @param orderHash Unique order identifier used to log information about the order in the protocol + */ struct IssuanceOrder { - address setAddress; // _addresses[0] - uint256 quantity; // _values[0] - address makerAddress; // _addresses[1] - address makerToken; // _addresses[2] - uint256 makerTokenAmount; // _values[1] - uint256 expiration; // _values[2] - address relayerAddress; // _addresses[3] - address relayerToken; // _addresses[4] - uint256 relayerTokenAmount; // _values[3] - uint256 salt; // _values[4] + address setAddress; // _addresses[0] + uint256 quantity; // _values[0] + address[] requiredComponents; // _requiredComponents + uint[] requiredComponentAmounts; // _requiredComponentAmounts + address makerAddress; // _addresses[1] + address makerToken; // _addresses[2] + uint256 makerTokenAmount; // _values[1] + uint256 expiration; // _values[2] + address relayerAddress; // _addresses[3] + address relayerToken; // _addresses[4] + uint256 relayerTokenAmount; // _values[3] + uint256 salt; // _values[4] bytes32 orderHash; } @@ -48,12 +67,16 @@ library OrderLibrary { /** * Create hash of order parameters * - * @param _addresses [setAddress, makerAddress, makerToken, relayerAddress, relayerToken] - * @param _values [quantity, makerTokenAmount, expiration, relayerTokenAmount, salt] + * @param _addresses [setAddress, makerAddress, makerToken, relayerAddress, relayerToken] + * @param _values [quantity, makerTokenAmount, expiration, relayerTokenAmount, salt] + * @param _requiredComponents Components to be acquired by exchange order + * @param _requiredComponentAmounts Amounts of each component to be acquired by exchange order */ function generateOrderHash( address[5] _addresses, - uint[5] _values + uint[5] _values, + address[] _requiredComponents, + uint[] _requiredComponentAmounts ) internal pure @@ -62,16 +85,18 @@ library OrderLibrary { // Hash the order parameters return keccak256( abi.encodePacked( - _addresses[0], // setAddress - _addresses[1], // makerAddress - _addresses[2], // makerToken - _addresses[3], // relayerAddress - _addresses[4], // relayerToken - _values[0], // quantity - _values[1], // makerTokenAmount - _values[2], // expiration - _values[3], // relayerTokenAmount - _values[4] // salt + _addresses[0], // setAddress + _addresses[1], // makerAddress + _addresses[2], // makerToken + _addresses[3], // relayerAddress + _addresses[4], // relayerToken + _values[0], // quantity + _values[1], // makerTokenAmount + _values[2], // expiration + _values[3], // relayerTokenAmount + _values[4], // salt + _requiredComponents, // _requiredComponents + _requiredComponentAmounts // _requiredComponentAmounts ) ); } @@ -112,5 +137,4 @@ library OrderLibrary { return recAddress == _signerAddress; } - } diff --git a/contracts/mocks/core/lib/OrderLibraryMock.sol b/contracts/mocks/core/lib/OrderLibraryMock.sol index 90ae71435..331eeb303 100644 --- a/contracts/mocks/core/lib/OrderLibraryMock.sol +++ b/contracts/mocks/core/lib/OrderLibraryMock.sol @@ -7,7 +7,9 @@ import { OrderLibrary } from "../../../core/lib/OrderLibrary.sol"; contract OrderLibraryMock { function testGenerateOrderHash( address[5] _addresses, - uint[5] _values + uint[5] _values, + address[] _requiredComponents, + uint[] _requiredComponentAmounts ) public pure @@ -15,7 +17,9 @@ contract OrderLibraryMock { { return OrderLibrary.generateOrderHash( _addresses, - _values + _values, + _requiredComponents, + _requiredComponentAmounts ); } diff --git a/test/contracts/core/extensions/coreIssuanceOrder.spec.ts b/test/contracts/core/extensions/coreIssuanceOrder.spec.ts index 802a87832..6c9864477 100644 --- a/test/contracts/core/extensions/coreIssuanceOrder.spec.ts +++ b/test/contracts/core/extensions/coreIssuanceOrder.spec.ts @@ -91,7 +91,7 @@ contract("CoreIssuanceOrder", (accounts) => { let subjectExchangeOrdersData: Bytes32; const naturalUnit: BigNumber = ether(2); - let components: StandardTokenMockContract[] = []; + let deployedTokens: StandardTokenMockContract[] = []; let componentUnits: BigNumber[]; let setToken: SetTokenContract; @@ -100,18 +100,34 @@ contract("CoreIssuanceOrder", (accounts) => { let signerAddress: Address; let relayerAddress: Address; let componentAddresses: Address[]; + let defaultComponentAmounts: BigNumber[]; + let requiredComponents: Address[]; + let requiredComponentAmounts: BigNumber[]; let orderQuantity: BigNumber; + let makerToken: StandardTokenMockContract; + let relayerToken: StandardTokenMockContract; let makerTokenAmount: BigNumber; + let relayerTokenAmount: BigNumber = ether(1); let timeToExpiration: number; + let orderCount: number; + let orderMakerTokenAmounts: number[]; + let issuanceOrderParams: any; beforeEach(async () => { - components = await erc20Wrapper.deployTokensAsync(2, signerAccount); //For current purposes issue to maker/signer - await erc20Wrapper.approveTransfersAsync(components, transferProxy.address, signerAccount); - - componentAddresses = _.map(components, (token) => token.address); - componentUnits = _.map(components, () => ether(4)); // Multiple of naturalUnit + deployedTokens = await erc20Wrapper.deployTokensAsync(4, ownerAccount); + await erc20Wrapper.approveTransfersAsync(deployedTokens, transferProxy.address, ownerAccount); + await erc20Wrapper.approveTransfersAsync(deployedTokens, transferProxy.address, signerAccount); + await erc20Wrapper.approveTransfersAsync(deployedTokens, transferProxy.address, takerAccount); + + // Give taker all tokens + await erc20Wrapper.transferTokensAsync(deployedTokens, takerAccount, DEPLOYED_TOKEN_QUANTITY.div(2), ownerAccount); + // Give maker their maker and relayer tokens + await erc20Wrapper.transferTokensAsync(deployedTokens.slice(2,4), signerAccount, DEPLOYED_TOKEN_QUANTITY.div(2), ownerAccount); + + componentAddresses = _.map(deployedTokens.slice(0, 2), (token) => token.address); + componentUnits = _.map(deployedTokens.slice(0, 2), () => ether(4)); // Multiple of naturalUnit setToken = await coreWrapper.createSetTokenAsync( core, setTokenFactory.address, @@ -120,21 +136,32 @@ contract("CoreIssuanceOrder", (accounts) => { naturalUnit, ); + defaultComponentAmounts = _.map(componentUnits, (unit) => unit.mul(orderQuantity || ether(4))); + await coreWrapper.registerDefaultExchanges(core); relayerAddress = relayerAccount; + makerToken = deployedTokens[2]; + relayerToken = deployedTokens[3]; issuanceOrderParams = await generateFillOrderParameters( setAddress || setToken.address, signerAddress || signerAccount, makerAddress || signerAccount, - componentAddresses[0], + requiredComponents || componentAddresses, + requiredComponentAmounts || defaultComponentAmounts, + makerToken.address, relayerAddress, + relayerToken.address, orderQuantity || ether(4), makerTokenAmount || ether(10), timeToExpiration || 10, ); - subjectExchangeOrdersData = generateOrdersDataForOrderCount(3); + subjectExchangeOrdersData = generateOrdersDataForOrderCount( + orderCount || 3, + makerToken.address, + orderMakerTokenAmounts || [3, 3, 3], + ); subjectCaller = takerAccount; subjectQuantityToIssue = ether(4); }); @@ -143,6 +170,8 @@ contract("CoreIssuanceOrder", (accounts) => { return core.fillOrder.sendTransactionAsync( issuanceOrderParams.addresses, issuanceOrderParams.values, + issuanceOrderParams.requiredComponents, + issuanceOrderParams.requiredComponentAmounts, subjectQuantityToIssue, issuanceOrderParams.signature.v, [issuanceOrderParams.signature.r, issuanceOrderParams.signature.s], @@ -151,37 +180,50 @@ contract("CoreIssuanceOrder", (accounts) => { ); } - afterEach(async () => { - setAddress = undefined; - signerAddress = undefined; - makerAddress = undefined; - orderQuantity = undefined; - makerTokenAmount = undefined; - timeToExpiration = undefined; - }); + it("transfers the full maker token amount from the maker", async () => { + const existingBalance = await makerToken.balanceOf.callAsync(signerAccount); + assertTokenBalance(makerToken, DEPLOYED_TOKEN_QUANTITY.div(2), signerAccount); + + await subject(); - it("transfers the required tokens from the user", async () => { - const component: StandardTokenMockContract = _.first(components); - const unit: BigNumber = _.first(componentUnits); + const fullMakerTokenAmount = ether(10); + const newBalance = await makerToken.balanceOf.callAsync(signerAccount); + const expectedNewBalance = existingBalance.sub(fullMakerTokenAmount); + assertTokenBalance(makerToken, expectedNewBalance, signerAccount); + }); - const existingBalance = await component.balanceOf.callAsync(signerAccount); - assertTokenBalance(component, DEPLOYED_TOKEN_QUANTITY, signerAccount); + it("transfers the remaining maker tokens to the taker", async () => { + const existingBalance = await makerToken.balanceOf.callAsync(subjectCaller); + assertTokenBalance(makerToken, DEPLOYED_TOKEN_QUANTITY.div(2), subjectCaller); await subject(); - const newBalance = await component.balanceOf.callAsync(signerAccount); - const expectedNewBalance = existingBalance.sub(subjectQuantityToIssue.div(naturalUnit).mul(unit)); - expect(newBalance).to.be.bignumber.equal(expectedNewBalance); + const testMakerTokenAmount = ether(10); // makerTokenAmount + const sumTestOrderMakerTokenAmounts = ether(9); // Sum orderMakerTokenAmounts + + const netMakerToTaker = testMakerTokenAmount.sub(sumTestOrderMakerTokenAmounts); + const expectedNewBalance = existingBalance.plus(netMakerToTaker); + assertTokenBalance(makerToken, expectedNewBalance, subjectCaller); }); - it("mints the correct quantity of the set for the user", async () => { - const existingBalance = await setToken.balanceOf.callAsync(signerAccount); + it("transfers the fees to the relayer", async () => { + const existingBalance = await relayerToken.balanceOf.callAsync(relayerAddress); + assertTokenBalance(relayerToken, ZERO, relayerAddress); await subject(); - assertTokenBalance(setToken, existingBalance.add(subjectQuantityToIssue), signerAccount); + const expectedNewBalance = relayerTokenAmount.mul(2); + assertTokenBalance(relayerToken, expectedNewBalance, relayerAddress); }); + // it("mints the correct quantity of the set for the maker", async () => { + // const existingBalance = await setToken.balanceOf.callAsync(signerAccount); + + // await subject(); + + // assertTokenBalance(setToken, existingBalance.add(subjectQuantityToIssue), signerAccount); + // }); + it("marks the correct amount as filled in orderFills mapping", async () => { const preFilled = await core.orderFills.callAsync(issuanceOrderParams.orderHash); expect(preFilled).to.be.bignumber.equal(ZERO); @@ -193,18 +235,63 @@ contract("CoreIssuanceOrder", (accounts) => { }); describe("when the fill size is less than the order quantity", async () => { + before(async () => { + orderMakerTokenAmounts = [1, 1, 1]; + }); + beforeEach(async () => { subjectQuantityToIssue = ether(2); }); - it("mints the correct quantity of the set for the user", async () => { - const existingBalance = await setToken.balanceOf.callAsync(signerAccount); + after(async () => { + orderMakerTokenAmounts = undefined; + }); + + it("transfers the partial maker token amount from the maker", async () => { + const existingBalance = await makerToken.balanceOf.callAsync(signerAccount); + assertTokenBalance(makerToken, DEPLOYED_TOKEN_QUANTITY.div(2), signerAccount); + + await subject(); + + const partialMakerTokenAmount = ether(10).mul(subjectQuantityToIssue).div(ether(4)); + const newBalance = await makerToken.balanceOf.callAsync(signerAccount); + const expectedNewBalance = existingBalance.sub(partialMakerTokenAmount); + assertTokenBalance(makerToken, expectedNewBalance, signerAccount); + }); + + it("transfers the remaining maker tokens to the taker", async () => { + const existingBalance = await makerToken.balanceOf.callAsync(subjectCaller); + assertTokenBalance(makerToken, DEPLOYED_TOKEN_QUANTITY.div(2), subjectCaller); + + await subject(); + + const testMakerTokenAmount = ether(10); // MakerTokenAmount + const sumTestOrderMakerTokenAmounts = ether(3); // Sum orderMakerTokenAmounts + + const partialMakerTokenAmount = testMakerTokenAmount.mul(subjectQuantityToIssue).div(ether(4)) + const netMakerToTaker = partialMakerTokenAmount.sub(sumTestOrderMakerTokenAmounts); + const expectedNewBalance = existingBalance.plus(netMakerToTaker); + assertTokenBalance(makerToken, expectedNewBalance, subjectCaller); + }); + + it("transfers the partial fees to the relayer", async () => { + const existingBalance = await relayerToken.balanceOf.callAsync(relayerAddress); + assertTokenBalance(relayerToken, ZERO, relayerAddress); await subject(); - assertTokenBalance(setToken, existingBalance.add(subjectQuantityToIssue), signerAccount); + const expectedNewBalance = relayerTokenAmount.mul(2).mul(subjectQuantityToIssue).div(ether(4)); + assertTokenBalance(relayerToken, expectedNewBalance, relayerAddress); }); + // it("mints the correct quantity of the set for the user", async () => { + // const existingBalance = await setToken.balanceOf.callAsync(signerAccount); + + // await subject(); + + // assertTokenBalance(setToken, existingBalance.add(subjectQuantityToIssue), signerAccount); + // }); + it("marks the correct amount as filled in orderFills mapping", async () => { const preFilled = await core.orderFills.callAsync(issuanceOrderParams.orderHash); expect(preFilled).to.be.bignumber.equal(ZERO); @@ -216,12 +303,24 @@ contract("CoreIssuanceOrder", (accounts) => { }); }); + describe("when submitted exchange orders use more maker tokens than alloted for trades", async () => { + beforeEach(async () => { + subjectQuantityToIssue = ether(2); + }); + + it("should revert", async () => { + await expectRevertError(subject()); + }); + }); + describe("when the full fill size has been taken", async () => { beforeEach(async () => { const quantityToCancel = ether(4); await core.cancelOrder.sendTransactionAsync( issuanceOrderParams.addresses, issuanceOrderParams.values, + issuanceOrderParams.requiredComponents, + issuanceOrderParams.requiredComponentAmounts, quantityToCancel, { from: signerAccount } ); @@ -238,6 +337,8 @@ contract("CoreIssuanceOrder", (accounts) => { await core.cancelOrder.sendTransactionAsync( issuanceOrderParams.addresses, issuanceOrderParams.values, + issuanceOrderParams.requiredComponents, + issuanceOrderParams.requiredComponentAmounts, quantityToCancel, { from: signerAccount } ); @@ -273,6 +374,10 @@ contract("CoreIssuanceOrder", (accounts) => { setAddress = NULL_ADDRESS; }); + after(async () => { + setAddress = undefined; + }); + it("should revert", async () => { await expectRevertError(subject()); }); @@ -293,6 +398,10 @@ contract("CoreIssuanceOrder", (accounts) => { orderQuantity = ether(5); }); + after(async () => { + orderQuantity = undefined; + }); + it("should revert", async () => { await expectRevertError(subject()); }); @@ -303,6 +412,10 @@ contract("CoreIssuanceOrder", (accounts) => { timeToExpiration = -1; }); + after(async () => { + timeToExpiration = undefined; + }); + it("should revert", async () => { await expectRevertError(subject()); }); @@ -313,6 +426,10 @@ contract("CoreIssuanceOrder", (accounts) => { orderQuantity = ZERO; }); + after(async () => { + orderQuantity = undefined; + }); + it("should revert", async () => { await expectRevertError(subject()); }); @@ -323,6 +440,10 @@ contract("CoreIssuanceOrder", (accounts) => { makerTokenAmount = ZERO; }); + after(async () => { + makerTokenAmount = undefined; + }); + it("should revert", async () => { await expectRevertError(subject()); }); @@ -333,6 +454,10 @@ contract("CoreIssuanceOrder", (accounts) => { makerAddress = makerAccount; }); + after(async () => { + makerAddress = undefined; + }); + it("should revert", async () => { await expectRevertError(subject()); }); @@ -359,8 +484,13 @@ contract("CoreIssuanceOrder", (accounts) => { let signerAddress: Address; let relayerAddress: Address; let componentAddresses: Address[]; + let defaultComponentAmounts: BigNumber[]; + let requiredComponents: Address[]; + let requiredComponentAmounts: BigNumber[]; let orderQuantity: BigNumber; let makerTokenAmount: BigNumber; + let makerToken: StandardTokenMockContract; + let relayerToken: StandardTokenMockContract; let timeToExpiration: number; let issuanceOrderParams: any; @@ -370,7 +500,7 @@ contract("CoreIssuanceOrder", (accounts) => { signerAddress = signerAccount; relayerAddress = relayerAccount; - const components = await erc20Wrapper.deployTokensAsync(2, signerAddress); //For current purposes issue to maker/signer + const components = await erc20Wrapper.deployTokensAsync(4, signerAddress); //For current purposes issue to maker/signer await erc20Wrapper.approveTransfersAsync(components, transferProxy.address, signerAddress); componentAddresses = _.map(components, (token) => token.address); @@ -383,16 +513,23 @@ contract("CoreIssuanceOrder", (accounts) => { naturalUnit, ); + defaultComponentAmounts = _.map(componentUnits, (unit) => unit.mul(ether(4))); //ether(4) for now but will be orderQuantity await coreWrapper.registerDefaultExchanges(core); + makerToken = components[2]; + relayerToken = components[3]; + subjectCaller = signerAccount; subjectQuantityToCancel = ether(2); issuanceOrderParams = await generateFillOrderParameters( setAddress || setToken.address, signerAddress || signerAccount, makerAddress || signerAccount, - componentAddresses[0], + requiredComponents || componentAddresses, + requiredComponentAmounts || defaultComponentAmounts, + makerToken.address, relayerAddress, + relayerToken.address, orderQuantity || ether(4), makerTokenAmount || ether(10), timeToExpiration || 10, @@ -403,20 +540,13 @@ contract("CoreIssuanceOrder", (accounts) => { return core.cancelOrder.sendTransactionAsync( issuanceOrderParams.addresses, issuanceOrderParams.values, + issuanceOrderParams.requiredComponents, + issuanceOrderParams.requiredComponentAmounts, subjectQuantityToCancel, { from: subjectCaller }, ); } - afterEach(async () => { - setAddress = undefined; - signerAddress = undefined; - makerAddress = undefined; - orderQuantity = undefined; - makerTokenAmount = undefined; - timeToExpiration = undefined; - }); - it("marks the correct amount as canceled in orderCancels mapping", async () => { const preCanceled = await core.orderCancels.callAsync(issuanceOrderParams.orderHash); expect(preCanceled).to.be.bignumber.equal(ZERO); @@ -469,6 +599,10 @@ contract("CoreIssuanceOrder", (accounts) => { timeToExpiration = -1; }); + after(async () => { + timeToExpiration = undefined; + }); + it("should revert", async () => { await expectRevertError(subject()); }); @@ -489,6 +623,10 @@ contract("CoreIssuanceOrder", (accounts) => { orderQuantity = ether(5); }); + after(async () => { + orderQuantity = undefined; + }); + it("should revert", async () => { await expectRevertError(subject()); }); @@ -499,6 +637,10 @@ contract("CoreIssuanceOrder", (accounts) => { orderQuantity = ZERO; }); + after(async () => { + orderQuantity = undefined; + }); + it("should revert", async () => { await expectRevertError(subject()); }); @@ -509,6 +651,10 @@ contract("CoreIssuanceOrder", (accounts) => { makerTokenAmount = ZERO; }); + after(async () => { + makerTokenAmount = undefined; + }); + it("should revert", async () => { await expectRevertError(subject()); }); diff --git a/test/contracts/core/lib/orderLibrary.spec.ts b/test/contracts/core/lib/orderLibrary.spec.ts index 16440f80e..ac4fab77f 100644 --- a/test/contracts/core/lib/orderLibrary.spec.ts +++ b/test/contracts/core/lib/orderLibrary.spec.ts @@ -44,7 +44,8 @@ contract("OrderLibrary", (accounts) => { signerAccount, relayerAccount, mockSetTokenAccount, - mockTokenAccount + mockTokenAccount, + mockTokenAccount2, ] = accounts; let orderLib: OrderLibraryMockContract; @@ -62,6 +63,8 @@ contract("OrderLibrary", (accounts) => { let relayerAddress: Address; let orderQuantity: BigNumber; let makerTokenAmount: BigNumber; + let requiredComponents: Address[]; + let requiredComponentAmounts: BigNumber[]; let timeToExpiration: number; let issuanceOrderParams: any; @@ -74,13 +77,18 @@ contract("OrderLibrary", (accounts) => { orderQuantity = ether(4); makerTokenAmount = ether(10); timeToExpiration = 10; + requiredComponents = [mockTokenAccount, mockTokenAccount2]; + requiredComponentAmounts = [ether(2), ether(2)]; issuanceOrderParams = await generateFillOrderParameters( mockSetTokenAccount, signerAddress, signerAddress, + requiredComponents, + requiredComponentAmounts, mockTokenAccount, relayerAddress, + mockTokenAccount2, orderQuantity, makerTokenAmount, timeToExpiration, @@ -122,8 +130,9 @@ contract("OrderLibrary", (accounts) => { let relayerAddress: Address; let orderQuantity: BigNumber; let makerTokenAmount: BigNumber; + let requiredComponents: Address[]; + let requiredComponentAmounts: BigNumber[]; let timeToExpiration: number; - let issuanceOrderParams: any; beforeEach(async () => { @@ -134,13 +143,18 @@ contract("OrderLibrary", (accounts) => { orderQuantity = ether(4); makerTokenAmount = ether(10); timeToExpiration = 10; + requiredComponents = [mockTokenAccount, mockTokenAccount2]; + requiredComponentAmounts = [ether(2), ether(2)]; issuanceOrderParams = await generateFillOrderParameters( mockSetTokenAccount, signerAddress, signerAddress, + requiredComponents, + requiredComponentAmounts, mockTokenAccount, relayerAddress, + mockTokenAccount2, orderQuantity, makerTokenAmount, timeToExpiration, @@ -151,11 +165,13 @@ contract("OrderLibrary", (accounts) => { return orderLib.testGenerateOrderHash.callAsync( issuanceOrderParams.addresses, issuanceOrderParams.values, + issuanceOrderParams.requiredComponents, + issuanceOrderParams.requiredComponentAmounts, { from: subjectCaller }, ); } - it("should return true", async () => { + it("off and on-chain orderHashes should match", async () => { const contractOrderHash = await subject(); expect(contractOrderHash).to.equal(issuanceOrderParams.orderHash); diff --git a/test/utils/erc20Wrapper.ts b/test/utils/erc20Wrapper.ts index 23dae22ae..9298c960f 100644 --- a/test/utils/erc20Wrapper.ts +++ b/test/utils/erc20Wrapper.ts @@ -192,6 +192,31 @@ export class ERC20Wrapper { await Promise.all(approvePromises); } + public async transferTokenAsync( + token: StandardTokenMockContract, + to: Address, + quantity: BigNumber, + from: Address = this._senderAccountAddress, + ) { + await this.transferTokensAsync([token], to, quantity, from); + } + + public async transferTokensAsync( + tokens: StandardTokenMockContract[], + to: Address, + amount: BigNumber, + from: Address = this._senderAccountAddress, + ) { + const transferPromises = _.map(tokens, (token) => + token.transfer.sendTransactionAsync( + to, + amount, + { from, gas: 100000 }, + ), + ); + await Promise.all(transferPromises); + } + public async approveInvalidTransferAsync( token: InvalidReturnTokenMockContract, to: Address, diff --git a/test/utils/ethereum-abi-arrays.ts b/test/utils/ethereum-abi-arrays.ts new file mode 100644 index 000000000..0ea3f87ed --- /dev/null +++ b/test/utils/ethereum-abi-arrays.ts @@ -0,0 +1,137 @@ +import * as ethUtil from "ethereumjs-util"; +import { BigNumber } from "bignumber.js"; +import { Address, Bytes, Bytes32, UInt, IssuanceOrder, SolidityTypes } from "../../types/common.js"; +import { bufferAndLPad32BigNumber } from "./encoding.js"; + +var ABI = function () { +} + +// Convert from short to canonical names +// FIXME: optimise or make this nicer? +function elementaryName ( + name: string, +): string { + if (name.startsWith('int[')) { + return 'int256' + name.slice(3) + } else if (name === 'int') { + return 'int256' + } else if (name.startsWith('uint[')) { + return 'uint256' + name.slice(4) + } else if (name === 'uint') { + return 'uint256' + } + return name +} + +// Parse N from type +function parseTypeN ( + type: string, +): number { + //Figure out how to mirror parseInt in typescript + return parseInt(/^\D+(\d+)$/.exec(type)[1], 10) +} + +function parseNumber ( + arg: any, +): any { + var type = typeof arg + if (type === 'number') { + return new BigNumber(arg) + } else if (type === 'object') { + // assume this is a BigNumber for the moment, replace with BN.isBN soon + return arg + } else { + throw new Error('Argument is not a supported type') + } +} + +export function solidityPack ( + types: string[], + values: any[], + ): Buffer { + if (types.length !== values.length) { + throw new Error('Number of types are not matching the values') + } + + let size: number; + let num: BigNumber; + let ret: Buffer[] = []; + let j: number; + + for (var i = 0; i < types.length; i++) { + const type = elementaryName(types[i]); + const value = values[i]; + + if (type === 'bytes') { + ret.push(value); + } else if (Array.isArray(values[i])) { + const typeArray = type.split('[')[0]; + + if(typeArray=='address') { + for(j=0;j= 1 || parseTypeN(typeArray) <= 32)) { + for(j=0;j 256)) { + throw new Error('Invalid uint width: ' + size) + } + for(j=0;j 32) { + throw new Error('Invalid bytes width: ' + size) + } + + ret.push(ethUtil.setLengthRight(value, size)) + } else if (type.startsWith('uint')) { + size = parseTypeN(type) + if ((size % 8) || (size < 8) || (size > 256)) { + throw new Error('Invalid uint width: ' + size) + } + + num = parseNumber(value) + ret.push(bufferAndLPad32BigNumber(num)) + } else if (type.startsWith('int')) { + size = parseTypeN(type) + if ((size % 8) || (size < 8) || (size > 256)) { + throw new Error('Invalid int width: ' + size) + } + + num = parseNumber(value) + ret.push(bufferAndLPad32BigNumber(num)) + } else { + // FIXME: support all other types + throw new Error('Unsupported or invalid type: ' + type) + } + } + return Buffer.concat(ret) +} + +export function soliditySHA3 ( + types: string[], + values: any[], +): Buffer { + return ethUtil.sha3(solidityPack(types, values)) +} diff --git a/test/utils/orderWrapper.ts b/test/utils/orderWrapper.ts index 3aafb558d..8fc9d46b3 100644 --- a/test/utils/orderWrapper.ts +++ b/test/utils/orderWrapper.ts @@ -1,6 +1,6 @@ import * as _ from "lodash"; import * as ethUtil from "ethereumjs-util"; -import * as ethABI from 'ethereumjs-abi'; +import { soliditySHA3 } from './ethereum-abi-arrays'; import { BigNumber } from "bignumber.js"; import BN = require('bn.js'); @@ -12,6 +12,7 @@ import { } from "../utils/constants"; import { ether } from "./units"; +import { bufferAndLPad32BigNumber } from "./encoding" function bigNumberToBN(value: BigNumber) { return new BN(value.toString(), 10); @@ -30,17 +31,17 @@ function parseSigHexAsRSV(sigHex: string): any { export function generateOrdersDataForOrderCount( orderCount: number, + makerTokenAddress: Address, + makerTokenAmounts: number[], ): Bytes32 { const exchangeOrderDatum: Buffer[] = []; _.times(orderCount, (index) => { const exchange = _.sample(EXCHANGES); exchangeOrderDatum.push(paddedBufferForData(exchange)); - const makerTokenAddress = 'someTokenAddress'; exchangeOrderDatum.push(paddedBufferForData(makerTokenAddress)); - const makerTokenAmount = _.random(200, 250); - exchangeOrderDatum.push(paddedBufferForData(makerTokenAmount)); + exchangeOrderDatum.push(bufferAndLPad32BigNumber(ether(makerTokenAmounts[index]))); const totalOrdersLength = _.random(200, 250); exchangeOrderDatum.push(paddedBufferForData(totalOrdersLength)); @@ -99,16 +100,18 @@ export function hashOrderHex( {value: order.makerToken, type: SolidityTypes.Address}, {value: order.relayerAddress, type: SolidityTypes.Address}, {value: order.relayerToken, type: SolidityTypes.Address}, - {value: bigNumberToBN(order.quantity), type: SolidityTypes.Uint256}, - {value: bigNumberToBN(order.makerTokenAmount), type: SolidityTypes.Uint256}, - {value: bigNumberToBN(order.expiration), type: SolidityTypes.Uint256}, - {value: bigNumberToBN(order.relayerTokenAmount), type: SolidityTypes.Uint256}, - {value: bigNumberToBN(order.salt), type: SolidityTypes.Uint256} + {value: order.quantity, type: SolidityTypes.Uint256}, + {value: order.makerTokenAmount, type: SolidityTypes.Uint256}, + {value: order.expiration, type: SolidityTypes.Uint256}, + {value: order.relayerTokenAmount, type: SolidityTypes.Uint256}, + {value: order.salt, type: SolidityTypes.Uint256}, + {value: order.requiredComponents, type: SolidityTypes.AddressArray}, + {value: order.requiredComponentAmounts, type: SolidityTypes.UintArray}, ] const types = _.map(orderParts, o => o.type); const values = _.map(orderParts, o => o.value); - const hashBuff = ethABI.soliditySHA3(types, values); + const hashBuff = soliditySHA3(types, values); const hashHex = ethUtil.bufferToHex(hashBuff); return hashHex; } @@ -128,8 +131,11 @@ export async function generateFillOrderParameters( setAddress: Address, signerAddress: Address, makerAddress: Address, - componentAddress: Address, + requiredComponents: Address[], + requiredComponentAmounts: BigNumber[], + makerToken: Address, relayerAddress: Address, + relayerToken: Address, quantity: BigNumber, makerTokenAmount: BigNumber, timeToExpiration: number, @@ -138,12 +144,14 @@ export async function generateFillOrderParameters( const order = { setAddress, quantity, + requiredComponents, + requiredComponentAmounts, makerAddress, - makerToken: componentAddress, + makerToken, relayerAddress, makerTokenAmount, expiration: generateTimeStamp(timeToExpiration), - relayerToken: componentAddress, + relayerToken, relayerTokenAmount: ether(1), salt: generateSalt() } as IssuanceOrder; @@ -158,5 +166,7 @@ export async function generateFillOrderParameters( values, orderHash, signature, + requiredComponents, + requiredComponentAmounts }; } diff --git a/truffle.js b/truffle.js index 02572c423..85d50a805 100644 --- a/truffle.js +++ b/truffle.js @@ -9,7 +9,8 @@ console.log("mnemonic", mnemonic); module.exports = { solc: { optimizer: { - enabled: true + enabled: true, + runs: 10000 } }, networks: { diff --git a/types/common.ts b/types/common.ts index 6e1beada3..6073a3271 100644 --- a/types/common.ts +++ b/types/common.ts @@ -32,6 +32,8 @@ export interface TxDataPayable extends TxData { export interface IssuanceOrder { setAddress: Address, quantity: BigNumber, + requiredComponents: Address[], + requiredComponentAmounts: BigNumber[], makerAddress: Address, makerToken: Address, makerTokenAmount: BigNumber, @@ -58,4 +60,6 @@ export enum SolidityTypes { Uint256 = 'uint256', Uint8 = 'uint8', Uint = 'uint', + AddressArray = 'address[]', + UintArray = 'uint256[]' }