Skip to content

Commit 23950d1

Browse files
madninjaclaude
andauthored
chore(helium-sub-daos): make the top-up mint-once gate falsifiable (#1285)
The gate deciding which issue_rewards_v0 pass mints the epoch's DAO-level backstop top-up is covered by nothing: every suite issues rewards for a single sub-DAO, so num_rewards_issued is 0 on the only pass and the condition is a no-op. Removing it changes no test. Extract it as mints_top_up(is_mobile, num_rewards_issued), which closes crate::TESTING over, delegating to a private mints_top_up_with(is_mobile, testing, num_rewards_issued) that the tests supply directly. TESTING is false under cargo test, so the first-pass arm is reachable no other way; keeping the seam private means the value production runs on is the constant rather than anything a caller chooses. Verified by mutation, one term at a time, with the diff confirmed byte-identical after each restore: drop is_mobile -> a_non_mobile_pass_never_mints invert !testing -> only_the_first_pass_mints_under_testing drop the testing conjunct -> only_the_first_pass_mints_under_testing n == 0 -> n <= 1 -> only_the_first_pass_mints_under_testing literal for crate::TESTING -> the_wrapper_passes_the_build_s_own_testing_value The wrapper test branches on crate::TESTING so each arm asserts what its build promises, and each catches one substituted literal. The suite is green under both cargo test and TESTING=true cargo test. No behaviour change: the expression and the constant it reads are the ones already in issue_rewards_v0. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 7af85df commit 23950d1

2 files changed

Lines changed: 81 additions & 12 deletions

File tree

programs/helium-sub-daos/src/backstop.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,27 @@ pub fn staker_overflow(deployer_pool: u64, deployer_cap_hnt: u64) -> u64 {
289289
deployer_pool.saturating_sub(deployer_cap_hnt)
290290
}
291291

292+
/// Whether this `issue_rewards_v0` pass mints the epoch's backstop top-up.
293+
///
294+
/// The top-up is a DAO-level amount, sized once by `calculate_utility_score_v0` and recorded
295+
/// once in `total_rewards`, so exactly one pass may mint it. `is_mobile` selects that pass in
296+
/// production. Under a `TESTING` build `is_mobile` is every sub-DAO, so the epoch's first pass
297+
/// is used instead: N sub-DAOs would otherwise mint N top-ups against the one the epoch
298+
/// recorded. `num_rewards_issued` is incremented after the mints, so 0 identifies the first.
299+
///
300+
/// In production the Mobile pass mints it whatever position it settles in: `issue_rewards_v0`
301+
/// is permissionless and the passes may arrive in any order.
302+
pub fn mints_top_up(is_mobile: bool, num_rewards_issued: u32) -> bool {
303+
mints_top_up_with(is_mobile, crate::TESTING, num_rewards_issued)
304+
}
305+
306+
/// The same decision over an explicit `testing`, which only the tests below supply: `TESTING`
307+
/// is false under `cargo test`, so the first-pass arm is reachable no other way. Private, so
308+
/// the value production runs on is the constant `mints_top_up` closes over.
309+
fn mints_top_up_with(is_mobile: bool, testing: bool, num_rewards_issued: u32) -> bool {
310+
is_mobile && (!testing || num_rewards_issued == 0)
311+
}
312+
292313
#[cfg(test)]
293314
// HNT amounts are written as whole-HNT then 8 decimals (e.g. 1_644_00000000) for
294315
// readability against the spec's HNT figures; same precedent as the treasury mint
@@ -511,6 +532,64 @@ mod tests {
511532
assert!(staker_overflow(pool, 1) <= pool);
512533
}
513534

535+
#[test]
536+
fn the_mobile_pass_mints_whatever_position_it_settles_in() {
537+
// issue_rewards_v0 is permissionless with no ordering guarantee, so in production the
538+
// Mobile pass may be the epoch's first or arrive after any number of others. 0 is the
539+
// ordinary mainnet case and is asserted here rather than only through the wrapper.
540+
for already_issued in [0u32, 1, 2, 7] {
541+
assert!(
542+
mints_top_up_with(true, false, already_issued),
543+
"num_rewards_issued {already_issued}: the Mobile pass mints the top-up whatever \
544+
order it arrives in"
545+
);
546+
}
547+
}
548+
549+
#[test]
550+
fn only_the_first_pass_mints_under_testing() {
551+
// Under TESTING every sub-DAO is a Mobile pass, and the epoch records one top-up, so
552+
// one pass mints it: the first.
553+
assert!(mints_top_up_with(true, true, 0), "the first pass mints it");
554+
for already_issued in [1u32, 2, 7] {
555+
assert!(
556+
!mints_top_up_with(true, true, already_issued),
557+
"num_rewards_issued {already_issued}: later passes must not mint it again"
558+
);
559+
}
560+
}
561+
562+
#[test]
563+
fn the_wrapper_passes_the_build_s_own_testing_value() {
564+
// mints_top_up closes TESTING over, so this is what pins that it forwards the build's
565+
// own value. Each arm asserts what its build promises, and each catches one substituted
566+
// literal: the default build catches `true`, a TESTING build catches `false`. CI runs
567+
// `cargo test` without TESTING, so the second arm is exercised only locally.
568+
assert!(mints_top_up(true, 0), "the first pass always mints it");
569+
if crate::TESTING {
570+
assert!(
571+
!mints_top_up(true, 3),
572+
"under TESTING only the epoch's first pass mints it"
573+
);
574+
} else {
575+
assert!(
576+
mints_top_up(true, 3),
577+
"in production the Mobile pass mints it whatever order it arrives in"
578+
);
579+
}
580+
assert!(!mints_top_up(false, 0), "and still only the Mobile pass");
581+
}
582+
583+
#[test]
584+
fn a_non_mobile_pass_never_mints() {
585+
// The top-up supports Mobile data deployers, so no other sub-DAO's pass carries it --
586+
// in production that is the whole of the gate.
587+
for already_issued in [0u32, 1, 7] {
588+
assert!(!mints_top_up_with(false, false, already_issued));
589+
assert!(!mints_top_up_with(false, true, already_issued));
590+
}
591+
}
592+
514593
#[test]
515594
fn mobile_share_zero_is_dormant() {
516595
// A sub-DAO taking no share of the emission has no deployer pool for the band to act

programs/helium-sub-daos/src/instructions/issue_rewards_v0.rs

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -235,19 +235,9 @@ pub fn handler(ctx: Context<IssueRewardsV0>, args: IssueRewardsArgsV0) -> Result
235235
// key off the DAO-level delegation_rewards_issued. A zero ceiling (no carrier burn
236236
// this epoch, or no price oracle) disables it.
237237
let is_mobile = TESTING || ctx.accounts.sub_dao.key() == crate::backstop::MOBILE_SUB_DAO;
238-
// The top-up is a DAO-level amount, sized once per epoch and recorded once in
239-
// total_rewards, so exactly one pass may mint it. In production is_mobile already selects
240-
// one pass. Under TESTING it selects every sub-DAO, so pin it to the epoch's first pass
241-
// there (num_rewards_issued is incremented after the mints below): N sub-DAOs would
242-
// otherwise mint N top-ups against the one the epoch recorded.
243-
//
244-
// The `!TESTING ||` is load-bearing and must not be simplified away. This instruction is
245-
// permissionless and the passes may arrive in any order, so in production the Mobile pass
246-
// is not necessarily the first: gating on num_rewards_issued alone would mint no top-up
247-
// whenever any other sub-DAO settled first, silently under-delivering the floor against a
248-
// total_rewards that already counted it.
238+
// Exactly one pass per epoch mints the DAO-level top-up; mints_top_up decides which.
249239
let is_top_up_pass =
250-
is_mobile && (!TESTING || ctx.accounts.dao_epoch_info.num_rewards_issued == 0);
240+
crate::backstop::mints_top_up(is_mobile, ctx.accounts.dao_epoch_info.num_rewards_issued);
251241
let top_up = if is_top_up_pass { backstop_top_up } else { 0 };
252242
// What the escrow mint would be before the cap redirect: the deployer portion of the
253243
// split (rewards_amount - delegator slice) plus the direct top-up.

0 commit comments

Comments
 (0)