-
Notifications
You must be signed in to change notification settings - Fork 93
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
Fix volume based cap calculation #2465
Changes from 2 commits
72428a5
a586fc8
56a55ed
e6ee605
dbd9eca
e96eef4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,7 +39,10 @@ use { | |
impl Fulfillment { | ||
/// Applies the protocol fee to the existing fulfillment creating a new one. | ||
pub fn with_protocol_fee(&self, prices: ClearingPrices) -> Result<Self, Error> { | ||
let protocol_fee = self.protocol_fee(prices)?; | ||
let protocol_fee = { | ||
let fee = self.protocol_fee(prices)?; | ||
self.in_sell_token(fee, prices)? | ||
}; | ||
|
||
// Increase the fee by the protocol fee | ||
let fee = match self.surplus_fee() { | ||
|
@@ -74,6 +77,7 @@ impl Fulfillment { | |
Fulfillment::new(order, executed, fee).map_err(Into::into) | ||
} | ||
|
||
/// Computed protocol fee in surplus token. | ||
fn protocol_fee(&self, prices: ClearingPrices) -> Result<eth::U256, Error> { | ||
// TODO: support multiple fee policies | ||
if self.order().protocol_fees.len() > 1 { | ||
|
@@ -113,6 +117,8 @@ impl Fulfillment { | |
|
||
/// Computes protocol fee compared to the given limit amounts taken from | ||
/// the order or a quote. | ||
/// | ||
/// The protocol fee is computed in surplus token. | ||
fn calculate_fee( | ||
&self, | ||
limit_sell_amount: eth::U256, | ||
|
@@ -130,6 +136,7 @@ impl Fulfillment { | |
Ok(protocol_fee) | ||
} | ||
|
||
/// Computes the surplus fee in the surplus token. | ||
fn fee_from_surplus( | ||
&self, | ||
sell_amount: eth::U256, | ||
|
@@ -138,33 +145,58 @@ impl Fulfillment { | |
factor: f64, | ||
) -> Result<eth::U256, Error> { | ||
let surplus = self.surplus_over_reference_price(sell_amount, buy_amount, prices)?; | ||
let surplus_in_sell_token = self.surplus_in_sell_token(surplus, prices)?; | ||
apply_factor(surplus_in_sell_token, factor) | ||
apply_factor(surplus, factor) | ||
} | ||
|
||
/// Computes the volume based fee in surplus token | ||
/// | ||
/// The volume is defined as a full sell amount (including fees) conversion | ||
/// to the full buy amount (user point of view) | ||
fn fee_from_volume(&self, prices: ClearingPrices, factor: f64) -> Result<eth::U256, Error> { | ||
let executed = self.executed().0; | ||
let executed_sell_amount = match self.order().side { | ||
let executed_in_surplus_token = match self.order().side { | ||
Side::Buy => { | ||
// How much `sell_token` we need to sell to buy `executed` amount of `buy_token` | ||
executed | ||
.checked_mul(prices.buy) | ||
.ok_or(trade::Error::Overflow)? | ||
.checked_div(prices.sell) | ||
.ok_or(trade::Error::DivisionByZero)? | ||
.checked_add( | ||
// surplus_fee is always expressed in sell token | ||
self.surplus_fee() | ||
.map(|fee| fee.0) | ||
.ok_or(trade::Error::ProtocolFeeOnStaticOrder)?, | ||
) | ||
.ok_or(trade::Error::Overflow)? | ||
} | ||
Side::Sell => executed, | ||
Side::Sell => { | ||
// How much `buy_token` we get when we sell `executed` amount of `sell_token` | ||
executed | ||
.checked_mul(prices.sell) | ||
.ok_or(trade::Error::Overflow)? | ||
.checked_div(prices.buy) | ||
.ok_or(trade::Error::DivisionByZero)? | ||
} | ||
}; | ||
apply_factor(executed_in_surplus_token, factor) | ||
} | ||
|
||
/// Returns the fee denominated in the sell token. | ||
pub fn in_sell_token( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't need to be public, and I'm not a fan of putting If we want to build an abstraction, maybe it could make sense to expose a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
But the function also requires I created the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You wouldn't need side if all functions returned let protocol_fee = self.protocol_fee_in_sell_token(prices)?; becomes let protocol_fee = self.protocol_fee().in_token(self.order().sell.token, prices) (for buy order protocol fee will be already in sell so ✨ But I'm also ok leaving this as is and leave this refactoring up for another time™️ |
||
&self, | ||
fee: eth::U256, | ||
prices: ClearingPrices, | ||
) -> Result<eth::U256, Error> { | ||
let fee_in_sell_token = match self.order().side { | ||
Side::Buy => fee, | ||
Side::Sell => fee | ||
.checked_mul(prices.buy) | ||
.ok_or(trade::Error::Overflow)? | ||
.checked_div(prices.sell) | ||
.ok_or(trade::Error::DivisionByZero)?, | ||
}; | ||
// Sell slightly more `sell_token` to capture the `surplus_fee` | ||
let executed_sell_amount_with_fee = executed_sell_amount | ||
.checked_add( | ||
// surplus_fee is always expressed in sell token | ||
self.surplus_fee() | ||
.map(|fee| fee.0) | ||
.ok_or(trade::Error::ProtocolFeeOnStaticOrder)?, | ||
) | ||
.ok_or(trade::Error::Overflow)?; | ||
apply_factor(executed_sell_amount_with_fee, factor) | ||
Ok(fee_in_sell_token) | ||
} | ||
} | ||
|
||
|
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 still don't see why this makes sense. Since solvers can trade-off fees and surplus 1:1 I don't understand why we exclude fees here (and only on one order type).
Wouldn't the following two executions be equivalent for the user/protocol but result in very different fees?
User Order: Sell 1 ETH for at least 3500 DAI
Maybe we shouldn't be using the clearing prices here, but instead the fee-adjusted prices and then add the fee back in both cases?
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 your example, since it's a sell order, volume based fee is in buy token, so both times volume is 3.5k DAI.
Can't use fee-adjusted prices since those do not exist yet and this code is used to determine the fee-adjusted prices.
I think the asymmetry (adding fee to buy case and not adding to sell case) comes from the fact that the fee is expressed/taken from only one of the sides, namely sell side.
Similar asymmetry exists in the functions that determine the traded amounts: https://github.com/cowprotocol/services/blob/main/crates/driver/src/domain/competition/solution/trade.rs#L104-L136
So, for sell orders, if we want to know the volume in buy token, we need to use uniform prices for conversion of executed amount. For buy orders, if we want to know the volume in sell token, we need to use custom prices. Since we don't have custom prices, alternative is using uniform prices but adding fee manually.
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.
Oh nice, I think this would actually make things a lot easier to understand. We take the volume in the surplus token so
makes a lot of sense and is much easier to write. It does require refactoring the different ways of passing prices (full vector vs. just the two prices) but this also makes sense imo to be more coherent across the domain.
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.
Refactored