-
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
Rank by surplus driver V3 #2448
Conversation
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.
Given the re-definition of all relevant data types, I wonder if we cannot create the relevant struct from the Solution and avoid relying on legacy boundary code (which we are trying to remove)
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.
Tricky to get an understanding of the overall change. Have to make another pass later today.
|
||
fn apply_factor(amount: eth::U256, factor: f64) -> Option<eth::U256> { | ||
Some( | ||
amount.checked_mul(eth::U256::from_f64_lossy(factor * 1000000000000000000.))? |
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.
You could multiply using BigRational
numbers. That way you should have accurate results without having to multiply the factor with a huge number.
Since I see you defining that function twice it might be a good idea to have that as a general helper function in our number
crate.
let limit_buy = self | ||
.executed | ||
.0 | ||
.checked_mul(self.buy.amount.into())? | ||
.checked_div(self.sell.amount.into())?; |
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.
Nit: For consistency I would have expected a scaled limit buy amount.
let limit_buy = self | |
.executed | |
.0 | |
.checked_mul(self.buy.amount.into())? | |
.checked_div(self.sell.amount.into())?; | |
let limit_buy = self | |
.buy | |
.amount | |
.0 | |
.checked_mul(self.executed.into())? | |
.checked_div(self.sell.amount.into())?; |
match self.policies.first()? { | ||
order::FeePolicy::Surplus { | ||
factor, | ||
max_volume_factor, | ||
} => Some(eth::Asset { | ||
token: match self.side { | ||
Side::Sell => self.buy.token, | ||
Side::Buy => self.sell.token, | ||
}, | ||
amount: std::cmp::min( | ||
{ | ||
// If the surplus after all fees is X, then the original | ||
// surplus before protocol fee is X / (1 - factor) | ||
apply_factor(self.surplus()?.amount.into(), factor / (1.0 - factor))? | ||
}, | ||
{ | ||
// Convert the executed amount to surplus token so it can be compared with | ||
// the surplus | ||
let executed_in_surplus_token = match self.side { | ||
Side::Sell => { | ||
self.executed.0 * self.prices.custom.sell / self.prices.custom.buy | ||
} | ||
Side::Buy => { | ||
self.executed.0 * self.prices.custom.buy / self.prices.custom.sell | ||
} | ||
}; | ||
apply_factor( | ||
executed_in_surplus_token, | ||
match self.side { | ||
Side::Sell => max_volume_factor / (1.0 - max_volume_factor), | ||
Side::Buy => max_volume_factor / (1.0 + max_volume_factor), | ||
}, | ||
)? | ||
}, | ||
) | ||
.into(), | ||
}), |
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 checked this part of the code and the logic looks good.
(I just added as comment to indicate which parts I checked.)
order::FeePolicy::Volume { factor } => Some(eth::Asset { | ||
token: match self.side { | ||
Side::Sell => self.buy.token, | ||
Side::Buy => self.sell.token, | ||
}, | ||
amount: { | ||
// Convert the executed amount to surplus token so it can be compared with | ||
// the surplus | ||
let executed_in_surplus_token = match self.side { | ||
Side::Sell => { | ||
self.executed.0 * self.prices.custom.sell / self.prices.custom.buy | ||
} | ||
Side::Buy => { | ||
self.executed.0 * self.prices.custom.buy / self.prices.custom.sell | ||
} | ||
}; | ||
apply_factor( | ||
executed_in_surplus_token, | ||
match self.side { | ||
Side::Sell => factor / (1.0 - factor), | ||
Side::Buy => factor / (1.0 + factor), | ||
}, | ||
)? | ||
} | ||
.into(), |
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 part implies a volume based fee in the surplus token. If this is consistent with what the driver does, it is fine.
There is an ongoing discussion about how to charge volume based fees when we actually switch them on. This part might need to be revised (in a different PR) after that discussion.
fn native_protocol_fee( | ||
&self, | ||
prices: &HashMap<eth::TokenAddress, auction::NormalizedPrice>, | ||
) -> Option<eth::TokenAmount> { | ||
big_rational_to_u256( | ||
&(self.protocol_fee()?.amount.0.to_big_rational() * self.surplus_token_price(prices).0), | ||
) | ||
.map(Into::into) | ||
.ok() | ||
} |
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 fine for the currently implemented volume fee in surplus token. If we go with a sell token based fee, this part would need to be revised. E.g. by adding a protocol_fee_token_price
functionality.
I pushed everything I wanted. Moving to review mode. |
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 we are getting close. I'm still somewhat worried about the amount of methods and dataypes we are introducing that future maintainers will have to understand, so. I'd like to explore if we can make a few more simplifications as to what we expose and keep most of the new logic private to the Scoring struct (currently called settled::Settlement
)
crates/e2e/tests/e2e/protocol_fee.rs
Outdated
@@ -88,46 +88,51 @@ async fn surplus_fee_sell_order_capped_test(web3: Web3) { | |||
max_volume_factor: 0.1, | |||
}; | |||
// Without protocol fee: | |||
// Expected executed_surplus_fee is 167058994203399 | |||
// Expected execution is 10000000000000000000 GNO for |
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.
Why did those tests change?
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 because of this code change: https://github.com/cowprotocol/services/pull/2448/files#diff-ceceb0ab17c45440702d1a3312aef5768ebad69027eac31792a581f9f486e0f4R159-R170
Here we skip adding the fee for sell orders.
This is decided to be done with solver team to remove the special case (deduction by observed_fee) when the volume based cap is calculation in post processing: https://github.com/cowprotocol/solver-rewards/blob/main/queries/orderbook/batch_rewards.sql#L89
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.
Btw we have an ongoing discussion again how to define trade volume more robustly and clearly.
#[error("factor {1} multiplication with {0} failed")] | ||
Factor(eth::U256, f64), | ||
#[error("overflow error while calculating protocol fee")] | ||
Overflow, | ||
#[error("division by zero error while calculating protocol fee")] | ||
DivisionByZero, |
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 we should avoid blindly adding a new error type for every potential call that could fail and instead ask us if we can group them in some sensible way. What is the advantage of having these strongly typed subtypes? I believe it's to then handle different types of errors differently (how are we realistically going to handle a TypeConversion
differently from a DivisionByZero
?)
Looking at Factor
for instance, the only way this can happen is if the multiplication overflows (so really it's the same as Error::Overflow). I wonder if even the last three should be combined to something like Math(anyhow::Error)
.
I think by and large there are three types of error here:
- Math error (overflow, underflow, div by 0, etc)
- Malformed auctions (ie. missing price, type conversions)
- Limitations of the current implementation (multiple fee policies, unsupported policies)
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.
Factor also happens when the factor = 1.0
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.
did some cleanup, not sure if good enough
.iter() | ||
.map(|trade| trade.score(prices)) | ||
.try_fold(eth::TokenAmount(eth::U256::zero()), |acc, score| { | ||
score.map(|score| acc + score) |
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.
Potential overflow of U256
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 overflow here is not realistic. Also I think we don't want to cover all theoretically possible places with checked
functions, but rather the ones that can realistically happen.
/// | ||
/// Denominated in NATIVE token | ||
pub fn score(&self, prices: &auction::NormalizedPrices) -> Result<eth::TokenAmount, Error> { | ||
Ok(self.native_surplus(prices)? + self.native_protocol_fee(prices)?) |
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.
Potential sum overflow
.ok_or(Error::Surplus(self.sell, self.buy))? | ||
.amount; | ||
let native_price = self.surplus_token_price(prices)?; | ||
big_rational_to_u256(&(surplus.0.to_big_rational() * native_price.0)) |
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.
Potential multiplication overflow
) -> Result<eth::TokenAmount, Error> { | ||
let protocol_fee = self.protocol_fee()?.amount; | ||
let native_price = self.surplus_token_price(prices)?; | ||
big_rational_to_u256(&(protocol_fee.0.to_big_rational() * native_price.0)) |
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.
Unchecked multiplication
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.
Looks good to me now (mostly nits from my side), thanks for working through all the changes.
I do think it would help this PR to not include the volume fee fixes that have been discussed, but rather base those on top of this PR so they can be reviewed separately.
sell: match trade.order().side { | ||
order::Side::Sell => trade | ||
.executed() | ||
.0 | ||
.checked_mul(uniform_prices.sell) | ||
.ok_or(Math::Overflow)? | ||
.checked_div(uniform_prices.buy) | ||
.ok_or(Math::DivisionByZero)?, | ||
order::Side::Buy => trade.executed().0, | ||
}, | ||
buy: match trade.order().side { | ||
order::Side::Sell => trade.executed().0 + trade.fee().0, | ||
order::Side::Buy => { | ||
(trade.executed().0) | ||
.checked_mul(uniform_prices.buy) | ||
.ok_or(Math::Overflow)? | ||
.checked_div(uniform_prices.sell) | ||
.ok_or(Math::DivisionByZero)? | ||
+ trade.fee().0 |
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.
nit: Do we need this logic elsewhere? Maybe it would make sense to expose custom_clearing_prices(uniform_prices)
as a method on Trade
?
Then one could probably even think about merging the two Trade
domain models (if they are semantically the same, since it looks like scoring::Trade
is mainly derived from solution::Trade
all that would be missing is to expose net_executed()
amounts after fee on Trade). However, I believe this can also be done in a later refactoring.
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.
Then one could probably even think about merging the two Trade domain models
We slowly converged to calculating score on fulfillment trade. :) However, this was not my intention at first. Initially, I wanted to do scoring strictly on different type, a type that would be final after all driver operations done on the solution received from solver. But since we are moving away from driver making any changes to the solver solutions, I guess this approach makes sense until we are careful about adding any features that could alter the score.
.surplus_token_price(prices)? | ||
.apply(self.protocol_fee()?.amount); | ||
// normalize | ||
Ok((protocol_fee.0 / *UNIT).into()) |
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.
ditto
#[error("factor {1} multiplication with {0} failed")] | ||
Factor(eth::U256, f64), | ||
#[error(transparent)] | ||
Math(#[from] super::Math), |
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.
Can we replace Factor and use Math in those cases (Factor can only happen in case of Overflow)?
Conceptually, this Math error is equivalent to solution::Error::Math
, so I think it would even make sense to refactor our solution domain's error model. However I don't think this is blocking this PR.
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.
Factor also covers f64::inf case which helped me few times during testing.
lazy_static::lazy_static! { | ||
static ref UNIT: eth::U256 = eth::U256::from(1_000_000_000_000_000_000_u128); | ||
} |
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.
Remove in favor of reusing ether_price
, it's also defined in
services/crates/solvers/src/domain/auction.rs
Lines 76 to 77 in 7304593
const BASE: u128 = 10_u128.pow(18); | |
# Description Extracted from #2448 to be a separate PR. In order to calculate the protocol fees based on the onchain settlement, solver rewards script needs to make a weird adjustment for sell orders (so that it's aligned with driver): https://github.com/cowprotocol/solver-rewards/blob/main/queries/orderbook/batch_rewards.sql#L89 (deduct the observed fee). In order to make the protocol fee calculation from autopilot side nice and clean, we need to skip adding fee to sell orders. ## How to test Updated protocol fee tests.
I believe I addressed all relevant code review comments (will address more nits in a follow up). |
Replacement for #2406 and #2421
Implements task 1 from #2382
Fixes #1494
Description
Implements surplus and protocol fees calculation, based on the solution (received from solvers) trades, which are in a settlement smart contract format. This ensures that these values are calculated after the protocol fee is already applied and the whole settlement is prepared for settling. This is important because the autopilot (and reward script) will use the same math so we are aligned 1:1.
The main calculation is in
crates/driver/src/domain/competition/settled/mod.rs
.CIP38 scoring is currently called in a read only mode. The score is calculated but not used.
How to test
Tested locally with e2e tests. Surplus + protocol fee is always equal to legacy
quality
-surplus_fee
which is expected.