Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

phase 2 - price ICQ to Osmosis twap store #987

Merged
merged 17 commits into from
Nov 28, 2023
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
9 changes: 5 additions & 4 deletions dockernet/config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ KEYS_LOGS=$DOCKERNET_HOME/logs/keys.log

# List of hosts enabled
# HOST_CHAINS have liquid staking support, ACCESSORY_CHAINS do not
HOST_CHAINS=()
ACCESSORY_CHAINS=()
HOST_CHAINS=(DYDX)
ACCESSORY_CHAINS=(NOBLE OSMO)

# If no host zones are specified above:
# `start-docker` defaults to just GAIA if HOST_CHAINS is empty
Expand Down Expand Up @@ -336,14 +336,15 @@ STRIDE_RELAYER_MNEMONICS=(
"$RELAYER_GAIA_ICS_MNEMONIC"
"$RELAYER_DYDX_MNEMONIC"
)
# Mnemonics for connections between two non-stride chains
# Mnemonics for connections between accessory chains
RELAYER_NOBLE_DYDX_MNEMONIC="sentence fruit crumble sail bar knife exact flame apart prosper hint myth clean among tiny burden depart purity select envelope identify cross physical emerge"
RELAYER_DYDX_NOBLE_MNEMONIC="aerobic breeze claw climb bounce morning tank victory eight funny employ bracket hire reduce fine flee lava domain warfare loop theme fly tattoo must"
RELAYER_NOBLE_OSMO_MNEMONIC="actual field visual wage orbit add human unit happy rich evil chair entire person february cactus deputy impact gasp elbow sunset brand possible fly"
RELAYER_OSMO_NOBLE_MNEMONIC="obey clinic miss grunt inflict laugh sell moral kitchen tumble gold song flavor rather horn exhaust state amazing poverty differ approve spike village device"
RELAYER_DYDX_OSMO_MNEMONIC="small fire step promote fox reward book seek arctic session illegal loyal because brass spoil minute wonder jazz shoe price muffin churn evil monitor"
RELAYER_OSMO_DYDX_MNEMONIC="risk wool reason sweet current strategy female miracle squeeze that wire develop ocean rapid domain lift blame monkey sick round museum item maze trumpet"

RELAYER_STRIDE_OSMO_MNEMONIC="father october lonely ticket leave regret pudding buffalo return asthma plastic piano beef orient ill clip right phone ready pottery helmet hip solid galaxy"
RELAYER_OSMO_STRIDE_MNEMONIC="narrow assist come feel canyon anxiety three reason satoshi inspire region little attend impulse what student dog armor economy faculty dutch distance upon calm"

