namada_shielded_token/
conversion.rs

1//! MASP rewards conversions
2
3#[cfg(any(feature = "multicore", test))]
4use std::collections::BTreeMap;
5
6#[cfg(any(feature = "multicore", test))]
7use masp_primitives::asset_type::AssetType;
8#[cfg(any(feature = "multicore", test))]
9use masp_primitives::convert::{AllowedConversion, UncheckedAllowedConversion};
10#[cfg(any(feature = "multicore", test))]
11use masp_primitives::transaction::components::I128Sum as MaspAmount;
12use namada_controller::PDController;
13use namada_core::address::{Address, MASP};
14#[cfg(any(feature = "multicore", test))]
15use namada_core::arith::CheckedAdd;
16use namada_core::arith::checked;
17#[cfg(any(feature = "multicore", test))]
18use namada_core::borsh::BorshSerializeExt;
19use namada_core::dec::Dec;
20#[cfg(any(feature = "multicore", test))]
21use namada_core::hash::Hash;
22use namada_core::masp::Precision;
23#[cfg(any(feature = "multicore", test))]
24use namada_core::masp::{MaspEpoch, encode_asset_type};
25#[cfg(any(feature = "multicore", test))]
26use namada_core::token::MaspDigitPos;
27use namada_core::token::{Amount, DenominatedAmount, Denomination};
28use namada_core::uint::Uint;
29#[cfg(any(feature = "multicore", test))]
30use namada_state::iter_prefix_with_filter_map;
31use namada_systems::{parameters, trans_token};
32
33#[cfg(any(feature = "multicore", test))]
34use crate::storage_key::{
35    is_masp_conversion_key, is_masp_scheduled_reward_precision_key,
36    masp_assets_hash_key, masp_conversion_key_prefix,
37    masp_scheduled_base_native_precision_key,
38    masp_scheduled_reward_precision_key_prefix, masp_token_map_key,
39};
40use crate::storage_key::{
41    masp_base_native_precision_key, masp_kd_gain_key, masp_kp_gain_key,
42    masp_last_inflation_key, masp_last_locked_amount_key,
43    masp_locked_amount_target_key, masp_max_reward_rate_key,
44    masp_reward_precision_key,
45};
46#[cfg(any(feature = "multicore", test))]
47use crate::{ConversionLeaf, Error, OptionExt, ResultExt};
48use crate::{Result, StorageRead, StorageWrite, WithConversionState};
49
50/// Compute shielded token inflation amount
51#[allow(clippy::too_many_arguments)]
52pub fn compute_inflation(
53    locked_amount: Uint,
54    total_native_amount: Uint,
55    max_reward_rate: Dec,
56    last_inflation_amount: Uint,
57    p_gain_nom: Dec,
58    d_gain_nom: Dec,
59    epochs_per_year: u64,
60    target_amount: Dec,
61    last_amount: Dec,
62) -> Uint {
63    let controller = PDController::new(
64        total_native_amount,
65        max_reward_rate,
66        last_inflation_amount,
67        p_gain_nom,
68        d_gain_nom,
69        epochs_per_year,
70        target_amount,
71        last_amount,
72    );
73
74    let metric = Dec::try_from(locked_amount)
75        .expect("Should not fail to convert Uint to Dec");
76    let control_coeff = max_reward_rate
77        .checked_div(controller.get_epochs_per_year())
78        .expect("Control coefficient overflow");
79
80    tracing::debug!(
81        "Shielded token inflation inputs: {controller:#?}, metric: {metric}, \
82         coefficient: {control_coeff}"
83    );
84    controller
85        .compute_inflation(control_coeff, metric)
86        .expect("Inflation calculation overflow")
87}
88
89// Infer the precision of a token from its denomination
90#[deprecated = "Token precisions are now read from storage instead of being \
91                inferred from their denominations."]
92fn infer_token_precision<S, TransToken>(
93    storage: &mut S,
94    addr: &Address,
95) -> Result<Precision>
96where
97    S: StorageWrite + StorageRead + WithConversionState,
98    TransToken: trans_token::Read<S>,
99{
100    // Since reading reward precision has failed, choose a
101    // thousandth of the given token. But clamp the precision above
102    // by 10^38, the maximum power of 10 that can be contained by a
103    // u128.
104    let denomination = TransToken::read_denom(storage, addr)?
105        .expect("failed to read token denomination");
106    let precision_denom =
107        u32::from(denomination.0).saturating_sub(3).clamp(0, 38);
108    let reward_precision = checked!(10u128 ^ precision_denom)?;
109    Ok(reward_precision)
110}
111
112// Read the base native precision from storage. And if it is not initialized,
113// fall back to inferring it based on native denomination.
114fn read_base_native_precision<S, TransToken>(
115    storage: &mut S,
116    native_token: &Address,
117) -> Result<Precision>
118where
119    S: StorageWrite + StorageRead + WithConversionState,
120    TransToken: trans_token::Keys + trans_token::Read<S>,
121{
122    let base_native_precision_key = masp_base_native_precision_key();
123    storage.read(&base_native_precision_key)?.map_or_else(
124        || -> Result<Precision> {
125            #[allow(deprecated)]
126            let precision =
127                infer_token_precision::<_, TransToken>(storage, native_token)?;
128            storage.write(&base_native_precision_key, precision)?;
129            Ok(precision)
130        },
131        Ok,
132    )
133}
134
135/// Compute the precision of MASP rewards for the given token. This function
136/// must be a non-zero constant for a given token.
137pub fn calculate_masp_rewards_precision<S, TransToken>(
138    storage: &mut S,
139    addr: &Address,
140) -> Result<Precision>
141where
142    S: StorageWrite + StorageRead + WithConversionState,
143    TransToken: trans_token::Keys + trans_token::Read<S>,
144{
145    // Inflation is implicitly denominated by this value. The lower this
146    // figure, the less precise inflation computations are. This is especially
147    // problematic when inflation is coming from a token with much higher
148    // denomination than the native token. The higher this figure, the higher
149    // the threshold of holdings required in order to receive non-zero rewards.
150    // This value should be fixed constant for each asset type. Here we read a
151    // value from storage and failing that we choose a thousandth of the given
152    // asset.
153    // Key to read/write reward precision from
154    let reward_precision_key = masp_reward_precision_key::<TransToken>(addr);
155    // Now read the desired reward precision for this token address
156    let reward_precision: Precision =
157        storage.read(&reward_precision_key)?.map_or_else(
158            || -> Result<Precision> {
159                let native_token = storage.get_native_token()?;
160                #[allow(deprecated)]
161                let prec = match storage.conversion_state().current_precision {
162                    // If this is the native token, then the precision was
163                    // actually stored in the conversion state.
164                    Some(native_precision) if *addr == native_token => {
165                        native_precision
166                    }
167                    // If the current precision is not defined for the native
168                    // token, then attempt to get it from the base native
169                    // precision.
170                    None if *addr == native_token => {
171                        read_base_native_precision::<_, TransToken>(
172                            storage, addr,
173                        )?
174                    }
175                    // Otherwise default to inferring the precision from the
176                    // token denomination
177                    _ => infer_token_precision::<_, TransToken>(storage, addr)?,
178                };
179                // Record the precision that is now being used so that it does
180                // not have to be recomputed each time, and to ensure that this
181                // value is not accidentally changed even by a change to this
182                // initialization algorithm.
183                storage.write(&reward_precision_key, prec)?;
184                Ok(prec)
185            },
186            Ok,
187        )?;
188
189    Ok(reward_precision)
190}
191
192/// Get the balance of the given token at the MASP address that is eligble to
193/// receive rewards.
194fn get_masp_dated_balance<S, TransToken>(
195    storage: &mut S,
196    token: &Address,
197) -> Result<Amount>
198where
199    S: StorageWrite + StorageRead,
200    TransToken: trans_token::Keys + trans_token::Read<S>,
201{
202    use crate::read_undated_balance;
203    let masp_addr = MASP;
204
205    // total locked amount in the Shielded pool
206    let total_tokens_in_masp =
207        TransToken::read_balance(storage, token, &masp_addr)?;
208    // Since dated and undated tokens are stored together in the pool, subtract
209    // the latter to get the dated balance
210    let masp_undated_balance = read_undated_balance(storage, token)?;
211    Ok(checked!(total_tokens_in_masp - masp_undated_balance)?)
212}
213
214/// Compute the MASP rewards by applying the PD-controller to the genesis
215/// parameters and the last inflation and last locked rewards ratio values.
216pub fn calculate_masp_rewards<S, TransToken>(
217    storage: &mut S,
218    token: &Address,
219    denomination: Denomination,
220    precision: Precision,
221    masp_epochs_per_year: u64,
222) -> Result<(u128, Precision)>
223where
224    S: StorageWrite + StorageRead,
225    TransToken: trans_token::Keys + trans_token::Read<S>,
226{
227    let masp_addr = MASP;
228
229    // Query the storage for information -------------------------
230
231    //// information about the amount of native tokens on the chain
232    let total_native_tokens =
233        TransToken::get_effective_total_native_supply(storage)?;
234
235    // total locked amount in the Shielded pool
236    let total_tokens_in_masp =
237        TransToken::read_balance(storage, token, &masp_addr)?;
238
239    //// Values from the last epoch
240    let last_inflation: Amount = storage
241        .read(&masp_last_inflation_key::<TransToken>(token))?
242        .expect("failure to read last inflation");
243
244    let last_locked_amount: Amount = storage
245        .read(&masp_last_locked_amount_key::<TransToken>(token))?
246        .expect("failure to read last inflation");
247
248    //// Parameters for each token
249    let max_reward_rate: Dec = storage
250        .read(&masp_max_reward_rate_key::<TransToken>(token))?
251        .expect("max reward should properly decode");
252
253    let kp_gain_nom: Dec = storage
254        .read(&masp_kp_gain_key::<TransToken>(token))?
255        .expect("kp_gain_nom reward should properly decode");
256
257    let kd_gain_nom: Dec = storage
258        .read(&masp_kd_gain_key::<TransToken>(token))?
259        .expect("kd_gain_nom reward should properly decode");
260
261    let target_locked_amount: Amount = storage
262        .read(&masp_locked_amount_target_key::<TransToken>(token))?
263        .expect("locked ratio target should properly decode");
264
265    let target_locked_dec = Dec::try_from(target_locked_amount.raw_amount())
266        .expect("Should not fail to convert Uint to Dec");
267    let last_locked_dec = Dec::try_from(last_locked_amount.raw_amount())
268        .expect("Should not fail to convert Uint to Dec");
269
270    // Initial computation of the new shielded inflation
271    let inflation = compute_inflation(
272        total_tokens_in_masp.raw_amount(),
273        total_native_tokens.raw_amount(),
274        max_reward_rate,
275        last_inflation.raw_amount(),
276        kp_gain_nom,
277        kd_gain_nom,
278        masp_epochs_per_year,
279        target_locked_dec,
280        last_locked_dec,
281    );
282
283    // total locked amount in the Shielded pool
284    let rewardable_tokens_in_masp =
285        get_masp_dated_balance::<S, TransToken>(storage, token)?;
286
287    // inflation-per-token = inflation / locked tokens = n/PRECISION
288    // ∴ n = (inflation * PRECISION) / locked tokens
289    // Since we must put the notes in a compatible format with the
290    // note format, we must make the inflation amount discrete.
291    let noterized_inflation = if rewardable_tokens_in_masp.is_zero() {
292        0u128
293    } else {
294        inflation
295            .checked_mul_div(
296                Uint::from(precision),
297                rewardable_tokens_in_masp.raw_amount(),
298            )
299            .and_then(|x| x.0.try_into().ok())
300            .unwrap_or_else(|| {
301                tracing::warn!(
302                    "MASP inflation for {} assumed to be 0 because the \
303                     computed value is too large. Please check the inflation \
304                     parameters.",
305                    *token
306                );
307                0u128
308            })
309    };
310    let inflation_amount = Amount::from_uint(
311        checked!(
312            rewardable_tokens_in_masp.raw_amount() / precision.into()
313                * Uint::from(noterized_inflation)
314        )?,
315        0,
316    )
317    .unwrap();
318    let denom_amount = DenominatedAmount::new(inflation_amount, denomination);
319    tracing::info!("MASP inflation for {token} is {denom_amount}");
320
321    tracing::debug!(
322        "Controller, call: total_in_masp {:?}, total_native_tokens {:?}, \
323         locked_target_amount {:?}, last_locked_amount {:?}, max_reward_rate \
324         {:?}, last_inflation {:?}, kp_gain_nom {:?}, kd_gain_nom {:?}, \
325         epochs_per_year {:?}",
326        total_tokens_in_masp,
327        total_native_tokens,
328        target_locked_amount,
329        last_locked_amount,
330        max_reward_rate,
331        last_inflation,
332        kp_gain_nom,
333        kd_gain_nom,
334        masp_epochs_per_year,
335    );
336    tracing::debug!("Token address: {:?}", token);
337    tracing::debug!("inflation from the pd controller {:?}", inflation);
338    tracing::debug!("total in the masp {:?}", total_tokens_in_masp);
339    tracing::debug!("precision {}", precision);
340    tracing::debug!("Noterized inflation: {}", noterized_inflation);
341
342    // Is it fine to write the inflation rate, this is accurate,
343    // but we should make sure the return value's ratio matches
344    // this new inflation rate in 'update_allowed_conversions',
345    // otherwise we will have an inaccurate view of inflation
346    storage.write(
347        &masp_last_inflation_key::<TransToken>(token),
348        inflation_amount,
349    )?;
350
351    storage.write(
352        &masp_last_locked_amount_key::<TransToken>(token),
353        total_tokens_in_masp,
354    )?;
355
356    Ok((noterized_inflation, precision))
357}
358
359/// Update the conversions for native tokens. Namely calculate the reward using
360/// the normed inflation as the denominator, make a 2-term allowed conversion,
361/// and compute how much needs to be minted in order to back the rewards.
362#[cfg(any(feature = "multicore", test))]
363fn update_native_conversions<S, TransToken>(
364    storage: &mut S,
365    token: &Address,
366    current_precision: &mut u128,
367    masp_epochs_per_year: u64,
368    masp_epoch: MaspEpoch,
369    current_convs: &mut BTreeMap<
370        (Address, Denomination, MaspDigitPos),
371        AllowedConversion,
372    >,
373) -> Result<(Denomination, (u128, u128))>
374where
375    S: StorageWrite + StorageRead + WithConversionState,
376    TransToken:
377        trans_token::Keys + trans_token::Read<S> + trans_token::Write<S>,
378{
379    let prev_masp_epoch =
380        masp_epoch.prev().ok_or_err_msg("MASP epoch underflow")?;
381    let denom = TransToken::read_denom(storage, token)?
382        .expect("failed to read token denomination");
383    let (reward, _precision) = calculate_masp_rewards::<S, TransToken>(
384        storage,
385        token,
386        denom,
387        *current_precision,
388        masp_epochs_per_year,
389    )?;
390    // The amount that will be given of the new native token for
391    // every amount of the native token given in the
392    // previous epoch
393    let current_precision_uint = Uint::from(*current_precision);
394    let reward = Uint::from(reward);
395    let new_precision = checked!(current_precision_uint + reward)?;
396    let new_precision = u128::try_from(new_precision).unwrap_or_else(|_| {
397        tracing::warn!(
398            "MASP precision for the native token {} is kept the same as in \
399             the last epoch because the computed value is too large. Please \
400             check the inflation parameters.",
401            token
402        );
403        *current_precision
404    });
405    for digit in MaspDigitPos::iter() {
406        // Provide an allowed conversion from previous timestamp. The
407        // negative sign allows each instance of the old asset to be
408        // cancelled out/replaced with the new asset
409        let old_asset = encode_asset_type(
410            token.clone(),
411            denom,
412            digit,
413            Some(prev_masp_epoch),
414        )
415        .into_storage_result()?;
416        let new_asset =
417            encode_asset_type(token.clone(), denom, digit, Some(masp_epoch))
418                .into_storage_result()?;
419        // The conversion is computed such that if consecutive
420        // conversions are added together, the intermediate native
421        // tokens cancel/telescope out
422        let cur_conv = MaspAmount::from_pair(
423            old_asset,
424            i128::try_from(*current_precision)
425                .ok()
426                .and_then(i128::checked_neg)
427                .ok_or_err_msg("Current inflation overflow")?,
428        );
429        let new_conv = MaspAmount::from_pair(
430            new_asset,
431            i128::try_from(new_precision).into_storage_result()?,
432        );
433        current_convs.insert(
434            (token.clone(), denom, digit),
435            checked!(cur_conv + &new_conv)?.into(),
436        );
437        // Add a conversion from the previous asset type
438        storage.conversion_state_mut().assets.insert(
439            old_asset,
440            ConversionLeaf {
441                token: token.clone(),
442                denom,
443                digit_pos: digit,
444                epoch: prev_masp_epoch,
445                conversion: MaspAmount::zero().into(),
446                leaf_pos: 0,
447            },
448        );
449    }
450    // Note the fraction used to compute rewards from balance
451    let reward_frac = (
452        new_precision
453            .checked_sub(*current_precision)
454            .unwrap_or_default(),
455        *current_precision,
456    );
457    // Save the new native reward precision
458    let reward_precision_key = masp_reward_precision_key::<TransToken>(token);
459    storage.write(&reward_precision_key, new_precision)?;
460    *current_precision = new_precision;
461    Ok((denom, reward_frac))
462}
463
464/// Update the conversions for non-native tokens. Namely calculate the reward,
465/// deflate it to real terms, make a 3-term allowed conversion, and compute how
466/// much needs to be minted in order to back the rewards.
467#[cfg(any(feature = "multicore", test))]
468#[allow(clippy::too_many_arguments)]
469fn update_non_native_conversions<S, TransToken>(
470    storage: &mut S,
471    token: &Address,
472    base_native_precision: u128,
473    current_native_precision: u128,
474    masp_epochs_per_year: u64,
475    masp_epoch: MaspEpoch,
476    reward_assets: [AssetType; 4],
477    total_reward: &mut Amount,
478    current_convs: &mut BTreeMap<
479        (Address, Denomination, MaspDigitPos),
480        AllowedConversion,
481    >,
482) -> Result<Denomination>
483where
484    S: StorageWrite + StorageRead + WithConversionState,
485    TransToken:
486        trans_token::Keys + trans_token::Read<S> + trans_token::Write<S>,
487{
488    let prev_masp_epoch =
489        masp_epoch.prev().ok_or_err_msg("MASP epoch underflow")?;
490    let denom = TransToken::read_denom(storage, token)?
491        .expect("failed to read token denomination");
492    let precision =
493        calculate_masp_rewards_precision::<S, TransToken>(storage, token)?;
494    let (reward, precision) = calculate_masp_rewards::<S, TransToken>(
495        storage,
496        token,
497        denom,
498        precision,
499        masp_epochs_per_year,
500    )?;
501    // Express the inflation reward in real terms, that is, with
502    // respect to the native asset in the zeroth epoch
503    let reward_uint = Uint::from(reward);
504    let base_native_precision_uint = Uint::from(base_native_precision);
505    let current_native_precision_uint = Uint::from(current_native_precision);
506    let real_reward = checked!(
507        (reward_uint * base_native_precision_uint)
508            / current_native_precision_uint
509    )?
510    .try_into()
511    .unwrap_or_else(|_| {
512        tracing::warn!(
513            "MASP reward for {} assumed to be 0 because the computed value is \
514             too large. Please check the inflation parameters.",
515            token
516        );
517        0u128
518    });
519    // The conversion is computed such that if consecutive
520    // conversions are added together, the
521    // intermediate tokens cancel/ telescope out
522    let precision_i128 = i128::try_from(precision).into_storage_result()?;
523    let real_reward_i128 = i128::try_from(real_reward).into_storage_result()?;
524    for digit in MaspDigitPos::iter() {
525        // Provide an allowed conversion from previous timestamp. The
526        // negative sign allows each instance of the old asset to be
527        // cancelled out/replaced with the new asset
528        let old_asset = encode_asset_type(
529            token.clone(),
530            denom,
531            digit,
532            Some(prev_masp_epoch),
533        )
534        .into_storage_result()?;
535        let new_asset =
536            encode_asset_type(token.clone(), denom, digit, Some(masp_epoch))
537                .into_storage_result()?;
538
539        current_convs.insert(
540            (token.clone(), denom, digit),
541            checked!(
542                MaspAmount::from_pair(old_asset, -precision_i128)
543                    + &MaspAmount::from_pair(new_asset, precision_i128)
544                    + &MaspAmount::from_pair(
545                        reward_assets[digit as usize],
546                        real_reward_i128,
547                    )
548            )?
549            .into(),
550        );
551        // Add a conversion from the previous asset type
552        storage.conversion_state_mut().assets.insert(
553            old_asset,
554            ConversionLeaf {
555                token: token.clone(),
556                denom,
557                digit_pos: digit,
558                epoch: prev_masp_epoch,
559                conversion: MaspAmount::zero().into(),
560                leaf_pos: 0,
561            },
562        );
563    }
564    // Dispense a transparent reward in parallel to the shielded rewards
565    let addr_bal = get_masp_dated_balance::<S, TransToken>(storage, token)?;
566    // The reward for each reward.1 units of the current asset
567    // is reward.0 units of the reward token
568    *total_reward = total_reward
569        .checked_add(
570            addr_bal
571                .u128_eucl_div_rem((real_reward, precision))
572                .ok_or_else(|| {
573                    Error::new_const("Total reward calculation overflow")
574                })?
575                .0,
576        )
577        .ok_or_else(|| Error::new_const("Total reward overflow"))?;
578    Ok(denom)
579}
580
581#[cfg(any(feature = "multicore", test))]
582/// Apply the conversion updates that are in storage to the in memory structure
583/// and delete them.
584fn apply_stored_conversion_updates<S, TransToken>(
585    storage: &mut S,
586    ep: &MaspEpoch,
587) -> Result<()>
588where
589    S: StorageWrite + StorageRead + WithConversionState,
590    TransToken:
591        trans_token::Keys + trans_token::Read<S> + trans_token::Write<S>,
592{
593    // Read and apply any scheduled base native precisions
594    let scheduled_base_precision_key =
595        masp_scheduled_base_native_precision_key(ep);
596    if let Some(precision) = storage.read(&scheduled_base_precision_key)? {
597        let base_precision_key = masp_base_native_precision_key();
598        storage.write::<Precision>(&base_precision_key, precision)?;
599    }
600
601    let scheduled_reward_precision_key_prefix =
602        masp_scheduled_reward_precision_key_prefix(ep);
603    let mut precision_updates = BTreeMap::<_, Precision>::new();
604    // Read scheduled precisions from storage and store them in a map
605    for prec_result in iter_prefix_with_filter_map(
606        storage,
607        &scheduled_reward_precision_key_prefix,
608        is_masp_scheduled_reward_precision_key,
609    )? {
610        match prec_result {
611            Ok(((_ep, addr), precision)) => {
612                precision_updates.insert(addr, precision);
613            }
614            Err(err) => {
615                tracing::warn!("Encountered malformed precision: {}", err);
616                continue;
617            }
618        }
619    }
620    // Apply the precision updates to storage
621    for (addr, precision) in precision_updates {
622        // Key to read/write reward precision from
623        let reward_precision_key =
624            masp_reward_precision_key::<TransToken>(&addr);
625        storage.write(&reward_precision_key, precision)?;
626    }
627    let conversion_key_prefix = masp_conversion_key_prefix(ep);
628    let mut conversion_updates =
629        BTreeMap::<_, UncheckedAllowedConversion>::new();
630    // Read conversion updates from storage and store them in a map
631    for conv_result in iter_prefix_with_filter_map(
632        storage,
633        &conversion_key_prefix,
634        is_masp_conversion_key,
635    )? {
636        match conv_result {
637            Ok(((_ep, asset_type), conv)) => {
638                conversion_updates.insert(asset_type, conv);
639            }
640            Err(err) => {
641                tracing::warn!("Encountered malformed conversion: {}", err);
642                continue;
643            }
644        }
645    }
646    // Apply the conversion updates to the in memory structure
647    let assets = &mut storage.conversion_state_mut().assets;
648    for (asset_type, conv) in conversion_updates {
649        let Some(leaf) = assets.get_mut(&asset_type) else {
650            tracing::warn!(
651                "Encountered non-existent asset type: {}",
652                asset_type
653            );
654            continue;
655        };
656        leaf.conversion = conv.0;
657    }
658    // Delete the updates now that they have been applied
659    storage.delete_prefix(&conversion_key_prefix)?;
660    storage.delete_prefix(&scheduled_reward_precision_key_prefix)?;
661    storage.delete(&scheduled_base_precision_key)?;
662    Ok(())
663}
664
665#[cfg(any(feature = "multicore", test))]
666/// Update the MASP's allowed conversions
667pub fn update_allowed_conversions<S, Params, TransToken>(
668    storage: &mut S,
669) -> Result<()>
670where
671    S: StorageWrite + StorageRead + WithConversionState,
672    Params: parameters::Read<S>,
673    TransToken:
674        trans_token::Keys + trans_token::Read<S> + trans_token::Write<S>,
675{
676    use std::cmp::Ordering;
677
678    use masp_primitives::bls12_381;
679    use masp_primitives::ff::PrimeField;
680    use masp_primitives::merkle_tree::FrozenCommitmentTree;
681    use masp_primitives::sapling::Node;
682    use namada_core::masp::encode_reward_asset_types;
683    use namada_core::token::NATIVE_MAX_DECIMAL_PLACES;
684    use rayon::iter::{
685        IndexedParallelIterator, IntoParallelIterator, ParallelIterator,
686    };
687    use rayon::prelude::ParallelSlice;
688
689    use crate::mint_rewards;
690
691    // Get the previous MASP epoch if there's any
692    let masp_epoch_multiplier = Params::masp_epoch_multiplier(storage)?;
693    let masp_epoch = MaspEpoch::try_from_epoch(
694        storage.get_block_epoch()?,
695        masp_epoch_multiplier,
696    )
697    .map_err(Error::new_const)?;
698    let Some(prev_masp_epoch) = masp_epoch.prev() else {
699        return Ok(());
700    };
701    apply_stored_conversion_updates::<_, TransToken>(
702        storage,
703        &prev_masp_epoch,
704    )?;
705    let token_map_key = masp_token_map_key();
706    let token_map: namada_core::masp::TokenMap =
707        storage.read(&token_map_key)?.unwrap_or_default();
708    let mut masp_reward_keys: Vec<_> = token_map.values().cloned().collect();
709    let mut masp_reward_denoms = BTreeMap::new();
710    // Put the native rewards first because other inflation computations depend
711    // on it
712    let native_token = storage.get_native_token()?;
713    masp_reward_keys.sort_unstable_by(|x, y| {
714        if (*x == native_token) == (*y == native_token) {
715            Ordering::Equal
716        } else if *x == native_token {
717            Ordering::Less
718        } else {
719            Ordering::Greater
720        }
721    });
722    // The total transparent value of the rewards being distributed
723    let mut total_deflated_reward = Amount::zero();
724
725    let reward_assets =
726        encode_reward_asset_types(&native_token).into_storage_result()?;
727    // Conversions from the previous to current asset for each address
728    let mut current_convs = BTreeMap::<
729        (Address, Denomination, MaspDigitPos),
730        AllowedConversion,
731    >::new();
732    // This is the base native token precision value
733    let base_native_precision =
734        read_base_native_precision::<_, TransToken>(storage, &native_token)?;
735    // Get the last rewarded amount of the native token
736    let mut current_native_precision = calculate_masp_rewards_precision::<
737        S,
738        TransToken,
739    >(storage, &native_token)?;
740
741    // Reward all tokens according to above reward rates
742    let epochs_per_year = Params::epochs_per_year(storage)?;
743    let masp_epochs_per_year =
744        checked!(epochs_per_year / masp_epoch_multiplier)?;
745    let mut native_reward_frac = None;
746    for token in &masp_reward_keys {
747        // Generate conversions from the last epoch to the current and update
748        // the reward backing accumulator
749        if *token == native_token {
750            let (denom, frac) = update_native_conversions::<_, TransToken>(
751                storage,
752                token,
753                &mut current_native_precision,
754                masp_epochs_per_year,
755                masp_epoch,
756                &mut current_convs,
757            )?;
758            masp_reward_denoms.insert(token.clone(), denom);
759            native_reward_frac = Some(frac);
760        } else {
761            let denom = update_non_native_conversions::<_, TransToken>(
762                storage,
763                token,
764                base_native_precision,
765                current_native_precision,
766                masp_epochs_per_year,
767                masp_epoch,
768                reward_assets,
769                &mut total_deflated_reward,
770                &mut current_convs,
771            )?;
772            masp_reward_denoms.insert(token.clone(), denom);
773        }
774    }
775    // Inflate the non-native rewards for all tokens in one operation
776    let non_native_reward = total_deflated_reward
777        .raw_amount()
778        .checked_mul_div(
779            current_native_precision.into(),
780            base_native_precision.into(),
781        )
782        .ok_or_else(|| Error::new_const("Total reward calculation overflow"))?;
783    // The total transparent value of the rewards being distributed. First
784    // accumulate the integer part of the non-native reward.
785    let mut total_reward = Amount::from(non_native_reward.0);
786    // And finally accumulate the fractional parts of the native and non-native
787    // rewards if their sum is more than one
788    if let Some(native_reward_frac) = native_reward_frac {
789        // Dispense a transparent reward in parallel to the shielded rewards
790        let addr_bal = TransToken::read_balance(storage, &native_token, &MASP)?;
791        // The reward for each reward.1 units of the current asset is reward.0
792        // units of the reward token
793        let native_reward = addr_bal
794            .raw_amount()
795            .checked_mul_div(
796                native_reward_frac.0.into(),
797                native_reward_frac.1.into(),
798            )
799            .ok_or_else(|| Error::new_const("Three digit reward overflow"))?;
800        // Accumulate the integer part of the native reward
801        checked!(total_reward += native_reward.0.into())?;
802
803        let base_native_precision = Uint::from(base_native_precision);
804        // Compute the fraction obtained by adding the fractional parts of the
805        // native reward and the non-native reward:
806        // native_reward.1/native_reward_frac.1 +
807        // non_native_reward.1/base_native_precision
808        let numerator = checked!(
809            native_reward.1 * base_native_precision
810                + Uint::from(native_reward_frac.1) * non_native_reward.1
811        )?;
812        let denominator =
813            checked!(base_native_precision * Uint::from(native_reward_frac.1))?;
814        // A fraction greater than or equal to one corresponds to the situation
815        // where combining non-native rewards with pre-existing NAM balance
816        // gives a greater reward than treating each separately.
817        if numerator >= denominator {
818            checked!(total_reward += 1.into())?;
819        }
820    }
821
822    // Try to distribute Merkle leaf updating as evenly as possible across
823    // multiple cores
824    let num_threads = rayon::current_num_threads();
825    // Put assets into vector to enable computation batching
826    let assets: Vec<_> = storage
827        .conversion_state_mut()
828        .assets
829        .values_mut()
830        .enumerate()
831        .collect();
832
833    #[allow(clippy::arithmetic_side_effects)]
834    let notes_per_thread_max = assets.len().div_ceil(num_threads);
835    // floor(assets.len() / num_threads)
836    #[allow(clippy::arithmetic_side_effects)]
837    let notes_per_thread_min = assets.len() / num_threads;
838
839    // Now on each core, add the latest conversion to each conversion
840    let conv_notes: Vec<Node> = assets
841        .into_par_iter()
842        .with_min_len(notes_per_thread_min)
843        .with_max_len(notes_per_thread_max)
844        .map(|(idx, leaf)| {
845            // Try to get the applicable conversion delta
846            let cur_conv_key = (leaf.token.clone(), leaf.denom, leaf.digit_pos);
847            if let Some(current_conv) = current_convs.get(&cur_conv_key) {
848                // Use transitivity to update conversion
849                #[allow(clippy::arithmetic_side_effects)]
850                {
851                    leaf.conversion += current_conv.clone();
852                }
853            }
854            // Update conversion position to leaf we are about to create
855            leaf.leaf_pos = idx;
856            // The merkle tree need only provide the conversion commitment,
857            // the remaining information is provided through the storage API
858            Node::new(leaf.conversion.cmu().to_repr())
859        })
860        .collect();
861
862    // Update the MASP's transparent reward token balance to ensure that it
863    // is sufficiently backed to redeem rewards
864    mint_rewards::<S, TransToken>(storage, total_reward)?;
865
866    // Try to distribute Merkle tree construction as evenly as possible
867    // across multiple cores
868    // Merkle trees must have exactly 2^n leaves to be mergeable
869    let mut notes_per_thread_rounded = 1;
870
871    // Cannot overflow
872    #[allow(clippy::arithmetic_side_effects)]
873    while notes_per_thread_max > notes_per_thread_rounded * 4 {
874        notes_per_thread_rounded *= 2;
875    }
876    // Make the sub-Merkle trees in parallel
877    let tree_parts: Vec<_> = conv_notes
878        .par_chunks(notes_per_thread_rounded)
879        .map(FrozenCommitmentTree::new)
880        .collect();
881
882    // Convert conversion vector into tree so that Merkle paths can be
883    // obtained
884    storage.conversion_state_mut().tree =
885        FrozenCommitmentTree::merge(&tree_parts);
886    // Update the anchor in storage
887    storage.write(
888        &crate::storage_key::masp_convert_anchor_key(),
889        namada_core::hash::Hash(
890            bls12_381::Scalar::from(storage.conversion_state().tree.root())
891                .to_bytes(),
892        ),
893    )?;
894
895    if !masp_reward_keys.contains(&native_token) {
896        // Since MASP rewards are denominated in NAM tokens, ensure that clients
897        // are able to decode them.
898        masp_reward_denoms
899            .insert(native_token.clone(), NATIVE_MAX_DECIMAL_PLACES.into());
900    }
901    // Add purely decoding entries to the assets map. These will be
902    // overwritten before the creation of the next commitment tree
903    for (addr, denom) in masp_reward_denoms {
904        for digit in MaspDigitPos::iter() {
905            // Add the decoding entry for the new asset type. An uncommitted
906            // node position is used since this is not a conversion.
907            let new_asset =
908                encode_asset_type(addr.clone(), denom, digit, Some(masp_epoch))
909                    .into_storage_result()?;
910            let tree_size = storage.conversion_state().tree.size();
911            storage.conversion_state_mut().assets.insert(
912                new_asset,
913                ConversionLeaf {
914                    token: addr.clone(),
915                    denom,
916                    digit_pos: digit,
917                    epoch: masp_epoch,
918                    conversion: MaspAmount::zero().into(),
919                    leaf_pos: tree_size,
920                },
921            );
922        }
923    }
924    // store only the assets hash because the size is quite large
925    let assets_hash =
926        Hash::sha256(storage.conversion_state().assets.serialize_to_vec());
927    storage.write(&masp_assets_hash_key(), assets_hash)?;
928
929    Ok(())
930}
931
932// This is only enabled when "wasm-runtime" is on, because we're using rayon
933#[cfg(not(any(feature = "multicore", test)))]
934/// Update the MASP's allowed conversions
935pub fn update_allowed_conversions<S, Params, TransToken>(
936    _storage: &mut S,
937) -> Result<()>
938where
939    S: StorageWrite + StorageRead + WithConversionState,
940    Params: parameters::Read<S>,
941    TransToken: trans_token::Keys,
942{
943    Ok(())
944}
945
946#[allow(clippy::arithmetic_side_effects)]
947#[cfg(test)]
948mod tests {
949    use std::str::FromStr;
950
951    use namada_core::address;
952    use namada_core::collections::HashMap;
953    use namada_core::dec::testing::arb_non_negative_dec;
954    use namada_core::token::testing::arb_amount;
955    use namada_state::testing::TestStorage;
956    use namada_trans_token::storage_key::{balance_key, minted_balance_key};
957    use namada_trans_token::write_denom;
958    use proptest::prelude::*;
959    use proptest::test_runner::Config;
960    use test_log::test;
961
962    use super::*;
963    use crate::ShieldedParams;
964
965    proptest! {
966        #![proptest_config(Config {
967            cases: 10,
968            .. Config::default()
969        })]
970        #[test]
971        fn test_updated_allowed_conversions(
972            initial_balance in arb_amount(),
973            masp_locked_ratio in arb_non_negative_dec(),
974        ) {
975            test_updated_allowed_conversions_aux(initial_balance, masp_locked_ratio)
976        }
977    }
978
979    fn test_updated_allowed_conversions_aux(
980        initial_balance: Amount,
981        masp_locked_ratio: Dec,
982    ) {
983        const ROUNDS: usize = 10;
984
985        let mut s = TestStorage::default();
986        // Initialize the state
987        {
988            // Parameters
989            namada_parameters::init_test_storage(&mut s).unwrap();
990
991            // Tokens
992            let token_params = ShieldedParams {
993                max_reward_rate: Dec::from_str("0.1").unwrap(),
994                kp_gain_nom: Dec::from_str("0.1").unwrap(),
995                kd_gain_nom: Dec::from_str("0.1").unwrap(),
996                locked_amount_target: 10_000_u64,
997            };
998
999            for (token_addr, (alias, denom)) in tokens() {
1000                namada_trans_token::write_params(&mut s, &token_addr).unwrap();
1001                crate::write_params::<_, namada_trans_token::Store<()>>(
1002                    &token_params,
1003                    &mut s,
1004                    &token_addr,
1005                    &denom,
1006                )
1007                .unwrap();
1008
1009                write_denom(&mut s, &token_addr, denom).unwrap();
1010
1011                // Write a minted token balance
1012                let total_token_balance = initial_balance;
1013                s.write(&minted_balance_key(&token_addr), total_token_balance)
1014                    .unwrap();
1015
1016                // Put the locked ratio into MASP
1017                s.write(
1018                    &balance_key(&token_addr, &address::MASP),
1019                    masp_locked_ratio * total_token_balance,
1020                )
1021                .unwrap();
1022
1023                // Insert tokens into MASP conversion state
1024                let token_map_key = masp_token_map_key();
1025                let mut token_map: namada_core::masp::TokenMap =
1026                    s.read(&token_map_key).unwrap().unwrap_or_default();
1027                token_map.insert(alias.to_string(), token_addr.clone());
1028                s.write(&token_map_key, token_map).unwrap();
1029            }
1030        }
1031
1032        for i in 0..ROUNDS {
1033            println!("Round {i}");
1034            update_allowed_conversions::<
1035                _,
1036                namada_parameters::Store<_>,
1037                namada_trans_token::Store<_>,
1038            >(&mut s)
1039            .unwrap();
1040            println!();
1041            println!();
1042        }
1043    }
1044
1045    pub fn tokens() -> HashMap<Address, (&'static str, Denomination)> {
1046        vec![
1047            (address::testing::nam(), ("nam", 6.into())),
1048            (address::testing::btc(), ("btc", 8.into())),
1049            (address::testing::eth(), ("eth", 18.into())),
1050            (address::testing::dot(), ("dot", 10.into())),
1051            (address::testing::schnitzel(), ("schnitzel", 6.into())),
1052            (address::testing::apfel(), ("apfel", 6.into())),
1053            (address::testing::kartoffel(), ("kartoffel", 6.into())),
1054        ]
1055        .into_iter()
1056        .collect()
1057    }
1058
1059    #[test]
1060    fn test_masp_inflation_playground() {
1061        let denom = Uint::from(1_000_000); // token denomination (usually 6)
1062        let total_tokens = 10_000_000_000_u64; // 10B naan
1063        let mut total_tokens = Uint::from(total_tokens) * denom;
1064        let locked_tokens_target = Uint::from(500_000) * denom; // Dependent on the token type
1065        let init_locked_ratio = Dec::from_str("0.1").unwrap(); // Arbitrary amount to play around with
1066        let init_locked_tokens = (init_locked_ratio
1067            * Dec::try_from(locked_tokens_target).unwrap())
1068        .to_uint()
1069        .unwrap();
1070        let epochs_per_year = 730_u64; // SE configuration
1071        let max_reward_rate = Dec::from_str("0.01").unwrap(); // Pre-determined based on token type
1072        let mut last_inflation_amount = Uint::zero();
1073        let p_gain_nom = Dec::from_str("25000").unwrap(); // To be configured
1074        let d_gain_nom = Dec::from_str("25000").unwrap(); // To be configured
1075
1076        let mut locked_amount = init_locked_tokens;
1077        let mut locked_tokens_last = init_locked_tokens;
1078
1079        let num_rounds = 10;
1080        println!();
1081
1082        for round in 0..num_rounds {
1083            let inflation = compute_inflation(
1084                locked_amount,
1085                total_tokens,
1086                max_reward_rate,
1087                last_inflation_amount,
1088                p_gain_nom,
1089                d_gain_nom,
1090                epochs_per_year,
1091                Dec::try_from(locked_tokens_target).unwrap(),
1092                Dec::try_from(locked_tokens_last).unwrap(),
1093            );
1094
1095            let rate = Dec::try_from(inflation).unwrap()
1096                * Dec::from(epochs_per_year)
1097                / Dec::try_from(total_tokens).unwrap();
1098
1099            println!(
1100                "Round {round}: Locked amount: {locked_amount}, inflation \
1101                 rate: {rate} -- (raw infl: {inflation})",
1102            );
1103            // dbg!(&controller);
1104
1105            last_inflation_amount = inflation;
1106            total_tokens += inflation;
1107            locked_tokens_last = locked_amount;
1108
1109            let change_staked_tokens = Uint::from(2) * locked_tokens_target;
1110            locked_amount += change_staked_tokens;
1111        }
1112    }
1113}