MEDIUM severity: Slippage is not checked when result amounts are zero. The _processDelta() function in BasePositionManager.sol fails to enforce slippage checks when delta.amount0() or delta.amount1() is exactly zero. This occurs because the slippage validation logic is only triggered when the delta is strictly less than or greater than zero, skipping the case when the...

Vulnerable code:
// SPDX-License-Identifier: BUSL-1.1
// Likwid Contracts
pragma solidity ^0.8.26;

// Openzeppelin
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721Enumerable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
// Solmate
import {Owned} from "solmate/src/auth/Owned.sol";
// Local
import {IBasePositionManager} from "../interfaces/IBasePositionManager.sol";
import {ImmutableState} from "./ImmutableState.sol";
import {PoolKey} from "../types/PoolKey.sol";
import {BalanceDelta} from "../types/BalanceDelta.sol";
import {PoolId} from "../types/PoolId.sol";
import {Currency, CurrencyLibrary} from "../types/Currency.sol";
import {IVault} from "../interfaces/IVault.sol";
import {IUnlockCallback} from "../interfaces/callback/IUnlockCallback.sol";
import {CustomRevert} from "../libraries/CustomRevert.sol";
import {CurrencyPoolLibrary} from "../libraries/CurrencyPoolLibrary.sol";

abstract contract BasePositionManager is
    IBasePositionManager,
    ImmutableState,
    IUnlockCallback,
    ERC721Enumerable,
    Owned
{
    using CurrencyPoolLibrary for Currency;
    using CustomRevert for bytes4;

    uint256 public nextId = 1;

    mapping(uint256 tokenId => PoolId poolId) public poolIds;
    mapping(PoolId poolId => PoolKey poolKey) public poolKeys;

    constructor(string memory name_, string memory symbol_, address initialOwner, IVault _vault)
        ImmutableState(_vault)
        Owned(initialOwner)
        ERC721(name_, symbol_)
    {}

    modifier ensure(uint256 deadline) {
        require(deadline >= block.timestamp, "EXPIRED");
        _;
    }

    function _requireAuth(address spender, uint256 tokenId) internal view {
        if (spender != ownerOf(tokenId)) {
            NotOwner.selector.revertWith();
        }
    }

    function _clearNative(address spender) internal {
        // clear any native currency left in the contract
        uint256 balance = address(this).balance;
        if (balance > 0) {
            CurrencyLibrary.ADDRESS_ZERO.transfer(spender, balance);
        }
    }

    function _processDelta(
        address sender,
        PoolKey memory key,
        BalanceDelta delta,
        uint256 amount0Min,
        uint256 amount1Min,
        uint256 amount0Max,
        uint256 amount1Max
    ) internal returns (uint256 amount0, uint256 amount1) {
        if (delta.amount0() < 0) {
            amount0 = uint256(-int256(delta.amount0()));
            if ((amount0Min > 0 && amount0 < amount0Min) || (amount0Max > 0 && amount0 > amount0Max)) {
                PriceSlippageTooHigh.selector.revertWith();
            }
            key.currency0.settle(vault, sender, amount0, false);
        } else if (delta.amount0() > 0) {
            amount0 = uint256(int256(delta.amount0()));
            if ((amount0Min > 0 && amount0 < amount0Min) || (amount0Max > 0 && amount0 > amount0Max)) {
                PriceSlippageTooHigh.selector.revertWith();
            }
            key.currency0.take(vault, sender, amount0, false);
        }

        if (delta.amount1() < 0) {
            amount1 = uint256(-int256(delta.amount1()));
            if ((amount1Min > 0 && amount1 < amount1Min) || (amount1Max > 0 && amount1 > amount1Max)) {
                PriceSlippageTooHigh.selector.revertWith();
            }
            key.currency1.settle(vault, sender, amount1, false);
        } else if (delta.amount1() > 0) {
            amount1 = uint256(int256(delta.amount1()));
            if ((amount1Min > 0 && amount1 < amount1Min) || (amount1Max > 0 && amount1 > amount1Max)) {
                PriceSlippageTooHigh.selector.revertWith();
            }
            key.currency1.take(vault, sender, amount1, false);
        }

        _clearNative(sender);
    }

    function _mintPosition(PoolKey memory key, address to) internal returns (uint256 tokenId) {
        tokenId = nextId++;
        _mint(to, tokenId);

        PoolId poolId = key.toId();
        poolIds[tokenId] = poolId;
        if (poolKeys[poolId].currency1 == Currency.wrap(address(0))) {
            poolKeys[poolId] = key;
        } else {
            PoolKey memory existingKey = poolKeys[poolId];
            if (
                !(
                    existingKey.currency0 == key.currency0 && existingKey.currency1 == key.currency1
                        && existingKey.fee == key.fee
                )
            ) {
                MismatchedPoolKey.selector.revertWith();
            }
        }
    }

    /// @inheritdoc IUnlockCallback
    function unlockCallback(bytes calldata data) external virtual returns (bytes memory);
}

