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
8 changes: 8 additions & 0 deletions modules/stablecoin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ Reads the singleton Protocol Parameters account through
`lez_core`, verifies its PDA and owner, and exactly decodes its
data. All `u128`, `i128`, and `u64` values are returned as decimal strings.

### `stabilityFeeAccumulator()`

Reads the singleton Stability Fee Accumulator account through `lez_core`,
verifies its PDA and owner, and exactly decodes its stored snapshot. The result
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.

### `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 @@ -30,6 +30,14 @@ char *stablecoin_program_info(const char *request_json);
*/
char *stablecoin_decode_protocol_parameters(const char *request_json);

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

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

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

Expand All @@ -24,6 +27,27 @@ pub fn decode_protocol_parameters(request: DecodeProtocolParametersRequest) -> S
Ok(parameters_value(account_id, &parameters))
}

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

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

let accumulator = StabilityFeeAccumulator::try_from(&account.data)
.map_err(|_| StablecoinApiError::new("invalid_stability_fee_accumulator_data"))?;
Ok(stability_fee_accumulator_value(account_id, &accumulator))
}

fn parameters_value(
account_id: lee_core::account::AccountId,
parameters: &ProtocolParameters,
Expand Down Expand Up @@ -52,3 +76,16 @@ fn parameters_value(
"isFrozen": parameters.is_frozen,
})
}

fn stability_fee_accumulator_value(
account_id: lee_core::account::AccountId,
accumulator: &StabilityFeeAccumulator,
) -> Value {
json!({
"accountId": account_id.to_string(),
"accountIdHex": account_id_hex(account_id),
"accumulatedRateAtLastAccrual":
accumulator.accumulated_rate_at_last_accrual.to_string(),
"lastAccruedAt": accumulator.last_accrued_at.to_string(),
})
}
5 changes: 3 additions & 2 deletions modules/stablecoin/ffi/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ mod tests;

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

pub use decode::decode_protocol_parameters;
pub use decode::{decode_protocol_parameters, decode_stability_fee_accumulator};
pub use plan::initialize_program_plan;
pub use program::program_info;
pub use request::{
DecodeProtocolParametersRequest, InitializeProgramPlanRequest, ProgramInfoRequest,
DecodeProtocolParametersRequest, 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 @@ -19,6 +19,13 @@ pub struct DecodeProtocolParametersRequest {
pub protocol_parameters: AccountRead,
}

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

#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct InitializeProgramPlanRequest {
Expand Down
100 changes: 97 additions & 3 deletions modules/stablecoin/ffi/src/api/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ 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,
StabilityFeeAccumulator,
};
use token_core::TokenDefinition;
use twap_oracle_core::OraclePriceAccount;

use super::{
decode_protocol_parameters, initialize_program_plan, program_info,
DecodeProtocolParametersRequest, InitializeProgramPlanRequest, ProgramInfoRequest,
StablecoinResult,
decode_protocol_parameters, decode_stability_fee_accumulator, initialize_program_plan,
program_info, DecodeProtocolParametersRequest, DecodeStabilityFeeAccumulatorRequest,
InitializeProgramPlanRequest, ProgramInfoRequest, StablecoinResult,
};
use crate::account::{account_id_hex, account_read, program_id_bytes};

Expand Down Expand Up @@ -91,6 +92,26 @@ fn protocol_request(parameters: &ProtocolParameters) -> DecodeProtocolParameters
}
}

fn stability_fee_accumulator() -> StabilityFeeAccumulator {
StabilityFeeAccumulator {
accumulated_rate_at_last_accrual: u128::MAX,
last_accrued_at: u64::MAX,
}
}

fn accumulator_request(
accumulator: &StabilityFeeAccumulator,
) -> DecodeStabilityFeeAccumulatorRequest {
let account_id = compute_stability_fee_accumulator_pda(STABLECOIN_PROGRAM_ID);
DecodeStabilityFeeAccumulatorRequest {
stablecoin_program_id: program_id_hex(),
stability_fee_accumulator: account_read(
account_id,
&account(STABLECOIN_PROGRAM_ID, Data::from(accumulator)),
),
}
}

fn initialize_request() -> InitializeProgramPlanRequest {
let collateral_id = id(10);
let stablecoin_definition_id = compute_stablecoin_definition_pda(STABLECOIN_PROGRAM_ID);
Expand Down Expand Up @@ -292,6 +313,79 @@ fn protocol_parameters_decode_rejects_wrong_pda_owner_and_non_exact_data() {
);
}

