## Findings **No high-confidence, specific exploitable vulnerabilities were found in `OffsetProgressiveCurve.sol` on this review.** The rounding directions in the four user-facing quotes all favor the protocol: | Function | Quantity rounded | Direction | Effect | |---|---|---|---| | `_convertToShares` (deposit) | `sqrt(s² + assets/halfSlope) − s` | down (plain `div`, plain `sqrt`) | fewer shares minted | | `previewMint` (shares→assets) | `(sNext² − s²)·halfSlope` | up (`squareUp`, `mulUp`) | more assets charged | | `previewWithdraw` (assets→shares) | `s − sqrt(s² − assets/halfSlope)` | up (`divUp` on deduct, `sqrt` down) | more shares burned | | `_convertToAssets` (redeem) | `(s² − sNext²)·halfSlope` | down (`square`, `mul`) | fewer assets returned | Without the `PCMath` library source and `BaseCurve` bounds checks in scope, I cannot verify boundary invariants (e.g., whether `squareUp(sNext)` can overflow when `sNext` is near `sqrt(uMAX/uUNIT)` in `previewMint`, or whether `MAX_SHARES` truly precludes overflow after the `add(s, shares)`). That is the main residual risk area, but the checks in `_checkMintBounds`/`_checkCurveDomains` appear to intend to prevent it. ### Things checked and considered clean - `slope18 % 2 == 0` guard makes `HALF_SLOPE = slope/2` exact, so `2·HALF_SLOPE = SLOPE` everywhere (no off-by-one price drift). - `OFFSET` shift is applied symmetrically to both sides of every area computation (`s² − sNext²` cancels offset-only contributions), so introducing `OFFSET` does not create an extraction inconsistency between deposit/mint vs withdraw/redeem. - `MAX_SHARES = sqrt(uMAX/uUNIT) − OFFSET` ensures `(s+OFFSET)²` stays within `uMAX_UD60x18` domain for `square` if `square` is the `mulWad`-style squaring. - `initializer` modifier prevents re-init; constructor disables initializers on the implementation. - `currentPrice` returns `(totalShares + OFFSET)·SLOPE` (wad mul) — consistent with derivative of the integrated area. - `previewWithdraw` uses `divUp` for `deduct`, which is the correct direction to make `inner` smaller and thus shares-burned larger. ### Things NOT verifiable from this file alone (flag to co-auditor) 1. `PCMath.square` vs `PCMath.squareUp` vs `PCMath.mulUp` / `divUp` semantics — specifically whether `square` is non-wad (raw `s*s`) or wad (`mulWad(s,s)`). The math only stays dimensionally consistent if `square` is raw (no /1e18) and `mul(·, HALF_SLOPE)` does wad-scale. If `PCMath.square` quietly wad-scales but `PCMath.squareUp` does not (or vice-versa), `previewMint` vs `_convertToAssets` would diverge catastrophically. Worth diffing the library. 2. `_checkDepositBounds` / `_checkMintBounds` / `_checkWithdraw` / `_checkRedeem` in `BaseCurve` — whether they reject `shares == 0` / `assets == 0` inputs. If not, a dust deposit can round to 0 shares and `_checkDepositOut` should catch it; need to verify. 3. Whether `sub(s, sqrt(inner))` in `previewWithdraw` can underflow when `inner` rounding combined with `sqrt` rounding produces `sqrt(inner) > s`. Because `deduct` is rounded up and `inner = s² − deduct`, `inner ≤ s²` so `sqrt(inner) ≤ s` — this is safe. ✔ ## Slither False Positives - **incorrect-exp / incorrect-shift / divide-before-multiply / assembly in `FixedPointMathLib.*`** — All inside Solady's audited math library; the `^` and shift patterns are intentional bit-packed lookup tables and Newton iterations, not bugs. Not in scope and not reachable via OffsetProgressiveCurve in any way that weaponizes the pattern. - **pragma (4 different versions)** — OZ/PRB/Solady standard; benign. - **cyclomatic-complexity in `exp2`/`log10`** — PRB-Math internal; not called by this contract. - **dead-code in prb-math** — unused casting/helpers in a generic math library. - **assembly in `Initializable._getInitializableStorage`** — OZ-standard ERC-7201 storage slot access. ## High-confidence observations - Rounding direction is correct in all four conversion paths (table above). - `slope18 % 2 == 0` enforces exactness of `HALF_SLOPE·2 == SLOPE`. - OFFSET is symmetric in deposit↔withdraw math; `(s+OFFSET)² − (sNext+OFFSET)²` identity holds, so round-trip consistency is preserved relative to the non-offset curve. - `MAX_SHARES`/`MAX_ASSETS` are computed once at init from `uMAX_UD60x18/uUNIT` bound, and applied via `_checkCurveDomains` on every path. - `initializer` + `_disableInitializers()` in constructor is correct for UUPS-style implementation. - No external calls, no reentrancy surface; contract is pure math with storage reads. - `currentPrice` matches the instantaneous derivative of the assets integral (consistent with mint/redeem area formula).