345 lines
15 KiB
TypeScript
345 lines
15 KiB
TypeScript
import { artifacts as erc20Artifacts, DummyERC20TokenContract } from '@0x/contracts-erc20';
|
|
import {
|
|
assertIntegerRoughlyEquals,
|
|
blockchainTests,
|
|
constants,
|
|
expect,
|
|
getRandomInteger,
|
|
randomAddress,
|
|
} from '@0x/contracts-test-utils';
|
|
import { Order } from '@0x/types';
|
|
import { BigNumber, hexUtils } from '@0x/utils';
|
|
import * as _ from 'lodash';
|
|
|
|
import { artifacts } from '../artifacts';
|
|
import { TestNativeOrderSamplerContract } from '../wrappers';
|
|
|
|
const { NULL_BYTES, ZERO_AMOUNT } = constants;
|
|
|
|
// tslint:disable: custom-no-magic-numbers
|
|
|
|
blockchainTests.resets('NativeOrderSampler contract', env => {
|
|
let testContract: TestNativeOrderSamplerContract;
|
|
let makerToken: string;
|
|
let takerToken: string;
|
|
let feeToken: string;
|
|
let erc20Proxy: string;
|
|
const ERC20_PROXY_ID = '0xf47261b0';
|
|
const VALID_SIGNATURE = '0x01';
|
|
const INVALID_SIGNATURE = '0x00';
|
|
|
|
before(async () => {
|
|
testContract = await TestNativeOrderSamplerContract.deployFrom0xArtifactAsync(
|
|
artifacts.TestNativeOrderSampler,
|
|
env.provider,
|
|
env.txDefaults,
|
|
{},
|
|
);
|
|
erc20Proxy = await testContract.getAssetProxy(ERC20_PROXY_ID).callAsync();
|
|
const NUM_TOKENS = new BigNumber(3);
|
|
[makerToken, takerToken, feeToken] = await testContract.createTokens(NUM_TOKENS).callAsync();
|
|
await testContract.createTokens(NUM_TOKENS).awaitTransactionSuccessAsync();
|
|
});
|
|
|
|
function getPackedHash(...args: string[]): string {
|
|
return hexUtils.hash(hexUtils.concat(...args.map(a => hexUtils.toHex(a))));
|
|
}
|
|
|
|
interface OrderInfo {
|
|
orderHash: string;
|
|
orderStatus: number;
|
|
orderTakerAssetFilledAmount: BigNumber;
|
|
}
|
|
|
|
function getOrderInfo(order: Order): OrderInfo {
|
|
const hash = getPackedHash(hexUtils.leftPad(order.salt));
|
|
const orderStatus = order.salt.mod(255).eq(0) ? 3 : 5;
|
|
const filledAmount = order.expirationTimeSeconds;
|
|
return {
|
|
orderStatus,
|
|
orderHash: hash,
|
|
orderTakerAssetFilledAmount: filledAmount,
|
|
};
|
|
}
|
|
|
|
function createFillableOrderSalt(): BigNumber {
|
|
return new BigNumber(hexUtils.concat(hexUtils.slice(hexUtils.random(), 0, -1), '0x01'));
|
|
}
|
|
|
|
function createUnfillableOrderSalt(): BigNumber {
|
|
return new BigNumber(hexUtils.concat(hexUtils.slice(hexUtils.random(), 0, -1), '0xff'));
|
|
}
|
|
|
|
function getOrderFillableTakerAmount(order: Order): BigNumber {
|
|
return order.takerAssetAmount.minus(getOrderInfo(order).orderTakerAssetFilledAmount);
|
|
}
|
|
|
|
function getERC20AssetData(tokenAddress: string): string {
|
|
return hexUtils.concat(ERC20_PROXY_ID, hexUtils.leftPad(tokenAddress));
|
|
}
|
|
|
|
function createOrder(fields: Partial<Order> = {}, filledTakerAssetAmount: BigNumber = ZERO_AMOUNT): Order {
|
|
return {
|
|
chainId: 1337,
|
|
exchangeAddress: randomAddress(),
|
|
makerAddress: randomAddress(),
|
|
takerAddress: randomAddress(),
|
|
senderAddress: randomAddress(),
|
|
feeRecipientAddress: randomAddress(),
|
|
makerAssetAmount: getRandomInteger(1e18, 10e18),
|
|
takerAssetAmount: getRandomInteger(1e18, 10e18),
|
|
makerFee: getRandomInteger(1e18, 10e18),
|
|
takerFee: getRandomInteger(1e18, 10e18),
|
|
makerAssetData: getERC20AssetData(makerToken),
|
|
takerAssetData: getERC20AssetData(takerToken),
|
|
makerFeeAssetData: getERC20AssetData(feeToken),
|
|
takerFeeAssetData: getERC20AssetData(randomAddress()),
|
|
salt: createFillableOrderSalt(),
|
|
// Expiration time will be used to determine filled amount.
|
|
expirationTimeSeconds: filledTakerAssetAmount,
|
|
...fields,
|
|
};
|
|
}
|
|
|
|
async function fundMakerAsync(
|
|
order: Order,
|
|
assetData: string,
|
|
balanceScaling: number = 1,
|
|
allowanceScaling: number = 1,
|
|
): Promise<void> {
|
|
let token;
|
|
let amount;
|
|
if (assetData === order.makerAssetData) {
|
|
token = makerToken;
|
|
amount =
|
|
order.makerAssetData === order.makerFeeAssetData
|
|
? order.makerAssetAmount.plus(order.makerFee)
|
|
: order.makerAssetAmount;
|
|
} else {
|
|
token = feeToken;
|
|
amount = order.makerFee;
|
|
}
|
|
amount = amount.times(getOrderFillableTakerAmount(order).div(BigNumber.max(1, order.takerAssetAmount)));
|
|
await testContract
|
|
.setTokenBalanceAndAllowance(
|
|
token,
|
|
order.makerAddress,
|
|
erc20Proxy,
|
|
amount.times(balanceScaling).integerValue(),
|
|
amount.times(allowanceScaling).integerValue(),
|
|
)
|
|
.awaitTransactionSuccessAsync();
|
|
}
|
|
|
|
describe('getTokenDecimals()', () => {
|
|
it('correctly returns the token balances', async () => {
|
|
const newMakerToken = await DummyERC20TokenContract.deployFrom0xArtifactAsync(
|
|
erc20Artifacts.DummyERC20Token,
|
|
env.provider,
|
|
env.txDefaults,
|
|
artifacts,
|
|
constants.DUMMY_TOKEN_NAME,
|
|
constants.DUMMY_TOKEN_SYMBOL,
|
|
new BigNumber(18),
|
|
constants.DUMMY_TOKEN_TOTAL_SUPPLY,
|
|
);
|
|
const newTakerToken = await DummyERC20TokenContract.deployFrom0xArtifactAsync(
|
|
erc20Artifacts.DummyERC20Token,
|
|
env.provider,
|
|
env.txDefaults,
|
|
artifacts,
|
|
constants.DUMMY_TOKEN_NAME,
|
|
constants.DUMMY_TOKEN_SYMBOL,
|
|
new BigNumber(6),
|
|
constants.DUMMY_TOKEN_TOTAL_SUPPLY,
|
|
);
|
|
const [makerDecimals, takerDecimals] = await testContract
|
|
.getTokenDecimals(newMakerToken.address, newTakerToken.address)
|
|
.callAsync();
|
|
expect(makerDecimals.toString()).to.eql('18');
|
|
expect(takerDecimals.toString()).to.eql('6');
|
|
});
|
|
});
|
|
|
|
describe('getOrderFillableTakerAmount()', () => {
|
|
it('returns the full amount for a fully funded order', async () => {
|
|
const order = createOrder();
|
|
const expected = getOrderFillableTakerAmount(order);
|
|
await fundMakerAsync(order, order.makerAssetData);
|
|
await fundMakerAsync(order, order.makerFeeAssetData);
|
|
const actual = await testContract
|
|
.getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
|
|
.callAsync();
|
|
expect(actual).to.bignumber.eq(expected);
|
|
});
|
|
|
|
it('returns the full amount for a fully funded order without maker fees', async () => {
|
|
const order = createOrder({ makerFee: ZERO_AMOUNT });
|
|
const expected = getOrderFillableTakerAmount(order);
|
|
await fundMakerAsync(order, order.makerAssetData);
|
|
await fundMakerAsync(order, order.makerFeeAssetData);
|
|
const actual = await testContract
|
|
.getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
|
|
.callAsync();
|
|
expect(actual).to.bignumber.eq(expected);
|
|
});
|
|
|
|
it('returns the full amount for a fully funded order without maker fee asset data', async () => {
|
|
const order = createOrder({ makerFeeAssetData: NULL_BYTES });
|
|
const expected = getOrderFillableTakerAmount(order);
|
|
await fundMakerAsync(order, order.makerAssetData);
|
|
await fundMakerAsync(order, order.makerFeeAssetData);
|
|
const actual = await testContract
|
|
.getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
|
|
.callAsync();
|
|
expect(actual).to.bignumber.eq(expected);
|
|
});
|
|
|
|
it('returns the full amount for a fully funded order with maker fees denominated in the maker asset', async () => {
|
|
const order = createOrder({ makerFeeAssetData: getERC20AssetData(makerToken) });
|
|
const expected = getOrderFillableTakerAmount(order);
|
|
await fundMakerAsync(order, order.makerAssetData);
|
|
await fundMakerAsync(order, order.makerFeeAssetData);
|
|
const actual = await testContract
|
|
.getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
|
|
.callAsync();
|
|
expect(actual).to.bignumber.eq(expected);
|
|
});
|
|
|
|
it('returns partial amount with insufficient maker asset balance', async () => {
|
|
const order = createOrder();
|
|
const expected = getOrderFillableTakerAmount(order)
|
|
.times(0.5)
|
|
.integerValue(BigNumber.ROUND_DOWN);
|
|
await fundMakerAsync(order, order.makerAssetData, 0.5);
|
|
await fundMakerAsync(order, order.makerFeeAssetData);
|
|
const actual = await testContract
|
|
.getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
|
|
.callAsync();
|
|
assertIntegerRoughlyEquals(actual, expected, 100);
|
|
});
|
|
|
|
it('returns partial amount with insufficient maker asset allowance', async () => {
|
|
const order = createOrder();
|
|
const expected = getOrderFillableTakerAmount(order)
|
|
.times(0.5)
|
|
.integerValue(BigNumber.ROUND_DOWN);
|
|
await fundMakerAsync(order, order.makerAssetData, 1, 0.5);
|
|
await fundMakerAsync(order, order.makerFeeAssetData);
|
|
const actual = await testContract
|
|
.getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
|
|
.callAsync();
|
|
assertIntegerRoughlyEquals(actual, expected, 100);
|
|
});
|
|
|
|
it('returns partial amount with insufficient maker fee asset balance', async () => {
|
|
const order = createOrder();
|
|
const expected = getOrderFillableTakerAmount(order)
|
|
.times(0.5)
|
|
.integerValue(BigNumber.ROUND_DOWN);
|
|
await fundMakerAsync(order, order.makerAssetData);
|
|
await fundMakerAsync(order, order.makerFeeAssetData, 0.5);
|
|
const actual = await testContract
|
|
.getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
|
|
.callAsync();
|
|
assertIntegerRoughlyEquals(actual, expected, 100);
|
|
});
|
|
|
|
it('returns partial amount with insufficient maker fee asset allowance', async () => {
|
|
const order = createOrder();
|
|
const expected = getOrderFillableTakerAmount(order)
|
|
.times(0.5)
|
|
.integerValue(BigNumber.ROUND_DOWN);
|
|
await fundMakerAsync(order, order.makerAssetData);
|
|
await fundMakerAsync(order, order.makerFeeAssetData, 1, 0.5);
|
|
const actual = await testContract
|
|
.getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
|
|
.callAsync();
|
|
assertIntegerRoughlyEquals(actual, expected, 100);
|
|
});
|
|
|
|
it('returns partial amount with insufficient maker asset balance (maker asset fees)', async () => {
|
|
const order = createOrder({ makerFeeAssetData: getERC20AssetData(makerToken) });
|
|
const expected = getOrderFillableTakerAmount(order)
|
|
.times(0.5)
|
|
.integerValue(BigNumber.ROUND_DOWN);
|
|
await fundMakerAsync(order, order.makerAssetData, 0.5);
|
|
const actual = await testContract
|
|
.getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
|
|
.callAsync();
|
|
assertIntegerRoughlyEquals(actual, expected, 100);
|
|
});
|
|
|
|
it('returns partial amount with insufficient maker asset allowance (maker asset fees)', async () => {
|
|
const order = createOrder({ makerFeeAssetData: getERC20AssetData(makerToken) });
|
|
const expected = getOrderFillableTakerAmount(order)
|
|
.times(0.5)
|
|
.integerValue(BigNumber.ROUND_DOWN);
|
|
await fundMakerAsync(order, order.makerAssetData, 1, 0.5);
|
|
const actual = await testContract
|
|
.getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
|
|
.callAsync();
|
|
assertIntegerRoughlyEquals(actual, expected, 100);
|
|
});
|
|
|
|
it('returns zero for an that is not fillable', async () => {
|
|
const order = {
|
|
...createOrder(),
|
|
salt: createUnfillableOrderSalt(),
|
|
};
|
|
await fundMakerAsync(order, order.makerAssetData);
|
|
await fundMakerAsync(order, order.makerFeeAssetData);
|
|
const fillableTakerAmount = await testContract
|
|
.getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
|
|
.callAsync();
|
|
expect(fillableTakerAmount).to.bignumber.eq(ZERO_AMOUNT);
|
|
});
|
|
|
|
it('returns zero for an order with zero maker asset amount', async () => {
|
|
const order = {
|
|
...createOrder(),
|
|
makerAssetAmount: ZERO_AMOUNT,
|
|
};
|
|
await fundMakerAsync(order, order.makerAssetData);
|
|
await fundMakerAsync(order, order.makerFeeAssetData);
|
|
const fillableTakerAmount = await testContract
|
|
.getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
|
|
.callAsync();
|
|
expect(fillableTakerAmount).to.bignumber.eq(ZERO_AMOUNT);
|
|
});
|
|
|
|
it('returns zero for an order with zero taker asset amount', async () => {
|
|
const order = {
|
|
...createOrder(),
|
|
takerAssetAmount: ZERO_AMOUNT,
|
|
};
|
|
await fundMakerAsync(order, order.makerAssetData);
|
|
await fundMakerAsync(order, order.makerFeeAssetData);
|
|
const fillableTakerAmount = await testContract
|
|
.getOrderFillableTakerAmount(order, VALID_SIGNATURE, testContract.address)
|
|
.callAsync();
|
|
expect(fillableTakerAmount).to.bignumber.eq(ZERO_AMOUNT);
|
|
});
|
|
|
|
it('returns zero for an order with an empty signature', async () => {
|
|
const order = createOrder();
|
|
await fundMakerAsync(order, order.makerAssetData);
|
|
await fundMakerAsync(order, order.makerFeeAssetData);
|
|
const fillableTakerAmount = await testContract
|
|
.getOrderFillableTakerAmount(order, NULL_BYTES, testContract.address)
|
|
.callAsync();
|
|
expect(fillableTakerAmount).to.bignumber.eq(ZERO_AMOUNT);
|
|
});
|
|
|
|
it('returns zero for an order with an invalid signature', async () => {
|
|
const order = createOrder();
|
|
await fundMakerAsync(order, order.makerAssetData);
|
|
await fundMakerAsync(order, order.makerFeeAssetData);
|
|
const fillableTakerAmount = await testContract
|
|
.getOrderFillableTakerAmount(order, INVALID_SIGNATURE, testContract.address)
|
|
.callAsync();
|
|
expect(fillableTakerAmount).to.bignumber.eq(ZERO_AMOUNT);
|
|
});
|
|
});
|
|
});
|