diff --git a/test/integration/uexecutor/gas_refund_test.go b/test/integration/uexecutor/gas_refund_test.go new file mode 100644 index 00000000..a5d238f2 --- /dev/null +++ b/test/integration/uexecutor/gas_refund_test.go @@ -0,0 +1,40 @@ +package integrationtest + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + utils "github.com/pushchain/push-chain-node/test/utils" + uexecutortypes "github.com/pushchain/push-chain-node/x/uexecutor/types" +) + +// TestInboundRevertGasNotRefunded proves an INBOUND_REVERT never refunds gas on +// settlement. A revert is protocol-initiated — the user was never charged a gas fee +// for it — so even when a GasFee budget is present (PRC20 reverts set one) and the +// observed gasFeeUsed is well below it, no refund must be attempted. +func TestInboundRevertGasNotRefunded(t *testing.T) { + chainApp, ctx, vals, utxId, ob, coreVals := setupOutboundVotingTest(t, 4) + + // Make the seeded outbound an INBOUND_REVERT carrying a gas budget with headroom + // that would otherwise trigger a refund (GasFee 1000, gasFeeUsed 100 below). + ob.TxType = uexecutortypes.TxType_INBOUND_REVERT + ob.GasFee = "1000" + ob.GasToken = "0x000000000000000000000000000000000000C0dE" + require.NoError(t, chainApp.UexecutorKeeper.UpdateOutbound(ctx, utxId, *ob)) + + // Settle it successfully with gasFeeUsed << GasFee. + for i := 0; i < 3; i++ { + valAddr, err := sdk.ValAddressFromBech32(coreVals[i].OperatorAddress) + require.NoError(t, err) + require.NoError(t, utils.ExecVoteOutbound( + t, ctx, chainApp, vals[i], sdk.AccAddress(valAddr).String(), utxId, ob, true, "", "100")) + } + + utx, found, err := chainApp.UexecutorKeeper.GetUniversalTx(ctx, utxId) + require.NoError(t, err) + require.True(t, found) + require.Nil(t, utx.OutboundTx[0].PcRefundExecution, + "INBOUND_REVERT must not attempt a gas refund — the user was never charged for it") +} diff --git a/x/uexecutor/keeper/outbound.go b/x/uexecutor/keeper/outbound.go index 6f8546a5..990997c2 100644 --- a/x/uexecutor/keeper/outbound.go +++ b/x/uexecutor/keeper/outbound.go @@ -176,6 +176,14 @@ func (k Keeper) handleSuccessfulOutbound(ctx sdk.Context, utxId string, outbound // It is called for both successful and failed outbounds — gas is consumed on the // external chain regardless of execution outcome. func (k Keeper) applyGasRefund(ctx sdk.Context, outbound *types.OutboundTx, obs *types.OutboundObservation) { + // INBOUND_REVERT is protocol-initiated: the user was never charged a gas fee for + // the revert, so its GasFee (when present) is only a relayer gas hint, not a + // user-paid budget. Refunding "excess" would hand the user funds they never paid, + // so never refund for a revert — regardless of PC20/PRC20 or whether GasFee is set. + if outbound.TxType == types.TxType_INBOUND_REVERT { + return + } + if obs.GasFeeUsed == "" || outbound.GasFee == "" || outbound.GasToken == "" { return }