feat: M010 reputation-signal + M012 dynamic-supply CosmWasm contracts#79
feat: M010 reputation-signal + M012 dynamic-supply CosmWasm contracts#79
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>
Implements the M010 Reputation Signal mechanism as a CosmWasm smart contract for Regen Network. Includes signal submission with activation delay, challenge/dispute lifecycle (submit, resolve, escalate), admin invalidation, v0 decay-weighted score computation, bond enforcement, arbiter management, and 38 passing unit tests covering all spec acceptance criteria. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…into merge/m010-m012-contracts # Conflicts: # .gitignore
…into merge/m010-m012-contracts # Conflicts: # .gitignore
There was a problem hiding this comment.
Code Review
This pull request introduces two new CosmWasm contracts: dynamic-supply, which implements an algorithmic token supply mechanism based on staking and ecological metrics, and reputation-signal, which provides a decentralized reputation system with a challenge-dispute lifecycle. The review feedback identifies several critical issues, including incorrect dependency versions in Cargo.toml and the use of non-deterministic floating-point arithmetic in reputation scoring. Additionally, the feedback suggests improving state safety by using saturating arithmetic, ensuring proper contract versioning with cw2, optimizing query efficiency through better indexing of active challenges, and standardizing entry point attributes for library compatibility.
| cosmwasm-std = "2.2" | ||
| cosmwasm-schema = "2.2" | ||
| cw-storage-plus = "2.0" | ||
| schemars = "0.8" | ||
| serde = { version = "1.0", default-features = false, features = ["derive"] } | ||
| thiserror = "2" |
There was a problem hiding this comment.
The versions specified for several dependencies are incorrect and will likely cause the build to fail:
cosmwasm-std&cosmwasm-schema: Version2.2does not exist on crates.io. The latest stable version is2.0.4.cw-storage-plus: Version2.0does not exist. The latest is1.2.0.thiserror: Version2is invalid. It should be1.0.
Additionally, the cw2 dependency is missing. It's required for setting the contract version with cw2::set_contract_version, which is crucial for managing upgrades.
| cosmwasm-std = "2.2" | |
| cosmwasm-schema = "2.2" | |
| cw-storage-plus = "2.0" | |
| schemars = "0.8" | |
| serde = { version = "1.0", default-features = false, features = ["derive"] } | |
| thiserror = "2" | |
| cosmwasm-std = "2.0.4" | |
| cosmwasm-schema = "2.0.4" | |
| cw-storage-plus = "1.2.0" | |
| cw2 = "1.1.1" | |
| schemars = "0.8" | |
| serde = { version = "1.0", default-features = false, features = ["derive"] } | |
| thiserror = "1.0" |
| cosmwasm-schema = "2.2" | ||
| cosmwasm-std = "2.2" | ||
| cw-storage-plus = "2.0" | ||
| cw2 = "2.0" | ||
| schemars = "0.8" | ||
| serde = { version = "1.0", default-features = false, features = ["derive"] } | ||
| thiserror = "2.0" |
There was a problem hiding this comment.
The versions specified for several dependencies are incorrect and will likely cause the build to fail:
cosmwasm-std&cosmwasm-schema: Version2.2does not exist on crates.io. The latest stable version is2.0.4.cw-storage-plus&cw2: Version2.0does not exist. The latest versions are1.2.0and1.1.1respectively.thiserror: Version2.0is invalid. It should be1.0.
| cosmwasm-schema = "2.2" | |
| cosmwasm-std = "2.2" | |
| cw-storage-plus = "2.0" | |
| cw2 = "2.0" | |
| schemars = "0.8" | |
| serde = { version = "1.0", default-features = false, features = ["derive"] } | |
| thiserror = "2.0" | |
| cosmwasm-schema = "2.0.4" | |
| cosmwasm-std = "2.0.4" | |
| cw-storage-plus = "1.2.0" | |
| cw2 = "1.1.1" | |
| schemars = "0.8" | |
| serde = { version = "1.0", default-features = false, features = ["derive"] } | |
| thiserror = "1.0" |
| // v0 scoring: decay-weighted average of endorsement_level/5 (no stake weighting) | ||
| // score = sum(decay * endorsement_level / 5) / sum(decay) | ||
| // decay = exp(-lambda * age_seconds) where lambda = ln(2) / half_life_seconds | ||
| let half_life = config.decay_half_life_seconds as f64; | ||
| let lambda = (2.0_f64).ln() / half_life; | ||
|
|
||
| let mut w_sum: f64 = 0.0; | ||
| let mut d_sum: f64 = 0.0; | ||
| let mut contributing: u32 = 0; | ||
|
|
||
| for id in &ids { | ||
| if let Ok(signal) = SIGNALS.load(deps.storage, *id) { | ||
| if !signal.status.contributes_to_score() { | ||
| continue; | ||
| } | ||
| contributing += 1; | ||
|
|
||
| let age_secs = now.seconds().saturating_sub(signal.submitted_at.seconds()) as f64; | ||
| let decay = (-lambda * age_secs).exp(); | ||
| let w = signal.endorsement_level as f64 / 5.0; | ||
|
|
||
| w_sum += w * decay; | ||
| d_sum += decay; | ||
| } | ||
| } | ||
|
|
||
| let score_0_1 = if d_sum > 0.0 { w_sum / d_sum } else { 0.0 }; | ||
| // Scale to 0-1000 | ||
| let score = (score_0_1 * 1000.0).round().min(1000.0) as u64; |
There was a problem hiding this comment.
The query_reputation_score function uses f64 floating-point arithmetic for calculating the score. Floating-point math can be non-deterministic across different machine architectures, which is a critical issue for blockchain applications that require all nodes to reach the same state. All calculations should be performed using the deterministic cosmwasm_std::Decimal type.
Since Decimal does not support ln() or exp(), you may need to use pre-calculated constants for lambda or use a Taylor series approximation for the exponential decay function.
| pub fn instantiate( | ||
| deps: DepsMut, | ||
| _env: Env, | ||
| info: MessageInfo, | ||
| msg: InstantiateMsg, | ||
| ) -> Result<Response, ContractError> { |
There was a problem hiding this comment.
The instantiate function is missing a call to cw2::set_contract_version. Setting the contract name and version is a best practice that is essential for managing contract upgrades and migrations. You will also need to add use cw2::set_contract_version; to the imports and the cw2 crate to your Cargo.toml.
| pub fn instantiate( | |
| deps: DepsMut, | |
| _env: Env, | |
| info: MessageInfo, | |
| msg: InstantiateMsg, | |
| ) -> Result<Response, ContractError> { | |
| pub fn instantiate( | |
| deps: DepsMut, | |
| _env: Env, | |
| info: MessageInfo, | |
| msg: InstantiateMsg, | |
| ) -> Result<Response, ContractError> { | |
| cw2::set_contract_version(deps.storage, "dynamic-supply", env!("CARGO_PKG_VERSION"))?; |
|
|
||
| // Apply supply adjustment: S[t+1] = S[t] + M[t] - B[t] | ||
| let supply_before = state.current_supply; | ||
| let supply_after_mint = state.current_supply + mint_amount; |
There was a problem hiding this comment.
The + operator is used for Uint128 arithmetic. This can lead to an overflow if the sum exceeds the maximum value of Uint128, causing it to wrap around. This could result in incorrect supply calculations. It's safer to use saturating_add to prevent this.
| let supply_after_mint = state.current_supply + mint_amount; | |
| let supply_after_mint = state.current_supply.saturating_add(mint_amount); |
| state.total_minted += effective_mint; | ||
| state.total_burned += effective_burn; |
There was a problem hiding this comment.
The += operator is used for Uint128 arithmetic, which can overflow and wrap around. This could lead to incorrect tracking of total minted and burned tokens. It's safer to use saturating_add to prevent this.
| state.total_minted += effective_mint; | |
| state.total_burned += effective_burn; | |
| state.total_minted = state.total_minted.saturating_add(effective_mint); | |
| state.total_burned = state.total_burned.saturating_add(effective_burn); |
| fn query_active_challenges( | ||
| deps: Deps, | ||
| start_after: Option<u64>, | ||
| limit: Option<u32>, | ||
| ) -> StdResult<ActiveChallengesResponse> { | ||
| let limit = limit.unwrap_or(30).min(100) as usize; | ||
| let start = start_after.map(|s| s + 1).unwrap_or(0); | ||
|
|
||
| let mut challenges = Vec::new(); | ||
| // Iterate challenges from start | ||
| for result in CHALLENGES | ||
| .range(deps.storage, Some(Bound::inclusive(start)), None, Order::Ascending) | ||
| { | ||
| let (_, challenge) = result?; | ||
| if matches!(challenge.outcome, ChallengeOutcome::Pending) { | ||
| challenges.push(challenge); | ||
| if challenges.len() >= limit { | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Ok(ActiveChallengesResponse { challenges }) | ||
| } |
There was a problem hiding this comment.
The query_active_challenges function iterates over the CHALLENGES map, which stores all challenges ever created, and filters for pending ones. As the number of challenges grows, this query will become very inefficient and may exceed gas limits.
A more scalable approach is to maintain a separate index of only the active (pending) challenges. For example:
- Create a new storage map for pending challenges:
pub const PENDING_CHALLENGES: Map<u64, ()> = Map::new("pending_challenges"); - In
exec_submit_challenge, add the newchallenge_idto this map. - In
exec_resolve_challenge, remove thechallenge_idfrom this map. query_active_challengescan then iterate over the much smallerPENDING_CHALLENGESmap, which would be more gas-efficient.
| // Instantiate | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| #[entry_point] |
There was a problem hiding this comment.
The contract uses the bare #[entry_point] attribute. The recommended best practice is to use #[cfg_attr(not(feature = "library"), entry_point)]. This allows the contract's logic to be imported as a library into other contracts without including the entry points, which can cause linker errors. This pattern is used in the reputation-signal contract and should be used here for consistency. This applies to execute (line 92) and query (line 358) as well.
| #[entry_point] | |
| #[cfg_attr(not(feature = "library"), entry_point)] |
| let signal_id = NEXT_SIGNAL_ID.load(deps.storage)?; | ||
| NEXT_SIGNAL_ID.save(deps.storage, &(signal_id + 1))?; |
There was a problem hiding this comment.
The signal ID is incremented in a non-atomic way (load then save). If an error occurs after the ID is saved but before the function completes successfully, the ID will have been consumed, but no signal created for it, leading to a gap in IDs. A safer pattern is to use Item::update. This also applies to NEXT_CHALLENGE_ID in exec_submit_challenge (lines 357-358).
| let signal_id = NEXT_SIGNAL_ID.load(deps.storage)?; | |
| NEXT_SIGNAL_ID.save(deps.storage, &(signal_id + 1))?; | |
| let signal_id = NEXT_SIGNAL_ID.update(|id| -> StdResult<_> { Ok(id + 1) })?; |
Summary
Merges the M010 (reputation-signal) and M012 (dynamic-supply) CosmWasm contracts from PRs #71 and #70, with .gitignore conflict resolution.
Resolves #71, Resolves #70
Review notes
contracts/reputation-signal/,contracts/dynamic-supply/)contracts/Cargo.tomlpost-merge🤖 Generated with Claude Code