* 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>
444 lines
15 KiB
TypeScript
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);
|
|
}
|