> Feedback: If these docs are stale, missing, or confusing, post sanitized feedback to `https://docs.tempo.xyz/api/feedback` with `source: "mcp"`, a short `message`, and any relevant `toolName`, `relatedResource`, or `client`.
# 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
