From aeb0742434471c0a4cb28bebf569b0d4af6b4485 Mon Sep 17 00:00:00 2001 From: Greg Hysen Date: Thu, 15 Mar 2018 13:58:54 -0700 Subject: Implementation and tests in the protocol for cancelling all orders up to a specific time --- .../current/protocol/Exchange/Exchange.sol | 2 +- .../current/protocol/Exchange/IExchange.sol | 34 ++++++----- .../protocol/Exchange/MixinExchangeCore.sol | 28 ++++++++- .../protocol/Exchange/mixins/MExchangeCore.sol | 3 + packages/contracts/src/utils/exchange_wrapper.ts | 12 ++++ packages/contracts/src/utils/types.ts | 4 ++ packages/contracts/test/exchange/core.ts | 67 ++++++++++++++++++++++ 7 files changed, 134 insertions(+), 16 deletions(-) (limited to 'packages') diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/Exchange.sol b/packages/contracts/src/contracts/current/protocol/Exchange/Exchange.sol index e925634df..7a705a0ee 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/Exchange.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/Exchange.sol @@ -30,7 +30,7 @@ contract Exchange is MixinSettlementProxy, MixinWrapperFunctions { - string constant public VERSION = "2.0.0-alpha"; + string constant public VERSION = "2.0.1-alpha"; function Exchange( IToken _zrxToken, diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/IExchange.sol b/packages/contracts/src/contracts/current/protocol/Exchange/IExchange.sol index 8f17f9315..3a43fca9b 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/IExchange.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/IExchange.sol @@ -53,38 +53,43 @@ contract IExchange { uint256 takerTokenCancelledAmount, bytes32 indexed orderHash ); - + + event LogCancelBefore( + address indexed maker, + uint256 salt + ); + function ZRX_TOKEN_CONTRACT() public view returns (address); - + function TOKEN_TRANSFER_PROXY_CONTRACT() public view returns (address); - + function EXTERNAL_QUERY_GAS_LIMIT() public view returns (uint16); - + function VERSION() public view returns (string); - + function filled(bytes32) public view returns (uint256); - + function cancelled(bytes32) public view returns (uint256); - + /// @dev Calculates the sum of values already filled and cancelled for a given order. /// @param orderHash The Keccak-256 hash of the given order. /// @return Sum of values already filled and cancelled. function getUnavailableTakerTokenAmount(bytes32 orderHash) public view returns (uint256 unavailableTakerTokenAmount); - + /// @dev Calculates partial value given a numerator and denominator. /// @param numerator Numerator. /// @param denominator Denominator. @@ -93,7 +98,7 @@ contract IExchange { function getPartialAmount(uint256 numerator, uint256 denominator, uint256 target) public pure returns (uint256 partialAmount); - + /// @dev Checks if rounding error > 0.1%. /// @param numerator Numerator. /// @param denominator Denominator. @@ -102,7 +107,7 @@ contract IExchange { function isRoundingError(uint256 numerator, uint256 denominator, uint256 target) public pure returns (bool isError); - + /// @dev Calculates Keccak-256 hash of order with specified parameters. /// @param orderAddresses Array of order's maker, taker, makerToken, takerToken, and feeRecipient. /// @param orderValues Array of order's makerTokenAmount, takerTokenAmount, makerFee, takerFee, expirationTimestampInSec, and salt. @@ -110,7 +115,7 @@ contract IExchange { function getOrderHash(address[5] orderAddresses, uint256[6] orderValues) public view returns (bytes32 orderHash); - + /// @dev Verifies that an order signature is valid. /// @param signer address of signer. /// @param hash Signed Keccak-256 hash. @@ -126,7 +131,7 @@ contract IExchange { bytes32 s) public pure returns (bool isValid); - + /// @dev Fills the input order. /// @param orderAddresses Array of order's maker, taker, makerToken, takerToken, and feeRecipient. /// @param orderValues Array of order's makerTokenAmount, takerTokenAmount, makerFee, takerFee, expirationTimestampInSec, and salt. @@ -144,7 +149,7 @@ contract IExchange { bytes32 s) public returns (uint256 takerTokenFilledAmount); - + /// @dev Cancels the input order. /// @param orderAddresses Array of order's maker, taker, makerToken, takerToken, and feeRecipient. /// @param orderValues Array of order's makerTokenAmount, takerTokenAmount, makerFee, takerFee, expirationTimestampInSec, and salt. @@ -157,6 +162,9 @@ contract IExchange { public returns (uint256 takerTokenCancelledAmount); + /// @dev Cancels all orders for a specified maker up to a certain time. + /// @param salt Orders created with a lower salt value will be cancelled + function cancelOrdersBefore(uint256 salt) external; /// @dev Fills an order with specified parameters and ECDSA signature. Throws if specified amount not filled entirely. /// @param orderAddresses Array of order's maker, taker, makerToken, takerToken, and feeRecipient. diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/MixinExchangeCore.sol b/packages/contracts/src/contracts/current/protocol/Exchange/MixinExchangeCore.sol index 1f3108188..6d69b1787 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/MixinExchangeCore.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/MixinExchangeCore.sol @@ -43,6 +43,10 @@ contract MixinExchangeCore is mapping (bytes32 => uint256) public filled; mapping (bytes32 => uint256) public cancelled; + // Mapping of makerAddress => lowest salt an order can have in order to be fillable + // Orders with a salt less than their maker's epoch are considered cancelled + mapping (address => uint256) public makerEpoch; + event LogFill( address indexed makerAddress, address takerAddress, @@ -66,6 +70,11 @@ contract MixinExchangeCore is bytes32 indexed orderHash ); + event LogCancelBefore( + address indexed maker, + uint256 salt + ); + /* * Core exchange functions */ @@ -119,6 +128,12 @@ contract MixinExchangeCore is return 0; } + // Validate order is not cancelled + if (order.salt < makerEpoch[order.makerAddress]) { + LogError(uint8(Errors.ORDER_FULLY_FILLED_OR_CANCELLED), orderHash); + return 0; + } + // Update state filled[orderHash] = safeAdd(filled[orderHash], takerTokenFilledAmount); @@ -154,7 +169,7 @@ contract MixinExchangeCore is { // Compute the order hash bytes32 orderHash = getOrderHash(order); - + // Validate the order require(order.makerTokenAmount > 0); require(order.takerTokenAmount > 0); @@ -186,7 +201,16 @@ contract MixinExchangeCore is ); return takerTokenCancelledAmount; } - + + /// @param salt Orders created with a salt less than this value will be cancelled. + function cancelOrdersBefore(uint256 salt) + external + { + require(salt > makerEpoch[msg.sender]); // epoch must be monotonically increasing + makerEpoch[msg.sender] = salt; + LogCancelBefore(msg.sender, salt); + } + /// @dev Checks if rounding error > 0.1%. /// @param numerator Numerator. /// @param denominator Denominator. diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MExchangeCore.sol b/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MExchangeCore.sol index e40120d01..b39996995 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MExchangeCore.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MExchangeCore.sol @@ -36,4 +36,7 @@ contract MExchangeCore is LibOrder { public returns (uint256 takerTokenCancelledAmount); + function cancelOrdersBefore( + uint256 salt) + external; } diff --git a/packages/contracts/src/utils/exchange_wrapper.ts b/packages/contracts/src/utils/exchange_wrapper.ts index 5996867cb..d4206adc3 100644 --- a/packages/contracts/src/utils/exchange_wrapper.ts +++ b/packages/contracts/src/utils/exchange_wrapper.ts @@ -167,6 +167,18 @@ export class ExchangeWrapper { const tx = await this._getTxWithDecodedExchangeLogsAsync(txHash); return tx; } + public async cancelOrdersBeforeAsync( + timestamp: BigNumber, + from: string, + ): Promise { + const txHash = await this._exchange.cancelOrdersBefore.sendTransactionAsync( + timestamp, + { from }, + ); + const tx = await this._getTxWithDecodedExchangeLogsAsync(txHash); + return tx; + } + public async getOrderHashAsync(signedOrder: SignedOrder): Promise { const order = orderUtils.getOrderStruct(signedOrder); const orderHash = await this._exchange.getOrderHash.callAsync(order); diff --git a/packages/contracts/src/utils/types.ts b/packages/contracts/src/utils/types.ts index 9f874d9ec..1a924a66d 100644 --- a/packages/contracts/src/utils/types.ts +++ b/packages/contracts/src/utils/types.ts @@ -28,6 +28,10 @@ export interface BatchCancelOrders { takerTokenCancelAmounts: BigNumber[]; } +export interface CancelOrdersBefore { + timestamp: BigNumber; +} + export interface DefaultOrderParams { exchangeAddress: string; makerAddress: string; diff --git a/packages/contracts/test/exchange/core.ts b/packages/contracts/test/exchange/core.ts index 1612ecdcc..3b787953b 100644 --- a/packages/contracts/test/exchange/core.ts +++ b/packages/contracts/test/exchange/core.ts @@ -741,4 +741,71 @@ describe('Exchange', () => { expect(errCode).to.be.equal(ExchangeContractErrs.ERROR_ORDER_EXPIRED); }); }); + + describe('cancelOrdersBefore', () => { + it('should fail to set timestamp less than existing CancelBefore timestamp', async () => { + const timestamp = new BigNumber(1); + await exWrapper.cancelOrdersBeforeAsync(timestamp, makerAddress); + const lesser_timestamp = new BigNumber(0); + return expect( + exWrapper.cancelOrdersBeforeAsync(lesser_timestamp, makerAddress), + ).to.be.rejectedWith(constants.REVERT); + }); + + it('should fail to set timestamp equal to existing CancelBefore timestamp', async () => { + const timestamp = new BigNumber(1); + await exWrapper.cancelOrdersBeforeAsync(timestamp, makerAddress); + return expect( + exWrapper.cancelOrdersBeforeAsync(timestamp, makerAddress), + ).to.be.rejectedWith(constants.REVERT); + }); + + it('should cancel only orders with a timestamp less than CancelBefore timestamp', async () => { + // Cancel all transactions with a timestamp less than 1 + const timestamp = new BigNumber(1); + await exWrapper.cancelOrdersBeforeAsync(timestamp, makerAddress); + + // Create 3 orders with timestamps 0,1,2 + // Since we cancelled with timestamp=1, orders with timestamp<1 will not be processed + balances = await dmyBalances.getAsync(); + const signedOrders = await Promise.all([ + orderFactory.newSignedOrder({ + makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(17), 18), + takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(17), 18), + salt: new BigNumber(0)}), + orderFactory.newSignedOrder({ + makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(97), 18), + takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(97), 18), + salt: new BigNumber(1)}), + orderFactory.newSignedOrder({ + makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(979), 18), + takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(979), 18), + salt: new BigNumber(2)}), + ]); + await exWrapper.batchFillOrdersNoThrowAsync(signedOrders, takerAddress); + + const newBalances = await dmyBalances.getAsync(); + const fillMakerTokenAmount = signedOrders[1].makerTokenAmount.add(signedOrders[2].makerTokenAmount); + const fillTakerTokenAmount = signedOrders[1].takerTokenAmount.add(signedOrders[2].takerTokenAmount); + const makerFeeAmount = signedOrders[1].makerFeeAmount.add(signedOrders[2].makerFeeAmount); + const takerFeeAmount = signedOrders[1].takerFeeAmount.add(signedOrders[2].takerFeeAmount); + expect(newBalances[makerAddress][signedOrders[2].makerTokenAddress]).to.be.bignumber.equal( + balances[makerAddress][signedOrders[2].makerTokenAddress].minus(fillMakerTokenAmount), + ); + expect(newBalances[makerAddress][signedOrders[2].takerTokenAddress]).to.be.bignumber.equal( + balances[makerAddress][signedOrders[2].takerTokenAddress].add(fillTakerTokenAmount), + ); + expect(newBalances[makerAddress][zrx.address]).to.be.bignumber.equal(balances[makerAddress][zrx.address].minus(makerFeeAmount)); + expect(newBalances[takerAddress][signedOrders[2].takerTokenAddress]).to.be.bignumber.equal( + balances[takerAddress][signedOrders[2].takerTokenAddress].minus(fillTakerTokenAmount), + ); + expect(newBalances[takerAddress][signedOrders[2].makerTokenAddress]).to.be.bignumber.equal( + balances[takerAddress][signedOrders[2].makerTokenAddress].add(fillMakerTokenAmount), + ); + expect(newBalances[takerAddress][zrx.address]).to.be.bignumber.equal(balances[takerAddress][zrx.address].minus(takerFeeAmount)); + expect(newBalances[feeRecipientAddress][zrx.address]).to.be.bignumber.equal( + balances[feeRecipientAddress][zrx.address].add(makerFeeAmount.add(takerFeeAmount)), + ); + }); + }); }); // tslint:disable-line:max-file-line-count -- cgit v1.2.3