Skip to content

Conversation

Amxx
Copy link
Collaborator

@Amxx Amxx commented Jul 30, 2024

@cairoeth WDYT?

PR Checklist

  • Changeset entry (run npx changeset add)

Copy link

changeset-bot bot commented Jul 30, 2024

🦋 Changeset detected

Latest commit: e20baa8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
openzeppelin-solidity Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

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

@Amxx Amxx changed the title Refactor ERC4626-maxRedeem to reflect preview functions overrides Refactor ERC4626-maxWithdraw to reflect other functions overrides Jul 30, 2024
Amxx and others added 2 commits July 30, 2024 17:08
Co-authored-by: Ernesto García <ernestognw@gmail.com>
ernestognw
ernestognw previously approved these changes Jul 30, 2024
@Amxx Amxx added this to the 5.1-after-freeze milestone Jul 30, 2024
@ernestognw ernestognw dismissed their stale review July 30, 2024 20:47

This is a breaking change

@ernestognw
Copy link
Member

On second thought (thanks @cairoeth), I don't think the changelog entry is enough. It seems like there are multiple cases of ERC4626 vaults that override previewRedeem, which would affect the logic in maxWithdraw.

To me, it's not a huge deal because I can't see how it may affect the vault critically. If we continue with this change, we should discuss whether a changelog entry under the "Breaking Changes" section is good enough, or whether the change will cause an issue in some projects.

@cairoeth
Copy link
Contributor

cairoeth commented Jul 30, 2024

Following up on ^, the initial problem is that maxRedeem and maxWithdraw do not consider fees in the ERC4626Fees implementation -- I was thinking that a solution is just overriding maxRedeem in ERC4626Fees with something like this PR, instead of making changes in ERC4626. I mainly saw it as a problem in the ERC4626Fees implementation rather that in ERC4626. Based on the examples @ernestognw provided, I think this PR would make it easier for developers to mistakenly modify maxWithdraw by overriding previewRedeem (here is a good example), whereas before they weren't really connected as maxWithdraw just used _convertToAssets.

All in all, I think the changes in the PR are good but imo we can avoid a breaking change by applying them directly in ERC4626Fees

@ernestognw
Copy link
Member

ernestognw commented Jul 30, 2024

Opened #5135 as an alternative. It would make sense to include the maxWithdraw override in ERC4626 given that anyone adding fees would need to override maxWithdraw as well, and it's perhaps not obvious.

I'm not sure that's worth documenting. The "raw" ERC4626 shouldn't have any concerns about fees, and overriding functions are already assumed under the developer's risk.

@pcaversaccio
Copy link
Contributor

So @cairoeth and myself quickly discussed this change, and if you read the maxWithdraw section of the EIP-4626 carefully, it states:

image

Now given the changeset for this PR, you could override previewRedeem with a function that reverts and thus it disables maxWithdraw and is hence not anymore EIP-4626 compliant. Note, that previewRedeem is allowed to revert:

MAY revert due to other conditions that would also cause redeem to revert.

Under these circumstances, I would not recommend implementing this change.

@Amxx
Copy link
Collaborator Author

Amxx commented Sep 10, 2024

@pcaversaccio

Now given the changeset for this PR, you could override previewRedeem with a function that reverts and thus it disables maxWithdraw and is hence not anymore EIP-4626 compliant.

My deep feeling is that withdraw and redeem are two ways of expressing the same operation. Its like saying "change 1$ into €" vs saying "change 0.91€ worth of $ into €". You give the number differently, but its essentially the same thing. I'd be curious why anyone would want to disable withdraw while keeping redeem functionnal. IMO it makes no sens.

This PR would increass the coupling between the two.

To me the consequence of this coupling are the following:

  • 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. We've example of code where one of the override was missed, and you end up with undesired inconsistent behavior.
  • Someone that wants to implement an inconsistency between the two function can still do it, but it may require 2 overrides instead of one.

It is my personal opinion that the first point (wanting consistency) is way more common (and desirable), and this is the one we should favor in our designs. We also identified instances of buggy vaults that would not have been buggy if this PR had been implemented.
I also think that the second point (implement inconsistent behavior between redeem and withdraw) is going to be an edge case that should be well documented. As such, I think asking the dev to override both side so that the inconsistency appears clearly is a good thing.

