Skip to content
This repository was archived by the owner on Jun 11, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,9 @@ docker-test: build-linux


test:
go test -v -race github.com/public-awesome/stargaze/v18/x/...
go test -v -race \
github.com/public-awesome/stargaze/v18/x/... \
github.com/public-awesome/stargaze/v18/app/...

test-pfm:
cd e2e && go test -v -race -run TestPacketForwardMiddleware .
Expand Down
1 change: 1 addition & 0 deletions app/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func (a *App) PrepareCheckStater(ctx sdk.Context) {

// BeginBlocker application updates every begin block
func (a *App) BeginBlocker(ctx sdk.Context) (sdk.BeginBlock, error) {
BeginBlockForks(ctx, a)
return a.ModuleManager.BeginBlock(ctx)
}

Expand Down
51 changes: 51 additions & 0 deletions app/forks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package app

import (
sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/public-awesome/stargaze/v18/app/upgrades"
"github.com/public-awesome/stargaze/v18/internal/sdkutil"
)

// init validates every registered Fork at startup so a misconfigured entry
// (missing ChainID, missing handler, etc.) prevents the binary from booting
// instead of silently failing to fire at UpgradeHeight on chain.
func init() {
for _, f := range Forks {
if err := f.Validate(); err != nil {
panic(err)
}
}
}

// BeginBlockForks runs in BeginBlock and applies a registered Fork when the
// current block height matches its UpgradeHeight and its ChainID matches the
// running chain (or is the wildcard upgrades.ChainIDAny). Only one fork can
// fire per block. Fork logic must be deterministic across all validators.
//
// The fork's BeginForkLogic runs inside a cache context: if it returns an
// error or panics, state changes are discarded and the error is logged so
// the chain keeps producing blocks instead of halting.
func BeginBlockForks(ctx sdk.Context, app *App) {
for _, fork := range Forks {
if ctx.BlockHeight() != fork.UpgradeHeight {
continue
}
if fork.ChainID != upgrades.ChainIDAny && fork.ChainID != ctx.ChainID() {
continue
}
err := sdkutil.ApplyFuncIfNoError(ctx, func(cacheCtx sdk.Context) error {
return fork.BeginForkLogic(cacheCtx, app.Keepers)
})
if err != nil {
ctx.Logger().Error(
"fork failed; state changes discarded",
"name", fork.UpgradeName,
"chain_id", fork.ChainID,
"height", fork.UpgradeHeight,
"err", err,
)
}
return
}
}
7 changes: 7 additions & 0 deletions app/upgrades.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
mainnetupgradesv16 "github.com/public-awesome/stargaze/v18/app/upgrades/mainnet/v16"
mainnetupgradesv17 "github.com/public-awesome/stargaze/v18/app/upgrades/mainnet/v17"
mainnetupgradesv18 "github.com/public-awesome/stargaze/v18/app/upgrades/mainnet/v18"
mainnetupgradesv18patch "github.com/public-awesome/stargaze/v18/app/upgrades/mainnet/v18_patch"
testnetupgradesv15b2 "github.com/public-awesome/stargaze/v18/app/upgrades/testnet/v15b2"
testnetupgradesv15b3 "github.com/public-awesome/stargaze/v18/app/upgrades/testnet/v15b3"
)
Expand All @@ -25,6 +26,12 @@ var Upgrades = []upgrades.Upgrade{
testnetupgradesv15b3.Upgrade,
}

// Forks lists hard forks that run at fixed block heights via BeginBlockForks.
// Use these when governance cannot be relied on to schedule an x/upgrade plan.
var Forks = []upgrades.Fork{
mainnetupgradesv18patch.Fork,
}

