import { ECSignature, SignedOrder, ZeroEx } from '0x.js';
import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils';
import { ExchangeContractErrs } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
import { Web3Wrapper } from '@0xproject/web3-wrapper';
import * as chai from 'chai';
import ethUtil = require('ethereumjs-util');
import 'make-promises-safe';
import * as Web3 from 'web3';
import { AccountLevelsContract } from '../../src/contract_wrappers/generated/account_levels';
import { ArbitrageContract } from '../../src/contract_wrappers/generated/arbitrage';
import { DummyTokenContract } from '../../src/contract_wrappers/generated/dummy_token';
import { EtherDeltaContract } from '../../src/contract_wrappers/generated/ether_delta';
import { ExchangeContract } from '../../src/contract_wrappers/generated/exchange';
import { TokenTransferProxyContract } from '../../src/contract_wrappers/generated/token_transfer_proxy';
import { artifacts } from '../../util/artifacts';
import { Balances } from '../../util/balances';
import { constants } from '../../util/constants';
import { crypto } from '../../util/crypto';
import { ExchangeWrapper } from '../../util/exchange_wrapper';
import { OrderFactory } from '../../util/order_factory';
import { BalancesByOwner, ContractName } from '../../util/types';
import { chaiSetup } from '../utils/chai_setup';
import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper';
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
describe('Arbitrage', () => {
let coinbase: string;
let maker: string;
let edMaker: string;
let edFrontRunner: string;
let amountGet: BigNumber;
let amountGive: BigNumber;
let makerTokenAmount: BigNumber;
let takerTokenAmount: BigNumber;
const feeRecipient = ZeroEx.NULL_ADDRESS;
const INITIAL_BALANCE = ZeroEx.toBaseUnitAmount(new BigNumber(10000), 18);
const INITIAL_ALLOWANCE = ZeroEx.toBaseUnitAmount(new BigNumber(10000), 18);
let weth: DummyTokenContract;
let zrx: DummyTokenContract;
let arbitrage: ArbitrageContract;
let etherDelta: EtherDeltaContract;
let signedOrder: SignedOrder;
let exWrapper: ExchangeWrapper;
let orderFactory: OrderFactory;
let zeroEx: ZeroEx;
// From a bird's eye view - we create two orders.
// 0x order of 1 ZRX (maker) for 1 WETH (taker)
// ED order of 2 WETH (tokenGive) for 1 ZRX (tokenGet)
// And then we do an atomic arbitrage between them which gives us 1 WETH.
before(async () => {
const accounts = await web3Wrapper.getAvailableAddressesAsync();
[coinbase, maker, edMaker, edFrontRunner] = accounts;
weth = await DummyTokenContract.deployFrom0xArtifactAsync(
artifacts.DummyToken,
provider,
txDefaults,
constants.DUMMY_TOKEN_NAME,
constants.DUMMY_TOKEN_SYMBOL,
constants.DUMMY_TOKEN_DECIMALS,
constants.DUMMY_TOKEN_TOTAL_SUPPLY,
);
zrx = await DummyTokenContract.deployFrom0xArtifactAsync(
artifacts.DummyToken,
provider,
txDefaults,
constants.DUMMY_TOKEN_NAME,
constants.DUMMY_TOKEN_SYMBOL,
constants.DUMMY_TOKEN_DECIMALS,
constants.DUMMY_TOKEN_TOTAL_SUPPLY,
);
const accountLevels = await AccountLevelsContract.deployFrom0xArtifactAsync(
artifacts.AccountLevels,
provider,
txDefaults,
);
const edAdminAddress = accounts[0];
const edMakerFee = new BigNumber(0);
const edTakerFee = new BigNumber(0);
const edFeeRebate = new BigNumber(0);
etherDelta = await EtherDeltaContract.deployFrom0xArtifactAsync(
artifacts.EtherDelta,
provider,
txDefaults,
edAdminAddress,
feeRecipient,
accountLevels.address,
edMakerFee,
edTakerFee,
edFeeRebate,
);
const tokenTransferProxy = await TokenTransferProxyContract.deployFrom0xArtifactAsync(
artifacts.TokenTransferProxy,
provider,
txDefaults,
);
const exchange = await ExchangeContract.deployFrom0xArtifactAsync(
artifacts.Exchange,
provider,
txDefaults,
zrx.address,
tokenTransferProxy.address,
);
await tokenTransferProxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { from: accounts[0] });
zeroEx = new ZeroEx(provider, {
exchangeContractAddress: exchange.address,
networkId: constants.TESTRPC_NETWORK_ID,
});
exWrapper = new ExchangeWrapper(exchange, zeroEx);
makerTokenAmount = ZeroEx.toBaseUnitAmount(new BigNumber(1), 18);
takerTokenAmount = makerTokenAmount;
const defaultOrderParams = {
exchangeContractAddress: exchange.address,
maker,
feeRecipient,
makerTokenAddress: zrx.address,
takerTokenAddress: weth.address,
makerTokenAmount,
takerTokenAmount,
makerFee: new BigNumber(0),
takerFee: new BigNumber(0),
};
orderFactory = new OrderFactory(zeroEx, defaultOrderParams);
arbitrage = await ArbitrageContract.deployFrom0xArtifactAsync(
artifacts.Arbitrage,
provider,
txDefaults,
exchange.address,
etherDelta.address,
tokenTransferProxy.address,
);
// Enable arbitrage and withdrawals of tokens
await arbitrage.setAllowances.sendTransactionAsync(weth.address, { from: coinbase });
await arbitrage.setAllowances.sendTransactionAsync(zrx.address, { from: coinbase });
// Give some tokens to arbitrage contract
await weth.setBalance.sendTransactionAsync(arbitrage.address, takerTokenAmount, { from: coinbase });
// Fund the maker on exchange side
await zrx.setBalance.sendTransactionAsync(maker, makerTokenAmount, { from: coinbase });
// Set the allowance for the maker on Exchange side
await zrx.approve.sendTransactionAsync(tokenTransferProxy.address, INITIAL_ALLOWANCE, { from: maker });
amountGive = ZeroEx.toBaseUnitAmount(new BigNumber(2), 18);
// Fund the maker on EtherDelta side
await weth.setBalance.sendTransactionAsync(edMaker, amountGive, { from: coinbase });
// Set the allowance for the maker on EtherDelta side
await weth.approve.sendTransactionAsync(etherDelta.address, INITIAL_ALLOWANCE, { from: edMaker });
// Deposit maker funds into EtherDelta
await etherDelta.depositToken.sendTransactionAsync(weth.address, amountGive, { from: edMaker });
amountGet = makerTokenAmount;
// Fund the front runner on EtherDelta side
await zrx.setBalance.sendTransactionAsync(edFrontRunner, amountGet, { from: coinbase });
// Set the allowance for the front-runner on EtherDelta side
await zrx.approve.sendTransactionAsync(etherDelta.address, INITIAL_ALLOWANCE, { from: edFrontRunner });
// Deposit front runner funds into EtherDelta
await etherDelta.depositToken.sendTransactionAsync(zrx.address, amountGet, { from: edFrontRunner });
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
describe('makeAtomicTrade', () => {
let addresses: string[];
let values: BigNumber[];
let v: number[];
let r: string[];
let s: string[];
let tokenGet: string;
let tokenGive: string;
let expires: BigNumber;
let nonce: BigNumber;
let edSignature: ECSignature;
before(async () => {
signedOrder = await orderFactory.newSignedOrderAsync();
tokenGet = zrx.address;
tokenGive = weth.address;
const blockNumber = await web3Wrapper.getBlockNumberAsync();
const ED_ORDER_EXPIRATION_IN_BLOCKS = 10;
expires = new BigNumber(blockNumber + ED_ORDER_EXPIRATION_IN_BLOCKS);
nonce = new BigNumber(42);
const edOrderHash = `0x${crypto
.solSHA256([etherDelta.address, tokenGet, amountGet, tokenGive, amountGive, expires, nonce])
.toString('hex')}`;
const shouldAddPersonalMessagePrefix = false;
edSignature = await zeroEx.signOrderHashAsync(edOrderHash, edMaker, shouldAddPersonalMessagePrefix);
addresses = [
signedOrder.maker,
signedOrder.taker,
signedOrder.makerTokenAddress,
signedOrder.takerTokenAddress,
signedOrder.feeRecipient,
edMaker,
];
const fillTakerTokenAmount = takerTokenAmount;
const edFillAmount = makerTokenAmount;
values = [
signedOrder.makerTokenAmount,
signedOrder.takerTokenAmount,
signedOrder.makerFee,
signedOrder.takerFee,
signedOrder.expirationUnixTimestampSec,
signedOrder.salt,
fillTakerTokenAmount,
amountGet,
amountGive,
expires,
nonce,
edFillAmount,
];
v = [signedOrder.ecSignature.v, edSignature.v];
r = [signedOrder.ecSignature.r, edSignature.r];
s = [signedOrder.ecSignature.s, edSignature.s];
});
it('should successfully execute the arbitrage if not front-runned', async () => {
const txHash = await arbitrage.makeAtomicTrade.sendTransactionAsync(addresses, values, v, r, s, {
from: coinbase,
});
const res = await zeroEx.awaitTransactionMinedAsync(txHash);
const postBalance = await weth.balanceOf.callAsync(arbitrage.address);
expect(postBalance).to.be.bignumber.equal(amountGive);
});
it('should fail and revert if front-runned', async () => {
const preBalance = await weth.balanceOf.callAsync(arbitrage.address);
// Front-running transaction
await etherDelta.trade.sendTransactionAsync(
tokenGet,
amountGet,
tokenGive,
amountGive,
expires,
nonce,
edMaker,
edSignature.v,
edSignature.r,
edSignature.s,
amountGet,
{ from: edFrontRunner },
);
// tslint:disable-next-line:await-promise
await expect(
arbitrage.makeAtomicTrade.sendTransactionAsync(addresses, values, v, r, s, { from: coinbase }),
).to.be.rejectedWith(constants.REVERT);
const postBalance = await weth.balanceOf.callAsync(arbitrage.address);
expect(preBalance).to.be.bignumber.equal(postBalance);
});
});
});