mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-06-10 02:09:49 +00:00
feat(stablecoin): implement repay_debt (#93)
This commit is contained in:
parent
1ae2b325ff
commit
cdb53a4d0c
@ -77,6 +77,41 @@
|
||||
"type": "u128"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "repay_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
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "amount",
|
||||
"type": "u128"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"accounts": [
|
||||
|
||||
@ -19,6 +19,10 @@ impl Keys {
|
||||
fn user_holding() -> PrivateKey {
|
||||
PrivateKey::try_new([42; 32]).expect("valid private key")
|
||||
}
|
||||
|
||||
fn user_stablecoin_holding() -> PrivateKey {
|
||||
PrivateKey::try_new([43; 32]).expect("valid private key")
|
||||
}
|
||||
}
|
||||
|
||||
impl Ids {
|
||||
@ -42,6 +46,16 @@ impl Ids {
|
||||
AccountId::from(&PublicKey::new_from_private_key(&Keys::user_holding()))
|
||||
}
|
||||
|
||||
fn stablecoin_definition() -> AccountId {
|
||||
AccountId::new([6; 32])
|
||||
}
|
||||
|
||||
fn user_stablecoin_holding() -> AccountId {
|
||||
AccountId::from(&PublicKey::new_from_private_key(
|
||||
&Keys::user_stablecoin_holding(),
|
||||
))
|
||||
}
|
||||
|
||||
fn position() -> AccountId {
|
||||
compute_position_pda(
|
||||
Self::stablecoin_program(),
|
||||
@ -67,6 +81,22 @@ impl Balances {
|
||||
fn collateral_withdraw() -> u128 {
|
||||
200_000
|
||||
}
|
||||
|
||||
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 {
|
||||
100
|
||||
}
|
||||
}
|
||||
|
||||
impl Accounts {
|
||||
@ -94,6 +124,45 @@ impl Accounts {
|
||||
nonce: Nonce(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn stablecoin_definition_init() -> 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,
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn user_stablecoin_holding_init() -> Account {
|
||||
Account {
|
||||
program_owner: Ids::token_program(),
|
||||
balance: 0_u128,
|
||||
data: Data::from(&TokenHolding::Fungible {
|
||||
definition_id: Ids::stablecoin_definition(),
|
||||
balance: Balances::user_stablecoin_holding_init(),
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn position_with_debt_init() -> 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(),
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn deploy_programs(state: &mut V03State) {
|
||||
@ -129,13 +198,35 @@ 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_with_genesis_accounts(&[], vec![], 0);
|
||||
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");
|
||||
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());
|
||||
assert_eq!(
|
||||
position.collateral_definition_id,
|
||||
Ids::collateral_definition()
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_fungible_balance(state: &V03State, account_id: AccountId, expected_balance: u128) {
|
||||
@ -212,8 +303,7 @@ fn stablecoin_open_position_then_withdraw_collateral() {
|
||||
withdraw,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set =
|
||||
public_transaction::WitnessSet::for_message(&message, &[&Keys::owner()]);
|
||||
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)
|
||||
@ -235,3 +325,74 @@ fn stablecoin_open_position_then_withdraw_collateral() {
|
||||
+ 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(),
|
||||
};
|
||||
let message = public_transaction::Message::try_new(
|
||||
Ids::stablecoin_program(),
|
||||
vec![
|
||||
Ids::owner(),
|
||||
Ids::position(),
|
||||
Ids::stablecoin_definition(),
|
||||
Ids::user_stablecoin_holding(),
|
||||
],
|
||||
vec![
|
||||
current_nonce(&state, Ids::owner()),
|
||||
current_nonce(&state, Ids::user_stablecoin_holding()),
|
||||
],
|
||||
repay,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(
|
||||
&message,
|
||||
&[&Keys::owner(), &Keys::user_stablecoin_holding()],
|
||||
);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
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());
|
||||
|
||||
// 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"),
|
||||
}
|
||||
|
||||
// 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()
|
||||
);
|
||||
}
|
||||
TokenHolding::NftMaster { .. } | TokenHolding::NftPrintedCopy { .. } => {
|
||||
panic!("expected Fungible holding")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,6 +50,32 @@ pub enum Instruction {
|
||||
/// 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: u128,
|
||||
},
|
||||
}
|
||||
|
||||
/// Persistent state held by a Stablecoin [`Position`] account.
|
||||
|
||||
@ -73,4 +73,33 @@ mod stablecoin {
|
||||
chained_calls,
|
||||
))
|
||||
}
|
||||
|
||||
/// 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).
|
||||
#[instruction]
|
||||
pub fn repay_debt(
|
||||
ctx: ProgramContext,
|
||||
owner: AccountWithMetadata,
|
||||
position: AccountWithMetadata,
|
||||
stablecoin_definition: AccountWithMetadata,
|
||||
user_stablecoin_holding: AccountWithMetadata,
|
||||
amount: u128,
|
||||
) -> SpelResult {
|
||||
let (post_states, chained_calls) = stablecoin_program::repay_debt::repay_debt(
|
||||
owner,
|
||||
position,
|
||||
stablecoin_definition,
|
||||
user_stablecoin_holding,
|
||||
ctx.self_program_id,
|
||||
amount,
|
||||
);
|
||||
Ok(spel_framework::SpelOutput::execute(
|
||||
post_states,
|
||||
chained_calls,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,9 @@ pub use stablecoin_core as core;
|
||||
/// 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;
|
||||
|
||||
/// Withdraw collateral from an existing position back to a user-controlled holding.
|
||||
pub mod withdraw_collateral;
|
||||
|
||||
|
||||
126
stablecoin/src/repay_debt.rs
Normal file
126
stablecoin/src/repay_debt.rs
Normal file
@ -0,0 +1,126 @@
|
||||
use nssa_core::{
|
||||
account::{Account, AccountWithMetadata, Data},
|
||||
program::{AccountPostState, ChainedCall, ProgramId},
|
||||
};
|
||||
use stablecoin_core::{verify_position_and_get_seed, Position};
|
||||
use token_core::TokenHolding;
|
||||
|
||||
/// 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
|
||||
/// 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)`.
|
||||
/// - `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`.
|
||||
pub fn repay_debt(
|
||||
owner: AccountWithMetadata,
|
||||
position: AccountWithMetadata,
|
||||
stablecoin_definition: AccountWithMetadata,
|
||||
user_stablecoin_holding: AccountWithMetadata,
|
||||
stablecoin_program_id: ProgramId,
|
||||
amount: u128,
|
||||
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||
assert!(owner.is_authorized, "Owner authorization is missing");
|
||||
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");
|
||||
// `verify_position_and_get_seed` asserts the position address matches the
|
||||
// (owner, collateral_definition) 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,
|
||||
stablecoin_program_id,
|
||||
);
|
||||
|
||||
assert!(
|
||||
user_stablecoin_holding.is_authorized,
|
||||
"User stablecoin holding authorization is missing"
|
||||
);
|
||||
assert_ne!(
|
||||
user_stablecoin_holding.account,
|
||||
Account::default(),
|
||||
"User stablecoin holding must be initialized"
|
||||
);
|
||||
assert_ne!(
|
||||
stablecoin_definition.account,
|
||||
Account::default(),
|
||||
"Stablecoin definition account 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_data = TokenHolding::try_from(&user_stablecoin_holding.account.data)
|
||||
.expect("User stablecoin holding must hold a valid TokenHolding");
|
||||
assert_eq!(
|
||||
user_holding_data.definition_id(),
|
||||
stablecoin_definition.account_id,
|
||||
"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)
|
||||
.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,
|
||||
collateral_amount: position_data.collateral_amount,
|
||||
debt_amount: new_debt,
|
||||
};
|
||||
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()),
|
||||
];
|
||||
|
||||
let token_program_id = user_stablecoin_holding.account.program_owner;
|
||||
let burn_call = ChainedCall::new(
|
||||
token_program_id,
|
||||
vec![stablecoin_definition, user_stablecoin_holding],
|
||||
&token_core::Instruction::Burn {
|
||||
amount_to_burn: amount,
|
||||
},
|
||||
);
|
||||
|
||||
(post_states, vec![burn_call])
|
||||
}
|
||||
@ -139,6 +139,41 @@ fn destination_holding_account() -> AccountWithMetadata {
|
||||
token_holding_account(destination_holding_id(), collateral_definition_id(), 0)
|
||||
}
|
||||
|
||||
fn stablecoin_definition_id() -> AccountId {
|
||||
AccountId::new([0x50u8; 32])
|
||||
}
|
||||
|
||||
fn user_stablecoin_holding_id() -> AccountId {
|
||||
AccountId::new([0x60u8; 32])
|
||||
}
|
||||
|
||||
fn stablecoin_definition_account() -> AccountWithMetadata {
|
||||
AccountWithMetadata {
|
||||
account: Account {
|
||||
program_owner: TOKEN_PROGRAM_ID,
|
||||
balance: 0,
|
||||
data: Data::from(&TokenDefinition::Fungible {
|
||||
name: "DAI".to_owned(),
|
||||
total_supply: 1_000_000,
|
||||
metadata_id: None,
|
||||
}),
|
||||
nonce: Nonce(0),
|
||||
},
|
||||
is_authorized: false,
|
||||
account_id: stablecoin_definition_id(),
|
||||
}
|
||||
}
|
||||
|
||||
fn user_stablecoin_holding_account(balance: u128) -> AccountWithMetadata {
|
||||
let mut account = token_holding_account(
|
||||
user_stablecoin_holding_id(),
|
||||
stablecoin_definition_id(),
|
||||
balance,
|
||||
);
|
||||
account.is_authorized = true;
|
||||
account
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_position_claims_pda_and_emits_chained_calls() {
|
||||
let collateral_amount: u128 = 500;
|
||||
@ -698,3 +733,241 @@ fn withdraw_collateral_rejects_overdraw() {
|
||||
200,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repay_debt_decreases_debt_and_emits_burn() {
|
||||
let initial_collateral: u128 = 500;
|
||||
let initial_debt: u128 = 300;
|
||||
let amount: u128 = 100;
|
||||
let holding_balance: u128 = 1_000;
|
||||
|
||||
let (post_states, chained_calls) = crate::repay_debt::repay_debt(
|
||||
owner_account(),
|
||||
init_position_account(initial_collateral, initial_debt),
|
||||
stablecoin_definition_account(),
|
||||
user_stablecoin_holding_account(holding_balance),
|
||||
STABLECOIN_PROGRAM_ID,
|
||||
amount,
|
||||
);
|
||||
|
||||
assert_eq!(post_states.len(), 4);
|
||||
|
||||
// Position post-state: plain `new`, holds the decremented Position.
|
||||
let position_post = &post_states[1];
|
||||
assert_eq!(position_post.required_claim(), None);
|
||||
let position = Position::try_from(&position_post.account().data).expect("valid Position");
|
||||
assert_eq!(
|
||||
position,
|
||||
Position {
|
||||
collateral_vault_id: vault_id(),
|
||||
collateral_definition_id: collateral_definition_id(),
|
||||
collateral_amount: initial_collateral,
|
||||
debt_amount: initial_debt - amount,
|
||||
}
|
||||
);
|
||||
assert_eq!(position_post.account().program_owner, STABLECOIN_PROGRAM_ID);
|
||||
|
||||
// Stablecoin definition and user holding post-states are pre-burn.
|
||||
assert_eq!(
|
||||
post_states[2].account(),
|
||||
&stablecoin_definition_account().account
|
||||
);
|
||||
assert_eq!(
|
||||
post_states[3].account(),
|
||||
&user_stablecoin_holding_account(holding_balance).account
|
||||
);
|
||||
|
||||
// Single chained Token::Burn, no PDA seeds (user-authorized burn source).
|
||||
assert_eq!(chained_calls.len(), 1);
|
||||
let expected_burn = ChainedCall::new(
|
||||
TOKEN_PROGRAM_ID,
|
||||
vec![
|
||||
stablecoin_definition_account(),
|
||||
user_stablecoin_holding_account(holding_balance),
|
||||
],
|
||||
&token_core::Instruction::Burn {
|
||||
amount_to_burn: amount,
|
||||
},
|
||||
);
|
||||
assert_eq!(chained_calls[0], expected_burn);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repay_debt_allows_full_repayment() {
|
||||
let debt: u128 = 300;
|
||||
let (post_states, _chained_calls) = crate::repay_debt::repay_debt(
|
||||
owner_account(),
|
||||
init_position_account(500, debt),
|
||||
stablecoin_definition_account(),
|
||||
user_stablecoin_holding_account(1_000),
|
||||
STABLECOIN_PROGRAM_ID,
|
||||
debt,
|
||||
);
|
||||
let position = Position::try_from(&post_states[1].account().data).expect("valid Position");
|
||||
assert_eq!(position.debt_amount, 0);
|
||||
assert_eq!(position.collateral_amount, 500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repay_debt_allows_zero_amount() {
|
||||
let initial_debt: u128 = 300;
|
||||
let (post_states, chained_calls) = crate::repay_debt::repay_debt(
|
||||
owner_account(),
|
||||
init_position_account(500, initial_debt),
|
||||
stablecoin_definition_account(),
|
||||
user_stablecoin_holding_account(1_000),
|
||||
STABLECOIN_PROGRAM_ID,
|
||||
0,
|
||||
);
|
||||
let position = Position::try_from(&post_states[1].account().data).expect("valid Position");
|
||||
assert_eq!(position.debt_amount, initial_debt);
|
||||
|
||||
let expected_burn = ChainedCall::new(
|
||||
TOKEN_PROGRAM_ID,
|
||||
vec![
|
||||
stablecoin_definition_account(),
|
||||
user_stablecoin_holding_account(1_000),
|
||||
],
|
||||
&token_core::Instruction::Burn { amount_to_burn: 0 },
|
||||
);
|
||||
assert_eq!(chained_calls, vec![expected_burn]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Owner authorization is missing")]
|
||||
fn repay_debt_requires_owner_authorization() {
|
||||
let mut owner = owner_account();
|
||||
owner.is_authorized = false;
|
||||
crate::repay_debt::repay_debt(
|
||||
owner,
|
||||
init_position_account(500, 300),
|
||||
stablecoin_definition_account(),
|
||||
user_stablecoin_holding_account(1_000),
|
||||
STABLECOIN_PROGRAM_ID,
|
||||
100,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Position account must be initialized")]
|
||||
fn repay_debt_rejects_uninitialized_position() {
|
||||
crate::repay_debt::repay_debt(
|
||||
owner_account(),
|
||||
uninit_position_account(),
|
||||
stablecoin_definition_account(),
|
||||
user_stablecoin_holding_account(1_000),
|
||||
STABLECOIN_PROGRAM_ID,
|
||||
100,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Position is not owned by this stablecoin program")]
|
||||
fn repay_debt_rejects_position_owned_by_other_program() {
|
||||
let mut position = init_position_account(500, 300);
|
||||
position.account.program_owner = [9u32; 8];
|
||||
crate::repay_debt::repay_debt(
|
||||
owner_account(),
|
||||
position,
|
||||
stablecoin_definition_account(),
|
||||
user_stablecoin_holding_account(1_000),
|
||||
STABLECOIN_PROGRAM_ID,
|
||||
100,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Position account ID does not match expected derivation")]
|
||||
fn repay_debt_rejects_wrong_position_address() {
|
||||
let mut position = init_position_account(500, 300);
|
||||
position.account_id = AccountId::new([0xFFu8; 32]);
|
||||
crate::repay_debt::repay_debt(
|
||||
owner_account(),
|
||||
position,
|
||||
stablecoin_definition_account(),
|
||||
user_stablecoin_holding_account(1_000),
|
||||
STABLECOIN_PROGRAM_ID,
|
||||
100,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "User stablecoin holding authorization is missing")]
|
||||
fn repay_debt_requires_user_holding_authorization() {
|
||||
let mut holding = user_stablecoin_holding_account(1_000);
|
||||
holding.is_authorized = false;
|
||||
crate::repay_debt::repay_debt(
|
||||
owner_account(),
|
||||
init_position_account(500, 300),
|
||||
stablecoin_definition_account(),
|
||||
holding,
|
||||
STABLECOIN_PROGRAM_ID,
|
||||
100,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "User stablecoin holding must be initialized")]
|
||||
fn repay_debt_rejects_uninitialized_user_holding() {
|
||||
let holding = AccountWithMetadata {
|
||||
account: Account::default(),
|
||||
is_authorized: true,
|
||||
account_id: user_stablecoin_holding_id(),
|
||||
};
|
||||
crate::repay_debt::repay_debt(
|
||||
owner_account(),
|
||||
init_position_account(500, 300),
|
||||
stablecoin_definition_account(),
|
||||
holding,
|
||||
STABLECOIN_PROGRAM_ID,
|
||||
100,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(
|
||||
expected = "Stablecoin holding and definition must be owned by the same Token Program"
|
||||
)]
|
||||
fn repay_debt_rejects_holding_with_different_token_program() {
|
||||
let mut holding = user_stablecoin_holding_account(1_000);
|
||||
holding.account.program_owner = [9u32; 8];
|
||||
crate::repay_debt::repay_debt(
|
||||
owner_account(),
|
||||
init_position_account(500, 300),
|
||||
stablecoin_definition_account(),
|
||||
holding,
|
||||
STABLECOIN_PROGRAM_ID,
|
||||
100,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Stablecoin holding does not match the provided stablecoin definition")]
|
||||
fn repay_debt_rejects_holding_for_other_definition() {
|
||||
let mut holding = user_stablecoin_holding_account(1_000);
|
||||
holding.account.data = Data::from(&TokenHolding::Fungible {
|
||||
definition_id: AccountId::new([0x21u8; 32]),
|
||||
balance: 1_000,
|
||||
});
|
||||
crate::repay_debt::repay_debt(
|
||||
owner_account(),
|
||||
init_position_account(500, 300),
|
||||
stablecoin_definition_account(),
|
||||
holding,
|
||||
STABLECOIN_PROGRAM_ID,
|
||||
100,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Repay amount exceeds outstanding debt")]
|
||||
fn repay_debt_rejects_overrepay() {
|
||||
crate::repay_debt::repay_debt(
|
||||
owner_account(),
|
||||
init_position_account(500, 100),
|
||||
stablecoin_definition_account(),
|
||||
user_stablecoin_holding_account(1_000),
|
||||
STABLECOIN_PROGRAM_ID,
|
||||
200,
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user