mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-07-03 13:39:38 +00:00
feat(stablecoin): Implement stability fee accrual
This commit is contained in:
parent
4a6192d84f
commit
d5c0f5c8a2
11
Cargo.lock
generated
11
Cargo.lock
generated
@ -462,9 +462,9 @@ checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.7"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe"
|
||||
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
|
||||
|
||||
[[package]]
|
||||
name = "ata-methods"
|
||||
@ -3307,9 +3307,9 @@ checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.2"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
||||
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hex"
|
||||
@ -3788,9 +3788,12 @@ dependencies = [
|
||||
name = "stablecoin_program"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"borsh",
|
||||
"clock_core",
|
||||
"lee_core",
|
||||
"stablecoin_core",
|
||||
"token_core",
|
||||
"twap_oracle_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@ -2,6 +2,168 @@
|
||||
"version": "0.1.0",
|
||||
"name": "stablecoin",
|
||||
"instructions": [
|
||||
{
|
||||
"name": "initialize_program",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "admin",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "protocol_parameters",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "stability_fee_accumulator",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "redemption_price_state",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "stablecoin_definition",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "stablecoin_master_holding",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "collateral_definition",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "market_price_oracle",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "clock",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "freeze_authority_account_id",
|
||||
"type": "account_id"
|
||||
},
|
||||
{
|
||||
"name": "initial_stability_fee_per_millisecond",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "initial_controller_proportional_gain",
|
||||
"type": "i128"
|
||||
},
|
||||
{
|
||||
"name": "initial_controller_integral_gain",
|
||||
"type": "i128"
|
||||
},
|
||||
{
|
||||
"name": "initial_minimum_collateralization_ratio",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "minimum_milliseconds_between_rate_updates",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "maximum_oracle_price_age_milliseconds",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "initial_redemption_price",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "stablecoin_name",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "accrue_stability_fee",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "caller",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "protocol_parameters",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "stability_fee_accumulator",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "clock",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "set_stability_fee_per_millisecond",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "admin",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "protocol_parameters",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "stability_fee_accumulator",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "clock",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "new_rate",
|
||||
"type": "u128"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "open_position",
|
||||
"accounts": [
|
||||
@ -30,7 +192,19 @@
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "token_definition",
|
||||
"name": "collateral_definition",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "protocol_parameters",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "clock",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
@ -38,7 +212,76 @@
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "collateral_amount",
|
||||
"name": "position_nonce",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "initial_collateral_amount",
|
||||
"type": "u128"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "generate_debt",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "owner",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "position",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "stablecoin_definition",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "user_stablecoin_holding",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "stability_fee_accumulator",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "redemption_price_state",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "market_price_oracle",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "protocol_parameters",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "clock",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "amount",
|
||||
"type": "u128"
|
||||
}
|
||||
]
|
||||
@ -69,6 +312,30 @@
|
||||
"writable": true,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "stability_fee_accumulator",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "redemption_price_state",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "protocol_parameters",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "clock",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
@ -104,6 +371,24 @@
|
||||
"writable": true,
|
||||
"signer": true,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "stability_fee_accumulator",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "protocol_parameters",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
},
|
||||
{
|
||||
"name": "clock",
|
||||
"writable": false,
|
||||
"signer": false,
|
||||
"init": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
@ -116,25 +401,129 @@
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"name": "Position",
|
||||
"name": "ProtocolParameters",
|
||||
"type": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
{
|
||||
"name": "collateral_vault_id",
|
||||
"name": "admin_account_id",
|
||||
"type": "account_id"
|
||||
},
|
||||
{
|
||||
"name": "freeze_authority_account_id",
|
||||
"type": "account_id"
|
||||
},
|
||||
{
|
||||
"name": "stablecoin_definition_id",
|
||||
"type": "account_id"
|
||||
},
|
||||
{
|
||||
"name": "collateral_definition_id",
|
||||
"type": "account_id"
|
||||
},
|
||||
{
|
||||
"name": "market_price_oracle_id",
|
||||
"type": "account_id"
|
||||
},
|
||||
{
|
||||
"name": "stability_fee_per_millisecond",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "controller_proportional_gain",
|
||||
"type": "i128"
|
||||
},
|
||||
{
|
||||
"name": "controller_integral_gain",
|
||||
"type": "i128"
|
||||
},
|
||||
{
|
||||
"name": "minimum_collateralization_ratio",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "minimum_milliseconds_between_rate_updates",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "maximum_oracle_price_age_milliseconds",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "is_frozen",
|
||||
"type": "bool"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "StabilityFeeAccumulator",
|
||||
"type": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
{
|
||||
"name": "accumulated_rate_at_last_accrual",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "last_accrued_at",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "RedemptionPriceState",
|
||||
"type": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
{
|
||||
"name": "redemption_price_at_last_update",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "redemption_rate_per_millisecond",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "controller_integral_term",
|
||||
"type": "i128"
|
||||
},
|
||||
{
|
||||
"name": "last_updated_at",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Position",
|
||||
"type": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
{
|
||||
"name": "owner_account_id",
|
||||
"type": "account_id"
|
||||
},
|
||||
{
|
||||
"name": "position_nonce",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "vault_account_id",
|
||||
"type": "account_id"
|
||||
},
|
||||
{
|
||||
"name": "collateral_amount",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "debt_amount",
|
||||
"name": "normalized_debt_amount",
|
||||
"type": "u128"
|
||||
},
|
||||
{
|
||||
"name": "opened_at",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,10 +1,29 @@
|
||||
#![allow(
|
||||
clippy::panic,
|
||||
clippy::unwrap_used,
|
||||
reason = "integration fixtures use fixed balances and transaction assertions"
|
||||
)]
|
||||
|
||||
use clock_core::{ClockAccountData, CLOCK_01_PROGRAM_ACCOUNT_ID};
|
||||
use nssa::{
|
||||
error::LeeError,
|
||||
program_deployment_transaction::{self, ProgramDeploymentTransaction},
|
||||
public_transaction, PrivateKey, PublicKey, PublicTransaction, V03State,
|
||||
};
|
||||
use nssa_core::account::{Account, AccountId, Data, Nonce};
|
||||
use stablecoin_core::{compute_position_pda, compute_position_vault_pda, Position};
|
||||
use stablecoin_core::{
|
||||
compound_rate, compute_position_pda, compute_position_vault_pda,
|
||||
compute_protocol_parameters_pda, compute_redemption_price_state_pda,
|
||||
compute_stability_fee_accumulator_pda, compute_stablecoin_definition_pda,
|
||||
compute_stablecoin_master_holding_pda, Position, ProtocolParameters, RedemptionPriceState,
|
||||
StabilityFeeAccumulator, FIXED_POINT_ONE,
|
||||
};
|
||||
use token_core::{TokenDefinition, TokenHolding};
|
||||
use twap_oracle_core::OraclePriceAccount;
|
||||
|
||||
const POSITION_NONCE: u64 = 7;
|
||||
const CLOCK_START: u64 = 1_000;
|
||||
const Q64_ONE: u128 = 18_446_744_073_709_551_616;
|
||||
|
||||
struct Keys;
|
||||
struct Ids;
|
||||
@ -12,11 +31,15 @@ struct Balances;
|
||||
struct Accounts;
|
||||
|
||||
impl Keys {
|
||||
fn admin() -> PrivateKey {
|
||||
PrivateKey::try_new([40; 32]).expect("valid private key")
|
||||
}
|
||||
|
||||
fn owner() -> PrivateKey {
|
||||
PrivateKey::try_new([41; 32]).expect("valid private key")
|
||||
}
|
||||
|
||||
fn user_holding() -> PrivateKey {
|
||||
fn user_collateral_holding() -> PrivateKey {
|
||||
PrivateKey::try_new([42; 32]).expect("valid private key")
|
||||
}
|
||||
|
||||
@ -34,20 +57,54 @@ impl Ids {
|
||||
stablecoin_methods::STABLECOIN_ID
|
||||
}
|
||||
|
||||
fn collateral_definition() -> AccountId {
|
||||
AccountId::new([5; 32])
|
||||
fn twap_oracle_program() -> nssa_core::program::ProgramId {
|
||||
twap_oracle_methods::TWAP_ORACLE_ID
|
||||
}
|
||||
|
||||
fn admin() -> AccountId {
|
||||
AccountId::from(&PublicKey::new_from_private_key(&Keys::admin()))
|
||||
}
|
||||
|
||||
fn freeze_authority() -> AccountId {
|
||||
AccountId::new([44; 32])
|
||||
}
|
||||
|
||||
fn owner() -> AccountId {
|
||||
AccountId::from(&PublicKey::new_from_private_key(&Keys::owner()))
|
||||
}
|
||||
|
||||
fn user_holding() -> AccountId {
|
||||
AccountId::from(&PublicKey::new_from_private_key(&Keys::user_holding()))
|
||||
fn collateral_definition() -> AccountId {
|
||||
AccountId::new([5; 32])
|
||||
}
|
||||
|
||||
fn user_collateral_holding() -> AccountId {
|
||||
AccountId::from(&PublicKey::new_from_private_key(
|
||||
&Keys::user_collateral_holding(),
|
||||
))
|
||||
}
|
||||
|
||||
fn oracle() -> AccountId {
|
||||
AccountId::new([7; 32])
|
||||
}
|
||||
|
||||
fn protocol_parameters() -> AccountId {
|
||||
compute_protocol_parameters_pda(Self::stablecoin_program())
|
||||
}
|
||||
|
||||
fn stability_fee_accumulator() -> AccountId {
|
||||
compute_stability_fee_accumulator_pda(Self::stablecoin_program())
|
||||
}
|
||||
|
||||
fn redemption_price_state() -> AccountId {
|
||||
compute_redemption_price_state_pda(Self::stablecoin_program())
|
||||
}
|
||||
|
||||
fn stablecoin_definition() -> AccountId {
|
||||
AccountId::new([6; 32])
|
||||
compute_stablecoin_definition_pda(Self::stablecoin_program())
|
||||
}
|
||||
|
||||
fn stablecoin_master_holding() -> AccountId {
|
||||
compute_stablecoin_master_holding_pda(Self::stablecoin_program())
|
||||
}
|
||||
|
||||
fn user_stablecoin_holding() -> AccountId {
|
||||
@ -57,11 +114,7 @@ impl Ids {
|
||||
}
|
||||
|
||||
fn position() -> AccountId {
|
||||
compute_position_pda(
|
||||
Self::stablecoin_program(),
|
||||
Self::owner(),
|
||||
Self::collateral_definition(),
|
||||
)
|
||||
compute_position_pda(Self::stablecoin_program(), Self::owner(), POSITION_NONCE)
|
||||
}
|
||||
|
||||
fn vault() -> AccountId {
|
||||
@ -70,43 +123,31 @@ impl Ids {
|
||||
}
|
||||
|
||||
impl Balances {
|
||||
fn user_holding_init() -> u128 {
|
||||
fn user_collateral_init() -> u128 {
|
||||
1_000_000
|
||||
}
|
||||
|
||||
fn collateral_deposit() -> u128 {
|
||||
500_000
|
||||
500
|
||||
}
|
||||
|
||||
fn collateral_withdraw() -> u128 {
|
||||
200_000
|
||||
400
|
||||
}
|
||||
|
||||
fn stablecoin_supply_init() -> u128 {
|
||||
1_000
|
||||
}
|
||||
|
||||
fn user_stablecoin_holding_init() -> u128 {
|
||||
1_000
|
||||
}
|
||||
|
||||
fn initial_debt() -> u128 {
|
||||
300
|
||||
}
|
||||
|
||||
fn debt_repay_amount() -> u128 {
|
||||
fn generated_debt() -> u128 {
|
||||
100
|
||||
}
|
||||
}
|
||||
|
||||
impl Accounts {
|
||||
fn collateral_definition_init() -> Account {
|
||||
fn collateral_definition() -> Account {
|
||||
Account {
|
||||
program_owner: Ids::token_program(),
|
||||
balance: 0_u128,
|
||||
balance: 0,
|
||||
data: Data::from(&TokenDefinition::Fungible {
|
||||
name: String::from("Gold"),
|
||||
total_supply: Balances::user_holding_init(),
|
||||
total_supply: Balances::user_collateral_init(),
|
||||
metadata_id: None,
|
||||
authority: None,
|
||||
}),
|
||||
@ -114,53 +155,41 @@ impl Accounts {
|
||||
}
|
||||
}
|
||||
|
||||
fn user_holding_init() -> Account {
|
||||
fn user_collateral_holding() -> Account {
|
||||
Account {
|
||||
program_owner: Ids::token_program(),
|
||||
balance: 0_u128,
|
||||
balance: 0,
|
||||
data: Data::from(&TokenHolding::Fungible {
|
||||
definition_id: Ids::collateral_definition(),
|
||||
balance: Balances::user_holding_init(),
|
||||
balance: Balances::user_collateral_init(),
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn stablecoin_definition_init() -> Account {
|
||||
fn user_stablecoin_holding() -> Account {
|
||||
Account {
|
||||
program_owner: Ids::token_program(),
|
||||
balance: 0_u128,
|
||||
data: Data::from(&TokenDefinition::Fungible {
|
||||
name: String::from("DAI"),
|
||||
total_supply: Balances::stablecoin_supply_init(),
|
||||
metadata_id: None,
|
||||
authority: None,
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn user_stablecoin_holding_init() -> Account {
|
||||
Account {
|
||||
program_owner: Ids::token_program(),
|
||||
balance: 0_u128,
|
||||
balance: 0,
|
||||
data: Data::from(&TokenHolding::Fungible {
|
||||
definition_id: Ids::stablecoin_definition(),
|
||||
balance: Balances::user_stablecoin_holding_init(),
|
||||
balance: 0,
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn position_with_debt_init() -> Account {
|
||||
fn oracle(timestamp: u64) -> Account {
|
||||
Account {
|
||||
program_owner: stablecoin_methods::STABLECOIN_ID,
|
||||
balance: 0_u128,
|
||||
data: Data::from(&Position {
|
||||
collateral_vault_id: Ids::vault(),
|
||||
collateral_definition_id: Ids::collateral_definition(),
|
||||
collateral_amount: Balances::collateral_deposit(),
|
||||
debt_amount: Balances::initial_debt(),
|
||||
program_owner: Ids::twap_oracle_program(),
|
||||
balance: 0,
|
||||
data: Data::from(&OraclePriceAccount {
|
||||
base_asset: Ids::stablecoin_definition(),
|
||||
quote_asset: Ids::collateral_definition(),
|
||||
price: Q64_ONE,
|
||||
timestamp,
|
||||
source_id: AccountId::new([8; 32]),
|
||||
confidence_interval: 0,
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
}
|
||||
@ -190,9 +219,18 @@ fn state_for_stablecoin_tests() -> V03State {
|
||||
deploy_programs(&mut state);
|
||||
state.force_insert_account(
|
||||
Ids::collateral_definition(),
|
||||
Accounts::collateral_definition_init(),
|
||||
Accounts::collateral_definition(),
|
||||
);
|
||||
state.force_insert_account(Ids::user_holding(), Accounts::user_holding_init());
|
||||
state.force_insert_account(
|
||||
Ids::user_collateral_holding(),
|
||||
Accounts::user_collateral_holding(),
|
||||
);
|
||||
state.force_insert_account(
|
||||
Ids::user_stablecoin_holding(),
|
||||
Accounts::user_stablecoin_holding(),
|
||||
);
|
||||
state.force_insert_account(Ids::oracle(), Accounts::oracle(CLOCK_START));
|
||||
advance_clock(&mut state, CLOCK_START);
|
||||
state
|
||||
}
|
||||
|
||||
@ -200,61 +238,59 @@ fn current_nonce(state: &V03State, account_id: AccountId) -> Nonce {
|
||||
state.get_account_by_id(account_id).nonce
|
||||
}
|
||||
|
||||
fn state_for_stablecoin_repay_tests() -> V03State {
|
||||
let mut state = V03State::new();
|
||||
deploy_programs(&mut state);
|
||||
state.force_insert_account(
|
||||
Ids::collateral_definition(),
|
||||
Accounts::collateral_definition_init(),
|
||||
);
|
||||
state.force_insert_account(
|
||||
Ids::stablecoin_definition(),
|
||||
Accounts::stablecoin_definition_init(),
|
||||
);
|
||||
state.force_insert_account(Ids::position(), Accounts::position_with_debt_init());
|
||||
state.force_insert_account(
|
||||
Ids::user_stablecoin_holding(),
|
||||
Accounts::user_stablecoin_holding_init(),
|
||||
);
|
||||
state
|
||||
}
|
||||
|
||||
fn assert_position(state: &V03State, expected_collateral: u128) {
|
||||
let position =
|
||||
Position::try_from(&state.get_account_by_id(Ids::position()).data).expect("valid Position");
|
||||
assert_eq!(position.collateral_amount, expected_collateral);
|
||||
assert_eq!(position.debt_amount, 0);
|
||||
assert_eq!(position.collateral_vault_id, Ids::vault());
|
||||
assert_eq!(
|
||||
position.collateral_definition_id,
|
||||
Ids::collateral_definition()
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_fungible_balance(state: &V03State, account_id: AccountId, expected_balance: u128) {
|
||||
let holding = TokenHolding::try_from(&state.get_account_by_id(account_id).data)
|
||||
.expect("valid TokenHolding");
|
||||
match holding {
|
||||
TokenHolding::Fungible {
|
||||
definition_id,
|
||||
balance,
|
||||
} => {
|
||||
assert_eq!(definition_id, Ids::collateral_definition());
|
||||
assert_eq!(balance, expected_balance);
|
||||
}
|
||||
TokenHolding::NftMaster { .. } | TokenHolding::NftPrintedCopy { .. } => {
|
||||
panic!("expected Fungible holding")
|
||||
}
|
||||
fn advance_clock(state: &mut V03State, timestamp: u64) {
|
||||
let data = ClockAccountData {
|
||||
block_id: 0,
|
||||
timestamp,
|
||||
}
|
||||
.to_bytes();
|
||||
let clock_account = Account {
|
||||
data: Data::try_from(data).expect("clock account data fits"),
|
||||
..Account::default()
|
||||
};
|
||||
state.force_insert_account(CLOCK_01_PROGRAM_ACCOUNT_ID, clock_account);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stablecoin_open_position_then_withdraw_collateral() {
|
||||
let mut state = state_for_stablecoin_tests();
|
||||
fn initialize_protocol(state: &mut V03State, rate: u128) {
|
||||
let instruction = stablecoin_core::Instruction::InitializeProgram {
|
||||
freeze_authority_account_id: Ids::freeze_authority(),
|
||||
initial_stability_fee_per_millisecond: rate,
|
||||
initial_controller_proportional_gain: 0,
|
||||
initial_controller_integral_gain: 0,
|
||||
initial_minimum_collateralization_ratio: FIXED_POINT_ONE / 10 * 11,
|
||||
minimum_milliseconds_between_rate_updates: 1,
|
||||
maximum_oracle_price_age_milliseconds: 86_400_000,
|
||||
initial_redemption_price: FIXED_POINT_ONE,
|
||||
stablecoin_name: String::from("DAI"),
|
||||
};
|
||||
let message = public_transaction::Message::try_new(
|
||||
Ids::stablecoin_program(),
|
||||
vec![
|
||||
Ids::admin(),
|
||||
Ids::protocol_parameters(),
|
||||
Ids::stability_fee_accumulator(),
|
||||
Ids::redemption_price_state(),
|
||||
Ids::stablecoin_definition(),
|
||||
Ids::stablecoin_master_holding(),
|
||||
Ids::collateral_definition(),
|
||||
Ids::oracle(),
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
],
|
||||
vec![current_nonce(state, Ids::admin())],
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::admin()]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
state
|
||||
.transition_from_public_transaction(&tx, 0, 0)
|
||||
.expect("initialize_program must succeed");
|
||||
}
|
||||
|
||||
// Open the position: deposit collateral from the user's holding into a fresh vault.
|
||||
let open = stablecoin_core::Instruction::OpenPosition {
|
||||
collateral_amount: Balances::collateral_deposit(),
|
||||
fn open_position(state: &mut V03State) {
|
||||
let instruction = stablecoin_core::Instruction::OpenPosition {
|
||||
position_nonce: POSITION_NONCE,
|
||||
initial_collateral_amount: Balances::collateral_deposit(),
|
||||
};
|
||||
let message = public_transaction::Message::try_new(
|
||||
Ids::stablecoin_program(),
|
||||
@ -262,78 +298,31 @@ fn stablecoin_open_position_then_withdraw_collateral() {
|
||||
Ids::owner(),
|
||||
Ids::position(),
|
||||
Ids::vault(),
|
||||
Ids::user_holding(),
|
||||
Ids::user_collateral_holding(),
|
||||
Ids::collateral_definition(),
|
||||
Ids::protocol_parameters(),
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
],
|
||||
vec![
|
||||
current_nonce(&state, Ids::owner()),
|
||||
current_nonce(&state, Ids::user_holding()),
|
||||
current_nonce(state, Ids::owner()),
|
||||
current_nonce(state, Ids::user_collateral_holding()),
|
||||
],
|
||||
open,
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(
|
||||
&message,
|
||||
&[&Keys::owner(), &Keys::user_holding()],
|
||||
&[&Keys::owner(), &Keys::user_collateral_holding()],
|
||||
);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
state
|
||||
.transition_from_public_transaction(&tx, 0, 0)
|
||||
.expect("open_position must succeed");
|
||||
|
||||
assert_position(&state, Balances::collateral_deposit());
|
||||
assert_fungible_balance(&state, Ids::vault(), Balances::collateral_deposit());
|
||||
assert_fungible_balance(
|
||||
&state,
|
||||
Ids::user_holding(),
|
||||
Balances::user_holding_init() - Balances::collateral_deposit(),
|
||||
);
|
||||
|
||||
// Withdraw part of the collateral back to the same user holding.
|
||||
let withdraw = stablecoin_core::Instruction::WithdrawCollateral {
|
||||
amount: Balances::collateral_withdraw(),
|
||||
};
|
||||
let message = public_transaction::Message::try_new(
|
||||
Ids::stablecoin_program(),
|
||||
vec![
|
||||
Ids::owner(),
|
||||
Ids::position(),
|
||||
Ids::vault(),
|
||||
Ids::user_holding(),
|
||||
],
|
||||
vec![current_nonce(&state, Ids::owner())],
|
||||
withdraw,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::owner()]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
state
|
||||
.transition_from_public_transaction(&tx, 0, 0)
|
||||
.expect("withdraw_collateral must succeed");
|
||||
|
||||
assert_position(
|
||||
&state,
|
||||
Balances::collateral_deposit() - Balances::collateral_withdraw(),
|
||||
);
|
||||
assert_fungible_balance(
|
||||
&state,
|
||||
Ids::vault(),
|
||||
Balances::collateral_deposit() - Balances::collateral_withdraw(),
|
||||
);
|
||||
assert_fungible_balance(
|
||||
&state,
|
||||
Ids::user_holding(),
|
||||
Balances::user_holding_init() - Balances::collateral_deposit()
|
||||
+ Balances::collateral_withdraw(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stablecoin_repay_debt_burns_stablecoins_and_decreases_debt() {
|
||||
let mut state = state_for_stablecoin_repay_tests();
|
||||
|
||||
let repay = stablecoin_core::Instruction::RepayDebt {
|
||||
amount: Balances::debt_repay_amount(),
|
||||
fn generate_debt(state: &mut V03State) {
|
||||
let instruction = stablecoin_core::Instruction::GenerateDebt {
|
||||
amount: Balances::generated_debt(),
|
||||
};
|
||||
let message = public_transaction::Message::try_new(
|
||||
Ids::stablecoin_program(),
|
||||
@ -342,12 +331,87 @@ fn stablecoin_repay_debt_burns_stablecoins_and_decreases_debt() {
|
||||
Ids::position(),
|
||||
Ids::stablecoin_definition(),
|
||||
Ids::user_stablecoin_holding(),
|
||||
Ids::stability_fee_accumulator(),
|
||||
Ids::redemption_price_state(),
|
||||
Ids::oracle(),
|
||||
Ids::protocol_parameters(),
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
],
|
||||
vec![current_nonce(state, Ids::owner())],
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::owner()]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
state
|
||||
.transition_from_public_transaction(&tx, 0, 0)
|
||||
.expect("generate_debt must succeed");
|
||||
}
|
||||
|
||||
fn accrue_stability_fee(state: &mut V03State) {
|
||||
execute_accrue_stability_fee(state).expect("accrue_stability_fee must succeed");
|
||||
}
|
||||
|
||||
fn execute_accrue_stability_fee(state: &mut V03State) -> Result<(), LeeError> {
|
||||
let instruction = stablecoin_core::Instruction::AccrueStabilityFee;
|
||||
let message = public_transaction::Message::try_new(
|
||||
Ids::stablecoin_program(),
|
||||
vec![
|
||||
Ids::owner(),
|
||||
Ids::protocol_parameters(),
|
||||
Ids::stability_fee_accumulator(),
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
],
|
||||
vec![current_nonce(state, Ids::owner())],
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::owner()]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
state.transition_from_public_transaction(&tx, 0, 0)
|
||||
}
|
||||
|
||||
fn set_stability_fee_per_millisecond(state: &mut V03State, new_rate: u128) {
|
||||
let instruction = stablecoin_core::Instruction::SetStabilityFeePerMillisecond { new_rate };
|
||||
let message = public_transaction::Message::try_new(
|
||||
Ids::stablecoin_program(),
|
||||
vec![
|
||||
Ids::admin(),
|
||||
Ids::protocol_parameters(),
|
||||
Ids::stability_fee_accumulator(),
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
],
|
||||
vec![current_nonce(state, Ids::admin())],
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::admin()]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
state
|
||||
.transition_from_public_transaction(&tx, 0, 0)
|
||||
.expect("set_stability_fee_per_millisecond must succeed");
|
||||
}
|
||||
|
||||
fn repay_debt(state: &mut V03State) {
|
||||
let instruction = stablecoin_core::Instruction::RepayDebt {
|
||||
amount: Balances::generated_debt(),
|
||||
};
|
||||
let message = public_transaction::Message::try_new(
|
||||
Ids::stablecoin_program(),
|
||||
vec![
|
||||
Ids::owner(),
|
||||
Ids::position(),
|
||||
Ids::stablecoin_definition(),
|
||||
Ids::user_stablecoin_holding(),
|
||||
Ids::stability_fee_accumulator(),
|
||||
Ids::protocol_parameters(),
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
],
|
||||
vec![
|
||||
current_nonce(&state, Ids::owner()),
|
||||
current_nonce(&state, Ids::user_stablecoin_holding()),
|
||||
current_nonce(state, Ids::owner()),
|
||||
current_nonce(state, Ids::user_stablecoin_holding()),
|
||||
],
|
||||
repay,
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(
|
||||
@ -358,43 +422,224 @@ fn stablecoin_repay_debt_burns_stablecoins_and_decreases_debt() {
|
||||
state
|
||||
.transition_from_public_transaction(&tx, 0, 0)
|
||||
.expect("repay_debt must succeed");
|
||||
}
|
||||
|
||||
// Position debt decreased; collateral untouched.
|
||||
let position =
|
||||
Position::try_from(&state.get_account_by_id(Ids::position()).data).expect("valid Position");
|
||||
assert_eq!(
|
||||
position.debt_amount,
|
||||
Balances::initial_debt() - Balances::debt_repay_amount()
|
||||
);
|
||||
assert_eq!(position.collateral_amount, Balances::collateral_deposit());
|
||||
fn withdraw_collateral(state: &mut V03State) {
|
||||
execute_withdraw_collateral(state, Balances::collateral_withdraw())
|
||||
.expect("withdraw_collateral must succeed");
|
||||
}
|
||||
|
||||
// Stablecoin total supply decreased by the burn amount.
|
||||
let definition =
|
||||
TokenDefinition::try_from(&state.get_account_by_id(Ids::stablecoin_definition()).data)
|
||||
.expect("valid TokenDefinition");
|
||||
match definition {
|
||||
TokenDefinition::Fungible { total_supply, .. } => {
|
||||
assert_eq!(
|
||||
total_supply,
|
||||
Balances::stablecoin_supply_init() - Balances::debt_repay_amount()
|
||||
);
|
||||
}
|
||||
TokenDefinition::NonFungible { .. } => panic!("expected Fungible definition"),
|
||||
}
|
||||
fn execute_withdraw_collateral(state: &mut V03State, amount: u128) -> Result<(), LeeError> {
|
||||
let instruction = stablecoin_core::Instruction::WithdrawCollateral { amount };
|
||||
let message = public_transaction::Message::try_new(
|
||||
Ids::stablecoin_program(),
|
||||
vec![
|
||||
Ids::owner(),
|
||||
Ids::position(),
|
||||
Ids::vault(),
|
||||
Ids::user_collateral_holding(),
|
||||
Ids::stability_fee_accumulator(),
|
||||
Ids::redemption_price_state(),
|
||||
Ids::protocol_parameters(),
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
],
|
||||
vec![current_nonce(state, Ids::owner())],
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::owner()]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
state.transition_from_public_transaction(&tx, 0, 0)
|
||||
}
|
||||
|
||||
// User stablecoin holding decreased by the burn amount.
|
||||
let holding =
|
||||
TokenHolding::try_from(&state.get_account_by_id(Ids::user_stablecoin_holding()).data)
|
||||
.expect("valid TokenHolding");
|
||||
match holding {
|
||||
TokenHolding::Fungible { balance, .. } => {
|
||||
assert_eq!(
|
||||
balance,
|
||||
Balances::user_stablecoin_holding_init() - Balances::debt_repay_amount()
|
||||
);
|
||||
}
|
||||
fn position(state: &V03State) -> Position {
|
||||
Position::try_from(&state.get_account_by_id(Ids::position()).data).expect("valid Position")
|
||||
}
|
||||
|
||||
fn stability_fee_accumulator(state: &V03State) -> StabilityFeeAccumulator {
|
||||
StabilityFeeAccumulator::try_from(
|
||||
&state
|
||||
.get_account_by_id(Ids::stability_fee_accumulator())
|
||||
.data,
|
||||
)
|
||||
.expect("valid StabilityFeeAccumulator")
|
||||
}
|
||||
|
||||
fn protocol_parameters(state: &V03State) -> ProtocolParameters {
|
||||
ProtocolParameters::try_from(&state.get_account_by_id(Ids::protocol_parameters()).data)
|
||||
.expect("valid ProtocolParameters")
|
||||
}
|
||||
|
||||
fn redemption_price_state(state: &V03State) -> RedemptionPriceState {
|
||||
RedemptionPriceState::try_from(&state.get_account_by_id(Ids::redemption_price_state()).data)
|
||||
.expect("valid RedemptionPriceState")
|
||||
}
|
||||
|
||||
fn fungible_balance(state: &V03State, account_id: AccountId) -> u128 {
|
||||
match TokenHolding::try_from(&state.get_account_by_id(account_id).data)
|
||||
.expect("valid TokenHolding")
|
||||
{
|
||||
TokenHolding::Fungible { balance, .. } => balance,
|
||||
TokenHolding::NftMaster { .. } | TokenHolding::NftPrintedCopy { .. } => {
|
||||
panic!("expected Fungible holding")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fungible_supply(state: &V03State, account_id: AccountId) -> u128 {
|
||||
match TokenDefinition::try_from(&state.get_account_by_id(account_id).data)
|
||||
.expect("valid TokenDefinition")
|
||||
{
|
||||
TokenDefinition::Fungible { total_supply, .. } => total_supply,
|
||||
TokenDefinition::NonFungible { .. } => panic!("expected Fungible definition"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stablecoin_initialize_then_accrue_fee() {
|
||||
let mut state = state_for_stablecoin_tests();
|
||||
let rate = FIXED_POINT_ONE + FIXED_POINT_ONE / 10;
|
||||
initialize_protocol(&mut state, rate);
|
||||
|
||||
let params = protocol_parameters(&state);
|
||||
assert_eq!(params.stability_fee_per_millisecond, rate);
|
||||
assert_eq!(
|
||||
params.stablecoin_definition_id,
|
||||
Ids::stablecoin_definition()
|
||||
);
|
||||
assert_eq!(
|
||||
params.collateral_definition_id,
|
||||
Ids::collateral_definition()
|
||||
);
|
||||
assert_eq!(params.market_price_oracle_id, Ids::oracle());
|
||||
assert_eq!(fungible_supply(&state, Ids::stablecoin_definition()), 0);
|
||||
|
||||
let accumulator = stability_fee_accumulator(&state);
|
||||
assert_eq!(
|
||||
accumulator.accumulated_rate_at_last_accrual,
|
||||
FIXED_POINT_ONE
|
||||
);
|
||||
assert_eq!(accumulator.last_accrued_at, CLOCK_START);
|
||||
let redemption = redemption_price_state(&state);
|
||||
assert_eq!(redemption.redemption_price_at_last_update, FIXED_POINT_ONE);
|
||||
assert_eq!(redemption.last_updated_at, CLOCK_START);
|
||||
|
||||
advance_clock(&mut state, CLOCK_START + 2);
|
||||
accrue_stability_fee(&mut state);
|
||||
|
||||
let updated = stability_fee_accumulator(&state);
|
||||
assert_eq!(
|
||||
updated.accumulated_rate_at_last_accrual,
|
||||
compound_rate(rate, 2)
|
||||
);
|
||||
assert_eq!(updated.last_accrued_at, CLOCK_START + 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stablecoin_set_fee_rate_anchors_old_rate_before_switching() {
|
||||
let mut state = state_for_stablecoin_tests();
|
||||
let old_rate = FIXED_POINT_ONE + FIXED_POINT_ONE / 10;
|
||||
initialize_protocol(&mut state, old_rate);
|
||||
|
||||
advance_clock(&mut state, CLOCK_START + 2);
|
||||
set_stability_fee_per_millisecond(&mut state, FIXED_POINT_ONE);
|
||||
|
||||
let updated = stability_fee_accumulator(&state);
|
||||
let old_window_anchor = compound_rate(old_rate, 2);
|
||||
assert_eq!(updated.accumulated_rate_at_last_accrual, old_window_anchor);
|
||||
assert_eq!(updated.last_accrued_at, CLOCK_START + 2);
|
||||
assert_eq!(
|
||||
protocol_parameters(&state).stability_fee_per_millisecond,
|
||||
FIXED_POINT_ONE
|
||||
);
|
||||
|
||||
advance_clock(&mut state, CLOCK_START + 4);
|
||||
accrue_stability_fee(&mut state);
|
||||
let reanchored = stability_fee_accumulator(&state);
|
||||
assert_eq!(
|
||||
reanchored.accumulated_rate_at_last_accrual,
|
||||
old_window_anchor
|
||||
);
|
||||
assert_eq!(reanchored.last_accrued_at, CLOCK_START + 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stablecoin_open_generate_repay_and_withdraw_flow() {
|
||||
let mut state = state_for_stablecoin_tests();
|
||||
initialize_protocol(&mut state, FIXED_POINT_ONE);
|
||||
|
||||
open_position(&mut state);
|
||||
let opened = position(&state);
|
||||
assert_eq!(opened.owner_account_id, Ids::owner());
|
||||
assert_eq!(opened.position_nonce, POSITION_NONCE);
|
||||
assert_eq!(opened.vault_account_id, Ids::vault());
|
||||
assert_eq!(opened.collateral_amount, Balances::collateral_deposit());
|
||||
assert_eq!(opened.normalized_debt_amount, 0);
|
||||
assert_eq!(opened.opened_at, CLOCK_START);
|
||||
assert_eq!(
|
||||
fungible_balance(&state, Ids::user_collateral_holding()),
|
||||
Balances::user_collateral_init() - Balances::collateral_deposit()
|
||||
);
|
||||
assert_eq!(
|
||||
fungible_balance(&state, Ids::vault()),
|
||||
Balances::collateral_deposit()
|
||||
);
|
||||
|
||||
generate_debt(&mut state);
|
||||
advance_clock(&mut state, CLOCK_START + 1);
|
||||
accrue_stability_fee(&mut state);
|
||||
let indebted = position(&state);
|
||||
assert_eq!(indebted.normalized_debt_amount, Balances::generated_debt());
|
||||
assert_eq!(
|
||||
fungible_supply(&state, Ids::stablecoin_definition()),
|
||||
Balances::generated_debt()
|
||||
);
|
||||
assert_eq!(
|
||||
fungible_balance(&state, Ids::user_stablecoin_holding()),
|
||||
Balances::generated_debt()
|
||||
);
|
||||
|
||||
repay_debt(&mut state);
|
||||
let repaid = position(&state);
|
||||
assert_eq!(repaid.normalized_debt_amount, 0);
|
||||
assert_eq!(fungible_supply(&state, Ids::stablecoin_definition()), 0);
|
||||
assert_eq!(fungible_balance(&state, Ids::user_stablecoin_holding()), 0);
|
||||
|
||||
withdraw_collateral(&mut state);
|
||||
let withdrawn = position(&state);
|
||||
assert_eq!(
|
||||
withdrawn.collateral_amount,
|
||||
Balances::collateral_deposit() - Balances::collateral_withdraw()
|
||||
);
|
||||
assert_eq!(
|
||||
fungible_balance(&state, Ids::vault()),
|
||||
Balances::collateral_deposit() - Balances::collateral_withdraw()
|
||||
);
|
||||
assert_eq!(
|
||||
fungible_balance(&state, Ids::user_collateral_holding()),
|
||||
Balances::user_collateral_init() - Balances::collateral_deposit()
|
||||
+ Balances::collateral_withdraw()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stablecoin_debt_bearing_withdraw_checks_collateralization() {
|
||||
let mut state = state_for_stablecoin_tests();
|
||||
initialize_protocol(&mut state, FIXED_POINT_ONE);
|
||||
open_position(&mut state);
|
||||
generate_debt(&mut state);
|
||||
|
||||
assert!(matches!(
|
||||
execute_withdraw_collateral(&mut state, 400),
|
||||
Err(LeeError::ProgramExecutionFailed(_))
|
||||
));
|
||||
assert_eq!(
|
||||
position(&state).collateral_amount,
|
||||
Balances::collateral_deposit()
|
||||
);
|
||||
|
||||
execute_withdraw_collateral(&mut state, 300)
|
||||
.expect("safe debt-bearing withdrawal must succeed");
|
||||
assert_eq!(position(&state).collateral_amount, 200);
|
||||
assert_eq!(fungible_balance(&state, Ids::vault()), 200);
|
||||
}
|
||||
|
||||
@ -5,5 +5,8 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc6", features = ["host"], package = "lee_core" }
|
||||
clock_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc6" }
|
||||
borsh = { version = "1.5", features = ["derive"] }
|
||||
stablecoin_core = { path = "core" }
|
||||
token_core = { path = "../token/core" }
|
||||
twap_oracle_core = { path = "../twap_oracle/core" }
|
||||
|
||||
@ -2,7 +2,9 @@
|
||||
|
||||
pub mod math;
|
||||
|
||||
use alloy_primitives::U256;
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
pub use math::{compound_rate, mul_div as mul_div_floor, mul_div_ceil, FIXED_POINT_ONE};
|
||||
use nssa_core::{
|
||||
account::{AccountId, AccountWithMetadata, Data},
|
||||
program::{PdaSeed, ProgramId},
|
||||
@ -10,93 +12,155 @@ use nssa_core::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use spel_framework_macros::account_type;
|
||||
|
||||
// Stable domain-separation tags for the position PDAs; these must stay unchanged for address
|
||||
// compatibility.
|
||||
/// Maximum elapsed time rolled into one compounding projection.
|
||||
pub const MAXIMUM_COMPOUNDING_WINDOW_MILLISECONDS: u64 = 86_400_000;
|
||||
|
||||
/// Maximum accepted stability-fee multiplier per millisecond.
|
||||
pub const MAX_STABILITY_FEE_PER_MILLISECOND: u128 =
|
||||
FIXED_POINT_ONE + FIXED_POINT_ONE / 100_000_000_000;
|
||||
|
||||
/// Minimum accepted collateralization ratio: 110%.
|
||||
pub const MINIMUM_COLLATERALIZATION_RATIO_LOWER_BOUND: u128 = FIXED_POINT_ONE / 10 * 11;
|
||||
|
||||
/// Maximum accepted collateralization ratio: 1000%.
|
||||
pub const MINIMUM_COLLATERALIZATION_RATIO_UPPER_BOUND: u128 = FIXED_POINT_ONE * 10;
|
||||
|
||||
/// Maximum accepted proportional gain magnitude.
|
||||
pub const MAX_CONTROLLER_PROPORTIONAL_GAIN: u128 = FIXED_POINT_ONE * 1_000;
|
||||
|
||||
/// Maximum accepted integral gain magnitude.
|
||||
pub const MAX_CONTROLLER_INTEGRAL_GAIN: u128 = FIXED_POINT_ONE;
|
||||
|
||||
/// Maximum accepted timing parameter in milliseconds.
|
||||
pub const MAX_TIMING_PARAMETER_MILLISECONDS: u64 = 86_400_000;
|
||||
|
||||
const PROTOCOL_PARAMETERS_PDA_DOMAIN: &[u8] = b"PROTOCOL_PARAMETERS";
|
||||
const STABILITY_FEE_ACCUMULATOR_PDA_DOMAIN: &[u8] = b"STABILITY_FEE_ACCUMULATOR";
|
||||
const REDEMPTION_PRICE_STATE_PDA_DOMAIN: &[u8] = b"REDEMPTION_PRICE_STATE";
|
||||
const STABLECOIN_DEFINITION_PDA_DOMAIN: &[u8] = b"STABLECOIN_DEFINITION";
|
||||
const STABLECOIN_MASTER_HOLDING_PDA_DOMAIN: &[u8] = b"STABLECOIN_MASTER_HOLDING";
|
||||
const POSITION_PDA_DOMAIN: &[u8] = b"POSITION";
|
||||
const POSITION_VAULT_PDA_DOMAIN: &[u8] = b"POSITION_VAULT";
|
||||
|
||||
/// Stablecoin Program Instruction.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum Instruction {
|
||||
/// Initialize protocol globals and stablecoin token definition.
|
||||
InitializeProgram {
|
||||
freeze_authority_account_id: AccountId,
|
||||
initial_stability_fee_per_millisecond: u128,
|
||||
initial_controller_proportional_gain: i128,
|
||||
initial_controller_integral_gain: i128,
|
||||
initial_minimum_collateralization_ratio: u128,
|
||||
minimum_milliseconds_between_rate_updates: u64,
|
||||
maximum_oracle_price_age_milliseconds: u64,
|
||||
initial_redemption_price: u128,
|
||||
stablecoin_name: String,
|
||||
},
|
||||
/// Permissionlessly roll the global stability-fee accumulator forward.
|
||||
AccrueStabilityFee,
|
||||
/// Update the stability-fee rate after accruing pending fees at the old rate.
|
||||
SetStabilityFeePerMillisecond { new_rate: u128 },
|
||||
/// Open a new collateral-only [`Position`] for the calling owner.
|
||||
///
|
||||
/// Required accounts (5):
|
||||
/// - Owner account (authorized)
|
||||
/// - Position account (uninitialized, address must match
|
||||
/// `compute_position_pda(self_program_id, owner, token_definition)`)
|
||||
/// - Position vault token holding account (uninitialized, address must match
|
||||
/// `compute_position_vault_pda(self_program_id, position_id)`)
|
||||
/// - Owner's source token holding for the collateral (authorized, initialized)
|
||||
/// - Token definition account for the collateral (matches the user holding's `definition_id`;
|
||||
/// its `program_owner` determines the Token Program used by the chained `InitializeAccount`
|
||||
/// / `Transfer` calls)
|
||||
OpenPosition {
|
||||
/// User-chosen nonce that disambiguates multiple positions.
|
||||
position_nonce: u64,
|
||||
/// Amount of collateral tokens to deposit into the position vault.
|
||||
collateral_amount: u128,
|
||||
initial_collateral_amount: u128,
|
||||
},
|
||||
/// Mint stablecoin debt against an existing position.
|
||||
GenerateDebt {
|
||||
/// Amount of stablecoin to mint.
|
||||
amount: u128,
|
||||
},
|
||||
/// Withdraw `amount` collateral tokens from a position back to a user-controlled holding.
|
||||
///
|
||||
/// Required accounts (4):
|
||||
/// - Owner account (authorized)
|
||||
/// - Position account (initialized, owned by `self_program_id`)
|
||||
/// - Position vault token holding (address must match
|
||||
/// `compute_position_vault_pda(self_program_id, position_id)`)
|
||||
/// - Destination user collateral holding (initialized, owned by the vault's Token Program,
|
||||
/// `TokenHolding.definition_id == Position.collateral_definition_id`)
|
||||
///
|
||||
/// `token_program_id` is derived from `vault.account.program_owner`;
|
||||
/// `collateral_definition_id` is read from the decoded [`Position`].
|
||||
///
|
||||
/// **Note:** until issues #97/#96/#95 land, this instruction hard-asserts
|
||||
/// `Position.debt_amount == 0` instead of accruing fees and checking the
|
||||
/// collateralization ratio.
|
||||
WithdrawCollateral {
|
||||
/// Amount of collateral tokens to move from the vault back to `destination`.
|
||||
amount: u128,
|
||||
},
|
||||
/// Repay `amount` of outstanding stablecoin debt against an existing position.
|
||||
///
|
||||
/// Required accounts (4):
|
||||
/// - Owner account (authorized; binds caller-as-owner via position PDA re-derivation)
|
||||
/// - Position account (initialized, owned by `self_program_id`)
|
||||
/// - Stablecoin token definition account (the definition of the stablecoin being repaid)
|
||||
/// - User's stablecoin holding (authorized, initialized, owned by the same Token Program as
|
||||
/// the definition, with `TokenHolding.definition_id == stablecoin_definition.account_id`)
|
||||
///
|
||||
/// `token_program_id` is derived from `user_stablecoin_holding.account.program_owner`.
|
||||
/// `collateral_definition_id` (for position PDA verification) is read from the
|
||||
/// decoded [`Position`].
|
||||
///
|
||||
/// **Note:** until issue #97 (stability fee accrual) lands, this instruction does
|
||||
/// not accrue fees before reducing debt. A `// TODO(#97)` comment in the host
|
||||
/// function marks where the accrual code will plug in. Today every position has
|
||||
/// `debt_amount = 0` (no `generate_debt` yet), so the precondition is vacuously met.
|
||||
///
|
||||
/// **Note:** until issue #91 (`generate_debt`) records the stablecoin definition
|
||||
/// into `Position`, this instruction cannot validate that the passed
|
||||
/// `stablecoin_token_definition` is the one this position's debt is denominated
|
||||
/// in. The caller is trusted for that until then.
|
||||
RepayDebt {
|
||||
/// Amount of stablecoin debt to repay (also the amount burned from the user's holding).
|
||||
/// Amount of stablecoin debt to repay and burn from the user's holding.
|
||||
amount: u128,
|
||||
},
|
||||
}
|
||||
|
||||
/// Protocol-level configuration account.
|
||||
#[account_type]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||
pub struct ProtocolParameters {
|
||||
pub admin_account_id: AccountId,
|
||||
pub freeze_authority_account_id: AccountId,
|
||||
pub stablecoin_definition_id: AccountId,
|
||||
pub collateral_definition_id: AccountId,
|
||||
pub market_price_oracle_id: AccountId,
|
||||
pub stability_fee_per_millisecond: u128,
|
||||
pub controller_proportional_gain: i128,
|
||||
pub controller_integral_gain: i128,
|
||||
pub minimum_collateralization_ratio: u128,
|
||||
pub minimum_milliseconds_between_rate_updates: u64,
|
||||
pub maximum_oracle_price_age_milliseconds: u64,
|
||||
pub is_frozen: bool,
|
||||
}
|
||||
|
||||
/// Global stability-fee accumulator.
|
||||
#[account_type]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||
pub struct StabilityFeeAccumulator {
|
||||
pub accumulated_rate_at_last_accrual: u128,
|
||||
pub last_accrued_at: u64,
|
||||
}
|
||||
|
||||
/// Redemption-price state used by collateralization checks.
|
||||
#[account_type]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||
pub struct RedemptionPriceState {
|
||||
pub redemption_price_at_last_update: u128,
|
||||
pub redemption_rate_per_millisecond: u128,
|
||||
pub controller_integral_term: i128,
|
||||
pub last_updated_at: u64,
|
||||
}
|
||||
|
||||
/// Persistent state held by a Stablecoin [`Position`] account.
|
||||
///
|
||||
/// `debt_amount` is included for forward compatibility with `generate_debt`; until that
|
||||
/// instruction lands `open_position` always initializes it to `0`.
|
||||
#[account_type]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||
pub struct Position {
|
||||
/// Token holding account (vault PDA) that custodies the collateral backing this position.
|
||||
pub collateral_vault_id: AccountId,
|
||||
/// Token definition for the collateral held in `collateral_vault_id`.
|
||||
pub collateral_definition_id: AccountId,
|
||||
/// Owner authorized for every position operation.
|
||||
pub owner_account_id: AccountId,
|
||||
/// User-chosen nonce. Together with owner, it derives the position PDA.
|
||||
pub position_nonce: u64,
|
||||
/// Token holding account that custodies collateral for this position.
|
||||
pub vault_account_id: AccountId,
|
||||
/// Amount of collateral tokens deposited.
|
||||
pub collateral_amount: u128,
|
||||
/// Outstanding stablecoin debt against this position.
|
||||
pub debt_amount: u128,
|
||||
/// Debt shares. Nominal debt is derived from this and the current accumulator.
|
||||
pub normalized_debt_amount: u128,
|
||||
/// Unix timestamp in milliseconds when the position was opened.
|
||||
pub opened_at: u64,
|
||||
}
|
||||
|
||||
impl TryFrom<&Data> for ProtocolParameters {
|
||||
type Error = std::io::Error;
|
||||
|
||||
fn try_from(data: &Data) -> Result<Self, Self::Error> {
|
||||
Self::try_from_slice(data.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Data> for StabilityFeeAccumulator {
|
||||
type Error = std::io::Error;
|
||||
|
||||
fn try_from(data: &Data) -> Result<Self, Self::Error> {
|
||||
Self::try_from_slice(data.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Data> for RedemptionPriceState {
|
||||
type Error = std::io::Error;
|
||||
|
||||
fn try_from(data: &Data) -> Result<Self, Self::Error> {
|
||||
Self::try_from_slice(data.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Data> for Position {
|
||||
@ -107,64 +171,158 @@ impl TryFrom<&Data> for Position {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Position> for Data {
|
||||
fn from(position: &Position) -> Self {
|
||||
let mut data = Vec::with_capacity(std::mem::size_of_val(position));
|
||||
BorshSerialize::serialize(position, &mut data)
|
||||
.expect("Serialization to Vec should not fail");
|
||||
Self::try_from(data).expect("Position encoded data should fit into Data")
|
||||
impl From<&ProtocolParameters> for Data {
|
||||
fn from(params: &ProtocolParameters) -> Self {
|
||||
serialize_to_data(params)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// so one owner can hold separate positions for separate collateral definitions.
|
||||
pub fn compute_position_pda_seed(
|
||||
owner_id: AccountId,
|
||||
collateral_definition_id: AccountId,
|
||||
) -> PdaSeed {
|
||||
impl From<&StabilityFeeAccumulator> for Data {
|
||||
fn from(accumulator: &StabilityFeeAccumulator) -> Self {
|
||||
serialize_to_data(accumulator)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RedemptionPriceState> for Data {
|
||||
fn from(state: &RedemptionPriceState) -> Self {
|
||||
serialize_to_data(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Position> for Data {
|
||||
fn from(position: &Position) -> Self {
|
||||
serialize_to_data(position)
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_to_data<T: BorshSerialize>(value: &T) -> Data {
|
||||
let bytes = borsh::to_vec(value).expect("Serialization to Vec should not fail");
|
||||
Data::try_from(bytes).expect("Encoded account data should fit into Data")
|
||||
}
|
||||
|
||||
fn hash_seed(parts: &[&[u8]]) -> PdaSeed {
|
||||
use risc0_zkvm::sha::{Impl, Sha256 as _};
|
||||
|
||||
let mut bytes = Vec::new();
|
||||
bytes.extend_from_slice(&owner_id.to_bytes());
|
||||
bytes.extend_from_slice(&collateral_definition_id.to_bytes());
|
||||
bytes.extend_from_slice(POSITION_PDA_DOMAIN);
|
||||
let total_len = parts
|
||||
.iter()
|
||||
.try_fold(0usize, |acc, part| acc.checked_add(part.len()))
|
||||
.expect("PDA seed length should fit in usize");
|
||||
let mut bytes = Vec::with_capacity(total_len);
|
||||
for part in parts {
|
||||
bytes.extend_from_slice(part);
|
||||
}
|
||||
|
||||
let mut out = [0u8; 32];
|
||||
out.copy_from_slice(Impl::hash_bytes(&bytes).as_bytes());
|
||||
PdaSeed::new(out)
|
||||
}
|
||||
|
||||
/// PDA seed for the protocol parameters singleton.
|
||||
#[must_use]
|
||||
pub fn compute_protocol_parameters_pda_seed() -> PdaSeed {
|
||||
hash_seed(&[PROTOCOL_PARAMETERS_PDA_DOMAIN])
|
||||
}
|
||||
|
||||
/// Account id of the protocol parameters singleton.
|
||||
#[must_use]
|
||||
pub fn compute_protocol_parameters_pda(stablecoin_program_id: ProgramId) -> AccountId {
|
||||
AccountId::for_public_pda(
|
||||
&stablecoin_program_id,
|
||||
&compute_protocol_parameters_pda_seed(),
|
||||
)
|
||||
}
|
||||
|
||||
/// PDA seed for the stability-fee accumulator singleton.
|
||||
#[must_use]
|
||||
pub fn compute_stability_fee_accumulator_pda_seed() -> PdaSeed {
|
||||
hash_seed(&[STABILITY_FEE_ACCUMULATOR_PDA_DOMAIN])
|
||||
}
|
||||
|
||||
/// Account id of the stability-fee accumulator singleton.
|
||||
#[must_use]
|
||||
pub fn compute_stability_fee_accumulator_pda(stablecoin_program_id: ProgramId) -> AccountId {
|
||||
AccountId::for_public_pda(
|
||||
&stablecoin_program_id,
|
||||
&compute_stability_fee_accumulator_pda_seed(),
|
||||
)
|
||||
}
|
||||
|
||||
/// PDA seed for the redemption-price state singleton.
|
||||
#[must_use]
|
||||
pub fn compute_redemption_price_state_pda_seed() -> PdaSeed {
|
||||
hash_seed(&[REDEMPTION_PRICE_STATE_PDA_DOMAIN])
|
||||
}
|
||||
|
||||
/// Account id of the redemption-price state singleton.
|
||||
#[must_use]
|
||||
pub fn compute_redemption_price_state_pda(stablecoin_program_id: ProgramId) -> AccountId {
|
||||
AccountId::for_public_pda(
|
||||
&stablecoin_program_id,
|
||||
&compute_redemption_price_state_pda_seed(),
|
||||
)
|
||||
}
|
||||
|
||||
/// PDA seed for the stablecoin token definition.
|
||||
#[must_use]
|
||||
pub fn compute_stablecoin_definition_pda_seed() -> PdaSeed {
|
||||
hash_seed(&[STABLECOIN_DEFINITION_PDA_DOMAIN])
|
||||
}
|
||||
|
||||
/// Account id of the stablecoin token definition.
|
||||
#[must_use]
|
||||
pub fn compute_stablecoin_definition_pda(stablecoin_program_id: ProgramId) -> AccountId {
|
||||
AccountId::for_public_pda(
|
||||
&stablecoin_program_id,
|
||||
&compute_stablecoin_definition_pda_seed(),
|
||||
)
|
||||
}
|
||||
|
||||
/// PDA seed for the stablecoin token definition's paired master holding.
|
||||
#[must_use]
|
||||
pub fn compute_stablecoin_master_holding_pda_seed() -> PdaSeed {
|
||||
hash_seed(&[STABLECOIN_MASTER_HOLDING_PDA_DOMAIN])
|
||||
}
|
||||
|
||||
/// Account id of the stablecoin token definition's paired master holding.
|
||||
#[must_use]
|
||||
pub fn compute_stablecoin_master_holding_pda(stablecoin_program_id: ProgramId) -> AccountId {
|
||||
AccountId::for_public_pda(
|
||||
&stablecoin_program_id,
|
||||
&compute_stablecoin_master_holding_pda_seed(),
|
||||
)
|
||||
}
|
||||
|
||||
/// PDA seed for a [`Position`] account owned by `owner_id` with `position_nonce`.
|
||||
#[must_use]
|
||||
pub fn compute_position_pda_seed(owner_id: AccountId, position_nonce: u64) -> PdaSeed {
|
||||
hash_seed(&[
|
||||
&owner_id.to_bytes(),
|
||||
&position_nonce.to_le_bytes(),
|
||||
POSITION_PDA_DOMAIN,
|
||||
])
|
||||
}
|
||||
|
||||
/// Account id of the [`Position`] PDA owned by `owner_id` under `stablecoin_program_id`.
|
||||
#[must_use]
|
||||
pub fn compute_position_pda(
|
||||
stablecoin_program_id: ProgramId,
|
||||
owner_id: AccountId,
|
||||
collateral_definition_id: AccountId,
|
||||
position_nonce: u64,
|
||||
) -> AccountId {
|
||||
AccountId::for_public_pda(
|
||||
&stablecoin_program_id,
|
||||
&compute_position_pda_seed(owner_id, collateral_definition_id),
|
||||
&compute_position_pda_seed(owner_id, position_nonce),
|
||||
)
|
||||
}
|
||||
|
||||
/// PDA seed for the collateral vault token holding bound to a [`Position`].
|
||||
///
|
||||
/// Derived from the position's address with a distinct domain-separation tag so the vault
|
||||
/// id cannot collide with the position id even though both PDAs share the same program.
|
||||
#[must_use]
|
||||
pub fn compute_position_vault_pda_seed(position_id: AccountId) -> PdaSeed {
|
||||
use risc0_zkvm::sha::{Impl, Sha256 as _};
|
||||
|
||||
let mut bytes = Vec::new();
|
||||
bytes.extend_from_slice(&position_id.to_bytes());
|
||||
bytes.extend_from_slice(POSITION_VAULT_PDA_DOMAIN);
|
||||
|
||||
let mut out = [0u8; 32];
|
||||
out.copy_from_slice(Impl::hash_bytes(&bytes).as_bytes());
|
||||
PdaSeed::new(out)
|
||||
hash_seed(&[&position_id.to_bytes(), POSITION_VAULT_PDA_DOMAIN])
|
||||
}
|
||||
|
||||
/// Account id of the collateral vault PDA for `position_id` under `stablecoin_program_id`.
|
||||
#[must_use]
|
||||
pub fn compute_position_vault_pda(
|
||||
stablecoin_program_id: ProgramId,
|
||||
position_id: AccountId,
|
||||
@ -175,20 +333,18 @@ pub fn compute_position_vault_pda(
|
||||
)
|
||||
}
|
||||
|
||||
/// Verify the position account's address matches
|
||||
/// `(stablecoin_program_id, owner, collateral_definition_id)` and return the [`PdaSeed`] for
|
||||
/// use in post-state claims.
|
||||
/// Verify a position account's address and return the PDA seed for post-state claims.
|
||||
///
|
||||
/// # Panics
|
||||
/// If `position.account_id` does not match the address derived from `owner`,
|
||||
/// `collateral_definition_id`, and `stablecoin_program_id`.
|
||||
/// If `position.account_id` does not match the expected PDA.
|
||||
#[must_use]
|
||||
pub fn verify_position_and_get_seed(
|
||||
position: &AccountWithMetadata,
|
||||
owner: &AccountWithMetadata,
|
||||
collateral_definition_id: AccountId,
|
||||
position_nonce: u64,
|
||||
stablecoin_program_id: ProgramId,
|
||||
) -> PdaSeed {
|
||||
let seed = compute_position_pda_seed(owner.account_id, collateral_definition_id);
|
||||
let seed = compute_position_pda_seed(owner.account_id, position_nonce);
|
||||
let expected_id = AccountId::for_public_pda(&stablecoin_program_id, &seed);
|
||||
assert_eq!(
|
||||
position.account_id, expected_id,
|
||||
@ -197,12 +353,11 @@ pub fn verify_position_and_get_seed(
|
||||
seed
|
||||
}
|
||||
|
||||
/// Verify the vault account's address matches `(stablecoin_program_id, position)` and
|
||||
/// return the [`PdaSeed`] for use in chained calls.
|
||||
/// Verify a vault account's address and return the PDA seed for chained calls.
|
||||
///
|
||||
/// # Panics
|
||||
/// If `vault.account_id` does not match the address derived from `position_id` and
|
||||
/// `stablecoin_program_id`.
|
||||
/// If `vault.account_id` does not match the expected PDA.
|
||||
#[must_use]
|
||||
pub fn verify_position_vault_and_get_seed(
|
||||
vault: &AccountWithMetadata,
|
||||
position_id: AccountId,
|
||||
@ -216,3 +371,237 @@ pub fn verify_position_vault_and_get_seed(
|
||||
);
|
||||
seed
|
||||
}
|
||||
|
||||
/// Projects the current global stability-fee accumulator.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if fixed-point multiplication overflows.
|
||||
#[must_use]
|
||||
pub fn current_accumulated_rate(
|
||||
state: &StabilityFeeAccumulator,
|
||||
params: &ProtocolParameters,
|
||||
now: u64,
|
||||
) -> u128 {
|
||||
let elapsed = now
|
||||
.saturating_sub(state.last_accrued_at)
|
||||
.min(MAXIMUM_COMPOUNDING_WINDOW_MILLISECONDS);
|
||||
let factor = compound_rate(params.stability_fee_per_millisecond, elapsed);
|
||||
mul_div_floor(
|
||||
state.accumulated_rate_at_last_accrual,
|
||||
factor,
|
||||
FIXED_POINT_ONE,
|
||||
)
|
||||
}
|
||||
|
||||
/// Projects the current redemption price.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if fixed-point multiplication overflows.
|
||||
#[must_use]
|
||||
pub fn current_redemption_price(state: &RedemptionPriceState, now: u64) -> u128 {
|
||||
let elapsed = now
|
||||
.saturating_sub(state.last_updated_at)
|
||||
.min(MAXIMUM_COMPOUNDING_WINDOW_MILLISECONDS);
|
||||
let factor = compound_rate(state.redemption_rate_per_millisecond, elapsed);
|
||||
mul_div_floor(
|
||||
state.redemption_price_at_last_update,
|
||||
factor,
|
||||
FIXED_POINT_ONE,
|
||||
)
|
||||
}
|
||||
|
||||
/// Computes nominal stablecoin debt from normalized position debt.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if fixed-point multiplication overflows.
|
||||
#[must_use]
|
||||
pub fn nominal_debt(normalized_debt_amount: u128, current_accumulator: u128) -> u128 {
|
||||
mul_div_floor(normalized_debt_amount, current_accumulator, FIXED_POINT_ONE)
|
||||
}
|
||||
|
||||
/// Checks the README collateralization inequality using 256-bit intermediates.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if an intermediate exceeds 256 bits.
|
||||
#[must_use]
|
||||
pub fn is_collateralized(
|
||||
collateral_amount: u128,
|
||||
normalized_debt_amount: u128,
|
||||
current_accumulator: u128,
|
||||
redemption_price: u128,
|
||||
minimum_collateralization_ratio: u128,
|
||||
) -> bool {
|
||||
let nominal_debt = nominal_debt(normalized_debt_amount, current_accumulator);
|
||||
if nominal_debt == 0 {
|
||||
return true;
|
||||
}
|
||||
|
||||
let fixed_point = U256::from(FIXED_POINT_ONE);
|
||||
let left = U256::from(collateral_amount)
|
||||
.checked_mul(fixed_point)
|
||||
.and_then(|value| value.checked_mul(fixed_point))
|
||||
.expect("collateral side should fit in U256");
|
||||
let right = U256::from(nominal_debt)
|
||||
.checked_mul(U256::from(redemption_price))
|
||||
.and_then(|value| value.checked_mul(U256::from(minimum_collateralization_ratio)))
|
||||
.expect("debt side should fit in U256");
|
||||
|
||||
left >= right
|
||||
}
|
||||
|
||||
/// Validates protocol parameter bounds from the stablecoin README.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if any parameter is outside the accepted range.
|
||||
pub fn assert_protocol_parameter_bounds(
|
||||
stability_fee_per_millisecond: u128,
|
||||
controller_proportional_gain: i128,
|
||||
controller_integral_gain: i128,
|
||||
minimum_collateralization_ratio: u128,
|
||||
minimum_milliseconds_between_rate_updates: u64,
|
||||
maximum_oracle_price_age_milliseconds: u64,
|
||||
initial_redemption_price: u128,
|
||||
) {
|
||||
assert_valid_stability_fee_per_millisecond(stability_fee_per_millisecond);
|
||||
assert!(
|
||||
controller_proportional_gain.unsigned_abs() <= MAX_CONTROLLER_PROPORTIONAL_GAIN,
|
||||
"Controller proportional gain is out of bounds"
|
||||
);
|
||||
assert!(
|
||||
controller_integral_gain.unsigned_abs() <= MAX_CONTROLLER_INTEGRAL_GAIN,
|
||||
"Controller integral gain is out of bounds"
|
||||
);
|
||||
assert!(
|
||||
(MINIMUM_COLLATERALIZATION_RATIO_LOWER_BOUND..=MINIMUM_COLLATERALIZATION_RATIO_UPPER_BOUND)
|
||||
.contains(&minimum_collateralization_ratio),
|
||||
"Minimum collateralization ratio is out of bounds"
|
||||
);
|
||||
assert!(
|
||||
(1..=MAX_TIMING_PARAMETER_MILLISECONDS)
|
||||
.contains(&minimum_milliseconds_between_rate_updates),
|
||||
"Minimum milliseconds between rate updates is out of bounds"
|
||||
);
|
||||
assert!(
|
||||
(1..=MAX_TIMING_PARAMETER_MILLISECONDS).contains(&maximum_oracle_price_age_milliseconds),
|
||||
"Maximum oracle price age is out of bounds"
|
||||
);
|
||||
assert!(
|
||||
initial_redemption_price != 0,
|
||||
"Initial redemption price must be non-zero"
|
||||
);
|
||||
}
|
||||
|
||||
/// Validates the stability-fee multiplier bound.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if `rate` is outside the accepted range.
|
||||
pub fn assert_valid_stability_fee_per_millisecond(rate: u128) {
|
||||
assert!(
|
||||
(FIXED_POINT_ONE..=MAX_STABILITY_FEE_PER_MILLISECOND).contains(&rate),
|
||||
"Stability fee per millisecond is out of bounds"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const PROGRAM_ID: ProgramId = [1u32; 8];
|
||||
|
||||
fn params(rate: u128) -> ProtocolParameters {
|
||||
ProtocolParameters {
|
||||
admin_account_id: AccountId::new([1u8; 32]),
|
||||
freeze_authority_account_id: AccountId::new([2u8; 32]),
|
||||
stablecoin_definition_id: AccountId::new([3u8; 32]),
|
||||
collateral_definition_id: AccountId::new([4u8; 32]),
|
||||
market_price_oracle_id: AccountId::new([5u8; 32]),
|
||||
stability_fee_per_millisecond: rate,
|
||||
controller_proportional_gain: 0,
|
||||
controller_integral_gain: 0,
|
||||
minimum_collateralization_ratio: MINIMUM_COLLATERALIZATION_RATIO_LOWER_BOUND,
|
||||
minimum_milliseconds_between_rate_updates: 1,
|
||||
maximum_oracle_price_age_milliseconds: 1,
|
||||
is_frozen: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compound_rate_returns_identity_for_zero_elapsed_time() {
|
||||
assert_eq!(
|
||||
compound_rate(FIXED_POINT_ONE + FIXED_POINT_ONE / 10, 0),
|
||||
FIXED_POINT_ONE
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compound_rate_keeps_one_rate_at_identity() {
|
||||
assert_eq!(compound_rate(FIXED_POINT_ONE, 123), FIXED_POINT_ONE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compound_rate_compounds_small_growth() {
|
||||
let rate = FIXED_POINT_ONE + FIXED_POINT_ONE / 10;
|
||||
|
||||
assert_eq!(
|
||||
compound_rate(rate, 2),
|
||||
FIXED_POINT_ONE + FIXED_POINT_ONE / 5 + FIXED_POINT_ONE / 100
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mul_div_floor_and_ceil_split_remainders() {
|
||||
assert_eq!(mul_div_floor(10, 10, 6), 16);
|
||||
assert_eq!(mul_div_ceil(10, 10, 6), 17);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_accumulated_rate_clamps_elapsed_time() {
|
||||
let rate = FIXED_POINT_ONE + 1;
|
||||
let state = StabilityFeeAccumulator {
|
||||
accumulated_rate_at_last_accrual: FIXED_POINT_ONE,
|
||||
last_accrued_at: 0,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
current_accumulated_rate(
|
||||
&state,
|
||||
¶ms(rate),
|
||||
MAXIMUM_COMPOUNDING_WINDOW_MILLISECONDS + 1
|
||||
),
|
||||
compound_rate(rate, MAXIMUM_COMPOUNDING_WINDOW_MILLISECONDS)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_stability_fee_compounds_within_the_clamped_window() {
|
||||
let factor = compound_rate(
|
||||
MAX_STABILITY_FEE_PER_MILLISECOND,
|
||||
MAXIMUM_COMPOUNDING_WINDOW_MILLISECONDS,
|
||||
);
|
||||
|
||||
assert!(factor >= FIXED_POINT_ONE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Stability fee per millisecond is out of bounds")]
|
||||
fn stability_fee_bound_rejects_rate_above_safe_maximum() {
|
||||
assert_valid_stability_fee_per_millisecond(MAX_STABILITY_FEE_PER_MILLISECOND + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nominal_debt_grows_with_accumulator() {
|
||||
assert_eq!(
|
||||
nominal_debt(100, FIXED_POINT_ONE + FIXED_POINT_ONE / 10),
|
||||
110
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pda_helpers_keep_position_and_vault_derivations_distinct() {
|
||||
let owner = AccountId::new([9u8; 32]);
|
||||
let position = compute_position_pda(PROGRAM_ID, owner, 7);
|
||||
let vault = compute_position_vault_pda(PROGRAM_ID, position);
|
||||
|
||||
assert_ne!(position, vault);
|
||||
}
|
||||
}
|
||||
|
||||
475
programs/stablecoin/methods/guest/Cargo.lock
generated
475
programs/stablecoin/methods/guest/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
#![cfg_attr(not(test), no_main)]
|
||||
|
||||
use nssa_core::account::AccountWithMetadata;
|
||||
use nssa_core::account::{AccountId, AccountWithMetadata};
|
||||
use spel_framework::context::ProgramContext;
|
||||
use spel_framework::prelude::*;
|
||||
|
||||
@ -12,11 +12,109 @@ mod stablecoin {
|
||||
#[allow(unused_imports)]
|
||||
use super::*;
|
||||
|
||||
/// Initialize protocol globals and the stablecoin token definition.
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "instruction interface initializes all stablecoin singleton accounts"
|
||||
)]
|
||||
#[instruction]
|
||||
pub fn initialize_program(
|
||||
ctx: ProgramContext,
|
||||
admin: AccountWithMetadata,
|
||||
protocol_parameters: AccountWithMetadata,
|
||||
stability_fee_accumulator: AccountWithMetadata,
|
||||
redemption_price_state: AccountWithMetadata,
|
||||
stablecoin_definition: AccountWithMetadata,
|
||||
stablecoin_master_holding: AccountWithMetadata,
|
||||
collateral_definition: AccountWithMetadata,
|
||||
market_price_oracle: AccountWithMetadata,
|
||||
clock: AccountWithMetadata,
|
||||
freeze_authority_account_id: AccountId,
|
||||
initial_stability_fee_per_millisecond: u128,
|
||||
initial_controller_proportional_gain: i128,
|
||||
initial_controller_integral_gain: i128,
|
||||
initial_minimum_collateralization_ratio: u128,
|
||||
minimum_milliseconds_between_rate_updates: u64,
|
||||
maximum_oracle_price_age_milliseconds: u64,
|
||||
initial_redemption_price: u128,
|
||||
stablecoin_name: String,
|
||||
) -> SpelResult {
|
||||
let (post_states, chained_calls) = stablecoin_program::initialize_program::initialize_program(
|
||||
admin,
|
||||
protocol_parameters,
|
||||
stability_fee_accumulator,
|
||||
redemption_price_state,
|
||||
stablecoin_definition,
|
||||
stablecoin_master_holding,
|
||||
collateral_definition,
|
||||
market_price_oracle,
|
||||
clock,
|
||||
ctx.self_program_id,
|
||||
freeze_authority_account_id,
|
||||
initial_stability_fee_per_millisecond,
|
||||
initial_controller_proportional_gain,
|
||||
initial_controller_integral_gain,
|
||||
initial_minimum_collateralization_ratio,
|
||||
minimum_milliseconds_between_rate_updates,
|
||||
maximum_oracle_price_age_milliseconds,
|
||||
initial_redemption_price,
|
||||
stablecoin_name,
|
||||
);
|
||||
Ok(spel_framework::SpelOutput::execute(
|
||||
post_states,
|
||||
chained_calls,
|
||||
))
|
||||
}
|
||||
|
||||
/// Roll the global stability-fee accumulator forward.
|
||||
#[instruction]
|
||||
pub fn accrue_stability_fee(
|
||||
ctx: ProgramContext,
|
||||
caller: AccountWithMetadata,
|
||||
protocol_parameters: AccountWithMetadata,
|
||||
stability_fee_accumulator: AccountWithMetadata,
|
||||
clock: AccountWithMetadata,
|
||||
) -> SpelResult {
|
||||
let (post_states, chained_calls) =
|
||||
stablecoin_program::accrue_stability_fee::accrue_stability_fee(
|
||||
caller,
|
||||
protocol_parameters,
|
||||
stability_fee_accumulator,
|
||||
clock,
|
||||
ctx.self_program_id,
|
||||
);
|
||||
Ok(spel_framework::SpelOutput::execute(
|
||||
post_states,
|
||||
chained_calls,
|
||||
))
|
||||
}
|
||||
|
||||
/// Update stability-fee rate after accruing pending fees at the old rate.
|
||||
#[instruction]
|
||||
pub fn set_stability_fee_per_millisecond(
|
||||
ctx: ProgramContext,
|
||||
admin: AccountWithMetadata,
|
||||
protocol_parameters: AccountWithMetadata,
|
||||
stability_fee_accumulator: AccountWithMetadata,
|
||||
clock: AccountWithMetadata,
|
||||
new_rate: u128,
|
||||
) -> SpelResult {
|
||||
let (post_states, chained_calls) =
|
||||
stablecoin_program::set_stability_fee_per_millisecond::set_stability_fee_per_millisecond(
|
||||
admin,
|
||||
protocol_parameters,
|
||||
stability_fee_accumulator,
|
||||
clock,
|
||||
ctx.self_program_id,
|
||||
new_rate,
|
||||
);
|
||||
Ok(spel_framework::SpelOutput::execute(
|
||||
post_states,
|
||||
chained_calls,
|
||||
))
|
||||
}
|
||||
|
||||
/// Open a new collateral-only position for the calling owner.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns the host program's panic-converted error if any precondition fails (see
|
||||
/// [`stablecoin_program::open_position::open_position`] for the full list).
|
||||
#[instruction]
|
||||
pub fn open_position(
|
||||
ctx: ProgramContext,
|
||||
@ -28,17 +126,23 @@ mod stablecoin {
|
||||
vault: AccountWithMetadata,
|
||||
#[account(mut, signer)]
|
||||
user_holding: AccountWithMetadata,
|
||||
token_definition: AccountWithMetadata,
|
||||
collateral_amount: u128,
|
||||
collateral_definition: AccountWithMetadata,
|
||||
protocol_parameters: AccountWithMetadata,
|
||||
clock: AccountWithMetadata,
|
||||
position_nonce: u64,
|
||||
initial_collateral_amount: u128,
|
||||
) -> SpelResult {
|
||||
let (post_states, chained_calls) = stablecoin_program::open_position::open_position(
|
||||
owner,
|
||||
position,
|
||||
vault,
|
||||
user_holding,
|
||||
token_definition,
|
||||
collateral_definition,
|
||||
protocol_parameters,
|
||||
clock,
|
||||
ctx.self_program_id,
|
||||
collateral_amount,
|
||||
position_nonce,
|
||||
initial_collateral_amount,
|
||||
);
|
||||
Ok(spel_framework::SpelOutput::execute(
|
||||
post_states,
|
||||
@ -46,14 +150,49 @@ mod stablecoin {
|
||||
))
|
||||
}
|
||||
|
||||
/// Withdraw `amount` collateral tokens from an existing position back to a
|
||||
/// user-controlled holding.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns the host program's panic-converted error if any precondition
|
||||
/// fails (see
|
||||
/// [`stablecoin_program::withdraw_collateral::withdraw_collateral`] for the
|
||||
/// full list).
|
||||
/// Mint stablecoin debt against an existing position.
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "instruction interface passes explicit position, token, oracle, and protocol accounts"
|
||||
)]
|
||||
#[instruction]
|
||||
pub fn generate_debt(
|
||||
ctx: ProgramContext,
|
||||
owner: AccountWithMetadata,
|
||||
position: AccountWithMetadata,
|
||||
stablecoin_definition: AccountWithMetadata,
|
||||
user_stablecoin_holding: AccountWithMetadata,
|
||||
stability_fee_accumulator: AccountWithMetadata,
|
||||
redemption_price_state: AccountWithMetadata,
|
||||
market_price_oracle: AccountWithMetadata,
|
||||
protocol_parameters: AccountWithMetadata,
|
||||
clock: AccountWithMetadata,
|
||||
amount: u128,
|
||||
) -> SpelResult {
|
||||
let (post_states, chained_calls) = stablecoin_program::generate_debt::generate_debt(
|
||||
owner,
|
||||
position,
|
||||
stablecoin_definition,
|
||||
user_stablecoin_holding,
|
||||
stability_fee_accumulator,
|
||||
redemption_price_state,
|
||||
market_price_oracle,
|
||||
protocol_parameters,
|
||||
clock,
|
||||
ctx.self_program_id,
|
||||
amount,
|
||||
);
|
||||
Ok(spel_framework::SpelOutput::execute(
|
||||
post_states,
|
||||
chained_calls,
|
||||
))
|
||||
}
|
||||
|
||||
/// Withdraw collateral from an existing position.
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "instruction interface passes explicit position and protocol accounts"
|
||||
)]
|
||||
#[instruction]
|
||||
pub fn withdraw_collateral(
|
||||
ctx: ProgramContext,
|
||||
@ -65,6 +204,10 @@ mod stablecoin {
|
||||
vault: AccountWithMetadata,
|
||||
#[account(mut)]
|
||||
destination: AccountWithMetadata,
|
||||
stability_fee_accumulator: AccountWithMetadata,
|
||||
redemption_price_state: AccountWithMetadata,
|
||||
protocol_parameters: AccountWithMetadata,
|
||||
clock: AccountWithMetadata,
|
||||
amount: u128,
|
||||
) -> SpelResult {
|
||||
let (post_states, chained_calls) =
|
||||
@ -73,6 +216,10 @@ mod stablecoin {
|
||||
position,
|
||||
vault,
|
||||
destination,
|
||||
stability_fee_accumulator,
|
||||
redemption_price_state,
|
||||
protocol_parameters,
|
||||
clock,
|
||||
ctx.self_program_id,
|
||||
amount,
|
||||
);
|
||||
@ -82,12 +229,7 @@ mod stablecoin {
|
||||
))
|
||||
}
|
||||
|
||||
/// Repay `amount` of outstanding stablecoin debt against an existing position.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns the host program's panic-converted error if any precondition
|
||||
/// fails (see [`stablecoin_program::repay_debt::repay_debt`] for the
|
||||
/// full list).
|
||||
/// Repay stablecoin debt against an existing position.
|
||||
#[instruction]
|
||||
pub fn repay_debt(
|
||||
ctx: ProgramContext,
|
||||
@ -99,6 +241,9 @@ mod stablecoin {
|
||||
stablecoin_definition: AccountWithMetadata,
|
||||
#[account(mut, signer)]
|
||||
user_stablecoin_holding: AccountWithMetadata,
|
||||
stability_fee_accumulator: AccountWithMetadata,
|
||||
protocol_parameters: AccountWithMetadata,
|
||||
clock: AccountWithMetadata,
|
||||
amount: u128,
|
||||
) -> SpelResult {
|
||||
let (post_states, chained_calls) = stablecoin_program::repay_debt::repay_debt(
|
||||
@ -106,6 +251,9 @@ mod stablecoin {
|
||||
position,
|
||||
stablecoin_definition,
|
||||
user_stablecoin_holding,
|
||||
stability_fee_accumulator,
|
||||
protocol_parameters,
|
||||
clock,
|
||||
ctx.self_program_id,
|
||||
amount,
|
||||
);
|
||||
|
||||
41
programs/stablecoin/src/accrue_stability_fee.rs
Normal file
41
programs/stablecoin/src/accrue_stability_fee.rs
Normal file
@ -0,0 +1,41 @@
|
||||
use nssa_core::{
|
||||
account::{AccountWithMetadata, Data},
|
||||
program::{AccountPostState, ChainedCall, ProgramId},
|
||||
};
|
||||
|
||||
use crate::shared::{
|
||||
accrue_stability_fee_state, read_clock_timestamp, read_protocol_parameters,
|
||||
read_stability_fee_accumulator,
|
||||
};
|
||||
|
||||
/// Rolls the global stability-fee accumulator forward.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if the caller is unauthorized or the protocol accounts are invalid.
|
||||
pub fn accrue_stability_fee(
|
||||
caller: AccountWithMetadata,
|
||||
protocol_parameters: AccountWithMetadata,
|
||||
stability_fee_accumulator: AccountWithMetadata,
|
||||
clock: AccountWithMetadata,
|
||||
stablecoin_program_id: ProgramId,
|
||||
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||
assert!(caller.is_authorized, "Caller authorization is missing");
|
||||
|
||||
let params = read_protocol_parameters(&protocol_parameters, stablecoin_program_id);
|
||||
let accumulator =
|
||||
read_stability_fee_accumulator(&stability_fee_accumulator, stablecoin_program_id);
|
||||
let now = read_clock_timestamp(&clock);
|
||||
let updated = accrue_stability_fee_state(&accumulator, ¶ms, now);
|
||||
|
||||
let mut accumulator_post = stability_fee_accumulator.account.clone();
|
||||
accumulator_post.data = Data::from(&updated);
|
||||
|
||||
let post_states = vec![
|
||||
AccountPostState::new(caller.account),
|
||||
AccountPostState::new(protocol_parameters.account),
|
||||
AccountPostState::new(accumulator_post),
|
||||
AccountPostState::new(clock.account),
|
||||
];
|
||||
|
||||
(post_states, vec![])
|
||||
}
|
||||
177
programs/stablecoin/src/generate_debt.rs
Normal file
177
programs/stablecoin/src/generate_debt.rs
Normal file
@ -0,0 +1,177 @@
|
||||
use nssa_core::{
|
||||
account::{Account, AccountWithMetadata, Data},
|
||||
program::{AccountPostState, ChainedCall, ProgramId},
|
||||
};
|
||||
use stablecoin_core::{
|
||||
compute_stablecoin_definition_pda_seed, verify_position_and_get_seed, Position, FIXED_POINT_ONE,
|
||||
};
|
||||
use token_core::TokenHolding;
|
||||
use twap_oracle_core::OraclePriceAccount;
|
||||
|
||||
use crate::shared::{
|
||||
read_clock_timestamp, read_protocol_parameters, read_redemption_price_state,
|
||||
read_stability_fee_accumulator,
|
||||
};
|
||||
|
||||
/// Mints stablecoin debt against an existing position.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if the position is not authorized, the oracle is stale, or the post-mint position would
|
||||
/// be undercollateralized.
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "instruction surface passes explicit position, token, oracle, and protocol accounts"
|
||||
)]
|
||||
pub fn generate_debt(
|
||||
owner: AccountWithMetadata,
|
||||
position: AccountWithMetadata,
|
||||
stablecoin_definition: AccountWithMetadata,
|
||||
user_stablecoin_holding: AccountWithMetadata,
|
||||
stability_fee_accumulator: AccountWithMetadata,
|
||||
redemption_price_state: AccountWithMetadata,
|
||||
market_price_oracle: AccountWithMetadata,
|
||||
protocol_parameters: AccountWithMetadata,
|
||||
clock: AccountWithMetadata,
|
||||
stablecoin_program_id: ProgramId,
|
||||
amount: u128,
|
||||
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||
assert!(owner.is_authorized, "Owner authorization is missing");
|
||||
let params = read_protocol_parameters(&protocol_parameters, stablecoin_program_id);
|
||||
assert!(
|
||||
!params.is_frozen,
|
||||
"Protocol is frozen; debt generation is disabled"
|
||||
);
|
||||
assert_eq!(
|
||||
stablecoin_definition.account_id, params.stablecoin_definition_id,
|
||||
"Stablecoin definition does not match protocol parameters"
|
||||
);
|
||||
|
||||
let accumulator =
|
||||
read_stability_fee_accumulator(&stability_fee_accumulator, stablecoin_program_id);
|
||||
let redemption_state =
|
||||
read_redemption_price_state(&redemption_price_state, stablecoin_program_id);
|
||||
let now = read_clock_timestamp(&clock);
|
||||
let current_accumulator = stablecoin_core::current_accumulated_rate(&accumulator, ¶ms, now);
|
||||
let current_redemption_price =
|
||||
stablecoin_core::current_redemption_price(&redemption_state, now);
|
||||
|
||||
assert_ne!(
|
||||
position.account,
|
||||
Account::default(),
|
||||
"Position account must be initialized"
|
||||
);
|
||||
assert_eq!(
|
||||
position.account.program_owner, stablecoin_program_id,
|
||||
"Position is not owned by this stablecoin program"
|
||||
);
|
||||
let position_data = Position::try_from(&position.account.data)
|
||||
.expect("Position account must hold valid Position state");
|
||||
assert_eq!(
|
||||
position_data.owner_account_id, owner.account_id,
|
||||
"Position owner does not match signer"
|
||||
);
|
||||
let _position_seed = verify_position_and_get_seed(
|
||||
&position,
|
||||
&owner,
|
||||
position_data.position_nonce,
|
||||
stablecoin_program_id,
|
||||
);
|
||||
|
||||
assert_ne!(
|
||||
stablecoin_definition.account,
|
||||
Account::default(),
|
||||
"Stablecoin definition account must be initialized"
|
||||
);
|
||||
assert_ne!(
|
||||
user_stablecoin_holding.account,
|
||||
Account::default(),
|
||||
"User stablecoin holding must be initialized"
|
||||
);
|
||||
assert_eq!(
|
||||
user_stablecoin_holding.account.program_owner, stablecoin_definition.account.program_owner,
|
||||
"Stablecoin holding and definition must be owned by the same Token Program"
|
||||
);
|
||||
let user_holding = TokenHolding::try_from(&user_stablecoin_holding.account.data)
|
||||
.expect("User stablecoin holding must hold a valid TokenHolding");
|
||||
assert_eq!(
|
||||
user_holding.definition_id(),
|
||||
stablecoin_definition.account_id,
|
||||
"Stablecoin holding does not match the provided stablecoin definition"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
market_price_oracle.account_id, params.market_price_oracle_id,
|
||||
"Market price oracle does not match protocol parameters"
|
||||
);
|
||||
let oracle = OraclePriceAccount::try_from(&market_price_oracle.account.data)
|
||||
.expect("Market price oracle account must hold a valid OraclePriceAccount");
|
||||
assert_eq!(
|
||||
oracle.base_asset, params.stablecoin_definition_id,
|
||||
"Market price oracle base asset must be the stablecoin definition"
|
||||
);
|
||||
assert_eq!(
|
||||
oracle.quote_asset, params.collateral_definition_id,
|
||||
"Market price oracle quote asset must be the collateral definition"
|
||||
);
|
||||
assert!(
|
||||
oracle.price != 0,
|
||||
"Market price oracle price must be non-zero"
|
||||
);
|
||||
assert!(
|
||||
now.saturating_sub(oracle.timestamp) <= params.maximum_oracle_price_age_milliseconds,
|
||||
"Market price oracle is stale"
|
||||
);
|
||||
|
||||
let normalized_delta =
|
||||
stablecoin_core::mul_div_ceil(amount, FIXED_POINT_ONE, current_accumulator);
|
||||
let new_normalized_debt = position_data
|
||||
.normalized_debt_amount
|
||||
.checked_add(normalized_delta)
|
||||
.expect("Position normalized debt overflow");
|
||||
assert!(
|
||||
stablecoin_core::is_collateralized(
|
||||
position_data.collateral_amount,
|
||||
new_normalized_debt,
|
||||
current_accumulator,
|
||||
current_redemption_price,
|
||||
params.minimum_collateralization_ratio,
|
||||
),
|
||||
"Position would be undercollateralized after debt generation"
|
||||
);
|
||||
|
||||
let updated_position = Position {
|
||||
owner_account_id: position_data.owner_account_id,
|
||||
position_nonce: position_data.position_nonce,
|
||||
vault_account_id: position_data.vault_account_id,
|
||||
collateral_amount: position_data.collateral_amount,
|
||||
normalized_debt_amount: new_normalized_debt,
|
||||
opened_at: position_data.opened_at,
|
||||
};
|
||||
let mut position_post = position.account.clone();
|
||||
position_post.data = Data::from(&updated_position);
|
||||
|
||||
let post_states = vec![
|
||||
AccountPostState::new(owner.account),
|
||||
AccountPostState::new(position_post),
|
||||
AccountPostState::new(stablecoin_definition.account.clone()),
|
||||
AccountPostState::new(user_stablecoin_holding.account.clone()),
|
||||
AccountPostState::new(stability_fee_accumulator.account),
|
||||
AccountPostState::new(redemption_price_state.account),
|
||||
AccountPostState::new(market_price_oracle.account),
|
||||
AccountPostState::new(protocol_parameters.account),
|
||||
AccountPostState::new(clock.account),
|
||||
];
|
||||
|
||||
let mut stablecoin_definition_authorized = stablecoin_definition;
|
||||
stablecoin_definition_authorized.is_authorized = true;
|
||||
let mint_call = ChainedCall::new(
|
||||
stablecoin_definition_authorized.account.program_owner,
|
||||
vec![stablecoin_definition_authorized, user_stablecoin_holding],
|
||||
&token_core::Instruction::Mint {
|
||||
amount_to_mint: amount,
|
||||
},
|
||||
)
|
||||
.with_pda_seeds(vec![compute_stablecoin_definition_pda_seed()]);
|
||||
|
||||
(post_states, vec![mint_call])
|
||||
}
|
||||
206
programs/stablecoin/src/initialize_program.rs
Normal file
206
programs/stablecoin/src/initialize_program.rs
Normal file
@ -0,0 +1,206 @@
|
||||
use nssa_core::{
|
||||
account::{Account, AccountWithMetadata, Data},
|
||||
program::{AccountPostState, ChainedCall, Claim, ProgramId},
|
||||
};
|
||||
use stablecoin_core::{
|
||||
assert_protocol_parameter_bounds, compute_protocol_parameters_pda,
|
||||
compute_protocol_parameters_pda_seed, compute_redemption_price_state_pda,
|
||||
compute_redemption_price_state_pda_seed, compute_stability_fee_accumulator_pda,
|
||||
compute_stability_fee_accumulator_pda_seed, compute_stablecoin_definition_pda,
|
||||
compute_stablecoin_definition_pda_seed, compute_stablecoin_master_holding_pda,
|
||||
compute_stablecoin_master_holding_pda_seed, ProtocolParameters, RedemptionPriceState,
|
||||
StabilityFeeAccumulator, FIXED_POINT_ONE,
|
||||
};
|
||||
use token_core::TokenDefinition;
|
||||
use twap_oracle_core::OraclePriceAccount;
|
||||
|
||||
use crate::shared::read_clock_timestamp;
|
||||
|
||||
/// Initializes stablecoin protocol globals and creates the stablecoin token definition.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if any account is not the expected PDA/state shape or any parameter is out of bounds.
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "instruction surface initializes all protocol singleton accounts in one call"
|
||||
)]
|
||||
pub fn initialize_program(
|
||||
admin: AccountWithMetadata,
|
||||
protocol_parameters: AccountWithMetadata,
|
||||
stability_fee_accumulator: AccountWithMetadata,
|
||||
redemption_price_state: AccountWithMetadata,
|
||||
stablecoin_definition: AccountWithMetadata,
|
||||
stablecoin_master_holding: AccountWithMetadata,
|
||||
collateral_definition: AccountWithMetadata,
|
||||
market_price_oracle: AccountWithMetadata,
|
||||
clock: AccountWithMetadata,
|
||||
stablecoin_program_id: ProgramId,
|
||||
freeze_authority_account_id: nssa_core::account::AccountId,
|
||||
initial_stability_fee_per_millisecond: u128,
|
||||
initial_controller_proportional_gain: i128,
|
||||
initial_controller_integral_gain: i128,
|
||||
initial_minimum_collateralization_ratio: u128,
|
||||
minimum_milliseconds_between_rate_updates: u64,
|
||||
maximum_oracle_price_age_milliseconds: u64,
|
||||
initial_redemption_price: u128,
|
||||
stablecoin_name: String,
|
||||
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||
assert!(admin.is_authorized, "Admin authorization is missing");
|
||||
assert_protocol_parameter_bounds(
|
||||
initial_stability_fee_per_millisecond,
|
||||
initial_controller_proportional_gain,
|
||||
initial_controller_integral_gain,
|
||||
initial_minimum_collateralization_ratio,
|
||||
minimum_milliseconds_between_rate_updates,
|
||||
maximum_oracle_price_age_milliseconds,
|
||||
initial_redemption_price,
|
||||
);
|
||||
|
||||
assert_uninitialized_pda(
|
||||
&protocol_parameters,
|
||||
compute_protocol_parameters_pda(stablecoin_program_id),
|
||||
"Protocol parameters",
|
||||
);
|
||||
assert_uninitialized_pda(
|
||||
&stability_fee_accumulator,
|
||||
compute_stability_fee_accumulator_pda(stablecoin_program_id),
|
||||
"Stability fee accumulator",
|
||||
);
|
||||
assert_uninitialized_pda(
|
||||
&redemption_price_state,
|
||||
compute_redemption_price_state_pda(stablecoin_program_id),
|
||||
"Redemption price state",
|
||||
);
|
||||
assert_uninitialized_pda(
|
||||
&stablecoin_definition,
|
||||
compute_stablecoin_definition_pda(stablecoin_program_id),
|
||||
"Stablecoin definition",
|
||||
);
|
||||
assert_uninitialized_pda(
|
||||
&stablecoin_master_holding,
|
||||
compute_stablecoin_master_holding_pda(stablecoin_program_id),
|
||||
"Stablecoin master holding",
|
||||
);
|
||||
|
||||
assert_ne!(
|
||||
collateral_definition.account,
|
||||
Account::default(),
|
||||
"Collateral definition account must be initialized"
|
||||
);
|
||||
let collateral_definition_data = TokenDefinition::try_from(&collateral_definition.account.data)
|
||||
.expect("Collateral definition account must hold a valid TokenDefinition");
|
||||
assert!(
|
||||
matches!(collateral_definition_data, TokenDefinition::Fungible { .. }),
|
||||
"Collateral definition must be fungible"
|
||||
);
|
||||
|
||||
assert_ne!(
|
||||
market_price_oracle.account,
|
||||
Account::default(),
|
||||
"Market price oracle account must be initialized"
|
||||
);
|
||||
let oracle = OraclePriceAccount::try_from(&market_price_oracle.account.data)
|
||||
.expect("Market price oracle account must hold a valid OraclePriceAccount");
|
||||
assert_eq!(
|
||||
oracle.base_asset, stablecoin_definition.account_id,
|
||||
"Market price oracle base asset must be the stablecoin definition"
|
||||
);
|
||||
assert_eq!(
|
||||
oracle.quote_asset, collateral_definition.account_id,
|
||||
"Market price oracle quote asset must be the collateral definition"
|
||||
);
|
||||
|
||||
let now = read_clock_timestamp(&clock);
|
||||
|
||||
let protocol_data = ProtocolParameters {
|
||||
admin_account_id: admin.account_id,
|
||||
freeze_authority_account_id,
|
||||
stablecoin_definition_id: stablecoin_definition.account_id,
|
||||
collateral_definition_id: collateral_definition.account_id,
|
||||
market_price_oracle_id: market_price_oracle.account_id,
|
||||
stability_fee_per_millisecond: initial_stability_fee_per_millisecond,
|
||||
controller_proportional_gain: initial_controller_proportional_gain,
|
||||
controller_integral_gain: initial_controller_integral_gain,
|
||||
minimum_collateralization_ratio: initial_minimum_collateralization_ratio,
|
||||
minimum_milliseconds_between_rate_updates,
|
||||
maximum_oracle_price_age_milliseconds,
|
||||
is_frozen: false,
|
||||
};
|
||||
let mut protocol_post = protocol_parameters.account.clone();
|
||||
protocol_post.data = Data::from(&protocol_data);
|
||||
|
||||
let mut fee_post = stability_fee_accumulator.account.clone();
|
||||
fee_post.data = Data::from(&StabilityFeeAccumulator {
|
||||
accumulated_rate_at_last_accrual: FIXED_POINT_ONE,
|
||||
last_accrued_at: now,
|
||||
});
|
||||
|
||||
let mut redemption_post = redemption_price_state.account.clone();
|
||||
redemption_post.data = Data::from(&RedemptionPriceState {
|
||||
redemption_price_at_last_update: initial_redemption_price,
|
||||
redemption_rate_per_millisecond: FIXED_POINT_ONE,
|
||||
controller_integral_term: 0,
|
||||
last_updated_at: now,
|
||||
});
|
||||
|
||||
let mut stablecoin_definition_authorized = stablecoin_definition.clone();
|
||||
stablecoin_definition_authorized.is_authorized = true;
|
||||
let mut stablecoin_master_holding_authorized = stablecoin_master_holding.clone();
|
||||
stablecoin_master_holding_authorized.is_authorized = true;
|
||||
let token_program_id = collateral_definition.account.program_owner;
|
||||
let create_stablecoin_call = ChainedCall::new(
|
||||
token_program_id,
|
||||
vec![
|
||||
stablecoin_definition_authorized,
|
||||
stablecoin_master_holding_authorized,
|
||||
],
|
||||
&token_core::Instruction::NewFungibleDefinition {
|
||||
name: stablecoin_name,
|
||||
total_supply: 0,
|
||||
mint_authority: Some(stablecoin_definition.account_id),
|
||||
},
|
||||
)
|
||||
.with_pda_seeds(vec![
|
||||
compute_stablecoin_definition_pda_seed(),
|
||||
compute_stablecoin_master_holding_pda_seed(),
|
||||
]);
|
||||
|
||||
let post_states = vec![
|
||||
AccountPostState::new(admin.account),
|
||||
AccountPostState::new_claimed(
|
||||
protocol_post,
|
||||
Claim::Pda(compute_protocol_parameters_pda_seed()),
|
||||
),
|
||||
AccountPostState::new_claimed(
|
||||
fee_post,
|
||||
Claim::Pda(compute_stability_fee_accumulator_pda_seed()),
|
||||
),
|
||||
AccountPostState::new_claimed(
|
||||
redemption_post,
|
||||
Claim::Pda(compute_redemption_price_state_pda_seed()),
|
||||
),
|
||||
AccountPostState::new(stablecoin_definition.account),
|
||||
AccountPostState::new(stablecoin_master_holding.account),
|
||||
AccountPostState::new(collateral_definition.account),
|
||||
AccountPostState::new(market_price_oracle.account),
|
||||
AccountPostState::new(clock.account),
|
||||
];
|
||||
|
||||
(post_states, vec![create_stablecoin_call])
|
||||
}
|
||||
|
||||
fn assert_uninitialized_pda(
|
||||
account: &AccountWithMetadata,
|
||||
expected_id: nssa_core::account::AccountId,
|
||||
label: &str,
|
||||
) {
|
||||
assert_eq!(
|
||||
account.account_id, expected_id,
|
||||
"{label} account ID does not match PDA"
|
||||
);
|
||||
assert_eq!(
|
||||
account.account,
|
||||
Account::default(),
|
||||
"{label} account must be uninitialized"
|
||||
);
|
||||
}
|
||||
@ -2,12 +2,26 @@
|
||||
|
||||
pub use stablecoin_core as core;
|
||||
|
||||
/// Roll the global stability-fee accumulator forward.
|
||||
pub mod accrue_stability_fee;
|
||||
|
||||
/// Mint stablecoin debt against an existing position.
|
||||
pub mod generate_debt;
|
||||
|
||||
/// Initialize protocol globals and the stablecoin token definition.
|
||||
pub mod initialize_program;
|
||||
|
||||
/// Open a new collateral-only position for a calling owner.
|
||||
pub mod open_position;
|
||||
|
||||
/// Repay outstanding stablecoin debt against an existing position.
|
||||
pub mod repay_debt;
|
||||
|
||||
/// Update the stability-fee rate after accruing at the old rate.
|
||||
pub mod set_stability_fee_per_millisecond;
|
||||
|
||||
mod shared;
|
||||
|
||||
/// Withdraw collateral from an existing position back to a user-controlled holding.
|
||||
pub mod withdraw_collateral;
|
||||
|
||||
|
||||
@ -5,16 +5,17 @@ use nssa_core::{
|
||||
use stablecoin_core::{verify_position_and_get_seed, verify_position_vault_and_get_seed, Position};
|
||||
use token_core::TokenHolding;
|
||||
|
||||
use crate::shared::{read_clock_timestamp, read_protocol_parameters};
|
||||
|
||||
/// Open a new collateral-only position for `owner`.
|
||||
///
|
||||
/// This claims the [`Position`] PDA, issues two chained token-program calls under the
|
||||
/// stablecoin's PDA authority, and stores `collateral_amount` with `debt_amount = 0`:
|
||||
/// stablecoin's PDA authority, and stores `initial_collateral_amount` with zero normalized debt:
|
||||
/// 1. `InitializeAccount` materializes the vault token holding for the collateral.
|
||||
/// 2. `Transfer` moves `collateral_amount` collateral tokens from the user's holding into the
|
||||
/// freshly initialized vault.
|
||||
/// 2. `Transfer` moves `initial_collateral_amount` collateral tokens from the user's holding into
|
||||
/// the freshly initialized vault.
|
||||
///
|
||||
/// `debt_amount` is deferred to a future `generate_debt` instruction and is intentionally
|
||||
/// not parameterized here.
|
||||
/// Stablecoin debt is created later by `generate_debt`; it is intentionally not parameterized here.
|
||||
///
|
||||
/// # Panics
|
||||
/// - `owner` or `user_holding` is not authorized.
|
||||
@ -23,16 +24,33 @@ use token_core::TokenHolding;
|
||||
/// - `user_holding` cannot be decoded as a [`TokenHolding`].
|
||||
/// - `user_holding`'s definition does not match `token_definition`.
|
||||
/// - `token_definition.program_owner` does not match `user_holding.program_owner`.
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "instruction surface passes explicit owner, position, vault, collateral, and protocol accounts"
|
||||
)]
|
||||
pub fn open_position(
|
||||
owner: AccountWithMetadata,
|
||||
position: AccountWithMetadata,
|
||||
vault: AccountWithMetadata,
|
||||
user_holding: AccountWithMetadata,
|
||||
token_definition: AccountWithMetadata,
|
||||
collateral_definition: AccountWithMetadata,
|
||||
protocol_parameters: AccountWithMetadata,
|
||||
clock: AccountWithMetadata,
|
||||
stablecoin_program_id: ProgramId,
|
||||
collateral_amount: u128,
|
||||
position_nonce: u64,
|
||||
initial_collateral_amount: u128,
|
||||
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||
assert!(owner.is_authorized, "Owner authorization is missing");
|
||||
let params = read_protocol_parameters(&protocol_parameters, stablecoin_program_id);
|
||||
assert!(
|
||||
!params.is_frozen,
|
||||
"Protocol is frozen; opening positions is disabled"
|
||||
);
|
||||
assert_eq!(
|
||||
collateral_definition.account_id, params.collateral_definition_id,
|
||||
"Collateral definition does not match protocol parameters"
|
||||
);
|
||||
|
||||
assert!(
|
||||
user_holding.is_authorized,
|
||||
"User collateral holding authorization is missing"
|
||||
@ -52,30 +70,29 @@ pub fn open_position(
|
||||
.expect("User holding must be a valid Token Holding")
|
||||
.definition_id();
|
||||
assert_eq!(
|
||||
user_holding_definition_id, token_definition.account_id,
|
||||
user_holding_definition_id, collateral_definition.account_id,
|
||||
"User collateral holding does not match the provided token definition"
|
||||
);
|
||||
let token_program_id = user_holding.account.program_owner;
|
||||
assert_eq!(
|
||||
token_definition.account.program_owner, token_program_id,
|
||||
collateral_definition.account.program_owner, token_program_id,
|
||||
"Collateral token definition is not owned by the user holding's Token Program"
|
||||
);
|
||||
|
||||
let position_seed = verify_position_and_get_seed(
|
||||
&position,
|
||||
&owner,
|
||||
token_definition.account_id,
|
||||
stablecoin_program_id,
|
||||
);
|
||||
let position_seed =
|
||||
verify_position_and_get_seed(&position, &owner, position_nonce, stablecoin_program_id);
|
||||
let vault_seed =
|
||||
verify_position_vault_and_get_seed(&vault, position.account_id, stablecoin_program_id);
|
||||
let now = read_clock_timestamp(&clock);
|
||||
|
||||
let mut position_post = position.account;
|
||||
position_post.data = Data::from(&Position {
|
||||
collateral_vault_id: vault.account_id,
|
||||
collateral_definition_id: token_definition.account_id,
|
||||
collateral_amount,
|
||||
debt_amount: 0,
|
||||
owner_account_id: owner.account_id,
|
||||
position_nonce,
|
||||
vault_account_id: vault.account_id,
|
||||
collateral_amount: initial_collateral_amount,
|
||||
normalized_debt_amount: 0,
|
||||
opened_at: now,
|
||||
});
|
||||
|
||||
let post_states = vec![
|
||||
@ -83,7 +100,9 @@ pub fn open_position(
|
||||
AccountPostState::new_claimed(position_post, Claim::Pda(position_seed)),
|
||||
AccountPostState::new(vault.account.clone()),
|
||||
AccountPostState::new(user_holding.account.clone()),
|
||||
AccountPostState::new(token_definition.account.clone()),
|
||||
AccountPostState::new(collateral_definition.account.clone()),
|
||||
AccountPostState::new(protocol_parameters.account),
|
||||
AccountPostState::new(clock.account.clone()),
|
||||
];
|
||||
|
||||
// Chained Token::InitializeAccount owns the vault as a Token holding. The Stablecoin
|
||||
@ -92,7 +111,7 @@ pub fn open_position(
|
||||
vault_authorized.is_authorized = true;
|
||||
let initialize_call = ChainedCall::new(
|
||||
token_program_id,
|
||||
vec![token_definition.clone(), vault_authorized],
|
||||
vec![collateral_definition.clone(), vault_authorized],
|
||||
&token_core::Instruction::InitializeAccount,
|
||||
)
|
||||
.with_pda_seeds(vec![vault_seed]);
|
||||
@ -105,7 +124,7 @@ pub fn open_position(
|
||||
program_owner: token_program_id,
|
||||
balance: 0,
|
||||
data: Data::from(&TokenHolding::Fungible {
|
||||
definition_id: token_definition.account_id,
|
||||
definition_id: collateral_definition.account_id,
|
||||
balance: 0,
|
||||
}),
|
||||
nonce: vault.account.nonce,
|
||||
@ -117,7 +136,7 @@ pub fn open_position(
|
||||
token_program_id,
|
||||
vec![user_holding, post_init_vault],
|
||||
&token_core::Instruction::Transfer {
|
||||
amount_to_transfer: collateral_amount,
|
||||
amount_to_transfer: initial_collateral_amount,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@ -5,42 +5,54 @@ use nssa_core::{
|
||||
use stablecoin_core::{verify_position_and_get_seed, Position};
|
||||
use token_core::TokenHolding;
|
||||
|
||||
use crate::shared::{
|
||||
read_clock_timestamp, read_protocol_parameters, read_stability_fee_accumulator,
|
||||
};
|
||||
|
||||
/// Repay `amount` of outstanding stablecoin debt against an existing position.
|
||||
///
|
||||
/// Burns `amount` stablecoins from `user_stablecoin_holding` via a chained
|
||||
/// `Token::Burn` and decreases `Position.debt_amount` by the same amount. The
|
||||
/// `Token::Burn` and decreases `Position.normalized_debt_amount` by the repayment's normalized
|
||||
/// equivalent. The
|
||||
/// position post-state uses plain [`AccountPostState::new`] — the PDA was
|
||||
/// already claimed at `open_position` time.
|
||||
///
|
||||
/// Until issue #97 (stability fee accrual) lands, the fee-accrual step is a
|
||||
/// no-op (every position structurally has `debt_amount = 0` today because
|
||||
/// `generate_debt` is unimplemented; "fees-accrued" is therefore vacuously
|
||||
/// true). A `// TODO(#97)` comment marks where the accrual code will plug in
|
||||
/// — right before the `checked_sub` below.
|
||||
///
|
||||
/// Until issue #91 (`generate_debt`) records the stablecoin definition into
|
||||
/// `Position`, this instruction cannot validate that `stablecoin_definition`
|
||||
/// is the correct one for the position's debt. The caller is trusted.
|
||||
///
|
||||
/// # Panics
|
||||
/// - `owner` is not authorized.
|
||||
/// - `position` is uninitialized, not owned by `stablecoin_program_id`, holds data that does not
|
||||
/// decode as a [`Position`], or sits at an address that does not match
|
||||
/// `compute_position_pda(stablecoin_program_id, owner, Position.collateral_definition_id)`.
|
||||
/// `compute_position_pda(stablecoin_program_id, owner, Position.position_nonce)`.
|
||||
/// - `user_stablecoin_holding` is not authorized, is uninitialized, is owned by a different Token
|
||||
/// Program than `stablecoin_definition`, or holds a [`TokenHolding`] whose `definition_id` does
|
||||
/// not match `stablecoin_definition.account_id`.
|
||||
/// - `stablecoin_definition` is uninitialized.
|
||||
/// - `amount > Position.debt_amount`.
|
||||
/// - `amount` is greater than the current nominal debt ceiling or normalizes to zero while nonzero.
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "instruction surface passes explicit position, token, fee, and protocol accounts"
|
||||
)]
|
||||
pub fn repay_debt(
|
||||
owner: AccountWithMetadata,
|
||||
position: AccountWithMetadata,
|
||||
stablecoin_definition: AccountWithMetadata,
|
||||
user_stablecoin_holding: AccountWithMetadata,
|
||||
stability_fee_accumulator: AccountWithMetadata,
|
||||
protocol_parameters: AccountWithMetadata,
|
||||
clock: AccountWithMetadata,
|
||||
stablecoin_program_id: ProgramId,
|
||||
amount: u128,
|
||||
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||
assert!(owner.is_authorized, "Owner authorization is missing");
|
||||
let params = read_protocol_parameters(&protocol_parameters, stablecoin_program_id);
|
||||
assert_eq!(
|
||||
stablecoin_definition.account_id, params.stablecoin_definition_id,
|
||||
"Stablecoin definition does not match protocol parameters"
|
||||
);
|
||||
let accumulator =
|
||||
read_stability_fee_accumulator(&stability_fee_accumulator, stablecoin_program_id);
|
||||
let now = read_clock_timestamp(&clock);
|
||||
let current_accumulator = stablecoin_core::current_accumulated_rate(&accumulator, ¶ms, now);
|
||||
|
||||
assert_ne!(
|
||||
position.account,
|
||||
Account::default(),
|
||||
@ -53,13 +65,17 @@ pub fn repay_debt(
|
||||
|
||||
let position_data = Position::try_from(&position.account.data)
|
||||
.expect("Position account must hold valid Position state");
|
||||
assert_eq!(
|
||||
position_data.owner_account_id, owner.account_id,
|
||||
"Position owner does not match signer"
|
||||
);
|
||||
// `verify_position_and_get_seed` asserts the position address matches the
|
||||
// (owner, collateral_definition) PDA derivation. The returned seed is
|
||||
// (owner, position_nonce) PDA derivation. The returned seed is
|
||||
// dropped — the position is already PDA-claimed.
|
||||
let _position_seed = verify_position_and_get_seed(
|
||||
&position,
|
||||
&owner,
|
||||
position_data.collateral_definition_id,
|
||||
position_data.position_nonce,
|
||||
stablecoin_program_id,
|
||||
);
|
||||
|
||||
@ -89,19 +105,37 @@ pub fn repay_debt(
|
||||
"Stablecoin holding does not match the provided stablecoin definition"
|
||||
);
|
||||
|
||||
// TODO(#97): accrue stability fees onto position_data.debt_amount here, before
|
||||
// the checked_sub below. Today every position has debt_amount = 0 (no
|
||||
// generate_debt yet), so the precondition is trivially met.
|
||||
let new_debt = position_data
|
||||
.debt_amount
|
||||
.checked_sub(amount)
|
||||
let maximum_repay_amount = stablecoin_core::mul_div_ceil(
|
||||
position_data.normalized_debt_amount,
|
||||
current_accumulator,
|
||||
stablecoin_core::FIXED_POINT_ONE,
|
||||
);
|
||||
assert!(
|
||||
amount <= maximum_repay_amount,
|
||||
"Repay amount exceeds outstanding debt"
|
||||
);
|
||||
|
||||
let normalized_delta = stablecoin_core::mul_div_floor(
|
||||
amount,
|
||||
stablecoin_core::FIXED_POINT_ONE,
|
||||
current_accumulator,
|
||||
);
|
||||
assert!(
|
||||
amount == 0 || normalized_delta != 0,
|
||||
"Repay amount is too small to reduce outstanding debt"
|
||||
);
|
||||
let new_normalized_debt = position_data
|
||||
.normalized_debt_amount
|
||||
.checked_sub(normalized_delta)
|
||||
.expect("Repay amount exceeds outstanding debt");
|
||||
|
||||
let updated_position = Position {
|
||||
collateral_vault_id: position_data.collateral_vault_id,
|
||||
collateral_definition_id: position_data.collateral_definition_id,
|
||||
owner_account_id: position_data.owner_account_id,
|
||||
position_nonce: position_data.position_nonce,
|
||||
vault_account_id: position_data.vault_account_id,
|
||||
collateral_amount: position_data.collateral_amount,
|
||||
debt_amount: new_debt,
|
||||
normalized_debt_amount: new_normalized_debt,
|
||||
opened_at: position_data.opened_at,
|
||||
};
|
||||
let mut position_post = position.account.clone();
|
||||
position_post.data = Data::from(&updated_position);
|
||||
@ -111,6 +145,9 @@ pub fn repay_debt(
|
||||
AccountPostState::new(position_post),
|
||||
AccountPostState::new(stablecoin_definition.account.clone()),
|
||||
AccountPostState::new(user_stablecoin_holding.account.clone()),
|
||||
AccountPostState::new(stability_fee_accumulator.account),
|
||||
AccountPostState::new(protocol_parameters.account),
|
||||
AccountPostState::new(clock.account),
|
||||
];
|
||||
|
||||
let token_program_id = user_stablecoin_holding.account.program_owner;
|
||||
|
||||
52
programs/stablecoin/src/set_stability_fee_per_millisecond.rs
Normal file
52
programs/stablecoin/src/set_stability_fee_per_millisecond.rs
Normal file
@ -0,0 +1,52 @@
|
||||
use nssa_core::{
|
||||
account::{AccountWithMetadata, Data},
|
||||
program::{AccountPostState, ChainedCall, ProgramId},
|
||||
};
|
||||
use stablecoin_core::assert_valid_stability_fee_per_millisecond;
|
||||
|
||||
use crate::shared::{
|
||||
accrue_stability_fee_state, read_clock_timestamp, read_protocol_parameters,
|
||||
read_stability_fee_accumulator,
|
||||
};
|
||||
|
||||
/// Updates the protocol stability-fee rate after accruing at the old rate.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if `admin` is not the configured admin or `new_rate` is out of bounds.
|
||||
pub fn set_stability_fee_per_millisecond(
|
||||
admin: AccountWithMetadata,
|
||||
protocol_parameters: AccountWithMetadata,
|
||||
stability_fee_accumulator: AccountWithMetadata,
|
||||
clock: AccountWithMetadata,
|
||||
stablecoin_program_id: ProgramId,
|
||||
new_rate: u128,
|
||||
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||
assert!(admin.is_authorized, "Admin authorization is missing");
|
||||
assert_valid_stability_fee_per_millisecond(new_rate);
|
||||
|
||||
let mut params = read_protocol_parameters(&protocol_parameters, stablecoin_program_id);
|
||||
assert_eq!(
|
||||
admin.account_id, params.admin_account_id,
|
||||
"Admin account does not match protocol admin"
|
||||
);
|
||||
|
||||
let accumulator =
|
||||
read_stability_fee_accumulator(&stability_fee_accumulator, stablecoin_program_id);
|
||||
let now = read_clock_timestamp(&clock);
|
||||
let updated_accumulator = accrue_stability_fee_state(&accumulator, ¶ms, now);
|
||||
params.stability_fee_per_millisecond = new_rate;
|
||||
|
||||
let mut protocol_post = protocol_parameters.account.clone();
|
||||
protocol_post.data = Data::from(¶ms);
|
||||
let mut accumulator_post = stability_fee_accumulator.account.clone();
|
||||
accumulator_post.data = Data::from(&updated_accumulator);
|
||||
|
||||
let post_states = vec![
|
||||
AccountPostState::new(admin.account),
|
||||
AccountPostState::new(protocol_post),
|
||||
AccountPostState::new(accumulator_post),
|
||||
AccountPostState::new(clock.account),
|
||||
];
|
||||
|
||||
(post_states, vec![])
|
||||
}
|
||||
107
programs/stablecoin/src/shared.rs
Normal file
107
programs/stablecoin/src/shared.rs
Normal file
@ -0,0 +1,107 @@
|
||||
use borsh::BorshDeserialize as _;
|
||||
use clock_core::{ClockAccountData, CLOCK_01_PROGRAM_ACCOUNT_ID};
|
||||
use nssa_core::{
|
||||
account::{Account, AccountWithMetadata},
|
||||
program::ProgramId,
|
||||
};
|
||||
use stablecoin_core::{
|
||||
compute_protocol_parameters_pda, compute_redemption_price_state_pda,
|
||||
compute_stability_fee_accumulator_pda, current_accumulated_rate, ProtocolParameters,
|
||||
RedemptionPriceState, StabilityFeeAccumulator,
|
||||
};
|
||||
|
||||
pub(crate) fn read_clock_timestamp(clock: &AccountWithMetadata) -> u64 {
|
||||
assert_eq!(
|
||||
clock.account_id, CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
"Clock account must be the canonical 1-block LEZ clock account"
|
||||
);
|
||||
assert_ne!(
|
||||
clock.account,
|
||||
Account::default(),
|
||||
"Clock account must be initialized"
|
||||
);
|
||||
ClockAccountData::try_from_slice(clock.account.data.as_ref())
|
||||
.expect("Clock account must hold valid ClockAccountData")
|
||||
.timestamp
|
||||
}
|
||||
|
||||
pub(crate) fn read_protocol_parameters(
|
||||
protocol_parameters: &AccountWithMetadata,
|
||||
stablecoin_program_id: ProgramId,
|
||||
) -> ProtocolParameters {
|
||||
assert_eq!(
|
||||
protocol_parameters.account_id,
|
||||
compute_protocol_parameters_pda(stablecoin_program_id),
|
||||
"Protocol parameters account ID does not match PDA"
|
||||
);
|
||||
assert_initialized_owned(
|
||||
protocol_parameters,
|
||||
stablecoin_program_id,
|
||||
"Protocol parameters",
|
||||
);
|
||||
ProtocolParameters::try_from(&protocol_parameters.account.data)
|
||||
.expect("Protocol parameters account must hold valid ProtocolParameters state")
|
||||
}
|
||||
|
||||
pub(crate) fn read_stability_fee_accumulator(
|
||||
stability_fee_accumulator: &AccountWithMetadata,
|
||||
stablecoin_program_id: ProgramId,
|
||||
) -> StabilityFeeAccumulator {
|
||||
assert_eq!(
|
||||
stability_fee_accumulator.account_id,
|
||||
compute_stability_fee_accumulator_pda(stablecoin_program_id),
|
||||
"Stability fee accumulator account ID does not match PDA"
|
||||
);
|
||||
assert_initialized_owned(
|
||||
stability_fee_accumulator,
|
||||
stablecoin_program_id,
|
||||
"Stability fee accumulator",
|
||||
);
|
||||
StabilityFeeAccumulator::try_from(&stability_fee_accumulator.account.data)
|
||||
.expect("Stability fee accumulator account must hold valid state")
|
||||
}
|
||||
|
||||
pub(crate) fn read_redemption_price_state(
|
||||
redemption_price_state: &AccountWithMetadata,
|
||||
stablecoin_program_id: ProgramId,
|
||||
) -> RedemptionPriceState {
|
||||
assert_eq!(
|
||||
redemption_price_state.account_id,
|
||||
compute_redemption_price_state_pda(stablecoin_program_id),
|
||||
"Redemption price state account ID does not match PDA"
|
||||
);
|
||||
assert_initialized_owned(
|
||||
redemption_price_state,
|
||||
stablecoin_program_id,
|
||||
"Redemption price state",
|
||||
);
|
||||
RedemptionPriceState::try_from(&redemption_price_state.account.data)
|
||||
.expect("Redemption price state account must hold valid state")
|
||||
}
|
||||
|
||||
pub(crate) fn accrue_stability_fee_state(
|
||||
accumulator: &StabilityFeeAccumulator,
|
||||
params: &ProtocolParameters,
|
||||
now: u64,
|
||||
) -> StabilityFeeAccumulator {
|
||||
StabilityFeeAccumulator {
|
||||
accumulated_rate_at_last_accrual: current_accumulated_rate(accumulator, params, now),
|
||||
last_accrued_at: now,
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_initialized_owned(
|
||||
account: &AccountWithMetadata,
|
||||
stablecoin_program_id: ProgramId,
|
||||
label: &str,
|
||||
) {
|
||||
assert_ne!(
|
||||
account.account,
|
||||
Account::default(),
|
||||
"{label} account must be initialized"
|
||||
);
|
||||
assert_eq!(
|
||||
account.account.program_owner, stablecoin_program_id,
|
||||
"{label} account is not owned by this stablecoin program"
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -5,6 +5,11 @@ use nssa_core::{
|
||||
use stablecoin_core::{verify_position_and_get_seed, verify_position_vault_and_get_seed, Position};
|
||||
use token_core::TokenHolding;
|
||||
|
||||
use crate::shared::{
|
||||
read_clock_timestamp, read_protocol_parameters, read_redemption_price_state,
|
||||
read_stability_fee_accumulator,
|
||||
};
|
||||
|
||||
/// Withdraw `amount` collateral tokens from `position`'s vault back to `destination`.
|
||||
///
|
||||
/// Decreases `Position.collateral_amount` by `amount` and emits a single chained
|
||||
@ -13,32 +18,49 @@ use token_core::TokenHolding;
|
||||
/// the initial PDA claim already happened in
|
||||
/// [`crate::open_position::open_position`].
|
||||
///
|
||||
/// Until issues #95 / #96 / #97 land (redemption price, price feed, stability
|
||||
/// fee accrual), this instruction hard-asserts `Position.debt_amount == 0`.
|
||||
/// When those land, this guard is replaced by real fee accrual + a
|
||||
/// collateralization-ratio check against the post-withdrawal collateral.
|
||||
///
|
||||
/// # Panics
|
||||
/// - `owner` is not authorized.
|
||||
/// - `position` is uninitialized, not owned by `stablecoin_program_id`, holds data that does not
|
||||
/// decode as a [`Position`], or sits at an address that does not match
|
||||
/// `compute_position_pda(stablecoin_program_id, owner, Position.collateral_definition_id)`.
|
||||
/// `compute_position_pda(stablecoin_program_id, owner, Position.position_nonce)`.
|
||||
/// - `vault` sits at an address that does not match
|
||||
/// `compute_position_vault_pda(stablecoin_program_id, position_id)`, or holds a [`TokenHolding`]
|
||||
/// whose `definition_id` does not match the position's collateral definition.
|
||||
/// whose `definition_id` does not match the protocol collateral definition.
|
||||
/// - `destination` is uninitialized, owned by a different Token Program than the vault, or holds a
|
||||
/// [`TokenHolding`] whose `definition_id` does not match the position's collateral definition.
|
||||
/// - `Position.debt_amount` is non-zero.
|
||||
/// [`TokenHolding`] whose `definition_id` does not match the protocol collateral definition.
|
||||
/// - `amount > Position.collateral_amount`.
|
||||
/// - the post-withdrawal position would be undercollateralized.
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "instruction surface passes explicit position, vault, fee, redemption, and protocol accounts"
|
||||
)]
|
||||
pub fn withdraw_collateral(
|
||||
owner: AccountWithMetadata,
|
||||
position: AccountWithMetadata,
|
||||
vault: AccountWithMetadata,
|
||||
destination: AccountWithMetadata,
|
||||
stability_fee_accumulator: AccountWithMetadata,
|
||||
redemption_price_state: AccountWithMetadata,
|
||||
protocol_parameters: AccountWithMetadata,
|
||||
clock: AccountWithMetadata,
|
||||
stablecoin_program_id: ProgramId,
|
||||
amount: u128,
|
||||
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||
assert!(owner.is_authorized, "Owner authorization is missing");
|
||||
let params = read_protocol_parameters(&protocol_parameters, stablecoin_program_id);
|
||||
assert!(
|
||||
!params.is_frozen,
|
||||
"Protocol is frozen; collateral withdrawal is disabled"
|
||||
);
|
||||
let accumulator =
|
||||
read_stability_fee_accumulator(&stability_fee_accumulator, stablecoin_program_id);
|
||||
let redemption_state =
|
||||
read_redemption_price_state(&redemption_price_state, stablecoin_program_id);
|
||||
let now = read_clock_timestamp(&clock);
|
||||
let current_accumulator = stablecoin_core::current_accumulated_rate(&accumulator, ¶ms, now);
|
||||
let current_redemption_price =
|
||||
stablecoin_core::current_redemption_price(&redemption_state, now);
|
||||
|
||||
assert_ne!(
|
||||
position.account,
|
||||
Account::default(),
|
||||
@ -51,15 +73,23 @@ pub fn withdraw_collateral(
|
||||
|
||||
let position_data = Position::try_from(&position.account.data)
|
||||
.expect("Position account must hold valid Position state");
|
||||
assert_eq!(
|
||||
position_data.owner_account_id, owner.account_id,
|
||||
"Position owner does not match signer"
|
||||
);
|
||||
// `verify_position_and_get_seed` asserts the position address matches the
|
||||
// (owner, collateral_definition) PDA derivation. We do not use the seed
|
||||
// (owner, position_nonce) PDA derivation. We do not use the seed
|
||||
// downstream — the position is already PDA-claimed.
|
||||
let _position_seed = verify_position_and_get_seed(
|
||||
&position,
|
||||
&owner,
|
||||
position_data.collateral_definition_id,
|
||||
position_data.position_nonce,
|
||||
stablecoin_program_id,
|
||||
);
|
||||
assert_eq!(
|
||||
vault.account_id, position_data.vault_account_id,
|
||||
"Vault account does not match position vault"
|
||||
);
|
||||
let vault_seed =
|
||||
verify_position_vault_and_get_seed(&vault, position.account_id, stablecoin_program_id);
|
||||
|
||||
@ -67,8 +97,8 @@ pub fn withdraw_collateral(
|
||||
.expect("Vault account must hold a valid TokenHolding");
|
||||
assert_eq!(
|
||||
vault_holding.definition_id(),
|
||||
position_data.collateral_definition_id,
|
||||
"Vault token holding is not for the position's collateral definition"
|
||||
params.collateral_definition_id,
|
||||
"Vault token holding does not match protocol collateral definition"
|
||||
);
|
||||
|
||||
let token_program_id = vault.account.program_owner;
|
||||
@ -85,24 +115,32 @@ pub fn withdraw_collateral(
|
||||
.expect("Destination account must hold a valid TokenHolding");
|
||||
assert_eq!(
|
||||
destination_holding.definition_id(),
|
||||
position_data.collateral_definition_id,
|
||||
"Destination token definition does not match the position's collateral definition"
|
||||
params.collateral_definition_id,
|
||||
"Destination token definition does not match protocol collateral definition"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
position_data.debt_amount, 0,
|
||||
"withdraw_collateral with debt is not supported yet — stability fee accrual and collateralization check land with #97/#96"
|
||||
);
|
||||
let new_collateral = position_data
|
||||
.collateral_amount
|
||||
.checked_sub(amount)
|
||||
.expect("Withdrawal amount exceeds position collateral");
|
||||
assert!(
|
||||
stablecoin_core::is_collateralized(
|
||||
new_collateral,
|
||||
position_data.normalized_debt_amount,
|
||||
current_accumulator,
|
||||
current_redemption_price,
|
||||
params.minimum_collateralization_ratio,
|
||||
),
|
||||
"Position would be undercollateralized after withdrawal"
|
||||
);
|
||||
|
||||
let updated_position = Position {
|
||||
collateral_vault_id: position_data.collateral_vault_id,
|
||||
collateral_definition_id: position_data.collateral_definition_id,
|
||||
owner_account_id: position_data.owner_account_id,
|
||||
position_nonce: position_data.position_nonce,
|
||||
vault_account_id: position_data.vault_account_id,
|
||||
collateral_amount: new_collateral,
|
||||
debt_amount: position_data.debt_amount,
|
||||
normalized_debt_amount: position_data.normalized_debt_amount,
|
||||
opened_at: position_data.opened_at,
|
||||
};
|
||||
let mut position_post = position.account.clone();
|
||||
position_post.data = Data::from(&updated_position);
|
||||
@ -112,6 +150,10 @@ pub fn withdraw_collateral(
|
||||
AccountPostState::new(position_post),
|
||||
AccountPostState::new(vault.account.clone()),
|
||||
AccountPostState::new(destination.account.clone()),
|
||||
AccountPostState::new(stability_fee_accumulator.account),
|
||||
AccountPostState::new(redemption_price_state.account),
|
||||
AccountPostState::new(protocol_parameters.account),
|
||||
AccountPostState::new(clock.account.clone()),
|
||||
];
|
||||
|
||||
let mut vault_authorized = vault.clone();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user