427 lines
20 KiB
TypeScript
427 lines
20 KiB
TypeScript
import { ContractAddresses } from '@0x/contract-addresses';
|
|
import { artifacts as erc20Artifacts, DummyERC20TokenContract } from '@0x/contracts-erc20';
|
|
import { IExchangeContract } from '@0x/contracts-exchange';
|
|
import { blockchainTests, constants, expect, getRandomPortion, verifyEventsFromLogs } from '@0x/contracts-test-utils';
|
|
import {
|
|
artifacts as exchangeProxyArtifacts,
|
|
IZeroExContract,
|
|
LogMetadataTransformerContract,
|
|
signCallData,
|
|
} from '@0x/contracts-zero-ex';
|
|
import { migrateOnceAsync } from '@0x/migrations';
|
|
import {
|
|
assetDataUtils,
|
|
encodeFillQuoteTransformerData,
|
|
encodePayTakerTransformerData,
|
|
ETH_TOKEN_ADDRESS,
|
|
FillQuoteTransformerSide,
|
|
findTransformerNonce,
|
|
signatureUtils,
|
|
SignedExchangeProxyMetaTransaction,
|
|
} from '@0x/order-utils';
|
|
import { AssetProxyId, Order, SignedOrder } from '@0x/types';
|
|
import { BigNumber, hexUtils, ZeroExRevertErrors } from '@0x/utils';
|
|
import * as ethjs from 'ethereumjs-util';
|
|
|
|
const { MAX_UINT256, NULL_ADDRESS, NULL_BYTES, NULL_BYTES32, ZERO_AMOUNT } = constants;
|
|
|
|
blockchainTests.resets('exchange proxy - meta-transactions', env => {
|
|
const quoteSignerKey = hexUtils.random();
|
|
const quoteSigner = hexUtils.toHex(ethjs.privateToAddress(ethjs.toBuffer(quoteSignerKey)));
|
|
let owner: string;
|
|
let relayer: string;
|
|
let maker: string;
|
|
let taker: string;
|
|
let flashWalletAddress: string;
|
|
let zeroEx: IZeroExContract;
|
|
let exchange: IExchangeContract;
|
|
let inputToken: DummyERC20TokenContract;
|
|
let outputToken: DummyERC20TokenContract;
|
|
let feeToken: DummyERC20TokenContract;
|
|
let addresses: ContractAddresses;
|
|
let protocolFee: BigNumber;
|
|
let metadataTransformer: LogMetadataTransformerContract;
|
|
const GAS_PRICE = new BigNumber('1e9');
|
|
const MAKER_BALANCE = new BigNumber('100e18');
|
|
const TAKER_BALANCE = new BigNumber('100e18');
|
|
const TAKER_FEE_BALANCE = new BigNumber('100e18');
|
|
|
|
before(async () => {
|
|
[, relayer, maker, taker] = await env.getAccountAddressesAsync();
|
|
addresses = await migrateOnceAsync(env.provider);
|
|
zeroEx = new IZeroExContract(addresses.exchangeProxy, env.provider, env.txDefaults, {
|
|
LogMetadataTransformer: LogMetadataTransformerContract.ABI(),
|
|
DummyERC20Token: DummyERC20TokenContract.ABI(),
|
|
});
|
|
exchange = new IExchangeContract(addresses.exchange, env.provider, env.txDefaults);
|
|
[inputToken, outputToken, feeToken] = await Promise.all(
|
|
[...new Array(3)].map(i =>
|
|
DummyERC20TokenContract.deployFrom0xArtifactAsync(
|
|
erc20Artifacts.DummyERC20Token,
|
|
env.provider,
|
|
env.txDefaults,
|
|
{},
|
|
`DummyToken-${i}`,
|
|
`TOK${i}`,
|
|
new BigNumber(18),
|
|
BigNumber.max(MAKER_BALANCE, TAKER_BALANCE),
|
|
),
|
|
),
|
|
);
|
|
// LogMetadataTransformer is not deployed in migrations.
|
|
metadataTransformer = await LogMetadataTransformerContract.deployFrom0xArtifactAsync(
|
|
exchangeProxyArtifacts.LogMetadataTransformer,
|
|
env.provider,
|
|
{
|
|
...env.txDefaults,
|
|
from: addresses.exchangeProxyTransformerDeployer,
|
|
},
|
|
{},
|
|
);
|
|
owner = await zeroEx.owner().callAsync();
|
|
protocolFee = await exchange.protocolFeeMultiplier().callAsync();
|
|
flashWalletAddress = await zeroEx.getTransformWallet().callAsync();
|
|
const erc20Proxy = await exchange.getAssetProxy(AssetProxyId.ERC20).callAsync();
|
|
const allowanceTarget = await zeroEx.getAllowanceTarget().callAsync();
|
|
await outputToken.mint(MAKER_BALANCE).awaitTransactionSuccessAsync({ from: maker });
|
|
await inputToken.mint(TAKER_BALANCE).awaitTransactionSuccessAsync({ from: taker });
|
|
await feeToken.mint(TAKER_FEE_BALANCE).awaitTransactionSuccessAsync({ from: taker });
|
|
await outputToken.approve(erc20Proxy, MAX_UINT256).awaitTransactionSuccessAsync({ from: maker });
|
|
await inputToken.approve(allowanceTarget, MAX_UINT256).awaitTransactionSuccessAsync({ from: taker });
|
|
await feeToken.approve(allowanceTarget, MAX_UINT256).awaitTransactionSuccessAsync({ from: taker });
|
|
await zeroEx.setQuoteSigner(quoteSigner).awaitTransactionSuccessAsync({ from: owner });
|
|
});
|
|
|
|
interface Transformation {
|
|
deploymentNonce: number;
|
|
data: string;
|
|
}
|
|
|
|
interface SwapInfo {
|
|
inputTokenAddress: string;
|
|
outputTokenAddress: string;
|
|
inputTokenAmount: BigNumber;
|
|
minOutputTokenAmount: BigNumber;
|
|
transformations: Transformation[];
|
|
orders: SignedOrder[];
|
|
}
|
|
|
|
async function generateSwapAsync(orderFields: Partial<Order> = {}, isRfqt: boolean = false): Promise<SwapInfo> {
|
|
const order = await signatureUtils.ecSignTypedDataOrderAsync(
|
|
env.provider,
|
|
{
|
|
chainId: 1337,
|
|
exchangeAddress: exchange.address,
|
|
expirationTimeSeconds: new BigNumber(Date.now()),
|
|
salt: new BigNumber(hexUtils.random()),
|
|
feeRecipientAddress: NULL_ADDRESS,
|
|
senderAddress: NULL_ADDRESS,
|
|
takerAddress: isRfqt ? flashWalletAddress : NULL_ADDRESS,
|
|
makerAddress: maker,
|
|
makerAssetData: assetDataUtils.encodeERC20AssetData(outputToken.address),
|
|
takerAssetData: assetDataUtils.encodeERC20AssetData(inputToken.address),
|
|
makerFeeAssetData: NULL_BYTES,
|
|
takerFeeAssetData: NULL_BYTES,
|
|
takerAssetAmount: getRandomPortion(TAKER_BALANCE),
|
|
makerAssetAmount: getRandomPortion(MAKER_BALANCE),
|
|
makerFee: ZERO_AMOUNT,
|
|
takerFee: ZERO_AMOUNT,
|
|
...orderFields,
|
|
},
|
|
maker,
|
|
);
|
|
const transformations = [
|
|
{
|
|
deploymentNonce: findTransformerNonce(
|
|
addresses.transformers.fillQuoteTransformer,
|
|
addresses.exchangeProxyTransformerDeployer,
|
|
),
|
|
data: encodeFillQuoteTransformerData({
|
|
orders: [order],
|
|
signatures: [order.signature],
|
|
buyToken: outputToken.address,
|
|
sellToken: inputToken.address,
|
|
fillAmount: order.takerAssetAmount,
|
|
maxOrderFillAmounts: [],
|
|
refundReceiver: hexUtils.leftPad(2, 20), // Send refund to sender.
|
|
rfqtTakerAddress: isRfqt ? taker : NULL_ADDRESS,
|
|
side: FillQuoteTransformerSide.Sell,
|
|
}),
|
|
},
|
|
{
|
|
deploymentNonce: findTransformerNonce(
|
|
addresses.transformers.payTakerTransformer,
|
|
addresses.exchangeProxyTransformerDeployer,
|
|
),
|
|
data: encodePayTakerTransformerData({
|
|
tokens: [inputToken.address, outputToken.address, ETH_TOKEN_ADDRESS],
|
|
amounts: [MAX_UINT256, MAX_UINT256, MAX_UINT256],
|
|
}),
|
|
},
|
|
{
|
|
deploymentNonce: findTransformerNonce(
|
|
metadataTransformer.address,
|
|
addresses.exchangeProxyTransformerDeployer,
|
|
),
|
|
data: NULL_BYTES,
|
|
},
|
|
];
|
|
return {
|
|
transformations,
|
|
orders: [order],
|
|
inputTokenAddress: inputToken.address,
|
|
outputTokenAddress: outputToken.address,
|
|
inputTokenAmount: order.takerAssetAmount,
|
|
minOutputTokenAmount: order.makerAssetAmount,
|
|
};
|
|
}
|
|
|
|
function getSwapData(swap: SwapInfo): string {
|
|
return zeroEx
|
|
.transformERC20(
|
|
swap.inputTokenAddress,
|
|
swap.outputTokenAddress,
|
|
swap.inputTokenAmount,
|
|
swap.minOutputTokenAmount,
|
|
swap.transformations,
|
|
)
|
|
.getABIEncodedTransactionData();
|
|
}
|
|
|
|
function getSignedSwapData(swap: SwapInfo, signerKey?: string): string {
|
|
return signCallData(
|
|
zeroEx
|
|
.transformERC20(
|
|
swap.inputTokenAddress,
|
|
swap.outputTokenAddress,
|
|
swap.inputTokenAmount,
|
|
swap.minOutputTokenAmount,
|
|
swap.transformations,
|
|
)
|
|
.getABIEncodedTransactionData(),
|
|
signerKey ? signerKey : quoteSignerKey,
|
|
);
|
|
}
|
|
|
|
async function createMetaTransactionAsync(
|
|
data: string,
|
|
value: BigNumber,
|
|
fee?: BigNumber | number,
|
|
): Promise<SignedExchangeProxyMetaTransaction> {
|
|
return signatureUtils.ecSignTypedDataExchangeProxyMetaTransactionAsync(
|
|
env.provider,
|
|
{
|
|
value,
|
|
signer: taker,
|
|
sender: relayer,
|
|
minGasPrice: GAS_PRICE,
|
|
maxGasPrice: GAS_PRICE,
|
|
expirationTimeSeconds: new BigNumber(Math.floor(Date.now() / 1000) + 60),
|
|
salt: new BigNumber(hexUtils.random()),
|
|
callData: data,
|
|
feeToken: feeToken.address,
|
|
feeAmount: fee !== undefined ? new BigNumber(fee) : getRandomPortion(TAKER_FEE_BALANCE),
|
|
domain: {
|
|
chainId: 1,
|
|
name: 'ZeroEx',
|
|
version: '1.0.0',
|
|
verifyingContract: zeroEx.address,
|
|
},
|
|
},
|
|
taker,
|
|
);
|
|
}
|
|
|
|
it('can call `transformERC20()` with signed calldata and no relayer fee', async () => {
|
|
const swap = await generateSwapAsync();
|
|
const callDataHash = hexUtils.hash(getSwapData(swap));
|
|
const signedSwapData = getSignedSwapData(swap);
|
|
const _protocolFee = protocolFee.times(GAS_PRICE).times(swap.orders.length + 1); // Pay a little more fee than needed.
|
|
const mtx = await createMetaTransactionAsync(signedSwapData, _protocolFee, 0);
|
|
const relayerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(relayer);
|
|
const receipt = await zeroEx
|
|
.executeMetaTransaction(mtx, mtx.signature)
|
|
.awaitTransactionSuccessAsync({ from: relayer, value: mtx.value, gasPrice: GAS_PRICE });
|
|
const relayerEthRefund = relayerEthBalanceBefore
|
|
.minus(await env.web3Wrapper.getBalanceInWeiAsync(relayer))
|
|
.minus(GAS_PRICE.times(receipt.gasUsed));
|
|
// Ensure the relayer got back the unused protocol fees.
|
|
expect(relayerEthRefund).to.bignumber.eq(protocolFee.times(GAS_PRICE));
|
|
// Ensure the relayer got paid no mtx fees.
|
|
expect(await feeToken.balanceOf(relayer).callAsync()).to.bignumber.eq(0);
|
|
// Ensure the taker got output tokens.
|
|
expect(await outputToken.balanceOf(taker).callAsync()).to.bignumber.eq(swap.minOutputTokenAmount);
|
|
// Ensure the maker got input tokens.
|
|
expect(await inputToken.balanceOf(maker).callAsync()).to.bignumber.eq(swap.inputTokenAmount);
|
|
// Check events.
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[
|
|
{
|
|
taker,
|
|
callDataHash,
|
|
sender: zeroEx.address,
|
|
data: NULL_BYTES,
|
|
},
|
|
],
|
|
'TransformerMetadata',
|
|
);
|
|
});
|
|
|
|
it('can call `transformERC20()` with signed calldata and a relayer fee', async () => {
|
|
const swap = await generateSwapAsync();
|
|
const callDataHash = hexUtils.hash(getSwapData(swap));
|
|
const signedSwapData = getSignedSwapData(swap);
|
|
const _protocolFee = protocolFee.times(GAS_PRICE).times(swap.orders.length + 1); // Pay a little more fee than needed.
|
|
const mtx = await createMetaTransactionAsync(signedSwapData, _protocolFee);
|
|
const relayerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(relayer);
|
|
const receipt = await zeroEx
|
|
.executeMetaTransaction(mtx, mtx.signature)
|
|
.awaitTransactionSuccessAsync({ from: relayer, value: mtx.value, gasPrice: GAS_PRICE });
|
|
const relayerEthRefund = relayerEthBalanceBefore
|
|
.minus(await env.web3Wrapper.getBalanceInWeiAsync(relayer))
|
|
.minus(GAS_PRICE.times(receipt.gasUsed));
|
|
// Ensure the relayer got back the unused protocol fees.
|
|
expect(relayerEthRefund).to.bignumber.eq(protocolFee.times(GAS_PRICE));
|
|
// Ensure the relayer got paid mtx fees.
|
|
expect(await feeToken.balanceOf(relayer).callAsync()).to.bignumber.eq(mtx.feeAmount);
|
|
// Ensure the taker got output tokens.
|
|
expect(await outputToken.balanceOf(taker).callAsync()).to.bignumber.eq(swap.minOutputTokenAmount);
|
|
// Ensure the maker got input tokens.
|
|
expect(await inputToken.balanceOf(maker).callAsync()).to.bignumber.eq(swap.inputTokenAmount);
|
|
// Check events.
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[
|
|
{
|
|
taker,
|
|
callDataHash,
|
|
sender: zeroEx.address,
|
|
data: NULL_BYTES,
|
|
},
|
|
],
|
|
'TransformerMetadata',
|
|
);
|
|
});
|
|
|
|
it('can call `transformERC20()` with wrongly signed calldata and a relayer fee', async () => {
|
|
const swap = await generateSwapAsync();
|
|
const signedSwapData = getSignedSwapData(swap, hexUtils.random());
|
|
const _protocolFee = protocolFee.times(GAS_PRICE).times(swap.orders.length + 1); // Pay a little more fee than needed.
|
|
const mtx = await createMetaTransactionAsync(signedSwapData, _protocolFee);
|
|
const relayerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(relayer);
|
|
const receipt = await zeroEx
|
|
.executeMetaTransaction(mtx, mtx.signature)
|
|
.awaitTransactionSuccessAsync({ from: relayer, value: mtx.value, gasPrice: GAS_PRICE });
|
|
const relayerEthRefund = relayerEthBalanceBefore
|
|
.minus(await env.web3Wrapper.getBalanceInWeiAsync(relayer))
|
|
.minus(GAS_PRICE.times(receipt.gasUsed));
|
|
// Ensure the relayer got back the unused protocol fees.
|
|
expect(relayerEthRefund).to.bignumber.eq(protocolFee.times(GAS_PRICE));
|
|
// Ensure the relayer got paid mtx fees.
|
|
expect(await feeToken.balanceOf(relayer).callAsync()).to.bignumber.eq(mtx.feeAmount);
|
|
// Ensure the taker got output tokens.
|
|
expect(await outputToken.balanceOf(taker).callAsync()).to.bignumber.eq(swap.minOutputTokenAmount);
|
|
// Ensure the maker got input tokens.
|
|
expect(await inputToken.balanceOf(maker).callAsync()).to.bignumber.eq(swap.inputTokenAmount);
|
|
// Check events.
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[
|
|
{
|
|
taker,
|
|
// Only signed calldata should have a nonzero hash.
|
|
callDataHash: NULL_BYTES32,
|
|
sender: zeroEx.address,
|
|
data: NULL_BYTES,
|
|
},
|
|
],
|
|
'TransformerMetadata',
|
|
);
|
|
});
|
|
|
|
it('`transformERC20()` can fill RFQT order if calldata is signed', async () => {
|
|
const swap = await generateSwapAsync({}, true);
|
|
const callDataHash = hexUtils.hash(getSwapData(swap));
|
|
const signedSwapData = getSignedSwapData(swap);
|
|
const _protocolFee = protocolFee.times(GAS_PRICE).times(swap.orders.length + 1); // Pay a little more fee than needed.
|
|
const mtx = await createMetaTransactionAsync(signedSwapData, _protocolFee, 0);
|
|
const relayerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(relayer);
|
|
const receipt = await zeroEx
|
|
.executeMetaTransaction(mtx, mtx.signature)
|
|
.awaitTransactionSuccessAsync({ from: relayer, value: mtx.value, gasPrice: GAS_PRICE });
|
|
const relayerEthRefund = relayerEthBalanceBefore
|
|
.minus(await env.web3Wrapper.getBalanceInWeiAsync(relayer))
|
|
.minus(GAS_PRICE.times(receipt.gasUsed));
|
|
// Ensure the relayer got back the unused protocol fees.
|
|
expect(relayerEthRefund).to.bignumber.eq(protocolFee.times(GAS_PRICE));
|
|
// Ensure the relayer got paid no mtx fees.
|
|
expect(await feeToken.balanceOf(relayer).callAsync()).to.bignumber.eq(0);
|
|
// Ensure the taker got output tokens.
|
|
expect(await outputToken.balanceOf(taker).callAsync()).to.bignumber.eq(swap.minOutputTokenAmount);
|
|
// Ensure the maker got input tokens.
|
|
expect(await inputToken.balanceOf(maker).callAsync()).to.bignumber.eq(swap.inputTokenAmount);
|
|
// Check events.
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[
|
|
{
|
|
taker,
|
|
callDataHash,
|
|
sender: zeroEx.address,
|
|
data: NULL_BYTES,
|
|
},
|
|
],
|
|
'TransformerMetadata',
|
|
);
|
|
});
|
|
|
|
it('`transformERC20()` can fill RFQT order if calldata is not signed but no quote signer configured', async () => {
|
|
const swap = await generateSwapAsync({}, true);
|
|
const callData = getSwapData(swap);
|
|
const callDataHash = hexUtils.hash(callData);
|
|
const _protocolFee = protocolFee.times(GAS_PRICE).times(swap.orders.length + 1); // Pay a little more fee than needed.
|
|
const mtx = await createMetaTransactionAsync(callData, _protocolFee, 0);
|
|
const relayerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(relayer);
|
|
await zeroEx.setQuoteSigner(NULL_ADDRESS).awaitTransactionSuccessAsync({ from: owner });
|
|
const receipt = await zeroEx
|
|
.executeMetaTransaction(mtx, mtx.signature)
|
|
.awaitTransactionSuccessAsync({ from: relayer, value: mtx.value, gasPrice: GAS_PRICE });
|
|
const relayerEthRefund = relayerEthBalanceBefore
|
|
.minus(await env.web3Wrapper.getBalanceInWeiAsync(relayer))
|
|
.minus(GAS_PRICE.times(receipt.gasUsed));
|
|
// Ensure the relayer got back the unused protocol fees.
|
|
expect(relayerEthRefund).to.bignumber.eq(protocolFee.times(GAS_PRICE));
|
|
// Ensure the relayer got paid no mtx fees.
|
|
expect(await feeToken.balanceOf(relayer).callAsync()).to.bignumber.eq(0);
|
|
// Ensure the taker got output tokens.
|
|
expect(await outputToken.balanceOf(taker).callAsync()).to.bignumber.eq(swap.minOutputTokenAmount);
|
|
// Ensure the maker got input tokens.
|
|
expect(await inputToken.balanceOf(maker).callAsync()).to.bignumber.eq(swap.inputTokenAmount);
|
|
// Check events.
|
|
verifyEventsFromLogs(
|
|
receipt.logs,
|
|
[
|
|
{
|
|
taker,
|
|
callDataHash,
|
|
sender: zeroEx.address,
|
|
data: NULL_BYTES,
|
|
},
|
|
],
|
|
'TransformerMetadata',
|
|
);
|
|
});
|
|
|
|
it('`transformERC20()` cannot fill RFQT order if calldata is not signed', async () => {
|
|
const swap = await generateSwapAsync({}, true);
|
|
const callData = getSwapData(swap);
|
|
const _protocolFee = protocolFee.times(GAS_PRICE).times(swap.orders.length + 1); // Pay a little more fee than needed.
|
|
const mtx = await createMetaTransactionAsync(callData, _protocolFee, 0);
|
|
const tx = zeroEx
|
|
.executeMetaTransaction(mtx, mtx.signature)
|
|
.awaitTransactionSuccessAsync({ from: relayer, value: mtx.value, gasPrice: GAS_PRICE });
|
|
return expect(tx).to.revertWith(new ZeroExRevertErrors.MetaTransactions.MetaTransactionCallFailedError());
|
|
});
|
|
});
|