Address spot check feedback (#251)

* UniswapV3 VIP (#237)

* `@0x/contracts-zero-ex`: Add UniswapV3Feature

* `@0x/contracts-zero-ex`: Add UniswapV3 VIP
`@0x/contract-artifacts`: Regenerate.
`@0x/contract-wrappers`: Regenerate.
`@0x/asset-swapper`: Add UniswapV3 VIP support.

* address review comments and appease linter

* `@0x/contracts-zero-ex`: Add UniswapV3Feature tests

* Multiplex UniswapV3 (#241)

* Add UniswapV3 support to Multiplex batchFill

* Add AssetSwapper support for Multiplex UniswapV3

* fix repo scripts that use PKG= env var (#242)

Co-authored-by: Lawrence Forman <me@merklejerk.com>

* `@0x/asset-swapper`: Adjust uniswap gas overhead

Co-authored-by: Lawrence Forman <me@merklejerk.com>
Co-authored-by: mzhu25 <mchl.zhu.96@gmail.com>

* OTC orders feature (#244)

* Add OTC orders feature contracts

* Address PR feedback

* Remove partial fills for takerSigned variant

* Add function to query the min valid nonce

* Add ETH support

* Tightly pack expiry, nonceBucket, and nonce

* Address PR feedback

* OTC orders unit tests

* Bump prettier version

* Skip unnecessary math if takerTokenFillAmount == order.takerAmount

* appease CI

* Update contract-artifacts and contract-wrappers and CHANGELOGs

* `@0x/contracts-zero-ex`: Address spot check feedback

* `regen wrappers

* prettier

* `@0x/asset-swapper`: prettier and tweak gas schedule slightly for uni3

Co-authored-by: Lawrence Forman <me@merklejerk.com>
Co-authored-by: mzhu25 <mchl.zhu.96@gmail.com>
This commit is contained in:
Lawrence Forman
2021-06-02 00:21:14 -04:00
committed by GitHub
parent 289474e2ce
commit 901d400d62
112 changed files with 6392 additions and 794 deletions

View File

@@ -141,7 +141,12 @@ blockchainTests.resets('BatchFillNativeOrdersFeature', env => {
await testUtils.prepareBalancesForOrdersAsync(orders);
const value = testUtils.protocolFee.times(orders.length);
const tx = await feature
.batchFillLimitOrders(orders, signatures, orders.map(order => order.takerAmount), false)
.batchFillLimitOrders(
orders,
signatures,
orders.map(order => order.takerAmount),
false,
)
.awaitTransactionSuccessAsync({ from: taker, value });
const [orderInfos] = await zeroEx.batchGetLimitOrderRelevantStates(orders, signatures).callAsync();
orderInfos.map((orderInfo, i) =>
@@ -192,7 +197,12 @@ blockchainTests.resets('BatchFillNativeOrdersFeature', env => {
await testUtils.prepareBalancesForOrdersAsync(orders);
const value = testUtils.protocolFee.times(orders.length).plus(420);
const tx = await feature
.batchFillLimitOrders(orders, signatures, orders.map(order => order.takerAmount), false)
.batchFillLimitOrders(
orders,
signatures,
orders.map(order => order.takerAmount),
false,
)
.awaitTransactionSuccessAsync({ from: taker, value });
const [orderInfos] = await zeroEx.batchGetLimitOrderRelevantStates(orders, signatures).callAsync();
orderInfos.map((orderInfo, i) =>
@@ -219,7 +229,12 @@ blockchainTests.resets('BatchFillNativeOrdersFeature', env => {
await testUtils.prepareBalancesForOrdersAsync(orders);
const value = testUtils.protocolFee.times(orders.length);
const tx = await feature
.batchFillLimitOrders(orders, signatures, orders.map(order => order.takerAmount), false)
.batchFillLimitOrders(
orders,
signatures,
orders.map(order => order.takerAmount),
false,
)
.awaitTransactionSuccessAsync({ from: taker, value });
const [orderInfos] = await zeroEx.batchGetLimitOrderRelevantStates(orders, signatures).callAsync();
const [expiredOrderInfo, ...filledOrderInfos] = orderInfos;
@@ -250,7 +265,12 @@ blockchainTests.resets('BatchFillNativeOrdersFeature', env => {
await testUtils.prepareBalancesForOrdersAsync(orders);
const value = testUtils.protocolFee.times(orders.length);
const tx = await feature
.batchFillLimitOrders(orders, signatures, orders.map(order => order.takerAmount), true)
.batchFillLimitOrders(
orders,
signatures,
orders.map(order => order.takerAmount),
true,
)
.awaitTransactionSuccessAsync({ from: taker, value });
const [orderInfos] = await zeroEx.batchGetLimitOrderRelevantStates(orders, signatures).callAsync();
orderInfos.map((orderInfo, i) =>
@@ -277,7 +297,12 @@ blockchainTests.resets('BatchFillNativeOrdersFeature', env => {
await testUtils.prepareBalancesForOrdersAsync(orders);
const value = testUtils.protocolFee.times(orders.length);
const tx = feature
.batchFillLimitOrders(orders, signatures, orders.map(order => order.takerAmount), true)
.batchFillLimitOrders(
orders,
signatures,
orders.map(order => order.takerAmount),
true,
)
.awaitTransactionSuccessAsync({ from: taker, value });
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.BatchFillIncompleteError(
@@ -299,7 +324,12 @@ blockchainTests.resets('BatchFillNativeOrdersFeature', env => {
await testUtils.prepareBalancesForOrdersAsync(orders);
const value = testUtils.protocolFee.times(orders.length);
const tx = feature
.batchFillLimitOrders(orders, signatures, orders.map(order => order.takerAmount), true)
.batchFillLimitOrders(
orders,
signatures,
orders.map(order => order.takerAmount),
true,
)
.awaitTransactionSuccessAsync({ from: taker, value });
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.BatchFillIncompleteError(
@@ -337,7 +367,12 @@ blockchainTests.resets('BatchFillNativeOrdersFeature', env => {
);
await testUtils.prepareBalancesForOrdersAsync(orders);
const tx = await feature
.batchFillRfqOrders(orders, signatures, orders.map(order => order.takerAmount), false)
.batchFillRfqOrders(
orders,
signatures,
orders.map(order => order.takerAmount),
false,
)
.awaitTransactionSuccessAsync({ from: taker });
const [orderInfos] = await zeroEx.batchGetRfqOrderRelevantStates(orders, signatures).callAsync();
orderInfos.map((orderInfo, i) =>
@@ -388,7 +423,12 @@ blockchainTests.resets('BatchFillNativeOrdersFeature', env => {
);
await testUtils.prepareBalancesForOrdersAsync(orders);
const tx = await feature
.batchFillRfqOrders(orders, signatures, orders.map(order => order.takerAmount), false)
.batchFillRfqOrders(
orders,
signatures,
orders.map(order => order.takerAmount),
false,
)
.awaitTransactionSuccessAsync({ from: taker });
const [orderInfos] = await zeroEx.batchGetRfqOrderRelevantStates(orders, signatures).callAsync();
const [expiredOrderInfo, ...filledOrderInfos] = orderInfos;
@@ -418,7 +458,12 @@ blockchainTests.resets('BatchFillNativeOrdersFeature', env => {
);
await testUtils.prepareBalancesForOrdersAsync(orders);
const tx = await feature
.batchFillRfqOrders(orders, signatures, orders.map(order => order.takerAmount), true)
.batchFillRfqOrders(
orders,
signatures,
orders.map(order => order.takerAmount),
true,
)
.awaitTransactionSuccessAsync({ from: taker });
const [orderInfos] = await zeroEx.batchGetRfqOrderRelevantStates(orders, signatures).callAsync();
orderInfos.map((orderInfo, i) =>
@@ -444,7 +489,12 @@ blockchainTests.resets('BatchFillNativeOrdersFeature', env => {
);
await testUtils.prepareBalancesForOrdersAsync(orders);
const tx = feature
.batchFillRfqOrders(orders, signatures, orders.map(order => order.takerAmount), true)
.batchFillRfqOrders(
orders,
signatures,
orders.map(order => order.takerAmount),
true,
)
.awaitTransactionSuccessAsync({ from: taker });
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.BatchFillIncompleteError(
@@ -465,7 +515,12 @@ blockchainTests.resets('BatchFillNativeOrdersFeature', env => {
);
await testUtils.prepareBalancesForOrdersAsync(orders);
const tx = feature
.batchFillRfqOrders(orders, signatures, orders.map(order => order.takerAmount), true)
.batchFillRfqOrders(
orders,
signatures,
orders.map(order => order.takerAmount),
true,
)
.awaitTransactionSuccessAsync({ from: taker });
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.BatchFillIncompleteError(

View File

@@ -304,7 +304,10 @@ blockchainTests.fork.skip('Multiplex feature', env => {
{
name: 'transformations',
type: 'tuple[]',
components: [{ name: 'deploymentNonce', type: 'uint32' }, { name: 'data', type: 'bytes' }],
components: [
{ name: 'deploymentNonce', type: 'uint32' },
{ name: 'data', type: 'bytes' },
],
},
{ name: 'ethValue', type: 'uint256' },
]);
@@ -439,7 +442,10 @@ blockchainTests.fork.skip('Multiplex feature', env => {
{
name: 'transformations',
type: 'tuple[]',
components: [{ name: 'deploymentNonce', type: 'uint32' }, { name: 'data', type: 'bytes' }],
components: [
{ name: 'deploymentNonce', type: 'uint32' },
{ name: 'data', type: 'bytes' },
],
},
{ name: 'ethValue', type: 'uint256' },
]);
@@ -460,7 +466,10 @@ blockchainTests.fork.skip('Multiplex feature', env => {
{
name: 'calls',
type: 'tuple[]',
components: [{ name: 'selector', type: 'bytes4' }, { name: 'data', type: 'bytes' }],
components: [
{ name: 'selector', type: 'bytes4' },
{ name: 'data', type: 'bytes' },
],
},
{ name: 'ethValue', type: 'uint256' },
]);

View File

@@ -0,0 +1,753 @@
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;
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, false)
.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/INVALID_UNWRAP_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, false)
.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, false)
.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, false)
.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', 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 partially fill an order with ETH', 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 refund excess ETH is msg.value > order.takerAmount', 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('Cannot fill an order if taker token is not WETH', async () => {
const order = getTestOtcOrder();
const tx = testUtils.fillOtcOrderWithEthAsync(order);
return expect(tx).to.revertWith('OtcOrdersFeature/INVALID_WRAP_ETH');
});
});
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.OrderNotSignedByTakerError(order.getHash(), notTaker, taker),
);
});
it('cannot fill order with bad maker signature', async () => {
const order = getTestOtcOrder({ taker, txOrigin });
// Overwrite chainId to result in a different hash and therefore different
// signature.
const tx = testUtils.fillTakerSignedOtcOrderAsync(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({ 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),
false,
)
.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/INVALID_UNWRAP_WETH');
});
it('allows for fills on orders signed by a approved signer (taker)', async () => {
const order = getTestOtcOrder({ txOrigin, taker: contractWallet.address });
await testUtils.prepareBalancesForOrdersAsync([order], contractWallet.address);
// allow signer
await contractWallet
.registerAllowedOrderSigner(contractWalletSigner, true)
.awaitTransactionSuccessAsync({ from: contractWalletOwner });
// fill should succeed
const receipt = await zeroEx
.fillTakerSignedOtcOrder(
order,
await order.getSignatureWithProviderAsync(env.provider),
await order.getSignatureWithProviderAsync(
env.provider,
SignatureType.EthSign,
contractWalletSigner,
),
false,
)
.awaitTransactionSuccessAsync({ from: txOrigin });
verifyEventsFromLogs(
receipt.logs,
[testUtils.createOtcOrderFilledEventArgs(order)],
IZeroExEvents.OtcOrderFilled,
);
await assertExpectedFinalBalancesFromOtcOrderFillAsync(order);
});
it(`doesn't allow fills with an unapproved signer (taker)`, async () => {
const order = getTestOtcOrder({ txOrigin, taker: contractWallet.address });
await testUtils.prepareBalancesForOrdersAsync([order], contractWallet.address);
// fill should succeed
const tx = zeroEx
.fillTakerSignedOtcOrder(
order,
await order.getSignatureWithProviderAsync(env.provider),
await order.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, notTaker),
false,
)
.awaitTransactionSuccessAsync({ from: txOrigin });
return expect(tx).to.revertWith(
new RevertErrors.NativeOrders.OrderNotSignedByTakerError(order.getHash(), notTaker, order.taker),
);
});
});
});

