bsips/bsip-0035.md

352 lines
17 KiB
Markdown
Raw Normal View History

2018-02-19 00:19:59 +00:00
BSIP: 0035
Title: A Solution To Something-For-Nothing Issue
Author: Abit More <https://github.com/abitmore>
Status: Draft
Type: Protocol
Created: 2018-02-19
Discussion: https://github.com/bitshares/bitshares-core/issues/132,
https://github.com/bitshares/bitshares-core/issues/184
Replaces: -
Worker: To be done
# Abstract
Under some circumstances, when two orders get matched, due to rounding,
one order may be paying something but receiving nothing,
the other order may be paying nothing but receiving something.
This is the so-called something-for-nothing issue.
This looks clearly unfair.
This BSIP proposes an overall mechanism to avoid something-for-nothing issue
completely.
This BSIP also sets a principle: something-for-nothing shouldn't happen when
matching orders.
# Motivation
There are mechanisms in the system to try to avoid something-for-nothing issue,
however, not all scenarios are well-handled, see [bitshares-core
issue #184](https://github.com/bitshares/bitshares-core/issues/184) for example.
# Rationale
## Amounts, Prices and Rounding
Amounts in the system are integers with per-asset fixed precisions.
The minimum positive amount of an asset is called one Satoshi.
Prices in the system are rational numbers, which are expressed as
`base_amount / quote_amount` (precisions are omitted here).
To calculate how much amount of asset B is equivalent to some amount of
asset A, need to calculate `amount_of_a * a_to_b_price` which is
`amount_of_a * b_amount_in_price / a_amount_in_price`. The accurate result
of this formula is a rational number. To convert it to the final result which
is an amount, which is an integer, may need to round.
## Order Matching
An order means someone is willing to give out some amount of asset X expecting
to get some amount of asset Y. The ratio between the two assets is the price of
the order. The price can be expressed as either `x_amount / y_amount` or
`y_amount / x_amount`, when we know which amount in the price is of which asset,
the two expressions are equivalent. The amount of asset X is known and fixed.
In a market, E.G. the X:Y market, some people are selling X for Y, some people
are selling Y for X (or say buying X with Y). Orders are classified by type
(buy or sell), then ordered by price. For each type, the order offering the
best price is on the top. So, in every market there may be a top buy order and
a top sell order, name them highest bid and lowest ask, so there is
a highest bid price (in terms of `asset X amount / asset Y amount`),
and a lowest ask price (in terms of `asset X amount / asset Y amount` as well).
When the highest bid price is higher or equal to the lowest ask price, the two
top orders can be matched with each other.
## The Match Price
In a continuous trading market, orders are placed one by one, when comparing
every two orders, it's deterministic that one order is placed earlier than the
other.
In BitShares, it doesn't mean that the transaction that contains the first
order is signed before the transaction contains the second, but means that
the first order is processed earlier than the second in the witness node that
produced the block that contains the second order.
When two orders get matched, the one placed earlier is maker, the other one
is taker. Say, the maker provides an offer, the taker accept the offer.
So, when calculating who will get how much, we use the maker order's price,
aka maker price, as the match price.
## The Need for Compromise
When matching two orders, due to rounding, usually we're unable to completely
satisfy both parties.
Here is an example mentioned in the 4th comment of [bitshares-core
issue #132](https://github.com/bitshares/bitshares-core/issues/132):
Alice's order: Sell CORE at $3 / 8, balance 1000000 CORE
Bob's order: Buy CORE at $19 / 50, balance $10
Both assets have precision of 1, i.e. the order balances are
1000000 CORE-satoshis and 10 USD-satoshis repectively.
Alice is selling at $3/8 CORE = $0.375 / CORE and Bob is buying at
$19 / 50 CORE = $0.38, so based on the price, Alice and Bob should match.
Bob's $10 / $0.38 ~ 26.3. So 26.3 is the fewest CORE he is willing to accept
(assuming that the meaning of "price" is "the least favorable exchange rate
a party is willing to accept in trade"). Combined with the design restriction
that satoshis are indivisible, in practice this means Bob will only accept 27
or more CORE for his $10.
But $10 / 27 gives a price smaller than $0.370 and $0.371, which is smaller
than Alice's sale price of $0.375. So neither party can fill this offer.
We need to come to a compromise.
## The Possible Solutions
There are some possible solutions listed in the 5th comment of [bitshares-core
issue #132](https://github.com/bitshares/bitshares-core/issues/132):
- (a) Fill someone at a less favorable exchange rate than the price they
specified in their order. Downside: This violates the above definition of
price; i.e. if a user enters a price intending the system to never sell below
that price in any circumstance, the system will not always behave in a way
which fulfills that user intent.
- (b) Keep both orders on the books. Downside: This complicates the matching
algorithm, as now Alice might be able to match an order behind Bob's order.
Naive implementation would have potentially unbounded matching complexity;
a more clever implementation might be possible but would require substantial
design and testing effort.
- (c) Cancel an order. This is complicated by the fact that an order such as
a margin call cannot be cancelled. Downside: When there are margin calls
happening, it seems perverse to delete a large order that's willing to fill
them just because the lead margin call happens to fall in a narrow window
which causes a rounding issue. Also, orders cancelled by this mechanism
cannot be refunded. Otherwise an attacker who wants to consume
a lot of memory on all nodes could create a large number of orders, then
trigger this case to cancel them all, getting their investment in deferred
cancellation fees back without paying the cancel op's per-order fee as
intended.
- (d) Require all orders to use the same denominator. Altcoin exchanges and
many real-world markets like the stock market solve this problem by specifying
one asset as the denominator asset, specifying a "tick" which is the smallest
unit of price precision, and requiring all prices to conform.
Downside: Complicates the implementation of flipped market UI, may require
re-working part of market GUI, reduces user flexibility, new asset fields
required to specify precision, if `n` assets exist then `O(n^2)` markets
could exist and we need to figure out how to determine the precision
requirement for all of them.
## The Chosen Solution
Current code actually implemented (a) in the first place: when matching two
orders, if there is a rounding issue, the order with smaller volume will be
filled at a less favorable price. It's the least bad compromise since it has
the most efficiency (highest traded volume while not hard to implement) among
the solutions.
The algorithm can be described as follows (sample code is
[here](https://github.com/bitshares/bitshares-core/blob/2.0.171105a/libraries/chain/db_market.cpp#L311-L324)):
Assuming the maker order is selling amount `X` of asset A, with price
`maker_price = maker_b_amount / maker_a_amount`; assuming the taker is buying
asset A with amount `Y` of asset B, with price
`taker_price = taker_b_amount / taker_a_amount`. Anyway, since the two orders
will be matched at maker price, the taker price doesn't matter here as long
as it's higher than or equal to maker price. Note: currently all limit orders
are implemented as sell limit orders, so in the example, the taker order can
only specify amount of asset B but not amount of asset A.
Now compare `X * maker_price` with `Y`. To be accurate (avoid rounding),
compare `X' = X * maker_b_amount` with `Y' = Y * maker_a_amount`.
* The best scenario is when `X' == Y'`, which means both orders can be
completely filled at `maker_price`.
* If `X' < Y'`, it means the maker order can be completely filled but the
taker order can't, aka the maker order is smaller.
In this case, maker pay amount `X` of asset A to taker, taker pay amount
`Y" = round_down( X' / maker_a_amount )` of asset B to maker.
2018-02-22 15:36:43 +00:00
Note: due to rounded down, it's possible that `Y"` is smaller than the
rational number
`X * maker_price`, which means `Y" / X` may be lower than `maker_price`,
that said, the maker order may has been filled at a less favorable price.
* If `X' > Y'`, it means the taker order can be completely filled but the
maker order can't, aka the taker order is smaller.
In this case, taker pay amount `Y` of asset B to maker, maker pay amount
`X" = round_down( Y' / maker_b_amount )` of asset A to taker.
2018-02-22 15:36:43 +00:00
Note: due to rounded down, it's possible that `X"` is smaller than the
rational number
`Y / taker_price`, which means `Y / X"` may be higher than `taker_price`,
that said, the taker order may has been filled at a less favorable price.
When filling a small order at a less favorable price, the receiving
2018-02-19 00:19:59 +00:00
amount is often rounded down to zero, thus causes the something-for-nothing
issue. Current code tried to solve the issue by cancelling the smaller order
when it would receive nothing, but only applied this rule in a few senarios
(the processed parties won't be paying something for nothing):
2018-02-19 00:19:59 +00:00
* when matching two limit orders, processed the maker
* when matching a limit order with a call order, processed the call order
* when matching a settle order with a call order, processed the call order
* when globally settling, processed the call order
Other senarios that need to be processed as well (these to-be-processed parties
may be paying something for nothing in current system):
2018-02-19 00:19:59 +00:00
* when matching two limit orders, process the taker
* when matching a limit order with a call order, process the limit order
2018-02-19 00:19:59 +00:00
* when matching a force settle order with a call order, process the settle order
* when globally settling, process the settlement fund
* when force settling after an asset has been globally settled, paying the force
settle order from global settlement fund, process the settle order
2018-02-19 00:19:59 +00:00
## The Improved Solution (This BSIP)
The detailed rules proposes in this BSIP (new rules highlighted):
* match in favor of taker, or say, match at maker price
* round down receiving amounts when possible
* when matching two limit orders, round down the receiving amounts in favor
of bigger order, or say, try to fill the smaller order
* **if the smaller order would get nothing after the round-down, cancel it**
* when matching a limit order with a call order, in favor of call order,
round down receiving collateral amount
* **if the call order is receiving the whole debt amount (so the short
position will be closed) but paying nothing, let it pay 1 Satoshi
(round up);**
* **otherwise, if the limit order would get nothing after the round-down,
cancel it (it's smaller, so safe to cancel)**
2018-02-19 00:19:59 +00:00
* when matching a settle order with a call order, in favor of call order,
round down receiving collateral amount
* **if the call order is receiving the whole debt amount (so the short
position will be closed) but paying nothing, let it pay 1 Satoshi
(round up);**
* **otherwise, if the settle order would be completely filled but would
receive nothing, cancel it;**
* **otherwise, it means both orders won't be completely filled, which may
due to hitting `maximum_force_settlement_volume`, in this case, don't fill
any one of the two orders, and stop matching for this asset at this block;**
* **that said, only round up when the call order is completely filled, so
won't trigger a black swan event, nor need to check for it.**
2018-02-19 00:19:59 +00:00
* when globally settling, in favor of call order, round down receiving
collateral amount
* **when the asset is not a prediction market, if a call order would pay
nothing, let it pay 1 Satoshi (round up).**
* when paying a settle order from global settlement fund, in favor of global
settlement fund, round down receiving collateral amount
* **when the asset is not a prediction market, if the settle order would
receive nothing, raise an exception (aka let the operation fail).**
2018-02-19 00:19:59 +00:00
Take the example mentioned in the 4th comment of [bitshares-core
issue #132](https://github.com/bitshares/bitshares-core/issues/132):
* Alice's order: Sell CORE at `$3 / 8 = $0.375`, balance `1000000 CORE`
* Bob's order: Buy CORE at `$19 / 50 = $0.38`, balance `$10`
Process:
* If both orders are limit orders
* If Alice's order is maker, use `$3 / 8` as match price;
since Bob's order is smaller, round in favor of Alice's order,
so Bob will get
`round_down($10 * 8 CORE / $3) = round_down(26.67 CORE) = 26 CORE`,
the effective price would be `$10 / 26 CORE = $0.3846`.
* If Bob's order is maker, use `$19 / 50` as match price; since Bob's
order is smaller, round in favor of Alice's order, so Bob will get
`round_down($10 * 50 CORE / $19 = round_down(26.32 CORE) = 26 CORE`,
the effective price would still be `$10 / 26 CORE = $0.3846`.
* If Alice's order is a call order, always round in favor of it, we get
same results.
If we change the example to this:
* Alice's order: Buy CORE at `3 CORE / $8 = 0.375`, balance `$1000000`
* Bob's order: Sell CORE at `19 CORE / $50 = 0.38`, balance `10 CORE`
Process:
* If both orders are limit orders, we get same results as above
* If Bob's order is a call order, we should always round in favor of it,
however, it should have a debt amount which is an integer, for example
`$27`, then Alice would get
* `round_down(27 * 3 / 8) = round_down(10.125) = 10 CORE` as a maker, or
* `round_down(27 * 19 / 50) = round_down(10.26) = 10 CORE` as a taker.
# Specifications
## When Matching Two Limit Orders
In `match( const limit_order_object&, OrderType ... )` function of `database`
class, after calculated `usd_receives` which is for the taker,
check if it is zero.
If the answer is `true`, skip filling and see the order is filled, return `1`,
so the order will be cancelled later.
## When Matching A Limit Order With A Call Order
In `check_call_orders(...)` function of `database` class,
after calculated `order_receives`, check if it is zero.
If the answer is `true`,
* if `call_receives` is equal to `call_itr->debt`, set `order_receives` to `1`;
* otherwise, skip filling and cancel the limit order.
2018-02-19 00:19:59 +00:00
## When Matching A Settle Order With A Call Order
In `match( const call_order_object&, ... )` function of `database` class,
after calculated `call_pays`, check if it is zero.
If the answer is `true`,
* if `call_receives` is equal to `call_debt`, set `call_pays` to `1`;
* otherwise, if `call_receives` is equal to `settle.balance`,
call `cancel_order(...)` with parameter set to `settle`,
then return a zero-amount collateral asset object;
* otherwise, return a zero-amount collateral asset object directly.
After returned, need to check the amount of returned asset at where calling the
`match(...)` function, specifically, `clear_expired_orders()` function of
`database` class. If the returned amount is `0`, break out of the `while` loop.
If the settle order is still there and the returned amount is `0`,
label that processing of this asset has completed. Also, in the outer loop,
need to check the label, if found it's completed, process next asset.
2018-02-19 00:19:59 +00:00
## When Globally Settling
In `global_settle_asset(...)` function of `database` class, check each `pays`,
once it's zero, and the asset is not a prediction market, let it be `1`.
## When Paying A Settle Order From Global Settlement Fund
In `do_apply(...)` function of `asset_settle_evaluator` class,
after calculated `settled_amount`, check if it is zero. If the answer is `true`,
and the asset is not a prediction maket, throw a `fc::exception`.
2018-02-19 00:19:59 +00:00
# Discussion
There is an argument suggests when matching call orders, we should always
round in favour of the call. If a settlement receives 0 collateral as a result,
that's acceptable, because the settlement price is unknown at the time when
settlement is requested, so no guarantee is violated (within the range of
rounding errors). This should keep the collateral > 0 as long as there is
outstanding debt. A counter-argument supports rounding up to 1 Satoshi since
rounding down to zero may break the promise of "every smart coin is backed
by something".
There is an argument says breaking the `min_to_receive` limit is a no-go,
because that's why it's called a "limit order". A counter-argument says
slightly breaking the limit is the least bad compromise.
# Summary for Shareholders
[to be added if any]
# Copyright
This document is placed in the public domain.
# See Also
* https://github.com/bitshares/bitshares-core/issues/132
* https://github.com/bitshares/bitshares-core/issues/184