Skip to content
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

feat: implement SwapToERC20 #395

Merged
merged 5 commits into from
Apr 16, 2024
Merged
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
98 changes: 98 additions & 0 deletions modules/token/keeper/erc20.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
29 changes: 21 additions & 8 deletions modules/token/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package keeper

import (
"context"
"encoding/hex"

errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
dreamer-zq marked this conversation as resolved.
Show resolved Hide resolved
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
}
22 changes: 17 additions & 5 deletions modules/token/types/v1/msgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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
}

Expand Down
11 changes: 11 additions & 0 deletions modules/token/types/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Loading