# Aggregated findings — intuition_demo Reviewed 2 files via dual-LLM (Claude + Gemini) + Slither. Generated 2026-04-20 22:04. ## src/protocol/wallet/AtomWallet.sol ### Claude Opus 4.7 ## Findings ### 1. `validUntil` / `validAfter` are not covered by the signature — attacker can extend or bypass user-specified time windows - **Severity:** High - **Location:** `src/protocol/wallet/AtomWallet.sol :: _validateSignature` / `_extractValidUntilAndValidAfterFromSignature` (≈L247–361) - **Root cause:** The ECDSA signature is computed over `keccak256("\x19Ethereum Signed Message:\n32" || userOpHash)` only. The 12-byte `validUntil || validAfter` suffix appended to `userOp.signature` is parsed out *before* recovery and is **never authenticated**. `userOpHash` itself (as computed by the EntryPoint) is derived from `userOp` fields excluding `signature`, so flipping any bytes in the suffix does not change `userOpHash`. - **Impact:** - A user signs a `UserOp` with a restrictive `[validAfter, validUntil]` (e.g., expecting the txn to expire in 5 minutes). Anyone observing the user's signed op in the mempool / bundler pool can strip or mutate the 12-byte suffix (e.g., re-encode as a 65-byte signature, which the contract interprets as `validUntil=0 → no expiry`, or set `validUntil=type(uint48).max`) and re-submit the op through the EntryPoint at an arbitrary later time. The nonce still matches and the underlying ECDSA signature still verifies. - Consequence: a user's time-bounded intent (e.g., "only execute this swap if it lands within the next block") becomes executable indefinitely until the nonce is consumed. Attackers / bundlers can hold expired ops and execute them under favorable market conditions, causing direct loss of user funds from the AtomWallet. - **Preconditions:** Permissionless. Needs to observe a single time-bounded `UserOp` from a user whose nonce has not yet been consumed. - **PoC sketch:** ```solidity // user signs userOp where signature = r||s||v || validUntil(=now+60) || validAfter(=0) // attacker waits until now > validUntil, then: bytes memory fakeSig = abi.encodePacked(r, s, v); // drop the 12-byte suffix → 65-byte path → validUntil=0 userOp.signature = fakeSig; entryPoint.handleOps([userOp], payable(attacker)); // succeeds; same userOpHash, same recovered owner ``` - **Remediation:** Include the time-window bytes in the signed payload, e.g. sign `keccak256(abi.encodePacked(userOpHash, validUntil, validAfter))` (and apply the EIP-191 prefix to that). Then the EntryPoint-provided `userOpHash` still determines replay/nonce semantics while the time window is authenticated. --- ## Slither False Positives - **arbitrary-send-eth (`_call`, `_payPrefund`)**: This is an ERC-4337 smart-account `execute`; calling arbitrary targets with value is the contract's whole purpose, gated by `onlyOwnerOrEntryPoint`. `_payPrefund` sending to `msg.sender == entryPoint` is per the 4337 spec. - **calls-loop (`executeBatch`)**: Batch execution with per-call revert propagation is the intended semantics; DoS-in-loop concerns don't apply (caller is the owner/EntryPoint). - **assembly**: All usages are storage-slot reads (ERC-7201 / OZ namespaced storage) and a standard low-level bubble-up revert. Correct. - **pragma / solc-version**: Inherited from vendored dependencies; AtomWallet itself pins 0.8.29. - **dead-code (Helpers.sol)**: Upstream account-abstraction lib; out of scope. - **naming-convention / unused-state (`__gap`)**: `__gap` is storage padding for upgrade safety, deliberately unused. - **low-level-calls**: Required for generic `execute` and 4337 prefund. ## High-confidence observations - `owner()` override correctly falls back to `multiVault.getAtomWarden()` until `isClaimed`, and `acceptOwnership` permanently flips `isClaimed` → `_owner` persisted via `_transferOwnership`. - ECDSA uses `tryRecover`, which rejects high-`s` malleability; explicit handling of `InvalidSignatureLength` / `InvalidSignatureS` / `InvalidSignature` branches is correct. - `_packValidationData(true, ...)` on `InvalidSignature` returns failed validation (not revert), matching 4337 expectations so bundlers properly drop the op. - Namespaced-storage slot constants match OZ's ERC-7201 derivations for `Ownable` and `Ownable2Step`, so reading them via `_getAtomWalletOwnerStorage` / `_getAtomWalletPendingOwnerStorage` is consistent with the inherited contracts. - `receive()` + `addDeposit()` + `withdrawDepositTo` gated on `owner()||address(this)` — safe. - `nonReentrant` on `execute`/`executeBatch`/`claimAtomWalletDepositFees` prevents cross-function reentrancy into the wallet during an outgoing call. - 77-byte parser: `metaOffset = 65`, 12-byte meta packed as `uint96` → `validUntil` high 48 bits, `validAfter` low 48 bits — arithmetic is correct (separate from the authentication bug above). - `transferOwnership` override preserves two-step semantics; pre-claim, only the current atomWarden can initiate transfer (because `onlyOwner` resolves through the overridden `owner()`). ### Gemini 3 Pro An in-depth review of the provided `AtomWallet` contract did not uncover any specific, exploitable, non-trivial vulnerabilities. The contract securely implements the ERC-4337 `BaseAccount` pattern, correctly handling custom signature parsing, storage slots across upgrades, and transitioning ownership safely. Below are the items explicitly analyzed and confirmed to be secure, alongside the explanation of why the corresponding static analysis alerts are false positives. ## High-Confidence Observations * **Signature Time-Window Parsing:** The inline assembly block inside `_extractValidUntilAndValidAfterFromSignature()` properly accounts for memory layout. Because converting a calldata slice to memory will zero-pad the rest of the 32-byte word, reading `add(meta, 32)` captures the exact 12 bytes of validity bounds left-aligned, which the bitwise right-shift (`>> 160`) seamlessly drops into the correct position. * **Ownership Transition (Claiming):** The dual-state ownership check mapping initial control to `multiVault.getAtomWarden()` and later mapping it seamlessly to a specific `$._owner` upon `isClaimed` acts explicitly as designed. The custom overriding of `transferOwnership` and `acceptOwnership()` cleanly wraps the standard `Ownable2Step` logic without breaking internal standard references. * **ERC-7201 Storage Gap Implementation:** The locations defined for `AtomWalletOwnerStorageLocation` and `AtomWalletPendingOwnerStorageLocation` accurately reproduce the OpenZeppelin 5.x standards (`Ownable` and `Ownable2Step`). The getters point perfectly to the target namespace mapping, protecting against upgrade collisions. * **Execution Isolation:** The core methods `execute` and `executeBatch` correctly restrict interaction to `onlyOwnerOrEntryPoint`. They further enforce a rigid standard logic where malicious `_call` returndata bombing would safely remain confined to failing the specific UserOp under execution, as expected in ERC-4337 environments. ## Slither False Positives * **`arbitrary-send-eth` & `low-level-calls`:** * *Issue:* Slither triggers here because `_call(target, value, data)` is used internally to make generic transactions where both value and destination are dynamically provided via variables. * *Justification:* This is exactly the intended behavior of an ERC-4337 abstract account implementing a transaction proxy. Access is rigidly walled off behind the `onlyOwnerOrEntryPoint` modifier, preventing unauthenticated arbitrary value transmission. * **`calls-loop`:** * *Issue:* Fired due to external calls operating inside a `while / for` loop inside `executeBatch()`. * *Justification:* Batch execution natively mandates a loop over destinations. The length check explicitly binds array sizing for synchronicity, and only authorized parties govern input. If one call consumes excessive gas or halts execution, it appropriately behaves as an aggregated UserOp failure. * **`assembly`:** * *Issue:* Flags structural uses of `assembly {}`. * *Justification:* Safe usages; in this contract, assembly is explicitly constrained strictly to retrieving custom ERC-7201 deterministic storage variable locations (avoiding upgrade-collisions) and ensuring unpadded precise read/writes for ECDSA signature extraction formatting. * **`unused-state`:** * *Issue:* Slither reports the variable `__gap` inside `AtomWallet` is unused. * *Justification:* Typical smart contract upgradeability infrastructure. It exists to pad trailing storage dynamically on compilation, avoiding alignment offsets for potential logic changes in subsequent protocol upgrades. --- ## src/protocol/curves/ProgressiveCurve.sol ### Claude Opus 4.7 # 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²`. ### Gemini 3 Pro I have thoroughly analyzed the provided `ProgressiveCurve.sol` and its related mathematical logic. Based on the Code4rena/Secure3 severity rubric, I have **zero findings** to report. The contract implements the integral pricing model for a linearly progressive bonding curve correctly and safely. Below is a detailed breakdown of the checks performed, why the code is secure, and false positive justifications. --- ### What I Checked (Zero Findings Justification) 1. **Bonding Curve Math & Integral Accuracy:** The contract correctly computes pricing using the continuous integral of the linear curve $P = S \times \text{SLOPE}$. The total reserve area under the curve is mathematically $A(S) = \frac{1}{2} \cdot \text{SLOPE} \cdot S^2$. The code implements this precisely in `previewMint` and `_convertToAssets` as `(S_{next}^2 - S^2) * HALF_SLOPE`. 2. **EIP-4626 Rounding Symmetries:** The implementation properly applies conservative rounding favorable to the protocol, satisfying standard vault constraints and preventing round-trip arbitrages: * `previewMint` (user buys shares): Uses `PCMath.squareUp` and `PCMath.mulUp` to **round up** the asset cost to the user. * `_convertToAssets` (user redeems shares): Uses `PCMath.square` and `mul` (truncate/round down) to **round down** the assets yielded back to the user. * `previewWithdraw` (user specifies exact assets out): Uses `divUp` to **round up** the shares deducted from the user. * `_convertToShares` (user specifies exact assets in): Uses `div` (round down) and `sqrt` (round down) to **round down** the shares given to the user. 3. **Inflation / Donation Attacks:** The curve logic relies strictly on `totalShares` to compute state transitions and pricing. Although `totalAssets` is passed to the functions, it is completely ignored in the pricing math. This breaks classic ERC-4626 donation attacks (where attackers send raw assets to the vault to inflate share prices) because the progressive curve rigidly prices shares based on *supply*, not the collateral ratio. 4. **Overflows / Extreme Slopes:** The constructor initializes `MAX_SHARES` and limits operations bounded to `uMAX_UD60x18`. For any reasonable slope values (and implicitly bound up to roughly $10^{36}$), the total shares squared and maximum asset math safely fit within the `UD60x18` capacity constraints ($~1.15 \times 10^{77}$) avoiding implicit reverts. 5. **Div-by-Zero and Granularity:** The initializer correctly enforces `slope18 == 0 || slope18 % 2 != 0`, guaranteeing that `HALF_SLOPE` retains exact precision and that divide-by-zero is impossible. --- ### Slither False Positives The output provided by Slither relies completely on upstream mathematical library issues, none of which affect `ProgressiveCurve.sol` directly. * **`incorrect-exp` & `incorrect-shift`:** Slither flags the bitwise `^` (XOR) and bit-shifting syntax used inside `solady/FixedPointMathLib.sol`. Solady is highly aggressively optimized and correctly uses assembly bitwise operations (not exponentiation). These are well-known solady FPs. * **`divide-before-multiply`:** Slither complains about divisions mathematically ordered before multiplication in Solady's approximation loops (e.g. `expWad`/`lambertW0Wad`). These are carefully bounded numerical approximations where intermediate ordering prevents overflow, not rounding-error bugs. * **`assembly`:** Flags `solady` and `openzeppelin` components for using inline assembly, which is required for their gas optimizations. * **`pragma`:** Notice indicating Solady, PRB math, and `ProgressiveCurve.sol` use differing versions (`^0.8.4` vs `>=0.8.19` vs `0.8.29`). Because the protocol is evidently compiling on `0.8.29`, no deprecated bug surfaces from older compilers. * **`dead-code` & `cyclomatic-complexity`:** Assorted noise triggered by PRBMath having functions imported but not utilized by your individual contract, which has zero deployment gas impact. --- ### High-Confidence Observations * **Mathematical exactness on deltas:** Because Solidity limits operate without IEEE-754 floating precision truncation, large initial share magnitudes ($s$) combined with tiny share mints (e.g., `minShare=1e6`) calculate exact integrals via difference of squares correctly, losing zero significant digits on delta increments. * **Valid Restrictive Withdrawals:** `previewWithdraw` can analytically revert on exact maximum mathematical bounds of `square(s)` because of its application of `divUp(assets)`. This strict implicit bound is desirable: it reverts users if they ask for fractional wei more than the exact down-rounded valuation of their shares limits. Users must interface symmetrically via redeem in terminal cases. * **Initialization robustness:** Validating `slope18 % 2 == 0` is an unusually thorough, positive check indicating strong awareness of parity truncation risks in `wrap(slope18 / 2)`. ---