Skip to main content

Fee Distribution

Overview

The PoA module implements a custom fee distribution mechanism based on validator power. Unlike the standard Cosmos SDK x/distribution module, PoA uses a checkpoint-based system to allocate fees proportionally to validators without automatic distribution.

How Fees Accumulate

Fees flow through the PoA system differently than standard Cosmos SDK:
  1. Block Fees: Transaction fees collected in each block go to the standard fee_collector module account
  2. Checkpoint System: Allocated fees are updated for validators when:
    • Any validator power changes
    • Any validator withdraws fees
Why Checkpointing?: Ensures fair distribution when power changes. If power changes mid-period, fees are allocated based on old power distribution before the change takes effect. Location: x/poa/keeper/distribution.go:18

Distribution Algorithm

Checkpoint-Based Allocation

The PoA module uses a checkpoint system to allocate fees fairly when validator power changes. Rather than distributing fees actively at every block, allocation efficiently happens at discrete checkpoints. Checkpoint Triggers:
  • Any validator power change (via MsgUpdateValidators)
  • Any fee withdrawal (via MsgWithdrawFees)
Unallocated Fees Calculation: At checkpoint time tt, calculate unallocated fees: Ut=Bcollector(t)Atotal(t)U_t = B_{collector}(t) - A_{total}(t) Where:
  • UtU_t = unallocated fees at checkpoint tt
  • Bcollector(t)B_{collector}(t) = current balance in the fee collector module account
  • Atotal(t)=i=1nFi(t)A_{total}(t) = \sum_{i=1}^{n} F_i(t) = sum of all previously- allocated fees across all validators (0 if no checkpoints have been done)
Proportional Share Allocation: For each active validator ii (where Pi(t)>0P_i(t) > 0), allocate a share proportional to their power: Si(t)=Ut×Pi(t)Ptotal(t)S_i(t) = U_t \times \frac{P_i(t)}{P_{total}(t)} Where:
  • Si(t)S_i(t) = share allocated to validator ii at checkpoint tt
  • Pi(t)P_i(t) = voting power of validator ii at checkpoint tt
  • Ptotal(t)=j=1nPj(t)P_{total}(t) = \sum_{j=1}^{n} P_j(t) = sum of all validator powers
Accumulated Fees Update: After allocation, update each validator’s accumulated fees: Fi(t+1)=Fi(t)+Si(t)F_i(t+1) = F_i(t) + S_i(t) Where:
  • Fi(t)F_i(t) = validator ii‘s accumulated fees before checkpoint
  • Fi(t+1)F_i(t+1) = validator ii‘s accumulated fees after checkpoint
  • Si(t)S_i(t) = share allocated in this checkpoint
Total Allocated Tracking: Update the global allocated tracker: Atotal(t+1)=Atotal(t)+UtA_{total}(t+1) = A_{total}(t) + U_t After this checkpoint, Atotal(t+1)=Bcollector(t)A_{total}(t+1) = B_{collector}(t) (all fees are now allocated).

Example Checkpoint Sequence

Initial State (before checkpoint):
  • Fee collector balance: Bcollector=1000B_{collector} = 1000 tokens
  • Total allocated: Atotal=400A_{total} = 400 tokens (from previous checkpoints)
  • Validator A: PA=50P_A = 50, FA=200F_A = 200 tokens allocated
  • Validator B: PB=50P_B = 50, FB=200F_B = 200 tokens allocated
  • Total power: Ptotal=100P_{total} = 100
Admin Action: Admin submits MsgUpdateValidators to change power distribution to 30/70 Checkpoint Triggered (before power change takes effect):
  1. Calculate unallocated: U=1000400=600U = 1000 - 400 = 600 tokens
  2. Allocate shares based on current power (50/50):
    • Validator A: SA=600×50100=300S_A = 600 \times \frac{50}{100} = 300 tokens
    • Validator B: SB=600×50100=300S_B = 600 \times \frac{50}{100} = 300 tokens
  3. Update accumulated fees:
    • Validator A: FA=200+300=500F_A = 200 + 300 = 500 tokens
    • Validator B: FB=200+300=500F_B = 200 + 300 = 500 tokens
  4. Update total allocated: Atotal=400+600=1000A_{total} = 400 + 600 = 1000 tokens
After Checkpoint - Power Change Applied:
  • Validator A: PA=30P_A = 30 (new power for future blocks)
  • Validator B: PB=70P_B = 70 (new power for future blocks)
  • All 1000 tokens now allocated (Atotal=BcollectorA_{total} = B_{collector})
  • Each validator has updated FiF_i available for withdrawal
Why This Matters: Validator A earned 300 tokens (50% share) based on their power during the period when those fees were collected. After the checkpoint, their power drops to 30%, so future fees will be split 30/70. Checkpointing ensures validators are rewarded based on the work they actually performed. Precision: Uses DecCoins (decimal coins) to prevent rounding dust accumulation. Each validator tracks fractional amounts that are too small to withdraw.

Withdrawing Fees

MsgWithdrawFees (x/poa/keeper/msg_server.go:91) Any validator operator can withdraw accumulated fees:
  1. Submit Withdrawal: Signed by operator address
  2. Checkpoint: System checkpoints all validators first (allocates any pending fees)
  3. Truncate: Decimal coins truncated to whole coins
  4. Transfer: Coins transferred from fee_collector to operator address
  5. Update Tracking: Total allocated decreases by withdrawn amount
  6. Remainder: Decimal remainder stays in validator’s allocated balance
Example:
Validator has: 100.7543 utokens allocated
Withdrawal:    100 utokens transferred to operator
Remainder:     0.7543 utokens remain allocated (less than least significant utoken digit)
Location: x/poa/keeper/distribution.go:106

Withdrawal Formula

When validator ii withdraws fees: Wi=FiW_i = \lfloor F_i \rfloor Fi=FiWiF_i' = F_i - W_i Atotal=AtotalWiA_{total}' = A_{total} - W_i Where:
  • WiW_i = amount withdrawn (truncated to integer coins)
  • FiF_i = validator’s allocated fees before withdrawal
  • FiF_i' = validator’s allocated fees after withdrawal (decimal remainder)
  • Fi\lfloor F_i \rfloor = floor function (truncate decimals)
  • AtotalA_{total}' = updated total allocated across all validators

Security Considerations

  1. Decimal Precision:
    • Uses DecCoins to prevent dust accumulation
    • Validators track fractional amounts
    • Remainders preserved across withdrawals
    • Prevents rounding errors from accumulating