ERC4626Lib
Title: ERC4626Lib
Authors: David Dada <daveproxy80@gmail.com> (https://github.com/dadadave80), Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/extensions/ERC4626.sol)
Library implementing the ERC-4626 Tokenized Vault Standard.
Mirrors OpenZeppelin v5.6.1 ERC4626 logic. All state lives in an ERC-7201 slot. The vault IS an ERC-20 share token — callers must also initialize ERC20Lib.
NAV source: OZ prices shares through the virtual totalAssets(), but a library call cannot dispatch
virtually. Every conversion, preview, max* and mutator therefore reads the NAV from the diamond's own
totalAssets() selector through a self-staticcall. On a plain ERC-4626 diamond that selector is the
ERC4626 facet (this library's idle-only totalAssets); on a VaultCore diamond it is VaultCore,
which adds strategy-deployed funds. totalAssets here never makes that self-call, so it cannot recurse.
Liquidity: exits pay out of the vault's own balance only, so maxWithdraw/maxRedeem are capped at
idle assets. If the NAV read fails (e.g. a strategy's balance read reverts), the vault fails closed:
every max* returns 0, deposit/mint/withdraw/redeem revert with the matching ERC4626ExceededMax*
error, and the converters and previews revert with the NAV read's error. On a VaultCore diamond
totalAssets() itself reverts while the NAV is unreadable: the self-staticcall has no other way to
signal an unknown NAV. Reverting totalAssets(), converters and previews deviate from ERC-4626's
"MUST NOT revert" wording, which is preferred over pricing shares on a partial NAV.
Share mints and burns call ERC20Lib directly: an ERC-20 movement-replacing extension (ERC20Pausable,
ERC20Votes) does not see them, and neither does ERC20Capped's cap, so they are mutually exclusive (D25,
#234). VaultCore's deposit/mint/withdraw/redeem share this path. GovernedVault is the combined facet
that wraps these mutators and moves voting units.
Differences from OpenZeppelin v5.6.1:
- The caller is
msg.sender, not_msgSender(); Lattice has no ERC-2771 context. - Nothing here is
virtual, so there are no_transferIn/_transferOuthooks (added in v5.6.0)._depositand_withdrawmove the asset through private SafeERC20-style helpers (see below); a vault that needs another transfer path replaces the mutator selectors (D25). - A failed asset transfer reverts
SafeERC20FailedOperation(asset)instead of bubbling the token's own revert data, so an unapproveddepositreportsSafeERC20FailedOperation, not the token'sERC20InsufficientAllowance. Returndata that does not decode as a bool (non-empty but shorter than 32 bytes, or a word other than 0 or 1) reverts with empty data; OpenZeppelin revertsSafeERC20FailedOperation. These helpers, and thedecimals()probe in __ERC4626_init, copy the whole returndata; OpenZeppelin bounds the copy (0x20 bytes for transfers, 0x40 for decimals). - Share math reads the NAV through the self-staticcall above and fails closed while it is unreadable.
maxRedeemis capped at the floor shares idle assets buy back; OpenZeppelin returnsbalanceOf(owner). The cap can bind after a loss, or while VaultCore has funds allocated.maxWithdrawismin(previewRedeem(balanceOf(owner)), idle), not OpenZeppelin's (since v5.5.0)previewRedeem(maxRedeem(owner)). On a plain ERC-4626 diamond it always equals OpenZeppelin's value: a holder's shares are never worth more than idle. Where themaxRedeemcap binds, derivingmaxWithdrawfrom it would report up to one share's value less; Lattice keeps the larger value, whichwithdrawaccepts (#234).decimalsOffsetis set once in __ERC4626_init, not by an overridable_decimalsOffset().- __ERC4626_init registers IERC4626 for ERC-165; OpenZeppelin's ERC4626 has no ERC-165.
- The ERC-4626 errors and
SafeERC20FailedOperationare declared on IERC4626; their selectors are the same.
Functions
erc4626Storage
function erc4626Storage() internal pure returns (ERC4626Storage storage $);Returns
| Name | Type | Description |
|---|---|---|
| $ | ERC4626Storage |
__ERC4626_init
Initializes the ERC-4626 module with an underlying asset and virtual share offset.
Must be called inside a pre/postInitializer block, after ERC20Lib.__ERC20_init.
IMPORTANT: decimalsOffset_ is stored once at init time and is immutable thereafter.
Unlike OZ's _decimalsOffset() which is an overridable internal view virtual function,
Lattice's Diamond-pattern constraint requires state to live in the ERC-7201 slot — there is
no constructor and facets cannot hold mutable storage. Consumers who need a non-zero offset
must supply it via this initializer. There is no upgrade path once set.
function __ERC4626_init(address asset_, uint8 decimalsOffset_) internal;Parameters
| Name | Type | Description |
|---|---|---|
| asset_ | address | The underlying ERC-20 token address. |
| decimalsOffset_ | uint8 | Virtual share decimals offset for inflation-attack mitigation (usually 0). |
registerInterface
Registers support for the IERC4626 interface via ERC-165.
function registerInterface() internal;asset
Returns the underlying asset address.
function asset() internal view returns (address);Returns
| Name | Type | Description |
|---|---|---|
| <none> | address |
decimals
Returns the vault's decimals: underlying decimals + offset.
function decimals() internal view returns (uint8);Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint8 |
totalAssets
Returns the vault's idle assets: its own balance of the underlying.
This is the ERC4626 facet's totalAssets() and the liquidity cap on exits. It is NOT the
pricing NAV on a diamond that replaces the totalAssets() selector (e.g. VaultCore); share math
reads that selector instead (see the library NatSpec). It must never call the converters.
function totalAssets() internal view returns (uint256);Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint256 |
convertToShares
Returns shares equivalent to assets at the diamond's NAV (floor rounding).
function convertToShares(uint256 assets) internal view returns (uint256);Parameters
| Name | Type | Description |
|---|---|---|
| assets | uint256 |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint256 |
convertToAssets
Returns assets equivalent to shares at the diamond's NAV (floor rounding).
function convertToAssets(uint256 shares) internal view returns (uint256);Parameters
| Name | Type | Description |
|---|---|---|
| shares | uint256 |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint256 |
maxDeposit
Returns the maximum depositable assets for receiver: unbounded, or 0 while the NAV is unreadable.
function maxDeposit(address) internal view returns (uint256);Parameters
| Name | Type | Description |
|---|---|---|
| _ | address |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint256 |
maxMint
Returns the maximum mintable shares for receiver: unbounded, or 0 while the NAV is unreadable.
function maxMint(address) internal view returns (uint256);Parameters
| Name | Type | Description |
|---|---|---|
| _ | address |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint256 |
maxWithdraw
Returns the maximum withdrawable assets for owner: the NAV value of their shares, capped at
idle assets (0 while the NAV is unreadable).
function maxWithdraw(address owner) internal view returns (uint256);Parameters
| Name | Type | Description |
|---|---|---|
| owner | address |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint256 |
maxRedeem
Returns the maximum redeemable shares for owner: their balance, capped at the shares idle
assets can pay out (0 while the NAV is unreadable).
function maxRedeem(address owner) internal view returns (uint256);Parameters
| Name | Type | Description |
|---|---|---|
| owner | address |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint256 |
previewDeposit
Simulates shares minted for a deposit of assets (floor rounding).
function previewDeposit(uint256 assets) internal view returns (uint256);Parameters
| Name | Type | Description |
|---|---|---|
| assets | uint256 |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint256 |
previewMint
Simulates assets required to mint exactly shares (ceiling rounding).
function previewMint(uint256 shares) internal view returns (uint256);Parameters
| Name | Type | Description |
|---|---|---|
| shares | uint256 |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint256 |
previewWithdraw
Simulates shares burned for a withdraw of assets (ceiling rounding).
function previewWithdraw(uint256 assets) internal view returns (uint256);Parameters
| Name | Type | Description |
|---|---|---|
| assets | uint256 |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint256 |
previewRedeem
Simulates assets returned for redeeming shares (floor rounding).
function previewRedeem(uint256 shares) internal view returns (uint256);Parameters
| Name | Type | Description |
|---|---|---|
| shares | uint256 |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint256 |
deposit
Deposits assets and mints shares to receiver.
function deposit(uint256 assets, address receiver) internal returns (uint256 shares);Parameters
| Name | Type | Description |
|---|---|---|
| assets | uint256 | |
| receiver | address |
Returns
| Name | Type | Description |
|---|---|---|
| shares | uint256 |
mint
Mints exactly shares to receiver, pulling the required assets.
function mint(uint256 shares, address receiver) internal returns (uint256 assets);Parameters
| Name | Type | Description |
|---|---|---|
| shares | uint256 | |
| receiver | address |
Returns
| Name | Type | Description |
|---|---|---|
| assets | uint256 |
withdraw
Withdraws assets from the vault, burning the required shares from owner.
function withdraw(uint256 assets, address receiver, address owner) internal returns (uint256 shares);Parameters
| Name | Type | Description |
|---|---|---|
| assets | uint256 | |
| receiver | address | |
| owner | address |
Returns
| Name | Type | Description |
|---|---|---|
| shares | uint256 |
redeem
Redeems shares from owner, transferring assets to receiver.
function redeem(uint256 shares, address receiver, address owner) internal returns (uint256 assets);Parameters
| Name | Type | Description |
|---|---|---|
| shares | uint256 | |
| receiver | address | |
| owner | address |
Returns
| Name | Type | Description |
|---|---|---|
| assets | uint256 |
_convertToShares
Converts assets to shares at the diamond's NAV using the given rounding direction.
function _convertToShares(uint256 assets, Math.Rounding rounding) internal view returns (uint256);Parameters
| Name | Type | Description |
|---|---|---|
| assets | uint256 | |
| rounding | Math.Rounding |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint256 |
_convertToAssets
Converts shares to assets at the diamond's NAV using the given rounding direction.
function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view returns (uint256);Parameters
| Name | Type | Description |
|---|---|---|
| shares | uint256 | |
| rounding | Math.Rounding |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint256 |
_convertToSharesFromTotals
Converts assets to shares from explicit totals.
Formula: assets * (totalSupply + 10**offset) / (totalAssets + 1)
function _convertToSharesFromTotals(
uint256 assets,
uint256 totalSupply_,
uint256 totalAssets_,
uint8 decimalsOffset_,
Math.Rounding rounding
) internal pure returns (uint256);Parameters
| Name | Type | Description |
|---|---|---|
| assets | uint256 | |
| totalSupply_ | uint256 | |
| totalAssets_ | uint256 | |
| decimalsOffset_ | uint8 | |
| rounding | Math.Rounding |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint256 |
_convertToAssetsFromTotals
Converts shares to assets from explicit totals.
Formula: shares * (totalAssets + 1) / (totalSupply + 10**offset)
function _convertToAssetsFromTotals(
uint256 shares,
uint256 totalSupply_,
uint256 totalAssets_,
uint8 decimalsOffset_,
Math.Rounding rounding
) internal pure returns (uint256);Parameters
| Name | Type | Description |
|---|---|---|
| shares | uint256 | |
| totalSupply_ | uint256 | |
| totalAssets_ | uint256 | |
| decimalsOffset_ | uint8 | |
| rounding | Math.Rounding |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint256 |
_maxWithdraw
maxWithdraw at a known NAV: the floor value of owner's shares, capped at idle assets.
function _maxWithdraw(address owner, uint256 totalSupply_, uint256 nav) private view returns (uint256);Parameters
| Name | Type | Description |
|---|---|---|
| owner | address | |
| totalSupply_ | uint256 | |
| nav | uint256 |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint256 |
_maxRedeem
maxRedeem at a known NAV: owner's balance, capped at the floor shares idle assets buy back.
Floor then floor keeps previewRedeem(maxRedeem) within idle.
function _maxRedeem(address owner, uint256 totalSupply_, uint256 nav) private view returns (uint256);Parameters
| Name | Type | Description |
|---|---|---|
| owner | address | |
| totalSupply_ | uint256 | |
| nav | uint256 |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint256 |
_mulDiv
Math.mulDiv in the given rounding direction. The round-up step is applied here rather than through
Math's rounding overload, which costs markedly more gas on these hot paths. As in
Math.unsignedRoundsUp, the odd modes (Ceil, Expand) round up; the checked increment panics if the
floor is already type(uint256).max.
function _mulDiv(uint256 x, uint256 y, uint256 d, Math.Rounding rounding) private pure returns (uint256 result);Parameters
| Name | Type | Description |
|---|---|---|
| x | uint256 | |
| y | uint256 | |
| d | uint256 | |
| rounding | Math.Rounding |
Returns
| Name | Type | Description |
|---|---|---|
| result | uint256 |
_decimalsOffset
function _decimalsOffset() private view returns (uint8);Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint8 |
_nav
Reads the NAV from the diamond's own totalAssets() selector, bubbling its revert on failure.
function _nav() private view returns (uint256);Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint256 |
_tryNav
Reads the NAV from the diamond's own totalAssets() selector; ok is false if the read failed.
function _tryNav() private view returns (bool ok, uint256 nav);Returns
| Name | Type | Description |
|---|---|---|
| ok | bool | |
| nav | uint256 |
_navStaticcall
Self-staticcall to totalAssets(), which dispatches to whichever facet owns that selector.
function _navStaticcall() private view returns (bool ok, bytes memory data);Returns
| Name | Type | Description |
|---|---|---|
| ok | bool | |
| data | bytes |
_deposit
Transfers assets in, mints shares, emits Deposit.
function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal;Parameters
| Name | Type | Description |
|---|---|---|
| caller | address | |
| receiver | address | |
| assets | uint256 | |
| shares | uint256 |
_withdraw
Spends allowance if needed, burns shares, transfers assets out, emits Withdraw.
function _withdraw(address caller, address receiver, address owner, uint256 assets, uint256 shares) internal;Parameters
| Name | Type | Description |
|---|---|---|
| caller | address | |
| receiver | address | |
| owner | address | |
| assets | uint256 | |
| shares | uint256 |
_safeTransfer
Calls token.transfer(to, amount) and reverts with SafeERC20FailedOperation if it fails or
returns false. Handles tokens that do not return a bool (e.g. USDT).
function _safeTransfer(address token, address to, uint256 amount) private;Parameters
| Name | Type | Description |
|---|---|---|
| token | address | |
| to | address | |
| amount | uint256 |
_safeTransferFrom
Calls token.transferFrom(from, to, amount) and reverts with SafeERC20FailedOperation if it
fails or returns false. Handles tokens that do not return a bool (e.g. USDT).
function _safeTransferFrom(address token, address from, address to, uint256 amount) private;Parameters
| Name | Type | Description |
|---|---|---|
| token | address | |
| from | address | |
| to | address | |
| amount | uint256 |