From b75f17c4973831937418c9651d64ff0c6d003aee Mon Sep 17 00:00:00 2001 From: Yusuf Filiz Date: Sun, 10 May 2026 16:22:51 +0300 Subject: [PATCH 1/2] Harden oracle snapshots and feeless priority --- ante/feeless.go | 2 +- x/oracle/keeper/keeper.go | 48 ++++++++++++++++++++++++++++++---- x/oracle/keeper/keeper_test.go | 22 ++++++++++++++++ 3 files changed, 66 insertions(+), 6 deletions(-) diff --git a/ante/feeless.go b/ante/feeless.go index cda6e366..dc0e809a 100644 --- a/ante/feeless.go +++ b/ante/feeless.go @@ -43,7 +43,7 @@ func (gd FeelessDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, // If feeless, ignore fee deduction if isFeeless { - ctx = ctx.WithPriority(math.MaxInt64) + ctx = ctx.WithPriority(math.MaxInt64 / 1_000_000) return next(ctx, tx, simulate) } diff --git a/x/oracle/keeper/keeper.go b/x/oracle/keeper/keeper.go index cd6f1f33..3b8deaaf 100644 --- a/x/oracle/keeper/keeper.go +++ b/x/oracle/keeper/keeper.go @@ -295,6 +295,30 @@ func (k Keeper) GetPriceSnapshotOrDefault(ctx sdk.Context, timestamp int64) (typ return priceSnapshot, nil } +// mergePriceSnapshotItems combines two snapshot item sets and keeps the latest +// value for each denom. +func mergePriceSnapshotItems(existing, incoming types.PriceSnapshotItems) types.PriceSnapshotItems { + merged := make(map[string]types.PriceSnapshotItem, len(existing)+len(incoming)) + for _, item := range existing { + merged[item.Denom] = item + } + for _, item := range incoming { + merged[item.Denom] = item + } + + denoms := make([]string, 0, len(merged)) + for denom := range merged { + denoms = append(denoms, denom) + } + sort.Strings(denoms) + + items := make(types.PriceSnapshotItems, 0, len(denoms)) + for _, denom := range denoms { + items = append(items, merged[denom]) + } + return items +} + // AddPriceSnapshot stores the snapshot on the KVStore and deletes snapshots older than the lookBackDuration // defined on the params func (k Keeper) AddPriceSnapshot(ctx sdk.Context, snapshot types.PriceSnapshot) error { @@ -305,30 +329,44 @@ func (k Keeper) AddPriceSnapshot(ctx sdk.Context, snapshot types.PriceSnapshot) } lookBackDuration := params.LookbackDuration + // Merge with the current snapshot if we already stored one for the same timestamp. + existingSnapshot, err := k.PriceSnapshot.Get(ctx, snapshot.SnapshotTimestamp) + if err != nil && !errors.Is(err, collections.ErrNotFound) { + return err + } + if err == nil { + snapshot.PriceSnapshotItems = mergePriceSnapshotItems(existingSnapshot.PriceSnapshotItems, snapshot.PriceSnapshotItems) + } + // Add snapshot on the KVStore err = k.PriceSnapshot.Set(ctx, snapshot.SnapshotTimestamp, snapshot) if err != nil { return err } - // Delete the snapshot that it's timestamps is older that the LookbackDuration + // Delete snapshots that are older than the lookback duration. var timestampsToDelete []int64 + currentTime := ctx.BlockTime().Unix() err = k.PriceSnapshot.Walk(ctx, nil, func(_ int64, snapshot types.PriceSnapshot) (bool, error) { - // If the snapshot is too old, mark it for deletion - if snapshot.SnapshotTimestamp+int64(lookBackDuration) < ctx.BlockTime().Unix() { + if snapshot.SnapshotTimestamp > currentTime { + return false, fmt.Errorf("snapshot timestamp %d is in the future", snapshot.SnapshotTimestamp) + } + + // If the snapshot is too old, mark it for deletion. + if currentTime-snapshot.SnapshotTimestamp > int64(lookBackDuration) { timestampsToDelete = append(timestampsToDelete, snapshot.SnapshotTimestamp) return false, nil // Continue iteration } - // If a valid snapshot is found, stop iterating + // If a valid snapshot is found, stop iterating. return true, nil }) if err != nil { return err } - // Delete all marked old snapshots + // Delete all marked old snapshots. for _, timeToDelete := range timestampsToDelete { err = k.PriceSnapshot.Remove(ctx, timeToDelete) if err != nil { diff --git a/x/oracle/keeper/keeper_test.go b/x/oracle/keeper/keeper_test.go index 51d8bfbe..eebd689f 100644 --- a/x/oracle/keeper/keeper_test.go +++ b/x/oracle/keeper/keeper_test.go @@ -737,3 +737,25 @@ func TestRemoveExcessFeedsWithError(t *testing.T) { _, err = oracleKeeper.ExchangeRate.Get(ctx, utils.AtomDenom) require.NoError(t, err) } + +func TestAddPriceSnapshotMergesDuplicateTimestamp(t *testing.T) { + init := CreateTestInput(t) + oracleKeeper := init.OracleKeeper + ctx := init.Ctx.WithBlockTime(time.Unix(3500, 0)) + + snapshot1 := types.NewPriceSnapshot(1000, types.PriceSnapshotItems{ + types.NewPriceSnapshotItem(utils.KiiDenom, types.OracleExchangeRate{ExchangeRate: math.LegacyNewDec(1)}), + }) + snapshot2 := types.NewPriceSnapshot(1000, types.PriceSnapshotItems{ + types.NewPriceSnapshotItem(utils.EthDenom, types.OracleExchangeRate{ExchangeRate: math.LegacyNewDec(2)}), + }) + + require.NoError(t, oracleKeeper.AddPriceSnapshot(ctx, snapshot1)) + require.NoError(t, oracleKeeper.AddPriceSnapshot(ctx, snapshot2)) + + stored, err := oracleKeeper.GetPriceSnapshotOrDefault(ctx, 1000) + require.NoError(t, err) + require.Len(t, stored.PriceSnapshotItems, 2) + require.Equal(t, utils.EthDenom, stored.PriceSnapshotItems[0].Denom) + require.Equal(t, utils.KiiDenom, stored.PriceSnapshotItems[1].Denom) +} From 167e110b85777d8ed2496f05af479a4e6d83f0f0 Mon Sep 17 00:00:00 2001 From: Yusuf Filiz Date: Sun, 10 May 2026 16:52:49 +0300 Subject: [PATCH 2/2] Address oracle review feedback --- x/oracle/keeper/keeper.go | 15 ++++++++++----- x/oracle/keeper/keeper_test.go | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/x/oracle/keeper/keeper.go b/x/oracle/keeper/keeper.go index 3b8deaaf..06831515 100644 --- a/x/oracle/keeper/keeper.go +++ b/x/oracle/keeper/keeper.go @@ -328,6 +328,12 @@ func (k Keeper) AddPriceSnapshot(ctx sdk.Context, snapshot types.PriceSnapshot) return err } lookBackDuration := params.LookbackDuration + currentTime := ctx.BlockTime().Unix() + + // Reject future-dated snapshots before mutating state. + if snapshot.SnapshotTimestamp > currentTime { + return fmt.Errorf("snapshot timestamp %d is in the future", snapshot.SnapshotTimestamp) + } // Merge with the current snapshot if we already stored one for the same timestamp. existingSnapshot, err := k.PriceSnapshot.Get(ctx, snapshot.SnapshotTimestamp) @@ -346,21 +352,19 @@ func (k Keeper) AddPriceSnapshot(ctx sdk.Context, snapshot types.PriceSnapshot) // Delete snapshots that are older than the lookback duration. var timestampsToDelete []int64 - currentTime := ctx.BlockTime().Unix() err = k.PriceSnapshot.Walk(ctx, nil, func(_ int64, snapshot types.PriceSnapshot) (bool, error) { + // If the snapshot is too old, mark it for deletion. if snapshot.SnapshotTimestamp > currentTime { return false, fmt.Errorf("snapshot timestamp %d is in the future", snapshot.SnapshotTimestamp) } - - // If the snapshot is too old, mark it for deletion. if currentTime-snapshot.SnapshotTimestamp > int64(lookBackDuration) { timestampsToDelete = append(timestampsToDelete, snapshot.SnapshotTimestamp) return false, nil // Continue iteration } - // If a valid snapshot is found, stop iterating. - return true, nil + // Continue checking the remaining snapshots so future-dated entries cannot hide later in the walk. + return false, nil }) if err != nil { return err @@ -539,3 +543,4 @@ func (k Keeper) ValidateLookBackSeconds(ctx sdk.Context, lookBackSeconds uint64) } return nil } + diff --git a/x/oracle/keeper/keeper_test.go b/x/oracle/keeper/keeper_test.go index eebd689f..f85b81dc 100644 --- a/x/oracle/keeper/keeper_test.go +++ b/x/oracle/keeper/keeper_test.go @@ -759,3 +759,22 @@ func TestAddPriceSnapshotMergesDuplicateTimestamp(t *testing.T) { require.Equal(t, utils.EthDenom, stored.PriceSnapshotItems[0].Denom) require.Equal(t, utils.KiiDenom, stored.PriceSnapshotItems[1].Denom) } + +func TestAddPriceSnapshotRejectsFutureTimestamp(t *testing.T) { + init := CreateTestInput(t) + oracleKeeper := init.OracleKeeper + ctx := init.Ctx.WithBlockTime(time.Unix(1000, 0)) + + snapshot := types.NewPriceSnapshot(1001, types.PriceSnapshotItems{ + types.NewPriceSnapshotItem(utils.KiiDenom, types.OracleExchangeRate{ExchangeRate: math.LegacyNewDec(1)}), + }) + + err := oracleKeeper.AddPriceSnapshot(ctx, snapshot) + require.Error(t, err) + require.Contains(t, err.Error(), "future") + + stored, err := oracleKeeper.GetPriceSnapshotOrDefault(ctx, 1001) + require.NoError(t, err) + require.Empty(t, stored.PriceSnapshotItems) +} +