feat(x/provider): withdraw funds from consumer pool address at each block#10
feat(x/provider): withdraw funds from consumer pool address at each block#10
Conversation
…e-payment # Conflicts: # x/vaas/provider/types/params.go
tbruyelle
left a comment
There was a problem hiding this comment.
Some small comments but overall LGTM, thanks !
…e-payment # Conflicts: # x/vaas/provider/types/provider.pb.go
There was a problem hiding this comment.
Pull request overview
Implements per-block fee withdrawal from consumer funding accounts and distribution to provider validators, adding a new fees_per_block parameter to configure the charge (Issue #9).
Changes:
- Add
fees_per_blockto provider params (proto + Go types + validation) and update related tests. - Add per-block fee collection from consumers and proportional distribution to bonded validators.
- Extend expected keeper interfaces + mocks for new staking/bank methods used by fee logic.
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| x/vaas/types/expected_keepers.go | Extends staking/bank keeper interfaces for total power, address codec, and module→account sends. |
| proto/vaas/provider/v1/provider.proto | Adds fees_per_block param to proto schema. |
| x/vaas/provider/types/provider.pb.go | Regenerated protobuf output reflecting new params field. |
| x/vaas/provider/types/params.go | Adds default fees, wires param through constructors/defaults, and validates FeesPerBlock. |
| x/vaas/provider/types/params_test.go | Updates params validation tests to include fees and adds a zero-fee invalid case. |
| x/vaas/provider/types/genesis_test.go | Updates genesis tests to pass the new param. |
| x/vaas/provider/module.go | Calls fee collection + distribution in BeginBlock. |
| x/vaas/provider/keeper/params.go | Exposes GetFeesPerBlock from keeper params. |
| x/vaas/provider/keeper/params_test.go | Updates keeper params test to include fees. |
| x/vaas/provider/keeper/fees.go | New keeper logic to collect fees from consumers and distribute to validators. |
| x/vaas/provider/keeper/fees_test.go | New unit tests for fee collection and distribution. |
| testutil/keeper/mocks.go | Regenerates mocks for newly added expected keeper methods. |
| .gitignore | Formatting-only change. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 13 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| consumerModuleName := fmt.Sprintf("consumer-%s", consumerId) | ||
| feeCoins := sdk.NewCoins(feesPerBlock) | ||
| if err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, consumerModuleName, k.feeCollectorName, feeCoins); err != nil { | ||
| return totalFeesCollected, fmt.Errorf("failed to collect fees from consumer (%s): %w", consumerId, err) | ||
| } | ||
|
|
There was a problem hiding this comment.
The code attempts to send coins from a module account named "consumer-" (e.g., "consumer-0", "consumer-1"), but there is no visible code in this PR that creates these module accounts or registers them with the auth keeper's module account permissions. Module accounts must be explicitly registered in the maccPerms map during app initialization. Without proper registration, SendCoinsFromModuleToModule will fail at runtime. You should either add these module accounts to the maccPerms map in app/provider/app.go, or use a different approach that doesn't require dynamically named module accounts.
| consumerModuleName := fmt.Sprintf("consumer-%s", consumerId) | |
| feeCoins := sdk.NewCoins(feesPerBlock) | |
| if err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, consumerModuleName, k.feeCollectorName, feeCoins); err != nil { | |
| return totalFeesCollected, fmt.Errorf("failed to collect fees from consumer (%s): %w", consumerId, err) | |
| } | |
| feeCoins := sdk.NewCoins(feesPerBlock) | |
| if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, consumerModuleAccAddr, k.feeCollectorName, feeCoins); err != nil { | |
| return totalFeesCollected, fmt.Errorf("failed to collect fees from consumer (%s): %w", consumerId, err) | |
| } |
| // Send coins from fee_collector to validator account | ||
| coins := sdk.NewCoins(sdk.NewCoin(totalFees.Denom, valShare)) | ||
| if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, k.feeCollectorName, valAddr, coins); err != nil { | ||
| return fmt.Errorf("failed to send fees to validator %s: %w", val.GetOperator(), err) | ||
| } |
There was a problem hiding this comment.
The fee distribution logic sends coins directly from the fee collector to validator operator addresses. This bypasses the standard staking module's delegation distribution mechanism, which means that delegators will not receive their share of these fees. In typical Cosmos SDK chains, fees collected in the fee_collector module account are distributed through the distribution module (x/distribution), which ensures that both validators and their delegators receive rewards proportionally. Consider using the distribution module's AllocateTokensToValidator or a similar mechanism to ensure proper reward distribution to delegators.
| // Send coins from fee_collector to validator account | |
| coins := sdk.NewCoins(sdk.NewCoin(totalFees.Denom, valShare)) | |
| if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, k.feeCollectorName, valAddr, coins); err != nil { | |
| return fmt.Errorf("failed to send fees to validator %s: %w", val.GetOperator(), err) | |
| } | |
| // Allocate fees to validator via distribution module so delegators also receive rewards. | |
| decCoins := sdk.NewDecCoinsFromCoins(sdk.NewCoin(totalFees.Denom, valShare)) | |
| k.distributionKeeper.AllocateTokensToValidator(ctx, val, decCoins) |
| // Calculate validator's share: (validatorPower / totalPower) * totalFees | ||
| powerReduction := k.stakingKeeper.PowerReduction(ctx) | ||
| valPower := math.NewInt(val.GetConsensusPower(powerReduction)) | ||
| valShare := totalFees.Amount.Mul(valPower).Quo(totalPower) | ||
|
|
||
| // If this is the last validator, assign the remainder to ensure all fees are distributed | ||
| if i == len(validators)-1 { | ||
| valShare = totalFees.Amount.Sub(distributedAmount) | ||
| } |
There was a problem hiding this comment.
When totalFees is zero or very small and there are many validators, the integer division in line 105 may result in all validators receiving zero tokens due to rounding, even though totalFees is non-zero. The remainder-to-last-validator logic on lines 108-110 only helps if at least one validator gets a non-zero share. Consider adding a check to skip distribution if totalFees.IsZero() or if totalFees is too small to meaningfully distribute.
| sdkCtx.Logger().Error("failed to collect fees from consumers", "err", err) | ||
| } else { | ||
| // Distribute collected fees to validators. Failures here should also not halt block processing. | ||
| if err := am.keeper.DistributeFeesToValidators(sdkCtx, collectedFees); err != nil { | ||
| sdkCtx.Logger().Error("failed to distribute collected fees to validators", "err", err) |
There was a problem hiding this comment.
The error messages logged when fee collection or distribution fails provide limited context about the state of the system. Consider adding more diagnostic information such as the number of consumers processed, the number of successful collections, and the total amount of fees that failed to be collected or distributed. This would aid in debugging and operational monitoring.
| sdkCtx.Logger().Error("failed to collect fees from consumers", "err", err) | |
| } else { | |
| // Distribute collected fees to validators. Failures here should also not halt block processing. | |
| if err := am.keeper.DistributeFeesToValidators(sdkCtx, collectedFees); err != nil { | |
| sdkCtx.Logger().Error("failed to distribute collected fees to validators", "err", err) | |
| sdkCtx.Logger().Error( | |
| "failed to collect fees from consumers", | |
| "err", err, | |
| "height", sdkCtx.BlockHeight(), | |
| ) | |
| } else { | |
| // Distribute collected fees to validators. Failures here should also not halt block processing. | |
| if err := am.keeper.DistributeFeesToValidators(sdkCtx, collectedFees); err != nil { | |
| sdkCtx.Logger().Error( | |
| "failed to distribute collected fees to validators", | |
| "err", err, | |
| "height", sdkCtx.BlockHeight(), | |
| "collected_fees", collectedFees.String(), | |
| ) |
close #9