That is why I continue to support this change.

If you can find any example of real production code (not POCs) that this would break, I'd love to study them!

@Amxx Amxx modified the milestones: 5.1-after-freeze, 5.2 Oct 2, 2024
@Amxx
Copy link
Collaborator Author

Amxx commented Oct 21, 2024

This is likelly going to be being delayed, from 5.2 to 5.3.

So far:

  1. We do have examples of production contracts where this change would have prevented issues/inconstency.
  2. We don't have concrete example of contracts where this change would have caused issues.

We are delaying this because we are waiting for example of the second. At some point we need to stop delaying that more. If 2. doesn't exist, we should merge this PR to benefit from 1..

@frangio @ernestognw @arr00

@Amxx Amxx modified the milestones: 5.2, 5.3 Oct 21, 2024
@Amxx Amxx modified the milestones: 5.3, 5.4 Feb 27, 2025
@Amxx Amxx removed this from the 5.4 milestone Jun 5, 2025
@Amxx Amxx added this to the 5.5 milestone Jun 5, 2025
james-toussaint
james-toussaint previously approved these changes Sep 1, 2025
function maxWithdraw(address owner) public view virtual returns (uint256) {
return _convertToAssets(balanceOf(owner), Math.Rounding.Floor);
return previewRedeem(maxRedeem(owner));
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]).

function previewWithdraw(uint256 assets) public view virtual returns (uint256) {
return _convertToShares(assets, Math.Rounding.Ceil);
}

/** @dev See {IERC4626-previewRedeem}. */
/// @inheritdoc IERC4626
Copy link
Contributor

Choose a reason for hiding this comment

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

We should add a note here that if a developer chooses to make previewRedeem revert under certain scenarios, they should ensure maxWithdraw won't revert.

function previewWithdraw(uint256 assets) public view virtual returns (uint256) {
return _convertToShares(assets, Math.Rounding.Ceil);
}

/** @dev See {IERC4626-previewRedeem}. */
/// @inheritdoc IERC4626
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// @inheritdoc IERC4626
/**
* @dev See {IERC4626-previewRedeem}.
* NOTE: If this function is overridden to introduce reverting logic, ensure that `maxWithdraw` is overridden as necessary to return successfully in all situations.
*/

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 messes up the documentation. Instead, I'm proposing to add an "override guide" at the contract level: fa67c3f

Fell free to propose adding to it.

Copy link

coderabbitai bot commented Sep 9, 2025

Note

Reviews paused

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Walkthrough

  • Updates changeset to bump openzeppelin-solidity minor version.
  • IERC4626.sol: fixes a docstring typo in previewRedeem (“redeemption” → “redemption”); no signature changes.
  • ERC4626.sol: adds a header note on overriding deposit/withdraw and clarifies maxWithdraw vs. maxRedeem relationship.
  • Replaces dev comments with “/// @inheritdoc IERC4626” across public methods; no behavior change from documentation updates.
  • Functional adjustment: maxWithdraw(owner) now uses previewRedeem(maxRedeem(owner)) instead of _convertToAssets(balanceOf(owner), Math.Rounding.Floor).
  • No exported/public interfaces modified.

Pre-merge checks (2 passed, 1 inconclusive)

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Description Check ❓ Inconclusive The description currently only addresses a reviewer and lists a checklist item without summarizing the pull request changes, making it too vague to convey meaningful information about the modifications. Please expand the description to include a concise summary of the key changes, their purpose, and any relevant background or impact details.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly identifies the primary change of refactoring the ERC4626 maxWithdraw implementation to align with other function overrides and clearly communicates the main objective of the pull request.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
contracts/token/ERC20/extensions/ERC4626.sol (2)

122-150: NatSpec inheritance: verify docs output preserves important local notes.

Switching to @inheritdoc is fine, but some generators only lift the next @dev from the interface. Confirm the rendered docs still include any OZ‑specific notes where needed (you already kept a local block for decimals()).

Also applies to: 157-161, 162-181


177-180: Add a local note on previewRedeem about maxWithdraw dependency.

Make the dependency explicit where users look for it. This mirrors prior feedback and reduces surprises.

