├── .nvmrc ├── migrations ├── 1_initial_migration.js └── 2_deploy_contracts.js ├── contracts ├── owned.sol ├── Migrations.sol ├── FixedSupplyToken.sol └── Exchange.sol ├── truffle.js ├── LICENSE ├── test ├── 02_fixed_supply_token_transfer_tokens_between_accounts_test.js ├── 01_fixed_supply_token_initial_token_ownership_test.js ├── 03_exchange_deposit_withdraw_ether_test.js ├── 09_exchange_trading_buy_tokens_creates_market_buy_order_test.js ├── 07_exchange_trading_sell_tokens_test.js ├── 08_exchange_trading_buy_tokens_creates_buy_limit_order_test.js ├── 05_exchange_without_add_token_throws_on_deposit_withdraw_test.js ├── 04_exchange_add_deposit_withdraw_tokens_test.js └── 06_exchange_trading_buy_sell_limit_order_test.js └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v9.8.0 2 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require("./Migrations.sol"); 2 | 3 | module.exports = (deployer, network, accounts) => { 4 | if (network == "development") { 5 | deployer.deploy(Migrations); 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /contracts/owned.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.18; 2 | 3 | contract owned { 4 | address owner; 5 | 6 | modifier onlyowner() { 7 | if (msg.sender == owner) { 8 | _; 9 | } 10 | } 11 | 12 | function owned() public { 13 | owner = msg.sender; 14 | } 15 | } -------------------------------------------------------------------------------- /migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | const FixedSupplyToken = artifacts.require("./FixedSupplyToken.sol"); 2 | const owned = artifacts.require("./owned.sol"); 3 | const Exchange = artifacts.require("./Exchange.sol"); 4 | 5 | module.exports = (deployer) => { 6 | deployer.deploy(FixedSupplyToken); 7 | deployer.deploy(owned); 8 | deployer.deploy(Exchange); 9 | }; -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.17; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | modifier restricted() { 8 | if (msg.sender == owner) _; 9 | } 10 | 11 | function Migrations() public { 12 | owner = msg.sender; 13 | } 14 | 15 | function setCompleted(uint completed) public restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) public restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // See 3 | // to customize your Truffle configuration! 4 | networks: { 5 | development: { 6 | host: "127.0.0.1", 7 | port: 8500, 8 | network_id: "3", // Match any network id 9 | gas: 7984452, // Block Gas Limit same as latest on Mainnet https://ethstats.net/ 10 | gasPrice: 2000000000, // same as latest on Mainnet https://ethstats.net/ 11 | // Mnemonic: "copy obey episode awake damp vacant protect hold wish primary travel shy" 12 | from: "0x7c06350cb8640a113a618004a828d3411a4f32d3" 13 | } 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Thomas Wiesner. Reference: https://github.com/tomw1808/distributed_exchange_truffle_class_3 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/02_fixed_supply_token_transfer_tokens_between_accounts_test.js: -------------------------------------------------------------------------------- 1 | // Specifically request an abstraction for FixedSupplyToken 2 | const fixedSupplyToken = artifacts.require("FixedSupplyToken"); 3 | 4 | contract('FixedSupplyToken - token transfer between accounts', (accounts) => { 5 | 6 | it("should send token correctly between accounts", () => { 7 | let token; 8 | 9 | // Get initial balances of first and second account 10 | const account_one = accounts[0]; 11 | const account_two = accounts[1]; 12 | 13 | let account_one_starting_balance; 14 | let account_two_starting_balance; 15 | let account_one_ending_balance; 16 | let account_two_ending_balance; 17 | 18 | let amount = 10; 19 | 20 | return fixedSupplyToken.deployed().then((instance) => { 21 | token = instance; 22 | return token.balanceOf.call(account_one); 23 | }).then((balance) => { 24 | account_one_starting_balance = balance.toNumber(); 25 | return token.balanceOf.call(account_two); 26 | }).then((balance) => { 27 | account_two_starting_balance = balance.toNumber(); 28 | return token.transfer(account_two, amount, {from: account_one}); 29 | }).then(() => { 30 | return token.balanceOf.call(account_one); 31 | }).then((balance) => { 32 | account_one_ending_balance = balance.toNumber(); 33 | return token.balanceOf.call(account_two); 34 | }).then((balance) => { 35 | account_two_ending_balance = balance.toNumber(); 36 | 37 | assert.equal( 38 | account_one_ending_balance, account_one_starting_balance - amount, 39 | "Amount was not correctly received from the sender" 40 | ); 41 | assert.equal( 42 | account_two_ending_balance, 43 | account_two_starting_balance + amount, 44 | "Amount was not correctly sent to the receiver" 45 | ); 46 | }); 47 | }); 48 | }); -------------------------------------------------------------------------------- /test/01_fixed_supply_token_initial_token_ownership_test.js: -------------------------------------------------------------------------------- 1 | // Specifically request an abstraction for FixedSupplyToken 2 | const fixedSupplyToken = artifacts.require("FixedSupplyToken"); 3 | 4 | contract('FixedSupplyToken - initial account token ownership upon deployment', (accounts) => { 5 | 6 | it("first account is owner and should own all tokens upon deployment of FixedSupplyToken contract", () => { 7 | let _totalSupply; 8 | let myTokenInstance; 9 | return fixedSupplyToken.deployed().then((instance) => { 10 | myTokenInstance = instance; 11 | return myTokenInstance.totalSupply.call(); 12 | }).then((totalSupply) => { 13 | _totalSupply = totalSupply; 14 | return myTokenInstance.balanceOf(accounts[0]); 15 | }).then((balanceAccountOwner) => { 16 | assert.equal(balanceAccountOwner.toNumber(), _totalSupply.toNumber(), "Total Amount of \ 17 | tokens should be owned by owner"); 18 | }); 19 | }); 20 | 21 | it("second account in TestRPC should own no tokens", () => { 22 | let myTokenInstance; 23 | return fixedSupplyToken.deployed().then((instance) => { 24 | myTokenInstance = instance; 25 | return myTokenInstance.balanceOf(accounts[1]); 26 | }).then((balanceAccountOwner) => { 27 | assert.equal(balanceAccountOwner.toNumber(), 0, "Total Amount of tokens should be owned \ 28 | by some other address"); 29 | }); 30 | }); 31 | 32 | it("should send token correctly between accounts", () => { 33 | let token; 34 | 35 | // Get initial balances of first and second account 36 | const account_one = accounts[0]; 37 | const account_two = accounts[1]; 38 | 39 | let account_one_starting_balance; 40 | let account_two_starting_balance; 41 | let account_one_ending_balance; 42 | let account_two_ending_balance; 43 | 44 | let amount = 10; 45 | 46 | return fixedSupplyToken.deployed().then((instance) => { 47 | token = instance; 48 | return token.balanceOf.call(account_one); 49 | }).then((balance) => { 50 | account_one_starting_balance = balance.toNumber(); 51 | return token.balanceOf.call(account_two); 52 | }).then((balance) => { 53 | account_two_starting_balance = balance.toNumber(); 54 | return token.transfer(account_two, amount, {from: account_one}); 55 | }).then(() => { 56 | return token.balanceOf.call(account_one); 57 | }).then((balance) => { 58 | account_one_ending_balance = balance.toNumber(); 59 | return token.balanceOf.call(account_two); 60 | }).then((balance) => { 61 | account_two_ending_balance = balance.toNumber(); 62 | 63 | assert.equal( 64 | account_one_ending_balance, account_one_starting_balance - amount, 65 | "Amount was not correctly received from the sender" 66 | ); 67 | assert.equal( 68 | account_two_ending_balance, account_two_starting_balance + amount, 69 | "Amount was not correctly sent to the receiver" 70 | ); 71 | }); 72 | }); 73 | }); -------------------------------------------------------------------------------- /test/03_exchange_deposit_withdraw_ether_test.js: -------------------------------------------------------------------------------- 1 | const fixedSupplyToken = artifacts.require("./FixedSupplyToken.sol"); 2 | const exchange = artifacts.require("./Exchange.sol"); 3 | 4 | contract('Exchange - deposit Ether into DEX from an account address and then withdraw it again and emit events', (accounts) => { 5 | 6 | it("should be possible to Deposit and Withdrawal Ether", () => { 7 | let myExchangeInstance; 8 | const balanceBeforeTransaction = web3.eth.getBalance(accounts[0]); 9 | let balanceAfterDeposit; 10 | let balanceAfterWithdrawal; 11 | let totalGasCostAccumulated = 0; 12 | 13 | return exchange.deployed().then((instance) => { 14 | myExchangeInstance = instance; 15 | 16 | // DEPOSIT INTO DEX FROM A GIVEN ACCOUNT ADDRESS 17 | return myExchangeInstance.depositEther({from: accounts[0], value: web3.toWei(1, "ether")}); 18 | }).then((txHash) => { 19 | // Event Log Test 20 | assert.equal( 21 | txHash.logs[0].event, 22 | "DepositForEthReceived", 23 | "DepositForEthReceived event should be emitted" 24 | ); 25 | totalGasCostAccumulated += txHash.receipt.cumulativeGasUsed * web3.eth.getTransaction(txHash.receipt.transactionHash).gasPrice.toNumber(); 26 | balanceAfterDeposit = web3.eth.getBalance(accounts[0]); 27 | return myExchangeInstance.getEthBalanceInWei.call(); 28 | }).then((balanceInWei) => { 29 | // Check DEX balance in Ether for the account calling the function 30 | assert.equal(balanceInWei.toNumber(), web3.toWei(1, "ether"), "DEX should have one Ether available"); 31 | // Check that at least the Deposited value (excluding gas costs) was take out of the account of the sender 32 | assert.isAtLeast( 33 | balanceBeforeTransaction.toNumber() - balanceAfterDeposit.toNumber(), 34 | web3.toWei(1, "ether"), 35 | "Deposited from the sender account address should be at least one Ether" 36 | ); 37 | 38 | // WITHDRAW FROM DEX TO THE ACCOUNT ADDRESS OF THE CALLER 39 | return myExchangeInstance.withdrawEther(web3.toWei(1, "ether")); 40 | }).then((txHash) => { 41 | // Event Log Test 42 | assert.equal( 43 | txHash.logs[0].event, 44 | "WithdrawalEth", 45 | "WithdrawalEth event should be emitted" 46 | ); 47 | totalGasCostAccumulated += txHash.receipt.cumulativeGasUsed * web3.eth.getTransaction(txHash.receipt.transactionHash).gasPrice.toNumber(); 48 | balanceAfterWithdrawal = web3.eth.getBalance(accounts[0]); 49 | return myExchangeInstance.getEthBalanceInWei.call(); 50 | }).then((balanceInWei) => { 51 | assert.equal(balanceInWei.toNumber(), 0, "DEX should have no Ether available anymore"); 52 | assert.isAtLeast( 53 | balanceAfterWithdrawal.toNumber(), 54 | balanceBeforeTransaction.toNumber() - totalGasCostAccumulated, 55 | "User Account Balance after Depositing an amount of Ether into the DEX and then Withdrawing \ 56 | the same amount should be at least their initial balance minus the total Gas Cost of both transactions" 57 | ); 58 | }); 59 | }); 60 | }); -------------------------------------------------------------------------------- /test/09_exchange_trading_buy_tokens_creates_market_buy_order_test.js: -------------------------------------------------------------------------------- 1 | const fixedSupplyToken = artifacts.require("./FixedSupplyToken.sol"); 2 | const exchange = artifacts.require("./Exchange.sol"); 3 | 4 | contract('Exchange - Buy Tokens - Market Buy Order', (accounts) => { 5 | 6 | before(() => { 7 | let instanceExchange; 8 | let instanceToken; 9 | return exchange.deployed().then((instance) => { 10 | instanceExchange = instance; 11 | return instanceExchange.depositEther({ 12 | from: accounts[0], 13 | value: web3.toWei(3, "ether") 14 | }); 15 | }).then((txResult) => { 16 | return fixedSupplyToken.deployed(); 17 | }).then((myTokenInstance) => { 18 | instanceToken = myTokenInstance; 19 | return instanceExchange.addToken("FIXED", instanceToken.address); 20 | }).then((txResult) => { 21 | return instanceToken.transfer(accounts[1], 2000); 22 | }).then((txResult) => { 23 | return instanceToken.approve(instanceExchange.address, 2000, {from: accounts[1]}); 24 | }).then((txResult) => { 25 | return instanceExchange.depositToken("FIXED", 2000, {from: accounts[1]}); 26 | }); 27 | }); 28 | 29 | it("should create a Market Buy Orders for Tokens when sell prices are below buy price", () => { 30 | let myExchangeInstance; 31 | return exchange.deployed().then((instance) => { 32 | myExchangeInstance = instance; 33 | return myExchangeInstance.getSellOrderBook.call("FIXED"); 34 | }).then((orderBook) => { 35 | assert.equal(orderBook.length, 2, "SellOrderBook should have 2 elements"); 36 | assert.equal(orderBook[0].length, 0, "OrderBook should have 0 buy offers"); 37 | return myExchangeInstance.sellToken("FIXED", web3.toWei(4, "finney"), 5, {from: accounts[1]}); 38 | }).then((txResult) => { 39 | // Event Log Test 40 | assert.equal(txResult.logs.length, 1, "One Log Message should have been emitted"); 41 | assert.equal(txResult.logs[0].event, "LimitSellOrderCreated", "Log Event should be LimitSellOrderCreated"); 42 | return myExchangeInstance.getSellOrderBook.call("FIXED"); 43 | }).then((orderBook) => { 44 | assert.equal(orderBook[0].length, 1, "OrderBook should have 1 sell offers"); 45 | assert.equal(orderBook[1].length, 1, "OrderBook should have 1 sell volume has one element"); 46 | assert.equal(orderBook[1][0], 5, "OrderBook should have a volume of 5 coins someone wants to sell"); 47 | // Note: priceInWei for the buyToken should be greater than or equal to the priceInWei for the sellToken to 48 | // execute a Market Buy Order immediately followed by a Buy Limit Order for the remainder, 49 | // otherwise only a Buy Limit Order is created. 50 | return myExchangeInstance.buyToken("FIXED", web3.toWei(4, "finney"), 5, {from: accounts[0], gas: 4000000}); 51 | }).then((txResult) => { 52 | console.log(txResult); 53 | console.log(txResult.logs[0].args); 54 | 55 | // Event Log Test 56 | assert.equal(txResult.logs.length, 1, "One Log Message should have been emitted"); 57 | assert.equal(txResult.logs[0].event, "BuyOrderFulfilled", "Log Event should be BuyOrderFulfilled"); 58 | return myExchangeInstance.getSellOrderBook.call("FIXED"); 59 | }).then((orderBook) => { 60 | assert.equal(orderBook[0].length, 0, "OrderBook should have 0 buy offers"); 61 | assert.equal(orderBook[1].length, 0, "OrderBook should have 0 buy volume has one element"); 62 | return myExchangeInstance.getBuyOrderBook.call("FIXED"); 63 | }).then((orderBook) => { 64 | assert.equal(orderBook[0].length, 0, "OrderBook should have 0 sell offers"); 65 | assert.equal(orderBook[1].length, 0, "OrderBook should have 0 sell volume elements"); 66 | }); 67 | }); 68 | }); -------------------------------------------------------------------------------- /test/07_exchange_trading_sell_tokens_test.js: -------------------------------------------------------------------------------- 1 | const fixedSupplyToken = artifacts.require("./FixedSupplyToken.sol"); 2 | const exchange = artifacts.require("./Exchange.sol"); 3 | 4 | contract('Exchange - Sell Tokens', (accounts) => { 5 | 6 | before(() => { 7 | let instanceExchange; 8 | let instanceToken; 9 | return exchange.deployed().then((instance) => { 10 | instanceExchange = instance; 11 | return instanceExchange.depositEther({ 12 | from: accounts[0], 13 | value: web3.toWei(3, "ether") 14 | }); 15 | }).then((txResult) => { 16 | return fixedSupplyToken.deployed(); 17 | }).then((myTokenInstance) => { 18 | instanceToken = myTokenInstance; 19 | return instanceExchange.addToken("FIXED", instanceToken.address); 20 | }).then((txResult) => { 21 | return instanceToken.transfer(accounts[1], 2000); 22 | }).then((txResult) => { 23 | return instanceToken.approve(instanceExchange.address, 2000, {from: accounts[1]}); 24 | }).then((txResult) => { 25 | return instanceExchange.depositToken("FIXED", 2000, {from: accounts[1]}); 26 | }); 27 | }); 28 | 29 | it("should be possible to Sell Tokens", () => { 30 | let myExchangeInstance; 31 | return exchange.deployed().then((instance) => { 32 | myExchangeInstance = instance; 33 | return myExchangeInstance.getBuyOrderBook.call("FIXED"); 34 | }).then((orderBook) => { 35 | assert.equal(orderBook.length, 2, "BuyOrderBook should have 2 elements"); 36 | assert.equal(orderBook[0].length, 0, "OrderBook should have 0 buy offers"); 37 | return myExchangeInstance.buyToken("FIXED", web3.toWei(3, "finney"), 5); 38 | }).then((txResult) => { 39 | // Event Log Test 40 | console.log(`Buy Token Event Logs: ${JSON.stringify(txResult.logs[0].args, null, 2)}`); 41 | assert.equal(txResult.logs.length, 1, "One Log Message should be emitted"); 42 | assert.equal(txResult.logs[0].event, "LimitBuyOrderCreated", "Log Event should be LimitBuyOrderCreated"); 43 | return myExchangeInstance.getBuyOrderBook.call("FIXED"); 44 | }).then((orderBook) => { 45 | console.log(`Order Book after Buy Token with Order Book 0: ${orderBook[0]}`); 46 | console.log(`Order Book after Buy Token with Order Book 1: ${orderBook[1]}`); 47 | assert.equal(orderBook[0].length, 1, "OrderBook should have 1 buy offers"); 48 | assert.equal(orderBook[1].length, 1, "OrderBook should have 1 buy volume has one element"); 49 | assert.equal(orderBook[1][0], 5, "OrderBook should have a volume of 5 coins someone wants to buy"); 50 | return myExchangeInstance.sellToken("FIXED", web3.toWei(2, "finney"), 5, {from: accounts[1]}); 51 | }).then((txResult) => { 52 | // Event Log Test 53 | console.log(`Sell Token Event Logs: ${JSON.stringify(txResult.logs[0].args, null, 2)}`); 54 | assert.equal(txResult.logs.length, 1, "One Log Message should be emitted"); 55 | assert.equal(txResult.logs[0].event, "SellOrderFulfilled", "Log Event should be SellOrderFulfilled"); 56 | return myExchangeInstance.getBuyOrderBook.call("FIXED"); 57 | }).then((orderBook) => { 58 | console.log(`Order Book after Sell Token with Order Book 0: ${orderBook[0]}`); 59 | console.log(`Order Book after Sell Token with Order Book 1: ${orderBook[1]}`); 60 | assert.equal(orderBook[0].length, 0, "OrderBook should have 0 buy offers"); 61 | assert.equal(orderBook[1].length, 0, "OrderBook should have 0 buy volume has one element"); 62 | return myExchangeInstance.getSellOrderBook.call("FIXED"); 63 | }).then((orderBook) => { 64 | assert.equal(orderBook[0].length, 0, "OrderBook should have 0 sell offers"); 65 | assert.equal(orderBook[1].length, 0, "OrderBook should have 0 sell volume elements"); 66 | }); 67 | }); 68 | }); -------------------------------------------------------------------------------- /test/08_exchange_trading_buy_tokens_creates_buy_limit_order_test.js: -------------------------------------------------------------------------------- 1 | const fixedSupplyToken = artifacts.require("./FixedSupplyToken.sol"); 2 | const exchange = artifacts.require("./Exchange.sol"); 3 | 4 | contract('Exchange - Buy Tokens - Buy Limit Order', (accounts) => { 5 | 6 | before(() => { 7 | let instanceExchange; 8 | let instanceToken; 9 | return exchange.deployed().then((instance) => { 10 | instanceExchange = instance; 11 | return instanceExchange.depositEther({ 12 | from: accounts[0], 13 | value: web3.toWei(3, "ether") 14 | }); 15 | }).then((txResult) => { 16 | return fixedSupplyToken.deployed(); 17 | }).then((myTokenInstance) => { 18 | instanceToken = myTokenInstance; 19 | return instanceExchange.addToken("FIXED", instanceToken.address); 20 | }).then((txResult) => { 21 | return instanceToken.transfer(accounts[1], 2000); 22 | }).then((txResult) => { 23 | return instanceToken.approve(instanceExchange.address, 2000, {from: accounts[1]}); 24 | }).then((txResult) => { 25 | return instanceExchange.depositToken("FIXED", 2000, {from: accounts[1]}); 26 | }); 27 | }); 28 | 29 | it("should create a Buy Limit Order for Tokens when no sell prices exist or sell prices are above buy price", () => { 30 | let myExchangeInstance; 31 | return exchange.deployed().then((instance) => { 32 | myExchangeInstance = instance; 33 | return myExchangeInstance.getSellOrderBook.call("FIXED"); 34 | }).then((orderBook) => { 35 | assert.equal(orderBook.length, 2, "SellOrderBook should have 2 elements"); 36 | assert.equal(orderBook[0].length, 0, "OrderBook should have 0 buy offers"); 37 | // Create a Sell Price that will be higher than the Buy Price 38 | return myExchangeInstance.sellToken("FIXED", web3.toWei(4, "finney"), 5, {from: accounts[1]}); 39 | }).then((txResult) => { 40 | // Event Log Test 41 | assert.equal(txResult.logs.length, 1, "One Log Message should have been emitted"); 42 | assert.equal(txResult.logs[0].event, "LimitSellOrderCreated", "Log Event should be LimitSellOrderCreated"); 43 | return myExchangeInstance.getSellOrderBook.call("FIXED"); 44 | }).then((orderBook) => { 45 | assert.equal(orderBook[0].length, 1, "OrderBook should have 1 sell offers"); 46 | assert.equal(orderBook[1].length, 1, "OrderBook should have 1 sell volume has one element"); 47 | assert.equal(orderBook[1][0], 5, "OrderBook should have a volume of 5 coins someone wants to sell"); 48 | // Create a Buy Price that is less than the lowest Sell Price of the token to trigger a Buy Limit Order 49 | // instead of an immediate Market Buy Order 50 | return myExchangeInstance.buyToken("FIXED", web3.toWei(3, "finney"), 5); 51 | }).then((txResult) => { 52 | // Event Log Test 53 | assert.equal(txResult.logs.length, 1, "One Log Message should have been emitted"); 54 | assert.equal(txResult.logs[0].event, "LimitBuyOrderCreated", "Log Event should be LimitBuyOrderCreated"); 55 | return myExchangeInstance.getSellOrderBook.call("FIXED"); 56 | }).then((orderBook) => { 57 | // Order Book should still have 1 Sell Offer since no Market Buy Order was executed as lowest Sell Price was higher than Buy Offer 58 | assert.equal(orderBook[0].length, 1, "OrderBook should still have 1 sell offers"); 59 | assert.equal(orderBook[1].length, 1, "OrderBook should still have 1 sell volume has one element"); 60 | return myExchangeInstance.getBuyOrderBook.call("FIXED"); 61 | }).then((orderBook) => { 62 | // Order Book should now have 1 Buy Offer since no Market Buy Order was executed as lowest Sell Price was higher than Buy Offer 63 | assert.equal(orderBook[0].length, 1, "OrderBook should have 0 buy offers"); 64 | assert.equal(orderBook[1].length, 1, "OrderBook should have 0 buy volume elements"); 65 | }); 66 | }); 67 | }); -------------------------------------------------------------------------------- /test/05_exchange_without_add_token_throws_on_deposit_withdraw_test.js: -------------------------------------------------------------------------------- 1 | const fixedSupplyToken = artifacts.require("./FixedSupplyToken.sol"); 2 | const exchange = artifacts.require("./Exchange.sol"); 3 | 4 | contract('Exchange - without adding token to DEX an error is thrown upon deposit token \ 5 | into DEX from an account and for withdraw and token balance checks', (accounts) => { 6 | 7 | it("should throw error when account address tries to check Balance of Tokens on DEX \ 8 | that were not previously added to DEX", () => { 9 | let myExchangeInstance; 10 | let myTokenInstance; 11 | return fixedSupplyToken.deployed().then((instance) => { 12 | myTokenInstance = instance; 13 | return instance; 14 | }).then((tokenInstance) => { 15 | myTokenInstance = tokenInstance; 16 | return exchange.deployed(); 17 | }).then((exchangeInstance) => { 18 | myExchangeInstance = exchangeInstance; 19 | return myTokenInstance.approve(myExchangeInstance.address, 2000); 20 | }).then((txResult) => { 21 | return myExchangeInstance.getBalance("FIXED"); 22 | }).then((returnValue) => { 23 | assert(false, "getBalance was supposed to throw but did not"); 24 | }).catch(function(error) { 25 | let expectedError = "revert" 26 | if(error.toString().indexOf(expectedError) != -1) { 27 | console.log(`Solidity threw an expected error: ${expectedError} successfully`); 28 | } else { 29 | assert(false, `Solidity threw an unexpected error: ${error.toString()}`); 30 | } 31 | }); 32 | }); 33 | 34 | it("should throw error when account address tries to Deposit Tokens into DEX \ 35 | that were not previously added to DEX", () => { 36 | let myExchangeInstance; 37 | let myTokenInstance; 38 | return fixedSupplyToken.deployed().then((instance) => { 39 | myTokenInstance = instance; 40 | return instance; 41 | }).then((tokenInstance) => { 42 | myTokenInstance = tokenInstance; 43 | return exchange.deployed(); 44 | }).then((exchangeInstance) => { 45 | myExchangeInstance = exchangeInstance; 46 | return myTokenInstance.approve(myExchangeInstance.address, 2000); 47 | }).then((txResult) => { 48 | return myExchangeInstance.depositToken("FIXED", 2000); 49 | }).then((returnValue) => { 50 | assert(false, "depositToken was supposed to throw but did not"); 51 | }).catch(function(error) { 52 | let expectedError = "revert" 53 | if(error.toString().indexOf(expectedError) != -1) { 54 | console.log(`Solidity threw an expected error: ${expectedError} successfully`); 55 | } else { 56 | assert(false, `Solidity threw an unexpected error: ${error.toString()}`); 57 | } 58 | }); 59 | }); 60 | 61 | it("should throw an error when account address tries to Withdraw Tokens from DEX \ 62 | when not previously added to DEX", () => { 63 | let myExchangeInstance; 64 | let myTokenInstance; 65 | let balancedTokenInExchangeBeforeWithdrawal; 66 | let balanceTokenInTokenBeforeWithdrawal; 67 | let balanceTokenInExchangeAfterWithdrawal; 68 | let balanceTokenInTokenAfterWithdrawal; 69 | 70 | return fixedSupplyToken.deployed().then((instance) => { 71 | myTokenInstance = instance; 72 | return instance; 73 | }).then((tokenInstance) => { 74 | myTokenInstance = tokenInstance; 75 | return exchange.deployed(); 76 | }).then((exchangeInstance) => { 77 | myExchangeInstance = exchangeInstance; 78 | return myExchangeInstance.getBalance.call("FIXED"); 79 | }).then((returnValue) => { 80 | assert(false, "getBalance was supposed to throw but did not"); 81 | }).catch(function(error) { 82 | let expectedError = "revert"; 83 | if(error.toString().indexOf(expectedError) != -1) { 84 | console.log(`Solidity threw an expected error: ${expectedError} successfully`); 85 | } else { 86 | // if the error is something else (e.g., the assert from previous promise), then we fail the test 87 | assert(false, `Solidity threw an unexpected error: ${error.toString()}`); 88 | } 89 | }); 90 | }); 91 | }); -------------------------------------------------------------------------------- /contracts/FixedSupplyToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.18; 2 | 3 | // ---------------------------------------------------------------------------------------------- 4 | // Sample fixed supply token contract 5 | // Enjoy. (c) BokkyPooBah 2017. The MIT Licence. 6 | // ---------------------------------------------------------------------------------------------- 7 | 8 | // ERC Token Standard #20 Interface 9 | // https://github.com/ethereum/EIPs/issues/20 10 | contract ERC20Interface { 11 | // Get the total token supply 12 | function totalSupply() public constant returns (uint256); 13 | 14 | // Get the account balance of another account with address _owner 15 | function balanceOf(address _owner) public constant returns (uint256 balance); 16 | 17 | // Send _value amount of tokens to address _to 18 | function transfer(address _to, uint256 _value) public returns (bool success); 19 | 20 | // Send _value amount of tokens from address _from to address _to 21 | function transferFrom(address _from, address _to, uint256 _value) public returns (bool success); 22 | 23 | // Allow _spender to withdraw from your account, multiple times, up to the _value amount. 24 | // If this function is called again it overwrites the current allowance with _value. 25 | // this function is required for some DEX functionality 26 | function approve(address _spender, uint256 _value) public returns (bool success); 27 | 28 | // Returns the amount which _spender is still allowed to withdraw from _owner 29 | function allowance(address _owner, address _spender) public constant returns (uint256 remaining); 30 | 31 | // Triggered when tokens are transferred. 32 | event Transfer(address indexed _from, address indexed _to, uint256 _value); 33 | 34 | // Triggered whenever approve(address _spender, uint256 _value) is called. 35 | event Approval(address indexed _owner, address indexed _spender, uint256 _value); 36 | } 37 | 38 | contract FixedSupplyToken is ERC20Interface { 39 | string public constant symbol = "FIXED"; 40 | string public constant name = "Example Fixed Supply Token"; 41 | uint8 public constant decimals = 0; 42 | uint256 _totalSupply = 1000000; 43 | 44 | // Owner of this contract 45 | address public owner; 46 | 47 | // Balances for each account 48 | mapping (address => uint256) balances; 49 | 50 | // Owner of account approves the transfer of an amount to another account 51 | mapping (address => mapping (address => uint256)) allowed; 52 | 53 | // Functions with this modifier can only be executed by the owner 54 | modifier onlyOwner() { 55 | if (msg.sender != owner) { 56 | revert(); 57 | } 58 | _; 59 | } 60 | 61 | // Constructor 62 | function FixedSupplyToken() public { 63 | owner = msg.sender; 64 | balances[owner] = _totalSupply; 65 | } 66 | 67 | function totalSupply() public constant returns (uint256) { 68 | return _totalSupply; 69 | } 70 | 71 | // What is the balance of a particular account? 72 | function balanceOf(address _owner) public constant returns (uint256 balance) { 73 | return balances[_owner]; 74 | } 75 | 76 | // Transfer the balance from owner's account to another account 77 | function transfer(address _to, uint256 _amount) public returns (bool success) { 78 | if (balances[msg.sender] >= _amount 79 | && _amount > 0 80 | && balances[_to] + _amount > balances[_to]) { 81 | balances[msg.sender] -= _amount; 82 | balances[_to] += _amount; 83 | Transfer(msg.sender, _to, _amount); 84 | return true; 85 | } 86 | else { 87 | return false; 88 | } 89 | } 90 | 91 | // Send _value amount of tokens from address _from to address _to 92 | // The transferFrom method is used for a withdraw workflow, allowing contracts to send 93 | // tokens on your behalf, for example to "deposit" to a contract address and/or to charge 94 | // fees in sub-currencies; the command should fail unless the _from account has 95 | // deliberately authorized the sender of the message via some mechanism; we propose 96 | // these standardized APIs for approval: 97 | function transferFrom( 98 | address _from, 99 | address _to, 100 | uint256 _amount 101 | ) public returns (bool success) { 102 | if (balances[_from] >= _amount 103 | && allowed[_from][msg.sender] >= _amount 104 | && _amount > 0 105 | && balances[_to] + _amount > balances[_to]) { 106 | balances[_from] -= _amount; 107 | allowed[_from][msg.sender] -= _amount; 108 | balances[_to] += _amount; 109 | Transfer(_from, _to, _amount); 110 | return true; 111 | } 112 | else { 113 | return false; 114 | } 115 | } 116 | 117 | // Allow _spender to withdraw from your account, multiple times, up to the _value amount. 118 | // If this function is called again it overwrites the current allowance with _value. 119 | function approve(address _spender, uint256 _amount) public returns (bool success) { 120 | allowed[msg.sender][_spender] = _amount; 121 | Approval(msg.sender, _spender, _amount); 122 | return true; 123 | } 124 | 125 | function allowance(address _owner, address _spender) public constant returns (uint256 remaining) { 126 | return allowed[_owner][_spender]; 127 | } 128 | 129 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | * Fork and clone the repository 4 | 5 | * [Install NVM](https://github.com/creationix/nvm) 6 | 7 | * Switch to Node version specified in .nvmrc 8 | 9 | ``` 10 | nvm install 11 | ``` 12 | 13 | * Terminal Tab 1 - Install Truffle 14 | * Reference: http://truffleframework.com/ 15 | 16 | ``` 17 | npm install -g truffle 18 | ``` 19 | 20 | * Terminal Tab 2 - Install Test Framework with Ethereum TestRPC 21 | 22 | ``` 23 | npm install -g ganache-cli 24 | ``` 25 | 26 | * Terminal Tab 2 - Start Ethereum Blockchain Protocol Node Simulation 27 | 28 | ``` 29 | ganache-cli \ 30 | --port="8500" \ 31 | --mnemonic "copy obey episode awake damp vacant protect hold wish primary travel shy" \ 32 | --verbose \ 33 | --networkId=3 \ 34 | --gasLimit=7984452 \ 35 | --gasPrice=2000000000; 36 | ``` 37 | 38 | * Optionally [Install Geth](https://github.com/ethereum/go-ethereum/wiki/Building-Ethereum) and run the Testnet using Geth in the project directory: 39 | 40 | * Show installation directory of Geth and Go, and show Go path [Reference](https://github.com/ethereum/go-ethereum/wiki/Developers%27-Guide) 41 | 42 | ``` 43 | which geth 44 | which go 45 | echo $GOPATH 46 | geth version 47 | ``` 48 | 49 | * Show where Geth Chain directories are stored: 50 | 51 | ``` 52 | find ~ -type d -name 'chaindata' 53 | ``` 54 | 55 | * [Install or Upgrade existing version of Geth](https://github.com/ethereum/go-ethereum/wiki/Installation-Instructions-for-Mac) (if not installed using Homebrew) 56 | 57 | ``` 58 | brew tap ethereum/ethereum 59 | brew install ethereum 60 | brew upgrade ethereum 61 | ``` 62 | 63 | * Terminal Tab 1 - Compile and Deploy the FixedSupplyToken Contract 64 | 65 | ``` 66 | truffle migrate --network development 67 | ``` 68 | 69 | * Terminal Tab 1 - Run Sample Unit Tests on the Truffle Contract. Truffle Re-Deploys the Contracts 70 | 71 | ``` 72 | truffle test 73 | ``` 74 | 75 | # Debugging 76 | 77 | * Debug the Solidity Smart Contract in Remix IDE 78 | * Verify the Solidity Smart Contract compiles by pasting it in MIST using https://github.com/ltfschoen/geth-node 79 | * Verify the Solidity Smart Contract compiles by deploying it to Ethereum TestRPC using Truffle 80 | 81 | # TODO 82 | 83 | * [ ] - Incorporate Automated Market Maker (AMM) similar to that described in 0x Whitepaper 84 | 85 | # Initial Setup - Truffle, TestRPC, Unit Tests (FixedSupplyContract) 86 | 87 | * Install Truffle 88 | * Reference: http://truffleframework.com/ 89 | 90 | ``` 91 | npm install -g truffle 92 | ``` 93 | 94 | * Setup Truffle to Manage Contracts (i.e. MetaCoin sample), Migrations and Unit Tests 95 | 96 | ``` 97 | truffle init 98 | 99 | truffle migrate --network development 100 | ``` 101 | 102 | * Initialise with Front-End 103 | * Truffle Default 104 | * https://github.com/trufflesuite/truffle-init-default 105 | * Truffle Webpack 106 | * https://github.com/trufflesuite/truffle-init-webpack 107 | * https://github.com/truffle-box/webpack-box 108 | * Truffle React 109 | * http://truffleframework.com/boxes/ 110 | 111 | * Truffle Configuration File Examples 112 | * http://truffleframework.com/docs/advanced/configuration 113 | * Note: Use `from` to specify the From Address for Truffle Contract Deployment. 114 | * Use the Mnemonic to Restart TestRPC with the same Accounts. 115 | * Note: Use `provider` to specify a Web3 Provider 116 | * Note: Truffle Build `truffle build` script for say Webpack is usually in package.json 117 | 118 | * Truffle with Test Framework using Ethereum TestRPC (avoid delays in mining transactions with Geth Testnet) 119 | * Ethereum TestRPC - In-Memory Blockchain Simulation 120 | 121 | ``` 122 | npm install -g ganache-cli 123 | ``` 124 | 125 | * Start Ethereum Blockchain Protocol Node Simulation with 10x Accounts on http://localhost:8500 126 | * Creates 10x Private Keys and provides a Mnemonic. Assigns to each associated Address 100 Ether. 127 | * Note: Private Keys may be imported into Metamask Client 128 | * Note: Mnemonic may be used subsequently with Ethereum TestRPC to re-create the Accounts with ` 129 | 130 | ``` 131 | ganache-cli 132 | ``` 133 | 134 | * Restart TestRPC with Same Accounts (i.e. `ganache-cli --mnemonic "copy obey episode awake damp vacant protect hold wish primary travel shy"`) 135 | 136 | ``` 137 | ganache-cli --port 8500 --mnemonic 138 | ``` 139 | 140 | * Add FixedSupplyToken to Truffle Contracts folder 141 | * Remove MetaCoin from Truffle Contracts folder 142 | * Update 2nd Migration file to deploy FixedSupplyToken 143 | 144 | * Compile and Deploy the FixedSupplyToken Contract 145 | 146 | ``` 147 | truffle migrate --network development 148 | ``` 149 | 150 | * Run Sample Unit Tests on the Truffle MetaCoin Contract. Truffle Re-Deploys the MetaCoin Contracts 151 | 152 | ``` 153 | truffle test 154 | ``` 155 | 156 | # Troubleshooting 157 | 158 | * Try restarting Ganache TestRPC if you encounter error `sender doesn't have enough funds to send tx. The upfront cost is: x and the sender's account only has: y` 159 | 160 | * Fix error `Error: Error: Exceeds block gas limit` that may occur when sending Gas Limit say of `50000000` when truffle.js has `gas` property set as `gas: 4712388,`, by changing to a smaller value: `myExchangeInstance.buyToken("FIXED", web3.toWei(4, "finney"), 5, {from: accounts[0], gas: 4000000});` 161 | 162 | * Fix `Error: VM Exception while processing transaction: out of gas`. In the `buyToken` function it always occurs after a certain line of code. Simply increase the Gas Limit to the Mainnet's limit (currently shown as `7984452` at https://ethstats.net/) in both Ganache CLI Flags and in truffle.js -------------------------------------------------------------------------------- /test/04_exchange_add_deposit_withdraw_tokens_test.js: -------------------------------------------------------------------------------- 1 | const fixedSupplyToken = artifacts.require("./FixedSupplyToken.sol"); 2 | const exchange = artifacts.require("./Exchange.sol"); 3 | 4 | contract('Exchange - add token into DEX, deposit token into DEX from an account and then withdraw \ 5 | it again', (accounts) => { 6 | 7 | it("should allow DEX owner to add token to DEX and emit event", () => { 8 | let myTokenInstance; 9 | let myExchangeInstance; 10 | let tokenSymbol = "FIXED"; 11 | return fixedSupplyToken.deployed().then((instance) => { 12 | myTokenInstance = instance; 13 | return exchange.deployed(); 14 | }).then(function (exchangeInstance) { 15 | myExchangeInstance = exchangeInstance; 16 | return myExchangeInstance.addToken(tokenSymbol, myTokenInstance.address); 17 | }).then(function (txHash) { 18 | // console.log(txHash); 19 | // Event Log Test 20 | assert.equal( 21 | txHash.logs[0].event, 22 | "TokenAddedToSystem", 23 | "TokenAddedToSystem event should be emitted" 24 | ); 25 | assert.equal( 26 | txHash.logs[0].args['_token'], 27 | "FIXED", 28 | `TokenAddedToSystem event should have added ${tokenSymbol} token` 29 | ); 30 | // console.log(txResult.logs[0].args['_token']); 31 | return myExchangeInstance.hasToken.call(tokenSymbol); 32 | }).then(function (booleanHasToken) { 33 | assert.equal(booleanHasToken, true, `Token ${tokenSymbol} provided could not be added to DEX`); 34 | return myExchangeInstance.hasToken.call("SOMETHING"); 35 | }).then(function (booleanHasNotToken) { 36 | assert.equal(booleanHasNotToken, false, "Token provided was found by was never added to DEX"); 37 | }); 38 | }); 39 | 40 | it("should allow an account address to Deposit Tokens into DEX and emit event", () => { 41 | let myExchangeInstance; 42 | let myTokenInstance; 43 | return fixedSupplyToken.deployed().then((instance) => { 44 | myTokenInstance = instance; 45 | return instance; 46 | }).then((tokenInstance) => { 47 | myTokenInstance = tokenInstance; 48 | return exchange.deployed(); 49 | }).then((exchangeInstance) => { 50 | myExchangeInstance = exchangeInstance; 51 | // Grant approval to DEX to transfer up to 2000 tokens on behalf of the caller's account address 52 | return myTokenInstance.approve(myExchangeInstance.address, 2000); 53 | }).then((txResult) => { 54 | return myExchangeInstance.depositToken("FIXED", 2000); 55 | }).then((txHash) => { 56 | // Event Log Test 57 | assert.equal( 58 | txHash.logs[0].event, 59 | "DepositForTokenReceived", 60 | "DepositForTokenReceived event should be emitted" 61 | ); 62 | return myExchangeInstance.getBalance("FIXED"); 63 | }).then((balanceToken) => { 64 | assert.equal(balanceToken, 2000, "DEX should have 2000 tokens for the \ 65 | account address that is calling the DEX"); 66 | }); 67 | }); 68 | 69 | it("should allow an account address to Withdraw Tokens from DEX", () => { 70 | let myExchangeInstance; 71 | let myTokenInstance; 72 | let balancedTokenInExchangeBeforeWithdrawal; 73 | let balanceTokenInTokenBeforeWithdrawal; 74 | let balanceTokenInExchangeAfterWithdrawal; 75 | let balanceTokenInTokenAfterWithdrawal; 76 | 77 | return fixedSupplyToken.deployed().then((instance) => { 78 | myTokenInstance = instance; 79 | return instance; 80 | }).then((tokenInstance) => { 81 | myTokenInstance = tokenInstance; 82 | return exchange.deployed(); 83 | }).then((exchangeInstance) => { 84 | myExchangeInstance = exchangeInstance; 85 | return myExchangeInstance.getBalance.call("FIXED"); 86 | }).then((balanceExchange) => { 87 | balancedTokenInExchangeBeforeWithdrawal = balanceExchange.toNumber(); 88 | return myTokenInstance.balanceOf.call(accounts[0]); 89 | }).then((balanceToken) => { 90 | balanceTokenInTokenBeforeWithdrawal = balanceToken.toNumber(); 91 | return myExchangeInstance.withdrawToken("FIXED", balancedTokenInExchangeBeforeWithdrawal); 92 | }).then((txHash) => { 93 | // Event Log Test 94 | assert.equal( 95 | txHash.logs[0].event, 96 | "WithdrawalToken", 97 | "WithdrawalToken event should be emitted" 98 | ); 99 | return myExchangeInstance.getBalance.call("FIXED"); 100 | }).then((balanceExchange) => { 101 | balanceTokenInExchangeAfterWithdrawal = balanceExchange.toNumber(); 102 | return myTokenInstance.balanceOf.call(accounts[0]); 103 | }).then((balanceToken) => { 104 | balanceTokenInTokenAfterWithdrawal = balanceToken.toNumber(); 105 | assert.equal( 106 | balanceTokenInExchangeAfterWithdrawal, 107 | 0, 108 | "DEX should have 0 tokens left after the withdrawal" 109 | ); 110 | assert.equal( 111 | balanceTokenInTokenAfterWithdrawal, 112 | balancedTokenInExchangeBeforeWithdrawal + balanceTokenInTokenBeforeWithdrawal, 113 | "Token Contract should have all the tokens withdrawn from the DEX" 114 | ); 115 | }); 116 | }); 117 | }); -------------------------------------------------------------------------------- /test/06_exchange_trading_buy_sell_limit_order_test.js: -------------------------------------------------------------------------------- 1 | const fixedSupplyToken = artifacts.require("./FixedSupplyToken.sol"); 2 | const exchange = artifacts.require("./Exchange.sol"); 3 | 4 | contract('Exchange - Buy, Sell, and Cancel Limit Orders', (accounts) => { 5 | 6 | before(() => { 7 | // Setup Exchange with 3 Ether and 2000 "FIXED" Tokens so we may Buy/Sell Tokens 8 | let instanceExchange; 9 | let instanceToken; 10 | return exchange.deployed().then((instance) => { 11 | instanceExchange = instance; 12 | return instanceExchange.depositEther({from: accounts[0], value: web3.toWei(3, "ether")}); 13 | }).then((txResult) => { 14 | return fixedSupplyToken.deployed(); 15 | }).then((myTokenInstance) => { 16 | instanceToken = myTokenInstance; 17 | return instanceExchange.addToken("FIXED", instanceToken.address); 18 | }).then((txResult) => { 19 | return instanceToken.approve(instanceExchange.address, 2000); 20 | }).then((txResult) => { 21 | return instanceExchange.depositToken("FIXED", 2000); 22 | }); 23 | }); 24 | 25 | it("should be possible to Add a Buy Limit Order and get Buy Order Book", () => { 26 | let myExchangeInstance; 27 | return exchange.deployed().then((instance) => { 28 | myExchangeInstance = instance; 29 | return myExchangeInstance.getBuyOrderBook.call("FIXED"); 30 | }).then((orderBook) => { 31 | // Verify the Buy Order Book has a Buy Prices Array and Buy Volume Array 32 | assert.equal(orderBook.length, 2, "BuyOrderBook should have 2 elements"); 33 | // Verify that in Initial State the Order Book has no Buy Offers 34 | assert.equal(orderBook[0].length, 0, "OrderBook should have 0 buy offers"); 35 | // Buy Limit Order for 5x "FIXED"-Tokens @ 1 Finney each 36 | return myExchangeInstance.buyToken("FIXED", web3.toWei(1, "finney"), 5); 37 | }).then((txResult) => { 38 | // console.log(txResult); 39 | // Event Log Test 40 | assert.equal(txResult.logs.length, 1, "One Log Message should have been emitted."); 41 | assert.equal(txResult.logs[0].event, "LimitBuyOrderCreated", "Log Event should be LimitBuyOrderCreated"); 42 | // Retireve the Buy Order Book from the Exchange for the "FIXED" Token 43 | return myExchangeInstance.getBuyOrderBook.call("FIXED"); 44 | }).then((orderBook) => { 45 | // Verify that the Buy Order Book "Prices Array" has a length of 1 (i.e. [ 1 Finney ]) 46 | assert.equal(orderBook[0].length, 1, "OrderBook should have 0 buy offers"); 47 | // Verify that the Buy Order Book "Volume Array" has a length of 1 (i.e. [ 5 OFF ]) 48 | assert.equal(orderBook[1].length, 1, "OrderBook should have 0 buy volume has one element"); 49 | }); 50 | }); 51 | 52 | // Add two more Buy Limit Orders in Buy Order Book, where one order should be at the End of the Linked List, 53 | // and the other in the Middle of the Linked List 54 | it("should be possible to Add three (3) Buy Limit Orders and get Buy Order Book", () => { 55 | let myExchangeInstance; 56 | let orderBookLengthBeforeBuy; 57 | return exchange.deployed().then((exchangeInstance) => { 58 | myExchangeInstance = exchangeInstance; 59 | return myExchangeInstance.getBuyOrderBook.call("FIXED"); 60 | }).then((orderBook) => { 61 | orderBookLengthBeforeBuy = orderBook[0].length; 62 | // Buy Limit Order for 5x "FIXED"-Tokens @ 2 Finney each (End of the Linked List) 63 | return myExchangeInstance.buyToken("FIXED", web3.toWei(2, "finney"), 5); 64 | }).then((txResult) => { 65 | assert.equal(txResult.logs[0].event, "LimitBuyOrderCreated", "Log Event should be LimitBuyOrderCreated"); 66 | // Buy Limit Order for 5x "FIXED"-Tokens @ 1.4 Finney each (Middle of the Linked List) 67 | return myExchangeInstance.buyToken("FIXED", web3.toWei(1.4, "finney"), 5); 68 | }).then((txResult) => { 69 | assert.equal(txResult.logs[0].event, "LimitBuyOrderCreated", "Log Event should be LimitBuyOrderCreated"); 70 | return myExchangeInstance.getBuyOrderBook.call("FIXED"); 71 | }).then((orderBook) => { 72 | // Verify that the Order Book has changed to 3x Price Entries 73 | assert.equal(orderBook[0].length, orderBookLengthBeforeBuy + 2, "OrderBook should have one more Buy Offers"); 74 | assert.equal(orderBook[1].length, orderBookLengthBeforeBuy + 2, "OrderBook should have 2x Buy Volume elements"); 75 | }); 76 | }); 77 | 78 | it("should be possible to Add two Sell Limit Orders and get Sell Order Book", () => { 79 | var myExchangeInstance; 80 | return exchange.deployed().then((instance) => { 81 | myExchangeInstance = instance; 82 | return myExchangeInstance.getSellOrderBook.call("FIXED"); 83 | }).then((orderBook) => { 84 | return myExchangeInstance.sellToken("FIXED", web3.toWei(3, "finney"), 5); 85 | }).then((txResult) => { 86 | // console.log(txResult); 87 | // Event Log Test 88 | assert.equal(txResult.logs.length, 1, "One Log Message should be emitted."); 89 | assert.equal(txResult.logs[0].event, "LimitSellOrderCreated", "Log Event should be LimitSellOrderCreated"); 90 | return myExchangeInstance.sellToken("FIXED", web3.toWei(6, "finney"), 5); 91 | }).then((txResult) => { 92 | return myExchangeInstance.getSellOrderBook.call("FIXED"); 93 | }).then((orderBook) => { 94 | assert.equal(orderBook[0].length, 2, "OrderBook should have 2 sell offers"); 95 | assert.equal(orderBook[1].length, 2, "OrderBook should have 2 sell volume elements"); 96 | }); 97 | }); 98 | 99 | it("should be possible to Create and Cancel a Buy Limit Order", () => { 100 | let myExchangeInstance; 101 | let orderBookLengthBeforeBuy, orderBookLengthAfterBuy, orderBookLengthAfterCancel, orderKey; 102 | return exchange.deployed().then((instance) => { 103 | myExchangeInstance = instance; 104 | return myExchangeInstance.getBuyOrderBook.call("FIXED"); 105 | }).then((orderBook) => { 106 | orderBookLengthBeforeBuy = orderBook[0].length; 107 | console.log(`Order Book Length Before Buy: ${orderBookLengthBeforeBuy}`); 108 | // Buy Limit Order for 5x "FIXED"-Tokens @ 2.2 Finney each (End of the Linked List) 109 | return myExchangeInstance.buyToken("FIXED", web3.toWei(2.2, "finney"), 5); 110 | }).then((txResult) => { 111 | // Event Log Test 112 | assert.equal(txResult.logs.length, 1, "One Log Message should be emitted."); 113 | assert.equal(txResult.logs[0].event, "LimitBuyOrderCreated", "Log Event should be LimitBuyOrderCreated"); 114 | orderKey = txResult.logs[0].args._orderKey; 115 | return myExchangeInstance.getBuyOrderBook.call("FIXED"); 116 | }).then((orderBook) => { 117 | orderBookLengthAfterBuy = orderBook[0].length; 118 | console.log(`Order Book Length After Buy: ${orderBookLengthAfterBuy}`); 119 | assert.equal(orderBookLengthAfterBuy, orderBookLengthBeforeBuy + 1, "OrderBook should have 1 Buy Offer more than before"); 120 | return myExchangeInstance.cancelOrder("FIXED", false, web3.toWei(2.2, "finney"), orderKey); 121 | }).then((txResult) => { 122 | console.log(txResult); 123 | console.log(txResult.logs[0].args); 124 | assert.equal(txResult.logs[0].event, "BuyOrderCanceled", "Log Event should be BuyOrderCanceled"); 125 | return myExchangeInstance.getBuyOrderBook.call("FIXED"); 126 | }).then((orderBook) => { 127 | orderBookLengthAfterCancel = orderBook[0].length; 128 | console.log(orderBook[1][orderBookLengthAfterCancel-1]) 129 | assert.equal( 130 | orderBookLengthAfterCancel, 131 | orderBookLengthAfterBuy, 132 | "OrderBook should have 1 Buy Offers. It is setting Volume to zero instead of Cancelling it"); 133 | assert.equal(orderBook[1][orderBookLengthAfterCancel-1], 0, "Available Volume should be zero"); 134 | }); 135 | }); 136 | }); -------------------------------------------------------------------------------- /contracts/Exchange.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.18; 2 | 3 | import "./owned.sol"; 4 | import "./FixedSupplyToken.sol"; 5 | 6 | contract Exchange is owned { 7 | 8 | /////////////////////// 9 | // GENERAL STRUCTURE // 10 | /////////////////////// 11 | 12 | struct Offer { 13 | uint amountTokens; 14 | address who; 15 | } 16 | 17 | struct OrderBook { 18 | uint higherPrice; 19 | uint lowerPrice; 20 | 21 | // All Keys are Initialised by Default in Solidity 22 | mapping (uint => Offer) offers; 23 | 24 | // Store in `offers_key` where we are in the Linked List 25 | uint offers_key; 26 | 27 | // Store amount of offers that we have 28 | uint offers_length; 29 | } 30 | 31 | struct Token { 32 | address tokenContract; 33 | string symbolName; 34 | 35 | // Note: Solidity Mappings have initialised state by default 36 | // (i.e. offers_length is initially 0) 37 | mapping (uint => OrderBook) buyBook; 38 | 39 | uint curBuyPrice; 40 | uint lowestBuyPrice; 41 | uint amountBuyPrices; 42 | 43 | mapping (uint => OrderBook) sellBook; 44 | 45 | uint curSellPrice; 46 | uint highestSellPrice; 47 | uint amountSellPrices; 48 | } 49 | 50 | // Max amount of tokens supported is 255 51 | mapping (uint8 => Token) tokens; 52 | uint8 symbolNameIndex; 53 | 54 | ////////////// 55 | // BALANCES // 56 | ////////////// 57 | 58 | mapping (address => mapping (uint8 => uint)) tokenBalanceForAddress; 59 | 60 | mapping (address => uint) balanceEthForAddress; 61 | 62 | //////////// 63 | // EVENTS // 64 | //////////// 65 | 66 | // Add Token to DEX 67 | event TokenAddedToSystem(uint _symbolIndex, string _token, uint _timestamp); 68 | 69 | 70 | // Deposit / Withdrawal of Tokens 71 | event DepositForTokenReceived(address indexed _from, uint indexed _symbolIndex, uint _amountTokens, uint _timestamp); 72 | event WithdrawalToken(address indexed _to, uint indexed _symbolIndex, uint _amountTokens, uint _timestamp); 73 | 74 | // Deposit / Withdrawal of Ether 75 | event DepositForEthReceived(address indexed _from, uint _amount, uint _timestamp); 76 | event WithdrawalEth(address indexed _to, uint _amount, uint _timestamp); 77 | 78 | // Creation of Buy / Sell Limit Orders 79 | event LimitBuyOrderCreated(uint indexed _symbolIndex, address indexed _who, uint _amountTokens, uint _priceInWei, uint _orderKey); 80 | event LimitSellOrderCreated(uint indexed _symbolIndex, address indexed _who, uint _amountTokens, uint _priceInWei, uint _orderKey); 81 | 82 | // Fulfillment of Buy / Sell Order 83 | event BuyOrderFulfilled(uint indexed _symbolIndex, uint _amountTokens, uint _priceInWei, uint _orderKey); 84 | event SellOrderFulfilled(uint indexed _symbolIndex, uint _amountTokens, uint _priceInWei, uint _orderKey); 85 | 86 | // Cancellation of Buy / Sell Order 87 | event BuyOrderCanceled(uint indexed _symbolIndex, uint _priceInWei, uint _orderKey); 88 | event SellOrderCanceled(uint indexed _symbolIndex, uint _priceInWei, uint _orderKey); 89 | 90 | // Only for use in Testnet 91 | event Debug(uint _test1, uint _test2, uint _test3, uint _test4); 92 | 93 | //////////////////////////////// 94 | // DEPOSIT / WITHDRAWAL ETHER // 95 | //////////////////////////////// 96 | 97 | function depositEther() public payable { 98 | // Overflow check since `uint` (i.e. uint 256) in mapping for 99 | // tokenBalanceForAddress has upper limit and undesirably 100 | // restarts from zero again if we overflow the limit 101 | require(balanceEthForAddress[msg.sender] + msg.value >= balanceEthForAddress[msg.sender]); 102 | balanceEthForAddress[msg.sender] += msg.value; 103 | DepositForEthReceived(msg.sender, msg.value, now); 104 | } 105 | 106 | function withdrawEther(uint amountInWei) public { 107 | // Balance sufficient to withdraw check 108 | require(balanceEthForAddress[msg.sender] - amountInWei >= 0); 109 | // Overflow check 110 | require(balanceEthForAddress[msg.sender] - amountInWei <= balanceEthForAddress[msg.sender]); 111 | // Deduct from balance and transfer the withdrawal amount 112 | balanceEthForAddress[msg.sender] -= amountInWei; 113 | msg.sender.transfer(amountInWei); 114 | WithdrawalEth(msg.sender, amountInWei, now); 115 | } 116 | 117 | function getEthBalanceInWei() public constant returns (uint) { 118 | // Get balance in Wei of calling address 119 | return balanceEthForAddress[msg.sender]; 120 | } 121 | 122 | ////////////////////// 123 | // TOKEN MANAGEMENT // 124 | ////////////////////// 125 | 126 | function addToken(string symbolName, address erc20TokenAddress) public onlyowner { 127 | // Modifier checks if caller is Admin "owner" of the Contract otherwise return early 128 | 129 | // Use `hasToken` to check if given Token Symbol Name 130 | // with ERC-20 Token Address is already in the Exchange 131 | require(!hasToken(symbolName)); 132 | 133 | // If given Token is not already in the Exchange then: 134 | // - Increment the `symbolNameIndex` by 1 135 | // - Assign to a Mapping a new entry with the new `symbolNameIndex`, 136 | // and the given Token Symbol Name and ERC-20 Token Address 137 | // Note: Throw an exception upon failure 138 | symbolNameIndex++; 139 | tokens[symbolNameIndex].symbolName = symbolName; 140 | tokens[symbolNameIndex].tokenContract = erc20TokenAddress; 141 | TokenAddedToSystem(symbolNameIndex, symbolName, now); 142 | } 143 | 144 | function hasToken(string symbolName) public constant returns (bool) { 145 | uint8 index = getSymbolIndex(symbolName); 146 | if (index == 0) { 147 | return false; 148 | } 149 | return true; 150 | } 151 | 152 | function getSymbolIndex(string symbolName) internal returns (uint8) { 153 | for (uint8 i = 1; i <= symbolNameIndex; i++) { 154 | if (stringsEqual(tokens[i].symbolName, symbolName)) { 155 | return i; 156 | } 157 | } 158 | return 0; 159 | } 160 | 161 | function getSymbolIndexOrThrow(string symbolName) returns (uint8) { 162 | uint8 index = getSymbolIndex(symbolName); 163 | require(index > 0); 164 | return index; 165 | } 166 | 167 | //////////////////////////////// 168 | // STRING COMPARISON FUNCTION // 169 | //////////////////////////////// 170 | 171 | // TODO - Try using sha3 to compare strings since it should use less gas than the loop comparing bytes 172 | function stringsEqual(string storage _a, string memory _b) internal returns (bool) { 173 | // Note: 174 | // - `storage` is default for Local Variables 175 | // - `storage` is what variables are forced to be if they are State Variables 176 | // - `memory` is the default for Function Parameters (and Function Return Parameters) 177 | // 178 | // Since first Function Parameter is a Local Variable (from the Mapping defined in the Class) 179 | // it is assigned as a `storage` Variable, whereas since the second Function Parameter is 180 | // a direct Function Argument it is assigned as a `memory` Variable 181 | bytes storage a = bytes(_a); 182 | bytes memory b = bytes(_b); 183 | 184 | // // Compare two strings quickly by length to try to avoid detailed loop comparison 185 | // // - Transaction cost (with 5x characters): ~24k gas 186 | // // - Execution cost upon early exit here: ~1.8k gas 187 | // if (a.length != b.length) 188 | // return false; 189 | 190 | // // Compare two strings in detail Bit-by-Bit 191 | // // - Transaction cost (with 5x characters): ~29.5k gas 192 | // // - Execution cost (with 5x characters): ~7.5k gas 193 | // for (uint i = 0; i < a.length; i++) 194 | // if (a[i] != b[i]) 195 | // return false; 196 | 197 | // // Byte values of string are the same 198 | // return true; 199 | 200 | // Compare two strings using SHA3, which is supposedly more Gas Efficient 201 | // - Transaction cost (with 5x characters): ~24k gas 202 | // - Execution cost upon early exit here: ~2.4k gas 203 | if (sha3(a) != sha3(b)) { return false; } 204 | return true; 205 | } 206 | 207 | //////////////////////////////// 208 | // DEPOSIT / WITHDRAWAL TOKEN // 209 | //////////////////////////////// 210 | 211 | function depositToken(string symbolName, uint amountTokens) public { 212 | uint8 symbolNameIndex = getSymbolIndexOrThrow(symbolName); 213 | // Check the Token Contract Address is initialised and not an uninitialised address(0) aka "0x0" 214 | require(tokens[symbolNameIndex].tokenContract != address(0)); 215 | 216 | ERC20Interface token = ERC20Interface(tokens[symbolNameIndex].tokenContract); 217 | 218 | // Transfer an amount to this DEX from the calling address 219 | require(token.transferFrom(msg.sender, address(this), amountTokens) == true); 220 | // Overflow check 221 | require(tokenBalanceForAddress[msg.sender][symbolNameIndex] + amountTokens >= tokenBalanceForAddress[msg.sender][symbolNameIndex]); 222 | // Credit the DEX token balance for the callinging address with the transferred amount 223 | tokenBalanceForAddress[msg.sender][symbolNameIndex] += amountTokens; 224 | DepositForTokenReceived(msg.sender, symbolNameIndex, amountTokens, now); 225 | } 226 | 227 | function withdrawToken(string symbolName, uint amountTokens) public { 228 | uint8 symbolNameIndex = getSymbolIndexOrThrow(symbolName); 229 | require(tokens[symbolNameIndex].tokenContract != address(0)); 230 | 231 | ERC20Interface token = ERC20Interface(tokens[symbolNameIndex].tokenContract); 232 | 233 | // Check sufficient balance to withdraw requested amount 234 | require(tokenBalanceForAddress[msg.sender][symbolNameIndex] - amountTokens >= 0); 235 | // Overflow check to ensure future balance less than or equal to the current balance after deducting the withdrawn amount 236 | require(tokenBalanceForAddress[msg.sender][symbolNameIndex] - amountTokens <= tokenBalanceForAddress[msg.sender][symbolNameIndex]); 237 | // Deduct amount requested to be withdrawing from the DEX Token Balance 238 | tokenBalanceForAddress[msg.sender][symbolNameIndex] -= amountTokens; 239 | // Check that the `transfer` function of the Token Contract returns true 240 | require(token.transfer(msg.sender, amountTokens) == true); 241 | WithdrawalToken(msg.sender, symbolNameIndex, amountTokens, now); 242 | } 243 | 244 | function getBalance(string symbolName) public constant returns (uint) { 245 | uint8 symbolNameIndex = getSymbolIndexOrThrow(symbolName); 246 | return tokenBalanceForAddress[msg.sender][symbolNameIndex]; 247 | } 248 | 249 | /////////////////////////////////// 250 | // ORDER BOOK - BID ORDERS // 251 | /////////////////////////////////// 252 | 253 | // Returns Buy Prices Array and Buy Volume Array for each of the Prices 254 | function getBuyOrderBook(string symbolName) public constant returns (uint[], uint[]) { 255 | uint8 tokenNameIndex = getSymbolIndexOrThrow(symbolName); 256 | // Initialise New Memory Arrays with the Exact Amount of Buy Prices in the Buy Order Book (not a Dynamic Array) 257 | uint[] memory arrPricesBuy = new uint[](tokens[tokenNameIndex].amountBuyPrices); 258 | uint[] memory arrVolumesBuy = new uint[](tokens[tokenNameIndex].amountBuyPrices); 259 | 260 | // Example: 261 | // - Assume 3x Buy Offers (1x 100 Wei, 1x 200 Wei, 1x 300 Wei) 262 | // - Start Counter at 0 with Lowest Buy Offer (i.e. 100 Wei) 263 | // - `whilePrice` becomes 100 Wei 264 | // - Add Price `whilePrice` of 100 Wei to Buy Prices Array for Counter 0 265 | // - Obtain Volume at 100 Wei by Summing all Offers for 100 Wei in Buy Order Book 266 | // - Add Volume at 100 Wei to Buy Prices Array for Counter 0 267 | // - Either 268 | // - Assign Next Buy Offer (i.e. 200 Wei) to `whilePrice`. 269 | // Else Break if we have reached Last Element (Higher Price of `whilePrice` points to `whilePrice`) 270 | // - Increment Counter to 1 (Next Index in Prices Array and Volume Array) 271 | // - Repeat 272 | // - Break when Higher Price of 300 Wei is also 300 Wei 273 | // - Return Buy Prices Array and Buy Volumes Array 274 | // 275 | // So if Buy Offers are: 50 Tokens @ 100 Wei, 70 Tokens @ 200 Wei, and 30 Tokens @ 300 Wei, then have: 276 | // - 3x Entries in Buy Prices Array (i.e. [ 100, 200, 300 ]) 277 | // - 3x Entries in Buy Volumes Array (i.e. [ 50, 70, 30 ] ) 278 | 279 | // Loop through Prices. Adding each to the Prices Array and Volume Array. 280 | // Starting from Lowest Buy Price until reach Current Buy Price (Highest Bid Price) 281 | uint whilePrice = tokens[tokenNameIndex].lowestBuyPrice; 282 | uint counter = 0; 283 | // Check Exists at Least One Order Book Entry 284 | if (tokens[tokenNameIndex].curBuyPrice > 0) { 285 | while (whilePrice <= tokens[tokenNameIndex].curBuyPrice) { 286 | arrPricesBuy[counter] = whilePrice; 287 | uint buyVolumeAtPrice = 0; 288 | uint buyOffersKey = 0; 289 | 290 | // Obtain the Volume from Summing all Offers Mapped to a Single Price inside the Buy Order Book 291 | buyOffersKey = tokens[tokenNameIndex].buyBook[whilePrice].offers_key; 292 | while (buyOffersKey <= tokens[tokenNameIndex].buyBook[whilePrice].offers_length) { 293 | buyVolumeAtPrice += tokens[tokenNameIndex].buyBook[whilePrice].offers[buyOffersKey].amountTokens; 294 | buyOffersKey++; 295 | } 296 | arrVolumesBuy[counter] = buyVolumeAtPrice; 297 | // Next whilePrice 298 | if (whilePrice == tokens[tokenNameIndex].buyBook[whilePrice].higherPrice) { 299 | break; 300 | } 301 | else { 302 | whilePrice = tokens[tokenNameIndex].buyBook[whilePrice].higherPrice; 303 | } 304 | counter++; 305 | } 306 | } 307 | return (arrPricesBuy, arrVolumesBuy); 308 | } 309 | 310 | /////////////////////////////////// 311 | // ORDER BOOK - ASK ORDERS // 312 | /////////////////////////////////// 313 | 314 | function getSellOrderBook(string symbolName) public constant returns (uint[], uint[]) { 315 | uint8 tokenNameIndex = getSymbolIndexOrThrow(symbolName); 316 | uint[] memory arrPricesSell = new uint[](tokens[tokenNameIndex].amountSellPrices); 317 | uint[] memory arrVolumesSell = new uint[](tokens[tokenNameIndex].amountSellPrices); 318 | uint sellWhilePrice = tokens[tokenNameIndex].curSellPrice; 319 | uint sellCounter = 0; 320 | if (tokens[tokenNameIndex].curSellPrice > 0) { 321 | while (sellWhilePrice <= tokens[tokenNameIndex].highestSellPrice) { 322 | arrPricesSell[sellCounter] = sellWhilePrice; 323 | uint sellVolumeAtPrice = 0; 324 | uint sellOffersKey = 0; 325 | sellOffersKey = tokens[tokenNameIndex].sellBook[sellWhilePrice].offers_key; 326 | while (sellOffersKey <= tokens[tokenNameIndex].sellBook[sellWhilePrice].offers_length) { 327 | sellVolumeAtPrice += tokens[tokenNameIndex].sellBook[sellWhilePrice].offers[sellOffersKey].amountTokens; 328 | sellOffersKey++; 329 | } 330 | arrVolumesSell[sellCounter] = sellVolumeAtPrice; 331 | if (tokens[tokenNameIndex].sellBook[sellWhilePrice].higherPrice == 0) { 332 | break; 333 | } 334 | else { 335 | sellWhilePrice = tokens[tokenNameIndex].sellBook[sellWhilePrice].higherPrice; 336 | } 337 | sellCounter++; 338 | } 339 | } 340 | return (arrPricesSell, arrVolumesSell); 341 | } 342 | 343 | ///////////////////////////////// 344 | // NEW ORDER - BID ORDER // 345 | ///////////////////////////////// 346 | 347 | // Market Buy Order Function 348 | // User wants to Buy X-Coins @ Y-Price per coin 349 | function buyToken(string symbolName, uint priceInWei, uint amount) public { 350 | // Obtain Symbol Index for given Symbol Name 351 | uint8 tokenNameIndex = getSymbolIndexOrThrow(symbolName); 352 | uint totalAmountOfEtherNecessary = 0; 353 | uint amountOfTokensNecessary = amount; 354 | 355 | if (tokens[tokenNameIndex].amountSellPrices == 0 || tokens[tokenNameIndex].curSellPrice > priceInWei) { 356 | createBuyLimitOrderForTokensUnableToMatchWithSellOrderForBuyer(symbolName, tokenNameIndex, priceInWei, amountOfTokensNecessary, totalAmountOfEtherNecessary); 357 | } else { 358 | // Execute Market Buy Order Immediately if: 359 | // - Existing Sell Limit Order exists that is less than or equal to the Buy Price Offered by the function caller 360 | 361 | uint totalAmountOfEtherAvailable = 0; 362 | // 09 Unit Test: `whilePrice` initially 4 finney (i.e. 4000000000000000 Wei) 363 | uint whilePrice = tokens[tokenNameIndex].curSellPrice; 364 | // `offers_key` declaration initialises it to 0 365 | uint offers_key; 366 | 367 | // 09 Unit Test: `priceInWei` initially 4 finney (i.e. 4000000000000000 Wei) 368 | while (whilePrice <= priceInWei && amountOfTokensNecessary > 0) { 369 | // 09 Unit Test: `offers_key` assigned to 1 370 | offers_key = tokens[tokenNameIndex].sellBook[whilePrice].offers_key; 371 | // 09 Unit Test: `tokens[tokenNameIndex].sellBook[whilePrice].offers_length` is 1 372 | // i.e. .sellBook[4000000000000000] 373 | // 09 Unit Test: `amountOfTokensNecessary` is 5 374 | while (offers_key <= tokens[tokenNameIndex].sellBook[whilePrice].offers_length && amountOfTokensNecessary > 0) { 375 | // 09 Unit Test: `volumeAtPriceFromAddress` assigned to 5 376 | uint volumeAtPriceFromAddress = tokens[tokenNameIndex].sellBook[whilePrice].offers[offers_key].amountTokens; 377 | 378 | // 5 <= 5 379 | if (volumeAtPriceFromAddress <= amountOfTokensNecessary) { 380 | // 20000000000000000 = 5 * 4000000000000000 381 | totalAmountOfEtherAvailable = volumeAtPriceFromAddress * whilePrice; 382 | 383 | // Overflow Check 384 | // 09 Unit Test: `balanceEthForAddress[msg.sender]` is initially 3000000000000000000 (3 ETH) 385 | require(balanceEthForAddress[msg.sender] >= totalAmountOfEtherAvailable); 386 | require(balanceEthForAddress[msg.sender] - totalAmountOfEtherAvailable <= balanceEthForAddress[msg.sender]); 387 | 388 | // Decrease the Buyer's Account Balance of tokens by the amount the Sell Offer Order Entry is willing to accept in exchange for ETH 389 | balanceEthForAddress[msg.sender] -= totalAmountOfEtherAvailable; 390 | 391 | // Overflow Checks 392 | require(balanceEthForAddress[msg.sender] >= totalAmountOfEtherAvailable); 393 | require(uint(1) > uint(0)); 394 | require(tokenBalanceForAddress[msg.sender][tokenNameIndex] + volumeAtPriceFromAddress >= tokenBalanceForAddress[msg.sender][tokenNameIndex]); 395 | require(balanceEthForAddress[tokens[tokenNameIndex].sellBook[whilePrice].offers[offers_key].who] + totalAmountOfEtherAvailable >= balanceEthForAddress[tokens[tokenNameIndex].sellBook[whilePrice].offers[offers_key].who]); 396 | 397 | // Increase the Buyer's Account Balance of tokens by the amount the Sell Offer Entry is willing to accept in exchange for the ETH 398 | tokenBalanceForAddress[msg.sender][tokenNameIndex] += volumeAtPriceFromAddress; 399 | // Reset the amount of ETH offered by the Current Sell Order Entry to zero 0 400 | tokens[tokenNameIndex].sellBook[whilePrice].offers[offers_key].amountTokens = 0; 401 | // Increase the Seller's Account Balance of ETH with all the ETH offered by the Current Buy Offer (in exchange for Seller's tokens) 402 | balanceEthForAddress[tokens[tokenNameIndex].sellBook[whilePrice].offers[offers_key].who] += totalAmountOfEtherAvailable; 403 | tokens[tokenNameIndex].sellBook[whilePrice].offers_key++; 404 | 405 | BuyOrderFulfilled(tokenNameIndex, volumeAtPriceFromAddress, whilePrice, offers_key); 406 | 407 | amountOfTokensNecessary -= volumeAtPriceFromAddress; 408 | } else { 409 | require(tokens[tokenNameIndex].sellBook[whilePrice].offers[offers_key].amountTokens > amountOfTokensNecessary); 410 | 411 | totalAmountOfEtherNecessary = amountOfTokensNecessary * whilePrice; 412 | 413 | // Overflow Check 414 | require(balanceEthForAddress[msg.sender] - totalAmountOfEtherNecessary <= balanceEthForAddress[msg.sender]); 415 | 416 | balanceEthForAddress[msg.sender] -= totalAmountOfEtherNecessary; 417 | 418 | // Overflow Check 419 | require(balanceEthForAddress[tokens[tokenNameIndex].sellBook[whilePrice].offers[offers_key].who] + totalAmountOfEtherNecessary >= balanceEthForAddress[tokens[tokenNameIndex].sellBook[whilePrice].offers[offers_key].who]); 420 | 421 | tokens[tokenNameIndex].sellBook[whilePrice].offers[offers_key].amountTokens -= amountOfTokensNecessary; 422 | balanceEthForAddress[tokens[tokenNameIndex].sellBook[whilePrice].offers[offers_key].who] += totalAmountOfEtherNecessary; 423 | tokenBalanceForAddress[msg.sender][tokenNameIndex] += amountOfTokensNecessary; 424 | amountOfTokensNecessary = 0; 425 | 426 | BuyOrderFulfilled(tokenNameIndex, amountOfTokensNecessary, whilePrice, offers_key); 427 | } 428 | 429 | if ( 430 | offers_key == tokens[tokenNameIndex].sellBook[whilePrice].offers_length && 431 | tokens[tokenNameIndex].sellBook[whilePrice].offers[offers_key].amountTokens == 0 432 | ) { 433 | tokens[tokenNameIndex].amountSellPrices--; 434 | if (whilePrice == tokens[tokenNameIndex].sellBook[whilePrice].higherPrice || tokens[tokenNameIndex].sellBook[whilePrice].higherPrice == 0) { 435 | tokens[tokenNameIndex].curSellPrice = 0; 436 | } else { 437 | tokens[tokenNameIndex].curSellPrice = tokens[tokenNameIndex].sellBook[whilePrice].higherPrice; 438 | tokens[tokenNameIndex].sellBook[tokens[tokenNameIndex].sellBook[whilePrice].higherPrice].lowerPrice = 0; 439 | } 440 | } 441 | offers_key++; 442 | } 443 | whilePrice = tokens[tokenNameIndex].curSellPrice; 444 | } 445 | 446 | if (amountOfTokensNecessary > 0) { 447 | createBuyLimitOrderForTokensUnableToMatchWithSellOrderForBuyer(symbolName, tokenNameIndex, priceInWei, amountOfTokensNecessary, totalAmountOfEtherNecessary); 448 | } 449 | } 450 | } 451 | 452 | function createBuyLimitOrderForTokensUnableToMatchWithSellOrderForBuyer( 453 | string symbolName, uint8 tokenNameIndex, uint priceInWei, uint amountOfTokensNecessary, uint totalAmountOfEtherNecessary 454 | ) internal { 455 | // Calculate Ether Balance necessary to Buy the Token Symbol Name. 456 | totalAmountOfEtherNecessary = amountOfTokensNecessary * priceInWei; 457 | 458 | // Overflow Checks 459 | require(totalAmountOfEtherNecessary >= amountOfTokensNecessary); 460 | require(totalAmountOfEtherNecessary >= priceInWei); 461 | require(balanceEthForAddress[msg.sender] >= totalAmountOfEtherNecessary); 462 | require(balanceEthForAddress[msg.sender] - totalAmountOfEtherNecessary >= 0); 463 | require(balanceEthForAddress[msg.sender] - totalAmountOfEtherNecessary <= balanceEthForAddress[msg.sender]); 464 | 465 | // Deduct from Exchange Balance the Ether amount necessary the Buy Limit Order. 466 | balanceEthForAddress[msg.sender] -= totalAmountOfEtherNecessary; 467 | 468 | // Create New Limit Order in the Order Book if either: 469 | // - No Sell Orders already exist that match the Buy Price Price Offered by the function caller 470 | // - Existing Sell Price is greater than the Buy Price Offered by the function caller 471 | 472 | // Add Buy Limit Order to Order Book 473 | addBuyOffer(tokenNameIndex, priceInWei, amountOfTokensNecessary, msg.sender); 474 | 475 | // Emit Event. 476 | LimitBuyOrderCreated(tokenNameIndex, msg.sender, amountOfTokensNecessary, priceInWei, tokens[tokenNameIndex].buyBook[priceInWei].offers_length); 477 | } 478 | 479 | /////////////////////////// 480 | // BID LIMIT ORDER LOGIC // 481 | /////////////////////////// 482 | 483 | function addBuyOffer(uint8 tokenIndex, uint priceInWei, uint amount, address who) internal { 484 | // Offers Length in the Buy Order Book for the Buy Limit Offer Price Entry is increased 485 | tokens[tokenIndex].buyBook[priceInWei].offers_length++; 486 | 487 | // Add Buy Offer to Buy Order Book under the Price Offered Entry for a Token Symbol 488 | tokens[tokenIndex].buyBook[priceInWei].offers[tokens[tokenIndex].buyBook[priceInWei].offers_length] = Offer(amount, who); 489 | 490 | // Update Linked List if the Price Offered Entry does not already exist in the Order Book 491 | // - Next Price Entry - Update Lower Price value 492 | // - Previous Price Entry - Update Higher Price value 493 | // 494 | // Note: If it is the First Offer at `priceInWei` in the Buy Order Book 495 | // then must inspect Buy Order Book to determine where to Insert the First Offer in the Linked List 496 | if (tokens[tokenIndex].buyBook[priceInWei].offers_length == 1) { 497 | tokens[tokenIndex].buyBook[priceInWei].offers_key = 1; 498 | // New Buy Order Received. Increment Counter. Set later with getOrderBook array 499 | tokens[tokenIndex].amountBuyPrices++; 500 | 501 | // Set Lower Buy Price and Higher Buy Price for the Token Symbol 502 | uint curBuyPrice = tokens[tokenIndex].curBuyPrice; 503 | uint lowestBuyPrice = tokens[tokenIndex].lowestBuyPrice; 504 | 505 | // Case 1 & 2: New Buy Offer is the First Order Entered or Lowest Entry 506 | if (lowestBuyPrice == 0 || lowestBuyPrice > priceInWei) { 507 | // Case 1: First Entry. No Orders Exist `lowestBuyPrice == 0`. Insert New (First) Order. Linked List with Single Entry 508 | if (curBuyPrice == 0) { 509 | // Set Current Buy Price to Buy Price of New (First) Order 510 | tokens[tokenIndex].curBuyPrice = priceInWei; 511 | // Set Buy Order Book Higher Price to Buy Price of New (First) Order 512 | tokens[tokenIndex].buyBook[priceInWei].higherPrice = priceInWei; 513 | // Set Buy Order Book Lower Price to 0 514 | tokens[tokenIndex].buyBook[priceInWei].lowerPrice = 0; 515 | // Case 2: New Buy Offer is the Lowest Entry (Less Than Lowest Existing Buy Price) `lowestBuyPrice > priceInWei` 516 | } else { 517 | // Set Buy Order Book Lowest Price to New Order Price (Lowest Entry in Linked List) 518 | tokens[tokenIndex].buyBook[lowestBuyPrice].lowerPrice = priceInWei; 519 | // Adjust Higher and Lower Prices of Linked List relative to New Lowest Entry in Linked List 520 | tokens[tokenIndex].buyBook[priceInWei].higherPrice = lowestBuyPrice; 521 | tokens[tokenIndex].buyBook[priceInWei].lowerPrice = 0; 522 | } 523 | tokens[tokenIndex].lowestBuyPrice = priceInWei; 524 | } 525 | // Case 3: New Buy Offer is the Highest Buy Price (Last Entry). Not Need Find Right Entry Location 526 | else if (curBuyPrice < priceInWei) { 527 | tokens[tokenIndex].buyBook[curBuyPrice].higherPrice = priceInWei; 528 | tokens[tokenIndex].buyBook[priceInWei].higherPrice = priceInWei; 529 | tokens[tokenIndex].buyBook[priceInWei].lowerPrice = curBuyPrice; 530 | tokens[tokenIndex].curBuyPrice = priceInWei; 531 | } 532 | // Case 4: New Buy Offer is between Existing Lowest and Highest Buy Prices. Find Location to Insert Depending on Gas Limit 533 | else { 534 | // Start Loop with Existing Highest Buy Price 535 | uint buyPrice = tokens[tokenIndex].curBuyPrice; 536 | bool weFoundLocation = false; 537 | // Loop Until Find 538 | while (buyPrice > 0 && !weFoundLocation) { 539 | if ( 540 | buyPrice < priceInWei && 541 | tokens[tokenIndex].buyBook[buyPrice].higherPrice > priceInWei 542 | ) { 543 | // Set New Order Book Entry Higher and Lower Prices of Linked List 544 | tokens[tokenIndex].buyBook[priceInWei].lowerPrice = buyPrice; 545 | tokens[tokenIndex].buyBook[priceInWei].higherPrice = tokens[tokenIndex].buyBook[buyPrice].higherPrice; 546 | // Set Order Book's Higher Price Entry's Lower Price to the New Offer Current Price 547 | tokens[tokenIndex].buyBook[tokens[tokenIndex].buyBook[buyPrice].higherPrice].lowerPrice = priceInWei; 548 | // Set Order Books's Lower Price Entry's Higher Price to the New Offer Current Price 549 | tokens[tokenIndex].buyBook[buyPrice].higherPrice = priceInWei; 550 | // Found Location to Insert New Entry where: 551 | // - Higher Buy Prices > Offer Buy Price, and 552 | // - Offer Buy Price > Entry Price 553 | weFoundLocation = true; 554 | } 555 | // Set Highest Buy Price to the Order Book's Highest Buy Price's Lower Entry Price on Each Iteration 556 | buyPrice = tokens[tokenIndex].buyBook[buyPrice].lowerPrice; 557 | } 558 | } 559 | } 560 | } 561 | 562 | ///////////////////////////////// 563 | // NEW ORDER - ASK ORDER // 564 | ///////////////////////////////// 565 | 566 | // Market Sell Order Function 567 | // User wants to Sell X-Coins @ Y-Price per coin 568 | function sellToken(string symbolName, uint priceInWei, uint amount) public payable { 569 | // Obtain Symbol Index for given Symbol Name 570 | uint8 tokenNameIndex = getSymbolIndexOrThrow(symbolName); 571 | uint totalAmountOfEtherNecessary = 0; 572 | uint totalAmountOfEtherAvailable = 0; 573 | // Given `amount` Volume of tokens to find necessary to fulfill the current Sell Order 574 | uint amountOfTokensNecessary = amount; 575 | 576 | if (tokens[tokenNameIndex].amountBuyPrices == 0 || tokens[tokenNameIndex].curBuyPrice < priceInWei) { 577 | createSellLimitOrderForTokensUnableToMatchWithBuyOrderForSeller(symbolName, tokenNameIndex, priceInWei, amountOfTokensNecessary, totalAmountOfEtherNecessary); 578 | } else { 579 | // Execute Market Sell Order Immediately if: 580 | // - Existing Buy Limit Order exists that is greater than the Sell Price Offered by the function caller 581 | 582 | // Start with the Highest Buy Price (since Seller wants to exchange their tokens with the highest bidder) 583 | uint whilePrice = tokens[tokenNameIndex].curBuyPrice; 584 | uint offers_key; 585 | // Iterate through the Buy Book (Buy Offers Mapping) to Find "Highest" Buy Offer Prices 586 | // (assign to Current Buy Price `whilePrice` each iteration) that are Higher than the Sell Offer 587 | // and whilst the Volume to find is not yet fulfilled. 588 | // Note: Since we are in the Sell Order `sellOrder` function we use the Buy Book 589 | while (whilePrice >= priceInWei && amountOfTokensNecessary > 0) { 590 | offers_key = tokens[tokenNameIndex].buyBook[whilePrice].offers_key; 591 | // Inner While - Iterate Buy Book (Buy Offers Mapping) Entries for the Current Buy Price using FIFO 592 | while (offers_key <= tokens[tokenNameIndex].buyBook[whilePrice].offers_length && amountOfTokensNecessary > 0) { 593 | uint volumeAtPriceFromAddress = tokens[tokenNameIndex].buyBook[whilePrice].offers[offers_key].amountTokens; 594 | // Case when Current Buy Order Entry Volume Only Partially fulfills the Sell Order Volume 595 | // (i.e. Sell Order wants to sell more than Current Buy Order Entry requires) 596 | // then we achieve Partial exchange from Sell Order to the Buy Order Entry and then 597 | // move to Next Address with a Buy Order Entry at the Current Buy Price for the symbolName 598 | // i.e. Sell Order amount is for 1000 tokens but Current Buy Order is for 500 tokens at Current Buy Price 599 | if (volumeAtPriceFromAddress <= amountOfTokensNecessary) { 600 | // Amount of Ether available to be exchanged in the Current Buy Book Offers Entry at the Current Buy Price 601 | totalAmountOfEtherAvailable = volumeAtPriceFromAddress * whilePrice; 602 | 603 | // Overflow Check 604 | require(tokenBalanceForAddress[msg.sender][tokenNameIndex] >= volumeAtPriceFromAddress); 605 | 606 | // Decrease the Seller's Account Balance of tokens by the amount the Buy Offer Order Entry is willing to accept in exchange for ETH 607 | tokenBalanceForAddress[msg.sender][tokenNameIndex] -= volumeAtPriceFromAddress; 608 | 609 | // Overflow Checks 610 | // - Assuming the Seller sells a proportion of their `symbolName` tokens in their Sell Offer 611 | // to the Current Buy Order Entry that is requesting `volumeAtPriceFromAddress` then we need to first 612 | // check that the Sellers account has sufficient Volumne of those tokens to execute the trade 613 | require(tokenBalanceForAddress[msg.sender][tokenNameIndex] - volumeAtPriceFromAddress >= 0); 614 | // - Check that fulfilling the Current Buy Order Entry by adding the amount of tokens sold by the Sell Offer does not overflow 615 | require(tokenBalanceForAddress[tokens[tokenNameIndex].buyBook[whilePrice].offers[offers_key].who][tokenNameIndex] + volumeAtPriceFromAddress >= tokenBalanceForAddress[tokens[tokenNameIndex].buyBook[whilePrice].offers[offers_key].who][tokenNameIndex]); 616 | // - Check that fulfilling the Current Buy Order Entry increases the Seller's ETH balance without overflowing 617 | require(balanceEthForAddress[msg.sender] + totalAmountOfEtherAvailable >= balanceEthForAddress[msg.sender]); 618 | 619 | // Increase the Buyer's Account Balance of tokens (for the matching Buy Order Entry) with the proportion tokens required from the Sell Order 620 | // (given that the Buy Offer originator is offering less or equal to the volume of the Sell Offer) 621 | tokenBalanceForAddress[tokens[tokenNameIndex].buyBook[whilePrice].offers[offers_key].who][tokenNameIndex] += volumeAtPriceFromAddress; 622 | // Reset the amount of ETH offered by the Current Buy Order Entry to zero 0 623 | tokens[tokenNameIndex].buyBook[whilePrice].offers[offers_key].amountTokens = 0; 624 | // Increase the Seller's Account Balance of ETH with all the ETH offered by the Current Buy Order Entry (in exchange for the Seller's token offering) 625 | balanceEthForAddress[msg.sender] += totalAmountOfEtherAvailable; 626 | // Move up one element in the Buy Book Offers Mapping (i.e. to the Next Buy Offer at the Current Buy Order Price) 627 | tokens[tokenNameIndex].buyBook[whilePrice].offers_key++; 628 | 629 | // Emit Event 630 | SellOrderFulfilled(tokenNameIndex, volumeAtPriceFromAddress, whilePrice, offers_key); 631 | 632 | // Decrease the amount necessary to be sold from the Seller's Offer by the amount of of tokens just exchanged for ETH with the Buyer at the Current Buy Order Price 633 | amountOfTokensNecessary -= volumeAtPriceFromAddress; 634 | 635 | // Case when Sell Order Volume Only Partially fulfills the Current Buy Order Entry Volume 636 | // (i.e. Sell Order wants to sell more than the Current Buy Order Entry needs) 637 | // then we achieve Partial exchange from Sell Order to the Buy Order Entry and then exit 638 | // i.e. Sell Order amount is for 500 tokens and Current Buy Order is for 1000 tokens at Current Buy Price 639 | } else { 640 | // Check that the equivalent value in tokens of the Buy Offer Order Entry is actually more than Sell Offer Volume 641 | require(volumeAtPriceFromAddress - amountOfTokensNecessary > 0); 642 | 643 | // Calculate amount in ETH necessary to buy the Seller's tokens based on the Current Buy Price 644 | totalAmountOfEtherNecessary = amountOfTokensNecessary * whilePrice; 645 | 646 | // Overflow Check 647 | require(tokenBalanceForAddress[msg.sender][tokenNameIndex] >= amountOfTokensNecessary); 648 | 649 | // Decrease the Seller's Account Balance of tokens by amount they are offering since the Buy Offer Order Entry is willing to accept it all in exchange for ETH 650 | tokenBalanceForAddress[msg.sender][tokenNameIndex] -= amountOfTokensNecessary; 651 | 652 | // Overflow Check 653 | require(tokenBalanceForAddress[msg.sender][tokenNameIndex] >= amountOfTokensNecessary); 654 | require(balanceEthForAddress[msg.sender] + totalAmountOfEtherNecessary >= balanceEthForAddress[msg.sender]); 655 | require(tokenBalanceForAddress[tokens[tokenNameIndex].buyBook[whilePrice].offers[offers_key].who][tokenNameIndex] + amountOfTokensNecessary >= tokenBalanceForAddress[tokens[tokenNameIndex].buyBook[whilePrice].offers[offers_key].who][tokenNameIndex]); 656 | 657 | // Decrease the Buy Offer Order Entry amount by the full amount necessary to be sold by the Sell Offer 658 | tokens[tokenNameIndex].buyBook[whilePrice].offers[offers_key].amountTokens -= amountOfTokensNecessary; 659 | // Increase the Seller's Account Balance of ETH with the equivalent ETH amount corresponding to that offered by the Current Buy Order Entry (in exchange for the Seller's token offering) 660 | balanceEthForAddress[msg.sender] += totalAmountOfEtherNecessary; 661 | // Increase the Buyer's Account Balance of tokens (for the matching Buy Order Entry) with all the tokens sold by the Sell Order 662 | tokenBalanceForAddress[tokens[tokenNameIndex].buyBook[whilePrice].offers[offers_key].who][tokenNameIndex] += amountOfTokensNecessary; 663 | 664 | // Emit Event 665 | SellOrderFulfilled(tokenNameIndex, amountOfTokensNecessary, whilePrice, offers_key); 666 | 667 | // Set the remaining amount necessary to be sold by the Sell Order to zero 0 since we have fulfilled the Sell Offer 668 | amountOfTokensNecessary = 0; 669 | } 670 | 671 | // Case when the Current Buy Offer is the last element in the list for the Current Buy Order Offer Price 672 | // and when we have exhausted exchanging the Sell Order's amount with Offers at the Current Buy Offer Price 673 | // then Move to the Next Highest Buy Order Offer Price in the Buy Book 674 | if ( 675 | offers_key == tokens[tokenNameIndex].buyBook[whilePrice].offers_length && 676 | tokens[tokenNameIndex].buyBook[whilePrice].offers[offers_key].amountTokens == 0 677 | ) { 678 | // Decrease the quantity of Buy Order Prices since we used up the entire volume of all the Buy Offers at that price 679 | tokens[tokenNameIndex].amountBuyPrices--; 680 | if (whilePrice == tokens[tokenNameIndex].buyBook[whilePrice].lowerPrice || tokens[tokenNameIndex].buyBook[whilePrice].lowerPrice == 0) { 681 | // Case when no more Buy Book Offers to iterate through for the Current Buy Price (Last element of Linked List) 682 | // then set Current Buy Price to zero 0 683 | tokens[tokenNameIndex].curBuyPrice = 0; 684 | } else { 685 | // REFERENCE "A" 686 | // Case when not yet fulfilled `amountOfTokensNecessary` Volume of Sell Offer then 687 | // set Proposed Current Buy Price to the Next Lower Buy Price in the Linked List 688 | // so we move to the Next Lowest Entry in the Buy Book Offers Linked List 689 | tokens[tokenNameIndex].curBuyPrice = tokens[tokenNameIndex].buyBook[whilePrice].lowerPrice; 690 | // Set the Higher Price of the Next Lowest Entry that we moved to, to the Current Buy Order Offer Price 691 | tokens[tokenNameIndex].buyBook[tokens[tokenNameIndex].buyBook[whilePrice].lowerPrice].higherPrice = tokens[tokenNameIndex].curBuyPrice; 692 | } 693 | } 694 | offers_key++; 695 | } 696 | // After Finishing an Iteration of an Entry in the Buy Book Offers (until exhausted all Buy Book Offers for the previous Current Buy Price) 697 | // and setting the Proposed Current Buy Price to the Next Lowest Buy Price in REFERENCE "A". 698 | // Move to the Next Lowest Buy Price to be Iterated over by setting the Current Buy Price `whilePrice` 699 | whilePrice = tokens[tokenNameIndex].curBuyPrice; 700 | } 701 | 702 | // Case when unable to find a suitable Buy Order Offer to perform an exchange with the Seller's tokens 703 | if (amountOfTokensNecessary > 0) { 704 | // Add a Sell Limit Order to the Sell Book since could not find a Market Order to exchange Seller's tokens immediately 705 | 706 | createSellLimitOrderForTokensUnableToMatchWithBuyOrderForSeller(symbolName, tokenNameIndex, priceInWei, amountOfTokensNecessary, totalAmountOfEtherNecessary); 707 | } 708 | } 709 | } 710 | 711 | function createSellLimitOrderForTokensUnableToMatchWithBuyOrderForSeller( 712 | string symbolName, uint8 tokenNameIndex, uint priceInWei, uint amountOfTokensNecessary, uint totalAmountOfEtherNecessary 713 | ) internal { 714 | // Calculate Ether Balance necessary on the Buy-side to Sell all tokens of Token Symbol Name. 715 | totalAmountOfEtherNecessary = amountOfTokensNecessary * priceInWei; 716 | 717 | // Overflow Check 718 | require(totalAmountOfEtherNecessary >= amountOfTokensNecessary); 719 | require(totalAmountOfEtherNecessary >= priceInWei); 720 | require(tokenBalanceForAddress[msg.sender][tokenNameIndex] >= amountOfTokensNecessary); 721 | require(tokenBalanceForAddress[msg.sender][tokenNameIndex] - amountOfTokensNecessary >= 0); 722 | require(balanceEthForAddress[msg.sender] + totalAmountOfEtherNecessary >= balanceEthForAddress[msg.sender]); 723 | 724 | // Deduct from Exchange Balance the Token amount for the Sell Limit Order 725 | tokenBalanceForAddress[msg.sender][tokenNameIndex] -= amountOfTokensNecessary; 726 | 727 | // Create New Sell Limit Order in the Sell Order Book if either: 728 | // - No Buy Orders already exist that match the Sell Price Price Offered by the function caller 729 | // - Existing Buy Price is less than the Sell Price Offered by the function caller 730 | 731 | // Add Sell Limit Order to Order Book 732 | addSellOffer(tokenNameIndex, priceInWei, amountOfTokensNecessary, msg.sender); 733 | 734 | // Emit Event 735 | LimitSellOrderCreated(tokenNameIndex, msg.sender, amountOfTokensNecessary, priceInWei, tokens[tokenNameIndex].sellBook[priceInWei].offers_length); 736 | } 737 | 738 | /////////////////////////// 739 | // ASK LIMIT ORDER LOGIC // 740 | /////////////////////////// 741 | 742 | function addSellOffer(uint8 tokenIndex, uint priceInWei, uint amount, address who) internal { 743 | // Offers Length in the Sell Order Book for the Sell Limit Offer Price Entry is increased 744 | tokens[tokenIndex].sellBook[priceInWei].offers_length++; 745 | 746 | // Add Sell Offer to Sell Order Book under the Price Offered Entry for a Token Symbol 747 | tokens[tokenIndex].sellBook[priceInWei].offers[tokens[tokenIndex].sellBook[priceInWei].offers_length] = Offer(amount, who); 748 | 749 | if (tokens[tokenIndex].sellBook[priceInWei].offers_length == 1) { 750 | tokens[tokenIndex].sellBook[priceInWei].offers_key = 1; 751 | tokens[tokenIndex].amountSellPrices++; 752 | 753 | uint curSellPrice = tokens[tokenIndex].curSellPrice; 754 | uint highestSellPrice = tokens[tokenIndex].highestSellPrice; 755 | 756 | // Case 1 & 2: New Sell Offer is the First Order Entered or Highest Entry 757 | if (highestSellPrice == 0 || highestSellPrice < priceInWei) { 758 | // Case 1: First Entry. No Sell Orders Exist `highestSellPrice == 0`. Insert New (First) Order 759 | if (curSellPrice == 0) { 760 | tokens[tokenIndex].curSellPrice = priceInWei; 761 | tokens[tokenIndex].sellBook[priceInWei].higherPrice = 0; 762 | tokens[tokenIndex].sellBook[priceInWei].lowerPrice = 0; 763 | // Case 2: New Sell Offer is the Highest Entry (Higher Than Highest Existing Sell Price) `highestSellPrice < priceInWei` 764 | } else { 765 | tokens[tokenIndex].sellBook[highestSellPrice].higherPrice = priceInWei; 766 | tokens[tokenIndex].sellBook[priceInWei].lowerPrice = highestSellPrice; 767 | tokens[tokenIndex].sellBook[priceInWei].higherPrice = 0; 768 | } 769 | tokens[tokenIndex].highestSellPrice = priceInWei; 770 | } 771 | // Case 3: New Sell Offer is the Lowest Sell Price (First Entry). Not Need Find Right Entry Location 772 | else if (curSellPrice > priceInWei) { 773 | tokens[tokenIndex].sellBook[curSellPrice].lowerPrice = priceInWei; 774 | tokens[tokenIndex].sellBook[priceInWei].higherPrice = curSellPrice; 775 | tokens[tokenIndex].sellBook[priceInWei].lowerPrice = 0; 776 | tokens[tokenIndex].curSellPrice = priceInWei; 777 | } 778 | // Case 4: New Sell Offer is between Existing Lowest and Highest Sell Prices. Find Location to Insert Depending on Gas Limit 779 | else { 780 | // Start Loop with Existing Lowest Sell Price 781 | uint sellPrice = tokens[tokenIndex].curSellPrice; 782 | bool weFoundLocation = false; 783 | // Loop Until Find 784 | while (sellPrice > 0 && !weFoundLocation) { 785 | if ( 786 | sellPrice < priceInWei && 787 | tokens[tokenIndex].sellBook[sellPrice].higherPrice > priceInWei 788 | ) { 789 | // Set New Order Book Entry Higher and Lower Prices of Linked List 790 | tokens[tokenIndex].sellBook[priceInWei].lowerPrice = sellPrice; 791 | tokens[tokenIndex].sellBook[priceInWei].higherPrice = tokens[tokenIndex].sellBook[sellPrice].higherPrice; 792 | // Set Order Book's Higher Price Entry's Lower Price to the New Offer Current Price 793 | tokens[tokenIndex].sellBook[tokens[tokenIndex].sellBook[sellPrice].higherPrice].lowerPrice = priceInWei; 794 | // Set Order Books's Lower Price Entry's Higher Price to the New Offer Current Price 795 | tokens[tokenIndex].sellBook[sellPrice].higherPrice = priceInWei; 796 | // Found Location to Insert New Entry where: 797 | // - Lower Sell Prices < Offer Sell Price, and 798 | // - Offer Sell Price < Entry Price 799 | weFoundLocation = true; 800 | } 801 | // Set Lowest Sell Price to the Order Book's Lowest Buy Price's Higher Entry Price on Each Iteration 802 | sellPrice = tokens[tokenIndex].sellBook[sellPrice].higherPrice; 803 | } 804 | } 805 | } 806 | } 807 | 808 | //////////////////////////////// 809 | // CANCEL ORDER - LIMIT ORDER // 810 | //////////////////////////////// 811 | 812 | function cancelOrder(string symbolName, bool isSellOrder, uint priceInWei, uint offerKey) public { 813 | // Retrieve Token Symbol Name Index 814 | uint8 symbolNameIndex = getSymbolIndexOrThrow(symbolName); 815 | 816 | // Case 1: Cancel Sell Limit Order 817 | if (isSellOrder) { 818 | // Verify that Caller Address of Cancel Order Function matches Original Address that Created Sell Limit Order 819 | // Note: `offerKey` obtained in front-end logic from Event Emitted at Creation of Sell Limit Order 820 | require(tokens[symbolNameIndex].sellBook[priceInWei].offers[offerKey].who == msg.sender); 821 | // Obtain Tokens Amount that were to be sold in the Sell Limit Order 822 | uint tokensAmount = tokens[symbolNameIndex].sellBook[priceInWei].offers[offerKey].amountTokens; 823 | // Overflow Check 824 | require(tokenBalanceForAddress[msg.sender][symbolNameIndex] + tokensAmount >= tokenBalanceForAddress[msg.sender][symbolNameIndex]); 825 | // Refund Tokens back to Balance 826 | tokenBalanceForAddress[msg.sender][symbolNameIndex] += tokensAmount; 827 | tokens[symbolNameIndex].sellBook[priceInWei].offers[offerKey].amountTokens = 0; 828 | SellOrderCanceled(symbolNameIndex, priceInWei, offerKey); 829 | 830 | } 831 | // Case 2: Cancel Buy Limit Order 832 | else { 833 | require(tokens[symbolNameIndex].buyBook[priceInWei].offers[offerKey].who == msg.sender); 834 | uint etherToRefund = tokens[symbolNameIndex].buyBook[priceInWei].offers[offerKey].amountTokens * priceInWei; 835 | // Overflow Check 836 | require(balanceEthForAddress[msg.sender] + etherToRefund >= balanceEthForAddress[msg.sender]); 837 | // Refund Ether back to Balance 838 | balanceEthForAddress[msg.sender] += etherToRefund; 839 | tokens[symbolNameIndex].buyBook[priceInWei].offers[offerKey].amountTokens = 0; 840 | BuyOrderCanceled(symbolNameIndex, priceInWei, offerKey); 841 | } 842 | } 843 | } --------------------------------------------------------------------------------