iota_genesis_builder/
stake.rs

1// Copyright (c) 2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4//! Logic and types to account for stake delegation during genesis.
5use anyhow::bail;
6use iota_config::genesis::{
7    Delegations, TokenAllocation, TokenDistributionSchedule, TokenDistributionScheduleBuilder,
8    ValidatorAllocation,
9};
10use iota_types::{
11    base_types::{IotaAddress, ObjectRef},
12    object::Object,
13    stardust::coin_kind::get_gas_balance_maybe,
14};
15
16use crate::stardust::migration::{ExpirationTimestamp, MigrationObjects};
17
18#[derive(Default, Debug, Clone)]
19pub struct GenesisStake {
20    token_allocation: Vec<TokenAllocation>,
21    gas_coins_to_destroy: Vec<ObjectRef>,
22    timelocks_to_destroy: Vec<ObjectRef>,
23    timelocks_to_split: Vec<(ObjectRef, u64, IotaAddress)>,
24}
25
26impl GenesisStake {
27    /// Take the inner gas-coin objects that must be destroyed.
28    ///
29    /// This follows the semantics of [`std::mem::take`].
30    pub fn take_gas_coins_to_destroy(&mut self) -> Vec<ObjectRef> {
31        std::mem::take(&mut self.gas_coins_to_destroy)
32    }
33
34    /// Take the inner timelock objects that must be destroyed.
35    ///
36    /// This follows the semantics of [`std::mem::take`].
37    pub fn take_timelocks_to_destroy(&mut self) -> Vec<ObjectRef> {
38        std::mem::take(&mut self.timelocks_to_destroy)
39    }
40
41    /// Take the inner timelock objects that must be split.
42    ///
43    /// This follows the semantics of [`std::mem::take`].
44    pub fn take_timelocks_to_split(&mut self) -> Vec<(ObjectRef, u64, IotaAddress)> {
45        std::mem::take(&mut self.timelocks_to_split)
46    }
47
48    pub fn is_empty(&self) -> bool {
49        self.token_allocation.is_empty()
50            && self.gas_coins_to_destroy.is_empty()
51            && self.timelocks_to_destroy.is_empty()
52            && self.timelocks_to_split.is_empty()
53    }
54
55    /// Calculate the total amount of token allocations.
56    pub fn sum_token_allocation(&self) -> u64 {
57        self.token_allocation
58            .iter()
59            .map(|allocation| allocation.amount_nanos)
60            .sum()
61    }
62
63    /// Create a new valid [`TokenDistributionSchedule`] from the
64    /// inner token allocations.
65    pub fn to_token_distribution_schedule(
66        &self,
67        total_supply_nanos: u64,
68    ) -> TokenDistributionSchedule {
69        let mut builder = TokenDistributionScheduleBuilder::new();
70
71        let pre_minted_supply = self.calculate_pre_minted_supply(total_supply_nanos);
72
73        builder.set_pre_minted_supply(pre_minted_supply);
74
75        for allocation in self.token_allocation.clone() {
76            builder.add_allocation(allocation);
77        }
78        builder.build()
79    }
80
81    /// Extend a [`TokenDistributionSchedule`] without migration with the
82    /// inner token allocations.
83    ///
84    /// The resulting schedule is guaranteed to contain allocations
85    /// that sum up the initial total supply of IOTA in nanos.
86    ///
87    /// ## Errors
88    ///
89    /// The method fails if the resulting schedule contains is invalid.
90    pub fn extend_token_distribution_schedule_without_migration(
91        &self,
92        mut schedule_without_migration: TokenDistributionSchedule,
93        total_supply_nanos: u64,
94    ) -> TokenDistributionSchedule {
95        schedule_without_migration
96            .allocations
97            .extend(self.token_allocation.clone());
98        schedule_without_migration.pre_minted_supply =
99            self.calculate_pre_minted_supply(total_supply_nanos);
100        schedule_without_migration.validate();
101        schedule_without_migration
102    }
103
104    /// Calculates the part of the IOTA supply that is pre-minted.
105    fn calculate_pre_minted_supply(&self, total_supply_nanos: u64) -> u64 {
106        total_supply_nanos - self.sum_token_allocation()
107    }
108
109    /// Creates a `GenesisStake` using a `Delegations` containing the necessary
110    /// allocations for validators by some delegators.
111    ///
112    /// This function invokes `delegate_genesis_stake` for each delegator found
113    /// in `Delegations`.
114    pub fn new_with_delegations(
115        delegations: Delegations,
116        migration_objects: &MigrationObjects,
117    ) -> anyhow::Result<Self> {
118        let mut stake = GenesisStake::default();
119
120        for (delegator, validators_allocations) in delegations.allocations {
121            // Fetch all timelock and gas objects owned by the delegator
122            let timelocks_pool =
123                migration_objects.get_sorted_timelocks_and_expiration_by_owner(delegator);
124            let gas_coins_pool = migration_objects.get_gas_coins_by_owner(delegator);
125            if timelocks_pool.is_none() && gas_coins_pool.is_none() {
126                bail!("no timelocks or gas-coin objects found for delegator {delegator:?}");
127            }
128            stake.delegate_genesis_stake(
129                &validators_allocations,
130                delegator,
131                &mut timelocks_pool.unwrap_or_default().into_iter(),
132                &mut gas_coins_pool
133                    .unwrap_or_default()
134                    .into_iter()
135                    .map(|object| (object, 0)),
136            )?;
137        }
138
139        Ok(stake)
140    }
141
142    fn create_token_allocation(
143        &mut self,
144        recipient_address: IotaAddress,
145        amount_nanos: u64,
146        staked_with_validator: Option<IotaAddress>,
147        staked_with_timelock_expiration: Option<u64>,
148    ) {
149        self.token_allocation.push(TokenAllocation {
150            recipient_address,
151            amount_nanos,
152            staked_with_validator,
153            staked_with_timelock_expiration,
154        });
155    }
156
157    /// Create the necessary allocations for `validators_allocations` using the
158    /// assets of the `delegator`.
159    ///
160    /// This function iterates in turn over [`TimeLock`] and
161    /// [`GasCoin`][iota_types::gas_coin::GasCoin] objects created
162    /// during stardust migration that are owned by the `delegator`.
163    pub fn delegate_genesis_stake<'obj>(
164        &mut self,
165        validators_allocations: &[ValidatorAllocation],
166        delegator: IotaAddress,
167        timelocks_pool: &mut impl Iterator<Item = (&'obj Object, ExpirationTimestamp)>,
168        gas_coins_pool: &mut impl Iterator<Item = (&'obj Object, ExpirationTimestamp)>,
169    ) -> anyhow::Result<()> {
170        // Temp stores for holding the surplus
171        let mut timelock_surplus = SurplusCoin::default();
172        let mut gas_surplus = SurplusCoin::default();
173
174        // Then, try to create new token allocations for each validator using the
175        // objects fetched above
176        for validator_allocation in validators_allocations {
177            // The validator address
178            let validator = validator_allocation.validator;
179            // The target amount of nanos to be staked, either with timelock or gas objects
180            let mut target_stake_nanos = validator_allocation.amount_nanos_to_stake;
181            // The gas to pay to the validator
182            let gas_to_pay_nanos = validator_allocation.amount_nanos_to_pay_gas;
183
184            // Start filling allocations with timelocks
185
186            // Pick fresh timelock objects (if present) and possibly reuse the surplus
187            // coming from the previous iteration.
188            // The method `pick_objects_for_allocation` firstly checks if the
189            // `timelock_surplus` can be used to reach or reduce the `target_stake_nanos`.
190            // Then it iterates over the `timelocks_pool`. For each timelock object, its
191            // balance is used to reduce the `target_stake_nanos` while its the object
192            // reference is placed into a vector `to_destroy`. At the end, the
193            // `pick_objects_for_allocation` method returns an `AllocationObjects` including
194            // the list of objects to destroy, the list `staked_with_timelock` containing
195            // the information for creating token allocations with timestamps
196            // and a CoinSurplus (even empty).
197            let mut timelock_allocation_objects = pick_objects_for_allocation(
198                timelocks_pool,
199                target_stake_nanos,
200                &mut timelock_surplus,
201            );
202            if !timelock_allocation_objects.staked_with_timelock.is_empty() {
203                // Inside this block some timelock objects were picked from the pool; so we can
204                // save all the references to timelocks to destroy, if there are any
205                self.timelocks_to_destroy
206                    .append(&mut timelock_allocation_objects.to_destroy);
207                // Finally we create some token allocations based on timelock_allocation_objects
208                timelock_allocation_objects
209                    .staked_with_timelock
210                    .iter()
211                    .for_each(|&(timelocked_amount, expiration_timestamp)| {
212                        // For timelocks we create a `TokenAllocation` object with
213                        // `staked_with_timelock` filled with entries
214                        self.create_token_allocation(
215                            delegator,
216                            timelocked_amount,
217                            Some(validator),
218                            Some(expiration_timestamp),
219                        );
220                    });
221            }
222            // The remainder of the target stake after timelock objects were used.
223            target_stake_nanos -= timelock_allocation_objects.amount_nanos;
224
225            // After allocating timelocked stakes, then
226            // 1. allocate gas coin stakes (if timelocked funds were not enough)
227            // 2. and/or allocate gas coin payments (if indicated in the validator
228            //    allocation).
229
230            // The target amount of gas coin nanos to be allocated, either with staking or
231            // to pay
232            let target_gas_nanos = target_stake_nanos + gas_to_pay_nanos;
233            // Pick fresh gas coin objects (if present) and possibly reuse the surplus
234            // coming from the previous iteration. The logic is the same as above with
235            // timelocks.
236            let mut gas_coin_objects =
237                pick_objects_for_allocation(gas_coins_pool, target_gas_nanos, &mut gas_surplus);
238            if gas_coin_objects.amount_nanos >= target_gas_nanos {
239                // Inside this block some gas coin objects were picked from the pool; so we can
240                // save all the references to gas coins to destroy
241                self.gas_coins_to_destroy
242                    .append(&mut gas_coin_objects.to_destroy);
243                // Then
244                // Case 1. allocate gas stakes
245                if target_stake_nanos > 0 {
246                    // For staking gas coins we create a `TokenAllocation` object with
247                    // an empty `staked_with_timelock`
248                    self.create_token_allocation(
249                        delegator,
250                        target_stake_nanos,
251                        Some(validator),
252                        None,
253                    );
254                }
255                // Case 2. allocate gas payments
256                if gas_to_pay_nanos > 0 {
257                    // For gas coins payments we create a `TokenAllocation` object with
258                    // `recipient_address` being the validator and no stake
259                    self.create_token_allocation(validator, gas_to_pay_nanos, None, None);
260                }
261            } else {
262                // It means the delegator finished all the timelock or gas funds
263                bail!("Not enough funds for delegator {:?}", delegator);
264            }
265        }
266
267        // If some surplus amount is left, then return it to the delegator
268        // In the case of a timelock object, it must be split during the `genesis` PTB
269        // execution
270        if let (Some(surplus_timelock), surplus_nanos, _) = timelock_surplus.take() {
271            self.timelocks_to_split
272                .push((surplus_timelock, surplus_nanos, delegator));
273        }
274        // In the case of a gas coin, it must be destroyed and the surplus re-allocated
275        // to the delegator (no split)
276        if let (Some(surplus_gas_coin), surplus_nanos, _) = gas_surplus.take() {
277            self.gas_coins_to_destroy.push(surplus_gas_coin);
278            self.create_token_allocation(delegator, surplus_nanos, None, None);
279        }
280
281        Ok(())
282    }
283}
284
285/// The objects picked for token allocation during genesis
286#[derive(Default, Debug, Clone)]
287struct AllocationObjects {
288    /// The list of objects to destroy for the allocations
289    to_destroy: Vec<ObjectRef>,
290    /// The total amount of nanos to be allocated from this
291    /// collection of objects.
292    amount_nanos: u64,
293    /// A (possible empty) vector of (amount, timelock_expiration) pairs
294    /// indicating the amount to timelock stake and its expiration
295    staked_with_timelock: Vec<(u64, u64)>,
296}
297
298/// The surplus object that should be split for this allocation. Only part
299/// of its balance will be used for this collection of this
300/// `AllocationObjects`, the surplus might be used later.
301#[derive(Default, Debug, Clone)]
302struct SurplusCoin {
303    // The reference of the coin to possibly split to get the surplus.
304    coin_object_ref: Option<ObjectRef>,
305    /// The surplus amount for that coin object.
306    surplus_nanos: u64,
307    /// Possibly indicate a timelock stake expiration.
308    timestamp: u64,
309}
310
311impl SurplusCoin {
312    // Check if the current surplus can be reused.
313    // The surplus coin_object_ref is returned to be included in a `to_destroy` list
314    // when surplus_nanos <= target_amount_nanos. Otherwise it means the
315    // target_amount_nanos is completely reached, so we can still keep
316    // coin_object_ref as surplus coin and only reduce the surplus_nanos value.
317    pub fn maybe_reuse_surplus(
318        &mut self,
319        target_amount_nanos: u64,
320    ) -> (Option<ObjectRef>, u64, u64) {
321        // If the surplus is some, then we can use the surplus nanos
322        if self.coin_object_ref.is_some() {
323            // If the surplus nanos are less or equal than the target, then use them all and
324            // return the coin object to be destroyed
325            if self.surplus_nanos <= target_amount_nanos {
326                let (coin_object_ref_opt, surplus, timestamp) = self.take();
327                (Some(coin_object_ref_opt.unwrap()), surplus, timestamp)
328            } else {
329                // If the surplus nanos more than the target, do not return the coin object
330                self.surplus_nanos -= target_amount_nanos;
331                (None, target_amount_nanos, self.timestamp)
332            }
333        } else {
334            (None, 0, 0)
335        }
336    }
337
338    // Destroy the `CoinSurplus` and take the fields.
339    pub fn take(&mut self) -> (Option<ObjectRef>, u64, u64) {
340        let surplus = self.surplus_nanos;
341        self.surplus_nanos = 0;
342        let timestamp = self.timestamp;
343        self.timestamp = 0;
344        (self.coin_object_ref.take(), surplus, timestamp)
345    }
346}
347
348/// Pick gas-coin like objects from a pool to cover
349/// the `target_amount_nanos`. It might also make use of a previous coin
350/// surplus.
351///
352/// This does not split any surplus balance, but delegates
353/// splitting to the caller.
354fn pick_objects_for_allocation<'obj>(
355    pool: &mut impl Iterator<Item = (&'obj Object, ExpirationTimestamp)>,
356    target_amount_nanos: u64,
357    surplus_coin: &mut SurplusCoin,
358) -> AllocationObjects {
359    // Vector used to keep track of timestamps while allocating timelock coins.
360    // Will be left empty in the case of gas coins
361    let mut staked_with_timelock = vec![];
362    // Vector used to keep track of the coins to destroy.
363    let mut to_destroy = vec![];
364    // Variable used to keep track of allocated nanos during the picking.
365    let mut allocation_amount_nanos = 0;
366
367    // Maybe use the surplus coin passed as input.
368    let (surplus_object_option, used_surplus_nanos, surplus_timestamp) =
369        surplus_coin.maybe_reuse_surplus(target_amount_nanos);
370
371    // If the surplus coin was used then allocate the nanos and maybe destroy it
372    if used_surplus_nanos > 0 {
373        allocation_amount_nanos += used_surplus_nanos;
374        if surplus_timestamp > 0 {
375            staked_with_timelock.push((used_surplus_nanos, surplus_timestamp));
376        }
377        // If the `surplus_object` is returned by `maybe_reuse_surplus`, then it means
378        // it used all its `used_surplus_nanos` and it can be destroyed.
379        if let Some(surplus_object) = surplus_object_option {
380            to_destroy.push(surplus_object);
381        }
382    }
383    // Else, if the `surplus_object` was not completely drained, then we
384    // don't need to continue. In this case `allocation_amount_nanos ==
385    // target_amount_nanos`.
386
387    // Only if `allocation_amount_nanos` < `target_amount_nanos` then pick an
388    // object (if we still have objects in the pool). If this object's balance is
389    // less than the difference required to reach the target, then push this
390    // object's reference into the `to_destroy` list. Else, take out only the
391    // required amount and set the object as a "surplus" (then break the loop).
392    while allocation_amount_nanos < target_amount_nanos {
393        if let Some((object, timestamp)) = pool.next() {
394            // In here we pick an object
395            let obj_ref = object.compute_object_reference();
396            let object_balance = get_gas_balance_maybe(object)
397                .expect("the pool should only contain gas coins or timelock balance objects")
398                .value();
399
400            // Then we create the allocation
401            let difference_from_target = target_amount_nanos - allocation_amount_nanos;
402            let to_allocate = object_balance.min(difference_from_target);
403            allocation_amount_nanos += to_allocate;
404            if timestamp > 0 {
405                staked_with_timelock.push((to_allocate, timestamp));
406            }
407
408            // If the balance is less or equal than the difference from target, then
409            // place `obj_ref` in `to_destroy` and continue
410            if object_balance <= difference_from_target {
411                to_destroy.push(obj_ref);
412            } else {
413                // Else, do NOT place `obj_ref` in `to_destroy` because it is reused in
414                // the SurplusCoin and then BREAK, because we reached the target
415                *surplus_coin = SurplusCoin {
416                    coin_object_ref: Some(obj_ref),
417                    surplus_nanos: object_balance - difference_from_target,
418                    timestamp,
419                };
420                break;
421            }
422        } else {
423            // We have no more objects to pick from the pool; the function will end with
424            // allocation_amount_nanos < target_amount_nanos
425            break;
426        }
427    }
428
429    AllocationObjects {
430        to_destroy,
431        amount_nanos: allocation_amount_nanos,
432        staked_with_timelock,
433    }
434}