// SPDX-License-Identifier: BUSL-1.1
// Likwid Contracts
pragma solidity ^0.8.26;

// Local
import {Math} from "./libraries/Math.sol";
import {BasePositionManager} from "./base/BasePositionManager.sol";
import {PoolKey} from "./types/PoolKey.sol";
import {PoolId} from "./types/PoolId.sol";
import {Currency} from "./types/Currency.sol";
import {BalanceDelta} from "./types/BalanceDelta.sol";
import {IVault} from "./interfaces/IVault.sol";
import {ILendPositionManager} from "./interfaces/ILendPositionManager.sol";
import {LendPosition} from "./libraries/LendPosition.sol";
import {StateLibrary} from "./libraries/StateLibrary.sol";
import {CustomRevert} from "./libraries/CustomRevert.sol";
import {CurrencyPoolLibrary} from "./libraries/CurrencyPoolLibrary.sol";
import {SafeCast} from "./libraries/SafeCast.sol";

contract LikwidLendPosition is ILendPositionManager, BasePositionManager {
    using CurrencyPoolLibrary for Currency;
    using CustomRevert for bytes4;
    using SafeCast for *;

    mapping(uint256 tokenId => bool lendForOne) public lendDirections;

    constructor(address initialOwner, IVault _vault)
        BasePositionManager("LIKWIDLendPositionManager", "LLPM", initialOwner, _vault)
    {}

    enum Actions {
        DEPOSIT,
        WITHDRAW,
        SWAP
    }

    function unlockCallback(bytes calldata data) external override returns (bytes memory) {
        (Actions action, bytes memory params) = abi.decode(data, (Actions, bytes));

        if (action == Actions.DEPOSIT) {
            return handleLend(params);
        } else if (action == Actions.WITHDRAW) {
            return handleLend(params);
        } else if (action == Actions.SWAP) {
            return handleSwap(params);
        } else {
            InvalidCallback.selector.revertWith();
        }
    }

    /// @inheritdoc ILendPositionManager
    function getPositionState(uint256 tokenId) external view returns (LendPosition.State memory position) {
        bytes32 salt = bytes32(tokenId);
        PoolId poolId = poolIds[tokenId];
        bool lendForOne = lendDirections[tokenId];
        (,, uint256 deposit0CumulativeLast, uint256 deposit1CumulativeLast) =
            StateLibrary.getBorrowDepositCumulative(vault, poolId);
        uint256 depositCumulativeLast = lendForOne ? deposit1CumulativeLast : deposit0CumulativeLast;
        position = StateLibrary.getLendPositionState(vault, poolId, address(this), lendForOne, salt);
        position.lendAmount = Math.mulDiv(
            position.lendAmount,
            depositCumulativeLast,
            position.depositCumulativeLast == 0 ? depositCumulativeLast : position.depositCumulativeLast
        ).toUint128();
        position.depositCumulativeLast = depositCumulativeLast;
    }

    /// @inheritdoc ILendPositionManager
    function addLending(PoolKey memory key, bool lendForOne, address recipient, uint256 amount)
        external
        payable
        returns (uint256 tokenId)
    {
        tokenId = _mintPosition(key, recipient);
        lendDirections[tokenId] = lendForOne;
        if (amount > 0) {
            _deposit(msg.sender, recipient, tokenId, amount);
        }
    }

    function _deposit(address sender, address tokenOwner, uint256 tokenId, uint256 amount) internal {
        _requireAuth(tokenOwner, tokenId);
        PoolId poolId = poolIds[tokenId];
        bool lendForOne = lendDirections[tokenId];
        PoolKey memory key = poolKeys[poolId];

        IVault.LendParams memory params =
            IVault.LendParams({lendForOne: lendForOne, lendAmount: -amount.toInt128(), salt: bytes32(tokenId)});

        bytes memory callbackData = abi.encode(sender, key, params);
        bytes memory data = abi.encode(Actions.DEPOSIT, callbackData);

        vault.unlock(data);
        Currency currency = lendForOne ? key.currency1 : key.currency0;
        emit Deposit(poolId, currency, sender, tokenId, tokenOwner, amount);
    }

    /// @inheritdoc ILendPositionManager
    function deposit(uint256 tokenId, uint256 amount) external payable {
        _deposit(msg.sender, msg.sender, tokenId, amount);
    }

    /// @inheritdoc ILendPositionManager
    function withdraw(uint256 tokenId, uint256 amount) external {
        _requireAuth(msg.sender, tokenId);
        PoolId poolId = poolIds[tokenId];
        bool lendForOne = lendDirections[tokenId];
        PoolKey memory key = poolKeys[poolId];

        IVault.LendParams memory params =
            IVault.LendParams({lendForOne: lendForOne, lendAmount: amount.toInt128(), salt: bytes32(tokenId)});

        bytes memory callbackData = abi.encode(msg.sender, key, params);
        bytes memory data = abi.encode(Actions.WITHDRAW, callbackData);

        vault.unlock(data);
        Currency currency = lendForOne ? key.currency1 : key.currency0;
        emit Withdraw(poolId, currency, msg.sender, tokenId, msg.sender, amount);
    }

    function handleLend(bytes memory _data) internal returns (bytes memory) {
        (address sender, PoolKey memory key, IVault.LendParams memory params) =
            abi.decode(_data, (address, PoolKey, IVault.LendParams));

        BalanceDelta delta = vault.lend(key, params);

        (uint256 amount0, uint256 amount1) = _processDelta(sender, key, delta, 0, 0, 0, 0);

        return abi.encode(amount0, amount1);
    }

    /// @inheritdoc ILendPositionManager
    function exactInput(SwapInputParams calldata params)
        external
        payable
        ensure(params.deadline)
        returns (uint24 swapFee, uint256 feeAmount, uint256 amountOut)
    {
        _requireAuth(msg.sender, params.tokenId);
        PoolKey memory key = poolKeys[params.poolId];
        if (params.zeroForOne != lendDirections[params.tokenId]) {
            InvalidCurrency.selector.revertWith();
        }
        int256 amountSpecified = -int256(params.amountIn);
        IVault.SwapParams memory swapParams = IVault.SwapParams({
            zeroForOne: params.zeroForOne,
            amountSpecified: amountSpecified,
            useMirror: true,
            salt: bytes32(params.tokenId)
        });
        uint256 amount0Min = params.zeroForOne ? 0 : params.amountOutMin;
        uint256 amount1Min = params.zeroForOne ? params.amountOutMin : 0;
        bytes memory callbackData = abi.encode(msg.sender, key, swapParams, amount0Min, amount1Min, 0, 0);
        bytes memory data = abi.encode(Actions.SWAP, callbackData);

        bytes memory result = vault.unlock(data);
        uint256 amount0;
        uint256 amount1;
        (swapFee, feeAmount, amount0, amount1) = abi.decode(result, (uint24, uint256, uint256, uint256));
        amountOut = params.zeroForOne ? amount1 : amount0;
    }

    /// @inheritdoc ILendPositionManager
    function exactOutput(SwapOutputParams calldata params)
        external
        payable
        ensure(params.deadline)
        returns (uint24 swapFee, uint256 feeAmount, uint256 amountIn)
    {
        _requireAuth(msg.sender, params.tokenId);
        PoolKey memory key = poolKeys[params.poolId];
        if (params.zeroForOne != lendDirections[params.tokenId]) {
            InvalidCurrency.selector.revertWith();
        }
        int256 amountSpecified = int256(params.amountOut);
        IVault.SwapParams memory swapParams = IVault.SwapParams({
            zeroForOne: params.zeroForOne,
            amountSpecified: amountSpecified,
            useMirror: true,
            salt: bytes32(params.tokenId)
        });
        uint256 amount0Max = params.zeroForOne ? params.amountInMax : 0;
        uint256 amount1Max = params.zeroForOne ? 0 : params.amountInMax;
        bytes memory callbackData = abi.encode(msg.sender, key, swapParams, 0, 0, amount0Max, amount1Max);
        bytes memory data = abi.encode(Actions.SWAP, callbackData);

        bytes memory result = vault.unlock(data);
        uint256 amount0;
        uint256 amount1;
        (swapFee, feeAmount, amount0, amount1) = abi.decode(result, (uint24, uint256, uint256, uint256));
        amountIn = params.zeroForOne ? amount0 : amount1;
    }

    function handleSwap(bytes memory _data) internal returns (bytes memory) {
        (
            address sender,
            PoolKey memory key,
            IVault.SwapParams memory params,
            uint256 amount0Min,
            uint256 amount1Min,
            uint256 amount0Max,
            uint256 amount1Max
        ) = abi.decode(_data, (address, PoolKey, IVault.SwapParams, uint256, uint256, uint256, uint256));

        (BalanceDelta delta, uint24 swapFee, uint256 feeAmount) = vault.swap(key, params);

        uint256 amount0;
        uint256 amount1;

        if (params.zeroForOne) {
            if (delta.amount0() < 0) {
                amount0 = uint256(-int256(delta.amount0()));
                if ((amount0Min > 0 && amount0 < amount0Min) || (amount0Max > 0 && amount0 > amount0Max)) {
                    PriceSlippageTooHigh.selector.revertWith();
                }
                key.currency0.settle(vault, sender, amount0, false);
            } else if (delta.amount0() > 0) {
                amount0 = uint256(int256(delta.amount0()));
                if ((amount0Min > 0 && amount0 < amount0Min) || (amount0Max > 0 && amount0 > amount0Max)) {
                    PriceSlippageTooHigh.selector.revertWith();
                }
                key.currency0.take(vault, sender, amount0, false);
            }
            if (delta.amount1() < 0) {
                amount1 = uint256(-int256(delta.amount1()));
                if ((amount1Min > 0 && amount1 < amount1Min) || (amount1Max > 0 && amount1 > amount1Max)) {
                    PriceSlippageTooHigh.selector.revertWith();
                }
            } else if (delta.amount1() > 0) {
                amount1 = uint256(int256(delta.amount1()));
                if ((amount1Min > 0 && amount1 < amount1Min) || (amount1Max > 0 && amount1 > amount1Max)) {
                    PriceSlippageTooHigh.selector.revertWith();
                }
            }
        } else {
            if (delta.amount0() < 0) {
                amount0 = uint256(-int256(delta.amount0()));
                if ((amount0Min > 0 && amount0 < amount0Min) || (amount0Max > 0 && amount0 > amount0Max)) {
                    PriceSlippageTooHigh.selector.revertWith();
                }
            } else if (delta.amount0() > 0) {
                amount0 = uint256(int256(delta.amount0()));
                if ((amount0Min > 0 && amount0 < amount0Min) || (amount0Max > 0 && amount0 > amount0Max)) {
                    PriceSlippageTooHigh.selector.revertWith();
                }
            }
            if (delta.amount1() < 0) {
                amount1 = uint256(-int256(delta.amount1()));
                if ((amount1Min > 0 && amount1 < amount1Min) || (amount1Max > 0 && amount1 > amount1Max)) {
                    PriceSlippageTooHigh.selector.revertWith();
                }
                key.currency1.settle(vault, sender, amount1, false);
            } else if (delta.amount1() > 0) {
                amount1 = uint256(int256(delta.amount1()));
                if ((amount1Min > 0 && amount1 < amount1Min) || (amount1Max > 0 && amount1 > amount1Max)) {
                    PriceSlippageTooHigh.selector.revertWith();
                }
                key.currency1.take(vault, sender, amount1, false);
            }
        }
        _clearNative(sender);

        return abi.encode(swapFee, feeAmount, amount0, amount1);
    }
}


Vulnerability type: CWE-693|CWE-424