Skip to content

Configurable range margins + optional deposit whitelist - #233

Open
razww wants to merge 2 commits into
feature/v3-lpfrom
audit/v3-lp-fixes
Open

Configurable range margins + optional deposit whitelist#233
razww wants to merge 2 commits into
feature/v3-lpfrom
audit/v3-lp-fixes

Conversation

@razww

@razww razww commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Two independent changes to the V3 LP collateral stack, both behaviour-neutral on the shipped defaults.

1. Range margins become deploy-time parameters

The rate-centered range was a single symmetric constant, so the two sides could not be sized independently — even though they do different jobs:

  • lower margin has to cover the LST's structural discount to its redemption rate (an LST mints instantly but redeems through a queue, so it trades below the rate and every swap lands on that side);
  • upper margin is not trading liquidity at all. It is the drift budget between recenters (the rate only moves up, so fair marches toward tickUpper, and reaching it closes deposits) and it also bounds the min(fair, spot) deposit haircut, which grows as discount / (upper + discount).

Changes:

  • rangeLowerBps / rangeUpperBps replace the constant, supplied to initialize and adjustable by MANAGER via setRangeBps.
  • The opening tick range moved into __V3DexAdapter_init, which now writes the margins before deriving the range from them. Subclass initializers previously computed the range themselves and passed it in, which would have read the margins before they were set.
  • wstETH / wbETH take maxTwapDeviationBps at initialize too, validated against the supplied upper margin: a TWAP clamped to the top of its band must not land at or past tickUpper, where the fair composition loses a leg and every deposit reverts. Its default drops to 25bps, which removes a post-deploy configuration step.
  • maxSpotDeviationBps is now sized off the measured spot-vs-rate discount instead of inheriting the range width; the two are unrelated.

Defaults keep the shipped 50/50 geometry, so on-chain behaviour is unchanged.

Sizing note: a fork measurement on the slisBNB/WBNB pool showed the upper margin driving the deposit haircut directly — at [-25, +15] the same deposit was credited 44% fewer shares than at [-25, +50]. The parameters are left at 50/50 pending a decision on the target geometry.

2. Optional deposit whitelist

An opt-in allowlist for a gated launch. Shares are already non-transferable except by Moolah, so closing the deposit entry point is enough to keep them off non-whitelisted addresses; nothing else needs a gate.

Both msg.sender and onBehalf are checked — gating only the caller would let a whitelisted address open a position for anyone.

withdraw, redeemShares, withdrawShares, the Moolah liquidation callback and the BOT paths are deliberately left open: gating an exit would strand a delisted holder's funds, and gating a seizure would socialise bad debt to lenders. Two of the new tests pin that behaviour so a later change cannot quietly extend the gate to those paths.

Disabled by default, so existing deployments and tests are unaffected until MANAGER turns it on.

Testing

All 8 suites touching the changed contracts pass — 382 tests, 0 failures. The tick assertions in the existing suites are relational (tickLower < tickUpper), so they were unaffected by the range split; only the derived default-value assertions needed updating.

🤖 Generated with Claude Code

razww and others added 2 commits August 21, 2026 16:34
The rate-centered range was a single symmetric constant, so the two sides could
not be sized independently even though they do different jobs: the lower margin
has to cover the LST's structural discount to its redemption rate, while the
upper margin is the drift budget between recenters and also bounds the
min(fair,spot) deposit haircut, which grows as discount/(upper + discount).

- Replace the constant with rangeLowerBps / rangeUpperBps storage, supplied to
  initialize and adjustable by MANAGER via setRangeBps.
- Move the opening tick range into __V3DexAdapter_init, which now writes the
  margins before deriving the range from them. Subclass initializers previously
  computed the range themselves and passed it in, which would have read the
  margins before they were set.
- wstETH / wbETH take maxTwapDeviationBps at initialize too, checked against the
  supplied upper margin: a TWAP clamped to the top of its band must not land at
  or past tickUpper, where the fair composition loses a leg and deposits revert.
  Its default drops to 25bps, which no longer needs setting post-deploy.
- Size maxSpotDeviationBps off the measured spot-vs-rate discount instead of
  inheriting the range width; the two are unrelated.

Defaults keep the shipped 50/50 geometry, so behaviour is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds an opt-in allowlist for a gated launch. Shares are already
non-transferable except by Moolah, so closing the deposit entry point is enough
to keep them off non-whitelisted addresses; nothing else needs a gate.

Both msg.sender and onBehalf are checked — gating only the caller would let a
whitelisted address open a position for anyone.

Withdraw, redeemShares, withdrawShares, the Moolah liquidation callback and the
BOT paths are deliberately left open. Gating an exit would strand a delisted
holder's funds, and gating a seizure would socialise bad debt to lenders; two of
the new tests pin that behaviour so a later change cannot quietly extend the
gate to those paths.

Disabled by default, so existing deployments and tests are unaffected until
MANAGER turns it on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@hashdit-bot

hashdit-bot Bot commented Aug 21, 2026

Copy link
Copy Markdown

Pull Request Review

This Solidity DeFi PR makes V3 LP range margins independently configurable at initialization and by the MANAGER role, while adding TWAP-band validation for the wstETH and wbETH adapters. It also introduces an optional deposit allowlist in V3Provider, updates proxy initialization arguments, and adds tests covering allowlisted deposits and unrestricted exits/liquidations.

Sensitive Content

No sensitive content detected.

Security Issues

🟠 [HIGH] Deposit whitelist can be bypassed through existing share-supply paths

File: src/provider/v3/V3Provider.sol
The new allowlist is enforced only in deposit(...), but an existing share holder can call supplyShares(..., onBehalf) for an unlisted beneficiary because that function performs no whitelist checks. More fundamentally, because transferFrom permits Moolah to transfer shares from any holder, a holder may approve Moolah and call its supplyCollateral entry point directly, bypassing V3Provider.deposit and supplyShares entirely. Thus, after any shares have been withdrawn or otherwise obtained, whitelisted holders can place collateral onto arbitrary non-whitelisted accounts, contradicting the intended gated-launch security boundary.
Recommendation: Enforce the allowlist at every path that can credit V3 LP collateral to an account, preferably in the Moolah collateral-accounting layer where onBehalf is known. If supply must be routed through V3Provider, gate both the caller and onBehalf in supplyShares and prevent direct Moolah pulls from wallet-held shares—for example, restrict transferFrom so Moolah may pull only shares staged at the provider—while carefully preserving withdrawal and liquidation flows. Add regression tests for supplyShares and direct MOOLAH.supplyCollateral attempts targeting an unlisted account.


Generated by Hashdit Bot. This tool can absolutely NOT replace manual audits.

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