Reproduced Exploit

Bearcave — L2 never learns about new sleepers, so 3 prizes stick

addToParty pushes new sleeper ids on L1 and does not send a message that would push them on L2. wakeSleeper on L2 reverts once numUsed equals the stale sleepoors.length.

Jun 2023•Other•logic•4 min read

Chain

Other

Category

logic

Date

Jun 2023

Source

AuditVault

EVM Playground

Source-level debugger — step opcodes and Solidity in sync

evm-hack-analyzer

The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.

Loading fork state…

Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 20609-c-01-state-is-not-properly-updated-on-l2-leading-to-stuck-pr. Standalone Foundry PoC and full write-up: 20609-c-01-state-is-not-properly-updated-on-l2-leading-to-stuck-pr_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/logic/state-update · vuln/logic/missing-check · vuln/dos/frozen-funds

Reproduction: a self-contained Foundry PoC that compiles and runs offline with only forge-std — no fork and no RPC. Trace: output.txt. PoC: test/20609-c-01-state-is-not-properly-updated-on-l2-leading-to-stuck-pr_exp.sol.


Key info#

ImpactHIGH — N = 3 prize NFTs, worth 300 PRIZE, cannot be woken
ProtocolBearcave HibernationDen
Vulnerable codewakeSleeper stops at party.sleepoors.length
Bug classaddToParty updates L1 and never messages L2
FindingPashov Audit Group — BearCave, 1 June 2023 · C-01 · id 20609
Report2023-06-01-BearCave
SourceAuditVault finding
StatusReproduced on one chain as the L2 den. No second chain.
Compilersolc 0.8.27, cancun, via IR

TL;DR#

addToParty pushes new sleeper ids on L1 and does not send a message that would push them on L2. wakeSleeper on L2 reverts once numUsed equals the stale sleepoors.length.

The L2 party was synced with 7 sleepers. Three more prize NFTs are held by the den. After the 7 are paid, the 8th wake reverts PartyAlreadyWoke. N = 3 NFTs stay owned by the den and 300 PRIZE stays with them.

Mirroring the three ids onto the L2 array lets all 10 wakes succeed and pays 1,000 PRIZE.

The vulnerable code#

SOLIDITY
function wakeSleeper(uint256 bundleId_) external {
    Party storage party = parties[bundleId_];
    // @> VULN: L2 length was never updated, so numUsed == length bricks the rest
    if (party.numUsed == party.sleepoors.length) revert PartyAlreadyWoke(bundleId_);
    uint256 id = party.sleepoors[party.numUsed];
    party.numUsed += 1;
    // pay the prize and move the NFT
}

The report quotes that exact check. addToParty on L1 only does sleepoors.push.

Root cause#

The L2 length is a copy of the party. addToParty changes the L1 copy and does not change the L2 copy. Every later calculation that trusts party.sleepoors.length on L2, including the "already woke" guard, is short by the sleepers that were added after the last message.

Preconditions#

  • L2 was synced when the party had 7 sleepers.
  • Three more prize NFTs are in the den's custody, each with 100 PRIZE.
  • addToParty has recorded those three ids on L1 only.

Attack walkthrough#

  1. Seed L2 with ids 1 through 7.
  2. Mint ids 8, 9 and 10 to the L2 den and fund 1,000 PRIZE.
  3. Call addToParty on L1 with those three ids. L1 length becomes 3. L2 length stays 7.
  4. wakeSleeper succeeds 7 times and pays 700 PRIZE.
  5. The 8th call reverts. Ids 8, 9 and 10 are still owned by the den. 300 PRIZE is left.

Control: mirror pushes the same three ids onto L2. Ten wakes pay 1,000 PRIZE and the den's balance is 0.

Diagrams#

flowchart TD A["L1 addToParty pushes 3 ids"] --> B["No message is sent to L2"] B --> C["L2 sleepoors.length stays 7"] C --> D["Seven wakes pay 700 PRIZE"] D --> E["numUsed equals the stale length"] E --> F["8th wake reverts and N = 3 prizes stay"]

Impact#

The three late sleepers cannot be claimed. 300 PRIZE and the three NFTs remain in the den. Every sleeper added after the last L2 sync is stuck the same way.

Remediation#

addToParty must send the cross-chain message that pushes the same ids into party.sleepoors on L2 before anyone relies on that length.

Sources#

Taxonomy#

  • vuln/logic/state-update · vuln/logic/missing-check · vuln/dos/frozen-funds

Sources & further analysis#

Reproductions & code

Alerts & third-party analyses

  • Web3Sec X hacked database: search.
  • Rekt leaderboard: search.
  • Solodit incident search: search.

These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.