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/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..8868227 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,220 @@ 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 ────────────────────────── + // 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(), + &fee_router::msg::ExecuteMsg::CollectFee { + tx_type: fee_router::msg::TxType::MarketplaceTrade, + value: trade_value, + }, + &[Coin::new(calc.fee_amount.u128(), DENOM)], + ) + .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); +} 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(())