Skip to content

Market Module 2.0 Implementation#664

Open
StrathCole wants to merge 62 commits into
classic-terra:mainfrom
Market-Module-2-0:feat/mm-implementation
Open

Market Module 2.0 Implementation#664
StrathCole wants to merge 62 commits into
classic-terra:mainfrom
Market-Module-2-0:feat/mm-implementation

Conversation

@StrathCole
Copy link
Copy Markdown
Collaborator

Summary of changes (Copilot)

This pull request introduces the v15 network upgrade and implements a new tax redirect mechanism for the market module, along with associated test coverage and module/account configuration changes. The upgrade restricts allowed swap denoms to uusd by default, ensures the oracle meta denom is present, and refactors the burn/tax split logic to support redirecting a portion of taxes to the market accumulator. Test logic is updated to cover these new behaviors.

Network Upgrade and Module Changes:

  • Added the v15 upgrade handler, which restricts allowed market swap denoms to uusd and ensures the oracle meta denom is included in vote targets. The upgrade is registered in the app and includes no store migrations. (app/upgrades/v15/constants.go, app/upgrades/v15/upgrades.go, app/app.go) [1] [2] [3] [4]
  • Updated the market keeper initialization to include the distribution keeper and set market hooks on the oracle keeper to track tally events for TWAP and freshness. (app/keepers/keepers.go)

Tax Redirect and Module Account Handling:

  • Modified module account permissions and allowed receiving accounts to add support for the new market accumulator module account. The market module can now receive funds, and the accumulator account is registered. (app/modules.go) [1] [2]
  • Updated the burn/tax split logic in tests to support the new tax redirect rate, ensuring correct distribution between fee collector, oracle, market accumulator, and community pool. The test logic now sets all relevant parameters atomically and validates the new distribution. (custom/auth/ante/fee_test.go) [1] [2] [3] [4] [5]

Test Improvements:

  • Enhanced ante handler tests to explicitly set the tax redirect rate to zero in legacy test cases, ensuring backward compatibility and focused test coverage for the new redirect logic. (custom/auth/ante/fee_test.go) [1] [2] [3]

Minor/Housekeeping:

  • Added import and formatting tweaks for clarity and to support new modules. (app/app.go, app/modules.go) [1] [2] [3]
  • Removed a redundant consensus node prefix setting in the CLI root command. (cmd/terrad/root.go)

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR ships "Market Module 2.0" together with a v15 network upgrade. It restructures the swap pipeline (allowed-denom guard, oracle freshness, TWAP deviation, daily cap, epoch burn/refill), redirects a configurable share of taxes to a new market_accumulator module account, adds oracle USD-price queries with a UST meta-denom, and wires market hooks into the oracle tally. It touches consensus-critical paths across app/, x/market, x/oracle, x/tax, x/treasury, plus extensive ante/e2e test changes.

Changes:

  • v15 upgrade: pin allowed swap denom to uusd, register UST meta-denom in oracle whitelist/Tobin-tax, add market_accumulator module account.
  • Market 2.0: in-memory allowed denoms, swap fee burn/community split, TWAP store + deviation check, daily cap, end-of-epoch burn-and-refill from accumulator, oracle freshness guard.
  • Tax/treasury: new TaxRedirectRate (default 0.6) routes a slice of taxes to the market accumulator before the existing oracle/community/burn split, with per-leg events.

Reviewed changes

Copilot reviewed 61 out of 63 changed files in this pull request and generated 17 comments.

