diff --git a/x/babylon/contract/in_message.go b/x/babylon/contract/in_message.go index 2905d40..d7df60b 100644 --- a/x/babylon/contract/in_message.go +++ b/x/babylon/contract/in_message.go @@ -1,11 +1,18 @@ package contract -// CustomMsg is a message sent from a smart contract to the Babylon module -// TODO: implement -type CustomMsg struct { - Test *TestMsg `json:"test,omitempty"` -} +import ( + wasmvmtypes "github.com/CosmWasm/wasmvm/v2/types" +) -type TestMsg struct { - Placeholder string `json:"placeholder,omitempty"` -} +// CustomMsg is a message sent from a smart contract to the Babylon module +type ( + CustomMsg struct { + MintRewards *MintRewardsMsg `json:"mint_rewards,omitempty"` + } + // MintRewardsMsg mints the specified number of block rewards, + // and sends them to the specified recipient (typically, the staking contract) + MintRewardsMsg struct { + Amount wasmvmtypes.Coin `json:"amount"` + Recipient string `json:"recipient"` + } +) diff --git a/x/babylon/keeper/handler_plugin.go b/x/babylon/keeper/handler_plugin.go index a7c655f..f14ec34 100644 --- a/x/babylon/keeper/handler_plugin.go +++ b/x/babylon/keeper/handler_plugin.go @@ -1,8 +1,8 @@ package keeper import ( + sdkmath "cosmossdk.io/math" "encoding/json" - wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" wasmvmtypes "github.com/CosmWasm/wasmvm/v2/types" @@ -22,10 +22,13 @@ type AuthSource interface { } // abstract keeper -type msKeeper interface{} +type babylonKeeper interface { + GetParams(ctx sdk.Context) types.Params + MintBlockRewards(ctx sdk.Context, recipient sdk.AccAddress, amount sdk.Coin) (sdkmath.Int, error) +} type CustomMsgHandler struct { - k msKeeper + k babylonKeeper auth AuthSource } @@ -36,7 +39,7 @@ func NewDefaultCustomMsgHandler(k *Keeper) *CustomMsgHandler { // NewCustomMsgHandler constructor to set up CustomMsgHandler with an individual auth source. // This is an extension point for non default contract authorization logic. -func NewCustomMsgHandler(k msKeeper, auth AuthSource) *CustomMsgHandler { +func NewCustomMsgHandler(k babylonKeeper, auth AuthSource) *CustomMsgHandler { return &CustomMsgHandler{k: k, auth: auth} } @@ -55,7 +58,7 @@ func (h CustomMsgHandler) DispatchMsg(ctx sdk.Context, contractAddr sdk.AccAddre if err := json.Unmarshal(msg.Custom, &customMsg); err != nil { return nil, nil, nil, sdkerrors.ErrJSONUnmarshal.Wrap("custom message") } - if customMsg.Test == nil { + if customMsg.MintRewards == nil { // not our message type return nil, nil, nil, wasmtypes.ErrUnknownMsg } @@ -64,11 +67,28 @@ func (h CustomMsgHandler) DispatchMsg(ctx sdk.Context, contractAddr sdk.AccAddre return nil, nil, nil, sdkerrors.ErrUnauthorized.Wrapf("contract has no permission for Babylon operations") } - return h.handleTestMsg(ctx, contractAddr, customMsg.Test) + return h.handleMintRewardsMsg(ctx, contractAddr, customMsg.MintRewards) } -func (h CustomMsgHandler) handleTestMsg(ctx sdk.Context, actor sdk.AccAddress, testMsg *contract.TestMsg) ([]sdk.Event, [][]byte, [][]*codectypes.Any, error) { - return []sdk.Event{}, nil, nil, nil +func (h CustomMsgHandler) handleMintRewardsMsg(ctx sdk.Context, actor sdk.AccAddress, mintMsg *contract.MintRewardsMsg) ([]sdk.Event, [][]byte, [][]*codectypes.Any, error) { + coin, err := wasmkeeper.ConvertWasmCoinToSdkCoin(mintMsg.Amount) + if err != nil { + return nil, nil, nil, err + } + params := h.k.GetParams(ctx) + // Validate actor + if actor.String() != params.BtcFinalityContractAddress { + return nil, nil, nil, sdkerrors.ErrUnauthorized.Wrapf("minter must be the finality contract") + } + + // Define recipient + recipient, err := sdk.AccAddressFromBech32(mintMsg.Recipient) + if err != nil { + return nil, nil, nil, err + } + + _, err = h.k.MintBlockRewards(ctx, recipient, coin) + return nil, nil, nil, err } // AuthSourceFn is helper for simple AuthSource types diff --git a/x/babylon/keeper/mint_rewards.go b/x/babylon/keeper/mint_rewards.go new file mode 100644 index 0000000..eb742d9 --- /dev/null +++ b/x/babylon/keeper/mint_rewards.go @@ -0,0 +1,56 @@ +package keeper + +import ( + sdkmath "cosmossdk.io/math" + "github.com/babylonlabs-io/babylon-sdk/x/babylon/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/errors" +) + +// MintBlockRewards mints new tokens and sends them to the staking contract for distribution. +// Authorization of the actor should be handled before entering this method. +// Authorization of the recipient is being handled within the method for safety, but can +// be removed for flexibility +func (k Keeper) MintBlockRewards(pCtx sdk.Context, recipient sdk.AccAddress, amt sdk.Coin) (sdkmath.Int, error) { + if amt.Amount.IsNil() || amt.Amount.IsZero() || amt.Amount.IsNegative() { + return sdkmath.ZeroInt(), errors.ErrInvalidRequest.Wrap("amount") + } + + // Ensure staking constraints + bondDenom, err := k.Staking.BondDenom(pCtx) + if err != nil { + return sdkmath.ZeroInt(), err + } + if amt.Denom != bondDenom { + return sdkmath.ZeroInt(), errors.ErrInvalidRequest.Wrapf("invalid coin denomination: got %s, expected %s", amt.Denom, bondDenom) + } + // FIXME? Remove this constraint for flexibility + params := k.GetParams(pCtx) + if recipient.String() != params.BtcStakingContractAddress { + return sdkmath.ZeroInt(), errors.ErrUnauthorized.Wrapf("invalid recipient: got %s, expected staking contract (%s)", + recipient, params.BtcStakingContractAddress) + } + + // TODO?: Ensure Babylon constraints + + cacheCtx, done := pCtx.CacheContext() // work in a cached store as Osmosis (safety net?) + + // Mint rewards tokens + coins := sdk.NewCoins(amt) + err = k.bank.MintCoins(cacheCtx, types.ModuleName, coins) + if err != nil { + return sdkmath.ZeroInt(), err + } + + // FIXME: Confirm we want this supply offset enabled for rewards, i.e. + // as virtual coins that do not count to the total supply + //k.bank.AddSupplyOffset(cacheCtx, bondDenom, amt.Amount.Neg()) + + err = k.bank.SendCoinsFromModuleToAccount(cacheCtx, types.ModuleName, recipient, coins) + if err != nil { + return sdkmath.ZeroInt(), err + } + + done() + return amt.Amount, err +} diff --git a/x/babylon/types/events.go b/x/babylon/types/events.go index 3343914..b54895c 100644 --- a/x/babylon/types/events.go +++ b/x/babylon/types/events.go @@ -12,6 +12,7 @@ const ( EventTypeMaxCapLimitUpdated = "max_cap_limit_updated" EventTypeUnbond = "instant_unbond" EventTypeDelegate = "instant_delegate" + EventTypeMintRewards = "mint_rewards" ) const ( diff --git a/x/babylon/types/expected_keepers.go b/x/babylon/types/expected_keepers.go index 56fe6e7..d7b4e53 100644 --- a/x/babylon/types/expected_keepers.go +++ b/x/babylon/types/expected_keepers.go @@ -2,6 +2,7 @@ package types import ( context "context" + sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" @@ -18,6 +19,8 @@ type BankKeeper interface { // StakingKeeper expected staking keeper. type StakingKeeper interface { + BondDenom(ctx context.Context) (string, error) + StakingTokenSupply(ctx context.Context) (sdkmath.Int, error) } // AccountKeeper interface contains functions for getting accounts and the module address