← Back

When someone asks me how a vault prices a redemption, the honest answer is: it depends on when the price gets fixed. In a synchronous ERC-4626 vault, the price is fixed in the same transaction that burns your shares. In an asynchronous ERC-7540 vault, there is a gap between asking to leave and actually leaving. The standard deliberately leaves the pricing of that gap to the implementation, and most of the bugs I look for live inside it.

This post walks through both standards as they are written, then the bugs I check first when I review a redemption path.

How does an ERC-4626 vault price a redemption?

ERC-4626 is atomic. You call redeem(shares, receiver, owner), the vault burns your shares and sends assets in the same transaction. The price is whatever the vault's accounting says at that block.

The standard gives you three views of that price, and they are not interchangeable:

  • convertToAssets(shares) is the "ideal" average-user rate. It must not include fees, must not vary by caller, and must round down. The spec explicitly allows it to be inexact, so it can be built as a robust oracle (for example, time-weighted).
  • previewRedeem(shares) simulates a redeem at the current block. It must include withdrawal fees and must return no more than what redeem would actually pay in the same transaction.
  • totalAssets() is everything the vault manages, including yield, and must be inclusive of any fees charged against assets in the vault.

The spec warns that preview* values are as exact as possible, so they can be manipulated by changing on-chain conditions, and they are not always safe to use as a price oracle. If another protocol prices your shares from previewRedeem, that is the first thing I look at.

For a sync vault, then, "how is the redemption priced?" really means "what goes into totalAssets() and totalSupply() at the moment redeem runs, and can someone move either one inside the same block?"

Why do RWA and staking vaults need async redemptions?

ERC-4626 assumes the vault can pay out now. That breaks as soon as the underlying asset can't be sold instantly: real-world assets, undercollateralised or cross-chain lending, liquid staking with an unbonding period, insurance modules. ERC-7540 names all of these in its motivation and extends ERC-4626 with Requests.

A vault can make deposits async, redemptions async, or both. For redemptions, the spec's own example is a liquid staking token: deposits are instant, exits wait out the unstaking period.

What happens when you call requestRedeem?

The redemption goes through three states:

  1. Pending. You call requestRedeem(shares, controller, owner). The vault takes control of your shares. It may lock them or burn them straight away, but they must leave the owner's custody now and be burned by the time the request is claimed. pendingRedeemRequest(requestId, controller) goes up.
  2. Claimable. The vault fulfils the request. How and when is up to the implementation: an operator batch, an epoch boundary, a timestamp passing. The spec has no event for this transition. claimableRedeemRequest goes up and pendingRedeemRequest goes down.
  3. Claimed. The controller (or an approved operator) calls redeem or withdraw to pull the assets. The vault must not push assets to you. Even if a request becomes claimable in the same block, it still needs a separate claim call.

A few spec details matter for pricing:

  • previewRedeem and previewWithdraw must revert in an async redemption vault. You cannot quote the exit on-chain.
  • requestRedeem returns a requestId, not an amount. The spec says plainly that the exchange rate isn't known when the request is made.
  • Requests that share a requestId are fungible. They become claimable together, at the same rate. If requestId is 0, everything is tracked per controller and aggregated.
  • There is no standard cancel flow. The spec leaves cancellation to a separate EIP, so every vault's cancel logic is custom code worth reading.

When is the redemption price actually fixed?

ERC-7540 does not say. In its words, the exchange rate "including fees and yield is up to the Vault implementation", and pending redemption requests "MAY NOT be yield-bearing and MAY NOT have a fixed exchange rate". The assets you finally receive may differ from convertToAssets(shares) at the time of the request, because the price can move between Pending and Claimed.

In practice I see three designs. I covered them in more depth, from the attacker's side, in Stale Share Pricing in Async RWA Vaults:

  • Price at request. The rate is locked when you call requestRedeem. You know exactly what you'll get.
  • Price at fulfilment. The rate is set when the vault moves the request to Claimable. The spec's reference code notes that it "may be more common to use the rate locked in upon Claimable stage", although its own naive example converts at claim time.
  • Price at claim. The rate is read when you call redeem. This is the riskiest choice, because the redeemer chooses when to claim and so can choose the price.

Who carries the NAV risk while a request is pending?

