# View::queryAssetBalances does not account for JIT penalties

Bug Deep Dive #23 · 19 December 2025

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

---

| Title | View::queryAssetBalances does not account for JIT penalties |
| Reward | $886, Unique |
| Contest | Ammplify - 2 Sep 2025 on Sherlock |
| Author | tedox |
| Context | Missing Logic |

`View::queryAssetBalances` reports how many assets a user would receive if they withdrew right now. The value is wrong, because it does not account for the JIT penalty the position may incur.\
When a user removes a maker position, the contract calculates the assets to return and then applies the JIT penalty afterwards.

```solidity
/// @inheritdoc IMaker
function removeMaker(
    address recipient,
    uint256 assetId,
    uint160 minSqrtPriceX96,
    uint160 maxSqrtPriceX96,
    bytes calldata rftData
) external nonReentrant returns (address token0, address token1, uint256 removedX, uint256 removedY) {
    Asset storage asset = AssetLib.getAsset(assetId);
    require(asset.owner == msg.sender, NotMakerOwner(asset.owner, msg.sender));
    require(asset.liqType == LiqType.MAKER || asset.liqType == LiqType.MAKER_NC, NotMaker(assetId));
    PoolInfo memory pInfo = PoolLib.getPoolInfo(asset.poolAddr);
    Data memory data = DataImpl.make(pInfo, asset, minSqrtPriceX96, maxSqrtPriceX96, 0);
    WalkerLib.modify(pInfo, asset.lowTick, asset.highTick, data);
    // Settle balances.
    PoolWalker.settle(pInfo, asset.lowTick, asset.highTick, data);
    removedX = uint256(-data.xBalance); // These are definitely negative.
    removedY = uint256(-data.yBalance);
->  (removedX, removedY) = FeeLib.applyJITPenalties(asset, removedX, removedY);
    //@audit JIT penalty applied to the full amount returned
    AssetLib.removeAsset(assetId);
    address[] memory tokens = pInfo.tokens();
    int256[] memory balances = new int256[](2);
    balances[0] = -int256(removedX); // We know they fit since they can only be less (in magnitude) than before.
    balances[1] = -int256(removedY);
    RFTLib.settle(recipient, tokens, balances, rftData);
    // Return values
    token0 = tokens[0];
    token1 = tokens[1];
}
```

However, when the view function is used it does not properly account for the JIT penalty.

```solidity
function queryAssetBalances(
    uint256 assetId
) external view returns (int256 netBalance0, int256 netBalance1, uint256 fees0, uint256 fees1) {
    Asset storage asset = AssetLib.getAsset(assetId);
    PoolInfo memory pInfo = PoolLib.getPoolInfo(asset.poolAddr);
    ViewData memory data = ViewDataImpl.make(pInfo, asset);
    ViewWalkerLib.viewAsset(pInfo, asset.lowTick, asset.highTick, data);
    if (asset.liqType == LiqType.TAKER) {
        uint256 vaultX = VaultLib.balanceOf(pInfo.token0, asset.xVaultIndex, assetId, false);
        uint256 vaultY = VaultLib.balanceOf(pInfo.token1, asset.yVaultIndex, assetId, false);
        // Balance and fees are owed, and vault balance is owned.
        netBalance0 = int256(vaultX) - int256(data.liqBalanceX);
        netBalance1 = int256(vaultY) - int256(data.liqBalanceY);
        fees0 = data.earningsX;
        fees1 = data.earningsY;
    } else {                                            //@audit missing JIT penalty
        netBalance0 = int256(data.liqBalanceX);
        netBalance1 = int256(data.liqBalanceY);
        fees0 = data.earningsX;
        fees1 = data.earningsY;
    }
}
```

The two values then differ by more than 0.01% whenever the JIT penalty exceeds 0.01%, which it usually does.

From the README: Issues that lead to getting incorrect return values (i.e. deviates from the withdrawal value of the asset by more than 0.01%) from the queryAssetBalance function (even if the appropriate input is used), which will lead to issues when executing other functions, may be considered valid with Medium severity at max.\
\
**Alpha:** view functions are not usually worth a medium, but this protocol explicitly asked for them to be considered.

**Conclusion**

This finding would earn you **\$886**. The easiest way to find these is to compare the state-changing function with the view function and check they agree — and to read the README.

[**Full Report**](https://audits.sherlock.xyz/contests/1054/report)\
[**Codebase**](https://github.com/itos-finance/AmmplifyPublic/tree/beb942cfbd54990fc9434fbfa99a1fc251d73689)

---

Newer: [ViewFacet.queryAssetBalances doesn't include uncollected uniswap fees for compounded maker position](https://0xsimao.com/the-contest-academy/bug-deep-dive-24) · Older: [The protocol doesnt support weird tokens like Tether Gold (that return false on every operation)](https://0xsimao.com/the-contest-academy/bug-deep-dive-22)