#[test]
fn stability_fee_accumulator_decode_preserves_fixed_id_and_boundary_values() {
let accumulator = stability_fee_accumulator();
let value = ok(decode_stability_fee_accumulator(accumulator_request(
&accumulator,
)));

assert_eq!(
value["accountId"],
"E4tfkjjkPz2g1G3bpgkXQx4M7e7g4Lr2Mr8bxvAsSmzE"
);
assert_eq!(
value["accountIdHex"],
"c22718073968c322725e4ba774b270036c294663a547f43285a214614c1c258f"
);
assert_eq!(value["accumulatedRateAtLastAccrual"], u128::MAX.to_string());
assert_eq!(value["lastAccruedAt"], u64::MAX.to_string());
}

#[test]
fn stability_fee_accumulator_decode_rejects_failed_reads_and_wrong_identity() {
let accumulator = stability_fee_accumulator();

let mut missing = accumulator_request(&accumulator);
missing.stability_fee_accumulator.status = String::from("not_found");
missing.stability_fee_accumulator.account = None;
assert_error(
decode_stability_fee_accumulator(missing),
"account_read_failed",
);

let mut wrong_pda = accumulator_request(&accumulator);
wrong_pda.stability_fee_accumulator.id = account_id_hex(id(20));
assert_error(
decode_stability_fee_accumulator(wrong_pda),
"stability_fee_accumulator_pda_mismatch",
);

let mut wrong_owner = accumulator_request(&accumulator);
if let Some(account) = &mut wrong_owner.stability_fee_accumulator.account {
account.program_owner = hex::encode(program_id_bytes(TOKEN_PROGRAM_ID));
}
assert_error(
decode_stability_fee_accumulator(wrong_owner),
"stablecoin_program_mismatch",
);
}

#[test]
fn stability_fee_accumulator_decode_rejects_truncated_and_trailing_data() {
let accumulator = stability_fee_accumulator();
let account_id = compute_stability_fee_accumulator_pda(STABLECOIN_PROGRAM_ID);
let encoded = Data::from(&accumulator).as_ref().to_vec();

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

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

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

#[derive(Serialize)]
Expand Down Expand Up @@ -109,6 +109,23 @@ pub unsafe extern "C" fn stablecoin_decode_protocol_parameters(
}
}

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

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

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

#[test]
fn crate_root_reexports_stablecoin_surface() {
let _program_info: fn(ProgramInfoRequest) -> StablecoinResult = program_info;
let _decode: fn(DecodeProtocolParametersRequest) -> StablecoinResult =
decode_protocol_parameters;
let _decode_accumulator: fn(DecodeStabilityFeeAccumulatorRequest) -> StablecoinResult =
decode_stability_fee_accumulator;
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 @@ -278,6 +278,33 @@ LogosMap StablecoinModuleImpl::protocolParameters() {
});
}

LogosMap StablecoinModuleImpl::stabilityFeeAccumulator() {
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, "stabilityFeeAccumulatorIdHex"));
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_stability_fee_accumulator,
{
{"stablecoinProgramId", info["programIdHex"]},
{"stabilityFeeAccumulator", read},
});
if (!decoded.ok) {
return publicError(stablecoin_module::detail::stableFfiError(decoded.error));
}

LogosMap result = publicOk();
result["stabilityFeeAccumulator"] = 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 @@ -24,6 +24,10 @@ class StablecoinModuleImpl : public LogosModuleContext {
/// Success adds `protocolParameters`; failures use stable error codes.
LogosMap protocolParameters();

/// Reads and exactly decodes the singleton StabilityFeeAccumulator account.
/// Returns the stored snapshot without projecting it to the current time.
LogosMap stabilityFeeAccumulator();

/// 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,10 +165,12 @@ std::string stableFfiError(const std::string& error) {
"invalid_program_binary",
"invalid_program_id",
"invalid_protocol_parameters_data",
"invalid_stability_fee_accumulator_data",
"invalid_stablecoin_name",
"oracle_asset_mismatch",
"program_id_mismatch",
"protocol_parameters_pda_mismatch",
"stability_fee_accumulator_pda_mismatch",
"stablecoin_program_mismatch",
};
return stable.find(error) == stable.end() ? std::string("backend_error") : error;
Expand Down
10 changes: 9 additions & 1 deletion modules/stablecoin/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,20 @@ project(StablecoinModuleTests LANGUAGES CXX)
include(LogosTest)

logos_test(
NAME stablecoin_module_support_tests
NAME stablecoin_module_tests
MODULE_SOURCES
../src/stablecoin_module_impl.cpp
../src/stablecoin_module_support.cpp
TEST_SOURCES
main.cpp
stablecoin_module_impl_test.cpp
stablecoin_module_support_test.cpp
MOCK_C_SOURCES
mocks/mock_stablecoin_ffi.cpp
GENERATED_SOURCES
../generated_code/lez_core_api.cpp
EXTRA_INCLUDES
../src
../ffi/include
../generated_code
)
Loading
Loading