diff --git a/x/onboarding/keeper/ibc_callbacks.go b/x/onboarding/keeper/ibc_callbacks.go index 3abe25ff..bf6acab7 100644 --- a/x/onboarding/keeper/ibc_callbacks.go +++ b/x/onboarding/keeper/ibc_callbacks.go @@ -137,8 +137,9 @@ func (k Keeper) OnRecvPacket( // Use cached context to revert the state if the conversion fails cacheCtx, writeCache := ctx.CacheContext() - if _, err = k.erc20Keeper.ConvertCoin(sdk.WrapSDKContext(cacheCtx), convertMsg); err != nil { + if _, err = k.erc20Keeper.ConvertCoin(cacheCtx, convertMsg); err != nil { logger.Error("failed to convert coins", "error", err) + convertCoin = sdk.NewCoin(transferredCoin.Denom, sdkmath.ZeroInt()) } else { writeCache() } diff --git a/x/onboarding/keeper/ibc_callbacks_test.go b/x/onboarding/keeper/ibc_callbacks_test.go index e86c96df..a679a0ca 100644 --- a/x/onboarding/keeper/ibc_callbacks_test.go +++ b/x/onboarding/keeper/ibc_callbacks_test.go @@ -339,6 +339,25 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { sdk.NewCoin(uusdcIbcdenom, sdkmath.NewInt(20998399)), sdkmath.NewInt(0), }, + { + "convert fail due to evm call failure - cached context is discarded (escrow occured during convert coin is reverted)", + func() { + cantoChannel = "channel-0" + transferAmount = sdkmath.NewIntWithDecimal(25, 6) + transfer := transfertypes.NewFungibleTokenPacketData(denom, transferAmount.String(), secpAddrCosmos, ethsecpAddrcanto, "") + bz := transfertypes.ModuleCdc.MustMarshalJSON(&transfer) + packet = channeltypes.NewPacket(bz, 100, transfertypes.PortID, sourceChannel, transfertypes.PortID, cantoChannel, timeoutHeight, 0) + mockErc20Keeper := NewMockErc20Keeper(suite.app.Erc20Keeper, suite.app.BankKeeper) + suite.app.OnboardingKeeper.SetErc20Keeper(mockErc20Keeper) + mockErc20Keeper.On("ConvertCoin", mock.Anything, mock.Anything).Return(nil, fmt.Errorf("Call EVM Failed")) + + }, + true, + sdk.NewCoins(sdk.NewCoin("acanto", sdkmath.ZeroInt())), + sdk.NewCoin("acanto", sdkmath.NewIntWithDecimal(4, 18)), + sdk.NewCoin(uusdcIbcdenom, sdkmath.NewInt(20998399)), + sdkmath.ZeroInt(), + }, } for _, tc := range testCases { suite.Run(fmt.Sprintf("Case %s", tc.name), func() { @@ -381,8 +400,6 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { suite.Require().NotNil(usdtPair) suite.app.Erc20Keeper.SetTokenPair(suite.ctx, *usdtPair) - tc.malleate() - // Set Denom Trace denomTrace := transfertypes.DenomTrace{ Path: path, @@ -416,6 +433,8 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { suite.Require().True(found) suite.app.OnboardingKeeper = keeper.NewKeeper(sp, suite.app.AccountKeeper, suite.app.BankKeeper, suite.app.IBCKeeper.ChannelKeeper, mockTransferKeeper, suite.app.CoinswapKeeper, suite.app.Erc20Keeper, authtypes.NewModuleAddress(govtypes.ModuleName).String()) + tc.malleate() + // Fund receiver account with canto, IBC vouchers testutil.FundAccount(suite.app.BankKeeper, suite.ctx, ethsecpAddr, tc.receiverBalance) // Fund receiver account with the transferred amount diff --git a/x/onboarding/keeper/keeper.go b/x/onboarding/keeper/keeper.go index 1116bf70..0deac832 100644 --- a/x/onboarding/keeper/keeper.go +++ b/x/onboarding/keeper/keeper.go @@ -100,3 +100,7 @@ func (k Keeper) WriteAcknowledgement(ctx sdk.Context, channelCap *capabilitytype func (k Keeper) GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool) { return k.ics4Wrapper.GetAppVersion(ctx, portID, channelID) } + +func (k *Keeper) SetErc20Keeper(ek types.Erc20Keeper) { + k.erc20Keeper = ek +} diff --git a/x/onboarding/keeper/utils_test.go b/x/onboarding/keeper/utils_test.go index 3ad3b7cf..d3c5c5a9 100644 --- a/x/onboarding/keeper/utils_test.go +++ b/x/onboarding/keeper/utils_test.go @@ -1,6 +1,15 @@ package keeper_test import ( + "context" + "math/big" + + errorsmod "cosmossdk.io/errors" + erc20keeper "github.com/Canto-Network/Canto/v8/x/erc20/keeper" + erc20types "github.com/Canto-Network/Canto/v8/x/erc20/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + evmtypes "github.com/evmos/ethermint/x/evm/types" "github.com/stretchr/testify/mock" tmbytes "github.com/cometbft/cometbft/libs/bytes" @@ -49,3 +58,52 @@ func (m *MockTransferKeeper) SendTransfer( return args.Error(0) } + +type MockErc20Keeper struct { + mock.Mock + erc20keeper erc20keeper.Keeper + bankKeeper bankkeeper.Keeper +} + +func NewMockErc20Keeper(ek erc20keeper.Keeper, bk bankkeeper.Keeper) *MockErc20Keeper { + return &MockErc20Keeper{erc20keeper: ek, bankKeeper: bk} +} + +func (m *MockErc20Keeper) ConvertCoin( + goCtx context.Context, + msg *erc20types.MsgConvertCoin, +) (*erc20types.MsgConvertCoinResponse, error) { + sender, _ := sdk.AccAddressFromBech32(msg.Sender) + + coins := sdk.Coins{msg.Coin} + err := m.bankKeeper.SendCoinsFromAccountToModule(goCtx, sender, types.ModuleName, coins) + if err != nil { + return nil, errorsmod.Wrap(err, "failed to escrow coins") + } + argsMock := m.Called(goCtx, msg) + return nil, argsMock.Error(1) +} + +func (m *MockErc20Keeper) GetTokenPairID(ctx sdk.Context, token string) []byte { + return m.erc20keeper.GetTokenPairID(ctx, token) +} + +func (m *MockErc20Keeper) GetTokenPair(ctx sdk.Context, id []byte) (erc20types.TokenPair, bool) { + return m.erc20keeper.GetTokenPair(ctx, id) +} + +func (m *MockErc20Keeper) BalanceOf(ctx sdk.Context, abi abi.ABI, contract, account common.Address) *big.Int { + return m.erc20keeper.BalanceOf(ctx, abi, contract, account) +} + +func (m *MockErc20Keeper) CallEVM( + ctx sdk.Context, + abi abi.ABI, + from, contract common.Address, + commit bool, + method string, + args ...interface{}, +) (*evmtypes.MsgEthereumTxResponse, error) { + argsMock := m.Called(ctx, abi, from, contract, commit, method, args) + return nil, argsMock.Error(1) +}