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
15 changes: 12 additions & 3 deletions modules/stablecoin/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Stablecoin core module

`stablecoin_module` is a headless Logos `core` module for the LEZ Stablecoin
Program. It exposes deployment discovery, protocol-parameter reads, and
protocol initialization through the same universal API used by `logoscore` and
UI modules.
Program. It exposes deployment discovery, protocol-state reads, and protocol
initialization through the same universal API used by `logoscore` and UI
modules.

The Qt-free C++ adapter handles live wallet reads and transaction submission.
`stablecoin_ffi` owns exact account decoding, PDA derivation, request
Expand Down Expand Up @@ -44,6 +44,15 @@ includes the account ID in base58 and lowercase hexadecimal form plus
`accumulatedRateAtLastAccrual` and `lastAccruedAt` as decimal strings. It does
not project the accumulator to the current time.

### `redemptionPriceState()`

Reads the singleton Redemption Price State account through `lez_core`, verifies
its PDA and owner, and exactly decodes its stored controller state. The result
includes the account ID in base58 and lowercase hexadecimal form plus
`redemptionPriceAtLastUpdate`, `redemptionRatePerMillisecond`,
`controllerIntegralTerm`, and `lastUpdatedAt` as decimal strings. It does not
project the current redemption price or simulate a controller update.

### `initializeProgram(request)`

Required request fields:
Expand Down
8 changes: 8 additions & 0 deletions modules/stablecoin/ffi/include/stablecoin_ffi.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ char *stablecoin_decode_protocol_parameters(const char *request_json);
*/
char *stablecoin_decode_stability_fee_accumulator(const char *request_json);

/**
* Decodes and validates the singleton `RedemptionPriceState` account.
*
* # Safety
* `request_json` must be null or point to a live NUL-terminated byte string.
*/
char *stablecoin_decode_redemption_price_state(const char *request_json);

