Skip to content

Gauges - #928

Open
JakeHartnell wants to merge 14 commits into
developmentfrom
feat/gauges
Open

Gauges#928
JakeHartnell wants to merge 14 commits into
developmentfrom
feat/gauges

Conversation

@JakeHartnell

@JakeHartnell JakeHartnell commented May 12, 2026

Copy link
Copy Markdown
Member

Summary

Rebases the long-dormant gauges-are-cool branch onto current development, drops the cw-orch test-harness migration that PR #875 reverted workspace-wide, and works through the open checklist from #844. Adds a second example adapter and fills in error-path test coverage. 72 tests, all green; fmt + clippy -D warnings clean; release wasm builds for all three contracts.

Status

Inspired by Curve, forked from WyndDAO (Apache-2.0; git history preserved via the LICENSE/NOTICE files in contracts/gauges/). Originally opened as #844 and stalled since 2024 when the workspace drifted out from under it. This PR brings it back to a mergeable state and addresses most of the #844 checklist.

What's in this PR

Rebase + dep drift

  • Rebased onto current development (0178cf55d).
  • Removed cw-orch from gauge-adapter. PR Revert "Finish cw-orch integration" #875 reverted the workspace-wide cw-orch integration; the gauge-adapter's Use cw-denom and payment utils in gauge adapter #847 migration was the last holdout. Rewrote the gauge-adapter multitest on plain cw_multi_test; kept Use cw-denom and payment utils in gauge adapter #847's
    cw-denom + payment-utils refactors.
  • Fixed dao-interface / dao-voting-cw4 / dao-proposal-single API drift in the orchestrator's multitest harness: ModuleInstantiateInfo now requires salt and uses funds: Option<Vec<Coin>>; GroupContract::New gained
    cw4_group_salt; ProposalSingleInstantiateMsg gained delegation_module; dao_interface::msg::InstantiateMsg gained initial_actions.

Audit fixes

  • gauge/src/contract.rs:142GAUGES.load(...).unwrap() inside the member_changed hook handler's or_insert_with closure could panic on a transient storage error. x/cw-hooks treats hook-callback panics as failures toward an
    auto-unregister threshold, so a single error here could brick further tally updates. Switched to ? propagation via an explicit Entry::Vacant insert.
  • gauge/src/contract.rs:971selected_set filter callback used o.as_ref().unwrap() and swallowed storage iterator errors. Refactored so errors flow through the filter and propagate via the existing ? in the downstream map.
  • gauge/src/contract.rs:585 — resolved the existing TODO ("this doesn't seem very safe... double check permissions here. Why is check option optional?"). Split add_option into a public validated add_option (called by
    ExecuteMsg::AddOption — runs adapter CheckOption + nonzero-voting-power anti-spam) and a private add_adapter_options for the trusted bulk-add path during attach_gauge. The bool parameter was easy to misuse by accident.

