diff --git a/src/PrizeVault.sol b/src/PrizeVault.sol index f7ee5cf..248d21a 100644 --- a/src/PrizeVault.sol +++ b/src/PrizeVault.sol @@ -355,47 +355,35 @@ contract PrizeVault is TwabERC20, Claimable, IERC4626, ILiquidationSource, Ownab /// @inheritdoc IERC4626 /// @dev The latent asset balance is included in the total asset count to account for the "dust collection /// strategy". + /// @dev This function uses `convertToAssets` to ensure it does not revert, but may result in some + /// approximation depending on the yield vault implementation. function totalAssets() public view returns (uint256) { return yieldVault.convertToAssets(yieldVault.balanceOf(address(this))) + _asset.balanceOf(address(this)); } /// @inheritdoc IERC4626 - function convertToShares(uint256 _assets) public view returns (uint256) { - uint256 totalDebt_ = totalDebt(); - uint256 _totalAssets = totalAssets(); - if (_totalAssets >= totalDebt_) { - return _assets; - } else { - // If the vault controls less assets than what has been deposited a share will be worth a - // proportional amount of the total assets. This can happen due to fees, slippage, or loss - // of funds in the underlying yield vault. - return _assets.mulDiv(totalDebt_, _totalAssets, Math.Rounding.Down); - } + /// @dev This function uses approximate total assets and should not be used for onchain conversions. + function convertToShares(uint256 _assets) external view returns (uint256) { + return _convertToShares(_assets, totalAssets(), totalDebt(), Math.Rounding.Down); } /// @inheritdoc IERC4626 - function convertToAssets(uint256 _shares) public view returns (uint256) { - uint256 totalDebt_ = totalDebt(); - uint256 _totalAssets = totalAssets(); - if (_totalAssets >= totalDebt_) { - return _shares; - } else { - // If the vault controls less assets than what has been deposited a share will be worth a - // proportional amount of the total assets. This can happen due to fees, slippage, or loss - // of funds in the underlying yield vault. - return _shares.mulDiv(_totalAssets, totalDebt_, Math.Rounding.Down); - } + /// @dev This function uses approximate total assets and should not be used for onchain conversions. + function convertToAssets(uint256 _shares) external view returns (uint256) { + return _convertToAssets(_shares, totalAssets(), totalDebt(), Math.Rounding.Down); } /// @inheritdoc IERC4626 /// @dev Considers the TWAB mint limit /// @dev Returns zero if any deposit would result in a loss of assets + /// @dev Returns zero if total assets cannot be determined /// @dev Any latent balance of assets in the prize vault will be swept in with the deposit as a part of /// the "dust collection strategy". This means that the max deposit must account for the latent balance /// by subtracting it from the max deposit available otherwise. function maxDeposit(address /* receiver */) public view returns (uint256) { uint256 _totalDebt = totalDebt(); - if (totalAssets() < _totalDebt) return 0; + (bool _success, uint256 _totalAssets) = _tryGetTotalPreciseAssets(); + if (!_success || _totalAssets < _totalDebt) return 0; uint256 _latentBalance = _asset.balanceOf(address(this)); uint256 _maxYieldVaultDeposit = yieldVault.maxDeposit(address(this)); @@ -422,17 +410,22 @@ contract PrizeVault is TwabERC20, Claimable, IERC4626, ILiquidationSource, Ownab /// @inheritdoc IERC4626 /// @dev The prize vault maintains a latent balance of assets as part of the "dust collection strategy". /// This latent balance are accounted for in the max withdraw limits. + /// @dev Returns zero if total assets cannot be determined function maxWithdraw(address _owner) public view returns (uint256) { + (bool _success, uint256 _totalAssets) = _tryGetTotalPreciseAssets(); + if (!_success) return 0; + uint256 _maxWithdraw = _maxYieldVaultWithdraw() + _asset.balanceOf(address(this)); // the owner may receive less than 1 asset per share, so we must convert their balance here - uint256 _ownerAssets = convertToAssets(balanceOf(_owner)); + uint256 _ownerAssets = _convertToAssets(balanceOf(_owner), _totalAssets, totalDebt(), Math.Rounding.Down); return _ownerAssets < _maxWithdraw ? _ownerAssets : _maxWithdraw; } /// @inheritdoc IERC4626 /// @dev The prize vault maintains a latent balance of assets as part of the "dust collection strategy". /// This latent balance are accounted for in the max redeem limits. + /// @dev Returns zero if total assets cannot be determined function maxRedeem(address _owner) public view returns (uint256) { uint256 _maxWithdraw = _maxYieldVaultWithdraw() + _asset.balanceOf(address(this)); uint256 _ownerShares = balanceOf(_owner); @@ -441,18 +434,15 @@ contract PrizeVault is TwabERC20, Claimable, IERC4626, ILiquidationSource, Ownab // withdraw to shares unless the owner has more shares than the max withdraw and is redeeming // at a loss (when 1 share is worth less than 1 asset). if (_ownerShares > _maxWithdraw) { - uint256 _totalAssets = totalAssets(); - uint256 totalDebt_ = totalDebt(); - if (_totalAssets >= totalDebt_) { - return _maxWithdraw; - } else { - // Convert to shares while rounding up. Since 1 asset is guaranteed to be worth more than - // 1 share and any upwards rounding will not exceed 1 share, we can be sure that when the - // shares are converted back to assets (rounding down) the resulting asset value won't - // exceed `_maxWithdraw`. - uint256 _maxScaledRedeem = _maxWithdraw.mulDiv(totalDebt_, _totalAssets, Math.Rounding.Up); - return _maxScaledRedeem >= _ownerShares ? _ownerShares : _maxScaledRedeem; - } + (bool _success, uint256 _totalAssets) = _tryGetTotalPreciseAssets(); + if (!_success) return 0; + + // Convert to shares while rounding up. Since 1 asset is guaranteed to be worth more than + // 1 share and any upwards rounding will not exceed 1 share, we can be sure that when the + // shares are converted back to assets (rounding down) the resulting asset value won't + // exceed `_maxWithdraw`. + uint256 _maxScaledRedeem = _convertToShares(_maxWithdraw, _totalAssets, totalDebt(), Math.Rounding.Up); + return _maxScaledRedeem >= _ownerShares ? _ownerShares : _maxScaledRedeem; } else { return _ownerShares; } @@ -473,23 +463,17 @@ contract PrizeVault is TwabERC20, Claimable, IERC4626, ILiquidationSource, Ownab /// @inheritdoc IERC4626 /// @dev Reverts if `totalAssets` in the vault is zero function previewWithdraw(uint256 _assets) public view returns (uint256) { - uint256 _totalAssets = totalAssets(); + uint256 _totalAssets = totalPreciseAssets(); // No withdrawals can occur if the vault controls no assets. if (_totalAssets == 0) revert ZeroTotalAssets(); - uint256 totalDebt_ = totalDebt(); - if (_totalAssets >= totalDebt_) { - return _assets; - } else { - // Follows the inverse conversion of `convertToAssets` - return _assets.mulDiv(totalDebt_, _totalAssets, Math.Rounding.Up); - } + return _convertToShares(_assets, _totalAssets, totalDebt(), Math.Rounding.Up); } /// @inheritdoc IERC4626 function previewRedeem(uint256 _shares) public view returns (uint256) { - return convertToAssets(_shares); + return _convertToAssets(_shares, totalPreciseAssets(), totalDebt(), Math.Rounding.Down); } /// @inheritdoc IERC4626 @@ -636,6 +620,17 @@ contract PrizeVault is TwabERC20, Claimable, IERC4626, ILiquidationSource, Ownab return totalSupply() + yieldFeeBalance; } + /// @notice Calculates the amount of assets the vault controls based on current onchain conditions. + /// @dev The latent asset balance is included in the total asset count to account for the "dust collection + /// strategy". + /// @dev This function should be favored over `totalAssets` for state-changing functions since it uses + /// `previewRedeem` over `convertToAssets`. + /// @dev May revert for reasons that would cause `yieldVault.previewRedeem` to revert. + /// @return The total assets controlled by the vault based on current onchain conditions + function totalPreciseAssets() public view returns (uint256) { + return yieldVault.previewRedeem(yieldVault.balanceOf(address(this))) + _asset.balanceOf(address(this)); + } + //////////////////////////////////////////////////////////////////////////////// // Yield Functions //////////////////////////////////////////////////////////////////////////////// @@ -644,20 +639,20 @@ contract PrizeVault is TwabERC20, Claimable, IERC4626, ILiquidationSource, Ownab /// @dev Equal to total assets minus total debt /// @return The total yield balance function totalYieldBalance() public view returns (uint256) { - return _totalYieldBalance(totalAssets(), totalDebt()); + return _totalYieldBalance(totalPreciseAssets(), totalDebt()); } /// @notice Total available yield on the vault /// @dev Equal to total assets minus total allocation (total debt + yield buffer) /// @return The available yield balance function availableYieldBalance() public view returns (uint256) { - return _availableYieldBalance(totalAssets(), totalDebt()); + return _availableYieldBalance(totalPreciseAssets(), totalDebt()); } /// @notice Current amount of assets available in the yield buffer /// @return The available assets in the yield buffer function currentYieldBuffer() external view returns (uint256) { - uint256 totalYieldBalance_ = _totalYieldBalance(totalAssets(), totalDebt()); + uint256 totalYieldBalance_ = _totalYieldBalance(totalPreciseAssets(), totalDebt()); uint256 _yieldBuffer = yieldBuffer; if (totalYieldBalance_ >= _yieldBuffer) { return _yieldBuffer; @@ -703,7 +698,7 @@ contract PrizeVault is TwabERC20, Claimable, IERC4626, ILiquidationSource, Ownab // The liquid yield is limited by the max that can be minted or withdrawn, depending on // `_tokenOut`. - uint256 _availableYield = _availableYieldBalance(totalAssets(), _totalDebt); + uint256 _availableYield = _availableYieldBalance(totalPreciseAssets(), _totalDebt); uint256 _liquidYield = _availableYield >= _maxAmountOut ? _maxAmountOut : _availableYield; // The final balance is computed by taking the liquid yield and multiplying it by @@ -724,7 +719,7 @@ contract PrizeVault is TwabERC20, Claimable, IERC4626, ILiquidationSource, Ownab if (_amountOut == 0) revert LiquidationAmountOutZero(); uint256 _totalDebtBefore = totalDebt(); - uint256 _availableYield = _availableYieldBalance(totalAssets(), _totalDebtBefore); + uint256 _availableYield = _availableYieldBalance(totalPreciseAssets(), _totalDebtBefore); uint32 _yieldFeePercentage = yieldFeePercentage; // Determine the proportional yield fee based on the amount being liquidated: @@ -844,6 +839,65 @@ contract PrizeVault is TwabERC20, Claimable, IERC4626, ILiquidationSource, Ownab return (false, 0); } + /// @notice Calculates the amount of assets the vault controls based on current onchain conditions. + /// @dev Calls `totalPreciseAssets` externally so it can catch `previewRedeem` failures and return + /// whether or not the call was successful. + /// @return _success Returns true if totalAssets was successfully calculated and false otherwise + /// @return _totalAssets The total assets controlled by the vault based on current onchain conditions + function _tryGetTotalPreciseAssets() internal view returns (bool _success, uint256 _totalAssets) { + try this.totalPreciseAssets() returns (uint256 _totalPreciseAssets) { + _success = true; + _totalAssets = _totalPreciseAssets; + } catch { + _success = false; + _totalAssets = 0; + } + } + + /// @notice Converts assets to shares with the given vault state and rounding direction. + /// @param _assets The assets to convert + /// @param _totalAssets The total assets that the vault controls + /// @param _totalDebt The total debt the vault owes + /// @param _rounding The rounding direction for the conversion + /// @return The resulting share balance + function _convertToShares( + uint256 _assets, + uint256 _totalAssets, + uint256 _totalDebt, + Math.Rounding _rounding + ) internal pure returns (uint256) { + if (_totalAssets >= _totalDebt) { + return _assets; + } else { + // If the vault controls less assets than what has been deposited a share will be worth a + // proportional amount of the total assets. This can happen due to fees, slippage, or loss + // of funds in the underlying yield vault. + return _assets.mulDiv(_totalDebt, _totalAssets, _rounding); + } + } + + /// @notice Converts shares to assets with the given vault state and rounding direction. + /// @param _shares The shares to convert + /// @param _totalAssets The total assets that the vault controls + /// @param _totalDebt The total debt the vault owes + /// @param _rounding The rounding direction for the conversion + /// @return The resulting asset balance + function _convertToAssets( + uint256 _shares, + uint256 _totalAssets, + uint256 _totalDebt, + Math.Rounding _rounding + ) internal pure returns (uint256) { + if (_totalAssets >= _totalDebt) { + return _shares; + } else { + // If the vault controls less assets than what has been deposited a share will be worth a + // proportional amount of the total assets. This can happen due to fees, slippage, or loss + // of funds in the underlying yield vault. + return _shares.mulDiv(_totalAssets, _totalDebt, _rounding); + } + } + /// @notice Returns the shares that can be minted without exceeding the TwabController supply limit. /// @dev The TwabController limits the total supply for each vault. /// @param _existingShares The current allocated prize vault shares (internal and external) @@ -933,8 +987,8 @@ contract PrizeVault is TwabERC20, Claimable, IERC4626, ILiquidationSource, Ownab // Enforce the mint limit and protect against lossy deposits. uint256 _totalDebtBeforeMint = totalDebt(); _enforceMintLimit(_totalDebtBeforeMint, _shares); - if (totalAssets() < _totalDebtBeforeMint + _shares) { - revert LossyDeposit(totalAssets(), _totalDebtBeforeMint + _shares); + if (totalPreciseAssets() < _totalDebtBeforeMint + _shares) { + revert LossyDeposit(totalPreciseAssets(), _totalDebtBeforeMint + _shares); } _mint(_receiver, _shares); diff --git a/test/contracts/wrapper/PrizeVaultWrapper.sol b/test/contracts/wrapper/PrizeVaultWrapper.sol index df88d90..3419dd2 100644 --- a/test/contracts/wrapper/PrizeVaultWrapper.sol +++ b/test/contracts/wrapper/PrizeVaultWrapper.sol @@ -21,6 +21,10 @@ contract PrizeVaultWrapper is PrizeVault { return _tryGetAssetDecimals(asset_); } + function tryGetTotalPreciseAssets() public view returns (bool, uint256) { + return _tryGetTotalPreciseAssets(); + } + function depositAndMint(address _caller, address _receiver, uint256 _assets, uint256 _shares) public { _depositAndMint(_caller, _receiver, _assets, _shares); } diff --git a/test/fuzz/PrizeVault/ERC4626AndLiquidation.t.sol b/test/fuzz/PrizeVault/ERC4626AndLiquidation.t.sol index 16f26f3..427e882 100644 --- a/test/fuzz/PrizeVault/ERC4626AndLiquidation.t.sol +++ b/test/fuzz/PrizeVault/ERC4626AndLiquidation.t.sol @@ -159,7 +159,7 @@ contract PrizeVaultERC4626AndLiquidationFuzzTest is ERC4626Test { abi.encodeWithSelector(PrizeVault.liquidatableBalanceOf.selector, _underlying_) ); - uint256 totalAssets = prizeVault.totalAssets(); + uint256 totalAssets = prizeVault.totalPreciseAssets(); uint256 totalDebt = prizeVault.totalDebt(); if (totalAssets < totalDebt + yieldBuffer) { diff --git a/test/integration/BaseIntegration.t.sol b/test/integration/BaseIntegration.t.sol index a008ffb..99975bf 100644 --- a/test/integration/BaseIntegration.t.sol +++ b/test/integration/BaseIntegration.t.sol @@ -164,9 +164,9 @@ abstract contract BaseIntegration is Test, Permit { /// @dev Each integration test must override the `_accrueYield` internal function for this to work. function accrueYield() public returns (uint256) { - uint256 assetsBefore = prizeVault.totalAssets(); + uint256 assetsBefore = prizeVault.totalPreciseAssets(); _accrueYield(); - uint256 assetsAfter = prizeVault.totalAssets(); + uint256 assetsAfter = prizeVault.totalPreciseAssets(); if (yieldVault.balanceOf(address(prizeVault)) > 0) { // if the prize vault has any yield vault shares, check to ensure yield has accrued require(assetsAfter > assetsBefore, "yield did not accrue"); @@ -185,9 +185,9 @@ abstract contract BaseIntegration is Test, Permit { /// @dev Each integration test must override the `_simulateLoss` internal function for this to work. /// @return The loss the prize vault has incurred as a result of yield vault loss (if any) function simulateLoss() public returns (uint256) { - uint256 assetsBefore = prizeVault.totalAssets(); + uint256 assetsBefore = prizeVault.totalPreciseAssets(); _simulateLoss(); - uint256 assetsAfter = prizeVault.totalAssets(); + uint256 assetsAfter = prizeVault.totalPreciseAssets(); if (yieldVault.balanceOf(address(prizeVault)) > 0) { // if the prize vault has any yield vault shares, check to ensure some loss has occurred require(assetsAfter < assetsBefore, "loss not simulated"); @@ -248,7 +248,7 @@ abstract contract BaseIntegration is Test, Permit { uint256 amount = 10 ** assetDecimals; dealAssets(alice, amount); - uint256 totalAssetsBefore = prizeVault.totalAssets(); + uint256 totalAssetsBefore = prizeVault.totalPreciseAssets(); uint256 totalSupplyBefore = prizeVault.totalSupply(); startPrank(alice); @@ -256,7 +256,7 @@ abstract contract BaseIntegration is Test, Permit { prizeVault.deposit(amount, alice); stopPrank(); - uint256 totalAssetsAfter = prizeVault.totalAssets(); + uint256 totalAssetsAfter = prizeVault.totalPreciseAssets(); uint256 totalSupplyAfter = prizeVault.totalSupply(); assertEq(prizeVault.balanceOf(alice), amount, "shares minted"); @@ -275,7 +275,7 @@ abstract contract BaseIntegration is Test, Permit { uint256 amount = (10 ** assetDecimals) * (i + 1); dealAssets(depositors[i], amount); - uint256 totalAssetsBefore = prizeVault.totalAssets(); + uint256 totalAssetsBefore = prizeVault.totalPreciseAssets(); uint256 totalSupplyBefore = prizeVault.totalSupply(); startPrank(depositors[i]); @@ -283,7 +283,7 @@ abstract contract BaseIntegration is Test, Permit { prizeVault.deposit(amount, depositors[i]); stopPrank(); - uint256 totalAssetsAfter = prizeVault.totalAssets(); + uint256 totalAssetsAfter = prizeVault.totalPreciseAssets(); uint256 totalSupplyAfter = prizeVault.totalSupply(); assertEq(prizeVault.balanceOf(depositors[i]), amount, "shares minted"); @@ -302,7 +302,7 @@ abstract contract BaseIntegration is Test, Permit { uint256 amount = (10 ** assetDecimals) * (i + 1); dealAssets(depositors[i], amount); - uint256 totalAssetsBefore = prizeVault.totalAssets(); + uint256 totalAssetsBefore = prizeVault.totalPreciseAssets(); uint256 totalSupplyBefore = prizeVault.totalSupply(); startPrank(depositors[i]); @@ -310,7 +310,7 @@ abstract contract BaseIntegration is Test, Permit { prizeVault.deposit(amount, depositors[i]); stopPrank(); - uint256 totalAssetsAfter = prizeVault.totalAssets(); + uint256 totalAssetsAfter = prizeVault.totalPreciseAssets(); uint256 totalSupplyAfter = prizeVault.totalSupply(); assertEq(prizeVault.balanceOf(depositors[i]), amount, "shares minted"); @@ -334,7 +334,7 @@ abstract contract BaseIntegration is Test, Permit { uint256 amount = 10 ** assetDecimals; dealAssets(alice, amount); - uint256 totalAssetsBefore = prizeVault.totalAssets(); + uint256 totalAssetsBefore = prizeVault.totalPreciseAssets(); uint256 totalSupplyBefore = prizeVault.totalSupply(); startPrank(alice); @@ -343,7 +343,7 @@ abstract contract BaseIntegration is Test, Permit { prizeVault.withdraw(amount, alice, alice); stopPrank(); - uint256 totalAssetsAfter = prizeVault.totalAssets(); + uint256 totalAssetsAfter = prizeVault.totalPreciseAssets(); uint256 totalSupplyAfter = prizeVault.totalSupply(); assertEq(prizeVault.balanceOf(alice), 0, "burns all user shares on full withdraw"); @@ -357,7 +357,7 @@ abstract contract BaseIntegration is Test, Permit { uint256 amount = 10 ** assetDecimals; dealAssets(alice, amount); - uint256 totalAssetsBefore = prizeVault.totalAssets(); + uint256 totalAssetsBefore = prizeVault.totalPreciseAssets(); uint256 totalSupplyBefore = prizeVault.totalSupply(); startPrank(alice); @@ -367,7 +367,7 @@ abstract contract BaseIntegration is Test, Permit { prizeVault.withdraw(amount, alice, alice); stopPrank(); - uint256 totalAssetsAfter = prizeVault.totalAssets(); + uint256 totalAssetsAfter = prizeVault.totalPreciseAssets(); uint256 totalSupplyAfter = prizeVault.totalSupply(); assertEq(prizeVault.balanceOf(alice), 0, "burns all user shares on full withdraw"); @@ -397,14 +397,14 @@ abstract contract BaseIntegration is Test, Permit { // withdraw for (uint256 i = 0; i < depositors.length; i++) { uint256 amount = (10 ** assetDecimals) * (i + 1); - uint256 totalAssetsBefore = prizeVault.totalAssets(); + uint256 totalAssetsBefore = prizeVault.totalPreciseAssets(); uint256 totalSupplyBefore = prizeVault.totalSupply(); startPrank(depositors[i]); prizeVault.withdraw(amount, depositors[i], depositors[i]); stopPrank(); - uint256 totalAssetsAfter = prizeVault.totalAssets(); + uint256 totalAssetsAfter = prizeVault.totalPreciseAssets(); uint256 totalSupplyAfter = prizeVault.totalSupply(); assertEq(prizeVault.balanceOf(depositors[i]), 0, "burned all user's shares on withdraw"); @@ -436,12 +436,12 @@ abstract contract BaseIntegration is Test, Permit { simulateLoss(); // ensure prize vault is in lossy state - assertLt(prizeVault.totalAssets(), prizeVault.totalDebt()); + assertLt(prizeVault.totalPreciseAssets(), prizeVault.totalDebt()); // verify all users can withdraw a proportional amount of assets for (uint256 i = 0; i < depositors.length; i++) { uint256 shares = prizeVault.balanceOf(depositors[i]); - uint256 totalAssetsBefore = prizeVault.totalAssets(); + uint256 totalAssetsBefore = prizeVault.totalPreciseAssets(); uint256 totalSupplyBefore = prizeVault.totalSupply(); uint256 totalDebtBefore = prizeVault.totalDebt(); uint256 expectedAssets = (shares * totalAssetsBefore) / totalDebtBefore; @@ -450,7 +450,7 @@ abstract contract BaseIntegration is Test, Permit { uint256 assets = prizeVault.redeem(shares, depositors[i], depositors[i]); stopPrank(); - uint256 totalAssetsAfter = prizeVault.totalAssets(); + uint256 totalAssetsAfter = prizeVault.totalPreciseAssets(); uint256 totalSupplyAfter = prizeVault.totalSupply(); assertEq(assets, expectedAssets, "assets received proportional to shares / totalDebt"); diff --git a/test/invariant/PrizeVault/LossyPrizeVaultInvariant.t.sol b/test/invariant/PrizeVault/LossyPrizeVaultInvariant.t.sol index 123a84f..6f6e49e 100644 --- a/test/invariant/PrizeVault/LossyPrizeVaultInvariant.t.sol +++ b/test/invariant/PrizeVault/LossyPrizeVaultInvariant.t.sol @@ -15,10 +15,10 @@ contract LossyPrizeVaultInvariant is Test { } function invariantDisableDepositsOnLoss() external { - uint256 totalAssets = lossyVaultHarness.vault().totalAssets(); + uint256 totalPreciseAssets = lossyVaultHarness.vault().totalPreciseAssets(); uint256 totalDebt = lossyVaultHarness.vault().totalDebt(); uint256 totalSupply = lossyVaultHarness.vault().totalSupply(); - if (totalDebt > totalAssets || type(uint96).max - totalSupply == 0) { + if (totalDebt > totalPreciseAssets || type(uint96).max - totalSupply == 0) { assertEq(lossyVaultHarness.vault().maxDeposit(address(this)), 0); } else { assertGt(lossyVaultHarness.vault().maxDeposit(address(this)), 0); @@ -26,9 +26,9 @@ contract LossyPrizeVaultInvariant is Test { } function invariantNoYieldWhenDebtExceedsAssets() external { - uint256 totalAssets = lossyVaultHarness.vault().totalAssets(); + uint256 totalPreciseAssets = lossyVaultHarness.vault().totalPreciseAssets(); uint256 totalDebt = lossyVaultHarness.vault().totalDebt(); - if (totalDebt >= totalAssets) { + if (totalDebt >= totalPreciseAssets) { assertEq(lossyVaultHarness.vault().liquidatableBalanceOf(address(lossyVaultHarness.underlyingAsset())), 0); assertEq(lossyVaultHarness.vault().liquidatableBalanceOf(address(lossyVaultHarness.vault())), 0); assertEq(lossyVaultHarness.vault().availableYieldBalance(), 0); @@ -42,22 +42,22 @@ contract LossyPrizeVaultInvariant is Test { } function invariantAllAssetsAccountedFor() external { - uint256 totalAssets = lossyVaultHarness.vault().totalAssets(); + uint256 totalPreciseAssets = lossyVaultHarness.vault().totalPreciseAssets(); uint256 totalDebt = lossyVaultHarness.vault().totalDebt(); - if (totalDebt >= totalAssets) { + if (totalDebt >= totalPreciseAssets) { // 1 wei rounding error since the convertToAssets function rounds down which means up to 1 asset may be lost on total conversion - assertApproxEqAbs(totalAssets, lossyVaultHarness.vault().convertToAssets(totalDebt), 1); + assertApproxEqAbs(totalPreciseAssets, lossyVaultHarness.vault().convertToAssets(totalDebt), 1); } else { // When assets cover debts, we have essentially the same test as the the sister test in `PrizeVaultInvariant.sol` // The debt is converted to assets using `convertToAssets` to test that it will always be 1:1 when the vault has ample collateral. uint256 currentYieldBuffer = lossyVaultHarness.vault().currentYieldBuffer(); uint256 availableYieldBalance = lossyVaultHarness.vault().availableYieldBalance(); uint256 totalAccounted = lossyVaultHarness.vault().convertToAssets(totalDebt) + currentYieldBuffer + availableYieldBalance; - assertEq(totalAssets, totalAccounted); + assertEq(totalPreciseAssets, totalAccounted); // totalYieldBalance = currentYieldBuffer + availableYieldBalance uint256 totalAccounted2 = totalDebt + lossyVaultHarness.vault().totalYieldBalance(); - assertEq(totalAssets, totalAccounted2); + assertEq(totalPreciseAssets, totalAccounted2); } } } \ No newline at end of file diff --git a/test/invariant/PrizeVault/PrizeVaultInvariant.t.sol b/test/invariant/PrizeVault/PrizeVaultInvariant.t.sol index 745481c..ad04e5b 100644 --- a/test/invariant/PrizeVault/PrizeVaultInvariant.t.sol +++ b/test/invariant/PrizeVault/PrizeVaultInvariant.t.sol @@ -22,7 +22,7 @@ contract PrizeVaultInvariant is Test { } function invariantAssetsCoverDebt() external useCurrentTime { - uint256 totalAssets = vaultHarness.vault().totalAssets(); + uint256 totalAssets = vaultHarness.vault().totalPreciseAssets(); uint256 totalDebt = vaultHarness.vault().totalDebt(); assertGe(totalAssets, totalDebt); } @@ -55,7 +55,7 @@ contract PrizeVaultInvariant is Test { function invariantAllAssetsAccountedFor() external useCurrentTime { PrizeVault vault = vaultHarness.vault(); - uint256 totalAssets = vault.totalAssets(); + uint256 totalAssets = vault.totalPreciseAssets(); uint256 totalDebt = vault.totalDebt(); uint256 currentYieldBuffer = vault.currentYieldBuffer(); uint256 availableYieldBalance = vault.availableYieldBalance(); diff --git a/test/unit/PrizeVault/AaveV3WhilePaused.t.sol b/test/unit/PrizeVault/AaveV3WhilePaused.t.sol index 17111ec..d680d21 100644 --- a/test/unit/PrizeVault/AaveV3WhilePaused.t.sol +++ b/test/unit/PrizeVault/AaveV3WhilePaused.t.sol @@ -250,9 +250,9 @@ contract AaveV3WhilePaused is UnitBaseSetup { // liquidating shares doesn't require the assets to be withdrawn from aave, so this will still work when paused. function testLiquidateSharesSucceeds() external { // accrue yield by letting time pass - uint256 totalAssetsBefore = vault.totalAssets(); + uint256 totalAssetsBefore = vault.totalPreciseAssets(); vm.warp(block.timestamp + 60 * 60 * 24); - uint256 totalAssetsAfter = vault.totalAssets(); + uint256 totalAssetsAfter = vault.totalPreciseAssets(); assertGt(totalAssetsAfter, totalAssetsBefore); // check share liquidation @@ -270,9 +270,9 @@ contract AaveV3WhilePaused is UnitBaseSetup { // liquidating assets requires that the assets are able to be withdrawn, which is not the case when paused. function testLiquidatableAssetsZero() external { // accrue yield by letting time pass - uint256 totalAssetsBefore = vault.totalAssets(); + uint256 totalAssetsBefore = vault.totalPreciseAssets(); vm.warp(block.timestamp + 60 * 60 * 24); - uint256 totalAssetsAfter = vault.totalAssets(); + uint256 totalAssetsAfter = vault.totalPreciseAssets(); assertGt(totalAssetsAfter, totalAssetsBefore); // check asset liquidation diff --git a/test/unit/PrizeVault/PrizeVault.t.sol b/test/unit/PrizeVault/PrizeVault.t.sol index 7d9b966..cc2e9e7 100644 --- a/test/unit/PrizeVault/PrizeVault.t.sol +++ b/test/unit/PrizeVault/PrizeVault.t.sol @@ -142,7 +142,7 @@ contract PrizeVaultTest is UnitBaseSetup { vault.deposit(1e18, alice); vm.stopPrank(); - assertEq(vault.totalAssets(), 1e18); + assertEq(vault.totalPreciseAssets(), 1e18); assertEq(vault.totalSupply(), 1e18); assertEq(vault.totalDebt(), 1e18); @@ -154,7 +154,7 @@ contract PrizeVaultTest is UnitBaseSetup { uint256 yieldFee = (1e18 - vault.yieldBuffer()) / (2 * 10); // 10% yield fee + 90% amountOut = 100% vault.transferTokensOut(address(0), bob, address(underlyingAsset), amountOut); - assertEq(vault.totalAssets(), 1e18 + 1e18 - amountOut); // existing balance + yield - amountOut + assertEq(vault.totalPreciseAssets(), 1e18 + 1e18 - amountOut); // existing balance + yield - amountOut assertEq(vault.totalSupply(), 1e18); // no change in supply since liquidation was for assets assertEq(vault.totalDebt(), 1e18 + yieldFee); // debt increased since we reserved shares for the yield fee @@ -201,6 +201,57 @@ contract PrizeVaultTest is UnitBaseSetup { ); } + /* ============ tryGetTotalPreciseAssets ============ */ + + function testTryGetTotalPreciseAssets() public { + { + (bool success, uint256 totalAssets) = vault.tryGetTotalPreciseAssets(); + assertEq(success, true); + assertEq(totalAssets, 0); + } + + // deposit some assets + underlyingAsset.mint(alice, 1e18); + vm.startPrank(alice); + underlyingAsset.approve(address(vault), 1e18); + vault.deposit(1e18, alice); + vm.stopPrank(); + + { + (bool success, uint256 totalAssets) = vault.tryGetTotalPreciseAssets(); + assertEq(success, true); + assertEq(totalAssets, 1e18); + } + } + + function testTryGetTotalPreciseAssets_FailsIfPreviewRedeemFails() public { + vm.mockCallRevert(address(yieldVault), abi.encodeWithSelector(IERC4626.previewRedeem.selector, 0), "force previewRedeem fail"); + (bool success, uint256 totalAssets) = vault.tryGetTotalPreciseAssets(); + assertEq(success, false); + assertEq(totalAssets, 0); + } + + /* ============ totalAssets ============ */ + + function testTotalAssets() public { + { + uint256 _totalAssets = vault.totalAssets(); + assertEq(_totalAssets, 0); + } + + // deposit some assets + underlyingAsset.mint(alice, 1e18); + vm.startPrank(alice); + underlyingAsset.approve(address(vault), 1e18); + vault.deposit(1e18, alice); + vm.stopPrank(); + + { + uint256 _totalAssets = vault.totalAssets(); + assertEq(_totalAssets, 1e18); + } + } + /* ============ maxDeposit / maxMint ============ */ function testMaxDeposit_SubtractsLatentBalance() public { @@ -234,6 +285,13 @@ contract PrizeVaultTest is UnitBaseSetup { assertEq(vault.maxDeposit(address(this)), uint256(type(uint96).max) - deposited); // remaining deposit room } + function testMaxDeposit_ReturnsZeroIfTotalPreciseAssetsFails() public { + assertGt(vault.maxDeposit(address(this)), 0); + + vm.mockCallRevert(address(yieldVault), abi.encodeWithSelector(IERC4626.previewRedeem.selector, 0), "force previewRedeem fail"); + assertEq(vault.maxDeposit(address(this)), 0); + } + /* ============ maxWithdraw ============ */ /// @dev all withdraw/redeem flows in prize vault go through the yield vault redeem, so the prize vault max must be limited appropriately @@ -258,6 +316,20 @@ contract PrizeVaultTest is UnitBaseSetup { assertEq(vault.maxWithdraw(address(this)), 0); } + function testMaxWithdraw_ReturnsZeroIfTotalPreciseAssetsFails() public { + // deposit some assets + underlyingAsset.mint(alice, 1e18); + vm.startPrank(alice); + underlyingAsset.approve(address(vault), 1e18); + vault.deposit(1e18, alice); + vm.stopPrank(); + + assertGt(vault.maxWithdraw(alice), 0); + + vm.mockCallRevert(address(yieldVault), abi.encodeWithSelector(IERC4626.previewRedeem.selector, yieldVault.balanceOf(address(vault))), "force previewRedeem fail"); + assertEq(vault.maxWithdraw(alice), 0); + } + /* ============ maxRedeem ============ */ /// @dev all withdraw/redeem flows in prize vault go through the yield vault redeem, so the prize vault max must be limited appropriately @@ -317,6 +389,22 @@ contract PrizeVaultTest is UnitBaseSetup { assertEq(vault.maxRedeem(address(this)), deposited / 2); } + function testMaxRedeem_ReturnsZeroIfTotalPreciseAssetsFails() public { + // deposit some assets + underlyingAsset.mint(alice, 1e18); + vm.startPrank(alice); + underlyingAsset.approve(address(vault), 1e18); + vault.deposit(1e18, alice); + vm.stopPrank(); + + assertGt(vault.maxRedeem(alice), 0); + + // mock a maxRedeem for the yield vault so that we get the desired branch on vault.maxRedeem() + vm.mockCall(address(yieldVault), abi.encodeWithSelector(IERC4626.maxRedeem.selector, address(vault)), abi.encode(yieldVault.balanceOf(address(vault)) / 2)); + vm.mockCallRevert(address(yieldVault), abi.encodeWithSelector(IERC4626.previewRedeem.selector, yieldVault.balanceOf(address(vault))), "force previewRedeem fail"); + assertEq(vault.maxRedeem(alice), 0); + } + /* ============ previewWithdraw ============ */ function testPreviewWithdraw() public { @@ -346,7 +434,7 @@ contract PrizeVaultTest is UnitBaseSetup { } function testPreviewWithdraw_ZeroTotalAssets() public { - assertEq(vault.totalAssets(), 0); + assertEq(vault.totalPreciseAssets(), 0); vm.expectRevert(abi.encodeWithSelector(PrizeVault.ZeroTotalAssets.selector)); vault.previewWithdraw(1); } diff --git a/test/unit/PrizeVault/WithdrawalLimits.t.sol b/test/unit/PrizeVault/WithdrawalLimits.t.sol index 91a13a4..d024f82 100644 --- a/test/unit/PrizeVault/WithdrawalLimits.t.sol +++ b/test/unit/PrizeVault/WithdrawalLimits.t.sol @@ -50,7 +50,7 @@ contract PrizeVaultWithdrawalLimitsTest is UnitBaseSetup { vm.stopPrank(); assertEq(vault.balanceOf(alice), 100); - assertEq(vault.totalAssets(), 100); + assertEq(vault.totalPreciseAssets(), 100); assertEq(yieldVault.balanceOf(address(vault)), 10); // set max withdraw on yield vault to 95 assets and max redeem to 9 shares @@ -74,7 +74,7 @@ contract PrizeVaultWithdrawalLimitsTest is UnitBaseSetup { _yieldVaultMaxSetter.setMaxWithdraw(5); _yieldVaultMaxSetter.setMaxRedeem(0); - assertEq(vault.totalAssets(), 10); + assertEq(vault.totalPreciseAssets(), 10); assertEq(vault.totalSupply(), 10); assertEq(yieldVault.balanceOf(address(vault)), 1); assertEq(vault.maxWithdraw(alice), 0); // no yv shares can be redeemed, so no pv assets can be withdrawn diff --git a/test/unit/PrizeVault/WithdrawalSlippage.t.sol b/test/unit/PrizeVault/WithdrawalSlippage.t.sol index 3c99b37..5f71995 100644 --- a/test/unit/PrizeVault/WithdrawalSlippage.t.sol +++ b/test/unit/PrizeVault/WithdrawalSlippage.t.sol @@ -18,14 +18,14 @@ contract PrizeVaultWithdrawalSlippageTest is UnitBaseSetup { vm.stopPrank(); assertEq(vault.balanceOf(alice), 100); - assertEq(vault.totalAssets(), 100); + assertEq(vault.totalPreciseAssets(), 100); // yield vault loses 50% of assets vm.startPrank(address(yieldVault)); underlyingAsset.burn(address(yieldVault), 50); vm.stopPrank(); - assertEq(vault.totalAssets(), 50); + assertEq(vault.totalPreciseAssets(), 50); // alice should be able to withdraw up to 50 assets for 100 shares assertEq(vault.maxWithdraw(alice), 50); @@ -65,14 +65,14 @@ contract PrizeVaultWithdrawalSlippageTest is UnitBaseSetup { vm.stopPrank(); assertEq(vault.balanceOf(alice), 100); - assertEq(vault.totalAssets(), 100); + assertEq(vault.totalPreciseAssets(), 100); // yield vault loses 50% of assets vm.startPrank(address(yieldVault)); underlyingAsset.burn(address(yieldVault), 50); vm.stopPrank(); - assertEq(vault.totalAssets(), 50); + assertEq(vault.totalPreciseAssets(), 50); // alice should be able to redeem up to 100 shares for 50 assets assertEq(vault.maxWithdraw(alice), 50);