Summary
The insurance-withdrawal rate limit — the cooldown from F-1 and the deposits-only ceiling from F-2 — can never be active. Both policy fields are assigned once, to zero, at market initialisation, and no instruction in the program can set them. Both enforcement helpers short-circuit on zero, so the checks that #385, #386 and #396 were closed by adding are unreachable in every deployed market.
Mechanism
The two enforcement helpers are present and correct, and each is a no-op when its policy field is zero.
check_insurance_withdraw_cooldown (src/v16_program.rs:9643-9655):
if cooldown_slots > 0 && last_slot != 0 {
let earliest = last_slot.checked_add(cooldown_slots)...;
if now_slot < earliest {
return Err(PercolatorError::InsuranceWithdrawCooldownActive.into());
}
}
apply_insurance_withdraw_ceiling (:9664-9669):
if deposits_only == 0 {
// ceiling not applied
Now the policy fields. insurance_withdraw_deposits_only (:1126) and insurance_withdraw_cooldown_slots (:1133) are each written in exactly one place in the file — the config literal inside handle_init_market (:7597), at :7697 and :7704 respectively:
insurance_withdraw_deposits_only: 0,
...
insurance_withdraw_cooldown_slots: 0,
There is no other assignment to either field anywhere in the program, and no instruction that configures them — there is no ConfigureInsuranceWithdrawPolicy, no SetInsurance*, nothing in the instruction enum that reaches them. InitMarket does not accept them as parameters either, so a market cannot even be created with the policy on.
The deposits-only ceiling is dead twice over. Its budget, insurance_withdraw_deposit_remaining (:1118), is initialised to zero (:7689) and only ever accrues inside if cfg.insurance_withdraw_deposits_only != 0 (:9761-9762) — a branch that can never be taken. So the budget stays zero, and the ceiling that would consume it never applies.
Reproduction
tests/poc_insurance_withdraw_policy_inert.rs, LiteSVM against the real BPF. Passes against main at 93372e4:
cargo test --test poc_insurance_withdraw_policy_inert
test insurance_withdraw_rate_limit_policy_is_permanently_inert ... ok
test result: ok. 1 passed; 0 failed
The test asserts the policy is off at genesis, funds an asset's insurance domain, and then performs two withdrawals in the same slot:
let (cooldown, deposits_only, remaining) = config_policy(&env);
assert_eq!(cooldown, 0, "cooldown_slots is 0 at init and has no setter");
assert_eq!(deposits_only, 0, "deposits_only is 0 at init and has no setter");
assert_eq!(remaining, 0, "the deposits-only budget never accrues");
// ... fund 1,000 into the asset's insurance domain ...
let slot_before = env.svm.get_sysvar::<Clock>().slot;
withdraw_insurance(&mut env, &stake_auth, dest, 600).expect("first withdrawal");
withdraw_insurance(&mut env, &stake_auth, dest, 400)
.expect("second withdrawal in the SAME slot must be blocked by an active cooldown — it is not");
let slot_after = env.svm.get_sysvar::<Clock>().slot;
assert_eq!(slot_before, slot_after, "both withdrawals executed in one slot");
assert_eq!(token_amount(&env.svm, dest), 1_000);
assert_eq!(market_insurance(&env), 0);
Both succeed, the reserve goes to zero in a single slot, and the policy fields are still zero afterwards. (The two amounts differ only so the transactions do not hash identically — LiteSVM rejects a byte-identical retry as AlreadyProcessed, which is a harness artifact rather than program behaviour.)
Impact
Three issues were closed on the strength of this policy being enforced:
The enforcement those closed with is real code on the real path — handle_withdraw_insurance calls the cooldown check at :10771, and handle_withdraw_insurance_asset calls it at :10959 — but it is gated behind a field that nothing can raise above zero. On every market that exists, the runtime behaviour is identical to having no policy at all, which is the state those issues described.
The helper itself is well tested in isolation — :18948-18974 exercises the boundary, the overflow case and the zero case directly — which is part of why the gap is easy to miss: the unit tests prove the function is correct, and correctness of the function was never the issue.
By itself this is a missing defence-in-depth control rather than a direct exploit: the whole reserve can already be withdrawn by whoever legitimately holds the authority, and this only means it can be done in one slot instead of several. What makes it worth raising now is that it removes the bound on other failures. We have separately reported that an asset's asset_admin can seize insurance_operator and insurance_authority without the holder's consent. A rate limit is exactly the control that would cap the damage from a seized authority and give an operator a window to notice; with the policy structurally off, a seizure converts directly into a single-slot drain of the full reserve.
We rate this medium on that basis — low direct severity, but it silently negates a mitigation that three closed issues assume is active.
Suggested direction
The gap is a missing setter rather than a defect in the enforcement, which reads correctly. Options, roughly in ascending order of change:
We have not opened a pull request, because the choice between a default, a setter, and a documented no-op is a policy decision rather than a code one, and a default value would need to come from you. Happy to implement whichever you prefer.
Environment
dcccrypto/percolator-prog main at 93372e4
- Engine
dcccrypto/percolator main at fb1f46d
cargo build-sbf --no-default-features --tools-version v1.52, then cargo test --test poc_insurance_withdraw_policy_inert, rustc 1.97.1
Summary
The insurance-withdrawal rate limit — the cooldown from F-1 and the deposits-only ceiling from F-2 — can never be active. Both policy fields are assigned once, to zero, at market initialisation, and no instruction in the program can set them. Both enforcement helpers short-circuit on zero, so the checks that #385, #386 and #396 were closed by adding are unreachable in every deployed market.
Mechanism
The two enforcement helpers are present and correct, and each is a no-op when its policy field is zero.
check_insurance_withdraw_cooldown(src/v16_program.rs:9643-9655):apply_insurance_withdraw_ceiling(:9664-9669):Now the policy fields.
insurance_withdraw_deposits_only(:1126) andinsurance_withdraw_cooldown_slots(:1133) are each written in exactly one place in the file — the config literal insidehandle_init_market(:7597), at:7697and:7704respectively:There is no other assignment to either field anywhere in the program, and no instruction that configures them — there is no
ConfigureInsuranceWithdrawPolicy, noSetInsurance*, nothing in the instruction enum that reaches them.InitMarketdoes not accept them as parameters either, so a market cannot even be created with the policy on.The deposits-only ceiling is dead twice over. Its budget,
insurance_withdraw_deposit_remaining(:1118), is initialised to zero (:7689) and only ever accrues insideif cfg.insurance_withdraw_deposits_only != 0(:9761-9762) — a branch that can never be taken. So the budget stays zero, and the ceiling that would consume it never applies.Reproduction
tests/poc_insurance_withdraw_policy_inert.rs, LiteSVM against the real BPF. Passes againstmainat93372e4:The test asserts the policy is off at genesis, funds an asset's insurance domain, and then performs two withdrawals in the same slot:
Both succeed, the reserve goes to zero in a single slot, and the policy fields are still zero afterwards. (The two amounts differ only so the transactions do not hash identically — LiteSVM rejects a byte-identical retry as
AlreadyProcessed, which is a harness artifact rather than program behaviour.)Impact
Three issues were closed on the strength of this policy being enforced:
deposits_onlyWithdrawal Ceiling Never Decremented — Policy Provides No Enforcementhandle_withdraw_insurance_asset(follow-up to F-1/F-2)The enforcement those closed with is real code on the real path —
handle_withdraw_insurancecalls the cooldown check at:10771, andhandle_withdraw_insurance_assetcalls it at:10959— but it is gated behind a field that nothing can raise above zero. On every market that exists, the runtime behaviour is identical to having no policy at all, which is the state those issues described.The helper itself is well tested in isolation —
:18948-18974exercises the boundary, the overflow case and the zero case directly — which is part of why the gap is easy to miss: the unit tests prove the function is correct, and correctness of the function was never the issue.By itself this is a missing defence-in-depth control rather than a direct exploit: the whole reserve can already be withdrawn by whoever legitimately holds the authority, and this only means it can be done in one slot instead of several. What makes it worth raising now is that it removes the bound on other failures. We have separately reported that an asset's
asset_admincan seizeinsurance_operatorandinsurance_authoritywithout the holder's consent. A rate limit is exactly the control that would cap the damage from a seized authority and give an operator a window to notice; with the policy structurally off, a seizure converts directly into a single-slot drain of the full reserve.We rate this medium on that basis — low direct severity, but it silently negates a mitigation that three closed issues assume is active.
Suggested direction
The gap is a missing setter rather than a defect in the enforcement, which reads correctly. Options, roughly in ascending order of change:
handle_init_marketalready writes both fields; givinginsurance_withdraw_cooldown_slotsa sensible default would make the existing enforcement live on new markets with no new instruction and no layout change. It does nothing for markets already deployed.marketauth-gated setter. The natural shape, and it covers existing markets. Worth pairing with a lower bound so the policy cannot be set to a value that is nominally on but operationally meaningless — the same concern we raised aboutstale_slotsin [Low] ConfigurePermissionlessResolve has no lower bound on stale_slots — a one-slot window lets a transient oracle gap permanently resolve the market #410.:7697/:7704saying so would be worth having, since the current code reads as an enforced control. In that case [F-1] [High] Insurance Withdrawal Cooldown Policy Not Enforced at Runtime #385, [F-2] [High] deposits_only Withdrawal Ceiling Never Decremented — Policy Provides No Enforcement #386 and Insurance-withdrawal cooldown/deposits-only policy not enforced in handle_withdraw_insurance_asset (follow-up to F-1/F-2) #396 are arguably still open rather than fixed, and we would suggest saying so on those issues.We have not opened a pull request, because the choice between a default, a setter, and a documented no-op is a policy decision rather than a code one, and a default value would need to come from you. Happy to implement whichever you prefer.
Environment
dcccrypto/percolator-progmainat93372e4dcccrypto/percolatormainatfb1f46dcargo build-sbf --no-default-features --tools-version v1.52, thencargo test --test poc_insurance_withdraw_policy_inert, rustc 1.97.1