Gauges - #928
Open
JakeHartnell wants to merge 14 commits into
Open
Conversation
* 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 Report❌ Patch coverage is 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. 🚀 New features to boost your workflow:
|
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
marked this pull request as ready for review
May 13, 2026 02:02
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Rebases the long-dormant
gauges-are-coolbranch onto currentdevelopment, drops thecw-orchtest-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 warningsclean; 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
development(0178cf55d).cw-orchfromgauge-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 plaincw_multi_test; kept Use cw-denom and payment utils in gauge adapter #847'scw-denom+ payment-utils refactors.dao-interface/dao-voting-cw4/dao-proposal-singleAPI drift in the orchestrator's multitest harness:ModuleInstantiateInfonow requiressaltand usesfunds: Option<Vec<Coin>>;GroupContract::Newgainedcw4_group_salt;ProposalSingleInstantiateMsggaineddelegation_module;dao_interface::msg::InstantiateMsggainedinitial_actions.Audit fixes
gauge/src/contract.rs:142—GAUGES.load(...).unwrap()inside themember_changedhook handler'sor_insert_withclosure could panic on a transient storage error.x/cw-hookstreats hook-callback panics as failures toward anauto-unregister threshold, so a single error here could brick further tally updates. Switched to
?propagation via an explicitEntry::Vacantinsert.gauge/src/contract.rs:971—selected_setfilter callback usedo.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?"). Splitadd_optioninto a public validatedadd_option(called byExecuteMsg::AddOption— runs adapterCheckOption+ nonzero-voting-power anti-spam) and a privateadd_adapter_optionsfor the trusted bulk-add path duringattach_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 * Decimaltruncate to 0 on both options — their vote silently erased. AddedContractError::VoteWeightRoundsToZero { weight, voting_power }and an early-reject inplace_votes. Regression test inmultitest/voting.rs::small_voting_power_rejects_round_to_zero_split.Bug found while expanding tests
create_gaugeskipped themax_available_percentage < 1.0validation thatupdate_gaugealready enforced — a gauge could be born with an invariant-violating config that no later update could repair. Added the check; covered bymultitest/errors.rs::create_gauge_rejects_max_available_percent_at_one.Test coverage expansion (#844 checklist)
New
contracts/gauges/gauge/src/multitest/errors.rswith 12 error-path tests covering the validation surfaces existing tests didn't exercise:create_gaugerejection cases,place_votesrejection cases (TooMuchVotingWeight,NoVotingPower,OptionDoesNotExists),GaugeMissingon bogus IDs,EpochNotReachedon premature execute, ownership-required ops, and hook-caller auth.Marketing adapter improvements (#844 checklist)
ExecuteMsg::Reject { submission, soft: bool }. Admin-gated per-submission removal. Soft refunds the bond to the original sender; hard forfeits to thecommunity_poolconfig field. Guards:SubmissionNotFound,CannotRejectDefault(prevents rejecting the default community-pool submission). No-op on the bond side if norequired_depositis configured.AdapterQueryMsg::SubmissionsBySender { sender }query. Returns all submissions whosesendermatches 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 — meaningfullysimpler than the marketing adapter. Re-uses
gauge-adapter'sAdapterQueryMsg/ response types as the orchestrator-facing surface (depended on withlibraryfeature so contract entry-points don't collide). Serves as both a workingsecond 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.mdexplains the orchestrator + adapter split; each contract's README is reviewable standalone.Conventions cleanup
src/bin/toexamples/to match the rest of the dao-contracts workspace;scripts/schema.shextended with acontracts/gauges/*loop.EmptyMsgtest struct (caught by clippy-D dead_code).try_into().unwrap()(usize→u128) withas u128; safe by construction.Cargo.toml: addedgauge-orchestrator,gauge-adapter,gauge-budget-allocatorentries; kept thecontracts/gauges/*workspace-members glob.What's intentionally NOT in this 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 warningson the three gauge crates → clean.RUSTFLAGS="-C link-arg=-s" cargo build --release --lib --target wasm32-unknown-unknownfor each gauge crate → success. Sizes:gauge_adapter.wasm259K,gauge_orchestrator.wasm492K,gauge_budget_allocator.wasm208K.scripts/schema.sh→ checked-inschema/*.jsonfiles updated for all three contracts.