// SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.7.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: Deposits and withdrawals may incur unexpected slippage. Users should verify that the amount received of * shares or assets is as expected. EOAs should operate through 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; IERC20Metadata private immutable _asset; /** * @dev Set the underlying asset contract. This must be an ERC20-compatible contract (ERC20 or ERC777). */ constructor(IERC20Metadata asset_) { _asset = asset_; } /** @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 shares) { return _convertToShares(assets, Math.Rounding.Down); } /** @dev See {IERC4626-convertToAssets}. */ function convertToAssets(uint256 shares) public view virtual override returns (uint256 assets) { return _convertToAssets(shares, Math.Rounding.Down); } /** @dev See {IERC4626-maxDeposit}. */ function maxDeposit(address) public view virtual override returns (uint256) { return _isVaultCollateralized() ? 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}. */ 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 amout of shares. */ function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual returns (uint256 shares) { uint256 supply = totalSupply(); return (assets == 0 || supply == 0) ? assets.mulDiv(10**decimals(), 10**_asset.decimals(), rounding) : assets.mulDiv(supply, totalAssets(), rounding); } /** * @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 assets) { uint256 supply = totalSupply(); return (supply == 0) ? shares.mulDiv(10**_asset.decimals(), 10**decimals(), rounding) : shares.mulDiv(totalAssets(), supply, rounding); } /** * @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 transfered 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 transfered, which is a valid state. _burn(owner, shares); SafeERC20.safeTransfer(_asset, receiver, assets); emit Withdraw(caller, receiver, owner, assets, shares); } function _isVaultCollateralized() private view returns (bool) { return totalAssets() > 0 || totalSupply() == 0; } }