Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ provider-start: build-apps
$(providerd) config set client chain-id provider-localnet
$(providerd) config set client keyring-backend test
$(providerd) keys add val
$(providerd) genesis add-genesis-account val 1000000000000uatone
$(providerd) genesis add-genesis-account val 1000000000000uatone,1000000000000uphoton
$(providerd) keys add user
$(providerd) genesis add-genesis-account user 1000000000uatone
$(providerd) genesis gentx val 1000000000uatone
Expand Down Expand Up @@ -167,7 +167,18 @@ consumer-start: consumer-init consumer-create
$(MAKE) consumer-genesis CONSUMER_ID=0
$(MAKE) consumer-run

.PHONY: consumer-init consumer-create consumer-genesis consumer-run consumer-start
# Fund a consumer's fee pool so the provider can collect its per-block fees and
# the consumer stays out of debt. The fee denom is uphoton (fixed at module
# wiring); FEE_POOL_AMOUNT must clear the min-deposit floor
# (fees_per_block_amount x min_deposit_blocks). Run after the consumer exists,
# e.g. make provider-fund-consumer-fee-pool CONSUMER_ID=0
FEE_POOL_AMOUNT ?= 100000000uphoton
provider-fund-consumer-fee-pool:
$(providerd) tx provider fund-consumer-fee-pool $(CONSUMER_ID) $(FEE_POOL_AMOUNT) \
--from val --keyring-backend test --chain-id provider-localnet \
--gas auto --gas-adjustment 1.5 --fees 10000uatone -y

.PHONY: consumer-init consumer-create consumer-genesis consumer-run consumer-start provider-fund-consumer-fee-pool

###############################################################################
### TS Relayer ###
Expand Down
2 changes: 2 additions & 0 deletions app/provider/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,8 @@ func New(
authcodec.NewBech32Codec(sdk.GetConfig().GetBech32ValidatorAddrPrefix()),
authcodec.NewBech32Codec(sdk.GetConfig().GetBech32ConsensusAddrPrefix()),
authtypes.FeeCollectorName,
// Per-block consumer fee denom, fixed for the lifetime of the binary.
providertypes.DefaultFeesPerBlockDenom,
)
app.BankKeeper.AppendSendRestriction(app.ProviderKeeper.FeePoolSendRestriction())

Expand Down
15 changes: 8 additions & 7 deletions proto/vaas/provider/v1/provider.proto
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ syntax = "proto3";
package vaas.provider.v1;

import "amino/amino.proto";
import "cosmos/base/v1beta1/coin.proto";
import "cosmos_proto/cosmos.proto";
import "gogoproto/gogo.proto";
import "cosmos/evidence/v1beta1/evidence.proto";
Expand Down Expand Up @@ -38,12 +37,14 @@ message Params {
// to the consensus engine on the provider.
int64 max_provider_consensus_validators = 4;

// The fee charged per block for consumer chain operation.
cosmos.base.v1beta1.Coin fees_per_block = 5 [
(gogoproto.casttype) = "github.com/cosmos/cosmos-sdk/types.Coin",
(amino.encoding) = "legacy_coin",
(amino.dont_omitempty) = true,
(gogoproto.nullable) = false
// The per-block fee amount charged for consumer chain operation. The denom
// is not a parameter: it is fixed at module wiring (see Keeper.feeDenom) and
// cannot be changed without a binary upgrade.
string fees_per_block_amount = 5 [
(cosmos_proto.scalar) = "cosmos.Int",
(gogoproto.customtype) = "cosmossdk.io/math.Int",
(gogoproto.nullable) = false,
(amino.dont_omitempty) = true
];

// Minimum deposit on MsgFundConsumerFeePool, expressed as a multiplier of
Expand Down
8 changes: 5 additions & 3 deletions proto/vaas/provider/v1/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ service Query {
}

// QueryConsumerFeesPerBlock returns the effective per-block fee for a
// consumer chain: the override if one is set, else Params.FeesPerBlock.
// consumer chain: the override amount if one is set, else
// Params.FeesPerBlockAmount, carried at the module's wired fee denom.
rpc QueryConsumerFeesPerBlock(QueryConsumerFeesPerBlockRequest)
returns (QueryConsumerFeesPerBlockResponse) {
option (google.api.http).get =
Expand Down Expand Up @@ -304,8 +305,9 @@ message QueryConsumerFeesPerBlockRequest {
uint64 consumer_id = 1;
}

// Effective fee for the consumer: the override if one is set, else
// Params.FeesPerBlock. is_override distinguishes the two cases.
// Effective fee for the consumer (a resolved coin at the module's wired fee
// denom): the override amount if one is set, else Params.FeesPerBlockAmount.
// is_override distinguishes the two cases.
message QueryConsumerFeesPerBlockResponse {
cosmos.base.v1beta1.Coin fees_per_block = 1 [
(gogoproto.casttype) = "github.com/cosmos/cosmos-sdk/types.Coin",
Expand Down
6 changes: 3 additions & 3 deletions proto/vaas/provider/v1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -174,12 +174,12 @@ message MsgSetConsumerFeesPerBlock {
// consumer_id is the internal numeric id of the target consumer.
uint64 consumer_id = 2;
// amount is the per-block fee charged to the consumer, expressed in the
// module's fee denom (Params.FeesPerBlock.Denom).
// module's fee denom (fixed at module wiring).
//
// Empty string clears any existing override; the consumer reverts to
// Params.FeesPerBlock.Amount. A non-empty value must parse as a
// Params.FeesPerBlockAmount. A non-empty value must parse as a
// cosmossdk.io/math.Int and must be strictly greater than the module-wide
// Params.FeesPerBlock.Amount, which acts as a floor: an override may only
// Params.FeesPerBlockAmount, which acts as a floor: an override may only
// raise a consumer's per-block fee above the global default, never lower it.
string amount = 3 [(cosmos_proto.scalar) = "cosmos.Int"];
}
Expand Down
1 change: 1 addition & 0 deletions tests/e2e/chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const (
providerHomePath = "/home/nonroot/.provider"
consumerHomePath = "/home/nonroot/.consumer"
bondDenom = "uatone"
feeDenom = "uphoton"
providerChainID = "provider-e2e"
consumerChainID = "consumer-e2e"
)
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/e2e_debt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ func (s *IntegrationTestSuite) testConsumerDebtFlow() {
s.T().Log("funding consumer fee pool on provider...")
// Must clear the min-deposit floor: fees_per_block (1000) *
// MinDepositBlocks (14400) = 14_400_000. Fund above it with margin.
s.providerFundConsumerFeePool("0", "20000000"+bondDenom)
s.providerFundConsumerFeePool("0", "20000000"+feeDenom)

s.T().Log("waiting for consumer to exit debt (bank send should succeed)...")
s.Require().Eventuallyf(func() bool {
Expand Down
8 changes: 4 additions & 4 deletions tests/e2e/e2e_fee_pool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func (s *IntegrationTestSuite) providerFundCommunityPool(amount string) {
func (s *IntegrationTestSuite) testFeePoolFundAndLockEnforcement() {
s.Run("fee pool fund and lock enforcement", func() {
const consumerID = "0"
denom := bondDenom
denom := feeDenom
valAddr := s.providerKeyAddress("val")
userAddr := s.providerKeyAddress("user")

Expand Down Expand Up @@ -222,7 +222,7 @@ func (s *IntegrationTestSuite) testFeePoolSendRestriction() {
func (s *IntegrationTestSuite) testFeePoolGovSubsidyClawback() {
s.Run("fee pool gov subsidy + clawback", func() {
const consumerID = "0"
denom := bondDenom
denom := feeDenom
govAddr := s.queryGovAuthority()
distrAddr := s.queryModuleAccountAddress("distribution")

Expand All @@ -242,7 +242,7 @@ func (s *IntegrationTestSuite) testFeePoolGovSubsidyClawback() {
"deposit": "10000000%s",
"title": "Subsidize consumer %s",
"summary": "e2e gov subsidy test"
}`, govAddr, consumerID, denom, denom, consumerID)
}`, govAddr, consumerID, denom, bondDenom, consumerID)

s.submitAndPassProposal(fundJSON)

Expand All @@ -263,7 +263,7 @@ func (s *IntegrationTestSuite) testFeePoolGovSubsidyClawback() {
"deposit": "10000000%s",
"title": "Clawback consumer %s subsidy",
"summary": "e2e gov clawback test"
}`, govAddr, consumerID, denom, denom, consumerID)
}`, govAddr, consumerID, denom, bondDenom, consumerID)

s.submitAndPassProposal(clawbackJSON)

Expand Down
11 changes: 4 additions & 7 deletions tests/e2e/e2e_setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,16 +249,13 @@ func (s *IntegrationTestSuite) initAndStartProvider() {
}
}

// Set fast epoch for VSC, and override fees_per_block to use the
// bond denom so the e2e debt-flow test can fund the consumer fee
// pool from existing genesis accounts.
// Set fast epoch for VSC and a small per-block fee amount. The fee
// denom is fixed to feeDenom at module wiring, only the amount is
// configurable here. val is funded with feeDenom in provider-init.sh
if provider, ok := appState["provider"].(map[string]any); ok {
if params, ok := provider["params"].(map[string]any); ok {
params["blocks_per_epoch"] = "5"
params["fees_per_block"] = map[string]any{
"denom": bondDenom,
"amount": "1000",
}
params["fees_per_block_amount"] = "1000"
}
}
})
Expand Down
3 changes: 2 additions & 1 deletion tests/e2e/scripts/provider-init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ BINARY="${BINARY:-provider}"
HOME_DIR="${HOME_DIR:-/home/nonroot/.provider}"
CHAIN_ID="${CHAIN_ID:-provider-e2e}"
DENOM="${DENOM:-uatone}"
FEE_DENOM="${FEE_DENOM:-uphoton}"
MNEMONIC="${MNEMONIC:-abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art}"

# Initialize chain
Expand All @@ -24,7 +25,7 @@ $BINARY keys add user --home "$HOME_DIR" --keyring-backend test
echo "$MNEMONIC" | $BINARY keys add relayer --recover --home "$HOME_DIR" --keyring-backend test

# Add genesis accounts
$BINARY genesis add-genesis-account val "1000000000000${DENOM}" --home "$HOME_DIR" --keyring-backend test
$BINARY genesis add-genesis-account val "1000000000000${DENOM},1000000000000${FEE_DENOM}" --home "$HOME_DIR" --keyring-backend test
$BINARY genesis add-genesis-account user "1000000000${DENOM}" --home "$HOME_DIR" --keyring-backend test
$BINARY genesis add-genesis-account relayer "100000000${DENOM}" --home "$HOME_DIR" --keyring-backend test

Expand Down
1 change: 1 addition & 0 deletions testutil/keeper/unit_test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ func NewInMemProviderKeeper(params InMemKeeperParams, mocks MockedKeepers) provi
address.NewBech32Codec("cosmosvaloper"),
address.NewBech32Codec("cosmosvalcons"),
authtypes.FeeCollectorName,
providertypes.DefaultFeesPerBlockDenom,
)
}

Expand Down
48 changes: 48 additions & 0 deletions x/vaas/provider/keeper/fee_denom_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package keeper_test

import (
"fmt"
"testing"

providerkeeper "github.com/allinbits/vaas/x/vaas/provider/keeper"
"github.com/stretchr/testify/require"

"cosmossdk.io/math"

sdk "github.com/cosmos/cosmos-sdk/types"
govkeeper "github.com/cosmos/cosmos-sdk/x/gov/keeper"
)

// NewKeeper fixes the per-block fee denom at construction. The guard exists so
// that GetFeesPerBlock's sdk.NewCoin(feeDenom, amount) can never panic at
// runtime: a bad denom is a wiring mistake that must fail loudly at startup
// rather than surface later as a malformed fee coin. For each rejected denom we
// assert both halves of that contract: the denom genuinely breaks coin
// construction, and NewKeeper refuses it up front. The guard is the first
// statement in NewKeeper, so nil dependencies are fine here; the valid-denom
// path is exercised by every other keeper test through the in-mem helper.
func TestNewKeeperRejectsInvalidFeeDenom(t *testing.T) {
assertRejected := func(denom string) {
require.Panicsf(t, func() {
sdk.NewCoin(denom, math.OneInt())
}, "sanity: %q must be an invalid coin denom for the guard to be meaningful", denom)

defer func() {
r := recover()
require.NotNilf(t, r, "expected NewKeeper to panic for fee denom %q", denom)
require.Containsf(t, fmt.Sprint(r), "fee denom", "denom %q should trip the fee-denom guard", denom)
}()
providerkeeper.NewKeeper(
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
govkeeper.Keeper{},
"authority",
nil, nil,
"fee_collector",
denom,
)
}

for _, denom := range []string{"", "1leadingdigit", "white space"} {
assertRejected(denom)
}
}
2 changes: 1 addition & 1 deletion x/vaas/provider/keeper/fees.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func (k Keeper) CollectFeesFromConsumers(ctx sdk.Context) sdk.Coin {
// does via BeginBlock VoteInfos. Signers that have since unbonded or been
// removed from the set forfeit their share — we only pay current participants.
func (k Keeper) DistributeFeesToValidators(ctx sdk.Context) error {
totalFees := k.bankKeeper.GetBalance(ctx, authtypes.NewModuleAddress(types.ModuleName), k.GetFeesPerBlock(ctx).Denom)
totalFees := k.bankKeeper.GetBalance(ctx, authtypes.NewModuleAddress(types.ModuleName), k.GetFeeDenom())
if totalFees.IsZero() {
return nil
}
Expand Down
Loading
Loading