Skip to content

Commit

Permalink
Merge branch 'autoswap-revenue-tokens' of github.com:Stride-Labs/stri…
Browse files Browse the repository at this point in the history
…de into autoswap-revenue-tokens
  • Loading branch information
sampocs committed Dec 8, 2023
2 parents 01eb42a + 407a510 commit 40a181a
Show file tree
Hide file tree
Showing 16 changed files with 1,267 additions and 19 deletions.
24 changes: 24 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
## How to Report a Security Bug

If you believe you have found a security vulnerability in Stride,
you can report it to our primary vulnerability disclosure email **[email protected]**.

Please include the issue details, reproduction, impact, and other
information. Please submit only one unique email thread per vulnerability.

Artifacts from an email report are saved at the time the email is triaged.
Please note: our team is not able to monitor dynamic content (e.g. a Google
Docs link that is edited after receipt) throughout the lifecycle of a report.
If you would like to share additional information or modify previous
information, please include it in an additional reply as an additional attachment.

***Please DO NOT file a public issue in this repository to report a security vulnerability.***

### Guidelines
We require that all researchers:
- Abide by this policy to disclose vulnerabilities, and avoid posting vulnerability information in public places, including GitHub, Discord, Telegram, and Twitter. Make every effort to avoid privacy violations, degradation of user experience, disruption to production systems (including but not limited to the Cosmos Hub), and destruction of data.
- Keep any information about vulnerabilities that you’ve discovered confidential between yourself and the Stride engineering team until the issue has been resolved and disclosed.
- Avoid posting personally identifiable information, privately or publicly.

If you follow these guidelines when reporting an issue to us, we commit to:
- Work with you to understand, resolve and ultimately disclose the issue in a timely fashion
47 changes: 47 additions & 0 deletions app/apptesting/test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"github.com/cosmos/cosmos-sdk/baseapp"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/bech32"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types"
"github.com/cosmos/gogoproto/proto"
Expand Down Expand Up @@ -355,6 +357,34 @@ func (s *AppTestHelper) UpdateChannelState(portId, channelId string, channelStat
s.App.IBCKeeper.ChannelKeeper.SetChannel(s.Ctx, portId, channelId, channel)
}

// Helper function to check if an ICA was submitted by seeing if the sequence number incremented
func (s *AppTestHelper) CheckICATxSubmitted(portId, channelId string, icaFunction func() error) {
// Get the sequence before the tested funciton is run
startSequence := s.MustGetNextSequenceNumber(portId, channelId)

// Run the test function and confirm there's no error
err := icaFunction()
s.Require().NoError(err, "no error expected executing tested function")

// Check that the sequence number incremented
endSequence := s.MustGetNextSequenceNumber(portId, channelId)
s.Require().Equal(startSequence+1, endSequence, "sequence number should have incremented from tested function")
}

// Helper function to check if an ICA was NOT submitted by seeing if the sequence number did not increment
func (s *AppTestHelper) CheckICATxNotSubmitted(portId, channelId string, icaFunction func() error) {
// Get the sequence before the tested funciton is run
startSequence := s.MustGetNextSequenceNumber(portId, channelId)

// Run the test function and confirm there's no error
err := icaFunction()
s.Require().NoError(err, "no error expected executing tested function")

// Check that the sequence number did not change
endSequence := s.MustGetNextSequenceNumber(portId, channelId)
s.Require().Equal(startSequence, endSequence, "sequence number should NOT have incremented from tested function")
}

// Constructs an ICA Packet Acknowledgement compatible with ibc-go v5+
func ICAPacketAcknowledgement(t *testing.T, msgType string, msgResponses []proto.Message) channeltypes.Acknowledgement {
txMsgData := &sdk.TxMsgData{
Expand Down Expand Up @@ -502,6 +532,23 @@ func (s *AppTestHelper) ConfirmUpgradeSucceededs(upgradeName string, upgradeHeig
})
}

// Returns the bank store key prefix for an address and denom
// Useful for testing balance ICQs
func (s *AppTestHelper) GetBankStoreKeyPrefix(address, denom string) []byte {
_, addressBz, err := bech32.DecodeAndConvert(address)
s.Require().NoError(err, "no error expected when bech decoding address")
return append(banktypes.CreateAccountBalancesPrefix(addressBz), []byte(denom)...)
}

// Extracts the address and denom from a bank store prefix
// Useful for testing balance ICQs as it can confirm that the serialized query request
// data has the proper address and denom
func (s *AppTestHelper) ExtractAddressAndDenomFromBankPrefix(data []byte) (address, denom string) {
addressBz, denom, err := banktypes.AddressAndDenomFromBalancesStore(data[1:]) // Remove BalancePrefix byte
s.Require().NoError(err, "no error expected when getting address and denom from balance store")
return addressBz.String(), denom
}

