Skip to content

Commit abec4f9

Browse files
authored
Merge pull request #8 from cowprotocol/fix/score-computation
Fixed bug in score computation with price improvement fees
2 parents ce6daa7 + 9797d43 commit abec4f9

File tree

2 files changed

+108
-15
lines changed

2 files changed

+108
-15
lines changed

circuit_breaker_validator/models.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -141,34 +141,50 @@ def surplus_token(self) -> HexBytes:
141141
raise ValueError(f"Order kind {self.kind} is invalid.")
142142

143143
def price_improvement(self, quote: Quote) -> int:
144-
"""Compute price improvement compared to a reference quote.
144+
"""Compute price improvement compared to a reference quote and limit price.
145145
146-
Price improvement measures how much better the executed trade is compared to quote.
147-
For sell orders: price_improvement = executed_buy_amount - expected_buy_amount_from_quote
148-
For buy orders: price_improvement = expected_sell_amount_from_quote - executed_sell_amount
146+
Price improvement measures how much better the executed trade is compared to the better
147+
of quote and limit price. The smaller improvement value is used, as that results in a
148+
smaller fee.
149+
150+
For sell orders: price_improvement =
151+
executed_buy_amount - max(buy_amount_from_quote, limit_buy_amount)
152+
For buy orders: price_improvement =
153+
min(sell_amount_from_quote, limit_sell_amount) - executed_sell_amount
149154
150155
The calculation uses the effective amounts from the quote that account for gas fees:
151156
1. Get effective sell and buy amounts from the quote
152157
2. Compare the actual trade to the hypothetical quote-based trade
158+
3. Compare with the limit price (surplus)
159+
4. Return the minimum of the two improvements
153160
154161
For partially fillable orders, rounding is such that the reference for computing price
155162
improvement is as if the quote would determine the limit price. That means that for sell
156163
orders the quote buy amount is rounded up and for buy orders the quote sell amount is
157164
rounded down.
158165
"""
166+
# Calculate price improvement compared to quote
159167
effective_sell_amount = quote.effective_sell_amount(self.kind)
160168
effective_buy_amount = quote.effective_buy_amount(self.kind)
161169
if self.kind == "sell":
162170
current_limit_quote_amount = math.ceil(
163171
effective_buy_amount * Fraction(self.sell_amount, effective_sell_amount)
164172
)
165-
return self.buy_amount - current_limit_quote_amount
166-
if self.kind == "buy":
173+
quote_price_improvement = self.buy_amount - current_limit_quote_amount
174+
elif self.kind == "buy":
167175
current_quote_sell_amount = int(
168176
effective_sell_amount * Fraction(self.buy_amount, effective_buy_amount)
169177
)
170-
return current_quote_sell_amount - self.sell_amount
171-
raise ValueError(f"Order kind {self.kind} is invalid.")
178+
quote_price_improvement = current_quote_sell_amount - self.sell_amount
179+
else:
180+
raise ValueError(f"Order kind {self.kind} is invalid.")
181+
182+
# Calculate price improvement compared to limit price (surplus)
183+
limit_price_improvement = self.surplus()
184+
185+
# Return the minimum of the two improvements
186+
# "Better" means smaller improvement, as that results in a smaller fee
187+
return min(quote_price_improvement, limit_price_improvement)
172188

173189

174190
class FeePolicy(ABC):
@@ -328,6 +344,8 @@ def reverse_protocol_fee(self, trade: OnchainTrade) -> OnchainTrade:
328344
Returns a new trade object with the fee reversed.
329345
"""
330346
new_trade = deepcopy(trade)
347+
348+
# Calculate price improvement compared to quote
331349
price_improvement = trade.price_improvement(self.quote)
332350
volume = trade.volume()
333351
price_improvement_fee = max(

tests/test_models.py

Lines changed: 82 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,22 @@ def test_trade_volume(kind, amount_field, expected_volume):
8484

8585

8686
@pytest.mark.parametrize(
87-
"kind,sell_amount,buy_amount,quote_sell_amount,quote_buy_amount,quote_fee_amount,expected_improvement",
87+
"kind,sell_amount,buy_amount,quote_sell_amount,quote_buy_amount,quote_fee_amount,surplus_value,expected_improvement",
8888
[
89+
# Basic cases
8990
# sell order
90-
("sell", 50, 5100, 100, 10000, 0, 100),
91+
("sell", 50, 5100, 100, 10000, 0, 100, 100),
9192
# buy order
92-
("buy", 49, 5000, 100, 10000, 0, 1),
93+
("buy", 49, 5000, 100, 10000, 0, 1, 1),
94+
# Cases where surplus is smaller than quote price improvement
95+
# sell order: surplus (1) < quote improvement (2)
96+
("sell", 100, 101, 100, 99, 0, 1, 1),
97+
# buy order: surplus (1) < quote improvement (2)
98+
("buy", 99, 100, 101, 100, 0, 1, 1),
99+
# sell order: large difference - surplus (5) < quote improvement (10)
100+
("sell", 100, 105, 100, 95, 0, 5, 5),
101+
# Edge case: sell order with zero surplus (0) < quote improvement (5)
102+
("sell", 100, 100, 100, 95, 0, 0, 0),
93103
],
94104
)
95105
def test_trade_price_improvement(
@@ -99,13 +109,24 @@ def test_trade_price_improvement(
99109
quote_sell_amount,
100110
quote_buy_amount,
101111
quote_fee_amount,
112+
surplus_value,
102113
expected_improvement,
103114
):
104-
"""Test price improvement calculation for different order kinds."""
115+
"""Test price improvement calculation for different order kinds.
116+
117+
This test verifies that price_improvement returns the minimum of:
118+
1. Quote price improvement (difference between executed and quote)
119+
2. Limit price improvement (surplus)
120+
121+
It includes cases where surplus is smaller than quote price improvement,
122+
which should result in the surplus value being returned.
123+
"""
105124
trade = Mock(spec=OnchainTrade)
106125
trade.sell_amount = sell_amount
107126
trade.buy_amount = buy_amount
108127
trade.kind = kind
128+
# Mock surplus to return the specified value
129+
trade.surplus = Mock(return_value=surplus_value)
109130

110131
quote = Quote(
111132
sell_amount=quote_sell_amount,
@@ -214,14 +235,15 @@ def test_surplus_protocol_fee(
214235

215236

216237
@pytest.mark.parametrize(
217-
"kind,amount_field,price_improvement_value,is_capped,price_improvement_factor,volume_factor,expected_formula",
238+
"kind,amount_field,price_improvement_value,surplus_value,is_capped,price_improvement_factor,volume_factor,expected_formula",
218239
[
219240
# Not capped cases (price improvement fee < volume fee)
220241
# sell order - not capped
221242
(
222243
"sell",
223244
"buy_amount",
224245
5 * 10**18, # positive price improvement
246+
5 * 10**18, # surplus equals price improvement
225247
False,
226248
Fraction(0.5),
227249
Fraction(0.5),
@@ -233,6 +255,7 @@ def test_surplus_protocol_fee(
233255
"buy",
234256
"sell_amount",
235257
5 * 10**18, # positive price improvement
258+
5 * 10**18, # surplus equals price improvement
236259
False,
237260
Fraction(0.5),
238261
Fraction(0.5),
@@ -245,6 +268,7 @@ def test_surplus_protocol_fee(
245268
"sell",
246269
"buy_amount",
247270
5 * 10**18, # positive price improvement
271+
5 * 10**18, # surplus equals price improvement
248272
True,
249273
Fraction(0.5),
250274
Fraction(0.01),
@@ -256,6 +280,7 @@ def test_surplus_protocol_fee(
256280
"buy",
257281
"sell_amount",
258282
5 * 10**18, # positive price improvement
283+
5 * 10**18, # surplus equals price improvement
259284
True,
260285
Fraction(0.5),
261286
Fraction(0.01),
@@ -268,6 +293,7 @@ def test_surplus_protocol_fee(
268293
"sell",
269294
"buy_amount",
270295
-5 * 10**18, # negative price improvement
296+
-5 * 10**18, # surplus equals price improvement
271297
False,
272298
Fraction(0.5),
273299
Fraction(0.01),
@@ -278,17 +304,55 @@ def test_surplus_protocol_fee(
278304
"buy",
279305
"sell_amount",
280306
-5 * 10**18, # negative price improvement
307+
-5 * 10**18, # surplus equals price improvement
281308
False,
282309
Fraction(0.5),
283310
Fraction(0.01),
284311
lambda amount, pi, pi_factor, v_factor: amount, # No change
285312
),
313+
# Cases where surplus is smaller than price improvement
314+
# sell order - surplus (1) < price improvement (2)
315+
(
316+
"sell",
317+
"buy_amount",
318+
2 * 10**18, # quote price improvement
319+
1 * 10**18, # surplus (smaller)
320+
False,
321+
Fraction(0.5),
322+
Fraction(0.5),
323+
lambda amount, pi, pi_factor, v_factor: amount
324+
+ round(pi_factor / (1 - pi_factor) * (1 * 10**18)), # Use surplus value
325+
),
326+
# buy order - surplus (1) < price improvement (2)
327+
(
328+
"buy",
329+
"sell_amount",
330+
2 * 10**18, # quote price improvement
331+
1 * 10**18, # surplus (smaller)
332+
False,
333+
Fraction(0.5),
334+
Fraction(0.5),
335+
lambda amount, pi, pi_factor, v_factor: amount
336+
- round(pi_factor / (1 - pi_factor) * (1 * 10**18)), # Use surplus value
337+
),
338+
# Edge case: sell order with zero surplus (0) < price improvement (5)
339+
(
340+
"sell",
341+
"buy_amount",
342+
5 * 10**18, # quote price improvement
343+
0, # zero surplus
344+
False,
345+
Fraction(0.5),
346+
Fraction(0.5),
347+
lambda amount, pi, pi_factor, v_factor: amount, # No fee applied
348+
),
286349
],
287350
)
288351
def test_price_improvement_protocol_fee(
289352
kind,
290353
amount_field,
291354
price_improvement_value,
355+
surplus_value,
292356
is_capped,
293357
price_improvement_factor,
294358
volume_factor,
@@ -300,6 +364,8 @@ def test_price_improvement_protocol_fee(
300364
1. Not capped cases - where price improvement fee is less than volume fee
301365
2. Capped cases - where price improvement fee is greater than volume fee
302366
3. Negative price improvement cases - where no fee is applied
367+
4. Cases where surplus is smaller than quote price improvement - should use the smaller value
368+
5. Edge case with zero surplus - should apply no fee
303369
"""
304370
amount = 15 * 10**18
305371

@@ -318,13 +384,22 @@ def test_price_improvement_protocol_fee(
318384
setattr(trade, amount_field, amount)
319385
trade.kind = kind
320386
trade.volume = Mock(return_value=amount)
321-
trade.price_improvement = Mock(return_value=price_improvement_value)
387+
388+
# Set up price_improvement to return the minimum of quote price improvement and surplus
389+
# This matches the actual behavior of the price_improvement method
390+
min_improvement = min(price_improvement_value, surplus_value)
391+
trade.price_improvement = Mock(return_value=min_improvement)
392+
393+
# Set up surplus to return the specified surplus value
394+
trade.surplus = Mock(return_value=surplus_value)
322395

323396
# Reverse the fee application
324397
new_trade = fee_policy.reverse_protocol_fee(trade)
325398

326399
# Verify the amount without fee
400+
# Use the minimum improvement value in the expected formula
401+
min_improvement = min(price_improvement_value, surplus_value)
327402
expected_amount = expected_formula(
328-
amount, price_improvement_value, price_improvement_factor, volume_factor
403+
amount, min_improvement, price_improvement_factor, volume_factor
329404
)
330405
assert getattr(new_trade, amount_field) == expected_amount

0 commit comments

Comments
 (0)