Skip to content
Request an audit

View::queryAssetBalances does not account for JIT penalties

TitleView::queryAssetBalances does not account for JIT penalties
Reward$886, Unique
ContestAmmplify - 2 Sep 2025 on Sherlock
Authortedox
ContextMissing 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
Codebase

Message on Telegram All 62 posts