/**
* Builds the exact wallet submission plan for `InitializeProgram`.
*
Expand Down
41 changes: 39 additions & 2 deletions modules/stablecoin/ffi/src/api/decode.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use serde_json::{json, Value};
use stablecoin_core::{
compute_protocol_parameters_pda, compute_stability_fee_accumulator_pda, ProtocolParameters,
compute_protocol_parameters_pda, compute_redemption_price_state_pda,
compute_stability_fee_accumulator_pda, ProtocolParameters, RedemptionPriceState,
StabilityFeeAccumulator,
};

use super::{
parse_stablecoin_program_id, DecodeProtocolParametersRequest,
DecodeStabilityFeeAccumulatorRequest, StablecoinApiError, StablecoinResult,
DecodeRedemptionPriceStateRequest, DecodeStabilityFeeAccumulatorRequest, StablecoinApiError,
StablecoinResult,
};
use crate::account::{account_id_hex, decode_account};

Expand Down Expand Up @@ -48,6 +50,27 @@ pub fn decode_stability_fee_accumulator(
Ok(stability_fee_accumulator_value(account_id, &accumulator))
}

pub fn decode_redemption_price_state(
request: DecodeRedemptionPriceStateRequest,
) -> StablecoinResult {
let stablecoin_program_id = parse_stablecoin_program_id(&request.stablecoin_program_id)?;
let (account_id, account) = decode_account(&request.redemption_price_state)
.map_err(|_| StablecoinApiError::new("account_read_failed"))?;

if account_id != compute_redemption_price_state_pda(stablecoin_program_id) {
return Err(StablecoinApiError::new(
"redemption_price_state_pda_mismatch",
));
}
if account.program_owner != stablecoin_program_id {
return Err(StablecoinApiError::new("stablecoin_program_mismatch"));
}

let state = RedemptionPriceState::try_from(&account.data)
.map_err(|_| StablecoinApiError::new("invalid_redemption_price_state_data"))?;
Ok(redemption_price_state_value(account_id, &state))
}

fn parameters_value(
account_id: lee_core::account::AccountId,
parameters: &ProtocolParameters,
Expand Down Expand Up @@ -89,3 +112,17 @@ fn stability_fee_accumulator_value(
"lastAccruedAt": accumulator.last_accrued_at.to_string(),
})
}

fn redemption_price_state_value(
account_id: lee_core::account::AccountId,
state: &RedemptionPriceState,
) -> Value {
json!({
"accountId": account_id.to_string(),
"accountIdHex": account_id_hex(account_id),
"redemptionPriceAtLastUpdate": state.redemption_price_at_last_update.to_string(),
"redemptionRatePerMillisecond": state.redemption_rate_per_millisecond.to_string(),
"controllerIntegralTerm": state.controller_integral_term.to_string(),
"lastUpdatedAt": state.last_updated_at.to_string(),
})
}
8 changes: 5 additions & 3 deletions modules/stablecoin/ffi/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ mod tests;

use std::{error::Error, fmt};

pub use decode::{decode_protocol_parameters, decode_stability_fee_accumulator};
pub use decode::{
decode_protocol_parameters, decode_redemption_price_state, decode_stability_fee_accumulator,
};
pub use plan::initialize_program_plan;
pub use program::program_info;
pub use request::{
DecodeProtocolParametersRequest, DecodeStabilityFeeAccumulatorRequest,
InitializeProgramPlanRequest, ProgramInfoRequest,
DecodeProtocolParametersRequest, DecodeRedemptionPriceStateRequest,
DecodeStabilityFeeAccumulatorRequest, InitializeProgramPlanRequest, ProgramInfoRequest,
};
use serde_json::Value;

Expand Down
7 changes: 7 additions & 0 deletions modules/stablecoin/ffi/src/api/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ pub struct DecodeStabilityFeeAccumulatorRequest {
pub stability_fee_accumulator: AccountRead,
}

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

#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct InitializeProgramPlanRequest {
Expand Down
112 changes: 109 additions & 3 deletions modules/stablecoin/ffi/src/api/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ use serde_json::{json, Value};
use stablecoin_core::{
compute_protocol_parameters_pda, compute_redemption_price_state_pda,
compute_stability_fee_accumulator_pda, compute_stablecoin_definition_pda,
compute_stablecoin_master_holding_pda, Instruction, ProtocolParameters,
compute_stablecoin_master_holding_pda, Instruction, ProtocolParameters, RedemptionPriceState,
StabilityFeeAccumulator,
};
use token_core::TokenDefinition;
use twap_oracle_core::OraclePriceAccount;

use super::{
decode_protocol_parameters, decode_stability_fee_accumulator, initialize_program_plan,
program_info, DecodeProtocolParametersRequest, DecodeStabilityFeeAccumulatorRequest,
decode_protocol_parameters, decode_redemption_price_state, decode_stability_fee_accumulator,
initialize_program_plan, program_info, DecodeProtocolParametersRequest,
DecodeRedemptionPriceStateRequest, DecodeStabilityFeeAccumulatorRequest,
InitializeProgramPlanRequest, ProgramInfoRequest, StablecoinResult,
};
use crate::account::{account_id_hex, account_read, program_id_bytes};
Expand Down Expand Up @@ -112,6 +113,28 @@ fn accumulator_request(
}
}

fn redemption_price_state(controller_integral_term: i128) -> RedemptionPriceState {
RedemptionPriceState {
redemption_price_at_last_update: u128::MAX,
redemption_rate_per_millisecond: u128::MAX,
controller_integral_term,
last_updated_at: u64::MAX,
}
}

fn redemption_price_state_request(
state: &RedemptionPriceState,
) -> DecodeRedemptionPriceStateRequest {
let account_id = compute_redemption_price_state_pda(STABLECOIN_PROGRAM_ID);
DecodeRedemptionPriceStateRequest {
stablecoin_program_id: program_id_hex(),
redemption_price_state: account_read(
account_id,
&account(STABLECOIN_PROGRAM_ID, Data::from(state)),
),
}
}

fn initialize_request() -> InitializeProgramPlanRequest {
let collateral_id = id(10);
let stablecoin_definition_id = compute_stablecoin_definition_pda(STABLECOIN_PROGRAM_ID);
Expand Down Expand Up @@ -386,6 +409,89 @@ fn stability_fee_accumulator_decode_rejects_truncated_and_trailing_data() {
}
}

#[test]
fn redemption_price_state_decode_preserves_fixed_id_and_boundary_values() {
let expected_id = compute_redemption_price_state_pda(STABLECOIN_PROGRAM_ID);
assert_eq!(
account_id_hex(expected_id),
"8ec72eaff1c70ac76ed0c139026671fc9991cae1466749dffb3280a5a5aed533"
);
assert_eq!(
expected_id.to_string(),
"AcM3xWAMKUEPPzCjT1EHssertgGhvDhLe8KGP1uvbRjp"
);

for controller_integral_term in [1, 0, -1, i128::MIN, i128::MAX] {
let state = redemption_price_state(controller_integral_term);
let value = ok(decode_redemption_price_state(
redemption_price_state_request(&state),
));

assert_eq!(value["accountId"], expected_id.to_string());
assert_eq!(value["accountIdHex"], account_id_hex(expected_id));
assert_eq!(value["redemptionPriceAtLastUpdate"], u128::MAX.to_string());
assert_eq!(value["redemptionRatePerMillisecond"], u128::MAX.to_string());
assert_eq!(
value["controllerIntegralTerm"],
controller_integral_term.to_string()
);
assert_eq!(value["lastUpdatedAt"], u64::MAX.to_string());
}
}

#[test]
fn redemption_price_state_decode_rejects_failed_reads_and_wrong_identity() {
let state = redemption_price_state(0);

for status in ["not_found", "backend_error"] {
let mut failed = redemption_price_state_request(&state);
failed.redemption_price_state.status = String::from(status);
failed.redemption_price_state.account = None;
assert_error(decode_redemption_price_state(failed), "account_read_failed");
}

let mut wrong_pda = redemption_price_state_request(&state);
wrong_pda.redemption_price_state.id = account_id_hex(id(20));
assert_error(
decode_redemption_price_state(wrong_pda),
"redemption_price_state_pda_mismatch",
);

let mut wrong_owner = redemption_price_state_request(&state);
if let Some(account) = &mut wrong_owner.redemption_price_state.account {
account.program_owner = hex::encode(program_id_bytes(TOKEN_PROGRAM_ID));
}
assert_error(
decode_redemption_price_state(wrong_owner),
"stablecoin_program_mismatch",
);
}

#[test]
fn redemption_price_state_decode_rejects_truncated_and_trailing_data() {
let state = redemption_price_state(i128::MIN);
let account_id = compute_redemption_price_state_pda(STABLECOIN_PROGRAM_ID);
let encoded = Data::from(&state).as_ref().to_vec();

for malformed in [encoded[..encoded.len() - 1].to_vec(), {
let mut trailing = encoded.clone();
trailing.push(0);
trailing
}] {
let request = DecodeRedemptionPriceStateRequest {
stablecoin_program_id: program_id_hex(),
redemption_price_state: account_read(
account_id,
&account(STABLECOIN_PROGRAM_ID, ok(Data::try_from(malformed))),
),
};
assert_error(
decode_redemption_price_state(request),
"invalid_redemption_price_state_data",
);
}
}

#[test]
fn initialize_plan_round_trips_all_boundary_values_and_exact_account_contract() {
let request = initialize_request();
Expand Down
19 changes: 17 additions & 2 deletions modules/stablecoin/ffi/src/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ use std::{
use serde::{de::DeserializeOwned, Serialize};

use crate::api::{
self, DecodeProtocolParametersRequest, DecodeStabilityFeeAccumulatorRequest,
InitializeProgramPlanRequest, ProgramInfoRequest, StablecoinResult,
self, DecodeProtocolParametersRequest, DecodeRedemptionPriceStateRequest,
DecodeStabilityFeeAccumulatorRequest, InitializeProgramPlanRequest, ProgramInfoRequest,
StablecoinResult,
};

#[derive(Serialize)]
Expand Down Expand Up @@ -126,6 +127,20 @@ pub unsafe extern "C" fn stablecoin_decode_stability_fee_accumulator(
}
}

#[unsafe(no_mangle)]
/// Decodes and validates the singleton `RedemptionPriceState` account.
///
/// # Safety
/// `request_json` must be null or point to a live NUL-terminated byte string.
pub unsafe extern "C" fn stablecoin_decode_redemption_price_state(
request_json: *const c_char,
) -> *mut c_char {
// SAFETY: Forwarded from this function's caller contract.
unsafe {
call::<DecodeRedemptionPriceStateRequest>(request_json, api::decode_redemption_price_state)
}
}

#[unsafe(no_mangle)]
/// Builds the exact wallet submission plan for `InitializeProgram`.
///
Expand Down
5 changes: 3 additions & 2 deletions modules/stablecoin/ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ pub mod api;

pub use account::{AccountRead, WalletAccount};
pub use api::{
decode_protocol_parameters, decode_stability_fee_accumulator, initialize_program_plan,
program_info, DecodeProtocolParametersRequest, DecodeStabilityFeeAccumulatorRequest,
decode_protocol_parameters, decode_redemption_price_state, decode_stability_fee_accumulator,
initialize_program_plan, program_info, DecodeProtocolParametersRequest,
DecodeRedemptionPriceStateRequest, DecodeStabilityFeeAccumulatorRequest,
InitializeProgramPlanRequest, ProgramInfoRequest, StablecoinApiError, StablecoinResponse,
StablecoinResult,
};
7 changes: 5 additions & 2 deletions modules/stablecoin/ffi/tests/public_api.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use stablecoin_ffi::{
decode_protocol_parameters, decode_stability_fee_accumulator, initialize_program_plan,
program_info, DecodeProtocolParametersRequest, DecodeStabilityFeeAccumulatorRequest,
decode_protocol_parameters, decode_redemption_price_state, decode_stability_fee_accumulator,
initialize_program_plan, program_info, DecodeProtocolParametersRequest,
DecodeRedemptionPriceStateRequest, DecodeStabilityFeeAccumulatorRequest,
InitializeProgramPlanRequest, ProgramInfoRequest, StablecoinResult,
};

Expand All @@ -11,5 +12,7 @@ fn crate_root_reexports_stablecoin_surface() {
decode_protocol_parameters;
let _decode_accumulator: fn(DecodeStabilityFeeAccumulatorRequest) -> StablecoinResult =
decode_stability_fee_accumulator;
let _decode_redemption_state: fn(DecodeRedemptionPriceStateRequest) -> StablecoinResult =
decode_redemption_price_state;
let _initialize: fn(InitializeProgramPlanRequest) -> StablecoinResult = initialize_program_plan;
}
27 changes: 27 additions & 0 deletions modules/stablecoin/src/stablecoin_module_impl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,33 @@ LogosMap StablecoinModuleImpl::stabilityFeeAccumulator() {
});
}

LogosMap StablecoinModuleImpl::redemptionPriceState() {
return guarded([&]() -> LogosMap {
std::string error;
const json info = stablecoinProgramInfo(error);
if (!info.is_object()) return publicError(error.empty() ? "backend_error" : error);

const json read = readPublicAccount(jsonString(info, "redemptionPriceStateIdHex"));
const std::string status = jsonString(read, "status");
if (status == "not_found") return publicError("not_initialized");
if (status != "ok") return publicError("account_read_failed");

const FfiResult decoded = callStablecoin(
stablecoin_decode_redemption_price_state,
{
{"stablecoinProgramId", info["programIdHex"]},
{"redemptionPriceState", read},
});
if (!decoded.ok) {
return publicError(stablecoin_module::detail::stableFfiError(decoded.error));
}

LogosMap result = publicOk();
result["redemptionPriceState"] = decoded.value;
return result;
});
}

LogosMap StablecoinModuleImpl::submitPlan(const nlohmann::json& plan) {
const auto accounts_field = plan.find("accountIds");
const auto signers_field = plan.find("signingRequirements");
Expand Down
4 changes: 4 additions & 0 deletions modules/stablecoin/src/stablecoin_module_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ class StablecoinModuleImpl : public LogosModuleContext {
/// Returns the stored snapshot without projecting it to the current time.
LogosMap stabilityFeeAccumulator();

/// Reads and exactly decodes the singleton RedemptionPriceState account.
/// Returns stored controller state without projecting the current price.
LogosMap redemptionPriceState();

/// Initializes the stablecoin protocol. Request fields are `adminId`,
/// `freezeAuthorityId`, `collateralDefinitionId`, `marketPriceOracleId`,
/// `initialStabilityFeePerMillisecond`,
Expand Down
2 changes: 2 additions & 0 deletions modules/stablecoin/src/stablecoin_module_support.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,13 @@ std::string stableFfiError(const std::string& error) {
"invalid_program_binary",
"invalid_program_id",
"invalid_protocol_parameters_data",
"invalid_redemption_price_state_data",
"invalid_stability_fee_accumulator_data",
"invalid_stablecoin_name",
"oracle_asset_mismatch",
"program_id_mismatch",
"protocol_parameters_pda_mismatch",
"redemption_price_state_pda_mismatch",
"stability_fee_accumulator_pda_mismatch",
"stablecoin_program_mismatch",
};
Expand Down
Loading
Loading