Skip to content

feat: add convert coin endpoint #85

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 110 additions & 96 deletions api/cosmos/evm/erc20/v1/tx.pulsar.go

Large diffs are not rendered by default.

41 changes: 41 additions & 0 deletions api/cosmos/evm/erc20/v1/tx_grpc.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion proto/cosmos/evm/erc20/v1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ package cosmos.evm.erc20.v1;

import "amino/amino.proto";
import "cosmos/base/v1beta1/coin.proto";
import "cosmos/evm/erc20/v1/genesis.proto";
import "cosmos/msg/v1/msg.proto";
import "cosmos_proto/cosmos.proto";
import "gogoproto/gogo.proto";
import "google/api/annotations.proto";
import "cosmos/evm/erc20/v1/genesis.proto";

option go_package = "github.com/cosmos/evm/x/erc20/types";

Expand All @@ -20,6 +20,11 @@ service Msg {
rpc ConvertERC20(MsgConvertERC20) returns (MsgConvertERC20Response) {
option (google.api.http).get = "/cosmos/evm/erc20/v1/tx/convert_erc20";
};
// ConvertCoin mints a ERC20 token representation of the native Cosmos coin
// that is registered on the token mapping.
rpc ConvertCoin(MsgConvertCoin) returns (MsgConvertCoinResponse) {
option (google.api.http).get = "/cosmos/evm/erc20/v1/tx/convert_coin";
}
// UpdateParams defines a governance operation for updating the x/erc20 module
// parameters. The authority is hard-coded to the Cosmos SDK x/gov module
// account
Expand Down Expand Up @@ -60,6 +65,8 @@ message MsgConvertERC20Response {}

// MsgConvertCoin defines a Msg to convert a native Cosmos coin to a ERC20 token
message MsgConvertCoin {
option (amino.name) = "cosmos/evm/x/erc20/MsgConvertCoin";
option (cosmos.msg.v1.signer) = "sender";
// coin is a Cosmos coin whose denomination is registered in a token pair. The
// coin amount defines the amount of coins to convert.
cosmos.base.v1beta1.Coin coin = 1 [ (gogoproto.nullable) = false ];
Expand Down
48 changes: 48 additions & 0 deletions x/erc20/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func NewTxCmd() *cobra.Command {
}

txCmd.AddCommand(
NewConvertCoinCmd(),
NewConvertERC20Cmd(),
)
return txCmd
Expand Down Expand Up @@ -79,3 +80,50 @@ func NewConvertERC20Cmd() *cobra.Command {
flags.AddTxFlagsToCmd(cmd)
return cmd
}

// NewConvertCoinCmd returns a CLI command handler for converting a Cosmos coin
func NewConvertCoinCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "convert-coin COIN [RECEIVER_HEX]",
Short: "Convert a Cosmos coin to ERC20. When the receiver [optional] is omitted, the ERC20 tokens are transferred to the sender.",
Args: cobra.RangeArgs(1, 2),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}

coin, err := sdk.ParseCoinNormalized(args[0])
if err != nil {
return err
}

var receiver string
sender := cliCtx.GetFromAddress()

if len(args) == 2 {
receiver = args[1]
if err := cosmosevmtypes.ValidateAddress(receiver); err != nil {
return fmt.Errorf("invalid receiver hex address %w", err)
}
} else {
receiver = common.BytesToAddress(sender).Hex()
}

msg := &types.MsgConvertCoin{
Coin: coin,
Receiver: receiver,
Sender: sender.String(),
}

if err := msg.ValidateBasic(); err != nil {
return err
}

return tx.GenerateOrBroadcastTxCLI(cliCtx, cmd.Flags(), msg)
},
}

flags.AddTxFlagsToCmd(cmd)
return cmd
}
40 changes: 40 additions & 0 deletions x/erc20/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,46 @@ func (k Keeper) convertERC20IntoCoinsForNativeToken(
return &types.MsgConvertERC20Response{}, nil
}

// ConvertCoin converts native Cosmos coins into ERC20 tokens for both
// Cosmos-native and ERC20 TokenPair Owners
func (k Keeper) ConvertCoin(
goCtx context.Context,
msg *types.MsgConvertCoin,
) (*types.MsgConvertCoinResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

// Error checked during msg validation
sender := sdk.MustAccAddressFromBech32(msg.Sender)
receiver := common.HexToAddress(msg.Receiver)

pair, err := k.MintingEnabled(ctx, sender, receiver.Bytes(), msg.Coin.Denom)
if err != nil {
return nil, err
}

// Check ownership and execute conversion
switch {
case pair.IsNativeERC20():
// Remove token pair if contract is suicided
acc := k.evmKeeper.GetAccountWithoutBalance(ctx, pair.GetERC20Contract())
if acc == nil || !acc.IsContract() {
k.DeleteTokenPair(ctx, pair)
k.Logger(ctx).Debug(
"deleting selfdestructed token pair from state",
"contract", pair.Erc20Address,
)
// NOTE: return nil error to persist the changes from the deletion
return nil, nil
}

return nil, k.ConvertCoinNativeERC20(ctx, pair, msg.Coin.Amount, receiver, sender)
case pair.IsNativeCoin():
return nil, types.ErrNativeConversionDisabled
}

return nil, types.ErrUndefinedOwner
}

// ConvertCoinNativeERC20 handles the coin conversion for a native ERC20 token
// pair:
// - escrow Coins on module account
Expand Down
31 changes: 31 additions & 0 deletions x/erc20/types/msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ var (
_ sdk.Msg = &MsgRegisterERC20{}
_ sdk.Msg = &MsgToggleConversion{}
_ sdk.HasValidateBasic = &MsgConvertERC20{}
_ sdk.HasValidateBasic = &MsgConvertCoin{}
_ sdk.HasValidateBasic = &MsgUpdateParams{}
_ sdk.HasValidateBasic = &MsgRegisterERC20{}
_ sdk.HasValidateBasic = &MsgToggleConversion{}
)

const (
TypeMsgConvertERC20 = "convert_ERC20"
TypeMsgConvertCoin = "convert_coin"
)

var MsgConvertERC20CustomGetSigner = txsigning.CustomGetSigner{
Expand Down Expand Up @@ -105,3 +107,32 @@ func (m *MsgToggleConversion) ValidateBasic() error {

return nil
}

// Route should return the name of the module
func (msg MsgConvertCoin) Route() string { return RouterKey }

// Type should return the action
func (msg MsgConvertCoin) Type() string { return TypeMsgConvertCoin }

// ValidateBasic runs stateless checks on the message
func (msg MsgConvertCoin) ValidateBasic() error {
if len(msg.Coin.Denom) == 0 {
return errorsmod.Wrapf(errortypes.ErrInvalidCoins, "denom cannot be empty")
}
if !msg.Coin.Amount.IsPositive() {
return errorsmod.Wrapf(errortypes.ErrInvalidCoins, "cannot mint a non-positive amount")
}
_, err := sdk.AccAddressFromBech32(msg.Sender)
if err != nil {
return errorsmod.Wrap(err, "invalid sender address")
}
if !common.IsHexAddress(msg.Receiver) {
return errorsmod.Wrapf(errortypes.ErrInvalidAddress, "invalid receiver hex address %s", msg.Receiver)
}
return nil
}

// GetSignBytes encodes the message for signing
func (msg MsgConvertCoin) GetSignBytes() []byte {
return sdk.MustSortJSON(AminoCdc.MustMarshalJSON(&msg))
}
Loading
Loading