Created
May 29, 2025 23:36
-
-
Save ernestognw/1a1c6b58227012afe13a26fd58faba3d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: MIT | |
// OpenZeppelin Contracts (last updated v5.3.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) { | |
return _previewAndDeposit(_msgSender(), assets, receiver); | |
} | |
function _previewAndDeposit(address caller, uint256 assets, address receiver) internal virtual returns (uint256) { | |
uint256 maxAssets = maxDeposit(receiver); | |
if (assets > maxAssets) { | |
revert ERC4626ExceededMaxDeposit(receiver, assets, maxAssets); | |
} | |
uint256 shares = previewDeposit(assets); | |
_deposit(caller, receiver, assets, shares); | |
return shares; | |
} | |
/// @inheritdoc IERC4626 | |
function mint(uint256 shares, address receiver) public virtual returns (uint256) { | |
return _previewAndMint(_msgSender(), shares, receiver); | |
} | |
function _previewAndMint(address caller, uint256 shares, address receiver) internal virtual returns (uint256) { | |
uint256 maxShares = maxMint(receiver); | |
if (shares > maxShares) { | |
revert ERC4626ExceededMaxMint(receiver, shares, maxShares); | |
} | |
uint256 assets = previewMint(shares); | |
_deposit(caller, 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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: MIT | |
pragma solidity ^0.8.20; | |
import {ERC165} from "../../../utils/introspection/ERC165.sol"; | |
import {ERC4626} from "./ERC4626.sol"; | |
import {IERC7540Operator} from "../../../interfaces/IERC7540.sol"; | |
import {IERC7575} from "../../../interfaces/IERC7575.sol"; | |
abstract contract ERC7540 is ERC165, ERC4626, IERC7575, IERC7540Operator { | |
mapping(address => mapping(address => bool)) private _operator; | |
function share() external view returns (address) { | |
return address(this); | |
} | |
function isOperator(address controller, address operator) public view virtual returns (bool) { | |
return _operator[controller][operator]; | |
} | |
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { | |
return | |
interfaceId == type(IERC7575).interfaceId || | |
interfaceId == type(IERC7540Operator).interfaceId || | |
super.supportsInterface(interfaceId); | |
} | |
function setOperator(address operator, bool approved) public virtual returns (bool) { | |
if (isOperator(msg.sender, operator) != approved) { | |
_operator[msg.sender][operator] = approved; | |
emit OperatorSet(msg.sender, operator, approved); | |
} // noop | |
return true; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: MIT | |
pragma solidity ^0.8.20; | |
import {IERC20} from "../IERC20.sol"; | |
import {IERC20Metadata} from "./IERC20Metadata.sol"; | |
import {ERC4626, IERC4626} from "./ERC4626.sol"; | |
import {ERC7540} from "./ERC7540.sol"; | |
import {IERC7540Deposit} from "../../../interfaces/IERC7540.sol"; | |
import {SafeERC20} from "../utils/SafeERC20.sol"; | |
import {Math} from "../../../utils/math/Math.sol"; | |
import {Time} from "../../../utils/types/Time.sol"; | |
abstract contract ERC7540Deposit is ERC7540, IERC7540Deposit { | |
// Invariant: _totalPendingDepositRequest == sum(_pendingDepositRequest[controller].assets) for all controllers | |
uint256 internal _totalPendingDepositRequest; | |
mapping(address account => uint256) internal _pendingDepositRequest; | |
mapping(address account => uint256) internal _averageDepositRequestTimepoint; | |
function totalAssets() public view virtual override(ERC4626, IERC4626) returns (uint256) { | |
return super.totalAssets() - _totalPendingDepositRequest; | |
} | |
function requestDeposit(uint256 assets, address controller, address owner) public virtual returns (uint256) { | |
address sender = _msgSender(); | |
require(owner == sender || isOperator(owner, sender)); | |
SafeERC20.safeTransferFrom(IERC20(asset()), owner, address(this), assets); | |
uint256 previousPendingDepositRequest = _pendingDepositRequest[controller]; | |
uint256 newPendingDepositRequest = previousPendingDepositRequest + assets; | |
_pendingDepositRequest[controller] = newPendingDepositRequest; | |
_totalPendingDepositRequest += assets; | |
_averageDepositRequestTimepoint[controller] = Math.mulDiv( | |
previousPendingDepositRequest, | |
_averageDepositRequestTimepoint[controller] + assets * Time.timestamp(), | |
newPendingDepositRequest | |
); | |
emit DepositRequest(controller, owner, 0, sender, assets); | |
return 0; | |
} | |
function pendingDepositRequest(uint256 /* requestId */, address controller) public view virtual returns (uint256) { | |
return _pendingDepositRequest[controller]; | |
} | |
function claimableDepositRequest(uint256 /* requestId */, address controller) public view virtual returns (uint256); | |
function deposit(uint256 assets, address receiver, address controller) public virtual returns (uint256) { | |
address sender = _msgSender(); | |
require(controller == sender || isOperator(controller, sender)); | |
return _previewAndDeposit(controller, assets, receiver); | |
} | |
function mint(uint256 shares, address receiver, address controller) public virtual returns (uint256) { | |
address sender = _msgSender(); | |
require(controller == sender || isOperator(controller, sender)); | |
return _previewAndMint(controller, shares, receiver); | |
} | |
function _previewAndDeposit( | |
address caller, | |
uint256 assets, | |
address receiver | |
) internal virtual override returns (uint256) { | |
return super._previewAndDeposit(caller, Math.min(assets, pendingDepositRequest(0, caller)), receiver); | |
} | |
// function _pendingToClaimingRate() internal view virtual returns (uint256) { | |
// // 1 token unit becomes claimable every second | |
// // TODO: Explain how to calculate and pick a better default (?) | |
// return 1 ** IERC20Metadata(asset()).decimals(); | |
// } | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: MIT | |
pragma solidity ^0.8.20; | |
interface IERC7540Operator { | |
event OperatorSet(address indexed controller, address indexed operator, bool approved); | |
function setOperator(address operator, bool approved) external returns (bool); | |
function isOperator(address controller, address operator) external view returns (bool status); | |
} | |
interface IERC7540Deposit { | |
event DepositRequest( | |
address indexed controller, | |
address indexed owner, | |
uint256 indexed requestId, | |
address sender, | |
uint256 assets | |
); | |
function requestDeposit(uint256 assets, address controller, address owner) external returns (uint256 requestId); | |
function pendingDepositRequest(uint256 requestId, address controller) external view returns (uint256 pendingAssets); | |
function claimableDepositRequest( | |
uint256 requestId, | |
address controller | |
) external view returns (uint256 claimableAssets); | |
function deposit(uint256 assets, address receiver, address controller) external returns (uint256 shares); | |
function mint(uint256 shares, address receiver, address controller) external returns (uint256 assets); | |
} | |
interface IERC7540Redeem { | |
event RedeemRequest( | |
address indexed controller, | |
address indexed owner, | |
uint256 indexed requestId, | |
address sender, | |
uint256 assets | |
); | |
function requestRedeem(uint256 shares, address controller, address owner) external returns (uint256 requestId); | |
function pendingRedeemRequest(uint256 requestId, address controller) external view returns (uint256 pendingShares); | |
function claimableRedeemRequest( | |
uint256 requestId, | |
address controller | |
) external view returns (uint256 claimableShares); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: MIT | |
pragma solidity ^0.8.20; | |
import {IERC165} from "./IERC165.sol"; | |
import {IERC4626} from "./IERC4626.sol"; | |
interface IERC7575 is IERC4626 { | |
function share() external view returns (address); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment