ERC4626

The information provided by Aloe Labs, Inc. (“we,” “us” or “our”) on docs.aloe.capital (the “Site”) is for general informational purposes only. All information on the Site is provided in good faith, however we make no representation or warranty of any kind, express or implied, regarding the accuracy, adequacy, validity, reliability, availability or completeness of any information on the Site.

Under no circumstance shall we have any liability to you for any loss or damage of any kind incurred as a result of the use of the site or reliance on any information provided on the site. Your use of the site and your reliance on any information on the site is solely at your own risk.

The Lender is ERC4626 compliant, with two exceptions: various view methods may revert upon reentrancy, and redeem treats type(uint256).max as a special case. Also, deposit and redeem are much more gas efficient than mint and withdraw.

See below for details.

asset

The address of the underlying token used for the Vault for accounting, depositing, and withdrawing.

  • MUST be an EIP-20 token contract.

  • MUST NOT revert

totalAssets

Total amount of the underlying asset that is “managed” by Vault.

  • SHOULD include any compounding that occurs from yield.

  • MUST be inclusive of any fees that are charged against assets in the Vault.

  • MUST NOT revert.

convertToShares

The amount of shares that the Vault would exchange for the amount of assets provided, in an ideal scenario where all the conditions are met.

  • MUST NOT be inclusive of any fees that are charged against assets in the Vault.

  • MUST NOT show any variations depending on the caller.

  • MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange.

  • MUST NOT revert unless due to integer overflow caused by an unreasonably large input.

  • MUST round down towards 0.

This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect the “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and from.


Notes

convertToShares(totalAssets()) != totalSupply() because totalAssets() includes compounding interest, while totalSupply() is static. However, if you use stats to get the updated total supply, the following will hold:

(, uint256 totalAssets, , uint256 totalSupply) = lender.stats();
assertApproxEqAbs(lender.convertToAssets(totalSupply), totalAssets, 1);
assertApproxEqAbs(lender.convertToShares(totalAssets), totalSupply, 1);
convertToAssets

The amount of assets that the Vault would exchange for the amount of shares provided, in an ideal scenario where all the conditions are met.

  • MUST NOT be inclusive of any fees that are charged against assets in the Vault.

  • MUST NOT show any variations depending on the caller.

  • MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange.

  • MUST NOT revert unless due to integer overflow caused by an unreasonably large input.

  • MUST round down towards 0.

This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect the “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and from.


Notes

convertToAssets(totalSupply()) != totalAssets() because totalAssets() includes compounding interest, while totalSupply() is static. However, if you use stats to get the updated total supply, the following will hold:

(, uint256 totalAssets, , uint256 totalSupply) = lender.stats();
assertApproxEqAbs(lender.convertToAssets(totalSupply), totalAssets, 1);
assertApproxEqAbs(lender.convertToShares(totalAssets), totalSupply, 1);
maxDeposit

Maximum amount of the underlying asset that can be deposited into the Vault for the receiver through a deposit call.

  • MUST return the maximum amount of assets deposit would allow to be deposited for receiver and not cause a revert, which MUST NOT be higher than the actual maximum that would be accepted (it should underestimate if necessary). This assume that the user has infinite assets, i.e. MUST NOT rely on balanceOf of asset.

  • MUST factor in both global and user-specific limits, like if deposits are entirely disabled (even temporarily) it MUST return 0.

  • MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of assets that may be deposited.

  • MUST NOT revert.


Notes

The precise maximum would be on the order of 1 << 112 with weird constraints coming from both lastBalance and totalSupply, which changes during interest accrual. Instead of doing complicated math, we return a conservative estimate of 1 << 96

It is unclear whether the spec would prefer for us to return 2 ** 256 - 1 — the only constraints involved arise from math precision/overflow.

maxMint

Maximum amount of shares that can be minted from the Vault for the receiver through a mint call.

  • MUST return the maximum amount of shares mint would allow to be deposited to receiver and not cause a revert, which MUST NOT be higher than the actual maximum that would be accepted (it should underestimate if necessary). This assumes that the user has infinite assets, i.e. MUST NOT rely on balanceOf of asset.

  • MUST factor in both global and user-specific limits, like if mints are entirely disabled (even temporarily) it MUST return 0.

  • MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of shares that may be minted.

  • MUST NOT revert.


Notes

