tape/program/
advance.rs

1use tape_api::prelude::*;
2use steel::*;
3
4const LOW_REWARD_THRESHOLD: u64  = 32;
5const HIGH_REWARD_THRESHOLD: u64 = 256;
6const SMOOTHING_FACTOR: u64      = 2;
7
8pub fn process_advance(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult {
9    let current_time = Clock::get()?.unix_timestamp;
10    let [
11        signer_info, 
12        spool_0_info, 
13        spool_1_info, 
14        spool_2_info, 
15        spool_3_info, 
16        spool_4_info, 
17        spool_5_info, 
18        spool_6_info, 
19        spool_7_info, 
20        epoch_info, 
21        mint_info, 
22        treasury_info, 
23        treasury_ata_info, 
24        token_program_info
25    ] = accounts else {
26        return Err(ProgramError::NotEnoughAccountKeys);
27    };
28
29    signer_info.is_signer()?;
30
31    let spool_0 = spool_0_info
32        .as_account_mut::<Spool>(&tape_api::ID)?
33        .assert_mut(|s| s.id == 0)?;
34    let spool_1 = spool_1_info
35        .as_account_mut::<Spool>(&tape_api::ID)?
36        .assert_mut(|s| s.id == 1)?;
37    let spool_2 = spool_2_info
38        .as_account_mut::<Spool>(&tape_api::ID)?
39        .assert_mut(|s| s.id == 2)?;
40    let spool_3 = spool_3_info
41        .as_account_mut::<Spool>(&tape_api::ID)?
42        .assert_mut(|s| s.id == 3)?;
43    let spool_4 = spool_4_info
44        .as_account_mut::<Spool>(&tape_api::ID)?
45        .assert_mut(|s| s.id == 4)?;
46    let spool_5 = spool_5_info
47        .as_account_mut::<Spool>(&tape_api::ID)?
48        .assert_mut(|s| s.id == 5)?;
49    let spool_6 = spool_6_info
50        .as_account_mut::<Spool>(&tape_api::ID)?
51        .assert_mut(|s| s.id == 6)?;
52    let spool_7 = spool_7_info
53        .as_account_mut::<Spool>(&tape_api::ID)?
54        .assert_mut(|s| s.id == 7)?;
55
56    let spools = [spool_0, spool_1, spool_2, spool_3, spool_4, spool_5, spool_6, spool_7];
57
58    let epoch = epoch_info
59        .is_epoch()?
60        .as_account_mut::<Epoch>(&tape_api::ID)?;
61
62    let mint = mint_info
63        .has_address(&MINT_ADDRESS)?
64        .is_writable()?
65        .as_mint()?;
66
67    treasury_info.is_treasury()?.is_writable()?;
68    treasury_ata_info.is_treasury_ata()?.is_writable()?;
69    token_program_info.is_program(&spl_token::ID)?;
70
71    // Check if the epoch is ready to be processed
72    if still_active(epoch, current_time) {
73        return Ok(());
74    } 
75
76    // Check if the max supply has been reached
77    let mint_supply = mint.supply();
78    if mint_supply >= MAX_SUPPLY {
79        return Err(TapeError::MaxSupply.into());
80    }
81
82    // Adjust emissions rate (per minute)
83    epoch.target_rate = get_emissions_rate(mint_supply);
84
85    // Calculate target rewards
86    let target_rewards = epoch.target_rate * EPOCH_DURATION_MINUTES as u64;
87
88    solana_program::msg!(
89        "epoch.target_rate: {}, target_rewards: {}",
90        epoch.target_rate,
91        target_rewards
92    );
93
94    // Process spools and calculate mint amount
95    let (amount_to_mint, actual_rewards) =
96        update_spools(spools, mint_supply, target_rewards);
97
98    // Update reward rate
99    epoch.base_rate = compute_new_reward_rate(
100        epoch.base_rate,
101        actual_rewards,
102        target_rewards,
103    );
104
105    // Adjust difficulty
106    adjust_difficulty(epoch);
107
108    solana_program::msg!(
109        "previous rewards: {}",
110        actual_rewards,
111    );
112
113    solana_program::msg!(
114        "new epoch.base_rate: {}",
115        epoch.base_rate
116    );
117
118    solana_program::msg!(
119        "new epoch.difficulty: {}",
120        epoch.difficulty
121    );
122
123    solana_program::msg!(
124        "minting: {}",
125        amount_to_mint,
126    );
127
128    // Fund the treasury token account.
129    mint_to_signed(
130        mint_info,
131        treasury_ata_info,
132        treasury_info,
133        token_program_info,
134        amount_to_mint,
135        &[TREASURY],
136    )?;
137
138    // Increment epoch number
139    epoch.number += 1;
140    epoch.last_epoch_at = current_time;
141
142    Ok(())
143}
144
145// Helper: Check if the epoch is still active.
146#[inline(always)]
147fn still_active(epoch: &Epoch, current_time: i64) -> bool {
148    epoch.last_epoch_at
149        .saturating_add(EPOCH_DURATION_MINUTES)
150        .gt(&current_time)
151}
152
153// Helper: Top up spools with rewards and calculate excess mint supply needed.
154#[inline(always)]
155fn update_spools(
156    spools: [&mut Spool; SPOOL_COUNT],
157    mint_supply: u64,
158    target_rewards: u64,
159) -> (u64, u64) {
160    let mut amount_to_mint = 0u64;
161    let mut available_supply = MAX_SUPPLY.saturating_sub(mint_supply);
162    let mut theoretical_rewards = 0u64;
163
164    for spool in spools {
165        let spool_topup = target_rewards
166            .saturating_sub(spool.available_rewards)
167            .min(available_supply);
168
169        theoretical_rewards       += spool.theoretical_rewards;
170        spool.theoretical_rewards  = 0;
171
172        available_supply          -= spool_topup;
173        amount_to_mint            += spool_topup;
174        spool.available_rewards   += spool_topup;
175    }
176
177    (amount_to_mint, theoretical_rewards)
178}
179
180// Helper: Adjust difficulty based on reward rate thresholds. Very much the same as what ORE does.
181// Taking their lead here for consistency and to avoid any potential issues with the reward rate.
182//
183// Reference: https://github.com/regolith-labs/ore/blob/c18503d0ee98b8a7823b993b38823b7867059659/program/src/reset.rs#L130-L140
184#[inline(always)]
185fn adjust_difficulty(epoch: &mut Epoch) {
186    if epoch.base_rate < LOW_REWARD_THRESHOLD {
187        epoch.difficulty += 1;
188        epoch.base_rate *= 2;
189    }
190
191    if epoch.base_rate >= HIGH_REWARD_THRESHOLD && epoch.difficulty > 1 {
192        epoch.difficulty -= 1;
193        epoch.base_rate /= 2;
194    }
195
196    epoch.difficulty = epoch.difficulty.max(7);
197}
198
199// Helper: Compute new reward rate based on current rate and epoch rewards.
200//
201// Formula:
202// new_rate = current_rate * (target_rewards / actual_rewards)
203//
204// Following what ORE here to avoid footguns.
205// Reference: https://github.com/regolith-labs/ore/blob/c18503d0ee98b8a7823b993b38823b7867059659/program/src/reset.rs#L146
206#[inline(always)]
207fn compute_new_reward_rate(
208    current_rate: u64,
209    actual_rewards: u64,
210    target_rewards: u64,
211) -> u64 {
212
213    if actual_rewards == 0 {
214        return current_rate;
215    }
216
217    let adjusted_rate = (current_rate as u128)
218        .saturating_mul(target_rewards as u128)
219        .saturating_div(actual_rewards as u128) as u64;
220
221    let min_rate = current_rate.saturating_div(SMOOTHING_FACTOR);
222    let max_rate = current_rate.saturating_mul(SMOOTHING_FACTOR);
223    let smoothed_rate = adjusted_rate.min(max_rate).max(min_rate);
224
225    smoothed_rate
226        .max(1)
227        .min(target_rewards)
228}
229
230// Pre-computed emissions rate based on current supply. Decay of ~14% every 12 months with
231// a target of 7 million TAPE.
232pub fn get_emissions_rate(current_supply: u64) -> u64 {
233    match current_supply {
234        n if n < ONE_TAPE * 1000000 => 19025875190, // Year 1: ~1.90 TAPE/min
235        n if n < ONE_TAPE * 1861000 => 16381278538, // Year 2: ~1.64 TAPE/min
236        n if n < ONE_TAPE * 2602321 => 14104280821, // Year 3: ~1.41 TAPE/min
237        n if n < ONE_TAPE * 3240598 => 12143785787, // Year 4: ~1.21 TAPE/min
238        n if n < ONE_TAPE * 3790155 => 10455799563, // Year 5: ~1.05 TAPE/min
239        n if n < ONE_TAPE * 4263323 => 9002443423,  // Year 6: ~0.90 TAPE/min
240        n if n < ONE_TAPE * 4670721 => 7751103787,  // Year 7: ~0.78 TAPE/min
241        n if n < ONE_TAPE * 5021491 => 6673700361,  // Year 8: ~0.67 TAPE/min
242        n if n < ONE_TAPE * 5323504 => 5746056011,  // Year 9: ~0.57 TAPE/min
243        n if n < ONE_TAPE * 5583536 => 4947354225,  // Year 10: ~0.49 TAPE/min
244        n if n < ONE_TAPE * 5807425 => 4259671988,  // Year 11: ~0.43 TAPE/min
245        n if n < ONE_TAPE * 6000193 => 3667577581,  // Year 12: ~0.37 TAPE/min
246        n if n < ONE_TAPE * 6166166 => 3157784298,  // Year 13: ~0.32 TAPE/min
247        n if n < ONE_TAPE * 6309069 => 2718852280,  // Year 14: ~0.27 TAPE/min
248        n if n < ONE_TAPE * 6432108 => 2340931813,  // Year 15: ~0.23 TAPE/min
249        n if n < ONE_TAPE * 6538045 => 2015542291,  // Year 16: ~0.20 TAPE/min
250        n if n < ONE_TAPE * 6629257 => 1735381912,  // Year 17: ~0.17 TAPE/min
251        n if n < ONE_TAPE * 6707790 => 1494163827,  // Year 18: ~0.15 TAPE/min
252        n if n < ONE_TAPE * 6775407 => 1286475055,  // Year 19: ~0.13 TAPE/min
253        n if n < ONE_TAPE * 6833625 => 1107655022,  // Year 20: ~0.11 TAPE/min
254        n if n < ONE_TAPE * 6883751 => 953690974,   // Year 21: ~0.10 TAPE/min
255        n if n < ONE_TAPE * 6926910 => 821127928,   // Year 22: ~0.08 TAPE/min
256        n if n < ONE_TAPE * 6964069 => 706991146,   // Year 23: ~0.07 TAPE/min
257        n if n < ONE_TAPE * 6996064 => 608719377,   // Year 24: ~0.06 TAPE/min
258        n if n < ONE_TAPE * 7000000 => 524107383,   // Year 25: ~0.05 TAPE/min
259        _ => 0,
260    }
261}