STRIDE_ADDRESS() {
# After an upgrade, the keys query can sometimes print migration info,
Expand Down
23 changes: 23 additions & 0 deletions dockernet/config/relayer_config_dydx_noble.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ global:
memo: ""
light-cache-size: 20
chains:
stride:
type: cosmos
value:
key: stride
chain-id: STRIDE
rpc-addr: http://stride1:26657
account-prefix: stride
keyring-backend: test
gas-adjustment: 1.3
gas-prices: 0.02ustrd
coin-type: 118
debug: false
timeout: 20s
output-format: json
sign-mode: direct
dydx:
type: cosmos
value:
Expand Down Expand Up @@ -50,6 +65,14 @@ chains:
output-format: json
sign-mode: direct
paths:
stride-osmo:
src:
chain-id: STRIDE
dst:
chain-id: OSMO
src-channel-filter:
rule: ""
channel-list: []
dydx-noble:
src:
chain-id: DYDX
Expand Down
7 changes: 7 additions & 0 deletions dockernet/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -350,3 +350,10 @@ services:
- ./state/relayer-osmo-dydx:/home/relayer/.relayer
restart: always
command: [ "bash", "start.sh", "osmo-dydx" ]

relayer-stride-osmo:
image: stridezone:relayer
volumes:
- ./state/relayer-stride-osmo:/home/relayer/.relayer
restart: always
command: [ "bash", "start.sh", "stride-osmo" ]
11 changes: 7 additions & 4 deletions dockernet/scripts/community-pool-staking/create_pool.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,21 @@ dydx_to_osmo_client=$(GET_CLIENT_ID_FROM_CHAIN_ID DYDX OSMO)
dydx_to_osmo_connection=$(GET_CONNECTION_ID_FROM_CLIENT_ID DYDX $dydx_to_osmo_client)
dydx_to_osmo_channel=$(GET_TRANSFER_CHANNEL_ID_FROM_CONNECTION_ID DYDX $dydx_to_osmo_connection)
osmo_to_dydx_channel=$(GET_COUNTERPARTY_TRANSFER_CHANNEL_ID DYDX $dydx_to_osmo_channel)
dydx_denom_on_osmo=$(GET_IBC_DENOM OSMO $osmo_to_dydx_channel $DYDX_DENOM)

echo -e "\nDYDX -> OSMO:"
echo " Client: $dydx_to_osmo_client"
echo " Connection: $dydx_to_osmo_connection"
echo " Transfer Channel: $dydx_to_osmo_channel -> $osmo_to_dydx_channel"
echo " IBC Denom: $dydx_denom_on_osmo"

noble_to_osmo_client=$(GET_CLIENT_ID_FROM_CHAIN_ID NOBLE OSMO)
noble_to_osmo_connection=$(GET_CONNECTION_ID_FROM_CLIENT_ID NOBLE $noble_to_osmo_client)
noble_to_osmo_channel=$(GET_TRANSFER_CHANNEL_ID_FROM_CONNECTION_ID NOBLE $noble_to_osmo_connection)
osmo_to_noble_channel=$(GET_COUNTERPARTY_TRANSFER_CHANNEL_ID NOBLE $noble_to_osmo_channel)
usdc_denom_on_osmo=$(GET_IBC_DENOM OSMO $osmo_to_noble_channel $USDC_DENOM)

echo -e "\nNOBLE -> OSMO:"
echo " Client: $noble_to_osmo_client"
echo " Connection: $noble_to_osmo_connection"
echo " Transfer Channel: $noble_to_osmo_channel -> $osmo_to_noble_channel"
echo " IBC Denom: $usdc_denom_on_osmo"

echo -e "\nSending dydx/usdc to osmosis for initial liquidity..."

Expand All @@ -45,6 +41,13 @@ sleep 15
echo ">>> Balances:"
$OSMO_MAIN_CMD q bank balances $(OSMO_ADDRESS)

echo -e "\nDetermining IBC Denoms..."
dydx_denom_on_osmo=$(GET_IBC_DENOM OSMO $osmo_to_dydx_channel $DYDX_DENOM)
usdc_denom_on_osmo=$(GET_IBC_DENOM OSMO $osmo_to_noble_channel $USDC_DENOM)

echo " ibc/dydx on Osmosis: $dydx_denom_on_osmo"
echo " ibc/usdc on Osmosis: $usdc_denom_on_osmo"

echo -e "\nCreating dydx/usdc pool on osmosis..."
pool_file=${STATE}/${OSMO_NODE_PREFIX}1/pool.json
cat << EOF > $pool_file
Expand Down
2 changes: 1 addition & 1 deletion dockernet/scripts/community-pool-staking/setup_relayers.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ set -eu
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
source ${SCRIPT_DIR}/../../config.sh

for path in "dydx-noble" "noble-osmo" "osmo-dydx"; do
for path in "dydx-noble" "noble-osmo" "osmo-dydx" "stride-osmo"; do
relayer_logs=${LOGS}/relayer-${path}.log
relayer_config=$STATE/relayer-${path}/config
relayer_exec="$DOCKER_COMPOSE run --rm relayer-$path"
Expand Down
66 changes: 57 additions & 9 deletions proto/osmosis/gamm/v1beta1/osmosis.proto
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ package osmosis.gamm.v1beta1;
import "gogoproto/gogo.proto";
import "amino/amino.proto";
import "cosmos/base/v1beta1/coin.proto";
import "google/protobuf/timestamp.proto";

option go_package = "github.com/Stride-Labs/stride/v16/x/stakeibc/types";

// MsgSwapExactAmountIn stores the tx Msg type to swap tokens in the trade ICA
message MsgSwapExactAmountIn {
option (amino.name) = "osmosis/gamm/swap-exact-amount-in";

Expand All @@ -24,17 +26,63 @@ message MsgSwapExactAmountIn {
];
}

message MsgSwapExactAmountInResponse {
string token_out_amount = 1 [

(gogoproto.customtype) = "cosmossdk.io/math.Int",
(gogoproto.moretags) = "yaml:\"token_out_amount\"",
(gogoproto.nullable) = false
];
}

message SwapAmountInRoute {
uint64 pool_id = 1 [ (gogoproto.moretags) = "yaml:\"pool_id\"" ];
string token_out_denom = 2
[ (gogoproto.moretags) = "yaml:\"token_out_denom\"" ];
}

// A TwapRecord stores the most recent price of a pair of denom's
message OsmosisTwapRecord {
uint64 pool_id = 1;
// Lexicographically smaller denom of the pair
ethan-stride marked this conversation as resolved.
Show resolved Hide resolved
string asset0_denom = 2;
// Lexicographically larger denom of the pair
string asset1_denom = 3;
// height this record corresponds to, for debugging purposes
int64 height = 4 [
(gogoproto.moretags) = "yaml:\"record_height\"",
(gogoproto.jsontag) = "record_height"
];
// This field should only exist until we have a global registry in the state
// machine, mapping prior block heights within {TIME RANGE} to times.
google.protobuf.Timestamp time = 5 [
(gogoproto.nullable) = false,
(gogoproto.stdtime) = true,
(gogoproto.moretags) = "yaml:\"record_time\""
];

// We store the last spot prices in the struct, so that we can interpolate
// accumulator values for times between when accumulator records are stored.
string p0_last_spot_price = 6 [
(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec",
(gogoproto.nullable) = false
];
string p1_last_spot_price = 7 [
(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec",
(gogoproto.nullable) = false
];

string p0_arithmetic_twap_accumulator = 8 [
(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec",
(gogoproto.nullable) = false
];
string p1_arithmetic_twap_accumulator = 9 [
(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec",
(gogoproto.nullable) = false
];

string geometric_twap_accumulator = 10 [
(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec",
(gogoproto.nullable) = false
];

// This field contains the time in which the last spot price error occured.
// It is used to alert the caller if they are getting a potentially erroneous
// TWAP, due to an unforeseen underlying error.
google.protobuf.Timestamp last_error_time = 11 [
(gogoproto.nullable) = false,
(gogoproto.stdtime) = true,
(gogoproto.moretags) = "yaml:\"last_error_time\""
];
}
29 changes: 25 additions & 4 deletions proto/stride/stakeibc/trade_route.proto
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package stride.stakeibc;

import "gogoproto/gogo.proto";
import "stride/stakeibc/ica_account.proto";
import "cosmos_proto/cosmos.proto";
import "google/protobuf/timestamp.proto";

option go_package = "github.com/Stride-Labs/stride/v16/x/stakeibc/types";

Expand Down Expand Up @@ -39,15 +41,34 @@ message TradeRoute {
// Currently Osmosis is the only trade chain so this is an osmosis pool id
uint64 pool_id = 9;

// Spot price is a decimal ratio of the input to output denom as a string
string spot_price = 10;
// Spot price in the pool to convert the reward denom to the host denom
// output_tokens = swap_price * input tokens
// This value may be slightly stale as it is updated by an ICQ
string swap_price = 10 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
// timestamp that the price was last updated
google.protobuf.Timestamp price_update_time = 11
sampocs marked this conversation as resolved.
Show resolved Hide resolved
[ (gogoproto.nullable) = false, (gogoproto.stdtime) = true ];

// Threshold defining the percentage of tokens that could be lost in the trade
// This captures both the loss from slippage and from a stale price value on
// stride 0.05 means the output from the trade can be no less than a 5%
// deviation from the current value
string max_allowed_swap_loss_rate = 12 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];

// min and max set boundaries of reward denom on trade chain we will swap
string min_swap_amount = 11 [
string min_swap_amount = 13 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];
string max_swap_amount = 12 [
string max_swap_amount = 14 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];
Expand Down
15 changes: 15 additions & 0 deletions x/interchainquery/types/keys.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package types

import fmt "fmt"

const (
// ModuleName defines the module name
ModuleName = "interchainquery"
Expand Down Expand Up @@ -31,6 +33,14 @@ const (
STAKING_STORE_QUERY_WITH_PROOF = "store/staking/key"
// The bank store is key'd by the account address
BANK_STORE_QUERY_WITH_PROOF = "store/bank/key"
// The Osmosis twap store - key'd by the pool ID and denom's
TWAP_STORE_QUERY_WITH_PROOF = "store/twap/key"
)

var (
// Osmosis TWAP query info
OsmosisKeySeparator = "|"
OsmosisMostRecentTWAPsPrefix = "recent_twap" + OsmosisKeySeparator
)

var (
Expand All @@ -42,3 +52,8 @@ var (
func KeyPrefix(p string) []byte {
return []byte(p)
}

func FormatOsmosisMostRecentTWAPKey(poolId uint64, denom1, denom2 string) []byte {
poolIdBz := fmt.Sprintf("%0.20d", poolId)
return []byte(fmt.Sprintf("%s%s%s%s%s%s", OsmosisMostRecentTWAPsPrefix, poolIdBz, OsmosisKeySeparator, denom1, OsmosisKeySeparator, denom2))
}
4 changes: 2 additions & 2 deletions x/stakeibc/keeper/icqcallbacks.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const (
ICQCallbackID_WithdrawalRewardBalance = "withdrawalrewardbalance"
ICQCallbackID_TradeRewardBalance = "traderewardbalance"
ICQCallbackID_TradeConvertedBalance = "tradeconvertedbalance"
ICQCallbackID_PoolSpotPrice = "poolspotprice"
ICQCallbackID_PoolPrice = "poolprice"
)

// ICQCallbacks wrapper struct for stakeibc keeper
Expand Down Expand Up @@ -58,5 +58,5 @@ func (c ICQCallbacks) RegisterICQCallbacks() icqtypes.QueryCallbacks {
AddICQCallback(ICQCallbackID_WithdrawalRewardBalance, ICQCallback(WithdrawalRewardBalanceCallback)).
AddICQCallback(ICQCallbackID_TradeRewardBalance, ICQCallback(TradeRewardBalanceCallback)).
AddICQCallback(ICQCallbackID_TradeConvertedBalance, ICQCallback(TradeConvertedBalanceCallback)).
AddICQCallback(ICQCallbackID_PoolSpotPrice, ICQCallback(PoolSpotPriceCallback))
AddICQCallback(ICQCallbackID_PoolPrice, ICQCallback(PoolPriceCallback))
}
91 changes: 91 additions & 0 deletions x/stakeibc/keeper/icqcallbacks_pool_price.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package keeper

import (
"fmt"

errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/gogoproto/proto"

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

// PoolPriceCallback is a callback handler for PoolPrice query.
// The query response returns an Osmosis TwapRecord for the associated pool denom's
//
// The assets in the response are identified by indicies and are sorted alphabetically
// (e.g. if the two denom's are ibc/AXXX, and ibc/BXXX,
// then Asset0Denom is ibc/AXXX and Asset1Denom is ibc/BXXX)
//
// The price fields (P0LastSpotPrice and P1LastSpotPrice) represent the relative
// ratios of tokens in the pool
//
// P0LastSpotPrice gives the ratio of Asset0Denom / Asset1Denom
// P1LastSpotPrice gives the ratio of Asset1Denom / Asset0Denom
//
// When storing down the price, we want to denominate the price of the TargetDenom,
// relative to the price of the RewardDenom, which means we have to take the inverse
// from the response
func PoolPriceCallback(k Keeper, ctx sdk.Context, args []byte, query icqtypes.Query) error {
k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(query.ChainId, ICQCallbackID_PoolPrice,
"Starting pool spot price callback, QueryId: %vs, QueryType: %s, Connection: %s", query.Id, query.QueryType, query.ConnectionId))

chainId := query.ChainId // should be the tradeZoneId, used in logging

// Unmarshal the query response args, should be a TwapRecord type
var twapRecord types.OsmosisTwapRecord
err := twapRecord.Unmarshal(args)
if err != nil {
return errorsmod.Wrap(err, "unable to unmarshal the query response")
}

// Unmarshal the callback data containing the tradeRoute we are on
var tradeRoute types.TradeRoute
if err := proto.Unmarshal(query.CallbackData, &tradeRoute); err != nil {
return errorsmod.Wrapf(err, "unable to unmarshal trade reward balance callback data")
}

// Confirm the denom's from the query response match the denom's in the route
if err := AssertTwapAssetsMatchTradeRoute(twapRecord, tradeRoute); err != nil {
return err
}

// Get the associate "SpotPrice" from the twap record, based on the asset ordering
// The "SpotPrice" is actually a ratio of the assets in the pool
var price sdk.Dec
if twapRecord.Asset0Denom == tradeRoute.TargetDenomOnTradeZone {
price = twapRecord.P0LastSpotPrice
} else {
price = twapRecord.P1LastSpotPrice
}

k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(chainId, ICQCallbackID_PoolPrice,
"Query response - price ratio of %s to %s is %s",
tradeRoute.RewardDenomOnTradeZone, tradeRoute.TargetDenomOnTradeZone, price))

// Update the price and time on the trade route data
tradeRoute.SwapPrice = price
tradeRoute.PriceUpdateTime = ctx.BlockTime()
k.SetTradeRoute(ctx, tradeRoute)

return nil
}

// Helper function to confirm that the two assets in the twap record match the assets in the trade route
// The assets in the twap record are sorted alphabetically, so we have to check both orderings
func AssertTwapAssetsMatchTradeRoute(twapRecord types.OsmosisTwapRecord, tradeRoute types.TradeRoute) error {
hostDenomMatchFirst := twapRecord.Asset0Denom == tradeRoute.TargetDenomOnTradeZone
rewardDenomMatchSecond := twapRecord.Asset1Denom == tradeRoute.RewardDenomOnTradeZone

rewardDenomMatchFirst := twapRecord.Asset0Denom == tradeRoute.RewardDenomOnTradeZone
hostDenomMatchSecond := twapRecord.Asset1Denom == tradeRoute.TargetDenomOnTradeZone

if (hostDenomMatchFirst && rewardDenomMatchSecond) || (rewardDenomMatchFirst && hostDenomMatchSecond) {
return nil
}

return fmt.Errorf("Assets in query response (%s, %s) do not match denom's from trade route (%s, %s)",
twapRecord.Asset0Denom, twapRecord.Asset1Denom, tradeRoute.TargetDenomOnTradeZone, tradeRoute.RewardDenomOnTradeZone)
}
Loading