* Refactor Multiplex into multiple files * Pull UniswapV3 into separate file * Add support for multihop nested within batch sell * Add useSelfBalance and recipient to _fillRfqOrder * Expose onlySelf variant in UniswapV3Feature for Multiplex * Add useSelfBalance and recipient to _transformERC20 * Add support for proportional fill amounts in batchSell * Comments and renaming * Unit tests * Use caps for immutables * Rename taker -> recipient in TransformContext and SettleOrderInfo * lint * Address nits * Swallow reverts for LiquidityProvider and UniswapV2 batch sells * Address spot-check findings (#279) * Check didSucceed in _callWithOptionalBooleanResult * Add takerToken=ETH support to OtcOrdersFeature (#287) * Add takerToken=ETH support to OtcOrdersFeature * Add batchFillTakerSignedOtcOrders * Add support for OTC to Multiplex * Address PR feedback * Update TransformERC20Feature (#303) * remove multiplex_utils * Update changelog * unbreak tests
870 lines
41 KiB
TypeScript
870 lines
41 KiB
TypeScript
import { blockchainTests, constants, describe, expect, verifyEventsFromLogs } from '@0x/contracts-test-utils';
|
||
import { OrderStatus, OtcOrder, RevertErrors, SignatureType } from '@0x/protocol-utils';
|
||
import { BigNumber } from '@0x/utils';
|
||
|
||
import { IOwnableFeatureContract, IZeroExContract, IZeroExEvents } from '../../src/wrappers';
|
||
import { artifacts } from '../artifacts';
|
||
import { abis } from '../utils/abis';
|
||
import { fullMigrateAsync } from '../utils/migration';
|
||
import {
|
||
computeOtcOrderFilledAmounts,
|
||
createExpiry,
|
||
getRandomOtcOrder,
|
||
NativeOrdersTestEnvironment,
|
||
} from '../utils/orders';
|
||
import {
|
||
OtcOrdersFeatureContract,
|
||
TestMintableERC20TokenContract,
|
||
TestOrderSignerRegistryWithContractWalletContract,
|
||
TestWethContract,
|
||
} from '../wrappers';
|
||
|
||
blockchainTests.resets('OtcOrdersFeature', env => {
|
||
const { NULL_ADDRESS, MAX_UINT256, ZERO_AMOUNT: ZERO } = constants;
|
||
const ETH_TOKEN_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee';
|
||
let maker: string;
|
||
let taker: string;
|
||
let notMaker: string;
|
||
let notTaker: string;
|
||
let contractWalletOwner: string;
|
||
let contractWalletSigner: string;
|
||
let txOrigin: string;
|
||
let notTxOrigin: string;
|
||
let zeroEx: IZeroExContract;
|
||
let verifyingContract: string;
|
||
let makerToken: TestMintableERC20TokenContract;
|
||
let takerToken: TestMintableERC20TokenContract;
|
||
let wethToken: TestWethContract;
|
||
let contractWallet: TestOrderSignerRegistryWithContractWalletContract;
|
||
let testUtils: NativeOrdersTestEnvironment;
|
||
|
||
before(async () => {
|
||
// Useful for ETH balance accounting
|
||
const txDefaults = { ...env.txDefaults, gasPrice: 0 };
|
||
let owner;
|
||
[
|
||
owner,
|
||
maker,
|
||
taker,
|
||
notMaker,
|
||
notTaker,
|
||
contractWalletOwner,
|
||
contractWalletSigner,
|
||
txOrigin,
|
||
notTxOrigin,
|
||
] = await env.getAccountAddressesAsync();
|
||
[makerToken, takerToken] = await Promise.all(
|
||
[...new Array(2)].map(async () =>
|
||
TestMintableERC20TokenContract.deployFrom0xArtifactAsync(
|
||
artifacts.TestMintableERC20Token,
|
||
env.provider,
|
||
txDefaults,
|
||
artifacts,
|
||
),
|
||
),
|
||
);
|
||
wethToken = await TestWethContract.deployFrom0xArtifactAsync(
|
||
artifacts.TestWeth,
|
||
env.provider,
|
||
txDefaults,
|
||
artifacts,
|
||
);
|
||
zeroEx = await fullMigrateAsync(owner, env.provider, txDefaults, {}, { wethAddress: wethToken.address });
|
||
const otcFeatureImpl = await OtcOrdersFeatureContract.deployFrom0xArtifactAsync(
|
||
artifacts.OtcOrdersFeature,
|
||
env.provider,
|
||
txDefaults,
|
||
artifacts,
|
||
zeroEx.address,
|
||
wethToken.address,
|
||
);
|
||
await new IOwnableFeatureContract(zeroEx.address, env.provider, txDefaults, abis)
|
||
.migrate(otcFeatureImpl.address, otcFeatureImpl.migrate().getABIEncodedTransactionData(), owner)
|
||
.awaitTransactionSuccessAsync();
|
||
verifyingContract = zeroEx.address;
|
||
|
||
await Promise.all([
|
||
makerToken.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({ from: maker }),
|
||
makerToken.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({ from: notMaker }),
|
||
takerToken.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({ from: taker }),
|
||
takerToken.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({ from: notTaker }),
|
||
wethToken.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({ from: maker }),
|
||
wethToken.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({ from: notMaker }),
|
||
wethToken.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({ from: taker }),
|
||
wethToken.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({ from: notTaker }),
|
||
]);
|
||
|
||
// contract wallet for signer delegation
|
||
contractWallet = await TestOrderSignerRegistryWithContractWalletContract.deployFrom0xArtifactAsync(
|
||
artifacts.TestOrderSignerRegistryWithContractWallet,
|
||
env.provider,
|
||
{
|
||
from: contractWalletOwner,
|
||
},
|
||
artifacts,
|
||
zeroEx.address,
|
||
);
|
||
|
||
await contractWallet
|
||
.approveERC20(makerToken.address, zeroEx.address, MAX_UINT256)
|
||
.awaitTransactionSuccessAsync({ from: contractWalletOwner });
|
||
await contractWallet
|
||
.approveERC20(takerToken.address, zeroEx.address, MAX_UINT256)
|
||
.awaitTransactionSuccessAsync({ from: contractWalletOwner });
|
||
|
||
testUtils = new NativeOrdersTestEnvironment(maker, taker, makerToken, takerToken, zeroEx, ZERO, ZERO, env);
|
||
});
|
||
|
||
function getTestOtcOrder(fields: Partial<OtcOrder> = {}): OtcOrder {
|
||
return getRandomOtcOrder({
|
||
maker,
|
||
verifyingContract,
|
||
chainId: 1337,
|
||
takerToken: takerToken.address,
|
||
makerToken: makerToken.address,
|
||
taker: NULL_ADDRESS,
|
||
txOrigin: taker,
|
||
...fields,
|
||
});
|
||
}
|
||
|
||
describe('getOtcOrderHash()', () => {
|
||
it('returns the correct hash', async () => {
|
||
const order = getTestOtcOrder();
|
||
const hash = await zeroEx.getOtcOrderHash(order).callAsync();
|
||
expect(hash).to.eq(order.getHash());
|
||
});
|
||
});
|
||
|
||
describe('lastOtcTxOriginNonce()', () => {
|
||
it('returns 0 if bucket is unused', async () => {
|
||
const nonce = await zeroEx.lastOtcTxOriginNonce(taker, ZERO).callAsync();
|
||
expect(nonce).to.bignumber.eq(0);
|
||
});
|
||
it('returns the last nonce used in a bucket', async () => {
|
||
const order = getTestOtcOrder();
|
||
await testUtils.fillOtcOrderAsync(order);
|
||
const nonce = await zeroEx.lastOtcTxOriginNonce(taker, order.nonceBucket).callAsync();
|
||
expect(nonce).to.bignumber.eq(order.nonce);
|
||
});
|
||
});
|
||
|
||
describe('getOtcOrderInfo()', () => {
|
||
it('unfilled order', async () => {
|
||
const order = getTestOtcOrder();
|
||
const info = await zeroEx.getOtcOrderInfo(order).callAsync();
|
||
expect(info).to.deep.equal({
|
||
status: OrderStatus.Fillable,
|
||
orderHash: order.getHash(),
|
||
});
|
||
});
|
||
|
||
it('unfilled expired order', async () => {
|
||
const expiry = createExpiry(-60);
|
||
const order = getTestOtcOrder({ expiry });
|
||
const info = await zeroEx.getOtcOrderInfo(order).callAsync();
|
||
expect(info).to.deep.equal({
|
||
status: OrderStatus.Expired,
|
||
orderHash: order.getHash(),
|
||
});
|
||
});
|
||
|
||
it('filled then expired order', async () => {
|
||
const expiry = createExpiry(60);
|
||
const order = getTestOtcOrder({ expiry });
|
||
await testUtils.fillOtcOrderAsync(order);
|
||
// Advance time to expire the order.
|
||
await env.web3Wrapper.increaseTimeAsync(61);
|
||
const info = await zeroEx.getOtcOrderInfo(order).callAsync();
|
||
expect(info).to.deep.equal({
|
||
status: OrderStatus.Invalid,
|
||
orderHash: order.getHash(),
|
||
});
|
||
});
|
||
|
||
it('filled order', async () => {
|
||
const order = getTestOtcOrder();
|
||
// Fill the order first.
|
||
await testUtils.fillOtcOrderAsync(order);
|
||
const info = await zeroEx.getOtcOrderInfo(order).callAsync();
|
||
expect(info).to.deep.equal({
|
||
status: OrderStatus.Invalid,
|
||
orderHash: order.getHash(),
|
||
});
|
||
});
|
||
});
|
||
|
||
async function assertExpectedFinalBalancesFromOtcOrderFillAsync(
|
||
order: OtcOrder,
|
||
takerTokenFillAmount: BigNumber = order.takerAmount,
|
||
): Promise<void> {
|
||
const { makerTokenFilledAmount, takerTokenFilledAmount } = computeOtcOrderFilledAmounts(
|
||
order,
|
||
takerTokenFillAmount,
|
||
);
|
||
const makerBalance = await new TestMintableERC20TokenContract(order.takerToken, env.provider)
|
||
.balanceOf(order.maker)
|
||
.callAsync();
|
||
const takerBalance = await new TestMintableERC20TokenContract(order.makerToken, env.provider)
|
||
.balanceOf(order.taker !== NULL_ADDRESS ? order.taker : taker)
|
||
.callAsync();
|
||
expect(makerBalance, 'maker balance').to.bignumber.eq(takerTokenFilledAmount);
|
||
expect(takerBalance, 'taker balance').to.bignumber.eq(makerTokenFilledAmount);
|
||
}
|
||
|
||
describe('fillOtcOrder()', () => {
|
||
it('can fully fill an order', async () => {
|
||
const order = getTestOtcOrder();
|
||
const receipt = await testUtils.fillOtcOrderAsync(order);
|
||
verifyEventsFromLogs(
|
||
receipt.logs,
|
||
[testUtils.createOtcOrderFilledEventArgs(order)],
|
||
IZeroExEvents.OtcOrderFilled,
|
||
);
|
||
await assertExpectedFinalBalancesFromOtcOrderFillAsync(order);
|
||
});
|
||
|
||
it('can partially fill an order', async () => {
|
||
const order = getTestOtcOrder();
|
||
const fillAmount = order.takerAmount.minus(1);
|
||
const receipt = await testUtils.fillOtcOrderAsync(order, fillAmount);
|
||
verifyEventsFromLogs(
|
||
receipt.logs,
|
||
[testUtils.createOtcOrderFilledEventArgs(order, fillAmount)],
|
||
IZeroExEvents.OtcOrderFilled,
|
||
);
|
||
await assertExpectedFinalBalancesFromOtcOrderFillAsync(order, fillAmount);
|
||
});
|
||
|
||
it('clamps fill amount to remaining available', async () => {
|
||
const order = getTestOtcOrder();
|
||
const fillAmount = order.takerAmount.plus(1);
|
||
const receipt = await testUtils.fillOtcOrderAsync(order, fillAmount);
|
||
verifyEventsFromLogs(
|
||
receipt.logs,
|
||
[testUtils.createOtcOrderFilledEventArgs(order, fillAmount)],
|
||
IZeroExEvents.OtcOrderFilled,
|
||
);
|
||
await assertExpectedFinalBalancesFromOtcOrderFillAsync(order, fillAmount);
|
||
});
|
||
|
||
it('cannot fill an order with wrong tx.origin', async () => {
|
||
const order = getTestOtcOrder();
|
||
const tx = testUtils.fillOtcOrderAsync(order, order.takerAmount, notTaker);
|
||
return expect(tx).to.revertWith(
|
||
new RevertErrors.NativeOrders.OrderNotFillableByOriginError(order.getHash(), notTaker, taker),
|
||
);
|
||
});
|
||
|
||
it('cannot fill an order with wrong taker', async () => {
|
||
const order = getTestOtcOrder({ taker: notTaker });
|
||
const tx = testUtils.fillOtcOrderAsync(order);
|
||
return expect(tx).to.revertWith(
|
||
new RevertErrors.NativeOrders.OrderNotFillableByTakerError(order.getHash(), taker, notTaker),
|
||
);
|
||
});
|
||
|
||
it('can fill an order from a different tx.origin if registered', async () => {
|
||
const order = getTestOtcOrder();
|
||
await zeroEx.registerAllowedRfqOrigins([notTaker], true).awaitTransactionSuccessAsync({ from: taker });
|
||
return testUtils.fillOtcOrderAsync(order, order.takerAmount, notTaker);
|
||
});
|
||
|
||
it('cannot fill an order with registered then unregistered tx.origin', async () => {
|
||
const order = getTestOtcOrder();
|
||
await zeroEx.registerAllowedRfqOrigins([notTaker], true).awaitTransactionSuccessAsync({ from: taker });
|
||
await zeroEx.registerAllowedRfqOrigins([notTaker], false).awaitTransactionSuccessAsync({ from: taker });
|
||
const tx = testUtils.fillOtcOrderAsync(order, order.takerAmount, notTaker);
|
||
return expect(tx).to.revertWith(
|
||
new RevertErrors.NativeOrders.OrderNotFillableByOriginError(order.getHash(), notTaker, taker),
|
||
);
|
||
});
|
||
|
||
it('cannot fill an order with a zero tx.origin', async () => {
|
||
const order = getTestOtcOrder({ txOrigin: NULL_ADDRESS });
|
||
const tx = testUtils.fillOtcOrderAsync(order);
|
||
return expect(tx).to.revertWith(
|
||
new RevertErrors.NativeOrders.OrderNotFillableByOriginError(order.getHash(), taker, NULL_ADDRESS),
|
||
);
|
||
});
|
||
|
||
it('cannot fill an expired order', async () => {
|
||
const order = getTestOtcOrder({ expiry: createExpiry(-60) });
|
||
const tx = testUtils.fillOtcOrderAsync(order);
|
||
return expect(tx).to.revertWith(
|
||
new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Expired),
|
||
);
|
||
});
|
||
|
||
it('cannot fill order with bad signature', async () => {
|
||
const order = getTestOtcOrder();
|
||
// Overwrite chainId to result in a different hash and therefore different
|
||
// signature.
|
||
const tx = testUtils.fillOtcOrderAsync(order.clone({ chainId: 1234 }));
|
||
return expect(tx).to.revertWith(
|
||
new RevertErrors.NativeOrders.OrderNotSignedByMakerError(order.getHash(), undefined, order.maker),
|
||
);
|
||
});
|
||
|
||
it('fails if ETH is attached', async () => {
|
||
const order = getTestOtcOrder();
|
||
await testUtils.prepareBalancesForOrdersAsync([order], taker);
|
||
const tx = zeroEx
|
||
.fillOtcOrder(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('cannot fill the same order twice', async () => {
|
||
const order = getTestOtcOrder();
|
||
await testUtils.fillOtcOrderAsync(order);
|
||
const tx = testUtils.fillOtcOrderAsync(order);
|
||
return expect(tx).to.revertWith(
|
||
new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Invalid),
|
||
);
|
||
});
|
||
|
||
it('cannot fill two orders with the same nonceBucket and nonce', async () => {
|
||
const order1 = getTestOtcOrder();
|
||
await testUtils.fillOtcOrderAsync(order1);
|
||
const order2 = getTestOtcOrder({ nonceBucket: order1.nonceBucket, nonce: order1.nonce });
|
||
const tx = testUtils.fillOtcOrderAsync(order2);
|
||
return expect(tx).to.revertWith(
|
||
new RevertErrors.NativeOrders.OrderNotFillableError(order2.getHash(), OrderStatus.Invalid),
|
||
);
|
||
});
|
||
|
||
it('cannot fill an order whose nonce is less than the nonce last used in that bucket', async () => {
|
||
const order1 = getTestOtcOrder();
|
||
await testUtils.fillOtcOrderAsync(order1);
|
||
const order2 = getTestOtcOrder({ nonceBucket: order1.nonceBucket, nonce: order1.nonce.minus(1) });
|
||
const tx = testUtils.fillOtcOrderAsync(order2);
|
||
return expect(tx).to.revertWith(
|
||
new RevertErrors.NativeOrders.OrderNotFillableError(order2.getHash(), OrderStatus.Invalid),
|
||
);
|
||
});
|
||
|
||
it('can fill two orders that use the same nonce bucket and increasing nonces', async () => {
|
||
const order1 = getTestOtcOrder();
|
||
const tx1 = await testUtils.fillOtcOrderAsync(order1);
|
||
verifyEventsFromLogs(
|
||
tx1.logs,
|
||
[testUtils.createOtcOrderFilledEventArgs(order1)],
|
||
IZeroExEvents.OtcOrderFilled,
|
||
);
|
||
const order2 = getTestOtcOrder({ nonceBucket: order1.nonceBucket, nonce: order1.nonce.plus(1) });
|
||
const tx2 = await testUtils.fillOtcOrderAsync(order2);
|
||
verifyEventsFromLogs(
|
||
tx2.logs,
|
||
[testUtils.createOtcOrderFilledEventArgs(order2)],
|
||
IZeroExEvents.OtcOrderFilled,
|
||
);
|
||
});
|
||
|
||
it('can fill two orders that use the same nonce but different nonce buckets', async () => {
|
||
const order1 = getTestOtcOrder();
|
||
const tx1 = await testUtils.fillOtcOrderAsync(order1);
|
||
verifyEventsFromLogs(
|
||
tx1.logs,
|
||
[testUtils.createOtcOrderFilledEventArgs(order1)],
|
||
IZeroExEvents.OtcOrderFilled,
|
||
);
|
||
const order2 = getTestOtcOrder({ nonce: order1.nonce });
|
||
const tx2 = await testUtils.fillOtcOrderAsync(order2);
|
||
verifyEventsFromLogs(
|
||
tx2.logs,
|
||
[testUtils.createOtcOrderFilledEventArgs(order2)],
|
||
IZeroExEvents.OtcOrderFilled,
|
||
);
|
||
});
|
||
|
||
it('can fill a WETH buy order and receive ETH', async () => {
|
||
const takerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker);
|
||
const order = getTestOtcOrder({ makerToken: wethToken.address, makerAmount: new BigNumber('1e18') });
|
||
await wethToken.deposit().awaitTransactionSuccessAsync({ from: maker, value: order.makerAmount });
|
||
const receipt = await testUtils.fillOtcOrderAsync(order, order.takerAmount, taker, true);
|
||
verifyEventsFromLogs(
|
||
receipt.logs,
|
||
[testUtils.createOtcOrderFilledEventArgs(order)],
|
||
IZeroExEvents.OtcOrderFilled,
|
||
);
|
||
const takerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(taker);
|
||
expect(takerEthBalanceAfter.minus(takerEthBalanceBefore)).to.bignumber.equal(order.makerAmount);
|
||
});
|
||
|
||
it('reverts if `unwrapWeth` is true but maker token is not WETH', async () => {
|
||
const order = getTestOtcOrder();
|
||
const tx = testUtils.fillOtcOrderAsync(order, order.takerAmount, taker, true);
|
||
return expect(tx).to.revertWith('OtcOrdersFeature::fillOtcOrderForEth/MAKER_TOKEN_NOT_WETH');
|
||
});
|
||
|
||
it('allows for fills on orders signed by a approved signer', async () => {
|
||
const order = getTestOtcOrder({ maker: contractWallet.address });
|
||
const sig = await order.getSignatureWithProviderAsync(
|
||
env.provider,
|
||
SignatureType.EthSign,
|
||
contractWalletSigner,
|
||
);
|
||
// covers taker
|
||
await testUtils.prepareBalancesForOrdersAsync([order]);
|
||
// need to provide contract wallet with a balance
|
||
await makerToken.mint(contractWallet.address, order.makerAmount).awaitTransactionSuccessAsync();
|
||
// allow signer
|
||
await contractWallet
|
||
.registerAllowedOrderSigner(contractWalletSigner, true)
|
||
.awaitTransactionSuccessAsync({ from: contractWalletOwner });
|
||
// fill should succeed
|
||
const receipt = await zeroEx
|
||
.fillOtcOrder(order, sig, order.takerAmount)
|
||
.awaitTransactionSuccessAsync({ from: taker });
|
||
verifyEventsFromLogs(
|
||
receipt.logs,
|
||
[testUtils.createOtcOrderFilledEventArgs(order)],
|
||
IZeroExEvents.OtcOrderFilled,
|
||
);
|
||
await assertExpectedFinalBalancesFromOtcOrderFillAsync(order);
|
||
});
|
||
|
||
it('disallows fills if the signer is revoked', async () => {
|
||
const order = getTestOtcOrder({ maker: contractWallet.address });
|
||
const sig = await order.getSignatureWithProviderAsync(
|
||
env.provider,
|
||
SignatureType.EthSign,
|
||
contractWalletSigner,
|
||
);
|
||
// covers taker
|
||
await testUtils.prepareBalancesForOrdersAsync([order]);
|
||
// need to provide contract wallet with a balance
|
||
await makerToken.mint(contractWallet.address, order.makerAmount).awaitTransactionSuccessAsync();
|
||
// first allow signer
|
||
await contractWallet
|
||
.registerAllowedOrderSigner(contractWalletSigner, true)
|
||
.awaitTransactionSuccessAsync({ from: contractWalletOwner });
|
||
// then disallow signer
|
||
await contractWallet
|
||
.registerAllowedOrderSigner(contractWalletSigner, false)
|
||
.awaitTransactionSuccessAsync({ from: contractWalletOwner });
|
||
// fill should revert
|
||
const tx = zeroEx.fillOtcOrder(order, sig, order.takerAmount).awaitTransactionSuccessAsync({ from: taker });
|
||
return expect(tx).to.revertWith(
|
||
new RevertErrors.NativeOrders.OrderNotSignedByMakerError(
|
||
order.getHash(),
|
||
contractWalletSigner,
|
||
order.maker,
|
||
),
|
||
);
|
||
});
|
||
|
||
it(`doesn't allow fills with an unapproved signer`, async () => {
|
||
const order = getTestOtcOrder({ maker: contractWallet.address });
|
||
const sig = await order.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, maker);
|
||
// covers taker
|
||
await testUtils.prepareBalancesForOrdersAsync([order]);
|
||
// need to provide contract wallet with a balance
|
||
await makerToken.mint(contractWallet.address, order.makerAmount).awaitTransactionSuccessAsync();
|
||
// fill should revert
|
||
const tx = zeroEx.fillOtcOrder(order, sig, order.takerAmount).awaitTransactionSuccessAsync({ from: taker });
|
||
return expect(tx).to.revertWith(
|
||
new RevertErrors.NativeOrders.OrderNotSignedByMakerError(order.getHash(), maker, order.maker),
|
||
);
|
||
});
|
||
});
|
||
describe('fillOtcOrderWithEth()', () => {
|
||
it('Can fill an order with ETH (takerToken=WETH)', async () => {
|
||
const order = getTestOtcOrder({ takerToken: wethToken.address });
|
||
const receipt = await testUtils.fillOtcOrderWithEthAsync(order);
|
||
verifyEventsFromLogs(
|
||
receipt.logs,
|
||
[testUtils.createOtcOrderFilledEventArgs(order)],
|
||
IZeroExEvents.OtcOrderFilled,
|
||
);
|
||
await assertExpectedFinalBalancesFromOtcOrderFillAsync(order);
|
||
});
|
||
it('Can fill an order with ETH (takerToken=ETH)', async () => {
|
||
const order = getTestOtcOrder({ takerToken: ETH_TOKEN_ADDRESS });
|
||
const makerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(maker);
|
||
const receipt = await testUtils.fillOtcOrderWithEthAsync(order);
|
||
verifyEventsFromLogs(
|
||
receipt.logs,
|
||
[testUtils.createOtcOrderFilledEventArgs(order)],
|
||
IZeroExEvents.OtcOrderFilled,
|
||
);
|
||
const takerBalance = await new TestMintableERC20TokenContract(order.makerToken, env.provider)
|
||
.balanceOf(taker)
|
||
.callAsync();
|
||
expect(takerBalance, 'taker balance').to.bignumber.eq(order.makerAmount);
|
||
const makerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(maker);
|
||
expect(makerEthBalanceAfter.minus(makerEthBalanceBefore), 'maker balance').to.bignumber.equal(
|
||
order.takerAmount,
|
||
);
|
||
});
|
||
it('Can partially fill an order with ETH (takerToken=WETH)', async () => {
|
||
const order = getTestOtcOrder({ takerToken: wethToken.address });
|
||
const fillAmount = order.takerAmount.minus(1);
|
||
const receipt = await testUtils.fillOtcOrderWithEthAsync(order, fillAmount);
|
||
verifyEventsFromLogs(
|
||
receipt.logs,
|
||
[testUtils.createOtcOrderFilledEventArgs(order, fillAmount)],
|
||
IZeroExEvents.OtcOrderFilled,
|
||
);
|
||
await assertExpectedFinalBalancesFromOtcOrderFillAsync(order, fillAmount);
|
||
});
|
||
it('Can partially fill an order with ETH (takerToken=ETH)', async () => {
|
||
const order = getTestOtcOrder({ takerToken: ETH_TOKEN_ADDRESS });
|
||
const fillAmount = order.takerAmount.minus(1);
|
||
const makerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(maker);
|
||
const receipt = await testUtils.fillOtcOrderWithEthAsync(order, fillAmount);
|
||
verifyEventsFromLogs(
|
||
receipt.logs,
|
||
[testUtils.createOtcOrderFilledEventArgs(order, fillAmount)],
|
||
IZeroExEvents.OtcOrderFilled,
|
||
);
|
||
const { makerTokenFilledAmount, takerTokenFilledAmount } = computeOtcOrderFilledAmounts(order, fillAmount);
|
||
const takerBalance = await new TestMintableERC20TokenContract(order.makerToken, env.provider)
|
||
.balanceOf(taker)
|
||
.callAsync();
|
||
expect(takerBalance, 'taker balance').to.bignumber.eq(makerTokenFilledAmount);
|
||
const makerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(maker);
|
||
expect(makerEthBalanceAfter.minus(makerEthBalanceBefore), 'maker balance').to.bignumber.equal(
|
||
takerTokenFilledAmount,
|
||
);
|
||
});
|
||
it('Can refund excess ETH is msg.value > order.takerAmount (takerToken=WETH)', async () => {
|
||
const order = getTestOtcOrder({ takerToken: wethToken.address });
|
||
const fillAmount = order.takerAmount.plus(420);
|
||
const takerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker);
|
||
const receipt = await testUtils.fillOtcOrderWithEthAsync(order, fillAmount);
|
||
verifyEventsFromLogs(
|
||
receipt.logs,
|
||
[testUtils.createOtcOrderFilledEventArgs(order)],
|
||
IZeroExEvents.OtcOrderFilled,
|
||
);
|
||
const takerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(taker);
|
||
expect(takerEthBalanceBefore.minus(takerEthBalanceAfter)).to.bignumber.equal(order.takerAmount);
|
||
await assertExpectedFinalBalancesFromOtcOrderFillAsync(order);
|
||
});
|
||
it('Can refund excess ETH is msg.value > order.takerAmount (takerToken=ETH)', async () => {
|
||
const order = getTestOtcOrder({ takerToken: ETH_TOKEN_ADDRESS });
|
||
const fillAmount = order.takerAmount.plus(420);
|
||
const takerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker);
|
||
const makerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(maker);
|
||
const receipt = await testUtils.fillOtcOrderWithEthAsync(order, fillAmount);
|
||
verifyEventsFromLogs(
|
||
receipt.logs,
|
||
[testUtils.createOtcOrderFilledEventArgs(order)],
|
||
IZeroExEvents.OtcOrderFilled,
|
||
);
|
||
const takerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(taker);
|
||
expect(takerEthBalanceBefore.minus(takerEthBalanceAfter), 'taker eth balance').to.bignumber.equal(
|
||
order.takerAmount,
|
||
);
|
||
const takerBalance = await new TestMintableERC20TokenContract(order.makerToken, env.provider)
|
||
.balanceOf(taker)
|
||
.callAsync();
|
||
expect(takerBalance, 'taker balance').to.bignumber.eq(order.makerAmount);
|
||
const makerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(maker);
|
||
expect(makerEthBalanceAfter.minus(makerEthBalanceBefore), 'maker balance').to.bignumber.equal(
|
||
order.takerAmount,
|
||
);
|
||
});
|
||
it('Cannot fill an order if taker token is not ETH or WETH', async () => {
|
||
const order = getTestOtcOrder();
|
||
const tx = testUtils.fillOtcOrderWithEthAsync(order);
|
||
return expect(tx).to.revertWith('OtcOrdersFeature::fillOtcOrderWithEth/INVALID_TAKER_TOKEN');
|
||
});
|
||
});
|
||
|
||
describe('fillTakerSignedOtcOrder()', () => {
|
||
it('can fully fill an order', async () => {
|
||
const order = getTestOtcOrder({ taker, txOrigin });
|
||
const receipt = await testUtils.fillTakerSignedOtcOrderAsync(order);
|
||
verifyEventsFromLogs(
|
||
receipt.logs,
|
||
[testUtils.createOtcOrderFilledEventArgs(order)],
|
||
IZeroExEvents.OtcOrderFilled,
|
||
);
|
||
await assertExpectedFinalBalancesFromOtcOrderFillAsync(order);
|
||
});
|
||
|
||
it('cannot fill an order with wrong tx.origin', async () => {
|
||
const order = getTestOtcOrder({ taker, txOrigin });
|
||
const tx = testUtils.fillTakerSignedOtcOrderAsync(order, notTxOrigin);
|
||
return expect(tx).to.revertWith(
|
||
new RevertErrors.NativeOrders.OrderNotFillableByOriginError(order.getHash(), notTxOrigin, txOrigin),
|
||
);
|
||
});
|
||
|
||
it('can fill an order from a different tx.origin if registered', async () => {
|
||
const order = getTestOtcOrder({ taker, txOrigin });
|
||
await zeroEx
|
||
.registerAllowedRfqOrigins([notTxOrigin], true)
|
||
.awaitTransactionSuccessAsync({ from: txOrigin });
|
||
return testUtils.fillTakerSignedOtcOrderAsync(order, notTxOrigin);
|
||
});
|
||
|
||
it('cannot fill an order with registered then unregistered tx.origin', async () => {
|
||
const order = getTestOtcOrder({ taker, txOrigin });
|
||
await zeroEx
|
||
.registerAllowedRfqOrigins([notTxOrigin], true)
|
||
.awaitTransactionSuccessAsync({ from: txOrigin });
|
||
await zeroEx
|
||
.registerAllowedRfqOrigins([notTxOrigin], false)
|
||
.awaitTransactionSuccessAsync({ from: txOrigin });
|
||
const tx = testUtils.fillTakerSignedOtcOrderAsync(order, notTxOrigin);
|
||
return expect(tx).to.revertWith(
|
||
new RevertErrors.NativeOrders.OrderNotFillableByOriginError(order.getHash(), notTxOrigin, txOrigin),
|
||
);
|
||
});
|
||
|
||
it('cannot fill an order with a zero tx.origin', async () => {
|
||
const order = getTestOtcOrder({ taker, txOrigin: NULL_ADDRESS });
|
||
const tx = testUtils.fillTakerSignedOtcOrderAsync(order, txOrigin);
|
||
return expect(tx).to.revertWith(
|
||
new RevertErrors.NativeOrders.OrderNotFillableByOriginError(order.getHash(), txOrigin, NULL_ADDRESS),
|
||
);
|
||
});
|
||
|
||
it('cannot fill an expired order', async () => {
|
||
const order = getTestOtcOrder({ taker, txOrigin, expiry: createExpiry(-60) });
|
||
const tx = testUtils.fillTakerSignedOtcOrderAsync(order);
|
||
return expect(tx).to.revertWith(
|
||
new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Expired),
|
||
);
|
||
});
|
||
|
||
it('cannot fill an order with bad taker signature', async () => {
|
||
const order = getTestOtcOrder({ taker, txOrigin });
|
||
const tx = testUtils.fillTakerSignedOtcOrderAsync(order, txOrigin, notTaker);
|
||
return expect(tx).to.revertWith(
|
||
new RevertErrors.NativeOrders.OrderNotFillableByTakerError(order.getHash(), notTaker, taker),
|
||
);
|
||
});
|
||
|
||
it('cannot fill order with bad maker signature', async () => {
|
||
const order = getTestOtcOrder({ taker, txOrigin });
|
||
const anotherOrder = getTestOtcOrder({ taker, txOrigin });
|
||
await testUtils.prepareBalancesForOrdersAsync([order], taker);
|
||
const tx = zeroEx
|
||
.fillTakerSignedOtcOrder(
|
||
order,
|
||
await anotherOrder.getSignatureWithProviderAsync(env.provider),
|
||
await order.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, taker),
|
||
)
|
||
.awaitTransactionSuccessAsync({ from: txOrigin });
|
||
|
||
return expect(tx).to.revertWith(
|
||
new RevertErrors.NativeOrders.OrderNotSignedByMakerError(order.getHash(), undefined, order.maker),
|
||
);
|
||
});
|
||
|
||
it('fails if ETH is attached', async () => {
|
||
const order = getTestOtcOrder({ taker, txOrigin });
|
||
await testUtils.prepareBalancesForOrdersAsync([order], taker);
|
||
const tx = zeroEx
|
||
.fillTakerSignedOtcOrder(
|
||
order,
|
||
await order.getSignatureWithProviderAsync(env.provider),
|
||
await order.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, taker),
|
||
)
|
||
.awaitTransactionSuccessAsync({ from: txOrigin, value: 1 });
|
||
// This will revert at the language level because the fill function is not payable.
|
||
return expect(tx).to.be.rejectedWith('revert');
|
||
});
|
||
|
||
it('cannot fill the same order twice', async () => {
|
||
const order = getTestOtcOrder({ taker, txOrigin });
|
||
await testUtils.fillTakerSignedOtcOrderAsync(order);
|
||
const tx = testUtils.fillTakerSignedOtcOrderAsync(order);
|
||
return expect(tx).to.revertWith(
|
||
new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Invalid),
|
||
);
|
||
});
|
||
|
||
it('cannot fill two orders with the same nonceBucket and nonce', async () => {
|
||
const order1 = getTestOtcOrder({ taker, txOrigin });
|
||
await testUtils.fillTakerSignedOtcOrderAsync(order1);
|
||
const order2 = getTestOtcOrder({ taker, txOrigin, nonceBucket: order1.nonceBucket, nonce: order1.nonce });
|
||
const tx = testUtils.fillTakerSignedOtcOrderAsync(order2);
|
||
return expect(tx).to.revertWith(
|
||
new RevertErrors.NativeOrders.OrderNotFillableError(order2.getHash(), OrderStatus.Invalid),
|
||
);
|
||
});
|
||
|
||
it('cannot fill an order whose nonce is less than the nonce last used in that bucket', async () => {
|
||
const order1 = getTestOtcOrder({ taker, txOrigin });
|
||
await testUtils.fillTakerSignedOtcOrderAsync(order1);
|
||
const order2 = getTestOtcOrder({
|
||
taker,
|
||
txOrigin,
|
||
nonceBucket: order1.nonceBucket,
|
||
nonce: order1.nonce.minus(1),
|
||
});
|
||
const tx = testUtils.fillTakerSignedOtcOrderAsync(order2);
|
||
return expect(tx).to.revertWith(
|
||
new RevertErrors.NativeOrders.OrderNotFillableError(order2.getHash(), OrderStatus.Invalid),
|
||
);
|
||
});
|
||
|
||
it('can fill two orders that use the same nonce bucket and increasing nonces', async () => {
|
||
const order1 = getTestOtcOrder({ taker, txOrigin });
|
||
const tx1 = await testUtils.fillTakerSignedOtcOrderAsync(order1);
|
||
verifyEventsFromLogs(
|
||
tx1.logs,
|
||
[testUtils.createOtcOrderFilledEventArgs(order1)],
|
||
IZeroExEvents.OtcOrderFilled,
|
||
);
|
||
const order2 = getTestOtcOrder({
|
||
taker,
|
||
txOrigin,
|
||
nonceBucket: order1.nonceBucket,
|
||
nonce: order1.nonce.plus(1),
|
||
});
|
||
const tx2 = await testUtils.fillTakerSignedOtcOrderAsync(order2);
|
||
verifyEventsFromLogs(
|
||
tx2.logs,
|
||
[testUtils.createOtcOrderFilledEventArgs(order2)],
|
||
IZeroExEvents.OtcOrderFilled,
|
||
);
|
||
});
|
||
|
||
it('can fill two orders that use the same nonce but different nonce buckets', async () => {
|
||
const order1 = getTestOtcOrder({ taker, txOrigin });
|
||
const tx1 = await testUtils.fillTakerSignedOtcOrderAsync(order1);
|
||
verifyEventsFromLogs(
|
||
tx1.logs,
|
||
[testUtils.createOtcOrderFilledEventArgs(order1)],
|
||
IZeroExEvents.OtcOrderFilled,
|
||
);
|
||
const order2 = getTestOtcOrder({ taker, txOrigin, nonce: order1.nonce });
|
||
const tx2 = await testUtils.fillTakerSignedOtcOrderAsync(order2);
|
||
verifyEventsFromLogs(
|
||
tx2.logs,
|
||
[testUtils.createOtcOrderFilledEventArgs(order2)],
|
||
IZeroExEvents.OtcOrderFilled,
|
||
);
|
||
});
|
||
|
||
it('can fill a WETH buy order and receive ETH', async () => {
|
||
const takerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker);
|
||
const order = getTestOtcOrder({
|
||
taker,
|
||
txOrigin,
|
||
makerToken: wethToken.address,
|
||
makerAmount: new BigNumber('1e18'),
|
||
});
|
||
await wethToken.deposit().awaitTransactionSuccessAsync({ from: maker, value: order.makerAmount });
|
||
const receipt = await testUtils.fillTakerSignedOtcOrderAsync(order, txOrigin, taker, true);
|
||
verifyEventsFromLogs(
|
||
receipt.logs,
|
||
[testUtils.createOtcOrderFilledEventArgs(order)],
|
||
IZeroExEvents.OtcOrderFilled,
|
||
);
|
||
const takerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(taker);
|
||
expect(takerEthBalanceAfter.minus(takerEthBalanceBefore)).to.bignumber.equal(order.makerAmount);
|
||
});
|
||
|
||
it('reverts if `unwrapWeth` is true but maker token is not WETH', async () => {
|
||
const order = getTestOtcOrder({ taker, txOrigin });
|
||
const tx = testUtils.fillTakerSignedOtcOrderAsync(order, txOrigin, taker, true);
|
||
return expect(tx).to.revertWith('OtcOrdersFeature::fillTakerSignedOtcOrder/MAKER_TOKEN_NOT_WETH');
|
||
});
|
||
});
|
||
|
||
describe('batchFillTakerSignedOtcOrders()', () => {
|
||
it('Fills multiple orders', async () => {
|
||
const order1 = getTestOtcOrder({ taker, txOrigin });
|
||
const order2 = getTestOtcOrder({
|
||
taker: notTaker,
|
||
txOrigin,
|
||
nonceBucket: order1.nonceBucket,
|
||
nonce: order1.nonce.plus(1),
|
||
});
|
||
await testUtils.prepareBalancesForOrdersAsync([order1], taker);
|
||
await testUtils.prepareBalancesForOrdersAsync([order2], notTaker);
|
||
const tx = await zeroEx
|
||
.batchFillTakerSignedOtcOrders(
|
||
[order1, order2],
|
||
[
|
||
await order1.getSignatureWithProviderAsync(env.provider),
|
||
await order2.getSignatureWithProviderAsync(env.provider),
|
||
],
|
||
[
|
||
await order1.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, taker),
|
||
await order2.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, notTaker),
|
||
],
|
||
[false, false],
|
||
)
|
||
.awaitTransactionSuccessAsync({ from: txOrigin });
|
||
verifyEventsFromLogs(
|
||
tx.logs,
|
||
[testUtils.createOtcOrderFilledEventArgs(order1), testUtils.createOtcOrderFilledEventArgs(order2)],
|
||
IZeroExEvents.OtcOrderFilled,
|
||
);
|
||
});
|
||
it('Fills multiple orders and unwraps WETH', async () => {
|
||
const order1 = getTestOtcOrder({ taker, txOrigin });
|
||
const order2 = getTestOtcOrder({
|
||
taker: notTaker,
|
||
txOrigin,
|
||
nonceBucket: order1.nonceBucket,
|
||
nonce: order1.nonce.plus(1),
|
||
makerToken: wethToken.address,
|
||
makerAmount: new BigNumber('1e18'),
|
||
});
|
||
await testUtils.prepareBalancesForOrdersAsync([order1], taker);
|
||
await testUtils.prepareBalancesForOrdersAsync([order2], notTaker);
|
||
await wethToken.deposit().awaitTransactionSuccessAsync({ from: maker, value: order2.makerAmount });
|
||
const tx = await zeroEx
|
||
.batchFillTakerSignedOtcOrders(
|
||
[order1, order2],
|
||
[
|
||
await order1.getSignatureWithProviderAsync(env.provider),
|
||
await order2.getSignatureWithProviderAsync(env.provider),
|
||
],
|
||
[
|
||
await order1.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, taker),
|
||
await order2.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, notTaker),
|
||
],
|
||
[false, true],
|
||
)
|
||
.awaitTransactionSuccessAsync({ from: txOrigin });
|
||
verifyEventsFromLogs(
|
||
tx.logs,
|
||
[testUtils.createOtcOrderFilledEventArgs(order1), testUtils.createOtcOrderFilledEventArgs(order2)],
|
||
IZeroExEvents.OtcOrderFilled,
|
||
);
|
||
});
|
||
it('Skips over unfillable orders', async () => {
|
||
const order1 = getTestOtcOrder({ taker, txOrigin });
|
||
const order2 = getTestOtcOrder({
|
||
taker: notTaker,
|
||
txOrigin,
|
||
nonceBucket: order1.nonceBucket,
|
||
nonce: order1.nonce.plus(1),
|
||
});
|
||
await testUtils.prepareBalancesForOrdersAsync([order1], taker);
|
||
await testUtils.prepareBalancesForOrdersAsync([order2], notTaker);
|
||
const tx = await zeroEx
|
||
.batchFillTakerSignedOtcOrders(
|
||
[order1, order2],
|
||
[
|
||
await order1.getSignatureWithProviderAsync(env.provider),
|
||
await order2.getSignatureWithProviderAsync(env.provider),
|
||
],
|
||
[
|
||
await order1.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, taker),
|
||
await order2.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, taker), // Invalid signature for order2
|
||
],
|
||
[false, false],
|
||
)
|
||
.awaitTransactionSuccessAsync({ from: txOrigin });
|
||
verifyEventsFromLogs(
|
||
tx.logs,
|
||
[testUtils.createOtcOrderFilledEventArgs(order1)],
|
||
IZeroExEvents.OtcOrderFilled,
|
||
);
|
||
});
|
||
});
|
||
});
|