Created
September 20, 2019 17:39
-
-
Save amisolution/5732c7b13696a086faf4ffa40450f4dc to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
pragma solidity ^0.4.24; | |
// Amis Dex OnChainOrderBook follows ERC20 Standards | |
contract ERC20 { | |
function totalSupply() constant returns (uint); | |
function balanceOf(address _owner) constant returns (uint balance); | |
function transfer(address _to, uint _value) returns (bool success); | |
function transferFrom(address _from, address _to, uint _value) returns (bool success); | |
function approve(address _spender, uint _value) returns (bool success); | |
function allowance(address _owner, address _spender) constant returns (uint remaining); | |
event Transfer(address indexed _from, address indexed _to, uint _value); | |
event Approval(address indexed _owner, address indexed _spender, uint _value); | |
} | |
// Amis Dex on-chain order book matching engine Version 0.1.3. | |
// https://github.com/amisolution/blob/master/ERC20-AMIS/contracts/OnChainOrderBookV013b.sol | |
// This smart contract is a variation of a neat ERC20 token as base, ETC as quoted, | |
// and standard fees with incentivized reward token. | |
// This contract allows minPriceExponent, baseMinInitialSize, and baseMinRemainingSize | |
// to be set at init() time appropriately for the token decimals and likely value. | |
// | |
contract OnChainOrderBookV013b { | |
enum BookType { | |
ERC20EthV1 | |
} | |
enum Direction { | |
Invalid, | |
Buy, | |
Sell | |
} | |
enum Status { | |
Unknown, | |
Rejected, | |
Open, | |
Done, | |
NeedsGas, | |
Sending, // not used by contract - web UI only | |
FailedSend, // not used by contract - web UI only | |
FailedTxn // not used by contract - web UI only | |
} | |
enum ReasonCode { | |
None, | |
InvalidPrice, | |
InvalidSize, | |
InvalidTerms, | |
InsufficientFunds, | |
WouldTake, | |
Unmatched, | |
TooManyMatches, | |
ClientCancel | |
} | |
enum Terms { | |
GTCNoGasTopup, | |
GTCWithGasTopup, | |
ImmediateOrCancel, | |
MakerOnly | |
} | |
struct Order { | |
// these are immutable once placed: | |
address client; | |
uint16 price; // packed representation of side + price | |
uint sizeBase; | |
Terms terms; | |
// these are mutable until Done or Rejected: | |
Status status; | |
ReasonCode reasonCode; | |
uint128 executedBase; // gross amount executed in base currency (before fee deduction) | |
uint128 executedCntr; // gross amount executed in quoted currency (before fee deduction) | |
uint128 feesBaseOrCntr; // base for buy, cntr for sell | |
uint128 feesRwrd; | |
} | |
struct OrderChain { | |
uint128 firstOrderId; | |
uint128 lastOrderId; | |
} | |
struct OrderChainNode { | |
uint128 nextOrderId; | |
uint128 prevOrderId; | |
} | |
// Rebuild the expected state of the contract given: | |
// - ClientPaymentEvent log history | |
// - ClientOrderEvent log history | |
// - Calling getOrder for the other immutable order fields of orders referenced by ClientOrderEvent | |
enum ClientPaymentEventType { | |
Deposit, | |
Withdraw, | |
TransferFrom, | |
Transfer | |
} | |
enum BalanceType { | |
Base, | |
Cntr, | |
Rwrd | |
} | |
event ClientPaymentEvent( | |
address indexed client, | |
ClientPaymentEventType clientPaymentEventType, | |
BalanceType balanceType, | |
int clientBalanceDelta | |
); | |
enum ClientOrderEventType { | |
Create, | |
Continue, | |
Cancel | |
} | |
event ClientOrderEvent( | |
address indexed client, | |
ClientOrderEventType clientOrderEventType, | |
uint128 orderId, | |
uint maxMatches | |
); | |
enum MarketOrderEventType { | |
// orderCount++, depth += depthBase | |
Add, | |
// orderCount--, depth -= depthBase | |
Remove, | |
// orderCount--, depth -= depthBase, traded += tradeBase | |
// (depth change and traded change differ when tiny remaining amount refunded) | |
CompleteFill, | |
// orderCount unchanged, depth -= depthBase, traded += tradeBase | |
PartialFill | |
} | |
// Technically not needed but these events can be used to maintain an order book or | |
// watch for fills. Note that the orderId and price are those of the maker. | |
event MarketOrderEvent( | |
uint256 indexed eventTimestamp, | |
uint128 indexed orderId, | |
MarketOrderEventType marketOrderEventType, | |
uint16 price, | |
uint depthBase, | |
uint tradeBase | |
); | |
// the base token (e.g. AMIS) | |
ERC20 baseToken; | |
// minimum order size (inclusive) | |
uint baseMinInitialSize; // set at init | |
// if following partial match, the remaining gets smaller than this, remove from Order Book and refund: | |
// generally we make this 10% of baseMinInitialSize | |
uint baseMinRemainingSize; // set at init | |
// maximum order size (exclusive) | |
// chosen so that even multiplied by the max price (or divided by the min price), | |
// and then multiplied by ethRwrdRate, it still fits in 2^127, allowing us to save | |
// some gas by storing executed + fee fields as uint128. | |
// even with 18 decimals, this still allows order sizes up to 1,000,000,000. | |
// if we encounter a token with e.g. 36 decimals we'll have to revisit ... | |
uint constant baseMaxSize = 10 ** 30; | |
// the counter currency or ETH traded pair | |
// (no address because it is ETH) | |
// avoid the book getting cluttered up with tiny amounts not worth the gas | |
uint constant cntrMinInitialSize = 10 finney; | |
// see comments for baseMaxSize | |
uint constant cntrMaxSize = 10 ** 30; | |
// the reward token that can be used to pay fees (WETC) | |
ERC20 rwrdToken; // set at init | |
// used to convert ETH amount to reward tokens when paying fee with reward tokens | |
uint constant ethRwrdRate = 100; | |
// funds that belong to clients (base, counter, and reward) | |
mapping (address => uint) balanceBaseForClient; | |
mapping (address => uint) balanceCntrForClient; | |
mapping (address => uint) balanceRwrdForClient; | |
// fee charged on liquidity taken, expressed as a divisor | |
// (e.g. 2000 means 1/2000, or 0.05%) | |
uint constant feeDivisor = 200; | |
// fees charged are given to: | |
address feeCollector; // set at init | |
// all orders ever created | |
mapping (uint128 => Order) orderForOrderId; | |
// Effectively a compact mapping from price to whether there are any open orders at that price. | |
// See "Price Calculation Constants" below as to why 85. | |
uint256[85] occupiedPriceBitmaps; | |
// These allow us to walk over the orders in the book at a given price level (and add more). | |
mapping (uint16 => OrderChain) orderChainForOccupiedPrice; | |
mapping (uint128 => OrderChainNode) orderChainNodeForOpenOrderId; | |
// These allow a client to (reasonably) efficiently find their own orders | |
// without relying on events (which even indexed are a bit expensive to search | |
// and cannot be accessed from smart contracts). See walkOrders. | |
mapping (address => uint128) mostRecentOrderIdForClient; | |
mapping (uint128 => uint128) clientPreviousOrderIdBeforeOrderId; | |
// Price Calculation Constants. | |
// | |
// We pack direction and price into a crafty decimal floating point representation | |
// for efficient indexing by price, the main thing we lose by doing so is precision - | |
// we only have 3 significant figures in our prices. | |
// | |
// An unpacked price consists of: | |
// | |
// direction - invalid / buy / sell | |
// mantissa - ranges from 100 to 999 representing 0.100 to 0.999 | |
// exponent - ranges from minimumPriceExponent to minimumPriceExponent + 11 | |
// (e.g. -5 to +6 for a typical pair where minPriceExponent = -5) | |
// | |
// The packed representation has 21601 different price values: | |
// | |
// 0 = invalid (can be used as marker value) | |
// 1 = buy at maximum price (0.999 * 10 ** 6) | |
// ... = other buy prices in descending order | |
// 5400 = buy at 1.00 | |
// ... = other buy prices in descending order | |
// 10800 = buy at minimum price (0.100 * 10 ** -5) | |
// 10801 = sell at minimum price (0.100 * 10 ** -5) | |
// ... = other sell prices in descending order | |
// 16201 = sell at 1.00 | |
// ... = other sell prices in descending order | |
// 21600 = sell at maximum price (0.999 * 10 ** 6) | |
// 21601+ = do not use | |
// | |
// If we want to map each packed price to a boolean value (which we do), | |
// we require 85 256-bit words. Or 42.5 for each side of the book. | |
int8 minPriceExponent; // set at init | |
uint constant invalidPrice = 0; | |
// careful: max = largest unpacked value, not largest packed value | |
uint constant maxBuyPrice = 1; | |
uint constant minBuyPrice = 10800; | |
uint constant minSellPrice = 10801; | |
uint constant maxSellPrice = 21600; | |
// Constructor. | |
// | |
// Sets feeCollector to the creator. Creator needs to call init() to finish setup. | |
// | |
function OnChainOrderBookV013b() { | |
address creator = msg.sender; | |
feeCollector = creator; | |
} | |
// "Public" Management - set address of base and reward tokens. | |
// | |
// Can only be done once (normally immediately after creation) by the fee collector. | |
// | |
// Used instead of a constructor to make deployment easier. | |
// | |
// baseMinInitialSize is the minimum order size in token-wei; | |
// the minimum resting size will be one tenth of that. | |
// | |
// minPriceExponent controls the range of prices supported by the contract; | |
// the range will be 0.100*10**minPriceExponent to 0.999*10**(minPriceExponent + 11) | |
// but careful; this is in token-wei : wei, ignoring the number of decimals of the token | |
// e.g. -5 implies 1 token-wei worth between 0.100e-5 to 0.999e+6 wei | |
// which implies same token:eth exchange rate if token decimals are 18 like eth, | |
// but if token decimals are 8, that would imply 1 token worth 10 wei to 0.000999 ETH. | |
// | |
function init(ERC20 _baseToken, ERC20 _rwrdToken, uint _baseMinInitialSize, int8 _minPriceExponent) public { | |
require(msg.sender == feeCollector); | |
require(address(baseToken) == 0); | |
require(address(_baseToken) != 0); | |
require(address(rwrdToken) == 0); | |
require(address(_rwrdToken) != 0); | |
require(_baseMinInitialSize >= 10); | |
require(_baseMinInitialSize < baseMaxSize / 1000000); | |
require(_minPriceExponent >= -20 && _minPriceExponent <= 20); | |
if (_minPriceExponent < 2) { | |
require(_baseMinInitialSize >= 10 ** uint(3-int(minPriceExponent))); | |
} | |
baseMinInitialSize = _baseMinInitialSize; | |
// dust prevention. truncation ok, know >= 10 | |
baseMinRemainingSize = _baseMinInitialSize / 10; | |
minPriceExponent = _minPriceExponent; | |
// attempt to catch bad tokens: | |
require(_baseToken.totalSupply() > 0); | |
baseToken = _baseToken; | |
require(_rwrdToken.totalSupply() > 0); | |
rwrdToken = _rwrdToken; | |
} | |
// "Public" Management - change fee collector | |
// | |
// The new fee collector only gets fees charged after this point. | |
// | |
function changeFeeCollector(address newFeeCollector) public { | |
address oldFeeCollector = feeCollector; | |
require(msg.sender == oldFeeCollector); | |
require(newFeeCollector != oldFeeCollector); | |
feeCollector = newFeeCollector; | |
} | |
// Public Info View - what is being traded here, what are the limits? | |
// | |
function getBookInfo() public constant returns ( | |
BookType _bookType, address _baseToken, address _rwrdToken, | |
uint _baseMinInitialSize, uint _cntrMinInitialSize, int8 _minPriceExponent, | |
uint _feeDivisor, address _feeCollector | |
) { | |
return ( | |
BookType.ERC20EthV1, | |
address(baseToken), | |
address(rwrdToken), | |
baseMinInitialSize, // can assume min resting size is one tenth of this | |
cntrMinInitialSize, | |
minPriceExponent, | |
feeDivisor, | |
feeCollector | |
); | |
} | |
// Public Funds View - get balances held by contract on behalf of the client, | |
// or balances approved for deposit but not yet claimed by the contract. | |
// | |
// Excludes funds in open orders. | |
// | |
// Helps a web ui get a consistent snapshot of balances. | |
// | |
// It would be nice to return the off-exchange ETH balance too but there's a | |
// bizarre bug in geth (and apparently as a result via MetaMask) that leads | |
// to unpredictable behaviour when looking up client balances in constant | |
// functions - see e.g. https://github.com/ethereum/solidity/issues/2325 . | |
// | |
function getClientBalances(address client) public constant returns ( | |
uint bookBalanceBase, | |
uint bookBalanceCntr, | |
uint bookBalanceRwrd, | |
uint approvedBalanceBase, | |
uint approvedBalanceRwrd, | |
uint ownBalanceBase, | |
uint ownBalanceRwrd | |
) { | |
bookBalanceBase = balanceBaseForClient[client]; | |
bookBalanceCntr = balanceCntrForClient[client]; | |
bookBalanceRwrd = balanceRwrdForClient[client]; | |
approvedBalanceBase = baseToken.allowance(client, address(this)); | |
approvedBalanceRwrd = rwrdToken.allowance(client, address(this)); | |
ownBalanceBase = baseToken.balanceOf(client); | |
ownBalanceRwrd = rwrdToken.balanceOf(client); | |
} | |
// Public Funds moves - deposit previously-approved base tokens. | |
// | |
function transferFromBase() public { | |
address client = msg.sender; | |
address book = address(this); | |
// we trust the ERC20 token contract not to do nasty things like call back into us - | |
// if we cannot trust the token then why are we allowing it to be traded? | |
uint amountBase = baseToken.allowance(client, book); | |
require(amountBase > 0); | |
// NB: needs change for older ERC20 tokens that don't return bool | |
require(baseToken.transferFrom(client, book, amountBase)); | |
// belt and braces | |
assert(baseToken.allowance(client, book) == 0); | |
balanceBaseForClient[client] += amountBase; | |
ClientPaymentEvent(client, ClientPaymentEventType.TransferFrom, BalanceType.Base, int(amountBase)); | |
} | |
// Public Funds moves - withdraw base tokens (as a transfer). | |
// | |
function transferBase(uint amountBase) public { | |
address client = msg.sender; | |
require(amountBase > 0); | |
require(amountBase <= balanceBaseForClient[client]); | |
// overflow safe since we checked less than balance above | |
balanceBaseForClient[client] -= amountBase; | |
// we trust the ERC20 token contract not to do nasty things like call back into us - | |
require(baseToken.transfer(client, amountBase)); | |
ClientPaymentEvent(client, ClientPaymentEventType.Transfer, BalanceType.Base, -int(amountBase)); | |
} | |
// Public Funds moves - deposit counter quoted currency (ETH or DAI). | |
// | |
function depositCntr() public payable { | |
address client = msg.sender; | |
uint amountCntr = msg.value; | |
require(amountCntr > 0); | |
// overflow safe - if someone owns pow(2,255) ETH we have bigger problems | |
balanceCntrForClient[client] += amountCntr; | |
ClientPaymentEvent(client, ClientPaymentEventType.Deposit, BalanceType.Cntr, int(amountCntr)); | |
} | |
// Public Funds Move - withdraw counter quoted currency (ETH or DAI). | |
// | |
function withdrawCntr(uint amountCntr) public { | |
address client = msg.sender; | |
require(amountCntr > 0); | |
require(amountCntr <= balanceCntrForClient[client]); | |
// overflow safe - checked less than balance above | |
balanceCntrForClient[client] -= amountCntr; | |
// safe - not enough gas to do anything interesting in fallback, already adjusted balance | |
client.transfer(amountCntr); | |
ClientPaymentEvent(client, ClientPaymentEventType.Withdraw, BalanceType.Cntr, -int(amountCntr)); | |
} | |
// Public Funds Move - deposit previously-approved incentivized reward tokens. | |
// | |
function transferFromRwrd() public { | |
address client = msg.sender; | |
address book = address(this); | |
uint amountRwrd = rwrdToken.allowance(client, book); | |
require(amountRwrd > 0); | |
// we created the incentivized reward token so we know it supports ERC20 properly and is not evil | |
require(rwrdToken.transferFrom(client, book, amountRwrd)); | |
// belt and braces | |
assert(rwrdToken.allowance(client, book) == 0); | |
balanceRwrdForClient[client] += amountRwrd; | |
ClientPaymentEvent(client, ClientPaymentEventType.TransferFrom, BalanceType.Rwrd, int(amountRwrd)); | |
} | |
// Public Funds Manipulation - withdraw base tokens (as a transfer). | |
// | |
function transferRwrd(uint amountRwrd) public { | |
address client = msg.sender; | |
require(amountRwrd > 0); | |
require(amountRwrd <= balanceRwrdForClient[client]); | |
// overflow safe - checked less than balance above | |
balanceRwrdForClient[client] -= amountRwrd; | |
// we created the incentivized reward token so we know it supports ERC20 properly and is not evil | |
require(rwrdToken.transfer(client, amountRwrd)); | |
ClientPaymentEvent(client, ClientPaymentEventType.Transfer, BalanceType.Rwrd, -int(amountRwrd)); | |
} | |
// Public Order View - get full details of an order. | |
// | |
// If the orderId does not exist, status will be Unknown. | |
// | |
function getOrder(uint128 orderId) public constant returns ( | |
address client, uint16 price, uint sizeBase, Terms terms, | |
Status status, ReasonCode reasonCode, uint executedBase, uint executedCntr, | |
uint feesBaseOrCntr, uint feesRwrd) { | |
Order storage order = orderForOrderId[orderId]; | |
return (order.client, order.price, order.sizeBase, order.terms, | |
order.status, order.reasonCode, order.executedBase, order.executedCntr, | |
order.feesBaseOrCntr, order.feesRwrd); | |
} | |
// Public Order View - get mutable details of an order. | |
// | |
// If the orderId does not exist, status will be Unknown. | |
// | |
function getOrderState(uint128 orderId) public constant returns ( | |
Status status, ReasonCode reasonCode, uint executedBase, uint executedCntr, | |
uint feesBaseOrCntr, uint feesRwrd) { | |
Order storage order = orderForOrderId[orderId]; | |
return (order.status, order.reasonCode, order.executedBase, order.executedCntr, | |
order.feesBaseOrCntr, order.feesRwrd); | |
} | |
// Public Order View - enumerate all recent orders + all open orders for one client. | |
// | |
// Not really designed for use from a smart contract transaction. | |
// Baseline concept: | |
// - client ensures order ids are generated so that most-signficant part is time-based; | |
// - client decides they want all orders after a certain point-in-time, | |
// and chooses minClosedOrderIdCutoff accordingly; | |
// - before that point-in-time they just get open and needs gas orders | |
// - client calls walkClientOrders with maybeLastOrderIdReturned = 0 initially; | |
// - then repeats with the orderId returned by walkClientOrders; | |
// - (and stops if it returns a zero orderId); | |
// | |
// Note that client is only used when maybeLastOrderIdReturned = 0. | |
// | |
function walkClientOrders( | |
address client, uint128 maybeLastOrderIdReturned, uint128 minClosedOrderIdCutoff | |
) public constant returns ( | |
uint128 orderId, uint16 price, uint sizeBase, Terms terms, | |
Status status, ReasonCode reasonCode, uint executedBase, uint executedCntr, | |
uint feesBaseOrCntr, uint feesRwrd) { | |
if (maybeLastOrderIdReturned == 0) { | |
orderId = mostRecentOrderIdForClient[client]; | |
} else { | |
orderId = clientPreviousOrderIdBeforeOrderId[maybeLastOrderIdReturned]; | |
} | |
while (true) { | |
if (orderId == 0) return; | |
Order storage order = orderForOrderId[orderId]; | |
if (orderId >= minClosedOrderIdCutoff) break; | |
if (order.status == Status.Open || order.status == Status.NeedsGas) break; | |
orderId = clientPreviousOrderIdBeforeOrderId[orderId]; | |
} | |
return (orderId, order.price, order.sizeBase, order.terms, | |
order.status, order.reasonCode, order.executedBase, order.executedCntr, | |
order.feesBaseOrCntr, order.feesRwrd); | |
} | |
// Internal Price Calculation - turn packed price into a friendlier unpacked price. | |
// | |
function unpackPrice(uint16 price) internal constant returns ( | |
Direction direction, uint16 mantissa, int8 exponent | |
) { | |
uint sidedPriceIndex = uint(price); | |
uint priceIndex; | |
if (sidedPriceIndex < 1 || sidedPriceIndex > maxSellPrice) { | |
direction = Direction.Invalid; | |
mantissa = 0; | |
exponent = 0; | |
return; | |
} else if (sidedPriceIndex <= minBuyPrice) { | |
direction = Direction.Buy; | |
priceIndex = minBuyPrice - sidedPriceIndex; | |
} else { | |
direction = Direction.Sell; | |
priceIndex = sidedPriceIndex - minSellPrice; | |
} | |
uint zeroBasedMantissa = priceIndex % 900; | |
uint zeroBasedExponent = priceIndex / 900; | |
mantissa = uint16(zeroBasedMantissa + 100); | |
exponent = int8(zeroBasedExponent) + minPriceExponent; | |
return; | |
} | |
// Internal Price Calculation - is a packed price on the buy side? | |
// | |
// Throws an error if price is invalid. | |
// | |
function isBuyPrice(uint16 price) internal constant returns (bool isBuy) { | |
// yes, this looks odd, but max here is highest _unpacked_ price | |
return price >= maxBuyPrice && price <= minBuyPrice; | |
} | |
// Internal Price Calculation - turn a packed buy price into a packed sell price. | |
// | |
// Invalid price remains invalid. | |
// | |
function computeOppositePrice(uint16 price) internal constant returns (uint16 opposite) { | |
if (price < maxBuyPrice || price > maxSellPrice) { | |
return uint16(invalidPrice); | |
} else if (price <= minBuyPrice) { | |
return uint16(maxSellPrice - (price - maxBuyPrice)); | |
} else { | |
return uint16(maxBuyPrice + (maxSellPrice - price)); | |
} | |
} | |
// Internal Price Calculation - compute amount in counter currency that would | |
// be obtained by selling baseAmount at the given unpacked price (if no fees). | |
// | |
// Notes: | |
// - Does not validate price - caller must ensure valid. | |
// - Could overflow producing very unexpected results if baseAmount very | |
// large - caller must check this. | |
// - This rounds the amount towards zero. | |
// - May truncate to zero if baseAmount very small - potentially allowing | |
// zero-cost buys or pointless sales - caller must check this. | |
// | |
function computeCntrAmountUsingUnpacked( | |
uint baseAmount, uint16 mantissa, int8 exponent | |
) internal constant returns (uint cntrAmount) { | |
if (exponent < 0) { | |
return baseAmount * uint(mantissa) / 1000 / 10 ** uint(-exponent); | |
} else { | |
return baseAmount * uint(mantissa) / 1000 * 10 ** uint(exponent); | |
} | |
} | |
// Internal Price Calculation - compute amount in counter currency that would | |
// be obtained by selling baseAmount at the given packed price (if no fees). | |
// | |
// Notes: | |
// - Does not validate price - caller must ensure valid. | |
// - Direction of the packed price is ignored. | |
// - Could overflow producing very unexpected results if baseAmount very | |
// large - caller must check this. | |
// - This rounds the amount towards zero (regardless of Buy or Sell). | |
// - May truncate to zero if baseAmount very small - potentially allowing | |
// zero-cost buys or pointless sales - caller must check this. | |
// | |
function computeCntrAmountUsingPacked( | |
uint baseAmount, uint16 price | |
) internal constant returns (uint) { | |
var (, mantissa, exponent) = unpackPrice(price); | |
return computeCntrAmountUsingUnpacked(baseAmount, mantissa, exponent); | |
} | |
// Public Order Placement - create order and try to match it and/or add it to the book. | |
// | |
function createOrder( | |
uint128 orderId, uint16 price, uint sizeBase, Terms terms, uint maxMatches | |
) public { | |
address client = msg.sender; | |
require(orderId != 0 && orderForOrderId[orderId].client == 0); | |
ClientOrderEvent(client, ClientOrderEventType.Create, orderId, maxMatches); | |
orderForOrderId[orderId] = | |
Order(client, price, sizeBase, terms, Status.Unknown, ReasonCode.None, 0, 0, 0, 0); | |
uint128 previousMostRecentOrderIdForClient = mostRecentOrderIdForClient[client]; | |
mostRecentOrderIdForClient[client] = orderId; | |
clientPreviousOrderIdBeforeOrderId[orderId] = previousMostRecentOrderIdForClient; | |
Order storage order = orderForOrderId[orderId]; | |
var (direction, mantissa, exponent) = unpackPrice(price); | |
if (direction == Direction.Invalid) { | |
order.status = Status.Rejected; | |
order.reasonCode = ReasonCode.InvalidPrice; | |
return; | |
} | |
if (sizeBase < baseMinInitialSize || sizeBase > baseMaxSize) { | |
order.status = Status.Rejected; | |
order.reasonCode = ReasonCode.InvalidSize; | |
return; | |
} | |
uint sizeCntr = computeCntrAmountUsingUnpacked(sizeBase, mantissa, exponent); | |
if (sizeCntr < cntrMinInitialSize || sizeCntr > cntrMaxSize) { | |
order.status = Status.Rejected; | |
order.reasonCode = ReasonCode.InvalidSize; | |
return; | |
} | |
if (terms == Terms.MakerOnly && maxMatches != 0) { | |
order.status = Status.Rejected; | |
order.reasonCode = ReasonCode.InvalidTerms; | |
return; | |
} | |
if (!debitFunds(client, direction, sizeBase, sizeCntr)) { | |
order.status = Status.Rejected; | |
order.reasonCode = ReasonCode.InsufficientFunds; | |
return; | |
} | |
processOrder(orderId, maxMatches); | |
} | |
// Public Order Placement - cancel order | |
// | |
function cancelOrder(uint128 orderId) public { | |
address client = msg.sender; | |
Order storage order = orderForOrderId[orderId]; | |
require(order.client == client); | |
Status status = order.status; | |
if (status != Status.Open && status != Status.NeedsGas) { | |
return; | |
} | |
ClientOrderEvent(client, ClientOrderEventType.Cancel, orderId, 0); | |
if (status == Status.Open) { | |
removeOpenOrderFromBook(orderId); | |
MarketOrderEvent(block.timestamp, orderId, MarketOrderEventType.Remove, order.price, | |
order.sizeBase - order.executedBase, 0); | |
} | |
refundUnmatchedAndFinish(orderId, Status.Done, ReasonCode.ClientCancel); | |
} | |
// Public Order Placement - continue placing an order in 'NeedsGas' state | |
// | |
function continueOrder(uint128 orderId, uint maxMatches) public { | |
address client = msg.sender; | |
Order storage order = orderForOrderId[orderId]; | |
require(order.client == client); | |
if (order.status != Status.NeedsGas) { | |
return; | |
} | |
ClientOrderEvent(client, ClientOrderEventType.Continue, orderId, maxMatches); | |
order.status = Status.Unknown; | |
processOrder(orderId, maxMatches); | |
} | |
// Internal Order Placement - remove a still-open order from the book. | |
// | |
// Caller's job to update/refund the order + raise event, this just | |
// updates the order chain and bitmask. | |
// | |
// Too expensive to do on each resting order match - we only do this for an | |
// order being cancelled. See matchWithOccupiedPrice for similar logic. | |
// | |
function removeOpenOrderFromBook(uint128 orderId) internal { | |
Order storage order = orderForOrderId[orderId]; | |
uint16 price = order.price; | |
OrderChain storage orderChain = orderChainForOccupiedPrice[price]; | |
OrderChainNode storage orderChainNode = orderChainNodeForOpenOrderId[orderId]; | |
uint128 nextOrderId = orderChainNode.nextOrderId; | |
uint128 prevOrderId = orderChainNode.prevOrderId; | |
if (nextOrderId != 0) { | |
OrderChainNode storage nextOrderChainNode = orderChainNodeForOpenOrderId[nextOrderId]; | |
nextOrderChainNode.prevOrderId = prevOrderId; | |
} else { | |
orderChain.lastOrderId = prevOrderId; | |
} | |
if (prevOrderId != 0) { | |
OrderChainNode storage prevOrderChainNode = orderChainNodeForOpenOrderId[prevOrderId]; | |
prevOrderChainNode.nextOrderId = nextOrderId; | |
} else { | |
orderChain.firstOrderId = nextOrderId; | |
} | |
if (nextOrderId == 0 && prevOrderId == 0) { | |
uint bmi = price / 256; // index into array of bitmaps | |
uint bti = price % 256; // bit position within bitmap | |
// we know was previously occupied so XOR clears | |
occupiedPriceBitmaps[bmi] ^= 2 ** bti; | |
} | |
} | |
// Internal Order Placement - credit funds received when taking liquidity from book | |
// | |
function creditExecutedFundsLessFees(uint128 orderId, uint originalExecutedBase, uint originalExecutedCntr) internal { | |
Order storage order = orderForOrderId[orderId]; | |
uint liquidityTakenBase = order.executedBase - originalExecutedBase; | |
uint liquidityTakenCntr = order.executedCntr - originalExecutedCntr; | |
// Normally we deduct the fee from the currency bought (base for buy, cntr for sell), | |
// however we also accept reward tokens from the reward balance if it covers the fee, | |
// with the reward amount converted from the ETH amount (the counter currency here) | |
// at a fixed exchange rate. | |
// Overflow safe since we ensure order size < 10^30 in both currencies (see baseMaxSize). | |
// Can truncate to zero, which is fine. | |
uint feesRwrd = liquidityTakenCntr / feeDivisor * ethRwrdRate; | |
uint feesBaseOrCntr; | |
address client = order.client; | |
uint availRwrd = balanceRwrdForClient[client]; | |
if (feesRwrd <= availRwrd) { | |
balanceRwrdForClient[client] = availRwrd - feesRwrd; | |
balanceRwrdForClient[feeCollector] = feesRwrd; | |
// Need += rather than = because could have paid some fees earlier in NeedsGas situation. | |
// Overflow safe since we ensure order size < 10^30 in both currencies (see baseMaxSize). | |
// Can truncate to zero, which is fine. | |
order.feesRwrd += uint128(feesRwrd); | |
if (isBuyPrice(order.price)) { | |
balanceBaseForClient[client] += liquidityTakenBase; | |
} else { | |
balanceCntrForClient[client] += liquidityTakenCntr; | |
} | |
} else if (isBuyPrice(order.price)) { | |
// See comments in branch above re: use of += and overflow safety. | |
feesBaseOrCntr = liquidityTakenBase / feeDivisor; | |
balanceBaseForClient[order.client] += (liquidityTakenBase - feesBaseOrCntr); | |
order.feesBaseOrCntr += uint128(feesBaseOrCntr); | |
balanceBaseForClient[feeCollector] += feesBaseOrCntr; | |
} else { | |
// See comments in branch above re: use of += and overflow safety. | |
feesBaseOrCntr = liquidityTakenCntr / feeDivisor; | |
balanceCntrForClient[order.client] += (liquidityTakenCntr - feesBaseOrCntr); | |
order.feesBaseOrCntr += uint128(feesBaseOrCntr); | |
balanceCntrForClient[feeCollector] += feesBaseOrCntr; | |
} | |
} | |
// Internal Order Placement - process a created and sanity checked order. | |
// | |
// Used both for new orders and for gas topup. | |
// | |
function processOrder(uint128 orderId, uint maxMatches) internal { | |
Order storage order = orderForOrderId[orderId]; | |
uint ourOriginalExecutedBase = order.executedBase; | |
uint ourOriginalExecutedCntr = order.executedCntr; | |
var (ourDirection,) = unpackPrice(order.price); | |
uint theirPriceStart = (ourDirection == Direction.Buy) ? minSellPrice : maxBuyPrice; | |
uint theirPriceEnd = computeOppositePrice(order.price); | |
MatchStopReason matchStopReason = | |
matchAgainstBook(orderId, theirPriceStart, theirPriceEnd, maxMatches); | |
creditExecutedFundsLessFees(orderId, ourOriginalExecutedBase, ourOriginalExecutedCntr); | |
if (order.terms == Terms.ImmediateOrCancel) { | |
if (matchStopReason == MatchStopReason.Satisfied) { | |
refundUnmatchedAndFinish(orderId, Status.Done, ReasonCode.None); | |
return; | |
} else if (matchStopReason == MatchStopReason.MaxMatches) { | |
refundUnmatchedAndFinish(orderId, Status.Done, ReasonCode.TooManyMatches); | |
return; | |
} else if (matchStopReason == MatchStopReason.BookExhausted) { | |
refundUnmatchedAndFinish(orderId, Status.Done, ReasonCode.Unmatched); | |
return; | |
} | |
} else if (order.terms == Terms.MakerOnly) { | |
if (matchStopReason == MatchStopReason.MaxMatches) { | |
refundUnmatchedAndFinish(orderId, Status.Rejected, ReasonCode.WouldTake); | |
return; | |
} else if (matchStopReason == MatchStopReason.BookExhausted) { | |
enterOrder(orderId); | |
return; | |
} | |
} else if (order.terms == Terms.GTCNoGasTopup) { | |
if (matchStopReason == MatchStopReason.Satisfied) { | |
refundUnmatchedAndFinish(orderId, Status.Done, ReasonCode.None); | |
return; | |
} else if (matchStopReason == MatchStopReason.MaxMatches) { | |
refundUnmatchedAndFinish(orderId, Status.Done, ReasonCode.TooManyMatches); | |
return; | |
} else if (matchStopReason == MatchStopReason.BookExhausted) { | |
enterOrder(orderId); | |
return; | |
} | |
} else if (order.terms == Terms.GTCWithGasTopup) { | |
if (matchStopReason == MatchStopReason.Satisfied) { | |
refundUnmatchedAndFinish(orderId, Status.Done, ReasonCode.None); | |
return; | |
} else if (matchStopReason == MatchStopReason.MaxMatches) { | |
order.status = Status.NeedsGas; | |
return; | |
} else if (matchStopReason == MatchStopReason.BookExhausted) { | |
enterOrder(orderId); | |
return; | |
} | |
} | |
assert(false); // should not be possible to reach here | |
} | |
// Used internally to indicate why we stopped matching an order against the book. | |
enum MatchStopReason { | |
None, | |
MaxMatches, | |
Satisfied, | |
PriceExhausted, | |
BookExhausted | |
} | |
// Internal Order Placement - Match the given order against the book. | |
// | |
// Resting orders matched will be updated, removed from book and funds credited to their owners. | |
// | |
// Only updates the executedBase and executedCntr of the given order - caller is responsible | |
// for crediting matched funds, charging fees, marking order as done / entering it into the book. | |
// | |
// matchStopReason returned will be one of MaxMatches, Satisfied or BookExhausted. | |
// | |
// Calling with maxMatches == 0 is ok - and expected when the order is a maker-only order. | |
// | |
function matchAgainstBook( | |
uint128 orderId, uint theirPriceStart, uint theirPriceEnd, uint maxMatches | |
) internal returns ( | |
MatchStopReason matchStopReason | |
) { | |
Order storage order = orderForOrderId[orderId]; | |
uint bmi = theirPriceStart / 256; // index into array of bitmaps | |
uint bti = theirPriceStart % 256; // bit position within bitmap | |
uint bmiEnd = theirPriceEnd / 256; // last bitmap to search | |
uint btiEnd = theirPriceEnd % 256; // stop at this bit in the last bitmap | |
uint cbm = occupiedPriceBitmaps[bmi]; // original copy of current bitmap | |
uint dbm = cbm; // dirty version of current bitmap where we may have cleared bits | |
uint wbm = cbm >> bti; // working copy of current bitmap which we keep shifting | |
// Optimized loops could render a better matching engine yet! | |
bool removedLastAtPrice; | |
matchStopReason = MatchStopReason.None; | |
while (bmi < bmiEnd) { | |
if (wbm == 0 || bti == 256) { | |
if (dbm != cbm) { | |
occupiedPriceBitmaps[bmi] = dbm; | |
} | |
bti = 0; | |
bmi++; | |
cbm = occupiedPriceBitmaps[bmi]; | |
wbm = cbm; | |
dbm = cbm; | |
} else { | |
if ((wbm & 1) != 0) { | |
// careful - copy-and-pasted in loop below ... | |
(removedLastAtPrice, maxMatches, matchStopReason) = | |
matchWithOccupiedPrice(order, uint16(bmi * 256 + bti), maxMatches); | |
if (removedLastAtPrice) { | |
dbm ^= 2 ** bti; | |
} | |
if (matchStopReason == MatchStopReason.PriceExhausted) { | |
matchStopReason = MatchStopReason.None; | |
} else if (matchStopReason != MatchStopReason.None) { | |
// we might still have changes in dbm to write back - see later | |
break; | |
} | |
} | |
bti += 1; | |
wbm /= 2; | |
} | |
} | |
if (matchStopReason == MatchStopReason.None) { | |
// we've reached the last bitmap we need to search, | |
// we'll stop at btiEnd not 256 this time. | |
while (bti <= btiEnd && wbm != 0) { | |
if ((wbm & 1) != 0) { | |
// careful - copy-and-pasted in loop above ... | |
(removedLastAtPrice, maxMatches, matchStopReason) = | |
matchWithOccupiedPrice(order, uint16(bmi * 256 + bti), maxMatches); | |
if (removedLastAtPrice) { | |
dbm ^= 2 ** bti; | |
} | |
if (matchStopReason == MatchStopReason.PriceExhausted) { | |
matchStopReason = MatchStopReason.None; | |
} else if (matchStopReason != MatchStopReason.None) { | |
break; | |
} | |
} | |
bti += 1; | |
wbm /= 2; | |
} | |
} | |
// Careful - if we exited the first loop early, or we went into the second loop, | |
// (luckily can't both happen) then we haven't flushed the dirty bitmap back to | |
// storage - do that now if we need to. | |
if (dbm != cbm) { | |
occupiedPriceBitmaps[bmi] = dbm; | |
} | |
if (matchStopReason == MatchStopReason.None) { | |
matchStopReason = MatchStopReason.BookExhausted; | |
} | |
} | |
// Internal Order Placement. | |
// | |
// Match our order against up to maxMatches resting orders at the given price (which | |
// is known by the caller to have at least one resting order). | |
// | |
// The matches (partial or complete) of the resting orders are recorded, and their | |
// funds are credited. | |
// | |
// The on chain orderbook with resting orders is updated, but the occupied price bitmap is NOT - | |
// the caller must clear the relevant bit if removedLastAtPrice = true is returned. | |
// | |
// Only updates the executedBase and executedCntr of our order - caller is responsible | |
// for e.g. crediting our matched funds, updating status. | |
// | |
// Calling with maxMatches == 0 is ok - and expected when the order is a maker-only order. | |
// | |
// Returns: | |
// removedLastAtPrice: | |
// true iff there are no longer any resting orders at this price - caller will need | |
// to update the occupied price bitmap. | |
// | |
// matchesLeft: | |
// maxMatches passed in minus the number of matches made by this call | |
// | |
// matchStopReason: | |
// If our order is completely matched, matchStopReason will be Satisfied. | |
// If our order is not completely matched, matchStopReason will be either: | |
// MaxMatches (we are not allowed to match any more times) | |
// or: | |
// PriceExhausted (nothing left on the book at this exact price) | |
// | |
function matchWithOccupiedPrice( | |
Order storage ourOrder, uint16 theirPrice, uint maxMatches | |
) internal returns ( | |
bool removedLastAtPrice, uint matchesLeft, MatchStopReason matchStopReason) { | |
matchesLeft = maxMatches; | |
uint workingOurExecutedBase = ourOrder.executedBase; | |
uint workingOurExecutedCntr = ourOrder.executedCntr; | |
uint128 theirOrderId = orderChainForOccupiedPrice[theirPrice].firstOrderId; | |
matchStopReason = MatchStopReason.None; | |
while (true) { | |
if (matchesLeft == 0) { | |
matchStopReason = MatchStopReason.MaxMatches; | |
break; | |
} | |
uint matchBase; | |
uint matchCntr; | |
(theirOrderId, matchBase, matchCntr, matchStopReason) = | |
matchWithTheirs((ourOrder.sizeBase - workingOurExecutedBase), theirOrderId, theirPrice); | |
workingOurExecutedBase += matchBase; | |
workingOurExecutedCntr += matchCntr; | |
matchesLeft -= 1; | |
if (matchStopReason != MatchStopReason.None) { | |
break; | |
} | |
} | |
ourOrder.executedBase = uint128(workingOurExecutedBase); | |
ourOrder.executedCntr = uint128(workingOurExecutedCntr); | |
if (theirOrderId == 0) { | |
orderChainForOccupiedPrice[theirPrice].firstOrderId = 0; | |
orderChainForOccupiedPrice[theirPrice].lastOrderId = 0; | |
removedLastAtPrice = true; | |
} else { | |
// NB: in some cases (e.g. maxMatches == 0) this is a no-op. | |
orderChainForOccupiedPrice[theirPrice].firstOrderId = theirOrderId; | |
orderChainNodeForOpenOrderId[theirOrderId].prevOrderId = 0; | |
removedLastAtPrice = false; | |
} | |
} | |
// Internal Order Placement. | |
// | |
// Match up to our remaining amount against a resting order in the book. | |
// | |
// The match (partial, complete or effectively-complete) of the resting order | |
// is recorded, and their funds are credited. | |
// | |
// Their order is NOT removed from the book by this call - the caller must do that | |
// if the nextTheirOrderId returned is not equal to the theirOrderId passed in. | |
// | |
// Returns: | |
// | |
// nextTheirOrderId: | |
// If we did not completely match their order, will be same as theirOrderId. | |
// If we completely matched their order, will be orderId of next order at the | |
// same price - or zero if this was the last order and we've now filled it. | |
// | |
// matchStopReason: | |
// If our order is completely matched, matchStopReason will be Satisfied. | |
// If our order is not completely matched, matchStopReason will be either | |
// PriceExhausted (if nothing left at this exact price) or None (if can continue). | |
// | |
function matchWithTheirs( | |
uint ourRemainingBase, uint128 theirOrderId, uint16 theirPrice) internal returns ( | |
uint128 nextTheirOrderId, uint matchBase, uint matchCntr, MatchStopReason matchStopReason) { | |
Order storage theirOrder = orderForOrderId[theirOrderId]; | |
uint theirRemainingBase = theirOrder.sizeBase - theirOrder.executedBase; | |
if (ourRemainingBase < theirRemainingBase) { | |
matchBase = ourRemainingBase; | |
} else { | |
matchBase = theirRemainingBase; | |
} | |
matchCntr = computeCntrAmountUsingPacked(matchBase, theirPrice); | |
// It may seem a bit odd to stop here if our remaining amount is very small - | |
// there could still be resting orders we can match it against. During network congestion gas | |
// cost of matching each order can become quite high - potentially high enough to | |
// wipe out the profit the taker hopes for from trading the tiny amount left. | |
if ((ourRemainingBase - matchBase) < baseMinRemainingSize) { | |
matchStopReason = MatchStopReason.Satisfied; | |
} else { | |
matchStopReason = MatchStopReason.None; | |
} | |
bool theirsDead = recordTheirMatch(theirOrder, theirOrderId, theirPrice, matchBase, matchCntr); | |
if (theirsDead) { | |
nextTheirOrderId = orderChainNodeForOpenOrderId[theirOrderId].nextOrderId; | |
if (matchStopReason == MatchStopReason.None && nextTheirOrderId == 0) { | |
matchStopReason = MatchStopReason.PriceExhausted; | |
} | |
} else { | |
nextTheirOrderId = theirOrderId; | |
} | |
} | |
// Internal Order Placement. | |
// | |
// Record match (partial or complete) of resting order, and credit them their funds. | |
// | |
// If their order is completely matched, the order is marked as done, | |
// and "theirsDead" is returned as true. | |
// | |
// The order is NOT removed from the book by this call - the caller | |
// must do that if theirsDead is true. | |
// | |
// No sanity checks are made - the caller must be sure the order is | |
// not already done and has sufficient remaining. (Yes, we'd like to | |
// check here too but we cannot afford the gas). | |
// | |
function recordTheirMatch( | |
Order storage theirOrder, uint128 theirOrderId, uint16 theirPrice, uint matchBase, uint matchCntr | |
) internal returns (bool theirsDead) { | |
// they are a maker so no fees | |
// overflow safe - see comments about baseMaxSize | |
// executedBase cannot go > sizeBase due to logic in matchWithTheirs | |
theirOrder.executedBase += uint128(matchBase); | |
theirOrder.executedCntr += uint128(matchCntr); | |
if (isBuyPrice(theirPrice)) { | |
// they have bought base (using the counter they already paid when creating the order) | |
balanceBaseForClient[theirOrder.client] += matchBase; | |
} else { | |
// they have bought counter (using the base they already paid when creating the order) | |
balanceCntrForClient[theirOrder.client] += matchCntr; | |
} | |
uint stillRemainingBase = theirOrder.sizeBase - theirOrder.executedBase; | |
// avoid leaving tiny amounts in the book - refund remaining if too small | |
if (stillRemainingBase < baseMinRemainingSize) { | |
refundUnmatchedAndFinish(theirOrderId, Status.Done, ReasonCode.None); | |
// someone building an UI on top needs to know how much was match and how much was refund | |
MarketOrderEvent(block.timestamp, theirOrderId, MarketOrderEventType.CompleteFill, | |
theirPrice, matchBase + stillRemainingBase, matchBase); | |
return true; | |
} else { | |
MarketOrderEvent(block.timestamp, theirOrderId, MarketOrderEventType.PartialFill, | |
theirPrice, matchBase, matchBase); | |
return false; | |
} | |
} | |
// Internal Order Placement. | |
// | |
// Refund any unmatched funds in an order (based on executed vs size) and move to a final state. | |
// | |
// The order is NOT removed from the book by this call and no event is raised. | |
// | |
// No sanity checks are made - the caller must be sure the order has not already been refunded. | |
// | |
function refundUnmatchedAndFinish(uint128 orderId, Status status, ReasonCode reasonCode) internal { | |
Order storage order = orderForOrderId[orderId]; | |
uint16 price = order.price; | |
if (isBuyPrice(price)) { | |
uint sizeCntr = computeCntrAmountUsingPacked(order.sizeBase, price); | |
balanceCntrForClient[order.client] += sizeCntr - order.executedCntr; | |
} else { | |
balanceBaseForClient[order.client] += order.sizeBase - order.executedBase; | |
} | |
order.status = status; | |
order.reasonCode = reasonCode; | |
} | |
// Internal Order Placement. | |
// | |
// Enter a not completely matched order into the book, marking the order as open. | |
// | |
// This updates the occupied price bitmap and chain. | |
// | |
// No sanity checks are made - the caller must be sure the order | |
// has some unmatched amount and has been paid for! | |
// | |
function enterOrder(uint128 orderId) internal { | |
Order storage order = orderForOrderId[orderId]; | |
uint16 price = order.price; | |
OrderChain storage orderChain = orderChainForOccupiedPrice[price]; | |
OrderChainNode storage orderChainNode = orderChainNodeForOpenOrderId[orderId]; | |
if (orderChain.firstOrderId == 0) { | |
orderChain.firstOrderId = orderId; | |
orderChain.lastOrderId = orderId; | |
orderChainNode.nextOrderId = 0; | |
orderChainNode.prevOrderId = 0; | |
uint bitmapIndex = price / 256; | |
uint bitIndex = price % 256; | |
occupiedPriceBitmaps[bitmapIndex] |= (2 ** bitIndex); | |
} else { | |
uint128 existingLastOrderId = orderChain.lastOrderId; | |
OrderChainNode storage existingLastOrderChainNode = orderChainNodeForOpenOrderId[existingLastOrderId]; | |
orderChainNode.nextOrderId = 0; | |
orderChainNode.prevOrderId = existingLastOrderId; | |
existingLastOrderChainNode.nextOrderId = orderId; | |
orderChain.lastOrderId = orderId; | |
} | |
MarketOrderEvent(block.timestamp, orderId, MarketOrderEventType.Add, | |
price, order.sizeBase - order.executedBase, 0); | |
order.status = Status.Open; | |
} | |
// Internal Order Placement. | |
// | |
// Charge the client for the cost of placing an order in the given direction. | |
// | |
// Return true if successful, false otherwise. | |
// | |
function debitFunds( | |
address client, Direction direction, uint sizeBase, uint sizeCntr | |
) internal returns (bool success) { | |
if (direction == Direction.Buy) { | |
uint availableCntr = balanceCntrForClient[client]; | |
if (availableCntr < sizeCntr) { | |
return false; | |
} | |
balanceCntrForClient[client] = availableCntr - sizeCntr; | |
return true; | |
} else if (direction == Direction.Sell) { | |
uint availableBase = balanceBaseForClient[client]; | |
if (availableBase < sizeBase) { | |
return false; | |
} | |
balanceBaseForClient[client] = availableBase - sizeBase; | |
return true; | |
} else { | |
return false; | |
} | |
} | |
// Public Book View | |
// | |
// Intended for public book depth enumeration from web3 (or similar). | |
// | |
// Not suitable for use from a smart contract transaction - gas usage | |
// could be very high if we have many orders at the same price. | |
// | |
// Start at the given inclusive price (and side) and walk down the book | |
// (getting less aggressive) until we find some open orders or reach the | |
// least aggressive price. | |
// | |
// Returns the price where we found the order(s), the depth at that price | |
// (zero if none found), order count there, and the current blockNumber. | |
// | |
// (The blockNumber is handy if you're taking a snapshot which you intend | |
// to keep up-to-date with the market order events). | |
// | |
// To walk through the on-chain orderbook, the caller should start by calling walkBook with the | |
// most aggressive buy price (Buy @ 999000). | |
// If the price returned is the least aggressive buy price (Buy @ 0.000001), | |
// the side is complete. | |
// Otherwise, call walkBook again with the (packed) price returned + 1. | |
// Then repeat for the sell side, starting with Sell @ 0.000001 and stopping | |
// when Sell @ 999000 is returned. | |
// | |
function walkBook(uint16 fromPrice) public constant returns ( | |
uint16 price, uint depthBase, uint orderCount, uint blockNumber | |
) { | |
uint priceStart = fromPrice; | |
uint priceEnd = (isBuyPrice(fromPrice)) ? minBuyPrice : maxSellPrice; | |
// See comments in matchAgainstBook re: how these crazy loops work. | |
uint bmi = priceStart / 256; | |
uint bti = priceStart % 256; | |
uint bmiEnd = priceEnd / 256; | |
uint btiEnd = priceEnd % 256; | |
uint wbm = occupiedPriceBitmaps[bmi] >> bti; | |
while (bmi < bmiEnd) { | |
if (wbm == 0 || bti == 256) { | |
bti = 0; | |
bmi++; | |
wbm = occupiedPriceBitmaps[bmi]; | |
} else { | |
if ((wbm & 1) != 0) { | |
// careful - copy-pasted in below loop | |
price = uint16(bmi * 256 + bti); | |
(depthBase, orderCount) = sumDepth(orderChainForOccupiedPrice[price].firstOrderId); | |
return (price, depthBase, orderCount, block.number); | |
} | |
bti += 1; | |
wbm /= 2; | |
} | |
} | |
// we've reached the last bitmap we need to search, stop at btiEnd not 256 this time. | |
while (bti <= btiEnd && wbm != 0) { | |
if ((wbm & 1) != 0) { | |
// careful - copy-pasted in above loop | |
price = uint16(bmi * 256 + bti); | |
(depthBase, orderCount) = sumDepth(orderChainForOccupiedPrice[price].firstOrderId); | |
return (price, depthBase, orderCount, block.number); | |
} | |
bti += 1; | |
wbm /= 2; | |
} | |
return (uint16(priceEnd), 0, 0, block.number); | |
} | |
// Internal Book View. | |
// | |
// See walkBook - adds up open depth at a price starting from an | |
// order which is assumed to be open. Careful - unlimited gas use. | |
// | |
function sumDepth(uint128 orderId) internal constant returns (uint depth, uint orderCount) { | |
while (true) { | |
Order storage order = orderForOrderId[orderId]; | |
depth += order.sizeBase - order.executedBase; | |
orderCount++; | |
orderId = orderChainNodeForOpenOrderId[orderId].nextOrderId; | |
if (orderId == 0) { | |
return (depth, orderCount); | |
} | |
} | |
} | |
} | |
// helper for automating book creation | |
contract OnChainOrderBookV013bFactory { | |
event BookCreated (address bookAddress); | |
function OnChainOrderBookV013bFactory() { | |
} | |
function createBook(ERC20 _baseToken, ERC20 _rwrdToken, address _feeCollector, uint _baseMinInitialSize, int8 _minPriceExponent) public { | |
OnChainOrderBookV013b book = new OnChainOrderBookV013b(); | |
book.init(_baseToken, _rwrdToken, _baseMinInitialSize, _minPriceExponent); | |
book.changeFeeCollector(_feeCollector); | |
BookCreated(address(book)); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment