# Incorrect rounding direction in geometric pool ask_exact_amount_out allows theft of funds

Bug Deep Dive #8 · 4 December 2025

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

---

| Title | Incorrect rounding direction in geometric pool ask_exact_amount_out allows theft of funds |
| Reward | $7467, 2 dups |
| Contest | Dango Dex - 15 Sep 2025 on Sherlock |
| Author | haxagon |
| Context | Incorrect Rounding |

Usually these findings are lows at best and have a thousand duplicates, but not this time. Why? The truncation was hidden, and you had to do the maths to confirm it was exploitable at all — gas fees can easily exceed the rounding error.\
\
**Alpha:** next time you spot a rounding error, do the maths. And bear in mind that non-Solidity languages are roughly where Solidity was in 2022 in terms of how well the ground is covered.

More specifically, there is an incorrect rounding direction in the geometric pool's `ask_exact_amount_out`, which allows theft of funds over multiple swaps.

```rust
fn ask_exact_amount_out(
    ask_amount_in_quote: Uint128,
    passive_bids: Box<dyn Iterator<Item = (Price, Uint128)>>,
) -> anyhow::Result<Uint128> {
    let mut remaining_ask_in_quote = ask_amount_in_quote.checked_into_dec::<6>()?;
    let mut input_amount = Udec128::ZERO;

    for (price, amount) in passive_bids {
        let remaining_ask = remaining_ask_in_quote.checked_div_dec_floor(price)?;
        let matched_amount = cmp::min(amount.checked_into_dec()?, remaining_ask);
        input_amount.checked_add_assign(matched_amount)?;

        let matched_amount_in_quote = matched_amount.checked_mul_dec_ceil(price)?;
        remaining_ask_in_quote.checked_sub_assign(matched_amount_in_quote)?;

        if remaining_ask_in_quote.is_zero()
            || remaining_ask.is_zero()
            || matched_amount_in_quote.is_zero()
        {
            return Ok(input_amount.into_int());
        }
    }

    bail!("not enough liquidity to fulfill the swap! remaining amount: {remaining_ask_in_quote}")
}
```

The interesting thing about this finding is that the bug is not in **checked_div_dec_floor()** (well it is but it is not exploitable there, difference is on the decimal part only), but is **input_amount.into_int()**, which completely removes the decimal part.\
\
Consider a BTC-USDC pool where 1 sat of BTC = 1999 units of USDC. An attacker can execute a swap with an exact amount out of 1999 units of USDC, which translates to 1.999 sats of BTC.\
\
As a result of the bug, the attacker obtains 1999 units of USDC while paying 1 sat of BTC on every swap (the actual price being 1000 units of USDC per sat). Repeated over many swaps, this drains the BTC side of the pool.

**Conclusion**

This finding would earn you **\$7467**, which is a lot. In Solidity it would have had many more duplicates, even though it turns on a particularity of the `into_int()` operation. Do what you want with that information.


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

---

Newer: [Users can swap at the best price by splitting swap into several small swap](https://0xsimao.com/the-contest-academy/bug-deep-dive-9) · Older: [Attackers will steal rewards from legitimate pools by making duplicate pools for listed token](https://0xsimao.com/the-contest-academy/bug-deep-dive-7)
