From 6ff2cfb3ca7dd408221ce192f365c6866f3785f5 Mon Sep 17 00:00:00 2001 From: Giuseppe Natale <12249307+giunatale@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:14:39 +0200 Subject: [PATCH 1/2] move consumer ante decorators into x/vaas/consumer/ante --- app/consumer/ante_handler.go | 2 +- .../vaas/consumer/ante/disabled_modules_ante.go | 0 {app => x/vaas}/consumer/ante/msg_filter_ante.go | 0 {app => x/vaas}/consumer/ante/msg_filter_ante_test.go | 0 4 files changed, 1 insertion(+), 1 deletion(-) rename app/consumer/ante/disabled_module_ante.go => x/vaas/consumer/ante/disabled_modules_ante.go (100%) rename {app => x/vaas}/consumer/ante/msg_filter_ante.go (100%) rename {app => x/vaas}/consumer/ante/msg_filter_ante_test.go (100%) diff --git a/app/consumer/ante_handler.go b/app/consumer/ante_handler.go index 6fd3773..3e93ab6 100644 --- a/app/consumer/ante_handler.go +++ b/app/consumer/ante_handler.go @@ -2,7 +2,7 @@ package app import ( errorsmod "cosmossdk.io/errors" - consumerante "github.com/allinbits/vaas/app/consumer/ante" + consumerante "github.com/allinbits/vaas/x/vaas/consumer/ante" ibcconsumerkeeper "github.com/allinbits/vaas/x/vaas/consumer/keeper" ibcante "github.com/cosmos/ibc-go/v10/modules/core/ante" ibckeeper "github.com/cosmos/ibc-go/v10/modules/core/keeper" diff --git a/app/consumer/ante/disabled_module_ante.go b/x/vaas/consumer/ante/disabled_modules_ante.go similarity index 100% rename from app/consumer/ante/disabled_module_ante.go rename to x/vaas/consumer/ante/disabled_modules_ante.go diff --git a/app/consumer/ante/msg_filter_ante.go b/x/vaas/consumer/ante/msg_filter_ante.go similarity index 100% rename from app/consumer/ante/msg_filter_ante.go rename to x/vaas/consumer/ante/msg_filter_ante.go diff --git a/app/consumer/ante/msg_filter_ante_test.go b/x/vaas/consumer/ante/msg_filter_ante_test.go similarity index 100% rename from app/consumer/ante/msg_filter_ante_test.go rename to x/vaas/consumer/ante/msg_filter_ante_test.go From 6b3ead7644ed3e96d68c597aaade8d0301214f9d Mon Sep 17 00:00:00 2001 From: Giuseppe Natale <12249307+giunatale@users.noreply.github.com> Date: Tue, 9 Jun 2026 22:03:24 +0200 Subject: [PATCH 2/2] add opt-in photon-only fee ante decorator for consumers --- x/vaas/consumer/ante/msg_filter_ante.go | 5 +- x/vaas/consumer/ante/photon_fee_ante.go | 82 +++++++++++++++ x/vaas/consumer/ante/photon_fee_ante_test.go | 105 +++++++++++++++++++ x/vaas/consumer/types/errors.go | 1 + 4 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 x/vaas/consumer/ante/photon_fee_ante.go create mode 100644 x/vaas/consumer/ante/photon_fee_ante_test.go diff --git a/x/vaas/consumer/ante/msg_filter_ante.go b/x/vaas/consumer/ante/msg_filter_ante.go index 03a59a1..a438daa 100644 --- a/x/vaas/consumer/ante/msg_filter_ante.go +++ b/x/vaas/consumer/ante/msg_filter_ante.go @@ -5,10 +5,11 @@ import ( "fmt" "strings" + consumertypes "github.com/allinbits/vaas/x/vaas/consumer/types" + errorsmod "cosmossdk.io/errors" - sdk "github.com/cosmos/cosmos-sdk/types" - consumertypes "github.com/allinbits/vaas/x/vaas/consumer/types" + sdk "github.com/cosmos/cosmos-sdk/types" ) type ( diff --git a/x/vaas/consumer/ante/photon_fee_ante.go b/x/vaas/consumer/ante/photon_fee_ante.go new file mode 100644 index 0000000..fcf16c7 --- /dev/null +++ b/x/vaas/consumer/ante/photon_fee_ante.go @@ -0,0 +1,82 @@ +package ante + +import ( + "context" + + consumertypes "github.com/allinbits/vaas/x/vaas/consumer/types" + + transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" + + errorsmod "cosmossdk.io/errors" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// This file implements the PhotonFeeDecorator, an ante decorator that enforces +// that transaction fees are paid in the one-hop photon voucher received from +// atomone over the provider client. This is a requirement for "core shards" +// as per the AtomOne constitution, and is opt-in for consumer chains. +// While the remainder of the VAAS implementation is built trying to make +// it as agnostic as possible with respect to what is the provider chain, +// this particular decorator is tightly coupled to the assumption that the +// provider is AtomOne. + +// PhotonBaseDenom is the AtomOne base (micro)denomination that is bridged to +// consumers as the photon fee voucher. +const PhotonBaseDenom = "uphoton" + +// PhotonFeeKeeper is the narrow consumer-keeper dependency: it resolves the +// provider (AtomOne) IBC client the photon voucher denom is anchored to. +type PhotonFeeKeeper interface { + GetProviderClientID(ctx context.Context) (string, bool) +} + +// PhotonFeeDecorator rejects transactions whose fees are not denominated in the +// one-hop photon voucher received directly from AtomOne over the provider client. +// +// It is opt-in: to use it a consumer wires it into its ante chain, +// immediately before ante.NewDeductFeeDecorator. Until the provider client is +// established (pre-VAAS) it is a no-op. +type PhotonFeeDecorator struct { + keeper PhotonFeeKeeper +} + +func NewPhotonFeeDecorator(k PhotonFeeKeeper) PhotonFeeDecorator { + return PhotonFeeDecorator{keeper: k} +} + +// ExpectedPhotonDenom derives the IBC voucher denom for one-hop uphoton received +// over the given provider client: ibc/SHA256("transfer//uphoton"). +func ExpectedPhotonDenom(providerClientID string) string { + denom := transfertypes.Denom{ + Base: PhotonBaseDenom, + Trace: []transfertypes.Hop{transfertypes.NewHop(transfertypes.PortID, providerClientID)}, + } + return denom.IBCDenom() +} + +func (d PhotonFeeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { + providerClientID, ok := d.keeper.GetProviderClientID(ctx) + if !ok { + // Pre-VAAS: provider client not established, so no photon voucher can exist + // yet. Stay out of the way so the IBC stack can be set up. + return next(ctx, tx, simulate) + } + + feeTx, ok := tx.(sdk.FeeTx) + if !ok { + return next(ctx, tx, simulate) + } + + expected := ExpectedPhotonDenom(providerClientID) + for _, coin := range feeTx.GetFee() { + if coin.Denom != expected { + return ctx, errorsmod.Wrapf( + consumertypes.ErrInvalidFeeDenom, + "fee denom %s is not the photon denom %s", coin.Denom, expected, + ) + } + } + + return next(ctx, tx, simulate) +} diff --git a/x/vaas/consumer/ante/photon_fee_ante_test.go b/x/vaas/consumer/ante/photon_fee_ante_test.go new file mode 100644 index 0000000..29cb4bb --- /dev/null +++ b/x/vaas/consumer/ante/photon_fee_ante_test.go @@ -0,0 +1,105 @@ +package ante + +import ( + "crypto/sha256" + "encoding/hex" + "strings" + "testing" + + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + protov2 "google.golang.org/protobuf/proto" + + consumertypes "github.com/allinbits/vaas/x/vaas/consumer/types" +) + +// mockFeeTx is a minimal sdk.FeeTx for testing the photon fee decorator. +type mockFeeTx struct { + fee sdk.Coins +} + +func (m mockFeeTx) GetMsgs() []sdk.Msg { return nil } +func (m mockFeeTx) GetMsgsV2() ([]protov2.Message, error) { return nil, nil } +func (m mockFeeTx) GetGas() uint64 { return 0 } +func (m mockFeeTx) GetFee() sdk.Coins { return m.fee } +func (m mockFeeTx) FeePayer() []byte { return nil } +func (m mockFeeTx) FeeGranter() []byte { return nil } + +func runPhotonDecorator(t *testing.T, k mockConsumerKeeper, tx sdk.Tx) (bool, error) { + t.Helper() + decorator := NewPhotonFeeDecorator(k) + nextCalled := false + _, err := decorator.AnteHandle(sdk.Context{}, tx, false, func(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) { + nextCalled = true + return ctx, nil + }) + return nextCalled, err +} + +func TestPhotonFeeDecorator(t *testing.T) { + photon := ExpectedPhotonDenom("07-tendermint-0") + testCases := []struct { + name string + providerClient bool + tx sdk.Tx + expectErr bool + }{ + { + name: "provider client not established is a no-op, even for a non-photon fee", + providerClient: false, + tx: mockFeeTx{fee: sdk.NewCoins(sdk.NewInt64Coin("uatone", 100))}, + }, + { + name: "fee in the photon voucher denom passes", + providerClient: true, + tx: mockFeeTx{fee: sdk.NewCoins(sdk.NewInt64Coin(photon, 100))}, + }, + { + name: "fee in any other denom is rejected", + providerClient: true, + tx: mockFeeTx{fee: sdk.NewCoins(sdk.NewInt64Coin("uatone", 100))}, + expectErr: true, + }, + { + name: "multi-coin fee containing a non-photon denom is rejected", + providerClient: true, + tx: mockFeeTx{fee: sdk.NewCoins(sdk.NewInt64Coin(photon, 100), sdk.NewInt64Coin("uatone", 5))}, + expectErr: true, + }, + { + name: "empty fee passes (denom-only policy as minimum-fee is out of scope)", + providerClient: true, + tx: mockFeeTx{fee: sdk.NewCoins()}, + }, + { + name: "non-FeeTx passes (nothing to check)", + providerClient: true, + tx: mockTx{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + nextCalled, err := runPhotonDecorator(t, mockConsumerKeeper{providerClientFound: tc.providerClient}, tc.tx) + if tc.expectErr { + require.Error(t, err) + require.True(t, errorsmod.IsOf(err, consumertypes.ErrInvalidFeeDenom)) + require.False(t, nextCalled) + return + } + require.NoError(t, err) + require.True(t, nextCalled) + }) + } +} + +// Wire-format pinning: ExpectedPhotonDenom must equal the ICS-20 denom +// ibc/UPPERHEX(SHA256("transfer//uphoton")), computed here from a raw +// literal independent of the transfertypes helper. +func TestExpectedPhotonDenomMatchesICS20Format(t *testing.T) { + const clientID = "07-tendermint-0" + sum := sha256.Sum256([]byte("transfer/" + clientID + "/uphoton")) + want := "ibc/" + strings.ToUpper(hex.EncodeToString(sum[:])) + require.Equal(t, want, ExpectedPhotonDenom(clientID)) +} diff --git a/x/vaas/consumer/types/errors.go b/x/vaas/consumer/types/errors.go index 22b78bc..41ad9d1 100644 --- a/x/vaas/consumer/types/errors.go +++ b/x/vaas/consumer/types/errors.go @@ -8,4 +8,5 @@ import ( var ( ErrInvalidProviderClient = errorsmod.Register(ModuleName, 2, "invalid provider client") ErrConsumerInDebt = errorsmod.Register(ModuleName, 3, "consumer chain is in debt") + ErrInvalidFeeDenom = errorsmod.Register(ModuleName, 4, "invalid fee denom: consumer requires photon") )