// Generates a valid and invalid test address (used for non-keeper tests)
func GenerateTestAddrs() (string, string) {
pk1 := ed25519.GenPrivKey().PubKey()
Expand Down
15 changes: 15 additions & 0 deletions x/stakeibc/keeper/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ func (k Keeper) BeforeEpochStart(ctx sdk.Context, epochInfo epochstypes.EpochInf
delegationInterval := k.GetParam(ctx, types.KeyDelegateInterval)
reinvestInterval := k.GetParam(ctx, types.KeyReinvestInterval)

// Claim accrued staking rewards at the beginning of the epoch
k.ClaimAccruedStakingRewards(ctx)

// Create a new deposit record for each host zone and the grab all deposit records
k.CreateDepositRecordsForEpoch(ctx, epochNumber)
depositRecords := k.RecordsKeeper.GetAllDepositRecord(ctx)
Expand Down Expand Up @@ -159,6 +162,18 @@ func (k Keeper) SetWithdrawalAddress(ctx sdk.Context) {
}
}

// Claim staking rewards for each host zone
func (k Keeper) ClaimAccruedStakingRewards(ctx sdk.Context) {
k.Logger(ctx).Info("Claiming Accrued Staking Rewards...")

for _, hostZone := range k.GetAllActiveHostZone(ctx) {
err := k.ClaimAccruedStakingRewardsOnHost(ctx, hostZone)
if err != nil {
k.Logger(ctx).Error(fmt.Sprintf("Unable to claim accrued staking rewards on %s, err: %s", hostZone.ChainId, err))
}
}
}

// Updates the redemption rate for each host zone
// At a high level, the redemption rate is equal to the amount of native tokens locked divided by the stTokens in existence.
// The equation is broken down further into the following sub-components:
Expand Down
7 changes: 0 additions & 7 deletions x/stakeibc/keeper/icacallbacks_delegate.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package keeper

import (
"errors"
"fmt"

"github.com/spf13/cast"
Expand Down Expand Up @@ -67,12 +66,6 @@ func (k Keeper) DelegateCallback(ctx sdk.Context, packet channeltypes.Packet, ac
// Regardless of failure/success/timeout, indicate that this ICA has completed
for _, splitDelegation := range delegateCallback.SplitDelegations {
if err := k.DecrementValidatorDelegationChangesInProgress(&hostZone, splitDelegation.Validator); err != nil {
// TODO: Revert after v14 upgrade
if errors.Is(err, types.ErrInvalidValidatorDelegationUpdates) {
k.Logger(ctx).Error(utils.LogICACallbackWithHostZone(chainId, ICACallbackID_Delegate,
"Invariant failed - delegation changes in progress fell below 0 for %s", splitDelegation.Validator))
continue
}
return err
}
}
Expand Down
6 changes: 0 additions & 6 deletions x/stakeibc/keeper/icacallbacks_undelegate.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,6 @@ func (k Keeper) UndelegateCallback(ctx sdk.Context, packet channeltypes.Packet,
}
for _, splitDelegation := range undelegateCallback.SplitDelegations {
if err := k.DecrementValidatorDelegationChangesInProgress(&hostZone, splitDelegation.Validator); err != nil {
// TODO: Revert after v14 upgrade
if errors.Is(err, types.ErrInvalidValidatorDelegationUpdates) {
k.Logger(ctx).Error(utils.LogICACallbackWithHostZone(chainId, ICACallbackID_Undelegate,
"Invariant failed - delegation changes in progress fell below 0 for %s", splitDelegation.Validator))
continue
}
return err
}
}
Expand Down
227 changes: 227 additions & 0 deletions x/stakeibc/keeper/icqcallbacks_pool_price_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
package keeper_test

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

icqtypes "github.com/Stride-Labs/stride/v16/x/interchainquery/types"
"github.com/Stride-Labs/stride/v16/x/stakeibc/keeper"
"github.com/Stride-Labs/stride/v16/x/stakeibc/types"
)

type PoolPriceQueryCallbackTestCase struct {
TradeRoute types.TradeRoute
Price sdk.Dec
Response ICQCallbackArgs
}

func (s *KeeperTestSuite) SetupPoolPriceCallbackTestCase(priceOnAsset0 bool) PoolPriceQueryCallbackTestCase {
hostPrice := sdk.MustNewDecFromStr("1.2")
rewardPrice := sdk.MustNewDecFromStr("0.8")

// Alphabetize the and sort the denom's according to the ordering
rewardDenom := "ibc/reward-denom-on-trade"
hostDenom := "ibc/host-denom-on-trade"

var twapRecord types.OsmosisTwapRecord
if priceOnAsset0 {
hostDenom = "a-" + hostDenom
rewardDenom = "z-" + rewardDenom

twapRecord = types.OsmosisTwapRecord{
Asset0Denom: hostDenom,
Asset1Denom: rewardDenom,
P0LastSpotPrice: hostPrice,
P1LastSpotPrice: rewardPrice,
}
} else {
hostDenom = "z-" + hostDenom
rewardDenom = "a-" + rewardDenom

twapRecord = types.OsmosisTwapRecord{
Asset0Denom: rewardDenom,
Asset1Denom: hostDenom,
P0LastSpotPrice: rewardPrice,
P1LastSpotPrice: hostPrice,
}
}

route := types.TradeRoute{
RewardDenomOnRewardZone: RewardDenom,
HostDenomOnHostZone: HostDenom,

RewardDenomOnTradeZone: rewardDenom,
HostDenomOnTradeZone: hostDenom,
}
s.App.StakeibcKeeper.SetTradeRoute(s.Ctx, route)

// Build query object and serialized query response
callbackDataBz, _ := proto.Marshal(&types.TradeRouteCallback{
RewardDenom: RewardDenom,
HostDenom: HostDenom,
})
query := icqtypes.Query{CallbackData: callbackDataBz}
queryResponse, _ := proto.Marshal(&twapRecord)

return PoolPriceQueryCallbackTestCase{
TradeRoute: route,
Price: hostPrice,
Response: ICQCallbackArgs{
Query: query,
CallbackArgs: queryResponse,
},
}
}

func (s *KeeperTestSuite) TestPoolPriceCallback_Successful_HostTokenFirst() {
hostTokenFirst := true
tc := s.SetupPoolPriceCallbackTestCase(hostTokenFirst)

err := keeper.PoolPriceCallback(s.App.StakeibcKeeper, s.Ctx, tc.Response.CallbackArgs, tc.Response.Query)
s.Require().NoError(err)

// Confirm the new price was set on the trade route
route, found := s.App.StakeibcKeeper.GetTradeRoute(s.Ctx, RewardDenom, HostDenom)
s.Require().True(found, "trade route should have been found")
s.Require().Equal(tc.Price.String(), route.TradeConfig.SwapPrice.String(), "pool price")
}

func (s *KeeperTestSuite) TestPoolPriceCallback_Successful_RewardTokenFirst() {
hostTokenFirst := false
tc := s.SetupPoolPriceCallbackTestCase(hostTokenFirst)

err := keeper.PoolPriceCallback(s.App.StakeibcKeeper, s.Ctx, tc.Response.CallbackArgs, tc.Response.Query)
s.Require().NoError(err)

// Confirm the new price was set on the trade route
route, found := s.App.StakeibcKeeper.GetTradeRoute(s.Ctx, RewardDenom, HostDenom)
s.Require().True(found, "trade route should have been found")
s.Require().Equal(tc.Price.String(), route.TradeConfig.SwapPrice.String(), "pool price")
}

func (s *KeeperTestSuite) TestPoolPriceCallback_InvalidArgs() {
tc := s.SetupPoolPriceCallbackTestCase(true) // ordering doesn't matter

// Submit callback with invalid callback args (so that it can't unmarshal into a coin)
invalidArgs := []byte("random bytes")

err := keeper.PoolPriceCallback(s.App.StakeibcKeeper, s.Ctx, invalidArgs, tc.Response.Query)
s.Require().ErrorContains(err, "unable to unmarshal the query response")
}

func (s *KeeperTestSuite) TestPoolPriceCallback_FailedToUnmarshalCallback() {
tc := s.SetupPoolPriceCallbackTestCase(true) // ordering doesn't matter

// Update the callback data so that it can't be successfully unmarshalled
invalidQuery := tc.Response.Query
invalidQuery.CallbackData = []byte("random bytes")

err := keeper.PoolPriceCallback(s.App.StakeibcKeeper, s.Ctx, tc.Response.CallbackArgs, invalidQuery)
s.Require().ErrorContains(err, "unable to unmarshal trade reward balance callback data")
}

func (s *KeeperTestSuite) TestPoolPriceCallback_TradeRouteNotFound() {
tc := s.SetupPoolPriceCallbackTestCase(true) // ordering doesn't matter

// Update the callback data so that it keys to a trade route that doesn't exist
invalidCallbackDataBz, _ := proto.Marshal(&types.TradeRouteCallback{
RewardDenom: RewardDenom,
HostDenom: "different-host-denom",
})
invalidQuery := tc.Response.Query
invalidQuery.CallbackData = invalidCallbackDataBz

err := keeper.PoolPriceCallback(s.App.StakeibcKeeper, s.Ctx, tc.Response.CallbackArgs, invalidQuery)
s.Require().ErrorContains(err, "trade route not found")
}

func (s *KeeperTestSuite) TestPoolPriceCallback_TradeDenomMismatch() {
tc := s.SetupPoolPriceCallbackTestCase(true) // ordering doesn't matter

// Update the trade route so that the denom's in the route don't match the query response
invalidTradeRoute := tc.TradeRoute
invalidTradeRoute.RewardDenomOnTradeZone = "different-denom"
s.App.StakeibcKeeper.SetTradeRoute(s.Ctx, invalidTradeRoute)

err := keeper.PoolPriceCallback(s.App.StakeibcKeeper, s.Ctx, tc.Response.CallbackArgs, tc.Response.Query)
s.Require().ErrorContains(err, "Assets in query response")
s.Require().ErrorContains(err, "do not match denom's from trade route")

// Do it again, but with the other denom
invalidTradeRoute = tc.TradeRoute
invalidTradeRoute.HostDenomOnTradeZone = "different-denom"
s.App.StakeibcKeeper.SetTradeRoute(s.Ctx, invalidTradeRoute)

err = keeper.PoolPriceCallback(s.App.StakeibcKeeper, s.Ctx, tc.Response.CallbackArgs, tc.Response.Query)
s.Require().ErrorContains(err, "Assets in query response")
s.Require().ErrorContains(err, "do not match denom's from trade route")
}

func (s *KeeperTestSuite) TestAssertTwapAssetsMatchTradeRoute() {
testCases := []struct {
name string
twapRecord types.OsmosisTwapRecord
tradeRoute types.TradeRoute
expectedMatch bool
}{
{
name: "successful match - 1",
twapRecord: types.OsmosisTwapRecord{Asset0Denom: "denom-a", Asset1Denom: "denom-b"},
tradeRoute: types.TradeRoute{RewardDenomOnTradeZone: "denom-a", HostDenomOnTradeZone: "denom-b"},
expectedMatch: true,
},
{
name: "successful match - 2",
twapRecord: types.OsmosisTwapRecord{Asset0Denom: "denom-b", Asset1Denom: "denom-a"},
tradeRoute: types.TradeRoute{RewardDenomOnTradeZone: "denom-b", HostDenomOnTradeZone: "denom-a"},
expectedMatch: true,
},
{
name: "successful match - 3",
twapRecord: types.OsmosisTwapRecord{Asset0Denom: "denom-a", Asset1Denom: "denom-b"},
tradeRoute: types.TradeRoute{RewardDenomOnTradeZone: "denom-b", HostDenomOnTradeZone: "denom-a"},
expectedMatch: true,
},
{
name: "successful match - 4",
twapRecord: types.OsmosisTwapRecord{Asset0Denom: "denom-b", Asset1Denom: "denom-a"},
tradeRoute: types.TradeRoute{RewardDenomOnTradeZone: "denom-a", HostDenomOnTradeZone: "denom-b"},
expectedMatch: true,
},
{
name: "mismatch osmo asset 0",
twapRecord: types.OsmosisTwapRecord{Asset0Denom: "denom-z", Asset1Denom: "denom-b"},
tradeRoute: types.TradeRoute{RewardDenomOnTradeZone: "denom-a", HostDenomOnTradeZone: "denom-b"},
expectedMatch: false,
},
{
name: "mismatch osmo asset 1",
twapRecord: types.OsmosisTwapRecord{Asset0Denom: "denom-a", Asset1Denom: "denom-z"},
tradeRoute: types.TradeRoute{RewardDenomOnTradeZone: "denom-a", HostDenomOnTradeZone: "denom-b"},
expectedMatch: false,
},
{
name: "mismatch route reward denom",
twapRecord: types.OsmosisTwapRecord{Asset0Denom: "denom-a", Asset1Denom: "denom-b"},
tradeRoute: types.TradeRoute{RewardDenomOnTradeZone: "denom-z", HostDenomOnTradeZone: "denom-b"},
expectedMatch: false,
},
{
name: "mismatch route host denom",
twapRecord: types.OsmosisTwapRecord{Asset0Denom: "denom-a", Asset1Denom: "denom-b"},
tradeRoute: types.TradeRoute{RewardDenomOnTradeZone: "denom-a", HostDenomOnTradeZone: "denom-z"},
expectedMatch: false,
},
}

for _, tc := range testCases {
s.Run(tc.name, func() {
err := keeper.AssertTwapAssetsMatchTradeRoute(tc.twapRecord, tc.tradeRoute)
if tc.expectedMatch {
s.Require().NoError(err)
} else {
s.Require().Error(err)
}
})
}
}
Loading

0 comments on commit 40a181a

Please sign in to comment.