← back to lessons

Modular Account V2 - LEARN-MODE lessons (Cycle 141 + Cycle 142C)

Target: Alchemy Modular Account V2 (alchemyplatform/modular-account v2.0.x) Audit chain: Spearbit 2024-01-31 (v1, 110+) + Quantstamp 2024-02-19 (v1 re-audit) + ChainLight 2024-12-03 (v2, 10) + Quantstamp 2024-12-11 (v2 re-audit, 23 total: 2H + 7L + 14I) Crowd review: Cantina contest 246de4d3-e138-4340-bdfc-fc4c95951491 - 306 findings submitted (live as of 2026-02-05; KYC required) Total findings (v2 era only): 33 (10 ChainLight + 23 Quantstamp final), Patched: 11, Acknowledged: 18, Open at Cantina: 4-ish unspecified Saturation score: 0.80 (LEARN-MODE-THEN-WALK verdict) Classes we now know: I4-via-deferred-action, I22-style selector-confusion-in-deferred-validation, deferred-validation-nonce-replay, NativeTokenLimit-bypass-class, allowlist-state-reset-class, validation-vs-execution-phase-parser-discrepancy class Classes we still don't know: SemiModularAccount7702 + EIP-7702 adversarial-delegator composition, cross-module emergent state (Allowlist + NativeTokenLimit + PaymasterGuard simultaneously), foreign-paymaster interactions, ERC-6900 v0.8.0 spec-vs-implementation drift on duplicate hooks Cycle 141 verdict: WALK CLEAN (Idan I10 cross-modality false-positive) Cycle 142C verdict: LEARN-MODE-THEN-WALK with lessons banked


Section 1 - Findings distillation

v2-era findings (the ones that apply to current code)

ID Severity Status Root cause class Attacker could
ALCHEMY-001 (ChainLight) High Patched I4 deferred-validation selector-check missing Call any native function (incl. installValidation) via execute() in deferred-validation scenario, bypassing the validation-module trust boundary
ALCHEMY-002 (ChainLight) High Patched I22 selector-extraction in executeUserOp Validation module pass through wrong selector to inner call, decoupling validated-intent from executed-intent
ALCHEMY-003 (ChainLight) Low Patched I4 malicious validation module via _validateUserOp Validation module forge return that allows bypass on conditional path
ALCHEMY-004 (ChainLight) Low Patched I4 deferred-action nonce conflict across validation modules Two different validation modules collide on the same deferredActionNonceUsed[nonce], allowing replay
ALCHEMY-005 (ChainLight) Informational Patched I4 deferred-validation install bypass Attacker constructs userOp that skips the install scope check
ALCHEMY-006 (ChainLight) Informational Acknowledged Execution-hook invariant on installValidation Execution hooks tied to installValidation can violate a higher-level invariant (accepted risk; documented)
ALCHEMY-007 (ChainLight) Informational Patched Missing invalidateDeferredValidationInstallNonce in selector list Stale nonces persist beyond intended TTL
ALCHEMY-008 (ChainLight) Informational Patched I4 duplicate replay protection in deferred action Redundant replay-protection layer that could mask future bugs
ALCHEMY-009 (ChainLight) Low Patched AllowlistModule state-reset on setAddressAllowlist setAddressAllowlist() always clears hasERC20SpendLimit flag - silently overwrites the operator-set spend limit when re-enabling an address
ALCHEMY-010 (ChainLight) Informational Patched Various minor suggestions (housekeeping)
ALC-1 (Quantstamp final) High Fixed Signature validation gating on isIModule Signature validation enabled when isIModule path active (parallels ALCHEMY-002 surface)
ALC-2 (Quantstamp final) High Fixed NativeTokenLimitModule bypass Bypass the per-account native-token spend cap via crafted path
ALC-3 (Quantstamp final) Low Fixed Missing function-selector check in isIModuleFunction() Sibling of ALCHEMY-001 surface (selector-not-checked anti-pattern)
ALC-4 (Quantstamp final) Low Fixed Incorrect masking of deadline Boundary-condition bug in time-bounded validity
ALC-11 (Quantstamp final) Low Fixed Misconfigured TimeRange DoS Wrong inputs to TimeRangeModule lead to permanent block
(10+ Informational Quantstamp findings) Inf Mostly fixed/ack Naming, NatSpec, view-fn classification Defence-in-depth notes

v1-era findings (legacy / mostly stale on v2)

Spearbit 2024-01-31 found 110+ findings on the SessionKeyPlugin architecture (v1). The v2 codebase replaced SessionKey with the validation-module + permission-hook architecture (ERC-6900 v0.8.0). Most v1 findings are stale:

The CLASS shape lives on (storage collisions, validator bypass, race conditions). The specific surfaces moved.


Section 2 - Methodology notes (auditor mental models)

Spearbit 2024-01-31 (v1)

Quantstamp 2024-02-19 (v1 re-audit)

ChainLight 2024-12-03 (v2)

Quantstamp 2024-12-11 (v2 final re-audit)

Cantina crowd review (open since 2026-02-05)


Section 3 - Negative space + accepted-risk rationale map (THE moat material)

The Cantina contest scope page lists explicit OOS items. For each, the auditor/sponsor rationale and Buddy's agreement:

