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;
10#[cfg(any(feature = "multicore", test))]
11use masp_primitives::transaction::components::I128Sum as MaspAmount;
12use namada_controller::PDController;
13use namada_core::address::{Address, MASP};
14use namada_core::arith::checked;
15#[cfg(any(feature = "multicore", test))]
16use namada_core::arith::CheckedAdd;
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;
22#[cfg(any(feature = "multicore", test))]
23use namada_core::masp::encode_asset_type;
24#[cfg(any(feature = "multicore", test))]
25use namada_core::masp::MaspEpoch;
26#[cfg(any(feature = "multicore", test))]
27use namada_core::token::MaspDigitPos;
28use namada_core::token::{Amount, DenominatedAmount, Denomination};
29use namada_core::uint::Uint;
30use namada_systems::{parameters, trans_token};
31
32#[cfg(any(feature = "multicore", test))]
33use crate::storage_key::{masp_assets_hash_key, masp_token_map_key};
34use crate::storage_key::{
35    masp_kd_gain_key, masp_kp_gain_key, masp_last_inflation_key,
36    masp_last_locked_amount_key, masp_locked_amount_target_key,
37    masp_max_reward_rate_key,
38};
39#[cfg(any(feature = "multicore", test))]
40use crate::{ConversionLeaf, Error, OptionExt, ResultExt};
41use crate::{Result, StorageRead, StorageWrite, WithConversionState};
42
43/// Compute shielded token inflation amount
44#[allow(clippy::too_many_arguments)]
45pub fn compute_inflation(
46    locked_amount: Uint,
47    total_native_amount: Uint,
48    max_reward_rate: Dec,
49    last_inflation_amount: Uint,
50    p_gain_nom: Dec,
51    d_gain_nom: Dec,
52    epochs_per_year: u64,
53    target_amount: Dec,
54    last_amount: Dec,
55) -> Uint {
56    let controller = PDController::new(
57        total_native_amount,
58        max_reward_rate,
59        last_inflation_amount,
60        p_gain_nom,
61        d_gain_nom,
62        epochs_per_year,
63        target_amount,
64        last_amount,
65    );
66
67    let metric = Dec::try_from(locked_amount)
68        .expect("Should not fail to convert Uint to Dec");
69    let control_coeff = max_reward_rate
70        .checked_div(controller.get_epochs_per_year())
71        .expect("Control coefficient overflow");
72
73    tracing::debug!(
74        "Shielded token inflation inputs: {controller:#?}, metric: {metric}, \
75         coefficient: {control_coeff}"
76    );
77    controller
78        .compute_inflation(control_coeff, metric)
79        .expect("Inflation calculation overflow")
80}
81
82/// Compute the precision of MASP rewards for the given token. This function
83/// must be a non-zero constant for a given token.
84pub fn calculate_masp_rewards_precision<S, TransToken>(
85    storage: &mut S,
86    addr: &Address,
87) -> Result<(u128, Denomination)>
88where
89    S: StorageWrite + StorageRead,
90    TransToken: trans_token::Read<S>,
91{
92    let denomination = TransToken::read_denom(storage, addr)?
93        .expect("failed to read token denomination");
94    // Inflation is implicitly denominated by this value. The lower this
95    // figure, the less precise inflation computations are. This is especially
96    // problematic when inflation is coming from a token with much higher
97    // denomination than the native token. The higher this figure, the higher
98    // the threshold of holdings required in order to receive non-zero rewards.
99    // This value should be fixed constant for each asset type. Here we choose
100    // a thousandth of the given asset.
101    let precision_denom = std::cmp::max(u32::from(denomination.0), 3)
102        .checked_sub(3)
103        .expect("Cannot underflow");
104    Ok((checked!(10u128 ^ precision_denom)?, denomination))
105}
106
107/// Compute the MASP rewards by applying the PD-controller to the genesis
108/// parameters and the last inflation and last locked rewards ratio values.
109pub fn calculate_masp_rewards<S, TransToken>(
110    storage: &mut S,
111    token: &Address,
112    denomination: Denomination,
113    precision: u128,
114    masp_epochs_per_year: u64,
115) -> Result<(u128, u128)>
116where
117    S: StorageWrite + StorageRead,
118    TransToken: trans_token::Keys + trans_token::Read<S>,
119{
120    let masp_addr = MASP;
121
122    // Query the storage for information -------------------------
123
124    //// information about the amount of native tokens on the chain
125    let total_native_tokens =
126        TransToken::get_effective_total_native_supply(storage)?;
127
128    // total locked amount in the Shielded pool
129    let total_tokens_in_masp =
130        TransToken::read_balance(storage, token, &masp_addr)?;
131
132    //// Values from the last epoch
133    let last_inflation: Amount = storage
134        .read(&masp_last_inflation_key::<TransToken>(token))?
135        .expect("failure to read last inflation");
136
137    let last_locked_amount: Amount = storage
138        .read(&masp_last_locked_amount_key::<TransToken>(token))?
139        .expect("failure to read last inflation");
140
141    //// Parameters for each token
142    let max_reward_rate: Dec = storage
143        .read(&masp_max_reward_rate_key::<TransToken>(token))?
144        .expect("max reward should properly decode");
145
146    let kp_gain_nom: Dec = storage
147        .read(&masp_kp_gain_key::<TransToken>(token))?
148        .expect("kp_gain_nom reward should properly decode");
149
150    let kd_gain_nom: Dec = storage
151        .read(&masp_kd_gain_key::<TransToken>(token))?
152        .expect("kd_gain_nom reward should properly decode");
153
154    let target_locked_amount: Amount = storage
155        .read(&masp_locked_amount_target_key::<TransToken>(token))?
156        .expect("locked ratio target should properly decode");
157
158    let target_locked_dec = Dec::try_from(target_locked_amount.raw_amount())
159        .expect("Should not fail to convert Uint to Dec");
160    let last_locked_dec = Dec::try_from(last_locked_amount.raw_amount())
161        .expect("Should not fail to convert Uint to Dec");
162
163    // Initial computation of the new shielded inflation
164    let inflation = compute_inflation(
165        total_tokens_in_masp.raw_amount(),
166        total_native_tokens.raw_amount(),
167        max_reward_rate,
168        last_inflation.raw_amount(),
169        kp_gain_nom,
170        kd_gain_nom,
171        masp_epochs_per_year,
172        target_locked_dec,
173        last_locked_dec,
174    );
175
176    // inflation-per-token = inflation / locked tokens = n/PRECISION
177    // ∴ n = (inflation * PRECISION) / locked tokens
178    // Since we must put the notes in a compatible format with the
179    // note format, we must make the inflation amount discrete.
180    let noterized_inflation = if total_tokens_in_masp.is_zero() {
181        0u128
182    } else {
183        inflation
184            .checked_mul_div(
185                Uint::from(precision),
186                total_tokens_in_masp.raw_amount(),
187            )
188            .and_then(|x| x.0.try_into().ok())
189            .unwrap_or_else(|| {
190                tracing::warn!(
191                    "MASP inflation for {} assumed to be 0 because the \
192                     computed value is too large. Please check the inflation \
193                     parameters.",
194                    *token
195                );
196                0u128
197            })
198    };
199    let inflation_amount = Amount::from_uint(
200        checked!(
201            total_tokens_in_masp.raw_amount() / precision.into()
202                * Uint::from(noterized_inflation)
203        )?,
204        0,
205    )
206    .unwrap();
207    let denom_amount = DenominatedAmount::new(inflation_amount, denomination);
208    tracing::info!("MASP inflation for {token} is {denom_amount}");
209
210    tracing::debug!(
211        "Controller, call: total_in_masp {:?}, total_native_tokens {:?}, \
212         locked_target_amount {:?}, last_locked_amount {:?}, max_reward_rate \
213         {:?}, last_inflation {:?}, kp_gain_nom {:?}, kd_gain_nom {:?}, \
214         epochs_per_year {:?}",
215        total_tokens_in_masp,
216        total_native_tokens,
217        target_locked_amount,
218        last_locked_amount,
219        max_reward_rate,
220        last_inflation,
221        kp_gain_nom,
222        kd_gain_nom,
223        masp_epochs_per_year,
224    );
225    tracing::debug!("Token address: {:?}", token);
226    tracing::debug!("inflation from the pd controller {:?}", inflation);
227    tracing::debug!("total in the masp {:?}", total_tokens_in_masp);
228    tracing::debug!("precision {}", precision);
229    tracing::debug!("Noterized inflation: {}", noterized_inflation);
230
231    // Is it fine to write the inflation rate, this is accurate,
232    // but we should make sure the return value's ratio matches
233    // this new inflation rate in 'update_allowed_conversions',
234    // otherwise we will have an inaccurate view of inflation
235    storage.write(
236        &masp_last_inflation_key::<TransToken>(token),
237        inflation_amount,
238    )?;
239
240    storage.write(
241        &masp_last_locked_amount_key::<TransToken>(token),
242        total_tokens_in_masp,
243    )?;
244
245    Ok((noterized_inflation, precision))
246}
247
248/// Update the conversions for native tokens. Namely calculate the reward using
249/// the normed inflation as the denominator, make a 2-term allowed conversion,
250/// and compute how much needs to be minted in order to back the rewards.
251#[cfg(any(feature = "multicore", test))]
252fn update_native_conversions<S, TransToken>(
253    storage: &mut S,
254    token: &Address,
255    normed_inflation: u128,
256    masp_epochs_per_year: u64,
257    masp_epoch: MaspEpoch,
258    total_reward: &mut Amount,
259    current_convs: &mut BTreeMap<
260        (Address, Denomination, MaspDigitPos),
261        AllowedConversion,
262    >,
263) -> Result<Denomination>
264where
265    S: StorageWrite + StorageRead + WithConversionState,
266    TransToken:
267        trans_token::Keys + trans_token::Read<S> + trans_token::Write<S>,
268{
269    let prev_masp_epoch =
270        masp_epoch.prev().ok_or_err_msg("MASP epoch underflow")?;
271    let denom = TransToken::read_denom(storage, token)?
272        .expect("failed to read token denomination");
273    let (reward, _precision) = calculate_masp_rewards::<S, TransToken>(
274        storage,
275        token,
276        denom,
277        normed_inflation,
278        masp_epochs_per_year,
279    )?;
280    // The amount that will be given of the new native token for
281    // every amount of the native token given in the
282    // previous epoch
283    let inflation_uint = Uint::from(normed_inflation);
284    let reward = Uint::from(reward);
285    let new_normed_inflation = checked!(inflation_uint + reward)?;
286    let new_normed_inflation = u128::try_from(new_normed_inflation)
287        .unwrap_or_else(|_| {
288            tracing::warn!(
289                "MASP inflation for the native token {} is kept the same as \
290                 in the last epoch because the computed value is too large. \
291                 Please check the inflation parameters.",
292                token
293            );
294            normed_inflation
295        });
296    for digit in MaspDigitPos::iter() {
297        // Provide an allowed conversion from previous timestamp. The
298        // negative sign allows each instance of the old asset to be
299        // cancelled out/replaced with the new asset
300        let old_asset = encode_asset_type(
301            token.clone(),
302            denom,
303            digit,
304            Some(prev_masp_epoch),
305        )
306        .into_storage_result()?;
307        let new_asset =
308            encode_asset_type(token.clone(), denom, digit, Some(masp_epoch))
309                .into_storage_result()?;
310        // The conversion is computed such that if consecutive
311        // conversions are added together, the intermediate native
312        // tokens cancel/telescope out
313        let cur_conv = MaspAmount::from_pair(
314            old_asset,
315            i128::try_from(normed_inflation)
316                .ok()
317                .and_then(i128::checked_neg)
318                .ok_or_err_msg("Current inflation overflow")?,
319        );
320        let new_conv = MaspAmount::from_pair(
321            new_asset,
322            i128::try_from(new_normed_inflation).into_storage_result()?,
323        );
324        current_convs.insert(
325            (token.clone(), denom, digit),
326            checked!(cur_conv + &new_conv)?.into(),
327        );
328        // Add a conversion from the previous asset type
329        storage.conversion_state_mut().assets.insert(
330            old_asset,
331            ConversionLeaf {
332                token: token.clone(),
333                denom,
334                digit_pos: digit,
335                epoch: prev_masp_epoch,
336                conversion: MaspAmount::zero().into(),
337                leaf_pos: 0,
338            },
339        );
340    }
341    // Dispense a transparent reward in parallel to the shielded rewards
342    let addr_bal = TransToken::read_balance(storage, token, &MASP)?;
343    // The reward for each reward.1 units of the current asset
344    // is reward.0 units of the reward token
345    let native_reward = addr_bal
346        .u128_eucl_div_rem((new_normed_inflation, normed_inflation))
347        .ok_or_else(|| Error::new_const("Three digit reward overflow"))?;
348    *total_reward = total_reward
349        .checked_add(
350            native_reward
351                .0
352                .checked_add(native_reward.1)
353                .unwrap_or(Amount::max())
354                .checked_sub(addr_bal)
355                .unwrap_or_default(),
356        )
357        .ok_or_else(|| Error::new_const("Three digit total reward overflow"))?;
358    // Save the new normed inflation
359    let _ = storage
360        .conversion_state_mut()
361        .normed_inflation
362        .insert(new_normed_inflation);
363    Ok(denom)
364}
365
366/// Update the conversions for non-native tokens. Namely calculate the reward,
367/// deflate it to real terms, make a 3-term allowed conversion, and compute how
368/// much needs to be minted in order to back the rewards.
369#[cfg(any(feature = "multicore", test))]
370#[allow(clippy::too_many_arguments)]
371fn update_non_native_conversions<S, TransToken>(
372    storage: &mut S,
373    token: &Address,
374    ref_inflation: u128,
375    normed_inflation: u128,
376    masp_epochs_per_year: u64,
377    masp_epoch: MaspEpoch,
378    reward_assets: [AssetType; 4],
379    total_reward: &mut Amount,
380    current_convs: &mut BTreeMap<
381        (Address, Denomination, MaspDigitPos),
382        AllowedConversion,
383    >,
384) -> Result<Denomination>
385where
386    S: StorageWrite + StorageRead + WithConversionState,
387    TransToken:
388        trans_token::Keys + trans_token::Read<S> + trans_token::Write<S>,
389{
390    let prev_masp_epoch =
391        masp_epoch.prev().ok_or_err_msg("MASP epoch underflow")?;
392    let (precision, denom) =
393        calculate_masp_rewards_precision::<S, TransToken>(storage, token)?;
394    let (reward, precision) = calculate_masp_rewards::<S, TransToken>(
395        storage,
396        token,
397        denom,
398        precision,
399        masp_epochs_per_year,
400    )?;
401    // Express the inflation reward in real terms, that is, with
402    // respect to the native asset in the zeroth epoch
403    let reward_uint = Uint::from(reward);
404    let ref_inflation_uint = Uint::from(ref_inflation);
405    let inflation_uint = Uint::from(normed_inflation);
406    let real_reward =
407        checked!((reward_uint * ref_inflation_uint) / inflation_uint)?
408            .try_into()
409            .unwrap_or_else(|_| {
410                tracing::warn!(
411                    "MASP reward for {} assumed to be 0 because the computed \
412                     value is too large. Please check the inflation \
413                     parameters.",
414                    token
415                );
416                0u128
417            });
418    // The conversion is computed such that if consecutive
419    // conversions are added together, the
420    // intermediate tokens cancel/ telescope out
421    let precision_i128 = i128::try_from(precision).into_storage_result()?;
422    let real_reward_i128 = i128::try_from(real_reward).into_storage_result()?;
423    for digit in MaspDigitPos::iter() {
424        // Provide an allowed conversion from previous timestamp. The
425        // negative sign allows each instance of the old asset to be
426        // cancelled out/replaced with the new asset
427        let old_asset = encode_asset_type(
428            token.clone(),
429            denom,
430            digit,
431            Some(prev_masp_epoch),
432        )
433        .into_storage_result()?;
434        let new_asset =
435            encode_asset_type(token.clone(), denom, digit, Some(masp_epoch))
436                .into_storage_result()?;
437
438        current_convs.insert(
439            (token.clone(), denom, digit),
440            checked!(
441                MaspAmount::from_pair(old_asset, -precision_i128)
442                    + &MaspAmount::from_pair(new_asset, precision_i128)
443                    + &MaspAmount::from_pair(
444                        reward_assets[digit as usize],
445                        real_reward_i128,
446                    )
447            )?
448            .into(),
449        );
450        // Add a conversion from the previous asset type
451        storage.conversion_state_mut().assets.insert(
452            old_asset,
453            ConversionLeaf {
454                token: token.clone(),
455                denom,
456                digit_pos: digit,
457                epoch: prev_masp_epoch,
458                conversion: MaspAmount::zero().into(),
459                leaf_pos: 0,
460            },
461        );
462    }
463    // Dispense a transparent reward in parallel to the shielded rewards
464    let addr_bal = TransToken::read_balance(storage, token, &MASP)?;
465    // The reward for each reward.1 units of the current asset
466    // is reward.0 units of the reward token
467    *total_reward = total_reward
468        .checked_add(
469            addr_bal
470                .u128_eucl_div_rem((reward, precision))
471                .ok_or_else(|| {
472                    Error::new_const("Total reward calculation overflow")
473                })?
474                .0,
475        )
476        .ok_or_else(|| Error::new_const("Total reward overflow"))?;
477    Ok(denom)
478}
479
480#[cfg(any(feature = "multicore", test))]
481/// Update the MASP's allowed conversions
482pub fn update_allowed_conversions<S, Params, TransToken>(
483    storage: &mut S,
484) -> Result<()>
485where
486    S: StorageWrite + StorageRead + WithConversionState,
487    Params: parameters::Read<S>,
488    TransToken:
489        trans_token::Keys + trans_token::Read<S> + trans_token::Write<S>,
490{
491    use std::cmp::Ordering;
492
493    use masp_primitives::bls12_381;
494    use masp_primitives::ff::PrimeField;
495    use masp_primitives::merkle_tree::FrozenCommitmentTree;
496    use masp_primitives::sapling::Node;
497    use namada_core::masp::encode_reward_asset_types;
498    use namada_core::token::NATIVE_MAX_DECIMAL_PLACES;
499    use rayon::iter::{
500        IndexedParallelIterator, IntoParallelIterator, ParallelIterator,
501    };
502    use rayon::prelude::ParallelSlice;
503
504    use crate::mint_rewards;
505
506    let token_map_key = masp_token_map_key();
507    let token_map: namada_core::masp::TokenMap =
508        storage.read(&token_map_key)?.unwrap_or_default();
509    let mut masp_reward_keys: Vec<_> = token_map.values().cloned().collect();
510    let mut masp_reward_denoms = BTreeMap::new();
511    // Put the native rewards first because other inflation computations depend
512    // on it
513    let native_token = storage.get_native_token()?;
514    masp_reward_keys.sort_unstable_by(|x, y| {
515        if (*x == native_token) == (*y == native_token) {
516            Ordering::Equal
517        } else if *x == native_token {
518            Ordering::Less
519        } else {
520            Ordering::Greater
521        }
522    });
523    // The total transparent value of the rewards being distributed
524    let mut total_reward = Amount::zero();
525
526    // Construct MASP asset type for rewards. Always deflate and timestamp
527    // reward tokens with the zeroth epoch to minimize the number of convert
528    // notes clients have to use. This trick works under the assumption that
529    // reward tokens will then be reinflated back to the current epoch.
530    let reward_assets =
531        encode_reward_asset_types(&native_token).into_storage_result()?;
532    // Conversions from the previous to current asset for each address
533    let mut current_convs = BTreeMap::<
534        (Address, Denomination, MaspDigitPos),
535        AllowedConversion,
536    >::new();
537    // Native token inflation values are always with respect to this
538    let ref_inflation = calculate_masp_rewards_precision::<S, TransToken>(
539        storage,
540        &native_token,
541    )?
542    .0;
543
544    // Reward all tokens according to above reward rates
545    let masp_epoch_multiplier = Params::masp_epoch_multiplier(storage)?;
546    let masp_epoch = MaspEpoch::try_from_epoch(
547        storage.get_block_epoch()?,
548        masp_epoch_multiplier,
549    )
550    .map_err(Error::new_const)?;
551    if masp_epoch.prev().is_none() {
552        return Ok(());
553    }
554    let epochs_per_year = Params::epochs_per_year(storage)?;
555    let masp_epochs_per_year =
556        checked!(epochs_per_year / masp_epoch_multiplier)?;
557    for token in &masp_reward_keys {
558        // Get the last rewarded amount of the native token
559        let normed_inflation = *storage
560            .conversion_state_mut()
561            .normed_inflation
562            .get_or_insert(ref_inflation);
563
564        // Generate conversions from the last epoch to the current and update
565        // the reward backing accumulator
566        let denom = if *token == native_token {
567            update_native_conversions::<_, TransToken>(
568                storage,
569                token,
570                normed_inflation,
571                masp_epochs_per_year,
572                masp_epoch,
573                &mut total_reward,
574                &mut current_convs,
575            )
576        } else {
577            update_non_native_conversions::<_, TransToken>(
578                storage,
579                token,
580                ref_inflation,
581                normed_inflation,
582                masp_epochs_per_year,
583                masp_epoch,
584                reward_assets,
585                &mut total_reward,
586                &mut current_convs,
587            )
588        }?;
589        masp_reward_denoms.insert(token.clone(), denom);
590    }
591
592    // Try to distribute Merkle leaf updating as evenly as possible across
593    // multiple cores
594    let num_threads = rayon::current_num_threads();
595    // Put assets into vector to enable computation batching
596    let assets: Vec<_> = storage
597        .conversion_state_mut()
598        .assets
599        .values_mut()
600        .enumerate()
601        .collect();
602
603    #[allow(clippy::arithmetic_side_effects)]
604    let notes_per_thread_max = (assets.len() + num_threads - 1) / num_threads;
605    // floor(assets.len() / num_threads)
606    #[allow(clippy::arithmetic_side_effects)]
607    let notes_per_thread_min = assets.len() / num_threads;
608
609    // Now on each core, add the latest conversion to each conversion
610    let conv_notes: Vec<Node> = assets
611        .into_par_iter()
612        .with_min_len(notes_per_thread_min)
613        .with_max_len(notes_per_thread_max)
614        .map(|(idx, leaf)| {
615            // Try to get the applicable conversion delta
616            let cur_conv_key = (leaf.token.clone(), leaf.denom, leaf.digit_pos);
617            if let Some(current_conv) = current_convs.get(&cur_conv_key) {
618                // Use transitivity to update conversion
619                #[allow(clippy::arithmetic_side_effects)]
620                {
621                    leaf.conversion += current_conv.clone();
622                }
623            }
624            // Update conversion position to leaf we are about to create
625            leaf.leaf_pos = idx;
626            // The merkle tree need only provide the conversion commitment,
627            // the remaining information is provided through the storage API
628            Node::new(leaf.conversion.cmu().to_repr())
629        })
630        .collect();
631
632    // Update the MASP's transparent reward token balance to ensure that it
633    // is sufficiently backed to redeem rewards
634    mint_rewards::<S, TransToken>(storage, total_reward)?;
635
636    // Try to distribute Merkle tree construction as evenly as possible
637    // across multiple cores
638    // Merkle trees must have exactly 2^n leaves to be mergeable
639    let mut notes_per_thread_rounded = 1;
640
641    // Cannot overflow
642    #[allow(clippy::arithmetic_side_effects)]
643    while notes_per_thread_max > notes_per_thread_rounded * 4 {
644        notes_per_thread_rounded *= 2;
645    }
646    // Make the sub-Merkle trees in parallel
647    let tree_parts: Vec<_> = conv_notes
648        .par_chunks(notes_per_thread_rounded)
649        .map(FrozenCommitmentTree::new)
650        .collect();
651
652    // Convert conversion vector into tree so that Merkle paths can be
653    // obtained
654    storage.conversion_state_mut().tree =
655        FrozenCommitmentTree::merge(&tree_parts);
656    // Update the anchor in storage
657    storage.write(
658        &crate::storage_key::masp_convert_anchor_key(),
659        namada_core::hash::Hash(
660            bls12_381::Scalar::from(storage.conversion_state().tree.root())
661                .to_bytes(),
662        ),
663    )?;
664
665    if !masp_reward_keys.contains(&native_token) {
666        // Since MASP rewards are denominated in NAM tokens, ensure that clients
667        // are able to decode them.
668        masp_reward_denoms
669            .insert(native_token.clone(), NATIVE_MAX_DECIMAL_PLACES.into());
670    }
671    // Add purely decoding entries to the assets map. These will be
672    // overwritten before the creation of the next commitment tree
673    for (addr, denom) in masp_reward_denoms {
674        for digit in MaspDigitPos::iter() {
675            // Add the decoding entry for the new asset type. An uncommitted
676            // node position is used since this is not a conversion.
677            let new_asset =
678                encode_asset_type(addr.clone(), denom, digit, Some(masp_epoch))
679                    .into_storage_result()?;
680            let tree_size = storage.conversion_state().tree.size();
681            storage.conversion_state_mut().assets.insert(
682                new_asset,
683                ConversionLeaf {
684                    token: addr.clone(),
685                    denom,
686                    digit_pos: digit,
687                    epoch: masp_epoch,
688                    conversion: MaspAmount::zero().into(),
689                    leaf_pos: tree_size,
690                },
691            );
692        }
693    }
694    // store only the assets hash because the size is quite large
695    let assets_hash =
696        Hash::sha256(storage.conversion_state().assets.serialize_to_vec());
697    storage.write(&masp_assets_hash_key(), assets_hash)?;
698
699    Ok(())
700}
701
702// This is only enabled when "wasm-runtime" is on, because we're using rayon
703#[cfg(not(any(feature = "multicore", test)))]
704/// Update the MASP's allowed conversions
705pub fn update_allowed_conversions<S, Params, TransToken>(
706    _storage: &mut S,
707) -> Result<()>
708where
709    S: StorageWrite + StorageRead + WithConversionState,
710    Params: parameters::Read<S>,
711    TransToken: trans_token::Keys,
712{
713    Ok(())
714}
715
716#[allow(clippy::arithmetic_side_effects)]
717#[cfg(test)]
718mod tests {
719    use std::str::FromStr;
720
721    use namada_core::address;
722    use namada_core::collections::HashMap;
723    use namada_core::dec::testing::arb_non_negative_dec;
724    use namada_core::token::testing::arb_amount;
725    use namada_state::testing::TestStorage;
726    use namada_trans_token::storage_key::{balance_key, minted_balance_key};
727    use namada_trans_token::write_denom;
728    use proptest::prelude::*;
729    use proptest::test_runner::Config;
730    use test_log::test;
731
732    use super::*;
733    use crate::ShieldedParams;
734
735    proptest! {
736        #![proptest_config(Config {
737            cases: 10,
738            .. Config::default()
739        })]
740        #[test]
741        fn test_updated_allowed_conversions(
742            initial_balance in arb_amount(),
743            masp_locked_ratio in arb_non_negative_dec(),
744        ) {
745            test_updated_allowed_conversions_aux(initial_balance, masp_locked_ratio)
746        }
747    }
748
749    fn test_updated_allowed_conversions_aux(
750        initial_balance: Amount,
751        masp_locked_ratio: Dec,
752    ) {
753        const ROUNDS: usize = 10;
754
755        let mut s = TestStorage::default();
756        // Initialize the state
757        {
758            // Parameters
759            namada_parameters::init_test_storage(&mut s).unwrap();
760
761            // Tokens
762            let token_params = ShieldedParams {
763                max_reward_rate: Dec::from_str("0.1").unwrap(),
764                kp_gain_nom: Dec::from_str("0.1").unwrap(),
765                kd_gain_nom: Dec::from_str("0.1").unwrap(),
766                locked_amount_target: 10_000_u64,
767            };
768
769            for (token_addr, (alias, denom)) in tokens() {
770                namada_trans_token::write_params(&mut s, &token_addr).unwrap();
771                crate::write_params::<_, namada_trans_token::Store<()>>(
772                    &token_params,
773                    &mut s,
774                    &token_addr,
775                    &denom,
776                )
777                .unwrap();
778
779                write_denom(&mut s, &token_addr, denom).unwrap();
780
781                // Write a minted token balance
782                let total_token_balance = initial_balance;
783                s.write(&minted_balance_key(&token_addr), total_token_balance)
784                    .unwrap();
785
786                // Put the locked ratio into MASP
787                s.write(
788                    &balance_key(&token_addr, &address::MASP),
789                    masp_locked_ratio * total_token_balance,
790                )
791                .unwrap();
792
793                // Insert tokens into MASP conversion state
794                let token_map_key = masp_token_map_key();
795                let mut token_map: namada_core::masp::TokenMap =
796                    s.read(&token_map_key).unwrap().unwrap_or_default();
797                token_map.insert(alias.to_string(), token_addr.clone());
798                s.write(&token_map_key, token_map).unwrap();
799            }
800        }
801
802        for i in 0..ROUNDS {
803            println!("Round {i}");
804            update_allowed_conversions::<
805                _,
806                namada_parameters::Store<_>,
807                namada_trans_token::Store<_>,
808            >(&mut s)
809            .unwrap();
810            println!();
811            println!();
812        }
813    }
814
815    pub fn tokens() -> HashMap<Address, (&'static str, Denomination)> {
816        vec![
817            (address::testing::nam(), ("nam", 6.into())),
818            (address::testing::btc(), ("btc", 8.into())),
819            (address::testing::eth(), ("eth", 18.into())),
820            (address::testing::dot(), ("dot", 10.into())),
821            (address::testing::schnitzel(), ("schnitzel", 6.into())),
822            (address::testing::apfel(), ("apfel", 6.into())),
823            (address::testing::kartoffel(), ("kartoffel", 6.into())),
824        ]
825        .into_iter()
826        .collect()
827    }
828
829    #[test]
830    fn test_masp_inflation_playground() {
831        let denom = Uint::from(1_000_000); // token denomination (usually 6)
832        let total_tokens = 10_000_000_000_u64; // 10B naan
833        let mut total_tokens = Uint::from(total_tokens) * denom;
834        let locked_tokens_target = Uint::from(500_000) * denom; // Dependent on the token type
835        let init_locked_ratio = Dec::from_str("0.1").unwrap(); // Arbitrary amount to play around with
836        let init_locked_tokens = (init_locked_ratio
837            * Dec::try_from(locked_tokens_target).unwrap())
838        .to_uint()
839        .unwrap();
840        let epochs_per_year = 730_u64; // SE configuration
841        let max_reward_rate = Dec::from_str("0.01").unwrap(); // Pre-determined based on token type
842        let mut last_inflation_amount = Uint::zero();
843        let p_gain_nom = Dec::from_str("25000").unwrap(); // To be configured
844        let d_gain_nom = Dec::from_str("25000").unwrap(); // To be configured
845
846        let mut locked_amount = init_locked_tokens;
847        let mut locked_tokens_last = init_locked_tokens;
848
849        let num_rounds = 10;
850        println!();
851
852        for round in 0..num_rounds {
853            let inflation = compute_inflation(
854                locked_amount,
855                total_tokens,
856                max_reward_rate,
857                last_inflation_amount,
858                p_gain_nom,
859                d_gain_nom,
860                epochs_per_year,
861                Dec::try_from(locked_tokens_target).unwrap(),
862                Dec::try_from(locked_tokens_last).unwrap(),
863            );
864
865            let rate = Dec::try_from(inflation).unwrap()
866                * Dec::from(epochs_per_year)
867                / Dec::try_from(total_tokens).unwrap();
868
869            println!(
870                "Round {round}: Locked amount: {locked_amount}, inflation \
871                 rate: {rate} -- (raw infl: {inflation})",
872            );
873            // dbg!(&controller);
874
875            last_inflation_amount = inflation;
876            total_tokens += inflation;
877            locked_tokens_last = locked_amount;
878
879            let change_staked_tokens = Uint::from(2) * locked_tokens_target;
880            locked_amount += change_staked_tokens;
881        }
882    }
883}