Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(YAUDIT-COVE-6): correct yearn vault v2 preview functions #302

Merged
merged 7 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ ignore:
- "src/deps/*"
- "src/deps/**/*"
- "src/interfaces/deps/**/*"
- "src/libraries/YearnVaultV2Helper.sol"
57 changes: 7 additions & 50 deletions src/Yearn4626RouterExt.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { IPermit2 } from "permit2/interfaces/IPermit2.sol";
import { ISignatureTransfer } from "permit2/interfaces/ISignatureTransfer.sol";
import { IWETH9 } from "Yearn-ERC4626-Router/external/PeripheryPayments.sol";
import { IYearn4626RouterExt } from "./interfaces/IYearn4626RouterExt.sol";
import { IERC20Metadata, IERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { YearnVaultV2Helper } from "./libraries/YearnVaultV2Helper.sol";
import { SafeERC20, IERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol";
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
import { Address } from "@openzeppelin/contracts/utils/Address.sol";
Expand All @@ -25,6 +25,7 @@ import { IStakeDaoVault } from "./interfaces/deps/stakeDAO/IStakeDaoVault.sol";
*/
contract Yearn4626RouterExt is IYearn4626RouterExt, Yearn4626Router {
using SafeERC20 for IERC20;
using YearnVaultV2Helper for IYearnVaultV2;

// slither-disable-next-line naming-convention
IPermit2 private immutable _PERMIT2;
Expand Down Expand Up @@ -60,30 +61,6 @@ contract Yearn4626RouterExt is IYearn4626RouterExt, Yearn4626Router {

// ------------- YEARN VAULT V2 FUNCTIONS ------------- //

/**
* @notice Deposits the specified `amount` of tokens into the Yearn Vault V2.
* @dev Calls the `deposit` function of the Yearn Vault V2 contract and checks if the returned shares are above the
* `minSharesOut`.
* Reverts with `InsufficientShares` if the condition is not met.
* @param vault The Yearn Vault V2 contract instance.
* @param amount The amount of tokens to deposit.
* @param to The address to which the shares will be minted.
* @param minSharesOut The minimum number of shares expected to be received.
* @return sharesOut The actual number of shares minted to the `to` address.
*/
function depositToVaultV2(
IYearnVaultV2 vault,
uint256 amount,
address to,
uint256 minSharesOut
)
public
payable
returns (uint256 sharesOut)
{
if ((sharesOut = vault.deposit(amount, to)) < minSharesOut) revert InsufficientShares();
}

/**
* @notice Redeems the specified `shares` from the Yearn Vault V2.
* @dev The shares must exist in this router before calling this function.
Expand Down Expand Up @@ -230,12 +207,7 @@ contract Yearn4626RouterExt is IYearn4626RouterExt, Yearn4626Router {
(success, data) = vault.staticcall(abi.encodeCall(IYearnVaultV2.token, ()));
if (success) {
vaultAsset = abi.decode(data, (address));
sharesOut[i] = Math.mulDiv(
assetsIn,
10 ** IERC20Metadata(vault).decimals(),
IYearnVaultV2(vault).pricePerShare(),
Math.Rounding.Down
) - 1;
sharesOut[i] = IYearnVaultV2(vault).previewDeposit(assetsIn);
} else {
revert PreviewNonVaultAddressInPath(vault);
}
Expand Down Expand Up @@ -289,12 +261,7 @@ contract Yearn4626RouterExt is IYearn4626RouterExt, Yearn4626Router {
(success, data) = vault.staticcall(abi.encodeCall(IYearnVaultV2.token, ()));
if (success) {
vaultAsset = abi.decode(data, (address));
assetsIn[i - 1] = Math.mulDiv(
sharesOut,
IYearnVaultV2(vault).pricePerShare(),
10 ** IERC20Metadata(vault).decimals(),
Math.Rounding.Up
) + 1;
assetsIn[i - 1] = IYearnVaultV2(vault).previewMint(sharesOut);
} else {
revert PreviewNonVaultAddressInPath(vault);
}
Expand Down Expand Up @@ -349,12 +316,7 @@ contract Yearn4626RouterExt is IYearn4626RouterExt, Yearn4626Router {
(success, data) = vault.staticcall(abi.encodeCall(IYearnVaultV2.token, ()));
if (success) {
vaultAsset = abi.decode(data, (address));
sharesIn[i] = Math.mulDiv(
assetsOut,
10 ** IERC20Metadata(vault).decimals(),
IYearnVaultV2(vault).pricePerShare(),
Math.Rounding.Up
);
sharesIn[i] = IYearnVaultV2(vault).previewWithdraw(assetsOut);
} else {
// StakeDAO gauge token
// StakeDaoGauge.staking_token().token() is the yearn vault v2 token
Expand Down Expand Up @@ -413,12 +375,7 @@ contract Yearn4626RouterExt is IYearn4626RouterExt, Yearn4626Router {
(success, data) = vault.staticcall(abi.encodeCall(IYearnVaultV2.token, ()));
if (success) {
vaultAsset = abi.decode(data, (address));
assetsOut[i] = Math.mulDiv(
sharesIn,
IYearnVaultV2(vault).pricePerShare(),
10 ** IERC20Metadata(vault).decimals(),
Math.Rounding.Down
);
assetsOut[i] = IYearnVaultV2(vault).previewRedeem(sharesIn);
} else {
// StakeDAO gauge token
// StakeDaoGauge.staking_token().token() is the yearn vault v2 token
Expand Down
10 changes: 0 additions & 10 deletions src/interfaces/IYearn4626RouterExt.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,6 @@ import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol";
import { IStakeDaoGauge } from "./deps/stakeDAO/IStakeDaoGauge.sol";

interface IYearn4626RouterExt is IYearn4626Router {
function depositToVaultV2(
IYearnVaultV2 vault,
uint256 amount,
address to,
uint256 minSharesOut
)
external
payable
returns (uint256 sharesOut);

function redeemVaultV2(
IYearnVaultV2 vault,
uint256 shares,
Expand Down
5 changes: 5 additions & 0 deletions src/interfaces/deps/yearn/veYFI/IYearnVaultV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@ interface IYearnVaultV2 {
function deposit(uint256 amount) external returns (uint256 shares);
function withdraw(uint256 shares, address recipient) external returns (uint256 amount);
function pricePerShare() external view returns (uint256);
function totalSupply() external view returns (uint256);
function totalAssets() external view returns (uint256);
function lastReport() external view returns (uint256);
function lockedProfitDegradation() external view returns (uint256);
function lockedProfit() external view returns (uint256);
}
100 changes: 100 additions & 0 deletions src/libraries/YearnVaultV2Helper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.18;

import { IYearnVaultV2 } from "src/interfaces/deps/yearn/veYFI/IYearnVaultV2.sol";
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";

/**
* @title YearnVaultV2Helper
* @notice Helper functions for Yearn Vault V2 contracts. Since Yearn Vault V2 contracts are not ERC-4626 compliant,
* they do not provide `previewDeposit`, `previewMint`, `previewRedeem`, and `previewWithdraw` functions. This library
* provides these functions for previewing share based deposit/mint/redeem/withdraw estimations.
* @dev These functions are only to be used off-chain for previewing. Due to how Yearn Vault V2 contracts work,
* share based withdraw/redeem estimations may not be accurate if the vault incurs a loss, thus share price changes.
* Coverage is currently disabled for this library due to forge limitations. TODO: Once the fix PR is merged,
* https://github.com/foundry-rs/foundry/pull/7510 coverage should be re-enabled.
*/
library YearnVaultV2Helper {
/**
* @notice Calculates the currently free funds in a Yearn Vault V2 contract.
* @param vault The Yearn Vault V2 contract.
* @return The free funds in the vault.
* @dev This is based on Yearn Vault V2 contract's free funds calculation logic.
* https://github.com/yearn/yearn-vaults/blob/97ca1b2e4fcf20f4be0ff456dabd020bfeb6697b/contracts/Vault.vy#L844-L847
*/
function freeFunds(IYearnVaultV2 vault) internal view returns (uint256) {
uint256 lockedProfit = vault.lockedProfit();
uint256 lockedFundsRatio = (block.timestamp - vault.lastReport()) * vault.lockedProfitDegradation();
// slither-disable-next-line timestamp
if (lockedFundsRatio < 1e18) {
lockedProfit -= (lockedProfit * lockedFundsRatio) / 1e18;
} else {
lockedProfit = 0;
}
return vault.totalAssets() - lockedProfit;
}

/**
* @notice Preview the amount of shares to be issued for a given deposit amount.
* @param vault The Yearn Vault V2 contract.
* @param assetsIn The amount of assets to be deposited.
* @return The number of shares that would be issued for the deposited assets.
*/
function previewDeposit(IYearnVaultV2 vault, uint256 assetsIn) internal view returns (uint256) {
uint256 totalSupply = vault.totalSupply();
if (totalSupply > 0) {
return Math.mulDiv(assetsIn, totalSupply, freeFunds(vault), Math.Rounding.Down);
}
return assetsIn;
}

/**
* @notice Preview the amount of assets required to mint a given amount of shares.
* @param vault The Yearn Vault V2 contract.
* @param sharesOut The number of shares to be minted.
* @return The amount of assets required to mint the specified number of shares.
*/
function previewMint(IYearnVaultV2 vault, uint256 sharesOut) internal view returns (uint256) {
uint256 totalSupply = vault.totalSupply();
if (totalSupply > 0) {
return Math.mulDiv(sharesOut, freeFunds(vault), totalSupply, Math.Rounding.Up);
}
return sharesOut;
}

/**
* @notice Preview the amount of assets to be received for redeeming a given amount of shares.
* @param vault The Yearn Vault V2 contract.
* @param sharesIn The number of shares to be redeemed.
* @return The amount of assets that would be received for the redeemed shares.
*/
function previewRedeem(IYearnVaultV2 vault, uint256 sharesIn) internal view returns (uint256) {
uint256 totalSupply = vault.totalSupply();
if (sharesIn > totalSupply) {
return freeFunds(vault);
}
if (totalSupply > 0) {
return Math.mulDiv(sharesIn, freeFunds(vault), totalSupply, Math.Rounding.Down);
}
return 0;
}

/**
* @notice Preview the number of shares to be redeemed for a given withdrawal amount of assets.
* @param vault The Yearn Vault V2 contract.
* @param assetsOut The amount of assets to be withdrawn.
* @return The number of shares that would be redeemed for the withdrawn assets.
*/
function previewWithdraw(IYearnVaultV2 vault, uint256 assetsOut) internal view returns (uint256) {
uint256 freeFunds_ = freeFunds(vault);
// slither-disable-next-line timestamp
if (assetsOut > freeFunds_) {
return vault.totalSupply();
}
// slither-disable-next-line timestamp
if (freeFunds_ > 0) {
return Math.mulDiv(assetsOut, vault.totalSupply(), freeFunds(vault), Math.Rounding.Up);
}
return 0;
}
}
Loading
Loading