OOS item Sponsor rationale (inferred) Accepted-risk? Buddy agreement
Known issues from previous audits Already fixed or accepted; re-finding adds no value YES AGREE (Rule 38 anti-pattern: re-submitting patched bugs)
Specific SMA7702 upgrade scenarios EIP-7702 has its own adversarial surface; isolating SMA7702 from EOA-takeover is treated as a wider EIP-7702 concern, not modular-account-specific YES PARTIAL DISAGREE - SMA7702 + adversarial EIP-7702 delegator is an under-explored composition. This is a Cycle 143 candidate (Section 5).
Deferred-action replacement by bundlers Bundlers can drop or reorder userOps; the ERC-4337 standard places this responsibility on bundler operators, not on the account YES AGREE - if you trust the bundler, you accept this. The interesting question is "what happens when a bundler is adversarial" but that's an ERC-4337-wide question.
User errors Operator-error class is broad and not the account's job to defend YES AGREE in principle but the boundary is fuzzy. "User error" can hide design-time misuse-of-API bugs (Spearbit 5.2.3 was a "prevent createAccount user error" finding that was fixed). Pay attention to the line between "user could shoot themselves in the foot" and "we shipped a footgun."
Owner misconduct Owner-as-trust-root is the entire premise of an SCA. If the owner key is compromised, all bets are off YES AGREE. Adjacent: "what if owner is half-honest" (e.g. owner installs malicious module then "rents out" the account)? Not addressed.
EntryPoint issues EntryPoint is the ERC-4337 standard; not the account's code YES AGREE. The EntryPoint is heavily audited separately + multi-vendor.
Cryptographic attacks The signature schemes (ECDSA, P-256 via WebAuthn) are out-of-scope cryptographic primitives YES AGREE for raw primitives. DISAGREE on the boundary - signature MALLEABILITY + replay protection are I22-shaped vulns at the protocol layer, not the crypto layer, and DO live in scope.
Protocol design choices Some choices (per-account vs per-module spend limits, etc.) are design-time tradeoffs, not bugs YES AGREE in principle but "design choice" is the most-abused OOS bucket. ChainLight ALCHEMY-006 (acknowledged) is a documented choice; check the Cantina submission history for any disputed "design choice" rejections.
Rounding errors The account does not handle financial math directly; module errors are module-specific YES AGREE for the account; DISAGREE for AllowlistModule's ERC20 spend tracking (rounding could compound).
Gas consumption concerns Gas griefing is broad + accepted-by-construction for permissionless mempool YES AGREE for griefing. DISAGREE for "gas consumption that triggers a state inconsistency" (storage-write-then-revert class).

The most valuable single accepted-risk: "execution hooks can violate invariants" (ALCHEMY-006, Acknowledged)

This is the most interesting Acknowledged finding because it documents a deliberate trust assumption: an execution hook on installValidation can violate an account-wide invariant, and the team accepted that. The rationale (likely): the operator is responsible for which hooks they install. If they install a malicious hook, that's owner-misconduct (which is OOS).

This is the moat surface: a sophisticated attacker who can social-engineer a user into installing a "harmless-looking" hook module can then violate an invariant the user assumed was protocol-enforced. The audit accepted this risk; the team accepted it; but a user-facing Pejji-like consumer of Modular Account V2 could ship a wrapper that DOES enforce these invariants. That's product surface, not protocol bug surface.


Section 4 - Pattern library diff (bidirectional)

Classes NEW to Buddy from this audit

Class Source Buddy Skill candidate
Deferred-validation selector-check missing (ALCHEMY-001 / ALC-3) ChainLight New Skill /deferred-action-selector-check - grep every _validateUserOp path for selector verification before execute() invocation.
Deferred-action nonce-conflict across validation modules (ALCHEMY-004) ChainLight New Skill /cross-module-nonce-conflict-check - audit nonce-namespace partitioning between independent permission/validation modules.
AllowlistModule state-reset class (ALCHEMY-009) ChainLight Extension of /audit-documented-safety-net-check Skill - "every setter that re-enables a flag must preserve, not reset, the orthogonal flags."
NativeTokenLimit bypass (ALC-2) Quantstamp final Extension of /deep-invariant-analysis Skill - "per-account spend-cap enforcement under cross-module composition."
Signature-validation gated on isIModule (ALC-1) Quantstamp final New Skill /path-gated-signature-validation-check - when signature-validation is enabled conditionally, verify the condition cannot be attacker-influenced.

Classes Buddy has that this audit MISSED (moat signal)

Class Buddy Skill What was missed
Cross-bridge reconciliation primitives /cross-bridge-reconciliation-primitives Not applicable here (no bridge surface), but the meta-pattern of "two state-transition paths must reconcile" applies to the deferred-validation install path vs the regular install path. Audit caught the regular path; deferred path took 4 findings (001, 004, 005, 007, 008) before settling.
NatSpec implementation drift /natspec-implementation-drift-check Spearbit 5.5.34 was "Missing or Incomplete NatSpec" as Informational. Buddy's Skill would have flagged the same items + cross-checked NatSpec claims against implementation.
Skeptic Gate 7 audit-coverage-clean (Gate 5) /skeptic-gate-7-automator The audit chain density score (4 audits + 306 findings) is now MECHANICALLY computable via Cycle 142B.1 Skill. Pre-Cycle-142B Buddy would have to manually re-derive this on every encounter.
Saturation-hunt mandate (Phase 1.5.8) CLAUDE.md doctrine Applied to vulnerability-saturation hunt; the audit-saturation analog is now /audit-saturation-pre-check-and-learn. Two complementary scopes.

The moat signal: Buddy's pattern library catches NatSpec drift + cross-bridge-style two-path reconciliation + audit-saturation explicitly. The audit chain caught the specific instances but did not produce reusable patterns for downstream consumers. Buddy's value-add is the meta-pattern, not the specific find.

Bidirectional summary


Section 5 - What we still don't know

Honest list of surfaces NOT covered at depth by the audit chain:

Surfaces under-covered

  1. SemiModularAccount7702 + adversarial EIP-7702 delegator. Cantina OOS-listed this explicitly. The audit chain did not probe it. Open question: if an EIP-7702 EOA installs a Pejji-pattern SMA7702 delegation and then the EOA's private key is rotated mid-validation, what state is the SMA in? Buddy hypothesis: there are race conditions in the delegation-state vs validation-module-state. UNTESTED.

  2. AllowlistModule + NativeTokenLimitModule + PaymasterGuardModule simultaneous. Each module audited individually. The audit chain did not test all 3 active simultaneously with adversarial userOps. The accepted-risk note on ALCHEMY-006 (hooks can violate invariants) suggests cross-module interaction is a known soft area.

  3. Foreign paymaster + PaymasterGuardModule. The PaymasterGuard enforces a specific paymaster; what happens if the foreign paymaster is itself a contract that re-enters the account during validatePaymasterUserOp? Untested.

Threat models the audit did NOT consider

  1. Operator-vs-module collusion. Audits assume operator = trusted, modules = untrusted. What about operator + 1 malicious module colluding against another module? E.g., operator installs a "harmless" hook + a sketchy validation module; the hook drains state the validation module would have caught.

  2. Long-lived deferred actions. The deferred-validation path has nonce-based replay protection but no per-action expiry by default. A signed deferred-action sitting in a bundler queue for hours/days is structurally valid - any state change in the interim is irrelevant to the deferred-action's already-signed envelope. Surface for value-extraction via timing.

  3. Cross-account composition. ModularAccount V2 is one account. What about 2 ModularAccounts owned by the same operator, with one calling the other via execute() + the receiving account having validation hooks that don't expect the sender to be a sibling account? The audit chain treats one account in isolation.

