mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-05-19 15:39:28 +00:00
fix(stablecoin): address open position review feedback
This commit is contained in:
parent
1ecdd4e4c5
commit
bb6f21db7f
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -3044,6 +3044,7 @@ dependencies = [
|
|||||||
"nssa_core",
|
"nssa_core",
|
||||||
"risc0-zkvm",
|
"risc0-zkvm",
|
||||||
"serde",
|
"serde",
|
||||||
|
"spel-framework-macros",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@ -2,18 +2,6 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"name": "stablecoin",
|
"name": "stablecoin",
|
||||||
"instructions": [
|
"instructions": [
|
||||||
{
|
|
||||||
"name": "noop",
|
|
||||||
"accounts": [
|
|
||||||
{
|
|
||||||
"name": "account",
|
|
||||||
"writable": false,
|
|
||||||
"signer": false,
|
|
||||||
"init": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"args": []
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "open_position",
|
"name": "open_position",
|
||||||
"accounts": [
|
"accounts": [
|
||||||
@ -49,10 +37,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"args": [
|
"args": [
|
||||||
{
|
|
||||||
"name": "stablecoin_program_id",
|
|
||||||
"type": "program_id"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "collateral_amount",
|
"name": "collateral_amount",
|
||||||
"type": "u128"
|
"type": "u128"
|
||||||
@ -60,5 +44,166 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": "Position",
|
||||||
|
"type": {
|
||||||
|
"kind": "struct",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "collateral_vault_id",
|
||||||
|
"type": "account_id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "collateral_definition_id",
|
||||||
|
"type": "account_id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "collateral_amount",
|
||||||
|
"type": "u128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "debt_amount",
|
||||||
|
"type": "u128"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TokenDefinition",
|
||||||
|
"type": {
|
||||||
|
"kind": "enum",
|
||||||
|
"variants": [
|
||||||
|
{
|
||||||
|
"name": "Fungible",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "total_supply",
|
||||||
|
"type": "u128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "metadata_id",
|
||||||
|
"type": {
|
||||||
|
"option": "account_id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NonFungible",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "printable_supply",
|
||||||
|
"type": "u128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "metadata_id",
|
||||||
|
"type": "account_id"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TokenHolding",
|
||||||
|
"type": {
|
||||||
|
"kind": "enum",
|
||||||
|
"variants": [
|
||||||
|
{
|
||||||
|
"name": "Fungible",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "definition_id",
|
||||||
|
"type": "account_id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "balance",
|
||||||
|
"type": "u128"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NftMaster",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "definition_id",
|
||||||
|
"type": "account_id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "print_balance",
|
||||||
|
"type": "u128"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NftPrintedCopy",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "definition_id",
|
||||||
|
"type": "account_id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "owned",
|
||||||
|
"type": "bool"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TokenMetadata",
|
||||||
|
"type": {
|
||||||
|
"kind": "struct",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "definition_id",
|
||||||
|
"type": "account_id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "standard",
|
||||||
|
"type": {
|
||||||
|
"defined": "MetadataStandard"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "uri",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "creators",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "primary_sale_date",
|
||||||
|
"type": "u64"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"types": [
|
||||||
|
{
|
||||||
|
"name": "MetadataStandard",
|
||||||
|
"kind": "enum",
|
||||||
|
"variants": [
|
||||||
|
{
|
||||||
|
"name": "Simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Expanded"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"instruction_type": "stablecoin_core::Instruction"
|
"instruction_type": "stablecoin_core::Instruction"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,3 +8,4 @@ nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.gi
|
|||||||
borsh = { version = "1.5", features = ["derive"] }
|
borsh = { version = "1.5", features = ["derive"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
risc0-zkvm = { version = "=3.0.5", default-features = false }
|
risc0-zkvm = { version = "=3.0.5", default-features = false }
|
||||||
|
spel-framework-macros = { git = "https://github.com/logos-co/spel.git", rev = "6473ab4c400bc59bac8db83a286faaeafa7d1999", package = "spel-framework-macros" }
|
||||||
|
|||||||
@ -6,34 +6,27 @@ use nssa_core::{
|
|||||||
program::{PdaSeed, ProgramId},
|
program::{PdaSeed, ProgramId},
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use spel_framework_macros::account_type;
|
||||||
|
|
||||||
// Domain-separation tags for the PDA seeds derived by the Stablecoin Program.
|
const POSITION_PDA_DOMAIN: [u8; 32] = [0; 32];
|
||||||
// These bytes are part of the on-chain derivation scheme and must stay unchanged for
|
const POSITION_VAULT_PDA_DOMAIN: [u8; 32] = [1; 32];
|
||||||
// account-address compatibility.
|
|
||||||
const POSITION_PDA_DOMAIN: [u8; 32] = *b"stablecoin::position::seed::v1\0\0";
|
|
||||||
const POSITION_VAULT_PDA_DOMAIN: [u8; 32] = *b"stablecoin::position::vault::v1\0";
|
|
||||||
|
|
||||||
/// Stablecoin Program Instruction.
|
/// Stablecoin Program Instruction.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub enum Instruction {
|
pub enum Instruction {
|
||||||
/// Heartbeat / sanity-check entry point that returns the input account unchanged.
|
|
||||||
Noop,
|
|
||||||
|
|
||||||
/// Open a new collateral-only [`Position`] for the calling owner.
|
/// Open a new collateral-only [`Position`] for the calling owner.
|
||||||
///
|
///
|
||||||
/// Required accounts (5):
|
/// Required accounts (5):
|
||||||
/// - Owner account (authorized)
|
/// - Owner account (authorized)
|
||||||
/// - Position account (uninitialized, address must match
|
/// - Position account (uninitialized, address must match
|
||||||
/// `compute_position_pda(stablecoin_program_id, owner)`)
|
/// `compute_position_pda(self_program_id, owner, token_definition)`)
|
||||||
/// - Position vault token holding account (uninitialized, address must match
|
/// - Position vault token holding account (uninitialized, address must match
|
||||||
/// `compute_position_vault_pda(stablecoin_program_id, position_id)`)
|
/// `compute_position_vault_pda(self_program_id, position_id)`)
|
||||||
/// - Owner's source token holding for the collateral (authorized, initialized)
|
/// - Owner's source token holding for the collateral (authorized, initialized)
|
||||||
/// - Token definition account for the collateral (matches the user holding's `definition_id`;
|
/// - 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`
|
/// its `program_owner` determines the Token Program used by the chained `InitializeAccount`
|
||||||
/// / `Transfer` calls)
|
/// / `Transfer` calls)
|
||||||
OpenPosition {
|
OpenPosition {
|
||||||
/// `ProgramId` under which the [`Position`] and vault PDAs are derived.
|
|
||||||
stablecoin_program_id: ProgramId,
|
|
||||||
/// Amount of collateral tokens to deposit into the position vault.
|
/// Amount of collateral tokens to deposit into the position vault.
|
||||||
collateral_amount: u128,
|
collateral_amount: u128,
|
||||||
},
|
},
|
||||||
@ -43,6 +36,7 @@ pub enum Instruction {
|
|||||||
///
|
///
|
||||||
/// `debt_amount` is included for forward compatibility with `generate_debt`; until that
|
/// `debt_amount` is included for forward compatibility with `generate_debt`; until that
|
||||||
/// instruction lands `open_position` always initializes it to `0`.
|
/// instruction lands `open_position` always initializes it to `0`.
|
||||||
|
#[account_type]
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||||
pub struct Position {
|
pub struct Position {
|
||||||
/// Token holding account (vault PDA) that custodies the collateral backing this position.
|
/// Token holding account (vault PDA) that custodies the collateral backing this position.
|
||||||
@ -66,30 +60,26 @@ impl TryFrom<&Data> for Position {
|
|||||||
impl From<&Position> for Data {
|
impl From<&Position> for Data {
|
||||||
fn from(position: &Position) -> Self {
|
fn from(position: &Position) -> Self {
|
||||||
let mut data = Vec::with_capacity(std::mem::size_of_val(position));
|
let mut data = Vec::with_capacity(std::mem::size_of_val(position));
|
||||||
#[allow(
|
|
||||||
clippy::expect_used,
|
|
||||||
reason = "BorshSerialize::serialize is infallible when writing to a Vec"
|
|
||||||
)]
|
|
||||||
BorshSerialize::serialize(position, &mut data)
|
BorshSerialize::serialize(position, &mut data)
|
||||||
.expect("Serialization to Vec should not fail");
|
.expect("Serialization to Vec should not fail");
|
||||||
#[allow(
|
|
||||||
clippy::expect_used,
|
|
||||||
reason = "Position encodes to a small, bounded byte length that fits Data"
|
|
||||||
)]
|
|
||||||
Self::try_from(data).expect("Position encoded data should fit into Data")
|
Self::try_from(data).expect("Position encoded data should fit into Data")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PDA seed for the [`Position`] account owned by `owner_id`.
|
/// PDA seed for the [`Position`] account owned by `owner_id` for `collateral_definition_id`.
|
||||||
///
|
///
|
||||||
/// Derived from the owner's address with a domain-separation tag so the resulting seed
|
/// Derived from the owner and collateral definition addresses with a domain-separation tag
|
||||||
/// cannot collide with other stablecoin-managed PDAs that hash the same owner id.
|
/// so one owner can hold separate positions for separate collateral definitions.
|
||||||
pub fn compute_position_pda_seed(owner_id: AccountId) -> PdaSeed {
|
pub fn compute_position_pda_seed(
|
||||||
use risc0_zkvm::sha::{Impl, Sha256};
|
owner_id: AccountId,
|
||||||
|
collateral_definition_id: AccountId,
|
||||||
|
) -> PdaSeed {
|
||||||
|
use risc0_zkvm::sha::{Impl, Sha256 as _};
|
||||||
|
|
||||||
let mut bytes = [0u8; 64];
|
let mut bytes = [0u8; 96];
|
||||||
bytes[0..32].copy_from_slice(&owner_id.to_bytes());
|
bytes[0..32].copy_from_slice(&owner_id.to_bytes());
|
||||||
bytes[32..64].copy_from_slice(&POSITION_PDA_DOMAIN);
|
bytes[32..64].copy_from_slice(&collateral_definition_id.to_bytes());
|
||||||
|
bytes[64..96].copy_from_slice(&POSITION_PDA_DOMAIN);
|
||||||
|
|
||||||
let mut out = [0u8; 32];
|
let mut out = [0u8; 32];
|
||||||
out.copy_from_slice(Impl::hash_bytes(&bytes).as_bytes());
|
out.copy_from_slice(Impl::hash_bytes(&bytes).as_bytes());
|
||||||
@ -97,8 +87,15 @@ pub fn compute_position_pda_seed(owner_id: AccountId) -> PdaSeed {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Account id of the [`Position`] PDA owned by `owner_id` under `stablecoin_program_id`.
|
/// Account id of the [`Position`] PDA owned by `owner_id` under `stablecoin_program_id`.
|
||||||
pub fn compute_position_pda(stablecoin_program_id: ProgramId, owner_id: AccountId) -> AccountId {
|
pub fn compute_position_pda(
|
||||||
AccountId::from((&stablecoin_program_id, &compute_position_pda_seed(owner_id)))
|
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),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PDA seed for the collateral vault token holding bound to a [`Position`].
|
/// PDA seed for the collateral vault token holding bound to a [`Position`].
|
||||||
@ -106,7 +103,7 @@ pub fn compute_position_pda(stablecoin_program_id: ProgramId, owner_id: AccountI
|
|||||||
/// Derived from the position's address with a distinct domain-separation tag so the vault
|
/// 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.
|
/// 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 {
|
pub fn compute_position_vault_pda_seed(position_id: AccountId) -> PdaSeed {
|
||||||
use risc0_zkvm::sha::{Impl, Sha256};
|
use risc0_zkvm::sha::{Impl, Sha256 as _};
|
||||||
|
|
||||||
let mut bytes = [0u8; 64];
|
let mut bytes = [0u8; 64];
|
||||||
bytes[0..32].copy_from_slice(&position_id.to_bytes());
|
bytes[0..32].copy_from_slice(&position_id.to_bytes());
|
||||||
@ -122,25 +119,27 @@ pub fn compute_position_vault_pda(
|
|||||||
stablecoin_program_id: ProgramId,
|
stablecoin_program_id: ProgramId,
|
||||||
position_id: AccountId,
|
position_id: AccountId,
|
||||||
) -> AccountId {
|
) -> AccountId {
|
||||||
AccountId::from((
|
AccountId::for_public_pda(
|
||||||
&stablecoin_program_id,
|
&stablecoin_program_id,
|
||||||
&compute_position_vault_pda_seed(position_id),
|
&compute_position_vault_pda_seed(position_id),
|
||||||
))
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verify the position account's address matches `(stablecoin_program_id, owner)` and
|
/// Verify the position account's address matches
|
||||||
/// return the [`PdaSeed`] for use in chained calls.
|
/// `(stablecoin_program_id, owner, collateral_definition_id)` and return the [`PdaSeed`] for
|
||||||
|
/// use in post-state claims.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
/// If `position.account_id` does not match the address derived from `owner` and
|
/// If `position.account_id` does not match the address derived from `owner`,
|
||||||
/// `stablecoin_program_id`.
|
/// `collateral_definition_id`, and `stablecoin_program_id`.
|
||||||
pub fn verify_position_and_get_seed(
|
pub fn verify_position_and_get_seed(
|
||||||
position: &AccountWithMetadata,
|
position: &AccountWithMetadata,
|
||||||
owner: &AccountWithMetadata,
|
owner: &AccountWithMetadata,
|
||||||
|
collateral_definition_id: AccountId,
|
||||||
stablecoin_program_id: ProgramId,
|
stablecoin_program_id: ProgramId,
|
||||||
) -> PdaSeed {
|
) -> PdaSeed {
|
||||||
let seed = compute_position_pda_seed(owner.account_id);
|
let seed = compute_position_pda_seed(owner.account_id, collateral_definition_id);
|
||||||
let expected_id = AccountId::from((&stablecoin_program_id, &seed));
|
let expected_id = AccountId::for_public_pda(&stablecoin_program_id, &seed);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
position.account_id, expected_id,
|
position.account_id, expected_id,
|
||||||
"Position account ID does not match expected derivation"
|
"Position account ID does not match expected derivation"
|
||||||
@ -160,7 +159,7 @@ pub fn verify_position_vault_and_get_seed(
|
|||||||
stablecoin_program_id: ProgramId,
|
stablecoin_program_id: ProgramId,
|
||||||
) -> PdaSeed {
|
) -> PdaSeed {
|
||||||
let seed = compute_position_vault_pda_seed(position_id);
|
let seed = compute_position_vault_pda_seed(position_id);
|
||||||
let expected_id = AccountId::from((&stablecoin_program_id, &seed));
|
let expected_id = AccountId::for_public_pda(&stablecoin_program_id, &seed);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vault.account_id, expected_id,
|
vault.account_id, expected_id,
|
||||||
"Position vault account ID does not match expected derivation"
|
"Position vault account ID does not match expected derivation"
|
||||||
|
|||||||
1
stablecoin/methods/guest/Cargo.lock
generated
1
stablecoin/methods/guest/Cargo.lock
generated
@ -2948,6 +2948,7 @@ dependencies = [
|
|||||||
"nssa_core",
|
"nssa_core",
|
||||||
"risc0-zkvm",
|
"risc0-zkvm",
|
||||||
"serde",
|
"serde",
|
||||||
|
"spel-framework-macros",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@ -1,65 +1,29 @@
|
|||||||
//! 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.
|
|
||||||
|
|
||||||
#![no_main]
|
#![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 nssa_core::account::AccountWithMetadata;
|
||||||
|
use spel_framework::context::ProgramContext;
|
||||||
use spel_framework::prelude::*;
|
use spel_framework::prelude::*;
|
||||||
|
|
||||||
risc0_zkvm::guest::entry!(main);
|
risc0_zkvm::guest::entry!(main);
|
||||||
|
|
||||||
#[lez_program(instruction = "stablecoin_core::Instruction")]
|
#[lez_program(instruction = "stablecoin_core::Instruction")]
|
||||||
mod stablecoin {
|
mod stablecoin {
|
||||||
#[allow(
|
#[allow(unused_imports)]
|
||||||
unused_imports,
|
|
||||||
reason = "lez_program expansion may or may not reference every super:: import"
|
|
||||||
)]
|
|
||||||
use super::*;
|
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.
|
/// Open a new collateral-only position for the calling owner.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Returns the host program's panic-converted error if any precondition fails (see
|
/// Returns the host program's panic-converted error if any precondition fails (see
|
||||||
/// [`stablecoin_program::open_position::open_position`] for the full list).
|
/// [`stablecoin_program::open_position::open_position`] for the full list).
|
||||||
#[instruction]
|
#[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(
|
pub fn open_position(
|
||||||
|
ctx: ProgramContext,
|
||||||
owner: AccountWithMetadata,
|
owner: AccountWithMetadata,
|
||||||
position: AccountWithMetadata,
|
position: AccountWithMetadata,
|
||||||
vault: AccountWithMetadata,
|
vault: AccountWithMetadata,
|
||||||
user_holding: AccountWithMetadata,
|
user_holding: AccountWithMetadata,
|
||||||
token_definition: AccountWithMetadata,
|
token_definition: AccountWithMetadata,
|
||||||
stablecoin_program_id: ProgramId,
|
|
||||||
collateral_amount: u128,
|
collateral_amount: u128,
|
||||||
) -> SpelResult {
|
) -> SpelResult {
|
||||||
let (post_states, chained_calls) = stablecoin_program::open_position::open_position(
|
let (post_states, chained_calls) = stablecoin_program::open_position::open_position(
|
||||||
@ -68,9 +32,12 @@ mod stablecoin {
|
|||||||
vault,
|
vault,
|
||||||
user_holding,
|
user_holding,
|
||||||
token_definition,
|
token_definition,
|
||||||
stablecoin_program_id,
|
ctx.self_program_id,
|
||||||
collateral_amount,
|
collateral_amount,
|
||||||
);
|
);
|
||||||
Ok(SpelOutput::with_chained_calls(post_states, chained_calls))
|
Ok(spel_framework::SpelOutput::execute(
|
||||||
|
post_states,
|
||||||
|
chained_calls,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
pub use stablecoin_core as core;
|
pub use stablecoin_core as core;
|
||||||
|
|
||||||
/// No-op instruction used as a heartbeat / sanity-check entry point.
|
|
||||||
pub mod noop;
|
|
||||||
/// Open a new collateral-only position for a calling owner.
|
/// Open a new collateral-only position for a calling owner.
|
||||||
pub mod open_position;
|
pub mod open_position;
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
use nssa_core::{account::AccountWithMetadata, program::AccountPostState};
|
|
||||||
|
|
||||||
/// Pass `account` through unchanged as a single post-state entry.
|
|
||||||
pub fn noop(account: AccountWithMetadata) -> Vec<AccountPostState> {
|
|
||||||
vec![AccountPostState::new(account.account)]
|
|
||||||
}
|
|
||||||
@ -23,11 +23,6 @@ use token_core::TokenHolding;
|
|||||||
/// - `user_holding` cannot be decoded as a [`TokenHolding`].
|
/// - `user_holding` cannot be decoded as a [`TokenHolding`].
|
||||||
/// - `user_holding`'s definition does not match `token_definition`.
|
/// - `user_holding`'s definition does not match `token_definition`.
|
||||||
/// - `token_definition.program_owner` does not match `user_holding.program_owner`.
|
/// - `token_definition.program_owner` does not match `user_holding.program_owner`.
|
||||||
#[allow(
|
|
||||||
clippy::needless_pass_by_value,
|
|
||||||
reason = "instruction handler shape: spel #[instruction] macro deserializes owned \
|
|
||||||
AccountWithMetadata values into these parameters"
|
|
||||||
)]
|
|
||||||
pub fn open_position(
|
pub fn open_position(
|
||||||
owner: AccountWithMetadata,
|
owner: AccountWithMetadata,
|
||||||
position: AccountWithMetadata,
|
position: AccountWithMetadata,
|
||||||
@ -53,15 +48,6 @@ pub fn open_position(
|
|||||||
"Position vault account must be uninitialized"
|
"Position vault account must be uninitialized"
|
||||||
);
|
);
|
||||||
|
|
||||||
let position_seed = verify_position_and_get_seed(&position, &owner, stablecoin_program_id);
|
|
||||||
let vault_seed =
|
|
||||||
verify_position_vault_and_get_seed(&vault, position.account_id, stablecoin_program_id);
|
|
||||||
|
|
||||||
#[allow(
|
|
||||||
clippy::expect_used,
|
|
||||||
reason = "open_position uses the same panic-on-bad-input pattern as the surrounding \
|
|
||||||
assert!/assert_eq! invariant checks; runtime must supply a valid TokenHolding"
|
|
||||||
)]
|
|
||||||
let user_holding_definition_id = TokenHolding::try_from(&user_holding.account.data)
|
let user_holding_definition_id = TokenHolding::try_from(&user_holding.account.data)
|
||||||
.expect("User holding must be a valid Token Holding")
|
.expect("User holding must be a valid Token Holding")
|
||||||
.definition_id();
|
.definition_id();
|
||||||
@ -75,6 +61,15 @@ pub fn open_position(
|
|||||||
"Collateral token definition is not owned by the user holding's Token Program"
|
"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 vault_seed =
|
||||||
|
verify_position_vault_and_get_seed(&vault, position.account_id, stablecoin_program_id);
|
||||||
|
|
||||||
let mut position_post = position.account;
|
let mut position_post = position.account;
|
||||||
position_post.program_owner = stablecoin_program_id;
|
position_post.program_owner = stablecoin_program_id;
|
||||||
position_post.data = Data::from(&Position {
|
position_post.data = Data::from(&Position {
|
||||||
@ -92,9 +87,8 @@ pub fn open_position(
|
|||||||
AccountPostState::new(token_definition.account.clone()),
|
AccountPostState::new(token_definition.account.clone()),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Chained Token::InitializeAccount sets the vault up as a zero-balance holding of the
|
// Chained Token::InitializeAccount owns the vault as a Token holding. The Stablecoin
|
||||||
// collateral token. We hand the runtime the vault marked authorized via PDA so it can
|
// program only authorizes that claim by passing the vault PDA seed to the chained call.
|
||||||
// satisfy `InitializeAccount`'s authorization requirement without a user signature.
|
|
||||||
let mut vault_authorized = vault.clone();
|
let mut vault_authorized = vault.clone();
|
||||||
vault_authorized.is_authorized = true;
|
vault_authorized.is_authorized = true;
|
||||||
let initialize_call = ChainedCall::new(
|
let initialize_call = ChainedCall::new(
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
#![allow(
|
#![allow(
|
||||||
clippy::expect_used,
|
|
||||||
clippy::indexing_slicing,
|
clippy::indexing_slicing,
|
||||||
clippy::panic,
|
clippy::panic,
|
||||||
clippy::unwrap_used,
|
clippy::unwrap_used,
|
||||||
@ -32,7 +31,11 @@ fn user_holding_id() -> AccountId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn position_id() -> AccountId {
|
fn position_id() -> AccountId {
|
||||||
compute_position_pda(STABLECOIN_PROGRAM_ID, owner_id())
|
compute_position_pda(
|
||||||
|
STABLECOIN_PROGRAM_ID,
|
||||||
|
owner_id(),
|
||||||
|
collateral_definition_id(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn vault_id() -> AccountId {
|
fn vault_id() -> AccountId {
|
||||||
@ -96,17 +99,6 @@ fn uninit_vault_account() -> AccountWithMetadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn noop_returns_single_post_state() {
|
|
||||||
let account = AccountWithMetadata {
|
|
||||||
account: Account::default(),
|
|
||||||
is_authorized: false,
|
|
||||||
account_id: AccountId::new([0u8; 32]),
|
|
||||||
};
|
|
||||||
let post_states = crate::noop::noop(account);
|
|
||||||
assert_eq!(post_states.len(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn open_position_claims_pda_and_emits_chained_calls() {
|
fn open_position_claims_pda_and_emits_chained_calls() {
|
||||||
let collateral_amount: u128 = 500;
|
let collateral_amount: u128 = 500;
|
||||||
@ -126,7 +118,10 @@ fn open_position_claims_pda_and_emits_chained_calls() {
|
|||||||
let position_post = &post_states[1];
|
let position_post = &post_states[1];
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
position_post.required_claim(),
|
position_post.required_claim(),
|
||||||
Some(Claim::Pda(compute_position_pda_seed(owner_id())))
|
Some(Claim::Pda(compute_position_pda_seed(
|
||||||
|
owner_id(),
|
||||||
|
collateral_definition_id()
|
||||||
|
)))
|
||||||
);
|
);
|
||||||
let position = Position::try_from(&position_post.account().data).expect("valid Position");
|
let position = Position::try_from(&position_post.account().data).expect("valid Position");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -356,23 +351,44 @@ fn open_position_rejects_definition_with_wrong_token_program() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn position_pda_is_deterministic_and_owner_specific() {
|
fn position_pda_is_deterministic_and_owner_and_collateral_specific() {
|
||||||
let id_a = compute_position_pda(STABLECOIN_PROGRAM_ID, owner_id());
|
let id_a = compute_position_pda(
|
||||||
let id_b = compute_position_pda(STABLECOIN_PROGRAM_ID, owner_id());
|
STABLECOIN_PROGRAM_ID,
|
||||||
|
owner_id(),
|
||||||
|
collateral_definition_id(),
|
||||||
|
);
|
||||||
|
let id_b = compute_position_pda(
|
||||||
|
STABLECOIN_PROGRAM_ID,
|
||||||
|
owner_id(),
|
||||||
|
collateral_definition_id(),
|
||||||
|
);
|
||||||
assert_eq!(id_a, id_b);
|
assert_eq!(id_a, id_b);
|
||||||
|
|
||||||
let other_owner = AccountId::new([0x11u8; 32]);
|
let other_owner = AccountId::new([0x11u8; 32]);
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
compute_position_pda(STABLECOIN_PROGRAM_ID, other_owner),
|
compute_position_pda(
|
||||||
|
STABLECOIN_PROGRAM_ID,
|
||||||
|
other_owner,
|
||||||
|
collateral_definition_id()
|
||||||
|
),
|
||||||
|
id_a
|
||||||
|
);
|
||||||
|
|
||||||
|
let other_definition = AccountId::new([0x21u8; 32]);
|
||||||
|
assert_ne!(
|
||||||
|
compute_position_pda(STABLECOIN_PROGRAM_ID, owner_id(), other_definition),
|
||||||
id_a
|
id_a
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn position_pda_and_vault_pda_do_not_collide() {
|
fn position_pda_and_vault_pda_do_not_collide() {
|
||||||
// Distinct domain tags must keep the position id and its vault id disjoint, even
|
// Distinct domain tags must keep the position id and its vault id disjoint.
|
||||||
// though both derivations involve only the owner's address.
|
let position = compute_position_pda(
|
||||||
let position = compute_position_pda(STABLECOIN_PROGRAM_ID, owner_id());
|
STABLECOIN_PROGRAM_ID,
|
||||||
|
owner_id(),
|
||||||
|
collateral_definition_id(),
|
||||||
|
);
|
||||||
let vault = compute_position_vault_pda(STABLECOIN_PROGRAM_ID, position);
|
let vault = compute_position_vault_pda(STABLECOIN_PROGRAM_ID, position);
|
||||||
assert_ne!(position, vault);
|
assert_ne!(position, vault);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user