-
Notifications
You must be signed in to change notification settings - Fork 648
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement BSIP 74: Margin Call Fee Ratio #2130
Conversation
|
Conflicts don't show on command line, so not sure what's going on there. I will research the Travis difficulties. NOTE: I'd like to see BSIP 86 merged in first, as I'd like to use some of those changes in this. Also, there will be conflicts after the BSIP 86 merge as the same code was touched. |
I've merged bsip86 just now. |
Note: The dockercloud failure looks to be a timeout. The build ran for 116 minutes, and then stopped with no real error message.
|
The conflicts need to be fixed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I stopped review so far due to the mismatch between the implementation and the specification.
libraries/chain/db_market.cpp
Outdated
|| *trade_asset.options.extensions.value.margin_call_fee_ratio == 0 ) | ||
return asset(0, trade_amount.asset_id); | ||
|
||
auto value = detail::calculate_percent(trade_amount.amount, *trade_asset.options.extensions.value.margin_call_fee_ratio); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems different from the specification.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem I have is clarifying the specifications. I apologize for the lengthy post, but it is how my mind works. The BSIP says:
When a margin call trading happens, the buyer sells smartcoin with quantity X and get collaterals in quantity
X*(MSSR-MCFR)/settlement price
, the margin call order owner sells collaterals in quantityX*MSSR/settlement price
and get smartcoin in quantity X, the delta in paid and received collaterals in quantityX*MCFR/settlement price
will be paid to the owner of the smartcoin as margin call fee.
My interpretation (which I will attempt to keep updated within this comment, and probably move to my blog to help me in the future):
Doug the Debtor put up collateral of token C which created token D. Doug was charged an operation fee I think, although I am not sure but it is unimportant. We will assume Doug then sold or transferred D ( also unimportant in this context).
Larry the creator of a Limit order would like to sell the desired quantity of token D and receive the desired quantity of token C. Upon placing the order, Larry was charged a limit order create fee, which is not important in this context.
When the debt position was created, a call order was created for Doug the debtor. Within that call order is the current debt and collateral balances.
When the settlement price of the price feed reaches a certain level, Doug's position will get called. Such a margin call will generate revenue for the asset issuer, if the issuer has set the new margin_call_fee_ratio
within that asset's options.
This fee does not affect Larry. He simply placed a limit order on the book of the exchange.
Doug will see the fee if he compares the amount of collateral he received from the transaction with the amount of debt that was paid off by the margin call.
Now the more technical details:
When will these orders be matched? When the settlement price from the price feed pushes the value of Doug's collateral below the allowed threshold for the debt asset (see db_market.cpp#database::check_call_orders
).
What will Larry receive? Exactly what he wished in his limit order.
What will Doug receive? Relief from (some or all of) his debt, and some of his collateral back. He will not receive the full value of his collateral, based on the feed's settlement price. Some of the value of the transaction was paid to the issuer of the debt asset as a margin call fee.
Implementation details:
database::check_call_orders (db_market.cpp) knows when to trigger a margin call. When triggered, fill_call_order
is called, and then fill_limit_order
is called.
Within fill_call_order
the fee is taken from the debtor's debt position and given to the asset issuer as a vesting balance. The remaining value of the debt is used to calculate the collateral that the debtor receives. That collateral is then placed in the debtor's available account balance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In looking at the spec, I believe that I need to adjust the "trigger" of the margin call. The spec reads:
Margin call order price = settlement price/(MSSR-MCFR)
And that is part of check_call_orders
Update: No, this only affects the debtor, not when the margin call happens.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I may have written something wrong in previous comments. Let's forget them and start here.
With bsip74,
- condition when a call order is possible to be matched does not change, it's still call_price >= feed_price;
- condition where (at what price, aka match_price) the call order be matched does change,
- previously, if a limit order buys at
feed_price / mssr
, it will match the call order, - now, if a limit order buys at
feed_price / (mssr - mcfr)
, it will match the call order,
- previously, if a limit order buys at
- when matched, the limit order gets collateral (be filled) at
feed_price / (mssr - mcfr)
, the call order pays (be filled) atfeed_price / mssr
, the difference goes to the debt asset owner; - the call order can be a maker or a taker, there is a small difference in match_price, iirc maker and taker are handled in different functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One important thing, the fee is some amount of the collateral asset, so it can not go to the accumulated_fees
field of the debt asset, which means we likely can not reuse the code of pay_market_fees
. It can not go to the accumulated_fees
field of the collateral asset either, since the asset can be owned by someone else.
In addition, a complex scenario is the debt asset can change from one collateral to another, so we need to store the fees in a multi-asset container. Adding new fields into asset_object or another object means we need to add new operations to claim them, that's too much work to implement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another thing in mind: mssr-mcfr
in the specification, and we don't allow something for nonthing, so it's implied that mssr>mcfr
. However, mssr is updated by price feed producers and an internal timer, mcfr is updated by asset owner, so likely we can't validate when one of them is updated. So we need to cap the value of mcfr when use it in calculation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe I am clear that this change modifies which limit orders qualify to be matched.
To be clear, let me walk through what I am seeing in db_market.cpp::check_call_orders. And just focusing on the price the limit order will transact at...
Prior to this change, the limit order was filled at the price dictated by the order itself:
price match_price = limit_order.sell_price;
Based on what I'm reading in your comment above, after the hardfork, the limit order will transact at a price based on feed price, MSSR and MCFR. That will result in the limit order receiving a price potentially very different than their order.
Or it could be that I am misreading your comment above, and you were only presenting the extreme example of when a limit order just happens to match right at the new calculated price for what qualifies a match, which would mean the order gets exactly what it asked for.
Or I am making assumptions in the code that are incorrect.
Would you clarify please?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please keep in mind that match price is always price of the maker, see BSIP 32.
- In check_call_orders, the call order is the taker and the limit order is the maker, so the match price will always be the price of the limit order.
- In another function where the call order is the maker and the limit order is the taker, the match price will be settlement_price / (mssr-mcfr).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the reference to BSIP 32. There are some inconsistencies in that BSIP that make it difficult to understand, but I believe the gist is as you say, when the limit order is the maker, the maker price is what is used.
So that leads me to believe that BSIP 74 applies only when the call order is the maker. Hence the margin call fee will not apply when the limit order is the maker. Is that correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO the margin call fee should always apply, although how much to pay is not clearly described in the BSIP when Peter approved the pull request.
In case when the call order is taker, I think it's fine to charge the amount filled_debt * mcfr / feed_price
from the call order as fee.
@@ -52,6 +53,7 @@ struct proposal_operation_hardfork_visitor | |||
// hf_1774 | |||
void operator()(const graphene::chain::asset_update_operation &v) const { | |||
detail::check_asset_options_hf_1774(block_time, v.new_options); | |||
detail::check_asset_options_bsip74(block_time, v.new_options); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing code about asset_create_operation
(a few lines above).
@abitmore I would appreciate a review of this. Please take special note of bitasset_tests/bsip74_insufficient_collateral_tests (around line 1956) I am unsure if I have coded something incorrectly, or if we haven't talked about this particular case. Call order is taker, fee takes more collateral than what is left. Meanwhile I will continue researching to attempt to see if it is my misunderstanding. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, the code doesn't look good.
Due to the new MCFR parameter, new code should include logic changes about matching/filling price, but I didn't see code about it in this pull request.
Margin call fees should be always paid by the debt position owner, but not the limit order owner. The fee should be stored in fee
field of the virtual fill_order_operation
which contains the call order.
The limit order still needs to pay market fee when being matched with a call order.
I didn't review test cases.
a *= (ratio-GRAPHENE_COLLATERAL_RATIO_DENOM); | ||
a /= GRAPHENE_COLLATERAL_RATIO_DENOM; | ||
return static_cast<int64_t>(a); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function name is misleading due to the ratio-GRAPHENE_COLLATERAL_RATIO_DENOM
calculation in the implementation. By the way ratio-GRAPHENE_COLLATERAL_RATIO_DENOM
may underflow.
* @param feed the debt asset's price feed | ||
* @returns the max short squeeze price | ||
*/ | ||
price database::get_max_short_squeeze_price( const fc::time_point_sec& block_time, const price_feed& feed)const |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's better to move this function from database
class to price_feed
class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about that, but all other HARDFORK code (which is the basis for this function) has been kept away from the protocol code. Should I break that rule?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, protocol code doesn't include chain/hardfork.hpp. We usually check the time in chain code and convert the result to another meaningful parameter to pass in.
@@ -685,7 +690,7 @@ int database::match( const limit_order_object& bid, const call_order_object& ask | |||
order_pays = call_receives; | |||
|
|||
int result = 0; | |||
result |= fill_limit_order( bid, order_pays, order_receives, cull_taker, match_price, false ); // the limit order is taker |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's better to keep the comment.
libraries/chain/db_market.cpp
Outdated
auto issuer_fees = pay_market_fees(&seller, recv_asset, receives, is_maker); | ||
asset issuer_fees; | ||
if (!is_maker && is_margin_call) | ||
issuer_fees = pay_margin_fees(pays.asset_id(*this), receives ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The margin call fee should be stored in the fee
field of the fill_order_operation
which contains the call order, but not the limit order.
Market fees is unrelated to the margin call fee. The limit order should pay market fee regardless.
libraries/chain/db_market.cpp
Outdated
* @param receives what the call order will receive from the other party (debt) | ||
* @param fill_price the price at which the call order will execute | ||
* @param is_maker TRUE if the call order is the maker, FALSE if it is the taker | ||
* @param is_margin_call TRUE if this method was called due to a margin call |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't fully understand what's your definition of "a margin call" nor the logic around this parameter. Looks like it is always equal to !is_maker
.
libraries/chain/db_market.cpp
Outdated
|| !ba.options.extensions.value.margin_call_fee_ratio.valid()) | ||
return asset(0); | ||
auto ratio = ba.adjusted_mcfr(price_feed); | ||
auto amount = detail::calculate_ratio( collateral.amount, ratio ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is incorrect.
All the specs and additional comments say fee = debt / feed_price * fee_rate
, but not collateral * fee_rate
, because debt / feed_price != collateral
due to MSSR
.
libraries/chain/db_market.cpp
Outdated
|| price_feed.settlement_price.base.amount == 0 | ||
|| !ba.options.extensions.value.margin_call_fee_ratio.valid()) | ||
return asset(0); | ||
auto ratio = ba.adjusted_mcfr(price_feed); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's inefficient to adjust the param on every order. Since it's only related to the price feed, it's best to save an adjusted value whenever the price feed changes or the param itself changes.
@@ -103,6 +104,7 @@ namespace graphene { namespace protocol { | |||
|
|||
struct ext | |||
{ | |||
fc::optional<uint16_t> margin_call_fee_ratio; // BSIP 74 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although this time it doesn't matter since all parameters are newly added, we should always add new parameters to the end of the structs used by extensions
, otherwise it breaks consensus.
By the way, need new code in the cpp file to validate the new parameter.
@@ -283,7 +283,6 @@ namespace graphene { namespace protocol { | |||
|
|||
price price_feed::max_short_squeeze_price()const | |||
{ | |||
// settlement price is in debt/collateral |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's better to keep the comment.
Thank you for your review. Please pause your review while I will attempt to understand then implement your changes. |
My reading of the specification contradicts the idea that the debt position owner should pay the margin call fee. The spec mandates that the call order pays Unless the argument is that, in accordance with " the margin call orders should be placed at the real price without margin call fee to avoid misleading the buyer", the buyer's/limit order owner's expectation of the "real price" is |
Yes. |
@abitmore We have a draft of this implementation which is causing two previously existing margin call tests (1 and 2) to fail when BSIP74 activates. BSIP74 is being activated, indirectly, by the these BSIP77 tests when the tests advance to the BSIP77 hardfork activation time, which is after the BSIP74 time. (Separately we should discuss synchronizing these future activation times to test for potential interactions of new features.) These two BSIP77 tests are failing because they were expecting the margin call order to be matched against the standing limit order (no conflict here) and filled at the limit order's asking price (conflict here). In these examples, these current tests are expecting "filled core" of 105 which is the minimum being asked by the standing limit order. However the draft BSIP74 implementation is filling at the effective price of settlement_price/(MSSR-MCFR); in these BSIP77 tests, MCFR = 0, so the effective price is settlement_price/MSSR. Should we modify these BSIP77 tests with a check about whether BSIP74 is also activated? And then if BSIP74 is also activated in the test, change the test's literal values from, for example,
|
@MichelSantos wrote:
IMO, when MCFR is zero, all existing test cases should pass. According to BSIP32, in any case, the matching/filling price is the maker price (also see an earlier comment: #2130 (comment)). BSIP74 doesn't change it. If it is not clear in the specification of BSIP74, I think it's better to make it clear via bitshares/bsips#273. |
Follow-up here |
Closing in favor of #2180. |
Fixes #2129
Implements a margin call fee per BSIP 74 specifications
To Test: