Skip to content

feat: M015 CosmWasm contribution-rewards contract with unit tests#72

Open
brawlaphant wants to merge 1 commit intoregen-network:mainfrom
brawlaphant:pr/m015-cosmwasm-contract
Open

feat: M015 CosmWasm contribution-rewards contract with unit tests#72
brawlaphant wants to merge 1 commit intoregen-network:mainfrom
brawlaphant:pr/m015-cosmwasm-contract

Conversation

@brawlaphant
Copy link
Copy Markdown
Contributor

Summary

  • Implements M015 Contribution-Weighted Rewards as a CosmWasm smart contract at contracts/contribution-rewards/
  • Replaces passive staking with activity-based distribution from Community Pool, following the M015 SPEC
  • Adds target/ and Cargo.lock to .gitignore for Rust build artifacts

Contract Design

Activity tracking for 5 contribution types with configurable weights (bps):

Activity Default Weight SPEC Reference
Credit Purchase 30% Primary demand signal
Credit Retirement 30% Terminal ecological impact
Platform Facilitation 20% Ecosystem infrastructure
Governance Voting 10% Governance participation
Proposal Submission 10% (conditional) Anti-gaming: full/half/zero based on quorum

Epoch management: Time-bounded reward periods with snapshots, calibration phase before distribution.

Stability tier: Optional commitment mechanism -- 6% annual return, 6-24 month lock, 50% early exit penalty, capped at 30% of community pool inflow.

Mechanism lifecycle: INACTIVE -> TRACKING -> DISTRIBUTING with governance-controlled transitions and circuit breaker.

Security: Transaction dedup via tx_hash, integer arithmetic only (Uint128), admin-gated mutations, all parameters governance-updatable.

Files

File Purpose
Cargo.toml Dependencies: cosmwasm-std 2.2, cw-storage-plus 2.0, cw2 2.0
src/lib.rs Module exports
src/msg.rs InstantiateMsg, ExecuteMsg (12 variants), QueryMsg (9 endpoints)
src/state.rs Config, ContractState, ActivityRecord, StabilityCommitment, storage maps
src/error.rs ContractError enum (14 error variants)
src/contract.rs Instantiate, execute, query handlers + 27 unit tests

Test Coverage (27 tests)

Maps to SPEC acceptance tests AT-1 through AT-22:

  • Activity scoring (AT-1 to AT-6): single activity, all activities, proposal outcomes, zero activity
  • Distribution (AT-7 to AT-11): proportional rewards, stability cap, activity pool invariant
  • Stability tier (AT-12 to AT-16): min amount, lock period bounds, early exit penalty
  • Security (AT-17, AT-19, AT-20): revenue constraint, dedup, governance override
  • State machine (AT-21, AT-22): lifecycle transitions, calibration
  • Additional: pause/resume, simulate score, claim rewards, epoch finalization

All 27 tests pass: cargo test in contracts/contribution-rewards/

Test plan

  • cargo build compiles without errors
  • cargo test -- 27/27 tests pass
  • Review against M015 SPEC for completeness
  • Review integer arithmetic for rounding edge cases
  • Consider integration test scenarios with M013 fee-router

Generated with Claude Code

Implements the Contribution-Weighted Rewards mechanism (M015) as a CosmWasm
smart contract. Replaces passive staking with activity-based distribution
from Community Pool, per the M015 SPEC.

Key features:
- Activity tracking for 5 contribution types (credit purchase, retirement,
  facilitation, governance votes, proposal submission)
- Weighted scoring with configurable bps weights (default 30/30/20/10/10)
- Proposal anti-gaming: full/half/zero credit based on quorum outcome
- Epoch-based reward distribution proportional to activity scores
- Stability tier: 6% annual return, 6-24 month lock, 50% early exit penalty
- Stability cap at 30% of community pool inflow
- Mechanism lifecycle: INACTIVE -> TRACKING -> DISTRIBUTING
- Governance-controlled parameters, circuit breaker (pause/resume)
- Transaction dedup via tx_hash tracking
- Integer arithmetic only (Uint128, no floating point)

27 unit tests covering SPEC acceptance tests AT-1 through AT-22.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the contribution-rewards CosmWasm contract, which implements activity-based reward distribution and a stability tier for long-term commitments. The review identifies critical security and performance concerns, specifically a double-claiming vulnerability in stability rewards and potential gas exhaustion from O(N) iterations during epoch finalization. Further feedback points to missing validation for community pool inflows, a logic error in proposal credit calculations, and potential state bloat in transaction hash tracking.

