@@ -307,3 +307,216 @@ let if_ b ~(then_ : var) ~(else_ : var) =
307307let deriver obj =
308308 let open Fields_derivers_zkapps in
309309 iso_record ~to_record ~of_record As_record. deriver obj
310+
311+ (* * A module defining the vesting parameter update procedure from the slot
312+ reduction MIP. See
313+ https://github.com/MinaProtocol/MIPs/blob/main/MIPS/mip-0006-slot-reduction-90s.md.
314+ To summarize, this vesting parameter update is intended to be applied as an
315+ edit to the ledgers during a hard fork that reduces the slot time by half.
316+ The update only applies to accounts that are actively vesting at the time of
317+ the hard fork; these are accounts that either have not reached their cliff
318+ time yet, or have reached their cliff time but still have a positive minimum
319+ balance. The update procedure does the best it can to adjust the account
320+ timing information so that actively vesting accounts will still unlock funds
321+ at the same system times as they would have had the hard fork not occurred.
322+ It will do this perfectly for any account with a "reasonable" vesting
323+ schedule, i.e., one that will finish before half the remaining life of the
324+ chain (at the time of the hard fork) has elapsed.
325+
326+ An important note on the parameters: the slot at which the hard fork occurs
327+ is called [hardfork_slot] below. This slot is the global slot since genesis
328+ that is set in the genesis constants of the new (post hard fork) chain. It
329+ is not the global slot of the hard fork block, nor the [slot_tx_end], nor
330+ the [slot_chain_end]. The update procedure also assumes that the genesis
331+ timestamp of the new chain is set to be equal to the timestamp of the
332+ [hardfork_slot] {i relative to the old (pre hard fork) chain}. This
333+ simplifies the equations.
334+ *)
335+ module Slot_reduction_update = struct
336+ open Unsigned
337+
338+ (* * A form of [As_record.t] where all the fields are lifted to
339+ the same [UInt64.t] type, so the arithmetic in the vesting update
340+ equations becomes simpler. *)
341+ type t =
342+ ( unit
343+ , Unsigned_extended.UInt64 .t
344+ , Unsigned_extended.UInt64 .t
345+ , Unsigned_extended.UInt64 .t
346+ , Unsigned_extended.UInt64 .t )
347+ As_record .t
348+ [@@ deriving equal , sexp_of ]
349+
350+ let clamp_uint64_to_uint32 x =
351+ UInt64. (
352+ if compare x (of_uint32 UInt32. max_int) < = 0 then to_uint32 x
353+ else UInt32. max_int)
354+
355+ let of_record (t : as_record ) : t =
356+ { is_timed = ()
357+ ; initial_minimum_balance = t.initial_minimum_balance |> Balance. to_uint64
358+ ; cliff_time =
359+ t.cliff_time |> Global_slot_since_genesis. to_uint32 |> UInt64. of_uint32
360+ ; cliff_amount = t.cliff_amount |> Amount. to_uint64
361+ ; vesting_period =
362+ t.vesting_period |> Global_slot_span. to_uint32 |> UInt64. of_uint32
363+ ; vesting_increment = t.vesting_increment |> Amount. to_uint64
364+ }
365+
366+ (* * Convert to a regular [as_record] by clamping all the values
367+ that might be out of range, as specified in the MIP *)
368+ let to_record (t : t ) : as_record =
369+ { is_timed = true
370+ ; initial_minimum_balance = t.initial_minimum_balance |> Balance. of_uint64
371+ ; cliff_time =
372+ t.cliff_time |> clamp_uint64_to_uint32
373+ |> Global_slot_since_genesis. of_uint32
374+ ; cliff_amount = t.cliff_amount |> Amount. of_uint64
375+ ; vesting_period =
376+ t.vesting_period |> clamp_uint64_to_uint32 |> Global_slot_span. of_uint32
377+ ; vesting_increment = t.vesting_increment |> Amount. of_uint64
378+ }
379+
380+ (* * Calculate the total number of iterations needed for this account to vest
381+ completely, beyond the initial cliff unlock. This is [None] (undefined) if
382+ the vesting increment is zero and the cliff amount is smaller than the
383+ initial minimum balance. *)
384+ let vesting_iterations (t : t ) : UInt64.t option =
385+ UInt64. (
386+ if compare t.initial_minimum_balance t.cliff_amount < = 0 then
387+ (* Account will complete vesting instantly at the cliff *)
388+ Some zero
389+ else if equal t.vesting_increment zero then
390+ (* Number of iterations is undefined - account is permanently stuck with
391+ a minimum balance *)
392+ None
393+ else
394+ let balance_to_unlock =
395+ Infix. (t.initial_minimum_balance - t.cliff_amount)
396+ in
397+ let full_increment_iterations =
398+ Infix. (balance_to_unlock / t.vesting_increment)
399+ in
400+ if equal Infix. (balance_to_unlock mod t.vesting_increment) zero then
401+ (* The account unlocks an equal amount of funds during each iteration *)
402+ Some full_increment_iterations
403+ else
404+ (* The account needs one more iteration to unlock the last little bit
405+ of funds. Note: if this happens, then full_increment_iterations
406+ will necessarily be well below UInt64.max_int, because division by
407+ t.vesting_increment will have decreased balance_to_unlock by a
408+ factor of at least two. *)
409+ Some Infix. (full_increment_iterations + one))
410+
411+ (* * True if an account has started vesting but the slot at which it completes
412+ vesting is still in the future *)
413+ let is_partially_vested ~global_slot (t : t ) =
414+ let global_slot =
415+ global_slot |> Global_slot_since_genesis. to_uint32 |> UInt64. of_uint32
416+ in
417+ match vesting_iterations t with
418+ | None ->
419+ false
420+ | Some iterations ->
421+ UInt64. (
422+ compare global_slot t.cliff_time > = 0
423+ && compare iterations
424+ Infix. ((global_slot - t.cliff_time) / t.vesting_period)
425+ > 0 )
426+
427+ (* * True if an account has not started vesting *)
428+ let not_yet_vesting ~global_slot (t : t ) =
429+ let global_slot =
430+ global_slot |> Global_slot_since_genesis. to_uint32 |> UInt64. of_uint32
431+ in
432+ UInt64. compare global_slot t.cliff_time < 0
433+
434+ (* * True if an account is actively vesting, as defined by the slot reduction
435+ MIP. Note that this is (almost) equivalent to the minimum balance of the
436+ account being positive at [global_slot].
437+
438+ One subtlety: this is not equivalent to the statement "the account
439+ unlocked funds at [global_slot]". Once an account has a minimum balance of
440+ zero, it is no longer considered to be participating in the vesting
441+ system. In particular, at the [final_vesting_slot] of the timing the
442+ account will have unlocked funds, and yet it will not be actively vesting
443+ at that slot. (Unlocking funds happens between slots, so to speak).
444+
445+ Second subtlety: this isn't exactly equivalent to the minimum balance of
446+ an account being positive. If an account has zero [vesting_increment] and
447+ didn't vest completely at [cliff_time], then it will be stuck with a
448+ permanent positive minimum balance. Such accounts are not actively
449+ vesting. See [vesting_iterations]. *)
450+ let is_actively_vesting ~global_slot (t : t ) =
451+ not_yet_vesting ~global_slot t || is_partially_vested ~global_slot t
452+
453+ (* * Hardfork adjustment assuming that t is actively vesting *)
454+ let actively_vesting_hardfork_adjustment ~hardfork_slot (t : t ) =
455+ let hardfork_slot =
456+ hardfork_slot |> Global_slot_since_genesis. to_uint32 |> UInt64. of_uint32
457+ in
458+ UInt64. (
459+ if compare hardfork_slot t.cliff_time < 0 then
460+ (* t has not started vesting *)
461+ { t with
462+ cliff_time =
463+ (* global_slot and cliff_time are in the uint32 range, so this will
464+ not wrap *)
465+ Infix. (hardfork_slot + ((t.cliff_time - hardfork_slot) * of_int 2 ))
466+ ; vesting_period =
467+ (* vesting period is in the uint32 range, so this will not wrap *)
468+ Infix. (of_int 2 * t.vesting_period)
469+ }
470+ else
471+ (* t is partially but not fully vested *)
472+ { t with
473+ initial_minimum_balance =
474+ (let balance_after_cliff =
475+ assert (compare t.initial_minimum_balance t.cliff_amount > 0 ) ;
476+ Infix. (t.initial_minimum_balance - t.cliff_amount)
477+ in
478+ let elapsed_vesting_periods =
479+ assert (compare t.vesting_period zero > 0 ) ;
480+ Infix. ((hardfork_slot - t.cliff_time) / t.vesting_period)
481+ in
482+ let incremental_unlocked_balance =
483+ assert (
484+ equal elapsed_vesting_periods zero
485+ || compare t.vesting_increment
486+ (div max_int elapsed_vesting_periods)
487+ < = 0 ) ;
488+ Infix. (t.vesting_increment * elapsed_vesting_periods)
489+ in
490+ assert (
491+ compare balance_after_cliff incremental_unlocked_balance > = 0 ) ;
492+ Infix. (balance_after_cliff - incremental_unlocked_balance) )
493+ ; cliff_time =
494+ (* All the times and spans are in the uint32 range, so none of this
495+ will overflow *)
496+ Infix. (
497+ hardfork_slot
498+ + of_int 2
499+ * ( t.vesting_period
500+ - ((hardfork_slot - t.cliff_time) mod t.vesting_period) ))
501+ ; cliff_amount = t.vesting_increment
502+ ; vesting_period =
503+ (* vesting_period is in the uint32 range, so this will not wrap *)
504+ Infix. (of_int 2 * t.vesting_period)
505+ })
506+
507+ (* * Apply the hardfork adjustment to the given timing, doing nothing if it is
508+ not actively vesting *)
509+ let hardfork_adjustment ~hardfork_slot (t : t ) =
510+ if is_actively_vesting ~global_slot: hardfork_slot t then
511+ actively_vesting_hardfork_adjustment ~hardfork_slot t
512+ else t
513+ end
514+
515+ (* * Apply the slot reduction update to the [as_record] timing. This does nothing
516+ if the account is not actively vesting at [hardfork_slot]. See the
517+ [Slot_reduction_update] module documentation for general usage notes. *)
518+ let slot_reduction_update ~hardfork_slot (t : as_record ) =
519+ if t.is_timed then
520+ Slot_reduction_update. (
521+ t |> of_record |> hardfork_adjustment ~hardfork_slot |> to_record)
522+ else t
0 commit comments