Skip to content

Instantly share code, notes, and snippets.

@amisolution
Created September 20, 2019 17:39
Show Gist options
  • Save amisolution/5732c7b13696a086faf4ffa40450f4dc to your computer and use it in GitHub Desktop.
Save amisolution/5732c7b13696a086faf4ffa40450f4dc to your computer and use it in GitHub Desktop.
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