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 fee_collector module account by default, or to the PoA module account if configured (see Fee Routing Setup)
  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 PoA 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):
  • PoA module account 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 the PoA module account 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

Fee Routing Setup

PoA has its own module account for collecting fees. Enabling the PoA module account is recommended to keep fee accounting isolated and accurate. If not enabled, fees are deposited into the standard fee_collector account by default. To enable the PoA module account, two wiring changes are required:

1. Register the PoA Module Account

Register poatypes.ModuleName in the maccPerms map passed to authkeeper.NewAccountKeeper:
app.AccountKeeper = authkeeper.NewAccountKeeper(
    appCodec,
    runtime.NewKVStoreService(storeKeys[authtypes.StoreKey]),
    authtypes.ProtoBaseAccount,
    map[string][]string{
        authtypes.FeeCollectorName: nil,
        govtypes.ModuleName:        {authtypes.Burner, authtypes.Staking},
        poatypes.ModuleName:        nil, // register PoA module account
    },
    // ...
)
Source: simapp/app.go

2. Configure the Ante Handler

Use WithFeeRecipientModule on NewDeductFeeDecorator to route fees to the PoA module account:
anteDecorators := []sdk.AnteDecorator{
    ante.NewSetUpContextDecorator(),
    ante.NewExtensionOptionsDecorator(options.ExtensionOptionChecker),
    ante.NewValidateBasicDecorator(),
    ante.NewTxTimeoutHeightDecorator(),
    ante.NewValidateMemoDecorator(options.AccountKeeper),
    ante.NewConsumeGasForTxSizeDecorator(options.AccountKeeper),
    ante.NewDeductFeeDecorator(options.AccountKeeper, options.BankKeeper, options.FeegrantKeeper, options.TxFeeChecker).
        WithFeeRecipientModule(poatypes.ModuleName), // redirect fees to PoA module account
    ante.NewSetPubKeyDecorator(options.AccountKeeper),
    ante.NewValidateSigCountDecorator(options.AccountKeeper),
    ante.NewSigGasConsumeDecorator(options.AccountKeeper, options.SigGasConsumer),
    ante.NewSigVerificationDecorator(options.AccountKeeper, options.SignModeHandler, options.SigVerifyOptions...),
    ante.NewIncrementSequenceDecorator(options.AccountKeeper),
}
Source: simapp/ante.go WithFeeRecipientModule is backwards compatible — omitting it defaults to the standard fee_collector behavior.

Security Considerations

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