Skip to content

[Plan 5 · 02] freeze and unfreeze instructions #189

Description

@gravityblast

Issue 02 — freeze and unfreeze instructions

Plan: Stablecoin Plan 5 — Emergency Freeze
Depends on: Plan 1 (ProtocolParameters with is_frozen + freeze_authority_account_id fields, compute_protocol_parameters_pda / compute_protocol_parameters_pda_seed); issue 01 in this plan (the gate that gives freeze real effect).
Blocks: issue 03 (guest wiring + integration tests).

Goal: Add the Instruction::Freeze and Instruction::Unfreeze variants to stablecoin_core, implement both host functions in a new programs/stablecoin/src/freeze.rs module, and cover every code path with unit tests — happy paths for each, authorization rejections, freeze-authority handle mismatches, and idempotency (calling freeze when already frozen succeeds and the flag stays true; same shape for unfreeze).

Spec reference

Why

The freeze authority's job is to halt the protocol when something goes wrong (broken oracle, exploit in progress). Issue 01 landed the gate that makes a true value of is_frozen block risky ops; this issue lands the actual instructions that flip the bit. Together they make the kill switch real.

Both instructions are identical except for the target bit value, so bundling them in one issue (and one host module) keeps the two cases trivially in sync. The auth check pattern mirrors the admin auth check from Plan 4 issue 01 — same structural shape (assert!(authority.is_authorized) plus assert_eq!(authority.account_id, params.freeze_authority_account_id)), targeting freeze_authority_account_id instead of admin_account_id. We don't extract a shared trait between Plan 4 and Plan 5 for v1; if a third authority lands (RFP-001/002 wrappers), we can refactor then.

Architecture

A new programs/stablecoin/src/freeze.rs module exposes two pub fns:

pub fn freeze(
    freeze_authority: AccountWithMetadata,
    protocol_parameters: AccountWithMetadata,
    stablecoin_program_id: ProgramId,
) -> (Vec<AccountPostState>, Vec<ChainedCall>);

pub fn unfreeze(
    freeze_authority: AccountWithMetadata,
    protocol_parameters: AccountWithMetadata,
    stablecoin_program_id: ProgramId,
) -> (Vec<AccountPostState>, Vec<ChainedCall>);

Both share a private set_is_frozen(...) helper that takes a target_value: bool and the three inputs. This keeps the two public entry points one-liners around the helper and means the auth check, PDA check, and post-state shape only exist in one place.

Two new Instruction variants in stablecoin_core::Instruction: Freeze and Unfreeze, both unit variants (no scalar parameters). The 2-account input list comes from the guest entry signature; no instruction-level args needed.

Files

  • Modify: programs/stablecoin/core/src/lib.rs — add the two new Instruction variants with doc comments.
  • Create: programs/stablecoin/src/freeze.rs — the two public host functions + the private helper.
  • Modify: programs/stablecoin/src/lib.rspub mod freeze;.
  • Create: programs/stablecoin/src/tests/freeze_tests.rs — the unit tests.
  • Modify: programs/stablecoin/src/tests/mod.rs — register mod freeze_tests;.

Acceptance criteria

  • cargo test -p stablecoin_core is green (the two new Instruction variants serialize / deserialize cleanly).
  • cargo test -p stablecoin_program freeze_tests:: passes the full test list:
    • freeze_happy_path_sets_is_frozen_true
    • unfreeze_happy_path_sets_is_frozen_false
    • freeze_rejects_unauthorized_freeze_authority
    • unfreeze_rejects_unauthorized_freeze_authority
    • freeze_rejects_wrong_freeze_authority_account_id
    • unfreeze_rejects_wrong_freeze_authority_account_id
    • freeze_rejects_uninitialized_protocol_parameters
    • unfreeze_rejects_uninitialized_protocol_parameters
    • freeze_rejects_wrong_protocol_parameters_pda
    • unfreeze_rejects_wrong_protocol_parameters_pda
    • freeze_is_idempotent_when_already_frozen
    • unfreeze_is_idempotent_when_already_unfrozen
    • freeze_does_not_change_any_other_protocol_parameters_field
    • unfreeze_does_not_change_any_other_protocol_parameters_field
    • freeze_emits_no_chained_calls
    • unfreeze_emits_no_chained_calls
  • make clippy is green.
  • Exactly 1 post-state per call (the protocol_parameters account, claimed via Claim::Pda(compute_protocol_parameters_pda_seed())). Zero chained calls.