View File

@@ -43,12 +43,14 @@ blockchainTests.resets('TransformERC20 feature', env => {
env.provider,
env.txDefaults,
{
transformERC20: (await TestTransformERC20Contract.deployFrom0xArtifactAsync(
artifacts.TestTransformERC20,
env.provider,
env.txDefaults,
artifacts,
)).address,
transformERC20: (
await TestTransformERC20Contract.deployFrom0xArtifactAsync(
artifacts.TestTransformERC20,
env.provider,
env.txDefaults,
artifacts,
)
).address,
},
{ transformerDeployer },
);

View File

@@ -0,0 +1,258 @@
import {
blockchainTests,
constants,
describe,
expect,
getRandomPortion,
randomAddress,
} from '@0x/contracts-test-utils';
import { BigNumber, hexUtils } from '@0x/utils';
import { LogWithDecodedArgs } from 'ethereum-types';
import { artifacts } from '../artifacts';
import {
TestMintableERC20TokenContract,
TestNoEthRecipientContract,
TestUniswapV3FactoryContract,
TestUniswapV3FactoryPoolCreatedEventArgs,
TestUniswapV3PoolContract,
TestWethContract,
UniswapV3FeatureContract,
} from '../wrappers';
blockchainTests.resets('UniswapV3Feature', env => {
const { MAX_UINT256, NULL_ADDRESS, ZERO_AMOUNT } = constants;
const POOL_FEE = 1234;
const MAX_SUPPLY = new BigNumber('10e18');
let uniFactory: TestUniswapV3FactoryContract;
let feature: UniswapV3FeatureContract;
let weth: TestWethContract;
let tokens: TestMintableERC20TokenContract[];
const sellAmount = getRandomPortion(MAX_SUPPLY);
const buyAmount = getRandomPortion(MAX_SUPPLY);
let taker: string;
const recipient = randomAddress();
let noEthRecipient: TestNoEthRecipientContract;
before(async () => {
[, taker] = await env.getAccountAddressesAsync();
weth = await TestWethContract.deployFrom0xArtifactAsync(
artifacts.TestWeth,
env.provider,
env.txDefaults,
artifacts,
);
tokens = await Promise.all(
[...new Array(3)].map(async () =>
TestMintableERC20TokenContract.deployFrom0xArtifactAsync(
artifacts.TestMintableERC20Token,
env.provider,
env.txDefaults,
artifacts,
),
),
);
noEthRecipient = await TestNoEthRecipientContract.deployFrom0xArtifactAsync(
artifacts.TestNoEthRecipient,
env.provider,
env.txDefaults,
artifacts,
);
uniFactory = await TestUniswapV3FactoryContract.deployFrom0xArtifactAsync(
artifacts.TestUniswapV3Factory,
env.provider,
env.txDefaults,
artifacts,
);
feature = await UniswapV3FeatureContract.deployFrom0xArtifactAsync(
artifacts.TestUniswapV3Feature,
env.provider,
env.txDefaults,
artifacts,
weth.address,
uniFactory.address,
await uniFactory.POOL_INIT_CODE_HASH().callAsync(),
);
await Promise.all(
[...tokens, weth].map(t =>
t.approve(feature.address, MAX_UINT256).awaitTransactionSuccessAsync({ from: taker }),
),
);
});
function isWethContract(t: TestMintableERC20TokenContract | TestWethContract): t is TestWethContract {
return !!(t as any).deposit;
}
async function mintToAsync(
token: TestMintableERC20TokenContract | TestWethContract,
owner: string,
amount: BigNumber,
): Promise<void> {
if (isWethContract(token)) {
await token.depositTo(owner).awaitTransactionSuccessAsync({ value: amount });
} else {
await token.mint(owner, amount).awaitTransactionSuccessAsync();
}
}
async function createPoolAsync(
token0: TestMintableERC20TokenContract | TestWethContract,
token1: TestMintableERC20TokenContract | TestWethContract,
balance0: BigNumber,
balance1: BigNumber,
): Promise<TestUniswapV3PoolContract> {
const r = await uniFactory
.createPool(token0.address, token1.address, new BigNumber(POOL_FEE))
.awaitTransactionSuccessAsync();
const pool = new TestUniswapV3PoolContract(
(r.logs[0] as LogWithDecodedArgs<TestUniswapV3FactoryPoolCreatedEventArgs>).args.pool,
env.provider,
env.txDefaults,
);
await mintToAsync(token0, pool.address, balance0);
await mintToAsync(token1, pool.address, balance1);
return pool;
}
function encodePath(tokens_: Array<TestMintableERC20TokenContract | TestWethContract>): string {
const elems: string[] = [];
tokens_.forEach((t, i) => {
if (i) {
elems.push(hexUtils.leftPad(POOL_FEE, 3));
}
elems.push(hexUtils.leftPad(t.address, 20));
});
return hexUtils.concat(...elems);
}
describe('sellTokenForTokenToUniswapV3()', () => {
it('1-hop swap', async () => {
const [sellToken, buyToken] = tokens;
const pool = await createPoolAsync(sellToken, buyToken, ZERO_AMOUNT, buyAmount);
await mintToAsync(sellToken, taker, sellAmount);
await feature
.sellTokenForTokenToUniswapV3(encodePath([sellToken, buyToken]), sellAmount, buyAmount, recipient)
.awaitTransactionSuccessAsync({ from: taker });
// Test pools always ask for full sell amount and pay entire balance.
expect(await sellToken.balanceOf(taker).callAsync()).to.bignumber.eq(0);
expect(await buyToken.balanceOf(recipient).callAsync()).to.bignumber.eq(buyAmount);
expect(await sellToken.balanceOf(pool.address).callAsync()).to.bignumber.eq(sellAmount);
});
it('2-hop swap', async () => {
const pools = [
await createPoolAsync(tokens[0], tokens[1], ZERO_AMOUNT, buyAmount),
await createPoolAsync(tokens[1], tokens[2], ZERO_AMOUNT, buyAmount),
];
await mintToAsync(tokens[0], taker, sellAmount);
await feature
.sellTokenForTokenToUniswapV3(encodePath(tokens), sellAmount, buyAmount, recipient)
.awaitTransactionSuccessAsync({ from: taker });
// Test pools always ask for full sell amount and pay entire balance.
expect(await tokens[0].balanceOf(taker).callAsync()).to.bignumber.eq(0);
expect(await tokens[2].balanceOf(recipient).callAsync()).to.bignumber.eq(buyAmount);
expect(await tokens[0].balanceOf(pools[0].address).callAsync()).to.bignumber.eq(sellAmount);
expect(await tokens[1].balanceOf(pools[1].address).callAsync()).to.bignumber.eq(buyAmount);
});
it('1-hop underbuy fails', async () => {
const [sellToken, buyToken] = tokens;
await createPoolAsync(sellToken, buyToken, ZERO_AMOUNT, buyAmount.minus(1));
await mintToAsync(sellToken, taker, sellAmount);
const tx = feature
.sellTokenForTokenToUniswapV3(encodePath([sellToken, buyToken]), sellAmount, buyAmount, recipient)
.awaitTransactionSuccessAsync({ from: taker });
return expect(tx).to.revertWith('UniswapV3Feature/UNDERBOUGHT');
});
it('2-hop underbuy fails', async () => {
await createPoolAsync(tokens[0], tokens[1], ZERO_AMOUNT, buyAmount);
await createPoolAsync(tokens[1], tokens[2], ZERO_AMOUNT, buyAmount.minus(1));
await mintToAsync(tokens[0], taker, sellAmount);
const tx = feature
.sellTokenForTokenToUniswapV3(encodePath(tokens), sellAmount, buyAmount, recipient)
.awaitTransactionSuccessAsync({ from: taker });
return expect(tx).to.revertWith('UniswapV3Feature/UNDERBOUGHT');
});
it('null recipient is sender', async () => {
const [sellToken, buyToken] = tokens;
await createPoolAsync(sellToken, buyToken, ZERO_AMOUNT, buyAmount);
await mintToAsync(sellToken, taker, sellAmount);
await feature
.sellTokenForTokenToUniswapV3(encodePath([sellToken, buyToken]), sellAmount, buyAmount, NULL_ADDRESS)
.awaitTransactionSuccessAsync({ from: taker });
// Test pools always ask for full sell amount and pay entire balance.
expect(await buyToken.balanceOf(taker).callAsync()).to.bignumber.eq(buyAmount);
});
});
describe('sellEthForTokenToUniswapV3()', () => {
it('1-hop swap', async () => {
const [buyToken] = tokens;
const pool = await createPoolAsync(weth, buyToken, ZERO_AMOUNT, buyAmount);
await feature
.sellEthForTokenToUniswapV3(encodePath([weth, buyToken]), buyAmount, recipient)
.awaitTransactionSuccessAsync({ from: taker, value: sellAmount });
// Test pools always ask for full sell amount and pay entire balance.
expect(await buyToken.balanceOf(recipient).callAsync()).to.bignumber.eq(buyAmount);
expect(await weth.balanceOf(pool.address).callAsync()).to.bignumber.eq(sellAmount);
});
it('null recipient is sender', async () => {
const [buyToken] = tokens;
const pool = await createPoolAsync(weth, buyToken, ZERO_AMOUNT, buyAmount);
await feature
.sellEthForTokenToUniswapV3(encodePath([weth, buyToken]), buyAmount, NULL_ADDRESS)
.awaitTransactionSuccessAsync({ from: taker, value: sellAmount });
// Test pools always ask for full sell amount and pay entire balance.
expect(await buyToken.balanceOf(taker).callAsync()).to.bignumber.eq(buyAmount);
expect(await weth.balanceOf(pool.address).callAsync()).to.bignumber.eq(sellAmount);
});
});
describe('sellTokenForEthToUniswapV3()', () => {
it('1-hop swap', async () => {
const [sellToken] = tokens;
const pool = await createPoolAsync(sellToken, weth, ZERO_AMOUNT, buyAmount);
await mintToAsync(sellToken, taker, sellAmount);
await feature
.sellTokenForEthToUniswapV3(encodePath([sellToken, weth]), sellAmount, buyAmount, recipient)
.awaitTransactionSuccessAsync({ from: taker });
// Test pools always ask for full sell amount and pay entire balance.
expect(await sellToken.balanceOf(taker).callAsync()).to.bignumber.eq(0);
expect(await env.web3Wrapper.getBalanceInWeiAsync(recipient)).to.bignumber.eq(buyAmount);
expect(await sellToken.balanceOf(pool.address).callAsync()).to.bignumber.eq(sellAmount);
});
it('null recipient is sender', async () => {
const [sellToken] = tokens;
const pool = await createPoolAsync(sellToken, weth, ZERO_AMOUNT, buyAmount);
await mintToAsync(sellToken, taker, sellAmount);
const takerBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker);
await feature
.sellTokenForEthToUniswapV3(encodePath([sellToken, weth]), sellAmount, buyAmount, NULL_ADDRESS)
.awaitTransactionSuccessAsync({ from: taker, gasPrice: ZERO_AMOUNT });
// Test pools always ask for full sell amount and pay entire balance.
expect((await env.web3Wrapper.getBalanceInWeiAsync(taker)).minus(takerBalanceBefore)).to.bignumber.eq(
buyAmount,
);
expect(await sellToken.balanceOf(pool.address).callAsync()).to.bignumber.eq(sellAmount);
});
it('fails if receipient cannot receive ETH', async () => {
const [sellToken] = tokens;
await mintToAsync(sellToken, taker, sellAmount);
const tx = feature
.sellTokenForEthToUniswapV3(
encodePath([sellToken, weth]),
sellAmount,
buyAmount,
noEthRecipient.address,
)
.awaitTransactionSuccessAsync({ from: taker });
return expect(tx).to.be.rejectedWith('revert');
});
});
});