Skip to content
5 changes: 5 additions & 0 deletions .changeset/shiny-dolphins-lick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`ERC4626`: compute `maxWithdraw` using `maxRedeem` and `previewRedeem` so that changes to the preview functions affect the max functions.
2 changes: 1 addition & 1 deletion contracts/interfaces/IERC4626.sol
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ interface IERC4626 is IERC20, IERC20Metadata {
function maxRedeem(address owner) external view returns (uint256 maxShares);

/**
* @dev Allows an on-chain or off-chain user to simulate the effects of their redeemption at the current block,
* @dev 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
Expand Down
54 changes: 37 additions & 17 deletions contracts/token/ERC20/extensions/ERC4626.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,26 @@ import {Math} from "../../../utils/math/Math.sol";
*
* To learn more, check out our xref:ROOT:erc4626.adoc[ERC-4626 guide].
* ====
*
* [NOTE]
* ====
* When overriding this contract, some elements must to be considered:
*
* * When overriding the behavior of the deposit or withdraw mechanisms, it is recommended to override the internal
* functions. Overriding {_deposit} automatically affects both {deposit} and {mint}. Similarly, overriding {_withdraw}
* automatically affects both {withdraw} and {redeem}. Overall it is not recommended to override the public facing
* functions since that could lead to inconsistent behaviors between the {deposit} and {mint} or between {withdraw} and
* {redeem}, which is documented to have lead to loss of funds.
*
* * Overrides to the deposit or withdraw mechanism must be reflected in the preview functions as well.
*
* * {maxWithdraw} depends on {maxRedeem}. Therefore, overriding {maxRedeem} only is enough. On the other hand,
* overriding {maxWithdraw} only would have no effect on {maxRedeem}, and could create an inconsistency between the two
* functions.
*
* * If {previewRedeem} is overridden to revert, {maxWithdraw} must be overridden as necessary to ensure it
* always return successfully.
* ====
*/
abstract contract ERC4626 is ERC20, IERC4626 {
using Math for uint256;
Expand Down Expand Up @@ -107,67 +127,67 @@ abstract contract ERC4626 is ERC20, IERC4626 {
return _underlyingDecimals + _decimalsOffset();
}

/** @dev See {IERC4626-asset}. */
/// @inheritdoc IERC4626
function asset() public view virtual returns (address) {
return address(_asset);
}

/** @dev See {IERC4626-totalAssets}. */
/// @inheritdoc IERC4626
function totalAssets() public view virtual returns (uint256) {
return _asset.balanceOf(address(this));
}

/** @dev See {IERC4626-convertToShares}. */
/// @inheritdoc IERC4626
function convertToShares(uint256 assets) public view virtual returns (uint256) {
return _convertToShares(assets, Math.Rounding.Floor);
}

/** @dev See {IERC4626-convertToAssets}. */
/// @inheritdoc IERC4626
function convertToAssets(uint256 shares) public view virtual returns (uint256) {
return _convertToAssets(shares, Math.Rounding.Floor);
}

/** @dev See {IERC4626-maxDeposit}. */
/// @inheritdoc IERC4626
function maxDeposit(address) public view virtual returns (uint256) {
return type(uint256).max;
}

/** @dev See {IERC4626-maxMint}. */
/// @inheritdoc IERC4626
function maxMint(address) public view virtual returns (uint256) {
return type(uint256).max;
}

/** @dev See {IERC4626-maxWithdraw}. */
/// @inheritdoc IERC4626
function maxWithdraw(address owner) public view virtual returns (uint256) {
return _convertToAssets(balanceOf(owner), Math.Rounding.Floor);
return previewRedeem(maxRedeem(owner));
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the change I propose.

In the case of ERC4626Fees, the fees inclusion in previewRedeem woudl reflect here.
Additionally, if maxRedeem is ever overriden, it would reflect here (but the inverse is not true)

Copy link
Collaborator

@james-toussaint james-toussaint Sep 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be useful to add doc stating that only previewRedeem(..) needs to be override, something close to this comment or close to a previous comment from you "Someone that want to change the behvarior of both functions only has one function to override, and both behavior would be consistent with one another" into the top documentation of the contract (before or after [CAUTION]).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does that look good ? fa67c3f

}

/** @dev See {IERC4626-maxRedeem}. */
/// @inheritdoc IERC4626
function maxRedeem(address owner) public view virtual returns (uint256) {
return balanceOf(owner);
}

/** @dev See {IERC4626-previewDeposit}. */
/// @inheritdoc IERC4626
function previewDeposit(uint256 assets) public view virtual returns (uint256) {
return _convertToShares(assets, Math.Rounding.Floor);
}

/** @dev See {IERC4626-previewMint}. */
/// @inheritdoc IERC4626
function previewMint(uint256 shares) public view virtual returns (uint256) {
return _convertToAssets(shares, Math.Rounding.Ceil);
}

/** @dev See {IERC4626-previewWithdraw}. */
/// @inheritdoc IERC4626
function previewWithdraw(uint256 assets) public view virtual returns (uint256) {
return _convertToShares(assets, Math.Rounding.Ceil);
}

/** @dev See {IERC4626-previewRedeem}. */
/// @inheritdoc IERC4626
function previewRedeem(uint256 shares) public view virtual returns (uint256) {
return _convertToAssets(shares, Math.Rounding.Floor);
}

/** @dev See {IERC4626-deposit}. */
/// @inheritdoc IERC4626
function deposit(uint256 assets, address receiver) public virtual returns (uint256) {
uint256 maxAssets = maxDeposit(receiver);
if (assets > maxAssets) {
Expand All @@ -180,7 +200,7 @@ abstract contract ERC4626 is ERC20, IERC4626 {
return shares;
}

/** @dev See {IERC4626-mint}. */
/// @inheritdoc IERC4626
function mint(uint256 shares, address receiver) public virtual returns (uint256) {
uint256 maxShares = maxMint(receiver);
if (shares > maxShares) {
Expand All @@ -193,7 +213,7 @@ abstract contract ERC4626 is ERC20, IERC4626 {
return assets;
}

/** @dev See {IERC4626-withdraw}. */
/// @inheritdoc IERC4626
function withdraw(uint256 assets, address receiver, address owner) public virtual returns (uint256) {
uint256 maxAssets = maxWithdraw(owner);
if (assets > maxAssets) {
Expand All @@ -206,7 +226,7 @@ abstract contract ERC4626 is ERC20, IERC4626 {
return shares;
}

/** @dev See {IERC4626-redeem}. */
/// @inheritdoc IERC4626
function redeem(uint256 shares, address receiver, address owner) public virtual returns (uint256) {
uint256 maxShares = maxRedeem(owner);
if (shares > maxShares) {
Expand Down