Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source
Source file staking_pseudotokens_storage.ml
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496(*****************************************************************************)(* *)(* Open Source License *)(* Copyright (c) 2023 Nomadic Labs <contact@nomadic-labs.com> *)(* *)(* Permission is hereby granted, free of charge, to any person obtaining a *)(* copy of this software and associated documentation files (the "Software"),*)(* to deal in the Software without restriction, including without limitation *)(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *)(* and/or sell copies of the Software, and to permit persons to whom the *)(* Software is furnished to do so, subject to the following conditions: *)(* *)(* The above copyright notice and this permission notice shall be included *)(* in all copies or substantial portions of the Software. *)(* *)(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*)(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *)(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *)(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*)(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *)(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *)(* DEALINGS IN THE SOFTWARE. *)(* *)(*****************************************************************************)(** {0} Introduction
This module is responsible for maintaining the
{!Storage.Contract.Frozen_deposits_pseudotokens} and
{!Storage.Contract.Staking_pseudotokens} tables.
{1} Terminology
Even so a registered delegate is always technically in a delegation
relation with itself, in this module, when we use the word
"delegator", we always mean a delegator different from the
delegate itself. The word "staker" means for a delegator who
participates in staking. The word "staker" means any participant
in staking, either a delegate or a staker. In this module, we
use the word "contract" to mean either a delegate or a delegator.
{1} Full staking balance of a delegate
For each delegate, the {!Stake_storage} module is responsible to
track three tez quantities which can be requested with the
{!Stake_storage.get_full_staking_balance} function: [own_frozen]
is the frozen deposits of the delegate, [staked_frozen] is the
sum of all frozen deposits of its stakers, and [delegate] is the
sum of all tez delegated to the delegate (some of which may belong
to the delegate itself). This module is in charge of tracking the
frozen deposits of each staker. Since we already have access to
their sum ([staked_frozen]) we only need to track the proportion
of this sum owned by each staker.
{1} Pseudo-tokens
The {!Storage.Contract.Frozen_deposits_pseudotokens} and
{!Storage.Contract.Staking_pseudotokens} tables are used to keep
track of this proportion. The amounts stored in these tables don't
have a fixed value in tez, they can be seen as shares of the total
frozen deposits of a delegate's stakers, we call them
pseudotokens. Pseudotokens are minted when a staker increases
its share using the stake pseudo-operation; they are burnt when a
staker decreases its share using the request-unstake
pseudo-operation. Events which modify uniformly the frozen
deposits of all the stakers of a delegate (reward distribution
and slashing) don't lead to minting nor burning any pseudotokens;
that's the main motivation for using these pseudotokens: thanks to
them we never need to iterate over the stakers of a delegate
(whose number is unbounded).
{1} Conversion rate:
The conversion rate between pseudotokens and mutez (the value in
mutez of a pseudotoken) should be given by the ratio between the
delegate's current staked frozen deposits and the total number
of pseudotokens of the delegate; it's actually the case when this
total number of pseudotokens is positive. When the total number of
pseudotokens of a delegate is null, the conversion rate could
theoretically have any value but extreme values are dangerous
because of overflows and loss of precision; for these reasons, we
use one as the conversion rate when the total number of
pseudotokens is null, which can happen in two situations:
- the first time a delegator stakes since the
migration which created the pseudotoken tables, and
- when stakers empty their delegate's staked frozen deposits and later
receive rewards.
{2} Implementation:
The {!Storage.Contract.Staking_pseudotokens} table stores for
each staker its {i staking balance pseudotokens} which is the
number of pseudotokens owned by the staker.
The {!Storage.Contract.Frozen_deposits_pseudotokens} table stores
for each delegate the {i frozen deposits pseudotokens} of the
delegate which is defined as the sum of all the staking balance
pseudotokens of its stakers.
For both tables, pseudotokens are represented using the
[Pseudotoken_repr.t] type which is, like [Tez_repr.t], stored on
non-negative signed int64.
{2} Invariants:
{3} Invariant 1: frozen deposits pseudotokens initialization
For {!Storage.Contract.Frozen_deposits_pseudotokens}, a missing
key is equivalent to a value of [0]. This case means that there are
no pseudotokens, the delegate has no staker, the conversion rate is [1].
{3} Invariant 2: staking balance pseudotokens initialization
For {!Storage.Contract.Staking_pseudotokens}, a missing key is
equivalent to a value of [0].
{3} Invariant 3: relationship between frozen deposits and staking
balance pseudotokens
For a given delegate, their frozen deposits pseudotokens equal
the sum of all staking pseudotokens of their delegators.
{3} Invariant 4: delegates have no staking pseudotokens.
*)(** When a delegate gets totally slashed, the value of its
pseudotokens becomes 0 and before minting any new token we would
need to iterate over all stakers to empty their pseudotoken
balances. We want to avoid iterating over stakers so we forbid
{b stake} in this case. *)typeerror+=Cannot_stake_on_fully_slashed_delegate(** These two types are not exported, they are views to the portions
of the storage which are relevant in this module when a delegate
or a staker are considered. *)typedelegate_balances={delegate:Signature.public_key_hash;frozen_deposits_staked_tez:Tez_repr.t;frozen_deposits_pseudotokens:Staking_pseudotoken_repr.t;}typedelegator_balances={delegator:Contract_repr.t;pseudotoken_balance:Staking_pseudotoken_repr.t;delegate_balances:delegate_balances;}(** {0} Functions reading from the storage *)(** [get_frozen_deposits_staked_tez ctxt ~delegate] returns the sum of frozen
deposits, in tez, of the delegate's stakers. *)letget_frozen_deposits_staked_tezctxt~delegate=letopenLwt_result_syntaxinlet+{staked_frozen;delegated=_;own_frozen=_}=Stake_storage.get_full_staking_balancectxtdelegateinstaked_frozenletget_own_frozen_depositsctxt~delegate=letopenLwt_result_syntaxinlet+{own_frozen;delegated=_;staked_frozen=_}=Stake_storage.get_full_staking_balancectxtdelegateinown_frozen(** [get_frozen_deposits_pseudotokens ctxt ~delegate] returns the total
number of pseudotokens in circulation for the given
[delegate]. This should, by invariant 3 be the sum of the
staking balance (in pseudotokens) of all its delegators.
To preserve invariant 1, this should be the only function of this
module reading from the
{!Storage.Contract.Frozen_deposits_pseudotokens} table. *)letget_frozen_deposits_pseudotokensctxt~delegate=letopenLwt_result_syntaxinlet+frozen_deposits_pseudotokens_opt=Storage.Contract.Frozen_deposits_pseudotokens.findctxt(Implicitdelegate)inOption.valuefrozen_deposits_pseudotokens_opt~default:Staking_pseudotoken_repr.zero(** [staking_pseudotokens_balance ctxt ~delegator] returns
[delegator]'s current staking balance in pseudotokens.
To preserve invariant 2, this should be the only function of this
module reading from the {!Storage.Contract.Staking_pseudotokens}
table.
*)letstaking_pseudotokens_balancectxt~delegator=letopenLwt_result_syntaxinlet+staking_pseudotokens_opt=Storage.Contract.Staking_pseudotokens.findctxtdelegatorinOption.value~default:Staking_pseudotoken_repr.zerostaking_pseudotokens_opt(** [get_delegate_balances ctxt ~delegate] records the staked frozen deposits
in tez and pseudotokens of a given delegate.
Postcondition:
delegate = result.delegate /\
get_frozen_deposits_staked_tez ctxt ~delegate = return result.frozen_deposits_staked_tez /\
get_frozen_deposits_pseudotokens ctxt ~delegate = return result.frozen_deposits_pseudotokens
*)letget_delegate_balancesctxt~delegate=letopenLwt_result_syntaxinlet*frozen_deposits_staked_tez=get_frozen_deposits_staked_tezctxt~delegateinlet+frozen_deposits_pseudotokens=get_frozen_deposits_pseudotokensctxt~delegatein{delegate;frozen_deposits_staked_tez;frozen_deposits_pseudotokens}(** [get_delegator_balances ctxt ~delegator ~delegate_balances] enriches
the [delegate_balances] with [delegator]'s pseudotoken balance.
Precondition:
unchecked: [delegator != delegate_balance.delegate] /\
unchecked: [delegator] delegates to [delegate_balance.delegate]
unchecked: get_delegate_balances ctxt ~delegate = return delegate_balances
Postcondition:
result.delegator = delegator /\
result.delegate_balances = delegate_balances /\
staking_pseudotokens_balance ctxt ~delegator = return result.pseudotoken_balance
*)letget_delegator_balancesctxt~delegator~delegate_balances=letopenLwt_result_syntaxinlet+pseudotoken_balance=staking_pseudotokens_balancectxt~delegatorin{delegator;pseudotoken_balance;delegate_balances}(** [mint_pseudotokens ctxt delegator_balances_before
pseudotokens_to_mint] mints [pseudotokens_to_mint] pseudotokens
and assign them to [delegator_balances_before.delegator]. Both
tables are updated to maintain invariant 3.
Precondition:
unchecked: get_delegator_balances ctxt delegator_balances_before.delegator = return delegator_balances_before /\
unchecked: invariant3(ctxt)
Postcondition:
get_delegator_balances ctxt delegator_balances_before.delegator =
return {delegator_balances_before with
pseudotoken_balance += pseudotokens_to_mint;
delegate_balances.frozen_deposits_pseudotokens += pseudotokens_to_mint} /\
invariant3(ctxt)
*)letmint_pseudotokensctxt(delegator_balances_before:delegator_balances)pseudotokens_to_mint=letopenLwt_result_syntaxinlet*?new_pseudotoken_balance=Staking_pseudotoken_repr.(delegator_balances_before.pseudotoken_balance+?pseudotokens_to_mint)inlet*?new_delegate_total_frozen_deposits_pseudotokens=Staking_pseudotoken_repr.(delegator_balances_before.delegate_balances.frozen_deposits_pseudotokens+?pseudotokens_to_mint)inlet*!ctxt=Storage.Contract.Staking_pseudotokens.addctxtdelegator_balances_before.delegatornew_pseudotoken_balanceinlet*!ctxt=Storage.Contract.Frozen_deposits_pseudotokens.addctxt(Implicitdelegator_balances_before.delegate_balances.delegate)new_delegate_total_frozen_deposits_pseudotokensinreturnctxt(** [burn_pseudotokens ctxt delegator_balances_before
pseudotokens_to_burn] burns [pseudotokens_to_burn] pseudotokens
from the balance of [delegator_balances_before.delegator]. Both
tables are updated to maintain invariant 3.
Precondition:
unchecked: get_delegator_balances ctxt delegator_balances_before.delegator = return delegator_balances_before /\
unchecked: invariant3(ctxt)
Postcondition:
get_delegator_balances ctxt delegator_balances_before.delegator =
return {delegator_balances_before with
pseudotoken_balance -= pseudotokens_to_mint;
delegate_balances.frozen_deposits_pseudotokens -= pseudotokens_to_mint} /\
invariant3(ctxt)
*)letburn_pseudotokensctxt(delegator_balances_before:delegator_balances)pseudotokens_to_burn=letopenLwt_result_syntaxinlet*?new_pseudotoken_balance=Staking_pseudotoken_repr.(delegator_balances_before.pseudotoken_balance-?pseudotokens_to_burn)inlet*?new_delegate_total_frozen_deposits_pseudotokens=Staking_pseudotoken_repr.(delegator_balances_before.delegate_balances.frozen_deposits_pseudotokens-?pseudotokens_to_burn)inlet*!ctxt=Storage.Contract.Staking_pseudotokens.addctxtdelegator_balances_before.delegatornew_pseudotoken_balanceinlet*!ctxt=Storage.Contract.Frozen_deposits_pseudotokens.addctxt(Implicitdelegator_balances_before.delegate_balances.delegate)new_delegate_total_frozen_deposits_pseudotokensinreturnctxt(** {0} Conversion between tez and pseudotokens *)(** Tez -> pseudotokens conversion.
Precondition:
tez_amount <> 0 /\
delegate_balances.frozen_deposits_pseudotokens <> 0 /\
delegate_balances.frozen_deposits_staked_tez <> 0.
Postcondition:
result <> 0.
*)letpseudotokens_of(delegate_balances:delegate_balances)tez_amount=assert(Staking_pseudotoken_repr.(delegate_balances.frozen_deposits_pseudotokens<>zero));assert(Tez_repr.(delegate_balances.frozen_deposits_staked_tez<>zero));assert(Tez_repr.(tez_amount<>zero));letfrozen_deposits_staked_tez_z=Z.of_int64(Tez_repr.to_mutezdelegate_balances.frozen_deposits_staked_tez)inletfrozen_deposits_pseudotokens_z=Staking_pseudotoken_repr.to_zdelegate_balances.frozen_deposits_pseudotokensinlettez_amount_z=Z.of_int64(Tez_repr.to_muteztez_amount)inletres_z=Z.div(Z.multez_amount_zfrozen_deposits_pseudotokens_z)frozen_deposits_staked_tez_zinStaking_pseudotoken_repr.of_z_exnres_z(** Pseudotokens -> tez conversion.
Precondition:
delegate_balances.frozen_deposits_pseudotokens <> 0.
*)lettez_of(delegate_balances:delegate_balances)pseudotoken_amount=assert(Staking_pseudotoken_repr.(delegate_balances.frozen_deposits_pseudotokens<>zero));letfrozen_deposits_staked_tez_z=Z.of_int64(Tez_repr.to_mutezdelegate_balances.frozen_deposits_staked_tez)inletfrozen_deposits_pseudotokens_z=Staking_pseudotoken_repr.to_zdelegate_balances.frozen_deposits_pseudotokensinletpseudotoken_amount_z=Staking_pseudotoken_repr.to_zpseudotoken_amountinletres_z=Z.div(Z.mulfrozen_deposits_staked_tez_zpseudotoken_amount_z)frozen_deposits_pseudotokens_zinTez_repr.of_mutez_exn(Z.to_int64res_z)(** [compute_pseudotoken_credit_for_tez_amount delegate_balances
tez_amount] is a safe wrapper around [pseudotokens_of
delegate_balances tez_amount].
*)letcompute_pseudotoken_credit_for_tez_amountdelegate_balancestez_amount=letopenResult_syntaxinifTez_repr.(tez_amount=zero)then(* This is dead code because Apply.apply_stake already forbids the
amount=0 case. We keep this dead code here to avoid putting too
many preconditions on the usage of this module. *)returnStaking_pseudotoken_repr.zeroelseifStaking_pseudotoken_repr.(delegate_balances.frozen_deposits_pseudotokens=zero)then(* Pseudotokens are not yet initialized, the conversion rate is
1. *)return@@Staking_pseudotoken_repr.init_of_teztez_amountelseifTez_repr.(delegate_balances.frozen_deposits_staked_tez=zero)then(* Can only happen in an attempt to stake after a full
slashing. We forbid this case to avoid having to iterate over
all stakers to reset their pseudotoken balances. *)tzfailCannot_stake_on_fully_slashed_delegateelsereturn@@pseudotokens_ofdelegate_balancestez_amountletstakectxt~delegator~delegatetez_amount=letopenLwt_result_syntaxinlet*delegate_balances=get_delegate_balancesctxt~delegateinlet*?pseudotokens_to_credit=compute_pseudotoken_credit_for_tez_amountdelegate_balancestez_amountinlet*delegator_balances=get_delegator_balancesctxt~delegator~delegate_balancesinmint_pseudotokensctxtdelegator_balancespseudotokens_to_credit(** {0} Exported functions, see the mli file. *)letstakectxt~contract~delegatetez_amount=ifContract_repr.(contract=Implicitdelegate)then(* No pseudotokens for delegates. *)Lwt_result_syntax.returnctxtelsestakectxt~delegator:contract~delegatetez_amountletrequest_unstakectxt~delegator~delegaterequested_amount=letopenLwt_result_syntaxinlet*delegate_balances=get_delegate_balancesctxt~delegateinifTez_repr.(delegate_balances.frozen_deposits_staked_tez=zero)thenreturn(ctxt,Tez_repr.zero)elselet*delegator_balances=get_delegator_balancesctxt~delegator~delegate_balancesinifStaking_pseudotoken_repr.(delegator_balances.pseudotoken_balance=zero)thenreturn(ctxt,Tez_repr.zero)else(assert(Staking_pseudotoken_repr.(delegate_balances.frozen_deposits_pseudotokens<>zero));letpseudotokens_to_unstake,tez_to_unstake=ifTez_repr.(requested_amount>=delegate_balances.frozen_deposits_staked_tez)then(* definitely a full unstake, make sure we can empty the staking
balance *)(delegator_balances.pseudotoken_balance,ifStaking_pseudotoken_repr.(delegate_balances.frozen_deposits_pseudotokens=delegator_balances.pseudotoken_balance)then(* ...and the frozen deposits if from last staker *)delegate_balances.frozen_deposits_staked_tezelsetez_ofdelegate_balancesdelegator_balances.pseudotoken_balance)elseletrequested_pseudotokens=pseudotokens_ofdelegate_balancesrequested_amountinassert(Staking_pseudotoken_repr.(requested_pseudotokens<>zero));(* by postcondition of pseudotokens_of *)ifStaking_pseudotoken_repr.(requested_pseudotokens<delegator_balances.pseudotoken_balance)then(requested_pseudotokens,requested_amount)else(delegator_balances.pseudotoken_balance,tez_ofdelegate_balancesdelegator_balances.pseudotoken_balance)inlet+ctxt=burn_pseudotokensctxtdelegator_balancespseudotokens_to_unstakein(ctxt,tez_to_unstake))letrequest_unstakectxt~contract~delegaterequested_amount=letopenLwt_result_syntaxinifTez_repr.(requested_amount=zero)thenreturn(ctxt,Tez_repr.zero)elseifContract_repr.(contract=Implicitdelegate)thenlet+delegate_own_frozen_deposits=get_own_frozen_depositsctxt~delegatein(ctxt,Tez_repr.mindelegate_own_frozen_depositsrequested_amount)elserequest_unstakectxt~delegator:contract~delegaterequested_amountmoduleFor_RPC=structletstaked_balancectxt~delegator~delegate=letopenLwt_result_syntaxinlet*delegate_balances=get_delegate_balancesctxt~delegateinlet*delegator_balances=get_delegator_balancesctxt~delegator~delegate_balancesinifStaking_pseudotoken_repr.(delegate_balances.frozen_deposits_pseudotokens<>zero)thenreturn@@tez_ofdelegate_balancesdelegator_balances.pseudotoken_balanceelse(assert(Staking_pseudotoken_repr.(delegator_balances.pseudotoken_balance=zero));returnTez_repr.zero)letstaked_balancectxt~contract~delegate=ifContract_repr.(contract=Implicitdelegate)thenget_own_frozen_depositsctxt~delegateelsestaked_balancectxt~delegator:contract~delegateend