diff --git a/x/crosschain/keeper/abci.go b/x/crosschain/keeper/abci.go index 85e52e7cc..e0f743379 100644 --- a/x/crosschain/keeper/abci.go +++ b/x/crosschain/keeper/abci.go @@ -14,6 +14,7 @@ func (k Keeper) EndBlocker(ctx sdk.Context) { signedWindow := k.GetSignedWindow(ctx) k.slashing(ctx, signedWindow) k.cleanupTimedOutBatches(ctx) + k.cleanupTimeOutRefund(ctx) k.createOracleSetRequest(ctx) k.pruneOracleSet(ctx, signedWindow) } @@ -146,6 +147,47 @@ func (k Keeper) cleanupTimedOutBatches(ctx sdk.Context) { }) } +func (k Keeper) cleanupTimeOutRefund(ctx sdk.Context) { + externalBlockHeight := k.GetLastObservedBlockHeight(ctx).ExternalBlockHeight + k.IterRefundRecord(ctx, func(record *types.RefundRecord) bool { + if record.Timeout > externalBlockHeight { + return true + } + receiver, coins, err := k.refundTokenToReceiver(ctx, record) + if err != nil { + k.Logger(ctx).Error("clean up refund timeout", "event nonce", record.EventNonce, "error", err.Error()) + return false + } + k.DeleteRefundRecord(ctx, record) + k.DeleteRefundConfirm(ctx, record.EventNonce) + k.RemoveEventSnapshotOracle(ctx, record.OracleSetNonce, record.EventNonce) + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent(types.EventTypeRefundTimeout, + sdk.NewAttribute(sdk.AttributeKeySender, record.Receiver), + sdk.NewAttribute(types.AttributeKeyRefundAddress, receiver.String()), + sdk.NewAttribute(types.AttributeKeyEventNonce, fmt.Sprint(record.EventNonce)), + sdk.NewAttribute(sdk.AttributeKeyAmount, coins.String()), + ), + }) + return false + }) +} + +func (k Keeper) refundTokenToReceiver(ctx sdk.Context, record *types.RefundRecord) (sdk.AccAddress, sdk.Coins, error) { + receiverAddr := types.ExternalAddressToAccAddress(k.moduleName, record.Receiver) + cacheCtx, commit := ctx.CacheContext() + coins, err := k.bridgeCallTransferToSender(cacheCtx, receiverAddr.Bytes(), record.Tokens) + if err != nil { + return nil, nil, err + } + if err = k.bridgeCallTransferToReceiver(cacheCtx, receiverAddr, receiverAddr.Bytes(), coins); err != nil { + return nil, nil, err + } + commit() + return receiverAddr, coins, nil +} + func (k Keeper) pruneOracleSet(ctx sdk.Context, signedOracleSetsWindow uint64) { // Oracle set pruning // prune all Oracle sets with a nonce less than the diff --git a/x/crosschain/keeper/abci_test.go b/x/crosschain/keeper/abci_test.go index d2ab77fc7..5c22fe688 100644 --- a/x/crosschain/keeper/abci_test.go +++ b/x/crosschain/keeper/abci_test.go @@ -3,13 +3,18 @@ package keeper_test import ( "encoding/hex" "fmt" + "math/big" sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" abci "github.com/tendermint/tendermint/abci/types" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "github.com/functionx/fx-core/v7/testutil/helpers" fxtypes "github.com/functionx/fx-core/v7/types" @@ -556,3 +561,116 @@ func (suite *KeeperTestSuite) TestSlashOracle() { require.Equal(suite.T(), int64(1), oracle.SlashTimes) } } + +func (suite *KeeperTestSuite) TestCleanUpRefundTimeout() { + normalMsg := &types.MsgBondedOracle{ + OracleAddress: suite.oracleAddrs[0].String(), + BridgerAddress: suite.bridgerAddrs[0].String(), + ExternalAddress: suite.PubKeyToExternalAddr(suite.externalPris[0].PublicKey), + ValidatorAddress: suite.valAddrs[0].String(), + DelegateAmount: types.NewDelegateAmount(sdkmath.NewInt(10 * 1e3).MulRaw(1e18)), + ChainName: suite.chainName, + } + _, err := suite.MsgServer().BondedOracle(sdk.WrapSDKContext(suite.ctx), normalMsg) + require.NoError(suite.T(), err) + + suite.Commit() + + bridgeToken := helpers.GenerateAddressByModule(suite.chainName) + addBridgeTokenClaim := &types.MsgBridgeTokenClaim{ + EventNonce: 1, + BlockHeight: 1000, + TokenContract: bridgeToken, + Name: "Test Token", + Symbol: "TEST", + Decimals: 18, + BridgerAddress: suite.bridgerAddrs[0].String(), + ChannelIbc: hex.EncodeToString([]byte("transfer/channel-0")), + ChainName: suite.chainName, + } + _, err = suite.MsgServer().BridgeTokenClaim(sdk.WrapSDKContext(suite.ctx), addBridgeTokenClaim) + require.NoError(suite.T(), err) + + denomResp, err := suite.QueryClient().TokenToDenom(sdk.WrapSDKContext(suite.ctx), &types.QueryTokenToDenomRequest{ + ChainName: suite.chainName, + Token: bridgeToken, + }) + suite.NoError(err) + + _, err = suite.app.Erc20Keeper.RegisterNativeCoin(suite.ctx, banktypes.Metadata{ + Description: "Function X cross chain token", + DenomUnits: []*banktypes.DenomUnit{ + { + Denom: "test", + Exponent: 0, + Aliases: []string{denomResp.Denom}, + }, + { + Denom: "TEST", + Exponent: 18, + Aliases: nil, + }, + }, + Base: "test", + Display: "TEST", + Name: "Test Token", + Symbol: "TEST", + }) + suite.NoError(err) + + suite.Commit() + + tokenAddr := common.BytesToAddress(types.ExternalAddressToAccAddress(suite.chainName, bridgeToken).Bytes()) + asset, err := types.PackERC20AssetWithType([]common.Address{tokenAddr}, []*big.Int{big.NewInt(1)}) + suite.NoError(err) + + bridgeCallClaim := &types.MsgBridgeCallClaim{ + DstChainId: types.FxcoreChainID, + EventNonce: 2, + Sender: helpers.GenerateAddressByModule(suite.chainName), + Receiver: helpers.GenerateAddressByModule(suite.chainName), + Asset: asset, + To: helpers.GenerateAddressByModule(suite.chainName), + Message: hex.EncodeToString([]byte{0x1}), + Value: sdkmath.NewInt(1), + GasLimit: 3000000, + BlockHeight: 1001, + BridgerAddress: suite.bridgerAddrs[0].String(), + ChainName: suite.chainName, + } + _, err = suite.MsgServer().BridgeCallClaim(sdk.WrapSDKContext(suite.ctx), bridgeCallClaim) + suite.NoError(err) + + recordExist := false + for _, event := range suite.ctx.EventManager().Events() { + if event.Type == types.EventTypeBridgeCallRefund { + recordExist = true + } + } + suite.True(recordExist) + refundRecord, err := suite.QueryClient().RefundRecordByNonce(sdk.WrapSDKContext(suite.ctx), &types.QueryRefundRecordByNonceRequest{ChainName: suite.chainName, EventNonce: 2}) + suite.NoError(err) + suite.Equal(uint64(2), refundRecord.Record.EventNonce) + + suite.Commit() + + sendToFxSendAddr := helpers.GenerateAddressByModule(suite.chainName) + sendToFxClaim := &types.MsgSendToFxClaim{ + EventNonce: 3, + BlockHeight: refundRecord.Record.Timeout + 1, + TokenContract: bridgeToken, + Amount: sdkmath.NewInt(1234), + Sender: sendToFxSendAddr, + Receiver: sdk.AccAddress(helpers.GenerateAddress().Bytes()).String(), + TargetIbc: hex.EncodeToString([]byte("px/transfer/channel-0")), + BridgerAddress: suite.bridgerAddrs[0].String(), + ChainName: suite.chainName, + } + _, err = suite.MsgServer().SendToFxClaim(sdk.WrapSDKContext(suite.ctx), sendToFxClaim) + require.NoError(suite.T(), err) + + suite.Commit() + + _, err = suite.QueryClient().RefundRecordByNonce(sdk.WrapSDKContext(suite.ctx), &types.QueryRefundRecordByNonceRequest{ChainName: suite.chainName, EventNonce: 2}) + suite.ErrorIs(err, status.Error(codes.NotFound, "refund record"), suite.chainName) +} diff --git a/x/crosschain/keeper/bridge_call_refund.go b/x/crosschain/keeper/bridge_call_refund.go index 827713630..6aec421cd 100644 --- a/x/crosschain/keeper/bridge_call_refund.go +++ b/x/crosschain/keeper/bridge_call_refund.go @@ -20,22 +20,7 @@ func (k Keeper) HandleRefundTokenClaim(ctx sdk.Context, claim *types.MsgRefundTo k.DeleteRefundConfirm(ctx, claim.RefundNonce) // 3. delete snapshot oracle event nonce or snapshot oracle - oracle, found := k.GetSnapshotOracle(ctx, record.OracleSetNonce) - if !found { - return - } - - for i, nonce := range oracle.EventNonces { - if nonce == claim.RefundNonce { - oracle.EventNonces = append(oracle.EventNonces[:i], oracle.EventNonces[i+1:]...) - break - } - } - if len(oracle.EventNonces) == 0 { - k.DeleteSnapshotOracle(ctx, record.OracleSetNonce) - } else { - k.SetSnapshotOracle(ctx, oracle) - } + k.RemoveEventSnapshotOracle(ctx, record.OracleSetNonce, claim.RefundNonce) } func (k Keeper) AddRefundRecord(ctx sdk.Context, receiver string, eventNonce uint64, tokens []types.ERC20Token) error { @@ -142,6 +127,25 @@ func (k Keeper) DeleteSnapshotOracle(ctx sdk.Context, nonce uint64) { store.Delete(types.GetSnapshotOracleKey(nonce)) } +func (k Keeper) RemoveEventSnapshotOracle(ctx sdk.Context, oracleNonce, eventNonce uint64) { + oracle, found := k.GetSnapshotOracle(ctx, oracleNonce) + if !found { + return + } + + for i, nonce := range oracle.EventNonces { + if nonce == eventNonce { + oracle.EventNonces = append(oracle.EventNonces[:i], oracle.EventNonces[i+1:]...) + break + } + } + if len(oracle.EventNonces) == 0 { + k.DeleteSnapshotOracle(ctx, oracleNonce) + } else { + k.SetSnapshotOracle(ctx, oracle) + } +} + func (k Keeper) GetRefundConfirm(ctx sdk.Context, nonce uint64, addr sdk.AccAddress) (*types.MsgConfirmRefund, bool) { store := ctx.KVStore(k.storeKey) bz := store.Get(types.GetRefundConfirmKey(nonce, addr)) diff --git a/x/crosschain/keeper/keeper_test.go b/x/crosschain/keeper/keeper_test.go index 47a734b58..e936af398 100644 --- a/x/crosschain/keeper/keeper_test.go +++ b/x/crosschain/keeper/keeper_test.go @@ -9,6 +9,7 @@ import ( "testing" sdkmath "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/crypto" tronaddress "github.com/fbsobreira/gotron-sdk/pkg/address" @@ -89,6 +90,16 @@ func (suite *KeeperTestSuite) MsgServer() types.MsgServer { return keeper.NewMsgServerImpl(suite.Keeper()) } +func (suite *KeeperTestSuite) QueryClient() types.QueryClient { + queryHelper := baseapp.NewQueryServerTestHelper(suite.ctx, suite.app.InterfaceRegistry()) + if suite.chainName == trontypes.ModuleName { + types.RegisterQueryServer(queryHelper, suite.app.TronKeeper) + return types.NewQueryClient(queryHelper) + } + types.RegisterQueryServer(queryHelper, suite.Keeper()) + return types.NewQueryClient(queryHelper) +} + func (suite *KeeperTestSuite) Keeper() keeper.Keeper { switch suite.chainName { case bsctypes.ModuleName: diff --git a/x/crosschain/types/events.go b/x/crosschain/types/events.go index 3d90e9427..76d26e470 100644 --- a/x/crosschain/types/events.go +++ b/x/crosschain/types/events.go @@ -39,4 +39,6 @@ const ( EventTypeBridgeCallRefund = "bridge_call_refund" AttributeKeyRefundAddress = "refund_address" + + EventTypeRefundTimeout = "refund_timeout" )