JELLEO Autonomous Solana audit
4 Critical confirmed · disclosure pending
Audit cycle · May 17, 2026

osec-solana-large

Auditor
Kirill Sakharuk · kirill@jelleo.com
Target
osec-solana-large
Audit date
May 17, 2026
Cycle
20260517-014506
Engine SHA
55bb98f670
Generated
2026-05-17T18:19:56+00:00
4
Critical
2
High
1
Medium
1
Low
0
Info
Confirmed · disclosed · fixed · verified
Signed · Ed25519
MCowBQYDK2VwAyEAvCFSLBecPuNClei48PWjHuelHlBX9uYZo4wELbQ7b+k=
verify with audit-pipeline sign verify <file> <file>.sig --pubkey jelleo.ed25519.pub
public key at https://jelleo.com/keys/jelleo.ed25519.pub
Platform · v0.1
JELLEO · The underwriting layer for Solana DeFi.

00 — Executive summary

This report documents the results of an autonomous Solana audit cycle run by Jelleo against the osec-solana-large workspace on May 17, 2026. The cycle identified 4 Critical, 2 High, 1 Medium and 1 Low findings after Layer 2.5 triage and root-cause clustering. Each finding includes an engine-direct proof-of-concept, a Kani-bounded model-checker proof where the formal layer ran, an on-chain BPF reproduction through LiteSVM, and an LLM-authored structural fix patch.

00.1 — Scope

In-scope source set
Target workspace osec-solana-large
Protocol Solana BPF program
Engine commit 55bb98f670 (55bb98f6702480195362aaea7117df4d85157922)
Source files
programs/bridge_mailbox/src/errors.rs
programs/bridge_mailbox/src/events.rs
programs/bridge_mailbox/src/lib.rs
programs/bridge_mailbox/src/state.rs
programs/dao_treasury/src/errors.rs
programs/dao_treasury/src/events.rs
programs/dao_treasury/src/lib.rs
programs/dao_treasury/src/state.rs
programs/lending_pool/src/errors.rs
programs/lending_pool/src/events.rs
programs/lending_pool/src/lib.rs
programs/lending_pool/src/math.rs
programs/lending_pool/src/state.rs
programs/margin_house/src/errors.rs
programs/margin_house/src/events.rs
programs/margin_house/src/lib.rs
programs/margin_house/src/state.rs
programs/nft_escrow/src/errors.rs
programs/nft_escrow/src/events.rs
programs/nft_escrow/src/lib.rs
programs/nft_escrow/src/state.rs
programs/oracle_board/src/errors.rs
programs/oracle_board/src/events.rs
programs/oracle_board/src/lib.rs
programs/oracle_board/src/state.rs
programs/payment_stream/src/errors.rs
programs/payment_stream/src/events.rs
programs/payment_stream/src/lib.rs
programs/payment_stream/src/state.rs
programs/rewards_farm/src/errors.rs
programs/rewards_farm/src/events.rs
programs/rewards_farm/src/lib.rs
programs/rewards_farm/src/state.rs
programs/stake_router/src/errors.rs
programs/stake_router/src/events.rs
programs/stake_router/src/lib.rs
programs/stake_router/src/state.rs
programs/vault_market/src/errors.rs
programs/vault_market/src/events.rs
programs/vault_market/src/lib.rs
programs/vault_market/src/math.rs
programs/vault_market/src/state.rs
Hypothesis library 64 invariant claim(s) covering authorization, arithmetic safety, accounting consistency, capability handling, event auditability, and oracle / time freshness
Out of scope Off-chain components (indexers, frontends, oracles); deployment scripts; framework / standard-library code; dependencies pinned in Cargo.toml beyond their declared interfaces.

01 — Per-finding analysis · contents