Compositions the audit did not test

  1. AllowlistModule entityId reuse across migrations. If a user uninstalls then reinstalls AllowlistModule with the same entityId, does the residual state from the old install correctly clear? ALCHEMY-009 was the analog finding (state reset on re-enable) - the broader "lifecycle invariant under uninstall+reinstall" wasn't fully tested.

  2. Signature scheme upgrade. SingleSigner -> WebAuthn migration: can a userOp signed under the old scheme be replayed after the migration to WebAuthn? Audit chain treats each scheme in isolation.

  3. EIP-1271 contract-signer + Module collision. A contract signer (via EIP-1271 on SingleSigner) + a validation module installed for the same entityId - which path takes precedence? Audit chain doesn't surface a clear answer.


Closing: 5 concrete future-hunt candidates for Cycle 143+

Each is a HYPOTHESIS (no PoC), with the surface anchor.

Candidate 1 - SMA7702 + EIP-7702 key rotation race

Candidate 2 - Long-lived deferred-action vs state-mutation in interim

Candidate 3 - AllowlistModule + NativeTokenLimit triple-composition

Candidate 4 - EIP-1271 contract-signer + module validation collision

Candidate 5 - Pejji wrapper-layer enforcement on accepted-risk surfaces


Sign-off

This is the first concrete LEARN-MODE pass. Future LEARN-MODE-THEN-WALK verdicts add to this library at ~/bounty/lessons-library/. Over 90 days, this becomes the BlessedOps moat: accumulated knowledge of what auditors found, what they didn't, what is accepted business risk and why, and what mental models they used. A copycat with the same tooling cannot replicate years of compounded learning.

Babakizo


Cycle 143 hunt-mode results - what we now know that we didn't

Cycle: 143 (Day 40, 2026-05-18 17:54 UTC fire) Verdict: WALK CLEAN with banked Foundry fuzz negative results Time on target: ~2h wall-clock (cap 6-8h, walked early at Phase 4 boundary) LLM spend: $0 (no LLM-required steps) Test file: test/poc/TripleCompositionInvariantTest.t.sol in the cloned repo Forge command: forge test --match-path "test/poc/TripleCompositionInvariantTest.t.sol" -vv --fuzz-runs 5000

What we now know (from running the fuzz)

  1. NativeTokenLimit.preExecutionHook value-accounting is robust under executeBatch fuzz. 5000 random (value1, value2) pairs across executeBatch([{value1, ...}, {value2, ...}]): every case where value1 + value2 > NATIVE_LIMIT reverted with ExceededNativeTokenLimit; every case where value1 + value2 <= NATIVE_LIMIT succeeded with exact limit - (value1+value2) decrement. No off-by-one, no overflow path, no batch-iteration accumulation bug. The decrement happens AT-ONCE on the sum, not per-iteration. Confirmed via nativeLimit.limits(H_ENT, address(account1)) post-hook state check.

  2. PaymasterGuard.preUserOpValidationHook rejects every disallowed paymaster. 5000 random non-allowed paymaster addresses (with vm.assume(attackerPaymaster != allowedPaymaster && != address(0))): every call reverts with BadPaymasterSpecified. The only paymaster-bypass that exists is the by-design "no paymaster specified" case, which the audit chain documented as intentional.

  3. executeUserOp prefix parser consistency holds structurally. The _validationPhaseGetSelectorAndCalldata and _executionPhaseGetSelectorAndCalldata parsers in ModuleBase.sol extract the same inner selector + inner calldata from the same wrapped input. ChainLight ALCHEMY-002 already audited + patched this surface; this cycle's fuzz exercises the patched code and confirms structural consistency under fuzzed (value, target) inputs.

What we now know (from reading the code carefully)

  1. AllowlistModule has a TWO-PHASE enforcement split: validation-phase target+selector check; execution-phase ERC20 spend decrement. This is documented in the source but not called out as a class in any audit report. The validation hook (preUserOpValidationHook) calls checkAllowlistCalldata which gates target+selector. The execution hook (preExecutionHook) ONLY calls _decrementLimitIfApplies; it does NOT re-check the target+selector allowlist. The implicit invariant: validation phase guarantees execution phase will only see allowlisted targets. If a userOp could skip validation but still trigger execution, the execution hook would silently decrement limits without gating. In ERC-4337 v0.7 this should be impossible (validation gates whether execution proceeds), but the implicit dependency is a class shape worth banking.

  2. AllowlistModule.preExecutionHook reverts on ANY non-execute/executeBatch selector (SpendingRequestNotAllowed). This is the strict gatekeeper. The brief's hypothesis #1 ("allowlist update mid-userOp could change the address the NativeTokenLimit cap applies to") is structurally blocked because: (a) the Allowlist preExecutionHook rejects native function selectors entirely (no installValidation via this validation), (b) the NativeTokenLimit cap is per-entityId not per-target, so target changes don't shift caps.

  3. NativeTokenLimit.preExecutionHook silently skips non-execute/executeBatch/performCreate selectors (no revert, value stays at 0, limit unchanged). This is asymmetric to Allowlist's strict gatekeeper. Composed together, Allowlist's strict revert dominates - any non-execute selector reverts before NativeTokenLimit's silent-skip is reached. The asymmetry only matters in a configuration where NativeTokenLimit is installed WITHOUT Allowlist. In that config, native-function calls via the validated path would bypass any cap (since NativeTokenLimit doesn't see value for non-execute selectors). Probably-not-a-finding (operator chose that config) but worth banking as a config-warning class.

What we still don't know after this cycle

Future-hunt candidate status update

Per the lessons-file closing list:

Banked rules from Cycle 143

  1. Foundry-first hunt cycles produce structured negative knowledge when WALK happens. Even a clean walk produces "we now know this surface holds under N fuzz runs" - banked as a coverage record. Future hunts on adjacent classes can reference this corpus.

  2. _validationPhaseGetSelectorAndCalldata and _executionPhaseGetSelectorAndCalldata parser consistency is the load-bearing assertion across multiple modules. Any future audit cycle on a Modular-Account-derived account should re-fuzz these parsers as the FIRST step (cheaper than full triple-composition setup).

  3. Module-pair asymmetry (strict-gate vs silent-skip on non-execute selectors) creates config-dependent risk. Operators who install NativeTokenLimit without Allowlist may not realize native-function calls via that validation bypass the cap silently. Class for the audit-coverage-check Skill: "any permission hook that silently passes on unknown selectors must be paired with a strict-gate hook to enforce the closed-world assumption."


Cycle 144 hunt-mode results - SMA7702 race surface

Cycle: 144 (Day 40, 2026-05-18 18:37 UTC fire, sequential with Cycle 145) Verdict: WALK CLEAN with banked structural-defense confirmations Time on target: ~1.5h wall-clock (cap 4-6h, walked early - hypotheses were structurally bounded) LLM spend: $0 Test file: test/poc/SMA7702RaceTest.t.sol (5 tests, 5000 fuzz runs each on the parameterized one)

Hypotheses tested + outcomes

Brief Hypothesis 1 (key-rotation race): STRUCTURALLY BLOCKED. EIP-7702 delegations are FIXED for a single transaction at the consensus layer. address(this).code cannot change mid-transaction. Validation and execution within one userOp happen in one bundler transaction per ERC-4337. So delegate-flip between validation and execution within a userOp is impossible by construction. The cross-transaction case (EOA flips delegate after bundler has signed userOp but before bundler executes it) is by EIP-7702 design - the EOA owner is responsible for their delegations. Owner misconduct is OOS per Cantina contest.

Empirical confirmation: test_DelegateFlipBetweenValidationAndExecution_StructurallyBlocked verifies (a) getFallbackSignerData returns the EOA at T1, (b) after vm.etch(eoa, hex"") the EOA has empty code, (c) any account-shape call against an empty-code EOA cannot dispatch. PASS.

Brief Hypothesis 2 (concurrent delegation collision): STRUCTURALLY BLOCKED via FallbackValidationInstallationNotAllowed (a v2 ALCHEMY-001 patched class). The fallback validation lookup key is unique by design (entityId 0 + reserved namespace). Other validations have their own entityIds. SemiModularAccountBase.installValidation explicitly reverts on validationConfig.entityId() == FALLBACK_VALIDATION_ID && validationConfig.module() != address(0).

Empirical: test_FallbackValidationIdReinstallBlocked tries to install a SingleSignerValidationModule with entityId=0 + module != 0 via EntryPoint. Reverts as expected. PASS.

Brief Hypothesis 3 (nonce decoupling replay): STRUCTURALLY BLOCKED via signature-hash binding. The 3 nonce systems (EIP-7702 authorization nonce, ERC-4337 EP nonce, ERC-6900 deferred-validation nonce) live in separate namespaces. ERC-4337 signatures are over userOp hashes that include the EP nonce - changing the EP nonce changes the hash and invalidates the signature.

Empirical: test_NonceSystemsDoNotShareSignedHashSpace signs hash A, then verifies the signature does NOT validate against a different hash B. PASS.

State-consistency fuzz (Hypothesis 0): PASS at 5000 random (newSigner, isDisabled) pairs. updateFallbackSignerData correctly stores both fields; getFallbackSignerData returns the resolved signer (storage if non-zero, else address(this) per SMA7702 override) + the disabled flag.

Things we now know about SMA7702 + EIP-7702 composition

  1. EIP-7702 + ERC-4337 + SMA7702 composition is robust by structural design choices. The race surfaces described in the brief (key rotation mid-flight, validation collision, nonce replay) require properties that EIP-7702 + V2's lookup-key uniqueness + ECDSA signature-hash binding all preclude.

  2. The fallback validation reinstall guard (FallbackValidationInstallationNotAllowed) is THE structural defense for hypothesis 2. Without this guard, an operator could install a module at entityId 0 and confuse the validation routing. ChainLight ALCHEMY-001's general class lives here as well.

  3. msg.sender == address(this) bypass in _checkPermittedCallerAndAssociatedHooks is what lets the EIP-7702-delegated EOA self-administer. When the EOA calls the account directly (msg.sender = EOA = address(this) under EIP-7702), validation hooks are SKIPPED. This is by design for SMA7702's "EOA is the implicit owner" model. It DOES mean any hooks installed for the EOA's own validation (e.g. Allowlist, NativeTokenLimit) DO NOT FIRE on the EOA's direct calls to native functions. This is an accepted product-trust assumption.

  4. vm.etch can simulate EIP-7702 delegation for testing without needing forge-std EIP-7702 cheatcodes. Forge-std submodule at this commit (pre-EIP-7702 cheatcodes) - but vm.etch(eoa, contract_code) achieves the same code-at-EOA-address effect for unit testing the contract paths. The signing primitives (vm.sign(eoaKey, hash)) are independent.

Things we still don't know after this cycle

Future-hunt candidate status update

Banked rules from Cycle 144

  1. Structural-defense confirmation cycles produce coverage data on saturated targets even when zero bugs land. The Cantina contest accepts WALK outcomes; the banked lessons (3 hypotheses x structural-block reason + 4 things-we-now-know) are real institutional knowledge that compounds.

  2. EIP-7702 + ERC-4337 + ERC-6900 composition has multiple anti-collision designs that auditors named but didn't enumerate as a composition class. Reusable methodology: when auditing a triple-spec contract (3+ standards composed), the highest-EV fuzz target is the BOUNDARIES between the specs, not the individual spec compliance. The boundaries here (delegation-fixity, lookup-key uniqueness, signature-hash binding) all hold.

  3. vm.etch is the lightweight substitute for EIP-7702 cheatcodes when fuzzing the contract-layer behavior. Forge-std versions before EIP-7702 cheatcodes (pre-2025-late) cannot natively signDelegation/attachDelegation, but vm.etch achieves equivalent code-at-address effects for the contract paths.


Cycle 145 hunt-mode results - executeBatch self-call to installValidation

Cycle: 145 (Day 40 sequential pair with 144, draining Candidate 3 sub-surface) Date: 2026-05-18 19:15-19:55 UTC Time on target: ~40 min wall-clock (cap 4-6h, walked early - structural answers fell out of source re-read) Verdict: WALK CLEAN with 3 Foundry tests confirming the sub-candidate is structurally bounded

