From ec53ccaf2eac9b59a8214970e3c4cf52736f14c6 Mon Sep 17 00:00:00 2001 From: Christian Lohr Date: Mon, 30 Dec 2024 16:02:50 +0100 Subject: [PATCH] feat: experimental wasm bindings for token factory & scheduler (#1321) # Related Github tickets - https://github.com/VolumeFi/paloma/issues/2459 # Background This change adds experimental bindings for the token factory, as well as a limited scope of the scheduler module, allowing jobs to be queried and created from smart contracts. Bindings are untested and should be treated as experimental. # Testing completed - [ ] test coverage exists or has been added/updated - [x] tested in a private testnet # Breaking changes - [x] I have checked my code for breaking changes - [x] If there are breaking changes, there is a supporting migration. --- app/app.go | 24 +- x/scheduler/bindings/msg_plugin.go | 90 ++++++++ x/scheduler/bindings/query_plugin.go | 67 ++++++ x/scheduler/bindings/types/msg.go | 23 ++ x/scheduler/bindings/types/query.go | 17 ++ x/scheduler/bindings/types/types.go | 18 ++ x/scheduler/bindings/wasm.go | 23 ++ x/scheduler/client/cli/tx_create_job.go | 9 +- x/scheduler/types/message_create_job.go | 4 +- x/tokenfactory/bindings/msg_plugin.go | 294 ++++++++++++++++++++++++ x/tokenfactory/bindings/queries.go | 58 +++++ x/tokenfactory/bindings/query_plugin.go | 120 ++++++++++ x/tokenfactory/bindings/types/msg.go | 62 +++++ x/tokenfactory/bindings/types/query.go | 52 +++++ x/tokenfactory/bindings/types/types.go | 37 +++ x/tokenfactory/bindings/wasm.go | 27 +++ 16 files changed, 910 insertions(+), 15 deletions(-) create mode 100644 x/scheduler/bindings/msg_plugin.go create mode 100644 x/scheduler/bindings/query_plugin.go create mode 100644 x/scheduler/bindings/types/msg.go create mode 100644 x/scheduler/bindings/types/query.go create mode 100644 x/scheduler/bindings/types/types.go create mode 100644 x/scheduler/bindings/wasm.go create mode 100644 x/tokenfactory/bindings/msg_plugin.go create mode 100644 x/tokenfactory/bindings/queries.go create mode 100644 x/tokenfactory/bindings/query_plugin.go create mode 100644 x/tokenfactory/bindings/types/msg.go create mode 100644 x/tokenfactory/bindings/types/query.go create mode 100644 x/tokenfactory/bindings/types/types.go create mode 100644 x/tokenfactory/bindings/wasm.go diff --git a/app/app.go b/app/app.go index 725191dc..09a93792 100644 --- a/app/app.go +++ b/app/app.go @@ -139,6 +139,7 @@ import ( palomamodulekeeper "github.com/palomachain/paloma/v2/x/paloma/keeper" palomamoduletypes "github.com/palomachain/paloma/v2/x/paloma/types" schedulermodule "github.com/palomachain/paloma/v2/x/scheduler" + schedulerbindings "github.com/palomachain/paloma/v2/x/scheduler/bindings" schedulermodulekeeper "github.com/palomachain/paloma/v2/x/scheduler/keeper" schedulermoduletypes "github.com/palomachain/paloma/v2/x/scheduler/types" skywaymodule "github.com/palomachain/paloma/v2/x/skyway" @@ -146,6 +147,7 @@ import ( skywaymodulekeeper "github.com/palomachain/paloma/v2/x/skyway/keeper" skywaymoduletypes "github.com/palomachain/paloma/v2/x/skyway/types" "github.com/palomachain/paloma/v2/x/tokenfactory" + tokenfactorybindings "github.com/palomachain/paloma/v2/x/tokenfactory/bindings" tokenfactorymodulekeeper "github.com/palomachain/paloma/v2/x/tokenfactory/keeper" tokenfactorymoduletypes "github.com/palomachain/paloma/v2/x/tokenfactory/types" treasurymodule "github.com/palomachain/paloma/v2/x/treasury" @@ -739,6 +741,21 @@ func New( "cosmwasm_1_4", "cosmwasm_2_0", } + + opts := []wasmkeeper.Option{ + wasmkeeper.WithMessageHandlerDecorator(func(old wasmkeeper.Messenger) wasmkeeper.Messenger { + return wasmkeeper.NewMessageHandlerChain( + old, + app.SchedulerKeeper.ExecuteWasmJobEventListener(), + ) + }), + } + bbk, ok := app.BankKeeper.(bankkeeper.BaseKeeper) + if !ok { + panic("bankkeeper is not a BaseKeeper") + } + opts = append(opts, tokenfactorybindings.RegisterCustomPlugins(&bbk, &app.TokenFactoryKeeper)...) + opts = append(opts, schedulerbindings.RegisterCustomPlugins(&app.SchedulerKeeper)...) app.wasmKeeper = wasmkeeper.NewKeeper( appCodec, runtime.NewKVStoreService(keys[wasmtypes.StoreKey]), @@ -757,12 +774,7 @@ func New( wasmConfig, wasmAvailableCapabilities, authorityAddress, - wasmkeeper.WithMessageHandlerDecorator(func(old wasmkeeper.Messenger) wasmkeeper.Messenger { - return wasmkeeper.NewMessageHandlerChain( - old, - app.SchedulerKeeper.ExecuteWasmJobEventListener(), - ) - }), + opts..., ) app.AuthzKeeper = authzkeeper.NewKeeper(runtime.NewKVStoreService(keys[authzkeeper.StoreKey]), appCodec, app.MsgServiceRouter(), app.AccountKeeper) diff --git a/x/scheduler/bindings/msg_plugin.go b/x/scheduler/bindings/msg_plugin.go new file mode 100644 index 00000000..4b7aca36 --- /dev/null +++ b/x/scheduler/bindings/msg_plugin.go @@ -0,0 +1,90 @@ +package bindings + +import ( + "encoding/json" + + sdkerrors "cosmossdk.io/errors" + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + wasmvmtypes "github.com/CosmWasm/wasmvm/v2/types" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + errtypes "github.com/cosmos/cosmos-sdk/types/errors" + bindingstypes "github.com/palomachain/paloma/v2/x/scheduler/bindings/types" + schedulerkeeper "github.com/palomachain/paloma/v2/x/scheduler/keeper" + schedulertypes "github.com/palomachain/paloma/v2/x/scheduler/types" +) + +func CustomMessageDecorator(scheduler *schedulerkeeper.Keeper) func(wasmkeeper.Messenger) wasmkeeper.Messenger { + return func(old wasmkeeper.Messenger) wasmkeeper.Messenger { + return &CustomMessenger{ + wrapped: old, + scheduler: scheduler, + } + } +} + +type CustomMessenger struct { + wrapped wasmkeeper.Messenger + scheduler *schedulerkeeper.Keeper +} + +var _ wasmkeeper.Messenger = (*CustomMessenger)(nil) + +func (m *CustomMessenger) DispatchMsg(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) ([]sdk.Event, [][]byte, [][]*codectypes.Any, error) { + if msg.Custom != nil { + var contractMsg bindingstypes.SchedulerMsg + if err := json.Unmarshal(msg.Custom, &contractMsg); err != nil { + return nil, nil, nil, sdkerrors.Wrap(err, "scheduler msg") + } + if contractMsg.Message == nil { + return nil, nil, nil, sdkerrors.Wrap(errtypes.ErrUnknownRequest, "nil message field") + } + msgType := contractMsg.Message + if msgType.CreateJob != nil { + return m.createJob(ctx, contractAddr, msgType.CreateJob) + } + } + return m.wrapped.DispatchMsg(ctx, contractAddr, contractIBCPortID, msg) +} + +func (m *CustomMessenger) createJob(ctx sdk.Context, contractAddr sdk.AccAddress, createJob *bindingstypes.CreateJob) ([]sdk.Event, [][]byte, [][]*codectypes.Any, error) { + if createJob == nil { + return nil, nil, nil, wasmvmtypes.InvalidRequest{Err: "null create job"} + } + if createJob.Job == nil { + return nil, nil, nil, wasmvmtypes.InvalidRequest{Err: "null job"} + } + + j := &schedulertypes.Job{ + ID: createJob.Job.JobId, + Routing: schedulertypes.Routing{ + ChainType: createJob.Job.ChainType, + ChainReferenceID: createJob.Job.ChainReferenceId, + }, + Definition: []byte(createJob.Job.Definition), + Payload: []byte(createJob.Job.Payload), + IsPayloadModifiable: createJob.Job.PayloadModifiable, + EnforceMEVRelay: createJob.Job.IsMEV, + } + + if err := j.ValidateBasic(); err != nil { + return nil, nil, nil, sdkerrors.Wrap(err, "failed to validate job") + } + + msgServer := schedulerkeeper.NewMsgServerImpl(m.scheduler) + msgCreateJob := schedulertypes.NewMsgCreateJob(contractAddr.String(), j) + if err := msgCreateJob.ValidateBasic(); err != nil { + return nil, nil, nil, sdkerrors.Wrap(err, "failed validating MsgCreateJob") + } + + resp, err := msgServer.CreateJob(ctx, msgCreateJob) + if err != nil { + return nil, nil, nil, sdkerrors.Wrap(err, "failed to create job") + } + + bz, err := resp.Marshal() + if err != nil { + return nil, nil, nil, sdkerrors.Wrap(err, "failed to marshal response") + } + return nil, [][]byte{bz}, nil, nil +} diff --git a/x/scheduler/bindings/query_plugin.go b/x/scheduler/bindings/query_plugin.go new file mode 100644 index 00000000..a9a1b0d6 --- /dev/null +++ b/x/scheduler/bindings/query_plugin.go @@ -0,0 +1,67 @@ +package bindings + +import ( + "encoding/json" + + sdkerrors "cosmossdk.io/errors" + wasmvmtypes "github.com/CosmWasm/wasmvm/v2/types" + sdk "github.com/cosmos/cosmos-sdk/types" + errtypes "github.com/cosmos/cosmos-sdk/types/errors" + bindingstypes "github.com/palomachain/paloma/v2/x/scheduler/bindings/types" + schedulerkeeper "github.com/palomachain/paloma/v2/x/scheduler/keeper" +) + +type QueryPlugin struct { + scheduler *schedulerkeeper.Keeper +} + +func NewQueryPlugin(s *schedulerkeeper.Keeper) *QueryPlugin { + return &QueryPlugin{ + scheduler: s, + } +} + +func CustomQuerier(qp *QueryPlugin) func(ctx sdk.Context, request json.RawMessage) ([]byte, error) { + return func(ctx sdk.Context, request json.RawMessage) ([]byte, error) { + var contractQuery bindingstypes.SchedulerQuery + if err := json.Unmarshal(request, &contractQuery); err != nil { + return nil, sdkerrors.Wrap(err, "query") + } + if contractQuery.Query == nil { + return nil, sdkerrors.Wrap(errtypes.ErrUnknownRequest, "nil query field") + } + queryType := contractQuery.Query + + switch { + case queryType.JobById != nil: + j, err := qp.scheduler.GetJob(ctx, queryType.JobById.JobId) + if err != nil { + return nil, sdkerrors.Wrap(err, "failed to query for job") + } + if j == nil { + return nil, sdkerrors.Wrap(errtypes.ErrNotFound, "job id") + } + + res := bindingstypes.JobByIdResponse{ + Job: &bindingstypes.Job{ + JobId: j.ID, + ChainType: j.Routing.ChainType, + ChainReferenceId: j.Routing.ChainReferenceID, + Definition: string(j.Definition), + Payload: string(j.Payload), + PayloadModifiable: j.IsPayloadModifiable, + IsMEV: j.EnforceMEVRelay, + }, + } + + bz, err := json.Marshal(res) + if err != nil { + return nil, sdkerrors.Wrap(err, "failed to marshal response") + } + + return bz, nil + default: + return nil, wasmvmtypes.UnsupportedRequest{Kind: "unknown scheduler query variant"} + } + } +} diff --git a/x/scheduler/bindings/types/msg.go b/x/scheduler/bindings/types/msg.go new file mode 100644 index 00000000..9df1439d --- /dev/null +++ b/x/scheduler/bindings/types/msg.go @@ -0,0 +1,23 @@ +package types + +type SchedulerMsg struct { + Message *SchedulerMsgType `json:"scheduler_msg_type,omitempty"` +} + +type SchedulerMsgType struct { + // Contracts can create new jobs. Any number of jobs + // may be created, so lang as job IDs stay unique. + CreateJob *CreateJob `json:"create_job,omitempty"` +} + +// CreateJob is a message to create a new job. +// JobId is a unique identifier for the job. +// ChainType is the type of chain the job is for (e.g. "evm"). +// ChainReferenceId is the reference for the chain (e.g. "eth-main"). +// Definition containts the ABI of the target contract. +// Payload is the data to be sent to the contract. +// PayloadModifiable indicates whether the payload can be modified. +// IsMEV indicates whether the job should be routed via an MEV pool. +type CreateJob struct { + Job *Job `json:"job,omitempty"` +} diff --git a/x/scheduler/bindings/types/query.go b/x/scheduler/bindings/types/query.go new file mode 100644 index 00000000..b87eeb00 --- /dev/null +++ b/x/scheduler/bindings/types/query.go @@ -0,0 +1,17 @@ +package types + +type SchedulerQuery struct { + Query *SchedulerQueryType `json:"query,omitempty"` +} + +type SchedulerQueryType struct { + JobById *JobByIdRequest `json:"full_denom,omitempty"` +} + +type JobByIdRequest struct { + JobId string `json:"job_id"` +} + +type JobByIdResponse struct { + Job *Job `json:"job"` +} diff --git a/x/scheduler/bindings/types/types.go b/x/scheduler/bindings/types/types.go new file mode 100644 index 00000000..cedfd2a7 --- /dev/null +++ b/x/scheduler/bindings/types/types.go @@ -0,0 +1,18 @@ +package types + +// JobId is a unique identifier for the job. +// ChainType is the type of chain the job is for (e.g. "evm"). +// ChainReferenceId is the reference for the chain (e.g. "eth-main"). +// Definition containts the ABI of the target contract. +// Payload is the data to be sent to the contract. +// PayloadModifiable indicates whether the payload can be modified. +// IsMEV indicates whether the job should be routed via an MEV pool. +type Job struct { + JobId string `json:"job_id"` + ChainType string `json:"chain_type"` + ChainReferenceId string `json:"chain_reference_id"` + Definition string `json:"definition"` + Payload string `json:"payload"` + PayloadModifiable bool `json:"payload_modifiable"` + IsMEV bool `json:"is_mev"` +} diff --git a/x/scheduler/bindings/wasm.go b/x/scheduler/bindings/wasm.go new file mode 100644 index 00000000..00545c04 --- /dev/null +++ b/x/scheduler/bindings/wasm.go @@ -0,0 +1,23 @@ +package bindings + +import ( + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + schedulerkeeper "github.com/palomachain/paloma/v2/x/scheduler/keeper" +) + +func RegisterCustomPlugins( + scheduler *schedulerkeeper.Keeper, +) []wasmkeeper.Option { + wasmQueryPlugin := NewQueryPlugin(scheduler) + queryPluginOpt := wasmkeeper.WithQueryPlugins(&wasmkeeper.QueryPlugins{ + Custom: CustomQuerier(wasmQueryPlugin), + }) + messengerDecoratorOpt := wasmkeeper.WithMessageHandlerDecorator( + CustomMessageDecorator(scheduler), + ) + + return []wasmkeeper.Option{ + queryPluginOpt, + messengerDecoratorOpt, + } +} diff --git a/x/scheduler/client/cli/tx_create_job.go b/x/scheduler/client/cli/tx_create_job.go index 90a30136..215de9dd 100644 --- a/x/scheduler/client/cli/tx_create_job.go +++ b/x/scheduler/client/cli/tx_create_job.go @@ -8,7 +8,6 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" "github.com/palomachain/paloma/v2/x/scheduler/types" - vtypes "github.com/palomachain/paloma/v2/x/valset/types" "github.com/spf13/cobra" ) @@ -58,13 +57,7 @@ func CmdCreateJob() *cobra.Command { } creator := clientCtx.GetFromAddress().String() - msg := &types.MsgCreateJob{ - Job: job, - Metadata: vtypes.MsgMetadata{ - Creator: creator, - Signers: []string{creator}, - }, - } + msg := types.NewMsgCreateJob(creator, job) if err := msg.ValidateBasic(); err != nil { return err } diff --git a/x/scheduler/types/message_create_job.go b/x/scheduler/types/message_create_job.go index b8ebff4e..3df08fca 100644 --- a/x/scheduler/types/message_create_job.go +++ b/x/scheduler/types/message_create_job.go @@ -10,11 +10,13 @@ const TypeMsgCreateJob = "create_job" var _ sdk.Msg = &MsgCreateJob{} -func NewMsgCreateJob(creator string) *MsgCreateJob { +func NewMsgCreateJob(creator string, j *Job) *MsgCreateJob { return &MsgCreateJob{ Metadata: types.MsgMetadata{ Creator: creator, + Signers: []string{creator}, }, + Job: j, } } diff --git a/x/tokenfactory/bindings/msg_plugin.go b/x/tokenfactory/bindings/msg_plugin.go new file mode 100644 index 00000000..8388d50b --- /dev/null +++ b/x/tokenfactory/bindings/msg_plugin.go @@ -0,0 +1,294 @@ +package bindings + +import ( + "encoding/json" + + sdkerrors "cosmossdk.io/errors" + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + wasmvmtypes "github.com/CosmWasm/wasmvm/v2/types" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + errtypes "github.com/cosmos/cosmos-sdk/types/errors" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + + bindingstypes "github.com/palomachain/paloma/v2/x/tokenfactory/bindings/types" + tokenfactorykeeper "github.com/palomachain/paloma/v2/x/tokenfactory/keeper" + tokenfactorytypes "github.com/palomachain/paloma/v2/x/tokenfactory/types" +) + +func CustomMessageDecorator(bank *bankkeeper.BaseKeeper, tokenFactory *tokenfactorykeeper.Keeper) func(wasmkeeper.Messenger) wasmkeeper.Messenger { + return func(old wasmkeeper.Messenger) wasmkeeper.Messenger { + return &CustomMessenger{ + wrapped: old, + bank: bank, + tokenFactory: tokenFactory, + } + } +} + +type CustomMessenger struct { + wrapped wasmkeeper.Messenger + bank *bankkeeper.BaseKeeper + tokenFactory *tokenfactorykeeper.Keeper +} + +var _ wasmkeeper.Messenger = (*CustomMessenger)(nil) + +func (m *CustomMessenger) DispatchMsg(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) ([]sdk.Event, [][]byte, [][]*codectypes.Any, error) { + if msg.Custom != nil { + // only handle the happy path where this is really creating / minting / swapping ... + // leave everything else for the wrapped version + var contractMsg bindingstypes.TokenFactoryMsg + if err := json.Unmarshal(msg.Custom, &contractMsg); err != nil { + return nil, nil, nil, sdkerrors.Wrap(err, "token factory msg") + } + if contractMsg.Token == nil { + return nil, nil, nil, sdkerrors.Wrap(errtypes.ErrUnknownRequest, "nil token field") + } + tokenMsg := contractMsg.Token + + if tokenMsg.CreateDenom != nil { + return m.createDenom(ctx, contractAddr, tokenMsg.CreateDenom) + } + if tokenMsg.MintTokens != nil { + return m.mintTokens(ctx, contractAddr, tokenMsg.MintTokens) + } + if tokenMsg.ChangeAdmin != nil { + return m.changeAdmin(ctx, contractAddr, tokenMsg.ChangeAdmin) + } + if tokenMsg.BurnTokens != nil { + return m.burnTokens(ctx, contractAddr, tokenMsg.BurnTokens) + } + if tokenMsg.SetMetadata != nil { + return m.setMetadata(ctx, contractAddr, tokenMsg.SetMetadata) + } + } + return m.wrapped.DispatchMsg(ctx, contractAddr, contractIBCPortID, msg) +} + +func (m *CustomMessenger) createDenom(ctx sdk.Context, contractAddr sdk.AccAddress, createDenom *bindingstypes.CreateDenom) ([]sdk.Event, [][]byte, [][]*codectypes.Any, error) { + bz, err := PerformCreateDenom(m.tokenFactory, m.bank, ctx, contractAddr, createDenom) + if err != nil { + return nil, nil, nil, sdkerrors.Wrap(err, "perform create denom") + } + return nil, [][]byte{bz}, nil, nil +} + +func PerformCreateDenom(f *tokenfactorykeeper.Keeper, b *bankkeeper.BaseKeeper, ctx sdk.Context, contractAddr sdk.AccAddress, createDenom *bindingstypes.CreateDenom) ([]byte, error) { + if createDenom == nil { + return nil, wasmvmtypes.InvalidRequest{Err: "create denom null create denom"} + } + + msgServer := tokenfactorykeeper.NewMsgServerImpl(*f) + msgCreateDenom := tokenfactorytypes.NewMsgCreateDenom(contractAddr.String(), createDenom.Subdenom) + if err := msgCreateDenom.ValidateBasic(); err != nil { + return nil, sdkerrors.Wrap(err, "failed validating MsgCreateDenom") + } + + resp, err := msgServer.CreateDenom(ctx, msgCreateDenom) + if err != nil { + return nil, sdkerrors.Wrap(err, "creating denom") + } + + if createDenom.Metadata != nil { + newDenom := resp.NewTokenDenom + err := PerformSetMetadata(f, b, ctx, contractAddr, newDenom, *createDenom.Metadata) + if err != nil { + return nil, sdkerrors.Wrap(err, "setting metadata") + } + } + + return resp.Marshal() +} + +func (m *CustomMessenger) mintTokens(ctx sdk.Context, contractAddr sdk.AccAddress, mint *bindingstypes.MintTokens) ([]sdk.Event, [][]byte, [][]*codectypes.Any, error) { + err := PerformMint(m.tokenFactory, m.bank, ctx, contractAddr, mint) + if err != nil { + return nil, nil, nil, sdkerrors.Wrap(err, "perform mint") + } + return nil, nil, nil, nil +} + +func PerformMint(f *tokenfactorykeeper.Keeper, b *bankkeeper.BaseKeeper, ctx sdk.Context, contractAddr sdk.AccAddress, mint *bindingstypes.MintTokens) error { + if mint == nil { + return wasmvmtypes.InvalidRequest{Err: "mint token null mint"} + } + rcpt, err := parseAddress(mint.MintToAddress) + if err != nil { + return err + } + + coin := sdk.Coin{Denom: mint.Denom, Amount: mint.Amount} + sdkMsg := tokenfactorytypes.NewMsgMint(contractAddr.String(), coin) + if err = sdkMsg.ValidateBasic(); err != nil { + return err + } + + msgServer := tokenfactorykeeper.NewMsgServerImpl(*f) + _, err = msgServer.Mint(ctx, sdkMsg) + if err != nil { + return sdkerrors.Wrap(err, "minting coins from message") + } + err = b.SendCoins(ctx, contractAddr, rcpt, sdk.NewCoins(coin)) + if err != nil { + return sdkerrors.Wrap(err, "sending newly minted coins from message") + } + return nil +} + +func (m *CustomMessenger) changeAdmin(ctx sdk.Context, contractAddr sdk.AccAddress, changeAdmin *bindingstypes.ChangeAdmin) ([]sdk.Event, [][]byte, [][]*codectypes.Any, error) { + err := ChangeAdmin(m.tokenFactory, ctx, contractAddr, changeAdmin) + if err != nil { + return nil, nil, nil, sdkerrors.Wrap(err, "failed to change admin") + } + return nil, nil, nil, nil +} + +func ChangeAdmin(f *tokenfactorykeeper.Keeper, ctx sdk.Context, contractAddr sdk.AccAddress, changeAdmin *bindingstypes.ChangeAdmin) error { + if changeAdmin == nil { + return wasmvmtypes.InvalidRequest{Err: "changeAdmin is nil"} + } + newAdminAddr, err := parseAddress(changeAdmin.NewAdminAddress) + if err != nil { + return err + } + + changeAdminMsg := tokenfactorytypes.NewMsgChangeAdmin(contractAddr.String(), changeAdmin.Denom, newAdminAddr.String()) + if err := changeAdminMsg.ValidateBasic(); err != nil { + return err + } + + msgServer := tokenfactorykeeper.NewMsgServerImpl(*f) + _, err = msgServer.ChangeAdmin(ctx, changeAdminMsg) + if err != nil { + return sdkerrors.Wrap(err, "failed changing admin from message") + } + return nil +} + +func (m *CustomMessenger) burnTokens(ctx sdk.Context, contractAddr sdk.AccAddress, burn *bindingstypes.BurnTokens) ([]sdk.Event, [][]byte, [][]*codectypes.Any, error) { + err := PerformBurn(m.tokenFactory, ctx, contractAddr, burn) + if err != nil { + return nil, nil, nil, sdkerrors.Wrap(err, "perform burn") + } + return nil, nil, nil, nil +} + +func PerformBurn(f *tokenfactorykeeper.Keeper, ctx sdk.Context, contractAddr sdk.AccAddress, burn *bindingstypes.BurnTokens) error { + if burn == nil { + return wasmvmtypes.InvalidRequest{Err: "burn token null mint"} + } + if burn.BurnFromAddress != "" && burn.BurnFromAddress != contractAddr.String() { + return wasmvmtypes.InvalidRequest{Err: "BurnFromAddress must be \"\""} + } + + coin := sdk.Coin{Denom: burn.Denom, Amount: burn.Amount} + sdkMsg := tokenfactorytypes.NewMsgBurn(contractAddr.String(), coin) + if err := sdkMsg.ValidateBasic(); err != nil { + return err + } + + msgServer := tokenfactorykeeper.NewMsgServerImpl(*f) + _, err := msgServer.Burn(ctx, sdkMsg) + if err != nil { + return sdkerrors.Wrap(err, "burning coins from message") + } + return nil +} + +func (m *CustomMessenger) setMetadata(ctx sdk.Context, contractAddr sdk.AccAddress, setMetadata *bindingstypes.SetMetadata) ([]sdk.Event, [][]byte, [][]*codectypes.Any, error) { + err := PerformSetMetadata(m.tokenFactory, m.bank, ctx, contractAddr, setMetadata.Denom, setMetadata.Metadata) + if err != nil { + return nil, nil, nil, sdkerrors.Wrap(err, "perform create denom") + } + return nil, nil, nil, nil +} + +func PerformSetMetadata(f *tokenfactorykeeper.Keeper, b *bankkeeper.BaseKeeper, ctx sdk.Context, contractAddr sdk.AccAddress, denom string, metadata bindingstypes.Metadata) error { + auth, err := f.GetAuthorityMetadata(ctx, denom) + if err != nil { + return err + } + if auth.Admin != contractAddr.String() { + return wasmvmtypes.InvalidRequest{Err: "only admin can set metadata"} + } + + // ensure we are setting proper denom metadata (bank uses Base field, fill it if missing) + if metadata.Base == "" { + metadata.Base = denom + } else if metadata.Base != denom { + // this is the key that we set + return wasmvmtypes.InvalidRequest{Err: "Base must be the same as denom"} + } + + bankMetadata := WasmMetadataToSdk(metadata) + if err := bankMetadata.Validate(); err != nil { + return err + } + + b.SetDenomMetaData(ctx, bankMetadata) + return nil +} + +func getFullDenom(contract string, subDenom string) (string, error) { + if _, err := parseAddress(contract); err != nil { + return "", err + } + fullDenom, err := tokenfactorytypes.GetTokenDenom(contract, subDenom) + if err != nil { + return "", sdkerrors.Wrap(err, "validate sub-denom") + } + + 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 WasmMetadataToSdk(metadata bindingstypes.Metadata) banktypes.Metadata { + denoms := []*banktypes.DenomUnit{} + for _, unit := range metadata.DenomUnits { + denoms = append(denoms, &banktypes.DenomUnit{ + Denom: unit.Denom, + Exponent: unit.Exponent, + Aliases: unit.Aliases, + }) + } + return banktypes.Metadata{ + Description: metadata.Description, + Display: metadata.Display, + Base: metadata.Base, + Name: metadata.Name, + Symbol: metadata.Symbol, + DenomUnits: denoms, + } +} + +func SdkMetadataToWasm(metadata banktypes.Metadata) *bindingstypes.Metadata { + denoms := []bindingstypes.DenomUnit{} + for _, unit := range metadata.DenomUnits { + denoms = append(denoms, bindingstypes.DenomUnit{ + Denom: unit.Denom, + Exponent: unit.Exponent, + Aliases: unit.Aliases, + }) + } + return &bindingstypes.Metadata{ + Description: metadata.Description, + Display: metadata.Display, + Base: metadata.Base, + Name: metadata.Name, + Symbol: metadata.Symbol, + DenomUnits: denoms, + } +} diff --git a/x/tokenfactory/bindings/queries.go b/x/tokenfactory/bindings/queries.go new file mode 100644 index 00000000..323a78b2 --- /dev/null +++ b/x/tokenfactory/bindings/queries.go @@ -0,0 +1,58 @@ +package bindings + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + + bindingstypes "github.com/palomachain/paloma/v2/x/tokenfactory/bindings/types" + tokenfactorykeeper "github.com/palomachain/paloma/v2/x/tokenfactory/keeper" +) + +type QueryPlugin struct { + bankKeeper *bankkeeper.BaseKeeper + tokenFactoryKeeper *tokenfactorykeeper.Keeper +} + +func NewQueryPlugin(b *bankkeeper.BaseKeeper, tfk *tokenfactorykeeper.Keeper) *QueryPlugin { + return &QueryPlugin{ + bankKeeper: b, + tokenFactoryKeeper: tfk, + } +} + +func (qp QueryPlugin) GetDenomAdmin(ctx sdk.Context, denom string) (*bindingstypes.AdminResponse, error) { + metadata, err := qp.tokenFactoryKeeper.GetAuthorityMetadata(ctx, denom) + if err != nil { + return nil, fmt.Errorf("failed to get admin for denom: %s", denom) + } + return &bindingstypes.AdminResponse{Admin: metadata.Admin}, nil +} + +func (qp QueryPlugin) GetDenomsByCreator(ctx sdk.Context, creator string) (*bindingstypes.DenomsByCreatorResponse, error) { + if _, err := parseAddress(creator); err != nil { + return nil, fmt.Errorf("invalid creator address: %s", creator) + } + + denoms := qp.tokenFactoryKeeper.GetDenomsFromCreator(ctx, creator) + return &bindingstypes.DenomsByCreatorResponse{Denoms: denoms}, nil +} + +func (qp QueryPlugin) GetMetadata(ctx sdk.Context, denom string) (*bindingstypes.MetadataResponse, error) { + metadata, found := qp.bankKeeper.GetDenomMetaData(ctx, denom) + var parsed *bindingstypes.Metadata + if found { + parsed = SdkMetadataToWasm(metadata) + } + return &bindingstypes.MetadataResponse{Metadata: parsed}, nil +} + +func (qp QueryPlugin) GetParams(ctx sdk.Context) (*bindingstypes.ParamsResponse, error) { + params := qp.tokenFactoryKeeper.GetParams(ctx) + return &bindingstypes.ParamsResponse{ + Params: bindingstypes.Params{ + DenomCreationFee: ConvertSdkCoinsToWasmCoins(params.DenomCreationFee), + }, + }, nil +} diff --git a/x/tokenfactory/bindings/query_plugin.go b/x/tokenfactory/bindings/query_plugin.go new file mode 100644 index 00000000..5bb1d113 --- /dev/null +++ b/x/tokenfactory/bindings/query_plugin.go @@ -0,0 +1,120 @@ +package bindings + +import ( + "encoding/json" + "fmt" + + sdkerrors "cosmossdk.io/errors" + wasmvmtypes "github.com/CosmWasm/wasmvm/v2/types" + sdk "github.com/cosmos/cosmos-sdk/types" + errtypes "github.com/cosmos/cosmos-sdk/types/errors" + + bindingstypes "github.com/palomachain/paloma/v2/x/tokenfactory/bindings/types" +) + +func CustomQuerier(qp *QueryPlugin) func(ctx sdk.Context, request json.RawMessage) ([]byte, error) { + return func(ctx sdk.Context, request json.RawMessage) ([]byte, error) { + var contractQuery bindingstypes.TokenFactoryQuery + if err := json.Unmarshal(request, &contractQuery); err != nil { + return nil, sdkerrors.Wrap(err, "osmosis query") + } + if contractQuery.Token == nil { + return nil, sdkerrors.Wrap(errtypes.ErrUnknownRequest, "nil token field") + } + tokenQuery := contractQuery.Token + + switch { + case tokenQuery.FullDenom != nil: + creator := tokenQuery.FullDenom.CreatorAddr + subdenom := tokenQuery.FullDenom.Subdenom + + fullDenom, err := getFullDenom(creator, subdenom) + if err != nil { + return nil, sdkerrors.Wrap(err, "full denom query") + } + + res := bindingstypes.FullDenomResponse{ + Denom: fullDenom, + } + + bz, err := json.Marshal(res) + if err != nil { + return nil, sdkerrors.Wrap(err, "failed to marshal FullDenomResponse") + } + + return bz, nil + + case tokenQuery.Admin != nil: + res, err := qp.GetDenomAdmin(ctx, tokenQuery.Admin.Denom) + if err != nil { + return nil, err + } + + bz, err := json.Marshal(res) + if err != nil { + return nil, fmt.Errorf("failed to JSON marshal AdminResponse: %w", err) + } + + return bz, nil + + case tokenQuery.Metadata != nil: + res, err := qp.GetMetadata(ctx, tokenQuery.Metadata.Denom) + if err != nil { + return nil, err + } + + bz, err := json.Marshal(res) + if err != nil { + return nil, fmt.Errorf("failed to JSON marshal MetadataResponse: %w", err) + } + + return bz, nil + + case tokenQuery.DenomsByCreator != nil: + res, err := qp.GetDenomsByCreator(ctx, tokenQuery.DenomsByCreator.Creator) + if err != nil { + return nil, err + } + + bz, err := json.Marshal(res) + if err != nil { + return nil, fmt.Errorf("failed to JSON marshal DenomsByCreatorResponse: %w", err) + } + + return bz, nil + + case tokenQuery.Params != nil: + res, err := qp.GetParams(ctx) + if err != nil { + return nil, err + } + + bz, err := json.Marshal(res) + if err != nil { + return nil, fmt.Errorf("failed to JSON marshal ParamsResponse: %w", err) + } + + return bz, nil + + default: + return nil, wasmvmtypes.UnsupportedRequest{Kind: "unknown token query variant"} + } + } +} + +func ConvertSdkCoinsToWasmCoins(coins []sdk.Coin) []wasmvmtypes.Coin { + var toSend []wasmvmtypes.Coin + for _, coin := range coins { + c := ConvertSdkCoinToWasmCoin(coin) + toSend = append(toSend, c) + } + return toSend +} + +func ConvertSdkCoinToWasmCoin(coin sdk.Coin) wasmvmtypes.Coin { + return wasmvmtypes.Coin{ + Denom: coin.Denom, + // Note: tokens have 18 decimal places, so 10^22 is common, no longer in u64 range + Amount: coin.Amount.String(), + } +} diff --git a/x/tokenfactory/bindings/types/msg.go b/x/tokenfactory/bindings/types/msg.go new file mode 100644 index 00000000..25b7d95c --- /dev/null +++ b/x/tokenfactory/bindings/types/msg.go @@ -0,0 +1,62 @@ +package types + +import ( + sdkmath "cosmossdk.io/math" +) + +type TokenFactoryMsg struct { + Token *TokenMsg `json:"token,omitempty"` +} + +type TokenMsg struct { + /// Contracts can create denoms, namespaced under the contract's address. + /// A contract may create any number of independent sub-denoms. + CreateDenom *CreateDenom `json:"create_denom,omitempty"` + /// Contracts can change the admin of a denom that they are the admin of. + ChangeAdmin *ChangeAdmin `json:"change_admin,omitempty"` + /// Contracts can mint native tokens for an existing factory denom + /// that they are the admin of. + MintTokens *MintTokens `json:"mint_tokens,omitempty"` + /// Contracts can burn native tokens for an existing factory denom + /// that they are the admin of. + /// Currently, the burn from address must be the admin contract. + BurnTokens *BurnTokens `json:"burn_tokens,omitempty"` + /// Sets the metadata on a denom which the contract controls + SetMetadata *SetMetadata `json:"set_metadata,omitempty"` +} + +// CreateDenom creates a new factory denom, of denomination: +// factory/{creating contract address}/{Subdenom} +// Subdenom can be of length at most 44 characters, in [0-9a-zA-Z./] +// The (creating contract address, subdenom) pair must be unique. +// The created denom's admin is the creating contract address, +// but this admin can be changed using the ChangeAdmin binding. +type CreateDenom struct { + Subdenom string `json:"subdenom"` + Metadata *Metadata `json:"metadata,omitempty"` +} + +// ChangeAdmin changes the admin for a factory denom. +// If the NewAdminAddress is empty, the denom has no admin. +type ChangeAdmin struct { + Denom string `json:"denom"` + NewAdminAddress string `json:"new_admin_address"` +} + +type MintTokens struct { + Denom string `json:"denom"` + Amount sdkmath.Int `json:"amount"` + MintToAddress string `json:"mint_to_address"` +} + +type BurnTokens struct { + Denom string `json:"denom"` + Amount sdkmath.Int `json:"amount"` + // BurnFromAddress must be set to "" for now. + BurnFromAddress string `json:"burn_from_address"` +} + +type SetMetadata struct { + Denom string `json:"denom"` + Metadata Metadata `json:"metadata"` +} diff --git a/x/tokenfactory/bindings/types/query.go b/x/tokenfactory/bindings/types/query.go new file mode 100644 index 00000000..fb7c9616 --- /dev/null +++ b/x/tokenfactory/bindings/types/query.go @@ -0,0 +1,52 @@ +package types + +type TokenFactoryQuery struct { + Token *TokenQuery `json:"token,omitempty"` +} + +type TokenQuery struct { + FullDenom *FullDenom `json:"full_denom,omitempty"` + Admin *DenomAdmin `json:"admin,omitempty"` + Metadata *GetMetadata `json:"metadata,omitempty"` + DenomsByCreator *DenomsByCreator `json:"denoms_by_creator,omitempty"` + Params *GetParams `json:"params,omitempty"` +} + +type FullDenom struct { + CreatorAddr string `json:"creator_addr"` + Subdenom string `json:"subdenom"` +} + +type FullDenomResponse struct { + Denom string `json:"denom"` +} + +type GetMetadata struct { + Denom string `json:"denom"` +} + +type MetadataResponse struct { + Metadata *Metadata `json:"metadata,omitempty"` +} + +type DenomAdmin struct { + Denom string `json:"denom"` +} + +type AdminResponse struct { + Admin string `json:"admin"` +} + +type DenomsByCreator struct { + Creator string `json:"creator"` +} + +type DenomsByCreatorResponse struct { + Denoms []string `json:"denoms"` +} + +type GetParams struct{} + +type ParamsResponse struct { + Params Params `json:"params"` +} diff --git a/x/tokenfactory/bindings/types/types.go b/x/tokenfactory/bindings/types/types.go new file mode 100644 index 00000000..4eeeed90 --- /dev/null +++ b/x/tokenfactory/bindings/types/types.go @@ -0,0 +1,37 @@ +package types + +import ( + wasmvmtypes "github.com/CosmWasm/wasmvm/v2/types" +) + +type Metadata struct { + Description string `json:"description"` + // DenomUnits represents the list of DenomUnit's for a given coin + DenomUnits []DenomUnit `json:"denom_units"` + // Base represents the base denom (should be the DenomUnit with exponent = 0). + Base string `json:"base"` + // Display indicates the suggested denom that should be displayed in clients. + Display string `json:"display"` + // Name defines the name of the token (eg: Cosmos Atom) + Name string `json:"name"` + // Symbol is the token symbol usually shown on exchanges (eg: ATOM). + // This can be the same as the display. + Symbol string `json:"symbol"` +} + +type DenomUnit struct { + // Denom represents the string name of the given denom unit (e.g uatom). + Denom string `json:"denom"` + // Exponent represents power of 10 exponent that one must + // raise the base_denom to in order to equal the given DenomUnit's denom + // 1 denom = 1^exponent base_denom + // (e.g. with a base_denom of uatom, one can create a DenomUnit of 'atom' with + // exponent = 6, thus: 1 atom = 10^6 uatom). + Exponent uint32 `json:"exponent"` + // Aliases is a list of string aliases for the given denom + Aliases []string `json:"aliases"` +} + +type Params struct { + DenomCreationFee []wasmvmtypes.Coin `json:"denom_creation_fee"` +} diff --git a/x/tokenfactory/bindings/wasm.go b/x/tokenfactory/bindings/wasm.go new file mode 100644 index 00000000..8d09f889 --- /dev/null +++ b/x/tokenfactory/bindings/wasm.go @@ -0,0 +1,27 @@ +package bindings + +import ( + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + tokenfactorykeeper "github.com/palomachain/paloma/v2/x/tokenfactory/keeper" +) + +func RegisterCustomPlugins( + bank *bankkeeper.BaseKeeper, + tokenFactory *tokenfactorykeeper.Keeper, +) []wasmkeeper.Option { + wasmQueryPlugin := NewQueryPlugin(bank, tokenFactory) + + queryPluginOpt := wasmkeeper.WithQueryPlugins(&wasmkeeper.QueryPlugins{ + Custom: CustomQuerier(wasmQueryPlugin), + }) + messengerDecoratorOpt := wasmkeeper.WithMessageHandlerDecorator( + CustomMessageDecorator(bank, tokenFactory), + ) + + return []wasmkeeper.Option{ + queryPluginOpt, + messengerDecoratorOpt, + } +}