# ProgressiveCurve.sol Audit ## Findings ### 1. `previewWithdraw` reverts for valid inputs due to `_checkWithdraw` / domain ordering, plus `deduct > s²` reverts near full drains **Severity:** Low **Location:** `src/protocol/curves/ProgressiveCurve.sol::previewWithdraw` (lines ~116-135) **Root cause:** `deduct = assets / HALF_SLOPE` uses plain `div` (round-down in PRBMath? actually PRBMath's `div` rounds toward zero). When `assets` equals the full invariant area `HALF_SLOPE * s²`, floating-point rounding in `squareUp` used by deposit/mint means the stored `totalAssets` may slightly exceed the exact area of `HALF_SLOPE * s²`, so `divUp(assets, HALF_SLOPE)` > `square(s)` → `sub` underflows and reverts. The function is asymmetric with `previewMint` which uses `squareUp`/`mulUp` on the inverse path. **Impact:** A user redeeming the last (or near-last) shares via `previewWithdraw(assets=...)` can get a revert instead of correct shares, bricking the withdraw-exact-assets path at curve extremes. Deposit path uses `div` (round-down), withdraw path uses `divUp` (round-up) — the rounding direction in `previewWithdraw` is the root of the `sub` underflow. **Preconditions:** totalShares near MAX, or totalAssets inflated via repeated rounding-up mints such that `assets ≥ HALF_SLOPE * totalShares²` in UD60x18. **PoC sketch:** ``` // mint up to s, call previewMint to get assets_stored // set totalAssets = assets_stored (rounded up) // call previewWithdraw(assets_stored, totalAssets, s) // divUp(assets_stored, HALF_SLOPE) > square(s) → sub reverts ``` **Remediation:** Clamp `deduct = min(deduct, square(s))`, or use `div` (round-down) in `previewWithdraw` to match the deposit-side rounding and make the inverse consistent. Rounding direction should favor the protocol (round shares-out up when withdrawing assets), not revert. --- ### 2. `initialize` slope parity check is a footgun, not a safety measure **Severity:** Low **Location:** `initialize`, line `if (slope18 == 0 || slope18 % 2 != 0) revert` **Root cause:** Requiring `slope18 % 2 == 0` to compute `HALF_SLOPE = slope18/2` exactly is fine, but the subsequent math uses `HALF_SLOPE` as a UD60x18 in division (`div(wrap(assets), HALF_SLOPE)`). This is a fixed-point division, so the parity check is irrelevant for precision — it just makes common slopes like `1e18` (even) pass but `1e18+1` revert arbitrarily. Not exploitable, just confusing. **Impact:** Deployment-time misconfiguration risk only. **Remediation:** Either remove the parity check and compute `HALF_SLOPE` as a UD60x18 divison `div(SLOPE, wrap(2e18))`, or document the limitation. Purely cosmetic. --- ## No High/Critical Findings I did not identify any high or critical vulnerabilities exploitable under realistic preconditions. The curve math is standard (area under `y = slope * x` is `slope/2 * (s_next² - s²)`), uses PRBMath UD60x18 consistently, rounding directions for deposit/mint favor the protocol (mint uses `squareUp`+`mulUp`, deposit uses plain `div`/`sqrt` rounding down), and domain checks are inherited from `BaseCurve`. ## Slither False Positives - **incorrect-exp / incorrect-shift / divide-before-multiply / assembly in `FixedPointMathLib`** — these are well-known Solady idioms in hand-rolled assembly; not used by this contract (it uses PRBMath, not Solady's FPML for the curve math). False positive by transitivity through deps. - **pragma mismatch** — dependency hygiene; no runtime impact. - **cyclomatic-complexity / dead-code in prb-math** — library code, not relevant. - **assembly in `Initializable._getInitializableStorage`** — OpenZeppelin standard ERC-7201 pattern. ## High-confidence observations - `previewMint` correctly uses `squareUp` + `mulUp` so protocol over-charges on mint rounding. - `_convertToShares` uses plain `sqrt`/`sub`, rounding shares-out down — correct for deposit. - `_convertToAssets` uses plain `square`/`mul`, rounding assets-out down on redeem — correct (user loses dust, protocol safe). - `MAX_SHARES = sqrt(uMAX_UD60x18 / uUNIT)` bounds `square(s)` within UD60x18 range, preventing overflow in all area computations. - `currentPrice = totalShares * SLOPE` matches the linear price `p(s) = slope * s`; consistent with area formula. - Constructor disables initializers; `initialize` is `initializer`-gated — upgrade-safe against re-init. - Slope parity + `HALF_SLOPE = slope/2` preserves exact half even in raw uint, though see Finding 2. - `previewWithdraw`'s `sub(s, sqrt(inner))` is monotonically non-negative for `inner ≤ s²`, ensured when `deduct ≤ s²`.