When a dispute is resolved, the loser's bond is slashed and the winning counterparty must claim their share via Action::AddBondInvoice. But the order can reach a terminal status (success, canceledbyadmin, …) while that bond is still in PendingPayout. If the winner restores their session at that point, the restore response contains neither the order (excluded by its terminal status), nor the dispute (excluded because it is no longer active), nor the pending bond payout (restore never queries the bonds table).
The client ends up with no context for the order, so it ignores the add-bond-invoice messages the daemon keeps re-sending every tick. The winner can never submit an invoice and the payout is eventually lost to Forfeited when the claim window elapses.
This is reproducible from two different routes, both of which slash a bond and then drop the order/dispute into
a terminal/inactive status:
- admin-settle → order ends as
success
- admin-cancel → order ends as
canceledbyadmin, dispute as SellerRefunded
Steps to reproduce
- Complete a sell order up to
active, with both maker and taker bonds locked.
- Open a dispute; admin takes it and resolves it (settle or cancel). Loser's bond →
PendingPayout, winner is asked for a payout invoice (add-bond-invoice).
- Let the order reach its terminal status:
- settle path: hold invoice settled + release paid → order becomes
success
- cancel path:
admin_cancel_action sets order to CanceledByAdmin, dispute to SellerRefunded
- As the winner, send a
RestoreSession.
Observed: restore response is empty for this trade — the order/dispute are not listed and no pending bond payout
is signaled. The daemon keeps emitting add-bond-invoice for the order, but the client no longer tracks it and
discards them. The user can never claim; the bond eventually forfeits.
Expected: restore surfaces the pending bond-payout claim so the winner can submit their invoice and receive the
slashed-bond share.
Root cause
Restore-session is order-status-centric, while the bond payout has its own state machine in the bonds table, decoupled from order/dispute status:
find_user_orders_by_master_key (src/db.rs) filters out EXCLUDED_ORDER_STATUSES, which includes 'success', 'canceledbyadmin', 'settledbyadmin', 'completedbyadmin'.
find_user_disputes_by_master_key only restores 'initiated'/'in-progress' disputes; a resolved dispute (settled, SellerRefunded, …) is excluded.
RestoreSessionInfo only carries restore_orders + restore_disputes. It has no notion of pending bond payouts, so a PendingPayout bond whose recipient is the restoring user is invisible to recovery.
Both resolution paths reach this through bond::apply_bond_resolution(... BondSlashReason::LostDispute) (called from admin_settle and admin_cancel), which transitions the loser's bond to PendingPayout and triggers add-bond-invoice. The order/dispute then land in statuses the restore queries exclude. The trade is "done" from the order's point of view, yet the winner is still owed a slashed-bond share, and that outstanding claim is exactly what restore drops.
Possible solutions
- (Preferred) Surface pending bond payouts in restore. Add to
RestoreSessionInfo a list of PendingPayout bonds where the restoring master key is the resolved recipient (resolve_payout_recipient), regardless of order/dispute status. The client rebuilds context and can answer add-bond-invoice.
- Re-include terminal orders that have a pending payout. Join
bonds in the restore order query so terminal-status orders are returned when a PendingPayout bond targets the user. Cheaper, but the client still has to reconstruct the claim from the order.
- Re-trigger the request on restore. On restore, immediately re-emit
add-bond-invoice (or call request_payout_invoice) for the user's pending payouts instead of waiting for the next scheduler tick.
Because the bug reaches this state via multiple routes (admin-settle and admin-cancel) and affects every terminal status that can coexist with a PendingPayout bond, partial fixes based only on tweaking the excluded-status list are insufficient. Option 1 addresses the root cause: restore must surface pending bond-payout obligations, not just order/dispute status.
When a dispute is resolved, the loser's bond is slashed and the winning counterparty must claim their share via
Action::AddBondInvoice. But the order can reach a terminal status (success,canceledbyadmin, …) while that bond is still inPendingPayout. If the winner restores their session at that point, the restore response contains neither the order (excluded by its terminal status), nor the dispute (excluded because it is no longer active), nor the pending bond payout (restore never queries thebondstable).The client ends up with no context for the order, so it ignores the
add-bond-invoicemessages the daemon keeps re-sending every tick. The winner can never submit an invoice and the payout is eventually lost toForfeitedwhen the claim window elapses.This is reproducible from two different routes, both of which slash a bond and then drop the order/dispute into
a terminal/inactive status:
successcanceledbyadmin, dispute asSellerRefundedSteps to reproduce
active, with both maker and taker bonds locked.PendingPayout, winner is asked for a payout invoice (add-bond-invoice).successadmin_cancel_actionsets order toCanceledByAdmin, dispute toSellerRefundedRestoreSession.Observed: restore response is empty for this trade — the order/dispute are not listed and no pending bond payout
is signaled. The daemon keeps emitting
add-bond-invoicefor the order, but the client no longer tracks it anddiscards them. The user can never claim; the bond eventually forfeits.
Expected: restore surfaces the pending bond-payout claim so the winner can submit their invoice and receive the
slashed-bond share.
Root cause
Restore-session is order-status-centric, while the bond payout has its own state machine in the
bondstable, decoupled from order/dispute status:find_user_orders_by_master_key(src/db.rs) filters outEXCLUDED_ORDER_STATUSES, which includes'success','canceledbyadmin','settledbyadmin','completedbyadmin'.find_user_disputes_by_master_keyonly restores'initiated'/'in-progress'disputes; a resolved dispute (settled,SellerRefunded, …) is excluded.RestoreSessionInfoonly carriesrestore_orders+restore_disputes. It has no notion of pending bond payouts, so aPendingPayoutbond whose recipient is the restoring user is invisible to recovery.Both resolution paths reach this through
bond::apply_bond_resolution(... BondSlashReason::LostDispute)(called fromadmin_settleandadmin_cancel), which transitions the loser's bond toPendingPayoutand triggersadd-bond-invoice. The order/dispute then land in statuses the restore queries exclude. The trade is "done" from the order's point of view, yet the winner is still owed a slashed-bond share, and that outstanding claim is exactly what restore drops.Possible solutions
RestoreSessionInfoa list ofPendingPayoutbonds where the restoring master key is the resolved recipient (resolve_payout_recipient), regardless of order/dispute status. The client rebuilds context and can answeradd-bond-invoice.bondsin the restore order query so terminal-status orders are returned when aPendingPayoutbond targets the user. Cheaper, but the client still has to reconstruct the claim from the order.add-bond-invoice(or callrequest_payout_invoice) for the user's pending payouts instead of waiting for the next scheduler tick.Because the bug reaches this state via multiple routes (admin-settle and admin-cancel) and affects every terminal status that can coexist with a
PendingPayoutbond, partial fixes based only on tweaking the excluded-status list are insufficient. Option 1 addresses the root cause: restore must surface pending bond-payout obligations, not just order/dispute status.