diff --git a/contracts/core/extensions/CoreIssuanceOrder.sol b/contracts/core/extensions/CoreIssuanceOrder.sol index c0329b876..36f3b6404 100644 --- a/contracts/core/extensions/CoreIssuanceOrder.sol +++ b/contracts/core/extensions/CoreIssuanceOrder.sol @@ -45,8 +45,9 @@ contract CoreIssuanceOrder is /* ============ Constants ============ */ uint256 constant HEADER_LENGTH = 64; - + string constant INVALID_EXCHANGE = "Exchange does not exist."; + string constant INVALID_CANCEL_ORDER = "Only maker can cancel order."; string constant INVALID_SIGNATURE = "Invalid order signature."; string constant INVALID_TOKEN_AMOUNTS = "Quantity and makerTokenAmount should be greater than 0."; string constant ORDER_EXPIRED = "This order has expired."; @@ -94,7 +95,7 @@ contract CoreIssuanceOrder is ) }); - // Verify order is valid + // Verify order is valid and return amount to be filled validateOrder( order, _fillQuantity @@ -115,6 +116,10 @@ contract CoreIssuanceOrder is // Execute exchange orders executeExchangeOrders(_orderData); + // TO DO: When openOrder amount functionality added these must change + // Tally fill in orderFills mapping + state.orderFills[order.orderHash] = state.orderFills[order.orderHash].add(_fillQuantity); + //Issue Set issueInternal( order.makerAddress, @@ -123,6 +128,51 @@ contract CoreIssuanceOrder is ); } + /** + * Cancel an issuance order + * + * @param _addresses [setAddress, makerAddress, makerToken, relayerToken] + * @param _values [quantity, makerTokenAmount, expiration, relayerTokenAmount, salt] + * @param _cancelQuantity Quantity of set to be filled + */ + function cancelOrder( + address[4] _addresses, + uint[5] _values, + uint _cancelQuantity + ) + external + isPositiveQuantity(_cancelQuantity) + { + OrderLibrary.IssuanceOrder memory order = OrderLibrary.IssuanceOrder({ + setAddress: _addresses[0], + quantity: _values[0], + makerAddress: _addresses[1], + makerToken: _addresses[2], + makerTokenAmount: _values[1], + expiration: _values[2], + relayerToken: _addresses[3], + relayerTokenAmount: _values[3], + salt: _values[4], + orderHash: OrderLibrary.generateOrderHash( + _addresses, + _values + ) + }); + + // Make sure cancel order comes from maker + require(order.makerAddress == msg.sender, INVALID_CANCEL_ORDER); + + // Verify order is valid and return amount to be cancelled + validateOrder( + order, + _cancelQuantity + ); + + // TO DO: When openOrder amount functionality added these must change + // Tally cancel in orderCancels mapping + state.orderCancels[order.orderHash] = state.orderCancels[order.orderHash].add(_cancelQuantity); + } + /* ============ Private Functions ============ */ /** diff --git a/contracts/core/lib/CoreState.sol b/contracts/core/lib/CoreState.sol index 2254e3a1e..6d3b4dd74 100644 --- a/contracts/core/lib/CoreState.sol +++ b/contracts/core/lib/CoreState.sol @@ -96,4 +96,20 @@ contract CoreState { { return state.validSets[_set]; } + + function orderFills(bytes32 _orderHash) + public + view + returns(uint) + { + return state.orderFills[_orderHash]; + } + + function orderCancels(bytes32 _orderHash) + public + view + returns(uint) + { + return state.orderCancels[_orderHash]; + } } diff --git a/test/core/extensions/coreIssuanceOrder.spec.ts b/test/core/extensions/coreIssuanceOrder.spec.ts index 076544683..c9a31e00b 100644 --- a/test/core/extensions/coreIssuanceOrder.spec.ts +++ b/test/core/extensions/coreIssuanceOrder.spec.ts @@ -156,6 +156,16 @@ contract("CoreIssuanceOrder", (accounts) => { assertTokenBalance(setToken, existingBalance.add(subjectQuantityToIssue), signerAddress); }); + 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); + + await subject(); + + const filled = await core.orderFills.callAsync(issuanceOrderParams.orderHash); + expect(filled).to.be.bignumber.equal(subjectQuantityToIssue); + }); + describe("when the quantity to issue is not positive", async () => { beforeEach(async () => { subjectQuantityToIssue = ZERO; @@ -236,4 +246,112 @@ contract("CoreIssuanceOrder", (accounts) => { }); }); }); + + describe("#cancelOrder", async () => { + let subjectCaller: Address; + let subjectQuantityToCancel: BigNumber; + let subjectExchangeOrdersData: Bytes32; + + const naturalUnit: BigNumber = ether(2); + let components: StandardTokenMockContract[] = []; + let componentUnits: BigNumber[]; + let setToken: SetTokenContract; + let signerAddress: Address; + let componentAddresses: Address[]; + + let issuanceOrderParams: any; + + beforeEach(async () => { + signerAddress = signerAccount; + + components = await erc20Wrapper.deployTokensAsync(2, signerAddress); //For current purposes issue to maker/signer + await erc20Wrapper.approveTransfersAsync(components, transferProxy.address, signerAddress); + + componentAddresses = _.map(components, (token) => token.address); + componentUnits = _.map(components, () => ether(4)); // Multiple of naturalUnit + setToken = await coreWrapper.createSetTokenAsync( + core, + setTokenFactory.address, + componentAddresses, + componentUnits, + naturalUnit, + ); + + await coreWrapper.registerDefaultExchanges(core); + + subjectCaller = signerAccount; + subjectQuantityToCancel = ether(2); + issuanceOrderParams = await generateFillOrderParameters(setToken.address, signerAddress, signerAddress, componentAddresses[0]); + subjectExchangeOrdersData = generateOrdersDataForOrderCount(3); + }); + + async function subject(): Promise { + return core.cancelOrder.sendTransactionAsync( + issuanceOrderParams.addresses, + issuanceOrderParams.values, + subjectQuantityToCancel, + { from: subjectCaller }, + ); + } + + 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); + + await subject(); + + const canceled = await core.orderCancels.callAsync(issuanceOrderParams.orderHash); + expect(canceled).to.be.bignumber.equal(subjectQuantityToCancel); + }); + + describe("when the quantity to cancel is not positive", async () => { + beforeEach(async () => { + subjectQuantityToCancel = ZERO; + }); + + it("should revert", async () => { + await expectRevertError(subject()); + }); + }); + + describe("when the transaction sender is not the maker", async () => { + beforeEach(async () => { + subjectCaller = takerAccount; + }); + + it("should revert", async () => { + await expectRevertError(subject()); + }); + }); + + describe("when the order has expired", async () => { + beforeEach(async () => { + issuanceOrderParams = await generateFillOrderParameters(setToken.address, signerAddress, signerAddress, componentAddresses[0], undefined, undefined, -1) + }); + + it("should revert", async () => { + await expectRevertError(subject()); + }); + }); + + describe("when invalid Set Token quantity in Issuance Order", async () => { + beforeEach(async () => { + issuanceOrderParams = await generateFillOrderParameters(setToken.address, signerAddress, signerAddress, componentAddresses[0], ZERO) + }); + + it("should revert", async () => { + await expectRevertError(subject()); + }); + }); + + describe("when invalid makerTokenAmount in Issuance Order", async () => { + beforeEach(async () => { + issuanceOrderParams = await generateFillOrderParameters(setToken.address, signerAddress, signerAddress, componentAddresses[0], undefined, ZERO) + }); + + it("should revert", async () => { + await expectRevertError(subject()); + }); + }); + }); });