feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
//! Core data structures and utilities for the Stablecoin Program.
|
|
|
|
|
|
|
|
|
|
use borsh::{BorshDeserialize, BorshSerialize};
|
|
|
|
|
use nssa_core::{
|
|
|
|
|
account::{AccountId, AccountWithMetadata, Data},
|
|
|
|
|
program::{PdaSeed, ProgramId},
|
|
|
|
|
};
|
2026-05-11 14:51:50 +02:00
|
|
|
use serde::{Deserialize, Serialize};
|
2026-05-12 13:04:59 -03:00
|
|
|
use spel_framework_macros::account_type;
|
2026-05-11 14:51:50 +02:00
|
|
|
|
2026-05-12 13:04:59 -03:00
|
|
|
const POSITION_PDA_DOMAIN: [u8; 32] = [0; 32];
|
|
|
|
|
const POSITION_VAULT_PDA_DOMAIN: [u8; 32] = [1; 32];
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
|
|
|
|
|
/// Stablecoin Program Instruction.
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
2026-05-11 14:51:50 +02:00
|
|
|
pub enum Instruction {
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
/// Open a new collateral-only [`Position`] for the calling owner.
|
|
|
|
|
///
|
2026-05-11 17:32:47 -03:00
|
|
|
/// Required accounts (5):
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
/// - Owner account (authorized)
|
|
|
|
|
/// - Position account (uninitialized, address must match
|
2026-05-12 13:04:59 -03:00
|
|
|
/// `compute_position_pda(self_program_id, owner, token_definition)`)
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
/// - Position vault token holding account (uninitialized, address must match
|
2026-05-12 13:04:59 -03:00
|
|
|
/// `compute_position_vault_pda(self_program_id, position_id)`)
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
/// - Owner's source token holding for the collateral (authorized, initialized)
|
2026-05-11 18:03:13 -03:00
|
|
|
/// - 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)
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
OpenPosition {
|
|
|
|
|
/// Amount of collateral tokens to deposit into the position vault.
|
|
|
|
|
collateral_amount: u128,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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`.
|
2026-05-12 13:04:59 -03:00
|
|
|
#[account_type]
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
#[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,
|
|
|
|
|
/// Amount of collateral tokens deposited.
|
|
|
|
|
pub collateral_amount: u128,
|
|
|
|
|
/// Outstanding stablecoin debt against this position.
|
|
|
|
|
pub debt_amount: u128,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl TryFrom<&Data> for Position {
|
|
|
|
|
type Error = std::io::Error;
|
|
|
|
|
|
|
|
|
|
fn try_from(data: &Data) -> Result<Self, Self::Error> {
|
|
|
|
|
Self::try_from_slice(data.as_ref())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<&Position> for Data {
|
|
|
|
|
fn from(position: &Position) -> Self {
|
2026-05-11 17:32:47 -03:00
|
|
|
let mut data = Vec::with_capacity(std::mem::size_of_val(position));
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
BorshSerialize::serialize(position, &mut data)
|
|
|
|
|
.expect("Serialization to Vec should not fail");
|
|
|
|
|
Self::try_from(data).expect("Position encoded data should fit into Data")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 13:04:59 -03:00
|
|
|
/// PDA seed for the [`Position`] account owned by `owner_id` for `collateral_definition_id`.
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
///
|
2026-05-12 13:04:59 -03:00
|
|
|
/// 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 {
|
|
|
|
|
use risc0_zkvm::sha::{Impl, Sha256 as _};
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
|
2026-05-12 13:04:59 -03:00
|
|
|
let mut bytes = [0u8; 96];
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
bytes[0..32].copy_from_slice(&owner_id.to_bytes());
|
2026-05-12 13:04:59 -03:00
|
|
|
bytes[32..64].copy_from_slice(&collateral_definition_id.to_bytes());
|
|
|
|
|
bytes[64..96].copy_from_slice(&POSITION_PDA_DOMAIN);
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
|
|
|
|
|
let mut out = [0u8; 32];
|
|
|
|
|
out.copy_from_slice(Impl::hash_bytes(&bytes).as_bytes());
|
|
|
|
|
PdaSeed::new(out)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Account id of the [`Position`] PDA owned by `owner_id` under `stablecoin_program_id`.
|
2026-05-12 13:04:59 -03:00
|
|
|
pub fn compute_position_pda(
|
|
|
|
|
stablecoin_program_id: ProgramId,
|
|
|
|
|
owner_id: AccountId,
|
|
|
|
|
collateral_definition_id: AccountId,
|
|
|
|
|
) -> AccountId {
|
|
|
|
|
AccountId::for_public_pda(
|
|
|
|
|
&stablecoin_program_id,
|
|
|
|
|
&compute_position_pda_seed(owner_id, collateral_definition_id),
|
|
|
|
|
)
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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.
|
|
|
|
|
pub fn compute_position_vault_pda_seed(position_id: AccountId) -> PdaSeed {
|
2026-05-12 13:04:59 -03:00
|
|
|
use risc0_zkvm::sha::{Impl, Sha256 as _};
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
|
|
|
|
|
let mut bytes = [0u8; 64];
|
|
|
|
|
bytes[0..32].copy_from_slice(&position_id.to_bytes());
|
|
|
|
|
bytes[32..64].copy_from_slice(&POSITION_VAULT_PDA_DOMAIN);
|
|
|
|
|
|
|
|
|
|
let mut out = [0u8; 32];
|
|
|
|
|
out.copy_from_slice(Impl::hash_bytes(&bytes).as_bytes());
|
|
|
|
|
PdaSeed::new(out)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Account id of the collateral vault PDA for `position_id` under `stablecoin_program_id`.
|
|
|
|
|
pub fn compute_position_vault_pda(
|
|
|
|
|
stablecoin_program_id: ProgramId,
|
|
|
|
|
position_id: AccountId,
|
|
|
|
|
) -> AccountId {
|
2026-05-12 13:04:59 -03:00
|
|
|
AccountId::for_public_pda(
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
&stablecoin_program_id,
|
|
|
|
|
&compute_position_vault_pda_seed(position_id),
|
2026-05-12 13:04:59 -03:00
|
|
|
)
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-12 13:04:59 -03:00
|
|
|
/// Verify the position account's address matches
|
|
|
|
|
/// `(stablecoin_program_id, owner, collateral_definition_id)` and return the [`PdaSeed`] for
|
|
|
|
|
/// use in post-state claims.
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
///
|
|
|
|
|
/// # Panics
|
2026-05-12 13:04:59 -03:00
|
|
|
/// If `position.account_id` does not match the address derived from `owner`,
|
|
|
|
|
/// `collateral_definition_id`, and `stablecoin_program_id`.
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
pub fn verify_position_and_get_seed(
|
|
|
|
|
position: &AccountWithMetadata,
|
|
|
|
|
owner: &AccountWithMetadata,
|
2026-05-12 13:04:59 -03:00
|
|
|
collateral_definition_id: AccountId,
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
stablecoin_program_id: ProgramId,
|
|
|
|
|
) -> PdaSeed {
|
2026-05-12 13:04:59 -03:00
|
|
|
let seed = compute_position_pda_seed(owner.account_id, collateral_definition_id);
|
|
|
|
|
let expected_id = AccountId::for_public_pda(&stablecoin_program_id, &seed);
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
assert_eq!(
|
|
|
|
|
position.account_id, expected_id,
|
|
|
|
|
"Position account ID does not match expected derivation"
|
|
|
|
|
);
|
|
|
|
|
seed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Verify the vault account's address matches `(stablecoin_program_id, position)` and
|
|
|
|
|
/// return the [`PdaSeed`] for use in chained calls.
|
|
|
|
|
///
|
|
|
|
|
/// # Panics
|
|
|
|
|
/// If `vault.account_id` does not match the address derived from `position_id` and
|
|
|
|
|
/// `stablecoin_program_id`.
|
|
|
|
|
pub fn verify_position_vault_and_get_seed(
|
|
|
|
|
vault: &AccountWithMetadata,
|
|
|
|
|
position_id: AccountId,
|
|
|
|
|
stablecoin_program_id: ProgramId,
|
|
|
|
|
) -> PdaSeed {
|
|
|
|
|
let seed = compute_position_vault_pda_seed(position_id);
|
2026-05-12 13:04:59 -03:00
|
|
|
let expected_id = AccountId::for_public_pda(&stablecoin_program_id, &seed);
|
feat(stablecoin): implement `open_position`
Adds the `open_position` instruction to the Stablecoin Program. The instruction
claims a per-owner `Position` PDA, initializes a collateral vault token holding
via a chained `Token::InitializeAccount` under the vault's PDA authority, and
moves `collateral_amount` from the user's holding into the vault with a chained
`Token::Transfer`. `Position` is persisted with `collateral_amount` and
`debt_amount = 0`; the debt path is deferred to `generate_debt`.
- Add `Position` struct, `OpenPosition` instruction variant, and
`compute_position_pda{,_seed}` / `compute_position_vault_pda{,_seed}` helpers
in `stablecoin_core` with domain-separated PDA seeds.
- Implement `open_position::open_position` mirroring the ATA `create` and AMM
`new_definition` patterns: authorization and uninitialized-state asserts, PDA
verification, and same-transaction chained `InitializeAccount` + `Transfer`.
- Wire the new instruction through the SPEL guest and regenerate the stablecoin
IDL artifact.
- Cover the happy path, all assertion paths, and PDA determinism /
non-collision in 11 new unit tests.
2026-05-11 17:14:27 -03:00
|
|
|
assert_eq!(
|
|
|
|
|
vault.account_id, expected_id,
|
|
|
|
|
"Position vault account ID does not match expected derivation"
|
|
|
|
|
);
|
|
|
|
|
seed
|
2026-05-11 14:51:50 +02:00
|
|
|
}
|