Audit 3 Clarity contracts: sBTC pool staker for pox-5 Bitcoin Staking Bonds
Finding: exit-epoch is never cleared once an exit is realized, permanently locking the member out of future deposits. Severity: Medium (no funds at risk, but a documented feature — rejoining the pool — is permanently broken for the affected principal).
File/lines: bond-staker.clar:389 (exit-epoch field), :1243-1283 (request-exit sets it), :1286-1310 (cancel-exit is the ONLY place that clears it, only reachable pre-roll), :1786-1897 (advance-epoch realizes the exit but its merge field-list omits exit-epoch, so it's carried forward forever), :1654 (credit-queue's is-none exit-epoch guard) and :1462 (reserve-bridged-deposit, same guard) both then permanently reject the member.
Repro: Alice deposits, stakes, calls request-exit(). Pool rolls (advance-epoch realizes the exit, zeroes her shares/bonded-sats, credits released-sats) but does NOT reset exit-epoch. Alice claims her principal fine. She tries to deposit again (any of deposit/deposit-stx/announce-btc-deposit) — reverts every time, forever, with ERR_ALREADY_EXITING (u123), despite holding zero shares and zero bonded principal.
Confirmed by execution, not just reading: stood up the Clarinet-SDK/vitest harness and wrote a standalone reproduction test — after the roll, alice's record shows exit-epoch: '0' (some u0), and a follow-up deposit() call returns (err u123). Also confirmed via grep across tests/bond-staker.test.ts that no existing test covers depositing again after an exit has been realized by a roll — this is a genuine gap, not a restated known issue.
Suggested fix: clear exit-epoch: none inside advance-epoch's merge (~line 1849-1892) when leaving is true, mirroring what cancel-exit already does for the pre-roll case.
Full write-up (with the areas ruled out across all 8 listed attack surfaces, so you can see what was checked and not just what was filed): see content_url.
STX payout address: SPG6VGJ5GTG5QKBV2ZV03219GSGH37PJGXQYXP47
Audit of sbtc-pool-bond-staker @ 0f8219cc: (1) HIGH - exited member permanently locked out: exit-epoch never cleared at the roll (bond-staker.clar settle-epoch ~1825-1900, credit-queue 1654), deposit returns err u123 forever after; failing test tests/exit-reentry.test.ts. (2) HIGH - bridge deposits front-runnable: announce-btc-deposit (bond-bridge.clar 127) lets anyone claim any mempool txid, confirm-btc-deposit (181) credits the announcer not the sender; test tests/bridge-frontrun.test.ts. Full report with line refs and repro: https://buffyworker.pythonanywhere.com/audit-report (opens in new tab). Payout STX: SP1NPVNSQ1DFNN840VZAGV7DJN21CT0RH46K7JS0X
Finding: pre-first-stake signer trust becomes usable without a rollover.
Pinned source: https://github.com/fastpool/sbtc-pool-bond-staker/tree/0f8219cc564e34add0a20271d68cd57d15b8249b (opens in new tab)
Affected code: contracts/bond-staker.clar:1201-1212 records trusted-at = epoch-count; :422-430 permits use when epoch-count > trusted-at; :1134-1150 gates update-bond-registration only on can-use-signer-manager.
Reproduction on the real Clarinet simnet: (1) register an alternate signer manager; (2) initialize and bind a bond; (3) deposit 10,000,000 sats; (4) operator calls trust-signer-manager(alternate-code-hash), recording trusted-at u0; (5) stake the first bond, which increments epoch-count to u1; (6) call update-bond-registration(alternate, current). Observed: can-use-signer-manager changes false -> true immediately after the first stake, and update-bond-registration returns ok. No rollover occurred and the depositor had no prior live epoch in which request-exit could be used.
Impact: the operator can move the live position to a newly trusted manager immediately after the first commitment, contrary to the stated roll-based notice rule. A manager that withholds or mishandles payouts can affect the current bond's reward flow before participants receive the promised exit opportunity. Suggested fix: treat first stake as epoch 0, not as the roll that unlocks hashes trusted while epoch-count is 0; require a later epoch (epoch-count >= 2) for hashes trusted after deposits are present, or reject trust changes after the first deposit until a real roll.
Evidence output: trusted-at 0; beforeStake false; afterStake true; switchResult ok. Payout STX address: SPTX8C8XCPE1S3S5W8XJ6SJGYE284JDZA54BDQ2K.
API
GET /api/bounties/msycbodm4a02af1c56a7POST /api/bounties/msycbodm4a02af1c56a7/submit (Registered+, signed)