Skip to content

feat(governance): derive Mission 70 maturity modulation from XRC price history#9847

Open
jasonz-dfinity wants to merge 13 commits intomasterfrom
jason/NNS1-4319-compute-maturity-modulation
Open

feat(governance): derive Mission 70 maturity modulation from XRC price history#9847
jasonz-dfinity wants to merge 13 commits intomasterfrom
jason/NNS1-4319-compute-maturity-modulation

Conversation

@jasonz-dfinity
Copy link
Copy Markdown
Contributor

@jasonz-dfinity jasonz-dfinity commented Apr 13, 2026

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 update
  • The existing CMC-polled maturity modulation remains active for spawning/disbursement (switchover is in a later PR)

PR Chain

Testing

Unit tests for the price history buffer, maturity modulation computation, and UTC midnight timer logic.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 IcpPriceHistory and MaturityModulation.
  • 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.

Comment thread rs/nns/governance/src/timer_tasks/update_icp_xdr_rate_related_data.rs Outdated
@jasonz-dfinity jasonz-dfinity force-pushed the jason/NNS1-4319-extract-exchange-rate-client branch from bda03a8 to 7491aee Compare April 15, 2026 19:23
@jasonz-dfinity jasonz-dfinity force-pushed the jason/NNS1-4319-compute-maturity-modulation branch from 0724790 to 9a31ba9 Compare April 15, 2026 19:27
@jasonz-dfinity jasonz-dfinity force-pushed the jason/NNS1-4319-compute-maturity-modulation branch from 9a31ba9 to a7108f0 Compare April 15, 2026 21:29
@jasonz-dfinity jasonz-dfinity marked this pull request as ready for review April 15, 2026 22:32
@jasonz-dfinity jasonz-dfinity requested a review from a team as a code owner April 15, 2026 22:32
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

This pull request changes code owned by the Governance team. Therefore, make sure that
you have considered the following (for Governance-owned code):

  1. Update unreleased_changelog.md (if there are behavior changes, even if they are
    non-breaking).

  2. Are there BREAKING changes?

  3. Is a data migration needed?

  4. Security review?

How to Satisfy This Automatic Review

  1. Go to the bottom of the pull request page.

  2. Look for where it says this bot is requesting changes.

  3. Click the three dots to the right.

  4. Select "Dismiss review".

  5. 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(
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.

Why not just call this new?

Comment on lines +247 to +248
// 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.
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.

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(
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.

I would skip this if previous_day == current_day


#[async_trait]
impl RecurringAsyncTask for UpdateIcpXdrRateRelatedData {
async fn execute(self) -> (Duration, Self) {
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.

Task execution looks brittle. If there is a panic in this method, execution never happens again.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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| {
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.

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);
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.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Do you mean if it can be run BEFORE the intended time? I doubt that is possible, but let me confirm.

Comment on lines +168 to +169
MIN_MATURITY_MODULATION_PERMYRIAD as i128,
MAX_MATURITY_MODULATION_PERMYRIAD as i128,
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.

These are still the old values (-500, 500). What we want is (-1000, 200)

@jasonz-dfinity jasonz-dfinity force-pushed the jason/NNS1-4319-compute-maturity-modulation branch from 1455d28 to 876292a Compare May 5, 2026 23:05
Base automatically changed from jason/NNS1-4319-extract-exchange-rate-client to master May 6, 2026 17:22
@jasonz-dfinity jasonz-dfinity dismissed github-actions[bot]’s stale review May 6, 2026 18:18
  1. Changelog entry added under Added.
  2. Existing APIs behave as before (additive proto fields only).
  3. New proto fields are additive; no migration needed.
  4. New value computed but not yet consumed by spawning/disbursement (switchover in follow-up PR).
@jasonz-dfinity jasonz-dfinity requested a review from Copilot May 6, 2026 18:23
pull Bot pushed a commit to mikeyhodl/ic that referenced this pull request May 6, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants