Skip to content
Lattice

ERC4626Lib

Git Source

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/_transferOut hooks (added in v5.6.0). _deposit and _withdraw move 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 unapproved deposit reports SafeERC20FailedOperation, not the token's ERC20InsufficientAllowance. 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 reverts SafeERC20FailedOperation. These helpers, and the decimals() 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.
  • maxRedeem is capped at the floor shares idle assets buy back; OpenZeppelin returns balanceOf(owner). The cap can bind after a loss, or while VaultCore has funds allocated.
  • maxWithdraw is min(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 the maxRedeem cap binds, deriving maxWithdraw from it would report up to one share's value less; Lattice keeps the larger value, which withdraw accepts (#234).
  • decimalsOffset is 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 SafeERC20FailedOperation are declared on IERC4626; their selectors are the same.

Functions

erc4626Storage

function erc4626Storage() internal pure returns (ERC4626Storage storage $);

Returns

NameTypeDescription
$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

NameTypeDescription
asset_addressThe underlying ERC-20 token address.
decimalsOffset_uint8Virtual 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

NameTypeDescription
<none>address

decimals

Returns the vault's decimals: underlying decimals + offset.

function decimals() internal view returns (uint8);

Returns

NameTypeDescription
<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

NameTypeDescription
<none>uint256

convertToShares

Returns shares equivalent to assets at the diamond's NAV (floor rounding).

function convertToShares(uint256 assets) internal view returns (uint256);

Parameters

NameTypeDescription
assetsuint256

Returns

NameTypeDescription
<none>uint256

convertToAssets

Returns assets equivalent to shares at the diamond's NAV (floor rounding).

function convertToAssets(uint256 shares) internal view returns (uint256);

Parameters

NameTypeDescription
sharesuint256

Returns

NameTypeDescription
<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

NameTypeDescription
_address

Returns

NameTypeDescription
<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

NameTypeDescription
_address

Returns

NameTypeDescription
<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

NameTypeDescription
owneraddress

Returns

NameTypeDescription
<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

NameTypeDescription
owneraddress

Returns

NameTypeDescription
<none>uint256

previewDeposit

Simulates shares minted for a deposit of assets (floor rounding).

function previewDeposit(uint256 assets) internal view returns (uint256);

Parameters

NameTypeDescription
assetsuint256

Returns

NameTypeDescription
<none>uint256

previewMint

Simulates assets required to mint exactly shares (ceiling rounding).

function previewMint(uint256 shares) internal view returns (uint256);

Parameters

NameTypeDescription
sharesuint256

Returns

NameTypeDescription
<none>uint256

previewWithdraw

Simulates shares burned for a withdraw of assets (ceiling rounding).

function previewWithdraw(uint256 assets) internal view returns (uint256);

Parameters

NameTypeDescription
assetsuint256

Returns

NameTypeDescription
<none>uint256

previewRedeem

Simulates assets returned for redeeming shares (floor rounding).

function previewRedeem(uint256 shares) internal view returns (uint256);

Parameters

NameTypeDescription
sharesuint256

Returns

NameTypeDescription
<none>uint256

deposit

Deposits assets and mints shares to receiver.

function deposit(uint256 assets, address receiver) internal returns (uint256 shares);

Parameters

NameTypeDescription
assetsuint256
receiveraddress

Returns

NameTypeDescription
sharesuint256

mint

Mints exactly shares to receiver, pulling the required assets.

function mint(uint256 shares, address receiver) internal returns (uint256 assets);

Parameters

NameTypeDescription
sharesuint256
receiveraddress

Returns

NameTypeDescription
assetsuint256

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

NameTypeDescription
assetsuint256
receiveraddress
owneraddress

Returns

NameTypeDescription
sharesuint256

redeem

Redeems shares from owner, transferring assets to receiver.

function redeem(uint256 shares, address receiver, address owner) internal returns (uint256 assets);

Parameters

NameTypeDescription
sharesuint256
receiveraddress
owneraddress

Returns

NameTypeDescription
assetsuint256

_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

NameTypeDescription
assetsuint256
roundingMath.Rounding

Returns

NameTypeDescription
<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

NameTypeDescription
sharesuint256
roundingMath.Rounding

Returns

NameTypeDescription
<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

NameTypeDescription
assetsuint256
totalSupply_uint256
totalAssets_uint256
decimalsOffset_uint8
roundingMath.Rounding

Returns

NameTypeDescription
<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

NameTypeDescription
sharesuint256
totalSupply_uint256
totalAssets_uint256
decimalsOffset_uint8
roundingMath.Rounding

Returns

NameTypeDescription
<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

NameTypeDescription
owneraddress
totalSupply_uint256
navuint256

Returns

NameTypeDescription
<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

NameTypeDescription
owneraddress
totalSupply_uint256
navuint256

Returns

NameTypeDescription
<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

NameTypeDescription
xuint256
yuint256
duint256
roundingMath.Rounding

Returns

NameTypeDescription
resultuint256

_decimalsOffset

function _decimalsOffset() private view returns (uint8);

Returns

NameTypeDescription
<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

NameTypeDescription
<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

NameTypeDescription
okbool
navuint256

_navStaticcall

Self-staticcall to totalAssets(), which dispatches to whichever facet owns that selector.

function _navStaticcall() private view returns (bool ok, bytes memory data);

Returns

NameTypeDescription
okbool
databytes

_deposit

Transfers assets in, mints shares, emits Deposit.

function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal;

Parameters

NameTypeDescription
calleraddress
receiveraddress
assetsuint256
sharesuint256

_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

NameTypeDescription
calleraddress
receiveraddress
owneraddress
assetsuint256
sharesuint256

_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

NameTypeDescription
tokenaddress
toaddress
amountuint256

_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

NameTypeDescription
tokenaddress
fromaddress
toaddress
amountuint256