Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
94 changes: 94 additions & 0 deletions app/ante/authz_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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 exactly as app.New does, with the real disabled-msg list
anteHandler, err := ante.NewAnteHandler(ante.HandlerOptions{
Comment thread
kingpinXD marked this conversation as resolved.
Outdated
AccountKeeper: zetaApp.AccountKeeper,
BankKeeper: zetaApp.BankKeeper,
EvmKeeper: zetaApp.EvmKeeper,
FeeMarketKeeper: zetaApp.FeeMarketKeeper,
SignModeHandler: encCfg.TxConfig.SignModeHandler(),
SigGasConsumer: ante.DefaultSigVerificationGasConsumer,
MaxTxGasWanted: 0,
Comment thread
kingpinXD marked this conversation as resolved.
Outdated
DisabledAuthzMsgs: app.DisabledAuthzMsgs(),
ObserverKeeper: zetaApp.ObserverKeeper,
})
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")
}
186 changes: 186 additions & 0 deletions app/ante/authz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
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
}
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 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)
}
})
}
}
38 changes: 22 additions & 16 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,19 @@ 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{}),
}
}

// New returns a reference to an initialized ZetaApp.
func New(
logger log.Logger,
Expand Down Expand Up @@ -779,22 +792,15 @@ func New(
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,
AccountKeeper: app.AccountKeeper,
BankKeeper: app.BankKeeper,
EvmKeeper: app.EvmKeeper,
FeeMarketKeeper: app.FeeMarketKeeper,
SignModeHandler: encodingConfig.TxConfig.SignModeHandler(),
SigGasConsumer: ante.DefaultSigVerificationGasConsumer,
MaxTxGasWanted: TransactionGasLimit,
DisabledAuthzMsgs: DisabledAuthzMsgs(),
ObserverKeeper: app.ObserverKeeper,
}

anteHandler, err := ante.NewAnteHandler(options)
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