bsip35: overall solution for rounding issues

This commit is contained in:
abitmore 2018-02-25 21:24:21 +00:00
parent 485df5cd4f
commit d6c7fdc785
2 changed files with 190 additions and 69 deletions

View file

@ -42,4 +42,4 @@ Number | Title |
[32](bsip-0032.md) | Always Match Orders At Maker Price | Abit More | Protocol | Draft [32](bsip-0032.md) | Always Match Orders At Maker Price | Abit More | Protocol | Draft
[33](bsip-0033.md) | Maker Orders With Better Prices Take Precedence | Abit More | Protocol | Draft [33](bsip-0033.md) | Maker Orders With Better Prices Take Precedence | Abit More | Protocol | Draft
[34](bsip-0034.md) | Always Trigger Margin Call When Call Price Above Or At Price Feed | Abit More | Protocol | Draft [34](bsip-0034.md) | Always Trigger Margin Call When Call Price Above Or At Price Feed | Abit More | Protocol | Draft
[35](bsip-0035.md) | A Solution To Something-For-Nothing Issue | Abit More | Protocol | Draft [35](bsip-0035.md) | Mitigate Rounding Issue On Order Matching | Abit More | Protocol | Draft

View file

@ -1,5 +1,5 @@
BSIP: 0035 BSIP: 0035
Title: A Solution To Something-For-Nothing Issue Title: Mitigate Rounding Issue On Order Matching
Author: Abit More <https://github.com/abitmore> Author: Abit More <https://github.com/abitmore>
Status: Draft Status: Draft
Type: Protocol Type: Protocol
@ -12,17 +12,15 @@
# Abstract # Abstract
Under some circumstances, when two orders get matched, due to rounding, Under some circumstances, when two orders get matched, due to rounding,
one order may be paying something but receiving nothing, one order may be paying more than enough, even paying something but receiving
the other order may be paying nothing but receiving something. nothing. This looks unfair.
This is the so-called something-for-nothing issue.
This looks clearly unfair. This BSIP proposes an overall mechanism to mitigate rounding issue when matching
orders and avoid something-for-nothing issue completely.
This BSIP proposes an overall mechanism to avoid something-for-nothing issue This BSIP also sets two principles for order matching:
completely. * never pay more than enough, and
* something-for-nothing shouldn't happen.
This BSIP also sets a principle: something-for-nothing shouldn't happen when
matching orders.
# Motivation # Motivation
@ -30,6 +28,9 @@ There are mechanisms in the system to try to avoid something-for-nothing issue,
however, not all scenarios are well-handled, see [bitshares-core however, not all scenarios are well-handled, see [bitshares-core
issue #184](https://github.com/bitshares/bitshares-core/issues/184) for example. issue #184](https://github.com/bitshares/bitshares-core/issues/184) for example.
Other than that, rounding issue occurs frequently and has led to a lot of
confusion among market participants.
# Rationale # Rationale
## Amounts, Prices and Rounding ## Amounts, Prices and Rounding
@ -188,6 +189,10 @@ compare `X' = X * maker_b_amount` with `Y' = Y * maker_a_amount`.
`Y / taker_price`, which means `Y / X"` may be higher than `taker_price`, `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. that said, the taker order may has been filled at a less favorable price.
## Issues With The Chosen Solution
### The Something-for-nothing Issue
When filling a small order at a less favorable price, the receiving When filling a small order at a less favorable price, the receiving
amount is often rounded down to zero, thus causes the something-for-nothing 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 issue. Current code tried to solve the issue by cancelling the smaller order
@ -207,43 +212,101 @@ may be paying something for nothing in current system):
* when force settling after an asset has been globally settled, paying the force * when force settling after an asset has been globally settled, paying the force
settle order from global settlement fund, process the settle order settle order from global settlement fund, process the settle order
## The Improved Solution (This BSIP) ### The Broader Rounding Issue
The detailed rules proposes in this BSIP (new rules highlighted): Something-for-nothing is only a subset of rounding issues, it's the most extreme
* match in favor of taker, or say, match at maker price one. There are much more scenarios that one of the matched parties would be
* round down receiving amounts when possible paying more than enough, although they're not paying something for nothing
* when matching two limit orders, round down the receiving amounts in favor overall. Some scenarios are discussed in [bitshares-core
of bigger order, or say, try to fill the smaller order issue #342](https://github.com/bitshares/bitshares-core/issues/342).
* **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, Take a scenario similar to the one described in the 4th comment of
round down receiving collateral amount [bitshare-core
* **if the call order is receiving the whole debt amount (so the short issue #132](https://github.com/bitshares/bitshares-core/issues/132) as an
position will be closed) but paying nothing, let it pay 1 Satoshi example:
(round up);** * Alice's order: Sell CORE at `$3 / 80 = $0.0375`, balance `50 CORE`
* **otherwise, if the limit order would get nothing after the round-down, * Bob's order: Buy CORE at `$19 / 500 = $0.038`, balance `$100`
cancel it (it's smaller, so safe to cancel)**
* when matching a settle order with a call order, in favor of call order, Current system would process them as follows:
round down receiving collateral amount * If Alice's order is maker, use `$3 / 80` as match price; since Alice's order
* **if the call order is receiving the whole debt amount (so the short is smaller, round in favor of Bob's order, so Alice will pay the whole `50
position will be closed) but paying nothing, let it pay 1 Satoshi CORE` and get `round_down(50 CORE * $3 / 80 CORE) = round_down($1.6) = $1`,
(round up);** the effective price would be `$1 / 50 = $0.02`;
* **otherwise, if the settle order would be completely filled but would * If Bob's order is maker, use `$19 / 500` as match price; since Alice's order
receive nothing, cancel it;** is smaller, round in favor of Bob's order, so Alice will pay the whole `50
* **otherwise, it means both orders won't be completely filled, which may CORE` and get `round_down(50 CORE * $19 / 500 CORE = round_down($1.9) = $1`,
due to hitting `maximum_force_settlement_volume`, in this case, don't fill the effective price would still be `$1 / 50 = $0.02`.
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 Both results are far from Alice's desired price `$0.0375`. Actually, according
won't trigger a black swan event, nor need to check for it.** to Bob's desired price, paying `round_up($1 * 500 CORE / $19) = 27 CORE` would
* when globally settling, in favor of call order, round down receiving be enough, then the effective price would be `$1 / 27 = $0.037`, which is
collateral amount still below Alice's desired price `$0.0375`, but much closer than `$0.02`.
* **when the asset is not a prediction market, if a call order would pay
nothing, let it pay 1 Satoshi (round up).** ## The Improved Solution Proposed By This BSIP
* when paying a settle order from global settlement fund, in favor of global
settlement fund, round down receiving collateral amount The detailed rules proposed by this BSIP with new rules highlighted:
* If the settling amount is equal to total supply of that asset, pay the
* match in favor of taker, or say, match at maker price;
* round the receiving amounts according to rules below.
* When matching two limit orders, round down the receiving amount of the
smaller order,
* **if the smaller order would get nothing, cancel it;**
* **otherwise, calculate the amount that the smaller order would pay as
`round_up(receiving_amount * match_price)`.**
* **After filled both orders, for each remaining order (with a positive
amount remaining), check the remaining amount, if the amount is too small
so the order would receive nothing on next match, cancel the order.**
* When matching a limit order with a call order,
* **if the call order is receiving the whole debt amount, which means it's
smaller and the short position will be closed after the match, round up its
paying amount; otherwise, round down its paying amount.**
* **In the latter case, if the limit order would receive nothing, cancel it
(it's smaller, so safe to cancel).**
* When matching a settle order with a call order,
* **if the call order is receiving the whole debt amount, which means it's
smaller and the short position will be closed after the match, round up its
paying amount; otherwise, round down its paying amount.**
* **In the latter case,**
* **if the settle order would receive nothing,**
* **if the settle order would be completely filled, 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 of the two orders, and stop matching for this asset at
this block;**
* **otherwise (if the settle order would not receive nothing), calculate
the amount that the settle order would pay as
`round_up(receiving_amount * match_price)`. After filled both orders,
match the settle order with the call order again. In the new match, either
the settle order will be cancelled due to too small, or we will stop
matching due to hitting `maximum_force_settlement_volume`.**
* **That said, only round up the collateral amount paid by the call order
when it is completely filled, so if the call order still exist after the
match, its collateral ratio won't be lower than before, which means we won't
trigger a black swan event, nor need to check whether a black swan event
would be triggered.**
* When globally settling, **in favor of global settlement fund, round up
collateral amount.**
* When paying a settle order from global settlement fund, for predition
markets, there would be no rounding issue, also no need to deal with
something-for-nothing issue; for other assets, apply rules below:
* if the settling amount is equal to total supply of that asset, pay the
whole remaining settlement fund to the settle order; whole remaining settlement fund to the settle order;
* **when the asset is not a prediction market, if the settle order would * otherwise, in favor of global settlement fund since its volume is bigger,
receive nothing, raise an exception (aka let the operation fail).** round down collateral amount. **If the settle order would receive nothing,
raise an exception (aka let the operation fail). Otherwise, calculate the
amount that the settle order would pay as
`round_up(receiving_amount * match_price)`; after filled the order, if there
is still some amount remaining in the order, return it to the owner.**
## Examples Of The Improved Solution
### Example 1
Take the example mentioned in the 4th comment of [bitshares-core Take the example mentioned in the 4th comment of [bitshares-core
issue #132](https://github.com/bitshares/bitshares-core/issues/132): issue #132](https://github.com/bitshares/bitshares-core/issues/132):
@ -256,55 +319,106 @@ Process:
since Bob's order is smaller, round in favor of Alice's order, since Bob's order is smaller, round in favor of Alice's order,
so Bob will get so Bob will get
`round_down($10 * 8 CORE / $3) = round_down(26.67 CORE) = 26 CORE`, `round_down($10 * 8 CORE / $3) = round_down(26.67 CORE) = 26 CORE`,
and Alice will get
`round_up(26 CORE * $3 / 8 CORE) = round_up($9.75) = $10`,
the effective price would be `$10 / 26 CORE = $0.3846`. 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 * 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 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`, `round_down($10 * 50 CORE / $19 = round_down(26.32 CORE) = 26 CORE`,
and Alice will get
`round_up(26 CORE * $19 / 50 CORE) = round_up($9.88) = $10`,
the effective price would still be `$10 / 26 CORE = $0.3846`. 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 * If Alice's order is a call order, since it's bigger, round in favor of it,
same results. we will get same results.
### Example 2
If we change the example to this: If we change the example to this:
* Alice's order: Buy CORE at `3 CORE / $8 = 0.375`, balance `$1000000` * 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` * Bob's order: Sell CORE at `19 CORE / $50 = 0.38`, balance `10 CORE`
Process: Process:
* If both orders are limit orders, we get same results as above * If both orders are limit orders, we get similar results as above.
* If Bob's order is a call order, we should always round in favor of it, * If Bob's order is a call order, it should have a debt amount which is an
however, it should have a debt amount which is an integer, for example integer, for example `$26`, then
`$27`, then Alice would get * Alice would get
* `round_down(27 * 3 / 8) = round_down(10.125) = 10 CORE` as a maker, or * `round_up(26 * 3 / 8) = round_up(9.75) = 10 CORE` as a maker, or
* `round_down(27 * 19 / 50) = round_down(10.26) = 10 CORE` as a taker. * `round_up(26 * 19 / 50) = round_up(9.88) = 10 CORE` as a taker.
* Bob would get the full debt amount which is `$26`.
* If Bob's order is a call order, but the debt amount is a bit high,
for example `$27`, then Alice would get
* `round_up(27 * 3 / 8) = round_up(10.125) = 11 CORE` as a maker, or
* `round_up(27 * 19 / 50) = round_up(10.26) = 11 CORE` as a taker.
However, since the collateral is only `10 CORE`, this match will fail and
trigger a black swan event.
### Example 3
If we change the example to that one used above:
* Alice's order: Sell CORE at `$3 / 80 = $0.0375`, balance `50 CORE`
* Bob's order: Buy CORE at `$19 / 500 = $0.038`, balance `$100`
Assuming both orders are limit orders, they'll be processed as follows:
* If Alice's order is maker, use `$3 / 80` as match price; since Alice's order
is smaller, round in favor of Bob's order, so Alice will get
`round_down(50 CORE * $3 / 80 CORE) = round_down($1.6) = $1`,
and Bob will get `round_up($1 * 80 CORE / $3) = round_up($26.67) = $27`,
the effective price would be `$1 / 27 = $0.037`;
* If Bob's order is maker, use `$19 / 500` as match price; since Alice's order
is smaller, round in favor of Bob's order, so Alice will get
`round_down(50 CORE * $19 / 500 CORE = round_down($1.9) = $1`,
and Bob will get `round_up($1 * 500 CORE / $19) = round_up($26.3) = $27`,
the effective price would also be `$1 / 27 = $0.037`.
# Specifications # Specifications
## When Matching Two Limit Orders ## When Matching Two Limit Orders
### Handling Something-For-Nothing Issue
In `match( const limit_order_object&, OrderType ... )` function of `database` In `match( const limit_order_object&, OrderType ... )` function of `database`
class, after calculated `usd_receives` which is for the taker, class, after calculated `usd_receives` which is for the taker,
check if it is zero. check if it is zero.
If the answer is `true`, skip filling and see the order is filled, return `1`, If the answer is `true`, skip filling and see the order is filled, return `1`,
so the order will be cancelled later. so the order will be cancelled later.
### Handling Rounding Issue
In `match( const limit_order_object&, OrderType ... )` function of `database`
class, after calculated `receives` for the smaller order, if it isn't zero,
calculate `pays` for it as `round_up(receives * match_price)`.
If the smaller order is taker, after filled, even if there is still some amount
remaining in the order, see it as completely filled and set the lowest bit of
return value to `1`.
If the smaller order is maker, since it will be culled when filling,
no need to change the logic.
## When Matching A Limit Order With A Call Order ## When Matching A Limit Order With A Call Order
In `check_call_orders(...)` function of `database` class, In `check_call_orders(...)` function of `database` class,
after calculated `order_receives`, check if it is zero. if the call order is smaller, round up `order_receives`,
If the answer is `true`, otherwise round down `order_receives`.
* if `call_receives` is equal to `call_itr->debt`, set `order_receives` to `1`;
* otherwise, skip filling and cancel the limit order. In the latter case, if `order_receives` is zero, skip filling and cancel the
limit order.
## When Matching A Settle Order With A Call Order ## When Matching A Settle Order With A Call Order
In `match( const call_order_object&, ... )` function of `database` class, In `match( const call_order_object&, ... )` function of `database` class,
after calculated `call_pays`, check if it is zero. if the call order is smaller, round up `call_pays`,
If the answer is `true`, otherwise round down `call_pays`.
* if `call_receives` is equal to `call_debt`, set `call_pays` to `1`;
* otherwise, if `call_receives` is equal to `settle.balance`, In the latter case, check if `call_pays` is zero.
call `cancel_order(...)` with parameter set to `settle`, * If the answer is `true`,
then return a zero-amount collateral asset object; * if `call_receives` is equal to `settle.balance`,
* otherwise, return a zero-amount collateral asset object directly. 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.
* Otherwise, calculate `call_receives` as `round_up(call_pays * match_price)`,
then fill both orders normally. If the settle order still exists after the
match, it will be processed again later but with different condition.
After returned, need to check the amount of returned asset at where calling the After returned, need to check the amount of returned asset at where calling the
`match(...)` function, specifically, `clear_expired_orders()` function of `match(...)` function, specifically, `clear_expired_orders()` function of
@ -315,15 +429,22 @@ need to check the label, if found it's completed, process next asset.
## When Globally Settling ## When Globally Settling
In `global_settle_asset(...)` function of `database` class, check each `pays`, In `global_settle_asset(...)` function of `database` class, round up `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 ## When Paying A Settle Order From Global Settlement Fund
In `do_apply(...)` function of `asset_settle_evaluator` class, In `do_apply(...)` function of `asset_settle_evaluator` class,
after calculated `settled_amount` and adjusted it according to the "total after calculated `settled_amount` and adjusted it according to the "total
supply" rule, check if it's zero. If the answer is `true`, supply" rule, check if it's zero.
and the asset is not a prediction maket, throw a `fc::exception`.
If the answer is `true`, and the asset is not a prediction market,
throw a `fc::exception`.
If the answer is `false`, and the asset is not a prediction market,
and `op.amount.amount` is not equal to `mia_dyn.current_supply`,
calculate `pays` as `round_up(settled_amount * bitasset.settlement_price)`,
then, only deduct `pays` from total supply, and refund
`op.amount.amount - pays` to the user.
# Discussion # Discussion