What we now know

3 Foundry tests, 3/3 PASS. The Cycle 143-banked sub-candidate "executeBatch self-call to installValidation" is structurally bounded by V2's per-userOp validation-context binding + AllowlistModule's two-phase enforcement + per-module storage namespace isolation.

  1. Module B installed via batched self-call does NOT retroactively authorize the current userOp. The validation that approved the current userOp is Module A's (the one signed for in userOp.signature). Module B becomes queryable only on subsequent userOps. The current tx's authorization is signed-hash-bound to Module A. No immediate escalation.

  2. Sequential batch ordering of nested installValidation calls works as expected with no state pollution. Two SingleSignerValidationModule instances installed in one batch (different entityIds, different signers) each record their own signer correctly. Storage is keyed by (entityId, address(this)) so the two installs don't collide.

  3. AllowlistModule state-reset via setAddressAllowlist is INDEPENDENT of installValidation. ALCHEMY-009's patched setAddressAllowlist now takes hasERC20SpendLimit as an explicit parameter (no longer hard-coded false). A batched [setAddressAllowlist (reset), installValidation (install Module B)] correctly resets the flag on Module A's allowlist AND installs Module B - the two state changes don't cross-contaminate because Module A and Module B have separate storage namespaces (keyed by entityId).

What we now know from source re-read

What we still don't know

Future-hunt candidate status update

Banked rules from Cycle 145

  1. Validation-phase authorization is signed-hash-bound to the current validation module. Mid-tx installation of a new validation module does NOT retroactively authorize the current tx. This is a strong invariant of ERC-4337 + ERC-6900 composition: the userOp.signature is verified against a fixed validation context determined at userOp construction time. New validations installed during the userOp's execution are inert for the current tx.

  2. Per-module storage namespacing (keyed by entityId + msg.sender) prevents cross-module state bleed during batched installs. A general property of ERC-6900 module design - each module owns its own storage namespace. Tests should explicitly verify this for any "compose two state-mutating modules in one batch" hypothesis.

  3. Mid-batch source re-read is sometimes faster than fuzz. When the brief hypotheses fall out structurally from source on second reading, walk in 40 min not 4 hours. Cycle 144 walked in 1.5h, Cycle 145 in 40 min. The diminishing-returns boundary on a saturated audit chain is real - once you've read the source 3-4 times across cycles, the structural answers come fast.


Cycle 147 hunt-mode results - long-lived deferred-action race surface

Cycle: 147 (Day 40 sequential pair-mate with 146; draining Candidate 2 from Cycle 142C LEARN-MODE pass) Date: 2026-05-18 19:08-19:55 UTC Time on target: ~45 min wall-clock (cap 4-6h, walked early - structural answers from source re-read) Verdict: WALK CLEAN with 4 Foundry tests confirming the surface is structurally bounded

What we now know

4 Foundry tests, 4/4 PASS (6 actual test executions counting withSMATest double-runs on 2 of them). Cycle 142C-banked Candidate 2 "long-lived deferred-action timing race" is structurally bounded by: - Signature-hash-binding (the EIP-712 typedDataHash includes nonce + deadline + selfCallHash + chainId + verifyingContract; NOT signer-at-sign-time or module-version-at-sign-time) - Current-signer-resolution-at-validation-time (SingleSignerValidationModule's validateSignature reads signers[entityId][account] AT SUBMISSION TIME; rotation breaks the sig) - EntryPoint nonce-as-single-revocation-mechanism (the userOp nonce IS the deferred-action nonce; burning via incrementNonce blocks resubmission with AA25) - Storage-tied lookup-key resolution (the lookupKey only encodes entityId; the module address is read from validationStorage[lookupKey].module at submission, so module-swap-at-same-entityId resolves to the NEW module's signer mapping)

  1. Long-lived deadline alone does not decay the signature. A 30-day deadline + vm.warp 25 days still validates cleanly. Confirms there's no time-based silent expiry separate from the explicit deadline parameter.

  2. Signer rotation (via SMA fallback updateFallbackSignerData OR via SSVM transferSigner) immediately breaks pending deferred-action signatures. The sig was bound to alice's recovery; the current signer is now bob; ECDSA recovery fails → DeferredActionSignatureInvalid → AA23 revert. By design.

  3. Nonce-burning via EntryPoint.incrementNonce (after a 28-day warp) reverts the deferred action at the EP layer with "AA25 invalid account nonce". Confirms that the long-lived window does NOT decouple the deferred-action's nonce from the EP's nonce-tracking - they're the same slot. Single source of truth for revocation.

  4. Signer-rotation-then-rotation-BACK within the deadline window allows the original signature to validate normally. This is the by-design EIP-712 semantics: signatures are hash-bound to (nonce, deadline, call), not to module-state-at-sign-time. The deadline is the ONLY time-based revocation; nonce-burn is the ONLY state-based revocation. If a user wants to PERMANENTLY revoke a signed deferred action, they must burn the nonce, not just rotate the signer.

What we now know from source re-read

What we still don't know

Future-hunt candidate status update

After Cycle 147, the remaining MA V2 surface is LOWER-EV (sub-sub-surfaces from prior cycles, edge-case timing). Strong signal that the codebase is well-defended structurally on the major surfaces.

Banked rules from Cycle 147

  1. Signature-hash-binding + nonce-as-single-revocation is the ERC-4337 + EIP-712 design contract. Signers can rotate; lookups are at submission time. The ONLY ways to revoke a signed deferred action are: (a) burn the nonce, (b) wait past deadline, (c) rotate the signer permanently (deadline-bounded protection). Worth banking as a positive case-study; the rotation-then-back replay is an EXPECTED behavior, not a bug.

  2. Storage-tied lookup-key resolution means module-swap is a same-entityId-only operation. Two modules cannot coexist at the same entityId; one must be uninstalled first. This bounds the H1-style "module-swap mid-flight" hypothesis to the EQUIVALENCE class of "uninstall the validation, then sign-after-uninstall fails" (which is already structurally clear).

  3. Long-lived-deadline tests can use vm.warp + EP-direct-submission to compress the test runtime. No need for actual real-time delays. The deferred action's hash-bound semantics are time-invariant within the deadline window; vm.warp 28 days behaves identically to actual elapsed time. Bank as the standard idiom for timing-race tests on this codebase.


