<!-- canonical: https://0xsimao.com/findings/pooltogether-the-prize-layer-for-defi-liquidations-prize-liquidatable-mint -->

# DoSed liquidations as `PrizeVault::liquidatableBalanceOf()` does not take into account the `mintLimit` when the token out is the asset

Medium · Sherlock · Raffle · 16th May 2024

Finding M-9 of the PoolTogether Prize Layer competition.

- Protocol: https://audits.sherlock.xyz/contests/225
- Report: /reports/pooltogether-the-prize-layer-for-defi
- Codebase: https://github.com/0xsimao/2024-05-pooltogether/tree/1aa1b8c028b659585e4c7a6b9b652fb075f86db3
- Source: https://github.com/sherlock-audit/2024-05-pooltogether-judging/issues/88

---

## Summary

`PrizeVault::liquidatableBalanceOf()` is called in `TpdaLiquidationPair::_availableBalance()` to get the maximum amount to liquidate, which will be incorrect when `_tokenOut` is the `asset` of the `PrizeVault`, due to not taking the minted yield fee into account. Thus, it will overestimate the amount to liquidate and revert.

## Vulnerability Detail

`TpdaLiquidationPair::_availableBalance()` is called in `TpdaLiquidationPair::swapExactAmountOut()` to revert if the amount to liquidate exceeds the maximum and in `TpdaLiquidationPair::maxAmountOut()` to get the maximum liquidatable amount. Thus, users or smart contracts will [call](https://dev.pooltogether.com/protocol/guides/bots/liquidating-yield/#2-compute-the-available-liquidity) `TpdaLiquidationPair::maxAmountOut()` to get the maximum amount out and then `TpdaLiquidationPair::swapExactAmountOut()` with this amount to liquidate.
> Compute how much yield is available using the [maxAmountOut](https://dev.pooltogether.com/protocol/reference/liquidator/TpdaLiquidationPair#maxamountout) function on the Liquidation Pair. This function returns the maximum number of tokens you can swap out.

However, this is going to revert whenever the minted yield fee exceeds the mint limit, as `PrizeVault::liquidatableBalanceOf()` does not consider it when the asset to liquidate is the asset of the `PrizeVault`. Consider `PrizeVault::liquidatableBalanceOf()`:
```solidity
function liquidatableBalanceOf(address _tokenOut) external view returns (uint256) {
    ...
    } else if (_tokenOut == address(_asset)) { //@audit missing yield percentage for mintLimit
        // Liquidation of yield assets is capped at the max yield vault withdraw plus any latent balance.
        _maxAmountOut = _maxYieldVaultWithdraw() + _asset.balanceOf(address(this));
    }
    ...
}
```
As can be seen from the code snipped above, the minted yield fee is not taken into account and the mint limit is not calculated. On `PrizeVault::transferTokensOut()`, a mint fee given by `_yieldFee = (_amountOut * FEE_PRECISION) / (FEE_PRECISION - _yieldFeePercentage) - _amountOut;` is always minted and the limit is enforced at the end of the function `_enforceMintLimit(_totalDebtBefore, _yieldFee);`. Thus, without limiting the liquidatable assets to the amount that would trigger a yield fee that reaches the mint limit, liquidations will be DoSed.

## Impact

DoSed liquidations when the asset out is the asset of the `PrizeVault`.

## Code Snippet

https://github.com/sherlock-audit/2024-05-pooltogether/blob/main/pt-v5-vault/src/PrizeVault.sol#L693-L696

## Tool used

Manual Review

Vscode

## Recommendation

The correct formula can be obtained by inverting `_yieldFee = (_amountOut * FEE_PRECISION) / (FEE_PRECISION - _yieldFeePercentage) - _amountOut;`, leading to:
```solidity
function liquidatableBalanceOf(address _tokenOut) external view returns (uint256) {
    ...
    } else if (_tokenOut == address(_asset)) {
        // Liquidation of yield assets is capped at the max yield vault withdraw plus any latent balance.
        _maxAmountOut = _maxYieldVaultWithdraw() + _asset.balanceOf(address(this));
        // Limit to the fee amount
        uint256  mintLimitDueToFee = (FEE_PRECISION - yieldFeePercentage) * _mintLimit(_totalDebt) / yieldFeePercentage;
       _maxAmountOut = _maxAmountOut >= mintLimitDueToFee ? mintLimitDueToFee : _maxAmountOut;
    }
    ...
}
```

---

Related findings:

- [`ViewLogic::maxLiquidatable()` doesn't take the bonus into account, making the agent liquidatable again](https://0xsimao.com/findings/cap-view-liquidatable-bonus-agent): Cap
- [`PreDepositVault::maxDeposit/Mint()` are missing `maxDepositLimit` as per the ERC4626 spec](https://0xsimao.com/findings/gaib-deposit-mint-erc4626-spec): GAIB Pre-Vaults
- [Balance check in Vault::withdraw() does not take fees into account](https://0xsimao.com/findings/orange-balance-withdraw-fees-account): Orange Bridge
- [DoSed withdrawals due to `ManagedLeveragedVault::executeWithdrawalEpoch()` repaying debt above limit](https://0xsimao.com/findings/beraborrow-i-withdrawals-withdrawal-epoch-debt): Beraborrow Managed Dens