Implementation steps

  • Step 1: Add the two Instruction variants

In programs/stablecoin/core/src/lib.rs, after the existing Instruction variants (the position-lifecycle ones from Plan 3 and admin ones from Plan 4), add:

    /// Set [`ProtocolParameters::is_frozen`] to `true`, halting risk-increasing
    /// position operations until [`Instruction::Unfreeze`] flips it back.
    /// Idempotent: calling `Freeze` when already frozen succeeds and leaves
    /// `is_frozen = true`.
    ///
    /// Required accounts (2):
    /// - `freeze_authority` (authorized; `account_id` must equal
    ///   `ProtocolParameters.freeze_authority_account_id`)
    /// - `protocol_parameters` (initialized, writable; PDA verified)
    ///
    /// See spec §10.17 for the full contract.
    Freeze,
    /// Set [`ProtocolParameters::is_frozen`] to `false`, resuming normal
    /// operation. Idempotent: calling `Unfreeze` when already unfrozen
    /// succeeds and leaves `is_frozen = false`.
    ///
    /// Required accounts (2):
    /// - `freeze_authority` (authorized; `account_id` must equal
    ///   `ProtocolParameters.freeze_authority_account_id`)
    /// - `protocol_parameters` (initialized, writable; PDA verified)
    ///
    /// See spec §10.18 for the full contract.
    Unfreeze,

Run cargo test -p stablecoin_core. Expected: still green; the two new variants serialize via the existing serde derive.

  • Step 2: Create the freeze.rs host module

Create programs/stablecoin/src/freeze.rs:

//! Host-side implementation of `Instruction::Freeze` and `Instruction::Unfreeze`
//! (spec §10.17 / §10.18).
//!
//! Both entries flip `ProtocolParameters.is_frozen` to a fixed target value.
//! Idempotent — calling `freeze` when already frozen (or `unfreeze` when
//! already unfrozen) succeeds and leaves the flag at the target value.

use nssa_core::{
    account::{Account, AccountWithMetadata, Data},
    program::{AccountPostState, ChainedCall, Claim, ProgramId},
};
use stablecoin_core::{
    compute_protocol_parameters_pda, compute_protocol_parameters_pda_seed, ProtocolParameters,
};

/// Set [`ProtocolParameters::is_frozen`] to `true`. See module-level docs and
/// spec §10.17 for the full contract.
///
/// # Panics
/// - `freeze_authority.is_authorized = false`.
/// - `freeze_authority.account_id` does not match
///   `ProtocolParameters.freeze_authority_account_id`.
/// - `protocol_parameters` is uninitialized or sits at the wrong PDA.
/// - `protocol_parameters.account.data` does not decode as
///   [`ProtocolParameters`].
pub fn freeze(
    freeze_authority: AccountWithMetadata,
    protocol_parameters: AccountWithMetadata,
    stablecoin_program_id: ProgramId,
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
    set_is_frozen(freeze_authority, protocol_parameters, stablecoin_program_id, true)
}

/// Set [`ProtocolParameters::is_frozen`] to `false`. See module-level docs and
/// spec §10.18 for the full contract.
///
/// Panic conditions are identical to [`freeze`].
pub fn unfreeze(
    freeze_authority: AccountWithMetadata,
    protocol_parameters: AccountWithMetadata,
    stablecoin_program_id: ProgramId,
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
    set_is_frozen(freeze_authority, protocol_parameters, stablecoin_program_id, false)
}

fn set_is_frozen(
    freeze_authority: AccountWithMetadata,
    protocol_parameters: AccountWithMetadata,
    stablecoin_program_id: ProgramId,
    target_value: bool,
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
    // 1. Freeze-authority authorization.
    assert!(
        freeze_authority.is_authorized,
        "Freeze authority authorization is missing"
    );

    // 2. ProtocolParameters must be initialized and at the right PDA.
    assert_ne!(
        protocol_parameters.account,
        Account::default(),
        "ProtocolParameters account must be initialized"
    );
    assert_eq!(
        protocol_parameters.account_id,
        compute_protocol_parameters_pda(stablecoin_program_id),
        "ProtocolParameters account ID does not match expected PDA derivation"
    );

    // 3. Decode and check the freeze-authority handle.
    let mut params = ProtocolParameters::try_from(&protocol_parameters.account.data)
        .expect("ProtocolParameters must decode");
    assert_eq!(
        freeze_authority.account_id, params.freeze_authority_account_id,
        "Freeze authority account ID does not match the recorded freeze_authority_account_id"
    );

    // 4. Flip the bit. Idempotent by construction: setting target_value when
    //    is_frozen already equals target_value is a no-op write.
    params.is_frozen = target_value;

    // 5. Build the post-state.
    let mut protocol_parameters_post = protocol_parameters.account;
    protocol_parameters_post.data = Data::from(&params);

    let post_states = vec![AccountPostState::new_claimed(
        protocol_parameters_post,
        Claim::Pda(compute_protocol_parameters_pda_seed()),
    )];

    (post_states, Vec::new())
}

