Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions modules/stablecoin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,31 @@ read-only quote with `canSubmit: false`, `code: "blocked"`, machine-readable
`errors`, and explicit `null` next-controller values. Multiple blockers are
reported in on-chain gate order. The frozen flag does not block this operation.

### Permissionless maintenance transactions

`accrueStabilityFee(callerId)`, `updateRedemptionRate(callerId)`, and
`refreshGlobals(callerId)` submit permissionless protocol-maintenance
transactions. `callerId` accepts base58 or 64-character hexadecimal form, must
be a public account controlled by the connected wallet, and is the sole signer.
Success adds `transactionId` to the standard response envelope.

The module reads live protocol state, derives every singleton account and the
canonical `CLOCK_01` account internally, then submits these exact account
orders:

| Method | Accounts |
| --- | --- |
| `accrueStabilityFee` | caller, Protocol Parameters, Stability Fee Accumulator, `CLOCK_01` |
| `updateRedemptionRate` | caller, Protocol Parameters, Redemption Price State, configured market-price oracle, `CLOCK_01` |
| `refreshGlobals` | caller, Protocol Parameters, Stability Fee Accumulator, Redemption Price State, configured market-price oracle, `CLOCK_01` |

`updateRedemptionRate` runs the live quote preflight and does not submit when
the first on-chain gate is `oracle_stale`, `oracle_price_zero`, or
`rate_update_too_soon`. `refreshGlobals` intentionally submits under those soft
gates: its fee-accrual half still runs while the on-chain instruction may skip
the controller update. A frozen protocol does not block any of the three
maintenance methods.

### `initializeProgram(request)`

Required request fields:
Expand Down
24 changes: 24 additions & 0 deletions modules/stablecoin/ffi/include/stablecoin_ffi.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,30 @@ char *stablecoin_current_global_state(const char *request_json);
*/
char *stablecoin_redemption_rate_update_quote(const char *request_json);

/**
* Builds the exact wallet submission plan for `AccrueStabilityFee`.
*
* # Safety
* `request_json` must be null or point to a live NUL-terminated byte string.
*/
char *stablecoin_accrue_stability_fee_plan(const char *request_json);

/**
* Builds a preflighted wallet submission plan for `UpdateRedemptionRate`.
*
* # Safety
* `request_json` must be null or point to a live NUL-terminated byte string.
*/
char *stablecoin_update_redemption_rate_plan(const char *request_json);

/**
* Builds the best-effort wallet submission plan for `RefreshGlobals`.
*
* # Safety
* `request_json` must be null or point to a live NUL-terminated byte string.
*/
char *stablecoin_refresh_globals_plan(const char *request_json);

