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}