diff --git a/README.md b/README.md index fa86104..05d9fa2 100644 --- a/README.md +++ b/README.md @@ -42,4 +42,4 @@ Number | Title | [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 [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 diff --git a/bsip-0035.md b/bsip-0035.md index dd5ab11..d0a6e21 100644 --- a/bsip-0035.md +++ b/bsip-0035.md @@ -1,5 +1,5 @@ BSIP: 0035 - Title: A Solution To Something-For-Nothing Issue + Title: Mitigate Rounding Issue On Order Matching Author: Abit More Status: Draft Type: Protocol @@ -12,17 +12,15 @@ # 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. +one order may be paying more than enough, even paying something but receiving +nothing. This looks unfair. -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 -completely. - -This BSIP also sets a principle: something-for-nothing shouldn't happen when -matching orders. +This BSIP also sets two principles for order matching: +* never pay more than enough, and +* something-for-nothing shouldn't happen. # 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 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 ## 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`, 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 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 @@ -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 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): -* 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)** - * 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.** - * 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 - * If the settling amount is equal to total supply of that asset, pay the +Something-for-nothing is only a subset of rounding issues, it's the most extreme +one. There are much more scenarios that one of the matched parties would be +paying more than enough, although they're not paying something for nothing +overall. Some scenarios are discussed in [bitshares-core +issue #342](https://github.com/bitshares/bitshares-core/issues/342). + +Take a scenario similar to the one described in the 4th comment of +[bitshare-core +issue #132](https://github.com/bitshares/bitshares-core/issues/132) as an +example: +* 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` + +Current system would process them 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 pay the whole `50 + CORE` and get `round_down(50 CORE * $3 / 80 CORE) = round_down($1.6) = $1`, + the effective price would be `$1 / 50 = $0.02`; +* 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 pay the whole `50 + CORE` and get `round_down(50 CORE * $19 / 500 CORE = round_down($1.9) = $1`, + the effective price would still be `$1 / 50 = $0.02`. + +Both results are far from Alice's desired price `$0.0375`. Actually, according +to Bob's desired price, paying `round_up($1 * 500 CORE / $19) = 27 CORE` would +be enough, then the effective price would be `$1 / 27 = $0.037`, which is +still below Alice's desired price `$0.0375`, but much closer than `$0.02`. + +## The Improved Solution Proposed By This BSIP + +The detailed rules proposed by this BSIP with new rules highlighted: + +* 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; - * **when the asset is not a prediction market, if the settle order would - receive nothing, raise an exception (aka let the operation fail).** + * otherwise, in favor of global settlement fund since its volume is bigger, + 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 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, so Bob will get `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`. * 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`, + 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`. -* If Alice's order is a call order, always round in favor of it, we get - same results. +* If Alice's order is a call order, since it's bigger, round in favor of it, + we will get same results. + +### Example 2 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. +* If both orders are limit orders, we get similar results as above. +* If Bob's order is a call order, it should have a debt amount which is an + integer, for example `$26`, then + * Alice would get + * `round_up(26 * 3 / 8) = round_up(9.75) = 10 CORE` as a maker, or + * `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 ## When Matching Two Limit Orders +### Handling Something-For-Nothing Issue + 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. +### 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 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. +if the call order is smaller, round up `order_receives`, +otherwise round down `order_receives`. + +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 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. +if the call order is smaller, round up `call_pays`, +otherwise round down `call_pays`. + +In the latter case, check if `call_pays` is zero. +* If the answer is `true`, + * 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. +* 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 `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 -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`. +In `global_settle_asset(...)` function of `database` class, round up `pays`. ## When Paying A Settle Order From Global Settlement Fund In `do_apply(...)` function of `asset_settle_evaluator` class, after calculated `settled_amount` and adjusted it according to the "total -supply" rule, check if it's zero. If the answer is `true`, -and the asset is not a prediction maket, throw a `fc::exception`. +supply" rule, check if it's zero. + +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