Run cargo check -p stablecoin_program. Expected: clean.

  • Step 3: Wire the module into stablecoin_program

In programs/stablecoin/src/lib.rs, add (alongside the existing pub mod declarations):

/// Emergency kill switch — flip [`stablecoin_core::ProtocolParameters::is_frozen`].
pub mod freeze;

Run cargo check -p stablecoin_program. Expected: clean.

  • Step 4: Create the test file scaffolding

Create programs/stablecoin/src/tests/freeze_tests.rs:

#![allow(
    clippy::indexing_slicing,
    clippy::panic,
    clippy::unwrap_used,
    reason = "tests deliberately panic on bad state via assert!/#[should_panic] and index fixed-size vectors"
)]

use nssa_core::{
    account::{Account, AccountId, AccountWithMetadata, Data, Nonce},
    program::{Claim, ProgramId},
};
use stablecoin_core::{
    compute_protocol_parameters_pda, compute_protocol_parameters_pda_seed, ProtocolParameters,
};

use stablecoin_program::freeze::{freeze, unfreeze};

const STABLECOIN_PROGRAM_ID: ProgramId = [3u32; 8];

fn freeze_authority_id() -> AccountId {
    AccountId::new([0xFE; 32])
}

fn admin_id() -> AccountId {
    AccountId::new([0xA0; 32])
}

fn stablecoin_definition_id() -> AccountId {
    AccountId::new([0x11; 32])
}

fn collateral_definition_id() -> AccountId {
    AccountId::new([0x12; 32])
}

fn oracle_id() -> AccountId {
    AccountId::new([0x13; 32])
}

fn protocol_parameters_id() -> AccountId {
    compute_protocol_parameters_pda(STABLECOIN_PROGRAM_ID)
}

fn baseline_protocol_parameters(is_frozen: bool) -> ProtocolParameters {
    ProtocolParameters {
        admin_account_id: admin_id(),
        freeze_authority_account_id: freeze_authority_id(),
        stablecoin_definition_id: stablecoin_definition_id(),
        collateral_definition_id: collateral_definition_id(),
        market_price_oracle_id: oracle_id(),
        stability_fee_per_millisecond: 1_000_000_000_000_000_000_000_000_001,
        controller_proportional_gain: 0,
        controller_integral_gain: 0,
        minimum_collateralization_ratio: 1_500_000_000_000_000_000_000_000_000,
        minimum_milliseconds_between_rate_updates: 300,
        maximum_oracle_price_age_milliseconds: 900,
        is_frozen,
    }
}

fn protocol_parameters_account(is_frozen: bool) -> AccountWithMetadata {
    AccountWithMetadata {
        account: Account {
            program_owner: STABLECOIN_PROGRAM_ID,
            balance: 0,
            data: Data::from(&baseline_protocol_parameters(is_frozen)),
            nonce: Nonce(0),
        },
        is_authorized: false,
        account_id: protocol_parameters_id(),
    }
}

fn authorized_freeze_authority_account() -> AccountWithMetadata {
    AccountWithMetadata {
        account: Account::default(),
        is_authorized: true,
        account_id: freeze_authority_id(),
    }
}
  • Step 5: Register the test file

Edit programs/stablecoin/src/tests/mod.rs to add:

mod freeze_tests;

Run cargo test -p stablecoin_program freeze_tests. Expected: 0 tests so far — module compiles.

  • Step 6: Happy-path tests

Append to freeze_tests.rs:

#[test]
fn freeze_happy_path_sets_is_frozen_true() {
    let (post_states, chained_calls) = freeze(
        authorized_freeze_authority_account(),
        protocol_parameters_account(false),
        STABLECOIN_PROGRAM_ID,
    );
    assert_eq!(post_states.len(), 1);
    assert!(chained_calls.is_empty());

    let pp_post = &post_states[0];
    assert_eq!(
        pp_post.required_claim(),
        Some(&Claim::Pda(compute_protocol_parameters_pda_seed()))
    );
    let decoded = ProtocolParameters::try_from(&pp_post.account().data).expect("decode");
    assert!(decoded.is_frozen);
}

