feat: M012 CosmWasm dynamic-supply contract with unit tests#70
feat: M012 CosmWasm dynamic-supply contract with unit tests#70brawlaphant wants to merge 2 commits intoregen-network:mainfrom
Conversation
Implements the Fixed Cap Dynamic Supply mechanism (M012 SPEC) as a CosmWasm smart contract. Covers: - Hard-capped supply with configurable cap (default 221M REGEN) - Algorithmic mint (regrowth) from cap headroom: M[t] = r * (C - S[t]) - Phase-gated effective multiplier (staking vs stability, M014 integration) - Ecological multiplier (v0 disabled, ready for v1 oracle) - Supply state machine: Transition -> Dynamic -> Equilibrium (with shock reversion) - Admin controls for regrowth rate, M014 phase, ecological toggle, equilibrium params - Query endpoints: supply state, params, period history, simulate - 29 unit tests covering all 20 SPEC acceptance tests and security invariants Follows the same structure and patterns as the M013 fee-router contract. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request implements a dynamic supply CosmWasm contract with mint/burn logic driven by staking, stability, and ecological multipliers. It includes administrative controls and comprehensive tests. Feedback focuses on preventing potential panics by using checked arithmetic and ratio operations, and improving maintainability by removing unused error variants and state phases.
| state.total_minted += effective_mint; | ||
| state.total_burned += effective_burn; |
There was a problem hiding this comment.
The cumulative totals total_minted and total_burned are updated using the += operator, which will panic on overflow. While Uint128 has a very large range, it is best practice in CosmWasm to use checked_add to handle potential overflows gracefully, especially for values that accumulate over the lifetime of the contract.
| state.total_minted += effective_mint; | |
| state.total_burned += effective_burn; | |
| state.total_minted = state.total_minted.checked_add(effective_mint).map_err(StdError::overflow)?; | |
| state.total_burned = state.total_burned.checked_add(effective_burn).map_err(StdError::overflow)?; |
| effective_burn - effective_mint | ||
| }; | ||
| let would_reach_equilibrium = diff < params.equilibrium_threshold | ||
| && state.consecutive_equilibrium_periods + 1 >= params.equilibrium_periods_required; |
There was a problem hiding this comment.
The addition state.consecutive_equilibrium_periods + 1 could theoretically overflow if consecutive_equilibrium_periods reaches u64::MAX. Although this is extremely unlikely in practice, using checked_add is a safer pattern for state-dependent calculations.
| && state.consecutive_equilibrium_periods + 1 >= params.equilibrium_periods_required; | |
| && state.consecutive_equilibrium_periods.checked_add(1).unwrap_or(u64::MAX) >= params.equilibrium_periods_required; |
| let ratio = Decimal::from_ratio(staked, current_supply); | ||
| let raw = Decimal::one() + ratio; |
There was a problem hiding this comment.
Decimal::from_ratio will panic if the resulting ratio exceeds the maximum value of a Decimal (approx 3.4e20). If staked is significantly larger than current_supply, this could cause a panic. Since the result is clamped to MAX_MULTIPLIER (2.0) anyway, checking if staked >= current_supply first avoids the risk of overflow.
if staked >= current_supply {
return MAX_MULTIPLIER;
}
let ratio = Decimal::from_ratio(staked, current_supply);
let raw = Decimal::one() + ratio;| let ratio = Decimal::from_ratio(stability_committed, current_supply); | ||
| let raw = Decimal::one() + ratio; |
There was a problem hiding this comment.
Similar to the staking multiplier, Decimal::from_ratio can panic if stability_committed is much larger than current_supply. Checking the bounds before calculation prevents potential panics.
if stability_committed >= current_supply {
return MAX_MULTIPLIER;
}
let ratio = Decimal::from_ratio(stability_committed, current_supply);
let raw = Decimal::one() + ratio;| let ratio = delta / reference_value; | ||
| if ratio >= Decimal::one() { | ||
| Decimal::zero() | ||
| } else { | ||
| Decimal::one() - ratio | ||
| } |
There was a problem hiding this comment.
The division delta / reference_value can panic if the result exceeds the capacity of a Decimal. It is safer to check if delta >= reference_value first, which also simplifies the logic since the multiplier is floored at zero.
| let ratio = delta / reference_value; | |
| if ratio >= Decimal::one() { | |
| Decimal::zero() | |
| } else { | |
| Decimal::one() - ratio | |
| } | |
| if delta >= reference_value { | |
| Decimal::zero() | |
| } else { | |
| let ratio = delta / reference_value; | |
| Decimal::one() - ratio | |
| } |
| #[error("Burn amount must be greater than zero")] | ||
| ZeroBurnAmount {}, | ||
|
|
||
| #[error("Mint amount must be greater than zero")] | ||
| ZeroMintAmount {}, | ||
|
|
||
| #[error("Ecological reference value must be greater than zero")] | ||
| ZeroReferenceValue {}, | ||
|
|
||
| #[error("Invalid M014 phase: {phase}")] | ||
| InvalidPhase { phase: String }, | ||
|
|
||
| #[error("Mint would exceed hard cap (supply {supply} + mint {mint} > cap {cap})")] | ||
| MintExceedsCap { | ||
| supply: String, | ||
| mint: String, | ||
| cap: String, | ||
| }, | ||
|
|
||
| #[error("Burn amount {burn} exceeds current supply {supply}")] | ||
| BurnExceedsSupply { burn: String, supply: String }, |
| #[cw_serde] | ||
| pub enum SupplyPhase { | ||
| /// Legacy inflationary PoS (before M012 activation) | ||
| Inflationary, |
Summary
contracts/dynamic-supply/S[t+1] = S[t] + M[t] - B[t]withM[t] = r * (C - S[t])wherer = r_base * effective_multiplier * ecological_multipliertarget/andCargo.lockto.gitignorefor Rust build artifactsTest coverage (29 tests, all passing)
Covers all 20 SPEC acceptance tests:
Test plan
cargo test— 29 tests passingcargo wasm— compiles to wasm32-unknown-unknown🤖 Generated with Claude Code