@@ -4436,6 +4436,104 @@ test('unstake is rejected when called during the prepare phase', () => {
44364436 expect ( result . value ) . toBe ( errorCodes . ERR_UNSTAKE_IN_PREPARE_PHASE ) ;
44374437} ) ;
44384438
4439+ /**
4440+ * Regression for stacks-network/stacks-core#7295. `unstake-sbtc` mutates
4441+ * next-cycle bond / signer shares, and the next-cycle signer set is frozen
4442+ * during the current cycle's prepare phase. The other share-mutating
4443+ * entry-points (`stake`, `stake-update`, `register-for-bond`,
4444+ * `update-bond-registration`) all gate on `verify-not-prepare-phase`;
4445+ * `unstake-sbtc` previously side-stepped it. After the fix it returns
4446+ * `ERR_STAKE_IN_PREPARE_PHASE` mid-prepare and succeeds once the next
4447+ * cycle starts.
4448+ */
4449+ test ( 'unstake-sbtc is rejected during the prepare phase' , ( ) => {
4450+ const signer = testSigner . identifier ;
4451+ const aliceSbtc = 100000n ;
4452+ const unstakeAmount = 40000n ;
4453+
4454+ registerSigner ( ) ;
4455+ setupBondForAllowlist ( [ { maxSats : aliceSbtc , staker : alice } ] ) ;
4456+ txOk (
4457+ pox5 . registerForBond ( {
4458+ bondIndex : 0n ,
4459+ signerManager : signer ,
4460+ amountUstx : stxToUStx ( 50_000 ) ,
4461+ btcLockup : err ( aliceSbtc ) ,
4462+ signerCalldata : null ,
4463+ } ) ,
4464+ alice ,
4465+ ) ;
4466+
4467+ // Cycle 0 prepare phase begins at (cycle-1-start - 10). Land mid-prepare.
4468+ mineUntil ( rov ( pox5 . rewardCycleToBurnHeight ( 1n ) ) - 9n ) ;
4469+ expect ( rov ( pox5 . isInPreparePhase ( rov ( pox5 . currentPoxRewardCycle ( ) ) ) ) ) . toBe (
4470+ true ,
4471+ ) ;
4472+
4473+ expect (
4474+ txErr (
4475+ pox5 . unstakeSbtc ( {
4476+ signerManager : signer ,
4477+ amountToWithdrawalSats : unstakeAmount ,
4478+ } ) ,
4479+ alice ,
4480+ ) . value ,
4481+ ) . toBe ( pox5Errors . ERR_STAKE_IN_PREPARE_PHASE ) ;
4482+
4483+ // Crossing into the next cycle clears the prepare phase: the same call
4484+ // now succeeds, confirming the guard was the sole blocker.
4485+ mineUntil ( rov ( pox5 . rewardCycleToBurnHeight ( 1n ) ) ) ;
4486+ expect ( rov ( pox5 . isInPreparePhase ( rov ( pox5 . currentPoxRewardCycle ( ) ) ) ) ) . toBe (
4487+ false ,
4488+ ) ;
4489+ txOk (
4490+ pox5 . unstakeSbtc ( {
4491+ signerManager : signer ,
4492+ amountToWithdrawalSats : unstakeAmount ,
4493+ } ) ,
4494+ alice ,
4495+ ) ;
4496+ } ) ;
4497+
4498+ /**
4499+ * Regression for stacks-network/stacks-core#7295. `announce-l1-early-exit`
4500+ * also mutates next-cycle bond / signer shares (zeros the staker's
4501+ * `amount-sats`, debits `protocol-bonds-total-staked`, and rewrites the
4502+ * per-cycle bond-share maps via `remove-staker-from-bond-cycles`), so it
4503+ * must reject during the current cycle's prepare phase too. Simnet can't
4504+ * register a real L1-lock bond (fake burn header hashes fail
4505+ * `ERR_INVALID_BTC_HEADER`), so we exercise the guard on an sBTC bond:
4506+ * pre-fix the call falls through to `ERR_CANNOT_ANNOUNCE_L1_EARLY_UNLOCK`
4507+ * at the `is-l1-lock` assertion; post-fix the prepare-phase guard fires
4508+ * first.
4509+ */
4510+ test ( 'announce-l1-early-exit is rejected during the prepare phase' , ( ) => {
4511+ const signer = testSigner . identifier ;
4512+ const aliceSbtc = 100000n ;
4513+
4514+ registerSigner ( ) ;
4515+ setupBondForAllowlist ( [ { maxSats : aliceSbtc , staker : alice } ] ) ;
4516+ txOk (
4517+ pox5 . registerForBond ( {
4518+ bondIndex : 0n ,
4519+ signerManager : signer ,
4520+ amountUstx : stxToUStx ( 50_000 ) ,
4521+ btcLockup : err ( aliceSbtc ) ,
4522+ signerCalldata : null ,
4523+ } ) ,
4524+ alice ,
4525+ ) ;
4526+
4527+ mineUntil ( rov ( pox5 . rewardCycleToBurnHeight ( 1n ) ) - 9n ) ;
4528+ expect ( rov ( pox5 . isInPreparePhase ( rov ( pox5 . currentPoxRewardCycle ( ) ) ) ) ) . toBe (
4529+ true ,
4530+ ) ;
4531+
4532+ expect ( txErr ( pox5 . announceL1EarlyExit ( alice , signer ) , alice ) . value ) . toBe (
4533+ pox5Errors . ERR_STAKE_IN_PREPARE_PHASE ,
4534+ ) ;
4535+ } ) ;
4536+
44394537/**
44404538 * After the bond period ends, an sBTC bond participant should still be able to
44414539 * retrieve their locked sBTC.
0 commit comments