123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282 |
- // SPDX-License-Identifier: MIT
- // OpenZeppelin Contracts (last updated v5.4.0) (token/ERC20/extensions/ERC4626.sol)
- pragma solidity ^0.8.20;
- import {IERC20, IERC20Metadata, ERC20} from "../ERC20.sol";
- import {SafeERC20} from "../utils/SafeERC20.sol";
- import {IERC4626} from "../../../interfaces/IERC4626.sol";
- import {Math} from "../../../utils/math/Math.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].
- * ====
- */
- abstract contract ERC4626 is ERC20, IERC4626 {
- using Math for uint256;
- IERC20 private immutable _asset;
- uint8 private immutable _underlyingDecimals;
- /**
- * @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 `receiver`.
- */
- error ERC4626ExceededMaxWithdraw(address owner, uint256 assets, uint256 max);
- /**
- * @dev Attempted to redeem more shares than the max amount for `receiver`.
- */
- 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).
- */
- constructor(IERC20 asset_) {
- (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) {
- (bool success, bytes memory encodedDecimals) = address(asset_).staticcall(
- abi.encodeCall(IERC20Metadata.decimals, ())
- );
- 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 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, ERC20) returns (uint8) {
- return _underlyingDecimals + _decimalsOffset();
- }
- /// @inheritdoc IERC4626
- function asset() public view virtual returns (address) {
- 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 _convertToAssets(balanceOf(owner), Math.Rounding.Floor);
- }
- /// @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
- SafeERC20.safeTransferFrom(IERC20(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 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);
- SafeERC20.safeTransfer(IERC20(asset()), receiver, assets);
- emit Withdraw(caller, receiver, owner, assets, shares);
- }
- function _decimalsOffset() internal view virtual returns (uint8) {
- return 0;
- }
- }
|