Skip to content

Instantly share code, notes, and snippets.

@ernestognw
Created May 29, 2025 23:36
Show Gist options
  • Save ernestognw/1a1c6b58227012afe13a26fd58faba3d to your computer and use it in GitHub Desktop.
Save ernestognw/1a1c6b58227012afe13a26fd58faba3d to your computer and use it in GitHub Desktop.
// 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;
}
}
// 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;
}
}
// 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();
// }
}
// 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);
}
// 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