This is the question I want every async vault to answer in its docs, because the code always answers it whether or not anyone meant it to.

  • Price at request: the redeemer is protected, and the remaining holders (or the operator) carry the move. If NAV falls before the assets are actually raised, someone else covers the gap.
  • Price at fulfilment: the redeemer carries the price risk for the whole pending window, often without earning yield on shares that are already locked or burned. Remaining holders are protected, provided the price used at fulfilment is fresh.
  • Price at claim: whoever claims last gets an option on the price, paid for by everyone else.

Once a request is Claimable with a fixed amount, those assets belong to the redeemer. From that point they shouldn't earn yield for, or take losses from, anyone else.

What are the most common redemption pricing bugs?

These are the classes I check first. None of them are exotic, and the standards themselves point at most of them.

Stale pricing

If the price comes from a NAV that's updated daily, weekly or monthly, anyone who can estimate the next update can time their request against it. This is a design risk more than a coding mistake, and it gets worse the longer the update cycle and the more volatile the asset. The fix is in the design: fix the price at a fresh fulfilment, use a window or a fee that makes timing unprofitable, or both. My stale-pricing article covers the design space.

Rounding in the wrong direction

ERC-4626's security section is explicit: favour the vault. Round down when calculating assets paid out for shares, and round up when calculating shares a user must give up for a given amount of assets. convertTo* always rounds down. Async vaults add more places to get this wrong: pro-rata partial fulfilment across one requestId, splitting a claimable amount across several claims, and converting between pending shares and claimable assets. I look for any path where many small claims add up to more than one big one.

Donation and inflation attacks

An empty or nearly empty vault that reads totalAssets() from its own token balance can have its rate pushed up by a direct transfer. A victim's deposit then rounds down to zero shares. OpenZeppelin's ERC-4626 docs walk through the maths and the virtual shares and offset defence. For redemptions, the related question is whether anyone can change totalAssets() between fulfilment and claim, or between a NAV update and fulfilment, by donating, by reporting, or by moving assets in and out of strategies.

Accounting for pending and claimable amounts

This one is specific to async vaults, and I find it the most interesting:

  • If shares are burned at request but their assets stay in totalAssets() until fulfilment, the share price for remaining holders jumps the moment someone requests.
  • If assets set aside for Claimable requests are still counted in totalAssets(), those assets are claimed twice: once by the redeemer and once, through the share price, by everyone else.
  • If locked shares still count towards totalSupply() in the rate calculation, they dilute the price that's being used to fulfil them.

Pick one model, write it down, and test it with invariants.

Fee timing

Management and performance fees are usually charged by minting shares or deducting assets at some checkpoint. The question is whether that checkpoint runs before the fulfilment price is read. If it doesn't, redeemers who exit just before the fee accrues avoid it, and the remaining holders pay their share. Also check that exit fees are counted once. ERC-4626 requires previewRedeem to include withdrawal fees, so an async vault that applies a fee at fulfilment and again at claim is overcharging.

Operator and controller permissions

In ERC-7540 an operator can move a controller's assets and shares. The claim functions use controller, and msg.sender must be the controller or an approved operator. I check every request, claim and cancel path for the right controller, owner and receiver checks, because a mix-up here is a direct theft path rather than a pricing error.

A redemption pricing checklist

Before an async vault ships, I want a clear "yes" to each of these:

  • The docs say which moment fixes the redemption price: request, fulfilment or claim.
  • The price used at fulfilment can't be older than a defined maximum age, and the code enforces it.
  • Pending shares and claimable assets are excluded from (or included in) totalAssets() and totalSupply() consistently, and the choice is written down.
  • Every conversion rounds in the vault's favour, including pro-rata partial fulfilments.
  • previewRedeem and previewWithdraw revert, as ERC-7540 requires, and nothing else in the protocol relies on them.
  • Fees are accrued before the fulfilment price is read, and each fee is charged once.
  • Requests can't be claimed twice, and claims are pulled rather than pushed.
  • Cancellation, if there is one, can't be used to back out after the price is known.
  • Operator, controller and owner checks are correct on every request and claim path.
  • Invariant tests cover these properties: the sum of all claims stays within the assets set aside, and the share price for remaining holders doesn't move when someone requests, is fulfilled or claims.

Getting your redemption path reviewed

Redemption pricing is where I spend most of my time when I review ERC-4626 and ERC-7540 vaults. I write Foundry invariant tests for the properties above, hand you a severity-ranked report, walk your team through it on a call, and re-check the fixes. If you're about to ship an async vault and want a second pair of eyes before the main audit, tell me about it.

Sources: ERC-4626, ERC-7540, OpenZeppelin ERC-4626 docs.