Comment on lines +1070 to +1073
let existing = PENDING_STABILITY_REWARDS
.may_load(storage, &addr)?
.unwrap_or_default();
PENDING_STABILITY_REWARDS.save(storage, &addr, &(existing + share))?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-critical critical

Stability rewards are being added to both commitment.accrued_rewards and PENDING_STABILITY_REWARDS. Since execute_claim_rewards clears the latter and execute_claim_matured pays out the former, users can claim the same rewards twice. According to the SPEC (Section 6.2), stability rewards should be paid out upon maturity. You should remove the redundant tracking in PENDING_STABILITY_REWARDS.

Comment on lines +301 to +330
let participants: Vec<(String, bool)> = EPOCH_PARTICIPANTS
.prefix(epoch)
.range(deps.storage, None, None, Order::Ascending)
.collect::<StdResult<Vec<_>>>()?;

for (addr_str, _) in &participants {
let record = ACTIVITY_RECORDS.load(deps.storage, (epoch, addr_str.as_str()))?;
let score = compute_weighted_score(&record, &config.activity_weights);
if !score.is_zero() {
total_score += score;
let addr = deps.api.addr_validate(addr_str)?;
participant_scores.push((addr, score));
}
}

// Distribute activity rewards proportionally
if state.mechanism_state == MechanismState::Distributing && !total_score.is_zero() {
for (addr, score) in &participant_scores {
let reward = activity_pool.multiply_ratio(*score, total_score);
if !reward.is_zero() {
let existing = PENDING_ACTIVITY_REWARDS
.may_load(deps.storage, addr.as_str())?
.unwrap_or_default();
PENDING_ACTIVITY_REWARDS.save(
deps.storage,
addr.as_str(),
&(existing + reward),
)?;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This block performs an O(N) operation by iterating over all participants to distribute rewards. This will eventually exceed the block gas limit and prevent epoch finalization. \n\nRecommendation:\n1. Track aggregate activity values (e.g., total_credit_purchase_value) incrementally in execute_record_contribution to calculate total_score in O(1).\n2. Switch to a 'pull' model where users claim rewards for specific epochs, calculating their share on-the-fly.

Comment on lines +1053 to +1074
let commitments: Vec<(String, StabilityCommitment)> = STABILITY_COMMITMENTS
.range(storage, None, None, Order::Ascending)
.filter_map(|item| item.ok())
.filter(|(_, c)| c.state == CommitmentState::Committed)
.collect();

let total_committed: Uint128 = commitments.iter().map(|(_, c)| c.amount).sum();

if total_committed.is_zero() {
return Ok(());
}

for (addr, mut commitment) in commitments {
let share = stability_allocation.multiply_ratio(commitment.amount, total_committed);
commitment.accrued_rewards += share;
STABILITY_COMMITMENTS.save(storage, &addr, &commitment)?;

let existing = PENDING_STABILITY_REWARDS
.may_load(storage, &addr)?
.unwrap_or_default();
PENDING_STABILITY_REWARDS.save(storage, &addr, &(existing + share))?;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Iterating over all stability commitments to update rewards is an O(N) operation that will lead to gas exhaustion. \n\nRecommendation:\nImplement a global reward index (e.g., global_index += epoch_rewards / total_committed). Each commitment should store its own reward_debt to allow O(1) reward calculation when the user interacts with the contract.

Comment on lines +278 to +280
if community_pool_inflow.is_zero() {
return Err(ContractError::ZeroInflow {});
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The contract does not verify that the community_pool_inflow amount matches the actual tokens sent in the transaction. This could allow an admin to record rewards that are not backed by contract balance.

Some(ProposalOutcome::ReachedQuorumFailed) => Uint128::new(50), // 0.5
Some(ProposalOutcome::FailedQuorum) | None => Uint128::zero(), // 0.0
};
record.proposal_credits_x100 += credit_x100;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The value parameter is ignored for ProposalSubmission. If multiple proposals are recorded in a single call, the participant only receives credit for one.

Suggested change
record.proposal_credits_x100 += credit_x100;
record.proposal_credits_x100 += credit_x100 * value;

{
return Err(ContractError::DuplicateContribution { tx_hash });
}
RECORDED_TX_HASHES.save(deps.storage, &tx_hash, &true)?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The RECORDED_TX_HASHES map grows indefinitely, leading to state bloat. Including the epoch in the key would allow for future pruning of old transaction hashes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant