From a2e45f9436ca5fa70af5662256189f24bd95f778 Mon Sep 17 00:00:00 2001 From: brawlaphant <35781613+brawlaphant@users.noreply.github.com> Date: Sat, 11 Apr 2026 11:59:42 -0700 Subject: [PATCH 1/3] fix(contracts): resolve clippy errors + wire 3 orphaned contracts into workspace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Repairs two pre-existing CI breakages that have been blocking every PR against main: 1. `cargo clippy --workspace -- -D warnings` failed on Rust 1.94 with 17 errors across 6 contracts — pre-existing lint issues from before the toolchain auto-upgraded. Fixed mechanically with no behavior change. 2. `reputation-signal`, `dynamic-supply`, and `fee-router` were present as sibling package directories under `contracts/` but NOT listed in `contracts/Cargo.toml`'s `workspace.members` array. The packages were in limbo — cargo refused to run their tests standalone (refusing with "current package believes it's in a workspace when it's not"), AND they were invisible to workspace-scoped commands like `cargo test --workspace` and `cargo build --release --target wasm32-unknown-unknown --workspace`. The result was that ~80 unit tests (38 in reputation-signal, 29 in dynamic-supply, 13 in fee-router) were running nowhere. This PR adds them to the workspace. Combined impact: - Contracts CI clippy job: FAIL → PASS - Contracts CI test job: 98 tests → 178 tests (+80) - Contracts CI wasm build job: 6 contracts → 9 contracts (+3) - Future integration tests can now depend on the three previously-orphaned contracts (they were already valid CosmWasm contracts, just not visible to the workspace) ## Clippy fixes (mechanical, no behavior change) attestation-bonding/src/contract.rs - Line 545, 571: `start_after.map(|s| cw_storage_plus::Bound::exclusive(s))` → `start_after.map(cw_storage_plus::Bound::exclusive)` (redundant_closure) - Line 552, 553, 576: `.map_or(true, |x| ...)` → `.is_none_or(|x| ...)` (unnecessary_map_or — modern Rust idiom) credit-class-voting/src/contract.rs - Line 560: `execute_update_config` has 8 parameters; added `#[allow(clippy::too_many_arguments)]` above the fn declaration (all 8 are legitimately independent optional fields on the config) - Line 653, 680: redundant closure on `Bound::exclusive` contribution-rewards/src/contract.rs - Line 390: `execute_record_activity` has 8 parameters; added `#[allow(clippy::too_many_arguments)]` marketplace-curation/src/contract.rs - Line 847: redundant closure on `Bound::exclusive` - Line 863: `&coll.curator != c` comparing a reference to a reference → `coll.curator != *c` (op_ref on left operand) service-escrow/src/contract.rs - Line 767: `execute_update_config` has 11 parameters; added `#[allow(clippy::too_many_arguments)]` - Line 916: `value < MIN_BOND_RATIO || value > MAX_BOND_RATIO` → `!(MIN_BOND_RATIO..=MAX_BOND_RATIO).contains(&value)` (manual_range_contains — modern Rust idiom) reputation-signal/src/contract.rs - Line 162: `exec_submit_signal` has 8 parameters; added `#[allow(clippy::too_many_arguments)]` - Line 173: `endorsement_level < 1 || endorsement_level > 5` → `!(1..=5).contains(&endorsement_level)` (manual_range_contains) - Line 537: `exec_update_config` has 9 parameters; added `#[allow(clippy::too_many_arguments)]` - Line 632: `config.arbiters.retain(|a| a != &addr)` → `config.arbiters.retain(|a| *a != addr)` (op_ref on right operand) ## Workspace wiring contracts/Cargo.toml: added `"dynamic-supply"`, `"fee-router"`, `"reputation-signal"` to `workspace.members` in alphabetical order between existing entries. No change to `workspace.dependencies`. ## Validation Ran locally on `rustc 1.94.0 (85eff7c80 2026-01-15)` with the same commands CI uses: - `cargo clippy --workspace -- -D warnings` — PASS (0 errors) - `cargo test --workspace` — 178 passed, 0 failed - `cargo build --release --target wasm32-unknown-unknown --workspace` — all 9 contracts compile, release profile, ~55s cold Every behavior change in this PR is a mechanical refactor that the Rust compiler can prove equivalent (clippy's suggestions are peephole transformations). No state machine, no fee math, no access control was touched. - Lands in: `contracts/` - Changes: fix 17 clippy errors + add 3 orphaned contracts to workspace.members - Validate: `cd contracts && cargo test --workspace && cargo clippy --workspace -- -D warnings` --- contracts/Cargo.toml | 3 +++ contracts/attestation-bonding/src/contract.rs | 10 +++++----- contracts/contribution-rewards/src/contract.rs | 1 + contracts/credit-class-voting/src/contract.rs | 5 +++-- contracts/marketplace-curation/src/contract.rs | 4 ++-- contracts/reputation-signal/src/contract.rs | 6 ++++-- contracts/service-escrow/src/contract.rs | 3 ++- 7 files changed, 20 insertions(+), 12 deletions(-) diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 13c72d7..018ca81 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -3,8 +3,11 @@ members = [ "attestation-bonding", "contribution-rewards", "credit-class-voting", + "dynamic-supply", + "fee-router", "integration-tests", "marketplace-curation", + "reputation-signal", "service-escrow", "validator-governance", ] diff --git a/contracts/attestation-bonding/src/contract.rs b/contracts/attestation-bonding/src/contract.rs index b11af31..cc37e3f 100644 --- a/contracts/attestation-bonding/src/contract.rs +++ b/contracts/attestation-bonding/src/contract.rs @@ -542,15 +542,15 @@ fn query_attestations( start_after: Option, limit: Option, ) -> StdResult { let limit = limit.unwrap_or(DEFAULT_QUERY_LIMIT).min(MAX_QUERY_LIMIT) as usize; - let start = start_after.map(|s| cw_storage_plus::Bound::exclusive(s)); + let start = start_after.map(cw_storage_plus::Bound::exclusive); let attester_addr = attester.map(|a| deps.api.addr_validate(&a)).transpose()?; let attestations: Vec<_> = ATTESTATIONS .range(deps.storage, start, None, Order::Ascending) .filter_map(|item| item.ok()) .filter(|(_, a)| { - status.as_ref().map_or(true, |s| a.status == *s) - && attester_addr.as_ref().map_or(true, |addr| a.attester == *addr) + status.as_ref().is_none_or(|s| a.status == *s) + && attester_addr.as_ref().is_none_or(|addr| a.attester == *addr) }) .take(limit) .map(|(_, a)| a) @@ -568,12 +568,12 @@ fn query_challenges( deps: Deps, attestation_id: Option, start_after: Option, limit: Option, ) -> StdResult { let limit = limit.unwrap_or(DEFAULT_QUERY_LIMIT).min(MAX_QUERY_LIMIT) as usize; - let start = start_after.map(|s| cw_storage_plus::Bound::exclusive(s)); + let start = start_after.map(cw_storage_plus::Bound::exclusive); let challenges: Vec<_> = CHALLENGES .range(deps.storage, start, None, Order::Ascending) .filter_map(|item| item.ok()) - .filter(|(_, c)| attestation_id.map_or(true, |aid| c.attestation_id == aid)) + .filter(|(_, c)| attestation_id.is_none_or(|aid| c.attestation_id == aid)) .take(limit) .map(|(_, c)| c) .collect(); diff --git a/contracts/contribution-rewards/src/contract.rs b/contracts/contribution-rewards/src/contract.rs index 5eeafa5..9d90270 100644 --- a/contracts/contribution-rewards/src/contract.rs +++ b/contracts/contribution-rewards/src/contract.rs @@ -387,6 +387,7 @@ fn execute_claim_matured( .add_attribute("total", total_return)) } +#[allow(clippy::too_many_arguments)] fn execute_record_activity( deps: DepsMut, info: MessageInfo, diff --git a/contracts/credit-class-voting/src/contract.rs b/contracts/credit-class-voting/src/contract.rs index eecc486..a35129f 100644 --- a/contracts/credit-class-voting/src/contract.rs +++ b/contracts/credit-class-voting/src/contract.rs @@ -557,6 +557,7 @@ fn execute_finalize_proposal( // ── Update Config ───────────────────────────────────────────────────── +#[allow(clippy::too_many_arguments)] fn execute_update_config( deps: DepsMut, info: MessageInfo, @@ -650,7 +651,7 @@ fn query_proposals( limit: Option, ) -> StdResult { let limit = limit.unwrap_or(DEFAULT_QUERY_LIMIT).min(MAX_QUERY_LIMIT) as usize; - let start = start_after.map(|s| cw_storage_plus::Bound::exclusive(s)); + let start = start_after.map(cw_storage_plus::Bound::exclusive); let proposals: Vec = PROPOSALS .range(deps.storage, start, None, Order::Ascending) @@ -677,7 +678,7 @@ fn query_proposals_by_admin( ) -> StdResult { let admin_addr = deps.api.addr_validate(&admin_address)?; let limit = limit.unwrap_or(DEFAULT_QUERY_LIMIT).min(MAX_QUERY_LIMIT) as usize; - let start = start_after.map(|s| cw_storage_plus::Bound::exclusive(s)); + let start = start_after.map(cw_storage_plus::Bound::exclusive); let proposals: Vec = PROPOSALS .range(deps.storage, start, None, Order::Ascending) diff --git a/contracts/marketplace-curation/src/contract.rs b/contracts/marketplace-curation/src/contract.rs index f1a0a4e..704365c 100644 --- a/contracts/marketplace-curation/src/contract.rs +++ b/contracts/marketplace-curation/src/contract.rs @@ -844,7 +844,7 @@ fn query_collections( limit: Option, ) -> StdResult { let limit = limit.unwrap_or(DEFAULT_QUERY_LIMIT).min(MAX_QUERY_LIMIT) as usize; - let start = start_after.map(|s| cw_storage_plus::Bound::exclusive(s)); + let start = start_after.map(cw_storage_plus::Bound::exclusive); let curator_addr = curator .map(|c| deps.api.addr_validate(&c)) @@ -860,7 +860,7 @@ fn query_collections( } } if let Some(ref c) = curator_addr { - if &coll.curator != c { + if coll.curator != *c { return None; } } diff --git a/contracts/reputation-signal/src/contract.rs b/contracts/reputation-signal/src/contract.rs index 7bd05d5..8327935 100644 --- a/contracts/reputation-signal/src/contract.rs +++ b/contracts/reputation-signal/src/contract.rs @@ -159,6 +159,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { // Execute handlers // --------------------------------------------------------------------------- +#[allow(clippy::too_many_arguments)] fn exec_submit_signal( deps: DepsMut, env: Env, @@ -170,7 +171,7 @@ fn exec_submit_signal( evidence: Evidence, ) -> Result { // Validate endorsement level 1-5 - if endorsement_level < 1 || endorsement_level > 5 { + if !(1..=5).contains(&endorsement_level) { return Err(ContractError::InvalidEndorsementLevel { level: endorsement_level, }); @@ -534,6 +535,7 @@ fn exec_invalidate_signal( .add_attribute("rationale", &rationale)) } +#[allow(clippy::too_many_arguments)] fn exec_update_config( deps: DepsMut, info: MessageInfo, @@ -629,7 +631,7 @@ fn exec_remove_arbiter( } let addr = deps.api.addr_validate(&address)?; - config.arbiters.retain(|a| a != &addr); + config.arbiters.retain(|a| *a != addr); CONFIG.save(deps.storage, &config)?; Ok(Response::new() diff --git a/contracts/service-escrow/src/contract.rs b/contracts/service-escrow/src/contract.rs index 5e67a7d..4656f0d 100644 --- a/contracts/service-escrow/src/contract.rs +++ b/contracts/service-escrow/src/contract.rs @@ -764,6 +764,7 @@ fn execute_cancel( Ok(resp) } +#[allow(clippy::too_many_arguments)] fn execute_update_config( deps: DepsMut, info: MessageInfo, arbiter_dao: Option, community_pool: Option, @@ -913,7 +914,7 @@ fn remaining_escrow(agreement: &ServiceAgreement) -> Uint128 { } fn validate_bond_ratio(value: u64) -> Result<(), ContractError> { - if value < MIN_BOND_RATIO || value > MAX_BOND_RATIO { + if !(MIN_BOND_RATIO..=MAX_BOND_RATIO).contains(&value) { return Err(ContractError::BondRatioOutOfRange { value, min: MIN_BOND_RATIO, max: MAX_BOND_RATIO }); } Ok(()) From a2dffdf77355eea679b4ebac4aa82f55c2b96e7e Mon Sep 17 00:00:00 2001 From: brawlaphant <35781613+brawlaphant@users.noreply.github.com> Date: Sat, 11 Apr 2026 12:54:21 -0700 Subject: [PATCH 2/3] =?UTF-8?q?test(integration):=20M013=20fee-router=20?= =?UTF-8?q?=E2=86=92=20M012=20dynamic-supply=20burn=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new integration test that exercises the canonical Economic Reboot flow: fees are collected by M013 fee-router, the burn portion accumulates in its burn pool, and an off-chain aggregator hands that burn amount to M012 dynamic-supply as the burn_amount argument to ExecutePeriod. The two contracts do not message each other on-chain in v0 — but their data contract at the boundary must be bit-exact. This is now possible because PR #90 wired both fee-router and dynamic-supply into the workspace members list. Before that, they could not be imported by integration-tests. ## What this test pins 1. **Workspace coexistence** — both contracts deploy in the same App and instantiate without collisions. 2. **Fee Conservation inside m013** — after collecting a fee, the burn share lands in `burn_pool` and matches the dry-run calculation from `CalculateFee`. Specifically: burn + validator + community + agent == fee_amount A 10B uregen marketplace trade at the default 1% rate produces fee 100M, split 30M/40M/25M/5M (pins the Model A 30/40/25/5 distribution). 3. **Supply conservation inside m012** — after ExecutePeriod runs: current_supply = supply_before + total_minted - total_burned The test asserts this identity directly rather than hardcoding exact supply numbers (which would pin the regrowth math as a side effect). The identity must hold regardless of the effective multiplier's numerical value. 4. **m013 → m012 hand-off is bit-exact** — the burn amount m012 records via `total_burned` equals the burn amount we read from m013's `pools.burn_pool`. No off-by-one, no rounding drift. This is the single most important assertion in the test — if the two contracts' uregen accounting ever drifts, the Economic Reboot burn ladder breaks silently. ## What this test does NOT do It is not a cross-contract MESSAGE flow — m012 does not call m013 and vice versa. The hand-off is off-chain in v0. A future upgrade that adds a real IBC or Wasm-level query can extend this test to cover that path; for now the test guards the workspace wiring and the uregen data contract at the boundary. ## Cargo.toml change Adds `fee-router` and `dynamic-supply` to `contracts/integration-tests/Cargo.toml` under `[dev-dependencies]`. Both contracts were already in the workspace members list after PR #90. ## Validation $ cd contracts && cargo test --package integration-tests \ test_fee_router_to_dynamic_supply_burn_flow test result: ok. 1 passed; 0 failed The full workspace test suite still passes — 180 tests total after this PR (179 after #101, +1 from this test). - Lands in: `contracts/integration-tests/` - Changes: new M013→M012 burn flow test (222 LOC) + Cargo.toml deps - Validate: `cd contracts && cargo test --workspace` ## PR relationship Based on PR #90 (which wired fee-router and dynamic-supply into the workspace members list). Sibling to #101 (M010/M011 coexistence test). The two integration tests together exercise two of the three conceptual flows in the Economic Reboot design — with the third (M009 escrow → M015 rewards) deferred to a future follow-up. Refs `mechanisms/m013-value-based-fee-routing/SPEC.md` §5 Refs `mechanisms/m012-fixed-cap-dynamic-supply/SPEC.md` §5.3 --- contracts/integration-tests/Cargo.toml | 2 + .../integration-tests/tests/integration.rs | 226 ++++++++++++++++++ 2 files changed, 228 insertions(+) diff --git a/contracts/integration-tests/Cargo.toml b/contracts/integration-tests/Cargo.toml index c38e3ed..697d5e6 100644 --- a/contracts/integration-tests/Cargo.toml +++ b/contracts/integration-tests/Cargo.toml @@ -14,6 +14,8 @@ schemars = { workspace = true } serde = { workspace = true } attestation-bonding = { path = "../attestation-bonding", features = ["library"] } credit-class-voting = { path = "../credit-class-voting", features = ["library"] } +dynamic-supply = { path = "../dynamic-supply", features = ["library"] } +fee-router = { path = "../fee-router", features = ["library"] } marketplace-curation = { path = "../marketplace-curation", features = ["library"] } service-escrow = { path = "../service-escrow", features = ["library"] } contribution-rewards = { path = "../contribution-rewards", features = ["library"] } diff --git a/contracts/integration-tests/tests/integration.rs b/contracts/integration-tests/tests/integration.rs index c765a3b..c95757a 100644 --- a/contracts/integration-tests/tests/integration.rs +++ b/contracts/integration-tests/tests/integration.rs @@ -62,6 +62,22 @@ fn validator_governance_contract() -> Box> { )) } +fn fee_router_contract() -> Box> { + Box::new(ContractWrapper::new( + fee_router::contract::execute, + fee_router::contract::instantiate, + fee_router::contract::query, + )) +} + +fn dynamic_supply_contract() -> Box> { + Box::new(ContractWrapper::new( + dynamic_supply::contract::execute, + dynamic_supply::contract::instantiate, + dynamic_supply::contract::query, + )) +} + // ── App builder helper ──────────────────────────────────────────────── /// Build an App with initial balances for the given addresses. @@ -1726,3 +1742,213 @@ fn test_full_ecosystem_flow() { assert_eq!(state_resp.state.total_active, 3); assert!(state_resp.state.last_compensation_distribution.is_some()); } + +// ═══════════════════════════════════════════════════════════════════════ +// Test 6: M013 fee-router → M012 dynamic-supply burn flow +// ═══════════════════════════════════════════════════════════════════════ +// +// This test exercises the canonical Economic Reboot flow: fees are +// collected by the M013 fee router, the burn portion accumulates in +// the fee router's burn pool, and an aggregator hands that burn +// amount to the M012 dynamic supply contract to apply the burn in +// the next period. The two contracts do not message each other on- +// chain in v0 — the hand-off is off-chain — but the data contract +// at the boundary is exact: the `burn_pool` field in +// `PoolBalancesResponse` must be consumable as the `burn_amount` +// argument to `ExecutePeriod` without conversion, reshape, or +// rounding. +// +// What this test pins: +// +// 1. Both contracts coexist in the same App and are typed +// compatibly (both use `Uint128` uregen amounts). +// +// 2. Fee Conservation inside m013: when a fee is collected, the +// burn share of the fee amount lands in `burn_pool` and matches +// the dry-run calculation from `CalculateFee`. +// +// 3. Supply conservation inside m012: after `ExecutePeriod` runs +// with a non-trivial burn amount, `total_burned` reflects the +// burn, `current_supply` equals `supply_before + mint - burn`, +// and the cap headroom responds correctly. +// +// 4. The m013→m012 hand-off is numerically exact — the burn amount +// the aggregator reads from m013 is the same value m012 applies. +// No off-by-one, no rounding drift. + +#[test] +fn test_fee_router_to_dynamic_supply_burn_flow() { + use cosmwasm_std::Decimal; + + let admin = addr("admin"); + + let mut app = build_app(&[(&admin, 10_000_000_000_000u128)]); + + // ── Deploy fee-router (M013) ────────────────────────────────── + // Fee rates match SPEC Model A: issuance 2%, transfer 0.1%, + // retirement 0.5%, marketplace 1%. Distribution shares are + // 30/40/25/5 (burn / validator / community / agent). + let fr_code_id = app.store_code(fee_router_contract()); + let fr_addr = app + .instantiate_contract( + fr_code_id, + admin.clone(), + &fee_router::msg::InstantiateMsg { + issuance_rate: Decimal::percent(2), + transfer_rate: Decimal::from_ratio(1u128, 1000u128), + retirement_rate: Decimal::from_ratio(5u128, 1000u128), + trade_rate: Decimal::percent(1), + burn_share: Decimal::percent(30), + validator_share: Decimal::percent(40), + community_share: Decimal::percent(25), + agent_share: Decimal::percent(5), + min_fee: Uint128::new(1_000_000), + }, + &[], + "fee-router", + None, + ) + .unwrap(); + + // ── Deploy dynamic-supply (M012) ────────────────────────────── + // Hard cap at 221M REGEN (221T uregen), initial supply 220M — + // below the cap so the instantiate guard `initial_supply <= + // hard_cap` passes. The test then runs one ExecutePeriod with + // a real burn from m013's pool, so the supply trajectory is + // `supply + mint - burn` and we check that identity directly + // rather than pinning an exact supply number (which would + // hardcode the regrowth math). + let ds_code_id = app.store_code(dynamic_supply_contract()); + let ds_addr = app + .instantiate_contract( + ds_code_id, + admin.clone(), + &dynamic_supply::msg::InstantiateMsg { + hard_cap: Uint128::new(221_000_000_000_000u128), + initial_supply: Uint128::new(220_000_000_000_000u128), + base_regrowth_rate: Decimal::from_ratio(2u128, 100u128), + ecological_multiplier_enabled: false, + ecological_reference_value: Decimal::from_ratio(50u128, 1u128), + m014_phase: dynamic_supply::state::M014Phase::Inactive, + equilibrium_threshold: Uint128::new(1_000_000_000_000u128), + equilibrium_periods_required: 12, + }, + &[], + "dynamic-supply", + None, + ) + .unwrap(); + + // ── Step 1: Dry-run a fee calculation ───────────────────────── + // A 10B uregen marketplace trade at the default 1% rate should + // produce a fee of 100M uregen with no min-fee floor applied. + let trade_value = Uint128::new(10_000_000_000u128); + let calc: fee_router::msg::CalculateFeeResponse = app + .wrap() + .query_wasm_smart( + fr_addr.clone(), + &fee_router::msg::QueryMsg::CalculateFee { + tx_type: fee_router::msg::TxType::MarketplaceTrade, + value: trade_value, + }, + ) + .unwrap(); + assert_eq!(calc.fee_amount, Uint128::new(100_000_000)); + assert!(!calc.min_fee_applied); + assert_eq!(calc.burn_amount, Uint128::new(30_000_000)); + assert_eq!(calc.validator_amount, Uint128::new(40_000_000)); + assert_eq!(calc.community_amount, Uint128::new(25_000_000)); + assert_eq!(calc.agent_amount, Uint128::new(5_000_000)); + + // Fee Conservation: burn + validator + community + agent == fee + assert_eq!( + calc.burn_amount + calc.validator_amount + calc.community_amount + calc.agent_amount, + calc.fee_amount + ); + + // ── Step 2: Collect the fee for real ────────────────────────── + app.execute_contract( + admin.clone(), + fr_addr.clone(), + &fee_router::msg::ExecuteMsg::CollectFee { + tx_type: fee_router::msg::TxType::MarketplaceTrade, + value: trade_value, + }, + &[], + ) + .unwrap(); + + // ── Step 3: Read pool balances from m013 ────────────────────── + let pools: fee_router::msg::PoolBalancesResponse = app + .wrap() + .query_wasm_smart(fr_addr.clone(), &fee_router::msg::QueryMsg::PoolBalances {}) + .unwrap(); + assert_eq!(pools.burn_pool, Uint128::new(30_000_000)); + assert_eq!(pools.validator_fund, Uint128::new(40_000_000)); + assert_eq!(pools.community_pool, Uint128::new(25_000_000)); + assert_eq!(pools.agent_infra, Uint128::new(5_000_000)); + + // ── Step 4: Read initial m012 supply state ──────────────────── + let state_before: dynamic_supply::msg::SupplyStateResponse = app + .wrap() + .query_wasm_smart( + ds_addr.clone(), + &dynamic_supply::msg::QueryMsg::SupplyState {}, + ) + .unwrap(); + assert_eq!( + state_before.current_supply, + Uint128::new(220_000_000_000_000u128) + ); + assert_eq!(state_before.total_burned, Uint128::zero()); + assert_eq!(state_before.total_minted, Uint128::zero()); + assert_eq!(state_before.period_count, 0); + // Initial headroom is cap - supply = 1T uregen. + assert_eq!(state_before.cap_headroom, Uint128::new(1_000_000_000_000u128)); + + // ── Step 5: Hand the burn amount from m013 to m012 ──────────── + // This is the off-chain aggregator's job in v0 — an integrator + // reads `pools.burn_pool` from m013 and supplies it as the + // `burn_amount` in m012's `ExecutePeriod` message. The two + // values must be bit-identical at the boundary. + let burn_for_period = pools.burn_pool; + app.execute_contract( + admin.clone(), + ds_addr.clone(), + &dynamic_supply::msg::ExecuteMsg::ExecutePeriod { + burn_amount: burn_for_period, + staked_amount: Uint128::new(100_000_000_000_000u128), + stability_committed: Uint128::zero(), + delta_co2: None, + }, + &[], + ) + .unwrap(); + + // ── Step 6: Verify m012 applied the burn ────────────────────── + let state_after: dynamic_supply::msg::SupplyStateResponse = app + .wrap() + .query_wasm_smart( + ds_addr.clone(), + &dynamic_supply::msg::QueryMsg::SupplyState {}, + ) + .unwrap(); + + // m013 → m012 hand-off is bit-exact: the burn amount m012 + // records matches the burn amount we read from m013. + assert_eq!(state_after.total_burned, burn_for_period); + assert_eq!(state_after.period_count, 1); + + // Supply conservation invariant: + // current_supply = supply_before + total_minted - total_burned + // + // We do NOT hardcode an exact supply number because the regrowth + // math depends on the effective multiplier, which in turn + // depends on the staking input. Instead we assert the identity + // — the supply equation must hold regardless of the mint + // amount, and the total_minted / total_burned counters must + // account for every delta. + let expected_supply = state_before.current_supply + state_after.total_minted + - state_after.total_burned; + assert_eq!(state_after.current_supply, expected_supply); +} From 03236965096ecc265b7d5b8811a7a38d8aef0303 Mon Sep 17 00:00:00 2001 From: brawlaphant Date: Sat, 11 Apr 2026 14:28:38 -0700 Subject: [PATCH 3/3] test(integration): send calculated fee as funds in CollectFee call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Gemini review feedback on PR #102: the CollectFee call passed `&[]` for funds, which both hid the intended integrator call shape and masked any future on-chain funds-validation check. Wire `calc.fee_amount` through the mock bank as `Coin { denom: DENOM, amount: calc.fee_amount }` and annotate that v0 m013 is accounting-only so the funds currently just sit in the contract — but the test will fail closed the day m013 enforces that `info.funds == calculated_fee`. Co-Authored-By: Claude Opus 4.6 (1M context) --- contracts/integration-tests/tests/integration.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/contracts/integration-tests/tests/integration.rs b/contracts/integration-tests/tests/integration.rs index c95757a..8868227 100644 --- a/contracts/integration-tests/tests/integration.rs +++ b/contracts/integration-tests/tests/integration.rs @@ -1867,6 +1867,13 @@ fn test_fee_router_to_dynamic_supply_burn_flow() { ); // ── Step 2: Collect the fee for real ────────────────────────── + // Send the calculated fee as actual funds so an integrator reading + // this test sees the intended call shape: caller transfers the fee + // into the router and the router updates its pool accounting in + // lockstep. v0 m013 is an accounting-only contract (it does not yet + // compare `info.funds` against the calculated fee), but wiring the + // funds through the mock bank ensures the test fails closed the day + // m013 adds on-chain funds validation. app.execute_contract( admin.clone(), fr_addr.clone(), @@ -1874,7 +1881,7 @@ fn test_fee_router_to_dynamic_supply_burn_flow() { tx_type: fee_router::msg::TxType::MarketplaceTrade, value: trade_value, }, - &[], + &[Coin::new(calc.fee_amount.u128(), DENOM)], ) .unwrap();