Skip to content

Commit

Permalink
ERC4626VotesUpgradeable
Browse files Browse the repository at this point in the history
  • Loading branch information
haythemsellami committed Jul 17, 2024
1 parent f7cc381 commit e047716
Show file tree
Hide file tree
Showing 2 changed files with 324 additions and 11 deletions.
310 changes: 310 additions & 0 deletions src/core/ERC4626VotesUpgradeable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/ERC4626.sol)

pragma solidity ^0.8.20;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {ERC20VotesUpgradeable, ERC20Upgradeable} from "@openzeppelin-upgradeable/token/ERC20/extensions/ERC20VotesUpgradeable.sol";
import {SafeERC20} 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";

/**
* @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]
* ====
* 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 uses virtual assets and shares to 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, 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: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 ERC4626VotesUpgradeable is ERC20VotesUpgradeable, 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 `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 (ERC20 or ERC777).
*/
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, uint8) {
(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, ERC20Upgradeable) returns (uint8) {
ERC4626Storage storage $ = _getERC4626Storage();
return $._underlyingDecimals + _decimalsOffset();
}

/** @dev See {IERC4626-asset}. */
function asset() public view virtual returns (address) {
ERC4626Storage storage $ = _getERC4626Storage();
return address($._asset);
}

/** @dev See {IERC4626-totalAssets}. */
function totalAssets() public view virtual returns (uint256) {
ERC4626Storage storage $ = _getERC4626Storage();
return $._asset.balanceOf(address(this));
}

/** @dev See {IERC4626-convertToShares}. */
function convertToShares(uint256 assets) public view virtual returns (uint256) {
return _convertToShares(assets, Math.Rounding.Floor);
}

/** @dev See {IERC4626-convertToAssets}. */
function convertToAssets(uint256 shares) public view virtual returns (uint256) {
return _convertToAssets(shares, Math.Rounding.Floor);
}

/** @dev See {IERC4626-maxDeposit}. */
function maxDeposit(address) public view virtual returns (uint256) {
return type(uint256).max;
}

/** @dev See {IERC4626-maxMint}. */
function maxMint(address) public view virtual returns (uint256) {
return type(uint256).max;
}

/** @dev See {IERC4626-maxWithdraw}. */
function maxWithdraw(address owner) public view virtual returns (uint256) {
return _convertToAssets(balanceOf(owner), Math.Rounding.Floor);
}

/** @dev See {IERC4626-maxRedeem}. */
function maxRedeem(address owner) public view virtual returns (uint256) {
return balanceOf(owner);
}

/** @dev See {IERC4626-previewDeposit}. */
function previewDeposit(uint256 assets) public view virtual returns (uint256) {
return _convertToShares(assets, Math.Rounding.Floor);
}

/** @dev See {IERC4626-previewMint}. */
function previewMint(uint256 shares) public view virtual returns (uint256) {
return _convertToAssets(shares, Math.Rounding.Ceil);
}

/** @dev See {IERC4626-previewWithdraw}. */
function previewWithdraw(uint256 assets) public view virtual returns (uint256) {
return _convertToShares(assets, Math.Rounding.Ceil);
}

/** @dev See {IERC4626-previewRedeem}. */
function previewRedeem(uint256 shares) public view virtual returns (uint256) {
return _convertToAssets(shares, Math.Rounding.Floor);
}

/** @dev See {IERC4626-deposit}. */
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;
}

/** @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 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;
}

/** @dev See {IERC4626-withdraw}. */
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;
}

/** @dev See {IERC4626-redeem}. */
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 {
ERC4626Storage storage $ = _getERC4626Storage();
// If _asset is ERC777, `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($._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 {
ERC4626Storage storage $ = _getERC4626Storage();
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);
}

function _decimalsOffset() internal view virtual returns (uint8) {
return 0;
}
}
25 changes: 14 additions & 11 deletions src/core/EulerAggregationVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ import {IEulerAggregationVault} from "./interface/IEulerAggregationVault.sol";
import {IWithdrawalQueue} from "./interface/IWithdrawalQueue.sol";
// contracts
import {Dispatch} from "./Dispatch.sol";
// import {
// ERC20Upgradeable,
// ERC4626Upgradeable
// } from "@openzeppelin-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol";
// import {ERC20VotesUpgradeable} from "@openzeppelin-upgradeable/token/ERC20/extensions/ERC20VotesUpgradeable.sol";
import {
ERC20Upgradeable,
ERC4626Upgradeable
} from "@openzeppelin-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol";
import {ERC20VotesUpgradeable} from "@openzeppelin-upgradeable/token/ERC20/extensions/ERC20VotesUpgradeable.sol";
ERC4626VotesUpgradeable
} from "./ERC4626VotesUpgradeable.sol";
import {AccessControlEnumerableUpgradeable} from
"@openzeppelin-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol";
import {Shared} from "./common/Shared.sol";
Expand All @@ -33,8 +37,7 @@ import {EventsLib as Events} from "./lib/EventsLib.sol";
/// @dev Do NOT use with rebasing tokens
/// @dev inspired by Yearn v3 ❤️
contract EulerAggregationVault is
ERC4626Upgradeable,
ERC20VotesUpgradeable,
ERC4626VotesUpgradeable,
AccessControlEnumerableUpgradeable,
Dispatch,
IEulerAggregationVault
Expand Down Expand Up @@ -429,10 +432,10 @@ contract EulerAggregationVault is
return super.redeem(_shares, _receiver, _owner);
}

/// @dev See {IERC20Metadata-decimals}.
function decimals() public view virtual override (ERC4626Upgradeable, ERC20Upgradeable) returns (uint8) {
return ERC4626Upgradeable.decimals();
}
// /// @dev See {IERC20Metadata-decimals}.
// function decimals() public view virtual override (ERC4626Upgradeable, ERC20Upgradeable) returns (uint8) {
// return ERC4626Upgradeable.decimals();
// }

/// @notice Return the total amount of assets deposited, plus the accrued interest.
/// @return uint256 total amount
Expand Down Expand Up @@ -605,9 +608,9 @@ contract EulerAggregationVault is
/// @param to Address receiving the amount
function _update(address from, address to, uint256 value)
internal
override (ERC20VotesUpgradeable, ERC20Upgradeable)
override
{
ERC20VotesUpgradeable._update(from, to, value);
super._update(from, to, value);

if (from == to) return;

Expand Down

0 comments on commit e047716

Please sign in to comment.