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.
This commit is contained in:
Ricardo Guilherme Schmidt
2026-05-12 12:43:42 -03:00
parent 22b41bdb3d
commit 11d73ae284
14 changed files with 801 additions and 5 deletions
+1
View File
@@ -1,3 +1,4 @@
//! Build script that embeds the stablecoin RISC Zero guest ELF as host-side constants.
fn main() {
risc0_build::embed_methods();
}
+13
View File
@@ -2937,6 +2937,7 @@ dependencies = [
"spel-framework",
"stablecoin_core",
"stablecoin_program",
"token_core",
]
[[package]]
@@ -2945,6 +2946,7 @@ version = "0.1.0"
dependencies = [
"borsh",
"nssa_core",
"risc0-zkvm",
"serde",
]
@@ -2954,6 +2956,7 @@ version = "0.1.0"
dependencies = [
"nssa_core",
"stablecoin_core",
"token_core",
]
[[package]]
@@ -3140,6 +3143,16 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "token_core"
version = "0.1.0"
dependencies = [
"borsh",
"nssa_core",
"serde",
"spel-framework-macros",
]
[[package]]
name = "tokio"
version = "1.52.3"
+1
View File
@@ -15,5 +15,6 @@ nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.gi
risc0-zkvm = { version = "=3.0.5", default-features = false }
stablecoin_core = { path = "../../core" }
stablecoin_program = { path = "../..", package = "stablecoin_program" }
token_core = { path = "../../../token/core" }
serde = { version = "1.0", features = ["derive"] }
borsh = "1.5"
+60 -3
View File
@@ -1,19 +1,76 @@
#![no_main]
//! RISC Zero guest binary for the Stablecoin Program.
//!
//! Wires the host-side `stablecoin_program` instruction handlers to the LEZ framework via
//! `#[lez_program]` so the entry points can be invoked from a deployed program.
use nssa_core::account::AccountWithMetadata;
#![no_main]
#![allow(
missing_docs,
reason = "lez_program / instruction proc macros emit module, function, and constant items \
that do not carry doc strings; user-written handlers document inline"
)]
use nssa_core::{account::AccountWithMetadata, program::ProgramId};
use spel_framework::prelude::*;
risc0_zkvm::guest::entry!(main);
#[lez_program(instruction = "stablecoin_core::Instruction")]
mod stablecoin {
#[allow(unused_imports)]
#[allow(
unused_imports,
reason = "lez_program expansion may or may not reference every super:: import"
)]
use super::*;
/// Heartbeat instruction that returns the input account unchanged.
///
/// # Errors
/// Currently never; reserved for future validation paths.
#[instruction]
#[allow(
deprecated,
reason = "SpelOutput::states_only: lez_program macro only rewrites vec![a, b, ...] \
literals into execute_with_claims; this handler delegates to a host function \
that returns Vec<AccountPostState>, so migration requires restructuring the \
handler shape — workspace-wide follow-up across token/amm/ata"
)]
pub fn noop(account: AccountWithMetadata) -> SpelResult {
Ok(spel_framework::SpelOutput::execute(stablecoin_program::noop::noop(
account,
), vec![]))
}
/// 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]
#[allow(
deprecated,
reason = "SpelOutput::with_chained_calls: same reason as noop above — migration to \
SpelOutput::execute requires the macro's vec![...] literal shape, which \
conflicts with delegating to host helpers"
)]
pub fn open_position(
owner: AccountWithMetadata,
position: AccountWithMetadata,
vault: AccountWithMetadata,
user_holding: AccountWithMetadata,
token_definition: AccountWithMetadata,
stablecoin_program_id: ProgramId,
collateral_amount: u128,
) -> SpelResult {
let (post_states, chained_calls) = stablecoin_program::open_position::open_position(
owner,
position,
vault,
user_holding,
token_definition,
stablecoin_program_id,
collateral_amount,
);
Ok(SpelOutput::with_chained_calls(post_states, chained_calls))
}
}
+11
View File
@@ -1 +1,12 @@
//! Host-side embedding of the stablecoin RISC Zero guest ELF.
//!
//! Re-exports the constants produced by `build.rs` via `risc0_build::embed_methods` —
//! `STABLECOIN_ELF`, `STABLECOIN_PATH`, and `STABLECOIN_ID` — used by host code to
//! load and identify the guest binary.
#![allow(
missing_docs,
reason = "constants below are generated by risc0_build::embed_methods at build time"
)]
include!(concat!(env!("OUT_DIR"), "/methods.rs"));