From f3760bf242c8cf1f2461c29b8e9daf5ed6cc80e7 Mon Sep 17 00:00:00 2001 From: dreamer Date: Mon, 15 Apr 2024 14:08:06 +0800 Subject: [PATCH] implement SwapFromERC20 --- contracts/erc20.go | 5 +- modules/token/keeper/erc20.go | 129 +++++++++++++++++++++++++++++ modules/token/keeper/evm.go | 38 +++++++++ modules/token/keeper/msg_server.go | 22 ++++- modules/token/types/v1/msgs.go | 16 +++- 5 files changed, 206 insertions(+), 4 deletions(-) diff --git a/contracts/erc20.go b/contracts/erc20.go index a0b667ab..32806389 100644 --- a/contracts/erc20.go +++ b/contracts/erc20.go @@ -9,6 +9,9 @@ import ( const ( EventSwapToNative = "SwapToNative" + MethodMint = "mint" + MethodBurn = "burn" + MethodBalanceOf = "balanceOf" ) var ( @@ -28,4 +31,4 @@ func init() { if len(ERC20TokenContract.Bin) == 0 { panic("load contract failed") } -} \ No newline at end of file +} diff --git a/modules/token/keeper/erc20.go b/modules/token/keeper/erc20.go index 5ed53dab..2910feda 100644 --- a/modules/token/keeper/erc20.go +++ b/modules/token/keeper/erc20.go @@ -1,6 +1,8 @@ package keeper import ( + "math/big" + errorsmod "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -69,6 +71,133 @@ func (k Keeper) DeployERC20( return contractAddr, nil } +// SwapFromERC20 executes a swap from an ERC20 token to a native token. +// +// Parameters: +// +// ctx - the context in which the swap is executed +// sender - the address of the sender +// receiver - the address of the receiver +// wantedAmount - the amount of the token to be swapped out +// +// Return type: error +func (k Keeper) SwapFromERC20( + ctx sdk.Context, + sender common.Address, + receiver sdk.AccAddress, + wantedAmount sdk.Coin, +) error { + token, err := k.getTokenByMinUnit(ctx, wantedAmount.Denom) + if err != nil { + return err + } + if len(token.Contract) == 0 { + return errorsmod.Wrapf(types.ErrERC20NotDeployed, "The token %s is not bound to the corresponding erc20 token", wantedAmount.Denom) + } + + contract := common.HexToAddress(token.Contract) + amount := wantedAmount.Amount.BigInt() + balance := k.BalanceOf(ctx, contract, sender) + if r := balance.Cmp(amount); r < 0 { + return errorsmod.Wrapf( + sdkerrors.ErrInsufficientFunds, + "balance: %d, swap: %d", + balance, + amount, + ) + } + if err := k.BurnERC20(ctx, contract, sender, amount.Uint64()); err != nil { + return err + } + + mintedCoins := sdk.NewCoins(wantedAmount) + if err := k.bankKeeper.MintCoins(ctx, types.ModuleName, mintedCoins); err != nil { + return err + } + + if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, receiver, mintedCoins); err != nil { + return err + } + + ctx.EventManager().EmitTypedEvent(&v1.EventSwapFromERC20{ + FromContract: contract.String(), + WantedAmount: &wantedAmount, + Sender: sender.String(), + Receiver: receiver.String(), + }) + return nil +} + +// BurnERC20 burns a specific amount of ERC20 tokens from a given contract and address. +// +// Parameters: +// - ctx: the context in which the transaction is executed +// - contract: the contract address of the ERC20 token +// - from: the address from which the tokens are burned +// - amount: the amount of tokens to burn +// +// Returns an error. +func (k Keeper) BurnERC20( + ctx sdk.Context, + contract, from common.Address, + amount uint64, +) error { + balanceBefore := k.BalanceOf(ctx, contract, from) + abi := contracts.ERC20TokenContract.ABI + res, err := k.CallEVM(ctx, abi, k.moduleAddress(), contract, true, contracts.MethodBurn, from, amount) + if err != nil { + return err + } + + if res.Failed() { + return errorsmod.Wrapf(types.ErrVMExecution, "failed to burn %d", amount) + } + + balanceAfter := k.BalanceOf(ctx, contract, from) + expectBalance := big.NewInt(0).Sub(balanceBefore, big.NewInt(int64(amount))) + if r := expectBalance.Cmp(balanceAfter); r != 0 { + return errorsmod.Wrapf( + types.ErrVMExecution, "failed to burn contract: %s, expect %d, actual %d, ", + contract.String(), + expectBalance.Int64(), + balanceAfter.Int64(), + ) + } + return nil +} + +// BalanceOf retrieves the balance of a specific account in the contract. +// +// Parameters: +// - ctx: the sdk.Context for the function +// - contract: the address of the contract +// - account: the address of the account to retrieve the balance for +// +// Returns: +// - *big.Int: the balance of the specified account +func (k Keeper) BalanceOf( + ctx sdk.Context, + contract, account common.Address, +) *big.Int { + abi := contracts.ERC20TokenContract.ABI + res, err := k.CallEVM(ctx, abi, k.moduleAddress(), contract, false, contracts.MethodBalanceOf, account) + if err != nil { + return nil + } + + unpacked, err := abi.Unpack(contracts.MethodBalanceOf, res.Ret) + if err != nil || len(unpacked) == 0 { + return nil + } + + balance, ok := unpacked[0].(*big.Int) + if !ok { + return nil + } + + return balance +} + func (k Keeper) moduleAddress() common.Address { moduleAddr := k.accountKeeper.GetModuleAddress(types.ModuleName) return common.BytesToAddress(moduleAddr.Bytes()) diff --git a/modules/token/keeper/evm.go b/modules/token/keeper/evm.go index 0a7eb2f4..9449de42 100644 --- a/modules/token/keeper/evm.go +++ b/modules/token/keeper/evm.go @@ -4,6 +4,7 @@ import ( "encoding/json" "math/big" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" ethtypes "github.com/ethereum/go-ethereum/core/types" @@ -15,6 +16,43 @@ import ( "github.com/irisnet/irismod/types" ) +// CallEVM calls the EVM with the provided contract ABI, sender and receiver addresses, method, and arguments. +// +// Parameters: +// - ctx: the context in which the EVM call is executed +// - contractABI: the ABI of the contract +// - from: the sender address +// - to: the receiver address +// - commit: boolean indicating whether the EVM call should be committed +// - method: the name of the method to be called +// - args: the arguments to be passed to the method +// +// Returns: +// - *types.Result: the result of the EVM call +// - error: an error if the EVM call encounters any issues +func (k Keeper) CallEVM( + ctx sdk.Context, + contractABI abi.ABI, + from, to common.Address, + commit bool, + method string, + args ...interface{}, +) (*types.Result, error) { + data, err := contractABI.Pack(method, args...) + if err != nil { + return nil, errorsmod.Wrap( + tokentypes.ErrABIPack, + errorsmod.Wrap(err, "failed to create transaction data").Error(), + ) + } + + resp, err := k.CallEVMWithData(ctx, from, &to, data, commit) + if err != nil { + return nil, errorsmod.Wrapf(err, "contract call failed: method '%s', contract '%s'", method, to) + } + return resp, nil +} + // CallEVMWithData executes an Ethereum Virtual Machine (EVM) call with the provided data. // // Parameters: diff --git a/modules/token/keeper/msg_server.go b/modules/token/keeper/msg_server.go index d054b2ab..f7a0a6ec 100644 --- a/modules/token/keeper/msg_server.go +++ b/modules/token/keeper/msg_server.go @@ -6,6 +6,7 @@ import ( errorsmod "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/ethereum/go-ethereum/common" "github.com/irisnet/irismod/modules/token/types" v1 "github.com/irisnet/irismod/modules/token/types/v1" @@ -302,8 +303,25 @@ func (m msgServer) DeployERC20(goCtx context.Context, msg *v1.MsgDeployERC20) (* } // SwapFromERC20 implements v1.MsgServer. -func (m msgServer) SwapFromERC20(context.Context, *v1.MsgSwapFromERC20) (*v1.MsgSwapFromERC20Response, error) { - panic("unimplemented") +func (m msgServer) SwapFromERC20(goCtx context.Context, msg *v1.MsgSwapFromERC20) (*v1.MsgSwapFromERC20Response, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + sender, err := sdk.AccAddressFromBech32(msg.Receiver) + if err != nil { + return nil, err + } + + receiver := sender + if len(msg.Receiver) > 0 { + 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 { + return nil, err + } + return &v1.MsgSwapFromERC20Response{}, nil } // SwapToERC20 implements v1.MsgServer. diff --git a/modules/token/types/v1/msgs.go b/modules/token/types/v1/msgs.go index f1d39484..f5a1385a 100644 --- a/modules/token/types/v1/msgs.go +++ b/modules/token/types/v1/msgs.go @@ -391,7 +391,21 @@ func (m *MsgDeployERC20) GetSigners() []sdk.AccAddress { // ValidateBasic implements Msg func (m *MsgSwapFromERC20) ValidateBasic() error { - // TODO + if _, err := sdk.AccAddressFromBech32(m.Sender); err != nil { + return errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, "invalid sender 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() { + return errorsmod.Wrap(sdkerrors.ErrInvalidCoins, m.WantedAmount.String()) + } + + if !m.WantedAmount.IsPositive() { + return errorsmod.Wrap(sdkerrors.ErrInvalidCoins, m.WantedAmount.String()) + } return nil }