Cycle 152 hunt-mode results - sub-surface drain (Surfaces A / B / C)

Cycle: 152 (Day 40 sequential after 151, draining 3 highest-EV banked sub-sub-surfaces) Date: 2026-05-18 21:11-21:30 UTC Time on target: ~19 min wall-clock (cap 2-3h, walked early thanks to 6th-cycle compound knowledge effect) Verdict: WALK CLEAN with 7 Foundry tests, 7/7 PASS, 0 findings, 3 sub-sub-surfaces marked EXHAUSTED

What we now know - Surface A (contract-as-EIP-7702-delegate 1271 recursion)

2 Foundry tests, 2/2 PASS. The Cycle 144 banked sub-surface "contract-as-EIP-7702-delegate infinite recursion in _exec1271Validation" is structurally bounded by the validation-locator-prefix requirement on the outer isValidSignature.

  1. Recursion bottoms at depth 1, not at the EVM call-depth limit (1024) or gas exhaustion. When SignatureChecker.isValidERC1271SignatureNow does signer.staticcall(isValidSignature(hash, sig)) and the signer is the same SMA7702 (delegated EOA), the inner call reverts with ValidationSignatureSegmentMissing() because the raw 65-byte sig payload passed by SignatureChecker has no validation-locator prefix. The inner staticcall returns success=false, SignatureChecker returns false, outer call returns _1271_INVALID (0xffffffff). Termination is graceful at depth 1.

  2. Nested CONTRACT_OWNER payloads (sig payload itself starts with SignatureType.CONTRACT_OWNER=1) cannot deepen the recursion either. The same termination mechanism fires at depth 1 because each layer's parser demands prefix encoding the previous layer can't provide.

What we now know - Surface B (hook-reentrancy via installValidation.selector executionHook)

2 Foundry tests, 2/2 PASS. The Cycle 145 banked sub-sub-surface "hook-reentrancy via installValidation.selector" is structurally bounded by the DIRECT-CALL VALIDATION GATE in _checkPermittedCallerAndAssociatedHooks, NOT by SelfCallRecursionDepthExceeded.

  1. When the hook module re-enters via account.installValidation(...), msg.sender = hook module address (not EntryPoint, not address(this)). This routes into the direct-call branch of _checkPermittedCallerAndAssociatedHooks. The hook has no directCallValidation registered, so _checkIfValidationAppliesCallData reverts with ValidationFunctionMissing(installValidation.selector).

  2. The revert is a TYPED error, not a generic require. Selector captured: 0xcf7b49f6 = ValidationFunctionMissing.selector. Argument: 0x1bbf564c = installValidation.selector. Auditor-grade error signaling.

  3. The outer install completes successfully despite the inner revert. The hook's re-entrant call uses low-level .call() (catches the revert in returndata). The outer wrapNativeFunction frame continues, completes the original install. So a malicious hook can WASTE GAS but cannot ESCALATE state.

What we now know - Surface C (EIP-1271 + Module collision, Candidate 4 untouched through 5 cycles)

3 Foundry tests, 3/3 PASS. The Cycle 142C banked "EIP-1271 + Module collision" hypothesis was MOOT because there is NO module iteration.

  1. The signature blob's first 5 bytes (for non-direct-call) are the ValidationLocator: 1 byte options + 4 bytes entityId. loadFromSignature decodes this and routes to ONE specific module. The sig submitter chooses which module. There is no "first wins" or "iterate all modules" logic.

  2. Cross-locator attacks are impossible by design. Sig signed for ENTITY_1's signer, with locator pointing to ENTITY_2, results in ENTITY_2's module checking against ENTITY_2's signer, which fails ECDSA recovery, returns INVALID.

  3. Validation modules installed with isSignatureValidation=false are rejected at the gate. _exec1271Validation checks validationFlags.isSignatureValidation() (line 854 of ModularAccountBase) and reverts with SignatureValidationInvalid if false. Defense in depth - even an attacker who knows the locator cannot exploit a non-sig-flagged module.

What we now know from source re-read

What we still don't know

Future-hunt candidate status update

After Cycle 152, the MA V2 codebase's mid-and-high-EV surfaces are FULLY EXHAUSTED. Remaining work is either (a) low-EV edge cases requiring archive-node or 10-year warp, (b) Pejji wrapper-layer (strategic for product, not direct findings).

Cross-target reverse-transfer evidence (Base Azul Cycle 150 -> MA V2 Cycle 152)

This is the SECOND-DIRECTION META cross-target validation (Cycle 151 codified direction 1; this cycle tests direction 2).

Base Azul pattern (Cycle 150) MA V2 instance (Cycle 152) Match quality
TEE-MED-1 "deregistering PCR0 does not invalidate signers" = "setter without cascade-invalidation" test_SurfaceC_UninstallCascadeInvalidatesSigPath - uninstall validation, verify sig no longer validates STRONG MATCH - test wouldn't have been written without the Base Azul framework
Module-pair asymmetry STRICT-GATE + SILENT-SKIP Asked: is the Surface B hook-reentrancy gate fail-closed or fail-silent? Answer: STRICT-GATE (ValidationFunctionMissing revert) FRAMEWORK MATCH - same investigative question, different (clean) outcome
TEE attestation lifecycle (registration -> staleness -> deregistration -> CRL cascade) No analog in 1271 sig path NO MATCH (not all classes transfer)
CWIA offset doc drift No analog NO MATCH

Verdict on 2nd-direction META validation: Confirmed. Base Azul -> MA V2 transferred 1 STRONG pattern + 1 FRAMEWORK pattern. Less voluminous than direction 1 (MA V2 -> Base Azul transferred 4 patterns) but non-zero compound effect. The cross-target methodology compounds in BOTH DIRECTIONS, just asymmetrically.

The compound principle: each new target's lessons sharpen the audit questions on PREVIOUSLY-AUDITED targets. The MA V2 codebase is "completed" at Cycle 152 in terms of direct surfaces, but a future Cycle 153 auditing a NEW target might surface NEW patterns that THEN apply backward to MA V2 (a Cycle 154 reverse-pass). The methodology is asymptotic, not terminating.

