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
2 changes: 1 addition & 1 deletion app/consumer/ante_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
82 changes: 82 additions & 0 deletions x/vaas/consumer/ante/photon_fee_ante.go
Original file line number Diff line number Diff line change
@@ -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/<providerClientID>/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)
}
105 changes: 105 additions & 0 deletions x/vaas/consumer/ante/photon_fee_ante_test.go
Original file line number Diff line number Diff line change
@@ -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/<clientID>/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))
}
1 change: 1 addition & 0 deletions x/vaas/consumer/types/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)
Loading