diff --git a/Cargo.lock b/Cargo.lock index 95c058375..a47fee9da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12155,6 +12155,7 @@ dependencies = [ "solana-packet", "solana-program-option 3.1.0", "solana-program-pack 3.1.0", + "solana-program-runtime", "solana-pubkey 3.0.0", "solana-pubsub-client", "solana-rpc-client", diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 103ee8dde..85c646329 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -117,6 +117,7 @@ axum = { version = "0.8", default-features = false, features = ["tokio", "http1" [dev-dependencies] ed25519-dalek = "1.0.1" +solana-program-runtime = "4.2.1" libsecp256k1 = "0.7.2" p256 = { version = "0.13", default-features = false, features = ["ecdsa"] } test-case = { workspace = true } diff --git a/crates/core/src/scenarios/protocols/mod.rs b/crates/core/src/scenarios/protocols/mod.rs index 99f0b0967..b6af45e27 100644 --- a/crates/core/src/scenarios/protocols/mod.rs +++ b/crates/core/src/scenarios/protocols/mod.rs @@ -1 +1,2 @@ pub mod pump; +pub mod tessera; diff --git a/crates/core/src/scenarios/protocols/tessera/README.md b/crates/core/src/scenarios/protocols/tessera/README.md new file mode 100644 index 000000000..3e33c0626 --- /dev/null +++ b/crates/core/src/scenarios/protocols/tessera/README.md @@ -0,0 +1,136 @@ +# Tessera + +Tessera is a proprietary market maker that publishes no IDL. Surfpool writes its market accounts +through the raw byte layout in `v1/overrides.yaml`. It prepares state; it does not construct or +submit a swap. + +## Deployment + +- Program: `TessVdML9pBGgG9yGks7o4HewRaXVAMuoVj4x83GLQH` +- ProgramData: `BzSXM6KLDpHQQChzr7Fdgbzwp8r8zRYWFFrHK2uZmDYV` +- Upgrade authority: `7bJ9xu9UGVZPtYzH1fMwdaKdvfhqeSJtoFc2eGrXBPhK` +- Deploy slot: `446053401` +- ELF SHA-256: `82fd37995fcece47a253b1a00c1dd7e4c3fff706b2e42384110e2cbabf4201b3` + +The live suite checks the layout and behavior against this pinned deployment and fails when +its identity changes. A redeploy voids the layout evidence — re-verify +before trusting the templates again. + +## The guard, and what it does not cover + +The manifest requires 1264 bytes and `05 00 00 00 00 00 00 00` at offset 96, then discovery +validates ownership and mint identities. These bytes are a conservative observed-state filter, +not a version discriminator: the deployed program also reads this field during age adjustment. +A different value may exclude a valid market, and a matching value does not authenticate a future +layout. Deployment revalidation remains necessary. + +The shared raw-layout schema has no owner predicate, so a foreign account of the same size carrying +the same eight bytes would pass a raw template. `TesseraMarket` validation adds the ownership +check used by discovery and both scenario tools; the depth builder also validates its input +account. Composing the raw template against an arbitrary address does not. That is a property of the shared schema, not of +this integration, and the raw scenario API is unvalidated by contract. + +## Templates + +| Template | Prepared state | +|---|---| +| `tessera-fair-value` | both directional atomic-ratio fields | +| `tessera-depth` | all twenty directional capacities on both ladders | +| `tessera-curve` | all twenty directional output factors on both ladders | +| `tessera-halt` | all twenty enabled flags on both ladders | +| `tessera-stale-quote` | offset 120, aged by the lead you pass (default -20) | +| `tessera-freshness` | offset 120, the current materialization slot | + +The direct field at offset 128 is quote atomic units per base atomic unit multiplied by `10^15`. +The reciprocal at offset 144 uses the same scale, so their product is approximately `10^30` after +integer-floor rounding. Changing only one of them moves one quote direction and leaves the other +where it was, which is why they are one invariant. + +The sell ladder occupies bytes 160 through 639 and the buy ladder 640 through 1119. Each holds +twenty 24-byte records: directional capacity at `+0`, marginal-price factor at `+8`, enabled flag +at `+16`. Capacity and factor changes affect only their active quote direction. For a fill +starting and ending in level zero, the modeled output is +`floor(input_atoms * directional_price * first_level_factor / 10^21)`. The live tests check exact +atomic output after clearing the captured flow counters at 0/8 and neutralizing the five +selectable configurations at 1136 + 12*i (ppm adjustment 0, factor scale 1,000,000, no skipped +levels) in their local fixtures. A small input alone cannot +establish this condition: prior flow or the selected configuration can start at a later level. +These fixture controls are not exposed as scenario properties. + +`tessera-halt` writes zero to every enabled flag using two strided byte properties. Its existing +property names are retained, but each now covers twenty levels. Clearing only level zero can +leave later levels tradable. The live regression checks both captured state and an explicit +one-level skip, including a control where clearing only level zero still allows a swap. + +Offset 88 stores the age at which the program rejects a quote. Age 19 succeeds and age 20 fails +with custom error 65535 on a market configured at 20. + +Both slot templates take the lead from the caller: the value supplied for `last_update_slot` is +added to the materialization slot, and only `null` falls back to the template's own lead. One stale +template therefore covers every market, including one configured at a limit nobody has seen yet. +`list_tessera_markets` returns each market's limit alongside its address; callers pass its negation. Passing a number where you meant the default is the one trap: `0` on the stale +template writes a perfectly fresh quote. + +## Live market discovery + +`list_tessera_markets` queries Tessera program accounts through the selected Surfnet RPC. +It filters by the manifest's account size and pinned bytes, validates ownership and mint identities, +and reads decimals from the referenced mint accounts. The freshness limit comes from offset 88. +The existing Surfnet account resolver merges remote discovery with local accounts, preferring local +state. Discovery needs a datasource that supports `getProgramAccounts`; offline instances can list +only their local accounts. + +The six shared templates retain the SOL/USDC default address for callers that omit an account. + +Labels use mint symbols from Surfpool's existing token metadata. An unknown mint is displayed by +its full address, so missing symbol metadata never hides a discovered market. Addresses are the +identities; symbols are not unique. Market membership, decimals and freshness limits are not taken +from the token metadata catalog. + +## Builders and tools + +The fair-value builder converts a human price into reciprocal atomic ratios using both mints' +decimals. It is a pure function over account data. `create_tessera_fair_value_scenario` reads the market and both mints through the +surfnet's own RPC, so local state wins and only missing accounts fall back to the datasource, then +stages the scenario through the shared path. + +Builder-created overrides keep `fetchBeforeUse: false`: creation has already read and hydrated +the target account, and the scenario must use that prepared local snapshot. When composing a +direct template scenario, set `fetchBeforeUse: true` on the first override for each account not +yet in local state. Freshness overrides pass `last_update_slot: null` to write the materialization +slot itself. They are applied once; a scenario that runs past the market's freshness window +schedules another refresh at a later slot. + +The `create_tessera_depth_scenario` tool reads current Surfnet state and takes remaining basis points +per direction: 1000 retains 10%, 10000 leaves that direction unchanged. It scales only enabled +capacities, with integer-floor rounding, and rejects zero capacities or increases. Prices, factors +and disabled levels are preserved. The scenario combines `tessera-depth` with a freshness override, +both applied once. Creating another scenario reads the then-current state again. +Curve changes remain available through the raw template. + +## Behavioral evidence + +The live suite loads the pinned ELF into LiteSVM and exercises price direction isolation, +depth reductions, curve factors, freshness boundaries, halted ladders, vault bindings, +invalid sentinel/global account metas, and live market discovery. Raw writes are checked against +complete expected buffers or permitted byte ranges. + +Run it serially. The public endpoint sheds queued requests right after a `getProgramAccounts` scan, +sometimes as a 413 that looks like a request-size error: + +```bash +SURFPOOL_TEST_RPC_URL= cargo test -p surfpool-core --features integration-tests \ + tests::tessera -- --test-threads=1 --nocapture +``` + +`SURFPOOL_TEST_RPC_URL` is optional and defaults to the public mainnet endpoint. Set it to a +private endpoint when the public one rate-limits. + +## Known boundaries + +No separate fee field is exposed. Exact controlled first-level output does not establish how +Tessera decomposes its price factor into spread, fee, or another adjustment. The region from 1120 +onward includes configuration-dependent quote adjustments and leading-level selection; its full +economic meaning remains unmodeled and it is not exposed by the templates. Vault depletion is +not exposed by the Tessera templates. The generic SPL Token balance template uses the shared +typed token-account writer, but Tessera vault depletion behavior is outside this suite's coverage. diff --git a/crates/core/src/scenarios/protocols/tessera/mod.rs b/crates/core/src/scenarios/protocols/tessera/mod.rs new file mode 100644 index 000000000..a3a6d96c3 --- /dev/null +++ b/crates/core/src/scenarios/protocols/tessera/mod.rs @@ -0,0 +1 @@ +pub mod v1; diff --git a/crates/core/src/scenarios/protocols/tessera/v1/depth.rs b/crates/core/src/scenarios/protocols/tessera/v1/depth.rs new file mode 100644 index 000000000..ec3d37bbe --- /dev/null +++ b/crates/core/src/scenarios/protocols/tessera/v1/depth.rs @@ -0,0 +1,163 @@ +use std::collections::HashMap; + +use solana_account::Account; +use solana_pubkey::Pubkey; +use surfpool_types::{AccountAddress, OverrideInstance, Scenario}; + +use super::{ + TesseraMarket, + fair_value::{FRESHNESS_TEMPLATE, freshness_override, template}, +}; +use crate::{ + error::{SurfpoolError, SurfpoolResult}, + scenarios::TemplateRegistry, +}; + +pub fn build_tessera_depth_scenario( + market: Pubkey, + account: &Account, + sell_remaining_bps: u16, + buy_remaining_bps: u16, +) -> SurfpoolResult { + if [sell_remaining_bps, buy_remaining_bps] + .iter() + .any(|bps| !(1..=10_000).contains(bps)) + { + return Err(SurfpoolError::internal( + "Remaining depth must be 1..10000 basis points; 1000 keeps 10%, 10000 leaves a side unchanged", + )); + } + TesseraMarket::mint_addresses(account)?; + let registry = TemplateRegistry::new(); + let depth_template = template(®istry, "tessera-depth")?; + template(®istry, FRESHNESS_TEMPLATE)?; + let mut values = HashMap::new(); + for property in &depth_template.properties { + let bps = if property.path.starts_with("sell_levels.") { + sell_remaining_bps + } else { + buy_remaining_bps + }; + let offset = property.offset.expect("Tessera capacity offset"); + // The enabled flag follows capacity and price factor in each 24-byte level. + if bps == 10_000 || account.data[offset + 16] == 0 { + continue; + } + let current = u64::from_le_bytes(account.data[offset..offset + 8].try_into().unwrap()); + let scaled = (u128::from(current) * u128::from(bps) / 10_000) as u64; + if scaled == 0 { + return Err(SurfpoolError::internal(format!( + "{} would have zero capacity while enabled; retain more depth", + property.path + ))); + } + values.insert(property.path.clone(), serde_json::json!(scaled.to_string())); + } + if values.is_empty() { + return Err(SurfpoolError::internal( + "No enabled levels selected for depth reduction", + )); + } + let percent = |bps: u16| format!("{}.{:02}%", bps / 100, bps % 100); + let mut scenario = Scenario::new( + "Tessera depth stress".to_string(), + format!( + "Keep {} of sell depth and {} of buy depth on market {market}, preserving prices and keeping quotes fresh.", + percent(sell_remaining_bps), + percent(buy_remaining_bps) + ), + ); + let target = AccountAddress::Pubkey(market.to_string()); + scenario.add_override( + OverrideInstance::new(depth_template.id.clone(), 0, target.clone()) + .with_values(values) + .with_label("Reduce Tessera depth".to_string()), + ); + scenario.add_override(freshness_override(target)); + scenario.tags = vec![ + "tessera".to_string(), + "pmm".to_string(), + "depth-stress".to_string(), + ]; + Ok(scenario) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::scenarios::protocols::tessera::v1::{TESSERA_PROGRAM_ID, fair_value::MARKET_LAYOUT}; + + fn market() -> Account { + let mut data = vec![0; MARKET_LAYOUT.account_size]; + let magic = MARKET_LAYOUT.magic.as_ref().unwrap(); + data[magic.offset..magic.offset + magic.bytes.len()].copy_from_slice(&magic.bytes); + data[24..56].copy_from_slice(Pubkey::new_unique().as_ref()); + data[56..88].copy_from_slice(Pubkey::new_unique().as_ref()); + for (offset, amount, enabled) in [(160, u64::MAX, 1), (184, 101, 0), (640, 12345, 1)] { + data[offset..offset + 8].copy_from_slice(&amount.to_le_bytes()); + data[offset + 16] = enabled; + } + Account { + data, + owner: TESSERA_PROGRAM_ID, + ..Account::default() + } + } + + #[test] + fn scales_exactly_and_preserves_unselected_bytes() { + let account = market(); + let address = Pubkey::new_unique(); + for (sell, buy, expected_sell, expected_buy) in [ + (5000, 10000, 9223372036854775807u64, 12345u64), + (1000, 2500, 1844674407370955161u64, 3086), + ] { + let scenario = build_tessera_depth_scenario(address, &account, sell, buy).unwrap(); + assert_eq!(scenario.overrides.len(), 2); + let mut actual = account.data.clone(); + let mut expected = actual.clone(); + expected[160..168].copy_from_slice(&expected_sell.to_le_bytes()); + expected[640..648].copy_from_slice(&expected_buy.to_le_bytes()); + expected[120..128].copy_from_slice(&42u64.to_le_bytes()); + let registry = TemplateRegistry::new(); + for instance in &scenario.overrides { + assert_eq!( + instance.account, + AccountAddress::Pubkey(address.to_string()) + ); + assert_eq!(instance.scenario_relative_slot, 0); + assert!(!instance.fetch_before_use); + let template = registry.get(&instance.template_id).unwrap(); + actual = template + .raw_layout + .as_ref() + .unwrap() + .materialize(&actual, &template.properties, &instance.values, 42) + .unwrap(); + } + assert_eq!(actual, expected); + assert!( + scenario.overrides[0] + .values + .values() + .all(|value| value.is_string()) + ); + } + } + + #[test] + fn rejects_invalid_reductions_and_accounts() { + let mut account = market(); + let address = Pubkey::new_unique(); + for (sell, buy) in [(0, 10000), (10000, 10001), (10000, 10000)] { + assert!(build_tessera_depth_scenario(address, &account, sell, buy).is_err()); + } + account.data[160..168].copy_from_slice(&1u64.to_le_bytes()); + assert!(build_tessera_depth_scenario(address, &account, 1000, 10000).is_err()); + account.owner = Pubkey::new_unique(); + assert!(build_tessera_depth_scenario(address, &account, 1000, 1000).is_err()); + account.owner = TESSERA_PROGRAM_ID; + account.data.truncate(100); + assert!(build_tessera_depth_scenario(address, &account, 1000, 1000).is_err()); + } +} diff --git a/crates/core/src/scenarios/protocols/tessera/v1/fair_value.rs b/crates/core/src/scenarios/protocols/tessera/v1/fair_value.rs new file mode 100644 index 000000000..acef3c1bb --- /dev/null +++ b/crates/core/src/scenarios/protocols/tessera/v1/fair_value.rs @@ -0,0 +1,417 @@ +//! Tessera fair-value state preparation. +//! +//! Tessera publishes no IDL. Every write goes through the raw layout in `overrides.yaml`; this +//! module exists only for the one thing a template cannot express: turning a human price into the +//! pair of reciprocal atomic ratios the program reads, which needs both mints' decimals. + +use std::{collections::HashMap, sync::LazyLock}; + +use solana_account::Account; +use solana_pubkey::Pubkey; +use surfpool_types::{ + AccountAddress, OverrideInstance, OverrideTemplate, RawLayout, Scenario, + VERIFIED_TOKENS_BY_SYMBOL, +}; + +use crate::{ + error::{SurfpoolError, SurfpoolResult}, + scenarios::TemplateRegistry, + types::MintAccount, +}; + +pub const TESSERA_PROGRAM_ID: Pubkey = + Pubkey::from_str_const("TessVdML9pBGgG9yGks7o4HewRaXVAMuoVj4x83GLQH"); +pub const TESSERA_DEFAULT_MARKET: Pubkey = + Pubkey::from_str_const("FLckHLGMJy5gEoXWwcE68Nprde1D4araK4TGLw4pQq2n"); + +/// The two mint offsets are read, never written, so no template declares them. +const BASE_MINT_OFFSET: usize = 24; +const QUOTE_MINT_OFFSET: usize = 56; + +/// The size and pinned bytes a Tessera market must have, taken from the manifest the raw templates +/// are written against so there is one definition of them. Built once; the manifest is compiled in. +pub(super) static MARKET_LAYOUT: LazyLock = LazyLock::new(|| { + template(&TemplateRegistry::new(), FAIR_VALUE_TEMPLATE) + .and_then(|template| { + template.raw_layout.clone().ok_or_else(|| { + SurfpoolError::internal("the Tessera manifest carries no raw layout") + }) + }) + .expect("the Tessera manifest is compiled in and always parses") +}); + +const FAIR_VALUE_TEMPLATE: &str = "tessera-fair-value"; +pub(super) const FRESHNESS_TEMPLATE: &str = "tessera-freshness"; + +/// Both ratio fields are integers scaled by 10^15, so their product is 10^30. +const ATOMIC_RATIO_SCALE: u128 = 1_000_000_000_000_000; +const RECIPROCAL_PRODUCT: u128 = ATOMIC_RATIO_SCALE * ATOMIC_RATIO_SCALE; + +/// Both overrides apply on Play, before any slot advance. +const PREPARATION_SLOT: u64 = 0; + +/// The parts of a Tessera market a price needs: which mints it quotes, and at what scale. +#[derive(Clone, Debug, PartialEq)] +pub struct TesseraMarket { + pub address: Pubkey, + pub base_mint: Pubkey, + pub quote_mint: Pubkey, + pub base_decimals: u8, + pub quote_decimals: u8, + pub freshness_limit_slots: u64, +} + +impl TesseraMarket { + pub fn mint_addresses(market_account: &Account) -> SurfpoolResult<(Pubkey, Pubkey)> { + validate_tessera_market_layout(market_account)?; + let base_mint = read_pubkey(&market_account.data, BASE_MINT_OFFSET)?; + let quote_mint = read_pubkey(&market_account.data, QUOTE_MINT_OFFSET)?; + if base_mint == Pubkey::default() + || quote_mint == Pubkey::default() + || base_mint == quote_mint + { + return Err(invalid("market has invalid mint identities")); + } + Ok((base_mint, quote_mint)) + } + + pub fn validate( + address: Pubkey, + market_account: &Account, + base_mint_account: &Account, + quote_mint_account: &Account, + ) -> SurfpoolResult { + let (base_mint, quote_mint) = Self::mint_addresses(market_account)?; + + validate_mint_owner(base_mint_account, "base")?; + validate_mint_owner(quote_mint_account, "quote")?; + let base_decimals = MintAccount::unpack(&base_mint_account.data) + .map_err(|_| invalid("base mint account is invalid"))? + .decimals(); + let quote_decimals = MintAccount::unpack("e_mint_account.data) + .map_err(|_| invalid("quote mint account is invalid"))? + .decimals(); + + Ok(Self { + address, + base_mint, + quote_mint, + base_decimals, + quote_decimals, + freshness_limit_slots: u64::from_le_bytes( + market_account.data[88..96].try_into().unwrap(), + ), + }) + } + + pub fn label(&self) -> String { + let symbol = |mint: &Pubkey| { + let address = mint.to_string(); + VERIFIED_TOKENS_BY_SYMBOL + .values() + .filter(|token| token.address == address) + .map(|token| token.symbol.as_str()) + .min() + .map(str::to_string) + .unwrap_or(address) + }; + format!("{}/{}", symbol(&self.base_mint), symbol(&self.quote_mint)) + } +} + +/// Rejects an account that is not a Tessera market. +/// +/// The shared raw-layout guard has no owner predicate, so a foreign account of the same size +/// carrying the same pinned bytes would pass it. Every builder-made scenario comes through here, +/// which adds the ownership check the schema cannot express. +pub fn validate_tessera_market_layout(account: &Account) -> SurfpoolResult<()> { + if account.owner != TESSERA_PROGRAM_ID { + return Err(invalid("market is not owned by Tessera")); + } + MARKET_LAYOUT.guard(&account.data).map_err(invalid) +} + +#[derive(Clone, Debug, PartialEq)] +pub struct TesseraFairValuePreparation { + pub scenario: Scenario, + pub market: Pubkey, + pub quote_atoms_per_base_atom_x1e15: u64, + pub base_atoms_per_quote_atom_x1e15: u64, +} + +pub fn build_tessera_fair_value_scenario( + market: &TesseraMarket, + price: &str, +) -> SurfpoolResult { + let quote_atoms_per_base_atom_x1e15 = + human_price_to_atomic_ratio(price, market.base_decimals, market.quote_decimals)?; + let reciprocal = RECIPROCAL_PRODUCT / u128::from(quote_atoms_per_base_atom_x1e15); + let base_atoms_per_quote_atom_x1e15 = u64::try_from(reciprocal) + .map_err(|_| invalid("price is too small for Tessera's reciprocal u64 field"))?; + + let registry = TemplateRegistry::new(); + let fair_value = template(®istry, FAIR_VALUE_TEMPLATE)?; + template(®istry, FRESHNESS_TEMPLATE)?; + let market_name = market.label(); + let target = AccountAddress::Pubkey(market.address.to_string()); + + // Creation hydrates the market into local state. Both overrides must use that prepared + // snapshot, so they leave fetch_before_use false. + let price_override = + OverrideInstance::new(fair_value.id.clone(), PREPARATION_SLOT, target.clone()) + .with_values(HashMap::from([ + ( + "quote_atoms_per_base_atom_x1e15".to_string(), + serde_json::json!(quote_atoms_per_base_atom_x1e15.to_string()), + ), + ( + "base_atoms_per_quote_atom_x1e15".to_string(), + serde_json::json!(base_atoms_per_quote_atom_x1e15.to_string()), + ), + ])) + .with_label(format!("Tessera {market_name} fair value")); + + let normalized_price = price.trim(); + let mut scenario = Scenario::new( + format!("Tessera {market_name} at {normalized_price}"), + format!( + "Prepare Tessera market {} to quote one base token at {normalized_price} quote tokens; no swap is sent.", + market.address + ), + ); + scenario.tags = vec![ + "tessera".to_string(), + "pmm".to_string(), + "price-dislocation".to_string(), + ]; + scenario.add_override(price_override); + scenario.add_override(freshness_override(target)); + + Ok(TesseraFairValuePreparation { + scenario, + market: market.address, + quote_atoms_per_base_atom_x1e15, + base_atoms_per_quote_atom_x1e15, + }) +} + +/// Applied once, at the scenario's own slot: nothing on a fork republishes the quote, and nothing +/// overwrites it either. A scenario that spans enough slots to age past the market's window +/// refreshes it again at a later slot. +pub(super) fn freshness_override(target: AccountAddress) -> OverrideInstance { + OverrideInstance::new(FRESHNESS_TEMPLATE.to_string(), PREPARATION_SLOT, target) + .with_values(HashMap::from([( + "last_update_slot".to_string(), + serde_json::Value::Null, + )])) + .with_label("Keep Tessera quote fresh".to_string()) +} + +fn read_pubkey(data: &[u8], offset: usize) -> SurfpoolResult { + let bytes: [u8; 32] = data[offset..offset + 32] + .try_into() + .map_err(|_| invalid("market mint bytes are truncated"))?; + Ok(Pubkey::new_from_array(bytes)) +} + +fn validate_mint_owner(account: &Account, side: &str) -> SurfpoolResult<()> { + if account.owner != spl_token_interface::ID && account.owner != spl_token_2022_interface::ID { + return Err(invalid(format!( + "{side} mint is not owned by a supported token program" + ))); + } + Ok(()) +} + +fn human_price_to_atomic_ratio( + price: &str, + base_decimals: u8, + quote_decimals: u8, +) -> SurfpoolResult { + let value = price.trim(); + let mut parts = value.split('.'); + let whole = parts.next().unwrap_or_default(); + let fractional = parts.next().unwrap_or_default(); + if parts.next().is_some() + || whole.is_empty() + || !whole.bytes().all(|byte| byte.is_ascii_digit()) + || !fractional.bytes().all(|byte| byte.is_ascii_digit()) + { + return Err(invalid("price must be a positive decimal string")); + } + + let digits = format!("{whole}{fractional}") + .parse::() + .map_err(|_| invalid("price is too large"))?; + let exponent = i32::from(quote_decimals) + 15 + - i32::from(base_decimals) + - i32::try_from(fractional.len()).map_err(|_| invalid("price is too precise"))?; + let scaled = if exponent >= 0 { + digits + .checked_mul(checked_power_of_ten(exponent as u32)?) + .ok_or_else(|| invalid("price is too large"))? + } else { + digits / checked_power_of_ten(exponent.unsigned_abs())? + }; + if scaled == 0 { + return Err(invalid( + "price is too small for this market's mint decimals", + )); + } + u64::try_from(scaled).map_err(|_| { + let scale = i32::from(quote_decimals) + 15 - i32::from(base_decimals); + let max_price = checked_power_of_ten(scale.unsigned_abs()) + .map(|power| u128::from(u64::MAX) / power) + .unwrap_or_default(); + invalid(format!( + "price is too large for Tessera's u64 field; this market accepts at most about {max_price} quote per base" + )) + }) +} + +fn checked_power_of_ten(exponent: u32) -> SurfpoolResult { + 10u128 + .checked_pow(exponent) + .ok_or_else(|| invalid("price scale exceeds supported precision")) +} + +pub(super) fn template<'a>( + registry: &'a TemplateRegistry, + id: &str, +) -> SurfpoolResult<&'a OverrideTemplate> { + registry + .get(id) + .ok_or_else(|| SurfpoolError::internal(format!("Tessera template {id} is unavailable"))) +} + +fn invalid(message: impl Into) -> SurfpoolError { + SurfpoolError::internal(message.into()) +} + +#[cfg(test)] +mod tests { + use solana_program_pack::Pack; + + use super::*; + + fn mint_account(decimals: u8) -> Account { + let mut data = vec![0; spl_token_interface::state::Mint::LEN]; + spl_token_interface::state::Mint { + decimals, + is_initialized: true, + ..Default::default() + } + .pack_into_slice(&mut data); + Account { + data, + owner: spl_token_interface::ID, + ..Account::default() + } + } + + fn market_account(base_mint: &Pubkey, quote_mint: &Pubkey) -> Account { + let mut data = vec![0; MARKET_LAYOUT.account_size]; + data[BASE_MINT_OFFSET..BASE_MINT_OFFSET + 32].copy_from_slice(base_mint.as_ref()); + data[QUOTE_MINT_OFFSET..QUOTE_MINT_OFFSET + 32].copy_from_slice(quote_mint.as_ref()); + let magic = MARKET_LAYOUT.magic.as_ref().expect("manifest byte guard"); + data[magic.offset..magic.offset + magic.bytes.len()].copy_from_slice(&magic.bytes); + Account { + data, + owner: TESSERA_PROGRAM_ID, + ..Account::default() + } + } + + fn market(base_decimals: u8, quote_decimals: u8) -> TesseraMarket { + let base_mint = Pubkey::new_unique(); + let quote_mint = Pubkey::new_unique(); + TesseraMarket::validate( + Pubkey::new_unique(), + &market_account(&base_mint, "e_mint), + &mint_account(base_decimals), + &mint_account(quote_decimals), + ) + .expect("valid Tessera market") + } + + #[test] + fn reads_metadata_for_a_market_outside_the_token_catalog() { + let base = Pubkey::new_unique(); + let quote = Pubkey::new_unique(); + let address = Pubkey::new_unique(); + let mut account = market_account(&base, "e); + account.data[88..96].copy_from_slice(&37u64.to_le_bytes()); + let market = + TesseraMarket::validate(address, &account, &mint_account(8), &mint_account(6)).unwrap(); + assert_eq!(market.address, address); + assert_eq!((market.base_decimals, market.quote_decimals), (8, 6)); + assert_eq!(market.freshness_limit_slots, 37); + assert_eq!(market.label(), format!("{base}/{quote}")); + } + + #[test] + fn derives_price_scale_from_market_mint_decimals() { + for (base_decimals, quote_decimals, price, expected) in [ + (9, 6, "100.25", 100_250_000_000_000u64), + (8, 6, "78.8477010015472512", 788_477_010_015_472), + ] { + let market = market(base_decimals, quote_decimals); + let preparation = build_tessera_fair_value_scenario(&market, price).unwrap(); + assert_eq!(preparation.market, market.address); + assert_eq!(preparation.quote_atoms_per_base_atom_x1e15, expected); + assert_eq!( + preparation.base_atoms_per_quote_atom_x1e15, + (RECIPROCAL_PRODUCT / u128::from(expected)) as u64 + ); + let [price, freshness] = &preparation.scenario.overrides[..] else { + panic!("expected exactly a price and a freshness override"); + }; + assert_eq!( + price.account, + AccountAddress::Pubkey(market.address.to_string()) + ); + assert!(!price.fetch_before_use); + assert!(!freshness.fetch_before_use); + assert_eq!( + freshness.values.get("last_update_slot"), + Some(&serde_json::Value::Null) + ); + } + } + + #[test] + fn rejects_invalid_price_and_market_inputs() { + let market = market(9, 6); + for price in ["0", "-1", "1.2.3", "not-a-price", ""] { + assert!(build_tessera_fair_value_scenario(&market, price).is_err()); + } + + let base_mint = mint_account(9); + let quote_mint = mint_account(6); + let wrong_owner = Account { + owner: Pubkey::new_unique(), + ..market_account(&Pubkey::new_unique(), &Pubkey::new_unique()) + }; + assert!( + TesseraMarket::validate(Pubkey::new_unique(), &wrong_owner, &base_mint, "e_mint) + .is_err() + ); + // The raw guard cannot see the owner, which is the whole reason this check sits on top. + assert!(MARKET_LAYOUT.guard(&wrong_owner.data).is_ok()); + + let mut wrong_magic = market_account(&Pubkey::new_unique(), &Pubkey::new_unique()); + wrong_magic.data[MARKET_LAYOUT.magic.as_ref().unwrap().offset] ^= 1; + assert!(TesseraMarket::mint_addresses(&wrong_magic).is_err()); + + let same_mint = Pubkey::new_unique(); + assert!( + TesseraMarket::validate( + Pubkey::new_unique(), + &market_account(&same_mint, &same_mint), + &base_mint, + "e_mint, + ) + .is_err() + ); + } +} diff --git a/crates/core/src/scenarios/protocols/tessera/v1/markets.rs b/crates/core/src/scenarios/protocols/tessera/v1/markets.rs new file mode 100644 index 000000000..d42a537f0 --- /dev/null +++ b/crates/core/src/scenarios/protocols/tessera/v1/markets.rs @@ -0,0 +1,158 @@ +use std::collections::HashMap; + +use solana_account::Account; +use solana_account_decoder::UiAccountEncoding; +use solana_client::{ + rpc_config::RpcAccountInfoConfig, + rpc_filter::{Memcmp, RpcFilterType}, +}; +use solana_commitment_config::CommitmentConfig; +use solana_pubkey::Pubkey; + +use super::{TESSERA_DEFAULT_MARKET, TESSERA_PROGRAM_ID, TesseraMarket, fair_value::MARKET_LAYOUT}; +use crate::{ + error::{SurfpoolError, SurfpoolResult}, + surfnet::remote::SurfnetRemoteClient, +}; + +pub async fn discover_tessera_markets( + client: &SurfnetRemoteClient, +) -> SurfpoolResult> { + let mut filters = vec![RpcFilterType::DataSize(MARKET_LAYOUT.account_size as u64)]; + if let Some(magic) = &MARKET_LAYOUT.magic { + filters.push(RpcFilterType::Memcmp(Memcmp::new_raw_bytes( + magic.offset, + magic.bytes.clone(), + ))); + } + let accounts = client + .get_program_accounts( + &TESSERA_PROGRAM_ID, + RpcAccountInfoConfig { + encoding: Some(UiAccountEncoding::Base64), + commitment: Some(CommitmentConfig::confirmed()), + ..Default::default() + }, + Some(filters), + ) + .await? + .into_result()?; + + // One obsolete or malformed market must not hide every valid one, so a market that fails to + // decode or validate is skipped with a warning and the rest of the catalog is still returned. + // This is the same warn-and-continue rule the materializer applies per override. + let candidates = accounts.len(); + let mut retained = Vec::new(); + for (address, encoded) in accounts { + let Some(account) = encoded.to_account() else { + warn!("Skipping Tessera market {address}: its account data could not be decoded"); + continue; + }; + match TesseraMarket::mint_addresses(&account) { + Ok(mints) => retained.push((address, account, mints)), + Err(error) => warn!("Skipping Tessera market {address}: {error}"), + } + } + let accounts = retained; + + let mut mints = accounts + .iter() + .flat_map(|(_, _, (base, quote))| [*base, *quote]) + .collect::>(); + mints.sort_unstable(); + mints.dedup(); + let mut mint_accounts = HashMap::new(); + for batch in mints.chunks(100) { + // A datasource failure or an unreadable mint disqualifies only the markets that point at + // it, which the validation below reports per market. + let fetched = match client + .get_multiple_accounts(batch, CommitmentConfig::confirmed()) + .await + { + Ok(fetched) => fetched, + Err(error) => { + warn!("Skipping {} Tessera mints: {error}", batch.len()); + continue; + } + }; + for (address, account) in batch.iter().zip(fetched) { + match account.map_account() { + Ok(account) => { + mint_accounts.insert(*address, account); + } + Err(error) => warn!("Skipping Tessera mint {address}: {error}"), + } + } + } + + let mut markets = Vec::new(); + for (address, account, (base, quote)) in &accounts { + let mint = |address| { + mint_accounts.get(address).ok_or_else(|| { + SurfpoolError::internal(format!("Tessera mint {address} was not found")) + }) + }; + match mint(base) + .and_then(|base| Ok((base, mint(quote)?))) + .and_then(|(base, quote)| TesseraMarket::validate(*address, account, base, quote)) + { + Ok(market) => markets.push(market), + Err(error) => warn!("Skipping Tessera market {address}: {error}"), + } + } + // An empty catalog from a program that does own markets is a failure, not a partial result. + if markets.is_empty() && candidates > 0 { + return Err(SurfpoolError::internal(format!( + "none of the {candidates} discovered Tessera markets validated; the integration needs a refresh" + ))); + } + markets.sort_by_cached_key(|market| { + ( + market.address != TESSERA_DEFAULT_MARKET, + market.label(), + market.address, + ) + }); + Ok(markets) +} +/// Validates one discovered market against the mint accounts fetched for the whole catalog. A +/// market whose mints are missing or unreadable fails here alone, so the rest of the catalog +/// still resolves. +fn resolve_market( + address: Pubkey, + account: &Account, + mints: (Pubkey, Pubkey), + mint_accounts: &HashMap, +) -> SurfpoolResult { + let mint = |address: &Pubkey| { + mint_accounts + .get(address) + .ok_or_else(|| SurfpoolError::internal(format!("Tessera mint {address} was not found"))) + }; + let (base, quote) = mints; + TesseraMarket::validate(address, account, mint(&base)?, mint("e)?) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_market_whose_mints_are_missing_fails_alone() { + let market = Pubkey::new_unique(); + let base = Pubkey::new_unique(); + let quote = Pubkey::new_unique(); + let account = Account { + data: vec![0; MARKET_LAYOUT.account_size], + owner: TESSERA_PROGRAM_ID, + ..Account::default() + }; + + let error = resolve_market(market, &account, (base, quote), &HashMap::new()) + .expect_err("a market with no mint accounts must not resolve"); + assert!( + error.to_string().contains(&base.to_string()), + "the error must name the missing mint: {error}" + ); + } +} diff --git a/crates/core/src/scenarios/protocols/tessera/v1/mod.rs b/crates/core/src/scenarios/protocols/tessera/v1/mod.rs new file mode 100644 index 000000000..1985c10c1 --- /dev/null +++ b/crates/core/src/scenarios/protocols/tessera/v1/mod.rs @@ -0,0 +1,9 @@ +mod depth; +mod fair_value; +mod markets; + +pub use depth::build_tessera_depth_scenario; +pub use fair_value::{ + TESSERA_DEFAULT_MARKET, TESSERA_PROGRAM_ID, TesseraMarket, build_tessera_fair_value_scenario, +}; +pub use markets::discover_tessera_markets; diff --git a/crates/core/src/scenarios/protocols/tessera/v1/overrides.yaml b/crates/core/src/scenarios/protocols/tessera/v1/overrides.yaml new file mode 100644 index 000000000..a5a6f12b0 --- /dev/null +++ b/crates/core/src/scenarios/protocols/tessera/v1/overrides.yaml @@ -0,0 +1,523 @@ +protocol: Tessera +version: deployed-446053401 +account_type: MarketState + +raw_layout: + account_size: 1264 + magic: + offset: 96 + bytes: [5, 0, 0, 0, 0, 0, 0, 0] + +tags: + - pmm + - prop-amm + - swap + +templates: + - id: tessera-fair-value + name: Override Tessera Fair Value + description: Move a Tessera market's reference price atomically in both directions + idl_account_name: MarketState + address: + type: pubkey + value: FLckHLGMJy5gEoXWwcE68Nprde1D4araK4TGLw4pQq2n + properties: + - path: quote_atoms_per_base_atom_x1e15 + offset: 128 + encoding: u64 + label: Quote per base + description: "Quote atomic units per base atomic unit multiplied by 10^15. Use the builder to derive this from mint decimals." + - path: base_atoms_per_quote_atom_x1e15 + offset: 144 + encoding: u64 + label: Base per quote + description: "Base atomic units per quote atomic unit multiplied by 10^15. Use the builder to derive the reciprocal atomically." + llm_context: | + For a direct template scenario, set fetchBeforeUse: true on the first override for each + account not yet in local state. + Builder-created scenarios keep fetchBeforeUse: false to use the prepared local snapshot. + + Use list_tessera_markets to select the override account. + SET BOTH FIELDS AS ONE INVARIANT. Their product is approximately 10^30, with integer-floor + rounding in the reciprocal field. Changing only offset 128 moves base-to-quote sells but + leaves quote-to-base buys unchanged; changing only offset 144 does the opposite. + + Use the Tessera fair-value builder when starting from a human price. It calculates both exact + integer fields from the selected market's mint decimals and rejects zero or overflow. If + composing the raw template directly, use decimal integer strings rather than JSON numbers. + + Tessera rejects a quote once it reaches the market's freshness limit, with program error + 0xffff. Pair long-running scenarios with tessera-freshness. + + EXAMPLE - WSOL/USDC at 100 quote tokens per base token: + quote_atoms_per_base_atom_x1e15: "100000000000000" + base_atoms_per_quote_atom_x1e15: "10000000000000000" + + - id: tessera-depth + name: Override Tessera Depth + description: Change the directional capacities of Tessera's sell and buy ladders + idl_account_name: MarketState + address: + type: pubkey + value: FLckHLGMJy5gEoXWwcE68Nprde1D4araK4TGLw4pQq2n + properties: + - path: sell_levels.0.amount + offset: 160 + encoding: u64 + label: Sell level 1 capacity + - path: sell_levels.1.amount + offset: 184 + encoding: u64 + label: Sell level 2 capacity + - path: sell_levels.2.amount + offset: 208 + encoding: u64 + label: Sell level 3 capacity + - path: sell_levels.3.amount + offset: 232 + encoding: u64 + label: Sell level 4 capacity + - path: sell_levels.4.amount + offset: 256 + encoding: u64 + label: Sell level 5 capacity + - path: sell_levels.5.amount + offset: 280 + encoding: u64 + label: Sell level 6 capacity + - path: sell_levels.6.amount + offset: 304 + encoding: u64 + label: Sell level 7 capacity + - path: sell_levels.7.amount + offset: 328 + encoding: u64 + label: Sell level 8 capacity + - path: sell_levels.8.amount + offset: 352 + encoding: u64 + label: Sell level 9 capacity + - path: sell_levels.9.amount + offset: 376 + encoding: u64 + label: Sell level 10 capacity + - path: sell_levels.10.amount + offset: 400 + encoding: u64 + label: Sell level 11 capacity + - path: sell_levels.11.amount + offset: 424 + encoding: u64 + label: Sell level 12 capacity + - path: sell_levels.12.amount + offset: 448 + encoding: u64 + label: Sell level 13 capacity + - path: sell_levels.13.amount + offset: 472 + encoding: u64 + label: Sell level 14 capacity + - path: sell_levels.14.amount + offset: 496 + encoding: u64 + label: Sell level 15 capacity + - path: sell_levels.15.amount + offset: 520 + encoding: u64 + label: Sell level 16 capacity + - path: sell_levels.16.amount + offset: 544 + encoding: u64 + label: Sell level 17 capacity + - path: sell_levels.17.amount + offset: 568 + encoding: u64 + label: Sell level 18 capacity + - path: sell_levels.18.amount + offset: 592 + encoding: u64 + label: Sell level 19 capacity + - path: sell_levels.19.amount + offset: 616 + encoding: u64 + label: Sell level 20 capacity + - path: buy_levels.0.amount + offset: 640 + encoding: u64 + label: Buy level 1 capacity + - path: buy_levels.1.amount + offset: 664 + encoding: u64 + label: Buy level 2 capacity + - path: buy_levels.2.amount + offset: 688 + encoding: u64 + label: Buy level 3 capacity + - path: buy_levels.3.amount + offset: 712 + encoding: u64 + label: Buy level 4 capacity + - path: buy_levels.4.amount + offset: 736 + encoding: u64 + label: Buy level 5 capacity + - path: buy_levels.5.amount + offset: 760 + encoding: u64 + label: Buy level 6 capacity + - path: buy_levels.6.amount + offset: 784 + encoding: u64 + label: Buy level 7 capacity + - path: buy_levels.7.amount + offset: 808 + encoding: u64 + label: Buy level 8 capacity + - path: buy_levels.8.amount + offset: 832 + encoding: u64 + label: Buy level 9 capacity + - path: buy_levels.9.amount + offset: 856 + encoding: u64 + label: Buy level 10 capacity + - path: buy_levels.10.amount + offset: 880 + encoding: u64 + label: Buy level 11 capacity + - path: buy_levels.11.amount + offset: 904 + encoding: u64 + label: Buy level 12 capacity + - path: buy_levels.12.amount + offset: 928 + encoding: u64 + label: Buy level 13 capacity + - path: buy_levels.13.amount + offset: 952 + encoding: u64 + label: Buy level 14 capacity + - path: buy_levels.14.amount + offset: 976 + encoding: u64 + label: Buy level 15 capacity + - path: buy_levels.15.amount + offset: 1000 + encoding: u64 + label: Buy level 16 capacity + - path: buy_levels.16.amount + offset: 1024 + encoding: u64 + label: Buy level 17 capacity + - path: buy_levels.17.amount + offset: 1048 + encoding: u64 + label: Buy level 18 capacity + - path: buy_levels.18.amount + offset: 1072 + encoding: u64 + label: Buy level 19 capacity + - path: buy_levels.19.amount + offset: 1096 + encoding: u64 + label: Buy level 20 capacity + llm_context: | + For a direct template scenario, set fetchBeforeUse: true on the first override for each + account not yet in local state. + Builder-created scenarios keep fetchBeforeUse: false to use the prepared local snapshot. + + Use list_tessera_markets to select the override account. + For percentage reductions, use create_tessera_depth_scenario, which reads the current Surfnet + account and scales enabled levels exactly. Pass remaining basis points: 1000 keeps 10%, + 10000 leaves a direction unchanged. It also refreshes the quote once. + These are directional capacities, not token vault balances. Do not assume they + are monotonic cumulative breakpoints. To make one direction shallower, scale the currently + enabled amount fields for that direction by the same ratio. Lower sell_levels for a large + base-to-quote sell and buy_levels for a large quote-to-base buy. The opposite ladder is + behaviorally inactive for that direction. + + Small trades may not reveal a depth change; use a large fill to observe reduced capacity. + + Do not invent missing levels or enable disabled levels. Fetch the live account first and copy + all twenty current values before applying a uniform ratio only to enabled levels. A ratio that + rounds a live nonzero capacity down to zero leaves the level flagged enabled with nothing + behind it, which is not a state the market produces on its own; raise the ratio instead. + + - id: tessera-curve + name: Override Tessera Curve + description: Scale the directional output factors while preserving the live ladder ordering + idl_account_name: MarketState + address: + type: pubkey + value: FLckHLGMJy5gEoXWwcE68Nprde1D4araK4TGLw4pQq2n + properties: + - path: sell_levels.0.factor + offset: 168 + encoding: u64 + label: Sell level 1 output factor + - path: sell_levels.1.factor + offset: 192 + encoding: u64 + label: Sell level 2 output factor + - path: sell_levels.2.factor + offset: 216 + encoding: u64 + label: Sell level 3 output factor + - path: sell_levels.3.factor + offset: 240 + encoding: u64 + label: Sell level 4 output factor + - path: sell_levels.4.factor + offset: 264 + encoding: u64 + label: Sell level 5 output factor + - path: sell_levels.5.factor + offset: 288 + encoding: u64 + label: Sell level 6 output factor + - path: sell_levels.6.factor + offset: 312 + encoding: u64 + label: Sell level 7 output factor + - path: sell_levels.7.factor + offset: 336 + encoding: u64 + label: Sell level 8 output factor + - path: sell_levels.8.factor + offset: 360 + encoding: u64 + label: Sell level 9 output factor + - path: sell_levels.9.factor + offset: 384 + encoding: u64 + label: Sell level 10 output factor + - path: sell_levels.10.factor + offset: 408 + encoding: u64 + label: Sell level 11 output factor + - path: sell_levels.11.factor + offset: 432 + encoding: u64 + label: Sell level 12 output factor + - path: sell_levels.12.factor + offset: 456 + encoding: u64 + label: Sell level 13 output factor + - path: sell_levels.13.factor + offset: 480 + encoding: u64 + label: Sell level 14 output factor + - path: sell_levels.14.factor + offset: 504 + encoding: u64 + label: Sell level 15 output factor + - path: sell_levels.15.factor + offset: 528 + encoding: u64 + label: Sell level 16 output factor + - path: sell_levels.16.factor + offset: 552 + encoding: u64 + label: Sell level 17 output factor + - path: sell_levels.17.factor + offset: 576 + encoding: u64 + label: Sell level 18 output factor + - path: sell_levels.18.factor + offset: 600 + encoding: u64 + label: Sell level 19 output factor + - path: sell_levels.19.factor + offset: 624 + encoding: u64 + label: Sell level 20 output factor + - path: buy_levels.0.factor + offset: 648 + encoding: u64 + label: Buy level 1 output factor + - path: buy_levels.1.factor + offset: 672 + encoding: u64 + label: Buy level 2 output factor + - path: buy_levels.2.factor + offset: 696 + encoding: u64 + label: Buy level 3 output factor + - path: buy_levels.3.factor + offset: 720 + encoding: u64 + label: Buy level 4 output factor + - path: buy_levels.4.factor + offset: 744 + encoding: u64 + label: Buy level 5 output factor + - path: buy_levels.5.factor + offset: 768 + encoding: u64 + label: Buy level 6 output factor + - path: buy_levels.6.factor + offset: 792 + encoding: u64 + label: Buy level 7 output factor + - path: buy_levels.7.factor + offset: 816 + encoding: u64 + label: Buy level 8 output factor + - path: buy_levels.8.factor + offset: 840 + encoding: u64 + label: Buy level 9 output factor + - path: buy_levels.9.factor + offset: 864 + encoding: u64 + label: Buy level 10 output factor + - path: buy_levels.10.factor + offset: 888 + encoding: u64 + label: Buy level 11 output factor + - path: buy_levels.11.factor + offset: 912 + encoding: u64 + label: Buy level 12 output factor + - path: buy_levels.12.factor + offset: 936 + encoding: u64 + label: Buy level 13 output factor + - path: buy_levels.13.factor + offset: 960 + encoding: u64 + label: Buy level 14 output factor + - path: buy_levels.14.factor + offset: 984 + encoding: u64 + label: Buy level 15 output factor + - path: buy_levels.15.factor + offset: 1008 + encoding: u64 + label: Buy level 16 output factor + - path: buy_levels.16.factor + offset: 1032 + encoding: u64 + label: Buy level 17 output factor + - path: buy_levels.17.factor + offset: 1056 + encoding: u64 + label: Buy level 18 output factor + - path: buy_levels.18.factor + offset: 1080 + encoding: u64 + label: Buy level 19 output factor + - path: buy_levels.19.factor + offset: 1104 + encoding: u64 + label: Buy level 20 output factor + llm_context: | + For a direct template scenario, set fetchBeforeUse: true on the first override for each + account not yet in local state. + + Use list_tessera_markets to select the override account. + These forty fields control the directional output curve. Scaling every nonzero factor on + the active side scales its output; the opposite side is inactive for that quote direction. + + Read the live account first and copy all twenty current factors per direction, then apply one + ratio to every nonzero factor on the side you are stressing. Two rules are not optional. The + factors descend across the ladder, and a single factor that breaks that order is rejected by + the deployed program with custom error 8. A ratio that rounds a live nonzero factor down to + zero disables a level that the market has enabled. Check both before you write, and do not + edit one factor independently. + + - id: tessera-halt + name: Halt Tessera Liquidity + description: Disable every level on both directional ladders + idl_account_name: MarketState + address: + type: pubkey + value: FLckHLGMJy5gEoXWwcE68Nprde1D4araK4TGLw4pQq2n + properties: + - path: sell_level_0_enabled + offset: 176 + encoding: + u8_strided: { count: 20, stride: 24 } + label: Sell liquidity enabled + description: Set to zero to disable all twenty sell levels. Only zero is supported. + - path: buy_level_0_enabled + offset: 656 + encoding: + u8_strided: { count: 20, stride: 24 } + label: Buy liquidity enabled + description: Set to zero to disable all twenty buy levels. Only zero is supported. + llm_context: | + For a direct template scenario, set fetchBeforeUse: true on the first override for each + account not yet in local state. + + Use list_tessera_markets to select the override account. + SET BOTH FIELDS TO ZERO AS ONE INVARIANT. The current deployed program rejects either quote + direction with custom error 0xffff when every level is disabled. The existing field names + address the start of each twenty-level write. Disabling only level zero is insufficient: + consumed depth or the selected configuration may start a quote at a later level. Use only + zero; enabling previously disabled levels is not supported. + + - id: tessera-stale-quote + name: Make Tessera Quote Stale + description: Age a Tessera quote to its market's rejection boundary + idl_account_name: MarketState + address: + type: pubkey + value: FLckHLGMJy5gEoXWwcE68Nprde1D4araK4TGLw4pQq2n + properties: + - path: last_update_slot + offset: 120 + encoding: + slot: + lead: -20 + label: Slot lead + description: >- + How far behind the materialization slot to place the quote, as a negative integer. + Pass null to use -20, the limit of most markets and of the default one. + llm_context: | + For a direct template scenario, set fetchBeforeUse: true on the first override for each + account not yet in local state. + + The value you pass IS the lead: Surfpool writes the materialization slot plus it, clamped at + zero. Pass null to take the -20 default. This is one template for every market, not one per + limit. + + Rejection is at age greater than or equal to the market's own limit, so the lead must be at + most minus that limit. Call list_tessera_markets, select the market address, and negate its + freshnessLimitSlots. Set the override account to that address. Offset 88 on the market + account stores the same limit; do not assume all markets share a limit. + + At the boundary the current deployed program rejects the swap with custom error 65535, while + one slot younger succeeds. Do not refresh the quote afterwards: it should stay stale. + + Keep override labels short ("SOL/USDC stale quote"). + + - id: tessera-freshness + name: Refresh Tessera Quote + description: Publish the materialization slot into Tessera's freshness field + idl_account_name: MarketState + address: + type: pubkey + value: FLckHLGMJy5gEoXWwcE68Nprde1D4araK4TGLw4pQq2n + properties: + - path: last_update_slot + offset: 120 + encoding: + slot: + lead: 0 + label: Current materialization slot + description: Slot lead, as an integer. Pass null to take the lead of zero and write the materialization slot itself. + llm_context: | + For a direct template scenario, set fetchBeforeUse: true on the first override for each + account not yet in local state. + Builder-created scenarios keep fetchBeforeUse: false to use the prepared local snapshot. + + Call list_tessera_markets and set the override account to the chosen address. The returned + freshnessLimitSlots is the age at which that market rejects quotes with custom error 0xffff. + + Pass null for last_update_slot to take this template's lead of zero, which writes the exact + materialization slot. A number would be read as the lead instead, so passing 0 happens to mean + the same thing here and -5 would quietly age the quote by five slots. + + To keep a quote live past the market's freshness window, schedule this override again at a + later slot of the same scenario; each application writes the slot it materializes at. diff --git a/crates/core/src/scenarios/registry.rs b/crates/core/src/scenarios/registry.rs index 97480ea72..3338662c2 100644 --- a/crates/core/src/scenarios/registry.rs +++ b/crates/core/src/scenarios/registry.rs @@ -23,6 +23,9 @@ pub const METEORA_DLMM_OVERRIDES_CONTENT: &str = pub const KAMINO_V1_IDL_CONTENT: &str = include_str!("./protocols/kamino/v1/idl.json"); pub const KAMINO_V1_OVERRIDES_CONTENT: &str = include_str!("./protocols/kamino/v1/overrides.yaml"); +pub const TESSERA_V1_OVERRIDES_CONTENT: &str = + include_str!("./protocols/tessera/v1/overrides.yaml"); + pub const KAMINO_SCOPE_IDL_CONTENT: &str = include_str!("./protocols/kamino/scope/v1/idl.json"); pub const KAMINO_SCOPE_OVERRIDES_CONTENT: &str = include_str!("./protocols/kamino/scope/v1/overrides.yaml"); @@ -76,6 +79,7 @@ impl TemplateRegistry { default.load_raydium_overrides(); default.load_meteora_overrides(); default.load_kamino_overrides(); + default.load_tessera_overrides(); default.load_drift_overrides(); default.load_whirlpool_overrides(); default.load_spl_token_overrides(); @@ -116,6 +120,10 @@ impl TemplateRegistry { ); } + pub fn load_tessera_overrides(&mut self) { + self.load_raw_layout_overrides(TESSERA_V1_OVERRIDES_CONTENT, "tessera"); + } + pub fn load_kamino_overrides(&mut self) { self.load_protocol_overrides(KAMINO_V1_IDL_CONTENT, KAMINO_V1_OVERRIDES_CONTENT, "kamino"); @@ -291,6 +299,7 @@ mod tests { str::FromStr, }; + use anchor_lang_idl::types::IdlType; use solana_pubkey::Pubkey; use surfpool_types::{AccountAddress, PdaSeed}; @@ -518,11 +527,11 @@ mod tests { // Pyth (1) + Jupiter (1) + Raydium CLMM (1) + Raydium AMM v4 (4) + Drift (4) + Meteora (2) // + Kamino (Lend 17, Scope 3, Farms 5, Swap 2, Vault 5, Liquidity 4 = 36) - // + Whirlpool (6) + SPL Token (2) + Pump (2) + PumpSwap (3) = 62 + // + Whirlpool (6) + SPL Token (2) + Pump (2) + PumpSwap (3) + Tessera (6) = 68 assert_eq!( registry.count(), - 62, - "Registry should load 62 templates total" + 68, + "Registry should load 68 templates total" ); assert!(registry.contains("pyth-price-feed-v2")); diff --git a/crates/core/src/tests/live.rs b/crates/core/src/tests/live.rs new file mode 100644 index 000000000..a9891ecfb --- /dev/null +++ b/crates/core/src/tests/live.rs @@ -0,0 +1,68 @@ +//! Shared plumbing for tests that read mainnet. +//! +//! Set `SURFPOOL_TEST_RPC_URL` to use a private endpoint if the public one rate-limits. + +use solana_account::Account; +use solana_commitment_config::CommitmentConfig; +use solana_pubkey::Pubkey; + +use crate::surfnet::remote::SurfnetRemoteClient; + +pub const RPC_URL_ENV: &str = "SURFPOOL_TEST_RPC_URL"; +pub const DEFAULT_RPC_URL: &str = "https://api.mainnet-beta.solana.com"; + +pub fn client() -> SurfnetRemoteClient { + SurfnetRemoteClient::new( + std::env::var(RPC_URL_ENV).unwrap_or_else(|_| DEFAULT_RPC_URL.to_string()), + ) +} + +/// Fetches the accounts in one request, so every account returned is from the same slot. +pub async fn fetch(addresses: &[Pubkey]) -> Vec { + // The public endpoint throttles and intermittently 503s, which has nothing to do with what + // the callers assert. Retry a few times with backoff so a transient refusal is not read as a + // failure. + let mut attempt = 0; + let mut errors = Vec::new(); + let results = loop { + match client() + .get_multiple_accounts(addresses, CommitmentConfig::confirmed()) + .await + { + Ok(results) => break results, + Err(error) if attempt < 4 => { + attempt += 1; + errors.push(format!("attempt {attempt}: {error}")); + tokio::time::sleep(std::time::Duration::from_millis(500 * attempt)).await; + } + Err(error) => { + errors.push(format!("attempt {}: {error}", attempt + 1)); + panic!( + "failed to fetch {addresses:?} from mainnet after {} attempts: {}", + errors.len(), + errors.join("; ") + ); + } + } + }; + + results + .into_iter() + .zip(addresses) + .map(|(result, address)| { + result.map_account().unwrap_or_else(|_| { + panic!("{address} no longer exists on mainnet; the integration needs a new address") + }) + }) + .collect() +} + +/// The offsets at which two buffers differ. +pub fn diff_indices(left: &[u8], right: &[u8]) -> Vec { + left.iter() + .zip(right) + .enumerate() + .filter(|(_, (a, b))| a != b) + .map(|(index, _)| index) + .collect() +} diff --git a/crates/core/src/tests/mod.rs b/crates/core/src/tests/mod.rs index b2dd37925..cb11aa467 100644 --- a/crates/core/src/tests/mod.rs +++ b/crates/core/src/tests/mod.rs @@ -2,7 +2,11 @@ pub mod helpers; pub mod integration; #[cfg(feature = "integration-tests")] pub mod kamino; +#[cfg(feature = "integration-tests")] +pub mod live; pub mod plugin; #[cfg(feature = "integration-tests")] pub mod pump; pub mod simnet_events; +#[cfg(feature = "integration-tests")] +pub mod tessera; diff --git a/crates/core/src/tests/tessera/mod.rs b/crates/core/src/tests/tessera/mod.rs new file mode 100644 index 000000000..323177072 --- /dev/null +++ b/crates/core/src/tests/tessera/mod.rs @@ -0,0 +1,1310 @@ +//! Behavioral proofs for Tessera's raw market layout against the current deployed program. + +use std::collections::HashMap; + +use sha2::{Digest, Sha256}; +use solana_account::Account; +use solana_instruction::{AccountMeta, Instruction}; +use solana_program_runtime::{ + declare_process_instruction, solana_sbpf::program::BuiltinFunctionDefinition, +}; +use solana_pubkey::Pubkey; + +use crate::{ + scenarios::{ + TemplateRegistry, + protocols::tessera::v1::{ + TesseraMarket, build_tessera_depth_scenario, build_tessera_fair_value_scenario, + discover_tessera_markets, + }, + }, + surfnet::svm::SurfnetSvm, + tests::live, +}; + +const TESSERA_PROGRAM: &str = "TessVdML9pBGgG9yGks7o4HewRaXVAMuoVj4x83GLQH"; +const TESSERA_PROGRAMDATA: &str = "BzSXM6KLDpHQQChzr7Fdgbzwp8r8zRYWFFrHK2uZmDYV"; +const TESSERA_GLOBAL_STATE: &str = "8ekCy2jHHUbW2yeNGFWYJT9Hm9FW7SvZcZK66dSZCDiF"; +const TESSERA_SOL_USDC_MARKET: &str = "FLckHLGMJy5gEoXWwcE68Nprde1D4araK4TGLw4pQq2n"; +const TESSERA_CBB_USDC_MARKET: &str = "9NkuAWB4LgCVFV77omEkJEjXqgV5PGupwMTu3B3pBRhc"; +const TESSERA_CBB_VAULT: &str = "37hggNyT4Ec8GEcxMLrWrZyrMSSFMSiFT6VBayRYceZH"; +const CBB_MINT: &str = "cbbtcf3aa214zXHbiAZQwf4122FBYbraNdFqgw4iMij"; +const JUPITER_PROGRAM: &str = "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4"; +const TESSERA_SOL_VAULT: &str = "5pVN5XZB8cYBjNLFrsBCPWkCQBan5K5Mq2dWGzwPgGJV"; +const TESSERA_USDC_VAULT: &str = "9t4P5wMwfFkyn92Z7hf463qYKEZf8ERVZsGBEPNp8uJx"; +const TESSERA_V11_SENTINEL: &str = "8xeaWCsJYxRoudEZGJWURdfrtFhLYZz9b4iHJnW5tb3d"; +const TESSERA_V11_CONFIG: &str = "BAT1Ndpu5gbLTp2AZkSXP79LJBZfCH4B3zGhi6LtvdhK"; +const TESSERA_V11_MARKET_RECORD: &str = "4cG31VNF9TzFinNc7BmnjhFvGjxkY3sCETVMtMgbrhPs"; +const WSOL_MINT: &str = "So11111111111111111111111111111111111111112"; +const USDC_MINT: &str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; +const TOKEN_PROGRAM: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"; +const DFLOW_PROGRAM: &str = "DF1ow4tspfHX9JwWJsAb9epbkA8hmpSEAtxXy1V27QBH"; +const CURRENT_DEPLOY_SLOT: u64 = 446_053_401; +const CURRENT_ELF_SHA256: &str = "82fd37995fcece47a253b1a00c1dd7e4c3fff706b2e42384110e2cbabf4201b3"; + +#[derive(Clone, Copy)] +struct JupiterMarketSpec { + address: &'static str, + base_vault: &'static str, + quote_vault: &'static str, + base_mint: &'static str, + quote_mint: &'static str, + amount_in: u64, + direction: u8, +} + +const JUPITER_MARKETS: [JupiterMarketSpec; 5] = [ + JupiterMarketSpec { + address: TESSERA_CBB_USDC_MARKET, + base_vault: TESSERA_CBB_VAULT, + quote_vault: TESSERA_USDC_VAULT, + base_mint: CBB_MINT, + quote_mint: USDC_MINT, + amount_in: 125_853, + direction: 1, + }, + JupiterMarketSpec { + address: "5X9A6PpFQEsc9D5VdTGfgVyfVn8HnsArQpMMUZZfFg1a", + base_vault: "8FNRrFbq5APT6uZGH6U5DcMNo3U6SDpKoQD3CQMZ5RTU", + quote_vault: TESSERA_USDC_VAULT, + base_mint: "SPCXxcqXj6e5dJDVNovHN8744zkbhM2bYudU45BimGb", + quote_mint: USDC_MINT, + amount_in: 4_000_000, + direction: 0, + }, + JupiterMarketSpec { + address: "7sJf1SmKDDAFtBmMtg253rTbjG7zVFm3zTNounSgSNc9", + base_vault: TESSERA_SOL_VAULT, + quote_vault: "Ci3HZCb6fr5YiYLG9R6XbxHcjm2mDb1R3ugQ3bPZ7oKZ", + base_mint: WSOL_MINT, + quote_mint: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", + amount_in: 77_868_902, + direction: 0, + }, + JupiterMarketSpec { + address: "Ce8WKGKeNPrtk85inFtkpskekaNibZiogSZBrcP7yhTN", + base_vault: "GYaM9Coc9gG4vzqTLRzGAZS6HFaBrebDohMUMYkyADRm", + quote_vault: TESSERA_USDC_VAULT, + base_mint: "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs", + quote_mint: USDC_MINT, + amount_in: 3_931_067, + direction: 1, + }, + JupiterMarketSpec { + address: "DNhfyh75AApg1L1Yig3fErvERKutYRqfWLGb496iViSZ", + base_vault: "FhdiaEWUX8ZrW5TT2iNjWivMCzuBZhUJutrpfw6CvsxU", + quote_vault: TESSERA_USDC_VAULT, + base_mint: "pumpCmXqMfrsAkQ5r49WcJnRayYRqmXz6ae8H7H9Dfn", + quote_mint: USDC_MINT, + amount_in: 8_702, + direction: 0, + }, +]; + +struct JupiterMarketFork { + spec: JupiterMarketSpec, + market: Account, + base_vault: Account, + quote_vault: Account, + base_mint: Account, + quote_mint: Account, +} + +struct TesseraFork { + elf: Vec, + global_state: Account, + market: Account, + base_vault: Account, + quote_vault: Account, + base_mint: Account, + quote_mint: Account, + sentinel: Account, + config: Account, + market_record: Account, + jupiter_markets: Vec, +} + +declare_process_instruction!(TesseraCpiWrapper, 1, |invoke_context| { + let instruction = { + let context = invoke_context + .transaction_context + .get_current_instruction_context()?; + let accounts = (1..context.get_number_of_instruction_accounts()) + .map(|index| { + Ok(AccountMeta { + pubkey: *context.get_key_of_instruction_account(index)?, + is_signer: context.is_instruction_account_signer(index)?, + is_writable: context.is_instruction_account_writable(index)?, + }) + }) + .collect::, solana_instruction::error::InstructionError>>()?; + Instruction { + program_id: Pubkey::from_str_const(TESSERA_PROGRAM), + accounts, + data: context.get_instruction_data().to_vec(), + } + }; + invoke_context.native_invoke_signed(instruction, &[]) +}); + +async fn fetch_accounts(addresses: &[&str]) -> Vec { + let pubkeys: Vec = addresses + .iter() + .map(|address| Pubkey::from_str_const(address)) + .collect(); + live::fetch(&pubkeys).await +} + +async fn tessera_fork() -> TesseraFork { + let (cbb, spcx, wsol_usdt, weth, pump) = tokio::join!( + fetch_jupiter_market(JUPITER_MARKETS[0]), + fetch_jupiter_market(JUPITER_MARKETS[1]), + fetch_jupiter_market(JUPITER_MARKETS[2]), + fetch_jupiter_market(JUPITER_MARKETS[3]), + fetch_jupiter_market(JUPITER_MARKETS[4]), + ); + let jupiter_markets = vec![cbb, spcx, wsol_usdt, weth, pump]; + let mut accounts = fetch_accounts(&[ + TESSERA_PROGRAMDATA, + TESSERA_GLOBAL_STATE, + TESSERA_SOL_USDC_MARKET, + TESSERA_SOL_VAULT, + TESSERA_USDC_VAULT, + WSOL_MINT, + USDC_MINT, + TESSERA_V11_SENTINEL, + TESSERA_V11_CONFIG, + TESSERA_V11_MARKET_RECORD, + ]) + .await; + let programdata = accounts.remove(0); + assert_eq!(programdata.data.len(), 576_977, "ProgramData size changed"); + assert_eq!( + read_u64(&programdata.data, 4), + CURRENT_DEPLOY_SLOT, + "Tessera was redeployed; revalidate the raw layout" + ); + let elf = programdata.data[45..].to_vec(); + assert_eq!( + hex::encode(Sha256::digest(&elf)), + CURRENT_ELF_SHA256, + "Tessera ELF changed without a ProgramData address change" + ); + + TesseraFork { + elf, + global_state: accounts.remove(0), + market: accounts.remove(0), + base_vault: accounts.remove(0), + quote_vault: accounts.remove(0), + base_mint: accounts.remove(0), + quote_mint: accounts.remove(0), + sentinel: accounts.remove(0), + config: accounts.remove(0), + market_record: accounts.remove(0), + jupiter_markets, + } +} + +async fn fetch_jupiter_market(spec: JupiterMarketSpec) -> JupiterMarketFork { + let mut accounts = fetch_accounts(&[ + spec.address, + spec.base_vault, + spec.quote_vault, + spec.base_mint, + spec.quote_mint, + ]) + .await; + JupiterMarketFork { + spec, + market: accounts.remove(0), + base_vault: accounts.remove(0), + quote_vault: accounts.remove(0), + base_mint: accounts.remove(0), + quote_mint: accounts.remove(0), + } +} + +fn token_account(mint: &Pubkey, owner: &Pubkey, amount: u64) -> Vec { + let mut data = vec![0u8; 165]; + data[0..32].copy_from_slice(mint.as_ref()); + data[32..64].copy_from_slice(owner.as_ref()); + data[64..72].copy_from_slice(&amount.to_le_bytes()); + data[108] = 1; + data +} + +fn native_token_account(mint: &Pubkey, owner: &Pubkey, amount: u64) -> Vec { + let mut data = token_account(mint, owner, amount); + data[109..113].copy_from_slice(&1u32.to_le_bytes()); + data[113..121].copy_from_slice(&2_039_280u64.to_le_bytes()); + data +} + +fn token_amount(data: &[u8]) -> u64 { + read_u64(data, 64) +} + +fn token_owner(data: &[u8]) -> Pubkey { + Pubkey::new_from_array(data[32..64].try_into().expect("token owner")) +} + +fn last_restart_slot_account() -> Account { + Account { + lamports: 1_000_000, + data: 246_464_040u64.to_le_bytes().to_vec(), + owner: Pubkey::from_str_const("Sysvar1111111111111111111111111111111111111"), + executable: false, + rent_epoch: 0, + } +} + +fn user_token_account( + mint: &Pubkey, + owner: &Pubkey, + token_program: Pubkey, + amount: u64, +) -> Account { + let is_native = mint == &Pubkey::from_str_const(WSOL_MINT); + Account { + lamports: if is_native { + amount.saturating_add(2_039_280) + } else { + 10_000_000 + }, + data: if is_native { + native_token_account(mint, owner, amount) + } else { + token_account(mint, owner, amount) + }, + owner: token_program, + executable: false, + rent_epoch: 0, + } +} + +fn sign_and_send( + svm: &mut litesvm::LiteSVM, + taker: &solana_keypair::Keypair, + instructions: &[Instruction], + destination_key: Pubkey, +) -> Result { + use solana_signer::Signer; + use solana_transaction::Transaction; + + let mut message = solana_message::Message::new(instructions, Some(&taker.pubkey())); + message.recent_blockhash = svm.latest_blockhash(); + let signature_count = message.header.num_required_signatures as usize; + let mut transaction = Transaction::new_unsigned(message); + transaction.signatures = vec![solana_signature::Signature::default(); signature_count]; + transaction.signatures[0] = taker.sign_message(&transaction.message.serialize()); + + svm.send_transaction(transaction) + .map_err(|error| format!("{error:?}"))?; + Ok(token_amount( + &svm.get_account(&destination_key) + .expect("destination account") + .data, + )) +} + +fn tessera_run( + fork: &TesseraFork, + amount_in: u64, + direction: u8, + sentinel_signer: bool, + global_writable: bool, + mutate: impl FnOnce(&mut Vec), +) -> Result { + use litesvm::LiteSVM; + use solana_keypair::Keypair; + use solana_signer::Signer; + + let program_id = Pubkey::from_str_const(TESSERA_PROGRAM); + let global_state_key = Pubkey::from_str_const(TESSERA_GLOBAL_STATE); + let market_key = Pubkey::from_str_const(TESSERA_SOL_USDC_MARKET); + let base_vault_key = Pubkey::from_str_const(TESSERA_SOL_VAULT); + let quote_vault_key = Pubkey::from_str_const(TESSERA_USDC_VAULT); + let base_mint_key = Pubkey::from_str_const(WSOL_MINT); + let quote_mint_key = Pubkey::from_str_const(USDC_MINT); + let token_program = Pubkey::from_str_const(TOKEN_PROGRAM); + let sentinel_key = Pubkey::from_str_const(TESSERA_V11_SENTINEL); + let config_key = Pubkey::from_str_const(TESSERA_V11_CONFIG); + let market_record_key = Pubkey::from_str_const(TESSERA_V11_MARKET_RECORD); + let mut market = fork.market.data.clone(); + let market_slot = read_u64(&market, 120); + mutate(&mut market); + + let mut svm = LiteSVM::new() + .with_sigverify(false) + .with_blockhash_check(false); + svm.add_program(program_id, &fork.elf) + .map_err(|error| format!("add_program: {error:?}"))?; + svm.add_builtin( + Pubkey::from_str_const(DFLOW_PROGRAM), + TesseraCpiWrapper::register, + ); + let mut clock: solana_clock::Clock = svm.get_sysvar(); + clock.slot = market_slot + 1; + clock.unix_timestamp = 1_787_551_143; + svm.set_sysvar(&clock); + svm.set_account( + Pubkey::from_str_const("SysvarLastRestartS1ot1111111111111111111111"), + last_restart_slot_account(), + ) + .map_err(|error| format!("set last restart slot: {error:?}"))?; + svm.set_account(global_state_key, fork.global_state.clone()) + .map_err(|error| format!("set global state: {error:?}"))?; + let mut market_account = fork.market.clone(); + market_account.data = market; + svm.set_account(market_key, market_account) + .map_err(|error| format!("set market: {error:?}"))?; + svm.set_account(base_vault_key, fork.base_vault.clone()) + .map_err(|error| format!("set base vault: {error:?}"))?; + svm.set_account(quote_vault_key, fork.quote_vault.clone()) + .map_err(|error| format!("set quote vault: {error:?}"))?; + svm.set_account(base_mint_key, fork.base_mint.clone()) + .map_err(|error| format!("set base mint: {error:?}"))?; + svm.set_account(quote_mint_key, fork.quote_mint.clone()) + .map_err(|error| format!("set quote mint: {error:?}"))?; + svm.set_account(sentinel_key, fork.sentinel.clone()) + .map_err(|error| format!("set sentinel: {error:?}"))?; + svm.set_account(config_key, fork.config.clone()) + .map_err(|error| format!("set config: {error:?}"))?; + svm.set_account(market_record_key, fork.market_record.clone()) + .map_err(|error| format!("set market record: {error:?}"))?; + + let taker = Keypair::new(); + svm.airdrop(&taker.pubkey(), 10_000_000_000) + .map_err(|error| format!("airdrop: {error:?}"))?; + let source_key = Pubkey::new_unique(); + let destination_key = Pubkey::new_unique(); + let (source_mint, destination_mint) = if direction == 1 { + (base_mint_key, quote_mint_key) + } else { + (quote_mint_key, base_mint_key) + }; + let (user_base_key, user_quote_key) = if direction == 1 { + (source_key, destination_key) + } else { + (destination_key, source_key) + }; + svm.set_account( + source_key, + user_token_account(&source_mint, &taker.pubkey(), token_program, amount_in), + ) + .map_err(|error| format!("set source: {error:?}"))?; + svm.set_account( + destination_key, + user_token_account(&destination_mint, &taker.pubkey(), token_program, 0), + ) + .map_err(|error| format!("set destination: {error:?}"))?; + + let mut data = vec![0x11, direction]; + data.extend_from_slice(&amount_in.to_le_bytes()); + data.extend_from_slice(&0u64.to_le_bytes()); + data.push(0); + let mut budget = vec![2u8]; + budget.extend_from_slice(&1_400_000u32.to_le_bytes()); + let global_state_meta = if global_writable { + AccountMeta::new(global_state_key, false) + } else { + AccountMeta::new_readonly(global_state_key, false) + }; + let instructions = vec![ + Instruction { + program_id: Pubkey::from_str_const("ComputeBudget111111111111111111111111111111"), + accounts: vec![], + data: budget, + }, + Instruction { + program_id: Pubkey::from_str_const(DFLOW_PROGRAM), + accounts: vec![ + AccountMeta::new_readonly(program_id, false), + global_state_meta, + AccountMeta::new(market_key, false), + AccountMeta::new(taker.pubkey(), true), + AccountMeta::new(base_vault_key, false), + AccountMeta::new(quote_vault_key, false), + AccountMeta::new(user_base_key, false), + AccountMeta::new(user_quote_key, false), + AccountMeta::new_readonly(base_mint_key, false), + AccountMeta::new_readonly(quote_mint_key, false), + AccountMeta::new_readonly(token_program, false), + AccountMeta::new_readonly(token_program, false), + AccountMeta::new_readonly(sentinel_key, sentinel_signer), + AccountMeta::new_readonly(config_key, false), + AccountMeta::new_readonly(market_record_key, false), + ], + data, + }, + ]; + sign_and_send(&mut svm, &taker, &instructions, destination_key) +} + +fn tessera_run_jupiter( + fork: &TesseraFork, + market_fork: &JupiterMarketFork, + amount_in: u64, + direction: u8, + mutate: impl FnOnce(&mut Vec), +) -> Result { + use litesvm::LiteSVM; + use solana_keypair::Keypair; + use solana_signer::Signer; + + let program_id = Pubkey::from_str_const(TESSERA_PROGRAM); + let global_state_key = Pubkey::from_str_const(TESSERA_GLOBAL_STATE); + let market_key = Pubkey::from_str_const(market_fork.spec.address); + let base_vault_key = Pubkey::from_str_const(market_fork.spec.base_vault); + let quote_vault_key = Pubkey::from_str_const(market_fork.spec.quote_vault); + let base_mint_key = Pubkey::from_str_const(market_fork.spec.base_mint); + let quote_mint_key = Pubkey::from_str_const(market_fork.spec.quote_mint); + let base_token_program = market_fork.base_mint.owner; + let quote_token_program = market_fork.quote_mint.owner; + let instructions_sysvar = Pubkey::from_str_const("Sysvar1nstructions1111111111111111111111111"); + let config_key = Pubkey::from_str_const(TESSERA_V11_CONFIG); + let market_record_key = Pubkey::from_str_const(TESSERA_V11_MARKET_RECORD); + let mut market = market_fork.market.data.clone(); + let market_slot = read_u64(&market, 120); + mutate(&mut market); + + let mut svm = LiteSVM::new() + .with_sigverify(false) + .with_blockhash_check(false); + svm.add_program(program_id, &fork.elf) + .map_err(|error| format!("add_program: {error:?}"))?; + svm.add_builtin( + Pubkey::from_str_const(JUPITER_PROGRAM), + TesseraCpiWrapper::register, + ); + let mut clock: solana_clock::Clock = svm.get_sysvar(); + clock.slot = market_slot + 1; + clock.unix_timestamp = 1_787_662_925; + svm.set_sysvar(&clock); + svm.set_account( + Pubkey::from_str_const("SysvarLastRestartS1ot1111111111111111111111"), + last_restart_slot_account(), + ) + .map_err(|error| format!("set last restart slot: {error:?}"))?; + svm.set_account(global_state_key, fork.global_state.clone()) + .map_err(|error| format!("set global state: {error:?}"))?; + svm.set_account(config_key, fork.config.clone()) + .map_err(|error| format!("set config: {error:?}"))?; + svm.set_account(market_record_key, fork.market_record.clone()) + .map_err(|error| format!("set market record: {error:?}"))?; + let mut market_account = market_fork.market.clone(); + market_account.data = market; + svm.set_account(market_key, market_account) + .map_err(|error| format!("set market: {error:?}"))?; + svm.set_account(base_vault_key, market_fork.base_vault.clone()) + .map_err(|error| format!("set base vault: {error:?}"))?; + svm.set_account(quote_vault_key, market_fork.quote_vault.clone()) + .map_err(|error| format!("set quote vault: {error:?}"))?; + svm.set_account(base_mint_key, market_fork.base_mint.clone()) + .map_err(|error| format!("set base mint: {error:?}"))?; + svm.set_account(quote_mint_key, market_fork.quote_mint.clone()) + .map_err(|error| format!("set quote mint: {error:?}"))?; + + let taker = Keypair::new(); + svm.airdrop(&taker.pubkey(), 10_000_000_000) + .map_err(|error| format!("airdrop: {error:?}"))?; + let source_key = Pubkey::new_unique(); + let destination_key = Pubkey::new_unique(); + let (source_mint, source_program, destination_mint, destination_program) = if direction == 1 { + ( + base_mint_key, + base_token_program, + quote_mint_key, + quote_token_program, + ) + } else { + ( + quote_mint_key, + quote_token_program, + base_mint_key, + base_token_program, + ) + }; + let (user_base_key, user_quote_key) = if direction == 1 { + (source_key, destination_key) + } else { + (destination_key, source_key) + }; + svm.set_account( + source_key, + user_token_account(&source_mint, &taker.pubkey(), source_program, amount_in), + ) + .map_err(|error| format!("set source: {error:?}"))?; + svm.set_account( + destination_key, + user_token_account(&destination_mint, &taker.pubkey(), destination_program, 0), + ) + .map_err(|error| format!("set destination: {error:?}"))?; + + let mut data = vec![0x10, direction]; + data.extend_from_slice(&amount_in.to_le_bytes()); + data.extend_from_slice(&0u64.to_le_bytes()); + let instruction = Instruction { + program_id: Pubkey::from_str_const(JUPITER_PROGRAM), + accounts: vec![ + AccountMeta::new_readonly(program_id, false), + AccountMeta::new_readonly(global_state_key, false), + AccountMeta::new(market_key, false), + AccountMeta::new(taker.pubkey(), true), + AccountMeta::new(base_vault_key, false), + AccountMeta::new(quote_vault_key, false), + AccountMeta::new(user_base_key, false), + AccountMeta::new(user_quote_key, false), + AccountMeta::new_readonly(base_mint_key, false), + AccountMeta::new_readonly(quote_mint_key, false), + AccountMeta::new_readonly(base_token_program, false), + AccountMeta::new_readonly(quote_token_program, false), + AccountMeta::new_readonly(instructions_sysvar, false), + AccountMeta::new_readonly(config_key, false), + AccountMeta::new_readonly(market_record_key, false), + ], + data, + }; + sign_and_send(&mut svm, &taker, &[instruction], destination_key) +} + +fn write_u64(data: &mut [u8], offset: usize, value: u64) { + data[offset..offset + 8].copy_from_slice(&value.to_le_bytes()); +} + +fn read_u64(data: &[u8], offset: usize) -> u64 { + u64::from_le_bytes(data[offset..offset + 8].try_into().expect("u64 field")) +} + +fn expected_first_level_output(market: &[u8], amount_in: u64, direction: u8) -> u64 { + let (price_offset, factor_offset) = match direction { + 0 => (144, 648), + 1 => (128, 168), + _ => panic!("unsupported Tessera direction {direction}"), + }; + let output = u128::from(amount_in) + .checked_mul(u128::from(read_u64(market, price_offset))) + .and_then(|value| value.checked_mul(u128::from(read_u64(market, factor_offset)))) + .expect("Tessera first-level quote multiplication") + / 1_000_000_000_000_000_000_000_u128; + u64::try_from(output).expect("Tessera first-level quote fits u64") +} + +// A small input can still start beyond level zero because prior flow and the selected +// configuration advance the ladder. Formula probes explicitly isolate an unconsumed start. +/// Puts the local fixture in the state the first-level formula assumes: no consumed depth at +/// 0/8, and all five selectable configurations neutral (no ppm adjustment at 1136, factor scale +/// 1,000,000 at 1140, `skipped_levels` leading levels at 1144). Which configuration the program +/// selects depends on the market record's age, so every one of them is normalized. +fn set_quote_start(data: &mut [u8], skipped_levels: u8) { + write_u64(data, 0, 0); + write_u64(data, 8, 0); + for selector in 0..5 { + let config = 1136 + 12 * selector; + data[config..config + 4].copy_from_slice(&0u32.to_le_bytes()); + data[config + 4..config + 8].copy_from_slice(&1_000_000u32.to_le_bytes()); + data[config + 8] = skipped_levels; + } +} + +fn scale_ladder(market: &[u8], sell_bps: u16, buy_bps: u16) -> HashMap { + const SELL_FACTOR: usize = 168; + const BUY_FACTOR: usize = 648; + let mut values = HashMap::with_capacity(LADDER_LEVELS * 2); + for (side, first_offset, bps) in [ + ("sell_levels", SELL_FACTOR, sell_bps), + ("buy_levels", BUY_FACTOR, buy_bps), + ] { + for level in 0..LADDER_LEVELS { + let live = read_u64(market, first_offset + level * LADDER_RECORD_SIZE); + let scaled = (u128::from(live) * u128::from(bps) / 10_000) as u64; + assert!( + live == 0 || scaled > 0, + "{side}.{level}.factor rounds a live nonzero value to zero at {bps} bps" + ); + values.insert( + format!("{side}.{level}.factor"), + serde_json::json!(scaled.to_string()), + ); + } + } + values +} + +const LADDER_LEVELS: usize = 20; +const LADDER_RECORD_SIZE: usize = 24; + +fn apply_template( + data: &mut Vec, + template_id: &str, + values: HashMap, + target_slot: u64, +) { + let registry = TemplateRegistry::new(); + let template = registry.get(template_id).expect("Tessera template"); + *data = template + .raw_layout + .as_ref() + .expect("Tessera raw layout") + .materialize(data, &template.properties, &values, target_slot) + .unwrap_or_else(|error| panic!("{template_id} did not materialize: {error}")); +} + +fn assert_only_ranges_changed(before: &[u8], after: &[u8], ranges: &[(usize, usize)]) { + assert_eq!(after.len(), before.len()); + for index in live::diff_indices(before, after) { + assert!( + ranges + .iter() + .any(|(start, end)| (*start..*end).contains(&index)), + "unexpected changed byte at {index}" + ); + } +} + +#[tokio::test] +async fn tessera_templates_guard_market_and_preserve_unwritten_bytes() { + let fork = tessera_fork().await; + let program_id = Pubkey::from_str_const(TESSERA_PROGRAM); + assert_eq!(fork.market.owner, program_id); + assert_eq!(fork.market.data.len(), 1264); + let vault_authority = token_owner(&fork.base_vault.data); + assert_eq!(token_owner(&fork.quote_vault.data), vault_authority); + for market in &fork.jupiter_markets { + assert_eq!(token_owner(&market.base_vault.data), vault_authority); + assert_eq!(token_owner(&market.quote_vault.data), vault_authority); + } + eprintln!("Tessera shared vault authority: {vault_authority}"); + + let registry = TemplateRegistry::new(); + let fair_value = registry.get("tessera-fair-value").expect("fair value"); + let layout = fair_value.raw_layout.as_ref().expect("raw layout"); + assert!(layout.guard(&fork.market.data).is_ok()); + for market in &fork.jupiter_markets { + assert_eq!(market.market.owner, program_id); + assert_eq!(market.market.data.len(), 1264); + assert!(layout.guard(&market.market.data).is_ok()); + } + let mut wrong_guard_bytes = fork.market.data.clone(); + wrong_guard_bytes[96] ^= 1; + assert!(layout.guard(&wrong_guard_bytes).is_err()); + assert!(layout.guard(&fork.market.data[..1263]).is_err()); + + let template = registry.get("tessera-depth").expect("ladder template"); + let values: HashMap = template + .properties + .iter() + .map(|property| { + let offset = property.offset.expect("raw property offset"); + let current = read_u64(&fork.market.data, offset); + (property.path.clone(), serde_json::json!(current / 2)) + }) + .collect(); + let forged = template + .raw_layout + .as_ref() + .expect("raw layout") + .materialize(&fork.market.data, &template.properties, &values, 0) + .expect("depth materializes"); + let ranges: Vec<(usize, usize)> = template + .properties + .iter() + .map(|property| { + let offset = property.offset.expect("raw property offset"); + let expected = values[&property.path].as_u64().expect("u64 value"); + assert_eq!(read_u64(&forged, offset), expected); + (offset, offset + 8) + }) + .collect(); + assert_only_ranges_changed(&fork.market.data, &forged, &ranges); +} + +#[tokio::test] +async fn tessera_builders_materialize_and_keep_quotes_fresh() { + const BASE_SLOT: u64 = 1_000_000; + + let fork = tessera_fork().await; + let market_key = Pubkey::from_str_const(TESSERA_SOL_USDC_MARKET); + let market = + TesseraMarket::validate(market_key, &fork.market, &fork.base_mint, &fork.quote_mint) + .expect("validate WSOL/USDC market"); + let preparation = build_tessera_fair_value_scenario(&market, "100.25") + .expect("build Tessera fair-value scenario"); + let original = fork.market.data.clone(); + let (mut svm, _simnet_events_rx, _geyser_events_rx) = SurfnetSvm::default(); + svm.inner + .set_account(market_key, fork.market) + .expect("seed Tessera market"); + svm.register_scenario(preparation.scenario, Some(BASE_SLOT)) + .expect("register Tessera scenario"); + + svm.materialize_overrides_for_slot(&None, BASE_SLOT) + .await + .expect("materialize Tessera scenario"); + let materialized = svm + .inner + .get_account(&market_key) + .expect("get Tessera market") + .expect("Tessera market present") + .data; + assert_eq!(read_u64(&materialized, 120), BASE_SLOT); + assert_eq!( + read_u64(&materialized, 128), + preparation.quote_atoms_per_base_atom_x1e15 + ); + assert_eq!( + read_u64(&materialized, 144), + preparation.base_atoms_per_quote_atom_x1e15 + ); + assert_only_ranges_changed( + &original, + &materialized, + &[(120, 128), (128, 136), (144, 152)], + ); + + // The builder queues nothing past its preparation slot: the prepared bytes stay as written. + assert!( + svm.scheduled_overrides + .get(&(BASE_SLOT + 1)) + .expect("read the next slot's queue") + .is_none(), + "the builder must not queue an override past its preparation slot" + ); + svm.materialize_overrides_for_slot(&None, BASE_SLOT + 1) + .await + .expect("materialize the next Tessera slot"); + let next_slot = svm + .inner + .get_account(&market_key) + .expect("get Tessera market") + .expect("Tessera market present") + .data; + assert_eq!(read_u64(&next_slot, 120), BASE_SLOT); + assert_eq!( + read_u64(&next_slot, 128), + preparation.quote_atoms_per_base_atom_x1e15 + ); + assert_eq!( + read_u64(&next_slot, 144), + preparation.base_atoms_per_quote_atom_x1e15 + ); + assert_eq!( + next_slot, materialized, + "an override that is not scheduled again leaves the account alone" + ); +} + +#[tokio::test] +async fn tessera_depth_builder_prepares_cbb_swaps_in_both_directions() { + let mut fork = tessera_fork().await; + set_quote_start(&mut fork.jupiter_markets[0].market.data, 0); + let cbb = &fork.jupiter_markets[0]; + let address = Pubkey::from_str_const(cbb.spec.address); + let slot = read_u64(&cbb.market.data, 120) + 1; + for (sell_bps, buy_bps) in [(1000, 10000), (10000, 1000), (5000, 2500)] { + let scenario = + build_tessera_depth_scenario(address, &cbb.market, sell_bps, buy_bps).unwrap(); + let mut expected = cbb.market.data.clone(); + for (start, bps) in [(160, sell_bps), (640, buy_bps)] { + for level in 0..20 { + let offset = start + level * 24; + if expected[offset + 16] != 0 { + let capacity = read_u64(&cbb.market.data, offset); + write_u64( + &mut expected, + offset, + (u128::from(capacity) * u128::from(bps) / 10000) as u64, + ); + } + } + } + write_u64(&mut expected, 120, slot); + eprintln!( + "cbBTC depth sell_bps={sell_bps} buy_bps={buy_bps}: expected sell level 1 {} -> {}, buy level 1 {} -> {}", + read_u64(&cbb.market.data, 160), + read_u64(&expected, 160), + read_u64(&cbb.market.data, 640), + read_u64(&expected, 640) + ); + let (mut svm, _events, _geyser) = SurfnetSvm::default(); + svm.inner.set_account(address, cbb.market.clone()).unwrap(); + svm.register_scenario(scenario, Some(slot)).unwrap(); + assert_eq!( + svm.inner.get_account(&address).unwrap().unwrap(), + cbb.market + ); + svm.materialize_overrides_for_slot(&None, slot) + .await + .unwrap(); + let prepared = svm.inner.get_account(&address).unwrap().unwrap(); + let mut expected_account = cbb.market.clone(); + expected_account.data = expected; + assert_eq!(prepared, expected_account); + for (direction, offset, bps) in [(1, 160, sell_bps), (0, 640, buy_bps)] { + let first_capacity = read_u64(&cbb.market.data, offset); + let small_input = first_capacity / 20; + let large_input = first_capacity.checked_mul(2).unwrap(); + assert!(small_input > 0); + let small_expected = + expected_first_level_output(&cbb.market.data, small_input, direction); + let small_before = + tessera_run_jupiter(&fork, cbb, small_input, direction, |_| {}).unwrap(); + let small_after = tessera_run_jupiter(&fork, cbb, small_input, direction, |data| { + *data = prepared.data.clone() + }) + .unwrap(); + assert_eq!(small_before, small_expected); + assert_eq!(small_after, small_expected); + let large_before = + tessera_run_jupiter(&fork, cbb, large_input, direction, |_| {}).unwrap(); + let large_after = tessera_run_jupiter(&fork, cbb, large_input, direction, |data| { + *data = prepared.data.clone() + }) + .unwrap(); + if bps == 10000 { + assert_eq!(large_after, large_before); + } else { + assert!( + large_after > 0 && large_after < large_before, + "reduced depth must worsen a large fill: {large_before} -> {large_after}" + ); + } + eprintln!( + "cbBTC direction={direction} small_in={small_input} expected_out={small_expected} actual_out={small_after}; large_in={large_input} baseline_out={large_before} prepared_out={large_after}" + ); + } + } +} + +#[tokio::test] +async fn tessera_stale_quote_template_lands_every_configured_rejection_boundary() { + let fork = tessera_fork().await; + let base_slot = read_u64(&fork.market.data, 120) + 1; + let amount_in = 238_781_608; + let baseline = tessera_run(&fork, amount_in, 1, true, false, |_| {}) + .expect("fresh quote must fill the control"); + assert!(baseline > 0); + + // One template covers every limit because the supplied value is the lead. Null takes the + // template's own -20, which is what the default market needs. + for (lead, age_slots) in [ + (serde_json::Value::Null, 20), + (serde_json::json!(-20), 20), + (serde_json::json!(-25), 25), + (serde_json::json!(-55), 55), + ] { + let mut configured = fork.market.clone(); + write_u64(&mut configured.data, 88, age_slots); + let original = configured.data.clone(); + + let mut staged = configured.data.clone(); + apply_template( + &mut staged, + "tessera-stale-quote", + HashMap::from([("last_update_slot".to_string(), lead.clone())]), + base_slot, + ); + assert_eq!( + read_u64(&staged, 120), + base_slot - age_slots, + "lead {lead} must write the materialization slot minus {age_slots}" + ); + assert_only_ranges_changed(&original, &staged, &[(88, 96), (120, 128)]); + + let stale = tessera_run(&fork, amount_in, 1, true, false, |market| { + *market = staged; + }) + .expect_err("a quote at the configured rejection age must be rejected"); + assert!(stale.contains("Custom(65535)"), "lead {lead}: {stale}"); + } + + // One slot younger than the boundary still fills, which is what makes the boundary a boundary. + let mut fresh_enough = fork.market.data.clone(); + apply_template( + &mut fresh_enough, + "tessera-stale-quote", + HashMap::from([("last_update_slot".to_string(), serde_json::json!(-19))]), + base_slot, + ); + let accepted = tessera_run(&fork, amount_in, 1, true, false, |market| { + *market = fresh_enough; + }) + .expect("age 19 must still fill"); + assert!(accepted > 0); +} + +#[tokio::test] +async fn tessera_current_layout_controls_price_depth_and_freshness() { + let mut fork = tessera_fork().await; + set_quote_start(&mut fork.market.data, 0); + assert_eq!(fork.market.data.len(), 1264, "market layout size changed"); + assert_eq!( + &fork.market.data[24..56], + Pubkey::from_str_const(WSOL_MINT).as_ref() + ); + assert_eq!( + &fork.market.data[56..88], + Pubkey::from_str_const(USDC_MINT).as_ref() + ); + + let amount_in = 238_781_608; + let baseline = tessera_run(&fork, amount_in, 1, true, false, |_| {}).expect("baseline sell"); + let inverse_only = tessera_run(&fork, amount_in, 1, true, false, |market| { + let inverse = read_u64(market, 144); + write_u64(market, 144, inverse / 2); + }) + .expect("sell with buy-side-only mutation"); + let doubled = tessera_run(&fork, amount_in, 1, true, false, |market| { + let price = read_u64(market, 128); + let inverse = read_u64(market, 144); + apply_template( + market, + "tessera-fair-value", + HashMap::from([ + ( + "quote_atoms_per_base_atom_x1e15".to_string(), + serde_json::json!(price * 2), + ), + ( + "base_atoms_per_quote_atom_x1e15".to_string(), + serde_json::json!(inverse / 2), + ), + ]), + 0, + ); + }) + .expect("doubled-price swap"); + let buy_amount_in = 22_000_000; + let baseline_buy = + tessera_run(&fork, buy_amount_in, 0, true, false, |_| {}).expect("baseline buy"); + let direct_only_buy = tessera_run(&fork, buy_amount_in, 0, true, false, |market| { + let price = read_u64(market, 128); + write_u64(market, 128, price * 2); + }) + .expect("buy with sell-side-only mutation"); + let doubled_price_buy = tessera_run(&fork, buy_amount_in, 0, true, false, |market| { + let price = read_u64(market, 128); + let inverse = read_u64(market, 144); + apply_template( + market, + "tessera-fair-value", + HashMap::from([ + ( + "quote_atoms_per_base_atom_x1e15".to_string(), + serde_json::json!(price * 2), + ), + ( + "base_atoms_per_quote_atom_x1e15".to_string(), + serde_json::json!(inverse / 2), + ), + ]), + 0, + ); + }) + .expect("doubled-price buy"); + let market_slot = read_u64(&fork.market.data, 120); + let clock_slot = market_slot + 1; + let configured_freshness_boundary = 5; + let fresh_at_configured_boundary = tessera_run(&fork, amount_in, 1, true, false, |market| { + write_u64(market, 88, configured_freshness_boundary); + apply_template( + market, + "tessera-freshness", + HashMap::from([("last_update_slot".to_string(), serde_json::json!(0))]), + clock_slot.saturating_sub(configured_freshness_boundary - 1), + ); + }); + let stale_at_configured_boundary = tessera_run(&fork, amount_in, 1, true, false, |market| { + write_u64(market, 88, configured_freshness_boundary); + apply_template( + market, + "tessera-freshness", + HashMap::from([("last_update_slot".to_string(), serde_json::json!(0))]), + clock_slot.saturating_sub(configured_freshness_boundary), + ); + }); + let unsigned_sentinel = tessera_run(&fork, amount_in, 1, false, false, |_| {}) + .expect_err("unsigned DFlow sentinel must be rejected"); + let writable_global = tessera_run(&fork, amount_in, 1, true, true, |_| {}) + .expect_err("writable global state must be rejected"); + + let expected_first_level_sell_output = + expected_first_level_output(&fork.market.data, amount_in, 1); + let expected_first_level_buy_output = + expected_first_level_output(&fork.market.data, buy_amount_in, 0); + + eprintln!( + "Tessera sell={baseline}, expected_sell={expected_first_level_sell_output}, inverse_only={inverse_only}, doubled={doubled}, buy={baseline_buy}, expected_buy={expected_first_level_buy_output}, direct_only_buy={direct_only_buy}, doubled_price_buy={doubled_price_buy}" + ); + assert!( + baseline > 0, + "the current deployed program must fill the control" + ); + assert_eq!(baseline, expected_first_level_sell_output); + assert_eq!(baseline_buy, expected_first_level_buy_output); + assert!( + doubled > baseline * 19 / 10 && doubled < baseline * 21 / 10, + "atomic price override should approximately double the quote" + ); + assert_eq!( + inverse_only, baseline, + "the buy-side inverse must not affect a base-to-quote sell" + ); + assert!( + doubled_price_buy > baseline_buy * 4 / 10 && doubled_price_buy < baseline_buy * 6 / 10, + "doubling quote/base price must approximately halve base bought with quote" + ); + assert_eq!( + direct_only_buy, baseline_buy, + "the sell-side direct price must not affect a quote-to-base buy" + ); + assert!(unsigned_sentinel.contains("Custom(0)")); + assert!(writable_global.contains("Custom(1)")); + assert!(fresh_at_configured_boundary.is_ok()); + assert!( + stale_at_configured_boundary + .expect_err("configured freshness boundary must reject") + .contains("Custom(65535)") + ); +} + +#[tokio::test] +async fn tessera_cbb_market_proves_generic_price_and_curve_layout() { + let mut fork = tessera_fork().await; + set_quote_start(&mut fork.jupiter_markets[0].market.data, 0); + let cbb = &fork.jupiter_markets[0]; + let market_key = Pubkey::from_str_const(TESSERA_CBB_USDC_MARKET); + let market = TesseraMarket::validate(market_key, &cbb.market, &cbb.base_mint, &cbb.quote_mint) + .expect("validate CBB/USDC market"); + assert_eq!(market.base_mint, Pubkey::from_str_const(CBB_MINT)); + assert_eq!(market.quote_mint, Pubkey::from_str_const(USDC_MINT)); + assert_eq!(market.base_decimals, 8); + assert_eq!(market.quote_decimals, 6); + + let amount_in = cbb.spec.amount_in.min(read_u64(&cbb.market.data, 160) / 2); + assert!(amount_in > 0); + let active_curve_values = scale_ladder(&cbb.market.data, 5_000, 10_000); + let inactive_curve_values = scale_ladder(&cbb.market.data, 10_000, 5_000); + let baseline = + tessera_run_jupiter(&fork, cbb, amount_in, 1, |_| {}).expect("CBB baseline sell"); + let factors_half = tessera_run_jupiter(&fork, cbb, amount_in, 1, |market| { + apply_template(market, "tessera-curve", active_curve_values.clone(), 0); + }) + .expect("CBB half-factor sell"); + let inactive_factors_half = tessera_run_jupiter(&fork, cbb, amount_in, 1, |market| { + apply_template(market, "tessera-curve", inactive_curve_values.clone(), 0); + }) + .expect("CBB inactive-factor control"); + let invalid_single_factor = tessera_run_jupiter(&fork, cbb, amount_in, 1, |market| { + write_u64(market, 168, 500_000); + }) + .expect_err("a single unordered factor must be rejected"); + let disabled_first_level = tessera_run_jupiter(&fork, cbb, amount_in, 1, |market| { + market[176] = 0; + }) + .expect_err("disabling the required first level must reject the quote"); + + assert!(baseline > 0); + assert_eq!( + baseline, + expected_first_level_output(&cbb.market.data, amount_in, 1) + ); + assert!(factors_half > baseline * 49 / 100 && factors_half < baseline * 51 / 100); + assert_eq!(inactive_factors_half, baseline); + assert!(invalid_single_factor.contains("Custom(8)")); + assert!(disabled_first_level.contains("Custom(65535)")); +} + +#[tokio::test] +async fn tessera_halt_template_rejects_both_quote_directions() { + let mut fork = tessera_fork().await; + for skipped_levels in [None, Some(1)] { + if let Some(skip) = skipped_levels { + set_quote_start(&mut fork.jupiter_markets[0].market.data, skip); + } + let cbb = &fork.jupiter_markets[0]; + let mut halted = cbb.market.data.clone(); + apply_template( + &mut halted, + "tessera-halt", + HashMap::from([ + ("sell_level_0_enabled".to_string(), serde_json::json!(0)), + ("buy_level_0_enabled".to_string(), serde_json::json!(0)), + ]), + 0, + ); + let mut expected = cbb.market.data.clone(); + for start in [176, 656] { + for level in 0..LADDER_LEVELS { + expected[start + level * LADDER_RECORD_SIZE] = 0; + } + } + assert_eq!(halted, expected); + for (direction, amount) in [(1, cbb.spec.amount_in), (0, 4_000_000)] { + let baseline = tessera_run_jupiter(&fork, cbb, amount, direction, |_| {}).unwrap(); + assert!(baseline > 0); + if skipped_levels.is_some() { + let first_only = tessera_run_jupiter(&fork, cbb, amount, direction, |data| { + data[176] = 0; + data[656] = 0; + }) + .unwrap(); + assert_eq!( + first_only, baseline, + "disabling skipped levels cannot halt a quote" + ); + } + let error = tessera_run_jupiter(&fork, cbb, amount, direction, |data| { + *data = halted.clone(); + }) + .expect_err("halting every level must reject either direction"); + assert!(error.contains("Custom(65535)"), "{error}"); + } + } +} + +#[tokio::test] +async fn tessera_four_additional_markets_prove_price_and_curve_directions() { + let mut fork = tessera_fork().await; + for market in &mut fork.jupiter_markets[1..] { + set_quote_start(&mut market.market.data, 0); + } + for market_fork in &fork.jupiter_markets[1..] { + let market_key = Pubkey::from_str_const(market_fork.spec.address); + // Every one of these markets must clear the same owner, size and byte guard. + TesseraMarket::validate( + market_key, + &market_fork.market, + &market_fork.base_mint, + &market_fork.quote_mint, + ) + .unwrap_or_else(|error| panic!("{} validation failed: {error}", market_fork.spec.address)); + + let direction = market_fork.spec.direction; + // Live ladders move between runs; the first-level formula only holds while the probe + // fits in level 1, so the probe follows the live capacity down. + let first_capacity = read_u64( + &market_fork.market.data, + if direction == 1 { 160 } else { 640 }, + ); + assert!( + first_capacity >= 2, + "{} has insufficient first-level capacity for the probe: {first_capacity}", + market_fork.spec.address + ); + let amount_in = market_fork.spec.amount_in.min(first_capacity / 2); + let baseline = tessera_run_jupiter(&fork, market_fork, amount_in, direction, |_| {}) + .unwrap_or_else(|error| { + panic!("{} baseline failed: {error}", market_fork.spec.address) + }); + let repriced = tessera_run_jupiter(&fork, market_fork, amount_in, direction, |market| { + let direct = read_u64(market, 128); + let inverse = read_u64(market, 144); + write_u64(market, 128, direct * 2); + write_u64(market, 144, inverse / 2); + }) + .unwrap_or_else(|error| { + panic!("{} repriced swap failed: {error}", market_fork.spec.address) + }); + let (active_sell_bps, active_buy_bps) = if direction == 1 { + (5_000, 10_000) + } else { + (10_000, 5_000) + }; + let active_curve_values = + scale_ladder(&market_fork.market.data, active_sell_bps, active_buy_bps); + let inactive_curve_values = + scale_ladder(&market_fork.market.data, active_buy_bps, active_sell_bps); + let active_factors_half = + tessera_run_jupiter(&fork, market_fork, amount_in, direction, |market| { + apply_template(market, "tessera-curve", active_curve_values.clone(), 0) + }) + .unwrap_or_else(|error| { + panic!( + "{} active-factor swap failed: {error}", + market_fork.spec.address + ) + }); + let inactive_factors_half = + tessera_run_jupiter(&fork, market_fork, amount_in, direction, |market| { + apply_template(market, "tessera-curve", inactive_curve_values.clone(), 0) + }) + .unwrap_or_else(|error| { + panic!( + "{} inactive-factor swap failed: {error}", + market_fork.spec.address + ) + }); + + eprintln!( + "Tessera market={} direction={} baseline={} repriced={} active_factors_half={} inactive_factors_half={}", + market_fork.spec.address, + direction, + baseline, + repriced, + active_factors_half, + inactive_factors_half + ); + assert!(baseline > 0); + assert_eq!( + baseline, + expected_first_level_output(&market_fork.market.data, amount_in, direction) + ); + if direction == 1 { + assert!(repriced > baseline * 19 / 10 && repriced < baseline * 21 / 10); + } else { + assert!(repriced > baseline * 4 / 10 && repriced < baseline * 6 / 10); + } + assert!( + active_factors_half > baseline * 49 / 100 && active_factors_half < baseline * 51 / 100 + ); + assert_eq!(inactive_factors_half, baseline); + } +} + +#[tokio::test] +async fn tessera_discovers_live_markets() { + let markets = discover_tessera_markets(&live::client()) + .await + .expect("discover live markets"); + assert!(!markets.is_empty(), "Tessera must expose market accounts"); + let addresses = markets + .iter() + .map(|market| market.address) + .collect::>(); + let unique = addresses.iter().collect::>(); + assert_eq!(unique.len(), markets.len()); + let mut accounts = Vec::new(); + for batch in addresses.chunks(100) { + accounts.extend(live::fetch(batch).await); + } + let mut mints = markets + .iter() + .flat_map(|market| [market.base_mint, market.quote_mint]) + .collect::>(); + mints.sort_unstable(); + mints.dedup(); + let mut mint_accounts = Vec::new(); + for batch in mints.chunks(100) { + mint_accounts.extend(live::fetch(batch).await); + } + for (market, account) in markets.iter().zip(&accounts) { + let (base, quote) = + TesseraMarket::mint_addresses(account).expect("valid discovered market"); + let index = |mint| mints.binary_search(mint).expect("fetched mint"); + let expected = TesseraMarket::validate( + market.address, + account, + &mint_accounts[index(&base)], + &mint_accounts[index("e)], + ) + .unwrap(); + assert_eq!(*market, expected); + assert!(!market.label().is_empty()); + } + eprintln!( + "Discovered {} Tessera markets from program accounts", + markets.len() + ); +} diff --git a/crates/mcp/src/surfpool/mod.rs b/crates/mcp/src/surfpool/mod.rs index d98665cbd..aa810e967 100644 --- a/crates/mcp/src/surfpool/mod.rs +++ b/crates/mcp/src/surfpool/mod.rs @@ -19,8 +19,14 @@ use start_surfnet::StartSurfnetResponse; use surfpool_core::{ scenarios::{ TemplateRegistry, - protocols::pump::v1::graduation_builder::{ - build_pump_graduation_scenario, pump_graduation_addresses, + protocols::{ + pump::v1::graduation_builder::{ + build_pump_graduation_scenario, pump_graduation_addresses, + }, + tessera::v1::{ + TESSERA_DEFAULT_MARKET, TesseraMarket, build_tessera_depth_scenario, + build_tessera_fair_value_scenario, discover_tessera_markets, + }, }, }, solana_account::Account, @@ -37,6 +43,47 @@ use crate::helpers::find_next_available_surfnet_port; mod set_token_account; mod start_surfnet; +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct CreateTesseraFairValueScenarioParams { + #[schemars( + description = "The Tessera market account. Resolve an address through list_tessera_markets; omit to use the default SOL/USDC market." + )] + pub market: Option, + #[schemars( + description = "The price of one base token in quote tokens, as a positive decimal string such as \"100.25\". Not atomic units: the builder derives the scale from the market's mint decimals." + )] + pub price: String, + #[schemars( + description = "The port of the target running local surfnet instance (e.g., 8899, 18899, 28899, etc.). Omit to use the default port, 8899." + )] + pub surfnet_port: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct ListTesseraMarketsParams { + #[schemars(description = "The target local Surfnet RPC port. Omit to use 8899.")] + pub surfnet_port: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct CreateTesseraDepthScenarioParams { + #[schemars(description = "Market account address from list_tessera_markets.")] + pub market: String, + #[schemars( + description = "Remaining base-to-quote sell depth in basis points, 1..10000. Reducing by 90% means 1000; 10000 leaves sells unchanged." + )] + pub sell_remaining_bps: u16, + #[schemars( + description = "Remaining quote-to-base buy depth in basis points, 1..10000. Reducing by 90% means 1000; 10000 leaves buys unchanged." + )] + pub buy_remaining_bps: u16, + #[schemars(description = "Target local Surfnet RPC port. Omit to use 8899.")] + pub surfnet_port: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct StartSurfnetParams { #[schemars( @@ -399,7 +446,23 @@ impl TokenAddressResponse { } } +fn parse_market(address: &str) -> Result { + Pubkey::from_str(address.trim()) + .map_err(|error| format!("Invalid Tessera market pubkey: {error}")) +} + impl Surfpool { + async fn tessera_market_account( + &self, + surfnet_port: Option, + market: Pubkey, + ) -> Result { + self.fetch_surfnet_accounts(surfnet_port, &[market]) + .await? + .remove(0) + .ok_or_else(|| format!("Tessera market account {market} was not found")) + } + /// Reads through the surfnet's own RPC: local state wins, only missing /// accounts fall back to its remote source. async fn fetch_surfnet_accounts( @@ -1023,6 +1086,109 @@ impl Surfpool { self.stage_scenario(preparation.scenario).await } + #[tool( + description = "Lists Tessera markets discovered from program accounts on the target Surfnet. Returns market addresses, pair labels, base/quote mints and decimals, and each market's freshness limit. Use addresses to create scenarios; labels are display names and unknown symbols use mint addresses." + )] + async fn list_tessera_markets( + &self, + Parameters(params): Parameters, + ) -> Result { + let port = params.surfnet_port.unwrap_or(DEFAULT_RPC_PORT); + let client = SurfnetRemoteClient::new(format!("http://127.0.0.1:{port}")); + let markets = match discover_tessera_markets(&client).await { + Ok(markets) => markets, + Err(error) => return Ok(scenario_tool_error(error.to_string())), + }; + let markets = markets + .iter() + .map(|market| { + serde_json::json!({ + "address": market.address.to_string(), + "label": market.label(), + "baseMint": market.base_mint.to_string(), + "quoteMint": market.quote_mint.to_string(), + "baseDecimals": market.base_decimals, + "quoteDecimals": market.quote_decimals, + "freshnessLimitSlots": market.freshness_limit_slots, + }) + }) + .collect::>(); + Ok(CallToolResult::success(vec![Content::text( + serde_json::json!({ "count": markets.len(), "markets": markets }).to_string(), + )])) + } + + #[tool( + description = "Creates one editable Tessera fair-value scenario for a live market. Reads the market and both mint accounts from the running surfnet, derives the pair of reciprocal atomic ratios from their decimals, and keeps the quote fresh while the scenario runs. Prepares state; sends no swap. Resolve `market` through list_tessera_markets." + )] + async fn create_tessera_fair_value_scenario( + &self, + Parameters(params): Parameters, + ) -> Result { + let scenario: Result = async { + let market_address = match params.market.as_deref().map(str::trim) { + None | Some("") => TESSERA_DEFAULT_MARKET, + Some(address) => parse_market(address)?, + }; + let market_account = self + .tessera_market_account(params.surfnet_port, market_address) + .await?; + let (base_mint, quote_mint) = TesseraMarket::mint_addresses(&market_account) + .map_err(|error| error.to_string())?; + let mints = self + .fetch_surfnet_accounts(params.surfnet_port, &[base_mint, quote_mint]) + .await?; + let (Some(base_account), Some(quote_account)) = (mints[0].as_ref(), mints[1].as_ref()) + else { + return Err(format!( + "Tessera market {market_address} references a mint that was not found" + )); + }; + let market = TesseraMarket::validate( + market_address, + &market_account, + base_account, + quote_account, + ) + .map_err(|error| error.to_string())?; + build_tessera_fair_value_scenario(&market, ¶ms.price) + .map(|preparation| preparation.scenario) + .map_err(|error| error.to_string()) + } + .await; + match scenario { + Ok(scenario) => self.stage_scenario(scenario).await, + Err(error) => Ok(scenario_tool_error(error)), + } + } + + #[tool( + description = "Creates one editable Tessera depth-reduction scenario from the selected market's current Surfnet state. Scales only enabled capacities with exact integer arithmetic, preserving prices, factors, disabled levels and any unchanged direction. Keeps quotes fresh. A 90% reduction means 1000 remaining basis points. Prepares state; does not Play or send swaps. Report validation failures without substituting another market." + )] + async fn create_tessera_depth_scenario( + &self, + Parameters(params): Parameters, + ) -> Result { + let scenario: Result = async { + let market = parse_market(¶ms.market)?; + let account = self + .tessera_market_account(params.surfnet_port, market) + .await?; + build_tessera_depth_scenario( + market, + &account, + params.sell_remaining_bps, + params.buy_remaining_bps, + ) + .map_err(|error| error.to_string()) + } + .await; + match scenario { + Ok(scenario) => self.stage_scenario(scenario).await, + Err(error) => Ok(scenario_tool_error(error)), + } + } + #[tool( description = "Lists all override templates as a light index: {id, name, description, protocol, accountType, tags, hasLlmContext}. Call this first to pick a templateId, then get_override_template for that one template's full detail (properties, address, llmContext). Constants are resolved with search_constant_options." )] @@ -1372,6 +1538,40 @@ mod tests { }) } + #[tokio::test] + async fn tessera_tools_reject_a_bad_market_before_any_rpc() { + let surfpool = Surfpool::new(); + let results = [ + surfpool + .create_tessera_fair_value_scenario(Parameters( + CreateTesseraFairValueScenarioParams { + surfnet_port: None, + market: Some("not-a-pubkey".to_string()), + price: "100.25".to_string(), + }, + )) + .await, + surfpool + .create_tessera_depth_scenario(Parameters(CreateTesseraDepthScenarioParams { + market: "not-a-pubkey".to_string(), + sell_remaining_bps: 1000, + buy_remaining_bps: 10000, + surfnet_port: None, + })) + .await, + ]; + for result in results { + let result = result + .expect("the tool reports input errors in its payload, not as a protocol error"); + assert!( + json_of(&result)["error"] + .as_str() + .unwrap() + .contains("Invalid Tessera market pubkey") + ); + } + } + #[tokio::test] async fn the_template_index_is_light_and_omits_the_heavy_fields() { let surfpool = Surfpool::new(); diff --git a/crates/types/src/scenarios.rs b/crates/types/src/scenarios.rs index 2da0f3a21..3f1453456 100644 --- a/crates/types/src/scenarios.rs +++ b/crates/types/src/scenarios.rs @@ -1016,6 +1016,10 @@ pub enum RawEncoding { count: usize, stride: usize, }, + U8Strided { + count: usize, + stride: usize, + }, /// A base58 pubkey, written as 32 bytes. Bytes32, /// The slot the override materializes at, plus `lead` (may be negative). @@ -1028,7 +1032,7 @@ impl RawEncoding { /// Byte width of this encoding. pub fn width(&self) -> usize { match self { - RawEncoding::U8 => 1, + RawEncoding::U8 | RawEncoding::U8Strided { .. } => 1, RawEncoding::U16 => 2, RawEncoding::U32 | RawEncoding::I32 | RawEncoding::I32Strided { .. } => 4, RawEncoding::U64 | RawEncoding::I64 | RawEncoding::Slot { .. } => 8, @@ -1043,7 +1047,8 @@ impl RawEncoding { /// encodings with the same loop instead of special-casing one of them. pub fn placements(&self) -> (usize, usize) { match self { - RawEncoding::I32Strided { count, stride } => (*count, *stride), + RawEncoding::I32Strided { count, stride } + | RawEncoding::U8Strided { count, stride } => (*count, *stride), other => (1, other.width()), } } @@ -1077,7 +1082,7 @@ impl RawEncoding { }}; } Ok(match self { - RawEncoding::U8 => int!(u8, "u8"), + RawEncoding::U8 | RawEncoding::U8Strided { .. } => int!(u8, "u8"), RawEncoding::U16 => int!(u16, "u16"), RawEncoding::U32 => int!(u32, "u32"), RawEncoding::U64 => int!(u64, "u64"), @@ -1752,6 +1757,83 @@ mod tests { assert!(err.contains("exceeds"), "unexpected error: {err}"); } + #[test] + fn u8_strided_round_trips_and_rejects_out_of_range_values() { + use super::RawEncoding; + + let encoding = RawEncoding::U8Strided { + count: 3, + stride: 8, + }; + let serialized = json!({"u8_strided": {"count": 3, "stride": 8}}); + assert_eq!(serde_json::to_value(&encoding).unwrap(), serialized); + assert_eq!( + serde_json::from_value::(serialized).unwrap(), + encoding + ); + for value in [json!(-1), json!(256), json!("256")] { + let err = encoding.encode(&value, 0).expect_err("outside u8 range"); + assert!(err.contains("invalid u8"), "unexpected error: {err}"); + } + } + + #[test] + fn u8_strided_writes_single_bytes_and_preserves_padding() { + use super::{Property, RawEncoding, RawLayout}; + + let layout = RawLayout { + account_size: 19, + magic: None, + }; + let mut property = Property::field("flags".to_string()); + property.offset = Some(2); + property.encoding = Some(RawEncoding::U8Strided { + count: 3, + stride: 8, + }); + let properties = [property]; + layout.validate_properties(&properties).unwrap(); + for value in [0, 255] { + let out = layout + .materialize( + &[0xa5; 19], + &properties, + &HashMap::from([("flags".to_string(), json!(value))]), + 0, + ) + .unwrap(); + let mut expected = [0xa5; 19]; + for offset in [2, 10, 18] { + expected[offset] = value; + } + assert_eq!(out, expected); + } + } + + #[test] + fn u8_strided_rejects_invalid_run_bounds_count_and_stride() { + use super::{Property, RawEncoding, RawLayout}; + + let layout = RawLayout { + account_size: 19, + magic: None, + }; + for (offset, count, stride, message) in [ + (3, 3, 8, "beyond"), + (2, 0, 8, "zero placements"), + (2, 3, usize::MAX, "stride overflow"), + (usize::MAX, 1, 1, "offset overflow"), + ] { + let mut property = Property::field("flags".to_string()); + property.offset = Some(offset); + property.encoding = Some(RawEncoding::U8Strided { count, stride }); + let err = layout + .validate_properties(&[property]) + .expect_err("invalid strided run"); + assert!(err.contains(message), "unexpected error: {err}"); + } + } + #[test] fn u16_be_ref_rejects_out_of_range_values() { let seed = PdaSeed::U16BeRef("index".to_string());