Banked rules from Cycle 152

  1. Self-referential EIP-1271 recursion is bounded if the outer contract requires structured input parsing the inner staticcall cannot satisfy. Specifically: when a contract's isValidSignature(hash, sig) parses sig as [locator][segments][payload] and the locator MUST be present, then SignatureChecker's staticcall passing the raw payload will fail the locator-parse on the inner frame. Recursion terminates at depth 1. Positive case-study material; reusable design pattern for any module-system that wants to expose EIP-1271 safely while allowing the signer-of-record to be the same contract.

  2. Hook-reentrancy on ERC-6900 native selectors (installValidation, executeBatch, execute, etc.) is blocked by the direct-call-validation gate, not by SelfCallRecursionDepthExceeded. This refines the Cycle 145 banked open-question. The two defenses operate on different paths: direct-call gate fires when msg.sender is an external module, recursion-depth check fires when target = address(this) in execute(). They don't overlap; they cover complementary scenarios.

  3. EIP-1271 implementations that route by-prefix (caller selects validator) have NO collision risk by construction. Compare to implementations that iterate-and-first-wins (which DO have collision risk). This is a design-choice trade-off worth banking as a Skill check: /1271-routing-mode-check asks "does the contract iterate validators or select-by-prefix?" The latter is safer by construction.

  4. The 6th-cycle compound-knowledge effect is real. Cycle 152 walked in 19 min wall-clock vs the 2-3h cap. After 5 prior cycles on the same target, Phase 1 source re-read drops from 30-45 min to ~2 min, Phase 2 scaffold drops from 45-60 min to ~3 min (with 1 reload for sig encoding fix). Doctrine update worth proposing: after 5 cycles on a target, the per-cycle budget should HALVE per cycle, not stay constant. The diminishing-returns boundary is REAL.

  5. Cross-target reverse-transfer (target N's lessons -> target M's audit, when N audited AFTER M) compounds asymmetrically. Direction 1 (older target's lessons applied to newer target during the newer audit) typically yields more transfer because the older lessons are more mature. Direction 2 (newer target's lessons applied retroactively to older target) yields less but non-zero transfer. Both directions are worth running. Bank as a methodology rule: every Cycle N auditing target T should NOT just check "do T's lessons apply backward to all prior targets?" - it should also check "do all prior targets' lessons predict anything in T?"

  6. Bank-and-action timing on banked sub-surfaces: < 5 cycles is the safe-zone, > 10 cycles is past-due. Cycle 144 banked Surface A; Cycle 145 banked Surface B; Cycle 142C banked Surface C. They were tested at Cycle 152: cadence of 5-10 cycles between bank and test. All within tolerance. Bank as the "MA V2 follow-up cadence" data point for future calibration of the 24-Hour Implementation Rule. The 24h rule is for HIGH-PRIORITY banked patterns; lower-priority sub-sub-surfaces tolerate 5-10 cycles, beyond which they become past-due.


Cross-bridge class banked from Verus drain (2026-05-18, Cycle 153)

Pattern: PartialTransactionProof / state-root-signed-but-backing-unchecked

Source: Verus Ethereum bridge drained $11.58M on 2026-05-18 (1,625 ETH + 103.6 tBTC + 147,000 USDC + later swapped to 5,402 ETH).

Mechanism per PeckShield + on-chain forensics: - Verus bridge validates a PartialTransactionProof consisting of (a) state-root proof + (b) payout hash commitment - Notaries sign off on the state root (real Verus state, no fabrication) - BUT the Ethereum-side contract releases funds based on the payout-hash component WITHOUT verifying that the source-side actually has backing for the claimed payout - Attacker crafted a low-cost (~$10 fee) Verus tx that committed to a payout hash while leaving source totals effectively empty, then submitted the matching serialized payload to Ethereum side, which released funds without backing-check

Class name: missing-backing-check on cross-chain proof

Why it matches our cross-bridge audit framework

This is a SPECIFIC INSTANCE of the more general class our framework already covers: - Receipt-vs-backing asymmetry - the protocol verifies the proof shape but not the proof economic backing - Notary-signed-state ≠ backing-checked-state - the signer set guarantees the source state is consistent, NOT that the source state has the funds to back the destination payout - Two-component proof with one component-unverified - similar to (a) Cycle 91 cross-target patterns we banked on CCIP RMN cursing, (b) any LayerZero ULN config that splits payload from receipt without joint verification

Cross-target inquiry hooks this banks

For every future cross-chain bridge audit (Cycle 154+), ASK: 1. Does the destination-chain contract verify ONLY the proof-shape, or does it verify the source-chain backing as well? 2. Is the proof composed of multiple components (state-root + payout-hash + receipt-merkle) and are ALL components jointly validated? 3. Can a low-cost source-chain tx commit to a payout hash without actually escrowing backing? 4. Is the notary set incentivized to verify backing, or only to verify state-consistency? 5. Are notary signatures over the FULL backing claim or only over the state root?

Comparison to MA V2 EIP-1271 path

The MA V2 EIP-1271 audit (Cycle 152) banked: "self-referential recursion is bounded if outer parser requires structured input the inner staticcall cannot satisfy." This is the OPPOSITE direction of the Verus class - MA V2's defense is structural-parsing-blocks-recursion (DEFENSE), Verus's exploit is structural-parsing-misses-the-economic-component (ATTACK). Both are about "what does the parser verify?" but one is positive (parser saves you) and one is negative (parser misses what matters).

Securva pitch value of banking this

This class extends Securva's lessons-library across an entirely new target type (cross-chain bridge w/ partial proofs). Prior banked patterns were predominantly EVM-single-chain (MA V2, Pendle, CCIP RMN, Aave Capped Oracle). Adding the Verus bridge class: - Extends lessons-library coverage to non-EVM-source bridges (Verus is its own chain) - Adds a "missing-economic-backing-check" line of inquiry that generalizes to ANY cross-chain pattern - Provides a concrete fresh case-study (May 18, 2026) for the Securva soft-launch pitch

Banked test plan for next bridge audit cycle

For Cycle 154+ if a cross-chain bridge target enters the pipeline: 1. Run all 5 inquiry-hook questions above 2. Differential fuzz: forge a malformed source-tx with valid state-root but empty backing, check destination contract behavior 3. If destination releases funds without backing check = REPRODUCED Verus class, bank as finding 4. If destination correctly reverts on missing backing = positive case-study, bank as defense pattern

