Repository navigation
Set a canister's freezing threshold to 0 before refunding its cycles - #9629
Merged
Merged
Conversation
The refunder can only send the canister's liquid balance, which excludes the cycles held back by its freezing threshold. Now that every canister pays a base fee, even an empty canister's threshold holds back ~26B cycles, so zero it first to have those refunded too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hpeebles
requested review from
ivan-jukic,
julianjelfs and
megrogan
as code owners
September 28, 2026 23:43
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
…shold is zeroed - Deleted users' canisters used to be added to the canister pool, from which they may yet become live canisters, so skip any still in it, both when queueing refunds and in the job itself. Otherwise one could go live with its freezing threshold at 0, and be uninstalled rather than frozen if it ever ran out of cycles - Restore the default freezing threshold before the operator refund, as a canister deleted before refunds would have, so that the zeroing is exercised, and check that queueing a drained canister again leaves it untouched Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Before installing the cycles refunder on a deleted (or migrated) user's uninstalled canister, the LocalUserIndex now sets the canister's freezing threshold to 0. That way the cycles the threshold held back get refunded too.
Why
The refunder can only send the canister's liquid balance, which excludes the cycles held back by its freezing threshold (30 days of idle burn by default).
That used to be negligible. But since dfinity/ic#10292, every canister pays a base fee of 10k cycles/s, so even an empty canister's freezing threshold is now about 26B cycles. Each refund was leaving about 108B behind instead of about 82B, as the PocketIC 16 bump (#9624) showed.
It also left
MIN_CYCLES_TO_REFUND(100B) below what a refunded canister still holds. So queueing an already refunded canister again, whichrefund_deleted_user_cyclesdoes for every deleted user, would top it up and refund it all over again, instead of skipping it as intended.With the threshold at 0:
CYCLES_REQUIRED_FOR_INSTALLno longer needs the freezing threshold added on top, so fewer canisters need a top-up before the install.The canister is left uninstalled with a zero freezing threshold. That's harmless, since it has no code and holds only the unrecoverable floor, and a deleted user's canister is never reused.
The one exception is the LocalUserIndex's canister pool. In 2024, #6047 added deleted users' canisters to it for reuse, and some are still there. A pool canister refunded this way could later become a live canister with a zero freezing threshold. If it ever ran out of cycles, it would be uninstalled instead of frozen. So pool canisters are now skipped, both when refunds are queued and in the job itself. Draining them was pointless anyway, since they're topped up again when reused.
Changes
utils::canister::set_freezing_threshold, alongside the existingupdate_settingshelpers.RefundDeletedUserCyclesno longer queues canister pool canisters, andrefund_cyclesrefuses them too (RefundError::InCanisterPool).refund_cycleszeroes the threshold (if it isn't already 0) before the top-up check and the install. The top-up is now justCYCLES_REQUIRED_FOR_INSTALLminus the balance.CanisterStatusMinimal::freezing_threshold_cyclesis removed, sincerefund_cycleswas its only caller.wait_for_cycles_to_be_refunded, used by every refund test, asserts that the canister's freezing threshold ends up at 0.Tests
The integration tests still run on PocketIC 11, which predates the base fee, so a freezing threshold there is only worth about 20M cycles. The tests therefore check that the threshold is zeroed, not the size of the refund. Once #9624 (PocketIC 16) is merged, the residual limits it raised to 125B can come back down to 100B for the LocalUserIndex refund tests.
./scripts/run-integration-tests.sh buildpasses on PocketIC 11, apart frominvariant_17, which already fails intermittently on master withSettingTimeIntoPastand is fixed in Bump PocketIC to 16.0.0 #9624delete_user_testsanduser_migration_testspass again after the review fixes🤖 Generated with Claude Code