Skip to content

feat: align RLS contraction clamp with stable-first transfer semantics#723

Open
asker61 wants to merge 3 commits intodevelopfrom
fix/rls-stable-stable-clamp
Open

feat: align RLS contraction clamp with stable-first transfer semantics#723
asker61 wants to merge 3 commits intodevelopfrom
fix/rls-stable-stable-clamp

Conversation

@asker61
Copy link
Copy Markdown

@asker61 asker61 commented Mar 18, 2026

Summary:
ReserveLiquidityStrategy currently clamps contraction against ReserveV2 inventory even when the token sent to the pool is configured as a stable asset and will be minted. That makes stable/stable pools behave incorrectly and diverges from the actual transfer path.

This PR makes _clampContraction follow the same stable-first semantics as _transferToPool:

  • stable assets return the ideal contraction amounts without reserve-balance clamping
  • collateral assets keep the existing reserve-backed clamp
  • unsupported assets still revert

Tests:

  • stable/collateral still clamps as before
  • stable/stable no longer clamps on reserve inventory
  • dual stable+collateral registration follows stable-first semantics

@asker61 asker61 requested review from a team, Andrew718PLTS and bowd March 18, 2026 11:59
@bayological bayological requested a review from mento-val March 18, 2026 12:06
Copy link
Copy Markdown

@mento-val mento-val left a comment

Choose a reason for hiding this comment

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

Code Review — _clampContraction stable-first fix

Thanks for the clear PR description and the well-structured test file. The fix makes sense conceptually — _clampContraction was out of sync with _transferToPool's token dispatch logic, and this brings them back in alignment.

Here's what I found, categorized by severity:


🟡 Medium — Minting is unbounded for stable collateral tokens during contraction

Location: _clampContraction, stable-asset branch

if (reserve.isStableAsset(collateralToken)) {
    // Stable assets are minted, so no reserve balance constraint
    return (idealDebtToContract, idealCollateralToReceive);
}

For stable/stable pools, contraction now returns the ideal amounts unclamped. _transferToPool will then call IERC20MintableBurnable(token).mint(pool, amount) for the full ideal amount — with no on-chain cap.

The ideal amount is bounded only by FPMM AMM math (pool reserves + oracle price). Under normal conditions this is fine, but if the oracle is manipulated or goes stale, an attacker able to trigger a rebalance could cause the strategy to mint a disproportionately large amount of stable tokens, inflating supply.

Likelihood: Low — Mento's oracle stack (medianizer, report age checks) provides protection, and this PR doesn't introduce the oracle risk. But it does remove the reserve-balance cap as a defense-in-depth layer for stable/stable pools, which existed for free before.

Suggestion (non-blocking): Consider a configurable per-pool maxContractionAmount cap in _clampContraction for stable collateral tokens. Not required to merge, but worth a follow-up.


🟢 Low — Error code silently changes for unregistered tokens

Location: _clampContraction, else branch

Before: Unregistered tokens → balanceOf(reserve) returns 0 → revert RLS_RESERVE_OUT_OF_COLLATERAL
After: Unregistered tokens → revert RLS_TOKEN_IN_NOT_SUPPORTED

The new error is semantically more correct. But if any off-chain keeper or monitoring tool catches RLS_RESERVE_OUT_OF_COLLATERAL as a proxy for "misconfigured token", it'll silently stop catching this case. Worth a check before merging.


🟢 Low — _clampContraction and _transferToPool are now silently coupled

Both functions must check isStableAsset before isCollateralAsset. If _transferToPool is ever modified to flip the priority order, _clampContraction will diverge silently — stable tokens would be clamped against a reserve balance they'll never consume, and collateral tokens might be minted unboundedly.

Suggestion: Add a comment:

// Token dispatch order must mirror _transferToPool.

🟢 Nitpick — Formatting diff mixed with logic diff

The PR changes indentation from 2-space to 4-space throughout the entire file alongside the logic change. Makes the diff harder to review and increases cognitive load for future bisects. Consider separating formatting commits from logic commits.


🟢 Nitpick — No isToken0Debt = false coverage in new test file

All new tests in ReserveLiquidityStrategy_StableStableClamp.t.sol use fpmmToken0Debt(18, 18). The symmetric path (token1 as debt, stable/stable) isn't exercised. Suggest adding at least one fpmmToken1Debt variant.


What's correct ✅

  • The core logic fix is sound. _clampContraction now mirrors _transferToPool's stable-first classification — stable assets bypass the clamp (minted), collateral assets apply reserve-balance clamping (transferred), unregistered tokens revert explicitly.
  • Dual-registered tokens (stable + collateral): stable-first check correctly prevents unnecessary clamping, consistent with _transferToPool.
  • Incentive math in _handleCallback is unchanged and correct — protocolIncentiveAmount + liquiditySourceIncentiveAmount is always ≤ amountToTransferToReserve due to LS_INCENTIVE_TOO_HIGH guards in _addPool.
  • EIP-1153 transient storage hook guard: unchanged and correct.
  • Test file is well-structured. Formula derivations in comments are a nice touch and make the expected values verifiable.

Recommendation: Approve with suggestions. No blockers. The medium finding is a pre-existing oracle-risk concern that this PR doesn't introduce — I'd track it as a hardening follow-up rather than hold this merge.

Copy link
Copy Markdown

@mento-val mento-val left a comment

Choose a reason for hiding this comment

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

QA Review — APPROVE

PR: feat: align RLS contraction clamp with stable-first transfer semantics
Reviewer: val-mento (QA)


CI Status ✅

All checks passed: Lint & Test, Slither, Echidna (EchidnaFixidityLib).


Logic Review ✅

The fix is correct. The old _clampContraction always read IERC20(collateralToken).balanceOf(address(reserve)) regardless of whether the token would be minted (stable) or transferred (collateral). This was a bug for stable/stable pools:

  • _transferToPool already uses stable-first semantics (mint vs. transfer)
  • _clampContraction did not — it always applied reserve-balance clamping
  • Result: stable/stable contractions could revert or be undersized when reserve balance was incidentally low

The fix aligns the two paths under the same classification:

collateral type new behavior
isStableAsset → true return ideal amounts (no clamp)
isCollateralAsset keep existing reserve-balance clamp incl. zero-balance revert
neither revert RLS_TOKEN_IN_NOT_SUPPORTED()

The stable-first ordering for dual-registered tokens is explicitly documented with an invariant comment and mirrors _transferToPool. This is correct.


Test Coverage ✅

New file ReserveLiquidityStrategy_StableStableClamp.t.sol (404 lines) covers:

  • stable/collateral existing behavior: clamping path + zero-balance revert ✓
  • stable/stable new behavior: unclamped, with incentives, formula validation, token1-as-debt ordering ✓
  • dual-registered (stable + collateral): stable-first semantics, low reserve balance ✓
  • unregistered token: RLS_TOKEN_IN_NOT_SUPPORTED revert ✓

Minor observations (not blocking):

  1. No test for the collateralBalance >= idealCollateralToReceive success path in the pure-collateral case (the else-branch returning ideal amounts). This is a pre-existing gap not introduced by this PR.
  2. No fpmmToken1Debt variant for the stable/collateral clamping test (minor coverage gap).

Neither gap affects correctness of this PR.


Security Assessment

No security risk introduced. The change removes an incorrect invariant (reserve-balance check for tokens that are minted, not transferred). Slither passes cleanly.

Verdict: APPROVE

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.

2 participants