Small voting power (#844 checklist)

A user with 1 unit of voting power (e.g. one staked NFT) who splits 50/50 across two options had Uint128 * Decimal truncate to 0 on both options — their vote silently erased. Added ContractError::VoteWeightRoundsToZero { weight, voting_power } and an early-reject in place_votes. Regression test in multitest/voting.rs::small_voting_power_rejects_round_to_zero_split.

Bug found while expanding tests

create_gauge skipped the max_available_percentage < 1.0 validation that update_gauge already enforced — a gauge could be born with an invariant-violating config that no later update could repair. Added the check; covered by
multitest/errors.rs::create_gauge_rejects_max_available_percent_at_one.

Test coverage expansion (#844 checklist)

New contracts/gauges/gauge/src/multitest/errors.rs with 12 error-path tests covering the validation surfaces existing tests didn't exercise: create_gauge rejection cases, place_votes rejection cases (TooMuchVotingWeight,
NoVotingPower, OptionDoesNotExists), GaugeMissing on bogus IDs, EpochNotReached on premature execute, ownership-required ops, and hook-caller auth.

Marketing adapter improvements (#844 checklist)

  • New ExecuteMsg::Reject { submission, soft: bool }. Admin-gated per-submission removal. Soft refunds the bond to the original sender; hard forfeits to the community_pool config field. Guards: SubmissionNotFound,
    CannotRejectDefault (prevents rejecting the default community-pool submission). No-op on the bond side if no required_deposit is configured.
  • New AdapterQueryMsg::SubmissionsBySender { sender } query. Returns all submissions whose sender matches the given address — useful for "my submissions" views in registration flows.

Second example adapter (#844 checklist)

New contract: contracts/gauges/budget-allocator/ (gauge-budget-allocator). A minimal adapter that distributes a fixed native-token budget proportional to gauge weights. Admin-curated option list, no bond, native-only — meaningfully
simpler than the marketing adapter. Re-uses gauge-adapter's AdapterQueryMsg / response types as the orchestrator-facing surface (depended on with library feature so contract entry-points don't collide). Serves as both a working
second example and a starting point for adapters that need a similar shape (treasury allocation, validator-preference signaling, AMM-incentive routing). 9 tests; 208K release wasm.

Documentation (#844 checklist)

Rewrote all three READMEs with architecture diagrams, lifecycle walk-throughs, full ExecuteMsg / QueryMsg / error tables, storage layout docs, hook-registration requirements, and a "writing your own adapter" guide. Top-level
contracts/gauges/README.md explains the orchestrator + adapter split; each contract's README is reviewable standalone.

Conventions cleanup

  • Schema generators moved from src/bin/ to examples/ to match the rest of the dao-contracts workspace; scripts/schema.sh extended with a contracts/gauges/* loop.
  • Dropped unused EmptyMsg test struct (caught by clippy -D dead_code).
  • Replaced one try_into().unwrap() (usizeu128) with as u128; safe by construction.
  • Workspace Cargo.toml: added gauge-orchestrator, gauge-adapter, gauge-budget-allocator entries; kept the contracts/gauges/* workspace-members glob.

What's intentionally NOT in this PR

  • Decay (Gauge Contracts #844 bonus item). Votes losing weight over time until re-cast, forcing re-engagement. Design space is open; left for a separate PR.

Test plan

  • cargo test -p gauge-adapter -p gauge-orchestrator -p gauge-budget-allocator → 22 + 41 + 9 = 72 tests, all green.
  • cargo fmt --all -- --check → clean.
  • cargo clippy --all-targets -- -D warnings on the three gauge crates → clean.
  • RUSTFLAGS="-C link-arg=-s" cargo build --release --lib --target wasm32-unknown-unknown for each gauge crate → success. Sizes: gauge_adapter.wasm 259K, gauge_orchestrator.wasm 492K, gauge_budget_allocator.wasm 208K.
  • Schema regeneration via scripts/schema.sh → checked-in schema/*.json files updated for all three contracts.

Jake Hartnell and others added 11 commits May 12, 2026 22:23
* Use cw-denom and payment utils in gauge adapter

TODO: 2 tests are failing, because the cw20 cannot be validated with cw-denom. This should be fixed with a cw-orch refactor.

* gauge adapter cw-orch tests

* include suite changes

* cleanup adapter test

* A couple tweaks to gauge adapter tests

* cleanup tests a bit

* cargo lock

* bump prost, lint

* lint

---------

Co-authored-by: hard-nett <hardnettt@proton.me>
Co-authored-by: Hard-Nett <123711748+hard-nett@users.noreply.github.com>
PR #875 reverted cw-orch workspace-wide. Strip its remaining footprint
from gauge-adapter (introduced by #847) by removing the cw_orch derive
macros from msg.rs and rewriting the cw-orch test harness on plain
cw-multi-test + cw20-base.

Also fix the gauge-orchestrator multitest harness for dao-interface
drift since the branch's last sync with development:
- ModuleInstantiateInfo: new salt field, funds is now Option<Vec<Coin>>
- dao_voting_cw4::GroupContract::New: new cw4_group_salt field
- dao_proposal_single::InstantiateMsg: new delegation_module field
- dao_interface::msg::InstantiateMsg: new initial_actions field

All 43 gauge tests pass (15 gauge-adapter + 28 gauge-orchestrator).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Move schema generators from src/bin/ to examples/ per dao-contracts
  convention; scripts/schema.sh uses --example schema.
- Add contracts/gauges/* loop to scripts/schema.sh.
- Drop unused EmptyMsg test struct in gauge multitest harness
  (clippy -D dead_code).
- Replace try_into().unwrap() with `as u128` in NFT stake-change path;
  usize is always representable.
- Apply rustfmt to all gauges sources.
- Regenerate schema JSON for both contracts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three fixes from the PR #844 checklist + audit:

1. member_changed_hook: replace GAUGES.load(...).unwrap() inside the
   batch-cache or_insert_with closure. x/cw-hooks treats any panic in a
   staking-hook callback as a failure and counts it toward the
   auto-unregister threshold (see hack-juno-gauge-design.md Tier 1 #2),
   so a transient storage error here could brick the gauge's tally
   updates. Propagate the error via `?` instead.

2. selected_set query: replace o.as_ref().unwrap() in the filter
   callback. Storage iterator errors used to be silently unwrapped to a
   panic inside the closure; let them pass through the filter and
   propagate via the existing `?` in the downstream map.

3. add_option: resolve the long-standing TODO ("this doesn't seem very
   safe... double check permissions here. Why is check option
   optional?"). Split the function into a public validated `add_option`
   (used by ExecuteMsg::AddOption — adapter CheckOption + nonzero
   voting-power anti-spam) and a private `add_adapter_options` for the
   trusted bulk-add path during gauge attachment. The bool parameter
   was making the safety property easy to break by accident.

4. place_votes: reject vote weights that round to zero against the
   voter's power. A user with 1 staked NFT splitting 50/50 across two
   options used to have *both* options counted as 0 — silently erasing
   their voice. New `VoteWeightRoundsToZero` error, regression test
   covers it.

All 29 gauge-orchestrator tests pass (+1 new). Wasm + clippy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the placeholder READMEs with full docs covering: lifecycle,
config knobs, ExecuteMsg/QueryMsg/error tables, storage layout, hook
registration requirements, and a guide on writing alternative adapters.
Top-level README explains the orchestrator + adapter split with an
ASCII diagram. Closes the "Better READMEs" item on PR #844's checklist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New multitest/errors.rs module exercises 12 validation error paths that
existing tests don't trigger:
- create_gauge: EpochSizeTooShort, MinPercentSelectedTooBig,
  MaxOptionsSelectedTooSmall, MaxAvailablePercentTooBig.
- place_votes: TooMuchVotingWeight, NoVotingPower, OptionDoesNotExists.
- execute: EpochNotReached.
- ownership: stop_gauge / update_gauge require owner.
- hooks: MemberChangedHook rejects callers other than hook_caller.
- nonexistent gauge: GaugeMissing.

Found a bug while writing the tests: create_gauge skipped the
`max_available_percentage < 1.0` check that update_gauge enforced, so a
gauge could be born with an invariant-violating config. Add the check.

Closes the "More tests" item on PR #844's checklist.

Total gauge tests: 41 (29 prior + 12 new). All green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two improvements toward the "Improve GaugeAdapter API + add second
example" item on PR #844's checklist:

1. New contract `gauge-budget-allocator`. A minimal adapter that
   distributes a fixed native-token budget proportionally to gauge
   weights. Admin-curated option list, no bond, no cw20. Lives at
   contracts/gauges/budget-allocator. Serves as both a working second
   example and a starting point for adapters that need a similar shape
   (treasury allocation, validator preference, AMM incentive routing,
   etc.). 9 tests, all green; release wasm = 208K.

2. Marketing-gauge-adapter: new `SubmissionsBySender { sender }` query.
   Returns all submissions whose `sender` matches the given address.
   Useful for "my submissions" views in registration flows. Test added.

Re-uses gauge-adapter's `AdapterQueryMsg` / response types as the
orchestrator-facing surface, depended on with `library` feature so its
contract entry-points don't collide with budget-allocator's at link
time.

cw-ownable migration of the marketing-gauge admin model deferred: the
Hack Juno design memo points at a curator-panel ownership model that
may want a richer authority shape than cw-ownable provides, so an
in-place migration now risks design churn. Leave for a follow-up PR
informed by Hack Juno's actual requirements.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a per-submission reject flow so the marketing adapter can back a
curated funding program (e.g. Hack Juno) without a fork. The admin
(typically a curator sub-DAO at this Addr) can remove a submission
mid-program with one of two bond outcomes:

- soft = true  → refund the bond to the original sender (good-faith,
                 wrong-fit reject).
- soft = false → forfeit the bond to the community pool (spam,
                 duplicate, malicious).

Both paths remove the submission from the registry. The pre-existing
ReturnDeposits flow is unchanged — it remains the bulk-refund-all path
for end-of-program wind-down.

Guards: only admin can call; cannot target the default community-pool
submission; explicit SubmissionNotFound on missing target. With no
required_deposit configured, Reject is just a registry remove with no
bond movement.

7 new tests cover both soft/hard paths for native + cw20 bonds, the
no-deposit case, unauthorized caller, missing submission, and the
community-pool guard. 22 gauge-adapter tests total now (15 prior + 7
new). Schema regenerated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented May 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.11462% with 101 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.05%. Comparing base (0178cf5) to head (d2da47e).

Files with missing lines Patch % Lines
contracts/gauges/gauge/src/contract.rs 93.84% 51 Missing ⚠️
contracts/gauges/budget-allocator/src/contract.rs 87.06% 15 Missing ⚠️
contracts/gauges/gauge/src/helpers.rs 0.00% 12 Missing ⚠️
contracts/gauges/gauge/src/state.rs 97.85% 10 Missing ⚠️
contracts/gauges/gauge-adapter/src/contract.rs 97.51% 9 Missing ⚠️
contracts/gauges/gauge/src/multitest/hooks.rs 98.83% 3 Missing ⚠️
...gauges/budget-allocator/src/multitest/allocator.rs 99.33% 1 Missing ⚠️
Additional details and impacted files
@@               Coverage Diff               @@
##           development     #928      +/-   ##
===============================================
- Coverage        96.56%   93.05%   -3.51%     
===============================================
  Files              199      178      -21     
  Lines            67407    34131   -33276     
===============================================
- Hits             65094    31762   -33332     
- Misses            2313     2369      +56     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

juno-ai-dev and others added 3 commits May 12, 2026 23:45
Brings the marketing gauge-adapter and the new budget-allocator onto the
standard DAO DAO ownership pattern. The previous `admin: Addr` field on
each adapter's `Config` is replaced with a `cw_ownable`-managed owner,
adding the standard two-step transfer flow (TransferOwnership +
AcceptOwnership) and RenounceOwnership.

InstantiateMsg: `admin` → `owner` (breaking — no migration helpers
shipped; these are unreleased contracts). ExecuteMsg gains
`UpdateOwnership(cw_ownable::Action)`; QueryMsg gains
`Ownership {} -> Ownership<Addr>`. The `Unauthorized` variant is removed
from both error enums in favor of `Ownership(OwnershipError)` propagated
transparently.

Marketing adapter: `Reject` and `ReturnDeposits` switch from explicit
sender == admin checks to `cw_ownable::assert_owner`. Budget-allocator
gates all three mutating ops through `assert_owner` at the top of
`execute`. Both contracts initialize the owner from `InstantiateMsg.owner`.

Tests: 4 new ownership tests on the marketing adapter, 3 on the
budget-allocator — covering initial ownership query, two-step transfer
(former owner loses auth, pending owner gains it on accept), renounce
locking owner-gated methods, and the not-owner reject on transfer init.
Existing tests updated to expect `Ownership(OwnershipError::NotOwner)`
where they previously asserted `Unauthorized`.

Schemas regenerated for both contracts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…face

Adds a subscriber-style hook fired by the gauge orchestrator on every
PlaceVotes. Subscribers receive the new vote state — gauge id, voter,
the new `Vec<Vote>` (empty on abstain), the voter's `voting_power` at
the snapshot, and the block height. The intended consumer is a
participation-reward distributor (e.g. dao-rewards-distributor), but the
shape is general enough for off-chain analytics or notification routing.

New module `src/hooks.rs` defines `GaugeVoteHookMsg::NewVotes` and a
`new_vote_hook_msgs` helper that builds `SubMsg::reply_on_error`
submessages over the registered subscriber list, indexed by hook
position for use as reply IDs.

ExecuteMsg gains owner-gated `AddHook { addr }` and `RemoveHook { addr }`.
QueryMsg gains `GetHooks {} -> GetHooksResponse { hooks: Vec<String> }`.
A new `reply` entry-point auto-unregisters any subscriber that errors
via `VOTE_HOOKS.remove_hook_by_index(deps.storage, msg.id)`. PlaceVotes
remains a successful tx even when a subscriber rejects the call, so a
misconfigured downstream contract cannot block voting — it self-prunes
on first failure.

`cw-controllers::Hooks` is wired in via the workspace `cw-hooks` crate;
`ContractError` gains a transparent `Hooks(HookError)` variant.

Tests (5 new): add/remove auth is owner-only, hook payload matches the
voter's submission, abstain ships an empty `votes` Vec, and a failing
hook gets auto-unregistered while a co-registered recorder survives and
keeps receiving payloads. ContractWrapper in the multitest suite now
wires `with_reply_empty(crate::contract::reply)` so the reply path is
exercised by the harness.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…01-08

CI failed with "lock file version 4 requires -Znext-lockfile-bump" because
the lockfile had been bumped to v4 by a newer cargo at some earlier commit
on this branch. CI runs nightly-2024-01-08 (rustc 1.77) which only reads v3.

Regenerated under the pinned nightly to drop back to v3, then pinned a
handful of transitive deps that have since started requiring the
edition2024 cargo feature (base64ct, home, indexmap, url, idna,
unicode-bidi/normalization, pest family). All versions chosen to match
what `development` already ships, plus pest 2.7.11 (the last release
before pest required rustc >= 1.83).

No source changes — purely a lockfile refresh. All 84 gauge tests pass
locally with --locked under the pinned toolchain; fmt + clippy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@JakeHartnell
JakeHartnell marked this pull request as ready for review May 13, 2026 02:02
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.

3 participants