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