10 KiB
Fee Distribution
Overview
Fees are pooled separately and withdrawn lazily, at any time. They are not bonded, and can be paid in multiple tokens. An adjustment factor is maintained for each validator and delegator to determine the true proportion of fees in the pool they are entitled too. Adjustment factors are updated every time a validator or delegator's voting power changes. Validators and delegators must withdraw all fees they are entitled too before they can bond or unbond Atoms.
Affect on Staking
Because fees are optimized to note
Commission on Atom Provisions and having atoms autobonded are mutually exclusive (we can’t have both). The reason for this is that if there are atoms commissions and autobonding, the portion of atoms the fee distribution calculation would become very large as the atom portion for each delegator would change each block making a withdrawal of fees for a delegator require a calculation for every single block since the last withdrawal. Conclusion we can only have atom commission and unbonded atoms provisions, or bonded atom provisions and no atom commission
Fee Calculations
Collected fees are pooled globally and divided out passively to validators and delegators. Each validator has the opportunity to charge commission to the delegators on the fees collected on behalf of the delegators by the validators. Fees are paid directly into a global fee pool. Due to the nature of of passive accounting whenever changes to parameters which affect the rate of fee distribution occurs, withdrawal of fees must also occur.
- when withdrawing one must withdrawal the maximum amount they are entitled too, leaving nothing in the pool,
- when bonding, unbonding, or re-delegating tokens to an existing account a full withdrawal of the fees must occur (as the rules for lazy accounting change),
- when a validator chooses to change the commission on fees, all accumulated commission fees must be simultaneously withdrawn.
When the validator is the proposer of the round, that validator (and their
delegators) receives between 1% and 5% of fee rewards, the reserve tax is then
charged, then the remainder is distributed socially by voting power to all
validators including the proposer validator. The amount of proposer reward is
calculated from pre-commits Tendermint messages. All provision rewards are
added to a provision reward pool which validator holds individually. Here note
that BondedShares
represents the sum of all voting power saved in the
GlobalState
(denoted gs
).
proposerReward = feesCollected * (0.01 + 0.04
* sumOfVotingPowerOfPrecommitValidators / gs.BondedShares)
validator.ProposerRewardPool += proposerReward
reserveTaxed = feesCollected * params.ReserveTax
gs.ReservePool += reserveTaxed
distributedReward = feesCollected - proposerReward - reserveTaxed
gs.FeePool += distributedReward
gs.SumFeesReceived += distributedReward
gs.RecentFee = distributedReward
The entitlement to the fee pool held by the each validator can be accounted for
lazily. First we must account for a validator's count
and adjustment
. The
count
represents a lazy accounting of what that validators entitlement to the
fee pool would be if there VotingPower
was to never change and they were to
never withdraw fees.
validator.count = validator.VotingPower * BlockHeight
Similarly the GlobalState count can be passively calculated whenever needed,
where BondedShares
is the updated sum of voting powers from all validators.
gs.count = gs.BondedShares * BlockHeight
The adjustment
term accounts for changes in voting power and withdrawals of
fees. The adjustment factor must be persisted with the validator and modified
whenever fees are withdrawn from the validator or the voting power of the
validator changes. When the voting power of the validator changes the
Adjustment
factor is increased/decreased by the cumulative difference in the
voting power if the voting power has been the new voting power as opposed to
the old voting power for the entire duration of the blockchain up the previous
block. Each time there is an adjustment change the GlobalState (denoted gs
)
Adjustment
must also be updated.
simplePool = validator.count / gs.count * gs.SumFeesReceived
projectedPool = validator.PrevPower * (height-1)
/ (gs.PrevPower * (height-1)) * gs.PrevFeesReceived
+ validator.Power / gs.Power * gs.RecentFee
AdjustmentChange = simplePool - projectedPool
validator.AdjustmentRewardPool += AdjustmentChange
gs.Adjustment += AdjustmentChange
Every instance that the voting power changes, information about the state of
the validator set during the change must be recorded as a powerChange
for
other validators to run through. Before any validator modifies its voting power
it must first run through the above calculation to determine the change in
their caandidate.AdjustmentRewardPool
for all historical changes in the set
of powerChange
which they have not yet synced to. The set of all
powerChange
may be trimmed from its oldest members once all validators have
synced past the height of the oldest powerChange
. This trim procedure will
occur on an epoch basis.
type powerChange struct {
height int64 // block height at change
power rational.Rat // total power at change
prevpower rational.Rat // total power at previous height-1
feesin coins.Coin // fees in at block height
prevFeePool coins.Coin // total fees in at previous block height
}
Note that the adjustment factor may result as negative if the voting power of a different validator has decreased.
validator.AdjustmentRewardPool += withdrawn
gs.Adjustment += withdrawn
Now the entitled fee pool of each validator can be lazily accounted for at any given block:
validator.feePool = validator.simplePool - validator.Adjustment
So far we have covered two sources fees which can be withdrawn from: Fees from
proposer rewards (validator.ProposerRewardPool
), and fees from the fee pool
(validator.feePool
). However we should note that all fees from fee pool are
subject to commission rate from the owner of the validator. These next
calculations outline the math behind withdrawing fee rewards as either a
delegator to a validator providing commission, or as the owner of a validator
who is receiving commission.
Calculations For Delegators and Validators
The same mechanism described to calculate the fees which an entire validator is
entitled to is be applied to delegator level to determine the entitled fees for
each delegator and the validators entitled commission from gs.FeesPool
and
validator.ProposerRewardPool
.
The calculations are identical with a few modifications to the parameters:
- Delegator's entitlement to
gs.FeePool
:- entitled party voting power should be taken as the effective voting power
after commission is retrieved,
bond.Shares/validator.TotalDelegatorShares * validator.VotingPower * (1 - validator.Commission)
- entitled party voting power should be taken as the effective voting power
after commission is retrieved,
- Delegator's entitlement to
validator.ProposerFeePool
- global power in this context is actually shares
validator.TotalDelegatorShares
- entitled party voting power should be taken as the effective shares after
commission is retrieved,
bond.Shares * (1 - validator.Commission)
- global power in this context is actually shares
- Validator's commission entitlement to
gs.FeePool
- entitled party voting power should be taken as the effective voting power
of commission portion of total voting power,
validator.VotingPower * validator.Commission
- entitled party voting power should be taken as the effective voting power
of commission portion of total voting power,
- Validator's commission entitlement to
validator.ProposerFeePool
- global power in this context is actually shares
validator.TotalDelegatorShares
- entitled party voting power should be taken as the of commission portion
of total delegators shares,
validator.TotalDelegatorShares * validator.Commission
- global power in this context is actually shares
For more implementation ideas see spreadsheet spec/AbsoluteFeeDistrModel.xlsx
As mentioned earlier, every time the voting power of a delegator bond is changing either by unbonding or further bonding, all fees must be simultaneously withdrawn. Similarly if the validator changes the commission rate, all commission on fees must be simultaneously withdrawn.
Other general notes on fees accounting
- When a delegator chooses to re-delegate shares, fees continue to accumulate until the re-delegation queue reaches maturity. At the block which the queue reaches maturity and shares are re-delegated all available fees are simultaneously withdrawn.
- Whenever a totally new validator is added to the validator set, the
accum
of the entire validator must be 0, meaning that the initial value forvalidator.Adjustment
must be set to the value ofcanidate.Count
for the height which the validator is added on the validator set. - The feePool of a new delegator bond will be 0 for the height at which the bond
was added. This is achieved by setting
DelegatorBond.FeeWithdrawalHeight
to the height which the bond was added.
Atom provisions
Validator provisions are minted on an hourly basis (the first block of a new hour). The annual target of between 7% and 20%. The long-term target ratio of bonded tokens to unbonded tokens is 67%.
The target annual inflation rate is recalculated for each provisions cycle. The inflation is also subject to a rate change (positive or negative) depending on the distance from the desired ratio (67%). The maximum rate change possible is defined to be 13% per year, however the annual inflation is capped as between 7% and 20%.
inflationRateChange(0) = 0
Inflation(0) = 0.07
bondedRatio = Pool.BondedTokens / Pool.TotalSupplyTokens
AnnualInflationRateChange = (1 - bondedRatio / 0.67) * 0.13
annualInflation += AnnualInflationRateChange
if annualInflation > 0.20 then Inflation = 0.20
if annualInflation < 0.07 then Inflation = 0.07
provisionTokensHourly = Pool.TotalSupplyTokens * Inflation / (365.25*24)
Because the validators hold a relative bonded share (GlobalStakeShares
), when
more bonded tokens are added proportionally to all validators, the only term
which needs to be updated is the GlobalState.BondedPool
. So for each
provisions cycle:
Pool.BondedPool += provisionTokensHourly