Merge pull request #714 from 0xProject/feature/contracts/cancelOrdersUpToSender

Make cancelOrdersUpTo compatible with sender abstraction
This commit is contained in:
Amir Bandeali
2018-06-19 15:23:28 -07:00
committed by GitHub
16 changed files with 600 additions and 96 deletions

View File

@@ -26,6 +26,7 @@
"ERC20Proxy",
"ERC721Proxy",
"Exchange",
"ExchangeWrapper",
"MixinAuthorizable",
"MultiSigWallet",
"MultiSigWalletWithTimeLock",

View File

@@ -33,7 +33,8 @@
"test:circleci": "yarn test"
},
"config": {
"abis": "../migrations/artifacts/2.0.0/@(AssetProxyOwner|DummyERC20Token|DummyERC721Receiver|DummyERC721Token|ERC20Proxy|ERC721Proxy|Exchange|MixinAuthorizable|MultiSigWallet|MultiSigWalletWithTimeLock|TestAssetDataDecoders|TestAssetProxyDispatcher|TestLibBytes|TestLibMem|TestLibs|TestSignatureValidator|TokenRegistry|Whitelist|WETH9|ZRXToken).json"
"abis":
"../migrations/artifacts/2.0.0/@(AssetProxyOwner|DummyERC20Token|DummyERC721Receiver|DummyERC721Token|ERC20Proxy|ERC721Proxy|Exchange|ExchangeWrapper|MixinAuthorizable|MultiSigWallet|MultiSigWalletWithTimeLock|TestAssetDataDecoders|TestAssetProxyDispatcher|TestLibBytes|TestLibMem|TestLibs|TestSignatureValidator|TokenRegistry|Whitelist|WETH9|ZRXToken).json"
},
"repository": {
"type": "git",

View File

@@ -44,32 +44,36 @@ contract MixinExchangeCore is
// Mapping of orderHash => cancelled
mapping (bytes32 => bool) 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;
// Mapping of makerAddress => senderAddress => lowest salt an order can have in order to be fillable
// Orders with specified senderAddress and with a salt less than their epoch to are considered cancelled
mapping (address => mapping (address => uint256)) public orderEpoch;
////// Core exchange functions //////
/// @dev Cancels all orders created by sender with a salt less than or equal to the specified salt value.
/// @param salt Orders created with a salt less or equal to this value will be cancelled.
function cancelOrdersUpTo(uint256 salt)
/// @dev Cancels all orders created by makerAddress with a salt less than or equal to the targetOrderEpoch
/// and senderAddress equal to msg.sender (or null address if msg.sender == makerAddress).
/// @param targetOrderEpoch Orders created with a salt less or equal to this value will be cancelled.
function cancelOrdersUpTo(uint256 targetOrderEpoch)
external
{
address makerAddress = getCurrentContextAddress();
// If this function is called via `executeTransaction`, we only update the orderEpoch for the makerAddress/msg.sender combination.
// This allows external filter contracts to add rules to how orders are cancelled via this function.
address senderAddress = makerAddress == msg.sender ? address(0) : msg.sender;
// makerEpoch is initialized to 0, so to cancelUpTo we need salt + 1
uint256 newMakerEpoch = salt + 1;
uint256 oldMakerEpoch = makerEpoch[makerAddress];
// orderEpoch is initialized to 0, so to cancelUpTo we need salt + 1
uint256 newOrderEpoch = targetOrderEpoch + 1;
uint256 oldOrderEpoch = orderEpoch[makerAddress][senderAddress];
// Ensure makerEpoch is monotonically increasing
// Ensure orderEpoch is monotonically increasing
require(
newMakerEpoch > oldMakerEpoch,
INVALID_NEW_MAKER_EPOCH
newOrderEpoch > oldOrderEpoch,
INVALID_NEW_ORDER_EPOCH
);
// Update makerEpoch
makerEpoch[makerAddress] = newMakerEpoch;
emit CancelUpTo(makerAddress, newMakerEpoch);
// Update orderEpoch
orderEpoch[makerAddress][senderAddress] = newOrderEpoch;
emit CancelUpTo(makerAddress, senderAddress, newOrderEpoch);
}
/// @dev Fills the input order.
@@ -180,7 +184,7 @@ contract MixinExchangeCore is
orderInfo.orderStatus = uint8(OrderStatus.CANCELLED);
return orderInfo;
}
if (makerEpoch[order.makerAddress] > order.salt) {
if (orderEpoch[order.makerAddress][order.senderAddress] > order.salt) {
orderInfo.orderStatus = uint8(OrderStatus.CANCELLED);
return orderInfo;
}

View File

@@ -24,9 +24,10 @@ import "../libs/LibFillResults.sol";
contract IExchangeCore {
/// @dev Cancels all orders reated by sender with a salt less than or equal to the specified salt value.
/// @param salt Orders created with a salt less or equal to this value will be cancelled.
function cancelOrdersUpTo(uint256 salt)
/// @dev Cancels all orders created by makerAddress with a salt less than or equal to the targetOrderEpoch
/// and senderAddress equal to msg.sender (or null address if msg.sender == makerAddress).
/// @param targetOrderEpoch Orders created with a salt less or equal to this value will be cancelled.
function cancelOrdersUpTo(uint256 targetOrderEpoch)
external;
/// @dev Fills the input order.

View File

@@ -36,7 +36,7 @@ contract LibExchangeErrors {
string constant SIGNATURE_UNSUPPORTED = "SIGNATURE_UNSUPPORTED"; // Signature type unsupported.
/// cancelOrdersUptTo errors ///
string constant INVALID_NEW_MAKER_EPOCH = "INVALID_NEW_MAKER_EPOCH"; // Specified salt must be greater than or equal to existing makerEpoch.
string constant INVALID_NEW_ORDER_EPOCH = "INVALID_NEW_ORDER_EPOCH"; // Specified salt must be greater than or equal to existing orderEpoch.
/// fillOrKillOrder errors ///
string constant COMPLETE_FILL_FAILED = "COMPLETE_FILL_FAILED"; // Desired takerAssetFillAmount could not be completely filled.
@@ -55,7 +55,7 @@ contract LibExchangeErrors {
string constant ASSET_PROXY_ID_MISMATCH = "ASSET_PROXY_ID_MISMATCH"; // newAssetProxyId does not match given assetProxyId.
/// Length validation errors ///
string constant LENGTH_GREATER_THAN_0_REQUIRED = "LENGTH_GREATER_THAN_0_REQUIRED"; // Byte array must have a length greater than 0.
string constant LENGTH_GREATER_THAN_0_REQUIRED = "LENGTH_GREATER_THAN_0_REQUIRED"; // Byte array must have a length greater than 0.
string constant LENGTH_0_REQUIRED = "LENGTH_1_REQUIRED"; // Byte array must have a length of 1.
string constant LENGTH_65_REQUIRED = "LENGTH_66_REQUIRED"; // Byte array must have a length of 66.
}

View File

@@ -52,7 +52,8 @@ contract MExchangeCore is
// CancelUpTo event is emitted whenever `cancelOrdersUpTo` is executed succesfully.
event CancelUpTo(
address indexed makerAddress,
uint256 makerEpoch
address indexed senderAddress,
uint256 orderEpoch
);
/// @dev Updates state with results of a fill order.

View File

@@ -0,0 +1,98 @@
/*
Copyright 2018 ZeroEx Intl.
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;
pragma experimental ABIEncoderV2;
import "../../protocol/Exchange/interfaces/IExchange.sol";
import "../../protocol/Exchange/libs/LibOrder.sol";
contract ExchangeWrapper {
// Exchange contract.
IExchange EXCHANGE;
constructor (address _exchange)
public
{
EXCHANGE = IExchange(_exchange);
}
/// @dev Fills an order using `msg.sender` as the taker.
/// @param order Order struct containing order specifications.
/// @param takerAssetFillAmount Desired amount of takerAsset to sell.
/// @param salt Arbitrary value to gaurantee uniqueness of 0x transaction hash.
/// @param orderSignature Proof that order has been created by maker.
/// @param takerSignature Proof that taker wishes to call this function with given params.
function fillOrder(
LibOrder.Order memory order,
uint256 takerAssetFillAmount,
uint256 salt,
bytes memory orderSignature,
bytes memory takerSignature
)
public
{
address takerAddress = msg.sender;
// Encode arguments into byte array.
bytes memory data = abi.encodeWithSelector(
EXCHANGE.fillOrder.selector,
order,
takerAssetFillAmount,
orderSignature
);
// Call `fillOrder` via `executeTransaction`.
EXCHANGE.executeTransaction(
salt,
takerAddress,
data,
takerSignature
);
}
/// @dev Cancels all orders created by sender with a salt less than or equal to the targetOrderEpoch
/// and senderAddress equal to this contract.
/// @param targetOrderEpoch Orders created with a salt less or equal to this value will be cancelled.
/// @param salt Arbitrary value to gaurantee uniqueness of 0x transaction hash.
/// @param makerSignature Proof that maker wishes to call this function with given params.
function cancelOrdersUpTo(
uint256 targetOrderEpoch,
uint256 salt,
bytes makerSignature
)
external
{
address makerAddress = msg.sender;
// Encode arguments into byte array.
bytes memory data = abi.encodeWithSelector(
EXCHANGE.cancelOrdersUpTo.selector,
targetOrderEpoch
);
// Call `cancelOrdersUpTo` via `executeTransaction`.
EXCHANGE.executeTransaction(
salt,
makerAddress,
data,
makerSignature
);
}
}

View File

@@ -16,7 +16,7 @@
*/
pragma solidity ^0.4.23;
pragma solidity ^0.4.24;
pragma experimental ABIEncoderV2;
import "../../protocol/Exchange/interfaces/IExchange.sol";

View File

@@ -7,6 +7,7 @@ import * as DummyERC721Token from '../artifacts/DummyERC721Token.json';
import * as ERC20Proxy from '../artifacts/ERC20Proxy.json';
import * as ERC721Proxy from '../artifacts/ERC721Proxy.json';
import * as Exchange from '../artifacts/Exchange.json';
import * as ExchangeWrapper from '../artifacts/ExchangeWrapper.json';
import * as MixinAuthorizable from '../artifacts/MixinAuthorizable.json';
import * as MultiSigWallet from '../artifacts/MultiSigWallet.json';
import * as MultiSigWalletWithTimeLock from '../artifacts/MultiSigWalletWithTimeLock.json';
@@ -29,6 +30,7 @@ export const artifacts = {
ERC20Proxy: (ERC20Proxy as any) as ContractArtifact,
ERC721Proxy: (ERC721Proxy as any) as ContractArtifact,
Exchange: (Exchange as any) as ContractArtifact,
ExchangeWrapper: (ExchangeWrapper as any) as ContractArtifact,
EtherToken: (EtherToken as any) as ContractArtifact,
MixinAuthorizable: (MixinAuthorizable as any) as ContractArtifact,
MultiSigWallet: (MultiSigWallet as any) as ContractArtifact,

View File

@@ -620,30 +620,30 @@ describe('Exchange core', () => {
});
describe('cancelOrdersUpTo', () => {
it('should fail to set makerEpoch less than current makerEpoch', async () => {
const makerEpoch = new BigNumber(1);
await exchangeWrapper.cancelOrdersUpToAsync(makerEpoch, makerAddress);
const lesserMakerEpoch = new BigNumber(0);
it('should fail to set orderEpoch less than current orderEpoch', async () => {
const orderEpoch = new BigNumber(1);
await exchangeWrapper.cancelOrdersUpToAsync(orderEpoch, makerAddress);
const lesserOrderEpoch = new BigNumber(0);
return expectRevertOrAlwaysFailingTransactionAsync(
exchangeWrapper.cancelOrdersUpToAsync(lesserMakerEpoch, makerAddress),
exchangeWrapper.cancelOrdersUpToAsync(lesserOrderEpoch, makerAddress),
);
});
it('should fail to set makerEpoch equal to existing makerEpoch', async () => {
const makerEpoch = new BigNumber(1);
await exchangeWrapper.cancelOrdersUpToAsync(makerEpoch, makerAddress);
it('should fail to set orderEpoch equal to existing orderEpoch', async () => {
const orderEpoch = new BigNumber(1);
await exchangeWrapper.cancelOrdersUpToAsync(orderEpoch, makerAddress);
return expectRevertOrAlwaysFailingTransactionAsync(
exchangeWrapper.cancelOrdersUpToAsync(makerEpoch, makerAddress),
exchangeWrapper.cancelOrdersUpToAsync(orderEpoch, makerAddress),
);
});
it('should cancel only orders with a makerEpoch less than existing makerEpoch', async () => {
// Cancel all transactions with a makerEpoch less than 1
const makerEpoch = new BigNumber(1);
await exchangeWrapper.cancelOrdersUpToAsync(makerEpoch, makerAddress);
it('should cancel only orders with a orderEpoch less than existing orderEpoch', async () => {
// Cancel all transactions with a orderEpoch less than 1
const orderEpoch = new BigNumber(1);
await exchangeWrapper.cancelOrdersUpToAsync(orderEpoch, makerAddress);
// Create 3 orders with makerEpoch values: 0,1,2,3
// Since we cancelled with makerEpoch=1, orders with makerEpoch<=1 will not be processed
// Create 3 orders with orderEpoch values: 0,1,2,3
// Since we cancelled with orderEpoch=1, orders with orderEpoch<=1 will not be processed
erc20Balances = await erc20Wrapper.getBalancesAsync();
const signedOrders = [
orderFactory.newSignedOrder({

View File

@@ -7,6 +7,7 @@ import * as chai from 'chai';
import { DummyERC20TokenContract } from '../../src/generated_contract_wrappers/dummy_e_r_c20_token';
import { ERC20ProxyContract } from '../../src/generated_contract_wrappers/e_r_c20_proxy';
import { ExchangeContract } from '../../src/generated_contract_wrappers/exchange';
import { ExchangeWrapperContract } from '../../src/generated_contract_wrappers/exchange_wrapper';
import { WhitelistContract } from '../../src/generated_contract_wrappers/whitelist';
import { artifacts } from '../../src/utils/artifacts';
import { expectRevertOrAlwaysFailingTransactionAsync } from '../../src/utils/assertions';
@@ -198,6 +199,117 @@ describe('Exchange transactions', () => {
);
});
});
describe('cancelOrdersUpTo', () => {
let exchangeWrapperContract: ExchangeWrapperContract;
before(async () => {
exchangeWrapperContract = await ExchangeWrapperContract.deployFrom0xArtifactAsync(
artifacts.ExchangeWrapper,
provider,
txDefaults,
exchange.address,
);
});
it("should cancel an order if called from the order's sender", async () => {
const orderSalt = new BigNumber(0);
signedOrder = orderFactory.newSignedOrder({
senderAddress: exchangeWrapperContract.address,
salt: orderSalt,
});
const targetOrderEpoch = orderSalt.add(1);
const cancelData = exchange.cancelOrdersUpTo.getABIEncodedTransactionData(targetOrderEpoch);
const signedCancelTx = makerTransactionFactory.newSignedTransaction(cancelData);
await exchangeWrapperContract.cancelOrdersUpTo.sendTransactionAsync(
targetOrderEpoch,
signedCancelTx.salt,
signedCancelTx.signature,
{
from: makerAddress,
},
);
const takerAssetFillAmount = signedOrder.takerAssetAmount;
orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder);
const fillData = exchange.fillOrder.getABIEncodedTransactionData(
orderWithoutExchangeAddress,
takerAssetFillAmount,
signedOrder.signature,
);
const signedFillTx = takerTransactionFactory.newSignedTransaction(fillData);
return expectRevertOrAlwaysFailingTransactionAsync(
exchangeWrapperContract.fillOrder.sendTransactionAsync(
orderWithoutExchangeAddress,
takerAssetFillAmount,
signedFillTx.salt,
signedOrder.signature,
signedFillTx.signature,
{ from: takerAddress },
),
);
});
it("should not cancel an order if not called from the order's sender", async () => {
const orderSalt = new BigNumber(0);
signedOrder = orderFactory.newSignedOrder({
senderAddress: exchangeWrapperContract.address,
salt: orderSalt,
});
const targetOrderEpoch = orderSalt.add(1);
await exchangeWrapper.cancelOrdersUpToAsync(targetOrderEpoch, makerAddress);
erc20Balances = await erc20Wrapper.getBalancesAsync();
const takerAssetFillAmount = signedOrder.takerAssetAmount;
orderWithoutExchangeAddress = orderUtils.getOrderWithoutExchangeAddress(signedOrder);
const data = exchange.fillOrder.getABIEncodedTransactionData(
orderWithoutExchangeAddress,
takerAssetFillAmount,
signedOrder.signature,
);
signedTx = takerTransactionFactory.newSignedTransaction(data);
await exchangeWrapperContract.fillOrder.sendTransactionAsync(
orderWithoutExchangeAddress,
takerAssetFillAmount,
signedTx.salt,
signedOrder.signature,
signedTx.signature,
{ from: takerAddress },
);
const newBalances = await erc20Wrapper.getBalancesAsync();
const makerAssetFillAmount = takerAssetFillAmount
.times(signedOrder.makerAssetAmount)
.dividedToIntegerBy(signedOrder.takerAssetAmount);
const makerFeePaid = signedOrder.makerFee
.times(makerAssetFillAmount)
.dividedToIntegerBy(signedOrder.makerAssetAmount);
const takerFeePaid = signedOrder.takerFee
.times(makerAssetFillAmount)
.dividedToIntegerBy(signedOrder.makerAssetAmount);
expect(newBalances[makerAddress][defaultMakerTokenAddress]).to.be.bignumber.equal(
erc20Balances[makerAddress][defaultMakerTokenAddress].minus(makerAssetFillAmount),
);
expect(newBalances[makerAddress][defaultTakerTokenAddress]).to.be.bignumber.equal(
erc20Balances[makerAddress][defaultTakerTokenAddress].add(takerAssetFillAmount),
);
expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal(
erc20Balances[makerAddress][zrxToken.address].minus(makerFeePaid),
);
expect(newBalances[takerAddress][defaultTakerTokenAddress]).to.be.bignumber.equal(
erc20Balances[takerAddress][defaultTakerTokenAddress].minus(takerAssetFillAmount),
);
expect(newBalances[takerAddress][defaultMakerTokenAddress]).to.be.bignumber.equal(
erc20Balances[takerAddress][defaultMakerTokenAddress].add(makerAssetFillAmount),
);
expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal(
erc20Balances[takerAddress][zrxToken.address].minus(takerFeePaid),
);
expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal(
erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)),
);
});
});
});
describe('Whitelist', () => {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long