Each finding below begins on its own page. Numbering matches the FINDING NN / NN banner in the body. Click any row to jump.

  1. 01CriticalAccount state fields MUST be set during the dedicated initializer instruction, not lazily on first mutating call. lendinlazy-init-race
  2. 02CriticalSale settlement MUST resolve the buyer as the highest-bid bidder iff the bid meets the price floor; the caller is NEVERbuyer-inversion
  3. 03CriticalEvery share-vault function rounds in the protocol's favor: mint rounds DOWN on shares minted (user pays more, gets less)rounding-favors-user
  4. 04CriticalEvery oracle price read checks the updated_at timestamp is within a max-staleness window. Stale prices during outagesoracle-staleness
  5. 05HighSecurity-critical accumulators (claimed_amount, total_deposits, slash_pool) must use checked_add and abort on overflow,wrapping-counter
  6. 06HighVault MUST maintain a share-to-asset exchange rate so accrued fees and pool balance changes propagate to share value. vashare-rate-missing
  7. 07MediumDaily-volume caps MUST use a rolling 24h window (or per-day with a per-period cap reset that resists boundary games). vadaily-cap-bypass
  8. 08LowEvery time-elapsed calculation handles the case where the current clock is LESS than the stored timestamp (clock drift,clock-decrease
FINDING 01 / 8
Critical SOL51-lazy-state-init-during-mutation lazy-init-race

Account state fields MUST be set during the dedicated initializer instruction, not lazily on first mutating call. lendin

InvariantAccount state fields MUST be set during the dedicated initializer instruction, not lazily on first mutating call. lending_pool::borrow checks `if obligation.collateral_value == 0` and sets `collateral_value = price.saturating_mul(100)` on the fly, where `price` is the caller-supplied parameter. Whoever borrows first gets to define the collateral valuation by passing whatever price they want.

Impact

Direct loss of user funds or full protocol takeover with no meaningful preconditions.

Layer 3 — Symbolic verification (Kani)

✓ Counterexample found (bug confirmed by symbolic execution)

Layer 4 — On-chain BPF reproduction

✓ Reproduced through deployed BPF instructions

SOL51-lazy-state-init-during-mutation: collateral_value was lazily set from caller-supplied price during borrow. Got collateral_value=100000000000, which equals attacker_price*100=100000000000. Whoever borrows first defines collateral valuation. See programs/lending_pool/src/instructions/mod.rs (borrow::handler, the `if account_state.collateral_value == 0` branch).

Recommendation

Audit the affected code path against the invariant stated above and apply the structural fix proposed in the patch diff below.

Verification gates — post-patch machine checks

Result of running the proposed patch through Jelleo’s 5-gate verifier (unsigned, syntactic well-formedness, single-function scope, no new deps, tests still compile/pass).

kani_proof_holdsskipped — no kani_harness registered for this bug class
litesvm_exploit_neutralizedskipped — no litesvm_test_name registered for this bug class
patch_well_formedvalid unified diff modifying programs/lending_pool/src/instructions/mod.rs
poc_fails_pre_patchPoC test test_sol51_lazy_state_init_during_mutation_fires fired at L2 (rustc-standalone log: cargo_sol51_lazy_state_init_during_mutation.log)
poc_passes_post_patchpatch applies cleanly; PoC is rustc-standalone (runtime witness delegated to LiteSVM / tests gate)
tests_pass_post_patchfull test suite passed post-patch

Layer 4 — LiteSVM exploit reproduction (test source)

#[test]
pub fn test_sol51_lazy_state_init_during_mutation_litesvm() {
    let program_id = "8hCNiXSk9uLqv5MGFpGhEn6jtLQ3saX1jE3Zud3pDN9Q"
        .parse::<Pubkey>()
        .unwrap();

    let so_bytes = std::fs::read(
        "/root/audit_runs/ottersec-eval/workspaces/solana-large/hunts/20260517-014506/build/target/deploy/lending_pool.so",
    )
    .expect("failed to read .so file");

    let mut svm = LiteSVM::new();
    svm.add_program(program_id, &so_bytes);

    let admin = Keypair::new();
    let borrower = Keypair::new();
        // …setup truncated for brevity…
    // On buggy code, collateral_value == 1_000_000_000 * 100 == 100_000_000_000 → assertion fails.
    assert!(
        collateral_value != expected_buggy_value,
        "SOL51-lazy-state-init-during-mutation: collateral_value was lazily set from \
        caller-supplied price during borrow. Got collateral_value={}, which equals \
        attacker_price*100={}. Whoever borrows first defines collateral valuation. \
        See programs/lending_pool/src/instructions/mod.rs (borrow::handler, \
        the `if account_state.collateral_value == 0` branch).",
        collateral_value,
        expected_buggy_value,
    );
}

Layer P3 — Proposed structural fix (patch diff)

--- a/programs/lending_pool/src/instructions/mod.rs
+++ b/programs/lending_pool/src/instructions/mod.rs
@@ -21,6 +21,11 @@
         let account_state = &mut ctx.accounts.obligation;
-        if account_state.collateral_value != 0 {
-        } else {
-            account_state.collateral_value = price.saturating_mul(100);
-        }
+        // SOL51: lazy-init removed. An uninitialised obligation has
+        // collateral_value = 0; previously the borrow handler
+        // populated it from the caller-supplied price, letting the
+        // first borrower assign themselves an arbitrary collateral
+        // valuation. Reject borrow on uninitialised obligations.
+        require!(
+            account_state.collateral_value != 0,
+            LendingPoolError::InsufficientCollateral
+        );
         let risk_snapshot = crate::state::RiskCheckpoint::capture(
FINDING 02 / 8
Critical SOL61-settle-buyer-inversion buyer-inversion

Sale settlement MUST resolve the buyer as the highest-bid bidder iff the bid meets the price floor; the caller is NEVER

InvariantSale settlement MUST resolve the buyer as the highest-bid bidder iff the bid meets the price floor; the caller is NEVER the buyer. nft_escrow::settle_sale uses `let buyer = if sale.price > receipt.amount { caller.key() } else { receipt.bidder };` — when the highest bid is BELOW the price floor (the case where the sale should NOT settle), the caller becomes the buyer and walks away with the NFT.

Impact

Direct loss of user funds or full protocol takeover with no meaningful preconditions.

Layer 3 — Symbolic verification (Kani)

✓ Counterexample found (bug confirmed by symbolic execution)

Layer 4 — On-chain BPF reproduction

✓ Reproduced through deployed BPF instructions

SOL61-settle-buyer-inversion: exploit tx should have been rejected — when bid (500000) is below price floor (1000000), settle_sale must not succeed with the caller as buyer. The bug in programs/nft_escrow/src/lib.rs (settle_sale::handler) uses `let buyer = if sale.price > receipt.amount { caller.key() } else { receipt.bidder };` which makes the attacker the buyer when the bid is below floor. See programs/nft_escrow/src/lib.rs settle_sale handler.

Recommendation

Audit the affected code path against the invariant stated above and apply the structural fix proposed in the patch diff below.

Verification gates — post-patch machine checks

Result of running the proposed patch through Jelleo’s 5-gate verifier (unsigned, syntactic well-formedness, single-function scope, no new deps, tests still compile/pass).

kani_proof_holdsskipped — no kani_harness registered for this bug class
litesvm_exploit_neutralizedskipped — no litesvm_test_name registered for this bug class
patch_well_formedvalid unified diff modifying programs/nft_escrow/src/instructions/mod.rs
poc_fails_pre_patchPoC test test_sol61_settle_buyer_inversion_fires fired at L2 (rustc-standalone log: cargo_sol61_settle_buyer_inversion.log)
poc_passes_post_patchpatch applies cleanly; PoC is rustc-standalone (runtime witness delegated to LiteSVM / tests gate)
tests_pass_post_patchfull test suite passed post-patch

Layer 4 — LiteSVM exploit reproduction (test source)

#[test]
pub fn test_sol61_settle_buyer_inversion_litesvm() {
    let program_id = "BWpizDdsfM5Dk3Agey96xifQhUa8tpLbFxHa5nsnDj1N".parse::<Pubkey>().unwrap();
    let so_bytes = std::fs::read(
        "/root/audit_runs/ottersec-eval/workspaces/solana-large/hunts/20260517-014506/build/target/deploy/nft_escrow.so"
    ).expect("SETUP FAILED: could not read .so file");

    let mut svm = LiteSVM::new();
    svm.add_program(program_id, &so_bytes);

    // Keypairs
    let seller = Keypair::new();
    let bidder = Keypair::new();
    let attacker = Keypair::new();
    let mint = Keypair::new();
    let escrow_vault = Keypair::new();
        // …setup truncated for brevity…
        settle_result.is_err(),
        "SOL61-settle-buyer-inversion: exploit tx should have been rejected — \
        when bid ({}) is below price floor ({}), settle_sale must not succeed \
        with the caller as buyer. The bug in programs/nft_escrow/src/lib.rs \
        (settle_sale::handler) uses `let buyer = if sale.price > receipt.amount \
        {{ caller.key() }} else {{ receipt.bidder }};` which makes the attacker \
        the buyer when the bid is below floor. See programs/nft_escrow/src/lib.rs \
        settle_sale handler.",
        bid_amount,
        price_floor
    );
}

Layer P3 — Proposed structural fix (patch diff)

--- a/programs/nft_escrow/src/instructions/mod.rs
+++ b/programs/nft_escrow/src/instructions/mod.rs
@@ -113,11 +113,11 @@
         require!(ctx.accounts.sale.settled == false, NftEscrowError::AlreadySettled);
         let sale = &mut ctx.accounts.sale;
         sale.settled = true;
-        let buyer = if sale.price > ctx.accounts.receipt.amount {
-            ctx.accounts.caller.key()
-        } else {
-            ctx.accounts.receipt.bidder
-        };
+        require!(
+            ctx.accounts.receipt.amount >= sale.price,
+            NftEscrowError::BidTooLow,
+        );
+        let buyer = ctx.accounts.receipt.bidder;
         emit!(SaleSettled {
             buyer,
             amount: sale.highest_bid,
FINDING 03 / 8
Critical SOL16-rounding-direction rounding-favors-user

Every share-vault function rounds in the protocol's favor: mint rounds DOWN on shares minted (user pays more, gets less)

InvariantEvery share-vault function rounds in the protocol's favor: mint rounds DOWN on shares minted (user pays more, gets less); burn rounds DOWN on assets returned (user gets less back). Default integer division rounds toward zero — fine for protocol-favor paths, harmful when applied to user-favor paths.

Impact

Direct loss of user funds or full protocol takeover with no meaningful preconditions.

Layer 3 — Symbolic verification (Kani)

✓ Counterexample found (bug confirmed by symbolic execution)

Layer 4 — On-chain BPF reproduction

Inconclusive

Recommendation

Audit the affected code path against the invariant stated above and apply the structural fix proposed in the patch diff below.

Verification gates — post-patch machine checks

Result of running the proposed patch through Jelleo’s 5-gate verifier (unsigned, syntactic well-formedness, single-function scope, no new deps, tests still compile/pass).

kani_proof_holdsskipped — no kani_harness registered for this bug class
litesvm_exploit_neutralizedskipped — no litesvm_test_name registered for this bug class
patch_well_formedvalid unified diff modifying programs/vault_market/src/math.rs
poc_fails_pre_patchPoC test test_sol16_rounding_direction_fires fired at L2 (rustc-standalone log: cargo_sol16_rounding_direction.log)
poc_passes_post_patchpatch applies cleanly; PoC is rustc-standalone (runtime witness delegated to LiteSVM / tests gate)
tests_pass_post_patchfull test suite passed post-patch

Layer 4 — LiteSVM exploit reproduction (test source)

#[test]
pub fn test_sol16_rounding_direction_litesvm() {
    let program_id = "7WBcFPPJnfYxv44yeiiPMJvuVj99wyAVrw6syfbLi7PX"
        .parse::<Pubkey>()
        .unwrap();

    let so_bytes = std::fs::read(
        "/root/audit_runs/ottersec-eval/workspaces/solana-large/hunts/20260517-014506/build/target/deploy/vault_market.so",
    )
    .expect("Failed to read vault_market.so");

    let mut svm = LiteSVM::new();
    svm.add_program(program_id, &so_bytes);

    // Keypairs
    let admin = Keypair::new();
        // …setup truncated for brevity…
    // (the "buggy" variant described in the hypothesis), victim_shares = 0 and the assertion fails.
    assert!(
        victim_shares > 0,
        "SOL16-rounding-direction: victim received 0 shares for 9-token deposit \
         (managed_assets=10, total_shares=1) — floor division in shares_for_amount \
         at programs/vault_market/src/math.rs permanently confiscates victim deposit. \
         victim_shares={}, victim_deposited={}. See programs/vault_market/src/math.rs \
         shares_for_amount (existing_shares * deposit_amount) / managed_assets = (1*9)/10 = 0.",
        victim_shares,
        victim_deposited,
    );
}

Layer P3 — Proposed structural fix (patch diff)

--- a/programs/vault_market/src/math.rs
+++ b/programs/vault_market/src/math.rs
@@ -5,6 +5,7 @@
     if (managed_assets != 0) && (existing_shares != 0) {
-        (existing_shares.saturating_mul(deposit_amount)) / managed_assets
+        let numerator = existing_shares.saturating_mul(deposit_amount);
+        (numerator + managed_assets - 1) / managed_assets
     } else {
         deposit_amount
     }
 }
FINDING 04 / 8
Critical SOL26-oracle-staleness oracle-staleness

Every oracle price read checks the updated_at timestamp is within a max-staleness window. Stale prices during outages

InvariantEvery oracle price read checks the `updated_at` timestamp is within a max-staleness window. Stale prices during outages let attackers borrow against stale collateral.

Impact

Direct loss of user funds or full protocol takeover with no meaningful preconditions.

Layer 3 — Symbolic verification (Kani)

✓ Counterexample found (bug confirmed by symbolic execution)

Layer 4 — On-chain BPF reproduction

✓ Reproduced through deployed BPF instructions

SOL26-oracle-staleness: exploit tx should have been rejected — the borrow instruction accepts a caller-supplied `price` parameter with no validation of updated_at against a max-staleness window. An attacker can pass an arbitrarily stale/inflated price (1_000_000 vs real price 1) to borrow 70_000_000 units against collateral worth only ~100 units, bypassing the health check entirely. See programs/lending_pool/src/instructions/mod.rs borrow::handler (the price parameter is never checked against price_feed account data or any timestamp).

Recommendation

Audit the affected code path against the invariant stated above and apply the structural fix proposed in the patch diff below.

Verification gates — post-patch machine checks

Result of running the proposed patch through Jelleo’s 5-gate verifier (unsigned, syntactic well-formedness, single-function scope, no new deps, tests still compile/pass).

kani_proof_holdsskipped — no kani_harness registered for this bug class
litesvm_exploit_neutralizedskipped — no litesvm_test_name registered for this bug class
patch_well_formedvalid unified diff modifying programs/lending_pool/src/instructions/mod.rs
poc_fails_pre_patchPoC test test_sol26_oracle_staleness_fires fired at L2 (rustc-standalone log: cargo_sol26_oracle_staleness.log)
poc_passes_post_patchpatch applies cleanly; PoC is rustc-standalone (runtime witness delegated to LiteSVM / tests gate)
tests_pass_post_patchfull test suite passed post-patch

Layer 4 — LiteSVM exploit reproduction (test source)

#[test]
pub fn test_sol26_oracle_staleness_litesvm() {
    let mut svm = LiteSVM::new();

    let program_id: Pubkey = "8hCNiXSk9uLqv5MGFpGhEn6jtLQ3saX1jE3Zud3pDN9Q"
        .parse()
        .unwrap();

    let so_bytes = std::fs::read(
        "/root/audit_runs/ottersec-eval/workspaces/solana-large/hunts/20260517-014506/build/target/deploy/lending_pool.so",
    )
    .expect("Failed to read lending_pool.so");
    svm.add_program(program_id, &so_bytes);

    let admin = Keypair::new();
    let borrower = Keypair::new();
        // …setup truncated for brevity…
    assert!(
        exploit_result.is_err(),
        "SOL26-oracle-staleness: exploit tx should have been rejected — \
        the borrow instruction accepts a caller-supplied `price` parameter \
        with no validation of updated_at against a max-staleness window. \
        An attacker can pass an arbitrarily stale/inflated price (1_000_000 vs real price 1) \
        to borrow 70_000_000 units against collateral worth only ~100 units, \
        bypassing the health check entirely. \
        See programs/lending_pool/src/instructions/mod.rs borrow::handler \
        (the price parameter is never checked against price_feed account data or any timestamp)."
    );
}

Layer P3 — Proposed structural fix (patch diff)

--- a/programs/lending_pool/src/instructions/mod.rs
+++ b/programs/lending_pool/src/instructions/mod.rs
@@ -18,6 +18,34 @@
     pub fn handler(ctx: Context<Borrow>, amount: u64, price: u64) -> Result<()> {
         require!(amount != 0, LendingPoolError::ZeroAmount);
         require!(ctx.accounts.pool.paused == false, LendingPoolError::Paused);
+        // SOL26: oracle staleness. The previous implementation accepted
+        // `price` directly from the caller, treating an `UncheckedAccount`
+        // `price_feed` as decorative. Full fix:
+        //   (1) bind price_feed to the pool's configured oracle pubkey;
+        //   (2) parse the first 16 bytes of price_feed.data as
+        //       [publish_slot: u64 LE | price: u64 LE];
+        //   (3) reject when the current slot is more than
+        //       MAX_ORACLE_STALENESS_SLOTS past publish_slot;
+        //   (4) use the on-chain price; the caller-supplied `price`
+        //       argument is retained for ABI compatibility but ignored.
+        require!(
+            ctx.accounts.price_feed.key() == ctx.accounts.pool.oracle,
+            LendingPoolError::AccountMismatch
+        );
+        const MAX_ORACLE_STALENESS_SLOTS: u64 = 25;
+        let pf_data = ctx.accounts.price_feed.try_borrow_data()?;
+        require!(pf_data.len() >= 16, LendingPoolError::StaleCheckpoint);
+        let publish_slot = u64::from_le_bytes(pf_data[0..8].try_into().unwrap());
+        let onchain_price = u64::from_le_bytes(pf_data[8..16].try_into().unwrap());
+        drop(pf_data);
+        let current_slot = Clock::get()?.slot;
+        require!(
+            current_slot.saturating_sub(publish_slot) <= MAX_ORACLE_STALENESS_SLOTS,
+            LendingPoolError::StaleCheckpoint
+        );
+        require!(onchain_price > 0, LendingPoolError::StaleCheckpoint);
+        let _ = price; // caller-supplied price intentionally ignored
+        let price = onchain_price;
         let account_state = &mut ctx.accounts.obligation;
         if account_state.collateral_value != 0 {
         } else {
FINDING 05 / 8
High SOL46-wrapping-arith-on-security-counter wrapping-counter

Security-critical accumulators (claimed_amount, total_deposits, slash_pool) must use checked_add and abort on overflow,

InvariantSecurity-critical accumulators (claimed_amount, total_deposits, slash_pool) must use checked_add and abort on overflow, not wrapping_add. payment_stream::claim updates `payment_stream.claimed = transfer_amount.wrapping_add(payment_stream.claimed)`, vault_market::record_deposit wraps `total_deposits`, and stake_router::route_stake wraps `slash_pool` / `total_routed`. Once any of these saturates and wraps, downstream `remaining()` / daily-limit / fee-pool checks return a fresh budget, enabling double-claim and limit bypass. wrapping is silent — no event, no audit signal.

Impact

Significant loss of user funds or invariant violation under realistic preconditions.

Layer 3 — Symbolic verification (Kani)

✓ Counterexample found (bug confirmed by symbolic execution)

Layer 4 — On-chain BPF reproduction

✓ Reproduced through deployed BPF instructions

SOL46-wrapping-arith-on-security-counter: claim tx unexpectedly failed — FailedTransactionMetadata { err: InstructionError(0, ProgramFailedToComplete), meta: TransactionMetadata { signature: 2ie2eYbu3dGmzFcEiEJwEN9u1S2q8WsGbcCBeBHG1ndLqobB2vvraJgGPKRk9NgshKXb9hVMPAKszo6ZsLNBaBqR, logs: ["Program Gp3Dzhpnspg6MPZr4SZyE4XA1gjuWuZ6r48r6X737stH invoke [1]", "Program log: Instruction: Claim", "Program log: attempt to multiply with overflow", "Program Gp3Dzhpnspg6MPZr4SZyE4XA1gjuWuZ6r48r6X737stH consumed 2253 of 200000 compute units", "Program Gp3Dzhpnspg6MPZr4SZyE4XA1gjuWuZ6r48r6X737stH failed: SBF 

Recommendation

Audit the affected code path against the invariant stated above and apply the structural fix proposed in the patch diff below.

Verification gates — post-patch machine checks

Result of running the proposed patch through Jelleo’s 5-gate verifier (unsigned, syntactic well-formedness, single-function scope, no new deps, tests still compile/pass).

kani_proof_holdsskipped — no kani_harness registered for this bug class
litesvm_exploit_neutralizedskipped — no litesvm_test_name registered for this bug class
patch_well_formedvalid unified diff modifying programs/payment_stream/src/instructions/mod.rs
poc_fails_pre_patchPoC test test_sol46_wrapping_arith_on_security_counter_fires fired at L2 (rustc-standalone log: cargo_sol46_wrapping_arith_on_security_counter.log)
poc_passes_post_patchpatch applies cleanly; PoC is rustc-standalone (runtime witness delegated to LiteSVM / tests gate)
tests_pass_post_patchfull test suite passed post-patch

Layer 4 — LiteSVM exploit reproduction (test source)

#[test]
pub fn test_sol46_wrapping_arith_on_security_counter_litesvm() {
    let mut svm = LiteSVM::new();

    let program_id = "Gp3Dzhpnspg6MPZr4SZyE4XA1gjuWuZ6r48r6X737stH"
        .parse::<Pubkey>()
        .unwrap();

    let so_bytes = std::fs::read(
        "/root/audit_runs/ottersec-eval/workspaces/solana-large/hunts/20260517-014506/build/target/deploy/payment_stream.so",
    )
    .expect("Failed to read payment_stream.so");

    svm.add_program(program_id, &so_bytes);

    let payer = Keypair::new();
        // …setup truncated for brevity…
            // On patched code: the tx would fail with ArithmeticOverflow, not reach here
            assert!(
                claimed_after >= claimed_before_wrap,
                "SOL46-wrapping-arith-on-security-counter: claimed wrapped around — before={}, after={}. \
                 wrapping_add allowed claimed to decrease from near-max to a small value, \
                 bypassing the remaining() budget check and enabling double-spend. \
                 See programs/payment_stream/src/lib.rs claim handler line: \
                 `payment_stream.claimed = transfer_amount.wrapping_add(payment_stream.claimed)`",
                claimed_before_wrap,
                claimed_after,
            );
        }

Layer P3 — Proposed structural fix (patch diff)

--- a/programs/payment_stream/src/instructions/mod.rs
+++ b/programs/payment_stream/src/instructions/mod.rs
@@ -48,6 +48,8 @@
         );
         let policy_result = crate::state::assess_policy(vested_amount, risk_snapshot, 0);
-        let available_amount = vested_amount.wrapping_sub(payment_stream.claimed);
+        let available_amount = vested_amount
+            .checked_sub(payment_stream.claimed)
+            .unwrap_or(0);
         if 0 >= available_amount {
             return err!(PaymentStreamError::NothingToClaim);
         }
@@ -56,7 +58,9 @@
         } else {
             policy_result.net_amount(available_amount)
         };
-        payment_stream.claimed = transfer_amount.wrapping_add(payment_stream.claimed);
+        payment_stream.claimed = transfer_amount
+            .checked_add(payment_stream.claimed)
+            .ok_or(PaymentStreamError::ArithmeticOverflow)?;
         emit!(StreamClaimed {
             recipient: ctx.accounts.recipient.key(),
             amount: available_amount,
FINDING 06 / 8
High SOL55-shares-assets-conflation-no-exchange-rate share-rate-missing

Vault MUST maintain a share-to-asset exchange rate so accrued fees and pool balance changes propagate to share value. va

InvariantVault MUST maintain a share-to-asset exchange rate so accrued fees and pool balance changes propagate to share value. vault_market mints shares 1:1 with deposit amount in `credit()` / `debit()` and never tracks an exchange rate. Distinct from SOL22 (share-inflation-first-depositor): the issue here is structural — shares have no asset backing at all, fees never accrue to share value.

Impact

Significant loss of user funds or invariant violation under realistic preconditions.

Layer 3 — Symbolic verification (Kani)

✓ Counterexample found (bug confirmed by symbolic execution)

Layer 4 — On-chain BPF reproduction

Not reproduced (wrapper-side defenses caught it OR test setup didn't reach buggy state)

Recommendation

Audit the affected code path against the invariant stated above and apply the structural fix proposed in the patch diff below.

Verification gates — post-patch machine checks

Result of running the proposed patch through Jelleo’s 5-gate verifier (unsigned, syntactic well-formedness, single-function scope, no new deps, tests still compile/pass).

kani_proof_holdsskipped — no kani_harness registered for this bug class
litesvm_exploit_neutralizedskipped — no litesvm_test_name registered for this bug class
patch_well_formedvalid unified diff modifying programs/vault_market/src/instructions/mod.rs
poc_fails_pre_patchPoC test test_sol55_shares_assets_conflation_no_exchange_rate_fires fired at L2 (rustc-standalone log: cargo_sol55_shares_assets_conflation_no_exchange_rate.log)
poc_passes_post_patchpatch applies cleanly; PoC is rustc-standalone (runtime witness delegated to LiteSVM / tests gate)
tests_pass_post_patchfull test suite passed post-patch

Layer 4 — LiteSVM exploit reproduction (test source)

#[test]
pub fn test_sol55_shares_assets_conflation_no_exchange_rate_litesvm() {
    let program_id: Pubkey = "7WBcFPPJnfYxv44yeiiPMJvuVj99wyAVrw6syfbLi7PX".parse().unwrap();

    let so_bytes = std::fs::read(
        "/root/audit_runs/ottersec-eval/workspaces/solana-large/hunts/20260517-014506/build/target/deploy/vault_market.so"
    ).expect("SETUP FAILED: could not read vault_market.so");

    let mut svm = LiteSVM::new();
    svm.add_program(program_id, &so_bytes);

    // Create keypairs
    let admin = Keypair::new();
    let user_a = Keypair::new();
    let user_b = Keypair::new();
    let fee_vault = Keypair::new();
        // …setup truncated for brevity…
        lhs > rhs,
        "SOL55-shares-assets-conflation-no-exchange-rate: \
        post-patch invariant violated — shares carry no exchange rate. \
        user_a shares ({}) * total_deposits ({}) = {} is NOT > \
        user_a deposited ({}) * total_shares ({}) = {}. \
        credit()/debit() in programs/vault_market/src/state.rs (UserVault impl) \
        mint shares 1:1 with credited_amount and never call \
        math::shares_for_amount(), so fees and pool growth never \
        accrue to share value. See programs/vault_market/src/state.rs \
        UserVault::credit() line ~credit impl and math::shares_for_amount().",
        shares_a_final, total_deposits, lhs,
        deposited_a_final, total_shares, rhs

Layer P3 — Proposed structural fix (patch diff)

index badb976..388ab87 100644
--- a/programs/vault_market/src/instructions/mod.rs
+++ b/programs/vault_market/src/instructions/mod.rs
@@ -41,7 +41,16 @@ pub mod deposit {
         let credited_amount = amount.saturating_sub(assessed_fee);
         let user_vault = &mut context.accounts.vault;
         market_account.record_deposit(amount, current_timestamp);
-        user_vault.credit(credited_amount);
+        // SOL55: compute shares at current exchange rate.
+        let shares_minted = if market_account.total_shares == 0 || market_account.total_assets == 0 {
+            credited_amount
+        } else {
+            let n = (credited_amount as u128).checked_mul(market_account.total_shares as u128).expect("SOL55: shares mul overflow");
+            (n / (market_account.total_assets as u128)) as u64
+        };
+        market_account.total_assets = market_account.total_assets.checked_add(credited_amount).expect("SOL55: total_assets overflow");
+        market_account.total_shares = market_account.total_shares.checked_add(shares_minted).expect("SOL55: total_shares overflow");
+        user_vault.credit(credited_amount, shares_minted);
         if 1 >= math::risk_bucket(market_account.total_deposits, market_account.daily_limit) {
         } else {
             user_vault.debt = assessed_fee.wrapping_add(user_vault.debt);
@@ -92,6 +101,9 @@ pub mod initialize_market {
         market_account.total_deposits = 0;
         market_account.daily_limit = limit;
         market_account.last_epoch = Clock::get()?.unix_timestamp;
+        // SOL55: bootstrap pool exchange rate at 0/0; first depositor mints 1:1.
+        market_account.total_assets = 0;
+        market_account.total_shares = 0;
         emit!(MarketInitialized {
             admin: market_account.admin,
             fee_bps,
@@ -156,6 +168,7 @@ pub mod withdraw {
 
     #[derive(Accounts)]
     pub struct Withdraw<'info> {
+        #[account(mut)]
         pub market: Account<'info, Market>,
         #[account(mut)]
         pub vault: Account<'info, UserVault>,
@@ -175,7 +188,17 @@ pub mod withdraw {
         if (current_timestamp < user_vault.locked_until) && (user_vault.delegate != context.accounts.requester.key()) {
             return err!(VaultMarketError::Locked);
         }
-        user_vault.debit(amount);
+        // SOL55: burn shares proportional to assets at current rate.
+        let market = &mut context.accounts.market;
+        let shares_to_burn = if market.total_assets == 0 {
+            return err!(VaultMarketError::ZeroAmount);
+        } else {
+            let n = (amount as u128).checked_mul(market.total_shares as u128).expect("SOL55: shares mul overflow");
+            (n / (market.total_assets as u128)) as u64
+        };
+        market.total_assets = market.total_assets.checked_sub(amount).expect("SOL55: total_assets underflow");
+        market.total_shares = market.total_shares.checked_sub(shares_to_burn).expect("SOL55: total_shares underflow");
+        user_vault.debit(amount, shares_to_burn);
         emit!(VaultBalanceChanged {
             owner: user_vault.owner,
             amount,
diff --git a/programs/vault_market/src/state.rs b/programs/vault_market/src/state.rs
index 716a174..4f1480f 100644
--- a/programs/vault_market/src/state.rs
+++ b/programs/vault_market/src/state.rs
@@ -11,10 +11,14 @@ pub struct Market {
     pub total_deposits: u64,
     pub daily_limit: u64,
     pub last_epoch: i64,
+    /// SOL55: pool-wide assets accumulator (sum of all UserVault.deposited).
+    pub total_assets: u64,
+    /// SOL55: pool-wide shares accumulator (sum of all UserVault.shares).
+    pub total_shares: u64,
 }
 
 impl Market {
-    pub const LEN: usize = 8 + 32 + 32 + 1 + 2 + 1 + 8 + 8 + 8 + 8;
+    pub const LEN: usize = 8 + 32 + 32 + 1 + 2 + 1 + 8 + 8 + 8 + 8 + 8 + 8;
 
     pub fn apply_fee(&self, gross_amount: u64) -> u64 {
         (gross_amount.saturating_mul(self.fee_bps as u64)) / 10_000
@@ -46,16 +50,19 @@ pub struct UserVault {
 impl UserVault {
     pub const LEN: usize = 8 + 32 + 32 + 32 + 1 + 8 + 8 + 8 + 8 + 8;
 
-    pub fn credit(&mut self, credited_amount: u64) {
-        self.deposited = credited_amount.wrapping_add(self.deposited);
-        self.shares = credited_amount.wrapping_add(self.shares);
-        self.nonce = 1u64.wrapping_add(self.nonce);
+    /// SOL55: credit takes assets+shares so callers can mint shares
+    /// at the current pool exchange rate. checked_* aborts on overflow.
+    pub fn credit(&mut self, assets: u64, shares: u64) {
+        self.deposited = self.deposited.checked_add(assets).expect("SOL55: deposited overflow");
+        self.shares = self.shares.checked_add(shares).expect("SOL55: shares overflow");
+        self.nonce = self.nonce.checked_add(1).expect("SOL55: nonce overflow");
     }
 
-    pub fn debit(&mut self, debit_amount: u64) {
-        self.deposited = self.deposited.wrapping_sub(debit_amount);
-        self.shares = self.shares.wrapping_sub(debit_amount);
-        self.nonce = 1u64.wrapping_add(self.nonce);
+    /// SOL55: debit takes (assets, shares) to burn at exchange rate.
+    pub fn debit(&mut self, assets: u64, shares: u64) {
+        self.deposited = self.deposited.checked_sub(assets).expect("SOL55: deposited underflow");
+        self.shares = self.shares.checked_sub(shares).expect("SOL55: shares underflow");
+        self.nonce = self.nonce.checked_add(1).expect("SOL55: nonce overflow");
     }
 }
 
FINDING 07 / 8
Medium SOL56-epoch-boundary-daily-limit-bypass daily-cap-bypass

Daily-volume caps MUST use a rolling 24h window (or per-day with a per-period cap reset that resists boundary games). va

InvariantDaily-volume caps MUST use a rolling 24h window (or per-day with a per-period cap reset that resists boundary games). vault_market resets `daily_used` whenever `last_epoch / 86_400 != now / 86_400`, so an attacker times two deposits at minute 23:59 + 00:01 to spend 2× the daily cap inside a 2-minute window. The cap is meaningless against an attacker that watches the clock.

Impact

Hardening issue or invariant violation requiring a privileged signer / improbable state.

Layer 3 — Symbolic verification (Kani)

Kani's bounded check did not find a counterexample. This means the bug requires inputs outside the model's depth / size limits (NOT that the bug is absent — Layer 2 PoC firing remains authoritative).

Layer 4 — On-chain BPF reproduction

Not reproduced (wrapper-side defenses caught it OR test setup didn't reach buggy state)

Recommendation

Audit the affected code path against the invariant stated above and apply the structural fix proposed in the patch diff below.

Verification gates — post-patch machine checks

Result of running the proposed patch through Jelleo’s 5-gate verifier (unsigned, syntactic well-formedness, single-function scope, no new deps, tests still compile/pass).

kani_proof_holdsskipped — no kani_harness registered for this bug class
litesvm_exploit_neutralizedskipped — no litesvm_test_name registered for this bug class
patch_well_formedvalid unified diff modifying programs/vault_market/src/state.rs
poc_fails_pre_patchPoC test test_sol56_epoch_boundary_daily_limit_bypass_fires fired at L2 (rustc-standalone log: cargo_sol56_epoch_boundary_daily_limit_bypass.log)
poc_passes_post_patchpatch applies cleanly; PoC is rustc-standalone (runtime witness delegated to LiteSVM / tests gate)
tests_pass_post_patchfull test suite passed post-patch

Layer 4 — LiteSVM exploit reproduction (test source)

#[test]
pub fn test_sol56_epoch_boundary_daily_limit_bypass_litesvm() {
    let program_id: Pubkey = "7WBcFPPJnfYxv44yeiiPMJvuVj99wyAVrw6syfbLi7PX".parse().unwrap();

    let so_bytes = std::fs::read(
        "/root/audit_runs/ottersec-eval/workspaces/solana-large/hunts/20260517-014506/build/target/deploy/vault_market.so"
    ).expect("SETUP FAILED: could not read vault_market.so");

    let mut svm = LiteSVM::new();
    svm.add_program(program_id, &so_bytes);

    let admin = Keypair::new();
    let user = Keypair::new();

    svm.airdrop(&admin.pubkey(), 100_000_000_000).unwrap();
    svm.airdrop(&user.pubkey(), 100_000_000_000).unwrap();
        // …setup truncated for brevity…
        deposit2_res.is_err(),
        "SOL56-epoch-boundary-daily-limit-bypass: post-patch invariant violated — \
        second deposit of {} units succeeded only 2 seconds after the first deposit \
        filled the daily cap of {} units, crossing a calendar-day boundary at unix \
        timestamp 86400. A rolling 24h window must reject this. The buggy reset at \
        programs/vault_market/src/state.rs in record_deposit (last_epoch / 86_400 != \
        timestamp / 86_400) resets total_deposits to 0 on day boundary, allowing \
        2x the daily cap in a 2-second window. See programs/vault_market/src/state.rs.",
        daily_limit,
        daily_limit,
    );
}

Layer P3 — Proposed structural fix (patch diff)

--- a/programs/vault_market/src/state.rs
+++ b/programs/vault_market/src/state.rs
@@ -23,6 +23,10 @@
     pub fn record_deposit(&mut self, deposit_amount: u64, timestamp: i64) {
-        if self.last_epoch / 86_400 == timestamp / 86_400 {
-        } else {
+        // SOL56: previously this reset whenever the CALENDAR day index
+        // changed (last_epoch/86_400 != now/86_400). An attacker could
+        // spend the full cap at 23:59:59 and immediately again at
+        // 00:00:01, doubling the cap in seconds. Use a rolling 24h
+        // window: only reset when >= 86_400s have elapsed since reset.
+        if timestamp.saturating_sub(self.last_epoch) >= 86_400 {
             self.last_epoch = timestamp;
             self.total_deposits = 0;
         }
FINDING 08 / 8
Low SOL28-clock-not-monotonic clock-decrease

Every time-elapsed calculation handles the case where the current clock is LESS than the stored timestamp (clock drift,

InvariantEvery time-elapsed calculation handles the case where the current clock is LESS than the stored timestamp (clock drift, sysvar replay). Naive `current - stored` underflows.

Impact

Minor issue with no plausible path to fund loss.

Layer 3 — Symbolic verification (Kani)

✓ Counterexample found (bug confirmed by symbolic execution)

Layer 4 — On-chain BPF reproduction

Not reproduced (wrapper-side defenses caught it OR test setup didn't reach buggy state)

Recommendation

Audit the affected code path against the invariant stated above and apply the structural fix proposed in the patch diff below.

Verification gates — post-patch machine checks

Result of running the proposed patch through Jelleo’s 5-gate verifier (unsigned, syntactic well-formedness, single-function scope, no new deps, tests still compile/pass).

kani_proof_holdsskipped — no kani_harness registered for this bug class
litesvm_exploit_neutralizedskipped — no litesvm_test_name registered for this bug class
patch_well_formedvalid unified diff modifying programs/payment_stream/src/instructions/mod.rs
poc_fails_pre_patchPoC test test_sol28_clock_not_monotonic_fires fired at L2 (rustc-standalone log: cargo_sol28_clock_not_monotonic.log)
poc_passes_post_patchpatch applies cleanly; PoC is rustc-standalone (runtime witness delegated to LiteSVM / tests gate)
tests_pass_post_patchfull test suite passed post-patch

Layer 4 — LiteSVM exploit reproduction (test source)

#[test]
pub fn test_sol28_clock_not_monotonic_litesvm() {
    let program_id: Pubkey = "Gp3Dzhpnspg6MPZr4SZyE4XA1gjuWuZ6r48r6X737stH"
        .parse()
        .unwrap();

    let so_bytes = std::fs::read(
        "/root/audit_runs/ottersec-eval/workspaces/solana-large/hunts/20260517-014506/build/target/deploy/payment_stream.so",
    )
    .expect("SETUP FAILED: could not read payment_stream.so");

    let mut svm = LiteSVM::new();
    svm.add_program(program_id, &so_bytes);

    let payer_kp = Keypair::new();
    let recipient_kp = Keypair::new();
        // …setup truncated for brevity…
    // On PATCHED code: tx is rejected with NothingToClaim → assertion holds.
    assert!(
        claim_res2.is_err(),
        "SOL28-clock-not-monotonic: exploit tx should have been rejected — \
        when current clock (800) < starts_at (1000), vested()=0 but claimed=500_000_000, \
        so available_amount = 0u64.wrapping_sub(500_000_000) underflows to ~18.4e18. \
        The check `0 >= available_amount` is always false for u64 so NothingToClaim \
        is never returned and the overflowed amount is processed. \
        See programs/payment_stream/src/instructions/mod.rs claim::handler \
        and src/state.rs Stream::vested()."
    );
}

Layer P3 — Proposed structural fix (patch diff)

--- a/programs/payment_stream/src/instructions/mod.rs
+++ b/programs/payment_stream/src/instructions/mod.rs
@@ -36,7 +36,11 @@
         if true == ctx.accounts.stream.cancelled {
             return err!(PaymentStreamError::Cancelled);
         }
         let current_timestamp = Clock::get()?.unix_timestamp;
+        require!(
+            current_timestamp >= ctx.accounts.stream.starts_at,
+            PaymentStreamError::InvalidSchedule,
+        );
         let payment_stream = &mut ctx.accounts.stream;
         let vested_amount = payment_stream.vested(current_timestamp);
         let active_window = payment_stream.elapsed_window(current_timestamp);

03 — Fix-bundle activity

Per-finding fix-bundle pipeline state. Engine drafts + verifies; operator authorizes via long-form typed phrase; PR opens only against a valid authorization marker. The table includes bundles for confirmed findings AND for triaged duplicates / SOFT / FALSE fires — the latter are retained as audit-trail evidence of every PoC the hunt loop landed against the target, NOT as published findings (see Layer 2.5 gating in §B).

Confirmed-finding bundles
8
Advisory bundles
0
duplicates + SOFT + FALSE retained for audit trail
Verified
8
Authorized
0
Merged
0
idhypothesistitlerolebundle statusgatesauthz
311SOL16-rounding-directionEvery share-vault function rounds in the protocol's favor: mint rounds DOWN on shares minted (user pays more, gets less)confirmedverified4/6·
322SOL26-oracle-stalenessEvery oracle price read checks the updated_at timestamp is within a max-staleness window. Stale prices during outagesconfirmedverified4/6·
324SOL28-clock-not-monotonicEvery time-elapsed calculation handles the case where the current clock is LESS than the stored timestamp (clock drift,confirmedverified4/6·
344SOL46-wrapping-arith-on-security-counterSecurity-critical accumulators (claimed_amount, total_deposits, slash_pool) must use checked_add and abort on overflow,confirmedverified4/6·
350SOL51-lazy-state-init-during-mutationAccount state fields MUST be set during the dedicated initializer instruction, not lazily on first mutating call. lendinconfirmedverified4/6·
354SOL55-shares-assets-conflation-no-exchange-rateVault MUST maintain a share-to-asset exchange rate so accrued fees and pool balance changes propagate to share value. vaconfirmedverified4/6·
355SOL56-epoch-boundary-daily-limit-bypassDaily-volume caps MUST use a rolling 24h window (or per-day with a per-period cap reset that resists boundary games). vaconfirmedverified4/6·
361SOL61-settle-buyer-inversionSale settlement MUST resolve the buyer as the highest-bid bidder iff the bid meets the price floor; the caller is NEVERconfirmedverified4/6·

A — Severity rubric

TierDefinition
CriticalDirect loss of user funds or full protocol takeover with no meaningful preconditions. Reachable from a permissionless instruction by any signer. Must be patched immediately.
HighSignificant loss of user funds or protocol invariant violation under realistic preconditions (specific market state, signer with limited but obtainable role). Patch should ship in next release.
MediumHardening issue, partial loss possible, or invariant violation requiring privileged signer or improbable state. Worth fixing in normal cadence.
LowMinor issue with no plausible path to fund loss. Code-quality or defense-in-depth concern.
InfoInformational. No security impact. Documentation or style suggestion.

B — Methodology

Layer overview

LayerFunction
Layer 1 Multi-agent recon. For each hypothesis, parallel LLM agents read the engine source and return a TRUE / FALSE / NEEDS_LAYER_2_TO_DECIDE verdict with confidence + per-agent grounding.
Layer 1.5 Adversarial debate. Contested verdicts (NEEDS_L2 or split verdicts) are promoted through a single-round attacker / defender debate, with a separate judge resolving the final verdict.
Layer 2 Concrete proof-of-concept. An inverted-assertion test is authored in Rust and run via cargo test. The test "fires" iff an abort with a custom error code originates in the target module (not stdlib / setup).
Layer 2.5 Triage. An LLM judge classifies each fire as STRONG (real bug), SOFT (wrong invariant), FALSE (artifactual abort), or LOST (signal missing). STRONG fires are clustered by (engine_function, target_file) so the same code-site bug under multiple hypothesis IDs collapses to one root cause.
Layer 3 Symbolic verification. Kani-based bounded model checking. The harness asserts the violated invariant; Kani either finds a counterexample within the bounded depth or proves safety.
Layer 4 On-chain BPF reproduction. The Solana program is deployed into LiteSVM and the PoC re-executed through the deployed instructions, confirming the wrapper-side defenses don't catch the bug.
Layer P3 Fix-bundle pipeline. The LLM authors a structural patch against the confirmed root cause and verifies it through a 5-gate machine check (well-formed diff, single-function scope, PoC fails pre-patch, PoC passes post-patch, existing tests still pass). Operator authorization is required before any upstream PR is opened.

Cycle execution

This cycle was produced by Jelleo's continuous, hypothesis-driven Solana audit loop. Every finding originates as a falsifiable invariant claim from a per-protocol hypothesis library, dispatched to Layer 1 multi-agent recon, promoted on contested verdicts via Layer 1.5 adversarial debate, and confirmed empirically through a Layer 2 cargo test proof-of-concept. Layer 2.5 triage classifies each fire as STRONG / SOFT / FALSE / LOST; only STRONG cluster representatives advance to confirmed and appear in §01 above. SOFT and STRONG duplicates land in triaged; FALSE fires return to new. Lifecycle: new → triaged → confirmed → disclosed → fixed → verified. Every cycle is signed Ed25519 against the platform key — see the cover-page receipt.

Hypotheses
64
from class library
PoC fires
28
28 fires triaged
STRONG
8
STRONG 8 · SOFT 20 · FALSE 0 · LOST 0
Root causes
8
8 STRONG → 8 after curation
Confirmed
8
reach this report

Non-fire accounting36 hypotheses tested but PoC did not fire — 25× rejected, 11× new. These are hypotheses where Layer 1 / Layer 1.5 returned a verdict but the Layer 2 PoC author either declined to produce a test (no plausible attack) or the test ran without an abort in the target module.

Cycle wall-clock135m 0s

§ B.1 — Cycle funnel. Hypotheses tested → PoC fires → Layer 2.5 judge filters out artifactual / mis-invariant fires → surviving STRONG fires cluster by code site → cluster representatives become published findings.

C — Audit artifacts

All cycle artifacts are persisted on disk and verifiable independently of this report. The table below lists the canonical paths under the cycle workspace so a reviewer can re-execute every layer or recompute the cycle Merkle root.

ArtifactPath (relative to workspace)
Cycle summary (manifest of every step)hunts/<cycle>/hunt_summary.json
Per-step event loghunts/<cycle>/hunt.log.jsonl
Layer 2.5 triage verdictshunts/<cycle>/triage.jsonl
Layer 2 PoC sources (Rust)hunts/<cycle>/poc/test_<slug>.rs
Layer 2 PoC run logshunts/<cycle>/poc/cargo_<slug>.log
Layer 3 Kani harnesses + verdictshunts/<cycle>/kani/<slug>/
Layer 4 LiteSVM exploit testshunts/<cycle>/litesvm/<slug>/
Layer P3 fix bundles (patch.diff + evidence/ + manifest.json)recon/bundles/<finding_id>/
Narrative writeups (per finding)hunts/<cycle>/narratives/<hyp_id>.md
Cycle Merkle root (tamper-evidence)hunts/<cycle>/merkle.json
Findings DB (SQLite)findings.db
Ed25519 public key for receipt verificationhttps://jelleo.com/keys/jelleo.ed25519.pub

D — Disclaimers

Findings in this report reflect the state of the engine source at the commit hash on the cover page. Subsequent changes to the codebase are not analyzed. The report is not a guarantee of code correctness or security: it documents invariants that fired (or held) under the hypothesis library applied during this cycle. Out-of-scope items are listed in §00.1 (Scope). Findings flagged by Layer 2.5 as SOFT / FALSE / LOST are retained in the audit trail (§03) but are not published findings; readers should not interpret a bundle in §03 as a confirmed vulnerability unless its finding row is also present in §01.

Communication channel: security@jelleo.com (PGP key on jelleo.com/security.html). Coordinated disclosure follows the timeline published in our security policy; pre-disclosure leak protections are enforced at the report level (the --public renderer suppresses confirmed-but-not-disclosed findings).

Methodology spec: docs/methodology/ · Live reference: jelleo.com/methodology.html · Source: github.com/Copenhagen0x/audit-pipeline-cli