You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

272 lines
11 KiB
Solidity

1 year ago
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/extensions/ERC4626.sol)
pragma solidity ^0.8.0;
import "../ERC20.sol";
import "../utils/SafeERC20.sol";
import "../../../interfaces/IERC4626.sol";
import "../../../utils/math/Math.sol";
/**
* @dev Implementation of the ERC4626 "Tokenized Vault Standard" as defined in
* https://eips.ethereum.org/EIPS/eip-4626[EIP-4626].
*
* This extension allows the minting and burning of "shares" (represented using the ERC20 inheritance) in exchange for
* underlying "assets" through standardized {deposit}, {mint}, {redeem} and {burn} workflows. This contract extends
* the ERC20 standard. Any additional extensions included along it would affect the "shares" token represented by this
* contract and not the "assets" token which is an independent contract.
*
* CAUTION: When the vault is empty or nearly empty, deposits are at high risk of being stolen through frontrunning with
* a "donation" to the vault that inflates the price of a share. This is variously known as a donation or inflation
* attack and is essentially a problem of slippage. Vault deployers can protect against this attack by making an initial
* deposit of a non-trivial amount of the asset, such that price manipulation becomes infeasible. Withdrawals may
* similarly be affected by slippage. Users can protect against this attack as well unexpected slippage in general by
* verifying the amount received is as expected, using a wrapper that performs these checks such as
* https://github.com/fei-protocol/ERC4626#erc4626router-and-base[ERC4626Router].
*
* _Available since v4.7._
*/
abstract contract ERC4626 is ERC20, IERC4626 {
using Math for uint256;
IERC20 private immutable _asset;
uint8 private immutable _decimals;
/**
* @dev Set the underlying asset contract. This must be an ERC20-compatible contract (ERC20 or ERC777).
*/
constructor(IERC20 asset_) {
(bool success, uint8 assetDecimals) = _tryGetAssetDecimals(asset_);
_decimals = success ? assetDecimals : super.decimals();
_asset = asset_;
}
/**
* @dev Attempts to fetch the asset decimals. A return value of false indicates that the attempt failed in some way.
*/
function _tryGetAssetDecimals(IERC20 asset_) private view returns (bool, uint8) {
(bool success, bytes memory encodedDecimals) = address(asset_).staticcall(
abi.encodeWithSelector(IERC20Metadata.decimals.selector)
);
if (success && encodedDecimals.length >= 32) {
uint256 returnedDecimals = abi.decode(encodedDecimals, (uint256));
if (returnedDecimals <= type(uint8).max) {
return (true, uint8(returnedDecimals));
}
}
return (false, 0);
}
/**
* @dev Decimals are read from the underlying asset in the constructor and cached. If this fails (e.g., the asset
* has not been created yet), the cached value is set to a default obtained by `super.decimals()` (which depends on
* inheritance but is most likely 18). Override this function in order to set a guaranteed hardcoded value.
* See {IERC20Metadata-decimals}.
*/
function decimals() public view virtual override(IERC20Metadata, ERC20) returns (uint8) {
return _decimals;
}
/** @dev See {IERC4626-asset}. */
function asset() public view virtual override returns (address) {
return address(_asset);
}
/** @dev See {IERC4626-totalAssets}. */
function totalAssets() public view virtual override returns (uint256) {
return _asset.balanceOf(address(this));
}
/** @dev See {IERC4626-convertToShares}. */
function convertToShares(uint256 assets) public view virtual override returns (uint256) {
return _convertToShares(assets, Math.Rounding.Down);
}
/** @dev See {IERC4626-convertToAssets}. */
function convertToAssets(uint256 shares) public view virtual override returns (uint256) {
return _convertToAssets(shares, Math.Rounding.Down);
}
/** @dev See {IERC4626-maxDeposit}. */
function maxDeposit(address) public view virtual override returns (uint256) {
return _isVaultHealthy() ? type(uint256).max : 0;
}
/** @dev See {IERC4626-maxMint}. */
function maxMint(address) public view virtual override returns (uint256) {
return type(uint256).max;
}
/** @dev See {IERC4626-maxWithdraw}. */
function maxWithdraw(address owner) public view virtual override returns (uint256) {
return _convertToAssets(balanceOf(owner), Math.Rounding.Down);
}
/** @dev See {IERC4626-maxRedeem}. */
function maxRedeem(address owner) public view virtual override returns (uint256) {
return balanceOf(owner);
}
/** @dev See {IERC4626-previewDeposit}. */
function previewDeposit(uint256 assets) public view virtual override returns (uint256) {
return _convertToShares(assets, Math.Rounding.Down);
}
/** @dev See {IERC4626-previewMint}. */
function previewMint(uint256 shares) public view virtual override returns (uint256) {
return _convertToAssets(shares, Math.Rounding.Up);
}
/** @dev See {IERC4626-previewWithdraw}. */
function previewWithdraw(uint256 assets) public view virtual override returns (uint256) {
return _convertToShares(assets, Math.Rounding.Up);
}
/** @dev See {IERC4626-previewRedeem}. */
function previewRedeem(uint256 shares) public view virtual override returns (uint256) {
return _convertToAssets(shares, Math.Rounding.Down);
}
/** @dev See {IERC4626-deposit}. */
function deposit(uint256 assets, address receiver) public virtual override returns (uint256) {
require(assets <= maxDeposit(receiver), "ERC4626: deposit more than max");
uint256 shares = previewDeposit(assets);
_deposit(_msgSender(), receiver, assets, shares);
return shares;
}
/** @dev See {IERC4626-mint}.
*
* As opposed to {deposit}, minting is allowed even if the vault is in a state where the price of a share is zero.
* In this case, the shares will be minted without requiring any assets to be deposited.
*/
function mint(uint256 shares, address receiver) public virtual override returns (uint256) {
require(shares <= maxMint(receiver), "ERC4626: mint more than max");
uint256 assets = previewMint(shares);
_deposit(_msgSender(), receiver, assets, shares);
return assets;
}
/** @dev See {IERC4626-withdraw}. */
function withdraw(uint256 assets, address receiver, address owner) public virtual override returns (uint256) {
require(assets <= maxWithdraw(owner), "ERC4626: withdraw more than max");
uint256 shares = previewWithdraw(assets);
_withdraw(_msgSender(), receiver, owner, assets, shares);
return shares;
}
/** @dev See {IERC4626-redeem}. */
function redeem(uint256 shares, address receiver, address owner) public virtual override returns (uint256) {
require(shares <= maxRedeem(owner), "ERC4626: redeem more than max");
uint256 assets = previewRedeem(shares);
_withdraw(_msgSender(), receiver, owner, assets, shares);
return assets;
}
/**
* @dev Internal conversion function (from assets to shares) with support for rounding direction.
*
* Will revert if assets > 0, totalSupply > 0 and totalAssets = 0. That corresponds to a case where any asset
* would represent an infinite amount of shares.
*/
function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual returns (uint256) {
uint256 supply = totalSupply();
return
(assets == 0 || supply == 0)
? _initialConvertToShares(assets, rounding)
: assets.mulDiv(supply, totalAssets(), rounding);
}
/**
* @dev Internal conversion function (from assets to shares) to apply when the vault is empty.
*
* NOTE: Make sure to keep this function consistent with {_initialConvertToAssets} when overriding it.
*/
function _initialConvertToShares(
uint256 assets,
Math.Rounding /*rounding*/
) internal view virtual returns (uint256 shares) {
return assets;
}
/**
* @dev Internal conversion function (from shares to assets) with support for rounding direction.
*/
function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual returns (uint256) {
uint256 supply = totalSupply();
return
(supply == 0) ? _initialConvertToAssets(shares, rounding) : shares.mulDiv(totalAssets(), supply, rounding);
}
/**
* @dev Internal conversion function (from shares to assets) to apply when the vault is empty.
*
* NOTE: Make sure to keep this function consistent with {_initialConvertToShares} when overriding it.
*/
function _initialConvertToAssets(
uint256 shares,
Math.Rounding /*rounding*/
) internal view virtual returns (uint256) {
return shares;
}
/**
* @dev Deposit/mint common workflow.
*/
function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual {
// If _asset is ERC777, `transferFrom` can trigger a reenterancy BEFORE the transfer happens through the
// `tokensToSend` hook. On the other hand, the `tokenReceived` hook, that is triggered after the transfer,
// calls the vault, which is assumed not malicious.
//
// Conclusion: we need to do the transfer before we mint so that any reentrancy would happen before the
// assets are transferred and before the shares are minted, which is a valid state.
// slither-disable-next-line reentrancy-no-eth
SafeERC20.safeTransferFrom(_asset, caller, address(this), assets);
_mint(receiver, shares);
emit Deposit(caller, receiver, assets, shares);
}
/**
* @dev Withdraw/redeem common workflow.
*/
function _withdraw(
address caller,
address receiver,
address owner,
uint256 assets,
uint256 shares
) internal virtual {
if (caller != owner) {
_spendAllowance(owner, caller, shares);
}
// If _asset is ERC777, `transfer` can trigger a reentrancy AFTER the transfer happens through the
// `tokensReceived` hook. On the other hand, the `tokensToSend` hook, that is triggered before the transfer,
// calls the vault, which is assumed not malicious.
//
// Conclusion: we need to do the transfer after the burn so that any reentrancy would happen after the
// shares are burned and after the assets are transferred, which is a valid state.
_burn(owner, shares);
SafeERC20.safeTransfer(_asset, receiver, assets);
emit Withdraw(caller, receiver, owner, assets, shares);
}
/**
* @dev Checks if vault is "healthy" in the sense of having assets backing the circulating shares.
*/
function _isVaultHealthy() private view returns (bool) {
return totalAssets() > 0 || totalSupply() == 0;
}
}