# XYK reflect_curve omits swap fee in order sizing, leaking LP fees

Bug Deep Dive #11 · 6 December 2025

Bug Deep Dives · [The Contest Academy](https://0xsimao.com/the-contest-academy) · 0xSimao

---

| Title | XYK reflect_curve omits swap fee in order sizing, leaking LP fees |
| Reward | $1493, 2 dups |
| Contest | Dango Dex - 15 Sep 2025 on Sherlock |
| Author | blockace |
| Context | Business Logic |

The `reflect_curve` function for the XYK pool sizes passive bid and ask orders without accounting for the swap fee, letting takers match them effectively fee-free. Fees that should accrue to liquidity providers are under-collected, a direct loss on top of ordinary impermanent loss.

In `dango/dex/src/core/xyk.rs`, the total order size at price $p$ is computed from the reserves as if there were no swap fee. For bids, the size uses `quote_reserve / p - base_reserve`; for asks, it uses `base_reserve - quote_reserve / p`. The fee factor $(1 - f)$ is omitted, so passive orders are reflected at sizes that do not accrue the fee when taken.

```rust
// dango/dex/src/core/xyk.rs (bids)
let quote_reserve_div_price = quote_reserve.checked_div_dec(price).ok()?;
let mut size = quote_reserve_div_price.checked_sub(base_reserve).ok()?;

// dango/dex/src/core/xyk.rs (asks)
let quote_reserve_div_price = quote_reserve.checked_div_dec(price).ok()?;
let size = base_reserve.checked_sub(quote_reserve_div_price).ok()?;
```

Mathematically, for a bid at price $p$ with reserves $R_b$ and $R_q$ and fee $f$, the equality

$$s \cdot p = \frac{R_q \cdot s}{R_b + s} \cdot (1 - f)$$

yields

$$s = \frac{R_q \cdot (1 - f)}{p} - R_b$$

The implementation lacks the $(1 - f)$ term. A symmetric adjustment is required for asks as well.

**Detailed derivation**

Not needed to understand the issue, but worth going through.

**Bids (pool buys base, taker sells base):**

Reserves: $R_b$ (base), $R_q$ (quote); price $p$ (quote per base); fee $f$.

Exact-in (base in) output after fee:

$$Q_{out} = \frac{R_q \cdot s}{R_b + s} \cdot (1 - f)$$

A passive bid at price $p$ promises $s \cdot p$ quote for $s$ base, so set $s \cdot p = Q_{out}$.

Solving:

$$p = \frac{R_q}{R_b + s} \cdot (1 - f) \Rightarrow R_b + s = \frac{R_q \cdot (1 - f)}{p} \Rightarrow s = \frac{R_q \cdot (1 - f)}{p} - R_b$$

Therefore the total size at price $p$ must be $\frac{R_q \cdot (1 - f)}{p} - R_b$.

**Asks (pool sells base, taker buys base):**

The taker pays quote $x = s \cdot p$ and wants to receive $s$ base.

Exact-in (quote in) output after fee:

$$B_{out} = \frac{R_b \cdot x}{R_q + x} \cdot (1 - f)$$

Set $s = B_{out}$ with $x = s \cdot p$:

$$s = \frac{R_b \cdot s \cdot p}{R_q + s \cdot p} \cdot (1 - f) \Rightarrow 1 = \frac{R_b \cdot p}{R_q + s \cdot p} \cdot (1 - f)$$

Hence:

$$R_q + s \cdot p = R_b \cdot p \cdot (1 - f) \Rightarrow s \cdot p = R_b \cdot p \cdot (1 - f) - R_q \Rightarrow s = R_b \cdot (1 - f) - \frac{R_q}{p}$$

Therefore the total size at price $p$ must be $R_b \cdot (1 - f) - \frac{R_q}{p}$.

**Alpha:** always make sure fees are charged in every scenario. If you are not sure, submit anyway.

**Conclusion**

This finding would earn you **\$1493**, basically from missing functionality, so make sure to always think about what is missing, and not only what is wrong.

[**Full Report**](https://audits.sherlock.xyz/contests/1066/report)\
**Codebase**

---

Newer: [User can pause all auctions by overflow in mid-price average](https://0xsimao.com/the-contest-academy/bug-deep-dive-12) · Older: [Asymmetric liquidity provision to geometric pool can allow attacker to purchase at flat oracle price, irrespective of trade size](https://0xsimao.com/the-contest-academy/bug-deep-dive-10)
