mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-05-19 15:39:28 +00:00
feat(oracle): add mock oracle feed program
Add shared oracle_core price-feed types and account-backed validation. Wire stablecoin to re-export the shared oracle interface and add the mock_oracle program plus IDL artifact. Update ATA integration tests for the current Transfer instruction shape. Refs #96
This commit is contained in:
parent
a422949f0a
commit
56dbf251d3
42
Cargo.lock
generated
42
Cargo.lock
generated
@ -1823,6 +1823,33 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mock-oracle-methods"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"mock_oracle_core",
|
||||||
|
"risc0-build",
|
||||||
|
"risc0-zkvm",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mock_oracle_core"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"nssa_core",
|
||||||
|
"oracle_core",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mock_oracle_program"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"mock_oracle_core",
|
||||||
|
"nssa_core",
|
||||||
|
"oracle_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "no_std_strings"
|
name = "no_std_strings"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
@ -1972,6 +1999,16 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "oracle_core"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"borsh",
|
||||||
|
"nssa_core",
|
||||||
|
"serde",
|
||||||
|
"spel-framework-macros",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
@ -3042,6 +3079,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"borsh",
|
"borsh",
|
||||||
"nssa_core",
|
"nssa_core",
|
||||||
|
"oracle_core",
|
||||||
"risc0-zkvm",
|
"risc0-zkvm",
|
||||||
"serde",
|
"serde",
|
||||||
"spel-framework-macros",
|
"spel-framework-macros",
|
||||||
@ -4081,9 +4119,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerofrom"
|
name = "zerofrom"
|
||||||
version = "0.1.7"
|
version = "0.1.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
|
checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerofrom-derive",
|
"zerofrom-derive",
|
||||||
]
|
]
|
||||||
|
|||||||
10
Cargo.toml
10
Cargo.toml
@ -9,9 +9,13 @@ members = [
|
|||||||
"ata/core",
|
"ata/core",
|
||||||
"ata",
|
"ata",
|
||||||
"ata/methods",
|
"ata/methods",
|
||||||
|
"oracle/core",
|
||||||
"stablecoin/core",
|
"stablecoin/core",
|
||||||
"stablecoin",
|
"stablecoin",
|
||||||
"stablecoin/methods",
|
"stablecoin/methods",
|
||||||
|
"mock_oracle/core",
|
||||||
|
"mock_oracle",
|
||||||
|
"mock_oracle/methods",
|
||||||
"integration_tests",
|
"integration_tests",
|
||||||
"tools/idl-gen",
|
"tools/idl-gen",
|
||||||
]
|
]
|
||||||
@ -20,6 +24,7 @@ exclude = [
|
|||||||
"amm/methods/guest",
|
"amm/methods/guest",
|
||||||
"ata/methods/guest",
|
"ata/methods/guest",
|
||||||
"stablecoin/methods/guest",
|
"stablecoin/methods/guest",
|
||||||
|
"mock_oracle/methods/guest",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
@ -32,10 +37,13 @@ amm_core = { path = "amm/core" }
|
|||||||
amm_program = { path = "amm" }
|
amm_program = { path = "amm" }
|
||||||
ata_core = { path = "ata/core" }
|
ata_core = { path = "ata/core" }
|
||||||
ata_program = { path = "ata" }
|
ata_program = { path = "ata" }
|
||||||
|
oracle_core = { path = "oracle/core" }
|
||||||
stablecoin_core = { path = "stablecoin/core" }
|
stablecoin_core = { path = "stablecoin/core" }
|
||||||
stablecoin_program = { path = "stablecoin" }
|
stablecoin_program = { path = "stablecoin" }
|
||||||
|
mock_oracle_core = { path = "mock_oracle/core" }
|
||||||
|
mock_oracle_program = { path = "mock_oracle" }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
borsh = { version = "1.0", features = ["derive"] }
|
borsh = { version = "1.5", features = ["derive"] }
|
||||||
risc0-zkvm = { version = "=3.0.5" }
|
risc0-zkvm = { version = "=3.0.5" }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tokio = { version = "1.28.2", features = ["net", "rt-multi-thread", "sync", "macros"] }
|
tokio = { version = "1.28.2", features = ["net", "rt-multi-thread", "sync", "macros"] }
|
||||||
|
|||||||
78
artifacts/mock_oracle-idl.json
Normal file
78
artifacts/mock_oracle-idl.json
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
"version": "0.1.0",
|
||||||
|
"name": "mock_oracle",
|
||||||
|
"instructions": [
|
||||||
|
{
|
||||||
|
"name": "set_price",
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": "price_account",
|
||||||
|
"writable": false,
|
||||||
|
"signer": false,
|
||||||
|
"init": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "base_asset",
|
||||||
|
"type": "account_id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "quote_asset",
|
||||||
|
"type": "account_id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "price",
|
||||||
|
"type": "u128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "source_identifier",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "confidence_interval",
|
||||||
|
"type": "u128"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": "OraclePriceAccount",
|
||||||
|
"type": {
|
||||||
|
"kind": "struct",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "base_asset",
|
||||||
|
"type": "account_id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "quote_asset",
|
||||||
|
"type": "account_id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "price",
|
||||||
|
"type": "u128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "source_identifier",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "confidence_interval",
|
||||||
|
"type": "u128"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"instruction_type": "mock_oracle_core::Instruction"
|
||||||
|
}
|
||||||
@ -45,30 +45,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"accounts": [
|
"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": "OraclePriceAccount",
|
"name": "OraclePriceAccount",
|
||||||
"type": {
|
"type": {
|
||||||
@ -101,6 +77,30 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"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",
|
"name": "TokenDefinition",
|
||||||
"type": {
|
"type": {
|
||||||
|
|||||||
9
mock_oracle/Cargo.toml
Normal file
9
mock_oracle/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "mock_oracle_program"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc3", features = ["host"] }
|
||||||
|
mock_oracle_core = { path = "core" }
|
||||||
|
oracle_core = { path = "../oracle/core" }
|
||||||
9
mock_oracle/core/Cargo.toml
Normal file
9
mock_oracle/core/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "mock_oracle_core"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc3", features = ["host"] }
|
||||||
|
oracle_core = { path = "../../oracle/core" }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
55
mock_oracle/core/src/lib.rs
Normal file
55
mock_oracle/core/src/lib.rs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
//! Core data structures for the Mock Oracle Program.
|
||||||
|
|
||||||
|
use nssa_core::account::AccountId;
|
||||||
|
use oracle_core::OraclePriceAccount;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Source identifier written by default examples and tests for this mock oracle.
|
||||||
|
pub const MOCK_ORACLE_SOURCE_IDENTIFIER: &str = "mock-oracle";
|
||||||
|
|
||||||
|
/// Mock Oracle Program instruction.
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub enum Instruction {
|
||||||
|
/// Write a price into an authorized mock oracle account.
|
||||||
|
///
|
||||||
|
/// Required accounts (1):
|
||||||
|
/// - Price account (authorized, uninitialized or already owned by the Mock Oracle Program)
|
||||||
|
SetPrice {
|
||||||
|
base_asset: AccountId,
|
||||||
|
quote_asset: AccountId,
|
||||||
|
price: u128,
|
||||||
|
timestamp: u64,
|
||||||
|
source_identifier: String,
|
||||||
|
confidence_interval: u128,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Price payload accepted by the Mock Oracle Program.
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct PriceUpdate {
|
||||||
|
/// Canonical identifier for the priced asset.
|
||||||
|
pub base_asset: AccountId,
|
||||||
|
/// Canonical identifier for the quote asset that denominates `price`.
|
||||||
|
pub quote_asset: AccountId,
|
||||||
|
/// Amount of `quote_asset` one unit of `base_asset` is worth.
|
||||||
|
pub price: u128,
|
||||||
|
/// Price observation timestamp.
|
||||||
|
pub timestamp: u64,
|
||||||
|
/// Identifier of the mock source publishing this price.
|
||||||
|
pub source_identifier: String,
|
||||||
|
/// Source-provided confidence interval, or zero when unavailable.
|
||||||
|
pub confidence_interval: u128,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&PriceUpdate> for OraclePriceAccount {
|
||||||
|
fn from(update: &PriceUpdate) -> Self {
|
||||||
|
Self {
|
||||||
|
base_asset: update.base_asset,
|
||||||
|
quote_asset: update.quote_asset,
|
||||||
|
price: update.price,
|
||||||
|
timestamp: update.timestamp,
|
||||||
|
source_identifier: update.source_identifier.clone(),
|
||||||
|
confidence_interval: update.confidence_interval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
mock_oracle/methods/Cargo.toml
Normal file
14
mock_oracle/methods/Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "mock-oracle-methods"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
risc0-build = "=3.0.5"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
risc0-zkvm = { version = "=3.0.5", features = ["std"] }
|
||||||
|
mock_oracle_core = { path = "../core" }
|
||||||
|
|
||||||
|
[package.metadata.risc0]
|
||||||
|
methods = ["guest"]
|
||||||
4
mock_oracle/methods/build.rs
Normal file
4
mock_oracle/methods/build.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
//! Build script that embeds the mock oracle RISC Zero guest ELF as host-side constants.
|
||||||
|
fn main() {
|
||||||
|
risc0_build::embed_methods();
|
||||||
|
}
|
||||||
4045
mock_oracle/methods/guest/Cargo.lock
generated
Normal file
4045
mock_oracle/methods/guest/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
mock_oracle/methods/guest/Cargo.toml
Normal file
20
mock_oracle/methods/guest/Cargo.toml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
[package]
|
||||||
|
name = "mock-oracle-guest"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "mock_oracle"
|
||||||
|
path = "src/bin/mock_oracle.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
spel-framework = { git = "https://github.com/logos-co/spel.git", rev = "6473ab4c400bc59bac8db83a286faaeafa7d1999", package = "spel-framework" }
|
||||||
|
nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc3" }
|
||||||
|
risc0-zkvm = { version = "=3.0.5", default-features = false }
|
||||||
|
mock_oracle_core = { path = "../../core" }
|
||||||
|
mock_oracle_program = { path = "../..", package = "mock_oracle_program" }
|
||||||
|
oracle_core = { path = "../../../oracle/core" }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
borsh = { version = "1.5", features = ["derive"] }
|
||||||
43
mock_oracle/methods/guest/src/bin/mock_oracle.rs
Normal file
43
mock_oracle/methods/guest/src/bin/mock_oracle.rs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
#![no_main]
|
||||||
|
|
||||||
|
use mock_oracle_core::PriceUpdate;
|
||||||
|
use nssa_core::account::AccountId;
|
||||||
|
use nssa_core::account::AccountWithMetadata;
|
||||||
|
use spel_framework::context::ProgramContext;
|
||||||
|
use spel_framework::prelude::*;
|
||||||
|
|
||||||
|
risc0_zkvm::guest::entry!(main);
|
||||||
|
|
||||||
|
#[lez_program(instruction = "mock_oracle_core::Instruction")]
|
||||||
|
mod mock_oracle {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Write a price into an authorized mock oracle account.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns the host program's panic-converted error if any precondition fails.
|
||||||
|
#[instruction]
|
||||||
|
pub fn set_price(
|
||||||
|
ctx: ProgramContext,
|
||||||
|
price_account: AccountWithMetadata,
|
||||||
|
base_asset: AccountId,
|
||||||
|
quote_asset: AccountId,
|
||||||
|
price: u128,
|
||||||
|
timestamp: u64,
|
||||||
|
source_identifier: String,
|
||||||
|
confidence_interval: u128,
|
||||||
|
) -> SpelResult {
|
||||||
|
let update = PriceUpdate {
|
||||||
|
base_asset,
|
||||||
|
quote_asset,
|
||||||
|
price,
|
||||||
|
timestamp,
|
||||||
|
source_identifier,
|
||||||
|
confidence_interval,
|
||||||
|
};
|
||||||
|
let post_states =
|
||||||
|
mock_oracle_program::set_price::set_price(price_account, ctx.self_program_id, &update);
|
||||||
|
Ok(spel_framework::SpelOutput::execute(post_states, vec![]))
|
||||||
|
}
|
||||||
|
}
|
||||||
11
mock_oracle/methods/src/lib.rs
Normal file
11
mock_oracle/methods/src/lib.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
//! Host-side embedding of the mock oracle RISC Zero guest ELF.
|
||||||
|
//!
|
||||||
|
//! Re-exports the constants produced by `build.rs` via `risc0_build::embed_methods`:
|
||||||
|
//! `MOCK_ORACLE_ELF`, `MOCK_ORACLE_PATH`, and `MOCK_ORACLE_ID`.
|
||||||
|
|
||||||
|
#![allow(
|
||||||
|
missing_docs,
|
||||||
|
reason = "constants below are generated by risc0_build::embed_methods at build time"
|
||||||
|
)]
|
||||||
|
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/methods.rs"));
|
||||||
9
mock_oracle/src/lib.rs
Normal file
9
mock_oracle/src/lib.rs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
//! The Mock Oracle Program implementation.
|
||||||
|
|
||||||
|
pub use mock_oracle_core as core;
|
||||||
|
|
||||||
|
/// Write prices into mock oracle feed accounts.
|
||||||
|
pub mod set_price;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
44
mock_oracle/src/set_price.rs
Normal file
44
mock_oracle/src/set_price.rs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
use mock_oracle_core::PriceUpdate;
|
||||||
|
use nssa_core::{
|
||||||
|
account::{Account, AccountWithMetadata, Data},
|
||||||
|
program::{AccountPostState, ProgramId},
|
||||||
|
};
|
||||||
|
use oracle_core::OraclePriceAccount;
|
||||||
|
|
||||||
|
/// Write `update` into a mock oracle price account.
|
||||||
|
///
|
||||||
|
/// This program is a test/development feed producer. It intentionally does not verify an
|
||||||
|
/// upstream oracle source; it only materializes the canonical price-account shape expected
|
||||||
|
/// by stablecoin consumers.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// - `price_account` is not authorized.
|
||||||
|
/// - `price_account` is initialized under a different program.
|
||||||
|
/// - `update.price` is zero.
|
||||||
|
/// - `update.source_identifier` is empty.
|
||||||
|
pub fn set_price(
|
||||||
|
price_account: AccountWithMetadata,
|
||||||
|
mock_oracle_program_id: ProgramId,
|
||||||
|
update: &PriceUpdate,
|
||||||
|
) -> Vec<AccountPostState> {
|
||||||
|
assert!(
|
||||||
|
price_account.is_authorized,
|
||||||
|
"Price account authorization is missing"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
price_account.account == Account::default()
|
||||||
|
|| price_account.account.program_owner == mock_oracle_program_id,
|
||||||
|
"Price account must be uninitialized or owned by the Mock Oracle Program"
|
||||||
|
);
|
||||||
|
assert!(update.price > 0, "Price must be non-zero");
|
||||||
|
assert!(
|
||||||
|
!update.source_identifier.is_empty(),
|
||||||
|
"Source identifier must not be empty"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut price_post = price_account.account;
|
||||||
|
price_post.program_owner = mock_oracle_program_id;
|
||||||
|
price_post.data = Data::from(&OraclePriceAccount::from(update));
|
||||||
|
|
||||||
|
vec![AccountPostState::new(price_post)]
|
||||||
|
}
|
||||||
140
mock_oracle/src/tests.rs
Normal file
140
mock_oracle/src/tests.rs
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
#![allow(
|
||||||
|
clippy::panic,
|
||||||
|
clippy::unwrap_used,
|
||||||
|
reason = "tests deliberately panic on rejected state and unwrap decoded fixtures"
|
||||||
|
)]
|
||||||
|
|
||||||
|
use mock_oracle_core::{PriceUpdate, MOCK_ORACLE_SOURCE_IDENTIFIER};
|
||||||
|
use nssa_core::{
|
||||||
|
account::{Account, AccountId, AccountWithMetadata, Data, Nonce},
|
||||||
|
program::ProgramId,
|
||||||
|
};
|
||||||
|
use oracle_core::OraclePriceAccount;
|
||||||
|
|
||||||
|
const MOCK_ORACLE_PROGRAM_ID: ProgramId = [4u32; 8];
|
||||||
|
const FOREIGN_PROGRAM_ID: ProgramId = [9u32; 8];
|
||||||
|
|
||||||
|
fn price_account_id() -> AccountId {
|
||||||
|
AccountId::new([0x40u8; 32])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_asset_id() -> AccountId {
|
||||||
|
AccountId::new([0x50u8; 32])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quote_asset_id() -> AccountId {
|
||||||
|
AccountId::new([0x60u8; 32])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(price: u128) -> PriceUpdate {
|
||||||
|
PriceUpdate {
|
||||||
|
base_asset: base_asset_id(),
|
||||||
|
quote_asset: quote_asset_id(),
|
||||||
|
price,
|
||||||
|
timestamp: 1_000,
|
||||||
|
source_identifier: MOCK_ORACLE_SOURCE_IDENTIFIER.to_owned(),
|
||||||
|
confidence_interval: 5,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn uninitialized_price_account() -> AccountWithMetadata {
|
||||||
|
AccountWithMetadata {
|
||||||
|
account: Account::default(),
|
||||||
|
is_authorized: true,
|
||||||
|
account_id: price_account_id(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initialized_price_account(price: u128) -> AccountWithMetadata {
|
||||||
|
AccountWithMetadata {
|
||||||
|
account: Account {
|
||||||
|
program_owner: MOCK_ORACLE_PROGRAM_ID,
|
||||||
|
balance: 0,
|
||||||
|
data: Data::from(&OraclePriceAccount::from(&update(price))),
|
||||||
|
nonce: Nonce(0),
|
||||||
|
},
|
||||||
|
is_authorized: true,
|
||||||
|
account_id: price_account_id(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_price_initializes_authorized_price_account() {
|
||||||
|
let price_update = update(1_050_000);
|
||||||
|
|
||||||
|
let post_states = crate::set_price::set_price(
|
||||||
|
uninitialized_price_account(),
|
||||||
|
MOCK_ORACLE_PROGRAM_ID,
|
||||||
|
&price_update,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(post_states.len(), 1);
|
||||||
|
let post_account = post_states[0].account();
|
||||||
|
assert_eq!(post_account.program_owner, MOCK_ORACLE_PROGRAM_ID);
|
||||||
|
|
||||||
|
let price_account =
|
||||||
|
OraclePriceAccount::try_from(&post_account.data).expect("valid oracle price account");
|
||||||
|
assert_eq!(price_account, OraclePriceAccount::from(&price_update));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_price_updates_existing_mock_oracle_account() {
|
||||||
|
let price_update = update(1_100_000);
|
||||||
|
|
||||||
|
let post_states = crate::set_price::set_price(
|
||||||
|
initialized_price_account(1_050_000),
|
||||||
|
MOCK_ORACLE_PROGRAM_ID,
|
||||||
|
&price_update,
|
||||||
|
);
|
||||||
|
|
||||||
|
let price_account = OraclePriceAccount::try_from(&post_states[0].account().data)
|
||||||
|
.expect("valid oracle price account");
|
||||||
|
assert_eq!(price_account.price, 1_100_000);
|
||||||
|
assert_eq!(
|
||||||
|
price_account.source_identifier,
|
||||||
|
MOCK_ORACLE_SOURCE_IDENTIFIER
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "Price account authorization is missing")]
|
||||||
|
fn set_price_requires_price_account_authorization() {
|
||||||
|
let mut account = uninitialized_price_account();
|
||||||
|
account.is_authorized = false;
|
||||||
|
|
||||||
|
crate::set_price::set_price(account, MOCK_ORACLE_PROGRAM_ID, &update(1_050_000));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(
|
||||||
|
expected = "Price account must be uninitialized or owned by the Mock Oracle Program"
|
||||||
|
)]
|
||||||
|
fn set_price_rejects_foreign_owned_account() {
|
||||||
|
let mut account = initialized_price_account(1_050_000);
|
||||||
|
account.account.program_owner = FOREIGN_PROGRAM_ID;
|
||||||
|
|
||||||
|
crate::set_price::set_price(account, MOCK_ORACLE_PROGRAM_ID, &update(1_050_000));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "Price must be non-zero")]
|
||||||
|
fn set_price_rejects_zero_price() {
|
||||||
|
crate::set_price::set_price(
|
||||||
|
uninitialized_price_account(),
|
||||||
|
MOCK_ORACLE_PROGRAM_ID,
|
||||||
|
&update(0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "Source identifier must not be empty")]
|
||||||
|
fn set_price_rejects_empty_source_identifier() {
|
||||||
|
let mut price_update = update(1_050_000);
|
||||||
|
price_update.source_identifier.clear();
|
||||||
|
|
||||||
|
crate::set_price::set_price(
|
||||||
|
uninitialized_price_account(),
|
||||||
|
MOCK_ORACLE_PROGRAM_ID,
|
||||||
|
&price_update,
|
||||||
|
);
|
||||||
|
}
|
||||||
10
oracle/core/Cargo.toml
Normal file
10
oracle/core/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
[package]
|
||||||
|
name = "oracle_core"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc3", features = ["host"] }
|
||||||
|
borsh = { version = "1.5", features = ["derive"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
spel-framework-macros = { git = "https://github.com/logos-co/spel.git", rev = "6473ab4c400bc59bac8db83a286faaeafa7d1999", package = "spel-framework-macros" }
|
||||||
402
oracle/core/src/lib.rs
Normal file
402
oracle/core/src/lib.rs
Normal file
@ -0,0 +1,402 @@
|
|||||||
|
//! Shared price-feed account structures and consumer helpers.
|
||||||
|
|
||||||
|
use borsh::{BorshDeserialize, BorshSerialize};
|
||||||
|
use nssa_core::account::{Account, AccountId, AccountWithMetadata, Data};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use spel_framework_macros::account_type;
|
||||||
|
|
||||||
|
/// Canonical oracle price account consumed by LEZ programs.
|
||||||
|
///
|
||||||
|
/// Oracle producers own how this account is written; consumers only read and validate it.
|
||||||
|
#[account_type]
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||||
|
pub struct OraclePriceAccount {
|
||||||
|
/// Canonical identifier for the priced asset.
|
||||||
|
pub base_asset: AccountId,
|
||||||
|
/// Canonical identifier for the quote asset that denominates `price`.
|
||||||
|
pub quote_asset: AccountId,
|
||||||
|
/// Amount of `quote_asset` one unit of `base_asset` is worth.
|
||||||
|
///
|
||||||
|
/// `u128` keeps the consumer-side interface non-negative; zero is rejected on read.
|
||||||
|
pub price: u128,
|
||||||
|
/// Price observation timestamp. Consumers choose the time unit by matching this with
|
||||||
|
/// `max_age`.
|
||||||
|
pub timestamp: u64,
|
||||||
|
/// Identifier of the source that populated this account, such as a TWAP or external adaptor.
|
||||||
|
pub source_identifier: String,
|
||||||
|
/// Source-provided confidence interval, or zero when the source does not provide one.
|
||||||
|
pub confidence_interval: u128,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Price-feed configuration owned by the consuming protocol.
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
pub struct PriceFeedConfig {
|
||||||
|
/// External oracle account expected for this feed.
|
||||||
|
pub feed_account_id: AccountId,
|
||||||
|
/// Asset the consumer expects to price.
|
||||||
|
pub expected_base_asset: AccountId,
|
||||||
|
/// Quote asset expected by the consumer.
|
||||||
|
pub expected_quote_asset: AccountId,
|
||||||
|
/// Maximum accepted age for a price observation.
|
||||||
|
pub max_age: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validated price-feed reading returned to consumer logic.
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct PriceFeedReading {
|
||||||
|
pub base_asset: AccountId,
|
||||||
|
pub quote_asset: AccountId,
|
||||||
|
pub price: u128,
|
||||||
|
pub timestamp: u64,
|
||||||
|
pub source_identifier: String,
|
||||||
|
pub confidence_interval: u128,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recoverable price-feed failures. Stale or unavailable feeds should pause dependent actions.
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
|
pub enum PriceFeedError {
|
||||||
|
/// The supplied account is not the configured feed account.
|
||||||
|
FeedAccountMismatch {
|
||||||
|
expected: AccountId,
|
||||||
|
actual: AccountId,
|
||||||
|
},
|
||||||
|
/// The configured feed account is not initialized.
|
||||||
|
FeedUnavailable,
|
||||||
|
/// The feed account data does not match [`OraclePriceAccount`].
|
||||||
|
DecodeFailed,
|
||||||
|
/// The feed account does not price the configured asset pair.
|
||||||
|
AssetPairMismatch,
|
||||||
|
/// The feed reported a zero price.
|
||||||
|
InvalidPrice,
|
||||||
|
/// The feed timestamp is newer than the caller's current timestamp.
|
||||||
|
PriceTimestampInFuture {
|
||||||
|
current_timestamp: u64,
|
||||||
|
price_timestamp: u64,
|
||||||
|
},
|
||||||
|
/// The feed is older than the configured maximum age.
|
||||||
|
StalePrice {
|
||||||
|
current_timestamp: u64,
|
||||||
|
price_timestamp: u64,
|
||||||
|
max_age: u64,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Swappable price-feed interface for production oracle accounts and tests.
|
||||||
|
pub trait PriceFeed {
|
||||||
|
/// Read and validate the current market price for the configured asset pair.
|
||||||
|
fn read_market_price(
|
||||||
|
&self,
|
||||||
|
config: &PriceFeedConfig,
|
||||||
|
current_timestamp: u64,
|
||||||
|
) -> Result<PriceFeedReading, PriceFeedError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Price-feed implementation backed by an external oracle account.
|
||||||
|
pub struct AccountPriceFeed<'a> {
|
||||||
|
feed_account: &'a AccountWithMetadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> AccountPriceFeed<'a> {
|
||||||
|
pub fn new(feed_account: &'a AccountWithMetadata) -> Self {
|
||||||
|
Self { feed_account }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PriceFeed for AccountPriceFeed<'_> {
|
||||||
|
fn read_market_price(
|
||||||
|
&self,
|
||||||
|
config: &PriceFeedConfig,
|
||||||
|
current_timestamp: u64,
|
||||||
|
) -> Result<PriceFeedReading, PriceFeedError> {
|
||||||
|
if self.feed_account.account_id != config.feed_account_id {
|
||||||
|
return Err(PriceFeedError::FeedAccountMismatch {
|
||||||
|
expected: config.feed_account_id,
|
||||||
|
actual: self.feed_account.account_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if self.feed_account.account == Account::default() {
|
||||||
|
return Err(PriceFeedError::FeedUnavailable);
|
||||||
|
}
|
||||||
|
|
||||||
|
let price_account = OraclePriceAccount::try_from(&self.feed_account.account.data)
|
||||||
|
.map_err(|_| PriceFeedError::DecodeFailed)?;
|
||||||
|
validate_price_account(&price_account, config, current_timestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate a decoded oracle price account using consumer-owned policy.
|
||||||
|
pub fn validate_price_account(
|
||||||
|
price_account: &OraclePriceAccount,
|
||||||
|
config: &PriceFeedConfig,
|
||||||
|
current_timestamp: u64,
|
||||||
|
) -> Result<PriceFeedReading, PriceFeedError> {
|
||||||
|
if price_account.base_asset != config.expected_base_asset
|
||||||
|
|| price_account.quote_asset != config.expected_quote_asset
|
||||||
|
{
|
||||||
|
return Err(PriceFeedError::AssetPairMismatch);
|
||||||
|
}
|
||||||
|
if price_account.price == 0 {
|
||||||
|
return Err(PriceFeedError::InvalidPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
let age = current_timestamp
|
||||||
|
.checked_sub(price_account.timestamp)
|
||||||
|
.ok_or(PriceFeedError::PriceTimestampInFuture {
|
||||||
|
current_timestamp,
|
||||||
|
price_timestamp: price_account.timestamp,
|
||||||
|
})?;
|
||||||
|
if age > config.max_age {
|
||||||
|
return Err(PriceFeedError::StalePrice {
|
||||||
|
current_timestamp,
|
||||||
|
price_timestamp: price_account.timestamp,
|
||||||
|
max_age: config.max_age,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(PriceFeedReading {
|
||||||
|
base_asset: price_account.base_asset,
|
||||||
|
quote_asset: price_account.quote_asset,
|
||||||
|
price: price_account.price,
|
||||||
|
timestamp: price_account.timestamp,
|
||||||
|
source_identifier: price_account.source_identifier.clone(),
|
||||||
|
confidence_interval: price_account.confidence_interval,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&Data> for OraclePriceAccount {
|
||||||
|
type Error = borsh::io::Error;
|
||||||
|
|
||||||
|
fn try_from(data: &Data) -> Result<Self, Self::Error> {
|
||||||
|
Self::try_from_slice(data.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&OraclePriceAccount> for Data {
|
||||||
|
fn from(price_account: &OraclePriceAccount) -> Self {
|
||||||
|
let mut data = Vec::new();
|
||||||
|
BorshSerialize::serialize(price_account, &mut data)
|
||||||
|
.expect("Serialization to Vec should not fail");
|
||||||
|
Self::try_from(data).expect("Oracle price account encoded data should fit into Data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use nssa_core::{
|
||||||
|
account::{AccountWithMetadata, Nonce},
|
||||||
|
program::ProgramId,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const ORACLE_PROGRAM_ID: ProgramId = [4u32; 8];
|
||||||
|
|
||||||
|
fn feed_account_id() -> AccountId {
|
||||||
|
AccountId::new([0x40u8; 32])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stablecoin_asset_id() -> AccountId {
|
||||||
|
AccountId::new([0x50u8; 32])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usd_asset_id() -> AccountId {
|
||||||
|
AccountId::new([0x60u8; 32])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn price_account(price: u128, timestamp: u64) -> OraclePriceAccount {
|
||||||
|
OraclePriceAccount {
|
||||||
|
base_asset: stablecoin_asset_id(),
|
||||||
|
quote_asset: usd_asset_id(),
|
||||||
|
price,
|
||||||
|
timestamp,
|
||||||
|
source_identifier: "mock-oracle".to_owned(),
|
||||||
|
confidence_interval: 5,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config(max_age: u64) -> PriceFeedConfig {
|
||||||
|
PriceFeedConfig {
|
||||||
|
feed_account_id: feed_account_id(),
|
||||||
|
expected_base_asset: stablecoin_asset_id(),
|
||||||
|
expected_quote_asset: usd_asset_id(),
|
||||||
|
max_age,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn feed_account(price_account: &OraclePriceAccount) -> AccountWithMetadata {
|
||||||
|
AccountWithMetadata {
|
||||||
|
account: Account {
|
||||||
|
program_owner: ORACLE_PROGRAM_ID,
|
||||||
|
balance: 0,
|
||||||
|
data: Data::from(price_account),
|
||||||
|
nonce: Nonce(0),
|
||||||
|
},
|
||||||
|
is_authorized: false,
|
||||||
|
account_id: feed_account_id(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MockPriceFeed {
|
||||||
|
feed_account_id: AccountId,
|
||||||
|
price_account: Option<OraclePriceAccount>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MockPriceFeed {
|
||||||
|
fn available(price_account: OraclePriceAccount) -> Self {
|
||||||
|
Self {
|
||||||
|
feed_account_id: feed_account_id(),
|
||||||
|
price_account: Some(price_account),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PriceFeed for MockPriceFeed {
|
||||||
|
fn read_market_price(
|
||||||
|
&self,
|
||||||
|
config: &PriceFeedConfig,
|
||||||
|
current_timestamp: u64,
|
||||||
|
) -> Result<PriceFeedReading, PriceFeedError> {
|
||||||
|
if self.feed_account_id != config.feed_account_id {
|
||||||
|
return Err(PriceFeedError::FeedAccountMismatch {
|
||||||
|
expected: config.feed_account_id,
|
||||||
|
actual: self.feed_account_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let price_account = self
|
||||||
|
.price_account
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(PriceFeedError::FeedUnavailable)?;
|
||||||
|
validate_price_account(price_account, config, current_timestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn account_price_feed_reads_fresh_market_price() {
|
||||||
|
let oracle_account = price_account(1_050_000, 1_000);
|
||||||
|
let feed_account = feed_account(&oracle_account);
|
||||||
|
|
||||||
|
let reading = AccountPriceFeed::new(&feed_account)
|
||||||
|
.read_market_price(&config(60), 1_030)
|
||||||
|
.expect("fresh price should read");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
reading,
|
||||||
|
PriceFeedReading {
|
||||||
|
base_asset: stablecoin_asset_id(),
|
||||||
|
quote_asset: usd_asset_id(),
|
||||||
|
price: 1_050_000,
|
||||||
|
timestamp: 1_000,
|
||||||
|
source_identifier: "mock-oracle".to_owned(),
|
||||||
|
confidence_interval: 5,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn account_price_feed_rejects_wrong_feed_account() {
|
||||||
|
let oracle_account = price_account(1_050_000, 1_000);
|
||||||
|
let mut feed_account = feed_account(&oracle_account);
|
||||||
|
feed_account.account_id = AccountId::new([0x41u8; 32]);
|
||||||
|
|
||||||
|
let error = AccountPriceFeed::new(&feed_account)
|
||||||
|
.read_market_price(&config(60), 1_030)
|
||||||
|
.expect_err("wrong account must fail");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
error,
|
||||||
|
PriceFeedError::FeedAccountMismatch {
|
||||||
|
expected: feed_account_id(),
|
||||||
|
actual: AccountId::new([0x41u8; 32]),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn account_price_feed_rejects_stale_price() {
|
||||||
|
let oracle_account = price_account(1_050_000, 1_000);
|
||||||
|
let feed_account = feed_account(&oracle_account);
|
||||||
|
|
||||||
|
let error = AccountPriceFeed::new(&feed_account)
|
||||||
|
.read_market_price(&config(60), 1_061)
|
||||||
|
.expect_err("stale price must pause dependent actions");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
error,
|
||||||
|
PriceFeedError::StalePrice {
|
||||||
|
current_timestamp: 1_061,
|
||||||
|
price_timestamp: 1_000,
|
||||||
|
max_age: 60,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn account_price_feed_rejects_asset_pair_mismatch() {
|
||||||
|
let mut oracle_account = price_account(1_050_000, 1_000);
|
||||||
|
oracle_account.quote_asset = AccountId::new([0x61u8; 32]);
|
||||||
|
let feed_account = feed_account(&oracle_account);
|
||||||
|
|
||||||
|
let error = AccountPriceFeed::new(&feed_account)
|
||||||
|
.read_market_price(&config(60), 1_030)
|
||||||
|
.expect_err("wrong pair must fail");
|
||||||
|
|
||||||
|
assert_eq!(error, PriceFeedError::AssetPairMismatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn account_price_feed_rejects_zero_price() {
|
||||||
|
let oracle_account = price_account(0, 1_000);
|
||||||
|
let feed_account = feed_account(&oracle_account);
|
||||||
|
|
||||||
|
let error = AccountPriceFeed::new(&feed_account)
|
||||||
|
.read_market_price(&config(60), 1_030)
|
||||||
|
.expect_err("zero price must fail");
|
||||||
|
|
||||||
|
assert_eq!(error, PriceFeedError::InvalidPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn account_price_feed_rejects_future_timestamp() {
|
||||||
|
let oracle_account = price_account(1_050_000, 1_050);
|
||||||
|
let feed_account = feed_account(&oracle_account);
|
||||||
|
|
||||||
|
let error = AccountPriceFeed::new(&feed_account)
|
||||||
|
.read_market_price(&config(60), 1_030)
|
||||||
|
.expect_err("future timestamp must fail");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
error,
|
||||||
|
PriceFeedError::PriceTimestampInFuture {
|
||||||
|
current_timestamp: 1_030,
|
||||||
|
price_timestamp: 1_050,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn account_price_feed_rejects_unavailable_feed_account() {
|
||||||
|
let feed_account = AccountWithMetadata {
|
||||||
|
account: Account::default(),
|
||||||
|
is_authorized: false,
|
||||||
|
account_id: feed_account_id(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let error = AccountPriceFeed::new(&feed_account)
|
||||||
|
.read_market_price(&config(60), 1_030)
|
||||||
|
.expect_err("missing feed must fail");
|
||||||
|
|
||||||
|
assert_eq!(error, PriceFeedError::FeedUnavailable);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn price_feed_trait_accepts_mock_oracle() {
|
||||||
|
let mock_oracle = MockPriceFeed::available(price_account(1_050_000, 1_000));
|
||||||
|
|
||||||
|
let reading = mock_oracle
|
||||||
|
.read_market_price(&config(60), 1_030)
|
||||||
|
.expect("mock oracle should use the same consumer checks");
|
||||||
|
|
||||||
|
assert_eq!(reading.price, 1_050_000);
|
||||||
|
assert_eq!(reading.source_identifier, "mock-oracle");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,5 +7,6 @@ edition = "2021"
|
|||||||
nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc3", features = ["host"] }
|
nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc3", features = ["host"] }
|
||||||
borsh = { version = "1.5", features = ["derive"] }
|
borsh = { version = "1.5", features = ["derive"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
oracle_core = { path = "../../oracle/core" }
|
||||||
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" }
|
spel-framework-macros = { git = "https://github.com/logos-co/spel.git", rev = "6473ab4c400bc59bac8db83a286faaeafa7d1999", package = "spel-framework-macros" }
|
||||||
|
|||||||
@ -2,9 +2,13 @@
|
|||||||
|
|
||||||
use borsh::{BorshDeserialize, BorshSerialize};
|
use borsh::{BorshDeserialize, BorshSerialize};
|
||||||
use nssa_core::{
|
use nssa_core::{
|
||||||
account::{Account, AccountId, AccountWithMetadata, Data},
|
account::{AccountId, AccountWithMetadata, Data},
|
||||||
program::{PdaSeed, ProgramId},
|
program::{PdaSeed, ProgramId},
|
||||||
};
|
};
|
||||||
|
pub use oracle_core::{
|
||||||
|
validate_price_account, AccountPriceFeed, OraclePriceAccount, PriceFeed, PriceFeedConfig,
|
||||||
|
PriceFeedError, PriceFeedReading,
|
||||||
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use spel_framework_macros::account_type;
|
use spel_framework_macros::account_type;
|
||||||
|
|
||||||
@ -49,165 +53,6 @@ pub struct Position {
|
|||||||
pub debt_amount: u128,
|
pub debt_amount: u128,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Canonical oracle price account consumed by the Stablecoin Program.
|
|
||||||
///
|
|
||||||
/// This mirrors the shared oracle shape from the RFP-019/RFP-020 documentation. Oracle
|
|
||||||
/// producers own how this account is written; stablecoin code only reads and validates it.
|
|
||||||
#[account_type]
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
|
||||||
pub struct OraclePriceAccount {
|
|
||||||
/// Canonical identifier for the priced asset.
|
|
||||||
pub base_asset: AccountId,
|
|
||||||
/// Canonical identifier for the quote asset that denominates `price`.
|
|
||||||
pub quote_asset: AccountId,
|
|
||||||
/// Amount of `quote_asset` one unit of `base_asset` is worth.
|
|
||||||
///
|
|
||||||
/// `u128` keeps the consumer-side interface non-negative; zero is rejected on read.
|
|
||||||
pub price: u128,
|
|
||||||
/// Price observation timestamp. Consumers choose the time unit by matching this with
|
|
||||||
/// `max_age`.
|
|
||||||
pub timestamp: u64,
|
|
||||||
/// Identifier of the source that populated this account, such as a TWAP or external adaptor.
|
|
||||||
pub source_identifier: String,
|
|
||||||
/// Source-provided confidence interval, or zero when the source does not provide one.
|
|
||||||
pub confidence_interval: u128,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stablecoin price-feed configuration owned by the consuming protocol.
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
|
|
||||||
pub struct PriceFeedConfig {
|
|
||||||
/// External oracle account expected for this feed.
|
|
||||||
pub feed_account_id: AccountId,
|
|
||||||
/// Asset the stablecoin controller expects to price.
|
|
||||||
pub expected_base_asset: AccountId,
|
|
||||||
/// Quote asset expected by the stablecoin controller.
|
|
||||||
pub expected_quote_asset: AccountId,
|
|
||||||
/// Maximum accepted age for a price observation.
|
|
||||||
pub max_age: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validated price-feed reading returned to stablecoin controller logic.
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct PriceFeedReading {
|
|
||||||
pub base_asset: AccountId,
|
|
||||||
pub quote_asset: AccountId,
|
|
||||||
pub price: u128,
|
|
||||||
pub timestamp: u64,
|
|
||||||
pub source_identifier: String,
|
|
||||||
pub confidence_interval: u128,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Recoverable price-feed failures. Stale or unavailable feeds should pause rate updates.
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
|
||||||
pub enum PriceFeedError {
|
|
||||||
/// The supplied account is not the configured feed account.
|
|
||||||
FeedAccountMismatch {
|
|
||||||
expected: AccountId,
|
|
||||||
actual: AccountId,
|
|
||||||
},
|
|
||||||
/// The configured feed account is not initialized.
|
|
||||||
FeedUnavailable,
|
|
||||||
/// The feed account data does not match [`OraclePriceAccount`].
|
|
||||||
DecodeFailed,
|
|
||||||
/// The feed account does not price the configured asset pair.
|
|
||||||
AssetPairMismatch,
|
|
||||||
/// The feed reported a zero price.
|
|
||||||
InvalidPrice,
|
|
||||||
/// The feed timestamp is newer than the caller's current timestamp.
|
|
||||||
PriceTimestampInFuture {
|
|
||||||
current_timestamp: u64,
|
|
||||||
price_timestamp: u64,
|
|
||||||
},
|
|
||||||
/// The feed is older than the configured maximum age.
|
|
||||||
StalePrice {
|
|
||||||
current_timestamp: u64,
|
|
||||||
price_timestamp: u64,
|
|
||||||
max_age: u64,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Swappable price-feed interface for production oracle accounts and tests.
|
|
||||||
pub trait PriceFeed {
|
|
||||||
/// Read and validate the current market price for the configured asset pair.
|
|
||||||
fn read_market_price(
|
|
||||||
&self,
|
|
||||||
config: &PriceFeedConfig,
|
|
||||||
current_timestamp: u64,
|
|
||||||
) -> Result<PriceFeedReading, PriceFeedError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Price-feed implementation backed by an external oracle account.
|
|
||||||
pub struct AccountPriceFeed<'a> {
|
|
||||||
feed_account: &'a AccountWithMetadata,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> AccountPriceFeed<'a> {
|
|
||||||
pub fn new(feed_account: &'a AccountWithMetadata) -> Self {
|
|
||||||
Self { feed_account }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PriceFeed for AccountPriceFeed<'_> {
|
|
||||||
fn read_market_price(
|
|
||||||
&self,
|
|
||||||
config: &PriceFeedConfig,
|
|
||||||
current_timestamp: u64,
|
|
||||||
) -> Result<PriceFeedReading, PriceFeedError> {
|
|
||||||
if self.feed_account.account_id != config.feed_account_id {
|
|
||||||
return Err(PriceFeedError::FeedAccountMismatch {
|
|
||||||
expected: config.feed_account_id,
|
|
||||||
actual: self.feed_account.account_id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if self.feed_account.account == Account::default() {
|
|
||||||
return Err(PriceFeedError::FeedUnavailable);
|
|
||||||
}
|
|
||||||
|
|
||||||
let price_account = OraclePriceAccount::try_from(&self.feed_account.account.data)
|
|
||||||
.map_err(|_| PriceFeedError::DecodeFailed)?;
|
|
||||||
validate_price_account(&price_account, config, current_timestamp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validate a decoded oracle price account using consumer-owned policy.
|
|
||||||
pub fn validate_price_account(
|
|
||||||
price_account: &OraclePriceAccount,
|
|
||||||
config: &PriceFeedConfig,
|
|
||||||
current_timestamp: u64,
|
|
||||||
) -> Result<PriceFeedReading, PriceFeedError> {
|
|
||||||
if price_account.base_asset != config.expected_base_asset
|
|
||||||
|| price_account.quote_asset != config.expected_quote_asset
|
|
||||||
{
|
|
||||||
return Err(PriceFeedError::AssetPairMismatch);
|
|
||||||
}
|
|
||||||
if price_account.price == 0 {
|
|
||||||
return Err(PriceFeedError::InvalidPrice);
|
|
||||||
}
|
|
||||||
|
|
||||||
let age = current_timestamp
|
|
||||||
.checked_sub(price_account.timestamp)
|
|
||||||
.ok_or(PriceFeedError::PriceTimestampInFuture {
|
|
||||||
current_timestamp,
|
|
||||||
price_timestamp: price_account.timestamp,
|
|
||||||
})?;
|
|
||||||
if age > config.max_age {
|
|
||||||
return Err(PriceFeedError::StalePrice {
|
|
||||||
current_timestamp,
|
|
||||||
price_timestamp: price_account.timestamp,
|
|
||||||
max_age: config.max_age,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(PriceFeedReading {
|
|
||||||
base_asset: price_account.base_asset,
|
|
||||||
quote_asset: price_account.quote_asset,
|
|
||||||
price: price_account.price,
|
|
||||||
timestamp: price_account.timestamp,
|
|
||||||
source_identifier: price_account.source_identifier.clone(),
|
|
||||||
confidence_interval: price_account.confidence_interval,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<&Data> for Position {
|
impl TryFrom<&Data> for Position {
|
||||||
type Error = std::io::Error;
|
type Error = std::io::Error;
|
||||||
|
|
||||||
@ -225,23 +70,6 @@ impl From<&Position> for Data {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&Data> for OraclePriceAccount {
|
|
||||||
type Error = std::io::Error;
|
|
||||||
|
|
||||||
fn try_from(data: &Data) -> Result<Self, Self::Error> {
|
|
||||||
Self::try_from_slice(data.as_ref())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&OraclePriceAccount> for Data {
|
|
||||||
fn from(price_account: &OraclePriceAccount) -> Self {
|
|
||||||
let mut data = Vec::with_capacity(std::mem::size_of_val(price_account));
|
|
||||||
BorshSerialize::serialize(price_account, &mut data)
|
|
||||||
.expect("Serialization to Vec should not fail");
|
|
||||||
Self::try_from(data).expect("Oracle price account encoded data should fit into Data")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// PDA seed for the [`Position`] account owned by `owner_id` for `collateral_definition_id`.
|
/// PDA seed for the [`Position`] account owned by `owner_id` for `collateral_definition_id`.
|
||||||
///
|
///
|
||||||
/// Derived from the owner and collateral definition addresses with a domain-separation tag
|
/// Derived from the owner and collateral definition addresses with a domain-separation tag
|
||||||
@ -342,221 +170,3 @@ pub fn verify_position_vault_and_get_seed(
|
|||||||
);
|
);
|
||||||
seed
|
seed
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use nssa_core::account::Nonce;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
const ORACLE_PROGRAM_ID: ProgramId = [4u32; 8];
|
|
||||||
|
|
||||||
fn feed_account_id() -> AccountId {
|
|
||||||
AccountId::new([0x40u8; 32])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stablecoin_asset_id() -> AccountId {
|
|
||||||
AccountId::new([0x50u8; 32])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn usd_asset_id() -> AccountId {
|
|
||||||
AccountId::new([0x60u8; 32])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn price_account(price: u128, timestamp: u64) -> OraclePriceAccount {
|
|
||||||
OraclePriceAccount {
|
|
||||||
base_asset: stablecoin_asset_id(),
|
|
||||||
quote_asset: usd_asset_id(),
|
|
||||||
price,
|
|
||||||
timestamp,
|
|
||||||
source_identifier: "mock-oracle".to_owned(),
|
|
||||||
confidence_interval: 5,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn config(max_age: u64) -> PriceFeedConfig {
|
|
||||||
PriceFeedConfig {
|
|
||||||
feed_account_id: feed_account_id(),
|
|
||||||
expected_base_asset: stablecoin_asset_id(),
|
|
||||||
expected_quote_asset: usd_asset_id(),
|
|
||||||
max_age,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn feed_account(price_account: &OraclePriceAccount) -> AccountWithMetadata {
|
|
||||||
AccountWithMetadata {
|
|
||||||
account: Account {
|
|
||||||
program_owner: ORACLE_PROGRAM_ID,
|
|
||||||
balance: 0,
|
|
||||||
data: Data::from(price_account),
|
|
||||||
nonce: Nonce(0),
|
|
||||||
},
|
|
||||||
is_authorized: false,
|
|
||||||
account_id: feed_account_id(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MockPriceFeed {
|
|
||||||
feed_account_id: AccountId,
|
|
||||||
price_account: Option<OraclePriceAccount>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MockPriceFeed {
|
|
||||||
fn available(price_account: OraclePriceAccount) -> Self {
|
|
||||||
Self {
|
|
||||||
feed_account_id: feed_account_id(),
|
|
||||||
price_account: Some(price_account),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PriceFeed for MockPriceFeed {
|
|
||||||
fn read_market_price(
|
|
||||||
&self,
|
|
||||||
config: &PriceFeedConfig,
|
|
||||||
current_timestamp: u64,
|
|
||||||
) -> Result<PriceFeedReading, PriceFeedError> {
|
|
||||||
if self.feed_account_id != config.feed_account_id {
|
|
||||||
return Err(PriceFeedError::FeedAccountMismatch {
|
|
||||||
expected: config.feed_account_id,
|
|
||||||
actual: self.feed_account_id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let price_account = self
|
|
||||||
.price_account
|
|
||||||
.as_ref()
|
|
||||||
.ok_or(PriceFeedError::FeedUnavailable)?;
|
|
||||||
validate_price_account(price_account, config, current_timestamp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn account_price_feed_reads_fresh_market_price() {
|
|
||||||
let oracle_account = price_account(1_050_000, 1_000);
|
|
||||||
let feed_account = feed_account(&oracle_account);
|
|
||||||
|
|
||||||
let reading = AccountPriceFeed::new(&feed_account)
|
|
||||||
.read_market_price(&config(60), 1_030)
|
|
||||||
.expect("fresh price should read");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
reading,
|
|
||||||
PriceFeedReading {
|
|
||||||
base_asset: stablecoin_asset_id(),
|
|
||||||
quote_asset: usd_asset_id(),
|
|
||||||
price: 1_050_000,
|
|
||||||
timestamp: 1_000,
|
|
||||||
source_identifier: "mock-oracle".to_owned(),
|
|
||||||
confidence_interval: 5,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn account_price_feed_rejects_wrong_feed_account() {
|
|
||||||
let oracle_account = price_account(1_050_000, 1_000);
|
|
||||||
let mut feed_account = feed_account(&oracle_account);
|
|
||||||
feed_account.account_id = AccountId::new([0x41u8; 32]);
|
|
||||||
|
|
||||||
let error = AccountPriceFeed::new(&feed_account)
|
|
||||||
.read_market_price(&config(60), 1_030)
|
|
||||||
.expect_err("wrong account must fail");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
error,
|
|
||||||
PriceFeedError::FeedAccountMismatch {
|
|
||||||
expected: feed_account_id(),
|
|
||||||
actual: AccountId::new([0x41u8; 32]),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn account_price_feed_rejects_stale_price() {
|
|
||||||
let oracle_account = price_account(1_050_000, 1_000);
|
|
||||||
let feed_account = feed_account(&oracle_account);
|
|
||||||
|
|
||||||
let error = AccountPriceFeed::new(&feed_account)
|
|
||||||
.read_market_price(&config(60), 1_061)
|
|
||||||
.expect_err("stale price must pause rate updates");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
error,
|
|
||||||
PriceFeedError::StalePrice {
|
|
||||||
current_timestamp: 1_061,
|
|
||||||
price_timestamp: 1_000,
|
|
||||||
max_age: 60,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn account_price_feed_rejects_asset_pair_mismatch() {
|
|
||||||
let mut oracle_account = price_account(1_050_000, 1_000);
|
|
||||||
oracle_account.quote_asset = AccountId::new([0x61u8; 32]);
|
|
||||||
let feed_account = feed_account(&oracle_account);
|
|
||||||
|
|
||||||
let error = AccountPriceFeed::new(&feed_account)
|
|
||||||
.read_market_price(&config(60), 1_030)
|
|
||||||
.expect_err("wrong pair must fail");
|
|
||||||
|
|
||||||
assert_eq!(error, PriceFeedError::AssetPairMismatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn account_price_feed_rejects_zero_price() {
|
|
||||||
let oracle_account = price_account(0, 1_000);
|
|
||||||
let feed_account = feed_account(&oracle_account);
|
|
||||||
|
|
||||||
let error = AccountPriceFeed::new(&feed_account)
|
|
||||||
.read_market_price(&config(60), 1_030)
|
|
||||||
.expect_err("zero price must fail");
|
|
||||||
|
|
||||||
assert_eq!(error, PriceFeedError::InvalidPrice);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn account_price_feed_rejects_future_timestamp() {
|
|
||||||
let oracle_account = price_account(1_050_000, 1_050);
|
|
||||||
let feed_account = feed_account(&oracle_account);
|
|
||||||
|
|
||||||
let error = AccountPriceFeed::new(&feed_account)
|
|
||||||
.read_market_price(&config(60), 1_030)
|
|
||||||
.expect_err("future timestamp must fail");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
error,
|
|
||||||
PriceFeedError::PriceTimestampInFuture {
|
|
||||||
current_timestamp: 1_030,
|
|
||||||
price_timestamp: 1_050,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn account_price_feed_rejects_unavailable_feed_account() {
|
|
||||||
let feed_account = AccountWithMetadata {
|
|
||||||
account: Account::default(),
|
|
||||||
is_authorized: false,
|
|
||||||
account_id: feed_account_id(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let error = AccountPriceFeed::new(&feed_account)
|
|
||||||
.read_market_price(&config(60), 1_030)
|
|
||||||
.expect_err("missing feed must fail");
|
|
||||||
|
|
||||||
assert_eq!(error, PriceFeedError::FeedUnavailable);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn price_feed_trait_accepts_mock_oracle() {
|
|
||||||
let mock_feed = MockPriceFeed::available(price_account(1_050_000, 1_000));
|
|
||||||
|
|
||||||
let reading = mock_feed
|
|
||||||
.read_market_price(&config(60), 1_030)
|
|
||||||
.expect("mock feed should use the same consumer checks");
|
|
||||||
|
|
||||||
assert_eq!(reading.price, 1_050_000);
|
|
||||||
assert_eq!(reading.source_identifier, "mock-oracle");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
16
stablecoin/methods/guest/Cargo.lock
generated
16
stablecoin/methods/guest/Cargo.lock
generated
@ -1860,6 +1860,16 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "oracle_core"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"borsh",
|
||||||
|
"nssa_core",
|
||||||
|
"serde",
|
||||||
|
"spel-framework-macros",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
@ -2932,6 +2942,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"borsh",
|
"borsh",
|
||||||
"nssa_core",
|
"nssa_core",
|
||||||
|
"oracle_core",
|
||||||
"risc0-zkvm",
|
"risc0-zkvm",
|
||||||
"serde",
|
"serde",
|
||||||
"spel-framework",
|
"spel-framework",
|
||||||
@ -2946,6 +2957,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"borsh",
|
"borsh",
|
||||||
"nssa_core",
|
"nssa_core",
|
||||||
|
"oracle_core",
|
||||||
"risc0-zkvm",
|
"risc0-zkvm",
|
||||||
"serde",
|
"serde",
|
||||||
"spel-framework-macros",
|
"spel-framework-macros",
|
||||||
@ -3968,9 +3980,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerofrom"
|
name = "zerofrom"
|
||||||
version = "0.1.7"
|
version = "0.1.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
|
checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerofrom-derive",
|
"zerofrom-derive",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -13,6 +13,7 @@ path = "src/bin/stablecoin.rs"
|
|||||||
spel-framework = { git = "https://github.com/logos-co/spel.git", rev = "6473ab4c400bc59bac8db83a286faaeafa7d1999", package = "spel-framework" }
|
spel-framework = { git = "https://github.com/logos-co/spel.git", rev = "6473ab4c400bc59bac8db83a286faaeafa7d1999", package = "spel-framework" }
|
||||||
nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc3" }
|
nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc3" }
|
||||||
risc0-zkvm = { version = "=3.0.5", default-features = false }
|
risc0-zkvm = { version = "=3.0.5", default-features = false }
|
||||||
|
oracle_core = { path = "../../../oracle/core" }
|
||||||
stablecoin_core = { path = "../../core" }
|
stablecoin_core = { path = "../../core" }
|
||||||
stablecoin_program = { path = "../..", package = "stablecoin_program" }
|
stablecoin_program = { path = "../..", package = "stablecoin_program" }
|
||||||
token_core = { path = "../../../token/core" }
|
token_core = { path = "../../../token/core" }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user