// SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.6.0) (token/ERC20/extensions/ERC4626.sol) pragma solidity ^0.8.24; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import {ERC20Upgradeable} from "../ERC20Upgradeable.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; import {LowLevelCall} from "@openzeppelin/contracts/utils/LowLevelCall.sol"; import {Memory} from "@openzeppelin/contracts/utils/Memory.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; /** * @dev Implementation of the ERC-4626 "Tokenized Vault Standard" as defined in * https://eips.ethereum.org/EIPS/eip-4626[ERC-4626]. * * This extension allows the minting and burning of "shares" (represented using the ERC-20 inheritance) in exchange for * underlying "assets" through standardized {deposit}, {mint}, {redeem} and {burn} workflows. This contract extends * the ERC-20 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] * ==== * In empty (or nearly empty) ERC-4626 vaults, 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 as 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]. * * Since v4.9, this implementation introduces configurable virtual assets and shares to help developers mitigate that risk. * The `_decimalsOffset()` corresponds to an offset in the decimal representation between the underlying asset's decimals * and the vault decimals. This offset also determines the rate of virtual shares to virtual assets in the vault, which * itself determines the initial exchange rate. While not fully preventing the attack, analysis shows that the default * offset (0) makes it non-profitable even if an attacker is able to capture value from multiple user deposits, as a result * of the value being captured by the virtual shares (out of the attacker's donation) matching the attacker's expected gains. * With a larger offset, the attack becomes orders of magnitude more expensive than it is profitable. More details about the * underlying math can be found xref:ROOT:erc4626.adoc#inflation-attack[here]. * * The drawback of this approach is that the virtual shares do capture (a very small) part of the value being accrued * to the vault. Also, if the vault experiences losses, the users try to exit the vault, the virtual shares and assets * will cause the first user to exit to experience reduced losses in detriment to the last users that will experience * bigger losses. Developers willing to revert back to the pre-v4.9 behavior just need to override the * `_convertToShares` and `_convertToAssets` functions. * * To learn more, check out our xref:ROOT:erc4626.adoc[ERC-4626 guide]. * ==== * * [NOTE] * ==== * When overriding this contract, some elements must be considered: * * * When overriding the behavior of the deposit or withdraw mechanisms, it is recommended to override the internal * functions. Overriding {_deposit} automatically affects both {deposit} and {mint}. Similarly, overriding {_withdraw} * automatically affects both {withdraw} and {redeem}. Overall it is not recommended to override the public facing * functions since that could lead to inconsistent behaviors between the {deposit} and {mint} or between {withdraw} and * {redeem}, which is documented to have led to loss of funds. * * * Overrides to the deposit or withdraw mechanism must be reflected in the preview functions as well. * * * {maxWithdraw} depends on {maxRedeem}. Therefore, overriding {maxRedeem} only is enough. On the other hand, * overriding {maxWithdraw} only would have no effect on {maxRedeem}, and could create an inconsistency between the two * functions. * * * If {previewRedeem} is overridden to revert, {maxWithdraw} must be overridden as necessary to ensure it * always return successfully. * ==== */ abstract contract ERC4626Upgradeable is Initializable, ERC20Upgradeable, IERC4626 { using Math for uint256; /// @custom:storage-location erc7201:openzeppelin.storage.ERC4626 struct ERC4626Storage { IERC20 _asset; uint8 _underlyingDecimals; } // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ERC4626")) - 1)) & ~bytes32(uint256(0xff)) bytes32 private constant ERC4626StorageLocation = 0x0773e532dfede91f04b12a73d3d2acd361424f41f76b4fb79f090161e36b4e00; function _getERC4626Storage() private pure returns (ERC4626Storage storage $) { assembly { $.slot := ERC4626StorageLocation } } /** * @dev Attempted to deposit more assets than the max amount for `receiver`. */ error ERC4626ExceededMaxDeposit(address receiver, uint256 assets, uint256 max); /** * @dev Attempted to mint more shares than the max amount for `receiver`. */ error ERC4626ExceededMaxMint(address receiver, uint256 shares, uint256 max); /** * @dev Attempted to withdraw more assets than the max amount for `owner`. */ error ERC4626ExceededMaxWithdraw(address owner, uint256 assets, uint256 max); /** * @dev Attempted to redeem more shares than the max amount for `owner`. */ error ERC4626ExceededMaxRedeem(address owner, uint256 shares, uint256 max); /** * @dev Set the underlying asset contract. This must be an ERC20-compatible contract (ERC-20 or ERC-777). */ function __ERC4626_init(IERC20 asset_) internal onlyInitializing { __ERC4626_init_unchained(asset_); } function __ERC4626_init_unchained(IERC20 asset_) internal onlyInitializing { ERC4626Storage storage $ = _getERC4626Storage(); (bool success, uint8 assetDecimals) = _tryGetAssetDecimals(asset_); $._underlyingDecimals = success ? assetDecimals : 18; $._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 ok, uint8 assetDecimals) { Memory.Pointer ptr = Memory.getFreeMemoryPointer(); (bool success, bytes32 returnedDecimals, ) = LowLevelCall.staticcallReturn64Bytes( address(asset_), abi.encodeCall(IERC20Metadata.decimals, ()) ); Memory.unsafeSetFreeMemoryPointer(ptr); return (success && LowLevelCall.returnDataSize() >= 32 && uint256(returnedDecimals) <= type(uint8).max) ? (true, uint8(uint256(returnedDecimals))) : (false, 0); } /** * @dev Decimals are computed by adding the decimal offset on top of the underlying asset's decimals. This * "original" value is cached during construction of the vault contract. If this read operation fails (e.g., the * asset has not been created yet), a default of 18 is used to represent the underlying asset's decimals. * * See {IERC20Metadata-decimals}. */ function decimals() public view virtual override(IERC20Metadata, ERC20Upgradeable) returns (uint8) { ERC4626Storage storage $ = _getERC4626Storage(); return $._underlyingDecimals + _decimalsOffset(); } /// @inheritdoc IERC4626 function asset() public view virtual returns (address) { ERC4626Storage storage $ = _getERC4626Storage(); return address($._asset); } /// @inheritdoc IERC4626 function totalAssets() public view virtual returns (uint256) { return IERC20(asset()).balanceOf(address(this)); } /// @inheritdoc IERC4626 function convertToShares(uint256 assets) public view virtual returns (uint256) { return _convertToShares(assets, Math.Rounding.Floor); } /// @inheritdoc IERC4626 function convertToAssets(uint256 shares) public view virtual returns (uint256) { return _convertToAssets(shares, Math.Rounding.Floor); } /// @inheritdoc IERC4626 function maxDeposit(address) public view virtual returns (uint256) { return type(uint256).max; } /// @inheritdoc IERC4626 function maxMint(address) public view virtual returns (uint256) { return type(uint256).max; } /// @inheritdoc IERC4626 function maxWithdraw(address owner) public view virtual returns (uint256) { return previewRedeem(maxRedeem(owner)); } /// @inheritdoc IERC4626 function maxRedeem(address owner) public view virtual returns (uint256) { return balanceOf(owner); } /// @inheritdoc IERC4626 function previewDeposit(uint256 assets) public view virtual returns (uint256) { return _convertToShares(assets, Math.Rounding.Floor); } /// @inheritdoc IERC4626 function previewMint(uint256 shares) public view virtual returns (uint256) { return _convertToAssets(shares, Math.Rounding.Ceil); } /// @inheritdoc IERC4626 function previewWithdraw(uint256 assets) public view virtual returns (uint256) { return _convertToShares(assets, Math.Rounding.Ceil); } /// @inheritdoc IERC4626 function previewRedeem(uint256 shares) public view virtual returns (uint256) { return _convertToAssets(shares, Math.Rounding.Floor); } /// @inheritdoc IERC4626 function deposit(uint256 assets, address receiver) public virtual returns (uint256) { uint256 maxAssets = maxDeposit(receiver); if (assets > maxAssets) { revert ERC4626ExceededMaxDeposit(receiver, assets, maxAssets); } uint256 shares = previewDeposit(assets); _deposit(_msgSender(), receiver, assets, shares); return shares; } /// @inheritdoc IERC4626 function mint(uint256 shares, address receiver) public virtual returns (uint256) { uint256 maxShares = maxMint(receiver); if (shares > maxShares) { revert ERC4626ExceededMaxMint(receiver, shares, maxShares); } uint256 assets = previewMint(shares); _deposit(_msgSender(), receiver, assets, shares); return assets; } /// @inheritdoc IERC4626 function withdraw(uint256 assets, address receiver, address owner) public virtual returns (uint256) { uint256 maxAssets = maxWithdraw(owner); if (assets > maxAssets) { revert ERC4626ExceededMaxWithdraw(owner, assets, maxAssets); } uint256 shares = previewWithdraw(assets); _withdraw(_msgSender(), receiver, owner, assets, shares); return shares; } /// @inheritdoc IERC4626 function redeem(uint256 shares, address receiver, address owner) public virtual returns (uint256) { uint256 maxShares = maxRedeem(owner); if (shares > maxShares) { revert ERC4626ExceededMaxRedeem(owner, shares, maxShares); } 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. */ function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual returns (uint256) { return assets.mulDiv(totalSupply() + 10 ** _decimalsOffset(), totalAssets() + 1, 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) { return shares.mulDiv(totalAssets() + 1, totalSupply() + 10 ** _decimalsOffset(), rounding); } /** * @dev Deposit/mint common workflow. */ function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual { // If asset() is ERC-777, `transferFrom` can trigger a reentrancy 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 _transferIn(caller, 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 ERC-777, `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); _transferOut(receiver, assets); emit Withdraw(caller, receiver, owner, assets, shares); } /// @dev Performs a transfer in of underlying assets. The default implementation uses `SafeERC20`. Used by {_deposit}. function _transferIn(address from, uint256 assets) internal virtual { SafeERC20.safeTransferFrom(IERC20(asset()), from, address(this), assets); } /// @dev Performs a transfer out of underlying assets. The default implementation uses `SafeERC20`. Used by {_withdraw}. function _transferOut(address to, uint256 assets) internal virtual { SafeERC20.safeTransfer(IERC20(asset()), to, assets); } function _decimalsOffset() internal view virtual returns (uint8) { return 0; } }