feat(governance): derive Mission 70 maturity modulation from XRC price history#9847
feat(governance): derive Mission 70 maturity modulation from XRC price history#9847jasonz-dfinity wants to merge 13 commits intomasterfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates NNS Governance to fetch ICP/XDR rates directly from the Exchange Rate Canister (XRC), persist a bounded daily price history in Governance state, and locally compute Mission 70 maturity modulation from that history.
Changes:
- Add a new recurring async timer task (
UpdateIcpXdrRateRelatedData) that backfills up to 365 daily rates from XRC and then switches to UTC-midnight-aligned daily updates. - Extend Governance state (proto + heap data) with
IcpPriceHistoryandMaturityModulation. - Add unit tests covering the rate history buffer behavior, modulation computation, and UTC-midnight delay logic.
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| rs/nns/governance/src/timer_tasks/update_icp_xdr_rate_related_data.rs | Implements XRC fetching, price-history maintenance, and maturity modulation computation + scheduling logic. |
| rs/nns/governance/src/timer_tasks/update_icp_xdr_rate_related_data_tests.rs | Adds unit tests for buffer updates, modulation math, and timer delay behavior. |
| rs/nns/governance/src/timer_tasks/mod.rs | Wires the new timer task into the governance timer scheduling (wasm32-gated). |
| rs/nns/governance/src/heap_governance_data.rs | Persists the new proto fields into heap governance state and reassembles them on upgrade. |
| rs/nns/governance/src/gen/ic_nns_governance.pb.v1.rs | Generated Rust bindings for new proto messages/fields. |
| rs/nns/governance/proto/ic_nns_governance/pb/v1/governance.proto | Adds SampledPrice, IcpPriceHistory, and MaturityModulation messages + Governance fields (tags 33/34). |
| rs/nns/governance/Cargo.toml | Adds ic-xrc-types dependency. |
| rs/nns/governance/BUILD.bazel | Adds Bazel dependency on ic-xrc-types. |
| Cargo.lock | Locks the new ic-xrc-types dependency. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
bda03a8 to
7491aee
Compare
0724790 to
9a31ba9
Compare
9a31ba9 to
a7108f0
Compare
There was a problem hiding this comment.
This pull request changes code owned by the Governance team. Therefore, make sure that
you have considered the following (for Governance-owned code):
-
Update
unreleased_changelog.md(if there are behavior changes, even if they are
non-breaking). -
Are there BREAKING changes?
-
Is a data migration needed?
-
Security review?
How to Satisfy This Automatic Review
-
Go to the bottom of the pull request page.
-
Look for where it says this bot is requesting changes.
-
Click the three dots to the right.
-
Select "Dismiss review".
-
In the text entry box, respond to each of the numbered items in the previous
section, declare one of the following:
-
Done.
-
$REASON_WHY_NO_NEED. E.g. for
unreleased_changelog.md, "No
canister behavior changes.", or for item 2, "Existing APIs
behave as before.".
Brief Guide to "Externally Visible" Changes
"Externally visible behavior change" is very often due to some NEW canister API.
Changes to EXISTING APIs are more likely to be "breaking".
If these changes are breaking, make sure that clients know how to migrate, how to
maintain their continuity of operations.
If your changes are behind a feature flag, then, do NOT add entrie(s) to
unreleased_changelog.md in this PR! But rather, add entrie(s) later, in the PR
that enables these changes in production.
Reference(s)
For a more comprehensive checklist, see here.
GOVERNANCE_CHECKLIST_REMINDER_DEDUP
| } | ||
|
|
||
| impl UpdateIcpXdrRateRelatedData { | ||
| pub fn new_with_client( |
There was a problem hiding this comment.
Why not just call this new?
| // Verify that XRC returned a rate for the day we requested. If not, the rate | ||
| // won't fill the expected slot and backfill would loop on the same day. |
There was a problem hiding this comment.
Can you ask the FI team in what cases this might happen to confirm that it generally wouldn't?
| let previous_permyriad = maturity_modulation.current_value_permyriad.unwrap_or(0) as i64; | ||
| let previous_day = maturity_modulation.updated_at_days_since_epoch.unwrap_or(0); | ||
|
|
||
| let new_permyriad = compute_maturity_modulation_permyriad( |
There was a problem hiding this comment.
I would skip this if previous_day == current_day
|
|
||
| #[async_trait] | ||
| impl RecurringAsyncTask for UpdateIcpXdrRateRelatedData { | ||
| async fn execute(self) -> (Duration, Self) { |
There was a problem hiding this comment.
Task execution looks brittle. If there is a panic in this method, execution never happens again.
There was a problem hiding this comment.
Right, but in the case of a panic, there isn't really a good way of recovering. OTOH, If the instruction limit is hit unintentionally, NOT rescheduling is strictly better. Do you have a suggestion?
| } | ||
|
|
||
| // History is complete for current_day: compute maturity modulation. | ||
| self.governance.with_borrow_mut(|gov| { |
There was a problem hiding this comment.
dup code, just remove this and return with a short duration
| }); | ||
|
|
||
| let now = self.governance.with_borrow(|gov| gov.env.now()); | ||
| return (duration_until_next_midnight_utc(now), self); |
There was a problem hiding this comment.
I don't know if timer scheduling is exact enough that it always hits midnight or later or if in practice this might be already executed short before midnight and in the worst case one day might get skipped. Definitely worth testing and eventually adding a small delta that ensures that we're executing after midnight.
There was a problem hiding this comment.
Do you mean if it can be run BEFORE the intended time? I doubt that is possible, but let me confirm.
| MIN_MATURITY_MODULATION_PERMYRIAD as i128, | ||
| MAX_MATURITY_MODULATION_PERMYRIAD as i128, |
There was a problem hiding this comment.
These are still the old values (-500, 500). What we want is (-1000, 200)
…ract-exchange-rate-client
…ract-exchange-rate-client
…name to duration_until_next_midnight_utc
1455d28 to
876292a
Compare
- Changelog entry added under Added.
- Existing APIs behave as before (additive proto fields only).
- New proto fields are additive; no migration needed.
- New value computed but not yet consumed by spawning/disbursement (switchover in follow-up PR).
…m-clients (dfinity#9846) ## Why CMC and the upcoming Governance timer task (next PR) both need XRC-calling logic. This extracts the shared code to avoid duplication. https://dfinity.atlassian.net/browse/NNS1-4319 ## What Moved exchange-rate-canister calling logic from CMC into a new shared module in ic-nervous-system-clients: - New exchange_rate_canister_client.rs with ExchangeRateCanisterClient trait, RealExchangeRateCanisterClient, error types, validation helpers, and shared constants - The trait adds timestamp: Option<u64> so callers can request historical rates; CMC always passes None - CMC exchange_rate_canister.rs is updated to use the shared types; its local duplicates are removed This is a pure refactor -- no behavior change to CMC. ### PR Chain - ➡️ Next: dfinity#9847 ## Testing Existing CMC tests cover the refactor.
Why
Previously, Governance relied on CMC (Cycles Minting Canister) as a middleman for ICP price data used in maturity modulation. This PR cuts out the middleman by having Governance fetch ICP/XDR rates directly from the Exchange Rate Canister (XRC) and derive Mission 70 maturity modulation locally.
https://dfinity.atlassian.net/browse/NNS1-4319
What
IcpPriceHistory: bounded sorted vector of up to 365 daily ICP/XDR rates in Governance state (new proto field 33)UpdateIcpXdrRateRelatedData: timer task that fetches rates directly from XRC, backfills the last 365 days on first upgrade (~30 min at 5-second intervals), then switches to daily fetches aligned to UTC midnight; recomputes Mission 70 maturity modulation after each updatePR Chain
Testing
Unit tests for the price history buffer, maturity modulation computation, and UTC midnight timer logic.