Lawrence Forman 901d400d62
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>
2021-06-02 14:21:14 +10:00

444 lines
15 KiB
TypeScript

import {
BlockchainTestsEnvironment,
constants,
expect,
getRandomInteger,
randomAddress,
} from '@0x/contracts-test-utils';
import {
LimitOrder,
LimitOrderFields,
OrderBase,
OrderInfo,
OtcOrder,
RfqOrder,
RfqOrderFields,
SignatureType,
} from '@0x/protocol-utils';
import { BigNumber, hexUtils } from '@0x/utils';
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import {
IZeroExContract,
IZeroExLimitOrderFilledEventArgs,
IZeroExOtcOrderFilledEventArgs,
IZeroExRfqOrderFilledEventArgs,
} from '../../src/wrappers';
import { artifacts } from '../artifacts';
import { fullMigrateAsync } from '../utils/migration';
import { TestMintableERC20TokenContract } from '../wrappers';
const { ZERO_AMOUNT: ZERO, NULL_ADDRESS } = constants;
interface RfqOrderFilledAmounts {
makerTokenFilledAmount: BigNumber;
takerTokenFilledAmount: BigNumber;
}
interface OtcOrderFilledAmounts extends RfqOrderFilledAmounts {}
interface LimitOrderFilledAmounts {
makerTokenFilledAmount: BigNumber;
takerTokenFilledAmount: BigNumber;
takerTokenFeeFilledAmount: BigNumber;
}
export enum OtcOrderWethOptions {
LeaveAsWeth,
WrapEth,
UnwrapWeth,
}
export class NativeOrdersTestEnvironment {
public static async createAsync(
env: BlockchainTestsEnvironment,
gasPrice: BigNumber = new BigNumber('123e9'),
protocolFeeMultiplier: number = 70e3,
): Promise<NativeOrdersTestEnvironment> {
const [owner, maker, taker] = await env.getAccountAddressesAsync();
const [makerToken, takerToken] = await Promise.all(
[...new Array(2)].map(async () =>
TestMintableERC20TokenContract.deployFrom0xArtifactAsync(
artifacts.TestMintableERC20Token,
env.provider,
{ ...env.txDefaults, gasPrice },
artifacts,
),
),
);
const zeroEx = await fullMigrateAsync(owner, env.provider, env.txDefaults, {}, { protocolFeeMultiplier });
await makerToken.approve(zeroEx.address, constants.MAX_UINT256).awaitTransactionSuccessAsync({ from: maker });
await takerToken.approve(zeroEx.address, constants.MAX_UINT256).awaitTransactionSuccessAsync({ from: taker });
return new NativeOrdersTestEnvironment(
maker,
taker,
makerToken,
takerToken,
zeroEx,
gasPrice,
gasPrice.times(protocolFeeMultiplier),
env,
);
}
constructor(
public readonly maker: string,
public readonly taker: string,
public readonly makerToken: TestMintableERC20TokenContract,
public readonly takerToken: TestMintableERC20TokenContract,
public readonly zeroEx: IZeroExContract,
public readonly gasPrice: BigNumber,
public readonly protocolFee: BigNumber,
private readonly _env: BlockchainTestsEnvironment,
) {}
public async prepareBalancesForOrdersAsync(
orders: LimitOrder[] | RfqOrder[] | OtcOrder[],
taker: string = this.taker,
): Promise<void> {
await this.makerToken
.mint(this.maker, BigNumber.sum(...(orders as OrderBase[]).map(order => order.makerAmount)))
.awaitTransactionSuccessAsync();
await this.takerToken
.mint(
taker,
BigNumber.sum(
...(orders as OrderBase[]).map(order =>
order.takerAmount.plus(order instanceof LimitOrder ? order.takerTokenFeeAmount : 0),
),
),
)
.awaitTransactionSuccessAsync();
}
public async fillLimitOrderAsync(
order: LimitOrder,
opts: Partial<{
fillAmount: BigNumber | number;
taker: string;
protocolFee: BigNumber | number;
}> = {},
): Promise<TransactionReceiptWithDecodedLogs> {
const { fillAmount, taker, protocolFee } = {
taker: this.taker,
fillAmount: order.takerAmount,
...opts,
};
await this.prepareBalancesForOrdersAsync([order], taker);
const value = protocolFee === undefined ? this.protocolFee : protocolFee;
return this.zeroEx
.fillLimitOrder(
order,
await order.getSignatureWithProviderAsync(this._env.provider),
new BigNumber(fillAmount),
)
.awaitTransactionSuccessAsync({ from: taker, value });
}
public async fillRfqOrderAsync(
order: RfqOrder,
fillAmount: BigNumber | number = order.takerAmount,
taker: string = this.taker,
): Promise<TransactionReceiptWithDecodedLogs> {
await this.prepareBalancesForOrdersAsync([order], taker);
return this.zeroEx
.fillRfqOrder(
order,
await order.getSignatureWithProviderAsync(this._env.provider),
new BigNumber(fillAmount),
)
.awaitTransactionSuccessAsync({ from: taker });
}
public async fillOtcOrderAsync(
order: OtcOrder,
fillAmount: BigNumber | number = order.takerAmount,
taker: string = this.taker,
unwrapWeth: boolean = false,
): Promise<TransactionReceiptWithDecodedLogs> {
await this.prepareBalancesForOrdersAsync([order], taker);
return this.zeroEx
.fillOtcOrder(
order,
await order.getSignatureWithProviderAsync(this._env.provider),
new BigNumber(fillAmount),
unwrapWeth,
)
.awaitTransactionSuccessAsync({ from: taker });
}
public async fillTakerSignedOtcOrderAsync(
order: OtcOrder,
origin: string = order.txOrigin,
taker: string = order.taker,
unwrapWeth: boolean = false,
): Promise<TransactionReceiptWithDecodedLogs> {
await this.prepareBalancesForOrdersAsync([order], taker);
return this.zeroEx
.fillTakerSignedOtcOrder(
order,
await order.getSignatureWithProviderAsync(this._env.provider),
await order.getSignatureWithProviderAsync(this._env.provider, SignatureType.EthSign, taker),
unwrapWeth,
)
.awaitTransactionSuccessAsync({ from: origin });
}
public async fillOtcOrderWithEthAsync(
order: OtcOrder,
fillAmount: BigNumber | number = order.takerAmount,
taker: string = this.taker,
): Promise<TransactionReceiptWithDecodedLogs> {
await this.prepareBalancesForOrdersAsync([order], taker);
return this.zeroEx
.fillOtcOrderWithEth(order, await order.getSignatureWithProviderAsync(this._env.provider))
.awaitTransactionSuccessAsync({ from: taker, value: fillAmount });
}
public createLimitOrderFilledEventArgs(
order: LimitOrder,
takerTokenFillAmount: BigNumber = order.takerAmount,
takerTokenAlreadyFilledAmount: BigNumber = ZERO,
): IZeroExLimitOrderFilledEventArgs {
const {
makerTokenFilledAmount,
takerTokenFilledAmount,
takerTokenFeeFilledAmount,
} = computeLimitOrderFilledAmounts(order, takerTokenFillAmount, takerTokenAlreadyFilledAmount);
const protocolFee = order.taker !== NULL_ADDRESS ? ZERO : this.protocolFee;
return {
takerTokenFilledAmount,
makerTokenFilledAmount,
takerTokenFeeFilledAmount,
orderHash: order.getHash(),
maker: order.maker,
taker: this.taker,
feeRecipient: order.feeRecipient,
makerToken: order.makerToken,
takerToken: order.takerToken,
protocolFeePaid: protocolFee,
pool: order.pool,
};
}
public createRfqOrderFilledEventArgs(
order: RfqOrder,
takerTokenFillAmount: BigNumber = order.takerAmount,
takerTokenAlreadyFilledAmount: BigNumber = ZERO,
): IZeroExRfqOrderFilledEventArgs {
const { makerTokenFilledAmount, takerTokenFilledAmount } = computeRfqOrderFilledAmounts(
order,
takerTokenFillAmount,
takerTokenAlreadyFilledAmount,
);
return {
takerTokenFilledAmount,
makerTokenFilledAmount,
orderHash: order.getHash(),
maker: order.maker,
taker: this.taker,
makerToken: order.makerToken,
takerToken: order.takerToken,
pool: order.pool,
};
}
public createOtcOrderFilledEventArgs(
order: OtcOrder,
takerTokenFillAmount: BigNumber = order.takerAmount,
): IZeroExOtcOrderFilledEventArgs {
const { makerTokenFilledAmount, takerTokenFilledAmount } = computeOtcOrderFilledAmounts(
order,
takerTokenFillAmount,
);
return {
takerTokenFilledAmount,
makerTokenFilledAmount,
orderHash: order.getHash(),
maker: order.maker,
taker: order.taker !== NULL_ADDRESS ? order.taker : this.taker,
makerToken: order.makerToken,
takerToken: order.takerToken,
};
}
}
/**
* Generate a random limit order.
*/
export function getRandomLimitOrder(fields: Partial<LimitOrderFields> = {}): LimitOrder {
return new LimitOrder({
makerToken: randomAddress(),
takerToken: randomAddress(),
makerAmount: getRandomInteger('1e18', '100e18'),
takerAmount: getRandomInteger('1e6', '100e6'),
takerTokenFeeAmount: getRandomInteger('0.01e18', '1e18'),
maker: randomAddress(),
taker: randomAddress(),
sender: randomAddress(),
feeRecipient: randomAddress(),
pool: hexUtils.random(),
expiry: new BigNumber(Math.floor(Date.now() / 1000 + 60)),
salt: new BigNumber(hexUtils.random()),
...fields,
});
}
/**
* Generate a random RFQ order.
*/
export function getRandomRfqOrder(fields: Partial<RfqOrderFields> = {}): RfqOrder {
return new RfqOrder({
makerToken: randomAddress(),
takerToken: randomAddress(),
makerAmount: getRandomInteger('1e18', '100e18'),
takerAmount: getRandomInteger('1e6', '100e6'),
maker: randomAddress(),
txOrigin: randomAddress(),
pool: hexUtils.random(),
expiry: new BigNumber(Math.floor(Date.now() / 1000 + 60)),
salt: new BigNumber(hexUtils.random()),
...fields,
});
}
/**
* Generate a random OTC Order
*/
export function getRandomOtcOrder(fields: Partial<OtcOrder> = {}): OtcOrder {
return new OtcOrder({
makerToken: randomAddress(),
takerToken: randomAddress(),
makerAmount: getRandomInteger('1e18', '100e18'),
takerAmount: getRandomInteger('1e6', '100e6'),
maker: randomAddress(),
taker: randomAddress(),
txOrigin: randomAddress(),
expiryAndNonce: OtcOrder.encodeExpiryAndNonce(
fields.expiry ?? new BigNumber(Math.floor(Date.now() / 1000 + 60)), // expiry
fields.nonceBucket ?? getRandomInteger(0, OtcOrder.MAX_NONCE_BUCKET), // nonceBucket
fields.nonce ?? getRandomInteger(0, OtcOrder.MAX_NONCE_VALUE), // nonce
),
...fields,
});
}
/**
* Asserts the fields of an OrderInfo object.
*/
export function assertOrderInfoEquals(actual: OrderInfo, expected: OrderInfo): void {
expect(actual.status, 'Order status').to.eq(expected.status);
expect(actual.orderHash, 'Order hash').to.eq(expected.orderHash);
expect(actual.takerTokenFilledAmount, 'Order takerTokenFilledAmount').to.bignumber.eq(
expected.takerTokenFilledAmount,
);
}
/**
* Creates an order expiry field.
*/
export function createExpiry(deltaSeconds: number = 60): BigNumber {
return new BigNumber(Math.floor(Date.now() / 1000) + deltaSeconds);
}
/**
* Computes the maker, taker, and taker token fee amounts filled for
* the given limit order.
*/
export function computeLimitOrderFilledAmounts(
order: LimitOrder,
takerTokenFillAmount: BigNumber = order.takerAmount,
takerTokenAlreadyFilledAmount: BigNumber = ZERO,
): LimitOrderFilledAmounts {
const fillAmount = BigNumber.min(
order.takerAmount,
takerTokenFillAmount,
order.takerAmount.minus(takerTokenAlreadyFilledAmount),
);
const makerTokenFilledAmount = fillAmount
.times(order.makerAmount)
.div(order.takerAmount)
.integerValue(BigNumber.ROUND_DOWN);
const takerTokenFeeFilledAmount = fillAmount
.times(order.takerTokenFeeAmount)
.div(order.takerAmount)
.integerValue(BigNumber.ROUND_DOWN);
return {
makerTokenFilledAmount,
takerTokenFilledAmount: fillAmount,
takerTokenFeeFilledAmount,
};
}
/**
* Computes the maker and taker amounts filled for the given RFQ order.
*/
export function computeRfqOrderFilledAmounts(
order: RfqOrder,
takerTokenFillAmount: BigNumber = order.takerAmount,
takerTokenAlreadyFilledAmount: BigNumber = ZERO,
): RfqOrderFilledAmounts {
const fillAmount = BigNumber.min(
order.takerAmount,
takerTokenFillAmount,
order.takerAmount.minus(takerTokenAlreadyFilledAmount),
);
const makerTokenFilledAmount = fillAmount
.times(order.makerAmount)
.div(order.takerAmount)
.integerValue(BigNumber.ROUND_DOWN);
return {
makerTokenFilledAmount,
takerTokenFilledAmount: fillAmount,
};
}
/**
* Computes the maker and taker amounts filled for the given OTC order.
*/
export function computeOtcOrderFilledAmounts(
order: OtcOrder,
takerTokenFillAmount: BigNumber = order.takerAmount,
): OtcOrderFilledAmounts {
const fillAmount = BigNumber.min(order.takerAmount, takerTokenFillAmount, order.takerAmount);
const makerTokenFilledAmount = fillAmount
.times(order.makerAmount)
.div(order.takerAmount)
.integerValue(BigNumber.ROUND_DOWN);
return {
makerTokenFilledAmount,
takerTokenFilledAmount: fillAmount,
};
}
/**
* Computes the remaining fillable amount in maker token for
* the given order.
*/
export function getFillableMakerTokenAmount(
order: LimitOrder | RfqOrder,
takerTokenFilledAmount: BigNumber = ZERO,
): BigNumber {
return order.takerAmount
.minus(takerTokenFilledAmount)
.times(order.makerAmount)
.div(order.takerAmount)
.integerValue(BigNumber.ROUND_DOWN);
}
/**
* Computes the remaining fillable amnount in taker token, based on
* the amount already filled and the maker's balance/allowance.
*/
export function getActualFillableTakerTokenAmount(
order: LimitOrder | RfqOrder,
makerBalance: BigNumber = order.makerAmount,
makerAllowance: BigNumber = order.makerAmount,
takerTokenFilledAmount: BigNumber = ZERO,
): BigNumber {
const fillableMakerTokenAmount = getFillableMakerTokenAmount(order, takerTokenFilledAmount);
return BigNumber.min(fillableMakerTokenAmount, makerBalance, makerAllowance)
.times(order.takerAmount)
.div(order.makerAmount)
.integerValue(BigNumber.ROUND_UP);
}