Skip to content

Commit 1e672f3

Browse files
authored
fix: Expedited Governance Proposal Whitelist Bypass via authz Wrapping (#353)
# Description The `GovExpeditedProposalsDecorator` only inspected top level messages, so a non-whitelisted `MsgSubmitProposal` marked as expedited could slip through by being wrapped inside an `authz.MsgExec`. That let non-whitelisted proposal types use the shortened expedited voting window that governance policy intended to reserve for the standard timeline. The fix unwraps `authz.MsgExec` recursively and applies the same expedited whitelist validation to the wrapped proposals, matching the pattern already used by `GovVoteDecorator`. No new dependencies. ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) # How Has This Been Tested? ```bash go test -tags=test ./ante/ -run TestGovExpeditedProposalsDecorator -v ``` - [x] A non-whitelisted expedited proposal wrapped in `authz.MsgExec` (and nested `exec(exec(...))`) is now rejected - [x] A whitelisted expedited proposal in authz still passes, and non-expedited proposals in authz are unaffected # PR Checklist: - [x] Updated changelog with PR's intent - [x] Lint with `make lint-fix`
2 parents 4313157 + c53b016 commit 1e672f3

3 files changed

Lines changed: 100 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
### Fixed
1313

14+
- Close an expedited-governance whitelist bypass in `GovExpeditedProposalsDecorator` where the check only inspected top-level messages: a non-whitelisted `MsgSubmitProposal` wrapped in `authz.MsgExec` could enter the expedited voting path. The decorator now recurses into `authz.MsgExec` (including nested execs) and applies the expedited whitelist validation to wrapped proposals
1415
- Compute the oracle ballot `StandardDeviation` as a stake-weighted variance (weight each squared deviation by the vote's power and divide by total voting power) instead of an unweighted average divided by the vote count, aligning the reward-band width with the stake-weighted median and preventing a group of low-stake validators from inflating the deviation to widen the accepted vote window
1516
- Close an oracle slashing bypass in the `EndBlocker` where validators were scored against the post-filtered `voteTargets` map: a denom that received votes but was pushed below the vote threshold (e.g. by a coordinated group abstaining) was dropped from the scoring denominator, letting the abstainers avoid miss penalties. Participation is now scored against the configured targets that received votes (passing targets plus below-threshold targets), crediting validators that voted on a below-threshold target while counting abstention on it as a miss; targets that received no votes at all are still excluded so a legitimately unpriceable denom cannot mass-slash the validator set
1617
- Allow EIP-7702 delegated EOAs to send direct EVM transactions by exempting delegation-designator code from the externally-owned-account-only check in `VerifyIfAccountExists`, so accounts that delegate via `SetCodeTx` can still manage (and revoke) their own delegation without a sponsored transaction

ante/gov_expedited_ante.go

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/cosmos/cosmos-sdk/codec"
77
sdk "github.com/cosmos/cosmos-sdk/types"
8+
"github.com/cosmos/cosmos-sdk/x/authz"
89
govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1"
910

1011
xerrors "github.com/kiichain/kiichain/v7/x/types/errors"
@@ -23,11 +24,12 @@ var expeditedPropsWhitelist = map[string]struct{}{
2324
"/cosmos.upgrade.v1beta1.MsgCancelUpgrade": {},
2425
}
2526

26-
// Check if the proposal is whitelisted for expedited voting.
27+
// GovExpeditedProposalsDecorator rejects expedited proposals whose messages are not whitelisted.
2728
type GovExpeditedProposalsDecorator struct {
2829
cdc codec.BinaryCodec
2930
}
3031

32+
// NewGovExpeditedProposalsDecorator returns a new GovExpeditedProposalsDecorator.
3133
func NewGovExpeditedProposalsDecorator(cdc codec.BinaryCodec) GovExpeditedProposalsDecorator {
3234
return GovExpeditedProposalsDecorator{
3335
cdc: cdc,
@@ -39,26 +41,56 @@ func NewGovExpeditedProposalsDecorator(cdc codec.BinaryCodec) GovExpeditedPropos
3941
// Legacy proposals submitted using "kiichaind tx gov submit-legacy-proposal" cannot be marked as expedited.
4042
func (g GovExpeditedProposalsDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) {
4143
if expeditedPropDecoratorEnabled {
42-
for _, msg := range tx.GetMsgs() {
43-
prop, ok := msg.(*govv1.MsgSubmitProposal)
44-
if !ok {
45-
continue
44+
if err := g.validateMsgs(tx.GetMsgs()); err != nil {
45+
return ctx, err
46+
}
47+
}
48+
return next(ctx, tx, simulate)
49+
}
50+
51+
// validateMsgs validates each message, unwrapping authz.MsgExec to prevent whitelist bypass.
52+
func (g GovExpeditedProposalsDecorator) validateMsgs(msgs []sdk.Msg) error {
53+
for _, msg := range msgs {
54+
if execMsg, ok := msg.(*authz.MsgExec); ok {
55+
if err := g.validateAuthzExec(execMsg); err != nil {
56+
return err
4657
}
47-
if prop.Expedited {
48-
if err := g.validateExpeditedGovProp(prop); err != nil {
49-
return ctx, err
50-
}
58+
continue
59+
}
60+
61+
prop, ok := msg.(*govv1.MsgSubmitProposal)
62+
if !ok {
63+
continue
64+
}
65+
if prop.Expedited {
66+
if err := g.validateExpeditedGovProp(prop); err != nil {
67+
return err
5168
}
5269
}
5370
}
54-
return next(ctx, tx, simulate)
71+
return nil
72+
}
73+
74+
// validateAuthzExec unpacks the inner messages of an authz.MsgExec and validates them.
75+
func (g GovExpeditedProposalsDecorator) validateAuthzExec(execMsg *authz.MsgExec) error {
76+
innerMsgs := make([]sdk.Msg, 0, len(execMsg.Msgs))
77+
for _, v := range execMsg.Msgs {
78+
var innerMsg sdk.Msg
79+
if err := g.cdc.UnpackAny(v, &innerMsg); err != nil {
80+
return errorsmod.Wrapf(xerrors.ErrInvalidExpeditedProposal, "cannot unmarshal authz exec msg (type %s): %v", v.TypeUrl, err)
81+
}
82+
innerMsgs = append(innerMsgs, innerMsg)
83+
}
84+
return g.validateMsgs(innerMsgs)
5585
}
5686

87+
// isWhitelisted reports whether the given message type may be expedited.
5788
func (g GovExpeditedProposalsDecorator) isWhitelisted(msgType string) bool {
5889
_, ok := expeditedPropsWhitelist[msgType]
5990
return ok
6091
}
6192

93+
// validateExpeditedGovProp ensures every message in an expedited proposal is whitelisted.
6294
func (g GovExpeditedProposalsDecorator) validateExpeditedGovProp(prop *govv1.MsgSubmitProposal) error {
6395
msgs := prop.GetMessages()
6496
if len(msgs) == 0 {

ante/gov_expedited_ante_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
upgradetypes "cosmossdk.io/x/upgrade/types"
1212

1313
sdk "github.com/cosmos/cosmos-sdk/types"
14+
"github.com/cosmos/cosmos-sdk/x/authz"
1415
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
1516
distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types"
1617
govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1"
@@ -163,6 +164,57 @@ func TestGovExpeditedProposalsDecorator(t *testing.T) {
163164
},
164165
expectErr: true,
165166
},
167+
{
168+
name: "authz - expedited whitelisted - MsgSoftwareUpgrade",
169+
ctx: sdk.Context{},
170+
msgs: []sdk.Msg{
171+
newAuthzExec(newGovProp([]sdk.Msg{&upgradetypes.MsgSoftwareUpgrade{
172+
Authority: "kii10d07y265gmmuvt4z0w9aw880jnsr700jrff0qv",
173+
Plan: upgradetypes.Plan{
174+
Name: "upgrade plan-plan",
175+
Info: "some text here",
176+
Height: 123456789,
177+
},
178+
}}, true)),
179+
},
180+
expectErr: false,
181+
},
182+
{
183+
name: "authz - normal non-whitelisted - MsgCommunityPoolSpend",
184+
ctx: sdk.Context{},
185+
msgs: []sdk.Msg{
186+
newAuthzExec(newGovProp([]sdk.Msg{&distrtypes.MsgCommunityPoolSpend{
187+
Authority: "kii10d07y265gmmuvt4z0w9aw880jnsr700jrff0qv",
188+
Recipient: sdk.AccAddress{}.String(),
189+
Amount: sdk.NewCoins(sdk.NewCoin("stake", math.NewInt(100))),
190+
}}, false)),
191+
},
192+
expectErr: false,
193+
},
194+
{
195+
name: "fail - authz - expedited non-whitelisted - MsgCommunityPoolSpend",
196+
ctx: sdk.Context{},
197+
msgs: []sdk.Msg{
198+
newAuthzExec(newGovProp([]sdk.Msg{&distrtypes.MsgCommunityPoolSpend{
199+
Authority: "kii10d07y265gmmuvt4z0w9aw880jnsr700jrff0qv",
200+
Recipient: sdk.AccAddress{}.String(),
201+
Amount: sdk.NewCoins(sdk.NewCoin("stake", math.NewInt(100))),
202+
}}, true)),
203+
},
204+
expectErr: true,
205+
},
206+
{
207+
name: "fail - nested authz - expedited non-whitelisted - MsgSend",
208+
ctx: sdk.Context{},
209+
msgs: []sdk.Msg{
210+
newAuthzExec(newAuthzExec(newGovProp([]sdk.Msg{&banktypes.MsgSend{
211+
FromAddress: "kii10d07y265gmmuvt4z0w9aw880jnsr700jrff0qv",
212+
ToAddress: "kii10d07y265gmmuvt4z0w9aw880jnsr700jrff0qv",
213+
Amount: sdk.NewCoins(sdk.NewCoin("stake", math.NewInt(100))),
214+
}}, true))),
215+
},
216+
expectErr: true,
217+
},
166218
}
167219

168220
for _, tc := range testCases {
@@ -218,3 +270,8 @@ func newGovProp(msgs []sdk.Msg, expedite bool) *govv1.MsgSubmitProposal {
218270
// fmt.Println("### msg ###", msg, "err", err)
219271
return msg
220272
}
273+
274+
func newAuthzExec(msgs ...sdk.Msg) sdk.Msg {
275+
m := authz.NewMsgExec(sdk.AccAddress("expedited-authz-grantee"), msgs)
276+
return &m
277+
}

0 commit comments

Comments
 (0)