Show a summary per file
File Description
app/app.go, app/keepers/keepers.go, app/modules.go Register v15 upgrade, wire distribution keeper into market, add market_accumulator module account, set oracle MarketHooks
app/upgrades/v15/{constants,upgrades}.go New upgrade handler: set allowed denom to uusd, ensure UST meta denom in whitelist + Tobin tax store
x/market/keeper/keeper.go, msg_server.go, abci.go Allowed-denom map, freshness/TWAP/daily-cap safeguards, epoch burn+refill, swap fee split
x/market/types/{keys.go,params.pb.go,market.pb.go} & proto New params (epoch length, fee splits, oracle age, TWAP window/deviation, daily-cap factor), accumulator module name, store keys
x/market/keeper/{epoch_test.go,safeguards_test.go,msg_server_test.go,test_utils.go} Unit tests for new safeguards + epoch; market account perms updated in test setup
x/market/simulation/* Minor genesis/param additions (does not yet cover new safeguards)
x/oracle/keeper/{keeper.go,querier.go,ballot.go}, types/params.go, abci.go, client/cli, proto Add MetaUSDDenom = "UST", GetUSDPrice/IterateUSDPrices, USDPrice/USDPrices RPCs, MarketHooks tally callback
x/tax/keeper/tax_split.go, types/events.go, tax_split_test.go, handlers/market_msg_server.go Market-redirect-first split with per-leg events; new Swap reverse-charge handler; focused test
x/treasury/{types/params.go, keeper/params.go, types/params_test.go, keeper/test_utils.go} Add TaxRedirectRate param, getter/setter, validation, default 0.6
custom/auth/ante/fee_test.go Updated burn/tax-split test harness to account for redirect; legacy cases pin redirect to 0
cmd/terrad/root.go Removes SetBech32PrefixForConsensusNode call
tests/e2e/* New end-to-end coverage for upgrade, safeguards, redirect; config knobs lowered for fast epochs
tests/interchaintest/{setup.go,go.sum} Pin tax_redirect_rate to 0; regenerated go.sum with some suspicious entries
scripts/protocgen.sh, scripts/upgrade-test.sh Minor script tweaks
proto/terra/{market,treasury,oracle}/v1beta1/*.proto + generated pb.go Schema additions for new params and USD price RPCs

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread x/treasury/types/params.go
Comment thread x/oracle/keeper/keeper.go Outdated
Comment thread x/oracle/keeper/querier.go Outdated
Comment thread x/market/keeper/keeper.go
Comment on lines 53 to +87
@@ -57,6 +72,20 @@ func (k Keeper) Logger(ctx sdk.Context) log.Logger {
return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName))
}

// SetAllowedSwapDenoms sets which denoms are allowed to be swapped with uluna.
// Note: This is intended for configuration/tests; it is not persisted.
func (k *Keeper) SetAllowedSwapDenoms(denoms []string) {
m := make(map[string]bool, len(denoms))
for _, d := range denoms {
m[d] = true
}
k.allowedSwapDenoms = m
}

func (k Keeper) isAllowedSwapDenom(denom string) bool {
return k.allowedSwapDenoms[denom]
}
Comment thread x/market/keeper/keeper.go
Comment on lines +231 to +312
// GetTWAPPrices returns the recent price snapshots for a denom
func (k Keeper) GetTWAPPrices(ctx sdk.Context, denom string) []PriceSnapshot {
store := ctx.KVStore(k.storeKey)
bz := store.Get(types.GetTWAPPriceKey(denom))
if bz == nil {
return []PriceSnapshot{}
}

var snapshots []PriceSnapshot
// Encoding: each snapshot is height (8 bytes) + price length (4 bytes) + price (variable)
offset := 0
for offset < len(bz) {
if offset+8 > len(bz) {
break
}
height := int64(sdk.BigEndianToUint64(bz[offset : offset+8]))
offset += 8

// Read price length (4 bytes)
if offset+4 > len(bz) {
break
}
priceLen := int(uint32(bz[offset])<<24 | uint32(bz[offset+1])<<16 | uint32(bz[offset+2])<<8 | uint32(bz[offset+3]))
offset += 4

// Read price bytes
if offset+priceLen > len(bz) {
break
}
priceBytes := bz[offset : offset+priceLen]
offset += priceLen

var dp sdk.DecProto
if err := k.cdc.Unmarshal(priceBytes, &dp); err == nil {
snapshots = append(snapshots, PriceSnapshot{Height: height, Price: dp.Dec})
}
}
return snapshots
}

// AddTWAPPrice adds a new price snapshot and prunes old ones
func (k Keeper) AddTWAPPrice(ctx sdk.Context, denom string, price math.LegacyDec) {
snapshots := k.GetTWAPPrices(ctx, denom)
currentHeight := ctx.BlockHeight()
lookback := int64(k.TwapLookbackWindow(ctx))

// Add new snapshot
snapshots = append(snapshots, PriceSnapshot{Height: currentHeight, Price: price})

// Prune old snapshots (keep only those within lookback window)
pruned := []PriceSnapshot{}
for _, snap := range snapshots {
if currentHeight-snap.Height <= lookback {
pruned = append(pruned, snap)
}
}

// Encode and store
store := ctx.KVStore(k.storeKey)
var bz []byte
for _, snap := range pruned {
heightBytes := sdk.Uint64ToBigEndian(uint64(snap.Height))
priceBytes := k.cdc.MustMarshal(&sdk.DecProto{Dec: snap.Price})
// Store length + data for variable-length protobuf (4 bytes for length)
priceLen := uint32(len(priceBytes))
lengthBytes := []byte{
byte(priceLen >> 24),
byte(priceLen >> 16),
byte(priceLen >> 8),
byte(priceLen),
}
bz = append(bz, heightBytes...)
bz = append(bz, lengthBytes...)
bz = append(bz, priceBytes...)
}

if len(bz) > 0 {
store.Set(types.GetTWAPPriceKey(denom), bz)
} else {
store.Delete(types.GetTWAPPriceKey(denom))
}
}
Comment thread scripts/upgrade-test.sh
Comment thread tests/e2e/configurer/chain/commands.go
Comment on lines +49 to +65
// Swap handles MsgSwap with tax deduction
func (s *MarketMsgServer) Swap(ctx context.Context, msg *markettypes.MsgSwap) (*markettypes.MsgSwapResponse, error) {
sdkCtx := sdk.UnwrapSDKContext(ctx)

if !s.taxKeeper.IsReverseCharge(sdkCtx, true) {
return s.messageServer.Swap(ctx, msg)
}

sender := sdk.MustAccAddressFromBech32(msg.Trader)
netOfferCoin, err := s.taxKeeper.DeductTax(sdkCtx, sender, sdk.NewCoins(msg.OfferCoin), false)
if err != nil {
return nil, err
}
msg.OfferCoin = netOfferCoin[0]

return s.messageServer.Swap(ctx, msg)
}
Comment on lines +64 to +68
var epochLengthBlocks uint64
simState.AppParams.GetOrGenerate(
string(types.KeyEpochLengthBlocks), &epochLengthBlocks, simState.Rand,
func(r *rand.Rand) { epochLengthBlocks = GenEpochLengthBlocks(r) },
)
Comment on lines +27 to +66
// Number of blocks per epoch for market burn/refill. Default: 30 days worth of blocks.
uint64 epoch_length_blocks = 4 [(gogoproto.moretags) = "yaml:\"epoch_length_blocks\""];

// Fraction of swap fee to burn [0,1]
bytes swap_fee_burn_rate = 5 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.moretags) = "yaml:\"swap_fee_burn_rate\"",
(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec",
(gogoproto.nullable) = false
];

// Fraction of swap fee to send to Community Pool [0,1]
bytes swap_fee_community_rate = 6 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.moretags) = "yaml:\"swap_fee_community_rate\"",
(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec",
(gogoproto.nullable) = false
];

// Maximum age in seconds for oracle prices before swaps are denied. Default: 75 seconds (25 blocks * 3s)
uint64 max_oracle_age_seconds = 7 [(gogoproto.moretags) = "yaml:\"max_oracle_age_seconds\""];

// Number of blocks for TWAP calculation window. Default: 45 blocks
uint64 twap_lookback_window = 8 [(gogoproto.moretags) = "yaml:\"twap_lookback_window\""];

// Maximum deviation from TWAP before swap is rejected [0,1]. Default: 0.10 (10%)
bytes max_twap_deviation = 9 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.moretags) = "yaml:\"max_twap_deviation\"",
(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec",
(gogoproto.nullable) = false
];

// Daily cap factor: fraction of pool balance usable per day [0,1]. Default: 0.10 (10%)
bytes daily_cap_factor = 10 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.moretags) = "yaml:\"daily_cap_factor\"",
(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec",
(gogoproto.nullable) = false
];

// Initialize/ensure allowed swap denoms for market: restrict to uusd by default.
k.MarketKeeper.SetAllowedSwapDenoms([]string{"uusd"})

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should initialized TaxRedirectRate k.TreasuryKeeper.SetTaxRedirectRate(sdkCtx, sdkmath.LegacyZeroDec()),

Comment thread app/modules.go
oracletypes.ModuleName: true,
treasurytypes.BurnModuleName: true,
markettypes.ModuleName: true,
markettypes.AccumulatorModuleName: true,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maccPerms registers AccumulatorModuleName for new chains (via InitChainer), but for an existing chain upgrading, the account doesn't exist in the auth store. The upgrade handler must create it

DefaultBurnTaxSplit = sdkmath.LegacyNewDecWithPrec(1, 1) // 10% goes to community pool, 90% burn
DefaultMinInitialDepositRatio = sdkmath.LegacyZeroDec() // 0% min initial deposit
DefaultOracleSplit = sdkmath.LegacyOneDec() // 100% oracle, community tax (CP) is deducted before oracle split
DefaultTaxRedirectRate = sdkmath.LegacyNewDecWithPrec(6, 1) // 0.6 redirected to market accumulator (pre-oracle-split)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default 60% is ok, right?

@hoank101 hoank101 self-requested a review June 4, 2026 00:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants