From d1ff261d3c1d1bf7ed47cedfcdac85fd3d24159d Mon Sep 17 00:00:00 2001 From: Tanmay Date: Fri, 9 Feb 2024 15:50:02 -0500 Subject: [PATCH] use outbound amount for refunds instead of inbound --- x/crosschain/keeper/cctx.go | 2 +- x/crosschain/keeper/cctx_utils.go | 16 ++++ x/crosschain/keeper/cctx_utils_test.go | 41 ++++++++++ .../keeper/msg_server_refund_aborted_tx.go | 24 ++++-- .../msg_server_refund_aborted_tx_test.go | 78 +++++++++++++------ x/crosschain/keeper/refund.go | 17 ++-- x/crosschain/keeper/refund_test.go | 76 +++++++++++++++++- 7 files changed, 213 insertions(+), 41 deletions(-) diff --git a/x/crosschain/keeper/cctx.go b/x/crosschain/keeper/cctx.go index e3a2a06f71..972875785e 100644 --- a/x/crosschain/keeper/cctx.go +++ b/x/crosschain/keeper/cctx.go @@ -49,7 +49,7 @@ func (k Keeper) SetCctxAndNonceToCctxAndInTxHashToCctx(ctx sdk.Context, cctx typ }) } if cctx.CctxStatus.Status == types.CctxStatus_Aborted && cctx.GetCurrentOutTxParam().CoinType == common.CoinType_Zeta { - k.AddZetaAbortedAmount(ctx, cctx.GetCurrentOutTxParam().Amount) + k.AddZetaAbortedAmount(ctx, GetAbortedAmount(cctx)) } } diff --git a/x/crosschain/keeper/cctx_utils.go b/x/crosschain/keeper/cctx_utils.go index 31422b7e63..8c2edd5013 100644 --- a/x/crosschain/keeper/cctx_utils.go +++ b/x/crosschain/keeper/cctx_utils.go @@ -4,6 +4,7 @@ import ( "fmt" cosmoserrors "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" "github.com/pkg/errors" sdk "github.com/cosmos/cosmos-sdk/types" @@ -89,3 +90,18 @@ func IsPending(cctx types.CrossChainTx) bool { // pending inbound is not considered a "pending" state because it has not reached consensus yet return cctx.CctxStatus.Status == types.CctxStatus_PendingOutbound || cctx.CctxStatus.Status == types.CctxStatus_PendingRevert } + +// GetAbortedAmount returns the amount to refund for a given CCTX . +// If the CCTX has an outbound transaction, it returns the amount of the outbound transaction. +// If OutTxParams is nil or the amount is zero, it returns the amount of the inbound transaction. +// This is because there might be a case where the transaction is set to be aborted before paying gas or creating an outbound transaction.In such a situation we can refund the entire amount that has been locked in connector or TSS +func GetAbortedAmount(cctx types.CrossChainTx) sdkmath.Uint { + if cctx.OutboundTxParams != nil && !cctx.GetCurrentOutTxParam().Amount.IsZero() { + return cctx.GetCurrentOutTxParam().Amount + } + if cctx.InboundTxParams != nil { + return cctx.InboundTxParams.Amount + } + + return sdkmath.ZeroUint() +} diff --git a/x/crosschain/keeper/cctx_utils_test.go b/x/crosschain/keeper/cctx_utils_test.go index cba32c6a75..319ef35a54 100644 --- a/x/crosschain/keeper/cctx_utils_test.go +++ b/x/crosschain/keeper/cctx_utils_test.go @@ -4,10 +4,12 @@ import ( "math/big" "testing" + sdkmath "cosmossdk.io/math" "github.com/stretchr/testify/require" "github.com/zeta-chain/zetacore/common" keepertest "github.com/zeta-chain/zetacore/testutil/keeper" "github.com/zeta-chain/zetacore/testutil/sample" + crosschainkeeper "github.com/zeta-chain/zetacore/x/crosschain/keeper" "github.com/zeta-chain/zetacore/x/crosschain/types" fungibletypes "github.com/zeta-chain/zetacore/x/fungible/types" ) @@ -150,3 +152,42 @@ func TestGetRevertGasLimit(t *testing.T) { require.ErrorIs(t, err, fungibletypes.ErrContractCall) }) } + +func TestGetAbortedAmount(t *testing.T) { + amount := sdkmath.NewUint(100) + t.Run("should return the inbound amount if outbound not present", func(t *testing.T) { + cctx := types.CrossChainTx{ + InboundTxParams: &types.InboundTxParams{ + Amount: amount, + }, + } + a := crosschainkeeper.GetAbortedAmount(cctx) + require.Equal(t, amount, a) + }) + t.Run("should return the amount outbound amount", func(t *testing.T) { + cctx := types.CrossChainTx{ + InboundTxParams: &types.InboundTxParams{ + Amount: sdkmath.ZeroUint(), + }, + OutboundTxParams: []*types.OutboundTxParams{ + {Amount: amount}, + }, + } + a := crosschainkeeper.GetAbortedAmount(cctx) + require.Equal(t, amount, a) + }) + t.Run("should return the zero if outbound amount is not present and inbound is 0", func(t *testing.T) { + cctx := types.CrossChainTx{ + InboundTxParams: &types.InboundTxParams{ + Amount: sdkmath.ZeroUint(), + }, + } + a := crosschainkeeper.GetAbortedAmount(cctx) + require.Equal(t, sdkmath.ZeroUint(), a) + }) + t.Run("should return the zero if no amounts are present", func(t *testing.T) { + cctx := types.CrossChainTx{} + a := crosschainkeeper.GetAbortedAmount(cctx) + require.Equal(t, sdkmath.ZeroUint(), a) + }) +} diff --git a/x/crosschain/keeper/msg_server_refund_aborted_tx.go b/x/crosschain/keeper/msg_server_refund_aborted_tx.go index 8cc85b6a19..0c6181aa71 100644 --- a/x/crosschain/keeper/msg_server_refund_aborted_tx.go +++ b/x/crosschain/keeper/msg_server_refund_aborted_tx.go @@ -1,6 +1,8 @@ package keeper import ( + "errors" + errorsmod "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" ethcommon "github.com/ethereum/go-ethereum/common" @@ -37,10 +39,16 @@ func (k msgServer) RefundAbortedCCTX(goCtx context.Context, msg *types.MsgRefund // Check if aborted amount is available to maintain zeta accounting // NOTE: Need to verify if this check works / is required in athens 3 if cctx.InboundTxParams.CoinType == common.CoinType_Zeta { - err := k.RemoveZetaAbortedAmount(ctx, cctx.InboundTxParams.Amount) - if err != nil { + err := k.RemoveZetaAbortedAmount(ctx, GetAbortedAmount(cctx)) + // if the zeta accounting is not found, it means the zeta accounting is not set yet and the refund should not be processed + if errors.Is(err, types.ErrUnableToFindZetaAccounting) { return nil, errorsmod.Wrap(types.ErrUnableProcessRefund, err.Error()) } + // if the zeta accounting is found but the amount is insufficient, it means the refund can be processed but the zeta accounting is not maintained properly + // aborted amounts for zeta accounting would need to be updated in the envionment via a migration script + if errors.Is(err, types.ErrInsufficientZetaAmount) { + ctx.Logger().Error("Zeta Accounting Error: ", err) + } } refundAddress, err := GetRefundAddress(cctx, msg.RefundAddress) @@ -64,17 +72,17 @@ func (k msgServer) RefundAbortedCCTX(goCtx context.Context, msg *types.MsgRefund return &types.MsgRefundAbortedCCTXResponse{}, nil } +// Set the proper refund address. +// For BTC sender chain the refund address is the one provided in the message in the RefundAddress field. +// For EVM chain with coin type ERC20 the refund address is the sender , but can be overridden by the RefundAddress field in the message. +// For EVM chain with coin type Zeta the refund address is the tx origin, but can be overridden by the RefundAddress field in the message. +// For EVM chain with coin type Gas the refund address is the tx origin, but can be overridden by the RefundAddress field in the message. + func GetRefundAddress(cctx types.CrossChainTx, optionalRefundAddress string) (ethcommon.Address, error) { // make sure a separate refund address is provided for a bitcoin chain as we cannot refund to tx origin or sender in this case if common.IsBitcoinChain(cctx.InboundTxParams.SenderChainId) && optionalRefundAddress == "" { return ethcommon.Address{}, errorsmod.Wrap(types.ErrInvalidAddress, "refund address is required for bitcoin chain") } - // Set the proper refund address. - // For BTC sender chain the refund address is the one provided in the message in the RefundAddress field. - // For EVM chain with coin type ERC20 the refund address is the sender , but can be overridden by the RefundAddress field in the message. - // For EVM chain with coin type Zeta the refund address is the tx origin, but can be overridden by the RefundAddress field in the message. - // For EVM chain with coin type Gas the refund address is the tx origin, but can be overridden by the RefundAddress field in the message. - refundAddress := ethcommon.HexToAddress(cctx.InboundTxParams.TxOrigin) if cctx.InboundTxParams.CoinType == common.CoinType_ERC20 { refundAddress = ethcommon.HexToAddress(cctx.InboundTxParams.Sender) diff --git a/x/crosschain/keeper/msg_server_refund_aborted_tx_test.go b/x/crosschain/keeper/msg_server_refund_aborted_tx_test.go index 4423a5f3b6..54d6304444 100644 --- a/x/crosschain/keeper/msg_server_refund_aborted_tx_test.go +++ b/x/crosschain/keeper/msg_server_refund_aborted_tx_test.go @@ -3,7 +3,6 @@ package keeper_test import ( "testing" - sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" ethcommon "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" @@ -14,6 +13,7 @@ import ( "github.com/zeta-chain/zetacore/x/crosschain/keeper" crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" fungibletypes "github.com/zeta-chain/zetacore/x/fungible/types" + observertypes "github.com/zeta-chain/zetacore/x/observer/types" ) func Test_GetRefundAddress(t *testing.T) { @@ -106,7 +106,7 @@ func Test_GetRefundAddress(t *testing.T) { } func TestMsgServer_RefundAbortedCCTX(t *testing.T) { - t.Run("Successfully refund tx for coin-type Gas", func(t *testing.T) { + t.Run("successfully refund tx for coin-type gas", func(t *testing.T) { k, ctx, sdkk, zk := keepertest.CrosschainKeeper(t) admin := sample.AccAddress() chainID := getValidEthChainID(t) @@ -134,12 +134,12 @@ func TestMsgServer_RefundAbortedCCTX(t *testing.T) { refundAddress := ethcommon.HexToAddress(cctx.InboundTxParams.TxOrigin) balance, err := zk.FungibleKeeper.BalanceOfZRC4(ctx, zrc20, refundAddress) require.NoError(t, err) - require.Equal(t, cctx.InboundTxParams.Amount.Uint64(), balance.Uint64()) + require.Equal(t, cctx.GetCurrentOutTxParam().Amount.Uint64(), balance.Uint64()) c, found := k.GetCrossChainTx(ctx, cctx.Index) require.True(t, found) require.True(t, c.CctxStatus.IsAbortRefunded) }) - t.Run("Successfully refund tx for coin-type Zeta", func(t *testing.T) { + t.Run("successfully refund tx for coin-type zeta", func(t *testing.T) { k, ctx, sdkk, zk := keepertest.CrosschainKeeper(t) admin := sample.AccAddress() chainID := getValidEthChainID(t) @@ -154,7 +154,41 @@ func TestMsgServer_RefundAbortedCCTX(t *testing.T) { cctx.InboundTxParams.SenderChainId = chainID cctx.InboundTxParams.CoinType = common.CoinType_Zeta k.SetCrossChainTx(ctx, *cctx) - k.SetZetaAccounting(ctx, crosschaintypes.ZetaAccounting{AbortedZetaAmount: cctx.InboundTxParams.Amount}) + k.SetZetaAccounting(ctx, crosschaintypes.ZetaAccounting{AbortedZetaAmount: cctx.GetCurrentOutTxParam().Amount}) + deploySystemContracts(t, ctx, zk.FungibleKeeper, sdkk.EvmKeeper) + + _, err := msgServer.RefundAbortedCCTX(ctx, &crosschaintypes.MsgRefundAbortedCCTX{ + Creator: admin, + CctxIndex: cctx.Index, + RefundAddress: "", + }) + require.NoError(t, err) + + refundAddress := ethcommon.HexToAddress(cctx.InboundTxParams.TxOrigin) + refundAddressCosmos := sdk.AccAddress(refundAddress.Bytes()) + balance := sdkk.BankKeeper.GetBalance(ctx, refundAddressCosmos, config.BaseDenom) + require.Equal(t, cctx.GetCurrentOutTxParam().Amount.Uint64(), balance.Amount.Uint64()) + c, found := k.GetCrossChainTx(ctx, cctx.Index) + require.True(t, found) + require.True(t, c.CctxStatus.IsAbortRefunded) + }) + t.Run("successfully refund tx to inbound amount if outbound is not found for coin-type zeta", func(t *testing.T) { + k, ctx, sdkk, zk := keepertest.CrosschainKeeper(t) + admin := sample.AccAddress() + chainID := getValidEthChainID(t) + setAdminPolicies(ctx, zk, admin) + msgServer := keeper.NewMsgServerImpl(*k) + k.GetAuthKeeper().GetModuleAccount(ctx, fungibletypes.ModuleName) + + cctx := sample.CrossChainTx(t, "sample-index") + cctx.CctxStatus.Status = crosschaintypes.CctxStatus_Aborted + cctx.CctxStatus.IsAbortRefunded = false + cctx.InboundTxParams.TxOrigin = cctx.InboundTxParams.Sender + cctx.InboundTxParams.SenderChainId = chainID + cctx.InboundTxParams.CoinType = common.CoinType_Zeta + cctx.OutboundTxParams = nil + k.SetCrossChainTx(ctx, *cctx) + k.SetZetaAccounting(ctx, crosschaintypes.ZetaAccounting{AbortedZetaAmount: cctx.GetCurrentOutTxParam().Amount}) deploySystemContracts(t, ctx, zk.FungibleKeeper, sdkk.EvmKeeper) _, err := msgServer.RefundAbortedCCTX(ctx, &crosschaintypes.MsgRefundAbortedCCTX{ @@ -172,7 +206,7 @@ func TestMsgServer_RefundAbortedCCTX(t *testing.T) { require.True(t, found) require.True(t, c.CctxStatus.IsAbortRefunded) }) - t.Run("Successfully refund to optional refund address if provided", func(t *testing.T) { + t.Run("successfully refund to optional refund address if provided", func(t *testing.T) { k, ctx, sdkk, zk := keepertest.CrosschainKeeper(t) admin := sample.AccAddress() chainID := getValidEthChainID(t) @@ -200,12 +234,12 @@ func TestMsgServer_RefundAbortedCCTX(t *testing.T) { refundAddressCosmos := sdk.AccAddress(refundAddress.Bytes()) balance := sdkk.BankKeeper.GetBalance(ctx, refundAddressCosmos, config.BaseDenom) - require.Equal(t, cctx.InboundTxParams.Amount.Uint64(), balance.Amount.Uint64()) + require.Equal(t, cctx.GetCurrentOutTxParam().Amount.Uint64(), balance.Amount.Uint64()) c, found := k.GetCrossChainTx(ctx, cctx.Index) require.True(t, found) require.True(t, c.CctxStatus.IsAbortRefunded) }) - t.Run("Successfully refund tx for coin-type ERC20", func(t *testing.T) { + t.Run("successfully refund tx for coin-type ERC20", func(t *testing.T) { k, ctx, sdkk, zk := keepertest.CrosschainKeeper(t) admin := sample.AccAddress() chainID := getValidEthChainID(t) @@ -244,12 +278,12 @@ func TestMsgServer_RefundAbortedCCTX(t *testing.T) { refundAddress := ethcommon.HexToAddress(cctx.InboundTxParams.Sender) balance, err := zk.FungibleKeeper.BalanceOfZRC4(ctx, zrc20Addr, refundAddress) require.NoError(t, err) - require.Equal(t, cctx.InboundTxParams.Amount.Uint64(), balance.Uint64()) + require.Equal(t, cctx.GetCurrentOutTxParam().Amount.Uint64(), balance.Uint64()) c, found := k.GetCrossChainTx(ctx, cctx.Index) require.True(t, found) require.True(t, c.CctxStatus.IsAbortRefunded) }) - t.Run("Successfully refund tx for coin-type Gas with BTC sender", func(t *testing.T) { + t.Run("successfully refund tx for coin-type Gas with BTC sender", func(t *testing.T) { k, ctx, sdkk, zk := keepertest.CrosschainKeeper(t) admin := sample.AccAddress() chainID := getValidBtcChainID() @@ -277,12 +311,12 @@ func TestMsgServer_RefundAbortedCCTX(t *testing.T) { refundAddress := ethcommon.HexToAddress(cctx.InboundTxParams.TxOrigin) balance, err := zk.FungibleKeeper.BalanceOfZRC4(ctx, zrc20, refundAddress) require.NoError(t, err) - require.Equal(t, cctx.InboundTxParams.Amount.Uint64(), balance.Uint64()) + require.Equal(t, cctx.GetCurrentOutTxParam().Amount.Uint64(), balance.Uint64()) c, found := k.GetCrossChainTx(ctx, cctx.Index) require.True(t, found) require.True(t, c.CctxStatus.IsAbortRefunded) }) - t.Run("Fail refund if address provided is invalid", func(t *testing.T) { + t.Run("fail refund if address provided is invalid", func(t *testing.T) { k, ctx, sdkk, zk := keepertest.CrosschainKeeper(t) admin := sample.AccAddress() chainID := getValidEthChainID(t) @@ -307,7 +341,7 @@ func TestMsgServer_RefundAbortedCCTX(t *testing.T) { }) require.ErrorContains(t, err, "invalid refund address") }) - t.Run("Fail refund if address provided is invalid 2 ", func(t *testing.T) { + t.Run("fail refund if address provided is null ", func(t *testing.T) { k, ctx, sdkk, zk := keepertest.CrosschainKeeper(t) admin := sample.AccAddress() chainID := getValidEthChainID(t) @@ -332,7 +366,7 @@ func TestMsgServer_RefundAbortedCCTX(t *testing.T) { }) require.ErrorContains(t, err, "invalid refund address") }) - t.Run("Fail refund if status is not aborted", func(t *testing.T) { + t.Run("fail refund if status is not aborted", func(t *testing.T) { k, ctx, sdkk, zk := keepertest.CrosschainKeeper(t) admin := sample.AccAddress() chainID := getValidEthChainID(t) @@ -359,7 +393,7 @@ func TestMsgServer_RefundAbortedCCTX(t *testing.T) { require.True(t, found) require.False(t, c.CctxStatus.IsAbortRefunded) }) - t.Run("Fail refund if status cctx not found", func(t *testing.T) { + t.Run("fail refund if status cctx not found", func(t *testing.T) { k, ctx, sdkk, zk := keepertest.CrosschainKeeper(t) admin := sample.AccAddress() chainID := getValidEthChainID(t) @@ -382,7 +416,7 @@ func TestMsgServer_RefundAbortedCCTX(t *testing.T) { }) require.ErrorContains(t, err, "cannot find cctx") }) - t.Run("Fail refund if refund address not provided for BTC chain", func(t *testing.T) { + t.Run("fail refund if refund address not provided for BTC chain", func(t *testing.T) { k, ctx, sdkk, zk := keepertest.CrosschainKeeper(t) admin := sample.AccAddress() chainID := getValidBtcChainID() @@ -407,7 +441,7 @@ func TestMsgServer_RefundAbortedCCTX(t *testing.T) { }) require.ErrorContains(t, err, "refund address is required for bitcoin chain") }) - t.Run("Fail refund tx for coin-type Zeta if zeta accounting object is not present", func(t *testing.T) { + t.Run("fail refund tx for coin-type Zeta if zeta accounting object is not present", func(t *testing.T) { k, ctx, sdkk, zk := keepertest.CrosschainKeeper(t) admin := sample.AccAddress() chainID := getValidEthChainID(t) @@ -431,7 +465,7 @@ func TestMsgServer_RefundAbortedCCTX(t *testing.T) { }) require.ErrorContains(t, err, "unable to find zeta accounting") }) - t.Run("Fail refund tx for coin-type Zeta if zeta accounting does not have enough aborted amount", func(t *testing.T) { + t.Run("fail refund if non admin account is the creator", func(t *testing.T) { k, ctx, sdkk, zk := keepertest.CrosschainKeeper(t) admin := sample.AccAddress() chainID := getValidEthChainID(t) @@ -444,16 +478,16 @@ func TestMsgServer_RefundAbortedCCTX(t *testing.T) { cctx.CctxStatus.IsAbortRefunded = false cctx.InboundTxParams.TxOrigin = cctx.InboundTxParams.Sender cctx.InboundTxParams.SenderChainId = chainID - cctx.InboundTxParams.CoinType = common.CoinType_Zeta + cctx.InboundTxParams.CoinType = common.CoinType_Gas k.SetCrossChainTx(ctx, *cctx) - k.SetZetaAccounting(ctx, crosschaintypes.ZetaAccounting{AbortedZetaAmount: sdkmath.OneUint()}) deploySystemContracts(t, ctx, zk.FungibleKeeper, sdkk.EvmKeeper) + _ = setupGasCoin(t, ctx, zk.FungibleKeeper, sdkk.EvmKeeper, cctx.InboundTxParams.SenderChainId, "foobar", "foobar") _, err := msgServer.RefundAbortedCCTX(ctx, &crosschaintypes.MsgRefundAbortedCCTX{ - Creator: admin, + Creator: sample.AccAddress(), CctxIndex: cctx.Index, RefundAddress: "", }) - require.ErrorContains(t, err, "insufficient zeta amount") + require.ErrorIs(t, err, observertypes.ErrNotAuthorized) }) } diff --git a/x/crosschain/keeper/refund.go b/x/crosschain/keeper/refund.go index ddb3028ef7..e3f059e385 100644 --- a/x/crosschain/keeper/refund.go +++ b/x/crosschain/keeper/refund.go @@ -28,11 +28,12 @@ func (k Keeper) RefundAbortedAmountOnZetaChain(ctx sdk.Context, cctx types.Cross // RefundAmountOnZetaChainGas refunds the amount of the cctx on ZetaChain in case of aborted cctx with cointype gas func (k Keeper) RefundAmountOnZetaChainGas(ctx sdk.Context, cctx types.CrossChainTx, refundAddress ethcommon.Address) error { // refund in gas token to refund address - if cctx.InboundTxParams.Amount.IsNil() || cctx.InboundTxParams.Amount.IsZero() { + // Refund the the amount was previously + refundAmount := GetAbortedAmount(cctx) + if refundAmount.IsNil() || refundAmount.IsZero() { return errors.New("no amount to refund") } chainID := cctx.InboundTxParams.SenderChainId - amountOfGasTokenLocked := cctx.InboundTxParams.Amount // get the zrc20 contract address fcSenderChain, found := k.fungibleKeeper.GetGasCoinForForeignCoin(ctx, chainID) if !found { @@ -43,7 +44,7 @@ func (k Keeper) RefundAmountOnZetaChainGas(ctx sdk.Context, cctx types.CrossChai return errorsmod.Wrapf(types.ErrForeignCoinNotFound, "zrc20 contract address not found for chain %d", chainID) } // deposit the amount to the tx origin instead of receiver as this is a refund - if _, err := k.fungibleKeeper.DepositZRC20(ctx, zrc20, refundAddress, amountOfGasTokenLocked.BigInt()); err != nil { + if _, err := k.fungibleKeeper.DepositZRC20(ctx, zrc20, refundAddress, refundAmount.BigInt()); err != nil { return errors.New("failed to refund zeta on ZetaChain" + err.Error()) } return nil @@ -52,6 +53,8 @@ func (k Keeper) RefundAmountOnZetaChainGas(ctx sdk.Context, cctx types.CrossChai // RefundAmountOnZetaChainGas refunds the amount of the cctx on ZetaChain in case of aborted cctx with cointype zeta func (k Keeper) RefundAmountOnZetaChainZeta(ctx sdk.Context, cctx types.CrossChainTx, refundAddress ethcommon.Address) error { // if coin type is Zeta, handle this as a deposit ZETA to zEVM. + refundAmount := GetAbortedAmount(cctx) + fmt.Println("RefundAmountOnZetaChainZeta: refundAmount: ", refundAmount) chainID := cctx.InboundTxParams.SenderChainId // check if chain is an EVM chain if !common.IsEVMChain(chainID) { @@ -61,7 +64,7 @@ func (k Keeper) RefundAmountOnZetaChainZeta(ctx sdk.Context, cctx types.CrossCha return errors.New("no amount to refund") } // deposit the amount to refund address - if err := k.fungibleKeeper.DepositCoinZeta(ctx, refundAddress, cctx.InboundTxParams.Amount.BigInt()); err != nil { + if err := k.fungibleKeeper.DepositCoinZeta(ctx, refundAddress, refundAmount.BigInt()); err != nil { return errors.New("failed to refund zeta on ZetaChain" + err.Error()) } return nil @@ -71,7 +74,7 @@ func (k Keeper) RefundAmountOnZetaChainZeta(ctx sdk.Context, cctx types.CrossCha // NOTE: GetCurrentOutTxParam should contain the last up to date cctx amount // Refund address should already be validated before calling this function func (k Keeper) RefundAmountOnZetaChainERC20(ctx sdk.Context, cctx types.CrossChainTx, refundAddress ethcommon.Address) error { - inputAmount := cctx.InboundTxParams.Amount + refundAmount := GetAbortedAmount(cctx) // preliminary checks if cctx.InboundTxParams.CoinType != common.CoinType_ERC20 { return errors.New("unsupported coin type for refund on ZetaChain") @@ -80,7 +83,7 @@ func (k Keeper) RefundAmountOnZetaChainERC20(ctx sdk.Context, cctx types.CrossCh return errors.New("only EVM chains are supported for refund on ZetaChain") } - if inputAmount.IsNil() || inputAmount.IsZero() { + if refundAmount.IsNil() || refundAmount.IsZero() { return errors.New("no amount to refund") } @@ -95,7 +98,7 @@ func (k Keeper) RefundAmountOnZetaChainERC20(ctx sdk.Context, cctx types.CrossCh } // deposit the amount to the sender - if _, err := k.fungibleKeeper.DepositZRC20(ctx, zrc20, refundAddress, inputAmount.BigInt()); err != nil { + if _, err := k.fungibleKeeper.DepositZRC20(ctx, zrc20, refundAddress, refundAmount.BigInt()); err != nil { return errors.New("failed to deposit zrc20 on ZetaChain" + err.Error()) } diff --git a/x/crosschain/keeper/refund_test.go b/x/crosschain/keeper/refund_test.go index 04fbda1359..e9ecfd346f 100644 --- a/x/crosschain/keeper/refund_test.go +++ b/x/crosschain/keeper/refund_test.go @@ -30,8 +30,12 @@ func TestKeeper_RefundAmountOnZetaChainGas(t *testing.T) { SenderChainId: chainID, Sender: sender.String(), TxOrigin: sender.String(), - Amount: math.NewUint(42), + Amount: math.NewUint(20), + }, + OutboundTxParams: []*types.OutboundTxParams{{ + Amount: math.NewUint(42), }}, + }, sender, ) require.NoError(t, err) @@ -39,6 +43,30 @@ func TestKeeper_RefundAmountOnZetaChainGas(t *testing.T) { require.NoError(t, err) require.Equal(t, uint64(42), balance.Uint64()) }) + t.Run("should refund inbound amount zrc20 gas on zeta chain", func(t *testing.T) { + k, ctx, sdkk, zk := keepertest.CrosschainKeeper(t) + k.GetAuthKeeper().GetModuleAccount(ctx, fungibletypes.ModuleName) + sender := sample.EthAddress() + chainID := getValidEthChainID(t) + deploySystemContracts(t, ctx, zk.FungibleKeeper, sdkk.EvmKeeper) + zrc20 := setupGasCoin(t, ctx, zk.FungibleKeeper, sdkk.EvmKeeper, chainID, "foobar", "foobar") + + err := k.RefundAmountOnZetaChainGas(ctx, types.CrossChainTx{ + InboundTxParams: &types.InboundTxParams{ + CoinType: common.CoinType_Gas, + SenderChainId: chainID, + Sender: sender.String(), + TxOrigin: sender.String(), + Amount: math.NewUint(20), + }, + }, + sender, + ) + require.NoError(t, err) + balance, err := zk.FungibleKeeper.BalanceOfZRC4(ctx, zrc20, sender) + require.NoError(t, err) + require.Equal(t, uint64(20), balance.Uint64()) + }) t.Run("failed refund zrc20 gas on zeta chain if gas coin not found", func(t *testing.T) { k, ctx, sdkk, zk := keepertest.CrosschainKeeper(t) k.GetAuthKeeper().GetModuleAccount(ctx, fungibletypes.ModuleName) @@ -51,8 +79,13 @@ func TestKeeper_RefundAmountOnZetaChainGas(t *testing.T) { SenderChainId: chainID, Sender: sender.String(), TxOrigin: sender.String(), - Amount: math.NewUint(42), + Amount: math.NewUint(20), + }, + OutboundTxParams: []*types.OutboundTxParams{{ + Amount: math.NewUint(42), }}, + }, + sender, ) require.ErrorContains(t, err, types.ErrForeignCoinNotFound.Error()) @@ -72,7 +105,11 @@ func TestKeeper_RefundAmountOnZetaChainGas(t *testing.T) { Sender: sender.String(), TxOrigin: sender.String(), Amount: math.ZeroUint(), + }, + OutboundTxParams: []*types.OutboundTxParams{{ + Amount: math.ZeroUint(), }}, + }, sender, ) require.ErrorContains(t, err, "no amount to refund") @@ -93,8 +130,12 @@ func TestKeeper_RefundAmountOnZetaChainZeta(t *testing.T) { SenderChainId: chainID, Sender: sender.String(), TxOrigin: sender.String(), - Amount: math.NewUint(42), + Amount: math.NewUint(20), + }, + OutboundTxParams: []*types.OutboundTxParams{{ + Amount: math.NewUint(42), }}, + }, sender, ) require.NoError(t, err) @@ -102,6 +143,27 @@ func TestKeeper_RefundAmountOnZetaChainZeta(t *testing.T) { fmt.Println(coin.Amount.String()) require.Equal(t, "42", coin.Amount.String()) }) + t.Run("should refund inbound amount on zeta chain if outbound is not present", func(t *testing.T) { + k, ctx, sdkk, _ := keepertest.CrosschainKeeper(t) + k.GetAuthKeeper().GetModuleAccount(ctx, fungibletypes.ModuleName) + sender := sample.EthAddress() + chainID := getValidEthChainID(t) + + err := k.RefundAmountOnZetaChainZeta(ctx, types.CrossChainTx{ + InboundTxParams: &types.InboundTxParams{ + CoinType: common.CoinType_Gas, + SenderChainId: chainID, + Sender: sender.String(), + TxOrigin: sender.String(), + Amount: math.NewUint(20), + }, + }, + sender, + ) + require.NoError(t, err) + coin := sdkk.BankKeeper.GetBalance(ctx, sdk.AccAddress(sender.Bytes()), config.BaseDenom) + require.Equal(t, "20", coin.Amount.String()) + }) t.Run("failed refund amount on zeta chain amount is 0", func(t *testing.T) { k, ctx, _, _ := keepertest.CrosschainKeeper(t) k.GetAuthKeeper().GetModuleAccount(ctx, fungibletypes.ModuleName) @@ -115,7 +177,11 @@ func TestKeeper_RefundAmountOnZetaChainZeta(t *testing.T) { Sender: sender.String(), TxOrigin: sender.String(), Amount: math.ZeroUint(), + }, + OutboundTxParams: []*types.OutboundTxParams{{ + Amount: math.ZeroUint(), }}, + }, sender, ) require.ErrorContains(t, err, "no amount to refund") @@ -150,7 +216,11 @@ func TestKeeper_RefundAmountOnZetaChainERC20(t *testing.T) { Sender: sender.String(), Asset: asset, Amount: math.NewUint(42), + }, + OutboundTxParams: []*types.OutboundTxParams{{ + Amount: math.NewUint(42), }}, + }, sender, ) require.NoError(t, err)