219220 return REQUEST_ID;
220221 }
221222
222- /// @notice Fulfill a pending redemption — burns escrowed shares and transfers assets.
223+ /// @notice Fulfill `shares` of a receiver's pending redemption — burns the escrowed shares and
224+ /// pays the assets. Reverts while paused.
225+ /// @dev The reserved gross is released in proportion to `shares`; the final fulfilment takes
226+ /// the exact remainder so no dust is left behind. The operator chooses the price: the
227+ /// reserved gross (the default), or the current oracle price (`atCurrentPrice`) for
228+ /// exceptional events such as a haircut on the underlying. Current-price settlement is
229+ /// refused when the entry's current value exceeds its reservation, so it can only reduce
230+ /// the payout and never draws on liquidity reserved for others. The withdrawal fee
231+ /// applies at the live rate either way; `totalPendingAssets` always releases the reserved
232+ /// amount. A slice whose gross value rounds to zero is refused. Only the current-price
233+ /// path reads the oracle.
223234 /// @param receiver Address whose pending request is being fulfilled.
224- /// @param shares Amount of escrowed shares to burn.
225- /// @param assetsWithFee Gross asset amount (including withdrawal fee).
226- function fulfillRedeem(address receiver, uint256 shares, uint256 assetsWithFee) external requiresAuth {
235+ /// @param shares Escrowed shares to settle; the reserved assets follow proportionally.
236+ /// @param atCurrentPrice Pay the shares at the current oracle price instead of the reserved gross.
237+ function fulfillRedeem(address receiver, uint256 shares, bool atCurrentPrice) external requiresAuth whenNotPaused {
227238 PendingRedeem storage pending = _pendingRedeem[receiver];
228- require(pending.shares != 0 && shares <= pending.shares, Errors.InvalidSharesAmount());
229- require(pending.assets != 0 && assetsWithFee <= pending.assets, Errors.InvalidAssetsAmount());
239+ uint256 pendingShares = pending.shares;
240+ require(shares != 0 && shares <= pendingShares, Errors.InvalidSharesAmount());
241+ uint256 pendingAssets = pending.assets;
230242
231- pending.shares -= shares;
232- pending.assets -= assetsWithFee;
233- totalPendingAssets -= assetsWithFee;
243+ // Pro-rata release; on the final slice `mulDiv(a, n, n) == a`, so the exact remainder settles.
244+ uint256 reservedAssets = pendingAssets.mulDiv(shares, pendingShares, Math.Rounding.Floor);
245+ uint256 assetsWithFee = reservedAssets;
246+ if (atCurrentPrice) {
247+ // Judged on the whole entry. Reservations were floored per request, so an entry built
248+ // from several requests can exceed its reservation by up to (requests - 1) wei at an
249+ // unchanged price and be refused; use the request price then — the payout is the same.
250+ uint256 currentValue = _convertToAssets(pendingShares, Math.Rounding.Floor);
251+ require(currentValue <= pendingAssets, Errors.CurrentPriceAboveRequestPrice(currentValue, pendingAssets));
252+ assetsWithFee = currentValue.mulDiv(shares, pendingShares, Math.Rounding.Floor);
253+ }
254+ // Refuse slices whose gross rounds to zero, so at most sub-wei rounding is ever parked in
255+ // the reservation. (The receiver's net can still round to zero on wei-sized slices under a
256+ // nonzero fee; that wei is collected as fee, not lost.)
257+ require(assetsWithFee != 0, Errors.InvalidAssetsAmount());
234258
259+ pending.shares = pendingShares - shares;
260+ pending.assets = pendingAssets - reservedAssets;
261+ totalPendingAssets -= reservedAssets;
262+
235263 emit RequestFulfilled(receiver, shares, assetsWithFee);
236264 // burn the shares from the vault and transfer the assets to the receiver
237265 _withdraw(address(this), receiver, address(this), assetsWithFee, shares);
238266 }
239267
240- /// @notice Cancel a pending redemption — returns escrowed shares to the receiver.
268+ /// @notice Cancel a receiver's whole pending redemption — returns the escrowed shares to the
269+ /// receiver. Reverts while paused.
241270 /// @param receiver Address whose pending request is being cancelled.
242- /// @param shares Amount of escrowed shares to return.
243- /// @param assetsWithFee Gross asset amount to release from the pending total.
244- function cancelRedeem(address receiver, uint256 shares, uint256 assetsWithFee) external requiresAuth {
271+ function cancelRedeem(address receiver) external requiresAuth whenNotPaused {
245272 PendingRedeem storage pending = _pendingRedeem[receiver];
246- require(pending.shares != 0 && shares <= pending.shares, Errors.InvalidSharesAmount());
247- require(pending.assets != 0 && assetsWithFee <= pending.assets, Errors.InvalidAssetsAmount());
273+ uint256 shares = pending.shares;
274+ require(shares != 0, Errors.InvalidSharesAmount());
275+ uint256 reservedAssets = pending.assets;
248276
249- pending.shares -= shares;
250- pending.assets -= assetsWithFee;
251- totalPendingAssets -= assetsWithFee;
277+ delete _pendingRedeem[receiver];
278+ totalPendingAssets -= reservedAssets;
252279
253- emit RequestCancelled(receiver, shares, assetsWithFee);
254- // transfer the shares back to the owner
280+ emit RequestCancelled(receiver, shares, reservedAssets);
281+ // return the escrowed shares to the receiver
255282 _transfer(address(this), receiver, shares);
256283 }
257284