mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 14:11:09 +00:00
feat(redemption-controller): implement initialize and update functions for redemption rate feedback controller
- Added `initialize_redemption_controller` and `update_redemption_controller` functions to manage redemption rate feedback. - Introduced `RedemptionController` struct to maintain state for redemption rates. - Updated `Cargo.toml` and `Cargo.lock` to include `twap_oracle_core` dependency. - Modified integration tests to cover new redemption controller functionality.
This commit is contained in:
@@ -14,6 +14,12 @@ use spel_framework_macros::account_type;
|
||||
// compatibility.
|
||||
const POSITION_PDA_DOMAIN: &[u8] = b"POSITION";
|
||||
const POSITION_VAULT_PDA_DOMAIN: &[u8] = b"POSITION_VAULT";
|
||||
const REDEMPTION_CONTROLLER_PDA_DOMAIN: &[u8] = b"REDEMPTION_CONTROLLER";
|
||||
|
||||
/// Fixed-point denominator for controller gain parameters.
|
||||
///
|
||||
/// A gain of [`CONTROLLER_GAIN_SCALE`] means `1.0`.
|
||||
pub const CONTROLLER_GAIN_SCALE: u128 = 1_000_000_000;
|
||||
|
||||
/// Stablecoin Program Instruction.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -80,6 +86,47 @@ pub enum Instruction {
|
||||
/// Amount of stablecoin debt to repay (also the amount burned from the user's holding).
|
||||
amount: u128,
|
||||
},
|
||||
/// Initialize the global redemption-rate feedback controller for one stablecoin/feed pair.
|
||||
///
|
||||
/// Required accounts (3):
|
||||
/// - Redemption controller account (uninitialized, address must match
|
||||
/// `compute_redemption_controller_pda(self_program_id, stablecoin_definition, price_feed)`)
|
||||
/// - Stablecoin token definition account (initialized fungible token)
|
||||
/// - Oracle price feed account (initialized; its `program_owner` becomes the configured oracle
|
||||
/// program)
|
||||
///
|
||||
/// `proportional_gain` and `integral_gain` use [`CONTROLLER_GAIN_SCALE`] fixed-point
|
||||
/// precision. For example, `CONTROLLER_GAIN_SCALE / 10` represents `0.1`.
|
||||
InitializeRedemptionController {
|
||||
/// Asset that denominates both the oracle market price and redemption price.
|
||||
reference_asset_id: AccountId,
|
||||
/// Initial redemption price, in the same units and precision as the oracle price.
|
||||
initial_redemption_price: u128,
|
||||
/// Proportional controller gain, scaled by [`CONTROLLER_GAIN_SCALE`].
|
||||
proportional_gain: u128,
|
||||
/// Integral controller gain, scaled by [`CONTROLLER_GAIN_SCALE`].
|
||||
integral_gain: u128,
|
||||
/// Maximum absolute accumulated error before the integral term is clamped.
|
||||
max_integral_error: u128,
|
||||
/// Maximum absolute redemption rate, in price units per timestamp unit.
|
||||
max_redemption_rate: u128,
|
||||
/// Maximum allowed oracle price age. Uses the same timestamp unit as the oracle feed.
|
||||
max_price_feed_age: u64,
|
||||
/// Timestamp used to initialize the controller state.
|
||||
current_timestamp: u64,
|
||||
},
|
||||
/// Permissionlessly update redemption price and rate from the configured price feed.
|
||||
///
|
||||
/// Required accounts (2):
|
||||
/// - Redemption controller account (initialized, owned by `self_program_id`)
|
||||
/// - Configured oracle price feed account
|
||||
///
|
||||
/// If the configured feed is stale or unavailable, the controller account is emitted
|
||||
/// unchanged so redemption updates are paused.
|
||||
UpdateRedemptionController {
|
||||
/// Current block timestamp. The guest constrains output validity to this exact timestamp.
|
||||
current_timestamp: u64,
|
||||
},
|
||||
}
|
||||
|
||||
/// Persistent state held by a Stablecoin [`Position`] account.
|
||||
@@ -99,6 +146,42 @@ pub struct Position {
|
||||
pub debt_amount: u128,
|
||||
}
|
||||
|
||||
/// Global redemption feedback controller state for a stablecoin/feed pair.
|
||||
///
|
||||
/// `redemption_rate` is signed price drift per timestamp unit. Positive rates raise the
|
||||
/// redemption price; negative rates lower it. `accumulated_error` stores the integral term
|
||||
/// before gain scaling and is clamped by `max_integral_error`.
|
||||
#[account_type]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||
pub struct RedemptionController {
|
||||
/// Stablecoin token definition priced by the oracle feed.
|
||||
pub stablecoin_definition_id: AccountId,
|
||||
/// Asset that denominates both oracle prices and redemption price.
|
||||
pub reference_asset_id: AccountId,
|
||||
/// Configured oracle price feed account.
|
||||
pub price_feed_id: AccountId,
|
||||
/// Program expected to own `price_feed_id`.
|
||||
pub oracle_program_id: ProgramId,
|
||||
/// Current redemption price in oracle price units.
|
||||
pub redemption_price: u128,
|
||||
/// Current redemption rate in price units per timestamp unit.
|
||||
pub redemption_rate: i128,
|
||||
/// Integral controller state, clamped to `max_integral_error`.
|
||||
pub accumulated_error: i128,
|
||||
/// Proportional controller gain, scaled by [`CONTROLLER_GAIN_SCALE`].
|
||||
pub proportional_gain: u128,
|
||||
/// Integral controller gain, scaled by [`CONTROLLER_GAIN_SCALE`].
|
||||
pub integral_gain: u128,
|
||||
/// Maximum absolute accumulated error.
|
||||
pub max_integral_error: u128,
|
||||
/// Maximum absolute redemption rate.
|
||||
pub max_redemption_rate: u128,
|
||||
/// Maximum allowed oracle price age.
|
||||
pub max_price_feed_age: u64,
|
||||
/// Last timestamp at which the controller accepted a live oracle reading.
|
||||
pub last_update_timestamp: u64,
|
||||
}
|
||||
|
||||
impl TryFrom<&Data> for Position {
|
||||
type Error = std::io::Error;
|
||||
|
||||
@@ -116,6 +199,23 @@ impl From<&Position> for Data {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Data> for RedemptionController {
|
||||
type Error = std::io::Error;
|
||||
|
||||
fn try_from(data: &Data) -> Result<Self, Self::Error> {
|
||||
Self::try_from_slice(data.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RedemptionController> for Data {
|
||||
fn from(controller: &RedemptionController) -> Self {
|
||||
let mut data = Vec::with_capacity(std::mem::size_of_val(controller));
|
||||
BorshSerialize::serialize(controller, &mut data)
|
||||
.expect("Serialization to Vec should not fail");
|
||||
Self::try_from(data).expect("Redemption controller encoded data should fit into Data")
|
||||
}
|
||||
}
|
||||
|
||||
/// PDA seed for the [`Position`] account owned by `owner_id` for `collateral_definition_id`.
|
||||
///
|
||||
/// Derived from the owner and collateral definition addresses with a domain-separation tag
|
||||
@@ -175,6 +275,54 @@ pub fn compute_position_vault_pda(
|
||||
)
|
||||
}
|
||||
|
||||
/// PDA seed for the [`RedemptionController`] bound to a stablecoin and price feed.
|
||||
pub fn compute_redemption_controller_pda_seed(
|
||||
stablecoin_definition_id: AccountId,
|
||||
price_feed_id: AccountId,
|
||||
) -> PdaSeed {
|
||||
use risc0_zkvm::sha::{Impl, Sha256 as _};
|
||||
|
||||
let mut bytes = Vec::new();
|
||||
bytes.extend_from_slice(&stablecoin_definition_id.to_bytes());
|
||||
bytes.extend_from_slice(&price_feed_id.to_bytes());
|
||||
bytes.extend_from_slice(REDEMPTION_CONTROLLER_PDA_DOMAIN);
|
||||
|
||||
let mut out = [0u8; 32];
|
||||
out.copy_from_slice(Impl::hash_bytes(&bytes).as_bytes());
|
||||
PdaSeed::new(out)
|
||||
}
|
||||
|
||||
/// Account id of the [`RedemptionController`] PDA for a stablecoin/feed pair.
|
||||
pub fn compute_redemption_controller_pda(
|
||||
stablecoin_program_id: ProgramId,
|
||||
stablecoin_definition_id: AccountId,
|
||||
price_feed_id: AccountId,
|
||||
) -> AccountId {
|
||||
AccountId::for_public_pda(
|
||||
&stablecoin_program_id,
|
||||
&compute_redemption_controller_pda_seed(stablecoin_definition_id, price_feed_id),
|
||||
)
|
||||
}
|
||||
|
||||
/// Verify a redemption controller account address and return its PDA seed.
|
||||
///
|
||||
/// # Panics
|
||||
/// If `controller.account_id` does not match the configured PDA derivation.
|
||||
pub fn verify_redemption_controller_and_get_seed(
|
||||
controller: &AccountWithMetadata,
|
||||
stablecoin_definition_id: AccountId,
|
||||
price_feed_id: AccountId,
|
||||
stablecoin_program_id: ProgramId,
|
||||
) -> PdaSeed {
|
||||
let seed = compute_redemption_controller_pda_seed(stablecoin_definition_id, price_feed_id);
|
||||
let expected_id = AccountId::for_public_pda(&stablecoin_program_id, &seed);
|
||||
assert_eq!(
|
||||
controller.account_id, expected_id,
|
||||
"Redemption controller account ID does not match expected derivation"
|
||||
);
|
||||
seed
|
||||
}
|
||||
|
||||
/// Verify the position account's address matches
|
||||
/// `(stablecoin_program_id, owner, collateral_definition_id)` and return the [`PdaSeed`] for
|
||||
/// use in post-state claims.
|
||||
|
||||
Reference in New Issue
Block a user