From 619ebaf6199612e23413fd6c665a10d95e5ec6a8 Mon Sep 17 00:00:00 2001 From: matteyu Date: Fri, 21 Aug 2026 10:20:32 -0700 Subject: [PATCH 01/15] feat: add v7.3.2 upgrade with evm fork v0.6.2-fork.1 --- CHANGELOG.md | 1 + app/app.go | 2 ++ app/upgrades/v7_3_2/constants.go | 18 ++++++++++++++++ app/upgrades/v7_3_2/upgrade.go | 36 ++++++++++++++++++++++++++++++++ go.mod | 4 ++-- go.sum | 4 ++-- 6 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 app/upgrades/v7_3_2/constants.go create mode 100644 app/upgrades/v7_3_2/upgrade.go diff --git a/CHANGELOG.md b/CHANGELOG.md index b04c5921..b8eb963e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Dependencies +- Bump EVM fork to `v0.6.2-fork.1`, applied via the coordinated `v7.3.2` upgrade (private Cosmos EVM security hotfix; cannot build from public source until disclosure) - [EVM](https://github.com/KiiChain/evm) fork from v0.6.0-fork.1 to [v0.6.0-fork.2](https://github.com/KiiChain/evm/releases/tag/v0.6.0-fork.2): bounded internal EVM call gas limit, EVM fee refunds, distribution precompile 32-byte withdraw fix, and CosmWasm EVM query undercharge fix - [EVM](https://github.com/KiiChain/evm) fork bump from `v0.6.0-fork.2` to [v0.6.1-fork.1](https://github.com/KiiChain/evm/releases/tag/v0.6.1-fork.1), applied via the coordinated `v7.3.0` upgrade: the July 2026 Cosmos EVM hotfix (precompile gas accounting alignment and StateDB locked-balance snapshotting), published upstream as [cosmos/evm v0.6.1](https://github.com/cosmos/evm/releases/tag/v0.6.1) diff --git a/app/app.go b/app/app.go index 5c9fc607..d6f0db01 100644 --- a/app/app.go +++ b/app/app.go @@ -68,6 +68,7 @@ import ( "github.com/kiichain/kiichain/v7/app/keepers" "github.com/kiichain/kiichain/v7/app/upgrades" v7_3_1 "github.com/kiichain/kiichain/v7/app/upgrades/v7_3_1" + v7_3_2 "github.com/kiichain/kiichain/v7/app/upgrades/v7_3_2" "github.com/kiichain/kiichain/v7/client/docs" ) @@ -78,6 +79,7 @@ var ( // Upgrades is a list of all the upgrades that are available for the application. Upgrades = []upgrades.Upgrade{ v7_3_1.Upgrade, + v7_3_2.Upgrade, } ) diff --git a/app/upgrades/v7_3_2/constants.go b/app/upgrades/v7_3_2/constants.go new file mode 100644 index 00000000..8a917016 --- /dev/null +++ b/app/upgrades/v7_3_2/constants.go @@ -0,0 +1,18 @@ +package v732 + +import ( + "github.com/kiichain/kiichain/v7/app/upgrades" +) + +const ( + // UpgradeName is the name of the upgrade + UpgradeName = "v7.3.2" +) + +// Upgrade defines the coordinated upgrade that ships the August 2026 Cosmos EVM +// hotfix. No store migrations are required; the handler only runs pending +// module migrations so validators switch binaries at the same height. +var Upgrade = upgrades.Upgrade{ + UpgradeName: UpgradeName, + CreateUpgradeHandler: CreateUpgradeHandler, +} diff --git a/app/upgrades/v7_3_2/upgrade.go b/app/upgrades/v7_3_2/upgrade.go new file mode 100644 index 00000000..9170ba09 --- /dev/null +++ b/app/upgrades/v7_3_2/upgrade.go @@ -0,0 +1,36 @@ +package v732 + +import ( + "context" + + upgradetypes "cosmossdk.io/x/upgrade/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + + "github.com/kiichain/kiichain/v7/app/keepers" +) + +// CreateUpgradeHandler creates the upgrade handler for the v7.3.2 upgrade. +// This upgrade coordinates the binary switch for the August 2026 Cosmos EVM +// hotfix. No custom state migrations are needed, so the handler only runs +// pending module migrations. +func CreateUpgradeHandler( + mm *module.Manager, + configurator module.Configurator, + _ *keepers.AppKeepers, +) upgradetypes.UpgradeHandler { + return func(c context.Context, _ upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) { + ctx := sdk.UnwrapSDKContext(c) + + ctx.Logger().Info("Starting module migrations for v7.3.2...") + + vm, err := mm.RunMigrations(ctx, configurator, vm) + if err != nil { + return vm, err + } + + ctx.Logger().Info("Upgrade v7.3.2 complete") + return vm, nil + } +} diff --git a/go.mod b/go.mod index b8fdcd6e..d25ed498 100644 --- a/go.mod +++ b/go.mod @@ -311,8 +311,8 @@ replace ( // Use cosmos keyring github.com/99designs/keyring => github.com/cosmos/keyring v1.2.0 - // Use our fork w/ fee abstraction possibility - github.com/cosmos/evm => github.com/KiiChain/evm v0.6.1-fork.1 + // Private August 2026 EVM hotfix; switch back to KiiChain/evm after disclosure + github.com/cosmos/evm => github.com/KiiChain/evm-private v0.6.2-fork.1 // TODO: remove it: https://github.com/cosmos/cosmos-sdk/issues/13134 github.com/dgrijalva/jwt-go => github.com/golang-jwt/jwt/v4 v4.4.2 diff --git a/go.sum b/go.sum index 9d3626f3..5bef793f 100644 --- a/go.sum +++ b/go.sum @@ -705,8 +705,8 @@ github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXY github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= -github.com/KiiChain/evm v0.6.1-fork.1 h1:o6kbWW26YbiSW2NSAIi1Nf1Cty66HvXm60Ki5hzb7Uo= -github.com/KiiChain/evm v0.6.1-fork.1/go.mod h1:QnaJDtxqon2mywiYqxM8VwW8FKeFazi0au0qzVpFAG8= +github.com/KiiChain/evm-private v0.6.2-fork.1 h1:UDQgQsdIYDWHfMueq4HZ7Zcqobf6FuqG4XBC8ahY19s= +github.com/KiiChain/evm-private v0.6.2-fork.1/go.mod h1:QnaJDtxqon2mywiYqxM8VwW8FKeFazi0au0qzVpFAG8= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= From 402521e6babf2670d136cc6d1fb31e73be9e16c4 Mon Sep 17 00:00:00 2001 From: matteyu Date: Fri, 21 Aug 2026 12:43:18 -0700 Subject: [PATCH 02/15] docs: add public release for hotfix --- CHANGELOG.md | 61 +++++++++++++++++++++++++++++++--------------------- go.mod | 3 ++- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8eb963e..0299c635 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,39 @@ ### Dependencies -- Bump EVM fork to `v0.6.2-fork.1`, applied via the coordinated `v7.3.2` upgrade (private Cosmos EVM security hotfix; cannot build from public source until disclosure) +- Bump EVM fork to `v0.6.2-fork.1`, applied via the coordinated `v7.3.2` upgrade (private Cosmos EVM security hotfix; cannot build from public source until 2026-08-28) + +## v7.3.1 - 2026-08-06 + +### Fixed + +- Keep the gov module account on the bank blocked list so EVM BalanceHandler does not mirror gov deposits into StateDB (fixes Safe → gov precompile `deposit` failing on commit with `unauthorized`) ([#368](https://github.com/KiiChain/kiichain/pull/368)) + +## v7.3.0 - 2026-07-28 + +### Dependencies + - [EVM](https://github.com/KiiChain/evm) fork from v0.6.0-fork.1 to [v0.6.0-fork.2](https://github.com/KiiChain/evm/releases/tag/v0.6.0-fork.2): bounded internal EVM call gas limit, EVM fee refunds, distribution precompile 32-byte withdraw fix, and CosmWasm EVM query undercharge fix - [EVM](https://github.com/KiiChain/evm) fork bump from `v0.6.0-fork.2` to [v0.6.1-fork.1](https://github.com/KiiChain/evm/releases/tag/v0.6.1-fork.1), applied via the coordinated `v7.3.0` upgrade: the July 2026 Cosmos EVM hotfix (precompile gas accounting alignment and StateDB locked-balance snapshotting), published upstream as [cosmos/evm v0.6.1](https://github.com/cosmos/evm/releases/tag/v0.6.1) +### Fixed + +- 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 ([#353](https://github.com/KiiChain/kiichain/pull/353)) +- 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 ([#354](https://github.com/KiiChain/kiichain/pull/354)) +- 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 ([#352](https://github.com/KiiChain/kiichain/pull/352)) +- 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 ([#350](https://github.com/KiiChain/kiichain/pull/350)) +- Remove the forced minimum 1-unit-per-block reward release in `CalculateReward` and skip (instead of deactivating) sub-unit blocks in the rewards `BeginBlocker`, so the proportional share accumulates and the pool follows the configured schedule independent of block time ([#347](https://github.com/KiiChain/kiichain/pull/347)) +- Reject `MsgEthereumTx` from being dispatched through the authz keeper (including when nested inside `authz.MsgExec`), closing an EVM ante bypass on message-router execution paths that skip the ante handler ([#342](https://github.com/KiiChain/kiichain/pull/342)) +- Fix feegrant denomination bypass in the cosmos fee ante handler by converting the fee before consuming the grant, so `UseGrantedFees` is checked against the same coins later deducted ([#343](https://github.com/KiiChain/kiichain/pull/343)) +- Bound tokenfactory denom metadata size (`MaxDenomMetadataSize`) in `MsgSetDenomMetadata.ValidateBasic` and `msgServer.SetDenomMetadata` to prevent oversized metadata rewrites from forcing unbounded native store writes ([#341](https://github.com/KiiChain/kiichain/pull/341)) +- Fix native token supply inflation from the stateful precompiles by wrapping the account address codec (`evmAddressCodec`) to reject non-20-byte accounts at decode time ([#340](https://github.com/KiiChain/kiichain/pull/340)) +- Close governance vote minimum-stake bypass in `GovVoteDecorator` by enforcing the stake check on `MsgVoteWeighted` and recursing into nested `authz.MsgExec` messages ([#344](https://github.com/KiiChain/kiichain/pull/344)) +- Prevent a chain halt in the rewards `BeginBlocker` by routing `SendCoinsFromModuleToModule` failures through `haltSchedule` instead of returning a fatal error ([#346](https://github.com/KiiChain/kiichain/pull/346)) +- Add a `ValidateModuleAccounting` check (rewards module bank balance must cover the `CommunityPool`) and run it at genesis +- Fix CosmWasm EVM query path repeatable undercharged EVM execution ([#345](https://github.com/KiiChain/kiichain/pull/345)) + +## v7.2.0 - 2026-04-16 + ### Added - Emit `update_params`, `fund_pool`, `change_schedule`, and `reward_distributed` events from x/rewards @@ -17,16 +46,9 @@ ### Fixed -- 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 -- 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 -- 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 -- 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 -- Remove the forced minimum 1-unit-per-block reward release in `CalculateReward` and skip (instead of deactivating) sub-unit blocks in the rewards `BeginBlocker`, so the proportional share accumulates and the pool follows the configured schedule independent of block time (previously a 10-year, 1M-unit schedule drained in ~12 days at the 1s target block time and ~28 days at the current ~2.4s rate, regardless of the configured duration) -- Reject `MsgEthereumTx` from being dispatched through the authz keeper (including when nested inside `authz.MsgExec`), closing an EVM ante bypass on message-router execution paths that skip the ante handler -- Fix feegrant denomination bypass in the cosmos fee ante handler by converting the fee before consuming the grant, so `UseGrantedFees` is checked against the same coins later deducted (prevents a grantee from forcing the granter to pay in a non-granted fee-abstraction denom) -- Refactor `PerformSetMetadata` in wasmbinding to delegate to `msgServer.SetDenomMetadata`, ensuring the `EnableSetMetadata` capability check is enforced -- Ensure that `UpdateTokenMetadata.Decimals` matches the ERC20 or bank records -- Fixed odd validation on tokenfactory change admin that blocked removing admin from the token +- Refactor `PerformSetMetadata` in wasmbinding to delegate to `msgServer.SetDenomMetadata`, ensuring the `EnableSetMetadata` capability check is enforced ([#329](https://github.com/KiiChain/kiichain/pull/329)) +- Ensure that `UpdateTokenMetadata.Decimals` matches the ERC20 or bank records ([#325](https://github.com/KiiChain/kiichain/pull/325)) +- Fixed odd validation on tokenfactory change admin that blocked removing admin from the token ([#324](https://github.com/KiiChain/kiichain/pull/324)) - Fix division-by-zero chain halt in `CalculateReward` caused by sub-second schedule durations; replace `Seconds()` truncation with `Nanoseconds()` precision and release full remaining reward when `EndTime <= LastReleaseTime` ([#267](https://github.com/KiiChain/kiichain/issues/267)) - Add denom string length validation (max 128 bytes) to oracle precompile and query server to prevent memory exhaustion via oversized inputs - Add result limits to oracle list queries (ExchangeRates, Actives, VoteTargets capped at 1000; PriceSnapshotHistory capped at 500) to prevent unbounded iteration @@ -38,26 +60,15 @@ - Validate rewards baseDenom using sdk.ValidateDenom to enforce proper denom format (min 3 chars, valid characters, no leading digits) - Ensure feeTokens is not nil at genesis - Ensure feeTokenMetadata initial prices after updateFeeTokenMetadata is picked up from oracle -- Use `DecCoins.Validate()` on `RewardPool.ValidateGenesis` to catch malformed denom formats, duplicate denoms, bad ordering +- Use `DecCoins.Validate()` on `RewardPool.ValidateGenesis` to catch malformed denom formats, duplicate denoms, bad ordering ([#323](https://github.com/KiiChain/kiichain/pull/323)) - Enforce denom consistency in `GenesisState.Validate` with `Params.TokenDenom` -- Bound tokenfactory denom metadata size (`MaxDenomMetadataSize`) in `MsgSetDenomMetadata.ValidateBasic` and `msgServer.SetDenomMetadata` to prevent oversized metadata rewrites (including via the CosmWasm binding) from forcing unbounded native store writes that overrun the transaction's declared gas -- Limited tokenfactory queries, removing denial of service possibility -- Indexed admins to reduce query space on tokenfactory denom queries -- Fix native token supply inflation from the stateful precompiles by wrapping the account address codec (`evmAddressCodec`) to reject non-20-byte accounts (e.g. a 32-byte bech32 withdraw, module, or CosmWasm contract address) at decode time, preventing such addresses from being truncated and minted a duplicate balance when mirrored into the EVM StateDB -- Close governance vote minimum-stake bypass in `GovVoteDecorator` by enforcing the stake check on `MsgVoteWeighted` (`govv1` and `govv1beta1`) and recursing into nested `authz.MsgExec` messages so wrapped votes can no longer skip the requirement -- Prevent a chain halt in the rewards `BeginBlocker` by routing `SendCoinsFromModuleToModule` failures through `haltSchedule` (graceful schedule deactivation) instead of returning a fatal error, matching the other reward release error paths -- Add a `ValidateModuleAccounting` check (rewards module bank balance must cover the `CommunityPool`) and run it at genesis to surface accounting/bank divergences early +- Limited tokenfactory queries, removing denial of service possibility ([#328](https://github.com/KiiChain/kiichain/pull/328)) +- Indexed admins to reduce query space on tokenfactory denom queries ([#328](https://github.com/KiiChain/kiichain/pull/328)) ### Removed - Removed price field input in updateTokenMetadata request -## v7.3.1 - 2026-08-06 - -### Fixed - -- Keep the gov module account on the bank blocked list so EVM BalanceHandler does not mirror gov deposits into StateDB (fixes Safe → gov precompile `deposit` failing on commit with `unauthorized`) ([#368](https://github.com/KiiChain/kiichain/pull/368)) - ## v7.1.0-mainnet - 2026-03-13 ### Fixed diff --git a/go.mod b/go.mod index d25ed498..bc7cd969 100644 --- a/go.mod +++ b/go.mod @@ -311,7 +311,8 @@ replace ( // Use cosmos keyring github.com/99designs/keyring => github.com/cosmos/keyring v1.2.0 - // Private August 2026 EVM hotfix; switch back to KiiChain/evm after disclosure + // Private August 2026 EVM hotfix for v7.3.2. + // Switch back to github.com/KiiChain/evm v0.6.2-fork.1 on 2026-08-28 (GHSA public). github.com/cosmos/evm => github.com/KiiChain/evm-private v0.6.2-fork.1 // TODO: remove it: https://github.com/cosmos/cosmos-sdk/issues/13134 From 393491aa3d1f505feeddb9710a50cc24a056a7df Mon Sep 17 00:00:00 2001 From: matteyu Date: Mon, 24 Aug 2026 10:13:49 -0700 Subject: [PATCH 03/15] fix: block vesting account creation in ante --- CHANGELOG.md | 4 ++ ante/ante_cosmos.go | 3 ++ ante/vesting_ante.go | 71 ++++++++++++++++++++++++++++ ante/vesting_ante_test.go | 99 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 177 insertions(+) create mode 100644 ante/vesting_ante.go create mode 100644 ante/vesting_ante_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 0299c635..ace73b70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Fixed + +- Reject `MsgCreateVestingAccount`, `MsgCreatePeriodicVestingAccount`, and `MsgCreatePermanentLockedAccount` in the Cosmos ante (top-level and nested in `authz.MsgExec`) so new vesting / locked accounts cannot be opened after the v7.3.2 upgrade + ### Dependencies - Bump EVM fork to `v0.6.2-fork.1`, applied via the coordinated `v7.3.2` upgrade (private Cosmos EVM security hotfix; cannot build from public source until 2026-08-28) diff --git a/ante/ante_cosmos.go b/ante/ante_cosmos.go index 32a45855..07b5baa0 100644 --- a/ante/ante_cosmos.go +++ b/ante/ante_cosmos.go @@ -29,7 +29,10 @@ func NewCosmosAnteHandler(ctx sdk.Context, options HandlerOptions) sdk.AnteHandl evmcosmosante.NewAuthzLimiterDecorator( // disable the Msg types that cannot be included on an authz.MsgExec msgs field sdk.MsgTypeURL(&evmtypes.MsgEthereumTx{}), sdk.MsgTypeURL(&sdkvesting.MsgCreateVestingAccount{}), + sdk.MsgTypeURL(&sdkvesting.MsgCreatePeriodicVestingAccount{}), + sdk.MsgTypeURL(&sdkvesting.MsgCreatePermanentLockedAccount{}), ), + NewVestingAccountCreationDecorator(options.Cdc), // reject vesting-create msgs at the top level and inside authz ante.NewSetUpContextDecorator(), oracle.NewVoteAloneDecorator(), // Since this only iterate TXs, it must be executed early diff --git a/ante/vesting_ante.go b/ante/vesting_ante.go new file mode 100644 index 00000000..ad83d5de --- /dev/null +++ b/ante/vesting_ante.go @@ -0,0 +1,71 @@ +package ante + +import ( + errorsmod "cosmossdk.io/errors" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkvesting "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" + "github.com/cosmos/cosmos-sdk/x/authz" + + xerrors "github.com/kiichain/kiichain/v7/x/types/errors" +) + +// blockedVestingCreateMsgs is the set of vesting-module messages that open a +// new account with LockedCoins. Those accounts are the book the EVM locked- +// balance snapshot must stay consistent with; new creates are rejected until +// that path is safe. +var blockedVestingCreateMsgs = map[string]struct{}{ + sdk.MsgTypeURL(&sdkvesting.MsgCreateVestingAccount{}): {}, + sdk.MsgTypeURL(&sdkvesting.MsgCreatePeriodicVestingAccount{}): {}, + sdk.MsgTypeURL(&sdkvesting.MsgCreatePermanentLockedAccount{}): {}, +} + +// VestingAccountCreationDecorator rejects messages that create vesting or +// permanently locked accounts, including when nested in authz.MsgExec. +type VestingAccountCreationDecorator struct { + cdc codec.BinaryCodec +} + +// NewVestingAccountCreationDecorator returns a decorator that blocks new +// vesting account creation. +func NewVestingAccountCreationDecorator(cdc codec.BinaryCodec) VestingAccountCreationDecorator { + return VestingAccountCreationDecorator{cdc: cdc} +} + +// AnteHandle rejects blocked vesting-create messages at any authz nesting depth. +func (d VestingAccountCreationDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { + if err := d.validateMsgs(tx.GetMsgs()); err != nil { + return ctx, err + } + return next(ctx, tx, simulate) +} + +func (d VestingAccountCreationDecorator) validateMsgs(msgs []sdk.Msg) error { + for _, msg := range msgs { + if execMsg, ok := msg.(*authz.MsgExec); ok { + if err := d.validateAuthzExec(execMsg); err != nil { + return err + } + continue + } + + typeURL := sdk.MsgTypeURL(msg) + if _, blocked := blockedVestingCreateMsgs[typeURL]; blocked { + return errorsmod.Wrapf(xerrors.ErrUnauthorized, "vesting account creation is disabled: %s", typeURL) + } + } + return nil +} + +func (d VestingAccountCreationDecorator) validateAuthzExec(execMsg *authz.MsgExec) error { + innerMsgs := make([]sdk.Msg, 0, len(execMsg.Msgs)) + for _, v := range execMsg.Msgs { + var innerMsg sdk.Msg + if err := d.cdc.UnpackAny(v, &innerMsg); err != nil { + return errorsmod.Wrapf(xerrors.ErrUnauthorized, "cannot unmarshal authz exec msg (type %s): %v", v.TypeUrl, err) + } + innerMsgs = append(innerMsgs, innerMsg) + } + return d.validateMsgs(innerMsgs) +} diff --git a/ante/vesting_ante_test.go b/ante/vesting_ante_test.go new file mode 100644 index 00000000..9d2fa62f --- /dev/null +++ b/ante/vesting_ante_test.go @@ -0,0 +1,99 @@ +//go:build test + +package ante_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkvesting "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + + "github.com/kiichain/kiichain/v7/ante" + "github.com/kiichain/kiichain/v7/app/helpers" +) + +func TestVestingAccountCreationDecorator(t *testing.T) { + kiiApp := helpers.Setup(t) + from := sdk.AccAddress("from________________") + to := sdk.AccAddress("to__________________") + coins := sdk.NewCoins(sdk.NewCoin("akii", math.NewInt(1))) + + testCases := []struct { + name string + msgs []sdk.Msg + expectErr bool + }{ + { + name: "allow bank send", + msgs: []sdk.Msg{&banktypes.MsgSend{ + FromAddress: from.String(), + ToAddress: to.String(), + Amount: coins, + }}, + expectErr: false, + }, + { + name: "block MsgCreateVestingAccount", + msgs: []sdk.Msg{sdkvesting.NewMsgCreateVestingAccount(from, to, coins, 1, false)}, + expectErr: true, + }, + { + name: "block delayed MsgCreateVestingAccount", + msgs: []sdk.Msg{sdkvesting.NewMsgCreateVestingAccount(from, to, coins, 1, true)}, + expectErr: true, + }, + { + name: "block MsgCreatePeriodicVestingAccount", + msgs: []sdk.Msg{sdkvesting.NewMsgCreatePeriodicVestingAccount(from, to, 1, []sdkvesting.Period{{ + Length: 1, + Amount: coins, + }})}, + expectErr: true, + }, + { + name: "block MsgCreatePermanentLockedAccount", + msgs: []sdk.Msg{sdkvesting.NewMsgCreatePermanentLockedAccount(from, to, coins)}, + expectErr: true, + }, + { + name: "block MsgCreateVestingAccount inside authz.MsgExec", + msgs: []sdk.Msg{newAuthzExec(sdkvesting.NewMsgCreateVestingAccount(from, to, coins, 1, false))}, + expectErr: true, + }, + { + name: "block MsgCreatePeriodicVestingAccount inside nested authz.MsgExec", + msgs: []sdk.Msg{newAuthzExec(newAuthzExec(sdkvesting.NewMsgCreatePeriodicVestingAccount(from, to, 1, []sdkvesting.Period{{Length: 1, Amount: coins}})))}, + expectErr: true, + }, + { + name: "block MsgCreatePermanentLockedAccount inside authz.MsgExec", + msgs: []sdk.Msg{newAuthzExec(sdkvesting.NewMsgCreatePermanentLockedAccount(from, to, coins))}, + expectErr: true, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + txCfg := kiiApp.GetTxConfig() + decorator := ante.NewVestingAccountCreationDecorator(kiiApp.AppCodec()) + + txBuilder := txCfg.NewTxBuilder() + require.NoError(t, txBuilder.SetMsgs(tc.msgs...)) + + _, err := decorator.AnteHandle(sdk.Context{}, txBuilder.GetTx(), false, + func(ctx sdk.Context, _ sdk.Tx, _ bool) (sdk.Context, error) { return ctx, nil }) + if tc.expectErr { + require.Error(t, err) + require.ErrorContains(t, err, "vesting account creation is disabled") + } else { + require.NoError(t, err) + } + }) + } +} From c3b0c7071b6641148d03a770019aa67b27a369c3 Mon Sep 17 00:00:00 2001 From: matteyu Date: Mon, 24 Aug 2026 11:08:35 -0700 Subject: [PATCH 04/15] fix: test --- ..._test.go => vesting_ante_internal_test.go} | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) rename ante/{vesting_ante_test.go => vesting_ante_internal_test.go} (66%) diff --git a/ante/vesting_ante_test.go b/ante/vesting_ante_internal_test.go similarity index 66% rename from ante/vesting_ante_test.go rename to ante/vesting_ante_internal_test.go index 9d2fa62f..274b0f25 100644 --- a/ante/vesting_ante_test.go +++ b/ante/vesting_ante_internal_test.go @@ -1,6 +1,4 @@ -//go:build test - -package ante_test +package ante import ( "testing" @@ -9,20 +7,30 @@ import ( "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" sdkvesting "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" - - "github.com/kiichain/kiichain/v7/ante" - "github.com/kiichain/kiichain/v7/app/helpers" ) func TestVestingAccountCreationDecorator(t *testing.T) { - kiiApp := helpers.Setup(t) + registry := codectypes.NewInterfaceRegistry() + authz.RegisterInterfaces(registry) + sdkvesting.RegisterInterfaces(registry) + banktypes.RegisterInterfaces(registry) + decorator := NewVestingAccountCreationDecorator(codec.NewProtoCodec(registry)) + from := sdk.AccAddress("from________________") to := sdk.AccAddress("to__________________") coins := sdk.NewCoins(sdk.NewCoin("akii", math.NewInt(1))) + exec := func(msgs ...sdk.Msg) sdk.Msg { + m := authz.NewMsgExec(from, msgs) + return &m + } + testCases := []struct { name string msgs []sdk.Msg @@ -35,7 +43,6 @@ func TestVestingAccountCreationDecorator(t *testing.T) { ToAddress: to.String(), Amount: coins, }}, - expectErr: false, }, { name: "block MsgCreateVestingAccount", @@ -62,32 +69,27 @@ func TestVestingAccountCreationDecorator(t *testing.T) { }, { name: "block MsgCreateVestingAccount inside authz.MsgExec", - msgs: []sdk.Msg{newAuthzExec(sdkvesting.NewMsgCreateVestingAccount(from, to, coins, 1, false))}, + msgs: []sdk.Msg{exec(sdkvesting.NewMsgCreateVestingAccount(from, to, coins, 1, false))}, expectErr: true, }, { - name: "block MsgCreatePeriodicVestingAccount inside nested authz.MsgExec", - msgs: []sdk.Msg{newAuthzExec(newAuthzExec(sdkvesting.NewMsgCreatePeriodicVestingAccount(from, to, 1, []sdkvesting.Period{{Length: 1, Amount: coins}})))}, + name: "block MsgCreatePeriodicVestingAccount inside nested authz.MsgExec", + msgs: []sdk.Msg{exec(exec(sdkvesting.NewMsgCreatePeriodicVestingAccount(from, to, 1, []sdkvesting.Period{{ + Length: 1, + Amount: coins, + }})))}, expectErr: true, }, { name: "block MsgCreatePermanentLockedAccount inside authz.MsgExec", - msgs: []sdk.Msg{newAuthzExec(sdkvesting.NewMsgCreatePermanentLockedAccount(from, to, coins))}, + msgs: []sdk.Msg{exec(sdkvesting.NewMsgCreatePermanentLockedAccount(from, to, coins))}, expectErr: true, }, } for _, tc := range testCases { - tc := tc t.Run(tc.name, func(t *testing.T) { - txCfg := kiiApp.GetTxConfig() - decorator := ante.NewVestingAccountCreationDecorator(kiiApp.AppCodec()) - - txBuilder := txCfg.NewTxBuilder() - require.NoError(t, txBuilder.SetMsgs(tc.msgs...)) - - _, err := decorator.AnteHandle(sdk.Context{}, txBuilder.GetTx(), false, - func(ctx sdk.Context, _ sdk.Tx, _ bool) (sdk.Context, error) { return ctx, nil }) + err := decorator.validateMsgs(tc.msgs) if tc.expectErr { require.Error(t, err) require.ErrorContains(t, err, "vesting account creation is disabled") From 66b06bd883f0f80af2f21d4742c38ad538323da4 Mon Sep 17 00:00:00 2001 From: Andres Ramirez Date: Mon, 24 Aug 2026 16:22:04 -0500 Subject: [PATCH 05/15] feat: create v7.4.0 upgrade handler --- app/upgrades/v7_4/constants.go | 21 ++++ app/upgrades/v7_4/upgrade.go | 196 ++++++++++++++++++++++++++++++ app/upgrades/v7_4/upgrade_test.go | 136 +++++++++++++++++++++ 3 files changed, 353 insertions(+) create mode 100644 app/upgrades/v7_4/constants.go create mode 100644 app/upgrades/v7_4/upgrade.go create mode 100644 app/upgrades/v7_4/upgrade_test.go diff --git a/app/upgrades/v7_4/constants.go b/app/upgrades/v7_4/constants.go new file mode 100644 index 00000000..0874bc30 --- /dev/null +++ b/app/upgrades/v7_4/constants.go @@ -0,0 +1,21 @@ +package v740 + +import "github.com/kiichain/kiichain/v7/app/upgrades" + +const ( + // UpgradeName is the on-chain identifier for this emergency upgrade Plan. + UpgradeName = "v7.4.0" + + // UpgradeHeight is the height at which the Plan is scheduled and applied + // within the same PreBlocker pass — the first height produced after the + // manual halt (H+1). Mainnet was halted at height 9355722. + UpgradeHeight = int64(9355723) +) + +// Upgrade registers the emergency fund-recovery handler with x/upgrade. The +// Plan itself is scheduled programmatically from app.PreBlocker (see app.go) +// instead of via governance, so no on-chain vote is required. +var Upgrade = upgrades.Upgrade{ + UpgradeName: UpgradeName, + CreateUpgradeHandler: CreateUpgradeHandler, +} diff --git a/app/upgrades/v7_4/upgrade.go b/app/upgrades/v7_4/upgrade.go new file mode 100644 index 00000000..7fa4fd3b --- /dev/null +++ b/app/upgrades/v7_4/upgrade.go @@ -0,0 +1,196 @@ +package v740 + +import ( + "context" + "fmt" + + "cosmossdk.io/math" + upgradetypes "cosmossdk.io/x/upgrade/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + + "github.com/kiichain/kiichain/v7/app/keepers" +) + +// denom is the chain's native, 18-decimal token denom. +const denom = "akii" + +// attackerAddrs holds the exploited accounts whose full balance must be +// clawed back into stagingAddr before redistribution. +var attackerAddrs = []string{ + "kii1peafvgnleuyl20tyfwnyvtvvwwvnaujxmqe5qe", + "kii1vvwu93nya4ku9yds3v6ns2uq0fsmrnf4cf4yht", + "kii1p3zmn7m6xq82jna6me04p8awt7k4u4k2alwu99", + "kii1zamzjyjcwl0dejjvr90rtrwttxx2zhspqx4sm5", + "kii1zlqdn7706xym7q3k2mdleag0uqjnhv8wu4sfsj", + "kii1rehngnge8qn3ngszw4a8xxf2kqwmact602wtm8", + "kii1y8m0qyc4n3m0rw4rcd7qnqahjh3r7p9uu3ert8", + "kii19p9h2nw2y4fs85sgwgj2qrhhx7jmz6zujldh3n", + "kii183h7rz9p4r8a7j8q2ardnrc7pgwnjp9jvhc8kq", + "kii1gp7ar4hdlqntl5qkerm5n8mfxhqkegm76zqskr", + "kii1gf9a9jjnnv8q3zcr8kczx0r5425zcfgpdw72tt", + "kii1t7gzjh4gsrcuyfx3xdsem05chluqfsa43j9g54", + "kii1wucgj4wxe0zvmmew2000cltc5qrl99eedtrzv4", + "kii1syetlh585kl6yv5hmflhfehla5re7ay4um2skh", + "kii1s7jw5ffqgjfn4ywxtgtq3nhpgcn05z28fsmkhm", + "kii13ndtp734ntzx0jqvr80rlmj62slztqm9agzwce", + "kii13umhqxg56cxwa9wv4gu6l9v4vyz9e70g4hupvn", + "kii156expaxlymu5uhepe2dh647c9lu4slxpyml28q", + "kii1k8vyx8d9ru2hk3k207p3az84xedjxz2gkdyle0", + "kii16tr429kvneexqf4jttueuecm75ptc5l3gtj34q", + "kii1mkhdmdgklsskgcgzz699nzhafav2hkea4qp2dj", + "kii1a5v3eaeaugdh3vk57nlh8q8xcu7z46w0ttlrw9", +} + +// stagingAddr is the chain's "evm" module account. BankKeeper.SendCoins moves balances +// by address and using this address as a plain intermediate +const stagingAddr = "kii1vqu8rska6swzdmnhf90zuv0xmelej4lq5el7zh" + +// remainderAddr receives whatever is left in stagingAddr once every payout +// below has been sent +const remainderAddr = "kii1c6cgjmsx0ewl6j552sp06musutmfcvxcaq4n9h" + +// payout is one redistribution leg out of stagingAddr. +type payout struct { + addr string + amount string +} + +// payouts lists the redistribution from stagingAddr, already in send order +// (the recovery plan requires paying out the last computed amount first and +// the first computed amount last) — distributePayouts just walks it forward. +var payouts = []payout{ + {"kii19c6q309u7c9atnvefqajdjzzjhn82cfcakx4cc", "1023953000000000000000"}, + {"kii14r6lynfqtl6cznllajhms8xy9ce8udnqw73zwz", "366930000000000000000000"}, + {"kii1af3ecamzq3zcllmdahx2sc0gaqxsp0r72h6x6j", "959472000000000000000000"}, + {"kii10jtnkmhlkqnprvng9yenqgr0jtujp0guk3pysp", "982062000000000000000000"}, + {"kii1meq2vy7rlnnceurju0uz9qeshfaaun0x5xsg06", "992286000000000000000000"}, + {"kii196ceqskhhyj93hejczj6py8f0am7vs3fykark7", "1004832000000000000000000"}, + {"kii1rt7arm6ckp0lcfuq9r0fmyl22urdcyfgfer35g", "1022706000000000000000000"}, + {"kii18fqfr2j7v96xy4lggvaca5cef586jp7pry27v0", "1038420000000000000000000"}, + {"kii1ehfns3qwnuhlunkhk5l2d0la8d8erenjn0482a", "1115262000000000000000000"}, + {"kii18ufxrsncyegu9qactah4hzrn0xmqqlkxr6p3z5", "1116000000000000000000000"}, + {"kii1qqyn3zg7g648pwc46y0depq8f82rj9400ulj4g", "1116000000000000000000000"}, + {"kii13u7hu5lscvdc2yqg0x5t2qj27m8fj8jw4ez046", "3247115883451428571420551"}, + {"kii1fc80es03yhle3xjpqp8e8pezl7an65h50fl2pm", "4496107929952857142857255"}, + {"kii1syezrzevu6ycshvgtm4sxtreh3pxvk0mtfe6rd", "5139906382822857142856976"}, + {"kii106tcwjead6wdj9xegyes80vfxd4da6sr4f5npu", "9000001000000000000000000"}, + {"kii1n4mskp6c83rzvl9eraddqdgwuqt6zec46qv06q", "36000001000000000000000000"}, +} + +// CreateUpgradeHandler creates the handler for the emergency fund recovery. +// It runs when the Plan scheduled from app.PreBlocker (see app.go) is applied +// by x/upgrade's own PreBlocker, in that same block +func CreateUpgradeHandler( + mm *module.Manager, + configurator module.Configurator, + k *keepers.AppKeepers, +) upgradetypes.UpgradeHandler { + return func(c context.Context, _ upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) { + ctx := sdk.UnwrapSDKContext(c) + ctx.Logger().Info("EMERGENCY FIX: starting funds recovery", "height", ctx.BlockHeight()) + + if err := recoverFunds(ctx, k); err != nil { + panic(fmt.Errorf("emergency fix failed: %w", err)) + } + + ctx.Logger().Info("EMERGENCY FIX: funds recovery completed successfully", "height", ctx.BlockHeight()) + + vm, err := mm.RunMigrations(ctx, configurator, vm) + if err != nil { + return vm, err + } + + // Log the upgrade completion + ctx.Logger().Info("Upgrade v7.2.0 complete") + return vm, nil + } +} + +// recoverFunds runs the three-stage recovery: sweep the exploited wallets +// into stagingAddr, pay out the computed amounts from stagingAddr, then send +// whatever remains in stagingAddr to remainderAddr. Each stage must finish +// before the next reads stagingAddr's balance, which holds here because all +// three run sequentially against the same, still-uncommitted block context +func recoverFunds(ctx sdk.Context, k *keepers.AppKeepers) error { + staging, err := sdk.AccAddressFromBech32(stagingAddr) + if err != nil { + return fmt.Errorf("invalid staging address: %w", err) + } + + if err := sweepAttackerFunds(ctx, k, staging); err != nil { + return err + } + + if err := distributePayouts(ctx, k, staging); err != nil { + return err + } + + return sweepRemainder(ctx, k, staging) +} + +// sweepAttackerFunds moves every attacker wallet's full balance into staging +// These are plain accounts/contracts, not vesting accounts, so a direct +// bank transfer is all that's needed +func sweepAttackerFunds(ctx sdk.Context, k *keepers.AppKeepers, staging sdk.AccAddress) error { + for _, addrStr := range attackerAddrs { + attackerAddr, err := sdk.AccAddressFromBech32(addrStr) + if err != nil { + return fmt.Errorf("invalid attacker address %s: %w", addrStr, err) + } + + balance := k.BankKeeper.GetAllBalances(ctx, attackerAddr) + if balance.IsZero() { + continue + } + if err := k.BankKeeper.SendCoins(ctx, attackerAddr, staging, balance); err != nil { + return fmt.Errorf("sweep from %s: %w", addrStr, err) + } + ctx.Logger().Info("emergency-fix: swept to staging", "addr", addrStr, "amount", balance.String()) + } + + return nil +} + +// distributePayouts sends each payout out of staging, in the order listed +func distributePayouts(ctx sdk.Context, k *keepers.AppKeepers, staging sdk.AccAddress) error { + for _, p := range payouts { + recipient, err := sdk.AccAddressFromBech32(p.addr) + if err != nil { + return fmt.Errorf("invalid payout address %s: %w", p.addr, err) + } + + amount, ok := math.NewIntFromString(p.amount) + if !ok { + return fmt.Errorf("invalid payout amount %q for %s", p.amount, p.addr) + } + + coins := sdk.NewCoins(sdk.NewCoin(denom, amount)) + if err := k.BankKeeper.SendCoins(ctx, staging, recipient, coins); err != nil { + return fmt.Errorf("payout to %s: %w", p.addr, err) + } + ctx.Logger().Info("emergency-fix: payout sent", "addr", p.addr, "amount", coins.String()) + } + + return nil +} + +// sweepRemainder sends whatever is left in staging to remainderAddr +func sweepRemainder(ctx sdk.Context, k *keepers.AppKeepers, staging sdk.AccAddress) error { + remainder, err := sdk.AccAddressFromBech32(remainderAddr) + if err != nil { + return fmt.Errorf("invalid remainder address: %w", err) + } + + balance := k.BankKeeper.GetAllBalances(ctx, staging) + if balance.IsZero() { + return nil + } + if err := k.BankKeeper.SendCoins(ctx, staging, remainder, balance); err != nil { + return fmt.Errorf("remainder sweep: %w", err) + } + ctx.Logger().Info("emergency-fix: remainder swept", "amount", balance.String()) + + return nil +} diff --git a/app/upgrades/v7_4/upgrade_test.go b/app/upgrades/v7_4/upgrade_test.go new file mode 100644 index 00000000..a6237f34 --- /dev/null +++ b/app/upgrades/v7_4/upgrade_test.go @@ -0,0 +1,136 @@ +package v740_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "cosmossdk.io/math" + upgradetypes "cosmossdk.io/x/upgrade/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + + kiichain "github.com/kiichain/kiichain/v7/app" + kiihelpers "github.com/kiichain/kiichain/v7/app/helpers" + v740 "github.com/kiichain/kiichain/v7/app/upgrades/v7_4" + tokenfactorytypes "github.com/kiichain/kiichain/v7/x/tokenfactory/types" +) + +const denom = "akii" + +// attacker1 and attacker2 are two of the real, hardcoded attackerAddrs from +// v7_4's upgrade.go. stagingAddr and remainderAddr mirror its hardcoded +// constants of the same name. +const ( + attacker1 = "kii1peafvgnleuyl20tyfwnyvtvvwwvnaujxmqe5qe" + attacker2 = "kii1a5v3eaeaugdh3vk57nlh8q8xcu7z46w0ttlrw9" + stagingAddr = "kii1vqu8rska6swzdmnhf90zuv0xmelej4lq5el7zh" + remainderAddr = "kii1c6cgjmsx0ewl6j552sp06musutmfcvxcaq4n9h" +) + +// payouts mirrors v7_4's hardcoded redistribution list exactly, so the test +// can fund staging with precisely enough to cover it and verify every leg. +var payouts = []struct { + addr string + amount string +}{ + {"kii19c6q309u7c9atnvefqajdjzzjhn82cfcakx4cc", "1023953000000000000000"}, + {"kii14r6lynfqtl6cznllajhms8xy9ce8udnqw73zwz", "366930000000000000000000"}, + {"kii1af3ecamzq3zcllmdahx2sc0gaqxsp0r72h6x6j", "959472000000000000000000"}, + {"kii10jtnkmhlkqnprvng9yenqgr0jtujp0guk3pysp", "982062000000000000000000"}, + {"kii1meq2vy7rlnnceurju0uz9qeshfaaun0x5xsg06", "992286000000000000000000"}, + {"kii196ceqskhhyj93hejczj6py8f0am7vs3fykark7", "1004832000000000000000000"}, + {"kii1rt7arm6ckp0lcfuq9r0fmyl22urdcyfgfer35g", "1022706000000000000000000"}, + {"kii18fqfr2j7v96xy4lggvaca5cef586jp7pry27v0", "1038420000000000000000000"}, + {"kii1ehfns3qwnuhlunkhk5l2d0la8d8erenjn0482a", "1115262000000000000000000"}, + {"kii18ufxrsncyegu9qactah4hzrn0xmqqlkxr6p3z5", "1116000000000000000000000"}, + {"kii1qqyn3zg7g648pwc46y0depq8f82rj9400ulj4g", "1116000000000000000000000"}, + {"kii13u7hu5lscvdc2yqg0x5t2qj27m8fj8jw4ez046", "3247115883451428571420551"}, + {"kii1fc80es03yhle3xjpqp8e8pezl7an65h50fl2pm", "4496107929952857142857255"}, + {"kii1syezrzevu6ycshvgtm4sxtreh3pxvk0mtfe6rd", "5139906382822857142856976"}, + {"kii106tcwjead6wdj9xegyes80vfxd4da6sr4f5npu", "9000001000000000000000000"}, + {"kii1n4mskp6c83rzvl9eraddqdgwuqt6zec46qv06q", "36000001000000000000000000"}, +} + +// fund mints coins via tokenfactory (the same source app/apptesting's +// FundAcc helper uses) and sends them to addr. +func fund(t *testing.T, app *kiichain.KiichainApp, ctx sdk.Context, addr string, amount math.Int) { + t.Helper() + coins := sdk.NewCoins(sdk.NewCoin(denom, amount)) + require.NoError(t, app.BankKeeper.MintCoins(ctx, tokenfactorytypes.ModuleName, coins)) + require.NoError(t, app.BankKeeper.SendCoinsFromModuleToAccount(ctx, tokenfactorytypes.ModuleName, sdk.MustAccAddressFromBech32(addr), coins)) +} + +// totalPayouts sums payouts using math.Int so the huge amounts aren't +// hand-added (and hand-added wrong). +func totalPayouts(t *testing.T) math.Int { + t.Helper() + total := math.ZeroInt() + for _, p := range payouts { + amt, ok := math.NewIntFromString(p.amount) + require.True(t, ok, "bad test fixture amount %q", p.amount) + total = total.Add(amt) + } + return total +} + +// TestCreateUpgradeHandler_RecoversAndRedistributesFunds runs the full +// three-stage recovery: sweep two attacker wallets into the evm module +// account (stagingAddr), pay out the fixed redistribution list, then sweep +// whatever remains to remainderAddr. +func TestCreateUpgradeHandler_RecoversAndRedistributesFunds(t *testing.T) { + app, ctx := kiihelpers.SetupWithContext(t) + + remainder := math.NewIntWithDecimal(500, 18) // 500 KII left over, on purpose + total := totalPayouts(t).Add(remainder) + + // Split the funding across two real attacker addresses to exercise the + // sweep loop over more than one account. + attacker2Amount := math.NewIntWithDecimal(1, 18) // 1 KII + attacker1Amount := total.Sub(attacker2Amount) + + fund(t, app, ctx, attacker1, attacker1Amount) + fund(t, app, ctx, attacker2, attacker2Amount) + + mm := app.GetModuleManager() + handler := v740.CreateUpgradeHandler(mm, app.GetConfigurator(), &app.AppKeepers) + vm, err := handler(ctx, upgradetypes.Plan{Name: v740.UpgradeName}, mm.GetVersionMap()) + require.NoError(t, err) + require.NotNil(t, vm) + + // Attacker wallets end up empty. + require.True(t, app.BankKeeper.GetAllBalances(ctx, sdk.MustAccAddressFromBech32(attacker1)).IsZero()) + require.True(t, app.BankKeeper.GetAllBalances(ctx, sdk.MustAccAddressFromBech32(attacker2)).IsZero()) + + // Staging (the evm module account) passes everything through. + require.True(t, app.BankKeeper.GetAllBalances(ctx, sdk.MustAccAddressFromBech32(stagingAddr)).IsZero()) + + // Every payout landed exactly as specified. + for _, p := range payouts { + expected, ok := math.NewIntFromString(p.amount) + require.True(t, ok) + got := app.BankKeeper.GetBalance(ctx, sdk.MustAccAddressFromBech32(p.addr), denom) + require.Equal(t, expected.String(), got.Amount.String(), "payout mismatch for %s", p.addr) + } + + // Whatever was left over went to the remainder address. + gotRemainder := app.BankKeeper.GetBalance(ctx, sdk.MustAccAddressFromBech32(remainderAddr), denom) + require.Equal(t, remainder.String(), gotRemainder.Amount.String()) +} + +// TestCreateUpgradeHandler_PanicsWhenStagingCannotCoverPayouts verifies the +// handler fails closed: if the swept balance can't cover the fixed payout +// list, it panics instead of sending partial or incorrect amounts. +func TestCreateUpgradeHandler_PanicsWhenStagingCannotCoverPayouts(t *testing.T) { + app, ctx := kiihelpers.SetupWithContext(t) + + // Fund the attacker with far less than the fixed payout list requires. + fund(t, app, ctx, attacker1, math.OneInt()) + + mm := app.GetModuleManager() + handler := v740.CreateUpgradeHandler(mm, app.GetConfigurator(), &app.AppKeepers) + + require.Panics(t, func() { + _, _ = handler(ctx, upgradetypes.Plan{Name: v740.UpgradeName}, mm.GetVersionMap()) + }) +} From 3cd9fbb0ae0a8c12b8438fd785e49c0b535d0854 Mon Sep 17 00:00:00 2001 From: Andres Ramirez Date: Mon, 24 Aug 2026 16:22:27 -0500 Subject: [PATCH 06/15] feat: add upgrade plan v7.4.0 and update preblocker --- app/app.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/app.go b/app/app.go index d6f0db01..1fb372a3 100644 --- a/app/app.go +++ b/app/app.go @@ -69,6 +69,7 @@ import ( "github.com/kiichain/kiichain/v7/app/upgrades" v7_3_1 "github.com/kiichain/kiichain/v7/app/upgrades/v7_3_1" v7_3_2 "github.com/kiichain/kiichain/v7/app/upgrades/v7_3_2" + v7_4_0 "github.com/kiichain/kiichain/v7/app/upgrades/v7_4" "github.com/kiichain/kiichain/v7/client/docs" ) @@ -80,6 +81,7 @@ var ( Upgrades = []upgrades.Upgrade{ v7_3_1.Upgrade, v7_3_2.Upgrade, + v7_4_0.Upgrade, } ) @@ -362,6 +364,18 @@ func (app *KiichainApp) Name() string { return app.BaseApp.Name() } // PreBlocker application updates every pre block func (app *KiichainApp) PreBlocker(ctx sdk.Context, _ *abci.RequestFinalizeBlock) (*sdk.ResponsePreBlock, error) { + if ctx.BlockHeight() == v7_4_0.UpgradeHeight { + if _, err := app.UpgradeKeeper.GetUpgradePlan(ctx); err != nil { + plan := upgradetypes.Plan{ + Name: v7_4_0.UpgradeName, + Height: ctx.BlockHeight(), + Info: "emergency fund recovery post-exploit", + } + if err := app.UpgradeKeeper.ScheduleUpgrade(ctx, plan); err != nil { + panic(fmt.Errorf("failed to schedule emergency upgrade: %w", err)) + } + } + } return app.mm.PreBlock(ctx) } From 9fbfad9d4835d01d9757e5e9bc48fedc7dacb25c Mon Sep 17 00:00:00 2001 From: Andres Ramirez Date: Mon, 24 Aug 2026 16:48:54 -0500 Subject: [PATCH 07/15] chore: add validations for the upgrade to be done only on mainnet --- app/app.go | 2 +- app/upgrades/v7_4/constants.go | 10 ++++++++-- app/upgrades/v7_4/upgrade.go | 16 ++++++++++------ app/upgrades/v7_4/upgrade_test.go | 26 ++++++++++++++++++++++++++ 4 files changed, 45 insertions(+), 9 deletions(-) diff --git a/app/app.go b/app/app.go index 1fb372a3..af8fcd3a 100644 --- a/app/app.go +++ b/app/app.go @@ -364,7 +364,7 @@ func (app *KiichainApp) Name() string { return app.BaseApp.Name() } // PreBlocker application updates every pre block func (app *KiichainApp) PreBlocker(ctx sdk.Context, _ *abci.RequestFinalizeBlock) (*sdk.ResponsePreBlock, error) { - if ctx.BlockHeight() == v7_4_0.UpgradeHeight { + if ctx.BlockHeight() == v7_4_0.UpgradeHeight && ctx.ChainID() == v7_4_0.MainnetChainID { if _, err := app.UpgradeKeeper.GetUpgradePlan(ctx); err != nil { plan := upgradetypes.Plan{ Name: v7_4_0.UpgradeName, diff --git a/app/upgrades/v7_4/constants.go b/app/upgrades/v7_4/constants.go index 0874bc30..5f1626dc 100644 --- a/app/upgrades/v7_4/constants.go +++ b/app/upgrades/v7_4/constants.go @@ -8,8 +8,14 @@ const ( // UpgradeHeight is the height at which the Plan is scheduled and applied // within the same PreBlocker pass — the first height produced after the - // manual halt (H+1). Mainnet was halted at height 9355722. - UpgradeHeight = int64(9355723) + // manual halt (H+1). Mainnet's last committed height before the halt was + // 9355723, per `latest_block_height` on the halted node's RPC. + UpgradeHeight = int64(9355724) + + // MainnetChainID is the only chain-id this emergency upgrade is allowed to + // move funds on, confirmed via the halted mainnet node's own RPC status + // ("network": "kiichain_1783-1"). + MainnetChainID = "kiichain_1783-1" ) // Upgrade registers the emergency fund-recovery handler with x/upgrade. The diff --git a/app/upgrades/v7_4/upgrade.go b/app/upgrades/v7_4/upgrade.go index 7fa4fd3b..d29c3e46 100644 --- a/app/upgrades/v7_4/upgrade.go +++ b/app/upgrades/v7_4/upgrade.go @@ -112,8 +112,12 @@ func CreateUpgradeHandler( // into stagingAddr, pay out the computed amounts from stagingAddr, then send // whatever remains in stagingAddr to remainderAddr. Each stage must finish // before the next reads stagingAddr's balance, which holds here because all -// three run sequentially against the same, still-uncommitted block context +// three run sequentially against the same, still-uncommitted block context. func recoverFunds(ctx sdk.Context, k *keepers.AppKeepers) error { + if ctx.ChainID() != MainnetChainID { + return fmt.Errorf("refusing to move funds: chain-id %q is not mainnet (%q)", ctx.ChainID(), MainnetChainID) + } + staging, err := sdk.AccAddressFromBech32(stagingAddr) if err != nil { return fmt.Errorf("invalid staging address: %w", err) @@ -127,7 +131,7 @@ func recoverFunds(ctx sdk.Context, k *keepers.AppKeepers) error { return err } - return sweepRemainder(ctx, k, staging) + return distributeRemainder(ctx, k, staging) } // sweepAttackerFunds moves every attacker wallet's full balance into staging @@ -176,8 +180,8 @@ func distributePayouts(ctx sdk.Context, k *keepers.AppKeepers, staging sdk.AccAd return nil } -// sweepRemainder sends whatever is left in staging to remainderAddr -func sweepRemainder(ctx sdk.Context, k *keepers.AppKeepers, staging sdk.AccAddress) error { +// distributeRemainder sends whatever is left in staging to remainderAddr. +func distributeRemainder(ctx sdk.Context, k *keepers.AppKeepers, staging sdk.AccAddress) error { remainder, err := sdk.AccAddressFromBech32(remainderAddr) if err != nil { return fmt.Errorf("invalid remainder address: %w", err) @@ -188,9 +192,9 @@ func sweepRemainder(ctx sdk.Context, k *keepers.AppKeepers, staging sdk.AccAddre return nil } if err := k.BankKeeper.SendCoins(ctx, staging, remainder, balance); err != nil { - return fmt.Errorf("remainder sweep: %w", err) + return fmt.Errorf("remainder distribution: %w", err) } - ctx.Logger().Info("emergency-fix: remainder swept", "amount", balance.String()) + ctx.Logger().Info("emergency-fix: remainder distributed", "amount", balance.String()) return nil } diff --git a/app/upgrades/v7_4/upgrade_test.go b/app/upgrades/v7_4/upgrade_test.go index a6237f34..09edbc45 100644 --- a/app/upgrades/v7_4/upgrade_test.go +++ b/app/upgrades/v7_4/upgrade_test.go @@ -80,6 +80,7 @@ func totalPayouts(t *testing.T) math.Int { // whatever remains to remainderAddr. func TestCreateUpgradeHandler_RecoversAndRedistributesFunds(t *testing.T) { app, ctx := kiihelpers.SetupWithContext(t) + ctx = ctx.WithChainID(v740.MainnetChainID) remainder := math.NewIntWithDecimal(500, 18) // 500 KII left over, on purpose total := totalPayouts(t).Add(remainder) @@ -123,6 +124,7 @@ func TestCreateUpgradeHandler_RecoversAndRedistributesFunds(t *testing.T) { // list, it panics instead of sending partial or incorrect amounts. func TestCreateUpgradeHandler_PanicsWhenStagingCannotCoverPayouts(t *testing.T) { app, ctx := kiihelpers.SetupWithContext(t) + ctx = ctx.WithChainID(v740.MainnetChainID) // Fund the attacker with far less than the fixed payout list requires. fund(t, app, ctx, attacker1, math.OneInt()) @@ -134,3 +136,27 @@ func TestCreateUpgradeHandler_PanicsWhenStagingCannotCoverPayouts(t *testing.T) _, _ = handler(ctx, upgradetypes.Plan{Name: v740.UpgradeName}, mm.GetVersionMap()) }) } + +// TestCreateUpgradeHandler_PanicsWhenNotMainnet verifies the second, +// independent chain-id guard inside recoverFunds: even with staging funded +// generously enough to cover every payout, the handler must still refuse to +// move funds on a non-mainnet chain-id. +func TestCreateUpgradeHandler_PanicsWhenNotMainnet(t *testing.T) { + app, ctx := kiihelpers.SetupWithContext(t) // default test chain-id, not v740.MainnetChainID + + fund(t, app, ctx, attacker1, totalPayouts(t).Add(math.NewIntWithDecimal(1, 18))) + + mm := app.GetModuleManager() + handler := v740.CreateUpgradeHandler(mm, app.GetConfigurator(), &app.AppKeepers) + + defer func() { + r := recover() + require.NotNil(t, r, "expected a panic") + err, ok := r.(error) + require.True(t, ok, "panic value should be an error, got %T", r) + require.Contains(t, err.Error(), "not mainnet") + }() + + _, _ = handler(ctx, upgradetypes.Plan{Name: v740.UpgradeName}, mm.GetVersionMap()) + t.Fatal("expected handler to panic") +} From a0b14cf1c8a1b666dcc156bfa64f8e740abdba91 Mon Sep 17 00:00:00 2001 From: matteyu Date: Mon, 24 Aug 2026 11:29:26 -0700 Subject: [PATCH 08/15] feat: add blacklist for block address --- CHANGELOG.md | 2 + ante/ante_cosmos.go | 1 + ante/ante_evm.go | 1 + ante/blocked_addrs.go | 92 +++++++++++++++++++++ ante/blocked_addrs_ante.go | 122 ++++++++++++++++++++++++++++ ante/blocked_addrs_internal_test.go | 102 +++++++++++++++++++++++ app/app.go | 3 + app/blocked_addrs_proposal.go | 76 +++++++++++++++++ app/evm_mempool.go | 4 +- 9 files changed, 402 insertions(+), 1 deletion(-) create mode 100644 ante/blocked_addrs.go create mode 100644 ante/blocked_addrs_ante.go create mode 100644 ante/blocked_addrs_internal_test.go create mode 100644 app/blocked_addrs_proposal.go diff --git a/CHANGELOG.md b/CHANGELOG.md index ace73b70..06be25bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ### Fixed - Reject `MsgCreateVestingAccount`, `MsgCreatePeriodicVestingAccount`, and `MsgCreatePermanentLockedAccount` in the Cosmos ante (top-level and nested in `authz.MsgExec`) so new vesting / locked accounts cannot be opened after the v7.3.2 upgrade +- Block the 22 Aug 2026 incident addresses in Cosmos/EVM ante (and strip/reject them in Prepare/ProcessProposal) so they cannot send, deploy, or be called after the v7.3.2 restart + ### Dependencies diff --git a/ante/ante_cosmos.go b/ante/ante_cosmos.go index 07b5baa0..53dbce32 100644 --- a/ante/ante_cosmos.go +++ b/ante/ante_cosmos.go @@ -33,6 +33,7 @@ func NewCosmosAnteHandler(ctx sdk.Context, options HandlerOptions) sdk.AnteHandl sdk.MsgTypeURL(&sdkvesting.MsgCreatePermanentLockedAccount{}), ), NewVestingAccountCreationDecorator(options.Cdc), // reject vesting-create msgs at the top level and inside authz + NewBlockedAddrDecorator(options.Cdc), // reject incident addrs (signer / bank / nested authz) ante.NewSetUpContextDecorator(), oracle.NewVoteAloneDecorator(), // Since this only iterate TXs, it must be executed early diff --git a/ante/ante_evm.go b/ante/ante_evm.go index 52228bea..d8033af2 100644 --- a/ante/ante_evm.go +++ b/ante/ante_evm.go @@ -13,6 +13,7 @@ func newMonoEVMAnteHandler(ctx sdk.Context, options HandlerOptions) sdk.AnteHand evmParams := options.EvmKeeper.GetParams(ctx) feemarketParams := options.FeeMarketKeeper.GetParams(ctx) decorators := []sdk.AnteDecorator{ + NewBlockedAddrDecorator(options.Cdc), // reject incident addrs before EVM execution kiievmante.NewEVMMonoDecorator( options.AccountKeeper, options.FeeMarketKeeper, diff --git a/ante/blocked_addrs.go b/ante/blocked_addrs.go new file mode 100644 index 00000000..2bb3203e --- /dev/null +++ b/ante/blocked_addrs.go @@ -0,0 +1,92 @@ +package ante + +import ( + "encoding/hex" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// blockedAddrs is the incident deny list (hex + bech32). Normalized to +// lowercase at init. Live in the binary so it applies on the first block +// after the v7.3.2 swap, including a tx already packed in that block. +var blockedAddrs = map[string]struct{}{} + +var blockedAddrPairs = [][2]string{ + {"0x0e7a96227fcf09f53d644ba6462d8c73993ef246", "kii1peafvgnleuyl20tyfwnyvtvvwwvnaujxmqe5qe"}, + {"0x631dc2c664ed6dc291b08b35382b807a61b1cd35", "kii1vvwu93nya4ku9yds3v6ns2uq0fsmrnf4cf4yht"}, + {"0x0c45b9fb7a300ea94fbade5f509fae5fad5e56ca", "kii1p3zmn7m6xq82jna6me04p8awt7k4u4k2alwu99"}, + {"0x177629125877dedcca4c195e358dcb598ca15e01", "kii1zamzjyjcwl0dejjvr90rtrwttxx2zhspqx4sm5"}, + {"0x17c0d9fbcfd189bf023656dbfcf50fe0253bb0ee", "kii1zlqdn7706xym7q3k2mdleag0uqjnhv8wu4sfsj"}, + {"0x1e6f344d19382719a202757a73192ab01dbee17a", "kii1rehngnge8qn3ngszw4a8xxf2kqwmact602wtm8"}, + {"0x21f6f013159c76f1baa3c37c0983b795e23f04bc", "kii1y8m0qyc4n3m0rw4rcd7qnqahjh3r7p9uu3ert8"}, + {"0x284b754dca255303d2087224a00ef737a5b1685c", "kii19p9h2nw2y4fs85sgwgj2qrhhx7jmz6zujldh3n"}, + {"0x3c6fe188a1a8cfdf48e05746d98f1e0a1d3904b2", "kii183h7rz9p4r8a7j8q2ardnrc7pgwnjp9jvhc8kq"}, + {"0x407dd1d6edf826bfd016c8f7499f6935c16ca37e", "kii1gp7ar4hdlqntl5qkerm5n8mfxhqkegm76zqskr"}, + {"0x424bd2ca539b0e088b033db0233c74aaa82c2501", "kii1gf9a9jjnnv8q3zcr8kczx0r5425zcfgpdw72tt"}, + {"0x5f90295ea880f1c224d133619dbe98bff804c3b5", "kii1t7gzjh4gsrcuyfx3xdsem05chluqfsa43j9g54"}, + {"0x77308955c6cbc4cdef2e53defc7d78a007f29739", "kii1wucgj4wxe0zvmmew2000cltc5qrl99eedtrzv4"}, + {"0x8132bfde87a5bfa23297da7f74e6ffed079f7495", "kii1syetlh585kl6yv5hmflhfehla5re7ay4um2skh"}, + {"0x87a4ea252044933a91c65a1608cee14626fa0947", "kii1s7jw5ffqgjfn4ywxtgtq3nhpgcn05z28fsmkhm"}, + {"0x8cdab0fa359ac467c80c19de3fee5a543e258365", "kii13ndtp734ntzx0jqvr80rlmj62slztqm9agzwce"}, + {"0x8f37701914d60cee95ccaa39af959561045cf9e8", "kii13umhqxg56cxwa9wv4gu6l9v4vyz9e70g4hupvn"}, + {"0xa6b260f4df26f94e5f21ca9b7d57d82ff9587cc1", "kii156expaxlymu5uhepe2dh647c9lu4slxpyml28q"}, + {"0xb1d8431da51f157b46ca7f831e88f5365b230948", "kii1k8vyx8d9ru2hk3k207p3az84xedjxz2gkdyle0"}, + {"0xd2c75516cc9e726026b25af99e671bf502bc53f1", "kii16tr429kvneexqf4jttueuecm75ptc5l3gtj34q"}, + {"0xddaeddb516fc21646102168a598afd4f58abdb3d", "kii1mkhdmdgklsskgcgzz699nzhafav2hkea4qp2dj"}, + {"0xed191cf73de21b78b2d4f4ff7380e6c73c2ae9cf", "kii1a5v3eaeaugdh3vk57nlh8q8xcu7z46w0ttlrw9"}, + {"0x603871c2ddd41c26ee77495e2e31e6de7f9957e0", "kii1vqu8rska6swzdmnhf90zuv0xmelej4lq5el7zh"}, + {"0xc6b0896e067e5dfd4a945402fd6f90e2f69c30d8", "kii1c6cgjmsx0ewl6j552sp06musutmfcvxcaq4n9h"}, + {"0x9d770b07583c46267cb91f5ad0350ee017a16715", "kii1n4mskp6c83rzvl9eraddqdgwuqt6zec46qv06q"}, + {"0x7e97874b3d6e9cd914d9413303bd89336adeea03", "kii106tcwjead6wdj9xegyes80vfxd4da6sr4f5npu"}, + {"0x8132218b2ce689885d885eeb032c79bc426659fb", "kii1syezrzevu6ycshvgtm4sxtreh3pxvk0mtfe6rd"}, + {"0x4e0efcc1f125ff989a41004f938722ffbb3d52f4", "kii1fc80es03yhle3xjpqp8e8pezl7an65h50fl2pm"}, + {"0x8f3d7e53f0c31b85100879a8b5024af6ce991e4e", "kii13u7hu5lscvdc2yqg0x5t2qj27m8fj8jw4ez046"}, + {"0x000938891e46aa70bb15d11edc840749d43916af", "kii1qqyn3zg7g648pwc46y0depq8f82rj9400ulj4g"}, + {"0x3f1261c2782651c283b85f6f5b887379b6007ec6", "kii18ufxrsncyegu9qactah4hzrn0xmqqlkxr6p3z5"}, + {"0xcdd338440e9f2ffe4ed7b53ea6bffd3b4f91e672", "kii1ehfns3qwnuhlunkhk5l2d0la8d8erenjn0482a"}, + {"0x3a4091aa5e61746257e8433b8ed3194d0fa907c1", "kii18fqfr2j7v96xy4lggvaca5cef586jp7pry27v0"}, + {"0x1afdd1ef58b05ffc278028de9d93ea5706dc1128", "kii1rt7arm6ckp0lcfuq9r0fmyl22urdcyfgfer35g"}, + {"0x2eb19042d7b92458df32c0a5a090e97f77e64229", "kii196ceqskhhyj93hejczj6py8f0am7vs3fykark7"}, + {"0xde40a613c3fce78cf072e3f8228330ba7bde4de6", "kii1meq2vy7rlnnceurju0uz9qeshfaaun0x5xsg06"}, + {"0x7c973b6effb02611b268293330206f92f920bd1c", "kii10jtnkmhlkqnprvng9yenqgr0jtujp0guk3pysp"}, + {"0xea639c776204458fff6dedcca861e8e80d00bc7e", "kii1af3ecamzq3zcllmdahx2sc0gaqxsp0r72h6x6j"}, + {"0xa8f5f24d205ff5814fffecafb81cc42e327e3660", "kii14r6lynfqtl6cznllajhms8xy9ce8udnqw73zwz"}, + {"0x2e3408bcbcf60bd5cd99483b26c84295e6756138", "kii19c6q309u7c9atnvefqajdjzzjhn82cfcakx4cc"}, +} + +func init() { + for _, pair := range blockedAddrPairs { + blockedAddrs[normalizeAddr(pair[0])] = struct{}{} + blockedAddrs[normalizeAddr(pair[1])] = struct{}{} + } +} + +func normalizeAddr(addr string) string { + return strings.ToLower(strings.TrimSpace(addr)) +} + +// IsBlockedAddr reports whether addr (hex or bech32) is on the deny list. +func IsBlockedAddr(addr string) bool { + n := normalizeAddr(addr) + if _, blocked := blockedAddrs[n]; blocked { + return true + } + bz, err := sdk.AccAddressFromBech32(addr) + if err != nil { + return false + } + _, blocked := blockedAddrs["0x"+hex.EncodeToString(bz)] + return blocked +} + +// IsBlockedAccAddress reports whether addr's hex or bech32 form is denied. +func IsBlockedAccAddress(addr sdk.AccAddress) bool { + if len(addr) == 0 { + return false + } + if IsBlockedAddr("0x" + hex.EncodeToString(addr.Bytes())) { + return true + } + return IsBlockedAddr(addr.String()) +} diff --git a/ante/blocked_addrs_ante.go b/ante/blocked_addrs_ante.go new file mode 100644 index 00000000..6c1a75a6 --- /dev/null +++ b/ante/blocked_addrs_ante.go @@ -0,0 +1,122 @@ +package ante + +import ( + errorsmod "cosmossdk.io/errors" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + "github.com/cosmos/cosmos-sdk/x/authz" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + + evmtypes "github.com/cosmos/evm/x/vm/types" + + xerrors "github.com/kiichain/kiichain/v7/x/types/errors" +) + +// BlockedAddrDecorator rejects txs from or to addresses on the incident deny list. +type BlockedAddrDecorator struct { + cdc codec.BinaryCodec +} + +// NewBlockedAddrDecorator returns a decorator that enforces the deny list. +func NewBlockedAddrDecorator(cdc codec.BinaryCodec) BlockedAddrDecorator { + return BlockedAddrDecorator{cdc: cdc} +} + +// AnteHandle rejects a tx that signs, sends, or calls a denied address. +func (d BlockedAddrDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { + if err := CheckBlockedTx(d.cdc, tx); err != nil { + return ctx, err + } + return next(ctx, tx, simulate) +} + +// CheckBlockedTx returns an error if tx uses a denied address as signer, +// bank sender/recipient, or MsgEthereumTx from/to. +func CheckBlockedTx(cdc codec.BinaryCodec, tx sdk.Tx) error { + if sigTx, ok := tx.(authsigning.SigVerifiableTx); ok { + signers, err := sigTx.GetSigners() + if err == nil { + for _, signer := range signers { + if IsBlockedAccAddress(signer) { + return blockedAddrErr(sdk.AccAddress(signer).String()) + } + } + } + } + return checkBlockedMsgs(cdc, tx.GetMsgs()) +} + +func checkBlockedMsgs(cdc codec.BinaryCodec, msgs []sdk.Msg) error { + for _, msg := range msgs { + if execMsg, ok := msg.(*authz.MsgExec); ok { + if err := checkBlockedAuthzExec(cdc, execMsg); err != nil { + return err + } + continue + } + if err := checkBlockedMsg(msg); err != nil { + return err + } + } + return nil +} + +func checkBlockedAuthzExec(cdc codec.BinaryCodec, execMsg *authz.MsgExec) error { + innerMsgs := make([]sdk.Msg, 0, len(execMsg.Msgs)) + for _, v := range execMsg.Msgs { + var innerMsg sdk.Msg + if err := cdc.UnpackAny(v, &innerMsg); err != nil { + return errorsmod.Wrapf(xerrors.ErrUnauthorized, "cannot unmarshal authz exec msg (type %s): %v", v.TypeUrl, err) + } + innerMsgs = append(innerMsgs, innerMsg) + } + return checkBlockedMsgs(cdc, innerMsgs) +} + +func checkBlockedMsg(msg sdk.Msg) error { + switch m := msg.(type) { + case *banktypes.MsgSend: + if IsBlockedAddr(m.FromAddress) { + return blockedAddrErr(m.FromAddress) + } + if IsBlockedAddr(m.ToAddress) { + return blockedAddrErr(m.ToAddress) + } + case *banktypes.MsgMultiSend: + for _, in := range m.Inputs { + if IsBlockedAddr(in.Address) { + return blockedAddrErr(in.Address) + } + } + for _, out := range m.Outputs { + if IsBlockedAddr(out.Address) { + return blockedAddrErr(out.Address) + } + } + case *evmtypes.MsgEthereumTx: + if from := m.GetFrom(); IsBlockedAccAddress(from) { + return blockedAddrErr(from.String()) + } + if ethTx := m.AsTransaction(); ethTx != nil { + if to := ethTx.To(); to != nil && IsBlockedAddr(to.Hex()) { + return blockedAddrErr(to.Hex()) + } + } + } + + if hasSigners, ok := msg.(interface{ GetSigners() []sdk.AccAddress }); ok { + for _, signer := range hasSigners.GetSigners() { + if IsBlockedAccAddress(signer) { + return blockedAddrErr(signer.String()) + } + } + } + return nil +} + +func blockedAddrErr(addr string) error { + return errorsmod.Wrapf(errortypes.ErrUnauthorized, "address is blocked: %s", normalizeAddr(addr)) +} diff --git a/ante/blocked_addrs_internal_test.go b/ante/blocked_addrs_internal_test.go new file mode 100644 index 00000000..73e298d8 --- /dev/null +++ b/ante/blocked_addrs_internal_test.go @@ -0,0 +1,102 @@ +package ante + +import ( + "encoding/hex" + "testing" + + "github.com/stretchr/testify/require" + protov2 "google.golang.org/protobuf/proto" + + "cosmossdk.io/math" + + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/authz" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" +) + +type blockedMsgsTx struct { + msgs []sdk.Msg +} + +func (t blockedMsgsTx) GetMsgs() []sdk.Msg { return t.msgs } +func (t blockedMsgsTx) GetMsgsV2() ([]protov2.Message, error) { return nil, nil } + +func TestBlockedAddrPairs(t *testing.T) { + require.Len(t, blockedAddrPairs, 40) + for _, pair := range blockedAddrPairs { + require.True(t, IsBlockedAddr(pair[0]), pair[0]) + require.True(t, IsBlockedAddr(pair[1]), pair[1]) + require.True(t, IsBlockedAddr("0x"+pair[0][2:]), pair[0]) + + raw, err := hex.DecodeString(pair[0][2:]) + require.NoError(t, err) + require.True(t, IsBlockedAccAddress(sdk.AccAddress(raw)), pair[0]) + } + require.False(t, IsBlockedAddr("0x0000000000000000000000000000000000000001")) +} + +func TestBlockedAddrDecorator(t *testing.T) { + registry := codectypes.NewInterfaceRegistry() + authz.RegisterInterfaces(registry) + banktypes.RegisterInterfaces(registry) + cdc := codec.NewProtoCodec(registry) + decorator := NewBlockedAddrDecorator(cdc) + + blocked := sdk.AccAddress(mustDecodeHex("0e7a96227fcf09f53d644ba6462d8c73993ef246")) + allowed := sdk.AccAddress(mustDecodeHex("0000000000000000000000000000000000000001")) + coins := sdk.NewCoins(sdk.NewCoin("akii", math.NewInt(1))) + + exec := func(msgs ...sdk.Msg) sdk.Msg { + m := authz.NewMsgExec(allowed, msgs) + return &m + } + + testCases := []struct { + name string + msgs []sdk.Msg + expectErr bool + }{ + { + name: "allow bank send between unlisted addrs", + msgs: []sdk.Msg{banktypes.NewMsgSend(allowed, allowed, coins)}, + }, + { + name: "block bank send from listed addr", + msgs: []sdk.Msg{banktypes.NewMsgSend(blocked, allowed, coins)}, + expectErr: true, + }, + { + name: "block bank send to listed addr", + msgs: []sdk.Msg{banktypes.NewMsgSend(allowed, blocked, coins)}, + expectErr: true, + }, + { + name: "block bank send from listed addr inside authz.MsgExec", + msgs: []sdk.Msg{exec(banktypes.NewMsgSend(blocked, allowed, coins))}, + expectErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := decorator.AnteHandle(sdk.Context{}, blockedMsgsTx{msgs: tc.msgs}, false, + func(ctx sdk.Context, _ sdk.Tx, _ bool) (sdk.Context, error) { return ctx, nil }) + if tc.expectErr { + require.Error(t, err) + require.ErrorContains(t, err, "address is blocked") + } else { + require.NoError(t, err) + } + }) + } +} + +func mustDecodeHex(s string) []byte { + bz, err := hex.DecodeString(s) + if err != nil { + panic(err) + } + return bz +} diff --git a/app/app.go b/app/app.go index af8fcd3a..53c22340 100644 --- a/app/app.go +++ b/app/app.go @@ -279,6 +279,9 @@ func NewKiichainApp( if evmtypes.GetChainConfig() != nil { app.configureEVMMempool(appOpts, logger) } + if app.EVMMempool == nil { + app.SetProcessProposal(WrapProcessProposal(app.appCodec, app.txConfig.TxDecoder(), nil)) + } if manager := app.SnapshotManager(); manager != nil { err = manager.RegisterExtensions(wasmkeeper.NewWasmSnapshotter(app.CommitMultiStore(), &app.WasmKeeper)) diff --git a/app/blocked_addrs_proposal.go b/app/blocked_addrs_proposal.go new file mode 100644 index 00000000..74f7b2b8 --- /dev/null +++ b/app/blocked_addrs_proposal.go @@ -0,0 +1,76 @@ +package kiichain + +import ( + abci "github.com/cometbft/cometbft/abci/types" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + + kiiante "github.com/kiichain/kiichain/v7/ante" +) + +// WrapPrepareProposal strips denied-address txs before the inner selector runs. +func WrapPrepareProposal(cdc codec.BinaryCodec, decoder sdk.TxDecoder, next sdk.PrepareProposalHandler) sdk.PrepareProposalHandler { + return func(ctx sdk.Context, req *abci.RequestPrepareProposal) (*abci.ResponsePrepareProposal, error) { + req.Txs = filterBlockedProposalTxs(cdc, decoder, req.Txs) + if next == nil { + return &abci.ResponsePrepareProposal{Txs: req.Txs}, nil + } + return next(ctx, req) + } +} + +// WrapProcessProposal rejects a new proposal that still contains a denied-address tx. +// It does not return an error (that would stall a decided block if used in PreBlock). +func WrapProcessProposal(cdc codec.BinaryCodec, decoder sdk.TxDecoder, next sdk.ProcessProposalHandler) sdk.ProcessProposalHandler { + return func(ctx sdk.Context, req *abci.RequestProcessProposal) (*abci.ResponseProcessProposal, error) { + if proposalContainsBlockedTx(cdc, decoder, req.Txs) { + return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}, nil + } + if next == nil { + return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT}, nil + } + return next(ctx, req) + } +} + +func filterBlockedProposalTxs(cdc codec.BinaryCodec, decoder sdk.TxDecoder, txs [][]byte) [][]byte { + out := make([][]byte, 0, len(txs)) + for _, raw := range txs { + tx, err := decoder(raw) + if err != nil { + out = append(out, raw) + continue + } + if err := kiiante.CheckBlockedTx(cdc, tx); err != nil { + continue + } + out = append(out, raw) + } + return out +} + +func proposalContainsBlockedTx(cdc codec.BinaryCodec, decoder sdk.TxDecoder, txs [][]byte) bool { + for _, raw := range txs { + tx, err := decoder(raw) + if err != nil { + continue + } + if err := kiiante.CheckBlockedTx(cdc, tx); err != nil { + return true + } + } + return false +} + +func logBlockedFinalizeTxs(ctx sdk.Context, cdc codec.BinaryCodec, decoder sdk.TxDecoder, txs [][]byte) { + for i, raw := range txs { + tx, err := decoder(raw) + if err != nil { + continue + } + if err := kiiante.CheckBlockedTx(cdc, tx); err != nil { + ctx.Logger().Error("blocked address tx in finalize block; ante will reject it", "index", i, "err", err) + } + } +} diff --git a/app/evm_mempool.go b/app/evm_mempool.go index 6902038b..e5f3cf88 100644 --- a/app/evm_mempool.go +++ b/app/evm_mempool.go @@ -47,7 +47,9 @@ func (app *KiichainApp) configureEVMMempool(appOpts servertypes.AppOptions, logg sdkmempool.NewDefaultSignerExtractionAdapter(), ), ) - app.SetPrepareProposal(abciProposalHandler.PrepareProposalHandler()) + decoder := app.txConfig.TxDecoder() + app.SetPrepareProposal(WrapPrepareProposal(app.appCodec, decoder, abciProposalHandler.PrepareProposalHandler())) + app.SetProcessProposal(WrapProcessProposal(app.appCodec, decoder, abciProposalHandler.ProcessProposalHandler())) } // createMempoolConfig creates a new EVMMempoolConfig with the default configuration From b15ce265fe9a10f0a3bb86ccb48ad369f671dc24 Mon Sep 17 00:00:00 2001 From: matteyu Date: Mon, 24 Aug 2026 11:58:12 -0700 Subject: [PATCH 09/15] fix: block through BankKeeper --- CHANGELOG.md | 2 +- ante/ante_cosmos.go | 1 - ante/ante_evm.go | 1 - ante/blocked_addrs.go | 85 ++----------------------- ante/blocked_addrs_ante.go | 6 +- ante/blocked_addrs_internal_test.go | 6 +- app/blocked_addrs_proposal.go | 2 +- app/blockedaddrs/addrs.go | 92 ++++++++++++++++++++++++++++ app/blockedaddrs/addrs_test.go | 24 ++++++++ app/blockedaddrs/restriction.go | 31 ++++++++++ app/blockedaddrs/restriction_test.go | 39 ++++++++++++ app/keepers/keepers.go | 2 + 12 files changed, 204 insertions(+), 87 deletions(-) create mode 100644 app/blockedaddrs/addrs.go create mode 100644 app/blockedaddrs/addrs_test.go create mode 100644 app/blockedaddrs/restriction.go create mode 100644 app/blockedaddrs/restriction_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 06be25bf..bb417435 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### Fixed - Reject `MsgCreateVestingAccount`, `MsgCreatePeriodicVestingAccount`, and `MsgCreatePermanentLockedAccount` in the Cosmos ante (top-level and nested in `authz.MsgExec`) so new vesting / locked accounts cannot be opened after the v7.3.2 upgrade -- Block the 22 Aug 2026 incident addresses in Cosmos/EVM ante (and strip/reject them in Prepare/ProcessProposal) so they cannot send, deploy, or be called after the v7.3.2 restart +- Block the 22 Aug 2026 incident addresses with a bank `SendRestriction` so Cosmos, precompile, and EVM native transfers cannot send from or to them. Prepare/ProcessProposal still strip or reject txs that touch the list. ### Dependencies diff --git a/ante/ante_cosmos.go b/ante/ante_cosmos.go index 53dbce32..07b5baa0 100644 --- a/ante/ante_cosmos.go +++ b/ante/ante_cosmos.go @@ -33,7 +33,6 @@ func NewCosmosAnteHandler(ctx sdk.Context, options HandlerOptions) sdk.AnteHandl sdk.MsgTypeURL(&sdkvesting.MsgCreatePermanentLockedAccount{}), ), NewVestingAccountCreationDecorator(options.Cdc), // reject vesting-create msgs at the top level and inside authz - NewBlockedAddrDecorator(options.Cdc), // reject incident addrs (signer / bank / nested authz) ante.NewSetUpContextDecorator(), oracle.NewVoteAloneDecorator(), // Since this only iterate TXs, it must be executed early diff --git a/ante/ante_evm.go b/ante/ante_evm.go index d8033af2..52228bea 100644 --- a/ante/ante_evm.go +++ b/ante/ante_evm.go @@ -13,7 +13,6 @@ func newMonoEVMAnteHandler(ctx sdk.Context, options HandlerOptions) sdk.AnteHand evmParams := options.EvmKeeper.GetParams(ctx) feemarketParams := options.FeeMarketKeeper.GetParams(ctx) decorators := []sdk.AnteDecorator{ - NewBlockedAddrDecorator(options.Cdc), // reject incident addrs before EVM execution kiievmante.NewEVMMonoDecorator( options.AccountKeeper, options.FeeMarketKeeper, diff --git a/ante/blocked_addrs.go b/ante/blocked_addrs.go index 2bb3203e..f8a32b59 100644 --- a/ante/blocked_addrs.go +++ b/ante/blocked_addrs.go @@ -1,92 +1,17 @@ package ante import ( - "encoding/hex" - "strings" - sdk "github.com/cosmos/cosmos-sdk/types" -) - -// blockedAddrs is the incident deny list (hex + bech32). Normalized to -// lowercase at init. Live in the binary so it applies on the first block -// after the v7.3.2 swap, including a tx already packed in that block. -var blockedAddrs = map[string]struct{}{} - -var blockedAddrPairs = [][2]string{ - {"0x0e7a96227fcf09f53d644ba6462d8c73993ef246", "kii1peafvgnleuyl20tyfwnyvtvvwwvnaujxmqe5qe"}, - {"0x631dc2c664ed6dc291b08b35382b807a61b1cd35", "kii1vvwu93nya4ku9yds3v6ns2uq0fsmrnf4cf4yht"}, - {"0x0c45b9fb7a300ea94fbade5f509fae5fad5e56ca", "kii1p3zmn7m6xq82jna6me04p8awt7k4u4k2alwu99"}, - {"0x177629125877dedcca4c195e358dcb598ca15e01", "kii1zamzjyjcwl0dejjvr90rtrwttxx2zhspqx4sm5"}, - {"0x17c0d9fbcfd189bf023656dbfcf50fe0253bb0ee", "kii1zlqdn7706xym7q3k2mdleag0uqjnhv8wu4sfsj"}, - {"0x1e6f344d19382719a202757a73192ab01dbee17a", "kii1rehngnge8qn3ngszw4a8xxf2kqwmact602wtm8"}, - {"0x21f6f013159c76f1baa3c37c0983b795e23f04bc", "kii1y8m0qyc4n3m0rw4rcd7qnqahjh3r7p9uu3ert8"}, - {"0x284b754dca255303d2087224a00ef737a5b1685c", "kii19p9h2nw2y4fs85sgwgj2qrhhx7jmz6zujldh3n"}, - {"0x3c6fe188a1a8cfdf48e05746d98f1e0a1d3904b2", "kii183h7rz9p4r8a7j8q2ardnrc7pgwnjp9jvhc8kq"}, - {"0x407dd1d6edf826bfd016c8f7499f6935c16ca37e", "kii1gp7ar4hdlqntl5qkerm5n8mfxhqkegm76zqskr"}, - {"0x424bd2ca539b0e088b033db0233c74aaa82c2501", "kii1gf9a9jjnnv8q3zcr8kczx0r5425zcfgpdw72tt"}, - {"0x5f90295ea880f1c224d133619dbe98bff804c3b5", "kii1t7gzjh4gsrcuyfx3xdsem05chluqfsa43j9g54"}, - {"0x77308955c6cbc4cdef2e53defc7d78a007f29739", "kii1wucgj4wxe0zvmmew2000cltc5qrl99eedtrzv4"}, - {"0x8132bfde87a5bfa23297da7f74e6ffed079f7495", "kii1syetlh585kl6yv5hmflhfehla5re7ay4um2skh"}, - {"0x87a4ea252044933a91c65a1608cee14626fa0947", "kii1s7jw5ffqgjfn4ywxtgtq3nhpgcn05z28fsmkhm"}, - {"0x8cdab0fa359ac467c80c19de3fee5a543e258365", "kii13ndtp734ntzx0jqvr80rlmj62slztqm9agzwce"}, - {"0x8f37701914d60cee95ccaa39af959561045cf9e8", "kii13umhqxg56cxwa9wv4gu6l9v4vyz9e70g4hupvn"}, - {"0xa6b260f4df26f94e5f21ca9b7d57d82ff9587cc1", "kii156expaxlymu5uhepe2dh647c9lu4slxpyml28q"}, - {"0xb1d8431da51f157b46ca7f831e88f5365b230948", "kii1k8vyx8d9ru2hk3k207p3az84xedjxz2gkdyle0"}, - {"0xd2c75516cc9e726026b25af99e671bf502bc53f1", "kii16tr429kvneexqf4jttueuecm75ptc5l3gtj34q"}, - {"0xddaeddb516fc21646102168a598afd4f58abdb3d", "kii1mkhdmdgklsskgcgzz699nzhafav2hkea4qp2dj"}, - {"0xed191cf73de21b78b2d4f4ff7380e6c73c2ae9cf", "kii1a5v3eaeaugdh3vk57nlh8q8xcu7z46w0ttlrw9"}, - {"0x603871c2ddd41c26ee77495e2e31e6de7f9957e0", "kii1vqu8rska6swzdmnhf90zuv0xmelej4lq5el7zh"}, - {"0xc6b0896e067e5dfd4a945402fd6f90e2f69c30d8", "kii1c6cgjmsx0ewl6j552sp06musutmfcvxcaq4n9h"}, - {"0x9d770b07583c46267cb91f5ad0350ee017a16715", "kii1n4mskp6c83rzvl9eraddqdgwuqt6zec46qv06q"}, - {"0x7e97874b3d6e9cd914d9413303bd89336adeea03", "kii106tcwjead6wdj9xegyes80vfxd4da6sr4f5npu"}, - {"0x8132218b2ce689885d885eeb032c79bc426659fb", "kii1syezrzevu6ycshvgtm4sxtreh3pxvk0mtfe6rd"}, - {"0x4e0efcc1f125ff989a41004f938722ffbb3d52f4", "kii1fc80es03yhle3xjpqp8e8pezl7an65h50fl2pm"}, - {"0x8f3d7e53f0c31b85100879a8b5024af6ce991e4e", "kii13u7hu5lscvdc2yqg0x5t2qj27m8fj8jw4ez046"}, - {"0x000938891e46aa70bb15d11edc840749d43916af", "kii1qqyn3zg7g648pwc46y0depq8f82rj9400ulj4g"}, - {"0x3f1261c2782651c283b85f6f5b887379b6007ec6", "kii18ufxrsncyegu9qactah4hzrn0xmqqlkxr6p3z5"}, - {"0xcdd338440e9f2ffe4ed7b53ea6bffd3b4f91e672", "kii1ehfns3qwnuhlunkhk5l2d0la8d8erenjn0482a"}, - {"0x3a4091aa5e61746257e8433b8ed3194d0fa907c1", "kii18fqfr2j7v96xy4lggvaca5cef586jp7pry27v0"}, - {"0x1afdd1ef58b05ffc278028de9d93ea5706dc1128", "kii1rt7arm6ckp0lcfuq9r0fmyl22urdcyfgfer35g"}, - {"0x2eb19042d7b92458df32c0a5a090e97f77e64229", "kii196ceqskhhyj93hejczj6py8f0am7vs3fykark7"}, - {"0xde40a613c3fce78cf072e3f8228330ba7bde4de6", "kii1meq2vy7rlnnceurju0uz9qeshfaaun0x5xsg06"}, - {"0x7c973b6effb02611b268293330206f92f920bd1c", "kii10jtnkmhlkqnprvng9yenqgr0jtujp0guk3pysp"}, - {"0xea639c776204458fff6dedcca861e8e80d00bc7e", "kii1af3ecamzq3zcllmdahx2sc0gaqxsp0r72h6x6j"}, - {"0xa8f5f24d205ff5814fffecafb81cc42e327e3660", "kii14r6lynfqtl6cznllajhms8xy9ce8udnqw73zwz"}, - {"0x2e3408bcbcf60bd5cd99483b26c84295e6756138", "kii19c6q309u7c9atnvefqajdjzzjhn82cfcakx4cc"}, -} -func init() { - for _, pair := range blockedAddrPairs { - blockedAddrs[normalizeAddr(pair[0])] = struct{}{} - blockedAddrs[normalizeAddr(pair[1])] = struct{}{} - } -} - -func normalizeAddr(addr string) string { - return strings.ToLower(strings.TrimSpace(addr)) -} + "github.com/kiichain/kiichain/v7/app/blockedaddrs" +) -// IsBlockedAddr reports whether addr (hex or bech32) is on the deny list. +// IsBlockedAddr reports whether addr (hex or bech32) is on the incident deny list. func IsBlockedAddr(addr string) bool { - n := normalizeAddr(addr) - if _, blocked := blockedAddrs[n]; blocked { - return true - } - bz, err := sdk.AccAddressFromBech32(addr) - if err != nil { - return false - } - _, blocked := blockedAddrs["0x"+hex.EncodeToString(bz)] - return blocked + return blockedaddrs.IsBlockedAddr(addr) } // IsBlockedAccAddress reports whether addr's hex or bech32 form is denied. func IsBlockedAccAddress(addr sdk.AccAddress) bool { - if len(addr) == 0 { - return false - } - if IsBlockedAddr("0x" + hex.EncodeToString(addr.Bytes())) { - return true - } - return IsBlockedAddr(addr.String()) + return blockedaddrs.IsBlockedAccAddress(addr) } diff --git a/ante/blocked_addrs_ante.go b/ante/blocked_addrs_ante.go index 6c1a75a6..326e1060 100644 --- a/ante/blocked_addrs_ante.go +++ b/ante/blocked_addrs_ante.go @@ -1,6 +1,8 @@ package ante import ( + "strings" + errorsmod "cosmossdk.io/errors" "github.com/cosmos/cosmos-sdk/codec" @@ -16,6 +18,8 @@ import ( ) // BlockedAddrDecorator rejects txs from or to addresses on the incident deny list. +// Enforcement of leftover funds is the bank send restriction; this helper is +// still used by Prepare/ProcessProposal to drop packed incident txs. type BlockedAddrDecorator struct { cdc codec.BinaryCodec } @@ -118,5 +122,5 @@ func checkBlockedMsg(msg sdk.Msg) error { } func blockedAddrErr(addr string) error { - return errorsmod.Wrapf(errortypes.ErrUnauthorized, "address is blocked: %s", normalizeAddr(addr)) + return errorsmod.Wrapf(errortypes.ErrUnauthorized, "address is blocked: %s", strings.ToLower(strings.TrimSpace(addr))) } diff --git a/ante/blocked_addrs_internal_test.go b/ante/blocked_addrs_internal_test.go index 73e298d8..41ced941 100644 --- a/ante/blocked_addrs_internal_test.go +++ b/ante/blocked_addrs_internal_test.go @@ -14,6 +14,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/authz" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + + "github.com/kiichain/kiichain/v7/app/blockedaddrs" ) type blockedMsgsTx struct { @@ -24,8 +26,8 @@ func (t blockedMsgsTx) GetMsgs() []sdk.Msg { return t.msgs } func (t blockedMsgsTx) GetMsgsV2() ([]protov2.Message, error) { return nil, nil } func TestBlockedAddrPairs(t *testing.T) { - require.Len(t, blockedAddrPairs, 40) - for _, pair := range blockedAddrPairs { + require.Len(t, blockedaddrs.AddrPairs, 40) + for _, pair := range blockedaddrs.AddrPairs { require.True(t, IsBlockedAddr(pair[0]), pair[0]) require.True(t, IsBlockedAddr(pair[1]), pair[1]) require.True(t, IsBlockedAddr("0x"+pair[0][2:]), pair[0]) diff --git a/app/blocked_addrs_proposal.go b/app/blocked_addrs_proposal.go index 74f7b2b8..c02bc601 100644 --- a/app/blocked_addrs_proposal.go +++ b/app/blocked_addrs_proposal.go @@ -70,7 +70,7 @@ func logBlockedFinalizeTxs(ctx sdk.Context, cdc codec.BinaryCodec, decoder sdk.T continue } if err := kiiante.CheckBlockedTx(cdc, tx); err != nil { - ctx.Logger().Error("blocked address tx in finalize block; ante will reject it", "index", i, "err", err) + ctx.Logger().Error("blocked address tx in finalize block; bank send restriction will reject transfers", "index", i, "err", err) } } } diff --git a/app/blockedaddrs/addrs.go b/app/blockedaddrs/addrs.go new file mode 100644 index 00000000..4a8ea12d --- /dev/null +++ b/app/blockedaddrs/addrs.go @@ -0,0 +1,92 @@ +package blockedaddrs + +import ( + "encoding/hex" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// blockedAddrs is the 22 Aug 2026 incident deny list (hex + bech32). +// Normalized to lowercase at init. +var blockedAddrs = map[string]struct{}{} + +// AddrPairs is the incident deny list as [hex, bech32] pairs. +var AddrPairs = [][2]string{ + {"0x0e7a96227fcf09f53d644ba6462d8c73993ef246", "kii1peafvgnleuyl20tyfwnyvtvvwwvnaujxmqe5qe"}, + {"0x631dc2c664ed6dc291b08b35382b807a61b1cd35", "kii1vvwu93nya4ku9yds3v6ns2uq0fsmrnf4cf4yht"}, + {"0x0c45b9fb7a300ea94fbade5f509fae5fad5e56ca", "kii1p3zmn7m6xq82jna6me04p8awt7k4u4k2alwu99"}, + {"0x177629125877dedcca4c195e358dcb598ca15e01", "kii1zamzjyjcwl0dejjvr90rtrwttxx2zhspqx4sm5"}, + {"0x17c0d9fbcfd189bf023656dbfcf50fe0253bb0ee", "kii1zlqdn7706xym7q3k2mdleag0uqjnhv8wu4sfsj"}, + {"0x1e6f344d19382719a202757a73192ab01dbee17a", "kii1rehngnge8qn3ngszw4a8xxf2kqwmact602wtm8"}, + {"0x21f6f013159c76f1baa3c37c0983b795e23f04bc", "kii1y8m0qyc4n3m0rw4rcd7qnqahjh3r7p9uu3ert8"}, + {"0x284b754dca255303d2087224a00ef737a5b1685c", "kii19p9h2nw2y4fs85sgwgj2qrhhx7jmz6zujldh3n"}, + {"0x3c6fe188a1a8cfdf48e05746d98f1e0a1d3904b2", "kii183h7rz9p4r8a7j8q2ardnrc7pgwnjp9jvhc8kq"}, + {"0x407dd1d6edf826bfd016c8f7499f6935c16ca37e", "kii1gp7ar4hdlqntl5qkerm5n8mfxhqkegm76zqskr"}, + {"0x424bd2ca539b0e088b033db0233c74aaa82c2501", "kii1gf9a9jjnnv8q3zcr8kczx0r5425zcfgpdw72tt"}, + {"0x5f90295ea880f1c224d133619dbe98bff804c3b5", "kii1t7gzjh4gsrcuyfx3xdsem05chluqfsa43j9g54"}, + {"0x77308955c6cbc4cdef2e53defc7d78a007f29739", "kii1wucgj4wxe0zvmmew2000cltc5qrl99eedtrzv4"}, + {"0x8132bfde87a5bfa23297da7f74e6ffed079f7495", "kii1syetlh585kl6yv5hmflhfehla5re7ay4um2skh"}, + {"0x87a4ea252044933a91c65a1608cee14626fa0947", "kii1s7jw5ffqgjfn4ywxtgtq3nhpgcn05z28fsmkhm"}, + {"0x8cdab0fa359ac467c80c19de3fee5a543e258365", "kii13ndtp734ntzx0jqvr80rlmj62slztqm9agzwce"}, + {"0x8f37701914d60cee95ccaa39af959561045cf9e8", "kii13umhqxg56cxwa9wv4gu6l9v4vyz9e70g4hupvn"}, + {"0xa6b260f4df26f94e5f21ca9b7d57d82ff9587cc1", "kii156expaxlymu5uhepe2dh647c9lu4slxpyml28q"}, + {"0xb1d8431da51f157b46ca7f831e88f5365b230948", "kii1k8vyx8d9ru2hk3k207p3az84xedjxz2gkdyle0"}, + {"0xd2c75516cc9e726026b25af99e671bf502bc53f1", "kii16tr429kvneexqf4jttueuecm75ptc5l3gtj34q"}, + {"0xddaeddb516fc21646102168a598afd4f58abdb3d", "kii1mkhdmdgklsskgcgzz699nzhafav2hkea4qp2dj"}, + {"0xed191cf73de21b78b2d4f4ff7380e6c73c2ae9cf", "kii1a5v3eaeaugdh3vk57nlh8q8xcu7z46w0ttlrw9"}, + {"0x603871c2ddd41c26ee77495e2e31e6de7f9957e0", "kii1vqu8rska6swzdmnhf90zuv0xmelej4lq5el7zh"}, + {"0xc6b0896e067e5dfd4a945402fd6f90e2f69c30d8", "kii1c6cgjmsx0ewl6j552sp06musutmfcvxcaq4n9h"}, + {"0x9d770b07583c46267cb91f5ad0350ee017a16715", "kii1n4mskp6c83rzvl9eraddqdgwuqt6zec46qv06q"}, + {"0x7e97874b3d6e9cd914d9413303bd89336adeea03", "kii106tcwjead6wdj9xegyes80vfxd4da6sr4f5npu"}, + {"0x8132218b2ce689885d885eeb032c79bc426659fb", "kii1syezrzevu6ycshvgtm4sxtreh3pxvk0mtfe6rd"}, + {"0x4e0efcc1f125ff989a41004f938722ffbb3d52f4", "kii1fc80es03yhle3xjpqp8e8pezl7an65h50fl2pm"}, + {"0x8f3d7e53f0c31b85100879a8b5024af6ce991e4e", "kii13u7hu5lscvdc2yqg0x5t2qj27m8fj8jw4ez046"}, + {"0x000938891e46aa70bb15d11edc840749d43916af", "kii1qqyn3zg7g648pwc46y0depq8f82rj9400ulj4g"}, + {"0x3f1261c2782651c283b85f6f5b887379b6007ec6", "kii18ufxrsncyegu9qactah4hzrn0xmqqlkxr6p3z5"}, + {"0xcdd338440e9f2ffe4ed7b53ea6bffd3b4f91e672", "kii1ehfns3qwnuhlunkhk5l2d0la8d8erenjn0482a"}, + {"0x3a4091aa5e61746257e8433b8ed3194d0fa907c1", "kii18fqfr2j7v96xy4lggvaca5cef586jp7pry27v0"}, + {"0x1afdd1ef58b05ffc278028de9d93ea5706dc1128", "kii1rt7arm6ckp0lcfuq9r0fmyl22urdcyfgfer35g"}, + {"0x2eb19042d7b92458df32c0a5a090e97f77e64229", "kii196ceqskhhyj93hejczj6py8f0am7vs3fykark7"}, + {"0xde40a613c3fce78cf072e3f8228330ba7bde4de6", "kii1meq2vy7rlnnceurju0uz9qeshfaaun0x5xsg06"}, + {"0x7c973b6effb02611b268293330206f92f920bd1c", "kii10jtnkmhlkqnprvng9yenqgr0jtujp0guk3pysp"}, + {"0xea639c776204458fff6dedcca861e8e80d00bc7e", "kii1af3ecamzq3zcllmdahx2sc0gaqxsp0r72h6x6j"}, + {"0xa8f5f24d205ff5814fffecafb81cc42e327e3660", "kii14r6lynfqtl6cznllajhms8xy9ce8udnqw73zwz"}, + {"0x2e3408bcbcf60bd5cd99483b26c84295e6756138", "kii19c6q309u7c9atnvefqajdjzzjhn82cfcakx4cc"}, +} + +func init() { + for _, pair := range AddrPairs { + blockedAddrs[normalizeAddr(pair[0])] = struct{}{} + blockedAddrs[normalizeAddr(pair[1])] = struct{}{} + } +} + +func normalizeAddr(addr string) string { + return strings.ToLower(strings.TrimSpace(addr)) +} + +// IsBlockedAddr reports whether addr (hex or bech32) is on the deny list. +func IsBlockedAddr(addr string) bool { + n := normalizeAddr(addr) + if _, blocked := blockedAddrs[n]; blocked { + return true + } + bz, err := sdk.AccAddressFromBech32(addr) + if err != nil { + return false + } + _, blocked := blockedAddrs["0x"+hex.EncodeToString(bz)] + return blocked +} + +// IsBlockedAccAddress reports whether addr's hex or bech32 form is denied. +func IsBlockedAccAddress(addr sdk.AccAddress) bool { + if len(addr) == 0 { + return false + } + if IsBlockedAddr("0x" + hex.EncodeToString(addr.Bytes())) { + return true + } + return IsBlockedAddr(addr.String()) +} diff --git a/app/blockedaddrs/addrs_test.go b/app/blockedaddrs/addrs_test.go new file mode 100644 index 00000000..8f607e59 --- /dev/null +++ b/app/blockedaddrs/addrs_test.go @@ -0,0 +1,24 @@ +package blockedaddrs + +import ( + "encoding/hex" + "testing" + + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func TestAddrPairs(t *testing.T) { + require.Len(t, AddrPairs, 40) + for _, pair := range AddrPairs { + require.True(t, IsBlockedAddr(pair[0]), pair[0]) + require.True(t, IsBlockedAddr(pair[1]), pair[1]) + require.True(t, IsBlockedAddr("0x"+pair[0][2:]), pair[0]) + + raw, err := hex.DecodeString(pair[0][2:]) + require.NoError(t, err) + require.True(t, IsBlockedAccAddress(sdk.AccAddress(raw)), pair[0]) + } + require.False(t, IsBlockedAddr("0x0000000000000000000000000000000000000001")) +} diff --git a/app/blockedaddrs/restriction.go b/app/blockedaddrs/restriction.go new file mode 100644 index 00000000..af6289e2 --- /dev/null +++ b/app/blockedaddrs/restriction.go @@ -0,0 +1,31 @@ +package blockedaddrs + +import ( + "context" + + errorsmod "cosmossdk.io/errors" + + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" +) + +// SendRestriction rejects bank sends whose from or to address is on the +// incident list. It is registered on the bank keeper so Cosmos sends, +// precompile sends, and EVM native commits (mint/burn via SendCoins) are +// all covered. +func SendRestriction(_ context.Context, fromAddr, toAddr sdk.AccAddress, _ sdk.Coins) (sdk.AccAddress, error) { + if IsBlockedAccAddress(fromAddr) { + return nil, blockedSendErr(fromAddr.String()) + } + if IsBlockedAccAddress(toAddr) { + return nil, blockedSendErr(toAddr.String()) + } + return toAddr, nil +} + +func blockedSendErr(addr string) error { + return errorsmod.Wrapf(errortypes.ErrUnauthorized, "address is blocked: %s", normalizeAddr(addr)) +} + +var _ banktypes.SendRestrictionFn = SendRestriction diff --git a/app/blockedaddrs/restriction_test.go b/app/blockedaddrs/restriction_test.go new file mode 100644 index 00000000..ce9f1072 --- /dev/null +++ b/app/blockedaddrs/restriction_test.go @@ -0,0 +1,39 @@ +package blockedaddrs + +import ( + "context" + "encoding/hex" + "testing" + + "github.com/stretchr/testify/require" + + "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func TestSendRestriction(t *testing.T) { + blocked := sdk.AccAddress(mustDecodeHex("0e7a96227fcf09f53d644ba6462d8c73993ef246")) + allowed := sdk.AccAddress(mustDecodeHex("0000000000000000000000000000000000000001")) + coins := sdk.NewCoins(sdk.NewCoin("akii", math.NewInt(1))) + ctx := sdk.Context{}.WithContext(context.Background()) + + _, err := SendRestriction(ctx, allowed, allowed, coins) + require.NoError(t, err) + + _, err = SendRestriction(ctx, blocked, allowed, coins) + require.Error(t, err) + require.ErrorContains(t, err, "address is blocked") + + _, err = SendRestriction(ctx, allowed, blocked, coins) + require.Error(t, err) + require.ErrorContains(t, err, "address is blocked") +} + +func mustDecodeHex(s string) []byte { + bz, err := hex.DecodeString(s) + if err != nil { + panic(err) + } + return bz +} diff --git a/app/keepers/keepers.go b/app/keepers/keepers.go index cd21f691..19eee252 100644 --- a/app/keepers/keepers.go +++ b/app/keepers/keepers.go @@ -83,6 +83,7 @@ import ( evmkeeper "github.com/cosmos/evm/x/vm/keeper" evmtypes "github.com/cosmos/evm/x/vm/types" + "github.com/kiichain/kiichain/v7/app/blockedaddrs" kiiparams "github.com/kiichain/kiichain/v7/app/params" "github.com/kiichain/kiichain/v7/wasmbinding" feeabstractionkeeper "github.com/kiichain/kiichain/v7/x/feeabstraction/keeper" @@ -220,6 +221,7 @@ func NewAppKeeper( authtypes.NewModuleAddress(govtypes.ModuleName).String(), logger, ) + appKeepers.BankKeeper.AppendSendRestriction(blockedaddrs.SendRestriction) appKeepers.AuthzKeeper = authzkeeper.NewKeeper( runtime.NewKVStoreService(appKeepers.keys[authzkeeper.StoreKey]), From a4f2abc397d3b0c7fbd845c4fbc30d993e157286 Mon Sep 17 00:00:00 2001 From: matteyu Date: Mon, 24 Aug 2026 12:28:03 -0700 Subject: [PATCH 10/15] fix: remove from ante and go through bank --- CHANGELOG.md | 3 +- ante/blocked_addrs.go | 17 ---- ante/blocked_addrs_ante.go | 126 --------------------------- ante/blocked_addrs_internal_test.go | 104 ---------------------- app/app.go | 3 - app/blocked_addrs_proposal.go | 76 ---------------- app/blockedaddrs/restriction.go | 45 +++++++--- app/blockedaddrs/restriction_test.go | 25 ++++-- app/evm_mempool.go | 4 +- app/keepers/keepers.go | 2 +- app/upgrades/v7_3_2/constants.go | 3 +- app/upgrades/v7_3_2/upgrade.go | 12 ++- 12 files changed, 65 insertions(+), 355 deletions(-) delete mode 100644 ante/blocked_addrs.go delete mode 100644 ante/blocked_addrs_ante.go delete mode 100644 ante/blocked_addrs_internal_test.go delete mode 100644 app/blocked_addrs_proposal.go diff --git a/CHANGELOG.md b/CHANGELOG.md index bb417435..23edb6d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,8 @@ ### Fixed - Reject `MsgCreateVestingAccount`, `MsgCreatePeriodicVestingAccount`, and `MsgCreatePermanentLockedAccount` in the Cosmos ante (top-level and nested in `authz.MsgExec`) so new vesting / locked accounts cannot be opened after the v7.3.2 upgrade -- Block the 22 Aug 2026 incident addresses with a bank `SendRestriction` so Cosmos, precompile, and EVM native transfers cannot send from or to them. Prepare/ProcessProposal still strip or reject txs that touch the list. +- Enable a bank `SendRestriction` for the 22 Aug 2026 incident addresses in the `v7.3.2` upgrade so Cosmos, precompile, and EVM native transfers cannot send from or to them after the upgrade height + ### Dependencies diff --git a/ante/blocked_addrs.go b/ante/blocked_addrs.go deleted file mode 100644 index f8a32b59..00000000 --- a/ante/blocked_addrs.go +++ /dev/null @@ -1,17 +0,0 @@ -package ante - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" - - "github.com/kiichain/kiichain/v7/app/blockedaddrs" -) - -// IsBlockedAddr reports whether addr (hex or bech32) is on the incident deny list. -func IsBlockedAddr(addr string) bool { - return blockedaddrs.IsBlockedAddr(addr) -} - -// IsBlockedAccAddress reports whether addr's hex or bech32 form is denied. -func IsBlockedAccAddress(addr sdk.AccAddress) bool { - return blockedaddrs.IsBlockedAccAddress(addr) -} diff --git a/ante/blocked_addrs_ante.go b/ante/blocked_addrs_ante.go deleted file mode 100644 index 326e1060..00000000 --- a/ante/blocked_addrs_ante.go +++ /dev/null @@ -1,126 +0,0 @@ -package ante - -import ( - "strings" - - errorsmod "cosmossdk.io/errors" - - "github.com/cosmos/cosmos-sdk/codec" - sdk "github.com/cosmos/cosmos-sdk/types" - errortypes "github.com/cosmos/cosmos-sdk/types/errors" - authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" - "github.com/cosmos/cosmos-sdk/x/authz" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - - evmtypes "github.com/cosmos/evm/x/vm/types" - - xerrors "github.com/kiichain/kiichain/v7/x/types/errors" -) - -// BlockedAddrDecorator rejects txs from or to addresses on the incident deny list. -// Enforcement of leftover funds is the bank send restriction; this helper is -// still used by Prepare/ProcessProposal to drop packed incident txs. -type BlockedAddrDecorator struct { - cdc codec.BinaryCodec -} - -// NewBlockedAddrDecorator returns a decorator that enforces the deny list. -func NewBlockedAddrDecorator(cdc codec.BinaryCodec) BlockedAddrDecorator { - return BlockedAddrDecorator{cdc: cdc} -} - -// AnteHandle rejects a tx that signs, sends, or calls a denied address. -func (d BlockedAddrDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { - if err := CheckBlockedTx(d.cdc, tx); err != nil { - return ctx, err - } - return next(ctx, tx, simulate) -} - -// CheckBlockedTx returns an error if tx uses a denied address as signer, -// bank sender/recipient, or MsgEthereumTx from/to. -func CheckBlockedTx(cdc codec.BinaryCodec, tx sdk.Tx) error { - if sigTx, ok := tx.(authsigning.SigVerifiableTx); ok { - signers, err := sigTx.GetSigners() - if err == nil { - for _, signer := range signers { - if IsBlockedAccAddress(signer) { - return blockedAddrErr(sdk.AccAddress(signer).String()) - } - } - } - } - return checkBlockedMsgs(cdc, tx.GetMsgs()) -} - -func checkBlockedMsgs(cdc codec.BinaryCodec, msgs []sdk.Msg) error { - for _, msg := range msgs { - if execMsg, ok := msg.(*authz.MsgExec); ok { - if err := checkBlockedAuthzExec(cdc, execMsg); err != nil { - return err - } - continue - } - if err := checkBlockedMsg(msg); err != nil { - return err - } - } - return nil -} - -func checkBlockedAuthzExec(cdc codec.BinaryCodec, execMsg *authz.MsgExec) error { - innerMsgs := make([]sdk.Msg, 0, len(execMsg.Msgs)) - for _, v := range execMsg.Msgs { - var innerMsg sdk.Msg - if err := cdc.UnpackAny(v, &innerMsg); err != nil { - return errorsmod.Wrapf(xerrors.ErrUnauthorized, "cannot unmarshal authz exec msg (type %s): %v", v.TypeUrl, err) - } - innerMsgs = append(innerMsgs, innerMsg) - } - return checkBlockedMsgs(cdc, innerMsgs) -} - -func checkBlockedMsg(msg sdk.Msg) error { - switch m := msg.(type) { - case *banktypes.MsgSend: - if IsBlockedAddr(m.FromAddress) { - return blockedAddrErr(m.FromAddress) - } - if IsBlockedAddr(m.ToAddress) { - return blockedAddrErr(m.ToAddress) - } - case *banktypes.MsgMultiSend: - for _, in := range m.Inputs { - if IsBlockedAddr(in.Address) { - return blockedAddrErr(in.Address) - } - } - for _, out := range m.Outputs { - if IsBlockedAddr(out.Address) { - return blockedAddrErr(out.Address) - } - } - case *evmtypes.MsgEthereumTx: - if from := m.GetFrom(); IsBlockedAccAddress(from) { - return blockedAddrErr(from.String()) - } - if ethTx := m.AsTransaction(); ethTx != nil { - if to := ethTx.To(); to != nil && IsBlockedAddr(to.Hex()) { - return blockedAddrErr(to.Hex()) - } - } - } - - if hasSigners, ok := msg.(interface{ GetSigners() []sdk.AccAddress }); ok { - for _, signer := range hasSigners.GetSigners() { - if IsBlockedAccAddress(signer) { - return blockedAddrErr(signer.String()) - } - } - } - return nil -} - -func blockedAddrErr(addr string) error { - return errorsmod.Wrapf(errortypes.ErrUnauthorized, "address is blocked: %s", strings.ToLower(strings.TrimSpace(addr))) -} diff --git a/ante/blocked_addrs_internal_test.go b/ante/blocked_addrs_internal_test.go deleted file mode 100644 index 41ced941..00000000 --- a/ante/blocked_addrs_internal_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package ante - -import ( - "encoding/hex" - "testing" - - "github.com/stretchr/testify/require" - protov2 "google.golang.org/protobuf/proto" - - "cosmossdk.io/math" - - "github.com/cosmos/cosmos-sdk/codec" - codectypes "github.com/cosmos/cosmos-sdk/codec/types" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/authz" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - - "github.com/kiichain/kiichain/v7/app/blockedaddrs" -) - -type blockedMsgsTx struct { - msgs []sdk.Msg -} - -func (t blockedMsgsTx) GetMsgs() []sdk.Msg { return t.msgs } -func (t blockedMsgsTx) GetMsgsV2() ([]protov2.Message, error) { return nil, nil } - -func TestBlockedAddrPairs(t *testing.T) { - require.Len(t, blockedaddrs.AddrPairs, 40) - for _, pair := range blockedaddrs.AddrPairs { - require.True(t, IsBlockedAddr(pair[0]), pair[0]) - require.True(t, IsBlockedAddr(pair[1]), pair[1]) - require.True(t, IsBlockedAddr("0x"+pair[0][2:]), pair[0]) - - raw, err := hex.DecodeString(pair[0][2:]) - require.NoError(t, err) - require.True(t, IsBlockedAccAddress(sdk.AccAddress(raw)), pair[0]) - } - require.False(t, IsBlockedAddr("0x0000000000000000000000000000000000000001")) -} - -func TestBlockedAddrDecorator(t *testing.T) { - registry := codectypes.NewInterfaceRegistry() - authz.RegisterInterfaces(registry) - banktypes.RegisterInterfaces(registry) - cdc := codec.NewProtoCodec(registry) - decorator := NewBlockedAddrDecorator(cdc) - - blocked := sdk.AccAddress(mustDecodeHex("0e7a96227fcf09f53d644ba6462d8c73993ef246")) - allowed := sdk.AccAddress(mustDecodeHex("0000000000000000000000000000000000000001")) - coins := sdk.NewCoins(sdk.NewCoin("akii", math.NewInt(1))) - - exec := func(msgs ...sdk.Msg) sdk.Msg { - m := authz.NewMsgExec(allowed, msgs) - return &m - } - - testCases := []struct { - name string - msgs []sdk.Msg - expectErr bool - }{ - { - name: "allow bank send between unlisted addrs", - msgs: []sdk.Msg{banktypes.NewMsgSend(allowed, allowed, coins)}, - }, - { - name: "block bank send from listed addr", - msgs: []sdk.Msg{banktypes.NewMsgSend(blocked, allowed, coins)}, - expectErr: true, - }, - { - name: "block bank send to listed addr", - msgs: []sdk.Msg{banktypes.NewMsgSend(allowed, blocked, coins)}, - expectErr: true, - }, - { - name: "block bank send from listed addr inside authz.MsgExec", - msgs: []sdk.Msg{exec(banktypes.NewMsgSend(blocked, allowed, coins))}, - expectErr: true, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - _, err := decorator.AnteHandle(sdk.Context{}, blockedMsgsTx{msgs: tc.msgs}, false, - func(ctx sdk.Context, _ sdk.Tx, _ bool) (sdk.Context, error) { return ctx, nil }) - if tc.expectErr { - require.Error(t, err) - require.ErrorContains(t, err, "address is blocked") - } else { - require.NoError(t, err) - } - }) - } -} - -func mustDecodeHex(s string) []byte { - bz, err := hex.DecodeString(s) - if err != nil { - panic(err) - } - return bz -} diff --git a/app/app.go b/app/app.go index 53c22340..af8fcd3a 100644 --- a/app/app.go +++ b/app/app.go @@ -279,9 +279,6 @@ func NewKiichainApp( if evmtypes.GetChainConfig() != nil { app.configureEVMMempool(appOpts, logger) } - if app.EVMMempool == nil { - app.SetProcessProposal(WrapProcessProposal(app.appCodec, app.txConfig.TxDecoder(), nil)) - } if manager := app.SnapshotManager(); manager != nil { err = manager.RegisterExtensions(wasmkeeper.NewWasmSnapshotter(app.CommitMultiStore(), &app.WasmKeeper)) diff --git a/app/blocked_addrs_proposal.go b/app/blocked_addrs_proposal.go deleted file mode 100644 index c02bc601..00000000 --- a/app/blocked_addrs_proposal.go +++ /dev/null @@ -1,76 +0,0 @@ -package kiichain - -import ( - abci "github.com/cometbft/cometbft/abci/types" - - "github.com/cosmos/cosmos-sdk/codec" - sdk "github.com/cosmos/cosmos-sdk/types" - - kiiante "github.com/kiichain/kiichain/v7/ante" -) - -// WrapPrepareProposal strips denied-address txs before the inner selector runs. -func WrapPrepareProposal(cdc codec.BinaryCodec, decoder sdk.TxDecoder, next sdk.PrepareProposalHandler) sdk.PrepareProposalHandler { - return func(ctx sdk.Context, req *abci.RequestPrepareProposal) (*abci.ResponsePrepareProposal, error) { - req.Txs = filterBlockedProposalTxs(cdc, decoder, req.Txs) - if next == nil { - return &abci.ResponsePrepareProposal{Txs: req.Txs}, nil - } - return next(ctx, req) - } -} - -// WrapProcessProposal rejects a new proposal that still contains a denied-address tx. -// It does not return an error (that would stall a decided block if used in PreBlock). -func WrapProcessProposal(cdc codec.BinaryCodec, decoder sdk.TxDecoder, next sdk.ProcessProposalHandler) sdk.ProcessProposalHandler { - return func(ctx sdk.Context, req *abci.RequestProcessProposal) (*abci.ResponseProcessProposal, error) { - if proposalContainsBlockedTx(cdc, decoder, req.Txs) { - return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}, nil - } - if next == nil { - return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT}, nil - } - return next(ctx, req) - } -} - -func filterBlockedProposalTxs(cdc codec.BinaryCodec, decoder sdk.TxDecoder, txs [][]byte) [][]byte { - out := make([][]byte, 0, len(txs)) - for _, raw := range txs { - tx, err := decoder(raw) - if err != nil { - out = append(out, raw) - continue - } - if err := kiiante.CheckBlockedTx(cdc, tx); err != nil { - continue - } - out = append(out, raw) - } - return out -} - -func proposalContainsBlockedTx(cdc codec.BinaryCodec, decoder sdk.TxDecoder, txs [][]byte) bool { - for _, raw := range txs { - tx, err := decoder(raw) - if err != nil { - continue - } - if err := kiiante.CheckBlockedTx(cdc, tx); err != nil { - return true - } - } - return false -} - -func logBlockedFinalizeTxs(ctx sdk.Context, cdc codec.BinaryCodec, decoder sdk.TxDecoder, txs [][]byte) { - for i, raw := range txs { - tx, err := decoder(raw) - if err != nil { - continue - } - if err := kiiante.CheckBlockedTx(cdc, tx); err != nil { - ctx.Logger().Error("blocked address tx in finalize block; bank send restriction will reject transfers", "index", i, "err", err) - } - } -} diff --git a/app/blockedaddrs/restriction.go b/app/blockedaddrs/restriction.go index af6289e2..3b6df14e 100644 --- a/app/blockedaddrs/restriction.go +++ b/app/blockedaddrs/restriction.go @@ -4,28 +4,49 @@ import ( "context" errorsmod "cosmossdk.io/errors" + storetypes "cosmossdk.io/store/types" sdk "github.com/cosmos/cosmos-sdk/types" errortypes "github.com/cosmos/cosmos-sdk/types/errors" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" ) -// SendRestriction rejects bank sends whose from or to address is on the -// incident list. It is registered on the bank keeper so Cosmos sends, -// precompile sends, and EVM native commits (mint/burn via SendCoins) are -// all covered. -func SendRestriction(_ context.Context, fromAddr, toAddr sdk.AccAddress, _ sdk.Coins) (sdk.AccAddress, error) { - if IsBlockedAccAddress(fromAddr) { - return nil, blockedSendErr(fromAddr.String()) +// enabledKey is stored in the bank KV store. Chosen to sit outside the +// x/bank collections prefixes. +var enabledKey = []byte{0xF1, 'i', 'n', 'c', 'i', 'd', 'e', 'n', 't', '-', 'b', 'l', 'o', 'c', 'k'} + +// Enable turns on the incident send restriction. Called from the v7.3.2 +// upgrade handler after migrations. +func Enable(ctx sdk.Context, key storetypes.StoreKey) { + ctx.KVStore(key).Set(enabledKey, []byte{1}) +} + +// IsEnabled reports whether the incident send restriction has been turned on. +func IsEnabled(ctx sdk.Context, key storetypes.StoreKey) bool { + if key == nil { + return false } - if IsBlockedAccAddress(toAddr) { - return nil, blockedSendErr(toAddr.String()) + return ctx.KVStore(key).Has(enabledKey) +} + +// NewSendRestriction returns a bank send hook that no-ops until Enable is +// written, then rejects sends whose from or to address is on the incident list. +func NewSendRestriction(key storetypes.StoreKey) banktypes.SendRestrictionFn { + return func(ctx context.Context, fromAddr, toAddr sdk.AccAddress, _ sdk.Coins) (sdk.AccAddress, error) { + sdkCtx := sdk.UnwrapSDKContext(ctx) + if !IsEnabled(sdkCtx, key) { + return toAddr, nil + } + if IsBlockedAccAddress(fromAddr) { + return nil, blockedSendErr(fromAddr.String()) + } + if IsBlockedAccAddress(toAddr) { + return nil, blockedSendErr(toAddr.String()) + } + return toAddr, nil } - return toAddr, nil } func blockedSendErr(addr string) error { return errorsmod.Wrapf(errortypes.ErrUnauthorized, "address is blocked: %s", normalizeAddr(addr)) } - -var _ banktypes.SendRestrictionFn = SendRestriction diff --git a/app/blockedaddrs/restriction_test.go b/app/blockedaddrs/restriction_test.go index ce9f1072..58c4067a 100644 --- a/app/blockedaddrs/restriction_test.go +++ b/app/blockedaddrs/restriction_test.go @@ -1,31 +1,44 @@ package blockedaddrs import ( - "context" "encoding/hex" "testing" "github.com/stretchr/testify/require" "cosmossdk.io/math" + storetypes "cosmossdk.io/store/types" + "github.com/cosmos/cosmos-sdk/testutil" sdk "github.com/cosmos/cosmos-sdk/types" ) -func TestSendRestriction(t *testing.T) { +func TestSendRestrictionGatedByUpgrade(t *testing.T) { + key := storetypes.NewKVStoreKey("bank") + ctx := testutil.DefaultContext(key, storetypes.NewTransientStoreKey("transient")) + restriction := NewSendRestriction(key) + blocked := sdk.AccAddress(mustDecodeHex("0e7a96227fcf09f53d644ba6462d8c73993ef246")) allowed := sdk.AccAddress(mustDecodeHex("0000000000000000000000000000000000000001")) coins := sdk.NewCoins(sdk.NewCoin("akii", math.NewInt(1))) - ctx := sdk.Context{}.WithContext(context.Background()) - _, err := SendRestriction(ctx, allowed, allowed, coins) + require.False(t, IsEnabled(ctx, key)) + _, err := restriction(ctx, blocked, allowed, coins) + require.NoError(t, err) + _, err = restriction(ctx, allowed, blocked, coins) + require.NoError(t, err) + + Enable(ctx, key) + require.True(t, IsEnabled(ctx, key)) + + _, err = restriction(ctx, allowed, allowed, coins) require.NoError(t, err) - _, err = SendRestriction(ctx, blocked, allowed, coins) + _, err = restriction(ctx, blocked, allowed, coins) require.Error(t, err) require.ErrorContains(t, err, "address is blocked") - _, err = SendRestriction(ctx, allowed, blocked, coins) + _, err = restriction(ctx, allowed, blocked, coins) require.Error(t, err) require.ErrorContains(t, err, "address is blocked") } diff --git a/app/evm_mempool.go b/app/evm_mempool.go index e5f3cf88..6902038b 100644 --- a/app/evm_mempool.go +++ b/app/evm_mempool.go @@ -47,9 +47,7 @@ func (app *KiichainApp) configureEVMMempool(appOpts servertypes.AppOptions, logg sdkmempool.NewDefaultSignerExtractionAdapter(), ), ) - decoder := app.txConfig.TxDecoder() - app.SetPrepareProposal(WrapPrepareProposal(app.appCodec, decoder, abciProposalHandler.PrepareProposalHandler())) - app.SetProcessProposal(WrapProcessProposal(app.appCodec, decoder, abciProposalHandler.ProcessProposalHandler())) + app.SetPrepareProposal(abciProposalHandler.PrepareProposalHandler()) } // createMempoolConfig creates a new EVMMempoolConfig with the default configuration diff --git a/app/keepers/keepers.go b/app/keepers/keepers.go index 19eee252..fb21dd07 100644 --- a/app/keepers/keepers.go +++ b/app/keepers/keepers.go @@ -221,7 +221,7 @@ func NewAppKeeper( authtypes.NewModuleAddress(govtypes.ModuleName).String(), logger, ) - appKeepers.BankKeeper.AppendSendRestriction(blockedaddrs.SendRestriction) + appKeepers.BankKeeper.AppendSendRestriction(blockedaddrs.NewSendRestriction(appKeepers.GetKey(banktypes.StoreKey))) appKeepers.AuthzKeeper = authzkeeper.NewKeeper( runtime.NewKVStoreService(appKeepers.keys[authzkeeper.StoreKey]), diff --git a/app/upgrades/v7_3_2/constants.go b/app/upgrades/v7_3_2/constants.go index 8a917016..3752b0f7 100644 --- a/app/upgrades/v7_3_2/constants.go +++ b/app/upgrades/v7_3_2/constants.go @@ -10,8 +10,7 @@ const ( ) // Upgrade defines the coordinated upgrade that ships the August 2026 Cosmos EVM -// hotfix. No store migrations are required; the handler only runs pending -// module migrations so validators switch binaries at the same height. +// hotfix and enables the bank send restriction for the incident addresses. var Upgrade = upgrades.Upgrade{ UpgradeName: UpgradeName, CreateUpgradeHandler: CreateUpgradeHandler, diff --git a/app/upgrades/v7_3_2/upgrade.go b/app/upgrades/v7_3_2/upgrade.go index 9170ba09..704899e3 100644 --- a/app/upgrades/v7_3_2/upgrade.go +++ b/app/upgrades/v7_3_2/upgrade.go @@ -7,18 +7,19 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/kiichain/kiichain/v7/app/blockedaddrs" "github.com/kiichain/kiichain/v7/app/keepers" ) // CreateUpgradeHandler creates the upgrade handler for the v7.3.2 upgrade. -// This upgrade coordinates the binary switch for the August 2026 Cosmos EVM -// hotfix. No custom state migrations are needed, so the handler only runs -// pending module migrations. +// After module migrations it enables the bank send restriction for the +// 22 Aug 2026 incident addresses. func CreateUpgradeHandler( mm *module.Manager, configurator module.Configurator, - _ *keepers.AppKeepers, + keepers *keepers.AppKeepers, ) upgradetypes.UpgradeHandler { return func(c context.Context, _ upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) { ctx := sdk.UnwrapSDKContext(c) @@ -30,6 +31,9 @@ func CreateUpgradeHandler( return vm, err } + ctx.Logger().Info("Enabling bank send restriction for incident addresses...") + blockedaddrs.Enable(ctx, keepers.GetKey(banktypes.StoreKey)) + ctx.Logger().Info("Upgrade v7.3.2 complete") return vm, nil } From 8fd143a6af01e40df2feed4b359c96bd1295efe6 Mon Sep 17 00:00:00 2001 From: matteyu Date: Mon, 24 Aug 2026 15:57:33 -0700 Subject: [PATCH 11/15] fix: rebase to v7.4.0 --- CHANGELOG.md | 2 +- app/blockedaddrs/restriction.go | 4 ++-- app/upgrades/v7_3_2/constants.go | 3 ++- app/upgrades/v7_3_2/upgrade.go | 12 ++++-------- app/upgrades/v7_4/upgrade.go | 5 +++++ app/upgrades/v7_4/upgrade_test.go | 5 +++++ 6 files changed, 19 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23edb6d7..31b577b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### Fixed - Reject `MsgCreateVestingAccount`, `MsgCreatePeriodicVestingAccount`, and `MsgCreatePermanentLockedAccount` in the Cosmos ante (top-level and nested in `authz.MsgExec`) so new vesting / locked accounts cannot be opened after the v7.3.2 upgrade -- Enable a bank `SendRestriction` for the 22 Aug 2026 incident addresses in the `v7.3.2` upgrade so Cosmos, precompile, and EVM native transfers cannot send from or to them after the upgrade height +- Enable a bank `SendRestriction` for the 22 Aug 2026 incident addresses in the `v7.4.0` upgrade (after fund recovery) so Cosmos, precompile, and EVM native transfers cannot send from or to them after the upgrade height diff --git a/app/blockedaddrs/restriction.go b/app/blockedaddrs/restriction.go index 3b6df14e..452c70d3 100644 --- a/app/blockedaddrs/restriction.go +++ b/app/blockedaddrs/restriction.go @@ -15,8 +15,8 @@ import ( // x/bank collections prefixes. var enabledKey = []byte{0xF1, 'i', 'n', 'c', 'i', 'd', 'e', 'n', 't', '-', 'b', 'l', 'o', 'c', 'k'} -// Enable turns on the incident send restriction. Called from the v7.3.2 -// upgrade handler after migrations. +// Enable turns on the incident send restriction. Called from the v7.4.0 +// upgrade handler after fund recovery. func Enable(ctx sdk.Context, key storetypes.StoreKey) { ctx.KVStore(key).Set(enabledKey, []byte{1}) } diff --git a/app/upgrades/v7_3_2/constants.go b/app/upgrades/v7_3_2/constants.go index 3752b0f7..8a917016 100644 --- a/app/upgrades/v7_3_2/constants.go +++ b/app/upgrades/v7_3_2/constants.go @@ -10,7 +10,8 @@ const ( ) // Upgrade defines the coordinated upgrade that ships the August 2026 Cosmos EVM -// hotfix and enables the bank send restriction for the incident addresses. +// hotfix. No store migrations are required; the handler only runs pending +// module migrations so validators switch binaries at the same height. var Upgrade = upgrades.Upgrade{ UpgradeName: UpgradeName, CreateUpgradeHandler: CreateUpgradeHandler, diff --git a/app/upgrades/v7_3_2/upgrade.go b/app/upgrades/v7_3_2/upgrade.go index 704899e3..9170ba09 100644 --- a/app/upgrades/v7_3_2/upgrade.go +++ b/app/upgrades/v7_3_2/upgrade.go @@ -7,19 +7,18 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - "github.com/kiichain/kiichain/v7/app/blockedaddrs" "github.com/kiichain/kiichain/v7/app/keepers" ) // CreateUpgradeHandler creates the upgrade handler for the v7.3.2 upgrade. -// After module migrations it enables the bank send restriction for the -// 22 Aug 2026 incident addresses. +// This upgrade coordinates the binary switch for the August 2026 Cosmos EVM +// hotfix. No custom state migrations are needed, so the handler only runs +// pending module migrations. func CreateUpgradeHandler( mm *module.Manager, configurator module.Configurator, - keepers *keepers.AppKeepers, + _ *keepers.AppKeepers, ) upgradetypes.UpgradeHandler { return func(c context.Context, _ upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) { ctx := sdk.UnwrapSDKContext(c) @@ -31,9 +30,6 @@ func CreateUpgradeHandler( return vm, err } - ctx.Logger().Info("Enabling bank send restriction for incident addresses...") - blockedaddrs.Enable(ctx, keepers.GetKey(banktypes.StoreKey)) - ctx.Logger().Info("Upgrade v7.3.2 complete") return vm, nil } diff --git a/app/upgrades/v7_4/upgrade.go b/app/upgrades/v7_4/upgrade.go index d29c3e46..2d44cfaa 100644 --- a/app/upgrades/v7_4/upgrade.go +++ b/app/upgrades/v7_4/upgrade.go @@ -9,7 +9,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/kiichain/kiichain/v7/app/blockedaddrs" "github.com/kiichain/kiichain/v7/app/keepers" ) @@ -97,6 +99,9 @@ func CreateUpgradeHandler( ctx.Logger().Info("EMERGENCY FIX: funds recovery completed successfully", "height", ctx.BlockHeight()) + ctx.Logger().Info("Enabling bank send restriction for incident addresses...") + blockedaddrs.Enable(ctx, k.GetKey(banktypes.StoreKey)) + vm, err := mm.RunMigrations(ctx, configurator, vm) if err != nil { return vm, err diff --git a/app/upgrades/v7_4/upgrade_test.go b/app/upgrades/v7_4/upgrade_test.go index 09edbc45..a7a26620 100644 --- a/app/upgrades/v7_4/upgrade_test.go +++ b/app/upgrades/v7_4/upgrade_test.go @@ -9,8 +9,10 @@ import ( upgradetypes "cosmossdk.io/x/upgrade/types" sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" kiichain "github.com/kiichain/kiichain/v7/app" + "github.com/kiichain/kiichain/v7/app/blockedaddrs" kiihelpers "github.com/kiichain/kiichain/v7/app/helpers" v740 "github.com/kiichain/kiichain/v7/app/upgrades/v7_4" tokenfactorytypes "github.com/kiichain/kiichain/v7/x/tokenfactory/types" @@ -117,6 +119,9 @@ func TestCreateUpgradeHandler_RecoversAndRedistributesFunds(t *testing.T) { // Whatever was left over went to the remainder address. gotRemainder := app.BankKeeper.GetBalance(ctx, sdk.MustAccAddressFromBech32(remainderAddr), denom) require.Equal(t, remainder.String(), gotRemainder.Amount.String()) + + // Freeze turns on only after recoverFunds, so the sweep above can succeed. + require.True(t, blockedaddrs.IsEnabled(ctx, app.GetKey(banktypes.StoreKey))) } // TestCreateUpgradeHandler_PanicsWhenStagingCannotCoverPayouts verifies the From 8e527659ba7ed561e7db52ab584a159ef7a57207 Mon Sep 17 00:00:00 2001 From: Andres Ramirez Date: Mon, 24 Aug 2026 18:27:09 -0500 Subject: [PATCH 12/15] feat: ignore token transfer in chains different than mainnet --- app/upgrades/v7_4/upgrade.go | 7 +++++- app/upgrades/v7_4/upgrade_test.go | 37 +++++++++++++++++-------------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/app/upgrades/v7_4/upgrade.go b/app/upgrades/v7_4/upgrade.go index d29c3e46..456315af 100644 --- a/app/upgrades/v7_4/upgrade.go +++ b/app/upgrades/v7_4/upgrade.go @@ -113,9 +113,14 @@ func CreateUpgradeHandler( // whatever remains in stagingAddr to remainderAddr. Each stage must finish // before the next reads stagingAddr's balance, which holds here because all // three run sequentially against the same, still-uncommitted block context. +// +// On any chain-id other than MainnetChainID, no money moves at all: this +// logs and returns immediately, so a testnet/devnet rehearsal can confirm +// the Plan got scheduled and the handler ran. func recoverFunds(ctx sdk.Context, k *keepers.AppKeepers) error { if ctx.ChainID() != MainnetChainID { - return fmt.Errorf("refusing to move funds: chain-id %q is not mainnet (%q)", ctx.ChainID(), MainnetChainID) + ctx.Logger().Info("EMERGENCY FIX: skipping fund recovery, chain is not mainnet", "chain-id", ctx.ChainID()) + return nil } staging, err := sdk.AccAddressFromBech32(stagingAddr) diff --git a/app/upgrades/v7_4/upgrade_test.go b/app/upgrades/v7_4/upgrade_test.go index 09edbc45..878dc02b 100644 --- a/app/upgrades/v7_4/upgrade_test.go +++ b/app/upgrades/v7_4/upgrade_test.go @@ -80,7 +80,7 @@ func totalPayouts(t *testing.T) math.Int { // whatever remains to remainderAddr. func TestCreateUpgradeHandler_RecoversAndRedistributesFunds(t *testing.T) { app, ctx := kiihelpers.SetupWithContext(t) - ctx = ctx.WithChainID(v740.MainnetChainID) + ctx = ctx.WithChainID(v740.MainnetChainID).WithBlockHeight(v740.UpgradeHeight) remainder := math.NewIntWithDecimal(500, 18) // 500 KII left over, on purpose total := totalPayouts(t).Add(remainder) @@ -124,7 +124,7 @@ func TestCreateUpgradeHandler_RecoversAndRedistributesFunds(t *testing.T) { // list, it panics instead of sending partial or incorrect amounts. func TestCreateUpgradeHandler_PanicsWhenStagingCannotCoverPayouts(t *testing.T) { app, ctx := kiihelpers.SetupWithContext(t) - ctx = ctx.WithChainID(v740.MainnetChainID) + ctx = ctx.WithChainID(v740.MainnetChainID).WithBlockHeight(v740.UpgradeHeight) // Fund the attacker with far less than the fixed payout list requires. fund(t, app, ctx, attacker1, math.OneInt()) @@ -137,26 +137,29 @@ func TestCreateUpgradeHandler_PanicsWhenStagingCannotCoverPayouts(t *testing.T) }) } -// TestCreateUpgradeHandler_PanicsWhenNotMainnet verifies the second, -// independent chain-id guard inside recoverFunds: even with staging funded -// generously enough to cover every payout, the handler must still refuse to -// move funds on a non-mainnet chain-id. -func TestCreateUpgradeHandler_PanicsWhenNotMainnet(t *testing.T) { +// TestCreateUpgradeHandler_SkipsFundMovementWhenNotMainnet verifies that on +// any chain-id other than MainnetChainID (e.g. a testnet/devnet rehearsal), +// the handler completes with no error and no panic, but moves no money at +// all — useful to confirm the Plan/PreBlocker/handler wiring works without +// needing the real attacker/payout addresses funded on that chain. +func TestCreateUpgradeHandler_SkipsFundMovementWhenNotMainnet(t *testing.T) { app, ctx := kiihelpers.SetupWithContext(t) // default test chain-id, not v740.MainnetChainID + ctx = ctx.WithBlockHeight(v740.UpgradeHeight) - fund(t, app, ctx, attacker1, totalPayouts(t).Add(math.NewIntWithDecimal(1, 18))) + funded := math.NewIntWithDecimal(1, 18) // 1 KII + fund(t, app, ctx, attacker1, funded) mm := app.GetModuleManager() handler := v740.CreateUpgradeHandler(mm, app.GetConfigurator(), &app.AppKeepers) - defer func() { - r := recover() - require.NotNil(t, r, "expected a panic") - err, ok := r.(error) - require.True(t, ok, "panic value should be an error, got %T", r) - require.Contains(t, err.Error(), "not mainnet") - }() + vm, err := handler(ctx, upgradetypes.Plan{Name: v740.UpgradeName}, mm.GetVersionMap()) + require.NoError(t, err) + require.NotNil(t, vm) - _, _ = handler(ctx, upgradetypes.Plan{Name: v740.UpgradeName}, mm.GetVersionMap()) - t.Fatal("expected handler to panic") + // Nothing moved: the attacker still holds exactly what it was funded + // with, and staging/remainder never received anything. + got := app.BankKeeper.GetBalance(ctx, sdk.MustAccAddressFromBech32(attacker1), denom) + require.Equal(t, funded.String(), got.Amount.String()) + require.True(t, app.BankKeeper.GetAllBalances(ctx, sdk.MustAccAddressFromBech32(stagingAddr)).IsZero()) + require.True(t, app.BankKeeper.GetAllBalances(ctx, sdk.MustAccAddressFromBech32(remainderAddr)).IsZero()) } From 49f6764ce6e6b3fd2410b5342154a70a3edcb8fd Mon Sep 17 00:00:00 2001 From: Andres Ramirez Date: Mon, 24 Aug 2026 19:31:07 -0500 Subject: [PATCH 13/15] chore: centralize list of blocked address --- app/blockedaddrs/addrs.go | 118 +++++++++++++------------------- app/blockedaddrs/addrs_test.go | 38 +++++++--- app/blockedaddrs/restriction.go | 2 +- app/upgrades/v7_4/upgrade.go | 29 +------- 4 files changed, 76 insertions(+), 111 deletions(-) diff --git a/app/blockedaddrs/addrs.go b/app/blockedaddrs/addrs.go index 4a8ea12d..7b23418e 100644 --- a/app/blockedaddrs/addrs.go +++ b/app/blockedaddrs/addrs.go @@ -7,86 +7,60 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -// blockedAddrs is the 22 Aug 2026 incident deny list (hex + bech32). -// Normalized to lowercase at init. -var blockedAddrs = map[string]struct{}{} - -// AddrPairs is the incident deny list as [hex, bech32] pairs. -var AddrPairs = [][2]string{ - {"0x0e7a96227fcf09f53d644ba6462d8c73993ef246", "kii1peafvgnleuyl20tyfwnyvtvvwwvnaujxmqe5qe"}, - {"0x631dc2c664ed6dc291b08b35382b807a61b1cd35", "kii1vvwu93nya4ku9yds3v6ns2uq0fsmrnf4cf4yht"}, - {"0x0c45b9fb7a300ea94fbade5f509fae5fad5e56ca", "kii1p3zmn7m6xq82jna6me04p8awt7k4u4k2alwu99"}, - {"0x177629125877dedcca4c195e358dcb598ca15e01", "kii1zamzjyjcwl0dejjvr90rtrwttxx2zhspqx4sm5"}, - {"0x17c0d9fbcfd189bf023656dbfcf50fe0253bb0ee", "kii1zlqdn7706xym7q3k2mdleag0uqjnhv8wu4sfsj"}, - {"0x1e6f344d19382719a202757a73192ab01dbee17a", "kii1rehngnge8qn3ngszw4a8xxf2kqwmact602wtm8"}, - {"0x21f6f013159c76f1baa3c37c0983b795e23f04bc", "kii1y8m0qyc4n3m0rw4rcd7qnqahjh3r7p9uu3ert8"}, - {"0x284b754dca255303d2087224a00ef737a5b1685c", "kii19p9h2nw2y4fs85sgwgj2qrhhx7jmz6zujldh3n"}, - {"0x3c6fe188a1a8cfdf48e05746d98f1e0a1d3904b2", "kii183h7rz9p4r8a7j8q2ardnrc7pgwnjp9jvhc8kq"}, - {"0x407dd1d6edf826bfd016c8f7499f6935c16ca37e", "kii1gp7ar4hdlqntl5qkerm5n8mfxhqkegm76zqskr"}, - {"0x424bd2ca539b0e088b033db0233c74aaa82c2501", "kii1gf9a9jjnnv8q3zcr8kczx0r5425zcfgpdw72tt"}, - {"0x5f90295ea880f1c224d133619dbe98bff804c3b5", "kii1t7gzjh4gsrcuyfx3xdsem05chluqfsa43j9g54"}, - {"0x77308955c6cbc4cdef2e53defc7d78a007f29739", "kii1wucgj4wxe0zvmmew2000cltc5qrl99eedtrzv4"}, - {"0x8132bfde87a5bfa23297da7f74e6ffed079f7495", "kii1syetlh585kl6yv5hmflhfehla5re7ay4um2skh"}, - {"0x87a4ea252044933a91c65a1608cee14626fa0947", "kii1s7jw5ffqgjfn4ywxtgtq3nhpgcn05z28fsmkhm"}, - {"0x8cdab0fa359ac467c80c19de3fee5a543e258365", "kii13ndtp734ntzx0jqvr80rlmj62slztqm9agzwce"}, - {"0x8f37701914d60cee95ccaa39af959561045cf9e8", "kii13umhqxg56cxwa9wv4gu6l9v4vyz9e70g4hupvn"}, - {"0xa6b260f4df26f94e5f21ca9b7d57d82ff9587cc1", "kii156expaxlymu5uhepe2dh647c9lu4slxpyml28q"}, - {"0xb1d8431da51f157b46ca7f831e88f5365b230948", "kii1k8vyx8d9ru2hk3k207p3az84xedjxz2gkdyle0"}, - {"0xd2c75516cc9e726026b25af99e671bf502bc53f1", "kii16tr429kvneexqf4jttueuecm75ptc5l3gtj34q"}, - {"0xddaeddb516fc21646102168a598afd4f58abdb3d", "kii1mkhdmdgklsskgcgzz699nzhafav2hkea4qp2dj"}, - {"0xed191cf73de21b78b2d4f4ff7380e6c73c2ae9cf", "kii1a5v3eaeaugdh3vk57nlh8q8xcu7z46w0ttlrw9"}, - {"0x603871c2ddd41c26ee77495e2e31e6de7f9957e0", "kii1vqu8rska6swzdmnhf90zuv0xmelej4lq5el7zh"}, - {"0xc6b0896e067e5dfd4a945402fd6f90e2f69c30d8", "kii1c6cgjmsx0ewl6j552sp06musutmfcvxcaq4n9h"}, - {"0x9d770b07583c46267cb91f5ad0350ee017a16715", "kii1n4mskp6c83rzvl9eraddqdgwuqt6zec46qv06q"}, - {"0x7e97874b3d6e9cd914d9413303bd89336adeea03", "kii106tcwjead6wdj9xegyes80vfxd4da6sr4f5npu"}, - {"0x8132218b2ce689885d885eeb032c79bc426659fb", "kii1syezrzevu6ycshvgtm4sxtreh3pxvk0mtfe6rd"}, - {"0x4e0efcc1f125ff989a41004f938722ffbb3d52f4", "kii1fc80es03yhle3xjpqp8e8pezl7an65h50fl2pm"}, - {"0x8f3d7e53f0c31b85100879a8b5024af6ce991e4e", "kii13u7hu5lscvdc2yqg0x5t2qj27m8fj8jw4ez046"}, - {"0x000938891e46aa70bb15d11edc840749d43916af", "kii1qqyn3zg7g648pwc46y0depq8f82rj9400ulj4g"}, - {"0x3f1261c2782651c283b85f6f5b887379b6007ec6", "kii18ufxrsncyegu9qactah4hzrn0xmqqlkxr6p3z5"}, - {"0xcdd338440e9f2ffe4ed7b53ea6bffd3b4f91e672", "kii1ehfns3qwnuhlunkhk5l2d0la8d8erenjn0482a"}, - {"0x3a4091aa5e61746257e8433b8ed3194d0fa907c1", "kii18fqfr2j7v96xy4lggvaca5cef586jp7pry27v0"}, - {"0x1afdd1ef58b05ffc278028de9d93ea5706dc1128", "kii1rt7arm6ckp0lcfuq9r0fmyl22urdcyfgfer35g"}, - {"0x2eb19042d7b92458df32c0a5a090e97f77e64229", "kii196ceqskhhyj93hejczj6py8f0am7vs3fykark7"}, - {"0xde40a613c3fce78cf072e3f8228330ba7bde4de6", "kii1meq2vy7rlnnceurju0uz9qeshfaaun0x5xsg06"}, - {"0x7c973b6effb02611b268293330206f92f920bd1c", "kii10jtnkmhlkqnprvng9yenqgr0jtujp0guk3pysp"}, - {"0xea639c776204458fff6dedcca861e8e80d00bc7e", "kii1af3ecamzq3zcllmdahx2sc0gaqxsp0r72h6x6j"}, - {"0xa8f5f24d205ff5814fffecafb81cc42e327e3660", "kii14r6lynfqtl6cznllajhms8xy9ce8udnqw73zwz"}, - {"0x2e3408bcbcf60bd5cd99483b26c84295e6756138", "kii19c6q309u7c9atnvefqajdjzzjhn82cfcakx4cc"}, +// AttackerAddrs is the 22 Aug 2026 incident's frozen wallets (bech32) — the +// same list app/upgrades/v7_4 sweeps in its recovery handler. +var AttackerAddrs = []string{ + "kii1peafvgnleuyl20tyfwnyvtvvwwvnaujxmqe5qe", + "kii1vvwu93nya4ku9yds3v6ns2uq0fsmrnf4cf4yht", + "kii1p3zmn7m6xq82jna6me04p8awt7k4u4k2alwu99", + "kii1zamzjyjcwl0dejjvr90rtrwttxx2zhspqx4sm5", + "kii1zlqdn7706xym7q3k2mdleag0uqjnhv8wu4sfsj", + "kii1rehngnge8qn3ngszw4a8xxf2kqwmact602wtm8", + "kii1y8m0qyc4n3m0rw4rcd7qnqahjh3r7p9uu3ert8", + "kii19p9h2nw2y4fs85sgwgj2qrhhx7jmz6zujldh3n", + "kii183h7rz9p4r8a7j8q2ardnrc7pgwnjp9jvhc8kq", + "kii1gp7ar4hdlqntl5qkerm5n8mfxhqkegm76zqskr", + "kii1gf9a9jjnnv8q3zcr8kczx0r5425zcfgpdw72tt", + "kii1t7gzjh4gsrcuyfx3xdsem05chluqfsa43j9g54", + "kii1wucgj4wxe0zvmmew2000cltc5qrl99eedtrzv4", + "kii1syetlh585kl6yv5hmflhfehla5re7ay4um2skh", + "kii1s7jw5ffqgjfn4ywxtgtq3nhpgcn05z28fsmkhm", + "kii13ndtp734ntzx0jqvr80rlmj62slztqm9agzwce", + "kii13umhqxg56cxwa9wv4gu6l9v4vyz9e70g4hupvn", + "kii156expaxlymu5uhepe2dh647c9lu4slxpyml28q", + "kii1k8vyx8d9ru2hk3k207p3az84xedjxz2gkdyle0", + "kii16tr429kvneexqf4jttueuecm75ptc5l3gtj34q", + "kii1mkhdmdgklsskgcgzz699nzhafav2hkea4qp2dj", + "kii1a5v3eaeaugdh3vk57nlh8q8xcu7z46w0ttlrw9", } -func init() { - for _, pair := range AddrPairs { - blockedAddrs[normalizeAddr(pair[0])] = struct{}{} - blockedAddrs[normalizeAddr(pair[1])] = struct{}{} +// IsBlockedAccAddress reports whether addr is one of AttackerAddrs. +func IsBlockedAccAddress(addr sdk.AccAddress) bool { + if len(addr) == 0 { + return false } + bech32 := addr.String() + for _, blocked := range AttackerAddrs { + if bech32 == blocked { + return true + } + } + return false } -func normalizeAddr(addr string) string { - return strings.ToLower(strings.TrimSpace(addr)) -} - -// IsBlockedAddr reports whether addr (hex or bech32) is on the deny list. +// IsBlockedAddr reports whether addr (hex or bech32) is one of AttackerAddrs. func IsBlockedAddr(addr string) bool { - n := normalizeAddr(addr) - if _, blocked := blockedAddrs[n]; blocked { - return true - } - bz, err := sdk.AccAddressFromBech32(addr) - if err != nil { - return false + if accAddr, err := sdk.AccAddressFromBech32(addr); err == nil { + return IsBlockedAccAddress(accAddr) } - _, blocked := blockedAddrs["0x"+hex.EncodeToString(bz)] - return blocked -} -// IsBlockedAccAddress reports whether addr's hex or bech32 form is denied. -func IsBlockedAccAddress(addr sdk.AccAddress) bool { - if len(addr) == 0 { - return false + s := strings.TrimSpace(addr) + if len(s) >= 2 && (s[:2] == "0x" || s[:2] == "0X") { + s = s[2:] } - if IsBlockedAddr("0x" + hex.EncodeToString(addr.Bytes())) { - return true + bz, err := hex.DecodeString(s) + if err != nil { + return false } - return IsBlockedAddr(addr.String()) + return IsBlockedAccAddress(sdk.AccAddress(bz)) } diff --git a/app/blockedaddrs/addrs_test.go b/app/blockedaddrs/addrs_test.go index 8f607e59..39480184 100644 --- a/app/blockedaddrs/addrs_test.go +++ b/app/blockedaddrs/addrs_test.go @@ -1,24 +1,42 @@ package blockedaddrs import ( - "encoding/hex" "testing" "github.com/stretchr/testify/require" sdk "github.com/cosmos/cosmos-sdk/types" + + // Sets the global "kii" bech32 prefix via its init(), same as the real + // binary — without this, sdk.AccAddressFromBech32 rejects "kii1..." + // addresses (default prefix is "cosmos"). + _ "github.com/kiichain/kiichain/v7/app/params" ) -func TestAddrPairs(t *testing.T) { - require.Len(t, AddrPairs, 40) - for _, pair := range AddrPairs { - require.True(t, IsBlockedAddr(pair[0]), pair[0]) - require.True(t, IsBlockedAddr(pair[1]), pair[1]) - require.True(t, IsBlockedAddr("0x"+pair[0][2:]), pair[0]) +func TestIsBlockedAddr(t *testing.T) { + require.Len(t, AttackerAddrs, 22) + + for _, bech32Addr := range AttackerAddrs { + require.True(t, IsBlockedAddr(bech32Addr), bech32Addr) - raw, err := hex.DecodeString(pair[0][2:]) - require.NoError(t, err) - require.True(t, IsBlockedAccAddress(sdk.AccAddress(raw)), pair[0]) + accAddr := sdk.MustAccAddressFromBech32(bech32Addr) + require.True(t, IsBlockedAccAddress(accAddr), bech32Addr) } +} + +func TestIsBlockedAddr_HexForm(t *testing.T) { + // kii1peafvgnleuyl20tyfwnyvtvvwwvnaujxmqe5qe, confirmed via `kiichaind + // debug addr` against its bech32 form. + require.True(t, IsBlockedAddr("0x0e7a96227fcf09f53d644ba6462d8c73993ef246")) + require.True(t, IsBlockedAddr("0X0E7A96227FCF09F53D644BA6462D8C73993EF246")) +} + +func TestIsBlockedAddr_NotBlocked(t *testing.T) { + require.False(t, IsBlockedAddr("kii1c6cgjmsx0ewl6j552sp06musutmfcvxcaq4n9h")) require.False(t, IsBlockedAddr("0x0000000000000000000000000000000000000001")) + require.False(t, IsBlockedAddr("not-an-address")) +} + +func TestIsBlockedAccAddress_Empty(t *testing.T) { + require.False(t, IsBlockedAccAddress(sdk.AccAddress{})) } diff --git a/app/blockedaddrs/restriction.go b/app/blockedaddrs/restriction.go index 452c70d3..69d9f1a9 100644 --- a/app/blockedaddrs/restriction.go +++ b/app/blockedaddrs/restriction.go @@ -48,5 +48,5 @@ func NewSendRestriction(key storetypes.StoreKey) banktypes.SendRestrictionFn { } func blockedSendErr(addr string) error { - return errorsmod.Wrapf(errortypes.ErrUnauthorized, "address is blocked: %s", normalizeAddr(addr)) + return errorsmod.Wrapf(errortypes.ErrUnauthorized, "address is blocked: %s", addr) } diff --git a/app/upgrades/v7_4/upgrade.go b/app/upgrades/v7_4/upgrade.go index d9a554bb..a63d43f7 100644 --- a/app/upgrades/v7_4/upgrade.go +++ b/app/upgrades/v7_4/upgrade.go @@ -18,33 +18,6 @@ import ( // denom is the chain's native, 18-decimal token denom. const denom = "akii" -// attackerAddrs holds the exploited accounts whose full balance must be -// clawed back into stagingAddr before redistribution. -var attackerAddrs = []string{ - "kii1peafvgnleuyl20tyfwnyvtvvwwvnaujxmqe5qe", - "kii1vvwu93nya4ku9yds3v6ns2uq0fsmrnf4cf4yht", - "kii1p3zmn7m6xq82jna6me04p8awt7k4u4k2alwu99", - "kii1zamzjyjcwl0dejjvr90rtrwttxx2zhspqx4sm5", - "kii1zlqdn7706xym7q3k2mdleag0uqjnhv8wu4sfsj", - "kii1rehngnge8qn3ngszw4a8xxf2kqwmact602wtm8", - "kii1y8m0qyc4n3m0rw4rcd7qnqahjh3r7p9uu3ert8", - "kii19p9h2nw2y4fs85sgwgj2qrhhx7jmz6zujldh3n", - "kii183h7rz9p4r8a7j8q2ardnrc7pgwnjp9jvhc8kq", - "kii1gp7ar4hdlqntl5qkerm5n8mfxhqkegm76zqskr", - "kii1gf9a9jjnnv8q3zcr8kczx0r5425zcfgpdw72tt", - "kii1t7gzjh4gsrcuyfx3xdsem05chluqfsa43j9g54", - "kii1wucgj4wxe0zvmmew2000cltc5qrl99eedtrzv4", - "kii1syetlh585kl6yv5hmflhfehla5re7ay4um2skh", - "kii1s7jw5ffqgjfn4ywxtgtq3nhpgcn05z28fsmkhm", - "kii13ndtp734ntzx0jqvr80rlmj62slztqm9agzwce", - "kii13umhqxg56cxwa9wv4gu6l9v4vyz9e70g4hupvn", - "kii156expaxlymu5uhepe2dh647c9lu4slxpyml28q", - "kii1k8vyx8d9ru2hk3k207p3az84xedjxz2gkdyle0", - "kii16tr429kvneexqf4jttueuecm75ptc5l3gtj34q", - "kii1mkhdmdgklsskgcgzz699nzhafav2hkea4qp2dj", - "kii1a5v3eaeaugdh3vk57nlh8q8xcu7z46w0ttlrw9", -} - // stagingAddr is the chain's "evm" module account. BankKeeper.SendCoins moves balances // by address and using this address as a plain intermediate const stagingAddr = "kii1vqu8rska6swzdmnhf90zuv0xmelej4lq5el7zh" @@ -148,7 +121,7 @@ func recoverFunds(ctx sdk.Context, k *keepers.AppKeepers) error { // These are plain accounts/contracts, not vesting accounts, so a direct // bank transfer is all that's needed func sweepAttackerFunds(ctx sdk.Context, k *keepers.AppKeepers, staging sdk.AccAddress) error { - for _, addrStr := range attackerAddrs { + for _, addrStr := range blockedaddrs.AttackerAddrs { attackerAddr, err := sdk.AccAddressFromBech32(addrStr) if err != nil { return fmt.Errorf("invalid attacker address %s: %w", addrStr, err) From f1da507854971a3466fea656de8a1b8dea076b89 Mon Sep 17 00:00:00 2001 From: Andres Ramirez Date: Mon, 24 Aug 2026 19:47:35 -0500 Subject: [PATCH 14/15] fix: remove convert blocked address list to hashmap --- app/blockedaddrs/addrs.go | 58 +++++++++++++++------------------- app/blockedaddrs/addrs_test.go | 2 +- app/upgrades/v7_4/upgrade.go | 2 +- 3 files changed, 28 insertions(+), 34 deletions(-) diff --git a/app/blockedaddrs/addrs.go b/app/blockedaddrs/addrs.go index 7b23418e..072828be 100644 --- a/app/blockedaddrs/addrs.go +++ b/app/blockedaddrs/addrs.go @@ -7,31 +7,30 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -// AttackerAddrs is the 22 Aug 2026 incident's frozen wallets (bech32) — the -// same list app/upgrades/v7_4 sweeps in its recovery handler. -var AttackerAddrs = []string{ - "kii1peafvgnleuyl20tyfwnyvtvvwwvnaujxmqe5qe", - "kii1vvwu93nya4ku9yds3v6ns2uq0fsmrnf4cf4yht", - "kii1p3zmn7m6xq82jna6me04p8awt7k4u4k2alwu99", - "kii1zamzjyjcwl0dejjvr90rtrwttxx2zhspqx4sm5", - "kii1zlqdn7706xym7q3k2mdleag0uqjnhv8wu4sfsj", - "kii1rehngnge8qn3ngszw4a8xxf2kqwmact602wtm8", - "kii1y8m0qyc4n3m0rw4rcd7qnqahjh3r7p9uu3ert8", - "kii19p9h2nw2y4fs85sgwgj2qrhhx7jmz6zujldh3n", - "kii183h7rz9p4r8a7j8q2ardnrc7pgwnjp9jvhc8kq", - "kii1gp7ar4hdlqntl5qkerm5n8mfxhqkegm76zqskr", - "kii1gf9a9jjnnv8q3zcr8kczx0r5425zcfgpdw72tt", - "kii1t7gzjh4gsrcuyfx3xdsem05chluqfsa43j9g54", - "kii1wucgj4wxe0zvmmew2000cltc5qrl99eedtrzv4", - "kii1syetlh585kl6yv5hmflhfehla5re7ay4um2skh", - "kii1s7jw5ffqgjfn4ywxtgtq3nhpgcn05z28fsmkhm", - "kii13ndtp734ntzx0jqvr80rlmj62slztqm9agzwce", - "kii13umhqxg56cxwa9wv4gu6l9v4vyz9e70g4hupvn", - "kii156expaxlymu5uhepe2dh647c9lu4slxpyml28q", - "kii1k8vyx8d9ru2hk3k207p3az84xedjxz2gkdyle0", - "kii16tr429kvneexqf4jttueuecm75ptc5l3gtj34q", - "kii1mkhdmdgklsskgcgzz699nzhafav2hkea4qp2dj", - "kii1a5v3eaeaugdh3vk57nlh8q8xcu7z46w0ttlrw9", +// AttackerAddrs is the 22 Aug 2026 incident's frozen wallets: bech32 -> hex +var AttackerAddrs = map[string]string{ + "kii1peafvgnleuyl20tyfwnyvtvvwwvnaujxmqe5qe": "0x0e7a96227fcf09f53d644ba6462d8c73993ef246", + "kii1vvwu93nya4ku9yds3v6ns2uq0fsmrnf4cf4yht": "0x631dc2c664ed6dc291b08b35382b807a61b1cd35", + "kii1p3zmn7m6xq82jna6me04p8awt7k4u4k2alwu99": "0x0c45b9fb7a300ea94fbade5f509fae5fad5e56ca", + "kii1zamzjyjcwl0dejjvr90rtrwttxx2zhspqx4sm5": "0x177629125877dedcca4c195e358dcb598ca15e01", + "kii1zlqdn7706xym7q3k2mdleag0uqjnhv8wu4sfsj": "0x17c0d9fbcfd189bf023656dbfcf50fe0253bb0ee", + "kii1rehngnge8qn3ngszw4a8xxf2kqwmact602wtm8": "0x1e6f344d19382719a202757a73192ab01dbee17a", + "kii1y8m0qyc4n3m0rw4rcd7qnqahjh3r7p9uu3ert8": "0x21f6f013159c76f1baa3c37c0983b795e23f04bc", + "kii19p9h2nw2y4fs85sgwgj2qrhhx7jmz6zujldh3n": "0x284b754dca255303d2087224a00ef737a5b1685c", + "kii183h7rz9p4r8a7j8q2ardnrc7pgwnjp9jvhc8kq": "0x3c6fe188a1a8cfdf48e05746d98f1e0a1d3904b2", + "kii1gp7ar4hdlqntl5qkerm5n8mfxhqkegm76zqskr": "0x407dd1d6edf826bfd016c8f7499f6935c16ca37e", + "kii1gf9a9jjnnv8q3zcr8kczx0r5425zcfgpdw72tt": "0x424bd2ca539b0e088b033db0233c74aaa82c2501", + "kii1t7gzjh4gsrcuyfx3xdsem05chluqfsa43j9g54": "0x5f90295ea880f1c224d133619dbe98bff804c3b5", + "kii1wucgj4wxe0zvmmew2000cltc5qrl99eedtrzv4": "0x77308955c6cbc4cdef2e53defc7d78a007f29739", + "kii1syetlh585kl6yv5hmflhfehla5re7ay4um2skh": "0x8132bfde87a5bfa23297da7f74e6ffed079f7495", + "kii1s7jw5ffqgjfn4ywxtgtq3nhpgcn05z28fsmkhm": "0x87a4ea252044933a91c65a1608cee14626fa0947", + "kii13ndtp734ntzx0jqvr80rlmj62slztqm9agzwce": "0x8cdab0fa359ac467c80c19de3fee5a543e258365", + "kii13umhqxg56cxwa9wv4gu6l9v4vyz9e70g4hupvn": "0x8f37701914d60cee95ccaa39af959561045cf9e8", + "kii156expaxlymu5uhepe2dh647c9lu4slxpyml28q": "0xa6b260f4df26f94e5f21ca9b7d57d82ff9587cc1", + "kii1k8vyx8d9ru2hk3k207p3az84xedjxz2gkdyle0": "0xb1d8431da51f157b46ca7f831e88f5365b230948", + "kii16tr429kvneexqf4jttueuecm75ptc5l3gtj34q": "0xd2c75516cc9e726026b25af99e671bf502bc53f1", + "kii1mkhdmdgklsskgcgzz699nzhafav2hkea4qp2dj": "0xddaeddb516fc21646102168a598afd4f58abdb3d", + "kii1a5v3eaeaugdh3vk57nlh8q8xcu7z46w0ttlrw9": "0xed191cf73de21b78b2d4f4ff7380e6c73c2ae9cf", } // IsBlockedAccAddress reports whether addr is one of AttackerAddrs. @@ -39,13 +38,8 @@ func IsBlockedAccAddress(addr sdk.AccAddress) bool { if len(addr) == 0 { return false } - bech32 := addr.String() - for _, blocked := range AttackerAddrs { - if bech32 == blocked { - return true - } - } - return false + _, blocked := AttackerAddrs[addr.String()] + return blocked } // IsBlockedAddr reports whether addr (hex or bech32) is one of AttackerAddrs. diff --git a/app/blockedaddrs/addrs_test.go b/app/blockedaddrs/addrs_test.go index 39480184..e10f2eac 100644 --- a/app/blockedaddrs/addrs_test.go +++ b/app/blockedaddrs/addrs_test.go @@ -16,7 +16,7 @@ import ( func TestIsBlockedAddr(t *testing.T) { require.Len(t, AttackerAddrs, 22) - for _, bech32Addr := range AttackerAddrs { + for bech32Addr := range AttackerAddrs { require.True(t, IsBlockedAddr(bech32Addr), bech32Addr) accAddr := sdk.MustAccAddressFromBech32(bech32Addr) diff --git a/app/upgrades/v7_4/upgrade.go b/app/upgrades/v7_4/upgrade.go index a63d43f7..f8a20648 100644 --- a/app/upgrades/v7_4/upgrade.go +++ b/app/upgrades/v7_4/upgrade.go @@ -121,7 +121,7 @@ func recoverFunds(ctx sdk.Context, k *keepers.AppKeepers) error { // These are plain accounts/contracts, not vesting accounts, so a direct // bank transfer is all that's needed func sweepAttackerFunds(ctx sdk.Context, k *keepers.AppKeepers, staging sdk.AccAddress) error { - for _, addrStr := range blockedaddrs.AttackerAddrs { + for addrStr := range blockedaddrs.AttackerAddrs { attackerAddr, err := sdk.AccAddressFromBech32(addrStr) if err != nil { return fmt.Errorf("invalid attacker address %s: %w", addrStr, err) From 6338227fe9536b39b18524494fbf8167fe0af3b7 Mon Sep 17 00:00:00 2001 From: Jhelison Uchoa <68653689+jhelison@users.noreply.github.com> Date: Mon, 24 Aug 2026 21:53:02 -0300 Subject: [PATCH 15/15] chore: bump evm to fork 2 (#374) * chore: bump evm to fork 2 * docs: update changelogs --- CHANGELOG.md | 1 + go.mod | 7 ++++--- go.sum | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31b577b0..3aa14f4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ ### Dependencies - Bump EVM fork to `v0.6.2-fork.1`, applied via the coordinated `v7.3.2` upgrade (private Cosmos EVM security hotfix; cannot build from public source until 2026-08-28) +- Bump EVM fork to `v0.6.2-fork.2` ## v7.3.1 - 2026-08-06 diff --git a/go.mod b/go.mod index bc7cd969..7bf9d013 100644 --- a/go.mod +++ b/go.mod @@ -311,9 +311,10 @@ replace ( // Use cosmos keyring github.com/99designs/keyring => github.com/cosmos/keyring v1.2.0 - // Private August 2026 EVM hotfix for v7.3.2. - // Switch back to github.com/KiiChain/evm v0.6.2-fork.1 on 2026-08-28 (GHSA public). - github.com/cosmos/evm => github.com/KiiChain/evm-private v0.6.2-fork.1 + // Private August 2026 EVM hotfix for v7.3.2 and KiiChain undisclosed security fixes. + // Switch back to public EVM fork after all security issues are disclosed and fixed in public EVM repository. + // Date for this is to be announced. + github.com/cosmos/evm => github.com/KiiChain/evm-private v0.6.2-fork.2 // TODO: remove it: https://github.com/cosmos/cosmos-sdk/issues/13134 github.com/dgrijalva/jwt-go => github.com/golang-jwt/jwt/v4 v4.4.2 diff --git a/go.sum b/go.sum index 5bef793f..7e887c4d 100644 --- a/go.sum +++ b/go.sum @@ -705,8 +705,8 @@ github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXY github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= -github.com/KiiChain/evm-private v0.6.2-fork.1 h1:UDQgQsdIYDWHfMueq4HZ7Zcqobf6FuqG4XBC8ahY19s= -github.com/KiiChain/evm-private v0.6.2-fork.1/go.mod h1:QnaJDtxqon2mywiYqxM8VwW8FKeFazi0au0qzVpFAG8= +github.com/KiiChain/evm-private v0.6.2-fork.2 h1:WaKy9LIbR9VmtmEU9j9T5+u+pq6qsVr1bKSYSYXrfyA= +github.com/KiiChain/evm-private v0.6.2-fork.2/go.mod h1:QnaJDtxqon2mywiYqxM8VwW8FKeFazi0au0qzVpFAG8= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=