#[test]
fn unfreeze_happy_path_sets_is_frozen_false() {
    let (post_states, chained_calls) = unfreeze(
        authorized_freeze_authority_account(),
        protocol_parameters_account(true),
        STABLECOIN_PROGRAM_ID,
    );
    assert_eq!(post_states.len(), 1);
    assert!(chained_calls.is_empty());

    let pp_post = &post_states[0];
    assert_eq!(
        pp_post.required_claim(),
        Some(&Claim::Pda(compute_protocol_parameters_pda_seed()))
    );
    let decoded = ProtocolParameters::try_from(&pp_post.account().data).expect("decode");
    assert!(!decoded.is_frozen);
}

Run cargo test -p stablecoin_program freeze_tests::freeze_happy_path_sets_is_frozen_true freeze_tests::unfreeze_happy_path_sets_is_frozen_false. Expected: 2 PASS.

  • Step 7: Authorization rejection tests

Append:

#[test]
#[should_panic(expected = "Freeze authority authorization is missing")]
fn freeze_rejects_unauthorized_freeze_authority() {
    let mut freeze_authority = authorized_freeze_authority_account();
    freeze_authority.is_authorized = false;
    let _ = freeze(
        freeze_authority,
        protocol_parameters_account(false),
        STABLECOIN_PROGRAM_ID,
    );
}

#[test]
#[should_panic(expected = "Freeze authority authorization is missing")]
fn unfreeze_rejects_unauthorized_freeze_authority() {
    let mut freeze_authority = authorized_freeze_authority_account();
    freeze_authority.is_authorized = false;
    let _ = unfreeze(
        freeze_authority,
        protocol_parameters_account(true),
        STABLECOIN_PROGRAM_ID,
    );
}

#[test]
#[should_panic(
    expected = "Freeze authority account ID does not match the recorded freeze_authority_account_id"
)]
fn freeze_rejects_wrong_freeze_authority_account_id() {
    let mut freeze_authority = authorized_freeze_authority_account();
    freeze_authority.account_id = AccountId::new([0xBA; 32]);
    let _ = freeze(
        freeze_authority,
        protocol_parameters_account(false),
        STABLECOIN_PROGRAM_ID,
    );
}

#[test]
#[should_panic(
    expected = "Freeze authority account ID does not match the recorded freeze_authority_account_id"
)]
fn unfreeze_rejects_wrong_freeze_authority_account_id() {
    let mut freeze_authority = authorized_freeze_authority_account();
    freeze_authority.account_id = AccountId::new([0xBA; 32]);
    let _ = unfreeze(
        freeze_authority,
        protocol_parameters_account(true),
        STABLECOIN_PROGRAM_ID,
    );
}

Run cargo test -p stablecoin_program freeze_tests::. Expected: 6 PASS so far.

  • Step 8: ProtocolParameters precondition tests

Append:

#[test]
#[should_panic(expected = "ProtocolParameters account must be initialized")]
fn freeze_rejects_uninitialized_protocol_parameters() {
    let pp = AccountWithMetadata {
        account: Account::default(),
        is_authorized: false,
        account_id: protocol_parameters_id(),
    };
    let _ = freeze(
        authorized_freeze_authority_account(),
        pp,
        STABLECOIN_PROGRAM_ID,
    );
}

#[test]
#[should_panic(expected = "ProtocolParameters account must be initialized")]
fn unfreeze_rejects_uninitialized_protocol_parameters() {
    let pp = AccountWithMetadata {
        account: Account::default(),
        is_authorized: false,
        account_id: protocol_parameters_id(),
    };
    let _ = unfreeze(
        authorized_freeze_authority_account(),
        pp,
        STABLECOIN_PROGRAM_ID,
    );
}

#[test]
#[should_panic(
    expected = "ProtocolParameters account ID does not match expected PDA derivation"
)]
fn freeze_rejects_wrong_protocol_parameters_pda() {
    let mut pp = protocol_parameters_account(false);
    pp.account_id = AccountId::new([0xDE; 32]);
    let _ = freeze(
        authorized_freeze_authority_account(),
        pp,
        STABLECOIN_PROGRAM_ID,
    );
}

