Parent: #4193
Two independent defects found while tracing the recycled-IP scenario. Both create states that cannot be cleaned up by any existing instruction. Neither depends on the rest of the parent issue, and both should be fixed regardless of which direction the reclaim work takes.
1. A closed access pass makes its user account permanently undeletable
process_delete_user hard-requires the access pass account to exist:
// .../processors/user/delete.rs:108-111
if accesspass_account.data_is_empty() {
return Err(DoubleZeroError::AccessPassNotFound.into());
}
The body below it already handles the missing-pass case correctly, guarded by if !accesspass_account.data_is_empty(). The early return makes that branch unreachable. The SDK mirrors the requirement and fails with "You have no Access Pass" before it can even build the instruction (smartcontract/sdk/rs/src/commands/user/delete.rs:60-67).
So if a pass is ever closed while its user account still exists, that account can never be deleted. RequestBan does not close the account either, it only marks it Banned — which still occupies the PDA and still blocks every future connect at that IP. Banning a stale account does not free the address.
CloseAccessPass refuses when connection_count != 0, which usually prevents this ordering, but that counter drifts (see below) and uses saturating_sub, so it is a reachable state rather than a theoretical one.
Fix: drop the early return and let the existing optional branch handle it, relaxing the PDA assertion accordingly. Consider whether RequestBan should close the account rather than leaving a permanent squatter on the PDA.
2. RequestBan never decrements the access pass, so the pass can never be closed
process_request_ban_user does not take the access pass account at all (.../processors/user/requestban.rs:57-80). It deallocates resources and moves the user to Banned, but accesspass.connection_count keeps counting a connection that no longer exists.
process_close_access_pass refuses to close a pass with a nonzero count:
// .../processors/accesspass/close.rs
if accesspass.connection_count != 0 {
msg!("AccessPass has {} active connections, cannot close", accesspass.connection_count);
return Err(DoubleZeroError::AccessPassInUse.into());
}
Every ban therefore leaves an access pass that can never be closed, and inflates the count that seat accounting and the Connected / Disconnected status both read.
Fix: have RequestBan take the access pass account and run the same release path DeleteUser does — decrement connection_count, remove_user, release_feed_seats, recompute status. Decide separately whether existing drifted passes need a one-off repair instruction or an admin path to force a close.
Testing verification
- Delete a user whose access pass has been closed; it succeeds and settles device counters.
- Ban a user, then close its access pass; the close succeeds and seat counts are correct.
- Ban then delete still settles counters exactly once, with no double decrement.
Parent: #4193
Two independent defects found while tracing the recycled-IP scenario. Both create states that cannot be cleaned up by any existing instruction. Neither depends on the rest of the parent issue, and both should be fixed regardless of which direction the reclaim work takes.
1. A closed access pass makes its user account permanently undeletable
process_delete_userhard-requires the access pass account to exist:The body below it already handles the missing-pass case correctly, guarded by
if !accesspass_account.data_is_empty(). The early return makes that branch unreachable. The SDK mirrors the requirement and fails with "You have no Access Pass" before it can even build the instruction (smartcontract/sdk/rs/src/commands/user/delete.rs:60-67).So if a pass is ever closed while its user account still exists, that account can never be deleted.
RequestBandoes not close the account either, it only marks itBanned— which still occupies the PDA and still blocks every future connect at that IP. Banning a stale account does not free the address.CloseAccessPassrefuses whenconnection_count != 0, which usually prevents this ordering, but that counter drifts (see below) and usessaturating_sub, so it is a reachable state rather than a theoretical one.Fix: drop the early return and let the existing optional branch handle it, relaxing the PDA assertion accordingly. Consider whether
RequestBanshould close the account rather than leaving a permanent squatter on the PDA.2.
RequestBannever decrements the access pass, so the pass can never be closedprocess_request_ban_userdoes not take the access pass account at all (.../processors/user/requestban.rs:57-80). It deallocates resources and moves the user toBanned, butaccesspass.connection_countkeeps counting a connection that no longer exists.process_close_access_passrefuses to close a pass with a nonzero count:Every ban therefore leaves an access pass that can never be closed, and inflates the count that seat accounting and the
Connected/Disconnectedstatus both read.Fix: have
RequestBantake the access pass account and run the same release pathDeleteUserdoes — decrementconnection_count,remove_user,release_feed_seats, recompute status. Decide separately whether existing drifted passes need a one-off repair instruction or an admin path to force a close.Testing verification