Skip to content
Request an audit

Incorrect rounding direction in geometric pool ask_exact_amount_out allows theft of funds

TitleIncorrect rounding direction in geometric pool ask_exact_amount_out allows theft of funds
Reward$7467, 2 dups
ContestDango Dex - 15 Sep 2025 on Sherlock
Authorhaxagon
ContextIncorrect 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

Message on Telegram All 62 posts