-    /// @inheritdoc IERC4626
+    /**
+     * @inheritdoc IERC4626
+     * NOTE: {maxWithdraw} relies on this function. If you change rounding, fees, or add edge-case logic here,
+     * ensure `maxWithdraw` remains non-reverting and consistent with `maxRedeem`.
+     */
     function previewRedeem(uint256 shares) public view virtual returns (uint256) {
         return _convertToAssets(shares, Math.Rounding.Floor);
     }
🧹 Nitpick comments (3)
.changeset/shiny-dolphins-lick.md (1)

1-6: Strengthen the changeset with migration notes and cautions.

This subtly changes behavior for vaults that override previewRedeem/maxRedeem (e.g., to include fees/limits). Add a brief “Potential impact” and “Action for custom vaults” to guide upgraders.

 ---
 'openzeppelin-solidity': minor
 ---
 
 `ERC4626`: compute `maxWithdraw` using `maxRedeem` and `previewRedeem` so that changes to the preview functions affect the max functions.
+
+### Potential impact
+- Vaults overriding `previewRedeem` and/or `maxRedeem` will now affect `maxWithdraw`. This is typically desired (e.g., to include fees and limits), but note that `maxWithdraw` MUST NOT revert.
+
+### Action for custom vaults
+- Ensure your `previewRedeem` implementation complies with the ERC‑4626 requirement “MUST NOT revert”. If you intentionally add reverting logic in `previewRedeem`, also override `maxWithdraw` to keep it non‑reverting in all situations.
contracts/token/ERC20/extensions/ERC4626.sol (2)

47-58: Document the revert-coupling risk and single-override guidance.

Good addition. Please explicitly note that preview functions must not revert, and that reverting previewRedeem can make maxWithdraw revert after this change. This aligns expectations and answers prior concerns.

  * Note: When overriding this contract, some elements have to be considered
  *
  * * When overriding the behavior of the deposit or withdraw mechanism, one should override the internal functions.
  * Overriding {_deposit} automatically affects both {deposit} and {mint}. Similarly, overriding {_withdraw}
  * automatically affects both {withdraw} and {redeem}. Having inconsistent behavior between the {deposit} and {mint}
  * or between {withdraw} and {redeem} can lead to bugs. Overall it is not recommended to override the public facing
  * functions.
  *
  * * {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.
+ *
+ * * Per ERC‑4626, preview functions (e.g., {previewRedeem}) MUST NOT revert and MUST NOT depend on msg.sender. If you
+ *   introduce reverting logic in {previewRedeem}, override {maxWithdraw} accordingly to preserve its MUST‑NOT‑revert
+ *   guarantee.

152-155: Behavioral change LGTM; optional defensive non-reverting guard.

The new composition correctly propagates fees/limits via maxRedeem/previewRedeem. Optional: enforce the MUST‑NOT‑revert property even if a custom previewRedeem erroneously reverts, by using an external self-call with try/catch and falling back to 0.

-    function maxWithdraw(address owner) public view virtual returns (uint256) {
-        return previewRedeem(maxRedeem(owner));
-    }
+    function maxWithdraw(address owner) public view virtual returns (uint256) {
+        uint256 shares = maxRedeem(owner);
+        // Defensive: preserve MUST-NOT-revert even if an override of previewRedeem reverts.
+        try IERC4626(address(this)).previewRedeem(shares) returns (uint256 assets) {
+            return assets;
+        } catch {
+            return 0;
+        }
+    }

Note: This keeps semantics while avoiding unexpected reverts; cost is an external self-call in a view.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c1d49a3 and 687ddc7.

📒 Files selected for processing (3)
  • .changeset/shiny-dolphins-lick.md (1 hunks)
  • contracts/interfaces/IERC4626.sol (1 hunks)
  • contracts/token/ERC20/extensions/ERC4626.sol (5 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: coverage
  • GitHub Check: tests-upgradeable
  • GitHub Check: tests-foundry
  • GitHub Check: tests
🔇 Additional comments (1)
contracts/interfaces/IERC4626.sol (1)

201-215: Typo fix LGTM.

Spelling correction in the previewRedeem NatSpec is correct; no interface changes.

@Amxx
Copy link
Collaborator Author

Amxx commented Sep 9, 2025

@coderabbitai pause

Copy link

coderabbitai bot commented Sep 9, 2025

✅ Actions performed

Reviews paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants