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
93 changes: 93 additions & 0 deletions app/ante/authz_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//go:build test

// This file builds a full in-process app via zetasimulation.NewSimApp, which requires the `test`
// build tag; without it the EVM configurator panics. The tag keeps a bare `go test ./app/ante/...`
// (no -tags) from taking the rest of the package down with a confusing panic.

package ante_test

import (
"math/rand"
"testing"
"time"

"cosmossdk.io/log"
dbm "github.com/cosmos/cosmos-db"
simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims"
sdk "github.com/cosmos/cosmos-sdk/types"
vesting "github.com/cosmos/cosmos-sdk/x/auth/vesting/types"
"github.com/cosmos/cosmos-sdk/x/group"
"github.com/stretchr/testify/require"

"github.com/zeta-chain/node/app"
"github.com/zeta-chain/node/app/ante"
serverconfig "github.com/zeta-chain/node/server/config"
zetasimulation "github.com/zeta-chain/node/simulation"
"github.com/zeta-chain/node/testutil/sample"
)

// TestProductionAnteHandler_RejectsVestingViaGroupProposal is an in-process integration check that
// wires the real app keepers into the full production ante chain (via app.DisabledAuthzMsgs) and
// runs a signed group.MsgSubmitProposal that wraps MsgCreateVestingAccount through it. It asserts
// the tx is rejected — the same outcome the localnet e2e test (disallow_vesting_via_group_proposal)
// verifies over the wire, but executable without Docker.
func TestProductionAnteHandler_RejectsVestingViaGroupProposal(t *testing.T) {
// ARRANGE
zetaApp, err := zetasimulation.NewSimApp(
Comment thread
kingpinXD marked this conversation as resolved.
log.NewNopLogger(),
dbm.NewMemDB(),
simtestutil.AppOptionsMap{},
)
require.NoError(t, err)

encCfg := app.MakeEncodingConfig(serverconfig.DefaultEVMChainID)

// build the ante handler from the exact options app.New installs, so dropping any field there
// (e.g. DisabledAuthzMsgs) fails this test rather than silently passing
anteHandler, err := ante.NewAnteHandler(
app.AnteHandlerOptions(zetaApp, encCfg.TxConfig.SignModeHandler()),
)
require.NoError(t, err)

testPrivKey, testAddress := sample.PrivKeyAddressPair()
_, testAddress2 := sample.PrivKeyAddressPair()

vestingMsg := vesting.NewMsgCreateVestingAccount(
testAddress, testAddress2,
sdk.NewCoins(sdk.NewInt64Coin("azeta", 100_000_000)),
time.Date(2100, 1, 1, 0, 0, 0, 0, time.UTC).Unix(),
false,
)
proposalMsg, err := group.NewMsgSubmitProposal(
sample.AccAddress(),
[]string{testAddress.String()},
[]sdk.Msg{vestingMsg},
"",
group.Exec_EXEC_UNSPECIFIED,
"title",
"summary",
)
require.NoError(t, err)

tx, err := simtestutil.GenSignedMockTx(
rand.New(rand.NewSource(1)),
encCfg.TxConfig,
[]sdk.Msg{proposalMsg},
sdk.NewCoins(),
simtestutil.DefaultGenTxGas,
"testing-chain-id",
[]uint64{0},
[]uint64{0},
testPrivKey,
)
require.NoError(t, err)

ctx := zetaApp.NewContext(true)

// ACT
_, err = anteHandler(ctx, tx, false)

// ASSERT: the ante decorator rejects the group-wrapped vesting-create before the group module runs
require.Error(t, err)
require.ErrorContains(t, err, "found disabled msg type")
}
201 changes: 201 additions & 0 deletions app/ante/authz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package ante_test

import (
"math/rand"
"testing"
"time"

simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims"
sdk "github.com/cosmos/cosmos-sdk/types"
vesting "github.com/cosmos/cosmos-sdk/x/auth/vesting/types"
"github.com/cosmos/cosmos-sdk/x/authz"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1"
"github.com/cosmos/cosmos-sdk/x/group"
"github.com/stretchr/testify/require"

"github.com/zeta-chain/node/app"
"github.com/zeta-chain/node/app/ante"
serverconfig "github.com/zeta-chain/node/server/config"
"github.com/zeta-chain/node/testutil/sample"
)

// TestDisabledAuthzMsgs_WiresVestingCreation asserts the production disabled-msg list actually
// contains the three vesting-account creation messages. This binds the ante test to app.go's real
// wiring: dropping any of them from app.DisabledAuthzMsgs would fail here.
func TestDisabledAuthzMsgs_WiresVestingCreation(t *testing.T) {
disabled := app.DisabledAuthzMsgs()
require.Contains(t, disabled, sdk.MsgTypeURL(&vesting.MsgCreateVestingAccount{}))
require.Contains(t, disabled, sdk.MsgTypeURL(&vesting.MsgCreatePermanentLockedAccount{}))
require.Contains(t, disabled, sdk.MsgTypeURL(&vesting.MsgCreatePeriodicVestingAccount{}))
}
Comment thread
kingpinXD marked this conversation as resolved.

// TestAuthzLimiter_AnteHandle verifies the decorator blocks the disabled vesting msgs when they are
// wrapped in authz.MsgExec OR group.MsgSubmitProposal (including nested), and lets non-disabled
// messages through. The decorator is built from app.DisabledAuthzMsgs() (the real production list)
// so the test cannot pass while the chain wiring regresses.
func TestAuthzLimiter_AnteHandle(t *testing.T) {
// ARRANGE
txConfig := app.MakeEncodingConfig(serverconfig.DefaultEVMChainID).TxConfig

testPrivKey, testAddress := sample.PrivKeyAddressPair()
_, testAddress2 := sample.PrivKeyAddressPair()
_, policyAddress := sample.PrivKeyAddressPair()

decorator := ante.NewAuthzLimiterDecorator(app.DisabledAuthzMsgs()...)

createVestingMsg := vesting.NewMsgCreateVestingAccount(
testAddress, testAddress2,
sdk.NewCoins(sdk.NewInt64Coin("azeta", 100_000_000)),
time.Date(2100, 1, 1, 0, 0, 0, 0, time.UTC).Unix(),
false,
)
permanentLockedMsg := vesting.NewMsgCreatePermanentLockedAccount(
testAddress, testAddress2,
sdk.NewCoins(sdk.NewInt64Coin("azeta", 100_000_000)),
)
periodicVestingMsg := vesting.NewMsgCreatePeriodicVestingAccount(
testAddress, testAddress2,
time.Date(2100, 1, 1, 0, 0, 0, 0, time.UTC).Unix(),
nil,
)
bankSend := banktypes.NewMsgSend(
testAddress, testAddress2,
sdk.NewCoins(sdk.NewInt64Coin("azeta", 100_000_000)),
)

groupProposal := func(t *testing.T, inner sdk.Msg) sdk.Msg {
msg, err := group.NewMsgSubmitProposal(
policyAddress.String(),
[]string{testAddress.String()},
[]sdk.Msg{inner},
"",
group.Exec_EXEC_UNSPECIFIED,
"title",
"summary",
)
require.NoError(t, err)
return msg
}
authzExec := func(inner sdk.Msg) sdk.Msg {
msg := authz.NewMsgExec(testAddress, []sdk.Msg{inner})
return &msg
}
authzGrant := func(t *testing.T, msgTypeURL string) sdk.Msg {
msg, err := authz.NewMsgGrant(
testAddress, testAddress2,
authz.NewGenericAuthorization(msgTypeURL),
nil,
)
require.NoError(t, err)
return msg
}
govProposal := func(t *testing.T, inner sdk.Msg) sdk.Msg {
msg, err := govv1.NewMsgSubmitProposal(
[]sdk.Msg{inner},
sdk.NewCoins(sdk.NewInt64Coin("azeta", 100_000_000)),
testAddress.String(),
"",
"title",
"summary",
false,
)
require.NoError(t, err)
return msg
}

tests := []struct {
Comment thread
kingpinXD marked this conversation as resolved.
name string
msg sdk.Msg
wantHasErr bool
wantErr string
}{
{
"group proposal wrapping MsgCreateVestingAccount is blocked",
groupProposal(t, createVestingMsg),
true,
"found disabled msg type",
},
{
"group proposal wrapping MsgCreatePermanentLockedAccount is blocked",
groupProposal(t, permanentLockedMsg),
true,
"found disabled msg type",
},
{
"group proposal wrapping MsgCreatePeriodicVestingAccount is blocked",
groupProposal(t, periodicVestingMsg),
true,
"found disabled msg type",
},
{
"authz exec wrapping a vesting msg is blocked",
authzExec(createVestingMsg),
true,
"found disabled msg type",
},
{
"authz grant of a vesting-create authorization is blocked",
authzGrant(t, sdk.MsgTypeURL(&vesting.MsgCreateVestingAccount{})),
true,
"found disabled msg type",
},
{
"authz exec wrapping a group proposal wrapping a vesting msg is blocked (nested)",
authzExec(groupProposal(t, createVestingMsg)),
true,
"found disabled msg type",
},
{
"group proposal wrapping a non-disabled msg is allowed",
groupProposal(t, bankSend),
false,
"",
},
{
"top-level vesting msg is not blocked here (VestingAccountDecorator owns that)",
Comment thread
kingpinXD marked this conversation as resolved.
createVestingMsg,
false,
"",
},
{
// Documents a known gap: AuthzLimiterDecorator has no gov.MsgSubmitProposal case, so a
// governance proposal can still carry a vesting-create. That path is privileged (a passed
// gov vote, self-funded from the gov account), unlike the permissionless group path.
"gov proposal wrapping a vesting msg is NOT blocked by this decorator (gov is out of scope)",
govProposal(t, createVestingMsg),
false,
"",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tx, err := simtestutil.GenSignedMockTx(
rand.New(rand.NewSource(time.Now().UnixNano())),
txConfig,
[]sdk.Msg{tt.msg},
sdk.NewCoins(),
simtestutil.DefaultGenTxGas,
"testing-chain-id",
[]uint64{0},
[]uint64{0},
testPrivKey,
)
require.NoError(t, err)

mmd := MockAnteHandler{}
ctx := sdk.Context{}.WithIsCheckTx(true)

// ACT
_, err = decorator.AnteHandle(ctx, tx, false, mmd.AnteHandle)

// ASSERT
if tt.wantHasErr {
require.ErrorContains(t, err, tt.wantErr)
} else {
require.NoError(t, err)
}
})
}
}
50 changes: 32 additions & 18 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"cosmossdk.io/x/evidence"
evidencekeeper "cosmossdk.io/x/evidence/keeper"
evidencetypes "cosmossdk.io/x/evidence/types"
txsigning "cosmossdk.io/x/tx/signing"
"cosmossdk.io/x/upgrade"
upgradekeeper "cosmossdk.io/x/upgrade/keeper"
upgradetypes "cosmossdk.io/x/upgrade/types"
Expand Down Expand Up @@ -259,6 +260,36 @@ type App struct {
//transferModule transfer.AppModule
}

// DisabledAuthzMsgs returns the message type URLs that may not be executed indirectly, i.e. wrapped
// inside an authz.MsgExec or a group.MsgSubmitProposal. This is the single source of truth consumed
// by the ante HandlerOptions; tests assert against it so the wiring cannot silently regress.
func DisabledAuthzMsgs() []string {
return []string{
// MsgEthereumTx cannot be included on an authz.MsgExec msgs field
sdk.MsgTypeURL(&evmtypes.MsgEthereumTx{}),
sdk.MsgTypeURL(&vestingtypes.MsgCreateVestingAccount{}),
sdk.MsgTypeURL(&vestingtypes.MsgCreatePermanentLockedAccount{}),
sdk.MsgTypeURL(&vestingtypes.MsgCreatePeriodicVestingAccount{}),
}
}

// AnteHandlerOptions builds the ante HandlerOptions installed by New. It is exported so tests can
// exercise the exact options the app runs with, rather than rebuilding the struct by hand and
// drifting from the real wiring (e.g. dropping DisabledAuthzMsgs would then go unnoticed).
func AnteHandlerOptions(app *App, signModeHandler *txsigning.HandlerMap) ante.HandlerOptions {
return ante.HandlerOptions{
AccountKeeper: app.AccountKeeper,
BankKeeper: app.BankKeeper,
EvmKeeper: app.EvmKeeper,
FeeMarketKeeper: app.FeeMarketKeeper,
SignModeHandler: signModeHandler,
SigGasConsumer: ante.DefaultSigVerificationGasConsumer,
MaxTxGasWanted: TransactionGasLimit,
DisabledAuthzMsgs: DisabledAuthzMsgs(),
ObserverKeeper: app.ObserverKeeper,
}
}

// New returns a reference to an initialized ZetaApp.
func New(
logger log.Logger,
Expand Down Expand Up @@ -778,24 +809,7 @@ func New(
app.SetPreBlocker(app.PreBlocker)
app.SetBeginBlocker(app.BeginBlocker)

options := ante.HandlerOptions{
AccountKeeper: app.AccountKeeper,
BankKeeper: app.BankKeeper,
EvmKeeper: app.EvmKeeper,
FeeMarketKeeper: app.FeeMarketKeeper,
SignModeHandler: encodingConfig.TxConfig.SignModeHandler(),
SigGasConsumer: ante.DefaultSigVerificationGasConsumer,
MaxTxGasWanted: TransactionGasLimit,
DisabledAuthzMsgs: []string{
sdk.MsgTypeURL(
&evmtypes.MsgEthereumTx{},
), // disable the Msg types that cannot be included on an authz.MsgExec msgs field
sdk.MsgTypeURL(&vestingtypes.MsgCreateVestingAccount{}),
sdk.MsgTypeURL(&vestingtypes.MsgCreatePermanentLockedAccount{}),
sdk.MsgTypeURL(&vestingtypes.MsgCreatePeriodicVestingAccount{}),
},
ObserverKeeper: app.ObserverKeeper,
}
options := AnteHandlerOptions(app, encodingConfig.TxConfig.SignModeHandler())

anteHandler, err := ante.NewAnteHandler(options)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
### Tests

* [4539](https://github.com/zeta-chain/node/pull/4539) - add support for `signet` name in the e2e config
* [4635](https://github.com/zeta-chain/node/pull/4635) - add unit and e2e tests asserting `MsgCreateVestingAccount` is rejected when wrapped in a group proposal

## Release ReForge
- zetacored: v37.0.0
Expand Down
1 change: 1 addition & 0 deletions cmd/zetae2e/local/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,7 @@ func localE2ETest(cmd *cobra.Command, _ []string) {
e2etests.TestCriticalAdminTransactionsName,
e2etests.TestUpdateOperationalChainParamsName,
e2etests.TestBurnFungibleModuleAssetName,
e2etests.TestDisallowVestingViaGroupProposalName,
Comment thread
kingpinXD marked this conversation as resolved.

// Currently this test doesn't work with Anvil because pre-EIP1559 txs are not supported
// See issue below for details
Expand Down
Loading
Loading