feat: align RLS contraction clamp with stable-first transfer semantics#723
feat: align RLS contraction clamp with stable-first transfer semantics#723
Conversation
mento-val
left a comment
There was a problem hiding this comment.
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.
_clampContractionnow 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
_handleCallbackis unchanged and correct —protocolIncentiveAmount + liquiditySourceIncentiveAmountis always ≤amountToTransferToReservedue toLS_INCENTIVE_TOO_HIGHguards 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.
mento-val
left a comment
There was a problem hiding this comment.
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:
_transferToPoolalready uses stable-first semantics (mint vs. transfer)_clampContractiondid 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_SUPPORTEDrevert ✓
Minor observations (not blocking):
- No test for the
collateralBalance >= idealCollateralToReceivesuccess path in the pure-collateral case (the else-branch returning ideal amounts). This is a pre-existing gap not introduced by this PR. - No
fpmmToken1Debtvariant 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
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:
Tests: