ttv_er/
lib.rs

1use borsh::{BorshDeserialize, BorshSerialize};
2use solana_program::{account_info::AccountInfo, pubkey::Pubkey};
3use std::str::FromStr;
4use ttv_curve::math::{exchange_rate, logit, proportion_pt};
5use ttv_fixed_point::Number;
6
7/// Financial parameters for the market
8#[derive(BorshSerialize, BorshDeserialize, Clone, Default)]
9pub struct MarketFinancials {
10    /// Expiration timestamp, which is copied from the vault associated with the PT
11    pub expiration_ts: u64,
12
13    /// Balance of PT in the market
14    /// This amount is tracked separately to prevent bugs from token transfers directly to the market
15    pub pt_balance: u64,
16
17    /// Balance of SY in the market
18    /// This amount is tracked separately to prevent bugs from token transfers directly to the market
19    pub sy_balance: u64,
20
21    /// Initial log of fee rate, which decreases over time
22    pub ln_fee_rate_root: f64,
23
24    /// Last seen log of implied rate (APY) for PT
25    /// Used to maintain continuity of the APY between trades over time
26    pub last_ln_implied_rate: f64,
27
28    /// Initial rate scalar, which increases over time
29    pub rate_scalar_root: f64,
30}
31
32impl MarketFinancials {
33    pub const SIZE_OF: usize = 8 + 8 + 8 + 16 + 16 + 16;
34
35    fn asset_balance(&self, sy_exchange_rate: Number) -> Number {
36        Number::from_natural_u64(self.sy_balance) * sy_exchange_rate
37    }
38
39    fn current_rate_anchor(&self, sy_exchange_rate: Number, now: u64) -> f64 {
40        let sec_remaining = self.sec_remaining(now);
41        let asset = self.asset_balance(sy_exchange_rate).floor_u64();
42        let current_rate_scalar = self.current_rate_scalar(now);
43        ttv_curve::math::find_rate_anchor(
44            self.pt_balance,
45            asset,
46            current_rate_scalar,
47            self.last_ln_implied_rate.into(),
48            sec_remaining,
49        )
50    }
51
52    fn sec_remaining(&self, now: u64) -> u64 {
53        if now > self.expiration_ts {
54            0
55        } else {
56            self.expiration_ts - now
57        }
58    }
59
60    fn current_rate_scalar(&self, now: u64) -> f64 {
61        let sec_remaining = self.sec_remaining(now);
62        ttv_curve::math::rate_scalar::<f64>(self.rate_scalar_root, sec_remaining)
63    }
64}
65
66pub struct GetExchangeRateResult {
67    pub pt_asset_exchange_rate: f64,
68}
69
70pub fn get_exchange_rate(
71    market_account_info: &AccountInfo,
72    vault_account_info: &AccountInfo,
73    current_unix_timestamp: u64,
74) -> GetExchangeRateResult {
75    let program_id = Pubkey::from_str("ExponentnaRg3CQbW6dqQNZKXp7gtZ9DGMp1cwC4HAS7").unwrap();
76    assert_eq!(
77        market_account_info.owner, &program_id,
78        "Market account not owned by program"
79    );
80    assert_eq!(
81        vault_account_info.owner, &program_id,
82        "Vault account not owned by program"
83    );
84
85    let market_data = market_account_info.try_borrow_data().unwrap();
86    let vault_data = vault_account_info.try_borrow_data().unwrap();
87
88    assert_eq!(
89        &market_data[0..8],
90        &[212, 4, 132, 126, 169, 121, 121, 20],
91        "Invalid market account discriminator"
92    );
93    assert_eq!(
94        &vault_data[0..8],
95        &[211, 8, 232, 43, 2, 152, 117, 119],
96        "Invalid vault account discriminator"
97    );
98
99    let final_sy_exchange_rate = Number::from_bytes_le(&vault_data[401..401 + 32]);
100    let financials =
101        MarketFinancials::deserialize(&mut &market_data[364..364 + MarketFinancials::SIZE_OF])
102            .unwrap();
103
104    let asset_balance = financials.asset_balance(final_sy_exchange_rate);
105    let asset_balance_u64 = asset_balance.floor_u64();
106
107    let p = proportion_pt::<f64>(financials.pt_balance, asset_balance_u64);
108    let l_p = logit(p);
109
110    let current_rate_scalar = financials.current_rate_scalar(current_unix_timestamp);
111    let rate_anchor =
112        financials.current_rate_anchor(final_sy_exchange_rate, current_unix_timestamp);
113    let pt_asset_rate = exchange_rate(l_p, current_rate_scalar, rate_anchor);
114
115    GetExchangeRateResult {
116        pt_asset_exchange_rate: 1.00 / pt_asset_rate,
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    #[cfg(not(target_os = "solana"))]
124    use solana_client::rpc_client::RpcClient;
125
126    #[test]
127    #[cfg(not(target_os = "solana"))]
128    fn test_exchange_rates_with_real_data() {
129        // Initialize RPC client
130        let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
131        let client = RpcClient::new(rpc_url);
132
133        // Example vault address - replace with a real vault address for testing
134        let vault_address =
135            Pubkey::from_str("xzJdaENdahnFpPNZCTb36oRUtar3ZnRHMVB4ewDdRBe").unwrap();
136
137        // Derive market address
138        let program_id = Pubkey::from_str("ExponentnaRg3CQbW6dqQNZKXp7gtZ9DGMp1cwC4HAS7").unwrap();
139        let (market_address, _) =
140            Pubkey::find_program_address(&[b"market", vault_address.as_ref()], &program_id);
141
142        // Get current Solana timestamp
143        let slot = client.get_slot().expect("Failed to get slot");
144        let current_time = client
145            .get_block_time(slot)
146            .expect("Failed to get block time") as u64;
147
148        // Fetch accounts
149        let mut vault_account = client
150            .get_account(&vault_address)
151            .expect("Failed to get vault account");
152        let mut market_account = client
153            .get_account(&market_address)
154            .expect("Failed to get market account");
155
156        // Create AccountInfo structs
157        let mut vault_lamports = vault_account.lamports;
158        let mut market_lamports = market_account.lamports;
159
160        let vault_account_info = AccountInfo::new(
161            &vault_address,
162            false,
163            false,
164            &mut vault_lamports,
165            &mut vault_account.data,
166            &program_id,
167            false,
168            0,
169        );
170
171        let market_account_info = AccountInfo::new(
172            &market_address,
173            false,
174            false,
175            &mut market_lamports,
176            &mut market_account.data,
177            &program_id,
178            false,
179            0,
180        );
181
182        // Get exchange rates
183        let result = get_exchange_rate(&market_account_info, &vault_account_info, current_time);
184
185        println!("PT -> Asset rate: {}", result.pt_asset_exchange_rate);
186    }
187}