---
id: TIP-1005
title: Fix ask swap rounding loss
description: A fix for a rounding bug in the Stablecoin DEX where partial fills on ask orders can cause small amounts of quote tokens to be lost.
authors: Dan Robinson
status: Draft
---

# TIP-1005: Fix ask swap rounding loss

## Abstract

This TIP fixes a rounding bug in the `swapExactAmountIn` function when filling ask orders. Due to double-rounding, the maker can receive slightly less quote tokens than the taker paid, causing tokens to be lost.

## Motivation

When a taker swaps quote tokens for base tokens against an ask order, the following calculation occurs:

1. Convert taker's `amountIn` (quote) to base: `base_out = floor(amountIn / price)`
2. Credit maker with quote: `makerReceives = ceil(base_out * price)`

Due to the floor in step 1, `makerReceives` can be less than `amountIn`. For example:

- Taker pays `amountIn = 102001` quote at price 1.02 (tick 2000)
- `base_out = floor(102001 / 1.02) = 100000`
- `makerReceives = ceil(100000 * 1.02) = 102000`
- **1 token is lost**

This violates the zero-sum invariant: the taker pays more than the maker receives. It also means there is no canonical amount swapped—the trade for the maker is different from the trade for the taker.

---

# Specification

## Bug location

The bug is in `_fillOrdersExactIn` when processing ask orders (the `baseForQuote = false` path). Specifically, when a partial fill occurs:

1. `fillAmount` (base) is calculated by rounding down: `baseOut = (remainingIn * PRICE_SCALE) / price`
2. `_fillOrder` is called with `fillAmount`
3. Inside `_fillOrder`, the maker's quote credit is re-derived: `quoteAmount = ceil(fillAmount * price)`

The re-derivation in step 3 loses the original `remainingIn` information.

## Fix

For partial fills in the ask path, pass the actual `remainingIn` (quote) to `_fillOrder` and use it directly for the maker's credit, rather than re-deriving it from `fillAmount`.

The fix requires:

1. Modify `_fillOrder` to accept an optional `quoteOverride` parameter for ask orders
2. In `_fillOrdersExactIn`, when partially filling an ask, pass `remainingIn` as the quote override
3. When `quoteOverride` is provided, use it directly for the maker's balance increment instead of computing `ceil(fillAmount * price)`

## Reference implementation changes

The fix requires changes to two functions in [`docs/specs/src/StablecoinDEX.sol`](https://github.com/tempoxyz/tempo/blob/main/docs/specs/src/StablecoinDEX.sol):

### 1. `_fillOrder` ([line 551-556](https://github.com/tempoxyz/tempo/blob/main/docs/specs/src/StablecoinDEX.sol#L551-L556))

Add an optional `quoteOverride` parameter. When non-zero and the order is an ask, use `quoteOverride` directly for the maker's balance increment instead of computing `ceil(fillAmount * price)`.

```solidity
// Before:
uint128 quoteAmount =
    uint128((uint256(fillAmount) * uint256(price) + PRICE_SCALE - 1) / PRICE_SCALE);
balances[order.maker][book.quote] += quoteAmount;

// After:
uint128 quoteAmount = quoteOverride > 0
    ? quoteOverride
    : uint128((uint256(fillAmount) * uint256(price) + PRICE_SCALE - 1) / PRICE_SCALE);
balances[order.maker][book.quote] += quoteAmount;
```

### 2. `_fillOrdersExactIn` ([line 923-926](https://github.com/tempoxyz/tempo/blob/main/docs/specs/src/StablecoinDEX.sol#L923-L926))

In the partial fill branch for asks, pass `remainingIn` as the quote override:

```solidity
// Before:
orderId = _fillOrder(orderId, fillAmount);

// After (for partial fills where fillAmount == baseOut):
orderId = _fillOrder(orderId, fillAmount, remainingIn);
```

## Affected code paths

- `_fillOrdersExactIn` with `baseForQuote = false` (ask path), partial fill case only
- Full fills are not affected because the quote amount is derived from `order.remaining`, not `remainingIn`
- Bid swaps are not affected because the taker pays base tokens directly

## Example: Before and after

**Before (buggy):**
```
amountIn = 102001 quote
base_out = floor(102001 / 1.02) = 100000
makerReceives = ceil(100000 * 1.02) = 102000
Lost: 1 token
```

**After (fixed):**
```
amountIn = 102001 quote
base_out = floor(102001 / 1.02) = 100000
makerReceives = 102001 (passed directly)
Lost: 0 tokens
```

---

# Invariants

- Zero-sum: for any swap, `takerPaid == makerReceived` (within the same token)
- Taker receives `floor(amountIn / price)` base tokens (rounds in favor of protocol)
- Maker receives exactly what taker paid in quote tokens
