diff options
author | Fabio Berger <me@fabioberger.com> | 2018-03-02 21:44:53 +0800 |
---|---|---|
committer | Fabio Berger <me@fabioberger.com> | 2018-03-02 21:44:53 +0800 |
commit | 406aedfdc2062ecbdbc6db4a53ae2bb945fb79d3 (patch) | |
tree | d2b21e04c65608dcef473da44b902a9efa12016e | |
parent | 67c834841ea0f8fb4d8d194c0f68802f48e764ee (diff) | |
parent | 4a57f2a762619932a882d08e62006e83a584a684 (diff) | |
download | dexon-sol-tools-406aedfdc2062ecbdbc6db4a53ae2bb945fb79d3.tar.gz dexon-sol-tools-406aedfdc2062ecbdbc6db4a53ae2bb945fb79d3.tar.zst dexon-sol-tools-406aedfdc2062ecbdbc6db4a53ae2bb945fb79d3.zip |
Merge branch 'development' into fix/doc_bugs
* development: (21 commits)
Adjust the tests
Move tutorials to adhere to current dir structure
Make tests slightly nicer
Remove only
Improve the comments
Improve the comments
Add comments to Arbitrage contract
Don't pass tokenGet and tokenGive because we can get them from 0x order
Pretty-print ED contracts
Make external
Fix a typo
Change type to uint256
Make setAllowances external
Fix the comment
Put all ED contracts in one folder
Move tutorials contracts to src folder
Remove false-positive linter failure because of chai-as-pronmised incorrect types
Assert that the balances don't change if arbitrage fails
Initial implementation of Arbitrage contract with tests
Set max to 2 ETH/2 ZRX
...
-rw-r--r-- | lerna.json | 5 | ||||
-rw-r--r-- | packages/contracts/globals.d.ts | 1 | ||||
-rw-r--r-- | packages/contracts/package.json | 4 | ||||
-rw-r--r-- | packages/contracts/src/contracts/current/tutorials/Arbitrage/Arbitrage.sol | 114 | ||||
-rw-r--r-- | packages/contracts/src/contracts/current/tutorials/EtherDelta/AccountLevels.sol | 11 | ||||
-rw-r--r-- | packages/contracts/src/contracts/current/tutorials/EtherDelta/EtherDelta.sol | 168 | ||||
-rw-r--r-- | packages/contracts/test/tutorials/arbitrage.ts | 226 | ||||
-rw-r--r-- | packages/contracts/util/crypto.ts | 8 | ||||
-rw-r--r-- | packages/contracts/util/types.ts | 3 | ||||
-rw-r--r-- | packages/testnet-faucets/src/ts/dispense_asset_tasks.ts | 21 |
10 files changed, 558 insertions, 3 deletions
diff --git a/lerna.json b/lerna.json index dbe42bcb1..5f975fff9 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,11 @@ { "lerna": "2.5.1", "packages": ["packages/*"], + "commands": { + "publish": { + "allowBranch": "development" + } + }, "version": "independent", "commands": { "publish": { diff --git a/packages/contracts/globals.d.ts b/packages/contracts/globals.d.ts index 0e6586a4b..c6597054a 100644 --- a/packages/contracts/globals.d.ts +++ b/packages/contracts/globals.d.ts @@ -30,5 +30,6 @@ declare module 'web3-eth-abi' { declare module 'ethereumjs-abi' { const soliditySHA3: (argTypes: string[], args: any[]) => Buffer; + const soliditySHA256: (argTypes: string[], args: any[]) => Buffer; const methodID: (name: string, types: string[]) => Buffer; } diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 7cac30069..ca1715d0e 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -17,13 +17,13 @@ "compile:comment": "Yarn workspaces do not link binaries correctly so we need to reference them directly https://github.com/yarnpkg/yarn/issues/3846", "compile": "node ../deployer/lib/src/cli.js compile --contracts ${npm_package_config_contracts} --contracts-dir src/contracts --artifacts-dir src/artifacts", "clean": "shx rm -rf ./lib", - "generate_contract_wrappers": "node ../abi-gen/lib/index.js --abis 'src/artifacts/@(DummyToken|TokenTransferProxy|Exchange|TokenRegistry|MultiSigWallet|MultiSigWalletWithTimeLock|MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress|TokenRegistry|ZRXToken).json' --template ../contract_templates/contract.handlebars --partials '../contract_templates/partials/**/*.handlebars' --output src/contract_wrappers/generated --backend ethers && prettier --write 'src/contract_wrappers/generated/**.ts'", + "generate_contract_wrappers": "node ../abi-gen/lib/index.js --abis 'src/artifacts/@(DummyToken|TokenTransferProxy|Exchange|TokenRegistry|MultiSigWallet|MultiSigWalletWithTimeLock|MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress|TokenRegistry|ZRXToken|Arbitrage|EtherDelta|AccountLevels).json' --template ../contract_templates/contract.handlebars --partials '../contract_templates/partials/**/*.handlebars' --output src/contract_wrappers/generated --backend ethers && prettier --write 'src/contract_wrappers/generated/**.ts'", "migrate": "node ../deployer/lib/src/cli.js migrate", "lint": "tslint --project . 'migrations/**/*.ts' 'test/**/*.ts' 'util/**/*.ts' 'deploy/**/*.ts'", "test:circleci": "yarn test" }, "config": { - "contracts": "Exchange,DummyToken,ZRXToken,Token,WETH9,TokenTransferProxy,MultiSigWallet,MultiSigWalletWithTimeLock,MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress,MaliciousToken,TokenRegistry" + "contracts": "Exchange,DummyToken,ZRXToken,Token,WETH9,TokenTransferProxy,MultiSigWallet,MultiSigWalletWithTimeLock,MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress,MaliciousToken,TokenRegistry,Arbitrage,EtherDelta,AccountLevels" }, "repository": { "type": "git", diff --git a/packages/contracts/src/contracts/current/tutorials/Arbitrage/Arbitrage.sol b/packages/contracts/src/contracts/current/tutorials/Arbitrage/Arbitrage.sol new file mode 100644 index 000000000..a9f3c22e6 --- /dev/null +++ b/packages/contracts/src/contracts/current/tutorials/Arbitrage/Arbitrage.sol @@ -0,0 +1,114 @@ +pragma solidity ^0.4.19; + +import { Exchange } from "../../protocol/Exchange/Exchange.sol"; +import { EtherDelta } from "../EtherDelta/EtherDelta.sol"; +import { Ownable } from "../../utils/Ownable/Ownable.sol"; +import { Token } from "../../tokens/Token/Token.sol"; + +/// @title Arbitrage - Facilitates atomic arbitrage of ERC20 tokens between EtherDelta and 0x Exchange contract. +/// @author Leonid Logvinov - <leo@0xProject.com> +contract Arbitrage is Ownable { + + Exchange exchange; + EtherDelta etherDelta; + address proxyAddress; + + uint256 constant MAX_UINT = 2**256 - 1; + + function Arbitrage(address _exchangeAddress, address _etherDeltaAddress, address _proxyAddress) { + exchange = Exchange(_exchangeAddress); + etherDelta = EtherDelta(_etherDeltaAddress); + proxyAddress = _proxyAddress; + } + + /* + * Makes token tradeable by setting an allowance for etherDelta and 0x proxy contract. + * Also sets an allowance for the owner of the contracts therefore allowing to withdraw tokens. + */ + function setAllowances(address tokenAddress) external onlyOwner { + Token token = Token(tokenAddress); + token.approve(address(etherDelta), MAX_UINT); + token.approve(proxyAddress, MAX_UINT); + token.approve(owner, MAX_UINT); + } + + /* + * Because of the limits on the number of local variables in Solidity we need to compress parameters while loosing + * readability. Scheme of the parameter layout: + * + * addresses + * 0..4 orderAddresses + * 5 user + * + * values + * 0..5 orderValues + * 6 fillTakerTokenAmount + * 7 amountGet + * 8 amountGive + * 9 expires + * 10 nonce + * 11 amount + + * signature + * exchange then etherDelta + */ + function makeAtomicTrade( + address[6] addresses, uint[12] values, + uint8[2] v, bytes32[2] r, bytes32[2] s + ) external onlyOwner { + makeExchangeTrade(addresses, values, v, r, s); + makeEtherDeltaTrade(addresses, values, v, r, s); + } + + function makeEtherDeltaTrade( + address[6] addresses, uint[12] values, + uint8[2] v, bytes32[2] r, bytes32[2] s + ) internal { + uint amount = values[11]; + etherDelta.depositToken( + addresses[2], // tokenGet === makerToken + values[7] // amountGet + ); + etherDelta.trade( + addresses[2], // tokenGet === makerToken + values[7], // amountGet + addresses[3], // tokenGive === takerToken + values[8], // amountGive + values[9], // expires + values[10], // nonce + addresses[5], // user + v[1], + r[1], + s[1], + amount + ); + etherDelta.withdrawToken( + addresses[3], // tokenGive === tokenToken + values[8] // amountGive + ); + } + + function makeExchangeTrade( + address[6] addresses, uint[12] values, + uint8[2] v, bytes32[2] r, bytes32[2] s + ) internal { + address[5] memory orderAddresses = [ + addresses[0], // maker + addresses[1], // taker + addresses[2], // makerToken + addresses[3], // takerToken + addresses[4] // feeRecepient + ]; + uint[6] memory orderValues = [ + values[0], // makerTokenAmount + values[1], // takerTokenAmount + values[2], // makerFee + values[3], // takerFee + values[4], // expirationTimestampInSec + values[5] // salt + ]; + uint fillTakerTokenAmount = values[6]; // fillTakerTokenAmount + // Execute Exchange trade. It either succeeds in full or fails and reverts all the changes. + exchange.fillOrKillOrder(orderAddresses, orderValues, fillTakerTokenAmount, v[0], r[0], s[0]); + } +} diff --git a/packages/contracts/src/contracts/current/tutorials/EtherDelta/AccountLevels.sol b/packages/contracts/src/contracts/current/tutorials/EtherDelta/AccountLevels.sol new file mode 100644 index 000000000..8d7a930d3 --- /dev/null +++ b/packages/contracts/src/contracts/current/tutorials/EtherDelta/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/src/contracts/current/tutorials/EtherDelta/EtherDelta.sol b/packages/contracts/src/contracts/current/tutorials/EtherDelta/EtherDelta.sol new file mode 100644 index 000000000..49847ab48 --- /dev/null +++ b/packages/contracts/src/contracts/current/tutorials/EtherDelta/EtherDelta.sol @@ -0,0 +1,168 @@ +pragma solidity ^0.4.19; + +import { SafeMath } from "../../utils/SafeMath/SafeMath.sol"; +import { AccountLevels } from "./AccountLevels.sol"; +import { Token } from "../../tokens/Token/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<available2) return available1; + return available2; + } + + function amountFilled(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); + return orderFills[user][hash]; + } + + function cancelOrder(address tokenGet, uint amountGet, address tokenGive, uint amountGive, uint expires, uint nonce, uint8 v, bytes32 r, bytes32 s) { + bytes32 hash = sha256(this, tokenGet, amountGet, tokenGive, amountGive, expires, nonce); + if (!(orders[msg.sender][hash] || ecrecover(sha3("\x19Ethereum Signed Message:\n32", hash),v,r,s) == msg.sender)) throw; + orderFills[msg.sender][hash] = amountGet; + Cancel(tokenGet, amountGet, tokenGive, amountGive, expires, nonce, msg.sender, v, r, s); + } +} diff --git a/packages/contracts/test/tutorials/arbitrage.ts b/packages/contracts/test/tutorials/arbitrage.ts new file mode 100644 index 000000000..2bafbff0b --- /dev/null +++ b/packages/contracts/test/tutorials/arbitrage.ts @@ -0,0 +1,226 @@ +import { ECSignature, SignedOrder, 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 { ArbitrageContract } from '../../src/contract_wrappers/generated/arbitrage'; +import { EtherDeltaContract } from '../../src/contract_wrappers/generated/ether_delta'; +import { ExchangeContract } from '../../src/contract_wrappers/generated/exchange'; +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, 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('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: Web3.ContractInstance; + let zrx: Web3.ContractInstance; + 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 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; + const etherDeltaInstance = await deployer.deployAsync(ContractName.EtherDelta, [ + edAdminAddress, + feeRecipient, + accountLevels.address, + edMakerFee, + edTakerFee, + edFeeRebate, + ]); + etherDelta = new EtherDeltaContract(web3Wrapper, etherDeltaInstance.abi, etherDeltaInstance.address); + const tokenTransferProxy = await deployer.deployAsync(ContractName.TokenTransferProxy); + const exchangeInstance = await deployer.deployAsync(ContractName.Exchange, [ + zrx.address, + tokenTransferProxy.address, + ]); + await tokenTransferProxy.addAuthorizedAddress(exchangeInstance.address, { from: accounts[0] }); + zeroEx = new ZeroEx(web3.currentProvider, { + exchangeContractAddress: exchangeInstance.address, + networkId: constants.TESTRPC_NETWORK_ID, + }); + const exchange = new ExchangeContract(web3Wrapper, exchangeInstance.abi, exchangeInstance.address); + 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); + const arbitrageInstance = await deployer.deployAsync(ContractName.Arbitrage, [ + exchange.address, + etherDelta.address, + tokenTransferProxy.address, + ]); + arbitrage = new ArbitrageContract(web3Wrapper, arbitrageInstance.abi, arbitrageInstance.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(arbitrage.address, takerTokenAmount, { from: coinbase }); + + // Fund the maker on exchange side + await zrx.setBalance(maker, makerTokenAmount, { from: coinbase }); + // Set the allowance for the maker on Exchange side + await zrx.approve(tokenTransferProxy.address, INITIAL_ALLOWANCE, { from: maker }); + + amountGive = ZeroEx.toBaseUnitAmount(new BigNumber(2), 18); + // Fund the maker on EtherDelta side + await weth.setBalance(edMaker, amountGive, { 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.sendTransactionAsync(weth.address, amountGive, { from: edMaker }); + + amountGet = makerTokenAmount; + // Fund the front runner on EtherDelta side + await zrx.setBalance(edFrontRunner, amountGet, { from: coinbase }); + // Set the allowance for the front-runner on EtherDelta side + await zrx.approve(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(arbitrage.address); + expect(postBalance).to.be.bignumber.equal(amountGive); + }); + it('should fail and revert if front-runned', async () => { + const preBalance = await weth.balanceOf(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(arbitrage.address); + expect(preBalance).to.be.bignumber.equal(postBalance); + }); + }); +}); 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 { diff --git a/packages/testnet-faucets/src/ts/dispense_asset_tasks.ts b/packages/testnet-faucets/src/ts/dispense_asset_tasks.ts index 9aa47463c..56b0a9e45 100644 --- a/packages/testnet-faucets/src/ts/dispense_asset_tasks.ts +++ b/packages/testnet-faucets/src/ts/dispense_asset_tasks.ts @@ -9,11 +9,21 @@ import { utils } from './utils'; const DISPENSE_AMOUNT_ETHER = 0.1; const DISPENSE_AMOUNT_TOKEN = 0.1; +const DISPENSE_MAX_AMOUNT_TOKEN = 2; +const DISPENSE_MAX_AMOUNT_ETHER = 2; export const dispenseAssetTasks = { dispenseEtherTask(recipientAddress: string, web3: Web3) { return async () => { utils.consoleLog(`Processing ETH ${recipientAddress}`); + const userBalance = await promisify<BigNumber>(web3.eth.getBalance)(recipientAddress); + const maxAmountInWei = new BigNumber(web3.toWei(DISPENSE_MAX_AMOUNT_ETHER, 'ether')); + if (userBalance.greaterThanOrEqualTo(maxAmountInWei)) { + utils.consoleLog( + `User exceeded ETH balance maximum (${maxAmountInWei}) ${recipientAddress} ${userBalance} `, + ); + return; + } const sendTransactionAsync = promisify(web3.eth.sendTransaction); const txHash = await sendTransactionAsync({ from: configs.DISPENSER_ADDRESS, @@ -32,6 +42,17 @@ export const dispenseAssetTasks = { throw new Error(`Unsupported asset type: ${tokenSymbol}`); } const baseUnitAmount = ZeroEx.toBaseUnitAmount(amountToDispense, token.decimals); + const userBalanceBaseUnits = await zeroEx.token.getBalanceAsync(token.address, recipientAddress); + const maxAmountBaseUnits = ZeroEx.toBaseUnitAmount( + new BigNumber(DISPENSE_MAX_AMOUNT_TOKEN), + token.decimals, + ); + if (userBalanceBaseUnits.greaterThanOrEqualTo(maxAmountBaseUnits)) { + utils.consoleLog( + `User exceeded token balance maximum (${maxAmountBaseUnits}) ${recipientAddress} ${userBalanceBaseUnits} `, + ); + return; + } const txHash = await zeroEx.token.transferAsync( token.address, configs.DISPENSER_ADDRESS, |