#[test]
#[should_panic(
    expected = "ProtocolParameters account ID does not match expected PDA derivation"
)]
fn unfreeze_rejects_wrong_protocol_parameters_pda() {
    let mut pp = protocol_parameters_account(true);
    pp.account_id = AccountId::new([0xDE; 32]);
    let _ = unfreeze(
        authorized_freeze_authority_account(),
        pp,
        STABLECOIN_PROGRAM_ID,
    );
}

Run cargo test -p stablecoin_program freeze_tests::. Expected: 10 PASS so far.

  • Step 9: Idempotency tests

Append:

#[test]
fn freeze_is_idempotent_when_already_frozen() {
    let (post_states, chained_calls) = freeze(
        authorized_freeze_authority_account(),
        protocol_parameters_account(true), // already frozen
        STABLECOIN_PROGRAM_ID,
    );
    assert_eq!(post_states.len(), 1);
    assert!(chained_calls.is_empty());
    let decoded = ProtocolParameters::try_from(&post_states[0].account().data).expect("decode");
    assert!(
        decoded.is_frozen,
        "is_frozen must remain true when freeze is called on an already-frozen protocol"
    );
}

#[test]
fn unfreeze_is_idempotent_when_already_unfrozen() {
    let (post_states, chained_calls) = unfreeze(
        authorized_freeze_authority_account(),
        protocol_parameters_account(false), // already unfrozen
        STABLECOIN_PROGRAM_ID,
    );
    assert_eq!(post_states.len(), 1);
    assert!(chained_calls.is_empty());
    let decoded = ProtocolParameters::try_from(&post_states[0].account().data).expect("decode");
    assert!(
        !decoded.is_frozen,
        "is_frozen must remain false when unfreeze is called on an already-unfrozen protocol"
    );
}

Run cargo test -p stablecoin_program freeze_tests::. Expected: 12 PASS so far.

  • Step 10: Other-fields-untouched tests

Append:

#[test]
fn freeze_does_not_change_any_other_protocol_parameters_field() {
    let original = baseline_protocol_parameters(false);
    let (post_states, _) = freeze(
        authorized_freeze_authority_account(),
        protocol_parameters_account(false),
        STABLECOIN_PROGRAM_ID,
    );
    let decoded = ProtocolParameters::try_from(&post_states[0].account().data).expect("decode");

    // Every field except is_frozen must match the pre-state.
    assert_eq!(decoded.admin_account_id, original.admin_account_id);
    assert_eq!(
        decoded.freeze_authority_account_id,
        original.freeze_authority_account_id
    );
    assert_eq!(decoded.stablecoin_definition_id, original.stablecoin_definition_id);
    assert_eq!(decoded.collateral_definition_id, original.collateral_definition_id);
    assert_eq!(decoded.market_price_oracle_id, original.market_price_oracle_id);
    assert_eq!(decoded.stability_fee_per_millisecond, original.stability_fee_per_millisecond);
    assert_eq!(
        decoded.controller_proportional_gain,
        original.controller_proportional_gain
    );
    assert_eq!(decoded.controller_integral_gain, original.controller_integral_gain);
    assert_eq!(
        decoded.minimum_collateralization_ratio,
        original.minimum_collateralization_ratio
    );
    assert_eq!(
        decoded.minimum_milliseconds_between_rate_updates,
        original.minimum_milliseconds_between_rate_updates
    );
    assert_eq!(
        decoded.maximum_oracle_price_age_milliseconds,
        original.maximum_oracle_price_age_milliseconds
    );
    // is_frozen IS the field that changed:
    assert_ne!(decoded.is_frozen, original.is_frozen);
    assert!(decoded.is_frozen);
}

#[test]
fn unfreeze_does_not_change_any_other_protocol_parameters_field() {
    let original = baseline_protocol_parameters(true);
    let (post_states, _) = unfreeze(
        authorized_freeze_authority_account(),
        protocol_parameters_account(true),
        STABLECOIN_PROGRAM_ID,
    );
    let decoded = ProtocolParameters::try_from(&post_states[0].account().data).expect("decode");

    assert_eq!(decoded.admin_account_id, original.admin_account_id);
    assert_eq!(
        decoded.freeze_authority_account_id,
        original.freeze_authority_account_id
    );
    assert_eq!(decoded.stablecoin_definition_id, original.stablecoin_definition_id);
    assert_eq!(decoded.collateral_definition_id, original.collateral_definition_id);
    assert_eq!(decoded.market_price_oracle_id, original.market_price_oracle_id);
    assert_eq!(decoded.stability_fee_per_millisecond, original.stability_fee_per_millisecond);
    assert_eq!(
        decoded.controller_proportional_gain,
        original.controller_proportional_gain
    );
    assert_eq!(decoded.controller_integral_gain, original.controller_integral_gain);
    assert_eq!(
        decoded.minimum_collateralization_ratio,
        original.minimum_collateralization_ratio
    );
    assert_eq!(
        decoded.minimum_milliseconds_between_rate_updates,
        original.minimum_milliseconds_between_rate_updates
    );
    assert_eq!(
        decoded.maximum_oracle_price_age_milliseconds,
        original.maximum_oracle_price_age_milliseconds
    );
    assert_ne!(decoded.is_frozen, original.is_frozen);
    assert!(!decoded.is_frozen);
}

Run cargo test -p stablecoin_program freeze_tests::. Expected: 14 PASS so far.

  • Step 11: No-chained-calls tests

Append:

#[test]
fn freeze_emits_no_chained_calls() {
    let (_, chained_calls) = freeze(
        authorized_freeze_authority_account(),
        protocol_parameters_account(false),
        STABLECOIN_PROGRAM_ID,
    );
    assert!(chained_calls.is_empty(), "freeze must not emit any chained calls");
}

#[test]
fn unfreeze_emits_no_chained_calls() {
    let (_, chained_calls) = unfreeze(
        authorized_freeze_authority_account(),
        protocol_parameters_account(true),
        STABLECOIN_PROGRAM_ID,
    );
    assert!(
        chained_calls.is_empty(),
        "unfreeze must not emit any chained calls"
    );
}

Run cargo test -p stablecoin_program freeze_tests::. Expected: 16 PASS.

  • Step 12: Full sweep
make clippy
RISC0_DEV_MODE=1 cargo test -p stablecoin_core
RISC0_DEV_MODE=1 cargo test -p stablecoin_program

Expected: all green.

  • Step 13: Commit
git add programs/stablecoin/core/src/lib.rs \
        programs/stablecoin/src/freeze.rs \
        programs/stablecoin/src/lib.rs \
        programs/stablecoin/src/tests/freeze_tests.rs \
        programs/stablecoin/src/tests/mod.rs
cargo +nightly fmt --all
git add -u
git commit -m "feat(stablecoin): implement freeze and unfreeze host functions

Two new Instruction variants (Freeze, Unfreeze) plus the matching
host functions in programs/stablecoin/src/freeze.rs. Both share a
private set_is_frozen(target_value) helper so the auth check, PDA
check, and post-state shape exist in exactly one place.

Authorization mirrors the admin auth pattern: freeze_authority must be
is_authorized = true AND its account_id must equal the recorded
ProtocolParameters.freeze_authority_account_id. Both instructions are
idempotent — calling freeze when already frozen succeeds and leaves
is_frozen = true; same shape for unfreeze.

16 unit tests cover happy path (2), unauthorized freeze_authority (2),
freeze_authority handle mismatch (2), uninitialized ProtocolParameters
(2), wrong PDA (2), idempotency (2), other-fields-untouched (2), and
no-chained-calls (2)."

Notes for reviewer / future maintainers

  • The auth check pattern (is_authorized + account_id == recorded handle) deliberately mirrors the admin auth check landed in Plan 4 issue 01. We do NOT share a helper between them in v1 — two call sites are not enough indirection to justify a verify_authority(authority, recorded_id) abstraction, and the freeze authority's recorded field has a different name than the admin's. If a third authority lands (RFP-001/002 wrappers), revisit.
  • The bundling of freeze + unfreeze in one issue is justified: their host bodies differ by exactly one boolean argument, the auth check is identical, the post-state shape is identical. Splitting them would duplicate every test scaffold and obscure the symmetry.
  • Idempotency is explicit (the target_value is unconditionally assigned rather than only-on-change). This is intentional and the corresponding tests pin the behavior so a future "optimize: short-circuit on no-op" change can't silently turn an idempotent op into a panicking one.
  • The post-state always claims the ProtocolParameters PDA via Claim::Pda(...) even though the account is already claimed at initialize_program time. This matches the pattern other writers of ProtocolParameters (the admin set_* instructions in Plan 4) use. The runtime accepts re-claiming an already-PDA-claimed account.
  • No chained calls. freeze / unfreeze are a pure config flip — they do not touch the Token Program, the stability fee accumulator, or the redemption price state.

Dependencies

Depends on: Plan 1 (#156); #188.

Blocks: #190.

Metadata

Metadata

Assignees

No one assigned

    Type

    Fields

    No fields configured for Task.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions