fix(stablecoin): bound redemption controller price

This commit is contained in:
Ricardo Guilherme Schmidt 2026-06-29 13:55:59 -03:00
parent 1679d5a4b8
commit 35c4eb3373
No known key found for this signature in database
GPG Key ID: 1396EA17DE132FFE
3 changed files with 33 additions and 5 deletions

View File

@ -118,9 +118,9 @@
"accounts": [ "accounts": [
{ {
"name": "controller", "name": "controller",
"writable": false, "writable": true,
"signer": false, "signer": false,
"init": false "init": true
}, },
{ {
"name": "stablecoin_definition", "name": "stablecoin_definition",
@ -175,7 +175,7 @@
"accounts": [ "accounts": [
{ {
"name": "controller", "name": "controller",
"writable": false, "writable": true,
"signer": false, "signer": false,
"init": false "init": false
}, },

View File

@ -128,6 +128,7 @@ mod stablecoin {
#[instruction] #[instruction]
pub fn initialize_redemption_controller( pub fn initialize_redemption_controller(
ctx: ProgramContext, ctx: ProgramContext,
#[account(init)]
controller: AccountWithMetadata, controller: AccountWithMetadata,
stablecoin_definition: AccountWithMetadata, stablecoin_definition: AccountWithMetadata,
price_feed: AccountWithMetadata, price_feed: AccountWithMetadata,
@ -172,6 +173,7 @@ mod stablecoin {
#[instruction] #[instruction]
pub fn update_redemption_controller( pub fn update_redemption_controller(
ctx: ProgramContext, ctx: ProgramContext,
#[account(mut)]
controller: AccountWithMetadata, controller: AccountWithMetadata,
price_feed: AccountWithMetadata, price_feed: AccountWithMetadata,
current_timestamp: u64, current_timestamp: u64,

View File

@ -276,12 +276,13 @@ fn compute_next_controller_state(
fn apply_redemption_rate(redemption_price: u128, redemption_rate: i128, elapsed: u64) -> u128 { fn apply_redemption_rate(redemption_price: u128, redemption_rate: i128, elapsed: u64) -> u128 {
let drift = redemption_rate.saturating_mul(i128::from(elapsed)); let drift = redemption_rate.saturating_mul(i128::from(elapsed));
if drift >= 0 { let next_price = if drift >= 0 {
redemption_price.saturating_add(u128::try_from(drift).unwrap_or(u128::MAX)) redemption_price.saturating_add(u128::try_from(drift).unwrap_or(u128::MAX))
} else { } else {
let decrease = u128::try_from(drift.saturating_neg()).unwrap_or(u128::MAX); let decrease = u128::try_from(drift.saturating_neg()).unwrap_or(u128::MAX);
redemption_price.saturating_sub(decrease).max(1) redemption_price.saturating_sub(decrease).max(1)
} };
next_price.min(i128_max_as_u128())
} }
fn price_error(redemption_price: u128, market_price: u128) -> i128 { fn price_error(redemption_price: u128, market_price: u128) -> i128 {
@ -582,6 +583,31 @@ mod tests {
assert_eq!(updated.last_update_timestamp, 105); assert_eq!(updated.last_update_timestamp, 105);
} }
#[test]
fn controller_clamps_redemption_price_to_i128_max() {
let mut controller = controller_state();
controller.redemption_price = i128_max_as_u128() - 5;
controller.redemption_rate = 10;
controller.proportional_gain = 0;
let updated = compute_next_controller_state(&controller, i128_max_as_u128(), 101);
assert_eq!(updated.redemption_price, i128_max_as_u128());
assert_eq!(updated.redemption_rate, 0);
}
#[test]
fn controller_clamps_existing_redemption_price_above_i128_max() {
let mut controller = controller_state();
controller.redemption_price = i128_max_as_u128() + 5;
controller.redemption_rate = -1;
controller.proportional_gain = 0;
let updated = compute_next_controller_state(&controller, 1, 101);
assert_eq!(updated.redemption_price, i128_max_as_u128());
}
#[test] #[test]
fn controller_clamps_accumulated_error_and_redemption_rate() { fn controller_clamps_accumulated_error_and_redemption_rate() {
let mut controller = controller_state(); let mut controller = controller_state();