The precise maximum would be on the order of 1 << 112 with weird constraints coming from both lastBalance and totalSupply, which changes during interest accrual. Instead of doing complicated math, we return a conservative estimate of 1 << 96

It is unclear whether the spec would prefer for us to return 2 ** 256 - 1 — the only constraints involved arise from math precision/overflow.

maxWithdraw

Maximum amount of the underlying asset that can be withdrawn from the owner balance in the Vault, through a withdraw call.

  • MUST return the maximum amount of assets that could be transferred from owner through withdraw and not cause a revert, which MUST NOT be higher than the actual maximum that would be accepted (it should underestimate if necessary).

  • MUST factor in both global and user-specific limits, like if withdrawals are entirely disabled (even temporarily) it MUST return 0.

  • MUST NOT revert.


Notes

Our implementation of maxWithdraw(owner) simply returns convertToAssets(maxRedeem(owner)).

maxRedeem

Maximum amount of Vault shares that can be redeemed from the owner balance in the Vault, through a redeem call.

  • MUST return the maximum amount of shares that could be transferred from owner through redeem and not cause a revert, which MUST NOT be higher than the actual maximum that would be accepted (it should underestimate if necessary).

  • MUST factor in both global and user-specific limits, like if redemption is entirely disabled (even temporarily) it MUST return 0.

  • MUST NOT revert.

previewDeposit

Allows an on-chain or off-chain user to simulate the effects of their deposit at the current block, given current on-chain conditions.

  • MUST return as close to and no more than the exact amount of Vault shares that would be minted in a deposit call in the same transaction. i.e. deposit should return the same or more shares as previewDeposit if called in the same transaction.

  • MUST NOT account for deposit limits like those returned from maxDeposit and should always act as though the deposit would be accepted, regardless if the user has enough tokens approved, etc.

  • MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit fees.

  • MUST NOT revert due to vault specific user/global limits. MAY revert due to other conditions that would also cause deposit to revert.

Note that any unfavorable discrepancy between convertToShares and previewDeposit SHOULD be considered slippage in share price or some other type of condition, meaning the depositor will lose assets by depositing.


Notes

Our implementation of previewDeposit(assets) simply returns convertToShares(assets), as we have no deposit fees or slippage.

previewMint

Allows an on-chain or off-chain user to simulate the effects of their mint at the current block, given current on-chain conditions.

  • MUST return as close to and no more than the exact amount of assets that would be deposited in a mint call in the same transaction. i.e. mint should return the same or fewer assets as previewMint if called in the same transaction.

  • MUST NOT account for mint limits like those returned from maxMint and should always act as though the mint would be accepted, regardless if the user has enough tokens approved, etc.

  • MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit fees.

  • MUST NOT revert due to vault specific user/global limits. MAY revert due to other conditions that would also cause mint to revert.

Note that any unfavorable discrepancy between convertToAssets and previewMint SHOULD be considered slippage in share price or some other type of condition, meaning the depositor will lose assets by minting.


Notes

Our implementation of previewMint(assets) simply returns convertToAssets(shares) (rounding up), as we have no deposit fees or slippage.

previewWithdraw

Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block, given current on-chain conditions.

  • MUST return as close to and no fewer than the exact amount of Vault shares that would be burned in a withdraw call in the same transaction. i.e. withdraw should return the same or fewer shares as previewWithdraw if called in the same transaction.

  • MUST NOT account for withdrawal limits like those returned from maxWithdraw and should always act as though the withdrawal would be accepted, regardless if the user has enough shares, etc.

  • MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of withdrawal fees.

  • MUST NOT revert due to vault specific user/global limits. MAY revert due to other conditions that would also cause withdraw to revert.

Note that any unfavorable discrepancy between convertToShares and previewWithdraw SHOULD be considered slippage in share price or some other type of condition, meaning the depositor will lose assets by depositing.


Notes

Our implementation of previewWithdraw(assets) simply returns convertToShares(assets) (rounding up).

We do have withdrawal fees, but they’re user-specific, and without having owner as an argument, it’s impossible to account for them here. Instead, as a given user’s owed fees grow, the value returned by maxWithdraw(owner) will shrink. In other words, though balanceOf(owner) stays the same, their withdraw-able balance shrinks over time.

Since we “MUST NOT account for withdrawal limits” in previewWithdraw, and since our previewWithdraw function is accurate and precise for all shares that are actually withdraw-able, we argue that our implementation meets the spec — even though we don’t directly account for withdrawal fees.

