diff --git a/modules/token/keeper/erc20.go b/modules/token/keeper/erc20.go index 231ca42c..4c2d6977 100644 --- a/modules/token/keeper/erc20.go +++ b/modules/token/keeper/erc20.go @@ -31,8 +31,17 @@ func (k Keeper) DeployERC20( name string, symbol string, minUnit string, - scale int8, + scale uint8, ) (common.Address, error) { + token, err := k.buildERC20Token(ctx, name, symbol, minUnit, uint32(scale)) + if err != nil { + return common.Address{}, err + } + + if len(token.Contract) > 0 { + return common.Address{}, errorsmod.Wrapf(types.ErrERC20AlreadyExists, "token: %s already deployed erc20 contract: %s", token.Symbol, token.Contract) + } + contractArgs, err := contracts.ERC20TokenContract.ABI.Pack( "", name, @@ -61,6 +70,9 @@ func (k Keeper) DeployERC20( return common.Address{}, errorsmod.Wrapf(types.ErrVMExecution, "failed to deploy contract for %s, reason: %s", name, result.Revert()) } + token.Contract = contractAddr.String() + k.upsertToken(ctx, *token) + ctx.EventManager().EmitTypedEvent(&v1.EventDeployERC20{ Symbol: symbol, Name: name, @@ -159,7 +171,7 @@ func (k Keeper) SwapToERC20( return err } - if err := k.MintERC20(ctx, contract, receiver, amount.Amount.Uint64()); err != nil { + if err := k.MintERC20(ctx, contract, receiver, amount.Amount.BigInt()); err != nil { return err } @@ -185,9 +197,12 @@ func (k Keeper) SwapToERC20( func (k Keeper) MintERC20( ctx sdk.Context, contract, to common.Address, - amount uint64, + amount *big.Int, ) error { - balanceBefore := k.BalanceOf(ctx, contract, to) + balanceBefore, err := k.BalanceOf(ctx, contract, to) + if err != nil { + return err + } abi := contracts.ERC20TokenContract.ABI res, err := k.CallEVM(ctx, abi, k.moduleAddress(), contract, true, contracts.MethodMint, to, amount) @@ -203,8 +218,11 @@ func (k Keeper) MintERC20( ) } - balanceAfter := k.BalanceOf(ctx, contract, to) - expectBalance := big.NewInt(0).Add(balanceBefore, big.NewInt(int64(amount))) + balanceAfter, err := k.BalanceOf(ctx, contract, to) + if err != nil { + return err + } + expectBalance := big.NewInt(0).Add(balanceBefore, 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", @@ -230,7 +248,11 @@ func (k Keeper) BurnERC20( contract, from common.Address, amount *big.Int, ) error { - balanceBefore := k.BalanceOf(ctx, contract, from) + balanceBefore, err := k.BalanceOf(ctx, contract, from) + if err != nil { + return err + } + if r := balanceBefore.Cmp(amount); r < 0 { return errorsmod.Wrapf( sdkerrors.ErrInsufficientFunds, @@ -250,7 +272,10 @@ func (k Keeper) BurnERC20( return errorsmod.Wrapf(types.ErrVMExecution, "failed to burn %d", amount) } - balanceAfter := k.BalanceOf(ctx, contract, from) + balanceAfter, err := k.BalanceOf(ctx, contract, from) + if err != nil { + return err + } expectBalance := big.NewInt(0).Sub(balanceBefore, amount) if r := expectBalance.Cmp(balanceAfter); r != 0 { return errorsmod.Wrapf( @@ -275,24 +300,24 @@ func (k Keeper) BurnERC20( func (k Keeper) BalanceOf( ctx sdk.Context, contract, account common.Address, -) *big.Int { +) (*big.Int, error) { abi := contracts.ERC20TokenContract.ABI res, err := k.CallEVM(ctx, abi, k.moduleAddress(), contract, false, contracts.MethodBalanceOf, account) if err != nil { - return nil + return nil, err } unpacked, err := abi.Unpack(contracts.MethodBalanceOf, res.Ret) if err != nil || len(unpacked) == 0 { - return nil + return nil, err } balance, ok := unpacked[0].(*big.Int) if !ok { - return nil + return nil, err } - return balance + return balance, nil } func (k Keeper) moduleAddress() common.Address { diff --git a/modules/token/keeper/erc20_test.go b/modules/token/keeper/erc20_test.go new file mode 100644 index 00000000..35d97b01 --- /dev/null +++ b/modules/token/keeper/erc20_test.go @@ -0,0 +1,109 @@ +package keeper_test + +import ( + "math/big" + + "github.com/cometbft/cometbft/crypto/tmhash" + "github.com/ethereum/go-ethereum/common" + + sdk "github.com/cosmos/cosmos-sdk/types" + + v1 "github.com/irisnet/irismod/modules/token/types/v1" +) + +func (suite *KeeperTestSuite) TestDeployERC20() { + token := v1.NewToken("btc", "Bitcoin Network", "satoshi", 18, 21000000, 21000000, false, owner) + + err := suite.keeper.IssueToken( + suite.ctx, token.Symbol, token.Name, + token.MinUnit, token.Scale, token.InitialSupply, + token.MaxSupply, token.Mintable, token.GetOwner(), + ) + suite.NoError(err) + + hash, err := suite.keeper.DeployERC20(suite.ctx, token.Name, token.Symbol, token.MinUnit, uint8(token.Scale)) + suite.NoError(err) + token.Contract = hash.Hex() + + actual, err := suite.keeper.GetToken(suite.ctx, token.Symbol) + suite.NoError(err) + + suite.EqualValues(&token, actual.(*v1.Token)) +} + +func (suite *KeeperTestSuite) TestSwapFromERC20() { + token := v1.NewToken("btc", "Bitcoin Network", "satoshi", 18, 21000000, 21000000, false, owner) + + err := suite.keeper.IssueToken( + suite.ctx, token.Symbol, token.Name, + token.MinUnit, token.Scale, token.InitialSupply, + token.MaxSupply, token.Mintable, token.GetOwner(), + ) + suite.NoError(err) + + contract, err := suite.keeper.DeployERC20(suite.ctx, token.Name, token.Symbol, token.MinUnit, uint8(token.Scale)) + suite.NoError(err) + token.Contract = contract.Hex() + + actual, err := suite.keeper.GetToken(suite.ctx, token.Symbol) + suite.NoError(err) + + suite.EqualValues(&token, actual.(*v1.Token)) + + cosmosAddr := sdk.AccAddress(tmhash.SumTruncated([]byte("TestSwapFromERC20"))) + amount := big.NewInt(2e18) + evmAddr := common.BytesToAddress(cosmosAddr.Bytes()) + + suite.Run("mint erc20", func() { + err = suite.keeper.MintERC20(suite.ctx, contract, evmAddr, amount) + suite.NoError(err) + }) + + suite.Run("swap from erc20", func() { + wantedAmount := sdk.NewCoin(token.MinUnit, sdk.NewInt(1e18)) + + err = suite.keeper.SwapFromERC20(suite.ctx, evmAddr, cosmosAddr, wantedAmount) + suite.NoError(err) + + actual := suite.bk.GetBalance(suite.ctx, cosmosAddr, token.MinUnit) + suite.True(wantedAmount.Equal(actual), "SwapFromERC20 failed: %s != %s", wantedAmount.String(), actual.String()) + + balance, err := suite.keeper.BalanceOf(suite.ctx, contract, evmAddr) + suite.NoError(err) + + expect := big.NewInt(0).Sub(amount, wantedAmount.Amount.BigInt()) + suite.True(expect.Cmp(balance) == 0, "SwapFromERC20 failed balance: %s != %s", expect.String(), balance.String()) + }) +} + +func (suite *KeeperTestSuite) TestSwapToERC20() { + token := v1.NewToken("btc", "Bitcoin Network", "satoshi", 18, 21000000, 21000000, false, owner) + + err := suite.keeper.IssueToken( + suite.ctx, token.Symbol, token.Name, + token.MinUnit, token.Scale, token.InitialSupply, + token.MaxSupply, token.Mintable, token.GetOwner(), + ) + suite.NoError(err) + + contract, err := suite.keeper.DeployERC20(suite.ctx, token.Name, token.Symbol, token.MinUnit, uint8(token.Scale)) + suite.NoError(err) + + sender := token.GetOwner() + receiver := common.BytesToAddress(sender.Bytes()) + + balanceBefore := suite.bk.GetBalance(suite.ctx, sender,token.MinUnit) + suite.Run("swap to erc20", func() { + amount := sdk.NewCoin(token.MinUnit, sdk.NewInt(1e18)) + + err = suite.keeper.SwapToERC20(suite.ctx, sender, receiver, amount) + suite.NoError(err) + + balance, err := suite.keeper.BalanceOf(suite.ctx, contract, receiver) + suite.NoError(err) + suite.True(amount.Amount.BigInt().Cmp(balance) == 0, "SwapToERC20 failed %s != %s", amount.String(), balance.String()) + + actual := suite.bk.GetBalance(suite.ctx, sender, token.MinUnit) + suite.True(balanceBefore.Sub(amount).IsEqual(actual), "SwapToERC20 failed %s != %s", balanceBefore.String(), actual.String()) + }) +} diff --git a/modules/token/keeper/evm.go b/modules/token/keeper/evm.go index 9449de42..4a7df0bf 100644 --- a/modules/token/keeper/evm.go +++ b/modules/token/keeper/evm.go @@ -38,7 +38,7 @@ func (k Keeper) CallEVM( method string, args ...interface{}, ) (*types.Result, error) { - data, err := contractABI.Pack(method, args...) + data, err := contractABI.Pack(method, args...) if err != nil { return nil, errorsmod.Wrap( tokentypes.ErrABIPack, diff --git a/modules/token/keeper/msg_server.go b/modules/token/keeper/msg_server.go index dbd1f0af..150874f0 100644 --- a/modules/token/keeper/msg_server.go +++ b/modules/token/keeper/msg_server.go @@ -284,22 +284,10 @@ func (m msgServer) DeployERC20(goCtx context.Context, msg *v1.MsgDeployERC20) (* } ctx := sdk.UnwrapSDKContext(goCtx) - token, err := m.k.buildERC20Token(ctx, msg.Name, msg.Symbol, msg.MinUnit, msg.Scale) + _, err := m.k.DeployERC20(ctx, msg.Name, msg.Symbol, msg.MinUnit, uint8(msg.Scale)) if err != nil { return nil, err } - - if len(token.Contract) > 0 { - return nil, errorsmod.Wrapf(types.ErrERC20AlreadyExists, "token: %s already deployed erc20 contract: %s", token.Symbol, token.Contract) - } - - contractAddr, err := m.k.DeployERC20(ctx, token.Name, token.Symbol, token.MinUnit, int8(token.Scale)) - if err != nil { - return nil, err - } - - token.Contract = contractAddr.String() - m.k.upsertToken(ctx, *token) return &v1.MsgDeployERC20Response{}, nil } diff --git a/simapp/mocks/evm.go b/simapp/mocks/evm.go index 0cc36668..b585f658 100644 --- a/simapp/mocks/evm.go +++ b/simapp/mocks/evm.go @@ -2,15 +2,19 @@ package mocks import ( "context" + "fmt" "math/big" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/irisnet/irismod/contracts" tokentypes "github.com/irisnet/irismod/modules/token/types" "github.com/irisnet/irismod/types" ) @@ -26,7 +30,35 @@ type evm struct { // ApplyMessage implements types.EVMKeeper. func (e *evm) ApplyMessage(ctx sdk.Context, msg core.Message, tracer vm.EVMLogger, commit bool) (*types.Result, error) { - panic("unimplemented") + isCreate := msg.To() == nil + if isCreate { + contractAddr := crypto.CreateAddress(msg.From(), msg.Nonce()) + + data := msg.Data()[len(contracts.ERC20TokenContract.Bin):] + argss, err := contracts.ERC20TokenContract.ABI.Constructor.Inputs.Unpack(data) + if err != nil { + return nil, err + } + name, _ := argss[0].(string) + symbol, _ := argss[1].(string) + scale, _ := argss[2].(uint8) + e.erc20s[contractAddr] = &erc20{ + address: contractAddr, + scale: scale, + name: name, + symbol: symbol, + balance: make(map[common.Address]*big.Int), + } + return &types.Result{ + Hash: contractAddr.Hex(), + }, nil + } + + erc20Contract, ok := e.erc20s[*msg.To()] + if !ok { + return nil, fmt.Errorf("erc20 contract not found") + } + return e.dispatch(erc20Contract, msg.Data()) } // ChainID implements types.EVMKeeper. @@ -39,23 +71,71 @@ func (e *evm) EstimateGas(ctx context.Context, req *types.EthCallRequest) (uint6 return 3000000, nil } -// FeeDenom implements types.EVMKeeper. -func (e *evm) FeeDenom() string { - return "eris" -} - // SupportedKey implements types.EVMKeeper. func (e *evm) SupportedKey(pubKey cryptotypes.PubKey) bool { return true } +func (e *evm) dispatch(contract *erc20, data []byte) (*types.Result, error) { + method, err := contracts.ERC20TokenContract.ABI.MethodById(data[0:4]) + if err != nil { + return nil, err + } + + ret, err := contract.call(method, data[4:]) + if err != nil { + return nil, err + } + return &types.Result{ + Hash: contract.address.Hex(), + Ret: ret, + }, nil +} + type erc20 struct { - scale int8 + address common.Address + scale uint8 name, symbol string balance map[common.Address]*big.Int } +func (erc20 erc20) call(method *abi.Method, data []byte) ([]byte, error) { + args, err := method.Inputs.Unpack(data) + if err != nil { + return nil, err + } + + switch method.Name { + case "name": + return method.Outputs.Pack(erc20.name) + case "symbol": + return method.Outputs.Pack(erc20.symbol) + case "decimals": + return method.Outputs.Pack(erc20.scale) + case "balanceOf": + balance,ok := erc20.balance[args[0].(common.Address)] + if !ok { + return method.Outputs.Pack(big.NewInt(0)) + } + return method.Outputs.Pack(balance) + case "mint": + to := args[0].(common.Address) + balance,ok := erc20.balance[args[0].(common.Address)] + if !ok { + balance = big.NewInt(0) + } + erc20.balance[to] = new(big.Int).Add(balance, args[1].(*big.Int)) + return nil, nil + case "burn": + from := args[0].(common.Address) + erc20.balance[from] = new(big.Int).Sub(erc20.balance[from], args[1].(*big.Int)) + return nil, nil + default: + return nil, fmt.Errorf("unknown method %s", method.Name) + } +} + type transferKeeper struct{} // HasTrace implements types.ICS20Keeper.