feature: V3Provider - #151
Conversation
04e090b to
7d940c4
Compare
9ff43ab to
e1759f8
Compare
|
@audit-agent V3Liquidator.sol V3DexAdapter.sol V3Provider.sol SlisBNBV3DexAdapter.sol SlisBNBV3Provider.sol SlisBNBV3ProviderOracle.sol SlisBnbInventoryLib.sol V3PositionLib.sol |
🛡️ Cloud-Auditor — PR #151 reviewDispatched: 9 specialists · Head SHA: Tally
Top findingsH-01 — H-02 — M-01 — M-02 — Resilient-oracle TOKEN0/TOKEN1 legs must be rate-derived for the pool-manipulation invariant to hold M-03 — M-04 — M-05 — Lows (9)L-01 Missing Acknowledged / verified safe
Full report (PDF) delivered to the security Telegram group. |
Pull Request ReviewThis PR introduces a new V3 collateral-provider architecture for concentrated liquidity positions by adding multiple new Solidity contracts: a generic Sensitive ContentBlockchain Address:
Security Issues🟡 [MEDIUM] Input validation relaxed in
Generated by Hashdit Bot. This tool can absolutely NOT replace manual audits. |
Pull Request ReviewThis PR introduces a new V3 concentrated-liquidity provider architecture for slisBNB/WBNB by adding multiple new Solidity components: Sensitive ContentBlockchain Address:
Security IssuesNo serious security issues detected. Generated by Hashdit Bot. This tool can absolutely NOT replace manual audits. |
Pull Request ReviewThis PR introduces a major new Solidity web3 architecture for V3 concentrated-liquidity collateral: a split between Sensitive ContentBlockchain Address:
No sensitive key material or social account handles/links detected in non-test added files. Security IssuesNo serious security issues detected. Generated by Hashdit Bot. This tool can absolutely NOT replace manual audits. |
Pull Request ReviewThis PR introduces a new V3 concentrated-liquidity provider architecture for Moolah collateral by adding a large set of Solidity contracts: Sensitive ContentBlockchain Address:
Security Issues🟡 [MEDIUM] Input validation relaxed in
Generated by Hashdit Bot. This tool can absolutely NOT replace manual audits. |
Pull Request ReviewThis Solidity DeFi PR introduces an upgradeable V3 concentrated-liquidity collateral system comprising ERC-4626 provider vaults, DEX/NFT adapters, share-price oracles, and a dedicated liquidation contract for BSC and Ethereum LST pairs. It also adds rate-centered rebalancing, native-token handling, manipulation-resistant valuation, extensive fork tests, and a small SmartProvider minter-validation change. Sensitive ContentNo sensitive content detected. Security Issues🟠 [HIGH] Pre-existing native reserves can satisfy wrapped-native flash-liquidation repayment
Generated by Hashdit Bot. This tool can absolutely NOT replace manual audits. |
… fork Remove scattered LICENSE files under dex/v3/core/ and add a single NOTICE file with proper attribution. Transition expired BUSL-1.1 headers to GPL-2.0-or-later and fix GPL-3.0 incompatibility in periphery libraries. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove the ~9.4k-LoC local fork under src/dex/v3 (a Uniswap-v3 fork ported to 0.8.34) and depend on github.com/lista-dao/lista-v3 instead. lista-v3 master is Solidity 0.7.6, so it cannot be compiled into this 0.8.34 codebase wholesale. Split by what actually compiles under 0.8.34: - Pure interfaces (IListaV3Factory, IListaV3Pool, pragma >=0.5.0) now come from the lib/lista-v3 submodule via the `lista-v3/` remapping. - The V3 math libs V3Provider executes internally (TickMath, SqrtPriceMath, LiquidityAmounts, FullMath, FixedPoint96, SafeCast, UnsafeMath) are 0.7.6 upstream and need 0.8.x unchecked semantics, so the existing 0.8.34 ports are relocated to src/provider/libraries/. - INonfungiblePositionManager: the submodule's interface inherits OZ ERC721-upgradeable interfaces whose v4-era paths/names don't exist in this repo's OZ v5.2, so a minimal self-contained NPM interface (only the methods V3Provider calls) lives in src/provider/interfaces/. Delete test/dex/v3/ListaV3.t.sol (tested the fork implementation itself; the DEX is tested in the lista-v3 repo) and repoint the IListaV3Pool import in V3Provider/V3Liquidator tests to the submodule. The unrelated StableSwap DEX under src/dex/*.sol is untouched. forge build: compiler run successful. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the 7 locally-vendored 0.8.34 math-lib ports under
src/provider/libraries/ with the audited, 0.8-native libraries from the
lista-dao-contracts submodule (pinned at 3ac9ef2, matching master).
- Register lib/lista-dao-contracts.git as a submodule + `lista-dao-contracts/`
remapping. The entry mirrors origin/master exactly, so it is conflict-free
when this branch later integrates with master.
- V3Provider imports TickMath and LiquidityAmounts from the submodule.
- _getAmountsForLiquidity now delegates to LiquidityAmounts.getAmountsForLiquidity,
which is mathematically identical to the previous SqrtPriceMath.getAmount{0,1}Delta
(roundUp=false) path. This removes the need for SqrtPriceMath (which
lista-dao-contracts does not ship a 0.8 version of) and its deps.
- Delete all 7 local ports: TickMath, SqrtPriceMath, LiquidityAmounts, FullMath,
FixedPoint96, SafeCast, UnsafeMath.
Verified: forge build clean; 84 V3Provider + 27 V3Liquidator tests pass
(covering preview/withdraw/rebalance across below/inside/above-range, which
exercises the swapped getAmountsForLiquidity path).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-rate oracle Rename V3Provider → SlisBNBV3Provider and split into a generic abstract base (V3Provider) + slisBNB/BNB specialization, per the slisBNB/BNB v3 LP PRD. - Base/derived split via virtual hooks (_afterCollateralChange, _rebalanceInventory, _valuationSqrtPriceX96, peek/getTokenConfig/receive). - ERC-4626 shell: asset = WBNB, totalAssets() in BNB; single-asset entry disabled (two-token deposit/withdraw/redeemShares + withdrawShares/supplyShares). - Exchange-rate oracle (PRD §4.5): peek/totalAssets/getUserBalanceInBnb value the position at the slisBNB exchange-rate-implied price (StakeManager convertSnBnbToBnb/convertBnbToSnBnb), never the pool spot/TWAP — manipulation-resistant. - Auto-centered range: initialize derives ticks from exchangeRate ±1%; rebalance recenters and converts inventory via the StakeManager (deposit / instantWithdraw). - Libraries: V3PositionLib (NPM primitives) + SlisBnbInventoryLib (stake/redeem + optimal-ratio conversion); audited 0.8 math from lista-dao-contracts. - require strings → custom errors; AccessControl (non-enumerable). - Tests: SlisBNBV3Provider.t.sol (USDC/WBNB generic) + SlisBNBV3ProviderRate.t.sol (slisBNB/WBNB rate-path, forked). Note: runtime bytecode currently exceeds EIP-170; size-reduction refactor pending. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…+ oracle Split the monolithic SlisBNBV3Provider (28.5 KB, over EIP-170) into three UUPS contracts, each well under the limit, with clean responsibility boundaries: - V3Provider / SlisBNBV3Provider: ERC-4626 vLP shares + Moolah wiring + share accounting (holds no NFT); slisBNB subclass adds slisBNBx mirroring + BOT rebalance. - V3DexAdapter / SlisBNBV3DexAdapter: sole V3 NFT/idle custodian + all NPM/pool math; slisBNB subclass adds the exchange-rate-implied fair price, +/-1% tick centering, and the rate-centered rebalance + StakeManager inventory conversion. - SlisBNBV3ProviderOracle: Moolah market.oracle; prices the share off the adapter's fair composition (manipulation-resistant) + resilient oracle, capped haircut. Key points: - CEI: burn shares BEFORE adapter.removeLiquidity pushes underlying to the receiver, so totalSupply stays consistent with the reduced position during the BNB callback. - Oracle reverts on zero leg price / zero total value (finding D); raw-NAV vs haircut split is clean (adapter returns raw, oracle applies the only haircut). - slisBNB/BNB-only: adapter and oracle constructors reject any non-slisBNB/WBNB pair; the oracle also asserts its tokens match the adapter's (no wiring divergence). - IV3PoolMinimal: decode only slot0 sqrtPriceX96/tick, width-agnostic to feeProtocol (Uniswap uint8 vs PancakeSwap uint32) so the adapter works against any V3 fork. - Tests retargeted to the slisBNB/WBNB pool: functional 87, rate-path 8, liquidator 27 (etched StakeManager stand-in for the not-yet-deployed instantWithdraw). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…matting - foundry.lock: remove orphaned lib/v3-core and lib/v3-periphery entries (superseded by lista-v3; no .gitmodules entry, lib dir, remapping, or import). - Apply prettier formatting to the V3 vault/adapter contracts and the liquidator + rate-path tests (npm run check now passes). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tracts Switch V3Provider, V3DexAdapter and SlisBNBV3ProviderOracle from plain AccessControlUpgradeable to AccessControlEnumerableUpgradeable, matching the codebase standard (Moolah, SmartProvider, et al.) so roles are enumerable on-chain via getRoleMember/getRoleMemberCount. Existing role logic (grantRole/hasRole/onlyRole/getRoleAdmin) is unchanged; all three stay under EIP-170. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…quidator Match the V3 provider contracts and the codebase standard so roles are enumerable on-chain (getRoleMember/getRoleMemberCount). Existing role logic is unchanged; V3Liquidator stays well under EIP-170. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
V3Provider.deposit forwarded the unused-input refund to the depositor via a native-BNB call inside adapter.addLiquidity, BEFORE _mint + supplyCollateral. During that callback the adapter NAV already included the freshly-added liquidity while totalSupply() was still stale, so SlisBNBV3ProviderOracle.peek returned a transiently inflated share price (26x on a BSC fork). Moolah was not yet reentrancy-locked at that point, letting a malicious depositor reenter Moolah.borrow and over-borrow against pre-existing collateral, leaving bad debt. Fix: the adapter now refunds to the vault, and the vault forwards the refund to the depositor only AFTER _mint + supplyCollateral, when NAV and totalSupply are consistent. Adds a guarded receive() (adapter-only) + _refund helper. Regression: test_C1_depositRefundReentrancy_neutralized drives the full attack and asserts peek during the refund equals the normal price and the attacker position stays solvent. Fails pre-fix (26x inflation, insolvent), passes post-fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ETH/wbETH Generalize the slisBNB V3 LP provider into a chain/LST-agnostic base and add Ethereum wstETH/WETH and wbETH/WETH markets. - Base: chain/LST-agnostic V3Provider / V3DexAdapter / V3ProviderOracle (WRAPPED_NATIVE immutable; virtual _lstNativeRate() / _convertToOptimalRatio() hooks). slisBNB becomes a thin, byte-equivalent subclass. All V3 contracts relocated under src/provider/v3/. - New LST families: wstETH/WETH and wbETH/WETH adapters + vaults + IWstETH/IWbETH. - Oracle: rate-implied (slisBNB) / pool-TWAP-clamped-to-rate (wstETH/wbETH) share pricing; components valued rate-derived via the resilient oracle. - Rebalance swap is DEX-agnostic: backend-built swapData against a whitelisted swapPair (SwapInventoryLib generic swap, backend amountOutMin); drop SwapRouter02 coupling (delete ISwapRouter02). - V3Liquidator: chain-agnostic native leg via provider.WRAPPED_NATIVE() for both legs; wrap native back when loanToken == wrapped-native; reject sensitive swapPair (token/pool/NPM). - Cross-validate provider<->adapter<->oracle wiring in setProvider + oracle ctor. Tests: 245 passing — BSC slisBNB/liquidator regression unchanged + Ethereum fork suites for wstETH, wbETH and V3Liquidator (incl. native-leg + wiring guards). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…onversion Add maxInstantWithdrawSlippageBps to SlisBnbInventoryLib so the slisBNB→BNB instantWithdraw leg of a rebalance enforces a bound on the StakeManager instant-redeem fee/slippage, reverting InstantWithdrawSlippage when exceeded. Threaded through convertToOptimalRatio/convert/_convert; SlisBNBV3DexAdapter passes the configured bound. Adds tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nostic swap) Make the slisBNB/BNB rebalance inventory conversion identical to the wstETH/WETH and wbETH/WETH families: a backend-built swap against a whitelisted venue, swap-pair-agnostic on chain. instantWithdraw is conceptually just a swap, so the StakeManager special-casing is no longer needed — promoting one shared implementation also simplifies the code. - Promote the generic conversion to the base V3DexAdapter: swapPairWhitelist + setSwapPairWhitelist (rejecting TOKEN0/TOKEN1/POOL/NPM) + the decode-and-swap _convertToOptimalRatio (via SwapInventoryLib, backend amountOutMin). All three rate-implied subclasses inherit it; both declared on IV3DexAdapter. - SwapInventoryLib is native-aware: when a venue settles the wrapped-native leg as the native coin (e.g. a StakeManager instant-redeem → BNB), the received native is wrapped back into the wrapped-native ERC20 before amountOutMin is measured; receive() accepts native from a whitelisted venue. ⇒ instantWithdraw works as a whitelisted swap venue (slisBNB→BNB→WBNB), with no StakeManager special-casing on chain. - Slim SlisBNBV3DexAdapter to _lstNativeRate() (StakeManager rate) + the pair guard; drop the StakeManager conversion override, the instant-withdraw slippage config, and the StakeManager receive() override. Delete SlisBnbInventoryLib. - Slim WstETH/WbETHV3DexAdapter: swap logic now in the base; keep rate hook + TWAP-clamp. - SlisBNBV3Provider.rebalance gains bytes calldata swapData, forwarded to the adapter. Behavioral change: slisBNB rebalance converts via a whitelisted venue (a DEX/aggregator OR the StakeManager instant-redeem) using backend swapData + amountOutMin, instead of a hardcoded path. Tests: 257 passing — slisBNB swap-conversion (instantWithdraw-as-venue, both directions, amountOutMin-revert) + whitelist + sensitive-pair-reject; old StakeManager-special-case/slippage tests removed; wstETH/wbETH/liquidator/reentrancy green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Build on the unified DEX-agnostic rebalance swap (9ba9766) so the V3 LP base is correct for native-settling venues and non-18-decimal tokens. - SwapInventoryLib native-aware both directions: the swapData blob gains bool nativeIn — now (swapPair, sellToken0, amountIn, amountOutMin, nativeIn, innerSwapData). nativeIn ⇒ the wrapped-native input leg is unwrapped and forwarded as msg.value (supports StakeManager.deposit{value} for WBNB→slisBNB). Any native a venue delivers (e.g. instantWithdraw → BNB) is wrapped back into the wrapped-native ERC20 before amountOutMin is measured — never stranded; reverts UnexpectedNative if neither leg is the wrapped-native. - Non-18-decimal paired token: _sqrtPriceX96FromRate now scales the raw pool price by 10^(DECIMALS1 - DECIMALS0) instead of assuming equal decimals (pure→view; cascaded to _tickRangeForRate). The wrapped-native stays 18-dec; the paired token may have any decimals. - Oracle share-decimal correctness: V3ProviderOracle quotes peek(share) per ONE WHOLE share (10 ** SHARE_DECIMALS, read from the share token) instead of hardcoded 1e18, matching Moolah._getPrice which uses collateralToken.decimals(). No-op for the current 18-dec accounting assets. Tests: 263 passing — new V3DexAdapterDecimals.t.sol (non-18-dec USDC(6)/WETH(18) + 18/18) proves the sqrtPrice decimal math; slisBNB depositAsSwapVenue (nativeIn), wrongNativeOutput_reverts, instantWithdraw no-stranded-native; swapData blobs updated to the 6-field shape. Backend encoder must add nativeIn. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…per-rebalance + daily caps) Add three independent loss guards to the V3 LP collateral provider's rebalance so a compromised BOT (hot key controlling swapData / minAmount / minLiquidity / deadline) cannot bleed the managed position: - Per-swap rate floor (V3DexAdapter._convertToOptimalRatio): the inventory-conversion swap must return >= fairAtRate(spent) * (LOSS_DENOM - maxSwapLossBp) / LOSS_DENOM, measured against the LST<->native rate (not pool spot, not the BOT's amountOutMin). maxSwapLossBp in ppm, default 5%. - Per-rebalance fair-NAV neutrality (V3Provider._guardedRebalance): the position's fair USD value (never pool spot) may not drop more than maxRebalanceLossBp (ppm, default 2%) across one rebalance. Fail-closed (OracleZero) when shares exist but NAV reads 0. - Per-UTC-day cumulative loss cap: loss-only accumulator, reverts past maxDailyLossUsd (default 1000e8). All caps are MANAGER-settable and bounded, and are emitted on init and on change. The slisBNB / wstETH / wbETH vaults route rebalance() through _guardedRebalance; IV3DexAdapter gains maxSwapLossBp() / setMaxSwapLossBp. UUPS storage gaps shrunk (adapter 47->46, provider 50->46) to keep subclass layout stable. Tests: per-swap cap (revert / within-cap pass / sell-token1), NAV neutrality (revert + zero-price fail-closed), daily cap (single-rebalance revert + same-day accumulation + next-day reset), defaults, setter access/caps, and wstETH guard inheritance. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Compounding accrued V3 fees added liquidity at the pool's current spot price with zero slippage protection (amountMin = 0). Since this runs as a side effect of deposit/withdraw/redeemShares/rebalance, a flash-loan that skews the pool spot could force the vault to mint liquidity at a manipulated price and get sandwiched on the reverting back-swap. V3DexAdapter now compares spot against the rate-anchored fair price before re-adding compounded fees to the position. When the deviation exceeds a manager-configurable threshold it skips the re-add (holds the fees as idle, valued at fair) instead of reverting, so the hot-path deposit/withdraw call still succeeds. V3Provider also exposes a permissionless compound() so fees can be swept on a regular cadence instead of only as a side effect of user activity. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… profitability V3Liquidator.onMoolahLiquidate checked profitability with an absolute balance comparison (loanToken.balanceOf(this) < repaidAssets), which never separates loan token this liquidation generated from loan token the contract already held from unrelated prior activity. An unprofitable flash liquidation could pass by drawing down that pre-existing reserve, silently depleting it while reporting success. Snapshot the loanToken balance before redeeming shares and swapping, then require the increase over that snapshot to cover repaidAssets, matching the delta-based check already used in Liquidator.sol and BrokerLiquidator.sol. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…on one-sided idle Two related collateral-accounting fixes on the V3 provider. 1) Deposit share issuance no longer routes through a pool-spot mint that is then re-valued at the fair price. Previously a subsequent deposit added liquidity at the pool spot ratio but derived shares from the fair-price re-decomposition of the minted liquidity; when spot and fair diverged and idle inventory was present, the depositor was over-credited shares and could withdraw pro-rata for a net gain at existing holders' expense. The subsequent-deposit path now issues shares purely proportional to the existing position composition (snapshotted at the fair price, inclusive of idle and collected fees) and parks the consumed tokens as idle inventory instead of minting into the pool. The pool spot price never enters share issuance, so the deposit is value-conserving and cannot dilute existing holders. Parked idle is deployed into the pool later by the permissionless compound() (keeper cadence) or the next deposit/withdraw, always spot-gated. As a consequence, a deposit must now match the (two-sided) fair composition; a one-sided deposit binds the composition fraction to the empty leg and reverts with ZeroShares. The first-deposit path is unchanged (it opens the position and has no existing holders to dilute). amount0Min/amount1Min are reinterpreted as the minimum amount of each leg consumed. 2) collectAndCompound now no-ops on zero addable liquidity. When idle inventory is one-sided relative to the active range, the computed liquidity is zero and the underlying pool mint reverts, which would brick deposit, withdraw, redeemShares and the permissionless compound(). The adapter now holds the inventory as idle and returns instead of reverting. Adds a creditIdle(amount0, amount1) adapter entrypoint (onlyProvider) for the park-to-idle deposit path, and relaxes the provider receive() to also accept native from the wrapped-native token (the vault unwraps a WBNB leftover before refunding it as native). Provider suite: 111 passing, including two new counter-tests covering both fixes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a caller-specified share-slippage floor to V3Provider.deposit: the call now takes a minShares argument and reverts with InsufficientShares when the minted shares fall below it. This protects a depositor from being silently under-credited — a first-depositor inflation attack that inflates the position via a direct NPM.increaseLiquidity donation, or the fair composition shifting between the off-chain preview and on-chain execution. Passing 0 disables the check. The IV3Provider interface and all deposit call sites are updated for the new parameter. Also adds two view helpers so front-ends can size the two deposit legs to the current fair composition (minimising the refund under the proportional deposit path): - getFairComposition() returns (T0, T1) valued at the fair price, and - previewDepositForToken0/previewDepositForToken1 return the matching amount of the other leg at that ratio. Provider suites pass (299 tests), including a new counter-test asserting the minShares floor reverts an under-crediting deposit and passes at an achievable floor. SlisBNBV3Provider runtime size is unchanged (~23.1KB, well within the 24KB limit). The operational first-deposit seed that complements this guard is documented in the launch runbook. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
positionAmountsAt only added the tokensOwed checkpointed at the position's last NPM poke (mint/increase/decrease/collect), ignoring fees earned in the pool since then. Because this view underpins every valuation — totalAssets, the provider-share oracle peek, Moolah health checks and liquidations, and slisBNBx balances — collateral was systematically under-valued between fee checkpoints, causing premature liquidations and over-burnt slisBNBx. positionAmountsAt now adds the live uncollected fees, simulated from the position's feeGrowthInside deltas exactly as Uniswap V3's Tick.getFeeGrowthInside and Position.update compute them (all fee-growth arithmetic wraps by design, in unchecked blocks). The pending amount uses the pool's current tick and cumulative fee growth — pool facts that only ever correct a stale under-count, not spot-price-manipulable for value extraction. IV3PoolMinimal is extended with feeGrowthGlobal0X128/1X128 and a ticks() reader that decodes only the first four (fixed-width) fields, staying width-agnostic across V3 flavors like the existing slot0 reader. A fork test cross-checks the simulated pending against the pool's ground truth: NPM.collect (which pokes the pool) returns checkpointed owed plus freshly earned, so collected minus owed equals the simulated pending to the wei. Two rebalance tests that had used the (now fee-inclusive) full position value as a decreaseLiquidity principal floor are corrected to size that floor from the liquidity principal at spot; the minAmount semantic is unchanged. All V3 suites pass. The change lands entirely in the adapter; the provider bytecode is unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…red amount removeLiquidity forwarded the caller's minAmount0/1 to decreaseLiquidity, whose slippage check covers only the principal burned from the pool. The pro-rata idle inventory is added afterward, so the amount actually delivered to the receiver is principal + fees + idle. Callers size minAmount from previewRemoveLiquidity, which reports principal + pro-rata idle, so a withdraw or redeem reverted with the pool's slippage error whenever idle was a meaningful share of the payout — and, on an idle-only or dust exit (liquidityToRemove rounds to 0), no floor was enforced at all. decreaseLiquidity is now called with zero mins and the floor is checked once against the total delivered amount (principal + fees + idle), reverting with InsufficientAmount. This matches what previewRemoveLiquidity reports, removes the spurious reverts, and covers the idle-only/dust path. A counter-test drives a one-sided-idle redeem (so the pre-removeLiquidity compound no-ops and the idle survives) with a preview-sized floor: it reverted before, passes now. All V3 suites pass (303 tests). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_twapTick computed the cumulative-tick delta and its division under Solidity 0.8 checked math, deviating from Uniswap's OracleLibrary.consult, which was compiled under <0.8 and relies on the int56 subtraction wrapping. A wrapped tickCumulatives delta would therefore revert here instead of producing the reference tick. Wrap the delta/division/modulo in an unchecked block to match the reference. Behavior is unchanged for all realistic observations (the wrap is astronomically far off); this only removes the spurious-revert deviation. All TWAP-using suites pass (153 tests). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
rebalance enforced its minLiquidity floor only against the newly-minted liquidity, which can be satisfied by liquidity minted at a price manipulated around the call (then sandwiched on the attacker's back-swap). rebalance now takes a targetSqrtPriceX96 — the pool price the backend built the rebalance against — and, after the swap and re-mint, requires the actual pool spot to be within maxSpotDeviationBps of it, reverting with SpotDeviationTooHigh otherwise. A pool moved off the BOT's expectation therefore reverts instead of minting off-market liquidity. targetSqrtPriceX96 == 0 opts out (parity with the other caller-supplied guards) and maxSpotDeviationBps == 0 disables the gate. The new argument threads through IV3DexAdapter.rebalance, the adapter, the provider _guardedRebalance wrapper, and the three provider BOT entrypoints (slisBNB / wbETH / wstETH). Generalizes the existing spot-vs-fair comparison into a reusable _priceWithinDeviation helper. Adds seven rebalance target-price tests (opt-out, gate-disabled, low/high-side, band boundary, sandwich-with-fair target, matching-target pass). All V3 suites pass (307 tests). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ap legs SwapInventoryLib.swap wrapped any native-balance increase after the venue call back into the wrapped-native token, and reverted (UnexpectedNative) when neither swap leg was the wrapped-native token. A venue that forwards its native balance (e.g. a router that sweeps stray native — seedable by an attacker with a 1-wei donation) could therefore brick an ERC-20<->ERC-20 conversion on a pair where neither token is the wrapped-native. The re-wrap now runs only when a swap leg IS the wrapped-native token; when neither is, any native that appears is an unsolicited donation outside this swap's accounting and is left untouched rather than reverting. The legitimate native-out (proceeds) and native-in (unspent refund) paths are unchanged, and the nativeIn input guard still uses UnexpectedNative. Live pairs (slisBNB/WBNB, wstETH/WETH) always include the wrapped-native leg, so this only affects future non-native pairs; a delegatecall-harness test exercises the neither-leg path (a stray-native donation no longer reverts). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ation - V3ProviderOracle.initialize now emits HaircutChanged(_haircutBps) so the initial haircut is observable from events alone. - setHaircutBps returns early when the value is unchanged, avoiding a redundant storage write and a misleading no-op HaircutChanged event. - SlisBNBV3DexAdapter: the constructor already pins TOKEN0 == slisBNB and TOKEN1 == WBNB, so the always-true _isSlisBnbWbnbPool guard and the dead branch in _poolPriceRate are removed; _lstNativeRate returns STAKE_MANAGER.convertSnBnbToBnb(1e18) directly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…compound hardening - Compound accrued fees before the Moolah health-check valuation in withdraw / withdrawShares / supplyShares, so the position is valued with fees folded in rather than transiently under-valued. - Fail closed on a zero oracle leg: V3Provider._amountsValueUsd and SlisBNBV3Provider.getUserBalanceInBnb now revert OracleZero when any returned price is 0 (prevents a transient-zero feed silently burning a user's reward balance toward 0). - transferFrom override now debits the granted allowance via _spendAllowance before moving shares, instead of transferring on an unbounded allowance. - Compounded event now reports the amounts actually consumed by increaseLiquidity (used0/used1), not the raw pre-leftover inputs. - Clamp the initial tick range to the usable [MIN_TICK, MAX_TICK] band after floor/ceil alignment. - Short-circuit a pure no-op recenter (range unchanged and no swap, target, or min floors supplied) to an in-place compound, avoiding an unnecessary burn/mint cycle; guarded / swap paths still run in full. Adds counter-tests for the allowance-checked transfer and the no-op recenter boundary; updates the recenter tests to assert compound-in-place. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
flashLiquidate hardcoded the amount0 / amount1 fields of the V3Liquidation event to 0. The real redeemed amounts are only known inside the onMoolahLiquidate callback (redeemShares' return values), which runs during Moolah's liquidate call, so they never reached the emit site. Capture the redeemed leg amounts in the callback and pass them back to flashLiquidate via appended (upgrade-safe) storage, zeroed before each liquidate so a non-redeeming flash liquidation still reports 0. Adds a counter-test asserting both event amounts are non-zero after a redeem-and-swap flash liquidation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…st floor - removeLiquidity reverts ZeroAmount when a share-burning redeem would deliver (0,0), instead of silently burning the caller's shares. - SwapInventoryLib.swap fails fast with InsufficientInventory when the requested amountIn exceeds the position's balance, rather than silently capping (which desynced the venue allowance from swapData and could underflow the totals). - V3ProviderOracle.peek floors a dust position's price to a non-zero unit instead of reverting, so a tiny position stays liquidatable (a reverting peek would block Moolah.liquidate and accrue bad debt); zero-leg-price still fails closed. Counter-tests added for each. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
previewRemoveLiquidity (and its previewRedeemUnderlying wrapper) computed spot principal + idle only, understating the withdrawal by uncollected fees. Pro-rate positionAmountsAt(spot) instead — it simulates pending fees + idle, matching removeLiquidity's actual spot-priced delivery. Kept at spot (not fair) so the preview tracks the real burn for minAmount sizing. Counter-test accrues real swap fees and asserts the preview includes them. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The branch's IStakeManager (and its mock) add deposit() that new master also declares — on the old base there was no clash, but rebased onto current master the two collide into a duplicate function definition that fails to compile. Remove the redundant declaration; a single deposit()/instantWithdraw() remains. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pull Request ReviewThis Solidity DeFi PR introduces a UUPS-based V3 collateral architecture comprising ERC-4626 provider vaults, concentrated-liquidity adapters, share-price oracles, and a dedicated liquidation contract for BSC and Ethereum LST/native pairs. It also adds rate-anchored valuation and rebalancing, native-token handling, loss and slippage controls, new dependencies, and extensive fork and regression tests. Sensitive ContentNo sensitive content detected. Security Issues🟠 [HIGH] Pre-existing native reserves can falsely satisfy flash-liquidation profitability
Generated by Hashdit Bot. This tool can absolutely NOT replace manual audits. |
Summary
A Lista V3 concentrated-liquidity collateral provider for the slisBNB/BNB pair, used as Moolah collateral. The original monolith exceeded EIP-170, so it is split into three UUPS contracts with clean boundaries, plus a dedicated liquidator:
market.oracle. Prices the share off the adapter's manipulation-resistant fair composition + the resilient oracle, with a capped haircut.Architecture (3-contract split)
V3Provider(vault)V3DexAdapter(strategy)SlisBNBV3ProviderOracle(oracle)The vault never touches the NFT/pool; the adapter never imports Moolah or the share token (it takes
(shares, totalShares)as opaque fractions); the oracle reads only public views. Raw-NAV vs haircut is cleanly separated (adapter returns raw, oracle applies the only haircut).Key Design Decisions
NotSlisBnbWbnbPair); the oracle additionally asserts its tokens match the adapter's (AdapterPairMismatch), so a mis-wired deploy can't silently misprice.convertSnBnbToBnb), not pool spot/TWAP —peek/totalAssets/getUserBalanceInBnbare invariant to pool-price manipulation. Per-leg USD comes from the resilient oracle;peekreverts on a zero leg price or zero total value (finding D);supply == 0returns 0(pre-market).
shares = addedValue × supplyBefore / totalValueBefore, valuing freshly-added liquidity at the fair price (first deposit denominated in the accounting asset, WBNB). Fees are compounded before minting so existing holders capture them before dilution.deposit/instantWithdraw, measured by balance delta — not pool-manipulable), guarded by a configurable rate-drift threshold (centerRateThresholdBps) +deadline+minLiquidity. BOT-gated on the vault;onlyProvideron the adapter._mint+supplyCollateral(CEI), so the native-BNB refund callback can't observe an inflated share price (staletotalSupplyvs. already-added NAV) and reenterMoolah.borrow. Withdraw/redeem likewise burn before the adapter pushesunderlying to the receiver.
idleToken0/idleToken1storage (notbalanceOf) so donations can't inflate NAV.msg.value(auto-wrapped to WBNB); withdrawals unwrap WBNB to native BNB.AccessControlEnumerableUpgradeableacross vault/adapter/oracle/liquidator (codebase standard; enumerable roles).IV3PoolMinimalslot0 read — decodes onlysqrtPriceX96/tick, width-agnostic tofeeProtocol(Uniswapuint8vs PancakeSwapuint32), so the adapter works against any V3 fork.lista-dao-contractslibs; interfaces from thelista-v3submodule.Files
src/provider/V3Provider.solsrc/provider/SlisBNBV3Provider.solsrc/provider/V3DexAdapter.solsrc/provider/SlisBNBV3DexAdapter.solsrc/provider/SlisBNBV3ProviderOracle.solmarket.oracle— share pricing off fair view + haircutsrc/provider/interfaces/*.solIV3Provider,IV3DexAdapter,ISlisBNBV3DexAdapter,IV3ProviderOracle,IV3PoolMinimal,IStakeManagersrc/provider/libraries/V3PositionLib.solsrc/provider/libraries/SlisBnbInventoryLib.solsrc/liquidator/V3Liquidator.soltest/provider/SlisBNBV3Provider.t.soltest/provider/SlisBNBV3ProviderRate.t.soltest/provider/V3ProviderReentrancyPoC.t.soltest/liquidator/V3Liquidator.t.solTest Plan
All suites fork BSC mainnet at block 60541406 (slisBNB/WBNB 1bp pool; the live StakeManager is etched with a faithful
instantWithdrawstand-in for the rebalance conversion).SlisBNBV3Provider.t.sol) — deposit/withdraw/redeem/supplyShares/withdrawShares, value-based share math, previews, oraclepeek/getTokenConfig+ finding-D, one-sided & slippage guards, transfer restrictions, slisBNBx integration, rebalance (incl. out-of-range via inventory conversion), borrow/repay/liquidate.SlisBNBV3ProviderRate.t.sol) —peek/totalAssets/getUserBalanceInBnbinvariant to pool manipulation; rate-centered rebalance + drift/deadline/minLiquidity guards. (8 tests)V3Liquidator.t.sol) — pre-funded + flash liquidation, share redemption, token/BNB swaps, whitelist/access control. (27 tests)V3ProviderReentrancyPoC.t.sol) —test_C1_depositRefundReentrancy_neutralizeddrives the full attack; fails pre-fix (≈26× inflation, insolvent), passes post-fix.