Finally, our implementation does return “the exact amount of Vault shares that would be burned” because the fee shares are transferred not burned.

previewRedeem

Allows an on-chain or off-chain user to simulate the effects of their redemption at the current block, given current on-chain conditions.

  • MUST return as close to and no more than the exact amount of assets that would be withdrawn in a redeem call in the same transaction. i.e. redeem should return the same or more assets as previewRedeem if called in the same transaction.

  • MUST NOT account for redemption limits like those returned from maxRedeem and should always act as though the redemption would be accepted, regardless if the user has enough shares, etc.

  • MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of withdrawal fees.

  • MUST NOT revert due to vault specific user/global limits. MAY revert due to other conditions that would also cause redeem to revert.

Note that any unfavorable discrepancy between convertToAssets and previewRedeem SHOULD be considered slippage in share price or some other type of condition, meaning the depositor will lose assets by redeeming.


Notes

Our implementation of previewRedeem(shares) simply returns convertToAssets(shares).

We do have withdrawal fees, but they’re user-specific, and without having owner as an argument, it’s impossible to account for them here. Instead, as a given user’s owed fees grow, the value returned by maxRedeem(owner) will shrink. In other words, though balanceOf(owner) stays the same, their redeemable balance shrinks over time.

Since we “MUST NOT account for redemption limits” in previewRedeem, and since our previewRedeem function is accurate and precise for all shares that are actually redeemable, we argue that our implementation meets the spec — even though we don’t directly account for withdrawal fees.

deposit

Mints shares Vault shares to receiver by depositing exactly assets of underlying tokens.

  • MUST emit the Deposit event

  • MUST support EIP-20 approve/transferFrom on asset as a deposit flow. MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the deposit execution, and are accounted for during deposit.

  • MUST revert if all of assets cannot be deposited (due to deposit limit being reached, slippage, the user not approving enough underlying tokens to the Vault contract, etc).


Notes

deposit is much more efficient than mint in our application, and is the recommended way of depositing. Our application does support the additional flow in which underlying tokens are owned by the Vault before the deposit function, enabling straightforward Permit/Permit2 integration in the periphery.

mint

Mints exactly shares Vault shares to receiver by depositing assets of underlying tokens.

  • MUST emit the Deposit event.

  • MUST support EIP-20 approve/transferFrom on asset as a mint flow. MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the mint execution, and are accounted for during mint.

  • MUST revert if all of shares cannot be minted (due to deposit limit being reached, slippage, the user not approving enough underlying tokens to the Vault contract, etc).


Notes

Our implementation of mint simply converts shares to assets and calls deposit. It is less efficient and not recommended.

withdraw

Burns shares from owner and sends exactly assets of underlying tokens to receiver.

  • MUST emit the Withdraw event.

  • MUST support a withdraw flow where the shares are burned from owner directly where owner is msg.sender.

  • MUST support a withdraw flow where the shares are burned from owner directly where msg.sender has EIP-20 approval over the shares of owner.

  • SHOULD check msg.sender can spend owner funds, assets needs to be converted to shares and shares should be checked for allowance.

  • MUST revert if all of assets cannot be withdrawn (due to withdrawal limit being reached, slippage, the owner not having enough shares, etc).


Notes

Our implementation of withdraw simply converts amount to shares and calls redeem. It is less efficient and not recommended. It does not support the additional flow in which shares are transferred before the call to withdraw, and you do not have to pre-request before withdrawing.

redeem

Burns exactly shares from owner and sends assets of underlying tokens to receiver.

  • MUST emit the Withdraw event.

  • MUST support a redeem flow where the shares are burned from owner directly where owner is msg.sender.

  • MUST support a redeem flow where the shares are burned from owner directly where msg.sender has EIP-20 approval over the shares of owner.

  • SHOULD check msg.sender can spend owner funds using allowance.

  • MUST revert if all of shares cannot be redeemed (due to withdrawal limit being reached, slippage, the owner not having enough shares, etc).


Notes

If shares is set to type(uint256).max, the contract will attempt to redeem maxRedeem(owner) shares. This uses extra gas, and is only necessary if maxRedeem(owner) is changing over time, e.g. if owner has a courier or if high utilization is constraining withdraw-able amounts. In all other cases, prefer getting maxRedeem(owner) off-chain.

Last updated