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}