From 2d0940589eb9659cea5179d5b1665ce717ecdf2e Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Fri, 2 Feb 2018 18:30:17 +0100 Subject: Initial implementation of Arbitrage contract with tests --- .../contracts/tutorials/AccountLevels.sol | 11 ++ .../contracts/contracts/tutorials/Arbitrage.sol | 103 ++++++++++ .../contracts/contracts/tutorials/EtherDelta.sol | 168 +++++++++++++++++ packages/contracts/globals.d.ts | 1 + packages/contracts/test/tutorials/arbitrage.ts | 207 +++++++++++++++++++++ packages/contracts/util/crypto.ts | 8 +- packages/contracts/util/types.ts | 3 + 7 files changed, 500 insertions(+), 1 deletion(-) create mode 100644 packages/contracts/contracts/tutorials/AccountLevels.sol create mode 100644 packages/contracts/contracts/tutorials/Arbitrage.sol create mode 100644 packages/contracts/contracts/tutorials/EtherDelta.sol create mode 100644 packages/contracts/test/tutorials/arbitrage.ts (limited to 'packages/contracts') diff --git a/packages/contracts/contracts/tutorials/AccountLevels.sol b/packages/contracts/contracts/tutorials/AccountLevels.sol new file mode 100644 index 000000000..2214ed6b3 --- /dev/null +++ b/packages/contracts/contracts/tutorials/AccountLevels.sol @@ -0,0 +1,11 @@ +pragma solidity ^0.4.19; + +contract AccountLevels { + //given a user, returns an account level + //0 = regular user (pays take fee and make fee) + //1 = market maker silver (pays take fee, no make fee, gets rebate) + //2 = market maker gold (pays take fee, no make fee, gets entire counterparty's take fee as rebate) + function accountLevel(address user) constant returns(uint) { + return 0; + } +} diff --git a/packages/contracts/contracts/tutorials/Arbitrage.sol b/packages/contracts/contracts/tutorials/Arbitrage.sol new file mode 100644 index 000000000..726696966 --- /dev/null +++ b/packages/contracts/contracts/tutorials/Arbitrage.sol @@ -0,0 +1,103 @@ +pragma solidity ^0.4.19; +pragma experimental ABIEncoderV2; + +import "../Exchange.sol"; +import "../tokens/Token.sol"; +import "./EtherDelta.sol"; +import "../tokens/WETH9.sol"; + +contract Arbitrage is Ownable { + + Exchange exchange; + EtherDelta etherDelta; + address proxyAddress; + + uint constant MAX_UINT = 2**256 - 1; + + function Arbitrage(address _exchangeAddress, address _etherDeltaAddress, address _proxyAddress) { + exchange = Exchange(_exchangeAddress); + etherDelta = EtherDelta(_etherDeltaAddress); + proxyAddress = _proxyAddress; + } + + function setAllowances(address tokenAddress) public onlyOwner { + Token token = Token(tokenAddress); + token.approve(address(etherDelta), MAX_UINT); + token.approve(proxyAddress, MAX_UINT); + token.approve(owner, MAX_UINT); + } + + /* + * I 愛 the limitations on Solidity stack size! + * + * addresses + * 0..4 orderAddresses + * 5 tokenGet + * 6 tokenGive + * 7 user + * + * values + * 0..5 orderValues + * 6 fillTakerTokenAmount + * 7 amountGet + * 8 amountGive + * 9 expires + * 10 nonce + * 11 amount + + * signature + * exchange then etherDelta + */ + function makeAtomicTrade( + address[8] addresses, uint[12] values, + uint8[2] v, bytes32[2] r, bytes32[2] s + ) public onlyOwner { + makeExchangeTrade(addresses, values, v, r, s); + makeEtherDeltaTrade(addresses, values, v, r, s); + } + + function makeEtherDeltaTrade( + address[8] addresses, uint[12] values, + uint8[2] v, bytes32[2] r, bytes32[2] s + ) internal { + uint amount = values[11]; + etherDelta.depositToken(addresses[5], values[7]); + etherDelta.trade( + addresses[5], + values[7], + addresses[6], + values[8], + values[9], + values[10], + addresses[7], + v[1], + r[1], + s[1], + amount + ); + etherDelta.withdrawToken(addresses[6], values[8]); + } + + function makeExchangeTrade( + address[8] addresses, uint[12] values, + uint8[2] v, bytes32[2] r, bytes32[2] s + ) internal { + address[5] memory orderAddresses = [ + addresses[0], + addresses[1], + addresses[2], + addresses[3], + addresses[4] + ]; + uint[6] memory orderValues = [ + values[0], + values[1], + values[2], + values[3], + values[4], + values[5] + ]; + uint fillTakerTokenAmount = values[6]; + exchange.fillOrKillOrder(orderAddresses, orderValues, fillTakerTokenAmount, v[0], r[0], s[0]); + } +} diff --git a/packages/contracts/contracts/tutorials/EtherDelta.sol b/packages/contracts/contracts/tutorials/EtherDelta.sol new file mode 100644 index 000000000..74b4c08e8 --- /dev/null +++ b/packages/contracts/contracts/tutorials/EtherDelta.sol @@ -0,0 +1,168 @@ +pragma solidity ^0.4.19; + +import "../utils/SafeMath.sol"; +import "./AccountLevels.sol"; +import "../tokens/Token.sol"; + +contract EtherDelta is SafeMath { + address public admin; //the admin address + address public feeAccount; //the account that will receive fees + address public accountLevelsAddr; //the address of the AccountLevels contract + uint public feeMake; //percentage times (1 ether) + uint public feeTake; //percentage times (1 ether) + uint public feeRebate; //percentage times (1 ether) + mapping (address => mapping (address => uint)) public tokens; //mapping of token addresses to mapping of account balances (token=0 means Ether) + mapping (address => mapping (bytes32 => bool)) public orders; //mapping of user accounts to mapping of order hashes to booleans (true = submitted by user, equivalent to offchain signature) + mapping (address => mapping (bytes32 => uint)) public orderFills; //mapping of user accounts to mapping of order hashes to uints (amount of order that has been filled) + + event Order(address tokenGet, uint amountGet, address tokenGive, uint amountGive, uint expires, uint nonce, address user); + event Cancel(address tokenGet, uint amountGet, address tokenGive, uint amountGive, uint expires, uint nonce, address user, uint8 v, bytes32 r, bytes32 s); + event Trade(address tokenGet, uint amountGet, address tokenGive, uint amountGive, address get, address give); + event Deposit(address token, address user, uint amount, uint balance); + event Withdraw(address token, address user, uint amount, uint balance); + + function EtherDelta(address admin_, address feeAccount_, address accountLevelsAddr_, uint feeMake_, uint feeTake_, uint feeRebate_) { + admin = admin_; + feeAccount = feeAccount_; + accountLevelsAddr = accountLevelsAddr_; + feeMake = feeMake_; + feeTake = feeTake_; + feeRebate = feeRebate_; + } + + function() { + throw; + } + + function changeAdmin(address admin_) { + if (msg.sender != admin) throw; + admin = admin_; + } + + function changeAccountLevelsAddr(address accountLevelsAddr_) { + if (msg.sender != admin) throw; + accountLevelsAddr = accountLevelsAddr_; + } + + function changeFeeAccount(address feeAccount_) { + if (msg.sender != admin) throw; + feeAccount = feeAccount_; + } + + function changeFeeMake(uint feeMake_) { + if (msg.sender != admin) throw; + if (feeMake_ > feeMake) throw; + feeMake = feeMake_; + } + + function changeFeeTake(uint feeTake_) { + if (msg.sender != admin) throw; + if (feeTake_ > feeTake || feeTake_ < feeRebate) throw; + feeTake = feeTake_; + } + + function changeFeeRebate(uint feeRebate_) { + if (msg.sender != admin) throw; + if (feeRebate_ < feeRebate || feeRebate_ > feeTake) throw; + feeRebate = feeRebate_; + } + + function deposit() payable { + tokens[0][msg.sender] = safeAdd(tokens[0][msg.sender], msg.value); + Deposit(0, msg.sender, msg.value, tokens[0][msg.sender]); + } + + function withdraw(uint amount) { + if (tokens[0][msg.sender] < amount) throw; + tokens[0][msg.sender] = safeSub(tokens[0][msg.sender], amount); + if (!msg.sender.call.value(amount)()) throw; + Withdraw(0, msg.sender, amount, tokens[0][msg.sender]); + } + + function depositToken(address token, uint amount) { + //remember to call Token(address).approve(this, amount) or this contract will not be able to do the transfer on your behalf. + if (token==0) throw; + if (!Token(token).transferFrom(msg.sender, this, amount)) throw; + tokens[token][msg.sender] = safeAdd(tokens[token][msg.sender], amount); + Deposit(token, msg.sender, amount, tokens[token][msg.sender]); + } + + function withdrawToken(address token, uint amount) { + if (token==0) throw; + if (tokens[token][msg.sender] < amount) throw; + tokens[token][msg.sender] = safeSub(tokens[token][msg.sender], amount); + if (!Token(token).transfer(msg.sender, amount)) throw; + Withdraw(token, msg.sender, amount, tokens[token][msg.sender]); + } + + function balanceOf(address token, address user) constant returns (uint) { + return tokens[token][user]; + } + + function order(address tokenGet, uint amountGet, address tokenGive, uint amountGive, uint expires, uint nonce) { + bytes32 hash = sha256(this, tokenGet, amountGet, tokenGive, amountGive, expires, nonce); + orders[msg.sender][hash] = true; + Order(tokenGet, amountGet, tokenGive, amountGive, expires, nonce, msg.sender); + } + + function trade(address tokenGet, uint amountGet, address tokenGive, uint amountGive, uint expires, uint nonce, address user, uint8 v, bytes32 r, bytes32 s, uint amount) { + //amount is in amountGet terms + bytes32 hash = sha256(this, tokenGet, amountGet, tokenGive, amountGive, expires, nonce); + if (!( + (orders[user][hash] || ecrecover(sha3("\x19Ethereum Signed Message:\n32", hash),v,r,s) == user) && + block.number <= expires && + safeAdd(orderFills[user][hash], amount) <= amountGet + )) throw; + tradeBalances(tokenGet, amountGet, tokenGive, amountGive, user, amount); + orderFills[user][hash] = safeAdd(orderFills[user][hash], amount); + Trade(tokenGet, amount, tokenGive, amountGive * amount / amountGet, user, msg.sender); + } + + function tradeBalances(address tokenGet, uint amountGet, address tokenGive, uint amountGive, address user, uint amount) private { + uint feeMakeXfer = safeMul(amount, feeMake) / (1 ether); + uint feeTakeXfer = safeMul(amount, feeTake) / (1 ether); + uint feeRebateXfer = 0; + if (accountLevelsAddr != 0x0) { + uint accountLevel = AccountLevels(accountLevelsAddr).accountLevel(user); + if (accountLevel==1) feeRebateXfer = safeMul(amount, feeRebate) / (1 ether); + if (accountLevel==2) feeRebateXfer = feeTakeXfer; + } + tokens[tokenGet][msg.sender] = safeSub(tokens[tokenGet][msg.sender], safeAdd(amount, feeTakeXfer)); + tokens[tokenGet][user] = safeAdd(tokens[tokenGet][user], safeSub(safeAdd(amount, feeRebateXfer), feeMakeXfer)); + tokens[tokenGet][feeAccount] = safeAdd(tokens[tokenGet][feeAccount], safeSub(safeAdd(feeMakeXfer, feeTakeXfer), feeRebateXfer)); + tokens[tokenGive][user] = safeSub(tokens[tokenGive][user], safeMul(amountGive, amount) / amountGet); + tokens[tokenGive][msg.sender] = safeAdd(tokens[tokenGive][msg.sender], safeMul(amountGive, amount) / amountGet); + } + + function testTrade(address tokenGet, uint amountGet, address tokenGive, uint amountGive, uint expires, uint nonce, address user, uint8 v, bytes32 r, bytes32 s, uint amount, address sender) constant returns(bool) { + if (!( + tokens[tokenGet][sender] >= amount && + availableVolume(tokenGet, amountGet, tokenGive, amountGive, expires, nonce, user, v, r, s) >= amount + )) return false; + return true; + } + + function availableVolume(address tokenGet, uint amountGet, address tokenGive, uint amountGive, uint expires, uint nonce, address user, uint8 v, bytes32 r, bytes32 s) constant returns(uint) { + bytes32 hash = sha256(this, tokenGet, amountGet, tokenGive, amountGive, expires, nonce); + if (!( + (orders[user][hash] || ecrecover(sha3("\x19Ethereum Signed Message:\n32", hash),v,r,s) == user) && + block.number <= expires + )) return 0; + uint available1 = safeSub(amountGet, orderFills[user][hash]); + uint available2 = safeMul(tokens[tokenGive][user], amountGet) / amountGive; + if (available1 Buffer; + const soliditySHA256: (argTypes: string[], args: any[]) => Buffer; const methodID: (name: string, types: string[]) => Buffer; } diff --git a/packages/contracts/test/tutorials/arbitrage.ts b/packages/contracts/test/tutorials/arbitrage.ts new file mode 100644 index 000000000..b2f1c338a --- /dev/null +++ b/packages/contracts/test/tutorials/arbitrage.ts @@ -0,0 +1,207 @@ +import { ECSignature, ZeroEx } from '0x.js'; +import { BlockchainLifecycle, devConstants, web3Factory } from '@0xproject/dev-utils'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as chai from 'chai'; +import ethUtil = require('ethereumjs-util'); +import * as Web3 from 'web3'; + +import { Balances } from '../../util/balances'; +import { constants } from '../../util/constants'; +import { crypto } from '../../util/crypto'; +import { ExchangeWrapper } from '../../util/exchange_wrapper'; +import { Order } from '../../util/order'; +import { OrderFactory } from '../../util/order_factory'; +import { BalancesByOwner, ContractName, ExchangeContractErrs } from '../../util/types'; +import { chaiSetup } from '../utils/chai_setup'; +import { deployer } from '../utils/deployer'; + +chaiSetup.configure(); +const expect = chai.expect; +const web3 = web3Factory.create(); +const web3Wrapper = new Web3Wrapper(web3.currentProvider); +const blockchainLifecycle = new BlockchainLifecycle(); + +describe.only('Arbitrage', () => { + let coinbase: string; + let maker: string; + let edMaker: string; + let edFrontRunner: string; + 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: Web3.ContractInstance; + let zrx: Web3.ContractInstance; + let arbitrage: Web3.ContractInstance; + let etherDelta: Web3.ContractInstance; + + let order: Order; + let exWrapper: ExchangeWrapper; + let orderFactory: OrderFactory; + + let zeroEx: ZeroEx; + + before(async () => { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + [coinbase, maker, edMaker, edFrontRunner] = accounts; + weth = await deployer.deployAsync(ContractName.DummyToken); + zrx = await deployer.deployAsync(ContractName.DummyToken); + const accountLevels = await deployer.deployAsync(ContractName.AccountLevels); + const edAdminAddress = accounts[0]; + const edMakerFee = 0; + const edTakerFee = 0; + const edFeeRebate = 0; + etherDelta = await deployer.deployAsync(ContractName.EtherDelta, [ + edAdminAddress, + feeRecipient, + accountLevels.address, + edMakerFee, + edTakerFee, + edFeeRebate, + ]); + const tokenTransferProxy = await deployer.deployAsync(ContractName.TokenTransferProxy); + const exchange = await deployer.deployAsync(ContractName.Exchange, [zrx.address, tokenTransferProxy.address]); + await tokenTransferProxy.addAuthorizedAddress(exchange.address, { from: accounts[0] }); + zeroEx = new ZeroEx(web3.currentProvider, { + exchangeContractAddress: exchange.address, + networkId: constants.TESTRPC_NETWORK_ID, + }); + exWrapper = new ExchangeWrapper(exchange, zeroEx); + + const defaultOrderParams = { + exchangeContractAddress: exchange.address, + maker, + feeRecipient, + makerToken: zrx.address, + takerToken: weth.address, + makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(1), 18), + takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(1), 18), + makerFee: new BigNumber(0), + takerFee: new BigNumber(0), + }; + orderFactory = new OrderFactory(web3Wrapper, defaultOrderParams); + arbitrage = await deployer.deployAsync(ContractName.Arbitrage, [ + exchange.address, + etherDelta.address, + tokenTransferProxy.address, + ]); + // Enable arbitrage and withdrawals of tokens + await arbitrage.setAllowances(weth.address, { from: coinbase }); + await arbitrage.setAllowances(zrx.address, { from: coinbase }); + + // Give some tokens to arbitrage contract + await weth.setBalance(arbitrage.address, ZeroEx.toBaseUnitAmount(new BigNumber(1), 18), { from: coinbase }); + + // Fund the maker on exchange side + await zrx.setBalance(maker, ZeroEx.toBaseUnitAmount(new BigNumber(1), 18), { from: coinbase }); + // Set the allowance for the maker on Exchange side + await zrx.approve(tokenTransferProxy.address, INITIAL_ALLOWANCE, { from: maker }); + + const amountGive = ZeroEx.toBaseUnitAmount(new BigNumber(2), 18); + // Fund the maker on EtherDelta side + await weth.setBalance(edMaker, ZeroEx.toBaseUnitAmount(new BigNumber(2), 18), { from: coinbase }); + // Set the allowance for the maker on EtherDelta side + await weth.approve(etherDelta.address, INITIAL_ALLOWANCE, { from: edMaker }); + // Deposit maker funds into EtherDelta + await etherDelta.depositToken(weth.address, amountGive, { from: edMaker }); + + const amountGet = ZeroEx.toBaseUnitAmount(new BigNumber(1), 18); + // Fund the front runner on EtherDelta side + await zrx.setBalance(edFrontRunner, amountGet, { from: coinbase }); + // Set the allowance for the maker on EtherDelta side + await zrx.approve(etherDelta.address, INITIAL_ALLOWANCE, { from: edFrontRunner }); + // Deposit front runner funds into EtherDelta + await etherDelta.depositToken(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 amountGet: BigNumber; + let amountGive: BigNumber; + let expires: BigNumber; + let nonce: BigNumber; + let edSignature: ECSignature; + before(async () => { + order = await orderFactory.newSignedOrderAsync(); + const shouldAddPersonalMessagePrefix = false; + tokenGet = zrx.address; + amountGet = ZeroEx.toBaseUnitAmount(new BigNumber(1), 18); + tokenGive = weth.address; + amountGive = ZeroEx.toBaseUnitAmount(new BigNumber(2), 18); + const blockNumber = await web3Wrapper.getBlockNumberAsync(); + expires = new BigNumber(blockNumber + 10); + nonce = new BigNumber(42); + const edOrderHash = `0x${crypto + .solSHA256([etherDelta.address, tokenGet, amountGet, tokenGive, amountGive, expires, nonce]) + .toString('hex')}`; + edSignature = await zeroEx.signOrderHashAsync(edOrderHash, edMaker, shouldAddPersonalMessagePrefix); + addresses = [ + order.params.maker, + order.params.taker, + order.params.makerToken, + order.params.takerToken, + order.params.feeRecipient, + tokenGet, + tokenGive, + edMaker, + ]; + const fillTakerTokenAmount = ZeroEx.toBaseUnitAmount(new BigNumber(1), 18); + const edFillAmount = ZeroEx.toBaseUnitAmount(new BigNumber(1), 18); + values = [ + order.params.makerTokenAmount, + order.params.takerTokenAmount, + order.params.makerFee, + order.params.takerFee, + order.params.expirationTimestampInSec, + order.params.salt, + fillTakerTokenAmount, + amountGet, + amountGive, + expires, + nonce, + edFillAmount, + ]; + v = [order.params.v as number, edSignature.v]; + r = [order.params.r as string, edSignature.r]; + s = [order.params.s as string, edSignature.s]; + }); + it('1', async () => { + const txHash = await arbitrage.makeAtomicTrade(addresses, values, v, r, s, { from: coinbase }); + const res = await zeroEx.awaitTransactionMinedAsync(txHash); + const postBalance = await weth.balanceOf(arbitrage.address); + expect(postBalance).to.be.bignumber.equal(ZeroEx.toBaseUnitAmount(new BigNumber(2), 18)); + }); + it('2', async () => { + // Front-running transaction + await etherDelta.trade( + tokenGet, + amountGet, + tokenGive, + amountGive, + expires, + nonce, + edMaker, + edSignature.v, + edSignature.r, + edSignature.s, + ZeroEx.toBaseUnitAmount(new BigNumber(1), 18), + { from: edFrontRunner }, + ); + return expect(arbitrage.makeAtomicTrade(addresses, values, v, r, s, { from: coinbase })).to.be.rejectedWith( + constants.REVERT, + ); + }); + }); +}); diff --git a/packages/contracts/util/crypto.ts b/packages/contracts/util/crypto.ts index 9173df643..97b8f5643 100644 --- a/packages/contracts/util/crypto.ts +++ b/packages/contracts/util/crypto.ts @@ -13,6 +13,12 @@ export const crypto = { * valid Ethereum address -> address */ solSHA3(args: any[]): Buffer { + return crypto._solHash(args, ABI.soliditySHA3); + }, + solSHA256(args: any[]): Buffer { + return crypto._solHash(args, ABI.soliditySHA256); + }, + _solHash(args: any[], hashFunction: (types: string[], values: any[]) => Buffer) { const argTypes: string[] = []; _.each(args, (arg, i) => { const isNumber = _.isFinite(arg); @@ -31,7 +37,7 @@ export const crypto = { throw new Error(`Unable to guess arg type: ${arg}`); } }); - const hash = ABI.soliditySHA3(argTypes, args); + const hash = hashFunction(argTypes, args); return hash; }, }; diff --git a/packages/contracts/util/types.ts b/packages/contracts/util/types.ts index d6e4c587d..61a19acb4 100644 --- a/packages/contracts/util/types.ts +++ b/packages/contracts/util/types.ts @@ -96,6 +96,9 @@ export enum ContractName { EtherToken = 'WETH9', MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress = 'MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress', MaliciousToken = 'MaliciousToken', + AccountLevels = 'AccountLevels', + EtherDelta = 'EtherDelta', + Arbitrage = 'Arbitrage', } export interface Artifact { -- cgit v1.2.3