/**
* Builds the exact wallet submission plan for `InitializeProgram`.
*
Expand Down
12 changes: 8 additions & 4 deletions modules/stablecoin/ffi/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,18 @@ use std::{error::Error, fmt};
pub use decode::{
decode_protocol_parameters, decode_redemption_price_state, decode_stability_fee_accumulator,
};
pub use plan::initialize_program_plan;
pub use plan::{
accrue_stability_fee_plan, initialize_program_plan, refresh_globals_plan,
update_redemption_rate_plan,
};
pub use program::program_info;
pub use projection::current_global_state;
pub use quote::redemption_rate_update_quote;
pub use request::{
CurrentGlobalStateRequest, DecodeProtocolParametersRequest, DecodeRedemptionPriceStateRequest,
DecodeStabilityFeeAccumulatorRequest, InitializeProgramPlanRequest, ProgramInfoRequest,
RedemptionRateUpdateQuoteRequest,
AccrueStabilityFeePlanRequest, CurrentGlobalStateRequest, DecodeProtocolParametersRequest,
DecodeRedemptionPriceStateRequest, DecodeStabilityFeeAccumulatorRequest,
InitializeProgramPlanRequest, ProgramInfoRequest, RedemptionRateUpdateQuoteRequest,
RefreshGlobalsPlanRequest, UpdateRedemptionRatePlanRequest,
};
use serde_json::Value;

Expand Down
114 changes: 109 additions & 5 deletions modules/stablecoin/ffi/src/api/plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,16 @@ use token_core::TokenDefinition;
use twap_oracle_core::OraclePriceAccount;

use super::{
parse_stablecoin_program_id, InitializeProgramPlanRequest, StablecoinApiError, StablecoinResult,
decode::{
validated_protocol_parameters, validated_redemption_price_state,
validated_stability_fee_accumulator,
},
parse_stablecoin_program_id,
projection::clock_timestamp,
quote::{redemption_rate_update_quote, validated_market_price_oracle},
AccrueStabilityFeePlanRequest, InitializeProgramPlanRequest, RedemptionRateUpdateQuoteRequest,
RefreshGlobalsPlanRequest, StablecoinApiError, StablecoinResult,
UpdateRedemptionRatePlanRequest,
};
use crate::account::{
account_id_from_hex, account_id_hex, decode_account, program_id_bytes, AccountRead,
Expand Down Expand Up @@ -95,6 +104,101 @@ pub fn initialize_program_plan(request: InitializeProgramPlanRequest) -> Stablec
)
}

pub fn accrue_stability_fee_plan(request: AccrueStabilityFeePlanRequest) -> StablecoinResult {
let program_id = parse_stablecoin_program_id(&request.stablecoin_program_id)?;
let caller = parse_account_id(&request.caller_id)?;
validated_protocol_parameters(program_id, &request.protocol_parameters)?;
validated_stability_fee_accumulator(program_id, &request.stability_fee_accumulator)?;
clock_timestamp(&request.clock)?;

plan_response(
program_id,
[
caller,
compute_protocol_parameters_pda(program_id),
compute_stability_fee_accumulator_pda(program_id),
CLOCK_01_PROGRAM_ACCOUNT_ID,
],
[true, false, false, false],
Instruction::AccrueStabilityFee,
)
}

pub fn update_redemption_rate_plan(request: UpdateRedemptionRatePlanRequest) -> StablecoinResult {
let program_id = parse_stablecoin_program_id(&request.stablecoin_program_id)?;
let caller = parse_account_id(&request.caller_id)?;
let (_, parameters) = validated_protocol_parameters(program_id, &request.protocol_parameters)?;

let quote = redemption_rate_update_quote(RedemptionRateUpdateQuoteRequest {
stablecoin_program_id: request.stablecoin_program_id,
protocol_parameters: request.protocol_parameters,
redemption_price_state: request.redemption_price_state,
market_price_oracle: request.market_price_oracle,
clock: request.clock,
})?;
require_ready_quote(&quote)?;

plan_response(
program_id,
[
caller,
compute_protocol_parameters_pda(program_id),
compute_redemption_price_state_pda(program_id),
parameters.market_price_oracle_id,
CLOCK_01_PROGRAM_ACCOUNT_ID,
],
[true, false, false, false, false],
Instruction::UpdateRedemptionRate,
)
}

pub fn refresh_globals_plan(request: RefreshGlobalsPlanRequest) -> StablecoinResult {
let program_id = parse_stablecoin_program_id(&request.stablecoin_program_id)?;
let caller = parse_account_id(&request.caller_id)?;
let (_, parameters) = validated_protocol_parameters(program_id, &request.protocol_parameters)?;
validated_stability_fee_accumulator(program_id, &request.stability_fee_accumulator)?;
validated_redemption_price_state(program_id, &request.redemption_price_state)?;
validated_market_price_oracle(
&request.market_price_oracle,
parameters.market_price_oracle_id,
)?;
clock_timestamp(&request.clock)?;

plan_response(
program_id,
[
caller,
compute_protocol_parameters_pda(program_id),
compute_stability_fee_accumulator_pda(program_id),
compute_redemption_price_state_pda(program_id),
parameters.market_price_oracle_id,
CLOCK_01_PROGRAM_ACCOUNT_ID,
],
[true, false, false, false, false, false],
Instruction::RefreshGlobals,
)
}

fn require_ready_quote(quote: &Value) -> Result<(), StablecoinApiError> {
if quote.get("canSubmit").and_then(Value::as_bool) == Some(true) {
return Ok(());
}

let blocker = quote
.get("errors")
.and_then(Value::as_array)
.and_then(|errors| errors.first())
.and_then(|error| error.get("code"))
.and_then(Value::as_str);
let code = match blocker {
Some("oracle_stale") => "oracle_stale",
Some("oracle_price_zero") => "oracle_price_zero",
Some("rate_update_too_soon") => "rate_update_too_soon",
_ => "backend_error",
};
Err(StablecoinApiError::new(code))
}

fn required_account(
read: &AccountRead,
) -> Result<(AccountId, lee_core::account::Account), StablecoinApiError> {
Expand Down Expand Up @@ -177,18 +281,18 @@ fn parse_i128(value: &Value) -> Result<i128, StablecoinApiError> {
}
}

fn plan_response(
fn plan_response<const ACCOUNT_COUNT: usize>(
program_id: lee_core::program::ProgramId,
account_ids: [AccountId; 9],
signing_requirements: [bool; 9],
account_ids: [AccountId; ACCOUNT_COUNT],
signing_requirements: [bool; ACCOUNT_COUNT],
instruction: Instruction,
) -> StablecoinResult {
let instruction = risc0_zkvm::serde::to_vec(&instruction)
.map_err(|_| StablecoinApiError::new("backend_error"))?;
Ok(json!({
"programId": hex::encode(program_id_bytes(program_id)),
"accountIds": account_ids.into_iter().map(account_id_hex).collect::<Vec<_>>(),
"signingRequirements": signing_requirements,
"signingRequirements": signing_requirements.into_iter().collect::<Vec<_>>(),
"instruction": instruction,
}))
}
2 changes: 1 addition & 1 deletion modules/stablecoin/ffi/src/api/quote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ pub fn redemption_rate_update_quote(request: RedemptionRateUpdateQuoteRequest) -
}))
}

fn validated_market_price_oracle(
pub(super) fn validated_market_price_oracle(
read: &crate::AccountRead,
expected_id: lee_core::account::AccountId,
) -> Result<OraclePriceAccount, StablecoinApiError> {
Expand Down
33 changes: 33 additions & 0 deletions modules/stablecoin/ffi/src/api/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,39 @@ pub struct RedemptionRateUpdateQuoteRequest {
pub clock: AccountRead,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AccrueStabilityFeePlanRequest {
pub stablecoin_program_id: String,
pub caller_id: String,
pub protocol_parameters: AccountRead,
pub stability_fee_accumulator: AccountRead,
pub clock: AccountRead,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct UpdateRedemptionRatePlanRequest {
pub stablecoin_program_id: String,
pub caller_id: String,
pub protocol_parameters: AccountRead,
pub redemption_price_state: AccountRead,
pub market_price_oracle: AccountRead,
pub clock: AccountRead,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RefreshGlobalsPlanRequest {
pub stablecoin_program_id: String,
pub caller_id: String,
pub protocol_parameters: AccountRead,
pub stability_fee_accumulator: AccountRead,
pub redemption_price_state: AccountRead,
pub market_price_oracle: AccountRead,
pub clock: AccountRead,
}

#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct InitializeProgramPlanRequest {
Expand Down
Loading
Loading