diff --git a/modules/token/keeper/erc20.go b/modules/token/keeper/erc20.go index 08102837..231ca42c 100644 --- a/modules/token/keeper/erc20.go +++ b/modules/token/keeper/erc20.go @@ -118,6 +118,104 @@ func (k Keeper) SwapFromERC20( return nil } +// SwapToERC20 executes a swap from a native token to its ERC20 token counterpart +// +// Parameters: +// - ctx: the context +// - sender: the sender of the amount +// - receiver: the receiver of the erc20 token +// - amount: the amount to be swapped +// +// Returns: +// - error: error if any. +func (k Keeper) SwapToERC20( + ctx sdk.Context, + sender sdk.AccAddress, + receiver common.Address, + amount sdk.Coin, +) error { + receiverAcc := k.accountKeeper.GetAccount(ctx, sdk.AccAddress(receiver.Bytes())) + if receiverAcc != nil { + if !k.evmKeeper.SupportedKey(receiverAcc.GetPubKey()) { + return errorsmod.Wrapf(types.ErrUnsupportedKey, "key %s", receiverAcc.GetPubKey()) + } + } + + token, err := k.getTokenByMinUnit(ctx, amount.Denom) + if err != nil { + return err + } + if len(token.Contract) == 0 { + return errorsmod.Wrapf(types.ErrERC20NotDeployed, "token: %s is not bound to the corresponding erc20 token", amount.Denom) + } + contract := common.HexToAddress(token.Contract) + + amt := sdk.NewCoins(amount) + if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, sender, types.ModuleName, amt); err != nil { + return err + } + + if err := k.bankKeeper.BurnCoins(ctx, types.ModuleName, amt); err != nil { + return err + } + + if err := k.MintERC20(ctx, contract, receiver, amount.Amount.Uint64()); err != nil { + return err + } + + ctx.EventManager().EmitTypedEvent(&v1.EventSwapToERC20{ + Amount: amount, + Sender: sender.String(), + Receiver: receiver.String(), + ToContract: token.Contract, + }) + return nil +} + +// MintERC20 mints ERC20 tokens to an account. +// +// Parameters: +// - ctx: the sdk.Context for the function +// - contract: the address of the contract +// - to: the address of the receiver +// - amount: the amount to mint +// +// Returns: +// - err : error if any +func (k Keeper) MintERC20( + ctx sdk.Context, + contract, to common.Address, + amount uint64, +) error { + balanceBefore := k.BalanceOf(ctx, contract, to) + + abi := contracts.ERC20TokenContract.ABI + res, err := k.CallEVM(ctx, abi, k.moduleAddress(), contract, true, contracts.MethodMint, to, amount) + if err != nil { + return err + } + + if res.Failed() { + return errorsmod.Wrapf( + types.ErrVMExecution, "failed to mint contract: %s, reason: %s", + contract.String(), + res.Revert(), + ) + } + + balanceAfter := k.BalanceOf(ctx, contract, to) + expectBalance := big.NewInt(0).Add(balanceBefore, big.NewInt(int64(amount))) + if r := expectBalance.Cmp(balanceAfter); r != 0 { + return errorsmod.Wrapf( + types.ErrVMExecution, "failed to mint token correctly, expected after-mint amount is incorrect: %s, expected %d, actual %d", + contract.String(), + expectBalance.Int64(), + balanceAfter.Int64(), + ) + } + return nil +} + // BurnERC20 burns a specific amount of ERC20 tokens from a given contract and address. // // Parameters: diff --git a/modules/token/keeper/msg_server.go b/modules/token/keeper/msg_server.go index e294d0bd..dbd1f0af 100644 --- a/modules/token/keeper/msg_server.go +++ b/modules/token/keeper/msg_server.go @@ -2,6 +2,7 @@ package keeper import ( "context" + "encoding/hex" errorsmod "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" @@ -310,12 +311,9 @@ func (m msgServer) SwapFromERC20(goCtx context.Context, msg *v1.MsgSwapFromERC20 return nil, err } - receiver := sender - if len(msg.Receiver) > 0 { - receiver, err = sdk.AccAddressFromBech32(msg.Receiver) - if err != nil { - return nil, err - } + receiver, err := sdk.AccAddressFromBech32(msg.Receiver) + if err != nil { + return nil, err } if err := m.k.SwapFromERC20(ctx, common.BytesToAddress(sender.Bytes()), receiver, msg.WantedAmount); err != nil { @@ -325,6 +323,21 @@ func (m msgServer) SwapFromERC20(goCtx context.Context, msg *v1.MsgSwapFromERC20 } // SwapToERC20 implements v1.MsgServer. -func (m msgServer) SwapToERC20(context.Context, *v1.MsgSwapToERC20) (*v1.MsgSwapToERC20Response, error) { - panic("unimplemented") +func (m msgServer) SwapToERC20(goCtx context.Context, msg *v1.MsgSwapToERC20) (*v1.MsgSwapToERC20Response, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + sender, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { + return nil, err + } + + bz, err := hex.DecodeString(msg.Receiver) + if err != nil { + return nil, errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, "expecting a hex address of 0x, got %s", msg.Receiver) + } + receiver := common.BytesToAddress(bz) + + if err := m.k.SwapToERC20(ctx, sender, receiver, msg.Amount); err != nil { + return nil, err + } + return &v1.MsgSwapToERC20Response{}, nil } diff --git a/modules/token/types/v1/msgs.go b/modules/token/types/v1/msgs.go index 57ddded9..0153ae7c 100644 --- a/modules/token/types/v1/msgs.go +++ b/modules/token/types/v1/msgs.go @@ -394,10 +394,8 @@ func (m *MsgSwapFromERC20) ValidateBasic() error { return errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, "invalid sender address (%s)", err) } - if len(m.Receiver) > 0 { - if _, err := sdk.AccAddressFromBech32(m.Receiver); err != nil { - return errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, "invalid receiver address (%s)", err) - } + if _, err := sdk.AccAddressFromBech32(m.Receiver); err != nil { + return errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, "invalid receiver address (%s)", err) } if !m.WantedAmount.IsValid() { @@ -418,7 +416,21 @@ func (m *MsgSwapFromERC20) GetSigners() []sdk.AccAddress { // ValidateBasic implements Msg func (m *MsgSwapToERC20) ValidateBasic() error { - // TODO + if _, err := sdk.AccAddressFromBech32(m.Sender); err != nil { + return errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, "invalid sender address (%s)", err) + } + + if tokentypes.IsValidEthAddress(m.Receiver) { + return errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, "expecting a hex address of 0x, got %s", m.Receiver) + } + + if !m.Amount.IsValid() { + return errorsmod.Wrap(sdkerrors.ErrInvalidCoins, m.Amount.String()) + } + + if !m.Amount.IsPositive() { + return errorsmod.Wrap(sdkerrors.ErrInvalidCoins, m.Amount.String()) + } return nil } diff --git a/modules/token/types/validation.go b/modules/token/types/validation.go index 704ba5df..3a58eb10 100644 --- a/modules/token/types/validation.go +++ b/modules/token/types/validation.go @@ -50,6 +50,11 @@ var ( regexpMinUintFmt = fmt.Sprintf("^[a-z][a-z0-9]{%d,%d}$", MinimumMinUnitLen-1, MaximumMinUnitLen-1) regexpMinUint = regexp.MustCompile(regexpMinUintFmt).MatchString + + regexpEthAddressLowerStr = "^0x[0-9a-f]{40}$" + regexpEthAddressUpperStr = "^0x[0-9A-F]{40}$" + regexpEthAddressLower = regexp.MustCompile(regexpEthAddressLowerStr).MatchString + regexpEthAddressUpper = regexp.MustCompile(regexpEthAddressUpperStr).MatchString ) // ValidateInitialSupply verifies whether the initial supply is legal @@ -115,3 +120,9 @@ func ValidateCoin(coin sdk.Coin) error { } return ValidateMinUnit(coin.Denom) } + +// IsValidEthAddress checks if the given address is valid ethereum address +func IsValidEthAddress(address string) bool { + address = strings.ToLower(address) + return regexpEthAddressLower(address) || regexpEthAddressUpper(address) +}