diff --git a/app/app.go b/app/app.go index 2b6af34e8f..41521145f5 100644 --- a/app/app.go +++ b/app/app.go @@ -171,6 +171,7 @@ var ( stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, govtypes.ModuleName: {authtypes.Burner}, ibctransfertypes.ModuleName: {authtypes.Minter, authtypes.Burner}, + wasm.ModuleName: {authtypes.Minter, authtypes.Burner}, } // module accounts that are allowed to receive tokens diff --git a/x/wasm/internal/keeper/cosmwasm/msg.go b/x/wasm/internal/keeper/cosmwasm/msg.go index 59511e41c5..8ee1e2f89a 100644 --- a/x/wasm/internal/keeper/cosmwasm/msg.go +++ b/x/wasm/internal/keeper/cosmwasm/msg.go @@ -9,7 +9,7 @@ import ( // CosmosMsg is an rust enum and only (exactly) one of the fields should be set // Should we do a cleaner approach in Go? (type/data?) type CosmosMsg struct { - Bank *cosmwasmv1.BankMsg `json:"bank,omitempty"` + Bank *BankMsg `json:"bank,omitempty"` Custom json.RawMessage `json:"custom,omitempty"` Staking *cosmwasmv1.StakingMsg `json:"staking,omitempty"` Wasm *cosmwasmv1.WasmMsg `json:"wasm,omitempty"` @@ -56,3 +56,16 @@ type HandleResponse struct { // log message to return over abci interface Log []cosmwasmv1.LogAttribute `json:"log"` } + +type MintMsg struct { + Coin cosmwasmv1.Coin `json:"amount"` +} +type BurnMsg struct { + Coin cosmwasmv1.Coin `json:"amount"` +} + +type BankMsg struct { + Send *cosmwasmv1.SendMsg `json:"send,omitempty"` + Mint *MintMsg `json:"mint,omitempty"` + Burn *BurnMsg `json:"burn,omitempty"` +} diff --git a/x/wasm/internal/keeper/handler_plugin.go b/x/wasm/internal/keeper/handler_plugin.go index 79ff0a764b..fdd7bbc252 100644 --- a/x/wasm/internal/keeper/handler_plugin.go +++ b/x/wasm/internal/keeper/handler_plugin.go @@ -9,12 +9,15 @@ import ( "github.com/CosmWasm/wasmd/x/wasm/internal/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" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" ibctransfertypes "github.com/cosmos/cosmos-sdk/x/ibc-transfer/types" channeltypes "github.com/cosmos/cosmos-sdk/x/ibc/04-channel/types" host "github.com/cosmos/cosmos-sdk/x/ibc/24-host" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/tendermint/tendermint/crypto/tmhash" + tmbytes "github.com/tendermint/tendermint/libs/bytes" ) type MessageHandler struct { @@ -23,7 +26,7 @@ type MessageHandler struct { } func NewMessageHandler(router sdk.Router, customEncoders *MessageEncoders) MessageHandler { - encoders := DefaultEncoders(nil, nil).Merge(customEncoders) + encoders := DefaultEncoders(nil, nil, nil).Merge(customEncoders) return MessageHandler{ router: router, encoders: encoders, @@ -31,14 +34,14 @@ func NewMessageHandler(router sdk.Router, customEncoders *MessageEncoders) Messa } func NewMessageHandlerV2(router sdk.Router, channelKeeper types.ChannelKeeper, capabilityKeeper types.CapabilityKeeper, customEncoders *MessageEncoders) MessageHandler { - encoders := DefaultEncoders(channelKeeper, capabilityKeeper).Merge(customEncoders) + encoders := DefaultEncoders(channelKeeper, capabilityKeeper, nil).Merge(customEncoders) return MessageHandler{ router: router, encoders: encoders, } } -type BankEncoder func(sender sdk.AccAddress, msg *cosmwasmv1.BankMsg) ([]sdk.Msg, error) +type BankEncoder func(ctx sdk.Context, sender sdk.AccAddress, msg *cosmwasmv2.BankMsg) ([]sdk.Msg, error) type CustomEncoder func(sender sdk.AccAddress, msg json.RawMessage) ([]sdk.Msg, error) type StakingEncoder func(sender sdk.AccAddress, msg *cosmwasmv1.StakingMsg) ([]sdk.Msg, error) type WasmEncoder func(sender sdk.AccAddress, msg *cosmwasmv1.WasmMsg) ([]sdk.Msg, error) @@ -52,7 +55,12 @@ type MessageEncoders struct { IBC IBCEncoder } -func DefaultEncoders(channelKeeper types.ChannelKeeper, capabilityKeeper types.CapabilityKeeper) MessageEncoders { +type BankKeeper interface { + MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error + BurnCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error +} + +func DefaultEncoders(channelKeeper types.ChannelKeeper, capabilityKeeper types.CapabilityKeeper, bankKeeper bankkeeper.Keeper) MessageEncoders { e := MessageEncoders{ Bank: EncodeBankMsg, Custom: NoCustomMsg, @@ -62,6 +70,9 @@ func DefaultEncoders(channelKeeper types.ChannelKeeper, capabilityKeeper types.C if channelKeeper != nil { // todo: quick hack to keep tests happy e.IBC = EncodeIBCMsg(channelKeeper, capabilityKeeper) } + if bankKeeper != nil { + e.Bank = BankMsgEncoderV2(bankKeeper) + } return e } @@ -90,7 +101,7 @@ func (e MessageEncoders) Merge(o *MessageEncoders) MessageEncoders { func (e MessageEncoders) Encode(contractAddr sdk.AccAddress, msg cosmwasmv1.CosmosMsg) ([]sdk.Msg, error) { switch { case msg.Bank != nil: - return e.Bank(contractAddr, msg.Bank) + return e.Bank(sdk.Context{}, contractAddr, &cosmwasmv2.BankMsg{Send: msg.Bank.Send}) // TODO: quick hack for the spike case msg.Custom != nil: return e.Custom(contractAddr, msg.Custom) case msg.Staking != nil: @@ -105,7 +116,7 @@ func (e MessageEncoders) Encode(contractAddr sdk.AccAddress, msg cosmwasmv1.Cosm func (e MessageEncoders) EncodeV2(ctx sdk.Context, contractAddr sdk.AccAddress, source cosmwasmv2.IBCEndpoint, msg cosmwasmv2.CosmosMsg) ([]sdk.Msg, error) { switch { case msg.Bank != nil: - return e.Bank(contractAddr, msg.Bank) + return e.Bank(ctx, contractAddr, msg.Bank) case msg.Custom != nil: return e.Custom(contractAddr, msg.Custom) case msg.Staking != nil: @@ -118,7 +129,54 @@ func (e MessageEncoders) EncodeV2(ctx sdk.Context, contractAddr sdk.AccAddress, return nil, sdkerrors.Wrap(types.ErrInvalidMsg, "Unknown variant of Wasm") } -func EncodeBankMsg(sender sdk.AccAddress, msg *cosmwasmv1.BankMsg) ([]sdk.Msg, error) { +func BankMsgEncoderV2(bank bankkeeper.Keeper) BankEncoder { + return func(ctx sdk.Context, contractAddr sdk.AccAddress, msg *cosmwasmv2.BankMsg) ([]sdk.Msg, error) { + if msg.Mint != nil { + if len(msg.Mint.Coin.Amount) == 0 { + return nil, nil + } + voucher, err := coinToVoucher(contractAddr, msg.Mint.Coin) + if err != nil { + return nil, err + } + err = bank.MintCoins(ctx, types.ModuleName, voucher) + if err != nil { + return nil, err + } + return nil, bank.SendCoinsFromModuleToAccount( + ctx, types.ModuleName, contractAddr, voucher, + ) + } + if msg.Burn != nil { + if len(msg.Burn.Coin.Amount) == 0 { + return nil, nil + } + voucher, err := coinToVoucher(contractAddr, msg.Burn.Coin) + if err != nil { + return nil, err + } + err = bank.SendCoinsFromAccountToModule(ctx, contractAddr, types.ModuleName, voucher) + if err != nil { + return nil, err + } + return nil, bank.BurnCoins(ctx, types.ModuleName, voucher) + } + return EncodeBankMsg(ctx, contractAddr, msg) + } +} + +func coinToVoucher(contractAddr sdk.AccAddress, coin cosmwasmv1.Coin) (sdk.Coins, error) { + sdkCoin, err := convertWasmCoinToSdkCoin(coin) + if err != nil { + return nil, err + } + denumTrace := fmt.Sprintf("%s/%s/", contractAddr.String(), sdkCoin.Denom) + var hash tmbytes.HexBytes = tmhash.Sum([]byte(denumTrace)) + simpleVoucherDenum := fmt.Sprintf("wasm/%s", hash) + return sdk.NewCoins(sdk.NewCoin(simpleVoucherDenum, sdkCoin.Amount)), nil +} + +func EncodeBankMsg(_ sdk.Context, sender sdk.AccAddress, msg *cosmwasmv2.BankMsg) ([]sdk.Msg, error) { if msg.Send == nil { return nil, sdkerrors.Wrap(types.ErrInvalidMsg, "Unknown variant of Bank") } diff --git a/x/wasm/internal/keeper/handler_plugin_test.go b/x/wasm/internal/keeper/handler_plugin_test.go index 3e3b381f74..672301ba28 100644 --- a/x/wasm/internal/keeper/handler_plugin_test.go +++ b/x/wasm/internal/keeper/handler_plugin_test.go @@ -258,7 +258,7 @@ func TestEncoding(t *testing.T) { }, } - encoder := DefaultEncoders(nil, nil) + encoder := DefaultEncoders(nil, nil, nil) for name, tc := range cases { tc := tc t.Run(name, func(t *testing.T) { diff --git a/x/wasm/internal/keeper/keeper.go b/x/wasm/internal/keeper/keeper.go index 70045b4ef6..9173120e00 100644 --- a/x/wasm/internal/keeper/keeper.go +++ b/x/wasm/internal/keeper/keeper.go @@ -91,7 +91,7 @@ func NewKeeper( } // todo: revisit: DefaultEncoders are used twice now - quickHack := DefaultEncoders(channelKeeper, scopedKeeper).Merge(customEncoders) + quickHack := DefaultEncoders(channelKeeper, scopedKeeper, bankKeeper).Merge(customEncoders) keeper := Keeper{ storeKey: storeKey, cdc: cdc, diff --git a/x/wasm/internal/keeper/minter_test.go b/x/wasm/internal/keeper/minter_test.go new file mode 100644 index 0000000000..091708ba1b --- /dev/null +++ b/x/wasm/internal/keeper/minter_test.go @@ -0,0 +1,102 @@ +package keeper + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/CosmWasm/go-cosmwasm" + cosmwasmv1 "github.com/CosmWasm/go-cosmwasm/types" + cosmwasmv2 "github.com/CosmWasm/wasmd/x/wasm/internal/keeper/cosmwasm" + "github.com/cosmos/cosmos-sdk/store/prefix" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/stretchr/testify/require" +) + +func TestMinter(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) + accKeeper, keeper, bankKeeper := keepers.AccountKeeper, keepers.WasmKeeper, keepers.BankKeeper + totalSupply := types.NewSupply(sdk.NewCoins(sdk.NewInt64Coin("denom", 400000000))) + bankKeeper.SetSupply(ctx, totalSupply) + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := createFakeFundedAccount(t, ctx, accKeeper, bankKeeper, deposit) + + wasmCode, err := ioutil.ReadFile("./testdata/contract.wasm") + require.NoError(t, err) + + // create any dummy contract to mock later + codeID, err := keeper.Create(ctx, creator, wasmCode, "", "any/builder:tag", nil) + require.NoError(t, err) + // with random addresses + initMsgBz := []byte(`{"verifier": "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5", "beneficiary":"cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5"}`) + contractAddr, err := keeper.Instantiate(ctx, codeID, creator, nil, initMsgBz, "demo contract 3", deposit) + require.NoError(t, err) + require.Equal(t, "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5", contractAddr.String()) + + MockContracts[contractAddr.String()] = &minterContract{t: t, contractAddr: contractAddr} + fred := createFakeFundedAccount(t, ctx, accKeeper, bankKeeper, sdk.NewCoins(sdk.NewInt64Coin("denom", 5000))) + + // call execute to return a mint message + _, err = keeper.Execute(ctx, contractAddr, fred, []byte(`mint`), nil) + require.NoError(t, err) + + const contractsDenom = "wasm/5EFEC84ADDE58024AEF3A7BB9CD5945F91D180D0A4AC5AA2534BEF8F6194CD79" + t.Logf("+++ Contract owns: %s", bankKeeper.GetAllBalances(ctx, contractAddr).String()) + require.Equal(t, sdk.Coin{Denom: contractsDenom, Amount: sdk.NewInt(10000000)}, bankKeeper.GetBalance(ctx, contractAddr, contractsDenom)) + + // now call execute to return a burn message + _, err = keeper.Execute(ctx, contractAddr, fred, []byte(`burn`), nil) + require.NoError(t, err) + t.Logf("+++ Contract owns: %s", bankKeeper.GetAllBalances(ctx, contractAddr).String()) + require.Equal(t, sdk.Coin{Denom: contractsDenom, Amount: sdk.NewInt(1)}, bankKeeper.GetBalance(ctx, contractAddr, contractsDenom)) +} + +type minterContract struct { + t *testing.T + contractAddr sdk.AccAddress +} + +func (m *minterContract) Execute(hash []byte, params cosmwasmv1.Env, msg []byte, store prefix.Store, api cosmwasm.GoAPI, querier QueryHandler, meter sdk.GasMeter, gas uint64) (*cosmwasmv2.HandleResponse, uint64, error) { + msgs := map[string]*cosmwasmv2.BankMsg{ + "mint": { + Mint: &cosmwasmv2.MintMsg{Coin: cosmwasmv1.Coin{Denom: "alx", Amount: "10000000"}}, + }, + "burn": { + Burn: &cosmwasmv2.BurnMsg{Coin: cosmwasmv1.Coin{Denom: "alx", Amount: "9999999"}}, + }, + } + return &cosmwasmv2.HandleResponse{ + Messages: []cosmwasmv2.CosmosMsg{ + {Bank: msgs[string(msg)]}, + }, + }, 0, nil +} + +func (m minterContract) OnIBCPacketReceive(hash []byte, params cosmwasmv2.Env, packet cosmwasmv2.IBCPacket, store prefix.Store, api cosmwasm.GoAPI, querier QueryHandler, meter sdk.GasMeter, gas uint64) (*cosmwasmv2.IBCPacketReceiveResponse, uint64, error) { + panic("implement me") +} + +func (m minterContract) OnIBCPacketAcknowledgement(hash []byte, params cosmwasmv2.Env, packetAck cosmwasmv2.IBCAcknowledgement, store prefix.Store, api cosmwasm.GoAPI, querier QueryHandler, meter sdk.GasMeter, gas uint64) (*cosmwasmv2.IBCPacketAcknowledgementResponse, uint64, error) { + panic("implement me") +} + +func (m minterContract) OnIBCPacketTimeout(hash []byte, params cosmwasmv2.Env, packet cosmwasmv2.IBCPacket, store prefix.Store, api cosmwasm.GoAPI, querier QueryHandler, meter sdk.GasMeter, gas uint64) (*cosmwasmv2.IBCPacketTimeoutResponse, uint64, error) { + panic("implement me") +} + +func (m minterContract) OnIBCChannelOpen(hash []byte, params cosmwasmv2.Env, channel cosmwasmv2.IBCChannel, store prefix.Store, api cosmwasm.GoAPI, querier QueryHandler, meter sdk.GasMeter, gas uint64) (*cosmwasmv2.IBCChannelOpenResponse, uint64, error) { + panic("implement me") +} + +func (m minterContract) OnIBCChannelConnect(hash []byte, params cosmwasmv2.Env, channel cosmwasmv2.IBCChannel, store prefix.Store, api cosmwasm.GoAPI, querier QueryHandler, meter sdk.GasMeter, gas uint64) (*cosmwasmv2.IBCChannelConnectResponse, uint64, error) { + panic("implement me") +} + +func (m minterContract) OnIBCChannelClose(ctx sdk.Context, hash []byte, params cosmwasmv2.Env, channel cosmwasmv2.IBCChannel, meter sdk.GasMeter, gas uint64) (*cosmwasmv2.IBCChannelCloseResponse, uint64, error) { + panic("implement me") +} diff --git a/x/wasm/internal/keeper/test_common.go b/x/wasm/internal/keeper/test_common.go index c42215e516..5a41d2bd60 100644 --- a/x/wasm/internal/keeper/test_common.go +++ b/x/wasm/internal/keeper/test_common.go @@ -165,6 +165,7 @@ func CreateTestInput(t *testing.T, isCheckTx bool, tempDir string, supportedFeat stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, govtypes.ModuleName: {authtypes.Burner}, transfertypes.ModuleName: {authtypes.Minter, authtypes.Burner}, + wasmtypes.ModuleName: {authtypes.Minter, authtypes.Burner}, } authSubsp, _ := paramsKeeper.GetSubspace(authtypes.ModuleName) authKeeper := authkeeper.NewAccountKeeper( diff --git a/x/wasm/relay_test.go b/x/wasm/relay_test.go index 0e2f07f2e9..5a473a81e6 100644 --- a/x/wasm/relay_test.go +++ b/x/wasm/relay_test.go @@ -119,7 +119,7 @@ func (s *senderContract) OnIBCChannelConnect(hash []byte, params cosmwasmv2.Env, // abusing onConnect event to send the message. can be any execute event which is not mocked though escrowAddress := ibctransfertypes.GetEscrowAddress(channel.Endpoint.Port, channel.Endpoint.Channel) - sendToEscrowMsg := &cosmwasmv1.BankMsg{ + sendToEscrowMsg := &cosmwasmv2.BankMsg{ Send: &cosmwasmv1.SendMsg{ FromAddress: s.contractAddr.String(), ToAddress: escrowAddress.String(),