24-Hour Implementation Rule check

This banked class is queued for empirical test in the next 2 cycles per the 24-Hour rule. Test path: forge testbench against any open cross-chain bridge in Cantina/Immunefi pipeline (low probability currently, surfaces will be re-checked in Cycle 154-155).


Cycle 160.5B Verus class empirical validation (2026-05-19)

The Verus class banked at Cycle 153 ("missing-backing-check on cross-chain proof") has been empirically validated via a Foundry harness at ~/bounty/cycle160-5B-verus-class/.

Harness structure

3 contracts in src/VerusClassHarness.sol:

  1. MockSourceChain - records (payout_hash, source_backing) pairs and computes a state_root. Exposes both attestPartialProof (returns state_root + payout_hash only) and attestFullProof (also returns source_backing).

  2. VulnerableDestination - validates state_root + payout_hash but NOT source_backing. Mirrors the actual Verus Ethereum bridge bug shape.

  3. DefendedDestination - validates state_root + payout_hash AND source_backing >= claim amount. The fix pattern.

3 tests, all PASS

[PASS] test_VulnerablePattern_DrainSucceeds (gas: 104186)
  Logs: BUG REPRODUCED: VulnerableDestination drained 50 ETH on zero-backing payout hash

[PASS] test_DefendedPattern_DrainReverts (gas: 79822)
  Logs: DEFENSE WORKS: DefendedDestination correctly rejected zero-backing claim

[PASS] test_DefendedPattern_LegitimateFlowSucceeds (gas: 125128)
  Logs: LEGITIMATE FLOW: DefendedDestination released funds on backed payout hash

Empirically confirmed: - The Verus class IS exploitable when destination contract validates partial proof only - The defense (check source backing during claim) BLOCKS the attack without breaking legitimate flows - The class is a clean methodology test - one PASS for bug-reproduces, one PASS for defense-works, one PASS for legitimate-flow-unaffected

24-Hour Rule status: SATISFIED

Banked: 2026-05-18 ~21:30 (Cycle 153) Empirically tested: 2026-05-19 ~14:30 (Cycle 160.5B) Elapsed: ~17 hours Status: within 24-Hour Rule window. Class is no longer pending empirical validation.

Reusable Foundry harness banked

Location: /home/babakinzo/bounty/cycle160-5B-verus-class/

When auditing ANY future cross-chain bridge: 1. Identify the proof structure (state_root + payout_hash + ...) 2. Identify which destination contract validates the proof 3. Run the harness pattern: replace MockSourceChain with the real source-chain-state-attestation, replace VulnerableDestination/DefendedDestination with the actual destination contract 4. Run the 3 test patterns - the bug-reproduces test is the high-signal one

The general class is "verify the verification" - destination contracts must verify ALL components of the proof, not just the easy-to-check ones (signatures + hashes). The economic-backing component is the hard-to-check one because it requires reading source-chain state.

5 inquiry hooks for next cross-chain bridge audit (refined Cycle 160.5B)

For every cross-chain bridge audit (Cycle 161+), ASK:

  1. Does the destination-chain contract verify ONLY the proof-shape, or does it verify source-chain backing as well?
  2. Is the proof composed of multiple components (state-root + payout-hash + receipt-merkle) and are ALL components jointly validated?
  3. Can a low-cost source-chain tx commit to a payout hash without actually escrowing backing?
  4. Is the notary set incentivized to verify backing, or only to verify state-consistency?
  5. Are notary signatures over the FULL backing claim or only over the state root?

Cycle 161+ application guidance

If a Cantina / Code4rena / Immunefi / Sherlock contest opens with a cross-chain bridge target: - Run inquiry hooks 1-5 above - If ANY answer is "partial proof only" or "signatures only over state-root", high probability of Verus class bug - Drop the existing Foundry harness into the audit project, swap in real contracts - Run the 3 tests; the BUG REPRODUCES outcome is the submission-worthy finding

This is the cross-target lessons-library at full compound: a real-world bug class becomes a reusable test scaffold within ~17h.


Cycle 224 - Rule-38 regression delta re-check (develop HEAD)

Cycle: 224 (2026-06-10). Verdict: WALK CLEAN, delta NON-EMPTY but defended. 5/5 Foundry PASS. Test: ~/bounty/cycles/cycle-224-mav2-delta/repo/test/poc/Cycle224_EIP191RemovalRegression.t.sol

Branch topology (calibration that saved hours)

Real delta (after stripping pragma + forge-fmt noise)

Raw git diff c9e7683..develop -- src/ = 32 files, but only THREE real changes: 1. PR #350 88ee1ff - remove EIP-191 (toEthSignedMessageHash) wrapping from userOp validation (SMA fallback + SingleSigner + WebAuthn). HIGH signal. 2. PR #351 9e235f0 - EntryPoint v0.9 + pragma 0.8.26->0.8.28. No src logic (EP OOS). 3. PR #354 ab9c0c2 - factory impl code-length existence checks + delete WebAuthnFactory (consolidate to one factory). Hardening. ~29 other "changed" files = pragma bump + forge fmt reflow (PR #345), zero logic.

PR #350 hunt result - DEFENDED

Banked rules (Cycle 224)

  1. Rule-38 delta: branch identity beats commit identity. When a "delta" shows alarming file counts, FIRST classify each changed file {logic|pragma|fmt} by grepping the diff minus pragma solidity and checking for brace-only churn. Collapsed 32-file delta to 3 real changes in minutes. Also confirm the C141 commit is on the SAME branch as the current bounty default - here it was on a divergent deploy line.
  2. EIP-191-wrapping removal is fail-CLOSED. Old wrapped sigs get REJECTED, not replayed. The real review question for any wrapping-removal: "does ANY path consume a raw sig over an attacker-chosen hash -> cross-path replay?" Defended here by domain separation.
  3. MA V2 signature-domain map @ HEAD ab9c0c2: userOp=raw userOpHash; 1271=EIP-712 ReplaySafeHash(bytes32) domainSep(chainId,module,account); deferred=EIP-712 DeferredAction(nonce,deadline,call) account-domainSep. Three disjoint domains - reuse instead of re-deriving.
Generated 2026-07-02 13:15:05 UTC | auto-sync /15min