diff --git a/app/app.go b/app/app.go index 026737f3f23..6c7ab4307cf 100644 --- a/app/app.go +++ b/app/app.go @@ -138,7 +138,7 @@ func init() { DefaultNodeHome = filepath.Join(userHomeDir, ".osmosisd") } -// NewOsmosis returns a reference to an initialized Osmosis. +// NewOsmosisApp returns a reference to an initialized Osmosis. func NewOsmosisApp( logger log.Logger, db dbm.DB, diff --git a/app/keepers.go b/app/keepers.go index 7da313481a4..cca81eed4c5 100644 --- a/app/keepers.go +++ b/app/keepers.go @@ -45,6 +45,7 @@ import ( bech32ibctypes "github.com/osmosis-labs/bech32-ibc/x/bech32ibc/types" bech32ics20keeper "github.com/osmosis-labs/bech32-ibc/x/bech32ics20/keeper" + owasm "github.com/osmosis-labs/osmosis/v7/app/wasm" _ "github.com/osmosis-labs/osmosis/v7/client/docs/statik" claimkeeper "github.com/osmosis-labs/osmosis/v7/x/claim/keeper" claimtypes "github.com/osmosis-labs/osmosis/v7/x/claim/types" @@ -346,7 +347,10 @@ func (app *OsmosisApp) InitNormalKeepers( // The last arguments can contain custom message handlers, and custom query handlers, // if we want to allow any custom callbacks - supportedFeatures := "iterator,staking,stargate" + supportedFeatures := "iterator,staking,stargate,osmosis" + + wasmOpts = append(owasm.RegisterCustomPlugins(app.GAMMKeeper, app.BankKeeper), wasmOpts...) + wasmKeeper := wasm.NewKeeper( appCodec, keys[wasm.StoreKey], diff --git a/app/wasm/README.md b/app/wasm/README.md new file mode 100644 index 00000000000..99e944ee5d1 --- /dev/null +++ b/app/wasm/README.md @@ -0,0 +1,34 @@ +# CosmWasm support + +This package contains CosmWasm integration points. + +This package provides first class support for: + +* Queries + * Denoms + * Pools + * Prices +* Messages / Execution + * Mint + * Swap + +### Command line interface (CLI) + +* Commands + +```sh + osmosisd tx wasm -h +``` + +* Query + +```sh + osmosisd query wasm -h +``` + +### Tests + +This contains a few high level tests that `x/wasm` is properly integrated. + +Since the code tested is not in this repo, and we are just testing the application +integration (app.go), I figured this is the most suitable location for it. diff --git a/app/wasm/bindings/msg.go b/app/wasm/bindings/msg.go new file mode 100644 index 00000000000..c5ddfd5ac48 --- /dev/null +++ b/app/wasm/bindings/msg.go @@ -0,0 +1,23 @@ +package wasmbindings + +type OsmosisMsg struct { + // /// Contracts can mint native tokens that have an auto-generated denom + // /// namespaced under the contract's address. A contract may create any number + // /// of independent sub-denoms. + // MintTokens *MintTokens `json:"mint_tokens,omitempty"` + /// Swap over one or more pools + Swap *SwapMsg `json:"swap,omitempty"` +} + +// type MintTokens struct { +// /// Must be 2-32 alphanumeric characters +// SubDenom string `json:"sub_denom"` +// Amount sdk.Int `json:"amount"` +// Recipient string `json:"recipient"` +// } + +type SwapMsg struct { + First Swap `json:"first"` + Route []Step `json:"route"` + Amount SwapAmountWithLimit `json:"amount"` +} diff --git a/app/wasm/bindings/pool.go b/app/wasm/bindings/pool.go new file mode 100644 index 00000000000..7a3e545a93b --- /dev/null +++ b/app/wasm/bindings/pool.go @@ -0,0 +1,8 @@ +package wasmbindings + +import sdk "github.com/cosmos/cosmos-sdk/types" + +type PoolAssets struct { + Assets []sdk.Coin + Shares sdk.Coin +} diff --git a/app/wasm/bindings/query.go b/app/wasm/bindings/query.go new file mode 100644 index 00000000000..bc55d1f40dc --- /dev/null +++ b/app/wasm/bindings/query.go @@ -0,0 +1,73 @@ +package wasmbindings + +import ( + wasmvmtypes "github.com/CosmWasm/wasmvm/types" +) + +// OsmosisQuery contains osmosis custom queries. +// See https://github.com/confio/osmosis-bindings/blob/main/packages/bindings/src/query.rs +type OsmosisQuery struct { + // /// Given a subdenom minted by a contract via `OsmosisMsg::MintTokens`, + // /// returns the full denom as used by `BankMsg::Send`. + // FullDenom *FullDenom `json:"full_denom,omitempty"` + /// For a given pool ID, list all tokens traded on it with current liquidity (spot). + /// As well as the total number of LP shares and their denom. + PoolState *PoolState `json:"pool_state,omitempty"` + /// Return current spot price swapping In for Out on given pool ID. + /// Warning: this can easily be manipulated via sandwich attacks, do not use as price oracle. + /// We will add TWAP for more robust price feed. + SpotPrice *SpotPrice `json:"spot_price,omitempty"` + /// Return current spot price swapping In for Out on given pool ID. + EstimateSwap *EstimateSwap `json:"estimate_swap,omitempty"` +} + +// type FullDenom struct { +// Contract string `json:"contract"` +// SubDenom string `json:"sub_denom"` +// } + +type PoolState struct { + PoolId uint64 `json:"id"` +} + +type SpotPrice struct { + Swap Swap `json:"swap"` + WithSwapFee bool `json:"with_swap_fee"` +} + +type EstimateSwap struct { + Sender string `json:"sender"` + First Swap `json:"first"` + Route []Step `json:"route"` + Amount SwapAmount `json:"amount"` +} + +func (e *EstimateSwap) ToSwapMsg() *SwapMsg { + return &SwapMsg{ + First: e.First, + Route: e.Route, + Amount: e.Amount.Unlimited(), + } +} + +// type FullDenomResponse struct { +// Denom string `json:"denom"` +// } + +type PoolStateResponse struct { + /// The various assets that be swapped. Including current liquidity. + Assets []wasmvmtypes.Coin `json:"assets"` + /// The number of LP shares and their amount + Shares wasmvmtypes.Coin `json:"shares"` +} + +type SpotPriceResponse struct { + /// How many output we would get for 1 input + Price string `json:"price"` +} + +type EstimatePriceResponse struct { + // If you query with SwapAmount::Input, this is SwapAmount::Output. + // If you query with SwapAmount::Output, this is SwapAmount::Input. + Amount SwapAmount `json:"swap_amount"` +} diff --git a/app/wasm/bindings/types.go b/app/wasm/bindings/types.go new file mode 100644 index 00000000000..716207e7b81 --- /dev/null +++ b/app/wasm/bindings/types.go @@ -0,0 +1,74 @@ +package wasmbindings + +import ( + "math" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type Swap struct { + PoolId uint64 `json:"pool_id"` + DenomIn string `json:"denom_in"` + DenomOut string `json:"denom_out"` +} + +type Step struct { + PoolId uint64 `json:"pool_id"` + DenomOut string `json:"denom_out"` +} + +type SwapAmount struct { + In *sdk.Int `json:"in,omitempty"` + Out *sdk.Int `json:"out,omitempty"` +} + +// This returns SwapAmountWithLimit with the largest possible limits (that will never be hit) +func (s SwapAmount) Unlimited() SwapAmountWithLimit { + if s.In != nil { + return SwapAmountWithLimit{ + ExactIn: &ExactIn{ + Input: *s.In, + MinOutput: sdk.NewInt(1), + }, + } + } + if s.Out != nil { + return SwapAmountWithLimit{ + ExactOut: &ExactOut{ + Output: *s.Out, + MaxInput: sdk.NewInt(math.MaxInt64), + }, + } + } + panic("Must define In or Out") +} + +type SwapAmountWithLimit struct { + ExactIn *ExactIn `json:"exact_in,omitempty"` + ExactOut *ExactOut `json:"exact_out,omitempty"` +} + +// This returns the amount without min/max to use as simpler argument +func (s SwapAmountWithLimit) RemoveLimit() SwapAmount { + if s.ExactIn != nil { + return SwapAmount{ + In: &s.ExactIn.Input, + } + } + if s.ExactOut != nil { + return SwapAmount{ + Out: &s.ExactOut.Output, + } + } + panic("Must define ExactIn or ExactOut") +} + +type ExactIn struct { + Input sdk.Int `json:"input"` + MinOutput sdk.Int `json:"min_output"` +} + +type ExactOut struct { + MaxInput sdk.Int `json:"max_input"` + Output sdk.Int `json:"output"` +} diff --git a/app/wasm/bindings/types_test.go b/app/wasm/bindings/types_test.go new file mode 100644 index 00000000000..d3c0e5670d8 --- /dev/null +++ b/app/wasm/bindings/types_test.go @@ -0,0 +1,178 @@ +package wasmbindings + +import ( + "encoding/json" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +var ( + swapJson = []byte("{ \"pool_id\": 1, \"denom_in\": \"denomIn\", \"denom_out\": \"denomOut\" }") + swap = Swap{ + PoolId: 1, + DenomIn: "denomIn", + DenomOut: "denomOut", + } + stepJson = []byte("{ \"pool_id\": 2, \"denom_out\": \"denomOut\" }") + step = Step{ + PoolId: 2, + DenomOut: "denomOut", + } + swapAmountInJson = []byte("{ \"in\": \"123\", \"out\": null }") + in = sdk.NewInt(123) + swapAmountIn = SwapAmount{ + In: &in, + } + swapAmountOutJson = []byte("{ \"out\": \"456\" }") + out = sdk.NewInt(456) + swapAmountOut = SwapAmount{ + Out: &out, + } + exactIn = ExactIn{ + Input: sdk.NewInt(789), + MinOutput: sdk.NewInt(101112), + } + exactOut = ExactOut{ + MaxInput: sdk.NewInt(131415), + Output: sdk.NewInt(161718), + } + swapAmountExactInJson = []byte("{ \"exact_in\": { \"input\": \"789\", \"min_output\": \"101112\" } }") + swapAmountExactIn = SwapAmountWithLimit{ + ExactIn: &exactIn, + } + swapAmountExactOutJson = []byte("{ \"exact_in\": null, \"exact_out\": { \"max_input\": \"131415\", \"output\": \"161718\" } }") + swapAmountExactOut = SwapAmountWithLimit{ + ExactOut: &exactOut, + } +) + +func TestTypesEncodeDecode(t *testing.T) { + // Swap + // Marshal + bzSwap, err := json.Marshal(swap) + require.NoError(t, err) + // Unmarshal + var swap1 Swap + err = json.Unmarshal(bzSwap, &swap1) + require.NoError(t, err) + // Check + assert.Equal(t, swap, swap1) + + // Step + // Marshal + bzStep, err := json.Marshal(step) + require.NoError(t, err) + // Unmarshal + var step1 Step + err = json.Unmarshal(bzStep, &step1) + require.NoError(t, err) + // Check + assert.Equal(t, step, step1) + + // SwapAmount + // Marshal + bzSwapAmount, err := json.Marshal(swapAmountOut) + require.NoError(t, err) + // Unmarshal + var swapAmount1 SwapAmount + err = json.Unmarshal(bzSwapAmount, &swapAmount1) + require.NoError(t, err) + // Check + assert.Equal(t, swapAmountOut, swapAmount1) + + // SwapAmount in + // Marshal + bzSwapAmount2, err := json.Marshal(swapAmountIn) + require.NoError(t, err) + // Unmarshal + var swapAmount2 SwapAmount + err = json.Unmarshal(bzSwapAmount2, &swapAmount2) + require.NoError(t, err) + // Check + assert.Equal(t, swapAmountIn, swapAmount2) + + // SwapAmount out + // Marshal + bzSwapAmount3, err := json.Marshal(swapAmountOut) + require.NoError(t, err) + // Unmarshal + var swapAmount3 SwapAmount + err = json.Unmarshal(bzSwapAmount3, &swapAmount3) + require.NoError(t, err) + // Check + assert.Equal(t, swapAmountOut, swapAmount3) + + // SwapAmount exact in + // Marshal + bzSwapAmountWithLimit1, err := json.Marshal(swapAmountExactIn) + require.NoError(t, err) + // Unmarshal + var swapAmountWithLimit1 SwapAmountWithLimit + err = json.Unmarshal(bzSwapAmountWithLimit1, &swapAmountWithLimit1) + require.NoError(t, err) + // Check + assert.Equal(t, swapAmountExactIn, swapAmountWithLimit1) + + // SwapAmount exact out + // Marshal + bzSwapAmountWithLimit2, err := json.Marshal(swapAmountExactOut) + require.NoError(t, err) + // Unmarshal + var swapAmountWithLimit2 SwapAmountWithLimit + err = json.Unmarshal(bzSwapAmountWithLimit2, &swapAmountWithLimit2) + require.NoError(t, err) + // Check + assert.Equal(t, swapAmountExactOut, swapAmountWithLimit2) +} + +func TestTypesDecode(t *testing.T) { + // Swap + // Unmarshal + var swap1 Swap + err := json.Unmarshal([]byte(swapJson), &swap1) + require.NoError(t, err) + // Check + assert.Equal(t, swap, swap1) + + // Step + // Unmarshal + var step1 Step + err = json.Unmarshal(stepJson, &step1) + require.NoError(t, err) + // Check + assert.Equal(t, step, step1) + + // SwapAmount in + // Unmarshal + var swapAmount1 SwapAmount + err = json.Unmarshal(swapAmountInJson, &swapAmount1) + require.NoError(t, err) + // Check + assert.Equal(t, swapAmountIn, swapAmount1) + + // SwapAmount out + // Unmarshal + var swapAmount2 SwapAmount + err = json.Unmarshal(swapAmountOutJson, &swapAmount2) + require.NoError(t, err) + // Check + assert.Equal(t, swapAmountOut, swapAmount2) + + // SwapAmount exact in + // Unmarshal + var swapAmountWithLimit1 SwapAmountWithLimit + err = json.Unmarshal(swapAmountExactInJson, &swapAmountWithLimit1) + require.NoError(t, err) + // Check + assert.Equal(t, swapAmountExactIn, swapAmountWithLimit1) + + // SwapAmount exact out + // Unmarshal + var swapAmountWithLimit2 SwapAmountWithLimit + err = json.Unmarshal(swapAmountExactOutJson, &swapAmountWithLimit2) + require.NoError(t, err) + // Check + assert.Equal(t, swapAmountExactOut, swapAmountWithLimit2) +} diff --git a/app/wasm/message_plugin.go b/app/wasm/message_plugin.go new file mode 100644 index 00000000000..cb53f09f6c7 --- /dev/null +++ b/app/wasm/message_plugin.go @@ -0,0 +1,198 @@ +package wasm + +import ( + "encoding/json" + + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + + wasmbindings "github.com/osmosis-labs/osmosis/v7/app/wasm/bindings" + gammkeeper "github.com/osmosis-labs/osmosis/v7/x/gamm/keeper" + gammtypes "github.com/osmosis-labs/osmosis/v7/x/gamm/types" +) + +func CustomMessageDecorator(gammKeeper *gammkeeper.Keeper, bank *bankkeeper.BaseKeeper) func(wasmkeeper.Messenger) wasmkeeper.Messenger { + return func(old wasmkeeper.Messenger) wasmkeeper.Messenger { + return &CustomMessenger{ + wrapped: old, + bank: bank, + gammKeeper: gammKeeper, + } + } +} + +type CustomMessenger struct { + wrapped wasmkeeper.Messenger + bank *bankkeeper.BaseKeeper + gammKeeper *gammkeeper.Keeper +} + +var _ wasmkeeper.Messenger = (*CustomMessenger)(nil) + +func (m *CustomMessenger) DispatchMsg(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) ([]sdk.Event, [][]byte, error) { + if msg.Custom != nil { + // only handle the happy path where this is really minting / swapping ... + // leave everything else for the wrapped version + var contractMsg wasmbindings.OsmosisMsg + if err := json.Unmarshal(msg.Custom, &contractMsg); err != nil { + return nil, nil, sdkerrors.Wrap(err, "osmosis msg") + } + // if contractMsg.MintTokens != nil { + // return m.mintTokens(ctx, contractAddr, contractMsg.MintTokens) + // } + if contractMsg.Swap != nil { + return m.swapTokens(ctx, contractAddr, contractMsg.Swap) + } + } + return m.wrapped.DispatchMsg(ctx, contractAddr, contractIBCPortID, msg) +} + +// func (m *CustomMessenger) mintTokens(ctx sdk.Context, contractAddr sdk.AccAddress, mint *wasmbindings.MintTokens) ([]sdk.Event, [][]byte, error) { +// err := PerformMint(m.bank, ctx, contractAddr, mint) +// if err != nil { +// return nil, nil, sdkerrors.Wrap(err, "perform mint") +// } +// return nil, nil, nil +// } + +// func PerformMint(b *bankkeeper.BaseKeeper, ctx sdk.Context, contractAddr sdk.AccAddress, mint *wasmbindings.MintTokens) error { +// if mint == nil { +// return wasmvmtypes.InvalidRequest{Err: "mint token null mint"} +// } +// rcpt, err := parseAddress(mint.Recipient) +// if err != nil { +// return err +// } + +// denom, err := GetFullDenom(contractAddr.String(), mint.SubDenom) +// if err != nil { +// return sdkerrors.Wrap(err, "mint token denom") +// } +// if mint.Amount.IsNegative() { +// return wasmvmtypes.InvalidRequest{Err: "mint token negative amount"} +// } +// coins := []sdk.Coin{sdk.NewCoin(denom, mint.Amount)} + +// err = b.MintCoins(ctx, gammtypes.ModuleName, coins) +// if err != nil { +// return sdkerrors.Wrap(err, "minting coins from message") +// } +// err = b.SendCoinsFromModuleToAccount(ctx, gammtypes.ModuleName, rcpt, coins) +// if err != nil { +// return sdkerrors.Wrap(err, "sending newly minted coins from message") +// } +// return nil +// } + +func (m *CustomMessenger) swapTokens(ctx sdk.Context, contractAddr sdk.AccAddress, swap *wasmbindings.SwapMsg) ([]sdk.Event, [][]byte, error) { + _, err := PerformSwap(m.gammKeeper, ctx, contractAddr, swap) + if err != nil { + return nil, nil, sdkerrors.Wrap(err, "perform swap") + } + return nil, nil, nil +} + +// PerformSwap can be used both for the real swap, and the EstimateSwap query +func PerformSwap(keeper *gammkeeper.Keeper, ctx sdk.Context, contractAddr sdk.AccAddress, swap *wasmbindings.SwapMsg) (*wasmbindings.SwapAmount, error) { + if swap == nil { + return nil, wasmvmtypes.InvalidRequest{Err: "gamm perform swap null swap"} + } + if swap.Amount.ExactIn != nil { + routes := []gammtypes.SwapAmountInRoute{{ + PoolId: swap.First.PoolId, + TokenOutDenom: swap.First.DenomOut, + }} + for _, step := range swap.Route { + routes = append(routes, gammtypes.SwapAmountInRoute{ + PoolId: step.PoolId, + TokenOutDenom: step.DenomOut, + }) + } + if swap.Amount.ExactIn.Input.IsNegative() { + return nil, wasmvmtypes.InvalidRequest{Err: "gamm perform swap negative amount in"} + } + tokenIn := sdk.Coin{ + Denom: swap.First.DenomIn, + Amount: swap.Amount.ExactIn.Input, + } + tokenOutMinAmount := swap.Amount.ExactIn.MinOutput + tokenOutAmount, err := keeper.MultihopSwapExactAmountIn(ctx, contractAddr, routes, tokenIn, tokenOutMinAmount) + if err != nil { + return nil, sdkerrors.Wrap(err, "gamm perform swap exact amount in") + } + return &wasmbindings.SwapAmount{Out: &tokenOutAmount}, nil + } else if swap.Amount.ExactOut != nil { + routes := []gammtypes.SwapAmountOutRoute{{ + PoolId: swap.First.PoolId, + TokenInDenom: swap.First.DenomIn, + }} + output := swap.First.DenomOut + for _, step := range swap.Route { + routes = append(routes, gammtypes.SwapAmountOutRoute{ + PoolId: step.PoolId, + TokenInDenom: output, + }) + output = step.DenomOut + } + tokenInMaxAmount := swap.Amount.ExactOut.MaxInput + if swap.Amount.ExactOut.Output.IsNegative() { + return nil, wasmvmtypes.InvalidRequest{Err: "gamm perform swap negative amount out"} + } + tokenOut := sdk.Coin{ + Denom: output, + Amount: swap.Amount.ExactOut.Output, + } + tokenInAmount, err := keeper.MultihopSwapExactAmountOut(ctx, contractAddr, routes, tokenInMaxAmount, tokenOut) + if err != nil { + return nil, sdkerrors.Wrap(err, "gamm perform swap exact amount out") + } + return &wasmbindings.SwapAmount{In: &tokenInAmount}, nil + } else { + return nil, wasmvmtypes.UnsupportedRequest{Kind: "must support either Swap.ExactIn or Swap.ExactOut"} + } +} + +// // GetFullDenom is a function, not method, so the message_plugin can use it +// func GetFullDenom(contract string, subDenom string) (string, error) { +// // Address validation +// if _, err := parseAddress(contract); err != nil { +// return "", err +// } +// err := ValidateSubDenom(subDenom) +// if err != nil { +// return "", sdkerrors.Wrap(err, "validate sub-denom") +// } +// fullDenom := fmt.Sprintf("cw/%s/%s", contract, subDenom) + +// return fullDenom, nil +// } + +// func parseAddress(addr string) (sdk.AccAddress, error) { +// parsed, err := sdk.AccAddressFromBech32(addr) +// if err != nil { +// return nil, sdkerrors.Wrap(err, "address from bech32") +// } +// err = sdk.VerifyAddressFormat(parsed) +// if err != nil { +// return nil, sdkerrors.Wrap(err, "verify address format") +// } +// return parsed, nil +// } + +// const reSubdenomStr = `^[a-zA-Z][a-zA-Z0-9]{2,31}$` + +// var reSubdenom *regexp.Regexp + +// func init() { +// reSubdenom = regexp.MustCompile(reSubdenomStr) +// } + +// func ValidateSubDenom(subDenom string) error { +// if !reSubdenom.MatchString(subDenom) { +// return fmt.Errorf("invalid subdenom: %s", subDenom) +// } +// return nil +// } diff --git a/app/wasm/queries.go b/app/wasm/queries.go new file mode 100644 index 00000000000..4d15cb4de95 --- /dev/null +++ b/app/wasm/queries.go @@ -0,0 +1,83 @@ +package wasm + +import ( + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + wasmbindings "github.com/osmosis-labs/osmosis/v7/app/wasm/bindings" + gammkeeper "github.com/osmosis-labs/osmosis/v7/x/gamm/keeper" + gammtypes "github.com/osmosis-labs/osmosis/v7/x/gamm/types" +) + +type QueryPlugin struct { + gammKeeper *gammkeeper.Keeper +} + +// NewQueryPlugin constructor +func NewQueryPlugin( + gammK *gammkeeper.Keeper, +) *QueryPlugin { + return &QueryPlugin{ + gammKeeper: gammK, + } +} + +func (qp QueryPlugin) GetPoolState(ctx sdk.Context, poolID uint64) (*wasmbindings.PoolAssets, error) { + poolData, err := qp.gammKeeper.GetPool(ctx, poolID) + if err != nil { + return nil, sdkerrors.Wrap(err, "gamm get pool") + } + var poolAssets wasmbindings.PoolAssets + poolAssets.Assets = poolData.GetTotalPoolLiquidity(ctx) + poolAssets.Shares = sdk.Coin{ + Denom: gammtypes.GetPoolShareDenom(poolID), + Amount: poolData.GetTotalShares(), + } + return &poolAssets, nil +} + +func (qp QueryPlugin) GetSpotPrice(ctx sdk.Context, spotPrice *wasmbindings.SpotPrice) (*sdk.Dec, error) { + if spotPrice == nil { + return nil, wasmvmtypes.InvalidRequest{Err: "gamm spot price null"} + } + poolId := spotPrice.Swap.PoolId + denomIn := spotPrice.Swap.DenomIn + denomOut := spotPrice.Swap.DenomOut + withSwapFee := spotPrice.WithSwapFee + price, err := qp.gammKeeper.CalculateSpotPrice(ctx, poolId, denomIn, denomOut) + if err != nil { + return nil, sdkerrors.Wrap(err, "gamm get spot price") + } + if withSwapFee { + poolData, err := qp.gammKeeper.GetPool(ctx, poolId) + if err != nil { + return nil, sdkerrors.Wrap(err, "gamm get pool") + } + price = price.Mul(sdk.OneDec().Sub(poolData.GetSwapFee(ctx))) + } + return &price, nil +} + +func (qp QueryPlugin) EstimateSwap(ctx sdk.Context, estimateSwap *wasmbindings.EstimateSwap) (*wasmbindings.SwapAmount, error) { + if estimateSwap == nil { + return nil, wasmvmtypes.InvalidRequest{Err: "gamm estimate swap null"} + } + if err := sdk.ValidateDenom(estimateSwap.First.DenomIn); err != nil { + return nil, sdkerrors.Wrap(err, "gamm estimate swap denom in") + } + if err := sdk.ValidateDenom(estimateSwap.First.DenomOut); err != nil { + return nil, sdkerrors.Wrap(err, "gamm estimate swap denom out") + } + senderAddr, err := sdk.AccAddressFromBech32(estimateSwap.Sender) + if err != nil { + return nil, sdkerrors.Wrap(err, "gamm estimate swap sender address") + } + + if estimateSwap.Amount == (wasmbindings.SwapAmount{}) { + return nil, wasmvmtypes.InvalidRequest{Err: "gamm estimate swap empty swap"} + } + + estimate, err := PerformSwap(qp.gammKeeper, ctx, senderAddr, estimateSwap.ToSwapMsg()) + return estimate, err +} diff --git a/app/wasm/query_plugin.go b/app/wasm/query_plugin.go new file mode 100644 index 00000000000..64d48213407 --- /dev/null +++ b/app/wasm/query_plugin.go @@ -0,0 +1,105 @@ +package wasm + +import ( + "encoding/json" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + + bindings "github.com/osmosis-labs/osmosis/v7/app/wasm/bindings" +) + +func CustomQuerier(osmoKeeper *QueryPlugin) func(ctx sdk.Context, request json.RawMessage) ([]byte, error) { + return func(ctx sdk.Context, request json.RawMessage) ([]byte, error) { + var contractQuery bindings.OsmosisQuery + if err := json.Unmarshal(request, &contractQuery); err != nil { + return nil, sdkerrors.Wrap(err, "osmosis query") + } + + // if contractQuery.FullDenom != nil { + // contract := contractQuery.FullDenom.Contract + // subDenom := contractQuery.FullDenom.SubDenom + + // fullDenom, err := GetFullDenom(contract, subDenom) + // if err != nil { + // return nil, sdkerrors.Wrap(err, "osmo full denom query") + // } + + // res := bindings.FullDenomResponse{ + // Denom: fullDenom, + // } + // bz, err := json.Marshal(res) + // if err != nil { + // return nil, sdkerrors.Wrap(err, "osmo full denom query response") + // } + // return bz, nil + // } else if contractQuery.PoolState != nil { + if contractQuery.PoolState != nil { + poolId := contractQuery.PoolState.PoolId + + state, err := osmoKeeper.GetPoolState(ctx, poolId) + if err != nil { + return nil, sdkerrors.Wrap(err, "osmo pool state query") + } + + assets := ConvertSdkCoinsToWasmCoins(state.Assets) + shares := ConvertSdkCoinToWasmCoin(state.Shares) + + res := bindings.PoolStateResponse{ + Assets: assets, + Shares: shares, + } + bz, err := json.Marshal(res) + if err != nil { + return nil, sdkerrors.Wrap(err, "osmo pool state query response") + } + return bz, nil + } else if contractQuery.SpotPrice != nil { + spotPrice, err := osmoKeeper.GetSpotPrice(ctx, contractQuery.SpotPrice) + if err != nil { + return nil, sdkerrors.Wrap(err, "osmo spot price query") + } + + res := bindings.SpotPriceResponse{Price: spotPrice.String()} + bz, err := json.Marshal(res) + if err != nil { + return nil, sdkerrors.Wrap(err, "osmo spot price query response") + } + return bz, nil + } else if contractQuery.EstimateSwap != nil { + swapAmount, err := osmoKeeper.EstimateSwap(ctx, contractQuery.EstimateSwap) + if err != nil { + return nil, sdkerrors.Wrap(err, "osmo estimate swap query") + } + + res := bindings.EstimatePriceResponse{Amount: *swapAmount} + bz, err := json.Marshal(res) + if err != nil { + return nil, sdkerrors.Wrap(err, "osmo estimate swap query response") + } + return bz, nil + } + return nil, wasmvmtypes.UnsupportedRequest{Kind: "unknown osmosis query variant"} + } +} + +// ConvertSdkCoinsToWasmCoins converts sdk type coins to wasm vm type coins +func ConvertSdkCoinsToWasmCoins(coins []sdk.Coin) wasmvmtypes.Coins { + var toSend wasmvmtypes.Coins + for _, coin := range coins { + c := ConvertSdkCoinToWasmCoin(coin) + toSend = append(toSend, c) + } + return toSend +} + +// ConvertSdkCoinToWasmCoin converts a sdk type coin to a wasm vm type coin +func ConvertSdkCoinToWasmCoin(coin sdk.Coin) wasmvmtypes.Coin { + return wasmvmtypes.Coin{ + Denom: coin.Denom, + // Note: gamm tokens have 18 decimal places, so 10^22 is common, no longer in u64 range + Amount: coin.Amount.String(), + } +} diff --git a/app/wasm/test/custom_msg_test.go b/app/wasm/test/custom_msg_test.go new file mode 100644 index 00000000000..25477567edc --- /dev/null +++ b/app/wasm/test/custom_msg_test.go @@ -0,0 +1,469 @@ +package wasm + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/CosmWasm/wasmd/x/wasm/keeper" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/osmosis-labs/osmosis/v7/app" + "github.com/osmosis-labs/osmosis/v7/app/wasm/bindings" +) + +// func TestMintMsg(t *testing.T) { +// creator := RandomAccountAddress() +// osmosis, ctx := SetupCustomApp(t, creator) + +// lucky := RandomAccountAddress() +// reflect := instantiateReflectContract(t, ctx, osmosis, lucky) +// require.NotEmpty(t, reflect) + +// // lucky was broke +// balances := osmosis.BankKeeper.GetAllBalances(ctx, lucky) +// require.Empty(t, balances) + +// amount, ok := sdk.NewIntFromString("808010808") +// require.True(t, ok) +// msg := wasmbindings.OsmosisMsg{MintTokens: &wasmbindings.MintTokens{ +// SubDenom: "SUN", +// Amount: amount, +// Recipient: lucky.String(), +// }} +// err := executeCustom(t, ctx, osmosis, reflect, lucky, msg, sdk.Coin{}) +// require.NoError(t, err) + +// balances = osmosis.BankKeeper.GetAllBalances(ctx, lucky) +// require.Len(t, balances, 1) +// coin := balances[0] +// require.Equal(t, amount, coin.Amount) +// require.Contains(t, coin.Denom, "cw/") + +// // query the denom and see if it matches +// query := wasmbindings.OsmosisQuery{ +// FullDenom: &wasmbindings.FullDenom{ +// Contract: reflect.String(), +// SubDenom: "SUN", +// }, +// } +// resp := wasmbindings.FullDenomResponse{} +// queryCustom(t, ctx, osmosis, reflect, query, &resp) + +// require.Equal(t, resp.Denom, coin.Denom) +// } + +type BaseState struct { + StarPool uint64 + AtomPool uint64 + RegenPool uint64 +} + +func TestSwapMsg(t *testing.T) { + // table tests with this setup + cases := map[string]struct { + msg func(BaseState) *wasmbindings.SwapMsg + expectErr bool + initFunds sdk.Coin + finalFunds []sdk.Coin + }{ + "exact in: simple swap works": { + msg: func(state BaseState) *wasmbindings.SwapMsg { + return &wasmbindings.SwapMsg{ + First: wasmbindings.Swap{ + PoolId: state.StarPool, + DenomIn: "uosmo", + DenomOut: "ustar", + }, + // Note: you must use empty array, not nil, for valid Rust JSON + Route: []wasmbindings.Step{}, + Amount: wasmbindings.SwapAmountWithLimit{ + ExactIn: &wasmbindings.ExactIn{ + Input: sdk.NewInt(12000000), + MinOutput: sdk.NewInt(5000000), + }, + }, + } + }, + initFunds: sdk.NewInt64Coin("uosmo", 13000000), + finalFunds: []sdk.Coin{ + sdk.NewInt64Coin("uosmo", 1000000), + sdk.NewInt64Coin("ustar", 120000000), + }, + }, + "exact in: price too low": { + msg: func(state BaseState) *wasmbindings.SwapMsg { + return &wasmbindings.SwapMsg{ + First: wasmbindings.Swap{ + PoolId: state.StarPool, + DenomIn: "uosmo", + DenomOut: "ustar", + }, + // Note: you must use empty array, not nil, for valid Rust JSON + Route: []wasmbindings.Step{}, + Amount: wasmbindings.SwapAmountWithLimit{ + ExactIn: &wasmbindings.ExactIn{ + Input: sdk.NewInt(12000000), + MinOutput: sdk.NewInt(555000000), + }, + }, + } + }, + initFunds: sdk.NewInt64Coin("uosmo", 13000000), + expectErr: true, + }, + "exact in: not enough funds to swap": { + msg: func(state BaseState) *wasmbindings.SwapMsg { + return &wasmbindings.SwapMsg{ + First: wasmbindings.Swap{ + PoolId: state.StarPool, + DenomIn: "uosmo", + DenomOut: "ustar", + }, + // Note: you must use empty array, not nil, for valid Rust JSON + Route: []wasmbindings.Step{}, + Amount: wasmbindings.SwapAmountWithLimit{ + ExactIn: &wasmbindings.ExactIn{ + Input: sdk.NewInt(12000000), + MinOutput: sdk.NewInt(5000000), + }, + }, + } + }, + initFunds: sdk.NewInt64Coin("uosmo", 7000000), + expectErr: true, + }, + "exact in: invalidPool": { + msg: func(state BaseState) *wasmbindings.SwapMsg { + return &wasmbindings.SwapMsg{ + First: wasmbindings.Swap{ + PoolId: state.StarPool, + DenomIn: "uosmo", + DenomOut: "uatom", + }, + // Note: you must use empty array, not nil, for valid Rust JSON + Route: []wasmbindings.Step{}, + Amount: wasmbindings.SwapAmountWithLimit{ + ExactIn: &wasmbindings.ExactIn{ + Input: sdk.NewInt(12000000), + MinOutput: sdk.NewInt(100000), + }, + }, + } + }, + initFunds: sdk.NewInt64Coin("uosmo", 13000000), + expectErr: true, + }, + + // FIXME: this panics in GAMM module !?! hits a known TODO + // https://github.com/osmosis-labs/osmosis/blob/a380ab2fcd39fb94c2b10411e07daf664911257a/osmomath/math.go#L47-L51 + //"exact out: panics if too much swapped": { + // msg: func(state BaseState) *wasmbindings.SwapMsg { + // return &wasmbindings.SwapMsg{ + // First: wasmbindings.Swap{ + // PoolId: state.StarPool, + // DenomIn: "uosmo", + // DenomOut: "ustar", + // }, + // // Note: you must use empty array, not nil, for valid Rust JSON + // Route: []wasmbindings.Step{}, + // Amount: wasmbindings.SwapAmountWithLimit{ + // ExactOut: &wasmbindings.ExactOut{ + // MaxInput: sdk.NewInt(22000000), + // Output: sdk.NewInt(120000000), + // }, + // }, + // } + // }, + // initFunds: sdk.NewInt64Coin("uosmo", 15000000), + // finalFunds: []sdk.Coin{ + // sdk.NewInt64Coin("uosmo", 3000000), + // sdk.NewInt64Coin("ustar", 120000000), + // }, + //}, + + "exact out: simple swap works": { + msg: func(state BaseState) *wasmbindings.SwapMsg { + return &wasmbindings.SwapMsg{ + First: wasmbindings.Swap{ + PoolId: state.AtomPool, + DenomIn: "uosmo", + DenomOut: "uatom", + }, + // Note: you must use empty array, not nil, for valid Rust JSON + Route: []wasmbindings.Step{}, + Amount: wasmbindings.SwapAmountWithLimit{ + ExactOut: &wasmbindings.ExactOut{ + // 12 OSMO * 6 ATOM == 18 OSMO * 4 ATOM (+6 OSMO, -2 ATOM) + MaxInput: sdk.NewInt(7000000), + Output: sdk.NewInt(2000000), + }, + }, + } + }, + initFunds: sdk.NewInt64Coin("uosmo", 8000000), + finalFunds: []sdk.Coin{ + sdk.NewInt64Coin("uatom", 2000000), + sdk.NewInt64Coin("uosmo", 2000000), + }, + }, + + "exact in: 2 step multi-hop": { + msg: func(state BaseState) *wasmbindings.SwapMsg { + return &wasmbindings.SwapMsg{ + First: wasmbindings.Swap{ + PoolId: state.StarPool, + DenomIn: "ustar", + DenomOut: "uosmo", + }, + Route: []wasmbindings.Step{{ + PoolId: state.AtomPool, + DenomOut: "uatom", + }}, + Amount: wasmbindings.SwapAmountWithLimit{ + ExactIn: &wasmbindings.ExactIn{ + Input: sdk.NewInt(240000000), + MinOutput: sdk.NewInt(1999000), + }, + }, + } + }, + initFunds: sdk.NewInt64Coin("ustar", 240000000), + finalFunds: []sdk.Coin{ + // 240 STAR -> 6 OSMO + // 6 OSMO -> 2 ATOM (with minor rounding) + sdk.NewInt64Coin("uatom", 1999999), + }, + }, + "exact out: 2 step multi-hop": { + msg: func(state BaseState) *wasmbindings.SwapMsg { + return &wasmbindings.SwapMsg{ + First: wasmbindings.Swap{ + PoolId: state.AtomPool, + DenomIn: "uosmo", + DenomOut: "uatom", + }, + Route: []wasmbindings.Step{{ + PoolId: state.RegenPool, + DenomOut: "uregen", + }}, + Amount: wasmbindings.SwapAmountWithLimit{ + ExactOut: &wasmbindings.ExactOut{ + MaxInput: sdk.NewInt(6000000), + Output: sdk.NewInt(24000000), + }, + }, + } + }, + initFunds: sdk.NewInt64Coin("uosmo", 6000000), + finalFunds: []sdk.Coin{ + // 6 OSMO -> 2 ATOM + // 2 ATOM -> 24 REGEN (with minor rounding) + sdk.NewInt64Coin("uosmo", 5), + sdk.NewInt64Coin("uregen", 24000000), + }, + }, + + // FIXME: this panics in GAMM module !?! hits a known TODO + // https://github.com/osmosis-labs/osmosis/blob/a380ab2fcd39fb94c2b10411e07daf664911257a/osmomath/math.go#L47-L51 + // "exact out: panics on math power stuff": { + // msg: func(state BaseState) *wasmbindings.SwapMsg { + // return &wasmbindings.SwapMsg{ + // First: wasmbindings.Swap{ + // PoolId: state.StarPool, + // DenomIn: "ustar", + // DenomOut: "uosmo", + // }, + // Route: []wasmbindings.Step{{ + // PoolId: state.AtomPool, + // DenomOut: "uatom", + // }}, + // Amount: wasmbindings.SwapAmountWithLimit{ + // ExactOut: &wasmbindings.ExactOut{ + // MaxInput: sdk.NewInt(240005000), + // Output: sdk.NewInt(2000000), + // }, + // }, + // } + // }, + // initFunds: sdk.NewInt64Coin("ustar", 240005000), + // finalFunds: []sdk.Coin{ + // // 240 STAR -> 6 OSMO + // // 6 OSMO -> 2 ATOM (with minor rounding) + // sdk.NewInt64Coin("uatom", 2000000), + // sdk.NewInt64Coin("ustar", 5000), + // }, + // }, + + "exact in: 3 step multi-hop": { + msg: func(state BaseState) *wasmbindings.SwapMsg { + return &wasmbindings.SwapMsg{ + First: wasmbindings.Swap{ + PoolId: state.StarPool, + DenomIn: "ustar", + DenomOut: "uosmo", + }, + Route: []wasmbindings.Step{{ + PoolId: state.AtomPool, + DenomOut: "uatom", + }, { + PoolId: state.RegenPool, + DenomOut: "uregen", + }}, + Amount: wasmbindings.SwapAmountWithLimit{ + ExactIn: &wasmbindings.ExactIn{ + Input: sdk.NewInt(240000000), + MinOutput: sdk.NewInt(23900000), + }, + }, + } + }, + initFunds: sdk.NewInt64Coin("ustar", 240000000), + finalFunds: []sdk.Coin{ + // 240 STAR -> 6 OSMO + // 6 OSMO -> 2 ATOM + // 2 ATOM -> 24 REGEN (with minor rounding) + sdk.NewInt64Coin("uregen", 23999990), + }, + }, + + "exact out: 3 step multi-hop": { + msg: func(state BaseState) *wasmbindings.SwapMsg { + return &wasmbindings.SwapMsg{ + First: wasmbindings.Swap{ + PoolId: state.StarPool, + DenomIn: "ustar", + DenomOut: "uosmo", + }, + Route: []wasmbindings.Step{{ + PoolId: state.AtomPool, + DenomOut: "uatom", + }, { + PoolId: state.RegenPool, + DenomOut: "uregen", + }}, + Amount: wasmbindings.SwapAmountWithLimit{ + ExactOut: &wasmbindings.ExactOut{ + MaxInput: sdk.NewInt(240000000), + Output: sdk.NewInt(24000000), + }, + }, + } + }, + initFunds: sdk.NewInt64Coin("ustar", 240000000), + finalFunds: []sdk.Coin{ + // 240 STAR -> 6 OSMO + // 6 OSMO -> 2 ATOM + // 2 ATOM -> 24 REGEN (with minor rounding) + sdk.NewInt64Coin("uregen", 24000000), + sdk.NewInt64Coin("ustar", 400), + }, + }, + } + + for name, tc := range cases { + tc := tc + t.Run(name, func(t *testing.T) { + creator := RandomAccountAddress() + osmosis, ctx := SetupCustomApp(t, creator) + state := prepareSwapState(t, ctx, osmosis) + + trader := RandomAccountAddress() + fundAccount(t, ctx, osmosis, trader, []sdk.Coin{tc.initFunds}) + reflect := instantiateReflectContract(t, ctx, osmosis, trader) + require.NotEmpty(t, reflect) + + msg := wasmbindings.OsmosisMsg{Swap: tc.msg(state)} + err := executeCustom(t, ctx, osmosis, reflect, trader, msg, tc.initFunds) + if tc.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + balances := osmosis.BankKeeper.GetAllBalances(ctx, reflect) + // uncomment these to debug any confusing results (show balances, not (*big.Int)(0x140005e51e0)) + // fmt.Printf("Expected: %s\n", tc.finalFunds) + // fmt.Printf("Got: %s\n", balances) + require.EqualValues(t, tc.finalFunds, balances) + } + }) + } +} + +// test setup for each run through the table test above +func prepareSwapState(t *testing.T, ctx sdk.Context, osmosis *app.OsmosisApp) BaseState { + actor := RandomAccountAddress() + + var swapperFunds = sdk.NewCoins( + sdk.NewInt64Coin("uatom", 333000000), + sdk.NewInt64Coin("uosmo", 555000000+3*poolFee), + sdk.NewInt64Coin("uregen", 777000000), + sdk.NewInt64Coin("ustar", 999000000), + ) + fundAccount(t, ctx, osmosis, actor, swapperFunds) + + // 20 star to 1 osmo + funds1 := []sdk.Coin{ + sdk.NewInt64Coin("uosmo", 12000000), + sdk.NewInt64Coin("ustar", 240000000), + } + starPool := preparePool(t, ctx, osmosis, actor, funds1) + + // 2 osmo to 1 atom + funds2 := []sdk.Coin{ + sdk.NewInt64Coin("uatom", 6000000), + sdk.NewInt64Coin("uosmo", 12000000), + } + atomPool := preparePool(t, ctx, osmosis, actor, funds2) + + // 16 regen to 1 atom + funds3 := []sdk.Coin{ + sdk.NewInt64Coin("uatom", 6000000), + sdk.NewInt64Coin("uregen", 96000000), + } + regenPool := preparePool(t, ctx, osmosis, actor, funds3) + + return BaseState{ + StarPool: starPool, + AtomPool: atomPool, + RegenPool: regenPool, + } +} + +type ReflectExec struct { + ReflectMsg *ReflectMsgs `json:"reflect_msg,omitempty"` + ReflectSubMsg *ReflectSubMsgs `json:"reflect_sub_msg,omitempty"` +} + +type ReflectMsgs struct { + Msgs []wasmvmtypes.CosmosMsg `json:"msgs"` +} + +type ReflectSubMsgs struct { + Msgs []wasmvmtypes.SubMsg `json:"msgs"` +} + +func executeCustom(t *testing.T, ctx sdk.Context, osmosis *app.OsmosisApp, contract sdk.AccAddress, sender sdk.AccAddress, msg wasmbindings.OsmosisMsg, funds sdk.Coin) error { + customBz, err := json.Marshal(msg) + require.NoError(t, err) + reflectMsg := ReflectExec{ + ReflectMsg: &ReflectMsgs{ + Msgs: []wasmvmtypes.CosmosMsg{{ + Custom: customBz, + }}, + }, + } + reflectBz, err := json.Marshal(reflectMsg) + require.NoError(t, err) + + // no funds sent if amount is 0 + var coins sdk.Coins + if !funds.Amount.IsNil() { + coins = sdk.Coins{funds} + } + + contractKeeper := keeper.NewDefaultPermissionKeeper(osmosis.WasmKeeper) + _, err = contractKeeper.Execute(ctx, contract, sender, reflectBz, coins) + return err +} diff --git a/app/wasm/test/custom_query_test.go b/app/wasm/test/custom_query_test.go new file mode 100644 index 00000000000..7c2698086f2 --- /dev/null +++ b/app/wasm/test/custom_query_test.go @@ -0,0 +1,358 @@ +package wasm + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "strconv" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/CosmWasm/wasmd/x/wasm/keeper" + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + "github.com/cosmos/cosmos-sdk/simapp" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/osmosis-labs/osmosis/v7/app" + "github.com/osmosis-labs/osmosis/v7/app/wasm" + "github.com/osmosis-labs/osmosis/v7/app/wasm/bindings" + "github.com/osmosis-labs/osmosis/v7/x/gamm/pool-models/balancer" +) + +// we must pay this many uosmo for every pool we create +var poolFee int64 = 1000000000 + +var defaultFunds = sdk.NewCoins( + sdk.NewInt64Coin("uatom", 333000000), + sdk.NewInt64Coin("uosmo", 555000000+2*poolFee), + sdk.NewInt64Coin("ustar", 999000000), +) + +func SetupCustomApp(t *testing.T, addr sdk.AccAddress) (*app.OsmosisApp, sdk.Context) { + osmosis, ctx := CreateTestInput() + wasmKeeper := osmosis.WasmKeeper + + storeReflectCode(t, ctx, osmosis, addr) + + cInfo := wasmKeeper.GetCodeInfo(ctx, 1) + require.NotNil(t, cInfo) + + return osmosis, ctx +} + +// func TestQueryFullDenom(t *testing.T) { +// actor := RandomAccountAddress() +// osmosis, ctx := SetupCustomApp(t, actor) + +// reflect := instantiateReflectContract(t, ctx, osmosis, actor) +// require.NotEmpty(t, reflect) + +// // query full denom +// query := wasmbindings.OsmosisQuery{ +// FullDenom: &wasmbindings.FullDenom{ +// Contract: reflect.String(), +// SubDenom: "ustart", +// }, +// } +// resp := wasmbindings.FullDenomResponse{} +// queryCustom(t, ctx, osmosis, reflect, query, &resp) + +// expected := fmt.Sprintf("cw/%s/ustart", reflect.String()) +// require.EqualValues(t, expected, resp.Denom) +// } + +func TestQueryPool(t *testing.T) { + actor := RandomAccountAddress() + osmosis, ctx := SetupCustomApp(t, actor) + + fundAccount(t, ctx, osmosis, actor, defaultFunds) + + poolFunds := []sdk.Coin{ + sdk.NewInt64Coin("uosmo", 12000000), + sdk.NewInt64Coin("ustar", 240000000), + } + // 20 star to 1 osmo + starPool := preparePool(t, ctx, osmosis, actor, poolFunds) + + pool2Funds := []sdk.Coin{ + sdk.NewInt64Coin("uatom", 6000000), + sdk.NewInt64Coin("uosmo", 12000000), + } + // 20 star to 1 osmo + atomPool := preparePool(t, ctx, osmosis, actor, pool2Funds) + + reflect := instantiateReflectContract(t, ctx, osmosis, actor) + require.NotEmpty(t, reflect) + + // query pool state + query := wasmbindings.OsmosisQuery{ + PoolState: &wasmbindings.PoolState{PoolId: starPool}, + } + resp := wasmbindings.PoolStateResponse{} + queryCustom(t, ctx, osmosis, reflect, query, &resp) + expected := wasm.ConvertSdkCoinsToWasmCoins(poolFunds) + require.EqualValues(t, expected, resp.Assets) + assertValidShares(t, resp.Shares, starPool) + + // query second pool state + query = wasmbindings.OsmosisQuery{ + PoolState: &wasmbindings.PoolState{PoolId: atomPool}, + } + resp = wasmbindings.PoolStateResponse{} + queryCustom(t, ctx, osmosis, reflect, query, &resp) + expected = wasm.ConvertSdkCoinsToWasmCoins(pool2Funds) + require.EqualValues(t, expected, resp.Assets) + assertValidShares(t, resp.Shares, atomPool) +} + +func TestQuerySpotPrice(t *testing.T) { + actor := RandomAccountAddress() + osmosis, ctx := SetupCustomApp(t, actor) + swapFee := 0. // FIXME: Set / support an actual fee + epsilon := 1e-6 + + fundAccount(t, ctx, osmosis, actor, defaultFunds) + + poolFunds := []sdk.Coin{ + sdk.NewInt64Coin("uosmo", 12000000), + sdk.NewInt64Coin("ustar", 240000000), + } + // 20 star to 1 osmo + starPool := preparePool(t, ctx, osmosis, actor, poolFunds) + + reflect := instantiateReflectContract(t, ctx, osmosis, actor) + require.NotEmpty(t, reflect) + + // query spot price + query := wasmbindings.OsmosisQuery{ + SpotPrice: &wasmbindings.SpotPrice{ + Swap: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "ustar", + DenomOut: "uosmo", + }, + WithSwapFee: false, + }, + } + resp := wasmbindings.SpotPriceResponse{} + queryCustom(t, ctx, osmosis, reflect, query, &resp) + + price, err := strconv.ParseFloat(resp.Price, 64) + require.NoError(t, err) + + uosmo, err := poolFunds[0].Amount.ToDec().Float64() + require.NoError(t, err) + ustar, err := poolFunds[1].Amount.ToDec().Float64() + require.NoError(t, err) + + expected := ustar / uosmo + require.InEpsilonf(t, expected, price, epsilon, fmt.Sprintf("Outside of tolerance (%f)", epsilon)) + + // and the reverse conversion (with swap fee) + // query spot price + query = wasmbindings.OsmosisQuery{ + SpotPrice: &wasmbindings.SpotPrice{ + Swap: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "uosmo", + DenomOut: "ustar", + }, + WithSwapFee: true, + }, + } + resp = wasmbindings.SpotPriceResponse{} + queryCustom(t, ctx, osmosis, reflect, query, &resp) + + price, err = strconv.ParseFloat(resp.Price, 32) + require.NoError(t, err) + + expected = 1. / expected + require.InEpsilonf(t, expected+swapFee, price, epsilon, fmt.Sprintf("Outside of tolerance (%f)", epsilon)) +} + +func TestQueryEstimateSwap(t *testing.T) { + actor := RandomAccountAddress() + osmosis, ctx := SetupCustomApp(t, actor) + epsilon := 1e-3 + + fundAccount(t, ctx, osmosis, actor, defaultFunds) + + poolFunds := []sdk.Coin{ + sdk.NewInt64Coin("uosmo", 12000000), + sdk.NewInt64Coin("ustar", 240000000), + } + // 20 star to 1 osmo + starPool := preparePool(t, ctx, osmosis, actor, poolFunds) + + reflect := instantiateReflectContract(t, ctx, osmosis, actor) + require.NotEmpty(t, reflect) + + // The contract/sender needs to have funds for estimating the price + fundAccount(t, ctx, osmosis, reflect, defaultFunds) + + // Estimate swap rate + uosmo, err := poolFunds[0].Amount.ToDec().Float64() + require.NoError(t, err) + ustar, err := poolFunds[1].Amount.ToDec().Float64() + require.NoError(t, err) + swapRate := ustar / uosmo + + // Query estimate cost (Exact in. No route) + amountIn := sdk.NewInt(10000) + query := wasmbindings.OsmosisQuery{ + EstimateSwap: &wasmbindings.EstimateSwap{ + Sender: reflect.String(), + First: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "uosmo", + DenomOut: "ustar", + }, + Route: []wasmbindings.Step{}, + Amount: wasmbindings.SwapAmount{ + In: &amountIn, + }, + }, + } + resp := wasmbindings.EstimatePriceResponse{} + queryCustom(t, ctx, osmosis, reflect, query, &resp) + require.NotNil(t, resp.Amount.Out) + require.Nil(t, resp.Amount.In) + cost, err := (*resp.Amount.Out).ToDec().Float64() + require.NoError(t, err) + + amount, err := amountIn.ToDec().Float64() + require.NoError(t, err) + expected := amount * swapRate // out + require.InEpsilonf(t, expected, cost, epsilon, fmt.Sprintf("Outside of tolerance (%f)", epsilon)) + + // And the other way around + // Query estimate cost (Exact out. No route) + amountOut := sdk.NewInt(10000) + query = wasmbindings.OsmosisQuery{ + EstimateSwap: &wasmbindings.EstimateSwap{ + Sender: reflect.String(), + First: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "uosmo", + DenomOut: "ustar", + }, + Route: []wasmbindings.Step{}, + Amount: wasmbindings.SwapAmount{ + Out: &amountOut, + }, + }, + } + resp = wasmbindings.EstimatePriceResponse{} + queryCustom(t, ctx, osmosis, reflect, query, &resp) + require.NotNil(t, resp.Amount.In) + require.Nil(t, resp.Amount.Out) + cost, err = (*resp.Amount.In).ToDec().Float64() + require.NoError(t, err) + + amount, err = amountOut.ToDec().Float64() + require.NoError(t, err) + expected = amount * 1. / swapRate + require.InEpsilonf(t, expected, cost, epsilon, fmt.Sprintf("Outside of tolerance (%f)", epsilon)) +} + +type ReflectQuery struct { + Chain *ChainRequest `json:"chain,omitempty"` +} + +type ChainRequest struct { + Request wasmvmtypes.QueryRequest `json:"request"` +} + +type ChainResponse struct { + Data []byte `json:"data"` +} + +func queryCustom(t *testing.T, ctx sdk.Context, osmosis *app.OsmosisApp, contract sdk.AccAddress, request wasmbindings.OsmosisQuery, response interface{}) { + msgBz, err := json.Marshal(request) + require.NoError(t, err) + + query := ReflectQuery{ + Chain: &ChainRequest{ + Request: wasmvmtypes.QueryRequest{Custom: msgBz}, + }, + } + queryBz, err := json.Marshal(query) + require.NoError(t, err) + + resBz, err := osmosis.WasmKeeper.QuerySmart(ctx, contract, queryBz) + require.NoError(t, err) + var resp ChainResponse + err = json.Unmarshal(resBz, &resp) + require.NoError(t, err) + err = json.Unmarshal(resp.Data, response) + require.NoError(t, err) +} + +func assertValidShares(t *testing.T, shares wasmvmtypes.Coin, poolID uint64) { + // sanity check: check the denom and ensure at least 18 decimal places + denom := fmt.Sprintf("gamm/pool/%d", poolID) + require.Equal(t, denom, shares.Denom) + require.Greater(t, len(shares.Amount), 18) +} + +func storeReflectCode(t *testing.T, ctx sdk.Context, osmosis *app.OsmosisApp, addr sdk.AccAddress) { + govKeeper := osmosis.GovKeeper + wasmCode, err := ioutil.ReadFile("../testdata/osmo_reflect.wasm") + require.NoError(t, err) + + src := wasmtypes.StoreCodeProposalFixture(func(p *wasmtypes.StoreCodeProposal) { + p.RunAs = addr.String() + p.WASMByteCode = wasmCode + }) + + // when stored + storedProposal, err := govKeeper.SubmitProposal(ctx, src) + require.NoError(t, err) + + // and proposal execute + handler := govKeeper.Router().GetRoute(storedProposal.ProposalRoute()) + err = handler(ctx, storedProposal.GetContent()) + require.NoError(t, err) +} + +func instantiateReflectContract(t *testing.T, ctx sdk.Context, osmosis *app.OsmosisApp, funder sdk.AccAddress) sdk.AccAddress { + initMsgBz := []byte("{}") + contractKeeper := keeper.NewDefaultPermissionKeeper(osmosis.WasmKeeper) + codeID := uint64(1) + addr, _, err := contractKeeper.Instantiate(ctx, codeID, funder, funder, initMsgBz, "demo contract", nil) + require.NoError(t, err) + + return addr +} + +func fundAccount(t *testing.T, ctx sdk.Context, osmosis *app.OsmosisApp, addr sdk.AccAddress, coins sdk.Coins) { + err := simapp.FundAccount( + osmosis.BankKeeper, + ctx, + addr, + coins, + ) + require.NoError(t, err) +} + +func preparePool(t *testing.T, ctx sdk.Context, osmosis *app.OsmosisApp, addr sdk.AccAddress, funds []sdk.Coin) uint64 { + var assets []balancer.PoolAsset + for _, coin := range funds { + assets = append(assets, balancer.PoolAsset{ + Weight: sdk.NewInt(100), + Token: coin, + }) + } + + poolParams := balancer.PoolParams{ + SwapFee: sdk.NewDec(0), + ExitFee: sdk.NewDec(0), + } + + msg := balancer.NewMsgCreateBalancerPool(addr, poolParams, assets, "") + poolId, err := osmosis.GAMMKeeper.CreatePool(ctx, &msg) + require.NoError(t, err) + return poolId +} diff --git a/app/wasmtest/helpers_test.go b/app/wasm/test/helpers_test.go similarity index 98% rename from app/wasmtest/helpers_test.go rename to app/wasm/test/helpers_test.go index ee4eb1327b6..aa0f2e25a15 100644 --- a/app/wasmtest/helpers_test.go +++ b/app/wasm/test/helpers_test.go @@ -1,4 +1,4 @@ -package wasmtest +package wasm import ( "testing" diff --git a/app/wasm/test/messages_test.go b/app/wasm/test/messages_test.go new file mode 100644 index 00000000000..8d1b45b8d73 --- /dev/null +++ b/app/wasm/test/messages_test.go @@ -0,0 +1,521 @@ +package wasm + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/osmosis-labs/osmosis/v7/app/wasm" + wasmbindings "github.com/osmosis-labs/osmosis/v7/app/wasm/bindings" + "github.com/stretchr/testify/assert" + "math" + "testing" + + "github.com/stretchr/testify/require" +) + +// func TestMint(t *testing.T) { +// actor := RandomAccountAddress() +// osmosis, ctx := SetupCustomApp(t, actor) + +// lucky := RandomAccountAddress() + +// // lucky was broke +// balances := osmosis.BankKeeper.GetAllBalances(ctx, lucky) +// require.Empty(t, balances) + +// amount, ok := sdk.NewIntFromString("8080") +// require.True(t, ok) + +// specs := map[string]struct { +// mint *wasmbindings.MintTokens +// expErr bool +// }{ +// "valid mint": { +// mint: &wasmbindings.MintTokens{ +// SubDenom: "MOON", +// Amount: amount, +// Recipient: lucky.String(), +// }, +// }, +// "empty sub-denom": { +// mint: &wasmbindings.MintTokens{ +// SubDenom: "", +// Amount: amount, +// Recipient: lucky.String(), +// }, +// expErr: true, +// }, +// "invalid sub-denom": { +// mint: &wasmbindings.MintTokens{ +// SubDenom: "sub-denom_2", +// Amount: amount, +// Recipient: lucky.String(), +// }, +// expErr: true, +// }, +// "zero amount": { +// mint: &wasmbindings.MintTokens{ +// SubDenom: "MOON", +// Amount: sdk.ZeroInt(), +// Recipient: lucky.String(), +// }, +// expErr: true, +// }, +// "negative amount": { +// mint: &wasmbindings.MintTokens{ +// SubDenom: "MOON", +// Amount: amount.Neg(), +// Recipient: lucky.String(), +// }, +// expErr: true, +// }, +// "empty recipient": { +// mint: &wasmbindings.MintTokens{ +// SubDenom: "MOON", +// Amount: amount, +// Recipient: "", +// }, +// expErr: true, +// }, +// "invalid recipient": { +// mint: &wasmbindings.MintTokens{ +// SubDenom: "MOON", +// Amount: amount, +// Recipient: "invalid", +// }, +// expErr: true, +// }, +// "null mint": { +// mint: nil, +// expErr: true, +// }, +// } +// for name, spec := range specs { +// t.Run(name, func(t *testing.T) { +// // when +// gotErr := wasm.PerformMint(osmosis.BankKeeper, ctx, actor, spec.mint) +// // then +// if spec.expErr { +// require.Error(t, gotErr) +// return +// } +// require.NoError(t, gotErr) +// }) +// } + +// } + +func TestSwap(t *testing.T) { + actor := RandomAccountAddress() + osmosis, ctx := SetupCustomApp(t, actor) + epsilon := 1e-3 + + fundAccount(t, ctx, osmosis, actor, defaultFunds) + + poolFunds := []sdk.Coin{ + sdk.NewInt64Coin("uosmo", 12_000_000), + sdk.NewInt64Coin("ustar", 240_000_000), + } + // 20 star to 1 osmo + starPool := preparePool(t, ctx, osmosis, actor, poolFunds) + + // Estimate swap rate + uosmo := poolFunds[0].Amount.ToDec().MustFloat64() + ustar := poolFunds[1].Amount.ToDec().MustFloat64() + swapRate := ustar / uosmo + + amountIn := wasmbindings.ExactIn{ + Input: sdk.NewInt(10000), + MinOutput: sdk.OneInt(), + } + zeroAmountIn := amountIn + zeroAmountIn.Input = sdk.ZeroInt() + negativeAmountIn := amountIn + negativeAmountIn.Input = negativeAmountIn.Input.Neg() + + amountOut := wasmbindings.ExactOut{ + MaxInput: sdk.NewInt(math.MaxInt64), + Output: sdk.NewInt(10000), + } + zeroAmountOut := amountOut + zeroAmountOut.Output = sdk.ZeroInt() + negativeAmountOut := amountOut + negativeAmountOut.Output = negativeAmountOut.Output.Neg() + + amount := amountIn.Input.ToDec().MustFloat64() + starAmount := sdk.NewInt(int64(amount * swapRate)) + + starSwapAmount := wasmbindings.SwapAmount{Out: &starAmount} + + specs := map[string]struct { + swap *wasmbindings.SwapMsg + expCost *wasmbindings.SwapAmount + expErr bool + }{ + "valid swap (exact in)": { + swap: &wasmbindings.SwapMsg{ + First: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "uosmo", + DenomOut: "ustar", + }, + Route: nil, + Amount: wasmbindings.SwapAmountWithLimit{ + ExactIn: &amountIn, + }, + }, + expCost: &starSwapAmount, + }, + "non-existent pool id": { + swap: &wasmbindings.SwapMsg{ + First: wasmbindings.Swap{ + PoolId: starPool + 4, + DenomIn: "uosmo", + DenomOut: "ustar", + }, + Route: nil, + Amount: wasmbindings.SwapAmountWithLimit{ + ExactIn: &amountIn, + }, + }, + expErr: true, + }, + "zero pool id": { + swap: &wasmbindings.SwapMsg{ + First: wasmbindings.Swap{ + PoolId: 0, + DenomIn: "uosmo", + DenomOut: "ustar", + }, + Route: nil, + Amount: wasmbindings.SwapAmountWithLimit{ + ExactIn: &amountIn, + }, + }, + expErr: true, + }, + "invalid denom in": { + swap: &wasmbindings.SwapMsg{ + First: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "invalid", + DenomOut: "ustar", + }, + Route: nil, + Amount: wasmbindings.SwapAmountWithLimit{ + ExactIn: &amountIn, + }, + }, + expErr: true, + }, + "empty denom in": { + swap: &wasmbindings.SwapMsg{ + First: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "", + DenomOut: "ustar", + }, + Route: nil, + Amount: wasmbindings.SwapAmountWithLimit{ + ExactIn: &amountIn, + }, + }, + expErr: true, + }, + "invalid denom out": { + swap: &wasmbindings.SwapMsg{ + First: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "ustar", + DenomOut: "invalid", + }, + Route: nil, + Amount: wasmbindings.SwapAmountWithLimit{ + ExactIn: &amountIn, + }, + }, + expErr: true, + }, + "empty denom out": { + swap: &wasmbindings.SwapMsg{ + First: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "ustar", + DenomOut: "", + }, + Route: nil, + Amount: wasmbindings.SwapAmountWithLimit{ + ExactIn: &amountIn, + }, + }, + expErr: true, + }, + "null swap": { + swap: nil, + expErr: true, + }, + "empty swap amount": { + swap: &wasmbindings.SwapMsg{ + First: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "ustar", + DenomOut: "", + }, + Route: nil, + Amount: wasmbindings.SwapAmountWithLimit{}, + }, + expErr: true, + }, + "zero amount in": { + swap: &wasmbindings.SwapMsg{ + First: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "uosmo", + DenomOut: "ustar", + }, + Route: nil, + Amount: wasmbindings.SwapAmountWithLimit{ + ExactIn: &zeroAmountIn, + }, + }, + expErr: true, + }, + "zero amount out": { + swap: &wasmbindings.SwapMsg{ + First: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "uosmo", + DenomOut: "ustar", + }, + Route: nil, + Amount: wasmbindings.SwapAmountWithLimit{ + ExactOut: &zeroAmountOut, + }, + }, + expErr: true, + }, + "negative amount in": { + swap: &wasmbindings.SwapMsg{ + First: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "uosmo", + DenomOut: "ustar", + }, + Route: nil, + Amount: wasmbindings.SwapAmountWithLimit{ + ExactIn: &negativeAmountIn, + }, + }, + expErr: true, + }, + "negative amount out": { + swap: &wasmbindings.SwapMsg{ + First: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "uosmo", + DenomOut: "ustar", + }, + Route: nil, + Amount: wasmbindings.SwapAmountWithLimit{ + ExactOut: &negativeAmountOut, + }, + }, + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + // when + gotAmount, gotErr := wasm.PerformSwap(osmosis.GAMMKeeper, ctx, actor, spec.swap) + // then + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + assert.InEpsilonf(t, (*spec.expCost.Out).ToDec().MustFloat64(), (*gotAmount.Out).ToDec().MustFloat64(), epsilon, "exp %s but got %s", spec.expCost.Out.String(), gotAmount.Out.String()) + }) + } + +} + +func TestSwapMultiHop(t *testing.T) { + actor := RandomAccountAddress() + osmosis, ctx := SetupCustomApp(t, actor) + epsilon := 1e-3 + + fundAccount(t, ctx, osmosis, actor, defaultFunds) + + poolFunds := []sdk.Coin{ + sdk.NewInt64Coin("uosmo", 12_000_000), + sdk.NewInt64Coin("ustar", 240_000_000), + } + // 20 star to 1 osmo + starPool := preparePool(t, ctx, osmosis, actor, poolFunds) + + // 2 osmo to 1 atom + poolFunds2 := []sdk.Coin{ + sdk.NewInt64Coin("uatom", 6_000_000), + sdk.NewInt64Coin("uosmo", 12_000_000), + } + atomPool := preparePool(t, ctx, osmosis, actor, poolFunds2) + + amountIn := wasmbindings.ExactIn{ + Input: sdk.NewInt(1_000_000), + MinOutput: sdk.NewInt(20_000), + } + + // Multi-hop + // Estimate 1st swap rate + uosmo := poolFunds[0].Amount.ToDec().MustFloat64() + ustar := poolFunds[1].Amount.ToDec().MustFloat64() + expectedOut1 := uosmo - uosmo*ustar/(ustar+amountIn.Input.ToDec().MustFloat64()) + + // Estimate 2nd swap rate + uatom2 := poolFunds2[0].Amount.ToDec().MustFloat64() + uosmo2 := poolFunds2[1].Amount.ToDec().MustFloat64() + expectedOut2 := uatom2 - uosmo2*uatom2/(uosmo2+expectedOut1) + + atomAmount := sdk.NewInt(int64(expectedOut2)) + atomSwapAmount := wasmbindings.SwapAmount{Out: &atomAmount} + + specs := map[string]struct { + swap *wasmbindings.SwapMsg + expCost *wasmbindings.SwapAmount + expErr bool + }{ + "valid swap (exact in, 2 step multi-hop)": { + swap: &wasmbindings.SwapMsg{ + First: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "ustar", + DenomOut: "uosmo", + }, + Route: []wasmbindings.Step{{ + PoolId: atomPool, + DenomOut: "uatom", + }}, + Amount: wasmbindings.SwapAmountWithLimit{ + ExactIn: &amountIn, + }, + }, + expCost: &atomSwapAmount, + }, + "non-existent step pool id": { + swap: &wasmbindings.SwapMsg{ + First: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "ustar", + DenomOut: "uosmo", + }, + Route: []wasmbindings.Step{{ + PoolId: atomPool + 2, + DenomOut: "uatom", + }}, + Amount: wasmbindings.SwapAmountWithLimit{ + ExactIn: &amountIn, + }, + }, + expErr: true, + }, + "zero step pool id": { + swap: &wasmbindings.SwapMsg{ + First: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "ustar", + DenomOut: "uosmo", + }, + Route: []wasmbindings.Step{{ + PoolId: 0, + DenomOut: "uatom", + }}, + Amount: wasmbindings.SwapAmountWithLimit{ + ExactIn: &amountIn, + }, + }, + expErr: true, + }, + "wrong step denom out": { + swap: &wasmbindings.SwapMsg{ + First: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "ustar", + DenomOut: "uosmo", + }, + Route: []wasmbindings.Step{{ + PoolId: atomPool, + DenomOut: "ATOM", + }}, + Amount: wasmbindings.SwapAmountWithLimit{ + ExactIn: &amountIn, + }, + }, + expErr: true, + }, + "self-swap not allowed": { + swap: &wasmbindings.SwapMsg{ + First: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "ustar", + DenomOut: "uosmo", + }, + Route: []wasmbindings.Step{{ + PoolId: atomPool, + DenomOut: "uosmo", // this is same as the input (output of first swap) + }}, + Amount: wasmbindings.SwapAmountWithLimit{ + ExactIn: &amountIn, + }, + }, + expErr: true, + }, + "invalid step denom out": { + swap: &wasmbindings.SwapMsg{ + First: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "ustar", + DenomOut: "uosmo", + }, + Route: []wasmbindings.Step{{ + PoolId: atomPool, + DenomOut: "invalid", + }}, + Amount: wasmbindings.SwapAmountWithLimit{ + ExactIn: &amountIn, + }, + }, + expErr: true, + }, + "empty step denom out": { + swap: &wasmbindings.SwapMsg{ + First: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "ustar", + DenomOut: "uosmo", + }, + Route: []wasmbindings.Step{{ + PoolId: atomPool, + DenomOut: "", + }}, + Amount: wasmbindings.SwapAmountWithLimit{ + ExactIn: &amountIn, + }, + }, + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + // use scratch context to avoid interference between tests + subCtx, _ := ctx.CacheContext() + // when + gotAmount, gotErr := wasm.PerformSwap(osmosis.GAMMKeeper, subCtx, actor, spec.swap) + // then + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + assert.InEpsilonf(t, (*spec.expCost.Out).ToDec().MustFloat64(), (*gotAmount.Out).ToDec().MustFloat64(), epsilon, "exp %s but got %s", spec.expCost.Out.String(), gotAmount.Out.String()) + }) + } + +} diff --git a/app/wasm/test/queries_test.go b/app/wasm/test/queries_test.go new file mode 100644 index 00000000000..88855d1888a --- /dev/null +++ b/app/wasm/test/queries_test.go @@ -0,0 +1,490 @@ +package wasm + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/osmosis-labs/osmosis/v7/app/wasm" + wasmbindings "github.com/osmosis-labs/osmosis/v7/app/wasm/bindings" +) + +// func TestFullDenom(t *testing.T) { +// actor := RandomAccountAddress() + +// specs := map[string]struct { +// addr string +// subDenom string +// expFullDenom string +// expErr bool +// }{ +// "valid address": { +// addr: actor.String(), +// subDenom: "subDenom1", +// expFullDenom: fmt.Sprintf("cw/%s/subDenom1", actor.String()), +// }, +// "empty address": { +// addr: "", +// subDenom: "subDenom1", +// expErr: true, +// }, +// "invalid address": { +// addr: "invalid", +// subDenom: "subDenom1", +// expErr: true, +// }, +// "empty sub-denom": { +// addr: actor.String(), +// subDenom: "", +// expErr: true, +// }, +// "invalid sub-denom": { +// addr: actor.String(), +// subDenom: "sub_denom_1", +// expErr: true, +// }, +// } +// for name, spec := range specs { +// t.Run(name, func(t *testing.T) { +// // when +// gotFullDenom, gotErr := wasm.GetFullDenom(spec.addr, spec.subDenom) +// // then +// if spec.expErr { +// require.Error(t, gotErr) +// return +// } +// require.NoError(t, gotErr) +// assert.Equal(t, spec.expFullDenom, gotFullDenom, "exp %s but got %s", spec.expFullDenom, gotFullDenom) +// }) +// } +// } + +func TestPoolState(t *testing.T) { + actor := RandomAccountAddress() + osmosis, ctx := SetupCustomApp(t, actor) + + fundAccount(t, ctx, osmosis, actor, defaultFunds) + + poolFunds := []sdk.Coin{ + sdk.NewInt64Coin("uosmo", 12000000), + sdk.NewInt64Coin("ustar", 240000000), + } + // 20 star to 1 osmo + starPool := preparePool(t, ctx, osmosis, actor, poolFunds) + + // FIXME: Derive / obtain these values + starSharesDenom := fmt.Sprintf("gamm/pool/%d", starPool) + starSharedAmount, _ := sdk.NewIntFromString("100_000_000_000_000_000_000") + + queryPlugin := wasm.NewQueryPlugin(osmosis.GAMMKeeper) + + specs := map[string]struct { + poolId uint64 + expPoolState *wasmbindings.PoolAssets + expErr bool + }{ + "existent pool id": { + poolId: starPool, + expPoolState: &wasmbindings.PoolAssets{ + Assets: poolFunds, + Shares: sdk.NewCoin(starSharesDenom, starSharedAmount), + }, + }, + "non-existent pool id": { + poolId: starPool + 1, + expErr: true, + }, + "zero pool id": { + poolId: 0, + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + // when + gotPoolState, gotErr := queryPlugin.GetPoolState(ctx, spec.poolId) + // then + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + assert.Equal(t, spec.expPoolState, gotPoolState, "exp %s but got %s", spec.expPoolState, gotPoolState) + }) + } +} + +func TestSpotPrice(t *testing.T) { + actor := RandomAccountAddress() + swapFee := 0. // FIXME: Set / support an actual fee + epsilon := 1e-6 + osmosis, ctx := SetupCustomApp(t, actor) + + fundAccount(t, ctx, osmosis, actor, defaultFunds) + + poolFunds := []sdk.Coin{ + sdk.NewInt64Coin("uosmo", 12000000), + sdk.NewInt64Coin("ustar", 240000000), + } + // 20 star to 1 osmo + starPool := preparePool(t, ctx, osmosis, actor, poolFunds) + + uosmo := poolFunds[0].Amount.ToDec().MustFloat64() + ustar := poolFunds[1].Amount.ToDec().MustFloat64() + + starPrice := sdk.MustNewDecFromStr(fmt.Sprintf("%f", uosmo/ustar)) + starFee := sdk.MustNewDecFromStr(fmt.Sprintf("%f", swapFee)) + starPriceWithFee := starPrice.Add(starFee) + + queryPlugin := wasm.NewQueryPlugin(osmosis.GAMMKeeper) + + specs := map[string]struct { + spotPrice *wasmbindings.SpotPrice + expPrice *sdk.Dec + expErr bool + }{ + "valid spot price": { + spotPrice: &wasmbindings.SpotPrice{ + Swap: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "uosmo", + DenomOut: "ustar", + }, + WithSwapFee: false, + }, + expPrice: &starPrice, + }, + "valid spot price with fee": { + spotPrice: &wasmbindings.SpotPrice{ + Swap: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "uosmo", + DenomOut: "ustar", + }, + WithSwapFee: true, + }, + expPrice: &starPriceWithFee, + }, + "non-existent pool id": { + spotPrice: &wasmbindings.SpotPrice{ + Swap: wasmbindings.Swap{ + PoolId: starPool + 2, + DenomIn: "uosmo", + DenomOut: "ustar", + }, + WithSwapFee: false, + }, + expErr: true, + }, + "zero pool id": { + spotPrice: &wasmbindings.SpotPrice{ + Swap: wasmbindings.Swap{ + PoolId: 0, + DenomIn: "uosmo", + DenomOut: "ustar", + }, + WithSwapFee: false, + }, + expErr: true, + }, + "invalid denom in": { + spotPrice: &wasmbindings.SpotPrice{ + Swap: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "invalid", + DenomOut: "ustar", + }, + WithSwapFee: false, + }, + expErr: true, + }, + "empty denom in": { + spotPrice: &wasmbindings.SpotPrice{ + Swap: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "", + DenomOut: "ustar", + }, + WithSwapFee: false, + }, + expErr: true, + }, + "invalid denom out": { + spotPrice: &wasmbindings.SpotPrice{ + Swap: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "uosmo", + DenomOut: "invalid", + }, + WithSwapFee: false, + }, + expErr: true, + }, + "empty denom out": { + spotPrice: &wasmbindings.SpotPrice{ + Swap: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "uosmo", + DenomOut: "", + }, + WithSwapFee: false, + }, + expErr: true, + }, + "null spot price": { + spotPrice: nil, + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + // when + gotPrice, gotErr := queryPlugin.GetSpotPrice(ctx, spec.spotPrice) + // then + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + assert.InEpsilonf(t, spec.expPrice.MustFloat64(), gotPrice.MustFloat64(), epsilon, "exp %s but got %s", spec.expPrice.String(), gotPrice.String()) + }) + } +} + +func TestEstimateSwap(t *testing.T) { + actor := RandomAccountAddress() + osmosis, ctx := SetupCustomApp(t, actor) + epsilon := 1e-3 + + fundAccount(t, ctx, osmosis, actor, defaultFunds) + + poolFunds := []sdk.Coin{ + sdk.NewInt64Coin("uosmo", 12000000), + sdk.NewInt64Coin("ustar", 240000000), + } + // 20 star to 1 osmo + starPool := preparePool(t, ctx, osmosis, actor, poolFunds) + + // Estimate swap rate + uosmo := poolFunds[0].Amount.ToDec().MustFloat64() + ustar := poolFunds[1].Amount.ToDec().MustFloat64() + swapRate := ustar / uosmo + + amountIn := sdk.NewInt(10000) + zeroAmount := sdk.ZeroInt() + negativeAmount := amountIn.Neg() + + amount := amountIn.ToDec().MustFloat64() + starAmount := sdk.NewInt(int64(amount * swapRate)) + + starSwapAmount := wasmbindings.SwapAmount{Out: &starAmount} + + queryPlugin := wasm.NewQueryPlugin(osmosis.GAMMKeeper) + + specs := map[string]struct { + estimateSwap *wasmbindings.EstimateSwap + expCost *wasmbindings.SwapAmount + expErr bool + }{ + "valid estimate swap (exact in)": { + estimateSwap: &wasmbindings.EstimateSwap{ + Sender: actor.String(), + First: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "uosmo", + DenomOut: "ustar", + }, + Route: nil, + Amount: wasmbindings.SwapAmount{ + In: &amountIn, + }, + }, + expCost: &starSwapAmount, + }, + "non-existent pool id": { + estimateSwap: &wasmbindings.EstimateSwap{ + Sender: actor.String(), + First: wasmbindings.Swap{ + PoolId: starPool + 3, + DenomIn: "uosmo", + DenomOut: "ustar", + }, + Route: nil, + Amount: wasmbindings.SwapAmount{ + In: &amountIn, + }, + }, + expErr: true, + }, + "zero pool id": { + estimateSwap: &wasmbindings.EstimateSwap{ + Sender: actor.String(), + First: wasmbindings.Swap{ + PoolId: 0, + DenomIn: "uosmo", + DenomOut: "ustar", + }, + Route: nil, + Amount: wasmbindings.SwapAmount{ + In: &amountIn, + }, + }, + expErr: true, + }, + "invalid denom in": { + estimateSwap: &wasmbindings.EstimateSwap{ + Sender: actor.String(), + First: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "invalid", + DenomOut: "ustar", + }, + Route: nil, + Amount: wasmbindings.SwapAmount{ + In: &amountIn, + }, + }, + expErr: true, + }, + "empty denom in": { + estimateSwap: &wasmbindings.EstimateSwap{ + Sender: actor.String(), + First: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "", + DenomOut: "ustar", + }, + Route: nil, + Amount: wasmbindings.SwapAmount{ + In: &amountIn, + }, + }, + expErr: true, + }, + "invalid denom out": { + estimateSwap: &wasmbindings.EstimateSwap{ + Sender: actor.String(), + First: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "ustar", + DenomOut: "invalid", + }, + Route: nil, + Amount: wasmbindings.SwapAmount{ + In: &amountIn, + }, + }, + expErr: true, + }, + "empty denom out": { + estimateSwap: &wasmbindings.EstimateSwap{ + Sender: actor.String(), + First: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "ustar", + DenomOut: "", + }, + Route: nil, + Amount: wasmbindings.SwapAmount{ + In: &amountIn, + }, + }, + expErr: true, + }, + "null estimate swap": { + estimateSwap: nil, + expErr: true, + }, + "empty swap amount": { + estimateSwap: &wasmbindings.EstimateSwap{ + Sender: actor.String(), + First: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "uosmo", + DenomOut: "ustar", + }, + Route: nil, + Amount: wasmbindings.SwapAmount{}, + }, + expErr: true, + }, + "zero amount in": { + estimateSwap: &wasmbindings.EstimateSwap{ + Sender: actor.String(), + First: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "uosmo", + DenomOut: "ustar", + }, + Route: nil, + Amount: wasmbindings.SwapAmount{ + In: &zeroAmount, + }, + }, + expErr: true, + }, + "zero amount out": { + estimateSwap: &wasmbindings.EstimateSwap{ + Sender: actor.String(), + First: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "uosmo", + DenomOut: "ustar", + }, + Route: nil, + Amount: wasmbindings.SwapAmount{ + Out: &zeroAmount, + }, + }, + expErr: true, + }, + "negative amount in": { + estimateSwap: &wasmbindings.EstimateSwap{ + Sender: actor.String(), + First: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "uosmo", + DenomOut: "ustar", + }, + Route: nil, + Amount: wasmbindings.SwapAmount{ + In: &negativeAmount, + }, + }, + expErr: true, + }, + "negative amount out": { + estimateSwap: &wasmbindings.EstimateSwap{ + Sender: actor.String(), + First: wasmbindings.Swap{ + PoolId: starPool, + DenomIn: "uosmo", + DenomOut: "ustar", + }, + Route: nil, + Amount: wasmbindings.SwapAmount{ + Out: &negativeAmount, + }, + }, + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + // when + gotCost, gotErr := queryPlugin.EstimateSwap(ctx, spec.estimateSwap) + // then + if spec.expErr { + require.Error(t, gotErr) + return + } + require.NoError(t, gotErr) + assert.InEpsilonf(t, (*spec.expCost.Out).ToDec().MustFloat64(), (*gotCost.Out).ToDec().MustFloat64(), epsilon, "exp %s but got %s", spec.expCost.Out.String(), gotCost.Out.String()) + }) + } + +} diff --git a/app/wasmtest/store_run_test.go b/app/wasm/test/store_run_test.go similarity index 93% rename from app/wasmtest/store_run_test.go rename to app/wasm/test/store_run_test.go index 2dec41e456d..8b24bc149c8 100644 --- a/app/wasmtest/store_run_test.go +++ b/app/wasm/test/store_run_test.go @@ -1,4 +1,4 @@ -package wasmtest +package wasm import ( "encoding/json" @@ -26,7 +26,7 @@ func TestNoStorageWithoutProposal(t *testing.T) { _, _, creator := keyPubAddr() // upload reflect code - wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + wasmCode, err := ioutil.ReadFile("../testdata/hackatom.wasm") require.NoError(t, err) _, err = contractKeeper.Create(ctx, creator, wasmCode, nil) require.Error(t, err) @@ -34,7 +34,7 @@ func TestNoStorageWithoutProposal(t *testing.T) { func storeCodeViaProposal(t *testing.T, ctx sdk.Context, osmosis *app.OsmosisApp, addr sdk.AccAddress) { govKeeper := osmosis.GovKeeper - wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + wasmCode, err := ioutil.ReadFile("../testdata/hackatom.wasm") require.NoError(t, err) src := types.StoreCodeProposalFixture(func(p *types.StoreCodeProposal) { @@ -68,7 +68,7 @@ func TestStoreCodeProposal(t *testing.T) { storedCode, err := wasmKeeper.GetByteCode(ctx, 1) require.NoError(t, err) - wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + wasmCode, err := ioutil.ReadFile("../testdata/hackatom.wasm") require.NoError(t, err) assert.Equal(t, wasmCode, storedCode) } diff --git a/app/wasmtest/testdata/download_releases.sh b/app/wasm/testdata/download_releases.sh similarity index 71% rename from app/wasmtest/testdata/download_releases.sh rename to app/wasm/testdata/download_releases.sh index 6f162e85daf..34058c0c94d 100755 --- a/app/wasmtest/testdata/download_releases.sh +++ b/app/wasm/testdata/download_releases.sh @@ -7,7 +7,7 @@ if [ $# -ne 1 ]; then exit 1 fi -tag="$1" +tag="v1.0.0-beta6" for contract in hackatom reflect; do url="https://github.com/CosmWasm/cosmwasm/releases/download/$tag/${contract}.wasm" @@ -15,5 +15,10 @@ for contract in hackatom reflect; do wget -O "${contract}.wasm" "$url" done +tag="$1" +url="https://github.com/confio/osmosis-bindings/releases/download/$tag/osmo_reflect.wasm" +echo "Downloading $url ..." +wget -O "osmo_reflect.wasm" "$url" + rm -f version.txt echo "$tag" >version.txt \ No newline at end of file diff --git a/app/wasm/testdata/hackatom.wasm b/app/wasm/testdata/hackatom.wasm new file mode 100644 index 00000000000..47b19296eb9 Binary files /dev/null and b/app/wasm/testdata/hackatom.wasm differ diff --git a/app/wasm/testdata/osmo_reflect.wasm b/app/wasm/testdata/osmo_reflect.wasm new file mode 100644 index 00000000000..8e91ac27bd8 Binary files /dev/null and b/app/wasm/testdata/osmo_reflect.wasm differ diff --git a/app/wasm/testdata/reflect.wasm b/app/wasm/testdata/reflect.wasm new file mode 100644 index 00000000000..dadaadf7cc3 Binary files /dev/null and b/app/wasm/testdata/reflect.wasm differ diff --git a/app/wasm/testdata/version.txt b/app/wasm/testdata/version.txt new file mode 100644 index 00000000000..b043aa648f5 --- /dev/null +++ b/app/wasm/testdata/version.txt @@ -0,0 +1 @@ +v0.5.0 diff --git a/app/wasm/wasm.go b/app/wasm/wasm.go new file mode 100644 index 00000000000..79ceca66679 --- /dev/null +++ b/app/wasm/wasm.go @@ -0,0 +1,29 @@ +package wasm + +import ( + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + + "github.com/CosmWasm/wasmd/x/wasm" + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + + gammkeeper "github.com/osmosis-labs/osmosis/v7/x/gamm/keeper" +) + +func RegisterCustomPlugins( + gammKeeper *gammkeeper.Keeper, + bank *bankkeeper.BaseKeeper, +) []wasmkeeper.Option { + wasmQueryPlugin := NewQueryPlugin(gammKeeper) + + queryPluginOpt := wasmkeeper.WithQueryPlugins(&wasmkeeper.QueryPlugins{ + Custom: CustomQuerier(wasmQueryPlugin), + }) + messengerDecoratorOpt := wasmkeeper.WithMessageHandlerDecorator( + CustomMessageDecorator(gammKeeper, bank), + ) + + return []wasm.Option{ + queryPluginOpt, + messengerDecoratorOpt, + } +} diff --git a/app/wasmtest/README.md b/app/wasmtest/README.md deleted file mode 100644 index 23dc5277142..00000000000 --- a/app/wasmtest/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Wasm Tests - -This contains a few high level test that `x/wasm` is properly integrated. - -Since the code tested is not in this repo, and we are just testing the application -integration (app.go), I figured this is the most suitable location for it \ No newline at end of file diff --git a/app/wasmtest/testdata/hackatom.wasm b/app/wasmtest/testdata/hackatom.wasm deleted file mode 100644 index c52dd71df29..00000000000 Binary files a/app/wasmtest/testdata/hackatom.wasm and /dev/null differ diff --git a/app/wasmtest/testdata/reflect.wasm b/app/wasmtest/testdata/reflect.wasm deleted file mode 100644 index 5b8da45dc5b..00000000000 Binary files a/app/wasmtest/testdata/reflect.wasm and /dev/null differ diff --git a/app/wasmtest/testdata/version.txt b/app/wasmtest/testdata/version.txt deleted file mode 100644 index 4798387e104..00000000000 --- a/app/wasmtest/testdata/version.txt +++ /dev/null @@ -1 +0,0 @@ -v1.0.0-beta4