Skip to content

Refresh osToken converter after harvest and haircut withdrawable cap - #822

Open
cyc60 wants to merge 1 commit into
fix/redeem-loop-lazy-budgetfrom
fix/redeem-converter-refresh-haircut
Open

Refresh osToken converter after harvest and haircut withdrawable cap#822
cyc60 wants to merge 1 commit into
fix/redeem-loop-lazy-budgetfrom
fix/redeem-converter-refresh-haircut

Conversation

@cyc60

@cyc60 cyc60 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes deterministic InvalidReceivedAssets reverts on cap-bound partial redemptions by (1) recreating the osToken converter after update_vaults_state settles and (2) applying a small haircut when a position is capped to the vault's withdrawable assets.

OsTokenVaultController.convertToAssets grows every second via avgRewardPerSecond. The converter used for the shares↔assets math was snapshotted before update_vaults_state waited for harvest receipts, so by the time a redemption transaction landed, the true rate had moved past the snapshot.

That mattered exactly in the partial-redemption path: when a position is capped to the vault's withdrawable assets, the operator computed shares_to_redeem = to_shares(withdrawable) with the stale rate, i.e. shares whose true value already equals (or exceeds) everything the vault can pay out. On-chain, _redeemOsToken recomputes receivedAssets with the current rate and reverts with InvalidReceivedAssets when it exceeds availableAssets (OsTokenUtils.sol) — and the margin for error is < 2 wei of flooring slack against ~1e10 wei of rate growth per 12 seconds of lag.

Example. Vault has 5 ETH withdrawable; the position is worth 8 ETH, so it is capped. Converter snapshot says 5 ETH = 4.75 osETH shares; the operator submits 4.75 shares. Twenty-four seconds later the transaction is included, 4.75 shares now convert to 5.00000002 ETH > 5 ETH available → revert. Every cycle replays the same math with the same lag, so the vault contributes 0 toward the queue until it is topped up past the full position value.

Fix.

  • The converter is recreated after update_vaults_state (harvests change state and take receipt-wait time; a pre-harvest snapshot is guaranteed stale).
  • When capping to withdrawable assets, _apply_withdrawable_haircut targets 99.9% of the withdrawable amount (WITHDRAWABLE_ASSETS_HAIRCUT = 0.999), leaving ~5 ETH × 0.001 = 0.005 ETH of headroom — several orders of magnitude more than the rate drifts between simulation and inclusion, while the residual remains redeemable in the next cycle.

Final PR of the redeem-loop stack — based on #821's branch, which builds on #820.

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.

1 participant