Skip to content

Commit

Permalink
Mint token message (#8)
Browse files Browse the repository at this point in the history
* Convert messenge encoder to message decorator

* Implement and Test mint token message

* Add TODOs for future refactoring

* Update app/wasm/message_plugin.go

Co-authored-by: Mauro Lacy <[email protected]>

Co-authored-by: Mauro Lacy <[email protected]>
  • Loading branch information
ethanfrey and maurolacy authored Mar 20, 2022
1 parent e1ea3a6 commit 00b5070
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 80 deletions.
3 changes: 1 addition & 2 deletions app/keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -371,8 +371,7 @@ func (app *OsmosisApp) InitNormalKeepers(
// if we want to allow any custom callbacks
supportedFeatures := "iterator,staking,stargate,osmosis"

owasmQueryPlugin := owasm.NewQueryPlugin(app.GAMMKeeper)
wasmOpts = append(owasm.RegisterCustomPlugins(owasmQueryPlugin), wasmOpts...)
wasmOpts = append(owasm.RegisterCustomPlugins(app.GAMMKeeper, app.BankKeeper), wasmOpts...)

wasmKeeper := wasm.NewKeeper(
appCodec,
Expand Down
8 changes: 5 additions & 3 deletions app/wasm/bindings/msg.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package wasmbindings

import sdk "github.com/cosmos/cosmos-sdk/types"

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
Expand All @@ -11,9 +13,9 @@ type OsmosisMsg struct {

type MintTokens struct {
/// Must be 2-32 alphanumeric characters
SubDenom string `json:"sub_denom"`
Amount string `json:"amount"`
Recipient string `json:"recipient"`
SubDenom string `json:"sub_denom"`
Amount sdk.Int `json:"amount"`
Recipient string `json:"recipient"`
}

type SwapMsg struct {
Expand Down
147 changes: 117 additions & 30 deletions app/wasm/message_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,65 +2,152 @@ package wasm

import (
"encoding/json"
"fmt"

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"
wasm "github.com/osmosis-labs/osmosis/v7/app/wasm/bindings"
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 CustomEncoder(osmoKeeper *QueryPlugin) func(sender sdk.AccAddress, msg json.RawMessage) ([]sdk.Msg, error) {
return func(sender sdk.AccAddress, msg json.RawMessage) ([]sdk.Msg, error) {
var contractMsg wasm.OsmosisMsg
if err := json.Unmarshal(msg, &contractMsg); err != nil {
return nil, sdkerrors.Wrap(err, "osmosis msg")
func CustomMessageDecorator(gammKeeper *gammkeeper.Keeper, bank *bankkeeper.BaseKeeper) func(wasmkeeper.Messenger) wasmkeeper.Messenger {
return func(old wasmkeeper.Messenger) wasmkeeper.Messenger {
return &MintTokenMessenger{
wrapped: old,
bank: bank,
gammKeeper: gammKeeper,
}
}
}

type MintTokenMessenger struct {
wrapped wasmkeeper.Messenger
bank *bankkeeper.BaseKeeper
gammKeeper *gammkeeper.Keeper
}

var _ wasmkeeper.Messenger = (*MintTokenMessenger)(nil)

func (m *MintTokenMessenger) 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 nil, wasmvmtypes.UnsupportedRequest{Kind: "not implemented: mint tokens"}
return m.mintTokens(ctx, contractAddr, contractMsg.MintTokens)
}
if contractMsg.Swap != nil {
return buildSwapMsg(sender, contractMsg.Swap)
return m.swapTokens(ctx, contractAddr, contractMsg.Swap)
}
return nil, wasmvmtypes.UnsupportedRequest{Kind: "unknown osmosis message variant"}
}
return m.wrapped.DispatchMsg(ctx, contractAddr, contractIBCPortID, msg)
}

func buildSwapMsg(sender sdk.AccAddress, swap *wasm.SwapMsg) ([]sdk.Msg, error) {
func (m *MintTokenMessenger) mintTokens(ctx sdk.Context, contractAddr sdk.AccAddress, mint *wasmbindings.MintTokens) ([]sdk.Event, [][]byte, error) {
rcpt, err := parseAddress(mint.Recipient)
if err != nil {
return nil, nil, err
}

denom, err := GetFullDenom(contractAddr.String(), mint.SubDenom)
if err != nil {
return nil, nil, sdkerrors.Wrap(err, "mint token denom")
}
coins := []sdk.Coin{sdk.NewCoin(denom, mint.Amount)}

err = m.bank.MintCoins(ctx, gammtypes.ModuleName, coins)
if err != nil {
return nil, nil, sdkerrors.Wrap(err, "minting coins from message")
}
err = m.bank.SendCoinsFromModuleToAccount(ctx, gammtypes.ModuleName, rcpt, coins)
if err != nil {
return nil, nil, sdkerrors.Wrap(err, "sending newly minted coins from message")
}

return nil, nil, nil
}

// TODO: this is very close to QueryPlugin.EstimatePrice, maybe we can pull out common code into one function
// that these both use? at least the routes / token In/Out calculation
func (m *MintTokenMessenger) swapTokens(ctx sdk.Context, contractAddr sdk.AccAddress, swap *wasmbindings.SwapMsg) ([]sdk.Event, [][]byte, error) {
if len(swap.Route) != 0 {
return nil, wasmvmtypes.UnsupportedRequest{Kind: "TODO: multi-hop swaps"}
return nil, nil, wasmvmtypes.UnsupportedRequest{Kind: "TODO: multi-hop swaps"}
}
if swap.Amount.ExactIn != nil {
routes := []gammtypes.SwapAmountInRoute{{
PoolId: swap.First.PoolId,
TokenOutDenom: swap.First.DenomOut,
}}
msg := gammtypes.MsgSwapExactAmountIn{
Sender: sender.String(),
Routes: routes,
TokenIn: sdk.Coin{
Denom: swap.First.DenomIn,
Amount: swap.Amount.ExactIn.Input,
},
TokenOutMinAmount: swap.Amount.ExactIn.MinOutput,
tokenIn := sdk.Coin{
Denom: swap.First.DenomIn,
Amount: swap.Amount.ExactIn.Input,
}
tokenOutMinAmount := swap.Amount.ExactIn.MinOutput
_, err := m.gammKeeper.MultihopSwapExactAmountIn(ctx, contractAddr, routes, tokenIn, tokenOutMinAmount)
if err != nil {
return nil, nil, sdkerrors.Wrap(err, "gamm estimate price exact amount in")
}
return []sdk.Msg{&msg}, nil
return nil, nil, nil
} else if swap.Amount.ExactOut != nil {
routes := []gammtypes.SwapAmountOutRoute{{
PoolId: swap.First.PoolId,
TokenInDenom: swap.First.DenomIn,
}}
msg := gammtypes.MsgSwapExactAmountOut{
Sender: sender.String(),
Routes: routes,
TokenInMaxAmount: swap.Amount.ExactOut.MaxInput,
TokenOut: sdk.Coin{
Denom: swap.First.DenomOut,
Amount: swap.Amount.ExactOut.Output,
},
tokenInMaxAmount := swap.Amount.ExactOut.MaxInput
tokenOut := sdk.Coin{
Denom: swap.First.DenomOut,
Amount: swap.Amount.ExactOut.Output,
}
return []sdk.Msg{&msg}, nil
_, err := m.gammKeeper.MultihopSwapExactAmountOut(ctx, contractAddr, routes, tokenInMaxAmount, tokenOut)
if err != nil {
return nil, nil, sdkerrors.Wrap(err, "gamm estimate price exact amount out")
}
return nil, nil, nil
} else {
return nil, wasmvmtypes.UnsupportedRequest{Kind: "must support either Swap.ExactIn or Swap.ExactOut"}
return nil, nil, wasmvmtypes.UnsupportedRequest{Kind: "must support either Swap.ExactIn or Swap.ExactOut"}
}
}

// this 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")
}
// TODO: Confirm "cw" prefix
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
}

func ValidateSubDenom(subDenom string) error {
if len(subDenom) == 0 {
return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "empty sub-denom")
}
// TODO: Extra validations
return nil
}
40 changes: 2 additions & 38 deletions app/wasm/queries.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package wasm

import (
"fmt"
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
"math"

Expand All @@ -27,43 +26,6 @@ func NewQueryPlugin(
}
}

func (qp QueryPlugin) GetFullDenom(ctx sdk.Context, contract string, subDenom string) (string, error) {
err := ValidateAddress(contract)
if err != nil {
return "", sdkerrors.Wrap(err, "validate address")
}
err = ValidateSubDenom(subDenom)
if err != nil {
return "", sdkerrors.Wrap(err, "validate sub-denom")
}

// TODO: Confirm "cw" prefix
fullDenom := fmt.Sprintf("cw/%s/%s", contract, subDenom)

return fullDenom, nil
}

func ValidateAddress(address string) error {
addr, err := sdk.AccAddressFromBech32(address)
if err != nil {
return sdkerrors.Wrap(err, "address from bech32")
}

err = sdk.VerifyAddressFormat(addr)
if err != nil {
return sdkerrors.Wrap(err, "verify address format")
}
return nil
}

func ValidateSubDenom(subDenom string) error {
if len(subDenom) == 0 {
return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "empty sub-denom")
}
// TODO: Extra validations
return nil
}

func (qp QueryPlugin) GetPoolState(ctx sdk.Context, poolId uint64) (*types.PoolState, error) {
poolData, err := qp.gammKeeper.GetPool(ctx, poolId)
if err != nil {
Expand Down Expand Up @@ -97,6 +59,8 @@ func (qp QueryPlugin) GetSpotPrice(ctx sdk.Context, spotPrice *bindings.SpotPric
return &price, nil
}

// TODO: this is very close to MintTokenMessenger.swapTokens, maybe we can pull out common code into one function
// that these both use? at least the routes / token In/Out calculation
func (qp QueryPlugin) EstimatePrice(ctx sdk.Context, estimatePrice *bindings.EstimatePrice) (*bindings.SwapAmount, error) {
sender := estimatePrice.Contract
poolId := estimatePrice.First.PoolId
Expand Down
2 changes: 1 addition & 1 deletion app/wasm/query_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func CustomQuerier(osmoKeeper *QueryPlugin) func(ctx sdk.Context, request json.R
contract := contractQuery.FullDenom.Contract
subDenom := contractQuery.FullDenom.SubDenom

fullDenom, err := osmoKeeper.GetFullDenom(ctx, contract, subDenom)
fullDenom, err := GetFullDenom(contract, subDenom)
if err != nil {
return nil, sdkerrors.Wrap(err, "osmo full denom query")
}
Expand Down
49 changes: 48 additions & 1 deletion app/wasm/test/custom_msg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,47 @@ import (
"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
Expand Down Expand Up @@ -261,7 +302,13 @@ func executeCustom(t *testing.T, ctx sdk.Context, osmosis *app.OsmosisApp, contr
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, sdk.Coins{funds})
_, err = contractKeeper.Execute(ctx, contract, sender, reflectBz, coins)
return err
}
17 changes: 12 additions & 5 deletions app/wasm/wasm.go
Original file line number Diff line number Diff line change
@@ -1,22 +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(
wasmQueryPlugin *QueryPlugin,
gammKeeper *gammkeeper.Keeper,
bank *bankkeeper.BaseKeeper,
) []wasmkeeper.Option {
wasmQueryPlugin := NewQueryPlugin(gammKeeper)

queryPluginOpt := wasmkeeper.WithQueryPlugins(&wasmkeeper.QueryPlugins{
Custom: CustomQuerier(wasmQueryPlugin),
})
messagePluginOpt := wasmkeeper.WithMessageEncoders(&wasmkeeper.MessageEncoders{
Custom: CustomEncoder(wasmQueryPlugin),
})
messangerDecoratorOpt := wasmkeeper.WithMessageHandlerDecorator(
CustomMessageDecorator(gammKeeper, bank),
)

return []wasm.Option{
queryPluginOpt,
messagePluginOpt,
messangerDecoratorOpt,
}
}

0 comments on commit 00b5070

Please sign in to comment.