Remove protocol fees from RFQ orders (#45)

* `@0x/contracts-zero-ex`: Remove protocol fees from native orders in certain scenarios.

* `@0x/contracts-zero-ex`: update changelog

* `@0x/contracts-zero-ex`: Add `taker` field to RFQ orders.`
`@0x/contracts-zero-ex`: Introduce protocol fees to all limit orders again.

* `@0x/contracts-zero-ex`: Rebase.

Co-authored-by: Lawrence Forman <me@merklejerk.com>
This commit is contained in:
Lawrence Forman 2020-11-24 18:24:06 -05:00 committed by GitHub
parent ca20df4752
commit 7591e99316
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 140 additions and 76 deletions

View File

@ -37,6 +37,10 @@
{ {
"note": "Do not try to pull all tokens if selling all ETH in `TransformERC20Feature`", "note": "Do not try to pull all tokens if selling all ETH in `TransformERC20Feature`",
"pr": 46 "pr": 46
},
{
"note": "Remove protocol fees from all RFQ orders and add `taker` field to RFQ orders",
"pr": 45
} }
] ]
}, },

View File

@ -56,7 +56,6 @@ interface INativeOrdersFeature {
/// @param taker The taker of the order. /// @param taker The taker of the order.
/// @param takerTokenFilledAmount How much taker token was filled. /// @param takerTokenFilledAmount How much taker token was filled.
/// @param makerTokenFilledAmount How much maker token was filled. /// @param makerTokenFilledAmount How much maker token was filled.
/// @param protocolFeePaid How much protocol fee was paid.
/// @param pool The fee pool associated with this order. /// @param pool The fee pool associated with this order.
event RfqOrderFilled( event RfqOrderFilled(
bytes32 orderHash, bytes32 orderHash,
@ -66,7 +65,6 @@ interface INativeOrdersFeature {
address takerToken, address takerToken,
uint128 takerTokenFilledAmount, uint128 takerTokenFilledAmount,
uint128 makerTokenFilledAmount, uint128 makerTokenFilledAmount,
uint256 protocolFeePaid,
bytes32 pool bytes32 pool
); );
@ -126,8 +124,7 @@ interface INativeOrdersFeature {
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount); returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount);
/// @dev Fill an RFQ order for up to `takerTokenFillAmount` taker tokens. /// @dev Fill an RFQ order for up to `takerTokenFillAmount` taker tokens.
/// The taker will be the caller. ETH should be attached to pay the /// The taker will be the caller.
/// protocol fee.
/// @param order The RFQ order. /// @param order The RFQ order.
/// @param signature The order signature. /// @param signature The order signature.
/// @param takerTokenFillAmount Maximum taker token amount to fill this order with. /// @param takerTokenFillAmount Maximum taker token amount to fill this order with.
@ -139,7 +136,6 @@ interface INativeOrdersFeature {
uint128 takerTokenFillAmount uint128 takerTokenFillAmount
) )
external external
payable
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount); returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount);
/// @dev Fill an RFQ order for exactly `takerTokenFillAmount` taker tokens. /// @dev Fill an RFQ order for exactly `takerTokenFillAmount` taker tokens.
@ -160,9 +156,7 @@ interface INativeOrdersFeature {
returns (uint128 makerTokenFilledAmount); returns (uint128 makerTokenFilledAmount);
/// @dev Fill an RFQ order for exactly `takerTokenFillAmount` taker tokens. /// @dev Fill an RFQ order for exactly `takerTokenFillAmount` taker tokens.
/// The taker will be the caller. ETH protocol fees can be /// The taker will be the caller.
/// attached to this call. Any unspent ETH will be refunded to
/// the caller.
/// @param order The RFQ order. /// @param order The RFQ order.
/// @param signature The order signature. /// @param signature The order signature.
/// @param takerTokenFillAmount How much taker token to fill this order with. /// @param takerTokenFillAmount How much taker token to fill this order with.
@ -173,7 +167,6 @@ interface INativeOrdersFeature {
uint128 takerTokenFillAmount uint128 takerTokenFillAmount
) )
external external
payable
returns (uint128 makerTokenFilledAmount); returns (uint128 makerTokenFilledAmount);
/// @dev Fill a limit order. Internal variant. ETH protocol fees can be /// @dev Fill a limit order. Internal variant. ETH protocol fees can be
@ -197,9 +190,7 @@ interface INativeOrdersFeature {
payable payable
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount); returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount);
/// @dev Fill an RFQ order. Internal variant. ETH protocol fees can be /// @dev Fill an RFQ order. Internal variant.
/// attached to this call. Any unspent ETH will be refunded to
/// `msg.sender` (not `sender`).
/// @param order The RFQ order. /// @param order The RFQ order.
/// @param signature The order signature. /// @param signature The order signature.
/// @param takerTokenFillAmount Maximum taker token to fill this order with. /// @param takerTokenFillAmount Maximum taker token to fill this order with.
@ -213,7 +204,6 @@ interface INativeOrdersFeature {
address taker address taker
) )
external external
payable
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount); returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount);
/// @dev Cancel a single limit order. The caller must be the maker. /// @dev Cancel a single limit order. The caller must be the maker.

View File

@ -210,7 +210,6 @@ contract NativeOrdersFeature is
) )
public public
override override
payable
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount)
{ {
FillNativeOrderResults memory results = FillNativeOrderResults memory results =
@ -220,7 +219,6 @@ contract NativeOrdersFeature is
takerTokenFillAmount, takerTokenFillAmount,
msg.sender msg.sender
); );
_refundExcessProtocolFeeToSender(results.ethProtocolFeePaid);
(takerTokenFilledAmount, makerTokenFilledAmount) = ( (takerTokenFilledAmount, makerTokenFilledAmount) = (
results.takerTokenFilledAmount, results.takerTokenFilledAmount,
results.makerTokenFilledAmount results.makerTokenFilledAmount
@ -280,7 +278,6 @@ contract NativeOrdersFeature is
) )
public public
override override
payable
returns (uint128 makerTokenFilledAmount) returns (uint128 makerTokenFilledAmount)
{ {
FillNativeOrderResults memory results = FillNativeOrderResults memory results =
@ -298,7 +295,6 @@ contract NativeOrdersFeature is
takerTokenFillAmount takerTokenFillAmount
).rrevert(); ).rrevert();
} }
_refundExcessProtocolFeeToSender(results.ethProtocolFeePaid);
makerTokenFilledAmount = results.makerTokenFilledAmount; makerTokenFilledAmount = results.makerTokenFilledAmount;
} }
@ -359,7 +355,6 @@ contract NativeOrdersFeature is
public public
virtual virtual
override override
payable
onlySelf onlySelf
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount)
{ {
@ -370,7 +365,6 @@ contract NativeOrdersFeature is
takerTokenFillAmount, takerTokenFillAmount,
taker taker
); );
_refundExcessProtocolFeeToSender(results.ethProtocolFeePaid);
(takerTokenFilledAmount, makerTokenFilledAmount) = ( (takerTokenFilledAmount, makerTokenFilledAmount) = (
results.takerTokenFilledAmount, results.takerTokenFilledAmount,
results.makerTokenFilledAmount results.makerTokenFilledAmount
@ -898,6 +892,15 @@ contract NativeOrdersFeature is
} }
} }
// Must be fillable by the taker.
if (order.taker != address(0) && order.taker != taker) {
LibNativeOrdersRichErrors.OrderNotFillableByTakerError(
orderInfo.orderHash,
taker,
order.taker
).rrevert();
}
// Signature must be valid for the order. // Signature must be valid for the order.
{ {
address signer = LibSignature.getSignerOfHash(orderInfo.orderHash, signature); address signer = LibSignature.getSignerOfHash(orderInfo.orderHash, signature);
@ -910,9 +913,6 @@ contract NativeOrdersFeature is
} }
} }
// Pay the protocol fee.
results.ethProtocolFeePaid = _collectProtocolFee(order.pool);
// Settle between the maker and taker. // Settle between the maker and taker.
(results.takerTokenFilledAmount, results.makerTokenFilledAmount) = _settleOrder( (results.takerTokenFilledAmount, results.makerTokenFilledAmount) = _settleOrder(
SettleOrderInfo({ SettleOrderInfo({
@ -936,7 +936,6 @@ contract NativeOrdersFeature is
address(order.takerToken), address(order.takerToken),
results.takerTokenFilledAmount, results.takerTokenFilledAmount,
results.makerTokenFilledAmount, results.makerTokenFilledAmount,
results.ethProtocolFeePaid,
order.pool order.pool
); );
} }

View File

@ -56,6 +56,7 @@ library LibNativeOrder {
uint128 makerAmount; uint128 makerAmount;
uint128 takerAmount; uint128 takerAmount;
address maker; address maker;
address taker;
address txOrigin; address txOrigin;
bytes32 pool; bytes32 pool;
uint64 expiry; uint64 expiry;
@ -102,13 +103,14 @@ library LibNativeOrder {
// "uint128 takerAmount,", // "uint128 takerAmount,",
// "address maker,", // "address maker,",
// "address txOrigin,", // "address txOrigin,",
// "address taker,",
// "bytes32 pool,", // "bytes32 pool,",
// "uint64 expiry,", // "uint64 expiry,",
// "uint256 salt" // "uint256 salt"
// ")" // ")"
// )) // ))
uint256 private constant _RFQ_ORDER_TYPEHASH = uint256 private constant _RFQ_ORDER_TYPEHASH =
0xc6b3034376598bc7f28b05e81db7ed88486dcdb6b4a6c7300353fffc5f31f382; 0xe593d3fdfa8b60e5e17a1b2204662ecbe15c23f2084b9ad5bae40359540a7da9;
/// @dev Get the struct hash of a limit order. /// @dev Get the struct hash of a limit order.
/// @param order The limit order. /// @param order The limit order.
@ -181,6 +183,7 @@ library LibNativeOrder {
// order.makerAmount, // order.makerAmount,
// order.takerAmount, // order.takerAmount,
// order.maker, // order.maker,
// order.taker,
// order.txOrigin, // order.txOrigin,
// order.pool, // order.pool,
// order.expiry, // order.expiry,
@ -199,15 +202,17 @@ library LibNativeOrder {
mstore(add(mem, 0x80), and(UINT_128_MASK, mload(add(order, 0x60)))) mstore(add(mem, 0x80), and(UINT_128_MASK, mload(add(order, 0x60))))
// order.maker; // order.maker;
mstore(add(mem, 0xA0), and(ADDRESS_MASK, mload(add(order, 0x80)))) mstore(add(mem, 0xA0), and(ADDRESS_MASK, mload(add(order, 0x80))))
// order.txOrigin; // order.taker;
mstore(add(mem, 0xC0), and(ADDRESS_MASK, mload(add(order, 0xA0)))) mstore(add(mem, 0xC0), and(ADDRESS_MASK, mload(add(order, 0xA0))))
// order.txOrigin;
mstore(add(mem, 0xE0), and(ADDRESS_MASK, mload(add(order, 0xC0))))
// order.pool; // order.pool;
mstore(add(mem, 0xE0), mload(add(order, 0xC0))) mstore(add(mem, 0x100), mload(add(order, 0xE0)))
// order.expiry; // order.expiry;
mstore(add(mem, 0x100), and(UINT_64_MASK, mload(add(order, 0xE0)))) mstore(add(mem, 0x120), and(UINT_64_MASK, mload(add(order, 0x100))))
// order.salt; // order.salt;
mstore(add(mem, 0x120), mload(add(order, 0x100))) mstore(add(mem, 0x140), mload(add(order, 0x120)))
structHash := keccak256(mem, 0x140) structHash := keccak256(mem, 0x160)
} }
} }
} }

View File

@ -87,7 +87,6 @@ contract TestMetaTransactionsNativeOrdersFeature is
) )
public public
override override
payable
returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount)
{ {
emit FillRfqOrderCalled( emit FillRfqOrderCalled(

View File

@ -20,6 +20,7 @@ const COMMON_ORDER_DEFAULT_VALUES = {
makerAmount: ZERO, makerAmount: ZERO,
takerAmount: ZERO, takerAmount: ZERO,
maker: NULL_ADDRESS, maker: NULL_ADDRESS,
taker: NULL_ADDRESS,
pool: hexUtils.leftPad(0), pool: hexUtils.leftPad(0),
expiry: ZERO, expiry: ZERO,
salt: ZERO, salt: ZERO,
@ -29,7 +30,6 @@ const COMMON_ORDER_DEFAULT_VALUES = {
const LIMIT_ORDER_DEFAULT_VALUES = { const LIMIT_ORDER_DEFAULT_VALUES = {
...COMMON_ORDER_DEFAULT_VALUES, ...COMMON_ORDER_DEFAULT_VALUES,
takerTokenFeeAmount: ZERO, takerTokenFeeAmount: ZERO,
taker: NULL_ADDRESS,
sender: NULL_ADDRESS, sender: NULL_ADDRESS,
feeRecipient: NULL_ADDRESS, feeRecipient: NULL_ADDRESS,
}; };
@ -63,6 +63,7 @@ export abstract class OrderBase {
public takerAmount: BigNumber; public takerAmount: BigNumber;
public maker: string; public maker: string;
public pool: string; public pool: string;
public taker: string;
public expiry: BigNumber; public expiry: BigNumber;
public salt: BigNumber; public salt: BigNumber;
public chainId: number; public chainId: number;
@ -75,6 +76,7 @@ export abstract class OrderBase {
this.makerAmount = _fields.makerAmount; this.makerAmount = _fields.makerAmount;
this.takerAmount = _fields.takerAmount; this.takerAmount = _fields.takerAmount;
this.maker = _fields.maker; this.maker = _fields.maker;
this.taker = _fields.taker;
this.pool = _fields.pool; this.pool = _fields.pool;
this.expiry = _fields.expiry; this.expiry = _fields.expiry;
this.salt = _fields.salt; this.salt = _fields.salt;
@ -142,7 +144,6 @@ export class LimitOrder extends OrderBase {
); );
public takerTokenFeeAmount: BigNumber; public takerTokenFeeAmount: BigNumber;
public taker: string;
public sender: string; public sender: string;
public feeRecipient: string; public feeRecipient: string;
@ -150,7 +151,6 @@ export class LimitOrder extends OrderBase {
const _fields = { ...LIMIT_ORDER_DEFAULT_VALUES, ...fields }; const _fields = { ...LIMIT_ORDER_DEFAULT_VALUES, ...fields };
super(_fields); super(_fields);
this.takerTokenFeeAmount = _fields.takerTokenFeeAmount; this.takerTokenFeeAmount = _fields.takerTokenFeeAmount;
this.taker = _fields.taker;
this.sender = _fields.sender; this.sender = _fields.sender;
this.feeRecipient = _fields.feeRecipient; this.feeRecipient = _fields.feeRecipient;
} }
@ -246,6 +246,7 @@ export class RfqOrder extends OrderBase {
'uint128 makerAmount', 'uint128 makerAmount',
'uint128 takerAmount', 'uint128 takerAmount',
'address maker', 'address maker',
'address taker',
'address txOrigin', 'address txOrigin',
'bytes32 pool', 'bytes32 pool',
'uint64 expiry', 'uint64 expiry',
@ -272,6 +273,7 @@ export class RfqOrder extends OrderBase {
makerAmount: this.makerAmount, makerAmount: this.makerAmount,
takerAmount: this.takerAmount, takerAmount: this.takerAmount,
maker: this.maker, maker: this.maker,
taker: this.taker,
txOrigin: this.txOrigin, txOrigin: this.txOrigin,
pool: this.pool, pool: this.pool,
expiry: this.expiry, expiry: this.expiry,
@ -291,6 +293,7 @@ export class RfqOrder extends OrderBase {
hexUtils.leftPad(this.makerAmount), hexUtils.leftPad(this.makerAmount),
hexUtils.leftPad(this.takerAmount), hexUtils.leftPad(this.takerAmount),
hexUtils.leftPad(this.maker), hexUtils.leftPad(this.maker),
hexUtils.leftPad(this.taker),
hexUtils.leftPad(this.txOrigin), hexUtils.leftPad(this.txOrigin),
hexUtils.leftPad(this.pool), hexUtils.leftPad(this.pool),
hexUtils.leftPad(this.expiry), hexUtils.leftPad(this.expiry),
@ -309,6 +312,7 @@ export class RfqOrder extends OrderBase {
{ type: 'uint128', name: 'makerAmount' }, { type: 'uint128', name: 'makerAmount' },
{ type: 'uint128', name: 'takerAmount' }, { type: 'uint128', name: 'takerAmount' },
{ type: 'address', name: 'maker' }, { type: 'address', name: 'maker' },
{ type: 'address', name: 'taker' },
{ type: 'address', name: 'txOrigin' }, { type: 'address', name: 'txOrigin' },
{ type: 'bytes32', name: 'pool' }, { type: 'bytes32', name: 'pool' },
{ type: 'uint64', name: 'expiry' }, { type: 'uint64', name: 'expiry' },
@ -323,6 +327,7 @@ export class RfqOrder extends OrderBase {
makerAmount: this.makerAmount.toString(10), makerAmount: this.makerAmount.toString(10),
takerAmount: this.takerAmount.toString(10), takerAmount: this.takerAmount.toString(10),
maker: this.maker, maker: this.maker,
taker: this.taker,
txOrigin: this.txOrigin, txOrigin: this.txOrigin,
pool: this.pool, pool: this.pool,
expiry: this.expiry.toString(10), expiry: this.expiry.toString(10),

View File

@ -197,11 +197,12 @@ blockchainTests.resets('MetaTransactions feature', env => {
const fillAmount = new BigNumber(23456); const fillAmount = new BigNumber(23456);
const mtx = getRandomMetaTransaction({ const mtx = getRandomMetaTransaction({
callData: nativeOrdersFeature.fillRfqOrder(order, sig, fillAmount).getABIEncodedTransactionData(), callData: nativeOrdersFeature.fillRfqOrder(order, sig, fillAmount).getABIEncodedTransactionData(),
value: ZERO_AMOUNT,
}); });
const signature = await signMetaTransactionAsync(mtx); const signature = await signMetaTransactionAsync(mtx);
const callOpts = { const callOpts = {
gasPrice: mtx.minGasPrice, gasPrice: mtx.minGasPrice,
value: mtx.value, value: 0,
}; };
const rawResult = await feature.executeMetaTransaction(mtx, signature).callAsync(callOpts); const rawResult = await feature.executeMetaTransaction(mtx, signature).callAsync(callOpts);
expect(rawResult).to.eq(RAW_ORDER_SUCCESS_RESULT); expect(rawResult).to.eq(RAW_ORDER_SUCCESS_RESULT);

View File

@ -128,13 +128,22 @@ blockchainTests.resets('NativeOrdersFeature', env => {
async function fillLimitOrderAsync( async function fillLimitOrderAsync(
order: LimitOrder, order: LimitOrder,
fillAmount: BigNumber | number = order.takerAmount, opts: Partial<{
_taker: string = taker, fillAmount: BigNumber | number;
taker: string;
protocolFee?: BigNumber | number;
}> = {},
): Promise<TransactionReceiptWithDecodedLogs> { ): Promise<TransactionReceiptWithDecodedLogs> {
const { fillAmount, taker: _taker, protocolFee } = {
taker,
fillAmount: order.takerAmount,
...opts,
};
await prepareBalancesForOrderAsync(order, _taker); await prepareBalancesForOrderAsync(order, _taker);
const _protocolFee = protocolFee === undefined ? SINGLE_PROTOCOL_FEE : protocolFee;
return zeroEx return zeroEx
.fillLimitOrder(order, await order.getSignatureWithProviderAsync(env.provider), new BigNumber(fillAmount)) .fillLimitOrder(order, await order.getSignatureWithProviderAsync(env.provider), new BigNumber(fillAmount))
.awaitTransactionSuccessAsync({ from: _taker, value: SINGLE_PROTOCOL_FEE }); .awaitTransactionSuccessAsync({ from: _taker, value: _protocolFee });
} }
describe('getLimitOrderInfo()', () => { describe('getLimitOrderInfo()', () => {
@ -200,7 +209,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
const order = getTestLimitOrder(); const order = getTestLimitOrder();
const fillAmount = order.takerAmount.minus(1); const fillAmount = order.takerAmount.minus(1);
// Fill the order first. // Fill the order first.
await fillLimitOrderAsync(order, fillAmount); await fillLimitOrderAsync(order, { fillAmount });
const info = await zeroEx.getLimitOrderInfo(order).callAsync(); const info = await zeroEx.getLimitOrderInfo(order).callAsync();
assertOrderInfoEquals(info, { assertOrderInfoEquals(info, {
status: OrderStatus.Fillable, status: OrderStatus.Fillable,
@ -226,7 +235,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
const order = getTestLimitOrder(); const order = getTestLimitOrder();
const fillAmount = order.takerAmount.minus(1); const fillAmount = order.takerAmount.minus(1);
// Fill the order first. // Fill the order first.
await fillLimitOrderAsync(order, fillAmount); await fillLimitOrderAsync(order, { fillAmount });
await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker }); await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker });
const info = await zeroEx.getLimitOrderInfo(order).callAsync(); const info = await zeroEx.getLimitOrderInfo(order).callAsync();
assertOrderInfoEquals(info, { assertOrderInfoEquals(info, {
@ -245,7 +254,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
await prepareBalancesForOrderAsync(order, _taker); await prepareBalancesForOrderAsync(order, _taker);
return zeroEx return zeroEx
.fillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), new BigNumber(fillAmount)) .fillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), new BigNumber(fillAmount))
.awaitTransactionSuccessAsync({ from: _taker, value: SINGLE_PROTOCOL_FEE }); .awaitTransactionSuccessAsync({ from: _taker });
} }
describe('getRfqOrderInfo()', () => { describe('getRfqOrderInfo()', () => {
@ -287,9 +296,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
await prepareBalancesForOrderAsync(order); await prepareBalancesForOrderAsync(order);
const sig = await order.getSignatureWithProviderAsync(env.provider); const sig = await order.getSignatureWithProviderAsync(env.provider);
// Fill the order first. // Fill the order first.
await zeroEx await zeroEx.fillRfqOrder(order, sig, order.takerAmount).awaitTransactionSuccessAsync({ from: taker });
.fillRfqOrder(order, sig, order.takerAmount)
.awaitTransactionSuccessAsync({ from: taker, value: SINGLE_PROTOCOL_FEE });
// Advance time to expire the order. // Advance time to expire the order.
await env.web3Wrapper.increaseTimeAsync(61); await env.web3Wrapper.increaseTimeAsync(61);
const info = await zeroEx.getRfqOrderInfo(order).callAsync(); const info = await zeroEx.getRfqOrderInfo(order).callAsync();
@ -381,7 +388,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('can cancel a partially filled order', async () => { it('can cancel a partially filled order', async () => {
const order = getTestLimitOrder(); const order = getTestLimitOrder();
await fillLimitOrderAsync(order, order.takerAmount.minus(1)); await fillLimitOrderAsync(order, { fillAmount: order.takerAmount.minus(1) });
const receipt = await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker }); const receipt = await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker });
verifyEventsFromLogs( verifyEventsFromLogs(
receipt.logs, receipt.logs,
@ -748,6 +755,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
takerTokenFilledAmount, takerTokenFilledAmount,
takerTokenFeeFilledAmount, takerTokenFeeFilledAmount,
} = computeLimitOrderFilledAmounts(order, takerTokenFillAmount, takerTokenAlreadyFilledAmount); } = computeLimitOrderFilledAmounts(order, takerTokenFillAmount, takerTokenAlreadyFilledAmount);
const protocolFee = order.taker !== NULL_ADDRESS ? ZERO_AMOUNT : SINGLE_PROTOCOL_FEE;
return { return {
taker, taker,
takerTokenFilledAmount, takerTokenFilledAmount,
@ -758,16 +766,25 @@ blockchainTests.resets('NativeOrdersFeature', env => {
feeRecipient: order.feeRecipient, feeRecipient: order.feeRecipient,
makerToken: order.makerToken, makerToken: order.makerToken,
takerToken: order.takerToken, takerToken: order.takerToken,
protocolFeePaid: SINGLE_PROTOCOL_FEE, protocolFeePaid: protocolFee,
pool: order.pool, pool: order.pool,
}; };
} }
async function assertExpectedFinalBalancesFromLimitOrderFillAsync( async function assertExpectedFinalBalancesFromLimitOrderFillAsync(
order: LimitOrder, order: LimitOrder,
takerTokenFillAmount: BigNumber = order.takerAmount, opts: Partial<{
takerTokenAlreadyFilledAmount: BigNumber = ZERO_AMOUNT, takerTokenFillAmount: BigNumber;
takerTokenAlreadyFilledAmount: BigNumber;
receipt: TransactionReceiptWithDecodedLogs;
}> = {},
): Promise<void> { ): Promise<void> {
const { takerTokenFillAmount, takerTokenAlreadyFilledAmount, receipt } = {
takerTokenFillAmount: order.takerAmount,
takerTokenAlreadyFilledAmount: ZERO_AMOUNT,
receipt: undefined,
...opts,
};
const { const {
makerTokenFilledAmount, makerTokenFilledAmount,
takerTokenFilledAmount, takerTokenFilledAmount,
@ -779,6 +796,13 @@ blockchainTests.resets('NativeOrdersFeature', env => {
expect(makerBalance).to.bignumber.eq(takerTokenFilledAmount); expect(makerBalance).to.bignumber.eq(takerTokenFilledAmount);
expect(takerBalance).to.bignumber.eq(makerTokenFilledAmount); expect(takerBalance).to.bignumber.eq(makerTokenFilledAmount);
expect(feeRecipientBalance).to.bignumber.eq(takerTokenFeeFilledAmount); expect(feeRecipientBalance).to.bignumber.eq(takerTokenFeeFilledAmount);
if (receipt) {
const balanceOfTakerNow = await env.web3Wrapper.getBalanceInWeiAsync(taker);
const balanceOfTakerBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker, receipt.blockNumber - 1);
const protocolFee = order.taker === NULL_ADDRESS ? SINGLE_PROTOCOL_FEE : 0;
const totalCost = GAS_PRICE.times(receipt.gasUsed).plus(protocolFee);
expect(balanceOfTakerBefore.minus(totalCost)).to.bignumber.eq(balanceOfTakerNow);
}
} }
describe('fillLimitOrder()', () => { describe('fillLimitOrder()', () => {
@ -795,13 +819,13 @@ blockchainTests.resets('NativeOrdersFeature', env => {
status: OrderStatus.Filled, status: OrderStatus.Filled,
takerTokenFilledAmount: order.takerAmount, takerTokenFilledAmount: order.takerAmount,
}); });
await assertExpectedFinalBalancesFromLimitOrderFillAsync(order); await assertExpectedFinalBalancesFromLimitOrderFillAsync(order, { receipt });
}); });
it('can partially fill an order', async () => { it('can partially fill an order', async () => {
const order = getTestLimitOrder(); const order = getTestLimitOrder();
const fillAmount = order.takerAmount.minus(1); const fillAmount = order.takerAmount.minus(1);
const receipt = await fillLimitOrderAsync(order, fillAmount); const receipt = await fillLimitOrderAsync(order, { fillAmount });
verifyEventsFromLogs( verifyEventsFromLogs(
receipt.logs, receipt.logs,
[createLimitOrderFilledEventArgs(order, fillAmount)], [createLimitOrderFilledEventArgs(order, fillAmount)],
@ -812,13 +836,13 @@ blockchainTests.resets('NativeOrdersFeature', env => {
status: OrderStatus.Fillable, status: OrderStatus.Fillable,
takerTokenFilledAmount: fillAmount, takerTokenFilledAmount: fillAmount,
}); });
await assertExpectedFinalBalancesFromLimitOrderFillAsync(order, fillAmount); await assertExpectedFinalBalancesFromLimitOrderFillAsync(order, { takerTokenFillAmount: fillAmount });
}); });
it('can fully fill an order in two steps', async () => { it('can fully fill an order in two steps', async () => {
const order = getTestLimitOrder(); const order = getTestLimitOrder();
let fillAmount = order.takerAmount.dividedToIntegerBy(2); let fillAmount = order.takerAmount.dividedToIntegerBy(2);
let receipt = await fillLimitOrderAsync(order, fillAmount); let receipt = await fillLimitOrderAsync(order, { fillAmount });
verifyEventsFromLogs( verifyEventsFromLogs(
receipt.logs, receipt.logs,
[createLimitOrderFilledEventArgs(order, fillAmount)], [createLimitOrderFilledEventArgs(order, fillAmount)],
@ -826,7 +850,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
); );
const alreadyFilledAmount = fillAmount; const alreadyFilledAmount = fillAmount;
fillAmount = order.takerAmount.minus(fillAmount); fillAmount = order.takerAmount.minus(fillAmount);
receipt = await fillLimitOrderAsync(order, fillAmount); receipt = await fillLimitOrderAsync(order, { fillAmount });
verifyEventsFromLogs( verifyEventsFromLogs(
receipt.logs, receipt.logs,
[createLimitOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)], [createLimitOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)],
@ -842,7 +866,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('clamps fill amount to remaining available', async () => { it('clamps fill amount to remaining available', async () => {
const order = getTestLimitOrder(); const order = getTestLimitOrder();
const fillAmount = order.takerAmount.plus(1); const fillAmount = order.takerAmount.plus(1);
const receipt = await fillLimitOrderAsync(order, fillAmount); const receipt = await fillLimitOrderAsync(order, { fillAmount });
verifyEventsFromLogs( verifyEventsFromLogs(
receipt.logs, receipt.logs,
[createLimitOrderFilledEventArgs(order, fillAmount)], [createLimitOrderFilledEventArgs(order, fillAmount)],
@ -853,13 +877,13 @@ blockchainTests.resets('NativeOrdersFeature', env => {
status: OrderStatus.Filled, status: OrderStatus.Filled,
takerTokenFilledAmount: order.takerAmount, takerTokenFilledAmount: order.takerAmount,
}); });
await assertExpectedFinalBalancesFromLimitOrderFillAsync(order, fillAmount); await assertExpectedFinalBalancesFromLimitOrderFillAsync(order, { takerTokenFillAmount: fillAmount });
}); });
it('clamps fill amount to remaining available in partial filled order', async () => { it('clamps fill amount to remaining available in partial filled order', async () => {
const order = getTestLimitOrder(); const order = getTestLimitOrder();
let fillAmount = order.takerAmount.dividedToIntegerBy(2); let fillAmount = order.takerAmount.dividedToIntegerBy(2);
let receipt = await fillLimitOrderAsync(order, fillAmount); let receipt = await fillLimitOrderAsync(order, { fillAmount });
verifyEventsFromLogs( verifyEventsFromLogs(
receipt.logs, receipt.logs,
[createLimitOrderFilledEventArgs(order, fillAmount)], [createLimitOrderFilledEventArgs(order, fillAmount)],
@ -867,7 +891,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
); );
const alreadyFilledAmount = fillAmount; const alreadyFilledAmount = fillAmount;
fillAmount = order.takerAmount.minus(fillAmount).plus(1); fillAmount = order.takerAmount.minus(fillAmount).plus(1);
receipt = await fillLimitOrderAsync(order, fillAmount); receipt = await fillLimitOrderAsync(order, { fillAmount });
verifyEventsFromLogs( verifyEventsFromLogs(
receipt.logs, receipt.logs,
[createLimitOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)], [createLimitOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)],
@ -910,7 +934,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('non-taker cannot fill order', async () => { it('non-taker cannot fill order', async () => {
const order = getTestLimitOrder({ taker }); const order = getTestLimitOrder({ taker });
const tx = fillLimitOrderAsync(order, order.takerAmount, notTaker); const tx = fillLimitOrderAsync(order, { fillAmount: order.takerAmount, taker: notTaker });
return expect(tx).to.revertWith( return expect(tx).to.revertWith(
new RevertErrors.OrderNotFillableByTakerError(order.getHash(), notTaker, order.taker), new RevertErrors.OrderNotFillableByTakerError(order.getHash(), notTaker, order.taker),
); );
@ -918,7 +942,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
it('non-sender cannot fill order', async () => { it('non-sender cannot fill order', async () => {
const order = getTestLimitOrder({ sender: taker }); const order = getTestLimitOrder({ sender: taker });
const tx = fillLimitOrderAsync(order, order.takerAmount, notTaker); const tx = fillLimitOrderAsync(order, { fillAmount: order.takerAmount, taker: notTaker });
return expect(tx).to.revertWith( return expect(tx).to.revertWith(
new RevertErrors.OrderNotFillableBySenderError(order.getHash(), notTaker, order.sender), new RevertErrors.OrderNotFillableBySenderError(order.getHash(), notTaker, order.sender),
); );
@ -934,7 +958,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
); );
}); });
it('fails if no protocol fee attached (and no weth allowance)', async () => { it('fails if no protocol fee attached', async () => {
const order = getTestLimitOrder(); const order = getTestLimitOrder();
await prepareBalancesForOrderAsync(order); await prepareBalancesForOrderAsync(order);
const tx = zeroEx const tx = zeroEx
@ -948,6 +972,17 @@ blockchainTests.resets('NativeOrdersFeature', env => {
// token spender fallthroigh, so we won't get too specific. // token spender fallthroigh, so we won't get too specific.
return expect(tx).to.revertWith(new AnyRevertError()); return expect(tx).to.revertWith(new AnyRevertError());
}); });
it('refunds excess protocol fee', async () => {
const order = getTestLimitOrder();
const receipt = await fillLimitOrderAsync(order, { protocolFee: SINGLE_PROTOCOL_FEE.plus(1) });
verifyEventsFromLogs(
receipt.logs,
[createLimitOrderFilledEventArgs(order)],
IZeroExEvents.LimitOrderFilled,
);
await assertExpectedFinalBalancesFromLimitOrderFillAsync(order, { receipt });
});
}); });
interface RfqOrderFilledAmounts { interface RfqOrderFilledAmounts {
@ -993,7 +1028,6 @@ blockchainTests.resets('NativeOrdersFeature', env => {
maker: order.maker, maker: order.maker,
makerToken: order.makerToken, makerToken: order.makerToken,
takerToken: order.takerToken, takerToken: order.takerToken,
protocolFeePaid: SINGLE_PROTOCOL_FEE,
pool: order.pool, pool: order.pool,
}; };
} }
@ -1170,6 +1204,14 @@ blockchainTests.resets('NativeOrdersFeature', env => {
); );
}); });
it('non-taker cannot fill order', async () => {
const order = getTestRfqOrder({ taker, txOrigin: notTaker });
const tx = fillRfqOrderAsync(order, order.takerAmount, notTaker);
return expect(tx).to.revertWith(
new RevertErrors.OrderNotFillableByTakerError(order.getHash(), notTaker, order.taker),
);
});
it('cannot fill an expired order', async () => { it('cannot fill an expired order', async () => {
const order = getTestRfqOrder({ expiry: createExpiry(-60) }); const order = getTestRfqOrder({ expiry: createExpiry(-60) });
const tx = fillRfqOrderAsync(order); const tx = fillRfqOrderAsync(order);
@ -1208,19 +1250,14 @@ blockchainTests.resets('NativeOrdersFeature', env => {
); );
}); });
it('fails if no protocol fee attached (and no weth allowance)', async () => { it('fails if ETH is attached', async () => {
const order = getTestRfqOrder(); const order = getTestRfqOrder();
await prepareBalancesForOrderAsync(order); await prepareBalancesForOrderAsync(order, taker);
const tx = zeroEx const tx = zeroEx
.fillRfqOrder( .fillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount)
order, .awaitTransactionSuccessAsync({ from: taker, value: 1 });
await order.getSignatureWithProviderAsync(env.provider), // This will revert at the language level because the fill function is not payable.
new BigNumber(order.takerAmount), return expect(tx).to.be.rejectedWith('revert');
)
.awaitTransactionSuccessAsync({ from: taker, value: ZERO_AMOUNT });
// The exact revert error depends on whether we are still doing a
// token spender fallthrough, so we won't get too specific.
return expect(tx).to.revertWith(new AnyRevertError());
}); });
}); });
@ -1249,6 +1286,18 @@ blockchainTests.resets('NativeOrdersFeature', env => {
new RevertErrors.FillOrKillFailedError(order.getHash(), order.takerAmount, fillAmount), new RevertErrors.FillOrKillFailedError(order.getHash(), order.takerAmount, fillAmount),
); );
}); });
it('refunds excess protocol fee', async () => {
const order = getTestLimitOrder();
await prepareBalancesForOrderAsync(order);
const takerBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker);
const receipt = await zeroEx
.fillOrKillLimitOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount)
.awaitTransactionSuccessAsync({ from: taker, value: SINGLE_PROTOCOL_FEE.plus(1) });
const takerBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(taker);
const totalCost = GAS_PRICE.times(receipt.gasUsed).plus(SINGLE_PROTOCOL_FEE);
expect(takerBalanceBefore.minus(totalCost)).to.bignumber.eq(takerBalanceAfter);
});
}); });
describe('fillOrKillRfqOrder()', () => { describe('fillOrKillRfqOrder()', () => {
@ -1257,7 +1306,7 @@ blockchainTests.resets('NativeOrdersFeature', env => {
await prepareBalancesForOrderAsync(order); await prepareBalancesForOrderAsync(order);
const receipt = await zeroEx const receipt = await zeroEx
.fillOrKillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount) .fillOrKillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount)
.awaitTransactionSuccessAsync({ from: taker, value: SINGLE_PROTOCOL_FEE }); .awaitTransactionSuccessAsync({ from: taker });
verifyEventsFromLogs(receipt.logs, [createRfqOrderFilledEventArgs(order)], IZeroExEvents.RfqOrderFilled); verifyEventsFromLogs(receipt.logs, [createRfqOrderFilledEventArgs(order)], IZeroExEvents.RfqOrderFilled);
}); });
@ -1267,11 +1316,21 @@ blockchainTests.resets('NativeOrdersFeature', env => {
const fillAmount = order.takerAmount.plus(1); const fillAmount = order.takerAmount.plus(1);
const tx = zeroEx const tx = zeroEx
.fillOrKillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), fillAmount) .fillOrKillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), fillAmount)
.awaitTransactionSuccessAsync({ from: taker, value: SINGLE_PROTOCOL_FEE }); .awaitTransactionSuccessAsync({ from: taker });
return expect(tx).to.revertWith( return expect(tx).to.revertWith(
new RevertErrors.FillOrKillFailedError(order.getHash(), order.takerAmount, fillAmount), new RevertErrors.FillOrKillFailedError(order.getHash(), order.takerAmount, fillAmount),
); );
}); });
it('fails if ETH is attached', async () => {
const order = getTestRfqOrder();
await prepareBalancesForOrderAsync(order);
const tx = zeroEx
.fillOrKillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount)
.awaitTransactionSuccessAsync({ from: taker, value: 1 });
// This will revert at the language level because the fill function is not payable.
return expect(tx).to.be.rejectedWith('revert');
});
}); });
it.skip('RFQ gas benchmark', async () => { it.skip('RFQ gas benchmark', async () => {

View File

@ -282,7 +282,7 @@ RFQ orders are a stripped down version of standard limit orders, supporting fewe
Some notable differences from regular limit orders are: Some notable differences from regular limit orders are:
* The only fill restrictions that can be placed on an RFQ order is on the ``tx.origin`` of the transaction. * The only fill restrictions that can be placed on an RFQ order is on the ``tx.origin`` and ``taker`` of the transaction.
* There are no taker token fees. * There are no taker token fees.
Structure Structure
@ -303,6 +303,8 @@ The ``RFQOrder`` struct has the following fields:
+-----------------+-------------+-----------------------------------------------------------------------------+ +-----------------+-------------+-----------------------------------------------------------------------------+
| ``maker`` | ``address`` | The address of the maker, and signer, of this order. | | ``maker`` | ``address`` | The address of the maker, and signer, of this order. |
+-----------------+-------------+-----------------------------------------------------------------------------+ +-----------------+-------------+-----------------------------------------------------------------------------+
| ``taker`` | ``address`` | Allowed taker address. Set to zero to allow any taker. |
+-----------------+-------------+-----------------------------------------------------------------------------+
| ``txOrigin`` | ``address`` | The allowed address of the EOA that submitted the Ethereum transaction. | | ``txOrigin`` | ``address`` | The allowed address of the EOA that submitted the Ethereum transaction. |
+-----------------+-------------+-----------------------------------------------------------------------------+ +-----------------+-------------+-----------------------------------------------------------------------------+
| ``pool`` | ``bytes32`` | The staking pool to attribute the 0x protocol fee from this order. | | ``pool`` | ``bytes32`` | The staking pool to attribute the 0x protocol fee from this order. |
@ -348,6 +350,7 @@ The hash of the order is used to uniquely identify an order inside the protocol.
'uint128 makerAmount,', 'uint128 makerAmount,',
'uint128 takerAmount,', 'uint128 takerAmount,',
'address maker,' 'address maker,'
'address taker,'
'address txOrigin,' 'address txOrigin,'
'bytes32 pool,', 'bytes32 pool,',
'uint64 expiry,', 'uint64 expiry,',
@ -359,6 +362,7 @@ The hash of the order is used to uniquely identify an order inside the protocol.
order.makerAmount, order.makerAmount,
order.takerAmount, order.takerAmount,
order.maker, order.maker,
order.taker,
order.txOrigin, order.txOrigin,
order.pool, order.pool,
order.expiry, order.expiry,
@ -421,7 +425,6 @@ RFQ orders can be filled with the ``fillRfqOrder()`` or ``fillOrKillRfqOrder()``
uint128 takerTokenFillAmount uint128 takerTokenFillAmount
) )
external external
payable
// How much maker token from the order the taker received. // How much maker token from the order the taker received.
returns (uint128 takerTokenFillAmount, uint128 makerTokenFillAmount); returns (uint128 takerTokenFillAmount, uint128 makerTokenFillAmount);
@ -438,7 +441,6 @@ RFQ orders can be filled with the ``fillRfqOrder()`` or ``fillOrKillRfqOrder()``
uint128 takerTokenFillAmount uint128 takerTokenFillAmount
) )
external external
payable
// How much maker token from the order the taker received. // How much maker token from the order the taker received.
returns (uint128 makerTokenFillAmount); returns (uint128 makerTokenFillAmount);