func (app App) RegisterUpgradeHandlers(configurator module.Configurator) {
for _, u := range Upgrades {
app.Keepers.UpgradeKeeper.SetUpgradeHandler(
Expand Down
27 changes: 27 additions & 0 deletions app/upgrades/mainnet/v18_patch/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package v18patch

import (
"github.com/public-awesome/stargaze/v18/app/upgrades"
)

const (
// UpgradeName is the name of the v18-patch fork.
UpgradeName = "v18-patch"

// ChainID restricts this fork to the Stargaze mainnet chain. The fork will
// not fire on testnet, local chains, or e2e tests, even if they happen to
// reach UpgradeHeight.
ChainID = "stargaze-1"

// UpgradeHeight is the block height at which the v18 patch fork logic runs.
// Targets ~14:00 UTC on 2026-05-25 assuming a
// recent block time of ~2.43s
UpgradeHeight = int64(33_362_003)
)

var Fork = upgrades.Fork{
UpgradeName: UpgradeName,
ChainID: ChainID,
UpgradeHeight: UpgradeHeight,
BeginForkLogic: RunForkLogic,
}
104 changes: 104 additions & 0 deletions app/upgrades/mainnet/v18_patch/forks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package v18patch

import (
"fmt"
"time"

sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/public-awesome/stargaze/v18/app/keepers"
)

// RunForkLogic executes the v18-patch hard fork. Governance is broken on the
// chain, so this fork rewrites the gov module params at a fixed block height
// instead of relying on a MsgUpdateParams proposal.
//
// All values mirror what `starsd q gov params --node https://rpc.stargaze-apis.com:443`
// returned at the time of the fork, so existing behavior is preserved
// verbatim except for MinDepositRatio. That field was left empty at genesis
// by accident — the SDK does not validate it as non-empty — and is set to 1%
// here so each individual deposit must clear MinDeposit * MinDepositRatio,
// rejecting dust-deposit spam.
//
// Errors are returned to the caller (BeginBlockForks), which runs this inside
// a cache context: returning an error discards any partial writes and the
// chain keeps producing blocks instead of halting.
func RunForkLogic(ctx sdk.Context, k keepers.StargazeKeepers) error {
ctx.Logger().Info("applying v18-patch hard fork: rewriting gov params")

params, err := k.GovKeeper.Params.Get(ctx)
if err != nil {
return err
}

maxDepositPeriod := 336 * time.Hour
votingPeriod := 72 * time.Hour
expeditedVotingPeriod := 24 * time.Hour

// Total deposit required for a proposal to enter voting (500k stars).
params.MinDeposit = sdk.NewCoins(sdk.NewInt64Coin("ustars", 500_000_000_000))
// Window to accumulate MinDeposit before the proposal expires (14 days).
params.MaxDepositPeriod = &maxDepositPeriod
// Time voting is open once the proposal enters voting (3 days).
params.VotingPeriod = &votingPeriod
// Minimum fraction of staked supply that must vote for the result to count (20%).
params.Quorum = sdkmath.LegacyMustNewDecFromStr("0.2").String()
// Minimum YES share of non-abstain votes required to pass (50%).
params.Threshold = sdkmath.LegacyMustNewDecFromStr("0.5").String()
// NoWithVeto share that fails the proposal and burns the deposit (33.4%).
params.VetoThreshold = sdkmath.LegacyMustNewDecFromStr("0.334").String()
// Initial deposit must be at least this fraction of MinDeposit to register the proposal (20%).
params.MinInitialDepositRatio = sdkmath.LegacyMustNewDecFromStr("0.2").String()
// Fraction of the deposit burned when the proposer cancels the proposal (50%).
params.ProposalCancelRatio = sdkmath.LegacyMustNewDecFromStr("0.5").String()
// Voting window for expedited proposals (1 day).
params.ExpeditedVotingPeriod = &expeditedVotingPeriod
// YES share required to pass an expedited proposal (66.7%).
params.ExpeditedThreshold = sdkmath.LegacyMustNewDecFromStr("0.667").String()
// Total deposit required for an expedited proposal (1M stars).
params.ExpeditedMinDeposit = sdk.NewCoins(sdk.NewInt64Coin("ustars", 1_000_000_000_000))
// Burn the deposit when quorum is not reached.
params.BurnVoteQuorum = true
// Burn the deposit when a proposal never reaches MinDeposit and never enters voting.
params.BurnProposalDepositPrevote = true
// Burn the deposit when VetoThreshold is reached.
params.BurnVoteVeto = true
// Each deposit message must be at least this fraction of MinDeposit, blocking
// dust spam (1% = 5k stars minimum per deposit). Previously empty; the SDK
// does not validate this field as non-empty.
params.MinDepositRatio = sdkmath.LegacyMustNewDecFromStr("0.01").String()

// ValidateBasic catches malformed quorum / threshold / period / deposit
// values before they hit the store. It does NOT cover MinDepositRatio —
// the field this fork repairs — so that field is checked explicitly
// after Set below.
if err := params.ValidateBasic(); err != nil {
return err
}

if err := k.GovKeeper.Params.Set(ctx, params); err != nil {
return err
}

// Post-Set semantic check on MinDepositRatio. The runtime failure mode
// for an empty / unparseable ratio lives in keeper.AddDeposit
// (LegacyNewDecFromStr), well after Params.Set. Read back, parse, and
// also confirm the ratio falls in (0, 1] — the fork's intent is to
// ENABLE dust-spam protection, so 0 (disables it), negative, or > 1
// values all defeat the purpose. Returning an error here causes the
// cache context to drop the bad write and the chain keeps running on
// the prior state.
stored, err := k.GovKeeper.Params.Get(ctx)
if err != nil {
return err
}
ratio, err := sdkmath.LegacyNewDecFromStr(stored.MinDepositRatio)
if err != nil {
return fmt.Errorf("post-fork MinDepositRatio %q does not parse: %w", stored.MinDepositRatio, err)
}
if !ratio.IsPositive() || ratio.GT(sdkmath.LegacyOneDec()) {
return fmt.Errorf("post-fork MinDepositRatio %s is out of (0, 1]", ratio)
}
return nil
}
Loading
Loading