Skip to content

fix(ark): stop auto-board boarding away the exit-fee reserve - #189

Closed
CypherBoxLLC wants to merge 1 commit into
fix/ark-exit-reserve-target-stalefrom
fix/ark-autoboard-respects-reserve
Closed

CypherBoxLLC wants to merge 1 commit into
fix/ark-exit-reserve-target-stalefrom
fix/ark-autoboard-respects-reserve

Conversation

@CypherBoxLLC

Copy link
Copy Markdown
Owner

Stacked on #188, which it needs for resolveExitReserveTarget. GitHub will retarget this to main
when #188 merges.

Auto-board is the only thing that moves the on-chain balance without the user asking, and that
balance is where unilateral-exit CPFP fees are paid from. Boarding the wrong amount does not misplace
funds, it disarms the exit.

Defect 1: the hold target ignored the estimate

The estimate of what an exit costs is computed on the exit settings screen and was never
persisted
, so the sync loop could not see it and held only what the user had armed.

With the device figures from 2026-08-20, armed 3,654 against an estimate of 233,120:

old hold = armed = 3,654
surplus  = 233,120 - 3,654 - 1,500 = 227,966   >= 50,000 minimum, so it fires

A user who funds to the recommendation by external deposit gets 227,966 sats boarded straight back
into Ark
, undoing the top-up they made specifically to enable the exit. Nobody has hit this because
nobody has funded a six-figure reserve. Now that the gate asks for one (#188), somebody will.

Defect 2: the unarmed path had no board-minimum guard

The armed path guards correctly. The unarmed path called boardAll unconditionally, and a
sub-minimum balance can never board, so it retried every sync tick forever. The old comment
acknowledged the asymmetry as "the exact old boardAll behavior". This device sits at 6,259 sats
on-chain against a 50,000 minimum, which is exactly that case.

The fix

A pure decision function taking the greater of armed and recommended as the hold target, through
the same helper the gate uses in #188, so the screen and the sync loop can no longer disagree about
what "funded" means.

boardAll keeps its original semantics when there is genuinely nothing to hold, so no dust is
stranded for users who never touched exit funding, but it is now gated on the minimum like everything
else.

The estimate is persisted as arkExitRecommendedReserveSats, written whenever the settings screen
computes it and cleared by clearArkAuth so a stale figure from a deleted wallet cannot linger. It
is derived data: if missing, the hold falls back to the armed value, which is the old behaviour.

Verification

  • 11 tests using the device figures, mutation-checked: restoring either old behaviour fails 4
  • 38 suites, 269 tests green
  • tsc error set byte-identical to origin/main, zero differing lines, none in any touched file

Auto-board is the only thing that moves the on-chain balance without the user
asking, and that balance is where unilateral-exit CPFP fees are paid from.
Boarding the wrong amount does not misplace funds, it disarms the exit.

Two defects, both in the same block.

The hold target was the ARMED reserve only. The estimate of what an exit costs
is computed on the exit settings screen and was never persisted, so the sync
loop could not see it. A user who funds toward a recommendation far above what
they armed would have the difference boarded straight back into Ark, undoing the
top-up they made specifically to enable the exit. With the device figures from
2026-08-20, armed 3,654 and an estimate of 233,120, funding to the
recommendation left 227,966 sats eligible for boarding, comfortably over the
50,000 minimum, so it would have fired. Nobody has hit this yet because nobody
has funded a six-figure reserve. Now that the gate asks for one (#188), somebody
will.

The unarmed path called boardAll with NO board-minimum guard, while the armed
path guarded correctly. A sub-minimum on-chain balance can never board, so it
retried every sync tick forever. The old comment acknowledged the asymmetry as
"the exact old boardAll behavior". This device has 6,259 sats on-chain against a
50,000 minimum, which is precisely that case.

The decision is now a pure function taking the greater of armed and recommended
as the hold target, via the same helper the gate uses (#188), so the screen and
the sync loop can no longer disagree about what "funded" means. boardAll keeps
its original semantics when there is genuinely nothing to hold, so no dust is
stranded for users who never touched exit funding, but it is now gated on the
minimum like everything else.

The estimate is persisted as `arkExitRecommendedReserveSats`, written whenever
the settings screen computes it, and cleared by clearArkAuth so a stale figure
from a deleted wallet cannot linger. It is derived data: if it is missing, the
hold falls back to the armed value, which is the old behaviour.

11 tests using the device figures, mutation-checked: restoring either old
behaviour fails 4 of them.
@CypherBoxLLC
CypherBoxLLC requested a review from Bamskki as a code owner August 19, 2026 23:11
@CypherBoxLLC

Copy link
Copy Markdown
Owner Author

Merge note for whoever lands this.

Expect a conflict with #179 in authStore.ts and sync.ts, and resolve it as follows. I hit it merging into the QA integration branch, which already carries #179.

authStore.ts: #179 deliberately REMOVED the arkExitFeeReserveSats: 0 reset from clearArkAuth, because wiping the armed reserve and the exit destination there was a live fund-safety bug. This branch adds arkExitRecommendedReserveSats: null next to that line, which is correct against current main but not once #179 lands. Take #179's side and drop both resets. A stale estimate only makes auto-board hold MORE on-chain, which is the safe direction, and it is recomputed the next time the settings screen opens.

sync.ts: keep #179's ensureArkOnchainHandle import and simply add decideAutoBoard alongside it. This branch still shows getArkOnchainHandle because it predates that fix.

Verified with both resolutions applied: 50 suites, 442 tests green, tsc unchanged.

@CypherBoxLLC

Copy link
Copy Markdown
Owner Author

Landed in main via #208, which merged the device-tested integration branch as a unit.

This PR did not auto-close because its base was another feature branch rather than main, so GitHub never evaluated it against main. Retargeting is refused with "There are no new commits between base branch 'main' and head branch", which confirms the commits are already reachable from main.

Closing as merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant