Merge 092d82cd207ba899027823a2950aa124e76f61f1 into 065a4e4937a03424ec1d5141f1496e9d1bb40765

This commit is contained in:
Andrea Franz 2026-06-29 17:02:35 +02:00 committed by GitHub
commit 1f0f7fa433
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 223 additions and 0 deletions

View File

@ -115,6 +115,62 @@
}
],
"accounts": [
{
"name": "ProtocolParameters",
"type": {
"kind": "struct",
"fields": [
{
"name": "admin_account_id",
"type": "account_id"
},
{
"name": "freeze_authority_account_id",
"type": "account_id"
},
{
"name": "stablecoin_definition_id",
"type": "account_id"
},
{
"name": "collateral_definition_id",
"type": "account_id"
},
{
"name": "market_price_oracle_id",
"type": "account_id"
},
{
"name": "stability_fee_per_millisecond",
"type": "u128"
},
{
"name": "controller_proportional_gain",
"type": "i128"
},
{
"name": "controller_integral_gain",
"type": "i128"
},
{
"name": "minimum_collateralization_ratio",
"type": "u128"
},
{
"name": "minimum_milliseconds_between_rate_updates",
"type": "u64"
},
{
"name": "maximum_oracle_price_age_milliseconds",
"type": "u64"
},
{
"name": "is_frozen",
"type": "bool"
}
]
}
},
{
"name": "Position",
"type": {

View File

@ -2,11 +2,16 @@
pub mod math;
pub mod protocol_parameters;
use borsh::{BorshDeserialize, BorshSerialize};
use nssa_core::{
account::{AccountId, AccountWithMetadata, Data},
program::{PdaSeed, ProgramId},
};
pub use protocol_parameters::{
compute_protocol_parameters_pda, compute_protocol_parameters_pda_seed, ProtocolParameters,
};
use serde::{Deserialize, Serialize};
use spel_framework_macros::account_type;

View File

@ -0,0 +1,162 @@
//! Single-instance configuration account for the Stablecoin program.
//!
//! Created at [`initialize_program`] time, written by admin `set_*`
//! instructions and by `freeze` / `unfreeze`. Read by nearly every other
//! instruction.
use borsh::{BorshDeserialize, BorshSerialize};
use nssa_core::{
account::{AccountId, Data},
program::{PdaSeed, ProgramId},
};
use serde::{Deserialize, Serialize};
use spel_framework_macros::account_type;
/// PDA seed for the single-instance [`ProtocolParameters`] account.
const PROTOCOL_PARAMETERS_PDA_DOMAIN: &[u8; 32] = b"STABLECOIN__PROTOCOL_PARAMS_____";
/// Single source of truth for the protocol's configurable knobs.
///
/// See the spec, §4.1 for field semantics. Immutability comments mirror the
/// rationale documented there.
#[account_type]
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct ProtocolParameters {
/// Authority required for every parameter-update and admin-rotation
/// instruction.
pub admin_account_id: AccountId,
/// Authority required for `freeze` / `unfreeze`.
pub freeze_authority_account_id: AccountId,
/// The stablecoin's `TokenDefinition` PDA. IMMUTABLE — changing it would
/// break supply accounting against the existing on-chain stablecoin
/// float.
pub stablecoin_definition_id: AccountId,
/// The single accepted collateral's `TokenDefinition`. IMMUTABLE —
/// changing it would orphan every position vault.
pub collateral_definition_id: AccountId,
/// Producer of the market price observation used by
/// `update_redemption_rate` and the oracle staleness gate in
/// `generate_debt`.
pub market_price_oracle_id: AccountId,
/// Per-millisecond stability fee multiplier in fixed-point, stored as
/// `(1 + r_per_millisecond) * FIXED_POINT_ONE`.
pub stability_fee_per_millisecond: u128,
/// PI controller `Kp`. Signed.
pub controller_proportional_gain: i128,
/// PI controller `Ki`. Signed.
pub controller_integral_gain: i128,
/// Minimum collateralization ratio in fixed-point (e.g.
/// `1.5 * FIXED_POINT_ONE` = 150%).
pub minimum_collateralization_ratio: u128,
/// Minimum milliseconds between successful `update_redemption_rate` calls.
pub minimum_milliseconds_between_rate_updates: u64,
/// Reject oracle observations older than this (RFP R3 staleness gate).
pub maximum_oracle_price_age_milliseconds: u64,
/// `true` blocks `open_position`, `generate_debt`, `withdraw_collateral`.
pub is_frozen: bool,
}
impl TryFrom<&Data> for ProtocolParameters {
type Error = std::io::Error;
fn try_from(data: &Data) -> Result<Self, Self::Error> {
Self::try_from_slice(data.as_ref())
}
}
impl From<&ProtocolParameters> for Data {
fn from(params: &ProtocolParameters) -> Self {
let len = borsh::object_length(params).expect("ProtocolParameters length must be known");
let mut buf = Vec::with_capacity(len);
BorshSerialize::serialize(params, &mut buf)
.expect("ProtocolParameters serialization should not fail");
Self::try_from(buf).expect("ProtocolParameters encoded data should fit into Data")
}
}
/// PDA seed for [`ProtocolParameters`] (single-instance — no per-deployment
/// uniqueness needed; the program id already discriminates).
#[must_use]
pub fn compute_protocol_parameters_pda_seed() -> PdaSeed {
use risc0_zkvm::sha::{Impl, Sha256 as _};
let mut out = [0u8; 32];
out.copy_from_slice(Impl::hash_bytes(PROTOCOL_PARAMETERS_PDA_DOMAIN).as_bytes());
PdaSeed::new(out)
}
/// Account id of the [`ProtocolParameters`] PDA under the given stablecoin
/// program.
#[must_use]
pub fn compute_protocol_parameters_pda(stablecoin_program_id: ProgramId) -> AccountId {
AccountId::for_public_pda(
&stablecoin_program_id,
&compute_protocol_parameters_pda_seed(),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::FIXED_POINT_ONE;
fn sample_params() -> ProtocolParameters {
ProtocolParameters {
admin_account_id: AccountId::new([0xAA; 32]),
freeze_authority_account_id: AccountId::new([0xFF; 32]),
stablecoin_definition_id: AccountId::new([0x01; 32]),
collateral_definition_id: AccountId::new([0x02; 32]),
market_price_oracle_id: AccountId::new([0x03; 32]),
stability_fee_per_millisecond: FIXED_POINT_ONE + 1_500_000_000_000_000, /* ε ≈ 1.5×10^15 → ~5% annual */
controller_proportional_gain: -123_456_789,
controller_integral_gain: 987_654_321,
minimum_collateralization_ratio: FIXED_POINT_ONE * 3 / 2,
minimum_milliseconds_between_rate_updates: 300_000,
maximum_oracle_price_age_milliseconds: 900_000,
is_frozen: false,
}
}
#[test]
fn borsh_roundtrip_preserves_every_field() {
let params = sample_params();
let data: Data = (&params).into();
let decoded = ProtocolParameters::try_from(&data).expect("decode");
assert_eq!(decoded, params);
}
#[test]
fn borsh_roundtrip_with_is_frozen_true() {
let mut params = sample_params();
params.is_frozen = true;
let data: Data = (&params).into();
let decoded = ProtocolParameters::try_from(&data).expect("decode");
assert!(decoded.is_frozen);
assert_eq!(decoded, params);
}
#[test]
fn pda_is_deterministic_for_fixed_program_id() {
let program_id: ProgramId = [42u32; 8];
let first = compute_protocol_parameters_pda(program_id);
let second = compute_protocol_parameters_pda(program_id);
assert_eq!(first, second);
}
#[test]
fn pda_differs_for_different_program_ids() {
let id_a: ProgramId = [1u32; 8];
let id_b: ProgramId = [2u32; 8];
assert_ne!(
compute_protocol_parameters_pda(id_a),
compute_protocol_parameters_pda(id_b),
);
}
#[test]
fn seed_is_deterministic() {
let seed_a = compute_protocol_parameters_pda_seed();
let seed_b = compute_protocol_parameters_pda_seed();
assert_eq!(seed_a, seed_b);
}
}