diff --git a/src/PrizeVault.sol b/src/PrizeVault.sol index 0f2c18c..b27068e 100644 --- a/src/PrizeVault.sol +++ b/src/PrizeVault.sol @@ -686,6 +686,7 @@ contract PrizeVault is TwabERC20, Claimable, IERC4626, ILiquidationSource, Ownab /// @dev Supports the liquidation of either assets or prize vault shares. function liquidatableBalanceOf(address _tokenOut) external view returns (uint256) { uint256 _totalDebt = totalDebt(); + uint256 _yieldFeePercentage = yieldFeePercentage; uint256 _maxAmountOut; if (_tokenOut == address(this)) { // Liquidation of vault shares is capped to the mint limit. @@ -693,6 +694,14 @@ contract PrizeVault is TwabERC20, Claimable, IERC4626, ILiquidationSource, Ownab } 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)); + + // If a yield fee will be minted, then the liquidation will also be capped based on the remaining mint limit. + if (_yieldFeePercentage != 0) { + uint256 _maxAmountBasedOnFeeMintLimit = _mintLimit(_totalDebt).mulDiv(FEE_PRECISION, _yieldFeePercentage); + if (_maxAmountBasedOnFeeMintLimit < _maxAmountOut) { + _maxAmountOut = _maxAmountBasedOnFeeMintLimit; + } + } } else { return 0; } @@ -705,7 +714,7 @@ contract PrizeVault is TwabERC20, Claimable, IERC4626, ILiquidationSource, Ownab // The final balance is computed by taking the liquid yield and multiplying it by // (1 - yieldFeePercentage), rounding down, to ensure that enough yield is left for // the yield fee. - return _liquidYield.mulDiv(FEE_PRECISION - yieldFeePercentage, FEE_PRECISION); + return _liquidYield.mulDiv(FEE_PRECISION - _yieldFeePercentage, FEE_PRECISION); } /// @inheritdoc ILiquidationSource diff --git a/test/unit/PrizeVault/Liquidate.t.sol b/test/unit/PrizeVault/Liquidate.t.sol index 4de013e..4e43890 100644 --- a/test/unit/PrizeVault/Liquidate.t.sol +++ b/test/unit/PrizeVault/Liquidate.t.sol @@ -123,6 +123,50 @@ contract PrizeVaultLiquidationTest is UnitBaseSetup { vault.claimYieldFeeShares(supplyCapLeft - amountOut); } + function testLiquidatableBalanceOf_hasBalanceAtShareLimitWhenNoFee() public { + vault.setYieldFeePercentage(0); // no fee + + // make a large deposit to use all of the shares: + underlyingAsset.mint(address(alice), type(uint96).max); + vm.startPrank(alice); + underlyingAsset.approve(address(vault), type(uint96).max); + vault.deposit(type(uint96).max, alice); + vm.stopPrank(); + + underlyingAsset.mint(address(vault), 1e18); + uint256 availableYield = vault.availableYieldBalance(); + assertApproxEqAbs(availableYield, 1e18 - vault.yieldBuffer(), 1); + + assertEq(vault.liquidatableBalanceOf(address(underlyingAsset)), availableYield); // can still liquidate + } + + function testLiquidatableBalanceOf_respectsFeeShareLimitWithAssetBalance() public { + vault.setYieldFeePercentage(1e8); // 10% fee + + uint256 supplyCapLeft = 100; // this means that with a 10% fee, we should be able to liquidate 900 assets so that the yield fee is minted up to the supply cap, but no further + + // make a large deposit to use most of the shares: + underlyingAsset.mint(address(alice), type(uint96).max); + vm.startPrank(alice); + underlyingAsset.approve(address(vault), type(uint96).max - supplyCapLeft); + vault.deposit(type(uint96).max - supplyCapLeft, alice); + vm.stopPrank(); + + underlyingAsset.mint(address(vault), 1e18); + uint256 availableYield = vault.availableYieldBalance(); + assertApproxEqAbs(availableYield, 1e18 - vault.yieldBuffer(), 1); + + assertGt(availableYield, 900); + + assertEq(vault.liquidatableBalanceOf(address(underlyingAsset)), 900); // capped at 900 since that results in our max 100 yield fee mint + + // ensure a liquidation can occur at the max: + vault.setLiquidationPair(address(this)); + vm.expectEmit(); + emit TransferYieldOut(address(this), address(underlyingAsset), alice, 900, 100); + vault.transferTokensOut(address(0), alice, address(underlyingAsset), 900); + } + /* ============ transferTokensOut ============ */ function testTransferTokensOut_noFee() public {